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 0000000000..b54820aa60 Binary files /dev/null and b/src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_first#sample.verified.png differ 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 0000000000..b54820aa60 Binary files /dev/null and b/src/Verify.Tests/Converters/InstanceFileAppenderTests.AppendFileSettingsReuse_second#sample.verified.png differ 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 d9a286e724..4369d7786d 100644 --- a/src/Verify/Guards.cs +++ b/src/Verify/Guards.cs @@ -28,6 +28,19 @@ public static void BadParametersText(string value, [CallerArgumentExpression(nam } } + /// + /// 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)); } } }