1 # Copyright (C) 2014 The Android Open Source Project 2 # 3 # Licensed under the Apache License, Version 2.0 (the "License"); 4 # you may not use this file except in compliance with the License. 5 # You may obtain a copy of the License at 6 # 7 # http://www.apache.org/licenses/LICENSE-2.0 8 # 9 # Unless required by applicable law or agreed to in writing, software 10 # distributed under the License is distributed on an "AS IS" BASIS, 11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 # See the License for the specific language governing permissions and 13 # limitations under the License. 14 15 from collections import namedtuple 16 from common.immutables import ImmutableDict 17 from common.logger import Logger 18 from file_format.c1visualizer.struct import C1visualizerFile, C1visualizerPass 19 from file_format.checker.struct import CheckerFile, TestCase, TestAssertion 20 from match.line import MatchLines, EvaluateLine 21 22 MatchScope = namedtuple("MatchScope", ["start", "end"]) 23 MatchInfo = namedtuple("MatchInfo", ["scope", "variables"]) 24 25 class MatchFailedException(Exception): 26 def __init__(self, assertion, lineNo, variables): 27 self.assertion = assertion 28 self.lineNo = lineNo 29 self.variables = variables 30 31 def splitIntoGroups(assertions): 32 """ Breaks up a list of assertions, grouping instructions which should be 33 tested in the same scope (consecutive DAG and NOT instructions). 34 """ 35 splitAssertions = [] 36 lastVariant = None 37 for assertion in assertions: 38 if (assertion.variant == lastVariant and 39 assertion.variant in [TestAssertion.Variant.DAG, TestAssertion.Variant.Not]): 40 splitAssertions[-1].append(assertion) 41 else: 42 splitAssertions.append([assertion]) 43 lastVariant = assertion.variant 44 return splitAssertions 45 46 def findMatchingLine(assertion, c1Pass, scope, variables, excludeLines=[]): 47 """ Finds the first line in `c1Pass` which matches `assertion`. 48 49 Scan only lines numbered between `scope.start` and `scope.end` and not on the 50 `excludeLines` list. 51 52 Returns the index of the `c1Pass` line matching the assertion and variables 53 values after the match. 54 55 Raises MatchFailedException if no such `c1Pass` line can be found. 56 """ 57 for i in range(scope.start, scope.end): 58 if i in excludeLines: continue 59 newVariables = MatchLines(assertion, c1Pass.body[i], variables) 60 if newVariables is not None: 61 return MatchInfo(MatchScope(i, i), newVariables) 62 raise MatchFailedException(assertion, scope.start, variables) 63 64 def matchDagGroup(assertions, c1Pass, scope, variables): 65 """ Attempts to find matching `c1Pass` lines for a group of DAG assertions. 66 67 Assertions are matched in the list order and variable values propagated. Only 68 lines in `scope` are scanned and each line can only match one assertion. 69 70 Returns the range of `c1Pass` lines covered by this group (min/max of matching 71 line numbers) and the variable values after the match of the last assertion. 72 73 Raises MatchFailedException when an assertion cannot be satisfied. 74 """ 75 matchedLines = [] 76 for assertion in assertions: 77 assert assertion.variant == TestAssertion.Variant.DAG 78 match = findMatchingLine(assertion, c1Pass, scope, variables, matchedLines) 79 variables = match.variables 80 assert match.scope.start == match.scope.end 81 assert match.scope.start not in matchedLines 82 matchedLines.append(match.scope.start) 83 return MatchInfo(MatchScope(min(matchedLines), max(matchedLines)), variables) 84 85 def testNotGroup(assertions, c1Pass, scope, variables): 86 """ Verifies that none of the given NOT assertions matches a line inside 87 the given `scope` of `c1Pass` lines. 88 89 Raises MatchFailedException if an assertion matches a line in the scope. 90 """ 91 for i in range(scope.start, scope.end): 92 line = c1Pass.body[i] 93 for assertion in assertions: 94 assert assertion.variant == TestAssertion.Variant.Not 95 if MatchLines(assertion, line, variables) is not None: 96 raise MatchFailedException(assertion, i, variables) 97 98 def testEvalGroup(assertions, scope, variables): 99 for assertion in assertions: 100 if not EvaluateLine(assertion, variables): 101 raise MatchFailedException(assertion, scope.start, variables) 102 103 def MatchTestCase(testCase, c1Pass): 104 """ Runs a test case against a C1visualizer graph dump. 105 106 Raises MatchFailedException when an assertion cannot be satisfied. 107 """ 108 assert testCase.name == c1Pass.name 109 110 matchFrom = 0 111 variables = ImmutableDict() 112 c1Length = len(c1Pass.body) 113 114 # NOT assertions are verified retrospectively, once the scope is known. 115 pendingNotAssertions = None 116 117 # Prepare assertions by grouping those that are verified in the same scope. 118 # We also add None as an EOF assertion that will set scope for NOTs. 119 assertionGroups = splitIntoGroups(testCase.assertions) 120 assertionGroups.append(None) 121 122 for assertionGroup in assertionGroups: 123 if assertionGroup is None: 124 # EOF marker always matches the last+1 line of c1Pass. 125 match = MatchInfo(MatchScope(c1Length, c1Length), None) 126 elif assertionGroup[0].variant == TestAssertion.Variant.Not: 127 # NOT assertions will be tested together with the next group. 128 assert not pendingNotAssertions 129 pendingNotAssertions = assertionGroup 130 continue 131 elif assertionGroup[0].variant == TestAssertion.Variant.InOrder: 132 # Single in-order assertion. Find the first line that matches. 133 assert len(assertionGroup) == 1 134 scope = MatchScope(matchFrom, c1Length) 135 match = findMatchingLine(assertionGroup[0], c1Pass, scope, variables) 136 elif assertionGroup[0].variant == TestAssertion.Variant.NextLine: 137 # Single next-line assertion. Test if the current line matches. 138 assert len(assertionGroup) == 1 139 scope = MatchScope(matchFrom, matchFrom + 1) 140 match = findMatchingLine(assertionGroup[0], c1Pass, scope, variables) 141 elif assertionGroup[0].variant == TestAssertion.Variant.DAG: 142 # A group of DAG assertions. Match them all starting from the same point. 143 scope = MatchScope(matchFrom, c1Length) 144 match = matchDagGroup(assertionGroup, c1Pass, scope, variables) 145 else: 146 assert assertionGroup[0].variant == TestAssertion.Variant.Eval 147 scope = MatchScope(matchFrom, c1Length) 148 testEvalGroup(assertionGroup, scope, variables) 149 continue 150 151 if pendingNotAssertions: 152 # Previous group were NOT assertions. Make sure they don't match any lines 153 # in the [matchFrom, match.start) scope. 154 scope = MatchScope(matchFrom, match.scope.start) 155 testNotGroup(pendingNotAssertions, c1Pass, scope, variables) 156 pendingNotAssertions = None 157 158 # Update state. 159 assert matchFrom <= match.scope.end 160 matchFrom = match.scope.end + 1 161 variables = match.variables 162 163 def MatchFiles(checkerFile, c1File, targetArch, debuggableMode): 164 for testCase in checkerFile.testCases: 165 if testCase.testArch not in [None, targetArch]: 166 continue 167 if testCase.forDebuggable != debuggableMode: 168 continue 169 170 # TODO: Currently does not handle multiple occurrences of the same group 171 # name, e.g. when a pass is run multiple times. It will always try to 172 # match a check group against the first output group of the same name. 173 c1Pass = c1File.findPass(testCase.name) 174 if c1Pass is None: 175 Logger.fail("Test case not found in the CFG file", 176 testCase.fileName, testCase.startLineNo, testCase.name) 177 178 Logger.startTest(testCase.name) 179 try: 180 MatchTestCase(testCase, c1Pass) 181 Logger.testPassed() 182 except MatchFailedException as e: 183 lineNo = c1Pass.startLineNo + e.lineNo 184 if e.assertion.variant == TestAssertion.Variant.Not: 185 msg = "NOT assertion matched line {}" 186 else: 187 msg = "Assertion could not be matched starting from line {}" 188 msg = msg.format(lineNo) 189 Logger.testFailed(msg, e.assertion, e.variables) 190