From dddacdf9220be58f6ebb81a679e66fa915105cdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Valentin=20Breu=C3=9F?= Date: Sat, 6 Sep 2025 17:36:49 +0200 Subject: [PATCH] fix: use UTF8 encoding without preamble when no encoding is specified in File write or append methods --- .../FileSystem/FileMock.cs | 238 +++++++++++------- .../File/AppendAllLinesAsyncTests.cs | 42 +++- .../FileSystem/File/AppendAllLinesTests.cs | 26 ++ .../File/AppendAllTextAsyncTests.cs | 81 ++++-- .../FileSystem/File/AppendAllTextTests.cs | 33 ++- .../File/WriteAllLinesAsyncTests.cs | 41 ++- .../FileSystem/File/WriteAllLinesTests.cs | 26 ++ .../FileSystem/File/WriteAllTextAsyncTests.cs | 35 ++- .../FileSystem/File/WriteAllTextTests.cs | 46 +++- 9 files changed, 425 insertions(+), 143 deletions(-) diff --git a/Source/Testably.Abstractions.Testing/FileSystem/FileMock.cs b/Source/Testably.Abstractions.Testing/FileSystem/FileMock.cs index 4a3be755b..7e9bb1ae5 100644 --- a/Source/Testably.Abstractions.Testing/FileSystem/FileMock.cs +++ b/Source/Testably.Abstractions.Testing/FileSystem/FileMock.cs @@ -107,7 +107,12 @@ public void AppendAllLines(string path, IEnumerable contents) .File.RegisterMethod(nameof(AppendAllLines), path, contents); - AppendAllLines(path, contents, Encoding.Default); + _ = contents ?? throw new ArgumentNullException(nameof(contents)); + WriteText( + path, + contents.Aggregate(string.Empty, (a, b) => a + b + Environment.NewLine), + null, + true); } /// @@ -122,22 +127,30 @@ public void AppendAllLines( _ = contents ?? throw new ArgumentNullException(nameof(contents)); _ = encoding ?? throw new ArgumentNullException(nameof(encoding)); - AppendAllText( + WriteText( path, contents.Aggregate(string.Empty, (a, b) => a + b + Environment.NewLine), - encoding); + encoding, + true); } #if FEATURE_FILESYSTEM_ASYNC /// - public async Task AppendAllLinesAsync(string path, IEnumerable contents, + public Task AppendAllLinesAsync(string path, IEnumerable contents, CancellationToken cancellationToken = default) { using IDisposable registration = _fileSystem.StatisticsRegistration .File.RegisterMethod(nameof(AppendAllLinesAsync), path, contents, cancellationToken); - await AppendAllLinesAsync(path, contents, Encoding.Default, cancellationToken); + _ = contents ?? throw new ArgumentNullException(nameof(contents)); + ThrowIfCancelled(cancellationToken); + WriteText( + path, + contents.Aggregate(string.Empty, (a, b) => a + b + Environment.NewLine), + null, + true); + return Task.CompletedTask; } #endif @@ -151,8 +164,14 @@ public Task AppendAllLinesAsync(string path, IEnumerable contents, .File.RegisterMethod(nameof(AppendAllLinesAsync), path, contents, encoding, cancellationToken); + _ = contents ?? throw new ArgumentNullException(nameof(contents)); + _ = encoding ?? throw new ArgumentNullException(nameof(encoding)); ThrowIfCancelled(cancellationToken); - AppendAllLines(path, contents, encoding); + WriteText( + path, + contents.Aggregate(string.Empty, (a, b) => a + b + Environment.NewLine), + encoding, + true); return Task.CompletedTask; } #endif @@ -164,7 +183,11 @@ public void AppendAllText(string path, string? contents) .File.RegisterMethod(nameof(AppendAllText), path, contents); - AppendAllText(path, contents, Encoding.Default); + WriteText( + path, + contents, + null, + true); } /// @@ -174,31 +197,11 @@ public void AppendAllText(string path, string? contents, Encoding encoding) .File.RegisterMethod(nameof(AppendAllText), path, contents, encoding); - IStorageContainer container = - _fileSystem.Storage.GetOrCreateContainer( - _fileSystem.Storage.GetLocation( - path.EnsureValidFormat(_fileSystem)), - InMemoryContainer.NewFile); - - if (container.Type != FileSystemTypes.File) - { - throw ExceptionFactory.AccessToPathDenied(path); - } - - if (contents != null) - { - using (container.RequestAccess( - FileAccess.ReadWrite, - FileStreamFactoryMock.DefaultShare)) - { - if (container.GetBytes().Length == 0) - { - container.WriteBytes(encoding.GetPreamble()); - } - - container.AppendBytes(encoding.GetBytes(contents)); - } - } + WriteText( + path, + contents, + encoding, + true); } #if FEATURE_FILE_SPAN @@ -209,7 +212,11 @@ public void AppendAllText(string path, ReadOnlySpan contents) .File.RegisterMethod(nameof(AppendAllText), path, contents); - AppendAllText(path, contents.ToString(), Encoding.Default); + WriteText( + path, + contents.ToString(), + null, + true); } #endif @@ -221,20 +228,30 @@ public void AppendAllText(string path, ReadOnlySpan contents, Encoding enc .File.RegisterMethod(nameof(AppendAllText), path, contents, encoding); - AppendAllText(path, contents.ToString(), encoding); + WriteText( + path, + contents.ToString(), + encoding, + true); } #endif #if FEATURE_FILESYSTEM_ASYNC /// - public async Task AppendAllTextAsync(string path, string? contents, + public Task AppendAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default) { using IDisposable registration = _fileSystem.StatisticsRegistration .File.RegisterMethod(nameof(AppendAllTextAsync), path, contents, cancellationToken); - await AppendAllTextAsync(path, contents, Encoding.Default, cancellationToken); + ThrowIfCancelled(cancellationToken); + WriteText( + path, + contents, + null, + true); + return Task.CompletedTask; } #endif @@ -248,7 +265,11 @@ public Task AppendAllTextAsync(string path, string? contents, Encoding encoding, path, contents, encoding, cancellationToken); ThrowIfCancelled(cancellationToken); - AppendAllText(path, contents, encoding); + WriteText( + path, + contents, + encoding, + true); return Task.CompletedTask; } #endif @@ -262,7 +283,11 @@ public Task AppendAllTextAsync(string path, ReadOnlyMemory contents, Cance path, contents, cancellationToken); ThrowIfCancelled(cancellationToken); - AppendAllText(path, contents.ToString(), Encoding.Default); + WriteText( + path, + contents.ToString(), + null, + true); return Task.CompletedTask; } #endif @@ -277,7 +302,11 @@ public Task AppendAllTextAsync(string path, ReadOnlyMemory contents, Encod path, contents, encoding, cancellationToken); ThrowIfCancelled(cancellationToken); - AppendAllText(path, contents.ToString(), encoding); + WriteText( + path, + contents.ToString(), + encoding, + true); return Task.CompletedTask; } #endif @@ -881,7 +910,7 @@ public string[] ReadAllLines(string path) .File.RegisterMethod(nameof(ReadAllLines), path); - return ReadAllLines(path, Encoding.Default); + return ReadAllLines(path, Encoding.UTF8); } /// @@ -904,7 +933,7 @@ public async Task ReadAllLinesAsync( .File.RegisterMethod(nameof(ReadAllLinesAsync), path, cancellationToken); - return await ReadAllLinesAsync(path, Encoding.Default, cancellationToken); + return await ReadAllLinesAsync(path, Encoding.UTF8, cancellationToken); } #endif @@ -931,7 +960,7 @@ public string ReadAllText(string path) .File.RegisterMethod(nameof(ReadAllText), path); - return ReadAllText(path, Encoding.Default); + return ReadAllText(path, Encoding.UTF8); } /// @@ -969,7 +998,7 @@ public async Task ReadAllTextAsync( .File.RegisterMethod(nameof(ReadAllTextAsync), path, cancellationToken); - return await ReadAllTextAsync(path, Encoding.Default, cancellationToken); + return await ReadAllTextAsync(path, Encoding.UTF8, cancellationToken); } #endif @@ -996,7 +1025,7 @@ public IEnumerable ReadLines(string path) .File.RegisterMethod(nameof(ReadLines), path); - return ReadLines(path, Encoding.Default); + return ReadLines(path, Encoding.UTF8); } /// @@ -1411,7 +1440,9 @@ public void WriteAllLines(string path, string[] contents) .File.RegisterMethod(nameof(WriteAllLines), path, contents); - WriteAllLines(path, contents, Encoding.Default); + WriteText(path, + contents.Aggregate(string.Empty, (a, b) => a + b + Environment.NewLine), + null); } /// @@ -1421,7 +1452,9 @@ public void WriteAllLines(string path, IEnumerable contents) .File.RegisterMethod(nameof(WriteAllLines), path, contents); - WriteAllLines(path, contents, Encoding.Default); + WriteText(path, + contents.Aggregate(string.Empty, (a, b) => a + b + Environment.NewLine), + null); } /// @@ -1434,7 +1467,9 @@ public void WriteAllLines( .File.RegisterMethod(nameof(WriteAllLines), path, contents, encoding); - WriteAllLines(path, contents.AsEnumerable(), encoding); + WriteText(path, + contents.Aggregate(string.Empty, (a, b) => a + b + Environment.NewLine), + encoding); } /// @@ -1447,15 +1482,14 @@ public void WriteAllLines( .File.RegisterMethod(nameof(WriteAllLines), path, contents, encoding); - WriteAllText( - path, + WriteText(path, contents.Aggregate(string.Empty, (a, b) => a + b + Environment.NewLine), encoding); } #if FEATURE_FILESYSTEM_ASYNC /// - public async Task WriteAllLinesAsync( + public Task WriteAllLinesAsync( string path, IEnumerable contents, CancellationToken cancellationToken = default) @@ -1464,7 +1498,11 @@ public async Task WriteAllLinesAsync( .File.RegisterMethod(nameof(WriteAllLinesAsync), path, contents, cancellationToken); - await WriteAllLinesAsync(path, contents, Encoding.Default, cancellationToken); + ThrowIfCancelled(cancellationToken); + WriteText(path, + contents.Aggregate(string.Empty, (a, b) => a + b + Environment.NewLine), + null); + return Task.CompletedTask; } #endif @@ -1481,7 +1519,9 @@ public Task WriteAllLinesAsync( path, contents, encoding, cancellationToken); ThrowIfCancelled(cancellationToken); - WriteAllLines(path, contents, encoding); + WriteText(path, + contents.Aggregate(string.Empty, (a, b) => a + b + Environment.NewLine), + encoding); return Task.CompletedTask; } #endif @@ -1493,7 +1533,7 @@ public void WriteAllText(string path, string? contents) .File.RegisterMethod(nameof(WriteAllText), path, contents); - WriteAllText(path, contents, Encoding.Default); + WriteText(path, contents, null); } /// @@ -1503,37 +1543,7 @@ public void WriteAllText(string path, string? contents, Encoding encoding) .File.RegisterMethod(nameof(WriteAllText), path, contents, encoding); - IStorageContainer container = - _fileSystem.Storage.GetOrCreateContainer( - _fileSystem.Storage.GetLocation( - path.EnsureValidFormat(_fileSystem)), - InMemoryContainer.NewFile); - if (container is NullContainer) - { - return; - } - - if (container.Type != FileSystemTypes.File) - { - throw ExceptionFactory.AccessToPathDenied(path); - } - - if (contents != null) - { - if (_fileSystem.Execute.IsWindows && - container.Attributes.HasFlag(FileAttributes.Hidden)) - { - throw ExceptionFactory.AccessToPathDenied(); - } - - using (container.RequestAccess( - FileAccess.Write, - FileStreamFactoryMock.DefaultShare)) - { - container.WriteBytes(encoding.GetPreamble()); - container.AppendBytes(encoding.GetBytes(contents)); - } - } + WriteText(path, contents, encoding); } #if FEATURE_FILE_SPAN @@ -1544,7 +1554,7 @@ public void WriteAllText(string path, ReadOnlySpan contents) .File.RegisterMethod(nameof(WriteAllText), path, contents); - WriteAllText(path, contents.ToString(), Encoding.Default); + WriteText(path, contents.ToString(), null); } #endif @@ -1557,20 +1567,22 @@ public void WriteAllText(string path, ReadOnlySpan contents, Encoding enco .File.RegisterMethod(nameof(WriteAllText), path, contents, encoding); - WriteAllText(path, contents.ToString(), encoding); + WriteText(path, contents.ToString(), encoding); } #endif #if FEATURE_FILESYSTEM_ASYNC /// - public async Task WriteAllTextAsync(string path, string? contents, + public Task WriteAllTextAsync(string path, string? contents, CancellationToken cancellationToken = default) { using IDisposable registration = _fileSystem.StatisticsRegistration .File.RegisterMethod(nameof(WriteAllTextAsync), path, contents, cancellationToken); - await WriteAllTextAsync(path, contents, Encoding.Default, cancellationToken); + ThrowIfCancelled(cancellationToken); + WriteText(path, contents, null); + return Task.CompletedTask; } #endif @@ -1584,7 +1596,7 @@ public Task WriteAllTextAsync(string path, string? contents, Encoding encoding, path, contents, encoding, cancellationToken); ThrowIfCancelled(cancellationToken); - WriteAllText(path, contents, encoding); + WriteText(path, contents, encoding); return Task.CompletedTask; } #endif @@ -1598,7 +1610,7 @@ public Task WriteAllTextAsync(string path, ReadOnlyMemory contents, Cancel path, contents, cancellationToken); ThrowIfCancelled(cancellationToken); - WriteAllText(path, contents.ToString(), Encoding.Default); + WriteText(path, contents.ToString(), null); return Task.CompletedTask; } #endif @@ -1613,7 +1625,7 @@ public Task WriteAllTextAsync(string path, ReadOnlyMemory contents, Encodi path, contents, encoding, cancellationToken); ThrowIfCancelled(cancellationToken); - WriteAllText(path, contents.ToString(), encoding); + WriteText(path, contents.ToString(), encoding); return Task.CompletedTask; } #endif @@ -1674,6 +1686,50 @@ private IStorageContainer GetContainerFromSafeFileHandle(SafeFileHandle fileHand return container; } #endif + + private void WriteText(string path, string? contents, Encoding? encoding, bool append = false) + { + IStorageContainer container = + _fileSystem.Storage.GetOrCreateContainer( + _fileSystem.Storage.GetLocation( + path.EnsureValidFormat(_fileSystem)), + InMemoryContainer.NewFile); + if (container is NullContainer && !append) + { + return; + } + + if (container.Type != FileSystemTypes.File) + { + throw ExceptionFactory.AccessToPathDenied(path); + } + + if (contents != null) + { + if (!append && + _fileSystem.Execute.IsWindows && + container.Attributes.HasFlag(FileAttributes.Hidden)) + { + throw ExceptionFactory.AccessToPathDenied(); + } + + using (container.RequestAccess( + append ? FileAccess.ReadWrite : FileAccess.Write, + FileStreamFactoryMock.DefaultShare)) + { + if (encoding is not null && (!append || container.GetBytes().Length == 0)) + { + container.WriteBytes(encoding.GetPreamble()); + } + else if (!append) + { + container.WriteBytes([]); + } + encoding ??= Encoding.UTF8; + container.AppendBytes(encoding.GetBytes(contents)); + } + } + } #if FEATURE_FILESYSTEM_ASYNC private static void ThrowIfCancelled(CancellationToken cancellationToken) diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllLinesAsyncTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllLinesAsyncTests.cs index 58e1e59c9..bce6e4a3a 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllLinesAsyncTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllLinesAsyncTests.cs @@ -5,7 +5,6 @@ using System.Linq; using System.Text; using System.Threading; -using System.Threading.Tasks; namespace Testably.Abstractions.Tests.FileSystem.File; @@ -43,6 +42,20 @@ await FileSystem.File.AppendAllLinesAsync(path, contents, Encoding.UTF8, await That(Act).Throws().WithHResult(-2146233029); } + [Theory] + [AutoData] + public async Task AppendAllLinesAsync_Enumerable_WithoutEncoding_ShouldUseUtf8( + string path) + { + string[] contents = ["breuß"]; + + await FileSystem.File.AppendAllLinesAsync(path, contents.AsEnumerable(), + CancellationToken.None); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6 + Environment.NewLine.Length); + } + [Theory] [AutoData] public async Task AppendAllLinesAsync_ExistingFile_ShouldAppendLinesToFile( @@ -50,9 +63,11 @@ public async Task AppendAllLinesAsync_ExistingFile_ShouldAppendLinesToFile( { string expectedContent = string.Join(Environment.NewLine, previousContents.Concat(contents)) + Environment.NewLine; - await FileSystem.File.AppendAllLinesAsync(path, previousContents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllLinesAsync(path, previousContents, + TestContext.Current.CancellationToken); - await FileSystem.File.AppendAllLinesAsync(path, contents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllLinesAsync(path, contents, + TestContext.Current.CancellationToken); await That(FileSystem.File.Exists(path)).IsTrue(); await That(FileSystem.File.ReadAllText(path)).IsEqualTo(expectedContent); @@ -67,7 +82,8 @@ public async Task AppendAllLinesAsync_MissingDirectory_ShouldThrowDirectoryNotFo async Task Act() { - await FileSystem.File.AppendAllLinesAsync(filePath, contents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllLinesAsync(filePath, contents, + TestContext.Current.CancellationToken); } await That(Act).Throws().WithHResult(-2147024893); @@ -81,7 +97,8 @@ public async Task AppendAllLinesAsync_MissingFile_ShouldCreateFile( string expectedContent = string.Join(Environment.NewLine, contents) + Environment.NewLine; - await FileSystem.File.AppendAllLinesAsync(path, contents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllLinesAsync(path, contents, + TestContext.Current.CancellationToken); await That(FileSystem.File.Exists(path)).IsTrue(); await That(FileSystem.File.ReadAllText(path)).IsEqualTo(expectedContent); @@ -94,7 +111,8 @@ public async Task AppendAllLinesAsync_NullContent_ShouldThrowArgumentNullExcepti { async Task Act() { - await FileSystem.File.AppendAllLinesAsync(path, null!, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllLinesAsync(path, null!, + TestContext.Current.CancellationToken); } await That(Act).Throws() @@ -109,7 +127,8 @@ public async Task AppendAllLinesAsync_NullEncoding_ShouldThrowArgumentNullExcept { async Task Act() { - await FileSystem.File.AppendAllLinesAsync(path, [], null!, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllLinesAsync(path, [], null!, + TestContext.Current.CancellationToken); } await That(Act).Throws() @@ -124,7 +143,8 @@ public async Task AppendAllLinesAsync_ShouldEndWithNewline(string path) string[] contents = ["foo", "bar"]; string expectedResult = "foo" + Environment.NewLine + "bar" + Environment.NewLine; - await FileSystem.File.AppendAllLinesAsync(path, contents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllLinesAsync(path, contents, + TestContext.Current.CancellationToken); await That(FileSystem.File.Exists(path)).IsTrue(); await That(FileSystem.File.ReadAllText(path)).IsEqualTo(expectedResult); @@ -140,7 +160,8 @@ public async Task async Task Act() { - await FileSystem.File.AppendAllLinesAsync(path, contents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllLinesAsync(path, contents, + TestContext.Current.CancellationToken); } await That(Act).Throws().WithHResult(-2147024891); @@ -157,7 +178,8 @@ public async Task string path = new Fixture().Create(); string[] lines = new Fixture().Create(); lines[1] = specialLine; - await FileSystem.File.AppendAllLinesAsync(path, lines, writeEncoding, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllLinesAsync(path, lines, writeEncoding, + TestContext.Current.CancellationToken); string[] result = FileSystem.File.ReadAllLines(path, readEncoding); diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllLinesTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllLinesTests.cs index 83ad73b47..8f7069aaf 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllLinesTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllLinesTests.cs @@ -8,6 +8,19 @@ namespace Testably.Abstractions.Tests.FileSystem.File; [FileSystemTests] public partial class AppendAllLinesTests { + [Theory] + [AutoData] + public async Task AppendAllLines_Enumerable_WithoutEncoding_ShouldUseUtf8( + string path) + { + string[] contents = ["breuß"]; + + FileSystem.File.AppendAllLines(path, contents.AsEnumerable()); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6 + Environment.NewLine.Length); + } + [Theory] [AutoData] public async Task AppendAllLines_ExistingFile_ShouldAppendLinesToFile( @@ -112,4 +125,17 @@ public async Task AppendAllLines_WithDifferentEncoding_ShouldNotReturnWrittenTex await That(result).IsNotEqualTo(lines).InAnyOrder(); await That(result[0]).IsEqualTo(lines[0]); } + + [Theory] + [AutoData] + public async Task AppendAllLines_WithoutEncoding_ShouldUseUtf8( + string path) + { + string[] contents = ["breuß"]; + + FileSystem.File.AppendAllLines(path, contents); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6 + Environment.NewLine.Length); + } } diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllTextAsyncTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllTextAsyncTests.cs index d80e68762..b765c37f7 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllTextAsyncTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllTextAsyncTests.cs @@ -44,9 +44,11 @@ async Task Act() => public async Task AppendAllTextAsync_ExistingFile_ShouldAppendLinesToFile( string path, string previousContents, string contents) { - await FileSystem.File.AppendAllTextAsync(path, previousContents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(path, previousContents, + TestContext.Current.CancellationToken); - await FileSystem.File.AppendAllTextAsync(path, contents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(path, contents, + TestContext.Current.CancellationToken); await That(FileSystem.File.Exists(path)).IsTrue(); await That(FileSystem.File.ReadAllText(path)).IsEqualTo(previousContents + contents); @@ -61,7 +63,8 @@ public async Task AppendAllTextAsync_MissingDirectory_ShouldThrowDirectoryNotFou async Task Act() { - await FileSystem.File.AppendAllTextAsync(filePath, contents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(filePath, contents, + TestContext.Current.CancellationToken); } await That(Act).Throws().WithHResult(-2147024893); @@ -72,7 +75,8 @@ async Task Act() public async Task AppendAllTextAsync_MissingFile_ShouldCreateFile( string path, string contents) { - await FileSystem.File.AppendAllTextAsync(path, contents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(path, contents, + TestContext.Current.CancellationToken); await That(FileSystem.File.Exists(path)).IsTrue(); await That(FileSystem.File.ReadAllText(path)).IsEqualTo(contents); @@ -84,7 +88,8 @@ public async Task AppendAllTextAsync_ShouldNotEndWithNewline(string path) { string contents = "foo"; - await FileSystem.File.AppendAllTextAsync(path, contents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(path, contents, + TestContext.Current.CancellationToken); await That(FileSystem.File.Exists(path)).IsTrue(); await That(FileSystem.File.ReadAllText(path)).IsEqualTo(contents); @@ -100,7 +105,8 @@ public async Task async Task Act() { - await FileSystem.File.AppendAllTextAsync(path, contents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(path, contents, + TestContext.Current.CancellationToken); } await That(Act).Throws().WithHResult(-2147024891); @@ -114,13 +120,27 @@ public async Task AppendAllTextAsync_WithDifferentEncoding_ShouldNotReturnWritte string contents, Encoding writeEncoding, Encoding readEncoding) { string path = new Fixture().Create(); - await FileSystem.File.AppendAllTextAsync(path, contents, writeEncoding, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(path, contents, writeEncoding, + TestContext.Current.CancellationToken); string[] result = FileSystem.File.ReadAllLines(path, readEncoding); await That(result).IsNotEqualTo([contents]); } + [Theory] + [AutoData] + public async Task AppendAllTextAsync_WithoutEncoding_ShouldUseUtf8( + string path) + { + string contents = "breuß"; + + await FileSystem.File.AppendAllTextAsync(path, contents, CancellationToken.None); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6); + } + #if FEATURE_FILE_SPAN [Theory] [AutoData] @@ -146,7 +166,8 @@ public async Task cts.Cancel(); async Task Act() => - await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), Encoding.UTF8, cts.Token); + await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), Encoding.UTF8, + cts.Token); await That(Act).Throws().WithHResult(-2146233029); } @@ -156,9 +177,11 @@ async Task Act() => public async Task AppendAllTextAsync_ReadOnlyMemory_ExistingFile_ShouldAppendLinesToFile( string path, string previousContents, string contents) { - await FileSystem.File.AppendAllTextAsync(path, previousContents, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(path, previousContents, + TestContext.Current.CancellationToken); - await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), + TestContext.Current.CancellationToken); await That(FileSystem.File.Exists(path)).IsTrue(); await That(FileSystem.File.ReadAllText(path)).IsEqualTo(previousContents + contents); @@ -166,14 +189,16 @@ public async Task AppendAllTextAsync_ReadOnlyMemory_ExistingFile_ShouldAppendLin [Theory] [AutoData] - public async Task AppendAllTextAsync_ReadOnlyMemory_MissingDirectory_ShouldThrowDirectoryNotFoundException( - string missingPath, string fileName, string contents) + public async Task + AppendAllTextAsync_ReadOnlyMemory_MissingDirectory_ShouldThrowDirectoryNotFoundException( + string missingPath, string fileName, string contents) { string filePath = FileSystem.Path.Combine(missingPath, fileName); async Task Act() { - await FileSystem.File.AppendAllTextAsync(filePath, contents.AsMemory(), TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(filePath, contents.AsMemory(), + TestContext.Current.CancellationToken); } await That(Act).Throws().WithHResult(-2147024893); @@ -184,7 +209,8 @@ async Task Act() public async Task AppendAllTextAsync_ReadOnlyMemory_MissingFile_ShouldCreateFile( string path, string contents) { - await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), + TestContext.Current.CancellationToken); await That(FileSystem.File.Exists(path)).IsTrue(); await That(FileSystem.File.ReadAllText(path)).IsEqualTo(contents); @@ -196,7 +222,8 @@ public async Task AppendAllTextAsync_ReadOnlyMemory_ShouldNotEndWithNewline(stri { string contents = "foo"; - await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), + TestContext.Current.CancellationToken); await That(FileSystem.File.Exists(path)).IsTrue(); await That(FileSystem.File.ReadAllText(path)).IsEqualTo(contents); @@ -212,7 +239,8 @@ public async Task async Task Act() { - await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), + TestContext.Current.CancellationToken); } await That(Act).Throws().WithHResult(-2147024891); @@ -222,16 +250,31 @@ async Task Act() [Theory] [ClassData(typeof(TestDataGetEncodingDifference))] - public async Task AppendAllTextAsync_ReadOnlyMemory_WithDifferentEncoding_ShouldNotReturnWrittenText( - string contents, Encoding writeEncoding, Encoding readEncoding) + public async Task + AppendAllTextAsync_ReadOnlyMemory_WithDifferentEncoding_ShouldNotReturnWrittenText( + string contents, Encoding writeEncoding, Encoding readEncoding) { string path = new Fixture().Create(); - await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), writeEncoding, TestContext.Current.CancellationToken); + await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), writeEncoding, + TestContext.Current.CancellationToken); string[] result = FileSystem.File.ReadAllLines(path, readEncoding); await That(result).IsNotEqualTo([contents]); } + + [Theory] + [AutoData] + public async Task AppendAllTextAsync_ReadOnlyMemory_WithoutEncoding_ShouldUseUtf8( + string path) + { + string contents = "breuß"; + + await FileSystem.File.AppendAllTextAsync(path, contents.AsMemory(), CancellationToken.None); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6); + } #endif } #endif diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllTextTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllTextTests.cs index 9e530268c..f1422a764 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllTextTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/File/AppendAllTextTests.cs @@ -152,6 +152,19 @@ public async Task AppendAllText_WithDifferentEncoding_ShouldNotReturnWrittenText await That(result).IsNotEqualTo([contents]); } + [Theory] + [AutoData] + public async Task AppendAllText_WithoutEncoding_ShouldUseUtf8( + string path) + { + string contents = "breuß"; + + FileSystem.File.AppendAllText(path, contents); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6); + } + #if FEATURE_FILE_SPAN [Theory] [AutoData] @@ -172,6 +185,7 @@ public async Task AppendAllText_Span_MissingDirectory_ShouldThrowDirectoryNotFou string missingPath, string fileName, string contents) { string filePath = FileSystem.Path.Combine(missingPath, fileName); + void Act() { FileSystem.File.AppendAllText(filePath, contents.AsSpan()); @@ -224,12 +238,14 @@ public async Task AppendAllText_Span_ShouldAdjustTimes(string path, string conte if (Test.RunsOnWindows) { - await That(creationTime).IsBetween(creationTimeStart).And(creationTimeEnd).Within(TimeComparison.Tolerance); + await That(creationTime).IsBetween(creationTimeStart).And(creationTimeEnd) + .Within(TimeComparison.Tolerance); await That(lastAccessTime).IsOnOrAfter(updateTime.ApplySystemClockTolerance()); } else { - await That(lastAccessTime).IsBetween(creationTimeStart).And(creationTimeEnd).Within(TimeComparison.Tolerance); + await That(lastAccessTime).IsBetween(creationTimeStart).And(creationTimeEnd) + .Within(TimeComparison.Tolerance); } await That(lastWriteTime).IsOnOrAfter(updateTime.ApplySystemClockTolerance()); @@ -293,5 +309,18 @@ public async Task AppendAllText_Span_WithDifferentEncoding_ShouldNotReturnWritte await That(result).IsNotEqualTo([contents]); } + + [Theory] + [AutoData] + public async Task AppendAllText_Span_WithoutEncoding_ShouldUseUtf8( + string path) + { + string contents = "breuß"; + + FileSystem.File.AppendAllText(path, contents.AsSpan()); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6); + } #endif } diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllLinesAsyncTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllLinesAsyncTests.cs index 3e34f9c0e..e126ff593 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllLinesAsyncTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllLinesAsyncTests.cs @@ -3,7 +3,6 @@ using System.Linq; using System.Text; using System.Threading; -using System.Threading.Tasks; namespace Testably.Abstractions.Tests.FileSystem.File; @@ -78,10 +77,11 @@ public async Task { await FileSystem.File.WriteAllTextAsync(path, "foo", TestContext.Current.CancellationToken); - await FileSystem.File.WriteAllLinesAsync(path, contents.AsEnumerable(), TestContext.Current.CancellationToken); + await FileSystem.File.WriteAllLinesAsync(path, contents.AsEnumerable(), + TestContext.Current.CancellationToken); string[] result = - await FileSystem.File.ReadAllLinesAsync(path, TestContext.Current.CancellationToken); + await FileSystem.File.ReadAllLinesAsync(path, TestContext.Current.CancellationToken); await That(result).IsEqualTo(contents); } @@ -90,13 +90,28 @@ public async Task public async Task WriteAllLinesAsync_Enumerable_ShouldCreateFileWithText( string path, string[] contents) { - await FileSystem.File.WriteAllLinesAsync(path, contents.AsEnumerable(), TestContext.Current.CancellationToken); + await FileSystem.File.WriteAllLinesAsync(path, contents.AsEnumerable(), + TestContext.Current.CancellationToken); string[] result = - await FileSystem.File.ReadAllLinesAsync(path, TestContext.Current.CancellationToken); + await FileSystem.File.ReadAllLinesAsync(path, TestContext.Current.CancellationToken); await That(result).IsEqualTo(contents); } + [Theory] + [AutoData] + public async Task WriteAllLinesAsync_Enumerable_WithoutEncoding_ShouldUseUtf8( + string path) + { + string[] contents = ["breuß"]; + + await FileSystem.File.WriteAllLinesAsync(path, contents.AsEnumerable(), + CancellationToken.None); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6 + Environment.NewLine.Length); + } + [Theory] [AutoData] public async Task WriteAllLinesAsync_PreviousFile_ShouldOverwriteFileWithText( @@ -104,10 +119,11 @@ public async Task WriteAllLinesAsync_PreviousFile_ShouldOverwriteFileWithText( { await FileSystem.File.WriteAllTextAsync(path, "foo", TestContext.Current.CancellationToken); - await FileSystem.File.WriteAllLinesAsync(path, contents, TestContext.Current.CancellationToken); + await FileSystem.File.WriteAllLinesAsync(path, contents, + TestContext.Current.CancellationToken); string[] result = - await FileSystem.File.ReadAllLinesAsync(path, TestContext.Current.CancellationToken); + await FileSystem.File.ReadAllLinesAsync(path, TestContext.Current.CancellationToken); await That(result).IsEqualTo(contents); } @@ -116,10 +132,11 @@ public async Task WriteAllLinesAsync_PreviousFile_ShouldOverwriteFileWithText( public async Task WriteAllLinesAsync_ShouldCreateFileWithText( string path, string[] contents) { - await FileSystem.File.WriteAllLinesAsync(path, contents, TestContext.Current.CancellationToken); + await FileSystem.File.WriteAllLinesAsync(path, contents, + TestContext.Current.CancellationToken); string[] result = - await FileSystem.File.ReadAllLinesAsync(path, TestContext.Current.CancellationToken); + await FileSystem.File.ReadAllLinesAsync(path, TestContext.Current.CancellationToken); await That(result).IsEqualTo(contents); } @@ -133,7 +150,8 @@ public async Task async Task Act() { - await FileSystem.File.WriteAllLinesAsync(path, contents, TestContext.Current.CancellationToken); + await FileSystem.File.WriteAllLinesAsync(path, contents, + TestContext.Current.CancellationToken); } await That(Act).Throws().WithHResult(-2147024891); @@ -154,7 +172,8 @@ public async Task async Task Act() { - await FileSystem.File.WriteAllLinesAsync(path, contents, TestContext.Current.CancellationToken); + await FileSystem.File.WriteAllLinesAsync(path, contents, + TestContext.Current.CancellationToken); } await That(Act).Throws().WithHResult(-2147024891); diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllLinesTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllLinesTests.cs index 4ecfa6055..ad25fe8eb 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllLinesTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllLinesTests.cs @@ -42,6 +42,19 @@ public async Task WriteAllLines_Enumerable_WithEncoding_ShouldCreateFileWithText await That(result).IsEqualTo(contents); } + [Theory] + [AutoData] + public async Task WriteAllLines_Enumerable_WithoutEncoding_ShouldUseUtf8( + string path) + { + string[] contents = ["breuß"]; + + FileSystem.File.WriteAllLines(path, contents.AsEnumerable()); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6 + Environment.NewLine.Length); + } + [Theory] [AutoData] public async Task WriteAllLines_PreviousFile_ShouldOverwriteFileWithText( @@ -112,4 +125,17 @@ public async Task WriteAllLines_WithEncoding_ShouldCreateFileWithText( string[] result = FileSystem.File.ReadAllLines(path, encoding); await That(result).IsEqualTo(contents); } + + [Theory] + [AutoData] + public async Task WriteAllLines_WithoutEncoding_ShouldUseUtf8( + string path) + { + string[] contents = ["breuß"]; + + FileSystem.File.WriteAllLines(path, contents); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6 + Environment.NewLine.Length); + } } diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllTextAsyncTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllTextAsyncTests.cs index 4399be4f4..07aa8e76c 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllTextAsyncTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllTextAsyncTests.cs @@ -90,7 +90,8 @@ await FileSystem.File.WriteAllTextAsync(path, contents, string result = await FileSystem.File.ReadAllTextAsync(path, TestContext.Current.CancellationToken); - await That(result).IsEqualTo(contents).Because($"{contents} should be encoded and decoded identical."); + await That(result).IsEqualTo(contents) + .Because($"{contents} should be encoded and decoded identical."); } } @@ -146,6 +147,19 @@ await FileSystem.File.WriteAllTextAsync(path, contents, await That(Act).Throws().WithHResult(-2147024891); } + [Theory] + [AutoData] + public async Task WriteAllTextAsync_WithoutEncoding_ShouldUseUtf8( + string path) + { + string contents = "breuß"; + + await FileSystem.File.WriteAllTextAsync(path, contents, CancellationToken.None); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6); + } + #if FEATURE_FILE_SPAN [Theory] [AutoData] @@ -171,7 +185,8 @@ public async Task await cts.CancelAsync(); async Task Act() => - await FileSystem.File.WriteAllTextAsync(path, contents.AsMemory(), Encoding.UTF8, cts.Token); + await FileSystem.File.WriteAllTextAsync(path, contents.AsMemory(), Encoding.UTF8, + cts.Token); await That(Act).Throws().WithHResult(-2146233029); } @@ -229,7 +244,8 @@ await FileSystem.File.WriteAllTextAsync(path, contents.AsMemory(), string result = await FileSystem.File.ReadAllTextAsync(path, TestContext.Current.CancellationToken); - await That(result).IsEqualTo(contents).Because($"{contents} should be encoded and decoded identical."); + await That(result).IsEqualTo(contents) + .Because($"{contents} should be encoded and decoded identical."); } } @@ -271,6 +287,19 @@ await FileSystem.File.WriteAllTextAsync(path, contents.AsMemory(), await That(Act).Throws().WithHResult(-2147024891); } + + [Theory] + [AutoData] + public async Task WriteAllTextAsync_ReadOnlyMemory_WithoutEncoding_ShouldUseUtf8( + string path) + { + string contents = "breuß"; + + await FileSystem.File.WriteAllTextAsync(path, contents.AsMemory(), CancellationToken.None); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6); + } #endif } #endif diff --git a/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllTextTests.cs b/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllTextTests.cs index 323c11046..a19fac7c0 100644 --- a/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllTextTests.cs +++ b/Tests/Testably.Abstractions.Tests/FileSystem/File/WriteAllTextTests.cs @@ -196,6 +196,19 @@ await That(Act).Throws() .WithMessage($"Access to the path '{path}' is denied."); } + [Theory] + [AutoData] + public async Task WriteAllText_WithoutEncoding_ShouldUseUtf8( + string path) + { + string contents = "breuß"; + + FileSystem.File.WriteAllText(path, contents); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6); + } + #if FEATURE_FILE_SPAN [Theory] [AutoData] @@ -203,6 +216,7 @@ public async Task WriteAllText_Span_MissingDirectory_ShouldThrowDirectoryNotFoun string directory, string path) { string fullPath = FileSystem.Path.Combine(directory, path); + void Act() { FileSystem.File.WriteAllText(fullPath, "foo".AsSpan()); @@ -246,12 +260,14 @@ public async Task WriteAllText_Span_ShouldAdjustTimes(string path, string conten if (Test.RunsOnWindows) { - await That(creationTime).IsBetween(creationTimeStart).And(creationTimeEnd).Within(TimeComparison.Tolerance); + await That(creationTime).IsBetween(creationTimeStart).And(creationTimeEnd) + .Within(TimeComparison.Tolerance); await That(lastAccessTime).IsOnOrAfter(updateTime.ApplySystemClockTolerance()); } else { - await That(lastAccessTime).IsBetween(creationTimeStart).And(creationTimeEnd).Within(TimeComparison.Tolerance); + await That(lastAccessTime).IsBetween(creationTimeStart).And(creationTimeEnd) + .Within(TimeComparison.Tolerance); } await That(lastWriteTime).IsOnOrAfter(updateTime.ApplySystemClockTolerance()); @@ -301,14 +317,16 @@ public async Task WriteAllText_Span_SpecialCharacters_ShouldReturnSameText(strin string result = FileSystem.File.ReadAllText(path); - await That(result).IsEqualTo(contents).Because($"{contents} should be encoded and decoded identical."); + await That(result).IsEqualTo(contents) + .Because($"{contents} should be encoded and decoded identical."); } } [Theory] [AutoData] - public async Task WriteAllText_Span_WhenDirectoryWithSameNameExists_ShouldThrowUnauthorizedAccessException( - string path) + public async Task + WriteAllText_Span_WhenDirectoryWithSameNameExists_ShouldThrowUnauthorizedAccessException( + string path) { FileSystem.Directory.CreateDirectory(path); @@ -324,8 +342,9 @@ void Act() [Theory] [AutoData] - public async Task WriteAllText_Span_WhenFileIsHidden_ShouldThrowUnauthorizedAccessException_OnWindows( - string path, string contents) + public async Task + WriteAllText_Span_WhenFileIsHidden_ShouldThrowUnauthorizedAccessException_OnWindows( + string path, string contents) { Skip.IfNot(Test.RunsOnWindows); @@ -339,5 +358,18 @@ void Act() await That(Act).Throws().WithHResult(-2147024891); } + + [Theory] + [AutoData] + public async Task WriteAllText_Span_WithoutEncoding_ShouldUseUtf8( + string path) + { + string contents = "breuß"; + + FileSystem.File.WriteAllText(path, contents.AsSpan()); + + byte[] bytes = FileSystem.File.ReadAllBytes(path); + await That(bytes.Length).IsEqualTo(6); + } #endif }