#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.Text; using NUnit.Framework; namespace Nuclex.Networking { /// Unit tests for the line parser [TestFixture] public class LineParserTest { #region class TestParser /// Dummy parser for testing the line parser private class TestParser : LineParser { /// Initializes a new test parser public TestParser() : base() { } /// Initializes a new test parser /// /// Maximum size the message is allowed to have /// public TestParser(int maximumMessageSize) : base(maximumMessageSize) { } /// Adds bytes to be parsed /// Array containing the bytes to be parsed /// Index at which to begin reading the array /// Number of bytes to take from the array /// True if more data is required public void ProcessBytes(byte[] bytes, int start, int count) { SetReceivedData(bytes, start, count); for(; ; ) { string line = ParseLine(); if(line == null) { break; } ++parsedLineCount; } } /// Number of lines the parser has parsed so far public int ParsedLineCount { get { return this.parsedLineCount; } } /// /// Called when the message is growing beyond the maximum message size /// /// /// An exception that will be thrown to indicate the too large message /// protected override Exception HandleMessageTooLarge() { return new InsufficientMemoryException("Message too large"); } /// /// Called when the message contains a carriage return without a line feed /// protected override void HandleLoneCarriageReturn() { throw new FormatException("Lone carriage return encountered"); } /// /// Called to scan the bytes of a potential line for invalid characters /// /// /// Array containing the bytes that to can for invalid characters /// /// Index in the array at which to begin reading /// Number of bytes from the array to scan protected override void VerifyPotentialLine(byte[] buffer, int start, int count) { for(int index = start; index < start + count; ++index) { if(buffer[index] >= 128) { throw new FormatException("Invalid character encountered"); } } } /// /// Called to transform a received series of bytes into a string /// /// Buffer containing the bytes to be transformed /// Index of the first byte to transform /// Number of bytes to transform into a string /// The string produced from the bytes in the specified buffer /// /// This method allows you to use your own encoding for transforming the bytes /// in a line into a string. Always called to transform an entire line in one /// piece, excluding the CR LF characters at the line's end. /// protected override string TransformToString(byte[] buffer, int start, int count) { return Encoding.ASCII.GetString(buffer, start, count); } /// Number of lines that line parser has parsed private int parsedLineCount; } #endregion // class TestParser /// Verifies that the default constructor of the line parser works [Test] public void LineParserHasDefaultConstructor() { new TestParser(); } /// /// Tests whether the line parser works on a single line /// [Test] public void SingleLineCanBeParsed() { TestParser parser = new TestParser(128); byte[] requestData = Encoding.ASCII.GetBytes("This is a boring test string\r\n"); parser.ProcessBytes(requestData, 0, requestData.Length); Assert.AreEqual(1, parser.ParsedLineCount); } /// /// Tests whether the line parser can cope with an incomplete line /// [Test] public void UnterminatedLinesWillBeKeptInParser() { TestParser parser = new TestParser(128); byte[] requestData = Encoding.ASCII.GetBytes("This is a boring test string"); parser.ProcessBytes(requestData, 0, requestData.Length); Assert.AreEqual(0, parser.ParsedLineCount); } /// /// Tests whether the GetRemainingData() method returns the still unparsed data /// correctly /// [Test] public void RemainingDataCanBeRetrievedFromLineParser() { byte[] binaryData = new byte[9] { 0x80, 0x81, 0x82, 0x83, 0x84, 0x85, 0x86, 0x87, 0x88 }; byte[] requestData = Encoding.ASCII.GetBytes("This is a test\r\n123456789"); for(int index = 0; index < 9; ++index) { requestData[index + 16] = binaryData[index]; } TestParser parser = new TestParser(128); parser.SetReceivedData(requestData, 0, requestData.Length); Assert.AreEqual("This is a test", parser.ParseLine()); Assert.IsTrue( arraySegmentContentsAreEqual( new ArraySegment(binaryData, 0, 9), parser.GetRemainingData() ) ); } /// /// Verifies that the Reset() fully resets the parser /// [Test] public void ResetCompletelyRevertsParserState() { byte[] beginning = Encoding.ASCII.GetBytes("This is a test\r"); byte[] ending = Encoding.ASCII.GetBytes("\nA real test\r\n"); TestParser parser = new TestParser(128); parser.SetReceivedData(beginning, 0, beginning.Length); Assert.IsNull(parser.ParseLine()); parser.Reset(); // If the Reset() didn't work, the parser would now complain that it discovered // a lone carriage return inmidst of the data stream parser.SetReceivedData(beginning, 0, beginning.Length); Assert.IsNull(parser.ParseLine()); parser.SetReceivedData(ending, 0, ending.Length); Assert.AreEqual("This is a test", parser.ParseLine()); Assert.AreEqual("A real test", parser.ParseLine()); Assert.IsNull(parser.ParseLine()); } /// /// Tests whether a lone carriage return at the end of a split buffer is /// detected correctly. /// [Test] public void LineBreaksCanBeSplitAcrossPackets() { byte[] badData = Encoding.ASCII.GetBytes("This is a test\r"); TestParser parser = new TestParser(128); // Parse a chunk of data that leaves an open-ended carriage return. No exception // should be thrown here because the next chunk the parser get might start // with a line feed, thus forming a valid line. try { parser.SetReceivedData(badData, 0, badData.Length); Assert.IsNull(parser.ParseLine()); } catch(FormatException) { Assert.Fail("Line parser complained about what could yet become a valid line"); } // Parse the same thing again. Now the parser should notice that the carriage // return wasn't followed by a line feed and legitimately complain. parser.SetReceivedData(badData, 0, badData.Length); Assert.Throws( delegate() { parser.ParseLine(); } ); } /// /// Tests parsing of a message that is just by one byte larger than the maximum /// allowed message size /// [Test] public void MessageCannotBeLargerThanMaxMessageSize() { byte[] slightlyTooLargeData = Encoding.ASCII.GetBytes( new string(' ', 63) + "\r\n" ); TestParser parser = new TestParser(64); parser.SetReceivedData(slightlyTooLargeData, 0, slightlyTooLargeData.Length); Assert.Throws( delegate() { Console.WriteLine(parser.ParseLine()); } ); } /// /// Tests parsing of a message that is just by one byte larger than the maximum /// allowed message size, split into multiple lines /// [Test] public void SplitMessageCannotBeLargerThanMaxMessageSize() { byte[] slightlyTooLargeData = Encoding.ASCII.GetBytes( new string(' ', 30) + "\r\n" + new string(' ', 30) + "\r\n" + new string(' ', 31) + "\r\n" ); TestParser parser = new TestParser(96); parser.SetReceivedData(slightlyTooLargeData, 0, slightlyTooLargeData.Length); Assert.AreEqual(new string(' ', 30), parser.ParseLine()); Assert.AreEqual(new string(' ', 30), parser.ParseLine()); Assert.Throws( delegate() { Console.WriteLine(parser.ParseLine()); } ); } /// /// Tests parsing of a message that is way too large to fit in the receive buffer /// [Test] public void WayTooLargeMessagesDontCauseProblems() { byte[] farTooLargeData = Encoding.ASCII.GetBytes(new string(' ', 128)); TestParser parser = new TestParser(64); parser.SetReceivedData(farTooLargeData, 0, farTooLargeData.Length); Assert.Throws( delegate() { Console.WriteLine(parser.ParseLine()); } ); } /// /// Tests parsing of a message that barely fits in the receive buffer /// [Test] public void BarelyFittingMessageIsStillHandled() { byte[] barelyFittingData = Encoding.ASCII.GetBytes( new string(' ', 62) + "\r\n" ); TestParser parser = new TestParser(64); parser.SetReceivedData(barelyFittingData, 0, barelyFittingData.Length); Assert.AreEqual(new string(' ', 62), parser.ParseLine()); Assert.IsNull(parser.ParseLine()); } /// /// Tests parsing of a message that contains a lone carriage return character /// [Test] public void CarriageReturnWithoutLineFeedThrowsException() { byte[] badData = Encoding.ASCII.GetBytes("First line\r\nThis is a \r test"); TestParser parser = new TestParser(64); parser.SetReceivedData(badData, 0, badData.Length); Assert.AreEqual("First line", parser.ParseLine()); Assert.Throws( delegate() { parser.ParseLine(); } ); } /// /// Tests parsing of a line split into multiple messages /// [Test] public void LineSplitAcrossManyPacketsIsHandled() { byte[] firstPart = Encoding.ASCII.GetBytes("This "); byte[] secondPart = Encoding.ASCII.GetBytes("is a "); byte[] thirdPart = Encoding.ASCII.GetBytes("test\r\n"); TestParser parser = new TestParser(64); parser.SetReceivedData(firstPart, 0, firstPart.Length); Assert.IsNull(parser.ParseLine()); parser.SetReceivedData(secondPart, 0, secondPart.Length); Assert.IsNull(parser.ParseLine()); parser.SetReceivedData(thirdPart, 0, thirdPart.Length); Assert.AreEqual("This is a test", parser.ParseLine()); Assert.IsNull(parser.ParseLine()); } /// /// Tests parsing of a message that requires the store buffer to be enlarged /// [Test] public void LargeMessagesCanBeHandled() { byte[] firstPart = Encoding.ASCII.GetBytes(new string(' ', 128)); byte[] secondPart = Encoding.ASCII.GetBytes("This is a test\r\n"); TestParser parser = new TestParser(192); parser.SetReceivedData(firstPart, 0, firstPart.Length); Assert.IsNull(parser.ParseLine()); parser.SetReceivedData(secondPart, 0, secondPart.Length); Assert.AreEqual(new string(' ', 128) + "This is a test", parser.ParseLine()); Assert.IsNull(parser.ParseLine()); } /// Compares two array segments based on their contents /// First array segment that will be compared /// Second array segment that will be compared /// /// True if the contents in both array segments are identical, otherwise false /// private static bool arraySegmentContentsAreEqual( ArraySegment firstSegment, ArraySegment secondSegment ) { // If the lengths to not match, there's no point in doing any comparison if(firstSegment.Count != secondSegment.Count) { return false; } // Compare the contents of both array segments byte by byte for(int index = 0; index < firstSegment.Count; ++index) { byte leftByte = firstSegment.Array[firstSegment.Offset + index]; byte rightByte = secondSegment.Array[secondSegment.Offset + index]; if(leftByte != rightByte) { return false; } } // No differences found, the contents of both segments must be identical return true; } } } #endif // UNITTEST