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:
Gulshan Yadav 2026-01-11 17:56:11 +05:30
parent 3aff77a799
commit e2a3b66123
19 changed files with 4228 additions and 0 deletions

View 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>

View 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);
}
}

View 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);
}
}

View 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'));
});
});
}

View 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
View 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")
}
}

View 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]"));
}
}

View 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);
}
}

View 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);
});
});
});

View 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();
});
});
});

View 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);
});
});
});

View file

@ -0,0 +1 @@
# Tests for Synor Compute SDK

View 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

View 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

View 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

View 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
View 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

View file

@ -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
View 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"));
}
}