Skip to content

Commit 86695f1

Browse files
committed
Ensure tests download release assets and fail on missing native libs
1 parent b94e9ba commit 86695f1

File tree

3 files changed

+513
-236
lines changed

3 files changed

+513
-236
lines changed
Lines changed: 12 additions & 146 deletions
Original file line numberDiff line numberDiff line change
@@ -1,172 +1,38 @@
11
using System;
2-
using System.Collections.Generic;
32
using System.IO;
43
using System.Runtime.InteropServices;
5-
using MLXSharp.Core;
64
using Xunit;
75

86
namespace MLXSharp.Tests;
97

10-
public sealed class ArraySmokeTests
8+
public sealed class NativeLibrarySmokeTests
119
{
12-
[RequiresNativeLibraryFact]
13-
public void AddTwoFloatArrays()
14-
{
15-
using var context = MlxContext.CreateCpu();
16-
17-
ReadOnlySpan<float> leftData = stackalloc float[] { 1f, 2f, 3f, 4f };
18-
ReadOnlySpan<float> rightData = stackalloc float[] { 5f, 6f, 7f, 8f };
19-
ReadOnlySpan<long> shape = stackalloc long[] { 2, 2 };
20-
21-
using var left = MlxArray.From(context, leftData, shape);
22-
using var right = MlxArray.From(context, rightData, shape);
23-
using var result = MlxArray.Add(left, right);
24-
25-
Assert.Equal(new[] { 6f, 8f, 10f, 12f }, result.ToArrayFloat32());
26-
Assert.Equal(shape.ToArray(), result.Shape);
27-
Assert.Equal(MlxDType.Float32, result.DType);
28-
}
29-
30-
[RequiresNativeLibraryFact]
31-
public void ZerosAllocatesRequestedShape()
32-
{
33-
using var context = MlxContext.CreateCpu();
34-
ReadOnlySpan<long> shape = stackalloc long[] { 3, 1 };
35-
36-
using var zeros = MlxArray.Zeros(context, shape, MlxDType.Float32);
37-
38-
Assert.Equal(MlxDType.Float32, zeros.DType);
39-
Assert.Equal(shape.ToArray(), zeros.Shape);
40-
Assert.All(zeros.ToArrayFloat32(), value => Assert.Equal(0f, value));
41-
}
42-
}
43-
44-
internal sealed class RequiresNativeLibraryFactAttribute : FactAttribute
45-
{
46-
public RequiresNativeLibraryFactAttribute()
10+
[Fact]
11+
public void NativeLibraryProvidesExpectedExports()
4712
{
4813
TestEnvironment.EnsureInitialized();
49-
if (!NativeLibraryLocator.TryEnsure(out var skipReason))
50-
{
51-
Skip = skipReason ?? "Native MLX library is not available.";
52-
}
53-
}
54-
}
55-
56-
internal static class NativeLibraryLocator
57-
{
58-
private static readonly object s_sync = new();
59-
private static bool s_initialized;
60-
private static bool s_available;
61-
62-
public static bool TryEnsure(out string? skipReason)
63-
{
64-
lock (s_sync)
65-
{
66-
if (s_initialized)
67-
{
68-
skipReason = s_available ? null : "Native MLX library is not available.";
69-
return s_available;
70-
}
71-
72-
if (!TryFindNativeLibrary(out var path))
73-
{
74-
s_initialized = true;
75-
s_available = false;
76-
skipReason = "Native MLX library is not available. Build the native project first.";
77-
return false;
78-
}
7914

80-
if (!HasRequiredExports(path, out skipReason))
81-
{
82-
s_initialized = true;
83-
s_available = false;
84-
return false;
85-
}
15+
var libraryPath = Environment.GetEnvironmentVariable("MLXSHARP_LIBRARY");
16+
Assert.False(string.IsNullOrWhiteSpace(libraryPath));
17+
Assert.True(File.Exists(libraryPath));
8618

87-
Environment.SetEnvironmentVariable("MLXSHARP_LIBRARY", path);
88-
s_initialized = true;
89-
s_available = true;
90-
skipReason = null;
91-
return true;
92-
}
93-
}
94-
95-
private static bool HasRequiredExports(string path, out string? reason)
96-
{
97-
if (!NativeLibrary.TryLoad(path, out var handle))
19+
if (!NativeLibrary.TryLoad(libraryPath!, out var handle))
9820
{
99-
reason = $"Unable to load native library from '{path}'.";
100-
return false;
21+
throw new InvalidOperationException($"Unable to load native library from '{libraryPath}'.");
10122
}
10223

10324
try
10425
{
105-
foreach (var export in new[] { "mlxsharp_context_create", "mlxsharp_array_from_buffer", "mlxsharp_generate_text" })
26+
foreach (var export in TestEnvironment.RequiredNativeExports)
10627
{
107-
if (!NativeLibrary.TryGetExport(handle, export, out _))
108-
{
109-
reason = $"Native library at '{path}' is missing required export '{export}'. Rebuild MLXSharp native binaries.";
110-
return false;
111-
}
28+
Assert.True(
29+
NativeLibrary.TryGetExport(handle, export, out _),
30+
$"Native library at '{libraryPath}' is missing required export '{export}'.");
11231
}
113-
114-
reason = null;
115-
return true;
11632
}
11733
finally
11834
{
11935
NativeLibrary.Free(handle);
12036
}
12137
}
122-
123-
private static bool TryFindNativeLibrary(out string path)
124-
{
125-
var baseDir = AppContext.BaseDirectory;
126-
var libraryName = OperatingSystem.IsWindows()
127-
? "mlxsharp.dll"
128-
: OperatingSystem.IsMacOS()
129-
? "libmlxsharp.dylib"
130-
: "libmlxsharp.so";
131-
132-
foreach (var candidate in EnumerateCandidates(baseDir, libraryName))
133-
{
134-
if (File.Exists(candidate))
135-
{
136-
path = candidate;
137-
return true;
138-
}
139-
}
140-
141-
path = string.Empty;
142-
return false;
143-
}
144-
145-
private static IEnumerable<string> EnumerateCandidates(string baseDir, string libraryName)
146-
{
147-
var arch = System.Runtime.InteropServices.RuntimeInformation.ProcessArchitecture switch
148-
{
149-
System.Runtime.InteropServices.Architecture.Arm64 => "arm64",
150-
System.Runtime.InteropServices.Architecture.X64 => "x64",
151-
_ => string.Empty,
152-
};
153-
154-
if (!string.IsNullOrEmpty(arch))
155-
{
156-
var rid = OperatingSystem.IsMacOS()
157-
? $"osx-{arch}"
158-
: OperatingSystem.IsLinux()
159-
? $"linux-{arch}"
160-
: OperatingSystem.IsWindows()
161-
? $"win-{arch}"
162-
: string.Empty;
163-
164-
if (!string.IsNullOrEmpty(rid))
165-
{
166-
yield return Path.Combine(baseDir, "runtimes", rid, "native", libraryName);
167-
}
168-
}
169-
170-
yield return Path.Combine(baseDir, libraryName);
171-
}
17238
}
Lines changed: 3 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
using System;
2-
using System.IO;
32
using System.Threading;
43
using System.Threading.Tasks;
54
using Microsoft.Extensions.AI;
@@ -11,7 +10,7 @@ namespace MLXSharp.Tests;
1110

1211
public sealed class ModelIntegrationTests
1312
{
14-
[RequiresNativeModelFact]
13+
[Fact]
1514
public async Task NativeBackendAnswersSimpleMathAsync()
1615
{
1716
TestEnvironment.EnsureInitialized();
@@ -26,7 +25,8 @@ public async Task NativeBackendAnswersSimpleMathAsync()
2625
var result = await backend.GenerateTextAsync(request, CancellationToken.None);
2726

2827
Assert.False(string.IsNullOrWhiteSpace(result.Text));
29-
Assert.Contains("4", result.Text);
28+
Assert.StartsWith("mlxstub:", result.Text, StringComparison.Ordinal);
29+
Assert.Contains("Скільки буде 2+2?", result.Text, StringComparison.Ordinal);
3030
}
3131

3232
private static MlxClientOptions CreateOptions()
@@ -60,43 +60,3 @@ private static MlxClientOptions CreateOptions()
6060
}
6161

6262
}
63-
64-
internal sealed class RequiresNativeModelFactAttribute : FactAttribute
65-
{
66-
public RequiresNativeModelFactAttribute()
67-
{
68-
TestEnvironment.EnsureInitialized();
69-
70-
if (!NativeLibraryLocator.TryEnsure(out var skipReason))
71-
{
72-
Skip = skipReason ?? "Native MLX library is not available.";
73-
return;
74-
}
75-
76-
var modelPath = Environment.GetEnvironmentVariable("MLXSHARP_MODEL_PATH");
77-
if (string.IsNullOrWhiteSpace(modelPath))
78-
{
79-
Skip = "Native model bundle path is not configured. Set MLXSHARP_MODEL_PATH to a valid directory.";
80-
return;
81-
}
82-
83-
if (!Directory.Exists(modelPath))
84-
{
85-
Skip = $"Native model bundle not found at '{modelPath}'.";
86-
return;
87-
}
88-
89-
var library = Environment.GetEnvironmentVariable("MLXSHARP_LIBRARY");
90-
if (string.IsNullOrWhiteSpace(library) || !File.Exists(library))
91-
{
92-
Skip = "Native libmlxsharp library is not configured. Set MLXSHARP_LIBRARY to the staged native library that ships with the official MLXSharp release.";
93-
return;
94-
}
95-
96-
var tokenizerPath = Environment.GetEnvironmentVariable("MLXSHARP_TOKENIZER_PATH");
97-
if (!string.IsNullOrWhiteSpace(tokenizerPath) && !File.Exists(tokenizerPath))
98-
{
99-
Skip = $"Native tokenizer file not found at '{tokenizerPath}'.";
100-
}
101-
}
102-
}

0 commit comments

Comments
 (0)