2 # Copyright (c) 2014, 2016, 2019 Intel Corporation
4 # Permission is hereby granted, free of charge, to any person obtaining a copy
5 # of this software and associated documentation files (the "Software"), to deal
6 # in the Software without restriction, including without limitation the rights
7 # to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
8 # copies of the Software, and to permit persons to whom the Software is
9 # furnished to do so, subject to the following conditions:
11 # The above copyright notice and this permission notice shall be included in
12 # all copies or substantial portions of the Software.
14 # THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
15 # IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
16 # FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
17 # AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
18 # LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
19 # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22 """ Tests for the exectest module """
27 import subprocess32
as subprocess
33 from framework
import dmesg
34 from framework
import log
35 from framework
import monitoring
36 from framework
import status
37 from framework
.options
import _Options
as Options
38 from framework
.test
import base
40 from ..test_status
import PROBLEMS
43 # pylint: disable=invalid-name,no-self-use,protected-access
46 class _Test(base
.Test
):
47 """Helper with stubbed interpret_results method."""
48 def interpret_result(self
):
49 super(_Test
, self
).interpret_result()
52 class TestTest(object):
53 """Tests for the Test class."""
55 class TestRun(object):
56 """Tests for Test.run."""
58 def test_return_early_when_run_command_excepts(self
, mocker
):
59 """Test.run exits early when Test._run_command raises an exception.
62 mocker
.patch
.object(t
, '_run_command',
63 side_effect
=base
.TestRunError('foo', 'pass'))
64 mocker
.patch
.object(t
, 'interpret_result',
65 side_effect
=Exception('Test failure'))
70 class TestRunCommand(object):
71 """Tests for Test._run_command."""
75 if os
.name
== 'posix':
76 cls
.command
= ['sleep', '60']
78 cls
.command
= ['timeout', '/t', '/nobreak', '60']
80 @pytest.mark
.timeout(6)
81 def test_timeout_kill_children(self
, mocker
, tmpdir
):
82 """test.base.Test: kill children if terminate fails.
84 This creates a process that forks multiple times, and then checks
85 that the children have been killed.
87 This test could leave processes running if it fails.
89 # This function is the only user of psutil, and by putting it in
90 # the function itself we avoid needed to try/except it, and then
91 # skip if we don't have it.
92 psutil
= pytest
.importorskip('psutil')
94 class PopenProxy(object):
95 """An object that proxies Popen, and saves the Popen instance
98 This is useful for testing the Popen instance.
104 def __call__(self
, *args
, **kwargs
):
105 self
.popen
= subprocess
.Popen(*args
, **kwargs
)
107 # if communicate is called successfully then the proc will
108 # be reset to None, which will make the test fail.
109 self
.popen
.communicate
= mocker
.Mock(
110 return_value
=('out', 'err'))
114 # localpath doesn't have a seek option
115 with tmpdir
.join('test').open(mode
='w', ensure
=True) as f
:
116 # Create a file that will be executed as a python script Create
117 # a process with two subproccesses (not threads) that will run
119 f
.write(textwrap
.dedent("""\
121 from multiprocessing import Process
127 if __name__ == "__main__":
128 a = Process(target=p)
129 b = Process(target=p)
135 f
.seek(0) # we'll need to read the file back
137 # Create an object that will return a popen object, but also
138 # store it so we can access it later
141 test
= _Test(['python3', str(f
)])
144 # mock out subprocess.Popen with our proxy object
145 mock_subp
= mocker
.patch('framework.test.base.subprocess')
146 mock_subp
.Popen
= proxy
147 mock_subp
.TimeoutExpired
= subprocess
.TimeoutExpired
150 # Check to see if the Popen has children, even after it should
151 # have received a TimeoutExpired.
152 proc
= psutil
.Process(os
.getsid(proxy
.popen
.pid
))
153 children
= proc
.children(recursive
=True)
156 # If there are still running children attempt to clean them up,
157 # starting with the final generation and working back to the
159 for child
in reversed(children
):
162 raise Exception('Test process had children when it should not')
165 @pytest.mark
.timeout(6)
166 def test_timeout(self
):
167 """test.base.Test: Stops running test after timeout expires.
169 This is a little bit of extra time here, but without a sleep of 60
170 seconds if the test runs 6 seconds it's run too long, we do need to
171 allow a bit of time for teardown to happen.
173 test
= _Test(self
.command
)
178 @pytest.mark
.timeout(6)
179 def test_timeout_status(self
):
180 """test.base.Test: Setst the status to 'timeout' when then timeout
183 This is a little bit of extra time here, but without a sleep of 60
184 seconds if the test runs 6 seconds it's run too long, we do need to
185 allow a bit of time for teardown to happen.
187 test
= _Test(self
.command
)
190 assert test
.result
.result
is status
.TIMEOUT
192 class TestExecuteTraceback(object):
193 """Test.execute tests for Traceback handling."""
195 class Sentinel(Exception):
199 def shared_test(self
, mocker
):
201 test
= _Test(['foo'])
202 test
.run
= mocker
.Mock(side_effect
=self
.Sentinel
)
204 test
.execute(mocker
.Mock(spec
=str),
205 mocker
.Mock(spec
=log
.BaseLog
),
206 {'dmesg': mocker
.Mock(spec
=dmesg
.BaseDmesg
),
207 'monitor': mocker
.Mock(spec
=monitoring
.Monitoring
)})
210 def test_result(self
, shared_test
):
211 """Test.execute (exception): Sets the result to fail."""
212 assert shared_test
.result
is status
.FAIL
214 def test_traceback(self
, shared_test
):
215 """Test.execute (exception): Sets the traceback.
217 It's fragile to record the actual traceback, and it's unlikely that
218 it can easily be implemented differently than the way the original
219 code is implemented, so this doesn't do that, it just verifies
222 assert shared_test
.traceback
!= ''
223 assert isinstance(shared_test
.traceback
, str)
225 def test_exception(self
, shared_test
):
226 """Test.execute (exception): Sets the exception.
228 This is much like the traceback, it's difficult to get the correct
229 value, so just make sure it's being set.
231 assert shared_test
.exception
!= ''
232 assert isinstance(shared_test
.exception
, str)
234 class TestCommand(object):
235 """Tests for Test.command."""
237 def test_string_for_command(self
):
238 """Asserts if it is passed a string instead of a list."""
239 with pytest
.raises(AssertionError):
242 def test_mutation(self
):
243 """test.base.Test.command: does not mutate the value it was
246 There was a very subtle bug in all.py that causes the key values to
247 be changed before they are assigned in some cases. This is because
248 the right side of an assignment is evalated before the left side,
252 >>> args = ['a', 'b']
253 >>> profile[' '.join(args)] = PiglitGLTest(args)
254 >>> list(profile.keys())
258 def __init__(self
, *args
, **kwargs
):
259 super(MyTest
, self
).__init
__(*args
, **kwargs
)
260 self
._command
[0] = 'bin/' + self
._command
[0]
265 assert args
== ['a', 'b']
267 class TestInterpretResult(object):
268 """Tests for Test.interpret_result."""
270 def test_returncode_greater_zero(self
):
271 """A test with status > 0 is fail."""
272 test
= _Test(['foobar'])
273 test
.result
.returncode
= 1
274 test
.result
.out
= 'this is some\nstdout'
275 test
.result
.err
= 'this is some\nerrors'
276 test
.interpret_result()
278 assert test
.result
.result
is status
.FAIL
280 def test_crash_subtest_before_start(self
):
281 """A test for a test with a subtest, that crashes at the start
284 test
= _Test(['foobar'])
285 test
.result
.returncode
= -1
286 for x
in (str(y
) for y
in range(5)):
287 test
.result
.subtests
[x
] = status
.NOTRUN
288 test
.interpret_result()
290 assert test
.result
.result
is status
.CRASH
291 assert test
.result
.subtests
['0'] is status
.CRASH
292 for x
in (str(y
) for y
in range(1, 5)):
293 assert test
.result
.subtests
[x
] is status
.NOTRUN
295 def test_crash_subtest_mid(self
):
296 """A test for a test with a subtest, that crashes in the middle
299 test
= _Test(['foobar'])
300 test
.result
.returncode
= -1
301 for x
in (str(y
) for y
in range(2)):
302 test
.result
.subtests
[x
] = status
.PASS
303 for x
in (str(y
) for y
in range(2, 5)):
304 test
.result
.subtests
[x
] = status
.NOTRUN
305 test
.interpret_result()
307 assert test
.result
.result
is status
.CRASH
308 for x
in (str(y
) for y
in range(2)):
309 assert test
.result
.subtests
[x
] is status
.PASS
310 assert test
.result
.subtests
['2'] is status
.CRASH
311 for x
in (str(y
) for y
in range(3, 5)):
312 assert test
.result
.subtests
[x
] is status
.NOTRUN
315 class TestWindowResizeMixin(object):
316 """Tests for the WindowResizeMixin class."""
318 def test_rerun(self
):
319 """test.base.WindowResizeMixin: runs multiple when spurious resize
322 # Because of Python's inheritance order we need another mixin.
324 def __init__(self
, *args
, **kwargs
):
325 super(Mixin
, self
).__init
__(*args
, **kwargs
)
326 self
.__return
_spurious
= True
328 def _run_command(self
, *args
, **kwargs
): # pylint: disable=unused-argument
329 self
.result
.returncode
= None
331 # IF this is run only once we'll have "got spurious window resize"
332 # in result.out, if it runs multiple times we'll get 'all good'
333 if self
.__return
_spurious
:
334 self
.result
.out
= "Got spurious window resize"
335 self
.__return
_spurious
= False
337 self
.result
.out
= 'all good'
339 class Test_(base
.WindowResizeMixin
, Mixin
, _Test
):
342 test
= Test_(['foo'])
344 assert test
.result
.out
== 'all good'
347 class TestValgrindMixin(object):
348 """Tests for the ValgrindMixin class."""
350 def test_command(self
, mocker
):
351 """test.base.ValgrindMixin.command: self.command doesn't change."""
352 opts
= mocker
.patch('framework.test.base.OPTIONS',
353 new_callable
=Options
)
355 class Test(base
.ValgrindMixin
, _Test
):
361 assert test
.command
== ['foo']
363 def test_keys(self
, mocker
):
364 """test.base.ValgrindMixin.keys: return 'valgrind' with its keys."""
365 opts
= mocker
.patch('framework.test.base.OPTIONS',
366 new_callable
=Options
)
368 class Test(base
.ValgrindMixin
, _Test
):
374 assert test
.keys() == ['valgrind', '--quiet', '--error-exitcode=1',
377 class TestRun(object):
378 """Tests for the run method."""
381 def setup_class(cls
):
382 class _NoRunTest(_Test
):
384 self
.interpret_result()
386 class Test(base
.ValgrindMixin
, _NoRunTest
):
391 # The ids function here is a bit of a hack to work around the
392 # pytest-timeout plugin, which is broken. when 'timeout' is passed as a
393 # string using str it tries to grab that value and a flaming pile
395 @pytest.mark
.parametrize("starting", PROBLEMS
,
396 ids
=lambda x
: str(x
).upper())
397 def test_problem_status_changes_valgrind_enabled(self
, starting
, mocker
):
398 """When running with valgrind mode we're actually testing the test
399 binary itself, so any status other than pass is irrelevant, and
400 should be marked as skip.
402 mock_opts
= mocker
.patch('framework.test.base.OPTIONS',
403 new_callable
=Options
)
404 mock_opts
.valgrind
= True
406 test
= self
.test(['foo'])
407 test
.result
.result
= starting
409 assert test
.result
.result
is status
.SKIP
411 @pytest.mark
.parametrize("starting", PROBLEMS
,
412 ids
=lambda x
: str(x
).upper())
413 def test_problems_with_valgrind_disabled(self
, starting
, mocker
):
414 """When valgrind is disabled nothing should change
416 mock_opts
= mocker
.patch('framework.test.base.OPTIONS',
417 new_callable
=Options
)
418 mock_opts
.valgrind
= False
420 test
= self
.test(['foo'])
421 test
.result
.result
= starting
422 test
.result
.returncode
= 0
424 assert test
.result
.result
is starting
426 def test_passed_valgrind(self
, mocker
):
427 """test.base.ValgrindMixin.run: when test is 'pass' and returncode
428 is '0' result is pass.
430 mock_opts
= mocker
.patch('framework.test.base.OPTIONS',
431 new_callable
=Options
)
432 test
= self
.test(['foo'])
433 mock_opts
.valgrind
= True
434 test
.result
.result
= status
.PASS
435 test
.result
.returncode
= 0
437 assert test
.result
.result
is status
.PASS
439 def test_failed_valgrind(self
, mocker
):
440 """test.base.ValgrindMixin.run: when a test is 'pass' but
441 returncode is not 0 it's 'fail'.
443 mock_opts
= mocker
.patch('framework.test.base.OPTIONS',
444 new_callable
=Options
)
445 mock_opts
.valgrind
= True
446 test
= self
.test(['foo'])
447 test
.result
.result
= status
.PASS
448 test
.result
.returncode
= 1
449 test
.interpret_result()
450 assert test
.result
.result
is status
.FAIL
453 class TestReducedProcessMixin(object):
454 """Tests for the ReducedProcessMixin class."""
456 class MPTest(base
.ReducedProcessMixin
, _Test
):
457 def _resume(self
, current
):
458 return [self
.command
[0]] + self
._expected
[current
:]
460 def _is_subtest(self
, line
):
461 return line
.startswith('TEST')
463 def test_populate_subtests(self
):
464 test
= self
.MPTest(['foobar'], subtests
=['a', 'b', 'c'])
465 assert set(test
.result
.subtests
.keys()) == {'a', 'b', 'c'}
467 class TestRunCommand(object):
468 """Tests for the _run_command method."""
470 @pytest.fixture(scope
='module')
471 def test_class(self
):
472 """Defines a test class that uses generators to ease testing."""
474 """This shim goes between the Mixin and the Test class and
475 provides a way to set the output of the test.
478 def __init__(self
, *args
, **kwargs
):
479 super(_Shim
, self
).__init
__(*args
, **kwargs
)
480 self
.gen_rcode
= None
484 def _run_command(self
, *args
, **kwargs
): # pylint: disable=unused-argument
485 # pylint: disable=no-member
486 self
.result
.returncode
= next(self
.gen_rcode
)
487 self
.result
.out
= next(self
.gen_out
)
488 self
.result
.err
= next(self
.gen_err
)
490 class Test(base
.ReducedProcessMixin
, _Shim
, _Test
):
491 """The actual Class returned by the fixture.
493 This class implements the abstract bits from
494 ReducedProcessMixin, and inserts the _Shim class. The
495 _is_subtest method is implemented such that any line starting
496 with SUBTEST is a subtest.
499 def _is_subtest(self
, line
):
500 return line
.startswith('SUBTEST')
502 def _resume(self
, cur
, **kwargs
): # pylint: disable=unused-argument
503 return self
._expected
[cur
:]
505 def interpret_result(self
):
508 for line
in self
.result
.out
.split('\n'):
509 if self
._is
_subtest
(line
):
510 name
= line
[len('SUBTEST: '):]
511 elif line
.startswith('RESULT: '):
512 self
.result
.subtests
[name
] = line
[len('RESULT: '):]
517 def test_result(self
, test_class
):
518 """Test result attributes."""
519 test
= test_class(['foobar'], ['a', 'b'])
520 test
.gen_out
= iter(['SUBTEST: a', 'SUBTEST: b'])
521 test
.gen_err
= iter(['err output', 'err output'])
522 test
.gen_rcode
= iter([2, 0])
524 assert test
.result
.out
== \
525 'SUBTEST: a\n\n====RESUME====\n\nSUBTEST: b'
526 assert test
.result
.err
== \
527 'err output\n\n====RESUME====\n\nerr output'
528 assert test
.result
.returncode
== 2
530 @pytest.mark
.timeout(5)
531 def test_infinite_loop(self
, test_class
):
532 """Test that we don't get into an infinite loop."""
533 test
= test_class(['foobar'], ['a', 'b'])
534 test
.gen_out
= iter(['a', 'a'])
535 test
.gen_err
= iter(['a', 'a'])
536 test
.gen_rcode
= iter([1, 1])
539 def test_crash_first(self
, test_class
):
540 """Handles the first test crashing."""
541 test
= test_class(['foo'], ['a', 'b'])
542 test
.gen_out
= iter(['', 'SUBTEST: a'])
543 test
.gen_err
= iter(['', ''])
544 test
.gen_rcode
= iter([1, 0])
546 # Since interpret_result isn't called this would normally be left
547 # as NOTRUN, but we want to ensure that _run_command isn't mucking
548 # with it, so we set it to this PASS, which acts as a sentinel
549 test
.result
.subtests
['b'] = status
.PASS
552 assert test
.result
.subtests
['a'] is status
.CRASH
553 assert test
.result
.subtests
['b'] is status
.PASS
555 def test_middle_crash(self
, test_class
):
556 """handle the final subtest crashing."""
557 test
= test_class(['foo'], ['a', 'b', 'c'])
558 test
.gen_out
= iter(['SUBTEST: a\nRESULT: pass\nSUBTEST: b\n',
559 'SUBTEST: c\nRESULT: pass\n'])
560 test
.gen_err
= iter(['', ''])
561 test
.gen_rcode
= iter([1, 0])
564 test
.interpret_result()
566 assert test
.result
.subtests
['a'] == status
.PASS
567 assert test
.result
.subtests
['b'] == status
.CRASH
568 assert test
.result
.subtests
['c'] == status
.PASS
570 def test_final_crash(self
, test_class
):
571 """handle the final subtest crashing."""
572 test
= test_class(['foo'], ['a', 'b', 'c'])
573 test
.gen_out
= iter(['SUBTEST: a\nRESULT: pass\n'
574 'SUBTEST: b\nRESULT: pass\n'
576 test
.gen_err
= iter([''])
577 test
.gen_rcode
= iter([1])
580 test
.interpret_result()
582 assert test
.result
.subtests
['a'] == status
.PASS
583 assert test
.result
.subtests
['b'] == status
.PASS
584 assert test
.result
.subtests
['c'] == status
.CRASH