1#!/usr/bin/env python2 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 23# 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. 26# 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# 33# 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 appears 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 50# 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 74from __future__ import print_function 75import argparse 76import os 77import re 78import shutil 79import sys 80import tempfile 81 82class Logger(object): 83 84 class Level(object): 85 NoOutput, Error, Info = range(3) 86 87 class Color(object): 88 Default, Blue, Gray, Purple, Red = range(5) 89 90 @staticmethod 91 def terminalCode(color, out=sys.stdout): 92 if not out.isatty(): 93 return '' 94 elif color == Logger.Color.Blue: 95 return '\033[94m' 96 elif color == Logger.Color.Gray: 97 return '\033[37m' 98 elif color == Logger.Color.Purple: 99 return '\033[95m' 100 elif color == Logger.Color.Red: 101 return '\033[91m' 102 else: 103 return '\033[0m' 104 105 Verbosity = Level.Info 106 107 @staticmethod 108 def log(text, level=Level.Info, color=Color.Default, newLine=True, out=sys.stdout): 109 if level <= Logger.Verbosity: 110 text = Logger.Color.terminalCode(color, out) + text + \ 111 Logger.Color.terminalCode(Logger.Color.Default, out) 112 if newLine: 113 print(text, file=out) 114 else: 115 print(text, end="", file=out) 116 out.flush() 117 118 @staticmethod 119 def fail(msg, file=None, line=-1): 120 location = "" 121 if file: 122 location += file + ":" 123 if line > 0: 124 location += str(line) + ":" 125 if location: 126 location += " " 127 128 Logger.log(location, Logger.Level.Error, color=Logger.Color.Gray, newLine=False, out=sys.stderr) 129 Logger.log("error: ", Logger.Level.Error, color=Logger.Color.Red, newLine=False, out=sys.stderr) 130 Logger.log(msg, Logger.Level.Error, out=sys.stderr) 131 sys.exit(msg) 132 133 @staticmethod 134 def startTest(name): 135 Logger.log("TEST ", color=Logger.Color.Purple, newLine=False) 136 Logger.log(name + "... ", newLine=False) 137 138 @staticmethod 139 def testPassed(): 140 Logger.log("PASS", color=Logger.Color.Blue) 141 142 @staticmethod 143 def testFailed(msg, file=None, line=-1): 144 Logger.log("FAIL", color=Logger.Color.Red) 145 Logger.fail(msg, file, line) 146 147class CommonEqualityMixin: 148 """Mixin for class equality as equality of the fields.""" 149 def __eq__(self, other): 150 return (isinstance(other, self.__class__) 151 and self.__dict__ == other.__dict__) 152 153 def __ne__(self, other): 154 return not self.__eq__(other) 155 156 def __repr__(self): 157 return "<%s: %s>" % (type(self).__name__, str(self.__dict__)) 158 159 160class CheckElement(CommonEqualityMixin): 161 """Single element of the check line.""" 162 163 class Variant(object): 164 """Supported language constructs.""" 165 Text, Pattern, VarRef, VarDef, Separator = range(5) 166 167 rStartOptional = r"(" 168 rEndOptional = r")?" 169 170 rName = r"([a-zA-Z][a-zA-Z0-9]*)" 171 rRegex = r"(.+?)" 172 rPatternStartSym = r"(\{\{)" 173 rPatternEndSym = r"(\}\})" 174 rVariableStartSym = r"(\[\[)" 175 rVariableEndSym = r"(\]\])" 176 rVariableSeparator = r"(:)" 177 178 regexPattern = rPatternStartSym + rRegex + rPatternEndSym 179 regexVariable = rVariableStartSym + \ 180 rName + \ 181 (rStartOptional + rVariableSeparator + rRegex + rEndOptional) + \ 182 rVariableEndSym 183 184 def __init__(self, variant, name, pattern): 185 self.variant = variant 186 self.name = name 187 self.pattern = pattern 188 189 @staticmethod 190 def newSeparator(): 191 return CheckElement(CheckElement.Variant.Separator, None, None) 192 193 @staticmethod 194 def parseText(text): 195 return CheckElement(CheckElement.Variant.Text, None, re.escape(text)) 196 197 @staticmethod 198 def parsePattern(patternElem): 199 return CheckElement(CheckElement.Variant.Pattern, None, patternElem[2:-2]) 200 201 @staticmethod 202 def parseVariable(varElem): 203 colonPos = varElem.find(":") 204 if colonPos == -1: 205 # Variable reference 206 name = varElem[2:-2] 207 return CheckElement(CheckElement.Variant.VarRef, name, None) 208 else: 209 # Variable definition 210 name = varElem[2:colonPos] 211 body = varElem[colonPos+1:-2] 212 return CheckElement(CheckElement.Variant.VarDef, name, body) 213 214class CheckLine(CommonEqualityMixin): 215 """Representation of a single assertion in the check file formed of one or 216 more regex elements. Matching against an output line is successful only 217 if all regex elements can be matched in the given order.""" 218 219 class Variant(object): 220 """Supported types of assertions.""" 221 InOrder, DAG, Not = range(3) 222 223 def __init__(self, content, variant=Variant.InOrder, fileName=None, lineNo=-1): 224 self.fileName = fileName 225 self.lineNo = lineNo 226 self.content = content.strip() 227 228 self.variant = variant 229 self.lineParts = self.__parse(self.content) 230 if not self.lineParts: 231 Logger.fail("Empty check line", self.fileName, self.lineNo) 232 233 if self.variant == CheckLine.Variant.Not: 234 for elem in self.lineParts: 235 if elem.variant == CheckElement.Variant.VarDef: 236 Logger.fail("CHECK-NOT lines cannot define variables", self.fileName, self.lineNo) 237 238 def __eq__(self, other): 239 return (isinstance(other, self.__class__) and 240 self.variant == other.variant and 241 self.lineParts == other.lineParts) 242 243 # Returns True if the given Match object was at the beginning of the line. 244 def __isMatchAtStart(self, match): 245 return (match is not None) and (match.start() == 0) 246 247 # Takes in a list of Match objects and returns the minimal start point among 248 # them. If there aren't any successful matches it returns the length of 249 # the searched string. 250 def __firstMatch(self, matches, string): 251 starts = map(lambda m: len(string) if m is None else m.start(), matches) 252 return min(starts) 253 254 # This method parses the content of a check line stripped of the initial 255 # comment symbol and the CHECK keyword. 256 def __parse(self, line): 257 lineParts = [] 258 # Loop as long as there is something to parse. 259 while line: 260 # Search for the nearest occurrence of the special markers. 261 matchWhitespace = re.search(r"\s+", line) 262 matchPattern = re.search(CheckElement.regexPattern, line) 263 matchVariable = re.search(CheckElement.regexVariable, line) 264 265 # If one of the above was identified at the current position, extract them 266 # from the line, parse them and add to the list of line parts. 267 if self.__isMatchAtStart(matchWhitespace): 268 # A whitespace in the check line creates a new separator of line parts. 269 # This allows for ignored output between the previous and next parts. 270 line = line[matchWhitespace.end():] 271 lineParts.append(CheckElement.newSeparator()) 272 elif self.__isMatchAtStart(matchPattern): 273 pattern = line[0:matchPattern.end()] 274 line = line[matchPattern.end():] 275 lineParts.append(CheckElement.parsePattern(pattern)) 276 elif self.__isMatchAtStart(matchVariable): 277 var = line[0:matchVariable.end()] 278 line = line[matchVariable.end():] 279 lineParts.append(CheckElement.parseVariable(var)) 280 else: 281 # If we're not currently looking at a special marker, this is a plain 282 # text match all the way until the first special marker (or the end 283 # of the line). 284 firstMatch = self.__firstMatch([ matchWhitespace, matchPattern, matchVariable ], line) 285 text = line[0:firstMatch] 286 line = line[firstMatch:] 287 lineParts.append(CheckElement.parseText(text)) 288 return lineParts 289 290 # Returns the regex pattern to be matched in the output line. Variable 291 # references are substituted with their current values provided in the 292 # 'varState' argument. 293 # An exception is raised if a referenced variable is undefined. 294 def __generatePattern(self, linePart, varState): 295 if linePart.variant == CheckElement.Variant.VarRef: 296 try: 297 return re.escape(varState[linePart.name]) 298 except KeyError: 299 Logger.testFailed("Use of undefined variable \"" + linePart.name + "\"", 300 self.fileName, self.lineNo) 301 else: 302 return linePart.pattern 303 304 def __isSeparated(self, outputLine, matchStart): 305 return (matchStart == 0) or (outputLine[matchStart - 1:matchStart].isspace()) 306 307 # Attempts to match the check line against a line from the output file with 308 # the given initial variable values. It returns the new variable state if 309 # successful and None otherwise. 310 def match(self, outputLine, initialVarState): 311 # Do the full matching on a shadow copy of the variable state. If the 312 # matching fails half-way, we will not need to revert the state. 313 varState = dict(initialVarState) 314 315 matchStart = 0 316 isAfterSeparator = True 317 318 # Now try to parse all of the parts of the check line in the right order. 319 # Variable values are updated on-the-fly, meaning that a variable can 320 # be referenced immediately after its definition. 321 for part in self.lineParts: 322 if part.variant == CheckElement.Variant.Separator: 323 isAfterSeparator = True 324 continue 325 326 # Find the earliest match for this line part. 327 pattern = self.__generatePattern(part, varState) 328 while True: 329 match = re.search(pattern, outputLine[matchStart:]) 330 if (match is None) or (not isAfterSeparator and not self.__isMatchAtStart(match)): 331 return None 332 matchEnd = matchStart + match.end() 333 matchStart += match.start() 334 335 # Check if this is a valid match if we expect a whitespace separator 336 # before the matched text. Otherwise loop and look for another match. 337 if not isAfterSeparator or self.__isSeparated(outputLine, matchStart): 338 break 339 else: 340 matchStart += 1 341 342 if part.variant == CheckElement.Variant.VarDef: 343 if part.name in varState: 344 Logger.testFailed("Multiple definitions of variable \"" + part.name + "\"", 345 self.fileName, self.lineNo) 346 varState[part.name] = outputLine[matchStart:matchEnd] 347 348 matchStart = matchEnd 349 isAfterSeparator = False 350 351 # All parts were successfully matched. Return the new variable state. 352 return varState 353 354 355class CheckGroup(CommonEqualityMixin): 356 """Represents a named collection of check lines which are to be matched 357 against an output group of the same name.""" 358 359 def __init__(self, name, lines, fileName=None, lineNo=-1): 360 self.fileName = fileName 361 self.lineNo = lineNo 362 363 if not name: 364 Logger.fail("Check group does not have a name", self.fileName, self.lineNo) 365 if not lines: 366 Logger.fail("Check group does not have a body", self.fileName, self.lineNo) 367 368 self.name = name 369 self.lines = lines 370 371 def __eq__(self, other): 372 return (isinstance(other, self.__class__) and 373 self.name == other.name and 374 self.lines == other.lines) 375 376 def __headAndTail(self, list): 377 return list[0], list[1:] 378 379 # Splits a list of check lines at index 'i' such that lines[i] is the first 380 # element whose variant is not equal to the given parameter. 381 def __splitByVariant(self, lines, variant): 382 i = 0 383 while i < len(lines) and lines[i].variant == variant: 384 i += 1 385 return lines[:i], lines[i:] 386 387 # Extracts the first sequence of check lines which are independent of each 388 # other's match location, i.e. either consecutive DAG lines or a single 389 # InOrder line. Any Not lines preceeding this sequence are also extracted. 390 def __nextIndependentChecks(self, checkLines): 391 notChecks, checkLines = self.__splitByVariant(checkLines, CheckLine.Variant.Not) 392 if not checkLines: 393 return notChecks, [], [] 394 395 head, tail = self.__headAndTail(checkLines) 396 if head.variant == CheckLine.Variant.InOrder: 397 return notChecks, [head], tail 398 else: 399 assert head.variant == CheckLine.Variant.DAG 400 independentChecks, checkLines = self.__splitByVariant(checkLines, CheckLine.Variant.DAG) 401 return notChecks, independentChecks, checkLines 402 403 # If successful, returns the line number of the first output line matching the 404 # check line and the updated variable state. Otherwise returns -1 and None, 405 # respectively. The 'lineFilter' parameter can be used to supply a list of 406 # line numbers (counting from 1) which should be skipped. 407 def __findFirstMatch(self, checkLine, outputLines, startLineNo, lineFilter, varState): 408 matchLineNo = startLineNo 409 for outputLine in outputLines: 410 if matchLineNo not in lineFilter: 411 newVarState = checkLine.match(outputLine, varState) 412 if newVarState is not None: 413 return matchLineNo, newVarState 414 matchLineNo += 1 415 return -1, None 416 417 # Matches the given positive check lines against the output in order of 418 # appearance. Variable state is propagated but the scope of the search remains 419 # the same for all checks. Each output line can only be matched once. 420 # If all check lines are matched, the resulting variable state is returned 421 # together with the remaining output. The function also returns output lines 422 # which appear before either of the matched lines so they can be tested 423 # against Not checks. 424 def __matchIndependentChecks(self, checkLines, outputLines, startLineNo, varState): 425 # If no checks are provided, skip over the entire output. 426 if not checkLines: 427 return outputLines, [], startLineNo + len(outputLines), varState 428 429 # Keep track of which lines have been matched. 430 matchedLines = [] 431 432 # Find first unused output line which matches each check line. 433 for checkLine in checkLines: 434 matchLineNo, varState = \ 435 self.__findFirstMatch(checkLine, outputLines, startLineNo, matchedLines, varState) 436 if varState is None: 437 Logger.testFailed("Could not match check line \"" + checkLine.content + "\" " + 438 "starting from output line " + str(startLineNo), 439 self.fileName, checkLine.lineNo) 440 matchedLines.append(matchLineNo) 441 442 # Return new variable state and the output lines which lie outside the 443 # match locations of this independent group. 444 minMatchLineNo = min(matchedLines) 445 maxMatchLineNo = max(matchedLines) 446 preceedingLines = outputLines[:minMatchLineNo - startLineNo] 447 remainingLines = outputLines[maxMatchLineNo - startLineNo + 1:] 448 return preceedingLines, remainingLines, maxMatchLineNo + 1, varState 449 450 # Makes sure that the given check lines do not match any of the given output 451 # lines. Variable state does not change. 452 def __matchNotLines(self, checkLines, outputLines, startLineNo, varState): 453 for checkLine in checkLines: 454 assert checkLine.variant == CheckLine.Variant.Not 455 matchLineNo, matchVarState = \ 456 self.__findFirstMatch(checkLine, outputLines, startLineNo, [], varState) 457 if matchVarState is not None: 458 Logger.testFailed("CHECK-NOT line \"" + checkLine.content + "\" matches output line " + \ 459 str(matchLineNo), self.fileName, checkLine.lineNo) 460 461 # Matches the check lines in this group against an output group. It is 462 # responsible for running the checks in the right order and scope, and 463 # for propagating the variable state between the check lines. 464 def match(self, outputGroup): 465 varState = {} 466 checkLines = self.lines 467 outputLines = outputGroup.body 468 startLineNo = outputGroup.lineNo 469 470 while checkLines: 471 # Extract the next sequence of location-independent checks to be matched. 472 notChecks, independentChecks, checkLines = self.__nextIndependentChecks(checkLines) 473 474 # Match the independent checks. 475 notOutput, outputLines, newStartLineNo, newVarState = \ 476 self.__matchIndependentChecks(independentChecks, outputLines, startLineNo, varState) 477 478 # Run the Not checks against the output lines which lie between the last 479 # two independent groups or the bounds of the output. 480 self.__matchNotLines(notChecks, notOutput, startLineNo, varState) 481 482 # Update variable state. 483 startLineNo = newStartLineNo 484 varState = newVarState 485 486class OutputGroup(CommonEqualityMixin): 487 """Represents a named part of the test output against which a check group of 488 the same name is to be matched.""" 489 490 def __init__(self, name, body, fileName=None, lineNo=-1): 491 if not name: 492 Logger.fail("Output group does not have a name", fileName, lineNo) 493 if not body: 494 Logger.fail("Output group does not have a body", fileName, lineNo) 495 496 self.name = name 497 self.body = body 498 self.lineNo = lineNo 499 500 def __eq__(self, other): 501 return (isinstance(other, self.__class__) and 502 self.name == other.name and 503 self.body == other.body) 504 505 506class FileSplitMixin(object): 507 """Mixin for representing text files which need to be split into smaller 508 chunks before being parsed.""" 509 510 def _parseStream(self, stream): 511 lineNo = 0 512 allGroups = [] 513 currentGroup = None 514 515 for line in stream: 516 lineNo += 1 517 line = line.strip() 518 if not line: 519 continue 520 521 # Let the child class process the line and return information about it. 522 # The _processLine method can modify the content of the line (or delete it 523 # entirely) and specify whether it starts a new group. 524 processedLine, newGroupName = self._processLine(line, lineNo) 525 if newGroupName is not None: 526 currentGroup = (newGroupName, [], lineNo) 527 allGroups.append(currentGroup) 528 if processedLine is not None: 529 if currentGroup is not None: 530 currentGroup[1].append(processedLine) 531 else: 532 self._exceptionLineOutsideGroup(line, lineNo) 533 534 # Finally, take the generated line groups and let the child class process 535 # each one before storing the final outcome. 536 return list(map(lambda group: self._processGroup(group[0], group[1], group[2]), allGroups)) 537 538 539class CheckFile(FileSplitMixin): 540 """Collection of check groups extracted from the input test file.""" 541 542 def __init__(self, prefix, checkStream, fileName=None): 543 self.fileName = fileName 544 self.prefix = prefix 545 self.groups = self._parseStream(checkStream) 546 547 # Attempts to parse a check line. The regex searches for a comment symbol 548 # followed by the CHECK keyword, given attribute and a colon at the very 549 # beginning of the line. Whitespaces are ignored. 550 def _extractLine(self, prefix, line): 551 rIgnoreWhitespace = r"\s*" 552 rCommentSymbols = [r"//", r"#"] 553 regexPrefix = rIgnoreWhitespace + \ 554 r"(" + r"|".join(rCommentSymbols) + r")" + \ 555 rIgnoreWhitespace + \ 556 prefix + r":" 557 558 # The 'match' function succeeds only if the pattern is matched at the 559 # beginning of the line. 560 match = re.match(regexPrefix, line) 561 if match is not None: 562 return line[match.end():].strip() 563 else: 564 return None 565 566 # This function is invoked on each line of the check file and returns a pair 567 # which instructs the parser how the line should be handled. If the line is to 568 # be included in the current check group, it is returned in the first value. 569 # If the line starts a new check group, the name of the group is returned in 570 # the second value. 571 def _processLine(self, line, lineNo): 572 # Lines beginning with 'CHECK-START' start a new check group. 573 startLine = self._extractLine(self.prefix + "-START", line) 574 if startLine is not None: 575 return None, startLine 576 577 # Lines starting only with 'CHECK' are matched in order. 578 plainLine = self._extractLine(self.prefix, line) 579 if plainLine is not None: 580 return (plainLine, CheckLine.Variant.InOrder, lineNo), None 581 582 # 'CHECK-DAG' lines are no-order assertions. 583 dagLine = self._extractLine(self.prefix + "-DAG", line) 584 if dagLine is not None: 585 return (dagLine, CheckLine.Variant.DAG, lineNo), None 586 587 # 'CHECK-NOT' lines are no-order negative assertions. 588 notLine = self._extractLine(self.prefix + "-NOT", line) 589 if notLine is not None: 590 return (notLine, CheckLine.Variant.Not, lineNo), None 591 592 # Other lines are ignored. 593 return None, None 594 595 def _exceptionLineOutsideGroup(self, line, lineNo): 596 Logger.fail("Check line not inside a group", self.fileName, lineNo) 597 598 # Constructs a check group from the parser-collected check lines. 599 def _processGroup(self, name, lines, lineNo): 600 checkLines = list(map(lambda line: CheckLine(line[0], line[1], self.fileName, line[2]), lines)) 601 return CheckGroup(name, checkLines, self.fileName, lineNo) 602 603 def match(self, outputFile): 604 for checkGroup in self.groups: 605 # TODO: Currently does not handle multiple occurrences of the same group 606 # name, e.g. when a pass is run multiple times. It will always try to 607 # match a check group against the first output group of the same name. 608 outputGroup = outputFile.findGroup(checkGroup.name) 609 if outputGroup is None: 610 Logger.fail("Group \"" + checkGroup.name + "\" not found in the output", 611 self.fileName, checkGroup.lineNo) 612 Logger.startTest(checkGroup.name) 613 checkGroup.match(outputGroup) 614 Logger.testPassed() 615 616 617class OutputFile(FileSplitMixin): 618 """Representation of the output generated by the test and split into groups 619 within which the checks are performed. 620 621 C1visualizer format is parsed with a state machine which differentiates 622 between the 'compilation' and 'cfg' blocks. The former marks the beginning 623 of a method. It is parsed for the method's name but otherwise ignored. Each 624 subsequent CFG block represents one stage of the compilation pipeline and 625 is parsed into an output group named "<method name> <pass name>". 626 """ 627 628 class ParsingState: 629 OutsideBlock, InsideCompilationBlock, StartingCfgBlock, InsideCfgBlock = range(4) 630 631 def __init__(self, outputStream, fileName=None): 632 self.fileName = fileName 633 634 # Initialize the state machine 635 self.lastMethodName = None 636 self.state = OutputFile.ParsingState.OutsideBlock 637 self.groups = self._parseStream(outputStream) 638 639 # This function is invoked on each line of the output file and returns a pair 640 # which instructs the parser how the line should be handled. If the line is to 641 # be included in the current group, it is returned in the first value. If the 642 # line starts a new output group, the name of the group is returned in the 643 # second value. 644 def _processLine(self, line, lineNo): 645 if self.state == OutputFile.ParsingState.StartingCfgBlock: 646 # Previous line started a new 'cfg' block which means that this one must 647 # contain the name of the pass (this is enforced by C1visualizer). 648 if re.match("name\s+\"[^\"]+\"", line): 649 # Extract the pass name, prepend it with the name of the method and 650 # return as the beginning of a new group. 651 self.state = OutputFile.ParsingState.InsideCfgBlock 652 return (None, self.lastMethodName + " " + line.split("\"")[1]) 653 else: 654 Logger.fail("Expected output group name", self.fileName, lineNo) 655 656 elif self.state == OutputFile.ParsingState.InsideCfgBlock: 657 if line == "end_cfg": 658 self.state = OutputFile.ParsingState.OutsideBlock 659 return (None, None) 660 else: 661 return (line, None) 662 663 elif self.state == OutputFile.ParsingState.InsideCompilationBlock: 664 # Search for the method's name. Format: method "<name>" 665 if re.match("method\s+\"[^\"]*\"", line): 666 methodName = line.split("\"")[1].strip() 667 if not methodName: 668 Logger.fail("Empty method name in output", self.fileName, lineNo) 669 self.lastMethodName = methodName 670 elif line == "end_compilation": 671 self.state = OutputFile.ParsingState.OutsideBlock 672 return (None, None) 673 674 else: 675 assert self.state == OutputFile.ParsingState.OutsideBlock 676 if line == "begin_cfg": 677 # The line starts a new group but we'll wait until the next line from 678 # which we can extract the name of the pass. 679 if self.lastMethodName is None: 680 Logger.fail("Expected method header", self.fileName, lineNo) 681 self.state = OutputFile.ParsingState.StartingCfgBlock 682 return (None, None) 683 elif line == "begin_compilation": 684 self.state = OutputFile.ParsingState.InsideCompilationBlock 685 return (None, None) 686 else: 687 Logger.fail("Output line not inside a group", self.fileName, lineNo) 688 689 # Constructs an output group from the parser-collected output lines. 690 def _processGroup(self, name, lines, lineNo): 691 return OutputGroup(name, lines, self.fileName, lineNo + 1) 692 693 def findGroup(self, name): 694 for group in self.groups: 695 if group.name == name: 696 return group 697 return None 698 699 700def ParseArguments(): 701 parser = argparse.ArgumentParser() 702 parser.add_argument("tested_file", 703 help="text file the checks should be verified against") 704 parser.add_argument("source_path", nargs="?", 705 help="path to file/folder with checking annotations") 706 parser.add_argument("--check-prefix", dest="check_prefix", default="CHECK", metavar="PREFIX", 707 help="prefix of checks in the test files (default: CHECK)") 708 parser.add_argument("--list-groups", dest="list_groups", action="store_true", 709 help="print a list of all groups found in the tested file") 710 parser.add_argument("--dump-group", dest="dump_group", metavar="GROUP", 711 help="print the contents of an output group") 712 parser.add_argument("-q", "--quiet", action="store_true", 713 help="print only errors") 714 return parser.parse_args() 715 716 717def ListGroups(outputFilename): 718 outputFile = OutputFile(open(outputFilename, "r")) 719 for group in outputFile.groups: 720 Logger.log(group.name) 721 722 723def DumpGroup(outputFilename, groupName): 724 outputFile = OutputFile(open(outputFilename, "r")) 725 group = outputFile.findGroup(groupName) 726 if group: 727 lineNo = group.lineNo 728 maxLineNo = lineNo + len(group.body) 729 lenLineNo = len(str(maxLineNo)) + 2 730 for line in group.body: 731 Logger.log((str(lineNo) + ":").ljust(lenLineNo) + line) 732 lineNo += 1 733 else: 734 Logger.fail("Group \"" + groupName + "\" not found in the output") 735 736 737# Returns a list of files to scan for check annotations in the given path. Path 738# to a file is returned as a single-element list, directories are recursively 739# traversed and all '.java' files returned. 740def FindCheckFiles(path): 741 if not path: 742 Logger.fail("No source path provided") 743 elif os.path.isfile(path): 744 return [ path ] 745 elif os.path.isdir(path): 746 foundFiles = [] 747 for root, dirs, files in os.walk(path): 748 for file in files: 749 if os.path.splitext(file)[1] == ".java": 750 foundFiles.append(os.path.join(root, file)) 751 return foundFiles 752 else: 753 Logger.fail("Source path \"" + path + "\" not found") 754 755 756def RunChecks(checkPrefix, checkPath, outputFilename): 757 outputBaseName = os.path.basename(outputFilename) 758 outputFile = OutputFile(open(outputFilename, "r"), outputBaseName) 759 760 for checkFilename in FindCheckFiles(checkPath): 761 checkBaseName = os.path.basename(checkFilename) 762 checkFile = CheckFile(checkPrefix, open(checkFilename, "r"), checkBaseName) 763 checkFile.match(outputFile) 764 765 766if __name__ == "__main__": 767 args = ParseArguments() 768 769 if args.quiet: 770 Logger.Verbosity = Logger.Level.Error 771 772 if args.list_groups: 773 ListGroups(args.tested_file) 774 elif args.dump_group: 775 DumpGroup(args.tested_file, args.dump_group) 776 else: 777 RunChecks(args.check_prefix, args.source_path, args.tested_file) 778