Skip to content

Commit

Permalink
Add File.Append/WriteAllText/Bytes{Async} overloads for span/memory (#…
Browse files Browse the repository at this point in the history
…103308)

* Add File.Append/WriteAllText/Bytes{Async} overloads for span/memory

* Fix faulty assert
  • Loading branch information
stephentoub authored Jun 14, 2024
1 parent b02bc31 commit 37d1a8e
Show file tree
Hide file tree
Showing 11 changed files with 490 additions and 73 deletions.
337 changes: 282 additions & 55 deletions src/libraries/System.Private.CoreLib/src/System/IO/File.cs

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -279,7 +279,7 @@ private static unsafe (SafeFileHandle.OverlappedValueTaskSource? vts, int errorC
try
{
NativeOverlapped* nativeOverlapped = vts.PrepareForOperation(buffer, fileOffset, strategy);
Debug.Assert(vts._memoryHandle.Pointer != null);
Debug.Assert(vts._memoryHandle.Pointer != null || buffer.IsEmpty);

// Queue an async ReadFile operation.
if (Interop.Kernel32.ReadFile(handle, (byte*)vts._memoryHandle.Pointer, buffer.Length, IntPtr.Zero, nativeOverlapped) == 0)
Expand Down Expand Up @@ -372,7 +372,7 @@ private static unsafe (SafeFileHandle.OverlappedValueTaskSource? vts, int errorC
try
{
NativeOverlapped* nativeOverlapped = vts.PrepareForOperation(buffer, fileOffset, strategy);
Debug.Assert(vts._memoryHandle.Pointer != null);
Debug.Assert(vts._memoryHandle.Pointer != null || buffer.IsEmpty);

// Queue an async WriteFile operation.
if (Interop.Kernel32.WriteFile(handle, (byte*)vts._memoryHandle.Pointer, buffer.Length, IntPtr.Zero, nativeOverlapped) == 0)
Expand Down
12 changes: 12 additions & 0 deletions src/libraries/System.Runtime/ref/System.Runtime.cs
Original file line number Diff line number Diff line change
Expand Up @@ -10089,15 +10089,21 @@ public EnumerationOptions() { }
public static partial class File
{
public static void AppendAllBytes(string path, byte[] bytes) { }
public static void AppendAllBytes(string path, System.ReadOnlySpan<byte> bytes) { }
public static System.Threading.Tasks.Task AppendAllBytesAsync(string path, byte[] bytes, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task AppendAllBytesAsync(string path, System.ReadOnlyMemory<byte> bytes, System.Threading.CancellationToken cancellationToken = default) { throw null; }
public static void AppendAllLines(string path, System.Collections.Generic.IEnumerable<string> contents) { }
public static void AppendAllLines(string path, System.Collections.Generic.IEnumerable<string> contents, System.Text.Encoding encoding) { }
public static System.Threading.Tasks.Task AppendAllLinesAsync(string path, System.Collections.Generic.IEnumerable<string> contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task AppendAllLinesAsync(string path, System.Collections.Generic.IEnumerable<string> contents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static void AppendAllText(string path, string? contents) { }
public static void AppendAllText(string path, string? contents, System.Text.Encoding encoding) { }
public static void AppendAllText(string path, System.ReadOnlySpan<char> contents) { }
public static void AppendAllText(string path, System.ReadOnlySpan<char> contents, System.Text.Encoding encoding) { }
public static System.Threading.Tasks.Task AppendAllTextAsync(string path, string? contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task AppendAllTextAsync(string path, string? contents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task AppendAllTextAsync(string path, System.ReadOnlyMemory<char> contents, System.Threading.CancellationToken cancellationToken = default) { throw null; }
public static System.Threading.Tasks.Task AppendAllTextAsync(string path, System.ReadOnlyMemory<char> contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default) { throw null; }
public static System.IO.StreamWriter AppendText(string path) { throw null; }
public static void Copy(string sourceFileName, string destFileName) { }
public static void Copy(string sourceFileName, string destFileName, bool overwrite) { }
Expand Down Expand Up @@ -10176,7 +10182,9 @@ public static void SetUnixFileMode(Microsoft.Win32.SafeHandles.SafeFileHandle fi
[System.Runtime.Versioning.UnsupportedOSPlatformAttribute("windows")]
public static void SetUnixFileMode(string path, System.IO.UnixFileMode mode) { }
public static void WriteAllBytes(string path, byte[] bytes) { }
public static void WriteAllBytes(string path, System.ReadOnlySpan<byte> bytes) { }
public static System.Threading.Tasks.Task WriteAllBytesAsync(string path, byte[] bytes, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAllBytesAsync(string path, System.ReadOnlyMemory<byte> bytes, System.Threading.CancellationToken cancellationToken = default) { throw null; }
public static void WriteAllLines(string path, System.Collections.Generic.IEnumerable<string> contents) { }
public static void WriteAllLines(string path, System.Collections.Generic.IEnumerable<string> contents, System.Text.Encoding encoding) { }
public static void WriteAllLines(string path, string[] contents) { }
Expand All @@ -10185,8 +10193,12 @@ public static void WriteAllLines(string path, string[] contents, System.Text.Enc
public static System.Threading.Tasks.Task WriteAllLinesAsync(string path, System.Collections.Generic.IEnumerable<string> contents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static void WriteAllText(string path, string? contents) { }
public static void WriteAllText(string path, string? contents, System.Text.Encoding encoding) { }
public static void WriteAllText(string path, System.ReadOnlySpan<char> contents) { }
public static void WriteAllText(string path, System.ReadOnlySpan<char> contents, System.Text.Encoding encoding) { }
public static System.Threading.Tasks.Task WriteAllTextAsync(string path, string? contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAllTextAsync(string path, string? contents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; }
public static System.Threading.Tasks.Task WriteAllTextAsync(string path, System.ReadOnlyMemory<char> contents, System.Threading.CancellationToken cancellationToken = default) { throw null; }
public static System.Threading.Tasks.Task WriteAllTextAsync(string path, System.ReadOnlyMemory<char> contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default) { throw null; }
}
[System.FlagsAttribute]
public enum FileAccess
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,21 @@ protected override void Write(string path, string content, Encoding encoding)
}
}

public class File_AppendAllText_Span : File_ReadWriteAllText
{
protected override bool IsAppend => true;

protected override void Write(string path, string content)
{
File.AppendAllText(path, content.AsSpan());
}

protected override void Write(string path, string content, Encoding encoding)
{
File.AppendAllText(path, content.AsSpan(), encoding);
}
}

public class File_AppendAllText_Encoded : File_AppendAllText
{
protected override void Write(string path, string content)
Expand All @@ -54,6 +69,20 @@ public void NullEncoding()
}
}

public class File_AppendAllText_Span_Encoded : File_AppendAllText
{
protected override void Write(string path, string content)
{
File.AppendAllText(path, content.AsSpan(), new UTF8Encoding(false));
}

[Fact]
public void NullEncoding()
{
Assert.Throws<ArgumentNullException>(() => File.AppendAllText(GetTestFilePath(), "Text".AsSpan(), null));
}
}

public class File_AppendAllLines : File_ReadWriteAllLines_Enumerable
{
protected override bool IsAppend => true;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System.IO.Tests;
using System.Linq;
using System.Text;
using Xunit;
Expand All @@ -10,26 +9,28 @@ namespace System.IO.Tests
{
public class File_AppendAllBytes : FileSystemTest
{

[Fact]
public void NullParameters()
{
string path = GetTestFilePath();

Assert.Throws<ArgumentNullException>(() => File.AppendAllBytes(null, new byte[0]));
Assert.Throws<ArgumentNullException>(() => File.AppendAllBytes(null, ReadOnlySpan<byte>.Empty));
Assert.Throws<ArgumentNullException>(() => File.AppendAllBytes(path, null));
}

[Fact]
public void NonExistentPath()
{
Assert.Throws<DirectoryNotFoundException>(() => File.AppendAllBytes(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), new byte[0]));
Assert.Throws<DirectoryNotFoundException>(() => File.AppendAllBytes(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), ReadOnlySpan<byte>.Empty));
}

[Fact]
public void InvalidParameters()
{
Assert.Throws<ArgumentException>(() => File.AppendAllBytes(string.Empty, new byte[0]));
Assert.Throws<ArgumentException>(() => File.AppendAllBytes(string.Empty, ReadOnlySpan<byte>.Empty));
}


Expand All @@ -43,10 +44,11 @@ public void AppendAllBytes_WithValidInput_AppendsBytes()

File.WriteAllBytes(path, initialBytes);
File.AppendAllBytes(path, additionalBytes);
File.AppendAllBytes(path, additionalBytes.AsSpan());

byte[] result = File.ReadAllBytes(path);

byte[] expectedBytes = initialBytes.Concat(additionalBytes).ToArray();
byte[] expectedBytes = initialBytes.Concat(additionalBytes).Concat(additionalBytes).ToArray();

Assert.True(result.SequenceEqual(expectedBytes));
}
Expand All @@ -58,6 +60,7 @@ public void EmptyContentCreatesFile()
string path = GetTestFilePath();
Assert.False(File.Exists(path));
File.AppendAllBytes(path, new byte[0]);
File.AppendAllBytes(path, ReadOnlySpan<byte>.Empty);
Assert.True(File.Exists(path));
Assert.Empty(File.ReadAllBytes(path));
}
Expand All @@ -71,6 +74,7 @@ public void OpenFile_ThrowsIOException()
using (File.Create(path))
{
Assert.Throws<IOException>(() => File.AppendAllBytes(path, bytes));
Assert.Throws<IOException>(() => File.AppendAllBytes(path, bytes.AsSpan()));
}
}

Expand All @@ -97,6 +101,7 @@ public void AppendToReadOnlyFileAsync()
else
{
Assert.Throws<UnauthorizedAccessException>(() => File.AppendAllBytes(path, dataToAppend));
Assert.Throws<UnauthorizedAccessException>(() => File.AppendAllBytes(path, dataToAppend.AsSpan()));
}
}
finally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,19 +19,22 @@ public async Task NullParametersAsync()
string path = GetTestFilePath();

await Assert.ThrowsAsync<ArgumentNullException>("path", async () => await File.AppendAllBytesAsync(null, new byte[0]));
await Assert.ThrowsAsync<ArgumentNullException>("path", async () => await File.AppendAllBytesAsync(null, ReadOnlyMemory<byte>.Empty));
await Assert.ThrowsAsync<ArgumentNullException>("bytes", async () => await File.AppendAllBytesAsync(path, null));
}

[Fact]
public void NonExistentPathAsync()
{
Assert.ThrowsAsync<DirectoryNotFoundException>(() => File.AppendAllBytesAsync(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), new byte[0]));
Assert.ThrowsAsync<DirectoryNotFoundException>(() => File.AppendAllBytesAsync(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), ReadOnlyMemory<byte>.Empty));
}

[Fact]
public async Task InvalidParametersAsync()
{
await Assert.ThrowsAsync<ArgumentException>("path", async () => await File.AppendAllBytesAsync(string.Empty, new byte[0]));
await Assert.ThrowsAsync<ArgumentException>("path", async () => await File.AppendAllBytesAsync(string.Empty, ReadOnlyMemory<byte>.Empty));
}

[Fact]
Expand All @@ -44,10 +47,11 @@ public async Task AppendAllBytesAsync_WithValidInput_AppendsBytes()

await File.WriteAllBytesAsync(path, initialBytes);
await File.AppendAllBytesAsync(path, additionalBytes);
await File.AppendAllBytesAsync(path, additionalBytes.AsMemory());

byte[] result = await File.ReadAllBytesAsync(path);

byte[] expectedBytes = initialBytes.Concat(additionalBytes).ToArray();
byte[] expectedBytes = initialBytes.Concat(additionalBytes).Concat(additionalBytes).ToArray();

Assert.True(result.SequenceEqual(expectedBytes));
}
Expand All @@ -57,6 +61,7 @@ public async Task EmptyContentCreatesFileAsync()
{
string path = GetTestFilePath();
await File.AppendAllBytesAsync(path, new byte[0]);
await File.AppendAllBytesAsync(path, ReadOnlyMemory<byte>.Empty);
Assert.True(File.Exists(path));
Assert.Empty(await File.ReadAllBytesAsync(path));
}
Expand All @@ -70,6 +75,7 @@ public async Task OpenFile_ThrowsIOExceptionAsync()
using (File.Create(path))
{
await Assert.ThrowsAsync<IOException>(async () => await File.AppendAllBytesAsync(path, bytes));
await Assert.ThrowsAsync<IOException>(async () => await File.AppendAllBytesAsync(path, bytes.AsMemory()));
}
}

Expand All @@ -91,11 +97,13 @@ public async Task AppendToReadOnlyFileAsync()
if (PlatformDetection.IsNotWindows && PlatformDetection.IsPrivilegedProcess)
{
await File.AppendAllBytesAsync(path, dataToAppend);
Assert.Equal(dataToAppend, await File.ReadAllBytesAsync(path));
await File.AppendAllBytesAsync(path, dataToAppend.AsMemory());
Assert.Equal(dataToAppend.Concat(dataToAppend), await File.ReadAllBytesAsync(path));
}
else
{
await Assert.ThrowsAsync<UnauthorizedAccessException>(async () => await File.AppendAllBytesAsync(path, dataToAppend));
await Assert.ThrowsAsync<UnauthorizedAccessException>(async () => await File.AppendAllBytesAsync(path, dataToAppend.AsMemory()));
}
}
finally
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,26 @@ public override Task TaskAlreadyCanceledAsync()
}
}

public class File_AppendAllTextAsync_Memory : File_ReadWriteAllTextAsync
{
protected override bool IsAppend => true;

protected override Task WriteAsync(string path, string content) => File.AppendAllTextAsync(path, content.AsMemory());

protected override Task WriteAsync(string path, string content, Encoding encoding) => File.AppendAllTextAsync(path, content.AsMemory(), encoding);

[Fact]
public override Task TaskAlreadyCanceledAsync()
{
string path = GetTestFilePath();
CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;
source.Cancel();
Assert.True(File.AppendAllTextAsync(path, "".AsMemory(), token).IsCanceled);
return Assert.ThrowsAsync<TaskCanceledException>(async () => await File.AppendAllTextAsync(path, "".AsMemory(), token));
}
}

public class File_AppendAllTextAsync_Encoded : File_AppendAllTextAsync
{
protected override Task WriteAsync(string path, string content) =>
Expand All @@ -51,6 +71,29 @@ public override Task TaskAlreadyCanceledAsync()
}
}

public class File_AppendAllTextAsync_Memory_Encoded : File_AppendAllTextAsync
{
protected override Task WriteAsync(string path, string content) =>
File.AppendAllTextAsync(path, content.AsMemory(), new UTF8Encoding(false));

[Fact]
public Task NullEncodingAsync() => Assert.ThrowsAsync<ArgumentNullException>(
"encoding",
async () => await File.AppendAllTextAsync(GetTestFilePath(), "Text".AsMemory(), null));

[Fact]
public override Task TaskAlreadyCanceledAsync()
{
string path = GetTestFilePath();
CancellationTokenSource source = new CancellationTokenSource();
CancellationToken token = source.Token;
source.Cancel();
Assert.True(File.AppendAllTextAsync(path, "".AsMemory(), Encoding.UTF8, token).IsCanceled);
return Assert.ThrowsAsync<TaskCanceledException>(
async () => await File.AppendAllTextAsync(path, "".AsMemory(), Encoding.UTF8, token));
}
}

public class File_AppendAllLinesAsync : File_ReadWriteAllLines_EnumerableAsync
{
protected override bool IsAppend => true;
Expand Down
Loading

0 comments on commit 37d1a8e

Please sign in to comment.