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.
64 lines
2.3 KiB
C#
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);
|
|
}
|