Files
OpenNest/OpenNest.Tests/CuttingPlanning/DisplayPerimeterPostTests.cs
T
aj 00cb639c8e fix(posts): refuse unpostable blanks before writing and malformed tails
With the pre-post risks acknowledged, an unplanned blank reached the
Cincinnati and engraver posts, which created the output file before
refusing: an existing program at that path was emptied and the desktop
had no handler for the refusal. Both posts now check every part before
creating the file, and Post shows the refusal like the multi-file path.

A display tail marker is now honoured only as one marker ending the
part's own program, followed by a rapid and the outline alone. A marker
inside a sub-program or followed by real cuts is refused by posts and by
executed reads, so cuts after it can never be dropped from both
verification and output.
2026-10-22 20:30:33 -04: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 { 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));
}
}