blob: b1abea2fe5bb136f259fc48c69b2472a33b21dd4 [file] [log] [blame]
David Brazdilee690a32014-12-01 17:04:16 +00001#!/usr/bin/env python3
2#
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.
43# - CHECK-NOT: Must not match any output line which appear in the output group
44# 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
74import argparse
75import os
76import re
77import shutil
78import sys
79import tempfile
80from subprocess import check_call
81
David Brazdil2e15cd22014-12-31 17:28:38 +000082class Logger(object):
83 SilentMode = False
84
85 class Color(object):
86 Default, Blue, Gray, Purple, Red = range(5)
87
88 @staticmethod
89 def terminalCode(color, out=sys.stdout):
90 if not out.isatty():
91 return ''
92 elif color == Logger.Color.Blue:
93 return '\033[94m'
94 elif color == Logger.Color.Gray:
95 return '\033[37m'
96 elif color == Logger.Color.Purple:
97 return '\033[95m'
98 elif color == Logger.Color.Red:
99 return '\033[91m'
100 else:
101 return '\033[0m'
102
103 @staticmethod
104 def log(text, color=Color.Default, newLine=True, out=sys.stdout):
105 if not Logger.SilentMode:
106 text = Logger.Color.terminalCode(color, out) + text + \
107 Logger.Color.terminalCode(Logger.Color.Default, out)
108 if newLine:
109 print(text, file=out)
110 else:
111 print(text, end="", flush=True, file=out)
112
113 @staticmethod
114 def fail(msg, file=None, line=-1):
115 location = ""
116 if file:
117 location += file + ":"
118 if line > 0:
119 location += str(line) + ":"
120 if location:
121 location += " "
122
123 Logger.log(location, color=Logger.Color.Gray, newLine=False, out=sys.stderr)
124 Logger.log("error: ", color=Logger.Color.Red, newLine=False, out=sys.stderr)
125 Logger.log(msg, out=sys.stderr)
126 sys.exit(1)
127
128 @staticmethod
129 def startTest(name):
130 Logger.log("TEST ", color=Logger.Color.Purple, newLine=False)
131 Logger.log(name + "... ", newLine=False)
132
133 @staticmethod
134 def testPassed():
135 Logger.log("PASS", color=Logger.Color.Blue)
136
137 @staticmethod
138 def testFailed(msg, file=None, line=-1):
139 Logger.log("FAIL", color=Logger.Color.Red)
140 Logger.fail(msg, file, line)
141
David Brazdilee690a32014-12-01 17:04:16 +0000142class CommonEqualityMixin:
143 """Mixin for class equality as equality of the fields."""
144 def __eq__(self, other):
145 return (isinstance(other, self.__class__)
146 and self.__dict__ == other.__dict__)
147
148 def __ne__(self, other):
149 return not self.__eq__(other)
150
151 def __repr__(self):
152 return "<%s: %s>" % (type(self).__name__, str(self.__dict__))
153
154
155class CheckElement(CommonEqualityMixin):
156 """Single element of the check line."""
157
158 class Variant(object):
159 """Supported language constructs."""
160 Text, Pattern, VarRef, VarDef = range(4)
161
David Brazdilbe0cc082014-12-31 11:49:30 +0000162 rStartOptional = r"("
163 rEndOptional = r")?"
164
165 rName = r"([a-zA-Z][a-zA-Z0-9]*)"
166 rRegex = r"(.+?)"
167 rPatternStartSym = r"(\{\{)"
168 rPatternEndSym = r"(\}\})"
169 rVariableStartSym = r"(\[\[)"
170 rVariableEndSym = r"(\]\])"
171 rVariableSeparator = r"(:)"
172
173 regexPattern = rPatternStartSym + rRegex + rPatternEndSym
174 regexVariable = rVariableStartSym + \
175 rName + \
176 (rStartOptional + rVariableSeparator + rRegex + rEndOptional) + \
177 rVariableEndSym
178
David Brazdilee690a32014-12-01 17:04:16 +0000179 def __init__(self, variant, name, pattern):
180 self.variant = variant
181 self.name = name
182 self.pattern = pattern
183
184 @staticmethod
185 def parseText(text):
186 return CheckElement(CheckElement.Variant.Text, None, re.escape(text))
187
188 @staticmethod
189 def parsePattern(patternElem):
David Brazdilbe0cc082014-12-31 11:49:30 +0000190 return CheckElement(CheckElement.Variant.Pattern, None, patternElem[2:-2])
David Brazdilee690a32014-12-01 17:04:16 +0000191
192 @staticmethod
193 def parseVariable(varElem):
194 colonPos = varElem.find(":")
195 if colonPos == -1:
196 # Variable reference
David Brazdilbe0cc082014-12-31 11:49:30 +0000197 name = varElem[2:-2]
David Brazdilee690a32014-12-01 17:04:16 +0000198 return CheckElement(CheckElement.Variant.VarRef, name, None)
199 else:
200 # Variable definition
201 name = varElem[2:colonPos]
David Brazdilbe0cc082014-12-31 11:49:30 +0000202 body = varElem[colonPos+1:-2]
David Brazdilee690a32014-12-01 17:04:16 +0000203 return CheckElement(CheckElement.Variant.VarDef, name, body)
204
David Brazdilee690a32014-12-01 17:04:16 +0000205class CheckLine(CommonEqualityMixin):
206 """Representation of a single assertion in the check file formed of one or
207 more regex elements. Matching against an output line is successful only
208 if all regex elements can be matched in the given order."""
209
David Brazdil9a6f20e2014-12-19 11:17:21 +0000210 class Variant(object):
211 """Supported types of assertions."""
212 InOrder, DAG, Not = range(3)
David Brazdilee690a32014-12-01 17:04:16 +0000213
David Brazdil2e15cd22014-12-31 17:28:38 +0000214 def __init__(self, content, variant=Variant.InOrder, fileName=None, lineNo=-1):
215 self.fileName = fileName
David Brazdilee690a32014-12-01 17:04:16 +0000216 self.lineNo = lineNo
David Brazdil2e15cd22014-12-31 17:28:38 +0000217 self.content = content.strip()
David Brazdilee690a32014-12-01 17:04:16 +0000218
David Brazdil2e15cd22014-12-31 17:28:38 +0000219 self.variant = variant
David Brazdil9a6f20e2014-12-19 11:17:21 +0000220 self.lineParts = self.__parse(self.content)
David Brazdilee690a32014-12-01 17:04:16 +0000221 if not self.lineParts:
David Brazdil2e15cd22014-12-31 17:28:38 +0000222 Logger.fail("Empty check line", self.fileName, self.lineNo)
223
224 if self.variant == CheckLine.Variant.Not:
225 for elem in self.lineParts:
226 if elem.variant == CheckElement.Variant.VarDef:
227 Logger.fail("CHECK-NOT lines cannot define variables", self.fileName, self.lineNo)
228
229 def __eq__(self, other):
230 return (isinstance(other, self.__class__) and
231 self.variant == other.variant and
232 self.lineParts == other.lineParts)
David Brazdilee690a32014-12-01 17:04:16 +0000233
234 # Returns True if the given Match object was at the beginning of the line.
235 def __isMatchAtStart(self, match):
236 return (match is not None) and (match.start() == 0)
237
238 # Takes in a list of Match objects and returns the minimal start point among
239 # them. If there aren't any successful matches it returns the length of
240 # the searched string.
241 def __firstMatch(self, matches, string):
242 starts = map(lambda m: len(string) if m is None else m.start(), matches)
243 return min(starts)
244
David Brazdilee690a32014-12-01 17:04:16 +0000245 # This method parses the content of a check line stripped of the initial
246 # comment symbol and the CHECK keyword.
247 def __parse(self, line):
248 lineParts = []
249 # Loop as long as there is something to parse.
250 while line:
251 # Search for the nearest occurrence of the special markers.
David Brazdilbe0cc082014-12-31 11:49:30 +0000252 matchWhitespace = re.search(r"\s+", line)
253 matchPattern = re.search(CheckElement.regexPattern, line)
254 matchVariable = re.search(CheckElement.regexVariable, line)
David Brazdilee690a32014-12-01 17:04:16 +0000255
256 # If one of the above was identified at the current position, extract them
257 # from the line, parse them and add to the list of line parts.
258 if self.__isMatchAtStart(matchWhitespace):
259 # We want to be whitespace-agnostic so whenever a check line contains
260 # a whitespace, we add a regex pattern for an arbitrary non-zero number
261 # of whitespaces.
262 line = line[matchWhitespace.end():]
David Brazdilbe0cc082014-12-31 11:49:30 +0000263 lineParts.append(CheckElement.parsePattern(r"{{\s+}}"))
David Brazdilee690a32014-12-01 17:04:16 +0000264 elif self.__isMatchAtStart(matchPattern):
265 pattern = line[0:matchPattern.end()]
266 line = line[matchPattern.end():]
267 lineParts.append(CheckElement.parsePattern(pattern))
268 elif self.__isMatchAtStart(matchVariable):
269 var = line[0:matchVariable.end()]
270 line = line[matchVariable.end():]
David Brazdil2e15cd22014-12-31 17:28:38 +0000271 lineParts.append(CheckElement.parseVariable(var))
David Brazdilee690a32014-12-01 17:04:16 +0000272 else:
273 # If we're not currently looking at a special marker, this is a plain
274 # text match all the way until the first special marker (or the end
275 # of the line).
276 firstMatch = self.__firstMatch([ matchWhitespace, matchPattern, matchVariable ], line)
277 text = line[0:firstMatch]
278 line = line[firstMatch:]
279 lineParts.append(CheckElement.parseText(text))
280 return lineParts
281
282 # Returns the regex pattern to be matched in the output line. Variable
283 # references are substituted with their current values provided in the
284 # 'varState' argument.
285 # An exception is raised if a referenced variable is undefined.
286 def __generatePattern(self, linePart, varState):
287 if linePart.variant == CheckElement.Variant.VarRef:
288 try:
289 return re.escape(varState[linePart.name])
290 except KeyError:
David Brazdil2e15cd22014-12-31 17:28:38 +0000291 Logger.testFailed("Use of undefined variable \"" + linePart.name + "\"",
292 self.fileName, self.lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000293 else:
294 return linePart.pattern
295
296 # Attempts to match the check line against a line from the output file with
297 # the given initial variable values. It returns the new variable state if
298 # successful and None otherwise.
299 def match(self, outputLine, initialVarState):
300 initialSearchFrom = 0
301 initialPattern = self.__generatePattern(self.lineParts[0], initialVarState)
302 while True:
303 # Search for the first element on the regex parts list. This will mark
304 # the point on the line from which we will attempt to match the rest of
305 # the check pattern. If this iteration produces only a partial match,
306 # the next iteration will start searching further in the output.
307 firstMatch = re.search(initialPattern, outputLine[initialSearchFrom:])
308 if firstMatch is None:
309 return None
310 matchStart = initialSearchFrom + firstMatch.start()
311 initialSearchFrom += firstMatch.start() + 1
312
313 # Do the full matching on a shadow copy of the variable state. If the
314 # matching fails half-way, we will not need to revert the state.
315 varState = dict(initialVarState)
316
317 # Now try to parse all of the parts of the check line in the right order.
318 # Variable values are updated on-the-fly, meaning that a variable can
319 # be referenced immediately after its definition.
320 fullyMatched = True
321 for part in self.lineParts:
322 pattern = self.__generatePattern(part, varState)
323 match = re.match(pattern, outputLine[matchStart:])
324 if match is None:
325 fullyMatched = False
326 break
327 matchEnd = matchStart + match.end()
328 if part.variant == CheckElement.Variant.VarDef:
329 if part.name in varState:
David Brazdil2e15cd22014-12-31 17:28:38 +0000330 Logger.testFailed("Multiple definitions of variable \"" + part.name + "\"",
331 self.fileName, self.lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000332 varState[part.name] = outputLine[matchStart:matchEnd]
333 matchStart = matchEnd
334
335 # Return the new variable state if all parts were successfully matched.
336 # Otherwise loop and try to find another start point on the same line.
337 if fullyMatched:
338 return varState
339
340
341class CheckGroup(CommonEqualityMixin):
342 """Represents a named collection of check lines which are to be matched
343 against an output group of the same name."""
344
David Brazdil2e15cd22014-12-31 17:28:38 +0000345 def __init__(self, name, lines, fileName=None, lineNo=-1):
346 self.fileName = fileName
347 self.lineNo = lineNo
348
349 if not name:
350 Logger.fail("Check group does not have a name", self.fileName, self.lineNo)
351 if not lines:
352 Logger.fail("Check group does not have a body", self.fileName, self.lineNo)
353
354 self.name = name
355 self.lines = lines
356
357 def __eq__(self, other):
358 return (isinstance(other, self.__class__) and
359 self.name == other.name and
360 self.lines == other.lines)
David Brazdilee690a32014-12-01 17:04:16 +0000361
362 def __headAndTail(self, list):
363 return list[0], list[1:]
364
David Brazdil9a6f20e2014-12-19 11:17:21 +0000365 # Splits a list of check lines at index 'i' such that lines[i] is the first
366 # element whose variant is not equal to the given parameter.
367 def __splitByVariant(self, lines, variant):
368 i = 0
369 while i < len(lines) and lines[i].variant == variant:
370 i += 1
371 return lines[:i], lines[i:]
David Brazdilee690a32014-12-01 17:04:16 +0000372
David Brazdil9a6f20e2014-12-19 11:17:21 +0000373 # Extracts the first sequence of check lines which are independent of each
374 # other's match location, i.e. either consecutive DAG lines or a single
375 # InOrder line. Any Not lines preceeding this sequence are also extracted.
376 def __nextIndependentChecks(self, checkLines):
377 notChecks, checkLines = self.__splitByVariant(checkLines, CheckLine.Variant.Not)
378 if not checkLines:
379 return notChecks, [], []
380
381 head, tail = self.__headAndTail(checkLines)
382 if head.variant == CheckLine.Variant.InOrder:
383 return notChecks, [head], tail
384 else:
385 assert head.variant == CheckLine.Variant.DAG
386 independentChecks, checkLines = self.__splitByVariant(checkLines, CheckLine.Variant.DAG)
387 return notChecks, independentChecks, checkLines
388
389 # If successful, returns the line number of the first output line matching the
390 # check line and the updated variable state. Otherwise returns -1 and None,
391 # respectively. The 'lineFilter' parameter can be used to supply a list of
392 # line numbers (counting from 1) which should be skipped.
David Brazdil2e15cd22014-12-31 17:28:38 +0000393 def __findFirstMatch(self, checkLine, outputLines, startLineNo, lineFilter, varState):
394 matchLineNo = startLineNo
David Brazdil9a6f20e2014-12-19 11:17:21 +0000395 for outputLine in outputLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000396 if matchLineNo not in lineFilter:
397 newVarState = checkLine.match(outputLine, varState)
398 if newVarState is not None:
399 return matchLineNo, newVarState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000400 matchLineNo += 1
David Brazdil9a6f20e2014-12-19 11:17:21 +0000401 return -1, None
402
403 # Matches the given positive check lines against the output in order of
404 # appearance. Variable state is propagated but the scope of the search remains
405 # the same for all checks. Each output line can only be matched once.
406 # If all check lines are matched, the resulting variable state is returned
407 # together with the remaining output. The function also returns output lines
408 # which appear before either of the matched lines so they can be tested
409 # against Not checks.
David Brazdil2e15cd22014-12-31 17:28:38 +0000410 def __matchIndependentChecks(self, checkLines, outputLines, startLineNo, varState):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000411 # If no checks are provided, skip over the entire output.
412 if not checkLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000413 return outputLines, [], startLineNo + len(outputLines), varState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000414
415 # Keep track of which lines have been matched.
416 matchedLines = []
417
418 # Find first unused output line which matches each check line.
419 for checkLine in checkLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000420 matchLineNo, varState = \
421 self.__findFirstMatch(checkLine, outputLines, startLineNo, matchedLines, varState)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000422 if varState is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000423 Logger.testFailed("Could not match check line \"" + checkLine.content + "\" " +
424 "starting from output line " + str(startLineNo),
425 self.fileName, checkLine.lineNo)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000426 matchedLines.append(matchLineNo)
427
428 # Return new variable state and the output lines which lie outside the
429 # match locations of this independent group.
David Brazdil2e15cd22014-12-31 17:28:38 +0000430 minMatchLineNo = min(matchedLines)
431 maxMatchLineNo = max(matchedLines)
432 preceedingLines = outputLines[:minMatchLineNo - startLineNo]
433 remainingLines = outputLines[maxMatchLineNo - startLineNo + 1:]
434 return preceedingLines, remainingLines, maxMatchLineNo + 1, varState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000435
436 # Makes sure that the given check lines do not match any of the given output
437 # lines. Variable state does not change.
David Brazdil2e15cd22014-12-31 17:28:38 +0000438 def __matchNotLines(self, checkLines, outputLines, startLineNo, varState):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000439 for checkLine in checkLines:
440 assert checkLine.variant == CheckLine.Variant.Not
David Brazdil2e15cd22014-12-31 17:28:38 +0000441 matchLineNo, varState = \
442 self.__findFirstMatch(checkLine, outputLines, startLineNo, [], varState)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000443 if varState is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000444 Logger.testFailed("CHECK-NOT line \"" + checkLine.content + "\" matches output line " + \
445 str(matchLineNo), self.fileName, checkLine.lineNo)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000446
447 # Matches the check lines in this group against an output group. It is
448 # responsible for running the checks in the right order and scope, and
449 # for propagating the variable state between the check lines.
450 def match(self, outputGroup):
451 varState = {}
David Brazdilee690a32014-12-01 17:04:16 +0000452 checkLines = self.lines
453 outputLines = outputGroup.body
David Brazdil2e15cd22014-12-31 17:28:38 +0000454 startLineNo = outputGroup.lineNo
David Brazdilee690a32014-12-01 17:04:16 +0000455
David Brazdilee690a32014-12-01 17:04:16 +0000456 while checkLines:
David Brazdil9a6f20e2014-12-19 11:17:21 +0000457 # Extract the next sequence of location-independent checks to be matched.
458 notChecks, independentChecks, checkLines = self.__nextIndependentChecks(checkLines)
David Brazdil2e15cd22014-12-31 17:28:38 +0000459
David Brazdil9a6f20e2014-12-19 11:17:21 +0000460 # Match the independent checks.
David Brazdil2e15cd22014-12-31 17:28:38 +0000461 notOutput, outputLines, newStartLineNo, newVarState = \
462 self.__matchIndependentChecks(independentChecks, outputLines, startLineNo, varState)
463
David Brazdil9a6f20e2014-12-19 11:17:21 +0000464 # Run the Not checks against the output lines which lie between the last
465 # two independent groups or the bounds of the output.
David Brazdil2e15cd22014-12-31 17:28:38 +0000466 self.__matchNotLines(notChecks, notOutput, startLineNo, varState)
467
David Brazdil9a6f20e2014-12-19 11:17:21 +0000468 # Update variable state.
David Brazdil2e15cd22014-12-31 17:28:38 +0000469 startLineNo = newStartLineNo
David Brazdil9a6f20e2014-12-19 11:17:21 +0000470 varState = newVarState
David Brazdilee690a32014-12-01 17:04:16 +0000471
472class OutputGroup(CommonEqualityMixin):
473 """Represents a named part of the test output against which a check group of
474 the same name is to be matched."""
475
David Brazdil2e15cd22014-12-31 17:28:38 +0000476 def __init__(self, name, body, fileName=None, lineNo=-1):
477 if not name:
478 Logger.fail("Output group does not have a name", fileName, lineNo)
479 if not body:
480 Logger.fail("Output group does not have a body", fileName, lineNo)
481
482 self.name = name
483 self.body = body
484 self.lineNo = lineNo
485
486 def __eq__(self, other):
487 return (isinstance(other, self.__class__) and
488 self.name == other.name and
489 self.body == other.body)
David Brazdilee690a32014-12-01 17:04:16 +0000490
491
492class FileSplitMixin(object):
493 """Mixin for representing text files which need to be split into smaller
494 chunks before being parsed."""
495
496 def _parseStream(self, stream):
497 lineNo = 0
498 allGroups = []
499 currentGroup = None
500
501 for line in stream:
502 lineNo += 1
503 line = line.strip()
504 if not line:
505 continue
506
507 # Let the child class process the line and return information about it.
508 # The _processLine method can modify the content of the line (or delete it
509 # entirely) and specify whether it starts a new group.
510 processedLine, newGroupName = self._processLine(line, lineNo)
511 if newGroupName is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000512 currentGroup = (newGroupName, [], lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000513 allGroups.append(currentGroup)
514 if processedLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000515 if currentGroup is not None:
516 currentGroup[1].append(processedLine)
517 else:
518 self._exceptionLineOutsideGroup(line, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000519
520 # Finally, take the generated line groups and let the child class process
521 # each one before storing the final outcome.
David Brazdil2e15cd22014-12-31 17:28:38 +0000522 return list(map(lambda group: self._processGroup(group[0], group[1], group[2]), allGroups))
David Brazdilee690a32014-12-01 17:04:16 +0000523
524
525class CheckFile(FileSplitMixin):
526 """Collection of check groups extracted from the input test file."""
527
David Brazdil2e15cd22014-12-31 17:28:38 +0000528 def __init__(self, prefix, checkStream, fileName=None):
529 self.fileName = fileName
David Brazdilee690a32014-12-01 17:04:16 +0000530 self.prefix = prefix
531 self.groups = self._parseStream(checkStream)
532
533 # Attempts to parse a check line. The regex searches for a comment symbol
534 # followed by the CHECK keyword, given attribute and a colon at the very
535 # beginning of the line. Whitespaces are ignored.
536 def _extractLine(self, prefix, line):
David Brazdilbe0cc082014-12-31 11:49:30 +0000537 rIgnoreWhitespace = r"\s*"
538 rCommentSymbols = [r"//", r"#"]
539 regexPrefix = rIgnoreWhitespace + \
540 r"(" + r"|".join(rCommentSymbols) + r")" + \
541 rIgnoreWhitespace + \
542 prefix + r":"
David Brazdilee690a32014-12-01 17:04:16 +0000543
544 # The 'match' function succeeds only if the pattern is matched at the
545 # beginning of the line.
David Brazdilbe0cc082014-12-31 11:49:30 +0000546 match = re.match(regexPrefix, line)
David Brazdilee690a32014-12-01 17:04:16 +0000547 if match is not None:
548 return line[match.end():].strip()
549 else:
550 return None
551
552 def _processLine(self, line, lineNo):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000553 # Lines beginning with 'CHECK-START' start a new check group.
David Brazdilee690a32014-12-01 17:04:16 +0000554 startLine = self._extractLine(self.prefix + "-START", line)
555 if startLine is not None:
David Brazdil9a6f20e2014-12-19 11:17:21 +0000556 return None, startLine
557
558 # Lines starting only with 'CHECK' are matched in order.
559 plainLine = self._extractLine(self.prefix, line)
560 if plainLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000561 return (plainLine, CheckLine.Variant.InOrder, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000562
563 # 'CHECK-DAG' lines are no-order assertions.
564 dagLine = self._extractLine(self.prefix + "-DAG", line)
565 if dagLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000566 return (dagLine, CheckLine.Variant.DAG, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000567
568 # 'CHECK-NOT' lines are no-order negative assertions.
569 notLine = self._extractLine(self.prefix + "-NOT", line)
570 if notLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000571 return (notLine, CheckLine.Variant.Not, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000572
573 # Other lines are ignored.
574 return None, None
David Brazdilee690a32014-12-01 17:04:16 +0000575
576 def _exceptionLineOutsideGroup(self, line, lineNo):
David Brazdil2e15cd22014-12-31 17:28:38 +0000577 Logger.fail("Check line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000578
David Brazdil2e15cd22014-12-31 17:28:38 +0000579 def _processGroup(self, name, lines, lineNo):
580 checkLines = list(map(lambda line: CheckLine(line[0], line[1], self.fileName, line[2]), lines))
581 return CheckGroup(name, checkLines, self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000582
David Brazdil2e15cd22014-12-31 17:28:38 +0000583 def match(self, outputFile):
David Brazdilee690a32014-12-01 17:04:16 +0000584 for checkGroup in self.groups:
585 # TODO: Currently does not handle multiple occurrences of the same group
586 # name, e.g. when a pass is run multiple times. It will always try to
587 # match a check group against the first output group of the same name.
588 outputGroup = outputFile.findGroup(checkGroup.name)
589 if outputGroup is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000590 Logger.fail("Group \"" + checkGroup.name + "\" not found in the output",
591 self.fileName, checkGroup.lineNo)
592 Logger.startTest(checkGroup.name)
593 checkGroup.match(outputGroup)
594 Logger.testPassed()
David Brazdilee690a32014-12-01 17:04:16 +0000595
596
597class OutputFile(FileSplitMixin):
598 """Representation of the output generated by the test and split into groups
599 within which the checks are performed.
600
601 C1visualizer format is parsed with a state machine which differentiates
602 between the 'compilation' and 'cfg' blocks. The former marks the beginning
603 of a method. It is parsed for the method's name but otherwise ignored. Each
604 subsequent CFG block represents one stage of the compilation pipeline and
605 is parsed into an output group named "<method name> <pass name>".
606 """
607
608 class ParsingState:
609 OutsideBlock, InsideCompilationBlock, StartingCfgBlock, InsideCfgBlock = range(4)
610
David Brazdil2e15cd22014-12-31 17:28:38 +0000611 def __init__(self, outputStream, fileName=None):
612 self.fileName = fileName
613
David Brazdilee690a32014-12-01 17:04:16 +0000614 # Initialize the state machine
615 self.lastMethodName = None
616 self.state = OutputFile.ParsingState.OutsideBlock
617 self.groups = self._parseStream(outputStream)
618
619 def _processLine(self, line, lineNo):
620 if self.state == OutputFile.ParsingState.StartingCfgBlock:
621 # Previous line started a new 'cfg' block which means that this one must
622 # contain the name of the pass (this is enforced by C1visualizer).
623 if re.match("name\s+\"[^\"]+\"", line):
624 # Extract the pass name, prepend it with the name of the method and
625 # return as the beginning of a new group.
626 self.state = OutputFile.ParsingState.InsideCfgBlock
627 return (None, self.lastMethodName + " " + line.split("\"")[1])
628 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000629 Logger.fail("Expected output group name", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000630
631 elif self.state == OutputFile.ParsingState.InsideCfgBlock:
632 if line == "end_cfg":
633 self.state = OutputFile.ParsingState.OutsideBlock
634 return (None, None)
635 else:
636 return (line, None)
637
638 elif self.state == OutputFile.ParsingState.InsideCompilationBlock:
639 # Search for the method's name. Format: method "<name>"
David Brazdil2e15cd22014-12-31 17:28:38 +0000640 if re.match("method\s+\"[^\"]*\"", line):
641 methodName = line.split("\"")[1].strip()
642 if not methodName:
643 Logger.fail("Empty method name in output", self.fileName, lineNo)
644 self.lastMethodName = methodName
David Brazdilee690a32014-12-01 17:04:16 +0000645 elif line == "end_compilation":
646 self.state = OutputFile.ParsingState.OutsideBlock
647 return (None, None)
648
David Brazdil2e15cd22014-12-31 17:28:38 +0000649 else:
650 assert self.state == OutputFile.ParsingState.OutsideBlock
David Brazdilee690a32014-12-01 17:04:16 +0000651 if line == "begin_cfg":
652 # The line starts a new group but we'll wait until the next line from
653 # which we can extract the name of the pass.
654 if self.lastMethodName is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000655 Logger.fail("Expected method header", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000656 self.state = OutputFile.ParsingState.StartingCfgBlock
657 return (None, None)
658 elif line == "begin_compilation":
659 self.state = OutputFile.ParsingState.InsideCompilationBlock
660 return (None, None)
661 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000662 Logger.fail("Output line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000663
David Brazdil2e15cd22014-12-31 17:28:38 +0000664 def _processGroup(self, name, lines, lineNo):
665 return OutputGroup(name, lines, self.fileName, lineNo + 1)
David Brazdilee690a32014-12-01 17:04:16 +0000666
667 def findGroup(self, name):
668 for group in self.groups:
669 if group.name == name:
670 return group
671 return None
672
673
674def ParseArguments():
675 parser = argparse.ArgumentParser()
676 parser.add_argument("test_file", help="the source of the test with checking annotations")
677 parser.add_argument("--check-prefix", dest="check_prefix", default="CHECK", metavar="PREFIX",
678 help="prefix of checks in the test file (default: CHECK)")
679 parser.add_argument("--list-groups", dest="list_groups", action="store_true",
680 help="print a list of all groups found in the test output")
681 parser.add_argument("--dump-group", dest="dump_group", metavar="GROUP",
682 help="print the contents of an output group")
683 return parser.parse_args()
684
685
686class cd:
687 """Helper class which temporarily changes the working directory."""
688
689 def __init__(self, newPath):
690 self.newPath = newPath
691
692 def __enter__(self):
693 self.savedPath = os.getcwd()
694 os.chdir(self.newPath)
695
696 def __exit__(self, etype, value, traceback):
697 os.chdir(self.savedPath)
698
699
700def CompileTest(inputFile, tempFolder):
701 classFolder = tempFolder + "/classes"
702 dexFile = tempFolder + "/test.dex"
703 oatFile = tempFolder + "/test.oat"
704 outputFile = tempFolder + "/art.cfg"
705 os.makedirs(classFolder)
706
707 # Build a DEX from the source file. We pass "--no-optimize" to dx to avoid
708 # interference with its optimizations.
709 check_call(["javac", "-d", classFolder, inputFile])
710 check_call(["dx", "--dex", "--no-optimize", "--output=" + dexFile, classFolder])
711
712 # Run dex2oat and export the HGraph. The output is stored into ${PWD}/art.cfg.
713 with cd(tempFolder):
714 check_call(["dex2oat", "-j1", "--dump-passes", "--compiler-backend=Optimizing",
715 "--android-root=" + os.environ["ANDROID_HOST_OUT"],
716 "--boot-image=" + os.environ["ANDROID_HOST_OUT"] + "/framework/core-optimizing.art",
717 "--runtime-arg", "-Xnorelocate", "--dex-file=" + dexFile, "--oat-file=" + oatFile])
718
719 return outputFile
720
721
722def ListGroups(outputFilename):
723 outputFile = OutputFile(open(outputFilename, "r"))
724 for group in outputFile.groups:
David Brazdil2e15cd22014-12-31 17:28:38 +0000725 Logger.log(group.name)
David Brazdilee690a32014-12-01 17:04:16 +0000726
727
728def DumpGroup(outputFilename, groupName):
729 outputFile = OutputFile(open(outputFilename, "r"))
730 group = outputFile.findGroup(groupName)
731 if group:
David Brazdil2e15cd22014-12-31 17:28:38 +0000732 lineNo = group.lineNo
733 maxLineNo = lineNo + len(group.body)
734 lenLineNo = len(str(maxLineNo)) + 2
735 for line in group.body:
736 Logger.log((str(lineNo) + ":").ljust(lenLineNo) + line)
737 lineNo += 1
David Brazdilee690a32014-12-01 17:04:16 +0000738 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000739 Logger.fail("Group \"" + groupName + "\" not found in the output")
David Brazdilee690a32014-12-01 17:04:16 +0000740
741
742def RunChecks(checkPrefix, checkFilename, outputFilename):
David Brazdil2e15cd22014-12-31 17:28:38 +0000743 checkBaseName = os.path.basename(checkFilename)
744 outputBaseName = os.path.splitext(checkBaseName)[0] + ".cfg"
745
746 checkFile = CheckFile(checkPrefix, open(checkFilename, "r"), checkBaseName)
747 outputFile = OutputFile(open(outputFilename, "r"), outputBaseName)
748 checkFile.match(outputFile)
David Brazdilee690a32014-12-01 17:04:16 +0000749
750
751if __name__ == "__main__":
752 args = ParseArguments()
753 tempFolder = tempfile.mkdtemp()
754
755 try:
756 outputFile = CompileTest(args.test_file, tempFolder)
757 if args.list_groups:
758 ListGroups(outputFile)
759 elif args.dump_group:
760 DumpGroup(outputFile, args.dump_group)
761 else:
762 RunChecks(args.check_prefix, args.test_file, outputFile)
763 finally:
764 shutil.rmtree(tempFolder)