diff --git a/.hgignore b/.hgignore index a4f2c3fe6..402cafa0b 100644 --- a/.hgignore +++ b/.hgignore @@ -1,9 +1,15 @@ - # Automatically generated by `hgimportsvn` syntax:glob .svn .hgsvn +# Ignore local virtualenvs +syntax:glob +lib/ +bin/ +include/ +.Python/ + # These lines are suggested according to the svn:ignore property # Feel free to enable them by uncommenting them syntax:glob @@ -25,3 +31,4 @@ env/ .tox .cache .coverage +.ropeproject diff --git a/.hgtags b/.hgtags index e4ac129e7..ba85d24b0 100644 --- a/.hgtags +++ b/.hgtags @@ -54,3 +54,9 @@ acf0e1477fb19a1d35a4e40242b77fa6af32eb17 2.3.1 8738b828dec53937765db71951ef955cca4c51f6 2.3.2 7fe44182c434f8ac89149a3c340479872a5d5ccb 2.3.3 ef299e57f24218dbdd949498d7e660723636bcc3 2.3.4 +fc3a793e87ec907000a47ea0d3a372a2fe218c0a 2.3.5 +b93ac0cdae02effaa3c136a681cc45bba757fe46 1.4.14 +b93ac0cdae02effaa3c136a681cc45bba757fe46 1.4.14 +0000000000000000000000000000000000000000 1.4.14 +0000000000000000000000000000000000000000 1.4.14 +0000000000000000000000000000000000000000 1.4.14 diff --git a/AUTHORS b/AUTHORS index 9106dddef..0ac43650a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -6,9 +6,14 @@ Contributors include:: Ronny Pfannschmidt Benjamin Peterson Floris Bruynooghe +Jason R. Coombs +Wouter van Ackooy Samuele Pedroni +Brianna Laugher Carl Friedrich Bolz Armin Rigo +Maho +Jaap Broekhuizen Maciek Fijalkowski Guido Wesdorp Brian Dorsey @@ -25,3 +30,4 @@ Christian Tismer Daniel Nuri Graham Horler Andreas Zeidler +Brian Okken diff --git a/CHANGELOG b/CHANGELOG index 38031a3c3..a2a129fde 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,61 @@ -Changes between 2.3.4 and 2.3.5dev +Changes between 2.3.5 and 2.4.DEV ----------------------------------- +- fix issue323 - sorting of many module-scoped arg parametrizations + +- add support for setUpModule/tearDownModule detection, thanks Brian Okken. + +- make sessionfinish hooks execute with the same cwd-context as at + session start (helps fix plugin behaviour which write output files + with relative path such as pytest-cov) + +- fix issue316 - properly reference collection hooks in docs + +- fix issue 308 - allow to mark/xfail/skip individual parameter sets + when parametrizing. Thanks Brianna Laugher. + +- simplify parametrize() signature: allow to pass a CSV-separated string + to specify argnames. For example: ``pytest.mark.parametrize("input,expected", [(1,2), (2,3)])`` is possible now in addition to the prior + ``pytest.mark.parametrize(("input", "expected"), ...)``. + +- fix issue 306 - cleanup of -k/-m options to only match markers/test + names/keywords respectively. Thanks Wouter van Ackooy. + +- (experimental) allow fixture functions to be + implemented as context managers. Thanks Andreas Pelme, + Vladimir Keleshev. + +- (experimental) allow boolean expression directly with skipif/xfail + if a "reason" is also specified. Rework skipping documentation + to recommend "condition as booleans" because it prevents surprises + when importing markers between modules. Specifying conditions + as strings will remain fully supported. + +- improved doctest counting for doctests in python modules -- + files without any doctest items will not show up anymore + and doctest examples are counted as separate test items. + thanks Danilo Bellini. + +- fix issue245 by depending on the released py-1.4.14 + which fixes py.io.dupfile to work with files with no + mode. Thanks Jason R. Coombs. + +- fix junitxml generation when test output contains control characters, + addressing issue267, thanks Jaap Broekhuizen + +- honor --tb style for setup/teardown errors as well. Thanks Maho. + +- fix issue307 - use yaml.safe_load in example, thanks Mark Eichin. + + +Changes between 2.3.4 and 2.3.5 +----------------------------------- + +- never consider a fixture function for test function collection + +- allow re-running of test items / helps to fix pytest-reruntests plugin + and also help to keep less fixture/resource references alive + - put captured stdout/stderr into junitxml output even for passing tests (thanks Adam Goucher) diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 56e8690af..0fcc78673 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.5.dev8' +__version__ = '2.4.0.dev5' diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index e964ea9f8..482aa64f6 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -177,6 +177,10 @@ def _write_pyc(co, source_path, pyc): # This happens when we get a EEXIST in find_module creating the # __pycache__ directory and __pycache__ is by some non-dir node. return False + elif err == errno.EACCES: + # The directory is read-only; this can happen for example when + # running the tests in a package installed as root + return False raise try: fp.write(imp.get_magic()) @@ -215,11 +219,17 @@ def _rewrite_test(state, fn): if (not source.startswith(BOM_UTF8) and (not cookie_re.match(source[0:end1]) or not cookie_re.match(source[end1:end2]))): + if hasattr(state, "_indecode"): + return None # encodings imported us again, we don't rewrite + state._indecode = True try: - source.decode("ascii") - except UnicodeDecodeError: - # Let it fail in real import. - return None + try: + source.decode("ascii") + except UnicodeDecodeError: + # Let it fail in real import. + return None + finally: + del state._indecode # On Python versions which are not 2.7 and less than or equal to 3.1, the # parser expects *nix newlines. if REWRITE_NEWLINES: diff --git a/_pytest/assertion/util.py b/_pytest/assertion/util.py index 2179af541..a0cff6b18 100644 --- a/_pytest/assertion/util.py +++ b/_pytest/assertion/util.py @@ -10,6 +10,7 @@ BuiltinAssertionError = py.builtin.builtins.AssertionError # DebugInterpreter. _reprcompare = None + def format_explanation(explanation): """This formats an explanation @@ -85,7 +86,7 @@ except NameError: def assertrepr_compare(config, op, left, right): """Return specialised explanations for some operators/operands""" - width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op + width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op left_repr = py.io.saferepr(left, maxsize=int(width/2)) right_repr = py.io.saferepr(right, maxsize=width-len(left_repr)) summary = '%s %s %s' % (left_repr, op, right_repr) @@ -93,7 +94,7 @@ def assertrepr_compare(config, op, left, right): issequence = lambda x: isinstance(x, (list, tuple)) istext = lambda x: isinstance(x, basestring) isdict = lambda x: isinstance(x, dict) - isset = lambda x: isinstance(x, set) + isset = lambda x: isinstance(x, (set, frozenset)) verbose = config.getoption('verbose') explanation = None @@ -114,9 +115,9 @@ def assertrepr_compare(config, op, left, right): raise except: excinfo = py.code.ExceptionInfo() - explanation = ['(pytest_assertion plugin: representation of ' - 'details failed. Probably an object has a faulty __repr__.)', - str(excinfo)] + explanation = [ + '(pytest_assertion plugin: representation of details failed. ' + 'Probably an object has a faulty __repr__.)', str(excinfo)] if not explanation: return None @@ -132,7 +133,7 @@ def _diff_text(left, right, verbose=False): """ explanation = [] if not verbose: - i = 0 # just in case left or right has zero length + i = 0 # just in case left or right has zero length for i in range(min(len(left), len(right))): if left[i] != right[i]: break @@ -166,13 +167,15 @@ def _compare_eq_sequence(left, right, verbose=False): (i, left[i], right[i])] break if len(left) > len(right): - explanation += ['Left contains more items, ' - 'first extra item: %s' % py.io.saferepr(left[len(right)],)] + explanation += [ + 'Left contains more items, first extra item: %s' % + py.io.saferepr(left[len(right)],)] elif len(left) < len(right): - explanation += ['Right contains more items, ' - 'first extra item: %s' % py.io.saferepr(right[len(left)],)] - return explanation # + _diff_text(py.std.pprint.pformat(left), - # py.std.pprint.pformat(right)) + explanation += [ + 'Right contains more items, first extra item: %s' % + py.io.saferepr(right[len(left)],)] + return explanation # + _diff_text(py.std.pprint.pformat(left), + # py.std.pprint.pformat(right)) def _compare_eq_set(left, right, verbose=False): @@ -210,12 +213,12 @@ def _compare_eq_dict(left, right, verbose=False): if extra_left: explanation.append('Left contains more items:') explanation.extend(py.std.pprint.pformat( - dict((k, left[k]) for k in extra_left)).splitlines()) + dict((k, left[k]) for k in extra_left)).splitlines()) extra_right = set(right) - set(left) if extra_right: explanation.append('Right contains more items:') explanation.extend(py.std.pprint.pformat( - dict((k, right[k]) for k in extra_right)).splitlines()) + dict((k, right[k]) for k in extra_right)).splitlines()) return explanation diff --git a/_pytest/doctest.py b/_pytest/doctest.py index 702315883..29ddf33ad 100644 --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -34,6 +34,14 @@ class ReprFailDoctest(TerminalRepr): self.reprlocation.toterminal(tw) class DoctestItem(pytest.Item): + def __init__(self, name, parent, runner=None, dtest=None): + super(DoctestItem, self).__init__(name, parent) + self.runner = runner + self.dtest = dtest + + def runtest(self): + self.runner.run(self.dtest) + def repr_failure(self, excinfo): doctest = py.std.doctest if excinfo.errisinstance((doctest.DocTestFailure, @@ -76,7 +84,7 @@ class DoctestItem(pytest.Item): return super(DoctestItem, self).repr_failure(excinfo) def reportinfo(self): - return self.fspath, None, "[doctest]" + return self.fspath, None, "[doctest] %s" % self.name class DoctestTextfile(DoctestItem, pytest.File): def runtest(self): @@ -91,8 +99,8 @@ class DoctestTextfile(DoctestItem, pytest.File): extraglobs=dict(getfixture=fixture_request.getfuncargvalue), raise_on_error=True, verbose=0) -class DoctestModule(DoctestItem, pytest.File): - def runtest(self): +class DoctestModule(pytest.File): + def collect(self): doctest = py.std.doctest if self.fspath.basename == "conftest.py": module = self.config._conftest.importconftest(self.fspath) @@ -102,7 +110,11 @@ class DoctestModule(DoctestItem, pytest.File): self.funcargs = {} self._fixtureinfo = FuncFixtureInfo((), [], {}) fixture_request = FixtureRequest(self) - failed, tot = doctest.testmod( - module, raise_on_error=True, verbose=0, - extraglobs=dict(getfixture=fixture_request.getfuncargvalue), - optionflags=doctest.ELLIPSIS) + doctest_globals = dict(getfixture=fixture_request.getfuncargvalue) + # uses internal doctest module parsing mechanism + finder = doctest.DocTestFinder() + runner = doctest.DebugRunner(verbose=0, optionflags=doctest.ELLIPSIS) + for test in finder.find(module, module.__name__, + extraglobs=doctest_globals): + if test.examples: # skip empty doctests + yield DoctestItem(test.name, self, runner, test) diff --git a/_pytest/junitxml.py b/_pytest/junitxml.py index b4ffccfe2..cac51b55b 100644 --- a/_pytest/junitxml.py +++ b/_pytest/junitxml.py @@ -36,7 +36,8 @@ class Junit(py.xml.Namespace): # | [#x10000-#x10FFFF] _legal_chars = (0x09, 0x0A, 0x0d) _legal_ranges = ( - (0x20, 0xD7FF), + (0x20, 0x7E), + (0x80, 0xD7FF), (0xE000, 0xFFFD), (0x10000, 0x10FFFF), ) @@ -103,7 +104,7 @@ class LogXML(object): classnames.insert(0, self.prefix) self.tests.append(Junit.testcase( classname=".".join(classnames), - name=names[-1], + name=bin_xml_escape(names[-1]), time=getattr(report, 'duration', 0) )) diff --git a/_pytest/main.py b/_pytest/main.py index 95b2359bc..775294c7d 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -97,6 +97,7 @@ def wrap_session(config, doit): if session._testsfailed: session.exitstatus = EXIT_TESTSFAILED finally: + session.startdir.chdir() if initstate >= 2: config.hook.pytest_sessionfinish( session=session, @@ -216,6 +217,9 @@ class Node(object): #: keywords/markers collected from all scopes self.keywords = NodeKeywords(self) + #: allow adding of extra keywords to use for matching + self.extra_keyword_matches = set() + #self.extrainit() @property @@ -307,6 +311,14 @@ class Node(object): chain.reverse() return chain + def listextrakeywords(self): + """ Return a set of all extra keywords in self and any parents.""" + extra_keywords = set() + item = self + for item in self.listchain(): + extra_keywords.update(item.extra_keyword_matches) + return extra_keywords + def listnames(self): return [x.name for x in self.listchain()] @@ -441,6 +453,7 @@ class Session(FSCollector): self.shouldstop = False self.trace = config.trace.root.get("collection") self._norecursepatterns = config.getini("norecursedirs") + self.startdir = py.path.local() def pytest_collectstart(self): if self.shouldstop: diff --git a/_pytest/mark.py b/_pytest/mark.py index 7b6b2d00d..9961f9d0e 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -1,44 +1,56 @@ """ generic mechanism for marking and selecting python functions. """ import pytest, py + def pytest_namespace(): return {'mark': MarkGenerator()} + def pytest_addoption(parser): group = parser.getgroup("general") - group._addoption('-k', + group._addoption( + '-k', action="store", dest="keyword", default='', metavar="EXPRESSION", help="only run tests which match the given substring expression. " "An expression is a python evaluatable expression " "where all names are substring-matched against test names " - "and keywords. Example: -k 'test_method or test_other' " - "matches all test functions whose name contains " - "'test_method' or 'test_other'.") + "and their parent classes. Example: -k 'test_method or test " + "other' matches all test functions and classes whose name " + "contains 'test_method' or 'test_other'. " + "Additionally keywords are matched to classes and functions " + "containing extra names in their 'extra_keyword_matches' set, " + "as well as functions which have names assigned directly to them." + ) - group._addoption("-m", + group._addoption( + "-m", action="store", dest="markexpr", default="", metavar="MARKEXPR", help="only run tests matching given mark expression. " "example: -m 'mark1 and not mark2'." - ) + ) - group.addoption("--markers", action="store_true", help= - "show markers (builtin, plugin and per-project ones).") + group.addoption( + "--markers", action="store_true", + help="show markers (builtin, plugin and per-project ones)." + ) parser.addini("markers", "markers for test functions", 'linelist') + def pytest_cmdline_main(config): if config.option.markers: config.pluginmanager.do_configure(config) tw = py.io.TerminalWriter() for line in config.getini("markers"): name, rest = line.split(":", 1) - tw.write("@pytest.mark.%s:" % name, bold=True) + tw.write("@pytest.mark.%s:" % name, bold=True) tw.line(rest) tw.line() config.pluginmanager.do_unconfigure(config) return 0 pytest_cmdline_main.tryfirst = True + def pytest_collection_modifyitems(items, config): keywordexpr = config.option.keyword matchexpr = config.option.markexpr @@ -67,32 +79,76 @@ def pytest_collection_modifyitems(items, config): config.hook.pytest_deselected(items=deselected) items[:] = remaining -class BoolDict: - def __init__(self, mydict): - self._mydict = mydict - def __getitem__(self, name): - return name in self._mydict -class SubstringDict: - def __init__(self, mydict): - self._mydict = mydict - def __getitem__(self, name): - for key in self._mydict: - if name in key: +class MarkMapping: + """Provides a local mapping for markers. + Only the marker names from the given :class:`NodeKeywords` will be mapped, + so the names are taken only from :class:`MarkInfo` or + :class:`MarkDecorator` items. + """ + def __init__(self, keywords): + mymarks = set() + for key, value in keywords.items(): + if isinstance(value, MarkInfo) or isinstance(value, MarkDecorator): + mymarks.add(key) + self._mymarks = mymarks + + def __getitem__(self, markname): + return markname in self._mymarks + + +class KeywordMapping: + """Provides a local mapping for keywords. + Given a list of names, map any substring of one of these names to True. + """ + def __init__(self, names): + self._names = names + + def __getitem__(self, subname): + for name in self._names: + if subname in name: return True return False -def matchmark(colitem, matchexpr): - return eval(matchexpr, {}, BoolDict(colitem.keywords)) + +def matchmark(colitem, markexpr): + """Tries to match on any marker names, attached to the given colitem.""" + return eval(markexpr, {}, MarkMapping(colitem.keywords)) + def matchkeyword(colitem, keywordexpr): + """Tries to match given keyword expression to given collector item. + + Will match on the name of colitem, including the names of its parents. + Only matches names of items which are either a :class:`Class` or a + :class:`Function`. + Additionally, matches on names in the 'extra_keyword_matches' set of + any item, as well as names directly assigned to test functions. + """ keywordexpr = keywordexpr.replace("-", "not ") - return eval(keywordexpr, {}, SubstringDict(colitem.keywords)) + mapped_names = set() + + # Add the names of the current item and any parent items + for item in colitem.listchain(): + if not isinstance(item, pytest.Instance): + mapped_names.add(item.name) + + # Add the names added as extra keywords to current or parent items + for name in colitem.listextrakeywords(): + mapped_names.add(name) + + # Add the names attached to the current function through direct assignment + for name in colitem.function.__dict__: + mapped_names.add(name) + + return eval(keywordexpr, {}, KeywordMapping(mapped_names)) + def pytest_configure(config): if config.option.strict: pytest.mark._config = config + class MarkGenerator: """ Factory for :class:`MarkDecorator` objects - exposed as a ``py.test.mark`` singleton instance. Example:: @@ -126,6 +182,7 @@ class MarkGenerator: if name not in self._markers: raise AttributeError("%r not a registered marker" % (name,)) + class MarkDecorator: """ A decorator for test functions and test classes. When applied it will create :class:`MarkInfo` objects which may be @@ -149,7 +206,7 @@ class MarkDecorator: def __repr__(self): d = self.__dict__.copy() name = d.pop('markname') - return "" %(name, d) + return "" % (name, d) def __call__(self, *args, **kwargs): """ if passed a single callable argument: decorate it with mark info. @@ -162,15 +219,17 @@ class MarkDecorator: if hasattr(func, 'pytestmark'): l = func.pytestmark if not isinstance(l, list): - func.pytestmark = [l, self] + func.pytestmark = [l, self] else: - l.append(self) + l.append(self) else: - func.pytestmark = [self] + func.pytestmark = [self] else: holder = getattr(func, self.markname, None) if holder is None: - holder = MarkInfo(self.markname, self.args, self.kwargs) + holder = MarkInfo( + self.markname, self.args, self.kwargs + ) setattr(func, self.markname, holder) else: holder.add(self.args, self.kwargs) @@ -180,6 +239,7 @@ class MarkDecorator: args = self.args + args return self.__class__(self.markname, args=args, kwargs=kw) + class MarkInfo: """ Marking object created by :class:`MarkDecorator` instances. """ def __init__(self, name, args, kwargs): @@ -193,7 +253,8 @@ class MarkInfo: def __repr__(self): return "" % ( - self.name, self.args, self.kwargs) + self.name, self.args, self.kwargs + ) def add(self, args, kwargs): """ add a MarkInfo with the given args and kwargs. """ @@ -205,4 +266,3 @@ class MarkInfo: """ yield MarkInfo objects each relating to a marking-call. """ for args, kwargs in self._arglist: yield MarkInfo(self.name, args, kwargs) - diff --git a/_pytest/python.py b/_pytest/python.py index 22f61aec0..db5d2cca2 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -4,6 +4,7 @@ import inspect import sys import pytest from _pytest.main import getfslineno +from _pytest.mark import MarkDecorator, MarkInfo from _pytest.monkeypatch import monkeypatch from py._code.code import TerminalRepr @@ -177,7 +178,8 @@ def pytest_pycollect_makeitem(__multicall__, collector, name, obj): if collector.classnamefilter(name): Class = collector._getcustomclass("Class") return Class(name, parent=collector) - elif collector.funcnamefilter(name) and hasattr(obj, '__call__'): + elif collector.funcnamefilter(name) and hasattr(obj, '__call__') and \ + getfixturemarker(obj) is None: if is_generator(obj): return Generator(name, parent=collector) else: @@ -369,7 +371,9 @@ class Module(pytest.File, PyCollector): return mod def setup(self): - setup_module = xunitsetup(self.obj, "setup_module") + setup_module = xunitsetup(self.obj, "setUpModule") + if setup_module is None: + setup_module = xunitsetup(self.obj, "setup_module") if setup_module is not None: #XXX: nose compat hack, move to nose plugin # if it takes a positional arg, its probably a pytest style one @@ -380,7 +384,9 @@ class Module(pytest.File, PyCollector): setup_module() def teardown(self): - teardown_module = xunitsetup(self.obj, 'teardown_module') + teardown_module = xunitsetup(self.obj, 'tearDownModule') + if teardown_module is None: + teardown_module = xunitsetup(self.obj, 'teardown_module') if teardown_module is not None: #XXX: nose compat hack, move to nose plugin # if it takes a positional arg, its probably a py.test style one @@ -564,11 +570,13 @@ class CallSpec2(object): self._globalid_args = set() self._globalparam = _notexists self._arg2scopenum = {} # used for sorting parametrized resources + self.keywords = {} def copy(self, metafunc): cs = CallSpec2(self.metafunc) cs.funcargs.update(self.funcargs) cs.params.update(self.params) + cs.keywords.update(self.keywords) cs._arg2scopenum.update(self._arg2scopenum) cs._idlist = list(self._idlist) cs._globalid = self._globalid @@ -592,7 +600,7 @@ class CallSpec2(object): def id(self): return "-".join(map(str, filter(None, self._idlist))) - def setmulti(self, valtype, argnames, valset, id, scopenum=0): + def setmulti(self, valtype, argnames, valset, id, keywords, scopenum=0): for arg,val in zip(argnames, valset): self._checkargnotcontained(arg) getattr(self, valtype)[arg] = val @@ -604,6 +612,7 @@ class CallSpec2(object): if val is _notexists: self._emptyparamspecified = True self._idlist.append(id) + self.keywords.update(keywords) def setall(self, funcargs, id, param): for x in funcargs: @@ -644,13 +653,15 @@ class Metafunc(FuncargnamesCompatAttr): during the collection phase. If you need to setup expensive resources see about setting indirect=True to do it rather at test setup time. - :arg argnames: an argument name or a list of argument names + :arg argnames: a comma-separated string denoting one or more argument + names, or a list/tuple of argument strings. - :arg argvalues: The list of argvalues determines how often a test is invoked - with different argument values. If only one argname was specified argvalues - is a list of simple values. If N argnames were specified, argvalues must - be a list of N-tuples, where each tuple-element specifies a value for its - respective argname. + :arg argvalues: The list of argvalues determines how often a + test is invoked with different argument values. If only one + argname was specified argvalues is a list of simple values. If N + argnames were specified, argvalues must be a list of N-tuples, + where each tuple-element specifies a value for its respective + argname. :arg indirect: if True each argvalue corresponding to an argname will be passed as request.param to its respective argname fixture @@ -666,8 +677,24 @@ class Metafunc(FuncargnamesCompatAttr): It will also override any fixture-function defined scope, allowing to set a dynamic scope using test context or configuration. """ + + # individual parametrized argument sets can be wrapped in a + # marker in which case we unwrap the values and apply the mark + # at Function init + newkeywords = {} + unwrapped_argvalues = [] + for i, argval in enumerate(argvalues): + if isinstance(argval, MarkDecorator): + newmark = MarkDecorator(argval.markname, + argval.args[:-1], argval.kwargs) + newkeywords[i] = {newmark.markname: newmark} + argval = argval.args[-1] + unwrapped_argvalues.append(argval) + argvalues = unwrapped_argvalues + if not isinstance(argnames, (tuple, list)): - argnames = (argnames,) + argnames = [x.strip() for x in argnames.split(",") if x.strip()] + if len(argnames) == 1: argvalues = [(val,) for val in argvalues] if not argvalues: argvalues = [(_notexists,) * len(argnames)] @@ -690,7 +717,7 @@ class Metafunc(FuncargnamesCompatAttr): assert len(valset) == len(argnames) newcallspec = callspec.copy(self) newcallspec.setmulti(valtype, argnames, valset, ids[i], - scopenum) + newkeywords.get(i, {}), scopenum) newcalls.append(newcallspec) self._calls = newcalls @@ -907,6 +934,9 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr): for name, val in (py.builtin._getfuncdict(self.obj) or {}).items(): self.keywords[name] = val + if callspec: + for name, val in callspec.keywords.items(): + self.keywords[name] = val if keywords: for name, val in keywords.items(): self.keywords[name] = val @@ -917,20 +947,25 @@ class Function(FunctionMixin, pytest.Item, FuncargnamesCompatAttr): self.cls, funcargs=not isyield) self.fixturenames = fi.names_closure - if isyield: - assert not callspec, ( + if callspec is not None: + self.callspec = callspec + self._initrequest() + + def _initrequest(self): + if self._isyieldedfunction(): + assert not hasattr(self, "callspec"), ( "yielded functions (deprecated) cannot have funcargs") self.funcargs = {} else: - if callspec is not None: - self.callspec = callspec - self.funcargs = callspec.funcargs or {} + if hasattr(self, "callspec"): + callspec = self.callspec + self.funcargs = callspec.funcargs.copy() self._genid = callspec.id if hasattr(callspec, "param"): self.param = callspec.param else: self.funcargs = {} - self._request = req = FixtureRequest(self) + self._request = FixtureRequest(self) @property def function(self): @@ -1561,15 +1596,7 @@ class FixtureManager: continue # fixture functions have a pytest_funcarg__ prefix (pre-2.3 style) # or are "@pytest.fixture" marked - try: - marker = obj._pytestfixturefunction - except KeyboardInterrupt: - raise - except Exception: - # some objects raise errors like request (from flask import request) - # we don't expect them to be fixture functions - marker = None - + marker = getfixturemarker(obj) if marker is None: if not name.startswith(self._argprefix): continue @@ -1615,6 +1642,29 @@ class FixtureManager: except ValueError: pass +def call_fixture_func(fixturefunc, request, kwargs): + if is_generator(fixturefunc): + iter = fixturefunc(**kwargs) + next = getattr(iter, "__next__", None) + if next is None: + next = getattr(iter, "next") + res = next() + def teardown(): + try: + next() + except StopIteration: + pass + else: + fs, lineno = getfslineno(fixturefunc) + location = "%s:%s" % (fs, lineno+1) + pytest.fail( + "fixture function %s has more than one 'yield': \n%s" % + (fixturefunc.__name__, location), pytrace=False) + request.addfinalizer(teardown) + else: + res = fixturefunc(**kwargs) + return res + class FixtureDef: """ A container for a factory definition. """ def __init__(self, fixturemanager, baseid, argname, func, scope, params, @@ -1665,7 +1715,7 @@ class FixtureDef: fixturefunc = fixturefunc.__get__(request.instance) except AttributeError: pass - result = fixturefunc(**kwargs) + result = call_fixture_func(fixturefunc, request, kwargs) assert not hasattr(self, "cached_result") self.cached_result = result return result @@ -1697,29 +1747,39 @@ def getfuncargnames(function, startindex=None): def parametrize_sorted(items, ignore, cache, scopenum): if scopenum >= 3: return items - newitems = [] - olditems = [] + + # we pick the first item which has a arg/param combo in the + # requested scope and sort other items with the same combo + # into "newitems" which then is a list of all items using this + # arg/param. + + similar_items = [] + other_items = [] slicing_argparam = None + slicing_index = 0 for item in items: argparamlist = getfuncargparams(item, ignore, scopenum, cache) if slicing_argparam is None and argparamlist: slicing_argparam = argparamlist[0] - slicing_index = len(olditems) + slicing_index = len(other_items) if slicing_argparam in argparamlist: - newitems.append(item) + similar_items.append(item) else: - olditems.append(item) - if newitems: + other_items.append(item) + + if (len(similar_items) + slicing_index) > 1: newignore = ignore.copy() newignore.add(slicing_argparam) - newitems = parametrize_sorted(newitems + olditems[slicing_index:], - newignore, cache, scopenum) - old1 = parametrize_sorted(olditems[:slicing_index], newignore, - cache, scopenum+1) - return old1 + newitems + part2 = parametrize_sorted( + similar_items + other_items[slicing_index:], + newignore, cache, scopenum) + part1 = parametrize_sorted( + other_items[:slicing_index], newignore, + cache, scopenum+1) + return part1 + part2 else: - olditems = parametrize_sorted(olditems, ignore, cache, scopenum+1) - return olditems + newitems + other_items = parametrize_sorted(other_items, ignore, cache, scopenum+1) + return other_items + similar_items def getfuncargparams(item, ignore, scopenum, cache): """ return list of (arg,param) tuple, sorted by broader scope first. """ @@ -1766,6 +1826,18 @@ def getfuncargparams(item, ignore, scopenum, cache): def xunitsetup(obj, name): meth = getattr(obj, name, None) - if meth is not None: - if not hasattr(meth, "_pytestfixturefunction"): - return meth + if getfixturemarker(meth) is None: + return meth + +def getfixturemarker(obj): + """ return fixturemarker or None if it doesn't exist or raised + exceptions.""" + try: + return getattr(obj, "_pytestfixturefunction", None) + except KeyboardInterrupt: + raise + except Exception: + # some objects raise errors like request (from flask import request) + # we don't expect them to be fixture functions + return None + diff --git a/_pytest/runner.py b/_pytest/runner.py index 6014d6d9d..763f695ce 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -63,12 +63,20 @@ def pytest_runtest_protocol(item, nextitem): return True def runtestprotocol(item, log=True, nextitem=None): + hasrequest = hasattr(item, "_request") + if hasrequest and not item._request: + item._initrequest() rep = call_and_report(item, "setup", log) reports = [rep] if rep.passed: reports.append(call_and_report(item, "call", log)) reports.append(call_and_report(item, "teardown", log, nextitem=nextitem)) + # after all teardown hooks have been called + # want funcargs and request info to go away + if hasrequest: + item._request = False + item.funcargs = None return reports def pytest_runtest_setup(item): @@ -190,7 +198,8 @@ def pytest_runtest_makereport(item, call): if call.when == "call": longrepr = item.repr_failure(excinfo) else: # exception in setup or teardown - longrepr = item._repr_failure_py(excinfo) + longrepr = item._repr_failure_py(excinfo, + style=item.config.option.tbstyle) return TestReport(item.nodeid, item.location, keywords, outcome, longrepr, when, duration=duration) diff --git a/_pytest/skipping.py b/_pytest/skipping.py index a21399873..d691d9fd8 100644 --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -89,7 +89,11 @@ class MarkEvaluator: if isinstance(expr, py.builtin._basestring): result = cached_eval(self.item.config, expr, d) else: - pytest.fail("expression is not a string") + if self.get("reason") is None: + # XXX better be checked at collection time + pytest.fail("you need to specify reason=STRING " + "when using booleans as conditions.") + result = bool(expr) if result: self.result = True self.expr = expr diff --git a/_pytest/terminal.py b/_pytest/terminal.py index 6584179e6..c0ba5cdda 100644 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -454,10 +454,14 @@ class TerminalReporter: if val: parts.append("%d %s" %(len(val), key)) line = ", ".join(parts) - # XXX coloring msg = "%s in %.2f seconds" %(line, session_duration) if self.verbosity >= 0: - self.write_sep("=", msg, bold=True) + markup = dict(bold=True) + if 'failed' in self.stats: + markup['red'] = True + else: + markup['green'] = True + self.write_sep("=", msg, **markup) #else: # self.write_line(msg, bold=True) diff --git a/doc/en/_templates/layout.html b/doc/en/_templates/layout.html index 51d72c389..daa020cea 100644 --- a/doc/en/_templates/layout.html +++ b/doc/en/_templates/layout.html @@ -3,6 +3,12 @@ {% block relbaritems %} {{ super() }} + + + + {% endblock %} {% block footer %} diff --git a/doc/en/_templates/localtoc.html b/doc/en/_templates/localtoc.html index 916ea4149..25d56cad0 100644 --- a/doc/en/_templates/localtoc.html +++ b/doc/en/_templates/localtoc.html @@ -31,6 +31,8 @@ issues[bb] contact + + Talks/Posts {% extends "basic/localtoc.html" %} diff --git a/doc/en/announce/index.txt b/doc/en/announce/index.txt index 33072fc34..333542b98 100644 --- a/doc/en/announce/index.txt +++ b/doc/en/announce/index.txt @@ -5,6 +5,7 @@ Release announcements .. toctree:: :maxdepth: 2 + release-2.3.5 release-2.3.4 release-2.3.3 release-2.3.2 diff --git a/doc/en/announce/release-2.3.5.txt b/doc/en/announce/release-2.3.5.txt index 3025816db..c4e91e0e6 100644 --- a/doc/en/announce/release-2.3.5.txt +++ b/doc/en/announce/release-2.3.5.txt @@ -1,23 +1,61 @@ -pytest-2.3.5: bug fixes +pytest-2.3.5: bug fixes and little improvements =========================================================================== -pytest-2.3.5 is a bug fix release for the pytest testing tool. -See the changelog below for details. And +pytest-2.3.5 is a maintenance release with many bug fixes and little +improvements. See the changelog below for details. No backward +compatibility issues are foreseen and all plugins which worked with the +prior version are expected to work unmodified. Speaking of which, a +few interesting new plugins saw the light last month: + +- pytest-instafail: show failure information while tests are running +- pytest-qt: testing of GUI applications written with QT/Pyside +- pytest-xprocess: managing external processes across test runs +- pytest-random: randomize test ordering + +And several others like pytest-django saw maintenance releases. +For a more complete list, check out +https://pypi.python.org/pypi?%3Aaction=search&term=pytest&submit=search. + +For general information see: http://pytest.org/ -for general information. To install or upgrade pytest: +To install or upgrade pytest: pip install -U pytest # or easy_install -U pytest -best, +Particular thanks to Floris, Ronny, Benjamin and the many bug reporters +and fix providers. + +may the fixtures be with you, holger krekel Changes between 2.3.4 and 2.3.5 ----------------------------------- +- never consider a fixture function for test function collection + +- allow re-running of test items / helps to fix pytest-reruntests plugin + and also help to keep less fixture/resource references alive + +- put captured stdout/stderr into junitxml output even for passing tests + (thanks Adam Goucher) + +- Issue 265 - integrate nose setup/teardown with setupstate + so it doesnt try to teardown if it did not setup + +- issue 271 - dont write junitxml on slave nodes + +- Issue 274 - dont try to show full doctest example + when doctest does not know the example location + +- issue 280 - disable assertion rewriting on buggy CPython 2.6.0 + +- inject "getfixture()" helper to retrieve fixtures from doctests, + thanks Andreas Zeidler + - issue 259 - when assertion rewriting, be consistent with the default source encoding of ASCII on Python 2 @@ -26,7 +64,7 @@ Changes between 2.3.4 and 2.3.5 - issue250 unicode/str mixes in parametrization names and values now works - issue257, assertion-triggered compilation of source ending in a - comment line doesn't blow up in python2.5 (fixed through py>=1.4.13) + comment line doesn't blow up in python2.5 (fixed through py>=1.4.13.dev6) - fix --genscript option to generate standalone scripts that also work with python3.3 (importer ordering) diff --git a/doc/en/assert.txt b/doc/en/assert.txt index a7812c6bf..6411ed596 100644 --- a/doc/en/assert.txt +++ b/doc/en/assert.txt @@ -26,7 +26,7 @@ you will see the return value of the function call:: $ py.test test_assert1.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 1 items test_assert1.py F @@ -110,7 +110,7 @@ if you run this module:: $ py.test test_assert2.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 1 items test_assert2.py F diff --git a/doc/en/capture.txt b/doc/en/capture.txt index 20e0a9019..639412857 100644 --- a/doc/en/capture.txt +++ b/doc/en/capture.txt @@ -64,7 +64,7 @@ of the failing function and hide the other one:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 2 items test_module.py .F @@ -78,7 +78,7 @@ of the failing function and hide the other one:: test_module.py:9: AssertionError ----------------------------- Captured stdout ------------------------------ - setting up + setting up ==================== 1 failed, 1 passed in 0.01 seconds ==================== Accessing captured output from a test function diff --git a/doc/en/conf.py b/doc/en/conf.py index 0c50eebf5..f9fa0a4e7 100644 --- a/doc/en/conf.py +++ b/doc/en/conf.py @@ -17,7 +17,7 @@ # # The full version, including alpha/beta/rc tags. # The short X.Y version. -version = release = "2.3.4.1" +version = release = "2.3.5" import sys, os diff --git a/doc/en/doctest.txt b/doc/en/doctest.txt index 9f8b8a9db..dc1c125f5 100644 --- a/doc/en/doctest.txt +++ b/doc/en/doctest.txt @@ -44,7 +44,7 @@ then you can just invoke ``py.test`` without command line options:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 1 items mymodule.py . diff --git a/doc/en/example/markers.txt b/doc/en/example/markers.txt index fc3cea8f4..afc6aa3b5 100644 --- a/doc/en/example/markers.txt +++ b/doc/en/example/markers.txt @@ -28,7 +28,7 @@ You can then restrict a test run to only run tests marked with ``webtest``:: $ py.test -v -m webtest =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 3 items test_server.py:3: test_send_http PASSED @@ -40,7 +40,7 @@ Or the inverse, running all tests except the webtest ones:: $ py.test -v -m "not webtest" =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 3 items test_server.py:6: test_something_quick PASSED @@ -61,7 +61,7 @@ select tests based on their names:: $ py.test -v -k http # running with the above defined example module =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 3 items test_server.py:3: test_send_http PASSED @@ -73,7 +73,7 @@ And you can also run all tests except the ones that match the keyword:: $ py.test -k "not send_http" -v =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 3 items test_server.py:6: test_something_quick PASSED @@ -86,7 +86,7 @@ Or to select "http" and "quick" tests:: $ py.test -k "http or quick" -v =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 3 items test_server.py:3: test_send_http PASSED @@ -185,6 +185,29 @@ You can also set a module level marker:: in which case it will be applied to all functions and methods defined in the module. +.. _`marking individual tests when using parametrize`: + +Marking individual tests when using parametrize +----------------------------------------------- + +When using parametrize, applying a mark will make it apply +to each individual test. However it is also possible to +apply a marker to an individual test instance:: + + import pytest + + @pytest.mark.foo + @pytest.mark.parametrize(("n", "expected"), [ + (1, 2), + pytest.mark.bar((1, 3)), + (2, 3), + ]) + def test_increment(n, expected): + assert n + 1 == expected + +In this example the mark "foo" will apply to each of the three +tests, whereas the "bar" mark is only applied to the second test. +Skip and xfail marks can also be applied in this way, see :ref:`skip/xfail with parametrize`. .. _`adding a custom marker from a plugin`: @@ -232,7 +255,7 @@ the test needs:: $ py.test -E stage2 =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 1 items test_someenv.py s @@ -243,7 +266,7 @@ and here is one that specifies exactly the environment needed:: $ py.test -E stage1 =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 1 items test_someenv.py . @@ -360,12 +383,12 @@ then you will see two test skipped and two executed tests as expected:: $ py.test -rs # this option reports skip reasons =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 4 items test_plat.py s.s. ========================= short test summary info ========================== - SKIP [2] /tmp/doc-exec-133/conftest.py:12: cannot run on platform linux2 + SKIP [2] /tmp/doc-exec-273/conftest.py:12: cannot run on platform linux2 =================== 2 passed, 2 skipped in 0.01 seconds ==================== @@ -373,7 +396,7 @@ Note that if you specify a platform via the marker-command line option like this $ py.test -m linux2 =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 4 items test_plat.py . @@ -424,7 +447,7 @@ We can now use the ``-m option`` to select one set:: $ py.test -m interface --tb=short =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 4 items test_module.py FF @@ -445,7 +468,7 @@ or to select both "event" and "interface" tests:: $ py.test -m "interface or event" --tb=short =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 4 items test_module.py FFF diff --git a/doc/en/example/nonpython.txt b/doc/en/example/nonpython.txt index 7d56c02d4..7aefd7826 100644 --- a/doc/en/example/nonpython.txt +++ b/doc/en/example/nonpython.txt @@ -27,7 +27,7 @@ now execute the test specification:: nonpython $ py.test test_simple.yml =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 2 items test_simple.yml .F @@ -37,7 +37,7 @@ now execute the test specification:: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.09 seconds ==================== + ==================== 1 failed, 1 passed in 0.05 seconds ==================== You get one dot for the passing ``sub1: sub1`` check and one failure. Obviously in the above ``conftest.py`` you'll want to implement a more @@ -56,7 +56,7 @@ consulted when reporting in ``verbose`` mode:: nonpython $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 2 items test_simple.yml:1: usecase: ok PASSED @@ -67,17 +67,17 @@ consulted when reporting in ``verbose`` mode:: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.04 seconds ==================== + ==================== 1 failed, 1 passed in 0.05 seconds ==================== While developing your custom test collection and execution it's also interesting to just look at the collection tree:: nonpython $ py.test --collectonly =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 2 items - ============================= in 0.04 seconds ============================= + ============================= in 0.05 seconds ============================= diff --git a/doc/en/example/nonpython/conftest.py b/doc/en/example/nonpython/conftest.py index f55089e53..2406e8f10 100644 --- a/doc/en/example/nonpython/conftest.py +++ b/doc/en/example/nonpython/conftest.py @@ -9,7 +9,7 @@ def pytest_collect_file(parent, path): class YamlFile(pytest.File): def collect(self): import yaml # we need a yaml parser, e.g. PyYAML - raw = yaml.load(self.fspath.open()) + raw = yaml.safe_load(self.fspath.open()) for name, spec in raw.items(): yield YamlItem(name, self, spec) diff --git a/doc/en/example/parametrize.txt b/doc/en/example/parametrize.txt index 0ecbfc283..dd1a9c73f 100644 --- a/doc/en/example/parametrize.txt +++ b/doc/en/example/parametrize.txt @@ -104,21 +104,19 @@ this is a fully self-contained example which you can run with:: $ py.test test_scenarios.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.4.5dev3 - plugins: xdist, oejskit, pep8, cache, couchdbkit, quickcheck + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 4 items test_scenarios.py .... - ========================= 4 passed in 0.04 seconds ========================= + ========================= 4 passed in 0.01 seconds ========================= If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function:: $ py.test --collectonly test_scenarios.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.4.5dev3 - plugins: xdist, oejskit, pep8, cache, couchdbkit, quickcheck + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 4 items @@ -128,7 +126,7 @@ If you just collect tests you'll also nicely see 'advanced' and 'basic' as varia - ============================= in 0.03 seconds ============================= + ============================= in 0.01 seconds ============================= Note that we told ``metafunc.parametrize()`` that your scenario values should be considered class-scoped. With pytest-2.3 this leads to a @@ -182,14 +180,13 @@ Let's first see how it looks like at collection time:: $ py.test test_backends.py --collectonly =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.4.5dev3 - plugins: xdist, oejskit, pep8, cache, couchdbkit, quickcheck + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 2 items - ============================= in 0.03 seconds ============================= + ============================= in 0.00 seconds ============================= And then when we run the test:: @@ -198,7 +195,7 @@ And then when we run the test:: ================================= FAILURES ================================= _________________________ test_db_initialized[d2] __________________________ - db = + db = def test_db_initialized(db): # a dummy test @@ -253,7 +250,7 @@ argument sets to use for each test function. Let's run it:: ================================= FAILURES ================================= ________________________ TestClass.test_equals[1-2] ________________________ - self = , a = 1, b = 2 + self = , a = 1, b = 2 def test_equals(self, a, b): > assert a == b @@ -327,15 +324,14 @@ If you run this with reporting for skips enabled:: $ py.test -rs test_module.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.4.5dev3 - plugins: xdist, oejskit, pep8, cache, couchdbkit, quickcheck + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 2 items test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-11/conftest.py:10: could not import 'opt2' + SKIP [1] /tmp/doc-exec-275/conftest.py:10: could not import 'opt2' - =================== 1 passed, 1 skipped in 0.04 seconds ==================== + =================== 1 passed, 1 skipped in 0.01 seconds ==================== You'll see that we don't have a ``opt2`` module and thus the second test run of our ``test_func1`` was skipped. A few notes: diff --git a/doc/en/example/pythoncollection.txt b/doc/en/example/pythoncollection.txt index f51fb1aa1..82947a505 100644 --- a/doc/en/example/pythoncollection.txt +++ b/doc/en/example/pythoncollection.txt @@ -43,7 +43,7 @@ then the test collection looks like this:: $ py.test --collectonly =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 2 items @@ -82,7 +82,7 @@ You can always peek at the collection tree without running tests like this:: . $ py.test --collectonly pythoncollection.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 3 items @@ -135,7 +135,7 @@ interpreters and will leave out the setup.py file:: $ py.test --collectonly =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 1 items diff --git a/doc/en/example/reportingdemo.txt b/doc/en/example/reportingdemo.txt index df6045c4a..71e875270 100644 --- a/doc/en/example/reportingdemo.txt +++ b/doc/en/example/reportingdemo.txt @@ -13,7 +13,7 @@ get on the terminal - we are working on that): assertion $ py.test failure_demo.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 39 items failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF @@ -30,7 +30,7 @@ get on the terminal - we are working on that): failure_demo.py:15: AssertionError _________________________ TestFailing.test_simple __________________________ - self = + self = def test_simple(self): def f(): @@ -40,13 +40,13 @@ get on the terminal - we are working on that): > assert f() == g() E assert 42 == 43 - E + where 42 = () - E + and 43 = () + E + where 42 = () + E + and 43 = () failure_demo.py:28: AssertionError ____________________ TestFailing.test_simple_multiline _____________________ - self = + self = def test_simple_multiline(self): otherfunc_multi( @@ -66,19 +66,19 @@ get on the terminal - we are working on that): failure_demo.py:11: AssertionError ___________________________ TestFailing.test_not ___________________________ - self = + self = def test_not(self): def f(): return 42 > assert not f() E assert not 42 - E + where 42 = () + E + where 42 = () failure_demo.py:38: AssertionError _________________ TestSpecialisedExplanations.test_eq_text _________________ - self = + self = def test_eq_text(self): > assert 'spam' == 'eggs' @@ -89,7 +89,7 @@ get on the terminal - we are working on that): failure_demo.py:42: AssertionError _____________ TestSpecialisedExplanations.test_eq_similar_text _____________ - self = + self = def test_eq_similar_text(self): > assert 'foo 1 bar' == 'foo 2 bar' @@ -102,7 +102,7 @@ get on the terminal - we are working on that): failure_demo.py:45: AssertionError ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________ - self = + self = def test_eq_multiline_text(self): > assert 'foo\nspam\nbar' == 'foo\neggs\nbar' @@ -115,15 +115,15 @@ get on the terminal - we are working on that): failure_demo.py:48: AssertionError ______________ TestSpecialisedExplanations.test_eq_long_text _______________ - self = + self = def test_eq_long_text(self): a = '1'*100 + 'a' + '2'*100 b = '1'*100 + 'b' + '2'*100 > assert a == b E assert '111111111111...2222222222222' == '1111111111111...2222222222222' - E Skipping 90 identical leading characters in diff - E Skipping 91 identical trailing characters in diff + E Skipping 90 identical leading characters in diff, use -v to show + E Skipping 91 identical trailing characters in diff, use -v to show E - 1111111111a222222222 E ? ^ E + 1111111111b222222222 @@ -132,15 +132,15 @@ get on the terminal - we are working on that): failure_demo.py:53: AssertionError _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________ - self = + self = def test_eq_long_text_multiline(self): a = '1\n'*100 + 'a' + '2\n'*100 b = '1\n'*100 + 'b' + '2\n'*100 > assert a == b E assert '1\n1\n1\n1\n...n2\n2\n2\n2\n' == '1\n1\n1\n1\n1...n2\n2\n2\n2\n' - E Skipping 190 identical leading characters in diff - E Skipping 191 identical trailing characters in diff + E Skipping 190 identical leading characters in diff, use -v to show + E Skipping 191 identical trailing characters in diff, use -v to show E 1 E 1 E 1 @@ -156,7 +156,7 @@ get on the terminal - we are working on that): failure_demo.py:58: AssertionError _________________ TestSpecialisedExplanations.test_eq_list _________________ - self = + self = def test_eq_list(self): > assert [0, 1, 2] == [0, 1, 3] @@ -166,7 +166,7 @@ get on the terminal - we are working on that): failure_demo.py:61: AssertionError ______________ TestSpecialisedExplanations.test_eq_list_long _______________ - self = + self = def test_eq_list_long(self): a = [0]*100 + [1] + [3]*100 @@ -178,20 +178,23 @@ get on the terminal - we are working on that): failure_demo.py:66: AssertionError _________________ TestSpecialisedExplanations.test_eq_dict _________________ - self = + self = def test_eq_dict(self): - > assert {'a': 0, 'b': 1} == {'a': 0, 'b': 2} - E assert {'a': 0, 'b': 1} == {'a': 0, 'b': 2} - E - {'a': 0, 'b': 1} - E ? ^ - E + {'a': 0, 'b': 2} - E ? ^ + > assert {'a': 0, 'b': 1, 'c': 0} == {'a': 0, 'b': 2, 'd': 0} + E assert {'a': 0, 'b': 1, 'c': 0} == {'a': 0, 'b': 2, 'd': 0} + E Hiding 1 identical items, use -v to show + E Differing items: + E {'b': 1} != {'b': 2} + E Left contains more items: + E {'c': 0} + E Right contains more items: + E {'d': 0} failure_demo.py:69: AssertionError _________________ TestSpecialisedExplanations.test_eq_set __________________ - self = + self = def test_eq_set(self): > assert set([0, 10, 11, 12]) == set([0, 20, 21]) @@ -207,7 +210,7 @@ get on the terminal - we are working on that): failure_demo.py:72: AssertionError _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ - self = + self = def test_eq_longer_list(self): > assert [1,2] == [1,2,3] @@ -217,7 +220,7 @@ get on the terminal - we are working on that): failure_demo.py:75: AssertionError _________________ TestSpecialisedExplanations.test_in_list _________________ - self = + self = def test_in_list(self): > assert 1 in [0, 2, 3, 4, 5] @@ -226,7 +229,7 @@ get on the terminal - we are working on that): failure_demo.py:78: AssertionError __________ TestSpecialisedExplanations.test_not_in_text_multiline __________ - self = + self = def test_not_in_text_multiline(self): text = 'some multiline\ntext\nwhich\nincludes foo\nand a\ntail' @@ -244,7 +247,7 @@ get on the terminal - we are working on that): failure_demo.py:82: AssertionError ___________ TestSpecialisedExplanations.test_not_in_text_single ____________ - self = + self = def test_not_in_text_single(self): text = 'single foo line' @@ -257,7 +260,7 @@ get on the terminal - we are working on that): failure_demo.py:86: AssertionError _________ TestSpecialisedExplanations.test_not_in_text_single_long _________ - self = + self = def test_not_in_text_single_long(self): text = 'head ' * 50 + 'foo ' + 'tail ' * 20 @@ -270,7 +273,7 @@ get on the terminal - we are working on that): failure_demo.py:90: AssertionError ______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______ - self = + self = def test_not_in_text_single_long_term(self): text = 'head ' * 50 + 'f'*70 + 'tail ' * 20 @@ -289,7 +292,7 @@ get on the terminal - we are working on that): i = Foo() > assert i.b == 2 E assert 1 == 2 - E + where 1 = .b + E + where 1 = .b failure_demo.py:101: AssertionError _________________________ test_attribute_instance __________________________ @@ -299,8 +302,8 @@ get on the terminal - we are working on that): b = 1 > assert Foo().b == 2 E assert 1 == 2 - E + where 1 = .b - E + where = () + E + where 1 = .b + E + where = () failure_demo.py:107: AssertionError __________________________ test_attribute_failure __________________________ @@ -316,7 +319,7 @@ get on the terminal - we are working on that): failure_demo.py:116: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - self = + self = def _get_b(self): > raise Exception('Failed to get attrib') @@ -332,15 +335,15 @@ get on the terminal - we are working on that): b = 2 > assert Foo().b == Bar().b E assert 1 == 2 - E + where 1 = .b - E + where = () - E + and 2 = .b - E + where = () + E + where 1 = .b + E + where = () + E + and 2 = .b + E + where = () failure_demo.py:124: AssertionError __________________________ TestRaises.test_raises __________________________ - self = + self = def test_raises(self): s = 'qwe' @@ -352,10 +355,10 @@ get on the terminal - we are working on that): > int(s) E ValueError: invalid literal for int() with base 10: 'qwe' - <0-codegen /home/hpk/p/pytest/.tox/regen/lib/python2.7/site-packages/_pytest/python.py:851>:1: ValueError + <0-codegen /home/hpk/p/pytest/.tox/regen/local/lib/python2.7/site-packages/_pytest/python.py:858>:1: ValueError ______________________ TestRaises.test_raises_doesnt _______________________ - self = + self = def test_raises_doesnt(self): > raises(IOError, "int('3')") @@ -364,7 +367,7 @@ get on the terminal - we are working on that): failure_demo.py:136: Failed __________________________ TestRaises.test_raise ___________________________ - self = + self = def test_raise(self): > raise ValueError("demo error") @@ -373,7 +376,7 @@ get on the terminal - we are working on that): failure_demo.py:139: ValueError ________________________ TestRaises.test_tupleerror ________________________ - self = + self = def test_tupleerror(self): > a,b = [1] @@ -382,7 +385,7 @@ get on the terminal - we are working on that): failure_demo.py:142: ValueError ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______ - self = + self = def test_reinterpret_fails_with_print_for_the_fun_of_it(self): l = [1,2,3] @@ -395,7 +398,7 @@ get on the terminal - we are working on that): l is [1, 2, 3] ________________________ TestRaises.test_some_error ________________________ - self = + self = def test_some_error(self): > if namenotexi: @@ -423,7 +426,7 @@ get on the terminal - we are working on that): <2-codegen 'abc-123' /home/hpk/p/pytest/doc/en/example/assertion/failure_demo.py:162>:2: AssertionError ____________________ TestMoreErrors.test_complex_error _____________________ - self = + self = def test_complex_error(self): def f(): @@ -452,7 +455,7 @@ get on the terminal - we are working on that): failure_demo.py:5: AssertionError ___________________ TestMoreErrors.test_z1_unpack_error ____________________ - self = + self = def test_z1_unpack_error(self): l = [] @@ -462,7 +465,7 @@ get on the terminal - we are working on that): failure_demo.py:179: ValueError ____________________ TestMoreErrors.test_z2_type_error _____________________ - self = + self = def test_z2_type_error(self): l = 3 @@ -472,19 +475,19 @@ get on the terminal - we are working on that): failure_demo.py:183: TypeError ______________________ TestMoreErrors.test_startswith ______________________ - self = + self = def test_startswith(self): s = "123" g = "456" > assert s.startswith(g) - E assert ('456') - E + where = '123'.startswith + E assert ('456') + E + where = '123'.startswith failure_demo.py:188: AssertionError __________________ TestMoreErrors.test_startswith_nested ___________________ - self = + self = def test_startswith_nested(self): def f(): @@ -492,15 +495,15 @@ get on the terminal - we are working on that): def g(): return "456" > assert f().startswith(g()) - E assert ('456') - E + where = '123'.startswith - E + where '123' = () - E + and '456' = () + E assert ('456') + E + where = '123'.startswith + E + where '123' = () + E + and '456' = () failure_demo.py:195: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ - self = + self = def test_global_func(self): > assert isinstance(globf(42), float) @@ -510,18 +513,18 @@ get on the terminal - we are working on that): failure_demo.py:198: AssertionError _______________________ TestMoreErrors.test_instance _______________________ - self = + self = def test_instance(self): self.x = 6*7 > assert self.x != 42 E assert 42 != 42 - E + where 42 = .x + E + where 42 = .x failure_demo.py:202: AssertionError _______________________ TestMoreErrors.test_compare ________________________ - self = + self = def test_compare(self): > assert globf(10) < 5 @@ -531,7 +534,7 @@ get on the terminal - we are working on that): failure_demo.py:205: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ - self = + self = def test_try_finally(self): x = 1 @@ -540,4 +543,4 @@ get on the terminal - we are working on that): E assert 1 == 0 failure_demo.py:210: AssertionError - ======================== 39 failed in 0.25 seconds ========================= + ======================== 39 failed in 0.21 seconds ========================= diff --git a/doc/en/example/simple.txt b/doc/en/example/simple.txt index b5a9f3c70..084855daa 100644 --- a/doc/en/example/simple.txt +++ b/doc/en/example/simple.txt @@ -106,7 +106,7 @@ directory with the above conftest.py:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 0 items ============================= in 0.00 seconds ============================= @@ -150,12 +150,12 @@ and when running it will see a skipped "slow" test:: $ py.test -rs # "-rs" means report details on the little 's' =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 2 items test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-138/conftest.py:9: need --runslow option to run + SKIP [1] /tmp/doc-exec-278/conftest.py:9: need --runslow option to run =================== 1 passed, 1 skipped in 0.01 seconds ==================== @@ -163,7 +163,7 @@ Or run it including the ``slow`` marked test:: $ py.test --runslow =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 2 items test_module.py .. @@ -253,7 +253,7 @@ which will add the string to the test header accordingly:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 project deps: mylib-1.1 collected 0 items @@ -276,7 +276,7 @@ which will add info only when run with "--v":: $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python info1: did you know that ... did you? collecting ... collected 0 items @@ -287,7 +287,7 @@ and nothing when run plainly:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 0 items ============================= in 0.00 seconds ============================= @@ -319,7 +319,7 @@ Now we can profile which test functions execute the slowest:: $ py.test --durations=3 =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 3 items test_some_are_slow.py ... @@ -327,7 +327,7 @@ Now we can profile which test functions execute the slowest:: ========================= slowest 3 test durations ========================= 0.20s call test_some_are_slow.py::test_funcslow2 0.10s call test_some_are_slow.py::test_funcslow1 - 0.00s call test_some_are_slow.py::test_funcfast + 0.00s setup test_some_are_slow.py::test_funcfast ========================= 3 passed in 0.31 seconds ========================= incremental testing - test steps @@ -380,7 +380,7 @@ If we run this:: $ py.test -rx =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 4 items test_step.py .Fx. @@ -388,7 +388,7 @@ If we run this:: ================================= FAILURES ================================= ____________________ TestUserHandling.test_modification ____________________ - self = + self = def test_modification(self): > assert 0 @@ -398,7 +398,7 @@ If we run this:: ========================= short test summary info ========================== XFAIL test_step.py::TestUserHandling::()::test_deletion reason: previous test failed (test_modification) - ============== 1 failed, 2 passed, 1 xfailed in 0.02 seconds =============== + ============== 1 failed, 2 passed, 1 xfailed in 0.01 seconds =============== We'll see that ``test_deletion`` was not executed because ``test_modification`` failed. It is reported as an "expected failure". @@ -450,7 +450,7 @@ We can run this:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 7 items test_step.py .Fx. @@ -460,17 +460,17 @@ We can run this:: ================================== ERRORS ================================== _______________________ ERROR at setup of test_root ________________________ - file /tmp/doc-exec-138/b/test_error.py, line 1 + file /tmp/doc-exec-278/b/test_error.py, line 1 def test_root(db): # no db here, will error out fixture 'db' not found available fixtures: pytestconfig, recwarn, monkeypatch, capfd, capsys, tmpdir use 'py.test --fixtures [testpath]' for help on them. - /tmp/doc-exec-138/b/test_error.py:1 + /tmp/doc-exec-278/b/test_error.py:1 ================================= FAILURES ================================= ____________________ TestUserHandling.test_modification ____________________ - self = + self = def test_modification(self): > assert 0 @@ -479,23 +479,23 @@ We can run this:: test_step.py:9: AssertionError _________________________________ test_a1 __________________________________ - db = + db = def test_a1(db): > assert 0, db # to show value - E AssertionError: + E AssertionError: a/test_db.py:2: AssertionError _________________________________ test_a2 __________________________________ - db = + db = def test_a2(db): > assert 0, db # to show value - E AssertionError: + E AssertionError: a/test_db2.py:2: AssertionError - ========== 3 failed, 2 passed, 1 xfailed, 1 error in 0.04 seconds ========== + ========== 3 failed, 2 passed, 1 xfailed, 1 error in 0.03 seconds ========== The two test modules in the ``a`` directory see the same ``db`` fixture instance while the one test in the sister-directory ``b`` doesn't see it. We could of course @@ -550,7 +550,7 @@ and run them:: $ py.test test_module.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 2 items test_module.py FF @@ -558,7 +558,7 @@ and run them:: ================================= FAILURES ================================= ________________________________ test_fail1 ________________________________ - tmpdir = local('/tmp/pytest-543/test_fail10') + tmpdir = local('/tmp/pytest-326/test_fail10') def test_fail1(tmpdir): > assert 0 @@ -577,7 +577,7 @@ and run them:: you will have a "failures" file which contains the failing test ids:: $ cat failures - test_module.py::test_fail1 (/tmp/pytest-543/test_fail10) + test_module.py::test_fail1 (/tmp/pytest-326/test_fail10) test_module.py::test_fail2 Making test result information available in fixtures @@ -640,7 +640,7 @@ and run it:: $ py.test -s test_module.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 3 items test_module.py EFF diff --git a/doc/en/fixture.txt b/doc/en/fixture.txt index cd3b64a2b..b7b3df77e 100644 --- a/doc/en/fixture.txt +++ b/doc/en/fixture.txt @@ -7,14 +7,14 @@ pytest fixtures: explicit, modular, scalable .. currentmodule:: _pytest.python -.. versionadded:: 2.0/2.3 +.. versionadded:: 2.0/2.3/2.4 .. _`xUnit`: http://en.wikipedia.org/wiki/XUnit -.. _`general purpose of test fixtures`: http://en.wikipedia.org/wiki/Test_fixture#Software +.. _`purpose of test fixtures`: http://en.wikipedia.org/wiki/Test_fixture#Software .. _`Dependency injection`: http://en.wikipedia.org/wiki/Dependency_injection#Definition -The `general purpose of test fixtures`_ is to provide a fixed baseline -upon which tests can reliably and repeatedly execute. pytest-2.3 fixtures +The `purpose of test fixtures`_ is to provide a fixed baseline +upon which tests can reliably and repeatedly execute. pytest fixtures offer dramatic improvements over the classic xUnit style of setup/teardown functions: @@ -22,8 +22,7 @@ functions: from test functions, modules, classes or whole projects. * fixtures are implemented in a modular manner, as each fixture name - triggers a *fixture function* which can itself easily use other - fixtures. + triggers a *fixture function* which can itself use other fixtures. * fixture management scales from simple unit to complex functional testing, allowing to parametrize fixtures and tests according @@ -71,7 +70,7 @@ marked ``smtp`` fixture function. Running the test looks like this:: $ py.test test_smtpsimple.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 1 items test_smtpsimple.py F @@ -79,7 +78,7 @@ marked ``smtp`` fixture function. Running the test looks like this:: ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ - smtp = + smtp = def test_ehlo(smtp): response, msg = smtp.ehlo() @@ -89,7 +88,7 @@ marked ``smtp`` fixture function. Running the test looks like this:: E assert 0 test_smtpsimple.py:12: AssertionError - ========================= 1 failed in 0.17 seconds ========================= + ========================= 1 failed in 0.20 seconds ========================= In the failure traceback we see that the test function was called with a ``smtp`` argument, the ``smtplib.SMTP()`` instance created by the fixture @@ -129,10 +128,10 @@ Funcargs a prime example of dependency injection When injecting fixtures to test functions, pytest-2.0 introduced the term "funcargs" or "funcarg mechanism" which continues to be present -also in pytest-2.3 docs. It now refers to the specific case of injecting +also in docs today. It now refers to the specific case of injecting fixture values as arguments to test functions. With pytest-2.3 there are -more possibilities to use fixtures but "funcargs" probably will remain -as the main way of dealing with fixtures. +more possibilities to use fixtures but "funcargs" remain as the main way +as they allow to directly state the dependencies of a test function. As the following examples show in more detail, funcargs allow test functions to easily receive and work against specific pre-initialized @@ -154,10 +153,10 @@ can add a ``scope='module'`` parameter to the :py:func:`@pytest.fixture <_pytest.python.fixture>` invocation to cause the decorated ``smtp`` fixture function to only be invoked once per test module. Multiple test functions in a test module will thus -each receive the same ``smtp`` fixture instance. The next example also -extracts the fixture function into a separate ``conftest.py`` file so -that all tests in test modules in the directory can access the fixture -function:: +each receive the same ``smtp`` fixture instance. The next example puts +the fixture function into a separate ``conftest.py`` file so +that tests from multiple test modules in the directory can +access the fixture function:: # content of conftest.py import pytest @@ -189,7 +188,7 @@ inspect what is going on and can now run the tests:: $ py.test test_module.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 2 items test_module.py FF @@ -197,7 +196,7 @@ inspect what is going on and can now run the tests:: ================================= FAILURES ================================= ________________________________ test_ehlo _________________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -209,7 +208,7 @@ inspect what is going on and can now run the tests:: test_module.py:6: AssertionError ________________________________ test_noop _________________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -218,7 +217,7 @@ inspect what is going on and can now run the tests:: E assert 0 test_module.py:11: AssertionError - ========================= 2 failed in 0.23 seconds ========================= + ========================= 2 failed in 0.26 seconds ========================= You see the two ``assert 0`` failing and more importantly you can also see that the same (module-scoped) ``smtp`` object was passed into the two @@ -233,24 +232,91 @@ instance, you can simply declare it:: def smtp(...): # the returned fixture value will be shared for # all tests needing it + +.. _`contextfixtures`: +fixture finalization / teardowns +------------------------------------------------------------- + +pytest supports two styles of fixture finalization: + +- (new in pytest-2.4) by writing a contextmanager fixture + generator where a fixture value is "yielded" and the remainder + of the function serves as the teardown code. This integrates + very well with existing context managers. + +- by making a fixture function accept a ``request`` argument + with which it can call ``request.addfinalizer(teardownfunction)`` + to register a teardown callback function. + +Both methods are strictly equivalent from pytest's view and will +remain supported in the future. + +Because a number of people prefer the new contextmanager style +we describe it first:: + + # content of test_ctxfixture.py + + import smtplib + import pytest + + @pytest.fixture(scope="module") + def smtp(): + smtp = smtplib.SMTP("merlinux.eu") + yield smtp # provide the fixture value + print ("teardown smtp") + smtp.close() + +pytest detects that you are using a ``yield`` in your fixture function, +turns it into a generator and: + +a) iterates once into it for producing the value +b) iterates a second time for tearing the fixture down, expecting + a StopIteration (which is produced automatically from the Python + runtime when the generator returns). + +.. note:: + + The teardown will execute independently of the status of test functions. + You do not need to write the teardown code into a ``try-finally`` clause + like you would usually do with ``contextlib.contextmanager`` decorated + functions. + + If the fixture generator yields a second value pytest will report + an error. Yielding cannot be used for parametrization. We'll describe + ways to implement parametrization further below. + +Prior to pytest-2.4 you always needed to register a finalizer by accepting +a ``request`` object into your fixture function and calling +``request.addfinalizer`` with a teardown function:: + + import smtplib + import pytest + + @pytest.fixture(scope="module") + def smtp(request): + smtp = smtplib.SMTP("merlinux.eu") + def fin(): + print ("teardown smtp") + smtp.close() + return smtp # provide the fixture value + +This method of registering a finalizer reads more indirect +than the new contextmanager style syntax because ``fin`` +is a callback function. + + .. _`request-context`: Fixtures can interact with the requesting test context ------------------------------------------------------------- -Fixture functions can themselves use other fixtures by naming -them as an input argument just like test functions do, see -:ref:`interdependent fixtures`. Moreover, pytest -provides a builtin :py:class:`request ` object, +pytest provides a builtin :py:class:`request ` object, which fixture functions can use to introspect the function, class or module -for which they are invoked or to register finalizing (cleanup) -functions which are called when the last test finished execution. +for which they are invoked. Further extending the previous ``smtp`` fixture example, let's -read an optional server URL from the module namespace and register -a finalizer that closes the smtp connection after the last -test in a module finished execution:: +read an optional server URL from the module namespace:: # content of conftest.py import pytest @@ -260,26 +326,25 @@ test in a module finished execution:: def smtp(request): server = getattr(request.module, "smtpserver", "merlinux.eu") smtp = smtplib.SMTP(server) - def fin(): - print ("finalizing %s" % smtp) - smtp.close() - request.addfinalizer(fin) - return smtp + yield smtp # provide the fixture + print ("finalizing %s" % smtp) + smtp.close() -The registered ``fin`` function will be called when the last test -using it has executed:: +The finalizing part after the ``yield smtp`` statement will execute +when the last test using the ``smtp`` fixture has executed:: $ py.test -s -q --tb=no FF - finalizing + finalizing We see that the ``smtp`` instance is finalized after the two -tests using it tests executed. If we had specified ``scope='function'`` -then fixture setup and cleanup would occur around each single test. -Note that either case the test module itself does not need to change! +tests which use it finished executin. If we rather specify +``scope='function'`` then fixture setup and cleanup occurs +around each single test. Note that in either case the test +module itself does not need to change! Let's quickly create another test module that actually sets the -server URL and has a test to verify the fixture picks it up:: +server URL in its module namespace:: # content of test_anothersmtp.py @@ -298,6 +363,9 @@ Running it:: > assert 0, smtp.helo() E AssertionError: (250, 'mail.python.org') +voila! The ``smtp`` fixture function picked up our mail server name +from the module namespace. + .. _`fixture-parametrize`: @@ -324,11 +392,9 @@ through the special :py:class:`request ` object:: params=["merlinux.eu", "mail.python.org"]) def smtp(request): smtp = smtplib.SMTP(request.param) - def fin(): - print ("finalizing %s" % smtp) - smtp.close() - request.addfinalizer(fin) - return smtp + yield smtp + print ("finalizing %s" % smtp) + smtp.close() The main change is the declaration of ``params`` with :py:func:`@pytest.fixture <_pytest.python.fixture>`, a list of values @@ -341,7 +407,7 @@ So let's just do another run:: ================================= FAILURES ================================= __________________________ test_ehlo[merlinux.eu] __________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -353,7 +419,7 @@ So let's just do another run:: test_module.py:6: AssertionError __________________________ test_noop[merlinux.eu] __________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -364,18 +430,18 @@ So let's just do another run:: test_module.py:11: AssertionError ________________________ test_ehlo[mail.python.org] ________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() assert response[0] == 250 > assert "merlinux" in response[1] - E assert 'merlinux' in 'mail.python.org\nSIZE 10240000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' + E assert 'merlinux' in 'mail.python.org\nSIZE 25600000\nETRN\nSTARTTLS\nENHANCEDSTATUSCODES\n8BITMIME\nDSN' test_module.py:5: AssertionError ________________________ test_noop[mail.python.org] ________________________ - smtp = + smtp = def test_noop(smtp): response = smtp.noop() @@ -423,13 +489,13 @@ Here we declare an ``app`` fixture which receives the previously defined $ py.test -v test_appsetup.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 2 items test_appsetup.py:12: test_smtp_exists[merlinux.eu] PASSED test_appsetup.py:12: test_smtp_exists[mail.python.org] PASSED - ========================= 2 passed in 5.95 seconds ========================= + ========================= 2 passed in 5.38 seconds ========================= Due to the parametrization of ``smtp`` the test will run twice with two different ``App`` instances and respective smtp servers. There is no @@ -468,10 +534,8 @@ to show the setup/teardown flow:: def modarg(request): param = request.param print "create", param - def fin(): - print "fin", param - request.addfinalizer(fin) - return param + yield param + print ("fin %s" % param) @pytest.fixture(scope="function", params=[1,2]) def otherarg(request): @@ -488,7 +552,7 @@ Let's run the tests in verbose mode and with looking at the print-output:: $ py.test -v -s test_module.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 -- /home/hpk/p/pytest/.tox/regen/bin/python + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 -- /home/hpk/p/pytest/.tox/regen/bin/python collecting ... collected 8 items test_module.py:16: test_0[1] PASSED @@ -518,8 +582,8 @@ You can see that the parametrized module-scoped ``modarg`` resource caused an ordering of test execution that lead to the fewest possible "active" resources. The finalizer for the ``mod1`` parametrized resource was executed before the ``mod2`` resource was setup. -.. _`usefixtures`: +.. _`usefixtures`: using fixtures from classes, modules or projects ---------------------------------------------------------------------- diff --git a/doc/en/getting-started.txt b/doc/en/getting-started.txt index 8daeb71d1..323d9cbf0 100644 --- a/doc/en/getting-started.txt +++ b/doc/en/getting-started.txt @@ -23,7 +23,7 @@ Installation options:: To check your installation has installed the correct version:: $ py.test --version - This is py.test version 2.3.4, imported from /home/hpk/p/pytest/.tox/regen/lib/python2.7/site-packages/pytest.pyc + This is py.test version 2.3.5, imported from /home/hpk/p/pytest/.tox/regen/local/lib/python2.7/site-packages/pytest.py If you get an error checkout :ref:`installation issues`. @@ -45,7 +45,7 @@ That's it. You can execute the test function now:: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 1 items test_sample.py F @@ -122,7 +122,7 @@ run the module by passing its filename:: ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - self = + self = def test_two(self): x = "hello" @@ -157,7 +157,7 @@ before performing the test function call. Let's just run it:: ================================= FAILURES ================================= _____________________________ test_needsfiles ______________________________ - tmpdir = local('/tmp/pytest-539/test_needsfiles0') + tmpdir = local('/tmp/pytest-322/test_needsfiles0') def test_needsfiles(tmpdir): print tmpdir @@ -166,7 +166,7 @@ before performing the test function call. Let's just run it:: test_tmpdir.py:3: AssertionError ----------------------------- Captured stdout ------------------------------ - /tmp/pytest-539/test_needsfiles0 + /tmp/pytest-322/test_needsfiles0 Before the test runs, a unique-per-test-invocation temporary directory was created. More info at :ref:`tmpdir handling`. diff --git a/doc/en/img/cramer2.png b/doc/en/img/cramer2.png new file mode 100644 index 000000000..6bf0e92e2 Binary files /dev/null and b/doc/en/img/cramer2.png differ diff --git a/doc/en/img/gaynor3.png b/doc/en/img/gaynor3.png new file mode 100644 index 000000000..a577c168b Binary files /dev/null and b/doc/en/img/gaynor3.png differ diff --git a/doc/en/img/keleshev.png b/doc/en/img/keleshev.png new file mode 100644 index 000000000..0d5e571e2 Binary files /dev/null and b/doc/en/img/keleshev.png differ diff --git a/doc/en/img/theuni.png b/doc/en/img/theuni.png new file mode 100644 index 000000000..abeb737e7 Binary files /dev/null and b/doc/en/img/theuni.png differ diff --git a/doc/en/index.txt b/doc/en/index.txt index bb1172cd2..0ee4fed2f 100644 --- a/doc/en/index.txt +++ b/doc/en/index.txt @@ -4,15 +4,15 @@ pytest: helps you write better programs ============================================= -.. note:: Upcoming: `professional testing with pytest and tox `_ , 24th-26th June 2013, Leipzig. - - **a mature full-featured Python testing tool** - runs on Posix/Windows, Python 2.4-3.3, PyPy and Jython-2.5.1 - :ref:`comprehensive online ` and `PDF documentation `_ + - many :ref:`third party plugins ` and + :ref:`builtin helpers ` - used in :ref:`many projects and organisations `, in test - suites ranging from 10 to 10s of thousands of tests + suites with up to twenty thousand tests + - strict policy of remaining backward compatible across releases - comes with many :ref:`tested examples ` **provides easy no-boilerplate testing** @@ -26,13 +26,13 @@ pytest: helps you write better programs **scales from simple unit to complex functional testing** - - (new in 2.3) :ref:`modular parametrizeable fixtures ` + - :ref:`modular parametrizeable fixtures ` (new in 2.3, + improved in 2.4) - :ref:`parametrized test functions ` - :ref:`mark` - - :ref:`skipping` + - :ref:`skipping` (improved in 2.4) - can :ref:`distribute tests to multiple CPUs ` through :ref:`xdist plugin ` - can :ref:`continuously re-run failing tests ` - - many :ref:`builtin helpers ` and :ref:`plugins ` - flexible :ref:`Python test discovery` **integrates many common testing methods**: @@ -50,8 +50,9 @@ pytest: helps you write better programs **extensive plugin and customization system**: - all collection, reporting, running aspects are delegated to hook functions - - customizations can be per-directory, per-project or per PyPI released plugins - - it is easy to add command line options or do other kind of add-ons and customizations. + - customizations can be per-directory, per-project or per PyPI released plugin + - it is easy to add command line options or customize existing behaviour + .. _`Javascript unit- and functional testing`: http://pypi.python.org/pypi/oejskit diff --git a/doc/en/parametrize.txt b/doc/en/parametrize.txt index e62517a7d..5c326d0b2 100644 --- a/doc/en/parametrize.txt +++ b/doc/en/parametrize.txt @@ -30,7 +30,7 @@ pytest supports test parametrization in several well-integrated ways: .. regendoc: wipe -.. versionadded:: 2.2 +.. versionadded:: 2.2, improved in 2.4 The builtin ``pytest.mark.parametrize`` decorator enables parametrization of arguments for a test function. Here is a typical example @@ -39,7 +39,7 @@ to an expected output:: # content of test_expectation.py import pytest - @pytest.mark.parametrize(("input", "expected"), [ + @pytest.mark.parametrize("input,expected", [ ("3+5", 8), ("2+4", 6), ("6*9", 42), @@ -47,23 +47,24 @@ to an expected output:: def test_eval(input, expected): assert eval(input) == expected -Here, the ``@parametrize`` decorator defines three different argument -sets for the two ``(input, output)`` arguments of the ``test_eval`` function -which will thus run three times:: +Here, the ``@parametrize`` decorator defines three different ``(input,output)`` +tuples so that that the ``test_eval`` function will run three times using +them in turn:: $ py.test - =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + ============================= test session starts ============================== + platform linux2 -- Python 2.7.3 -- pytest-2.4.0.dev3 + plugins: xdist, cache, cli, pep8, xprocess, cov, capturelog, bdd-splinter, rerunfailures, instafail, localserver collected 3 items test_expectation.py ..F - ================================= FAILURES ================================= - ____________________________ test_eval[6*9-42] _____________________________ + =================================== FAILURES =================================== + ______________________________ test_eval[6*9-42] _______________________________ input = '6*9', expected = 42 - @pytest.mark.parametrize(("input", "expected"), [ + @pytest.mark.parametrize("input,expected", [ ("3+5", 8), ("2+4", 6), ("6*9", 42), @@ -74,14 +75,49 @@ which will thus run three times:: E + where 54 = eval('6*9') test_expectation.py:8: AssertionError - ==================== 1 failed, 2 passed in 0.01 seconds ==================== + ====================== 1 failed, 2 passed in 0.02 seconds ====================== -As expected only one pair of input/output values fails the simple test function. -And as usual with test function arguments, you can see the ``input`` and ``output`` values in the traceback. +As designed in this example, only one pair of input/output values fails +the simple test function. And as usual with test function arguments, +you can see the ``input`` and ``output`` values in the traceback. -Note that there ways how you can mark a class or a module, -see :ref:`mark`. +Note that you could also use the parametrize marker on a class or a module +(see :ref:`mark`) which would invoke several functions with the argument sets. +It is also possible to mark individual test instances within parametrize, +for example with the builtin ``mark.xfail``:: + + # content of test_expectation.py + import pytest + @pytest.mark.parametrize("input,expected", [ + ("3+5", 8), + ("2+4", 6), + pytest.mark.xfail(("6*9", 42)), + ]) + def test_eval(input, expected): + assert eval(input) == expected + +Let's run this:: + + $ py.test + ============================= test session starts ============================== + platform linux2 -- Python 2.7.3 -- pytest-2.4.0.dev3 + plugins: xdist, cache, cli, pep8, xprocess, cov, capturelog, bdd-splinter, rerunfailures, instafail, localserver + collected 3 items + + test_expectation.py ..x + + ===================== 2 passed, 1 xfailed in 0.02 seconds ====================== + +The one parameter set which caused a failure previously now +shows up as an "xfailed (expected to fail)" test. + +.. note:: + + In versions prior to 2.4 one needed to specify the argument + names as a tuple. This remains valid but the simpler ``"name1,name2,..."`` + comma-separated-string syntax is now advertised fist because + it's easier to write, produces less line noise. .. _`pytest_generate_tests`: @@ -128,15 +164,15 @@ Let's also run with a stringinput that will lead to a failing test:: $ py.test -q --stringinput="!" test_strings.py F - ================================= FAILURES ================================= - ___________________________ test_valid_string[!] ___________________________ + =================================== FAILURES =================================== + _____________________________ test_valid_string[!] _____________________________ stringinput = '!' def test_valid_string(stringinput): > assert stringinput.isalpha() - E assert () - E + where = '!'.isalpha + E assert () + E + where = '!'.isalpha test_strings.py:3: AssertionError @@ -148,8 +184,8 @@ listlist:: $ py.test -q -rs test_strings.py s - ========================= short test summary info ========================== - SKIP [1] /home/hpk/p/pytest/.tox/regen/lib/python2.7/site-packages/_pytest/python.py:962: got empty parameter set, function test_valid_string at /tmp/doc-exec-101/test_strings.py:1 + =========================== short test summary info ============================ + SKIP [1] /home/hpk/p/pytest/_pytest/python.py:999: got empty parameter set, function test_valid_string at /tmp/doc-exec-2/test_strings.py:1 For further examples, you might want to look at :ref:`more parametrization examples `. diff --git a/doc/en/plugins.txt b/doc/en/plugins.txt index ee941619b..4c427a21e 100644 --- a/doc/en/plugins.txt +++ b/doc/en/plugins.txt @@ -78,12 +78,22 @@ there is no need to activate it. Here is a initial list of known plugins: * `pytest-capturelog `_: to capture and assert about messages from the logging module +* `pytest-cov `_: + coverage reporting, compatible with distributed testing + * `pytest-xdist `_: to distribute tests to CPUs and remote hosts, to run in boxed mode which allows to survive segmentation faults, to run in looponfailing mode, automatically re-running failing tests on file changes, see also :ref:`xdist` +* `pytest-instafail `_: + to report failures while the test run is happening. + +* `pytest-bdd `_ and + `pytest-konira `_ + to write tests using behaviour-driven testing. + * `pytest-timeout `_: to timeout tests based on function marks or global definitions. @@ -91,9 +101,6 @@ there is no need to activate it. Here is a initial list of known plugins: to interactively re-run failing tests and help other plugins to store test run information across invocations. -* `pytest-cov `_: - coverage reporting, compatible with distributed testing - * `pytest-pep8 `_: a ``--pep8`` option to enable PEP8 compliance checking. @@ -338,15 +345,15 @@ Reporting hooks Session related reporting hooks: -.. autofunction: pytest_collectstart -.. autofunction: pytest_itemcollected -.. autofunction: pytest_collectreport -.. autofunction: pytest_deselected +.. autofunction:: pytest_collectstart +.. autofunction:: pytest_itemcollected +.. autofunction:: pytest_collectreport +.. autofunction:: pytest_deselected And here is the central hook for reporting about test execution: -.. autofunction: pytest_runtest_logreport +.. autofunction:: pytest_runtest_logreport Reference of objects involved in hooks =========================================================== diff --git a/doc/en/projects.txt b/doc/en/projects.txt index a076ba23e..544cde06b 100644 --- a/doc/en/projects.txt +++ b/doc/en/projects.txt @@ -1,5 +1,22 @@ .. _projects: +.. image:: img/gaynor3.png + :width: 400px + :align: right + +.. image:: img/theuni.png + :width: 400px + :align: right + +.. image:: img/cramer2.png + :width: 400px + :align: right + +.. image:: img/keleshev.png + :width: 400px + :align: right + + Project examples ========================== diff --git a/doc/en/recwarn.txt b/doc/en/recwarn.txt index 743e41018..faa1ad761 100644 --- a/doc/en/recwarn.txt +++ b/doc/en/recwarn.txt @@ -2,6 +2,8 @@ Asserting deprecation and other warnings ===================================================== +.. _function_argument: + The recwarn function argument ------------------------------------ @@ -24,6 +26,9 @@ The ``recwarn`` function argument provides these methods: * ``pop(category=None)``: return last warning matching the category. * ``clear()``: clear list of warnings + +.. _ensuring_function_triggers: + Ensuring a function triggers a deprecation warning ------------------------------------------------------- diff --git a/doc/en/skipping.txt b/doc/en/skipping.txt index 305c2d53f..1c45d4219 100644 --- a/doc/en/skipping.txt +++ b/doc/en/skipping.txt @@ -9,86 +9,110 @@ If you have test functions that cannot be run on certain platforms or that you expect to fail you can mark them accordingly or you may call helper functions during execution of setup or test functions. -A *skip* means that you expect your test to pass unless a certain -configuration or condition (e.g. wrong Python interpreter, missing -dependency) prevents it to run. And *xfail* means that your test -can run but you expect it to fail because there is an implementation problem. +A *skip* means that you expect your test to pass unless the environment +(e.g. wrong Python interpreter, missing dependency) prevents it to run. +And *xfail* means that your test can run but you expect it to fail +because there is an implementation problem. -py.test counts and lists *skip* and *xfail* tests separately. However, -detailed information about skipped/xfailed tests is not shown by default -to avoid cluttering the output. You can use the ``-r`` option to see -details corresponding to the "short" letters shown in the test -progress:: +py.test counts and lists *skip* and *xfail* tests separately. Detailed +information about skipped/xfailed tests is not shown by default to avoid +cluttering the output. You can use the ``-r`` option to see details +corresponding to the "short" letters shown in the test progress:: py.test -rxs # show extra info on skips and xfails (See :ref:`how to change command line options defaults`) .. _skipif: +.. _`condition booleans`: Marking a test function to be skipped ------------------------------------------- +.. versionadded:: 2.4 + Here is an example of marking a test function to be skipped -when run on a Python3 interpreter:: +when run on a Python3.3 interpreter:: import sys - @pytest.mark.skipif("sys.version_info >= (3,0)") + @pytest.mark.skipif(sys.version_info >= (3,3), + reason="requires python3.3") def test_function(): ... -During test function setup the skipif condition is -evaluated by calling ``eval('sys.version_info >= (3,0)', namespace)``. -(*New in version 2.0.2*) The namespace contains all the module globals of the test function so that -you can for example check for versions of a module you are using:: +During test function setup the condition ("sys.version_info >= (3,3)") is +checked. If it evaluates to True, the test function will be skipped +with the specified reason. Note that pytest enforces specifying a reason +in order to report meaningful "skip reasons" (e.g. when using ``-rs``). + +You can share skipif markers between modules. Consider this test module:: + + # content of test_mymodule.py import mymodule - - @pytest.mark.skipif("mymodule.__version__ < '1.2'") - def test_function(): - ... - -The test function will not be run ("skipped") if -``mymodule`` is below the specified version. The reason -for specifying the condition as a string is mainly that -py.test can report a summary of skip conditions. -For information on the construction of the ``namespace`` -see `evaluation of skipif/xfail conditions`_. - -You can of course create a shortcut for your conditional skip -decorator at module level like this:: - - win32only = pytest.mark.skipif("sys.platform != 'win32'") - - @win32only + minversion = pytest.mark.skipif(mymodule.__versioninfo__ >= (1,1), + reason="at least mymodule-1.1 required") + @minversion def test_function(): ... -Skip all test functions of a class --------------------------------------- +You can import it from another test module:: + + # test_myothermodule.py + from test_mymodule import minversion + + @minversion + def test_anotherfunction(): + ... + +For larger test suites it's usually a good idea to have one file +where you define the markers which you then consistently apply +throughout your test suite. + +Alternatively, the pre pytest-2.4 way to specify `condition strings `_ instead of booleans will remain fully supported in future +versions of pytest. It couldn't be easily used for importing markers +between test modules so it's no longer advertised as the primary method. + + +Skip all test functions of a class or module +--------------------------------------------- As with all function :ref:`marking ` you can skip test functions at the -`whole class- or module level`_. Here is an example -for skipping all methods of a test class based on the platform:: +`whole class- or module level`_. If your code targets python2.6 or above you +use the skipif decorator (and any other marker) on classes:: - class TestPosixCalls: - pytestmark = pytest.mark.skipif("sys.platform == 'win32'") - - def test_function(self): - "will not be setup or run under 'win32' platform" - -The ``pytestmark`` special name tells py.test to apply it to each test -function in the class. If your code targets python2.6 or above you can -more naturally use the skipif decorator (and any other marker) on -classes:: - - @pytest.mark.skipif("sys.platform == 'win32'") + @pytest.mark.skipif(sys.platform == 'win32', + reason="requires windows") class TestPosixCalls: def test_function(self): "will not be setup or run under 'win32' platform" -Using multiple "skipif" decorators on a single function is generally fine - it means that if any of the conditions apply the function execution will be skipped. +If the condition is true, this marker will produce a skip result for +each of the test methods. + +If your code targets python2.5 where class-decorators are not available, +you can set the ``pytestmark`` attribute of a class:: + + class TestPosixCalls: + pytestmark = pytest.mark.skipif(sys.platform == 'win32', + reason="requires Windows") + + def test_function(self): + "will not be setup or run under 'win32' platform" + +As with the class-decorator, the ``pytestmark`` special name tells +py.test to apply it to each test function in the class. + +If you want to skip all test functions of a module, you must use +the ``pytestmark`` name on the global level:: + + # test_module.py + + pytestmark = pytest.mark.skipif(...) + +If multiple "skipif" decorators are applied to a test function, it +will be skipped if any of the skip conditions is true. .. _`whole class- or module level`: mark.html#scoped-marking @@ -118,7 +142,8 @@ as if it weren't marked at all. As with skipif_ you can also mark your expectation of a failure on a particular platform:: - @pytest.mark.xfail("sys.version_info >= (3,0)") + @pytest.mark.xfail(sys.version_info >= (3,3), + reason="python3.3 api changes") def test_function(): ... @@ -132,7 +157,7 @@ Running it with the report-on-xfail option gives this output:: example $ py.test -rx xfail_demo.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 6 items xfail_demo.py xxxxxx @@ -151,41 +176,41 @@ Running it with the report-on-xfail option gives this output:: ======================== 6 xfailed in 0.05 seconds ========================= -.. _`evaluation of skipif/xfail conditions`: +.. _`skip/xfail with parametrize`: -Evaluation of skipif/xfail expressions ----------------------------------------------------- +Skip/xfail with parametrize +--------------------------- -.. versionadded:: 2.0.2 +It is possible to apply markers like skip and xfail to individual +test instances when using parametrize: -The evaluation of a condition string in ``pytest.mark.skipif(conditionstring)`` -or ``pytest.mark.xfail(conditionstring)`` takes place in a namespace -dictionary which is constructed as follows: + import pytest -* the namespace is initialized by putting the ``sys`` and ``os`` modules - and the pytest ``config`` object into it. - -* updated with the module globals of the test function for which the - expression is applied. - -The pytest ``config`` object allows you to skip based on a test configuration value -which you might have added:: - - @pytest.mark.skipif("not config.getvalue('db')") - def test_function(...): - ... + @pytest.mark.parametrize(("n", "expected"), [ + (1, 2), + pytest.mark.xfail((1, 0)), + pytest.mark.xfail(reason="some bug")((1, 3)), + (2, 3), + (3, 4), + (4, 5), + pytest.mark.skipif("sys.version_info >= (3,0)")((10, 11)), + ]) + def test_increment(n, expected): + assert n + 1 == expected Imperative xfail from within a test or setup function ------------------------------------------------------ -If you cannot declare xfail-conditions at import time -you can also imperatively produce an XFail-outcome from -within test or setup code. Example:: +If you cannot declare xfail- of skipif conditions at import +time you can also imperatively produce an according outcome +imperatively, in test or setup code:: def test_function(): if not valid_config(): - pytest.xfail("unsupported configuration") + pytest.xfail("failing configuration (but should work)") + # or + pytest.skipif("unsupported configuration") Skipping on a missing import dependency @@ -202,16 +227,61 @@ version number of a library:: docutils = pytest.importorskip("docutils", minversion="0.3") -The version will be read from the specified module's ``__version__`` attribute. +The version will be read from the specified +module's ``__version__`` attribute. -Imperative skip from within a test or setup function ------------------------------------------------------- -If for some reason you cannot declare skip-conditions -you can also imperatively produce a skip-outcome from -within test or setup code. Example:: +.. _`string conditions`: +specifying conditions as strings versus booleans +---------------------------------------------------------- + +Prior to pytest-2.4 the only way to specify skipif/xfail conditions was +to use strings:: + + import sys + @pytest.mark.skipif("sys.version_info >= (3,3)") def test_function(): - if not valid_config(): - pytest.skip("unsupported configuration") + ... + +During test function setup the skipif condition is evaluated by calling +``eval('sys.version_info >= (3,0)', namespace)``. The namespace contains +all the module globals, and ``os`` and ``sys`` as a minimum. + +Since pytest-2.4 `condition booleans`_ are considered preferable +because markers can then be freely imported between test modules. +With strings you need to import not only the marker but all variables +everything used by the marker, which violates encapsulation. + +The reason for specifying the condition as a string was that py.test can +report a summary of skip conditions based purely on the condition string. +With conditions as booleans you are required to specify a ``reason`` string. + +Note that string conditions will remain fully supported and you are free +to use them if you have no need for cross-importing markers. + +The evaluation of a condition string in ``pytest.mark.skipif(conditionstring)`` +or ``pytest.mark.xfail(conditionstring)`` takes place in a namespace +dictionary which is constructed as follows: + +* the namespace is initialized by putting the ``sys`` and ``os`` modules + and the pytest ``config`` object into it. + +* updated with the module globals of the test function for which the + expression is applied. + +The pytest ``config`` object allows you to skip based on a test +configuration value which you might have added:: + + @pytest.mark.skipif("not config.getvalue('db')") + def test_function(...): + ... + +The equivalent with "boolean conditions" is:: + + @pytest.mark.skipif(not pytest.config.getvalue("db"), + reason="--db was not specified") + def test_function(...): + pass + diff --git a/doc/en/talks.txt b/doc/en/talks.txt index ececdd49d..4d562c30f 100644 --- a/doc/en/talks.txt +++ b/doc/en/talks.txt @@ -4,8 +4,6 @@ Talks and Tutorials .. _`funcargs`: funcargs.html -.. note:: Upcoming: `professional testing with pytest and tox <`http://www.python-academy.com/courses/specialtopics/python_course_testing.html>`_ , 24th-26th June 2013, Leipzig. - Tutorial examples and blog postings --------------------------------------------- diff --git a/doc/en/tmpdir.txt b/doc/en/tmpdir.txt index 55b7c629b..519a1a348 100644 --- a/doc/en/tmpdir.txt +++ b/doc/en/tmpdir.txt @@ -29,7 +29,7 @@ Running this would result in a passed test except for the last $ py.test test_tmpdir.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 1 items test_tmpdir.py F @@ -37,7 +37,7 @@ Running this would result in a passed test except for the last ================================= FAILURES ================================= _____________________________ test_create_file _____________________________ - tmpdir = local('/tmp/pytest-540/test_create_file0') + tmpdir = local('/tmp/pytest-323/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") diff --git a/doc/en/unittest.txt b/doc/en/unittest.txt index a6a10929c..9184584fd 100644 --- a/doc/en/unittest.txt +++ b/doc/en/unittest.txt @@ -88,7 +88,7 @@ the ``self.db`` values in the traceback:: $ py.test test_unittest_db.py =========================== test session starts ============================ - platform linux2 -- Python 2.7.3 -- pytest-2.3.4 + platform linux2 -- Python 2.7.3 -- pytest-2.3.5 collected 2 items test_unittest_db.py FF @@ -101,7 +101,7 @@ the ``self.db`` values in the traceback:: def test_method1(self): assert hasattr(self, "db") > assert 0, self.db # fail for demo purposes - E AssertionError: + E AssertionError: test_unittest_db.py:9: AssertionError ___________________________ MyTest.test_method2 ____________________________ @@ -110,7 +110,7 @@ the ``self.db`` values in the traceback:: def test_method2(self): > assert 0, self.db # fail for demo purposes - E AssertionError: + E AssertionError: test_unittest_db.py:12: AssertionError ========================= 2 failed in 0.02 seconds ========================= diff --git a/setup.py b/setup.py index 946495ec6..c5e7890f7 100644 --- a/setup.py +++ b/setup.py @@ -12,7 +12,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.3.5.dev8', + version='2.4.0.dev5', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], @@ -21,7 +21,7 @@ def main(): entry_points= make_entry_points(), cmdclass = {'test': PyTest}, # the following should be enabled for release - install_requires=['py>=1.4.13dev6'], + install_requires=['py>=1.4.14'], classifiers=['Development Status :: 6 - Mature', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 891871c0f..87cd4f928 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1712,6 +1712,20 @@ class TestFixtureMarker: reprec = testdir.inline_run("-v") reprec.assertoutcome(passed=6) + def test_fixture_marked_function_not_collected_as_test(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.fixture + def test_app(): + return 1 + + def test_something(test_app): + assert test_app == 1 + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=1) + + class TestRequestScopeAccess: pytestmark = pytest.mark.parametrize(("scope", "ok", "error"),[ ["session", "", "fspath class function module"], @@ -1852,3 +1866,101 @@ class TestShowFixtures: *hello world* """) + + +class TestContextManagerFixtureFuncs: + def test_simple(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.fixture + def arg1(): + print ("setup") + yield 1 + print ("teardown") + def test_1(arg1): + print ("test1 %s" % arg1) + def test_2(arg1): + print ("test2 %s" % arg1) + assert 0 + """) + result = testdir.runpytest("-s") + result.stdout.fnmatch_lines(""" + setup + test1 1 + teardown + setup + test2 1 + teardown + """) + + def test_scoped(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.fixture(scope="module") + def arg1(): + print ("setup") + yield 1 + print ("teardown") + def test_1(arg1): + print ("test1 %s" % arg1) + def test_2(arg1): + print ("test2 %s" % arg1) + """) + result = testdir.runpytest("-s") + result.stdout.fnmatch_lines(""" + setup + test1 1 + test2 1 + teardown + """) + + def test_setup_exception(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.fixture(scope="module") + def arg1(): + pytest.fail("setup") + yield 1 + def test_1(arg1): + pass + """) + result = testdir.runpytest("-s") + result.stdout.fnmatch_lines(""" + *pytest.fail*setup* + *1 error* + """) + + def test_teardown_exception(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.fixture(scope="module") + def arg1(): + yield 1 + pytest.fail("teardown") + def test_1(arg1): + pass + """) + result = testdir.runpytest("-s") + result.stdout.fnmatch_lines(""" + *pytest.fail*teardown* + *1 passed*1 error* + """) + + + def test_yields_more_than_one(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.fixture(scope="module") + def arg1(): + yield 1 + yield 2 + def test_1(arg1): + pass + """) + result = testdir.runpytest("-s") + result.stdout.fnmatch_lines(""" + *fixture function* + *test_yields*:2* + """) + + diff --git a/testing/python/integration.py b/testing/python/integration.py index a191254c6..c716c7ceb 100644 --- a/testing/python/integration.py +++ b/testing/python/integration.py @@ -117,3 +117,35 @@ class TestMockDecoration: """) reprec = testdir.inline_run() reprec.assertoutcome(passed=2) + + +class TestReRunTests: + def test_rerun(self, testdir): + testdir.makeconftest(""" + from _pytest.runner import runtestprotocol + def pytest_runtest_protocol(item, nextitem): + runtestprotocol(item, log=False, nextitem=nextitem) + runtestprotocol(item, log=True, nextitem=nextitem) + """) + testdir.makepyfile(""" + import pytest + count = 0 + req = None + @pytest.fixture + def fix(request): + global count, req + assert request != req + req = request + print ("fix count %s" % count) + count += 1 + def test_fix(fix): + pass + """) + result = testdir.runpytest("-s") + result.stdout.fnmatch_lines(""" + *fix count 0* + *fix count 1* + """) + result.stdout.fnmatch_lines(""" + *2 passed* + """) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 60247212f..75e3aa461 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -221,6 +221,16 @@ class TestMetafunc: "*6 fail*", ]) + def test_parametrize_CSV(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.parametrize("x, y,", [(1,2), (2,3)]) + def test_func(x, y): + assert x+1 == y + """) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2) + def test_parametrize_class_scenarios(self, testdir): testdir.makepyfile(""" # same as doc/en/example/parametrize scenario example @@ -545,6 +555,20 @@ class TestMetafuncFunctional: reprec = testdir.inline_run() reprec.assertoutcome(passed=5) + def test_parametrize_issue323(self, testdir): + testdir.makepyfile(""" + import pytest + + @pytest.fixture(scope='module', params=range(966)) + def foo(request): + return request.param + + def test_it(foo): + pass + """) + reprec = testdir.inline_run("--collectonly") + assert not reprec.getcalls("pytest_internalerror") + def test_usefixtures_seen_in_generate_tests(self, testdir): testdir.makepyfile(""" import pytest @@ -578,3 +602,186 @@ class TestMetafuncFunctional: ]) +class TestMarkersWithParametrization: + pytestmark = pytest.mark.issue308 + def test_simple_mark(self, testdir): + s = """ + import pytest + + @pytest.mark.foo + @pytest.mark.parametrize(("n", "expected"), [ + (1, 2), + pytest.mark.bar((1, 3)), + (2, 3), + ]) + def test_increment(n, expected): + assert n + 1 == expected + """ + items = testdir.getitems(s) + assert len(items) == 3 + for item in items: + assert 'foo' in item.keywords + assert 'bar' not in items[0].keywords + assert 'bar' in items[1].keywords + assert 'bar' not in items[2].keywords + + def test_select_based_on_mark(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("n", "expected"), [ + (1, 2), + pytest.mark.foo((2, 3)), + (3, 4), + ]) + def test_increment(n, expected): + assert n + 1 == expected + """ + testdir.makepyfile(s) + rec = testdir.inline_run("-m", 'foo') + passed, skipped, fail = rec.listoutcomes() + assert len(passed) == 1 + assert len(skipped) == 0 + assert len(fail) == 0 + + @pytest.mark.xfail(reason="is this important to support??") + def test_nested_marks(self, testdir): + s = """ + import pytest + mastermark = pytest.mark.foo(pytest.mark.bar) + + @pytest.mark.parametrize(("n", "expected"), [ + (1, 2), + mastermark((1, 3)), + (2, 3), + ]) + def test_increment(n, expected): + assert n + 1 == expected + """ + items = testdir.getitems(s) + assert len(items) == 3 + for mark in ['foo', 'bar']: + assert mark not in items[0].keywords + assert mark in items[1].keywords + assert mark not in items[2].keywords + + def test_simple_xfail(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("n", "expected"), [ + (1, 2), + pytest.mark.xfail((1, 3)), + (2, 3), + ]) + def test_increment(n, expected): + assert n + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + # xfail is skip?? + reprec.assertoutcome(passed=2, skipped=1) + + def test_simple_xfail_single_argname(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize("n", [ + 2, + pytest.mark.xfail(3), + 4, + ]) + def test_isEven(n): + assert n % 2 == 0 + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2, skipped=1) + + def test_xfail_with_arg(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("n", "expected"), [ + (1, 2), + pytest.mark.xfail("True")((1, 3)), + (2, 3), + ]) + def test_increment(n, expected): + assert n + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2, skipped=1) + + def test_xfail_with_kwarg(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("n", "expected"), [ + (1, 2), + pytest.mark.xfail(reason="some bug")((1, 3)), + (2, 3), + ]) + def test_increment(n, expected): + assert n + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2, skipped=1) + + def test_xfail_with_arg_and_kwarg(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("n", "expected"), [ + (1, 2), + pytest.mark.xfail("True", reason="some bug")((1, 3)), + (2, 3), + ]) + def test_increment(n, expected): + assert n + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2, skipped=1) + + def test_xfail_passing_is_xpass(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("n", "expected"), [ + (1, 2), + pytest.mark.xfail("sys.version > 0", reason="some bug")((2, 3)), + (3, 4), + ]) + def test_increment(n, expected): + assert n + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + # xpass is fail, obviously :) + reprec.assertoutcome(passed=2, failed=1) + + def test_parametrize_called_in_generate_tests(self, testdir): + s = """ + import pytest + + + def pytest_generate_tests(metafunc): + passingTestData = [(1, 2), + (2, 3)] + failingTestData = [(1, 3), + (2, 2)] + + testData = passingTestData + [pytest.mark.xfail(d) + for d in failingTestData] + metafunc.parametrize(("n", "expected"), testData) + + + def test_increment(n, expected): + assert n + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2, skipped=2) diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 42c66e372..ff633b534 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -99,6 +99,11 @@ class TestAssert_reprcompare: expl = callequal(set([0, 1]), set([0, 2])) assert len(expl) > 1 + def test_frozenzet(self): + expl = callequal(frozenset([0, 1]), set([0, 2])) + print (expl) + assert len(expl) > 1 + def test_list_tuples(self): expl = callequal([], [(1,2)]) assert len(expl) > 1 @@ -315,3 +320,17 @@ def test_warn_missing(testdir): result.stderr.fnmatch_lines([ "*WARNING*assert statements are not executed*", ]) + +def test_recursion_source_decode(testdir): + testdir.makepyfile(""" + def test_something(): + pass + """) + testdir.makeini(""" + [pytest] + python_files = *.py + """) + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines(""" + + """) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 4841ff47c..34a1bc80a 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -1,4 +1,5 @@ import os +import stat import sys import zipfile import py @@ -319,10 +320,22 @@ class TestRewriteOnImport: def test_pycache_is_a_file(self, testdir): testdir.tmpdir.join("__pycache__").write("Hello") testdir.makepyfile(""" -def test_rewritten(): - assert "@py_builtins" in globals()""") + def test_rewritten(): + assert "@py_builtins" in globals()""") assert testdir.runpytest().ret == 0 + def test_pycache_is_readonly(self, testdir): + cache = testdir.tmpdir.mkdir("__pycache__") + old_mode = cache.stat().mode + cache.chmod(old_mode ^ stat.S_IWRITE) + testdir.makepyfile(""" + def test_rewritten(): + assert "@py_builtins" in globals()""") + try: + assert testdir.runpytest().ret == 0 + finally: + cache.chmod(old_mode) + def test_zipfile(self, testdir): z = testdir.tmpdir.join("myzip.zip") z_fn = str(z) @@ -334,9 +347,9 @@ def test_rewritten(): f.close() z.chmod(256) testdir.makepyfile(""" -import sys -sys.path.append(%r) -import test_gum.test_lizard""" % (z_fn,)) + import sys + sys.path.append(%r) + import test_gum.test_lizard""" % (z_fn,)) assert testdir.runpytest().ret == 0 def test_readonly(self, testdir): @@ -345,17 +358,21 @@ import test_gum.test_lizard""" % (z_fn,)) py.builtin._totext(""" def test_rewritten(): assert "@py_builtins" in globals() -""").encode("utf-8"), "wb") + """).encode("utf-8"), "wb") + old_mode = sub.stat().mode sub.chmod(320) - assert testdir.runpytest().ret == 0 + try: + assert testdir.runpytest().ret == 0 + finally: + sub.chmod(old_mode) def test_dont_write_bytecode(self, testdir, monkeypatch): testdir.makepyfile(""" -import os -def test_no_bytecode(): - assert "__pycache__" in __cached__ - assert not os.path.exists(__cached__) - assert not os.path.exists(os.path.dirname(__cached__))""") + import os + def test_no_bytecode(): + assert "__pycache__" in __cached__ + assert not os.path.exists(__cached__) + assert not os.path.exists(os.path.dirname(__cached__))""") monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1") assert testdir.runpytest().ret == 0 diff --git a/testing/test_doctest.py b/testing/test_doctest.py index 564c675e6..ca5e1388d 100644 --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,4 +1,4 @@ -from _pytest.doctest import DoctestModule, DoctestTextfile +from _pytest.doctest import DoctestItem, DoctestModule, DoctestTextfile import py, pytest class TestDoctests: @@ -19,13 +19,61 @@ class TestDoctests: items, reprec = testdir.inline_genitems(w) assert len(items) == 1 - def test_collect_module(self, testdir): + def test_collect_module_empty(self, testdir): path = testdir.makepyfile(whatever="#") + for p in (path, testdir.tmpdir): + items, reprec = testdir.inline_genitems(p, + '--doctest-modules') + assert len(items) == 0 + + def test_collect_module_single_modulelevel_doctest(self, testdir): + path = testdir.makepyfile(whatever='""">>> pass"""') for p in (path, testdir.tmpdir): items, reprec = testdir.inline_genitems(p, '--doctest-modules') assert len(items) == 1 - assert isinstance(items[0], DoctestModule) + assert isinstance(items[0], DoctestItem) + assert isinstance(items[0].parent, DoctestModule) + + def test_collect_module_two_doctest_one_modulelevel(self, testdir): + path = testdir.makepyfile(whatever=""" + '>>> x = None' + def my_func(): + ">>> magic = 42 " + """) + for p in (path, testdir.tmpdir): + items, reprec = testdir.inline_genitems(p, + '--doctest-modules') + assert len(items) == 2 + assert isinstance(items[0], DoctestItem) + assert isinstance(items[1], DoctestItem) + assert isinstance(items[0].parent, DoctestModule) + assert items[0].parent is items[1].parent + + def test_collect_module_two_doctest_no_modulelevel(self, testdir): + path = testdir.makepyfile(whatever=""" + '# Empty' + def my_func(): + ">>> magic = 42 " + def unuseful(): + ''' + # This is a function + # >>> # it doesn't have any doctest + ''' + def another(): + ''' + # This is another function + >>> import os # this one does have a doctest + ''' + """) + for p in (path, testdir.tmpdir): + items, reprec = testdir.inline_genitems(p, + '--doctest-modules') + assert len(items) == 2 + assert isinstance(items[0], DoctestItem) + assert isinstance(items[1], DoctestItem) + assert isinstance(items[0].parent, DoctestModule) + assert items[0].parent is items[1].parent def test_simple_doctestfile(self, testdir): p = testdir.maketxtfile(test_doc=""" @@ -164,3 +212,47 @@ class TestDoctests: """) reprec = testdir.inline_run(p, "--doctest-modules") reprec.assertoutcome(passed=1) + + def test_doctestmodule_three_tests(self, testdir): + p = testdir.makepyfile(""" + ''' + >>> dir = getfixture('tmpdir') + >>> type(dir).__name__ + 'LocalPath' + ''' + def my_func(): + ''' + >>> magic = 42 + >>> magic - 42 + 0 + ''' + def unuseful(): + pass + def another(): + ''' + >>> import os + >>> os is os + True + ''' + """) + reprec = testdir.inline_run(p, "--doctest-modules") + reprec.assertoutcome(passed=3) + + def test_doctestmodule_two_tests_one_fail(self, testdir): + p = testdir.makepyfile(""" + class MyClass: + def bad_meth(self): + ''' + >>> magic = 42 + >>> magic + 0 + ''' + def nice_meth(self): + ''' + >>> magic = 42 + >>> magic - 42 + 0 + ''' + """) + reprec = testdir.inline_run(p, "--doctest-modules") + reprec.assertoutcome(failed=1, passed=1) diff --git a/testing/test_junitxml.py b/testing/test_junitxml.py index dbb814961..139ba50e5 100644 --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -450,3 +450,16 @@ def test_logxml_changingdir(testdir): assert result.ret == 0 assert testdir.tmpdir.join("a/x.xml").check() +def test_escaped_parametrized_names_xml(testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.parametrize('char', ["\\x00"]) + def test_func(char): + assert char + """) + result, dom = runandparse(testdir) + assert result.ret == 0 + node = dom.getElementsByTagName("testcase")[0] + assert_attr(node, + name="test_func[#x00]") + diff --git a/testing/test_mark.py b/testing/test_mark.py index 48ca5204e..3caf625b2 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -345,6 +345,24 @@ class TestFunctional: assert l[0].args == ("pos0",) assert l[1].args == ("pos1",) + def test_no_marker_match_on_unmarked_names(self, testdir): + p = testdir.makepyfile(""" + import pytest + @pytest.mark.shouldmatch + def test_marked(): + assert 1 + + def test_unmarked(): + assert 1 + """) + reprec = testdir.inline_run("-m", "test_unmarked", p) + passed, skipped, failed = reprec.listoutcomes() + assert len(passed) + len(skipped) + len(failed) == 0 + dlist = reprec.getcalls("pytest_deselected") + deselected_tests = dlist[0].items + assert len(deselected_tests) == 2 + + def test_keywords_at_node_level(self, testdir): p = testdir.makepyfile(""" import pytest @@ -382,7 +400,6 @@ class TestKeywordSelection: assert len(reprec.getcalls('pytest_deselected')) == 1 for keyword in ['test_one', 'est_on']: - #yield check, keyword, 'test_one' check(keyword, 'test_one') check('TestClass and test', 'test_method_one') @@ -401,7 +418,7 @@ class TestKeywordSelection: def pytest_pycollect_makeitem(__multicall__, name): if name == "TestClass": item = __multicall__.execute() - item.keywords["xxx"] = True + item.extra_keyword_matches.add("xxx") return item """) reprec = testdir.inline_run(p.dirpath(), '-s', '-k', keyword) @@ -440,3 +457,22 @@ class TestKeywordSelection: reprec = testdir.inline_run("-k", "mykeyword", p) passed, skipped, failed = reprec.countoutcomes() assert failed == 1 + + def test_no_magic_values(self, testdir): + """Make sure the tests do not match on magic values, + no double underscored values, like '__dict__', + and no instance values, like '()'. + """ + p = testdir.makepyfile(""" + def test_one(): assert 1 + """) + def assert_test_is_not_selected(keyword): + reprec = testdir.inline_run("-k", keyword, p) + passed, skipped, failed = reprec.countoutcomes() + dlist = reprec.getcalls("pytest_deselected") + assert passed + skipped + failed == 0 + deselected_tests = dlist[0].items + assert len(deselected_tests) == 1 + + assert_test_is_not_selected("__") + assert_test_is_not_selected("()") diff --git a/testing/test_session.py b/testing/test_session.py index 0a475b814..df7463e02 100644 --- a/testing/test_session.py +++ b/testing/test_session.py @@ -226,3 +226,17 @@ def test_exclude(testdir): assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) +def test_sessionfinish_with_start(testdir): + testdir.makeconftest(""" + import os + l = [] + def pytest_sessionstart(): + l.append(os.getcwd()) + os.chdir("..") + + def pytest_sessionfinish(): + assert l[0] == os.getcwd() + + """) + res = testdir.runpytest("--collectonly") + assert res.ret == 0 diff --git a/testing/test_skipping.py b/testing/test_skipping.py index 03e9b6780..c74888746 100644 --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -569,7 +569,6 @@ def test_default_markers(testdir): "*xfail(*condition, reason=None, run=True)*expected failure*", ]) - def test_xfail_test_setup_exception(testdir): testdir.makeconftest(""" def pytest_runtest_setup(): @@ -610,3 +609,44 @@ def test_imperativeskip_on_xfail_test(testdir): """) +class TestBooleanCondition: + def test_skipif(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.skipif(True, reason="True123") + def test_func1(): + pass + @pytest.mark.skipif(False, reason="True123") + def test_func2(): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(""" + *1 passed*1 skipped* + """) + + def test_skipif_noreason(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.skipif(True) + def test_func(): + pass + """) + result = testdir.runpytest("-rs") + result.stdout.fnmatch_lines(""" + *1 error* + """) + + def test_xfail(self, testdir): + testdir.makepyfile(""" + import pytest + @pytest.mark.xfail(True, reason="True123") + def test_func(): + assert 0 + """) + result = testdir.runpytest("-rxs") + result.stdout.fnmatch_lines(""" + *XFAIL* + *True123* + *1 xfail* + """) diff --git a/testing/test_terminal.py b/testing/test_terminal.py index 5054ac398..d4316e9d4 100644 --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -665,3 +665,19 @@ def test_fdopen_kept_alive_issue124(testdir): result.stdout.fnmatch_lines([ "*2 passed*" ]) + + +def test_tbstyle_native_setup_error(testdir): + p = testdir.makepyfile(""" + import pytest + @pytest.fixture + def setup_error_fixture(): + raise Exception("error in exception") + + def test_error_fixture(setup_error_fixture): + pass + """) + result = testdir.runpytest("--tb=native") + result.stdout.fnmatch_lines([ + '*File *test_tbstyle_native_setup_error.py", line *, in setup_error_fixture*' + ]) diff --git a/testing/test_tmpdir.py b/testing/test_tmpdir.py index 31dd1776d..164ee68c6 100644 --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -16,6 +16,7 @@ def test_funcarg(testdir): # pytest_unconfigure has deleted the TempdirHandler already config = item.config config._tmpdirhandler = TempdirHandler(config) + item._initrequest() p = tmpdir(item._request) assert p.check() bn = p.basename.strip("0123456789") diff --git a/testing/test_unittest.py b/testing/test_unittest.py index eecc9da68..119cb9028 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -65,6 +65,28 @@ def test_setup(testdir): rep = reprec.matchreport("test_both", when="teardown") assert rep.failed and '42' in str(rep.longrepr) +def test_unittest_style_setup_teardown(testdir): + testpath = testdir.makepyfile(""" + l = [] + + def setUpModule(): + l.append(1) + + def tearDownModule(): + del l[0] + + def test_hello(): + assert l == [1] + + def test_world(): + assert l == [1] + """) + result = testdir.runpytest(testpath) + result.stdout.fnmatch_lines([ + "*2 passed*", + ]) + + def test_new_instances(testdir): testpath = testdir.makepyfile(""" import unittest diff --git a/tox.ini b/tox.ini index a25bd6eef..16e2c12d7 100644 --- a/tox.ini +++ b/tox.ini @@ -1,17 +1,13 @@ [tox] distshare={homedir}/.tox/distshare envlist=py25,py26,py27,py27-nobyte,py32,py33,py27-xdist,trial -indexserver= - pypi = https://pypi.python.org/simple - testrun = http://pypi.testrun.org - default = http://pypi.testrun.org [testenv] changedir=testing commands= py.test --lsof -rfsxX --junitxml={envlogdir}/junit-{envname}.xml [] deps= - :pypi:pexpect - :pypi:nose + pexpect + nose [testenv:genscript] changedir=. @@ -21,8 +17,8 @@ commands= py.test --genscript=pytest1 changedir=. basepython=python2.7 deps=pytest-xdist - :pypi:mock - :pypi:nose + mock + nose commands= py.test -n3 -rfsxX \ --junitxml={envlogdir}/junit-{envname}.xml testing @@ -35,12 +31,12 @@ setenv= PYTHONDONTWRITEBYTECODE=1 commands= py.test -n3 -rfsxX \ - --junitxml={envlogdir}/junit-{envname}.xml [] + --junitxml={envlogdir}/junit-{envname}.xml {posargs:testing} [testenv:trial] changedir=. -deps=:pypi:twisted - :pypi:pexpect +deps=twisted + pexpect commands= py.test -rsxf testing/test_unittest.py \ --junitxml={envlogdir}/junit-{envname}.xml {posargs:testing/test_unittest.py} @@ -51,17 +47,17 @@ deps= [testenv:py32] deps= - :pypi:nose + nose [testenv:py33] deps= - :pypi:nose + nose [testenv:doc] basepython=python changedir=doc/en -deps=:pypi:sphinx - :pypi:PyYAML +deps=sphinx + PyYAML commands= make clean @@ -70,15 +66,15 @@ commands= [testenv:regen] basepython=python changedir=doc/en -deps=:pypi:sphinx - :pypi:PyYAML +deps=sphinx + PyYAML commands= rm -rf /tmp/doc-exec* #pip install pytest==2.3.4 make regen [testenv:py31] -deps=:pypi:nose>=1.0 +deps=nose>=1.0 [testenv:py31-xdist] deps=pytest-xdist @@ -102,4 +98,4 @@ python_files=test_*.py *_test.py testing/*/*.py python_classes=Test Acceptance python_functions=test pep8ignore = E401 E225 E261 E128 E124 E302 -norecursedirs = .tox ja +norecursedirs = .tox ja .hg