blob: 5744c15488ee4ce03673c18693c91c54cb65344a [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 Brazdil2e15cd22014-12-31 17:28:38 +0000131 sys.exit(1)
132
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."""
165 Text, Pattern, VarRef, VarDef = range(4)
166
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
190 def parseText(text):
191 return CheckElement(CheckElement.Variant.Text, None, re.escape(text))
192
193 @staticmethod
194 def parsePattern(patternElem):
David Brazdilbe0cc082014-12-31 11:49:30 +0000195 return CheckElement(CheckElement.Variant.Pattern, None, patternElem[2:-2])
David Brazdilee690a32014-12-01 17:04:16 +0000196
197 @staticmethod
198 def parseVariable(varElem):
199 colonPos = varElem.find(":")
200 if colonPos == -1:
201 # Variable reference
David Brazdilbe0cc082014-12-31 11:49:30 +0000202 name = varElem[2:-2]
David Brazdilee690a32014-12-01 17:04:16 +0000203 return CheckElement(CheckElement.Variant.VarRef, name, None)
204 else:
205 # Variable definition
206 name = varElem[2:colonPos]
David Brazdilbe0cc082014-12-31 11:49:30 +0000207 body = varElem[colonPos+1:-2]
David Brazdilee690a32014-12-01 17:04:16 +0000208 return CheckElement(CheckElement.Variant.VarDef, name, body)
209
David Brazdilee690a32014-12-01 17:04:16 +0000210class CheckLine(CommonEqualityMixin):
211 """Representation of a single assertion in the check file formed of one or
212 more regex elements. Matching against an output line is successful only
213 if all regex elements can be matched in the given order."""
214
David Brazdil9a6f20e2014-12-19 11:17:21 +0000215 class Variant(object):
216 """Supported types of assertions."""
217 InOrder, DAG, Not = range(3)
David Brazdilee690a32014-12-01 17:04:16 +0000218
David Brazdil2e15cd22014-12-31 17:28:38 +0000219 def __init__(self, content, variant=Variant.InOrder, fileName=None, lineNo=-1):
220 self.fileName = fileName
David Brazdilee690a32014-12-01 17:04:16 +0000221 self.lineNo = lineNo
David Brazdil2e15cd22014-12-31 17:28:38 +0000222 self.content = content.strip()
David Brazdilee690a32014-12-01 17:04:16 +0000223
David Brazdil2e15cd22014-12-31 17:28:38 +0000224 self.variant = variant
David Brazdil9a6f20e2014-12-19 11:17:21 +0000225 self.lineParts = self.__parse(self.content)
David Brazdilee690a32014-12-01 17:04:16 +0000226 if not self.lineParts:
David Brazdil2e15cd22014-12-31 17:28:38 +0000227 Logger.fail("Empty check line", self.fileName, self.lineNo)
228
229 if self.variant == CheckLine.Variant.Not:
230 for elem in self.lineParts:
231 if elem.variant == CheckElement.Variant.VarDef:
232 Logger.fail("CHECK-NOT lines cannot define variables", self.fileName, self.lineNo)
233
234 def __eq__(self, other):
235 return (isinstance(other, self.__class__) and
236 self.variant == other.variant and
237 self.lineParts == other.lineParts)
David Brazdilee690a32014-12-01 17:04:16 +0000238
239 # Returns True if the given Match object was at the beginning of the line.
240 def __isMatchAtStart(self, match):
241 return (match is not None) and (match.start() == 0)
242
243 # Takes in a list of Match objects and returns the minimal start point among
244 # them. If there aren't any successful matches it returns the length of
245 # the searched string.
246 def __firstMatch(self, matches, string):
247 starts = map(lambda m: len(string) if m is None else m.start(), matches)
248 return min(starts)
249
David Brazdilee690a32014-12-01 17:04:16 +0000250 # This method parses the content of a check line stripped of the initial
251 # comment symbol and the CHECK keyword.
252 def __parse(self, line):
253 lineParts = []
254 # Loop as long as there is something to parse.
255 while line:
256 # Search for the nearest occurrence of the special markers.
David Brazdilbe0cc082014-12-31 11:49:30 +0000257 matchWhitespace = re.search(r"\s+", line)
258 matchPattern = re.search(CheckElement.regexPattern, line)
259 matchVariable = re.search(CheckElement.regexVariable, line)
David Brazdilee690a32014-12-01 17:04:16 +0000260
261 # If one of the above was identified at the current position, extract them
262 # from the line, parse them and add to the list of line parts.
263 if self.__isMatchAtStart(matchWhitespace):
264 # We want to be whitespace-agnostic so whenever a check line contains
265 # a whitespace, we add a regex pattern for an arbitrary non-zero number
266 # of whitespaces.
267 line = line[matchWhitespace.end():]
David Brazdilbe0cc082014-12-31 11:49:30 +0000268 lineParts.append(CheckElement.parsePattern(r"{{\s+}}"))
David Brazdilee690a32014-12-01 17:04:16 +0000269 elif self.__isMatchAtStart(matchPattern):
270 pattern = line[0:matchPattern.end()]
271 line = line[matchPattern.end():]
272 lineParts.append(CheckElement.parsePattern(pattern))
273 elif self.__isMatchAtStart(matchVariable):
274 var = line[0:matchVariable.end()]
275 line = line[matchVariable.end():]
David Brazdil2e15cd22014-12-31 17:28:38 +0000276 lineParts.append(CheckElement.parseVariable(var))
David Brazdilee690a32014-12-01 17:04:16 +0000277 else:
278 # If we're not currently looking at a special marker, this is a plain
279 # text match all the way until the first special marker (or the end
280 # of the line).
281 firstMatch = self.__firstMatch([ matchWhitespace, matchPattern, matchVariable ], line)
282 text = line[0:firstMatch]
283 line = line[firstMatch:]
284 lineParts.append(CheckElement.parseText(text))
285 return lineParts
286
287 # Returns the regex pattern to be matched in the output line. Variable
288 # references are substituted with their current values provided in the
289 # 'varState' argument.
290 # An exception is raised if a referenced variable is undefined.
291 def __generatePattern(self, linePart, varState):
292 if linePart.variant == CheckElement.Variant.VarRef:
293 try:
294 return re.escape(varState[linePart.name])
295 except KeyError:
David Brazdil2e15cd22014-12-31 17:28:38 +0000296 Logger.testFailed("Use of undefined variable \"" + linePart.name + "\"",
297 self.fileName, self.lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000298 else:
299 return linePart.pattern
300
301 # Attempts to match the check line against a line from the output file with
302 # the given initial variable values. It returns the new variable state if
303 # successful and None otherwise.
304 def match(self, outputLine, initialVarState):
305 initialSearchFrom = 0
306 initialPattern = self.__generatePattern(self.lineParts[0], initialVarState)
307 while True:
308 # Search for the first element on the regex parts list. This will mark
309 # the point on the line from which we will attempt to match the rest of
310 # the check pattern. If this iteration produces only a partial match,
311 # the next iteration will start searching further in the output.
312 firstMatch = re.search(initialPattern, outputLine[initialSearchFrom:])
313 if firstMatch is None:
314 return None
315 matchStart = initialSearchFrom + firstMatch.start()
316 initialSearchFrom += firstMatch.start() + 1
317
318 # Do the full matching on a shadow copy of the variable state. If the
319 # matching fails half-way, we will not need to revert the state.
320 varState = dict(initialVarState)
321
322 # Now try to parse all of the parts of the check line in the right order.
323 # Variable values are updated on-the-fly, meaning that a variable can
324 # be referenced immediately after its definition.
325 fullyMatched = True
326 for part in self.lineParts:
327 pattern = self.__generatePattern(part, varState)
328 match = re.match(pattern, outputLine[matchStart:])
329 if match is None:
330 fullyMatched = False
331 break
332 matchEnd = matchStart + match.end()
333 if part.variant == CheckElement.Variant.VarDef:
334 if part.name in varState:
David Brazdil2e15cd22014-12-31 17:28:38 +0000335 Logger.testFailed("Multiple definitions of variable \"" + part.name + "\"",
336 self.fileName, self.lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000337 varState[part.name] = outputLine[matchStart:matchEnd]
338 matchStart = matchEnd
339
340 # Return the new variable state if all parts were successfully matched.
341 # Otherwise loop and try to find another start point on the same line.
342 if fullyMatched:
343 return varState
344
345
346class CheckGroup(CommonEqualityMixin):
347 """Represents a named collection of check lines which are to be matched
348 against an output group of the same name."""
349
David Brazdil2e15cd22014-12-31 17:28:38 +0000350 def __init__(self, name, lines, fileName=None, lineNo=-1):
351 self.fileName = fileName
352 self.lineNo = lineNo
353
354 if not name:
355 Logger.fail("Check group does not have a name", self.fileName, self.lineNo)
356 if not lines:
357 Logger.fail("Check group does not have a body", self.fileName, self.lineNo)
358
359 self.name = name
360 self.lines = lines
361
362 def __eq__(self, other):
363 return (isinstance(other, self.__class__) and
364 self.name == other.name and
365 self.lines == other.lines)
David Brazdilee690a32014-12-01 17:04:16 +0000366
367 def __headAndTail(self, list):
368 return list[0], list[1:]
369
David Brazdil9a6f20e2014-12-19 11:17:21 +0000370 # Splits a list of check lines at index 'i' such that lines[i] is the first
371 # element whose variant is not equal to the given parameter.
372 def __splitByVariant(self, lines, variant):
373 i = 0
374 while i < len(lines) and lines[i].variant == variant:
375 i += 1
376 return lines[:i], lines[i:]
David Brazdilee690a32014-12-01 17:04:16 +0000377
David Brazdil9a6f20e2014-12-19 11:17:21 +0000378 # Extracts the first sequence of check lines which are independent of each
379 # other's match location, i.e. either consecutive DAG lines or a single
380 # InOrder line. Any Not lines preceeding this sequence are also extracted.
381 def __nextIndependentChecks(self, checkLines):
382 notChecks, checkLines = self.__splitByVariant(checkLines, CheckLine.Variant.Not)
383 if not checkLines:
384 return notChecks, [], []
385
386 head, tail = self.__headAndTail(checkLines)
387 if head.variant == CheckLine.Variant.InOrder:
388 return notChecks, [head], tail
389 else:
390 assert head.variant == CheckLine.Variant.DAG
391 independentChecks, checkLines = self.__splitByVariant(checkLines, CheckLine.Variant.DAG)
392 return notChecks, independentChecks, checkLines
393
394 # If successful, returns the line number of the first output line matching the
395 # check line and the updated variable state. Otherwise returns -1 and None,
396 # respectively. The 'lineFilter' parameter can be used to supply a list of
397 # line numbers (counting from 1) which should be skipped.
David Brazdil2e15cd22014-12-31 17:28:38 +0000398 def __findFirstMatch(self, checkLine, outputLines, startLineNo, lineFilter, varState):
399 matchLineNo = startLineNo
David Brazdil9a6f20e2014-12-19 11:17:21 +0000400 for outputLine in outputLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000401 if matchLineNo not in lineFilter:
402 newVarState = checkLine.match(outputLine, varState)
403 if newVarState is not None:
404 return matchLineNo, newVarState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000405 matchLineNo += 1
David Brazdil9a6f20e2014-12-19 11:17:21 +0000406 return -1, None
407
408 # Matches the given positive check lines against the output in order of
409 # appearance. Variable state is propagated but the scope of the search remains
410 # the same for all checks. Each output line can only be matched once.
411 # If all check lines are matched, the resulting variable state is returned
412 # together with the remaining output. The function also returns output lines
413 # which appear before either of the matched lines so they can be tested
414 # against Not checks.
David Brazdil2e15cd22014-12-31 17:28:38 +0000415 def __matchIndependentChecks(self, checkLines, outputLines, startLineNo, varState):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000416 # If no checks are provided, skip over the entire output.
417 if not checkLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000418 return outputLines, [], startLineNo + len(outputLines), varState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000419
420 # Keep track of which lines have been matched.
421 matchedLines = []
422
423 # Find first unused output line which matches each check line.
424 for checkLine in checkLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000425 matchLineNo, varState = \
426 self.__findFirstMatch(checkLine, outputLines, startLineNo, matchedLines, varState)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000427 if varState is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000428 Logger.testFailed("Could not match check line \"" + checkLine.content + "\" " +
429 "starting from output line " + str(startLineNo),
430 self.fileName, checkLine.lineNo)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000431 matchedLines.append(matchLineNo)
432
433 # Return new variable state and the output lines which lie outside the
434 # match locations of this independent group.
David Brazdil2e15cd22014-12-31 17:28:38 +0000435 minMatchLineNo = min(matchedLines)
436 maxMatchLineNo = max(matchedLines)
437 preceedingLines = outputLines[:minMatchLineNo - startLineNo]
438 remainingLines = outputLines[maxMatchLineNo - startLineNo + 1:]
439 return preceedingLines, remainingLines, maxMatchLineNo + 1, varState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000440
441 # Makes sure that the given check lines do not match any of the given output
442 # lines. Variable state does not change.
David Brazdil2e15cd22014-12-31 17:28:38 +0000443 def __matchNotLines(self, checkLines, outputLines, startLineNo, varState):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000444 for checkLine in checkLines:
445 assert checkLine.variant == CheckLine.Variant.Not
David Brazdil21df8892015-01-08 01:49:53 +0000446 matchLineNo, matchVarState = \
David Brazdil2e15cd22014-12-31 17:28:38 +0000447 self.__findFirstMatch(checkLine, outputLines, startLineNo, [], varState)
David Brazdil21df8892015-01-08 01:49:53 +0000448 if matchVarState is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000449 Logger.testFailed("CHECK-NOT line \"" + checkLine.content + "\" matches output line " + \
450 str(matchLineNo), self.fileName, checkLine.lineNo)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000451
452 # Matches the check lines in this group against an output group. It is
453 # responsible for running the checks in the right order and scope, and
454 # for propagating the variable state between the check lines.
455 def match(self, outputGroup):
456 varState = {}
David Brazdilee690a32014-12-01 17:04:16 +0000457 checkLines = self.lines
458 outputLines = outputGroup.body
David Brazdil2e15cd22014-12-31 17:28:38 +0000459 startLineNo = outputGroup.lineNo
David Brazdilee690a32014-12-01 17:04:16 +0000460
David Brazdilee690a32014-12-01 17:04:16 +0000461 while checkLines:
David Brazdil9a6f20e2014-12-19 11:17:21 +0000462 # Extract the next sequence of location-independent checks to be matched.
463 notChecks, independentChecks, checkLines = self.__nextIndependentChecks(checkLines)
David Brazdil2e15cd22014-12-31 17:28:38 +0000464
David Brazdil9a6f20e2014-12-19 11:17:21 +0000465 # Match the independent checks.
David Brazdil2e15cd22014-12-31 17:28:38 +0000466 notOutput, outputLines, newStartLineNo, newVarState = \
467 self.__matchIndependentChecks(independentChecks, outputLines, startLineNo, varState)
468
David Brazdil9a6f20e2014-12-19 11:17:21 +0000469 # Run the Not checks against the output lines which lie between the last
470 # two independent groups or the bounds of the output.
David Brazdil2e15cd22014-12-31 17:28:38 +0000471 self.__matchNotLines(notChecks, notOutput, startLineNo, varState)
472
David Brazdil9a6f20e2014-12-19 11:17:21 +0000473 # Update variable state.
David Brazdil2e15cd22014-12-31 17:28:38 +0000474 startLineNo = newStartLineNo
David Brazdil9a6f20e2014-12-19 11:17:21 +0000475 varState = newVarState
David Brazdilee690a32014-12-01 17:04:16 +0000476
477class OutputGroup(CommonEqualityMixin):
478 """Represents a named part of the test output against which a check group of
479 the same name is to be matched."""
480
David Brazdil2e15cd22014-12-31 17:28:38 +0000481 def __init__(self, name, body, fileName=None, lineNo=-1):
482 if not name:
483 Logger.fail("Output group does not have a name", fileName, lineNo)
484 if not body:
485 Logger.fail("Output group does not have a body", fileName, lineNo)
486
487 self.name = name
488 self.body = body
489 self.lineNo = lineNo
490
491 def __eq__(self, other):
492 return (isinstance(other, self.__class__) and
493 self.name == other.name and
494 self.body == other.body)
David Brazdilee690a32014-12-01 17:04:16 +0000495
496
497class FileSplitMixin(object):
498 """Mixin for representing text files which need to be split into smaller
499 chunks before being parsed."""
500
501 def _parseStream(self, stream):
502 lineNo = 0
503 allGroups = []
504 currentGroup = None
505
506 for line in stream:
507 lineNo += 1
508 line = line.strip()
509 if not line:
510 continue
511
512 # Let the child class process the line and return information about it.
513 # The _processLine method can modify the content of the line (or delete it
514 # entirely) and specify whether it starts a new group.
515 processedLine, newGroupName = self._processLine(line, lineNo)
516 if newGroupName is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000517 currentGroup = (newGroupName, [], lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000518 allGroups.append(currentGroup)
519 if processedLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000520 if currentGroup is not None:
521 currentGroup[1].append(processedLine)
522 else:
523 self._exceptionLineOutsideGroup(line, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000524
525 # Finally, take the generated line groups and let the child class process
526 # each one before storing the final outcome.
David Brazdil2e15cd22014-12-31 17:28:38 +0000527 return list(map(lambda group: self._processGroup(group[0], group[1], group[2]), allGroups))
David Brazdilee690a32014-12-01 17:04:16 +0000528
529
530class CheckFile(FileSplitMixin):
531 """Collection of check groups extracted from the input test file."""
532
David Brazdil2e15cd22014-12-31 17:28:38 +0000533 def __init__(self, prefix, checkStream, fileName=None):
534 self.fileName = fileName
David Brazdilee690a32014-12-01 17:04:16 +0000535 self.prefix = prefix
536 self.groups = self._parseStream(checkStream)
537
538 # Attempts to parse a check line. The regex searches for a comment symbol
539 # followed by the CHECK keyword, given attribute and a colon at the very
540 # beginning of the line. Whitespaces are ignored.
541 def _extractLine(self, prefix, line):
David Brazdilbe0cc082014-12-31 11:49:30 +0000542 rIgnoreWhitespace = r"\s*"
543 rCommentSymbols = [r"//", r"#"]
544 regexPrefix = rIgnoreWhitespace + \
545 r"(" + r"|".join(rCommentSymbols) + r")" + \
546 rIgnoreWhitespace + \
547 prefix + r":"
David Brazdilee690a32014-12-01 17:04:16 +0000548
549 # The 'match' function succeeds only if the pattern is matched at the
550 # beginning of the line.
David Brazdilbe0cc082014-12-31 11:49:30 +0000551 match = re.match(regexPrefix, line)
David Brazdilee690a32014-12-01 17:04:16 +0000552 if match is not None:
553 return line[match.end():].strip()
554 else:
555 return None
556
David Brazdil48942de2015-01-07 21:19:50 +0000557 # This function is invoked on each line of the check file and returns a pair
558 # which instructs the parser how the line should be handled. If the line is to
559 # be included in the current check group, it is returned in the first value.
560 # If the line starts a new check group, the name of the group is returned in
561 # the second value.
David Brazdilee690a32014-12-01 17:04:16 +0000562 def _processLine(self, line, lineNo):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000563 # Lines beginning with 'CHECK-START' start a new check group.
David Brazdilee690a32014-12-01 17:04:16 +0000564 startLine = self._extractLine(self.prefix + "-START", line)
565 if startLine is not None:
David Brazdil9a6f20e2014-12-19 11:17:21 +0000566 return None, startLine
567
568 # Lines starting only with 'CHECK' are matched in order.
569 plainLine = self._extractLine(self.prefix, line)
570 if plainLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000571 return (plainLine, CheckLine.Variant.InOrder, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000572
573 # 'CHECK-DAG' lines are no-order assertions.
574 dagLine = self._extractLine(self.prefix + "-DAG", line)
575 if dagLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000576 return (dagLine, CheckLine.Variant.DAG, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000577
578 # 'CHECK-NOT' lines are no-order negative assertions.
579 notLine = self._extractLine(self.prefix + "-NOT", line)
580 if notLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000581 return (notLine, CheckLine.Variant.Not, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000582
583 # Other lines are ignored.
584 return None, None
David Brazdilee690a32014-12-01 17:04:16 +0000585
586 def _exceptionLineOutsideGroup(self, line, lineNo):
David Brazdil2e15cd22014-12-31 17:28:38 +0000587 Logger.fail("Check line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000588
David Brazdil48942de2015-01-07 21:19:50 +0000589 # Constructs a check group from the parser-collected check lines.
David Brazdil2e15cd22014-12-31 17:28:38 +0000590 def _processGroup(self, name, lines, lineNo):
591 checkLines = list(map(lambda line: CheckLine(line[0], line[1], self.fileName, line[2]), lines))
592 return CheckGroup(name, checkLines, self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000593
David Brazdil2e15cd22014-12-31 17:28:38 +0000594 def match(self, outputFile):
David Brazdilee690a32014-12-01 17:04:16 +0000595 for checkGroup in self.groups:
596 # TODO: Currently does not handle multiple occurrences of the same group
597 # name, e.g. when a pass is run multiple times. It will always try to
598 # match a check group against the first output group of the same name.
599 outputGroup = outputFile.findGroup(checkGroup.name)
600 if outputGroup is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000601 Logger.fail("Group \"" + checkGroup.name + "\" not found in the output",
602 self.fileName, checkGroup.lineNo)
603 Logger.startTest(checkGroup.name)
604 checkGroup.match(outputGroup)
605 Logger.testPassed()
David Brazdilee690a32014-12-01 17:04:16 +0000606
607
608class OutputFile(FileSplitMixin):
609 """Representation of the output generated by the test and split into groups
610 within which the checks are performed.
611
612 C1visualizer format is parsed with a state machine which differentiates
613 between the 'compilation' and 'cfg' blocks. The former marks the beginning
614 of a method. It is parsed for the method's name but otherwise ignored. Each
615 subsequent CFG block represents one stage of the compilation pipeline and
616 is parsed into an output group named "<method name> <pass name>".
617 """
618
619 class ParsingState:
620 OutsideBlock, InsideCompilationBlock, StartingCfgBlock, InsideCfgBlock = range(4)
621
David Brazdil2e15cd22014-12-31 17:28:38 +0000622 def __init__(self, outputStream, fileName=None):
623 self.fileName = fileName
624
David Brazdilee690a32014-12-01 17:04:16 +0000625 # Initialize the state machine
626 self.lastMethodName = None
627 self.state = OutputFile.ParsingState.OutsideBlock
628 self.groups = self._parseStream(outputStream)
629
David Brazdil48942de2015-01-07 21:19:50 +0000630 # This function is invoked on each line of the output file and returns a pair
631 # which instructs the parser how the line should be handled. If the line is to
632 # be included in the current group, it is returned in the first value. If the
633 # line starts a new output group, the name of the group is returned in the
634 # second value.
David Brazdilee690a32014-12-01 17:04:16 +0000635 def _processLine(self, line, lineNo):
636 if self.state == OutputFile.ParsingState.StartingCfgBlock:
637 # Previous line started a new 'cfg' block which means that this one must
638 # contain the name of the pass (this is enforced by C1visualizer).
639 if re.match("name\s+\"[^\"]+\"", line):
640 # Extract the pass name, prepend it with the name of the method and
641 # return as the beginning of a new group.
642 self.state = OutputFile.ParsingState.InsideCfgBlock
643 return (None, self.lastMethodName + " " + line.split("\"")[1])
644 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000645 Logger.fail("Expected output group name", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000646
647 elif self.state == OutputFile.ParsingState.InsideCfgBlock:
648 if line == "end_cfg":
649 self.state = OutputFile.ParsingState.OutsideBlock
650 return (None, None)
651 else:
652 return (line, None)
653
654 elif self.state == OutputFile.ParsingState.InsideCompilationBlock:
655 # Search for the method's name. Format: method "<name>"
David Brazdil2e15cd22014-12-31 17:28:38 +0000656 if re.match("method\s+\"[^\"]*\"", line):
657 methodName = line.split("\"")[1].strip()
658 if not methodName:
659 Logger.fail("Empty method name in output", self.fileName, lineNo)
660 self.lastMethodName = methodName
David Brazdilee690a32014-12-01 17:04:16 +0000661 elif line == "end_compilation":
662 self.state = OutputFile.ParsingState.OutsideBlock
663 return (None, None)
664
David Brazdil2e15cd22014-12-31 17:28:38 +0000665 else:
666 assert self.state == OutputFile.ParsingState.OutsideBlock
David Brazdilee690a32014-12-01 17:04:16 +0000667 if line == "begin_cfg":
668 # The line starts a new group but we'll wait until the next line from
669 # which we can extract the name of the pass.
670 if self.lastMethodName is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000671 Logger.fail("Expected method header", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000672 self.state = OutputFile.ParsingState.StartingCfgBlock
673 return (None, None)
674 elif line == "begin_compilation":
675 self.state = OutputFile.ParsingState.InsideCompilationBlock
676 return (None, None)
677 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000678 Logger.fail("Output line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000679
David Brazdil48942de2015-01-07 21:19:50 +0000680 # Constructs an output group from the parser-collected output lines.
David Brazdil2e15cd22014-12-31 17:28:38 +0000681 def _processGroup(self, name, lines, lineNo):
682 return OutputGroup(name, lines, self.fileName, lineNo + 1)
David Brazdilee690a32014-12-01 17:04:16 +0000683
684 def findGroup(self, name):
685 for group in self.groups:
686 if group.name == name:
687 return group
688 return None
689
690
691def ParseArguments():
692 parser = argparse.ArgumentParser()
David Brazdil32beaff2015-01-15 01:32:23 +0000693 parser.add_argument("tested_file",
694 help="text file the checks should be verified against")
695 parser.add_argument("source_path", nargs="?",
696 help="path to file/folder with checking annotations")
David Brazdilee690a32014-12-01 17:04:16 +0000697 parser.add_argument("--check-prefix", dest="check_prefix", default="CHECK", metavar="PREFIX",
David Brazdil32beaff2015-01-15 01:32:23 +0000698 help="prefix of checks in the test files (default: CHECK)")
David Brazdilee690a32014-12-01 17:04:16 +0000699 parser.add_argument("--list-groups", dest="list_groups", action="store_true",
David Brazdil32beaff2015-01-15 01:32:23 +0000700 help="print a list of all groups found in the tested file")
David Brazdilee690a32014-12-01 17:04:16 +0000701 parser.add_argument("--dump-group", dest="dump_group", metavar="GROUP",
702 help="print the contents of an output group")
David Brazdil7cca5df2015-01-15 00:40:56 +0000703 parser.add_argument("-q", "--quiet", action="store_true",
704 help="print only errors")
David Brazdilee690a32014-12-01 17:04:16 +0000705 return parser.parse_args()
706
707
David Brazdilee690a32014-12-01 17:04:16 +0000708def ListGroups(outputFilename):
709 outputFile = OutputFile(open(outputFilename, "r"))
710 for group in outputFile.groups:
David Brazdil2e15cd22014-12-31 17:28:38 +0000711 Logger.log(group.name)
David Brazdilee690a32014-12-01 17:04:16 +0000712
713
714def DumpGroup(outputFilename, groupName):
715 outputFile = OutputFile(open(outputFilename, "r"))
716 group = outputFile.findGroup(groupName)
717 if group:
David Brazdil2e15cd22014-12-31 17:28:38 +0000718 lineNo = group.lineNo
719 maxLineNo = lineNo + len(group.body)
720 lenLineNo = len(str(maxLineNo)) + 2
721 for line in group.body:
722 Logger.log((str(lineNo) + ":").ljust(lenLineNo) + line)
723 lineNo += 1
David Brazdilee690a32014-12-01 17:04:16 +0000724 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000725 Logger.fail("Group \"" + groupName + "\" not found in the output")
David Brazdilee690a32014-12-01 17:04:16 +0000726
727
David Brazdil3f7dce82015-01-16 23:31:11 +0000728# Returns a list of files to scan for check annotations in the given path. Path
729# to a file is returned as a single-element list, directories are recursively
730# traversed and all '.java' files returned.
David Brazdil32beaff2015-01-15 01:32:23 +0000731def FindCheckFiles(path):
732 if not path:
733 Logger.fail("No source path provided")
734 elif os.path.isfile(path):
735 return [ path ]
736 elif os.path.isdir(path):
737 foundFiles = []
738 for root, dirs, files in os.walk(path):
739 for file in files:
740 if os.path.splitext(file)[1] == ".java":
741 foundFiles.append(os.path.join(root, file))
742 return foundFiles
743 else:
744 Logger.fail("Source path \"" + path + "\" not found")
David Brazdil2e15cd22014-12-31 17:28:38 +0000745
David Brazdil32beaff2015-01-15 01:32:23 +0000746
747def RunChecks(checkPrefix, checkPath, outputFilename):
748 outputBaseName = os.path.basename(outputFilename)
David Brazdil2e15cd22014-12-31 17:28:38 +0000749 outputFile = OutputFile(open(outputFilename, "r"), outputBaseName)
David Brazdil32beaff2015-01-15 01:32:23 +0000750
751 for checkFilename in FindCheckFiles(checkPath):
752 checkBaseName = os.path.basename(checkFilename)
753 checkFile = CheckFile(checkPrefix, open(checkFilename, "r"), checkBaseName)
754 checkFile.match(outputFile)
David Brazdilee690a32014-12-01 17:04:16 +0000755
756
757if __name__ == "__main__":
758 args = ParseArguments()
David Brazdil3f7dce82015-01-16 23:31:11 +0000759
David Brazdil7cca5df2015-01-15 00:40:56 +0000760 if args.quiet:
761 Logger.Verbosity = Logger.Level.Error
David Brazdilee690a32014-12-01 17:04:16 +0000762
David Brazdil3f7dce82015-01-16 23:31:11 +0000763 if args.list_groups:
764 ListGroups(args.tested_file)
765 elif args.dump_group:
766 DumpGroup(args.tested_file, args.dump_group)
767 else:
768 RunChecks(args.check_prefix, args.source_path, args.tested_file)