1# coding=utf-8
2# Copyright (C) 2012, 2014-2016, 2019 Intel Corporation
3#
4# Permission is hereby granted, free of charge, to any person
5# obtaining a copy of this software and associated documentation
6# files (the "Software"), to deal in the Software without
7# restriction, including without limitation the rights to use,
8# copy, modify, merge, publish, distribute, sublicense, and/or
9# sell copies of the Software, and to permit persons to whom the
10# Software is furnished to do so, subject to the following
11# conditions:
12#
13# This permission notice shall be included in all copies or
14# substantial portions of the Software.
15#
16# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
17# KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
18# WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
19# PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE AUTHOR(S) BE
20# LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
21# AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
22# OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
23# DEALINGS IN THE SOFTWARE.
24
25""" This module enables running shader tests. """
26
27import io
28import os
29import re
30
31from framework import exceptions
32from framework import status
33from framework import options
34from .base import ReducedProcessMixin, TestIsSkip
35from .opengl import FastSkipMixin, FastSkip
36from .piglit_test import PiglitBaseTest, ROOT_DIR
37
38__all__ = [
39    'ShaderTest',
40]
41
42
43class Parser(object):
44    """An object responsible for parsing a shader_test file."""
45
46    _is_gl = re.compile(r'GL (<|<=|=|>=|>) \d\.\d')
47    _match_gl_version = re.compile(
48        r'^GL\s+(?P<profile>(ES|CORE|COMPAT))?\s*(?P<op>(<|<=|=|>=|>))\s*(?P<ver>\d\.\d)')
49    _match_glsl_version = re.compile(
50        r'^GLSL\s+(?P<es>ES)?\s*(?P<op>(<|<=|=|>=|>))\s*(?P<ver>\d\.\d+)')
51
52    def __init__(self, filename):
53        self.filename = filename
54        self.extensions = set()
55        self.api_version = 0.0
56        self.shader_version = 0.0
57        self.api = None
58        self.prog = None
59        self.__op = None
60        self.__sl_op = None
61
62    def parse(self):
63        # Iterate over the lines in shader file looking for the config section.
64        # By using a generator this can be split into two for loops at minimal
65        # cost. The first one looks for the start of the config block or raises
66        # an exception. The second looks for the GL version or raises an
67        # exception
68        with io.open(os.path.join(ROOT_DIR, self.filename), 'r', encoding='utf-8') as shader_file:
69            lines = (l for l in shader_file.readlines())
70
71            # Find the config section
72            for line in lines:
73                # We need to find the first line of the configuration file, as
74                # soon as we do then we can move on to getting the
75                # configuration. The first line needs to be parsed by the next
76                # block.
77                if line.startswith('[require]'):
78                    break
79            else:
80                raise exceptions.PiglitFatalError(
81                    "In file {}: Config block not found".format(self.filename))
82
83        for line in lines:
84            if line.startswith('GL_'):
85                line = line.strip()
86                if not (line.startswith('GL_MAX') or line.startswith('GL_NUM')):
87                    self.extensions.add(line)
88                    if line == 'GL_ARB_compatibility':
89                        assert self.api is None or self.api == 'compat'
90                        self.api = 'compat'
91                continue
92
93            # Find any GLES requirements.
94            if not self.api_version:
95                m = self._match_gl_version.match(line)
96                if m:
97                    self.__op = m.group('op')
98                    self.api_version = float(m.group('ver'))
99                    if m.group('profile') == 'ES':
100                        assert self.api is None or self.api == 'gles2'
101                        self.api = 'gles2'
102                    elif m.group('profile') == 'COMPAT':
103                        assert self.api is None or self.api == 'compat'
104                        self.api = 'compat'
105                    elif self.api_version >= 3.1:
106                        assert self.api is None or self.api == 'core'
107                        self.api = 'core'
108                    continue
109
110            if not self.shader_version:
111                # Find any GLSL requirements
112                m = self._match_glsl_version.match(line)
113                if m:
114                    self.__sl_op = m.group('op')
115                    self.shader_version = float(m.group('ver'))
116                    if m.group('es'):
117                        assert self.api is None or self.api == 'gles2'
118                        self.api = 'gles2'
119                    continue
120
121            if line.startswith('['):
122                if not self.api:
123                    # Because this is inferred rather than explicitly declared
124                    # check this after al other requirements are parsed. It's
125                    # possible that a test can declare glsl >= 1.30 and GL >=
126                    # 4.0
127                    if self.shader_version < 1.4:
128                        self.api = 'compat'
129                    else:
130                        self.api = 'core'
131                break
132
133        # Select the correct binary to run the test, but be as conservative as
134        # possible by always selecting the lowest version that meets the
135        # criteria.
136        if self.api == 'gles2':
137            if self.__op in ['<', '<='] or (
138                    self.__op in ['=', '>='] and self.api_version is not None
139                    and self.api_version < 3):
140                self.prog = 'shader_runner_gles2'
141            else:
142                self.prog = 'shader_runner_gles3'
143        else:
144            self.prog = 'shader_runner'
145
146
147class ShaderTest(FastSkipMixin, PiglitBaseTest):
148    """ Parse a shader test file and return a PiglitTest instance
149
150    This function parses a shader test to determine if it's a GL, GLES2 or
151    GLES3 test, and then returns a PiglitTest setup properly.
152
153    """
154
155    def __init__(self, command, api=None, extensions=set(),
156                  shader_version=None, api_version=None, env=None, **kwargs):
157        super(ShaderTest, self).__init__(
158            command,
159            run_concurrent=True,
160            api=api,
161            extensions=extensions,
162            shader_version=shader_version,
163            api_version=api_version,
164            env=env)
165
166    @classmethod
167    def new(cls, filename, installed_name=None):
168        """Parse an XML file and create a new instance.
169
170        :param str filename: The name of the file to parse
171        :param str installed_name: The relative path to the file when installed
172            if not the same as the parsed name
173        """
174        parser = Parser(filename)
175        parser.parse()
176
177        return cls(
178            [parser.prog, installed_name or filename],
179            run_concurrent=True,
180            api=parser.api,
181            extensions=parser.extensions,
182            shader_version=parser.shader_version,
183            api_version=parser.api_version)
184
185    @PiglitBaseTest.command.getter
186    def command(self):
187        """ Add -auto, -fbo and -glsl (if needed) to the test command """
188
189        command = super(ShaderTest, self).command
190        shaderfile = os.path.join(ROOT_DIR, command[1])
191
192        if options.OPTIONS.force_glsl:
193            return [command[0]] + [shaderfile, '-auto', '-fbo', '-glsl']
194        else:
195            return [command[0]] + [shaderfile, '-auto', '-fbo']
196
197    @command.setter
198    def command(self, new):
199        self._command = [n for n in new if n not in ['-auto', '-fbo']]
200
201
202class MultiShaderTest(ReducedProcessMixin, PiglitBaseTest):
203    """A Shader class that can run more than one test at a time.
204
205    This class can call shader_runner with multiple shader_files at a time, and
206    interpret the results, as well as handle pre-mature exit through crashes or
207    from breaking import assupmtions in the utils about skipping.
208
209    Arguments:
210    filenames -- a list of absolute paths to shader test files
211    """
212
213    def __init__(self, prog, files, subtests, skips, env=None):
214        super(MultiShaderTest, self).__init__(
215            [prog] + files,
216            subtests=subtests,
217            run_concurrent=True,
218            env=env)
219
220        self.prog = prog
221        self.files = files
222        self.subtests = subtests
223        self.skips = [FastSkip(**s) for s in skips]
224
225    @classmethod
226    def new(cls, filenames, installednames=None):
227        # TODO
228        assert filenames
229        prog = None
230        subtests = []
231        skips = []
232
233        # Walk each subtest, and either add it to the list of tests to run, or
234        # determine it is skip, and set the result of that test in the subtests
235        # dictionary to skip without adding it to the list of tests to run.
236        for each in filenames:
237            parser = Parser(each)
238            parser.parse()
239            subtests.append(os.path.basename(os.path.splitext(each)[0]).lower())
240
241            if prog is not None:
242                # This allows mixing GLES2 and GLES3 shader test files
243                # together. Since GLES2 profiles can be promoted to GLES3, this
244                # is fine.
245                if parser.prog != prog:
246                    # Pylint can't figure out that prog is not None.
247                    if 'gles' in parser.prog and 'gles' in prog:  # pylint: disable=unsupported-membership-test
248                        prog = max(parser.prog, prog)
249                    else:
250                        # The only way we can get here is if one is GLES and
251                        # one is not, since there is only one desktop runner
252                        # thus it will never fail the is parser.prog != prog
253                        # check
254                        raise exceptions.PiglitInternalError(
255                            'GLES and GL shaders in the same command!\n'
256                            'Cannot pick a shader_runner binary!\n'
257                            'in file: {}'.format(os.path.dirname(each)))
258            else:
259                prog = parser.prog
260
261            skips.append({
262                'extensions': parser.extensions,
263                'api_version': parser.api_version,
264                'shader_version': parser.shader_version,
265                'api': parser.api,
266            })
267
268        return cls(prog, installednames or filenames, subtests, skips)
269
270    def _process_skips(self):
271        r_files = []
272        r_subtests = []
273        r_skips = []
274        for f, s, k in zip(self.files, self.subtests, self.skips):
275            try:
276                k.test()
277            except TestIsSkip:
278                r_skips.append(s)
279            else:
280                r_files.append(f)
281                r_subtests.append(s)
282
283        assert len(r_subtests) + len(r_skips) == len(self.files), \
284            'not all tests accounted for'
285
286        for name in r_skips:
287            self.result.subtests[name] = status.SKIP
288
289        self._expected = r_subtests
290        self._command = [self._command[0]] + r_files
291
292    def run(self):
293        self._process_skips()
294        super(MultiShaderTest, self).run()
295
296    @PiglitBaseTest.command.getter
297    def command(self):
298        command = super(MultiShaderTest, self).command
299        shaderfiles = (x for x in command[1:] if not x.startswith('-'))
300        shaderfiles = [os.path.join(ROOT_DIR, s) for s in shaderfiles]
301        return [command[0]] + shaderfiles + ['-auto', '-report-subtests']
302
303    def _is_subtest(self, line):
304        return line.startswith('PIGLIT TEST:')
305
306    def _resume(self, current):
307        command = [self.command[0]]
308        command.extend(self.command[current + 1:])
309        return command
310
311    def _stop_status(self):
312        # If the lower level framework skips then return a status for that
313        # subtest as skip, and resume.
314        if self.result.out.endswith('PIGLIT: {"result": "skip" }\n'):
315            return status.SKIP
316        if self.result.returncode > 0:
317            return status.FAIL
318        return status.CRASH
319
320    def _is_cherry(self):
321        # Due to the way that piglt is architected if a particular feature
322        # isn't supported it causes the test to exit with status 0. There is no
323        # straightforward way to fix this, so we work around it by looking for
324        # the message that feature provides and marking the test as not
325        # "cherry" when it is found at the *end* of stdout. (We don't want to
326        # match other places or we'll end up in an infinite loop)
327        return (
328            self.result.returncode == 0 and not
329            self.result.out.endswith(
330                'not supported on this implementation\n') and not
331            self.result.out.endswith(
332                'PIGLIT: {"result": "skip" }\n'))
333