mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-11-04 13:41:37 -05:00
DeadlineNestingEngine wraps any engine with a wall-clock limit (any positive time up to the cancellation timer's ~49.7 days). An engine that finishes in time keeps its result, as does a result it returns after the limit. When the limit cancels it, the best whole-job plan it reported (PlanImproved, ranked like Default ranks candidates, a tie going to the later plan) is returned if it passes the layout check; with none the solve fails with TimeoutException. The caller's own cancellation is never turned into a result. Engine plate commits are dropped and the returned result's commits reported once it is known; plans are forwarded only when they beat every plan shown so far. Default now runs Rectangles, then Irregular, then Circles, so a search stopped early already holds a valid plan. Ties still break by candidate order (Irregular, Rectangles, Circles), so for these three the choice is unchanged, and when every candidate throws, the first in that order is still the one rethrown. A test seam takes a run order. Desktop, Console and MCP do not apply a limit yet; Console can solve a job twice (cutout trial and plain), which needs one shared deadline. Mutations killed (10/10): no fallback at the limit, invalid plan accepted, caller cancellation becomes a result, engine commits forwarded, a worse later plan replaces the best, a tie keeps the earlier plan, no upper limit bound, failure rethrown in run order, built-in run order unchanged, run order ignored.
275 lines
9.2 KiB
C#
275 lines
9.2 KiB
C#
using OpenNest.Engine.Jobs;
|
|
using OpenNest.Engine.NestingEngines;
|
|
using OpenNest.Engine.NestingEngines.Default;
|
|
using static OpenNest.Engine.Tests.NestingEngines.JobBuilder;
|
|
using static OpenNest.Engine.Tests.NestingEngines.Shapes;
|
|
|
|
namespace OpenNest.Engine.Tests.NestingEngines;
|
|
|
|
public class DeadlineNestingEngineTests
|
|
{
|
|
// Two 4x4 squares on 10x10 sheets with no spacing.
|
|
private static NestJob SquaresJob() =>
|
|
Job([Part("p", Rectangle(4, 4), 2, RotationPolicy.Fixed(0))], [Stock("s", 10, 10)]);
|
|
|
|
private static NestJobResult Layout(NestJob job, params (double X, double Y)[][] sheets)
|
|
{
|
|
var builder = new NestJobResultBuilder(job);
|
|
foreach (var sheet in sheets)
|
|
builder.AddSheet(job.Plates[0], sheet.Select(p => ("p", p.X, p.Y, 0.0)));
|
|
return builder.Build(
|
|
builder.IsComplete ? NestJobStopReason.Completed : NestJobStopReason.NoPlacementFound
|
|
);
|
|
}
|
|
|
|
private static NestJobResult OneSheet(NestJob job) => Layout(job, [(0, 0), (5, 0)]);
|
|
|
|
private static NestJobResult TwoSheets(NestJob job) => Layout(job, [(0, 0)], [(0, 0)]);
|
|
|
|
private static NestJobResult Overlapping(NestJob job) => Layout(job, [(0, 0), (1, 0)]);
|
|
|
|
private sealed class Stub(
|
|
Func<NestJob, IProgress<NestJobProgress>?, CancellationToken, NestJobResult> solve
|
|
) : INestingEngine
|
|
{
|
|
public NestJobResult Solve(
|
|
NestJob job,
|
|
IProgress<NestJobProgress>? progress = null,
|
|
CancellationToken token = default
|
|
) => solve(job, progress, token);
|
|
}
|
|
|
|
private sealed class Recorder : IProgress<NestJobProgress>
|
|
{
|
|
public List<NestJobProgress> Reports { get; } = new();
|
|
|
|
public void Report(NestJobProgress value) => Reports.Add(value);
|
|
}
|
|
|
|
private static void Plan(IProgress<NestJobProgress>? progress, NestJobResult plan) =>
|
|
progress?.Report(new NestJobProgress(NestJobStage.PlanImproved, "", -1, 0, 0, Plan: plan));
|
|
|
|
/// <summary>Reports the given plans, then waits for cancellation like a long search.</summary>
|
|
private static Stub ReportsThenRunsUntilCancelled(params Func<NestJob, NestJobResult>[] plans) =>
|
|
new((job, progress, token) =>
|
|
{
|
|
foreach (var plan in plans)
|
|
Plan(progress, plan(job));
|
|
token.WaitHandle.WaitOne(TimeSpan.FromSeconds(30));
|
|
token.ThrowIfCancellationRequested();
|
|
throw new InvalidOperationException("the limit never cancelled the search");
|
|
});
|
|
|
|
private static readonly TimeSpan Short = TimeSpan.FromMilliseconds(200);
|
|
|
|
[Fact]
|
|
public void AnEngineThatFinishesInTimeKeepsItsResult()
|
|
{
|
|
var job = SquaresJob();
|
|
var result = TwoSheets(job);
|
|
var progress = new Recorder();
|
|
|
|
var solved = new DeadlineNestingEngine(
|
|
new Stub((_, p, _) =>
|
|
{
|
|
Plan(p, OneSheet(job));
|
|
return result;
|
|
}),
|
|
TimeSpan.FromMinutes(1)
|
|
).Solve(job, progress);
|
|
|
|
Assert.Same(result, solved);
|
|
var commits = progress.Reports.Where(r => r.Stage == NestJobStage.PlateCommitted).ToList();
|
|
Assert.Equal(2, commits.Count);
|
|
Assert.Equal(2, commits[^1].CommittedParts);
|
|
}
|
|
|
|
[Fact]
|
|
public void AtTheLimitTheBestValidPlanIsReturnedAndCommitted()
|
|
{
|
|
var job = SquaresJob();
|
|
NestJobResult? cheap = null;
|
|
var progress = new Recorder();
|
|
|
|
var solved = new DeadlineNestingEngine(
|
|
ReportsThenRunsUntilCancelled(TwoSheets, j => cheap = OneSheet(j), TwoSheets),
|
|
Short
|
|
).Solve(job, progress);
|
|
|
|
Assert.Same(cheap, solved);
|
|
var commits = progress.Reports.Where(r => r.Stage == NestJobStage.PlateCommitted).ToList();
|
|
Assert.Single(commits);
|
|
Assert.Equal(2, commits[0].CommittedParts);
|
|
// The later, worse plan is not shown after the better one.
|
|
var shown = progress.Reports.Where(r => r.Stage == NestJobStage.PlanImproved).ToList();
|
|
Assert.Equal(2, shown.Count);
|
|
Assert.Same(cheap, shown[^1].Plan);
|
|
}
|
|
|
|
[Fact]
|
|
public void AnInvalidPlanIsNeverReturned()
|
|
{
|
|
var job = SquaresJob();
|
|
Assert.NotEmpty(NestLayoutCheck.Violations(job, Overlapping(job)));
|
|
|
|
var error = Assert.Throws<TimeoutException>(() =>
|
|
new DeadlineNestingEngine(ReportsThenRunsUntilCancelled(Overlapping), Short).Solve(job)
|
|
);
|
|
|
|
Assert.Contains("No valid layout", error.Message);
|
|
}
|
|
|
|
[Fact]
|
|
public void AValidPlanBeatsAnEarlierInvalidOne()
|
|
{
|
|
var job = SquaresJob();
|
|
NestJobResult? valid = null;
|
|
|
|
var solved = new DeadlineNestingEngine(
|
|
ReportsThenRunsUntilCancelled(Overlapping, j => valid = TwoSheets(j)),
|
|
Short
|
|
).Solve(job);
|
|
|
|
Assert.Same(valid, solved);
|
|
}
|
|
|
|
[Fact]
|
|
public void WithNoPlanTheLimitIsATimeout()
|
|
{
|
|
var job = SquaresJob();
|
|
|
|
Assert.Throws<TimeoutException>(() =>
|
|
new DeadlineNestingEngine(ReportsThenRunsUntilCancelled(), Short).Solve(job)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheCallersCancellationIsNeverTurnedIntoAResult()
|
|
{
|
|
var job = SquaresJob();
|
|
using var caller = new CancellationTokenSource(Short);
|
|
|
|
Assert.ThrowsAny<OperationCanceledException>(() =>
|
|
new DeadlineNestingEngine(ReportsThenRunsUntilCancelled(OneSheet), TimeSpan.FromMinutes(1))
|
|
.Solve(job, null, caller.Token)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheCallersCancellationIsNeverReportedAsATimeout()
|
|
{
|
|
var job = SquaresJob();
|
|
using var caller = new CancellationTokenSource(Short);
|
|
|
|
Assert.ThrowsAny<OperationCanceledException>(() =>
|
|
new DeadlineNestingEngine(ReportsThenRunsUntilCancelled(), TimeSpan.FromMinutes(1))
|
|
.Solve(job, null, caller.Token)
|
|
);
|
|
}
|
|
|
|
[Fact]
|
|
public void TheEnginesOwnCommitsAreNotForwarded()
|
|
{
|
|
var job = SquaresJob();
|
|
var result = OneSheet(job);
|
|
var progress = new Recorder();
|
|
|
|
new DeadlineNestingEngine(
|
|
new Stub((_, p, _) =>
|
|
{
|
|
p?.Report(new NestJobProgress(NestJobStage.PlateCommitted, "s", 0, 1, 2));
|
|
return result;
|
|
}),
|
|
TimeSpan.FromMinutes(1)
|
|
).Solve(job, progress);
|
|
|
|
Assert.Single(progress.Reports, r => r.Stage == NestJobStage.PlateCommitted);
|
|
}
|
|
|
|
[Fact]
|
|
public void ALimitMustFitACancellationTimer()
|
|
{
|
|
var engine = new Stub((_, _, _) => null!);
|
|
foreach (var bad in new[] { TimeSpan.Zero, TimeSpan.FromSeconds(-1), TimeSpan.FromDays(50) })
|
|
Assert.Throws<ArgumentOutOfRangeException>(() => new DeadlineNestingEngine(engine, bad));
|
|
Assert.Equal(
|
|
DeadlineNestingEngine.MaxLimit,
|
|
new DeadlineNestingEngine(engine, DeadlineNestingEngine.MaxLimit).Limit
|
|
);
|
|
Assert.Throws<ArgumentNullException>(() => new DeadlineNestingEngine(null!, Short));
|
|
}
|
|
|
|
[Fact]
|
|
public void ATiedLaterPlanReplacesTheEarlierOne()
|
|
{
|
|
// Engines report only plans they rank higher, so a tie follows the engine's judgment.
|
|
var job = SquaresJob();
|
|
NestJobResult? later = null;
|
|
var progress = new Recorder();
|
|
|
|
var solved = new DeadlineNestingEngine(
|
|
ReportsThenRunsUntilCancelled(OneSheet, j => later = Layout(j, [(6, 0), (6, 4)])),
|
|
Short
|
|
).Solve(job, progress);
|
|
|
|
Assert.Same(later, solved);
|
|
Assert.Same(later, progress.Reports.Last(r => r.Stage == NestJobStage.PlanImproved).Plan);
|
|
}
|
|
|
|
[Fact]
|
|
public void AResultReturnedAfterTheLimitIsTheEnginesOwn()
|
|
{
|
|
var job = SquaresJob();
|
|
var result = TwoSheets(job);
|
|
|
|
var solved = new DeadlineNestingEngine(
|
|
new Stub((j, p, token) =>
|
|
{
|
|
Plan(p, OneSheet(j));
|
|
token.WaitHandle.WaitOne(TimeSpan.FromSeconds(30));
|
|
return result;
|
|
}),
|
|
Short
|
|
).Solve(job);
|
|
|
|
Assert.Same(result, solved);
|
|
}
|
|
|
|
[Fact]
|
|
public void OtherProgressIsForwarded()
|
|
{
|
|
var job = SquaresJob();
|
|
var progress = new Recorder();
|
|
var stage = new NestJobProgress(NestJobStage.EvaluatingCandidate, "s", 0, 0, 2);
|
|
|
|
new DeadlineNestingEngine(
|
|
new Stub((j, p, _) =>
|
|
{
|
|
p?.Report(stage);
|
|
return OneSheet(j);
|
|
}),
|
|
TimeSpan.FromMinutes(1)
|
|
).Solve(job, progress);
|
|
|
|
Assert.Same(stage, progress.Reports[0]);
|
|
}
|
|
|
|
[Fact]
|
|
public void DefaultUnderALimitReturnsTheFasterCandidatesResult()
|
|
{
|
|
// The slow candidate is first in tie-break order but runs second, so the limit finds
|
|
// the quick candidate's layout already reported.
|
|
var job = SquaresJob();
|
|
NestJobResult? quick = null;
|
|
var slow = ReportsThenRunsUntilCancelled();
|
|
var fast = new Stub((j, _, _) => quick = TwoSheets(j));
|
|
|
|
var solved = new DeadlineNestingEngine(
|
|
new DefaultNestingEngine(new Func<INestingEngine>[] { () => slow, () => fast }, new[] { 1, 0 }),
|
|
Short
|
|
).Solve(job);
|
|
|
|
Assert.Same(quick, solved);
|
|
}
|
|
}
|