blob: 0bce236223c5d0a02aa9adcde660e0881c2ee55d [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:
Calin Juravle3cf48772015-01-26 16:47:33 +0000113 print(text, file=out)
David Brazdil2e15cd22014-12-31 17:28:38 +0000114 else:
Calin Juravle3cf48772015-01-26 16:47:33 +0000115 print(text, end="", file=out)
116 out.flush()
David Brazdil2e15cd22014-12-31 17:28:38 +0000117
118 @staticmethod
119 def fail(msg, file=None, line=-1):
120 location = ""
121 if file:
122 location += file + ":"
123 if line > 0:
124 location += str(line) + ":"
125 if location:
126 location += " "
127
David Brazdil7cca5df2015-01-15 00:40:56 +0000128 Logger.log(location, Logger.Level.Error, color=Logger.Color.Gray, newLine=False, out=sys.stderr)
129 Logger.log("error: ", Logger.Level.Error, color=Logger.Color.Red, newLine=False, out=sys.stderr)
130 Logger.log(msg, Logger.Level.Error, out=sys.stderr)
David Brazdilb86e7792015-01-27 15:54:30 +0000131 sys.exit(msg)
David Brazdil2e15cd22014-12-31 17:28:38 +0000132
133 @staticmethod
134 def startTest(name):
135 Logger.log("TEST ", color=Logger.Color.Purple, newLine=False)
136 Logger.log(name + "... ", newLine=False)
137
138 @staticmethod
139 def testPassed():
140 Logger.log("PASS", color=Logger.Color.Blue)
141
142 @staticmethod
143 def testFailed(msg, file=None, line=-1):
144 Logger.log("FAIL", color=Logger.Color.Red)
145 Logger.fail(msg, file, line)
146
David Brazdilee690a32014-12-01 17:04:16 +0000147class CommonEqualityMixin:
148 """Mixin for class equality as equality of the fields."""
149 def __eq__(self, other):
150 return (isinstance(other, self.__class__)
151 and self.__dict__ == other.__dict__)
152
153 def __ne__(self, other):
154 return not self.__eq__(other)
155
156 def __repr__(self):
157 return "<%s: %s>" % (type(self).__name__, str(self.__dict__))
158
159
160class CheckElement(CommonEqualityMixin):
161 """Single element of the check line."""
162
163 class Variant(object):
164 """Supported language constructs."""
David Brazdilb86e7792015-01-27 15:54:30 +0000165 Text, Pattern, VarRef, VarDef, Separator = range(5)
David Brazdilee690a32014-12-01 17:04:16 +0000166
David Brazdilbe0cc082014-12-31 11:49:30 +0000167 rStartOptional = r"("
168 rEndOptional = r")?"
169
170 rName = r"([a-zA-Z][a-zA-Z0-9]*)"
171 rRegex = r"(.+?)"
172 rPatternStartSym = r"(\{\{)"
173 rPatternEndSym = r"(\}\})"
174 rVariableStartSym = r"(\[\[)"
175 rVariableEndSym = r"(\]\])"
176 rVariableSeparator = r"(:)"
177
178 regexPattern = rPatternStartSym + rRegex + rPatternEndSym
179 regexVariable = rVariableStartSym + \
180 rName + \
181 (rStartOptional + rVariableSeparator + rRegex + rEndOptional) + \
182 rVariableEndSym
183
David Brazdilee690a32014-12-01 17:04:16 +0000184 def __init__(self, variant, name, pattern):
185 self.variant = variant
186 self.name = name
187 self.pattern = pattern
188
189 @staticmethod
David Brazdilb86e7792015-01-27 15:54:30 +0000190 def newSeparator():
191 return CheckElement(CheckElement.Variant.Separator, None, None)
192
193 @staticmethod
David Brazdilee690a32014-12-01 17:04:16 +0000194 def parseText(text):
195 return CheckElement(CheckElement.Variant.Text, None, re.escape(text))
196
197 @staticmethod
198 def parsePattern(patternElem):
David Brazdilbe0cc082014-12-31 11:49:30 +0000199 return CheckElement(CheckElement.Variant.Pattern, None, patternElem[2:-2])
David Brazdilee690a32014-12-01 17:04:16 +0000200
201 @staticmethod
202 def parseVariable(varElem):
203 colonPos = varElem.find(":")
204 if colonPos == -1:
205 # Variable reference
David Brazdilbe0cc082014-12-31 11:49:30 +0000206 name = varElem[2:-2]
David Brazdilee690a32014-12-01 17:04:16 +0000207 return CheckElement(CheckElement.Variant.VarRef, name, None)
208 else:
209 # Variable definition
210 name = varElem[2:colonPos]
David Brazdilbe0cc082014-12-31 11:49:30 +0000211 body = varElem[colonPos+1:-2]
David Brazdilee690a32014-12-01 17:04:16 +0000212 return CheckElement(CheckElement.Variant.VarDef, name, body)
213
David Brazdilee690a32014-12-01 17:04:16 +0000214class CheckLine(CommonEqualityMixin):
215 """Representation of a single assertion in the check file formed of one or
216 more regex elements. Matching against an output line is successful only
217 if all regex elements can be matched in the given order."""
218
David Brazdil9a6f20e2014-12-19 11:17:21 +0000219 class Variant(object):
220 """Supported types of assertions."""
221 InOrder, DAG, Not = range(3)
David Brazdilee690a32014-12-01 17:04:16 +0000222
David Brazdil2e15cd22014-12-31 17:28:38 +0000223 def __init__(self, content, variant=Variant.InOrder, fileName=None, lineNo=-1):
224 self.fileName = fileName
David Brazdilee690a32014-12-01 17:04:16 +0000225 self.lineNo = lineNo
David Brazdil2e15cd22014-12-31 17:28:38 +0000226 self.content = content.strip()
David Brazdilee690a32014-12-01 17:04:16 +0000227
David Brazdil2e15cd22014-12-31 17:28:38 +0000228 self.variant = variant
David Brazdil9a6f20e2014-12-19 11:17:21 +0000229 self.lineParts = self.__parse(self.content)
David Brazdilee690a32014-12-01 17:04:16 +0000230 if not self.lineParts:
David Brazdil2e15cd22014-12-31 17:28:38 +0000231 Logger.fail("Empty check line", self.fileName, self.lineNo)
232
233 if self.variant == CheckLine.Variant.Not:
234 for elem in self.lineParts:
235 if elem.variant == CheckElement.Variant.VarDef:
236 Logger.fail("CHECK-NOT lines cannot define variables", self.fileName, self.lineNo)
237
238 def __eq__(self, other):
239 return (isinstance(other, self.__class__) and
240 self.variant == other.variant and
241 self.lineParts == other.lineParts)
David Brazdilee690a32014-12-01 17:04:16 +0000242
243 # Returns True if the given Match object was at the beginning of the line.
244 def __isMatchAtStart(self, match):
245 return (match is not None) and (match.start() == 0)
246
247 # Takes in a list of Match objects and returns the minimal start point among
248 # them. If there aren't any successful matches it returns the length of
249 # the searched string.
250 def __firstMatch(self, matches, string):
251 starts = map(lambda m: len(string) if m is None else m.start(), matches)
252 return min(starts)
253
David Brazdilee690a32014-12-01 17:04:16 +0000254 # This method parses the content of a check line stripped of the initial
255 # comment symbol and the CHECK keyword.
256 def __parse(self, line):
257 lineParts = []
258 # Loop as long as there is something to parse.
259 while line:
260 # Search for the nearest occurrence of the special markers.
David Brazdilbe0cc082014-12-31 11:49:30 +0000261 matchWhitespace = re.search(r"\s+", line)
262 matchPattern = re.search(CheckElement.regexPattern, line)
263 matchVariable = re.search(CheckElement.regexVariable, line)
David Brazdilee690a32014-12-01 17:04:16 +0000264
265 # If one of the above was identified at the current position, extract them
266 # from the line, parse them and add to the list of line parts.
267 if self.__isMatchAtStart(matchWhitespace):
David Brazdilb86e7792015-01-27 15:54:30 +0000268 # A whitespace in the check line creates a new separator of line parts.
269 # This allows for ignored output between the previous and next parts.
David Brazdilee690a32014-12-01 17:04:16 +0000270 line = line[matchWhitespace.end():]
David Brazdilb86e7792015-01-27 15:54:30 +0000271 lineParts.append(CheckElement.newSeparator())
David Brazdilee690a32014-12-01 17:04:16 +0000272 elif self.__isMatchAtStart(matchPattern):
273 pattern = line[0:matchPattern.end()]
274 line = line[matchPattern.end():]
275 lineParts.append(CheckElement.parsePattern(pattern))
276 elif self.__isMatchAtStart(matchVariable):
277 var = line[0:matchVariable.end()]
278 line = line[matchVariable.end():]
David Brazdil2e15cd22014-12-31 17:28:38 +0000279 lineParts.append(CheckElement.parseVariable(var))
David Brazdilee690a32014-12-01 17:04:16 +0000280 else:
281 # If we're not currently looking at a special marker, this is a plain
282 # text match all the way until the first special marker (or the end
283 # of the line).
284 firstMatch = self.__firstMatch([ matchWhitespace, matchPattern, matchVariable ], line)
285 text = line[0:firstMatch]
286 line = line[firstMatch:]
287 lineParts.append(CheckElement.parseText(text))
288 return lineParts
289
290 # Returns the regex pattern to be matched in the output line. Variable
291 # references are substituted with their current values provided in the
292 # 'varState' argument.
293 # An exception is raised if a referenced variable is undefined.
294 def __generatePattern(self, linePart, varState):
295 if linePart.variant == CheckElement.Variant.VarRef:
296 try:
297 return re.escape(varState[linePart.name])
298 except KeyError:
David Brazdil2e15cd22014-12-31 17:28:38 +0000299 Logger.testFailed("Use of undefined variable \"" + linePart.name + "\"",
300 self.fileName, self.lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000301 else:
302 return linePart.pattern
303
David Brazdilb86e7792015-01-27 15:54:30 +0000304 def __isSeparated(self, outputLine, matchStart):
305 return (matchStart == 0) or (outputLine[matchStart - 1:matchStart].isspace())
306
David Brazdilee690a32014-12-01 17:04:16 +0000307 # Attempts to match the check line against a line from the output file with
308 # the given initial variable values. It returns the new variable state if
309 # successful and None otherwise.
310 def match(self, outputLine, initialVarState):
David Brazdilb86e7792015-01-27 15:54:30 +0000311 # Do the full matching on a shadow copy of the variable state. If the
312 # matching fails half-way, we will not need to revert the state.
313 varState = dict(initialVarState)
David Brazdilee690a32014-12-01 17:04:16 +0000314
David Brazdilb86e7792015-01-27 15:54:30 +0000315 matchStart = 0
316 isAfterSeparator = True
David Brazdilee690a32014-12-01 17:04:16 +0000317
David Brazdilb86e7792015-01-27 15:54:30 +0000318 # Now try to parse all of the parts of the check line in the right order.
319 # Variable values are updated on-the-fly, meaning that a variable can
320 # be referenced immediately after its definition.
321 for part in self.lineParts:
322 if part.variant == CheckElement.Variant.Separator:
323 isAfterSeparator = True
324 continue
325
326 # Find the earliest match for this line part.
327 pattern = self.__generatePattern(part, varState)
328 while True:
329 match = re.search(pattern, outputLine[matchStart:])
330 if (match is None) or (not isAfterSeparator and not self.__isMatchAtStart(match)):
331 return None
David Brazdilee690a32014-12-01 17:04:16 +0000332 matchEnd = matchStart + match.end()
David Brazdilb86e7792015-01-27 15:54:30 +0000333 matchStart += match.start()
David Brazdilee690a32014-12-01 17:04:16 +0000334
David Brazdilb86e7792015-01-27 15:54:30 +0000335 # Check if this is a valid match if we expect a whitespace separator
336 # before the matched text. Otherwise loop and look for another match.
337 if not isAfterSeparator or self.__isSeparated(outputLine, matchStart):
338 break
339 else:
340 matchStart += 1
341
342 if part.variant == CheckElement.Variant.VarDef:
343 if part.name in varState:
344 Logger.testFailed("Multiple definitions of variable \"" + part.name + "\"",
345 self.fileName, self.lineNo)
346 varState[part.name] = outputLine[matchStart:matchEnd]
347
348 matchStart = matchEnd
349 isAfterSeparator = False
350
351 # All parts were successfully matched. Return the new variable state.
352 return varState
David Brazdilee690a32014-12-01 17:04:16 +0000353
354
355class CheckGroup(CommonEqualityMixin):
356 """Represents a named collection of check lines which are to be matched
357 against an output group of the same name."""
358
David Brazdil2e15cd22014-12-31 17:28:38 +0000359 def __init__(self, name, lines, fileName=None, lineNo=-1):
360 self.fileName = fileName
361 self.lineNo = lineNo
362
363 if not name:
364 Logger.fail("Check group does not have a name", self.fileName, self.lineNo)
365 if not lines:
366 Logger.fail("Check group does not have a body", self.fileName, self.lineNo)
367
368 self.name = name
369 self.lines = lines
370
371 def __eq__(self, other):
372 return (isinstance(other, self.__class__) and
373 self.name == other.name and
374 self.lines == other.lines)
David Brazdilee690a32014-12-01 17:04:16 +0000375
376 def __headAndTail(self, list):
377 return list[0], list[1:]
378
David Brazdil9a6f20e2014-12-19 11:17:21 +0000379 # Splits a list of check lines at index 'i' such that lines[i] is the first
380 # element whose variant is not equal to the given parameter.
381 def __splitByVariant(self, lines, variant):
382 i = 0
383 while i < len(lines) and lines[i].variant == variant:
384 i += 1
385 return lines[:i], lines[i:]
David Brazdilee690a32014-12-01 17:04:16 +0000386
David Brazdil9a6f20e2014-12-19 11:17:21 +0000387 # Extracts the first sequence of check lines which are independent of each
388 # other's match location, i.e. either consecutive DAG lines or a single
389 # InOrder line. Any Not lines preceeding this sequence are also extracted.
390 def __nextIndependentChecks(self, checkLines):
391 notChecks, checkLines = self.__splitByVariant(checkLines, CheckLine.Variant.Not)
392 if not checkLines:
393 return notChecks, [], []
394
395 head, tail = self.__headAndTail(checkLines)
396 if head.variant == CheckLine.Variant.InOrder:
397 return notChecks, [head], tail
398 else:
399 assert head.variant == CheckLine.Variant.DAG
400 independentChecks, checkLines = self.__splitByVariant(checkLines, CheckLine.Variant.DAG)
401 return notChecks, independentChecks, checkLines
402
403 # If successful, returns the line number of the first output line matching the
404 # check line and the updated variable state. Otherwise returns -1 and None,
405 # respectively. The 'lineFilter' parameter can be used to supply a list of
406 # line numbers (counting from 1) which should be skipped.
David Brazdil2e15cd22014-12-31 17:28:38 +0000407 def __findFirstMatch(self, checkLine, outputLines, startLineNo, lineFilter, varState):
408 matchLineNo = startLineNo
David Brazdil9a6f20e2014-12-19 11:17:21 +0000409 for outputLine in outputLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000410 if matchLineNo not in lineFilter:
411 newVarState = checkLine.match(outputLine, varState)
412 if newVarState is not None:
413 return matchLineNo, newVarState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000414 matchLineNo += 1
David Brazdil9a6f20e2014-12-19 11:17:21 +0000415 return -1, None
416
417 # Matches the given positive check lines against the output in order of
418 # appearance. Variable state is propagated but the scope of the search remains
419 # the same for all checks. Each output line can only be matched once.
420 # If all check lines are matched, the resulting variable state is returned
421 # together with the remaining output. The function also returns output lines
422 # which appear before either of the matched lines so they can be tested
423 # against Not checks.
David Brazdil2e15cd22014-12-31 17:28:38 +0000424 def __matchIndependentChecks(self, checkLines, outputLines, startLineNo, varState):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000425 # If no checks are provided, skip over the entire output.
426 if not checkLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000427 return outputLines, [], startLineNo + len(outputLines), varState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000428
429 # Keep track of which lines have been matched.
430 matchedLines = []
431
432 # Find first unused output line which matches each check line.
433 for checkLine in checkLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000434 matchLineNo, varState = \
435 self.__findFirstMatch(checkLine, outputLines, startLineNo, matchedLines, varState)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000436 if varState is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000437 Logger.testFailed("Could not match check line \"" + checkLine.content + "\" " +
438 "starting from output line " + str(startLineNo),
439 self.fileName, checkLine.lineNo)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000440 matchedLines.append(matchLineNo)
441
442 # Return new variable state and the output lines which lie outside the
443 # match locations of this independent group.
David Brazdil2e15cd22014-12-31 17:28:38 +0000444 minMatchLineNo = min(matchedLines)
445 maxMatchLineNo = max(matchedLines)
446 preceedingLines = outputLines[:minMatchLineNo - startLineNo]
447 remainingLines = outputLines[maxMatchLineNo - startLineNo + 1:]
448 return preceedingLines, remainingLines, maxMatchLineNo + 1, varState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000449
450 # Makes sure that the given check lines do not match any of the given output
451 # lines. Variable state does not change.
David Brazdil2e15cd22014-12-31 17:28:38 +0000452 def __matchNotLines(self, checkLines, outputLines, startLineNo, varState):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000453 for checkLine in checkLines:
454 assert checkLine.variant == CheckLine.Variant.Not
David Brazdil21df8892015-01-08 01:49:53 +0000455 matchLineNo, matchVarState = \
David Brazdil2e15cd22014-12-31 17:28:38 +0000456 self.__findFirstMatch(checkLine, outputLines, startLineNo, [], varState)
David Brazdil21df8892015-01-08 01:49:53 +0000457 if matchVarState is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000458 Logger.testFailed("CHECK-NOT line \"" + checkLine.content + "\" matches output line " + \
459 str(matchLineNo), self.fileName, checkLine.lineNo)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000460
461 # Matches the check lines in this group against an output group. It is
462 # responsible for running the checks in the right order and scope, and
463 # for propagating the variable state between the check lines.
464 def match(self, outputGroup):
465 varState = {}
David Brazdilee690a32014-12-01 17:04:16 +0000466 checkLines = self.lines
467 outputLines = outputGroup.body
David Brazdil2e15cd22014-12-31 17:28:38 +0000468 startLineNo = outputGroup.lineNo
David Brazdilee690a32014-12-01 17:04:16 +0000469
David Brazdilee690a32014-12-01 17:04:16 +0000470 while checkLines:
David Brazdil9a6f20e2014-12-19 11:17:21 +0000471 # Extract the next sequence of location-independent checks to be matched.
472 notChecks, independentChecks, checkLines = self.__nextIndependentChecks(checkLines)
David Brazdil2e15cd22014-12-31 17:28:38 +0000473
David Brazdil9a6f20e2014-12-19 11:17:21 +0000474 # Match the independent checks.
David Brazdil2e15cd22014-12-31 17:28:38 +0000475 notOutput, outputLines, newStartLineNo, newVarState = \
476 self.__matchIndependentChecks(independentChecks, outputLines, startLineNo, varState)
477
David Brazdil9a6f20e2014-12-19 11:17:21 +0000478 # Run the Not checks against the output lines which lie between the last
479 # two independent groups or the bounds of the output.
David Brazdil2e15cd22014-12-31 17:28:38 +0000480 self.__matchNotLines(notChecks, notOutput, startLineNo, varState)
481
David Brazdil9a6f20e2014-12-19 11:17:21 +0000482 # Update variable state.
David Brazdil2e15cd22014-12-31 17:28:38 +0000483 startLineNo = newStartLineNo
David Brazdil9a6f20e2014-12-19 11:17:21 +0000484 varState = newVarState
David Brazdilee690a32014-12-01 17:04:16 +0000485
486class OutputGroup(CommonEqualityMixin):
487 """Represents a named part of the test output against which a check group of
488 the same name is to be matched."""
489
David Brazdil2e15cd22014-12-31 17:28:38 +0000490 def __init__(self, name, body, fileName=None, lineNo=-1):
491 if not name:
492 Logger.fail("Output group does not have a name", fileName, lineNo)
493 if not body:
494 Logger.fail("Output group does not have a body", fileName, lineNo)
495
496 self.name = name
497 self.body = body
498 self.lineNo = lineNo
499
500 def __eq__(self, other):
501 return (isinstance(other, self.__class__) and
502 self.name == other.name and
503 self.body == other.body)
David Brazdilee690a32014-12-01 17:04:16 +0000504
505
506class FileSplitMixin(object):
507 """Mixin for representing text files which need to be split into smaller
508 chunks before being parsed."""
509
510 def _parseStream(self, stream):
511 lineNo = 0
512 allGroups = []
513 currentGroup = None
514
515 for line in stream:
516 lineNo += 1
517 line = line.strip()
518 if not line:
519 continue
520
521 # Let the child class process the line and return information about it.
522 # The _processLine method can modify the content of the line (or delete it
523 # entirely) and specify whether it starts a new group.
524 processedLine, newGroupName = self._processLine(line, lineNo)
525 if newGroupName is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000526 currentGroup = (newGroupName, [], lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000527 allGroups.append(currentGroup)
528 if processedLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000529 if currentGroup is not None:
530 currentGroup[1].append(processedLine)
531 else:
532 self._exceptionLineOutsideGroup(line, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000533
534 # Finally, take the generated line groups and let the child class process
535 # each one before storing the final outcome.
David Brazdil2e15cd22014-12-31 17:28:38 +0000536 return list(map(lambda group: self._processGroup(group[0], group[1], group[2]), allGroups))
David Brazdilee690a32014-12-01 17:04:16 +0000537
538
539class CheckFile(FileSplitMixin):
540 """Collection of check groups extracted from the input test file."""
541
David Brazdil2e15cd22014-12-31 17:28:38 +0000542 def __init__(self, prefix, checkStream, fileName=None):
543 self.fileName = fileName
David Brazdilee690a32014-12-01 17:04:16 +0000544 self.prefix = prefix
545 self.groups = self._parseStream(checkStream)
546
547 # Attempts to parse a check line. The regex searches for a comment symbol
548 # followed by the CHECK keyword, given attribute and a colon at the very
549 # beginning of the line. Whitespaces are ignored.
550 def _extractLine(self, prefix, line):
David Brazdilbe0cc082014-12-31 11:49:30 +0000551 rIgnoreWhitespace = r"\s*"
552 rCommentSymbols = [r"//", r"#"]
553 regexPrefix = rIgnoreWhitespace + \
554 r"(" + r"|".join(rCommentSymbols) + r")" + \
555 rIgnoreWhitespace + \
556 prefix + r":"
David Brazdilee690a32014-12-01 17:04:16 +0000557
558 # The 'match' function succeeds only if the pattern is matched at the
559 # beginning of the line.
David Brazdilbe0cc082014-12-31 11:49:30 +0000560 match = re.match(regexPrefix, line)
David Brazdilee690a32014-12-01 17:04:16 +0000561 if match is not None:
562 return line[match.end():].strip()
563 else:
564 return None
565
David Brazdil48942de2015-01-07 21:19:50 +0000566 # This function is invoked on each line of the check file and returns a pair
567 # which instructs the parser how the line should be handled. If the line is to
568 # be included in the current check group, it is returned in the first value.
569 # If the line starts a new check group, the name of the group is returned in
570 # the second value.
David Brazdilee690a32014-12-01 17:04:16 +0000571 def _processLine(self, line, lineNo):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000572 # Lines beginning with 'CHECK-START' start a new check group.
David Brazdilee690a32014-12-01 17:04:16 +0000573 startLine = self._extractLine(self.prefix + "-START", line)
574 if startLine is not None:
David Brazdil9a6f20e2014-12-19 11:17:21 +0000575 return None, startLine
576
577 # Lines starting only with 'CHECK' are matched in order.
578 plainLine = self._extractLine(self.prefix, line)
579 if plainLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000580 return (plainLine, CheckLine.Variant.InOrder, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000581
582 # 'CHECK-DAG' lines are no-order assertions.
583 dagLine = self._extractLine(self.prefix + "-DAG", line)
584 if dagLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000585 return (dagLine, CheckLine.Variant.DAG, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000586
587 # 'CHECK-NOT' lines are no-order negative assertions.
588 notLine = self._extractLine(self.prefix + "-NOT", line)
589 if notLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000590 return (notLine, CheckLine.Variant.Not, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000591
592 # Other lines are ignored.
593 return None, None
David Brazdilee690a32014-12-01 17:04:16 +0000594
595 def _exceptionLineOutsideGroup(self, line, lineNo):
David Brazdil2e15cd22014-12-31 17:28:38 +0000596 Logger.fail("Check line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000597
David Brazdil48942de2015-01-07 21:19:50 +0000598 # Constructs a check group from the parser-collected check lines.
David Brazdil2e15cd22014-12-31 17:28:38 +0000599 def _processGroup(self, name, lines, lineNo):
600 checkLines = list(map(lambda line: CheckLine(line[0], line[1], self.fileName, line[2]), lines))
601 return CheckGroup(name, checkLines, self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000602
David Brazdil2e15cd22014-12-31 17:28:38 +0000603 def match(self, outputFile):
David Brazdilee690a32014-12-01 17:04:16 +0000604 for checkGroup in self.groups:
605 # TODO: Currently does not handle multiple occurrences of the same group
606 # name, e.g. when a pass is run multiple times. It will always try to
607 # match a check group against the first output group of the same name.
608 outputGroup = outputFile.findGroup(checkGroup.name)
609 if outputGroup is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000610 Logger.fail("Group \"" + checkGroup.name + "\" not found in the output",
611 self.fileName, checkGroup.lineNo)
612 Logger.startTest(checkGroup.name)
613 checkGroup.match(outputGroup)
614 Logger.testPassed()
David Brazdilee690a32014-12-01 17:04:16 +0000615
616
617class OutputFile(FileSplitMixin):
618 """Representation of the output generated by the test and split into groups
619 within which the checks are performed.
620
621 C1visualizer format is parsed with a state machine which differentiates
622 between the 'compilation' and 'cfg' blocks. The former marks the beginning
623 of a method. It is parsed for the method's name but otherwise ignored. Each
624 subsequent CFG block represents one stage of the compilation pipeline and
625 is parsed into an output group named "<method name> <pass name>".
626 """
627
628 class ParsingState:
629 OutsideBlock, InsideCompilationBlock, StartingCfgBlock, InsideCfgBlock = range(4)
630
David Brazdil2e15cd22014-12-31 17:28:38 +0000631 def __init__(self, outputStream, fileName=None):
632 self.fileName = fileName
633
David Brazdilee690a32014-12-01 17:04:16 +0000634 # Initialize the state machine
635 self.lastMethodName = None
636 self.state = OutputFile.ParsingState.OutsideBlock
637 self.groups = self._parseStream(outputStream)
638
David Brazdil48942de2015-01-07 21:19:50 +0000639 # This function is invoked on each line of the output file and returns a pair
640 # which instructs the parser how the line should be handled. If the line is to
641 # be included in the current group, it is returned in the first value. If the
642 # line starts a new output group, the name of the group is returned in the
643 # second value.
David Brazdilee690a32014-12-01 17:04:16 +0000644 def _processLine(self, line, lineNo):
645 if self.state == OutputFile.ParsingState.StartingCfgBlock:
646 # Previous line started a new 'cfg' block which means that this one must
647 # contain the name of the pass (this is enforced by C1visualizer).
648 if re.match("name\s+\"[^\"]+\"", line):
649 # Extract the pass name, prepend it with the name of the method and
650 # return as the beginning of a new group.
651 self.state = OutputFile.ParsingState.InsideCfgBlock
652 return (None, self.lastMethodName + " " + line.split("\"")[1])
653 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000654 Logger.fail("Expected output group name", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000655
656 elif self.state == OutputFile.ParsingState.InsideCfgBlock:
657 if line == "end_cfg":
658 self.state = OutputFile.ParsingState.OutsideBlock
659 return (None, None)
660 else:
661 return (line, None)
662
663 elif self.state == OutputFile.ParsingState.InsideCompilationBlock:
664 # Search for the method's name. Format: method "<name>"
David Brazdil2e15cd22014-12-31 17:28:38 +0000665 if re.match("method\s+\"[^\"]*\"", line):
666 methodName = line.split("\"")[1].strip()
667 if not methodName:
668 Logger.fail("Empty method name in output", self.fileName, lineNo)
669 self.lastMethodName = methodName
David Brazdilee690a32014-12-01 17:04:16 +0000670 elif line == "end_compilation":
671 self.state = OutputFile.ParsingState.OutsideBlock
672 return (None, None)
673
David Brazdil2e15cd22014-12-31 17:28:38 +0000674 else:
675 assert self.state == OutputFile.ParsingState.OutsideBlock
David Brazdilee690a32014-12-01 17:04:16 +0000676 if line == "begin_cfg":
677 # The line starts a new group but we'll wait until the next line from
678 # which we can extract the name of the pass.
679 if self.lastMethodName is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000680 Logger.fail("Expected method header", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000681 self.state = OutputFile.ParsingState.StartingCfgBlock
682 return (None, None)
683 elif line == "begin_compilation":
684 self.state = OutputFile.ParsingState.InsideCompilationBlock
685 return (None, None)
686 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000687 Logger.fail("Output line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000688
David Brazdil48942de2015-01-07 21:19:50 +0000689 # Constructs an output group from the parser-collected output lines.
David Brazdil2e15cd22014-12-31 17:28:38 +0000690 def _processGroup(self, name, lines, lineNo):
691 return OutputGroup(name, lines, self.fileName, lineNo + 1)
David Brazdilee690a32014-12-01 17:04:16 +0000692
693 def findGroup(self, name):
694 for group in self.groups:
695 if group.name == name:
696 return group
697 return None
698
699
700def ParseArguments():
701 parser = argparse.ArgumentParser()
David Brazdil32beaff2015-01-15 01:32:23 +0000702 parser.add_argument("tested_file",
703 help="text file the checks should be verified against")
704 parser.add_argument("source_path", nargs="?",
705 help="path to file/folder with checking annotations")
David Brazdilee690a32014-12-01 17:04:16 +0000706 parser.add_argument("--check-prefix", dest="check_prefix", default="CHECK", metavar="PREFIX",
David Brazdil32beaff2015-01-15 01:32:23 +0000707 help="prefix of checks in the test files (default: CHECK)")
David Brazdilee690a32014-12-01 17:04:16 +0000708 parser.add_argument("--list-groups", dest="list_groups", action="store_true",
David Brazdil32beaff2015-01-15 01:32:23 +0000709 help="print a list of all groups found in the tested file")
David Brazdilee690a32014-12-01 17:04:16 +0000710 parser.add_argument("--dump-group", dest="dump_group", metavar="GROUP",
711 help="print the contents of an output group")
David Brazdil7cca5df2015-01-15 00:40:56 +0000712 parser.add_argument("-q", "--quiet", action="store_true",
713 help="print only errors")
David Brazdilee690a32014-12-01 17:04:16 +0000714 return parser.parse_args()
715
716
David Brazdilee690a32014-12-01 17:04:16 +0000717def ListGroups(outputFilename):
718 outputFile = OutputFile(open(outputFilename, "r"))
719 for group in outputFile.groups:
David Brazdil2e15cd22014-12-31 17:28:38 +0000720 Logger.log(group.name)
David Brazdilee690a32014-12-01 17:04:16 +0000721
722
723def DumpGroup(outputFilename, groupName):
724 outputFile = OutputFile(open(outputFilename, "r"))
725 group = outputFile.findGroup(groupName)
726 if group:
David Brazdil2e15cd22014-12-31 17:28:38 +0000727 lineNo = group.lineNo
728 maxLineNo = lineNo + len(group.body)
729 lenLineNo = len(str(maxLineNo)) + 2
730 for line in group.body:
731 Logger.log((str(lineNo) + ":").ljust(lenLineNo) + line)
732 lineNo += 1
David Brazdilee690a32014-12-01 17:04:16 +0000733 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000734 Logger.fail("Group \"" + groupName + "\" not found in the output")
David Brazdilee690a32014-12-01 17:04:16 +0000735
736
David Brazdil3f7dce82015-01-16 23:31:11 +0000737# Returns a list of files to scan for check annotations in the given path. Path
738# to a file is returned as a single-element list, directories are recursively
739# traversed and all '.java' files returned.
David Brazdil32beaff2015-01-15 01:32:23 +0000740def FindCheckFiles(path):
741 if not path:
742 Logger.fail("No source path provided")
743 elif os.path.isfile(path):
744 return [ path ]
745 elif os.path.isdir(path):
746 foundFiles = []
747 for root, dirs, files in os.walk(path):
748 for file in files:
749 if os.path.splitext(file)[1] == ".java":
750 foundFiles.append(os.path.join(root, file))
751 return foundFiles
752 else:
753 Logger.fail("Source path \"" + path + "\" not found")
David Brazdil2e15cd22014-12-31 17:28:38 +0000754
David Brazdil32beaff2015-01-15 01:32:23 +0000755
756def RunChecks(checkPrefix, checkPath, outputFilename):
757 outputBaseName = os.path.basename(outputFilename)
David Brazdil2e15cd22014-12-31 17:28:38 +0000758 outputFile = OutputFile(open(outputFilename, "r"), outputBaseName)
David Brazdil32beaff2015-01-15 01:32:23 +0000759
760 for checkFilename in FindCheckFiles(checkPath):
761 checkBaseName = os.path.basename(checkFilename)
762 checkFile = CheckFile(checkPrefix, open(checkFilename, "r"), checkBaseName)
763 checkFile.match(outputFile)
David Brazdilee690a32014-12-01 17:04:16 +0000764
765
766if __name__ == "__main__":
767 args = ParseArguments()
David Brazdil3f7dce82015-01-16 23:31:11 +0000768
David Brazdil7cca5df2015-01-15 00:40:56 +0000769 if args.quiet:
770 Logger.Verbosity = Logger.Level.Error
David Brazdilee690a32014-12-01 17:04:16 +0000771
David Brazdil3f7dce82015-01-16 23:31:11 +0000772 if args.list_groups:
773 ListGroups(args.tested_file)
774 elif args.dump_group:
775 DumpGroup(args.tested_file, args.dump_group)
776 else:
777 RunChecks(args.check_prefix, args.source_path, args.tested_file)