#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