#!/usr/bin/env python # Copyright 2020 The Tint Authors. # # Licensed under the Apache License, Version 2.0 (the "License"); # you may not use this file except in compliance with the License. # You may obtain a copy of the License at # # http://www.apache.org/licenses/LICENSE-2.0 # # Unless required by applicable law or agreed to in writing, software # distributed under the License is distributed on an "AS IS" BASIS, # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. # See the License for the specific language governing permissions and # limitations under the License. # Test runner for executing a test of tests with Tint. The runner will # find all .wgsl files in the given folder and attempt to convert them # to each of the backend formats. If the file contains a '.fail.' in the # name then the runner will expect the file to fail conversion. import base64 import copy import difflib import optparse import os import platform import re import subprocess import sys import tempfile """ A single test case to be executed. Stores the path to the test file and the result of executing the test. """ class TestCase: def __init__(self, input_path, parse_only): self.input_path = input_path self.parse_only = parse_only self.results = {} def IsExpectedFail(self): fail_re = re.compile('^.+[\.]fail[\.]wgsl') return fail_re.match(self.GetInputPath()) def IsParseOnly(self): return self.parse_only def GetInputPath(self): return self.input_path def GetResult(self, fmt): return self.results[fmt] def GetReason(self): with open(self.GetInputPath()) as test: first_line = test.readline() if (first_line.startswith("# v-")): reason = first_line[2:8] else: reason = '' return reason """ The test runner, will execute a series of test cases and record the results. """ class TestRunner: def RunTest(self, tc): """Runs a single test.""" print("Testing {}".format(tc.GetInputPath())) cmd = [self.options.test_prog_path] if tc.IsParseOnly(): cmd += ['--parse-only'] languages = ["wgsl", "spvasm", "msl", "hlsl"] try: for lang in languages: lang_cmd = copy.copy(cmd) lang_cmd += ['--format', lang] lang_cmd += [tc.GetInputPath()] err = subprocess.check_output(lang_cmd, stderr=subprocess.STDOUT) except Exception as e: failure_reason = "{}".format("".join(map(chr, bytearray(e.output)))) if tc.IsExpectedFail(): right_reason = tc.GetReason() if (right_reason in failure_reason): return False, "" else: return False, right_reason if not tc.IsExpectedFail(): print(failure_reason) print(e) return False, "" return True, "" def RunTests(self): """Runs a set of test cases""" for tc in self.test_cases: result, reason = self.RunTest(tc) """evaluate final result based on result, tc.IsExpectedFail() and reason""" if not result: # result == false, expected true, reason:don't care if not tc.IsExpectedFail(): print("Expected: " + tc.GetInputPath() + " to pass but failed.") self.failures.append(tc.GetInputPath()) # result == false, expected false, reason: wrong else: if reason.startswith("v-"): print("Failed for a wrong reason: " + tc.GetInputPath() + " expected with error code: " + reason) self.failures_wrong_reason.append(tc.GetInputPath()) # result == true, expected false, reason:don't care elif tc.IsExpectedFail(): print("Expected: " + tc.GetInputPath() + " to fail but passed.") self.failures.append(tc.GetInputPath()) def GetUnexpectedFailures(self): for failure in self.failures + self.failures_wrong_reason: if failure not in self.known_failures: self.unexpected_failures.append(failure) return def SummarizeResults(self): """Prints a summarization of the test results to STDOUT""" if len(self.unexpected_failures): self.unexpected_failures.sort() print('\nSummary of unexpected failures:') for unexpected_fail in self.unexpected_failures: print(unexpected_fail) for f in self.known_failures: if f not in self.failures_wrong_reason + self.failures: self.unexpected_successes.append(f) if len(self.unexpected_successes): print('\nSummary of unexpected successes:') for s in self.unexpected_successes: print(s) print('') print('Test cases executed: {}'.format(len(self.test_cases))) print(' Successes: {}'.format( (len(self.test_cases) - len(self.failures) - len(self.failures_wrong_reason)))) print(' Failures: {}'.format( len(self.failures) + len(self.failures_wrong_reason))) print(' Unexpected Failures: {}'.format(len( self.unexpected_failures))) print(' Unexpected Successes: {}'.format( len(self.unexpected_successes))) print('') def Run(self): """Executes the test runner.""" base_path = os.path.abspath( os.path.join(os.path.dirname(__file__), '..')) usage = 'usage: %prog [options] (file)' parser = optparse.OptionParser(usage=usage) parser.add_option('--build-dir', default=os.path.join(base_path, 'out', 'Debug'), help='path to build directory') parser.add_option('--test-dir', default=os.path.join(os.path.dirname(__file__), '..', 'third_party', 'gpuweb-cts', 'src', 'webgpu', 'shader', 'validation', 'wgsl'), help='path to directory containing test files') parser.add_option( '--known-failures-file', default=os.path.join(base_path, 'tools', 'known_tint_failures'), help='path to directory containing the known failures file') parser.add_option( '--test-prog-path', default=None, help='path to program to test (default build-dir/tint)') parser.add_option('--parse-only', action="store_true", default=False, help='only parse test cases; do not compile') self.options, self.args = parser.parse_args() if self.options.test_prog_path == None: test_prog = os.path.abspath( os.path.join(self.options.build_dir, 'tint')) if not os.path.isfile(test_prog): print("Cannot find test program {}".format(test_prog)) return 1 self.options.test_prog_path = test_prog if not os.path.isfile(self.options.test_prog_path): print("Cannot find test program '{}'".format( self.options.test_prog_path)) return 1 input_file_re = re.compile('^.+[\.]wgsl') self.test_cases = [] if self.args: for filename in self.args: input_path = os.path.join(self.options.test_dir, filename) if not os.path.isfile(input_path): print("Cannot find test file '{}'".format(filename)) return 1 self.test_cases.append( TestCase(input_path, self.options.parse_only)) else: for file_dir, _, filename_list in os.walk(self.options.test_dir): for input_filename in filename_list: if input_file_re.match(input_filename): input_path = os.path.join(file_dir, input_filename) if os.path.isfile(input_path): self.test_cases.append( TestCase(input_path, self.options.parse_only)) known_failure_file = self.options.known_failures_file self.known_failures = [] with open(known_failure_file, 'r') as f: for failure_filename in f.read().splitlines(): self.known_failures.append( os.path.join(self.options.test_dir, failure_filename)) self.failures = [] self.failures_wrong_reason = [] self.unexpected_failures = [] self.unexpected_successes = [] self.RunTests() self.GetUnexpectedFailures() self.SummarizeResults() return not len(self.unexpected_failures + self.unexpected_successes) def main(): runner = TestRunner() return runner.Run() if __name__ == '__main__': sys.exit(main())