blob: 0813d0c33562b6022579b86fc7d34ff5530e2afb [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
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 Brazdil21df8892015-01-08 01:49:53 +0000443 matchLineNo, matchVarState = \
David Brazdil2e15cd22014-12-31 17:28:38 +0000444 self.__findFirstMatch(checkLine, outputLines, startLineNo, [], varState)
David Brazdil21df8892015-01-08 01:49:53 +0000445 if matchVarState 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
554 def _processLine(self, line, lineNo):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000555 # Lines beginning with 'CHECK-START' start a new check group.
David Brazdilee690a32014-12-01 17:04:16 +0000556 startLine = self._extractLine(self.prefix + "-START", line)
557 if startLine is not None:
David Brazdil9a6f20e2014-12-19 11:17:21 +0000558 return None, startLine
559
560 # Lines starting only with 'CHECK' are matched in order.
561 plainLine = self._extractLine(self.prefix, line)
562 if plainLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000563 return (plainLine, CheckLine.Variant.InOrder, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000564
565 # 'CHECK-DAG' lines are no-order assertions.
566 dagLine = self._extractLine(self.prefix + "-DAG", line)
567 if dagLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000568 return (dagLine, CheckLine.Variant.DAG, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000569
570 # 'CHECK-NOT' lines are no-order negative assertions.
571 notLine = self._extractLine(self.prefix + "-NOT", line)
572 if notLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000573 return (notLine, CheckLine.Variant.Not, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000574
575 # Other lines are ignored.
576 return None, None
David Brazdilee690a32014-12-01 17:04:16 +0000577
578 def _exceptionLineOutsideGroup(self, line, lineNo):
David Brazdil2e15cd22014-12-31 17:28:38 +0000579 Logger.fail("Check line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000580
David Brazdil2e15cd22014-12-31 17:28:38 +0000581 def _processGroup(self, name, lines, lineNo):
582 checkLines = list(map(lambda line: CheckLine(line[0], line[1], self.fileName, line[2]), lines))
583 return CheckGroup(name, checkLines, self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000584
David Brazdil2e15cd22014-12-31 17:28:38 +0000585 def match(self, outputFile):
David Brazdilee690a32014-12-01 17:04:16 +0000586 for checkGroup in self.groups:
587 # TODO: Currently does not handle multiple occurrences of the same group
588 # name, e.g. when a pass is run multiple times. It will always try to
589 # match a check group against the first output group of the same name.
590 outputGroup = outputFile.findGroup(checkGroup.name)
591 if outputGroup is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000592 Logger.fail("Group \"" + checkGroup.name + "\" not found in the output",
593 self.fileName, checkGroup.lineNo)
594 Logger.startTest(checkGroup.name)
595 checkGroup.match(outputGroup)
596 Logger.testPassed()
David Brazdilee690a32014-12-01 17:04:16 +0000597
598
599class OutputFile(FileSplitMixin):
600 """Representation of the output generated by the test and split into groups
601 within which the checks are performed.
602
603 C1visualizer format is parsed with a state machine which differentiates
604 between the 'compilation' and 'cfg' blocks. The former marks the beginning
605 of a method. It is parsed for the method's name but otherwise ignored. Each
606 subsequent CFG block represents one stage of the compilation pipeline and
607 is parsed into an output group named "<method name> <pass name>".
608 """
609
610 class ParsingState:
611 OutsideBlock, InsideCompilationBlock, StartingCfgBlock, InsideCfgBlock = range(4)
612
David Brazdil2e15cd22014-12-31 17:28:38 +0000613 def __init__(self, outputStream, fileName=None):
614 self.fileName = fileName
615
David Brazdilee690a32014-12-01 17:04:16 +0000616 # Initialize the state machine
617 self.lastMethodName = None
618 self.state = OutputFile.ParsingState.OutsideBlock
619 self.groups = self._parseStream(outputStream)
620
621 def _processLine(self, line, lineNo):
622 if self.state == OutputFile.ParsingState.StartingCfgBlock:
623 # Previous line started a new 'cfg' block which means that this one must
624 # contain the name of the pass (this is enforced by C1visualizer).
625 if re.match("name\s+\"[^\"]+\"", line):
626 # Extract the pass name, prepend it with the name of the method and
627 # return as the beginning of a new group.
628 self.state = OutputFile.ParsingState.InsideCfgBlock
629 return (None, self.lastMethodName + " " + line.split("\"")[1])
630 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000631 Logger.fail("Expected output group name", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000632
633 elif self.state == OutputFile.ParsingState.InsideCfgBlock:
634 if line == "end_cfg":
635 self.state = OutputFile.ParsingState.OutsideBlock
636 return (None, None)
637 else:
638 return (line, None)
639
640 elif self.state == OutputFile.ParsingState.InsideCompilationBlock:
641 # Search for the method's name. Format: method "<name>"
David Brazdil2e15cd22014-12-31 17:28:38 +0000642 if re.match("method\s+\"[^\"]*\"", line):
643 methodName = line.split("\"")[1].strip()
644 if not methodName:
645 Logger.fail("Empty method name in output", self.fileName, lineNo)
646 self.lastMethodName = methodName
David Brazdilee690a32014-12-01 17:04:16 +0000647 elif line == "end_compilation":
648 self.state = OutputFile.ParsingState.OutsideBlock
649 return (None, None)
650
David Brazdil2e15cd22014-12-31 17:28:38 +0000651 else:
652 assert self.state == OutputFile.ParsingState.OutsideBlock
David Brazdilee690a32014-12-01 17:04:16 +0000653 if line == "begin_cfg":
654 # The line starts a new group but we'll wait until the next line from
655 # which we can extract the name of the pass.
656 if self.lastMethodName is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000657 Logger.fail("Expected method header", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000658 self.state = OutputFile.ParsingState.StartingCfgBlock
659 return (None, None)
660 elif line == "begin_compilation":
661 self.state = OutputFile.ParsingState.InsideCompilationBlock
662 return (None, None)
663 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000664 Logger.fail("Output line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000665
David Brazdil2e15cd22014-12-31 17:28:38 +0000666 def _processGroup(self, name, lines, lineNo):
667 return OutputGroup(name, lines, self.fileName, lineNo + 1)
David Brazdilee690a32014-12-01 17:04:16 +0000668
669 def findGroup(self, name):
670 for group in self.groups:
671 if group.name == name:
672 return group
673 return None
674
675
676def ParseArguments():
677 parser = argparse.ArgumentParser()
678 parser.add_argument("test_file", help="the source of the test with checking annotations")
679 parser.add_argument("--check-prefix", dest="check_prefix", default="CHECK", metavar="PREFIX",
680 help="prefix of checks in the test file (default: CHECK)")
681 parser.add_argument("--list-groups", dest="list_groups", action="store_true",
682 help="print a list of all groups found in the test output")
683 parser.add_argument("--dump-group", dest="dump_group", metavar="GROUP",
684 help="print the contents of an output group")
685 return parser.parse_args()
686
687
688class cd:
689 """Helper class which temporarily changes the working directory."""
690
691 def __init__(self, newPath):
692 self.newPath = newPath
693
694 def __enter__(self):
695 self.savedPath = os.getcwd()
696 os.chdir(self.newPath)
697
698 def __exit__(self, etype, value, traceback):
699 os.chdir(self.savedPath)
700
701
702def CompileTest(inputFile, tempFolder):
703 classFolder = tempFolder + "/classes"
704 dexFile = tempFolder + "/test.dex"
705 oatFile = tempFolder + "/test.oat"
706 outputFile = tempFolder + "/art.cfg"
707 os.makedirs(classFolder)
708
709 # Build a DEX from the source file. We pass "--no-optimize" to dx to avoid
710 # interference with its optimizations.
711 check_call(["javac", "-d", classFolder, inputFile])
712 check_call(["dx", "--dex", "--no-optimize", "--output=" + dexFile, classFolder])
713
714 # Run dex2oat and export the HGraph. The output is stored into ${PWD}/art.cfg.
715 with cd(tempFolder):
716 check_call(["dex2oat", "-j1", "--dump-passes", "--compiler-backend=Optimizing",
717 "--android-root=" + os.environ["ANDROID_HOST_OUT"],
718 "--boot-image=" + os.environ["ANDROID_HOST_OUT"] + "/framework/core-optimizing.art",
719 "--runtime-arg", "-Xnorelocate", "--dex-file=" + dexFile, "--oat-file=" + oatFile])
720
721 return outputFile
722
723
724def ListGroups(outputFilename):
725 outputFile = OutputFile(open(outputFilename, "r"))
726 for group in outputFile.groups:
David Brazdil2e15cd22014-12-31 17:28:38 +0000727 Logger.log(group.name)
David Brazdilee690a32014-12-01 17:04:16 +0000728
729
730def DumpGroup(outputFilename, groupName):
731 outputFile = OutputFile(open(outputFilename, "r"))
732 group = outputFile.findGroup(groupName)
733 if group:
David Brazdil2e15cd22014-12-31 17:28:38 +0000734 lineNo = group.lineNo
735 maxLineNo = lineNo + len(group.body)
736 lenLineNo = len(str(maxLineNo)) + 2
737 for line in group.body:
738 Logger.log((str(lineNo) + ":").ljust(lenLineNo) + line)
739 lineNo += 1
David Brazdilee690a32014-12-01 17:04:16 +0000740 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000741 Logger.fail("Group \"" + groupName + "\" not found in the output")
David Brazdilee690a32014-12-01 17:04:16 +0000742
743
744def RunChecks(checkPrefix, checkFilename, outputFilename):
David Brazdil2e15cd22014-12-31 17:28:38 +0000745 checkBaseName = os.path.basename(checkFilename)
746 outputBaseName = os.path.splitext(checkBaseName)[0] + ".cfg"
747
748 checkFile = CheckFile(checkPrefix, open(checkFilename, "r"), checkBaseName)
749 outputFile = OutputFile(open(outputFilename, "r"), outputBaseName)
750 checkFile.match(outputFile)
David Brazdilee690a32014-12-01 17:04:16 +0000751
752
753if __name__ == "__main__":
754 args = ParseArguments()
755 tempFolder = tempfile.mkdtemp()
756
757 try:
758 outputFile = CompileTest(args.test_file, tempFolder)
759 if args.list_groups:
760 ListGroups(outputFile)
761 elif args.dump_group:
762 DumpGroup(outputFile, args.dump_group)
763 else:
764 RunChecks(args.check_prefix, args.test_file, outputFile)
765 finally:
766 shutil.rmtree(tempFolder)