#region CPL License /* Nuclex Framework Copyright (C) 2002-2012 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] internal 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] public void TestThrowOnReadFromUnreadableStream() { MemoryStream memoryStream = new MemoryStream(); TestStream testStream = new TestStream(memoryStream, false, true, true); ChainStream chainer = new ChainStream(new Stream[] { testStream }); Assert.Throws( delegate() { chainer.Read(new byte[5], 0, 5); } ); } /// /// Tests writing to a stream chainer that contains an unwriteable stream /// [Test] public void TestThrowOnWriteToUnwriteableStream() { MemoryStream memoryStream = new MemoryStream(); TestStream testStream = new TestStream(memoryStream, true, false, true); ChainStream chainer = new ChainStream(new Stream[] { testStream }); Assert.Throws( delegate() { 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] public void TestThrowOnLengthChange() { ChainStream chainer = chainTwoStreamsOfTenBytes(); Assert.Throws( delegate() { 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] public void TestThrowOnSeekWithUnseekableStream() { MemoryStream memoryStream = new MemoryStream(); TestStream testStream = new TestStream(memoryStream, true, true, false); ChainStream chainer = new ChainStream(new Stream[] { testStream }); Assert.Throws( delegate() { 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] public void TestThrowOnGetPositionWithUnseekableStream() { MemoryStream memoryStream = new MemoryStream(); TestStream testStream = new TestStream(memoryStream, true, true, false); ChainStream chainer = new ChainStream(new Stream[] { testStream }); Assert.Throws( delegate() { Console.WriteLine(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] public void TestThrowOnSetPositionWithUnseekableStream() { MemoryStream memoryStream = new MemoryStream(); TestStream testStream = new TestStream(memoryStream, true, true, false); ChainStream chainer = new ChainStream(new Stream[] { testStream }); Assert.Throws( delegate() { 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] public void TestThrowOnGetLengthWithUnseekableStream() { MemoryStream memoryStream = new MemoryStream(); TestStream testStream = new TestStream(memoryStream, true, true, false); ChainStream chainer = new ChainStream(new Stream[] { testStream }); Assert.Throws( delegate() { 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] public void TestThrowOnInvalidSeekReferencePoint() { ChainStream chainer = chainTwoStreamsOfTenBytes(); Assert.Throws( delegate() { 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