using OpenNest.CNC.CuttingStrategy; using OpenNest.Data; using OpenNest.Geometry; using OpenNest.IO; using OpenNest.Shapes; namespace OpenNest.Server.Tests; /// /// The whole release path: code-built drawings in two materials are packaged into the /// library, collected into a set, released ×2 through the real clients, and the nests and /// release record are read back from the production routes. /// public sealed class SetReleaseEndToEndTests { private static Drawing SyntheticDrawing(string name, double length) { var drawing = new RectangleShape { Name = name, Length = length, Width = 2 }.GetDrawing(); drawing.CutPins.Add(new ContourCutPin(new Vector(length, 2), new LineLeadIn { Length = 0.25 }, new NoLeadOut(), new NormalTab { Size = 0.03 })); return drawing; } private static async Task Store(RemoteDrawingLibrary library, string name, double length, string material, CutType cutType) { var record = new DrawingLibraryRecord { Id = Guid.NewGuid(), Name = name, Customer = "Synthetic customer", MaterialName = material, MaterialGrade = "Grade A", Thickness = 0.25, Units = Units.Inches, CutType = cutType, SavedBy = "Synthetic operator", }; return await library.UploadAsync(record, DrawingPackage.Write(SyntheticDrawing(name, length), Units.Inches)); } private static byte[] Write(Nest nest) { using var stream = new MemoryStream(); new NestWriter(nest).Write(stream); return stream.ToArray(); } private static Nest Read(byte[] archive) { using var stream = new MemoryStream(archive, writable: false); return new NestReader(stream).Read(); } [Fact] public async Task Release_MakesOneDraftNestPerMaterialAndRecordsThem() { using var factory = new ServerFactory(); using var client = factory.CreateClient(); var url = client.BaseAddress!.ToString(); using var library = new RemoteDrawingLibrary(client, url); using var sets = new RemoteDrawingSetStore(client, url); using var nests = new RemoteNestRepository(client, url); var a = await Store(library, "SYN-A", 4, "Synthetic Steel", CutType.Laser); var b = await Store(library, "SYN-B", 5, "Synthetic Steel", CutType.Laser); var c = await Store(library, "SYN-C", 6, "Synthetic Aluminum", CutType.Waterjet); var created = await sets.CreateAsync(new DrawingSetRecord { Id = Guid.NewGuid(), Name = "Synthetic kit", Customer = "Synthetic set customer", Members = new[] { (a, 3), (b, 1), (c, 2) } .Select(m => new DrawingSetMember { DrawingId = m.Item1.Id, Quantity = m.Item2 }).ToList(), }); var set = await sets.GetAsync(created.Id); var plan = SetReleasePlan.Build(set!, 2, includeArchived: false); var number = 0; var session = new SetReleaseSession(sets, library, nests, url, bytes => { var contents = DrawingPackage.Read(bytes); return new LibraryPackage(bytes, contents.Drawing, contents.Units, contents.Warnings); }, Write, (group, _) => Task.FromResult(new Nest($"Synthetic release {++number}") { Units = group.Units })); var result = await session.RunAsync(plan, plan.Groups, "Synthetic operator", "Synthetic release notes"); Assert.Equal("", result.ReleaseError); Assert.Equal(2, result.Created.Count); foreach (var group in result.Groups) { var stored = Read((await nests.GetFileAsync(group.Record!.Id))!); Assert.Equal(NestStatus.Draft, (await nests.GetMetadataAsync(group.Record.Id))!.Status); Assert.Equal((NestStatus.Draft, group.Group.MaterialName, 0.25, "Synthetic set customer"), (stored.Status, stored.Material.Name, stored.Thickness, stored.Customer)); foreach (var item in group.Group.Items) { var drawing = stored.Drawings.Single(d => d.Name == item.Drawing.Name); Assert.Equal(item.Member.Quantity * 2, drawing.Quantity.Required); Assert.Equal((item.Drawing.Id, 1, url.TrimEnd('/')), (drawing.Library!.Id, drawing.Library.Revision, drawing.Library.ServerKey)); Assert.Single(drawing.CutPins); } } var steel = result.Groups.Single(g => g.Group.MaterialName == "Synthetic Steel"); Assert.Equal(new[] { "SYN-A", "SYN-B" }, steel.Group.Items.Select(i => i.Drawing.Name)); var releases = await sets.QueryReleasesAsync(setId: created.Id); var release = Assert.Single(releases.Items); var line = Assert.Single(release.Sets); Assert.Equal((result.Release!.Id, 1, 2, "Synthetic release notes"), (release.Id, line.SetRevision, line.Quantity, release.Notes)); Assert.Equal(result.Groups.Select(g => g.Record!.Id), release.Nests.Select(n => n.NestId)); Assert.All(release.Nests, n => Assert.Equal((NestStatus.Draft, SetReleaseNestKind.Created, 1), (n.Nest!.Status, n.Kind, n.RevisionAfter))); Assert.Equal(CutType.Waterjet, release.Nests.Single(n => n.MaterialName == "Synthetic Aluminum").CutType); Assert.Equal(new[] { (a.Id, 6), (b.Id, 2) }, release.Nests.Single(n => n.NestId == steel.Record!.Id).Items.Select(i => (i.DrawingId, i.Quantity))); Assert.Equal(release.Id, (await sets.QueryReleasesAsync(nestId: steel.Record!.Id)).Items.Single().Id); // Undo deletes every nest the release created. var undone = await sets.UndoReleaseAsync(release.Id, new SetReleaseUndoRequest { UndoneBy = "Synthetic operator" }); Assert.All(undone.Nests, n => Assert.Equal(SetReleaseNestUndo.Deleted, n.Undo)); foreach (var group in result.Groups) Assert.Null(await nests.GetMetadataAsync(group.Record!.Id)); } [Fact] public async Task Release_OfASecondKit_MergesIntoTheFirstKitsDraft_AndUndoRestoresIt() { using var factory = new ServerFactory(); using var client = factory.CreateClient(); var url = client.BaseAddress!.ToString(); using var library = new RemoteDrawingLibrary(client, url); using var sets = new RemoteDrawingSetStore(client, url); using var nests = new RemoteNestRepository(client, url); var a = await Store(library, "SYN-A", 4, "Synthetic Steel", CutType.Laser); var b = await Store(library, "SYN-B", 5, "Synthetic Steel", CutType.Laser); DrawingSetRecord Kit(string name, params (DrawingLibraryRecord Drawing, int Quantity)[] members) => new() { Id = Guid.NewGuid(), Name = name, Members = members.Select(m => new DrawingSetMember { DrawingId = m.Drawing.Id, Quantity = m.Quantity }).ToList(), }; var firstKit = await sets.GetAsync((await sets.CreateAsync(Kit("First kit", (a, 2)))).Id); var secondKit = await sets.GetAsync((await sets.CreateAsync(Kit("Second kit", (a, 1), (b, 3)))).Id); var number = 0; SetReleaseSession Session() => new(sets, library, nests, url, bytes => { var contents = DrawingPackage.Read(bytes); return new LibraryPackage(bytes, contents.Drawing, contents.Units, contents.Warnings); }, Write, (group, _) => Task.FromResult(new Nest($"Synthetic merge {++number}") { Units = group.Units }), Read); var firstPlan = SetReleasePlan.Build(firstKit!, 1, includeArchived: false); var first = await Session().RunAsync(firstPlan, firstPlan.Groups, "Synthetic operator", ""); var draft = (await SetReleasePlan.FindDraftsAsync(nests, firstPlan.Groups[0])).Single(); Assert.Equal(first.Created.Single().Record!.Id, draft.Id); var secondPlan = SetReleasePlan.Build(secondKit!, 2, includeArchived: false); var second = await Session().RunAsync(secondPlan, secondPlan.Groups, "Synthetic operator", "", mergeInto: new Dictionary { [secondPlan.Groups[0]] = draft }); Assert.Equal("", second.ReleaseError); var merged = Read((await nests.GetFileAsync(draft.Id))!); Assert.Equal(new[] { ("SYN-A", 4), ("SYN-B", 6) }, merged.Drawings.OrderBy(d => d.Name).Select(d => (d.Name, d.Quantity.Required))); var mergeRelease = second.Release!; Assert.Equal((SetReleaseNestKind.Merged, (int?)draft.Revision, draft.Revision + 1), (mergeRelease.Nests[0].Kind, mergeRelease.Nests[0].RevisionBefore, mergeRelease.Nests[0].RevisionAfter)); Assert.Equal(2, (await sets.QueryReleasesAsync(nestId: draft.Id)).Total); // Undo the merge: the draft is the first kit's again. await sets.UndoReleaseAsync(mergeRelease.Id, new SetReleaseUndoRequest { UndoneBy = "Synthetic operator" }); var restored = Read((await nests.GetFileAsync(draft.Id))!); Assert.Equal(new[] { ("SYN-A", 2) }, restored.Drawings.Select(d => (d.Name, d.Quantity.Required))); Assert.Equal(draft.Revision, (await nests.GetMetadataAsync(draft.Id))!.RestoredFromRevision); } }