Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
PojavLauncherTeam
GitHub Repository: PojavLauncherTeam/shaderc
Path: blob/main/glslc/test/glslc_test_framework.py
1560 views
1
#!/usr/bin/env python
2
# Copyright 2015 The Shaderc Authors. All rights reserved.
3
#
4
# Licensed under the Apache License, Version 2.0 (the "License");
5
# you may not use this file except in compliance with the License.
6
# You may obtain a copy of the License at
7
#
8
# http://www.apache.org/licenses/LICENSE-2.0
9
#
10
# Unless required by applicable law or agreed to in writing, software
11
# distributed under the License is distributed on an "AS IS" BASIS,
12
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13
# See the License for the specific language governing permissions and
14
# limitations under the License.
15
16
"""Manages and runs tests from the current working directory.
17
18
This will traverse the current working directory and look for python files that
19
contain subclasses of GlslCTest.
20
21
If a class has an @inside_glslc_testsuite decorator, an instance of that
22
class will be created and serve as a test case in that testsuite. The test
23
case is then run by the following steps:
24
25
1. A temporary directory will be created.
26
2. The glslc_args member variable will be inspected and all placeholders in it
27
will be expanded by calling instantiate_for_glslc_args() on placeholders.
28
The transformed list elements are then supplied as glslc arguments.
29
3. If the environment member variable exists, its write() method will be
30
invoked.
31
4. All expected_* member variables will be inspected and all placeholders in
32
them will be expanded by calling instantiate_for_expectation() on those
33
placeholders. After placeholder expansion, if the expected_* variable is
34
a list, its element will be joined together with '' to form a single
35
string. These expected_* variables are to be used by the check_*() methods.
36
5. glslc will be run with the arguments supplied in glslc_args.
37
6. All check_*() member methods will be called by supplying a TestStatus as
38
argument. Each check_*() method is expected to return a (Success, Message)
39
pair where Success is a boolean indicating success and Message is an error
40
message.
41
7. If any check_*() method fails, the error message is outputted and the
42
current test case fails.
43
44
If --leave-output was not specified, all temporary files and directories will
45
be deleted.
46
"""
47
48
import argparse
49
import fnmatch
50
import inspect
51
import os
52
import shutil
53
import subprocess
54
import sys
55
import tempfile
56
from collections import defaultdict
57
from placeholder import PlaceHolder
58
59
60
EXPECTED_BEHAVIOR_PREFIX = 'expected_'
61
VALIDATE_METHOD_PREFIX = 'check_'
62
63
64
def get_all_variables(instance):
65
"""Returns the names of all the variables in instance."""
66
return [v for v in dir(instance) if not callable(getattr(instance, v))]
67
68
69
def get_all_methods(instance):
70
"""Returns the names of all methods in instance."""
71
return [m for m in dir(instance) if callable(getattr(instance, m))]
72
73
74
def get_all_superclasses(cls):
75
"""Returns all superclasses of a given class. Omits root 'object' superclass.
76
77
Returns:
78
A list of superclasses of the given class. The order guarantees that
79
* A Base class precedes its derived classes, e.g., for "class B(A)", it
80
will be [..., A, B, ...].
81
* When there are multiple base classes, base classes declared first
82
precede those declared later, e.g., for "class C(A, B), it will be
83
[..., A, B, C, ...]
84
"""
85
classes = []
86
for superclass in cls.__bases__:
87
for c in get_all_superclasses(superclass):
88
if c is not object and c not in classes:
89
classes.append(c)
90
for superclass in cls.__bases__:
91
if superclass is not object and superclass not in classes:
92
classes.append(superclass)
93
return classes
94
95
96
def get_all_test_methods(test_class):
97
"""Gets all validation methods.
98
99
Returns:
100
A list of validation methods. The order guarantees that
101
* A method defined in superclass precedes one defined in subclass,
102
e.g., for "class A(B)", methods defined in B precedes those defined
103
in A.
104
* If a subclass has more than one superclass, e.g., "class C(A, B)",
105
then methods defined in A precedes those defined in B.
106
"""
107
classes = get_all_superclasses(test_class)
108
classes.append(test_class)
109
all_tests = [m for c in classes
110
for m in get_all_methods(c)
111
if m.startswith(VALIDATE_METHOD_PREFIX)]
112
unique_tests = []
113
for t in all_tests:
114
if t not in unique_tests:
115
unique_tests.append(t)
116
return unique_tests
117
118
119
class GlslCTest:
120
"""Base class for glslc test cases.
121
122
Subclasses define test cases' facts (shader source code, glslc command,
123
result validation), which will be used by the TestCase class for running
124
tests. Subclasses should define glslc_args (specifying glslc command
125
arguments), and at least one check_*() method (for result validation) for
126
a full-fledged test case. All check_*() methods should take a TestStatus
127
parameter and return a (Success, Message) pair, in which Success is a
128
boolean indicating success and Message is an error message. The test passes
129
iff all check_*() methods returns true.
130
131
Often, a test case class will delegate the check_* behaviors by inheriting
132
from other classes.
133
"""
134
135
def name(self):
136
return self.__class__.__name__
137
138
139
class TestStatus:
140
"""A struct for holding run status of a test case."""
141
142
def __init__(self, test_manager, returncode, stdout, stderr, directory, inputs, input_filenames):
143
self.test_manager = test_manager
144
self.returncode = returncode
145
self.stdout = stdout
146
self.stderr = stderr
147
# temporary directory where the test runs
148
self.directory = directory
149
# List of inputs, as PlaceHolder objects.
150
self.inputs = inputs
151
# the names of input shader files (potentially including paths)
152
self.input_filenames = input_filenames
153
154
155
class GlslCTestException(Exception):
156
"""GlslCTest exception class."""
157
pass
158
159
160
def inside_glslc_testsuite(testsuite_name):
161
"""Decorator for subclasses of GlslCTest.
162
163
This decorator checks that a class meets the requirements (see below)
164
for a test case class, and then puts the class in a certain testsuite.
165
* The class needs to be a subclass of GlslCTest.
166
* The class needs to have glslc_args defined as a list.
167
* The class needs to define at least one check_*() methods.
168
* All expected_* variables required by check_*() methods can only be
169
of bool, str, or list type.
170
* Python runtime will throw an exception if the expected_* member
171
attributes required by check_*() methods are missing.
172
"""
173
def actual_decorator(cls):
174
if not inspect.isclass(cls):
175
raise GlslCTestException('Test case should be a class')
176
if not issubclass(cls, GlslCTest):
177
raise GlslCTestException(
178
'All test cases should be subclasses of GlslCTest')
179
if 'glslc_args' not in get_all_variables(cls):
180
raise GlslCTestException('No glslc_args found in the test case')
181
if not isinstance(cls.glslc_args, list):
182
raise GlslCTestException('glslc_args needs to be a list')
183
if not any([
184
m.startswith(VALIDATE_METHOD_PREFIX)
185
for m in get_all_methods(cls)]):
186
raise GlslCTestException(
187
'No check_*() methods found in the test case')
188
if not all([
189
isinstance(v, (bool, str, list))
190
for v in get_all_variables(cls)]):
191
raise GlslCTestException(
192
'expected_* variables are only allowed to be bool, str, or '
193
'list type.')
194
cls.parent_testsuite = testsuite_name
195
return cls
196
return actual_decorator
197
198
199
class TestManager:
200
"""Manages and runs a set of tests."""
201
202
def __init__(self, executable_path, disassembler_path):
203
self.executable_path = executable_path
204
self.disassembler_path = disassembler_path
205
self.num_successes = 0
206
self.num_failures = 0
207
self.num_tests = 0
208
self.leave_output = False
209
self.tests = defaultdict(list)
210
211
def notify_result(self, test_case, success, message):
212
"""Call this to notify the manager of the results of a test run."""
213
self.num_successes += 1 if success else 0
214
self.num_failures += 0 if success else 1
215
counter_string = str(
216
self.num_successes + self.num_failures) + '/' + str(self.num_tests)
217
print('%-10s %-40s ' % (counter_string, test_case.test.name()) +
218
('Passed' if success else '-Failed-'))
219
if not success:
220
print(' '.join(test_case.command))
221
print(message)
222
223
def add_test(self, testsuite, test):
224
"""Add this to the current list of test cases."""
225
self.tests[testsuite].append(TestCase(test, self))
226
self.num_tests += 1
227
228
def run_tests(self):
229
for suite in self.tests:
230
print('Glslc test suite: "{suite}"'.format(suite=suite))
231
for x in self.tests[suite]:
232
x.runTest()
233
234
235
class TestCase:
236
"""A single test case that runs in its own directory."""
237
238
def __init__(self, test, test_manager):
239
self.test = test
240
self.test_manager = test_manager
241
self.inputs = [] # inputs, as PlaceHolder objects.
242
self.file_shaders = [] # filenames of shader files.
243
self.stdin_shader = None # text to be passed to glslc as stdin
244
245
def setUp(self):
246
"""Creates environment and instantiates placeholders for the test case."""
247
248
self.directory = tempfile.mkdtemp(dir=os.getcwd())
249
glslc_args = self.test.glslc_args
250
# Instantiate placeholders in glslc_args
251
self.test.glslc_args = [
252
arg.instantiate_for_glslc_args(self)
253
if isinstance(arg, PlaceHolder) else arg
254
for arg in self.test.glslc_args]
255
# Get all shader files' names
256
self.inputs = [arg for arg in glslc_args if isinstance(arg, PlaceHolder)]
257
self.file_shaders = [arg.filename for arg in self.inputs]
258
259
if 'environment' in get_all_variables(self.test):
260
self.test.environment.write(self.directory)
261
262
expectations = [v for v in get_all_variables(self.test)
263
if v.startswith(EXPECTED_BEHAVIOR_PREFIX)]
264
# Instantiate placeholders in expectations
265
for expectation_name in expectations:
266
expectation = getattr(self.test, expectation_name)
267
if isinstance(expectation, list):
268
expanded_expections = [
269
element.instantiate_for_expectation(self)
270
if isinstance(element, PlaceHolder) else element
271
for element in expectation]
272
setattr(
273
self.test, expectation_name,
274
''.join(expanded_expections))
275
elif isinstance(expectation, PlaceHolder):
276
setattr(self.test, expectation_name,
277
expectation.instantiate_for_expectation(self))
278
279
280
def tearDown(self):
281
"""Removes the directory if we were not instructed to do otherwise."""
282
if not self.test_manager.leave_output:
283
shutil.rmtree(self.directory)
284
285
def runTest(self):
286
"""Sets up and runs a test, reports any failures and then cleans up."""
287
self.setUp()
288
success = False
289
message = ''
290
try:
291
self.command = [self.test_manager.executable_path]
292
self.command.extend(self.test.glslc_args)
293
294
process = subprocess.Popen(
295
args=self.command, stdin=subprocess.PIPE,
296
stdout=subprocess.PIPE, stderr=subprocess.PIPE,
297
cwd=self.directory)
298
output = process.communicate(self.stdin_shader)
299
test_status = TestStatus(
300
self.test_manager,
301
process.returncode, output[0], output[1],
302
self.directory, self.inputs, self.file_shaders)
303
run_results = [getattr(self.test, test_method)(test_status)
304
for test_method in get_all_test_methods(
305
self.test.__class__)]
306
success, message = list(zip(*run_results))
307
success = all(success)
308
message = '\n'.join(message)
309
except Exception as e:
310
success = False
311
message = str(e)
312
self.test_manager.notify_result(self, success, message)
313
self.tearDown()
314
315
316
def main():
317
parser = argparse.ArgumentParser()
318
parser.add_argument('glslc', metavar='path/to/glslc', type=str, nargs=1,
319
help='Path to glslc')
320
parser.add_argument('spirvdis', metavar='path/to/glslc', type=str, nargs=1,
321
help='Path to spirv-dis')
322
parser.add_argument('--leave-output', action='store_const', const=1,
323
help='Do not clean up temporary directories')
324
parser.add_argument('--test-dir', nargs=1,
325
help='Directory to gather the tests from')
326
args = parser.parse_args()
327
default_path = sys.path
328
root_dir = os.getcwd()
329
if args.test_dir:
330
root_dir = args.test_dir[0]
331
manager = TestManager(args.glslc[0], args.spirvdis[0])
332
if args.leave_output:
333
manager.leave_output = True
334
for root, _, filenames in os.walk(root_dir):
335
for filename in fnmatch.filter(filenames, '*.py'):
336
if filename.endswith('unittest.py'):
337
# Skip unit tests, which are for testing functions of
338
# the test framework.
339
continue
340
sys.path = default_path
341
sys.path.append(root)
342
try:
343
mod = __import__(os.path.splitext(filename)[0])
344
for _, obj, in inspect.getmembers(mod):
345
if inspect.isclass(obj) and hasattr(obj, 'parent_testsuite'):
346
manager.add_test(obj.parent_testsuite, obj())
347
except:
348
print("Failed to load " + filename)
349
raise
350
manager.run_tests()
351
if manager.num_failures > 0:
352
sys.exit(-1)
353
354
if __name__ == '__main__':
355
main()
356
357