graphics_dEQP.py revision 8af26fa68550b4a04bfe80ece52682caec08a9af
1# Copyright 2014 The Chromium OS Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5import bz2
6import glob
7import logging
8import os
9import shutil
10import tempfile
11import xml.etree.cElementTree as et
12from autotest_lib.client.bin import test, utils
13from autotest_lib.client.common_lib import error
14from autotest_lib.client.cros import service_stopper
15
16
17class graphics_dEQP(test.test):
18    """Run the drawElements Quality Program test suite.
19    """
20    version = 1
21    _services = None
22    _board = None
23    _cpu_type = None
24    _gpu_type = None
25    _filter = None
26    _width = 256  # Use smallest width for which all tests run/pass.
27    _height = 256  # Use smallest height for which all tests run/pass.
28    _timeout = 40  # Slightly larger than the dEQP watchdog timeout at 30s.
29
30    DEQP_BASEDIR = '/usr/local/deqp'
31    DEQP_MODULES = {
32        'dEQP-EGL':    'egl',
33        'dEQP-GLES2':  'gles2',
34        'dEQP-GLES3':  'gles3',
35        'dEQP-GLES31': 'gles31'
36    }
37
38    def initialize(self):
39        self._gpu_type = utils.get_gpu_family()
40        self._cpu_type = utils.get_cpu_soc_family()
41        self._board = utils.get_board()
42        self._services = service_stopper.ServiceStopper(['ui', 'powerd'])
43
44    def cleanup(self):
45        if self._services:
46            self._services.restore_services()
47
48    def _parse_test_results(self, result_filename):
49        """Handles result files with one or more test results.
50
51        @param result_filename: log file to parse.
52
53        @return: dictionary of parsed test results.
54        """
55        xml = ''
56        xml_start = False
57        xml_complete = False
58        xml_bad = False
59        result = 'ParseTestResultFail'
60        test_results = {}
61
62        if not os.path.isfile(result_filename):
63            return {}
64
65        with open(result_filename) as result_file:
66            for line in result_file.readlines():
67                # If the test terminates early, the XML will be incomplete
68                # and should not be parsed.
69                if line.startswith('#terminateTestCaseResult'):
70                    result = line.strip().split()[1]
71                    xml_bad = True
72                # Will only see #endTestCaseResult if the test does not
73                # terminate early.
74                elif line.startswith('#endTestCaseResult'):
75                    xml_complete = True
76                elif xml_start:
77                    xml += line
78                elif line.startswith('#beginTestCaseResult'):
79                    # If we see another begin before an end then something is
80                    # wrong.
81                    if xml_start:
82                        xml_bad = True
83                    else:
84                        xml_start = True
85
86                if xml_complete or xml_bad:
87                    if xml_complete:
88                        root = et.fromstring(xml)
89                        result = root.find('Result').get('StatusCode').strip()
90                        xml_complete = False
91                    test_results[result] = test_results.get(result, 0) + 1
92                    xml_bad = False
93                    xml_start = False
94                    result = 'ParseTestResultFail'
95                    xml = ''
96
97        return test_results
98
99    def _bootstrap_new_test_cases(self, executable, test_filter):
100        """Ask dEQP for all test cases and removes non-Pass'ing ones.
101
102        This function assumes that the '*.Pass' file does not exist and that
103        everything else found in the directory should be removed from the tests
104        to run. This can be used incrementally updating failing/hangin tests
105        over several runs.
106
107        @param executable: location '/usr/local/deqp/modules/gles2/deqp-gles2'.
108        @param test_filter: string like 'dEQP-GLES2.info', 'dEQP-GLES3.stress'.
109
110        @return: List of dEQP tests to run.
111        """
112        test_cases = []
113        not_passing_cases = []
114        # We did not find passing cases in expectations. Assume everything else
115        # that is there should not be run this time.
116        expectations_dir = os.path.join(self.bindir, 'expectations',
117                                        self._gpu_type)
118        subset_spec = '%s.*' % test_filter
119        subset_paths = glob.glob(os.path.join(expectations_dir, subset_spec))
120        for subset_file in subset_paths:
121            not_passing_cases.extend(
122                bz2.BZ2File(subset_file).read().splitlines())
123
124        # Now ask dEQP executable nicely for whole list of tests. Needs to be
125        # run in executable directory. Output file is plain text file named
126        # e.g. 'dEQP-GLES2-cases.txt'.
127        command = ('%s '
128                   '--deqp-runmode=txt-caselist '
129                   '--deqp-surface-type=fbo ' % executable)
130        logging.info('Running command %s', command)
131        utils.run(command,
132                  timeout=60,
133                  stderr_is_expected=False,
134                  ignore_status=False,
135                  stdin=None,
136                  stdout_tee=utils.TEE_TO_LOGS,
137                  stderr_tee=utils.TEE_TO_LOGS)
138
139        # Now read this caselist file.
140        caselist_name = '%s-cases.txt' % test_filter.split('.')[0]
141        caselist_file = os.path.join(os.path.dirname(executable), caselist_name)
142        if not os.path.isfile(caselist_file):
143            raise error.TestError('No caselist file at %s!' % caselist_file)
144
145        # And remove non-Pass'ing expectations from caselist.
146        caselist = open(caselist_file).read().splitlines()
147        # Contains lines like "TEST: dEQP-GLES2.capability"
148        test_cases = []
149        match = 'TEST: %s' % test_filter
150        logging.info('Bootstrapping test cases matching "%s".', match)
151        for case in caselist:
152            if case.startswith(match):
153                test = case.split('TEST: ')[1]
154                logging.info('Found test "%s".', test)
155                test_cases.append(test)
156
157        test_cases = list(set(test_cases) - set(not_passing_cases))
158        if not test_cases:
159            raise error.TestError('Unable to bootstrap %s!' % test_filter)
160        test_cases.sort()
161        return test_cases
162
163    def _get_test_cases(self, executable, test_filter, subset):
164        """Gets the test cases for 'Pass', 'Fail' etc. expectations.
165
166        This function supports bootstrapping of new GPU families and dEQP
167        binaries. In particular if there are not 'Pass' expectations found for
168        this GPU family it will query the dEQP executable for a list of all
169        available tests. It will then remove known non-'Pass'ing tests from this
170        list to avoid getting into hangs/crashes etc.
171
172        @param executable: location '/usr/local/deqp/modules/gles2/deqp-gles2'.
173        @param test_filter: string like 'dEQP-GLES2.info', 'dEQP-GLES3.stress'.
174        @param subset: string from 'Pass', 'Fail', 'Timeout' etc.
175
176        @return: List of dEQP tests to run.
177        """
178        expectations_dir = os.path.join(self.bindir, 'expectations',
179                                        self._gpu_type)
180        subset_name = '%s.%s.bz2' % (test_filter, subset)
181        subset_path = os.path.join(expectations_dir, subset_name)
182        if not os.path.isfile(subset_path):
183            if subset != 'Pass':
184                raise error.TestError('No subset file found for %s!' %
185                                      subset_path)
186            return self._bootstrap_new_test_cases(executable, test_filter)
187
188        test_cases = bz2.BZ2File(subset_path).read().splitlines()
189        if not test_cases:
190            raise error.TestError('No test cases found in subset file %s!' %
191                                  subset_path)
192        return test_cases
193
194    def run_tests_individually(self, executable, test_cases):
195        """Runs tests as isolated from each other, but slowly.
196
197        This function runs each test case separately as a command.
198        This means a new context for each test etc. Failures will be more
199        isolated, but runtime quite high due to overhead.
200
201        @param executable: dEQP executable path.
202        @param test_cases: List of dEQP test case strings.
203
204        @return: dictionary of test results.
205        """
206        test_results = {}
207        width = self._width
208        height = self._height
209
210        log_path = os.path.join(tempfile.gettempdir(), '%s-logs' % self._filter)
211        shutil.rmtree(log_path, ignore_errors=True)
212        os.mkdir(log_path)
213
214        i = 0
215        for test_case in test_cases:
216            i += 1
217            logging.info('[%d/%d] TestCase: %s', i, len(test_cases), test_case)
218            log_file = '%s.log' % os.path.join(log_path, test_case)
219            command = ('%s '
220                       '--deqp-case=%s '
221                       '--deqp-surface-type=fbo '
222                       '--deqp-log-images=disable '
223                       '--deqp-watchdog=enable '
224                       '--deqp-surface-width=%d '
225                       '--deqp-surface-height=%d '
226                       '--deqp-log-filename=%s' %
227                       (executable, test_case, width, height, log_file))
228
229            try:
230                logging.info('Running single: %s', command)
231                utils.run(command,
232                          timeout=self._timeout,
233                          stderr_is_expected=False,
234                          ignore_status=True,
235                          stdout_tee=utils.TEE_TO_LOGS,
236                          stderr_tee=utils.TEE_TO_LOGS)
237                result_counts = self._parse_test_results(log_file)
238                if result_counts:
239                    result = result_counts.keys()[0]
240                else:
241                    result = 'Unknown'
242            except error.CmdTimeoutError:
243                result = 'TestTimeout'
244            except error.CmdError:
245                result = 'CommandFailed'
246            except:
247                result = 'UnexpectedError'
248
249            logging.info('Result: %s', result)
250            test_results[result] = test_results.get(result, 0) + 1
251
252        return test_results
253
254    def run_tests_hasty(self, executable, test_cases):
255        """Runs tests as quickly as possible.
256
257        This function runs all the test cases, but does not isolate tests and
258        may take shortcuts/not run all tests to provide maximum coverage at
259        minumum runtime.
260
261        @param executable: dEQP executable path.
262        @param test_cases: List of dEQP test case strings.
263
264        @return: dictionary of test results.
265        """
266        # TODO(ihf): It saves half the test time to use 32*32 but a few tests
267        # fail as they need surfaces larger than 200*200.
268        width = self._width
269        height = self._height
270        command = ('%s '
271                   '--deqp-stdin-caselist '
272                   '--deqp-surface-type=fbo '
273                   '--deqp-log-images=disable '
274                   '--deqp-visibility=hidden '
275                   '--deqp-watchdog=enable '
276                   '--deqp-surface-width=%d '
277                   '--deqp-surface-height=%d ' % (executable, width, height))
278
279        log_file = os.path.join(tempfile.gettempdir(),
280                                '%s_hasty.log' % self._filter)
281        if os.path.exists(log_file):
282            os.remove(log_file)
283        command += '--deqp-log-filename=' + log_file
284
285        logging.info('Running hasty %d test cases: %s', len(test_cases),
286                     command)
287        # We are trying to handle all errors by parsing the log file.
288        try:
289            utils.run(command,
290                      timeout=3600,  # All tests combined less than 1h in hasty.
291                      stderr_is_expected=False,
292                      ignore_status=False,
293                      stdin='\n'.join(test_cases),
294                      stdout_tee=utils.TEE_TO_LOGS,
295                      stderr_tee=utils.TEE_TO_LOGS)
296        except:
297            pass
298
299        return self._parse_test_results(log_file)
300
301    def run_once(self, opts=[]):
302        options = dict(filter='',
303                       timeout=self._timeout,
304                       subset_to_run='Pass',  # Pass, Fail, Timeout etc.
305                       hasty='False',
306                       shard=1,  # TODO(ihf): Support sharding for bvt-cq.
307                       shards=1)
308        options.update(utils.args_to_dict(opts))
309        logging.info('Test Options: %s', options)
310
311        hasty = (options['hasty'] == 'True')
312        self._timeout = int(options['timeout'])
313        self._filter = options['filter']
314        if not self._filter:
315            raise error.TestError('No dEQP test filter specified')
316
317        # Some information to help postprocess logs into blacklists later.
318        logging.info('ChromeOS BOARD = %s', self._board)
319        logging.info('ChromeOS CPU family = %s', self._cpu_type)
320        logging.info('ChromeOS GPU family = %s', self._gpu_type)
321        logging.info('dEQP test filter = %s', self._filter)
322
323        # Determine module from filter.
324        test_prefix, test_group = self._filter.split('.')
325        if test_prefix in self.DEQP_MODULES:
326            module = self.DEQP_MODULES[test_prefix]
327        else:
328            raise error.TestError('Invalid test filter: %s' % self._filter)
329
330        executable_path = os.path.join(self.DEQP_BASEDIR, 'modules', module)
331        executable = os.path.join(executable_path, 'deqp-%s' % module)
332
333        self._services.stop_services()
334
335        # Must be in the executable directory when running for it to find it's
336        # test data files!
337        os.chdir(executable_path)
338        test_cases = self._get_test_cases(executable, self._filter,
339                                          options['subset_to_run'])
340        logging.info('Test cases:')
341        logging.info(test_cases)
342
343        test_results = {}
344        if hasty:
345            logging.info('Running in hasty mode.')
346            test_results = self.run_tests_hasty(executable, test_cases)
347        else:
348            logging.info('Running each test individually.')
349            test_results = self.run_tests_individually(executable, test_cases)
350
351        logging.info('Test results:')
352        logging.info(test_results)
353        self.write_perf_keyval(test_results)
354
355        test_count = 0
356        test_failures = 0
357        for result in test_results:
358            test_count += test_results[result]
359            if result.lower() not in ['pass', 'notsupported']:
360                test_failures += test_results[result]
361        # The text "Completed all tests." is used by the process_log.py script
362        # and should always appear at the end of a completed test run.
363        logging.info('Completed all tests. Saw %d tests and %d failures.',
364                     test_count, test_failures)
365
366        if test_failures:
367            raise error.TestFail('%d/%d tests failed.' %
368                                 (test_failures, test_count))
369
370        if test_count == 0:
371            raise error.TestError('No test cases found for filter: %s!' %
372                                  self._filter)
373