You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

642 lines
18 KiB
Python

from __future__ import print_function, absolute_import, division
import re
import sys
import os
import traceback
import unittest
import threading
import subprocess
from time import sleep
from . import six
from gevent._config import validate_bool
from gevent._compat import perf_counter
from gevent.monkey import get_original
# pylint: disable=broad-except,attribute-defined-outside-init
runtimelog = []
MIN_RUNTIME = 1.0
BUFFER_OUTPUT = False
# This is set by the testrunner, defaulting to true (be quiet)
# But if we're run standalone, default to false
QUIET = validate_bool(os.environ.get('GEVENTTEST_QUIET', '0'))
class Popen(subprocess.Popen):
"""
Depending on when we're imported and if the process has been monkey-patched,
this could use cooperative or native Popen.
"""
timer = None # a threading.Timer instance
def __enter__(self):
return self
def __exit__(self, *args):
kill(self)
# Coloring code based on zope.testrunner
# These colors are carefully chosen to have enough contrast
# on terminals with both black and white background.
_colorscheme = {
'normal': 'normal',
'default': 'default',
'actual-output': 'red',
'character-diffs': 'magenta',
'debug': 'cyan',
'diff-chunk': 'magenta',
'error': 'brightred',
'error-number': 'brightred',
'exception': 'red',
'expected-output': 'green',
'failed-example': 'cyan',
'filename': 'lightblue',
'info': 'normal',
'lineno': 'lightred',
'number': 'green',
'ok-number': 'green',
'skipped': 'brightyellow',
'slow-test': 'brightmagenta',
'suboptimal-behaviour': 'magenta',
'testname': 'lightcyan',
'warning': 'cyan',
}
_prefixes = [
('dark', '0;'),
('light', '1;'),
('bright', '1;'),
('bold', '1;'),
]
_colorcodes = {
'default': 0,
'normal': 0,
'black': 30,
'red': 31,
'green': 32,
'brown': 33, 'yellow': 33,
'blue': 34,
'magenta': 35,
'cyan': 36,
'grey': 37, 'gray': 37, 'white': 37
}
def _color_code(color):
prefix_code = ''
for prefix, code in _prefixes:
if color.startswith(prefix):
color = color[len(prefix):]
prefix_code = code
break
color_code = _colorcodes[color]
return '\033[%s%sm' % (prefix_code, color_code)
def _color(what):
return _color_code(_colorscheme[what])
def _colorize(what, message, normal='normal'):
return _color(what) + message + _color(normal)
def log(message, *args, **kwargs):
"""
Log a *message*
:keyword str color: One of the values from _colorscheme
"""
color = kwargs.pop('color', 'normal')
if args:
string = message % args
else:
string = message
string = _colorize(color, string)
with output_lock: # pylint:disable=not-context-manager
sys.stderr.write(string + '\n')
def debug(message, *args, **kwargs):
"""
Log the *message* only if we're not in quiet mode.
"""
if not QUIET:
kwargs.setdefault('color', 'debug')
log(message, *args, **kwargs)
def killpg(pid):
if not hasattr(os, 'killpg'):
return
try:
return os.killpg(pid, 9)
except OSError as ex:
if ex.errno != 3:
log('killpg(%r, 9) failed: %s: %s', pid, type(ex).__name__, ex)
except Exception as ex:
log('killpg(%r, 9) failed: %s: %s', pid, type(ex).__name__, ex)
def kill_processtree(pid):
ignore_msg = 'ERROR: The process "%s" not found.' % pid
err = Popen('taskkill /F /PID %s /T' % pid, stderr=subprocess.PIPE).communicate()[1]
if err and err.strip() not in [ignore_msg, '']:
log('%r', err)
def _kill(popen):
if hasattr(popen, 'kill'):
try:
popen.kill()
except OSError as ex:
if ex.errno == 3: # No such process
return
if ex.errno == 13: # Permission denied (translated from windows error 5: "Access is denied")
return
raise
else:
try:
os.kill(popen.pid, 9)
except EnvironmentError:
pass
def kill(popen):
if popen.timer is not None:
popen.timer.cancel()
popen.timer = None
if popen.poll() is not None:
return
popen.was_killed = True
try:
if getattr(popen, 'setpgrp_enabled', None):
killpg(popen.pid)
elif sys.platform.startswith('win'):
kill_processtree(popen.pid)
except Exception:
traceback.print_exc()
try:
_kill(popen)
except Exception:
traceback.print_exc()
try:
popen.wait()
except Exception:
traceback.print_exc()
# A set of environment keys we ignore for printing purposes
IGNORED_GEVENT_ENV_KEYS = {
'GEVENTTEST_QUIET',
'GEVENT_DEBUG',
'GEVENTSETUP_EV_VERIFY',
'GEVENTSETUP_EMBED',
}
# A set of (name, value) pairs we ignore for printing purposes.
# These should match the defaults.
IGNORED_GEVENT_ENV_ITEMS = {
('GEVENT_RESOLVER', 'thread'),
('GEVENT_RESOLVER_NAMESERVERS', '8.8.8.8'),
('GEVENTTEST_USE_RESOURCES', 'all'),
}
def getname(command, env=None, setenv=None):
result = []
env = (env or os.environ).copy()
env.update(setenv or {})
for key, value in sorted(env.items()):
if not key.startswith('GEVENT'):
continue
if key in IGNORED_GEVENT_ENV_KEYS:
continue
if (key, value) in IGNORED_GEVENT_ENV_ITEMS:
continue
result.append('%s=%s' % (key, value))
if isinstance(command, six.string_types):
result.append(command)
else:
result.extend(command)
return ' '.join(result)
def start(command, quiet=False, **kwargs):
timeout = kwargs.pop('timeout', None)
preexec_fn = None
if not os.environ.get('DO_NOT_SETPGRP'):
preexec_fn = getattr(os, 'setpgrp', None)
env = kwargs.pop('env', None)
setenv = kwargs.pop('setenv', None) or {}
name = getname(command, env=env, setenv=setenv)
if preexec_fn is not None:
setenv['DO_NOT_SETPGRP'] = '1'
if setenv:
env = env.copy() if env else os.environ.copy()
env.update(setenv)
if not quiet:
log('+ %s', name)
popen = Popen(command, preexec_fn=preexec_fn, env=env, **kwargs)
popen.name = name
popen.setpgrp_enabled = preexec_fn is not None
popen.was_killed = False
if timeout is not None:
t = get_original('threading', 'Timer')(timeout, kill, args=(popen, ))
popen.timer = t
t.setDaemon(True)
t.start()
popen.timer = t
return popen
class RunResult(object):
"""
The results of running an external command.
If the command was successful, this has a boolean
value of True; otherwise, a boolean value of false.
The integer value of this object is the command's exit code.
"""
def __init__(self,
command,
run_kwargs,
code,
output=None, # type: str
error=None, # type: str
name=None,
run_count=0, skipped_count=0):
self.command = command
self.run_kwargs = run_kwargs
self.code = code
self.output = output
self.error = error
self.name = name
self.run_count = run_count
self.skipped_count = skipped_count
@property
def output_lines(self):
return self.output.splitlines()
def __bool__(self):
return not bool(self.code)
__nonzero__ = __bool__
def __int__(self):
return self.code
def __repr__(self):
return (
"RunResult of: %r\n"
"Code: %s\n"
"kwargs: %r\n"
"Output:\n"
"----\n"
"%s"
"----\n"
"Error:\n"
"----\n"
"%s"
"----\n"
) % (
self.command,
self.code,
self.run_kwargs,
self.output,
self.error
)
def _should_show_warning_output(out):
if 'Warning' in out:
# Strip out some patterns we specifically do not
# care about.
# from test.support for monkey-patched tests
out = out.replace('Warning -- reap_children', 'NADA')
out = out.replace("Warning -- threading_cleanup", 'NADA')
# The below *could* be done with sophisticated enough warning
# filters passed to the children
# collections.abc is the new home; setuptools uses the old one,
# as does dnspython
out = out.replace("DeprecationWarning: Using or importing the ABCs", 'NADA')
# libuv poor timer resolution
out = out.replace('UserWarning: libuv only supports', 'NADA')
# Packages on Python 2
out = out.replace('ImportWarning: Not importing directory', 'NADA')
# Testing that U mode does the same thing
out = out.replace("DeprecationWarning: 'U' mode is deprecated", 'NADA')
out = out.replace("DeprecationWarning: dns.hash module", 'NADA')
return 'Warning' in out
output_lock = threading.Lock()
def _find_test_status(took, out):
status = '[took %.1fs%s]'
skipped = ''
run_count = 0
skipped_count = 0
if out:
m = re.search(r"Ran (\d+) tests in", out)
if m:
result = out[m.start():m.end()]
status = status.replace('took', result)
run_count = int(out[m.start(1):m.end(1)])
m = re.search(r' \(skipped=(\d+)\)$', out)
if m:
skipped = _colorize('skipped', out[m.start():m.end()])
skipped_count = int(out[m.start(1):m.end(1)])
status = status % (took, skipped)
if took > 10:
status = _colorize('slow-test', status)
return status, run_count, skipped_count
def run(command, **kwargs): # pylint:disable=too-many-locals
"""
Execute *command*, returning a `RunResult`.
This blocks until *command* finishes or until it times out.
"""
buffer_output = kwargs.pop('buffer_output', BUFFER_OUTPUT)
quiet = kwargs.pop('quiet', QUIET)
verbose = not quiet
nested = kwargs.pop('nested', False)
if buffer_output:
assert 'stdout' not in kwargs and 'stderr' not in kwargs, kwargs
kwargs['stderr'] = subprocess.STDOUT
kwargs['stdout'] = subprocess.PIPE
popen = start(command, quiet=nested, **kwargs)
name = popen.name
try:
time_start = perf_counter()
out, err = popen.communicate()
took = perf_counter() - time_start
if popen.was_killed or popen.poll() is None:
result = 'TIMEOUT'
else:
result = popen.poll()
finally:
kill(popen)
assert popen.timer is None
failed = bool(result)
if out:
out = out.strip()
out = out if isinstance(out, str) else out.decode('utf-8', 'ignore')
if out and (failed or verbose or _should_show_warning_output(out)):
out = ' ' + out.replace('\n', '\n ')
out = out.rstrip()
out += '\n'
log('| %s\n%s', name, out)
status, run_count, skipped_count = _find_test_status(took, out)
if result:
log('! %s [code %s] %s', name, result, status, color='error')
elif not nested:
log('- %s %s', name, status)
if took >= MIN_RUNTIME:
runtimelog.append((-took, name))
return RunResult(
command, kwargs, result,
output=out, error=err,
name=name,
run_count=run_count,
skipped_count=skipped_count
)
class NoSetupPyFound(Exception):
"Raised by find_setup_py_above"
def find_setup_py_above(a_file):
"Return the directory containing setup.py somewhere above *a_file*"
root = os.path.dirname(os.path.abspath(a_file))
while not os.path.exists(os.path.join(root, 'setup.py')):
prev, root = root, os.path.dirname(root)
if root == prev:
# Let's avoid infinite loops at root
raise NoSetupPyFound('could not find my setup.py above %r' % (a_file,))
return root
def search_for_setup_py(a_file=None, a_module_name=None, a_class=None, climb_cwd=True):
if a_file is not None:
try:
return find_setup_py_above(a_file)
except NoSetupPyFound:
pass
if a_class is not None:
try:
return find_setup_py_above(sys.modules[a_class.__module__].__file__)
except NoSetupPyFound:
pass
if a_module_name is not None:
try:
return find_setup_py_above(sys.modules[a_module_name].__file__)
except NoSetupPyFound:
pass
if climb_cwd:
return find_setup_py_above("./dne")
raise NoSetupPyFound("After checking %r" % (locals(),))
def _version_dir_components():
directory = '%s.%s' % sys.version_info[:2]
full_directory = '%s.%s.%s' % sys.version_info[:3]
if hasattr(sys, 'pypy_version_info'):
directory += 'pypy'
full_directory += 'pypy'
return directory, full_directory
def find_stdlib_tests():
"""
Return a sequence of directories that could contain
stdlib tests for the running version of Python.
The most specific tests are at the end of the sequence.
No checks are performed on existence of the directories.
"""
setup_py = search_for_setup_py(a_file=__file__)
greentest = os.path.join(setup_py, 'src', 'greentest')
directory, full_directory = _version_dir_components()
directory = '%s.%s' % sys.version_info[:2]
full_directory = '%s.%s.%s' % sys.version_info[:3]
if hasattr(sys, 'pypy_version_info'):
directory += 'pypy'
full_directory += 'pypy'
directory = os.path.join(greentest, directory)
full_directory = os.path.join(greentest, full_directory)
return directory, full_directory
def absolute_pythonpath():
"""
Return the PYTHONPATH environment variable (if set) with each
entry being an absolute path. If not set, returns None.
"""
if 'PYTHONPATH' not in os.environ:
return None
path = os.environ['PYTHONPATH']
path = [os.path.abspath(p) for p in path.split(os.path.pathsep)]
return os.path.pathsep.join(path)
class ExampleMixin(object):
"""
Something that uses the ``examples/`` directory
from the root of the gevent distribution.
The `cwd` property is set to the root of the gevent distribution.
"""
#: Arguments to pass to the example file.
example_args = []
before_delay = 3
after_delay = 0.5
#: Path of the example Python file, relative to `cwd`
example = None # subclasses define this to be the path to the server.py
#: Keyword arguments to pass to the start or run method.
start_kwargs = None
def find_setup_py(self):
"Return the directory containing setup.py"
return search_for_setup_py(
a_file=__file__,
a_class=type(self)
)
@property
def cwd(self):
try:
root = self.find_setup_py()
except NoSetupPyFound as e:
raise unittest.SkipTest("Unable to locate file/dir to run: %s" % (e,))
return os.path.join(root, 'examples')
@property
def setenv(self):
"""
Returns a dictionary of environment variables to set for the
child in addition to (or replacing) the ones already in the
environment.
Since the child is run in `cwd`, relative paths in ``PYTHONPATH``
need to be converted to absolute paths.
"""
abs_pythonpath = absolute_pythonpath()
return {'PYTHONPATH': abs_pythonpath} if abs_pythonpath else None
def _start(self, meth):
if getattr(self, 'args', None):
raise AssertionError("Invalid test", self, self.args)
if getattr(self, 'server', None):
raise AssertionError("Invalid test", self, self.server)
try:
# These could be or are properties that can raise
server = self.example
server_dir = self.cwd
except NoSetupPyFound as e:
raise unittest.SkipTest("Unable to locate file/dir to run: %s" % (e,))
kwargs = self.start_kwargs or {}
setenv = self.setenv
if setenv:
if 'setenv' in kwargs:
kwargs['setenv'].update(setenv)
else:
kwargs['setenv'] = setenv
return meth(
[sys.executable, '-W', 'ignore', '-u', server] + self.example_args,
cwd=server_dir,
**kwargs
)
def start_example(self):
return self._start(meth=start)
def run_example(self):# run() is a unittest method.
return self._start(meth=run)
class TestServer(ExampleMixin,
unittest.TestCase):
popen = None
def running_server(self):
from contextlib import contextmanager
@contextmanager
def running_server():
with self.start_example() as popen:
self.popen = popen
self.before()
yield
self.after()
return running_server()
def test(self):
with self.running_server():
self._run_all_tests()
def before(self):
if self.before_delay is not None:
sleep(self.before_delay)
self.assertIsNone(self.popen.poll(),
'%s died with code %s' % (
self.example, self.popen.poll(),
))
def after(self):
if self.after_delay is not None:
sleep(self.after_delay)
self.assertIsNone(self.popen.poll(),
'%s died with code %s' % (
self.example, self.popen.poll(),
))
def _run_all_tests(self):
ran = False
for method in sorted(dir(self)):
if method.startswith('_test'):
function = getattr(self, method)
if callable(function):
function()
ran = True
assert ran
class alarm(threading.Thread):
# can't use signal.alarm because of Windows
def __init__(self, timeout):
threading.Thread.__init__(self)
self.setDaemon(True)
self.timeout = timeout
self.start()
def run(self):
sleep(self.timeout)
sys.stderr.write('Timeout.\n')
os._exit(5)