diff --git a/docs/mdsource/doc-index.include.md b/docs/mdsource/doc-index.include.md index 50e487c51..34428ced8 100644 --- a/docs/mdsource/doc-index.include.md +++ b/docs/mdsource/doc-index.include.md @@ -41,6 +41,7 @@ * [Recording](/docs/recording.md) * [Explicit Targets](/docs/explicit-targets.md) * [TempDirectory](/docs/temp-directory.md) + * [TempFile](/docs/temp-file.md) * [FSharp Usage](/docs/fsharp.md) * [Compared to ApprovalTests](/docs/compared-to-approvaltests.md) * [Plugins](/docs/plugins.md) \ No newline at end of file diff --git a/docs/mdsource/temp-file.source.md b/docs/mdsource/temp-file.source.md index ff5e75292..7a2c4bfcf 100644 --- a/docs/mdsource/temp-file.source.md +++ b/docs/mdsource/temp-file.source.md @@ -98,6 +98,58 @@ Result: snippet: TempFileTests.Scrubbing.verified.txt +### Create Method + +Creates a new temporary file with optional extension and encoding. + +snippet: TempFileCreate + + +#### With Extension + +Create a temporary file with a specific extension: + +snippet: TempFileCreateWithExtension + + +#### With Encoding + +Create a temporary file with a specific text encoding and BOM: + +snippet: TempFileCreateWithEncoding + + +### CreateText Method + +Creates a new temporary file with text content asynchronously. + +snippet: TempFileCreateText + + +#### With Extension + +snippet: TempFileCreateTextWithExtension + + +#### With Encoding + +Create a text file with specific encoding: + +snippet: TempFileCreateTextWithEncoding + + +### CreateBinary Method + +Creates a new temporary file with binary content asynchronously. + +snippet: TempFileCreateBinary + + +#### With Extension + +snippet: TempFileCreateBinaryWithExtension + + ### Debugging Given `TempFile` deletes the file on test completion (even failure), it can be difficult to debug what caused the failure. diff --git a/docs/readme.md b/docs/readme.md index c41978779..dbf211f4c 100644 --- a/docs/readme.md +++ b/docs/readme.md @@ -50,6 +50,7 @@ To change this file edit the source file and then run MarkdownSnippets. * [Recording](/docs/recording.md) * [Explicit Targets](/docs/explicit-targets.md) * [TempDirectory](/docs/temp-directory.md) + * [TempFile](/docs/temp-file.md) * [FSharp Usage](/docs/fsharp.md) * [Compared to ApprovalTests](/docs/compared-to-approvaltests.md) * [Plugins](/docs/plugins.md) diff --git a/docs/temp-file.md b/docs/temp-file.md index e60b8ba65..a5bad7648 100644 --- a/docs/temp-file.md +++ b/docs/temp-file.md @@ -91,7 +91,7 @@ public void StringConversion() Trace.WriteLine(content); } ``` -snippet source | anchor +snippet source | anchor @@ -114,7 +114,7 @@ public void FileInfoConversion() Trace.WriteLine(directoryName); } ``` -snippet source | anchor +snippet source | anchor @@ -135,7 +135,7 @@ public void InfoProperty() Trace.WriteLine(directoryName); } ``` -snippet source | anchor +snippet source | anchor @@ -224,6 +224,139 @@ Result: +### Create Method + +Creates a new temporary file with optional extension and encoding. + + + +```cs +using var temp = TempFile.Create(); + +File.WriteAllText(temp, "content"); + +// file automatically deleted here +``` +snippet source | anchor + + + +#### With Extension + +Create a temporary file with a specific extension: + + + +```cs +using var temp = TempFile.Create(".txt"); + +File.WriteAllText(temp, "content"); +``` +snippet source | anchor + + + +#### With Encoding + +Create a temporary file with a specific text encoding and BOM: + + + +```cs +using var temp = TempFile.Create(".txt", Encoding.UTF8); + +File.Exists(temp.Path); +``` +snippet source | anchor + + + +### CreateText Method + +Creates a new temporary file with text content asynchronously. + + + +```cs +using var temp = await TempFile.CreateText("Hello, World!"); + +var content = await File.ReadAllTextAsync(temp); +Assert.Equal("Hello, World!", content); +``` +snippet source | anchor + + + +#### With Extension + + + +```cs +var json = """ + { + "name": "test", + "value": 123 + } + """; + +using var temp = await TempFile.CreateText(json, ".json"); + +var content = await File.ReadAllTextAsync(temp); +Assert.Equal(json, content); +``` +snippet source | anchor + + + +#### With Encoding + +Create a text file with specific encoding: + + + +```cs +using var temp = await TempFile.CreateText( + "Content with special chars: äöü", + ".txt", + Encoding.UTF8); + +var content = await File.ReadAllTextAsync(temp, Encoding.UTF8); +Assert.Equal("Content with special chars: äöü", content); +``` +snippet source | anchor + + + +### CreateBinary Method + +Creates a new temporary file with binary content asynchronously. + + + +```cs +byte[] data = [0x01, 0x02, 0x03, 0x04]; + +using var temp = await TempFile.CreateBinary(data); + +var readData = await File.ReadAllBytesAsync(temp); +Assert.Equal(data, readData); +``` +snippet source | anchor + + + +#### With Extension + + + +```cs +byte[] data = [0x01, 0x02, 0x03, 0x04]; +using var temp = await TempFile.CreateBinary(data, ".bin"); +``` +snippet source | anchor + + + ### Debugging Given `TempFile` deletes the file on test completion (even failure), it can be difficult to debug what caused the failure. diff --git a/readme.md b/readme.md index 7cd6a70ca..250e35b08 100644 --- a/readme.md +++ b/readme.md @@ -1112,6 +1112,7 @@ To opt out of this feature, include the following in the project file: * [Recording](/docs/recording.md) * [Explicit Targets](/docs/explicit-targets.md) * [TempDirectory](/docs/temp-directory.md) + * [TempFile](/docs/temp-file.md) * [FSharp Usage](/docs/fsharp.md) * [Compared to ApprovalTests](/docs/compared-to-approvaltests.md) * [Plugins](/docs/plugins.md) diff --git a/src/Verify.Tests/TempFileTests.CreateBinary_CanBeVerified.verified.bin b/src/Verify.Tests/TempFileTests.CreateBinary_CanBeVerified.verified.bin new file mode 100644 index 000000000..177e962b3 --- /dev/null +++ b/src/Verify.Tests/TempFileTests.CreateBinary_CanBeVerified.verified.bin @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/Verify.Tests/TempFileTests.CreateText_CanBeVerified.verified.txt b/src/Verify.Tests/TempFileTests.CreateText_CanBeVerified.verified.txt new file mode 100644 index 000000000..2f28b19bc --- /dev/null +++ b/src/Verify.Tests/TempFileTests.CreateText_CanBeVerified.verified.txt @@ -0,0 +1 @@ +test content \ No newline at end of file diff --git a/src/Verify.Tests/TempFileTests.cs b/src/Verify.Tests/TempFileTests.cs index 50708e6d6..0003f07ec 100644 --- a/src/Verify.Tests/TempFileTests.cs +++ b/src/Verify.Tests/TempFileTests.cs @@ -160,6 +160,17 @@ public void Create_WithEncoding_CreatesFileWithBom() Assert.True(bytes.Length >= 3); } + [Fact] + public void CreateText_WithEncoding_CreatesFileWithBom() + { + using var temp = TempFile.Create(".txt", Encoding.UTF8); + + Assert.True(File.Exists(temp.Path)); + var bytes = File.ReadAllBytes(temp.Path); + // UTF8 BOM should be present + Assert.True(bytes.Length >= 3); + } + [Fact] public void Dispose_DeletesFile() { @@ -380,4 +391,344 @@ public void InfoProperty() [Fact] public void Constructor_WithEmptyExtension_ThrowsArgumentException() => Assert.Throws(() => new TempFile(string.Empty)); + + [Fact] + public async Task CreateText_WithContent_CreatesFileWithContent() + { + var content = "Hello, World!"; + + using var temp = await TempFile.CreateText(content); + + Assert.True(File.Exists(temp.Path)); + var readContent = await File.ReadAllTextAsync(temp.Path); + Assert.Equal(content, readContent); + } + + [Fact] + public async Task CreateText_WithExtension_CreatesFileWithExtensionAndContent() + { + var content = "Test content"; + + using var temp = await TempFile.CreateText(content, ".txt"); + + Assert.EndsWith(".txt", temp.Path); + Assert.True(File.Exists(temp.Path)); + var readContent = await File.ReadAllTextAsync(temp.Path); + Assert.Equal(content, readContent); + } + + [Fact] + public async Task CreateText_WithEncoding_CreatesFileWithSpecifiedEncoding() + { + var content = "Test with special characters: äöü"; + + using var temp = await TempFile.CreateText(content, ".txt", Encoding.UTF8); + + Assert.True(File.Exists(temp.Path)); + var readContent = await File.ReadAllTextAsync(temp.Path, Encoding.UTF8); + Assert.Equal(content, readContent); + } + + [Fact] + public async Task CreateText_WithAsciiEncoding_CreatesFileWithAsciiEncoding() + { + var content = "ASCII content only"; + + using var temp = await TempFile.CreateText(content, ".txt", Encoding.ASCII); + + Assert.True(File.Exists(temp.Path)); + var readContent = await File.ReadAllTextAsync(temp.Path, Encoding.ASCII); + Assert.Equal(content, readContent); + } + + [Fact] + public async Task CreateText_WithEmptyContent_CreatesEmptyFile() + { + using var temp = await TempFile.CreateText(string.Empty, ".txt"); + + Assert.True(File.Exists(temp.Path)); + var readContent = await File.ReadAllTextAsync(temp.Path); + Assert.Empty(readContent); + } + + [Fact] + public async Task CreateText_WithMultilineContent_PreservesLineBreaks() + { + var content = """ + Line 1 + Line 2 + Line 3 + """; + + using var temp = await TempFile.CreateText(content, ".txt"); + + var readContent = await File.ReadAllTextAsync(temp.Path); + Assert.Equal(content, readContent); + } + + [Fact] + public async Task CreateText_DisposesCorrectly() + { + string path; + { + using var temp = await TempFile.CreateText("content", ".txt"); + path = temp.Path; + Assert.True(File.Exists(path)); + } + + Assert.False(File.Exists(path)); + } + + [Fact] + public async Task CreateBinary_WithContent_CreatesFileWithBinaryContent() + { + var data = "Hello"u8.ToArray(); // "Hello" in ASCII + + using var temp = await TempFile.CreateBinary(data); + + Assert.True(File.Exists(temp.Path)); + var readData = await File.ReadAllBytesAsync(temp.Path); + Assert.Equal(data, readData); + } + + [Fact] + public async Task CreateBinary_WithExtension_CreatesFileWithExtensionAndContent() + { + byte[] data = [0x01, 0x02, 0x03, 0x04]; + + using var temp = await TempFile.CreateBinary(data, ".bin"); + + Assert.EndsWith(".bin", temp.Path); + Assert.True(File.Exists(temp.Path)); + var readData = await File.ReadAllBytesAsync(temp.Path); + Assert.Equal(data, readData); + } + + [Fact] + public async Task CreateBinary_WithEmptyContent_CreatesEmptyFile() + { + using var temp = await TempFile.CreateBinary(ReadOnlyMemory.Empty, ".bin"); + + Assert.True(File.Exists(temp.Path)); + var readData = await File.ReadAllBytesAsync(temp.Path); + Assert.Empty(readData); + } + + [Fact] + public async Task CreateBinary_WithLargeBinaryData_WritesCorrectly() + { + var data = new byte[1024]; + Random.Shared.NextBytes(data); + + using var temp = await TempFile.CreateBinary(data, ".dat"); + + Assert.True(File.Exists(temp.Path)); + var readData = await File.ReadAllBytesAsync(temp.Path); + Assert.Equal(data, readData); + Assert.Equal(1024, readData.Length); + } + + [Fact] + public async Task CreateBinary_DisposesCorrectly() + { + string path; + byte[] data = [0xFF, 0xFE, 0xFD]; + + { + using var temp = await TempFile.CreateBinary(data, ".bin"); + path = temp.Path; + Assert.True(File.Exists(path)); + } + + Assert.False(File.Exists(path)); + } + + [Fact] + public async Task CreateBinary_WithImageData_CreatesValidFile() + { + // Simulate a simple 1x1 PNG (minimal valid PNG) + byte[] pngData = + [ + 0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A, // PNG signature + 0x00, 0x00, 0x00, 0x0D, 0x49, 0x48, 0x44, 0x52 // IHDR chunk start + ]; + + using var temp = await TempFile.CreateBinary(pngData, ".png"); + + Assert.EndsWith(".png", temp.Path); + var readData = await File.ReadAllBytesAsync(temp.Path); + Assert.Equal(pngData, readData); + } + + [Fact] + public async Task CreateText_CanBeVerified() + { + using var temp = await TempFile.CreateText("test content", ".txt"); + + var content = await File.ReadAllTextAsync(temp.Path); + await Verify(content); + } + + [Fact] + public async Task CreateBinary_CanBeVerified() + { + byte[] data = [0x01, 0x02, 0x03, 0x04, 0x05]; + + using var temp = await TempFile.CreateBinary(data, ".bin"); + + var readData = await File.ReadAllBytesAsync(temp.Path); + await Verify(readData); + } + + [Fact] + public async Task CreateText_WithJsonContent_CreatesValidJsonFile() + { + var jsonContent = """ + { + "name": "test", + "value": 123 + } + """; + + using var temp = await TempFile.CreateText(jsonContent, ".json"); + + Assert.EndsWith(".json", temp.Path); + var readContent = await File.ReadAllTextAsync(temp.Path); + Assert.Equal(jsonContent, readContent); + } + + [Fact] + public async Task CreateText_WithXmlContent_CreatesValidXmlFile() + { + var xmlContent = """ + + + value + + """; + + using var temp = await TempFile.CreateText(xmlContent, ".xml"); + + Assert.EndsWith(".xml", temp.Path); + var readContent = await File.ReadAllTextAsync(temp.Path); + Assert.Equal(xmlContent, readContent); + } + + [Fact] + public void Create() + { + #region TempFileCreate + + using var temp = TempFile.Create(); + + File.WriteAllText(temp, "content"); + + // file automatically deleted here + + #endregion + } + + + [Fact] + public void CreateWithExtension() + { + #region TempFileCreateWithExtension + + using var temp = TempFile.Create(".txt"); + + File.WriteAllText(temp, "content"); + + #endregion + } + + + + [Fact] + public void CreateWithEncoding() + { + #region TempFileCreateWithEncoding + + using var temp = TempFile.Create(".txt", Encoding.UTF8); + + File.Exists(temp.Path); + + #endregion + } + + [Fact] + public async Task CreateText() + { + #region TempFileCreateText + + using var temp = await TempFile.CreateText("Hello, World!"); + + var content = await File.ReadAllTextAsync(temp); + Assert.Equal("Hello, World!", content); + + #endregion + } + + [Fact] + public async Task CreateTextWithExtension() + { + #region TempFileCreateTextWithExtension + + var json = """ + { + "name": "test", + "value": 123 + } + """; + + using var temp = await TempFile.CreateText(json, ".json"); + + var content = await File.ReadAllTextAsync(temp); + Assert.Equal(json, content); + + #endregion + } + + [Fact] + public async Task CreateTextWithEncoding() + { + #region TempFileCreateTextWithEncoding + + using var temp = await TempFile.CreateText( + "Content with special chars: äöü", + ".txt", + Encoding.UTF8); + + var content = await File.ReadAllTextAsync(temp, Encoding.UTF8); + Assert.Equal("Content with special chars: äöü", content); + + #endregion + } + + + + [Fact] + public async Task CreateBinary() + { + #region TempFileCreateBinary + + byte[] data = [0x01, 0x02, 0x03, 0x04]; + + using var temp = await TempFile.CreateBinary(data); + + var readData = await File.ReadAllBytesAsync(temp); + Assert.Equal(data, readData); + + #endregion + } + + [Fact] + public async Task CreateBinaryWithExtension() + { + #region TempFileCreateBinaryWithExtension + + byte[] data = [0x01, 0x02, 0x03, 0x04]; + using var temp = await TempFile.CreateBinary(data, ".bin"); + + #endregion + } } \ No newline at end of file diff --git a/src/Verify/TempFile.cs b/src/Verify/TempFile.cs index 33d9f5644..c6dc7305e 100644 --- a/src/Verify/TempFile.cs +++ b/src/Verify/TempFile.cs @@ -146,6 +146,43 @@ public TempFile(string? extension = null) } } + /// + /// Creates a new temporary file with optional extension and encoding. + /// + /// + /// Optional file extension (e.g., ".txt", ".json"). + /// If not provided, no extension is added. + /// The extension should include the leading dot. + /// + /// + /// Optional text encoding to use when creating the file. + /// If provided and the extension is recognized as a text format, the file will be created with the appropriate BOM. + /// If null, the file is created as an empty binary file. + /// + /// + /// A new instance representing the created file. + /// The caller is responsible for disposing the instance to ensure cleanup. + /// + /// + /// The file is created immediately upon calling this method. If encoding is specified and the extension + /// is recognized as a text format, the file will be initialized with the appropriate byte order mark (BOM). + /// Otherwise, an empty file is created. + /// + /// + /// Thrown if the file cannot be created (e.g., due to permissions or disk space). + /// + /// + /// + /// // Create empty temp file + /// using var temp = TempFile.Create(); + /// + /// // Create temp file with extension + /// using var txtFile = TempFile.Create(".txt"); + /// + /// // Create temp file with UTF-8 encoding and BOM + /// using var utf8File = TempFile.Create(".txt", Encoding.UTF8); + /// + /// public static TempFile Create(string? extension = null, Encoding? encoding = null) { var file = new TempFile(extension); @@ -159,6 +196,127 @@ public static TempFile Create(string? extension = null, Encoding? encoding = nul return file; } + /// + /// Creates a new temporary file with the specified text content. + /// + /// + /// The text content to write to the file. + /// + /// + /// Optional file extension (e.g., ".txt", ".json", ".xml"). + /// If not provided, no extension is added. + /// The extension should include the leading dot. + /// + /// + /// Optional text encoding to use when writing the content. + /// If null, the default UTF-8 encoding without BOM is used. + /// + /// + /// A task that represents the asynchronous operation. + /// The task result contains a new instance with the written content. + /// The caller is responsible for disposing the instance to ensure cleanup. + /// + /// + /// + /// The file is created and written asynchronously. The content is written using the specified + /// encoding, or UTF-8 without BOM if no encoding is specified. + /// + /// + /// This method is ideal for creating temporary test data files, configuration files, or + /// any text-based content that needs to be written to disk for testing purposes. + /// + /// + /// + /// Thrown if the file cannot be created or written to (e.g., due to permissions or disk space). + /// + /// + /// + /// // Create temp file with simple text + /// using var temp = await TempFile.CreateText("Hello, World!"); + /// + /// // Create JSON file + /// using var json = await TempFile.CreateText( + /// "{\"name\": \"test\"}", + /// ".json"); + /// + /// // Create file with specific encoding + /// using var utf8 = await TempFile.CreateText( + /// "Content with special chars: äöü", + /// ".txt", + /// Encoding.UTF8); + /// + /// + public static async Task CreateText(string content, string? extension = null, Encoding? encoding = null) + { + var file = new TempFile(extension); + + if (encoding == null) + { + await File.WriteAllTextAsync(file, content); + } + else + { + await File.WriteAllTextAsync(file, content, encoding); + } + + return file; + } + + /// + /// Creates a new temporary file with the specified binary content. + /// + /// + /// The binary content to write to the file. + /// Can be empty to create an empty file. + /// + /// + /// Optional file extension (e.g., ".bin", ".dat", ".png"). + /// If not provided, no extension is added. + /// The extension should include the leading dot. + /// + /// + /// A task that represents the asynchronous operation. + /// The task result contains a new instance with the written content. + /// The caller is responsible for disposing the instance to ensure cleanup. + /// + /// + /// + /// The file is created and written asynchronously with the exact binary content provided. + /// This method is suitable for creating temporary files containing binary data such as + /// images, serialized objects, or any non-text data. + /// + /// + /// The content is written as-is without any encoding or transformation. + /// + /// + /// + /// Thrown if the file cannot be created or written to (e.g., due to permissions or disk space). + /// + /// + /// + /// // Create temp file with binary data + /// byte[] data = [0x01, 0x02, 0x03, 0x04]; + /// using var temp = await TempFile.CreateBinary(data); + /// + /// // Create image file + /// byte[] imageData = await File.ReadAllBytesAsync("source.png"); + /// using var image = await TempFile.CreateBinary(imageData, ".png"); + /// + /// // Create empty binary file + /// using var empty = await TempFile.CreateBinary( + /// ReadOnlyMemory<byte>.Empty, + /// ".bin"); + /// + /// + public static async Task CreateBinary(ReadOnlyMemory content, string? extension = null) + { + var file = new TempFile(extension); + + await File.WriteAllBytesAsync(file, content); + + return file; + } + public void Dispose() { if (File.Exists(Path))