3 # Permission is hereby granted, free of charge, to any person
4 # obtaining a copy of this software and associated documentation
5 # files (the "Software"), to deal in the Software without
6 # restriction, including without limitation the rights to use,
7 # copy, modify, merge, publish, distribute, sublicense, and/or
8 # sell copies of the Software, and to permit persons to whom the
9 # Software is furnished to do so, subject to the following
12 # This permission notice shall be included in all copies or
13 # substantial portions of the Software.
15 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY
16 # KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE
17 # WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
18 # PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHOR(S) BE
19 # LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN
20 # AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF
21 # OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
22 # DEALINGS IN THE SOFTWARE.
24 """ Module provides a base class for Tests """
38 from framework
import exceptions
39 from framework
import status
40 from framework
.options
import OPTIONS
41 from framework
.results
import TestResult
43 _EXTRA_POPEN_ARGS
= {}
45 if os
.name
== 'posix':
46 # This should work for all *nix systems, Linux, the BSDs, and OSX.
47 # This speicifically creates a session group for each test, so that
48 # it's children can be killed if it times out.
49 _EXTRA_POPEN_ARGS
= {'start_new_session': True}
51 # pylint: enable=wrong-import-position,wrong-import-order
60 'is_crash_returncode',
63 # Allows timeouts to be suppressed by setting the environment variable
64 # PIGLIT_NO_TIMEOUT to anything that bool() will resolve as True
65 _SUPPRESS_TIMEOUT
= bool(os
.environ
.get('PIGLIT_NO_TIMEOUT', False))
68 class TestIsSkip(exceptions
.PiglitException
):
69 """Exception raised in is_skip() if the test is a skip."""
70 def __init__(self
, reason
):
71 super(TestIsSkip
, self
).__init
__()
75 class TestRunError(exceptions
.PiglitException
):
76 """Exception raised if the test fails to run."""
77 def __init__(self
, message
, status
):
78 super(TestRunError
, self
).__init
__(message
)
82 def is_crash_returncode(returncode
):
83 """Determine whether the given process return code correspond to a
86 # In python 2 NoneType and other types can be compaired, in python3 this
88 if returncode
is None:
91 if sys
.platform
== 'win32':
93 # - For uncaught exceptions the process terminates with the exception
94 # code, which is usually negative
95 # - MSVCRT's abort() terminates process with exit code 3
96 return returncode
< 0 or returncode
== 3
101 class Test(metaclass
=abc
.ABCMeta
):
102 """ Abstract base class for Test classes
104 This class provides the framework for running tests, with several methods
105 and properties that can be overwritten to produce a specialized class for
106 running test suites other than piglit.
108 It provides two methods for running tests, execute and run.
109 execute() provides lots of features, and is invoked when running piglit
110 from the command line, run() is a more basic method for running the test,
111 and is called internally by execute(), but is can be useful outside of it.
114 command -- a value to be passed to subprocess.Popen
117 run_concurrent -- If True the test is thread safe. Default: False
120 __slots__
= ['run_concurrent', 'env', 'result', 'cwd', '_command']
123 def __init__(self
, command
, run_concurrent
=False, env
=None, cwd
=None):
124 assert isinstance(command
, list), command
126 self
.run_concurrent
= run_concurrent
127 self
._command
= copy
.copy(command
)
129 self
.result
= TestResult()
132 def execute(self
, path
, log
, options
):
135 Run a test, but with features. This times the test, uses dmesg checking
136 (if requested), and runs the logger.
139 path -- the name of the test
140 log -- a log.Log instance
141 options -- a dictionary containing dmesg and monitoring objects
147 self
.result
.time
.start
= time
.time()
148 options
['dmesg'].update_dmesg()
149 options
['monitor'].update_monitoring()
151 self
.result
.time
.end
= time
.time()
152 self
.result
= options
['dmesg'].update_result(self
.result
)
153 options
['monitor'].check_monitoring()
154 # This is a rare case where a bare exception is okay, since we're
155 # using it to log exceptions
157 exc_type
, exc_value
, exc_traceback
= sys
.exc_info()
158 traceback
.print_exc(file=sys
.stderr
)
159 self
.result
.result
= 'fail'
160 self
.result
.exception
= "{}{}".format(exc_type
, exc_value
)
161 self
.result
.traceback
= "".join(
162 traceback
.format_tb(exc_traceback
))
164 log
.log(self
.result
.result
)
174 def command(self
, new
):
175 assert isinstance(new
, list), 'Test.command must be a list'
179 def interpret_result(self
):
180 """Convert the raw output of the test into a form piglit understands.
182 if is_crash_returncode(self
.result
.returncode
):
183 self
.result
.result
= status
.CRASH
184 if self
.result
.subtests
:
185 # We know because subtests are ordered that the first test with
186 # a status of NOTRUN is the subtest that crashed, mark that
188 for k
, v
in self
.result
.subtests
.items():
189 if v
== status
.NOTRUN
:
190 self
.result
.subtests
[k
] = status
.CRASH
192 elif self
.result
.returncode
!= 0:
193 if self
.result
.result
== status
.PASS
:
194 self
.result
.result
= status
.WARN
196 self
.result
.result
= status
.FAIL
200 Run a test. The return value will be a dictionary with keys
201 including 'result', 'info', 'returncode' and 'command'.
202 * For 'result', the value may be one of 'pass', 'fail', 'skip',
204 * For 'info', the value will include stderr/out text.
205 * For 'returncode', the value will be the numeric exit code/value.
206 * For 'command', the value will be command line program and arguments.
208 self
.result
.command
= ' '.join(map(str, self
.command
))
209 self
.result
.environment
= " ".join(
210 '{0}="{1}"'.format(k
, v
) for k
, v
in itertools
.chain(
211 OPTIONS
.env
.items(), self
.env
.items()))
215 except TestIsSkip
as e
:
216 self
.result
.result
= status
.SKIP
217 for each
in self
.result
.subtests
.keys():
218 self
.result
.subtests
[each
] = status
.SKIP
219 self
.result
.out
= e
.reason
220 self
.result
.returncode
= None
225 except TestRunError
as e
:
226 self
.result
.result
= str(e
.status
)
227 for each
in self
.result
.subtests
.keys():
228 self
.result
.subtests
[each
] = str(e
.status
)
229 self
.result
.out
= str(e
)
230 self
.result
.returncode
= None
233 self
.interpret_result()
236 """ Application specific check for skip
238 If this function returns a truthy value then the current test will be
239 skipped. The base version will always return False
244 def _run_command(self
, **kwargs
):
245 """ Run the test command and get the result
247 This method sets environment options, then runs the executable. If the
248 executable isn't found it sets the result to skip.
251 # This allows the ReducedProcessMixin to work without having to whack
252 # self.command (which should be treated as immutable), but is
253 # considered private.
254 command
= kwargs
.pop('_command', self
.command
)
256 # Setup the environment for the test. Environment variables are taken
257 # from the following sources, listed in order of increasing precedence:
259 # 1. This process's current environment.
260 # 2. Global test options. (Some of these are command line options to
261 # Piglit's runner script).
262 # 3. Per-test environment variables set in all.py.
264 # Piglit chooses this order because Unix tradition dictates that
265 # command line options (2) override environment variables (1); and
266 # Piglit considers environment variables set in all.py (3) to be test
268 _base
= itertools
.chain(os
.environ
.items(),
271 fullenv
= {str(k
): str(v
) for k
, v
in _base
}
274 proc
= subprocess
.Popen(map(str, command
),
275 stdout
=subprocess
.PIPE
,
276 stderr
=subprocess
.PIPE
,
279 universal_newlines
=True,
282 self
.result
.pid
.append(proc
.pid
)
283 if not _SUPPRESS_TIMEOUT
:
284 out
, err
= proc
.communicate(timeout
=self
.timeout
)
286 out
, err
= proc
.communicate()
287 returncode
= proc
.returncode
289 # Different sets of tests get built under different build
290 # configurations. If a developer chooses to not build a test,
291 # Piglit should not report that test as having failed.
292 if e
.errno
== errno
.ENOENT
:
293 raise TestRunError("Test executable not found.\n", 'skip')
296 except subprocess
.TimeoutExpired
:
297 # This can only be reached if subprocess32 is present or on python
298 # 3.x, since # TimeoutExpired is never raised by the python 2.7
303 # XXX: This is probably broken on windows, since os.getpgid doesn't
304 # exist on windows. What is the right way to handle this?
305 if proc
.poll() is None:
307 os
.killpg(os
.getpgid(proc
.pid
), signal
.SIGKILL
)
309 # Since the process isn't running it's safe to get any remaining
310 # stdout/stderr values out and store them.
311 self
.result
.out
, self
.result
.err
= proc
.communicate()
314 'Test run time exceeded timeout value ({} seconds)\n'.format(
317 # LLVM prints colored text into stdout/stderr on error, which raises:
318 except UnicodeDecodeError as e
:
319 raise TestRunError("UnicodeDecodeError.\n", 'crash')
321 # The setter handles the bytes/unicode conversion
322 self
.result
.out
= out
323 self
.result
.err
= err
324 self
.result
.returncode
= returncode
326 def __eq__(self
, other
):
327 return self
.command
== other
.command
329 def __ne__(self
, other
):
330 return not self
== other
333 class DummyTest(Test
):
334 def __init__(self
, name
, result
):
335 super(DummyTest
, self
).__init
__([name
])
336 self
.result
.result
= result
338 def execute(self
, path
, log
, options
):
341 def interpret_result(self
):
345 class WindowResizeMixin(object):
346 """ Mixin class that deals with spurious window resizes
348 On gnome (and possible other DE's) the window manager may decide to resize
349 a window. This causes the test to fail even though otherwise would not.
350 This Mixin overides the _run_command method to run the test 5 times, each
351 time searching for the string 'Got suprious window resize' in the output,
352 if it fails to find it it will break the loop and continue.
354 see: https://bugzilla.gnome.org/show_bug.cgi?id=680214
357 def _run_command(self
, *args
, **kwargs
):
358 """Run a test up 5 times when window resize is detected.
360 Rerun the command up to 5 times if the window size changes, if it
361 changes 6 times mark the test as fail and return True, which will cause
362 Test.run() to return early.
366 super(WindowResizeMixin
, self
)._run
_command
(*args
, **kwargs
)
367 if "Got spurious window resize" not in self
.result
.out
:
370 # If we reach this point then there has been no error, but spurious
371 # resize was detected more than 5 times. Set the result to fail
372 raise TestRunError('Got spurious resize more than 5 times', 'fail')
375 class ValgrindMixin(object):
376 """Mixin class that adds support for running tests through valgrind.
378 This mixin allows a class to run with the --valgrind option.
383 return super(ValgrindMixin
, self
).command
387 return ['valgrind', '--quiet', '--error-exitcode=1',
392 def interpret_result(self
):
393 """Set the status to the valgrind status.
395 It is important that the valgrind interpret_results code is run last,
396 since it depends on the statuses already set and passed to it,
397 including the Test.interpret_result() method. To this end it executes
398 super().interpret_result(), then calls it's own result.
401 super(ValgrindMixin
, self
).interpret_result()
404 # If the underlying test failed, simply report
405 # 'skip' for this valgrind test.
406 if self
.result
.result
!= 'pass' and (
407 self
.result
.result
!= 'warn' and
408 self
.result
.returncode
!= 0):
409 self
.result
.result
= 'skip'
410 elif self
.result
.returncode
== 0:
411 # Test passes and is valgrind clean.
412 self
.result
.result
= 'pass'
414 # Test passed but has valgrind errors.
415 self
.result
.result
= 'fail'
418 class ReducedProcessMixin(metaclass
=abc
.ABCMeta
):
419 """This Mixin simplifies writing Test classes that run more than one test
422 Although one of the benefits of piglit is it's process isolation, there are
423 times that process isolation is too expensive for day to day runs, and
424 running more than one test in a single process is a valid trade-off for
425 decreased run times. This class helps to ease writing a Test class for such
426 a purpose, while not suffering all of the drawback of the approach.
428 The first way that this helps is that it provides crash detection and
429 recovery, allowing a single subtest to crash
432 def __init__(self
, command
, subtests
=None, **kwargs
):
433 assert subtests
is not None
434 super(ReducedProcessMixin
, self
).__init
__(command
, **kwargs
)
435 self
._expected
= subtests
436 self
._populate
_subtests
()
439 """Skip if the length of expected is 0."""
440 if not self
._expected
:
441 raise TestIsSkip('All subtests skipped')
442 super(ReducedProcessMixin
, self
).is_skip()
444 def __find_sub(self
):
445 """Helper for getting the next index."""
446 return len([l
for l
in self
.result
.out
.split('\n')
447 if self
._is
_subtest
(l
)])
450 def _subtest_name(test
):
451 """If the name provided isn't the subtest name, this method does."""
454 def _stop_status(self
):
455 """This method returns the status of the test that stopped the run.
457 By default this will return status.CRASH, but this may not be suitable
458 for some suites, which may require special considerations and need to
459 require a different status in some cases, like SKIP.
463 def _run_command(self
, *args
, **kwargs
):
464 """Run the command until all of the subtests have completed or crashed.
466 This method will try to run all of the subtests, resuming the run if
467 it's interrupted, and combining the stdout and stderr attributes
468 together for parsing later. I will separate those values with
469 "\n\n====RESUME====\n\n".
471 super(ReducedProcessMixin
, self
)._run
_command
(*args
, **kwargs
)
473 if not self
._is
_cherry
():
474 returncode
= self
.result
.returncode
475 out
= [self
.result
.out
]
476 err
= [self
.result
.err
]
477 cur_sub
= self
.__find
_sub
() or 1
478 last
= len(self
._expected
)
480 while cur_sub
< last
:
481 self
.result
.subtests
[
482 self
._subtest
_name
(self
._expected
[cur_sub
- 1])] = \
485 super(ReducedProcessMixin
, self
)._run
_command
(
486 _command
=self
._resume
(cur_sub
) + list(args
), **kwargs
)
488 out
.append(self
.result
.out
)
489 err
.append(self
.result
.err
)
491 # If the index is 0 the next test failed without printing a
492 # name, increase by 1 so that test will be marked crash and we
493 # don't get stuck in an infinite loop, otherwise return the
494 # number of tests that did complete.
495 cur_sub
+= self
.__find
_sub
() or 1
497 if not self
._is
_cherry
():
498 self
.result
.subtests
[
499 self
._subtest
_name
(self
._expected
[cur_sub
- 1])] = \
502 # Restore and keep the original returncode (so that it remains a
503 # non-pass, since only one test might fail and the resumed part
505 self
.result
.returncode
= returncode
506 self
.result
.out
= '\n\n====RESUME====\n\n'.join(out
)
507 self
.result
.err
= '\n\n====RESUME====\n\n'.join(err
)
509 def _is_cherry(self
):
510 """Method used to determine if rerunning is required.
512 If this returns False then the rerun path will be entered, otherwise
513 _run_command is effectively a bare call to super().
515 Classes using this mixin may need to overwrite this if the binary
516 they're calling can stop prematurely but return 0.
518 return self
.result
.returncode
== 0
520 def _populate_subtests(self
):
521 """Default implementation of subtest prepopulation.
523 It may be necissary to override this depending on the subtest format.
525 self
.result
.subtests
.update({x
: status
.NOTRUN
for x
in self
._expected
})
528 def _resume(self
, current
):
529 """Method that defines how to resume the case if it crashes.
531 This method will be provided with a completed count, which is the index
532 into self._expected of the first subtest that hasn't been run. This
533 method should return the command to restart, and the ReduceProcessMixin
534 will handle actually restarting the the process with the new command.
538 def _is_subtest(self
, line
):
539 """Determines if a line in stdout contains a subtest name.
541 This method is used during the resume detection phase of the
542 _run_command method to determine how many subtests have successfully
545 Should simply return True if the line reprents a test starting, or
546 False if it does not.