From 51b482aefbfa24de2d44511b1cbad8068a98ccf7 Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 16 Mar 2026 13:02:15 -0400 Subject: [PATCH] test: add RemnantFinder edge cases and FillScore comparison tests RemnantFinder: obstacle clipping, overlapping obstacles, iterative workflow, grid pattern, no-overlap invariant, constructor/AddObstacles. FillScore: count-vs-density ordering, operators, Compute edge cases. Co-Authored-By: Claude Opus 4.6 (1M context) --- OpenNest.Tests/FillScoreTests.cs | 89 ++++++++++++++++ OpenNest.Tests/RemnantFinderTests.cs | 147 +++++++++++++++++++++++++++ 2 files changed, 236 insertions(+) create mode 100644 OpenNest.Tests/FillScoreTests.cs diff --git a/OpenNest.Tests/FillScoreTests.cs b/OpenNest.Tests/FillScoreTests.cs new file mode 100644 index 0000000..a069675 --- /dev/null +++ b/OpenNest.Tests/FillScoreTests.cs @@ -0,0 +1,89 @@ +namespace OpenNest.Tests; + +public class FillScoreTests +{ + [Fact] + public void HigherCount_WinsOverLowerCount() + { + var a = new FillScore(10, 0.5); + var b = new FillScore(5, 0.9); + + Assert.True(a > b); + Assert.False(b > a); + } + + [Fact] + public void SameCount_HigherDensityWins() + { + var a = new FillScore(10, 0.8); + var b = new FillScore(10, 0.5); + + Assert.True(a > b); + Assert.False(b > a); + } + + [Fact] + public void EqualScores_AreNotGreaterOrLess() + { + var a = new FillScore(10, 0.5); + var b = new FillScore(10, 0.5); + + Assert.False(a > b); + Assert.False(a < b); + Assert.True(a >= b); + Assert.True(a <= b); + } + + [Fact] + public void Default_IsZero() + { + var score = default(FillScore); + + Assert.Equal(0, score.Count); + Assert.Equal(0, score.Density); + } + + [Fact] + public void Compute_NullParts_ReturnsDefault() + { + var score = FillScore.Compute(null, new Geometry.Box(0, 0, 100, 100)); + + Assert.Equal(0, score.Count); + } + + [Fact] + public void Compute_EmptyParts_ReturnsDefault() + { + var score = FillScore.Compute(new System.Collections.Generic.List(), new Geometry.Box(0, 0, 100, 100)); + + Assert.Equal(0, score.Count); + } + + [Fact] + public void Compute_WithParts_ReturnsCorrectCount() + { + var parts = new System.Collections.Generic.List + { + TestHelpers.MakePartAt(0, 0, 10), + TestHelpers.MakePartAt(20, 0, 10), + TestHelpers.MakePartAt(40, 0, 10) + }; + var score = FillScore.Compute(parts, new Geometry.Box(0, 0, 100, 100)); + + Assert.Equal(3, score.Count); + Assert.True(score.Density > 0); + } + + [Fact] + public void CompareTo_IsConsistentWithOperators() + { + var a = new FillScore(10, 0.8); + var b = new FillScore(5, 0.9); + + Assert.True(a.CompareTo(b) > 0); + Assert.True(a > b); + Assert.True(b < a); + Assert.True(a >= b); + Assert.True(b <= a); + } +} diff --git a/OpenNest.Tests/RemnantFinderTests.cs b/OpenNest.Tests/RemnantFinderTests.cs index f77602b..db4d154 100644 --- a/OpenNest.Tests/RemnantFinderTests.cs +++ b/OpenNest.Tests/RemnantFinderTests.cs @@ -122,4 +122,151 @@ public class RemnantFinderTests Assert.True(remnants.Count >= 1); Assert.True(remnants[0].Area() < plate.WorkArea().Area()); } + + [Fact] + public void ObstacleOutsideWorkArea_IsIgnored() + { + var finder = new RemnantFinder(new Box(0, 0, 100, 100)); + finder.AddObstacle(new Box(200, 200, 50, 50)); + var remnants = finder.FindRemnants(); + + Assert.Single(remnants); + Assert.Equal(100 * 100, remnants[0].Area(), 0.1); + } + + [Fact] + public void ObstaclePartiallyOutsideWorkArea_IsClipped() + { + var finder = new RemnantFinder(new Box(0, 0, 100, 100)); + // Obstacle extends 20 units past the right edge + finder.AddObstacle(new Box(80, 0, 40, 100)); + var remnants = finder.FindRemnants(); + + // Should find the 80x100 strip on the left + var left = remnants.FirstOrDefault(r => + r.Width >= 79.9 && r.Width <= 80.1 && + r.Length >= 99.9); + Assert.NotNull(left); + } + + [Fact] + public void OverlappingObstacles_HandledCorrectly() + { + var finder = new RemnantFinder(new Box(0, 0, 100, 100)); + finder.AddObstacle(new Box(0, 0, 60, 60)); + finder.AddObstacle(new Box(40, 40, 60, 60)); // overlaps first + var remnants = finder.FindRemnants(); + + // No remnant should overlap either obstacle + foreach (var r in remnants) + { + Assert.False( + r.Left < 60 && r.Right > 0 && r.Bottom < 60 && r.Top > 0 + && r.Left < 100 && r.Right > 40 && r.Bottom < 100 && r.Top > 40, + "Remnant should not overlap both obstacles simultaneously in their shared region"); + } + + // Total remnant area + obstacle coverage should not exceed work area + var totalRemnantArea = remnants.Sum(r => r.Area()); + Assert.True(totalRemnantArea < 100 * 100); + Assert.True(totalRemnantArea > 0); + } + + [Fact] + public void ConstructorWithObstaclesList() + { + var obstacles = new List + { + new Box(0, 0, 40, 100), + new Box(60, 0, 40, 100) + }; + var finder = new RemnantFinder(new Box(0, 0, 100, 100), obstacles); + var remnants = finder.FindRemnants(); + + var gap = remnants.FirstOrDefault(r => + r.Width >= 19.9 && r.Width <= 20.1); + Assert.NotNull(gap); + } + + [Fact] + public void AddObstacles_Plural_AddsMultiple() + { + var finder = new RemnantFinder(new Box(0, 0, 100, 100)); + finder.AddObstacles(new[] + { + new Box(0, 0, 40, 100), + new Box(60, 0, 40, 100) + }); + var remnants = finder.FindRemnants(); + + var gap = remnants.FirstOrDefault(r => + r.Width >= 19.9 && r.Width <= 20.1); + Assert.NotNull(gap); + } + + [Fact] + public void IterativeWorkflow_AddObstacleThenRequery() + { + var finder = new RemnantFinder(new Box(0, 0, 100, 100)); + + // First fill: obstacle in bottom-left + finder.AddObstacle(new Box(0, 0, 50, 50)); + var pass1 = finder.FindRemnants(); + Assert.True(pass1.Count >= 2); + + // Simulate filling the largest remnant by adding another obstacle + var largest = pass1[0]; + finder.AddObstacle(new Box(largest.X, largest.Y, largest.Width / 2, largest.Length)); + var pass2 = finder.FindRemnants(); + + // Should have more, smaller remnants now + var pass2TotalArea = pass2.Sum(r => r.Area()); + var pass1TotalArea = pass1.Sum(r => r.Area()); + Assert.True(pass2TotalArea < pass1TotalArea); + } + + [Fact] + public void NoRemnantOverlapsObstacle() + { + var finder = new RemnantFinder(new Box(0, 0, 100, 100)); + finder.AddObstacle(new Box(20, 20, 30, 30)); + finder.AddObstacle(new Box(60, 10, 25, 80)); + var remnants = finder.FindRemnants(); + + foreach (var r in remnants) + { + // Check no remnant overlaps obstacle 1 + var overlaps1 = r.Left < 50 && r.Right > 20 && r.Bottom < 50 && r.Top > 20; + Assert.False(overlaps1, $"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 1"); + + // Check no remnant overlaps obstacle 2 + var overlaps2 = r.Left < 85 && r.Right > 60 && r.Bottom < 90 && r.Top > 10; + Assert.False(overlaps2, $"Remnant ({r.X},{r.Y} {r.Width}x{r.Length}) overlaps obstacle 2"); + } + } + + [Fact] + public void ManyObstacles_GridPattern() + { + var finder = new RemnantFinder(new Box(0, 0, 100, 100)); + + // Place a 5x5 grid of 10x10 obstacles with 10-unit gaps + for (var row = 0; row < 5; row++) + for (var col = 0; col < 5; col++) + finder.AddObstacle(new Box(col * 20, row * 20, 10, 10)); + + var remnants = finder.FindRemnants(); + + // All remnants should be within the work area + foreach (var r in remnants) + { + Assert.True(r.Left >= 0); + Assert.True(r.Bottom >= 0); + Assert.True(r.Right <= 100); + Assert.True(r.Top <= 100); + } + + // Should find gaps between obstacles + Assert.True(remnants.Count > 0); + } }