Files
OpenNest/OpenNest.Tests/Reporting/AtomicReportFileTests.cs
T
aj 74d2b6aa68 feat(reporting): add one-plate nest report PDF library slice
Add OpenNest.Reporting (net8.0, PDFsharp-MigraDoc 6.2.4) with a detached
report snapshot and a PDF writer for an empty/demand-only job or a single
plate layout: Letter portrait summary with vector part thumbnails and a
landscape plate page with a vector sheet diagram, both with Page X of Y.

- Snapshot uses reference identity for document-local R### IDs, recounts
  nested quantities from placements x copies with checked wide integers,
  excludes cutoffs from accounting, and retains no live domain objects.
- Geometry keeps Cut/Display material paths, drops rapid/scribe/lead paths,
  preserves native arcs, holes and intentional tab gaps, and rejects
  malformed programs with plate/part identification.
- Open contours and cutoffs are stroked per contour so PDFsharp cannot join
  a tab gap to the next contour.
- Bundled DejaVu Sans via a custom resolver (including MigraDoc's error
  font); unsupported text fails with the field and code point.
- Layouts beyond this slice (multiple plates, page overflow, labels that
  do not fit) fail with NotSupportedException before the destination is
  replaced; output is rendered to a temporary sibling and moved atomically.
2026-09-29 13:13:32 -04:00

64 lines
2.3 KiB
C#

using OpenNest.Reporting;
namespace OpenNest.Tests.Reporting;
public sealed class AtomicReportFileTests : IDisposable
{
private readonly string directory = Path.Combine(Path.GetTempPath(), "opennest-report-" + Guid.NewGuid().ToString("N"));
public AtomicReportFileTests() => Directory.CreateDirectory(directory);
[Fact]
public void SuccessfulRenderReplacesExistingFileOnlyAfterRendering()
{
var path = Path.Combine(directory, "existing.pdf");
File.WriteAllText(path, "original");
AtomicReportFile.Write(path, stream =>
{
Assert.Equal("original", File.ReadAllText(path));
stream.Write("replacement"u8);
});
Assert.Equal("replacement", File.ReadAllText(path));
Assert.Equal(new[] { path }, Directory.GetFiles(directory));
}
[Fact]
public void LateRenderFailurePreservesExistingFileAndRemovesTemporarySibling()
{
var path = Path.Combine(directory, "existing.pdf");
File.WriteAllText(path, "original");
Assert.Throws<IOException>(() => AtomicReportFile.Write(path, stream =>
{
stream.Write("partial PDF"u8);
Assert.Equal(2, Directory.GetFiles(directory).Length);
throw new IOException("injected late render failure");
}));
Assert.Equal("original", File.ReadAllText(path));
Assert.Equal(new[] { path }, Directory.GetFiles(directory));
}
[Fact]
public void FailedNewExportLeavesNoOutput()
{
var path = Path.Combine(directory, "new.pdf");
Assert.Throws<IOException>(() => AtomicReportFile.Write(path, stream =>
{
stream.Write("partial"u8);
throw new IOException("injected write failure");
}));
Assert.Empty(Directory.GetFiles(directory));
}
[Fact]
public void FailedReplacementPreservesDirectoryTargetAndCleansTemporaryFile()
{
var path = Path.Combine(directory, "not-a-file.pdf");
Directory.CreateDirectory(path);
Assert.ThrowsAny<IOException>(() => AtomicReportFile.Write(path, stream => stream.Write("PDF"u8)));
Assert.True(Directory.Exists(path));
Assert.Empty(Directory.GetFiles(directory));
}
public void Dispose() => Directory.Delete(directory, true);
}