1 // ***********************************************************************
2 // Copyright (c) 2015 Charlie Poole
4 // Permission is hereby granted, free of charge, to any person obtaining
5 // a copy of this software and associated documentation files (the
6 // "Software"), to deal in the Software without restriction, including
7 // without limitation the rights to use, copy, modify, merge, publish,
8 // distribute, sublicense, and/or sell copies of the Software, and to
9 // permit persons to whom the Software is furnished to do so, subject to
10 // the following conditions:
12 // The above copyright notice and this permission notice shall be
13 // included in all copies or substantial portions of the Software.
15 // THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16 // EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17 // MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18 // NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19 // LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20 // OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21 // WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22 // ***********************************************************************
25 #define NUNIT_FRAMEWORK
30 using System.Collections.Generic;
34 #pragma warning disable 1591
37 namespace NUnit.Engine
39 namespace NUnit.Common
42 public class TestSelectionParser
44 private Tokenizer _tokenizer;
46 private static readonly Token LPAREN = new Token(TokenKind.Symbol, "(");
47 private static readonly Token RPAREN = new Token(TokenKind.Symbol, ")");
48 private static readonly Token AND_OP1 = new Token(TokenKind.Symbol, "&");
49 private static readonly Token AND_OP2 = new Token(TokenKind.Symbol, "&&");
50 private static readonly Token AND_OP3 = new Token(TokenKind.Word, "and");
51 private static readonly Token AND_OP4 = new Token(TokenKind.Word, "AND");
52 private static readonly Token OR_OP1 = new Token(TokenKind.Symbol, "|");
53 private static readonly Token OR_OP2 = new Token(TokenKind.Symbol, "||");
54 private static readonly Token OR_OP3 = new Token(TokenKind.Word, "or");
55 private static readonly Token OR_OP4 = new Token(TokenKind.Word, "OR");
56 private static readonly Token NOT_OP = new Token(TokenKind.Symbol, "!");
58 private static readonly Token EQ_OP1 = new Token(TokenKind.Symbol, "=");
59 private static readonly Token EQ_OP2 = new Token(TokenKind.Symbol, "==");
60 private static readonly Token NE_OP = new Token(TokenKind.Symbol, "!=");
61 private static readonly Token MATCH_OP = new Token(TokenKind.Symbol, "=~");
62 private static readonly Token NOMATCH_OP = new Token(TokenKind.Symbol, "!~");
64 private static readonly Token[] AND_OPS = new Token[] { AND_OP1, AND_OP2, AND_OP3, AND_OP4 };
65 private static readonly Token[] OR_OPS = new Token[] { OR_OP1, OR_OP2, OR_OP3, OR_OP4 };
66 private static readonly Token[] EQ_OPS = new Token[] { EQ_OP1, EQ_OP2 };
67 private static readonly Token[] REL_OPS = new Token[] { EQ_OP1, EQ_OP2, NE_OP, MATCH_OP, NOMATCH_OP };
69 private static readonly Token EOF = new Token(TokenKind.Eof);
71 public string Parse(string input)
73 _tokenizer = new Tokenizer(input);
75 if (_tokenizer.LookAhead == EOF)
76 throw new TestSelectionParserException("No input provided for test selection.");
78 var result = ParseFilterExpression();
85 /// Parse a single term or an or expression, returning the xml
87 /// <returns></returns>
88 public string ParseFilterExpression()
90 var terms = new List<string>();
91 terms.Add(ParseFilterTerm());
93 while (LookingAt(OR_OPS))
96 terms.Add(ParseFilterTerm());
102 var sb = new StringBuilder("<or>");
104 foreach (string term in terms)
109 return sb.ToString();
113 /// Parse a single element or an and expression and return the xml
115 public string ParseFilterTerm()
117 var elements = new List<string>();
118 elements.Add(ParseFilterElement());
120 while (LookingAt(AND_OPS))
123 elements.Add(ParseFilterElement());
126 if (elements.Count == 1)
129 var sb = new StringBuilder("<and>");
131 foreach (string element in elements)
136 return sb.ToString();
140 /// Parse a single filter element such as a category expression
141 /// and return the xml representation of the filter.
143 public string ParseFilterElement()
145 if (LookingAt(LPAREN, NOT_OP))
146 return ParseExpressionInParentheses();
148 Token lhs = Expect(TokenKind.Word);
158 Token op = lhs.Text == "id"
161 Token rhs = Expect(TokenKind.String, TokenKind.Word);
162 return EmitFilterElement(lhs, op, rhs);
165 // Assume it's a property name
166 op = Expect(REL_OPS);
167 rhs = Expect(TokenKind.String, TokenKind.Word);
168 return EmitPropertyElement(lhs, op, rhs);
169 //throw InvalidTokenError(lhs);
173 private static string EmitFilterElement(Token lhs, Token op, Token rhs)
177 if (op == EQ_OP1 || op == EQ_OP2)
178 fmt = "<{0}>{1}</{0}>";
179 else if (op == NE_OP)
180 fmt = "<not><{0}>{1}</{0}></not>";
181 else if (op == MATCH_OP)
182 fmt = "<{0} re='1'>{1}</{0}>";
183 else if (op == NOMATCH_OP)
184 fmt = "<not><{0} re='1'>{1}</{0}></not>";
186 fmt = "<{0} op='" + op.Text + "'>{1}</{0}>";
188 return EmitElement(fmt, lhs, rhs);
191 private static string EmitPropertyElement(Token lhs, Token op, Token rhs)
195 if (op == EQ_OP1 || op == EQ_OP2)
196 fmt = "<prop name='{0}'>{1}</prop>";
197 else if (op == NE_OP)
198 fmt = "<not><prop name='{0}'>{1}</prop></not>";
199 else if (op == MATCH_OP)
200 fmt = "<prop name='{0}' re='1'>{1}</prop>";
201 else if (op == NOMATCH_OP)
202 fmt = "<not><prop name='{0}' re='1'>{1}</prop></not>";
204 fmt = "<prop name='{0}' op='" + op.Text + "'>{1}</prop>";
206 return EmitElement(fmt, lhs, rhs);
209 private static string EmitElement(string fmt, Token lhs, Token rhs)
211 return string.Format(fmt, lhs.Text, XmlEscape(rhs.Text));
214 private string ParseExpressionInParentheses()
216 Token op = Expect(LPAREN, NOT_OP);
218 if (op == NOT_OP) Expect(LPAREN);
220 string result = ParseFilterExpression();
225 result = "<not>" + result + "</not>";
230 // Require a token of one or more kinds
231 private Token Expect(params TokenKind[] kinds)
233 Token token = NextToken();
235 foreach (TokenKind kind in kinds)
236 if (token.Kind == kind)
239 throw InvalidTokenError(token);
242 // Require a token from a list of tokens
243 private Token Expect(params Token[] valid)
245 Token token = NextToken();
247 foreach (Token item in valid)
251 throw InvalidTokenError(token);
254 private Exception InvalidTokenError(Token token)
256 return new TestSelectionParserException(string.Format(
257 "Unexpected token '{0}' at position {1} in selection expression.", token.Text, token.Pos));
260 private Token LookAhead
262 get { return _tokenizer.LookAhead; }
265 private bool LookingAt(params Token[] tokens)
267 foreach (Token token in tokens)
268 if (LookAhead == token)
274 private Token NextToken()
276 return _tokenizer.NextToken();
279 private static string XmlEscape(string text)
282 .Replace("&", "&")
283 .Replace("\"", """)
284 .Replace("<", "<")
285 .Replace(">", ">")
286 .Replace("'", "'");