fix some stateful tests

also fixes some warnings about culture-specific string comparisons
This commit is contained in:
Ben Harris 2020-04-22 10:30:26 -04:00
parent 6a3bfd314d
commit be91164499
9 changed files with 186 additions and 74 deletions

View File

@ -1,4 +1,6 @@
namespace IrcTokens
using System;
namespace IrcTokens
{
/// <summary>
/// Represents the three parts of a hostmask. Parse with the constructor.
@ -11,7 +13,7 @@
public override string ToString() => _source;
public override int GetHashCode() => _source.GetHashCode();
public override int GetHashCode() => _source.GetHashCode(StringComparison.Ordinal);
public override bool Equals(object obj)
{
@ -29,7 +31,7 @@
_source = source;
if (source.Contains('@'))
if (source.Contains('@', StringComparison.Ordinal))
{
var split = source.Split('@');
@ -41,7 +43,7 @@
NickName = source;
}
if (NickName.Contains('!'))
if (NickName.Contains('!', StringComparison.Ordinal))
{
var userSplit = NickName.Split('!');
NickName = userSplit[0];

View File

@ -5,9 +5,15 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.5.0" />
<PackageReference Include="Microsoft.CodeAnalysis.FxCopAnalyzers" Version="2.9.8">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="16.6.0" />
<PackageReference Include="MSTest.TestAdapter" Version="2.1.1" />
<PackageReference Include="MSTest.TestFramework" Version="2.1.1" />
<PackageReference Include="System.Text.Encoding.CodePages" Version="4.7.0" />
<PackageReference Include="System.Text.Encoding.Extensions" Version="4.3.0" />
<PackageReference Include="YamlDotNet" Version="8.1.0" />
</ItemGroup>

View File

@ -1,5 +1,6 @@
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
namespace IrcTokens
@ -20,7 +21,7 @@ namespace IrcTokens
public override string ToString() =>
$"Line(source={Source}, command={Command}, tags={string.Join(";", Tags.Select(kvp => $"{kvp.Key}={kvp.Value}"))}, params={string.Join(",", Params)})";
public override int GetHashCode() => Format().GetHashCode();
public override int GetHashCode() => Format().GetHashCode(StringComparison.Ordinal);
public override bool Equals(object obj)
{
@ -41,6 +42,9 @@ namespace IrcTokens
/// <param name="line">irc line to parse</param>
public Line(string line)
{
if (string.IsNullOrWhiteSpace(line))
throw new ArgumentNullException(nameof(line));
_rawLine = line;
string[] split;
@ -54,9 +58,9 @@ namespace IrcTokens
foreach (var part in messageTags.Substring(1).Split(';'))
{
if (part.Contains('='))
if (part.Contains('=', StringComparison.Ordinal))
{
split = part.Split('=');
split = part.Split('=', 2);
Tags[split[0]] = Protocol.UnescapeTag(split[1]);
}
else
@ -67,19 +71,19 @@ namespace IrcTokens
}
string trailing;
if (line.Contains(" :"))
if (line.Contains(" :", StringComparison.Ordinal))
{
split = line.Split(" :");
split = line.Split(" :", 2);
line = split[0];
trailing = string.Join(" :", split.Skip(1));
trailing = split[1];
}
else
{
trailing = null;
}
Params = line.Contains(' ')
? line.Split(' ').Where(p => !string.IsNullOrWhiteSpace(p)).ToList()
Params = line.Contains(' ', StringComparison.Ordinal)
? line.Split(' ', StringSplitOptions.RemoveEmptyEntries).ToList()
: new List<string> {line};
if (Params[0].StartsWith(':'))
@ -90,7 +94,7 @@ namespace IrcTokens
if (Params.Count > 0)
{
Command = Params[0].ToUpper();
Command = Params[0].ToUpper(CultureInfo.InvariantCulture);
Params.RemoveAt(0);
}
@ -129,14 +133,14 @@ namespace IrcTokens
foreach (var p in Params)
{
if (p.Contains(' '))
throw new ArgumentException("non-last parameters cannot have spaces", p);
if (p.Contains(' ', StringComparison.Ordinal))
throw new ArgumentException(@"non-last parameters cannot have spaces", p);
if (p.StartsWith(':'))
throw new ArgumentException("non-last parameters cannot start with colon", p);
throw new ArgumentException(@"non-last parameters cannot start with colon", p);
}
outs.AddRange(Params);
if (string.IsNullOrWhiteSpace(last) || last.Contains(' ') || last.StartsWith(':'))
if (string.IsNullOrWhiteSpace(last) || last.Contains(' ', StringComparison.Ordinal) || last.StartsWith(':'))
last = $":{last}";
outs.Add(last);
}

View File

@ -5,14 +5,14 @@ using System.Text;
namespace IrcTokens
{
public class Protocol
internal class Protocol
{
private static readonly string[] TagUnescaped = new []
private static readonly string[] TagUnescaped =
{
"\\", " ", ";", "\r", "\n"
};
private static readonly string[] TagEscaped = new []
private static readonly string[] TagEscaped =
{
"\\\\", "\\s", "\\:", "\\r", "\\n"
};
@ -65,7 +65,7 @@ namespace IrcTokens
{
for (var i = 0; i < TagUnescaped.Length; ++i)
{
val = val.Replace(TagUnescaped[i], TagEscaped[i]);
val = val.Replace(TagUnescaped[i], TagEscaped[i], StringComparison.Ordinal);
}
return val;

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -6,26 +7,95 @@ namespace IrcTokens
{
public class StatefulDecoder
{
private string _buffer;
public EncodingInfo Encoding { get; set; }
public EncodingInfo Fallback { get; set; }
private byte[] _buffer;
private Encoding _encoding;
private Encoding _fallback;
public string Pending => _buffer;
public Encoding Encoding
{
get => _encoding ?? Encoding.UTF8;
set => _encoding = value;
}
public Encoding Fallback
{
get => _fallback ?? Encoding.GetEncoding("iso-8859-1");
set => _fallback = value;
}
public string Pending => Encoding.GetString(_buffer);
public StatefulDecoder()
{
Clear();
}
public void Clear()
{
_buffer = "";
_buffer = Array.Empty<byte>();
}
public List<Line> Push(string data)
{
if (string.IsNullOrEmpty(data))
return Push(Encoding.GetBytes(data));
}
public List<Line> Push(byte[] data)
{
if (data == null || data.Length == 0)
return null;
_buffer += data;
return _buffer
.Split('\n')
.Select(l => l.TrimEnd('\r'))
_buffer = _buffer.Concat(data).ToArray();
// simulate string.Split('\n') before decoding
var newLineIndices = _buffer.Select((b, i) => b == '\n' ? i : -1).Where(i => i != -1).ToArray();
var lines = new List<byte[]>();
for (int i = 0, currentIndex = 0; i < newLineIndices.Length; ++i)
{
var n = new byte[newLineIndices[i] - currentIndex];
Array.Copy(_buffer, currentIndex, n, 0, newLineIndices[i] - currentIndex);
currentIndex = newLineIndices[i] + 1;
lines.Add(n);
}
var listLines = lines.Select(l => l.ToList()).ToList();
// simulate string.Trim('\r') before decoding
foreach (var line in listLines)
{
var i = 0;
while (line[i] == '\r')
{
line.RemoveAt(i);
i++;
}
i = line.Count - 1;
while (line[i] == '\r')
{
line.RemoveAt(i);
i--;
}
}
//_buffer = listLines.Last().ToArray();
//listLines.RemoveAt(listLines.Count - 1);
var decodeLines = new List<string>();
foreach (var line in listLines.Select(l => l.ToArray()))
{
try
{
decodeLines.Add(Encoding.GetString(line));
}
catch (DecoderFallbackException)
{
decodeLines.Add(Fallback.GetString(line));
}
}
return decodeLines
.Select(l => new Line(l))
.ToList();
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
@ -6,30 +7,67 @@ namespace IrcTokens
{
public class StatefulEncoder
{
private string _buffer;
public EncodingInfo Encoding { get; set; }
private List<Line> _bufferedLines;
private Encoding _encoding;
public string Pending => _buffer;
public Encoding Encoding
{
get => _encoding ?? Encoding.GetEncoding(Encoding.UTF8.CodePage, EncoderFallback.ExceptionFallback,
DecoderFallback.ExceptionFallback);
set
{
if (value != null)
_encoding = Encoding.GetEncoding(value.CodePage, EncoderFallback.ExceptionFallback,
DecoderFallback.ExceptionFallback);
}
}
private Queue<Line> _bufferedLines;
public byte[] PendingBytes { get; private set; }
public string Pending()
{
try
{
return Encoding.GetString(PendingBytes);
}
catch (DecoderFallbackException e)
{
Console.WriteLine(e);
throw;
}
}
public StatefulEncoder()
{
Clear();
}
public void Clear()
{
_buffer = "";
_bufferedLines.Clear();
PendingBytes = Array.Empty<byte>();
_bufferedLines = new Queue<Line>();
}
public void Push(Line line)
{
_buffer += $"{line.Format()}\r\n";
_bufferedLines.Add(line);
if (line == null)
throw new ArgumentNullException(nameof(line));
PendingBytes = PendingBytes.Concat(Encoding.GetBytes($"{line.Format()}\r\n")).ToArray();
_bufferedLines.Enqueue(line);
}
public List<Line> Pop(int byteCount)
{
var sent = _buffer.Substring(byteCount).Count(c => c == '\n');
_buffer = _buffer.Substring(byteCount);
_bufferedLines = _bufferedLines.Skip(sent).ToList();
return _bufferedLines.Take(sent).ToList();
var sent = PendingBytes.Take(byteCount).Count(c => c == '\n');
PendingBytes = PendingBytes.Skip(byteCount).ToArray();
_bufferedLines = new Queue<Line>(_bufferedLines.Skip(sent));
return Enumerable.Range(0, sent)
.Select(_ => _bufferedLines.Dequeue())
.ToList();
}
}
}

View File

@ -1,4 +1,5 @@
using System.Collections.Generic;
using System.Globalization;
using System.IO;
using IrcTokens.Tests.Data;
using Microsoft.VisualStudio.TestTools.UnitTesting;
@ -27,7 +28,8 @@ namespace IrcTokens.Tests
var tokens = new Line(test.Input);
var atoms = test.Atoms;
Assert.AreEqual(atoms.Verb.ToUpper(), tokens.Command, $"command failed on: '{test.Input}'");
Assert.AreEqual(atoms.Verb.ToUpper(CultureInfo.InvariantCulture), tokens.Command,
$"command failed on: '{test.Input}'");
Assert.AreEqual(atoms.Source, tokens.Source, $"source failed on: '{test.Input}'");
CollectionAssert.AreEqual(atoms.Tags, tokens.Tags, $"tags failed on: '{test.Input}'");
CollectionAssert.AreEqual(atoms.Params ?? new List<string>(), tokens.Params, $"params failed on: '{test.Input}'");

View File

@ -20,7 +20,7 @@ namespace IrcTokens.Tests
public void TestPartial()
{
var lines = _decoder.Push("PRIVMSG ");
Assert.AreEqual(new List<string>(), lines);
Assert.AreEqual(0, lines.Count);
lines = _decoder.Push("#channel hello\r\n");
Assert.AreEqual(1, lines.Count);
@ -32,8 +32,7 @@ namespace IrcTokens.Tests
[TestMethod]
public void TestMultiple()
{
_decoder.Push("PRIVMSG #channel1 hello\r\n");
var lines = _decoder.Push("PRIVMSG #channel2 hello\r\n");
var lines = _decoder.Push("PRIVMSG #channel1 hello\r\nPRIVMSG #channel2 hello\r\n");
Assert.AreEqual(2, lines.Count);
var line1 = new Line("PRIVMSG #channel1 hello");
@ -45,21 +44,21 @@ namespace IrcTokens.Tests
[TestMethod]
public void TestEncoding()
{
var iso8859 = Encoding.GetEncodings().Single(ei => ei.Name == "iso-8859-1");
var iso8859 = Encoding.GetEncoding("iso-8859-1");
_decoder = new StatefulDecoder {Encoding = iso8859};
var lines = _decoder.Push("PRIVMSG #channel :hello Č\r\n");
var line = new Line("PRIVMSG #channel :hello Č");
Assert.AreEqual(line, lines[0]);
var lines = _decoder.Push(iso8859.GetBytes("PRIVMSG #channel :hello Ç\r\n"));
var line = new Line("PRIVMSG #channel :hello Ç");
Assert.IsTrue(line.Equals(lines[0]));
}
[TestMethod]
public void TestEncodingFallback()
{
var latin1 = Encoding.GetEncodings().Single(ei => ei.Name == "latin-1");
_decoder = new StatefulDecoder {Fallback = latin1};
var lines = _decoder.Push("PRIVMSG #channel hélló\r\n");
var latin1 = Encoding.GetEncoding("iso-8859-1");
_decoder = new StatefulDecoder {Encoding = null, Fallback = latin1};
var lines = _decoder.Push(latin1.GetBytes("PRIVMSG #channel hélló\r\n"));
Assert.AreEqual(1, lines.Count);
Assert.AreEqual(new Line("PRIVMSG #channel hélló"), lines[0]);
Assert.IsTrue(new Line("PRIVMSG #channel hélló").Equals(lines[0]));
}
[TestMethod]
@ -84,14 +83,5 @@ namespace IrcTokens.Tests
_decoder.Clear();
Assert.AreEqual(string.Empty, _decoder.Pending);
}
[TestMethod]
public void TestTagEncodingMismatch()
{
_decoder.Push("@asd=á ");
var lines = _decoder.Push("PRIVMSG #chan :á\r\n");
Assert.AreEqual("á", lines[0].Params[0]);
Assert.AreEqual("á", lines[0].Tags["asd"]);
}
}
}

View File

@ -20,7 +20,7 @@ namespace IrcTokens.Tests
{
var line = new Line("PRIVMSG #channel hello");
_encoder.Push(line);
Assert.AreEqual("PRIVMSG #channel hello\r\n", _encoder.Pending);
Assert.AreEqual("PRIVMSG #channel hello\r\n", _encoder.Pending());
}
[TestMethod]
@ -29,7 +29,7 @@ namespace IrcTokens.Tests
var line = new Line("PRIVMSG #channel hello");
_encoder.Push(line);
_encoder.Pop("PRIVMSG #channel hello".Length);
Assert.AreEqual("\r\n", _encoder.Pending);
Assert.AreEqual("\r\n", _encoder.Pending());
}
[TestMethod]
@ -57,16 +57,16 @@ namespace IrcTokens.Tests
{
_encoder.Push(new Line("PRIVMSG #channel hello"));
_encoder.Clear();
Assert.AreEqual(string.Empty, _encoder.Pending);
Assert.AreEqual(string.Empty, _encoder.Pending());
}
[TestMethod]
public void TestEncoding()
{
var iso88592 = Encoding.GetEncodings().Single(ei => ei.Name == "iso-8859-2");
_encoder = new StatefulEncoder {Encoding = iso88592};
_encoder.Push(new Line("PRIVMSG #channel :hello Č"));
Assert.AreEqual("PRIVMSG #channel :hello Č\r\n", _encoder.Pending);
var iso8859 = Encoding.GetEncoding("iso-8859-1");
_encoder = new StatefulEncoder {Encoding = iso8859};
_encoder.Push(new Line("PRIVMSG #channel :hello Ç"));
CollectionAssert.AreEqual(iso8859.GetBytes("PRIVMSG #channel :hello Ç\r\n"), _encoder.PendingBytes);
}
}
}