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 client;
|
||||||
mod error;
|
mod error;
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests;
|
||||||
|
|
||||||
pub use types::*;
|
pub use types::*;
|
||||||
pub use tensor::Tensor;
|
pub use tensor::Tensor;
|
||||||
pub use client::SynorCompute;
|
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