|
|
|
|
|
|
|
|
|
|
|
|
|
"""Unit test runner, providing new features on top of unittest module: |
|
- colourized output |
|
- parallel run (UNIX only) |
|
- print failures/tracebacks on CTRL+C |
|
- re-run failed tests only (make test-failed). |
|
|
|
Invocation examples: |
|
- make test |
|
- make test-failed |
|
|
|
Parallel: |
|
- make test-parallel |
|
- make test-process ARGS=--parallel |
|
""" |
|
|
|
from __future__ import print_function |
|
|
|
import atexit |
|
import optparse |
|
import os |
|
import sys |
|
import textwrap |
|
import time |
|
import unittest |
|
|
|
|
|
try: |
|
import ctypes |
|
except ImportError: |
|
ctypes = None |
|
|
|
try: |
|
import concurrencytest |
|
except ImportError: |
|
concurrencytest = None |
|
|
|
import psutil |
|
from psutil._common import hilite |
|
from psutil._common import print_color |
|
from psutil._common import term_supports_colors |
|
from psutil._compat import super |
|
from psutil.tests import CI_TESTING |
|
from psutil.tests import import_module_by_path |
|
from psutil.tests import print_sysinfo |
|
from psutil.tests import reap_children |
|
from psutil.tests import safe_rmpath |
|
|
|
|
|
VERBOSITY = 2 |
|
FAILED_TESTS_FNAME = '.failed-tests.txt' |
|
NWORKERS = psutil.cpu_count() or 1 |
|
USE_COLORS = not CI_TESTING and term_supports_colors() |
|
|
|
HERE = os.path.abspath(os.path.dirname(__file__)) |
|
loadTestsFromTestCase = ( |
|
unittest.defaultTestLoader.loadTestsFromTestCase |
|
) |
|
|
|
|
|
def cprint(msg, color, bold=False, file=None): |
|
if file is None: |
|
file = sys.stderr if color == 'red' else sys.stdout |
|
if USE_COLORS: |
|
print_color(msg, color, bold=bold, file=file) |
|
else: |
|
print(msg, file=file) |
|
|
|
|
|
class TestLoader: |
|
|
|
testdir = HERE |
|
skip_files = ['test_memleaks.py'] |
|
if "WHEELHOUSE_UPLOADER_USERNAME" in os.environ: |
|
skip_files.extend(['test_osx.py', 'test_linux.py', 'test_posix.py']) |
|
|
|
def _get_testmods(self): |
|
return [ |
|
os.path.join(self.testdir, x) |
|
for x in os.listdir(self.testdir) |
|
if x.startswith('test_') |
|
and x.endswith('.py') |
|
and x not in self.skip_files |
|
] |
|
|
|
def _iter_testmod_classes(self): |
|
"""Iterate over all test files in this directory and return |
|
all TestCase classes in them. |
|
""" |
|
for path in self._get_testmods(): |
|
mod = import_module_by_path(path) |
|
for name in dir(mod): |
|
obj = getattr(mod, name) |
|
if isinstance(obj, type) and issubclass( |
|
obj, unittest.TestCase |
|
): |
|
yield obj |
|
|
|
def all(self): |
|
suite = unittest.TestSuite() |
|
for obj in self._iter_testmod_classes(): |
|
test = loadTestsFromTestCase(obj) |
|
suite.addTest(test) |
|
return suite |
|
|
|
def last_failed(self): |
|
|
|
suite = unittest.TestSuite() |
|
if not os.path.isfile(FAILED_TESTS_FNAME): |
|
return suite |
|
with open(FAILED_TESTS_FNAME) as f: |
|
names = f.read().split() |
|
for n in names: |
|
test = unittest.defaultTestLoader.loadTestsFromName(n) |
|
suite.addTest(test) |
|
return suite |
|
|
|
def from_name(self, name): |
|
if name.endswith('.py'): |
|
name = os.path.splitext(os.path.basename(name))[0] |
|
return unittest.defaultTestLoader.loadTestsFromName(name) |
|
|
|
|
|
class ColouredResult(unittest.TextTestResult): |
|
def addSuccess(self, test): |
|
unittest.TestResult.addSuccess(self, test) |
|
cprint("OK", "green") |
|
|
|
def addError(self, test, err): |
|
unittest.TestResult.addError(self, test, err) |
|
cprint("ERROR", "red", bold=True) |
|
|
|
def addFailure(self, test, err): |
|
unittest.TestResult.addFailure(self, test, err) |
|
cprint("FAIL", "red") |
|
|
|
def addSkip(self, test, reason): |
|
unittest.TestResult.addSkip(self, test, reason) |
|
cprint("skipped: %s" % reason.strip(), "brown") |
|
|
|
def printErrorList(self, flavour, errors): |
|
flavour = hilite(flavour, "red", bold=flavour == 'ERROR') |
|
super().printErrorList(flavour, errors) |
|
|
|
|
|
class ColouredTextRunner(unittest.TextTestRunner): |
|
"""A coloured text runner which also prints failed tests on |
|
KeyboardInterrupt and save failed tests in a file so that they can |
|
be re-run. |
|
""" |
|
|
|
resultclass = ColouredResult if USE_COLORS else unittest.TextTestResult |
|
|
|
def __init__(self, *args, **kwargs): |
|
super().__init__(*args, **kwargs) |
|
self.failed_tnames = set() |
|
|
|
def _makeResult(self): |
|
|
|
|
|
self.result = super()._makeResult() |
|
return self.result |
|
|
|
def _write_last_failed(self): |
|
if self.failed_tnames: |
|
with open(FAILED_TESTS_FNAME, "w") as f: |
|
for tname in self.failed_tnames: |
|
f.write(tname + '\n') |
|
|
|
def _save_result(self, result): |
|
if not result.wasSuccessful(): |
|
for t in result.errors + result.failures: |
|
tname = t[0].id() |
|
self.failed_tnames.add(tname) |
|
|
|
def _run(self, suite): |
|
try: |
|
result = super().run(suite) |
|
except (KeyboardInterrupt, SystemExit): |
|
result = self.runner.result |
|
result.printErrors() |
|
raise sys.exit(1) |
|
else: |
|
self._save_result(result) |
|
return result |
|
|
|
def _exit(self, success): |
|
if success: |
|
cprint("SUCCESS", "green", bold=True) |
|
safe_rmpath(FAILED_TESTS_FNAME) |
|
sys.exit(0) |
|
else: |
|
cprint("FAILED", "red", bold=True) |
|
self._write_last_failed() |
|
sys.exit(1) |
|
|
|
def run(self, suite): |
|
result = self._run(suite) |
|
self._exit(result.wasSuccessful()) |
|
|
|
|
|
class ParallelRunner(ColouredTextRunner): |
|
@staticmethod |
|
def _parallelize(suite): |
|
def fdopen(fd, mode, *kwds): |
|
stream = orig_fdopen(fd, mode) |
|
atexit.register(stream.close) |
|
return stream |
|
|
|
|
|
|
|
orig_fdopen = os.fdopen |
|
concurrencytest.os.fdopen = fdopen |
|
forker = concurrencytest.fork_for_tests(NWORKERS) |
|
return concurrencytest.ConcurrentTestSuite(suite, forker) |
|
|
|
@staticmethod |
|
def _split_suite(suite): |
|
serial = unittest.TestSuite() |
|
parallel = unittest.TestSuite() |
|
for test in suite: |
|
if test.countTestCases() == 0: |
|
continue |
|
if isinstance(test, unittest.TestSuite): |
|
test_class = test._tests[0].__class__ |
|
elif isinstance(test, unittest.TestCase): |
|
test_class = test |
|
else: |
|
raise TypeError("can't recognize type %r" % test) |
|
|
|
if getattr(test_class, '_serialrun', False): |
|
serial.addTest(test) |
|
else: |
|
parallel.addTest(test) |
|
return (serial, parallel) |
|
|
|
def run(self, suite): |
|
ser_suite, par_suite = self._split_suite(suite) |
|
par_suite = self._parallelize(par_suite) |
|
|
|
|
|
cprint( |
|
"starting parallel tests using %s workers" % NWORKERS, |
|
"green", |
|
bold=True, |
|
) |
|
t = time.time() |
|
par = self._run(par_suite) |
|
par_elapsed = time.time() - t |
|
|
|
|
|
|
|
orphans = psutil.Process().children() |
|
_gone, alive = psutil.wait_procs(orphans, timeout=1) |
|
if alive: |
|
cprint("alive processes %s" % alive, "red") |
|
reap_children() |
|
|
|
|
|
t = time.time() |
|
ser = self._run(ser_suite) |
|
ser_elapsed = time.time() - t |
|
|
|
|
|
if not par.wasSuccessful() and ser_suite.countTestCases() > 0: |
|
par.printErrors() |
|
par_fails, par_errs, par_skips = map( |
|
len, (par.failures, par.errors, par.skipped) |
|
) |
|
ser_fails, ser_errs, ser_skips = map( |
|
len, (ser.failures, ser.errors, ser.skipped) |
|
) |
|
print( |
|
textwrap.dedent( |
|
""" |
|
+----------+----------+----------+----------+----------+----------+ |
|
| | total | failures | errors | skipped | time | |
|
+----------+----------+----------+----------+----------+----------+ |
|
| parallel | %3s | %3s | %3s | %3s | %.2fs | |
|
+----------+----------+----------+----------+----------+----------+ |
|
| serial | %3s | %3s | %3s | %3s | %.2fs | |
|
+----------+----------+----------+----------+----------+----------+ |
|
""" |
|
% ( |
|
par.testsRun, |
|
par_fails, |
|
par_errs, |
|
par_skips, |
|
par_elapsed, |
|
ser.testsRun, |
|
ser_fails, |
|
ser_errs, |
|
ser_skips, |
|
ser_elapsed, |
|
) |
|
) |
|
) |
|
print( |
|
"Ran %s tests in %.3fs using %s workers" |
|
% ( |
|
par.testsRun + ser.testsRun, |
|
par_elapsed + ser_elapsed, |
|
NWORKERS, |
|
) |
|
) |
|
ok = par.wasSuccessful() and ser.wasSuccessful() |
|
self._exit(ok) |
|
|
|
|
|
def get_runner(parallel=False): |
|
def warn(msg): |
|
cprint(msg + " Running serial tests instead.", "red") |
|
|
|
if parallel: |
|
if psutil.WINDOWS: |
|
warn("Can't run parallel tests on Windows.") |
|
elif concurrencytest is None: |
|
warn("concurrencytest module is not installed.") |
|
elif NWORKERS == 1: |
|
warn("Only 1 CPU available.") |
|
else: |
|
return ParallelRunner(verbosity=VERBOSITY) |
|
return ColouredTextRunner(verbosity=VERBOSITY) |
|
|
|
|
|
|
|
def run_from_name(name): |
|
if CI_TESTING: |
|
print_sysinfo() |
|
suite = TestLoader().from_name(name) |
|
runner = get_runner() |
|
runner.run(suite) |
|
|
|
|
|
def setup(): |
|
psutil._set_debug(True) |
|
|
|
|
|
def main(): |
|
setup() |
|
usage = "python3 -m psutil.tests [opts] [test-name]" |
|
parser = optparse.OptionParser(usage=usage, description="run unit tests") |
|
parser.add_option( |
|
"--last-failed", |
|
action="store_true", |
|
default=False, |
|
help="only run last failed tests", |
|
) |
|
parser.add_option( |
|
"--parallel", |
|
action="store_true", |
|
default=False, |
|
help="run tests in parallel", |
|
) |
|
opts, args = parser.parse_args() |
|
|
|
if not opts.last_failed: |
|
safe_rmpath(FAILED_TESTS_FNAME) |
|
|
|
|
|
loader = TestLoader() |
|
if args: |
|
if len(args) > 1: |
|
parser.print_usage() |
|
return sys.exit(1) |
|
else: |
|
suite = loader.from_name(args[0]) |
|
elif opts.last_failed: |
|
suite = loader.last_failed() |
|
else: |
|
suite = loader.all() |
|
|
|
if CI_TESTING: |
|
print_sysinfo() |
|
runner = get_runner(opts.parallel) |
|
runner.run(suite) |
|
|
|
|
|
if __name__ == '__main__': |
|
main() |
|
|