1# Copyright (c) 2018 Google LLC 2# 3# Licensed under the Apache License, Version 2.0 (the "License"); 4# you may not use this file except in compliance with the License. 5# You may obtain a copy of the License at 6# 7# http://www.apache.org/licenses/LICENSE-2.0 8# 9# Unless required by applicable law or agreed to in writing, software 10# distributed under the License is distributed on an "AS IS" BASIS, 11# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12# See the License for the specific language governing permissions and 13# limitations under the License. 14"""A number of common spirv result checks coded in mixin classes. 15 16A test case can use these checks by declaring their enclosing mixin classes 17as superclass and providing the expected_* variables required by the check_*() 18methods in the mixin classes. 19""" 20import difflib 21import functools 22import os 23import re 24import subprocess 25import traceback 26from spirv_test_framework import SpirvTest 27from builtins import bytes 28 29DEFAULT_SPIRV_VERSION = 0x010000 30 31def convert_to_unix_line_endings(source): 32 """Converts all line endings in source to be unix line endings.""" 33 result = source.replace('\r\n', '\n').replace('\r', '\n') 34 return result 35 36 37def substitute_file_extension(filename, extension): 38 """Substitutes file extension, respecting known shader extensions. 39 40 foo.vert -> foo.vert.[extension] [similarly for .frag, .comp, etc.] 41 foo.glsl -> foo.[extension] 42 foo.unknown -> foo.[extension] 43 foo -> foo.[extension] 44 """ 45 if filename[-5:] not in [ 46 '.vert', '.frag', '.tesc', '.tese', '.geom', '.comp', '.spvasm' 47 ]: 48 return filename.rsplit('.', 1)[0] + '.' + extension 49 else: 50 return filename + '.' + extension 51 52 53def get_object_filename(source_filename): 54 """Gets the object filename for the given source file.""" 55 return substitute_file_extension(source_filename, 'spv') 56 57 58def get_assembly_filename(source_filename): 59 """Gets the assembly filename for the given source file.""" 60 return substitute_file_extension(source_filename, 'spvasm') 61 62 63def verify_file_non_empty(filename): 64 """Checks that a given file exists and is not empty.""" 65 if not os.path.isfile(filename): 66 return False, 'Cannot find file: ' + filename 67 if not os.path.getsize(filename): 68 return False, 'Empty file: ' + filename 69 return True, '' 70 71 72class ReturnCodeIsZero(SpirvTest): 73 """Mixin class for checking that the return code is zero.""" 74 75 def check_return_code_is_zero(self, status): 76 if status.returncode: 77 return False, 'Non-zero return code: {ret}\n'.format( 78 ret=status.returncode) 79 return True, '' 80 81 82class ReturnCodeIsNonZero(SpirvTest): 83 """Mixin class for checking that the return code is not zero.""" 84 85 def check_return_code_is_nonzero(self, status): 86 if not status.returncode: 87 return False, 'return code is 0' 88 return True, '' 89 90 91class NoOutputOnStdout(SpirvTest): 92 """Mixin class for checking that there is no output on stdout.""" 93 94 def check_no_output_on_stdout(self, status): 95 if status.stdout: 96 return False, 'Non empty stdout: {out}\n'.format(out=status.stdout) 97 return True, '' 98 99 100class NoOutputOnStderr(SpirvTest): 101 """Mixin class for checking that there is no output on stderr.""" 102 103 def check_no_output_on_stderr(self, status): 104 if status.stderr: 105 return False, 'Non empty stderr: {err}\n'.format(err=status.stderr) 106 return True, '' 107 108 109class SuccessfulReturn(ReturnCodeIsZero, NoOutputOnStdout, NoOutputOnStderr): 110 """Mixin class for checking that return code is zero and no output on 111 stdout and stderr.""" 112 pass 113 114 115class NoGeneratedFiles(SpirvTest): 116 """Mixin class for checking that there is no file generated.""" 117 118 def check_no_generated_files(self, status): 119 all_files = os.listdir(status.directory) 120 input_files = status.input_filenames 121 if all([f.startswith(status.directory) for f in input_files]): 122 all_files = [os.path.join(status.directory, f) for f in all_files] 123 generated_files = set(all_files) - set(input_files) 124 if len(generated_files) == 0: 125 return True, '' 126 else: 127 return False, 'Extra files generated: {}'.format(generated_files) 128 129 130class CorrectBinaryLengthAndPreamble(SpirvTest): 131 """Provides methods for verifying preamble for a SPIR-V binary.""" 132 133 def verify_binary_length_and_header(self, binary, spv_version=0x10000): 134 """Checks that the given SPIR-V binary has valid length and header. 135 136 Returns: 137 False, error string if anything is invalid 138 True, '' otherwise 139 Args: 140 binary: a bytes object containing the SPIR-V binary 141 spv_version: target SPIR-V version number, with same encoding 142 as the version word in a SPIR-V header. 143 """ 144 145 def read_word(binary, index, little_endian): 146 """Reads the index-th word from the given binary file.""" 147 word = binary[index * 4:(index + 1) * 4] 148 if little_endian: 149 word = reversed(word) 150 return functools.reduce(lambda w, b: (w << 8) | b, word, 0) 151 152 def check_endianness(binary): 153 """Checks the endianness of the given SPIR-V binary. 154 155 Returns: 156 True if it's little endian, False if it's big endian. 157 None if magic number is wrong. 158 """ 159 first_word = read_word(binary, 0, True) 160 if first_word == 0x07230203: 161 return True 162 first_word = read_word(binary, 0, False) 163 if first_word == 0x07230203: 164 return False 165 return None 166 167 num_bytes = len(binary) 168 if num_bytes % 4 != 0: 169 return False, ('Incorrect SPV binary: size should be a multiple' 170 ' of words') 171 if num_bytes < 20: 172 return False, 'Incorrect SPV binary: size less than 5 words' 173 174 preamble = binary[0:19] 175 little_endian = check_endianness(preamble) 176 # SPIR-V module magic number 177 if little_endian is None: 178 return False, 'Incorrect SPV binary: wrong magic number' 179 180 # SPIR-V version number 181 version = read_word(preamble, 1, little_endian) 182 # TODO(dneto): Recent Glslang uses version word 0 for opengl_compat 183 # profile 184 185 if version != spv_version and version != 0: 186 return False, 'Incorrect SPV binary: wrong version number: ' + hex(version) + ' expected ' + hex(spv_version) 187 # Shaderc-over-Glslang (0x000d....) or 188 # SPIRV-Tools (0x0007....) generator number 189 if read_word(preamble, 2, little_endian) != 0x000d0007 and \ 190 read_word(preamble, 2, little_endian) != 0x00070000: 191 return False, ('Incorrect SPV binary: wrong generator magic ' 'number') 192 # reserved for instruction schema 193 if read_word(preamble, 4, little_endian) != 0: 194 return False, 'Incorrect SPV binary: the 5th byte should be 0' 195 196 return True, '' 197 198 199class CorrectObjectFilePreamble(CorrectBinaryLengthAndPreamble): 200 """Provides methods for verifying preamble for a SPV object file.""" 201 202 def verify_object_file_preamble(self, 203 filename, 204 spv_version=DEFAULT_SPIRV_VERSION): 205 """Checks that the given SPIR-V binary file has correct preamble.""" 206 207 success, message = verify_file_non_empty(filename) 208 if not success: 209 return False, message 210 211 with open(filename, 'rb') as object_file: 212 object_file.seek(0, os.SEEK_END) 213 num_bytes = object_file.tell() 214 215 object_file.seek(0) 216 217 binary = bytes(object_file.read()) 218 return self.verify_binary_length_and_header(binary, spv_version) 219 220 return True, '' 221 222 223class CorrectAssemblyFilePreamble(SpirvTest): 224 """Provides methods for verifying preamble for a SPV assembly file.""" 225 226 def verify_assembly_file_preamble(self, filename): 227 success, message = verify_file_non_empty(filename) 228 if not success: 229 return False, message 230 231 with open(filename) as assembly_file: 232 line1 = assembly_file.readline() 233 line2 = assembly_file.readline() 234 line3 = assembly_file.readline() 235 236 if (line1 != '; SPIR-V\n' or line2 != '; Version: 1.0\n' or 237 (not line3.startswith('; Generator: Google Shaderc over Glslang;'))): 238 return False, 'Incorrect SPV assembly' 239 240 return True, '' 241 242 243class ValidObjectFile(SuccessfulReturn, CorrectObjectFilePreamble): 244 """Mixin class for checking that every input file generates a valid SPIR-V 1.0 245 object file following the object file naming rule, and there is no output on 246 stdout/stderr.""" 247 248 def check_object_file_preamble(self, status): 249 for input_filename in status.input_filenames: 250 object_filename = get_object_filename(input_filename) 251 success, message = self.verify_object_file_preamble( 252 os.path.join(status.directory, object_filename)) 253 if not success: 254 return False, message 255 return True, '' 256 257 258class ValidObjectFile1_3(ReturnCodeIsZero, CorrectObjectFilePreamble): 259 """Mixin class for checking that every input file generates a valid SPIR-V 1.3 260 object file following the object file naming rule, and there is no output on 261 stdout/stderr.""" 262 263 def check_object_file_preamble(self, status): 264 for input_filename in status.input_filenames: 265 object_filename = get_object_filename(input_filename) 266 success, message = self.verify_object_file_preamble( 267 os.path.join(status.directory, object_filename), 0x10300) 268 if not success: 269 return False, message 270 return True, '' 271 272 273class ValidObjectFile1_5(ReturnCodeIsZero, CorrectObjectFilePreamble): 274 """Mixin class for checking that every input file generates a valid SPIR-V 1.5 275 object file following the object file naming rule, and there is no output on 276 stdout/stderr.""" 277 278 def check_object_file_preamble(self, status): 279 for input_filename in status.input_filenames: 280 object_filename = get_object_filename(input_filename) 281 success, message = self.verify_object_file_preamble( 282 os.path.join(status.directory, object_filename), 0x10500) 283 if not success: 284 return False, message 285 return True, '' 286 287 288class ValidObjectFileWithAssemblySubstr(SuccessfulReturn, 289 CorrectObjectFilePreamble): 290 """Mixin class for checking that every input file generates a valid object 291 292 file following the object file naming rule, there is no output on 293 stdout/stderr, and the disassmbly contains a specified substring per 294 input. 295 """ 296 297 def check_object_file_disassembly(self, status): 298 for an_input in status.inputs: 299 object_filename = get_object_filename(an_input.filename) 300 obj_file = str(os.path.join(status.directory, object_filename)) 301 success, message = self.verify_object_file_preamble(obj_file) 302 if not success: 303 return False, message 304 cmd = [status.test_manager.disassembler_path, '--no-color', obj_file] 305 process = subprocess.Popen( 306 args=cmd, 307 stdin=subprocess.PIPE, 308 stdout=subprocess.PIPE, 309 stderr=subprocess.PIPE, 310 cwd=status.directory) 311 output = process.communicate(None) 312 disassembly = output[0] 313 if not isinstance(an_input.assembly_substr, str): 314 return False, 'Missing assembly_substr member' 315 if an_input.assembly_substr not in disassembly: 316 return False, ('Incorrect disassembly output:\n{asm}\n' 317 'Expected substring not found:\n{exp}'.format( 318 asm=disassembly, exp=an_input.assembly_substr)) 319 return True, '' 320 321 322class ValidNamedObjectFile(SuccessfulReturn, CorrectObjectFilePreamble): 323 """Mixin class for checking that a list of object files with the given 324 names are correctly generated, and there is no output on stdout/stderr. 325 326 To mix in this class, subclasses need to provide expected_object_filenames 327 as the expected object filenames. 328 """ 329 330 def check_object_file_preamble(self, status): 331 for object_filename in self.expected_object_filenames: 332 success, message = self.verify_object_file_preamble( 333 os.path.join(status.directory, object_filename)) 334 if not success: 335 return False, message 336 return True, '' 337 338 339class ValidFileContents(SpirvTest): 340 """Mixin class to test that a specific file contains specific text 341 To mix in this class, subclasses need to provide expected_file_contents as 342 the contents of the file and target_filename to determine the location.""" 343 344 def check_file(self, status): 345 target_filename = os.path.join(status.directory, self.target_filename) 346 if not os.path.isfile(target_filename): 347 return False, 'Cannot find file: ' + target_filename 348 with open(target_filename, 'r') as target_file: 349 file_contents = target_file.read() 350 if isinstance(self.expected_file_contents, str): 351 if file_contents == self.expected_file_contents: 352 return True, '' 353 return False, ('Incorrect file output: \n{act}\n' 354 'Expected:\n{exp}' 355 'With diff:\n{diff}'.format( 356 act=file_contents, 357 exp=self.expected_file_contents, 358 diff='\n'.join( 359 list( 360 difflib.unified_diff( 361 self.expected_file_contents.split('\n'), 362 file_contents.split('\n'), 363 fromfile='expected_output', 364 tofile='actual_output'))))) 365 elif isinstance(self.expected_file_contents, type(re.compile(''))): 366 if self.expected_file_contents.search(file_contents): 367 return True, '' 368 return False, ('Incorrect file output: \n{act}\n' 369 'Expected matching regex pattern:\n{exp}'.format( 370 act=file_contents, 371 exp=self.expected_file_contents.pattern)) 372 return False, ( 373 'Could not open target file ' + target_filename + ' for reading') 374 375 376class ValidAssemblyFile(SuccessfulReturn, CorrectAssemblyFilePreamble): 377 """Mixin class for checking that every input file generates a valid assembly 378 file following the assembly file naming rule, and there is no output on 379 stdout/stderr.""" 380 381 def check_assembly_file_preamble(self, status): 382 for input_filename in status.input_filenames: 383 assembly_filename = get_assembly_filename(input_filename) 384 success, message = self.verify_assembly_file_preamble( 385 os.path.join(status.directory, assembly_filename)) 386 if not success: 387 return False, message 388 return True, '' 389 390 391class ValidAssemblyFileWithSubstr(ValidAssemblyFile): 392 """Mixin class for checking that every input file generates a valid assembly 393 file following the assembly file naming rule, there is no output on 394 stdout/stderr, and all assembly files have the given substring specified 395 by expected_assembly_substr. 396 397 To mix in this class, subclasses need to provde expected_assembly_substr 398 as the expected substring. 399 """ 400 401 def check_assembly_with_substr(self, status): 402 for input_filename in status.input_filenames: 403 assembly_filename = get_assembly_filename(input_filename) 404 success, message = self.verify_assembly_file_preamble( 405 os.path.join(status.directory, assembly_filename)) 406 if not success: 407 return False, message 408 with open(assembly_filename, 'r') as f: 409 content = f.read() 410 if self.expected_assembly_substr not in convert_to_unix_line_endings( 411 content): 412 return False, ('Incorrect assembly output:\n{asm}\n' 413 'Expected substring not found:\n{exp}'.format( 414 asm=content, exp=self.expected_assembly_substr)) 415 return True, '' 416 417 418class ValidAssemblyFileWithoutSubstr(ValidAssemblyFile): 419 """Mixin class for checking that every input file generates a valid assembly 420 file following the assembly file naming rule, there is no output on 421 stdout/stderr, and no assembly files have the given substring specified 422 by unexpected_assembly_substr. 423 424 To mix in this class, subclasses need to provde unexpected_assembly_substr 425 as the substring we expect not to see. 426 """ 427 428 def check_assembly_for_substr(self, status): 429 for input_filename in status.input_filenames: 430 assembly_filename = get_assembly_filename(input_filename) 431 success, message = self.verify_assembly_file_preamble( 432 os.path.join(status.directory, assembly_filename)) 433 if not success: 434 return False, message 435 with open(assembly_filename, 'r') as f: 436 content = f.read() 437 if self.unexpected_assembly_substr in convert_to_unix_line_endings( 438 content): 439 return False, ('Incorrect assembly output:\n{asm}\n' 440 'Unexpected substring found:\n{unexp}'.format( 441 asm=content, exp=self.unexpected_assembly_substr)) 442 return True, '' 443 444 445class ValidNamedAssemblyFile(SuccessfulReturn, CorrectAssemblyFilePreamble): 446 """Mixin class for checking that a list of assembly files with the given 447 names are correctly generated, and there is no output on stdout/stderr. 448 449 To mix in this class, subclasses need to provide expected_assembly_filenames 450 as the expected assembly filenames. 451 """ 452 453 def check_object_file_preamble(self, status): 454 for assembly_filename in self.expected_assembly_filenames: 455 success, message = self.verify_assembly_file_preamble( 456 os.path.join(status.directory, assembly_filename)) 457 if not success: 458 return False, message 459 return True, '' 460 461 462class ErrorMessage(SpirvTest): 463 """Mixin class for tests that fail with a specific error message. 464 465 To mix in this class, subclasses need to provide expected_error as the 466 expected error message. 467 468 The test should fail if the subprocess was terminated by a signal. 469 """ 470 471 def check_has_error_message(self, status): 472 if not status.returncode: 473 return False, ('Expected error message, but returned success from ' 474 'command execution') 475 if status.returncode < 0: 476 # On Unix, a negative value -N for Popen.returncode indicates 477 # termination by signal N. 478 # https://docs.python.org/2/library/subprocess.html 479 return False, ('Expected error message, but command was terminated by ' 480 'signal ' + str(status.returncode)) 481 if not status.stderr: 482 return False, 'Expected error message, but no output on stderr' 483 if self.expected_error != convert_to_unix_line_endings(status.stderr): 484 return False, ('Incorrect stderr output:\n{act}\n' 485 'Expected:\n{exp}'.format( 486 act=status.stderr, exp=self.expected_error)) 487 return True, '' 488 489 490class ErrorMessageSubstr(SpirvTest): 491 """Mixin class for tests that fail with a specific substring in the error 492 message. 493 494 To mix in this class, subclasses need to provide expected_error_substr as 495 the expected error message substring. 496 497 The test should fail if the subprocess was terminated by a signal. 498 """ 499 500 def check_has_error_message_as_substring(self, status): 501 if not status.returncode: 502 return False, ('Expected error message, but returned success from ' 503 'command execution') 504 if status.returncode < 0: 505 # On Unix, a negative value -N for Popen.returncode indicates 506 # termination by signal N. 507 # https://docs.python.org/2/library/subprocess.html 508 return False, ('Expected error message, but command was terminated by ' 509 'signal ' + str(status.returncode)) 510 if not status.stderr: 511 return False, 'Expected error message, but no output on stderr' 512 if self.expected_error_substr not in convert_to_unix_line_endings( 513 status.stderr): 514 return False, ('Incorrect stderr output:\n{act}\n' 515 'Expected substring not found in stderr:\n{exp}'.format( 516 act=status.stderr, exp=self.expected_error_substr)) 517 return True, '' 518 519 520class WarningMessage(SpirvTest): 521 """Mixin class for tests that succeed but have a specific warning message. 522 523 To mix in this class, subclasses need to provide expected_warning as the 524 expected warning message. 525 """ 526 527 def check_has_warning_message(self, status): 528 if status.returncode: 529 return False, ('Expected warning message, but returned failure from' 530 ' command execution') 531 if not status.stderr: 532 return False, 'Expected warning message, but no output on stderr' 533 if self.expected_warning != convert_to_unix_line_endings(status.stderr): 534 return False, ('Incorrect stderr output:\n{act}\n' 535 'Expected:\n{exp}'.format( 536 act=status.stderr, exp=self.expected_warning)) 537 return True, '' 538 539 540class ValidObjectFileWithWarning(NoOutputOnStdout, CorrectObjectFilePreamble, 541 WarningMessage): 542 """Mixin class for checking that every input file generates a valid object 543 file following the object file naming rule, with a specific warning message. 544 """ 545 546 def check_object_file_preamble(self, status): 547 for input_filename in status.input_filenames: 548 object_filename = get_object_filename(input_filename) 549 success, message = self.verify_object_file_preamble( 550 os.path.join(status.directory, object_filename)) 551 if not success: 552 return False, message 553 return True, '' 554 555 556class ValidAssemblyFileWithWarning(NoOutputOnStdout, 557 CorrectAssemblyFilePreamble, WarningMessage): 558 """Mixin class for checking that every input file generates a valid assembly 559 file following the assembly file naming rule, with a specific warning 560 message.""" 561 562 def check_assembly_file_preamble(self, status): 563 for input_filename in status.input_filenames: 564 assembly_filename = get_assembly_filename(input_filename) 565 success, message = self.verify_assembly_file_preamble( 566 os.path.join(status.directory, assembly_filename)) 567 if not success: 568 return False, message 569 return True, '' 570 571 572class StdoutMatch(SpirvTest): 573 """Mixin class for tests that can expect output on stdout. 574 575 To mix in this class, subclasses need to provide expected_stdout as the 576 expected stdout output. 577 578 For expected_stdout, if it's True, then they expect something on stdout but 579 will not check what it is. If it's a string, expect an exact match. If it's 580 anything else, it is assumed to be a compiled regular expression which will 581 be matched against re.search(). It will expect 582 expected_stdout.search(status.stdout) to be true. 583 """ 584 585 def check_stdout_match(self, status): 586 # "True" in this case means we expect something on stdout, but we do not 587 # care what it is, we want to distinguish this from "blah" which means we 588 # expect exactly the string "blah". 589 if self.expected_stdout is True: 590 if not status.stdout: 591 return False, 'Expected something on stdout' 592 elif type(self.expected_stdout) == str: 593 if self.expected_stdout != convert_to_unix_line_endings(status.stdout): 594 return False, ('Incorrect stdout output:\n{ac}\n' 595 'Expected:\n{ex}'.format( 596 ac=status.stdout, ex=self.expected_stdout)) 597 else: 598 converted = convert_to_unix_line_endings(status.stdout) 599 if not self.expected_stdout.search(converted): 600 return False, ('Incorrect stdout output:\n{ac}\n' 601 'Expected to match regex:\n{ex}'.format( 602 ac=status.stdout, ex=self.expected_stdout.pattern)) 603 return True, '' 604 605 606class StderrMatch(SpirvTest): 607 """Mixin class for tests that can expect output on stderr. 608 609 To mix in this class, subclasses need to provide expected_stderr as the 610 expected stderr output. 611 612 For expected_stderr, if it's True, then they expect something on stderr, 613 but will not check what it is. If it's a string, expect an exact match. 614 If it's anything else, it is assumed to be a compiled regular expression 615 which will be matched against re.search(). It will expect 616 expected_stderr.search(status.stderr) to be true. 617 """ 618 619 def check_stderr_match(self, status): 620 # "True" in this case means we expect something on stderr, but we do not 621 # care what it is, we want to distinguish this from "blah" which means we 622 # expect exactly the string "blah". 623 if self.expected_stderr is True: 624 if not status.stderr: 625 return False, 'Expected something on stderr' 626 elif type(self.expected_stderr) == str: 627 if self.expected_stderr != convert_to_unix_line_endings(status.stderr): 628 return False, ('Incorrect stderr output:\n{ac}\n' 629 'Expected:\n{ex}'.format( 630 ac=status.stderr, ex=self.expected_stderr)) 631 else: 632 if not self.expected_stderr.search( 633 convert_to_unix_line_endings(status.stderr)): 634 return False, ('Incorrect stderr output:\n{ac}\n' 635 'Expected to match regex:\n{ex}'.format( 636 ac=status.stderr, ex=self.expected_stderr.pattern)) 637 return True, '' 638 639 640class StdoutNoWiderThan80Columns(SpirvTest): 641 """Mixin class for tests that require stdout to 80 characters or narrower. 642 643 To mix in this class, subclasses need to provide expected_stdout as the 644 expected stdout output. 645 """ 646 647 def check_stdout_not_too_wide(self, status): 648 if not status.stdout: 649 return True, '' 650 else: 651 for line in status.stdout.splitlines(): 652 if len(line) > 80: 653 return False, ('Stdout line longer than 80 columns: %s' % line) 654 return True, '' 655 656 657class NoObjectFile(SpirvTest): 658 """Mixin class for checking that no input file has a corresponding object 659 file.""" 660 661 def check_no_object_file(self, status): 662 for input_filename in status.input_filenames: 663 object_filename = get_object_filename(input_filename) 664 full_object_file = os.path.join(status.directory, object_filename) 665 print('checking %s' % full_object_file) 666 if os.path.isfile(full_object_file): 667 return False, ( 668 'Expected no object file, but found: %s' % full_object_file) 669 return True, '' 670 671 672class NoNamedOutputFiles(SpirvTest): 673 """Mixin class for checking that no specified output files exist. 674 675 The expected_output_filenames member should be full pathnames.""" 676 677 def check_no_named_output_files(self, status): 678 for object_filename in self.expected_output_filenames: 679 if os.path.isfile(object_filename): 680 return False, ( 681 'Expected no output file, but found: %s' % object_filename) 682 return True, '' 683 684 685class ExecutedListOfPasses(SpirvTest): 686 """Mixin class for checking that a list of passes where executed. 687 688 It works by analyzing the output of the --print-all flag to spirv-opt. 689 690 For this mixin to work, the class member expected_passes should be a sequence 691 of pass names as returned by Pass::name(). 692 """ 693 694 def check_list_of_executed_passes(self, status): 695 # Collect all the output lines containing a pass name. 696 pass_names = [] 697 pass_name_re = re.compile(r'.*IR before pass (?P<pass_name>[\S]+)') 698 for line in status.stderr.splitlines(): 699 match = pass_name_re.match(line) 700 if match: 701 pass_names.append(match.group('pass_name')) 702 703 for (expected, actual) in zip(self.expected_passes, pass_names): 704 if expected != actual: 705 return False, ( 706 'Expected pass "%s" but found pass "%s"\n' % (expected, actual)) 707 708 return True, '' 709