From b34c35ee8fe1516118d1d172a5e05b263ccbd93d Mon Sep 17 00:00:00 2001 From: David Brazdil Date: Thu, 20 Aug 2015 11:46:04 +0100 Subject: [PATCH] ART: Expression evaluation in Checker It can be useful for tests to evaluate small `assert`-like expressions. This patch adds such support to Checker, with a new CHECK-EVAL line. See README file for more details. Change-Id: I184f7c8e8b53f7e93cfb08fcf9630b4724fa5412 --- tools/checker/README | 62 ++++++++++------- tools/checker/file_format/checker/parser.py | 38 ++++++++--- tools/checker/file_format/checker/struct.py | 44 ++++++------ tools/checker/file_format/checker/test.py | 101 +++++++++++++++++++++++----- tools/checker/match/file.py | 15 ++++- tools/checker/match/line.py | 59 ++++++++++------ tools/checker/match/test.py | 16 ++++- tools/checker/run_unit_tests.py | 5 +- 8 files changed, 244 insertions(+), 96 deletions(-) diff --git a/tools/checker/README b/tools/checker/README index 259691e50..65f5bd25a 100644 --- a/tools/checker/README +++ b/tools/checker/README @@ -2,10 +2,10 @@ Checker is a testing tool which compiles a given test file and compares the state of the control-flow graph before and after each optimization pass against a set of assertions specified alongside the tests. -Tests are written in Java, turned into DEX and compiled with the Optimizing -compiler. "Check lines" are assertions formatted as comments of the Java file. -They begin with prefix 'CHECK' followed by a pattern that the engine attempts -to match in the compiler-generated output. +Tests are written in Java or Smali, turned into DEX and compiled with the +Optimizing compiler. "Check lines" are assertions formatted as comments of the +source file. They begin with prefix "/// CHECK" or "## CHECK", respectively, +followed by a pattern that the engine attempts to match in the compiler output. Assertions are tested in groups which correspond to the individual compiler passes. Each group of check lines therefore must start with a 'CHECK-START' @@ -15,19 +15,23 @@ be listed with the '--list-passes' command-line flag). Matching of check lines is carried out in the order of appearance in the source file. There are three types of check lines: - - CHECK: Must match an output line which appears in the output group - later than lines matched against any preceeding checks. Output - lines must therefore match the check lines in the same order. - These are referred to as "in-order" checks in the code. - - CHECK-DAG: Must match an output line which appears in the output group - later than lines matched against any preceeding in-order checks. - In other words, the order of output lines does not matter - between consecutive DAG checks. - - CHECK-NOT: Must not match any output line which appears in the output group - later than lines matched against any preceeding checks and - earlier than lines matched against any subsequent checks. - Surrounding non-negative checks (or boundaries of the group) - therefore create a scope within which the assertion is verified. + - CHECK: Must match an output line which appears in the output group + later than lines matched against any preceeding checks. Output + lines must therefore match the check lines in the same order. + These are referred to as "in-order" checks in the code. + - CHECK-DAG: Must match an output line which appears in the output group + later than lines matched against any preceeding in-order checks. + In other words, the order of output lines does not matter + between consecutive DAG checks. + - CHECK-NOT: Must not match any output line which appears in the output group + later than lines matched against any preceeding checks and + earlier than lines matched against any subsequent checks. + Surrounding non-negative checks (or boundaries of the group) + therefore create a scope within which the assertion is verified. + - CHECK-NEXT: Must match the output line which comes right after the line which + matched the previous check. Cannot be used after any but the + in-order CHECK. + - CHECK-EVAL: Specifies a Python expression which must evaluate to 'True'. Check-line patterns are treated as plain text rather than regular expressions but are whitespace agnostic. @@ -45,18 +49,30 @@ be redefined or used undefined. Example: The following assertions can be placed in a Java source file: - // CHECK-START: int MyClass.MyMethod() constant_folding (after) - // CHECK: <> IntConstant {{11|22}} - // CHECK: Return [<>] + /// CHECK-START: int MyClass.MyMethod() constant_folding (after) + /// CHECK: <> IntConstant {{11|22}} + /// CHECK: Return [<>] The engine will attempt to match the check lines against the output of the group named on the first line. Together they verify that the CFG after constant folding returns an integer constant with value either 11 or 22. + +Of the language constructs above, 'CHECK-EVAL' lines support only referencing of +variables. Any other surrounding text will be passed to Python's `eval` as is. + +Example: + /// CHECK-START: int MyClass.MyMethod() liveness (after) + /// CHECK: InstructionA liveness:<> + /// CHECK: InstructionB liveness:<> + /// CHECK-EVAL: <> != <> + + A group of check lines can be made architecture-specific by inserting '-' after the 'CHECK-START' keyword. The previous example can be updated to run for arm64 only with: - // CHECK-START-ARM64: int MyClass.MyMethod() constant_folding (after) - // CHECK: <> IntConstant {{11|22}} - // CHECK: Return [<>] +Example: + /// CHECK-START-ARM64: int MyClass.MyMethod() constant_folding (after) + /// CHECK: <> IntConstant {{11|22}} + /// CHECK: Return [<>] diff --git a/tools/checker/file_format/checker/parser.py b/tools/checker/file_format/checker/parser.py index 001f72a22..446302fed 100644 --- a/tools/checker/file_format/checker/parser.py +++ b/tools/checker/file_format/checker/parser.py @@ -15,7 +15,7 @@ from common.archs import archs_list from common.logger import Logger from file_format.common import SplitStream -from file_format.checker.struct import CheckerFile, TestCase, TestAssertion, RegexExpression +from file_format.checker.struct import CheckerFile, TestCase, TestAssertion, TestExpression import re @@ -81,6 +81,11 @@ def __processLine(line, lineNo, prefix, fileName): if notLine is not None: return (notLine, TestAssertion.Variant.Not, lineNo), None, None + # 'CHECK-EVAL' lines evaluate a Python expression. + evalLine = __extractLine(prefix + "-EVAL", line) + if evalLine is not None: + return (evalLine, TestAssertion.Variant.Eval, lineNo), None, None + Logger.fail("Checker assertion could not be parsed: '" + line + "'", fileName, lineNo) def __isMatchAtStart(match): @@ -97,16 +102,24 @@ def __firstMatch(matches, string): def ParseCheckerAssertion(parent, line, variant, lineNo): """ This method parses the content of a check line stripped of the initial - comment symbol and the CHECK keyword. + comment symbol and the CHECK-* keyword. """ assertion = TestAssertion(parent, variant, line, lineNo) + isEvalLine = (variant == TestAssertion.Variant.Eval) + # Loop as long as there is something to parse. while line: # Search for the nearest occurrence of the special markers. - matchWhitespace = re.search(r"\s+", line) - matchPattern = re.search(RegexExpression.Regex.regexPattern, line) - matchVariableReference = re.search(RegexExpression.Regex.regexVariableReference, line) - matchVariableDefinition = re.search(RegexExpression.Regex.regexVariableDefinition, line) + if isEvalLine: + # The following constructs are not supported in CHECK-EVAL lines + matchWhitespace = None + matchPattern = None + matchVariableDefinition = None + else: + matchWhitespace = re.search(r"\s+", line) + matchPattern = re.search(TestExpression.Regex.regexPattern, line) + matchVariableDefinition = re.search(TestExpression.Regex.regexVariableDefinition, line) + matchVariableReference = re.search(TestExpression.Regex.regexVariableReference, line) # If one of the above was identified at the current position, extract them # from the line, parse them and add to the list of line parts. @@ -114,24 +127,24 @@ def ParseCheckerAssertion(parent, line, variant, lineNo): # A whitespace in the check line creates a new separator of line parts. # This allows for ignored output between the previous and next parts. line = line[matchWhitespace.end():] - assertion.addExpression(RegexExpression.createSeparator()) + assertion.addExpression(TestExpression.createSeparator()) elif __isMatchAtStart(matchPattern): pattern = line[0:matchPattern.end()] pattern = pattern[2:-2] line = line[matchPattern.end():] - assertion.addExpression(RegexExpression.createPattern(pattern)) + assertion.addExpression(TestExpression.createPattern(pattern)) elif __isMatchAtStart(matchVariableReference): var = line[0:matchVariableReference.end()] line = line[matchVariableReference.end():] name = var[2:-2] - assertion.addExpression(RegexExpression.createVariableReference(name)) + assertion.addExpression(TestExpression.createVariableReference(name)) elif __isMatchAtStart(matchVariableDefinition): var = line[0:matchVariableDefinition.end()] line = line[matchVariableDefinition.end():] colonPos = var.find(":") name = var[2:colonPos] body = var[colonPos+1:-2] - assertion.addExpression(RegexExpression.createVariableDefinition(name, body)) + assertion.addExpression(TestExpression.createVariableDefinition(name, body)) else: # If we're not currently looking at a special marker, this is a plain # text match all the way until the first special marker (or the end @@ -143,7 +156,10 @@ def ParseCheckerAssertion(parent, line, variant, lineNo): line) text = line[0:firstMatch] line = line[firstMatch:] - assertion.addExpression(RegexExpression.createText(text)) + if isEvalLine: + assertion.addExpression(TestExpression.createPlainText(text)) + else: + assertion.addExpression(TestExpression.createPatternFromPlainText(text)) return assertion def ParseCheckerStream(fileName, prefix, stream): diff --git a/tools/checker/file_format/checker/struct.py b/tools/checker/file_format/checker/struct.py index 2b2e4429e..8181c4ce0 100644 --- a/tools/checker/file_format/checker/struct.py +++ b/tools/checker/file_format/checker/struct.py @@ -74,7 +74,7 @@ class TestAssertion(PrintableMixin): class Variant(object): """Supported types of assertions.""" - InOrder, NextLine, DAG, Not = range(4) + InOrder, NextLine, DAG, Not, Eval = range(5) def __init__(self, parent, variant, originalText, lineNo): assert isinstance(parent, TestCase) @@ -92,9 +92,9 @@ class TestAssertion(PrintableMixin): return self.parent.fileName def addExpression(self, new_expression): - assert isinstance(new_expression, RegexExpression) + assert isinstance(new_expression, TestExpression) if self.variant == TestAssertion.Variant.Not: - if new_expression.variant == RegexExpression.Variant.VarDef: + if new_expression.variant == TestExpression.Variant.VarDef: Logger.fail("CHECK-NOT lines cannot define variables", self.fileName, self.lineNo) self.expressions.append(new_expression) @@ -102,10 +102,10 @@ class TestAssertion(PrintableMixin): """ Returns a regex pattern for this entire assertion. Only used in tests. """ regex = "" for expression in self.expressions: - if expression.variant == RegexExpression.Variant.Separator: + if expression.variant == TestExpression.Variant.Separator: regex = regex + ", " else: - regex = regex + "(" + expression.pattern + ")" + regex = regex + "(" + expression.text + ")" return regex def __eq__(self, other): @@ -114,11 +114,11 @@ class TestAssertion(PrintableMixin): and self.expressions == other.expressions -class RegexExpression(EqualityMixin, PrintableMixin): +class TestExpression(EqualityMixin, PrintableMixin): class Variant(object): """Supported language constructs.""" - Text, Pattern, VarRef, VarDef, Separator = range(5) + PlainText, Pattern, VarRef, VarDef, Separator = range(5) class Regex(object): rName = r"([a-zA-Z][a-zA-Z0-9]*)" @@ -131,37 +131,43 @@ class RegexExpression(EqualityMixin, PrintableMixin): regexPattern = rPatternStartSym + rRegex + rPatternEndSym regexVariableReference = rVariableStartSym + rName + rVariableEndSym - regexVariableDefinition = rVariableStartSym + rName + rVariableSeparator + rRegex + rVariableEndSym + regexVariableDefinition = (rVariableStartSym + + rName + rVariableSeparator + rRegex + + rVariableEndSym) - def __init__(self, variant, name, pattern): + def __init__(self, variant, name, text): self.variant = variant self.name = name - self.pattern = pattern + self.text = text def __eq__(self, other): return isinstance(other, self.__class__) \ and self.variant == other.variant \ and self.name == other.name \ - and self.pattern == other.pattern + and self.text == other.text @staticmethod def createSeparator(): - return RegexExpression(RegexExpression.Variant.Separator, None, None) + return TestExpression(TestExpression.Variant.Separator, None, None) @staticmethod - def createText(text): - return RegexExpression(RegexExpression.Variant.Text, None, re.escape(text)) + def createPlainText(text): + return TestExpression(TestExpression.Variant.PlainText, None, text) + + @staticmethod + def createPatternFromPlainText(text): + return TestExpression(TestExpression.Variant.Pattern, None, re.escape(text)) @staticmethod def createPattern(pattern): - return RegexExpression(RegexExpression.Variant.Pattern, None, pattern) + return TestExpression(TestExpression.Variant.Pattern, None, pattern) @staticmethod def createVariableReference(name): - assert re.match(RegexExpression.Regex.rName, name) - return RegexExpression(RegexExpression.Variant.VarRef, name, None) + assert re.match(TestExpression.Regex.rName, name) + return TestExpression(TestExpression.Variant.VarRef, name, None) @staticmethod def createVariableDefinition(name, pattern): - assert re.match(RegexExpression.Regex.rName, name) - return RegexExpression(RegexExpression.Variant.VarDef, name, pattern) + assert re.match(TestExpression.Regex.rName, name) + return TestExpression(TestExpression.Variant.VarDef, name, pattern) diff --git a/tools/checker/file_format/checker/test.py b/tools/checker/file_format/checker/test.py index 36ed4b159..495dabc58 100644 --- a/tools/checker/file_format/checker/test.py +++ b/tools/checker/file_format/checker/test.py @@ -17,7 +17,7 @@ from common.archs import archs_list from common.testing import ToUnicode from file_format.checker.parser import ParseCheckerStream -from file_format.checker.struct import CheckerFile, TestCase, TestAssertion, RegexExpression +from file_format.checker.struct import CheckerFile, TestCase, TestAssertion, TestExpression import io import unittest @@ -73,10 +73,11 @@ class CheckerParser_PrefixTest(unittest.TestCase): self.assertParses(" ///CHECK: foo") self.assertParses("/// CHECK: foo") -class CheckerParser_RegexExpressionTest(unittest.TestCase): +class CheckerParser_TestExpressionTest(unittest.TestCase): def parseAssertion(self, string, variant=""): - checkerText = u"/// CHECK-START: pass\n/// CHECK" + ToUnicode(variant) + u": " + ToUnicode(string) + checkerText = (u"/// CHECK-START: pass\n" + + u"/// CHECK" + ToUnicode(variant) + u": " + ToUnicode(string)) checkerFile = ParseCheckerStream("", "CHECK", io.StringIO(checkerText)) self.assertEqual(len(checkerFile.testCases), 1) testCase = checkerFile.testCases[0] @@ -92,17 +93,17 @@ class CheckerParser_RegexExpressionTest(unittest.TestCase): self.assertEqual(expected, self.parseAssertion(string).toRegex()) def assertEqualsText(self, string, text): - self.assertEqual(self.parseExpression(string), RegexExpression.createText(text)) + self.assertEqual(self.parseExpression(string), TestExpression.createPatternFromPlainText(text)) def assertEqualsPattern(self, string, pattern): - self.assertEqual(self.parseExpression(string), RegexExpression.createPattern(pattern)) + self.assertEqual(self.parseExpression(string), TestExpression.createPattern(pattern)) def assertEqualsVarRef(self, string, name): - self.assertEqual(self.parseExpression(string), RegexExpression.createVariableReference(name)) + self.assertEqual(self.parseExpression(string), TestExpression.createVariableReference(name)) def assertEqualsVarDef(self, string, name, pattern): self.assertEqual(self.parseExpression(string), - RegexExpression.createVariableDefinition(name, pattern)) + TestExpression.createVariableDefinition(name, pattern)) def assertVariantNotEqual(self, string, variant): self.assertNotEqual(variant, self.parseExpression(string).variant) @@ -166,17 +167,17 @@ class CheckerParser_RegexExpressionTest(unittest.TestCase): self.assertEqualsVarDef("<>", "ABC", "(a[bc])") def test_Empty(self): - self.assertVariantNotEqual("{{}}", RegexExpression.Variant.Pattern) - self.assertVariantNotEqual("<<>>", RegexExpression.Variant.VarRef) - self.assertVariantNotEqual("<<:>>", RegexExpression.Variant.VarDef) + self.assertEqualsText("{{}}", "{{}}") + self.assertVariantNotEqual("<<>>", TestExpression.Variant.VarRef) + self.assertVariantNotEqual("<<:>>", TestExpression.Variant.VarDef) def test_InvalidVarName(self): - self.assertVariantNotEqual("<<0ABC>>", RegexExpression.Variant.VarRef) - self.assertVariantNotEqual("<>", RegexExpression.Variant.VarRef) - self.assertVariantNotEqual("<>", RegexExpression.Variant.VarRef) - self.assertVariantNotEqual("<<0ABC:abc>>", RegexExpression.Variant.VarDef) - self.assertVariantNotEqual("<>", RegexExpression.Variant.VarDef) - self.assertVariantNotEqual("<>", RegexExpression.Variant.VarDef) + self.assertVariantNotEqual("<<0ABC>>", TestExpression.Variant.VarRef) + self.assertVariantNotEqual("<>", TestExpression.Variant.VarRef) + self.assertVariantNotEqual("<>", TestExpression.Variant.VarRef) + self.assertVariantNotEqual("<<0ABC:abc>>", TestExpression.Variant.VarDef) + self.assertVariantNotEqual("<>", TestExpression.Variant.VarDef) + self.assertVariantNotEqual("<>", TestExpression.Variant.VarDef) def test_BodyMatchNotGreedy(self): self.assertEqualsRegex("{{abc}}{{def}}", "(abc)(def)") @@ -201,7 +202,7 @@ class CheckerParser_FileLayoutTest(unittest.TestCase): content = assertionEntry[0] variant = assertionEntry[1] assertion = TestAssertion(testCase, variant, content, 0) - assertion.addExpression(RegexExpression.createText(content)) + assertion.addExpression(TestExpression.createPatternFromPlainText(content)) return testFile def assertParsesTo(self, checkerText, expectedData): @@ -279,9 +280,15 @@ class CheckerParser_FileLayoutTest(unittest.TestCase): self.parse( """ /// CHECK-START: Example Group + /// CHECK-EVAL: foo + /// CHECK-NEXT: bar + """) + with self.assertRaises(CheckerException): + self.parse( + """ + /// CHECK-START: Example Group /// CHECK-NEXT: bar """) - class CheckerParser_ArchTests(unittest.TestCase): @@ -329,3 +336,61 @@ class CheckerParser_ArchTests(unittest.TestCase): self.assertEqual(len(checkerFile.testCases), 1) self.assertEqual(len(checkerFile.testCasesForArch(arch)), 1) self.assertEqual(len(checkerFile.testCases[0].assertions), 4) + + +class CheckerParser_EvalTests(unittest.TestCase): + def parseTestCase(self, string): + checkerText = u"/// CHECK-START: pass\n" + ToUnicode(string) + checkerFile = ParseCheckerStream("", "CHECK", io.StringIO(checkerText)) + self.assertEqual(len(checkerFile.testCases), 1) + return checkerFile.testCases[0] + + def parseExpressions(self, string): + testCase = self.parseTestCase("/// CHECK-EVAL: " + string) + self.assertEqual(len(testCase.assertions), 1) + assertion = testCase.assertions[0] + self.assertEqual(assertion.variant, TestAssertion.Variant.Eval) + self.assertEqual(assertion.originalText, string) + return assertion.expressions + + def assertParsesToPlainText(self, text): + testCase = self.parseTestCase("/// CHECK-EVAL: " + text) + self.assertEqual(len(testCase.assertions), 1) + assertion = testCase.assertions[0] + self.assertEqual(assertion.variant, TestAssertion.Variant.Eval) + self.assertEqual(assertion.originalText, text) + self.assertEqual(len(assertion.expressions), 1) + expression = assertion.expressions[0] + self.assertEqual(expression.variant, TestExpression.Variant.PlainText) + self.assertEqual(expression.text, text) + + def test_PlainText(self): + self.assertParsesToPlainText("XYZ") + self.assertParsesToPlainText("True") + self.assertParsesToPlainText("{{abc}}") + self.assertParsesToPlainText("<>") + self.assertParsesToPlainText("<>") + + def test_VariableReference(self): + self.assertEqual(self.parseExpressions("<>"), + [ TestExpression.createVariableReference("ABC") ]) + self.assertEqual(self.parseExpressions("123<>"), + [ TestExpression.createPlainText("123"), + TestExpression.createVariableReference("ABC") ]) + self.assertEqual(self.parseExpressions("123 <>"), + [ TestExpression.createPlainText("123 "), + TestExpression.createVariableReference("ABC") ]) + self.assertEqual(self.parseExpressions("<>XYZ"), + [ TestExpression.createVariableReference("ABC"), + TestExpression.createPlainText("XYZ") ]) + self.assertEqual(self.parseExpressions("<> XYZ"), + [ TestExpression.createVariableReference("ABC"), + TestExpression.createPlainText(" XYZ") ]) + self.assertEqual(self.parseExpressions("123<>XYZ"), + [ TestExpression.createPlainText("123"), + TestExpression.createVariableReference("ABC"), + TestExpression.createPlainText("XYZ") ]) + self.assertEqual(self.parseExpressions("123 <> XYZ"), + [ TestExpression.createPlainText("123 "), + TestExpression.createVariableReference("ABC"), + TestExpression.createPlainText(" XYZ") ]) diff --git a/tools/checker/match/file.py b/tools/checker/match/file.py index 42ca7df43..6601a1e96 100644 --- a/tools/checker/match/file.py +++ b/tools/checker/match/file.py @@ -17,7 +17,7 @@ from common.immutables import ImmutableDict from common.logger import Logger from file_format.c1visualizer.struct import C1visualizerFile, C1visualizerPass from file_format.checker.struct import CheckerFile, TestCase, TestAssertion -from match.line import MatchLines +from match.line import MatchLines, EvaluateLine MatchScope = namedtuple("MatchScope", ["start", "end"]) MatchInfo = namedtuple("MatchInfo", ["scope", "variables"]) @@ -94,6 +94,11 @@ def testNotGroup(assertions, c1Pass, scope, variables): if MatchLines(assertion, line, variables) is not None: raise MatchFailedException(assertion, i) +def testEvalGroup(assertions, scope, variables): + for assertion in assertions: + if not EvaluateLine(assertion, variables): + raise MatchFailedException(assertion, scope.start) + def MatchTestCase(testCase, c1Pass): """ Runs a test case against a C1visualizer graph dump. @@ -132,11 +137,15 @@ def MatchTestCase(testCase, c1Pass): assert len(assertionGroup) == 1 scope = MatchScope(matchFrom, matchFrom + 1) match = findMatchingLine(assertionGroup[0], c1Pass, scope, variables) - else: + elif assertionGroup[0].variant == TestAssertion.Variant.DAG: # A group of DAG assertions. Match them all starting from the same point. - assert assertionGroup[0].variant == TestAssertion.Variant.DAG scope = MatchScope(matchFrom, c1Length) match = matchDagGroup(assertionGroup, c1Pass, scope, variables) + else: + assert assertionGroup[0].variant == TestAssertion.Variant.Eval + scope = MatchScope(matchFrom, c1Length) + testEvalGroup(assertionGroup, scope, variables) + continue if pendingNotAssertions: # Previous group were NOT assertions. Make sure they don't match any lines diff --git a/tools/checker/match/line.py b/tools/checker/match/line.py index ce11e2a52..08f001f66 100644 --- a/tools/checker/match/line.py +++ b/tools/checker/match/line.py @@ -13,7 +13,7 @@ # limitations under the License. from common.logger import Logger -from file_format.checker.struct import RegexExpression +from file_format.checker.struct import TestExpression, TestAssertion import re @@ -21,30 +21,40 @@ def headAndTail(list): return list[0], list[1:] def splitAtSeparators(expressions): - """ Splits a list of RegexExpressions at separators. """ + """ Splits a list of TestExpressions at separators. """ splitExpressions = [] wordStart = 0 for index, expression in enumerate(expressions): - if expression.variant == RegexExpression.Variant.Separator: + if expression.variant == TestExpression.Variant.Separator: splitExpressions.append(expressions[wordStart:index]) wordStart = index + 1 splitExpressions.append(expressions[wordStart:]) return splitExpressions +def getVariable(name, variables, pos): + if name in variables: + return variables[name] + else: + Logger.testFailed("Missing definition of variable \"{}\"".format(name), + pos.fileName, pos.lineNo) + +def setVariable(name, value, variables, pos): + if name not in variables: + return variables.copyWith(name, value) + else: + Logger.testFailed("Multiple definitions of variable \"{}\"".format(name), + pos.fileName, pos.lineNo) + def matchWords(checkerWord, stringWord, variables, pos): - """ Attempts to match a list of RegexExpressions against a string. + """ Attempts to match a list of TestExpressions against a string. Returns updated variable dictionary if successful and None otherwise. """ for expression in checkerWord: # If `expression` is a variable reference, replace it with the value. - if expression.variant == RegexExpression.Variant.VarRef: - if expression.name in variables: - pattern = re.escape(variables[expression.name]) - else: - Logger.testFailed("Missing definition of variable \"{}\"".format(expression.name), - pos.fileName, pos.lineNo) + if expression.variant == TestExpression.Variant.VarRef: + pattern = re.escape(getVariable(expression.name, variables, pos)) else: - pattern = expression.pattern + pattern = expression.text # Match the expression's regex pattern against the remainder of the word. # Note: re.match will succeed only if matched from the beginning. @@ -53,12 +63,8 @@ def matchWords(checkerWord, stringWord, variables, pos): return None # If `expression` was a variable definition, set the variable's value. - if expression.variant == RegexExpression.Variant.VarDef: - if expression.name not in variables: - variables = variables.copyWith(expression.name, stringWord[:match.end()]) - else: - Logger.testFailed("Multiple definitions of variable \"{}\"".format(expression.name), - pos.fileName, pos.lineNo) + if expression.variant == TestExpression.Variant.VarDef: + variables = setVariable(expression.name, stringWord[:match.end()], variables, pos) # Move cursor by deleting the matched characters. stringWord = stringWord[match.end():] @@ -73,11 +79,13 @@ def MatchLines(checkerLine, stringLine, variables): """ Attempts to match a CHECK line against a string. Returns variable state after the match if successful and None otherwise. """ + assert checkerLine.variant != TestAssertion.Variant.Eval + checkerWords = splitAtSeparators(checkerLine.expressions) stringWords = stringLine.split() while checkerWords: - # Get the next run of RegexExpressions which must match one string word. + # Get the next run of TestExpressions which must match one string word. checkerWord, checkerWords = headAndTail(checkerWords) # Keep reading words until a match is found. @@ -92,5 +100,18 @@ def MatchLines(checkerLine, stringLine, variables): if not wordMatched: return None - # All RegexExpressions matched. Return new variable state. + # All TestExpressions matched. Return new variable state. return variables + +def getEvalText(expression, variables, pos): + if expression.variant == TestExpression.Variant.PlainText: + return expression.text + else: + assert expression.variant == TestExpression.Variant.VarRef + return getVariable(expression.name, variables, pos) + +def EvaluateLine(checkerLine, variables): + assert checkerLine.variant == TestAssertion.Variant.Eval + eval_string = "".join(map(lambda expr: getEvalText(expr, variables, checkerLine), + checkerLine.expressions)) + return eval(eval_string) diff --git a/tools/checker/match/test.py b/tools/checker/match/test.py index ca748c772..5144ca9dc 100644 --- a/tools/checker/match/test.py +++ b/tools/checker/match/test.py @@ -17,7 +17,7 @@ from common.testing import ToUnicode from file_format.c1visualizer.parser import ParseC1visualizerStream from file_format.c1visualizer.struct import C1visualizerFile, C1visualizerPass from file_format.checker.parser import ParseCheckerStream, ParseCheckerAssertion -from file_format.checker.struct import CheckerFile, TestCase, TestAssertion, RegexExpression +from file_format.checker.struct import CheckerFile, TestCase, TestAssertion from match.file import MatchTestCase, MatchFailedException from match.line import MatchLines @@ -386,3 +386,17 @@ class MatchFiles_Test(unittest.TestCase): abc bar """) + + def test_EvalAssertions(self): + self.assertMatches("/// CHECK-EVAL: True", "foo") + self.assertDoesNotMatch("/// CHECK-EVAL: False", "foo") + + self.assertMatches("/// CHECK-EVAL: 1 + 2 == 3", "foo") + self.assertDoesNotMatch("/// CHECK-EVAL: 1 + 2 == 4", "foo") + + twoVarTestCase = """ + /// CHECK-DAG: <> <> + /// CHECK-EVAL: <> > <> + """ + self.assertMatches(twoVarTestCase, "42 41"); + self.assertDoesNotMatch(twoVarTestCase, "42 43") diff --git a/tools/checker/run_unit_tests.py b/tools/checker/run_unit_tests.py index 2f5b1feaa..2e8f2083b 100755 --- a/tools/checker/run_unit_tests.py +++ b/tools/checker/run_unit_tests.py @@ -17,9 +17,10 @@ from common.logger import Logger from file_format.c1visualizer.test import C1visualizerParser_Test from file_format.checker.test import CheckerParser_PrefixTest, \ - CheckerParser_RegexExpressionTest, \ + CheckerParser_TestExpressionTest, \ CheckerParser_FileLayoutTest, \ - CheckerParser_ArchTests + CheckerParser_ArchTests, \ + CheckerParser_EvalTests from match.test import MatchLines_Test, \ MatchFiles_Test -- 2.11.0