Files
OpenNest/OpenNest.Tests/Fill/FillComparerTests.cs
T

380 lines
12 KiB
C#

using OpenNest.CNC;
using OpenNest.Engine;
using OpenNest.Engine.Fill;
using OpenNest.Geometry;
using OpenNest.Tests.BestFit;
namespace OpenNest.Tests.Fill;
public class DefaultFillComparerTests
{
private readonly IFillComparer comparer = new DefaultFillComparer();
private readonly Box workArea = new(0, 0, 100, 100);
[Fact]
public void NullCandidate_ReturnsFalse()
{
var current = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
Assert.False(comparer.IsBetter(null, current, workArea));
}
[Fact]
public void EmptyCandidate_ReturnsFalse()
{
var current = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
Assert.False(comparer.IsBetter(new List<Part>(), current, workArea));
}
[Fact]
public void NullCurrent_ReturnsTrue()
{
var candidate = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
Assert.True(comparer.IsBetter(candidate, null, workArea));
}
[Fact]
public void HigherCount_Wins()
{
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(20, 0, 10),
TestHelpers.MakePartAt(40, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(20, 0, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
[Fact]
public void SameCount_HigherDensityWins()
{
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(50, 0, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
[Fact]
public void LowerCount_ReturnsFalse()
{
var candidate = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(20, 0, 10),
};
Assert.False(comparer.IsBetter(candidate, current, workArea));
}
[Fact]
public void SameCount_LowerDensity_ReturnsFalse()
{
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(50, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10),
};
Assert.False(comparer.IsBetter(candidate, current, workArea));
}
[Fact]
public void ExactScoreTie_ReturnsFalseInBothOrders()
{
var candidate = new List<Part>
{
TestHelpers.MakePartAt(5, 7, 10),
TestHelpers.MakePartAt(25, 7, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(20, 0, 10),
};
Assert.Equal(FillScore.Compute(candidate, workArea), FillScore.Compute(current, workArea));
Assert.False(comparer.IsBetter(candidate, current, workArea));
Assert.False(comparer.IsBetter(current, candidate, workArea));
Assert.False(comparer.IsBetter(candidate, candidate, workArea));
}
[Fact]
public void UnequalCounts_SmallerLayoutIsDenser_ButCountWinsInBothOrders()
{
var larger = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(40, 0, 10),
TestHelpers.MakePartAt(80, 0, 10),
};
var smaller = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10),
};
Assert.True(FillScore.Compute(smaller, workArea).Density > FillScore.Compute(larger, workArea).Density);
Assert.True(comparer.IsBetter(larger, smaller, workArea));
Assert.False(comparer.IsBetter(smaller, larger, workArea));
}
[Theory]
[InlineData(0, 0, false)]
[InlineData(0, 1, false)]
[InlineData(0, 2, false)]
[InlineData(1, 0, false)]
[InlineData(1, 1, false)]
[InlineData(1, 2, false)]
[InlineData(2, 0, true)]
[InlineData(2, 1, true)]
[InlineData(2, 2, false)]
public void NullEmptyAndNonemptyInputs_PreserveGuardOrder(int candidateKind, int currentKind, bool expected)
{
// 0 = null, 1 = empty, 2 = one valid part; comparing that part to itself ties.
var inputs = new List<Part>?[]
{
null,
new(),
new() { TestHelpers.MakePartAt(0, 0, 10) },
};
Assert.Equal(expected, comparer.IsBetter(inputs[candidateKind], inputs[currentKind], workArea));
}
[Fact]
public void ValidLayoutMatrix_MatchesReferenceInBothOrdersAndTies_WithoutMutatingInputs()
{
var layouts = new List<List<Part>> { new() };
foreach (var count in new[] { 1, 2, 4, 7 })
foreach (var size in new[] { 1.0, 3.0 })
foreach (var pitch in new[] { 4.0, 12.0 })
foreach (var origin in new[] { new Vector(0, 0), new Vector(5, 9) })
{
var parts = new List<Part>();
for (var i = 0; i < count; i++)
parts.Add(TestHelpers.MakePartAt(origin.X + i % 3 * pitch, origin.Y + i / 3 * pitch, size));
// Also vary enumeration order; the comparer must not reorder caller lists.
if (origin.X > 0)
parts.Reverse();
layouts.Add(parts);
}
foreach (var parts in layouts)
{
for (var i = 0; i < parts.Count; i++)
{
var part = parts[i];
Assert.True(double.IsFinite(part.BaseDrawing.Area));
Assert.True(part.BaseDrawing.Area > 0);
Assert.True(workArea.Contains(part.BoundingBox));
foreach (var value in new[] { part.Left, part.Right, part.Top, part.Bottom, part.Rotation })
Assert.True(double.IsFinite(value));
for (var j = 0; j < i; j++)
Assert.False(part.BoundingBox.Intersects(parts[j].BoundingBox));
}
}
var before = layouts.Select(Snapshot).ToArray();
var workAreaBefore = (workArea.X, workArea.Y, workArea.Length, workArea.Width);
// Full Cartesian matrix includes both argument orders, self-comparisons,
// translated/permuted exact ties, and equal/unequal counts and densities.
foreach (var candidate in layouts)
foreach (var current in layouts)
{
var expected = FillScore.Compute(candidate, workArea) > FillScore.Compute(current, workArea);
Assert.Equal(expected, comparer.IsBetter(candidate, current, workArea));
}
for (var i = 0; i < layouts.Count; i++)
Assert.Equal(before[i], Snapshot(layouts[i]));
Assert.Equal(workAreaBefore, (workArea.X, workArea.Y, workArea.Length, workArea.Width));
}
private static object[] Snapshot(List<Part> parts)
{
var values = new List<object>();
foreach (var part in parts)
{
values.Add(part);
values.Add(part.BaseDrawing);
values.Add(part.BaseDrawing.Area);
values.Add(part.Location);
values.Add(part.Rotation);
values.Add(part.BoundingBox);
values.Add((part.Left, part.Right, part.Top, part.Bottom));
foreach (var program in new[] { part.Program, part.BaseDrawing.Program })
{
values.Add(program);
values.Add(program.Mode);
values.Add(program.Rotation);
foreach (var code in program.Codes)
{
values.Add(code);
if (code is Motion motion)
values.Add(motion.EndPoint);
}
}
}
return values.ToArray();
}
}
#if DEBUG
// PerfCounters is process-wide. This collection excludes all parallel tests;
// always clear counters in finally, including when the skipped-work assertion fails.
[Collection(nameof(FillCacheCollection))]
public class DefaultFillComparerWorkTests
{
[Theory]
[InlineData(2, 1, 0)]
[InlineData(1, 2, 0)]
[InlineData(2, 2, 2)]
[InlineData(0, 1, 0)]
[InlineData(1, 0, 0)]
public void IsBetter_ComputesScoresOnlyForNonemptyEqualCounts(int candidateCount, int currentCount, long expectedComputations)
{
var candidate = Enumerable.Range(0, candidateCount).Select(i => TestHelpers.MakePartAt(i * 20, 0, 10)).ToList();
var current = Enumerable.Range(0, currentCount).Select(i => TestHelpers.MakePartAt(i * 20, 0, 10)).ToList();
var workArea = new Box(0, 0, 100, 100);
var comparer = new DefaultFillComparer();
var expected = FillScore.Compute(candidate, workArea) > FillScore.Compute(current, workArea);
PerfCounters.Reset();
try
{
Assert.Equal(expected, comparer.IsBetter(candidate, current, workArea));
Assert.Equal(expectedComputations, PerfCounters.FillScoreComputations);
}
finally
{
PerfCounters.Reset();
}
}
}
#endif
public class VerticalRemnantComparerTests
{
private readonly IFillComparer comparer = new VerticalRemnantComparer();
private readonly Box workArea = new(0, 0, 100, 100);
[Fact]
public void HigherCount_WinsRegardlessOfExtent()
{
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(40, 0, 10),
TestHelpers.MakePartAt(80, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
[Fact]
public void SameCount_SmallerXExtent_Wins()
{
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(12, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(50, 0, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
[Fact]
public void SameCount_SameExtent_HigherDensityWins()
{
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(40, 0, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(40, 40, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
[Fact]
public void NullCandidate_ReturnsFalse()
{
var current = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
Assert.False(comparer.IsBetter(null, current, workArea));
}
[Fact]
public void NullCurrent_ReturnsTrue()
{
var candidate = new List<Part> { TestHelpers.MakePartAt(0, 0, 10) };
Assert.True(comparer.IsBetter(candidate, null, workArea));
}
}
public class HorizontalRemnantComparerTests
{
private readonly IFillComparer comparer = new HorizontalRemnantComparer();
private readonly Box workArea = new(0, 0, 100, 100);
[Fact]
public void SameCount_SmallerYExtent_Wins()
{
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 12, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 50, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
[Fact]
public void HigherCount_WinsRegardlessOfExtent()
{
var candidate = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 40, 10),
TestHelpers.MakePartAt(0, 80, 10),
};
var current = new List<Part>
{
TestHelpers.MakePartAt(0, 0, 10),
TestHelpers.MakePartAt(0, 12, 10),
};
Assert.True(comparer.IsBetter(candidate, current, workArea));
}
}