Path: blob/main/glslc/test/glslc_test_framework.py
1560 views
#!/usr/bin/env python1# Copyright 2015 The Shaderc Authors. All rights reserved.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 at6#7# http://www.apache.org/licenses/LICENSE-2.08#9# Unless required by applicable law or agreed to in writing, software10# 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 and13# limitations under the License.1415"""Manages and runs tests from the current working directory.1617This will traverse the current working directory and look for python files that18contain subclasses of GlslCTest.1920If a class has an @inside_glslc_testsuite decorator, an instance of that21class will be created and serve as a test case in that testsuite. The test22case is then run by the following steps:23241. A temporary directory will be created.252. The glslc_args member variable will be inspected and all placeholders in it26will be expanded by calling instantiate_for_glslc_args() on placeholders.27The transformed list elements are then supplied as glslc arguments.283. If the environment member variable exists, its write() method will be29invoked.304. All expected_* member variables will be inspected and all placeholders in31them will be expanded by calling instantiate_for_expectation() on those32placeholders. After placeholder expansion, if the expected_* variable is33a list, its element will be joined together with '' to form a single34string. These expected_* variables are to be used by the check_*() methods.355. glslc will be run with the arguments supplied in glslc_args.366. All check_*() member methods will be called by supplying a TestStatus as37argument. Each check_*() method is expected to return a (Success, Message)38pair where Success is a boolean indicating success and Message is an error39message.407. If any check_*() method fails, the error message is outputted and the41current test case fails.4243If --leave-output was not specified, all temporary files and directories will44be deleted.45"""4647import argparse48import fnmatch49import inspect50import os51import shutil52import subprocess53import sys54import tempfile55from collections import defaultdict56from placeholder import PlaceHolder575859EXPECTED_BEHAVIOR_PREFIX = 'expected_'60VALIDATE_METHOD_PREFIX = 'check_'616263def get_all_variables(instance):64"""Returns the names of all the variables in instance."""65return [v for v in dir(instance) if not callable(getattr(instance, v))]666768def get_all_methods(instance):69"""Returns the names of all methods in instance."""70return [m for m in dir(instance) if callable(getattr(instance, m))]717273def get_all_superclasses(cls):74"""Returns all superclasses of a given class. Omits root 'object' superclass.7576Returns:77A list of superclasses of the given class. The order guarantees that78* A Base class precedes its derived classes, e.g., for "class B(A)", it79will be [..., A, B, ...].80* When there are multiple base classes, base classes declared first81precede those declared later, e.g., for "class C(A, B), it will be82[..., A, B, C, ...]83"""84classes = []85for superclass in cls.__bases__:86for c in get_all_superclasses(superclass):87if c is not object and c not in classes:88classes.append(c)89for superclass in cls.__bases__:90if superclass is not object and superclass not in classes:91classes.append(superclass)92return classes939495def get_all_test_methods(test_class):96"""Gets all validation methods.9798Returns:99A list of validation methods. The order guarantees that100* A method defined in superclass precedes one defined in subclass,101e.g., for "class A(B)", methods defined in B precedes those defined102in A.103* If a subclass has more than one superclass, e.g., "class C(A, B)",104then methods defined in A precedes those defined in B.105"""106classes = get_all_superclasses(test_class)107classes.append(test_class)108all_tests = [m for c in classes109for m in get_all_methods(c)110if m.startswith(VALIDATE_METHOD_PREFIX)]111unique_tests = []112for t in all_tests:113if t not in unique_tests:114unique_tests.append(t)115return unique_tests116117118class GlslCTest:119"""Base class for glslc test cases.120121Subclasses define test cases' facts (shader source code, glslc command,122result validation), which will be used by the TestCase class for running123tests. Subclasses should define glslc_args (specifying glslc command124arguments), and at least one check_*() method (for result validation) for125a full-fledged test case. All check_*() methods should take a TestStatus126parameter and return a (Success, Message) pair, in which Success is a127boolean indicating success and Message is an error message. The test passes128iff all check_*() methods returns true.129130Often, a test case class will delegate the check_* behaviors by inheriting131from other classes.132"""133134def name(self):135return self.__class__.__name__136137138class TestStatus:139"""A struct for holding run status of a test case."""140141def __init__(self, test_manager, returncode, stdout, stderr, directory, inputs, input_filenames):142self.test_manager = test_manager143self.returncode = returncode144self.stdout = stdout145self.stderr = stderr146# temporary directory where the test runs147self.directory = directory148# List of inputs, as PlaceHolder objects.149self.inputs = inputs150# the names of input shader files (potentially including paths)151self.input_filenames = input_filenames152153154class GlslCTestException(Exception):155"""GlslCTest exception class."""156pass157158159def inside_glslc_testsuite(testsuite_name):160"""Decorator for subclasses of GlslCTest.161162This decorator checks that a class meets the requirements (see below)163for a test case class, and then puts the class in a certain testsuite.164* The class needs to be a subclass of GlslCTest.165* The class needs to have glslc_args defined as a list.166* The class needs to define at least one check_*() methods.167* All expected_* variables required by check_*() methods can only be168of bool, str, or list type.169* Python runtime will throw an exception if the expected_* member170attributes required by check_*() methods are missing.171"""172def actual_decorator(cls):173if not inspect.isclass(cls):174raise GlslCTestException('Test case should be a class')175if not issubclass(cls, GlslCTest):176raise GlslCTestException(177'All test cases should be subclasses of GlslCTest')178if 'glslc_args' not in get_all_variables(cls):179raise GlslCTestException('No glslc_args found in the test case')180if not isinstance(cls.glslc_args, list):181raise GlslCTestException('glslc_args needs to be a list')182if not any([183m.startswith(VALIDATE_METHOD_PREFIX)184for m in get_all_methods(cls)]):185raise GlslCTestException(186'No check_*() methods found in the test case')187if not all([188isinstance(v, (bool, str, list))189for v in get_all_variables(cls)]):190raise GlslCTestException(191'expected_* variables are only allowed to be bool, str, or '192'list type.')193cls.parent_testsuite = testsuite_name194return cls195return actual_decorator196197198class TestManager:199"""Manages and runs a set of tests."""200201def __init__(self, executable_path, disassembler_path):202self.executable_path = executable_path203self.disassembler_path = disassembler_path204self.num_successes = 0205self.num_failures = 0206self.num_tests = 0207self.leave_output = False208self.tests = defaultdict(list)209210def notify_result(self, test_case, success, message):211"""Call this to notify the manager of the results of a test run."""212self.num_successes += 1 if success else 0213self.num_failures += 0 if success else 1214counter_string = str(215self.num_successes + self.num_failures) + '/' + str(self.num_tests)216print('%-10s %-40s ' % (counter_string, test_case.test.name()) +217('Passed' if success else '-Failed-'))218if not success:219print(' '.join(test_case.command))220print(message)221222def add_test(self, testsuite, test):223"""Add this to the current list of test cases."""224self.tests[testsuite].append(TestCase(test, self))225self.num_tests += 1226227def run_tests(self):228for suite in self.tests:229print('Glslc test suite: "{suite}"'.format(suite=suite))230for x in self.tests[suite]:231x.runTest()232233234class TestCase:235"""A single test case that runs in its own directory."""236237def __init__(self, test, test_manager):238self.test = test239self.test_manager = test_manager240self.inputs = [] # inputs, as PlaceHolder objects.241self.file_shaders = [] # filenames of shader files.242self.stdin_shader = None # text to be passed to glslc as stdin243244def setUp(self):245"""Creates environment and instantiates placeholders for the test case."""246247self.directory = tempfile.mkdtemp(dir=os.getcwd())248glslc_args = self.test.glslc_args249# Instantiate placeholders in glslc_args250self.test.glslc_args = [251arg.instantiate_for_glslc_args(self)252if isinstance(arg, PlaceHolder) else arg253for arg in self.test.glslc_args]254# Get all shader files' names255self.inputs = [arg for arg in glslc_args if isinstance(arg, PlaceHolder)]256self.file_shaders = [arg.filename for arg in self.inputs]257258if 'environment' in get_all_variables(self.test):259self.test.environment.write(self.directory)260261expectations = [v for v in get_all_variables(self.test)262if v.startswith(EXPECTED_BEHAVIOR_PREFIX)]263# Instantiate placeholders in expectations264for expectation_name in expectations:265expectation = getattr(self.test, expectation_name)266if isinstance(expectation, list):267expanded_expections = [268element.instantiate_for_expectation(self)269if isinstance(element, PlaceHolder) else element270for element in expectation]271setattr(272self.test, expectation_name,273''.join(expanded_expections))274elif isinstance(expectation, PlaceHolder):275setattr(self.test, expectation_name,276expectation.instantiate_for_expectation(self))277278279def tearDown(self):280"""Removes the directory if we were not instructed to do otherwise."""281if not self.test_manager.leave_output:282shutil.rmtree(self.directory)283284def runTest(self):285"""Sets up and runs a test, reports any failures and then cleans up."""286self.setUp()287success = False288message = ''289try:290self.command = [self.test_manager.executable_path]291self.command.extend(self.test.glslc_args)292293process = subprocess.Popen(294args=self.command, stdin=subprocess.PIPE,295stdout=subprocess.PIPE, stderr=subprocess.PIPE,296cwd=self.directory)297output = process.communicate(self.stdin_shader)298test_status = TestStatus(299self.test_manager,300process.returncode, output[0], output[1],301self.directory, self.inputs, self.file_shaders)302run_results = [getattr(self.test, test_method)(test_status)303for test_method in get_all_test_methods(304self.test.__class__)]305success, message = list(zip(*run_results))306success = all(success)307message = '\n'.join(message)308except Exception as e:309success = False310message = str(e)311self.test_manager.notify_result(self, success, message)312self.tearDown()313314315def main():316parser = argparse.ArgumentParser()317parser.add_argument('glslc', metavar='path/to/glslc', type=str, nargs=1,318help='Path to glslc')319parser.add_argument('spirvdis', metavar='path/to/glslc', type=str, nargs=1,320help='Path to spirv-dis')321parser.add_argument('--leave-output', action='store_const', const=1,322help='Do not clean up temporary directories')323parser.add_argument('--test-dir', nargs=1,324help='Directory to gather the tests from')325args = parser.parse_args()326default_path = sys.path327root_dir = os.getcwd()328if args.test_dir:329root_dir = args.test_dir[0]330manager = TestManager(args.glslc[0], args.spirvdis[0])331if args.leave_output:332manager.leave_output = True333for root, _, filenames in os.walk(root_dir):334for filename in fnmatch.filter(filenames, '*.py'):335if filename.endswith('unittest.py'):336# Skip unit tests, which are for testing functions of337# the test framework.338continue339sys.path = default_path340sys.path.append(root)341try:342mod = __import__(os.path.splitext(filename)[0])343for _, obj, in inspect.getmembers(mod):344if inspect.isclass(obj) and hasattr(obj, 'parent_testsuite'):345manager.add_test(obj.parent_testsuite, obj())346except:347print("Failed to load " + filename)348raise349manager.run_tests()350if manager.num_failures > 0:351sys.exit(-1)352353if __name__ == '__main__':354main()355356357