From b2cb93e06d4f8bf6415e282ac01c55209029fb74 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 22 Apr 2013 10:35:48 +0200 Subject: [PATCH 01/62] allow re-running of a test item (as exercised by the pytest-rerunfailures plugins) by re-initializing and removing request/funcargs information in runtestprotocol() - which is a slightly odd place to add funcarg-related functionality but it allows all pytest_runtest_setup/teardown hooks to properly see a valid request/funcarg content on test items. --- CHANGELOG | 3 +++ _pytest/__init__.py | 2 +- _pytest/python.py | 17 +++++++++++------ _pytest/runner.py | 8 ++++++++ setup.py | 2 +- testing/python/integration.py | 32 ++++++++++++++++++++++++++++++++ testing/test_tmpdir.py | 1 + 7 files changed, 57 insertions(+), 8 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 38031a3c3..4d3d99b9f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ Changes between 2.3.4 and 2.3.5dev ----------------------------------- +- allow re-running of test items / helps to fix pytest-reruntests plugin + and also should 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..c1b1da136 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.5.dev8' +__version__ = '2.3.5.dev16' diff --git a/_pytest/python.py b/_pytest/python.py index 22f61aec0..34e7fffbd 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -917,20 +917,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): diff --git a/_pytest/runner.py b/_pytest/runner.py index 6014d6d9d..27f9c5f90 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): diff --git a/setup.py b/setup.py index 946495ec6..8563872cd 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.3.5.dev16', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], 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/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") From 3c317dc35ea05858c76b446b849d7b617b40479c Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Sun, 28 Apr 2013 20:56:56 +0100 Subject: [PATCH 02/62] Minor style cleanup --- _pytest/assertion/util.py | 29 ++++++++++++++++------------- 1 file changed, 16 insertions(+), 13 deletions(-) diff --git a/_pytest/assertion/util.py b/_pytest/assertion/util.py index 2179af541..0d428e47c 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) @@ -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 From 3ab94544b9f3ad4c48dc855891c07c2c3caacac1 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Sun, 28 Apr 2013 20:57:52 +0100 Subject: [PATCH 03/62] Ingore rope auto-generated files --- .hgignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgignore b/.hgignore index a4f2c3fe6..7184c073e 100644 --- a/.hgignore +++ b/.hgignore @@ -25,3 +25,4 @@ env/ .tox .cache .coverage +.ropeproject From 7a90515d49f495cae5b430dc252286d429322c34 Mon Sep 17 00:00:00 2001 From: Floris Bruynooghe Date: Sun, 28 Apr 2013 20:59:10 +0100 Subject: [PATCH 04/62] Treat frozenset as a set Thanks to Brianna Laugher. --- _pytest/assertion/util.py | 2 +- testing/test_assertion.py | 5 +++++ 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/_pytest/assertion/util.py b/_pytest/assertion/util.py index 0d428e47c..a0cff6b18 100644 --- a/_pytest/assertion/util.py +++ b/_pytest/assertion/util.py @@ -94,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 diff --git a/testing/test_assertion.py b/testing/test_assertion.py index 42c66e372..c85bec336 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 From c5f995878363dd5e452cba5ec42dd818dd379657 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 29 Apr 2013 10:31:51 +0200 Subject: [PATCH 05/62] never consider a fixture function for test function collection --- CHANGELOG | 4 +++- _pytest/python.py | 31 ++++++++++++++++++------------- testing/python/fixture.py | 14 ++++++++++++++ testing/test_assertion.py | 2 +- 4 files changed, 36 insertions(+), 15 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 4d3d99b9f..08ebea21e 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,8 +1,10 @@ Changes between 2.3.4 and 2.3.5dev ----------------------------------- +- never consider a fixture function for test function collection + - allow re-running of test items / helps to fix pytest-reruntests plugin - and also should help to keep less fixture/resource references alive + 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/python.py b/_pytest/python.py index 34e7fffbd..b78e33513 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -177,7 +177,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: @@ -1566,15 +1567,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 @@ -1771,6 +1764,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/testing/python/fixture.py b/testing/python/fixture.py index 02cd55c21..938555002 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1641,6 +1641,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"], diff --git a/testing/test_assertion.py b/testing/test_assertion.py index c85bec336..02199ef43 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -101,7 +101,7 @@ class TestAssert_reprcompare: def test_frozenzet(self): expl = callequal(frozenset([0, 1]), set([0, 2])) - print expl + print (expl) assert len(expl) > 1 def test_list_tuples(self): From 05c4ecf892b3ca11566a8d89aa5e776608349f1d Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 30 Apr 2013 12:05:58 +0200 Subject: [PATCH 06/62] fix recursion within import hook and source.decode in particular --- _pytest/assertion/rewrite.py | 14 ++++++++++---- testing/test_assertion.py | 14 ++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index e964ea9f8..8f51c30f3 100644 --- a/_pytest/assertion/rewrite.py +++ b/_pytest/assertion/rewrite.py @@ -215,11 +215,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/testing/test_assertion.py b/testing/test_assertion.py index 02199ef43..ff633b534 100644 --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -320,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(""" + + """) From 8c7ae7f7a5beb922443a73c9946f459ebd1cda52 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 30 Apr 2013 12:26:30 +0200 Subject: [PATCH 07/62] release 2.3.5 packaging --- _pytest/__init__.py | 2 +- doc/en/announce/release-2.3.5.txt | 50 +++++++++-- doc/en/assert.txt | 4 +- doc/en/capture.txt | 4 +- doc/en/doctest.txt | 2 +- doc/en/example/markers.txt | 24 ++--- doc/en/example/nonpython.txt | 12 +-- doc/en/example/parametrize.txt | 26 +++--- doc/en/example/pythoncollection.txt | 6 +- doc/en/example/reportingdemo.txt | 131 ++++++++++++++-------------- doc/en/example/simple.txt | 50 +++++------ doc/en/fixture.txt | 33 ++++--- doc/en/getting-started.txt | 10 +-- doc/en/parametrize.txt | 8 +- doc/en/skipping.txt | 2 +- doc/en/tmpdir.txt | 4 +- doc/en/unittest.txt | 6 +- setup.py | 2 +- 18 files changed, 206 insertions(+), 170 deletions(-) diff --git a/_pytest/__init__.py b/_pytest/__init__.py index c1b1da136..48c3378c7 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.5.dev16' +__version__ = '2.3.5' 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/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..309669350 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 @@ -232,7 +232,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 +243,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 +360,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 +373,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 +424,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 +445,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/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..e75d710e3 100644 --- a/doc/en/fixture.txt +++ b/doc/en/fixture.txt @@ -71,7 +71,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 +79,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 +89,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 @@ -189,7 +189,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 +197,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 +209,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 +218,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 @@ -271,7 +271,7 @@ using it 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'`` @@ -298,7 +298,6 @@ Running it:: > assert 0, smtp.helo() E AssertionError: (250, 'mail.python.org') - .. _`fixture-parametrize`: Parametrizing a fixture @@ -341,7 +340,7 @@ So let's just do another run:: ================================= FAILURES ================================= __________________________ test_ehlo[merlinux.eu] __________________________ - smtp = + smtp = def test_ehlo(smtp): response = smtp.ehlo() @@ -353,7 +352,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 +363,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 +422,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 @@ -488,7 +487,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 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/parametrize.txt b/doc/en/parametrize.txt index e62517a7d..6cc59ffda 100644 --- a/doc/en/parametrize.txt +++ b/doc/en/parametrize.txt @@ -53,7 +53,7 @@ which will thus run three times:: $ 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 3 items test_expectation.py ..F @@ -135,8 +135,8 @@ Let's also run with a stringinput that will lead to a failing test:: def test_valid_string(stringinput): > assert stringinput.isalpha() - E assert () - E + where = '!'.isalpha + E assert () + E + where = '!'.isalpha test_strings.py:3: AssertionError @@ -149,7 +149,7 @@ 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 + SKIP [1] /home/hpk/p/pytest/.tox/regen/local/lib/python2.7/site-packages/_pytest/python.py:974: got empty parameter set, function test_valid_string at /tmp/doc-exec-240/test_strings.py:1 For further examples, you might want to look at :ref:`more parametrization examples `. diff --git a/doc/en/skipping.txt b/doc/en/skipping.txt index 305c2d53f..9cb4cd92a 100644 --- a/doc/en/skipping.txt +++ b/doc/en/skipping.txt @@ -132,7 +132,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 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 8563872cd..6acb95fc2 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.dev16', + version='2.3.5', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From 4ac3445056f86557ca8c647b89a264dc2de780b8 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 30 Apr 2013 16:41:01 +0200 Subject: [PATCH 08/62] Added tag 2.3.5 for changeset fc3a793e87ec --- .hgtags | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgtags b/.hgtags index e4ac129e7..ee41596a0 100644 --- a/.hgtags +++ b/.hgtags @@ -54,3 +54,4 @@ acf0e1477fb19a1d35a4e40242b77fa6af32eb17 2.3.1 8738b828dec53937765db71951ef955cca4c51f6 2.3.2 7fe44182c434f8ac89149a3c340479872a5d5ccb 2.3.3 ef299e57f24218dbdd949498d7e660723636bcc3 2.3.4 +fc3a793e87ec907000a47ea0d3a372a2fe218c0a 2.3.5 From 331bd84ef4b737a39e5ffa03fbe9458c0ec9c13b Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 5 May 2013 14:23:47 +0200 Subject: [PATCH 09/62] change version --- doc/en/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 From 8e41ef5776e8a42b36137fd688c8b84891ffef15 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 5 May 2013 14:48:17 +0200 Subject: [PATCH 10/62] bump version --- _pytest/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 48c3378c7..b1e096143 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.5' +__version__ = '2.3.6.dev1' diff --git a/setup.py b/setup.py index 6acb95fc2..8365b0626 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', + version='2.3.6.dev1', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From 56aa9962fc7f7ba9185309c25781557c1435ea8a Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 5 May 2013 14:48:37 +0200 Subject: [PATCH 11/62] allow fixture functions to be implemented as context managers: @pytest.fixture def myfix(): # setup yield 1 # teardown --- CHANGELOG | 10 +++- _pytest/__init__.py | 2 +- _pytest/python.py | 25 +++++++++- setup.py | 2 +- testing/python/fixture.py | 98 +++++++++++++++++++++++++++++++++++++++ 5 files changed, 133 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 08ebea21e..9fd20860f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,12 @@ -Changes between 2.3.4 and 2.3.5dev +Changes between 2.3.5 and DEV +----------------------------------- + +- (experimental) allow fixture functions to be + implemented as context managers + + + +Changes between 2.3.4 and 2.3.5 ----------------------------------- - never consider a fixture function for test function collection diff --git a/_pytest/__init__.py b/_pytest/__init__.py index b1e096143..9df86916d 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.6.dev1' +__version__ = '2.3.6.dev2' diff --git a/_pytest/python.py b/_pytest/python.py index b78e33513..529e0d688 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1613,6 +1613,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, @@ -1663,7 +1686,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 diff --git a/setup.py b/setup.py index 8365b0626..1b4725108 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.6.dev1', + version='2.3.6.dev2', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff --git a/testing/python/fixture.py b/testing/python/fixture.py index 938555002..e82e38366 100644 --- a/testing/python/fixture.py +++ b/testing/python/fixture.py @@ -1795,3 +1795,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* + """) + + From d2dc797779262d9ebb28ae1301c31e72c78a1fef Mon Sep 17 00:00:00 2001 From: hg Date: Sun, 5 May 2013 22:15:06 +0200 Subject: [PATCH 12/62] #299 --- _pytest/runner.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/runner.py b/_pytest/runner.py index 27f9c5f90..3c491cd29 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -198,7 +198,7 @@ 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) From 19f3e06ab06112201251f8ae2ead95d1b6fb97ce Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 7 May 2013 10:48:13 +0200 Subject: [PATCH 13/62] Added tag 1.4.14 for changeset b93ac0cdae02 --- .hgtags | 1 + 1 file changed, 1 insertion(+) diff --git a/.hgtags b/.hgtags index ee41596a0..c6da72698 100644 --- a/.hgtags +++ b/.hgtags @@ -55,3 +55,4 @@ acf0e1477fb19a1d35a4e40242b77fa6af32eb17 2.3.1 7fe44182c434f8ac89149a3c340479872a5d5ccb 2.3.3 ef299e57f24218dbdd949498d7e660723636bcc3 2.3.4 fc3a793e87ec907000a47ea0d3a372a2fe218c0a 2.3.5 +b93ac0cdae02effaa3c136a681cc45bba757fe46 1.4.14 From 51688270ac48f3fe840691458cec12207a1b95b5 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 7 May 2013 10:53:31 +0200 Subject: [PATCH 14/62] implemented as context managers. Thanks Andreas Pelme, ladimir Keleshev. 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. --- AUTHORS | 1 + CHANGELOG | 6 +++++- setup.py | 2 +- 3 files changed, 7 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 9106dddef..bbf2f4327 100644 --- a/AUTHORS +++ b/AUTHORS @@ -6,6 +6,7 @@ Contributors include:: Ronny Pfannschmidt Benjamin Peterson Floris Bruynooghe +Jason R. Coombs Samuele Pedroni Carl Friedrich Bolz Armin Rigo diff --git a/CHANGELOG b/CHANGELOG index 9fd20860f..adc220622 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -2,8 +2,12 @@ Changes between 2.3.5 and DEV ----------------------------------- - (experimental) allow fixture functions to be - implemented as context managers + implemented as context managers. Thanks Andreas Pelme, + ladimir Keleshev. +- 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. Changes between 2.3.4 and 2.3.5 diff --git a/setup.py b/setup.py index 1b4725108..b0b02a87c 100644 --- a/setup.py +++ b/setup.py @@ -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', From 77d2f6adde71d7a545ff5bf6dc8d4ef0bf075a02 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 7 May 2013 10:54:05 +0200 Subject: [PATCH 15/62] fix issue245 by depending on py-1.4.14 which fixes py.io.dupfile to not assume file.mode is present. --- _pytest/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 9df86916d..5fedeed6e 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.6.dev2' +__version__ = '2.3.6.dev3' diff --git a/setup.py b/setup.py index b0b02a87c..4245dc859 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.6.dev2', + version='2.3.6.dev3', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From 71b4908233374d0131ca1b492468bc9db51fae7e Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 7 May 2013 10:54:46 +0200 Subject: [PATCH 16/62] Removed tag 1.4.14 --- .hgtags | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.hgtags b/.hgtags index c6da72698..6adff38a4 100644 --- a/.hgtags +++ b/.hgtags @@ -56,3 +56,5 @@ acf0e1477fb19a1d35a4e40242b77fa6af32eb17 2.3.1 ef299e57f24218dbdd949498d7e660723636bcc3 2.3.4 fc3a793e87ec907000a47ea0d3a372a2fe218c0a 2.3.5 b93ac0cdae02effaa3c136a681cc45bba757fe46 1.4.14 +b93ac0cdae02effaa3c136a681cc45bba757fe46 1.4.14 +0000000000000000000000000000000000000000 1.4.14 From 5fb4a100c9f9cecb0686d292b83c2b0189d91e5d Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 7 May 2013 10:55:41 +0200 Subject: [PATCH 17/62] Removed tag 1.4.14 --- .hgtags | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.hgtags b/.hgtags index 6adff38a4..ba85d24b0 100644 --- a/.hgtags +++ b/.hgtags @@ -58,3 +58,5 @@ fc3a793e87ec907000a47ea0d3a372a2fe218c0a 2.3.5 b93ac0cdae02effaa3c136a681cc45bba757fe46 1.4.14 b93ac0cdae02effaa3c136a681cc45bba757fe46 1.4.14 0000000000000000000000000000000000000000 1.4.14 +0000000000000000000000000000000000000000 1.4.14 +0000000000000000000000000000000000000000 1.4.14 From 3279cfa28bebad59e61c7ce56796f3a880daeb2a Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 7 May 2013 16:26:56 +0200 Subject: [PATCH 18/62] don't use indexservers anymore --- tox.ini | 30 +++++++++++++----------------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/tox.ini b/tox.ini index a25bd6eef..98c295fde 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 @@ -39,8 +35,8 @@ commands= [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 From bbd265184da359a166afd05cd7f5218d9cd3a842 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 7 May 2013 18:40:26 +0200 Subject: [PATCH 19/62] support boolean condition expressions in skipif/xfail change documentation to prefer it over string expressions --- CHANGELOG | 10 +- _pytest/__init__.py | 2 +- _pytest/skipping.py | 6 +- doc/en/skipping.txt | 222 ++++++++++++++++++++++++--------------- setup.py | 2 +- testing/test_skipping.py | 42 +++++++- 6 files changed, 191 insertions(+), 93 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index adc220622..d8cf212d1 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,9 +1,15 @@ -Changes between 2.3.5 and DEV +Changes between 2.3.5 and 2.4.DEV ----------------------------------- - (experimental) allow fixture functions to be implemented as context managers. Thanks Andreas Pelme, - ladimir Keleshev. + 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. - fix issue245 by depending on the released py-1.4.14 which fixes py.io.dupfile to work with files with no diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 5fedeed6e..bc428dcb5 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.3.6.dev3' +__version__ = '2.4.0.dev1' 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/doc/en/skipping.txt b/doc/en/skipping.txt index 9cb4cd92a..c2d738667 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(): ... @@ -151,41 +176,19 @@ Running it with the report-on-xfail option gives this output:: ======================== 6 xfailed in 0.05 seconds ========================= -.. _`evaluation of skipif/xfail conditions`: - -Evaluation of skipif/xfail expressions ----------------------------------------------------- - -.. versionadded:: 2.0.2 - -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(...): - ... - 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 +205,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/setup.py b/setup.py index 4245dc859..d257a973c 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.6.dev3', + version='2.4.0.dev1', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], 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* + """) From 9d8645b45d3fcaf96df883143d9d24c4d5928848 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 7 May 2013 21:34:59 +0200 Subject: [PATCH 20/62] enhance index page, fix announcement index --- doc/en/announce/index.txt | 1 + doc/en/index.txt | 15 +++++++++------ doc/en/plugins.txt | 13 ++++++++++--- 3 files changed, 20 insertions(+), 9 deletions(-) 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/index.txt b/doc/en/index.txt index bb1172cd2..912d6a4be 100644 --- a/doc/en/index.txt +++ b/doc/en/index.txt @@ -11,8 +11,11 @@ pytest: helps you write better programs - 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 +29,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 +53,8 @@ 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/plugins.txt b/doc/en/plugins.txt index ee941619b..1893ef3e0 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. From 150ad0172fa84b1808b52433d56bdfcee5b3d826 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 7 May 2013 21:37:08 +0200 Subject: [PATCH 21/62] document context fixtures, also improve plugin docs --- CHANGELOG | 2 +- doc/en/fixture.txt | 151 ++++++++++++++++++++++++++++++++------------- 2 files changed, 109 insertions(+), 44 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index d8cf212d1..75e8527a9 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -3,7 +3,7 @@ Changes between 2.3.5 and 2.4.DEV - (experimental) allow fixture functions to be implemented as context managers. Thanks Andreas Pelme, - Vladimir Keleshev. + Vladimir Keleshev. - (experimental) allow boolean expression directly with skipif/xfail if a "reason" is also specified. Rework skipping documentation diff --git a/doc/en/fixture.txt b/doc/en/fixture.txt index e75d710e3..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 @@ -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 @@ -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 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,10 @@ 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`: Parametrizing a fixture @@ -323,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 @@ -467,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): @@ -517,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 ---------------------------------------------------------------------- From 55cd3d8bf3238624db041247080a24ea5e5fbed7 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 7 May 2013 21:39:30 +0200 Subject: [PATCH 22/62] bump version --- _pytest/__init__.py | 2 +- setup.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/_pytest/__init__.py b/_pytest/__init__.py index bc428dcb5..9695ad68a 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.4.0.dev1' +__version__ = '2.4.0.dev2' diff --git a/setup.py b/setup.py index d257a973c..883bc2d63 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.4.0.dev1', + version='2.4.0.dev2', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From 963b944e795a1c434ac87e29bccd2abd5d0388c9 Mon Sep 17 00:00:00 2001 From: Jaap Broekhuizen Date: Wed, 8 May 2013 15:15:43 +0200 Subject: [PATCH 23/62] Fix junitxml generation when using special characters in parametrized tests. --- _pytest/junitxml.py | 5 +++-- pytest.py | 0 2 files changed, 3 insertions(+), 2 deletions(-) mode change 100644 => 100755 pytest.py 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.py b/pytest.py old mode 100644 new mode 100755 From 0e5f2847f1667d57168a0e157c62d17cb6e60b6e Mon Sep 17 00:00:00 2001 From: Jaap Broekhuizen Date: Wed, 8 May 2013 16:11:55 +0200 Subject: [PATCH 24/62] Fix pytest.py permissions. --- pytest.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 pytest.py diff --git a/pytest.py b/pytest.py old mode 100755 new mode 100644 From 9e3cd037213ffd4ce656c6901bd2006371360a20 Mon Sep 17 00:00:00 2001 From: maho Date: Wed, 8 May 2013 17:01:20 +0200 Subject: [PATCH 25/62] #299 - polishing --- _pytest/runner.py | 3 ++- testing/test_terminal.py | 16 ++++++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/_pytest/runner.py b/_pytest/runner.py index 3c491cd29..763f695ce 100644 --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -198,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,style=item.config.option.tbstyle) + 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/testing/test_terminal.py b/testing/test_terminal.py index e6977c457..bdafe27c1 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*' + ]) From d69c9da6562677a84b38a993abddd83d1fb4d241 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 9 May 2013 15:37:51 +0200 Subject: [PATCH 26/62] add maho as contributor --- AUTHORS | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS b/AUTHORS index bbf2f4327..b0f945ce0 100644 --- a/AUTHORS +++ b/AUTHORS @@ -10,6 +10,7 @@ Jason R. Coombs Samuele Pedroni Carl Friedrich Bolz Armin Rigo +Maho Maciek Fijalkowski Guido Wesdorp Brian Dorsey From 36e7cc1b9c5fa20d2d7b778439b48b9d8697a0d5 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 9 May 2013 15:50:09 +0200 Subject: [PATCH 27/62] honor --tb style for setup/teardown errors as well. --- CHANGELOG | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG b/CHANGELOG index 75e8527a9..bfbc9a5cf 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,6 +15,7 @@ Changes between 2.3.5 and 2.4.DEV which fixes py.io.dupfile to work with files with no mode. Thanks Jason R. Coombs. +- honor --tb style for setup/teardown errors as well. Changes between 2.3.4 and 2.3.5 ----------------------------------- From c610c903f683cd5d31c598b4b07b42d2752e03c9 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 9 May 2013 15:50:28 +0200 Subject: [PATCH 28/62] mention --tb style change in changelog --- CHANGELOG | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index bfbc9a5cf..5193c331b 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -15,7 +15,7 @@ Changes between 2.3.5 and 2.4.DEV which fixes py.io.dupfile to work with files with no mode. Thanks Jason R. Coombs. -- honor --tb style for setup/teardown errors as well. +- honor --tb style for setup/teardown errors as well. Thanks Maho. Changes between 2.3.4 and 2.3.5 ----------------------------------- From 7803bca335646df1e4155fa9e5464e193d16b466 Mon Sep 17 00:00:00 2001 From: Jaap Broekhuizen Date: Thu, 9 May 2013 21:16:57 +0200 Subject: [PATCH 29/62] Implemented a test for xml control character fail. --- testing/test_junitxml.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) 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]") + From 5a1ce3c45c5a1d287416d43787f030398d54b045 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 10 May 2013 08:14:39 +0200 Subject: [PATCH 30/62] add Jaap Broekhuizen for junitxml gen --- AUTHORS | 1 + CHANGELOG | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/AUTHORS b/AUTHORS index b0f945ce0..6e4f7714f 100644 --- a/AUTHORS +++ b/AUTHORS @@ -11,6 +11,7 @@ Samuele Pedroni Carl Friedrich Bolz Armin Rigo Maho +Jaap Broekhuizen Maciek Fijalkowski Guido Wesdorp Brian Dorsey diff --git a/CHANGELOG b/CHANGELOG index e989776e1..7d068cc13 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -16,7 +16,7 @@ Changes between 2.3.5 and 2.4.DEV mode. Thanks Jason R. Coombs. - fix junitxml generation when test output contains control characters, - addressing issue267 + addressing issue267, thanks Jaap Broekhuizen - honor --tb style for setup/teardown errors as well. Thanks Maho. From e6e86fa462fb4e4f44201afe84c6118e87a1f63a Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 16 May 2013 09:59:48 +0200 Subject: [PATCH 31/62] fix issue307 - use yaml.safe_load instead of yaml.load, thanks Mark Eichin. --- CHANGELOG | 2 ++ doc/en/example/nonpython/conftest.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG b/CHANGELOG index 7d068cc13..90ebf6e49 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -20,6 +20,8 @@ Changes between 2.3.5 and 2.4.DEV - 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 ----------------------------------- 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) From 5373a630086c2443adc8e3087305853194e7d88f Mon Sep 17 00:00:00 2001 From: Brianna Laugher Date: Fri, 17 May 2013 18:46:36 +1000 Subject: [PATCH 32/62] issue #308 first attempt, mark individual parametrize test instances with other marks (like xfail) --- _pytest/python.py | 23 ++++- testing/python/metafunc.py | 171 +++++++++++++++++++++++++++++++++++++ 2 files changed, 192 insertions(+), 2 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index 529e0d688..e55fe2148 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 @@ -565,11 +566,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 @@ -593,7 +596,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 @@ -605,6 +608,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: @@ -673,6 +677,18 @@ class Metafunc(FuncargnamesCompatAttr): if not argvalues: argvalues = [(_notexists,) * len(argnames)] + # these marks/keywords will be applied in Function init + newkeywords = {} + for i, argval in enumerate(argvalues): + newkeywords[i] = {} + if isinstance(argval, MarkDecorator): + # convert into a mark without the test content mixed in + newmark = MarkDecorator(argval.markname, argval.args[:-1], argval.kwargs) + newkeywords[i] = {newmark.markname: newmark} + + argvalues = [av.args[-1] if isinstance(av, MarkDecorator) else av + for av in argvalues] + if scope is None: scope = "subfunction" scopenum = scopes.index(scope) @@ -691,7 +707,7 @@ class Metafunc(FuncargnamesCompatAttr): assert len(valset) == len(argnames) newcallspec = callspec.copy(self) newcallspec.setmulti(valtype, argnames, valset, ids[i], - scopenum) + newkeywords[i], scopenum) newcalls.append(newcallspec) self._calls = newcalls @@ -908,6 +924,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 diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 60247212f..16f2da493 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -577,4 +577,175 @@ class TestMetafuncFunctional: "*3 passed*" ]) + @pytest.mark.issue308 + def test_mark_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.foo + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.bar((1, 3)), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 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 + + @pytest.mark.issue308 + def test_select_individual_parametrize_instance_based_on_mark(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.foo((2, 3)), + (3, 4), + ]) + def test_increment(input, expected): + assert input + 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("is this important to support??") + @pytest.mark.issue308 + def test_nested_marks_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.foo(pytest.mark.bar((1, 3))), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 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 + + @pytest.mark.xfail(reason="is this important to support??") + @pytest.mark.issue308 + def test_nested_marks_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + mastermark = pytest.mark.foo(pytest.mark.bar) + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + mastermark((1, 3)), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 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 + + @pytest.mark.issue308 + def test_simple_xfail_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.xfail((1, 3)), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + # xfail is skip?? + reprec.assertoutcome(passed=2, skipped=1) + + @pytest.mark.issue308 + def test_xfail_with_arg_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.xfail("sys.version > 0")((1, 3)), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2, skipped=1) + + @pytest.mark.issue308 + def test_xfail_with_kwarg_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.xfail(reason="some bug")((1, 3)), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2, skipped=1) + + @pytest.mark.issue308 + def test_xfail_with_arg_and_kwarg_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.xfail("sys.version > 0", reason="some bug")((1, 3)), + (2, 3), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + reprec.assertoutcome(passed=2, skipped=1) + + @pytest.mark.issue308 + def test_xfail_is_xpass_on_individual_parametrize_instance(self, testdir): + s = """ + import pytest + + @pytest.mark.parametrize(("input", "expected"), [ + (1, 2), + pytest.mark.xfail("sys.version > 0", reason="some bug")((2, 3)), + (3, 4), + ]) + def test_increment(input, expected): + assert input + 1 == expected + """ + testdir.makepyfile(s) + reprec = testdir.inline_run() + # xpass is fail, obviously :) + reprec.assertoutcome(passed=2, failed=1) From 242b67de178b2faa0604b997d23c8595b82be5da Mon Sep 17 00:00:00 2001 From: Danilo de Jesus da Silva Bellini Date: Fri, 17 May 2013 12:18:22 -0300 Subject: [PATCH 33/62] zero to many doctests from module instead of one --- _pytest/doctest.py | 26 ++++++++--- testing/test_doctest.py | 98 +++++++++++++++++++++++++++++++++++++++-- 2 files changed, 114 insertions(+), 10 deletions(-) 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/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) From afbeb056f065732c524e5ff39826b8ddcfe95314 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 17 May 2013 20:48:51 +0200 Subject: [PATCH 34/62] added changelog for improved doctest counting --- CHANGELOG | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 90ebf6e49..52feb271f 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,11 @@ Changes between 2.3.5 and 2.4.DEV 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. From ee65ca10f479c015030c93f00f83ba2bb4c6b486 Mon Sep 17 00:00:00 2001 From: Brianna Laugher Date: Mon, 20 May 2013 12:52:20 +1000 Subject: [PATCH 35/62] issue #308 address some comments by @hpk42 on 0b9d82e : - move tests into their own class, rename - add test showing metafunc.parametrize called in pytest_generate_tests rather than as decorator - add test and fix single-argname case - convert two loops into one in parametrize() also - renamed 'input' to 'n', since 'input' is a built-in --- _pytest/python.py | 27 ++++---- testing/python/metafunc.py | 134 ++++++++++++++++++++----------------- 2 files changed, 88 insertions(+), 73 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index e55fe2148..eab5ce8e6 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -671,24 +671,27 @@ class Metafunc(FuncargnamesCompatAttr): It will also override any fixture-function defined scope, allowing to set a dynamic scope using test context or configuration. """ + # remove any marks applied to individual tests instances + # these marks will be applied in Function init + newkeywords = {} + strippedargvalues = [] + for i, argval in enumerate(argvalues): + if isinstance(argval, MarkDecorator): + # convert into a mark without the test content mixed in + newmark = MarkDecorator(argval.markname, argval.args[:-1], argval.kwargs) + newkeywords[i] = {newmark.markname: newmark} + strippedargvalues.append(argval.args[-1]) + else: + newkeywords[i] = {} + strippedargvalues.append(argval) + argvalues = strippedargvalues + if not isinstance(argnames, (tuple, list)): argnames = (argnames,) argvalues = [(val,) for val in argvalues] if not argvalues: argvalues = [(_notexists,) * len(argnames)] - # these marks/keywords will be applied in Function init - newkeywords = {} - for i, argval in enumerate(argvalues): - newkeywords[i] = {} - if isinstance(argval, MarkDecorator): - # convert into a mark without the test content mixed in - newmark = MarkDecorator(argval.markname, argval.args[:-1], argval.kwargs) - newkeywords[i] = {newmark.markname: newmark} - - argvalues = [av.args[-1] if isinstance(av, MarkDecorator) else av - for av in argvalues] - if scope is None: scope = "subfunction" scopenum = scopes.index(scope) diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 16f2da493..786446637 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -577,19 +577,21 @@ class TestMetafuncFunctional: "*3 passed*" ]) - @pytest.mark.issue308 - def test_mark_on_individual_parametrize_instance(self, testdir): + +@pytest.mark.issue308 +class TestMarkersWithParametrization: + def test_simple_mark(self, testdir): s = """ import pytest @pytest.mark.foo - @pytest.mark.parametrize(("input", "expected"), [ + @pytest.mark.parametrize(("n", "expected"), [ (1, 2), pytest.mark.bar((1, 3)), (2, 3), ]) - def test_increment(input, expected): - assert input + 1 == expected + def test_increment(n, expected): + assert n + 1 == expected """ items = testdir.getitems(s) assert len(items) == 3 @@ -599,18 +601,17 @@ class TestMetafuncFunctional: assert 'bar' in items[1].keywords assert 'bar' not in items[2].keywords - @pytest.mark.issue308 - def test_select_individual_parametrize_instance_based_on_mark(self, testdir): + def test_select_based_on_mark(self, testdir): s = """ import pytest - @pytest.mark.parametrize(("input", "expected"), [ + @pytest.mark.parametrize(("n", "expected"), [ (1, 2), pytest.mark.foo((2, 3)), (3, 4), ]) - def test_increment(input, expected): - assert input + 1 == expected + def test_increment(n, expected): + assert n + 1 == expected """ testdir.makepyfile(s) rec = testdir.inline_run("-m", 'foo') @@ -619,41 +620,19 @@ class TestMetafuncFunctional: assert len(skipped) == 0 assert len(fail) == 0 - @pytest.mark.xfail("is this important to support??") - @pytest.mark.issue308 - def test_nested_marks_on_individual_parametrize_instance(self, testdir): - s = """ - import pytest - - @pytest.mark.parametrize(("input", "expected"), [ - (1, 2), - pytest.mark.foo(pytest.mark.bar((1, 3))), - (2, 3), - ]) - def test_increment(input, expected): - assert input + 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 - @pytest.mark.xfail(reason="is this important to support??") - @pytest.mark.issue308 - def test_nested_marks_on_individual_parametrize_instance(self, testdir): + def test_nested_marks(self, testdir): s = """ import pytest mastermark = pytest.mark.foo(pytest.mark.bar) - @pytest.mark.parametrize(("input", "expected"), [ + @pytest.mark.parametrize(("n", "expected"), [ (1, 2), mastermark((1, 3)), (2, 3), ]) - def test_increment(input, expected): - assert input + 1 == expected + def test_increment(n, expected): + assert n + 1 == expected """ items = testdir.getitems(s) assert len(items) == 3 @@ -662,90 +641,123 @@ class TestMetafuncFunctional: assert mark in items[1].keywords assert mark not in items[2].keywords - @pytest.mark.issue308 - def test_simple_xfail_on_individual_parametrize_instance(self, testdir): + def test_simple_xfail(self, testdir): s = """ import pytest - @pytest.mark.parametrize(("input", "expected"), [ + @pytest.mark.parametrize(("n", "expected"), [ (1, 2), pytest.mark.xfail((1, 3)), (2, 3), ]) - def test_increment(input, expected): - assert input + 1 == expected + 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) - @pytest.mark.issue308 - def test_xfail_with_arg_on_individual_parametrize_instance(self, testdir): + def test_simple_xfail_single_argname(self, testdir): s = """ import pytest - @pytest.mark.parametrize(("input", "expected"), [ + @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("sys.version > 0")((1, 3)), (2, 3), ]) - def test_increment(input, expected): - assert input + 1 == expected + def test_increment(n, expected): + assert n + 1 == expected """ testdir.makepyfile(s) reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=1) - @pytest.mark.issue308 - def test_xfail_with_kwarg_on_individual_parametrize_instance(self, testdir): + def test_xfail_with_kwarg(self, testdir): s = """ import pytest - @pytest.mark.parametrize(("input", "expected"), [ + @pytest.mark.parametrize(("n", "expected"), [ (1, 2), pytest.mark.xfail(reason="some bug")((1, 3)), (2, 3), ]) - def test_increment(input, expected): - assert input + 1 == expected + def test_increment(n, expected): + assert n + 1 == expected """ testdir.makepyfile(s) reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=1) - @pytest.mark.issue308 - def test_xfail_with_arg_and_kwarg_on_individual_parametrize_instance(self, testdir): + def test_xfail_with_arg_and_kwarg(self, testdir): s = """ import pytest - @pytest.mark.parametrize(("input", "expected"), [ + @pytest.mark.parametrize(("n", "expected"), [ (1, 2), pytest.mark.xfail("sys.version > 0", reason="some bug")((1, 3)), (2, 3), ]) - def test_increment(input, expected): - assert input + 1 == expected + def test_increment(n, expected): + assert n + 1 == expected """ testdir.makepyfile(s) reprec = testdir.inline_run() reprec.assertoutcome(passed=2, skipped=1) - @pytest.mark.issue308 - def test_xfail_is_xpass_on_individual_parametrize_instance(self, testdir): + def test_xfail_passing_is_xpass(self, testdir): s = """ import pytest - @pytest.mark.parametrize(("input", "expected"), [ + @pytest.mark.parametrize(("n", "expected"), [ (1, 2), pytest.mark.xfail("sys.version > 0", reason="some bug")((2, 3)), (3, 4), ]) - def test_increment(input, expected): - assert input + 1 == expected + 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) From fe27f3cc7d5e2a07ec79f08713ad42b4c74b0ed6 Mon Sep 17 00:00:00 2001 From: Wouter van Ackooy Date: Mon, 20 May 2013 14:37:58 +0200 Subject: [PATCH 36/62] Fixed issue #306: Keywords and markers are now matched in a defined way. Also applied some pep8 formatting while fixing. --- _pytest/main.py | 12 +++++ _pytest/mark.py | 122 ++++++++++++++++++++++++++++++++----------- testing/test_mark.py | 3 +- 3 files changed, 104 insertions(+), 33 deletions(-) diff --git a/_pytest/main.py b/_pytest/main.py index 95b2359bc..a039cf221 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -216,6 +216,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 = [] + #self.extrainit() @property @@ -307,6 +310,15 @@ class Node(object): chain.reverse() return chain + def listextrakeywords(self): + """ Return a list of all extra keywords in self and any parents.""" + extra_keywords = [] + item = self + while item is not None: + extra_keywords.extend(item.extra_keyword_matches) + item = item.parent + return extra_keywords + def listnames(self): return [x.name for x in self.listchain()] diff --git a/_pytest/mark.py b/_pytest/mark.py index 7b6b2d00d..d5981553b 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'.") + "where all names are substring-matched against test names" + "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' list, " + "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 = [] + for key, value in keywords.items(): + if isinstance(value, MarkInfo) or isinstance(value, MarkDecorator): + mymarks.append(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' list of + any item, as well as names directly assigned to test functions. + """ keywordexpr = keywordexpr.replace("-", "not ") - return eval(keywordexpr, {}, SubstringDict(colitem.keywords)) + mapped_names = [] + + # Add the names of the current item and any parent items + for item in colitem.listchain(): + if isinstance(item, pytest.Class) or isinstance(item, pytest.Function): + mapped_names.append(item.name) + + # Add the names added as extra keywords to current or parent items + for name in colitem.listextrakeywords(): + mapped_names.append(name) + + # Add the names attached to the current function through direct assignment + for name in colitem.function.func_dict: + mapped_names.append(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/testing/test_mark.py b/testing/test_mark.py index 48ca5204e..68788ead4 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -382,7 +382,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 +400,7 @@ class TestKeywordSelection: def pytest_pycollect_makeitem(__multicall__, name): if name == "TestClass": item = __multicall__.execute() - item.keywords["xxx"] = True + item.extra_keyword_matches.append("xxx") return item """) reprec = testdir.inline_run(p.dirpath(), '-s', '-k', keyword) From d8bc40271a19c0a1296fd3987094463da1f37405 Mon Sep 17 00:00:00 2001 From: Brianna Laugher Date: Tue, 21 May 2013 11:12:45 +1000 Subject: [PATCH 37/62] issue #308 + docs --- doc/en/example/markers.txt | 23 +++++++++++++++++++++++ doc/en/parametrize.txt | 12 ++++++++++++ doc/en/skipping.txt | 22 ++++++++++++++++++++++ 3 files changed, 57 insertions(+) diff --git a/doc/en/example/markers.txt b/doc/en/example/markers.txt index 309669350..afc6aa3b5 100644 --- a/doc/en/example/markers.txt +++ b/doc/en/example/markers.txt @@ -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`: diff --git a/doc/en/parametrize.txt b/doc/en/parametrize.txt index 6cc59ffda..779cef60e 100644 --- a/doc/en/parametrize.txt +++ b/doc/en/parametrize.txt @@ -82,6 +82,18 @@ And as usual with test function arguments, you can see the ``input`` and ``outpu Note that there ways how you can mark a class or a module, see :ref:`mark`. +It is also possible to mark individual test instances within parametrize:: + + # 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 + .. _`pytest_generate_tests`: diff --git a/doc/en/skipping.txt b/doc/en/skipping.txt index c2d738667..1c45d4219 100644 --- a/doc/en/skipping.txt +++ b/doc/en/skipping.txt @@ -176,6 +176,28 @@ Running it with the report-on-xfail option gives this output:: ======================== 6 xfailed in 0.05 seconds ========================= +.. _`skip/xfail with parametrize`: + +Skip/xfail with parametrize +--------------------------- + +It is possible to apply markers like skip and xfail to individual +test instances when using parametrize: + + import pytest + + @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 ------------------------------------------------------ From f78408df77f2369d7382ae82d381285abbc40ab3 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 21 May 2013 16:05:32 +0200 Subject: [PATCH 38/62] add holger's gittip account, would also like to add ronny's --- doc/en/_templates/layout.html | 6 ++++++ 1 file changed, 6 insertions(+) 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 %} From 02511d1564aa963d0bc5dbd625296eb6e0c175ae Mon Sep 17 00:00:00 2001 From: Wouter van Ackooy Date: Wed, 22 May 2013 07:41:46 +0200 Subject: [PATCH 39/62] Added lost space. --- _pytest/mark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/mark.py b/_pytest/mark.py index d5981553b..5f3d880d5 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -13,7 +13,7 @@ def pytest_addoption(parser): 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" + "where all names are substring-matched against test names " "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'. " From 8a0a18e9b369f758b7114b524f097ee69e75e1a3 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Wed, 22 May 2013 15:24:58 +0200 Subject: [PATCH 40/62] - add Brianna (@pfctdayelise ) to changelog and contributors - fix some broken tests on py32/py33 (related to issue308 merge) - re-format docstrings - --- AUTHORS | 1 + CHANGELOG | 3 +++ _pytest/python.py | 33 +++++++++++++++++---------------- testing/python/metafunc.py | 6 +++--- tox.ini | 2 +- 5 files changed, 25 insertions(+), 20 deletions(-) diff --git a/AUTHORS b/AUTHORS index 6e4f7714f..8cfa17f00 100644 --- a/AUTHORS +++ b/AUTHORS @@ -8,6 +8,7 @@ Benjamin Peterson Floris Bruynooghe Jason R. Coombs Samuele Pedroni +Brianna Laugher Carl Friedrich Bolz Armin Rigo Maho diff --git a/CHANGELOG b/CHANGELOG index 52feb271f..d561c8088 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,9 @@ Changes between 2.3.5 and 2.4.DEV ----------------------------------- +- fix issue 308 - allow to mark/xfail/skip individual parameter sets + when parametrizing. Thanks Brianna Laugher. + - (experimental) allow fixture functions to be implemented as context managers. Thanks Andreas Pelme, Vladimir Keleshev. diff --git a/_pytest/python.py b/_pytest/python.py index eab5ce8e6..1327fe1af 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -651,11 +651,12 @@ class Metafunc(FuncargnamesCompatAttr): :arg argnames: an argument name or a list of argument names - :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 @@ -671,20 +672,20 @@ class Metafunc(FuncargnamesCompatAttr): It will also override any fixture-function defined scope, allowing to set a dynamic scope using test context or configuration. """ - # remove any marks applied to individual tests instances - # these marks will be applied in Function init + + # 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 = {} - strippedargvalues = [] + unwrapped_argvalues = [] for i, argval in enumerate(argvalues): if isinstance(argval, MarkDecorator): - # convert into a mark without the test content mixed in - newmark = MarkDecorator(argval.markname, argval.args[:-1], argval.kwargs) + newmark = MarkDecorator(argval.markname, + argval.args[:-1], argval.kwargs) newkeywords[i] = {newmark.markname: newmark} - strippedargvalues.append(argval.args[-1]) - else: - newkeywords[i] = {} - strippedargvalues.append(argval) - argvalues = strippedargvalues + argval = argval.args[-1] + unwrapped_argvalues.append(argval) + argvalues = unwrapped_argvalues if not isinstance(argnames, (tuple, list)): argnames = (argnames,) @@ -710,7 +711,7 @@ class Metafunc(FuncargnamesCompatAttr): assert len(valset) == len(argnames) newcallspec = callspec.copy(self) newcallspec.setmulti(valtype, argnames, valset, ids[i], - newkeywords[i], scopenum) + newkeywords.get(i, {}), scopenum) newcalls.append(newcallspec) self._calls = newcalls diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 786446637..ccc1b2f22 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -578,8 +578,8 @@ class TestMetafuncFunctional: ]) -@pytest.mark.issue308 class TestMarkersWithParametrization: + pytestmark = pytest.mark.issue308 def test_simple_mark(self, testdir): s = """ import pytest @@ -680,7 +680,7 @@ class TestMarkersWithParametrization: @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.xfail("sys.version > 0")((1, 3)), + pytest.mark.xfail("True")((1, 3)), (2, 3), ]) def test_increment(n, expected): @@ -712,7 +712,7 @@ class TestMarkersWithParametrization: @pytest.mark.parametrize(("n", "expected"), [ (1, 2), - pytest.mark.xfail("sys.version > 0", reason="some bug")((1, 3)), + pytest.mark.xfail("True", reason="some bug")((1, 3)), (2, 3), ]) def test_increment(n, expected): diff --git a/tox.ini b/tox.ini index 98c295fde..3af69e880 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,7 @@ setenv= PYTHONDONTWRITEBYTECODE=1 commands= py.test -n3 -rfsxX \ - --junitxml={envlogdir}/junit-{envname}.xml [] + --junitxml={envlogdir}/junit-{envname}.xml {posargs:testing} [testenv:trial] changedir=. From 583c736f0c006642414dd9fab2ce607c677fbf91 Mon Sep 17 00:00:00 2001 From: Wouter van Ackooy Date: Thu, 23 May 2013 09:12:50 +0200 Subject: [PATCH 41/62] Added a test to check there is no matching on magic values. --- testing/test_mark.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/testing/test_mark.py b/testing/test_mark.py index 68788ead4..bbddf5476 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -439,3 +439,21 @@ 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 + assert len(dlist) == 1 + + assert_test_is_not_selected("__") + assert_test_is_not_selected("()") From 72afbbbd710ea2a3e984cedd159ad0d77bf45f1d Mon Sep 17 00:00:00 2001 From: Wouter van Ackooy Date: Thu, 23 May 2013 12:21:40 +0200 Subject: [PATCH 42/62] Added new test to check on matching markers to full test names, which was possible before. Also adjusted check on number of deselected tests. --- testing/test_mark.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/testing/test_mark.py b/testing/test_mark.py index bbddf5476..cc5b3290e 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 @@ -453,7 +471,8 @@ class TestKeywordSelection: passed, skipped, failed = reprec.countoutcomes() dlist = reprec.getcalls("pytest_deselected") assert passed + skipped + failed == 0 - assert len(dlist) == 1 + deselected_tests = dlist[0].items + assert len(deselected_tests) == 1 assert_test_is_not_selected("__") assert_test_is_not_selected("()") From 60906f7a46863416b67d329d45e39bcb01d5a28c Mon Sep 17 00:00:00 2001 From: Wouter van Ackooy Date: Mon, 27 May 2013 17:58:39 +0200 Subject: [PATCH 43/62] Issue 306: Use the names of all the parents in the chain for matching, except the Instance objects. --- _pytest/mark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/mark.py b/_pytest/mark.py index 5f3d880d5..67ff92817 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -130,7 +130,7 @@ def matchkeyword(colitem, keywordexpr): # Add the names of the current item and any parent items for item in colitem.listchain(): - if isinstance(item, pytest.Class) or isinstance(item, pytest.Function): + if not isinstance(item, pytest.Instance): mapped_names.append(item.name) # Add the names added as extra keywords to current or parent items From 212f4b4d64247a190ec25fd8c39f34dfa090e406 Mon Sep 17 00:00:00 2001 From: Wouter van Ackooy Date: Mon, 27 May 2013 18:14:35 +0200 Subject: [PATCH 44/62] Issue 306: Used a set for the extra_keywords, and used listchain for parent iteration. --- _pytest/main.py | 11 +++++------ _pytest/mark.py | 16 ++++++++-------- testing/test_mark.py | 2 +- 3 files changed, 14 insertions(+), 15 deletions(-) diff --git a/_pytest/main.py b/_pytest/main.py index a039cf221..9ef47faf9 100644 --- a/_pytest/main.py +++ b/_pytest/main.py @@ -217,7 +217,7 @@ class Node(object): self.keywords = NodeKeywords(self) #: allow adding of extra keywords to use for matching - self.extra_keyword_matches = [] + self.extra_keyword_matches = set() #self.extrainit() @@ -311,12 +311,11 @@ class Node(object): return chain def listextrakeywords(self): - """ Return a list of all extra keywords in self and any parents.""" - extra_keywords = [] + """ Return a set of all extra keywords in self and any parents.""" + extra_keywords = set() item = self - while item is not None: - extra_keywords.extend(item.extra_keyword_matches) - item = item.parent + for item in self.listchain(): + extra_keywords.update(item.extra_keyword_matches) return extra_keywords def listnames(self): diff --git a/_pytest/mark.py b/_pytest/mark.py index 67ff92817..eda74bff5 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -18,7 +18,7 @@ def pytest_addoption(parser): "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' list, " + "containing extra names in their 'extra_keyword_matches' set, " "as well as functions which have names assigned directly to them." ) @@ -87,10 +87,10 @@ class MarkMapping: :class:`MarkDecorator` items. """ def __init__(self, keywords): - mymarks = [] + mymarks = set() for key, value in keywords.items(): if isinstance(value, MarkInfo) or isinstance(value, MarkDecorator): - mymarks.append(key) + mymarks.add(key) self._mymarks = mymarks def __getitem__(self, markname): @@ -122,24 +122,24 @@ def matchkeyword(colitem, keywordexpr): 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' list of + 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 ") - mapped_names = [] + 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.append(item.name) + mapped_names.add(item.name) # Add the names added as extra keywords to current or parent items for name in colitem.listextrakeywords(): - mapped_names.append(name) + mapped_names.add(name) # Add the names attached to the current function through direct assignment for name in colitem.function.func_dict: - mapped_names.append(name) + mapped_names.add(name) return eval(keywordexpr, {}, KeywordMapping(mapped_names)) diff --git a/testing/test_mark.py b/testing/test_mark.py index cc5b3290e..3caf625b2 100644 --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -418,7 +418,7 @@ class TestKeywordSelection: def pytest_pycollect_makeitem(__multicall__, name): if name == "TestClass": item = __multicall__.execute() - item.extra_keyword_matches.append("xxx") + item.extra_keyword_matches.add("xxx") return item """) reprec = testdir.inline_run(p.dirpath(), '-s', '-k', keyword) From bc5a5a63f229ad629f6600677d102cde4d262308 Mon Sep 17 00:00:00 2001 From: Benjamin Peterson Date: Mon, 27 May 2013 14:04:53 -0700 Subject: [PATCH 45/62] use __dict__ not func_dict for Python 3 compatibility --- _pytest/mark.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/_pytest/mark.py b/_pytest/mark.py index eda74bff5..9961f9d0e 100644 --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -138,7 +138,7 @@ def matchkeyword(colitem, keywordexpr): mapped_names.add(name) # Add the names attached to the current function through direct assignment - for name in colitem.function.func_dict: + for name in colitem.function.__dict__: mapped_names.add(name) return eval(keywordexpr, {}, KeywordMapping(mapped_names)) From c294a417bdb57eba265cd0f123ec313cf7b515bc Mon Sep 17 00:00:00 2001 From: holger krekel Date: Tue, 28 May 2013 10:32:54 +0200 Subject: [PATCH 46/62] allow to specify parametrize inputs as a comma-separated string add Wouter to changelog and to authors --- AUTHORS | 1 + CHANGELOG | 7 ++++ _pytest/__init__.py | 2 +- _pytest/python.py | 6 ++-- doc/en/parametrize.txt | 70 +++++++++++++++++++++++++------------- setup.py | 2 +- testing/python/metafunc.py | 10 ++++++ 7 files changed, 71 insertions(+), 27 deletions(-) diff --git a/AUTHORS b/AUTHORS index 8cfa17f00..8dfc8e58e 100644 --- a/AUTHORS +++ b/AUTHORS @@ -7,6 +7,7 @@ Ronny Pfannschmidt Benjamin Peterson Floris Bruynooghe Jason R. Coombs +Wouter van Ackooy Samuele Pedroni Brianna Laugher Carl Friedrich Bolz diff --git a/CHANGELOG b/CHANGELOG index d561c8088..9b63f95ed 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -4,6 +4,13 @@ Changes between 2.3.5 and 2.4.DEV - 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. diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 9695ad68a..a5d89a853 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.4.0.dev2' +__version__ = '2.4.0.dev3' diff --git a/_pytest/python.py b/_pytest/python.py index 1327fe1af..3dcc0836f 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -649,7 +649,8 @@ 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 @@ -688,7 +689,8 @@ class Metafunc(FuncargnamesCompatAttr): 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)] diff --git a/doc/en/parametrize.txt b/doc/en/parametrize.txt index 779cef60e..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.5 + ============================= 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,19 +75,21 @@ 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:: +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"), [ + @pytest.mark.parametrize("input,expected", [ ("3+5", 8), ("2+4", 6), pytest.mark.xfail(("6*9", 42)), @@ -94,6 +97,27 @@ It is also possible to mark individual test instances within parametrize:: 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`: @@ -140,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 @@ -160,8 +184,8 @@ listlist:: $ py.test -q -rs test_strings.py s - ========================= short test summary info ========================== - SKIP [1] /home/hpk/p/pytest/.tox/regen/local/lib/python2.7/site-packages/_pytest/python.py:974: got empty parameter set, function test_valid_string at /tmp/doc-exec-240/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/setup.py b/setup.py index 883bc2d63..538d267ce 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.4.0.dev2', + version='2.4.0.dev3', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index ccc1b2f22..71438870a 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 From b1595d3f619169c44281ab7f0ffb245c01fbd22d Mon Sep 17 00:00:00 2001 From: Erik Bray Date: Tue, 28 May 2013 18:11:12 -0400 Subject: [PATCH 47/62] Adds a test for and fixes #112. If attempting to write to the __pycache__ directory raises a permission error _write_pyc() should just return False to prevent any further write attempts. --- _pytest/assertion/rewrite.py | 4 ++++ testing/test_assertrewrite.py | 19 ++++++++++++++++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/_pytest/assertion/rewrite.py b/_pytest/assertion/rewrite.py index 8f51c30f3..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()) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 4841ff47c..439fcc8e5 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 @@ -323,6 +324,18 @@ 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) @@ -346,8 +359,12 @@ import test_gum.test_lizard""" % (z_fn,)) def test_rewritten(): assert "@py_builtins" in globals() """).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(""" From da1996b5f5f66f4209cd6428307cfeb04146b59e Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 3 Jun 2013 10:04:50 +0200 Subject: [PATCH 48/62] fix issue316 - properly reference collection hooks in docs --- CHANGELOG | 3 +++ doc/en/plugins.txt | 10 +++++----- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 9b63f95ed..1fb52a037 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ Changes between 2.3.5 and 2.4.DEV ----------------------------------- +- fix issue316 - properly reference collection hooks in docs + - fix issue 308 - allow to mark/xfail/skip individual parameter sets when parametrizing. Thanks Brianna Laugher. @@ -37,6 +39,7 @@ Changes between 2.3.5 and 2.4.DEV - fix issue307 - use yaml.safe_load in example, thanks Mark Eichin. + Changes between 2.3.4 and 2.3.5 ----------------------------------- diff --git a/doc/en/plugins.txt b/doc/en/plugins.txt index 1893ef3e0..4c427a21e 100644 --- a/doc/en/plugins.txt +++ b/doc/en/plugins.txt @@ -345,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 =========================================================== From 0cfc4a49ea873edade736b565590db8c18c2d934 Mon Sep 17 00:00:00 2001 From: Alfredo Deza Date: Mon, 3 Jun 2013 10:07:14 -0400 Subject: [PATCH 49/62] adding ref targets on recwarn --- doc/en/recwarn.txt | 5 +++++ 1 file changed, 5 insertions(+) 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 ------------------------------------------------------- From 17e110658460bc464592cbc4c9885524b0917cba Mon Sep 17 00:00:00 2001 From: Erik Bray Date: Fri, 7 Jun 2013 17:30:10 -0400 Subject: [PATCH 50/62] reindent a few of the blockquotes in these tests --- testing/test_assertrewrite.py | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/testing/test_assertrewrite.py b/testing/test_assertrewrite.py index 439fcc8e5..34a1bc80a 100644 --- a/testing/test_assertrewrite.py +++ b/testing/test_assertrewrite.py @@ -320,8 +320,8 @@ 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): @@ -329,8 +329,8 @@ def test_rewritten(): old_mode = cache.stat().mode cache.chmod(old_mode ^ stat.S_IWRITE) testdir.makepyfile(""" -def test_rewritten(): - assert "@py_builtins" in globals()""") + def test_rewritten(): + assert "@py_builtins" in globals()""") try: assert testdir.runpytest().ret == 0 finally: @@ -347,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): @@ -358,7 +358,7 @@ 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) try: @@ -368,11 +368,11 @@ def test_rewritten(): 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 From ac3d8800fdf9750080a9c70843e14825f4c59ec9 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Mon, 10 Jun 2013 10:09:28 +0200 Subject: [PATCH 51/62] 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) --- CHANGELOG | 4 ++++ _pytest/main.py | 2 ++ testing/test_session.py | 14 ++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/CHANGELOG b/CHANGELOG index 1fb52a037..ccffa39cb 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,10 @@ Changes between 2.3.5 and 2.4.DEV ----------------------------------- +- 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 diff --git a/_pytest/main.py b/_pytest/main.py index 9ef47faf9..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, @@ -452,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/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 From 4af052b0981b6a0b5a46d5728c32caa0560281dd Mon Sep 17 00:00:00 2001 From: holger krekel Date: Thu, 20 Jun 2013 14:05:16 +0200 Subject: [PATCH 52/62] added some endorsements, not quite properly layouted --- doc/en/img/cramer2.png | Bin 0 -> 25291 bytes doc/en/img/gaynor3.png | Bin 0 -> 23032 bytes doc/en/img/keleshev.png | Bin 0 -> 23246 bytes doc/en/img/theuni.png | Bin 0 -> 31476 bytes doc/en/index.txt | 1 + doc/en/projects.txt | 17 +++++++++++++++++ 6 files changed, 18 insertions(+) create mode 100644 doc/en/img/cramer2.png create mode 100644 doc/en/img/gaynor3.png create mode 100644 doc/en/img/keleshev.png create mode 100644 doc/en/img/theuni.png diff --git a/doc/en/img/cramer2.png b/doc/en/img/cramer2.png new file mode 100644 index 0000000000000000000000000000000000000000..6bf0e92e20d9a7fe7579c3c8b56f56de1fb14268 GIT binary patch literal 25291 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 h!vAX!UYYpC literal 0 HcmV?d00001 diff --git a/doc/en/img/gaynor3.png b/doc/en/img/gaynor3.png new file mode 100644 index 0000000000000000000000000000000000000000..a577c168b391c54113cdc08ced5eb7436b16ce4b GIT binary patch literal 23032 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 HuiyU<;^WO; literal 0 HcmV?d00001 diff --git a/doc/en/img/theuni.png b/doc/en/img/theuni.png new file mode 100644 index 0000000000000000000000000000000000000000..abeb737e79b2811684d7eb829856465c367c4749 GIT binary patch literal 31476 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 RgSVsxBt&F|D+TrZ{};LwL`whw literal 0 HcmV?d00001 diff --git a/doc/en/index.txt b/doc/en/index.txt index 912d6a4be..e411ad9b0 100644 --- a/doc/en/index.txt +++ b/doc/en/index.txt @@ -56,6 +56,7 @@ pytest: helps you write better programs - 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 .. _`easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html 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 ========================== From 3bcd3317adb57213de153347cbf36015094b8719 Mon Sep 17 00:00:00 2001 From: variedthoughts Date: Thu, 20 Jun 2013 14:43:42 +0000 Subject: [PATCH 53/62] support unittest setUpModule/tearDownModule --- _pytest/python.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index 3dcc0836f..f2defda1b 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -371,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 @@ -382,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 From 5e77eb23ebd0b9a3ad34eb65892790482c64b610 Mon Sep 17 00:00:00 2001 From: Brian Okken Date: Sat, 22 Jun 2013 09:35:10 -0700 Subject: [PATCH 54/62] add test_unittest_style_setup_teardown() to test setUpModule() and tearDownModule() --- testing/test_unittest.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index eecc9da68..74c499939 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): + 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('-p', 'nose') + result.stdout.fnmatch_lines([ + "*2 passed*", + ]) + + def test_new_instances(testdir): testpath = testdir.makepyfile(""" import unittest From 28b28597185d006b1224cfdf187f9761fd9c221d Mon Sep 17 00:00:00 2001 From: Brian Okken Date: Sat, 22 Jun 2013 09:42:31 -0700 Subject: [PATCH 55/62] change how the test is called --- testing/test_unittest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/testing/test_unittest.py b/testing/test_unittest.py index 74c499939..119cb9028 100644 --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -66,7 +66,7 @@ def test_setup(testdir): assert rep.failed and '42' in str(rep.longrepr) def test_unittest_style_setup_teardown(testdir): - testdir.makepyfile(""" + testpath = testdir.makepyfile(""" l = [] def setUpModule(): @@ -81,7 +81,7 @@ def test_unittest_style_setup_teardown(testdir): def test_world(): assert l == [1] """) - result = testdir.runpytest('-p', 'nose') + result = testdir.runpytest(testpath) result.stdout.fnmatch_lines([ "*2 passed*", ]) From f9720a38fe0ae741937a1b4ca7de8b9552bc11a7 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Sun, 23 Jun 2013 09:24:48 +0200 Subject: [PATCH 56/62] mention added support for setUpModule/tearDownModule detection, thanks Brian Okken. --- AUTHORS | 1 + CHANGELOG | 2 ++ _pytest/__init__.py | 2 +- setup.py | 2 +- 4 files changed, 5 insertions(+), 2 deletions(-) diff --git a/AUTHORS b/AUTHORS index 8dfc8e58e..0ac43650a 100644 --- a/AUTHORS +++ b/AUTHORS @@ -30,3 +30,4 @@ Christian Tismer Daniel Nuri Graham Horler Andreas Zeidler +Brian Okken diff --git a/CHANGELOG b/CHANGELOG index ccffa39cb..155f6fffa 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ Changes between 2.3.5 and 2.4.DEV ----------------------------------- +- 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) diff --git a/_pytest/__init__.py b/_pytest/__init__.py index a5d89a853..654c9a59a 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.4.0.dev3' +__version__ = '2.4.0.dev4' diff --git a/setup.py b/setup.py index 538d267ce..d8b32746b 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.4.0.dev3', + version='2.4.0.dev4', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From 469830fffa08a912ebf7a722e3d8438249944058 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 28 Jun 2013 12:54:10 +0200 Subject: [PATCH 57/62] some internal renaming to make more sense of the sorting algo, no semantical changes. --- _pytest/python.py | 28 +++++++++++++++------------- 1 file changed, 15 insertions(+), 13 deletions(-) diff --git a/_pytest/python.py b/_pytest/python.py index f2defda1b..1c2b00bfa 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1747,29 +1747,31 @@ def getfuncargnames(function, startindex=None): def parametrize_sorted(items, ignore, cache, scopenum): if scopenum >= 3: return items - newitems = [] - olditems = [] + similar_items = [] + other_items = [] slicing_argparam = None 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 similar_items: 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. """ From c4c966683c082aedb461f94b49e1ef73e4346037 Mon Sep 17 00:00:00 2001 From: holger krekel Date: Fri, 28 Jun 2013 12:57:10 +0200 Subject: [PATCH 58/62] fix issue323 - parametrize() of many module-scoped params --- CHANGELOG | 2 ++ _pytest/__init__.py | 2 +- _pytest/python.py | 10 +++++++++- setup.py | 2 +- testing/python/metafunc.py | 14 ++++++++++++++ tox.ini | 2 +- 6 files changed, 28 insertions(+), 4 deletions(-) diff --git a/CHANGELOG b/CHANGELOG index 155f6fffa..a2a129fde 100644 --- a/CHANGELOG +++ b/CHANGELOG @@ -1,6 +1,8 @@ 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 diff --git a/_pytest/__init__.py b/_pytest/__init__.py index 654c9a59a..0fcc78673 100644 --- a/_pytest/__init__.py +++ b/_pytest/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '2.4.0.dev4' +__version__ = '2.4.0.dev5' diff --git a/_pytest/python.py b/_pytest/python.py index 1c2b00bfa..db5d2cca2 100644 --- a/_pytest/python.py +++ b/_pytest/python.py @@ -1747,9 +1747,16 @@ def getfuncargnames(function, startindex=None): def parametrize_sorted(items, ignore, cache, scopenum): if scopenum >= 3: return items + + # 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: @@ -1759,7 +1766,8 @@ def parametrize_sorted(items, ignore, cache, scopenum): similar_items.append(item) else: other_items.append(item) - if similar_items: + + if (len(similar_items) + slicing_index) > 1: newignore = ignore.copy() newignore.add(slicing_argparam) part2 = parametrize_sorted( diff --git a/setup.py b/setup.py index d8b32746b..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.4.0.dev4', + version='2.4.0.dev5', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], diff --git a/testing/python/metafunc.py b/testing/python/metafunc.py index 71438870a..75e3aa461 100644 --- a/testing/python/metafunc.py +++ b/testing/python/metafunc.py @@ -555,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 diff --git a/tox.ini b/tox.ini index 3af69e880..16e2c12d7 100644 --- a/tox.ini +++ b/tox.ini @@ -98,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 From ea4a3adfd6fe78df5103b499fb7123433041aa7a Mon Sep 17 00:00:00 2001 From: holger krekel Date: Wed, 3 Jul 2013 11:47:18 +0200 Subject: [PATCH 59/62] - add my ep2013 talk to talks page - add "talks/blogs" to the navigation side bar --- doc/en/_templates/localtoc.html | 2 ++ doc/en/index.txt | 3 --- doc/en/talks.txt | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) 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/index.txt b/doc/en/index.txt index e411ad9b0..0ee4fed2f 100644 --- a/doc/en/index.txt +++ b/doc/en/index.txt @@ -4,9 +4,6 @@ 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 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 --------------------------------------------- From ca88c02507e0bb47d281ea9017dc93f2e05a6112 Mon Sep 17 00:00:00 2001 From: Christian Theune Date: Wed, 3 Jul 2013 19:41:05 +0200 Subject: [PATCH 60/62] Support working in a local virtualenv. --- .hgignore | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.hgignore b/.hgignore index 7184c073e..bbf48ddac 100644 --- a/.hgignore +++ b/.hgignore @@ -4,6 +4,13 @@ syntax:glob .svn .hgsvn +# Ingore 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 From d9f0a28da2a5225669cd41c7d9bfd040e04b533f Mon Sep 17 00:00:00 2001 From: Christian Theune Date: Wed, 3 Jul 2013 19:43:18 +0200 Subject: [PATCH 61/62] Compatibility with my spinal cord reflexes: colorize last summary line. Provide a red bar if there are any 'failures'. Otherwise make it green. --- _pytest/terminal.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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) From 1934f515efcdfdb3a2d97440c8e35ec9810a3f97 Mon Sep 17 00:00:00 2001 From: Christian Theune Date: Wed, 3 Jul 2013 17:48:57 +0000 Subject: [PATCH 62/62] Typo --HG-- branch : ctheune/typo-1372873724648 --- .hgignore | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.hgignore b/.hgignore index bbf48ddac..402cafa0b 100644 --- a/.hgignore +++ b/.hgignore @@ -1,10 +1,9 @@ - # Automatically generated by `hgimportsvn` syntax:glob .svn .hgsvn -# Ingore local virtualenvs +# Ignore local virtualenvs syntax:glob lib/ bin/