From 8fcd3660d8b40a294d0b4895894e52f5814241ad Mon Sep 17 00:00:00 2001 From: AJ Isaacs Date: Mon, 9 Mar 2026 21:07:04 -0400 Subject: [PATCH] feat: add xUnit test project with fill and remnant fill tests Co-Authored-By: Claude Opus 4.6 --- .gitignore | 2 + FillTests.cs | 71 ++++++++++++++++++++++++++++++++ OpenNest.Test.csproj | 24 +++++++++++ RemnantFillTests.cs | 97 ++++++++++++++++++++++++++++++++++++++++++++ TestData.cs | 64 +++++++++++++++++++++++++++++ 5 files changed, 258 insertions(+) create mode 100644 .gitignore create mode 100644 FillTests.cs create mode 100644 OpenNest.Test.csproj create mode 100644 RemnantFillTests.cs create mode 100644 TestData.cs diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..cd42ee3 --- /dev/null +++ b/.gitignore @@ -0,0 +1,2 @@ +bin/ +obj/ diff --git a/FillTests.cs b/FillTests.cs new file mode 100644 index 0000000..3199893 --- /dev/null +++ b/FillTests.cs @@ -0,0 +1,71 @@ +using OpenNest.Geometry; +using Xunit; +using Xunit.Abstractions; + +namespace OpenNest.Test; + +public class FillTests +{ + private readonly ITestOutputHelper _output; + + public FillTests(ITestOutputHelper output) + { + _output = output; + } + + [SkippableFact] + [Trait("Category", "Fill")] + public void N0308_008_HingePlate_FillsAtLeast75() + { + Skip.IfNot(TestData.IsAvailable, TestData.SkipReason); + + var nest = TestData.LoadNest("N0308-008.zip"); + var hinge = nest.Drawings.First(d => d.Name.Contains("HINGE PLATE #2")); + var plate = TestData.CleanPlateFrom(nest.Plates[0]); + + var engine = new NestEngine(plate); + var sw = System.Diagnostics.Stopwatch.StartNew(); + engine.Fill(new NestItem { Drawing = hinge, Quantity = 0 }); + sw.Stop(); + + _output.WriteLine($"Parts: {plate.Parts.Count} | Time: {sw.ElapsedMilliseconds}ms"); + + Assert.True(plate.Parts.Count >= 75, + $"Expected >= 75 parts, got {plate.Parts.Count}"); + AssertNoOverlaps(plate.Parts.ToList()); + } + + [SkippableFact] + [Trait("Category", "Fill")] + public void RemainderStripRefill_30pcs_FillsAtLeast32() + { + Skip.IfNot(TestData.IsAvailable, TestData.SkipReason); + + var nest = TestData.LoadNest("30pcs Fill.zip"); + var drawing = nest.Drawings.First(); + var plate = TestData.CleanPlateFrom(nest.Plates[0]); + + var engine = new NestEngine(plate); + var sw = System.Diagnostics.Stopwatch.StartNew(); + engine.Fill(new NestItem { Drawing = drawing, Quantity = 0 }); + sw.Stop(); + + _output.WriteLine($"Parts: {plate.Parts.Count} | Time: {sw.ElapsedMilliseconds}ms"); + + Assert.True(plate.Parts.Count >= 32, + $"Expected >= 32 parts, got {plate.Parts.Count}"); + AssertNoOverlaps(plate.Parts.ToList()); + } + + private void AssertNoOverlaps(List parts) + { + for (var i = 0; i < parts.Count; i++) + { + for (var j = i + 1; j < parts.Count; j++) + { + if (parts[i].Intersects(parts[j], out _)) + Assert.Fail($"Overlap detected: part [{i}] vs [{j}]"); + } + } + } +} diff --git a/OpenNest.Test.csproj b/OpenNest.Test.csproj new file mode 100644 index 0000000..b29dd95 --- /dev/null +++ b/OpenNest.Test.csproj @@ -0,0 +1,24 @@ + + + + net8.0-windows + enable + enable + false + true + + + + + + + + + + + + + + + + diff --git a/RemnantFillTests.cs b/RemnantFillTests.cs new file mode 100644 index 0000000..c19a329 --- /dev/null +++ b/RemnantFillTests.cs @@ -0,0 +1,97 @@ +using OpenNest.Geometry; +using Xunit; +using Xunit.Abstractions; + +namespace OpenNest.Test; + +public class RemnantFillTests +{ + private readonly ITestOutputHelper _output; + + public RemnantFillTests(ITestOutputHelper output) + { + _output = output; + } + + [SkippableFact] + [Trait("Category", "Remnant")] + public void N0308_017_PT02_RemnantFillsAtLeast10() + { + Skip.IfNot(TestData.IsAvailable, TestData.SkipReason); + + var nest = TestData.LoadNest("N0308-017.zip"); + var plate = nest.Plates[0]; + var pt02 = nest.Drawings.First(d => d.Name.Contains("PT02")); + var remnant = plate.GetRemnants()[0]; + + _output.WriteLine($"Remnant: ({remnant.X:F2},{remnant.Y:F2}) {remnant.Width:F2}x{remnant.Height:F2}"); + + var countBefore = plate.Parts.Count; + var engine = new NestEngine(plate); + var sw = System.Diagnostics.Stopwatch.StartNew(); + engine.Fill(new NestItem { Drawing = pt02, Quantity = 0 }, remnant); + sw.Stop(); + + var added = plate.Parts.Count - countBefore; + _output.WriteLine($"Added: {added} parts | Time: {sw.ElapsedMilliseconds}ms"); + + Assert.True(added >= 10, $"Expected >= 10 parts in remnant, got {added}"); + + var newParts = plate.Parts.Skip(countBefore).ToList(); + AssertNoOverlaps(newParts); + AssertNoCrossOverlaps(plate.Parts.Take(countBefore).ToList(), newParts); + } + + [SkippableFact] + [Trait("Category", "Remnant")] + public void N0308_008_HingePlate_RemnantFillsAtLeast8() + { + Skip.IfNot(TestData.IsAvailable, TestData.SkipReason); + + var nest = TestData.LoadNest("N0308-008.zip"); + var plate = nest.Plates[0]; + var hinge = nest.Drawings.First(d => d.Name.Contains("HINGE PLATE #2")); + var remnants = plate.GetRemnants(); + + _output.WriteLine($"Remnant 0: ({remnants[0].X:F2},{remnants[0].Y:F2}) {remnants[0].Width:F2}x{remnants[0].Height:F2}"); + + var countBefore = plate.Parts.Count; + var engine = new NestEngine(plate); + var sw = System.Diagnostics.Stopwatch.StartNew(); + engine.Fill(new NestItem { Drawing = hinge, Quantity = 0 }, remnants[0]); + sw.Stop(); + + var added = plate.Parts.Count - countBefore; + _output.WriteLine($"Added: {added} parts | Time: {sw.ElapsedMilliseconds}ms"); + + Assert.True(added >= 8, $"Expected >= 8 parts in remnant, got {added}"); + + var newParts = plate.Parts.Skip(countBefore).ToList(); + AssertNoOverlaps(newParts); + AssertNoCrossOverlaps(plate.Parts.Take(countBefore).ToList(), newParts); + } + + private void AssertNoOverlaps(List parts) + { + for (var i = 0; i < parts.Count; i++) + { + for (var j = i + 1; j < parts.Count; j++) + { + if (parts[i].Intersects(parts[j], out _)) + Assert.Fail($"Overlap detected: part [{i}] vs [{j}]"); + } + } + } + + private void AssertNoCrossOverlaps(List existing, List added) + { + for (var i = 0; i < existing.Count; i++) + { + for (var j = 0; j < added.Count; j++) + { + if (existing[i].Intersects(added[j], out _)) + Assert.Fail($"Cross-overlap: existing [{i}] vs added [{j}]"); + } + } + } +} diff --git a/TestData.cs b/TestData.cs new file mode 100644 index 0000000..fb30d32 --- /dev/null +++ b/TestData.cs @@ -0,0 +1,64 @@ +using OpenNest.IO; + +namespace OpenNest.Test; + +public static class TestData +{ + private static readonly string? BasePath = ResolveBasePath(); + + public static bool IsAvailable => BasePath != null; + + public static string SkipReason => + "Test data not found. Fixture .zip files should be in the repo root."; + + public static string GetPath(string filename) + { + if (BasePath == null) + throw new InvalidOperationException(SkipReason); + + var path = Path.Combine(BasePath, filename); + + if (!File.Exists(path)) + throw new FileNotFoundException($"Test fixture not found: {path}"); + + return path; + } + + public static Nest LoadNest(string filename) + { + var reader = new NestReader(GetPath(filename)); + return reader.Read(); + } + + public static Plate CleanPlateFrom(Plate reference) + { + var plate = new Plate(); + plate.Size = reference.Size; + plate.PartSpacing = reference.PartSpacing; + plate.EdgeSpacing = reference.EdgeSpacing; + plate.Quadrant = reference.Quadrant; + return plate; + } + + private static string? ResolveBasePath() + { + // Walk up from bin/Debug/net8.0-windows to find the repo root + // (where the .zip fixture files live alongside the csproj). + var dir = AppContext.BaseDirectory; + + for (var i = 0; i < 6; i++) + { + var parent = Directory.GetParent(dir); + + if (parent == null) + break; + + dir = parent.FullName; + + if (File.Exists(Path.Combine(dir, "OpenNest.Test.csproj"))) + return dir; + } + + return null; + } +}