Skip to content
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,9 @@
# Mono auto generated files
mono_crash.*

# Temporary install scripts
dotnet-install.sh

# Build results
[Dd]ebug/
[Dd]ebugPublic/
Expand Down
36 changes: 34 additions & 2 deletions src/System/IO/StreamFile.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
namespace OwlCore.Storage.System.IO;

/// <summary>
/// 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"/>.
/// 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.
/// </summary>
public class StreamFile : IFile
{
Expand All @@ -16,12 +16,28 @@ public class StreamFile : IFile
/// </summary>
public Stream Stream { get; }

/// <summary>
/// Gets a value indicating whether the underlying stream should be disposed when the returned stream from <see cref="OpenStreamAsync"/> is disposed.
/// When true, the underlying stream is returned directly. When false, the stream is wrapped in a non-disposable wrapper.
/// </summary>
public bool ShouldDispose { get; }

/// <summary>
/// Creates a new instance of <see cref="StreamFile"/>.
/// </summary>
/// <param name="stream">An existing stream which is provided as the file contents.</param>
public StreamFile(Stream stream)
: this(stream, $"{stream.GetHashCode()}", $"{stream.GetHashCode()}")
: this(stream, $"{stream.GetHashCode()}", $"{stream.GetHashCode()}", false)
{
}

/// <summary>
/// Creates a new instance of <see cref="StreamFile"/>.
/// </summary>
/// <param name="stream">An existing stream which is provided as the file contents.</param>
/// <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>
public StreamFile(Stream stream, bool shouldDispose)
: this(stream, $"{stream.GetHashCode()}", $"{stream.GetHashCode()}", shouldDispose)
{
}

Expand All @@ -32,10 +48,23 @@ public StreamFile(Stream stream)
/// <param name="id">A unique and consistent identifier for this file or folder.</param>
/// <param name="name">The name of the file or folder, with the extension (if any).</param>
public StreamFile(Stream stream, string id, string name)
: this(stream, id, name, false)
{
}

/// <summary>
/// Creates a new instance of <see cref="StreamFile"/>.
/// </summary>
/// <param name="stream">An existing stream which is provided as the file contents.</param>
/// <param name="id">A unique and consistent identifier for this file or folder.</param>
/// <param name="name">The name of the file or folder, with the extension (if any).</param>
/// <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>
public StreamFile(Stream stream, string id, string name, bool shouldDispose)
{
Stream = stream;
Id = id;
Name = name;
ShouldDispose = shouldDispose;
}

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

if (ShouldDispose)
return Task.FromResult(Stream);

return Task.FromResult<Stream>(new NonDisposableStreamWrapper(Stream));
}
}
141 changes: 140 additions & 1 deletion tests/OwlCore.Storage.Tests/StreamFileTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ public override async Task<IFile> CreateFileAsync()
{
var randomData = GenerateRandomData(256_000);
using var tempStr = new MemoryStream(randomData);

var memoryStream = new MemoryStream();
await tempStr.CopyToAsync(memoryStream);
memoryStream.Position = 0;
Expand All @@ -27,5 +27,144 @@ static byte[] GenerateRandomData(int length)
return b;
}
}

[TestMethod]
public void ShouldDispose_DefaultValue_IsFalse()
{
// Arrange
var memoryStream = new MemoryStream();

// Act
var streamFile = new StreamFile(memoryStream);

// Assert
Assert.IsFalse(streamFile.ShouldDispose);
}

[TestMethod]
public void ShouldDispose_Constructor_SetsCorrectValue()
{
// Arrange
var memoryStream = new MemoryStream();

// Act
var streamFileTrue = new StreamFile(memoryStream, true);
var streamFileFalse = new StreamFile(memoryStream, false);

// Assert
Assert.IsTrue(streamFileTrue.ShouldDispose);
Assert.IsFalse(streamFileFalse.ShouldDispose);
}

[TestMethod]
public void ShouldDispose_ConstructorWithIdAndName_SetsCorrectValue()
{
// Arrange
var memoryStream = new MemoryStream();
var id = "test-id";
var name = "test-name";

// Act
var streamFileTrue = new StreamFile(memoryStream, id, name, true);
var streamFileFalse = new StreamFile(memoryStream, id, name, false);

// Assert
Assert.IsTrue(streamFileTrue.ShouldDispose);
Assert.IsFalse(streamFileFalse.ShouldDispose);
}

[TestMethod]
public async Task OpenStreamAsync_ShouldDisposeTrue_ReturnsUnderlyingStream()
{
// Arrange
var memoryStream = new MemoryStream();
var streamFile = new StreamFile(memoryStream, true);

// Act
var resultStream = await streamFile.OpenStreamAsync();

// Assert
Assert.AreSame(memoryStream, resultStream);
}

[TestMethod]
public async Task OpenStreamAsync_ShouldDisposeFalse_ReturnsWrappedStream()
{
// Arrange
var memoryStream = new MemoryStream();
var streamFile = new StreamFile(memoryStream, false);

// Act
var resultStream = await streamFile.OpenStreamAsync();

// Assert
Assert.AreNotSame(memoryStream, resultStream);
Assert.AreEqual(typeof(OwlCore.Storage.Memory.NonDisposableStreamWrapper), resultStream.GetType());
}

[TestMethod]
public async Task OpenStreamAsync_ShouldDisposeTrue_DisposingReturnedStreamDisposesUnderlying()
{
// Arrange
var memoryStream = new MemoryStream();
var streamFile = new StreamFile(memoryStream, true);

// Act
var resultStream = await streamFile.OpenStreamAsync();
resultStream.Dispose();

// Assert
Assert.ThrowsException<ObjectDisposedException>(() => memoryStream.ReadByte());
}

[TestMethod]
public async Task OpenStreamAsync_ShouldDisposeFalse_DisposingReturnedStreamDoesNotDisposeUnderlying()
{
// Arrange
var data = new byte[] { 1, 2, 3, 4, 5 };
var memoryStream = new MemoryStream(data);
var streamFile = new StreamFile(memoryStream, false);

// Act
var resultStream = await streamFile.OpenStreamAsync();
resultStream.Dispose();

// Assert - underlying stream should still be usable
memoryStream.Position = 0;
Assert.AreEqual(1, memoryStream.ReadByte());
}

[TestMethod]
public async Task OpenStreamAsync_ShouldDisposeTrue_MultipleOpensReturnSameStream()
{
// Arrange
var memoryStream = new MemoryStream();
var streamFile = new StreamFile(memoryStream, true);

// Act
var stream1 = await streamFile.OpenStreamAsync();
var stream2 = await streamFile.OpenStreamAsync();

// Assert
Assert.AreSame(stream1, stream2);
Assert.AreSame(memoryStream, stream1);
}

[TestMethod]
public async Task OpenStreamAsync_ShouldDisposeFalse_MultipleOpensReturnDifferentWrappers()
{
// Arrange
var memoryStream = new MemoryStream();
var streamFile = new StreamFile(memoryStream, false);

// Act
var stream1 = await streamFile.OpenStreamAsync();
var stream2 = await streamFile.OpenStreamAsync();

// Assert
Assert.AreNotSame(stream1, stream2);
Assert.AreEqual(typeof(OwlCore.Storage.Memory.NonDisposableStreamWrapper), stream1.GetType());
Assert.AreEqual(typeof(OwlCore.Storage.Memory.NonDisposableStreamWrapper), stream2.GetType());
}
}
}
Loading