a subprocess. Several of these tests fail, error or are skipped on purpose.
"""
+import atexit
+import os
import time
import unittest
from test.support import isolation
@classmethod
def setUpClass(cls):
pass
+
+
+# The exit code the samples below die with, after their tests have run.
+EXIT_CODE = 3
+
+
+def _die_at_exit():
+ atexit.register(os._exit, EXIT_CODE)
+
+
+class MethodExitSample(unittest.TestCase):
+
+ @isolation.runInSubprocess()
+ def test_passes_then_dies(self):
+ _die_at_exit()
+
+ @isolation.runInSubprocess()
+ def test_fails_and_dies(self):
+ _die_at_exit()
+ self.fail('the test itself failed')
+
+
+@isolation.runInSubprocess()
+class ClassExitSample(unittest.TestCase):
+
+ def test_pass(self):
+ pass
+
+ def test_dies(self):
+ _die_at_exit()
raise exc from _remote(outcome['detail'])
+def _check_returncode(returncode, output, what):
+ # The subprocess writes its result before exiting, so a non-zero exit code
+ # means it died afterwards, during finalization, unnoticed by the result.
+ if returncode:
+ exc = _SubprocessTestError(
+ f'the subprocess exited with code {returncode} '
+ f'after running the {what}')
+ raise exc from _remote(output)
+
+
def _isolate_method(func):
@functools.wraps(func)
def wrapper(self, /, *args, **kwargs):
raise exc from _remote(output)
# The parent measures this method's own duration (the real cost of the
# isolated run, subprocess startup included), so nothing to forward here.
+ # Replay the outcomes first: a failure of the test itself is more useful.
_replay_outcomes(self, payload['outcomes'])
+ _check_returncode(returncode, output, 'test')
return wrapper
by_id.setdefault(outcome['id'], []).append(outcome)
cls._isolated_outcomes = by_id
cls._isolated_durations = dict(payload.get('durations', ()))
+ # Report the crash from tearDownClass(), after replaying the outcomes.
+ cls._isolated_exit = (returncode, output)
def tearDownClass(cls):
if runningInSubprocess:
orig_tearDownClass(cls)
- else:
- cls._isolated_outcomes = None
- cls._isolated_durations = None
+ return
+ cls._isolated_outcomes = None
+ cls._isolated_durations = None
+ # Missing if an overriding setUpClass() bypassed the subprocess.
+ exited = getattr(cls, '_isolated_exit', None)
+ cls._isolated_exit = None
+ if exited is not None:
+ _check_returncode(*exited, 'class')
def _callSetUp(self):
# In the parent the real test does not run, so neither should setUp().
self.assertEqual(len(result.errors), 1)
self.assertIn('did not run in a subprocess', result.errors[0][1])
+ @support.requires_subprocess()
+ def test_subprocess_dying_after_the_test_is_reported(self):
+ from test._isolated_sample import EXIT_CODE
+ result = self._run('MethodExitSample.test_passes_then_dies')
+ self.assertEqual(result.testsRun, 1)
+ self.assertEqual(len(result.errors), 1)
+ self.assertIn(f'exited with code {EXIT_CODE}', result.errors[0][1])
+
+ @support.requires_subprocess()
+ def test_subprocess_dying_does_not_hide_the_failure(self):
+ result = self._run('MethodExitSample.test_fails_and_dies')
+ self.assertEqual(self._names(result.failures), ['test_fails_and_dies'])
+ self.assertEqual(result.errors, [])
+
+ @support.requires_subprocess()
+ def test_class_subprocess_dying_after_the_tests_is_reported(self):
+ # The tests that ran are still reported, and the crash once, for the class.
+ from test._isolated_sample import EXIT_CODE
+ result = self._run('ClassExitSample')
+ self.assertEqual(result.testsRun, 2)
+ self.assertEqual(result.failures, [])
+ self.assertEqual(len(result.errors), 1)
+ self.assertIn('tearDownClass', str(result.errors[0][0]))
+ self.assertIn(f'exited with code {EXIT_CODE}', result.errors[0][1])
+
def test_skipped_without_subprocess_support(self):
# On a platform without subprocess support the test is skipped in the
# parent, before any subprocess is spawned.