1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
|
#!/usr/bin/env python
#
# Permission is hereby granted, free of charge, to any person
# obtaining a copy of this software and associated documentation
# files (the "Software"), to deal in the Software without
# restriction, including without limitation the rights to use,
# copy, modify, merge, publish, distribute, sublicense, and/or
# sell copies of the Software, and to permit persons to whom the
# Software is furnished to do so, subject to the following
# conditions:
#
# This permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
# PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR(S) BE
# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
# DEALINGS IN THE SOFTWARE.
"""This module enables the running of GLSL parser tests.
This module can be used to add parser tests to a Piglit test group or to run
standalone tests on the command line. To add a test to a Piglit group, us
``add_glsl_parser_test()``. To run a single standalone test, execute
``glsl_parser_test.py TEST_FILE``.
"""
usage_message = "usage: glsl_parser_test.py TEST_FILE"
import ConfigParser
import os
import os.path as path
import re
import subprocess
import sys
from ConfigParser import SafeConfigParser
from core import Test, testBinDir, TestResult
from cStringIO import StringIO
from exectest import PlainExecTest
def add_glsl_parser_test(group, filepath, test_name):
"""Add an instance of GLSLParserTest to the given group."""
group[test_name] = GLSLParserTest(filepath)
def import_glsl_parser_tests(group, basepath, subdirectories):
"""
Recursively register each shader source file in the given
``subdirectories`` as a GLSLParserTest .
:subdirectories: A list of subdirectories under the basepath.
The name with which each test is registered into the given group is
the shader source file's path relative to ``basepath``. For example,
if::
import_glsl_parser_tests(group, 'a', ['b1', 'b2'])
is called and the file 'a/b/c/d.frag' exists, then the test is
registered into the group as ``group['b1/c/d.frag']``.
"""
for d in subdirectories:
walk_dir = path.join(basepath, d)
for (dirpath, dirnames, filenames) in os.walk(walk_dir):
# Ignore dirnames.
for f in filenames:
# Add f as a test if its file extension is good.
ext = f.rsplit('.')[-1]
if ext in ['vert', 'geom', 'frag']:
filepath = path.join(dirpath, f)
# testname := filepath with initial
# three directories removed.
testname = '/'.join(filepath.split(os.sep)[3:])
assert(type(testname) is str)
add_glsl_parser_test(
group,
filepath,
testname)
class GLSLParserTest(PlainExecTest):
"""Test for the GLSL parser (and more) on a GLSL source file.
This test takes a GLSL source file and passes it to the executable
``glslparsertest``. The GLSL source file being tested must have a GLSL
file extension: one of ``.vert``, ``.geom``, or ``.frag``. The test file
must have a properly formatted comment section containing configuration
data (see below).
For example test files, see the directory
'piglit.repo/examples/glsl_parser_text`.
Quirks
------
It is not completely corect to state that this is a test for the GLSL
parser, because it also tests later compilation stages, such as AST
construction and static type checking. Specifically, this tests the
success of the executable ``glslparsertest``, which in turn tests the
success of the native function ``glCompileShader()``.
Config Section
--------------
The GLSL source file must contain a special config section in its
comments. This section can appear anywhere in the file: above,
within, or below the actual GLSL source code. The syntax of the config
section is essentially the syntax of
``ConfigParser.SafeConfigParser``.
The beginning of the config section is marked by a comment line that
contains only '[config]'. The end of the config section is marked by
a comment line that contains only '[end config]'. All intervening
lines become the text of the config section.
Whitespace is significant, because ``ConfigParser`` treats it so. The
config text of each non-empty line begins on the same column as the
``[`` in the ``[config]`` line. (A line is considered empty if it
contains whitespace and an optional C comment marker: ``//``, ``*``,
``/*``). Therefore, option names must be aligned on this column. Text
that begins to the right of this column is considered to be a line
continuation.
Required Options
----------------
* glsl_version: A valid GLSL version number, such as 1.10.
* expect_result: Either ``pass`` or ``fail``.
Nonrequired Options
-------------------
* require_extensions: List of GL extensions. If an extension is not
supported, the test is skipped. Each extension name must begin
with GL and elements are separated by whitespace.
Examples
--------
::
// [config]
// glsl_version: 1.30
// expect_result: pass
// # Lists may be single-line.
// require_extensions: GL_ARB_fragment_coord_conventions GL_AMD_conservative_depth
// [end config]
::
/* [config]
* glsl_version: 1.30
* expect_result: pass
* # Lists may be span multiple lines.
* required_extensions:
* GL_ARB_fragment_coord_conventions
* GL_AMD_conservative_depth
* [end config]
*/
::
/*
[config]
glsl_version: 1.30
expect_result: pass
[end config]
*/
An incorrect example, where text is not properly aligned::
/* [config]
glsl_version: 1.30
expect_result: pass
[end config]
*/
Another alignment problem::
// [config]
// glsl_version: 1.30
// expect_result: pass
// [end config]
"""
__required_opts = [
'expect_result',
'glsl_version'
]
__config_defaults = {
'require_extensions' : '',
}
def __init__(self, filepath, runConcurrent = True):
"""
:filepath: Must end in one '.vert', '.geom', or '.frag'.
"""
Test.__init__(self, runConcurrent)
self.__config = None
self.__command = None
self.__filepath = filepath
self.result = None
def __get_config(self):
"""Extract the config section from the test file.
Set ``self.__cached_config``. If the config section is missing
or invalid, or any other errors occur, then set ``self.result``
to failure.
:return: None
"""
cls = self.__class__
# Text of config section.
text_io = StringIO()
# Parsing state.
PARSE_FIND_START = 0
PARSE_IN_CONFIG = 1
PARSE_DONE = 2
PARSE_ERROR = 3
parse_state = PARSE_FIND_START
# Regexen that change parser state.
start = re.compile(r'\A(?P<indent>\s*(|//|/\*|\*)\s*)(?P<content>\[config\]\s*\n)\Z')
empty = None # Empty line in config body.
internal = None # Non-empty line in config body.
end = None # Marks end of config body.
try:
f = open(self.__filepath, 'r')
except IOError:
self.result = TestResult()
self.result['result'] = 'fail'
self.result['errors'] = ["Failed to open test file '{0}'".format(self.__filepath)]
return
for line in f:
if parse_state == PARSE_FIND_START:
m = start.match(line)
if m is not None:
parse_state = PARSE_IN_CONFIG
text_io.write(m.group('content'))
indent = '.' * len(m.group('indent'))
empty = re.compile(r'\A\s*(|//|/\*|\*)\s*\n\Z')
internal = re.compile(r'\A{indent}(?P<content>.*\n)\Z'.format(indent=indent))
end = re.compile(r'\A{indent}\[end( |_)config\]\s*\n\Z'.format(indent=indent))
elif parse_state == PARSE_IN_CONFIG:
if start.match(line) is not None:
parse_state = PARSE_ERROR
break
if end.match(line) is not None:
parse_state = PARSE_DONE
break
m = internal.match(line)
if m is not None:
text_io.write(m.group('content'))
continue
m = empty.match(line)
if m is not None:
text_io.write('\n')
continue
parse_state = PARSE_ERROR
break
else:
assert(False)
if parse_state == PARSE_DONE:
pass
elif parse_state == PARSE_FIND_START:
self.result = TestResult()
self.result['result'] = 'fail'
self.result['errors'] = ["Config section of test file '{0}' is missing".format(self.__filepath)]
self.result['errors'] += ["Failed to find initial line of config section '// [config]'"]
self.result['note'] = "See the docstring in file '{0}'".format(__file__)
return
elif parse_state == PARSE_IN_CONFIG:
self.result = TestResult()
self.result['result'] = 'fail'
self.result['errors'] = ["Config section of test file '{0}' does not terminate".format(self.__filepath)]
self.result['errors'] += ["Failed to find terminal line of config section '// [end config]'"]
self.result['note'] = "See the docstring in file '{0}'".format(__file__)
return
elif parse_state == PARSE_ERROR:
self.result = TestResult()
self.result['result'] = 'fail'
self.result['errors'] = ["Config section of test file '{0}' is ill formed, most likely due to whitespace".format(self.__filepath)]
self.result['note'] = "See the docstring in file '{0}'".format(__file__)
return
else:
assert(False)
config = ConfigParser.SafeConfigParser(cls.__config_defaults)
try:
text = text_io.getvalue()
text_io.close()
config.readfp(StringIO(text))
except ConfigParser.Error as e:
self.result = TestResult()
self.result['result'] = 'fail'
self.result['errors'] = ['Errors exist in config section of test file']
self.result['errors'] += [e.message]
self.result['note'] = "See the docstring in file '{0}'".format(__file__)
return
self.__config = config
def __validate_config(self):
"""Validate config.
Check that that all required options are present. If
validation fails, set ``self.result`` to failure.
Currently, this function does not validate the options'
values.
:return: None
"""
cls = self.__class__
if self.__config is None:
return
for o in cls.__required_opts:
if not self.__config.has_option('config', o):
self.result = TestResult()
self.result['result'] = 'fail'
self.result['errors'] = ['Errors exist in config section of test file']
self.result['errors'] += ["Option '{0}' is required".format(o)]
self.result['note'] = "See the docstring in file '{0}'".format(__file__)
return
def run_standalone(self):
"""Run the test as a standalone process outside of Piglit."""
if self.result is not None:
sys.stdout.write(self.result)
sys.exit(1)
assert(self.command is not None)
env = os.environ.copy()
for e in self.env:
env[e] = str(self.env[e])
p = subprocess.Popen(self.command, env=env)
p.communicate()
@property
def config(self):
if self.__config is None:
self.__get_config()
self.__validate_config()
return self.__config
@property
def command(self):
"""Command line arguments for 'glslparsertest'.
The command line arguments are constructed by parsing the
config section of the test file. If any errors are present in
the config section, then ``self.result`` is set to failure and
this returns ``None``.
:return: [str] or None
"""
if self.result is not None:
return None
assert(self.config is not None)
command = [
path.join(testBinDir, 'glslparsertest'),
self.__filepath,
self.config.get('config', 'expect_result'),
self.config.get('config', 'glsl_version')
]
command += self.config.get('config', 'require_extensions').split()
return command
@property
def env(self):
return dict()
if __name__ == '__main__':
if len(sys.argv) != 2:
sys.stderr.write("{0}: usage error\n\n".format(sys.argv[0]))
sys.stderr.write(usage_message)
test_file = sys.argv[1]
test = GLSLParserTest(test_file)
test.run_standalone()
# vim: noet ts=8 sw=8:
|