using OpenNest.Engine.Jobs; using OpenNest.Engine.Jobs.Remote; using OpenNest.Geometry; namespace OpenNest.Engine.Tests.Jobs; /// /// The nest server worker's live progress: the recording engine passes each solve's progress /// on, and a plan sent over the wire is drawn on the desktop as the same sheets a local run /// shows in its best-plan-so-far strip. /// public class RemoteNestProgressTests { private sealed class Collect : IProgress { public readonly List Reports = new(); public void Report(NestJobProgress value) { lock (Reports) Reports.Add(value); } } private static NestPipelineRequest Request(string engine) { var template = new Plate(48, 96) { EdgeSpacing = new Spacing(0.25, 0.25, 0.25, 0.25), PartSpacing = 0.25 }; return new NestPipelineRequest(engine, [ new NestItem { Drawing = new Drawing("bracket", TestDrawingFactory.Rectangle(10, 20)), Quantity = 9 }, new NestItem { Drawing = new Drawing("ell", TestDrawingFactory.LShape()), Quantity = 6, Priority = -1 }, ], NestStockBuilder.FromTemplate(template, null), new NestJobOptions(maxPlates: 5)); } private static IEnumerable<(string Name, double X, double Y, double Rotation)> Poses(IEnumerable sheets) => sheets.SelectMany(s => s.Parts.Select(p => (p.BaseDrawing.Name, p.Location.X, p.Location.Y, p.Rotation))); [Theory] [InlineData("Rectangles")] [InlineData("Default")] public void AWirePlanDrawsAsTheLocalPlanSheets(string engine) { var request = Request(engine); var local = new Collect(); NestPipeline.Run(request, local); var plans = local.Reports.Where(r => r.Stage == NestJobStage.PlanImproved && r.PlanSheets != null).ToList(); Assert.NotEmpty(plans); var preview = new RemoteNestPlanPreview(request); foreach (var plan in plans) { var sheets = preview.Sheets(RemoteNestPlan.From(plan.Plan).ToJson()); Assert.NotNull(sheets); Assert.Equal(plan.PlanSheets.Select(s => (s.Stock.Id, s.Quantity)), sheets.Select(s => (s.Stock.Id, s.Quantity))); Assert.Equal(Poses(plan.PlanSheets), Poses(sheets)); // Detached drawings: the caller's are never touched. Assert.All(sheets.SelectMany(s => s.Parts), p => Assert.DoesNotContain(request.Items, i => ReferenceEquals(i.Drawing, p.BaseDrawing))); } } [Fact] public void ACutoutTrialPlanDrawsItsFrameProxies() { // A frame whose window takes the strap: the trial's plans name a frame proxy, which the // preview bundles on the desktop the first time it sees one. var frame = TestDrawingFactory.Rectangle(20, 20); frame.Codes.Add(new OpenNest.CNC.RapidMove(4, 3)); frame.Codes.Add(new OpenNest.CNC.LinearMove(16, 3)); frame.Codes.Add(new OpenNest.CNC.LinearMove(16, 17)); frame.Codes.Add(new OpenNest.CNC.LinearMove(4, 17)); frame.Codes.Add(new OpenNest.CNC.LinearMove(4, 3)); var request = new NestPipelineRequest("Default", [ new NestItem { Drawing = new Drawing("frame", frame), Quantity = 2 }, new NestItem { Drawing = new Drawing("strap", TestDrawingFactory.Rectangle(13, 1.25)), Quantity = 2 }, ], new[] { new NestPlateStock("sheet", new Size(48, 48), 2, 0.25) }); var tap = new Collect(); NestPipeline.Run(new RecordingNestingEngine(NestingEngineRegistry.Create("Default")) { Progress = tap }, "Default", request); var proxyPlans = tap.Reports.Where(r => r.Stage == NestJobStage.PlanImproved && r.Plan != null && r.Plan.Plates.Any(p => p.Placements.Any(x => x.PartId.StartsWith("__cutout-proxy-")))).ToList(); Assert.NotEmpty(proxyPlans); var preview = new RemoteNestPlanPreview(request); var sheets = preview.Sheets(RemoteNestPlan.From(proxyPlans[^1].Plan).ToJson()); Assert.NotNull(sheets); Assert.Equal(proxyPlans[^1].Plan.Plates.Sum(p => p.Placements.Count), sheets.Sum(s => s.Quantity * s.Parts.Count)); } [Fact] public void UnreadableOrForeignPlansDrawNothing() { var preview = new RemoteNestPlanPreview(Request("Rectangles")); Assert.Null(preview.Sheets("")); Assert.Null(preview.Sheets("not json")); Assert.Null(preview.Sheets("""{"version":99,"plan":{"plates":[]}}""")); var stranger = new NestJobResult(NestJobStatus.Complete, NestJobStopReason.Completed, [new NestJobPlateResult(0, Request("Rectangles").Stock[0], [new NestJobPlacement("part-7", 0, 1, 1, 0)])], [], []); Assert.Null(preview.Sheets(RemoteNestPlan.From(stranger).ToJson())); } [Fact] public void TheRecorderPassesEachSolvesProgressOn() { var request = Request("Rectangles"); var tap = new Collect(); var recorder = new RecordingNestingEngine(NestingEngineRegistry.Create(request.EngineName)) { Progress = tap }; NestPipeline.Run(recorder, request.EngineName, request); Assert.Contains(tap.Reports, r => r.Stage == NestJobStage.PlanImproved && r.Plan != null); // Before the pipeline draws it: the worker sends placements, not parts. Assert.All(tap.Reports, r => Assert.Null(r.PlanSheets)); } [Fact] public void AProgressReportSurvivesTheWire() { var report = new NestJobProgress(NestJobStage.PlateCommitted, "stock-1", 2, 3, 41); var back = RemoteNestProgress.FromJson(RemoteNestProgress.From(report).ToJson())!.ToProgress(); Assert.Equal(report, back); Assert.Null(RemoteNestProgress.FromJson("")); Assert.Null(RemoteNestProgress.FromJson("{\"version\":2}")); } }