From 0a319bfa51864a2da16cfcc7e259506282c4ff87 Mon Sep 17 00:00:00 2001 From: Simon Cropp Date: Sat, 29 Aug 2026 17:17:22 +1000 Subject: [PATCH] Build appended file targets per verification Appended files were stored as Targets, and a binary one holds a live stream. The engine disposes the stream of every target it writes, so a second Verify with the same settings instance failed with ObjectDisposedException, even though the caller had handed over re-readable bytes or a file path. Settings are reused routinely, since SettingsTask copies them per Verify call. They are now stored as factories and built once per verification: AppendContentAsFile(byte[]) opens a fresh MemoryStream over the same array, and AppendFile(string) opens the file per verification instead of holding a handle from the moment it was appended. Existence is still checked eagerly, so a missing path is still reported where it was passed. AppendFile(Stream) is unchanged and still single use: the stream is owned by the caller and cannot be replayed. That is now documented on the overload. --- ...ileSettingsReuse_first#sample.verified.png | Bin 0 -> 4621 bytes ...AppendFileSettingsReuse_first.verified.txt | 1 + ...leSettingsReuse_second#sample.verified.png | Bin 0 -> 4621 bytes ...ppendFileSettingsReuse_second.verified.txt | 1 + ...ingsReuse_first#appendedBytes.verified.bin | 1 + ...inaryBytesSettingsReuse_first.verified.txt | 1 + ...ngsReuse_second#appendedBytes.verified.bin | 1 + ...naryBytesSettingsReuse_second.verified.txt | 1 + .../Converters/InstanceFileAppenderTests.cs | 27 ++++++++ src/Verify/Guards.cs | 13 ++++ src/Verify/Splitters/Settings_FileAppender.cs | 60 ++++++++++++++---- 11 files changed, 95 insertions(+), 11 deletions(-) create mode 100644 src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_first#sample.verified.png create mode 100644 src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_first.verified.txt create mode 100644 src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_second#sample.verified.png create mode 100644 src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_second.verified.txt create mode 100644 src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_first#appendedBytes.verified.bin create mode 100644 src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_first.verified.txt create mode 100644 src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_second#appendedBytes.verified.bin create mode 100644 src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_second.verified.txt diff --git a/src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_first#sample.verified.png b/src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_first#sample.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..b54820aa60a6fca03967c68c61231b07f8978da8 GIT binary patch literal 4621 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 DTes+R literal 0 HcmV?d00001 diff --git a/src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_first.verified.txt b/src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_first.verified.txt new file mode 100644 index 0000000000..ca25d6d7b1 --- /dev/null +++ b/src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_first.verified.txt @@ -0,0 +1 @@ +First \ No newline at end of file diff --git a/src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_second#sample.verified.png b/src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_second#sample.verified.png new file mode 100644 index 0000000000000000000000000000000000000000..b54820aa60a6fca03967c68c61231b07f8978da8 GIT binary patch literal 4621 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 DTes+R literal 0 HcmV?d00001 diff --git a/src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_second.verified.txt b/src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_second.verified.txt new file mode 100644 index 0000000000..ad4facd1db --- /dev/null +++ b/src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_second.verified.txt @@ -0,0 +1 @@ +Second \ No newline at end of file diff --git a/src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_first#appendedBytes.verified.bin b/src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_first#appendedBytes.verified.bin new file mode 100644 index 0000000000..aed2973e4b --- /dev/null +++ b/src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_first#appendedBytes.verified.bin @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_first.verified.txt b/src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_first.verified.txt new file mode 100644 index 0000000000..ca25d6d7b1 --- /dev/null +++ b/src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_first.verified.txt @@ -0,0 +1 @@ +First \ No newline at end of file diff --git a/src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_second#appendedBytes.verified.bin b/src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_second#appendedBytes.verified.bin new file mode 100644 index 0000000000..aed2973e4b --- /dev/null +++ b/src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_second#appendedBytes.verified.bin @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_second.verified.txt b/src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_second.verified.txt new file mode 100644 index 0000000000..ad4facd1db --- /dev/null +++ b/src/Verify.Tests/Converters/InstanceFileAppenderTests.BinaryBytesSettingsReuse_second.verified.txt @@ -0,0 +1 @@ +Second \ No newline at end of file diff --git a/src/Verify.Tests/Converters/InstanceFileAppenderTests.cs b/src/Verify.Tests/Converters/InstanceFileAppenderTests.cs index 92aaf8ffd6..7d71760a6c 100644 --- a/src/Verify.Tests/Converters/InstanceFileAppenderTests.cs +++ b/src/Verify.Tests/Converters/InstanceFileAppenderTests.cs @@ -55,4 +55,31 @@ public Task TextBytesFluent() => public Task TextStreamFluent() => Verify("Foo") .AppendFile(new MemoryStream("appendedFile"u8.ToArray())); + + // The engine disposes the stream of every target it writes, so an appended binary file + // held as a live stream was dead after the first verification. Both of these are backed + // by something re-readable, so reusing the settings has to work. + [Fact] + public async Task BinaryBytesSettingsReuse() + { + var reused = new VerifySettings(); + reused.AppendContentAsFile(new byte[] {1, 2, 3}, "bin", "appendedBytes"); + + await Verify("First", reused) + .UseMethodName("BinaryBytesSettingsReuse_first"); + await Verify("Second", reused) + .UseMethodName("BinaryBytesSettingsReuse_second"); + } + + [Fact] + public async Task AppendFileSettingsReuse() + { + var reused = new VerifySettings(); + reused.AppendFile("sample.png"); + + await Verify("First", reused) + .UseMethodName("AppendFileSettingsReuse_first"); + await Verify("Second", reused) + .UseMethodName("AppendFileSettingsReuse_second"); + } } \ No newline at end of file diff --git a/src/Verify/Guards.cs b/src/Verify/Guards.cs index 96cb8cbc31..2709168b86 100644 --- a/src/Verify/Guards.cs +++ b/src/Verify/Guards.cs @@ -16,6 +16,19 @@ public static void BadFileName(string name, [CallerArgumentExpression("name")] s } } + /// + /// Kept as a fail fast for APIs that record a path and read it later, so a missing + /// file is reported where the caller passed it rather than at verification time. + /// + public static void FileExists(string path, [CallerArgumentExpression(nameof(path))] string argumentName = "") + { + Ensure.NotNullOrEmpty(path, argumentName); + if (!File.Exists(path)) + { + throw new FileNotFoundException($"File not found. Path: {path}", path); + } + } + static char[] invalidPathChars = Path .GetInvalidPathChars() .Concat(invalidFileChars.Except(['/', '\\', ':'])) diff --git a/src/Verify/Splitters/Settings_FileAppender.cs b/src/Verify/Splitters/Settings_FileAppender.cs index e4bf1581d5..e2a28e491d 100644 --- a/src/Verify/Splitters/Settings_FileAppender.cs +++ b/src/Verify/Splitters/Settings_FileAppender.cs @@ -20,9 +20,9 @@ internal static IEnumerable GetFileAppenders(VerifySettings settings) if (settings.appendedFiles != null) { - foreach (var target in settings.appendedFiles) + foreach (var buildTarget in settings.appendedFiles) { - yield return target; + yield return buildTarget(); } } } @@ -37,18 +37,23 @@ public static void RegisterFileAppender(FileAppender appender) public partial class VerifySettings { - internal List? appendedFiles; + /// + /// Built once per verification rather than held as Targets. The engine disposes the + /// stream of every target it writes, so a stored stream is dead after the first + /// verification, and settings are reused: SettingsTask copies them per Verify call. + /// + internal List>? appendedFiles; public void AppendContentAsFile(string content, string extension = "txt", string? name = null) { appendedFiles ??= []; - appendedFiles.Add(new(extension, content, name)); + appendedFiles.Add(() => new(extension, content, name)); } public void AppendContentAsFile(StringBuilder content, string extension = "txt", string? name = null) { appendedFiles ??= []; - appendedFiles.Add(new(extension, content, name)); + appendedFiles.Add(() => new(extension, content, name)); } public void AppendContentAsFile(byte[] content, string extension = "txt", string? name = null) @@ -56,16 +61,26 @@ public void AppendContentAsFile(byte[] content, string extension = "txt", string appendedFiles ??= []; if (FileExtensions.IsTextExtension(extension)) { - appendedFiles.Add(new(extension, Encoding.UTF8.GetString(content), name)); + var text = Encoding.UTF8.GetString(content); + appendedFiles.Add(() => new(extension, text, name)); } else { - appendedFiles.Add(new(extension, new MemoryStream(content), name)); + // A fresh stream per verification: the bytes stay re-readable, so reusing the + // settings for a second Verify works. + appendedFiles.Add(() => new(extension, new MemoryStream(content), name)); } } - public void AppendFile(string file, string? name = null) => - AppendFile(IoHelpers.OpenRead(file), name); + public void AppendFile(string file, string? name = null) + { + // Opened per verification rather than held open from here, for the same reason, + // and so the handle is not held for the lifetime of the settings. + Guards.FileExists(file); + var extension = Path.GetExtension(file); + extension = extension.Length == 0 ? "noextension" : extension[1..]; + AppendFile(() => IoHelpers.OpenRead(file), extension, name ?? Path.GetFileNameWithoutExtension(file)); + } public void AppendFile(FileInfo file, string? name = null) => AppendFile(file.FullName, name); @@ -73,6 +88,12 @@ public void AppendFile(FileInfo file, string? name = null) => public void AppendFile(FileStream stream, string? name = null) => AppendFile(stream, stream.Extension(), name ?? Path.GetFileNameWithoutExtension(stream.Name)); + /// + /// The stream is owned by the caller and can only be read once, so unlike the other + /// overloads this one cannot be replayed for a second verification with the same + /// settings. Use or + /// where that matters. + /// public void AppendFile(Stream stream, string extension = "txt", string? name = null) { stream.MoveToStart(); @@ -80,11 +101,28 @@ public void AppendFile(Stream stream, string extension = "txt", string? name = n if (FileExtensions.IsTextExtension(extension)) { using var reader = new StreamReader(stream, Encoding.UTF8); - appendedFiles.Add(new(extension, reader.ReadToEnd(), name)); + var text = reader.ReadToEnd(); + appendedFiles.Add(() => new(extension, text, name)); + } + else + { + appendedFiles.Add(() => new(extension, stream, name)); + } + } + + void AppendFile(Func openStream, string extension, string? name) + { + appendedFiles ??= []; + if (FileExtensions.IsTextExtension(extension)) + { + using var stream = openStream(); + using var reader = new StreamReader(stream, Encoding.UTF8); + var text = reader.ReadToEnd(); + appendedFiles.Add(() => new(extension, text, name)); } else { - appendedFiles.Add(new(extension, stream, name)); + appendedFiles.Add(() => new(extension, openStream(), name)); } } }