blob: 3d5ca45c80bbf262e74c7902413ed5f3e11cfaa6 [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.
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
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
162 def __init__(self, variant, name, pattern):
163 self.variant = variant
164 self.name = name
165 self.pattern = pattern
166
167 @staticmethod
168 def parseText(text):
169 return CheckElement(CheckElement.Variant.Text, None, re.escape(text))
170
171 @staticmethod
172 def parsePattern(patternElem):
173 return CheckElement(CheckElement.Variant.Pattern, None, patternElem[2:len(patternElem)-2])
174
175 @staticmethod
176 def parseVariable(varElem):
177 colonPos = varElem.find(":")
178 if colonPos == -1:
179 # Variable reference
180 name = varElem[2:len(varElem)-2]
181 return CheckElement(CheckElement.Variant.VarRef, name, None)
182 else:
183 # Variable definition
184 name = varElem[2:colonPos]
185 body = varElem[colonPos+1:len(varElem)-2]
186 return CheckElement(CheckElement.Variant.VarDef, name, body)
187
188
189class CheckLine(CommonEqualityMixin):
190 """Representation of a single assertion in the check file formed of one or
191 more regex elements. Matching against an output line is successful only
192 if all regex elements can be matched in the given order."""
193
David Brazdil9a6f20e2014-12-19 11:17:21 +0000194 class Variant(object):
195 """Supported types of assertions."""
196 InOrder, DAG, Not = range(3)
David Brazdilee690a32014-12-01 17:04:16 +0000197
David Brazdil2e15cd22014-12-31 17:28:38 +0000198 def __init__(self, content, variant=Variant.InOrder, fileName=None, lineNo=-1):
199 self.fileName = fileName
David Brazdilee690a32014-12-01 17:04:16 +0000200 self.lineNo = lineNo
David Brazdil2e15cd22014-12-31 17:28:38 +0000201 self.content = content.strip()
David Brazdilee690a32014-12-01 17:04:16 +0000202
David Brazdil2e15cd22014-12-31 17:28:38 +0000203 self.variant = variant
David Brazdil9a6f20e2014-12-19 11:17:21 +0000204 self.lineParts = self.__parse(self.content)
David Brazdilee690a32014-12-01 17:04:16 +0000205 if not self.lineParts:
David Brazdil2e15cd22014-12-31 17:28:38 +0000206 Logger.fail("Empty check line", self.fileName, self.lineNo)
207
208 if self.variant == CheckLine.Variant.Not:
209 for elem in self.lineParts:
210 if elem.variant == CheckElement.Variant.VarDef:
211 Logger.fail("CHECK-NOT lines cannot define variables", self.fileName, self.lineNo)
212
213 def __eq__(self, other):
214 return (isinstance(other, self.__class__) and
215 self.variant == other.variant and
216 self.lineParts == other.lineParts)
David Brazdilee690a32014-12-01 17:04:16 +0000217
218 # Returns True if the given Match object was at the beginning of the line.
219 def __isMatchAtStart(self, match):
220 return (match is not None) and (match.start() == 0)
221
222 # Takes in a list of Match objects and returns the minimal start point among
223 # them. If there aren't any successful matches it returns the length of
224 # the searched string.
225 def __firstMatch(self, matches, string):
226 starts = map(lambda m: len(string) if m is None else m.start(), matches)
227 return min(starts)
228
229 # Returns the regex for finding a regex pattern in the check line.
230 def __getPatternRegex(self):
231 rStartSym = "\{\{"
232 rEndSym = "\}\}"
233 rBody = ".+?"
234 return rStartSym + rBody + rEndSym
235
236 # Returns the regex for finding a variable use in the check line.
237 def __getVariableRegex(self):
238 rStartSym = "\[\["
239 rEndSym = "\]\]"
240 rStartOptional = "("
241 rEndOptional = ")?"
242 rName = "[a-zA-Z][a-zA-Z0-9]*"
243 rSeparator = ":"
244 rBody = ".+?"
245 return rStartSym + rName + rStartOptional + rSeparator + rBody + rEndOptional + rEndSym
246
247 # This method parses the content of a check line stripped of the initial
248 # comment symbol and the CHECK keyword.
249 def __parse(self, line):
250 lineParts = []
251 # Loop as long as there is something to parse.
252 while line:
253 # Search for the nearest occurrence of the special markers.
254 matchWhitespace = re.search("\s+", line)
255 matchPattern = re.search(self.__getPatternRegex(), line)
256 matchVariable = re.search(self.__getVariableRegex(), line)
257
258 # If one of the above was identified at the current position, extract them
259 # from the line, parse them and add to the list of line parts.
260 if self.__isMatchAtStart(matchWhitespace):
261 # We want to be whitespace-agnostic so whenever a check line contains
262 # a whitespace, we add a regex pattern for an arbitrary non-zero number
263 # of whitespaces.
264 line = line[matchWhitespace.end():]
265 lineParts.append(CheckElement.parsePattern("{{\s+}}"))
266 elif self.__isMatchAtStart(matchPattern):
267 pattern = line[0:matchPattern.end()]
268 line = line[matchPattern.end():]
269 lineParts.append(CheckElement.parsePattern(pattern))
270 elif self.__isMatchAtStart(matchVariable):
271 var = line[0:matchVariable.end()]
272 line = line[matchVariable.end():]
David Brazdil2e15cd22014-12-31 17:28:38 +0000273 lineParts.append(CheckElement.parseVariable(var))
David Brazdilee690a32014-12-01 17:04:16 +0000274 else:
275 # If we're not currently looking at a special marker, this is a plain
276 # text match all the way until the first special marker (or the end
277 # of the line).
278 firstMatch = self.__firstMatch([ matchWhitespace, matchPattern, matchVariable ], line)
279 text = line[0:firstMatch]
280 line = line[firstMatch:]
281 lineParts.append(CheckElement.parseText(text))
282 return lineParts
283
284 # Returns the regex pattern to be matched in the output line. Variable
285 # references are substituted with their current values provided in the
286 # 'varState' argument.
287 # An exception is raised if a referenced variable is undefined.
288 def __generatePattern(self, linePart, varState):
289 if linePart.variant == CheckElement.Variant.VarRef:
290 try:
291 return re.escape(varState[linePart.name])
292 except KeyError:
David Brazdil2e15cd22014-12-31 17:28:38 +0000293 Logger.testFailed("Use of undefined variable \"" + linePart.name + "\"",
294 self.fileName, self.lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000295 else:
296 return linePart.pattern
297
298 # Attempts to match the check line against a line from the output file with
299 # the given initial variable values. It returns the new variable state if
300 # successful and None otherwise.
301 def match(self, outputLine, initialVarState):
302 initialSearchFrom = 0
303 initialPattern = self.__generatePattern(self.lineParts[0], initialVarState)
304 while True:
305 # Search for the first element on the regex parts list. This will mark
306 # the point on the line from which we will attempt to match the rest of
307 # the check pattern. If this iteration produces only a partial match,
308 # the next iteration will start searching further in the output.
309 firstMatch = re.search(initialPattern, outputLine[initialSearchFrom:])
310 if firstMatch is None:
311 return None
312 matchStart = initialSearchFrom + firstMatch.start()
313 initialSearchFrom += firstMatch.start() + 1
314
315 # Do the full matching on a shadow copy of the variable state. If the
316 # matching fails half-way, we will not need to revert the state.
317 varState = dict(initialVarState)
318
319 # Now try to parse all of the parts of the check line in the right order.
320 # Variable values are updated on-the-fly, meaning that a variable can
321 # be referenced immediately after its definition.
322 fullyMatched = True
323 for part in self.lineParts:
324 pattern = self.__generatePattern(part, varState)
325 match = re.match(pattern, outputLine[matchStart:])
326 if match is None:
327 fullyMatched = False
328 break
329 matchEnd = matchStart + match.end()
330 if part.variant == CheckElement.Variant.VarDef:
331 if part.name in varState:
David Brazdil2e15cd22014-12-31 17:28:38 +0000332 Logger.testFailed("Multiple definitions of variable \"" + part.name + "\"",
333 self.fileName, self.lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000334 varState[part.name] = outputLine[matchStart:matchEnd]
335 matchStart = matchEnd
336
337 # Return the new variable state if all parts were successfully matched.
338 # Otherwise loop and try to find another start point on the same line.
339 if fullyMatched:
340 return varState
341
342
343class CheckGroup(CommonEqualityMixin):
344 """Represents a named collection of check lines which are to be matched
345 against an output group of the same name."""
346
David Brazdil2e15cd22014-12-31 17:28:38 +0000347 def __init__(self, name, lines, fileName=None, lineNo=-1):
348 self.fileName = fileName
349 self.lineNo = lineNo
350
351 if not name:
352 Logger.fail("Check group does not have a name", self.fileName, self.lineNo)
353 if not lines:
354 Logger.fail("Check group does not have a body", self.fileName, self.lineNo)
355
356 self.name = name
357 self.lines = lines
358
359 def __eq__(self, other):
360 return (isinstance(other, self.__class__) and
361 self.name == other.name and
362 self.lines == other.lines)
David Brazdilee690a32014-12-01 17:04:16 +0000363
364 def __headAndTail(self, list):
365 return list[0], list[1:]
366
David Brazdil9a6f20e2014-12-19 11:17:21 +0000367 # Splits a list of check lines at index 'i' such that lines[i] is the first
368 # element whose variant is not equal to the given parameter.
369 def __splitByVariant(self, lines, variant):
370 i = 0
371 while i < len(lines) and lines[i].variant == variant:
372 i += 1
373 return lines[:i], lines[i:]
David Brazdilee690a32014-12-01 17:04:16 +0000374
David Brazdil9a6f20e2014-12-19 11:17:21 +0000375 # Extracts the first sequence of check lines which are independent of each
376 # other's match location, i.e. either consecutive DAG lines or a single
377 # InOrder line. Any Not lines preceeding this sequence are also extracted.
378 def __nextIndependentChecks(self, checkLines):
379 notChecks, checkLines = self.__splitByVariant(checkLines, CheckLine.Variant.Not)
380 if not checkLines:
381 return notChecks, [], []
382
383 head, tail = self.__headAndTail(checkLines)
384 if head.variant == CheckLine.Variant.InOrder:
385 return notChecks, [head], tail
386 else:
387 assert head.variant == CheckLine.Variant.DAG
388 independentChecks, checkLines = self.__splitByVariant(checkLines, CheckLine.Variant.DAG)
389 return notChecks, independentChecks, checkLines
390
391 # If successful, returns the line number of the first output line matching the
392 # check line and the updated variable state. Otherwise returns -1 and None,
393 # respectively. The 'lineFilter' parameter can be used to supply a list of
394 # line numbers (counting from 1) which should be skipped.
David Brazdil2e15cd22014-12-31 17:28:38 +0000395 def __findFirstMatch(self, checkLine, outputLines, startLineNo, lineFilter, varState):
396 matchLineNo = startLineNo
David Brazdil9a6f20e2014-12-19 11:17:21 +0000397 for outputLine in outputLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000398 if matchLineNo not in lineFilter:
399 newVarState = checkLine.match(outputLine, varState)
400 if newVarState is not None:
401 return matchLineNo, newVarState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000402 matchLineNo += 1
David Brazdil9a6f20e2014-12-19 11:17:21 +0000403 return -1, None
404
405 # Matches the given positive check lines against the output in order of
406 # appearance. Variable state is propagated but the scope of the search remains
407 # the same for all checks. Each output line can only be matched once.
408 # If all check lines are matched, the resulting variable state is returned
409 # together with the remaining output. The function also returns output lines
410 # which appear before either of the matched lines so they can be tested
411 # against Not checks.
David Brazdil2e15cd22014-12-31 17:28:38 +0000412 def __matchIndependentChecks(self, checkLines, outputLines, startLineNo, varState):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000413 # If no checks are provided, skip over the entire output.
414 if not checkLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000415 return outputLines, [], startLineNo + len(outputLines), varState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000416
417 # Keep track of which lines have been matched.
418 matchedLines = []
419
420 # Find first unused output line which matches each check line.
421 for checkLine in checkLines:
David Brazdil2e15cd22014-12-31 17:28:38 +0000422 matchLineNo, varState = \
423 self.__findFirstMatch(checkLine, outputLines, startLineNo, matchedLines, varState)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000424 if varState is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000425 Logger.testFailed("Could not match check line \"" + checkLine.content + "\" " +
426 "starting from output line " + str(startLineNo),
427 self.fileName, checkLine.lineNo)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000428 matchedLines.append(matchLineNo)
429
430 # Return new variable state and the output lines which lie outside the
431 # match locations of this independent group.
David Brazdil2e15cd22014-12-31 17:28:38 +0000432 minMatchLineNo = min(matchedLines)
433 maxMatchLineNo = max(matchedLines)
434 preceedingLines = outputLines[:minMatchLineNo - startLineNo]
435 remainingLines = outputLines[maxMatchLineNo - startLineNo + 1:]
436 return preceedingLines, remainingLines, maxMatchLineNo + 1, varState
David Brazdil9a6f20e2014-12-19 11:17:21 +0000437
438 # Makes sure that the given check lines do not match any of the given output
439 # lines. Variable state does not change.
David Brazdil2e15cd22014-12-31 17:28:38 +0000440 def __matchNotLines(self, checkLines, outputLines, startLineNo, varState):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000441 for checkLine in checkLines:
442 assert checkLine.variant == CheckLine.Variant.Not
David Brazdil2e15cd22014-12-31 17:28:38 +0000443 matchLineNo, varState = \
444 self.__findFirstMatch(checkLine, outputLines, startLineNo, [], varState)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000445 if varState is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000446 Logger.testFailed("CHECK-NOT line \"" + checkLine.content + "\" matches output line " + \
447 str(matchLineNo), self.fileName, checkLine.lineNo)
David Brazdil9a6f20e2014-12-19 11:17:21 +0000448
449 # Matches the check lines in this group against an output group. It is
450 # responsible for running the checks in the right order and scope, and
451 # for propagating the variable state between the check lines.
452 def match(self, outputGroup):
453 varState = {}
David Brazdilee690a32014-12-01 17:04:16 +0000454 checkLines = self.lines
455 outputLines = outputGroup.body
David Brazdil2e15cd22014-12-31 17:28:38 +0000456 startLineNo = outputGroup.lineNo
David Brazdilee690a32014-12-01 17:04:16 +0000457
David Brazdilee690a32014-12-01 17:04:16 +0000458 while checkLines:
David Brazdil9a6f20e2014-12-19 11:17:21 +0000459 # Extract the next sequence of location-independent checks to be matched.
460 notChecks, independentChecks, checkLines = self.__nextIndependentChecks(checkLines)
David Brazdil2e15cd22014-12-31 17:28:38 +0000461
David Brazdil9a6f20e2014-12-19 11:17:21 +0000462 # Match the independent checks.
David Brazdil2e15cd22014-12-31 17:28:38 +0000463 notOutput, outputLines, newStartLineNo, newVarState = \
464 self.__matchIndependentChecks(independentChecks, outputLines, startLineNo, varState)
465
David Brazdil9a6f20e2014-12-19 11:17:21 +0000466 # Run the Not checks against the output lines which lie between the last
467 # two independent groups or the bounds of the output.
David Brazdil2e15cd22014-12-31 17:28:38 +0000468 self.__matchNotLines(notChecks, notOutput, startLineNo, varState)
469
David Brazdil9a6f20e2014-12-19 11:17:21 +0000470 # Update variable state.
David Brazdil2e15cd22014-12-31 17:28:38 +0000471 startLineNo = newStartLineNo
David Brazdil9a6f20e2014-12-19 11:17:21 +0000472 varState = newVarState
David Brazdilee690a32014-12-01 17:04:16 +0000473
474class OutputGroup(CommonEqualityMixin):
475 """Represents a named part of the test output against which a check group of
476 the same name is to be matched."""
477
David Brazdil2e15cd22014-12-31 17:28:38 +0000478 def __init__(self, name, body, fileName=None, lineNo=-1):
479 if not name:
480 Logger.fail("Output group does not have a name", fileName, lineNo)
481 if not body:
482 Logger.fail("Output group does not have a body", fileName, lineNo)
483
484 self.name = name
485 self.body = body
486 self.lineNo = lineNo
487
488 def __eq__(self, other):
489 return (isinstance(other, self.__class__) and
490 self.name == other.name and
491 self.body == other.body)
David Brazdilee690a32014-12-01 17:04:16 +0000492
493
494class FileSplitMixin(object):
495 """Mixin for representing text files which need to be split into smaller
496 chunks before being parsed."""
497
498 def _parseStream(self, stream):
499 lineNo = 0
500 allGroups = []
501 currentGroup = None
502
503 for line in stream:
504 lineNo += 1
505 line = line.strip()
506 if not line:
507 continue
508
509 # Let the child class process the line and return information about it.
510 # The _processLine method can modify the content of the line (or delete it
511 # entirely) and specify whether it starts a new group.
512 processedLine, newGroupName = self._processLine(line, lineNo)
513 if newGroupName is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000514 currentGroup = (newGroupName, [], lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000515 allGroups.append(currentGroup)
516 if processedLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000517 if currentGroup is not None:
518 currentGroup[1].append(processedLine)
519 else:
520 self._exceptionLineOutsideGroup(line, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000521
522 # Finally, take the generated line groups and let the child class process
523 # each one before storing the final outcome.
David Brazdil2e15cd22014-12-31 17:28:38 +0000524 return list(map(lambda group: self._processGroup(group[0], group[1], group[2]), allGroups))
David Brazdilee690a32014-12-01 17:04:16 +0000525
526
527class CheckFile(FileSplitMixin):
528 """Collection of check groups extracted from the input test file."""
529
David Brazdil2e15cd22014-12-31 17:28:38 +0000530 def __init__(self, prefix, checkStream, fileName=None):
531 self.fileName = fileName
David Brazdilee690a32014-12-01 17:04:16 +0000532 self.prefix = prefix
533 self.groups = self._parseStream(checkStream)
534
535 # Attempts to parse a check line. The regex searches for a comment symbol
536 # followed by the CHECK keyword, given attribute and a colon at the very
537 # beginning of the line. Whitespaces are ignored.
538 def _extractLine(self, prefix, line):
539 ignoreWhitespace = "\s*"
540 commentSymbols = ["//", "#"]
541 prefixRegex = ignoreWhitespace + \
542 "(" + "|".join(commentSymbols) + ")" + \
543 ignoreWhitespace + \
544 prefix + ":"
545
546 # The 'match' function succeeds only if the pattern is matched at the
547 # beginning of the line.
548 match = re.match(prefixRegex, line)
549 if match is not None:
550 return line[match.end():].strip()
551 else:
552 return None
553
David Brazdil48942de2015-01-07 21:19:50 +0000554 # This function is invoked on each line of the check file and returns a pair
555 # which instructs the parser how the line should be handled. If the line is to
556 # be included in the current check group, it is returned in the first value.
557 # If the line starts a new check group, the name of the group is returned in
558 # the second value.
David Brazdilee690a32014-12-01 17:04:16 +0000559 def _processLine(self, line, lineNo):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000560 # Lines beginning with 'CHECK-START' start a new check group.
David Brazdilee690a32014-12-01 17:04:16 +0000561 startLine = self._extractLine(self.prefix + "-START", line)
562 if startLine is not None:
David Brazdil9a6f20e2014-12-19 11:17:21 +0000563 return None, startLine
564
565 # Lines starting only with 'CHECK' are matched in order.
566 plainLine = self._extractLine(self.prefix, line)
567 if plainLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000568 return (plainLine, CheckLine.Variant.InOrder, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000569
570 # 'CHECK-DAG' lines are no-order assertions.
571 dagLine = self._extractLine(self.prefix + "-DAG", line)
572 if dagLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000573 return (dagLine, CheckLine.Variant.DAG, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000574
575 # 'CHECK-NOT' lines are no-order negative assertions.
576 notLine = self._extractLine(self.prefix + "-NOT", line)
577 if notLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000578 return (notLine, CheckLine.Variant.Not, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000579
580 # Other lines are ignored.
581 return None, None
David Brazdilee690a32014-12-01 17:04:16 +0000582
583 def _exceptionLineOutsideGroup(self, line, lineNo):
David Brazdil2e15cd22014-12-31 17:28:38 +0000584 Logger.fail("Check line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000585
David Brazdil48942de2015-01-07 21:19:50 +0000586 # Constructs a check group from the parser-collected check lines.
David Brazdil2e15cd22014-12-31 17:28:38 +0000587 def _processGroup(self, name, lines, lineNo):
588 checkLines = list(map(lambda line: CheckLine(line[0], line[1], self.fileName, line[2]), lines))
589 return CheckGroup(name, checkLines, self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000590
David Brazdil2e15cd22014-12-31 17:28:38 +0000591 def match(self, outputFile):
David Brazdilee690a32014-12-01 17:04:16 +0000592 for checkGroup in self.groups:
593 # TODO: Currently does not handle multiple occurrences of the same group
594 # name, e.g. when a pass is run multiple times. It will always try to
595 # match a check group against the first output group of the same name.
596 outputGroup = outputFile.findGroup(checkGroup.name)
597 if outputGroup is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000598 Logger.fail("Group \"" + checkGroup.name + "\" not found in the output",
599 self.fileName, checkGroup.lineNo)
600 Logger.startTest(checkGroup.name)
601 checkGroup.match(outputGroup)
602 Logger.testPassed()
David Brazdilee690a32014-12-01 17:04:16 +0000603
604
605class OutputFile(FileSplitMixin):
606 """Representation of the output generated by the test and split into groups
607 within which the checks are performed.
608
609 C1visualizer format is parsed with a state machine which differentiates
610 between the 'compilation' and 'cfg' blocks. The former marks the beginning
611 of a method. It is parsed for the method's name but otherwise ignored. Each
612 subsequent CFG block represents one stage of the compilation pipeline and
613 is parsed into an output group named "<method name> <pass name>".
614 """
615
616 class ParsingState:
617 OutsideBlock, InsideCompilationBlock, StartingCfgBlock, InsideCfgBlock = range(4)
618
David Brazdil2e15cd22014-12-31 17:28:38 +0000619 def __init__(self, outputStream, fileName=None):
620 self.fileName = fileName
621
David Brazdilee690a32014-12-01 17:04:16 +0000622 # Initialize the state machine
623 self.lastMethodName = None
624 self.state = OutputFile.ParsingState.OutsideBlock
625 self.groups = self._parseStream(outputStream)
626
David Brazdil48942de2015-01-07 21:19:50 +0000627 # This function is invoked on each line of the output file and returns a pair
628 # which instructs the parser how the line should be handled. If the line is to
629 # be included in the current group, it is returned in the first value. If the
630 # line starts a new output group, the name of the group is returned in the
631 # second value.
David Brazdilee690a32014-12-01 17:04:16 +0000632 def _processLine(self, line, lineNo):
633 if self.state == OutputFile.ParsingState.StartingCfgBlock:
634 # Previous line started a new 'cfg' block which means that this one must
635 # contain the name of the pass (this is enforced by C1visualizer).
636 if re.match("name\s+\"[^\"]+\"", line):
637 # Extract the pass name, prepend it with the name of the method and
638 # return as the beginning of a new group.
639 self.state = OutputFile.ParsingState.InsideCfgBlock
640 return (None, self.lastMethodName + " " + line.split("\"")[1])
641 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000642 Logger.fail("Expected output group name", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000643
644 elif self.state == OutputFile.ParsingState.InsideCfgBlock:
645 if line == "end_cfg":
646 self.state = OutputFile.ParsingState.OutsideBlock
647 return (None, None)
648 else:
649 return (line, None)
650
651 elif self.state == OutputFile.ParsingState.InsideCompilationBlock:
652 # Search for the method's name. Format: method "<name>"
David Brazdil2e15cd22014-12-31 17:28:38 +0000653 if re.match("method\s+\"[^\"]*\"", line):
654 methodName = line.split("\"")[1].strip()
655 if not methodName:
656 Logger.fail("Empty method name in output", self.fileName, lineNo)
657 self.lastMethodName = methodName
David Brazdilee690a32014-12-01 17:04:16 +0000658 elif line == "end_compilation":
659 self.state = OutputFile.ParsingState.OutsideBlock
660 return (None, None)
661
David Brazdil2e15cd22014-12-31 17:28:38 +0000662 else:
663 assert self.state == OutputFile.ParsingState.OutsideBlock
David Brazdilee690a32014-12-01 17:04:16 +0000664 if line == "begin_cfg":
665 # The line starts a new group but we'll wait until the next line from
666 # which we can extract the name of the pass.
667 if self.lastMethodName is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000668 Logger.fail("Expected method header", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000669 self.state = OutputFile.ParsingState.StartingCfgBlock
670 return (None, None)
671 elif line == "begin_compilation":
672 self.state = OutputFile.ParsingState.InsideCompilationBlock
673 return (None, None)
674 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000675 Logger.fail("Output line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000676
David Brazdil48942de2015-01-07 21:19:50 +0000677 # Constructs an output group from the parser-collected output lines.
David Brazdil2e15cd22014-12-31 17:28:38 +0000678 def _processGroup(self, name, lines, lineNo):
679 return OutputGroup(name, lines, self.fileName, lineNo + 1)
David Brazdilee690a32014-12-01 17:04:16 +0000680
681 def findGroup(self, name):
682 for group in self.groups:
683 if group.name == name:
684 return group
685 return None
686
687
688def ParseArguments():
689 parser = argparse.ArgumentParser()
690 parser.add_argument("test_file", help="the source of the test with checking annotations")
691 parser.add_argument("--check-prefix", dest="check_prefix", default="CHECK", metavar="PREFIX",
692 help="prefix of checks in the test file (default: CHECK)")
693 parser.add_argument("--list-groups", dest="list_groups", action="store_true",
694 help="print a list of all groups found in the test output")
695 parser.add_argument("--dump-group", dest="dump_group", metavar="GROUP",
696 help="print the contents of an output group")
697 return parser.parse_args()
698
699
700class cd:
701 """Helper class which temporarily changes the working directory."""
702
703 def __init__(self, newPath):
704 self.newPath = newPath
705
706 def __enter__(self):
707 self.savedPath = os.getcwd()
708 os.chdir(self.newPath)
709
710 def __exit__(self, etype, value, traceback):
711 os.chdir(self.savedPath)
712
713
714def CompileTest(inputFile, tempFolder):
715 classFolder = tempFolder + "/classes"
716 dexFile = tempFolder + "/test.dex"
717 oatFile = tempFolder + "/test.oat"
718 outputFile = tempFolder + "/art.cfg"
719 os.makedirs(classFolder)
720
721 # Build a DEX from the source file. We pass "--no-optimize" to dx to avoid
722 # interference with its optimizations.
723 check_call(["javac", "-d", classFolder, inputFile])
724 check_call(["dx", "--dex", "--no-optimize", "--output=" + dexFile, classFolder])
725
726 # Run dex2oat and export the HGraph. The output is stored into ${PWD}/art.cfg.
727 with cd(tempFolder):
728 check_call(["dex2oat", "-j1", "--dump-passes", "--compiler-backend=Optimizing",
729 "--android-root=" + os.environ["ANDROID_HOST_OUT"],
730 "--boot-image=" + os.environ["ANDROID_HOST_OUT"] + "/framework/core-optimizing.art",
731 "--runtime-arg", "-Xnorelocate", "--dex-file=" + dexFile, "--oat-file=" + oatFile])
732
733 return outputFile
734
735
736def ListGroups(outputFilename):
737 outputFile = OutputFile(open(outputFilename, "r"))
738 for group in outputFile.groups:
David Brazdil2e15cd22014-12-31 17:28:38 +0000739 Logger.log(group.name)
David Brazdilee690a32014-12-01 17:04:16 +0000740
741
742def DumpGroup(outputFilename, groupName):
743 outputFile = OutputFile(open(outputFilename, "r"))
744 group = outputFile.findGroup(groupName)
745 if group:
David Brazdil2e15cd22014-12-31 17:28:38 +0000746 lineNo = group.lineNo
747 maxLineNo = lineNo + len(group.body)
748 lenLineNo = len(str(maxLineNo)) + 2
749 for line in group.body:
750 Logger.log((str(lineNo) + ":").ljust(lenLineNo) + line)
751 lineNo += 1
David Brazdilee690a32014-12-01 17:04:16 +0000752 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000753 Logger.fail("Group \"" + groupName + "\" not found in the output")
David Brazdilee690a32014-12-01 17:04:16 +0000754
755
756def RunChecks(checkPrefix, checkFilename, outputFilename):
David Brazdil2e15cd22014-12-31 17:28:38 +0000757 checkBaseName = os.path.basename(checkFilename)
758 outputBaseName = os.path.splitext(checkBaseName)[0] + ".cfg"
759
760 checkFile = CheckFile(checkPrefix, open(checkFilename, "r"), checkBaseName)
761 outputFile = OutputFile(open(outputFilename, "r"), outputBaseName)
762 checkFile.match(outputFile)
David Brazdilee690a32014-12-01 17:04:16 +0000763
764
765if __name__ == "__main__":
766 args = ParseArguments()
767 tempFolder = tempfile.mkdtemp()
768
769 try:
770 outputFile = CompileTest(args.test_file, tempFolder)
771 if args.list_groups:
772 ListGroups(outputFile)
773 elif args.dump_group:
774 DumpGroup(outputFile, args.dump_group)
775 else:
776 RunChecks(args.check_prefix, args.test_file, outputFile)
777 finally:
778 shutil.rmtree(tempFolder)