using System.Runtime.CompilerServices;
using OpenNest.Engine;
using OpenNest.Engine.BestFit;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using OpenNest.Math;
using OpenNest.Shapes;
namespace OpenNest.Tests.BestFit;
// BestFitCache is process-wide static state and these tests swap its evaluator factory,
// so they must not run alongside other tests.
[CollectionDefinition(nameof(FillCacheCollection), DisableParallelization = true)]
public class FillCacheCollection { }
[Collection(nameof(FillCacheCollection))]
public class BestFitCacheTests
{
private const double Spacing = 0.25;
private static Drawing MakeRotatedTShape()
{
var drawing = new TShape { Width = 10, Height = 8 }.GetDrawing();
drawing.Program.Rotate(Angle.ToRadians(30), drawing.Program.BoundingBox().Center);
drawing.RecomputeCanonicalAngle();
return drawing;
}
///
/// Counts best-fit computations for one source drawing by wrapping the evaluator factory.
///
private sealed class EvaluatorSpy : IDisposable
{
private readonly Func previous;
private readonly WeakReference source;
private int count;
public EvaluatorSpy(Drawing source)
{
this.source = new WeakReference(source);
previous = BestFitCache.CreateEvaluator;
BestFitCache.CreateEvaluator = (drawing, spacing) =>
{
if (this.source.TryGetTarget(out var target)
&& ReferenceEquals(CanonicalFrame.SourceOf(drawing), target))
Interlocked.Increment(ref count);
return new PairEvaluator();
};
}
public int Count => Volatile.Read(ref count);
public void Dispose() => BestFitCache.CreateEvaluator = previous;
}
[Fact]
public void GetOrCompute_CanonicalCopiesOfOneDrawing_ShareOneComputation()
{
var drawing = MakeRotatedTShape();
using var spy = new EvaluatorSpy(drawing);
var first = BestFitCache.GetOrCompute(CanonicalFrame.AsCanonicalCopy(drawing), 60, 40, Spacing);
var copyOfCopy = CanonicalFrame.AsCanonicalCopy(CanonicalFrame.AsCanonicalCopy(drawing));
var second = BestFitCache.GetOrCompute(copyOfCopy, 60, 40, Spacing);
var third = BestFitCache.GetOrCompute(drawing, 60, 40, Spacing);
Assert.Same(first, second);
Assert.Same(first, third);
Assert.Equal(1, spy.Count);
}
[Fact]
public void GetOrCompute_TwoPlateSizes_RunsFinderOnceAndMatchesPerSizeFinder()
{
var drawing = MakeRotatedTShape();
using var spy = new EvaluatorSpy(drawing);
var sizes = new[] { (Width: 60.0, Height: 40.0), (Width: 14.0, Height: 30.0) };
var cached = sizes
.Select(s => BestFitCache.GetOrCompute(drawing, s.Width, s.Height, Spacing))
.ToList();
Assert.Equal(1, spy.Count);
var canonical = CanonicalFrame.AsCanonicalCopy(drawing);
for (var i = 0; i < sizes.Length; i++)
{
var expected = new BestFitFinder(sizes[i].Width, sizes[i].Height)
.FindBestFits(canonical, Spacing, 0.25);
Assert.Equal(Signature(expected), Signature(cached[i]));
}
// The small plate must actually reject something the large one keeps.
Assert.True(cached[1].Count(r => r.Keep) < cached[0].Count(r => r.Keep));
}
[Fact]
public void GetOrCompute_ReplacingProgram_InvalidatesEntry()
{
var drawing = MakeRotatedTShape();
using var spy = new EvaluatorSpy(drawing);
var before = BestFitCache.GetOrCompute(drawing, 60, 40, Spacing);
drawing.Program = (OpenNest.CNC.Program)drawing.Program.Clone();
var after = BestFitCache.GetOrCompute(drawing, 60, 40, Spacing);
Assert.NotSame(before, after);
Assert.Equal(2, spy.Count);
}
[Fact]
public void GetOrCompute_ReleasedDrawing_IsCollectable()
{
var weak = ComputeForTransientDrawing();
for (var i = 0; i < 3 && weak.IsAlive; i++)
{
GC.Collect();
GC.WaitForPendingFinalizers();
}
Assert.False(weak.IsAlive);
}
[MethodImpl(MethodImplOptions.NoInlining)]
private static WeakReference ComputeForTransientDrawing()
{
var drawing = MakeRotatedTShape();
var canonical = CanonicalFrame.AsCanonicalCopy(drawing);
BestFitCache.GetOrCompute(canonical, 60, 40, Spacing);
FillResultCache.Store(canonical, new Box(0, 0, 60, 40), Spacing, new List { new Part(canonical) });
return new WeakReference(drawing);
}
[Fact]
public void Populate_FeedsGetOrComputeAndRoundTripsThroughGetAllForDrawing()
{
var drawing = MakeRotatedTShape();
using var spy = new EvaluatorSpy(drawing);
var results = new List
{
new BestFitResult { Candidate = new PairCandidate { Drawing = drawing }, Keep = true },
};
BestFitCache.Populate(drawing, 60, 40, Spacing, results);
Assert.Same(results, BestFitCache.GetOrCompute(CanonicalFrame.AsCanonicalCopy(drawing), 60, 40, Spacing));
Assert.Equal(0, spy.Count);
var all = BestFitCache.GetAllForDrawing(drawing);
Assert.Single(all);
Assert.Same(results, all[(60, 40, Spacing)]);
}
[Fact]
public void FillResultCache_HitThroughSecondCanonicalCopy_RebindsToSameParts()
{
var drawing = MakeRotatedTShape();
var box = new Box(5, 7, 80, 50);
var firstCopy = CanonicalFrame.AsCanonicalCopy(drawing);
var computed = new FillLinear(box, Spacing).Fill(firstCopy, 0, NestDirection.Horizontal);
Assert.NotEmpty(computed);
FillResultCache.Store(firstCopy, box, Spacing, computed);
var secondCopy = CanonicalFrame.AsCanonicalCopy(drawing);
var hit = FillResultCache.Get(secondCopy, box, Spacing);
Assert.NotNull(hit);
// A non-canonical caller must not be served canonical-frame parts.
Assert.Null(FillResultCache.Get(drawing, box, Spacing));
var expected = CanonicalFrame.RebindToOriginal(computed.Select(p => (Part)p.Clone()).ToList(), drawing);
var actual = CanonicalFrame.RebindToOriginal(hit, drawing);
Assert.Equal(expected.Count, actual.Count);
for (var i = 0; i < expected.Count; i++)
{
Assert.Same(drawing, actual[i].BaseDrawing);
Assert.Equal(expected[i].Rotation, actual[i].Rotation, 9);
Assert.Equal(expected[i].BoundingBox.X, actual[i].BoundingBox.X, 6);
Assert.Equal(expected[i].BoundingBox.Y, actual[i].BoundingBox.Y, 6);
}
}
private static List Signature(List results) =>
results
.Select(r =>
FormattableString.Invariant(
$"{r.Candidate.Part2Rotation:F6}|{r.Candidate.Part2Offset.X:F6}|{r.Candidate.Part2Offset.Y:F6}|{r.RotatedArea:F6}|{r.Keep}|{r.Reason}"
)
)
.OrderBy(s => s, StringComparer.Ordinal)
.ToList();
}