Skip to content

Commit

Permalink
Fix exception arising from parsing test name
Browse files Browse the repository at this point in the history
  • Loading branch information
CharliePoole committed Nov 30, 2024
1 parent 9ae2fe1 commit 1af35ed
Show file tree
Hide file tree
Showing 3 changed files with 176 additions and 43 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

using NUnit.Framework;
using System;
using System.Collections;
using System.Collections.Generic;
using System.Xml;

namespace NUnit.Engine.Tests
Expand All @@ -16,47 +18,19 @@ public void CreateParser()
_parser = new TestSelectionParser();
}

[TestCase("cat=Urgent", "<cat>Urgent</cat>")]
[TestCase("cat==Urgent", "<cat>Urgent</cat>")]
[TestCase("cat!=Urgent", "<not><cat>Urgent</cat></not>")]
[TestCase("cat =~ Urgent", "<cat re='1'>Urgent</cat>")]
[TestCase("cat !~ Urgent", "<not><cat re='1'>Urgent</cat></not>")]
[TestCase("cat = Urgent || cat = High", "<or><cat>Urgent</cat><cat>High</cat></or>")]
[TestCase("Priority == High", "<prop name='Priority'>High</prop>")]
[TestCase("Priority != Urgent", "<not><prop name='Priority'>Urgent</prop></not>")]
[TestCase("Author =~ Jones", "<prop name='Author' re='1'>Jones</prop>")]
[TestCase("Author !~ Jones", "<not><prop name='Author' re='1'>Jones</prop></not>")]
[TestCase("name='SomeTest'", "<name>SomeTest</name>")]
[TestCase("method=TestMethod", "<method>TestMethod</method>")]
[TestCase("method=Test1||method=Test2||method=Test3", "<or><method>Test1</method><method>Test2</method><method>Test3</method></or>")]
[TestCase("namespace=Foo", "<namespace>Foo</namespace>")]
[TestCase("namespace=Foo.Bar", "<namespace>Foo.Bar</namespace>")]
[TestCase("namespace=Foo||namespace=Bar", "<or><namespace>Foo</namespace><namespace>Bar</namespace></or>")]
[TestCase("namespace=Foo.Bar||namespace=Bar.Baz", "<or><namespace>Foo.Bar</namespace><namespace>Bar.Baz</namespace></or>")]
[TestCase("test='My.Test.Fixture.Method(42)'", "<test>My.Test.Fixture.Method(42)</test>")]
[TestCase("test='My.Test.Fixture.Method(\"xyz\")'", "<test>My.Test.Fixture.Method(&quot;xyz&quot;)</test>")]
[TestCase("test='My.Test.Fixture.Method(\"abc\\'s\")'", "<test>My.Test.Fixture.Method(&quot;abc&apos;s&quot;)</test>")]
[TestCase("test='My.Test.Fixture.Method(\"x&y&z\")'", "<test>My.Test.Fixture.Method(&quot;x&amp;y&amp;z&quot;)</test>")]
[TestCase("test='My.Test.Fixture.Method(\"<xyz>\")'", "<test>My.Test.Fixture.Method(&quot;&lt;xyz&gt;&quot;)</test>")]
[TestCase("test=='Issue1510.TestSomething(Option1,\"ABC\")'", "<test>Issue1510.TestSomething(Option1,&quot;ABC&quot;)</test>")]
[TestCase("cat==Urgent && test=='My.Tests'", "<and><cat>Urgent</cat><test>My.Tests</test></and>")]
[TestCase("cat==Urgent and test=='My.Tests'", "<and><cat>Urgent</cat><test>My.Tests</test></and>")]
[TestCase("cat==Urgent || test=='My.Tests'", "<or><cat>Urgent</cat><test>My.Tests</test></or>")]
[TestCase("cat==Urgent or test=='My.Tests'", "<or><cat>Urgent</cat><test>My.Tests</test></or>")]
[TestCase("cat==Urgent || test=='My.Tests' && cat == high", "<or><cat>Urgent</cat><and><test>My.Tests</test><cat>high</cat></and></or>")]
[TestCase("cat==Urgent && test=='My.Tests' || cat == high", "<or><and><cat>Urgent</cat><test>My.Tests</test></and><cat>high</cat></or>")]
[TestCase("cat==Urgent && (test=='My.Tests' || cat == high)", "<and><cat>Urgent</cat><or><test>My.Tests</test><cat>high</cat></or></and>")]
[TestCase("cat==Urgent && !(test=='My.Tests' || cat == high)", "<and><cat>Urgent</cat><not><or><test>My.Tests</test><cat>high</cat></or></not></and>")]
[TestCase("!(test!='My.Tests')", "<not><not><test>My.Tests</test></not></not>")]
[TestCase("!(cat!=Urgent)", "<not><not><cat>Urgent</cat></not></not>")]
public void TestParser(string input, string output)
[TestCaseSource(nameof(UniqueOutputs))]
public void AllOutputsAreValidXml(string output)
{
Assert.That(_parser.Parse(input), Is.EqualTo(output));

XmlDocument doc = new XmlDocument();
Assert.DoesNotThrow(() => doc.LoadXml(output));
}

[TestCaseSource(nameof(ParserTestCases))]
public void TestParser(string input, string output)
{
Assert.That(_parser.Parse(input), Is.EqualTo(output));
}

[TestCase(null, typeof(ArgumentNullException))]
[TestCase("", typeof(TestSelectionParserException))]
[TestCase(" ", typeof(TestSelectionParserException))]
Expand All @@ -65,5 +39,84 @@ public void TestParser_InvalidInput(string input, Type type)
{
Assert.That(() => _parser.Parse(input), Throws.TypeOf(type));
}

private static readonly TestCaseData[] ParserTestCases = new[]
{
// Category Filter
new TestCaseData("cat=Urgent", "<cat>Urgent</cat>"),
new TestCaseData("cat=/Urgent/", "<cat>Urgent</cat>"),
new TestCaseData("cat='Urgent'", "<cat>Urgent</cat>"),
new TestCaseData("cat==Urgent", "<cat>Urgent</cat>"),
new TestCaseData("cat!=Urgent", "<not><cat>Urgent</cat></not>"),
new TestCaseData("cat =~ Urgent", "<cat re='1'>Urgent</cat>"),
new TestCaseData("cat !~ Urgent", "<not><cat re='1'>Urgent</cat></not>"),
// Property Filter
new TestCaseData("Priority == High", "<prop name='Priority'>High</prop>"),
new TestCaseData("Priority != Urgent", "<not><prop name='Priority'>Urgent</prop></not>"),
new TestCaseData("Author =~ Jones", "<prop name='Author' re='1'>Jones</prop>"),
new TestCaseData("Author !~ Jones", "<not><prop name='Author' re='1'>Jones</prop></not>"),
// Name Filter
new TestCaseData("name='SomeTest'", "<name>SomeTest</name>"),
// Method Filter
new TestCaseData("method=TestMethod", "<method>TestMethod</method>"),
new TestCaseData("method=Test1||method=Test2||method=Test3", "<or><method>Test1</method><method>Test2</method><method>Test3</method></or>"),
// Namespace Filter
new TestCaseData("namespace=Foo", "<namespace>Foo</namespace>"),
new TestCaseData("namespace=Foo.Bar", "<namespace>Foo.Bar</namespace>"),
new TestCaseData("namespace=Foo||namespace=Bar", "<or><namespace>Foo</namespace><namespace>Bar</namespace></or>"),
new TestCaseData("namespace=Foo.Bar||namespace=Bar.Baz", "<or><namespace>Foo.Bar</namespace><namespace>Bar.Baz</namespace></or>"),
// Test Filter
new TestCaseData("test='My.Test.Fixture.Method(42)'", "<test>My.Test.Fixture.Method(42)</test>"),
new TestCaseData("test='My.Test.Fixture.Method(\"xyz\")'", "<test>My.Test.Fixture.Method(&quot;xyz&quot;)</test>"),
new TestCaseData("test='My.Test.Fixture.Method(\"abc\\'s\")'", "<test>My.Test.Fixture.Method(&quot;abc&apos;s&quot;)</test>"),
new TestCaseData("test='My.Test.Fixture.Method(\"x&y&z\")'", "<test>My.Test.Fixture.Method(&quot;x&amp;y&amp;z&quot;)</test>"),
new TestCaseData("test='My.Test.Fixture.Method(\"<xyz>\")'", "<test>My.Test.Fixture.Method(&quot;&lt;xyz&gt;&quot;)</test>"),
new TestCaseData("test=='Issue1510.TestSomething ( Option1 , \"ABC\" ) '", "<test>Issue1510.TestSomething(Option1,&quot;ABC&quot;)</test>"),
new TestCaseData("test=='Issue1510.TestSomething ( Option1 , \"A B C\" ) '", "<test>Issue1510.TestSomething(Option1,&quot;A B C&quot;)</test>"),
new TestCaseData("test=/My.Test.Fixture.Method(42)/", "<test>My.Test.Fixture.Method(42)</test>"),
new TestCaseData("test=/My.Test.Fixture.Method(\"xyz\")/", "<test>My.Test.Fixture.Method(&quot;xyz&quot;)</test>"),
new TestCaseData("test=/My.Test.Fixture.Method(\"abc\\'s\")/", "<test>My.Test.Fixture.Method(&quot;abc&apos;s&quot;)</test>"),
new TestCaseData("test=/My.Test.Fixture.Method(\"x&y&z\")/", "<test>My.Test.Fixture.Method(&quot;x&amp;y&amp;z&quot;)</test>"),
new TestCaseData("test=/My.Test.Fixture.Method(\"<xyz>\")/", "<test>My.Test.Fixture.Method(&quot;&lt;xyz&gt;&quot;)</test>"),
new TestCaseData("test==/Issue1510.TestSomething ( Option1 , \"ABC\" ) /", "<test>Issue1510.TestSomething(Option1,&quot;ABC&quot;)</test>"),
new TestCaseData("test==/Issue1510.TestSomething ( Option1 , \"A B C\" ) /", "<test>Issue1510.TestSomething(Option1,&quot;A B C&quot;)</test>"),
new TestCaseData("test=My.Test.Fixture.Method(42)", "<test>My.Test.Fixture.Method(42)</test>"),
new TestCaseData("test=My.Test.Fixture.Method(\"xyz\")", "<test>My.Test.Fixture.Method(&quot;xyz&quot;)</test>"),
new TestCaseData("test=My.Test.Fixture.Method(\"abc\\'s\")", "<test>My.Test.Fixture.Method(&quot;abc&apos;s&quot;)</test>"),
new TestCaseData("test=My.Test.Fixture.Method(\"x&y&z\")", "<test>My.Test.Fixture.Method(&quot;x&amp;y&amp;z&quot;)</test>"),
new TestCaseData("test=My.Test.Fixture.Method(\"<xyz>\")", "<test>My.Test.Fixture.Method(&quot;&lt;xyz&gt;&quot;)</test>"),
new TestCaseData("test==Issue1510.TestSomething ( Option1 , \"ABC\" ) ", "<test>Issue1510.TestSomething(Option1,&quot;ABC&quot;)</test>"),
new TestCaseData("test==Issue1510.TestSomething ( Option1 , \"A B C\" ) ", "<test>Issue1510.TestSomething(Option1,&quot;A B C&quot;)</test>"),
// And Filter
new TestCaseData("cat==Urgent && test=='My.Tests'", "<and><cat>Urgent</cat><test>My.Tests</test></and>"),
new TestCaseData("cat==Urgent and test=='My.Tests'", "<and><cat>Urgent</cat><test>My.Tests</test></and>"),
// Or Filter
new TestCaseData("cat==Urgent || test=='My.Tests'", "<or><cat>Urgent</cat><test>My.Tests</test></or>"),
new TestCaseData("cat==Urgent or test=='My.Tests'", "<or><cat>Urgent</cat><test>My.Tests</test></or>"),
// Mixed And Filter with Or Filter
new TestCaseData("cat = Urgent || cat = High", "<or><cat>Urgent</cat><cat>High</cat></or>"),
new TestCaseData("cat==Urgent || test=='My.Tests' && cat == high", "<or><cat>Urgent</cat><and><test>My.Tests</test><cat>high</cat></and></or>"),
new TestCaseData("cat==Urgent && test=='My.Tests' || cat == high", "<or><and><cat>Urgent</cat><test>My.Tests</test></and><cat>high</cat></or>"),
new TestCaseData("cat==Urgent && (test=='My.Tests' || cat == high)", "<and><cat>Urgent</cat><or><test>My.Tests</test><cat>high</cat></or></and>"),
new TestCaseData("cat==Urgent && !(test=='My.Tests' || cat == high)", "<and><cat>Urgent</cat><not><or><test>My.Tests</test><cat>high</cat></or></not></and>"),
// Not Filter
new TestCaseData("!(test!='My.Tests')", "<not><not><test>My.Tests</test></not></not>"),
new TestCaseData("!(cat!=Urgent)", "<not><not><cat>Urgent</cat></not></not>")
};

private static IEnumerable<string> UniqueOutputs()
{
List<string> alreadyReturned = new List<string>();

foreach (var testCase in ParserTestCases)
{
var output = testCase.Arguments[1] as string;
if (!alreadyReturned.Contains(output))
{
alreadyReturned.Add(output);
yield return output;
}
}
}
}
}
91 changes: 85 additions & 6 deletions src/NUnitEngine/nunit.engine/Services/TestSelectionParser.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

using System;
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using System.Text;

// Missing XML Docs
Expand Down Expand Up @@ -37,6 +38,7 @@ public class TestSelectionParser
private static readonly Token[] REL_OPS = new Token[] { EQ_OP1, EQ_OP2, NE_OP, MATCH_OP, NOMATCH_OP };

private static readonly Token EOF = new Token(TokenKind.Eof);
private static readonly Token COMMA = new Token(TokenKind.Symbol, ",");

public string Parse(string input)
{
Expand Down Expand Up @@ -116,21 +118,29 @@ public string ParseFilterElement()
return ParseExpressionInParentheses();

Token lhs = Expect(TokenKind.Word);
Token op;
Token rhs;

switch (lhs.Text)
{
case "id":
case "test":
op = Expect(REL_OPS);
rhs = GetTestName();
return EmitFilterElement(lhs, op, rhs);

case "cat":
case "method":
case "class":
case "name":
case "test":
case "namespace":
case "partition":
Token op = lhs.Text == "id"
? Expect(EQ_OPS)
: Expect(REL_OPS);
Token rhs = Expect(TokenKind.String, TokenKind.Word);
op = Expect(REL_OPS);
rhs = Expect(TokenKind.String, TokenKind.Word);
return EmitFilterElement(lhs, op, rhs);

case "id":
op = Expect(EQ_OPS);
rhs = Expect(TokenKind.String, TokenKind.Word);
return EmitFilterElement(lhs, op, rhs);

default:
Expand All @@ -142,6 +152,75 @@ public string ParseFilterElement()
}
}

// TODO: We do extra work for test names due to the fact that
// Windows drops double quotes from arguments in many situations.
// It would be better to parse the command-line directly but
// that will mean a significant rewrite.
private Token GetTestName()
{
var result = Expect(TokenKind.String, TokenKind.Word);
var sb = new StringBuilder();

if (result.Kind == TokenKind.String)
{
int index = result.Text.IndexOf('(');

if (index < 0)
return result;

// Remove white space around arguments
string testName = result.Text;
sb = new StringBuilder(testName.Substring(0, index).Trim());
sb.Append('(');
bool done = false;

while (++index < testName.Length && !done)
{
char ch = testName[index];
switch (ch)
{
case '"':
sb.Append(ch);
while (++index < testName.Length && testName[index] != '"')
sb.Append(testName[index]);
sb.Append('"');
break;
case ' ':
break;
default:
sb.Append(ch);
done = ch == ')';
break;
}
}
}
else
{
// Word Token - check to see if it's followed by a left parenthesis
if (_tokenizer.LookAhead != LPAREN) return result;

// We have a "Word" token followed by a left parenthesis
// This may be a testname entered without quotes or one
// using double quotes, which were removed by the shell.

sb = new StringBuilder(result.Text);
var token = NextToken();

while (token != EOF)
{
bool isString = token.Kind == TokenKind.String;

if (isString) sb.Append('"');
sb.Append(token.Text);
if (isString) sb.Append('"');

token = NextToken();
}
}

return new Token(TokenKind.String, sb.ToString());
}

private static string EmitFilterElement(Token lhs, Token op, Token rhs)
{
string fmt = null;
Expand Down
3 changes: 2 additions & 1 deletion src/NUnitEngine/nunit.engine/Services/Tokenizer.cs
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ public class Tokenizer
private int _index;

private const char EOF_CHAR = '\0';
private const string WORD_BREAK_CHARS = "=!()&|";
private const string WORD_BREAK_CHARS = "=!()&| \t,";
private readonly string[] DOUBLE_CHAR_SYMBOLS = new string[] { "==", "=~", "!=", "!~", "&&", "||" };

private Token _lookahead;
Expand Down Expand Up @@ -130,6 +130,7 @@ private Token GetNextToken()
// Single char symbols
case '(':
case ')':
case ',':
GetChar();
return new Token(TokenKind.Symbol, ch) { Pos = pos };

Expand Down

0 comments on commit 1af35ed

Please sign in to comment.