Files
OpenNest/OpenNest.Engine.Tests/PlateFillServiceTests.cs
T
ajandClaude Opus 5.5 4d55e11318 style: use one spelling across the codebase
The repo mixed variants such as center/centre and color/colour, so a
name could be spelled either way and searches missed half the hits.
Standardize on the US spelling, matching .NET's own APIs (Color, Center).

Wire values owned by other systems keep their spelling: the material
ledger's "Cancelled" hold status and GitHub's "cancelled" job status.

No behavior change beyond the spelling of a few status messages.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-30 16:11:30 -04:00

237 lines
8.3 KiB
C#

using System.Threading;
using OpenNest.Engine.Jobs.Placement;
using OpenNest.Engine.Tests.Jobs;
using OpenNest.Geometry;
namespace OpenNest.Engine.Tests;
public class PlateFillServiceTests
{
private static readonly string[] Strategies =
[
"Fill",
"Strip",
"Vertical Remnant",
"Horizontal Remnant",
];
[Theory]
[MemberData(nameof(StrategiesData))]
public void FillItem_ResolvesBuiltInStrategy_AndRebindsToCallerDrawing(string strategy)
{
var plate = new Plate(new Size(60, 80));
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
var progress = new CapturingProgress();
var parts = PlateFillService.FillItem(
strategy,
plate,
new NestItem { Drawing = drawing, Quantity = 6 },
plate.WorkArea(),
progress,
CancellationToken.None
);
Assert.NotEmpty(parts);
Assert.All(parts, part => Assert.Same(drawing, part.BaseDrawing));
Assert.NotEmpty(progress.Reports);
}
[Fact]
public void FillItem_DoesNotMutateCallerPlate()
{
var plate = new Plate(new Size(60, 80));
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
var parts = PlateFillService.FillItem(
"Fill",
plate,
new NestItem { Drawing = drawing, Quantity = 6 },
plate.WorkArea(),
null,
CancellationToken.None
);
Assert.NotEmpty(parts);
Assert.Empty(plate.Parts);
}
[Theory]
[MemberData(nameof(StrategiesData))]
public void FillGroup_ResolvesBuiltInStrategy(string strategy)
{
var plate = new Plate(new Size(60, 80));
var drawing = new Drawing("group", TestDrawingFactory.Rectangle(6, 4));
var groupParts = new List<Part> { new(drawing), new(drawing) };
var parts = PlateFillService.FillGroup(
strategy,
plate,
groupParts,
plate.WorkArea(),
null,
CancellationToken.None
);
Assert.True(parts.Count >= 2, $"Expected the group template placed at least once, got {parts.Count} parts for '{strategy}'.");
Assert.All(parts, part => Assert.Same(drawing, part.BaseDrawing));
}
[Theory]
[MemberData(nameof(StrategiesData))]
public void PackArea_ResolvesBuiltInStrategy(string strategy)
{
var plate = new Plate(new Size(60, 80));
var drawing = new Drawing("packed", TestDrawingFactory.Rectangle(6, 4));
var items = new List<NestItem>
{
new() { Drawing = drawing, Quantity = 3 },
};
var parts = PlateFillService.PackArea(
strategy,
plate,
plate.WorkArea(),
items,
null,
CancellationToken.None
);
Assert.NotEmpty(parts);
Assert.All(parts, part => Assert.Same(drawing, part.BaseDrawing));
}
[Fact]
public void Nest_RejectsUnknownStrategy_AndReportsPlateNumberInDetail()
{
var plate = new Plate(new Size(60, 80));
// Big enough (240 area x4 >= 10% of the 4800-area plate) to route through the fill
// pipeline, which is where progress detail is emitted.
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(20, 12));
var items = new List<NestItem> { new() { Drawing = drawing, Quantity = 4 } };
Assert.Throws<NotSupportedException>(() =>
PlateFillService.Nest("StockLadder", plate, items, null, CancellationToken.None)
);
Assert.Throws<ArgumentNullException>(() =>
PlateFillService.Nest(null!, plate, items, null, CancellationToken.None)
);
// The plateNumber overload reports the caller's plate index like the legacy
// engine's PlateNumber did for interactive multi-plate loops.
var progress = new CapturingProgress();
var parts = PlateFillService.Nest("Fill", plate, items, 3, progress, CancellationToken.None);
Assert.NotEmpty(parts);
Assert.NotEmpty(progress.Reports);
Assert.All(progress.Reports, report => Assert.Equal(3, report.PlateNumber));
Assert.Empty(plate.Parts); // proposed parts only; committing stays with the caller
}
[Theory]
[MemberData(nameof(StrategiesData))]
public void FillItem_ReturnsNoParts_WhenTokenIsAlreadyCanceled(string strategy)
{
// Quantity > 2 keeps the request off the qty 1-2 fast path so the cancellation
// check inside the strategy pipeline is actually reached.
var plate = new Plate(new Size(60, 80));
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
var progress = new CapturingProgress();
using var cancellation = new CancellationTokenSource();
cancellation.Cancel();
var parts = PlateFillService.FillItem(
strategy,
plate,
new NestItem { Drawing = drawing, Quantity = 4 },
plate.WorkArea(),
progress,
cancellation.Token
);
Assert.Empty(parts);
Assert.Empty(progress.Reports);
Assert.Empty(plate.Parts);
}
[Theory]
[InlineData("Mystery Engine")]
[InlineData("")]
[InlineData("StockLadder")]
public void AllOperations_RejectUnknownStrategy(string strategy)
{
var plate = new Plate(new Size(60, 80));
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
Assert.Throws<NotSupportedException>(() =>
PlateFillService.FillItem(strategy, plate, new NestItem { Drawing = drawing, Quantity = 1 }, plate.WorkArea(), null, CancellationToken.None)
);
Assert.Throws<NotSupportedException>(() =>
PlateFillService.FillGroup(strategy, plate, new List<Part> { new(drawing) }, plate.WorkArea(), null, CancellationToken.None)
);
Assert.Throws<NotSupportedException>(() =>
PlateFillService.PackArea(strategy, plate, plate.WorkArea(), new List<NestItem> { new() { Drawing = drawing, Quantity = 1 } }, null, CancellationToken.None)
);
}
[Fact]
public void AllOperations_RejectNullStrategy()
{
var plate = new Plate(new Size(60, 80));
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
Assert.Throws<ArgumentNullException>(() =>
PlateFillService.FillItem(null!, plate, new NestItem { Drawing = drawing, Quantity = 1 }, plate.WorkArea(), null, CancellationToken.None)
);
Assert.Throws<ArgumentNullException>(() =>
PlateFillService.FillGroup(null!, plate, new List<Part> { new(drawing) }, plate.WorkArea(), null, CancellationToken.None)
);
Assert.Throws<ArgumentNullException>(() =>
PlateFillService.PackArea(null!, plate, plate.WorkArea(), new List<NestItem> { new() { Drawing = drawing, Quantity = 1 } }, null, CancellationToken.None)
);
}
[Fact]
public void FillItem_ResolvesStrategyNames_CaseInsensitively()
{
// The legacy registry matched ActiveEngineName with OrdinalIgnoreCase; the service
// keeps that tolerance for its explicit strategy parameter.
var plate = new Plate(new Size(60, 80));
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
var parts = PlateFillService.FillItem(
"vertical remnant",
plate,
new NestItem { Drawing = drawing, Quantity = 4 },
plate.WorkArea(),
null,
CancellationToken.None
);
Assert.NotEmpty(parts);
}
[Fact]
public void FillItem_RejectsNullPlate()
{
var drawing = new Drawing("part", TestDrawingFactory.Rectangle(6, 4));
Assert.Throws<ArgumentNullException>(() =>
PlateFillService.FillItem("Fill", null!, new NestItem { Drawing = drawing, Quantity = 1 }, new Box(0, 0, 10, 10), null, CancellationToken.None)
);
}
public static IEnumerable<object[]> StrategiesData() =>
Strategies.Select(strategy => new object[] { strategy });
private sealed class CapturingProgress : IProgress<NestProgress>
{
public List<NestProgress> Reports { get; } = new();
public void Report(NestProgress value)
{
Reports.Add(value);
}
}
}