Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 32 additions & 0 deletions Lib/test/_isolated_sample.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -5,6 +5,8 @@
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
Expand DownExpand Up@@ -109,3 +111,33 @@ class BrokenSubclassSample(SubclassingSample):
@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()
25 changes: 22 additions & 3 deletions Lib/test/support/isolation.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -163,6 +163,16 @@ def _raise_fixture_outcome(outcome):
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):
Expand All@@ -180,7 +190,9 @@ 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


Expand DownExpand Up@@ -219,13 +231,20 @@ def setUpClass(cls):
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().
Expand Down
25 changes: 25 additions & 0 deletions Lib/test/test_support.py
Original file line numberDiff line numberDiff line change
Expand Up@@ -1180,6 +1180,31 @@ def test_subclass_bypassing_setupclass_is_reported(self):
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.
Expand Down
Loading