Files
OpenNest/OpenNest.Tests/IO/NestStatusSerializationTests.cs
T
ajandClaude Opus 5.5 28ee04a480 feat(nests): add a Draft nest status
Nests released from drawing sets need a state before Quote: created but not
yet nested or checked. Draft is appended to NestStatus (value 3) because the
server still accepts integer statuses from existing callers, so existing
numbers must not shift; NestStatuses.WorkflowOrder puts it first wherever the
operator picks a status.

Older desktops read Draft as Quote (the documented unknown-status fallback),
and an older server refuses "draft", so the server must be upgraded before
desktops that can set it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-20 12:31:58 -04:00

209 lines
6.7 KiB
C#

using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Text;
using System.Text.Json;
using OpenNest.Geometry;
using OpenNest.IO;
namespace OpenNest.Tests.IO;
public class NestStatusSerializationTests
{
[Fact]
public void RoundTrip_StatusAndMadeBy_Preserved()
{
var nest = CreateMinimalNest();
nest.Status = NestStatus.ToBeCut;
nest.MadeBy = "AJ";
var loaded = RoundTrip(nest);
Assert.Equal(NestStatus.ToBeCut, loaded.Status);
Assert.Equal("AJ", loaded.MadeBy);
}
[Fact]
public void RoundTrip_DefaultStatus_IsQuote()
{
var loaded = RoundTrip(CreateMinimalNest());
Assert.Equal(NestStatus.Quote, loaded.Status);
Assert.Equal("", loaded.MadeBy);
}
[Fact]
public void RoundTrip_HasBeenCut_Preserved()
{
var nest = CreateMinimalNest();
nest.Status = NestStatus.HasBeenCut;
var loaded = RoundTrip(nest);
Assert.Equal(NestStatus.HasBeenCut, loaded.Status);
}
[Fact]
public void RoundTrip_Draft_PreservedAsName()
{
var nest = CreateMinimalNest();
nest.Status = NestStatus.Draft;
using var ms = new MemoryStream();
new NestWriter(nest).Write(ms);
using var doc = JsonDocument.Parse(ReadNestJson(ms));
ms.Position = 0;
var loaded = new NestReader(ms).Read();
Assert.Equal("Draft", doc.RootElement.GetProperty("status").GetString());
Assert.Equal(NestStatus.Draft, loaded.Status);
}
[Fact]
public void WorkflowOrder_StartsWithDraftAndListsEveryStatusOnce()
{
Assert.Equal(NestStatus.Draft, NestStatuses.WorkflowOrder[0]);
Assert.Equal(
Enum.GetValues<NestStatus>().OrderBy(s => s),
NestStatuses.WorkflowOrder.OrderBy(s => s));
}
[Fact]
public void ExistingStatusNumbers_AreUnchanged()
{
// The nest server still accepts integer statuses from existing callers.
Assert.Equal(0, (int)NestStatus.Quote);
Assert.Equal(1, (int)NestStatus.ToBeCut);
Assert.Equal(2, (int)NestStatus.HasBeenCut);
Assert.Equal(3, (int)NestStatus.Draft);
}
[Fact]
public void Status_SerializedAsEnumString()
{
var nest = CreateMinimalNest();
nest.Status = NestStatus.ToBeCut;
nest.MadeBy = "Rex";
using var ms = new MemoryStream();
new NestWriter(nest).Write(ms);
var dtoJson = ReadNestJson(ms);
using var doc = JsonDocument.Parse(dtoJson);
// Enum strings follow the existing units convention (PascalCase).
Assert.Equal("ToBeCut", doc.RootElement.GetProperty("status").GetString());
Assert.Equal("Rex", doc.RootElement.GetProperty("madeBy").GetString());
}
[Fact]
public void LegacyFile_WithoutStatusFields_LoadsDefaults()
{
var nest = CreateMinimalNest();
using var ms = new MemoryStream();
new NestWriter(nest).Write(ms);
// Rebuild the archive with the additive fields stripped from nest.json.
var rebuilt = StripFields(ms, "status", "madeBy");
var loaded = new NestReader(rebuilt).Read();
Assert.Equal(NestStatus.Quote, loaded.Status);
Assert.Equal("", loaded.MadeBy);
}
[Fact]
public void File_WithUnknownStatusValue_LoadsQuote()
{
var nest = CreateMinimalNest();
using var ms = new MemoryStream();
new NestWriter(nest).Write(ms);
var rebuilt = ReplaceField(ms, "status", "Banana");
var loaded = new NestReader(rebuilt).Read();
Assert.Equal(NestStatus.Quote, loaded.Status);
}
private static Nest CreateMinimalNest()
{
var program = new OpenNest.CNC.Program();
program.Codes.Add(new OpenNest.CNC.LinearMove(new Vector(10, 0)));
var drawing = new Drawing("Part", program);
var nest = new Nest { Name = "Test" };
nest.Drawings.Add(drawing);
var plate = new Plate(new Size(100, 100));
plate.Parts.Add(new Part(drawing, new Vector(0, 0)));
nest.Plates.Add(plate);
return nest;
}
private static Nest RoundTrip(Nest nest)
{
var ms = new MemoryStream();
new NestWriter(nest).Write(ms);
ms.Position = 0;
return new NestReader(ms).Read();
}
private static string ReadNestJson(MemoryStream archive)
{
archive.Position = 0;
using var zip = new ZipArchive(archive, ZipArchiveMode.Read, leaveOpen: true);
using var reader = new StreamReader(
zip.GetEntry("nest.json").Open(), Encoding.UTF8);
return reader.ReadToEnd();
}
private static MemoryStream RewriteNestJson(MemoryStream archive, string json)
{
var output = new MemoryStream();
archive.Position = 0;
using (var source = new ZipArchive(archive, ZipArchiveMode.Read, leaveOpen: true))
using (var target = new ZipArchive(output, ZipArchiveMode.Create, leaveOpen: true))
{
foreach (var entry in source.Entries)
{
var newEntry = target.CreateEntry(entry.FullName);
using var sourceStream = entry.Open();
using var targetStream = newEntry.Open();
if (entry.FullName == "nest.json")
{
using var writer = new StreamWriter(targetStream, Encoding.UTF8);
writer.Write(json);
}
else
{
sourceStream.CopyTo(targetStream);
}
}
}
output.Position = 0;
return output;
}
private static MemoryStream StripFields(MemoryStream archive, params string[] fields)
{
using var doc = JsonDocument.Parse(ReadNestJson(archive));
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var root = doc.RootElement.Deserialize<Dictionary<string, JsonElement>>(options);
foreach (var field in fields)
root.Remove(field);
return RewriteNestJson(archive, JsonSerializer.Serialize(root, options));
}
private static MemoryStream ReplaceField(MemoryStream archive, string field, string value)
{
using var doc = JsonDocument.Parse(ReadNestJson(archive));
var options = new JsonSerializerOptions
{
PropertyNamingPolicy = JsonNamingPolicy.CamelCase,
};
var root = doc.RootElement.Deserialize<Dictionary<string, JsonElement>>(options);
root[field] = JsonSerializer.Deserialize<JsonElement>($"\"{value}\"");
return RewriteNestJson(archive, JsonSerializer.Serialize(root, options));
}
}