ART: Refactor and simplify matching in Checker
Change-Id: Ib8a2b51488f66a7239e799f5fa5910b4ac2dfe08
diff --git a/tools/checker/match/file.py b/tools/checker/match/file.py
index 116fe9a..6cff2bf 100644
--- a/tools/checker/match/file.py
+++ b/tools/checker/match/file.py
@@ -12,127 +12,138 @@
# See the License for the specific language governing permissions and
# limitations under the License.
+from collections import namedtuple
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
-def __headAndTail(list):
- return list[0], list[1:]
+MatchScope = namedtuple("MatchScope", ["start", "end"])
+MatchInfo = namedtuple("MatchInfo", ["scope", "variables"])
-def __splitByVariant(lines, variant):
- """ Splits a list of check lines at index 'i' such that lines[i] is the first
- element whose variant is not equal to the given parameter.
+class MatchFailedException(Exception):
+ def __init__(self, assertion, lineNo):
+ self.assertion = assertion
+ self.lineNo = lineNo
+
+def splitIntoGroups(assertions):
+ """ Breaks up a list of assertions, grouping instructions which should be
+ tested in the same scope (consecutive DAG and NOT instructions).
+ """
+ splitAssertions = []
+ lastVariant = None
+ for assertion in assertions:
+ if (assertion.variant == lastVariant and
+ assertion.variant in [TestAssertion.Variant.DAG, TestAssertion.Variant.Not]):
+ splitAssertions[-1].append(assertion)
+ else:
+ splitAssertions.append([assertion])
+ lastVariant = assertion.variant
+ return splitAssertions
+
+def findMatchingLine(assertion, c1Pass, scope, variables, excludeLines=[]):
+ """ Finds the first line in `c1Pass` which matches `assertion`.
+
+ Scan only lines numbered between `scope.start` and `scope.end` and not on the
+ `excludeLines` list.
+
+ Returns the index of the `c1Pass` line matching the assertion and variables
+ values after the match.
+
+ Raises MatchFailedException if no such `c1Pass` line can be found.
"""
- i = 0
- while i < len(lines) and lines[i].variant == variant:
- i += 1
- return lines[:i], lines[i:]
+ for i in range(scope.start, scope.end):
+ if i in excludeLines: continue
+ newVariables = MatchLines(assertion, c1Pass.body[i], variables)
+ if newVariables is not None:
+ return MatchInfo(MatchScope(i, i), newVariables)
+ raise MatchFailedException(assertion, scope.start)
-def __nextIndependentChecks(checkLines):
- """ Extracts the first sequence of check lines which are independent of each
- other's match location, i.e. either consecutive DAG lines or a single
- InOrder line. Any Not lines preceeding this sequence are also extracted.
+def matchDagGroup(assertions, c1Pass, scope, variables):
+ """ Attempts to find matching `c1Pass` lines for a group of DAG assertions.
+
+ Assertions are matched in the list order and variable values propagated. Only
+ lines in `scope` are scanned and each line can only match one assertion.
+
+ Returns the range of `c1Pass` lines covered by this group (min/max of matching
+ line numbers) and the variable values after the match of the last assertion.
+
+ Raises MatchFailedException when an assertion cannot be satisfied.
"""
- notChecks, checkLines = __splitByVariant(checkLines, TestAssertion.Variant.Not)
- if not checkLines:
- return notChecks, [], []
-
- head, tail = __headAndTail(checkLines)
- if head.variant == TestAssertion.Variant.InOrder:
- return notChecks, [head], tail
- else:
- assert head.variant == TestAssertion.Variant.DAG
- independentChecks, checkLines = __splitByVariant(checkLines, TestAssertion.Variant.DAG)
- return notChecks, independentChecks, checkLines
-
-def __findFirstMatch(checkLine, outputLines, startLineNo, lineFilter, varState):
- """ If successful, returns the line number of the first output line matching
- the check line and the updated variable state. Otherwise returns -1 and
- None, respectively. The 'lineFilter' parameter can be used to supply a
- list of line numbers (counting from 1) which should be skipped.
- """
- matchLineNo = startLineNo
- for outputLine in outputLines:
- if matchLineNo not in lineFilter:
- newVarState = MatchLines(checkLine, outputLine, varState)
- if newVarState is not None:
- return matchLineNo, newVarState
- matchLineNo += 1
- return -1, None
-
-def __matchIndependentChecks(checkLines, outputLines, startLineNo, varState):
- """ Matches the given positive check lines against the output in order of
- appearance. Variable state is propagated but the scope of the search
- remains the same for all checks. Each output line can only be matched
- once. If all check lines are matched, the resulting variable state is
- returned together with the remaining output. The function also returns
- output lines which appear before either of the matched lines so they can
- be tested against Not checks.
- """
- # If no checks are provided, skip over the entire output.
- if not checkLines:
- return outputLines, [], startLineNo + len(outputLines), varState
-
- # Keep track of which lines have been matched.
matchedLines = []
+ for assertion in assertions:
+ assert assertion.variant == TestAssertion.Variant.DAG
+ match = findMatchingLine(assertion, c1Pass, scope, variables, matchedLines)
+ variables = match.variables
+ assert match.scope.start == match.scope.end
+ assert match.scope.start not in matchedLines
+ matchedLines.append(match.scope.start)
+ return MatchInfo(MatchScope(min(matchedLines), max(matchedLines)), variables)
- # Find first unused output line which matches each check line.
- for checkLine in checkLines:
- matchLineNo, varState = \
- __findFirstMatch(checkLine, outputLines, startLineNo, matchedLines, varState)
- if varState is None:
- Logger.testFailed("Could not match check line \"" + checkLine.originalText + "\" " +
- "starting from output line " + str(startLineNo),
- checkLine.fileName, checkLine.lineNo)
- matchedLines.append(matchLineNo)
+def testNotGroup(assertions, c1Pass, scope, variables):
+ """ Verifies that none of the given NOT assertions matches a line inside
+ the given `scope` of `c1Pass` lines.
- # Return new variable state and the output lines which lie outside the
- # match locations of this independent group.
- minMatchLineNo = min(matchedLines)
- maxMatchLineNo = max(matchedLines)
- preceedingLines = outputLines[:minMatchLineNo - startLineNo]
- remainingLines = outputLines[maxMatchLineNo - startLineNo + 1:]
- return preceedingLines, remainingLines, maxMatchLineNo + 1, varState
-
-def __matchNotLines(checkLines, outputLines, startLineNo, varState):
- """ Makes sure that the given check lines do not match any of the given output
- lines. Variable state does not change.
+ Raises MatchFailedException if an assertion matches a line in the scope.
"""
- for checkLine in checkLines:
- assert checkLine.variant == TestAssertion.Variant.Not
- matchLineNo, matchVarState = \
- __findFirstMatch(checkLine, outputLines, startLineNo, [], varState)
- if matchVarState is not None:
- Logger.testFailed("CHECK-NOT line \"" + checkLine.originalText + "\" matches output line " + \
- str(matchLineNo), checkLine.fileName, checkLine.lineNo)
+ for i in range(scope.start, scope.end):
+ line = c1Pass.body[i]
+ for assertion in assertions:
+ assert assertion.variant == TestAssertion.Variant.Not
+ if MatchLines(assertion, line, variables) is not None:
+ raise MatchFailedException(assertion, i)
-def __matchGroups(checkGroup, outputGroup):
- """ Matches the check lines in this group against an output group. It is
- responsible for running the checks in the right order and scope, and
- for propagating the variable state between the check lines.
+def MatchTestCase(testCase, c1Pass):
+ """ Runs a test case against a C1visualizer graph dump.
+
+ Raises MatchFailedException when an assertion cannot be satisfied.
"""
- varState = ImmutableDict()
- checkLines = checkGroup.assertions
- outputLines = outputGroup.body
- startLineNo = outputGroup.startLineNo
+ assert testCase.name == c1Pass.name
- while checkLines:
- # Extract the next sequence of location-independent checks to be matched.
- notChecks, independentChecks, checkLines = __nextIndependentChecks(checkLines)
+ matchFrom = 0
+ variables = ImmutableDict()
+ c1Length = len(c1Pass.body)
- # Match the independent checks.
- notOutput, outputLines, newStartLineNo, newVarState = \
- __matchIndependentChecks(independentChecks, outputLines, startLineNo, varState)
+ # NOT assertions are verified retrospectively, once the scope is known.
+ pendingNotAssertions = None
- # Run the Not checks against the output lines which lie between the last
- # two independent groups or the bounds of the output.
- __matchNotLines(notChecks, notOutput, startLineNo, varState)
+ # Prepare assertions by grouping those that are verified in the same scope.
+ # We also add None as an EOF assertion that will set scope for NOTs.
+ assertionGroups = splitIntoGroups(testCase.assertions)
+ assertionGroups.append(None)
- # Update variable state.
- startLineNo = newStartLineNo
- varState = newVarState
+ for assertionGroup in assertionGroups:
+ if assertionGroup is None:
+ # EOF marker always matches the last+1 line of c1Pass.
+ match = MatchInfo(MatchScope(c1Length, c1Length), None)
+ elif assertionGroup[0].variant == TestAssertion.Variant.Not:
+ # NOT assertions will be tested together with the next group.
+ assert not pendingNotAssertions
+ pendingNotAssertions = assertionGroup
+ continue
+ elif assertionGroup[0].variant == TestAssertion.Variant.InOrder:
+ # Single in-order assertion. Find the first line that matches.
+ assert len(assertionGroup) == 1
+ scope = MatchScope(matchFrom, c1Length)
+ match = findMatchingLine(assertionGroup[0], c1Pass, scope, variables)
+ else:
+ # 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)
+
+ if pendingNotAssertions:
+ # Previous group were NOT assertions. Make sure they don't match any lines
+ # in the [matchFrom, match.start) scope.
+ scope = MatchScope(matchFrom, match.scope.start)
+ testNotGroup(pendingNotAssertions, c1Pass, scope, variables)
+ pendingNotAssertions = None
+
+ # Update state.
+ assert matchFrom <= match.scope.end
+ matchFrom = match.scope.end + 1
+ variables = match.variables
def MatchFiles(checkerFile, c1File):
for testCase in checkerFile.testCases:
@@ -141,8 +152,18 @@
# match a check group against the first output group of the same name.
c1Pass = c1File.findPass(testCase.name)
if c1Pass is None:
- Logger.fail("Test case \"" + testCase.name + "\" not found in the C1visualizer output",
+ Logger.fail("Test case \"{}\" not found in the CFG file".format(testCase.name),
testCase.fileName, testCase.startLineNo)
+
Logger.startTest(testCase.name)
- __matchGroups(testCase, c1Pass)
- Logger.testPassed()
+ try:
+ MatchTestCase(testCase, c1Pass)
+ Logger.testPassed()
+ except MatchFailedException as e:
+ lineNo = c1Pass.startLineNo + e.lineNo
+ if e.assertion.variant == TestAssertion.Variant.Not:
+ Logger.testFailed("NOT assertion matched line {}".format(lineNo),
+ e.assertion.fileName, e.assertion.lineNo)
+ else:
+ Logger.testFailed("Assertion could not be matched starting from line {}".format(lineNo),
+ e.assertion.fileName, e.assertion.lineNo)
diff --git a/tools/checker/match/test.py b/tools/checker/match/test.py
index 97725ad..e4dd784 100644
--- a/tools/checker/match/test.py
+++ b/tools/checker/match/test.py
@@ -18,7 +18,7 @@
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 match.file import MatchFiles
+from match.file import MatchTestCase, MatchFailedException
from match.line import MatchLines
import io
@@ -38,68 +38,71 @@
ToUnicode(c1String),
ImmutableDict(varState))
- def matches(self, checkerString, c1String, varState={}):
- return self.tryMatch(checkerString, c1String, varState) is not None
+ def assertMatches(self, checkerString, c1String, varState={}):
+ self.assertIsNotNone(self.tryMatch(checkerString, c1String, varState))
+
+ def assertDoesNotMatch(self, checkerString, c1String, varState={}):
+ self.assertIsNone(self.tryMatch(checkerString, c1String, varState))
def test_TextAndWhitespace(self):
- self.assertTrue(self.matches("foo", "foo"))
- self.assertTrue(self.matches("foo", " foo "))
- self.assertTrue(self.matches("foo", "foo bar"))
- self.assertFalse(self.matches("foo", "XfooX"))
- self.assertFalse(self.matches("foo", "zoo"))
+ self.assertMatches("foo", "foo")
+ self.assertMatches("foo", " foo ")
+ self.assertMatches("foo", "foo bar")
+ self.assertDoesNotMatch("foo", "XfooX")
+ self.assertDoesNotMatch("foo", "zoo")
- self.assertTrue(self.matches("foo bar", "foo bar"))
- self.assertTrue(self.matches("foo bar", "abc foo bar def"))
- self.assertTrue(self.matches("foo bar", "foo foo bar bar"))
+ self.assertMatches("foo bar", "foo bar")
+ self.assertMatches("foo bar", "abc foo bar def")
+ self.assertMatches("foo bar", "foo foo bar bar")
- self.assertTrue(self.matches("foo bar", "foo X bar"))
- self.assertFalse(self.matches("foo bar", "foo Xbar"))
+ self.assertMatches("foo bar", "foo X bar")
+ self.assertDoesNotMatch("foo bar", "foo Xbar")
def test_Pattern(self):
- self.assertTrue(self.matches("foo{{A|B}}bar", "fooAbar"))
- self.assertTrue(self.matches("foo{{A|B}}bar", "fooBbar"))
- self.assertFalse(self.matches("foo{{A|B}}bar", "fooCbar"))
+ self.assertMatches("foo{{A|B}}bar", "fooAbar")
+ self.assertMatches("foo{{A|B}}bar", "fooBbar")
+ self.assertDoesNotMatch("foo{{A|B}}bar", "fooCbar")
def test_VariableReference(self):
- self.assertTrue(self.matches("foo<<X>>bar", "foobar", {"X": ""}))
- self.assertTrue(self.matches("foo<<X>>bar", "fooAbar", {"X": "A"}))
- self.assertTrue(self.matches("foo<<X>>bar", "fooBbar", {"X": "B"}))
- self.assertFalse(self.matches("foo<<X>>bar", "foobar", {"X": "A"}))
- self.assertFalse(self.matches("foo<<X>>bar", "foo bar", {"X": "A"}))
+ self.assertMatches("foo<<X>>bar", "foobar", {"X": ""})
+ self.assertMatches("foo<<X>>bar", "fooAbar", {"X": "A"})
+ self.assertMatches("foo<<X>>bar", "fooBbar", {"X": "B"})
+ self.assertDoesNotMatch("foo<<X>>bar", "foobar", {"X": "A"})
+ self.assertDoesNotMatch("foo<<X>>bar", "foo bar", {"X": "A"})
with self.assertRaises(CheckerException):
- self.assertTrue(self.matches("foo<<X>>bar", "foobar", {}))
+ self.tryMatch("foo<<X>>bar", "foobar", {})
def test_VariableDefinition(self):
- self.assertTrue(self.matches("foo<<X:A|B>>bar", "fooAbar"))
- self.assertTrue(self.matches("foo<<X:A|B>>bar", "fooBbar"))
- self.assertFalse(self.matches("foo<<X:A|B>>bar", "fooCbar"))
+ self.assertMatches("foo<<X:A|B>>bar", "fooAbar")
+ self.assertMatches("foo<<X:A|B>>bar", "fooBbar")
+ self.assertDoesNotMatch("foo<<X:A|B>>bar", "fooCbar")
env = self.tryMatch("foo<<X:A.*B>>bar", "fooABbar", {})
self.assertEqual(env, {"X": "AB"})
env = self.tryMatch("foo<<X:A.*B>>bar", "fooAxxBbar", {})
self.assertEqual(env, {"X": "AxxB"})
- self.assertTrue(self.matches("foo<<X:A|B>>bar<<X>>baz", "fooAbarAbaz"))
- self.assertTrue(self.matches("foo<<X:A|B>>bar<<X>>baz", "fooBbarBbaz"))
- self.assertFalse(self.matches("foo<<X:A|B>>bar<<X>>baz", "fooAbarBbaz"))
+ self.assertMatches("foo<<X:A|B>>bar<<X>>baz", "fooAbarAbaz")
+ self.assertMatches("foo<<X:A|B>>bar<<X>>baz", "fooBbarBbaz")
+ self.assertDoesNotMatch("foo<<X:A|B>>bar<<X>>baz", "fooAbarBbaz")
def test_NoVariableRedefinition(self):
with self.assertRaises(CheckerException):
- self.matches("<<X:...>><<X>><<X:...>><<X>>", "foofoobarbar")
+ self.tryMatch("<<X:...>><<X>><<X:...>><<X>>", "foofoobarbar")
def test_EnvNotChangedOnPartialMatch(self):
env = {"Y": "foo"}
- self.assertFalse(self.matches("<<X:A>>bar", "Abaz", env))
+ self.assertDoesNotMatch("<<X:A>>bar", "Abaz", env)
self.assertFalse("X" in env.keys())
def test_VariableContentEscaped(self):
- self.assertTrue(self.matches("<<X:..>>foo<<X>>", ".*foo.*"))
- self.assertFalse(self.matches("<<X:..>>foo<<X>>", ".*fooAAAA"))
+ self.assertMatches("<<X:..>>foo<<X>>", ".*foo.*")
+ self.assertDoesNotMatch("<<X:..>>foo<<X>>", ".*fooAAAA")
class MatchFiles_Test(unittest.TestCase):
- def matches(self, checkerString, c1String):
+ def assertMatches(self, checkerString, c1String):
checkerString = \
"""
// CHECK-START: MyMethod MyPass
@@ -119,22 +122,24 @@
"""
checkerFile = ParseCheckerStream("<test-file>", "CHECK", io.StringIO(ToUnicode(checkerString)))
c1File = ParseC1visualizerStream("<c1-file>", io.StringIO(ToUnicode(c1String)))
- try:
- MatchFiles(checkerFile, c1File)
- return True
- except CheckerException:
- return False
+ assert len(checkerFile.testCases) == 1
+ assert len(c1File.passes) == 1
+ MatchTestCase(checkerFile.testCases[0], c1File.passes[0])
+
+ def assertDoesNotMatch(self, checkerString, c1String):
+ with self.assertRaises(MatchFailedException):
+ self.assertMatches(checkerString, c1String)
def test_Text(self):
- self.assertTrue(self.matches( "// CHECK: foo bar", "foo bar"))
- self.assertFalse(self.matches("// CHECK: foo bar", "abc def"))
+ self.assertMatches("// CHECK: foo bar", "foo bar")
+ self.assertDoesNotMatch("// CHECK: foo bar", "abc def")
def test_Pattern(self):
- self.assertTrue(self.matches( "// CHECK: abc {{de.}}", "abc de#"))
- self.assertFalse(self.matches("// CHECK: abc {{de.}}", "abc d#f"))
+ self.assertMatches("// CHECK: abc {{de.}}", "abc de#")
+ self.assertDoesNotMatch("// CHECK: abc {{de.}}", "abc d#f")
def test_Variables(self):
- self.assertTrue(self.matches(
+ self.assertMatches(
"""
// CHECK: foo<<X:.>>bar
// CHECK: abc<<X>>def
@@ -142,8 +147,8 @@
"""
foo0bar
abc0def
- """))
- self.assertTrue(self.matches(
+ """)
+ self.assertMatches(
"""
// CHECK: foo<<X:([0-9]+)>>bar
// CHECK: abc<<X>>def
@@ -153,8 +158,8 @@
foo1234bar
abc1234def
### 1234 ###
- """))
- self.assertFalse(self.matches(
+ """)
+ self.assertDoesNotMatch(
"""
// CHECK: foo<<X:([0-9]+)>>bar
// CHECK: abc<<X>>def
@@ -162,16 +167,16 @@
"""
foo1234bar
abc1235def
- """))
+ """)
def test_WholeWordMustMatch(self):
- self.assertTrue(self.matches( "// CHECK: b{{.}}r", "abc bar def"))
- self.assertFalse(self.matches( "// CHECK: b{{.}}r", "abc Xbar def"))
- self.assertFalse(self.matches( "// CHECK: b{{.}}r", "abc barX def"))
- self.assertFalse(self.matches( "// CHECK: b{{.}}r", "abc b r def"))
+ self.assertMatches("// CHECK: b{{.}}r", "abc bar def")
+ self.assertDoesNotMatch("// CHECK: b{{.}}r", "abc Xbar def")
+ self.assertDoesNotMatch("// CHECK: b{{.}}r", "abc barX def")
+ self.assertDoesNotMatch("// CHECK: b{{.}}r", "abc b r def")
def test_InOrderAssertions(self):
- self.assertTrue(self.matches(
+ self.assertMatches(
"""
// CHECK: foo
// CHECK: bar
@@ -179,8 +184,8 @@
"""
foo
bar
- """))
- self.assertFalse(self.matches(
+ """)
+ self.assertDoesNotMatch(
"""
// CHECK: foo
// CHECK: bar
@@ -188,10 +193,10 @@
"""
bar
foo
- """))
+ """)
def test_DagAssertions(self):
- self.assertTrue(self.matches(
+ self.assertMatches(
"""
// CHECK-DAG: foo
// CHECK-DAG: bar
@@ -199,8 +204,8 @@
"""
foo
bar
- """))
- self.assertTrue(self.matches(
+ """)
+ self.assertMatches(
"""
// CHECK-DAG: foo
// CHECK-DAG: bar
@@ -208,10 +213,10 @@
"""
bar
foo
- """))
+ """)
def test_DagAssertionsScope(self):
- self.assertTrue(self.matches(
+ self.assertMatches(
"""
// CHECK: foo
// CHECK-DAG: abc
@@ -223,8 +228,8 @@
def
abc
bar
- """))
- self.assertFalse(self.matches(
+ """)
+ self.assertDoesNotMatch(
"""
// CHECK: foo
// CHECK-DAG: abc
@@ -236,8 +241,8 @@
abc
bar
def
- """))
- self.assertFalse(self.matches(
+ """)
+ self.assertDoesNotMatch(
"""
// CHECK: foo
// CHECK-DAG: abc
@@ -249,26 +254,26 @@
def
bar
abc
- """))
+ """)
def test_NotAssertions(self):
- self.assertTrue(self.matches(
+ self.assertMatches(
"""
// CHECK-NOT: foo
""",
"""
abc
def
- """))
- self.assertFalse(self.matches(
+ """)
+ self.assertDoesNotMatch(
"""
// CHECK-NOT: foo
""",
"""
abc foo
def
- """))
- self.assertFalse(self.matches(
+ """)
+ self.assertDoesNotMatch(
"""
// CHECK-NOT: foo
// CHECK-NOT: bar
@@ -276,10 +281,10 @@
"""
abc
def bar
- """))
+ """)
def test_NotAssertionsScope(self):
- self.assertTrue(self.matches(
+ self.assertMatches(
"""
// CHECK: abc
// CHECK-NOT: foo
@@ -288,8 +293,8 @@
"""
abc
def
- """))
- self.assertTrue(self.matches(
+ """)
+ self.assertMatches(
"""
// CHECK: abc
// CHECK-NOT: foo
@@ -299,8 +304,8 @@
abc
def
foo
- """))
- self.assertFalse(self.matches(
+ """)
+ self.assertDoesNotMatch(
"""
// CHECK: abc
// CHECK-NOT: foo
@@ -310,10 +315,10 @@
abc
foo
def
- """))
+ """)
def test_LineOnlyMatchesOnce(self):
- self.assertTrue(self.matches(
+ self.assertMatches(
"""
// CHECK-DAG: foo
// CHECK-DAG: foo
@@ -322,8 +327,8 @@
foo
abc
foo
- """))
- self.assertFalse(self.matches(
+ """)
+ self.assertDoesNotMatch(
"""
// CHECK-DAG: foo
// CHECK-DAG: foo
@@ -332,4 +337,4 @@
foo
abc
bar
- """))
+ """)