Files
OpenNest/OpenNest.WinForms.Tests/Forms/MainFormReleaseNestingTests.cs
T
aj c5b6e680cb fix(release): keep ledger codes in the undo step and share bundles per sheet
Review fixes for ledger-first Make Nests:

- The release renest (offer, staged run, codes, note) moves into
  OpenNest.Desktop.ReleaseRenest, shared by the headless panel, the
  live main window and the tests. Codes are given inside each commit, so
  the live tab's Auto Nest undo step records them; before, undo and redo
  dropped every ledger code.
- A bundle repeats one code per sheet: a later nest of the run lost the
  whole bundle once an earlier nest took one sheet. Taken sheets are now
  counted per code (LedgerCodeFilter.ExcludeCopies).
- A stage whose layout is not kept lists every copy it asked for as not
  placed (the proposal cannot be kept), and a rejected ledger layout
  says "MaterialLedger not used: Auto Nest could not keep a layout on
  its stock" instead of a bare "No MaterialLedger stock used".
- "no free stock" is said only when the ledger had none; otherwise the
  note counts what was left out (on other nests, taken by earlier nests
  of this release, too small, unknown shape).
- The ledger is not read for a nest with nothing to nest, the live read
  runs under the tab's stop token, a reason's own full stop is not
  doubled, and the sheet count is the codes given.
2026-11-03 17:53:26 -05:00

168 lines
7.0 KiB
C#

using System.Windows.Forms;
using OpenNest.Controls;
using OpenNest.Data;
using OpenNest.Data.MaterialLedger;
using OpenNest.Engine;
using OpenNest.Engine.Jobs;
using OpenNest.Forms;
using OpenNest.Geometry;
using OpenNest.Shapes;
namespace OpenNest.WinForms.Tests.Forms;
// Make Nests nesting live in the main window. Reads Application.OpenForms through MainForm,
// so it must not overlap fill-lifetime tests.
[Collection("Fill operation lifetime")]
public class MainFormReleaseNestingTests
{
private static readonly TimeSpan TestTimeout = TimeSpan.FromMinutes(1);
[Fact]
public void EachNestNestsInItsOwnTab_ASavedOneIsBoundNotReopened_AndAnUnsavedOneCloses() => StaTestThread.Run(() =>
{
using var main = new Host();
main.SolveReleasedNest = (_, request, cts) =>
Task.Run(() => NestPipeline.Run(NestingEngineRegistry.Create("Rectangles"), "Rectangles", request, null, cts.Token));
main.Show();
IReleaseNestingHost host = main;
var saved = NestOf("N26-0001", 6);
var unsaved = NestOf("N26-0002", 2);
host.Begin();
var first = Run(host.RenestAsync(saved, null, CancellationToken.None));
var tab = Assert.Single(Tabs(main));
Assert.Same(saved, tab.Nest);
Assert.True(first.Nested, first.Note);
Assert.StartsWith("Nested on 1 sheet", first.Note);
Assert.Equal(6, saved.Plates.Sum(p => p.Parts.Count * p.Quantity));
Assert.Equal(Guid.Empty, tab.Document.RemoteId);
// The kept layout is one undoable step in the tab; undo waits for the release.
Assert.Equal(1, tab.History.UndoCount);
Assert.Equal("Auto Nest", tab.History.UndoLabel);
Assert.False(main.TryUndo(tab));
Run(host.RenestAsync(unsaved, null, CancellationToken.None));
Assert.Equal(2, Tabs(main).Count);
// Make Nests opens what it saved: the tab already showing it is bound instead.
var record = new NestRecord { Id = Guid.NewGuid(), Revision = 1, Name = saved.Name };
main.OpenServerNest(saved, record);
Assert.Equal(2, Tabs(main).Count);
Assert.Equal((record.Id, 1), (tab.Document.RemoteId, tab.Document.RemoteRevision));
host.End(null!);
Application.DoEvents();
Assert.Same(tab, Assert.Single(Tabs(main)));
}, TestTimeout, "The live release nesting test did not complete.");
[Fact]
public void StoppingTheAutoNestStopsTheRelease_AndLeavesNoLayout() => StaTestThread.Run(() =>
{
using var main = new Host();
main.SolveReleasedNest = (_, _, cts) =>
{
// The progress window's Stop.
cts.Cancel();
return Task.FromException<NestPipelineResult>(new OperationCanceledException(cts.Token));
};
main.Show();
IReleaseNestingHost host = main;
var nest = NestOf("N26-0003", 4);
host.Begin();
var renest = host.RenestAsync(nest, null, CancellationToken.None);
PumpUntil(() => renest.IsCompleted);
Assert.True(renest.IsCanceled || renest.Exception?.InnerException is OperationCanceledException);
Assert.DoesNotContain(nest.Plates, p => p.Parts.Count > 0);
host.End(null!);
Application.DoEvents();
Assert.Empty(Tabs(main));
}, TestTimeout, "The live release stop test did not complete.");
[Fact]
public void LedgerStockIsNestedFirst_InTheTab_ThenTheRestOnTheNestDefaultsSheet() =>
StaTestThread.Run(() =>
{
using var main = new Host();
main.SolveReleasedNest = (_, request, cts) => Task.Run(() => NestPipeline.Run(
NestingEngineRegistry.Create("Rectangles"), "Rectangles", request, null, cts.Token));
main.Show();
IReleaseNestingHost host = main;
// One free 10x10 ledger sheet holds one 8x8 part; the second copy tops up on 48x96.
var nest = NestOf("N26-0004", 2, partSize: 8);
nest.Material = new Material("Synthetic Steel", "SYN-1");
nest.Thickness = 0.25;
var ledger = new ReleaseLedgerStock(
(_, _, _, _) => Task.FromResult<LedgerStockResult>(new LedgerStockResult.Ok(
Array.Empty<LedgerStockSpec>(),
[new LedgerStockItem("S-1", 1, LedgerItemKinds.FullSheet, 10, 10, 1,
LedgerItemShapes.Rectangular, "B3", "Bay 3", null, null, DateTime.UnixEpoch)])),
_ => Task.FromResult(LedgerCodesInUse.Known(Array.Empty<NestStockCodeUse>())));
host.Begin();
var renest = Run(host.RenestAsync(nest, ledger, CancellationToken.None));
var tab = Assert.Single(Tabs(main));
Assert.True(renest.Nested, renest.Note);
Assert.EndsWith(" 1 sheet from MaterialLedger stock.", renest.Note);
var coded = Assert.Single(nest.Plates, p => p.StockItemCodes.Count > 0);
Assert.Equal((new Size(10, 10), "S-1"), (coded.Size, Assert.Single(coded.StockItemCodes)));
Assert.Contains(nest.Plates, p => p.Parts.Count > 0 && p.Size.Equals(new Size(48, 96))
&& p.StockItemCodes.Count == 0);
Assert.Equal(2, nest.Plates.Sum(p => p.Parts.Count * p.Quantity));
Assert.Equal("B3", tab.LedgerLocations["S-1"]);
// Each stage is its own Auto Nest step in the tab, and its codes are part of that step.
Assert.Equal(2, tab.History.UndoCount);
tab.Undo();
tab.Undo();
tab.Redo();
tab.Redo();
var redone = Assert.Single(tab.Nest.Plates, p => p.StockItemCodes.Count > 0);
Assert.Equal("S-1", Assert.Single(redone.StockItemCodes));
host.End(null!);
Application.DoEvents();
}, TestTimeout, "The live release ledger test did not complete.");
private static Nest NestOf(string name, int quantity, double partSize = 0)
{
var nest = new Nest(name) { Units = Units.Inches };
nest.PlateDefaults.Size = new Size(48, 96);
var drawing = new RectangleShape
{
Name = name + "-A",
Length = partSize > 0 ? partSize : 4,
Width = partSize > 0 ? partSize : 2,
}.GetDrawing();
drawing.Quantity.Required = quantity;
nest.Drawings.Add(drawing);
return nest;
}
private static List<EditNestForm> Tabs(MainForm main) =>
main.MdiChildren.OfType<EditNestForm>().Where(f => !f.IsDisposed).ToList();
private static T Run<T>(Task<T> task)
{
PumpUntil(() => task.IsCompleted);
return task.GetAwaiter().GetResult();
}
private static void PumpUntil(Func<bool> condition)
{
var until = DateTime.UtcNow.AddSeconds(30);
while (!condition() && DateTime.UtcNow < until)
{
Application.DoEvents();
Thread.Sleep(10);
}
Assert.True(condition(), "Timed out.");
}
private sealed class Host : MainForm
{
// No startup migrations/automatic new document, and no settings save at close.
protected override void OnLoad(EventArgs e) { }
protected override void OnClosing(System.ComponentModel.CancelEventArgs e) { }
}
}