test(sdk): add comprehensive unit tests for all SDKs
Adds unit tests covering tensor operations, type enums, client functionality, and serialization for all 12 SDK implementations: - JavaScript (Vitest): tensor, types, client tests - Python (pytest): tensor, types, client tests - Go: standard library tests with httptest - Flutter (flutter_test): tensor, types tests - Java (JUnit 5): tensor, types tests - Rust: embedded module tests - Ruby (minitest): tensor, types tests - C# (xUnit): tensor, types tests Tests cover: - Tensor creation (zeros, ones, random, randn, eye, arange, linspace) - Tensor operations (reshape, transpose, indexing) - Reductions (sum, mean, std, min, max) - Activations (relu, sigmoid, softmax) - Serialization/deserialization - Type enums and configuration - Client request building - Error handling
This commit is contained in:
parent
3aff77a799
commit
e2a3b66123
19 changed files with 4228 additions and 0 deletions
23
sdk/csharp/SynorCompute.Tests/SynorCompute.Tests.csproj
Normal file
23
sdk/csharp/SynorCompute.Tests/SynorCompute.Tests.csproj
Normal file
|
|
@ -0,0 +1,23 @@
|
|||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.8.0" />
|
||||
<PackageReference Include="xunit" Version="2.6.2" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.5.4">
|
||||
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
|
||||
<PrivateAssets>all</PrivateAssets>
|
||||
</PackageReference>
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SynorCompute\SynorCompute.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
198
sdk/csharp/SynorCompute.Tests/TensorTests.cs
Normal file
198
sdk/csharp/SynorCompute.Tests/TensorTests.cs
Normal file
|
|
@ -0,0 +1,198 @@
|
|||
using Xunit;
|
||||
using SynorCompute;
|
||||
|
||||
namespace SynorCompute.Tests;
|
||||
|
||||
public class TensorTests
|
||||
{
|
||||
[Fact]
|
||||
public void Constructor_WithShapeAndData_CreatesTensor()
|
||||
{
|
||||
var tensor = new Tensor([2, 3], [1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
|
||||
|
||||
Assert.Equal([2, 3], tensor.Shape);
|
||||
Assert.Equal(6, tensor.Size);
|
||||
Assert.Equal(2, tensor.Ndim);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Zeros_CreatesZeroTensor()
|
||||
{
|
||||
var tensor = Tensor.Zeros([3, 4]);
|
||||
|
||||
Assert.Equal([3, 4], tensor.Shape);
|
||||
Assert.Equal(12, tensor.Size);
|
||||
Assert.All(tensor.Data, v => Assert.Equal(0.0, v));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Ones_CreatesOnesTensor()
|
||||
{
|
||||
var tensor = Tensor.Ones([2, 2]);
|
||||
|
||||
Assert.All(tensor.Data, v => Assert.Equal(1.0, v));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Random_CreatesRandomTensor()
|
||||
{
|
||||
var tensor = Tensor.Random([10, 10]);
|
||||
|
||||
Assert.Equal([10, 10], tensor.Shape);
|
||||
Assert.All(tensor.Data, v => Assert.InRange(v, 0.0, 1.0));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Randn_CreatesNormalDistributionTensor()
|
||||
{
|
||||
var tensor = Tensor.Randn([1000]);
|
||||
|
||||
var mean = tensor.Mean();
|
||||
var std = tensor.Std();
|
||||
|
||||
Assert.InRange(Math.Abs(mean), 0, 0.2);
|
||||
Assert.InRange(std, 0.8, 1.2);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Eye_CreatesIdentityMatrix()
|
||||
{
|
||||
var tensor = Tensor.Eye(3);
|
||||
|
||||
Assert.Equal([3, 3], tensor.Shape);
|
||||
Assert.Equal(1.0, tensor[0, 0]);
|
||||
Assert.Equal(1.0, tensor[1, 1]);
|
||||
Assert.Equal(1.0, tensor[2, 2]);
|
||||
Assert.Equal(0.0, tensor[0, 1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Arange_CreatesRangeTensor()
|
||||
{
|
||||
var tensor = Tensor.Arange(0, 5, 1);
|
||||
|
||||
Assert.Equal([5], tensor.Shape);
|
||||
Assert.Equal(0.0, tensor[0]);
|
||||
Assert.Equal(4.0, tensor[4]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Linspace_CreatesLinearlySpacedTensor()
|
||||
{
|
||||
var tensor = Tensor.Linspace(0, 10, 11);
|
||||
|
||||
Assert.Equal([11], tensor.Shape);
|
||||
Assert.Equal(0.0, tensor[0], 4);
|
||||
Assert.Equal(10.0, tensor[10], 4);
|
||||
Assert.Equal(5.0, tensor[5], 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reshape_ChangesShape()
|
||||
{
|
||||
var tensor = new Tensor([6], [1, 2, 3, 4, 5, 6]);
|
||||
var reshaped = tensor.Reshape([2, 3]);
|
||||
|
||||
Assert.Equal([2, 3], reshaped.Shape);
|
||||
Assert.Equal(6, reshaped.Size);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Reshape_ThrowsOnInvalidShape()
|
||||
{
|
||||
var tensor = Tensor.Zeros([4]);
|
||||
|
||||
Assert.Throws<ArgumentException>(() => tensor.Reshape([2, 3]));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Transpose_Transposes2DTensor()
|
||||
{
|
||||
var tensor = new Tensor([2, 3], [1, 2, 3, 4, 5, 6]);
|
||||
var transposed = tensor.Transpose();
|
||||
|
||||
Assert.Equal([3, 2], transposed.Shape);
|
||||
Assert.Equal(1.0, transposed[0, 0]);
|
||||
Assert.Equal(4.0, transposed[0, 1]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sum_CalculatesSum()
|
||||
{
|
||||
var tensor = new Tensor([4], [1, 2, 3, 4]);
|
||||
|
||||
Assert.Equal(10.0, tensor.Sum());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Mean_CalculatesMean()
|
||||
{
|
||||
var tensor = new Tensor([4], [1, 2, 3, 4]);
|
||||
|
||||
Assert.Equal(2.5, tensor.Mean());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Std_CalculatesStandardDeviation()
|
||||
{
|
||||
var tensor = new Tensor([4], [1, 2, 3, 4]);
|
||||
|
||||
Assert.Equal(1.118, tensor.Std(), 3);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Min_FindsMinimum()
|
||||
{
|
||||
var tensor = new Tensor([4], [3, 1, 4, 2]);
|
||||
|
||||
Assert.Equal(1.0, tensor.Min());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Max_FindsMaximum()
|
||||
{
|
||||
var tensor = new Tensor([4], [3, 1, 4, 2]);
|
||||
|
||||
Assert.Equal(4.0, tensor.Max());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ReLU_AppliesActivation()
|
||||
{
|
||||
var tensor = new Tensor([5], [-2, -1, 0, 1, 2]);
|
||||
var result = tensor.ReLU();
|
||||
|
||||
Assert.Equal([0.0, 0.0, 0.0, 1.0, 2.0], result.Data);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Sigmoid_AppliesActivation()
|
||||
{
|
||||
var tensor = new Tensor([1], [0]);
|
||||
var result = tensor.Sigmoid();
|
||||
|
||||
Assert.Equal(0.5, result[0], 4);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Softmax_AppliesActivation()
|
||||
{
|
||||
var tensor = new Tensor([3], [1, 2, 3]);
|
||||
var result = tensor.Softmax();
|
||||
|
||||
Assert.Equal(1.0, result.Sum(), 4);
|
||||
Assert.True(result[2] > result[1]);
|
||||
Assert.True(result[1] > result[0]);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Serialization_RoundTrip()
|
||||
{
|
||||
var original = new Tensor([2, 3], [1, 2, 3, 4, 5, 6]);
|
||||
var json = original.ToJson();
|
||||
var restored = Tensor.FromJson(json);
|
||||
|
||||
Assert.Equal(original.Shape, restored.Shape);
|
||||
Assert.Equal(original.Data, restored.Data);
|
||||
}
|
||||
}
|
||||
192
sdk/csharp/SynorCompute.Tests/TypesTests.cs
Normal file
192
sdk/csharp/SynorCompute.Tests/TypesTests.cs
Normal file
|
|
@ -0,0 +1,192 @@
|
|||
using Xunit;
|
||||
using SynorCompute;
|
||||
|
||||
namespace SynorCompute.Tests;
|
||||
|
||||
public class TypesTests
|
||||
{
|
||||
[Fact]
|
||||
public void ProcessorType_HasAllValues()
|
||||
{
|
||||
Assert.Equal("cpu", ProcessorType.Cpu.ToValue());
|
||||
Assert.Equal("gpu", ProcessorType.Gpu.ToValue());
|
||||
Assert.Equal("tpu", ProcessorType.Tpu.ToValue());
|
||||
Assert.Equal("npu", ProcessorType.Npu.ToValue());
|
||||
Assert.Equal("lpu", ProcessorType.Lpu.ToValue());
|
||||
Assert.Equal("auto", ProcessorType.Auto.ToValue());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ProcessorType_ParsesFromString()
|
||||
{
|
||||
Assert.Equal(ProcessorType.Gpu, ProcessorTypeExtensions.FromString("gpu"));
|
||||
Assert.Equal(ProcessorType.Tpu, ProcessorTypeExtensions.FromString("tpu"));
|
||||
Assert.Equal(ProcessorType.Auto, ProcessorTypeExtensions.FromString("invalid"));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Precision_HasAllValues()
|
||||
{
|
||||
Assert.Equal("fp64", Precision.FP64.ToValue());
|
||||
Assert.Equal("fp32", Precision.FP32.ToValue());
|
||||
Assert.Equal("fp16", Precision.FP16.ToValue());
|
||||
Assert.Equal("bf16", Precision.BF16.ToValue());
|
||||
Assert.Equal("int8", Precision.INT8.ToValue());
|
||||
Assert.Equal("int4", Precision.INT4.ToValue());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Priority_HasAllValues()
|
||||
{
|
||||
Assert.Equal("critical", Priority.Critical.ToValue());
|
||||
Assert.Equal("high", Priority.High.ToValue());
|
||||
Assert.Equal("normal", Priority.Normal.ToValue());
|
||||
Assert.Equal("low", Priority.Low.ToValue());
|
||||
Assert.Equal("background", Priority.Background.ToValue());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JobStatus_HasAllValues()
|
||||
{
|
||||
Assert.Equal("pending", JobStatus.Pending.ToValue());
|
||||
Assert.Equal("queued", JobStatus.Queued.ToValue());
|
||||
Assert.Equal("running", JobStatus.Running.ToValue());
|
||||
Assert.Equal("completed", JobStatus.Completed.ToValue());
|
||||
Assert.Equal("failed", JobStatus.Failed.ToValue());
|
||||
Assert.Equal("cancelled", JobStatus.Cancelled.ToValue());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JobStatus_IdentifiesTerminalStates()
|
||||
{
|
||||
Assert.True(JobStatus.Completed.IsTerminal());
|
||||
Assert.True(JobStatus.Failed.IsTerminal());
|
||||
Assert.True(JobStatus.Cancelled.IsTerminal());
|
||||
Assert.False(JobStatus.Pending.IsTerminal());
|
||||
Assert.False(JobStatus.Running.IsTerminal());
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SynorConfig_CreatesWithDefaults()
|
||||
{
|
||||
var config = new SynorConfig("test-key");
|
||||
|
||||
Assert.Equal("test-key", config.ApiKey);
|
||||
Assert.Equal("https://api.synor.io/compute/v1", config.BaseUrl);
|
||||
Assert.Equal(TimeSpan.FromSeconds(30), config.Timeout);
|
||||
Assert.Equal(ProcessorType.Auto, config.DefaultProcessor);
|
||||
Assert.Equal(Precision.FP32, config.DefaultPrecision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void SynorConfig_CreatesWithCustomValues()
|
||||
{
|
||||
var config = new SynorConfig("test-key")
|
||||
{
|
||||
BaseUrl = "https://custom.api.com",
|
||||
Timeout = TimeSpan.FromSeconds(60),
|
||||
DefaultProcessor = ProcessorType.Gpu,
|
||||
DefaultPrecision = Precision.FP16
|
||||
};
|
||||
|
||||
Assert.Equal("https://custom.api.com", config.BaseUrl);
|
||||
Assert.Equal(TimeSpan.FromSeconds(60), config.Timeout);
|
||||
Assert.Equal(ProcessorType.Gpu, config.DefaultProcessor);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void MatMulOptions_HasCorrectDefaults()
|
||||
{
|
||||
var options = new MatMulOptions();
|
||||
|
||||
Assert.Equal(Precision.FP32, options.Precision);
|
||||
Assert.Equal(ProcessorType.Auto, options.Processor);
|
||||
Assert.Equal(Priority.Normal, options.Priority);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void Conv2dOptions_HasCorrectDefaults()
|
||||
{
|
||||
var options = new Conv2dOptions();
|
||||
|
||||
Assert.Equal((1, 1), options.Stride);
|
||||
Assert.Equal((0, 0), options.Padding);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void AttentionOptions_HasCorrectDefaults()
|
||||
{
|
||||
var options = new AttentionOptions();
|
||||
|
||||
Assert.Equal(8, options.NumHeads);
|
||||
Assert.True(options.Flash);
|
||||
Assert.Equal(Precision.FP16, options.Precision);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void InferenceOptions_HasCorrectDefaults()
|
||||
{
|
||||
var options = new InferenceOptions();
|
||||
|
||||
Assert.Equal(256, options.MaxTokens);
|
||||
Assert.Equal(0.7, options.Temperature, 3);
|
||||
Assert.Equal(0.9, options.TopP, 3);
|
||||
Assert.Equal(50, options.TopK);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JobResult_IdentifiesSuccess()
|
||||
{
|
||||
var result = new JobResult<string>
|
||||
{
|
||||
JobId = "job-123",
|
||||
Status = JobStatus.Completed,
|
||||
Result = "output"
|
||||
};
|
||||
|
||||
Assert.True(result.IsSuccess);
|
||||
Assert.False(result.IsFailed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void JobResult_IdentifiesFailure()
|
||||
{
|
||||
var result = new JobResult<string>
|
||||
{
|
||||
JobId = "job-456",
|
||||
Status = JobStatus.Failed,
|
||||
Error = "Error message"
|
||||
};
|
||||
|
||||
Assert.False(result.IsSuccess);
|
||||
Assert.True(result.IsFailed);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModelInfo_FormatsParametersCorrectly()
|
||||
{
|
||||
var model = new ModelInfo
|
||||
{
|
||||
Id = "test",
|
||||
Name = "Test Model",
|
||||
Category = "llm",
|
||||
Parameters = 70_000_000_000
|
||||
};
|
||||
|
||||
Assert.Equal("70B", model.FormattedParameters);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public void ModelInfo_FormatsParametersInMillions()
|
||||
{
|
||||
var model = new ModelInfo
|
||||
{
|
||||
Id = "test",
|
||||
Name = "Test Model",
|
||||
Category = "embedding",
|
||||
Parameters = 350_000_000
|
||||
};
|
||||
|
||||
Assert.Equal("350M", model.FormattedParameters);
|
||||
}
|
||||
}
|
||||
400
sdk/flutter/test/tensor_test.dart
Normal file
400
sdk/flutter/test/tensor_test.dart
Normal file
|
|
@ -0,0 +1,400 @@
|
|||
import 'dart:convert';
|
||||
import 'dart:typed_data';
|
||||
|
||||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:synor_compute/synor_compute.dart';
|
||||
|
||||
void main() {
|
||||
group('Tensor Creation', () {
|
||||
test('creates tensor with shape and data', () {
|
||||
final tensor = Tensor(
|
||||
shape: [2, 3],
|
||||
data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
|
||||
);
|
||||
|
||||
expect(tensor.shape, equals([2, 3]));
|
||||
expect(tensor.size, equals(6));
|
||||
expect(tensor.ndim, equals(2));
|
||||
});
|
||||
|
||||
test('throws on data-shape mismatch', () {
|
||||
expect(
|
||||
() => Tensor(
|
||||
shape: [2, 3],
|
||||
data: [1.0, 2.0, 3.0], // Only 3 elements for shape [2, 3]
|
||||
),
|
||||
throwsA(isA<ArgumentError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('creates tensor from typed data', () {
|
||||
final data = Float64List.fromList([1.0, 2.0, 3.0, 4.0]);
|
||||
final tensor = Tensor.fromTypedData(
|
||||
shape: [2, 2],
|
||||
data: data,
|
||||
);
|
||||
|
||||
expect(tensor.shape, equals([2, 2]));
|
||||
expect(tensor.size, equals(4));
|
||||
});
|
||||
});
|
||||
|
||||
group('Tensor Factory Methods', () {
|
||||
test('creates zeros tensor', () {
|
||||
final tensor = Tensor.zeros([3, 4]);
|
||||
|
||||
expect(tensor.shape, equals([3, 4]));
|
||||
expect(tensor.size, equals(12));
|
||||
expect(tensor.data.every((v) => v == 0.0), isTrue);
|
||||
});
|
||||
|
||||
test('creates ones tensor', () {
|
||||
final tensor = Tensor.ones([2, 2]);
|
||||
|
||||
expect(tensor.shape, equals([2, 2]));
|
||||
expect(tensor.data.every((v) => v == 1.0), isTrue);
|
||||
});
|
||||
|
||||
test('creates full tensor with value', () {
|
||||
final tensor = Tensor.full([3, 3], 5.0);
|
||||
|
||||
expect(tensor.data.every((v) => v == 5.0), isTrue);
|
||||
});
|
||||
|
||||
test('creates random tensor', () {
|
||||
final tensor = Tensor.rand([10, 10]);
|
||||
|
||||
expect(tensor.shape, equals([10, 10]));
|
||||
expect(tensor.size, equals(100));
|
||||
// Values should be in [0, 1)
|
||||
expect(tensor.data.every((v) => v >= 0 && v < 1), isTrue);
|
||||
});
|
||||
|
||||
test('creates randn tensor with normal distribution', () {
|
||||
final tensor = Tensor.randn([1000]);
|
||||
|
||||
// Mean should be close to 0, std close to 1
|
||||
expect(tensor.mean().abs(), lessThan(0.2));
|
||||
expect(tensor.std(), closeTo(1.0, 0.2));
|
||||
});
|
||||
|
||||
test('creates identity matrix', () {
|
||||
final tensor = Tensor.eye(3);
|
||||
|
||||
expect(tensor.shape, equals([3, 3]));
|
||||
expect(tensor.at([0, 0]), equals(1.0));
|
||||
expect(tensor.at([1, 1]), equals(1.0));
|
||||
expect(tensor.at([2, 2]), equals(1.0));
|
||||
expect(tensor.at([0, 1]), equals(0.0));
|
||||
});
|
||||
|
||||
test('creates linspace tensor', () {
|
||||
final tensor = Tensor.linspace(0.0, 10.0, 11);
|
||||
|
||||
expect(tensor.shape, equals([11]));
|
||||
expect(tensor[0], equals(0.0));
|
||||
expect(tensor[10], equals(10.0));
|
||||
expect(tensor[5], equals(5.0));
|
||||
});
|
||||
|
||||
test('creates arange tensor', () {
|
||||
final tensor = Tensor.arange(0.0, 5.0, step: 1.0);
|
||||
|
||||
expect(tensor.shape, equals([5]));
|
||||
expect(tensor[0], equals(0.0));
|
||||
expect(tensor[4], equals(4.0));
|
||||
});
|
||||
});
|
||||
|
||||
group('Tensor Operations', () {
|
||||
test('reshapes tensor', () {
|
||||
final tensor = Tensor(
|
||||
shape: [6],
|
||||
data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
|
||||
);
|
||||
|
||||
final reshaped = tensor.reshape([2, 3]);
|
||||
|
||||
expect(reshaped.shape, equals([2, 3]));
|
||||
expect(reshaped.size, equals(6));
|
||||
});
|
||||
|
||||
test('throws on invalid reshape', () {
|
||||
final tensor = Tensor.zeros([4]);
|
||||
|
||||
expect(
|
||||
() => tensor.reshape([2, 3]),
|
||||
throwsA(isA<ArgumentError>()),
|
||||
);
|
||||
});
|
||||
|
||||
test('flattens tensor', () {
|
||||
final tensor = Tensor.zeros([2, 3, 4]);
|
||||
final flat = tensor.flatten();
|
||||
|
||||
expect(flat.shape, equals([24]));
|
||||
});
|
||||
|
||||
test('transposes 2D tensor', () {
|
||||
final tensor = Tensor(
|
||||
shape: [2, 3],
|
||||
data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
|
||||
);
|
||||
|
||||
final transposed = tensor.transpose();
|
||||
|
||||
expect(transposed.shape, equals([3, 2]));
|
||||
expect(transposed.at([0, 0]), equals(1.0));
|
||||
expect(transposed.at([0, 1]), equals(4.0));
|
||||
});
|
||||
|
||||
test('accesses element at index for 1D tensor', () {
|
||||
final tensor = Tensor(shape: [4], data: [1.0, 2.0, 3.0, 4.0]);
|
||||
|
||||
expect(tensor[0], equals(1.0));
|
||||
expect(tensor[3], equals(4.0));
|
||||
});
|
||||
|
||||
test('accesses element at multi-dimensional index', () {
|
||||
final tensor = Tensor(
|
||||
shape: [2, 3],
|
||||
data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
|
||||
);
|
||||
|
||||
expect(tensor.at([0, 0]), equals(1.0));
|
||||
expect(tensor.at([0, 2]), equals(3.0));
|
||||
expect(tensor.at([1, 1]), equals(5.0));
|
||||
});
|
||||
|
||||
test('throws on out-of-bounds index', () {
|
||||
final tensor = Tensor.zeros([2, 3]);
|
||||
|
||||
expect(
|
||||
() => tensor.at([2, 0]),
|
||||
throwsA(isA<RangeError>()),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('Tensor Reductions', () {
|
||||
test('calculates sum', () {
|
||||
final tensor = Tensor(shape: [4], data: [1.0, 2.0, 3.0, 4.0]);
|
||||
|
||||
expect(tensor.sum(), equals(10.0));
|
||||
});
|
||||
|
||||
test('calculates mean', () {
|
||||
final tensor = Tensor(shape: [4], data: [1.0, 2.0, 3.0, 4.0]);
|
||||
|
||||
expect(tensor.mean(), equals(2.5));
|
||||
});
|
||||
|
||||
test('calculates std', () {
|
||||
final tensor = Tensor(shape: [4], data: [1.0, 2.0, 3.0, 4.0]);
|
||||
|
||||
expect(tensor.std(), closeTo(1.118, 0.001));
|
||||
});
|
||||
|
||||
test('finds min', () {
|
||||
final tensor = Tensor(shape: [4], data: [3.0, 1.0, 4.0, 2.0]);
|
||||
|
||||
expect(tensor.min(), equals(1.0));
|
||||
});
|
||||
|
||||
test('finds max', () {
|
||||
final tensor = Tensor(shape: [4], data: [3.0, 1.0, 4.0, 2.0]);
|
||||
|
||||
expect(tensor.max(), equals(4.0));
|
||||
});
|
||||
|
||||
test('finds argmin', () {
|
||||
final tensor = Tensor(shape: [4], data: [3.0, 1.0, 4.0, 2.0]);
|
||||
|
||||
expect(tensor.argmin(), equals(1));
|
||||
});
|
||||
|
||||
test('finds argmax', () {
|
||||
final tensor = Tensor(shape: [4], data: [3.0, 1.0, 4.0, 2.0]);
|
||||
|
||||
expect(tensor.argmax(), equals(2));
|
||||
});
|
||||
});
|
||||
|
||||
group('Tensor Element-wise Operations', () {
|
||||
test('adds tensors', () {
|
||||
final a = Tensor(shape: [3], data: [1.0, 2.0, 3.0]);
|
||||
final b = Tensor(shape: [3], data: [4.0, 5.0, 6.0]);
|
||||
|
||||
final result = a.add(b);
|
||||
|
||||
expect(result.data, equals(Float64List.fromList([5.0, 7.0, 9.0])));
|
||||
});
|
||||
|
||||
test('subtracts tensors', () {
|
||||
final a = Tensor(shape: [3], data: [5.0, 7.0, 9.0]);
|
||||
final b = Tensor(shape: [3], data: [1.0, 2.0, 3.0]);
|
||||
|
||||
final result = a.sub(b);
|
||||
|
||||
expect(result.data, equals(Float64List.fromList([4.0, 5.0, 6.0])));
|
||||
});
|
||||
|
||||
test('multiplies tensors', () {
|
||||
final a = Tensor(shape: [3], data: [2.0, 3.0, 4.0]);
|
||||
final b = Tensor(shape: [3], data: [1.0, 2.0, 3.0]);
|
||||
|
||||
final result = a.mul(b);
|
||||
|
||||
expect(result.data, equals(Float64List.fromList([2.0, 6.0, 12.0])));
|
||||
});
|
||||
|
||||
test('divides tensors', () {
|
||||
final a = Tensor(shape: [3], data: [6.0, 8.0, 9.0]);
|
||||
final b = Tensor(shape: [3], data: [2.0, 4.0, 3.0]);
|
||||
|
||||
final result = a.div(b);
|
||||
|
||||
expect(result.data, equals(Float64List.fromList([3.0, 2.0, 3.0])));
|
||||
});
|
||||
|
||||
test('adds scalar', () {
|
||||
final tensor = Tensor(shape: [3], data: [1.0, 2.0, 3.0]);
|
||||
final result = tensor.addScalar(10.0);
|
||||
|
||||
expect(result.data, equals(Float64List.fromList([11.0, 12.0, 13.0])));
|
||||
});
|
||||
|
||||
test('multiplies by scalar', () {
|
||||
final tensor = Tensor(shape: [3], data: [1.0, 2.0, 3.0]);
|
||||
final result = tensor.mulScalar(2.0);
|
||||
|
||||
expect(result.data, equals(Float64List.fromList([2.0, 4.0, 6.0])));
|
||||
});
|
||||
|
||||
test('throws on shape mismatch', () {
|
||||
final a = Tensor.zeros([2, 3]);
|
||||
final b = Tensor.zeros([3, 2]);
|
||||
|
||||
expect(() => a.add(b), throwsA(isA<ArgumentError>()));
|
||||
});
|
||||
});
|
||||
|
||||
group('Tensor Activations', () {
|
||||
test('applies relu', () {
|
||||
final tensor = Tensor(shape: [5], data: [-2.0, -1.0, 0.0, 1.0, 2.0]);
|
||||
final result = tensor.relu();
|
||||
|
||||
expect(result.data, equals(Float64List.fromList([0.0, 0.0, 0.0, 1.0, 2.0])));
|
||||
});
|
||||
|
||||
test('applies sigmoid', () {
|
||||
final tensor = Tensor(shape: [1], data: [0.0]);
|
||||
final result = tensor.sigmoid();
|
||||
|
||||
expect(result[0], closeTo(0.5, 0.001));
|
||||
});
|
||||
|
||||
test('applies tanh', () {
|
||||
final tensor = Tensor(shape: [1], data: [0.0]);
|
||||
final result = tensor.tanh();
|
||||
|
||||
expect(result[0], closeTo(0.0, 0.001));
|
||||
});
|
||||
|
||||
test('applies softmax on 1D tensor', () {
|
||||
final tensor = Tensor(shape: [3], data: [1.0, 2.0, 3.0]);
|
||||
final result = tensor.softmax();
|
||||
|
||||
expect(result.sum(), closeTo(1.0, 0.001));
|
||||
expect(result[2], greaterThan(result[1]));
|
||||
expect(result[1], greaterThan(result[0]));
|
||||
});
|
||||
});
|
||||
|
||||
group('Tensor Serialization', () {
|
||||
test('serializes to JSON', () {
|
||||
final tensor = Tensor(
|
||||
shape: [2, 2],
|
||||
data: [1.0, 2.0, 3.0, 4.0],
|
||||
);
|
||||
|
||||
final json = tensor.toJson();
|
||||
|
||||
expect(json['shape'], equals([2, 2]));
|
||||
expect(json['dtype'], equals('float64'));
|
||||
expect(json['data'], isA<String>());
|
||||
});
|
||||
|
||||
test('deserializes from JSON with base64 data', () {
|
||||
final original = Tensor(
|
||||
shape: [2, 3],
|
||||
data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
|
||||
);
|
||||
final json = original.toJson();
|
||||
final restored = Tensor.fromJson(json);
|
||||
|
||||
expect(restored.shape, equals(original.shape));
|
||||
expect(restored.dtype, equals(original.dtype));
|
||||
for (var i = 0; i < original.size; i++) {
|
||||
expect(restored.data[i], closeTo(original.data[i], 0.0001));
|
||||
}
|
||||
});
|
||||
|
||||
test('deserializes from JSON with list data', () {
|
||||
final json = {
|
||||
'shape': [2, 2],
|
||||
'data': [
|
||||
[1.0, 2.0],
|
||||
[3.0, 4.0]
|
||||
],
|
||||
'dtype': 'float64',
|
||||
};
|
||||
|
||||
final tensor = Tensor.fromJson(json);
|
||||
|
||||
expect(tensor.shape, equals([2, 2]));
|
||||
expect(tensor.at([0, 0]), equals(1.0));
|
||||
expect(tensor.at([1, 1]), equals(4.0));
|
||||
});
|
||||
|
||||
test('converts to nested list', () {
|
||||
final tensor = Tensor(
|
||||
shape: [2, 3],
|
||||
data: [1.0, 2.0, 3.0, 4.0, 5.0, 6.0],
|
||||
);
|
||||
|
||||
final nested = tensor.toNestedList();
|
||||
|
||||
expect(nested, equals([
|
||||
[1.0, 2.0, 3.0],
|
||||
[4.0, 5.0, 6.0],
|
||||
]));
|
||||
});
|
||||
});
|
||||
|
||||
group('Tensor Properties', () {
|
||||
test('calculates nbytes correctly', () {
|
||||
final tensor = Tensor.zeros([10]);
|
||||
|
||||
// Float64List uses 8 bytes per element
|
||||
expect(tensor.nbytes, equals(80));
|
||||
});
|
||||
|
||||
test('equality works correctly', () {
|
||||
final a = Tensor(shape: [2], data: [1.0, 2.0]);
|
||||
final b = Tensor(shape: [2], data: [1.0, 2.0]);
|
||||
final c = Tensor(shape: [2], data: [1.0, 3.0]);
|
||||
|
||||
expect(a, equals(b));
|
||||
expect(a, isNot(equals(c)));
|
||||
});
|
||||
|
||||
test('toString provides useful info', () {
|
||||
final small = Tensor(shape: [2], data: [1.0, 2.0]);
|
||||
final large = Tensor.zeros([100, 100]);
|
||||
|
||||
expect(small.toString(), contains('Tensor'));
|
||||
expect(large.toString(), contains('shape'));
|
||||
});
|
||||
});
|
||||
}
|
||||
462
sdk/flutter/test/types_test.dart
Normal file
462
sdk/flutter/test/types_test.dart
Normal file
|
|
@ -0,0 +1,462 @@
|
|||
import 'package:flutter_test/flutter_test.dart';
|
||||
import 'package:synor_compute/synor_compute.dart';
|
||||
|
||||
void main() {
|
||||
group('Precision Enum', () {
|
||||
test('has all expected values', () {
|
||||
expect(Precision.values.length, equals(6));
|
||||
expect(Precision.fp64.value, equals('fp64'));
|
||||
expect(Precision.fp32.value, equals('fp32'));
|
||||
expect(Precision.fp16.value, equals('fp16'));
|
||||
expect(Precision.bf16.value, equals('bf16'));
|
||||
expect(Precision.int8.value, equals('int8'));
|
||||
expect(Precision.int4.value, equals('int4'));
|
||||
});
|
||||
|
||||
test('parses from string', () {
|
||||
expect(Precision.fromString('fp16'), equals(Precision.fp16));
|
||||
expect(Precision.fromString('bf16'), equals(Precision.bf16));
|
||||
expect(Precision.fromString('invalid'), equals(Precision.fp32)); // default
|
||||
});
|
||||
});
|
||||
|
||||
group('ProcessorType Enum', () {
|
||||
test('has all expected values', () {
|
||||
expect(ProcessorType.values.length, equals(10));
|
||||
expect(ProcessorType.cpu.value, equals('cpu'));
|
||||
expect(ProcessorType.gpu.value, equals('gpu'));
|
||||
expect(ProcessorType.tpu.value, equals('tpu'));
|
||||
expect(ProcessorType.npu.value, equals('npu'));
|
||||
expect(ProcessorType.lpu.value, equals('lpu'));
|
||||
expect(ProcessorType.auto.value, equals('auto'));
|
||||
});
|
||||
|
||||
test('parses from string', () {
|
||||
expect(ProcessorType.fromString('gpu'), equals(ProcessorType.gpu));
|
||||
expect(ProcessorType.fromString('invalid'), equals(ProcessorType.auto));
|
||||
});
|
||||
});
|
||||
|
||||
group('Priority Enum', () {
|
||||
test('has all expected values', () {
|
||||
expect(Priority.values.length, equals(4));
|
||||
expect(Priority.low.value, equals('low'));
|
||||
expect(Priority.normal.value, equals('normal'));
|
||||
expect(Priority.high.value, equals('high'));
|
||||
expect(Priority.critical.value, equals('critical'));
|
||||
});
|
||||
|
||||
test('parses from string', () {
|
||||
expect(Priority.fromString('high'), equals(Priority.high));
|
||||
expect(Priority.fromString('invalid'), equals(Priority.normal));
|
||||
});
|
||||
});
|
||||
|
||||
group('JobStatus Enum', () {
|
||||
test('has all expected values', () {
|
||||
expect(JobStatus.values.length, equals(6));
|
||||
expect(JobStatus.pending.value, equals('pending'));
|
||||
expect(JobStatus.queued.value, equals('queued'));
|
||||
expect(JobStatus.running.value, equals('running'));
|
||||
expect(JobStatus.completed.value, equals('completed'));
|
||||
expect(JobStatus.failed.value, equals('failed'));
|
||||
expect(JobStatus.cancelled.value, equals('cancelled'));
|
||||
});
|
||||
|
||||
test('identifies terminal states', () {
|
||||
expect(JobStatus.completed.isTerminal, isTrue);
|
||||
expect(JobStatus.failed.isTerminal, isTrue);
|
||||
expect(JobStatus.cancelled.isTerminal, isTrue);
|
||||
expect(JobStatus.pending.isTerminal, isFalse);
|
||||
expect(JobStatus.running.isTerminal, isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('BalancingStrategy Enum', () {
|
||||
test('has all expected values', () {
|
||||
expect(BalancingStrategy.speed.value, equals('speed'));
|
||||
expect(BalancingStrategy.cost.value, equals('cost'));
|
||||
expect(BalancingStrategy.energy.value, equals('energy'));
|
||||
expect(BalancingStrategy.balanced.value, equals('balanced'));
|
||||
});
|
||||
});
|
||||
|
||||
group('DType Enum', () {
|
||||
test('has all expected values', () {
|
||||
expect(DType.float64.value, equals('float64'));
|
||||
expect(DType.float32.value, equals('float32'));
|
||||
expect(DType.int8.value, equals('int8'));
|
||||
expect(DType.bool_.value, equals('bool'));
|
||||
});
|
||||
});
|
||||
|
||||
group('ModelCategory Enum', () {
|
||||
test('has all expected values', () {
|
||||
expect(ModelCategory.llm.value, equals('llm'));
|
||||
expect(ModelCategory.embedding.value, equals('embedding'));
|
||||
expect(ModelCategory.imageGeneration.value, equals('image_generation'));
|
||||
expect(ModelCategory.custom.value, equals('custom'));
|
||||
});
|
||||
});
|
||||
|
||||
group('SynorConfig', () {
|
||||
test('creates with required fields', () {
|
||||
final config = SynorConfig(apiKey: 'test-key');
|
||||
|
||||
expect(config.apiKey, equals('test-key'));
|
||||
expect(config.baseUrl, equals('https://compute.synor.io'));
|
||||
expect(config.timeout, equals(const Duration(seconds: 30)));
|
||||
expect(config.maxRetries, equals(3));
|
||||
expect(config.defaultProcessor, equals(ProcessorType.auto));
|
||||
expect(config.defaultPrecision, equals(Precision.fp32));
|
||||
});
|
||||
|
||||
test('creates with custom values', () {
|
||||
final config = SynorConfig(
|
||||
apiKey: 'test-key',
|
||||
baseUrl: 'https://custom.api.com',
|
||||
timeout: const Duration(seconds: 60),
|
||||
maxRetries: 5,
|
||||
defaultProcessor: ProcessorType.gpu,
|
||||
defaultPrecision: Precision.fp16,
|
||||
);
|
||||
|
||||
expect(config.baseUrl, equals('https://custom.api.com'));
|
||||
expect(config.timeout, equals(const Duration(seconds: 60)));
|
||||
expect(config.maxRetries, equals(5));
|
||||
});
|
||||
|
||||
test('copyWith updates fields', () {
|
||||
final original = SynorConfig(apiKey: 'test-key');
|
||||
final updated = original.copyWith(
|
||||
timeout: const Duration(seconds: 60),
|
||||
defaultProcessor: ProcessorType.tpu,
|
||||
);
|
||||
|
||||
expect(updated.apiKey, equals('test-key'));
|
||||
expect(updated.timeout, equals(const Duration(seconds: 60)));
|
||||
expect(updated.defaultProcessor, equals(ProcessorType.tpu));
|
||||
});
|
||||
});
|
||||
|
||||
group('MatMulOptions', () {
|
||||
test('creates with defaults', () {
|
||||
const options = MatMulOptions();
|
||||
|
||||
expect(options.precision, isNull);
|
||||
expect(options.processor, isNull);
|
||||
expect(options.transposeA, isFalse);
|
||||
expect(options.transposeB, isFalse);
|
||||
});
|
||||
|
||||
test('toJson generates correct output', () {
|
||||
const options = MatMulOptions(
|
||||
precision: Precision.fp16,
|
||||
processor: ProcessorType.gpu,
|
||||
transposeA: true,
|
||||
);
|
||||
|
||||
final json = options.toJson();
|
||||
|
||||
expect(json['precision'], equals('fp16'));
|
||||
expect(json['processor'], equals('gpu'));
|
||||
expect(json['transpose_a'], isTrue);
|
||||
expect(json['transpose_b'], isFalse);
|
||||
});
|
||||
});
|
||||
|
||||
group('Conv2dOptions', () {
|
||||
test('creates with defaults', () {
|
||||
const options = Conv2dOptions();
|
||||
|
||||
expect(options.kernel, equals([3, 3]));
|
||||
expect(options.stride, equals([1, 1]));
|
||||
expect(options.padding, equals([0, 0]));
|
||||
expect(options.groups, equals(1));
|
||||
});
|
||||
|
||||
test('toJson generates correct output', () {
|
||||
const options = Conv2dOptions(
|
||||
kernel: [5, 5],
|
||||
stride: [2, 2],
|
||||
padding: [2, 2],
|
||||
);
|
||||
|
||||
final json = options.toJson();
|
||||
|
||||
expect(json['kernel'], equals([5, 5]));
|
||||
expect(json['stride'], equals([2, 2]));
|
||||
expect(json['padding'], equals([2, 2]));
|
||||
});
|
||||
});
|
||||
|
||||
group('AttentionOptions', () {
|
||||
test('creates with required fields', () {
|
||||
const options = AttentionOptions(numHeads: 8);
|
||||
|
||||
expect(options.numHeads, equals(8));
|
||||
expect(options.causal, isFalse);
|
||||
expect(options.scale, isNull);
|
||||
});
|
||||
|
||||
test('toJson generates correct output', () {
|
||||
const options = AttentionOptions(
|
||||
numHeads: 12,
|
||||
scale: 0.125,
|
||||
causal: true,
|
||||
precision: Precision.fp16,
|
||||
);
|
||||
|
||||
final json = options.toJson();
|
||||
|
||||
expect(json['num_heads'], equals(12));
|
||||
expect(json['scale'], equals(0.125));
|
||||
expect(json['causal'], isTrue);
|
||||
expect(json['precision'], equals('fp16'));
|
||||
});
|
||||
});
|
||||
|
||||
group('InferenceOptions', () {
|
||||
test('creates with defaults', () {
|
||||
const options = InferenceOptions();
|
||||
|
||||
expect(options.maxTokens, equals(256));
|
||||
expect(options.temperature, equals(0.7));
|
||||
expect(options.topP, equals(1.0));
|
||||
expect(options.stream, isFalse);
|
||||
});
|
||||
|
||||
test('toJson generates correct output', () {
|
||||
const options = InferenceOptions(
|
||||
maxTokens: 512,
|
||||
temperature: 0.9,
|
||||
topK: 50,
|
||||
stream: true,
|
||||
);
|
||||
|
||||
final json = options.toJson();
|
||||
|
||||
expect(json['max_tokens'], equals(512));
|
||||
expect(json['temperature'], equals(0.9));
|
||||
expect(json['top_k'], equals(50));
|
||||
expect(json['stream'], isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('PricingInfo', () {
|
||||
test('parses from JSON', () {
|
||||
final json = {
|
||||
'processor': 'gpu',
|
||||
'price_per_second': 0.0001,
|
||||
'price_per_gflop': 0.000001,
|
||||
'available_units': 100,
|
||||
'utilization_percent': 75.5,
|
||||
'region': 'us-east-1',
|
||||
};
|
||||
|
||||
final pricing = PricingInfo.fromJson(json);
|
||||
|
||||
expect(pricing.processor, equals(ProcessorType.gpu));
|
||||
expect(pricing.pricePerSecond, equals(0.0001));
|
||||
expect(pricing.availableUnits, equals(100));
|
||||
expect(pricing.utilizationPercent, equals(75.5));
|
||||
});
|
||||
});
|
||||
|
||||
group('UsageStats', () {
|
||||
test('parses from JSON', () {
|
||||
final json = {
|
||||
'total_jobs': 1000,
|
||||
'completed_jobs': 950,
|
||||
'failed_jobs': 50,
|
||||
'total_compute_seconds': 3600.0,
|
||||
'total_cost': 0.36,
|
||||
'cost_by_processor': {
|
||||
'gpu': 0.30,
|
||||
'tpu': 0.06,
|
||||
},
|
||||
};
|
||||
|
||||
final stats = UsageStats.fromJson(json);
|
||||
|
||||
expect(stats.totalJobs, equals(1000));
|
||||
expect(stats.completedJobs, equals(950));
|
||||
expect(stats.failedJobs, equals(50));
|
||||
expect(stats.totalComputeSeconds, equals(3600.0));
|
||||
expect(stats.costByProcessor[ProcessorType.gpu], equals(0.30));
|
||||
});
|
||||
});
|
||||
|
||||
group('ModelInfo', () {
|
||||
test('parses from JSON', () {
|
||||
final json = {
|
||||
'id': 'llama-3-70b',
|
||||
'name': 'Llama 3 70B',
|
||||
'description': 'Large language model',
|
||||
'category': 'llm',
|
||||
'cid': 'QmTest...',
|
||||
'format': 'safetensors',
|
||||
'size_bytes': 140000000000,
|
||||
'parameters': 70000000000,
|
||||
'supported_precisions': ['fp16', 'bf16', 'fp32'],
|
||||
'recommended_processor': 'gpu',
|
||||
'context_length': 8192,
|
||||
'license': 'llama3',
|
||||
'provider': 'meta',
|
||||
'version': '1.0.0',
|
||||
'is_public': true,
|
||||
};
|
||||
|
||||
final model = ModelInfo.fromJson(json);
|
||||
|
||||
expect(model.id, equals('llama-3-70b'));
|
||||
expect(model.category, equals(ModelCategory.llm));
|
||||
expect(model.parameters, equals(70000000000));
|
||||
expect(model.contextLength, equals(8192));
|
||||
});
|
||||
|
||||
test('formats parameters correctly', () {
|
||||
final model = ModelInfo(
|
||||
id: 'test',
|
||||
name: 'Test',
|
||||
description: 'Test',
|
||||
category: ModelCategory.llm,
|
||||
cid: 'QmTest',
|
||||
format: ModelFormat.onnx,
|
||||
sizeBytes: 1000,
|
||||
parameters: 70000000000,
|
||||
supportedPrecisions: [Precision.fp16],
|
||||
recommendedProcessor: ProcessorType.gpu,
|
||||
license: 'MIT',
|
||||
provider: 'test',
|
||||
version: '1.0',
|
||||
isPublic: true,
|
||||
);
|
||||
|
||||
expect(model.formattedParameters, equals('70.0B'));
|
||||
});
|
||||
});
|
||||
|
||||
group('TrainingOptions', () {
|
||||
test('creates with defaults', () {
|
||||
const options = TrainingOptions();
|
||||
|
||||
expect(options.framework, equals(MlFramework.pytorch));
|
||||
expect(options.epochs, equals(1));
|
||||
expect(options.batchSize, equals(32));
|
||||
expect(options.learningRate, equals(0.001));
|
||||
});
|
||||
|
||||
test('toJson generates correct output', () {
|
||||
const options = TrainingOptions(
|
||||
framework: MlFramework.jax,
|
||||
epochs: 10,
|
||||
batchSize: 64,
|
||||
learningRate: 0.0001,
|
||||
distributed: true,
|
||||
);
|
||||
|
||||
final json = options.toJson();
|
||||
|
||||
expect(json['framework'], equals('jax'));
|
||||
expect(json['epochs'], equals(10));
|
||||
expect(json['batch_size'], equals(64));
|
||||
expect(json['distributed'], isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('TrainingResult', () {
|
||||
test('parses from JSON', () {
|
||||
final json = {
|
||||
'job_id': 'train-123',
|
||||
'model_cid': 'QmTrained...',
|
||||
'epochs': 10,
|
||||
'final_loss': 0.05,
|
||||
'metrics': {'accuracy': 0.95, 'f1': 0.94},
|
||||
'duration_ms': 3600000,
|
||||
'cost': 1.50,
|
||||
'checkpoint_cids': ['QmChk1', 'QmChk2'],
|
||||
};
|
||||
|
||||
final result = TrainingResult.fromJson(json);
|
||||
|
||||
expect(result.jobId, equals('train-123'));
|
||||
expect(result.epochs, equals(10));
|
||||
expect(result.finalLoss, equals(0.05));
|
||||
expect(result.metrics['accuracy'], equals(0.95));
|
||||
});
|
||||
});
|
||||
|
||||
group('DatasetUploadOptions', () {
|
||||
test('creates with defaults', () {
|
||||
const options = DatasetUploadOptions(name: 'test-dataset');
|
||||
|
||||
expect(options.name, equals('test-dataset'));
|
||||
expect(options.format, equals(DatasetFormat.jsonl));
|
||||
expect(options.type, equals(DatasetType.textCompletion));
|
||||
expect(options.isPublic, isFalse);
|
||||
});
|
||||
|
||||
test('toJson generates correct output', () {
|
||||
const options = DatasetUploadOptions(
|
||||
name: 'my-dataset',
|
||||
description: 'Test dataset',
|
||||
format: DatasetFormat.parquet,
|
||||
type: DatasetType.instructionTuning,
|
||||
isPublic: true,
|
||||
);
|
||||
|
||||
final json = options.toJson();
|
||||
|
||||
expect(json['name'], equals('my-dataset'));
|
||||
expect(json['format'], equals('parquet'));
|
||||
expect(json['type'], equals('instruction_tuning'));
|
||||
expect(json['is_public'], isTrue);
|
||||
});
|
||||
});
|
||||
|
||||
group('TrainingProgress', () {
|
||||
test('parses from JSON', () {
|
||||
final json = {
|
||||
'job_id': 'train-123',
|
||||
'epoch': 5,
|
||||
'total_epochs': 10,
|
||||
'step': 500,
|
||||
'total_steps': 1000,
|
||||
'loss': 0.15,
|
||||
'metrics': {'accuracy': 0.85},
|
||||
'learning_rate': 0.0001,
|
||||
'samples_per_second': 100,
|
||||
};
|
||||
|
||||
final progress = TrainingProgress.fromJson(json);
|
||||
|
||||
expect(progress.epoch, equals(5));
|
||||
expect(progress.progress, equals(0.5));
|
||||
expect(progress.progressText, contains('Epoch 6/10'));
|
||||
});
|
||||
});
|
||||
|
||||
group('SynorException', () {
|
||||
test('creates with message', () {
|
||||
const exception = SynorException('Test error', code: 'ERR_TEST');
|
||||
|
||||
expect(exception.message, equals('Test error'));
|
||||
expect(exception.code, equals('ERR_TEST'));
|
||||
expect(exception.toString(), contains('Test error'));
|
||||
});
|
||||
|
||||
test('parses from JSON', () {
|
||||
final json = {
|
||||
'message': 'API Error',
|
||||
'code': 'API_ERR',
|
||||
'status_code': 500,
|
||||
};
|
||||
|
||||
final exception = SynorException.fromJson(json);
|
||||
|
||||
expect(exception.message, equals('API Error'));
|
||||
expect(exception.statusCode, equals(500));
|
||||
});
|
||||
});
|
||||
}
|
||||
416
sdk/go/synor_test.go
Normal file
416
sdk/go/synor_test.go
Normal file
|
|
@ -0,0 +1,416 @@
|
|||
package synor
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestClientCreation tests client initialization.
|
||||
func TestClientCreation(t *testing.T) {
|
||||
t.Run("creates client with API key", func(t *testing.T) {
|
||||
client := NewClient("test-api-key")
|
||||
if client == nil {
|
||||
t.Fatal("expected non-nil client")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("creates client with config", func(t *testing.T) {
|
||||
config := Config{
|
||||
APIKey: "test-api-key",
|
||||
Endpoint: "https://custom.api.com",
|
||||
Strategy: Cost,
|
||||
Precision: FP16,
|
||||
Timeout: 60 * time.Second,
|
||||
}
|
||||
client := NewClientWithConfig(config)
|
||||
if client == nil {
|
||||
t.Fatal("expected non-nil client")
|
||||
}
|
||||
if client.config.Endpoint != "https://custom.api.com" {
|
||||
t.Errorf("expected custom endpoint, got %s", client.config.Endpoint)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestDefaultConfig tests default configuration.
|
||||
func TestDefaultConfig(t *testing.T) {
|
||||
config := DefaultConfig("my-key")
|
||||
|
||||
if config.APIKey != "my-key" {
|
||||
t.Errorf("expected API key 'my-key', got %s", config.APIKey)
|
||||
}
|
||||
if config.Endpoint != DefaultEndpoint {
|
||||
t.Errorf("expected default endpoint, got %s", config.Endpoint)
|
||||
}
|
||||
if config.Strategy != Balanced {
|
||||
t.Errorf("expected Balanced strategy, got %s", config.Strategy)
|
||||
}
|
||||
if config.Precision != FP32 {
|
||||
t.Errorf("expected FP32 precision, got %s", config.Precision)
|
||||
}
|
||||
if config.Timeout != 30*time.Second {
|
||||
t.Errorf("expected 30s timeout, got %v", config.Timeout)
|
||||
}
|
||||
}
|
||||
|
||||
// TestTensorCreation tests tensor creation.
|
||||
func TestTensorCreation(t *testing.T) {
|
||||
t.Run("creates tensor from data", func(t *testing.T) {
|
||||
data := []float32{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}
|
||||
shape := []int{2, 3}
|
||||
tensor := NewTensor(data, shape, FP32)
|
||||
|
||||
if tensor == nil {
|
||||
t.Fatal("expected non-nil tensor")
|
||||
}
|
||||
if len(tensor.Data) != 6 {
|
||||
t.Errorf("expected 6 elements, got %d", len(tensor.Data))
|
||||
}
|
||||
if len(tensor.Shape) != 2 {
|
||||
t.Errorf("expected 2 dimensions, got %d", len(tensor.Shape))
|
||||
}
|
||||
if tensor.DType != FP32 {
|
||||
t.Errorf("expected FP32 dtype, got %s", tensor.DType)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("creates zeros tensor", func(t *testing.T) {
|
||||
tensor := Zeros([]int{3, 4}, FP32)
|
||||
|
||||
if tensor == nil {
|
||||
t.Fatal("expected non-nil tensor")
|
||||
}
|
||||
if len(tensor.Data) != 12 {
|
||||
t.Errorf("expected 12 elements, got %d", len(tensor.Data))
|
||||
}
|
||||
for i, v := range tensor.Data {
|
||||
if v != 0.0 {
|
||||
t.Errorf("expected 0 at index %d, got %f", i, v)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestTensorSerialize tests tensor serialization.
|
||||
func TestTensorSerialize(t *testing.T) {
|
||||
data := []float32{1.0, 2.0, 3.0, 4.0}
|
||||
shape := []int{2, 2}
|
||||
tensor := NewTensor(data, shape, FP32)
|
||||
|
||||
serialized := tensor.Serialize()
|
||||
|
||||
if _, ok := serialized["data"]; !ok {
|
||||
t.Error("expected 'data' field in serialized tensor")
|
||||
}
|
||||
if _, ok := serialized["shape"]; !ok {
|
||||
t.Error("expected 'shape' field in serialized tensor")
|
||||
}
|
||||
if _, ok := serialized["dtype"]; !ok {
|
||||
t.Error("expected 'dtype' field in serialized tensor")
|
||||
}
|
||||
if serialized["dtype"] != FP32 {
|
||||
t.Errorf("expected FP32 dtype, got %v", serialized["dtype"])
|
||||
}
|
||||
}
|
||||
|
||||
// TestProcessorTypes tests processor type constants.
|
||||
func TestProcessorTypes(t *testing.T) {
|
||||
types := []ProcessorType{CPU, GPU, TPU, NPU, LPU, FPGA, WASM, WebGPU}
|
||||
expected := []string{"cpu", "gpu", "tpu", "npu", "lpu", "fpga", "wasm", "webgpu"}
|
||||
|
||||
for i, pt := range types {
|
||||
if string(pt) != expected[i] {
|
||||
t.Errorf("expected %s, got %s", expected[i], pt)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestPrecisionTypes tests precision type constants.
|
||||
func TestPrecisionTypes(t *testing.T) {
|
||||
precisions := []Precision{FP64, FP32, FP16, BF16, INT8, INT4}
|
||||
expected := []string{"fp64", "fp32", "fp16", "bf16", "int8", "int4"}
|
||||
|
||||
for i, p := range precisions {
|
||||
if string(p) != expected[i] {
|
||||
t.Errorf("expected %s, got %s", expected[i], p)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestJobStatus tests job status constants.
|
||||
func TestJobStatus(t *testing.T) {
|
||||
statuses := []JobStatus{Pending, Queued, Running, Completed, Failed, Cancelled}
|
||||
expected := []string{"pending", "queued", "running", "completed", "failed", "cancelled"}
|
||||
|
||||
for i, s := range statuses {
|
||||
if string(s) != expected[i] {
|
||||
t.Errorf("expected %s, got %s", expected[i], s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestBalancingStrategies tests balancing strategy constants.
|
||||
func TestBalancingStrategies(t *testing.T) {
|
||||
strategies := []BalancingStrategy{Speed, Cost, Energy, Latency, Balanced}
|
||||
expected := []string{"speed", "cost", "energy", "latency", "balanced"}
|
||||
|
||||
for i, s := range strategies {
|
||||
if string(s) != expected[i] {
|
||||
t.Errorf("expected %s, got %s", expected[i], s)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestSubmitJob tests job submission with mock server.
|
||||
func TestSubmitJob(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/jobs" && r.Method == "POST" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"job_id": "job-123",
|
||||
})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := Config{
|
||||
APIKey: "test-key",
|
||||
Endpoint: server.URL,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
client := NewClientWithConfig(config)
|
||||
|
||||
ctx := context.Background()
|
||||
job, err := client.SubmitJob(ctx, "matmul", map[string]interface{}{
|
||||
"a": map[string]interface{}{"data": "base64", "shape": []int{2, 2}},
|
||||
"b": map[string]interface{}{"data": "base64", "shape": []int{2, 2}},
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if job.ID != "job-123" {
|
||||
t.Errorf("expected job ID 'job-123', got %s", job.ID)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetJobStatus tests getting job status with mock server.
|
||||
func TestGetJobStatus(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/jobs/job-123" && r.Method == "GET" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"job_id": "job-123",
|
||||
"status": "completed",
|
||||
"data": map[string]interface{}{"result": "success"},
|
||||
})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := Config{
|
||||
APIKey: "test-key",
|
||||
Endpoint: server.URL,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
client := NewClientWithConfig(config)
|
||||
|
||||
ctx := context.Background()
|
||||
result, err := client.GetJobStatus(ctx, "job-123")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if result.Status != Completed {
|
||||
t.Errorf("expected Completed status, got %s", result.Status)
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetPricing tests getting pricing information with mock server.
|
||||
func TestGetPricing(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/pricing" && r.Method == "GET" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"pricing": []map[string]interface{}{
|
||||
{
|
||||
"processor_type": "gpu",
|
||||
"spot_price": 0.0001,
|
||||
"avg_price_24h": 0.00012,
|
||||
"aws_equivalent": 0.001,
|
||||
"savings_percent": 90.0,
|
||||
},
|
||||
{
|
||||
"processor_type": "tpu",
|
||||
"spot_price": 0.0002,
|
||||
"avg_price_24h": 0.00022,
|
||||
"aws_equivalent": 0.002,
|
||||
"savings_percent": 89.0,
|
||||
},
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := Config{
|
||||
APIKey: "test-key",
|
||||
Endpoint: server.URL,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
client := NewClientWithConfig(config)
|
||||
|
||||
ctx := context.Background()
|
||||
pricing, err := client.GetPricing(ctx)
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if len(pricing) != 2 {
|
||||
t.Errorf("expected 2 pricing entries, got %d", len(pricing))
|
||||
}
|
||||
}
|
||||
|
||||
// TestSynorError tests error formatting.
|
||||
func TestSynorError(t *testing.T) {
|
||||
err := &SynorError{
|
||||
Message: "Invalid API key",
|
||||
StatusCode: 401,
|
||||
}
|
||||
|
||||
expected := "synor: Invalid API key (status 401)"
|
||||
if err.Error() != expected {
|
||||
t.Errorf("expected error message '%s', got '%s'", expected, err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
// TestAPIError tests handling of API errors.
|
||||
func TestAPIError(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.WriteHeader(http.StatusUnauthorized)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"message": "Invalid API key",
|
||||
})
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := Config{
|
||||
APIKey: "bad-key",
|
||||
Endpoint: server.URL,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
client := NewClientWithConfig(config)
|
||||
|
||||
ctx := context.Background()
|
||||
_, err := client.SubmitJob(ctx, "test", nil)
|
||||
|
||||
if err == nil {
|
||||
t.Fatal("expected error, got nil")
|
||||
}
|
||||
|
||||
synorErr, ok := err.(*SynorError)
|
||||
if !ok {
|
||||
t.Fatalf("expected SynorError, got %T", err)
|
||||
}
|
||||
if synorErr.StatusCode != 401 {
|
||||
t.Errorf("expected status 401, got %d", synorErr.StatusCode)
|
||||
}
|
||||
}
|
||||
|
||||
// TestCancelJob tests job cancellation with mock server.
|
||||
func TestCancelJob(t *testing.T) {
|
||||
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.URL.Path == "/jobs/job-123" && r.Method == "DELETE" {
|
||||
w.WriteHeader(http.StatusOK)
|
||||
return
|
||||
}
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
}))
|
||||
defer server.Close()
|
||||
|
||||
config := Config{
|
||||
APIKey: "test-key",
|
||||
Endpoint: server.URL,
|
||||
Timeout: 5 * time.Second,
|
||||
}
|
||||
client := NewClientWithConfig(config)
|
||||
|
||||
ctx := context.Background()
|
||||
err := client.CancelJob(ctx, "job-123")
|
||||
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// TestJobMetrics tests job metrics structure.
|
||||
func TestJobMetrics(t *testing.T) {
|
||||
metrics := JobMetrics{
|
||||
ExecutionTimeMs: 150.5,
|
||||
QueueTimeMs: 10.2,
|
||||
ProcessorType: GPU,
|
||||
ProcessorID: "gpu-001",
|
||||
FLOPS: 1e12,
|
||||
MemoryBytes: 1073741824,
|
||||
CostMicro: 100,
|
||||
EnergyMJ: 5.5,
|
||||
}
|
||||
|
||||
if metrics.ExecutionTimeMs != 150.5 {
|
||||
t.Errorf("expected execution time 150.5, got %f", metrics.ExecutionTimeMs)
|
||||
}
|
||||
if metrics.ProcessorType != GPU {
|
||||
t.Errorf("expected GPU processor, got %s", metrics.ProcessorType)
|
||||
}
|
||||
}
|
||||
|
||||
// TestPricingInfo tests pricing info structure.
|
||||
func TestPricingInfo(t *testing.T) {
|
||||
pricing := PricingInfo{
|
||||
ProcessorType: GPU,
|
||||
SpotPrice: 0.0001,
|
||||
AvgPrice24h: 0.00012,
|
||||
AWSEquivalent: 0.001,
|
||||
SavingsPercent: 90.0,
|
||||
}
|
||||
|
||||
if pricing.ProcessorType != GPU {
|
||||
t.Errorf("expected GPU processor, got %s", pricing.ProcessorType)
|
||||
}
|
||||
if pricing.SavingsPercent != 90.0 {
|
||||
t.Errorf("expected 90%% savings, got %f%%", pricing.SavingsPercent)
|
||||
}
|
||||
}
|
||||
|
||||
// TestJobResult tests job result structure.
|
||||
func TestJobResult(t *testing.T) {
|
||||
result := JobResult{
|
||||
JobID: "job-123",
|
||||
Status: Completed,
|
||||
Data: map[string]interface{}{"output": "success"},
|
||||
Metrics: &JobMetrics{
|
||||
ExecutionTimeMs: 100,
|
||||
ProcessorType: GPU,
|
||||
},
|
||||
}
|
||||
|
||||
if result.JobID != "job-123" {
|
||||
t.Errorf("expected job ID 'job-123', got %s", result.JobID)
|
||||
}
|
||||
if result.Status != Completed {
|
||||
t.Errorf("expected Completed status, got %s", result.Status)
|
||||
}
|
||||
if result.Metrics == nil {
|
||||
t.Error("expected non-nil metrics")
|
||||
}
|
||||
}
|
||||
239
sdk/java/src/test/java/io/synor/compute/TensorTest.java
Normal file
239
sdk/java/src/test/java/io/synor/compute/TensorTest.java
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
package io.synor.compute;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
@DisplayName("Tensor Tests")
|
||||
class TensorTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("Creates tensor with shape and data")
|
||||
void testCreateTensor() {
|
||||
double[] data = {1.0, 2.0, 3.0, 4.0, 5.0, 6.0};
|
||||
int[] shape = {2, 3};
|
||||
Tensor tensor = new Tensor(shape, data);
|
||||
|
||||
assertArrayEquals(shape, tensor.getShape());
|
||||
assertEquals(6, tensor.getSize());
|
||||
assertEquals(2, tensor.getNdim());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Creates zeros tensor")
|
||||
void testZeros() {
|
||||
Tensor tensor = Tensor.zeros(new int[]{3, 4});
|
||||
|
||||
assertArrayEquals(new int[]{3, 4}, tensor.getShape());
|
||||
assertEquals(12, tensor.getSize());
|
||||
for (double v : tensor.getData()) {
|
||||
assertEquals(0.0, v);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Creates ones tensor")
|
||||
void testOnes() {
|
||||
Tensor tensor = Tensor.ones(new int[]{2, 2});
|
||||
|
||||
assertArrayEquals(new int[]{2, 2}, tensor.getShape());
|
||||
for (double v : tensor.getData()) {
|
||||
assertEquals(1.0, v);
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Creates random tensor")
|
||||
void testRandom() {
|
||||
Tensor tensor = Tensor.random(new int[]{10, 10});
|
||||
|
||||
assertArrayEquals(new int[]{10, 10}, tensor.getShape());
|
||||
assertEquals(100, tensor.getSize());
|
||||
for (double v : tensor.getData()) {
|
||||
assertTrue(v >= 0 && v < 1, "Random values should be in [0, 1)");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Creates randn tensor with normal distribution")
|
||||
void testRandn() {
|
||||
Tensor tensor = Tensor.randn(new int[]{1000});
|
||||
|
||||
double mean = tensor.mean();
|
||||
double std = tensor.std();
|
||||
|
||||
assertTrue(Math.abs(mean) < 0.2, "Mean should be close to 0");
|
||||
assertTrue(std > 0.8 && std < 1.2, "Std should be close to 1");
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Creates identity matrix")
|
||||
void testEye() {
|
||||
Tensor tensor = Tensor.eye(3);
|
||||
|
||||
assertArrayEquals(new int[]{3, 3}, tensor.getShape());
|
||||
assertEquals(1.0, tensor.get(0, 0));
|
||||
assertEquals(1.0, tensor.get(1, 1));
|
||||
assertEquals(1.0, tensor.get(2, 2));
|
||||
assertEquals(0.0, tensor.get(0, 1));
|
||||
assertEquals(0.0, tensor.get(1, 0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Creates arange tensor")
|
||||
void testArange() {
|
||||
Tensor tensor = Tensor.arange(0, 5, 1);
|
||||
|
||||
assertArrayEquals(new int[]{5}, tensor.getShape());
|
||||
assertEquals(0.0, tensor.get(0));
|
||||
assertEquals(4.0, tensor.get(4));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Creates linspace tensor")
|
||||
void testLinspace() {
|
||||
Tensor tensor = Tensor.linspace(0, 10, 11);
|
||||
|
||||
assertArrayEquals(new int[]{11}, tensor.getShape());
|
||||
assertEquals(0.0, tensor.get(0), 0.0001);
|
||||
assertEquals(10.0, tensor.get(10), 0.0001);
|
||||
assertEquals(5.0, tensor.get(5), 0.0001);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Reshapes tensor")
|
||||
void testReshape() {
|
||||
Tensor tensor = new Tensor(new int[]{6}, new double[]{1, 2, 3, 4, 5, 6});
|
||||
Tensor reshaped = tensor.reshape(new int[]{2, 3});
|
||||
|
||||
assertArrayEquals(new int[]{2, 3}, reshaped.getShape());
|
||||
assertEquals(6, reshaped.getSize());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Throws on invalid reshape")
|
||||
void testInvalidReshape() {
|
||||
Tensor tensor = Tensor.zeros(new int[]{4});
|
||||
|
||||
assertThrows(IllegalArgumentException.class, () -> {
|
||||
tensor.reshape(new int[]{2, 3});
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Transposes 2D tensor")
|
||||
void testTranspose() {
|
||||
Tensor tensor = new Tensor(new int[]{2, 3}, new double[]{1, 2, 3, 4, 5, 6});
|
||||
Tensor transposed = tensor.transpose();
|
||||
|
||||
assertArrayEquals(new int[]{3, 2}, transposed.getShape());
|
||||
assertEquals(1.0, transposed.get(0, 0));
|
||||
assertEquals(4.0, transposed.get(0, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Gets element at index")
|
||||
void testGet() {
|
||||
Tensor tensor = new Tensor(new int[]{2, 3}, new double[]{1, 2, 3, 4, 5, 6});
|
||||
|
||||
assertEquals(1.0, tensor.get(0, 0));
|
||||
assertEquals(3.0, tensor.get(0, 2));
|
||||
assertEquals(5.0, tensor.get(1, 1));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Calculates sum")
|
||||
void testSum() {
|
||||
Tensor tensor = new Tensor(new int[]{4}, new double[]{1, 2, 3, 4});
|
||||
|
||||
assertEquals(10.0, tensor.sum());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Calculates mean")
|
||||
void testMean() {
|
||||
Tensor tensor = new Tensor(new int[]{4}, new double[]{1, 2, 3, 4});
|
||||
|
||||
assertEquals(2.5, tensor.mean());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Calculates std")
|
||||
void testStd() {
|
||||
Tensor tensor = new Tensor(new int[]{4}, new double[]{1, 2, 3, 4});
|
||||
|
||||
assertEquals(1.118, tensor.std(), 0.001);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Finds min")
|
||||
void testMin() {
|
||||
Tensor tensor = new Tensor(new int[]{4}, new double[]{3, 1, 4, 2});
|
||||
|
||||
assertEquals(1.0, tensor.min());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Finds max")
|
||||
void testMax() {
|
||||
Tensor tensor = new Tensor(new int[]{4}, new double[]{3, 1, 4, 2});
|
||||
|
||||
assertEquals(4.0, tensor.max());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Applies ReLU activation")
|
||||
void testRelu() {
|
||||
Tensor tensor = new Tensor(new int[]{5}, new double[]{-2, -1, 0, 1, 2});
|
||||
Tensor result = tensor.relu();
|
||||
|
||||
assertArrayEquals(new double[]{0, 0, 0, 1, 2}, result.getData(), 0.0001);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Applies sigmoid activation")
|
||||
void testSigmoid() {
|
||||
Tensor tensor = new Tensor(new int[]{1}, new double[]{0});
|
||||
Tensor result = tensor.sigmoid();
|
||||
|
||||
assertEquals(0.5, result.get(0), 0.0001);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Applies softmax activation")
|
||||
void testSoftmax() {
|
||||
Tensor tensor = new Tensor(new int[]{3}, new double[]{1, 2, 3});
|
||||
Tensor result = tensor.softmax();
|
||||
|
||||
assertEquals(1.0, result.sum(), 0.0001);
|
||||
assertTrue(result.get(2) > result.get(1));
|
||||
assertTrue(result.get(1) > result.get(0));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Serializes and deserializes tensor")
|
||||
void testSerialization() {
|
||||
Tensor original = new Tensor(new int[]{2, 3}, new double[]{1, 2, 3, 4, 5, 6});
|
||||
var json = original.toJson();
|
||||
|
||||
assertNotNull(json.get("shape"));
|
||||
assertNotNull(json.get("data"));
|
||||
assertNotNull(json.get("dtype"));
|
||||
|
||||
Tensor restored = Tensor.fromJson(json);
|
||||
assertArrayEquals(original.getShape(), restored.getShape());
|
||||
assertArrayEquals(original.getData(), restored.getData(), 0.0001);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Converts to string representation")
|
||||
void testToString() {
|
||||
Tensor tensor = Tensor.zeros(new int[]{2, 3});
|
||||
String str = tensor.toString();
|
||||
|
||||
assertTrue(str.contains("Tensor"));
|
||||
assertTrue(str.contains("2, 3") || str.contains("[2,3]"));
|
||||
}
|
||||
}
|
||||
255
sdk/java/src/test/java/io/synor/compute/TypesTest.java
Normal file
255
sdk/java/src/test/java/io/synor/compute/TypesTest.java
Normal file
|
|
@ -0,0 +1,255 @@
|
|||
package io.synor.compute;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.DisplayName;
|
||||
import static org.junit.jupiter.api.Assertions.*;
|
||||
|
||||
@DisplayName("Types Tests")
|
||||
class TypesTest {
|
||||
|
||||
@Test
|
||||
@DisplayName("ProcessorType has all values")
|
||||
void testProcessorTypeValues() {
|
||||
assertEquals("cpu", ProcessorType.CPU.getValue());
|
||||
assertEquals("gpu", ProcessorType.GPU.getValue());
|
||||
assertEquals("tpu", ProcessorType.TPU.getValue());
|
||||
assertEquals("npu", ProcessorType.NPU.getValue());
|
||||
assertEquals("lpu", ProcessorType.LPU.getValue());
|
||||
assertEquals("auto", ProcessorType.AUTO.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ProcessorType parses from string")
|
||||
void testProcessorTypeFromString() {
|
||||
assertEquals(ProcessorType.GPU, ProcessorType.fromString("gpu"));
|
||||
assertEquals(ProcessorType.TPU, ProcessorType.fromString("tpu"));
|
||||
assertEquals(ProcessorType.AUTO, ProcessorType.fromString("invalid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Precision has all values")
|
||||
void testPrecisionValues() {
|
||||
assertEquals("fp64", Precision.FP64.getValue());
|
||||
assertEquals("fp32", Precision.FP32.getValue());
|
||||
assertEquals("fp16", Precision.FP16.getValue());
|
||||
assertEquals("bf16", Precision.BF16.getValue());
|
||||
assertEquals("int8", Precision.INT8.getValue());
|
||||
assertEquals("int4", Precision.INT4.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Precision parses from string")
|
||||
void testPrecisionFromString() {
|
||||
assertEquals(Precision.FP16, Precision.fromString("fp16"));
|
||||
assertEquals(Precision.BF16, Precision.fromString("bf16"));
|
||||
assertEquals(Precision.FP32, Precision.fromString("invalid"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Priority has all values")
|
||||
void testPriorityValues() {
|
||||
assertEquals("critical", Priority.CRITICAL.getValue());
|
||||
assertEquals("high", Priority.HIGH.getValue());
|
||||
assertEquals("normal", Priority.NORMAL.getValue());
|
||||
assertEquals("low", Priority.LOW.getValue());
|
||||
assertEquals("background", Priority.BACKGROUND.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("JobStatus has all values")
|
||||
void testJobStatusValues() {
|
||||
assertEquals("pending", JobStatus.PENDING.getValue());
|
||||
assertEquals("queued", JobStatus.QUEUED.getValue());
|
||||
assertEquals("running", JobStatus.RUNNING.getValue());
|
||||
assertEquals("completed", JobStatus.COMPLETED.getValue());
|
||||
assertEquals("failed", JobStatus.FAILED.getValue());
|
||||
assertEquals("cancelled", JobStatus.CANCELLED.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("JobStatus identifies terminal states")
|
||||
void testJobStatusTerminal() {
|
||||
assertTrue(JobStatus.COMPLETED.isTerminal());
|
||||
assertTrue(JobStatus.FAILED.isTerminal());
|
||||
assertTrue(JobStatus.CANCELLED.isTerminal());
|
||||
assertFalse(JobStatus.PENDING.isTerminal());
|
||||
assertFalse(JobStatus.RUNNING.isTerminal());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ModelCategory has all values")
|
||||
void testModelCategoryValues() {
|
||||
assertEquals("llm", ModelCategory.LLM.getValue());
|
||||
assertEquals("embedding", ModelCategory.EMBEDDING.getValue());
|
||||
assertEquals("image_generation", ModelCategory.IMAGE_GENERATION.getValue());
|
||||
assertEquals("custom", ModelCategory.CUSTOM.getValue());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SynorConfig creates with defaults")
|
||||
void testSynorConfigDefaults() {
|
||||
SynorConfig config = SynorConfig.builder()
|
||||
.apiKey("test-key")
|
||||
.build();
|
||||
|
||||
assertEquals("test-key", config.getApiKey());
|
||||
assertEquals("https://api.synor.io/compute/v1", config.getBaseUrl());
|
||||
assertEquals(30, config.getTimeoutSeconds());
|
||||
assertEquals(ProcessorType.AUTO, config.getDefaultProcessor());
|
||||
assertEquals(Precision.FP32, config.getDefaultPrecision());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("SynorConfig creates with custom values")
|
||||
void testSynorConfigCustom() {
|
||||
SynorConfig config = SynorConfig.builder()
|
||||
.apiKey("test-key")
|
||||
.baseUrl("https://custom.api.com")
|
||||
.timeoutSeconds(60)
|
||||
.defaultProcessor(ProcessorType.GPU)
|
||||
.defaultPrecision(Precision.FP16)
|
||||
.debug(true)
|
||||
.build();
|
||||
|
||||
assertEquals("https://custom.api.com", config.getBaseUrl());
|
||||
assertEquals(60, config.getTimeoutSeconds());
|
||||
assertEquals(ProcessorType.GPU, config.getDefaultProcessor());
|
||||
assertTrue(config.isDebug());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("MatMulOptions generates correct JSON")
|
||||
void testMatMulOptions() {
|
||||
MatMulOptions options = MatMulOptions.builder()
|
||||
.precision(Precision.FP16)
|
||||
.processor(ProcessorType.GPU)
|
||||
.priority(Priority.HIGH)
|
||||
.build();
|
||||
|
||||
var json = options.toJson();
|
||||
assertEquals("fp16", json.get("precision"));
|
||||
assertEquals("gpu", json.get("processor"));
|
||||
assertEquals("high", json.get("priority"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("Conv2dOptions has correct defaults")
|
||||
void testConv2dOptionsDefaults() {
|
||||
Conv2dOptions options = Conv2dOptions.builder().build();
|
||||
|
||||
assertArrayEquals(new int[]{1, 1}, options.getStride());
|
||||
assertArrayEquals(new int[]{0, 0}, options.getPadding());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("AttentionOptions generates correct JSON")
|
||||
void testAttentionOptions() {
|
||||
AttentionOptions options = AttentionOptions.builder()
|
||||
.numHeads(8)
|
||||
.flash(true)
|
||||
.precision(Precision.FP16)
|
||||
.build();
|
||||
|
||||
var json = options.toJson();
|
||||
assertEquals(8, json.get("num_heads"));
|
||||
assertEquals(true, json.get("flash"));
|
||||
assertEquals("fp16", json.get("precision"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("InferenceOptions has correct defaults")
|
||||
void testInferenceOptionsDefaults() {
|
||||
InferenceOptions options = InferenceOptions.builder().build();
|
||||
|
||||
assertEquals(256, options.getMaxTokens());
|
||||
assertEquals(0.7, options.getTemperature(), 0.001);
|
||||
assertEquals(0.9, options.getTopP(), 0.001);
|
||||
assertEquals(50, options.getTopK());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("InferenceOptions generates correct JSON")
|
||||
void testInferenceOptionsJson() {
|
||||
InferenceOptions options = InferenceOptions.builder()
|
||||
.maxTokens(512)
|
||||
.temperature(0.8)
|
||||
.topK(40)
|
||||
.build();
|
||||
|
||||
var json = options.toJson();
|
||||
assertEquals(512, json.get("max_tokens"));
|
||||
assertEquals(0.8, ((Number)json.get("temperature")).doubleValue(), 0.001);
|
||||
assertEquals(40, json.get("top_k"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("JobResult identifies success")
|
||||
void testJobResultSuccess() {
|
||||
JobResult<String> success = JobResult.<String>builder()
|
||||
.jobId("job-123")
|
||||
.status(JobStatus.COMPLETED)
|
||||
.result("output")
|
||||
.build();
|
||||
|
||||
assertTrue(success.isSuccess());
|
||||
assertFalse(success.isFailed());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("JobResult identifies failure")
|
||||
void testJobResultFailure() {
|
||||
JobResult<String> failure = JobResult.<String>builder()
|
||||
.jobId("job-456")
|
||||
.status(JobStatus.FAILED)
|
||||
.error("Error message")
|
||||
.build();
|
||||
|
||||
assertFalse(failure.isSuccess());
|
||||
assertTrue(failure.isFailed());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("ModelInfo formats parameters correctly")
|
||||
void testModelInfoFormattedParameters() {
|
||||
ModelInfo model = ModelInfo.builder()
|
||||
.id("test")
|
||||
.name("Test Model")
|
||||
.parameters(70000000000L)
|
||||
.build();
|
||||
|
||||
assertEquals("70B", model.getFormattedParameters());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("PricingInfo parses from JSON")
|
||||
void testPricingInfoFromJson() {
|
||||
var json = new java.util.HashMap<String, Object>();
|
||||
json.put("processor", "gpu");
|
||||
json.put("price_per_second", 0.0001);
|
||||
json.put("available_units", 100);
|
||||
json.put("utilization_percent", 75.5);
|
||||
|
||||
PricingInfo pricing = PricingInfo.fromJson(json);
|
||||
|
||||
assertEquals("gpu", pricing.getProcessor());
|
||||
assertEquals(0.0001, pricing.getPricePerSecond(), 0.00001);
|
||||
assertEquals(100, pricing.getAvailableUnits());
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisplayName("UsageStats parses from JSON")
|
||||
void testUsageStatsFromJson() {
|
||||
var json = new java.util.HashMap<String, Object>();
|
||||
json.put("total_jobs", 1000);
|
||||
json.put("completed_jobs", 950);
|
||||
json.put("failed_jobs", 50);
|
||||
json.put("total_compute_seconds", 3600.0);
|
||||
json.put("total_cost", 0.36);
|
||||
|
||||
UsageStats stats = UsageStats.fromJson(json);
|
||||
|
||||
assertEquals(1000, stats.getTotalJobs());
|
||||
assertEquals(950, stats.getCompletedJobs());
|
||||
assertEquals(3600.0, stats.getTotalComputeSeconds(), 0.001);
|
||||
}
|
||||
}
|
||||
369
sdk/js/src/__tests__/client.test.ts
Normal file
369
sdk/js/src/__tests__/client.test.ts
Normal file
|
|
@ -0,0 +1,369 @@
|
|||
/**
|
||||
* Unit tests for SynorCompute client
|
||||
*/
|
||||
|
||||
import { describe, it, expect, vi, beforeEach } from 'vitest';
|
||||
import { SynorCompute } from '../client';
|
||||
import { Tensor } from '../tensor';
|
||||
|
||||
// Mock fetch for API calls
|
||||
const mockFetch = vi.fn();
|
||||
global.fetch = mockFetch;
|
||||
|
||||
describe('SynorCompute Client', () => {
|
||||
let client: SynorCompute;
|
||||
|
||||
beforeEach(() => {
|
||||
mockFetch.mockReset();
|
||||
client = new SynorCompute({ apiKey: 'test-api-key' });
|
||||
});
|
||||
|
||||
describe('initialization', () => {
|
||||
it('should create client with API key', () => {
|
||||
const c = new SynorCompute({ apiKey: 'my-key' });
|
||||
expect(c).toBeInstanceOf(SynorCompute);
|
||||
});
|
||||
|
||||
it('should create client with config object', () => {
|
||||
const c = new SynorCompute({
|
||||
apiKey: 'my-key',
|
||||
baseUrl: 'https://custom.api.com',
|
||||
timeout: 60000,
|
||||
});
|
||||
expect(c).toBeInstanceOf(SynorCompute);
|
||||
});
|
||||
|
||||
it('should throw without API key', () => {
|
||||
expect(() => new SynorCompute({ apiKey: '' })).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('matmul', () => {
|
||||
it('should send matmul request', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
jobId: 'job-123',
|
||||
status: 'completed',
|
||||
result: { data: 'base64result', shape: [2, 4], dtype: 'fp32' },
|
||||
executionTimeMs: 15,
|
||||
processor: 'gpu',
|
||||
}),
|
||||
});
|
||||
|
||||
const a = Tensor.random([2, 3]);
|
||||
const b = Tensor.random([3, 4]);
|
||||
const result = await client.matmul({ a, b });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
expect(result.status).toBe('completed');
|
||||
expect(result.jobId).toBe('job-123');
|
||||
});
|
||||
|
||||
it('should support precision option', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
jobId: 'job-456',
|
||||
status: 'completed',
|
||||
result: { data: 'base64', shape: [2, 2], dtype: 'fp16' },
|
||||
}),
|
||||
});
|
||||
|
||||
const a = Tensor.random([2, 2]);
|
||||
const b = Tensor.random([2, 2]);
|
||||
await client.matmul({ a, b, precision: 'fp16' });
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body);
|
||||
expect(body.precision).toBe('fp16');
|
||||
});
|
||||
|
||||
it('should support processor selection', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
jobId: 'job-789',
|
||||
status: 'completed',
|
||||
result: { data: 'base64', shape: [2, 2], dtype: 'fp32' },
|
||||
}),
|
||||
});
|
||||
|
||||
const a = Tensor.random([2, 2]);
|
||||
const b = Tensor.random([2, 2]);
|
||||
await client.matmul({ a, b, processor: 'tpu' });
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body);
|
||||
expect(body.processor).toBe('tpu');
|
||||
});
|
||||
});
|
||||
|
||||
describe('conv2d', () => {
|
||||
it('should send conv2d request', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
jobId: 'conv-123',
|
||||
status: 'completed',
|
||||
result: { data: 'base64', shape: [1, 32, 30, 30], dtype: 'fp32' },
|
||||
}),
|
||||
});
|
||||
|
||||
const input = Tensor.random([1, 3, 32, 32]);
|
||||
const kernel = Tensor.random([32, 3, 3, 3]);
|
||||
const result = await client.conv2d({ input, kernel });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
expect(result.status).toBe('completed');
|
||||
});
|
||||
|
||||
it('should support stride and padding options', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
jobId: 'conv-456',
|
||||
status: 'completed',
|
||||
result: { data: 'base64', shape: [1, 32, 16, 16], dtype: 'fp32' },
|
||||
}),
|
||||
});
|
||||
|
||||
const input = Tensor.random([1, 3, 32, 32]);
|
||||
const kernel = Tensor.random([32, 3, 3, 3]);
|
||||
await client.conv2d({
|
||||
input,
|
||||
kernel,
|
||||
stride: [2, 2],
|
||||
padding: [1, 1],
|
||||
});
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body);
|
||||
expect(body.stride).toEqual([2, 2]);
|
||||
expect(body.padding).toEqual([1, 1]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('attention', () => {
|
||||
it('should send attention request', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
jobId: 'attn-123',
|
||||
status: 'completed',
|
||||
result: { data: 'base64', shape: [1, 8, 64, 64], dtype: 'fp16' },
|
||||
}),
|
||||
});
|
||||
|
||||
const query = Tensor.random([1, 8, 64, 64]);
|
||||
const key = Tensor.random([1, 8, 64, 64]);
|
||||
const value = Tensor.random([1, 8, 64, 64]);
|
||||
const result = await client.attention({ query, key, value });
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
expect(result.status).toBe('completed');
|
||||
});
|
||||
|
||||
it('should support flash attention option', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
jobId: 'attn-456',
|
||||
status: 'completed',
|
||||
result: { data: 'base64', shape: [1, 8, 64, 64], dtype: 'fp16' },
|
||||
}),
|
||||
});
|
||||
|
||||
const query = Tensor.random([1, 8, 64, 64]);
|
||||
const key = Tensor.random([1, 8, 64, 64]);
|
||||
const value = Tensor.random([1, 8, 64, 64]);
|
||||
await client.attention({ query, key, value, flash: true });
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body);
|
||||
expect(body.flash).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('inference', () => {
|
||||
it('should send inference request', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
jobId: 'inf-123',
|
||||
status: 'completed',
|
||||
result: 'The answer to your question is...',
|
||||
executionTimeMs: 2500,
|
||||
processor: 'gpu',
|
||||
}),
|
||||
});
|
||||
|
||||
const result = await client.inference({
|
||||
model: 'llama-3-70b',
|
||||
prompt: 'What is the capital of France?',
|
||||
});
|
||||
|
||||
expect(mockFetch).toHaveBeenCalledTimes(1);
|
||||
expect(result.status).toBe('completed');
|
||||
expect(result.result).toContain('answer');
|
||||
});
|
||||
|
||||
it('should support inference options', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
jobId: 'inf-456',
|
||||
status: 'completed',
|
||||
result: 'Generated text...',
|
||||
}),
|
||||
});
|
||||
|
||||
await client.inference({
|
||||
model: 'llama-3-70b',
|
||||
prompt: 'Hello',
|
||||
maxTokens: 512,
|
||||
temperature: 0.8,
|
||||
topP: 0.95,
|
||||
topK: 40,
|
||||
});
|
||||
|
||||
const [url, options] = mockFetch.mock.calls[0];
|
||||
const body = JSON.parse(options.body);
|
||||
expect(body.maxTokens).toBe(512);
|
||||
expect(body.temperature).toBe(0.8);
|
||||
expect(body.topP).toBe(0.95);
|
||||
expect(body.topK).toBe(40);
|
||||
});
|
||||
});
|
||||
|
||||
describe('model registry', () => {
|
||||
it('should list models', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
models: [
|
||||
{ id: 'llama-3-70b', name: 'Llama 3 70B', category: 'llm' },
|
||||
{ id: 'mistral-7b', name: 'Mistral 7B', category: 'llm' },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const models = await client.listModels();
|
||||
expect(models).toHaveLength(2);
|
||||
expect(models[0].id).toBe('llama-3-70b');
|
||||
});
|
||||
|
||||
it('should filter models by category', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
models: [
|
||||
{ id: 'sd-xl', name: 'Stable Diffusion XL', category: 'image_generation' },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
await client.listModels({ category: 'image_generation' });
|
||||
|
||||
const [url] = mockFetch.mock.calls[0];
|
||||
expect(url).toContain('category=image_generation');
|
||||
});
|
||||
|
||||
it('should get model by ID', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
id: 'llama-3-70b',
|
||||
name: 'Llama 3 70B',
|
||||
category: 'llm',
|
||||
parameters: 70000000000,
|
||||
contextLength: 8192,
|
||||
}),
|
||||
});
|
||||
|
||||
const model = await client.getModel('llama-3-70b');
|
||||
expect(model.id).toBe('llama-3-70b');
|
||||
expect(model.parameters).toBe(70000000000);
|
||||
});
|
||||
});
|
||||
|
||||
describe('pricing and usage', () => {
|
||||
it('should get pricing', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
pricing: [
|
||||
{ processor: 'gpu', pricePerSecond: 0.0001, availableUnits: 100 },
|
||||
{ processor: 'tpu', pricePerSecond: 0.0002, availableUnits: 50 },
|
||||
],
|
||||
}),
|
||||
});
|
||||
|
||||
const pricing = await client.getPricing();
|
||||
expect(pricing).toHaveLength(2);
|
||||
expect(pricing[0].processor).toBe('gpu');
|
||||
});
|
||||
|
||||
it('should get usage', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({
|
||||
totalJobs: 1000,
|
||||
completedJobs: 950,
|
||||
failedJobs: 50,
|
||||
totalComputeSeconds: 3600,
|
||||
totalCost: 0.36,
|
||||
}),
|
||||
});
|
||||
|
||||
const usage = await client.getUsage();
|
||||
expect(usage.totalJobs).toBe(1000);
|
||||
expect(usage.completedJobs).toBe(950);
|
||||
});
|
||||
});
|
||||
|
||||
describe('error handling', () => {
|
||||
it('should handle API errors', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
status: 401,
|
||||
json: () => Promise.resolve({ error: 'Invalid API key' }),
|
||||
});
|
||||
|
||||
const a = Tensor.random([2, 2]);
|
||||
const b = Tensor.random([2, 2]);
|
||||
|
||||
await expect(client.matmul({ a, b })).rejects.toThrow();
|
||||
});
|
||||
|
||||
it('should handle network errors', async () => {
|
||||
mockFetch.mockRejectedValueOnce(new Error('Network error'));
|
||||
|
||||
const a = Tensor.random([2, 2]);
|
||||
const b = Tensor.random([2, 2]);
|
||||
|
||||
await expect(client.matmul({ a, b })).rejects.toThrow('Network error');
|
||||
});
|
||||
});
|
||||
|
||||
describe('health check', () => {
|
||||
it('should return true for healthy service', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: true,
|
||||
json: () => Promise.resolve({ status: 'healthy' }),
|
||||
});
|
||||
|
||||
const healthy = await client.healthCheck();
|
||||
expect(healthy).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for unhealthy service', async () => {
|
||||
mockFetch.mockResolvedValueOnce({
|
||||
ok: false,
|
||||
});
|
||||
|
||||
const healthy = await client.healthCheck();
|
||||
expect(healthy).toBe(false);
|
||||
});
|
||||
});
|
||||
});
|
||||
175
sdk/js/src/__tests__/tensor.test.ts
Normal file
175
sdk/js/src/__tests__/tensor.test.ts
Normal file
|
|
@ -0,0 +1,175 @@
|
|||
/**
|
||||
* Unit tests for Tensor class
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import { Tensor } from '../tensor';
|
||||
|
||||
describe('Tensor', () => {
|
||||
describe('creation', () => {
|
||||
it('should create tensor from nested array', () => {
|
||||
const t = Tensor.from([[1, 2, 3], [4, 5, 6]]);
|
||||
expect(t.shape).toEqual([2, 3]);
|
||||
expect(t.size).toBe(6);
|
||||
expect(t.ndim).toBe(2);
|
||||
});
|
||||
|
||||
it('should create tensor from flat array', () => {
|
||||
const t = Tensor.from([1, 2, 3, 4]);
|
||||
expect(t.shape).toEqual([4]);
|
||||
expect(t.size).toBe(4);
|
||||
expect(t.ndim).toBe(1);
|
||||
});
|
||||
|
||||
it('should create tensor from Float32Array', () => {
|
||||
const data = new Float32Array([1.0, 2.0, 3.0]);
|
||||
const t = Tensor.from(data);
|
||||
expect(t.dtype).toBe('fp32');
|
||||
expect(t.size).toBe(3);
|
||||
});
|
||||
|
||||
it('should create tensor from Float64Array', () => {
|
||||
const data = new Float64Array([1.0, 2.0, 3.0]);
|
||||
const t = Tensor.from(data);
|
||||
expect(t.dtype).toBe('fp64');
|
||||
});
|
||||
});
|
||||
|
||||
describe('factory methods', () => {
|
||||
it('should create zeros tensor', () => {
|
||||
const t = Tensor.zeros([2, 3]);
|
||||
expect(t.shape).toEqual([2, 3]);
|
||||
expect(t.size).toBe(6);
|
||||
expect(Array.from(t.data).every(v => v === 0)).toBe(true);
|
||||
});
|
||||
|
||||
it('should create ones tensor', () => {
|
||||
const t = Tensor.ones([3, 2]);
|
||||
expect(t.shape).toEqual([3, 2]);
|
||||
expect(Array.from(t.data).every(v => v === 1)).toBe(true);
|
||||
});
|
||||
|
||||
it('should create random tensor', () => {
|
||||
const t = Tensor.random([10, 10]);
|
||||
expect(t.shape).toEqual([10, 10]);
|
||||
expect(t.size).toBe(100);
|
||||
// Values should be in [0, 1)
|
||||
expect(Array.from(t.data).every(v => v >= 0 && v < 1)).toBe(true);
|
||||
});
|
||||
|
||||
it('should create randn tensor', () => {
|
||||
const t = Tensor.randn([100]);
|
||||
expect(t.shape).toEqual([100]);
|
||||
// Check approximate normal distribution properties
|
||||
const values = Array.from(t.data);
|
||||
const mean = values.reduce((a, b) => a + b) / values.length;
|
||||
expect(Math.abs(mean)).toBeLessThan(0.5); // Should be close to 0
|
||||
});
|
||||
|
||||
it('should support different dtypes', () => {
|
||||
expect(Tensor.zeros([2, 2], 'fp64').dtype).toBe('fp64');
|
||||
expect(Tensor.zeros([2, 2], 'fp32').dtype).toBe('fp32');
|
||||
expect(Tensor.zeros([2, 2], 'int8').dtype).toBe('int8');
|
||||
});
|
||||
});
|
||||
|
||||
describe('operations', () => {
|
||||
it('should reshape tensor', () => {
|
||||
const t = Tensor.from([1, 2, 3, 4, 5, 6]);
|
||||
const reshaped = t.reshape([2, 3]);
|
||||
expect(reshaped.shape).toEqual([2, 3]);
|
||||
expect(reshaped.size).toBe(6);
|
||||
});
|
||||
|
||||
it('should throw on invalid reshape', () => {
|
||||
const t = Tensor.from([1, 2, 3, 4]);
|
||||
expect(() => t.reshape([2, 3])).toThrow();
|
||||
});
|
||||
|
||||
it('should convert dtype', () => {
|
||||
const t = Tensor.from([1.5, 2.5, 3.5], 'fp32');
|
||||
const converted = t.to('fp64');
|
||||
expect(converted.dtype).toBe('fp64');
|
||||
expect(converted.data).toBeInstanceOf(Float64Array);
|
||||
});
|
||||
|
||||
it('should get element at index', () => {
|
||||
const t = Tensor.from([[1, 2, 3], [4, 5, 6]]);
|
||||
expect(t.get(0, 0)).toBe(1);
|
||||
expect(t.get(0, 2)).toBe(3);
|
||||
expect(t.get(1, 1)).toBe(5);
|
||||
});
|
||||
|
||||
it('should set element at index', () => {
|
||||
const t = Tensor.from([[1, 2], [3, 4]]);
|
||||
t.set(99, 0, 1);
|
||||
expect(t.get(0, 1)).toBe(99);
|
||||
});
|
||||
|
||||
it('should convert to array', () => {
|
||||
const t = Tensor.from([[1, 2], [3, 4]]);
|
||||
const arr = t.toArray();
|
||||
expect(arr).toEqual([[1, 2], [3, 4]]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('properties', () => {
|
||||
it('should calculate byteSize correctly', () => {
|
||||
const fp32 = Tensor.zeros([10], 'fp32');
|
||||
expect(fp32.byteSize).toBe(40); // 10 * 4 bytes
|
||||
|
||||
const fp64 = Tensor.zeros([10], 'fp64');
|
||||
expect(fp64.byteSize).toBe(80); // 10 * 8 bytes
|
||||
});
|
||||
});
|
||||
|
||||
describe('serialization', () => {
|
||||
it('should serialize and deserialize tensor', () => {
|
||||
const original = Tensor.from([[1, 2, 3], [4, 5, 6]]);
|
||||
const serialized = original.serialize();
|
||||
|
||||
expect(serialized).toHaveProperty('data');
|
||||
expect(serialized).toHaveProperty('shape');
|
||||
expect(serialized).toHaveProperty('dtype');
|
||||
expect(serialized.shape).toEqual([2, 3]);
|
||||
|
||||
const restored = Tensor.deserialize(serialized);
|
||||
expect(restored.shape).toEqual(original.shape);
|
||||
expect(restored.dtype).toBe(original.dtype);
|
||||
expect(Array.from(restored.data)).toEqual(Array.from(original.data));
|
||||
});
|
||||
|
||||
it('should preserve dtype during serialization', () => {
|
||||
const fp64Tensor = Tensor.zeros([3], 'fp64');
|
||||
fp64Tensor.set(1.234567890123, 0);
|
||||
|
||||
const serialized = fp64Tensor.serialize();
|
||||
const restored = Tensor.deserialize(serialized);
|
||||
|
||||
expect(restored.dtype).toBe('fp64');
|
||||
});
|
||||
});
|
||||
|
||||
describe('edge cases', () => {
|
||||
it('should handle scalar-like tensor', () => {
|
||||
const t = Tensor.from([42]);
|
||||
expect(t.shape).toEqual([1]);
|
||||
expect(t.size).toBe(1);
|
||||
});
|
||||
|
||||
it('should handle large tensors', () => {
|
||||
const t = Tensor.zeros([1000, 1000]);
|
||||
expect(t.size).toBe(1000000);
|
||||
});
|
||||
|
||||
it('should throw on out-of-bounds access', () => {
|
||||
const t = Tensor.from([1, 2, 3]);
|
||||
expect(() => t.get(5)).toThrow();
|
||||
});
|
||||
|
||||
it('should throw on wrong number of indices', () => {
|
||||
const t = Tensor.from([[1, 2], [3, 4]]);
|
||||
expect(() => t.get(0)).toThrow();
|
||||
});
|
||||
});
|
||||
});
|
||||
109
sdk/js/src/__tests__/types.test.ts
Normal file
109
sdk/js/src/__tests__/types.test.ts
Normal file
|
|
@ -0,0 +1,109 @@
|
|||
/**
|
||||
* Unit tests for SDK types
|
||||
*/
|
||||
|
||||
import { describe, it, expect } from 'vitest';
|
||||
import type {
|
||||
ProcessorType,
|
||||
Precision,
|
||||
BalancingStrategy,
|
||||
TaskPriority,
|
||||
JobStatus,
|
||||
SynorConfig,
|
||||
MatMulRequest,
|
||||
InferenceRequest,
|
||||
} from '../types';
|
||||
|
||||
describe('Types', () => {
|
||||
describe('ProcessorType', () => {
|
||||
it('should accept valid processor types', () => {
|
||||
const processors: ProcessorType[] = [
|
||||
'cpu', 'gpu', 'tpu', 'npu', 'lpu', 'fpga', 'dsp', 'webgpu', 'wasm', 'auto'
|
||||
];
|
||||
expect(processors).toHaveLength(10);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Precision', () => {
|
||||
it('should accept valid precision types', () => {
|
||||
const precisions: Precision[] = [
|
||||
'fp64', 'fp32', 'fp16', 'bf16', 'int8', 'int4'
|
||||
];
|
||||
expect(precisions).toHaveLength(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('BalancingStrategy', () => {
|
||||
it('should accept valid balancing strategies', () => {
|
||||
const strategies: BalancingStrategy[] = [
|
||||
'cost', 'latency', 'energy', 'throughput', 'auto'
|
||||
];
|
||||
expect(strategies).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('TaskPriority', () => {
|
||||
it('should accept valid priority levels', () => {
|
||||
const priorities: TaskPriority[] = [
|
||||
'critical', 'high', 'normal', 'low', 'background'
|
||||
];
|
||||
expect(priorities).toHaveLength(5);
|
||||
});
|
||||
});
|
||||
|
||||
describe('JobStatus', () => {
|
||||
it('should accept valid job statuses', () => {
|
||||
const statuses: JobStatus[] = [
|
||||
'pending', 'queued', 'running', 'completed', 'failed', 'cancelled'
|
||||
];
|
||||
expect(statuses).toHaveLength(6);
|
||||
});
|
||||
});
|
||||
|
||||
describe('SynorConfig', () => {
|
||||
it('should create valid config object', () => {
|
||||
const config: SynorConfig = {
|
||||
apiKey: 'test-api-key',
|
||||
baseUrl: 'https://api.synor.io/compute/v1',
|
||||
timeout: 30000,
|
||||
};
|
||||
expect(config.apiKey).toBe('test-api-key');
|
||||
expect(config.baseUrl).toBeDefined();
|
||||
});
|
||||
|
||||
it('should allow optional fields', () => {
|
||||
const config: SynorConfig = {
|
||||
apiKey: 'test-api-key',
|
||||
};
|
||||
expect(config.apiKey).toBe('test-api-key');
|
||||
});
|
||||
});
|
||||
|
||||
describe('MatMulRequest', () => {
|
||||
it('should create valid matmul request', () => {
|
||||
const request: MatMulRequest = {
|
||||
a: { data: 'base64', shape: [2, 3], dtype: 'fp32' },
|
||||
b: { data: 'base64', shape: [3, 4], dtype: 'fp32' },
|
||||
precision: 'fp16',
|
||||
processor: 'gpu',
|
||||
};
|
||||
expect(request.a.shape).toEqual([2, 3]);
|
||||
expect(request.b.shape).toEqual([3, 4]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('InferenceRequest', () => {
|
||||
it('should create valid inference request', () => {
|
||||
const request: InferenceRequest = {
|
||||
model: 'llama-3-70b',
|
||||
prompt: 'Hello, world!',
|
||||
maxTokens: 256,
|
||||
temperature: 0.7,
|
||||
topP: 0.9,
|
||||
topK: 50,
|
||||
};
|
||||
expect(request.model).toBe('llama-3-70b');
|
||||
expect(request.maxTokens).toBe(256);
|
||||
});
|
||||
});
|
||||
});
|
||||
1
sdk/python/tests/__init__.py
Normal file
1
sdk/python/tests/__init__.py
Normal file
|
|
@ -0,0 +1 @@
|
|||
# Tests for Synor Compute SDK
|
||||
382
sdk/python/tests/test_client.py
Normal file
382
sdk/python/tests/test_client.py
Normal file
|
|
@ -0,0 +1,382 @@
|
|||
"""Unit tests for SynorCompute client."""
|
||||
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
import numpy as np
|
||||
|
||||
from synor_compute import SynorCompute, SynorError, Tensor
|
||||
from synor_compute.types import (
|
||||
ProcessorType,
|
||||
Precision,
|
||||
JobStatus,
|
||||
SynorConfig,
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_response():
|
||||
"""Create a mock HTTP response."""
|
||||
def _create_response(data, status=200):
|
||||
response = AsyncMock()
|
||||
response.status = status
|
||||
response.json = AsyncMock(return_value=data)
|
||||
response.text = AsyncMock(return_value=str(data))
|
||||
return response
|
||||
return _create_response
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def client():
|
||||
"""Create a test client."""
|
||||
return SynorCompute(api_key="test-api-key")
|
||||
|
||||
|
||||
class TestClientInitialization:
|
||||
"""Tests for client initialization."""
|
||||
|
||||
def test_create_with_api_key(self):
|
||||
"""Should create client with API key."""
|
||||
client = SynorCompute(api_key="my-key")
|
||||
assert client is not None
|
||||
|
||||
def test_create_with_config(self):
|
||||
"""Should create client with config."""
|
||||
config = SynorConfig(
|
||||
api_key="my-key",
|
||||
base_url="https://custom.api.com",
|
||||
timeout=60,
|
||||
)
|
||||
client = SynorCompute(config=config)
|
||||
assert client is not None
|
||||
|
||||
def test_raise_without_api_key(self):
|
||||
"""Should raise error without API key."""
|
||||
with pytest.raises((ValueError, TypeError)):
|
||||
SynorCompute(api_key="")
|
||||
|
||||
|
||||
class TestMatMul:
|
||||
"""Tests for matrix multiplication."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matmul_request(self, client, mock_response):
|
||||
"""Should send matmul request."""
|
||||
response_data = {
|
||||
"job_id": "job-123",
|
||||
"status": "completed",
|
||||
"result": {"data": "base64", "shape": [2, 4], "dtype": "fp32"},
|
||||
"execution_time_ms": 15,
|
||||
"processor": "gpu",
|
||||
}
|
||||
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = response_data
|
||||
|
||||
a = Tensor.random((2, 3))
|
||||
b = Tensor.random((3, 4))
|
||||
result = await client.matmul(a, b)
|
||||
|
||||
mock_req.assert_called_once()
|
||||
assert result.status == JobStatus.COMPLETED
|
||||
assert result.job_id == "job-123"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matmul_with_precision(self, client):
|
||||
"""Should support precision option."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"job_id": "job-456",
|
||||
"status": "completed",
|
||||
"result": {"data": "base64", "shape": [2, 2], "dtype": "fp16"},
|
||||
}
|
||||
|
||||
a = Tensor.random((2, 2))
|
||||
b = Tensor.random((2, 2))
|
||||
await client.matmul(a, b, precision=Precision.FP16)
|
||||
|
||||
call_args = mock_req.call_args
|
||||
assert call_args[1].get("precision") == "fp16" or \
|
||||
call_args[0][1].get("precision") == "fp16"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_matmul_with_processor(self, client):
|
||||
"""Should support processor selection."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"job_id": "job-789",
|
||||
"status": "completed",
|
||||
"result": {"data": "base64", "shape": [2, 2], "dtype": "fp32"},
|
||||
}
|
||||
|
||||
a = Tensor.random((2, 2))
|
||||
b = Tensor.random((2, 2))
|
||||
await client.matmul(a, b, processor=ProcessorType.TPU)
|
||||
|
||||
mock_req.assert_called_once()
|
||||
|
||||
|
||||
class TestConv2d:
|
||||
"""Tests for 2D convolution."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_conv2d_request(self, client):
|
||||
"""Should send conv2d request."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"job_id": "conv-123",
|
||||
"status": "completed",
|
||||
"result": {"data": "base64", "shape": [1, 32, 30, 30], "dtype": "fp32"},
|
||||
}
|
||||
|
||||
input_tensor = Tensor.random((1, 3, 32, 32))
|
||||
kernel = Tensor.random((32, 3, 3, 3))
|
||||
result = await client.conv2d(input_tensor, kernel)
|
||||
|
||||
assert result.status == JobStatus.COMPLETED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_conv2d_with_options(self, client):
|
||||
"""Should support stride and padding options."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"job_id": "conv-456",
|
||||
"status": "completed",
|
||||
"result": {"data": "base64", "shape": [1, 32, 16, 16], "dtype": "fp32"},
|
||||
}
|
||||
|
||||
input_tensor = Tensor.random((1, 3, 32, 32))
|
||||
kernel = Tensor.random((32, 3, 3, 3))
|
||||
await client.conv2d(
|
||||
input_tensor,
|
||||
kernel,
|
||||
stride=(2, 2),
|
||||
padding=(1, 1),
|
||||
)
|
||||
|
||||
mock_req.assert_called_once()
|
||||
|
||||
|
||||
class TestAttention:
|
||||
"""Tests for attention operation."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_attention_request(self, client):
|
||||
"""Should send attention request."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"job_id": "attn-123",
|
||||
"status": "completed",
|
||||
"result": {"data": "base64", "shape": [1, 8, 64, 64], "dtype": "fp16"},
|
||||
}
|
||||
|
||||
query = Tensor.random((1, 8, 64, 64))
|
||||
key = Tensor.random((1, 8, 64, 64))
|
||||
value = Tensor.random((1, 8, 64, 64))
|
||||
result = await client.attention(query, key, value)
|
||||
|
||||
assert result.status == JobStatus.COMPLETED
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_attention_flash(self, client):
|
||||
"""Should support flash attention option."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"job_id": "attn-456",
|
||||
"status": "completed",
|
||||
"result": {"data": "base64", "shape": [1, 8, 64, 64], "dtype": "fp16"},
|
||||
}
|
||||
|
||||
query = Tensor.random((1, 8, 64, 64))
|
||||
key = Tensor.random((1, 8, 64, 64))
|
||||
value = Tensor.random((1, 8, 64, 64))
|
||||
await client.attention(query, key, value, flash=True)
|
||||
|
||||
mock_req.assert_called_once()
|
||||
|
||||
|
||||
class TestInference:
|
||||
"""Tests for LLM inference."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inference_request(self, client):
|
||||
"""Should send inference request."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"job_id": "inf-123",
|
||||
"status": "completed",
|
||||
"result": "The answer to your question is...",
|
||||
"execution_time_ms": 2500,
|
||||
"processor": "gpu",
|
||||
}
|
||||
|
||||
result = await client.inference(
|
||||
model="llama-3-70b",
|
||||
prompt="What is the capital of France?",
|
||||
)
|
||||
|
||||
assert result.status == JobStatus.COMPLETED
|
||||
assert "answer" in result.result
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_inference_with_options(self, client):
|
||||
"""Should support inference options."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"job_id": "inf-456",
|
||||
"status": "completed",
|
||||
"result": "Generated text...",
|
||||
}
|
||||
|
||||
await client.inference(
|
||||
model="llama-3-70b",
|
||||
prompt="Hello",
|
||||
max_tokens=512,
|
||||
temperature=0.8,
|
||||
top_p=0.95,
|
||||
top_k=40,
|
||||
)
|
||||
|
||||
mock_req.assert_called_once()
|
||||
|
||||
|
||||
class TestModelRegistry:
|
||||
"""Tests for model registry operations."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_models(self, client):
|
||||
"""Should list available models."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"models": [
|
||||
{"id": "llama-3-70b", "name": "Llama 3 70B", "category": "llm"},
|
||||
{"id": "mistral-7b", "name": "Mistral 7B", "category": "llm"},
|
||||
]
|
||||
}
|
||||
|
||||
models = await client.list_models()
|
||||
|
||||
assert len(models) == 2
|
||||
assert models[0]["id"] == "llama-3-70b"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_list_models_by_category(self, client):
|
||||
"""Should filter models by category."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"models": [
|
||||
{"id": "sd-xl", "name": "Stable Diffusion XL", "category": "image_generation"},
|
||||
]
|
||||
}
|
||||
|
||||
models = await client.list_models(category="image_generation")
|
||||
|
||||
assert len(models) == 1
|
||||
assert models[0]["category"] == "image_generation"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_model(self, client):
|
||||
"""Should get model by ID."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"id": "llama-3-70b",
|
||||
"name": "Llama 3 70B",
|
||||
"category": "llm",
|
||||
"parameters": 70000000000,
|
||||
"context_length": 8192,
|
||||
}
|
||||
|
||||
model = await client.get_model("llama-3-70b")
|
||||
|
||||
assert model["id"] == "llama-3-70b"
|
||||
assert model["parameters"] == 70000000000
|
||||
|
||||
|
||||
class TestPricingAndUsage:
|
||||
"""Tests for pricing and usage APIs."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_pricing(self, client):
|
||||
"""Should get pricing information."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"pricing": [
|
||||
{"processor": "gpu", "price_per_second": 0.0001, "available_units": 100},
|
||||
{"processor": "tpu", "price_per_second": 0.0002, "available_units": 50},
|
||||
]
|
||||
}
|
||||
|
||||
pricing = await client.get_pricing()
|
||||
|
||||
assert len(pricing) == 2
|
||||
assert pricing[0]["processor"] == "gpu"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_get_usage(self, client):
|
||||
"""Should get usage statistics."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {
|
||||
"total_jobs": 1000,
|
||||
"completed_jobs": 950,
|
||||
"failed_jobs": 50,
|
||||
"total_compute_seconds": 3600,
|
||||
"total_cost": 0.36,
|
||||
}
|
||||
|
||||
usage = await client.get_usage()
|
||||
|
||||
assert usage["total_jobs"] == 1000
|
||||
assert usage["completed_jobs"] == 950
|
||||
|
||||
|
||||
class TestErrorHandling:
|
||||
"""Tests for error handling."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_api_error(self, client):
|
||||
"""Should handle API errors."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.side_effect = SynorError("Invalid API key", status_code=401)
|
||||
|
||||
a = Tensor.random((2, 2))
|
||||
b = Tensor.random((2, 2))
|
||||
|
||||
with pytest.raises(SynorError) as exc_info:
|
||||
await client.matmul(a, b)
|
||||
|
||||
assert exc_info.value.status_code == 401
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_network_error(self, client):
|
||||
"""Should handle network errors."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.side_effect = Exception("Network error")
|
||||
|
||||
a = Tensor.random((2, 2))
|
||||
b = Tensor.random((2, 2))
|
||||
|
||||
with pytest.raises(Exception, match="Network error"):
|
||||
await client.matmul(a, b)
|
||||
|
||||
|
||||
class TestHealthCheck:
|
||||
"""Tests for health check."""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_healthy_service(self, client):
|
||||
"""Should return True for healthy service."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.return_value = {"status": "healthy"}
|
||||
|
||||
healthy = await client.health_check()
|
||||
|
||||
assert healthy is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_unhealthy_service(self, client):
|
||||
"""Should return False for unhealthy service."""
|
||||
with patch.object(client, "_request", new_callable=AsyncMock) as mock_req:
|
||||
mock_req.side_effect = Exception("Service unavailable")
|
||||
|
||||
healthy = await client.health_check()
|
||||
|
||||
assert healthy is False
|
||||
239
sdk/python/tests/test_tensor.py
Normal file
239
sdk/python/tests/test_tensor.py
Normal file
|
|
@ -0,0 +1,239 @@
|
|||
"""Unit tests for Tensor class."""
|
||||
|
||||
import pytest
|
||||
import numpy as np
|
||||
from synor_compute import Tensor
|
||||
from synor_compute.types import Precision
|
||||
|
||||
|
||||
class TestTensorCreation:
|
||||
"""Tests for tensor creation methods."""
|
||||
|
||||
def test_from_numpy_array(self):
|
||||
"""Should create tensor from numpy array."""
|
||||
arr = np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32)
|
||||
tensor = Tensor.from_numpy(arr)
|
||||
|
||||
assert tensor.shape == (2, 3)
|
||||
assert tensor.size == 6
|
||||
assert tensor.ndim == 2
|
||||
assert tensor.dtype == Precision.FP32
|
||||
|
||||
def test_from_numpy_with_dtype(self):
|
||||
"""Should create tensor with specified dtype."""
|
||||
arr = np.array([1, 2, 3])
|
||||
tensor = Tensor.from_numpy(arr, dtype=Precision.FP16)
|
||||
|
||||
assert tensor.dtype == Precision.FP16
|
||||
|
||||
def test_infer_dtype_fp64(self):
|
||||
"""Should infer FP64 from float64 array."""
|
||||
arr = np.array([1.0, 2.0], dtype=np.float64)
|
||||
tensor = Tensor.from_numpy(arr)
|
||||
|
||||
assert tensor.dtype == Precision.FP64
|
||||
|
||||
def test_infer_dtype_int8(self):
|
||||
"""Should infer INT8 from int8 array."""
|
||||
arr = np.array([1, 2, 3], dtype=np.int8)
|
||||
tensor = Tensor.from_numpy(arr)
|
||||
|
||||
assert tensor.dtype == Precision.INT8
|
||||
|
||||
|
||||
class TestTensorFactoryMethods:
|
||||
"""Tests for tensor factory methods."""
|
||||
|
||||
def test_zeros(self):
|
||||
"""Should create tensor of zeros."""
|
||||
tensor = Tensor.zeros((2, 3))
|
||||
|
||||
assert tensor.shape == (2, 3)
|
||||
assert np.allclose(tensor.data, 0)
|
||||
|
||||
def test_zeros_with_dtype(self):
|
||||
"""Should create zeros tensor with specified dtype."""
|
||||
tensor = Tensor.zeros((2, 2), dtype=Precision.FP64)
|
||||
|
||||
assert tensor.dtype == Precision.FP64
|
||||
assert tensor.data.dtype == np.float64
|
||||
|
||||
def test_ones(self):
|
||||
"""Should create tensor of ones."""
|
||||
tensor = Tensor.ones((3, 2))
|
||||
|
||||
assert tensor.shape == (3, 2)
|
||||
assert np.allclose(tensor.data, 1)
|
||||
|
||||
def test_random(self):
|
||||
"""Should create tensor with random values in [0, 1)."""
|
||||
tensor = Tensor.random((10, 10))
|
||||
|
||||
assert tensor.shape == (10, 10)
|
||||
assert tensor.data.min() >= 0
|
||||
assert tensor.data.max() < 1
|
||||
|
||||
def test_randn(self):
|
||||
"""Should create tensor with normal distribution."""
|
||||
tensor = Tensor.randn((1000,))
|
||||
|
||||
# Check approximate normal distribution properties
|
||||
assert abs(tensor.data.mean()) < 0.2 # Mean ~= 0
|
||||
assert 0.8 < tensor.data.std() < 1.2 # Std ~= 1
|
||||
|
||||
|
||||
class TestTensorOperations:
|
||||
"""Tests for tensor operations."""
|
||||
|
||||
def test_reshape(self):
|
||||
"""Should reshape tensor."""
|
||||
tensor = Tensor.from_numpy(np.arange(6))
|
||||
reshaped = tensor.reshape((2, 3))
|
||||
|
||||
assert reshaped.shape == (2, 3)
|
||||
assert reshaped.size == 6
|
||||
|
||||
def test_reshape_preserves_data(self):
|
||||
"""Should preserve data when reshaping."""
|
||||
arr = np.array([1, 2, 3, 4, 5, 6])
|
||||
tensor = Tensor.from_numpy(arr)
|
||||
reshaped = tensor.reshape((2, 3))
|
||||
|
||||
assert reshaped.data[0, 0] == 1
|
||||
assert reshaped.data[1, 2] == 6
|
||||
|
||||
def test_to_different_dtype(self):
|
||||
"""Should convert to different dtype."""
|
||||
tensor = Tensor.from_numpy(np.array([1.5, 2.5], dtype=np.float32))
|
||||
converted = tensor.to(Precision.FP64)
|
||||
|
||||
assert converted.dtype == Precision.FP64
|
||||
assert converted.data.dtype == np.float64
|
||||
|
||||
def test_to_same_dtype_returns_self(self):
|
||||
"""Should return self when converting to same dtype."""
|
||||
tensor = Tensor.from_numpy(np.array([1.0, 2.0], dtype=np.float32))
|
||||
same = tensor.to(Precision.FP32)
|
||||
|
||||
assert same is tensor
|
||||
|
||||
def test_numpy_returns_underlying_array(self):
|
||||
"""Should return underlying numpy array."""
|
||||
arr = np.array([1, 2, 3], dtype=np.float32)
|
||||
tensor = Tensor.from_numpy(arr)
|
||||
|
||||
assert np.array_equal(tensor.numpy(), arr)
|
||||
|
||||
|
||||
class TestTensorProperties:
|
||||
"""Tests for tensor properties."""
|
||||
|
||||
def test_shape(self):
|
||||
"""Should return correct shape."""
|
||||
tensor = Tensor.zeros((3, 4, 5))
|
||||
assert tensor.shape == (3, 4, 5)
|
||||
|
||||
def test_size(self):
|
||||
"""Should return correct size."""
|
||||
tensor = Tensor.zeros((3, 4, 5))
|
||||
assert tensor.size == 60
|
||||
|
||||
def test_ndim(self):
|
||||
"""Should return correct number of dimensions."""
|
||||
tensor = Tensor.zeros((3, 4, 5))
|
||||
assert tensor.ndim == 3
|
||||
|
||||
def test_nbytes(self):
|
||||
"""Should return correct byte size."""
|
||||
tensor = Tensor.zeros((10,), dtype=Precision.FP32)
|
||||
assert tensor.nbytes == 40 # 10 * 4 bytes
|
||||
|
||||
|
||||
class TestTensorSerialization:
|
||||
"""Tests for tensor serialization."""
|
||||
|
||||
def test_serialize(self):
|
||||
"""Should serialize tensor to dict."""
|
||||
tensor = Tensor.from_numpy(np.array([[1, 2], [3, 4]], dtype=np.float32))
|
||||
serialized = tensor.serialize()
|
||||
|
||||
assert "data" in serialized
|
||||
assert "shape" in serialized
|
||||
assert "dtype" in serialized
|
||||
assert serialized["shape"] == [2, 2]
|
||||
assert serialized["dtype"] == "fp32"
|
||||
|
||||
def test_deserialize(self):
|
||||
"""Should deserialize tensor from dict."""
|
||||
original = Tensor.from_numpy(np.array([[1, 2, 3], [4, 5, 6]], dtype=np.float32))
|
||||
serialized = original.serialize()
|
||||
restored = Tensor.deserialize(serialized)
|
||||
|
||||
assert restored.shape == original.shape
|
||||
assert restored.dtype == original.dtype
|
||||
assert np.array_equal(restored.data, original.data)
|
||||
|
||||
def test_serialize_deserialize_fp64(self):
|
||||
"""Should preserve FP64 precision."""
|
||||
original = Tensor.from_numpy(np.array([1.234567890123456], dtype=np.float64))
|
||||
serialized = original.serialize()
|
||||
restored = Tensor.deserialize(serialized)
|
||||
|
||||
assert restored.dtype == Precision.FP64
|
||||
assert np.allclose(restored.data, original.data)
|
||||
|
||||
def test_serialize_deserialize_large_tensor(self):
|
||||
"""Should handle large tensors."""
|
||||
original = Tensor.random((100, 100))
|
||||
serialized = original.serialize()
|
||||
restored = Tensor.deserialize(serialized)
|
||||
|
||||
assert restored.shape == original.shape
|
||||
assert np.allclose(restored.data, original.data)
|
||||
|
||||
|
||||
class TestTensorRepr:
|
||||
"""Tests for tensor string representations."""
|
||||
|
||||
def test_repr(self):
|
||||
"""Should return informative repr."""
|
||||
tensor = Tensor.zeros((2, 3), dtype=Precision.FP32)
|
||||
rep = repr(tensor)
|
||||
|
||||
assert "Tensor" in rep
|
||||
assert "(2, 3)" in rep
|
||||
assert "fp32" in rep
|
||||
|
||||
def test_str(self):
|
||||
"""Should return string with data."""
|
||||
tensor = Tensor.from_numpy(np.array([1, 2, 3], dtype=np.float32))
|
||||
s = str(tensor)
|
||||
|
||||
assert "Tensor" in s
|
||||
assert "fp32" in s
|
||||
|
||||
|
||||
class TestTensorEdgeCases:
|
||||
"""Tests for edge cases."""
|
||||
|
||||
def test_scalar_like_tensor(self):
|
||||
"""Should handle 1-element tensor."""
|
||||
tensor = Tensor.from_numpy(np.array([42], dtype=np.float32))
|
||||
|
||||
assert tensor.shape == (1,)
|
||||
assert tensor.size == 1
|
||||
assert tensor.ndim == 1
|
||||
|
||||
def test_empty_shape_dimension(self):
|
||||
"""Should handle tensors with zero-size dimensions."""
|
||||
tensor = Tensor.zeros((0, 3))
|
||||
|
||||
assert tensor.shape == (0, 3)
|
||||
assert tensor.size == 0
|
||||
|
||||
def test_high_dimensional_tensor(self):
|
||||
"""Should handle high-dimensional tensors."""
|
||||
tensor = Tensor.zeros((2, 3, 4, 5, 6))
|
||||
|
||||
assert tensor.ndim == 5
|
||||
assert tensor.size == 720
|
||||
212
sdk/python/tests/test_types.py
Normal file
212
sdk/python/tests/test_types.py
Normal file
|
|
@ -0,0 +1,212 @@
|
|||
"""Unit tests for SDK types."""
|
||||
|
||||
import pytest
|
||||
from synor_compute.types import (
|
||||
ProcessorType,
|
||||
Precision,
|
||||
BalancingStrategy,
|
||||
TaskPriority,
|
||||
JobStatus,
|
||||
SynorConfig,
|
||||
JobResult,
|
||||
JobMetrics,
|
||||
PricingInfo,
|
||||
)
|
||||
|
||||
|
||||
class TestProcessorType:
|
||||
"""Tests for ProcessorType enum."""
|
||||
|
||||
def test_all_processor_types(self):
|
||||
"""Should have all expected processor types."""
|
||||
expected = ["cpu", "gpu", "tpu", "npu", "lpu", "fpga", "dsp", "webgpu", "wasm", "auto"]
|
||||
actual = [p.value for p in ProcessorType]
|
||||
|
||||
for e in expected:
|
||||
assert e in actual
|
||||
|
||||
def test_processor_value(self):
|
||||
"""Should have correct values."""
|
||||
assert ProcessorType.GPU.value == "gpu"
|
||||
assert ProcessorType.TPU.value == "tpu"
|
||||
assert ProcessorType.AUTO.value == "auto"
|
||||
|
||||
|
||||
class TestPrecision:
|
||||
"""Tests for Precision enum."""
|
||||
|
||||
def test_all_precision_types(self):
|
||||
"""Should have all expected precision types."""
|
||||
expected = ["fp64", "fp32", "fp16", "bf16", "int8", "int4"]
|
||||
actual = [p.value for p in Precision]
|
||||
|
||||
for e in expected:
|
||||
assert e in actual
|
||||
|
||||
def test_precision_value(self):
|
||||
"""Should have correct values."""
|
||||
assert Precision.FP64.value == "fp64"
|
||||
assert Precision.FP32.value == "fp32"
|
||||
assert Precision.FP16.value == "fp16"
|
||||
|
||||
|
||||
class TestBalancingStrategy:
|
||||
"""Tests for BalancingStrategy enum."""
|
||||
|
||||
def test_all_strategies(self):
|
||||
"""Should have all expected strategies."""
|
||||
expected = ["cost", "latency", "energy", "throughput", "auto"]
|
||||
actual = [s.value for s in BalancingStrategy]
|
||||
|
||||
for e in expected:
|
||||
assert e in actual
|
||||
|
||||
|
||||
class TestTaskPriority:
|
||||
"""Tests for TaskPriority enum."""
|
||||
|
||||
def test_all_priorities(self):
|
||||
"""Should have all expected priorities."""
|
||||
expected = ["critical", "high", "normal", "low", "background"]
|
||||
actual = [p.value for p in TaskPriority]
|
||||
|
||||
for e in expected:
|
||||
assert e in actual
|
||||
|
||||
|
||||
class TestJobStatus:
|
||||
"""Tests for JobStatus enum."""
|
||||
|
||||
def test_all_statuses(self):
|
||||
"""Should have all expected statuses."""
|
||||
expected = ["pending", "queued", "running", "completed", "failed", "cancelled"]
|
||||
actual = [s.value for s in JobStatus]
|
||||
|
||||
for e in expected:
|
||||
assert e in actual
|
||||
|
||||
|
||||
class TestSynorConfig:
|
||||
"""Tests for SynorConfig dataclass."""
|
||||
|
||||
def test_create_with_defaults(self):
|
||||
"""Should create config with defaults."""
|
||||
config = SynorConfig(api_key="test-key")
|
||||
|
||||
assert config.api_key == "test-key"
|
||||
assert config.base_url == "https://api.synor.io/compute/v1"
|
||||
assert config.timeout == 30
|
||||
|
||||
def test_create_with_custom_values(self):
|
||||
"""Should create config with custom values."""
|
||||
config = SynorConfig(
|
||||
api_key="test-key",
|
||||
base_url="https://custom.api.com",
|
||||
timeout=60,
|
||||
default_processor=ProcessorType.GPU,
|
||||
default_precision=Precision.FP16,
|
||||
)
|
||||
|
||||
assert config.base_url == "https://custom.api.com"
|
||||
assert config.timeout == 60
|
||||
assert config.default_processor == ProcessorType.GPU
|
||||
assert config.default_precision == Precision.FP16
|
||||
|
||||
|
||||
class TestJobResult:
|
||||
"""Tests for JobResult dataclass."""
|
||||
|
||||
def test_create_job_result(self):
|
||||
"""Should create job result."""
|
||||
result = JobResult(
|
||||
job_id="job-123",
|
||||
status=JobStatus.COMPLETED,
|
||||
result={"data": "value"},
|
||||
execution_time_ms=150,
|
||||
processor=ProcessorType.GPU,
|
||||
)
|
||||
|
||||
assert result.job_id == "job-123"
|
||||
assert result.status == JobStatus.COMPLETED
|
||||
assert result.execution_time_ms == 150
|
||||
|
||||
def test_is_success(self):
|
||||
"""Should identify successful job."""
|
||||
success = JobResult(
|
||||
job_id="job-1",
|
||||
status=JobStatus.COMPLETED,
|
||||
result="data",
|
||||
)
|
||||
failure = JobResult(
|
||||
job_id="job-2",
|
||||
status=JobStatus.FAILED,
|
||||
error="Error message",
|
||||
)
|
||||
|
||||
assert success.is_success()
|
||||
assert not failure.is_success()
|
||||
|
||||
def test_is_failed(self):
|
||||
"""Should identify failed job."""
|
||||
success = JobResult(
|
||||
job_id="job-1",
|
||||
status=JobStatus.COMPLETED,
|
||||
result="data",
|
||||
)
|
||||
failure = JobResult(
|
||||
job_id="job-2",
|
||||
status=JobStatus.FAILED,
|
||||
error="Error message",
|
||||
)
|
||||
|
||||
assert not success.is_failed()
|
||||
assert failure.is_failed()
|
||||
|
||||
|
||||
class TestJobMetrics:
|
||||
"""Tests for JobMetrics dataclass."""
|
||||
|
||||
def test_create_metrics(self):
|
||||
"""Should create job metrics."""
|
||||
metrics = JobMetrics(
|
||||
queue_time_ms=10,
|
||||
compute_time_ms=100,
|
||||
transfer_time_ms=5,
|
||||
total_time_ms=115,
|
||||
flops=1e12,
|
||||
memory_used_mb=1024,
|
||||
)
|
||||
|
||||
assert metrics.compute_time_ms == 100
|
||||
assert metrics.flops == 1e12
|
||||
|
||||
|
||||
class TestPricingInfo:
|
||||
"""Tests for PricingInfo dataclass."""
|
||||
|
||||
def test_create_pricing_info(self):
|
||||
"""Should create pricing info."""
|
||||
pricing = PricingInfo(
|
||||
processor=ProcessorType.GPU,
|
||||
price_per_second=0.0001,
|
||||
available_units=100,
|
||||
utilization_percent=75.5,
|
||||
)
|
||||
|
||||
assert pricing.processor == ProcessorType.GPU
|
||||
assert pricing.price_per_second == 0.0001
|
||||
assert pricing.available_units == 100
|
||||
|
||||
def test_optional_fields(self):
|
||||
"""Should handle optional fields."""
|
||||
pricing = PricingInfo(
|
||||
processor=ProcessorType.TPU,
|
||||
price_per_second=0.0002,
|
||||
available_units=50,
|
||||
utilization_percent=60.0,
|
||||
aws_equivalent_price=0.0010,
|
||||
savings_percent=80.0,
|
||||
)
|
||||
|
||||
assert pricing.aws_equivalent_price == 0.0010
|
||||
assert pricing.savings_percent == 80.0
|
||||
117
sdk/ruby/test/test_tensor.rb
Normal file
117
sdk/ruby/test/test_tensor.rb
Normal file
|
|
@ -0,0 +1,117 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'minitest/autorun'
|
||||
require_relative '../lib/synor_compute'
|
||||
|
||||
class TestTensor < Minitest::Test
|
||||
def test_creation
|
||||
tensor = SynorCompute::Tensor.new([2, 3], [1.0, 2.0, 3.0, 4.0, 5.0, 6.0])
|
||||
assert_equal [2, 3], tensor.shape
|
||||
assert_equal 6, tensor.size
|
||||
assert_equal 2, tensor.ndim
|
||||
end
|
||||
|
||||
def test_zeros
|
||||
tensor = SynorCompute::Tensor.zeros([3, 4])
|
||||
assert_equal [3, 4], tensor.shape
|
||||
assert_equal 12, tensor.size
|
||||
assert tensor.data.all? { |v| v == 0.0 }
|
||||
end
|
||||
|
||||
def test_ones
|
||||
tensor = SynorCompute::Tensor.ones([2, 2])
|
||||
assert tensor.data.all? { |v| v == 1.0 }
|
||||
end
|
||||
|
||||
def test_random
|
||||
tensor = SynorCompute::Tensor.rand([10, 10])
|
||||
assert_equal [10, 10], tensor.shape
|
||||
assert tensor.data.all? { |v| v >= 0 && v < 1 }
|
||||
end
|
||||
|
||||
def test_randn
|
||||
tensor = SynorCompute::Tensor.randn([1000])
|
||||
mean = tensor.mean
|
||||
std = tensor.std
|
||||
assert mean.abs < 0.2, "Mean should be close to 0, got #{mean}"
|
||||
assert std > 0.8 && std < 1.2, "Std should be close to 1, got #{std}"
|
||||
end
|
||||
|
||||
def test_eye
|
||||
tensor = SynorCompute::Tensor.eye(3)
|
||||
assert_equal [3, 3], tensor.shape
|
||||
assert_equal 1.0, tensor[[0, 0]]
|
||||
assert_equal 1.0, tensor[[1, 1]]
|
||||
assert_equal 0.0, tensor[[0, 1]]
|
||||
end
|
||||
|
||||
def test_arange
|
||||
tensor = SynorCompute::Tensor.arange(0, 5, 1)
|
||||
assert_equal [5], tensor.shape
|
||||
assert_equal 0.0, tensor[[0]]
|
||||
assert_equal 4.0, tensor[[4]]
|
||||
end
|
||||
|
||||
def test_linspace
|
||||
tensor = SynorCompute::Tensor.linspace(0, 10, 11)
|
||||
assert_equal [11], tensor.shape
|
||||
assert_in_delta 0.0, tensor[[0]], 0.0001
|
||||
assert_in_delta 10.0, tensor[[10]], 0.0001
|
||||
end
|
||||
|
||||
def test_reshape
|
||||
tensor = SynorCompute::Tensor.new([6], [1, 2, 3, 4, 5, 6])
|
||||
reshaped = tensor.reshape([2, 3])
|
||||
assert_equal [2, 3], reshaped.shape
|
||||
assert_equal 6, reshaped.size
|
||||
end
|
||||
|
||||
def test_transpose
|
||||
tensor = SynorCompute::Tensor.new([2, 3], [1, 2, 3, 4, 5, 6])
|
||||
transposed = tensor.transpose
|
||||
assert_equal [3, 2], transposed.shape
|
||||
end
|
||||
|
||||
def test_sum
|
||||
tensor = SynorCompute::Tensor.new([4], [1, 2, 3, 4])
|
||||
assert_equal 10.0, tensor.sum
|
||||
end
|
||||
|
||||
def test_mean
|
||||
tensor = SynorCompute::Tensor.new([4], [1, 2, 3, 4])
|
||||
assert_equal 2.5, tensor.mean
|
||||
end
|
||||
|
||||
def test_min_max
|
||||
tensor = SynorCompute::Tensor.new([4], [3, 1, 4, 2])
|
||||
assert_equal 1.0, tensor.min
|
||||
assert_equal 4.0, tensor.max
|
||||
end
|
||||
|
||||
def test_relu
|
||||
tensor = SynorCompute::Tensor.new([5], [-2, -1, 0, 1, 2])
|
||||
result = tensor.relu
|
||||
assert_equal [0, 0, 0, 1, 2], result.data
|
||||
end
|
||||
|
||||
def test_sigmoid
|
||||
tensor = SynorCompute::Tensor.new([1], [0])
|
||||
result = tensor.sigmoid
|
||||
assert_in_delta 0.5, result[[0]], 0.0001
|
||||
end
|
||||
|
||||
def test_softmax
|
||||
tensor = SynorCompute::Tensor.new([3], [1, 2, 3])
|
||||
result = tensor.softmax
|
||||
assert_in_delta 1.0, result.sum, 0.0001
|
||||
assert result[[2]] > result[[1]]
|
||||
end
|
||||
|
||||
def test_serialization
|
||||
original = SynorCompute::Tensor.new([2, 3], [1, 2, 3, 4, 5, 6])
|
||||
json = original.to_json
|
||||
restored = SynorCompute::Tensor.from_json(json)
|
||||
assert_equal original.shape, restored.shape
|
||||
assert_equal original.data, restored.data
|
||||
end
|
||||
end
|
||||
102
sdk/ruby/test/test_types.rb
Normal file
102
sdk/ruby/test/test_types.rb
Normal file
|
|
@ -0,0 +1,102 @@
|
|||
# frozen_string_literal: true
|
||||
|
||||
require 'minitest/autorun'
|
||||
require_relative '../lib/synor_compute'
|
||||
|
||||
class TestTypes < Minitest::Test
|
||||
def test_processor_type_values
|
||||
assert_equal 'cpu', SynorCompute::ProcessorType::CPU
|
||||
assert_equal 'gpu', SynorCompute::ProcessorType::GPU
|
||||
assert_equal 'tpu', SynorCompute::ProcessorType::TPU
|
||||
assert_equal 'npu', SynorCompute::ProcessorType::NPU
|
||||
assert_equal 'auto', SynorCompute::ProcessorType::AUTO
|
||||
end
|
||||
|
||||
def test_precision_values
|
||||
assert_equal 'fp64', SynorCompute::Precision::FP64
|
||||
assert_equal 'fp32', SynorCompute::Precision::FP32
|
||||
assert_equal 'fp16', SynorCompute::Precision::FP16
|
||||
assert_equal 'bf16', SynorCompute::Precision::BF16
|
||||
assert_equal 'int8', SynorCompute::Precision::INT8
|
||||
end
|
||||
|
||||
def test_priority_values
|
||||
assert_equal 'critical', SynorCompute::Priority::CRITICAL
|
||||
assert_equal 'high', SynorCompute::Priority::HIGH
|
||||
assert_equal 'normal', SynorCompute::Priority::NORMAL
|
||||
assert_equal 'low', SynorCompute::Priority::LOW
|
||||
end
|
||||
|
||||
def test_job_status_values
|
||||
assert_equal 'pending', SynorCompute::JobStatus::PENDING
|
||||
assert_equal 'running', SynorCompute::JobStatus::RUNNING
|
||||
assert_equal 'completed', SynorCompute::JobStatus::COMPLETED
|
||||
assert_equal 'failed', SynorCompute::JobStatus::FAILED
|
||||
end
|
||||
|
||||
def test_config_defaults
|
||||
config = SynorCompute::Config.new(api_key: 'test-key')
|
||||
assert_equal 'test-key', config.api_key
|
||||
assert_equal 'https://api.synor.io/compute/v1', config.base_url
|
||||
assert_equal 30, config.timeout
|
||||
end
|
||||
|
||||
def test_config_custom
|
||||
config = SynorCompute::Config.new(
|
||||
api_key: 'test-key',
|
||||
base_url: 'https://custom.api.com',
|
||||
timeout: 60,
|
||||
default_processor: SynorCompute::ProcessorType::GPU
|
||||
)
|
||||
assert_equal 'https://custom.api.com', config.base_url
|
||||
assert_equal 60, config.timeout
|
||||
end
|
||||
|
||||
def test_matmul_options_to_hash
|
||||
options = SynorCompute::MatMulOptions.new(
|
||||
precision: SynorCompute::Precision::FP16,
|
||||
processor: SynorCompute::ProcessorType::GPU
|
||||
)
|
||||
hash = options.to_h
|
||||
assert_equal 'fp16', hash[:precision]
|
||||
assert_equal 'gpu', hash[:processor]
|
||||
end
|
||||
|
||||
def test_inference_options_defaults
|
||||
options = SynorCompute::InferenceOptions.new
|
||||
assert_equal 256, options.max_tokens
|
||||
assert_in_delta 0.7, options.temperature, 0.001
|
||||
assert_in_delta 0.9, options.top_p, 0.001
|
||||
assert_equal 50, options.top_k
|
||||
end
|
||||
|
||||
def test_job_result_success
|
||||
result = SynorCompute::JobResult.new(
|
||||
job_id: 'job-123',
|
||||
status: SynorCompute::JobStatus::COMPLETED,
|
||||
result: 'output'
|
||||
)
|
||||
assert result.success?
|
||||
refute result.failed?
|
||||
end
|
||||
|
||||
def test_job_result_failure
|
||||
result = SynorCompute::JobResult.new(
|
||||
job_id: 'job-456',
|
||||
status: SynorCompute::JobStatus::FAILED,
|
||||
error: 'Error message'
|
||||
)
|
||||
refute result.success?
|
||||
assert result.failed?
|
||||
end
|
||||
|
||||
def test_model_info_formatted_parameters
|
||||
model = SynorCompute::ModelInfo.new(
|
||||
id: 'test',
|
||||
name: 'Test Model',
|
||||
category: 'llm',
|
||||
parameters: 70_000_000_000
|
||||
)
|
||||
assert_equal '70B', model.formatted_parameters
|
||||
end
|
||||
end
|
||||
|
|
@ -48,6 +48,9 @@ mod tensor;
|
|||
mod client;
|
||||
mod error;
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests;
|
||||
|
||||
pub use types::*;
|
||||
pub use tensor::Tensor;
|
||||
pub use client::SynorCompute;
|
||||
|
|
|
|||
334
sdk/rust/src/tests.rs
Normal file
334
sdk/rust/src/tests.rs
Normal file
|
|
@ -0,0 +1,334 @@
|
|||
//! Comprehensive tests for Synor Compute Rust SDK.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tensor_tests {
|
||||
use crate::tensor::Tensor;
|
||||
use crate::types::Precision;
|
||||
|
||||
#[test]
|
||||
fn test_tensor_creation() {
|
||||
let t = Tensor::new(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
|
||||
assert_eq!(t.shape(), &[2, 3]);
|
||||
assert_eq!(t.size(), 6);
|
||||
assert_eq!(t.ndim(), 2);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_zeros() {
|
||||
let t = Tensor::zeros(&[3, 3]);
|
||||
assert_eq!(t.shape(), &[3, 3]);
|
||||
assert!(t.data().iter().all(|&x| x == 0.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_ones() {
|
||||
let t = Tensor::ones(&[2, 2]);
|
||||
assert!(t.data().iter().all(|&x| x == 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_rand() {
|
||||
let t = Tensor::rand(&[10, 10]);
|
||||
assert_eq!(t.size(), 100);
|
||||
assert!(t.data().iter().all(|&x| x >= 0.0 && x < 1.0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_randn() {
|
||||
let t = Tensor::randn(&[1000]);
|
||||
let mean = t.mean();
|
||||
let std = t.std();
|
||||
assert!(mean.abs() < 0.2, "Mean should be close to 0");
|
||||
assert!(std > 0.8 && std < 1.2, "Std should be close to 1");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_eye() {
|
||||
let t = Tensor::eye(3);
|
||||
assert_eq!(t.shape(), &[3, 3]);
|
||||
assert_eq!(t.get(&[0, 0]), 1.0);
|
||||
assert_eq!(t.get(&[1, 1]), 1.0);
|
||||
assert_eq!(t.get(&[2, 2]), 1.0);
|
||||
assert_eq!(t.get(&[0, 1]), 0.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_arange() {
|
||||
let t = Tensor::arange(0.0, 5.0, 1.0);
|
||||
assert_eq!(t.shape(), &[5]);
|
||||
assert_eq!(t.data()[0], 0.0);
|
||||
assert_eq!(t.data()[4], 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_linspace() {
|
||||
let t = Tensor::linspace(0.0, 10.0, 11);
|
||||
assert_eq!(t.shape(), &[11]);
|
||||
assert!((t.data()[0] - 0.0).abs() < 1e-10);
|
||||
assert!((t.data()[10] - 10.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_reshape() {
|
||||
let t = Tensor::new(&[6], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
|
||||
let reshaped = t.reshape(&[2, 3]);
|
||||
assert_eq!(reshaped.shape(), &[2, 3]);
|
||||
assert_eq!(reshaped.size(), 6);
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[should_panic]
|
||||
fn test_tensor_invalid_reshape() {
|
||||
let t = Tensor::zeros(&[4]);
|
||||
t.reshape(&[2, 3]); // Should panic
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_transpose() {
|
||||
let t = Tensor::new(&[2, 3], vec![1.0, 2.0, 3.0, 4.0, 5.0, 6.0]);
|
||||
let transposed = t.transpose();
|
||||
assert_eq!(transposed.shape(), &[3, 2]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_mean() {
|
||||
let t = Tensor::new(&[4], vec![1.0, 2.0, 3.0, 4.0]);
|
||||
assert!((t.mean() - 2.5).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_sum() {
|
||||
let t = Tensor::new(&[4], vec![1.0, 2.0, 3.0, 4.0]);
|
||||
assert!((t.sum() - 10.0).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_std() {
|
||||
let t = Tensor::new(&[4], vec![1.0, 2.0, 3.0, 4.0]);
|
||||
assert!((t.std() - 1.118).abs() < 0.01);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_min_max() {
|
||||
let t = Tensor::new(&[4], vec![3.0, 1.0, 4.0, 2.0]);
|
||||
assert_eq!(t.min(), 1.0);
|
||||
assert_eq!(t.max(), 4.0);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_relu() {
|
||||
let t = Tensor::new(&[5], vec![-2.0, -1.0, 0.0, 1.0, 2.0]);
|
||||
let result = t.relu();
|
||||
assert_eq!(result.data(), &[0.0, 0.0, 0.0, 1.0, 2.0]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_sigmoid() {
|
||||
let t = Tensor::new(&[1], vec![0.0]);
|
||||
let result = t.sigmoid();
|
||||
assert!((result.data()[0] - 0.5).abs() < 1e-10);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_softmax() {
|
||||
let t = Tensor::new(&[3], vec![1.0, 2.0, 3.0]);
|
||||
let result = t.softmax();
|
||||
assert!((result.sum() - 1.0).abs() < 1e-10);
|
||||
assert!(result.data()[2] > result.data()[1]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_tensor_dtype() {
|
||||
let t = Tensor::zeros(&[2, 2]).with_dtype(Precision::FP16);
|
||||
assert_eq!(t.dtype(), Precision::FP16);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod types_tests {
|
||||
use crate::types::*;
|
||||
|
||||
#[test]
|
||||
fn test_processor_type_values() {
|
||||
assert_eq!(ProcessorType::Cpu.as_str(), "cpu");
|
||||
assert_eq!(ProcessorType::Gpu.as_str(), "gpu");
|
||||
assert_eq!(ProcessorType::Tpu.as_str(), "tpu");
|
||||
assert_eq!(ProcessorType::Npu.as_str(), "npu");
|
||||
assert_eq!(ProcessorType::Lpu.as_str(), "lpu");
|
||||
assert_eq!(ProcessorType::Auto.as_str(), "auto");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_precision_values() {
|
||||
assert_eq!(Precision::FP64.as_str(), "fp64");
|
||||
assert_eq!(Precision::FP32.as_str(), "fp32");
|
||||
assert_eq!(Precision::FP16.as_str(), "fp16");
|
||||
assert_eq!(Precision::BF16.as_str(), "bf16");
|
||||
assert_eq!(Precision::INT8.as_str(), "int8");
|
||||
assert_eq!(Precision::INT4.as_str(), "int4");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_priority_default() {
|
||||
let priority = Priority::default();
|
||||
assert_eq!(priority, Priority::Normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_job_status_default() {
|
||||
let status = JobStatus::default();
|
||||
assert_eq!(status, JobStatus::Pending);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_builder() {
|
||||
let config = Config::new("test-api-key");
|
||||
assert_eq!(config.api_key, "test-api-key");
|
||||
assert_eq!(config.base_url, "https://api.synor.io/compute/v1");
|
||||
assert_eq!(config.timeout_secs, 30);
|
||||
assert!(!config.debug);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_config_custom() {
|
||||
let config = Config::new("test-key")
|
||||
.base_url("https://custom.api.com")
|
||||
.default_processor(ProcessorType::Gpu)
|
||||
.default_precision(Precision::FP16)
|
||||
.timeout_secs(60)
|
||||
.debug(true);
|
||||
|
||||
assert_eq!(config.base_url, "https://custom.api.com");
|
||||
assert_eq!(config.default_processor, ProcessorType::Gpu);
|
||||
assert_eq!(config.default_precision, Precision::FP16);
|
||||
assert_eq!(config.timeout_secs, 60);
|
||||
assert!(config.debug);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_matmul_options_default() {
|
||||
let options = MatMulOptions::default();
|
||||
assert_eq!(options.precision, Precision::FP32);
|
||||
assert_eq!(options.processor, ProcessorType::Auto);
|
||||
assert_eq!(options.priority, Priority::Normal);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_conv2d_options_default() {
|
||||
let options = Conv2dOptions::default();
|
||||
assert_eq!(options.stride, (1, 1));
|
||||
assert_eq!(options.padding, (0, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_attention_options_default() {
|
||||
let options = AttentionOptions::default();
|
||||
assert_eq!(options.num_heads, 8);
|
||||
assert!(options.flash);
|
||||
assert_eq!(options.precision, Precision::FP16);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_inference_options_default() {
|
||||
let options = InferenceOptions::default();
|
||||
assert_eq!(options.max_tokens, 256);
|
||||
assert!((options.temperature - 0.7).abs() < 0.001);
|
||||
assert!((options.top_p - 0.9).abs() < 0.001);
|
||||
assert_eq!(options.top_k, 50);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_job_result_success() {
|
||||
let result: JobResult<String> = JobResult {
|
||||
job_id: Some("job-123".to_string()),
|
||||
status: JobStatus::Completed,
|
||||
result: Some("output".to_string()),
|
||||
error: None,
|
||||
execution_time_ms: Some(100),
|
||||
processor: Some(ProcessorType::Gpu),
|
||||
cost: Some(0.01),
|
||||
};
|
||||
|
||||
assert!(result.is_success());
|
||||
assert!(!result.is_failed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_job_result_failure() {
|
||||
let result: JobResult<String> = JobResult {
|
||||
job_id: Some("job-456".to_string()),
|
||||
status: JobStatus::Failed,
|
||||
result: None,
|
||||
error: Some("Error message".to_string()),
|
||||
execution_time_ms: None,
|
||||
processor: None,
|
||||
cost: None,
|
||||
};
|
||||
|
||||
assert!(!result.is_success());
|
||||
assert!(result.is_failed());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_info_formatted_parameters() {
|
||||
let model = ModelInfo {
|
||||
id: "test".to_string(),
|
||||
name: "Test Model".to_string(),
|
||||
description: None,
|
||||
category: "llm".to_string(),
|
||||
parameters: Some(70_000_000_000),
|
||||
context_length: Some(8192),
|
||||
format: None,
|
||||
recommended_processor: None,
|
||||
license: None,
|
||||
cid: None,
|
||||
};
|
||||
|
||||
assert_eq!(model.formatted_parameters(), "70B");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_model_info_formatted_parameters_millions() {
|
||||
let model = ModelInfo {
|
||||
id: "test".to_string(),
|
||||
name: "Test Model".to_string(),
|
||||
description: None,
|
||||
category: "embedding".to_string(),
|
||||
parameters: Some(350_000_000),
|
||||
context_length: None,
|
||||
format: None,
|
||||
recommended_processor: None,
|
||||
license: None,
|
||||
cid: None,
|
||||
};
|
||||
|
||||
assert_eq!(model.formatted_parameters(), "350M");
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod error_tests {
|
||||
use crate::error::Error;
|
||||
|
||||
#[test]
|
||||
fn test_client_closed_error() {
|
||||
let err = Error::ClientClosed;
|
||||
assert_eq!(format!("{}", err), "Client has been closed");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_api_error() {
|
||||
let err = Error::Api {
|
||||
status_code: 401,
|
||||
message: "Invalid API key".to_string(),
|
||||
};
|
||||
assert!(format!("{}", err).contains("401"));
|
||||
assert!(format!("{}", err).contains("Invalid API key"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_invalid_argument_error() {
|
||||
let err = Error::InvalidArgument("Bad parameter".to_string());
|
||||
assert!(format!("{}", err).contains("Bad parameter"));
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue