#region CPL License
/*
Nuclex Framework
Copyright (C) 2002-2009 Nuclex Development Labs
This library is free software; you can redistribute it and/or
modify it under the terms of the IBM Common Public License as
published by the IBM Corporation; either version 1.0 of the
License, or (at your option) any later version.
This library is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
IBM Common Public License for more details.
You should have received a copy of the IBM Common Public
License along with this library
*/
#endregion
#if UNITTEST
using System;
using System.IO;
using NUnit.Framework;
namespace Nuclex.Support.IO {
/// Unit Test for the stream chainer
[TestFixture]
public class ChainStreamTest {
#region class TestStream
/// Testing stream that allows specific features to be disabled
private class TestStream : Stream {
/// Initializes a new test stream
/// Stream that will be wrapped
/// Whether to allow reading from the stream
/// Whether to allow writing to the stream
/// Whether to allow seeking within the stream
public TestStream(
Stream wrappedStream, bool allowRead, bool allowWrite, bool allowSeek
) {
this.stream = wrappedStream;
this.readAllowed = allowRead;
this.writeAllowed = allowWrite;
this.seekAllowed = allowSeek;
}
/// Whether data can be read from the stream
public override bool CanRead {
get { return this.readAllowed; }
}
/// Whether the stream supports seeking
public override bool CanSeek {
get { return this.seekAllowed; }
}
/// Whether data can be written into the stream
public override bool CanWrite {
get { return this.writeAllowed; }
}
///
/// Clears all buffers for this stream and causes any buffered data to be written
/// to the underlying device.
///
public override void Flush() {
++this.flushCallCount;
this.stream.Flush();
}
/// Length of the stream in bytes
public override long Length {
get {
enforceSeekAllowed();
return this.stream.Length;
}
}
/// Absolute position of the file pointer within the stream
///
/// At least one of the chained streams does not support seeking
///
public override long Position {
get {
enforceSeekAllowed();
return this.stream.Position;
}
set {
enforceSeekAllowed();
this.stream.Position = value;
}
}
///
/// Reads a sequence of bytes from the stream and advances the position of
/// the file pointer by the number of bytes read.
///
/// Buffer that will receive the data read from the stream
///
/// Offset in the buffer at which the stream will place the data read
///
/// Maximum number of bytes that will be read
///
/// The number of bytes that were actually read from the stream and written into
/// the provided buffer
///
public override int Read(byte[] buffer, int offset, int count) {
enforceReadAllowed();
return this.stream.Read(buffer, offset, count);
}
/// Changes the position of the file pointer
///
/// Offset to move the file pointer by, relative to the position indicated by
/// the parameter.
///
///
/// Reference point relative to which the file pointer is placed
///
/// The new absolute position within the stream
public override long Seek(long offset, SeekOrigin origin) {
enforceSeekAllowed();
return this.stream.Seek(offset, origin);
}
/// Changes the length of the stream
/// New length the stream shall have
public override void SetLength(long value) {
enforceSeekAllowed();
this.stream.SetLength(value);
}
///
/// Writes a sequence of bytes to the stream and advances the position of
/// the file pointer by the number of bytes written.
///
///
/// Buffer containing the data that will be written to the stream
///
///
/// Offset in the buffer at which the data to be written starts
///
/// Number of bytes that will be written into the stream
public override void Write(byte[] buffer, int offset, int count) {
enforceWriteAllowed();
this.stream.Write(buffer, offset, count);
}
/// Number of times the Flush() method has been called
public int FlushCallCount {
get { return this.flushCallCount; }
}
/// Throws an exception if reading is not allowed
private void enforceReadAllowed() {
if(!this.readAllowed) {
throw new NotSupportedException("Reading has been disabled");
}
}
/// Throws an exception if writing is not allowed
private void enforceWriteAllowed() {
if(!this.writeAllowed) {
throw new NotSupportedException("Writing has been disabled");
}
}
/// Throws an exception if seeking is not allowed
private void enforceSeekAllowed() {
if(!this.seekAllowed) {
throw new NotSupportedException("Seeking has been disabled");
}
}
/// Stream being wrapped for testing
private Stream stream;
/// whether to allow reading from the wrapped stream
private bool readAllowed;
/// Whether to allow writing to the wrapped stream
private bool writeAllowed;
/// Whether to allow seeking within the wrapped stream
private bool seekAllowed;
/// Number of times the Flush() method has been called
private int flushCallCount;
}
#endregion // class TestStream
///
/// Tests whether the stream chainer correctly partitions a long write request
/// over its chained streams and appends any remaining data to the end of
/// the last chained stream.
///
[Test]
public void TestPartitionedWrite() {
ChainStream chainer = chainTwoStreamsOfTenBytes();
byte[] testData = new byte[20];
for(int index = 0; index < testData.Length; ++index) {
testData[index] = (byte)(index + 1);
}
chainer.Position = 5;
chainer.Write(testData, 0, testData.Length);
Assert.AreEqual(
new byte[] { 0, 0, 0, 0, 0, 1, 2, 3, 4, 5 },
((MemoryStream)chainer.ChainedStreams[0]).ToArray()
);
Assert.AreEqual(
new byte[] { 6, 7, 8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20 },
((MemoryStream)chainer.ChainedStreams[1]).ToArray()
);
}
///
/// Tests whether the stream chainer correctly partitions a long read request
/// over its chained streams.
///
[Test]
public void TestPartitionedRead() {
ChainStream chainer = chainTwoStreamsOfTenBytes();
((MemoryStream)chainer.ChainedStreams[0]).Write(
new byte[] { 1, 2, 3, 4, 5 }, 0, 5
);
((MemoryStream)chainer.ChainedStreams[1]).Write(
new byte[] { 6, 7, 8, 9, 10 }, 0, 5
);
chainer.Position = 3;
byte[] buffer = new byte[15];
int bytesRead = chainer.Read(buffer, 0, 14);
Assert.AreEqual(14, bytesRead);
Assert.AreEqual(new byte[] { 4, 5, 0, 0, 0, 0, 0, 6, 7, 8, 9, 10, 0, 0, 0 }, buffer);
}
///
/// Tests whether the stream chainer can handle a stream resize
///
[Test]
public void TestWriteAfterResize() {
ChainStream chainer = chainTwoStreamsOfTenBytes();
// The first stream has a size of 10 bytes, so this goes into the second stream
chainer.Position = 11;
chainer.Write(new byte[] { 12, 34 }, 0, 2);
// Now we resize the first stream to 15 bytes, so this goes into the first stream
((MemoryStream)chainer.ChainedStreams[0]).SetLength(15);
chainer.Write(new byte[] { 56, 78, 11, 22 }, 0, 4);
Assert.AreEqual(
new byte[] { 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 56, 78 },
((MemoryStream)chainer.ChainedStreams[0]).ToArray()
);
Assert.AreEqual(
new byte[] { 11, 22, 34, 0, 0, 0, 0, 0, 0, 0 },
((MemoryStream)chainer.ChainedStreams[1]).ToArray()
);
}
///
/// Tests writing to a stream chainer that contains an unseekable stream
///
[Test]
public void TestWriteToUnseekableStream() {
MemoryStream firstStream = new MemoryStream();
// Now the second stream _does_ support seeking. If the stream chainer ignores
// that, it would overwrite data in the second stream.
MemoryStream secondStream = new MemoryStream();
secondStream.Write(new byte[] { 0, 9, 8, 7, 6 }, 0, 5);
secondStream.Position = 0;
TestStream testStream = new TestStream(firstStream, true, true, false);
ChainStream chainer = new ChainStream(new Stream[] { testStream, secondStream });
chainer.Write(new byte[] { 1, 2, 3, 4, 5 }, 0, 5);
Assert.IsFalse(chainer.CanSeek);
Assert.AreEqual(0, firstStream.Length);
Assert.AreEqual(new byte[] { 0, 9, 8, 7, 6, 1, 2, 3, 4, 5 }, secondStream.ToArray());
}
///
/// Tests reading from a stream chainer that contains an unseekable stream
///
[Test]
public void TestReadFromUnseekableStream() {
MemoryStream firstStream = new MemoryStream();
// Now the second stream _does_ support seeking. If the stream chainer ignores
// that, it would overwrite data in the second stream.
MemoryStream secondStream = new MemoryStream();
secondStream.Write(new byte[] { 0, 9, 8, 7, 6 }, 0, 5);
secondStream.Position = 3;
TestStream testStream = new TestStream(firstStream, true, true, false);
ChainStream chainer = new ChainStream(new Stream[] { testStream, secondStream });
Assert.IsFalse(chainer.CanSeek);
byte[] buffer = new byte[5];
int readByteCount = chainer.Read(buffer, 0, 3);
Assert.AreEqual(3, readByteCount);
Assert.AreEqual(new byte[] { 0, 9, 8, 0, 0 }, buffer);
readByteCount = chainer.Read(buffer, 0, 3);
Assert.AreEqual(2, readByteCount);
Assert.AreEqual(new byte[] { 7, 6, 8, 0, 0 }, buffer);
}
///
/// Tests reading from a stream chainer that contains an unreadable stream
///
[Test, ExpectedException(typeof(NotSupportedException))]
public void TestThrowOnReadFromUnreadableStream() {
MemoryStream memoryStream = new MemoryStream();
TestStream testStream = new TestStream(memoryStream, false, true, true);
ChainStream chainer = new ChainStream(new Stream[] { testStream });
chainer.Read(new byte[5], 0, 5);
}
///
/// Tests writing to a stream chainer that contains an unwriteable stream
///
[Test, ExpectedException(typeof(NotSupportedException))]
public void TestThrowOnWriteToUnwriteableStream() {
MemoryStream memoryStream = new MemoryStream();
TestStream testStream = new TestStream(memoryStream, true, false, true);
ChainStream chainer = new ChainStream(new Stream[] { testStream });
chainer.Write(new byte[] { 1, 2, 3, 4, 5 }, 0, 5);
}
///
/// Verifies that the stream chainer throws an exception if the attempt is
/// made to change the length of the stream
///
[Test, ExpectedException(typeof(NotSupportedException))]
public void TestThrowOnLengthChange() {
ChainStream chainer = chainTwoStreamsOfTenBytes();
chainer.SetLength(123);
}
///
/// Verifies that the CanRead property is correctly determined by the stream chainer
///
[Test]
public void TestCanRead() {
MemoryStream yesStream = new MemoryStream();
TestStream noStream = new TestStream(yesStream, false, true, true);
Stream[] yesGroup = new Stream[] { yesStream, yesStream, yesStream, yesStream };
Stream[] partialGroup = new Stream[] { yesStream, yesStream, noStream, yesStream };
Stream[] noGroup = new Stream[] { noStream, noStream, noStream, noStream };
Assert.IsTrue(new ChainStream(yesGroup).CanRead);
Assert.IsFalse(new ChainStream(partialGroup).CanRead);
Assert.IsFalse(new ChainStream(noGroup).CanRead);
}
///
/// Verifies that the CanRead property is correctly determined by the stream chainer
///
[Test]
public void TestCanWrite() {
MemoryStream yesStream = new MemoryStream();
TestStream noStream = new TestStream(yesStream, true, false, true);
Stream[] yesGroup = new Stream[] { yesStream, yesStream, yesStream, yesStream };
Stream[] partialGroup = new Stream[] { yesStream, yesStream, noStream, yesStream };
Stream[] noGroup = new Stream[] { noStream, noStream, noStream, noStream };
Assert.IsTrue(new ChainStream(yesGroup).CanWrite);
Assert.IsFalse(new ChainStream(partialGroup).CanWrite);
Assert.IsFalse(new ChainStream(noGroup).CanWrite);
}
///
/// Verifies that the CanSeek property is correctly determined by the stream chainer
///
[Test]
public void TestCanSeek() {
MemoryStream yesStream = new MemoryStream();
TestStream noStream = new TestStream(yesStream, true, true, false);
Stream[] yesGroup = new Stream[] { yesStream, yesStream, yesStream, yesStream };
Stream[] partialGroup = new Stream[] { yesStream, yesStream, noStream, yesStream };
Stream[] noGroup = new Stream[] { noStream, noStream, noStream, noStream };
Assert.IsTrue(new ChainStream(yesGroup).CanSeek);
Assert.IsFalse(new ChainStream(partialGroup).CanSeek);
Assert.IsFalse(new ChainStream(noGroup).CanSeek);
}
///
/// Tests whether an exception is thrown if the Seek() method is called on
/// a stream chainer with streams that do not support seeking
///
[Test, ExpectedException(typeof(NotSupportedException))]
public void TestThrowOnSeekWithUnseekableStream() {
MemoryStream memoryStream = new MemoryStream();
TestStream testStream = new TestStream(memoryStream, true, true, false);
ChainStream chainer = new ChainStream(new Stream[] { testStream });
chainer.Seek(123, SeekOrigin.Begin);
}
///
/// Tests whether an exception is thrown if the Position property is retrieved
/// on a stream chainer with streams that do not support seeking
///
[Test, ExpectedException(typeof(NotSupportedException))]
public void TestThrowOnGetPositionWithUnseekableStream() {
MemoryStream memoryStream = new MemoryStream();
TestStream testStream = new TestStream(memoryStream, true, true, false);
ChainStream chainer = new ChainStream(new Stream[] { testStream });
Assert.IsTrue(chainer.Position != chainer.Position); // ;-)
}
///
/// Tests whether an exception is thrown if the Position property is set
/// on a stream chainer with streams that do not support seeking
///
[Test, ExpectedException(typeof(NotSupportedException))]
public void TestThrowOnSetPositionWithUnseekableStream() {
MemoryStream memoryStream = new MemoryStream();
TestStream testStream = new TestStream(memoryStream, true, true, false);
ChainStream chainer = new ChainStream(new Stream[] { testStream });
chainer.Position = 123;
}
///
/// Tests whether an exception is thrown if the Length property is retrieved
/// on a stream chainer with streams that do not support seeking
///
[Test, ExpectedException(typeof(NotSupportedException))]
public void TestThrowOnGetLengthWithUnseekableStream() {
MemoryStream memoryStream = new MemoryStream();
TestStream testStream = new TestStream(memoryStream, true, true, false);
ChainStream chainer = new ChainStream(new Stream[] { testStream });
Assert.IsTrue(chainer.Length != chainer.Length); // ;-)
}
///
/// Tests whether the Seek() method of the stream chainer is working
///
[Test]
public void TestSeeking() {
ChainStream chainer = chainTwoStreamsOfTenBytes();
Assert.AreEqual(7, chainer.Seek(-13, SeekOrigin.End));
Assert.AreEqual(14, chainer.Seek(7, SeekOrigin.Current));
Assert.AreEqual(11, chainer.Seek(11, SeekOrigin.Begin));
}
///
/// Tests whether the stream behaves correctly if data is read from beyond its end
///
[Test]
public void TestReadBeyondEndOfStream() {
ChainStream chainer = chainTwoStreamsOfTenBytes();
chainer.Seek(10, SeekOrigin.End);
// This is how the MemoryStream behaves: it returns 0 bytes.
int readByteCount = chainer.Read(new byte[1], 0, 1);
Assert.AreEqual(0, readByteCount);
}
///
/// Tests whether the Seek() method throws an exception if an invalid
/// reference point is provided
///
[Test, ExpectedException(typeof(ArgumentException))]
public void TestThrowOnInvalidSeekReferencePoint() {
ChainStream chainer = chainTwoStreamsOfTenBytes();
chainer.Seek(1, (SeekOrigin)12345);
}
/// Verifies that the position property works correctly
[Test]
public void TestPositionChange() {
ChainStream chainer = chainTwoStreamsOfTenBytes();
chainer.Position = 7;
Assert.AreEqual(chainer.Position, 7);
chainer.Position = 14;
Assert.AreEqual(chainer.Position, 14);
}
/// Tests the Flush() method of the stream chainer
[Test]
public void TestFlush() {
MemoryStream firstStream = new MemoryStream();
TestStream firstTestStream = new TestStream(firstStream, true, true, true);
MemoryStream secondStream = new MemoryStream();
TestStream secondTestStream = new TestStream(secondStream, true, true, true);
ChainStream chainer = new ChainStream(
new Stream[] { firstTestStream, secondTestStream }
);
Assert.AreEqual(0, firstTestStream.FlushCallCount);
Assert.AreEqual(0, secondTestStream.FlushCallCount);
chainer.Flush();
Assert.AreEqual(1, firstTestStream.FlushCallCount);
Assert.AreEqual(1, secondTestStream.FlushCallCount);
}
///
/// Creates a stream chainer with two streams that each have a size of 10 bytes
///
/// The new stream chainer with two chained 10-byte streams
private static ChainStream chainTwoStreamsOfTenBytes() {
MemoryStream firstStream = new MemoryStream(10);
MemoryStream secondStream = new MemoryStream(10);
firstStream.SetLength(10);
secondStream.SetLength(10);
return new ChainStream(
new Stream[] { firstStream, secondStream }
);
}
}
} // namespace Nuclex.Support.IO
#endif // UNITTEST