From 5b1873ee9a19c9312a4c4d056481e1b17e40744b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simona=20Kon=C3=AD=C4=8Dkov=C3=A1?= Date: Fri, 30 Jun 2023 13:12:39 +0200 Subject: [PATCH 1/3] Use `FileOptions.Asynchronous` when doing async IO --- src/ImageSharp/IO/IFileSystem.cs | 26 +++++- src/ImageSharp/IO/LocalFileSystem.cs | 20 +++- src/ImageSharp/Image.FromFile.cs | 8 +- src/ImageSharp/ImageExtensions.cs | 2 +- .../IO/LocalFileSystemTests.cs | 93 +++++++++++++++---- .../ImageSharp.Tests/Image/ImageSaveTests.cs | 8 +- .../Image/ImageTests.ImageLoadTestBase.cs | 6 ++ tests/ImageSharp.Tests/TestFileSystem.cs | 44 +++++---- .../TestUtilities/SingleStreamFileSystem.cs | 4 + 9 files changed, 161 insertions(+), 50 deletions(-) diff --git a/src/ImageSharp/IO/IFileSystem.cs b/src/ImageSharp/IO/IFileSystem.cs index 96a9b5ba01..0f5113eff4 100644 --- a/src/ImageSharp/IO/IFileSystem.cs +++ b/src/ImageSharp/IO/IFileSystem.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.IO; @@ -9,16 +9,32 @@ namespace SixLabors.ImageSharp.IO; internal interface IFileSystem { /// - /// Returns a readable stream as defined by the path. + /// Opens a file as defined by the path and returns it as a readable stream. /// /// Path to the file to open. - /// A stream representing the file to open. + /// A stream representing the opened file. Stream OpenRead(string path); /// - /// Creates or opens a file and returns it as a writable stream as defined by the path. + /// Opens a file as defined by the path and returns it as a readable stream + /// that can be used for asynchronous reading. /// /// Path to the file to open. - /// A stream representing the file to open. + /// A stream representing the opened file. + Stream OpenReadAsynchronous(string path); + + /// + /// Creates or opens a file as defined by the path and returns it as a writable stream. + /// + /// Path to the file to open. + /// A stream representing the opened file. Stream Create(string path); + + /// + /// Creates or opens a file as defined by the path and returns it as a writable stream + /// that can be used for asynchronous reading and writing. + /// + /// Path to the file to open. + /// A stream representing the opened file. + Stream CreateAsynchronous(string path); } diff --git a/src/ImageSharp/IO/LocalFileSystem.cs b/src/ImageSharp/IO/LocalFileSystem.cs index f4dfa2fe14..d1f619f486 100644 --- a/src/ImageSharp/IO/LocalFileSystem.cs +++ b/src/ImageSharp/IO/LocalFileSystem.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. namespace SixLabors.ImageSharp.IO; @@ -11,6 +11,24 @@ internal sealed class LocalFileSystem : IFileSystem /// public Stream OpenRead(string path) => File.OpenRead(path); + /// + public Stream OpenReadAsynchronous(string path) => File.Open(path, new FileStreamOptions + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Share = FileShare.Read, + Options = FileOptions.Asynchronous, + }); + /// public Stream Create(string path) => File.Create(path); + + /// + public Stream CreateAsynchronous(string path) => File.Open(path, new FileStreamOptions + { + Mode = FileMode.Create, + Access = FileAccess.ReadWrite, + Share = FileShare.None, + Options = FileOptions.Asynchronous, + }); } diff --git a/src/ImageSharp/Image.FromFile.cs b/src/ImageSharp/Image.FromFile.cs index 884acf7a40..a20e5d6c58 100644 --- a/src/ImageSharp/Image.FromFile.cs +++ b/src/ImageSharp/Image.FromFile.cs @@ -72,7 +72,7 @@ public static async Task DetectFormatAsync( { Guard.NotNull(options, nameof(options)); - using Stream stream = options.Configuration.FileSystem.OpenRead(path); + await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path); return await DetectFormatAsync(options, stream, cancellationToken).ConfigureAwait(false); } @@ -144,7 +144,7 @@ public static async Task IdentifyAsync( CancellationToken cancellationToken = default) { Guard.NotNull(options, nameof(options)); - using Stream stream = options.Configuration.FileSystem.OpenRead(path); + await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path); return await IdentifyAsync(options, stream, cancellationToken).ConfigureAwait(false); } @@ -214,7 +214,7 @@ public static async Task LoadAsync( string path, CancellationToken cancellationToken = default) { - using Stream stream = options.Configuration.FileSystem.OpenRead(path); + await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path); return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false); } @@ -291,7 +291,7 @@ public static async Task> LoadAsync( Guard.NotNull(options, nameof(options)); Guard.NotNull(path, nameof(path)); - using Stream stream = options.Configuration.FileSystem.OpenRead(path); + await using Stream stream = options.Configuration.FileSystem.OpenReadAsynchronous(path); return await LoadAsync(options, stream, cancellationToken).ConfigureAwait(false); } } diff --git a/src/ImageSharp/ImageExtensions.cs b/src/ImageSharp/ImageExtensions.cs index cf970b3166..75e4f13257 100644 --- a/src/ImageSharp/ImageExtensions.cs +++ b/src/ImageSharp/ImageExtensions.cs @@ -70,7 +70,7 @@ public static async Task SaveAsync( Guard.NotNull(path, nameof(path)); Guard.NotNull(encoder, nameof(encoder)); - using Stream fs = source.GetConfiguration().FileSystem.Create(path); + await using Stream fs = source.GetConfiguration().FileSystem.CreateAsynchronous(path); await source.SaveAsync(fs, encoder, cancellationToken).ConfigureAwait(false); } diff --git a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs index 3d512b7d27..30818f999a 100644 --- a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs +++ b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs @@ -1,4 +1,4 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. using SixLabors.ImageSharp.IO; @@ -11,36 +11,97 @@ public class LocalFileSystemTests public void OpenRead() { string path = Path.GetTempFileName(); - string testData = Guid.NewGuid().ToString(); - File.WriteAllText(path, testData); + try + { + string testData = Guid.NewGuid().ToString(); + File.WriteAllText(path, testData); - var fs = new LocalFileSystem(); + LocalFileSystem fs = new(); - using (var r = new StreamReader(fs.OpenRead(path))) - { - string data = r.ReadToEnd(); + using (Stream stream = fs.OpenRead(path)) + using (StreamReader reader = new(stream)) + { + string data = reader.ReadToEnd(); - Assert.Equal(testData, data); + Assert.Equal(testData, data); + } + } + finally + { + File.Delete(path); } + } - File.Delete(path); + [Fact] + public async Task OpenReadAsynchronous() + { + string path = Path.GetTempFileName(); + try + { + string testData = Guid.NewGuid().ToString(); + File.WriteAllText(path, testData); + + LocalFileSystem fs = new(); + + await using (Stream stream = fs.OpenReadAsynchronous(path)) + using (StreamReader reader = new(stream)) + { + string data = await reader.ReadToEndAsync(); + + Assert.Equal(testData, data); + } + } + finally + { + File.Delete(path); + } } [Fact] public void Create() { string path = Path.GetTempFileName(); - string testData = Guid.NewGuid().ToString(); - var fs = new LocalFileSystem(); + try + { + string testData = Guid.NewGuid().ToString(); + LocalFileSystem fs = new(); - using (var r = new StreamWriter(fs.Create(path))) + using (Stream stream = fs.Create(path)) + using (StreamWriter writer = new(stream)) + { + writer.Write(testData); + } + + string data = File.ReadAllText(path); + Assert.Equal(testData, data); + } + finally { - r.Write(testData); + File.Delete(path); } + } - string data = File.ReadAllText(path); - Assert.Equal(testData, data); + [Fact] + public async Task CreateAsynchronous() + { + string path = Path.GetTempFileName(); + try + { + string testData = Guid.NewGuid().ToString(); + LocalFileSystem fs = new(); - File.Delete(path); + await using (Stream stream = fs.CreateAsynchronous(path)) + using (StreamWriter writer = new(stream)) + { + await writer.WriteAsync(testData); + } + + string data = File.ReadAllText(path); + Assert.Equal(testData, data); + } + finally + { + File.Delete(path); + } } } diff --git a/tests/ImageSharp.Tests/Image/ImageSaveTests.cs b/tests/ImageSharp.Tests/Image/ImageSaveTests.cs index a3f03bed5a..f9c01ab564 100644 --- a/tests/ImageSharp.Tests/Image/ImageSaveTests.cs +++ b/tests/ImageSharp.Tests/Image/ImageSaveTests.cs @@ -44,7 +44,7 @@ public ImageSaveTests() [Fact] public void SavePath() { - var stream = new MemoryStream(); + using MemoryStream stream = new(); this.fileSystem.Setup(x => x.Create("path.png")).Returns(stream); this.image.Save("path.png"); @@ -54,7 +54,7 @@ public void SavePath() [Fact] public void SavePathWithEncoder() { - var stream = new MemoryStream(); + using MemoryStream stream = new(); this.fileSystem.Setup(x => x.Create("path.jpg")).Returns(stream); this.image.Save("path.jpg", this.encoderNotInFormat.Object); @@ -73,7 +73,7 @@ public void ToBase64String() [Fact] public void SaveStreamWithMime() { - var stream = new MemoryStream(); + using MemoryStream stream = new(); this.image.Save(stream, this.localImageFormat.Object); this.encoder.Verify(x => x.Encode(this.image, stream)); @@ -82,7 +82,7 @@ public void SaveStreamWithMime() [Fact] public void SaveStreamWithEncoder() { - var stream = new MemoryStream(); + using MemoryStream stream = new(); this.image.Save(stream, this.encoderNotInFormat.Object); diff --git a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs index a8f9981b44..996310d8c3 100644 --- a/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs +++ b/tests/ImageSharp.Tests/Image/ImageTests.ImageLoadTestBase.cs @@ -122,6 +122,7 @@ protected ImageLoadTestBase() Stream StreamFactory() => this.DataStream; this.LocalFileSystemMock.Setup(x => x.OpenRead(this.MockFilePath)).Returns(StreamFactory); + this.LocalFileSystemMock.Setup(x => x.OpenReadAsynchronous(this.MockFilePath)).Returns(StreamFactory); this.topLevelFileSystem.AddFile(this.MockFilePath, StreamFactory); this.LocalConfiguration.FileSystem = this.LocalFileSystemMock.Object; this.TopLevelConfiguration.FileSystem = this.topLevelFileSystem; @@ -132,6 +133,11 @@ public void Dispose() // Clean up the global object; this.localStreamReturnImageRgba32?.Dispose(); this.localStreamReturnImageAgnostic?.Dispose(); + + if (this.dataStreamLazy.IsValueCreated) + { + this.dataStreamLazy.Value.Dispose(); + } } protected virtual Stream CreateStream() => this.TestFormat.CreateStream(this.Marker); diff --git a/tests/ImageSharp.Tests/TestFileSystem.cs b/tests/ImageSharp.Tests/TestFileSystem.cs index 8aefbe320e..9013d15530 100644 --- a/tests/ImageSharp.Tests/TestFileSystem.cs +++ b/tests/ImageSharp.Tests/TestFileSystem.cs @@ -1,6 +1,8 @@ -// Copyright (c) Six Labors. +// Copyright (c) Six Labors. // Licensed under the Six Labors Split License. +#nullable enable + namespace SixLabors.ImageSharp.Tests; /// @@ -8,7 +10,7 @@ namespace SixLabors.ImageSharp.Tests; /// public class TestFileSystem : ImageSharp.IO.IFileSystem { - private readonly Dictionary> fileSystem = new Dictionary>(StringComparer.OrdinalIgnoreCase); + private readonly Dictionary> fileSystem = new(StringComparer.OrdinalIgnoreCase); public void AddFile(string path, Func data) { @@ -18,35 +20,39 @@ public void AddFile(string path, Func data) } } - public Stream Create(string path) + public Stream Create(string path) => this.GetStream(path) ?? File.Create(path); + + public Stream CreateAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions { - // if we have injected a fake file use it instead - lock (this.fileSystem) - { - if (this.fileSystem.ContainsKey(path)) - { - Stream stream = this.fileSystem[path](); - stream.Position = 0; - return stream; - } - } + Mode = FileMode.Create, + Access = FileAccess.ReadWrite, + Share = FileShare.None, + Options = FileOptions.Asynchronous, + }); - return File.Create(path); - } + public Stream OpenRead(string path) => this.GetStream(path) ?? File.OpenRead(path); + + public Stream OpenReadAsynchronous(string path) => this.GetStream(path) ?? File.Open(path, new FileStreamOptions + { + Mode = FileMode.Open, + Access = FileAccess.Read, + Share = FileShare.Read, + Options = FileOptions.Asynchronous, + }); - public Stream OpenRead(string path) + private Stream? GetStream(string path) { // if we have injected a fake file use it instead lock (this.fileSystem) { - if (this.fileSystem.ContainsKey(path)) + if (this.fileSystem.TryGetValue(path, out Func? streamFactory)) { - Stream stream = this.fileSystem[path](); + Stream stream = streamFactory(); stream.Position = 0; return stream; } } - return File.OpenRead(path); + return null; } } diff --git a/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs b/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs index 732948b8e0..7b519531ab 100644 --- a/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs +++ b/tests/ImageSharp.Tests/TestUtilities/SingleStreamFileSystem.cs @@ -13,5 +13,9 @@ internal class SingleStreamFileSystem : IFileSystem Stream IFileSystem.Create(string path) => this.stream; + Stream IFileSystem.CreateAsynchronous(string path) => this.stream; + Stream IFileSystem.OpenRead(string path) => this.stream; + + Stream IFileSystem.OpenReadAsynchronous(string path) => this.stream; } From c81e63984237c451db897f01e6152a48d2c3a543 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simona=20Kon=C3=AD=C4=8Dkov=C3=A1?= Date: Fri, 30 Jun 2023 15:22:24 +0200 Subject: [PATCH 2/3] Use `await using` where available --- tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs index 30818f999a..10acd61605 100644 --- a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs +++ b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs @@ -91,7 +91,7 @@ public async Task CreateAsynchronous() LocalFileSystem fs = new(); await using (Stream stream = fs.CreateAsynchronous(path)) - using (StreamWriter writer = new(stream)) + await using (StreamWriter writer = new(stream)) { await writer.WriteAsync(testData); } From 7a2631bdf011e415a2801b2b2e94a9a443da1a29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Simona=20Kon=C3=AD=C4=8Dkov=C3=A1?= Date: Fri, 30 Jun 2023 15:30:20 +0200 Subject: [PATCH 3/3] Add assertions for `FileStream` properties --- .../IO/LocalFileSystemTests.cs | 24 +++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs index 10acd61605..a1eeb25976 100644 --- a/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs +++ b/tests/ImageSharp.Tests/IO/LocalFileSystemTests.cs @@ -18,9 +18,13 @@ public void OpenRead() LocalFileSystem fs = new(); - using (Stream stream = fs.OpenRead(path)) + using (FileStream stream = (FileStream)fs.OpenRead(path)) using (StreamReader reader = new(stream)) { + Assert.False(stream.IsAsync); + Assert.True(stream.CanRead); + Assert.False(stream.CanWrite); + string data = reader.ReadToEnd(); Assert.Equal(testData, data); @@ -43,9 +47,13 @@ public async Task OpenReadAsynchronous() LocalFileSystem fs = new(); - await using (Stream stream = fs.OpenReadAsynchronous(path)) + await using (FileStream stream = (FileStream)fs.OpenReadAsynchronous(path)) using (StreamReader reader = new(stream)) { + Assert.True(stream.IsAsync); + Assert.True(stream.CanRead); + Assert.False(stream.CanWrite); + string data = await reader.ReadToEndAsync(); Assert.Equal(testData, data); @@ -66,9 +74,13 @@ public void Create() string testData = Guid.NewGuid().ToString(); LocalFileSystem fs = new(); - using (Stream stream = fs.Create(path)) + using (FileStream stream = (FileStream)fs.Create(path)) using (StreamWriter writer = new(stream)) { + Assert.False(stream.IsAsync); + Assert.True(stream.CanRead); + Assert.True(stream.CanWrite); + writer.Write(testData); } @@ -90,9 +102,13 @@ public async Task CreateAsynchronous() string testData = Guid.NewGuid().ToString(); LocalFileSystem fs = new(); - await using (Stream stream = fs.CreateAsynchronous(path)) + await using (FileStream stream = (FileStream)fs.CreateAsynchronous(path)) await using (StreamWriter writer = new(stream)) { + Assert.True(stream.IsAsync); + Assert.True(stream.CanRead); + Assert.True(stream.CanWrite); + await writer.WriteAsync(testData); }