Files
OpenNest/OpenNest.WinForms.Tests/Forms/SetReleaseFormsTests.cs
T
ajandClaude Opus 5.5 f600c5e58b feat(desktop): nest released nests live in the main window
With Nest after release ticked, Make Nests now minimizes the Drawing
Sets window and nests each group's built nest in its own main-window
tab, before it is saved, with Auto Nest's progress window and live plan
strip, one nest at a time in plan order. Stop there (or switching tabs)
stops the whole release, as the panel's Stop does; the layout is kept
by the same rules as the headless renest, without the Keep review.

After upload each tab is bound to its saved record instead of a second
tab opening; a tab whose nest was not saved is closed. The main window
stays in front, and the Drawing Sets window flashes its taskbar button
when a group failed or was refused or the release was not recorded.

NestRenest splits into CreateRequest and Keep so the solve runs off the
UI thread and the commit on it; AutoNestCommand's solve-with-progress
step is shared rather than copied.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-24 21:21:28 -04:00

1318 lines
65 KiB
C#
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
using System.Reflection;
using System.Security.Cryptography;
using System.Windows.Forms;
using OpenNest.Data;
using OpenNest.Forms;
using OpenNest.IO;
using OpenNest.Shapes;
using OpenNest.Tests.Data;
using IReleaseNestingHost = OpenNest.Controls.IReleaseNestingHost;
using ReleasePanel = OpenNest.Controls.ReleasePanel;
namespace OpenNest.WinForms.Tests.Forms;
public class SetReleaseFormsTests
{
private const string Server = "http://library.test:8090";
private static readonly TimeSpan TestTimeout = TimeSpan.FromSeconds(60);
[Fact]
public void MakeNests_CreatesOneDraftNestPerTickedGroup_OpensEach_AndRecordsTheRelease()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var b = library.Add("SYN-B", "Synthetic Steel");
var c = library.Add("SYN-C", "Synthetic Aluminum");
var old = library.Add("SYN-OLD", "Synthetic Steel", archived: true);
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 2), (b, 1), (c, 4), (old, 1));
var nests = new MemoryNests();
var opened = new List<(Nest Nest, NestRecord Record)>();
var nestsOpened = 0;
var (host, panel) = ShowPanel(sets, library, nests, (nest, record) => opened.Add((nest, record)));
using var _ = host;
panel.NestsOpened += (_, _) => nestsOpened++;
var grid = Field<DataGridView>(panel, "groupGrid");
var release = Field<Button>(panel, "releaseButton");
Assert.Equal(("Make Nests", false), (release.Text, release.Enabled));
Assert.True(panel.AddSet(set));
Assert.Equal(2, grid.Rows.Count);
Assert.Contains("'SYN-OLD'", Field<Label>(panel, "previewLabel").Text);
Assert.True(Field<CheckBox>(panel, "includeArchivedCheckBox").Visible);
Assert.Equal("Synthetic customer", Field<Label>(panel, "customerValue").Text);
var lines = Field<DataGridView>(panel, "lineGrid");
Assert.Equal(new object[] { "Synthetic kit", "1", "kit" }, lines.Rows[0].Cells.Cast<DataGridViewCell>().Take(3).Select(c => c.Value));
lines.Rows[0].Cells["lineQuantityColumn"].Value = "3";
Assert.Equal("kits", lines.Rows[0].Cells["lineUnitColumn"].Value);
Assert.Equal(new object[] { 12, 9 }, grid.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["partsColumn"].Value));
Assert.Equal("Synthetic Aluminum Grade A 0.25 in, Laser", grid.Rows[0].Cells["groupColumn"].Value);
Assert.Equal(("Make Nests (2)", true), (release.Text, release.Enabled));
Assert.All(grid.Rows.Cast<DataGridViewRow>(), r => Assert.Equal("→ new Draft nest, nested with Auto Nest.", Note(r)));
Field<TextBox>(panel, "notesBox").Text = "Synthetic run";
Run(panel.ReleaseAsync());
Assert.Equal(2, nests.Uploaded.Count);
Assert.Equal(2, opened.Count);
Assert.Equal(1, nestsOpened);
Assert.All(opened, o => Assert.Equal(NestStatus.Draft, o.Nest.Status));
var steel = opened.Single(o => o.Nest.Material.Name == "Synthetic Steel");
Assert.Equal(new[] { ("SYN-A", 6), ("SYN-B", 3) },
steel.Nest.Drawings.OrderBy(d => d.Name).Select(d => (d.Name, d.Quantity.Required)));
var recorded = Assert.Single(sets.Releases);
Assert.Equal((3, "Synthetic run", 2), (recorded.Sets.Single().Quantity, recorded.Notes, recorded.Nests.Count));
Assert.Same(recorded, panel.Result!.Release);
Assert.StartsWith("Done: 2 nests", Field<Label>(panel, "statusLabel").Text);
Assert.All(grid.Rows.Cast<DataGridViewRow>(), r => Assert.StartsWith("Created N26-", Note(r)));
Assert.False(release.Enabled);
Assert.True(lines.Columns["lineQuantityColumn"]!.ReadOnly);
Assert.Null(panel.TakeDraft());
// The next set starts the next release.
Assert.True(panel.AddSet(set));
Assert.Null(panel.Result);
Assert.Equal(("Synthetic kit", 1), panel.Lines().Select(l => (l.Set.Name, l.Quantity)).Single());
},
TestTimeout,
"The release test did not complete.");
}
[Fact]
public void UntickedAndRefusedGroups_AreNotReleased()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var twin = library.Add("SYN-A", "Synthetic Steel");
var c = library.Add("SYN-C", "Synthetic Aluminum");
var d = library.Add("SYN-D", "Synthetic Copper");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 1), (twin, 1), (c, 1), (d, 1));
var nests = new MemoryNests();
var (host, panel) = ShowPanel(sets, library, nests);
using var _ = host;
panel.AddSet(set);
var refused = GroupRow(panel, "Synthetic Steel");
Assert.Equal(false, refused.Cells["includeColumn"].Value);
Assert.True(refused.Cells["includeColumn"].ReadOnly);
Assert.Contains("named 'SYN-A'", Note(refused));
GroupRow(panel, "Synthetic Copper").Cells["includeColumn"].Value = false;
Assert.Equal("Left out of this release.", Note(GroupRow(panel, "Synthetic Copper")));
Assert.Equal("Make Nests (1)", Field<Button>(panel, "releaseButton").Text);
// Rebuilding the plan keeps the operator's choice.
Field<DataGridView>(panel, "lineGrid").Rows[0].Cells["lineQuantityColumn"].Value = "2";
Assert.Equal(false, GroupRow(panel, "Synthetic Copper").Cells["includeColumn"].Value);
Run(panel.ReleaseAsync());
Assert.Equal("Synthetic Aluminum", Assert.Single(Assert.Single(sets.Releases).Nests).MaterialName);
Assert.Single(nests.Uploaded);
},
TestTimeout,
"The release choice test did not complete.");
}
[Fact]
public void FixDrawings_EditsEachDrawingTheGroupIsRefusedFor_ThenTheGroupCanBeReleased()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var noMaterial = library.Add("SYN-B", "");
var noThickness = library.Add("SYN-C", "Synthetic Steel", thickness: 0);
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 1), (noMaterial, 2), (noThickness, 3));
var nests = new MemoryNests();
var edits = new List<string>();
var (host, panel) = ShowPanel(sets, library, nests);
using var _ = host;
panel.EditDrawing = edit =>
{
edits.Add(edit.Listed.Name);
return Run(edit.SaveAsync(edit.Details with { MaterialName = "Synthetic Steel", Thickness = 0.25 }, edit.Listed.Notes, edit.Listed.Settings ?? new DrawingNestingSettings()));
};
Field<CheckBox>(panel, "nestAfterReleaseCheckBox").Checked = false;
panel.AddSet(set);
var grid = Field<DataGridView>(panel, "groupGrid");
var fix = Field<Button>(panel, "fixDrawingsButton");
var refused = grid.Rows.Cast<DataGridViewRow>().Single(r => ((SetReleaseGroup)r.Tag!).IsUnassigned);
Assert.Contains("Edit Drawing", Note(refused));
Assert.Equal("(no material or thickness)", refused.Cells["groupColumn"].Value);
Assert.Contains("1 material can't be released yet: choose Fix Drawings.", Field<Label>(panel, "previewLabel").Text);
// With a releasable group selected, Fix Drawings takes the refused one.
grid.CurrentCell = grid.Rows[0].Cells["groupColumn"];
Assert.NotSame(refused, grid.CurrentRow);
Assert.True(fix.Visible && fix.Enabled);
Run(panel.FixSelectedGroupAsync());
Assert.Equal(new[] { "SYN-B", "SYN-C" }, edits);
Assert.False(fix.Visible);
var group = Assert.IsType<SetReleaseGroup>(Assert.Single(grid.Rows.Cast<DataGridViewRow>()).Tag);
Assert.True(group.CanRelease);
Assert.Equal(new[] { ("SYN-A", 1), ("SYN-B", 2), ("SYN-C", 2) }, group.Items.Select(i => (i.Drawing.Name, i.Drawing.Revision)));
Run(panel.ReleaseAsync());
Assert.Single(nests.Uploaded);
Assert.Equal("Synthetic Steel", Assert.Single(Assert.Single(sets.Releases).Nests).MaterialName);
},
TestTimeout,
"The fix drawings test did not complete.");
}
[Fact]
public void ReleaseNotRecorded_CanBeRecordedAgain_AndIsNotClearedByANewLine()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 1));
sets.FailNextPost = true;
var opened = 0;
var (host, panel) = ShowPanel(sets, library, new MemoryNests(), (_, _) => opened++);
using var _ = host;
panel.AddSet(set);
var release = Field<Button>(panel, "releaseButton");
Run(panel.ReleaseAsync());
Assert.Equal(1, opened);
Assert.Empty(sets.Releases);
Assert.Equal(("Try Again", true), (release.Text, release.Enabled));
Assert.EndsWith("Synthetic release post failure. Try Again records it; the nests are not made again.",
Field<Label>(panel, "statusLabel").Text);
Assert.False(panel.AddSet(set));
Assert.NotNull(panel.Result);
Run(panel.RetryRecordAsync());
Assert.Equal(2, sets.PostAttempts);
Assert.Same(Assert.Single(sets.Releases), panel.Result!.Release);
Assert.Equal(("Make Nests (1)", false), (release.Text, release.Enabled));
},
TestTimeout,
"The release retry test did not complete.");
}
[Fact]
public void OneMatchingDraft_IsMergedIntoByDefault()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var c = library.Add("SYN-C", "Synthetic Aluminum");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 1), (c, 1));
var nests = new MemoryNests();
var draft = nests.AddDraft("N26-0007", "Synthetic Steel", "Grade A", 0.25);
var (host, panel) = ShowPanel(sets, library, nests);
using var _ = host;
panel.AddSet(set);
var steel = GroupRow(panel, "Synthetic Steel");
var aluminum = GroupRow(panel, "Synthetic Aluminum");
Assert.Equal("N26-0007", steel.Cells["targetColumn"].Value);
Assert.Equal("→ merges into N26-0007; the whole draft (1 sheet) is nested again.", Note(steel));
Assert.Equal(ReleasePanel.NewNestChoice, aluminum.Cells["targetColumn"].Value);
Assert.True(aluminum.Cells["targetColumn"].ReadOnly);
Assert.Same(draft, panel.ChosenMerges().Values.Single());
Assert.StartsWith("1 new, 1 merged, 2 parts.", Field<Label>(panel, "previewLabel").Text);
Run(panel.ReleaseAsync());
var release = Assert.Single(sets.Releases);
var merged = release.Nests.Single(n => n.Kind == SetReleaseNestKind.Merged);
Assert.Equal((draft.Id, (int?)draft.Revision, draft.Revision + 1), (merged.NestId, merged.RevisionBefore, merged.RevisionAfter));
Assert.StartsWith("Merged into N26-0007", Note(steel));
},
TestTimeout,
"The one-draft merge test did not complete.");
}
[Fact]
public void SeveralMatchingDrafts_DefaultToTheNewest_AndTheChoiceIsInTheRow()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 1));
var nests = new MemoryNests();
var older = nests.AddDraft("N26-0005", "Synthetic Steel", "Grade A", 0.25, new DateTime(2026, 10, 1, 9, 0, 0, DateTimeKind.Utc));
var newer = nests.AddDraft("N26-0009", "Synthetic Steel", "Grade A", 0.25, new DateTime(2026, 10, 22, 9, 0, 0, DateTimeKind.Utc));
var (host, panel) = ShowPanel(sets, library, nests);
using var _ = host;
panel.AddSet(set);
var row = Field<DataGridView>(panel, "groupGrid").Rows[0];
var cell = (DataGridViewComboBoxCell)row.Cells["targetColumn"];
var group = (SetReleaseGroup)row.Tag!;
Assert.Equal(new object[] { "N26-0009", "N26-0005", ReleasePanel.NewNestChoice }, cell.Items.Cast<object>());
Assert.False(cell.ReadOnly);
Assert.Equal("N26-0009", cell.Value);
Assert.Same(newer, panel.TargetOf(group));
Assert.Contains("2 Draft nests match; the newest is chosen.", Note(row));
panel.ChooseTarget(row, group, "N26-0005");
Assert.Same(older, panel.TargetOf(group));
Assert.DoesNotContain("the newest is chosen", Note(row));
// The choice survives a rebuilt plan.
Field<DataGridView>(panel, "lineGrid").Rows[0].Cells["lineQuantityColumn"].Value = "2";
row = Field<DataGridView>(panel, "groupGrid").Rows[0];
Assert.Equal("N26-0005", row.Cells["targetColumn"].Value);
panel.ChooseTarget(row, (SetReleaseGroup)row.Tag!, ReleasePanel.NewNestChoice);
Assert.Empty(panel.ChosenMerges());
Assert.StartsWith("→ new Draft nest", Note(row));
Assert.Contains("2 Draft nests match; choose one to merge.", Note(row));
},
TestTimeout,
"The several-drafts merge test did not complete.");
}
[Fact]
public void NoMatchingDraft_MakesANewDraftNest()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 1));
var nests = new MemoryNests();
// Another material's draft does not match.
nests.AddDraft("N26-0007", "Synthetic Aluminum", "Grade A", 0.25);
var (host, panel) = ShowPanel(sets, library, nests);
using var _ = host;
panel.AddSet(set);
var row = Field<DataGridView>(panel, "groupGrid").Rows[0];
Assert.Equal(ReleasePanel.NewNestChoice, row.Cells["targetColumn"].Value);
Assert.Equal("→ new Draft nest, nested with Auto Nest.", Note(row));
Run(panel.ReleaseAsync());
Assert.Equal(SetReleaseNestKind.Created, Assert.Single(Assert.Single(sets.Releases).Nests).Kind);
},
TestTimeout,
"The no-draft release test did not complete.");
}
[Fact]
public void DraftOpenInATab_IsNeverMergedInto_AndMakeNestsWaitsUntilItIsClosed()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 1));
var nests = new MemoryNests();
var draft = nests.AddDraft("N26-0007", "Synthetic Steel", "Grade A", 0.25);
var openTabs = new HashSet<Guid> { draft.Id };
var messages = new List<string>();
var (host, panel) = ShowPanel(sets, library, nests, messages: messages);
using var _ = host;
panel.IsNestOpen = openTabs.Contains;
panel.AddSet(set);
var row = Field<DataGridView>(panel, "groupGrid").Rows[0];
var release = Field<Button>(panel, "releaseButton");
Assert.Equal("N26-0007", row.Cells["targetColumn"].Value);
Assert.Equal("N26-0007 is open — save and close it to merge into it.", Note(row));
Assert.False(release.Enabled);
Assert.Contains("Make Nests waits: N26-0007 is open in a tab.", Field<Label>(panel, "previewLabel").Text);
Run(panel.ReleaseAsync());
Assert.Empty(nests.Uploaded);
Assert.Empty(sets.Releases);
Assert.Contains("is open in a tab", Assert.Single(messages));
// Closed in its tab, as the window learns when it is activated again.
openTabs.Clear();
panel.RefreshOpenNests();
Assert.True(release.Enabled);
Assert.StartsWith("→ merges into N26-0007", Note(row));
Run(panel.ReleaseAsync());
Assert.Equal(SetReleaseNestKind.Merged, Assert.Single(Assert.Single(sets.Releases).Nests).Kind);
},
TestTimeout,
"The open-draft release test did not complete.");
}
[Fact]
public void NestAfterRelease_NestsEachGroupWithAutoNest_AndListsItsSheets()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 3));
var opened = new List<Nest>();
var (host, panel) = ShowPanel(sets, library, new MemoryNests(), (nest, _) => opened.Add(nest));
using var _ = host;
panel.CreateNest = (group, _) =>
{
var nest = new Nest("N26-0200") { Units = group.Units };
nest.PlateDefaults.Size = new OpenNest.Geometry.Size(48, 96);
return Task.FromResult(nest);
};
Field<CheckBox>(panel, "nestAfterReleaseCheckBox").Checked = true;
panel.AddSet(set);
Run(panel.ReleaseAsync());
var nest = Assert.Single(opened);
Assert.Equal(3, nest.Plates.Sum(p => p.Parts.Count * p.Quantity));
Assert.Equal(0, Assert.Single(nest.Drawings).Quantity.Remaining);
Assert.StartsWith("Created N26-0200 — Nested on 1 sheet, ", Note(Field<DataGridView>(panel, "groupGrid").Rows[0]));
},
TestTimeout,
"The nest after release test did not complete.");
}
[Fact]
public void NestAfterRelease_Unticked_SaysAMergedDraftIsLeftWithoutSheets()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 2));
var nests = new MemoryNests();
var draft = nests.AddDraft("N26-0007", "Synthetic Steel", "Grade A", 0.25);
var renested = 0;
var (host, panel) = ShowPanel(sets, library, nests);
using var _ = host;
panel.Renest = (_, _) => { renested++; return Task.FromResult(new SetReleaseRenest(true, "")); };
var nestAfter = Field<CheckBox>(panel, "nestAfterReleaseCheckBox");
nestAfter.Checked = true;
panel.AddSet(set);
var row = Field<DataGridView>(panel, "groupGrid").Rows[0];
Assert.Contains("the whole draft (1 sheet) is nested again", Note(row));
nestAfter.Checked = false;
Assert.Equal("→ merges into N26-0007; its 1 sheet is cleared, leaving the draft without sheets until it is nested.", Note(row));
Run(panel.ReleaseAsync());
Assert.Equal(0, renested);
var merged = Assert.Single(panel.Result!.Created);
Assert.Same(draft, merged.MergedInto);
Assert.Empty(merged.Nest!.Plates);
Assert.Null(merged.Renest);
Assert.False(nestAfter.Enabled);
},
TestTimeout,
"The release without nesting test did not complete.");
}
[Fact]
public void NestAfterRelease_RemembersTheOperatorsLastChoice()
{
StaTestThread.Run(
() =>
{
var settings = OpenNest.Properties.Settings.Default;
var saved = settings.ReleaseNestAfter;
try
{
settings.ReleaseNestAfter = true;
using (var first = new ReleasePanel())
{
var box = Field<CheckBox>(first, "nestAfterReleaseCheckBox");
Assert.True(box.Checked);
// Set by code, it is not remembered; clicked by the operator, it is.
box.Checked = false;
Assert.True(settings.ReleaseNestAfter);
box.Checked = true;
typeof(CheckBox).GetMethod("OnClick", BindingFlags.Instance | BindingFlags.NonPublic)!
.Invoke(box, new object[] { EventArgs.Empty });
Assert.False(box.Checked);
Assert.False(settings.ReleaseNestAfter);
}
using var second = new ReleasePanel();
Assert.False(Field<CheckBox>(second, "nestAfterReleaseCheckBox").Checked);
}
finally
{
settings.ReleaseNestAfter = saved;
settings.Save();
}
},
TestTimeout,
"The remembered nest-after test did not complete.");
}
[Fact]
public void NestAfterRelease_WithANestingHost_NestsEachNestThereInPlanOrder_AndTheWindowStepsAside()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var c = library.Add("SYN-C", "Synthetic Aluminum");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 1), (c, 2));
var nests = new MemoryNests();
var host = new FakeNestingHost();
var opened = new List<Nest>();
using var form = new DrawingSetsForm(sets, library, Server);
form.EnableRelease(nests, (nest, _) => opened.Add(nest), null, host);
var panel = form.ReleasePanel;
panel.CreateNest = (group, _) => Task.FromResult(new Nest("N") { Units = group.Units });
var finished = 0;
panel.MakeNestsFinished += (_, _) => finished++;
form.Show();
Field<CheckBox>(panel, "nestAfterReleaseCheckBox").Checked = true;
panel.AddSet(set);
Run(panel.ReleaseAsync());
Assert.Equal(new[] { "Begin", "Renest Synthetic Aluminum", "Renest Synthetic Steel", "End" }, host.Calls);
Assert.Equal(FormWindowState.Minimized, form.WindowState);
Assert.Same(panel.Result, host.Ended);
// The host's nests are the ones saved and opened (bound) afterwards.
Assert.Equal(host.Nests, opened);
Assert.Equal(2, nests.Uploaded.Count);
Assert.All(Field<DataGridView>(panel, "groupGrid").Rows.Cast<DataGridViewRow>(),
r => Assert.EndsWith("Nested live.", Note(r)));
Assert.False(panel.NeedsAttention);
Assert.Equal(1, finished);
},
TestTimeout,
"The live nesting test did not complete.");
}
[Fact]
public void NestingHostStop_StopsTheRelease_SavesNothing_AndEndsTheHost()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var c = library.Add("SYN-C", "Synthetic Aluminum");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 1), (c, 2));
var nests = new MemoryNests();
var host = new FakeNestingHost { StopAt = 2 };
var (window, panel) = ShowPanel(sets, library, nests);
using var _ = window;
panel.NestingHost = host;
panel.AddSet(set);
Run(panel.ReleaseAsync());
Assert.Equal(new[] { "Begin", "Renest Synthetic Aluminum", "Renest Synthetic Steel", "End" }, host.Calls);
Assert.True(panel.Result!.Canceled);
Assert.Empty(nests.Uploaded);
Assert.Empty(sets.Releases);
Assert.Same(panel.Result, host.Ended);
Assert.Empty(panel.Result.Created);
Assert.True(panel.NeedsAttention);
Assert.StartsWith("Canceled: 0 nests saved", Field<Label>(panel, "statusLabel").Text);
},
TestTimeout,
"The live nesting stop test did not complete.");
}
[Fact]
public void NestAfterRelease_Unticked_DoesNotUseTheNestingHost()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 1));
var host = new FakeNestingHost();
var (window, panel) = ShowPanel(sets, library, new MemoryNests());
using var _ = window;
panel.NestingHost = host;
Field<CheckBox>(panel, "nestAfterReleaseCheckBox").Checked = false;
panel.AddSet(set);
Run(panel.ReleaseAsync());
Assert.Empty(host.Calls);
Assert.Single(sets.Releases);
},
TestTimeout,
"The unticked live nesting test did not complete.");
}
[Fact]
public void SetsTab_DoubleClickAddsASetAtOneKit_AgainAddsAKit_AndAddToReleaseTakesEverySelectedSet()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
CreateSet(sets, "Synthetic kit", (a, 1));
CreateSet(sets, "Synthetic frame", (a, 2));
var edits = 0;
using var form = new DrawingSetsForm(sets, library, Server);
form.EnableRelease(new MemoryNests(), (_, _) => { });
var browser = form.Browser;
browser.EditSet = _ => { edits++; return null; };
form.Show();
var setGrid = Field<DataGridView>(browser, "setGrid");
PumpUntil(() => setGrid.Rows.Count == 2, "the set list");
var panel = form.ReleasePanel;
Assert.True(panel.Visible);
Assert.Equal("&Add to Release", Field<Button>(browser, "releaseButton").Text);
// Listed by name: frame, then kit.
setGrid.CurrentCell = setGrid.Rows[1].Cells[0];
DoubleClick(setGrid, 1);
PumpUntil(() => panel.Lines().Count == 1, "the set to be added");
Assert.Equal(("Synthetic kit", 1), (panel.Lines()[0].Set.Name, panel.Lines()[0].Quantity));
Assert.Equal("Synthetic Steel", Assert.Single(panel.Lines()[0].Set.Members).Drawing!.MaterialName);
DoubleClick(setGrid, 1);
PumpUntil(() => panel.Lines()[0].Quantity == 2, "the kit to be added");
Assert.Single(panel.Lines());
Assert.Equal(0, edits);
setGrid.Rows[0].Selected = true;
setGrid.Rows[1].Selected = true;
Run(browser.AddSelectedToReleaseAsync());
Assert.Equal(new[] { ("Synthetic kit", 3), ("Synthetic frame", 1) }, panel.Lines().Select(l => (l.Set.Name, l.Quantity)));
// Edit moved to its button and the right-click menu.
Assert.True(Field<ToolStripMenuItem>(browser, "editMenuItem").Available);
Run(browser.EditSelectedAsync());
Assert.Equal(1, edits);
// The panel shows beside the lists, never over the releases.
Field<TabControl>(form, "tabs").SelectedIndex = 2;
Assert.True(Field<SplitContainer>(form, "splitContainer").Panel2Collapsed);
form.ShowLibrary();
Assert.False(Field<SplitContainer>(form, "splitContainer").Panel2Collapsed);
},
TestTimeout,
"The sets tab add test did not complete.");
}
[Fact]
public void LibraryTab_DoubleClickAddsADrawingOnItsOwnAtTheQuantity_AgainAddsThatMore_IntoTheSameRelease()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, "Synthetic kit", (a, 1));
var edits = 0;
using var form = new DrawingSetsForm(sets, library, Server);
form.EnableRelease(new MemoryNests(), (_, _) => { });
var view = form.LibraryView;
view.EditDrawing = _ => { edits++; return null; };
view.LoadPreview = (_, _) => Task.FromResult<Drawing>(null!);
form.ShowLibrary();
form.Show();
var grid = Field<DataGridView>(view, "drawingGrid");
PumpUntil(() => grid.Rows.Count == 1, "the library page");
grid.CurrentCell = grid.Rows[0].Cells[0];
var panel = form.ReleasePanel;
Assert.True(panel.AddSet(set));
Field<NumericUpDown>(view, "quantityBox").Value = 4;
DoubleClick(grid, 0);
PumpUntil(() => panel.DrawingLines().Count == 1, "the drawing to be added");
Assert.Equal(("SYN-A", 4), (panel.DrawingLines()[0].Drawing.Name, panel.DrawingLines()[0].Quantity));
DoubleClick(grid, 0);
PumpUntil(() => panel.DrawingLines()[0].Quantity == 8, "the quantity to be added");
Assert.Equal(0, edits);
var lines = Field<DataGridView>(panel, "lineGrid");
Assert.Equal(new object[] { "Synthetic kit", "SYN-A" }, lines.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["lineNameColumn"].Value));
Assert.Equal("pcs", lines.Rows[1].Cells["lineUnitColumn"].Value);
// The set and the drawing on its own share one group.
Assert.Equal(9, Field<DataGridView>(panel, "groupGrid").Rows[0].Cells["partsColumn"].Value);
var addButton = Field<Button>(view, "addButton");
Assert.Equal(("Add to Release", true, true), (addButton.Text, addButton.Visible, addButton.Enabled));
Assert.Equal("Edit Drawing...", Field<Button>(view, "editButton").Text);
Run(view.EditSelectedAsync());
Assert.Equal(1, edits);
// ✕ removes a line.
typeof(DataGridView).GetMethod("OnCellContentClick", BindingFlags.Instance | BindingFlags.NonPublic)!
.Invoke(lines, new object[] { new DataGridViewCellEventArgs(lines.Columns["lineRemoveColumn"]!.Index, 0) });
Assert.Empty(panel.Lines());
Assert.Single(panel.DrawingLines());
// Closing keeps the release for the next time the window opens.
var draft = form.TakeRelease();
Assert.NotNull(draft);
using var again = new DrawingSetsForm(sets, library, Server);
again.EnableRelease(new MemoryNests(), (_, _) => { });
again.RestoreRelease(draft);
Assert.Equal(("SYN-A", 8), again.ReleasePanel.DrawingLines().Select(l => (l.Drawing.Name, l.Quantity)).Single());
},
TestTimeout,
"The library tab add test did not complete.");
}
[Fact]
public void ReleaseDialog_OfASetAndDrawingsAddedFromTheLibrary_GroupsThemTogether_AndRecordsBoth()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
library.Add("SYN-B", "Synthetic Steel");
library.Add("SYN-C", "Synthetic Aluminum");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 2));
var nests = new MemoryNests();
using var form = new ReleaseSetForm(sets, library, nests, Server, new[] { set }, Array.Empty<SetReleaseDrawingLine>(), (_, _) => { });
var panel = form.Panel;
panel.ShowMessage = (_, _, _) => DialogResult.OK;
var created = 0;
panel.CreateNest = (group, _) => Task.FromResult(new Nest($"N26-{++created:D4}") { Units = group.Units });
form.Show();
using var picker = new AddReleaseDrawingsForm(library, Server, panel.AddDrawings,
id => panel.DrawingLines().Any(l => l.Drawing.Id == id));
picker.Show(form);
Run(picker.RefreshAsync());
picker.Select("SYN-A", "SYN-C");
picker.Quantity = 5;
picker.AddSelectedDrawings();
Assert.Equal("Added 2 drawings.", picker.Status);
picker.Select("SYN-C", "SYN-B");
picker.Quantity = 1;
picker.AddSelectedDrawings();
Assert.Equal("Added 1 drawing and 1 more of 1 listed.", picker.Status);
var lines = Field<DataGridView>(panel, "lineGrid");
Assert.Equal(new object[] { "kit", "pcs", "pcs", "pc" },
lines.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["lineUnitColumn"].Value));
lines.Rows[2].Cells["lineQuantityColumn"].Value = "40";
Assert.Equal(new[] { ("SYN-A", 5), ("SYN-C", 40), ("SYN-B", 1) }, panel.DrawingLines().Select(l => (l.Drawing.Name, l.Quantity)));
var grid = Field<DataGridView>(panel, "groupGrid");
Assert.Equal(new object[] { 40, 8 }, grid.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["partsColumn"].Value));
Run(panel.ReleaseAsync());
var release = Assert.Single(sets.Releases);
Assert.Equal(new[] { 5, 40, 1 }, release.Drawings.Select(d => d.Quantity));
var steel = release.Nests.Single(n => n.MaterialName == "Synthetic Steel");
Assert.Equal(new[] { ((Guid?)set.Id, 2), (null, 5), (null, 1) }, steel.Items.Select(i => (i.SetId, i.Quantity)));
Assert.Equal(2, nests.Uploaded.Count);
},
TestTimeout,
"The drawing release test did not complete.");
}
[Fact]
public void Release_OfADrawingAlone_OnAServerWithoutDrawingsOnTheirOwn_SavesNothing()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets { WithoutDrawingReleases = true };
var nests = new MemoryNests();
var (host, panel) = ShowPanel(sets, library, nests);
using var _ = host;
Assert.Equal("Added 'SYN-A' ×4.", panel.AddDrawings(new[] { a }, 4));
Assert.Equal("Release drawing 'SYN-A' ×4", Field<Label>(panel, "headingLabel").Text);
Assert.Contains("4 parts", Field<Label>(panel, "previewLabel").Text);
Run(panel.ReleaseAsync());
Assert.Empty(nests.Uploaded);
Assert.Empty(sets.Releases);
Assert.Equal("Nothing saved. See What happens.", Field<Label>(panel, "statusLabel").Text);
Assert.Contains("does not support releasing library drawings on their own",
Note(Field<DataGridView>(panel, "groupGrid").Rows[0]));
},
TestTimeout,
"The old-server drawing release test did not complete.");
}
[Fact]
public void NewRelease_SwitchesToTheSetsTab_AndTheReleasesTabRereadsWhenShownAgain()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets, (a, 1));
var other = CreateSet(sets, "Synthetic frame", (a, 2));
SetReleaseRecord Release(DateTime at, SetReleaseLine[] lines, SetReleaseDrawing[] drawings) => new()
{
Id = Guid.NewGuid(),
Sets = lines,
Drawings = drawings,
ReleasedBy = "Synthetic operator",
ReleasedAt = at,
Nests = new[] { new SetReleaseNest { NestId = Guid.NewGuid(), Nest = new NestRecord { Name = "N1", Status = NestStatus.Draft } } },
};
sets.Releases.Add(Release(new DateTime(2026, 10, 20, 12, 0, 0, DateTimeKind.Utc),
new[]
{
new SetReleaseLine { SetId = set.Id, SetRevision = 1, SetName = set.Name, Quantity = 2 },
new SetReleaseLine { SetId = other.Id, SetRevision = 1, SetName = other.Name, Quantity = 1 },
},
new[] { new SetReleaseDrawing { DrawingId = a.Id, DrawingName = a.Name, Quantity = 4 } }));
sets.Releases.Add(Release(new DateTime(2026, 10, 21, 12, 0, 0, DateTimeKind.Utc),
Array.Empty<SetReleaseLine>(),
new[] { new SetReleaseDrawing { DrawingId = a.Id, DrawingName = a.Name, Quantity = 1 } }));
using var form = new DrawingSetsForm(sets, library, Server);
var view = form.Releases;
Run(form.ShowReleasesAsync());
form.Show();
var releases = Field<DataGridView>(view, "releaseGrid");
PumpUntil(() => releases.Rows.Count == 2, "the releases");
Assert.False(Field<TableLayoutPanel>(view, "filterTable").Visible);
Assert.Equal("Released", releases.Columns["multiplierColumn"]!.HeaderText);
Assert.Equal(new object[] { "SYN-A ×1", "Synthetic kit ×2, Synthetic frame; SYN-A ×4" },
releases.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["multiplierColumn"].Value));
var newRelease = Field<Button>(view, "newReleaseButton");
Assert.False(newRelease.Visible);
form.EnableRelease(new MemoryNests(), (_, _) => { });
Assert.True(newRelease.Visible);
newRelease.PerformClick();
Assert.False(form.ShowsReleases);
Assert.True(form.ReleasePanel.Visible);
// A release made meanwhile shows once the tab is shown again.
sets.Releases.Add(Release(DateTime.UtcNow, new[] { new SetReleaseLine { SetId = set.Id, SetRevision = 1, SetName = set.Name } },
Array.Empty<SetReleaseDrawing>()));
Run(form.ShowReleasesAsync());
PumpUntil(() => releases.Rows.Count == 3, "the re-read releases");
Assert.Equal("Synthetic kit", releases.Rows[0].Cells["multiplierColumn"].Value);
},
TestTimeout,
"The new release test did not complete.");
}
[Fact]
public void Releases_ListsEachReleaseWithProgress_AndItsNestsStates()
{
StaTestThread.Run(
() =>
{
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets);
SetReleaseNest Nest(string name, NestStatus status, bool deleted = false) => new()
{
NestId = Guid.NewGuid(),
MaterialName = "Synthetic Steel",
Thickness = 0.25,
Nest = new NestRecord { Name = name, Status = status, Deleted = deleted },
};
sets.Releases.Add(new SetReleaseRecord
{
Id = Guid.NewGuid(),
Sets = new[] { new SetReleaseLine { SetId = set.Id, SetRevision = 1, SetName = set.Name, Quantity = 2 } },
ReleasedBy = "Synthetic operator",
ReleasedAt = new DateTime(2026, 10, 20, 12, 0, 0, DateTimeKind.Utc),
Nests = new[] { Nest("N1", NestStatus.HasBeenCut), Nest("N2", NestStatus.ToBeCut), Nest("N3", NestStatus.Draft, deleted: true) },
});
using var form = new DrawingSetsForm(sets, new PackageLibrary(), Server);
Run(form.ShowReleasesAsync(set));
form.Show();
var view = form.Releases;
var releases = Field<DataGridView>(view, "releaseGrid");
var nests = Field<DataGridView>(view, "nestGrid");
PumpUntil(() => releases.Rows.Count == 1 && nests.Rows.Count == 3, "the releases");
Assert.True(form.ShowsReleases);
Assert.True(Field<TableLayoutPanel>(view, "filterTable").Visible);
Assert.Equal("Releases of set 'Synthetic kit'", Field<Label>(view, "filterLabel").Text);
Assert.True(releases.Columns["revisionColumn"]!.Visible);
Assert.Equal("1 of 2 cut, 1 deleted", releases.Rows[0].Cells["progressColumn"].Value);
Assert.Equal("×2", releases.Rows[0].Cells["multiplierColumn"].Value);
Assert.Equal(new object[] { "Has Been Cut", "To Be Cut", "Deleted" },
nests.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["statusColumn"].Value));
},
TestTimeout,
"The releases view test did not complete.");
}
[Fact]
public void Releases_UndoOffersToLeaveChangedNests_AndShowsWhatTheReleaseDid()
{
StaTestThread.Run(
() =>
{
var sets = new InMemoryDrawingSets();
var set = CreateSet(sets);
var created = new SetReleaseNest { NestId = Guid.NewGuid(), MaterialName = "Synthetic Steel", Thickness = 0.25, RevisionAfter = 1, Nest = new NestRecord { Name = "N1", Status = NestStatus.Draft } };
var merged = new SetReleaseNest
{
NestId = Guid.NewGuid(),
Kind = SetReleaseNestKind.Merged,
RevisionBefore = 3,
RevisionAfter = 4,
MaterialName = "Synthetic Aluminum",
Thickness = 0.25,
Nest = new NestRecord { Name = "N2", Status = NestStatus.Draft },
};
var release = new SetReleaseRecord
{
Id = Guid.NewGuid(),
Sets = new[]
{
new SetReleaseLine { SetId = set.Id, SetRevision = 1, SetName = set.Name, Quantity = 2 },
new SetReleaseLine { SetId = Guid.NewGuid(), SetRevision = 1, SetName = "Other kit", Quantity = 1 },
},
Nests = new[] { created, merged },
};
sets.Releases.Add(release);
sets.UndoHandler = (_, request) =>
{
if (request.SkipNests.Count == 0)
throw new SetReleaseUndoConflictException(new SetReleaseUndoConflict
{
Reason = SetReleaseUndoRefusal.NestsChanged,
Release = release,
ChangedNests = new[] { new NestRecord { Id = merged.NestId, Name = "N2", Revision = 6, Status = NestStatus.ToBeCut } },
});
release.UndoneAt = new DateTime(2026, 10, 21, 12, 0, 0, DateTimeKind.Utc);
created.Undo = SetReleaseNestUndo.Deleted;
merged.Undo = SetReleaseNestUndo.Skipped;
return release;
};
var questions = new List<string>();
using var form = new DrawingSetsForm(sets, new PackageLibrary(), Server);
var view = form.Releases;
view.ShowMessage = (text, buttons, _) =>
{
questions.Add(text);
return buttons == MessageBoxButtons.YesNo ? DialogResult.Yes : DialogResult.OK;
};
Run(form.ShowReleasesAsync(set));
form.Show();
var releases = Field<DataGridView>(view, "releaseGrid");
var nests = Field<DataGridView>(view, "nestGrid");
PumpUntil(() => releases.Rows.Count == 1 && nests.Rows.Count == 2, "the releases");
Assert.Equal("×2 (+1 other set)", releases.Rows[0].Cells["multiplierColumn"].Value);
Assert.Equal(new object[] { "Created", "Merged (revision 3 → 4)" }, nests.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["changeColumn"].Value));
Assert.True(Field<Button>(view, "undoButton").Enabled);
Run(view.UndoSelectedAsync());
Assert.Contains("1 nest it created is deleted", questions[0]);
Assert.Contains("1 draft it merged into goes back", questions[0]);
Assert.Contains("- N2: saved since the release (now revision 6, To Be Cut)", questions[1]);
Assert.Equal(new[] { 0, 1 }, sets.UndoRequests.Select(r => r.Request.SkipNests.Count));
Assert.Equal(merged.NestId, sets.UndoRequests[1].Request.SkipNests.Single());
PumpUntil(() => (string)releases.Rows[0].Cells["progressColumn"].Value == "Undone", "the release to read as undone");
Assert.Equal(new object[] { "Created; undo deleted it", "Merged (revision 3 → 4); undo left it as it was" },
nests.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["changeColumn"].Value));
Assert.False(Field<Button>(view, "undoButton").Enabled);
Assert.Same(set, view.FilterSet);
},
TestTimeout,
"The release undo test did not complete.");
}
[Fact]
public void Releases_OfADrawing_ListsReleasesThatHadItOnItsOwn_AndUndoesOne()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var release = new SetReleaseRecord
{
Id = Guid.NewGuid(),
Sets = new[] { new SetReleaseLine { SetId = Guid.NewGuid(), SetRevision = 1, SetName = "Synthetic kit", Quantity = 1 } },
Drawings = new[] { new SetReleaseDrawing { DrawingId = a.Id, DrawingName = a.Name, Quantity = 4 } },
ReleasedBy = "Synthetic operator",
Nests = new[] { new SetReleaseNest { NestId = Guid.NewGuid(), Nest = new NestRecord { Name = "N1", Status = NestStatus.Draft } } },
};
sets.Releases.Add(release);
sets.UndoHandler = (id, request) => new SetReleaseRecord { Id = id, UndoneAt = DateTime.UtcNow };
var questions = new List<string>();
using var form = new DrawingSetsForm(sets, library, Server);
var view = form.Releases;
view.ShowMessage = (text, _, _) => { questions.Add(text); return DialogResult.OK; };
Run(form.ShowReleasesAsync(drawing: a));
form.Show();
var releases = Field<DataGridView>(view, "releaseGrid");
PumpUntil(() => releases.Rows.Count == 1, "the releases");
Assert.Equal("Releases of drawing 'SYN-A' on its own", Field<Label>(view, "filterLabel").Text);
Assert.Equal("Quantity", releases.Columns["multiplierColumn"]!.HeaderText);
Assert.Equal("×4 (+1 set)", releases.Rows[0].Cells["multiplierColumn"].Value);
Assert.False(releases.Columns["revisionColumn"]!.Visible);
Run(view.UndoSelectedAsync());
Assert.Equal(release.Id, Assert.Single(sets.UndoRequests).Id);
Assert.Contains("deleted (they can be recovered)", Assert.Single(questions));
},
TestTimeout,
"The drawing releases view test did not complete.");
}
[Fact]
public void DrawingSetsWindow_ReadsEachTabWhenShown_AndASetsReleasesFilterTheReleasesTab_UntilCleared()
{
StaTestThread.Run(
() =>
{
var library = new PackageLibrary();
var a = library.Add("SYN-A", "Synthetic Steel");
var sets = new InMemoryDrawingSets();
var kit = CreateSet(sets, "Synthetic kit", (a, 1));
var frame = CreateSet(sets, "Synthetic frame", (a, 2));
SetReleaseRecord Release(DrawingSetRecord set) => new()
{
Id = Guid.NewGuid(),
Sets = new[] { new SetReleaseLine { SetId = set.Id, SetRevision = 1, SetName = set.Name, Quantity = 1 } },
ReleasedBy = "Synthetic operator",
Nests = new[] { new SetReleaseNest { NestId = Guid.NewGuid(), Nest = new NestRecord { Name = "N1", Status = NestStatus.Draft } } },
};
sets.Releases.Add(Release(kit));
sets.Releases.Add(Release(frame));
using var form = new DrawingSetsForm(sets, library, Server);
var browser = form.Browser;
var view = form.Releases;
form.Show();
var setGrid = Field<DataGridView>(browser, "setGrid");
var releases = Field<DataGridView>(view, "releaseGrid");
var filter = Field<TableLayoutPanel>(view, "filterTable");
PumpUntil(() => setGrid.Rows.Count == 2, "the set list");
Assert.False(form.ShowsReleases);
Assert.False(view.HasLoaded);
setGrid.CurrentCell = setGrid.Rows.Cast<DataGridViewRow>()
.Single(r => (string)r.Cells["nameColumn"].Value == "Synthetic kit").Cells[0];
Assert.True(Field<Button>(browser, "moreButton").Enabled);
Field<ToolStripMenuItem>(browser, "releasesMenuItem").PerformClick();
PumpUntil(() => releases.Rows.Count == 1, "the set's releases");
Assert.True(form.ShowsReleases);
Assert.Equal(kit.Id, view.FilterSet.Id);
Assert.True(filter.Visible);
Assert.Equal("Releases of set 'Synthetic kit'", Field<Label>(view, "filterLabel").Text);
Assert.Equal("Quantity", releases.Columns["multiplierColumn"]!.HeaderText);
Field<Button>(view, "clearFilterButton").PerformClick();
PumpUntil(() => releases.Rows.Count == 2, "every release");
Assert.Null(view.FilterSet);
Assert.False(filter.Visible);
Assert.Equal("Released", releases.Columns["multiplierColumn"]!.HeaderText);
Assert.False(releases.Columns["revisionColumn"]!.Visible);
// A release made meanwhile (as Make Nests on the Sets tab does) shows once the tab is shown again.
form.ShowSets();
Assert.False(form.ShowsReleases);
sets.Releases.Add(Release(frame));
Field<TabControl>(form, "tabs").SelectedIndex = 2;
PumpUntil(() => releases.Rows.Count == 3, "the re-read releases");
Assert.Equal("Releases 1-3 of 3", Field<Label>(view, "summaryLabel").Text);
},
TestTimeout,
"The drawing sets window tabs test did not complete.");
}
private static DrawingSetRecord CreateSet(InMemoryDrawingSets sets, params (DrawingLibraryRecord Drawing, int Quantity)[] members) =>
CreateSet(sets, "Synthetic kit", members);
private static DrawingSetRecord CreateSet(InMemoryDrawingSets sets, string name, params (DrawingLibraryRecord Drawing, int Quantity)[] members)
{
var task = sets.CreateAsync(new DrawingSetRecord
{
Id = Guid.NewGuid(),
Name = name,
Customer = "Synthetic customer",
Members = members.Select(m => new DrawingSetMember { DrawingId = m.Drawing.Id, Quantity = m.Quantity, Drawing = m.Drawing }).ToList(),
});
return task.GetAwaiter().GetResult();
}
/// <summary>A release panel in a shown window, initialized, with test hooks for messages and new nests.</summary>
private static (Form Host, ReleasePanel Panel) ShowPanel(InMemoryDrawingSets sets, PackageLibrary library, MemoryNests nests,
Action<Nest, NestRecord>? openNest = null, List<string>? messages = null)
{
var host = new Form { Width = 900, Height = 700 };
var panel = new ReleasePanel { Dock = DockStyle.Fill };
host.Controls.Add(panel);
panel.Initialize(sets, library, nests, Server, openNest ?? ((_, _) => { }));
panel.ShowMessage = (text, _, _) => { messages?.Add(text); return DialogResult.OK; };
var created = 0;
panel.CreateNest = (group, _) => Task.FromResult(new Nest($"N26-{++created:D4}") { Units = group.Units });
// As by default, whatever this PC last chose.
Field<CheckBox>(panel, "nestAfterReleaseCheckBox").Checked = true;
host.Show();
return (host, panel);
}
private static DataGridViewRow GroupRow(ReleasePanel panel, string material) =>
Field<DataGridView>(panel, "groupGrid").Rows.Cast<DataGridViewRow>()
.Single(r => ((SetReleaseGroup)r.Tag!).MaterialName == material);
private static string Note(DataGridViewRow row) => (string)row.Cells["statusColumn"].Value;
private static void DoubleClick(DataGridView grid, int row) =>
typeof(DataGridView).GetMethod("OnCellDoubleClick", BindingFlags.Instance | BindingFlags.NonPublic)!
.Invoke(grid, new object[] { new DataGridViewCellEventArgs(0, row) });
private static void Run(Task task) => PumpUntil(() => task.IsCompleted, "the operation to finish");
private static T Run<T>(Task<T> task)
{
Run((Task)task);
return task.GetAwaiter().GetResult();
}
private static void PumpUntil(Func<bool> condition, string what)
{
var until = DateTime.UtcNow.AddSeconds(15);
while (!condition() && DateTime.UtcNow < until)
{
Application.DoEvents();
Thread.Sleep(10);
}
Assert.True(condition(), $"Timed out waiting for {what}.");
}
private static T Field<T>(object owner, string name) =>
(T)owner.GetType().GetField(name, BindingFlags.Instance | BindingFlags.NonPublic)!.GetValue(owner)!;
/// <summary>Records what Make Nests asks of the main window, and can stop at a given nest.</summary>
private sealed class FakeNestingHost : IReleaseNestingHost
{
public List<string> Calls { get; } = new();
public List<Nest> Nests { get; } = new();
public SetReleaseResult? Ended { get; private set; }
public int StopAt { get; init; }
public void Begin() => Calls.Add("Begin");
public Task<SetReleaseRenest> RenestAsync(Nest nest, CancellationToken token)
{
Nests.Add(nest);
Calls.Add($"Renest {nest.Material.Name}");
if (Nests.Count == StopAt)
throw new OperationCanceledException();
return Task.FromResult(new SetReleaseRenest(true, "Nested live."));
}
public void End(SetReleaseResult result)
{
Calls.Add("End");
Ended = result;
}
}
/// <summary>Real drawing packages by revision, so the release reads them as the desktop does.</summary>
private sealed class PackageLibrary : IDrawingLibraryStore
{
private readonly List<DrawingLibraryRecord> _records = new();
private readonly Dictionary<(Guid, int), byte[]> _packages = new();
public DrawingLibraryRecord Add(string name, string material, bool archived = false, double thickness = 0.25)
{
var package = DrawingPackage.Write(new RectangleShape { Name = name, Length = 4, Width = 2 }.GetDrawing(), Units.Inches);
var record = new DrawingLibraryRecord
{
Id = Guid.NewGuid(),
Revision = 1,
Name = name,
MaterialName = material,
MaterialGrade = "Grade A",
Thickness = thickness,
Units = Units.Inches,
CutType = CutType.Laser,
Archived = archived,
PackageSha256 = Convert.ToHexString(SHA256.HashData(package)).ToLowerInvariant(),
};
_records.Add(record);
_packages[(record.Id, record.Revision)] = package;
return record;
}
public Task<byte[]?> GetRevisionPackageAsync(Guid id, int revision, CancellationToken cancellationToken = default) =>
Task.FromResult(_packages.TryGetValue((id, revision), out var package) ? package : null);
public Task<DrawingLibraryPage> QueryAsync(DrawingLibraryQuery query, CancellationToken cancellationToken = default) =>
Task.FromResult(new DrawingLibraryPage { Items = _records.ToArray(), Total = _records.Count, Offset = query.Offset, Limit = query.Limit });
public Task<DrawingLibraryRecord?> GetAsync(Guid id, CancellationToken cancellationToken = default) =>
Task.FromResult(_records.FirstOrDefault(r => r.Id == id));
public Task<DrawingLibraryPage?> GetRevisionsAsync(Guid id, int offset = 0,
int limit = DrawingLibraryRules.DefaultRevisionLimit, CancellationToken cancellationToken = default) =>
throw new NotSupportedException();
public Task<DrawingLibraryRecord> UploadAsync(DrawingLibraryRecord record, byte[] package,
CancellationToken cancellationToken = default) => throw new NotSupportedException();
public Task<DrawingLibraryRecord> UpdatePackageAsync(Guid id, DrawingLibraryRecord record, byte[] package,
int expectedRevision, CancellationToken cancellationToken = default) => throw new NotSupportedException();
/// <summary>Stores the next revision of the sent details with the same package.</summary>
public Task<DrawingLibraryRecord> UpdateMetadataAsync(Guid id, DrawingLibraryRecord record, int expectedRevision,
CancellationToken cancellationToken = default)
{
var index = _records.FindIndex(r => r.Id == id);
var current = _records[index];
if (current.Revision != expectedRevision)
throw new DrawingLibraryConflictException(current);
var saved = new DrawingLibraryRecord
{
Id = id,
Revision = expectedRevision + 1,
Name = record.Name,
Customer = record.Customer,
MaterialName = record.MaterialName,
MaterialGrade = record.MaterialGrade,
Thickness = record.Thickness,
Units = current.Units,
CutType = record.CutType,
Notes = record.Notes,
Settings = record.Settings,
PackageSha256 = current.PackageSha256,
};
_records[index] = saved;
_packages[(id, saved.Revision)] = _packages[(id, current.Revision)];
return Task.FromResult(saved);
}
}
/// <summary>Stores uploads in memory under their client-allocated ids, and drafts a release can merge into.</summary>
private sealed class MemoryNests : INestRepository
{
private readonly Dictionary<(Guid, int), byte[]> _revisions = new();
public List<NestRecord> Uploaded { get; } = new();
public List<NestRecord> Drafts { get; } = new();
/// <summary>A Draft nest on one empty sheet, stored as a real archive at revision 3.</summary>
public NestRecord AddDraft(string name, string material, string grade, double thickness, DateTime? savedAt = null)
{
var nest = new Nest(name) { Units = Units.Inches, Material = new Material(material, grade), Thickness = thickness, Status = NestStatus.Draft };
nest.CreatePlate();
using var stream = new MemoryStream();
new NestWriter(nest).Write(stream);
var record = new NestRecord
{
Id = Guid.NewGuid(),
Revision = 3,
Name = name,
Material = material,
MaterialGrade = grade,
Thickness = thickness,
Status = NestStatus.Draft,
PlateCount = 1,
SavedAt = savedAt ?? new DateTime(2026, 10, 21, 9, 0, 0, DateTimeKind.Utc),
};
_revisions[(record.Id, record.Revision)] = stream.ToArray();
Drafts.Add(record);
return record;
}
public Task<NestPage> QueryAsync(NestQuery query, CancellationToken ct = default)
{
var found = Drafts.Where(d => query.Status == d.Status && query.Material == d.Material
&& query.MaterialGrade == d.MaterialGrade && query.Thickness == d.Thickness).ToList();
return Task.FromResult(new NestPage { Items = found, Total = found.Count, Limit = query.Limit });
}
public Task<byte[]?> GetRevisionFileAsync(Guid id, int revisionNumber, CancellationToken cancellationToken = default) =>
Task.FromResult(_revisions.TryGetValue((id, revisionNumber), out var bytes) ? bytes : null);
public Task<NestRecord> UpdateFileAsync(Guid id, byte[] nestFile, NestRecord record, int expectedRevision,
CancellationToken cancellationToken = default)
{
var current = Drafts.Single(d => d.Id == id);
Assert.Equal(current.Revision, expectedRevision);
var saved = new NestRecord { Id = id, Revision = expectedRevision + 1, Name = record.Name, Status = record.Status };
_revisions[(id, saved.Revision)] = nestFile;
return Task.FromResult(saved);
}
public Task<NestRecord> UploadAsync(byte[] file, NestRecord record, CancellationToken ct = default)
{
Assert.NotEmpty(file);
var stored = new NestRecord { Id = record.Id, Revision = 1, Name = record.Name, Status = record.Status };
Uploaded.Add(stored);
return Task.FromResult(stored);
}
public Task<NestRecord?> GetMetadataAsync(Guid id, CancellationToken ct = default) =>
Task.FromResult(Uploaded.FirstOrDefault(r => r.Id == id));
public Task<IReadOnlyList<NestRecord>> ListAsync(CancellationToken ct = default) => throw new NotSupportedException();
public Task<byte[]?> GetFileAsync(Guid id, CancellationToken ct = default) => throw new NotSupportedException();
public Task<NestRecord> UpdateFileAsync(Guid id, byte[] file, NestRecord record, CancellationToken ct = default) => throw new NotSupportedException();
public Task<NestRecord> UpdateMetadataAsync(Guid id, NestRecord record, CancellationToken ct = default) => throw new NotSupportedException();
public Task DeleteAsync(Guid id, CancellationToken ct = default) => throw new NotSupportedException();
}
}