From 369cc91cf2be6fb8640d80d0f10400e72b452d3e Mon Sep 17 00:00:00 2001 From: Jon Hanna Date: Sat, 21 Jan 2017 21:39:37 +0000 Subject: [PATCH] Add async File.ReadAll*, File.AppendAll* and File.WriteAll Fixes #11220 --- .../ref/System.IO.FileSystem.cs | 14 + .../src/System.IO.FileSystem.csproj | 2 + .../src/System/IO/File.cs | 357 +++++++++++++++++- .../tests/File/AppendAsync.cs | 132 +++++++ .../tests/File/ReadWriteAllBytesAsync.cs | 136 +++++++ .../tests/File/ReadWriteAllLinesAsync.cs | 186 +++++++++ .../tests/File/ReadWriteAllTextAsync.cs | 173 +++++++++ .../tests/System.IO.FileSystem.Tests.csproj | 8 +- 8 files changed, 1000 insertions(+), 8 deletions(-) create mode 100644 src/System.IO.FileSystem/tests/File/AppendAsync.cs create mode 100644 src/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs create mode 100644 src/System.IO.FileSystem/tests/File/ReadWriteAllLinesAsync.cs create mode 100644 src/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs diff --git a/src/System.IO.FileSystem/ref/System.IO.FileSystem.cs b/src/System.IO.FileSystem/ref/System.IO.FileSystem.cs index 0174cf0b4aec..e4ba63826d02 100644 --- a/src/System.IO.FileSystem/ref/System.IO.FileSystem.cs +++ b/src/System.IO.FileSystem/ref/System.IO.FileSystem.cs @@ -91,8 +91,12 @@ public static partial class File { public static void AppendAllLines(string path, System.Collections.Generic.IEnumerable contents) { } public static void AppendAllLines(string path, System.Collections.Generic.IEnumerable contents, System.Text.Encoding encoding) { } + public static System.Threading.Tasks.Task AppendAllLinesAsync(string path, System.Collections.Generic.IEnumerable contents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task AppendAllLinesAsync(string path, System.Collections.Generic.IEnumerable contents, System.Text.Encoding encoding, 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 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, string contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { 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) { } @@ -119,10 +123,15 @@ public static void Move(string sourceFileName, string destFileName) { } public static System.IO.StreamReader OpenText(string path) { throw null; } public static System.IO.FileStream OpenWrite(string path) { throw null; } public static byte[] ReadAllBytes(string path) { throw null; } + public static System.Threading.Tasks.Task ReadAllBytesAsync(string path, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static string[] ReadAllLines(string path) { throw null; } public static string[] ReadAllLines(string path, System.Text.Encoding encoding) { throw null; } + public static System.Threading.Tasks.Task ReadAllLinesAsync(string path, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task ReadAllLinesAsync(string path, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static string ReadAllText(string path) { throw null; } public static string ReadAllText(string path, System.Text.Encoding encoding) { throw null; } + public static System.Threading.Tasks.Task ReadAllTextAsync(string path, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task ReadAllTextAsync(string path, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } public static System.Collections.Generic.IEnumerable ReadLines(string path) { throw null; } public static System.Collections.Generic.IEnumerable ReadLines(string path, System.Text.Encoding encoding) { throw null; } public static void Replace(string sourceFileName, string destinationFileName, string destinationBackupFileName) { } @@ -135,12 +144,17 @@ public static void SetLastAccessTimeUtc(string path, System.DateTime lastAccessT public static void SetLastWriteTime(string path, System.DateTime lastWriteTime) { } public static void SetLastWriteTimeUtc(string path, System.DateTime lastWriteTimeUtc) { } public static void WriteAllBytes(string path, 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 void WriteAllLines(string path, string[] contents) { } public static void WriteAllLines(string path, System.Collections.Generic.IEnumerable contents) { } public static void WriteAllLines(string path, string[] contents, System.Text.Encoding encoding) { } public static void WriteAllLines(string path, System.Collections.Generic.IEnumerable contents, System.Text.Encoding encoding) { } + public static System.Threading.Tasks.Task WriteAllLinesAsync(string path, System.Collections.Generic.IEnumerable contents, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } + public static System.Threading.Tasks.Task WriteAllLinesAsync(string path, System.Collections.Generic.IEnumerable contents, System.Text.Encoding encoding, 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 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, string contents, System.Text.Encoding encoding, System.Threading.CancellationToken cancellationToken = default(System.Threading.CancellationToken)) { throw null; } } public sealed partial class FileInfo : System.IO.FileSystemInfo { diff --git a/src/System.IO.FileSystem/src/System.IO.FileSystem.csproj b/src/System.IO.FileSystem/src/System.IO.FileSystem.csproj index d48e83c160c6..06eb1c9b62bd 100644 --- a/src/System.IO.FileSystem/src/System.IO.FileSystem.csproj +++ b/src/System.IO.FileSystem/src/System.IO.FileSystem.csproj @@ -371,6 +371,7 @@ + @@ -379,6 +380,7 @@ + diff --git a/src/System.IO.FileSystem/src/System/IO/File.cs b/src/System.IO.FileSystem/src/System/IO/File.cs index aeecf75cd3f8..27b1b3268fb0 100644 --- a/src/System.IO.FileSystem/src/System/IO/File.cs +++ b/src/System.IO.FileSystem/src/System/IO/File.cs @@ -2,18 +2,14 @@ // The .NET Foundation licenses this file to you under the MIT license. // See the LICENSE file in the project root for more information. -using System; -using System.Runtime.InteropServices; +using System.Buffers; using System.Collections.Generic; using System.Diagnostics; using System.Diagnostics.Contracts; -using System.Globalization; -using System.Runtime.Versioning; using System.Security; using System.Text; using System.Threading; - -using Microsoft.Win32.SafeHandles; +using System.Threading.Tasks; namespace System.IO { @@ -21,6 +17,8 @@ namespace System.IO // routines such as Delete, etc. public static class File { + private static Encoding _UTF8NoBOM; + internal const int DefaultBufferSize = 4096; public static StreamReader OpenText(String path) @@ -725,5 +723,352 @@ public static void Decrypt(String path) throw new PlatformNotSupportedException(); } + + // UTF-8 without BOM and with error detection. Same as the default encoding for StreamWriter. + private static Encoding UTF8NoBOM => _UTF8NoBOM ?? (_UTF8NoBOM = new UTF8Encoding(encoderShouldEmitUTF8Identifier: false, throwOnInvalidBytes: true)); + + // If we use the path-taking constructors we will not have FileOptions.Asynchronous set and + // we will have asynchronous file access faked by the thread pool. We want the real thing. + private static StreamReader AsyncStreamReader(string path, Encoding encoding) + { + FileStream stream = new FileStream( + path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultBufferSize, + FileOptions.Asynchronous | FileOptions.SequentialScan); + + return new StreamReader(stream, encoding, detectEncodingFromByteOrderMarks: true); + } + + private static StreamWriter AsyncStreamWriter(string path, Encoding encoding, bool append) + { + FileStream stream = new FileStream( + path, append ? FileMode.Append : FileMode.Create, FileAccess.Write, FileShare.Read, DefaultBufferSize, + FileOptions.Asynchronous | FileOptions.SequentialScan); + + return new StreamWriter(stream, encoding); + } + + public static Task ReadAllTextAsync(string path, CancellationToken cancellationToken = default(CancellationToken)) + => ReadAllTextAsync(path, Encoding.UTF8, cancellationToken); + + public static Task ReadAllTextAsync(string path, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + if (path.Length == 0) + throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + + return cancellationToken.IsCancellationRequested + ? Task.FromCanceled(cancellationToken) + : InternalReadAllTextAsync(path, encoding, cancellationToken); + } + + private static async Task InternalReadAllTextAsync(string path, Encoding encoding, CancellationToken cancellationToken) + { + Debug.Assert(!string.IsNullOrEmpty(path)); + Debug.Assert(encoding != null); + + char[] buffer = null; + StringBuilder sb = null; + StreamReader sr = AsyncStreamReader(path, encoding); + try + { + cancellationToken.ThrowIfCancellationRequested(); + sb = StringBuilderCache.Acquire(); + buffer = ArrayPool.Shared.Rent(sr.CurrentEncoding.GetMaxCharCount(DefaultBufferSize)); + for (;;) + { + int read = await sr.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); + if (read == 0) + { + return sb.ToString(); + } + + sb.Append(buffer, 0, read); + cancellationToken.ThrowIfCancellationRequested(); + } + } + finally + { + sr.Dispose(); + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + + if (sb != null) + { + StringBuilderCache.Release(sb); + } + } + } + + public static Task WriteAllTextAsync(string path, string contents, CancellationToken cancellationToken = default(CancellationToken)) + => WriteAllTextAsync(path, contents, UTF8NoBOM, cancellationToken); + + public static Task WriteAllTextAsync(string path, string contents, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + if (path.Length == 0) + throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + if (string.IsNullOrEmpty(contents)) + { + new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read).Dispose(); + return Task.CompletedTask; + } + + return InternalWriteAllTextAsync(AsyncStreamWriter(path, encoding, append: false), contents, cancellationToken); + } + + public static Task ReadAllBytesAsync(string path, CancellationToken cancellationToken = default(CancellationToken)) + { + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + FileStream fs = new FileStream( + path, FileMode.Open, FileAccess.Read, FileShare.Read, DefaultBufferSize, + FileOptions.Asynchronous | FileOptions.SequentialScan); + + bool returningInternalTask = false; + try + { + long fileLength = fs.Length; + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + if (fileLength > int.MaxValue) + { + return Task.FromException(new IOException(SR.IO_FileTooLong2GB)); + } + + if (fileLength == 0) + { + return Task.FromResult(Array.Empty()); + } + + returningInternalTask = true; + return InternalReadAllBytesAsync(fs, (int)fileLength, cancellationToken); + } + finally + { + if (!returningInternalTask) + { + fs.Dispose(); + } + } + } + + private static async Task InternalReadAllBytesAsync(FileStream fs, int count, CancellationToken cancellationToken) + { + using (fs) + { + int index = 0; + byte[] bytes = new byte[count]; + do + { + int n = await fs.ReadAsync(bytes, index, count - index, cancellationToken).ConfigureAwait(false); + if (n == 0) + { + throw Error.GetEndOfFile(); + } + + index += n; + } while (index < count); + + return bytes; + } + } + + public static Task WriteAllBytesAsync(string path, byte[] bytes, CancellationToken cancellationToken = default(CancellationToken)) + { + if (path == null) + throw new ArgumentNullException(nameof(path), SR.ArgumentNull_Path); + if (path.Length == 0) + throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + if (bytes == null) + throw new ArgumentNullException(nameof(bytes)); + + return cancellationToken.IsCancellationRequested + ? Task.FromCanceled(cancellationToken) + : InternalWriteAllBytesAsync(path, bytes, cancellationToken); + } + + private static async Task InternalWriteAllBytesAsync(String path, byte[] bytes, CancellationToken cancellationToken) + { + Debug.Assert(!string.IsNullOrEmpty(path)); + Debug.Assert(bytes != null); + + using (FileStream fs = new FileStream(path, FileMode.Create, FileAccess.Write, FileShare.Read, DefaultBufferSize, FileOptions.Asynchronous | FileOptions.SequentialScan)) + { + await fs.WriteAsync(bytes, 0, bytes.Length, cancellationToken).ConfigureAwait(false); + await fs.FlushAsync(cancellationToken).ConfigureAwait(false); + } + } + + public static Task ReadAllLinesAsync(string path, CancellationToken cancellationToken = default(CancellationToken)) + => ReadAllLinesAsync(path, Encoding.UTF8, cancellationToken); + + public static Task ReadAllLinesAsync(string path, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + if (path.Length == 0) + throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + + return cancellationToken.IsCancellationRequested + ? Task.FromCanceled(cancellationToken) + : InternalReadAllLinesAsync(path, encoding, cancellationToken); + } + + private static async Task InternalReadAllLinesAsync(string path, Encoding encoding, CancellationToken cancellationToken) + { + Debug.Assert(!string.IsNullOrEmpty(path)); + Debug.Assert(encoding != null); + + using (StreamReader sr = AsyncStreamReader(path, encoding)) + { + cancellationToken.ThrowIfCancellationRequested(); + string line; + List lines = new List(); + while ((line = await sr.ReadLineAsync().ConfigureAwait(false)) != null) + { + lines.Add(line); + cancellationToken.ThrowIfCancellationRequested(); + } + + return lines.ToArray(); + } + } + + public static Task WriteAllLinesAsync(string path, IEnumerable contents, CancellationToken cancellationToken = default(CancellationToken)) + => WriteAllLinesAsync(path, contents, UTF8NoBOM, cancellationToken); + + public static Task WriteAllLinesAsync(string path, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + if (contents == null) + throw new ArgumentNullException(nameof(contents)); + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + if (path.Length == 0) + throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + + return cancellationToken.IsCancellationRequested + ? Task.FromCanceled(cancellationToken) + : InternalWriteAllLinesAsync(AsyncStreamWriter(path, encoding, append: false), contents, cancellationToken); + } + + private static async Task InternalWriteAllLinesAsync(TextWriter writer, IEnumerable contents, CancellationToken cancellationToken) + { + Debug.Assert(writer != null); + Debug.Assert(contents != null); + + using (writer) + { + foreach (string line in contents) + { + cancellationToken.ThrowIfCancellationRequested(); + // Note that this working depends on the fix to #8662, and cannot be ported without + // either also porting that fix, or explicitly checking for line being null. + await writer.WriteLineAsync(line).ConfigureAwait(false); + } + + cancellationToken.ThrowIfCancellationRequested(); + await writer.FlushAsync().ConfigureAwait(false); + } + } + + private static async Task InternalWriteAllTextAsync(StreamWriter sw, string contents, CancellationToken cancellationToken) + { + char[] buffer = null; + try + { + buffer = ArrayPool.Shared.Rent(DefaultBufferSize); + int count = contents.Length; + int index = 0; + while (index < count) + { + int batchSize = Math.Min(DefaultBufferSize, count); + contents.CopyTo(index, buffer, 0, batchSize); + cancellationToken.ThrowIfCancellationRequested(); + await sw.WriteAsync(buffer, 0, batchSize).ConfigureAwait(false); + index += batchSize; + } + + cancellationToken.ThrowIfCancellationRequested(); + await sw.FlushAsync().ConfigureAwait(false); + } + finally + { + sw.Dispose(); + if (buffer != null) + { + ArrayPool.Shared.Return(buffer); + } + } + } + + public static Task AppendAllTextAsync(string path, string contents, CancellationToken cancellationToken = default(CancellationToken)) + => AppendAllTextAsync(path, contents, UTF8NoBOM, cancellationToken); + + public static Task AppendAllTextAsync(string path, string contents, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + if (path.Length == 0) + throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + + if (cancellationToken.IsCancellationRequested) + { + return Task.FromCanceled(cancellationToken); + } + + if (string.IsNullOrEmpty(contents)) + { + // Just to throw exception if there is a problem opening the file. + new FileStream(path, FileMode.Append, FileAccess.Write, FileShare.Read).Dispose(); + return Task.CompletedTask; + } + + return InternalWriteAllTextAsync(AsyncStreamWriter(path, encoding, append: true), contents, cancellationToken); + } + + public static Task AppendAllLinesAsync(string path, IEnumerable contents, CancellationToken cancellationToken = default(CancellationToken)) + => AppendAllLinesAsync(path, contents, UTF8NoBOM, cancellationToken); + + public static Task AppendAllLinesAsync(string path, IEnumerable contents, Encoding encoding, CancellationToken cancellationToken = default(CancellationToken)) + { + if (path == null) + throw new ArgumentNullException(nameof(path)); + if (contents == null) + throw new ArgumentNullException(nameof(contents)); + if (encoding == null) + throw new ArgumentNullException(nameof(encoding)); + if (path.Length == 0) + throw new ArgumentException(SR.Argument_EmptyPath, nameof(path)); + + return cancellationToken.IsCancellationRequested + ? Task.FromCanceled(cancellationToken) + : InternalWriteAllLinesAsync(AsyncStreamWriter(path, encoding, append: true), contents, cancellationToken); + } } } diff --git a/src/System.IO.FileSystem/tests/File/AppendAsync.cs b/src/System.IO.FileSystem/tests/File/AppendAsync.cs new file mode 100644 index 000000000000..438d03b30039 --- /dev/null +++ b/src/System.IO.FileSystem/tests/File/AppendAsync.cs @@ -0,0 +1,132 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Tests +{ + public class File_AppendTextAsync : File_ReadWriteAllTextAsync + { + protected override async Task WriteAsync(string path, string content) + { + using (var writer = File.AppendText(path)) + { + await writer.WriteAsync(content); + } + } + + [Fact] + public async override Task OverwriteAsync() + { + string path = GetTestFilePath(); + string lines = new string('c', 200); + string appendLines = new string('b', 100); + await WriteAsync(path, lines); + await WriteAsync(path, appendLines); + Assert.Equal(lines + appendLines, await ReadAsync(path)); + } + } + + public class File_AppendAllTextAsync : File_ReadWriteAllTextAsync + { + protected override Task WriteAsync(string path, string content) => File.AppendAllTextAsync(path, content); + + [Fact] + public override async Task OverwriteAsync() + { + string path = GetTestFilePath(); + string lines = new string('c', 200); + string appendLines = new string('b', 100); + await WriteAsync(path, lines); + await WriteAsync(path, appendLines); + Assert.Equal(lines + appendLines, await ReadAsync(path)); + } + + [Fact] + public override Task TaskAlreadyCanceledAsync() + { + string path = GetTestFilePath(); + CancellationTokenSource source = new CancellationTokenSource(); + CancellationToken token = source.Token; + source.Cancel(); + Assert.True(File.AppendAllTextAsync(path, "", token).IsCanceled); + return Assert.ThrowsAsync(async () => await File.AppendAllTextAsync(path, "", token)); + } + } + + public class File_AppendAllTextAsync_Encoded : File_AppendAllTextAsync + { + protected override Task WriteAsync(string path, string content) => + File.AppendAllTextAsync(path, content, new UTF8Encoding(false)); + + [Fact] + public Task NullEncodingAsync() => Assert.ThrowsAsync( + async () => await File.AppendAllTextAsync(GetTestFilePath(), "Text", null)); + + [Fact] + public override Task TaskAlreadyCanceledAsync() + { + string path = GetTestFilePath(); + CancellationTokenSource source = new CancellationTokenSource(); + CancellationToken token = source.Token; + source.Cancel(); + Assert.True(File.AppendAllTextAsync(path, "", Encoding.UTF8, token).IsCanceled); + return Assert.ThrowsAsync( + async () => await File.AppendAllTextAsync(path, "", Encoding.UTF8, token)); + } + } + + public class File_AppendAllLinesAsync : File_ReadWriteAllLines_EnumerableAsync + { + protected override Task WriteAsync(string path, string[] content) => File.AppendAllLinesAsync(path, content); + + [Fact] + public override async Task OverwriteAsync() + { + string path = GetTestFilePath(); + string[] lines = new string[] { new string('c', 200) }; + string[] appendLines = new string[] { new string('b', 100) }; + await WriteAsync(path, lines); + await WriteAsync(path, appendLines); + Assert.Equal(new string[] { lines[0], appendLines[0] }, await ReadAsync(path)); + } + + [Fact] + public override Task TaskAlreadyCanceledAsync() + { + string path = GetTestFilePath(); + CancellationTokenSource source = new CancellationTokenSource(); + CancellationToken token = source.Token; + source.Cancel(); + Assert.True(File.AppendAllLinesAsync(path, new[] { "" }, token).IsCanceled); + return Assert.ThrowsAsync( + async () => await File.AppendAllLinesAsync(path, new[] { "" }, token)); + } + } + + public class File_AppendAllLinesAsync_Encoded : File_AppendAllLinesAsync + { + protected override Task WriteAsync(string path, string[] content) => + File.AppendAllLinesAsync(path, content, new UTF8Encoding(false)); + + [Fact] + public Task NullEncodingAsync() => Assert.ThrowsAsync( + async () => await File.AppendAllLinesAsync(GetTestFilePath(), new string[] { "Text" }, null)); + + [Fact] + public override Task TaskAlreadyCanceledAsync() + { + string path = GetTestFilePath(); + CancellationTokenSource source = new CancellationTokenSource(); + CancellationToken token = source.Token; + source.Cancel(); + Assert.True(File.AppendAllLinesAsync(path, new[] { "" }, Encoding.UTF8, token).IsCanceled); + return Assert.ThrowsAsync( + async () => await File.AppendAllLinesAsync(path, new[] { "" }, Encoding.UTF8, token)); + } + } +} diff --git a/src/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs b/src/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs new file mode 100644 index 000000000000..9750dc983ec7 --- /dev/null +++ b/src/System.IO.FileSystem/tests/File/ReadWriteAllBytesAsync.cs @@ -0,0 +1,136 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Tests +{ + public class File_ReadWriteAllBytesAsync : FileSystemTest + { + [Fact] + public async Task NullParametersAsync() + { + string path = GetTestFilePath(); + await Assert.ThrowsAsync(async () => await File.WriteAllBytesAsync(null, new byte[0])); + await Assert.ThrowsAsync(async () => await File.WriteAllBytesAsync(path, null)); + await Assert.ThrowsAsync(async () => await File.ReadAllBytesAsync(null)); + } + + [Fact] + public async Task InvalidParametersAsync() + { + string path = GetTestFilePath(); + await Assert.ThrowsAsync(async () => await File.WriteAllBytesAsync(string.Empty, new byte[0])); + await Assert.ThrowsAsync(async () => await File.ReadAllBytesAsync(string.Empty)); + } + + [Fact] + public Task Read_FileNotFoundAsync() + { + string path = GetTestFilePath(); + return Assert.ThrowsAsync(async () => await File.ReadAllBytesAsync(path)); + } + + [Fact] + public async Task EmptyContentCreatesFileAsync() + { + string path = GetTestFilePath(); + await File.WriteAllBytesAsync(path, new byte[0]); + Assert.True(File.Exists(path)); + Assert.Empty(await File.ReadAllTextAsync(path)); + File.Delete(path); + } + + [Theory] + [InlineData(0)] + [InlineData(100)] + public async Task ValidWriteAsync(int size) + { + string path = GetTestFilePath(); + byte[] buffer = Encoding.UTF8.GetBytes(new string('c', size)); + await File.WriteAllBytesAsync(path, buffer); + Assert.Equal(buffer, await File.ReadAllBytesAsync(path)); + File.Delete(path); + } + + public Task AlreadyCanceledAsync() + { + string path = GetTestFilePath(); + CancellationTokenSource source = new CancellationTokenSource(); + CancellationToken token = source.Token; + Assert.True(File.WriteAllBytesAsync(path, new byte[0], token).IsCanceled); + return Assert.ThrowsAsync( + async () => await File.WriteAllBytesAsync(path, new byte[0], token)); + } + + [Fact] + [OuterLoop] + public Task ReadFileOver2GBAsync() + { + string path = GetTestFilePath(); + using (FileStream fs = File.Create(path)) + { + fs.SetLength(int.MaxValue + 1L); + } + + // File is too large for ReadAllBytes at once + return Assert.ThrowsAsync(async () => await File.ReadAllBytesAsync(path)); + } + + [Fact] + public async Task OverwriteAsync() + { + string path = GetTestFilePath(); + byte[] bytes = Encoding.UTF8.GetBytes(new string('c', 100)); + byte[] overwriteBytes = Encoding.UTF8.GetBytes(new string('b', 50)); + await File.WriteAllBytesAsync(path, bytes); + await File.WriteAllBytesAsync(path, overwriteBytes); + Assert.Equal(overwriteBytes, await File.ReadAllBytesAsync(path)); + } + + [Fact] + public async Task OpenFile_ThrowsIOExceptionAsync() + { + string path = GetTestFilePath(); + byte[] bytes = Encoding.UTF8.GetBytes(new string('c', 100)); + using (File.Create(path)) + { + await Assert.ThrowsAsync(async () => await File.WriteAllBytesAsync(path, bytes)); + await Assert.ThrowsAsync(async () => await File.ReadAllBytesAsync(path)); + } + } + + /// + /// On Unix, modifying a file that is ReadOnly will fail under normal permissions. + /// If the test is being run under the superuser, however, modification of a ReadOnly + /// file is allowed. + /// + [Fact] + public async Task WriteToReadOnlyFileAsync() + { + string path = GetTestFilePath(); + File.Create(path).Dispose(); + File.SetAttributes(path, FileAttributes.ReadOnly); + try + { + // Operation succeeds when being run by the Unix superuser + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && geteuid() == 0) + { + await File.WriteAllBytesAsync(path, Encoding.UTF8.GetBytes("text")); + Assert.Equal(Encoding.UTF8.GetBytes("text"), await File.ReadAllBytesAsync(path)); + } + else + await Assert.ThrowsAsync(async () => await File.WriteAllBytesAsync(path, Encoding.UTF8.GetBytes("text"))); + } + finally + { + File.SetAttributes(path, FileAttributes.Normal); + } + } + } +} diff --git a/src/System.IO.FileSystem/tests/File/ReadWriteAllLinesAsync.cs b/src/System.IO.FileSystem/tests/File/ReadWriteAllLinesAsync.cs new file mode 100644 index 000000000000..5f398c853ec5 --- /dev/null +++ b/src/System.IO.FileSystem/tests/File/ReadWriteAllLinesAsync.cs @@ -0,0 +1,186 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Collections.Generic; +using System.Linq; +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Tests +{ + public class File_ReadWriteAllLines_EnumerableAsync : FileSystemTest + { + #region Utilities + + protected virtual Task WriteAsync(string path, string[] content) => + File.WriteAllLinesAsync(path, content); + + protected virtual Task ReadAsync(string path) => File.ReadAllLinesAsync(path); + + #endregion + + #region UniversalTests + + [Fact] + public async Task InvalidPathAsync() + { + await Assert.ThrowsAsync(async () => await WriteAsync(null, new string[] { "Text" })); + await Assert.ThrowsAsync(async () => await WriteAsync(string.Empty, new string[] { "Text" })); + await Assert.ThrowsAsync(async () => await ReadAsync(null)); + await Assert.ThrowsAsync(async () => await ReadAsync(string.Empty)); + } + + [Fact] + public async Task NullLinesAsync() + { + string path = GetTestFilePath(); + await Assert.ThrowsAsync(async () => await WriteAsync(path, null)); + + await WriteAsync(path, new string[] { null }); + Assert.Equal(new string[] { "" }, await ReadAsync(path)); + } + + [Fact] + public async Task EmptyStringCreatesFileAsync() + { + string path = GetTestFilePath(); + await WriteAsync(path, new string[] { }); + Assert.True(File.Exists(path)); + Assert.Empty(await ReadAsync(path)); + } + + [Theory] + [InlineData(0)] + [InlineData(100)] + public async Task ValidWriteAsync(int size) + { + string path = GetTestFilePath(); + string[] lines = { new string('c', size) }; + File.Create(path).Dispose(); + await WriteAsync(path, lines); + Assert.Equal(lines, await ReadAsync(path)); + } + + [Fact] + public virtual async Task OverwriteAsync() + { + string path = GetTestFilePath(); + string[] lines = { new string('c', 200) }; + string[] overwriteLines = { new string('b', 100) }; + await WriteAsync(path, lines); + await WriteAsync(path, overwriteLines); + Assert.Equal(overwriteLines, await ReadAsync(path)); + } + + [Fact] + public async Task OpenFile_ThrowsIOExceptionAsync() + { + string path = GetTestFilePath(); + string[] lines = { new string('c', 200) }; + + using (File.Create(path)) + { + await Assert.ThrowsAsync(async () => await WriteAsync(path, lines)); + await Assert.ThrowsAsync(async () => await ReadAsync(path)); + } + } + + [Fact] + public Task Read_FileNotFound() => + Assert.ThrowsAsync(async () => await ReadAsync(GetTestFilePath())); + + /// + /// On Unix, modifying a file that is ReadOnly will fail under normal permissions. + /// If the test is being run under the superuser, however, modification of a ReadOnly + /// file is allowed. + /// + [Fact] + public async Task WriteToReadOnlyFile() + { + string path = GetTestFilePath(); + File.Create(path).Dispose(); + File.SetAttributes(path, FileAttributes.ReadOnly); + try + { + // Operation succeeds when being run by the Unix superuser + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && geteuid() == 0) + { + await WriteAsync(path, new string[] { "text" }); + Assert.Equal(new string[] { "text" }, await ReadAsync(path)); + } + else + await Assert.ThrowsAsync(async () => await WriteAsync(path, new[] { "text" })); + } + finally + { + File.SetAttributes(path, FileAttributes.Normal); + } + } + + [Fact] + public async Task DisposingEnumeratorClosesFileAsync() + { + string path = GetTestFilePath(); + await WriteAsync(path, new[] { "line1", "line2", "line3" }); + + IEnumerable readLines = File.ReadLines(path); + using (IEnumerator e1 = readLines.GetEnumerator()) + using (IEnumerator e2 = readLines.GetEnumerator()) + { + Assert.Same(readLines, e1); + Assert.NotSame(e1, e2); + } + + // File should be closed deterministically; this shouldn't throw. + File.OpenWrite(path).Dispose(); + } + + + [Fact] + public virtual Task TaskAlreadyCanceledAsync() + { + string path = GetTestFilePath(); + CancellationTokenSource source = new CancellationTokenSource(); + CancellationToken token = source.Token; + source.Cancel(); + Assert.True(File.WriteAllLinesAsync(path, new[] { "" }, token).IsCanceled); + return Assert.ThrowsAsync( + async () => await File.WriteAllLinesAsync(path, new[] { "" }, token)); + } + + #endregion + } + + public class File_ReadWriteAllLines_Enumerable_EncodedAsync : File_ReadWriteAllLines_EnumerableAsync + { + protected override Task WriteAsync(string path, string[] content) => + File.WriteAllLinesAsync(path, (IEnumerable)content, new UTF8Encoding(false)); + + protected override Task ReadAsync(string path) => + File.ReadAllLinesAsync(path, new UTF8Encoding(false)); + + [Fact] + public async Task NullEncodingAsync() + { + string path = GetTestFilePath(); + await Assert.ThrowsAsync(async () => await File.WriteAllLinesAsync(path, new string[] { "Text" }, null)); + await Assert.ThrowsAsync(async () => await File.ReadAllLinesAsync(path, null)); + } + + [Fact] + public override Task TaskAlreadyCanceledAsync() + { + string path = GetTestFilePath(); + CancellationTokenSource source = new CancellationTokenSource(); + CancellationToken token = source.Token; + source.Cancel(); + Assert.True(File.WriteAllLinesAsync(path, new[] { "" }, Encoding.UTF8, token).IsCanceled); + return Assert.ThrowsAsync( + async () => await File.WriteAllLinesAsync(path, new[] { "" }, Encoding.UTF8, token)); + } + } +} diff --git a/src/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs b/src/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs new file mode 100644 index 000000000000..37eca6ad559b --- /dev/null +++ b/src/System.IO.FileSystem/tests/File/ReadWriteAllTextAsync.cs @@ -0,0 +1,173 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System.Runtime.InteropServices; +using System.Text; +using System.Threading; +using System.Threading.Tasks; +using Xunit; + +namespace System.IO.Tests +{ + public class File_ReadWriteAllTextAsync : FileSystemTest + { + #region Utilities + + protected virtual Task WriteAsync(string path, string content) => File.WriteAllTextAsync(path, content); + + protected virtual Task ReadAsync(string path) => File.ReadAllTextAsync(path); + + #endregion + + #region UniversalTests + + [Fact] + public async Task NullParametersAsync() + { + string path = GetTestFilePath(); + await Assert.ThrowsAsync(async () => await WriteAsync(null, "Text")); + await Assert.ThrowsAsync(async () => await ReadAsync(null)); + } + + [Fact] + public Task NonExistentPathAsync() => Assert.ThrowsAsync( + async () => await WriteAsync(Path.Combine(TestDirectory, GetTestFileName(), GetTestFileName()), "Text")); + + [Fact] + public async Task NullContent_CreatesFileAsync() + { + string path = GetTestFilePath(); + await WriteAsync(path, null); + Assert.Empty(await ReadAsync(path)); + } + + [Fact] + public async Task EmptyStringContent_CreatesFileAsync() + { + string path = GetTestFilePath(); + await WriteAsync(path, string.Empty); + Assert.Empty(await ReadAsync(path)); + } + + [Fact] + public async Task InvalidParametersAsync() + { + string path = GetTestFilePath(); + await Assert.ThrowsAsync(async () => await WriteAsync(string.Empty, "Text")); + await Assert.ThrowsAsync(async () => await ReadAsync("")); + } + + [Theory] + [InlineData(0)] + [InlineData(100)] + public async Task ValidWriteAsync(int size) + { + string path = GetTestFilePath(); + string toWrite = new string('c', size); + + File.Create(path).Dispose(); + await WriteAsync(path, toWrite); + Assert.Equal(toWrite, await ReadAsync(path)); + } + + [Fact] + public virtual async Task OverwriteAsync() + { + string path = GetTestFilePath(); + string lines = new string('c', 200); + string overwriteLines = new string('b', 100); + await WriteAsync(path, lines); + await WriteAsync(path, overwriteLines); + Assert.Equal(overwriteLines, await ReadAsync(path)); + } + + [Fact] + public async Task OpenFile_ThrowsIOExceptionAsync() + { + string path = GetTestFilePath(); + string lines = new string('c', 200); + + using (File.Create(path)) + { + await Assert.ThrowsAsync(async () => await WriteAsync(path, lines)); + await Assert.ThrowsAsync(async () => await ReadAsync(path)); + } + } + + [Fact] + public Task Read_FileNotFoundAsync() => + Assert.ThrowsAsync(async () => await ReadAsync(GetTestFilePath())); + + /// + /// On Unix, modifying a file that is ReadOnly will fail under normal permissions. + /// If the test is being run under the superuser, however, modification of a ReadOnly + /// file is allowed. + /// + [Fact] + public async Task WriteToReadOnlyFileAsync() + { + string path = GetTestFilePath(); + File.Create(path).Dispose(); + File.SetAttributes(path, FileAttributes.ReadOnly); + try + { + // Operation succeeds when being run by the Unix superuser + if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows) && geteuid() == 0) + { + await WriteAsync(path, "text"); + Assert.Equal("text", await ReadAsync(path)); + } + else + await Assert.ThrowsAsync(async () => await WriteAsync(path, "text")); + } + finally + { + File.SetAttributes(path, FileAttributes.Normal); + } + } + + [Fact] + public virtual Task TaskAlreadyCanceledAsync() + { + string path = GetTestFilePath(); + CancellationTokenSource source = new CancellationTokenSource(); + CancellationToken token = source.Token; + source.Cancel(); + Assert.True(File.WriteAllTextAsync(path, "", token).IsCanceled); + return Assert.ThrowsAsync( + async () => await File.WriteAllTextAsync(path, "", token)); + } + + #endregion + } + + public class File_ReadWriteAllText_EncodedAsync : File_ReadWriteAllTextAsync + { + protected override Task WriteAsync(string path, string content) => + File.WriteAllTextAsync(path, content, new UTF8Encoding(false)); + + protected override Task ReadAsync(string path) => + File.ReadAllTextAsync(path, new UTF8Encoding(false)); + + [Fact] + public async Task NullEncodingAsync() + { + string path = GetTestFilePath(); + await Assert.ThrowsAsync(async () => await File.WriteAllTextAsync(path, "Text", null)); + await Assert.ThrowsAsync(async () => await File.ReadAllTextAsync(path, null)); + } + + [Fact] + public override Task TaskAlreadyCanceledAsync() + { + string path = GetTestFilePath(); + CancellationTokenSource source = new CancellationTokenSource(); + CancellationToken token = source.Token; + source.Cancel(); + Assert.True(File.WriteAllTextAsync(path, "", Encoding.UTF8, token).IsCanceled); + return Assert.ThrowsAsync( + async () => await File.WriteAllTextAsync(path, "", Encoding.UTF8, token)); + } + } +} diff --git a/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj b/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj index bd92e51ed61b..260e406785da 100644 --- a/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj +++ b/src/System.IO.FileSystem/tests/System.IO.FileSystem.Tests.csproj @@ -1,4 +1,4 @@ - + @@ -82,12 +82,16 @@ + + + + @@ -164,4 +168,4 @@ - + \ No newline at end of file