fix issue102 by introducing a --maxfailures=NUM option

also print an informative line about "stopped/interrupted" test runs
near the end.

--HG--
branch : trunk
This commit is contained in:
holger krekel 2010-05-25 16:52:09 +02:00
parent fbcf9ec543
commit c953c7d313
8 changed files with 71 additions and 19 deletions

View File

@ -12,6 +12,12 @@ New features
declarative approach with the @py.test.mark.xfail cannot declarative approach with the @py.test.mark.xfail cannot
be used as it would mark all configurations as xfail. be used as it would mark all configurations as xfail.
- issue102: introduce new --maxfail=NUM option to stop
test runs after NUM failures. This is a generalization
of the '-x' or '--exitfirst' option which is now equivalent
to '--maxfail=1'. Both '-x' and '--maxfail' will
now also print a line near the end indicating the Interruption.
- issue89: allow py.test.mark decorators to be used on classes - issue89: allow py.test.mark decorators to be used on classes
(class decorators were introduced with python2.6) and (class decorators were introduced with python2.6) and
also allow to have multiple markers applied at class/module level also allow to have multiple markers applied at class/module level
@ -41,6 +47,7 @@ Fixes / Maintenance
- improved and unified reporting for "--tb=short" option - improved and unified reporting for "--tb=short" option
- Errors during test module imports are much shorter, (using --tb=short style) - Errors during test module imports are much shorter, (using --tb=short style)
- raises shows shorter more relevant tracebacks - raises shows shorter more relevant tracebacks
- --fulltrace now more systematically makes traces longer / inhibits cutting
- improve support for raises and other dynamically compiled code by - improve support for raises and other dynamically compiled code by
manipulating python's linecache.cache instead of the previous manipulating python's linecache.cache instead of the previous

View File

@ -62,9 +62,12 @@ def pytest_report_iteminfo(item):
def pytest_addoption(parser): def pytest_addoption(parser):
group = parser.getgroup("general", "running and selection options") group = parser.getgroup("general", "running and selection options")
group._addoption('-x', '--exitfirst', group._addoption('-x', '--exitfirst', action="store_true", default=False,
action="store_true", dest="exitfirst", default=False, dest="exitfirst",
help="exit instantly on first error or failed test."), help="exit instantly on first error or failed test."),
group._addoption('--maxfail', metavar="num",
action="store", type="int", dest="maxfail", default=0,
help="exit after first num failures or errors.")
group._addoption('-k', group._addoption('-k',
action="store", dest="keyword", default='', action="store", dest="keyword", default='',
help="only run test items matching the given " help="only run test items matching the given "
@ -89,6 +92,9 @@ def pytest_addoption(parser):
def pytest_configure(config): def pytest_configure(config):
setsession(config) setsession(config)
# compat
if config.getvalue("exitfirst"):
config.option.maxfail = 1
def setsession(config): def setsession(config):
val = config.getvalue val = config.getvalue

View File

@ -45,7 +45,7 @@ def pytest_configure(__multicall__, config):
options = [opt for opt in options if opt._long_opts] options = [opt for opt in options if opt._long_opts]
options.sort(key=lambda x: x._long_opts) options.sort(key=lambda x: x._long_opts)
for opt in options: for opt in options:
if not opt._long_opts: if not opt._long_opts or not opt.dest:
continue continue
optstrings = list(opt._long_opts) # + list(opt._short_opts) optstrings = list(opt._long_opts) # + list(opt._short_opts)
optstrings = filter(None, optstrings) optstrings = filter(None, optstrings)

View File

@ -312,12 +312,14 @@ class TerminalReporter:
self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
def _report_keyboardinterrupt(self): def _report_keyboardinterrupt(self):
self.write_sep("!", "KEYBOARD INTERRUPT")
excrepr = self._keyboardinterrupt_memo excrepr = self._keyboardinterrupt_memo
if self.config.option.verbose: msg = excrepr.reprcrash.message
excrepr.toterminal(self._tw) self.write_sep("!", msg)
else: if "KeyboardInterrupt" in msg:
excrepr.reprcrash.toterminal(self._tw) if self.config.getvalue("fulltrace"):
excrepr.toterminal(self._tw)
else:
excrepr.reprcrash.toterminal(self._tw)
def _getcrashline(self, report): def _getcrashline(self, report):
try: try:

View File

@ -20,11 +20,14 @@ Collector = py.test.collect.Collector
class Session(object): class Session(object):
nodeid = "" nodeid = ""
class Interrupted(KeyboardInterrupt):
""" signals an interrupted test run. """
def __init__(self, config): def __init__(self, config):
self.config = config self.config = config
self.pluginmanager = config.pluginmanager # shortcut self.pluginmanager = config.pluginmanager # shortcut
self.pluginmanager.register(self) self.pluginmanager.register(self)
self._testsfailed = False self._testsfailed = 0
self._nomatch = False self._nomatch = False
self.shouldstop = False self.shouldstop = False
@ -52,7 +55,7 @@ class Session(object):
yield x yield x
self.config.hook.pytest_collectreport(report=rep) self.config.hook.pytest_collectreport(report=rep)
if self.shouldstop: if self.shouldstop:
break raise self.Interrupted(self.shouldstop)
def filteritems(self, colitems): def filteritems(self, colitems):
""" return items to process (some may be deselected)""" """ return items to process (some may be deselected)"""
@ -86,9 +89,11 @@ class Session(object):
def pytest_runtest_logreport(self, report): def pytest_runtest_logreport(self, report):
if report.failed: if report.failed:
self._testsfailed = True self._testsfailed += 1
if self.config.option.exitfirst: maxfail = self.config.getvalue("maxfail")
self.shouldstop = True if maxfail and self._testsfailed >= maxfail:
self.shouldstop = "stopping after %d failures" % (
self._testsfailed)
pytest_collectreport = pytest_runtest_logreport pytest_collectreport = pytest_runtest_logreport
def sessionfinishes(self, exitstatus): def sessionfinishes(self, exitstatus):
@ -122,7 +127,8 @@ class Session(object):
def _mainloop(self, colitems): def _mainloop(self, colitems):
for item in self.collect(colitems): for item in self.collect(colitems):
if self.shouldstop:
break
if not self.config.option.collectonly: if not self.config.option.collectonly:
item.config.hook.pytest_runtest_protocol(item=item) item.config.hook.pytest_runtest_protocol(item=item)
if self.shouldstop:
raise self.Interrupted(self.shouldstop)

View File

@ -347,7 +347,7 @@ class TestCaptureFuncarg:
""") """)
result = testdir.runpytest(p) result = testdir.runpytest(p)
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
"*KEYBOARD INTERRUPT*" "*KeyboardInterrupt*"
]) ])
assert result.ret == 2 assert result.ret == 2

View File

@ -17,9 +17,10 @@ def basic_run_report(item):
return runner.call_and_report(item, "call", log=False) return runner.call_and_report(item, "call", log=False)
class Option: class Option:
def __init__(self, verbose=False, dist=None): def __init__(self, verbose=False, dist=None, fulltrace=False):
self.verbose = verbose self.verbose = verbose
self.dist = dist self.dist = dist
self.fulltrace = fulltrace
def _getcmdargs(self): def _getcmdargs(self):
l = [] l = []
if self.verbose: if self.verbose:
@ -27,6 +28,8 @@ class Option:
if self.dist: if self.dist:
l.append('--dist=%s' % self.dist) l.append('--dist=%s' % self.dist)
l.append('--tx=popen') l.append('--tx=popen')
if self.fulltrace:
l.append('--fulltrace')
return l return l
def _getcmdstring(self): def _getcmdstring(self):
return " ".join(self._getcmdargs()) return " ".join(self._getcmdargs())
@ -35,6 +38,7 @@ def pytest_generate_tests(metafunc):
if "option" in metafunc.funcargnames: if "option" in metafunc.funcargnames:
metafunc.addcall(id="default", param=Option(verbose=False)) metafunc.addcall(id="default", param=Option(verbose=False))
metafunc.addcall(id="verbose", param=Option(verbose=True)) metafunc.addcall(id="verbose", param=Option(verbose=True))
metafunc.addcall(id="fulltrace", param=Option(fulltrace=True))
if not getattr(metafunc.function, 'nodist', False): if not getattr(metafunc.function, 'nodist', False):
metafunc.addcall(id="verbose-dist", metafunc.addcall(id="verbose-dist",
param=Option(dist='each', verbose=True)) param=Option(dist='each', verbose=True))
@ -284,11 +288,28 @@ class TestTerminal:
"E assert 0", "E assert 0",
"*_keyboard_interrupt.py:6: KeyboardInterrupt*", "*_keyboard_interrupt.py:6: KeyboardInterrupt*",
]) ])
if option.verbose: if option.fulltrace:
result.stdout.fnmatch_lines([ result.stdout.fnmatch_lines([
"*raise KeyboardInterrupt # simulating the user*", "*raise KeyboardInterrupt # simulating the user*",
]) ])
result.stdout.fnmatch_lines(['*KEYBOARD INTERRUPT*']) result.stdout.fnmatch_lines(['*KeyboardInterrupt*'])
def test_maxfailures(self, testdir, option):
p = testdir.makepyfile("""
def test_1():
assert 0
def test_2():
assert 0
def test_3():
assert 0
""")
result = testdir.runpytest("--maxfail=2", *option._getcmdargs())
result.stdout.fnmatch_lines([
"*def test_1():*",
"*def test_2():*",
"*!! Interrupted: stopping after 2 failures*!!*",
"*2 failed*",
])
def test_pytest_report_header(self, testdir): def test_pytest_report_header(self, testdir):
testdir.makeconftest(""" testdir.makeconftest("""

View File

@ -86,6 +86,16 @@ class SessionTests:
assert failed == 1 assert failed == 1
assert passed == skipped == 0 assert passed == skipped == 0
def test_maxfail(self, testdir):
reprec = testdir.inline_runsource("""
def test_one(): assert 0
def test_two(): assert 0
def test_three(): assert 0
""", '--maxfail=2')
passed, skipped, failed = reprec.countoutcomes()
assert failed == 2
assert passed == skipped == 0
def test_broken_repr(self, testdir): def test_broken_repr(self, testdir):
p = testdir.makepyfile(""" p = testdir.makepyfile("""
import py import py