Files
OpenNest/OpenNest.Tests/CuttingPlanning/DisplayPerimeterPostTests.cs
T
ajandClaude Opus 5.5 e8c52a354d fix(posts): refuse wrong-process output and keep settings across updates
Ahead of the new laser schedule, three ways a post could silently cut
with the wrong process, and one way its settings silently reset:

- Cincinnati CL wrote no G89 when no cut library matched the nest's
  material, thickness and gas (or no etch library for etch features),
  so the controller cut with whatever library it loaded last. It now
  refuses, unless the new "Require libraries" setting is turned off.
- Cincinnati CL declared G20/G21 from its own setting without comparing
  the nest's units, so a metric nest posted 25.4x too large. It now
  refuses a mismatch.
- CI Fiber posted any material missing from its table as the default
  code (MSN), so stainless or aluminum ran with mild-steel technology.
  A named, unmapped material is now refused; the default code covers
  only nests with no material.
- Posts saved their settings beside their DLL, inside the install
  folder a Velopack update replaces, so every update reset them to the
  shipped defaults. Saves now go to %APPDATA%\OpenNest\Posts and the
  shipped JSON only seeds them; constructing a post no longer writes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-11-02 09:51:11 -05:00

125 lines
4.6 KiB
C#

using System.Text;
using OpenNest.CNC;
using OpenNest.CNC.CuttingPlanning;
using OpenNest.Diagnostics;
using OpenNest.Engine.CuttingPlanning;
using OpenNest.Geometry;
using OpenNest.Posts.Cincinnati;
using OpenNest.Posts.CincinnatiCIFiber;
using OpenNest.Posts.GravographIS;
namespace OpenNest.Tests.CuttingPlanning;
/// <summary>No post may cut a supplied blank's display perimeter, planned or not.</summary>
public class DisplayPerimeterPostTests
{
public static IEnumerable<object[]> Posts() => [["cincinnati"], ["cifiber"], ["gravograph"]];
private static IPostProcessor Post(string name) => name switch
{
"cincinnati" => new CincinnatiPostProcessor(new CincinnatiPostConfig { RequireLibraries = false, ConfigurationName = "CL940", PostedAccuracy = 4 }),
"cifiber" => new CIFiberPostProcessor(new CIFiberPostConfig { ConfigurationName = "CI FIBER 8K" }),
_ => new GravographISPostProcessor(),
};
private static string Output(string post, Nest nest)
{
// The engraver's writer refuses this fixture's sheet for its work envelope, so compare the
// geometry its extractor hands the writer: the step that reads part programs.
if (post == "gravograph")
return string.Join("\n", new NestPolylineExtractor().ExtractLayered(nest).Select(poly =>
poly.Layer + ":" + string.Join(";", poly.Points.Select(v => FormattableString.Invariant($"{v.X:R},{v.Y:R}")))));
using var stream = new MemoryStream();
Post(post).Post(nest, stream);
return Encoding.UTF8.GetString(stream.ToArray());
}
private static (Nest Nest, Part Part) DoorNest()
{
var nest = new Nest("Door") { Units = Units.Inches };
var drawing = new Drawing("door", DisplayPerimeterTests.Door());
nest.Drawings.Add(drawing);
var plate = new Plate(new Size(20, 30));
var part = new Part(drawing, Vector.Zero);
plate.Parts.Add(part);
nest.Plates.Add(plate);
return (nest, part);
}
private static (Nest Nest, Part Part) PlannedDoorNest()
{
var (nest, part) = DoorNest();
var proposal = CuttingPlanBatch.Capture([nest.Plates[0]], ExplicitContourTests.Parameters(), false).Plan();
Assert.Equal(CuttingCommitStatus.Applied, proposal.Apply(acceptWarnings: false).Status);
Assert.True(DisplayPerimeterTail.HasTail(part.Program));
return (nest, part);
}
[Theory]
[MemberData(nameof(Posts))]
public void PlannedDoor_PostsExactlyItsCutsAndNeverTheOutline(string post)
{
var (planned, _) = PlannedDoorNest();
var (stripped, part) = PlannedDoorNest();
// The same applied plan with its tail removed by hand: what the machine may run.
Assert.True(part.RestoreLeadInProgram(DisplayPerimeterTail.WithoutTail(part.Program), locked: true));
Assert.Equal(Output(post, stripped), Output(post, planned));
}
[Theory]
[MemberData(nameof(Posts))]
public void UnplannedDoor_IsRefusedByEveryPost(string post)
{
var (nest, _) = DoorNest();
var error = Assert.Throws<InvalidOperationException>(() => Output(post, nest));
Assert.Contains("display perimeter", error.Message);
Assert.Contains("Plan Cutting", error.Message);
}
[Theory]
[MemberData(nameof(Posts))]
public void UnplannedDoor_IsRefusedBeforeTheOutputFileIsTouched(string post)
{
var (nest, _) = DoorNest();
var directory = Directory.CreateTempSubdirectory("display-perimeter-post-");
try
{
var path = Path.Combine(directory.FullName, "door.cnc");
File.WriteAllText(path, "PREVIOUS GOOD PROGRAM");
Assert.Throws<InvalidOperationException>(() => Post(post).Post(nest, path));
Assert.Equal("PREVIOUS GOOD PROGRAM", File.ReadAllText(path));
Assert.Single(directory.GetFiles());
}
finally
{
directory.Delete(recursive: true);
}
}
[Fact]
public void UnplannedDoor_PrePostReviewNamesTheDisplayPerimeter()
{
var (nest, _) = DoorNest();
var report = PostVerificationAnalyzer.Analyze(nest);
Assert.Contains(report.Findings, f => f.Message.Contains("display perimeter", StringComparison.OrdinalIgnoreCase));
Assert.False(report.CanPost(false));
}
[Fact]
public void PlannedDoor_PrePostReviewIsClean()
{
var (nest, _) = PlannedDoorNest();
var report = PostVerificationAnalyzer.Analyze(nest);
Assert.DoesNotContain(report.Findings, f => f.Message.Contains("display perimeter", StringComparison.OrdinalIgnoreCase));
}
}