blob: a7cde62fe8a4446f7c7c40c52c8eb7ca9fb9677b [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
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 Brazdil21df8892015-01-08 01:49:53 +0000441 matchLineNo, matchVarState = \
David Brazdil2e15cd22014-12-31 17:28:38 +0000442 self.__findFirstMatch(checkLine, outputLines, startLineNo, [], varState)
David Brazdil21df8892015-01-08 01:49:53 +0000443 if matchVarState 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
David Brazdil48942de2015-01-07 21:19:50 +0000552 # This function is invoked on each line of the check file and returns a pair
553 # which instructs the parser how the line should be handled. If the line is to
554 # be included in the current check group, it is returned in the first value.
555 # If the line starts a new check group, the name of the group is returned in
556 # the second value.
David Brazdilee690a32014-12-01 17:04:16 +0000557 def _processLine(self, line, lineNo):
David Brazdil9a6f20e2014-12-19 11:17:21 +0000558 # Lines beginning with 'CHECK-START' start a new check group.
David Brazdilee690a32014-12-01 17:04:16 +0000559 startLine = self._extractLine(self.prefix + "-START", line)
560 if startLine is not None:
David Brazdil9a6f20e2014-12-19 11:17:21 +0000561 return None, startLine
562
563 # Lines starting only with 'CHECK' are matched in order.
564 plainLine = self._extractLine(self.prefix, line)
565 if plainLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000566 return (plainLine, CheckLine.Variant.InOrder, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000567
568 # 'CHECK-DAG' lines are no-order assertions.
569 dagLine = self._extractLine(self.prefix + "-DAG", line)
570 if dagLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000571 return (dagLine, CheckLine.Variant.DAG, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000572
573 # 'CHECK-NOT' lines are no-order negative assertions.
574 notLine = self._extractLine(self.prefix + "-NOT", line)
575 if notLine is not None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000576 return (notLine, CheckLine.Variant.Not, lineNo), None
David Brazdil9a6f20e2014-12-19 11:17:21 +0000577
578 # Other lines are ignored.
579 return None, None
David Brazdilee690a32014-12-01 17:04:16 +0000580
581 def _exceptionLineOutsideGroup(self, line, lineNo):
David Brazdil2e15cd22014-12-31 17:28:38 +0000582 Logger.fail("Check line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000583
David Brazdil48942de2015-01-07 21:19:50 +0000584 # Constructs a check group from the parser-collected check lines.
David Brazdil2e15cd22014-12-31 17:28:38 +0000585 def _processGroup(self, name, lines, lineNo):
586 checkLines = list(map(lambda line: CheckLine(line[0], line[1], self.fileName, line[2]), lines))
587 return CheckGroup(name, checkLines, self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000588
David Brazdil2e15cd22014-12-31 17:28:38 +0000589 def match(self, outputFile):
David Brazdilee690a32014-12-01 17:04:16 +0000590 for checkGroup in self.groups:
591 # TODO: Currently does not handle multiple occurrences of the same group
592 # name, e.g. when a pass is run multiple times. It will always try to
593 # match a check group against the first output group of the same name.
594 outputGroup = outputFile.findGroup(checkGroup.name)
595 if outputGroup is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000596 Logger.fail("Group \"" + checkGroup.name + "\" not found in the output",
597 self.fileName, checkGroup.lineNo)
598 Logger.startTest(checkGroup.name)
599 checkGroup.match(outputGroup)
600 Logger.testPassed()
David Brazdilee690a32014-12-01 17:04:16 +0000601
602
603class OutputFile(FileSplitMixin):
604 """Representation of the output generated by the test and split into groups
605 within which the checks are performed.
606
607 C1visualizer format is parsed with a state machine which differentiates
608 between the 'compilation' and 'cfg' blocks. The former marks the beginning
609 of a method. It is parsed for the method's name but otherwise ignored. Each
610 subsequent CFG block represents one stage of the compilation pipeline and
611 is parsed into an output group named "<method name> <pass name>".
612 """
613
614 class ParsingState:
615 OutsideBlock, InsideCompilationBlock, StartingCfgBlock, InsideCfgBlock = range(4)
616
David Brazdil2e15cd22014-12-31 17:28:38 +0000617 def __init__(self, outputStream, fileName=None):
618 self.fileName = fileName
619
David Brazdilee690a32014-12-01 17:04:16 +0000620 # Initialize the state machine
621 self.lastMethodName = None
622 self.state = OutputFile.ParsingState.OutsideBlock
623 self.groups = self._parseStream(outputStream)
624
David Brazdil48942de2015-01-07 21:19:50 +0000625 # This function is invoked on each line of the output file and returns a pair
626 # which instructs the parser how the line should be handled. If the line is to
627 # be included in the current group, it is returned in the first value. If the
628 # line starts a new output group, the name of the group is returned in the
629 # second value.
David Brazdilee690a32014-12-01 17:04:16 +0000630 def _processLine(self, line, lineNo):
631 if self.state == OutputFile.ParsingState.StartingCfgBlock:
632 # Previous line started a new 'cfg' block which means that this one must
633 # contain the name of the pass (this is enforced by C1visualizer).
634 if re.match("name\s+\"[^\"]+\"", line):
635 # Extract the pass name, prepend it with the name of the method and
636 # return as the beginning of a new group.
637 self.state = OutputFile.ParsingState.InsideCfgBlock
638 return (None, self.lastMethodName + " " + line.split("\"")[1])
639 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000640 Logger.fail("Expected output group name", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000641
642 elif self.state == OutputFile.ParsingState.InsideCfgBlock:
643 if line == "end_cfg":
644 self.state = OutputFile.ParsingState.OutsideBlock
645 return (None, None)
646 else:
647 return (line, None)
648
649 elif self.state == OutputFile.ParsingState.InsideCompilationBlock:
650 # Search for the method's name. Format: method "<name>"
David Brazdil2e15cd22014-12-31 17:28:38 +0000651 if re.match("method\s+\"[^\"]*\"", line):
652 methodName = line.split("\"")[1].strip()
653 if not methodName:
654 Logger.fail("Empty method name in output", self.fileName, lineNo)
655 self.lastMethodName = methodName
David Brazdilee690a32014-12-01 17:04:16 +0000656 elif line == "end_compilation":
657 self.state = OutputFile.ParsingState.OutsideBlock
658 return (None, None)
659
David Brazdil2e15cd22014-12-31 17:28:38 +0000660 else:
661 assert self.state == OutputFile.ParsingState.OutsideBlock
David Brazdilee690a32014-12-01 17:04:16 +0000662 if line == "begin_cfg":
663 # The line starts a new group but we'll wait until the next line from
664 # which we can extract the name of the pass.
665 if self.lastMethodName is None:
David Brazdil2e15cd22014-12-31 17:28:38 +0000666 Logger.fail("Expected method header", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000667 self.state = OutputFile.ParsingState.StartingCfgBlock
668 return (None, None)
669 elif line == "begin_compilation":
670 self.state = OutputFile.ParsingState.InsideCompilationBlock
671 return (None, None)
672 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000673 Logger.fail("Output line not inside a group", self.fileName, lineNo)
David Brazdilee690a32014-12-01 17:04:16 +0000674
David Brazdil48942de2015-01-07 21:19:50 +0000675 # Constructs an output group from the parser-collected output lines.
David Brazdil2e15cd22014-12-31 17:28:38 +0000676 def _processGroup(self, name, lines, lineNo):
677 return OutputGroup(name, lines, self.fileName, lineNo + 1)
David Brazdilee690a32014-12-01 17:04:16 +0000678
679 def findGroup(self, name):
680 for group in self.groups:
681 if group.name == name:
682 return group
683 return None
684
685
686def ParseArguments():
687 parser = argparse.ArgumentParser()
688 parser.add_argument("test_file", help="the source of the test with checking annotations")
689 parser.add_argument("--check-prefix", dest="check_prefix", default="CHECK", metavar="PREFIX",
690 help="prefix of checks in the test file (default: CHECK)")
691 parser.add_argument("--list-groups", dest="list_groups", action="store_true",
692 help="print a list of all groups found in the test output")
693 parser.add_argument("--dump-group", dest="dump_group", metavar="GROUP",
694 help="print the contents of an output group")
695 return parser.parse_args()
696
697
698class cd:
699 """Helper class which temporarily changes the working directory."""
700
701 def __init__(self, newPath):
702 self.newPath = newPath
703
704 def __enter__(self):
705 self.savedPath = os.getcwd()
706 os.chdir(self.newPath)
707
708 def __exit__(self, etype, value, traceback):
709 os.chdir(self.savedPath)
710
711
712def CompileTest(inputFile, tempFolder):
713 classFolder = tempFolder + "/classes"
714 dexFile = tempFolder + "/test.dex"
715 oatFile = tempFolder + "/test.oat"
David Brazdil866c0312015-01-13 21:21:31 +0000716 outputFile = tempFolder + "/test.cfg"
David Brazdilee690a32014-12-01 17:04:16 +0000717 os.makedirs(classFolder)
718
719 # Build a DEX from the source file. We pass "--no-optimize" to dx to avoid
720 # interference with its optimizations.
721 check_call(["javac", "-d", classFolder, inputFile])
722 check_call(["dx", "--dex", "--no-optimize", "--output=" + dexFile, classFolder])
723
724 # Run dex2oat and export the HGraph. The output is stored into ${PWD}/art.cfg.
725 with cd(tempFolder):
David Brazdil866c0312015-01-13 21:21:31 +0000726 check_call(["dex2oat", "-j1", "--dump-cfg=" + outputFile, "--compiler-backend=Optimizing",
David Brazdilee690a32014-12-01 17:04:16 +0000727 "--android-root=" + os.environ["ANDROID_HOST_OUT"],
728 "--boot-image=" + os.environ["ANDROID_HOST_OUT"] + "/framework/core-optimizing.art",
729 "--runtime-arg", "-Xnorelocate", "--dex-file=" + dexFile, "--oat-file=" + oatFile])
730
731 return outputFile
732
733
734def ListGroups(outputFilename):
735 outputFile = OutputFile(open(outputFilename, "r"))
736 for group in outputFile.groups:
David Brazdil2e15cd22014-12-31 17:28:38 +0000737 Logger.log(group.name)
David Brazdilee690a32014-12-01 17:04:16 +0000738
739
740def DumpGroup(outputFilename, groupName):
741 outputFile = OutputFile(open(outputFilename, "r"))
742 group = outputFile.findGroup(groupName)
743 if group:
David Brazdil2e15cd22014-12-31 17:28:38 +0000744 lineNo = group.lineNo
745 maxLineNo = lineNo + len(group.body)
746 lenLineNo = len(str(maxLineNo)) + 2
747 for line in group.body:
748 Logger.log((str(lineNo) + ":").ljust(lenLineNo) + line)
749 lineNo += 1
David Brazdilee690a32014-12-01 17:04:16 +0000750 else:
David Brazdil2e15cd22014-12-31 17:28:38 +0000751 Logger.fail("Group \"" + groupName + "\" not found in the output")
David Brazdilee690a32014-12-01 17:04:16 +0000752
753
754def RunChecks(checkPrefix, checkFilename, outputFilename):
David Brazdil2e15cd22014-12-31 17:28:38 +0000755 checkBaseName = os.path.basename(checkFilename)
756 outputBaseName = os.path.splitext(checkBaseName)[0] + ".cfg"
757
758 checkFile = CheckFile(checkPrefix, open(checkFilename, "r"), checkBaseName)
759 outputFile = OutputFile(open(outputFilename, "r"), outputBaseName)
760 checkFile.match(outputFile)
David Brazdilee690a32014-12-01 17:04:16 +0000761
762
763if __name__ == "__main__":
764 args = ParseArguments()
765 tempFolder = tempfile.mkdtemp()
766
767 try:
768 outputFile = CompileTest(args.test_file, tempFolder)
769 if args.list_groups:
770 ListGroups(outputFile)
771 elif args.dump_group:
772 DumpGroup(outputFile, args.dump_group)
773 else:
774 RunChecks(args.check_prefix, args.test_file, outputFile)
775 finally:
776 shutil.rmtree(tempFolder)