Commit 0d037983 authored by Denis Bilenko's avatar Denis Bilenko

many improvements to tests

- testrunner.py: implement --full and --discover
- setup.py now recognizes GEVENTSETUP_EV_VERIFY env var which is passed as EV_VERIFY to make
- relevant env vars are now logged long test names
- add support for expected failures
- allow passing tests on the command line in test___monkey_patching.py
- show longest running tests before final status
- make exit status report number of failing tests
- greentest/util.py: add 'setenv' and 'name' arguments to run()
- add tests_that_dont_use_resolver.txt
- add expected_failures.txt
- fix test___monkey_patching.py to do clean up via atexit
parent 1d0c529b
...@@ -5,19 +5,16 @@ python: ...@@ -5,19 +5,16 @@ python:
- "2.7" - "2.7"
env: env:
- -
- GEVENT_RESOLVER=ares - GEVENTSETUP_EV_VERIFY=3
- GEVENT_FILE=thread - GEVENT_FILE=thread
- GEVENT_BACKEND=select - GEVENT_BACKEND=select GEVENTSETUP_EV_VERIFY=3
- GEVENT_BACKEND=poll - GEVENT_BACKEND=poll GEVENTSETUP_EV_VERIFY=3
- GEVENT_BACKEND=signalfd - GEVENT_BACKEND=signalfd
- GEVENT_BACKEND=nosigmask - GEVENT_BACKEND=nosigmask
- GEVENT_RESOLVER=ares GEVENT_BACKEND=signalfd - GEVENT_BACKEND=noinotify
- GEVENT_RESOLVER=ares GEVENT_FILE=thread
- NWORKERS=1 TESTS="*dns*.py" GEVENT_RESOLVER=ares GEVENTARES_SERVERS=8.8.8.8
matrix: matrix:
include: allow_failures:
- python: 2.7 GEVENT_BACKEND=signalfd
env: TESTS=xtest_pep8.py INSTALL_PEP8=1
install: install:
- deactivate - deactivate
- export VER=$TRAVIS_PYTHON_VERSION - export VER=$TRAVIS_PYTHON_VERSION
...@@ -34,14 +31,14 @@ install: ...@@ -34,14 +31,14 @@ install:
- sudo dpkg -i python$VER-psycopg2_2.4.5_i386.deb - sudo dpkg -i python$VER-psycopg2_2.4.5_i386.deb
- sudo dpkg -i python$VER-pysendfile_2.0.0_i386.deb - sudo dpkg -i python$VER-pysendfile_2.0.0_i386.deb
- tar -xf web.py-0.37.tar.gz && cd web.py-0.37 && sudo $PYTHON setup.py -q install && cd - - tar -xf web.py-0.37.tar.gz && cd web.py-0.37 && sudo $PYTHON setup.py -q install && cd -
- if [[ $INSTALL_PEP8 == '1' ]]; then sudo pip install --use-mirrors -q pep8; fi - if [[ $VER == '2.7' ]]; then sudo pip install --use-mirrors -q pep8; fi
- cython --version - cython --version
- $PYTHON -c 'import greenlet; print greenlet, greenlet.__version__; import psycopg2; print psycopg2, psycopg2.__version__; import web; print web, web.__version__' - $PYTHON -c 'import greenlet; print greenlet, greenlet.__version__; import psycopg2; print psycopg2, psycopg2.__version__; import web; print web, web.__version__'
- "echo `date`: Started installing gevent" - "echo `date`: Started installing gevent"
- sudo $PYTHON setup.py install - sudo $PYTHON setup.py install
script: script:
- "echo `date`: Started testing" - "echo `date`: Started testing"
- cd greentest && $PYTHON testrunner.py $TESTS - cd greentest && $PYTHON testrunner.py --full --expected expected_failures.txt
- "echo `date`: Finished testing" - "echo `date`: Finished testing"
notifications: notifications:
email: email:
...@@ -49,3 +46,4 @@ notifications: ...@@ -49,3 +46,4 @@ notifications:
- denis.bilenko@gmail.com - denis.bilenko@gmail.com
on_success: change on_success: change
on_failure: change on_failure: change
GEVENT_RESOLVER=ares test__socket_dns.py
GEVENT_RESOLVER=ares test__socket_dns6.py
import sys import sys
import os
import re import re
# By default, test cases are expected to switch and emit warnings if there was none # By default, test cases are expected to switch and emit warnings if there was none
...@@ -133,6 +134,14 @@ disabled_tests = \ ...@@ -133,6 +134,14 @@ disabled_tests = \
, 'test_thread.ThreadRunningTests.test__count' , 'test_thread.ThreadRunningTests.test__count'
] ]
# if 'signalfd' in os.environ.get('GEVENT_BACKEND', ''):
# # tests that don't interact well with signalfd
# disabled_tests.extend([
# 'test_signal.SiginterruptTest.test_siginterrupt_off',
# 'test_socketserver.SocketServerTest.test_ForkingTCPServer',
# 'test_socketserver.SocketServerTest.test_ForkingUDPServer',
# 'test_socketserver.SocketServerTest.test_ForkingUnixStreamServer'])
def disable_tests_in_source(source, name): def disable_tests_in_source(source, name):
my_disabled_tests = [x for x in disabled_tests if x.startswith(name + '.')] my_disabled_tests = [x for x in disabled_tests if x.startswith(name + '.')]
......
...@@ -2,9 +2,10 @@ import sys ...@@ -2,9 +2,10 @@ import sys
import os import os
import glob import glob
import util import util
import atexit
TIMEOUT = 120 TIMEOUT = 45
directory = '%s.%s' % sys.version_info[:2] directory = '%s.%s' % sys.version_info[:2]
version = '%s.%s.%s' % sys.version_info[:3] version = '%s.%s.%s' % sys.version_info[:3]
...@@ -14,23 +15,27 @@ def TESTRUNNER(tests=None): ...@@ -14,23 +15,27 @@ def TESTRUNNER(tests=None):
if preferred_version != version: if preferred_version != version:
util.log('WARNING: The tests in %s/ are from version %s and your Python is %s', directory, preferred_version, version) util.log('WARNING: The tests in %s/ are from version %s and your Python is %s', directory, preferred_version, version)
env = os.environ.copy()
env['PYTHONPATH'] = os.getcwd() + ':' + os.environ.get('PYTHONPATH', '')
if not tests: if not tests:
tests = sorted(glob.glob('%s/test_*.py' % directory)) tests = sorted(glob.glob('%s/test_*.py' % directory))
PYTHONPATH = (os.getcwd() + ':' + os.environ.get('PYTHONPATH', '')).rstrip(':')
tests = [os.path.basename(x) for x in tests] tests = [os.path.basename(x) for x in tests]
options = {'cwd': directory, 'env': env} options = {'cwd': directory,
'timeout': TIMEOUT,
'setenv': {'PYTHONPATH': PYTHONPATH}}
if tests:
atexit.register(os.system, 'rm -f */@test*_tmp')
for filename in tests: for filename in tests:
yield directory + '/' + filename, [sys.executable, '-u', '-m', 'monkey_test', filename], options yield directory + '/' + filename, [sys.executable, '-u', '-m', 'monkey_test', filename], options.copy()
yield directory + '/' + filename + '/Event', [sys.executable, '-u', '-m', 'monkey_test', '--Event', filename], options yield directory + '/' + filename + '/Event', [sys.executable, '-u', '-m', 'monkey_test', '--Event', filename], options.copy()
def main(): def main():
import testrunner import testrunner
return testrunner.run_many(TESTRUNNER()) return testrunner.run_many(TESTRUNNER(sys.argv[1:]))
if __name__ == '__main__': if __name__ == '__main__':
......
...@@ -5,7 +5,7 @@ from gevent import monkey; monkey.patch_all() ...@@ -5,7 +5,7 @@ from gevent import monkey; monkey.patch_all()
import sys import sys
import os import os
import glob import glob
import time from time import time
from gevent.pool import Pool from gevent.pool import Pool
import util import util
...@@ -16,50 +16,57 @@ NWORKERS = int(os.environ.get('NWORKERS') or 8) ...@@ -16,50 +16,57 @@ NWORKERS = int(os.environ.get('NWORKERS') or 8)
pool = None pool = None
def info():
lastmsg = None
while True:
gevent.sleep(10, ref=False)
if pool:
msg = '# Currently running: %s: %s' % (len(pool), ', '.join(x.name for x in pool))
if msg != lastmsg:
lastmsg = msg
util.log(msg)
def spawn(*args, **kwargs): def spawn(*args, **kwargs):
g = pool.spawn(*args, **kwargs) g = pool.spawn(*args, **kwargs)
g.link_exception(lambda *args: sys.exit('Internal error')) g.link_exception(lambda *args: sys.exit('Internal error'))
return g return g
def run_many(tests): def process_test(name, cmd, options):
options = options or {}
setenv = options.get('setenv', {}).copy()
setenv.pop('PYTHONPATH', '')
environ = options.get('env')
if environ is None:
environ = os.environ.copy()
for key, value in environ.items():
if key.startswith('GEVENT_') or key.startswith('GEVENTARES_'):
if key not in setenv:
setenv[key] = value
env_str = ' '.join('%s=%s' % x for x in setenv.items())
if env_str and env_str not in name:
name = env_str + ' ' + name
return name, cmd, options
def process_tests(tests):
return [process_test(name, cmd, options) for (name, cmd, options) in tests]
def run_many(tests, expected=None):
global NWORKERS, pool global NWORKERS, pool
start = time.time() start = time()
total = 0 total = 0
failed = {} failed = {}
tests = list(tests) tests = process_tests(tests)
NWORKERS = min(len(tests), NWORKERS) NWORKERS = min(len(tests), NWORKERS)
pool = Pool(NWORKERS) pool = Pool(NWORKERS)
util.BUFFER_OUTPUT = NWORKERS > 1 util.BUFFER_OUTPUT = NWORKERS > 1
def run_one(name, cmd, **kwargs): def run_one(name, cmd, **kwargs):
result = util.run(cmd, **kwargs) result = util.run(cmd, name=name, **kwargs)
if result: if result:
# the tests containing AssertionError might have failed because # the tests containing AssertionError might have failed because
# we spawned more workers than CPUs # we spawned more workers than CPUs
# we therefore will retry them sequentially # we therefore will retry them sequentially
failed[name] = [cmd, kwargs, 'AssertionError' in (result.output or '')] failed[name] = [cmd, kwargs, 'AssertionError' in (result.output or '')]
if NWORKERS > 1:
gevent.spawn(info)
try: try:
try: try:
for name, cmd, options in tests: for name, cmd, options in tests:
total += 1 total += 1
spawn(run_one, name, cmd, **options).name = ' '.join(cmd) spawn(run_one, name, cmd, **options)
gevent.run() gevent.run()
except KeyboardInterrupt: except KeyboardInterrupt:
try: try:
...@@ -67,7 +74,7 @@ def run_many(tests): ...@@ -67,7 +74,7 @@ def run_many(tests):
util.log('Waiting for currently running to finish...') util.log('Waiting for currently running to finish...')
pool.join() pool.join()
except KeyboardInterrupt: except KeyboardInterrupt:
util.report(total, failed, exit=False, took=time.time() - start) util.report(total, failed, exit=False, took=time() - start, expected=expected)
util.log('(partial results)\n') util.log('(partial results)\n')
raise raise
except: except:
...@@ -78,25 +85,31 @@ def run_many(tests): ...@@ -78,25 +85,31 @@ def run_many(tests):
failed_then_succeeded = [] failed_then_succeeded = []
if NWORKERS > 1 and toretry: if NWORKERS > 1 and toretry:
util.log('\nWill re-try %s failed tests without concurrency:\n- %s\n', len(toretry), '\n- '.join(toretry)) util.log('\nWill retry %s failed tests without concurrency:\n- %s\n', len(toretry), '\n- '.join(toretry))
for name, (cmd, kwargs, _ignore) in failed.items(): for name, (cmd, kwargs, _ignore) in failed.items():
if not util.run(cmd, buffer_output=False, **kwargs): if not util.run(cmd, name=name, buffer_output=False, **kwargs):
failed.pop(name) failed.pop(name)
failed_then_succeeded.append(name) failed_then_succeeded.append(name)
util.report(total, failed, took=time.time() - start)
if failed_then_succeeded: if failed_then_succeeded:
util.log('\n%s tests failed during concurrent run but succeeded when ran sequentially:', len(failed_then_succeeded)) util.log('\n%s tests failed during concurrent run but succeeded when ran sequentially:', len(failed_then_succeeded))
util.log('- ' + '\n- '.join(failed_then_succeeded)) util.log('- ' + '\n- '.join(failed_then_succeeded))
util.log('gevent version %s from %s', gevent.__version__, gevent.__file__)
util.report(total, failed, took=time() - start, expected=expected)
assert not pool, pool assert not pool, pool
os.system('rm -f */@test*_tmp')
def discover(tests=None, ignore=None):
if isinstance(ignore, basestring):
ignore = load_list_from_file(ignore)
ignore = set(ignore or [])
def discover(tests):
if not tests: if not tests:
tests = set(glob.glob('test_*.py')) - set(['test_support.py']) tests = set(glob.glob('test_*.py')) - set(['test_support.py'])
if ignore:
tests -= ignore
tests = sorted(tests) tests = sorted(tests)
to_process = [] to_process = []
...@@ -106,15 +119,67 @@ def discover(tests): ...@@ -106,15 +119,67 @@ def discover(tests):
if 'TESTRUNNER' in open(filename).read(): if 'TESTRUNNER' in open(filename).read():
module = __import__(filename.rsplit('.', 1)[0]) module = __import__(filename.rsplit('.', 1)[0])
for name, cmd, options in module.TESTRUNNER(): for name, cmd, options in module.TESTRUNNER():
if remove_options(cmd)[-1] in ignore:
continue
to_process.append((filename + ' ' + name, cmd, options)) to_process.append((filename + ' ' + name, cmd, options))
else: else:
to_process.append((filename, [sys.executable, '-u', filename], default_options)) to_process.append((filename, [sys.executable, '-u', filename], default_options.copy()))
return to_process return to_process
def remove_options(lst):
return [x for x in lst if x and not x.startswith('-')]
def load_list_from_file(filename):
result = []
if filename:
for x in open(filename):
x = x.split('#', 1)[0].strip()
if x:
result.append(x)
return result
def full(args=None):
tests = []
for setenv, ignore in [('GEVENT_RESOLVER=thread', None),
('GEVENT_RESOLVER=ares GEVENTARES_SERVERS=8.8.8.8', 'tests_that_dont_use_resolver.txt')]:
setenv = dict(x.split('=') for x in setenv.split())
for name, cmd, options in discover(args, ignore=ignore):
my_setenv = options.get('setenv', {})
my_setenv.update(setenv)
options['setenv'] = my_setenv
tests.append((name, cmd, options))
if sys.version_info[:2] == (2, 7):
tests.append(('xtest_pep8.py', [sys.executable, '-u', 'xtest_pep8.py'], None))
return tests
def main(): def main():
run_many(discover(sys.argv[1:])) import optparse
parser = optparse.OptionParser()
parser.add_option('--ignore')
parser.add_option('--discover', action='store_true')
parser.add_option('--full', action='store_true')
parser.add_option('--expected')
options, args = parser.parse_args()
options.expected = load_list_from_file(options.expected)
if options.full:
assert options.ignore is None, '--ignore and --full are not compatible'
tests = full(args)
else:
tests = discover(args, options.ignore)
if options.discover:
for name, cmd, options in process_tests(tests):
print '%s: %s' % (name, ' '.join(cmd))
print '%s tests found.' % len(tests)
else:
run_many(tests, expected=options.expected)
if __name__ == '__main__': if __name__ == '__main__':
......
test__all__.py
#uses socket test__api.py
test__api_timeout.py
test__ares_host_result.py
test_ares_timeout.py # explicitly uses ares resolver
# uses socket test__backdoor.py
test_close_backend_fd.py
test__core_async.py
test__core_callback.py
test__core_loop_run.py
test__core.py
test__core_stat.py
test__core_timer.py
test__core_watcher.py
test__destroy.py
# uses socket test__doctests.py
test__environ.py
test__event.py
# uses socket test__example_echoserver.py
# uses socket test__example_portforwarder.py
# uses socket test___example_servers.py
# uses bunch of things test__examples.py
# uses socket test__example_udp_client.py
# uses socket test__example_udp_server.py
test__exc_info.py
#test__execmodules.py
test__fileobject.py
# uses socket test__greenio.py
test__GreenletExit.py
test__greenlet.py
test__greenletset.py
# uses socket test__greenness.py
test_hub_join.py
test_hub_join_timeout.py
# uses socket test__hub.py
test_issue112.py
test__joinall.py
test__local.py
test__loop_callback.py
test__memleak.py
# uses lots of things test___monkey_patching.py
test__monkey.py
test__order.py
test__os.py
test__pool.py
# uses socket test__pywsgi.py
test__queue.py
test_queue.py
# uses socket test__refcount.py
test__select.py
test__semaphore.py
# uses socket test__server.py
# test__server_pywsgi.py
test__signal.py
# uses socket test__socket_close.py
# test__socket_dns6.py
# test__socket_dns.py
# test__socket_errors.py
# test__socket.py
# test__socket_ssl.py
# test__socket_timeout.py
test__subprocess_interrupted.py
test__subprocess.py
test__systemerror.py
test_threading_2.py
test__threading_patched_local.py
test__threading_vs_settrace.py
test__threadpool.py
test__timeout.py
# monkey patched standard tests:
test_queue.py
test_select.py
test_signal.py
test_subprocess.py
test_threading_local.py
test_threading.py
test_thread.py
...@@ -3,6 +3,7 @@ import sys ...@@ -3,6 +3,7 @@ import sys
import os import os
import traceback import traceback
import unittest import unittest
from datetime import timedelta
from time import time from time import time
from gevent import subprocess, sleep, spawn_later from gevent import subprocess, sleep, spawn_later
...@@ -24,7 +25,10 @@ class Popen(subprocess.Popen): ...@@ -24,7 +25,10 @@ class Popen(subprocess.Popen):
def log(message, *args): def log(message, *args):
try: try:
string = message % args if args:
string = message % args
else:
string = message
except Exception: except Exception:
traceback.print_exc() traceback.print_exc()
try: try:
...@@ -98,16 +102,24 @@ def getname(command): ...@@ -98,16 +102,24 @@ def getname(command):
def start(command, **kwargs): def start(command, **kwargs):
# XXX should not really need 'name' there: can still figure it out
# from command + environment vars starting with GEVENT_ and GEVENTARES_
name = kwargs.pop('name', None) or getname(command)
timeout = kwargs.pop('timeout', None) timeout = kwargs.pop('timeout', None)
preexec_fn = None preexec_fn = None
if not os.environ.get('DO_NOT_SETPGRP'): if not os.environ.get('DO_NOT_SETPGRP'):
preexec_fn = getattr(os, 'setpgrp', None) preexec_fn = getattr(os, 'setpgrp', None)
env = kwargs.pop('env', None) env = kwargs.pop('env', None)
setenv = kwargs.pop('setenv', None) or {}
if preexec_fn is not None: if preexec_fn is not None:
if env is None: setenv['DO_NOT_SETPGRP'] = '1'
if setenv:
if env:
env = env.copy()
else:
env = os.environ.copy() env = os.environ.copy()
env['DO_NOT_SETPGRP'] = '1' env.update(setenv)
log('+ %s', getname(command)) log('+ %s', name)
popen = Popen(command, preexec_fn=preexec_fn, env=env, **kwargs) popen = Popen(command, preexec_fn=preexec_fn, env=env, **kwargs)
popen.setpgrp_enabled = preexec_fn is not None popen.setpgrp_enabled = preexec_fn is not None
if timeout is not None: if timeout is not None:
...@@ -132,7 +144,7 @@ class RunResult(object): ...@@ -132,7 +144,7 @@ class RunResult(object):
def run(command, **kwargs): def run(command, **kwargs):
name = getname(command) name = kwargs.get('name') or getname(command)
buffer_output = kwargs.pop('buffer_output', BUFFER_OUTPUT) buffer_output = kwargs.pop('buffer_output', BUFFER_OUTPUT)
if buffer_output: if buffer_output:
assert 'stdout' not in kwargs and 'stderr' not in kwargs, kwargs assert 'stdout' not in kwargs and 'stderr' not in kwargs, kwargs
...@@ -156,6 +168,8 @@ def run(command, **kwargs): ...@@ -156,6 +168,8 @@ def run(command, **kwargs):
out = out.strip() out = out.strip()
if out: if out:
out = ' ' + out.replace('\n', '\n ') out = ' ' + out.replace('\n', '\n ')
out = out.rstrip()
out += '\n'
log('| %s\n%s', name, out) log('| %s\n%s', name, out)
if result: if result:
log('! %s [code %s] [took %.1fs]', name, result, took) log('! %s [code %s] [took %.1fs]', name, result, took)
...@@ -166,25 +180,52 @@ def run(command, **kwargs): ...@@ -166,25 +180,52 @@ def run(command, **kwargs):
return RunResult(result, out) return RunResult(result, out)
def report(total, failed, exit=True, took=None): def expected_match(name, expected):
name_parts = set(name.split())
assert name_parts
for item in expected:
item = set(item.split())
if item.issubset(name_parts):
return True
def format_seconds(seconds):
if seconds < 20:
return '%.1fs' % seconds
seconds = str(timedelta(seconds=round(seconds)))
if seconds.startswith('0:'):
seconds = seconds[2:]
return seconds
def report(total, failed, exit=True, took=None, expected=None):
if runtimelog:
log('\nLongest-running tests:')
runtimelog.sort()
length = len('%.1f' % -runtimelog[0][0])
frmt = '%' + str(length) + '.1f seconds: %s'
for delta, name in runtimelog[:5]:
log(frmt, -delta, name)
if took: if took:
took = ' in %.1fs' % took took = ' in %s' % format_seconds(took)
else: else:
took = '' took = ''
error_count = 0
if failed: if failed:
log('\n%s/%s tests failed%s\n- %s', len(failed), total, took, '\n- '.join(failed)) log('\n%s/%s tests failed%s', len(failed), total, took)
expected = set(expected or [])
for name in failed:
if expected_match(name, expected):
log('- %s (expected)', name)
else:
log('- %s', name)
error_count += 1
else: else:
log('\n%s tests passed%s', total, took) log('\n%s tests passed%s', total, took)
if runtimelog:
log('\nLongest-running tests:')
runtimelog.sort()
length = len('%.1f' % -runtimelog[0][0])
frmt = '%' + str(length) + '.1f seconds: %s'
for took, name in runtimelog[:5]:
log(frmt, -took, name)
if exit: if exit:
if failed: if error_count:
sys.exit(1) sys.exit(min(100, error_count))
if total <= 0: if total <= 0:
sys.exit('No tests found.') sys.exit('No tests found.')
......
...@@ -18,6 +18,9 @@ from distutils.errors import CCompilerError, DistutilsExecError, DistutilsPlatfo ...@@ -18,6 +18,9 @@ from distutils.errors import CCompilerError, DistutilsExecError, DistutilsPlatfo
ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError, IOError) ext_errors = (CCompilerError, DistutilsExecError, DistutilsPlatformError, IOError)
# XXX make all env variables that setup.py parses start with GEVENTSETUP_
__version__ = re.search("__version__\s*=\s*'(.*)'", open('gevent/__init__.py').read(), re.M).group(1) __version__ = re.search("__version__\s*=\s*'(.*)'", open('gevent/__init__.py').read(), re.M).group(1)
assert __version__ assert __version__
...@@ -173,6 +176,8 @@ if LIBEV_EMBED: ...@@ -173,6 +176,8 @@ if LIBEV_EMBED:
CORE.configure = configure_libev CORE.configure = configure_libev
if sys.platform == "darwin": if sys.platform == "darwin":
os.environ["CFLAGS"] = ("%s %s" % (os.environ.get("CFLAGS", ""), "-U__llvm__")).lstrip() os.environ["CFLAGS"] = ("%s %s" % (os.environ.get("CFLAGS", ""), "-U__llvm__")).lstrip()
if os.environ.get('GEVENTSETUP_EV_VERIFY') is not None:
CORE.define_macros.append(('EV_VERIFY', os.environ['GEVENTSETUP_EV_VERIFY']))
else: else:
CORE.libraries.append('ev') CORE.libraries.append('ev')
......
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