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