Commit bb7cf8fb authored by Jason Madden's avatar Jason Madden Committed by GitHub

Merge pull request #1313 from gevent/issue1296

Refactoring to run tests from an install
parents 9560b486 5a252e03
...@@ -24,6 +24,7 @@ from _setuputils import DEFINE_MACROS ...@@ -24,6 +24,7 @@ from _setuputils import DEFINE_MACROS
from _setuputils import glob_many from _setuputils import glob_many
from _setuputils import dep_abspath from _setuputils import dep_abspath
from _setuputils import RUNNING_ON_CI from _setuputils import RUNNING_ON_CI
from _setuputils import RUNNING_FROM_CHECKOUT
from _setuputils import cythonize1 from _setuputils import cythonize1
...@@ -51,14 +52,19 @@ ares_configure_command = ' '.join([ ...@@ -51,14 +52,19 @@ ares_configure_command = ' '.join([
def configure_ares(bext, ext): def configure_ares(bext, ext):
print("Embedding c-ares", bext, ext)
bdir = os.path.join(bext.build_temp, 'c-ares') bdir = os.path.join(bext.build_temp, 'c-ares')
ext.include_dirs.insert(0, bdir) ext.include_dirs.insert(0, bdir)
print("Inserted ", bdir, "in include dirs", ext.include_dirs)
if not os.path.isdir(bdir): if not os.path.isdir(bdir):
os.makedirs(bdir) os.makedirs(bdir)
if WIN: if WIN:
shutil.copy("deps\\c-ares\\ares_build.h.dist", os.path.join(bdir, "ares_build.h")) src = "deps\\c-ares\\ares_build.h.dist"
dest = os.path.join(bdir, "ares_build.h")
print("Copying %r to %r" % (src, dest))
shutil.copy(src, dest)
return return
cwd = os.getcwd() cwd = os.getcwd()
...@@ -87,7 +93,8 @@ ARES = Extension(name='gevent.resolver.cares', ...@@ -87,7 +93,8 @@ ARES = Extension(name='gevent.resolver.cares',
depends=glob_many('src/gevent/resolver/dnshelper.c', depends=glob_many('src/gevent/resolver/dnshelper.c',
'src/gevent/resolver/cares_*.[ch]')) 'src/gevent/resolver/cares_*.[ch]'))
ARES.optional = not RUNNING_ON_CI ares_required = RUNNING_ON_CI and RUNNING_FROM_CHECKOUT
ARES.optional = not ares_required
if CARES_EMBED: if CARES_EMBED:
...@@ -108,5 +115,6 @@ if CARES_EMBED: ...@@ -108,5 +115,6 @@ if CARES_EMBED:
else: else:
ARES.libraries.append('cares') ARES.libraries.append('cares')
ARES.define_macros += [('HAVE_NETDB_H', '')] ARES.define_macros += [('HAVE_NETDB_H', '')]
ARES.configure = lambda bext, ext: print("c-ares not embedded, not configuring", bext, ext)
ARES = cythonize1(ARES) ARES = cythonize1(ARES)
...@@ -15,6 +15,7 @@ from glob import glob ...@@ -15,6 +15,7 @@ from glob import glob
from setuptools import Extension as _Extension from setuptools import Extension as _Extension
from setuptools.command.build_ext import build_ext from setuptools.command.build_ext import build_ext
THIS_DIR = os.path.dirname(__file__)
## Exported configurations ## Exported configurations
...@@ -24,7 +25,7 @@ WIN = sys.platform.startswith('win') ...@@ -24,7 +25,7 @@ WIN = sys.platform.startswith('win')
RUNNING_ON_TRAVIS = os.environ.get('TRAVIS') RUNNING_ON_TRAVIS = os.environ.get('TRAVIS')
RUNNING_ON_APPVEYOR = os.environ.get('APPVEYOR') RUNNING_ON_APPVEYOR = os.environ.get('APPVEYOR')
RUNNING_ON_CI = RUNNING_ON_TRAVIS or RUNNING_ON_APPVEYOR RUNNING_ON_CI = RUNNING_ON_TRAVIS or RUNNING_ON_APPVEYOR
RUNNING_FROM_CHECKOUT = os.path.isdir(os.path.join(THIS_DIR, ".git"))
LIBRARIES = [] LIBRARIES = []
...@@ -37,8 +38,6 @@ if WIN: ...@@ -37,8 +38,6 @@ if WIN:
### File handling ### File handling
THIS_DIR = os.path.dirname(__file__)
def quoted_abspath(*segments): def quoted_abspath(*segments):
return '"' + os.path.abspath(os.path.join(*segments)) + '"' return '"' + os.path.abspath(os.path.join(*segments)) + '"'
...@@ -78,7 +77,7 @@ def _parse_environ(key): ...@@ -78,7 +77,7 @@ def _parse_environ(key):
value = value.lower().strip() value = value.lower().strip()
if value in ('1', 'true', 'on', 'yes'): if value in ('1', 'true', 'on', 'yes'):
return True return True
elif value in ('0', 'false', 'off', 'no'): if value in ('0', 'false', 'off', 'no'):
return False return False
raise ValueError('Environment variable %r has invalid value %r. ' raise ValueError('Environment variable %r has invalid value %r. '
'Please set it to 1, 0 or an empty string' % (key, value)) 'Please set it to 1, 0 or an empty string' % (key, value))
......
...@@ -138,7 +138,7 @@ cache: ...@@ -138,7 +138,7 @@ cache:
- '%LOCALAPPDATA%\pip\Cache' - '%LOCALAPPDATA%\pip\Cache'
build_script: build_script:
- "%PYEXE% -m pip install -U --upgrade-strategy=eager -e .[test,events,dnspython]" - "%PYEXE% -m pip install -U --upgrade-strategy=eager .[test,events,dnspython]"
test_script: test_script:
# Run the project tests # Run the project tests
......
...@@ -26,8 +26,6 @@ Cooperative ``subprocess`` module. ...@@ -26,8 +26,6 @@ Cooperative ``subprocess`` module.
from __future__ import absolute_import, print_function from __future__ import absolute_import, print_function
# Can we split this up to make it cleaner? See https://github.com/gevent/gevent/issues/748 # Can we split this up to make it cleaner? See https://github.com/gevent/gevent/issues/748
# pylint: disable=too-many-lines # pylint: disable=too-many-lines
# Import magic
# pylint: disable=undefined-all-variable,undefined-variable
# Most of this we inherit from the standard lib # Most of this we inherit from the standard lib
# pylint: disable=bare-except,too-many-locals,too-many-statements,attribute-defined-outside-init # pylint: disable=bare-except,too-many-locals,too-many-statements,attribute-defined-outside-init
# pylint: disable=too-many-branches,too-many-instance-attributes # pylint: disable=too-many-branches,too-many-instance-attributes
...@@ -51,7 +49,7 @@ from gevent._compat import fspath ...@@ -51,7 +49,7 @@ from gevent._compat import fspath
from gevent._compat import fsencode from gevent._compat import fsencode
from gevent._util import _NONE from gevent._util import _NONE
from gevent._util import copy_globals from gevent._util import copy_globals
from gevent.fileobject import FileObject
from gevent.greenlet import Greenlet, joinall from gevent.greenlet import Greenlet, joinall
spawn = Greenlet.spawn spawn = Greenlet.spawn
import subprocess as __subprocess__ import subprocess as __subprocess__
...@@ -274,7 +272,7 @@ def check_call(*popenargs, **kwargs): ...@@ -274,7 +272,7 @@ def check_call(*popenargs, **kwargs):
cmd = kwargs.get("args") cmd = kwargs.get("args")
if cmd is None: if cmd is None:
cmd = popenargs[0] cmd = popenargs[0]
raise CalledProcessError(retcode, cmd) raise CalledProcessError(retcode, cmd) # pylint:disable=undefined-variable
return 0 return 0
def check_output(*popenargs, **kwargs): def check_output(*popenargs, **kwargs):
...@@ -297,10 +295,10 @@ def check_output(*popenargs, **kwargs): ...@@ -297,10 +295,10 @@ def check_output(*popenargs, **kwargs):
To capture standard error in the result, use ``stderr=STDOUT``:: To capture standard error in the result, use ``stderr=STDOUT``::
>>> check_output(["/bin/sh", "-c", >>> print(check_output(["/bin/sh", "-c",
... "ls -l non_existent_file ; exit 0"], ... "ls -l non_existent_file ; exit 0"],
... stderr=STDOUT) ... stderr=STDOUT).decode('ascii').strip())
'ls: non_existent_file: No such file or directory\n' ls: non_existent_file: No such file or directory
There is an additional optional argument, "input", allowing you to There is an additional optional argument, "input", allowing you to
pass a string to the subprocess's stdin. If you use this argument pass a string to the subprocess's stdin. If you use this argument
...@@ -346,6 +344,7 @@ def check_output(*popenargs, **kwargs): ...@@ -346,6 +344,7 @@ def check_output(*popenargs, **kwargs):
raise raise
retcode = process.poll() retcode = process.poll()
if retcode: if retcode:
# pylint:disable=undefined-variable
raise CalledProcessError(retcode, process.args, output=output) raise CalledProcessError(retcode, process.args, output=output)
return output return output
...@@ -396,6 +395,13 @@ else: ...@@ -396,6 +395,13 @@ else:
_set_inheritable = lambda i, v: True _set_inheritable = lambda i, v: True
def FileObject(*args):
# Defer importing FileObject until we need it
# to allow it to be configured more easily.
from gevent.fileobject import FileObject as _FileObject
globals()['FileObject'] = _FileObject
return _FileObject(*args)
class Popen(object): class Popen(object):
""" """
The underlying process creation and management in this module is The underlying process creation and management in this module is
...@@ -516,6 +522,7 @@ class Popen(object): ...@@ -516,6 +522,7 @@ class Popen(object):
# Validate the combinations of text and universal_newlines # Validate the combinations of text and universal_newlines
if (text is not None and universal_newlines is not None if (text is not None and universal_newlines is not None
and bool(universal_newlines) != bool(text)): and bool(universal_newlines) != bool(text)):
# pylint:disable=undefined-variable
raise SubprocessError('Cannot disambiguate when both text ' raise SubprocessError('Cannot disambiguate when both text '
'and universal_newlines are supplied but ' 'and universal_newlines are supplied but '
'different. Pass one or the other.') 'different. Pass one or the other.')
...@@ -566,6 +573,7 @@ class Popen(object): ...@@ -566,6 +573,7 @@ class Popen(object):
# Python 3, so it's actually a unicode str # Python 3, so it's actually a unicode str
self._communicate_empty_value = '' self._communicate_empty_value = ''
if p2cwrite != -1: if p2cwrite != -1:
if PY3 and text_mode: if PY3 and text_mode:
# Under Python 3, if we left on the 'b' we'd get different results # Under Python 3, if we left on the 'b' we'd get different results
...@@ -814,6 +822,7 @@ class Popen(object): ...@@ -814,6 +822,7 @@ class Popen(object):
"""Construct and return tuple with IO objects: """Construct and return tuple with IO objects:
p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite p2cread, p2cwrite, c2pread, c2pwrite, errread, errwrite
""" """
# pylint:disable=undefined-variable
if stdin is None and stdout is None and stderr is None: if stdin is None and stdout is None and stderr is None:
return (-1, -1, -1, -1, -1, -1) return (-1, -1, -1, -1, -1, -1)
...@@ -896,12 +905,14 @@ class Popen(object): ...@@ -896,12 +905,14 @@ class Popen(object):
def _make_inheritable(self, handle): def _make_inheritable(self, handle):
"""Return a duplicate of handle, which is inheritable""" """Return a duplicate of handle, which is inheritable"""
# pylint:disable=undefined-variable
return DuplicateHandle(GetCurrentProcess(), return DuplicateHandle(GetCurrentProcess(),
handle, GetCurrentProcess(), 0, 1, handle, GetCurrentProcess(), 0, 1,
DUPLICATE_SAME_ACCESS) DUPLICATE_SAME_ACCESS)
def _find_w9xpopen(self): def _find_w9xpopen(self):
"""Find and return absolute path to w9xpopen.exe""" """Find and return absolute path to w9xpopen.exe"""
# pylint:disable=undefined-variable
w9xpopen = os.path.join(os.path.dirname(GetModuleFileName(0)), w9xpopen = os.path.join(os.path.dirname(GetModuleFileName(0)),
"w9xpopen.exe") "w9xpopen.exe")
if not os.path.exists(w9xpopen): if not os.path.exists(w9xpopen):
...@@ -938,7 +949,7 @@ class Popen(object): ...@@ -938,7 +949,7 @@ class Popen(object):
errread, errwrite, errread, errwrite,
unused_restore_signals, unused_start_new_session): unused_restore_signals, unused_start_new_session):
"""Execute program (MS Windows version)""" """Execute program (MS Windows version)"""
# pylint:disable=undefined-variable
assert not pass_fds, "pass_fds not supported on Windows." assert not pass_fds, "pass_fds not supported on Windows."
if not isinstance(args, string_types): if not isinstance(args, string_types):
...@@ -1053,6 +1064,7 @@ class Popen(object): ...@@ -1053,6 +1064,7 @@ class Popen(object):
"""Check if child process has terminated. Returns returncode """Check if child process has terminated. Returns returncode
attribute. attribute.
""" """
# pylint:disable=undefined-variable
if self.returncode is None: if self.returncode is None:
if WaitForSingleObject(self._handle, 0) == WAIT_OBJECT_0: if WaitForSingleObject(self._handle, 0) == WAIT_OBJECT_0:
self.returncode = GetExitCodeProcess(self._handle) self.returncode = GetExitCodeProcess(self._handle)
...@@ -1067,6 +1079,7 @@ class Popen(object): ...@@ -1067,6 +1079,7 @@ class Popen(object):
# XXX unlink # XXX unlink
def _blocking_wait(self): def _blocking_wait(self):
# pylint:disable=undefined-variable
WaitForSingleObject(self._handle, INFINITE) WaitForSingleObject(self._handle, INFINITE)
self.returncode = GetExitCodeProcess(self._handle) self.returncode = GetExitCodeProcess(self._handle)
return self.returncode return self.returncode
...@@ -1098,6 +1111,7 @@ class Popen(object): ...@@ -1098,6 +1111,7 @@ class Popen(object):
def terminate(self): def terminate(self):
"""Terminates the process """Terminates the process
""" """
# pylint:disable=undefined-variable
# Don't terminate a process that we know has already died. # Don't terminate a process that we know has already died.
if self.returncode is not None: if self.returncode is not None:
return return
...@@ -1169,7 +1183,7 @@ class Popen(object): ...@@ -1169,7 +1183,7 @@ class Popen(object):
pass pass
elif stderr == PIPE: elif stderr == PIPE:
errread, errwrite = self.pipe_cloexec() errread, errwrite = self.pipe_cloexec()
elif stderr == STDOUT: elif stderr == STDOUT: # pylint:disable=undefined-variable
if c2pwrite != -1: if c2pwrite != -1:
errwrite = c2pwrite errwrite = c2pwrite
else: # child's stdout is not set, use parent's stdout else: # child's stdout is not set, use parent's stdout
...@@ -1599,6 +1613,7 @@ class CompletedProcess(object): ...@@ -1599,6 +1613,7 @@ class CompletedProcess(object):
def check_returncode(self): def check_returncode(self):
"""Raise CalledProcessError if the exit code is non-zero.""" """Raise CalledProcessError if the exit code is non-zero."""
if self.returncode: if self.returncode:
# pylint:disable=undefined-variable
raise _with_stdout_stderr(CalledProcessError(self.returncode, self.args, self.stdout), self.stderr) raise _with_stdout_stderr(CalledProcessError(self.returncode, self.args, self.stdout), self.stderr)
...@@ -1667,6 +1682,7 @@ def run(*popenargs, **kwargs): ...@@ -1667,6 +1682,7 @@ def run(*popenargs, **kwargs):
raise raise
retcode = process.poll() retcode = process.poll()
if check and retcode: if check and retcode:
# pylint:disable=undefined-variable
raise _with_stdout_stderr(CalledProcessError(retcode, process.args, stdout), stderr) raise _with_stdout_stderr(CalledProcessError(retcode, process.args, stdout), stderr)
return CompletedProcess(process.args, retcode, stdout, stderr) return CompletedProcess(process.args, retcode, stdout, stderr)
...@@ -406,9 +406,6 @@ if LIBUV: ...@@ -406,9 +406,6 @@ if LIBUV:
'test_httpservers.CGIHTTPServerTestCase.test_invaliduri', 'test_httpservers.CGIHTTPServerTestCase.test_invaliduri',
'test_httpservers.CGIHTTPServerTestCase.test_issue19435', 'test_httpservers.CGIHTTPServerTestCase.test_issue19435',
# Sometimes raises LoopExit on CPython
'test_socket.BufferIOTest.testRecvFromIntoArray',
# Unexpected timeouts sometimes # Unexpected timeouts sometimes
'test_smtplib.TooLongLineTests.testLineTooLong', 'test_smtplib.TooLongLineTests.testLineTooLong',
'test_smtplib.GeneralTests.testTimeoutValue', 'test_smtplib.GeneralTests.testTimeoutValue',
...@@ -463,6 +460,10 @@ if LIBUV: ...@@ -463,6 +460,10 @@ if LIBUV:
# This test winds up hanging a long time. # This test winds up hanging a long time.
# Inserting GCs doesn't fix it. # Inserting GCs doesn't fix it.
'test_ssl.ThreadedTests.test_handshake_timeout', 'test_ssl.ThreadedTests.test_handshake_timeout',
# These sometimes raise LoopExit, for no apparent reason.
'test_socket.BufferIOTest.testRecvFromIntoBytearray',
'test_socket.BufferIOTest.testRecvFromIntoArray',
] ]
if PY3: if PY3:
......
...@@ -75,11 +75,13 @@ def run_many(tests, ...@@ -75,11 +75,13 @@ def run_many(tests,
total = 0 total = 0
failed = {} failed = {}
passed = {} passed = {}
total_cases = [0]
total_skipped = [0]
NWORKERS = min(len(tests), NWORKERS) or 1 NWORKERS = min(len(tests), NWORKERS) or 1
pool = ThreadPool(NWORKERS) pool = ThreadPool(NWORKERS)
util.BUFFER_OUTPUT = NWORKERS > 1 util.BUFFER_OUTPUT = NWORKERS > 1 or quiet
def run_one(cmd, **kwargs): def run_one(cmd, **kwargs):
kwargs['quiet'] = quiet kwargs['quiet'] = quiet
...@@ -90,6 +92,8 @@ def run_many(tests, ...@@ -90,6 +92,8 @@ def run_many(tests,
failed[result.name] = [cmd, kwargs] failed[result.name] = [cmd, kwargs]
else: else:
passed[result.name] = True passed[result.name] = True
total_cases[0] += result.run_count
total_skipped[0] += result.skipped_count
results = [] results = []
...@@ -143,7 +147,8 @@ def run_many(tests, ...@@ -143,7 +147,8 @@ def run_many(tests,
except KeyboardInterrupt: except KeyboardInterrupt:
pool.terminate() pool.terminate()
report(total, failed, passed, exit=False, took=time.time() - start, report(total, failed, passed, exit=False, took=time.time() - start,
configured_failing_tests=configured_failing_tests) configured_failing_tests=configured_failing_tests,
total_cases=total_cases[0], total_skipped=total_skipped[0])
log('(partial results)\n') log('(partial results)\n')
raise raise
except: except:
...@@ -153,7 +158,8 @@ def run_many(tests, ...@@ -153,7 +158,8 @@ def run_many(tests,
reap_all() reap_all()
report(total, failed, passed, took=time.time() - start, report(total, failed, passed, took=time.time() - start,
configured_failing_tests=configured_failing_tests) configured_failing_tests=configured_failing_tests,
total_cases=total_cases[0], total_skipped=total_skipped[0])
def discover( def discover(
tests=None, ignore_files=None, tests=None, ignore_files=None,
...@@ -194,7 +200,7 @@ def discover( ...@@ -194,7 +200,7 @@ def discover(
tests = sorted(tests) tests = sorted(tests)
to_process = [] to_process = []
to_import = []
for filename in tests: for filename in tests:
module_name = os.path.splitext(filename)[0] module_name = os.path.splitext(filename)[0]
...@@ -210,11 +216,7 @@ def discover( ...@@ -210,11 +216,7 @@ def discover(
contents = f.read() contents = f.read()
if b'TESTRUNNER' in contents: # test__monkey_patching.py if b'TESTRUNNER' in contents: # test__monkey_patching.py
# XXX: Rework this to avoid importing. # XXX: Rework this to avoid importing.
module = importlib.import_module(qualified_name) to_import.append(qualified_name)
for cmd, options in module.TESTRUNNER():
if remove_options(cmd)[-1] in ignore:
continue
to_process.append((cmd, options))
else: else:
cmd = [sys.executable, '-u'] cmd = [sys.executable, '-u']
if PYPY and PY2: if PYPY and PY2:
...@@ -232,6 +234,15 @@ def discover( ...@@ -232,6 +234,15 @@ def discover(
to_process.append((cmd, options)) to_process.append((cmd, options))
os.chdir(olddir) os.chdir(olddir)
# When we actually execute, do so from the original directory,
# this helps find setup.py
for qualified_name in to_import:
module = importlib.import_module(qualified_name)
for cmd, options in module.TESTRUNNER():
if remove_options(cmd)[-1] in ignore:
continue
to_process.append((cmd, options))
return to_process return to_process
...@@ -275,8 +286,9 @@ def format_seconds(seconds): ...@@ -275,8 +286,9 @@ def format_seconds(seconds):
def report(total, failed, passed, exit=True, took=None, def report(total, failed, passed, exit=True, took=None,
configured_failing_tests=()): configured_failing_tests=(),
# pylint:disable=redefined-builtin,too-many-branches total_cases=0, total_skipped=0):
# pylint:disable=redefined-builtin,too-many-branches,too-many-locals
runtimelog = util.runtimelog runtimelog = util.runtimelog
if runtimelog: if runtimelog:
log('\nLongest-running tests:') log('\nLongest-running tests:')
...@@ -319,7 +331,13 @@ def report(total, failed, passed, exit=True, took=None, ...@@ -319,7 +331,13 @@ def report(total, failed, passed, exit=True, took=None,
log('\n%s/%s unexpected failures', len(failed_unexpected), total, color='error') log('\n%s/%s unexpected failures', len(failed_unexpected), total, color='error')
print_list(failed_unexpected) print_list(failed_unexpected)
else: else:
log('\n%s tests passed%s', total, took) log(
'\nRan %s tests%s in %s files%s',
total_cases,
util._colorize('skipped', " (skipped=%d)" % total_skipped) if total_skipped else '',
total,
took,
)
if exit: if exit:
if failed_unexpected: if failed_unexpected:
......
from __future__ import print_function, absolute_import, division
import re
import sys import sys
import os import os
from . import six from . import six
...@@ -220,10 +222,14 @@ def start(command, quiet=False, **kwargs): ...@@ -220,10 +222,14 @@ def start(command, quiet=False, **kwargs):
class RunResult(object): class RunResult(object):
def __init__(self, code, output=None, name=None): def __init__(self, code,
output=None, name=None,
run_count=0, skipped_count=0):
self.code = code self.code = code
self.output = output self.output = output
self.name = name self.name = name
self.run_count = run_count
self.skipped_count = skipped_count
def __bool__(self): def __bool__(self):
...@@ -236,29 +242,50 @@ class RunResult(object): ...@@ -236,29 +242,50 @@ class RunResult(object):
def _should_show_warning_output(out): def _should_show_warning_output(out):
if b'Warning' in out: if 'Warning' in out:
# Strip out some patterns we specifically do not # Strip out some patterns we specifically do not
# care about. # care about.
# from test.support for monkey-patched tests # from test.support for monkey-patched tests
out = out.replace(b'Warning -- reap_children', b'NADA') out = out.replace('Warning -- reap_children', 'NADA')
out = out.replace(b"Warning -- threading_cleanup", b'NADA') out = out.replace("Warning -- threading_cleanup", 'NADA')
# The below *could* be done with sophisticated enough warning # The below *could* be done with sophisticated enough warning
# filters passed to the children # filters passed to the children
# collections.abc is the new home; setuptools uses the old one, # collections.abc is the new home; setuptools uses the old one,
# as does dnspython # as does dnspython
out = out.replace(b"DeprecationWarning: Using or importing the ABCs", b'NADA') out = out.replace("DeprecationWarning: Using or importing the ABCs", 'NADA')
# libuv poor timer resolution # libuv poor timer resolution
out = out.replace(b'UserWarning: libuv only supports', b'NADA') out = out.replace('UserWarning: libuv only supports', 'NADA')
# Packages on Python 2 # Packages on Python 2
out = out.replace(b'ImportWarning: Not importing directory', b'NADA') out = out.replace('ImportWarning: Not importing directory', 'NADA')
return b'Warning' in out return 'Warning' in out
output_lock = threading.Lock() output_lock = threading.Lock()
def _find_test_status(took, out):
def run(command, **kwargs): 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
buffer_output = kwargs.pop('buffer_output', BUFFER_OUTPUT) buffer_output = kwargs.pop('buffer_output', BUFFER_OUTPUT)
quiet = kwargs.pop('quiet', QUIET) quiet = kwargs.pop('quiet', QUIET)
verbose = not quiet verbose = not quiet
...@@ -282,21 +309,27 @@ def run(command, **kwargs): ...@@ -282,21 +309,27 @@ def run(command, **kwargs):
assert not err assert not err
with output_lock: # pylint:disable=not-context-manager with output_lock: # pylint:disable=not-context-manager
failed = bool(result) 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)): if out and (failed or verbose or _should_show_warning_output(out)):
out = out.strip().decode('utf-8', 'ignore')
if out: if out:
out = ' ' + out.replace('\n', '\n ') out = ' ' + out.replace('\n', '\n ')
out = out.rstrip() out = out.rstrip()
out += '\n' out += '\n'
log('| %s\n%s', name, out) log('| %s\n%s', name, out)
status, run_count, skipped_count = _find_test_status(took, out)
if result: if result:
log('! %s [code %s] [took %.1fs]', name, result, took, color='error') log('! %s [code %s] %s', name, result, status, color='error')
elif not nested: elif not nested:
log('- %s [took %.1fs]', name, took) log('- %s %s', name, status)
if took >= MIN_RUNTIME: if took >= MIN_RUNTIME:
runtimelog.append((-took, name)) runtimelog.append((-took, name))
return RunResult(result, out, name) return RunResult(result, out, name, run_count, skipped_count)
class NoSetupPyFound(Exception):
"Raised by find_setup_py_above"
def find_setup_py_above(a_file): def find_setup_py_above(a_file):
"Return the directory containing setup.py somewhere above *a_file*" "Return the directory containing setup.py somewhere above *a_file*"
...@@ -305,34 +338,68 @@ def find_setup_py_above(a_file): ...@@ -305,34 +338,68 @@ def find_setup_py_above(a_file):
prev, root = root, os.path.dirname(root) prev, root = root, os.path.dirname(root)
if root == prev: if root == prev:
# Let's avoid infinite loops at root # Let's avoid infinite loops at root
raise AssertionError('could not find my setup.py') raise NoSetupPyFound('could not find my setup.py above %r' % (a_file,))
return root 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
class TestServer(unittest.TestCase): if a_class is not None:
args = [] try:
before_delay = 3 return find_setup_py_above(sys.modules[a_class.__module__].__file__)
after_delay = 0.5 except NoSetupPyFound:
popen = None pass
server = None # subclasses define this to be the path to the server.py
start_kwargs = None 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(),))
class ExampleMixin(object):
"Something that uses the examples/ directory"
def find_setup_py(self): def find_setup_py(self):
"Return the directory containing setup.py" "Return the directory containing setup.py"
return find_setup_py_above(__file__) return search_for_setup_py(
# XXX: We need to extend this if we want it to be useful a_file=__file__,
# for other packages; our __file__ won't work for them. a_class=type(self)
# We can look at the CWD, and we can look at the __file__ of the )
# sys.modules[type(self).__module__].
@property @property
def cwd(self): def cwd(self):
try:
root = self.find_setup_py() 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') return os.path.join(root, 'examples')
class TestServer(ExampleMixin,
unittest.TestCase):
args = []
before_delay = 3
after_delay = 0.5
popen = None
server = None # subclasses define this to be the path to the server.py
start_kwargs = None
def start(self): def start(self):
try:
kwargs = self.start_kwargs or {} kwargs = self.start_kwargs or {}
return start([sys.executable, '-u', self.server] + self.args, cwd=self.cwd, **kwargs) return start([sys.executable, '-u', self.server] + self.args, cwd=self.cwd, **kwargs)
except NoSetupPyFound as e:
raise unittest.SkipTest("Unable to locate file/dir to run: %s" % (e,))
def running_server(self): def running_server(self):
from contextlib import contextmanager from contextlib import contextmanager
......
...@@ -7,44 +7,66 @@ import atexit ...@@ -7,44 +7,66 @@ import atexit
from gevent.testing import util from gevent.testing import util
TIMEOUT = 120
# XXX: Generalize this so other packages can use it. # XXX: Generalize this so other packages can use it.
setup_py = util.find_setup_py_above(__file__)
greentest = os.path.join(setup_py, 'src', 'greentest')
TIMEOUT = 120 def find_stdlib_tests():
directory = '%s.%s' % sys.version_info[:2] setup_py = util.search_for_setup_py(a_file=__file__)
full_directory = '%s.%s.%s' % sys.version_info[:3] greentest = os.path.join(setup_py, 'src', 'greentest')
if hasattr(sys, 'pypy_version_info'):
directory = '%s.%s' % sys.version_info[:2]
full_directory = '%s.%s.%s' % sys.version_info[:3]
if hasattr(sys, 'pypy_version_info'):
directory += 'pypy' directory += 'pypy'
full_directory += 'pypy' full_directory += 'pypy'
directory = os.path.join(greentest, directory) directory = os.path.join(greentest, directory)
full_directory = os.path.join(greentest, full_directory) full_directory = os.path.join(greentest, full_directory)
return directory, full_directory
version = '%s.%s.%s' % sys.version_info[:3] def get_python_version():
if sys.version_info[3] == 'alpha': version = '%s.%s.%s' % sys.version_info[:3]
if sys.version_info[3] == 'alpha':
version += 'a%s' % sys.version_info[4] version += 'a%s' % sys.version_info[4]
elif sys.version_info[3] == 'beta': elif sys.version_info[3] == 'beta':
version += 'b%s' % sys.version_info[4] version += 'b%s' % sys.version_info[4]
return version
def get_absolute_pythonpath(): def get_absolute_pythonpath():
paths = [os.path.abspath(p) for p in os.environ.get('PYTHONPATH', '').split(os.pathsep)] paths = [os.path.abspath(p) for p in os.environ.get('PYTHONPATH', '').split(os.pathsep)]
return os.pathsep.join(paths) return os.pathsep.join(paths)
def TESTRUNNER(tests=None): def TESTRUNNER(tests=None):
if not os.path.exists(directory): try:
util.log('WARNING: No test directory found at %s', directory) test_dir, version_test_dir = find_stdlib_tests()
except util.NoSetupPyFound as e:
util.log("WARNING: No setup.py and src/greentest found: %r", e,
color="suboptimal-behaviour")
return
if not os.path.exists(test_dir):
util.log('WARNING: No test directory found at %s', test_dir,
color="suboptimal-behaviour")
return return
with open(os.path.join(directory, 'version')) as f:
with open(os.path.join(test_dir, 'version')) as f:
preferred_version = f.read().strip() preferred_version = f.read().strip()
if preferred_version != version:
util.log('WARNING: The tests in %s/ are from version %s and your Python is %s', directory, preferred_version, version)
version_tests = glob.glob('%s/test_*.py' % full_directory) running_version = get_python_version()
if preferred_version != running_version:
util.log('WARNING: The tests in %s/ are from version %s and your Python is %s',
test_dir, preferred_version, running_version,
color="suboptimal-behaviour")
version_tests = glob.glob('%s/test_*.py' % version_test_dir)
version_tests = sorted(version_tests) version_tests = sorted(version_tests)
if not tests: if not tests:
tests = glob.glob('%s/test_*.py' % directory) tests = glob.glob('%s/test_*.py' % test_dir)
tests = sorted(tests) tests = sorted(tests)
PYTHONPATH = (os.getcwd() + os.pathsep + get_absolute_pythonpath()).rstrip(':') PYTHONPATH = (os.getcwd() + os.pathsep + get_absolute_pythonpath()).rstrip(':')
...@@ -53,7 +75,7 @@ def TESTRUNNER(tests=None): ...@@ -53,7 +75,7 @@ def TESTRUNNER(tests=None):
version_tests = [os.path.basename(x) for x in version_tests] version_tests = [os.path.basename(x) for x in version_tests]
options = { options = {
'cwd': directory, 'cwd': test_dir,
'timeout': TIMEOUT, 'timeout': TIMEOUT,
'setenv': { 'setenv': {
'PYTHONPATH': PYTHONPATH, 'PYTHONPATH': PYTHONPATH,
...@@ -73,11 +95,11 @@ def TESTRUNNER(tests=None): ...@@ -73,11 +95,11 @@ def TESTRUNNER(tests=None):
basic_args = [sys.executable, '-u', '-W', 'ignore', '-m' 'gevent.testing.monkey_test'] basic_args = [sys.executable, '-u', '-W', 'ignore', '-m' 'gevent.testing.monkey_test']
for filename in tests: for filename in tests:
if filename in version_tests: if filename in version_tests:
util.log("Overriding %s from %s with file from %s", filename, directory, full_directory) util.log("Overriding %s from %s with file from %s", filename, test_dir, version_test_dir)
continue continue
yield basic_args + [filename], options.copy() yield basic_args + [filename], options.copy()
options['cwd'] = full_directory options['cwd'] = version_test_dir
for filename in version_tests: for filename in version_tests:
yield basic_args + [filename], options.copy() yield basic_args + [filename], options.copy()
......
...@@ -7,11 +7,7 @@ import re ...@@ -7,11 +7,7 @@ import re
import sys import sys
import unittest import unittest
import gevent
from gevent import socket
from gevent.testing import walk_modules
from gevent.testing import sysinfo
from gevent.testing import util
# Ignore tracebacks: ZeroDivisionError # Ignore tracebacks: ZeroDivisionError
...@@ -39,19 +35,16 @@ class RENormalizingOutputChecker(doctest.OutputChecker): ...@@ -39,19 +35,16 @@ class RENormalizingOutputChecker(doctest.OutputChecker):
return doctest.OutputChecker.check_output(self, want, got, optionflags) return doctest.OutputChecker.check_output(self, want, got, optionflags)
FORBIDDEN_MODULES = set() FORBIDDEN_MODULES = set()
if sysinfo.WIN:
FORBIDDEN_MODULES |= {
# Uses commands only found on posix
'gevent.subprocess',
}
class Modules(object): class Modules(object):
def __init__(self, allowed_modules): def __init__(self, allowed_modules):
from gevent.testing import walk_modules
self.allowed_modules = allowed_modules self.allowed_modules = allowed_modules
self.modules = set() self.modules = set()
for path, module in walk_modules(): for path, module in walk_modules(recursive=True):
self.add_module(module, path) self.add_module(module, path)
...@@ -71,19 +64,36 @@ class Modules(object): ...@@ -71,19 +64,36 @@ class Modules(object):
return iter(self.modules) return iter(self.modules)
def main(): def main(): # pylint:disable=too-many-locals
cwd = os.getcwd() cwd = os.getcwd()
# Use pure_python to get the correct module source and docstrings
os.environ['PURE_PYTHON'] = '1'
import gevent
from gevent import socket
from gevent.testing import util
from gevent.testing import sysinfo
if sysinfo.WIN:
FORBIDDEN_MODULES.update({
# Uses commands only found on posix
'gevent.subprocess',
})
try: try:
allowed_modules = sys.argv[1:] allowed_modules = sys.argv[1:]
sys.path.append('.') sys.path.append('.')
os.chdir(util.find_setup_py_above(__file__))
globs = {'myfunction': myfunction, 'gevent': gevent, 'socket': socket} globs = {
'myfunction': myfunction,
'gevent': gevent,
'socket': socket,
}
modules = Modules(allowed_modules) modules = Modules(allowed_modules)
modules.add_module('setup', 'setup.py')
if not modules: if not modules:
sys.exit('No modules found matching %s' % ' '.join(allowed_modules)) sys.exit('No modules found matching %s' % ' '.join(allowed_modules))
...@@ -91,8 +101,9 @@ def main(): ...@@ -91,8 +101,9 @@ def main():
checker = RENormalizingOutputChecker(( checker = RENormalizingOutputChecker((
# Normalize subprocess.py: BSD ls is in the example, gnu ls outputs # Normalize subprocess.py: BSD ls is in the example, gnu ls outputs
# 'cannot access' # 'cannot access'
(re.compile('cannot access non_existent_file: No such file or directory'), (re.compile(
'non_existent_file: No such file or directory'), "ls: cannot access 'non_existent_file': No such file or directory"),
"ls: non_existent_file: No such file or directory"),
# Python 3 bytes add a "b". # Python 3 bytes add a "b".
(re.compile(r'b(".*?")'), r"\1"), (re.compile(r'b(".*?")'), r"\1"),
(re.compile(r"b('.*?')"), r"\1"), (re.compile(r"b('.*?')"), r"\1"),
...@@ -106,11 +117,12 @@ def main(): ...@@ -106,11 +117,12 @@ def main():
if re.search(br'^\s*>>> ', contents, re.M): if re.search(br'^\s*>>> ', contents, re.M):
s = doctest.DocTestSuite(m, extraglobs=globs, checker=checker) s = doctest.DocTestSuite(m, extraglobs=globs, checker=checker)
test_count = len(s._tests) test_count = len(s._tests)
print('%s (from %s): %s tests' % (m, path, test_count)) util.log('%s (from %s): %s tests', m, path, test_count)
suite.addTest(s) suite.addTest(s)
modules_count += 1 modules_count += 1
tests_count += test_count tests_count += test_count
print('Total: %s tests in %s modules' % (tests_count, modules_count))
util.log('Total: %s tests in %s modules', tests_count, modules_count)
# TODO: Pass this off to unittest.main() # TODO: Pass this off to unittest.main()
runner = unittest.TextTestRunner(verbosity=2) runner = unittest.TextTestRunner(verbosity=2)
runner.run(suite) runner.run(suite)
......
...@@ -2,13 +2,19 @@ import sys ...@@ -2,13 +2,19 @@ import sys
import os import os
import glob import glob
import time import time
import unittest
import gevent.testing as greentest import gevent.testing as greentest
from gevent.testing import util from gevent.testing import util
this_dir = os.path.dirname(__file__)
cwd = '../../examples/' def _find_files_to_ignore():
ignore = [ old_dir = os.getcwd()
try:
os.chdir(this_dir)
result = [
'wsgiserver.py', 'wsgiserver.py',
'wsgiserver_ssl.py', 'wsgiserver_ssl.py',
'webproxy.py', 'webproxy.py',
...@@ -17,8 +23,13 @@ ignore = [ ...@@ -17,8 +23,13 @@ ignore = [
'unixsocket_client.py', 'unixsocket_client.py',
'psycopg2_pool.py', 'psycopg2_pool.py',
'geventsendfile.py', 'geventsendfile.py',
] ]
ignore += [x[14:] for x in glob.glob('test__example_*.py')] result += [x[14:] for x in glob.glob('test__example_*.py')]
finally:
os.chdir(old_dir)
return result
default_time_range = (2, 4) default_time_range = (2, 4)
time_ranges = { time_ranges = {
...@@ -26,7 +37,7 @@ time_ranges = { ...@@ -26,7 +37,7 @@ time_ranges = {
'processes.py': (0, 4) 'processes.py': (0, 4)
} }
class _AbstractTestMixin(object): class _AbstractTestMixin(util.ExampleMixin):
time_range = (2, 4) time_range = (2, 4)
filename = None filename = None
...@@ -35,7 +46,7 @@ class _AbstractTestMixin(object): ...@@ -35,7 +46,7 @@ class _AbstractTestMixin(object):
min_time, max_time = self.time_range min_time, max_time = self.time_range
if util.run([sys.executable, '-u', self.filename], if util.run([sys.executable, '-u', self.filename],
timeout=max_time, timeout=max_time,
cwd=cwd, cwd=self.cwd,
quiet=True, quiet=True,
buffer_output=True, buffer_output=True,
nested=True, nested=True,
...@@ -45,17 +56,32 @@ class _AbstractTestMixin(object): ...@@ -45,17 +56,32 @@ class _AbstractTestMixin(object):
took = time.time() - start took = time.time() - start
self.assertGreaterEqual(took, min_time) self.assertGreaterEqual(took, min_time)
for filename in glob.glob(cwd + '/*.py'): def _build_test_classes():
result = {}
try:
example_dir = util.ExampleMixin().cwd
except unittest.SkipTest:
util.log("WARNING: No examples dir found", color='suboptimal-behaviour')
return result
ignore = _find_files_to_ignore()
for filename in glob.glob(example_dir + '/*.py'):
bn = os.path.basename(filename) bn = os.path.basename(filename)
if bn in ignore: if bn in ignore:
continue continue
tc = type('Test_' + bn, tc = type(
'Test_' + bn,
(_AbstractTestMixin, greentest.TestCase), (_AbstractTestMixin, greentest.TestCase),
{ {
'filename': bn, 'filename': bn,
'time_range': time_ranges.get(bn, _AbstractTestMixin.time_range) 'time_range': time_ranges.get(bn, _AbstractTestMixin.time_range)
}) }
locals()[tc.__name__] = tc )
result[tc.__name__] = tc
return result
for k, v in _build_test_classes().items():
locals()[k] = v
if __name__ == '__main__': if __name__ == '__main__':
greentest.main() greentest.main()
...@@ -45,7 +45,7 @@ import time ...@@ -45,7 +45,7 @@ import time
import unittest import unittest
import weakref import weakref
import lock_tests from gevent.tests import lock_tests
# A trivial mutable counter. # A trivial mutable counter.
......
...@@ -19,7 +19,7 @@ try: ...@@ -19,7 +19,7 @@ try:
except ImportError: except ImportError:
_testcapi = None _testcapi = None
import lock_tests # gevent: use local copy from gevent.tests import lock_tests # gevent: use local copy
# A trivial mutable counter. # A trivial mutable counter.
class Counter(object): class Counter(object):
......
...@@ -24,7 +24,7 @@ except: ...@@ -24,7 +24,7 @@ except:
# some lib_pypy/_testcapimodule.o file is truncated # some lib_pypy/_testcapimodule.o file is truncated
_testcapi = None _testcapi = None
import lock_tests # gevent: use local copy from gevent.tests import lock_tests # gevent: use local copy
# A trivial mutable counter. # A trivial mutable counter.
class Counter(object): class Counter(object):
......
...@@ -17,7 +17,7 @@ import weakref ...@@ -17,7 +17,7 @@ import weakref
import os import os
import subprocess import subprocess
import lock_tests # gevent: use our local copy from gevent.tests import lock_tests # gevent: use our local copy
from test import support from test import support
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment