# Copyright (C) 2014 The Android Open Source Project
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# 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, EvaluateLine
MatchScope = namedtuple("MatchScope", ["start", "end"])
MatchInfo = namedtuple("MatchInfo", ["scope", "variables"])
class MatchFailedException(Exception):
def __init__(self, assertion, lineNo, variables):
self.assertion = assertion
self.lineNo = lineNo
self.variables = variables
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.
"""
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, variables)
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.
"""
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)
def testNotGroup(assertions, c1Pass, scope, variables):
""" Verifies that none of the given NOT assertions matches a line inside
the given `scope` of `c1Pass` lines.
Raises MatchFailedException if an assertion matches a line in the scope.
"""
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, variables)
def testEvalGroup(assertions, scope, variables):
for assertion in assertions:
if not EvaluateLine(assertion, variables):
raise MatchFailedException(assertion, scope.start, variables)
def MatchTestCase(testCase, c1Pass):
""" Runs a test case against a C1visualizer graph dump.
Raises MatchFailedException when an assertion cannot be satisfied.
"""
assert testCase.name == c1Pass.name
matchFrom = 0
variables = ImmutableDict()
c1Length = len(c1Pass.body)
# NOT assertions are verified retrospectively, once the scope is known.
pendingNotAssertions = None
# 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)
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)
elif assertionGroup[0].variant == TestAssertion.Variant.NextLine:
# Single next-line assertion. Test if the current line matches.
assert len(assertionGroup) == 1
scope = MatchScope(matchFrom, matchFrom + 1)
match = findMatchingLine(assertionGroup[0], c1Pass, scope, variables)
elif assertionGroup[0].variant == TestAssertion.Variant.DAG:
# A group of DAG assertions. Match them all starting from the same point.
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
# 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, targetArch, debuggableMode):
for testCase in checkerFile.testCases:
if testCase.testArch not in [None, targetArch]:
continue
if testCase.forDebuggable != debuggableMode:
continue
# TODO: Currently does not handle multiple occurrences of the same group
# name, e.g. when a pass is run multiple times. It will always try to
# 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 not found in the CFG file",
testCase.fileName, testCase.startLineNo, testCase.name)
Logger.startTest(testCase.name)
try:
MatchTestCase(testCase, c1Pass)
Logger.testPassed()
except MatchFailedException as e:
lineNo = c1Pass.startLineNo + e.lineNo
if e.assertion.variant == TestAssertion.Variant.Not:
msg = "NOT assertion matched line {}"
else:
msg = "Assertion could not be matched starting from line {}"
msg = msg.format(lineNo)
Logger.testFailed(msg, e.assertion, e.variables)