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 ValidObjectFile1_6(ReturnCodeIsZero, CorrectObjectFilePreamble):
289  """Mixin class for checking that every input file generates a valid SPIR-V 1.6
290    object file following the object file naming rule, and there is no output on
291    stdout/stderr."""
292
293  def check_object_file_preamble(self, status):
294    for input_filename in status.input_filenames:
295      object_filename = get_object_filename(input_filename)
296      success, message = self.verify_object_file_preamble(
297          os.path.join(status.directory, object_filename), 0x10600)
298      if not success:
299        return False, message
300    return True, ''
301
302
303class ValidObjectFileWithAssemblySubstr(SuccessfulReturn,
304                                        CorrectObjectFilePreamble):
305  """Mixin class for checking that every input file generates a valid object
306
307    file following the object file naming rule, there is no output on
308    stdout/stderr, and the disassmbly contains a specified substring per
309    input.
310  """
311
312  def check_object_file_disassembly(self, status):
313    for an_input in status.inputs:
314      object_filename = get_object_filename(an_input.filename)
315      obj_file = str(os.path.join(status.directory, object_filename))
316      success, message = self.verify_object_file_preamble(obj_file)
317      if not success:
318        return False, message
319      cmd = [status.test_manager.disassembler_path, '--no-color', obj_file]
320      process = subprocess.Popen(
321          args=cmd,
322          stdin=subprocess.PIPE,
323          stdout=subprocess.PIPE,
324          stderr=subprocess.PIPE,
325          cwd=status.directory)
326      output = process.communicate(None)
327      disassembly = output[0]
328      if not isinstance(an_input.assembly_substr, str):
329        return False, 'Missing assembly_substr member'
330      if an_input.assembly_substr not in disassembly:
331        return False, ('Incorrect disassembly output:\n{asm}\n'
332                       'Expected substring not found:\n{exp}'.format(
333                           asm=disassembly, exp=an_input.assembly_substr))
334    return True, ''
335
336
337class ValidNamedObjectFile(SuccessfulReturn, CorrectObjectFilePreamble):
338  """Mixin class for checking that a list of object files with the given
339    names are correctly generated, and there is no output on stdout/stderr.
340
341    To mix in this class, subclasses need to provide expected_object_filenames
342    as the expected object filenames.
343    """
344
345  def check_object_file_preamble(self, status):
346    for object_filename in self.expected_object_filenames:
347      success, message = self.verify_object_file_preamble(
348          os.path.join(status.directory, object_filename))
349      if not success:
350        return False, message
351    return True, ''
352
353
354class ValidFileContents(SpirvTest):
355  """Mixin class to test that a specific file contains specific text
356    To mix in this class, subclasses need to provide expected_file_contents as
357    the contents of the file and target_filename to determine the location."""
358
359  def check_file(self, status):
360    target_filename = os.path.join(status.directory, self.target_filename)
361    if not os.path.isfile(target_filename):
362      return False, 'Cannot find file: ' + target_filename
363    with open(target_filename, 'r') as target_file:
364      file_contents = target_file.read()
365      if isinstance(self.expected_file_contents, str):
366        if file_contents == self.expected_file_contents:
367          return True, ''
368        return False, ('Incorrect file output: \n{act}\n'
369                       'Expected:\n{exp}'
370                       'With diff:\n{diff}'.format(
371                           act=file_contents,
372                           exp=self.expected_file_contents,
373                           diff='\n'.join(
374                               list(
375                                   difflib.unified_diff(
376                                       self.expected_file_contents.split('\n'),
377                                       file_contents.split('\n'),
378                                       fromfile='expected_output',
379                                       tofile='actual_output')))))
380      elif isinstance(self.expected_file_contents, type(re.compile(''))):
381        if self.expected_file_contents.search(file_contents):
382          return True, ''
383        return False, ('Incorrect file output: \n{act}\n'
384                       'Expected matching regex pattern:\n{exp}'.format(
385                           act=file_contents,
386                           exp=self.expected_file_contents.pattern))
387    return False, (
388        'Could not open target file ' + target_filename + ' for reading')
389
390
391class ValidAssemblyFile(SuccessfulReturn, CorrectAssemblyFilePreamble):
392  """Mixin class for checking that every input file generates a valid assembly
393    file following the assembly file naming rule, and there is no output on
394    stdout/stderr."""
395
396  def check_assembly_file_preamble(self, status):
397    for input_filename in status.input_filenames:
398      assembly_filename = get_assembly_filename(input_filename)
399      success, message = self.verify_assembly_file_preamble(
400          os.path.join(status.directory, assembly_filename))
401      if not success:
402        return False, message
403    return True, ''
404
405
406class ValidAssemblyFileWithSubstr(ValidAssemblyFile):
407  """Mixin class for checking that every input file generates a valid assembly
408    file following the assembly file naming rule, there is no output on
409    stdout/stderr, and all assembly files have the given substring specified
410    by expected_assembly_substr.
411
412    To mix in this class, subclasses need to provde expected_assembly_substr
413    as the expected substring.
414    """
415
416  def check_assembly_with_substr(self, status):
417    for input_filename in status.input_filenames:
418      assembly_filename = get_assembly_filename(input_filename)
419      success, message = self.verify_assembly_file_preamble(
420          os.path.join(status.directory, assembly_filename))
421      if not success:
422        return False, message
423      with open(assembly_filename, 'r') as f:
424        content = f.read()
425        if self.expected_assembly_substr not in convert_to_unix_line_endings(
426            content):
427          return False, ('Incorrect assembly output:\n{asm}\n'
428                         'Expected substring not found:\n{exp}'.format(
429                             asm=content, exp=self.expected_assembly_substr))
430    return True, ''
431
432
433class ValidAssemblyFileWithoutSubstr(ValidAssemblyFile):
434  """Mixin class for checking that every input file generates a valid assembly
435    file following the assembly file naming rule, there is no output on
436    stdout/stderr, and no assembly files have the given substring specified
437    by unexpected_assembly_substr.
438
439    To mix in this class, subclasses need to provde unexpected_assembly_substr
440    as the substring we expect not to see.
441    """
442
443  def check_assembly_for_substr(self, status):
444    for input_filename in status.input_filenames:
445      assembly_filename = get_assembly_filename(input_filename)
446      success, message = self.verify_assembly_file_preamble(
447          os.path.join(status.directory, assembly_filename))
448      if not success:
449        return False, message
450      with open(assembly_filename, 'r') as f:
451        content = f.read()
452        if self.unexpected_assembly_substr in convert_to_unix_line_endings(
453            content):
454          return False, ('Incorrect assembly output:\n{asm}\n'
455                         'Unexpected substring found:\n{unexp}'.format(
456                             asm=content, exp=self.unexpected_assembly_substr))
457    return True, ''
458
459
460class ValidNamedAssemblyFile(SuccessfulReturn, CorrectAssemblyFilePreamble):
461  """Mixin class for checking that a list of assembly files with the given
462    names are correctly generated, and there is no output on stdout/stderr.
463
464    To mix in this class, subclasses need to provide expected_assembly_filenames
465    as the expected assembly filenames.
466    """
467
468  def check_object_file_preamble(self, status):
469    for assembly_filename in self.expected_assembly_filenames:
470      success, message = self.verify_assembly_file_preamble(
471          os.path.join(status.directory, assembly_filename))
472      if not success:
473        return False, message
474    return True, ''
475
476
477class ErrorMessage(SpirvTest):
478  """Mixin class for tests that fail with a specific error message.
479
480    To mix in this class, subclasses need to provide expected_error as the
481    expected error message.
482
483    The test should fail if the subprocess was terminated by a signal.
484    """
485
486  def check_has_error_message(self, status):
487    if not status.returncode:
488      return False, ('Expected error message, but returned success from '
489                     'command execution')
490    if status.returncode < 0:
491      # On Unix, a negative value -N for Popen.returncode indicates
492      # termination by signal N.
493      # https://docs.python.org/2/library/subprocess.html
494      return False, ('Expected error message, but command was terminated by '
495                     'signal ' + str(status.returncode))
496    if not status.stderr:
497      return False, 'Expected error message, but no output on stderr'
498    if self.expected_error != convert_to_unix_line_endings(status.stderr):
499      return False, ('Incorrect stderr output:\n{act}\n'
500                     'Expected:\n{exp}'.format(
501                         act=status.stderr, exp=self.expected_error))
502    return True, ''
503
504
505class ErrorMessageSubstr(SpirvTest):
506  """Mixin class for tests that fail with a specific substring in the error
507    message.
508
509    To mix in this class, subclasses need to provide expected_error_substr as
510    the expected error message substring.
511
512    The test should fail if the subprocess was terminated by a signal.
513    """
514
515  def check_has_error_message_as_substring(self, status):
516    if not status.returncode:
517      return False, ('Expected error message, but returned success from '
518                     'command execution')
519    if status.returncode < 0:
520      # On Unix, a negative value -N for Popen.returncode indicates
521      # termination by signal N.
522      # https://docs.python.org/2/library/subprocess.html
523      return False, ('Expected error message, but command was terminated by '
524                     'signal ' + str(status.returncode))
525    if not status.stderr:
526      return False, 'Expected error message, but no output on stderr'
527    if self.expected_error_substr not in convert_to_unix_line_endings(
528        status.stderr):
529      return False, ('Incorrect stderr output:\n{act}\n'
530                     'Expected substring not found in stderr:\n{exp}'.format(
531                         act=status.stderr, exp=self.expected_error_substr))
532    return True, ''
533
534
535class WarningMessage(SpirvTest):
536  """Mixin class for tests that succeed but have a specific warning message.
537
538    To mix in this class, subclasses need to provide expected_warning as the
539    expected warning message.
540    """
541
542  def check_has_warning_message(self, status):
543    if status.returncode:
544      return False, ('Expected warning message, but returned failure from'
545                     ' command execution')
546    if not status.stderr:
547      return False, 'Expected warning message, but no output on stderr'
548    if self.expected_warning != convert_to_unix_line_endings(status.stderr):
549      return False, ('Incorrect stderr output:\n{act}\n'
550                     'Expected:\n{exp}'.format(
551                         act=status.stderr, exp=self.expected_warning))
552    return True, ''
553
554
555class ValidObjectFileWithWarning(NoOutputOnStdout, CorrectObjectFilePreamble,
556                                 WarningMessage):
557  """Mixin class for checking that every input file generates a valid object
558    file following the object file naming rule, with a specific warning message.
559    """
560
561  def check_object_file_preamble(self, status):
562    for input_filename in status.input_filenames:
563      object_filename = get_object_filename(input_filename)
564      success, message = self.verify_object_file_preamble(
565          os.path.join(status.directory, object_filename))
566      if not success:
567        return False, message
568    return True, ''
569
570
571class ValidAssemblyFileWithWarning(NoOutputOnStdout,
572                                   CorrectAssemblyFilePreamble, WarningMessage):
573  """Mixin class for checking that every input file generates a valid assembly
574    file following the assembly file naming rule, with a specific warning
575    message."""
576
577  def check_assembly_file_preamble(self, status):
578    for input_filename in status.input_filenames:
579      assembly_filename = get_assembly_filename(input_filename)
580      success, message = self.verify_assembly_file_preamble(
581          os.path.join(status.directory, assembly_filename))
582      if not success:
583        return False, message
584    return True, ''
585
586
587class StdoutMatch(SpirvTest):
588  """Mixin class for tests that can expect output on stdout.
589
590    To mix in this class, subclasses need to provide expected_stdout as the
591    expected stdout output.
592
593    For expected_stdout, if it's True, then they expect something on stdout but
594    will not check what it is. If it's a string, expect an exact match.  If it's
595    anything else, it is assumed to be a compiled regular expression which will
596    be matched against re.search(). It will expect
597    expected_stdout.search(status.stdout) to be true.
598    """
599
600  def check_stdout_match(self, status):
601    # "True" in this case means we expect something on stdout, but we do not
602    # care what it is, we want to distinguish this from "blah" which means we
603    # expect exactly the string "blah".
604    if self.expected_stdout is True:
605      if not status.stdout:
606        return False, 'Expected something on stdout'
607    elif type(self.expected_stdout) == str:
608      if self.expected_stdout != convert_to_unix_line_endings(status.stdout):
609        return False, ('Incorrect stdout output:\n{ac}\n'
610                       'Expected:\n{ex}'.format(
611                           ac=status.stdout, ex=self.expected_stdout))
612    else:
613      converted = convert_to_unix_line_endings(status.stdout)
614      if not self.expected_stdout.search(converted):
615        return False, ('Incorrect stdout output:\n{ac}\n'
616                       'Expected to match regex:\n{ex}'.format(
617                           ac=status.stdout, ex=self.expected_stdout.pattern))
618    return True, ''
619
620
621class StderrMatch(SpirvTest):
622  """Mixin class for tests that can expect output on stderr.
623
624    To mix in this class, subclasses need to provide expected_stderr as the
625    expected stderr output.
626
627    For expected_stderr, if it's True, then they expect something on stderr,
628    but will not check what it is. If it's a string, expect an exact match.
629    If it's anything else, it is assumed to be a compiled regular expression
630    which will be matched against re.search(). It will expect
631    expected_stderr.search(status.stderr) to be true.
632    """
633
634  def check_stderr_match(self, status):
635    # "True" in this case means we expect something on stderr, but we do not
636    # care what it is, we want to distinguish this from "blah" which means we
637    # expect exactly the string "blah".
638    if self.expected_stderr is True:
639      if not status.stderr:
640        return False, 'Expected something on stderr'
641    elif type(self.expected_stderr) == str:
642      if self.expected_stderr != convert_to_unix_line_endings(status.stderr):
643        return False, ('Incorrect stderr output:\n{ac}\n'
644                       'Expected:\n{ex}'.format(
645                           ac=status.stderr, ex=self.expected_stderr))
646    else:
647      if not self.expected_stderr.search(
648          convert_to_unix_line_endings(status.stderr)):
649        return False, ('Incorrect stderr output:\n{ac}\n'
650                       'Expected to match regex:\n{ex}'.format(
651                           ac=status.stderr, ex=self.expected_stderr.pattern))
652    return True, ''
653
654
655class StdoutNoWiderThan80Columns(SpirvTest):
656  """Mixin class for tests that require stdout to 80 characters or narrower.
657
658    To mix in this class, subclasses need to provide expected_stdout as the
659    expected stdout output.
660    """
661
662  def check_stdout_not_too_wide(self, status):
663    if not status.stdout:
664      return True, ''
665    else:
666      for line in status.stdout.splitlines():
667        if len(line) > 80:
668          return False, ('Stdout line longer than 80 columns: %s' % line)
669    return True, ''
670
671
672class NoObjectFile(SpirvTest):
673  """Mixin class for checking that no input file has a corresponding object
674    file."""
675
676  def check_no_object_file(self, status):
677    for input_filename in status.input_filenames:
678      object_filename = get_object_filename(input_filename)
679      full_object_file = os.path.join(status.directory, object_filename)
680      print('checking %s' % full_object_file)
681      if os.path.isfile(full_object_file):
682        return False, (
683            'Expected no object file, but found: %s' % full_object_file)
684    return True, ''
685
686
687class NoNamedOutputFiles(SpirvTest):
688  """Mixin class for checking that no specified output files exist.
689
690    The expected_output_filenames member should be full pathnames."""
691
692  def check_no_named_output_files(self, status):
693    for object_filename in self.expected_output_filenames:
694      if os.path.isfile(object_filename):
695        return False, (
696            'Expected no output file, but found: %s' % object_filename)
697    return True, ''
698
699
700class ExecutedListOfPasses(SpirvTest):
701  """Mixin class for checking that a list of passes where executed.
702
703  It works by analyzing the output of the --print-all flag to spirv-opt.
704
705  For this mixin to work, the class member expected_passes should be a sequence
706  of pass names as returned by Pass::name().
707  """
708
709  def check_list_of_executed_passes(self, status):
710    # Collect all the output lines containing a pass name.
711    pass_names = []
712    pass_name_re = re.compile(r'.*IR before pass (?P<pass_name>[\S]+)')
713    for line in status.stderr.splitlines():
714      match = pass_name_re.match(line)
715      if match:
716        pass_names.append(match.group('pass_name'))
717
718    for (expected, actual) in zip(self.expected_passes, pass_names):
719      if expected != actual:
720        return False, (
721            'Expected pass "%s" but found pass "%s"\n' % (expected, actual))
722
723    return True, ''
724