feat: add xUnit test project with fill and remnant fill tests
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@@ -0,0 +1,2 @@
|
||||
bin/
|
||||
obj/
|
||||
71
FillTests.cs
Normal file
71
FillTests.cs
Normal file
@@ -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<Part> 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}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
24
OpenNest.Test.csproj
Normal file
24
OpenNest.Test.csproj
Normal file
@@ -0,0 +1,24 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<TargetFramework>net8.0-windows</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<IsPackable>false</IsPackable>
|
||||
<IsTestProject>true</IsTestProject>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.*" />
|
||||
<PackageReference Include="xunit" Version="2.*" />
|
||||
<PackageReference Include="xunit.runner.visualstudio" Version="2.*" />
|
||||
<PackageReference Include="Xunit.SkippableFact" Version="1.*" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\OpenNest\OpenNest.Core\OpenNest.Core.csproj" />
|
||||
<ProjectReference Include="..\OpenNest\OpenNest.Engine\OpenNest.Engine.csproj" />
|
||||
<ProjectReference Include="..\OpenNest\OpenNest.IO\OpenNest.IO.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
97
RemnantFillTests.cs
Normal file
97
RemnantFillTests.cs
Normal file
@@ -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<Part> 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<Part> existing, List<Part> 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}]");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
64
TestData.cs
Normal file
64
TestData.cs
Normal file
@@ -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;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user