Skip to content

Commit 9925f50

Browse files
CopilotArlodotexe
andcommitted
Implement ShouldDispose property for StreamFile with comprehensive tests
Co-authored-by: Arlodotexe <9384894+Arlodotexe@users.noreply.github.com>
1 parent 2a113d2 commit 9925f50

File tree

4 files changed

+175
-4
lines changed

4 files changed

+175
-4
lines changed

src/OwlCore.Storage.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22
<PropertyGroup>
3-
<TargetFrameworks>netstandard2.0</TargetFrameworks>
3+
<TargetFrameworks>netstandard2.0;net9.0</TargetFrameworks>
44
<Nullable>enable</Nullable>
55
<LangVersion>12.0</LangVersion>
66
<WarningsAsErrors>nullable</WarningsAsErrors>

src/System/IO/StreamFile.cs

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@
77
namespace OwlCore.Storage.System.IO;
88

99
/// <summary>
10-
/// A file implementation which holds a reference to the provided <see cref="Stream"/> and returns it in a non-disposable wrapper for <see cref="OpenStreamAsync"/>.
10+
/// A file implementation which holds a reference to the provided <see cref="Stream"/> and returns it either wrapped in a non-disposable wrapper or directly, based on the <see cref="ShouldDispose"/> property.
1111
/// </summary>
1212
public class StreamFile : IFile
1313
{
@@ -16,12 +16,28 @@ public class StreamFile : IFile
1616
/// </summary>
1717
public Stream Stream { get; }
1818

19+
/// <summary>
20+
/// Gets a value indicating whether the underlying stream should be disposed when the returned stream from <see cref="OpenStreamAsync"/> is disposed.
21+
/// When true, the underlying stream is returned directly. When false, the stream is wrapped in a non-disposable wrapper.
22+
/// </summary>
23+
public bool ShouldDispose { get; }
24+
1925
/// <summary>
2026
/// Creates a new instance of <see cref="StreamFile"/>.
2127
/// </summary>
2228
/// <param name="stream">An existing stream which is provided as the file contents.</param>
2329
public StreamFile(Stream stream)
24-
: this(stream, $"{stream.GetHashCode()}", $"{stream.GetHashCode()}")
30+
: this(stream, $"{stream.GetHashCode()}", $"{stream.GetHashCode()}", false)
31+
{
32+
}
33+
34+
/// <summary>
35+
/// Creates a new instance of <see cref="StreamFile"/>.
36+
/// </summary>
37+
/// <param name="stream">An existing stream which is provided as the file contents.</param>
38+
/// <param name="shouldDispose">When true, the underlying stream will be disposed when the returned stream from <see cref="OpenStreamAsync"/> is disposed. When false, the stream is wrapped in a non-disposable wrapper.</param>
39+
public StreamFile(Stream stream, bool shouldDispose)
40+
: this(stream, $"{stream.GetHashCode()}", $"{stream.GetHashCode()}", shouldDispose)
2541
{
2642
}
2743

@@ -32,10 +48,23 @@ public StreamFile(Stream stream)
3248
/// <param name="id">A unique and consistent identifier for this file or folder.</param>
3349
/// <param name="name">The name of the file or folder, with the extension (if any).</param>
3450
public StreamFile(Stream stream, string id, string name)
51+
: this(stream, id, name, false)
52+
{
53+
}
54+
55+
/// <summary>
56+
/// Creates a new instance of <see cref="StreamFile"/>.
57+
/// </summary>
58+
/// <param name="stream">An existing stream which is provided as the file contents.</param>
59+
/// <param name="id">A unique and consistent identifier for this file or folder.</param>
60+
/// <param name="name">The name of the file or folder, with the extension (if any).</param>
61+
/// <param name="shouldDispose">When true, the underlying stream will be disposed when the returned stream from <see cref="OpenStreamAsync"/> is disposed. When false, the stream is wrapped in a non-disposable wrapper.</param>
62+
public StreamFile(Stream stream, string id, string name, bool shouldDispose)
3563
{
3664
Stream = stream;
3765
Id = id;
3866
Name = name;
67+
ShouldDispose = shouldDispose;
3968
}
4069

4170
/// <inheritdoc />
@@ -52,6 +81,9 @@ public Task<Stream> OpenStreamAsync(FileAccess accessMode = FileAccess.Read, Can
5281
if (accessMode == 0)
5382
throw new ArgumentOutOfRangeException(nameof(accessMode), $"{nameof(FileAccess)}.{accessMode} is not valid here.");
5483

84+
if (ShouldDispose)
85+
return Task.FromResult(Stream);
86+
5587
return Task.FromResult<Stream>(new NonDisposableStreamWrapper(Stream));
5688
}
5789
}

tests/OwlCore.Storage.Tests/OwlCore.Storage.Tests.csproj

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<Project Sdk="Microsoft.NET.Sdk">
22

33
<PropertyGroup>
4-
<TargetFramework>net8.0</TargetFramework>
4+
<TargetFramework>net9.0</TargetFramework>
55
<ImplicitUsings>enable</ImplicitUsings>
66
<Nullable>enable</Nullable>
77

tests/OwlCore.Storage.Tests/StreamFileTests.cs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,5 +27,144 @@ static byte[] GenerateRandomData(int length)
2727
return b;
2828
}
2929
}
30+
31+
[TestMethod]
32+
public void ShouldDispose_DefaultValue_IsFalse()
33+
{
34+
// Arrange
35+
var memoryStream = new MemoryStream();
36+
37+
// Act
38+
var streamFile = new StreamFile(memoryStream);
39+
40+
// Assert
41+
Assert.IsFalse(streamFile.ShouldDispose);
42+
}
43+
44+
[TestMethod]
45+
public void ShouldDispose_Constructor_SetsCorrectValue()
46+
{
47+
// Arrange
48+
var memoryStream = new MemoryStream();
49+
50+
// Act
51+
var streamFileTrue = new StreamFile(memoryStream, true);
52+
var streamFileFalse = new StreamFile(memoryStream, false);
53+
54+
// Assert
55+
Assert.IsTrue(streamFileTrue.ShouldDispose);
56+
Assert.IsFalse(streamFileFalse.ShouldDispose);
57+
}
58+
59+
[TestMethod]
60+
public void ShouldDispose_ConstructorWithIdAndName_SetsCorrectValue()
61+
{
62+
// Arrange
63+
var memoryStream = new MemoryStream();
64+
var id = "test-id";
65+
var name = "test-name";
66+
67+
// Act
68+
var streamFileTrue = new StreamFile(memoryStream, id, name, true);
69+
var streamFileFalse = new StreamFile(memoryStream, id, name, false);
70+
71+
// Assert
72+
Assert.IsTrue(streamFileTrue.ShouldDispose);
73+
Assert.IsFalse(streamFileFalse.ShouldDispose);
74+
}
75+
76+
[TestMethod]
77+
public async Task OpenStreamAsync_ShouldDisposeTrue_ReturnsUnderlyingStream()
78+
{
79+
// Arrange
80+
var memoryStream = new MemoryStream();
81+
var streamFile = new StreamFile(memoryStream, true);
82+
83+
// Act
84+
var resultStream = await streamFile.OpenStreamAsync();
85+
86+
// Assert
87+
Assert.AreSame(memoryStream, resultStream);
88+
}
89+
90+
[TestMethod]
91+
public async Task OpenStreamAsync_ShouldDisposeFalse_ReturnsWrappedStream()
92+
{
93+
// Arrange
94+
var memoryStream = new MemoryStream();
95+
var streamFile = new StreamFile(memoryStream, false);
96+
97+
// Act
98+
var resultStream = await streamFile.OpenStreamAsync();
99+
100+
// Assert
101+
Assert.AreNotSame(memoryStream, resultStream);
102+
Assert.AreEqual(typeof(OwlCore.Storage.Memory.NonDisposableStreamWrapper), resultStream.GetType());
103+
}
104+
105+
[TestMethod]
106+
public async Task OpenStreamAsync_ShouldDisposeTrue_DisposingReturnedStreamDisposesUnderlying()
107+
{
108+
// Arrange
109+
var memoryStream = new MemoryStream();
110+
var streamFile = new StreamFile(memoryStream, true);
111+
112+
// Act
113+
var resultStream = await streamFile.OpenStreamAsync();
114+
resultStream.Dispose();
115+
116+
// Assert
117+
Assert.ThrowsException<ObjectDisposedException>(() => memoryStream.ReadByte());
118+
}
119+
120+
[TestMethod]
121+
public async Task OpenStreamAsync_ShouldDisposeFalse_DisposingReturnedStreamDoesNotDisposeUnderlying()
122+
{
123+
// Arrange
124+
var data = new byte[] { 1, 2, 3, 4, 5 };
125+
var memoryStream = new MemoryStream(data);
126+
var streamFile = new StreamFile(memoryStream, false);
127+
128+
// Act
129+
var resultStream = await streamFile.OpenStreamAsync();
130+
resultStream.Dispose();
131+
132+
// Assert - underlying stream should still be usable
133+
memoryStream.Position = 0;
134+
Assert.AreEqual(1, memoryStream.ReadByte());
135+
}
136+
137+
[TestMethod]
138+
public async Task OpenStreamAsync_ShouldDisposeTrue_MultipleOpensReturnSameStream()
139+
{
140+
// Arrange
141+
var memoryStream = new MemoryStream();
142+
var streamFile = new StreamFile(memoryStream, true);
143+
144+
// Act
145+
var stream1 = await streamFile.OpenStreamAsync();
146+
var stream2 = await streamFile.OpenStreamAsync();
147+
148+
// Assert
149+
Assert.AreSame(stream1, stream2);
150+
Assert.AreSame(memoryStream, stream1);
151+
}
152+
153+
[TestMethod]
154+
public async Task OpenStreamAsync_ShouldDisposeFalse_MultipleOpensReturnDifferentWrappers()
155+
{
156+
// Arrange
157+
var memoryStream = new MemoryStream();
158+
var streamFile = new StreamFile(memoryStream, false);
159+
160+
// Act
161+
var stream1 = await streamFile.OpenStreamAsync();
162+
var stream2 = await streamFile.OpenStreamAsync();
163+
164+
// Assert
165+
Assert.AreNotSame(stream1, stream2);
166+
Assert.AreEqual(typeof(OwlCore.Storage.Memory.NonDisposableStreamWrapper), stream1.GetType());
167+
Assert.AreEqual(typeof(OwlCore.Storage.Memory.NonDisposableStreamWrapper), stream2.GetType());
168+
}
30169
}
31170
}

0 commit comments

Comments
 (0)