181 lines
6.1 KiB
C#
181 lines
6.1 KiB
C#
using OpenNest.Engine.ML;
|
|
|
|
namespace OpenNest.Tests.ML;
|
|
|
|
// The generic loader is the production session publication path. Reference objects
|
|
// isolate its lifetime/concurrency contract; they are not ONNX models or accuracy evidence.
|
|
public class AnglePredictorTests
|
|
{
|
|
[Fact]
|
|
public void PredictAngles_DefaultThreshold_RemainsPointThree()
|
|
{
|
|
var method = typeof(AnglePredictor).GetMethod(nameof(AnglePredictor.PredictAngles));
|
|
|
|
Assert.NotNull(method);
|
|
var threshold = method.GetParameters().Single(parameter => parameter.Name == "threshold");
|
|
Assert.True(threshold.HasDefaultValue);
|
|
Assert.Equal(0.3, threshold.DefaultValue);
|
|
}
|
|
|
|
[Fact]
|
|
public void SessionLoader_Success_IsLazyAndReusesSameSessionWithoutReload()
|
|
{
|
|
var attempts = 0;
|
|
var session = new object();
|
|
var loader = new SingleAttemptLoader<object>(() =>
|
|
{
|
|
attempts++;
|
|
return session;
|
|
});
|
|
|
|
Assert.Equal(0, attempts);
|
|
for (var i = 0; i < 5; i++)
|
|
Assert.Same(session, loader.GetValue());
|
|
Assert.Equal(1, attempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void SessionLoader_MissingModel_RemainsUnavailableWithoutRetry()
|
|
{
|
|
var attempts = 0;
|
|
var modelPresent = false;
|
|
var session = new object();
|
|
var loader = new SingleAttemptLoader<object>(() =>
|
|
{
|
|
attempts++;
|
|
return modelPresent ? session : null!;
|
|
});
|
|
|
|
Assert.Null(loader.GetValue());
|
|
modelPresent = true; // Even a subsequently available resource must not trigger a reload.
|
|
for (var i = 0; i < 5; i++)
|
|
Assert.Null(loader.GetValue());
|
|
Assert.Equal(1, attempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void SessionLoader_ThrowingLoad_RemainsUnavailableWithoutRetry()
|
|
{
|
|
var attempts = 0;
|
|
var loadFails = true;
|
|
var loader = new SingleAttemptLoader<object>(() =>
|
|
{
|
|
attempts++;
|
|
if (loadFails)
|
|
throw new InvalidDataException("Controlled model-load failure.");
|
|
return new object();
|
|
});
|
|
|
|
Assert.Null(loader.GetValue());
|
|
loadFails = false;
|
|
for (var i = 0; i < 5; i++)
|
|
Assert.Null(loader.GetValue());
|
|
Assert.Equal(1, attempts);
|
|
}
|
|
|
|
[Fact]
|
|
public void SessionLoaders_AreIndependent_NoGlobalTestSwitchesOrSharedFailures()
|
|
{
|
|
var session = new object();
|
|
var unavailable = new SingleAttemptLoader<object>(() => null!);
|
|
var available = new SingleAttemptLoader<object>(() => session);
|
|
|
|
Assert.Null(unavailable.GetValue());
|
|
Assert.Same(session, available.GetValue());
|
|
Assert.Null(unavailable.GetValue());
|
|
}
|
|
|
|
[Theory]
|
|
[InlineData("available")]
|
|
[InlineData("missing")]
|
|
[InlineData("throwing")]
|
|
public void SessionLoader_ConcurrentReaders_WaitForDefinitiveAvailability(string outcome)
|
|
{
|
|
using var loadEntered = new ManualResetEventSlim();
|
|
using var releaseLoad = new ManualResetEventSlim();
|
|
var readerEntered = 0;
|
|
var readerCompleted = 0;
|
|
var timeout = TimeSpan.FromSeconds(10);
|
|
var attempts = 0;
|
|
var session = new object();
|
|
var results = new object?[2];
|
|
var errors = new Exception?[2];
|
|
var loader = new SingleAttemptLoader<object>(() =>
|
|
{
|
|
Interlocked.Increment(ref attempts);
|
|
loadEntered.Set();
|
|
releaseLoad.Wait();
|
|
return outcome switch
|
|
{
|
|
"available" => session,
|
|
"missing" => null!,
|
|
_ => throw new InvalidDataException("Controlled model-load failure."),
|
|
};
|
|
});
|
|
var readSession = (int index) =>
|
|
{
|
|
try
|
|
{
|
|
results[index] = loader.GetValue();
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
errors[index] = ex;
|
|
}
|
|
};
|
|
var initializer = new Thread(() => readSession(0)) { IsBackground = true };
|
|
var reader = new Thread(() =>
|
|
{
|
|
Volatile.Write(ref readerEntered, 1);
|
|
try
|
|
{
|
|
readSession(1);
|
|
}
|
|
finally
|
|
{
|
|
Volatile.Write(ref readerCompleted, 1);
|
|
}
|
|
})
|
|
{ IsBackground = true };
|
|
|
|
try
|
|
{
|
|
initializer.Start();
|
|
Assert.True(loadEntered.Wait(timeout), "The first caller did not enter the loader.");
|
|
reader.Start();
|
|
Assert.True(SpinWait.SpinUntil(() => Volatile.Read(ref readerEntered) == 1, timeout),
|
|
"The concurrent reader did not start.");
|
|
|
|
// Observe an actual blocked reader, not just a Task that may not have run yet.
|
|
// The timeout only bounds a broken test; this is not a latency assertion.
|
|
Assert.True(SpinWait.SpinUntil(
|
|
() => Volatile.Read(ref readerCompleted) == 1 || (reader.ThreadState & ThreadState.WaitSleepJoin) != 0,
|
|
timeout), "The reader neither waited nor completed.");
|
|
Assert.False(Volatile.Read(ref readerCompleted) == 1,
|
|
"Availability was published before the blocked load had a definitive result.");
|
|
Assert.Equal(1, Volatile.Read(ref attempts));
|
|
}
|
|
finally
|
|
{
|
|
releaseLoad.Set();
|
|
if ((initializer.ThreadState & ThreadState.Unstarted) == 0)
|
|
Assert.True(initializer.Join(timeout), "The initializing caller did not terminate.");
|
|
if ((reader.ThreadState & ThreadState.Unstarted) == 0)
|
|
Assert.True(reader.Join(timeout), "The concurrent reader did not terminate.");
|
|
}
|
|
|
|
Assert.All(errors, error => Assert.Null(error));
|
|
if (outcome == "available")
|
|
{
|
|
Assert.All(results, result => Assert.Same(session, result));
|
|
Assert.Same(session, loader.GetValue());
|
|
}
|
|
else
|
|
{
|
|
Assert.All(results, result => Assert.Null(result));
|
|
Assert.Null(loader.GetValue());
|
|
}
|
|
Assert.Equal(1, attempts);
|
|
}
|
|
}
|