diff --git a/src/E2E/Expect/ScreenshotAssertion.cs b/src/E2E/Expect/ScreenshotAssertion.cs index 91353468..a068c9d8 100644 --- a/src/E2E/Expect/ScreenshotAssertion.cs +++ b/src/E2E/Expect/ScreenshotAssertion.cs @@ -253,9 +253,7 @@ private static async Task KeepAsync(Run run, Locator? subject, RgbaImage? expect } Directory.CreateDirectory(Path.GetDirectoryName(run.File)!); - var temporary = run.File + "." + Guid.NewGuid().ToString("N")[..8] + ".tmp"; - await System.IO.File.WriteAllBytesAsync(temporary, ImageOps.EncodePng(settled)).ConfigureAwait(false); - System.IO.File.Move(temporary, run.File, overwrite: true); + await AtomicWrite.WriteFileAtomicAsync(run.File, ImageOps.EncodePng(settled)).ConfigureAwait(false); store.MarkWritten(run.File); if (expected is null && !store.Update) { diff --git a/src/E2E/Internal/AiTrace.cs b/src/E2E/Internal/AiTrace.cs index b7ecfc54..0251845b 100644 --- a/src/E2E/Internal/AiTrace.cs +++ b/src/E2E/Internal/AiTrace.cs @@ -901,16 +901,13 @@ public void Flush(bool force) _pending = null; Directory.CreateDirectory(System.IO.Path.GetDirectoryName(Path)!); - var temp = Path + "." + Guid.NewGuid().ToString("n") + ".tmp"; try { - File.WriteAllText(temp, JsonSerializer.Serialize(Collector.Document(), Options)); - File.Move(temp, Path, overwrite: true); + AtomicWrite.WriteFileAtomic(Path, JsonSerializer.Serialize(Collector.Document(), Options)); _dirty = false; } finally { - File.Delete(temp); _written = Stopwatch.GetTimestamp(); } } diff --git a/src/E2E/Internal/AtomicWrite.cs b/src/E2E/Internal/AtomicWrite.cs index 3ca864ae..6225ff96 100644 --- a/src/E2E/Internal/AtomicWrite.cs +++ b/src/E2E/Internal/AtomicWrite.cs @@ -27,12 +27,16 @@ internal static class AtomicWrite private static readonly UTF8Encoding Utf8 = new(false); /// Writes to atomically; a failed write leaves no temporary file behind. - public static async Task WriteFileAtomicAsync(string filePath, string content, CancellationToken cancellationToken = default) + public static Task WriteFileAtomicAsync(string filePath, string content, CancellationToken cancellationToken = default) => + WriteFileAtomicAsync(filePath, Utf8.GetBytes(content), cancellationToken); + + /// The bytes form of (upstream accepts a Uint8Array). + public static async Task WriteFileAtomicAsync(string filePath, byte[] content, CancellationToken cancellationToken = default) { var temporary = TemporaryPath(filePath); try { - await File.WriteAllTextAsync(temporary, content, Utf8, cancellationToken).ConfigureAwait(false); + await File.WriteAllBytesAsync(temporary, content, cancellationToken).ConfigureAwait(false); for (var attempt = 1; ; attempt++) { try