blob: 55f015e68a1757792ce61fe5e865cf13c1ad684e [file] [log] [blame]
David Brazdil123c5e92015-01-20 09:28:38 +00001#!/usr/bin/env python2
David Brazdilee690a32014-12-01 17:04:16 +00002#
3# Copyright (C) 2014 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
17
18# Checker is a testing tool which compiles a given test file and compares the
19# state of the control-flow graph before and after each optimization pass
20# against a set of assertions specified alongside the tests.
21#
22# Tests are written in Java, turned into DEX and compiled with the Optimizing
David Brazdil9a6f20e2014-12-19 11:17:21 +000023# compiler. "Check lines" are assertions formatted as comments of the Java file.
24# They begin with prefix 'CHECK' followed by a pattern that the engine attempts
25# to match in the compiler-generated output.
David Brazdilee690a32014-12-01 17:04:16 +000026#
27# Assertions are tested in groups which correspond to the individual compiler
28# passes. Each group of check lines therefore must start with a 'CHECK-START'
29# header which specifies the output group it should be tested against. The group
30# name must exactly match one of the groups recognized in the output (they can
31# be listed with the '--list-groups' command-line flag).
32#
David Brazdil9a6f20e2014-12-19 11:17:21 +000033# Matching of check lines is carried out in the order of appearance in the
34# source file. There are three types of check lines:
35# - CHECK: Must match an output line which appears in the output group
36# later than lines matched against any preceeding checks. Output
37# lines must therefore match the check lines in the same order.
38# These are referred to as "in-order" checks in the code.
39# - CHECK-DAG: Must match an output line which appears in the output group
40# later than lines matched against any preceeding in-order checks.
41# In other words, the order of output lines does not matter
42# between consecutive DAG checks.
David Brazdil48942de2015-01-07 21:19:50 +000043# - CHECK-NOT: Must not match any output line which appears in the output group
David Brazdil9a6f20e2014-12-19 11:17:21 +000044# later than lines matched against any preceeding checks and
45# earlier than lines matched against any subsequent checks.
46# Surrounding non-negative checks (or boundaries of the group)
47# therefore create a scope within which the assertion is verified.
48#
49# Check-line patterns are treated as plain text rather than regular expressions
David Brazdilee690a32014-12-01 17:04:16 +000050# but are whitespace agnostic.
51#
52# Actual regex patterns can be inserted enclosed in '{{' and '}}' brackets. If
53# curly brackets need to be used inside the body of the regex, they need to be
54# enclosed in round brackets. For example, the pattern '{{foo{2}}}' will parse
55# the invalid regex 'foo{2', but '{{(fo{2})}}' will match 'foo'.
56#
57# Regex patterns can be named and referenced later. A new variable is defined
58# with '[[name:regex]]' and can be referenced with '[[name]]'. Variables are
59# only valid within the scope of the defining group. Within a group they cannot
60# be redefined or used undefined.
61#
62# Example:
63# The following assertions can be placed in a Java source file:
64#
65# // CHECK-START: int MyClass.MyMethod() constant_folding (after)
66# // CHECK: [[ID:i[0-9]+]] IntConstant {{11|22}}
67# // CHECK: Return [ [[ID]] ]
68#
69# The engine will attempt to match the check lines against the output of the
70# group named on the first line. Together they verify that the CFG after
71# constant folding returns an integer constant with value either 11 or 22.
72#
73
David Brazdil123c5e92015-01-20 09:28:38 +000074from __future__ import print_function
David Brazdilee690a32014-12-01 17:04:16 +000075import argparse
76import os
77import re
78import shutil
79import sys
80import tempfile
David Brazdilee690a32014-12-01 17:04:16 +000081
David Brazdil2e15cd22014-12-31 17:28:38 +000082class Logger(object):
David Brazdil7cca5df2015-01-15 00:40:56 +000083
84 class Level(object):
85 NoOutput, Error, Info = range(3)
David Brazdil2e15cd22014-12-31 17:28:38 +000086
87 class Color(object):
88 Default, Blue, Gray, Purple, Red = range(5)
89
90 @staticmethod
91 def terminalCode(color, out=sys.stdout):
92 if not out.isatty():
93 return ''
94 elif color == Logger.Color.Blue:
95 return '\033[94m'
96 elif color == Logger.Color.Gray:
97 return '\033[37m'
98 elif color == Logger.Color.Purple:
99 return '\033[95m'
100 elif color == Logger.Color.Red:
101 return '\033[91m'
102 else:
103 return '\033[0m'
104
David Brazdil7cca5df2015-01-15 00:40:56 +0000105 Verbosity = Level.Info
106
David Brazdil2e15cd22014-12-31 17:28:38 +0000107 @staticmethod
David Brazdil7cca5df2015-01-15 00:40:56 +0000108 def log(text, level=Level.Info, color=Color.Default, newLine=True, out=sys.stdout):
109 if level <= Logger.Verbosity:
David Brazdil2e15cd22014-12-31 17:28:38 +0000110 text = Logger.Color.terminalCode(color, out) + text + \
111 Logger.Color.terminalCode(Logger.Color.Default, out)
112 if newLine:
David Brazdil7cca5df2015-01-15 00:40:56 +0000113 print(text, flush=True, file=out)
David Brazdil2e15cd22014-12-31 17:28:38 +0000114 else:
115 print(text, end="", flush=True, file=out)
116
117 @staticmethod
118 def fail(msg, file=None, line=-1):
119 location = ""
120 if file:
121 location += file + ":"
122 if line > 0:
123 location += str(line) + ":"
124 if location:
125 location += " "
126
David Brazdil7cca5df2015-01-15 00:40:56 +0000127 Logger.log(location, Logger.Level.Error, color=Logger.Color.Gray, newLine=False, out=sys.stderr)
128 Logger.log("error: ", Logger.Level.Error, color=Logger.Color.Red, newLine=False, out=sys.stderr)
129 Logger.log(msg, Logger.Level.Error, out=sys.stderr)
David Brazdil2e15cd22014-12-31 17:28:38 +0000130 sys.exit(1)
131
132 @staticmethod
133 def startTest(name):
134 Logger.log("TEST ", color=Logger.Color.Purple, newLine=False)
135 Logger.log(name + "... ", newLine=False)
136
137 @staticmethod
138 def testPassed():
139 Logger.log("PASS", color=Logger.Color.Blue)
140
141 @staticmethod
142 def testFailed(msg, file=None, line=-1):
143 Logger.log("FAIL", color=Logger.Color.Red)
144 Logger.fail(msg, file, line)
145
David Brazdilee690a32014-12-01 17:04:16 +0000146class CommonEqualityMixin:
147 """Mixin for class equality as equality of the fields."""
148 def __eq__(self, other):
149 return (isinstance(other, self.__class__)
150 and self.__dict__ == other.__dict__)
151
152 def __ne__(self, other):
153 return not self.__eq__(other)
154
155 def __repr__(self):
156 return "<%s: %s>" % (type(self).__name__, str(self.__dict__))
157
158
159class CheckElement(CommonEqualityMixin):
160 """Single element of the check line."""
161
162 class Variant(object):
163 """Supported language constructs."""
164 Text, Pattern, VarRef, VarDef = range(4)
165
David Brazdilbe0cc082014-12-31 11:49:30 +0000166 rStartOptional = r"("
167 rEndOptional = r")?"
168
169 rName = r"([a-zA-Z][a-zA-Z0-9]*)"
170 rRegex = r"(.+?)"
171 rPatternStartSym = r"(\{\{)"
172 rPatternEndSym = r"(\}\})"
173 rVariableStartSym = r"(\[\[)"
174 rVariableEndSym = r"(\]\])"
175 rVariableSeparator = r"(:)"
176
177 regexPattern = rPatternStartSym + rRegex + rPatternEndSym
178 regexVariable = rVariableStartSym + \
179 rName + \
180 (rStartOptional + rVariableSeparator + rRegex + rEndOptional) + \
181 rVariableEndSym
182
David Brazdilee690a32014-12-01 17:04:16 +0000183 def __init__(self, variant, name, pattern):
184 self.variant = variant
185 self.name = name
186 self.pattern = pattern
187
188 @staticmethod
189 def parseText(text):
190 return CheckElement(CheckElement.Variant.Text, None, re.escape(text))
191
192 @staticmethod
193 def parsePattern(patternElem):
David Brazdilbe0cc082014-12-31 11:49:30 +0000194 return CheckElement(CheckElement.Variant.Pattern, None, patternElem[2:-2])
David Brazdilee690a32014-12-01 17:04:16 +0000195
196 @staticmethod
197 def parseVariable(varElem):
198 colonPos = varElem.find(":")
199 if colonPos == -1:
200 # Variable reference
David Brazdilbe0cc082014-12-31 11:49:30 +0000201 name = varElem[2:-2]
David Brazdilee690a32014-12-01 17:04:16 +0000202 return CheckElement(CheckElement.Variant.VarRef, name, None)
203 else:
204 # Variable definition
205 name = varElem[2:colonPos]
David Brazdilbe0cc082014-12-31 11:49:30 +0000206 body = varElem[colonPos+1:-2]
David Brazdilee690a32014-12-01 17:04:16 +0000207 return CheckElement(CheckElement.Variant.VarDef, name, body)
208
David Brazdilee690a32014-12-01 17:04:16 +0000209class CheckLine(CommonEqualityMixin):
210 """Representation of a single assertion in the check file formed of one or
211 more regex elements. Matching against an output line is successful only
212 if all regex elements can be matched in the given order."""
213
David Brazdil9a6f20e2014-12-19 11:17:21 +0000214 class Variant(object):
215 """Supported types of assertions."""
216 InOrder, DAG, Not = range(3)
David Brazdilee690a32014-12-01 17:04:16 +0000217
David Brazdil2e15cd22014-12-31 17:28:38 +0000218 def __init__(self, content, variant=Variant.InOrder, fileName=None, lineNo=-1):
219 self.fileName = fileName
David Brazdilee690a32014-12-01 17:04:16 +0000220 self.lineNo = lineNo
David Brazdil2e15cd22014-12-31 17:28:38 +0000221 self.content = content.strip()
David Brazdilee690a32014-12-01 17:04:16 +0000222
David Brazdil2e15cd22014-12-31 17:28:38 +0000223 self.variant = variant
David Brazdil9a6f20e2014-12-19 11:17:21 +0000224 self.lineParts = self.__parse(self.content)
David Brazdilee690a32014-12-01 17:04:16 +0000225 if not self.lineParts:
David Brazdil2e15cd22014-12-31 17:28:38 +0000226 Logger.fail("Empty check line", self.fileName, self.lineNo)
227
228 if self.variant == CheckLine.Variant.Not:
229 for elem in self.lineParts:
230 if elem.variant == CheckElement.Variant.VarDef:
231 Logger.fail("CHECK-NOT lines cannot define variables", self.fileName, self.lineNo)
232
233 def __eq__(self, other):
234 return (isinstance(other, self.__class__) and
235 self.variant == other.variant and
236 self.lineParts == other.lineParts)
David Brazdilee690a32014-12-01 17:04:16 +0000237
238 # Returns True if the given Match object was at the beginning of the line.
239 def __isMatchAtStart(self, match):
240 return (match is not None) and (match.start() == 0)
241
242 # Takes in a list of Match objects and returns the minimal start point among
243 # them. If there aren't any successful matches it returns the length of
244 # the searched string.
245 def __firstMatch(self, matches, string):
246 starts = map(lambda m: len(string) if m is None else m.start(), matches)
247 return min(starts)
248
David Brazdilee690a32014-12-01 17:04:16 +0000249 # This method parses the content of a check line stripped of the initial
250 # comment symbol and the CHECK keyword.
251 def __parse(self, line):
252 lineParts = []
253 # Loop as long as there is something to parse.
254 while line:
255 # Search for the nearest occurrence of the special markers.
David Brazdilbe0cc082014-12-31 11:49:30 +0000256 matchWhitespace = re.search(r"\s+", line)
257 matchPattern = re.search(CheckElement.regexPattern, line)
258 matchVariable = re.search(CheckElement.regexVariable, line)
David Brazdilee690a32014-12-01 17:04:16 +0000259
260 # If one of the above was identified at the current position, extract them
261 # from the line, parse them and add to the list of line parts.
262 if self.__isMatchAtStart(matchWhitespace):
263 # We want to be whitespace-agnostic so whenever a check line contains
264 # a whitespace, we add a regex pattern for an arbitrary non-zero number
265 # of whitespaces.
266 line = line[matchWhitespace.end():]
David Brazdilbe0cc082014-12-31 11:49:30 +0000267 lineParts.append(CheckElement.parsePattern(r"{{\s+}}"))
David Brazdilee690a32014-12-01 17:04:16 +0000268 elif self.__isMatchAtStart(matchPattern):
269 pattern = line[0:matchPattern.end()]
270 line = line[matchPattern.end():]
271 lineParts.append(CheckElement.parsePattern(pattern))
272 elif self.__isMatchAtStart(matchVariable):
273 var = line[0:matchVariable.end()]
274 line = line[matchVariable.end():]
David Brazdil2e15cd22014-12-31 17:28:38 +0000275 lineParts.append(CheckElement.parseVariable(var))
David Brazdilee690a32014-12-01 17:04:16 +0000276 else:
277 # If we're not currently looking at a special marker, this is a plain
278 # text match all the way until the first special marker (or the end
279 # of the line).
280 firstMatch = self.__firstMatch([ matchWhitespace, matchPattern, matchVariable ], line)
281 text = line[0:firstMatch]
282 line = line[firstMatch:]
283 lineParts.append(CheckElement.parseText(text))
284 return lineParts
285
286 # Returns the regex pattern to be matched in the output line. Variable
287 # references are substituted with their current values provided in the
288 # 'varState' argument.
289 # An exception is raised if a referenced variable is undefined.
290 def __generatePattern(self, linePart, varState):
291 if linePart.variant == CheckElement.Variant.VarRef:
292 try:
293 return re.escape(varState[linePart.name])
294 except KeyError:
David Brazdil2e15cd22014-12-31 17:28:38 +0000295 Logger.testFailed("Use of undefined variable \"" + linePart.name + "\"",
296 self.fileName, self.lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000297 else:
298 return linePart.pattern
299
300 # Attempts to match the check line against a line from the output file with
301 # the given initial variable values. It returns the new variable state if
302 # successful and None otherwise.
303 def match(self, outputLine, initialVarState):
304 initialSearchFrom = 0
305 initialPattern = self.__generatePattern(self.lineParts[0], initialVarState)
306 while True:
307 # Search for the first element on the regex parts list. This will mark
308 # the point on the line from which we will attempt to match the rest of
309 # the check pattern. If this iteration produces only a partial match,
310 # the next iteration will start searching further in the output.
311 firstMatch = re.search(initialPattern, outputLine[initialSearchFrom:])
312 if firstMatch is None:
313 return None
314 matchStart = initialSearchFrom + firstMatch.start()
315 initialSearchFrom += firstMatch.start() + 1
316
317 # Do the full matching on a shadow copy of the variable state. If the
318 # matching fails half-way, we will not need to revert the state.
319 varState = dict(initialVarState)
320
321 # Now try to parse all of the parts of the check line in the right order.
322 # Variable values are updated on-the-fly, meaning that a variable can
323 # be referenced immediately after its definition.
324 fullyMatched = True
325 for part in self.lineParts:
326 pattern = self.__generatePattern(part, varState)
327 match = re.match(pattern, outputLine[matchStart:])
328 if match is None:
329 fullyMatched = False
330 break
331 matchEnd = matchStart + match.end()
332 if part.variant == CheckElement.Variant.VarDef:
333 if part.name in varState:
David Brazdil2e15cd22014-12-31 17:28:38 +0000334 Logger.testFailed("Multiple definitions of variable \"" + part.name + "\"",
335 self.fileName, self.lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000336 varState[part.name] = outputLine[matchStart:matchEnd]
337 matchStart = matchEnd
338
339 # Return the new variable state if all parts were successfully matched.
340 # Otherwise loop and try to find another start point on the same line.
341 if fullyMatched:
342 return varState
343
344
345class CheckGroup(CommonEqualityMixin):
346 """Represents a named collection of check lines which are to be matched
347 against an output group of the same name."""
348
David Brazdil2e15cd22014-12-31 17:28:38 +0000349 def __init__(self, name, lines, fileName=None, lineNo=-1):
350 self.fileName = fileName
351 self.lineNo = lineNo
352
353 if not name:
354 Logger.fail("Check group does not have a name", self.fileName, self.lineNo)
355 if not lines:
356 Logger.fail("Check group does not have a body", self.fileName, self.lineNo)
357
358 self.name = name
359 self.lines = lines
360
361 def __eq__(self, other):
362 return (isinstance(other, self.__class__) and
363 self.name == other.name and
364 self.lines == other.lines)
David Brazdilee690a32014-12-01 17:04:16 +0000365
366 def __headAndTail(self, list):
367 return list[0], list[1:]
368
David Brazdil9a6f20e2014-12-19 11:17:21 +0000369 # Splits a list of check lines at index 'i' such that lines[i] is the first
370 # element whose variant is not equal to the given parameter.
371 def __splitByVariant(self, lines, variant):
372 i = 0
373 while i < len(lines) and lines[i].variant == variant:
374 i += 1
375 return lines[:i], lines[i:]
David Brazdilee690a32014-12-01 17:04:16 +0000376
David Brazdil9a6f20e2014-12-19 11:17:21 +0000377 # Extracts the first sequence of check lines which are independent of each
378 # other's match location, i.e. either consecutive DAG lines or a single
379 # InOrder line. Any Not lines preceeding this sequence are also extracted.
380 def __nextIndependentChecks(self, checkLines):
381 notChecks, checkLines = self.__splitByVariant(checkLines, CheckLine.Variant.Not)
382 if not checkLines:
383 return notChecks, [], []
384
385 head, tail = self.__headAndTail(checkLines)
386 if head.variant == CheckLine.Variant.InOrder:
387 return notChecks, [head], tail
388 else:
389 assert head.variant == CheckLine.Variant.DAG
390 independentChecks, checkLines = self.__splitByVariant(checkLines, CheckLine.Variant.DAG)
391 return notChecks, independentChecks, checkLines
392
393 # If successful, returns the line number of the first output line matching the
394 # check line and the updated variable state. Otherwise returns -1 and None,
395 # respectively. The 'lineFilter' parameter can be used to supply a list of
396 # line numbers (counting from 1) which should be skipped.
David Brazdil2e15cd22014-12-31 17:28:38 +0000397 def __findFirstMatch(self, checkLine, outputLines, startLineNo, lineFilter, varState):
398 matchLineNo = startLineNo
David Brazdil9a6f20e2014-12-19 11:17:21 +0000399 for outputLine in outputLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000400 if matchLineNo not in lineFilter:
401 newVarState = checkLine.match(outputLine, varState)
402 if newVarState is not None:
403 return matchLineNo, newVarState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000404 matchLineNo += 1
David Brazdil9a6f20e2014-12-19 11:17:21 +0000405 return -1, None
406
407 # Matches the given positive check lines against the output in order of
408 # appearance. Variable state is propagated but the scope of the search remains
409 # the same for all checks. Each output line can only be matched once.
410 # If all check lines are matched, the resulting variable state is returned
411 # together with the remaining output. The function also returns output lines
412 # which appear before either of the matched lines so they can be tested
413 # against Not checks.
David Brazdil2e15cd22014-12-31 17:28:38 +0000414 def __matchIndependentChecks(self, checkLines, outputLines, startLineNo, varState):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000415 # If no checks are provided, skip over the entire output.
416 if not checkLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000417 return outputLines, [], startLineNo + len(outputLines), varState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000418
419 # Keep track of which lines have been matched.
420 matchedLines = []
421
422 # Find first unused output line which matches each check line.
423 for checkLine in checkLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000424 matchLineNo, varState = \
425 self.__findFirstMatch(checkLine, outputLines, startLineNo, matchedLines, varState)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000426 if varState is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000427 Logger.testFailed("Could not match check line \"" + checkLine.content + "\" " +
428 "starting from output line " + str(startLineNo),
429 self.fileName, checkLine.lineNo)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000430 matchedLines.append(matchLineNo)
431
432 # Return new variable state and the output lines which lie outside the
433 # match locations of this independent group.
David Brazdil2e15cd22014-12-31 17:28:38 +0000434 minMatchLineNo = min(matchedLines)
435 maxMatchLineNo = max(matchedLines)
436 preceedingLines = outputLines[:minMatchLineNo - startLineNo]
437 remainingLines = outputLines[maxMatchLineNo - startLineNo + 1:]
438 return preceedingLines, remainingLines, maxMatchLineNo + 1, varState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000439
440 # Makes sure that the given check lines do not match any of the given output
441 # lines. Variable state does not change.
David Brazdil2e15cd22014-12-31 17:28:38 +0000442 def __matchNotLines(self, checkLines, outputLines, startLineNo, varState):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000443 for checkLine in checkLines:
444 assert checkLine.variant == CheckLine.Variant.Not
David Brazdil21df8892015-01-08 01:49:53 +0000445 matchLineNo, matchVarState = \
David Brazdil2e15cd22014-12-31 17:28:38 +0000446 self.__findFirstMatch(checkLine, outputLines, startLineNo, [], varState)
David Brazdil21df8892015-01-08 01:49:53 +0000447 if matchVarState is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000448 Logger.testFailed("CHECK-NOT line \"" + checkLine.content + "\" matches output line " + \
449 str(matchLineNo), self.fileName, checkLine.lineNo)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000450
451 # Matches the check lines in this group against an output group. It is
452 # responsible for running the checks in the right order and scope, and
453 # for propagating the variable state between the check lines.
454 def match(self, outputGroup):
455 varState = {}
David Brazdilee690a32014-12-01 17:04:16 +0000456 checkLines = self.lines
457 outputLines = outputGroup.body
David Brazdil2e15cd22014-12-31 17:28:38 +0000458 startLineNo = outputGroup.lineNo
David Brazdilee690a32014-12-01 17:04:16 +0000459
David Brazdilee690a32014-12-01 17:04:16 +0000460 while checkLines:
David Brazdil9a6f20e2014-12-19 11:17:21 +0000461 # Extract the next sequence of location-independent checks to be matched.
462 notChecks, independentChecks, checkLines = self.__nextIndependentChecks(checkLines)
David Brazdil2e15cd22014-12-31 17:28:38 +0000463
David Brazdil9a6f20e2014-12-19 11:17:21 +0000464 # Match the independent checks.
David Brazdil2e15cd22014-12-31 17:28:38 +0000465 notOutput, outputLines, newStartLineNo, newVarState = \
466 self.__matchIndependentChecks(independentChecks, outputLines, startLineNo, varState)
467
David Brazdil9a6f20e2014-12-19 11:17:21 +0000468 # Run the Not checks against the output lines which lie between the last
469 # two independent groups or the bounds of the output.
David Brazdil2e15cd22014-12-31 17:28:38 +0000470 self.__matchNotLines(notChecks, notOutput, startLineNo, varState)
471
David Brazdil9a6f20e2014-12-19 11:17:21 +0000472 # Update variable state.
David Brazdil2e15cd22014-12-31 17:28:38 +0000473 startLineNo = newStartLineNo
David Brazdil9a6f20e2014-12-19 11:17:21 +0000474 varState = newVarState
David Brazdilee690a32014-12-01 17:04:16 +0000475
476class OutputGroup(CommonEqualityMixin):
477 """Represents a named part of the test output against which a check group of
478 the same name is to be matched."""
479
David Brazdil2e15cd22014-12-31 17:28:38 +0000480 def __init__(self, name, body, fileName=None, lineNo=-1):
481 if not name:
482 Logger.fail("Output group does not have a name", fileName, lineNo)
483 if not body:
484 Logger.fail("Output group does not have a body", fileName, lineNo)
485
486 self.name = name
487 self.body = body
488 self.lineNo = lineNo
489
490 def __eq__(self, other):
491 return (isinstance(other, self.__class__) and
492 self.name == other.name and
493 self.body == other.body)
David Brazdilee690a32014-12-01 17:04:16 +0000494
495
496class FileSplitMixin(object):
497 """Mixin for representing text files which need to be split into smaller
498 chunks before being parsed."""
499
500 def _parseStream(self, stream):
501 lineNo = 0
502 allGroups = []
503 currentGroup = None
504
505 for line in stream:
506 lineNo += 1
507 line = line.strip()
508 if not line:
509 continue
510
511 # Let the child class process the line and return information about it.
512 # The _processLine method can modify the content of the line (or delete it
513 # entirely) and specify whether it starts a new group.
514 processedLine, newGroupName = self._processLine(line, lineNo)
515 if newGroupName is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000516 currentGroup = (newGroupName, [], lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000517 allGroups.append(currentGroup)
518 if processedLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000519 if currentGroup is not None:
520 currentGroup[1].append(processedLine)
521 else:
522 self._exceptionLineOutsideGroup(line, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000523
524 # Finally, take the generated line groups and let the child class process
525 # each one before storing the final outcome.
David Brazdil2e15cd22014-12-31 17:28:38 +0000526 return list(map(lambda group: self._processGroup(group[0], group[1], group[2]), allGroups))
David Brazdilee690a32014-12-01 17:04:16 +0000527
528
529class CheckFile(FileSplitMixin):
530 """Collection of check groups extracted from the input test file."""
531
David Brazdil2e15cd22014-12-31 17:28:38 +0000532 def __init__(self, prefix, checkStream, fileName=None):
533 self.fileName = fileName
David Brazdilee690a32014-12-01 17:04:16 +0000534 self.prefix = prefix
535 self.groups = self._parseStream(checkStream)
536
537 # Attempts to parse a check line. The regex searches for a comment symbol
538 # followed by the CHECK keyword, given attribute and a colon at the very
539 # beginning of the line. Whitespaces are ignored.
540 def _extractLine(self, prefix, line):
David Brazdilbe0cc082014-12-31 11:49:30 +0000541 rIgnoreWhitespace = r"\s*"
542 rCommentSymbols = [r"//", r"#"]
543 regexPrefix = rIgnoreWhitespace + \
544 r"(" + r"|".join(rCommentSymbols) + r")" + \
545 rIgnoreWhitespace + \
546 prefix + r":"
David Brazdilee690a32014-12-01 17:04:16 +0000547
548 # The 'match' function succeeds only if the pattern is matched at the
549 # beginning of the line.
David Brazdilbe0cc082014-12-31 11:49:30 +0000550 match = re.match(regexPrefix, line)
David Brazdilee690a32014-12-01 17:04:16 +0000551 if match is not None:
552 return line[match.end():].strip()
553 else:
554 return None
555
David Brazdil48942de2015-01-07 21:19:50 +0000556 # This function is invoked on each line of the check file and returns a pair
557 # which instructs the parser how the line should be handled. If the line is to
558 # be included in the current check group, it is returned in the first value.
559 # If the line starts a new check group, the name of the group is returned in
560 # the second value.
David Brazdilee690a32014-12-01 17:04:16 +0000561 def _processLine(self, line, lineNo):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000562 # Lines beginning with 'CHECK-START' start a new check group.
David Brazdilee690a32014-12-01 17:04:16 +0000563 startLine = self._extractLine(self.prefix + "-START", line)
564 if startLine is not None:
David Brazdil9a6f20e2014-12-19 11:17:21 +0000565 return None, startLine
566
567 # Lines starting only with 'CHECK' are matched in order.
568 plainLine = self._extractLine(self.prefix, line)
569 if plainLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000570 return (plainLine, CheckLine.Variant.InOrder, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000571
572 # 'CHECK-DAG' lines are no-order assertions.
573 dagLine = self._extractLine(self.prefix + "-DAG", line)
574 if dagLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000575 return (dagLine, CheckLine.Variant.DAG, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000576
577 # 'CHECK-NOT' lines are no-order negative assertions.
578 notLine = self._extractLine(self.prefix + "-NOT", line)
579 if notLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000580 return (notLine, CheckLine.Variant.Not, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000581
582 # Other lines are ignored.
583 return None, None
David Brazdilee690a32014-12-01 17:04:16 +0000584
585 def _exceptionLineOutsideGroup(self, line, lineNo):
David Brazdil2e15cd22014-12-31 17:28:38 +0000586 Logger.fail("Check line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000587
David Brazdil48942de2015-01-07 21:19:50 +0000588 # Constructs a check group from the parser-collected check lines.
David Brazdil2e15cd22014-12-31 17:28:38 +0000589 def _processGroup(self, name, lines, lineNo):
590 checkLines = list(map(lambda line: CheckLine(line[0], line[1], self.fileName, line[2]), lines))
591 return CheckGroup(name, checkLines, self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000592
David Brazdil2e15cd22014-12-31 17:28:38 +0000593 def match(self, outputFile):
David Brazdilee690a32014-12-01 17:04:16 +0000594 for checkGroup in self.groups:
595 # TODO: Currently does not handle multiple occurrences of the same group
596 # name, e.g. when a pass is run multiple times. It will always try to
597 # match a check group against the first output group of the same name.
598 outputGroup = outputFile.findGroup(checkGroup.name)
599 if outputGroup is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000600 Logger.fail("Group \"" + checkGroup.name + "\" not found in the output",
601 self.fileName, checkGroup.lineNo)
602 Logger.startTest(checkGroup.name)
603 checkGroup.match(outputGroup)
604 Logger.testPassed()
David Brazdilee690a32014-12-01 17:04:16 +0000605
606
607class OutputFile(FileSplitMixin):
608 """Representation of the output generated by the test and split into groups
609 within which the checks are performed.
610
611 C1visualizer format is parsed with a state machine which differentiates
612 between the 'compilation' and 'cfg' blocks. The former marks the beginning
613 of a method. It is parsed for the method's name but otherwise ignored. Each
614 subsequent CFG block represents one stage of the compilation pipeline and
615 is parsed into an output group named "<method name> <pass name>".
616 """
617
618 class ParsingState:
619 OutsideBlock, InsideCompilationBlock, StartingCfgBlock, InsideCfgBlock = range(4)
620
David Brazdil2e15cd22014-12-31 17:28:38 +0000621 def __init__(self, outputStream, fileName=None):
622 self.fileName = fileName
623
David Brazdilee690a32014-12-01 17:04:16 +0000624 # Initialize the state machine
625 self.lastMethodName = None
626 self.state = OutputFile.ParsingState.OutsideBlock
627 self.groups = self._parseStream(outputStream)
628
David Brazdil48942de2015-01-07 21:19:50 +0000629 # This function is invoked on each line of the output file and returns a pair
630 # which instructs the parser how the line should be handled. If the line is to
631 # be included in the current group, it is returned in the first value. If the
632 # line starts a new output group, the name of the group is returned in the
633 # second value.
David Brazdilee690a32014-12-01 17:04:16 +0000634 def _processLine(self, line, lineNo):
635 if self.state == OutputFile.ParsingState.StartingCfgBlock:
636 # Previous line started a new 'cfg' block which means that this one must
637 # contain the name of the pass (this is enforced by C1visualizer).
638 if re.match("name\s+\"[^\"]+\"", line):
639 # Extract the pass name, prepend it with the name of the method and
640 # return as the beginning of a new group.
641 self.state = OutputFile.ParsingState.InsideCfgBlock
642 return (None, self.lastMethodName + " " + line.split("\"")[1])
643 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000644 Logger.fail("Expected output group name", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000645
646 elif self.state == OutputFile.ParsingState.InsideCfgBlock:
647 if line == "end_cfg":
648 self.state = OutputFile.ParsingState.OutsideBlock
649 return (None, None)
650 else:
651 return (line, None)
652
653 elif self.state == OutputFile.ParsingState.InsideCompilationBlock:
654 # Search for the method's name. Format: method "<name>"
David Brazdil2e15cd22014-12-31 17:28:38 +0000655 if re.match("method\s+\"[^\"]*\"", line):
656 methodName = line.split("\"")[1].strip()
657 if not methodName:
658 Logger.fail("Empty method name in output", self.fileName, lineNo)
659 self.lastMethodName = methodName
David Brazdilee690a32014-12-01 17:04:16 +0000660 elif line == "end_compilation":
661 self.state = OutputFile.ParsingState.OutsideBlock
662 return (None, None)
663
David Brazdil2e15cd22014-12-31 17:28:38 +0000664 else:
665 assert self.state == OutputFile.ParsingState.OutsideBlock
David Brazdilee690a32014-12-01 17:04:16 +0000666 if line == "begin_cfg":
667 # The line starts a new group but we'll wait until the next line from
668 # which we can extract the name of the pass.
669 if self.lastMethodName is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000670 Logger.fail("Expected method header", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000671 self.state = OutputFile.ParsingState.StartingCfgBlock
672 return (None, None)
673 elif line == "begin_compilation":
674 self.state = OutputFile.ParsingState.InsideCompilationBlock
675 return (None, None)
676 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000677 Logger.fail("Output line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000678
David Brazdil48942de2015-01-07 21:19:50 +0000679 # Constructs an output group from the parser-collected output lines.
David Brazdil2e15cd22014-12-31 17:28:38 +0000680 def _processGroup(self, name, lines, lineNo):
681 return OutputGroup(name, lines, self.fileName, lineNo + 1)
David Brazdilee690a32014-12-01 17:04:16 +0000682
683 def findGroup(self, name):
684 for group in self.groups:
685 if group.name == name:
686 return group
687 return None
688
689
690def ParseArguments():
691 parser = argparse.ArgumentParser()
David Brazdil32beaff2015-01-15 01:32:23 +0000692 parser.add_argument("tested_file",
693 help="text file the checks should be verified against")
694 parser.add_argument("source_path", nargs="?",
695 help="path to file/folder with checking annotations")
David Brazdilee690a32014-12-01 17:04:16 +0000696 parser.add_argument("--check-prefix", dest="check_prefix", default="CHECK", metavar="PREFIX",
David Brazdil32beaff2015-01-15 01:32:23 +0000697 help="prefix of checks in the test files (default: CHECK)")
David Brazdilee690a32014-12-01 17:04:16 +0000698 parser.add_argument("--list-groups", dest="list_groups", action="store_true",
David Brazdil32beaff2015-01-15 01:32:23 +0000699 help="print a list of all groups found in the tested file")
David Brazdilee690a32014-12-01 17:04:16 +0000700 parser.add_argument("--dump-group", dest="dump_group", metavar="GROUP",
701 help="print the contents of an output group")
David Brazdil7cca5df2015-01-15 00:40:56 +0000702 parser.add_argument("-q", "--quiet", action="store_true",
703 help="print only errors")
David Brazdilee690a32014-12-01 17:04:16 +0000704 return parser.parse_args()
705
706
David Brazdilee690a32014-12-01 17:04:16 +0000707def ListGroups(outputFilename):
708 outputFile = OutputFile(open(outputFilename, "r"))
709 for group in outputFile.groups:
David Brazdil2e15cd22014-12-31 17:28:38 +0000710 Logger.log(group.name)
David Brazdilee690a32014-12-01 17:04:16 +0000711
712
713def DumpGroup(outputFilename, groupName):
714 outputFile = OutputFile(open(outputFilename, "r"))
715 group = outputFile.findGroup(groupName)
716 if group:
David Brazdil2e15cd22014-12-31 17:28:38 +0000717 lineNo = group.lineNo
718 maxLineNo = lineNo + len(group.body)
719 lenLineNo = len(str(maxLineNo)) + 2
720 for line in group.body:
721 Logger.log((str(lineNo) + ":").ljust(lenLineNo) + line)
722 lineNo += 1
David Brazdilee690a32014-12-01 17:04:16 +0000723 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000724 Logger.fail("Group \"" + groupName + "\" not found in the output")
David Brazdilee690a32014-12-01 17:04:16 +0000725
726
David Brazdil3f7dce82015-01-16 23:31:11 +0000727# Returns a list of files to scan for check annotations in the given path. Path
728# to a file is returned as a single-element list, directories are recursively
729# traversed and all '.java' files returned.
David Brazdil32beaff2015-01-15 01:32:23 +0000730def FindCheckFiles(path):
731 if not path:
732 Logger.fail("No source path provided")
733 elif os.path.isfile(path):
734 return [ path ]
735 elif os.path.isdir(path):
736 foundFiles = []
737 for root, dirs, files in os.walk(path):
738 for file in files:
739 if os.path.splitext(file)[1] == ".java":
740 foundFiles.append(os.path.join(root, file))
741 return foundFiles
742 else:
743 Logger.fail("Source path \"" + path + "\" not found")
David Brazdil2e15cd22014-12-31 17:28:38 +0000744
David Brazdil32beaff2015-01-15 01:32:23 +0000745
746def RunChecks(checkPrefix, checkPath, outputFilename):
747 outputBaseName = os.path.basename(outputFilename)
David Brazdil2e15cd22014-12-31 17:28:38 +0000748 outputFile = OutputFile(open(outputFilename, "r"), outputBaseName)
David Brazdil32beaff2015-01-15 01:32:23 +0000749
750 for checkFilename in FindCheckFiles(checkPath):
751 checkBaseName = os.path.basename(checkFilename)
752 checkFile = CheckFile(checkPrefix, open(checkFilename, "r"), checkBaseName)
753 checkFile.match(outputFile)
David Brazdilee690a32014-12-01 17:04:16 +0000754
755
756if __name__ == "__main__":
757 args = ParseArguments()
David Brazdil3f7dce82015-01-16 23:31:11 +0000758
David Brazdil7cca5df2015-01-15 00:40:56 +0000759 if args.quiet:
760 Logger.Verbosity = Logger.Level.Error
David Brazdilee690a32014-12-01 17:04:16 +0000761
David Brazdil3f7dce82015-01-16 23:31:11 +0000762 if args.list_groups:
763 ListGroups(args.tested_file)
764 elif args.dump_group:
765 DumpGroup(args.tested_file, args.dump_group)
766 else:
767 RunChecks(args.check_prefix, args.source_path, args.tested_file)