| """Run a single test method in this (sub)process and report the result. | |
| Invoked as ``python -m test.support.subprocess_runner MODULE QUALNAME OUTFILE | |
| CONFIG`` by :func:`test.support.isolation.runInSubprocess`. CONFIG is the | |
| marshalled test.support configuration of the parent test run, as a hex string. | |
| The outcome of the test (including that of each individual subtest) is | |
| marshalled to OUTFILE. This module is not meant to be imported. | |
| Import as little as possible before running the test: every module imported | |
| here is state that the test would not see in a normal test run. | |
| """ | |
| import marshal | |
| import sys | |
| import unittest | |
| from unittest.case import _SubTest | |
| if __name__ != '__main__': | |
| raise ImportError('this module cannot be directly imported') | |
| if len(sys.argv) != 5: | |
| print('usage: python -m test.support.subprocess_runner ' | |
| 'MODULE QUALNAME OUTFILE CONFIG', file=sys.stderr) | |
| sys.exit(2) | |
| module, qualname, outfile, config = sys.argv[1:] | |
| # Set up the child before importing the test. | |
| from test.support.isolation import _apply_child_config | |
| _apply_child_config(config) | |
| class _Result(unittest.TestResult): | |
| # Capture per-test durations keyed by test id, so the parent can report the | |
| # subprocess timings instead of its own replay time. | |
| def __init__(self): | |
| super().__init__() | |
| self.id_durations = [] | |
| def addDuration(self, test, elapsed): | |
| super().addDuration(test, elapsed) | |
| self.id_durations.append((test.id(), elapsed)) | |
| # Resolve the qualname in the imported module, rather than letting | |
| # loadTestsFromName() guess where the module name ends: it guesses by trying | |
| # imports that fail, and a failing import pulls in importlib.resources. | |
| __import__(module) | |
| suite = unittest.TestLoader().loadTestsFromName(qualname, sys.modules[module]) | |
| result = _Result() | |
| suite.run(result) | |
| def _outcome(kind, test, detail): | |
| subtest = isinstance(test, _SubTest) | |
| real = test.test_case if subtest else test | |
| return { | |
| 'kind': kind, | |
| 'subtest': subtest, | |
| 'desc': test._subDescription() if subtest else '', | |
| # id() groups outcomes by test method; a non-TestCase (e.g. an | |
| # _ErrorHolder) marks a setUpClass()/setUpModule() fixture failure. | |
| 'id': real.id(), | |
| 'fixture': not isinstance(real, unittest.TestCase), | |
| 'detail': detail, | |
| } | |
| outcomes = [_outcome('failure', t, tb) for t, tb in result.failures] | |
| outcomes += [_outcome('error', t, tb) for t, tb in result.errors] | |
| outcomes += [_outcome('expected_failure', t, tb) | |
| for t, tb in result.expectedFailures] | |
| outcomes += [_outcome('skipped', t, reason) for t, reason in result.skipped] | |
| def _usage(): | |
| """What this process used: peak resident set size in bytes, and the | |
| number of major page faults it took, either of which can be None. | |
| A major page fault is served from disk, so a non-zero count means swapping. | |
| The modules are imported here, after the test has run, so that the test | |
| does not see them. | |
| """ | |
| try: | |
| import resource | |
| except ImportError: | |
| pass | |
| else: | |
| usage = resource.getrusage(resource.RUSAGE_SELF) | |
| # Solaris and illumos leave these fields at 0, which no live process | |
| # has, so treat it as "not supported". | |
| if not usage.ru_maxrss: | |
| return {'maxrss': None, 'majflt': None} | |
| # ru_maxrss is in bytes on macOS, in kilobytes on Linux and the BSDs. | |
| maxrss = usage.ru_maxrss | |
| return {'maxrss': maxrss if sys.platform == 'darwin' else maxrss * 1024, | |
| 'majflt': usage.ru_majflt} | |
| try: | |
| import os | |
| import _winapi | |
| handle = _winapi.OpenProcess( | |
| _winapi.PROCESS_QUERY_LIMITED_INFORMATION, False, os.getpid()) | |
| except (ImportError, OSError): | |
| return {'maxrss': None, 'majflt': None} | |
| try: | |
| info = _winapi.GetProcessMemoryInfo(handle) | |
| finally: | |
| _winapi.CloseHandle(handle) | |
| # PageFaultCount counts all faults, not only the ones served from disk. | |
| return {'maxrss': info['PeakWorkingSetSize'], 'majflt': None} | |
| payload = {'outcomes': outcomes, 'durations': result.id_durations, **_usage()} | |
| with open(outfile, 'wb') as f: | |
| marshal.dump(payload, f) | |
| sys.exit(0) |