Skip to content

Commit 1af35ed

Browse files
committed
Fix exception arising from parsing test name
1 parent 9ae2fe1 commit 1af35ed

File tree

3 files changed

+176
-43
lines changed

3 files changed

+176
-43
lines changed

src/NUnitEngine/nunit.engine.tests/Services/TestSelectionParserTests.cs

Lines changed: 89 additions & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
using NUnit.Framework;
44
using System;
5+
using System.Collections;
6+
using System.Collections.Generic;
57
using System.Xml;
68

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

19-
[TestCase("cat=Urgent", "<cat>Urgent</cat>")]
20-
[TestCase("cat==Urgent", "<cat>Urgent</cat>")]
21-
[TestCase("cat!=Urgent", "<not><cat>Urgent</cat></not>")]
22-
[TestCase("cat =~ Urgent", "<cat re='1'>Urgent</cat>")]
23-
[TestCase("cat !~ Urgent", "<not><cat re='1'>Urgent</cat></not>")]
24-
[TestCase("cat = Urgent || cat = High", "<or><cat>Urgent</cat><cat>High</cat></or>")]
25-
[TestCase("Priority == High", "<prop name='Priority'>High</prop>")]
26-
[TestCase("Priority != Urgent", "<not><prop name='Priority'>Urgent</prop></not>")]
27-
[TestCase("Author =~ Jones", "<prop name='Author' re='1'>Jones</prop>")]
28-
[TestCase("Author !~ Jones", "<not><prop name='Author' re='1'>Jones</prop></not>")]
29-
[TestCase("name='SomeTest'", "<name>SomeTest</name>")]
30-
[TestCase("method=TestMethod", "<method>TestMethod</method>")]
31-
[TestCase("method=Test1||method=Test2||method=Test3", "<or><method>Test1</method><method>Test2</method><method>Test3</method></or>")]
32-
[TestCase("namespace=Foo", "<namespace>Foo</namespace>")]
33-
[TestCase("namespace=Foo.Bar", "<namespace>Foo.Bar</namespace>")]
34-
[TestCase("namespace=Foo||namespace=Bar", "<or><namespace>Foo</namespace><namespace>Bar</namespace></or>")]
35-
[TestCase("namespace=Foo.Bar||namespace=Bar.Baz", "<or><namespace>Foo.Bar</namespace><namespace>Bar.Baz</namespace></or>")]
36-
[TestCase("test='My.Test.Fixture.Method(42)'", "<test>My.Test.Fixture.Method(42)</test>")]
37-
[TestCase("test='My.Test.Fixture.Method(\"xyz\")'", "<test>My.Test.Fixture.Method(&quot;xyz&quot;)</test>")]
38-
[TestCase("test='My.Test.Fixture.Method(\"abc\\'s\")'", "<test>My.Test.Fixture.Method(&quot;abc&apos;s&quot;)</test>")]
39-
[TestCase("test='My.Test.Fixture.Method(\"x&y&z\")'", "<test>My.Test.Fixture.Method(&quot;x&amp;y&amp;z&quot;)</test>")]
40-
[TestCase("test='My.Test.Fixture.Method(\"<xyz>\")'", "<test>My.Test.Fixture.Method(&quot;&lt;xyz&gt;&quot;)</test>")]
41-
[TestCase("test=='Issue1510.TestSomething(Option1,\"ABC\")'", "<test>Issue1510.TestSomething(Option1,&quot;ABC&quot;)</test>")]
42-
[TestCase("cat==Urgent && test=='My.Tests'", "<and><cat>Urgent</cat><test>My.Tests</test></and>")]
43-
[TestCase("cat==Urgent and test=='My.Tests'", "<and><cat>Urgent</cat><test>My.Tests</test></and>")]
44-
[TestCase("cat==Urgent || test=='My.Tests'", "<or><cat>Urgent</cat><test>My.Tests</test></or>")]
45-
[TestCase("cat==Urgent or test=='My.Tests'", "<or><cat>Urgent</cat><test>My.Tests</test></or>")]
46-
[TestCase("cat==Urgent || test=='My.Tests' && cat == high", "<or><cat>Urgent</cat><and><test>My.Tests</test><cat>high</cat></and></or>")]
47-
[TestCase("cat==Urgent && test=='My.Tests' || cat == high", "<or><and><cat>Urgent</cat><test>My.Tests</test></and><cat>high</cat></or>")]
48-
[TestCase("cat==Urgent && (test=='My.Tests' || cat == high)", "<and><cat>Urgent</cat><or><test>My.Tests</test><cat>high</cat></or></and>")]
49-
[TestCase("cat==Urgent && !(test=='My.Tests' || cat == high)", "<and><cat>Urgent</cat><not><or><test>My.Tests</test><cat>high</cat></or></not></and>")]
50-
[TestCase("!(test!='My.Tests')", "<not><not><test>My.Tests</test></not></not>")]
51-
[TestCase("!(cat!=Urgent)", "<not><not><cat>Urgent</cat></not></not>")]
52-
public void TestParser(string input, string output)
21+
[TestCaseSource(nameof(UniqueOutputs))]
22+
public void AllOutputsAreValidXml(string output)
5323
{
54-
Assert.That(_parser.Parse(input), Is.EqualTo(output));
55-
5624
XmlDocument doc = new XmlDocument();
5725
Assert.DoesNotThrow(() => doc.LoadXml(output));
5826
}
5927

28+
[TestCaseSource(nameof(ParserTestCases))]
29+
public void TestParser(string input, string output)
30+
{
31+
Assert.That(_parser.Parse(input), Is.EqualTo(output));
32+
}
33+
6034
[TestCase(null, typeof(ArgumentNullException))]
6135
[TestCase("", typeof(TestSelectionParserException))]
6236
[TestCase(" ", typeof(TestSelectionParserException))]
@@ -65,5 +39,84 @@ public void TestParser_InvalidInput(string input, Type type)
6539
{
6640
Assert.That(() => _parser.Parse(input), Throws.TypeOf(type));
6741
}
42+
43+
private static readonly TestCaseData[] ParserTestCases = new[]
44+
{
45+
// Category Filter
46+
new TestCaseData("cat=Urgent", "<cat>Urgent</cat>"),
47+
new TestCaseData("cat=/Urgent/", "<cat>Urgent</cat>"),
48+
new TestCaseData("cat='Urgent'", "<cat>Urgent</cat>"),
49+
new TestCaseData("cat==Urgent", "<cat>Urgent</cat>"),
50+
new TestCaseData("cat!=Urgent", "<not><cat>Urgent</cat></not>"),
51+
new TestCaseData("cat =~ Urgent", "<cat re='1'>Urgent</cat>"),
52+
new TestCaseData("cat !~ Urgent", "<not><cat re='1'>Urgent</cat></not>"),
53+
// Property Filter
54+
new TestCaseData("Priority == High", "<prop name='Priority'>High</prop>"),
55+
new TestCaseData("Priority != Urgent", "<not><prop name='Priority'>Urgent</prop></not>"),
56+
new TestCaseData("Author =~ Jones", "<prop name='Author' re='1'>Jones</prop>"),
57+
new TestCaseData("Author !~ Jones", "<not><prop name='Author' re='1'>Jones</prop></not>"),
58+
// Name Filter
59+
new TestCaseData("name='SomeTest'", "<name>SomeTest</name>"),
60+
// Method Filter
61+
new TestCaseData("method=TestMethod", "<method>TestMethod</method>"),
62+
new TestCaseData("method=Test1||method=Test2||method=Test3", "<or><method>Test1</method><method>Test2</method><method>Test3</method></or>"),
63+
// Namespace Filter
64+
new TestCaseData("namespace=Foo", "<namespace>Foo</namespace>"),
65+
new TestCaseData("namespace=Foo.Bar", "<namespace>Foo.Bar</namespace>"),
66+
new TestCaseData("namespace=Foo||namespace=Bar", "<or><namespace>Foo</namespace><namespace>Bar</namespace></or>"),
67+
new TestCaseData("namespace=Foo.Bar||namespace=Bar.Baz", "<or><namespace>Foo.Bar</namespace><namespace>Bar.Baz</namespace></or>"),
68+
// Test Filter
69+
new TestCaseData("test='My.Test.Fixture.Method(42)'", "<test>My.Test.Fixture.Method(42)</test>"),
70+
new TestCaseData("test='My.Test.Fixture.Method(\"xyz\")'", "<test>My.Test.Fixture.Method(&quot;xyz&quot;)</test>"),
71+
new TestCaseData("test='My.Test.Fixture.Method(\"abc\\'s\")'", "<test>My.Test.Fixture.Method(&quot;abc&apos;s&quot;)</test>"),
72+
new TestCaseData("test='My.Test.Fixture.Method(\"x&y&z\")'", "<test>My.Test.Fixture.Method(&quot;x&amp;y&amp;z&quot;)</test>"),
73+
new TestCaseData("test='My.Test.Fixture.Method(\"<xyz>\")'", "<test>My.Test.Fixture.Method(&quot;&lt;xyz&gt;&quot;)</test>"),
74+
new TestCaseData("test=='Issue1510.TestSomething ( Option1 , \"ABC\" ) '", "<test>Issue1510.TestSomething(Option1,&quot;ABC&quot;)</test>"),
75+
new TestCaseData("test=='Issue1510.TestSomething ( Option1 , \"A B C\" ) '", "<test>Issue1510.TestSomething(Option1,&quot;A B C&quot;)</test>"),
76+
new TestCaseData("test=/My.Test.Fixture.Method(42)/", "<test>My.Test.Fixture.Method(42)</test>"),
77+
new TestCaseData("test=/My.Test.Fixture.Method(\"xyz\")/", "<test>My.Test.Fixture.Method(&quot;xyz&quot;)</test>"),
78+
new TestCaseData("test=/My.Test.Fixture.Method(\"abc\\'s\")/", "<test>My.Test.Fixture.Method(&quot;abc&apos;s&quot;)</test>"),
79+
new TestCaseData("test=/My.Test.Fixture.Method(\"x&y&z\")/", "<test>My.Test.Fixture.Method(&quot;x&amp;y&amp;z&quot;)</test>"),
80+
new TestCaseData("test=/My.Test.Fixture.Method(\"<xyz>\")/", "<test>My.Test.Fixture.Method(&quot;&lt;xyz&gt;&quot;)</test>"),
81+
new TestCaseData("test==/Issue1510.TestSomething ( Option1 , \"ABC\" ) /", "<test>Issue1510.TestSomething(Option1,&quot;ABC&quot;)</test>"),
82+
new TestCaseData("test==/Issue1510.TestSomething ( Option1 , \"A B C\" ) /", "<test>Issue1510.TestSomething(Option1,&quot;A B C&quot;)</test>"),
83+
new TestCaseData("test=My.Test.Fixture.Method(42)", "<test>My.Test.Fixture.Method(42)</test>"),
84+
new TestCaseData("test=My.Test.Fixture.Method(\"xyz\")", "<test>My.Test.Fixture.Method(&quot;xyz&quot;)</test>"),
85+
new TestCaseData("test=My.Test.Fixture.Method(\"abc\\'s\")", "<test>My.Test.Fixture.Method(&quot;abc&apos;s&quot;)</test>"),
86+
new TestCaseData("test=My.Test.Fixture.Method(\"x&y&z\")", "<test>My.Test.Fixture.Method(&quot;x&amp;y&amp;z&quot;)</test>"),
87+
new TestCaseData("test=My.Test.Fixture.Method(\"<xyz>\")", "<test>My.Test.Fixture.Method(&quot;&lt;xyz&gt;&quot;)</test>"),
88+
new TestCaseData("test==Issue1510.TestSomething ( Option1 , \"ABC\" ) ", "<test>Issue1510.TestSomething(Option1,&quot;ABC&quot;)</test>"),
89+
new TestCaseData("test==Issue1510.TestSomething ( Option1 , \"A B C\" ) ", "<test>Issue1510.TestSomething(Option1,&quot;A B C&quot;)</test>"),
90+
// And Filter
91+
new TestCaseData("cat==Urgent && test=='My.Tests'", "<and><cat>Urgent</cat><test>My.Tests</test></and>"),
92+
new TestCaseData("cat==Urgent and test=='My.Tests'", "<and><cat>Urgent</cat><test>My.Tests</test></and>"),
93+
// Or Filter
94+
new TestCaseData("cat==Urgent || test=='My.Tests'", "<or><cat>Urgent</cat><test>My.Tests</test></or>"),
95+
new TestCaseData("cat==Urgent or test=='My.Tests'", "<or><cat>Urgent</cat><test>My.Tests</test></or>"),
96+
// Mixed And Filter with Or Filter
97+
new TestCaseData("cat = Urgent || cat = High", "<or><cat>Urgent</cat><cat>High</cat></or>"),
98+
new TestCaseData("cat==Urgent || test=='My.Tests' && cat == high", "<or><cat>Urgent</cat><and><test>My.Tests</test><cat>high</cat></and></or>"),
99+
new TestCaseData("cat==Urgent && test=='My.Tests' || cat == high", "<or><and><cat>Urgent</cat><test>My.Tests</test></and><cat>high</cat></or>"),
100+
new TestCaseData("cat==Urgent && (test=='My.Tests' || cat == high)", "<and><cat>Urgent</cat><or><test>My.Tests</test><cat>high</cat></or></and>"),
101+
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>"),
102+
// Not Filter
103+
new TestCaseData("!(test!='My.Tests')", "<not><not><test>My.Tests</test></not></not>"),
104+
new TestCaseData("!(cat!=Urgent)", "<not><not><cat>Urgent</cat></not></not>")
105+
};
106+
107+
private static IEnumerable<string> UniqueOutputs()
108+
{
109+
List<string> alreadyReturned = new List<string>();
110+
111+
foreach (var testCase in ParserTestCases)
112+
{
113+
var output = testCase.Arguments[1] as string;
114+
if (!alreadyReturned.Contains(output))
115+
{
116+
alreadyReturned.Add(output);
117+
yield return output;
118+
}
119+
}
120+
}
68121
}
69122
}

src/NUnitEngine/nunit.engine/Services/TestSelectionParser.cs

Lines changed: 85 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
using System;
44
using System.Collections.Generic;
5+
using System.Runtime.CompilerServices;
56
using System.Text;
67

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

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

4143
public string Parse(string input)
4244
{
@@ -116,21 +118,29 @@ public string ParseFilterElement()
116118
return ParseExpressionInParentheses();
117119

118120
Token lhs = Expect(TokenKind.Word);
121+
Token op;
122+
Token rhs;
119123

120124
switch (lhs.Text)
121125
{
122-
case "id":
126+
case "test":
127+
op = Expect(REL_OPS);
128+
rhs = GetTestName();
129+
return EmitFilterElement(lhs, op, rhs);
130+
123131
case "cat":
124132
case "method":
125133
case "class":
126134
case "name":
127-
case "test":
128135
case "namespace":
129136
case "partition":
130-
Token op = lhs.Text == "id"
131-
? Expect(EQ_OPS)
132-
: Expect(REL_OPS);
133-
Token rhs = Expect(TokenKind.String, TokenKind.Word);
137+
op = Expect(REL_OPS);
138+
rhs = Expect(TokenKind.String, TokenKind.Word);
139+
return EmitFilterElement(lhs, op, rhs);
140+
141+
case "id":
142+
op = Expect(EQ_OPS);
143+
rhs = Expect(TokenKind.String, TokenKind.Word);
134144
return EmitFilterElement(lhs, op, rhs);
135145

136146
default:
@@ -142,6 +152,75 @@ public string ParseFilterElement()
142152
}
143153
}
144154

155+
// TODO: We do extra work for test names due to the fact that
156+
// Windows drops double quotes from arguments in many situations.
157+
// It would be better to parse the command-line directly but
158+
// that will mean a significant rewrite.
159+
private Token GetTestName()
160+
{
161+
var result = Expect(TokenKind.String, TokenKind.Word);
162+
var sb = new StringBuilder();
163+
164+
if (result.Kind == TokenKind.String)
165+
{
166+
int index = result.Text.IndexOf('(');
167+
168+
if (index < 0)
169+
return result;
170+
171+
// Remove white space around arguments
172+
string testName = result.Text;
173+
sb = new StringBuilder(testName.Substring(0, index).Trim());
174+
sb.Append('(');
175+
bool done = false;
176+
177+
while (++index < testName.Length && !done)
178+
{
179+
char ch = testName[index];
180+
switch (ch)
181+
{
182+
case '"':
183+
sb.Append(ch);
184+
while (++index < testName.Length && testName[index] != '"')
185+
sb.Append(testName[index]);
186+
sb.Append('"');
187+
break;
188+
case ' ':
189+
break;
190+
default:
191+
sb.Append(ch);
192+
done = ch == ')';
193+
break;
194+
}
195+
}
196+
}
197+
else
198+
{
199+
// Word Token - check to see if it's followed by a left parenthesis
200+
if (_tokenizer.LookAhead != LPAREN) return result;
201+
202+
// We have a "Word" token followed by a left parenthesis
203+
// This may be a testname entered without quotes or one
204+
// using double quotes, which were removed by the shell.
205+
206+
sb = new StringBuilder(result.Text);
207+
var token = NextToken();
208+
209+
while (token != EOF)
210+
{
211+
bool isString = token.Kind == TokenKind.String;
212+
213+
if (isString) sb.Append('"');
214+
sb.Append(token.Text);
215+
if (isString) sb.Append('"');
216+
217+
token = NextToken();
218+
}
219+
}
220+
221+
return new Token(TokenKind.String, sb.ToString());
222+
}
223+
145224
private static string EmitFilterElement(Token lhs, Token op, Token rhs)
146225
{
147226
string fmt = null;

src/NUnitEngine/nunit.engine/Services/Tokenizer.cs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -83,7 +83,7 @@ public class Tokenizer
8383
private int _index;
8484

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

8989
private Token _lookahead;
@@ -130,6 +130,7 @@ private Token GetNextToken()
130130
// Single char symbols
131131
case '(':
132132
case ')':
133+
case ',':
133134
GetChar();
134135
return new Token(TokenKind.Symbol, ch) { Pos = pos };
135136

0 commit comments

Comments
 (0)