Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[pull] main from nunit:main #198

Merged
merged 5 commits into from
Dec 2, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt

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

namespace NUnit.Engine.Tests
{
Expand All @@ -18,46 +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("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 @@ -66,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;
}
}
}
}
}
47 changes: 44 additions & 3 deletions src/NUnitEngine/nunit.engine.tests/Services/TokenizerTests.cs
Original file line number Diff line number Diff line change
@@ -1,8 +1,5 @@
// Copyright (c) Charlie Poole, Rob Prouse and Contributors. MIT License - see LICENSE.txt

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

namespace NUnit.Engine.Tests
Expand Down Expand Up @@ -45,6 +42,40 @@ public void WordsInUnicode()
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Eof)));
}

[Test]
public void WordsWithSpecialCharacters()
{
var tokenizer = new Tokenizer("word_with_underscores word-with-dashes word.with.dots");
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "word_with_underscores")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "word-with-dashes")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "word.with.dots")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Eof)));
}

private const string WORD_BREAK_CHARS = "=!()&| \t,";
[Test]
public void WordBreakCharacters()
{
var tokenizer = new Tokenizer("word1==word2!=word3 func(arg1, arg2) this&&that||both");
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "word1")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Symbol, "==")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "word2")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Symbol, "!=")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "word3")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "func")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Symbol, "(")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "arg1")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Symbol, ",")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "arg2")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Symbol, ")")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "this")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Symbol, "&&")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "that")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Symbol, "||")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "both")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Eof)));
}

[Test]
public void StringWithDoubleQuotes()
{
Expand Down Expand Up @@ -75,6 +106,16 @@ public void StringWithSlashes()
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Eof)));
}

[Test]
public void TestNameWithParameters()
{
var tokenizer = new Tokenizer("test=='Issue1510.TestSomething(Option1,\"ABC\")'");
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Word, "test")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Symbol, "==")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.String, "Issue1510.TestSomething(Option1,\"ABC\")")));
Assert.That(tokenizer.NextToken(), Is.EqualTo(new Token(TokenKind.Eof)));
}

[Test]
public void StringsMayContainEscapedQuoteChar()
{
Expand Down
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
Loading
Loading