mirror of
https://github.com/ajisaacs/OpenNest.git
synced 2026-10-27 03:51:05 -04:00
A release now ends in nests ready to cut rather than empty drafts, and a merge no longer leaves the draft it cleared waiting for someone to run Auto Nest. "Nest after release" (on by default) runs Auto Nest headless on every released group, new or merged, before anything is uploaded. NestRenest removes every plate and nests each drawing's required quantity onto sheets from the nest's plate defaults and plate-size options. It keeps a layout only when it is valid and keepable, so an invalid result never reaches the server; the nest is saved without sheets and the operator is told to run Auto Nest. Unplaced copies stay as remaining quantity, and the status column lists sheets, utilization and what did not fit. A new Nesting progress stage reports each group, and cancelling while nesting saves nothing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
599 lines
31 KiB
C#
599 lines
31 KiB
C#
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 DrawingSetBrowser = OpenNest.Controls.DrawingSetBrowser;
|
||
|
||
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 Release_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 questions = new List<string>();
|
||
|
||
using var form = new ReleaseSetForm(sets, library, nests, Server, set, (nest, record) => opened.Add((nest, record)));
|
||
form.ShowMessage = (text, _, _) => { questions.Add(text); return DialogResult.OK; };
|
||
var created = 0;
|
||
form.CreateNest = (group, _) => Task.FromResult(new Nest($"N26-{++created:D4}") { Units = group.Units });
|
||
form.Show();
|
||
var grid = Field<DataGridView>(form, "groupGrid");
|
||
var skipped = Field<Label>(form, "skippedLabel");
|
||
|
||
Assert.Equal(2, grid.Rows.Count);
|
||
Assert.True(skipped.Visible);
|
||
Assert.Contains("'SYN-OLD'", skipped.Text);
|
||
Field<DataGridView>(form, "setGrid").Rows[0].Cells["kitsColumn"].Value = "3";
|
||
Assert.Equal(new object[] { 12, 9 }, grid.Rows.Cast<DataGridViewRow>().Select(r => r.Cells["partsColumn"].Value));
|
||
Field<TextBox>(form, "notesBox").Text = "Synthetic run";
|
||
|
||
Run(form.ReleaseAsync());
|
||
|
||
Assert.Contains("Release set 'Synthetic kit' ×3 into 2 Draft nests", Assert.Single(questions));
|
||
Assert.Equal(2, nests.Uploaded.Count);
|
||
Assert.Equal(2, opened.Count);
|
||
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)));
|
||
Assert.Equal(steel.Record.Id, nests.Uploaded.Single(u => u.Name == steel.Nest.Name).Id);
|
||
|
||
var release = Assert.Single(sets.Releases);
|
||
Assert.Equal((3, "Synthetic run", 2), (release.Sets.Single().Quantity, release.Notes, release.Nests.Count));
|
||
Assert.Same(release, form.Result!.Release);
|
||
Assert.StartsWith("Released: 2 nests", Field<Label>(form, "statusLabel").Text);
|
||
Assert.All(grid.Rows.Cast<DataGridViewRow>(), r => Assert.StartsWith("Created N26-", (string)r.Cells["statusColumn"].Value));
|
||
Assert.False(Field<Button>(form, "releaseButton").Enabled);
|
||
Assert.True(Field<DataGridView>(form, "setGrid").ReadOnly);
|
||
Assert.False(Field<ComboBox>(form, "addSetCombo").Enabled);
|
||
},
|
||
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();
|
||
|
||
using var form = new ReleaseSetForm(sets, library, nests, Server, set, (_, _) => { });
|
||
form.ShowMessage = (_, _, _) => DialogResult.OK;
|
||
form.CreateNest = (group, _) => Task.FromResult(new Nest("N") { Units = group.Units });
|
||
form.Show();
|
||
var grid = Field<DataGridView>(form, "groupGrid");
|
||
var rows = grid.Rows.Cast<DataGridViewRow>().ToList();
|
||
var refused = rows.Single(r => ((SetReleaseGroup)r.Tag!).MaterialName == "Synthetic Steel");
|
||
var copper = rows.Single(r => ((SetReleaseGroup)r.Tag!).MaterialName == "Synthetic Copper");
|
||
|
||
Assert.Equal(false, refused.Cells["includeColumn"].Value);
|
||
Assert.True(refused.Cells["includeColumn"].ReadOnly);
|
||
Assert.Contains("named 'SYN-A'", (string)refused.Cells["statusColumn"].Value);
|
||
copper.Cells["includeColumn"].Value = false;
|
||
Assert.Contains("1 of 3 groups chosen", Field<Label>(form, "summaryLabel").Text);
|
||
// Rebuilding the plan keeps the operator's choice.
|
||
Field<CheckBox>(form, "includeArchivedCheckBox").Checked = true;
|
||
Assert.Equal(false, grid.Rows.Cast<DataGridViewRow>()
|
||
.Single(r => ((SetReleaseGroup)r.Tag!).MaterialName == "Synthetic Copper").Cells["includeColumn"].Value);
|
||
|
||
Run(form.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 ReleaseNotRecorded_CanBeRecordedAgain()
|
||
{
|
||
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;
|
||
|
||
using var form = new ReleaseSetForm(sets, library, new MemoryNests(), Server, set, (_, _) => opened++);
|
||
form.ShowMessage = (_, _, _) => DialogResult.OK;
|
||
form.CreateNest = (group, _) => Task.FromResult(new Nest("N") { Units = group.Units });
|
||
form.Show();
|
||
var release = Field<Button>(form, "releaseButton");
|
||
|
||
Run(form.ReleaseAsync());
|
||
|
||
Assert.Equal(1, opened);
|
||
Assert.Empty(sets.Releases);
|
||
Assert.Equal(("Record Release", true), (release.Text, release.Enabled));
|
||
Assert.Contains("Synthetic release post failure.", Field<Label>(form, "statusLabel").Text);
|
||
|
||
Run(form.RetryRecordAsync());
|
||
|
||
Assert.Equal(2, sets.PostAttempts);
|
||
Assert.Same(Assert.Single(sets.Releases), form.Result!.Release);
|
||
Assert.Equal(("Release", false), (release.Text, release.Enabled));
|
||
},
|
||
TestTimeout,
|
||
"The release retry 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 SetReleasesForm(sets, set);
|
||
form.Show();
|
||
var releases = Field<DataGridView>(form, "releaseGrid");
|
||
var nests = Field<DataGridView>(form, "nestGrid");
|
||
PumpUntil(() => releases.Rows.Count == 1 && nests.Rows.Count == 3, "the releases");
|
||
|
||
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 Release_OfATypedInSecondSet_MergesOneGroupIntoADraft_UnlessItIsOpen()
|
||
{
|
||
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 sets = new InMemoryDrawingSets();
|
||
var first = CreateSet(sets, "First kit", (a, 1), (c, 1));
|
||
CreateSet(sets, "Second kit", (b, 2));
|
||
var nests = new MemoryNests();
|
||
var draft = nests.AddDraft("N26-0007", "Synthetic Steel", "Grade A", 0.25);
|
||
var opened = new List<NestRecord>();
|
||
var messages = new List<string>();
|
||
|
||
using var form = new ReleaseSetForm(sets, library, nests, Server, first, (_, record) => opened.Add(record));
|
||
form.ShowMessage = (text, _, _) => { messages.Add(text); return DialogResult.OK; };
|
||
form.CreateNest = (group, _) => Task.FromResult(new Nest("N26-0100") { Units = group.Units });
|
||
var openTabs = new HashSet<Guid> { draft.Id };
|
||
form.IsNestOpen = openTabs.Contains;
|
||
form.Show();
|
||
|
||
Field<ComboBox>(form, "addSetCombo").Text = "second KIT";
|
||
Run(form.AddTypedSetAsync());
|
||
Assert.Equal(new[] { ("First kit", 1), ("Second kit", 1) }, form.Lines().Select(l => (l.Set.Name, l.Quantity)));
|
||
Assert.False(form.AddLine(first, 2));
|
||
Field<DataGridView>(form, "setGrid").Rows[1].Cells["kitsColumn"].Value = "2";
|
||
|
||
var grid = Field<DataGridView>(form, "groupGrid");
|
||
DataGridViewRow Steel() => grid.Rows.Cast<DataGridViewRow>().Single(r => ((SetReleaseGroup)r.Tag!).MaterialName == "Synthetic Steel");
|
||
PumpUntil(() => ((DataGridViewComboBoxCell)Steel().Cells["targetColumn"]).Items.Count == 2, "the drafts to merge into");
|
||
Assert.Equal(new object[] { 1, 4 }, new[] { a, b }.Select(d => (object)((SetReleaseGroup)Steel().Tag!).Items.Single(i => i.Drawing.Id == d.Id).Quantity));
|
||
var mergeChoice = (string)((DataGridViewComboBoxCell)Steel().Cells["targetColumn"]).Items[1]!;
|
||
Assert.StartsWith("Merge into N26-0007 (1 sheet", mergeChoice);
|
||
|
||
// An open draft is refused and the group stays a new nest.
|
||
Steel().Cells["targetColumn"].Value = mergeChoice;
|
||
Assert.Contains("is open in a tab here", Assert.Single(messages));
|
||
Assert.Equal("New Draft nest", Steel().Cells["targetColumn"].Value);
|
||
Assert.Empty(form.ChosenMerges());
|
||
|
||
openTabs.Clear();
|
||
Steel().Cells["targetColumn"].Value = mergeChoice;
|
||
Assert.Same(draft, form.ChosenMerges().Values.Single());
|
||
Assert.Contains("Merging discards the layout of 'N26-0007'", (string)Steel().Cells["statusColumn"].Value);
|
||
Assert.Contains("1 new Draft nest, 1 merge, 6 parts", Field<Label>(form, "summaryLabel").Text);
|
||
|
||
Run(form.ReleaseAsync());
|
||
|
||
Assert.Contains("1 new and 1 merged into existing drafts", messages[^1]);
|
||
var release = Assert.Single(sets.Releases);
|
||
Assert.Equal(new[] { ("First kit", 1), ("Second kit", 2) }, release.Sets.Select(l => (l.SetName, l.Quantity)));
|
||
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.Equal(SetReleaseNestKind.Created, release.Nests.Single(n => n.NestId != draft.Id).Kind);
|
||
Assert.Equal(2, opened.Count);
|
||
Assert.StartsWith("Merged into N26-0007", (string)Steel().Cells["statusColumn"].Value);
|
||
},
|
||
TestTimeout,
|
||
"The multi-set merge 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 questions = new List<string>();
|
||
|
||
using var form = new ReleaseSetForm(sets, library, new MemoryNests(), Server, set, (nest, _) => opened.Add(nest));
|
||
form.ShowMessage = (text, _, _) => { questions.Add(text); return DialogResult.OK; };
|
||
form.CreateNest = (group, _) =>
|
||
{
|
||
var nest = new Nest("N26-0200") { Units = group.Units };
|
||
nest.PlateDefaults.Size = new OpenNest.Geometry.Size(48, 96);
|
||
return Task.FromResult(nest);
|
||
};
|
||
form.Show();
|
||
Assert.True(Field<CheckBox>(form, "nestAfterReleaseCheckBox").Checked);
|
||
|
||
Run(form.ReleaseAsync());
|
||
|
||
Assert.Contains("Each is nested with Auto Nest", Assert.Single(questions));
|
||
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);
|
||
var status = (string)Field<DataGridView>(form, "groupGrid").Rows[0].Cells["statusColumn"].Value;
|
||
Assert.StartsWith("Created N26-0200 — Nested on 1 sheet, ", status);
|
||
},
|
||
TestTimeout,
|
||
"The nest after release test did not complete.");
|
||
}
|
||
|
||
[Fact]
|
||
public void NestAfterRelease_Unticked_LeavesNestsAndMergedDraftsForAutoNest()
|
||
{
|
||
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 questions = new List<string>();
|
||
var renested = 0;
|
||
|
||
using var form = new ReleaseSetForm(sets, library, nests, Server, set, (_, _) => { });
|
||
form.ShowMessage = (text, _, _) => { questions.Add(text); return DialogResult.OK; };
|
||
form.CreateNest = (group, _) => Task.FromResult(new Nest("N") { Units = group.Units });
|
||
form.Renest = (_, _) => { renested++; return Task.FromResult(new SetReleaseRenest(true, "")); };
|
||
form.Show();
|
||
var row = Field<DataGridView>(form, "groupGrid").Rows[0];
|
||
PumpUntil(() => ((DataGridViewComboBoxCell)row.Cells["targetColumn"]).Items.Count == 2, "the drafts to merge into");
|
||
row.Cells["targetColumn"].Value = ((DataGridViewComboBoxCell)row.Cells["targetColumn"]).Items[1];
|
||
Assert.Contains("and nests everything again", (string)row.Cells["statusColumn"].Value);
|
||
|
||
Field<CheckBox>(form, "nestAfterReleaseCheckBox").Checked = false;
|
||
Assert.Contains("and leaves everything for Auto Nest", (string)row.Cells["statusColumn"].Value);
|
||
|
||
Run(form.ReleaseAsync());
|
||
|
||
Assert.Equal(0, renested);
|
||
Assert.Contains("current layout, left for Auto Nest", Assert.Single(questions));
|
||
Assert.DoesNotContain("nested with Auto Nest", questions[0]);
|
||
var merged = Assert.Single(form.Result!.Created);
|
||
Assert.Same(draft, merged.MergedInto);
|
||
Assert.Empty(merged.Nest!.Plates);
|
||
Assert.Null(merged.Renest);
|
||
Assert.False(Field<CheckBox>(form, "nestAfterReleaseCheckBox").Enabled);
|
||
},
|
||
TestTimeout,
|
||
"The release without nesting 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 SetReleasesForm(sets, set);
|
||
form.ShowMessage = (text, buttons, _) =>
|
||
{
|
||
questions.Add(text);
|
||
return buttons == MessageBoxButtons.YesNo ? DialogResult.Yes : DialogResult.OK;
|
||
};
|
||
form.Show();
|
||
var releases = Field<DataGridView>(form, "releaseGrid");
|
||
var nests = Field<DataGridView>(form, "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>(form, "undoButton").Enabled);
|
||
|
||
Run(form.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>(form, "undoButton").Enabled);
|
||
},
|
||
TestTimeout,
|
||
"The release undo test did not complete.");
|
||
}
|
||
|
||
[Fact]
|
||
public void Browser_Release_ReadsTheSetWithItsMembers_OnlyWhenEnabled()
|
||
{
|
||
StaTestThread.Run(
|
||
() =>
|
||
{
|
||
var library = new PackageLibrary();
|
||
var a = library.Add("SYN-A", "Synthetic Steel");
|
||
var sets = new InMemoryDrawingSets();
|
||
CreateSet(sets, (a, 2));
|
||
var released = new List<DrawingSetRecord>();
|
||
|
||
using var host = new Form();
|
||
var browser = new DrawingSetBrowser { Dock = DockStyle.Fill };
|
||
host.Controls.Add(browser);
|
||
browser.Initialize(sets, library, Server);
|
||
browser.ShowRelease = released.Add;
|
||
host.Show();
|
||
Run(browser.RefreshAsync());
|
||
var releaseButton = Field<Button>(browser, "releaseButton");
|
||
Assert.False(releaseButton.Visible);
|
||
|
||
browser.EnableRelease(new MemoryNests(), (_, _) => { });
|
||
Assert.True(releaseButton.Visible);
|
||
Assert.True(releaseButton.Enabled);
|
||
Run(browser.ReleaseSelectedAsync());
|
||
|
||
var set = Assert.Single(released);
|
||
Assert.Equal("SYN-A", Assert.Single(set.Members).Drawing!.Name);
|
||
},
|
||
TestTimeout,
|
||
"The browser release 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();
|
||
}
|
||
|
||
private static void Run(Task task) => PumpUntil(() => task.IsCompleted, "the operation to finish");
|
||
|
||
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>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)
|
||
{
|
||
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 = 0.25,
|
||
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) =>
|
||
throw new NotSupportedException();
|
||
|
||
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();
|
||
|
||
public Task<DrawingLibraryRecord> UpdateMetadataAsync(Guid id, DrawingLibraryRecord record, int expectedRevision,
|
||
CancellationToken cancellationToken = default) => throw new NotSupportedException();
|
||
}
|
||
|
||
/// <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)
|
||
{
|
||
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 = 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();
|
||
}
|
||
}
|