386 lines
11 KiB
Python
386 lines
11 KiB
Python
#!/usr/bin/env python3
|
|
|
|
# Copyright (c) 2009, Giampaolo Rodola'. All rights reserved.
|
|
# Use of this source code is governed by a BSD-style license that can be
|
|
# found in the LICENSE file.
|
|
|
|
"""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 # pip install 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 = ( # noqa: N816
|
|
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):
|
|
# ...from previously failed test run
|
|
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):
|
|
# Store result instance so that it can be accessed on
|
|
# KeyboardInterrupt.
|
|
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
|
|
|
|
# Monkey patch concurrencytest lib bug (fdopen() stream not closed).
|
|
# https://github.com/cgoldberg/concurrencytest/issues/11
|
|
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)
|
|
|
|
# run parallel
|
|
cprint(
|
|
"starting parallel tests using %s workers" % NWORKERS,
|
|
"green",
|
|
bold=True,
|
|
)
|
|
t = time.time()
|
|
par = self._run(par_suite)
|
|
par_elapsed = time.time() - t
|
|
|
|
# At this point we should have N zombies (the workers), which
|
|
# will disappear with wait().
|
|
orphans = psutil.Process().children()
|
|
_gone, alive = psutil.wait_procs(orphans, timeout=1)
|
|
if alive:
|
|
cprint("alive processes %s" % alive, "red")
|
|
reap_children()
|
|
|
|
# run serial
|
|
t = time.time()
|
|
ser = self._run(ser_suite)
|
|
ser_elapsed = time.time() - t
|
|
|
|
# print
|
|
if not par.wasSuccessful() and ser_suite.countTestCases() > 0:
|
|
par.printErrors() # print them again at the bottom
|
|
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)
|
|
|
|
|
|
# Used by test_*,py modules.
|
|
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
|
|
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()
|