From commits-noreply at bitbucket.org Mon Nov 1 00:26:27 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 31 Oct 2010 18:26:27 -0500 (CDT) Subject: [py-svn] pytest commit ad3773acb10a: simplify session object and rename some more hooks, not exposed/released yet Message-ID: <20101031232627.113261E103D@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288567632 -3600 # Node ID ad3773acb10a58745b9919cc37d4d720ef2ed971 # Parent 629f096561d6e38a59731a4b10eae36427f5c71d simplify session object and rename some more hooks, not exposed/released yet --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -37,17 +37,17 @@ def pytest_configure(config): def pytest_unconfigure(config): """ called before test process is exited. """ -def pytest_runtest_mainloop(session): +def pytest_runtestloop(session): """ called for performing the main runtest loop (after collection. """ -pytest_runtest_mainloop.firstresult = True +pytest_runtestloop.firstresult = True # ------------------------------------------------------------------------- # collection hooks # ------------------------------------------------------------------------- -def pytest_collection_perform(session): +def pytest_collection(session): """ perform the collection protocol for the given session. """ -pytest_collection_perform.firstresult = True +pytest_collection.firstresult = True def pytest_collection_modifyitems(config, items): """ called after collection has been performed, may filter or re-order --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -8,6 +8,13 @@ import py import pytest import os, sys +# exitcodes for the command line +EXIT_OK = 0 +EXIT_TESTSFAILED = 1 +EXIT_INTERRUPTED = 2 +EXIT_INTERNALERROR = 3 +EXIT_NOHOSTS = 4 + def pytest_addoption(parser): group = parser.getgroup("general", "running and selection options") @@ -44,9 +51,35 @@ def pytest_configure(config): config.option.maxfail = 1 def pytest_cmdline_main(config): - return Session(config).main() + """ default command line protocol for initialization, collection, + running tests and reporting. """ + session = Session(config) + session.exitstatus = EXIT_OK + try: + config.pluginmanager.do_configure(config) + config.hook.pytest_sessionstart(session=session) + config.hook.pytest_collection(session=session) + config.hook.pytest_runtestloop(session=session) + except pytest.UsageError: + raise + except KeyboardInterrupt: + excinfo = py.code.ExceptionInfo() + config.hook.pytest_keyboard_interrupt(excinfo=excinfo) + session.exitstatus = EXIT_INTERRUPTED + except: + excinfo = py.code.ExceptionInfo() + config.pluginmanager.notify_exception(excinfo) + session.exitstatus = EXIT_INTERNALERROR + if excinfo.errisinstance(SystemExit): + sys.stderr.write("mainloop: caught Spurious SystemExit!\n") + if not session.exitstatus and session._testsfailed: + session.exitstatus = EXIT_TESTSFAILED + config.hook.pytest_sessionfinish(session=session, + exitstatus=session.exitstatus) + config.pluginmanager.do_unconfigure(config) + return session.exitstatus -def pytest_collection_perform(session): +def pytest_collection(session): collection = session.collection assert not hasattr(collection, 'items') hook = session.config.hook @@ -55,7 +88,7 @@ def pytest_collection_perform(session): hook.pytest_collection_finish(collection=collection) return True -def pytest_runtest_mainloop(session): +def pytest_runtestloop(session): if session.config.option.collectonly: return True for item in session.collection.items: @@ -86,16 +119,7 @@ def pytest_collect_directory(path, paren def pytest_report_iteminfo(item): return item.reportinfo() - -# exitcodes for the command line -EXIT_OK = 0 -EXIT_TESTSFAILED = 1 -EXIT_INTERRUPTED = 2 -EXIT_INTERNALERROR = 3 -EXIT_NOHOSTS = 4 - class Session(object): - nodeid = "" class Interrupted(KeyboardInterrupt): """ signals an interrupted test run. """ __module__ = 'builtins' # for py3 @@ -120,37 +144,6 @@ class Session(object): self._testsfailed) pytest_collectreport = pytest_runtest_logreport - def main(self): - """ main loop for running tests. """ - self.shouldstop = False - self.exitstatus = EXIT_OK - config = self.config - try: - config.pluginmanager.do_configure(config) - config.hook.pytest_sessionstart(session=self) - config.hook.pytest_collection_perform(session=self) - config.hook.pytest_runtest_mainloop(session=self) - except pytest.UsageError: - raise - except KeyboardInterrupt: - excinfo = py.code.ExceptionInfo() - self.config.hook.pytest_keyboard_interrupt(excinfo=excinfo) - self.exitstatus = EXIT_INTERRUPTED - except: - excinfo = py.code.ExceptionInfo() - self.config.pluginmanager.notify_exception(excinfo) - self.exitstatus = EXIT_INTERNALERROR - if excinfo.errisinstance(SystemExit): - sys.stderr.write("mainloop: caught Spurious SystemExit!\n") - - if not self.exitstatus and self._testsfailed: - self.exitstatus = EXIT_TESTSFAILED - self.config.hook.pytest_sessionfinish( - session=self, exitstatus=self.exitstatus, - ) - config.pluginmanager.do_unconfigure(config) - return self.exitstatus - class Collection: def __init__(self, config): self.config = config From commits-noreply at bitbucket.org Mon Nov 1 00:26:48 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 31 Oct 2010 18:26:48 -0500 (CDT) Subject: [py-svn] pytest-xdist commit ba07fdbea9d9: adapt to new pytest changes, use new addini/getini methods for rsyncdirs / rsyncignore options Message-ID: <20101031232648.C1DE76C129F@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1288567663 -3600 # Node ID ba07fdbea9d953cf4f55db67e2e242527948019f # Parent 407d2815cad2f31af54070ddbc9367c8cba31ee6 adapt to new pytest changes, use new addini/getini methods for rsyncdirs / rsyncignore options --- a/xdist/plugin.py +++ b/xdist/plugin.py @@ -126,17 +126,18 @@ put options values in a ``conftest.py`` Any commandline ``--tx`` specifictions will add to the list of available execution environments. -Specifying "rsync" dirs in a conftest.py -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ +Specifying "rsync" dirs in a setup.cfg|tox.ini ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -In your ``mypkg/conftest.py`` you may specify directories to synchronise -or to exclude:: +In a ``tox.ini`` or ``setup.cfg`` file in your root project directory +you may specify directories to include or to exclude in synchronisation:: - rsyncdirs = ['.', '../plugins'] - rsyncignore = ['_cache'] + [pytest] + rsyncdirs = . mypkg helperpkg + rsyncignore = .hg These directory specifications are relative to the directory -where the ``conftest.py`` is found. +where the configuration file was found. """ @@ -173,6 +174,11 @@ def pytest_addoption(parser): group.addoption('--rsyncdir', action="append", default=[], metavar="dir1", help="add directory for rsyncing to remote tx nodes.") + parser.addini('rsyncdirs', 'list of (relative) paths to be rsynced for' + ' remote distributed testing.', type="pathlist") + parser.addini('rsyncignore', 'list of (relative) paths to be ignored ' + 'for rsyncing.', type="pathlist") + # ------------------------------------------------------------------------- # distributed testing hooks # ------------------------------------------------------------------------- --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -168,12 +168,12 @@ class DSession: self.terminal = None def report_line(self, line): - if self.terminal: + if self.terminal and self.config.option.verbose >= 0: self.terminal.write_line(line) def pytest_sessionstart(self, session, __multicall__): #print "remaining multicall methods", __multicall__.methods - if not self.config.getvalue("verbose"): + if self.config.option.verbose > 0: self.report_line("instantiating gateways (use -v for details): %s" % ",".join(self.config.option.tx)) self.nodemanager = NodeManager(self.config) @@ -183,11 +183,11 @@ class DSession: """ teardown any resources after a test run. """ self.nodemanager.teardown_nodes() - def pytest_perform_collection(self, __multicall__): + def pytest_collection(self, __multicall__): # prohibit collection of test items in master process __multicall__.methods[:] = [] - def pytest_runtest_mainloop(self): + def pytest_runtestloop(self): numnodes = len(self.nodemanager.gwmanager.specs) dist = self.config.getvalue("dist") if dist == "load": @@ -316,13 +316,13 @@ class TerminalDistReporter: def pytest_gwmanage_newgateway(self, gateway): rinfo = gateway._rinfo() - if self.config.getvalue("verbose"): + if self.config.option.verbose >= 0: version = "%s.%s.%s" %rinfo.version_info[:3] self.write_line("[%s] %s Python %s cwd: %s" % ( gateway.id, rinfo.platform, version, rinfo.cwd)) def pytest_testnodeready(self, node): - if self.config.getvalue("verbose"): + if self.config.option.verbose >= 0: d = node.slaveinfo infoline = "[%s] Python %s" %( d['id'], --- a/testing/test_slavemanage.py +++ b/testing/test_slavemanage.py @@ -173,8 +173,9 @@ class TestNodeManager: source.ensure("dir1", "somefile", dir=1) dir2.ensure("hello") source.ensure("bogusdir", "file") - source.join("conftest.py").write(py.code.Source(""" - rsyncdirs = ['dir1/dir2'] + source.join("tox.ini").write(py.std.textwrap.dedent(""" + [pytest] + rsyncdirs=dir1/dir2 """)) config = testdir.reparseconfig([source]) nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) @@ -189,9 +190,10 @@ class TestNodeManager: dir5 = source.ensure("dir5", "dir6", "bogus") dirf = source.ensure("dir5", "file") dir2.ensure("hello") - source.join("conftest.py").write(py.code.Source(""" - rsyncdirs = ['dir1', 'dir5'] - rsyncignore = ['dir1/dir2', 'dir5/dir6'] + source.join("tox.ini").write(py.std.textwrap.dedent(""" + [pytest] + rsyncdirs = dir1 dir5 + rsyncignore = dir1/dir2 dir5/dir6 """)) config = testdir.reparseconfig([source]) nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,9 @@ 1.5a1 ------------------------- - +- adapt to pytest-2.0 changes, rsyncdirs and rsyncignore can now + only be specified in [pytest] sections of ini files, see "py.test -h" + for details. - major internal refactoring to match the py-1.4 event refactoring - perform test collection always at slave side instead of at the master - make python2/python3 bridging work, remove usage of pickling --- a/xdist/slavemanage.py +++ b/xdist/slavemanage.py @@ -1,4 +1,4 @@ -import py +import py, pytest import sys, os import execnet import xdist.remote @@ -18,7 +18,7 @@ class NodeManager(object): self.config.hook.pytest_trace(category="nodemanage", msg=msg) def config_getignores(self): - return self.config.getconftest_pathlist("rsyncignore") + return self.config.getini("rsyncignore") def rsync_roots(self): """ make sure that all remote gateways @@ -78,7 +78,7 @@ class NodeManager(object): else: xspeclist.extend([xspec[i+1:]] * num) if not xspeclist: - raise config.Error( + raise pytest.UsageError( "MISSING test execution (tx) nodes: please specify --tx") return [execnet.XSpec(x) for x in xspeclist] @@ -86,14 +86,14 @@ class NodeManager(object): config = self.config candidates = [py._pydir] candidates += config.option.rsyncdir - conftestroots = config.getconftest_pathlist("rsyncdirs") + conftestroots = config.getini("rsyncdirs") if conftestroots: candidates.extend(conftestroots) roots = [] for root in candidates: root = py.path.local(root).realpath() if not root.check(): - raise config.Error("rsyncdir doesn't exist: %r" %(root,)) + raise pytest.UsageError("rsyncdir doesn't exist: %r" %(root,)) if root not in roots: roots.append(root) return roots --- a/xdist/looponfail.py +++ b/xdist/looponfail.py @@ -145,7 +145,7 @@ class SlaveFailSession: if self.config.option.debug: print(" ".join(map(str, args))) - def pytest_perform_collection(self, session): + def pytest_collection(self, session): self.session = session self.collection = session.collection self.topdir, self.trails = self.current_command --- a/xdist/remote.py +++ b/xdist/remote.py @@ -42,10 +42,10 @@ class SlaveInteractor: self.sendevent("slavefinished", slaveoutput=self.config.slaveoutput) return res - def pytest_perform_collection(self, session): + def pytest_collection(self, session): self.sendevent("collectionstart") - def pytest_runtest_mainloop(self, session): + def pytest_runtestloop(self, session): self.log("entering main loop") while 1: name, kwargs = self.channel.receive() @@ -62,7 +62,7 @@ class SlaveInteractor: break return True - def pytest_log_finishcollection(self, collection): + def pytest_collection_finish(self, collection): ids = [collection.getid(item) for item in collection.items] self.sendevent("collectionfinish", topdir=str(collection.topdir), From commits-noreply at bitbucket.org Mon Nov 1 00:37:50 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 31 Oct 2010 18:37:50 -0500 (CDT) Subject: [py-svn] pytest commit b9dba72b749a: fix tests by using less likely existing import names Message-ID: <20101031233750.5A1271E103D@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288568324 -3600 # Node ID b9dba72b749a3bba95083da8bef0b5a562ca6e9f # Parent ad3773acb10a58745b9919cc37d4d720ef2ed971 fix tests by using less likely existing import names --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -85,8 +85,8 @@ class TestBootstrapping: def test_import_plugin_importname(self, testdir): pluginmanager = PluginManager() - py.test.raises(ImportError, 'pluginmanager.import_plugin("x.y")') - py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_x.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') reset = testdir.syspathinsert() pluginname = "pytest_hello" @@ -103,8 +103,8 @@ class TestBootstrapping: def test_import_plugin_dotted_name(self, testdir): pluginmanager = PluginManager() - py.test.raises(ImportError, 'pluginmanager.import_plugin("x.y")') - py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_x.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') reset = testdir.syspathinsert() testdir.mkpydir("pkg").join("plug.py").write("x=3") --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -295,8 +295,6 @@ class HookProxy: def __init__(self, node): self.node = node def __getattr__(self, name): - if name[0] == "_": - raise AttributeError(name) hookmethod = getattr(self.node.config.hook, name) def call_matching_hooks(**kwargs): plugins = self.node.config._getmatchingplugins(self.node.fspath) From commits-noreply at bitbucket.org Mon Nov 1 08:15:17 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 02:15:17 -0500 (CDT) Subject: [py-svn] pytest commit ff5f9cf9ff57: some test fixes and refinements Message-ID: <20101101071517.3E58F6C130B@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288595770 -3600 # Node ID ff5f9cf9ff576554f02c8ecd1e4717a22dbe0728 # Parent 55b2bb6f2902b67d249b46ec6b35170acc6af7b6 some test fixes and refinements --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -2,16 +2,6 @@ import py, sys from pytest.plugin import python as funcargs class TestModule: - def test_module_file_not_found(self, testdir): - tmpdir = testdir.tmpdir - fn = tmpdir.join('nada','no') - config=testdir.Config() - config.args = ["hello"] - col = py.test.collect.Module(fn, config=config, - collection=testdir.Collection(config)) - col.config = testdir.parseconfig(tmpdir) - py.test.raises(py.error.ENOENT, col.collect) - def test_failing_import(self, testdir): modcol = testdir.getmodulecol("import alksdjalskdjalkjals") py.test.raises(ImportError, modcol.collect) --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -297,7 +297,7 @@ class Config(object): if self.inicfg: newargs = self.inicfg.get("addargs", None) if newargs: - args[:] = args + py.std.shlex.split(newargs) + args[:] = py.std.shlex.split(newargs) + args self._checkversion() self.pluginmanager.consider_setuptools_entrypoints() self.pluginmanager.consider_env() @@ -414,6 +414,7 @@ class Config(object): def getcfg(args, inibasenames): + args = [x for x in args if str(x)[0] != "-"] if not args: args = [py.path.local()] for inibasename in inibasenames: --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev14' +__version__ = '2.0.0.dev15' __all__ = ['config', 'cmdline'] --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -13,6 +13,11 @@ class TestGeneralUsage: '*ERROR: hello' ]) + def test_file_not_found(self, testdir): + result = testdir.runpytest("asd") + assert result.ret != 0 + result.stderr.fnmatch_lines(["ERROR: file not found*asd"]) + def test_config_preparse_plugin_option(self, testdir): testdir.makepyfile(pytest_xyz=""" def pytest_addoption(parser): --- a/testing/test_config.py +++ b/testing/test_config.py @@ -66,6 +66,9 @@ class TestConfigTmpdir: assert not config2.getbasetemp().relto(config3.getbasetemp()) assert not config3.getbasetemp().relto(config2.getbasetemp()) + def test_reparse_filename_too_long(self, testdir): + config = testdir.reparseconfig(["--basetemp=%s" % ("123"*300)]) + class TestConfigAPI: def test_config_getvalue_honours_conftest(self, testdir): --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev14', + version='2.0.0.dev15', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/tox.ini +++ b/tox.ini @@ -52,4 +52,4 @@ commands= [pytest] minversion=2.0 plugins=pytester -#addargs=-q -x +#addargs=-rf From commits-noreply at bitbucket.org Mon Nov 1 08:54:45 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 02:54:45 -0500 (CDT) Subject: [py-svn] pytest commit 37e6a9570c34: rename addargs to addopts, make adding of opts configurable Message-ID: <20101101075445.629051E1167@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288598114 -3600 # Node ID 37e6a9570c348624983020109d83287e0b37ddf7 # Parent ff5f9cf9ff576554f02c8ecd1e4717a22dbe0728 rename addargs to addopts, make adding of opts configurable --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -11,7 +11,7 @@ def pytest_cmdline_parse(pluginmanager, return config def pytest_addoption(parser): - parser.addini('addargs', 'default command line arguments') + parser.addini('addopts', 'default command line arguments') parser.addini('minversion', 'minimally required pytest version') class Parser: @@ -292,10 +292,11 @@ class Config(object): sys.stderr.write(err) raise - def _preparse(self, args): + def _preparse(self, args, addopts=True): + self.inicfg = {} self.inicfg = getcfg(args, ["setup.cfg", "tox.ini",]) - if self.inicfg: - newargs = self.inicfg.get("addargs", None) + if self.inicfg and addopts: + newargs = self.inicfg.get("addopts", None) if newargs: args[:] = py.std.shlex.split(newargs) + args self._checkversion() --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev15' +__version__ = '2.0.0.dev16' __all__ = ['config', 'cmdline'] --- a/doc/customize.txt +++ b/doc/customize.txt @@ -45,13 +45,13 @@ builtin configuration file options minversion = 2.1 # will fail if we run with pytest-2.0 -.. confval:: addargs = OPTS +.. confval:: addopts = OPTS add the specified ``OPTS`` to the set of command line arguments as if they had been specified by the user. Example: if you have this ini file content:: [pytest] - addargs = --maxfail=2 -rf # exit after 2 failures, report fail info + addopts = --maxfail=2 -rf # exit after 2 failures, report fail info issuing ``py.test test_hello.py`` actually means:: --- a/testing/test_config.py +++ b/testing/test_config.py @@ -19,11 +19,15 @@ class TestParseIni: def test_append_parse_args(self, tmpdir): tmpdir.join("setup.cfg").write(py.code.Source(""" [pytest] - addargs = --verbose + addopts = --verbose """)) config = Config() config.parse([tmpdir]) assert config.option.verbose + config = Config() + args = [tmpdir,] + config._preparse(args, addopts=False) + assert len(args) == 1 def test_tox_ini_wrong_version(self, testdir): p = testdir.makefile('.ini', tox=""" --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -475,8 +475,8 @@ def test_getreportopt(): config.option.reportchars = "sfx" assert getreportopt(config) == "sfx" -def test_terminalreporter_reportopt_addargs(testdir): - testdir.makeini("[pytest]\naddargs=-rs") +def test_terminalreporter_reportopt_addopts(testdir): + testdir.makeini("[pytest]\naddopts=-rs") p = testdir.makepyfile(""" def pytest_funcarg__tr(request): tr = request.config.pluginmanager.getplugin("terminalreporter") --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev15', + version='2.0.0.dev16', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From commits-noreply at bitbucket.org Mon Nov 1 08:55:51 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 02:55:51 -0500 (CDT) Subject: [py-svn] pytest-xdist commit 76db8f1c237c: don't add opts in sub processes, add -rf by default Message-ID: <20101101075551.A7BC26C1322@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1288598207 -3600 # Node ID 76db8f1c237c0e804b05bc974815854f9f18064d # Parent ba07fdbea9d953cf4f55db67e2e242527948019f don't add opts in sub processes, add -rf by default --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -82,7 +82,7 @@ class TestDistribution: "*1 passed*", ]) - def test_dist_conftest_specified(self, testdir): + def test_dist_ini_specified(self, testdir): p1 = testdir.makepyfile(""" import py def test_fail0(): @@ -95,8 +95,9 @@ class TestDistribution: py.test.skip("hello") """, ) - testdir.makeconftest(""" - option_tx = 'popen popen popen'.split() + testdir.makeini(""" + [pytest] + addopts = --tx=3*popen """) result = testdir.runpytest(p1, '-d', "-v") result.stdout.fnmatch_lines([ --- a/testing/test_plugin.py +++ b/testing/test_plugin.py @@ -49,14 +49,15 @@ class TestDistOptions: p = py.path.local() for bn in 'x y z'.split(): p.mkdir(bn) - testdir.makeconftest(""" - rsyncdirs= 'x', + testdir.makeini(""" + [pytest] + rsyncdirs= x """) config = testdir.parseconfigure( testdir.tmpdir, '--rsyncdir=y', '--rsyncdir=z') nm = NodeManager(config, specs=[execnet.XSpec("popen")]) roots = nm._getrsyncdirs() - assert len(roots) == 3 + 1 # pylib + #assert len(roots) == 3 + 1 # pylib assert py.path.local('y') in roots assert py.path.local('z') in roots assert testdir.tmpdir.join('x') in roots --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5a2' +__version__ = '1.5a4' --- a/tox.ini +++ b/tox.ini @@ -15,3 +15,6 @@ commands= py.test -rsfxX \ deps= pytest pypi pexpect + +[pytest] +addopts = -rf --- a/xdist/remote.py +++ b/xdist/remote.py @@ -104,7 +104,7 @@ def getinfodict(): def remote_initconfig(option_dict, args): from pytest.plugin.config import Config config = Config() - config._preparse(args) + config._preparse(args, addopts=False) config.option.__dict__.update(option_dict) config.option.looponfail = False config.option.usepdb = False --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.5a2', + version='1.5a4', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=__doc__, license='GPLv2 or later', From commits-noreply at bitbucket.org Mon Nov 1 09:20:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 03:20:02 -0500 (CDT) Subject: [py-svn] pytest commit 7d2db9cfe921: allow unregistration by name Message-ID: <20101101082002.6AE7A6C12A7@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288599658 -3600 # Node ID 7d2db9cfe9216159f9b41347e659fffca6095564 # Parent 37e6a9570c348624983020109d83287e0b37ddf7 allow unregistration by name --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev16', + version='2.0.0.dev17', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -164,7 +164,7 @@ class TestBootstrapping: assert pp.getplugin('hello') == a2 pp.unregister(a1) assert not pp.isregistered(a1) - pp.unregister(a2) + pp.unregister(name="hello") assert not pp.isregistered(a2) def test_pm_ordering(self): --- a/pytest/_core.py +++ b/pytest/_core.py @@ -47,9 +47,11 @@ class PluginManager(object): self._plugins.insert(0, plugin) return True - def unregister(self, plugin): + def unregister(self, plugin=None, name=None): + if plugin is None: + plugin = self.getplugin(name=name) + self._plugins.remove(plugin) self.hook.pytest_plugin_unregistered(plugin=plugin) - self._plugins.remove(plugin) for name, value in list(self._name2plugin.items()): if value == plugin: del self._name2plugin[name] --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev16' +__version__ = '2.0.0.dev17' __all__ = ['config', 'cmdline'] From commits-noreply at bitbucket.org Mon Nov 1 09:22:19 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 03:22:19 -0500 (CDT) Subject: [py-svn] pytest-xdist commit 724fe27731c7: unregister terminal plugin in subprocesses before it gets configured Message-ID: <20101101082219.84D9F6C12A7@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1288599769 -3600 # Node ID 724fe27731c76cf8a91472cf8cdacfb336365765 # Parent 76db8f1c237c0e804b05bc974815854f9f18064d unregister terminal plugin in subprocesses before it gets configured --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -62,6 +62,7 @@ def test_remoteinitconfig(testdir): config1 = testdir.parseconfig() config2 = remote_initconfig(config1.option.__dict__, config1.args) assert config2.option.__dict__ == config1.option.__dict__ + py.test.raises(KeyError, 'config2.pluginmanager.getplugin("terminal")') class TestReportSerialization: def test_itemreport_outcomes(self, testdir): --- a/xdist/remote.py +++ b/xdist/remote.py @@ -104,6 +104,7 @@ def getinfodict(): def remote_initconfig(option_dict, args): from pytest.plugin.config import Config config = Config() + config.pluginmanager.unregister(name="terminal") config._preparse(args, addopts=False) config.option.__dict__.update(option_dict) config.option.looponfail = False From commits-noreply at bitbucket.org Mon Nov 1 23:08:47 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 17:08:47 -0500 (CDT) Subject: [py-svn] pytest commit ac86d3a188b1: majorly changing the unittest compatibility code, calling TestCase(name)(result) Message-ID: <20101101220847.0E58C6C140C@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288649296 -3600 # Node ID ac86d3a188b1da11c3df0fe6054805e7cb625a83 # Parent 7d2db9cfe9216159f9b41347e659fffca6095564 majorly changing the unittest compatibility code, calling TestCase(name)(result) --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -383,7 +383,8 @@ class Function(FunctionMixin, pytest.col config=config, collection=collection) self._args = args if self._isyieldedfunction(): - assert not callspec, "yielded functions (deprecated) cannot have funcargs" + assert not callspec, ( + "yielded functions (deprecated) cannot have funcargs") else: if callspec is not None: self.funcargs = callspec.funcargs or {} --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev17' +__version__ = '2.0.0.dev18' __all__ = ['config', 'cmdline'] --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,8 @@ Changes between 1.3.4 and 2.0.0dev0 ---------------------------------------------- - pytest-2.0 is now its own package and depends on pylib-2.0 +- try harder to run unittest test suites in a more compatible manner + by deferring setup/teardown semantics to the unittest package. - introduce a new way to set config options via ini-style files, by default setup.cfg and tox.ini files are searched. The old ways (certain environment variables, dynamic conftest.py reading --- a/testing/plugin/test_unittest.py +++ b/testing/plugin/test_unittest.py @@ -81,3 +81,25 @@ def test_module_level_pytestmark(testdir """) reprec = testdir.inline_run(testpath, "-s") reprec.assertoutcome(skipped=1) + +def test_class_setup(testdir): + testpath = testdir.makepyfile(""" + import unittest + import py + class MyTestCase(unittest.TestCase): + x = 0 + @classmethod + def setUpClass(cls): + cls.x += 1 + def test_func1(self): + assert self.x == 1 + def test_func2(self): + assert self.x == 1 + @classmethod + def tearDownClass(cls): + cls.x -= 1 + def test_teareddown(): + assert MyTestCase.x == 0 + """) + reprec = testdir.inline_run(testpath) + reprec.assertoutcome(passed=3) --- a/pytest/plugin/unittest.py +++ b/pytest/plugin/unittest.py @@ -19,51 +19,31 @@ def pytest_pycollect_makeitem(collector, class UnitTestCase(py.test.collect.Class): def collect(self): - return [UnitTestCaseInstance("()", self)] + loader = py.std.unittest.TestLoader() + for name in loader.getTestCaseNames(self.obj): + yield TestCaseFunction(name, parent=self) def setup(self): - pass + meth = getattr(self.obj, 'setUpClass', None) + if meth is not None: + meth() def teardown(self): + meth = getattr(self.obj, 'tearDownClass', None) + if meth is not None: + meth() + +class TestCaseFunction(py.test.collect.Function): + def startTest(self, testcase): pass - -_dummy = object() -class UnitTestCaseInstance(py.test.collect.Instance): - def collect(self): - loader = py.std.unittest.TestLoader() - names = loader.getTestCaseNames(self.obj.__class__) - l = [] - for name in names: - callobj = getattr(self.obj, name) - if py.builtin.callable(callobj): - l.append(UnitTestFunction(name, parent=self)) - return l - - def _getobj(self): - x = self.parent.obj - return self.parent.obj(methodName='run') - -class UnitTestFunction(py.test.collect.Function): - def __init__(self, name, parent, args=(), obj=_dummy, sort_value=None): - super(UnitTestFunction, self).__init__(name, parent) - self._args = args - if obj is not _dummy: - self._obj = obj - self._sort_value = sort_value - if hasattr(self.parent, 'newinstance'): - self.parent.newinstance() - self.obj = self._getobj() - + def addError(self, testcase, rawexcinfo): + py.builtin._reraise(*rawexcinfo) + def addFailure(self, testcase, rawexcinfo): + py.builtin._reraise(*rawexcinfo) + def addSuccess(self, testcase): + pass + def stopTest(self, testcase): + pass def runtest(self): - target = self.obj - args = self._args - target(*args) - - def setup(self): - instance = py.builtin._getimself(self.obj) - instance.setUp() - - def teardown(self): - instance = py.builtin._getimself(self.obj) - instance.tearDown() - + testcase = self.parent.obj(self.name) + testcase(result=self) --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev17', + version='2.0.0.dev18', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From commits-noreply at bitbucket.org Tue Nov 2 00:53:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 1 Nov 2010 18:53:02 -0500 (CDT) Subject: [py-svn] pytest commit 9a0938131cfa: massive documentation refinements Message-ID: <20101101235302.6BFDA241235@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288655633 -3600 # Node ID 9a0938131cfa4fb581bfbe872bff1625dfbad4d2 # Parent ac86d3a188b1da11c3df0fe6054805e7cb625a83 massive documentation refinements --- a/doc/index.txt +++ b/doc/index.txt @@ -1,5 +1,8 @@ py.test: no-boilerplate testing with Python ============================================== + +.. todolist:: + Welcome to ``py.test`` documentation: @@ -8,7 +11,7 @@ Welcome to ``py.test`` documentation: overview apiref - customize + plugins examples talks develop --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -32,7 +32,7 @@ Running the test looks like this:: $ py.test test_simplefactory.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_simplefactory.py test_simplefactory.py F @@ -133,7 +133,7 @@ Running this:: $ py.test test_example.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_example.py test_example.py .........F @@ -154,7 +154,7 @@ Note that the ``pytest_generate_tests(me the test collection phase. You can have a look at it with this:: $ py.test --collectonly test_example.py - + @@ -171,14 +171,31 @@ If you want to select only the run with $ py.test -v -k 7 test_example.py # or -k test_func[7] =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 -- /home/hpk/venv/0/bin/python test path 1: test_example.py + test_example.py:6: test_func[0] PASSED + test_example.py:6: test_func[1] PASSED + test_example.py:6: test_func[2] PASSED + test_example.py:6: test_func[3] PASSED + test_example.py:6: test_func[4] PASSED + test_example.py:6: test_func[5] PASSED + test_example.py:6: test_func[6] PASSED test_example.py:6: test_func[7] PASSED + test_example.py:6: test_func[8] PASSED + test_example.py:6: test_func[9] FAILED - ======================== 9 tests deselected by '7' ========================= - ================== 1 passed, 9 deselected in 0.01 seconds ================== - + ================================= FAILURES ================================= + _______________________________ test_func[9] _______________________________ + + numiter = 9 + + def test_func(numiter): + > assert numiter < 9 + E assert 9 < 9 + + test_example.py:7: AssertionError + ==================== 1 failed, 9 passed in 0.04 seconds ==================== .. _`metafunc object`: --- a/doc/mark.txt +++ b/doc/mark.txt @@ -88,8 +88,8 @@ You can use the ``-k`` command line opti $ py.test -k webtest # running with the above defined examples yields =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 - test path 1: /tmp/doc-exec-11 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + test path 1: /tmp/doc-exec-171 test_mark.py .. test_mark_classlevel.py .. @@ -100,8 +100,8 @@ And you can also run all tests except th $ py.test -k-webtest =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 - test path 1: /tmp/doc-exec-11 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + test path 1: /tmp/doc-exec-171 ===================== 4 tests deselected by '-webtest' ===================== ======================= 4 deselected in 0.01 seconds ======================= @@ -110,8 +110,8 @@ Or to only select the class:: $ py.test -kTestClass =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 - test path 1: /tmp/doc-exec-11 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + test path 1: /tmp/doc-exec-171 test_mark_classlevel.py .. --- a/doc/unittest.txt +++ b/doc/unittest.txt @@ -1,11 +1,11 @@ -unittest.py style testing integration +unittest.TestCase support ===================================================================== py.test has limited support for running Python `unittest.py style`_ tests. It will automatically collect ``unittest.TestCase`` subclasses and their ``test`` methods in test files. It will invoke ``setUp/tearDown`` methods but also perform py.test's standard ways -of treating tests like IO capturing:: +of treating tests like e.g. IO capturing:: # content of test_unittest.py @@ -21,7 +21,7 @@ Running it yields:: $ py.test test_unittest.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_unittest.py test_unittest.py F @@ -55,7 +55,5 @@ Running it yields:: hello ========================= 1 failed in 0.02 seconds ========================= -This plugin is enabled by default. - .. _`unittest.py style`: http://docs.python.org/library/unittest.html --- a/doc/cmdline.txt +++ b/doc/cmdline.txt @@ -11,9 +11,7 @@ Getting help on version, option names, e py.test --version # shows where pytest was imported from py.test --funcargs # show available builtin function arguments - py.test --help-config # show configuration values - - py.test -h | --help # show help + py.test -h | --help # show help on command line and config file options Stopping after the first (or N) failures --- a/doc/tmpdir.txt +++ b/doc/tmpdir.txt @@ -26,7 +26,7 @@ Running this would result in a passed te $ py.test test_tmpdir.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_tmpdir.py test_tmpdir.py F @@ -34,7 +34,7 @@ Running this would result in a passed te ================================= FAILURES ================================= _____________________________ test_create_file _____________________________ - tmpdir = local('/tmp/pytest-427/test_create_file0') + tmpdir = local('/tmp/pytest-1248/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") @@ -45,7 +45,7 @@ Running this would result in a passed te E assert 0 test_tmpdir.py:7: AssertionError - ========================= 1 failed in 0.03 seconds ========================= + ========================= 1 failed in 0.04 seconds ========================= .. _`base temporary directory`: --- a/doc/Makefile +++ b/doc/Makefile @@ -14,6 +14,9 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctree .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest +regen: + COLUMNS=76 regendoc --update *.txt */*.txt + help: @echo "Please use \`make ' where is one of" @echo " html to make standalone HTML files" --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -247,7 +247,8 @@ class Config(object): basetemp = None def __init__(self, pluginmanager=None): - #: command line option values + #: command line option values, usually added via parser.addoption(...) + #: or parser.getgroup(...).addoption(...) calls self.option = CmdOptions() self._parser = Parser( usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]", @@ -404,7 +405,7 @@ class Config(object): return self._getconftest(name, path, check=False) def getvalueorskip(self, name, path=None): - """ return getvalue(name) or call py.test.skip if no value exists. """ + """ (deprecated) return getvalue(name) or call py.test.skip if no value exists. """ try: val = self.getvalue(name, path) if val is None: --- a/doc/goodpractises.txt +++ b/doc/goodpractises.txt @@ -2,51 +2,91 @@ .. highlightlang:: python .. _`good practises`: -Good Practises +Good Integration Practises ================================================= -Recommendation: install tool and dependencies virtually +work with virtual environments ----------------------------------------------------------- -We recommend to work with virtual environments -(e.g. virtualenv_ or buildout_ based) and use easy_install_ -(or pip_) for installing py.test/pylib and any dependencies -you need to run your tests. Local virtual Python environments -(as opposed to system-wide "global" environments) make for a more -reproducible and reliable test environment. +We recommend to work with virtualenv_ environments and use easy_install_ +(or pip_) for installing your application dependencies as well as +the ``pytest`` package itself. This way you get a much more reproducible +environment. A good tool to help you automate test runs against multiple +dependency configurations or Python interpreters is `tox`_, +independently created by the main py.test author. The latter +is also useful for integration with the continous integration +server Hudson_. .. _`virtualenv`: http://pypi.python.org/pypi/virtualenv .. _`buildout`: http://www.buildout.org/ .. _pip: http://pypi.python.org/pypi/pip + +Choosing a test layout / import rules +------------------------------------------ + +py.test supports common test layouts: + +* inlining test directories into your application package, useful if you want to + keep (unit) tests and actually tested code close together:: + + mypkg/ + __init__.py + appmodule.py + ... + test/ + test_app.py + ... + +* putting tests into an extra directory outside your actual application + code, useful if you have many functional tests or want to keep + tests separate from actual application code:: + + mypkg/ + __init__.py + appmodule.py + tests/ + test_app.py + ... + +You can always run your tests by pointing to it:: + + py.test tests/test_app.py # for external test dirs + py.test mypkg/test/test_app.py # for inlined test dirs + py.test mypkg # run tests in all below test directories + py.test # run all tests below current dir + ... + +.. note:: + + Test modules are imported under their fully qualified name as follows: + + * ``basedir`` = first upward directory not containing an ``__init__.py`` + + * perform ``sys.path.insert(0, basedir)``. + + * ``import path.to.test_module`` + .. _standalone: - - -Choosing a test layout ----------------------------- - -py.test supports common test layouts. - -XXX - .. _`genscript method`: Generating a py.test standalone Script ------------------------------------------- -If you are a maintainer or application developer and want users -to run tests you can use a facility to generate a standalone -"py.test" script that you can tell users to run:: +If you are a maintainer or application developer and want others +to easily run tests you can generate a completely standalone "py.test" +script:: py.test --genscript=runtests.py -will generate a ``mytest`` script that is, in fact, a ``py.test`` under -disguise. You can tell people to download and then e.g. run it like this:: +generates a ``runtests.py`` script which is a fully functional basic +``py.test`` script, running unchanged under Python2 and Python3. +You can tell people to download and then e.g. run it like this to +produce a Paste URL:: python runtests.py --pastebin=all -and ask them to send you the resulting URL. The resulting script has -all core features and runs unchanged under Python2 and Python3 interpreters. +and ask them to send you the resulting URL. .. _`Distribute for installation`: http://pypi.python.org/pypi/distribute#installation-instructions .. _`distribute installation`: http://pypi.python.org/pypi/distribute --- a/doc/doctest.txt +++ b/doc/doctest.txt @@ -44,11 +44,7 @@ then you can just invoke ``py.test`` wit $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 - test path 1: /tmp/doc-exec-288 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + test path 1: /tmp/doc-exec-197 - conftest.py . - example.rst . - mymodule.py . - - ========================= 3 passed in 0.01 seconds ========================= + ============================= in 0.00 seconds ============================= --- a/doc/example/mysetup.txt +++ b/doc/example/mysetup.txt @@ -49,7 +49,7 @@ You can now run the test:: $ py.test test_sample.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_sample.py test_sample.py F @@ -57,7 +57,7 @@ You can now run the test:: ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - mysetup = + mysetup = def test_answer(mysetup): app = mysetup.myapp() @@ -122,12 +122,12 @@ Running it yields:: $ py.test test_ssh.py -rs =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_ssh.py test_ssh.py s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-9/conftest.py:22: 'specify ssh host with --ssh' + SKIP [1] /tmp/doc-exec-198/conftest.py:22: 'specify ssh host with --ssh' ======================== 1 skipped in 0.02 seconds ========================= --- /dev/null +++ b/doc/plugins.txt @@ -0,0 +1,282 @@ +Writing, managing and understanding plugins +============================================= + +.. _`local plugin`: + +py.test implements all aspects of configuration, collection, running and reporting by calling `well specified hooks`_. Virtually any Python module can be registered as a plugin. It can implement any number of hook functions (usually two or three) which all have a ``pytest_`` prefix, making hook functions easy to distinguish and find. There are three basic locations types:: + +* builtin plugins: loaded from py.test's own `pytest/plugin`_ directory. +* `external plugins`_: modules discovered through `setuptools entry points`_ +* `conftest.py plugins`_: modules auto-discovered in test directories + +.. _`pytest/plugin`: http://bitbucket.org/hpk42/pytest/src/tip/pytest/plugin/ +.. _`conftest.py plugins`: + +conftest.py: local per-directory plugins +-------------------------------------------------------------- + +local ``conftest.py`` plugins contain directory-specific hook +implementations. Collection and test running activities will +invoke all hooks defined in "higher up" ``conftest.py`` files. +Example: Assume the following layout and content of files:: + + a/conftest.py: + def pytest_runtest_setup(item): + # called for running each test in 'a' directory + print ("setting up", item) + + a/test_in_subdir.py: + def test_sub(): + pass + + test_flat.py: + def test_flat(): + pass + +Here is how you might run it:: + + py.test test_flat.py # will not show "setting up" + py.test a/test_sub.py # will show "setting up" + +A note on ordering: ``py.test`` loads all ``conftest.py`` files upwards +from the command line file arguments. It usually performs look up +right-to-left, i.e. the hooks in "closer" conftest files will be called +earlier than further away ones. + +.. Note:: + If you have ``conftest.py`` files which do not reside in a + python package directory (i.e. one containing an ``__init__.py``) then + "import conftest" can be ambigous because there might be other + ``conftest.py`` files as well on your PYTHONPATH or ``sys.path``. + It is thus good practise for projects to either put ``conftest.py`` + under a package scope or to never import anything from a + conftest.py file. + +.. _`installing plugins`: +.. _`external plugins`: + +Installing External Plugins +------------------------------------------------------ + +Installing a plugin happens through any usual Python installation +tool, for example:: + + pip install pytest-NAME + pip uninstall pytest-NAME + +If a plugin is installed, py.test automatically finds and integrates it, +there is no need to activate it. If you don't need a plugin anymore simply +de-install it. You can find a list of valid plugins through a +`pytest- pypi.python.org search`_. + +.. _`available installable plugins`: +.. _`pytest- pypi.python.org search`: http://pypi.python.org/pypi?%3Aaction=search&term=pytest-&submit=search + +.. _`setuptools entry points`: + +Writing an installable plugin +------------------------------------------------------ + +.. _`Distribute`: http://pypi.python.org/pypi/distribute +.. _`setuptools`: http://pypi.python.org/pypi/setuptools + +If you want to write a plugin, there are many real-life examples +you can copy from: + +* around 20 `builtin plugins`_ which comprise py.test's own functionality +* around 10 `external plugins`_ providing additional features + +If you want to make your plugin externally available, you +may define a so called entry point for your distribution so +that ``py.test`` finds your plugin module. Entry points are +a feature that is provided by `setuptools`_ or `Distribute`_. +The concrete entry point is ``pytest11``. To make your plugin +available you can insert the following lines in your +setuptools/distribute-based setup-invocation: + +.. sourcecode:: python + + # sample ./setup.py file + from setuptools import setup + + setup( + name="myproject", + packages = ['myproject'] + + # the following makes a plugin available to py.test + entry_points = { + 'pytest11': [ + 'name_of_plugin = myproject.pluginmodule', + ] + }, + ) + +If a package is installed this way, py.test will load +``myproject.pluginmodule`` and accordingly call functions +if they match the `well specified hooks`_. + +Plugin discovery order at tool startup +-------------------------------------------- + +py.test loads plugin modules at tool startup in the following way: + +* by loading all builtin plugins + +* by loading all plugins registered through `setuptools entry points`_. + +* by pre-scanning the command line for the ``-p name`` option + and loading the specified plugin before actual command line parsing. + +* by loading all :file:`conftest.py` files as inferred by the command line + invocation (test files and all of its *parent* directories). + Note that ``conftest.py`` files from *sub* directories are by default + not loaded at tool startup. + +* by recursively loading all plugins specified by the + ``pytest_plugins`` variable in ``conftest.py`` files + +Requiring/Loading plugins in a test module or conftest file +------------------------------------------------------------- + +You can require plugins in a test module or a conftest file like this:: + + pytest_plugins = "name1", "name2", + +When the test module or conftest plugin is loaded the specified plugins +will be loaded as well. You can also use dotted path like this:: + + pytest_plugins = "myapp.testsupport.myplugin" + +which will import the specified module as a py.test plugin. + +.. _`setuptools entry points`: +.. _registered: + + +Accessing another plugin by name +-------------------------------------------- + +If a plugin wants to collaborate with code from +another plugin it can obtain a reference through +the plugin manager like this: + +.. sourcecode:: python + + plugin = config.pluginmanager.getplugin("name_of_plugin") + +If you want to look at the names of existing plugins, use +the ``--traceconfig`` option. + +.. _`well specified hooks`: + +py.test hook reference +==================================== + +hook specification and validation +----------------------------------------- + +py.test calls hook functions to implement initialization, running, +test execution and reporting. When py.test loads a plugin it validates +that all hook functions conform to their respective hook specification. +Each hook function name and its argument names need to match a hook +specification exactly but it is allowed for a hook function to accept +*less* parameters than specified. If you mistype argument names or the +hook name itself you get useful errors. + +initialisation, command line and configuration hooks +-------------------------------------------------------------------- + +.. currentmodule:: pytest.hookspec + +.. autofunction:: pytest_cmdline_parse +.. autofunction:: pytest_namespace +.. autofunction:: pytest_addoption +.. autofunction:: pytest_cmdline_main +.. autofunction:: pytest_configure +.. autofunction:: pytest_unconfigure + +generic "runtest" hooks +------------------------------ + +All all runtest related hooks receive a :py:class:`pytest.collect.Item` object. + +.. autofunction:: pytest_runtest_protocol +.. autofunction:: pytest_runtest_setup +.. autofunction:: pytest_runtest_call +.. autofunction:: pytest_runtest_teardown +.. autofunction:: pytest_runtest_makereport + +For deeper understanding you may look at the default implementation of +these hooks in :py:mod:`pytest.plugin.runner` and maybe also +in :py:mod:`pytest.plugin.pdb` which intercepts creation +of reports in order to drop to interactive debugging. + +The :py:mod:`pytest.plugin.terminal` reported specifically uses +the reporting hook to print information about a test run. + +collection hooks +------------------------------ + +py.test calls the following hooks for collecting files and directories: + +.. autofunction:: pytest_ignore_collect +.. autofunction:: pytest_collect_directory +.. autofunction:: pytest_collect_file + +For influencing the collection of objects in Python modules +you can use the following hook: + +.. autofunction:: pytest_pycollect_makeitem + + +reporting hooks +------------------------------ + +Collection related reporting hooks: + +.. 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 + + +Reference of important objects involved in hooks +=========================================================== + +.. autoclass:: pytest.plugin.config.Config + :members: + +.. autoclass:: pytest.plugin.config.Parser + :members: + +.. autoclass:: pytest.plugin.session.Node(name, parent) + :members: + +.. + .. autoclass:: pytest.plugin.session.File(fspath, parent) + :members: + + .. autoclass:: pytest.plugin.session.Item(name, parent) + :members: + + .. autoclass:: pytest.plugin.python.Module(name, parent) + :members: + + .. autoclass:: pytest.plugin.python.Class(name, parent) + :members: + + .. autoclass:: pytest.plugin.python.Function(name, parent) + :members: + +.. autoclass:: pytest.plugin.runner.CallInfo + :members: + +.. autoclass:: pytest.plugin.runner.TestReport + :members: + + --- a/doc/monkeypatch.txt +++ b/doc/monkeypatch.txt @@ -39,8 +39,8 @@ will be undone. .. background check: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 - test path 1: /tmp/doc-exec-296 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + test path 1: /tmp/doc-exec-172 ============================= in 0.00 seconds ============================= --- a/doc/faq.txt +++ b/doc/faq.txt @@ -1,87 +1,56 @@ -Frequent Issues and Questions +Some Issues and Questions ================================== -.. _`installation issues`: +.. note:: -Installation issues ------------------------------- - -easy_install or pip not found? -++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -Consult distribute_ to install the ``easy_install`` tool on your machine. -You may also use the original but somewhat older `setuptools`_ project -although we generally recommend to use ``distribute`` because it contains -more bug fixes and also works for Python3. - -For Python2 you can also consult pip_ for the popular ``pip`` tool. - -However, If you want to install on Python3 you need to use Distribute_ which -provides the ``easy_install`` utility. - - -py.test not found on Windows despite installation? -++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -.. _`Python for Windows`: http://www.imladris.com/Scripts/PythonForWindows.html - - -- **Windows**: If "easy_install" or "py.test" are not found - please see here for preparing your environment for running - command line tools: `Python for Windows`_. You may alternatively - use an `ActivePython install`_ which makes command line tools - automatically available under Windows. - -.. _`ActivePython install`: http://www.activestate.com/activepython/downloads - -.. _`Jython does not create command line launchers`: http://bugs.jython.org/issue1491 - -- **Jython2.5.1 on Windows XP**: `Jython does not create command line launchers`_ - so ``py.test`` will not work correctly. You may install py.test on - CPython and type ``py.test --genscript=mytest`` and then use - ``jython mytest`` to run py.test for your tests to run in Jython. + If you don't find an answer here, checkout the :ref:`contact channels` + to get help. On naming, nosetests, licensing and magic XXX ------------------------------------------------ -Why the ``py.test`` naming, why not ``pytest``? +Why a ``py.test`` instead of a ``pytest`` command? ++++++++++++++++++++++++++++++++++++++++++++++++++ -XXX - -because of TAB-completion under Bash/Shells. If you hit -``py.`` you'll get a list of available development -tools that all share the ``py.`` prefix. Another motivation -was to unify the package ("py.test") and tool filename. - +Some historic, some practical reasons: ``py.test`` used to be part of +the ``py`` package which provided several developer utitilities, +all starting with ``py.``, providing nice TAB-completion. If +you install ``pip install pycmd`` you get these tools from a separate +package. These days the command line tool could be ``pytest`` +but then many people have gotten used to the old name and there +also is another tool with this same which would lead to some clashes. What's py.test's relation to ``nosetests``? +++++++++++++++++++++++++++++++++++++++++++++++++ py.test and nose_ share basic philosophy when it comes -to running Python tests. In fact, -with py.test-1.1.0 it is ever easier to run many test suites -that currently work with ``nosetests``. nose_ was created +to running Python tests. In fact, you can run many tests +written for unittest or nose with py.test. nose_ was originally created as a clone of ``py.test`` when py.test was in the ``0.8`` release -cycle so some of the newer features_ introduced with py.test-1.0 -and py.test-1.1 have no counterpart in nose_. +cycle. .. _features: test/features.html -.. _apipkg: http://pypi.python.org/pypi/apipkg What's this "magic" with py.test? ++++++++++++++++++++++++++++++++++++++++++ -Around 2007 it was claimed that py.test was magic implementation -wise XXX. It has been refactored. +Around 2007 (version ``0.8``) some several people claimed that py.test +was using too much "magic". It has been refactored a lot. It is today +probably one of the smallest, most universally runnable and most +customizable testing frameworks for Python. It remains true +that ``py.test`` uses metaprogramming techniques, i.e. it views +test code similar to how compilers view programs, using a +somewhat abstract internal model. -* when an ``assert`` statement fails, py.test re-interprets the expression - to show intermediate values if a test fails. If your expression - has side effects the intermediate values may not be the same, obfuscating - the initial error (this is also explained at the command line if it happens). - ``py.test --no-assert`` turns off assert re-intepretation. - Sidenote: it is good practise to avoid asserts with side effects. +It's also true that the no-boilerplate testing is implemented by making +use of the Python assert statement through "re-interpretation": +When an ``assert`` statement fails, py.test re-interprets the expression +to show intermediate values if a test fails. If your expression +has side effects the intermediate values may not be the same, obfuscating +the initial error (this is also explained at the command line if it happens). +``py.test --no-assert`` turns off assert re-intepretation. +Sidenote: it is good practise to avoid asserts with side effects. .. _`py namespaces`: index.html .. _`py/__init__.py`: http://bitbucket.org/hpk42/py-trunk/src/trunk/py/__init__.py @@ -92,10 +61,9 @@ function arguments, parametrized tests a .. _funcargs: test/funcargs.html -Is using funcarg- versus xUnit-based setup a style question? +Is using funcarg- versus xUnit setup a style question? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -XXX For simple applications and for people experienced with nose_ or unittest-style test setup using `xUnit style setup`_ feels natural. For larger test suites, parametrized testing --- a/distribute_setup.py +++ b/distribute_setup.py @@ -46,7 +46,7 @@ except ImportError: args = [quote(arg) for arg in args] return os.spawnl(os.P_WAIT, sys.executable, *args) == 0 -DEFAULT_VERSION = "0.6.13" +DEFAULT_VERSION = "0.6.14" DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/" SETUPTOOLS_FAKED_VERSION = "0.6c11" --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -1,4 +1,4 @@ -Getting Started +Installation and Getting Started =================================== .. _`easy_install`: @@ -20,7 +20,7 @@ To check your installation has installed If you get an error, checkout :ref:`installation issues`. -Writing a simple test function with an assertion +Our first test run ---------------------------------------------------------- Let's create a small file with a test function testing a function @@ -32,17 +32,17 @@ computes a certain value:: def test_answer(): assert func(3) == 5 -Now you can execute the test function:: +You can execute the test function:: $ py.test test_sample.py - ========================= test session starts ========================== - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev4 + =========================== test session starts ============================ + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_sample.py test_sample.py F - =============================== FAILURES =============================== - _____________________________ test_answer ______________________________ + ================================= FAILURES ================================= + _______________________________ test_answer ________________________________ def test_answer(): > assert func(3) == 5 @@ -50,23 +50,27 @@ Now you can execute the test function:: E + where 4 = func(3) test_sample.py:4: AssertionError - ======================= 1 failed in 0.02 seconds ======================= + ========================= 1 failed in 0.02 seconds ========================= -We got a failure because our little ``func(3)`` call did not return ``5``. -A few notes on this little test invocation: +We told py.test to run the ``test_sample.py`` file and it :ref:`discovered` the +``test_answer`` function because of the ``test_`` prefix. We got a +failure because our little ``func(3)`` call did not return ``5``. -* ``test_answer`` was identified as a test function because of the - ``test_`` prefix, +.. note:: -* we conveniently used the standard `assert statement`_ and the failure - report shows us the intermediate values. + You can simply use the `assert statement`_ for coding expectations because + intermediate values will be presented to you. Or to put it bluntly, + there is no need to learn all `the JUnit legacy methods`_ for expressing + assertions. + +.. _`the JUnit legacy methods`: http://docs.python.org/library/unittest.html#test-cases .. _`assert statement`: http://docs.python.org/reference/simple_stmts.html#the-assert-statement -Asserting that a certain exception is raised +Asserting a certain exception is raised -------------------------------------------------------------- -If you want to assert a test raises a certain exception you can +If you want to assert some code raises an exception you can use the ``raises`` helper:: # content of test_sysexit.py @@ -78,18 +82,49 @@ use the ``raises`` helper:: with py.test.raises(SystemExit): f() -Running it with:: +Running it with, this time in "quiet" reporting mode:: - $ py.test test_sysexit.py - ========================= test session starts ========================== - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev4 - test path 1: test_sysexit.py + $ py.test -q test_sysexit.py + . + 1 passed in 0.01 seconds + +.. todo:: For further ways to assert exceptions see the :pyfunc:`raises` + +Grouping multiple tests in a class +-------------------------------------------------------------- + +If you start to have more than a few tests it often makes sense +to group tests logically, in classes and modules. Let's put two +tests in a class like this:: + + # content of test_class.py + class TestClass: + def test_one(self): + x = "this" + assert 'h' in x + + def test_two(self): + x = "hello" + assert hasattr(x, 'check') + +The two tests will be discovered because of the default `automatic test +discovery`_. There is no need to subclass anything. If we now run +the module we'll see one passed and one failed test:: + + $ py.test -q test_class.py + .F + ================================= FAILURES ================================= + ____________________________ TestClass.test_two ____________________________ - test_sysexit.py . + self = - ======================= 1 passed in 0.01 seconds ======================= - -.. For further ways to assert exceptions see the :pyfunc:`raises` + def test_two(self): + x = "hello" + > assert hasattr(x, 'check') + E assert hasattr('hello', 'check') + + test_class.py:8: AssertionError + 1 failed, 1 passed in 0.02 seconds where to go from here ------------------------------------- @@ -99,6 +134,47 @@ Here are a few suggestions where to go n * :ref:`cmdline` for command line invocation examples * :ref:`good practises` for virtualenv, test layout, genscript support * :ref:`apiref` for documentation and examples on writing Python tests -* :ref:`examples` for more complex examples + +.. _`installation issues`: + +Installation issues +------------------------------ + +easy_install or pip not found? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Consult distribute_ to install the ``easy_install`` tool on your machine. +You may also use the original but somewhat older `setuptools`_ project +although we generally recommend to use ``distribute`` because it contains +more bug fixes and also works for Python3. + +For Python2 you can also consult pip_ for the popular ``pip`` tool. + +However, If you want to install on Python3 you need to use Distribute_ which +provides the ``easy_install`` utility. + + +py.test not found on Windows despite installation? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +.. _`Python for Windows`: http://www.imladris.com/Scripts/PythonForWindows.html + + +- **Windows**: If "easy_install" or "py.test" are not found + please see here for preparing your environment for running + command line tools: `Python for Windows`_. You may alternatively + use an `ActivePython install`_ which makes command line tools + automatically available under Windows. + +.. _`ActivePython install`: http://www.activestate.com/activepython/downloads + +.. _`Jython does not create command line launchers`: http://bugs.jython.org/issue1491 + +- **Jython2.5.1 on Windows XP**: `Jython does not create command line launchers`_ + so ``py.test`` will not work correctly. You may install py.test on + CPython and type ``py.test --genscript=mytest`` and then use + ``jython mytest`` to run py.test for your tests to run in Jython. + + :ref:`examples` for more complex examples .. include:: links.inc --- a/doc/customize.txt +++ b/doc/customize.txt @@ -1,7 +1,3 @@ -================================================ -Customizing and Extending py.test -================================================ - basic test configuration =================================== @@ -58,309 +54,4 @@ builtin configuration file options py.test --maxfail=2 -rf test_hello.py .. _`function arguments`: funcargs.html -.. _`extensions`: -Plugin basics and project configuration -============================================= - -.. _`local plugin`: - -py.test implements all aspects of its functionality by calling `well specified -hooks`_. Hook functions are discovered in :file:`conftest.py` files or in -`named plugins`_. :file:`conftest.py` files are useful for keeping test -extensions and customizations close to test code. - -local conftest.py plugins --------------------------------------------------------------- - -local ``conftest.py`` plugins contain directory-specific hook implemenations. Its contained runtest- and collection- related hooks are called when collecting or running tests in files or directories next to or below the ``conftest.py`` -file. Example: Assume the following layout and content of files:: - - a/conftest.py: - def pytest_runtest_setup(item): - print ("setting up", item) - - a/test_in_subdir.py: - def test_sub(): - pass - - test_flat.py: - def test_flat(): - pass - -Here is how you might run it:: - - py.test test_flat.py # will not show "setting up" - py.test a/test_sub.py # will show "setting up" - -``py.test`` loads all ``conftest.py`` files upwards from the command -line file arguments. It usually performs look up right-to-left, i.e. -the hooks in "closer" conftest files will be called earlier than further -away ones. This means you can even have a ``conftest.py`` file in your home -directory to customize test functionality globally for all of your projects. - -.. Note:: - If you have ``conftest.py`` files which do not reside in a - python package directory (i.e. one containing an ``__init__.py``) then - "import conftest" can be ambigous because there might be other - ``conftest.py`` files as well on your PYTHONPATH or ``sys.path``. - It is good practise for projects to put ``conftest.py`` within a package - scope or to never import anything from the conftest.py file. - -.. _`named plugins`: plugin/index.html - - -Plugin discovery at tool startup --------------------------------------------- - -py.test loads plugin modules at tool startup in the following way: - -* by loading all plugins registered through `setuptools entry points`_. - -* by pre-scanning the command line for the ``-p name`` option - and loading the specified plugin before actual command line parsing. - -* by loading all :file:`conftest.py` files as inferred by the command line - invocation (test files and all of its *parent* directories). - Note that ``conftest.py`` files from *sub* directories are by default - not loaded at tool startup. - -* by recursively loading all plugins specified by the - ``pytest_plugins`` variable in ``conftest.py`` files - -Requiring/Loading plugins in a test module or conftest file -------------------------------------------------------------- - -You can require plugins in a test module or a conftest file like this:: - - pytest_plugins = "name1", "name2", - -When the test module or conftest plugin is loaded the specified plugins -will be loaded as well. You can also use dotted path like this:: - - pytest_plugins = "myapp.testsupport.myplugin" - -which will import the specified module as a py.test plugin. - -.. _`setuptools entry points`: -.. _registered: - -Writing setuptools-registered plugins ------------------------------------------------------- - -.. _`Distribute`: http://pypi.python.org/pypi/distribute -.. _`setuptools`: http://pypi.python.org/pypi/setuptools - -If you want to make your plugin publically available, you -can use `setuptools`_ or `Distribute`_ which both allow -to register an entry point. ``py.test`` will register -all objects with the ``pytest11`` entry point. -To make your plugin available you may insert the following -lines in your setuptools/distribute-based setup-invocation: - -.. sourcecode:: python - - # sample ./setup.py file - from setuptools import setup - - setup( - name="myproject", - packages = ['myproject'] - - # the following makes a plugin available to py.test - entry_points = { - 'pytest11': [ - 'name_of_plugin = myproject.pluginmodule', - ] - }, - ) - -If a package is installed with this setup, py.test will load -``myproject.pluginmodule`` under the ``name_of_plugin`` name -and use it as a plugin. - -Accessing another plugin by name --------------------------------------------- - -If a plugin wants to collaborate with code from -another plugin it can obtain a reference through -the plugin manager like this: - -.. sourcecode:: python - - plugin = config.pluginmanager.getplugin("name_of_plugin") - -If you want to look at the names of existing plugins, use -the ``--traceconfig`` option. - -.. _`well specified hooks`: - -py.test hook reference -==================================== - -hook specification and validation ------------------------------------------ - -py.test calls hook functions to implement initialization, running, -test execution and reporting. When py.test loads a plugin it validates -that all hook functions conform to their respective hook specification. -Each hook function name and its argument names need to match a hook -specification exactly but it is allowed for a hook function to accept -*less* parameters than specified. If you mistype argument names or the -hook name itself you get useful errors. - -initialisation, command line and configuration hooks --------------------------------------------------------------------- - -.. currentmodule:: pytest.hookspec - -.. autofunction:: pytest_cmdline_parse -.. autofunction:: pytest_namespace -.. autofunction:: pytest_addoption -.. autofunction:: pytest_cmdline_main -.. autofunction:: pytest_configure -.. autofunction:: pytest_unconfigure - -generic "runtest" hooks ------------------------------- - -All all runtest related hooks receive a :py:class:`pytest.collect.Item` object. - -.. autofunction:: pytest_runtest_protocol -.. autofunction:: pytest_runtest_setup -.. autofunction:: pytest_runtest_call -.. autofunction:: pytest_runtest_teardown -.. autofunction:: pytest_runtest_makereport - -For deeper understanding you may look at the default implementation of -these hooks in :py:mod:`pytest.plugin.runner` and maybe also -in :py:mod:`pytest.plugin.pdb` which intercepts creation -of reports in order to drop to interactive debugging. - -The :py:mod:`pytest.plugin.terminal` reported specifically uses -the reporting hook to print information about a test run. - -collection hooks ------------------------------- - -py.test calls the following hooks for collecting files and directories: - -.. autofunction:: pytest_ignore_collect -.. autofunction:: pytest_collect_directory -.. autofunction:: pytest_collect_file - -For influencing the collection of objects in Python modules -you can use the following hook: - -.. autofunction:: pytest_pycollect_makeitem - - -reporting hooks ------------------------------- - -Collection related reporting hooks: - -.. 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 - -The test collection tree -====================================================== - - -Default filesystem test discovery ------------------------------------------------ - -Test collection starts from specified paths or from the current -directory. All tests are collected ahead of running the first test. -(This used to be different in earlier versions of ``py.test`` where -collection and running was interweaved which made test randomization -and distributed testing harder). - -Collection nodes which have children are called "Collectors" and otherwise -they are called "Items" or "test items". Here is an example of such a -tree:: - - example $ py.test --collectonly test_collectonly.py - - - - - - - - -By default all directories not starting with a dot are traversed, -looking for ``test_*.py`` and ``*_test.py`` files. Those Python -files are imported under their `package name`_. - -The Module collector looks for test functions -and test classes and methods. Test functions and methods -are prefixed ``test`` by default. Test classes must -start with a capitalized ``Test`` prefix. - -Customizing error messages -------------------------------------------------- - -On test and collection nodes ``py.test`` will invoke -the ``node.repr_failure(excinfo)`` function which -you may override and make it return an error -representation string of your choice. It -will be reported as a (red) string. - -.. _`package name`: - -constructing the package name for test modules -------------------------------------------------- - -Test modules are imported under their fully qualified -name. Given a filesystem ``fspath`` it is constructed as follows: - -* walk the directories up to the last one that contains - an ``__init__.py`` file. - -* perform ``sys.path.insert(0, basedir)``. - -* import the root package as ``root`` - -Reference of important objects involved in hooks -=========================================================== - -.. autoclass:: pytest.plugin.config.Config - :members: - -.. autoclass:: pytest.plugin.config.Parser - :members: - -.. autoclass:: pytest.plugin.session.Node(name, parent) - :members: - -.. - .. autoclass:: pytest.plugin.session.File(fspath, parent) - :members: - - .. autoclass:: pytest.plugin.session.Item(name, parent) - :members: - - .. autoclass:: pytest.plugin.python.Module(name, parent) - :members: - - .. autoclass:: pytest.plugin.python.Class(name, parent) - :members: - - .. autoclass:: pytest.plugin.python.Function(name, parent) - :members: - -.. autoclass:: pytest.plugin.runner.CallInfo - :members: - -.. autoclass:: pytest.plugin.runner.TestReport - :members: - - --- a/doc/apiref.txt +++ b/doc/apiref.txt @@ -7,6 +7,7 @@ py.test reference documentation .. toctree:: :maxdepth: 2 + customize.txt assert.txt funcargs.txt xunit_setup.txt @@ -15,7 +16,7 @@ py.test reference documentation tmpdir.txt skipping.txt mark.txt - recwarn.txt + recwarn.txt + unittest.txt doctest.txt - unittest.txt --- a/doc/contact.txt +++ b/doc/contact.txt @@ -1,23 +1,25 @@ -Contact and Communication points + +.. _`contact channels`: + +Contact channels =================================== -- `py-dev developers list`_ announcements and discussions. +- `new issue tracker`_ to report bugs or suggest features. + See also the `old issue tracker`_ but don't submit bugs there. + +- `Testing In Python`_: a mailing list for Python testing tools and discussion. + +- `py-dev developers list`_ pytest specific announcements and discussions. - #pylib on irc.freenode.net IRC channel for random questions. - - `tetamap`_: Holger Krekel's blog, often about testing and py.test related news. - -- `Testing In Python`_: a mailing list for testing tools and discussion. - -- `commit mailing list`_ or `@pylibcommit`_ to follow development commits, - -- `bitbucket issue tracker`_ use this bitbucket issue tracker to report - bugs or request features. +- `commit mailing list`_ - `merlinux.eu`_ offers on-site teaching and consulting services. -.. _`bitbucket issue tracker`: http://bitbucket.org/hpk42/py-trunk/issues/ +.. _`new issue tracker`: http://bitbucket.org/hpk42/pytest/issues/ +.. _`old issue tracker`: http://bitbucket.org/hpk42/py-trunk/issues/ .. _`merlinux.eu`: http://merlinux.eu @@ -28,20 +30,8 @@ Contact and Communication points .. _`@pylibcommit`: http://twitter.com/pylibcommit -.. - get an account on codespeak - --------------------------- - - codespeak_ is where the subversion repository is hosted. If you know - someone who is active on codespeak already or you are otherwise known in - the community (see also: FOAF_) you will get access. But even if - you are new to the python developer community please come to the IRC - or the mailing list and ask questions, get involved. - .. _`Testing in Python`: http://lists.idyll.org/listinfo/testing-in-python .. _FOAF: http://en.wikipedia.org/wiki/FOAF -.. _us: http://codespeak.net/mailman/listinfo/py-dev -.. _codespeak: http://codespeak.net/ .. _`py-dev`: .. _`development mailing list`: .. _`py-dev developers list`: http://codespeak.net/mailman/listinfo/py-dev --- a/doc/xunit_setup.txt +++ b/doc/xunit_setup.txt @@ -54,13 +54,13 @@ Similarly, the following methods are cal def setup_method(self, method): """ setup up any state tied to the execution of the given - method in a class. setup_method is invoked for every - test method of a class. + method in a class. setup_method is invoked for every + test method of a class. """ def teardown_method(self, method): """ teardown any state that was previously setup - with a setup_method call. + with a setup_method call. """ If you rather define test functions directly at module level @@ -68,12 +68,13 @@ you can also use the following functions def setup_function(function): """ setup up any state tied to the execution of the given - function. Invoked for every test function in the module. + function. Invoked for every test function in the module. """ def teardown_method(function): """ teardown any state that was previously setup - with a setup_function call. + with a setup_function call. + """ Note that it possible that setup/teardown pairs are invoked multiple times per testing process. --- a/doc/links.inc +++ b/doc/links.inc @@ -16,3 +16,4 @@ .. _`pip`: http://pypi.python.org/pypi/pip .. _`virtualenv`: http://pypi.python.org/pypi/virtualenv .. _hudson: http://hudson-ci.org/ +.. _tox: http://codespeak.net/tox --- a/doc/conf.py +++ b/doc/conf.py @@ -25,7 +25,8 @@ import sys, os # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', + 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. templates_path = ['_templates'] --- a/doc/example/controlskip.txt +++ b/doc/example/controlskip.txt @@ -36,12 +36,12 @@ and when running it will see a skipped " $ py.test test_module.py -rs # "-rs" means report on the little 's' =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_module.py test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-12/conftest.py:9: 'need --runslow option to run' + SKIP [1] /tmp/doc-exec-195/conftest.py:9: 'need --runslow option to run' =================== 1 passed, 1 skipped in 0.02 seconds ==================== @@ -49,7 +49,7 @@ Or run it including the ``slow`` marked $ py.test test_module.py --runslow =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_module.py test_module.py .. --- a/doc/features.txt +++ b/doc/features.txt @@ -4,9 +4,11 @@ py.test Features no-boilerplate testing with Python ---------------------------------- -- automatic customizable Python test discovery +- automatic, fully customizable Python test discovery +- :pep:`8` consistent testing style +- allows simple test functions +- ``assert`` statement for your assertions - powerful parametrization of test functions -- use the ``assert`` statement for your assertions - rely on powerful traceback and assertion reporting - use ``print`` or ``pdb`` debugging on failures --- a/doc/assert.txt +++ b/doc/assert.txt @@ -1,5 +1,5 @@ -Writing easy assertions in tests +Writing and reporting of assertions in tests ============================================ assert with the ``assert`` statement @@ -21,7 +21,7 @@ assertion fails you will see the value o $ py.test test_assert1.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_assert1.py test_assert1.py F @@ -101,7 +101,7 @@ if you run this module:: $ py.test test_assert2.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0dev0 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_assert2.py test_assert2.py F --- a/doc/example/nonpython.txt +++ b/doc/example/nonpython.txt @@ -25,8 +25,8 @@ now execute the test specification:: nonpython $ py.test test_simple.yml =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev10 - test path 1: /home/hpk/p/pytest/doc/example/nonpython + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + test path 1: test_simple.yml test_simple.yml .F @@ -35,7 +35,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.06 seconds ==================== + ==================== 1 failed, 1 passed in 0.37 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 @@ -45,7 +45,7 @@ reporting in ``verbose`` mode:: nonpython $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev10 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 -- /home/hpk/venv/0/bin/python test path 1: /home/hpk/p/pytest/doc/example/nonpython test_simple.yml:1: usecase: ok PASSED @@ -56,7 +56,7 @@ reporting in ``verbose`` mode:: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.06 seconds ==================== + ==================== 1 failed, 1 passed in 0.07 seconds ==================== While developing your custom test collection and execution it's also interesting to just look at the collection tree:: --- /dev/null +++ b/doc/discovery.txt @@ -0,0 +1,45 @@ + +Test collection and discovery +====================================================== + +.. _`discovered`: + +Default filesystem test discovery +----------------------------------------------- + +Test collection starts from paths specified at the command line or from +the current directory. Tests are collected ahead of running the first test. +(This used to be different in earlier versions of ``py.test`` where +collection and running was interweaved which made test randomization +and distributed testing harder). + +Collection nodes which have children are called "Collectors" and otherwise +they are called "Items" or "test items". Here is an example of such a +tree:: + + example $ py.test --collectonly test_collectonly.py + + + + + + + + +By default all directories not starting with a dot are traversed, +looking for ``test_*.py`` and ``*_test.py`` files. Those Python +files are imported under their `package name`_. + +The Module collector looks for test functions +and test classes and methods. Test functions and methods +are prefixed ``test`` by default. Test classes must +start with a capitalized ``Test`` prefix. + +Customizing error messages +------------------------------------------------- + +On test and collection nodes ``py.test`` will invoke +the ``node.repr_failure(excinfo)`` function which +you may override and make it return an error +representation string of your choice. It +will be reported as a (red) string. From commits-noreply at bitbucket.org Thu Nov 4 14:08:31 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 08:08:31 -0500 (CDT) Subject: [py-svn] apipkg commit 4380c721021d: adapt test_module_alias_import. Message-ID: <20101104130831.3EEDA6C1424@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User Ralf Schmitt # Date 1288766672 -3600 # Node ID 4380c721021dc1dc436fb40d5e681697b66b7b95 # Parent afac3bb76444fff702c7ee2ba06085c09f6749dd adapt test_module_alias_import. --- a/test_apipkg.py +++ b/test_apipkg.py @@ -134,7 +134,8 @@ class TestScenarios: """)) monkeypatch.syspath_prepend(tmpdir) import aliasimport - assert aliasimport.some is py.std.os.path + for k, v in py.std.os.path.__dict__.items(): + assert getattr(aliasimport.some, k) == v def test_from_module_alias_import(self, monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("fromaliasimport") From commits-noreply at bitbucket.org Thu Nov 4 14:08:31 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 08:08:31 -0500 (CDT) Subject: [py-svn] apipkg commit afac3bb76444: make importing from an alias module work. Message-ID: <20101104130831.337596C141C@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User Ralf Schmitt # Date 1288766668 -3600 # Node ID afac3bb76444fff702c7ee2ba06085c09f6749dd # Parent 1ccd1b2e2232569d72695e44295410a29c8e89bd make importing from an alias module work. --- a/apipkg.py +++ b/apipkg.py @@ -65,10 +65,17 @@ class ApiModule(ModuleType): attrname = parts and parts[0] or "" if modpath[0] == '.': modpath = implprefix + modpath - if name == '__doc__': - self.__doc__ = importobj(modpath, attrname) + + if not attrname: + subname = '%s.%s'%(self.__name__, name) + apimod = AliasModule(subname, modpath) + sys.modules[subname] = apimod + setattr(self, name, apimod) else: - self.__map__[name] = (modpath, attrname) + if name == '__doc__': + self.__doc__ = importobj(modpath, attrname) + else: + self.__map__[name] = (modpath, attrname) def __repr__(self): l = [] @@ -118,3 +125,35 @@ class ApiModule(ModuleType): pass return dict __dict__ = property(__dict__) + +class AliasModule(ModuleType): + def __init__(self, name, modpath): + self.__name__ = name + self.__modpath = modpath + + def __repr__(self): + l = [] + if hasattr(self, '__version__'): + l.append("version=" + repr(self.__version__)) + if hasattr(self, '__file__'): + l.append('from ' + repr(self.__file__)) + if l: + return '' % (self.__name__, " ".join(l)) + return '' % (self.__name__,) + + def __getattr__(self, name): + mod = importobj(self.__modpath, None) + result = getattr(mod, name) + setattr(self, name, result) + for k, v in mod.__dict__.items(): + setattr(self, k, v) + return result + + def __dict__(self): + # force all the content of the module to be loaded when __dict__ is read + dictdescr = ModuleType.__dict__['__dict__'] + dict = dictdescr.__get__(self) + if dict is not None: + hasattr(self, 'some') + return dict + __dict__ = property(__dict__) From commits-noreply at bitbucket.org Thu Nov 4 14:08:31 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 08:08:31 -0500 (CDT) Subject: [py-svn] apipkg commit ad8da71fea6d: bump version, add changelog Message-ID: <20101104130831.1CE0A6C111B@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User holger krekel # Date 1288876187 -3600 # Node ID ad8da71fea6dedc0757be6d7fedf7aa2e59362ce # Parent 4380c721021dc1dc436fb40d5e681697b66b7b95 bump version, add changelog --- a/apipkg.py +++ b/apipkg.py @@ -9,7 +9,7 @@ import os import sys from types import ModuleType -__version__ = "1.1" +__version__ = "1.2.dev1" def initpkg(pkgname, exportdefs, attr=dict()): """ initialize given package from the export definitions. """ --- a/CHANGELOG +++ b/CHANGELOG @@ -1,3 +1,8 @@ +1.2 +---------------------------------------- + +- Allow to import from Aliasmodules (thanks Ralf Schmitt) + 1.1 ---------------------------------------- From commits-noreply at bitbucket.org Thu Nov 4 14:08:31 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 08:08:31 -0500 (CDT) Subject: [py-svn] apipkg commit 1ccd1b2e2232: test that importing from an alias module works. Message-ID: <20101104130831.289476C1414@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User Ralf Schmitt # Date 1288766662 -3600 # Node ID 1ccd1b2e2232569d72695e44295410a29c8e89bd # Parent 5769180a95146f3b4a41d53331f88a5955b60862 test that importing from an alias module works. --- a/test_apipkg.py +++ b/test_apipkg.py @@ -136,6 +136,18 @@ class TestScenarios: import aliasimport assert aliasimport.some is py.std.os.path + def test_from_module_alias_import(self, monkeypatch, tmpdir): + pkgdir = tmpdir.mkdir("fromaliasimport") + pkgdir.join('__init__.py').write(py.code.Source(""" + import apipkg + apipkg.initpkg(__name__, exportdefs={ + 'some': 'os.path', + }) + """)) + monkeypatch.syspath_prepend(tmpdir) + from fromaliasimport.some import join + assert join is py.std.os.path.join + def xtest_nested_absolute_imports(): import email api_email = apipkg.ApiModule('email',{ From commits-noreply at bitbucket.org Thu Nov 4 16:02:15 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 10:02:15 -0500 (CDT) Subject: [py-svn] pylib commit 45b898021117: introduce a dirname attribute to path objects Message-ID: <20101104150215.6D1851E0FBF@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1288728276 -3600 # Node ID 45b89802111778fd14cfb57646dd17a54da282e4 # Parent 95f405382cc8501c1099c339345c5c680e96c22e introduce a dirname attribute to path objects --- a/py/_path/common.py +++ b/py/_path/common.py @@ -91,6 +91,11 @@ class PathBase(object): return self._getbyspec('basename')[0] basename = property(basename, None, None, basename.__doc__) + def dirname(self): + """ dirname part of path. """ + return self._getbyspec('dirname')[0] + dirname = property(dirname, None, None, dirname.__doc__) + def purebasename(self): """ pure base name of the path.""" return self._getbyspec('purebasename')[0] --- a/testing/path/common.py +++ b/testing/path/common.py @@ -68,6 +68,10 @@ class CommonFSTests(object): assert newpath.check(basename='sampledir') assert newpath.basename, 'sampledir' + def test_dirname(self, path1): + newpath = path1.join('sampledir') + assert newpath.dirname == str(path1) + def test_dirpath(self, path1): newpath = path1.join('sampledir') assert newpath.dirpath() == path1 --- a/CHANGELOG +++ b/CHANGELOG @@ -10,6 +10,7 @@ Changes between 1.3.4 and 2.0.0dev0 - use apipkg-1.1 and make py.apipkg.initpkg|ApiModule available - add py.iniconfig module for brain-dead easy ini-config file parsing - introduce py.builtin.any() +- path objects have a .dirname attribute now Changes between 1.3.3 and 1.3.4 ================================================== From commits-noreply at bitbucket.org Thu Nov 4 16:02:15 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 10:02:15 -0500 (CDT) Subject: [py-svn] pylib commit e8cc8ddb7505: add breadthfirst and sorted options to visit(), refactor to a Visit class Message-ID: <20101104150215.7EC821E0FC0@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1288882994 -3600 # Node ID e8cc8ddb7505e5b4a2928c09990d9974a4508c77 # Parent 45b89802111778fd14cfb57646dd17a54da282e4 add breadthfirst and sorted options to visit(), refactor to a Visit class to avoid passing around tons of arguments. --- a/py/_path/common.py +++ b/py/_path/common.py @@ -268,7 +268,7 @@ newline will be removed from the end of except AttributeError: return str(self) < str(other) - def visit(self, fil=None, rec=None, ignore=NeverRaised): + def visit(self, fil=None, rec=None, ignore=NeverRaised, bf=False, sort=False): """ yields all paths below the current one fil is a filter (glob pattern or callable), if not matching the @@ -280,26 +280,14 @@ newline will be removed from the end of ignore is an Exception class that is ignoredwhen calling dirlist() on any of the paths (by default, all exceptions are reported) + + bf if True will cause a breadthfirst search instead of the + default depthfirst. Default: False + + sort if True will sort entries within each directory level. """ - if isinstance(fil, str): - fil = FNMatcher(fil) - if rec: - if isinstance(rec, str): - rec = fnmatch(fil) - elif not hasattr(rec, '__call__'): - rec = None - try: - entries = self.listdir() - except ignore: - return - dirs = [p for p in entries - if p.check(dir=1) and (rec is None or rec(p))] - for subdir in dirs: - for p in subdir.visit(fil=fil, rec=rec, ignore=ignore): - yield p - for p in entries: - if fil is None or fil(p): - yield p + for x in Visitor(fil, rec, ignore, bf, sort).gen(self): + yield x def _sortlist(self, res, sort): if sort: @@ -312,6 +300,41 @@ newline will be removed from the end of """ return True if other refers to the same stat object as self. """ return self.strpath == str(other) +class Visitor: + def __init__(self, fil, rec, ignore, bf, sort): + if isinstance(fil, str): + fil = FNMatcher(fil) + if rec: + if isinstance(rec, str): + rec = fnmatch(fil) + else: + assert hasattr(rec, '__call__') + self.fil = fil + self.rec = rec + self.ignore = ignore + self.breadthfirst = bf + self.optsort = sort and sorted or (lambda x: x) + + def gen(self, path): + try: + entries = path.listdir() + except self.ignore: + return + rec = self.rec + dirs = self.optsort([p for p in entries + if p.check(dir=1) and (rec is None or rec(p))]) + if not self.breadthfirst: + for subdir in dirs: + for p in self.gen(subdir): + yield p + for p in self.optsort(entries): + if self.fil is None or self.fil(p): + yield p + if self.breadthfirst: + for subdir in dirs: + for p in self.gen(subdir): + yield p + class FNMatcher: def __init__(self, pattern): self.pattern = pattern --- a/testing/path/common.py +++ b/testing/path/common.py @@ -287,6 +287,12 @@ class CommonFSTests(object): def test_relto_wrong_type(self, path1): py.test.raises(TypeError, "path1.relto(42)") + def test_load(self, path1): + p = path1.join('samplepickle') + obj = p.load() + assert type(obj) is dict + assert obj.get('answer',None) == 42 + def test_visit_filesonly(self, path1): l = [] for i in path1.visit(lambda x: x.check(file=1)): @@ -294,12 +300,6 @@ class CommonFSTests(object): assert not "sampledir" in l assert path1.sep.join(["sampledir", "otherfile"]) in l - def test_load(self, path1): - p = path1.join('samplepickle') - obj = p.load() - assert type(obj) is dict - assert obj.get('answer',None) == 42 - def test_visit_nodotfiles(self, path1): l = [] for i in path1.visit(lambda x: x.check(dotfile=0)): @@ -308,6 +308,28 @@ class CommonFSTests(object): assert path1.sep.join(["sampledir", "otherfile"]) in l assert not ".dotfile" in l + def test_visit_breadthfirst(self, path1): + l = [] + for i in path1.visit(bf=True): + l.append(i.relto(path1)) + for i, p in enumerate(l): + if path1.sep in p: + for j in range(i, len(l)): + assert path1.sep in l[j] + break + else: + py.test.fail("huh") + + def test_visit_sort(self, path1): + l = [] + for i in path1.visit(bf=True, sort=True): + l.append(i.relto(path1)) + for i, p in enumerate(l): + if path1.sep in p: + break + assert l[:i] == sorted(l[:i]) + assert l[i:] == sorted(l[i:]) + def test_endswith(self, path1): def chk(p): return p.check(endswith="pickle") From commits-noreply at bitbucket.org Thu Nov 4 16:03:37 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 10:03:37 -0500 (CDT) Subject: [py-svn] pylib commit 577b5991dbce: bump version, add changelog Message-ID: <20101104150337.A86E3240F52@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1288883096 -3600 # Node ID 577b5991dbceffe0887001c8597fd9a2f4d50023 # Parent e8cc8ddb7505e5b4a2928c09990d9974a4508c77 bump version, add changelog --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def main(): long_description = long_description, install_requires=['py>=1.3.9', ], # force newer py version which removes 'py' namespace # # so we can occupy it - version='2.0.0.dev4', + version='2.0.0.dev5', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev4' +__version__ = '2.0.0.dev5' from py import _apipkg --- a/CHANGELOG +++ b/CHANGELOG @@ -11,6 +11,7 @@ Changes between 1.3.4 and 2.0.0dev0 - add py.iniconfig module for brain-dead easy ini-config file parsing - introduce py.builtin.any() - path objects have a .dirname attribute now +- path.visit() accepts breadthfirst (bf) and sort options Changes between 1.3.3 and 1.3.4 ================================================== From commits-noreply at bitbucket.org Thu Nov 4 22:35:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 16:35:01 -0500 (CDT) Subject: [py-svn] apipkg commit fd590ef45812: make initpkg work without an "old module" in sys.modules Message-ID: <20101104213501.B792F1E0FC0@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User Ralf Schmitt # Date 1288906297 -3600 # Node ID fd590ef45812b3b031ec3412e99b6f03324e9bf9 # Parent 41f548cd98ae6bc5afa5da737730dda749646d76 make initpkg work without an "old module" in sys.modules --- a/test_apipkg.py +++ b/test_apipkg.py @@ -400,3 +400,9 @@ def test_aliasmodule_repr(): assert "" == r am.version assert repr(am) == r + +def test_initpkg_without_old_module(): + apipkg.initpkg("initpkg_without_old_module", + dict(modules="sys:modules")) + from initpkg_without_old_module import modules + assert modules is sys.modules --- a/apipkg.py +++ b/apipkg.py @@ -13,7 +13,7 @@ __version__ = "1.2.dev1" def initpkg(pkgname, exportdefs, attr=dict()): """ initialize given package from the export definitions. """ - oldmod = sys.modules[pkgname] + oldmod = sys.modules.get(pkgname) d = {} f = getattr(oldmod, '__file__', None) if f: @@ -28,7 +28,8 @@ def initpkg(pkgname, exportdefs, attr=di if hasattr(oldmod, '__doc__'): d['__doc__'] = oldmod.__doc__ d.update(attr) - oldmod.__dict__.update(d) + if hasattr(oldmod, "__dict__"): + oldmod.__dict__.update(d) mod = ApiModule(pkgname, exportdefs, implprefix=pkgname, attr=d) sys.modules[pkgname] = mod From commits-noreply at bitbucket.org Thu Nov 4 22:35:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 16:35:01 -0500 (CDT) Subject: [py-svn] apipkg commit 41f548cd98ae: simplify AliasModule.__repr__. Message-ID: <20101104213501.AAB0D1E0FBF@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User Ralf Schmitt # Date 1288905387 -3600 # Node ID 41f548cd98ae6bc5afa5da737730dda749646d76 # Parent ad8da71fea6dedc0757be6d7fedf7aa2e59362ce simplify AliasModule.__repr__. also do not use __name__, rather load it from the original module. --- a/test_apipkg.py +++ b/test_apipkg.py @@ -393,3 +393,10 @@ def test_extra_attributes(tmpdir, monkey monkeypatch.syspath_prepend(tmpdir) import extra_attributes assert extra_attributes.foo == 'bar' + +def test_aliasmodule_repr(): + am = apipkg.AliasModule("mymod", "sys") + r = repr(am) + assert "" == r + am.version + assert repr(am) == r --- a/apipkg.py +++ b/apipkg.py @@ -128,18 +128,11 @@ class ApiModule(ModuleType): class AliasModule(ModuleType): def __init__(self, name, modpath): - self.__name__ = name + self.__name = name self.__modpath = modpath def __repr__(self): - l = [] - if hasattr(self, '__version__'): - l.append("version=" + repr(self.__version__)) - if hasattr(self, '__file__'): - l.append('from ' + repr(self.__file__)) - if l: - return '' % (self.__name__, " ".join(l)) - return '' % (self.__name__,) + return '' % (self.__name, self.__modpath) def __getattr__(self, name): mod = importobj(self.__modpath, None) From commits-noreply at bitbucket.org Thu Nov 4 22:50:05 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 4 Nov 2010 16:50:05 -0500 (CDT) Subject: [py-svn] apipkg commit f7ad3ae4e30f: dont load __doc__ early, making ApiModules now fully lazy Message-ID: <20101104215005.3CEFE1E0FC0@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project apipkg # URL http://bitbucket.org/hpk42/apipkg/overview # User holger krekel # Date 1288907454 -3600 # Node ID f7ad3ae4e30f937c2e789331e80ae22315b17c2c # Parent fd590ef45812b3b031ec3412e99b6f03324e9bf9 dont load __doc__ early, making ApiModules now fully lazy also bump and normalize __version__ setting --- a/setup.py +++ b/setup.py @@ -13,15 +13,13 @@ try: except ImportError: from distutils.core import setup -from apipkg import __version__ - def main(): setup( name='apipkg', description= 'apipkg: namespace control and lazy-import mechanism', long_description = open('README.txt').read(), - version= __version__, + version='1.2.dev4', url='http://bitbucket.org/hpk42/apipkg', license='MIT License', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/apipkg.py +++ b/apipkg.py @@ -25,7 +25,7 @@ def initpkg(pkgname, exportdefs, attr=di d['__loader__'] = oldmod.__loader__ if hasattr(oldmod, '__path__'): d['__path__'] = [os.path.abspath(p) for p in oldmod.__path__] - if hasattr(oldmod, '__doc__'): + if '__doc__' not in exportdefs and getattr(oldmod, '__doc__', None): d['__doc__'] = oldmod.__doc__ d.update(attr) if hasattr(oldmod, "__dict__"): @@ -45,6 +45,16 @@ def importobj(modpath, attrname): return retval class ApiModule(ModuleType): + def __docget(self): + try: + return self.__doc + except AttributeError: + if '__doc__' in self.__map__: + return self.__makeattr('__doc__') + def __docset(self, value): + self.__doc = value + __doc__ = property(__docget, __docset) + def __init__(self, name, importspec, implprefix=None, attr=None): self.__name__ = name self.__all__ = [x for x in importspec if x != '__onfirstaccess__'] @@ -73,10 +83,7 @@ class ApiModule(ModuleType): sys.modules[subname] = apimod setattr(self, name, apimod) else: - if name == '__doc__': - self.__doc__ = importobj(modpath, attrname) - else: - self.__map__[name] = (modpath, attrname) + self.__map__[name] = (modpath, attrname) def __repr__(self): l = [] --- a/test_apipkg.py +++ b/test_apipkg.py @@ -115,12 +115,12 @@ class TestScenarios: }) """)) pkgdir.join('submod.py').write(py.code.Source(""" - import recmodule + import recmodule class someclass: pass print (recmodule.__dict__) """)) monkeypatch.syspath_prepend(tmpdir) - import recmodule + import recmodule assert isinstance(recmodule, apipkg.ApiModule) assert recmodule.some.__name__ == "someclass" @@ -224,14 +224,22 @@ def test_initpkg_transfers_attrs(monkeyp assert newmod.__loader__ == mod.__loader__ assert newmod.__doc__ == mod.__doc__ -def test_initpkg_not_overwrite_exportdefs(monkeypatch): +def test_initpkg_nodoc(monkeypatch): mod = type(sys)('hello') - mod.__doc__ = "this is the documentation" + mod.__file__ = "hello.py" monkeypatch.setitem(sys.modules, 'hello', mod) + apipkg.initpkg('hello', {}) + newmod = sys.modules['hello'] + assert not newmod.__doc__ + +def test_initpkg_overwrite_doc(monkeypatch): + hello = type(sys)('hello') + hello.__doc__ = "this is the documentation" + monkeypatch.setitem(sys.modules, 'hello', hello) apipkg.initpkg('hello', {"__doc__": "sys:__doc__"}) - newmod = sys.modules['hello'] - assert newmod != mod - assert newmod.__doc__ == sys.__doc__ + newhello = sys.modules['hello'] + assert newhello != hello + assert newhello.__doc__ == sys.__doc__ def test_initpkg_not_transfers_not_existing_attrs(monkeypatch): mod = type(sys)('hello') @@ -289,7 +297,7 @@ def test_onfirstaccess(tmpdir, monkeypat """)) pkgdir.join('submod.py').write(py.code.Source(""" l = [] - def init(): + def init(): l.append(1) """)) monkeypatch.syspath_prepend(tmpdir) @@ -311,9 +319,9 @@ def test_onfirstaccess_setsnewattr(tmpdi ) """)) pkgdir.join('submod.py').write(py.code.Source(""" - def init(): + def init(): import %s as pkg - pkg.newattr = 42 + pkg.newattr = 42 """ % pkgname)) monkeypatch.syspath_prepend(tmpdir) mod = __import__(pkgname) --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,8 @@ -1.2 +1.2.dev ---------------------------------------- - Allow to import from Aliasmodules (thanks Ralf Schmitt) +- avoid loading __doc__ early, so ApiModule is now fully lazy 1.1 ---------------------------------------- From commits-noreply at bitbucket.org Sat Nov 6 00:46:34 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 5 Nov 2010 18:46:34 -0500 (CDT) Subject: [py-svn] pylib commit 5367bea2f477: update iniconfig Message-ID: <20101105234634.0F61A6C140E@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User Ronny Pfannschmidt # Date 1288999101 -3600 # Node ID 5367bea2f47738bdefa9eb55752e5a2a4b0e841b # Parent 577b5991dbceffe0887001c8597fd9a2f4d50023 update iniconfig --- a/testing/test_iniconfig.py +++ b/testing/test_iniconfig.py @@ -92,7 +92,7 @@ def parse(input): # only for testing purposes - _parse() does not use state except path ini = object.__new__(IniConfig) ini.path = "sample" - return ini._parse(input) + return ini._parse(input.splitlines(True)) def parse_a_error(input): return py.test.raises(ParseError, parse, input) --- a/py/_iniconfig.py +++ b/py/_iniconfig.py @@ -46,10 +46,13 @@ class IniConfig(object): self.path = str(path) # convenience if data is None: f = open(self.path) - data = f.read() - f.close() - tokens = self._parse(data) - + try: + tokens = self._parse(iter(f)) + finally: + f.close() + else: + tokens = self._parse(data.splitlines(True)) + self._sources = {} self.sections = {} @@ -69,10 +72,10 @@ class IniConfig(object): def _raise(self, lineno, msg): raise ParseError(self.path, lineno, msg) - def _parse(self, data): + def _parse(self, line_iter): result = [] section = None - for lineno, line in enumerate(data.splitlines(True)): + for lineno, line in enumerate(line_iter): name, data = self._parseline(line, lineno) # new value if name is not None and data is not None: @@ -116,7 +119,7 @@ class IniConfig(object): try: name, value = line.split(": ", 1) except ValueError: - self._raise(lineno, 'unexpected line: %s') + self._raise(lineno, 'unexpected line: %r' % line) return name.strip(), value.strip() # continuation else: From commits-noreply at bitbucket.org Sat Nov 6 09:57:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:00 -0500 (CDT) Subject: [py-svn] pytest commit 2dfb0db0864d: remove pytest_report_iteminfo hook, i strongly guess nobody needs or uses it. Message-ID: <20101106085700.96733241420@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288909283 -3600 # Node ID 2dfb0db0864d075300712366a86cdf642c7c68d9 # Parent 3253d770b03c29b46f2d5c96f46b00b01f882551 remove pytest_report_iteminfo hook, i strongly guess nobody needs or uses it. --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -1061,18 +1061,6 @@ class TestReportInfo: nodeinfo = runner.getitemnodeinfo(item) assert nodeinfo.location == ("ABCDE", 42, "custom") - def test_itemreport_pytest_report_iteminfo(self, testdir, linecomp): - item = testdir.getitem("def test_func(): pass") - tup = "FGHJ", 42, "custom" - class Plugin: - def pytest_report_iteminfo(self, item): - return tup - item.config.pluginmanager.register(Plugin()) - runner = runner = item.config.pluginmanager.getplugin("runner") - nodeinfo = runner.getitemnodeinfo(item) - location = nodeinfo.location - assert location == tup - def test_func_reportinfo(self, testdir): item = testdir.getitem("def test_func(): pass") fspath, lineno, modpath = item.reportinfo() --- a/pytest/plugin/nose.py +++ b/pytest/plugin/nose.py @@ -49,19 +49,6 @@ def pytest_runtest_makereport(__multical call2 = call.__class__(lambda: py.test.skip(str(call.excinfo.value)), call.when) call.excinfo = call2.excinfo -def pytest_report_iteminfo(item): - # nose 0.11.1 uses decorators for "raises" and other helpers. - # for reporting progress by filename we fish for the filename - if isinstance(item, py.test.collect.Function): - obj = item.obj - if hasattr(obj, 'compat_co_firstlineno'): - fn = sys.modules[obj.__module__].__file__ - if fn.endswith(".pyc"): - fn = fn[:-1] - #assert 0 - #fn = inspect.getsourcefile(obj) or inspect.getfile(obj) - lineno = obj.compat_co_firstlineno - return py.path.local(fn), lineno, obj.__module__ def pytest_runtest_setup(item): if isinstance(item, (py.test.collect.Function)): --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -123,8 +123,19 @@ class PyobjMixin(object): return self._fslineno def reportinfo(self): - fspath, lineno = self._getfslineno() - modpath = self.getmodpath() + obj = self.obj + if hasattr(obj, 'compat_co_firstlineno'): + # nose compatibility + fspath = sys.modules[obj.__module__].__file__ + if fspath.endswith(".pyc"): + fspath = fspath[:-1] + #assert 0 + #fn = inspect.getsourcefile(obj) or inspect.getfile(obj) + lineno = obj.compat_co_firstlineno + modpath = obj.__module__ + else: + fspath, lineno = self._getfslineno() + modpath = self.getmodpath() return fspath, lineno, modpath class PyCollectorMixin(PyobjMixin, pytest.collect.Collector): @@ -501,16 +512,16 @@ class Metafunc: :arg funcargs: argument keyword dictionary used when invoking the test function. - :arg id: used for reporting and identification purposes. If you + :arg id: used for reporting and identification purposes. If you don't supply an `id` the length of the currently list of calls to the test function will be used. :arg param: will be exposed to a later funcarg factory invocation through the ``request.param`` attribute. Setting it (instead of directly providing a ``funcargs`` ditionary) is called - *indirect parametrization*. Indirect parametrization is - preferable if test values are expensive to setup or can - only be created after certain fixtures or test-run related + *indirect parametrization*. Indirect parametrization is + preferable if test values are expensive to setup or can + only be created after certain fixtures or test-run related initialization code has been run. """ assert funcargs is None or isinstance(funcargs, dict) @@ -593,7 +604,7 @@ class FuncargRequest: def applymarker(self, marker): """ apply a marker to a single test function invocation. This method is useful if you don't want to have a keyword/marker - on all function invocations. + on all function invocations. :arg marker: a :py:class:`pytest.plugin.mark.MarkDecorator` object created by a call to ``py.test.mark.NAME(...)``. --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -20,7 +20,7 @@ def pytest_cmdline_parse(pluginmanager, pytest_cmdline_parse.firstresult = True def pytest_addoption(parser): - """add optparse-style options and ini-style config values via calls + """add optparse-style options and ini-style config values via calls to ``parser.addoption`` and ``parser.addini(...)``. """ @@ -194,14 +194,6 @@ pytest_report_teststatus.firstresult = T def pytest_terminal_summary(terminalreporter): """ add additional section in terminal summary reporting. """ -def pytest_report_iteminfo(item): - """ return (fspath, lineno, domainpath) location info for the item. - the information is used for result display and to sort tests. - fspath,lineno: file and linenumber of source of item definition. - domainpath: custom id - e.g. for python: dotted import address - """ -pytest_report_iteminfo.firstresult = True - # ------------------------------------------------------------------------- # doctest hooks # ------------------------------------------------------------------------- --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -116,9 +116,6 @@ def pytest_collect_directory(path, paren return return Directory(path, parent=parent) -def pytest_report_iteminfo(item): - return item.reportinfo() - class Session(object): class Interrupted(KeyboardInterrupt): """ signals an interrupted test run. """ --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -96,7 +96,7 @@ class TestTerminal: tr = TerminalReporter(item.config, file=linecomp.stringio) item.config.pluginmanager.register(tr) nodeid = item.collection.getid(item) - location = item.ihook.pytest_report_iteminfo(item=item) + location = item.reportinfo() tr.config.hook.pytest_runtest_logstart(nodeid=nodeid, location=location, fspath=str(item.fspath)) linecomp.assert_contains_lines([ --- a/pytest/plugin/runner.py +++ b/pytest/plugin/runner.py @@ -41,7 +41,7 @@ def getitemnodeinfo(item): try: return item._nodeinfo except AttributeError: - location = item.ihook.pytest_report_iteminfo(item=item) + location = item.reportinfo() location = (str(location[0]), location[1], str(location[2])) nodenames = tuple(item.listnames()) nodeid = item.collection.getid(item) --- a/testing/plugin/test_session.py +++ b/testing/plugin/test_session.py @@ -1,5 +1,4 @@ import py -from pytest.plugin.session import pytest_report_iteminfo class SessionTests: def test_basic_testitem_events(self, testdir): @@ -225,12 +224,3 @@ def test_exclude(testdir): result = testdir.runpytest("--ignore=hello", "--ignore=hello2") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) - -def test_pytest_report_iteminfo(): - class FakeItem(object): - - def reportinfo(self): - return "-reportinfo-" - - res = pytest_report_iteminfo(FakeItem()) - assert res == "-reportinfo-" From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit 125c0136ba20: add "linelist" type for ini-files Message-ID: <20101106085701.19801243F49@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996651 -3600 # Node ID 125c0136ba20fa7c651de76a4c679b1c59440487 # Parent e4e9f0f7c4dd2e8b39991b38db44f676802f1b99 add "linelist" type for ini-files --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -70,7 +70,7 @@ class Parser: def addini(self, name, help, type=None, default=None): """ add an ini-file option with the given name and description. """ - assert type in (None, "pathlist", "args") + assert type in (None, "pathlist", "args", "linelist") self._inidict[name] = (help, type, default) class OptionGroup: @@ -365,7 +365,9 @@ class Config(object): except KeyError: if default is not None: return default - return {'pathlist': [], 'args': [], None: ''}.get(type) + if type is None: + return '' + return [] if type == "pathlist": dp = py.path.local(self.inicfg.config.path).dirpath() l = [] @@ -374,6 +376,8 @@ class Config(object): return l elif type == "args": return py.std.shlex.split(value) + elif type == "linelist": + return filter(None, map(lambda x: x.strip(), value.split("\n"))) else: assert type is None return value --- a/testing/test_config.py +++ b/testing/test_config.py @@ -169,6 +169,24 @@ class TestConfigAPI: l = config.getini("a2") assert l == list("123") + def test_addini_linelist(self, testdir): + testdir.makeconftest(""" + def pytest_addoption(parser): + parser.addini("xy", "", type="linelist") + parser.addini("a2", "", "linelist") + """) + p = testdir.makeini(""" + [pytest] + xy= 123 345 + second line + """) + config = testdir.parseconfig() + l = config.getini("xy") + assert len(l) == 2 + assert l == ["123 345", "second line"] + l = config.getini("a2") + assert l == [] + def test_options_on_small_file_do_not_blow_up(testdir): def runfiletest(opts): reprec = testdir.inline_run(*opts) From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit 0aaee1071bb4: update issues Message-ID: <20101106085701.34796243F53@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996651 -3600 # Node ID 0aaee1071bb49468aa9dc35c3a18305262de9998 # Parent 4bdabf614c7658b422eba78d787f8bce41b81a2d update issues --- a/ISSUES.txt +++ b/ISSUES.txt @@ -3,11 +3,18 @@ checks / deprecations for next release tags: bug 2.4 core xdist * check oejskit plugin compatibility -* some simple profiling * move pytest_nose out of pylib because it implicitely extends the protocol now - setup/teardown is called at module level. consider making calling of setup/teardown configurable +profiling / hook call optimization +------------------------------------- +tags: enhancement 2.1 + +bench/bench.py reveals that for very quick running +unit tests the hook architecture is a bit slow. +Profile and improve hook calls. + do early-teardown of test modules ----------------------------------------- tags: feature 2.1 @@ -108,13 +115,12 @@ tags: feature 2.1 allow to name conftest.py files (in sub directories) that should be imported early, as to include command line options. -a central py.test ini/yml file +improve central py.test ini file ---------------------------------- tags: feature 2.1 -introduce a declarative configuration file that allows: -- default options -- to-be-collected test directories +introduce more declarative configuration options: +- (to-be-collected test directories) - required plugins - test func/class/file matching patterns - skip/xfail (non-intrusive) @@ -125,14 +131,11 @@ new documentation tags: feature 2.1 - logo py.test -- reference / customization -- writing a (local or global) plugin - examples for unittest or functional testing - resource management for functional testing - patterns: page object - parametrized testing - better / more integrated plugin docs - i.e. not per-plugin but per-feature referencing a plugin generalize parametrized testing to generate combinations ------------------------------------------------------------- From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit 4bdabf614c76: reverse options ordering in Parser class instead of on PluginManager Message-ID: <20101106085701.27F72243F4D@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996651 -3600 # Node ID 4bdabf614c7658b422eba78d787f8bce41b81a2d # Parent 125c0136ba20fa7c651de76a4c679b1c59440487 reverse options ordering in Parser class instead of on PluginManager --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -53,12 +53,12 @@ class Parser: def parse(self, args): self.optparser = optparser = MyOptionParser(self) - groups = self._groups + [self._anonymous] + groups = list(reversed(self._groups)) + [self._anonymous] for group in groups: if group.options: desc = group.description or group.name optgroup = py.std.optparse.OptionGroup(optparser, desc) - optgroup.add_options(group.options) + optgroup.add_options(reversed(group.options)) optparser.add_option_group(optgroup) return self.optparser.parse_args([str(x) for x in args]) @@ -304,7 +304,7 @@ class Config(object): self.pluginmanager.consider_env() self.pluginmanager.consider_preparse(args) self._setinitialconftest(args) - self.pluginmanager.do_addoption(self._parser) + self.hook.pytest_addoption(parser=self._parser) def _checkversion(self): minver = self.inicfg.get('minversion', None) --- a/pytest/_core.py +++ b/pytest/_core.py @@ -180,11 +180,6 @@ class PluginManager(object): for hint in self._hints: tw.line("hint: %s" % hint) - def do_addoption(self, parser): - mname = "pytest_addoption" - methods = reversed(self.listattr(mname)) - MultiCall(methods, {'parser': parser}).execute() - def do_configure(self, config): assert not hasattr(self, '_config') self._config = config From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit 22ce60ec51fe: introduce a minimal tag-based tracer, to be extended if needed, strike pytest_trace hook. Message-ID: <20101106085701.4D178243F54@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996651 -3600 # Node ID 22ce60ec51feb6e4464ba3142c5c008322d74557 # Parent 0aaee1071bb49468aa9dc35c3a18305262de9998 introduce a minimal tag-based tracer, to be extended if needed, strike pytest_trace hook. --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -8,6 +8,8 @@ import pytest def pytest_cmdline_parse(pluginmanager, args): config = Config(pluginmanager) config.parse(args) + if config.option.debug: + config.trace.root.setwriter(sys.stderr.write) return config class Parser: @@ -253,6 +255,7 @@ class Config(object): ) #: a pluginmanager instance self.pluginmanager = pluginmanager or PluginManager(load=True) + self.trace = self.pluginmanager.trace.get("config") self._conftest = Conftest(onimport=self._onimportconftest) self.hook = self.pluginmanager.hook @@ -272,10 +275,6 @@ class Config(object): plugins += self._conftest.getconftestmodules(fspath) return plugins - def trace(self, msg): - if getattr(self.option, 'traceconfig', None): - self.hook.pytest_trace(category="config", msg=msg) - def _setinitialconftest(self, args): # capture output during conftest init (#issue93) name = hasattr(os, 'dup') and 'StdCaptureFD' or 'StdCapture' --- a/testing/test_config.py +++ b/testing/test_config.py @@ -75,6 +75,14 @@ class TestConfigTmpdir: class TestConfigAPI: + def test_config_trace(self, testdir): + config = testdir.Config() + l = [] + config.trace.root.setwriter(l.append) + config.trace("hello") + assert len(l) == 1 + assert l[0] == "[pytest:config] hello\n" + def test_config_getvalue_honours_conftest(self, testdir): testdir.makepyfile(conftest="x=1") testdir.mkdir("sub").join("conftest.py").write("x=2 ; y = 3") --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -511,20 +511,28 @@ def test_tbstyle_short(testdir): assert 'x = 0' in s assert 'assert x' in s -def test_trace_reporting(testdir): +def test_traceconfig(testdir, monkeypatch): result = testdir.runpytest("--traceconfig") result.stdout.fnmatch_lines([ "*active plugins*" ]) assert result.ret == 0 -def test_trace_reporting(testdir): - result = testdir.runpytest("--traceconfig") - result.stdout.fnmatch_lines([ - "*active plugins*" +def test_debug(testdir, monkeypatch): + result = testdir.runpytest("--debug") + result.stderr.fnmatch_lines([ + "*registered*session*", ]) assert result.ret == 0 +def test_PYTEST_DEBUG(testdir, monkeypatch): + monkeypatch.setenv("PYTEST_DEBUG", "1") + result = testdir.runpytest() + assert result.ret == 0 + result.stderr.fnmatch_lines([ + "*registered*PluginManager*" + ]) + class TestGenericReporting: """ this test class can be subclassed with a different option --- a/pytest/_core.py +++ b/pytest/_core.py @@ -13,11 +13,53 @@ default_plugins = ( IMPORTPREFIX = "pytest_" +class TagTracer: + def __init__(self): + self._tag2proc = {} + self.writer = None + + def get(self, name): + return TagTracerSub(self, (name,)) + + def processmessage(self, tags, args): + if self.writer is not None: + prefix = ":".join(tags) + content = " ".join(map(str, args)) + self.writer("[%s] %s\n" %(prefix, content)) + try: + self._tag2proc[tags](tags, args) + except KeyError: + pass + + def setwriter(self, writer): + self.writer = writer + + def setprocessor(self, tags, processor): + if isinstance(tags, str): + tags = tuple(tags.split(":")) + else: + assert isinstance(tags, tuple) + self._tag2proc[tags] = processor + +class TagTracerSub: + def __init__(self, root, tags): + self.root = root + self.tags = tags + def __call__(self, *args): + self.root.processmessage(self.tags, args) + def setmyprocessor(self, processor): + self.root.setprocessor(self.tags, processor) + def get(self, name): + return self.__class__(self.root, self.tags + (name,)) + class PluginManager(object): def __init__(self, load=False): self._name2plugin = {} self._plugins = [] self._hints = [] + self.trace = TagTracer().get("pytest") + if os.environ.get('PYTEST_DEBUG'): + self.trace.root.setwriter(sys.stderr.write) self.hook = HookRelay([hookspec], pm=self) self.register(self) if load: @@ -41,6 +83,7 @@ class PluginManager(object): self._name2plugin[name] = plugin self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) self.hook.pytest_plugin_registered(manager=self, plugin=plugin) + self.trace("registered", plugin) if not prepend: self._plugins.append(plugin) else: --- a/pytest/plugin/terminal.py +++ b/pytest/plugin/terminal.py @@ -36,7 +36,7 @@ def pytest_configure(config): if config.option.collectonly: reporter = CollectonlyReporter(config) else: - # we try hard to make printing resilient against + # we try hard to make printing resilient against # later changes on FD level. stdout = py.std.sys.stdout if hasattr(os, 'dup') and hasattr(stdout, 'fileno'): @@ -50,6 +50,11 @@ def pytest_configure(config): config._toclose = stdout reporter = TerminalReporter(config, stdout) config.pluginmanager.register(reporter, 'terminalreporter') + if config.option.debug or config.option.traceconfig: + def mywriter(tags, args): + msg = " ".join(map(str, args)) + reporter.write_line("[traceconfig] " + msg) + config.trace.root.setprocessor("pytest:config", mywriter) def pytest_unconfigure(config): if hasattr(config, '_toclose'): @@ -152,11 +157,6 @@ class TerminalReporter: # which garbles our output if we use self.write_line self.write_line(msg) - def pytest_trace(self, category, msg): - if self.config.option.debug or \ - self.config.option.traceconfig and category.find("config") != -1: - self.write_line("[%s] %s" %(category, msg)) - def pytest_deselected(self, items): self.stats.setdefault('deselected', []).extend(items) --- a/testing/test_pluginmanager.py +++ b/testing/test_pluginmanager.py @@ -266,6 +266,19 @@ class TestBootstrapping: l = list(plugins.listattr('x')) assert l == [41, 42, 43] + def test_register_trace(self): + pm = PluginManager() + class api1: + x = 41 + l = [] + pm.trace.setmyprocessor(lambda kw, args: l.append((kw, args))) + p = api1() + pm.register(p) + assert len(l) == 1 + kw, args = l[0] + assert args[0] == "registered" + assert args[1] == p + class TestPytestPluginInteractions: def test_addhooks_conftestplugin(self, testdir): @@ -470,7 +483,7 @@ class TestMultiCall: reslist = MultiCall([f], dict(x=23, z=2)).execute() assert reslist == [25] - def test_keywords_call_error(self): + def test_tags_call_error(self): multicall = MultiCall([lambda x: x], {}) py.test.raises(TypeError, "multicall.execute()") @@ -537,3 +550,55 @@ class TestHookRelay: res = mcm.hello(arg=3) assert res == 4 +class TestTracer: + def test_simple(self): + from pytest._core import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("pytest") + log("hello") + l = [] + rootlogger.setwriter(l.append) + log("world") + assert len(l) == 1 + assert l[0] == "[pytest] world\n" + sublog = log.get("collection") + sublog("hello") + assert l[1] == "[pytest:collection] hello\n" + + def test_setprocessor(self): + from pytest._core import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + assert log2.tags == tuple("12") + l = [] + rootlogger.setprocessor(tuple("12"), lambda *args: l.append(args)) + log("not seen") + log2("seen") + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == ("seen",) + l2 = [] + rootlogger.setprocessor("1:2", lambda *args: l2.append(args)) + log2("seen") + tags, args = l2[0] + assert args == ("seen",) + + + def test_setmyprocessor(self): + from pytest._core import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + l = [] + log2.setmyprocessor(lambda *args: l.append(args)) + log("not seen") + assert not l + log2(42) + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == (42,) From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit e90c2abaefeb: add indent facility to tracing Message-ID: <20101106085701.AC695243F56@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289030717 -3600 # Node ID e90c2abaefeb33d96b672654b2698281420acc54 # Parent 09ceba1e4812bd91aecb71f2e31d78d663957f60 add indent facility to tracing --- a/testing/test_main.py +++ b/testing/test_main.py @@ -553,17 +553,39 @@ class TestHookRelay: class TestTracer: def test_simple(self): from pytest.main import TagTracer - rootlogger = TagTracer() + rootlogger = TagTracer("[my] ") log = rootlogger.get("pytest") log("hello") l = [] rootlogger.setwriter(l.append) log("world") assert len(l) == 1 - assert l[0] == "[pytest] world\n" + assert l[0] == "[my] world\n" sublog = log.get("collection") sublog("hello") - assert l[1] == "[pytest:collection] hello\n" + assert l[1] == "[my] hello\n" + + def test_indent(self): + from pytest.main import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + l = [] + log.root.setwriter(lambda arg: l.append(arg)) + log("hello") + log.root.indent += 1 + log("line1") + log("line2") + log.root.indent += 1 + log("line3") + log("line4") + log.root.indent -= 1 + log("line5") + log.root.indent -= 1 + log("last") + assert len(l) == 7 + names = [x.rstrip()[len(rootlogger.prefix):] for x in l] + assert names == ['hello', ' line1', ' line2', + ' line3', ' line4', ' line5', 'last'] def test_setprocessor(self): from pytest.main import TagTracer --- a/pytest/main.py +++ b/pytest/main.py @@ -19,18 +19,21 @@ default_plugins = ( IMPORTPREFIX = "pytest_" class TagTracer: - def __init__(self): + def __init__(self, prefix="[pytest] "): self._tag2proc = {} self.writer = None + self.indent = 0 + self.prefix = prefix def get(self, name): return TagTracerSub(self, (name,)) def processmessage(self, tags, args): if self.writer is not None: - prefix = ":".join(tags) - content = " ".join(map(str, args)) - self.writer("[%s] %s\n" %(prefix, content)) + if args: + indent = " " * self.indent + content = " ".join(map(str, args)) + self.writer("%s%s%s\n" %(self.prefix, indent, content)) try: self._tag2proc[tags](tags, args) except KeyError: @@ -62,7 +65,7 @@ class PluginManager(object): self._name2plugin = {} self._plugins = [] self._hints = [] - self.trace = TagTracer().get("pytest") + self.trace = TagTracer().get("pluginmanage") if os.environ.get('PYTEST_DEBUG'): self.trace.root.setwriter(sys.stderr.write) self.hook = HookRelay([hookspec], pm=self) @@ -340,6 +343,7 @@ class HookRelay: hookspecs = [hookspecs] self._hookspecs = [] self._pm = pm + self.trace = pm.trace.root.get("hook") for hookspec in hookspecs: self._addhooks(hookspec, prefix) @@ -376,6 +380,7 @@ class HookCaller: return mc.execute() def pcall(self, plugins, **kwargs): + self.hookrelay.trace(self.name, kwargs) methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) mc = MultiCall(methods, kwargs, firstresult=self.firstresult) return mc.execute() From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit 09ceba1e4812: implement and document new invocation mechanisms, see doc/usage.txt Message-ID: <20101106085701.988DB243F55@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996651 -3600 # Node ID 09ceba1e4812bd91aecb71f2e31d78d663957f60 # Parent 22ce60ec51feb6e4464ba3142c5c008322d74557 implement and document new invocation mechanisms, see doc/usage.txt also rename pytest._core to pytest.main for convenience. --- /dev/null +++ b/pytest/__main__.py @@ -0,0 +1,4 @@ +import pytest + +if __name__ == '__main__': + raise SystemExit(pytest.main()) --- a/doc/test/plugin/cov.txt +++ b/doc/test/plugin/cov.txt @@ -35,7 +35,7 @@ However easy_install does not provide an .. IMPORTANT:: - Ensure that you manually delete the init_cov_core.pth file in your site-packages directory. + Ensure that you manually delete the init_covmain.pth file in your site-packages directory. This file starts coverage collection of subprocesses if appropriate during site initialisation at python startup. --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -9,8 +9,6 @@ __version__ = '2.0.0.dev18' __all__ = ['config', 'cmdline'] -from pytest import _core as cmdline +from pytest import main as cmdline UsageError = cmdline.UsageError - -def __main__(): - raise SystemExit(cmdline.main()) +main = cmdline.main --- a/doc/index.txt +++ b/doc/index.txt @@ -3,10 +3,14 @@ py.test: no-boilerplate testing with Pyt .. todolist:: - +.. note:: + version 2.0 introduces ``pytest`` as the main Python import name + but for historic reasons ``py.test`` remains fully valid and + represents the same package. + Welcome to ``py.test`` documentation: -.. toctree:: +.. toctree:: :maxdepth: 2 overview @@ -27,4 +31,3 @@ Indices and tables * :ref:`genindex` * :ref:`modindex` * :ref:`search` - --- a/doc/cmdline.txt +++ /dev/null @@ -1,111 +0,0 @@ - -.. _cmdline: - -Using the interactive command line -=============================================== - -Getting help on version, option names, environment vars ------------------------------------------------------------ - -:: - - py.test --version # shows where pytest was imported from - py.test --funcargs # show available builtin function arguments - py.test -h | --help # show help on command line and config file options - - -Stopping after the first (or N) failures ---------------------------------------------------- - -To stop the testing process after the first (N) failures:: - - py.test -x # stop after first failure - py.test -maxfail=2 # stop after two failures - -Modifying Python traceback printing ----------------------------------------------- - -Examples for modifying traceback printing:: - - py.test --showlocals # show local variables in tracebacks - py.test -l # show local variables (shortcut) - - py.test --tb=long # the default informative traceback formatting - py.test --tb=native # the Python standard library formatting - py.test --tb=short # a shorter traceback format - py.test --tb=line # only one line per failure - -Dropping to PDB (Python Debugger) on failures ----------------------------------------------- - -.. _PDB: http://docs.python.org/library/pdb.html - -Python comes with a builtin Python debugger called PDB_. ``py.test`` -allows to drop into the PDB prompt via a command line option:: - - py.test --pdb - -This will invoke the Python debugger on every failure. Often you might -only want to do this for the first failing test to understand a certain -failure situation:: - - py.test -x --pdb # drop to PDB on first failure, then end test session - py.test --pdb --maxfail=3 # drop to PDB for the first three failures - - -Setting a breakpoint / aka ``set_trace()`` ----------------------------------------------------- - -If you want to set a breakpoint and enter the ``pdb.set_trace()`` you -can use a helper:: - - def test_function(): - ... - py.test.set_trace() # invoke PDB debugger and tracing - -.. versionadded: 2.0.0 - -In previous versions you could only enter PDB tracing if -you :ref:`disable capturing`. - -creating JUnitXML format files ----------------------------------------------------- - -To create result files which can be read by Hudson_ or other Continous -integration servers, use this invocation:: - - py.test --junitxml=path - -to create an XML file at ``path``. - -creating resultlog format files ----------------------------------------------------- - -To create plain-text machine-readable result files you can issue:: - - py.test --resultlog=path - -and look at the content at the ``path`` location. Such files are used e.g. -by the `PyPy-test`_ web page to show test results over several revisions. - -.. _`PyPy-test`: http://codespeak.net:8099/summary - - -send test report to pocoo pastebin service ------------------------------------------------------ - -**Creating a URL for each test failure**:: - - py.test --pastebin=failed - -This will submit test run information to a remote Paste service and -provide a URL for each failure. You may select tests as usual or add -for example ``-x`` if you only want to send one particular failure. - -**Creating a URL for a whole test session log**:: - - py.test --pastebin=all - -Currently only pasting to the http://paste.pocoo.org service is implemented. - -.. include:: links.inc --- a/pytest/_core.py +++ /dev/null @@ -1,397 +0,0 @@ -import sys, os -import inspect -import py -from pytest import hookspec - -assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " - "%s is too old, remove or upgrade 'py'" % (py.__version__)) - -default_plugins = ( - "config session terminal runner python pdb capture unittest mark skipping " - "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " - "junitxml doctest").split() - -IMPORTPREFIX = "pytest_" - -class TagTracer: - def __init__(self): - self._tag2proc = {} - self.writer = None - - def get(self, name): - return TagTracerSub(self, (name,)) - - def processmessage(self, tags, args): - if self.writer is not None: - prefix = ":".join(tags) - content = " ".join(map(str, args)) - self.writer("[%s] %s\n" %(prefix, content)) - try: - self._tag2proc[tags](tags, args) - except KeyError: - pass - - def setwriter(self, writer): - self.writer = writer - - def setprocessor(self, tags, processor): - if isinstance(tags, str): - tags = tuple(tags.split(":")) - else: - assert isinstance(tags, tuple) - self._tag2proc[tags] = processor - -class TagTracerSub: - def __init__(self, root, tags): - self.root = root - self.tags = tags - def __call__(self, *args): - self.root.processmessage(self.tags, args) - def setmyprocessor(self, processor): - self.root.setprocessor(self.tags, processor) - def get(self, name): - return self.__class__(self.root, self.tags + (name,)) - -class PluginManager(object): - def __init__(self, load=False): - self._name2plugin = {} - self._plugins = [] - self._hints = [] - self.trace = TagTracer().get("pytest") - if os.environ.get('PYTEST_DEBUG'): - self.trace.root.setwriter(sys.stderr.write) - self.hook = HookRelay([hookspec], pm=self) - self.register(self) - if load: - for spec in default_plugins: - self.import_plugin(spec) - - def _getpluginname(self, plugin, name): - if name is None: - if hasattr(plugin, '__name__'): - name = plugin.__name__.split(".")[-1] - else: - name = id(plugin) - return name - - def register(self, plugin, name=None, prepend=False): - assert not self.isregistered(plugin), plugin - assert not self.isregistered(plugin), plugin - name = self._getpluginname(plugin, name) - if name in self._name2plugin: - return False - self._name2plugin[name] = plugin - self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) - self.hook.pytest_plugin_registered(manager=self, plugin=plugin) - self.trace("registered", plugin) - if not prepend: - self._plugins.append(plugin) - else: - self._plugins.insert(0, plugin) - return True - - def unregister(self, plugin=None, name=None): - if plugin is None: - plugin = self.getplugin(name=name) - self._plugins.remove(plugin) - self.hook.pytest_plugin_unregistered(plugin=plugin) - for name, value in list(self._name2plugin.items()): - if value == plugin: - del self._name2plugin[name] - - def isregistered(self, plugin, name=None): - if self._getpluginname(plugin, name) in self._name2plugin: - return True - for val in self._name2plugin.values(): - if plugin == val: - return True - - def addhooks(self, spec): - self.hook._addhooks(spec, prefix="pytest_") - - def getplugins(self): - return list(self._plugins) - - def skipifmissing(self, name): - if not self.hasplugin(name): - py.test.skip("plugin %r is missing" % name) - - def hasplugin(self, name): - try: - self.getplugin(name) - return True - except KeyError: - return False - - def getplugin(self, name): - try: - return self._name2plugin[name] - except KeyError: - impname = canonical_importname(name) - return self._name2plugin[impname] - - # API for bootstrapping - # - def _envlist(self, varname): - val = py.std.os.environ.get(varname, None) - if val is not None: - return val.split(',') - return () - - def consider_env(self): - for spec in self._envlist("PYTEST_PLUGINS"): - self.import_plugin(spec) - - def consider_setuptools_entrypoints(self): - try: - from pkg_resources import iter_entry_points - except ImportError: - return # XXX issue a warning - for ep in iter_entry_points('pytest11'): - name = canonical_importname(ep.name) - if name in self._name2plugin: - continue - plugin = ep.load() - self.register(plugin, name=name) - - def consider_preparse(self, args): - for opt1,opt2 in zip(args, args[1:]): - if opt1 == "-p": - self.import_plugin(opt2) - - def consider_conftest(self, conftestmodule): - if self.register(conftestmodule, name=conftestmodule.__file__): - self.consider_module(conftestmodule) - - def consider_module(self, mod): - attr = getattr(mod, "pytest_plugins", ()) - if attr: - if not isinstance(attr, (list, tuple)): - attr = (attr,) - for spec in attr: - self.import_plugin(spec) - - def import_plugin(self, spec): - assert isinstance(spec, str) - modname = canonical_importname(spec) - if modname in self._name2plugin: - return - try: - mod = importplugin(modname) - except KeyboardInterrupt: - raise - except: - e = py.std.sys.exc_info()[1] - if not hasattr(py.test, 'skip'): - raise - elif not isinstance(e, py.test.skip.Exception): - raise - self._hints.append("skipped plugin %r: %s" %((modname, e.msg))) - else: - self.register(mod, modname) - self.consider_module(mod) - - def pytest_plugin_registered(self, plugin): - dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} - if dic: - self._setns(py.test, dic) - if hasattr(self, '_config'): - self.call_plugin(plugin, "pytest_addoption", - {'parser': self._config._parser}) - self.call_plugin(plugin, "pytest_configure", - {'config': self._config}) - - def _setns(self, obj, dic): - for name, value in dic.items(): - if isinstance(value, dict): - mod = getattr(obj, name, None) - if mod is None: - mod = py.std.types.ModuleType(name) - sys.modules['pytest.%s' % name] = mod - sys.modules['py.test.%s' % name] = mod - mod.__all__ = [] - setattr(obj, name, mod) - self._setns(mod, value) - else: - #print "setting", name, value, "on", obj - setattr(obj, name, value) - obj.__all__.append(name) - - def pytest_terminal_summary(self, terminalreporter): - tw = terminalreporter._tw - if terminalreporter.config.option.traceconfig: - for hint in self._hints: - tw.line("hint: %s" % hint) - - def do_configure(self, config): - assert not hasattr(self, '_config') - self._config = config - config.hook.pytest_configure(config=self._config) - - def do_unconfigure(self, config): - config = self._config - del self._config - config.hook.pytest_unconfigure(config=config) - config.pluginmanager.unregister(self) - - def notify_exception(self, excinfo): - excrepr = excinfo.getrepr(funcargs=True, showlocals=True) - res = self.hook.pytest_internalerror(excrepr=excrepr) - if not py.builtin.any(res): - for line in str(excrepr).split("\n"): - sys.stderr.write("INTERNALERROR> %s\n" %line) - sys.stderr.flush() - - def listattr(self, attrname, plugins=None): - if plugins is None: - plugins = self._plugins - l = [] - for plugin in plugins: - try: - l.append(getattr(plugin, attrname)) - except AttributeError: - continue - return l - - def call_plugin(self, plugin, methname, kwargs): - return MultiCall(methods=self.listattr(methname, plugins=[plugin]), - kwargs=kwargs, firstresult=True).execute() - -def canonical_importname(name): - if '.' in name: - return name - name = name.lower() - if not name.startswith(IMPORTPREFIX): - name = IMPORTPREFIX + name - return name - -def importplugin(importspec): - try: - return __import__(importspec, None, None, '__doc__') - except ImportError: - e = py.std.sys.exc_info()[1] - if str(e).find(importspec) == -1: - raise - name = importspec - try: - if name.startswith("pytest_"): - name = importspec[7:] - return __import__("pytest.plugin.%s" %(name), None, None, '__doc__') - except ImportError: - e = py.std.sys.exc_info()[1] - if str(e).find(name) == -1: - raise - # show the original exception, not the failing internal one - return __import__(importspec, None, None, '__doc__') - - -class MultiCall: - """ execute a call into multiple python functions/methods. """ - def __init__(self, methods, kwargs, firstresult=False): - self.methods = list(methods) - self.kwargs = kwargs - self.results = [] - self.firstresult = firstresult - - def __repr__(self): - status = "%d results, %d meths" % (len(self.results), len(self.methods)) - return "" %(status, self.kwargs) - - def execute(self): - while self.methods: - method = self.methods.pop() - kwargs = self.getkwargs(method) - res = method(**kwargs) - if res is not None: - self.results.append(res) - if self.firstresult: - return res - if not self.firstresult: - return self.results - - def getkwargs(self, method): - kwargs = {} - for argname in varnames(method): - try: - kwargs[argname] = self.kwargs[argname] - except KeyError: - if argname == "__multicall__": - kwargs[argname] = self - return kwargs - -def varnames(func): - if not inspect.isfunction(func) and not inspect.ismethod(func): - func = getattr(func, '__call__', func) - ismethod = inspect.ismethod(func) - rawcode = py.code.getrawcode(func) - try: - return rawcode.co_varnames[ismethod:rawcode.co_argcount] - except AttributeError: - return () - -class HookRelay: - def __init__(self, hookspecs, pm, prefix="pytest_"): - if not isinstance(hookspecs, list): - hookspecs = [hookspecs] - self._hookspecs = [] - self._pm = pm - for hookspec in hookspecs: - self._addhooks(hookspec, prefix) - - def _addhooks(self, hookspecs, prefix): - self._hookspecs.append(hookspecs) - added = False - for name, method in vars(hookspecs).items(): - if name.startswith(prefix): - if not method.__doc__: - raise ValueError("docstring required for hook %r, in %r" - % (method, hookspecs)) - firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(self, name, firstresult=firstresult) - setattr(self, name, hc) - added = True - #print ("setting new hook", name) - if not added: - raise ValueError("did not find new %r hooks in %r" %( - prefix, hookspecs,)) - - -class HookCaller: - def __init__(self, hookrelay, name, firstresult): - self.hookrelay = hookrelay - self.name = name - self.firstresult = firstresult - - def __repr__(self): - return "" %(self.name,) - - def __call__(self, **kwargs): - methods = self.hookrelay._pm.listattr(self.name) - mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - return mc.execute() - - def pcall(self, plugins, **kwargs): - methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) - mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - return mc.execute() - -pluginmanager = PluginManager(load=True) # will trigger default plugin importing - -def main(args=None): - global pluginmanager - if args is None: - args = sys.argv[1:] - hook = pluginmanager.hook - try: - config = hook.pytest_cmdline_parse( - pluginmanager=pluginmanager, args=args) - exitstatus = hook.pytest_cmdline_main(config=config) - except UsageError: - e = sys.exc_info()[1] - sys.stderr.write("ERROR: %s\n" %(e.args[0],)) - exitstatus = 3 - pluginmanager = PluginManager(load=True) - return exitstatus - -class UsageError(Exception): - """ error in py.test usage or invocation""" --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -1,7 +1,7 @@ import py import sys, os -from pytest._core import PluginManager +from pytest.main import PluginManager import pytest --- a/doc/goodpractises.txt +++ b/doc/goodpractises.txt @@ -76,16 +76,21 @@ You can always run your tests by pointin ... .. _`package name`: - -.. note:: + +.. note:: Test modules are imported under their fully qualified name as follows: - * ``basedir`` = first upward directory not containing an ``__init__.py`` + * find ``basedir`` -- this is the first "upward" directory not + containing an ``__init__.py`` - * perform ``sys.path.insert(0, basedir)``. + * perform ``sys.path.insert(0, basedir)`` to make the fully + qualified test module path importable. - * ``import path.to.test_module`` + * ``import path.to.test_module`` where the path is determined + by converting path separators into "." files. This means + you must follow the convention of having directory and file + names map to the import names. .. _standalone: .. _`genscript method`: @@ -94,7 +99,7 @@ Generating a py.test standalone Script ------------------------------------------- If you are a maintainer or application developer and want others -to easily run tests you can generate a completely standalone "py.test" +to easily run tests you can generate a completely standalone "py.test" script:: py.test --genscript=runtests.py --- a/setup.py +++ b/setup.py @@ -46,7 +46,7 @@ def main(): ) def cmdline_entrypoints(versioninfo, platform, basename): - target = 'pytest:__main__' + target = 'pytest:main' if platform.startswith('java'): points = {'py.test-jython': target} else: --- a/testing/test_pluginmanager.py +++ /dev/null @@ -1,604 +0,0 @@ -import py, os -from pytest._core import PluginManager, canonical_importname -from pytest._core import MultiCall, HookRelay, varnames - - -class TestBootstrapping: - def test_consider_env_fails_to_import(self, monkeypatch): - pluginmanager = PluginManager() - monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") - py.test.raises(ImportError, "pluginmanager.consider_env()") - - def test_preparse_args(self): - pluginmanager = PluginManager() - py.test.raises(ImportError, """ - pluginmanager.consider_preparse(["xyz", "-p", "hello123"]) - """) - - def test_plugin_skip(self, testdir, monkeypatch): - p = testdir.makepyfile(pytest_skipping1=""" - import py - py.test.skip("hello") - """) - p.copy(p.dirpath("pytest_skipping2.py")) - monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-p", "skipping1", "--traceconfig") - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*hint*skipping2*hello*", - "*hint*skipping1*hello*", - ]) - - def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(pytest_xy123="#") - monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') - l1 = len(pluginmanager.getplugins()) - pluginmanager.consider_env() - l2 = len(pluginmanager.getplugins()) - assert l2 == l1 + 1 - assert pluginmanager.getplugin('pytest_xy123') - pluginmanager.consider_env() - l3 = len(pluginmanager.getplugins()) - assert l2 == l3 - - def test_consider_setuptools_instantiation(self, monkeypatch): - pkg_resources = py.test.importorskip("pkg_resources") - def my_iter(name): - assert name == "pytest11" - class EntryPoint: - name = "mytestplugin" - def load(self): - class PseudoPlugin: - x = 42 - return PseudoPlugin() - return iter([EntryPoint()]) - - monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) - pluginmanager = PluginManager() - pluginmanager.consider_setuptools_entrypoints() - plugin = pluginmanager.getplugin("mytestplugin") - assert plugin.x == 42 - plugin2 = pluginmanager.getplugin("pytest_mytestplugin") - assert plugin2 == plugin - - def test_consider_setuptools_not_installed(self, monkeypatch): - monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', - py.std.types.ModuleType("pkg_resources")) - pluginmanager = PluginManager() - pluginmanager.consider_setuptools_entrypoints() - # ok, we did not explode - - def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): - x500 = testdir.makepyfile(pytest_x500="#") - p = testdir.makepyfile(""" - import py - def test_hello(pytestconfig): - plugin = pytestconfig.pluginmanager.getplugin('x500') - assert plugin is not None - """) - monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") - result = testdir.runpytest(p) - assert result.ret == 0 - result.stdout.fnmatch_lines(["*1 passed in*"]) - - def test_import_plugin_importname(self, testdir): - pluginmanager = PluginManager() - py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') - - reset = testdir.syspathinsert() - pluginname = "pytest_hello" - testdir.makepyfile(**{pluginname: ""}) - pluginmanager.import_plugin("hello") - len1 = len(pluginmanager.getplugins()) - pluginmanager.import_plugin("pytest_hello") - len2 = len(pluginmanager.getplugins()) - assert len1 == len2 - plugin1 = pluginmanager.getplugin("pytest_hello") - assert plugin1.__name__.endswith('pytest_hello') - plugin2 = pluginmanager.getplugin("hello") - assert plugin2 is plugin1 - - def test_import_plugin_dotted_name(self, testdir): - pluginmanager = PluginManager() - py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') - - reset = testdir.syspathinsert() - testdir.mkpydir("pkg").join("plug.py").write("x=3") - pluginname = "pkg.plug" - pluginmanager.import_plugin(pluginname) - mod = pluginmanager.getplugin("pkg.plug") - assert mod.x == 3 - - def test_consider_module(self, testdir): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(pytest_plug1="#") - testdir.makepyfile(pytest_plug2="#") - mod = py.std.types.ModuleType("temp") - mod.pytest_plugins = ["pytest_plug1", "pytest_plug2"] - pluginmanager.consider_module(mod) - assert pluginmanager.getplugin("plug1").__name__ == "pytest_plug1" - assert pluginmanager.getplugin("plug2").__name__ == "pytest_plug2" - - def test_consider_module_import_module(self, testdir): - mod = py.std.types.ModuleType("x") - mod.pytest_plugins = "pytest_a" - aplugin = testdir.makepyfile(pytest_a="#") - pluginmanager = PluginManager() - reprec = testdir.getreportrecorder(pluginmanager) - #syspath.prepend(aplugin.dirpath()) - py.std.sys.path.insert(0, str(aplugin.dirpath())) - pluginmanager.consider_module(mod) - call = reprec.getcall(pluginmanager.hook.pytest_plugin_registered.name) - assert call.plugin.__name__ == "pytest_a" - - # check that it is not registered twice - pluginmanager.consider_module(mod) - l = reprec.getcalls("pytest_plugin_registered") - assert len(l) == 1 - - def test_config_sets_conftesthandle_onimport(self, testdir): - config = testdir.parseconfig([]) - assert config._conftest._onimport == config._onimportconftest - - def test_consider_conftest_deps(self, testdir): - mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() - pp = PluginManager() - py.test.raises(ImportError, "pp.consider_conftest(mod)") - - def test_pm(self): - pp = PluginManager() - class A: pass - a1, a2 = A(), A() - pp.register(a1) - assert pp.isregistered(a1) - pp.register(a2, "hello") - assert pp.isregistered(a2) - l = pp.getplugins() - assert a1 in l - assert a2 in l - assert pp.getplugin('hello') == a2 - pp.unregister(a1) - assert not pp.isregistered(a1) - pp.unregister(name="hello") - assert not pp.isregistered(a2) - - def test_pm_ordering(self): - pp = PluginManager() - class A: pass - a1, a2 = A(), A() - pp.register(a1) - pp.register(a2, "hello") - l = pp.getplugins() - assert l.index(a1) < l.index(a2) - a3 = A() - pp.register(a3, prepend=True) - l = pp.getplugins() - assert l.index(a3) == 0 - - def test_register_imported_modules(self): - pp = PluginManager() - mod = py.std.types.ModuleType("x.y.pytest_hello") - pp.register(mod) - assert pp.isregistered(mod) - l = pp.getplugins() - assert mod in l - py.test.raises(AssertionError, "pp.register(mod)") - mod2 = py.std.types.ModuleType("pytest_hello") - #pp.register(mod2) # double pm - py.test.raises(AssertionError, "pp.register(mod)") - #assert not pp.isregistered(mod2) - assert pp.getplugins() == l - - def test_canonical_import(self, monkeypatch): - mod = py.std.types.ModuleType("pytest_xyz") - monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) - pp = PluginManager() - pp.import_plugin('xyz') - assert pp.getplugin('xyz') == mod - assert pp.getplugin('pytest_xyz') == mod - assert pp.isregistered(mod) - - def test_register_mismatch_method(self): - pp = PluginManager(load=True) - class hello: - def pytest_gurgel(self): - pass - py.test.raises(Exception, "pp.register(hello())") - - def test_register_mismatch_arg(self): - pp = PluginManager(load=True) - class hello: - def pytest_configure(self, asd): - pass - excinfo = py.test.raises(Exception, "pp.register(hello())") - - def test_canonical_importname(self): - for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': - impname = canonical_importname(name) - - def test_notify_exception(self, capfd): - pp = PluginManager() - excinfo = py.test.raises(ValueError, "raise ValueError(1)") - pp.notify_exception(excinfo) - out, err = capfd.readouterr() - assert "ValueError" in err - class A: - def pytest_internalerror(self, excrepr): - return True - pp.register(A()) - pp.notify_exception(excinfo) - out, err = capfd.readouterr() - assert not err - - def test_register(self): - pm = PluginManager(load=False) - class MyPlugin: - pass - my = MyPlugin() - pm.register(my) - assert pm.getplugins() - my2 = MyPlugin() - pm.register(my2) - assert pm.getplugins()[1:] == [my, my2] - - assert pm.isregistered(my) - assert pm.isregistered(my2) - pm.unregister(my) - assert not pm.isregistered(my) - assert pm.getplugins()[1:] == [my2] - - def test_listattr(self): - plugins = PluginManager() - class api1: - x = 41 - class api2: - x = 42 - class api3: - x = 43 - plugins.register(api1()) - plugins.register(api2()) - plugins.register(api3()) - l = list(plugins.listattr('x')) - assert l == [41, 42, 43] - - def test_register_trace(self): - pm = PluginManager() - class api1: - x = 41 - l = [] - pm.trace.setmyprocessor(lambda kw, args: l.append((kw, args))) - p = api1() - pm.register(p) - assert len(l) == 1 - kw, args = l[0] - assert args[0] == "registered" - assert args[1] == p - -class TestPytestPluginInteractions: - - def test_addhooks_conftestplugin(self, testdir): - newhooks = testdir.makepyfile(newhooks=""" - def pytest_myhook(xyz): - "new hook" - """) - conf = testdir.makeconftest(""" - import sys ; sys.path.insert(0, '.') - import newhooks - def pytest_addhooks(pluginmanager): - pluginmanager.addhooks(newhooks) - def pytest_myhook(xyz): - return xyz + 1 - """) - config = testdir.Config() - config._conftest.importconftest(conf) - print(config.pluginmanager.getplugins()) - res = config.hook.pytest_myhook(xyz=10) - assert res == [11] - - def test_addhooks_docstring_error(self, testdir): - newhooks = testdir.makepyfile(newhooks=""" - class A: # no pytest_ prefix - pass - def pytest_myhook(xyz): - pass - """) - conf = testdir.makeconftest(""" - import sys ; sys.path.insert(0, '.') - import newhooks - def pytest_addhooks(pluginmanager): - pluginmanager.addhooks(newhooks) - """) - res = testdir.runpytest() - assert res.ret != 0 - res.stderr.fnmatch_lines([ - "*docstring*pytest_myhook*newhooks*" - ]) - - def test_addhooks_nohooks(self, testdir): - conf = testdir.makeconftest(""" - import sys - def pytest_addhooks(pluginmanager): - pluginmanager.addhooks(sys) - """) - res = testdir.runpytest() - assert res.ret != 0 - res.stderr.fnmatch_lines([ - "*did not find*sys*" - ]) - - def test_do_option_conftestplugin(self, testdir): - p = testdir.makepyfile(""" - def pytest_addoption(parser): - parser.addoption('--test123', action="store_true") - """) - config = testdir.Config() - config._conftest.importconftest(p) - print(config.pluginmanager.getplugins()) - config.parse([]) - assert not config.option.test123 - - def test_namespace_early_from_import(self, testdir): - p = testdir.makepyfile(""" - from py.test.collect import Item - from pytest.collect import Item as Item2 - assert Item is Item2 - """) - result = testdir.runpython(p) - assert result.ret == 0 - - def test_do_ext_namespace(self, testdir): - testdir.makeconftest(""" - def pytest_namespace(): - return {'hello': 'world'} - """) - p = testdir.makepyfile(""" - from py.test import hello - import py - def test_hello(): - assert hello == "world" - assert 'hello' in py.test.__all__ - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) - - def test_do_option_postinitialize(self, testdir): - config = testdir.Config() - config.parse([]) - config.pluginmanager.do_configure(config=config) - assert not hasattr(config.option, 'test123') - p = testdir.makepyfile(""" - def pytest_addoption(parser): - parser.addoption('--test123', action="store_true", - default=True) - """) - config._conftest.importconftest(p) - assert config.option.test123 - - def test_configure(self, testdir): - config = testdir.parseconfig() - l = [] - class A: - def pytest_configure(self, config): - l.append(self) - - config.pluginmanager.register(A()) - assert len(l) == 0 - config.pluginmanager.do_configure(config=config) - assert len(l) == 1 - config.pluginmanager.register(A()) # this should lead to a configured() plugin - assert len(l) == 2 - assert l[0] != l[1] - - config.pluginmanager.do_unconfigure(config=config) - config.pluginmanager.register(A()) - assert len(l) == 2 - - # lower level API - - def test_listattr(self): - pluginmanager = PluginManager() - class My2: - x = 42 - pluginmanager.register(My2()) - assert not pluginmanager.listattr("hello") - assert pluginmanager.listattr("x") == [42] - -def test_namespace_has_default_and_env_plugins(testdir): - p = testdir.makepyfile(""" - import py - py.test.mark - """) - result = testdir.runpython(p) - assert result.ret == 0 - -def test_varnames(): - def f(x): - i = 3 - class A: - def f(self, y): - pass - class B(object): - def __call__(self, z): - pass - assert varnames(f) == ("x",) - assert varnames(A().f) == ('y',) - assert varnames(B()) == ('z',) - -class TestMultiCall: - def test_uses_copy_of_methods(self): - l = [lambda: 42] - mc = MultiCall(l, {}) - repr(mc) - l[:] = [] - res = mc.execute() - return res == 42 - - def test_call_passing(self): - class P1: - def m(self, __multicall__, x): - assert len(__multicall__.results) == 1 - assert not __multicall__.methods - return 17 - - class P2: - def m(self, __multicall__, x): - assert __multicall__.results == [] - assert __multicall__.methods - return 23 - - p1 = P1() - p2 = P2() - multicall = MultiCall([p1.m, p2.m], {'x': 23}) - assert "23" in repr(multicall) - reslist = multicall.execute() - assert len(reslist) == 2 - # ensure reversed order - assert reslist == [23, 17] - - def test_keyword_args(self): - def f(x): - return x + 1 - class A: - def f(self, x, y): - return x + y - multicall = MultiCall([f, A().f], dict(x=23, y=24)) - assert "'x': 23" in repr(multicall) - assert "'y': 24" in repr(multicall) - reslist = multicall.execute() - assert reslist == [24+23, 24] - assert "2 results" in repr(multicall) - - def test_keyword_args_with_defaultargs(self): - def f(x, z=1): - return x + z - reslist = MultiCall([f], dict(x=23, y=24)).execute() - assert reslist == [24] - reslist = MultiCall([f], dict(x=23, z=2)).execute() - assert reslist == [25] - - def test_tags_call_error(self): - multicall = MultiCall([lambda x: x], {}) - py.test.raises(TypeError, "multicall.execute()") - - def test_call_subexecute(self): - def m(__multicall__): - subresult = __multicall__.execute() - return subresult + 1 - - def n(): - return 1 - - call = MultiCall([n, m], {}, firstresult=True) - res = call.execute() - assert res == 2 - - def test_call_none_is_no_result(self): - def m1(): - return 1 - def m2(): - return None - res = MultiCall([m1, m2], {}, firstresult=True).execute() - assert res == 1 - res = MultiCall([m1, m2], {}).execute() - assert res == [1] - -class TestHookRelay: - def test_happypath(self): - pm = PluginManager() - class Api: - def hello(self, arg): - "api hook 1" - - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - assert hasattr(mcm, 'hello') - assert repr(mcm.hello).find("hello") != -1 - class Plugin: - def hello(self, arg): - return arg + 1 - pm.register(Plugin()) - l = mcm.hello(arg=3) - assert l == [4] - assert not hasattr(mcm, 'world') - - def test_only_kwargs(self): - pm = PluginManager() - class Api: - def hello(self, arg): - "api hook 1" - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - py.test.raises(TypeError, "mcm.hello(3)") - - def test_firstresult_definition(self): - pm = PluginManager() - class Api: - def hello(self, arg): - "api hook 1" - hello.firstresult = True - - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - class Plugin: - def hello(self, arg): - return arg + 1 - pm.register(Plugin()) - res = mcm.hello(arg=3) - assert res == 4 - -class TestTracer: - def test_simple(self): - from pytest._core import TagTracer - rootlogger = TagTracer() - log = rootlogger.get("pytest") - log("hello") - l = [] - rootlogger.setwriter(l.append) - log("world") - assert len(l) == 1 - assert l[0] == "[pytest] world\n" - sublog = log.get("collection") - sublog("hello") - assert l[1] == "[pytest:collection] hello\n" - - def test_setprocessor(self): - from pytest._core import TagTracer - rootlogger = TagTracer() - log = rootlogger.get("1") - log2 = log.get("2") - assert log2.tags == tuple("12") - l = [] - rootlogger.setprocessor(tuple("12"), lambda *args: l.append(args)) - log("not seen") - log2("seen") - assert len(l) == 1 - tags, args = l[0] - assert "1" in tags - assert "2" in tags - assert args == ("seen",) - l2 = [] - rootlogger.setprocessor("1:2", lambda *args: l2.append(args)) - log2("seen") - tags, args = l2[0] - assert args == ("seen",) - - - def test_setmyprocessor(self): - from pytest._core import TagTracer - rootlogger = TagTracer() - log = rootlogger.get("1") - log2 = log.get("2") - l = [] - log2.setmyprocessor(lambda *args: l.append(args)) - log("not seen") - assert not l - log2(42) - assert len(l) == 1 - tags, args = l[0] - assert "1" in tags - assert "2" in tags - assert args == (42,) --- /dev/null +++ b/testing/test_main.py @@ -0,0 +1,604 @@ +import py, os +from pytest.main import PluginManager, canonical_importname +from pytest.main import MultiCall, HookRelay, varnames + + +class TestBootstrapping: + def test_consider_env_fails_to_import(self, monkeypatch): + pluginmanager = PluginManager() + monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") + py.test.raises(ImportError, "pluginmanager.consider_env()") + + def test_preparse_args(self): + pluginmanager = PluginManager() + py.test.raises(ImportError, """ + pluginmanager.consider_preparse(["xyz", "-p", "hello123"]) + """) + + def test_plugin_skip(self, testdir, monkeypatch): + p = testdir.makepyfile(pytest_skipping1=""" + import py + py.test.skip("hello") + """) + p.copy(p.dirpath("pytest_skipping2.py")) + monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") + result = testdir.runpytest("-p", "skipping1", "--traceconfig") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*hint*skipping2*hello*", + "*hint*skipping1*hello*", + ]) + + def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): + pluginmanager = PluginManager() + testdir.syspathinsert() + testdir.makepyfile(pytest_xy123="#") + monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') + l1 = len(pluginmanager.getplugins()) + pluginmanager.consider_env() + l2 = len(pluginmanager.getplugins()) + assert l2 == l1 + 1 + assert pluginmanager.getplugin('pytest_xy123') + pluginmanager.consider_env() + l3 = len(pluginmanager.getplugins()) + assert l2 == l3 + + def test_consider_setuptools_instantiation(self, monkeypatch): + pkg_resources = py.test.importorskip("pkg_resources") + def my_iter(name): + assert name == "pytest11" + class EntryPoint: + name = "mytestplugin" + def load(self): + class PseudoPlugin: + x = 42 + return PseudoPlugin() + return iter([EntryPoint()]) + + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + pluginmanager = PluginManager() + pluginmanager.consider_setuptools_entrypoints() + plugin = pluginmanager.getplugin("mytestplugin") + assert plugin.x == 42 + plugin2 = pluginmanager.getplugin("pytest_mytestplugin") + assert plugin2 == plugin + + def test_consider_setuptools_not_installed(self, monkeypatch): + monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', + py.std.types.ModuleType("pkg_resources")) + pluginmanager = PluginManager() + pluginmanager.consider_setuptools_entrypoints() + # ok, we did not explode + + def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): + x500 = testdir.makepyfile(pytest_x500="#") + p = testdir.makepyfile(""" + import py + def test_hello(pytestconfig): + plugin = pytestconfig.pluginmanager.getplugin('x500') + assert plugin is not None + """) + monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") + result = testdir.runpytest(p) + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed in*"]) + + def test_import_plugin_importname(self, testdir): + pluginmanager = PluginManager() + py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') + + reset = testdir.syspathinsert() + pluginname = "pytest_hello" + testdir.makepyfile(**{pluginname: ""}) + pluginmanager.import_plugin("hello") + len1 = len(pluginmanager.getplugins()) + pluginmanager.import_plugin("pytest_hello") + len2 = len(pluginmanager.getplugins()) + assert len1 == len2 + plugin1 = pluginmanager.getplugin("pytest_hello") + assert plugin1.__name__.endswith('pytest_hello') + plugin2 = pluginmanager.getplugin("hello") + assert plugin2 is plugin1 + + def test_import_plugin_dotted_name(self, testdir): + pluginmanager = PluginManager() + py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') + + reset = testdir.syspathinsert() + testdir.mkpydir("pkg").join("plug.py").write("x=3") + pluginname = "pkg.plug" + pluginmanager.import_plugin(pluginname) + mod = pluginmanager.getplugin("pkg.plug") + assert mod.x == 3 + + def test_consider_module(self, testdir): + pluginmanager = PluginManager() + testdir.syspathinsert() + testdir.makepyfile(pytest_plug1="#") + testdir.makepyfile(pytest_plug2="#") + mod = py.std.types.ModuleType("temp") + mod.pytest_plugins = ["pytest_plug1", "pytest_plug2"] + pluginmanager.consider_module(mod) + assert pluginmanager.getplugin("plug1").__name__ == "pytest_plug1" + assert pluginmanager.getplugin("plug2").__name__ == "pytest_plug2" + + def test_consider_module_import_module(self, testdir): + mod = py.std.types.ModuleType("x") + mod.pytest_plugins = "pytest_a" + aplugin = testdir.makepyfile(pytest_a="#") + pluginmanager = PluginManager() + reprec = testdir.getreportrecorder(pluginmanager) + #syspath.prepend(aplugin.dirpath()) + py.std.sys.path.insert(0, str(aplugin.dirpath())) + pluginmanager.consider_module(mod) + call = reprec.getcall(pluginmanager.hook.pytest_plugin_registered.name) + assert call.plugin.__name__ == "pytest_a" + + # check that it is not registered twice + pluginmanager.consider_module(mod) + l = reprec.getcalls("pytest_plugin_registered") + assert len(l) == 1 + + def test_config_sets_conftesthandle_onimport(self, testdir): + config = testdir.parseconfig([]) + assert config._conftest._onimport == config._onimportconftest + + def test_consider_conftest_deps(self, testdir): + mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() + pp = PluginManager() + py.test.raises(ImportError, "pp.consider_conftest(mod)") + + def test_pm(self): + pp = PluginManager() + class A: pass + a1, a2 = A(), A() + pp.register(a1) + assert pp.isregistered(a1) + pp.register(a2, "hello") + assert pp.isregistered(a2) + l = pp.getplugins() + assert a1 in l + assert a2 in l + assert pp.getplugin('hello') == a2 + pp.unregister(a1) + assert not pp.isregistered(a1) + pp.unregister(name="hello") + assert not pp.isregistered(a2) + + def test_pm_ordering(self): + pp = PluginManager() + class A: pass + a1, a2 = A(), A() + pp.register(a1) + pp.register(a2, "hello") + l = pp.getplugins() + assert l.index(a1) < l.index(a2) + a3 = A() + pp.register(a3, prepend=True) + l = pp.getplugins() + assert l.index(a3) == 0 + + def test_register_imported_modules(self): + pp = PluginManager() + mod = py.std.types.ModuleType("x.y.pytest_hello") + pp.register(mod) + assert pp.isregistered(mod) + l = pp.getplugins() + assert mod in l + py.test.raises(AssertionError, "pp.register(mod)") + mod2 = py.std.types.ModuleType("pytest_hello") + #pp.register(mod2) # double pm + py.test.raises(AssertionError, "pp.register(mod)") + #assert not pp.isregistered(mod2) + assert pp.getplugins() == l + + def test_canonical_import(self, monkeypatch): + mod = py.std.types.ModuleType("pytest_xyz") + monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) + pp = PluginManager() + pp.import_plugin('xyz') + assert pp.getplugin('xyz') == mod + assert pp.getplugin('pytest_xyz') == mod + assert pp.isregistered(mod) + + def test_register_mismatch_method(self): + pp = PluginManager(load=True) + class hello: + def pytest_gurgel(self): + pass + py.test.raises(Exception, "pp.register(hello())") + + def test_register_mismatch_arg(self): + pp = PluginManager(load=True) + class hello: + def pytest_configure(self, asd): + pass + excinfo = py.test.raises(Exception, "pp.register(hello())") + + def test_canonical_importname(self): + for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': + impname = canonical_importname(name) + + def test_notify_exception(self, capfd): + pp = PluginManager() + excinfo = py.test.raises(ValueError, "raise ValueError(1)") + pp.notify_exception(excinfo) + out, err = capfd.readouterr() + assert "ValueError" in err + class A: + def pytest_internalerror(self, excrepr): + return True + pp.register(A()) + pp.notify_exception(excinfo) + out, err = capfd.readouterr() + assert not err + + def test_register(self): + pm = PluginManager(load=False) + class MyPlugin: + pass + my = MyPlugin() + pm.register(my) + assert pm.getplugins() + my2 = MyPlugin() + pm.register(my2) + assert pm.getplugins()[1:] == [my, my2] + + assert pm.isregistered(my) + assert pm.isregistered(my2) + pm.unregister(my) + assert not pm.isregistered(my) + assert pm.getplugins()[1:] == [my2] + + def test_listattr(self): + plugins = PluginManager() + class api1: + x = 41 + class api2: + x = 42 + class api3: + x = 43 + plugins.register(api1()) + plugins.register(api2()) + plugins.register(api3()) + l = list(plugins.listattr('x')) + assert l == [41, 42, 43] + + def test_register_trace(self): + pm = PluginManager() + class api1: + x = 41 + l = [] + pm.trace.setmyprocessor(lambda kw, args: l.append((kw, args))) + p = api1() + pm.register(p) + assert len(l) == 1 + kw, args = l[0] + assert args[0] == "registered" + assert args[1] == p + +class TestPytestPluginInteractions: + + def test_addhooks_conftestplugin(self, testdir): + newhooks = testdir.makepyfile(newhooks=""" + def pytest_myhook(xyz): + "new hook" + """) + conf = testdir.makeconftest(""" + import sys ; sys.path.insert(0, '.') + import newhooks + def pytest_addhooks(pluginmanager): + pluginmanager.addhooks(newhooks) + def pytest_myhook(xyz): + return xyz + 1 + """) + config = testdir.Config() + config._conftest.importconftest(conf) + print(config.pluginmanager.getplugins()) + res = config.hook.pytest_myhook(xyz=10) + assert res == [11] + + def test_addhooks_docstring_error(self, testdir): + newhooks = testdir.makepyfile(newhooks=""" + class A: # no pytest_ prefix + pass + def pytest_myhook(xyz): + pass + """) + conf = testdir.makeconftest(""" + import sys ; sys.path.insert(0, '.') + import newhooks + def pytest_addhooks(pluginmanager): + pluginmanager.addhooks(newhooks) + """) + res = testdir.runpytest() + assert res.ret != 0 + res.stderr.fnmatch_lines([ + "*docstring*pytest_myhook*newhooks*" + ]) + + def test_addhooks_nohooks(self, testdir): + conf = testdir.makeconftest(""" + import sys + def pytest_addhooks(pluginmanager): + pluginmanager.addhooks(sys) + """) + res = testdir.runpytest() + assert res.ret != 0 + res.stderr.fnmatch_lines([ + "*did not find*sys*" + ]) + + def test_do_option_conftestplugin(self, testdir): + p = testdir.makepyfile(""" + def pytest_addoption(parser): + parser.addoption('--test123', action="store_true") + """) + config = testdir.Config() + config._conftest.importconftest(p) + print(config.pluginmanager.getplugins()) + config.parse([]) + assert not config.option.test123 + + def test_namespace_early_from_import(self, testdir): + p = testdir.makepyfile(""" + from py.test.collect import Item + from pytest.collect import Item as Item2 + assert Item is Item2 + """) + result = testdir.runpython(p) + assert result.ret == 0 + + def test_do_ext_namespace(self, testdir): + testdir.makeconftest(""" + def pytest_namespace(): + return {'hello': 'world'} + """) + p = testdir.makepyfile(""" + from py.test import hello + import py + def test_hello(): + assert hello == "world" + assert 'hello' in py.test.__all__ + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + + def test_do_option_postinitialize(self, testdir): + config = testdir.Config() + config.parse([]) + config.pluginmanager.do_configure(config=config) + assert not hasattr(config.option, 'test123') + p = testdir.makepyfile(""" + def pytest_addoption(parser): + parser.addoption('--test123', action="store_true", + default=True) + """) + config._conftest.importconftest(p) + assert config.option.test123 + + def test_configure(self, testdir): + config = testdir.parseconfig() + l = [] + class A: + def pytest_configure(self, config): + l.append(self) + + config.pluginmanager.register(A()) + assert len(l) == 0 + config.pluginmanager.do_configure(config=config) + assert len(l) == 1 + config.pluginmanager.register(A()) # this should lead to a configured() plugin + assert len(l) == 2 + assert l[0] != l[1] + + config.pluginmanager.do_unconfigure(config=config) + config.pluginmanager.register(A()) + assert len(l) == 2 + + # lower level API + + def test_listattr(self): + pluginmanager = PluginManager() + class My2: + x = 42 + pluginmanager.register(My2()) + assert not pluginmanager.listattr("hello") + assert pluginmanager.listattr("x") == [42] + +def test_namespace_has_default_and_env_plugins(testdir): + p = testdir.makepyfile(""" + import py + py.test.mark + """) + result = testdir.runpython(p) + assert result.ret == 0 + +def test_varnames(): + def f(x): + i = 3 + class A: + def f(self, y): + pass + class B(object): + def __call__(self, z): + pass + assert varnames(f) == ("x",) + assert varnames(A().f) == ('y',) + assert varnames(B()) == ('z',) + +class TestMultiCall: + def test_uses_copy_of_methods(self): + l = [lambda: 42] + mc = MultiCall(l, {}) + repr(mc) + l[:] = [] + res = mc.execute() + return res == 42 + + def test_call_passing(self): + class P1: + def m(self, __multicall__, x): + assert len(__multicall__.results) == 1 + assert not __multicall__.methods + return 17 + + class P2: + def m(self, __multicall__, x): + assert __multicall__.results == [] + assert __multicall__.methods + return 23 + + p1 = P1() + p2 = P2() + multicall = MultiCall([p1.m, p2.m], {'x': 23}) + assert "23" in repr(multicall) + reslist = multicall.execute() + assert len(reslist) == 2 + # ensure reversed order + assert reslist == [23, 17] + + def test_keyword_args(self): + def f(x): + return x + 1 + class A: + def f(self, x, y): + return x + y + multicall = MultiCall([f, A().f], dict(x=23, y=24)) + assert "'x': 23" in repr(multicall) + assert "'y': 24" in repr(multicall) + reslist = multicall.execute() + assert reslist == [24+23, 24] + assert "2 results" in repr(multicall) + + def test_keyword_args_with_defaultargs(self): + def f(x, z=1): + return x + z + reslist = MultiCall([f], dict(x=23, y=24)).execute() + assert reslist == [24] + reslist = MultiCall([f], dict(x=23, z=2)).execute() + assert reslist == [25] + + def test_tags_call_error(self): + multicall = MultiCall([lambda x: x], {}) + py.test.raises(TypeError, "multicall.execute()") + + def test_call_subexecute(self): + def m(__multicall__): + subresult = __multicall__.execute() + return subresult + 1 + + def n(): + return 1 + + call = MultiCall([n, m], {}, firstresult=True) + res = call.execute() + assert res == 2 + + def test_call_none_is_no_result(self): + def m1(): + return 1 + def m2(): + return None + res = MultiCall([m1, m2], {}, firstresult=True).execute() + assert res == 1 + res = MultiCall([m1, m2], {}).execute() + assert res == [1] + +class TestHookRelay: + def test_happypath(self): + pm = PluginManager() + class Api: + def hello(self, arg): + "api hook 1" + + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") + assert hasattr(mcm, 'hello') + assert repr(mcm.hello).find("hello") != -1 + class Plugin: + def hello(self, arg): + return arg + 1 + pm.register(Plugin()) + l = mcm.hello(arg=3) + assert l == [4] + assert not hasattr(mcm, 'world') + + def test_only_kwargs(self): + pm = PluginManager() + class Api: + def hello(self, arg): + "api hook 1" + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") + py.test.raises(TypeError, "mcm.hello(3)") + + def test_firstresult_definition(self): + pm = PluginManager() + class Api: + def hello(self, arg): + "api hook 1" + hello.firstresult = True + + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") + class Plugin: + def hello(self, arg): + return arg + 1 + pm.register(Plugin()) + res = mcm.hello(arg=3) + assert res == 4 + +class TestTracer: + def test_simple(self): + from pytest.main import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("pytest") + log("hello") + l = [] + rootlogger.setwriter(l.append) + log("world") + assert len(l) == 1 + assert l[0] == "[pytest] world\n" + sublog = log.get("collection") + sublog("hello") + assert l[1] == "[pytest:collection] hello\n" + + def test_setprocessor(self): + from pytest.main import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + assert log2.tags == tuple("12") + l = [] + rootlogger.setprocessor(tuple("12"), lambda *args: l.append(args)) + log("not seen") + log2("seen") + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == ("seen",) + l2 = [] + rootlogger.setprocessor("1:2", lambda *args: l2.append(args)) + log2("seen") + tags, args = l2[0] + assert args == ("seen",) + + + def test_setmyprocessor(self): + from pytest.main import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + l = [] + log2.setmyprocessor(lambda *args: l.append(args)) + log("not seen") + assert not l + log2(42) + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == (42,) --- a/CHANGELOG +++ b/CHANGELOG @@ -2,6 +2,9 @@ Changes between 1.3.4 and 2.0.0dev0 ---------------------------------------------- - pytest-2.0 is now its own package and depends on pylib-2.0 +- new ability: python -m pytest / python -m pytest.main ability +- new python invcation: pytest.main(args, plugins) to load + some custom plugins early. - try harder to run unittest test suites in a more compatible manner by deferring setup/teardown semantics to the unittest package. - introduce a new way to set config options via ini-style files, --- a/doc/customize.txt +++ b/doc/customize.txt @@ -58,8 +58,8 @@ builtin configuration file options .. confval:: norecursedirs Set the directory basename patterns to avoid when recursing - for test discovery. The individual (fnmatch-style) patterns are - applied to the basename of a directory to decide if to recurse into it. + for test discovery. The individual (fnmatch-style) patterns are + applied to the basename of a directory to decide if to recurse into it. Pattern matching characters:: * matches everything @@ -68,7 +68,7 @@ builtin configuration file options [!seq] matches any char not in seq Default patterns are ``.* _* CVS {args}``. Setting a ``norecurse`` - replaces the default. Here is a customizing example for avoiding + replaces the default. Here is a customizing example for avoiding a different set of directories:: # content of setup.cfg --- a/doc/apiref.txt +++ b/doc/apiref.txt @@ -4,6 +4,7 @@ py.test reference documentation ================================================ + .. toctree:: :maxdepth: 2 --- /dev/null +++ b/doc/usage.txt @@ -0,0 +1,165 @@ + +.. _usage: + +Usage and Invocations +========================================== + + +.. _cmdline: + +Getting help on version, option names, environment vars +----------------------------------------------------------- + +:: + + py.test --version # shows where pytest was imported from + py.test --funcargs # show available builtin function arguments + py.test -h | --help # show help on command line and config file options + + +Stopping after the first (or N) failures +--------------------------------------------------- + +To stop the testing process after the first (N) failures:: + + py.test -x # stop after first failure + py.test -maxfail=2 # stop after two failures + +calling pytest from Python code +---------------------------------------------------- + +.. versionadded: 2.0 + +You can invoke ``py.test`` from Python code directly:: + + pytest.main() + +this acts as if you would call "py.test" from the command line. +It will not raise ``SystemExit`` but return the exitcode instead. +You can pass in options and arguments:: + + pytest.main(['x', 'mytestdir']) + +or pass in a string:: + + pytest.main("-x mytestdir") + +You can specify additional plugins to ``pytest.main``:: + + # content of myinvoke.py + import pytest + class MyPlugin: + def pytest_addoption(self, parser): + raise pytest.UsageError("hi from our plugin") + + pytest.main(plugins=[MyPlugin()]) + +Running it will exit quickly:: + + $ python myinvoke.py + ERROR: hi from our plugin + +calling pytest through ``python -m pytest`` +----------------------------------------------------- + +.. versionadded: 2.0 + +You can invoke testing through the Python interpreter from the command line:: + + python -m pytest.main [...] + +Python2.7 and Python3 introduced specifying packages to "-m" so there +you can also type:: + + python -m pytest [...] + +All of these invocations are equivalent to the ``py.test [...]`` command line invocation. + + +Modifying Python traceback printing +---------------------------------------------- + +Examples for modifying traceback printing:: + + py.test --showlocals # show local variables in tracebacks + py.test -l # show local variables (shortcut) + + py.test --tb=long # the default informative traceback formatting + py.test --tb=native # the Python standard library formatting + py.test --tb=short # a shorter traceback format + py.test --tb=line # only one line per failure + +Dropping to PDB (Python Debugger) on failures +---------------------------------------------- + +.. _PDB: http://docs.python.org/library/pdb.html + +Python comes with a builtin Python debugger called PDB_. ``py.test`` +allows to drop into the PDB prompt via a command line option:: + + py.test --pdb + +This will invoke the Python debugger on every failure. Often you might +only want to do this for the first failing test to understand a certain +failure situation:: + + py.test -x --pdb # drop to PDB on first failure, then end test session + py.test --pdb --maxfail=3 # drop to PDB for the first three failures + + +Setting a breakpoint / aka ``set_trace()`` +---------------------------------------------------- + +If you want to set a breakpoint and enter the ``pdb.set_trace()`` you +can use a helper:: + + def test_function(): + ... + py.test.set_trace() # invoke PDB debugger and tracing + +.. versionadded: 2.0.0 + +In previous versions you could only enter PDB tracing if +you :ref:`disable capturing`. + +creating JUnitXML format files +---------------------------------------------------- + +To create result files which can be read by Hudson_ or other Continous +integration servers, use this invocation:: + + py.test --junitxml=path + +to create an XML file at ``path``. + +creating resultlog format files +---------------------------------------------------- + +To create plain-text machine-readable result files you can issue:: + + py.test --resultlog=path + +and look at the content at the ``path`` location. Such files are used e.g. +by the `PyPy-test`_ web page to show test results over several revisions. + +.. _`PyPy-test`: http://codespeak.net:8099/summary + + +send test report to pocoo pastebin service +----------------------------------------------------- + +**Creating a URL for each test failure**:: + + py.test --pastebin=failed + +This will submit test run information to a remote Paste service and +provide a URL for each failure. You may select tests as usual or add +for example ``-x`` if you only want to send one particular failure. + +**Creating a URL for a whole test session log**:: + + py.test --pastebin=all + +Currently only pasting to the http://paste.pocoo.org service is implemented. + +.. include:: links.inc --- a/doc/overview.txt +++ b/doc/overview.txt @@ -7,7 +7,7 @@ Overview and Introduction features.txt getting-started.txt - cmdline.txt + usage.txt goodpractises.txt faq.txt --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -2,7 +2,7 @@ funcargs and support code for testing py.test's own functionality. """ -import py +import py, pytest import sys, os import re import inspect @@ -10,7 +10,7 @@ import time from fnmatch import fnmatch from pytest.plugin.session import Collection from py.builtin import print_ -from pytest._core import HookRelay +from pytest.main import HookRelay def pytest_addoption(parser): group = parser.getgroup("pylib") @@ -401,6 +401,10 @@ class TmpTestdir: #print "env", env return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) + def pytestmain(self, *args, **kwargs): + ret = pytest.main(*args, **kwargs) + if ret == 2: + raise KeyboardInterrupt() def run(self, *cmdargs): return self._run(*cmdargs) --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -1,4 +1,4 @@ -import sys, py +import sys, py, pytest class TestGeneralUsage: def test_config_error(self, testdir): @@ -82,36 +82,6 @@ class TestGeneralUsage: ]) - def test_earlyinit(self, testdir): - p = testdir.makepyfile(""" - import py - assert hasattr(py.test, 'mark') - """) - result = testdir.runpython(p) - assert result.ret == 0 - - def test_pydoc(self, testdir): - result = testdir.runpython_c("import py;help(py.test)") - assert result.ret == 0 - s = result.stdout.str() - assert 'MarkGenerator' in s - - def test_double_pytestcmdline(self, testdir): - p = testdir.makepyfile(run=""" - import py - py.test.cmdline.main() - py.test.cmdline.main() - """) - testdir.makepyfile(""" - def test_hello(): - pass - """) - result = testdir.runpython(p) - result.stdout.fnmatch_lines([ - "*1 passed*", - "*1 passed*", - ]) - @py.test.mark.xfail def test_early_skip(self, testdir): @@ -225,19 +195,6 @@ class TestGeneralUsage: "*1 pass*", ]) - - @py.test.mark.skipif("sys.version_info < (2,5)") - def test_python_minus_m_invocation_ok(self, testdir): - p1 = testdir.makepyfile("def test_hello(): pass") - res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) - assert res.ret == 0 - - @py.test.mark.skipif("sys.version_info < (2,5)") - def test_python_minus_m_invocation_fail(self, testdir): - p1 = testdir.makepyfile("def test_fail(): 0/0") - res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) - assert res.ret == 1 - def test_skip_on_generated_funcarg_id(self, testdir): testdir.makeconftest(""" import py @@ -253,3 +210,83 @@ class TestGeneralUsage: res = testdir.runpytest(p) assert res.ret == 0 res.stdout.fnmatch_lines(["*1 skipped*"]) + +class TestInvocationVariants: + def test_earlyinit(self, testdir): + p = testdir.makepyfile(""" + import py + assert hasattr(py.test, 'mark') + """) + result = testdir.runpython(p) + assert result.ret == 0 + + def test_pydoc(self, testdir): + result = testdir.runpython_c("import py;help(py.test)") + assert result.ret == 0 + s = result.stdout.str() + assert 'MarkGenerator' in s + + def test_double_pytestcmdline(self, testdir): + p = testdir.makepyfile(run=""" + import py + py.test.cmdline.main() + py.test.cmdline.main() + """) + testdir.makepyfile(""" + def test_hello(): + pass + """) + result = testdir.runpython(p) + result.stdout.fnmatch_lines([ + "*1 passed*", + "*1 passed*", + ]) + + @py.test.mark.skipif("sys.version_info < (2,5)") + def test_python_minus_m_invocation_ok(self, testdir): + p1 = testdir.makepyfile("def test_hello(): pass") + res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) + assert res.ret == 0 + + @py.test.mark.skipif("sys.version_info < (2,5)") + def test_python_minus_m_invocation_fail(self, testdir): + p1 = testdir.makepyfile("def test_fail(): 0/0") + res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) + assert res.ret == 1 + + def test_python_pytest_main(self, testdir): + p1 = testdir.makepyfile("def test_pass(): pass") + res = testdir.run(py.std.sys.executable, "-m", "pytest.main", str(p1)) + assert res.ret == 0 + res.stdout.fnmatch_lines(["*1 passed*"]) + + @py.test.mark.skipif("sys.version_info < (2,7)") + def test_python_pytest_package(self, testdir): + p1 = testdir.makepyfile("def test_pass(): pass") + res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1)) + assert res.ret == 0 + res.stdout.fnmatch_lines(["*1 passed*"]) + + def test_equivalence_pytest_pytest(self): + assert pytest.main == py.test.cmdline.main + + def test_invoke_with_string(self, capsys): + retcode = pytest.main("-h") + assert not retcode + out, err = capsys.readouterr() + assert "--help" in out + + def test_invoke_with_path(self, testdir, capsys): + retcode = testdir.pytestmain(testdir.tmpdir) + assert not retcode + out, err = capsys.readouterr() + + def test_invoke_plugin_api(self, capsys): + class MyPlugin: + def pytest_addoption(self, parser): + parser.addoption("--myopt") + + pytest.main(["-h"], plugins=[MyPlugin()]) + out, err = capsys.readouterr() + assert "--myopt" in out + --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -461,7 +461,7 @@ def hasinit(obj): def getfuncargnames(function): - # XXX merge with _core.py's varnames + # XXX merge with main.py's varnames argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] startindex = py.std.inspect.ismethod(function) and 1 or 0 defaults = getattr(function, 'func_defaults', --- a/doc/builtin.txt +++ b/doc/builtin.txt @@ -1,7 +1,19 @@ -pytest builtin helpers +py.test builtin helpers ================================================ +builtin py.test.* helpers +----------------------------------------------------- + +You can always use an interactive Python prompt and type:: + + import pytest + help(pytest) + +to get an overview on available globally available helpers. + +.. automodule:: pytest + :members: builtin function arguments ----------------------------------------------------- @@ -54,17 +66,3 @@ You can ask for available builtin or pro * ``pop(category=None)``: return last warning matching the category. * ``clear()``: clear list of warnings - -builtin py.test.* helpers ------------------------------------------------------ - -You can always use an interactive Python prompt and type:: - - import pytest - help(pytest) - -to get an overview on available globally available helpers. - -.. automodule:: pytest - :members: - --- a/testing/plugin/test_pytester.py +++ b/testing/plugin/test_pytester.py @@ -1,7 +1,7 @@ import py import os, sys from pytest.plugin.pytester import LineMatcher, LineComp, HookRecorder -from pytest._core import PluginManager +from pytest.main import PluginManager def test_reportrecorder(testdir): item = testdir.getitem("def test_func(): pass") @@ -97,7 +97,7 @@ def test_hookrecorder_basic_no_args_hook def test_functional(testdir, linecomp): reprec = testdir.inline_runsource(""" import py - from pytest._core import HookRelay, PluginManager + from pytest.main import HookRelay, PluginManager pytest_plugins="pytester" def test_func(_pytest): class ApiClass: --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -1,4 +1,4 @@ -""" hook specifications for pytest plugins, invoked from _core.py and builtin plugins. """ +""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ # ------------------------------------------------------------------------- # Initialization --- a/doc/features.txt +++ b/doc/features.txt @@ -5,8 +5,8 @@ no-boilerplate testing with Python ---------------------------------- - automatic, fully customizable Python test discovery -- :pep:`8` consistent testing style -- allows simple test functions +- allows fully :pep:`8` compliant coding style +- write simple test functions and freely group tests - ``assert`` statement for your assertions - powerful parametrization of test functions - rely on powerful traceback and assertion reporting @@ -25,8 +25,8 @@ extensive plugin and customization syste mature command line testing tool -------------------------------------- +- powerful :ref:`usage` possibilities - used in many projects, ranging from 10 to 10K tests -- autodiscovery of tests - simple well sorted command line options - runs on Unix, Windows from Python 2.4 up to Python 3.1 and 3.2 - is itself tested extensively on a CI server --- a/testing/plugin/conftest.py +++ b/testing/plugin/conftest.py @@ -2,7 +2,7 @@ import py import pytest.plugin plugindir = py.path.local(pytest.plugin.__file__).dirpath() -from pytest._core import default_plugins +from pytest.main import default_plugins def pytest_collect_file(path, parent): if path.basename.startswith("pytest_") and path.ext == ".py": --- /dev/null +++ b/pytest/main.py @@ -0,0 +1,412 @@ +""" +pytest PluginManager, basic initialization and tracing. +All else is in pytest/plugin. +(c) Holger Krekel 2004-2010 +""" +import sys, os +import inspect +import py +from pytest import hookspec # the extension point definitions + +assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " + "%s is too old, remove or upgrade 'py'" % (py.__version__)) + +default_plugins = ( + "config session terminal runner python pdb capture unittest mark skipping " + "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " + "junitxml doctest").split() + +IMPORTPREFIX = "pytest_" + +class TagTracer: + def __init__(self): + self._tag2proc = {} + self.writer = None + + def get(self, name): + return TagTracerSub(self, (name,)) + + def processmessage(self, tags, args): + if self.writer is not None: + prefix = ":".join(tags) + content = " ".join(map(str, args)) + self.writer("[%s] %s\n" %(prefix, content)) + try: + self._tag2proc[tags](tags, args) + except KeyError: + pass + + def setwriter(self, writer): + self.writer = writer + + def setprocessor(self, tags, processor): + if isinstance(tags, str): + tags = tuple(tags.split(":")) + else: + assert isinstance(tags, tuple) + self._tag2proc[tags] = processor + +class TagTracerSub: + def __init__(self, root, tags): + self.root = root + self.tags = tags + def __call__(self, *args): + self.root.processmessage(self.tags, args) + def setmyprocessor(self, processor): + self.root.setprocessor(self.tags, processor) + def get(self, name): + return self.__class__(self.root, self.tags + (name,)) + +class PluginManager(object): + def __init__(self, load=False): + self._name2plugin = {} + self._plugins = [] + self._hints = [] + self.trace = TagTracer().get("pytest") + if os.environ.get('PYTEST_DEBUG'): + self.trace.root.setwriter(sys.stderr.write) + self.hook = HookRelay([hookspec], pm=self) + self.register(self) + if load: + for spec in default_plugins: + self.import_plugin(spec) + + def _getpluginname(self, plugin, name): + if name is None: + if hasattr(plugin, '__name__'): + name = plugin.__name__.split(".")[-1] + else: + name = id(plugin) + return name + + def register(self, plugin, name=None, prepend=False): + assert not self.isregistered(plugin), plugin + assert not self.isregistered(plugin), plugin + name = self._getpluginname(plugin, name) + if name in self._name2plugin: + return False + self._name2plugin[name] = plugin + self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) + self.hook.pytest_plugin_registered(manager=self, plugin=plugin) + self.trace("registered", plugin) + if not prepend: + self._plugins.append(plugin) + else: + self._plugins.insert(0, plugin) + return True + + def unregister(self, plugin=None, name=None): + if plugin is None: + plugin = self.getplugin(name=name) + self._plugins.remove(plugin) + self.hook.pytest_plugin_unregistered(plugin=plugin) + for name, value in list(self._name2plugin.items()): + if value == plugin: + del self._name2plugin[name] + + def isregistered(self, plugin, name=None): + if self._getpluginname(plugin, name) in self._name2plugin: + return True + for val in self._name2plugin.values(): + if plugin == val: + return True + + def addhooks(self, spec): + self.hook._addhooks(spec, prefix="pytest_") + + def getplugins(self): + return list(self._plugins) + + def skipifmissing(self, name): + if not self.hasplugin(name): + py.test.skip("plugin %r is missing" % name) + + def hasplugin(self, name): + try: + self.getplugin(name) + return True + except KeyError: + return False + + def getplugin(self, name): + try: + return self._name2plugin[name] + except KeyError: + impname = canonical_importname(name) + return self._name2plugin[impname] + + # API for bootstrapping + # + def _envlist(self, varname): + val = py.std.os.environ.get(varname, None) + if val is not None: + return val.split(',') + return () + + def consider_env(self): + for spec in self._envlist("PYTEST_PLUGINS"): + self.import_plugin(spec) + + def consider_setuptools_entrypoints(self): + try: + from pkg_resources import iter_entry_points + except ImportError: + return # XXX issue a warning + for ep in iter_entry_points('pytest11'): + name = canonical_importname(ep.name) + if name in self._name2plugin: + continue + plugin = ep.load() + self.register(plugin, name=name) + + def consider_preparse(self, args): + for opt1,opt2 in zip(args, args[1:]): + if opt1 == "-p": + self.import_plugin(opt2) + + def consider_conftest(self, conftestmodule): + if self.register(conftestmodule, name=conftestmodule.__file__): + self.consider_module(conftestmodule) + + def consider_module(self, mod): + attr = getattr(mod, "pytest_plugins", ()) + if attr: + if not isinstance(attr, (list, tuple)): + attr = (attr,) + for spec in attr: + self.import_plugin(spec) + + def import_plugin(self, spec): + assert isinstance(spec, str) + modname = canonical_importname(spec) + if modname in self._name2plugin: + return + try: + mod = importplugin(modname) + except KeyboardInterrupt: + raise + except: + e = py.std.sys.exc_info()[1] + if not hasattr(py.test, 'skip'): + raise + elif not isinstance(e, py.test.skip.Exception): + raise + self._hints.append("skipped plugin %r: %s" %((modname, e.msg))) + else: + self.register(mod, modname) + self.consider_module(mod) + + def pytest_plugin_registered(self, plugin): + dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} + if dic: + self._setns(py.test, dic) + if hasattr(self, '_config'): + self.call_plugin(plugin, "pytest_addoption", + {'parser': self._config._parser}) + self.call_plugin(plugin, "pytest_configure", + {'config': self._config}) + + def _setns(self, obj, dic): + for name, value in dic.items(): + if isinstance(value, dict): + mod = getattr(obj, name, None) + if mod is None: + mod = py.std.types.ModuleType(name) + sys.modules['pytest.%s' % name] = mod + sys.modules['py.test.%s' % name] = mod + mod.__all__ = [] + setattr(obj, name, mod) + self._setns(mod, value) + else: + #print "setting", name, value, "on", obj + setattr(obj, name, value) + obj.__all__.append(name) + + def pytest_terminal_summary(self, terminalreporter): + tw = terminalreporter._tw + if terminalreporter.config.option.traceconfig: + for hint in self._hints: + tw.line("hint: %s" % hint) + + def do_configure(self, config): + assert not hasattr(self, '_config') + self._config = config + config.hook.pytest_configure(config=self._config) + + def do_unconfigure(self, config): + config = self._config + del self._config + config.hook.pytest_unconfigure(config=config) + config.pluginmanager.unregister(self) + + def notify_exception(self, excinfo): + excrepr = excinfo.getrepr(funcargs=True, showlocals=True) + res = self.hook.pytest_internalerror(excrepr=excrepr) + if not py.builtin.any(res): + for line in str(excrepr).split("\n"): + sys.stderr.write("INTERNALERROR> %s\n" %line) + sys.stderr.flush() + + def listattr(self, attrname, plugins=None): + if plugins is None: + plugins = self._plugins + l = [] + for plugin in plugins: + try: + l.append(getattr(plugin, attrname)) + except AttributeError: + continue + return l + + def call_plugin(self, plugin, methname, kwargs): + return MultiCall(methods=self.listattr(methname, plugins=[plugin]), + kwargs=kwargs, firstresult=True).execute() + +def canonical_importname(name): + if '.' in name: + return name + name = name.lower() + if not name.startswith(IMPORTPREFIX): + name = IMPORTPREFIX + name + return name + +def importplugin(importspec): + try: + return __import__(importspec, None, None, '__doc__') + except ImportError: + e = py.std.sys.exc_info()[1] + if str(e).find(importspec) == -1: + raise + name = importspec + try: + if name.startswith("pytest_"): + name = importspec[7:] + return __import__("pytest.plugin.%s" %(name), None, None, '__doc__') + except ImportError: + e = py.std.sys.exc_info()[1] + if str(e).find(name) == -1: + raise + # show the original exception, not the failing internal one + return __import__(importspec, None, None, '__doc__') + + +class MultiCall: + """ execute a call into multiple python functions/methods. """ + def __init__(self, methods, kwargs, firstresult=False): + self.methods = list(methods) + self.kwargs = kwargs + self.results = [] + self.firstresult = firstresult + + def __repr__(self): + status = "%d results, %d meths" % (len(self.results), len(self.methods)) + return "" %(status, self.kwargs) + + def execute(self): + while self.methods: + method = self.methods.pop() + kwargs = self.getkwargs(method) + res = method(**kwargs) + if res is not None: + self.results.append(res) + if self.firstresult: + return res + if not self.firstresult: + return self.results + + def getkwargs(self, method): + kwargs = {} + for argname in varnames(method): + try: + kwargs[argname] = self.kwargs[argname] + except KeyError: + if argname == "__multicall__": + kwargs[argname] = self + return kwargs + +def varnames(func): + if not inspect.isfunction(func) and not inspect.ismethod(func): + func = getattr(func, '__call__', func) + ismethod = inspect.ismethod(func) + rawcode = py.code.getrawcode(func) + try: + return rawcode.co_varnames[ismethod:rawcode.co_argcount] + except AttributeError: + return () + +class HookRelay: + def __init__(self, hookspecs, pm, prefix="pytest_"): + if not isinstance(hookspecs, list): + hookspecs = [hookspecs] + self._hookspecs = [] + self._pm = pm + for hookspec in hookspecs: + self._addhooks(hookspec, prefix) + + def _addhooks(self, hookspecs, prefix): + self._hookspecs.append(hookspecs) + added = False + for name, method in vars(hookspecs).items(): + if name.startswith(prefix): + if not method.__doc__: + raise ValueError("docstring required for hook %r, in %r" + % (method, hookspecs)) + firstresult = getattr(method, 'firstresult', False) + hc = HookCaller(self, name, firstresult=firstresult) + setattr(self, name, hc) + added = True + #print ("setting new hook", name) + if not added: + raise ValueError("did not find new %r hooks in %r" %( + prefix, hookspecs,)) + + +class HookCaller: + def __init__(self, hookrelay, name, firstresult): + self.hookrelay = hookrelay + self.name = name + self.firstresult = firstresult + + def __repr__(self): + return "" %(self.name,) + + def __call__(self, **kwargs): + methods = self.hookrelay._pm.listattr(self.name) + mc = MultiCall(methods, kwargs, firstresult=self.firstresult) + return mc.execute() + + def pcall(self, plugins, **kwargs): + methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) + mc = MultiCall(methods, kwargs, firstresult=self.firstresult) + return mc.execute() + +_preinit = [PluginManager(load=True)] # triggers default plugin importing + +def main(args=None, plugins=None): + if args is None: + args = sys.argv[1:] + elif not isinstance(args, (tuple, list)): + args = py.std.shlex.split(str(args)) + if _preinit: + _pluginmanager = _preinit.pop(0) + else: # subsequent calls to main will create a fresh instance + _pluginmanager = PluginManager(load=True) + hook = _pluginmanager.hook + try: + if plugins: + for plugin in plugins: + _pluginmanager.register(plugin) + config = hook.pytest_cmdline_parse( + pluginmanager=_pluginmanager, args=args) + exitstatus = hook.pytest_cmdline_main(config=config) + except UsageError: + e = sys.exc_info()[1] + sys.stderr.write("ERROR: %s\n" %(e.args[0],)) + exitstatus = 3 + return exitstatus + +class UsageError(Exception): + """ error in py.test usage or invocation""" + +if __name__ == '__main__': + raise SystemExit(main()) From commits-noreply at bitbucket.org Sat Nov 6 09:57:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:02 -0500 (CDT) Subject: [py-svn] pytest commit 3da7c9990c7d: majorly refactor collection process Message-ID: <20101106085702.052D3243F58@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289033884 -3600 # Node ID 3da7c9990c7de697f8a395956d68a277fff5c2fb # Parent e90c2abaefeb33d96b672654b2698281420acc54 majorly refactor collection process - get rid of py.test.collect.Directory alltogether. - introduce direct node.nodeid attribute - remove now superflous attributes on collect and test reports --- a/testing/test_collect.py +++ /dev/null @@ -1,288 +0,0 @@ -import py - -class TestCollector: - def test_collect_versus_item(self): - from pytest.collect import Collector, Item - assert not issubclass(Collector, Item) - assert not issubclass(Item, Collector) - - def test_compat_attributes(self, testdir, recwarn): - modcol = testdir.getmodulecol(""" - def test_pass(): pass - def test_fail(): assert 0 - """) - recwarn.clear() - assert modcol.Module == py.test.collect.Module - recwarn.pop(DeprecationWarning) - assert modcol.Class == py.test.collect.Class - recwarn.pop(DeprecationWarning) - assert modcol.Item == py.test.collect.Item - recwarn.pop(DeprecationWarning) - assert modcol.File == py.test.collect.File - recwarn.pop(DeprecationWarning) - assert modcol.Function == py.test.collect.Function - recwarn.pop(DeprecationWarning) - - def test_check_equality(self, testdir): - modcol = testdir.getmodulecol(""" - def test_pass(): pass - def test_fail(): assert 0 - """) - fn1 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn1, py.test.collect.Function) - fn2 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn2, py.test.collect.Function) - - assert fn1 == fn2 - assert fn1 != modcol - if py.std.sys.version_info < (3, 0): - assert cmp(fn1, fn2) == 0 - assert hash(fn1) == hash(fn2) - - fn3 = testdir.collect_by_name(modcol, "test_fail") - assert isinstance(fn3, py.test.collect.Function) - assert not (fn1 == fn3) - assert fn1 != fn3 - - for fn in fn1,fn2,fn3: - assert fn != 3 - assert fn != modcol - assert fn != [1,2,3] - assert [1,2,3] != fn - assert modcol != fn - - def test_getparent(self, testdir): - modcol = testdir.getmodulecol(""" - class TestClass: - def test_foo(): - pass - """) - cls = testdir.collect_by_name(modcol, "TestClass") - fn = testdir.collect_by_name( - testdir.collect_by_name(cls, "()"), "test_foo") - - parent = fn.getparent(py.test.collect.Module) - assert parent is modcol - - parent = fn.getparent(py.test.collect.Function) - assert parent is fn - - parent = fn.getparent(py.test.collect.Class) - assert parent is cls - - - def test_getcustomfile_roundtrip(self, testdir): - hello = testdir.makefile(".xxx", hello="world") - testdir.makepyfile(conftest=""" - import py - class CustomFile(py.test.collect.File): - pass - def pytest_collect_file(path, parent): - if path.ext == ".xxx": - return CustomFile(path, parent=parent) - """) - config = testdir.parseconfig(hello) - node = testdir.getnode(config, hello) - assert isinstance(node, py.test.collect.File) - assert node.name == "hello.xxx" - id = node.collection.getid(node) - nodes = node.collection.getbyid(id) - assert len(nodes) == 1 - assert isinstance(nodes[0], py.test.collect.File) - -class TestCollectFS: - def test_ignored_certain_directories(self, testdir): - tmpdir = testdir.tmpdir - tmpdir.ensure("_darcs", 'test_notfound.py') - tmpdir.ensure("CVS", 'test_notfound.py') - tmpdir.ensure("{arch}", 'test_notfound.py') - tmpdir.ensure(".whatever", 'test_notfound.py') - tmpdir.ensure(".bzr", 'test_notfound.py') - tmpdir.ensure("normal", 'test_found.py') - - result = testdir.runpytest("--collectonly") - s = result.stdout.str() - assert "test_notfound" not in s - assert "test_found" in s - - def test_custom_norecursedirs(self, testdir): - testdir.makeini(""" - [pytest] - norecursedirs = mydir xyz* - """) - tmpdir = testdir.tmpdir - tmpdir.ensure("mydir", "test_hello.py").write("def test_1(): pass") - tmpdir.ensure("xyz123", "test_2.py").write("def test_2(): 0/0") - tmpdir.ensure("xy", "test_ok.py").write("def test_3(): pass") - rec = testdir.inline_run() - rec.assertoutcome(passed=1) - rec = testdir.inline_run("xyz123/test_2.py") - rec.assertoutcome(failed=1) - - def test_found_certain_testfiles(self, testdir): - p1 = testdir.makepyfile(test_found = "pass", found_test="pass") - col = testdir.getnode(testdir.parseconfig(p1), p1.dirpath()) - items = col.collect() # Directory collect returns files sorted by name - assert len(items) == 2 - assert items[1].name == 'test_found.py' - assert items[0].name == 'found_test.py' - - def test_directory_file_sorting(self, testdir): - p1 = testdir.makepyfile(test_one="hello") - p1.dirpath().mkdir("x") - p1.dirpath().mkdir("dir1") - testdir.makepyfile(test_two="hello") - p1.dirpath().mkdir("dir2") - config = testdir.parseconfig() - col = testdir.getnode(config, p1.dirpath()) - names = [x.name for x in col.collect()] - assert names == ["dir1", "dir2", "test_one.py", "test_two.py", "x"] - -class TestCollectPluginHookRelay: - def test_pytest_collect_file(self, testdir): - tmpdir = testdir.tmpdir - wascalled = [] - class Plugin: - def pytest_collect_file(self, path, parent): - wascalled.append(path) - config = testdir.Config() - config.pluginmanager.register(Plugin()) - config.parse([tmpdir]) - col = testdir.getnode(config, tmpdir) - testdir.makefile(".abc", "xyz") - res = col.collect() - assert len(wascalled) == 1 - assert wascalled[0].ext == '.abc' - - def test_pytest_collect_directory(self, testdir): - tmpdir = testdir.tmpdir - wascalled = [] - class Plugin: - def pytest_collect_directory(self, path, parent): - wascalled.append(path.basename) - return py.test.collect.Directory(path, parent) - testdir.plugins.append(Plugin()) - testdir.mkdir("hello") - testdir.mkdir("world") - reprec = testdir.inline_run() - assert "hello" in wascalled - assert "world" in wascalled - # make sure the directories do not get double-appended - colreports = reprec.getreports("pytest_collectreport") - names = [rep.nodenames[-1] for rep in colreports] - assert names.count("hello") == 1 - -class TestPrunetraceback: - def test_collection_error(self, testdir): - p = testdir.makepyfile(""" - import not_exists - """) - result = testdir.runpytest(p) - assert "__import__" not in result.stdout.str(), "too long traceback" - result.stdout.fnmatch_lines([ - "*ERROR collecting*", - "*mport*not_exists*" - ]) - - def test_custom_repr_failure(self, testdir): - p = testdir.makepyfile(""" - import not_exists - """) - testdir.makeconftest(""" - import py - def pytest_collect_file(path, parent): - return MyFile(path, parent) - class MyError(Exception): - pass - class MyFile(py.test.collect.File): - def collect(self): - raise MyError() - def repr_failure(self, excinfo): - if excinfo.errisinstance(MyError): - return "hello world" - return py.test.collect.File.repr_failure(self, excinfo) - """) - - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*ERROR collecting*", - "*hello world*", - ]) - - @py.test.mark.xfail(reason="other mechanism for adding to reporting needed") - def test_collect_report_postprocessing(self, testdir): - p = testdir.makepyfile(""" - import not_exists - """) - testdir.makeconftest(""" - import py - def pytest_make_collect_report(__multicall__): - rep = __multicall__.execute() - rep.headerlines += ["header1"] - return rep - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*ERROR collecting*", - "*header1*", - ]) - - -class TestCustomConftests: - def test_ignore_collect_path(self, testdir): - testdir.makeconftest(""" - def pytest_ignore_collect(path, config): - return path.basename.startswith("x") or \ - path.basename == "test_one.py" - """) - testdir.mkdir("xy123").ensure("test_hello.py").write( - "syntax error" - ) - testdir.makepyfile("def test_hello(): pass") - testdir.makepyfile(test_one="syntax error") - result = testdir.runpytest() - assert result.ret == 0 - result.stdout.fnmatch_lines(["*1 passed*"]) - - def test_collectignore_exclude_on_option(self, testdir): - testdir.makeconftest(""" - collect_ignore = ['hello', 'test_world.py'] - def pytest_addoption(parser): - parser.addoption("--XX", action="store_true", default=False) - def pytest_configure(config): - if config.getvalue("XX"): - collect_ignore[:] = [] - """) - testdir.mkdir("hello") - testdir.makepyfile(test_world="#") - reprec = testdir.inline_run(testdir.tmpdir) - names = [rep.nodenames[-1] - for rep in reprec.getreports("pytest_collectreport")] - assert 'hello' not in names - assert 'test_world.py' not in names - reprec = testdir.inline_run(testdir.tmpdir, "--XX") - names = [rep.nodenames[-1] - for rep in reprec.getreports("pytest_collectreport")] - assert 'hello' in names - assert 'test_world.py' in names - - def test_pytest_fs_collect_hooks_are_seen(self, testdir): - conf = testdir.makeconftest(""" - import py - class MyDirectory(py.test.collect.Directory): - pass - class MyModule(py.test.collect.Module): - pass - def pytest_collect_directory(path, parent): - return MyDirectory(path, parent) - def pytest_collect_file(path, parent): - return MyModule(path, parent) - """) - sub = testdir.mkdir("sub") - p = testdir.makepyfile("def test_x(): pass") - result = testdir.runpytest("--collectonly") - result.stdout.fnmatch_lines([ - "*MyDirectory*", - "*MyModule*", - "*test_x*" - ]) --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev18' +__version__ = '2.0.0.dev19' __all__ = ['config', 'cmdline'] --- a/pytest/plugin/junitxml.py +++ b/pytest/plugin/junitxml.py @@ -3,6 +3,7 @@ """ import py +import os import time def pytest_addoption(parser): @@ -36,7 +37,9 @@ class LogXML(object): self._durations = {} def _opentestcase(self, report): - names = report.nodenames + names = report.nodeid.split("::") + names[0] = names[0].replace(os.sep, '.') + names = tuple(names) d = {'time': self._durations.pop(names, "0")} names = [x.replace(".py", "") for x in names if x != "()"] classnames = names[:-1] --- a/pytest/plugin/doctest.py +++ b/pytest/plugin/doctest.py @@ -17,7 +17,7 @@ def pytest_addoption(parser): def pytest_collect_file(path, parent): config = parent.config if path.ext == ".py": - if config.getvalue("doctestmodules"): + if config.option.doctestmodules: return DoctestModule(path, parent) elif path.check(fnmatch=config.getvalue("doctestglob")): return DoctestTextfile(path, parent) --- a/pytest/plugin/capture.py +++ b/pytest/plugin/capture.py @@ -133,7 +133,11 @@ class CaptureManager: def pytest_make_collect_report(self, __multicall__, collector): method = self._getmethod(collector.config, collector.fspath) - self.resumecapture(method) + try: + self.resumecapture(method) + except ValueError: + return # recursive collect, XXX refactor capturing + # to allow for more lightweight recursive capturing try: rep = __multicall__.execute() finally: --- a/testing/plugin/test_runner.py +++ b/testing/plugin/test_runner.py @@ -257,9 +257,9 @@ class TestCollectionReports: assert not rep.skipped assert rep.passed locinfo = rep.location - assert locinfo[0] == col.fspath + assert locinfo[0] == col.fspath.basename assert not locinfo[1] - assert locinfo[2] == col.fspath + assert locinfo[2] == col.fspath.basename res = rep.result assert len(res) == 2 assert res[0].name == "test_func1" --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -255,7 +255,7 @@ class Config(object): ) #: a pluginmanager instance self.pluginmanager = pluginmanager or PluginManager(load=True) - self.trace = self.pluginmanager.trace.get("config") + self.trace = self.pluginmanager.trace.root.get("config") self._conftest = Conftest(onimport=self._onimportconftest) self.hook = self.pluginmanager.hook --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -1,14 +1,9 @@ """ terminal reporting of the full testing process. """ -import py +import pytest,py import sys -# =============================================================================== -# plugin tests -# -# =============================================================================== - from pytest.plugin.terminal import TerminalReporter, \ CollectonlyReporter, repr_pythonversion, getreportopt from pytest.plugin import runner @@ -95,9 +90,8 @@ class TestTerminal: item = testdir.getitem("def test_func(): pass") tr = TerminalReporter(item.config, file=linecomp.stringio) item.config.pluginmanager.register(tr) - nodeid = item.collection.getid(item) location = item.reportinfo() - tr.config.hook.pytest_runtest_logstart(nodeid=nodeid, + tr.config.hook.pytest_runtest_logstart(nodeid=item.nodeid, location=location, fspath=str(item.fspath)) linecomp.assert_contains_lines([ "*test_show_runtest_logstart.py*" @@ -424,6 +418,7 @@ class TestTerminalFunctional: "*test_verbose_reporting.py:10: test_gen*FAIL*", ]) assert result.ret == 1 + pytest.xfail("repair xdist") pytestconfig.pluginmanager.skipifmissing("xdist") result = testdir.runpytest(p1, '-v', '-n 1') result.stdout.fnmatch_lines([ --- a/testing/plugin/test_junitxml.py +++ b/testing/plugin/test_junitxml.py @@ -9,11 +9,13 @@ def runandparse(testdir, *args): return result, xmldoc def assert_attr(node, **kwargs): + __tracebackhide__ = True for name, expected in kwargs.items(): anode = node.getAttributeNode(name) assert anode, "node %r has no attribute %r" %(node, name) val = anode.value - assert val == str(expected) + if val != str(expected): + py.test.fail("%r != %r" %(str(val), str(expected))) class TestPython: def test_summing_simple(self, testdir): @@ -50,7 +52,7 @@ class TestPython: assert_attr(node, errors=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_setup_error.test_setup_error", + classname="test_setup_error", name="test_function") fnode = tnode.getElementsByTagName("error")[0] assert_attr(fnode, message="test setup failure") @@ -68,9 +70,21 @@ class TestPython: assert_attr(node, failures=1) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_classname_instance.test_classname_instance.TestClass", + classname="test_classname_instance.TestClass", name="test_method") + def test_classname_nested_dir(self, testdir): + p = testdir.tmpdir.ensure("sub", "test_hello.py") + p.write("def test_func(): 0/0") + result, dom = runandparse(testdir) + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, failures=1) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + classname="sub.test_hello", + name="test_func") + def test_internal_error(self, testdir): testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0") testdir.makepyfile("def test_function(): pass") @@ -92,7 +106,7 @@ class TestPython: assert_attr(node, failures=1, tests=1) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_failure_function.test_failure_function", + classname="test_failure_function", name="test_fail") fnode = tnode.getElementsByTagName("failure")[0] assert_attr(fnode, message="test failure") @@ -112,11 +126,11 @@ class TestPython: assert_attr(node, failures=2, tests=2) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_failure_escape.test_failure_escape", + classname="test_failure_escape", name="test_func[<]") tnode = node.getElementsByTagName("testcase")[1] assert_attr(tnode, - classname="test_failure_escape.test_failure_escape", + classname="test_failure_escape", name="test_func[&]") def test_junit_prefixing(self, testdir): @@ -133,11 +147,11 @@ class TestPython: assert_attr(node, failures=1, tests=2) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="xyz.test_junit_prefixing.test_junit_prefixing", + classname="xyz.test_junit_prefixing", name="test_func") tnode = node.getElementsByTagName("testcase")[1] assert_attr(tnode, - classname="xyz.test_junit_prefixing.test_junit_prefixing." + classname="xyz.test_junit_prefixing." "TestHello", name="test_hello") @@ -153,7 +167,7 @@ class TestPython: assert_attr(node, skips=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_xfailure_function.test_xfailure_function", + classname="test_xfailure_function", name="test_xfail") fnode = tnode.getElementsByTagName("skipped")[0] assert_attr(fnode, message="expected test failure") @@ -172,7 +186,7 @@ class TestPython: assert_attr(node, skips=1, tests=0) tnode = node.getElementsByTagName("testcase")[0] assert_attr(tnode, - classname="test_xfailure_xpass.test_xfailure_xpass", + classname="test_xfailure_xpass", name="test_xpass") fnode = tnode.getElementsByTagName("skipped")[0] assert_attr(fnode, message="xfail-marked test passes unexpectedly") --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev18', + version='2.0.0.dev19', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/CHANGELOG +++ b/CHANGELOG @@ -6,10 +6,10 @@ Changes between 1.3.4 and 2.0.0dev0 - new python invcation: pytest.main(args, plugins) to load some custom plugins early. - try harder to run unittest test suites in a more compatible manner - by deferring setup/teardown semantics to the unittest package. + by deferring setup/teardown semantics to the unittest package. - introduce a new way to set config options via ini-style files, by default setup.cfg and tox.ini files are searched. The old - ways (certain environment variables, dynamic conftest.py reading + ways (certain environment variables, dynamic conftest.py reading is removed). - add a new "-q" option which decreases verbosity and prints a more nose/unittest-style "dot" output. @@ -26,7 +26,8 @@ Changes between 1.3.4 and 2.0.0dev0 output on assertion failures for comparisons and other cases (Floris Bruynooghe) - nose-plugin: pass through type-signature failures in setup/teardown functions instead of not calling them (Ed Singleton) -- major refactoring of internal collection handling +- remove py.test.collect.Directory (follows from a major refactoring + and simplification of the collection process) - majorly reduce py.test core code, shift function/python testing to own plugin - fix issue88 (finding custom test nodes from command line arg) - refine 'tmpdir' creation, will now create basenames better associated --- a/testing/test_config.py +++ b/testing/test_config.py @@ -81,7 +81,7 @@ class TestConfigAPI: config.trace.root.setwriter(l.append) config.trace("hello") assert len(l) == 1 - assert l[0] == "[pytest:config] hello\n" + assert l[0] == "[pytest] hello\n" def test_config_getvalue_honours_conftest(self, testdir): testdir.makepyfile(conftest="x=1") --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -14,11 +14,15 @@ EXIT_OK = 0 EXIT_TESTSFAILED = 1 EXIT_INTERRUPTED = 2 EXIT_INTERNALERROR = 3 -EXIT_NOHOSTS = 4 def pytest_addoption(parser): parser.addini("norecursedirs", "directory patterns to avoid for recursion", type="args", default=('.*', 'CVS', '_darcs', '{arch}')) + #parser.addini("dirpatterns", + # "patterns specifying possible locations of test files", + # type="linelist", default=["**/test_*.txt", + # "**/test_*.py", "**/*_test.py"] + #) group = parser.getgroup("general", "running and selection options") group._addoption('-x', '--exitfirst', action="store_true", default=False, dest="exitfirst", @@ -44,12 +48,11 @@ def pytest_addoption(parser): def pytest_namespace(): - return dict(collect=dict(Item=Item, Collector=Collector, - File=File, Directory=Directory)) + return dict(collect=dict(Item=Item, Collector=Collector, File=File)) def pytest_configure(config): py.test.config = config # compatibiltiy - if config.getvalue("exitfirst"): + if config.option.exitfirst: config.option.maxfail = 1 def pytest_cmdline_main(config): @@ -84,8 +87,10 @@ def pytest_cmdline_main(config): def pytest_collection(session): collection = session.collection assert not hasattr(collection, 'items') + + collection.perform_collect() hook = session.config.hook - collection.items = items = collection.perform_collect() + items = collection.items hook.pytest_collection_modifyitems(config=session.config, items=items) hook.pytest_collection_finish(collection=collection) return True @@ -108,18 +113,6 @@ def pytest_ignore_collect(path, config): ignore_paths.extend([py.path.local(x) for x in excludeopt]) return path in ignore_paths -def pytest_collect_directory(path, parent): - # check if cmdline specified this dir or a subdir directly - for arg in parent.collection._argfspaths: - if path == arg or arg.relto(path): - break - else: - patterns = parent.config.getini("norecursedirs") - for pat in patterns or []: - if path.check(fnmatch=pat): - return - return Directory(path, parent=parent) - class Session(object): class Interrupted(KeyboardInterrupt): """ signals an interrupted test run. """ @@ -145,160 +138,17 @@ class Session(object): self._testsfailed) pytest_collectreport = pytest_runtest_logreport -class Collection: - def __init__(self, config): - self.config = config - self.topdir = gettopdir(self.config.args) - self._argfspaths = [py.path.local(decodearg(x)[0]) - for x in self.config.args] - x = pytest.collect.Directory(fspath=self.topdir, - config=config, collection=self) - self._topcollector = x.consider_dir(self.topdir) - self._topcollector.parent = None - - def _normalizearg(self, arg): - return "::".join(self._parsearg(arg)) - - def _parsearg(self, arg, base=None): - """ return normalized name list for a command line specified id - which might be of the form x/y/z::name1::name2 - and should result into the form x::y::z::name1::name2 - """ - if base is None: - base = py.path.local() - parts = str(arg).split("::") - path = base.join(parts[0], abs=True) - if not path.check(): - raise pytest.UsageError("file not found: %s" %(path,)) - topdir = self.topdir - if path != topdir and not path.relto(topdir): - raise pytest.UsageError("path %r is not relative to %r" % - (str(path), str(topdir))) - topparts = path.relto(topdir).split(path.sep) - return topparts + parts[1:] - - def getid(self, node): - """ return id for node, relative to topdir. """ - path = node.fspath - chain = [x for x in node.listchain() if x.fspath == path] - chain = chain[1:] - names = [x.name for x in chain if x.name != "()"] - relpath = path.relto(self.topdir) - if not relpath: - assert path == self.topdir - path = '' - else: - path = relpath - if os.sep != "/": - path = str(path).replace(os.sep, "/") - names.insert(0, path) - return "::".join(names) - - def getbyid(self, id): - """ return one or more nodes matching the id. """ - names = [x for x in id.split("::") if x] - if names and '/' in names[0]: - names[:1] = names[0].split("/") - return list(self.matchnodes([self._topcollector], names)) - - def perform_collect(self): - items = [] - for arg in self.config.args: - names = self._parsearg(arg) - try: - for node in self.matchnodes([self._topcollector], names): - items.extend(self.genitems(node)) - except NoMatch: - raise pytest.UsageError("can't collect: %s" % (arg,)) - return items - - def matchnodes(self, matching, names): - if not matching: - return - if not names: - for x in matching: - yield x - return - name = names[0] - names = names[1:] - for node in matching: - if isinstance(node, pytest.collect.Item): - if not name: - yield node - continue - assert isinstance(node, pytest.collect.Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) - #print "matching", rep.result, "against name", name - if rep.passed: - if not name: - for x in rep.result: - yield x - else: - matched = False - for x in rep.result: - try: - if x.name == name or x.fspath.basename == name: - for x in self.matchnodes([x], names): - yield x - matched = True - elif x.name == "()": # XXX special Instance() case - for x in self.matchnodes([x], [name] + names): - yield x - matched = True - except NoMatch: - pass - if not matched: - node.ihook.pytest_collectreport(report=rep) - raise NoMatch(name) - node.ihook.pytest_collectreport(report=rep) - - def genitems(self, node): - if isinstance(node, pytest.collect.Item): - node.ihook.pytest_itemcollected(item=node) - yield node - else: - assert isinstance(node, pytest.collect.Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) - if rep.passed: - for subnode in rep.result: - for x in self.genitems(subnode): - yield x - node.ihook.pytest_collectreport(report=rep) - class NoMatch(Exception): """ raised if matching cannot locate a matching names. """ -def gettopdir(args): - """ return the top directory for the given paths. - if the common base dir resides in a python package - parent directory of the root package is returned. - """ - fsargs = [py.path.local(decodearg(arg)[0]) for arg in args] - p = fsargs and fsargs[0] or None - for x in fsargs[1:]: - p = p.common(x) - assert p, "cannot determine common basedir of %s" %(fsargs,) - pkgdir = p.pypkgpath() - if pkgdir is None: - if p.check(file=1): - p = p.dirpath() - return p - else: - return pkgdir.dirpath() - -def decodearg(arg): - arg = str(arg) - return arg.split("::") - class HookProxy: - def __init__(self, node): - self.node = node + def __init__(self, fspath, config): + self.fspath = fspath + self.config = config def __getattr__(self, name): - hookmethod = getattr(self.node.config.hook, name) + hookmethod = getattr(self.config.hook, name) def call_matching_hooks(**kwargs): - plugins = self.node.config._getmatchingplugins(self.node.fspath) + plugins = self.config._getmatchingplugins(self.fspath) return hookmethod.pcall(plugins, **kwargs) return call_matching_hooks @@ -329,7 +179,7 @@ class Node(object): #: the file where this item is contained/collected from. self.fspath = getattr(parent, 'fspath', None) - self.ihook = HookProxy(self) + self.ihook = self.collection.gethookproxy(self.fspath) self.keywords = {self.name: True} Module = compatproperty("Module") @@ -339,14 +189,19 @@ class Node(object): Item = compatproperty("Item") def __repr__(self): - if getattr(self.config.option, 'debug', False): - return "<%s %r %0x>" %(self.__class__.__name__, - getattr(self, 'name', None), id(self)) - else: - return "<%s %r>" %(self.__class__.__name__, - getattr(self, 'name', None)) + return "<%s %r>" %(self.__class__.__name__, getattr(self, 'name', None)) # methods for ordering nodes + @property + def nodeid(self): + try: + return self._nodeid + except AttributeError: + self._nodeid = x = self._makeid() + return x + + def _makeid(self): + return self.parent.nodeid + "::" + self.name def __eq__(self, other): if not isinstance(other, Node): @@ -447,7 +302,7 @@ class Collector(Node): def _memocollect(self): """ internal helper method to cache results of calling collect(). """ - return self._memoizedcall('_collected', self.collect) + return self._memoizedcall('_collected', lambda: list(self.collect())) def _prunetraceback(self, excinfo): if hasattr(self, 'fspath'): @@ -460,55 +315,177 @@ class Collector(Node): class FSCollector(Collector): def __init__(self, fspath, parent=None, config=None, collection=None): - fspath = py.path.local(fspath) - super(FSCollector, self).__init__(fspath.basename, - parent, config, collection) + fspath = py.path.local(fspath) # xxx only for test_resultlog.py? + name = parent and fspath.relto(parent.fspath) or fspath.basename + super(FSCollector, self).__init__(name, parent, config, collection) self.fspath = fspath + def _makeid(self): + if self == self.collection: + return "." + relpath = self.collection.fspath.bestrelpath(self.fspath) + if os.sep != "/": + relpath = str(path).replace(os.sep, "/") + return relpath + class File(FSCollector): """ base class for collecting tests from a file. """ -class Directory(FSCollector): - def collect(self): - l = [] - for path in self.fspath.listdir(sort=True): - res = self.consider(path) - if res is not None: - if isinstance(res, (list, tuple)): - l.extend(res) - else: - l.append(res) - return l - - def consider(self, path): - if self.ihook.pytest_ignore_collect(path=path, config=self.config): - return - if path.check(file=1): - res = self.consider_file(path) - elif path.check(dir=1): - res = self.consider_dir(path) - else: - res = None - if isinstance(res, list): - # throw out identical results - l = [] - for x in res: - if x not in l: - assert x.parent == self, (x.parent, self) - assert x.fspath == path, (x.fspath, path) - l.append(x) - res = l - return res - - def consider_file(self, path): - return self.ihook.pytest_collect_file(path=path, parent=self) - - def consider_dir(self, path): - return self.ihook.pytest_collect_directory(path=path, parent=self) - class Item(Node): """ a basic test invocation item. Note that for a single function there might be multiple test invocation items. """ def reportinfo(self): return self.fspath, None, "" + + @property + def location(self): + try: + return self._location + except AttributeError: + location = self.reportinfo() + location = (str(location[0]), location[1], str(location[2])) + self._location = location + return location + +class Collection(FSCollector): + def __init__(self, config): + super(Collection, self).__init__(py.path.local(), parent=None, + config=config, collection=self) + self.trace = config.trace.root.get("collection") + self._norecursepatterns = config.getini("norecursedirs") + + def isinitpath(self, path): + return path in self._initialpaths + + def gethookproxy(self, fspath): + return HookProxy(fspath, self.config) + + def perform_collect(self, args=None, genitems=True): + if args is None: + args = self.config.args + self.trace("perform_collect", self, args) + self.trace.root.indent += 1 + self._notfound = [] + self._initialpaths = set() + self._initialargs = args + for arg in args: + parts = self._parsearg(arg) + self._initialpaths.add(parts[0]) + self.ihook.pytest_collectstart(collector=self) + rep = self.ihook.pytest_make_collect_report(collector=self) + self.ihook.pytest_collectreport(report=rep) + self.trace.root.indent -= 1 + if self._notfound: + for arg, exc in self._notfound: + line = "no name %r in any of %r" % (exc.args[1], exc.args[0]) + raise pytest.UsageError("not found: %s\n%s" %(arg, line)) + if not genitems: + return rep.result + else: + self.items = items = [] + if rep.passed: + for node in rep.result: + self.items.extend(self.genitems(node)) + return items + + def collect(self): + for arg in self._initialargs: + self.trace("processing arg", arg) + self.trace.root.indent += 1 + try: + for x in self._collect(arg): + yield x + except NoMatch: + # we are inside a make_report hook so + # we cannot directly pass through the exception + self._notfound.append((arg, sys.exc_info()[1])) + self.trace.root.indent -= 1 + break + self.trace.root.indent -= 1 + + def _collect(self, arg): + names = self._parsearg(arg) + path = names.pop(0) + if path.check(dir=1): + assert not names, "invalid arg %r" %(arg,) + for path in path.visit(rec=self._recurse, bf=True, sort=True): + for x in self._collectfile(path): + yield x + else: + assert path.check(file=1) + for x in self.matchnodes(self._collectfile(path), names): + yield x + + def _collectfile(self, path): + ihook = self.gethookproxy(path) + if ihook.pytest_ignore_collect(path=path, config=self.config): + return () + return ihook.pytest_collect_file(path=path, parent=self) + + def _recurse(self, path): + ihook = self.gethookproxy(path) + if ihook.pytest_ignore_collect(path=path, config=self.config): + return + for pat in self._norecursepatterns: + if path.check(fnmatch=pat): + return False + ihook.pytest_collect_directory(path=path, parent=self) + return True + + def _parsearg(self, arg): + """ return (fspath, names) tuple after checking the file exists. """ + parts = str(arg).split("::") + path = self.fspath.join(parts[0], abs=True) + if not path.check(): + raise pytest.UsageError("file not found: %s" %(path,)) + parts[0] = path + return parts + + def matchnodes(self, matching, names): + self.trace("matchnodes", matching, names) + self.trace.root.indent += 1 + nodes = self._matchnodes(matching, names) + num = len(nodes) + self.trace("matchnodes finished -> ", num, "nodes") + self.trace.root.indent -= 1 + if num == 0: + raise NoMatch(matching, names[:1]) + return nodes + + def _matchnodes(self, matching, names): + if not matching or not names: + return matching + name = names[0] + assert name + nextnames = names[1:] + resultnodes = [] + for node in matching: + if isinstance(node, pytest.collect.Item): + resultnodes.append(node) + continue + assert isinstance(node, pytest.collect.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + if rep.passed: + for x in rep.result: + if x.name == name: + resultnodes.extend(self.matchnodes([x], nextnames)) + node.ihook.pytest_collectreport(report=rep) + return resultnodes + + def genitems(self, node): + self.trace("genitems", node) + if isinstance(node, pytest.collect.Item): + node.ihook.pytest_itemcollected(item=node) + yield node + else: + assert isinstance(node, pytest.collect.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + if rep.passed: + for subnode in rep.result: + for x in self.genitems(subnode): + yield x + node.ihook.pytest_collectreport(report=rep) + --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -105,6 +105,7 @@ class HookRecorder: return l def contains(self, entries): + __tracebackhide__ = True from py.builtin import print_ i = 0 entries = list(entries) @@ -123,8 +124,7 @@ class HookRecorder: break print_("NONAMEMATCH", name, "with", call) else: - raise AssertionError("could not find %r in %r" %( - name, self.calls[i:])) + py.test.fail("could not find %r check %r" % (name, check)) def popcall(self, name): for i, call in enumerate(self.calls): @@ -278,7 +278,16 @@ class TmpTestdir: Collection = Collection def getnode(self, config, arg): collection = Collection(config) - return collection.getbyid(collection._normalizearg(arg))[0] + assert '::' not in str(arg) + p = py.path.local(arg) + x = collection.fspath.bestrelpath(p) + return collection.perform_collect([x], genitems=False)[0] + + def getpathnode(self, path): + config = self.parseconfig(path) + collection = Collection(config) + x = collection.fspath.bestrelpath(path) + return collection.perform_collect([x], genitems=False)[0] def genitems(self, colitems): collection = colitems[0].collection @@ -291,8 +300,9 @@ class TmpTestdir: #config = self.parseconfig(*args) config = self.parseconfigure(*args) rec = self.getreportrecorder(config) - items = Collection(config).perform_collect() - return items, rec + collection = Collection(config) + collection.perform_collect() + return collection.items, rec def runitem(self, source): # used from runner functional tests @@ -469,11 +479,12 @@ class TmpTestdir: p = py.path.local.make_numbered_dir(prefix="runpytest-", keep=None, rootdir=self.tmpdir) args = ('--basetemp=%s' % p, ) + args - for x in args: - if '--confcutdir' in str(x): - break - else: - args = ('--confcutdir=.',) + args + #for x in args: + # if '--confcutdir' in str(x): + # break + #else: + # pass + # args = ('--confcutdir=.',) + args plugins = [x for x in self.plugins if isinstance(x, str)] if plugins: args = ('-p', plugins[0]) + args @@ -530,7 +541,7 @@ class ReportRecorder(object): """ return a testreport whose dotted import path matches """ l = [] for rep in self.getreports(names=names): - if not inamepart or inamepart in rep.nodenames: + if not inamepart or inamepart in rep.nodeid.split("::"): l.append(rep) if not l: raise ValueError("could not find test report matching %r: no test reports at all!" % @@ -616,6 +627,8 @@ class LineMatcher: raise ValueError("line %r not found in output" % line) def fnmatch_lines(self, lines2): + def show(arg1, arg2): + py.builtin.print_(arg1, arg2, file=py.std.sys.stderr) lines2 = self._getlines(lines2) lines1 = self.lines[:] nextline = None @@ -626,17 +639,17 @@ class LineMatcher: while lines1: nextline = lines1.pop(0) if line == nextline: - print_("exact match:", repr(line)) + show("exact match:", repr(line)) break elif fnmatch(nextline, line): - print_("fnmatch:", repr(line)) - print_(" with:", repr(nextline)) + show("fnmatch:", repr(line)) + show(" with:", repr(nextline)) break else: if not nomatchprinted: - print_("nomatch:", repr(line)) + show("nomatch:", repr(line)) nomatchprinted = True - print_(" and:", repr(nextline)) + show(" and:", repr(nextline)) extralines.append(nextline) else: - assert line == nextline + py.test.fail("remains unmatched: %r, see stderr" % (line,)) --- a/pytest/plugin/terminal.py +++ b/pytest/plugin/terminal.py @@ -115,10 +115,10 @@ class TerminalReporter: def write_fspath_result(self, fspath, res): if fspath != self.currentfspath: self.currentfspath = fspath - fspath = self.curdir.bestrelpath(fspath) + #fspath = self.curdir.bestrelpath(fspath) self._tw.line() - relpath = self.curdir.bestrelpath(fspath) - self._tw.write(relpath + " ") + #relpath = self.curdir.bestrelpath(fspath) + self._tw.write(fspath + " ") self._tw.write(res) def write_ensure_prefix(self, prefix, extra="", **kwargs): @@ -163,14 +163,15 @@ class TerminalReporter: def pytest__teardown_final_logerror(self, report): self.stats.setdefault("error", []).append(report) - def pytest_runtest_logstart(self, nodeid, location, fspath): + def pytest_runtest_logstart(self, nodeid, location): # ensure that the path is printed before the # 1st test of a module starts running + fspath = nodeid.split("::")[0] if self.showlongtestinfo: line = self._locationline(fspath, *location) self.write_ensure_prefix(line, "") elif self.showfspath: - self.write_fspath_result(py.path.local(fspath), "") + self.write_fspath_result(fspath, "") def pytest_runtest_logreport(self, report): rep = report --- a/pytest/plugin/runner.py +++ b/pytest/plugin/runner.py @@ -29,30 +29,12 @@ def pytest_sessionfinish(session, exitst session.exitstatus = 1 class NodeInfo: - def __init__(self, nodeid, nodenames, fspath, location): - self.nodeid = nodeid - self.nodenames = nodenames - self.fspath = fspath + def __init__(self, location): self.location = location -def getitemnodeinfo(item): - try: - return item._nodeinfo - except AttributeError: - location = item.reportinfo() - location = (str(location[0]), location[1], str(location[2])) - nodenames = tuple(item.listnames()) - nodeid = item.collection.getid(item) - fspath = item.fspath - item._nodeinfo = n = NodeInfo(nodeid, nodenames, fspath, location) - return n - def pytest_runtest_protocol(item): - nodeinfo = getitemnodeinfo(item) item.ihook.pytest_runtest_logstart( - nodeid=nodeinfo.nodeid, - location=nodeinfo.location, - fspath=str(item.fspath), + nodeid=item.nodeid, location=item.location, ) runtestprotocol(item) return True @@ -142,16 +124,18 @@ class BaseReport(object): failed = property(lambda x: x.outcome == "failed") skipped = property(lambda x: x.outcome == "skipped") + @property + def fspath(self): + return self.nodeid.split("::")[0] def pytest_runtest_makereport(item, call): - nodeinfo = getitemnodeinfo(item) when = call.when keywords = dict([(x,1) for x in item.keywords]) - excinfo = call.excinfo if not call.excinfo: outcome = "passed" longrepr = None else: + excinfo = call.excinfo if not isinstance(excinfo, py.code.ExceptionInfo): outcome = "failed" longrepr = excinfo @@ -164,25 +148,18 @@ def pytest_runtest_makereport(item, call longrepr = item.repr_failure(excinfo) else: # exception in setup or teardown longrepr = item._repr_failure_py(excinfo) - return TestReport(nodeinfo.nodeid, nodeinfo.nodenames, - nodeinfo.fspath, nodeinfo.location, + return TestReport(item.nodeid, item.location, keywords, outcome, longrepr, when) class TestReport(BaseReport): """ Basic test report object (also used for setup and teardown calls if they fail). """ - def __init__(self, nodeid, nodenames, fspath, location, + def __init__(self, nodeid, location, keywords, outcome, longrepr, when): #: normalized collection node id self.nodeid = nodeid - #: list of names indicating position in collection tree. - self.nodenames = nodenames - - #: the collected path of the file containing the test. - self.fspath = fspath # where the test was collected - #: a (filesystempath, lineno, domaininfo) tuple indicating the #: actual location of a test item - it might be different from the #: collected one e.g. if a method is inherited from a different module. @@ -212,39 +189,27 @@ class TeardownErrorReport(BaseReport): self.longrepr = longrepr def pytest_make_collect_report(collector): - result = excinfo = None - try: - result = collector._memocollect() - except KeyboardInterrupt: - raise - except: - excinfo = py.code.ExceptionInfo() - nodenames = tuple(collector.listnames()) - nodeid = collector.collection.getid(collector) - fspath = str(collector.fspath) + call = CallInfo(collector._memocollect, "memocollect") reason = longrepr = None - if not excinfo: + if not call.excinfo: outcome = "passed" else: - if excinfo.errisinstance(py.test.skip.Exception): + if call.excinfo.errisinstance(py.test.skip.Exception): outcome = "skipped" - reason = str(excinfo.value) - longrepr = collector._repr_failure_py(excinfo, "line") + reason = str(call.excinfo.value) + longrepr = collector._repr_failure_py(call.excinfo, "line") else: outcome = "failed" - errorinfo = collector.repr_failure(excinfo) + errorinfo = collector.repr_failure(call.excinfo) if not hasattr(errorinfo, "toterminal"): errorinfo = CollectErrorRepr(errorinfo) longrepr = errorinfo - return CollectReport(nodenames, nodeid, fspath, - outcome, longrepr, result, reason) + return CollectReport(collector.nodeid, outcome, longrepr, + getattr(call, 'result', None), reason) class CollectReport(BaseReport): - def __init__(self, nodenames, nodeid, fspath, outcome, - longrepr, result, reason): - self.nodenames = nodenames + def __init__(self, nodeid, outcome, longrepr, result, reason): self.nodeid = nodeid - self.fspath = fspath self.outcome = outcome self.longrepr = longrepr self.result = result or [] @@ -255,7 +220,8 @@ class CollectReport(BaseReport): return (self.fspath, None, self.fspath) def __repr__(self): - return "" % (self.nodeid, self.outcome) + return "" % ( + self.nodeid, len(self.result), self.outcome) class CollectErrorRepr(TerminalRepr): def __init__(self, msg): --- a/tox.ini +++ b/tox.ini @@ -52,4 +52,5 @@ commands= [pytest] minversion=2.0 plugins=pytester -#addargs=-rf +addargs=-rfx +rsyncdirs=pytest testing --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -235,7 +235,7 @@ class TestFunction: param = 1 funcargs = {} id = "world" - collection = object() + collection = testdir.Collection(config) f5 = py.test.collect.Function(name="name", config=config, callspec=callspec1, callobj=isinstance, collection=collection) f5b = py.test.collect.Function(name="name", config=config, @@ -395,8 +395,8 @@ def test_generate_tests_only_done_in_sub def test_modulecol_roundtrip(testdir): modcol = testdir.getmodulecol("pass", withinit=True) - trail = modcol.collection.getid(modcol) - newcol = modcol.collection.getbyid(trail)[0] + trail = modcol.nodeid + newcol = modcol.collection.perform_collect([trail], genitems=0)[0] assert modcol.name == newcol.name @@ -1058,8 +1058,7 @@ class TestReportInfo: """) item = testdir.getitem("def test_func(): pass") runner = item.config.pluginmanager.getplugin("runner") - nodeinfo = runner.getitemnodeinfo(item) - assert nodeinfo.location == ("ABCDE", 42, "custom") + assert item.location == ("ABCDE", 42, "custom") def test_func_reportinfo(self, testdir): item = testdir.getitem("def test_func(): pass") --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -76,14 +76,13 @@ class TestGeneralUsage: p1 = testdir.makepyfile("") p2 = testdir.makefile(".pyc", "123") result = testdir.runpytest(p1, p2) - assert result.ret != 0 + assert result.ret result.stderr.fnmatch_lines([ - "*ERROR: can't collect:*%s" %(p2.basename,) + "*ERROR: not found:*%s" %(p2.basename,) ]) - @py.test.mark.xfail def test_early_skip(self, testdir): testdir.mkdir("xyz") testdir.makeconftest(""" @@ -97,7 +96,6 @@ class TestGeneralUsage: "*1 skip*" ]) - def test_issue88_initial_file_multinodes(self, testdir): testdir.makeconftest(""" import py @@ -145,7 +143,7 @@ class TestGeneralUsage: print (py.__file__) print (py.__path__) os.chdir(os.path.dirname(os.getcwd())) - print (py.log.Producer) + print (py.log) """)) result = testdir.runpython(p, prepend=False) assert not result.ret @@ -210,6 +208,27 @@ class TestGeneralUsage: res = testdir.runpytest(p) assert res.ret == 0 res.stdout.fnmatch_lines(["*1 skipped*"]) + + def test_direct_addressing_selects(self, testdir): + p = testdir.makepyfile(""" + def pytest_generate_tests(metafunc): + metafunc.addcall({'i': 1}, id="1") + metafunc.addcall({'i': 2}, id="2") + def test_func(i): + pass + """) + res = testdir.runpytest(p.basename + "::" + "test_func[1]") + assert res.ret == 0 + res.stdout.fnmatch_lines(["*1 passed*"]) + + def test_direct_addressing_notfound(self, testdir): + p = testdir.makepyfile(""" + def test_func(): + pass + """) + res = testdir.runpytest(p.basename + "::" + "test_notfound") + assert res.ret + res.stderr.fnmatch_lines(["*ERROR*not found*"]) class TestInvocationVariants: def test_earlyinit(self, testdir): --- a/testing/plugin/test_session.py +++ b/testing/plugin/test_session.py @@ -17,13 +17,14 @@ class SessionTests: assert len(skipped) == 0 assert len(passed) == 1 assert len(failed) == 3 - assert failed[0].nodenames[-1] == "test_one_one" - assert failed[1].nodenames[-1] == "test_other" - assert failed[2].nodenames[-1] == "test_two" + end = lambda x: x.nodeid.split("::")[-1] + assert end(failed[0]) == "test_one_one" + assert end(failed[1]) == "test_other" + assert end(failed[2]) == "test_two" itemstarted = reprec.getcalls("pytest_itemcollected") assert len(itemstarted) == 4 colstarted = reprec.getcalls("pytest_collectstart") - assert len(colstarted) == 1 + 1 # XXX ExtraTopCollector + assert len(colstarted) == 1 + 1 col = colstarted[1].collector assert isinstance(col, py.test.collect.Module) @@ -186,7 +187,7 @@ class TestNewSession(SessionTests): started = reprec.getcalls("pytest_collectstart") finished = reprec.getreports("pytest_collectreport") assert len(started) == len(finished) - assert len(started) == 8 + 1 # XXX extra TopCollector + assert len(started) == 8 # XXX extra TopCollector colfail = [x for x in finished if x.failed] colskipped = [x for x in finished if x.skipped] assert len(colfail) == 1 --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -48,11 +48,10 @@ def pytest_pyfunc_call(__multicall__, py def pytest_collect_file(path, parent): ext = path.ext pb = path.purebasename - if pb.startswith("test_") or pb.endswith("_test") or \ - path in parent.collection._argfspaths: - if ext == ".py": - return parent.ihook.pytest_pycollect_makemodule( - path=path, parent=parent) + if ext == ".py" and (pb.startswith("test_") or pb.endswith("_test") or + parent.collection.isinitpath(path)): + return parent.ihook.pytest_pycollect_makemodule( + path=path, parent=parent) def pytest_pycollect_makemodule(path, parent): return Module(path, parent) @@ -713,11 +712,13 @@ class FuncargRequest: def showfuncargs(config): from pytest.plugin.session import Collection collection = Collection(config) - firstid = collection._normalizearg(config.args[0]) - colitem = collection.getbyid(firstid)[0] + collection.perform_collect() + if collection.items: + plugins = getplugins(collection.items[0]) + else: + plugins = getplugins(collection) curdir = py.path.local() tw = py.io.TerminalWriter() - plugins = getplugins(colitem, withpy=True) verbose = config.getvalue("verbose") for plugin in plugins: available = [] --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -126,7 +126,7 @@ def pytest_runtest_protocol(item): """ pytest_runtest_protocol.firstresult = True -def pytest_runtest_logstart(nodeid, location, fspath): +def pytest_runtest_logstart(nodeid, location): """ signal the start of a test run. """ def pytest_runtest_setup(item): --- a/pytest/main.py +++ b/pytest/main.py @@ -67,7 +67,10 @@ class PluginManager(object): self._hints = [] self.trace = TagTracer().get("pluginmanage") if os.environ.get('PYTEST_DEBUG'): - self.trace.root.setwriter(sys.stderr.write) + err = sys.stderr + if hasattr(os, 'dup'): + err = py.io.dupfile(err) + self.trace.root.setwriter(err.write) self.hook = HookRelay([hookspec], pm=self) self.register(self) if load: @@ -370,6 +373,7 @@ class HookCaller: self.hookrelay = hookrelay self.name = name self.firstresult = firstresult + self.trace = self.hookrelay.trace def __repr__(self): return "" %(self.name,) @@ -380,10 +384,15 @@ class HookCaller: return mc.execute() def pcall(self, plugins, **kwargs): - self.hookrelay.trace(self.name, kwargs) + self.trace(self.name, kwargs) + self.trace.root.indent += 1 methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - return mc.execute() + res = mc.execute() + if res: + self.trace(res) + self.trace.root.indent -= 1 + return res _preinit = [PluginManager(load=True)] # triggers default plugin importing --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,6 +1,283 @@ import py -from pytest.plugin.session import Collection, gettopdir +from pytest.plugin.session import Collection + +class TestCollector: + def test_collect_versus_item(self): + from pytest.collect import Collector, Item + assert not issubclass(Collector, Item) + assert not issubclass(Item, Collector) + + def test_compat_attributes(self, testdir, recwarn): + modcol = testdir.getmodulecol(""" + def test_pass(): pass + def test_fail(): assert 0 + """) + recwarn.clear() + assert modcol.Module == py.test.collect.Module + recwarn.pop(DeprecationWarning) + assert modcol.Class == py.test.collect.Class + recwarn.pop(DeprecationWarning) + assert modcol.Item == py.test.collect.Item + recwarn.pop(DeprecationWarning) + assert modcol.File == py.test.collect.File + recwarn.pop(DeprecationWarning) + assert modcol.Function == py.test.collect.Function + recwarn.pop(DeprecationWarning) + + def test_check_equality(self, testdir): + modcol = testdir.getmodulecol(""" + def test_pass(): pass + def test_fail(): assert 0 + """) + fn1 = testdir.collect_by_name(modcol, "test_pass") + assert isinstance(fn1, py.test.collect.Function) + fn2 = testdir.collect_by_name(modcol, "test_pass") + assert isinstance(fn2, py.test.collect.Function) + + assert fn1 == fn2 + assert fn1 != modcol + if py.std.sys.version_info < (3, 0): + assert cmp(fn1, fn2) == 0 + assert hash(fn1) == hash(fn2) + + fn3 = testdir.collect_by_name(modcol, "test_fail") + assert isinstance(fn3, py.test.collect.Function) + assert not (fn1 == fn3) + assert fn1 != fn3 + + for fn in fn1,fn2,fn3: + assert fn != 3 + assert fn != modcol + assert fn != [1,2,3] + assert [1,2,3] != fn + assert modcol != fn + + def test_getparent(self, testdir): + modcol = testdir.getmodulecol(""" + class TestClass: + def test_foo(): + pass + """) + cls = testdir.collect_by_name(modcol, "TestClass") + fn = testdir.collect_by_name( + testdir.collect_by_name(cls, "()"), "test_foo") + + parent = fn.getparent(py.test.collect.Module) + assert parent is modcol + + parent = fn.getparent(py.test.collect.Function) + assert parent is fn + + parent = fn.getparent(py.test.collect.Class) + assert parent is cls + + + def test_getcustomfile_roundtrip(self, testdir): + hello = testdir.makefile(".xxx", hello="world") + testdir.makepyfile(conftest=""" + import py + class CustomFile(py.test.collect.File): + pass + def pytest_collect_file(path, parent): + if path.ext == ".xxx": + return CustomFile(path, parent=parent) + """) + node = testdir.getpathnode(hello) + assert isinstance(node, py.test.collect.File) + assert node.name == "hello.xxx" + nodes = node.collection.perform_collect([node.nodeid], genitems=False) + assert len(nodes) == 1 + assert isinstance(nodes[0], py.test.collect.File) + +class TestCollectFS: + def test_ignored_certain_directories(self, testdir): + tmpdir = testdir.tmpdir + tmpdir.ensure("_darcs", 'test_notfound.py') + tmpdir.ensure("CVS", 'test_notfound.py') + tmpdir.ensure("{arch}", 'test_notfound.py') + tmpdir.ensure(".whatever", 'test_notfound.py') + tmpdir.ensure(".bzr", 'test_notfound.py') + tmpdir.ensure("normal", 'test_found.py') + + result = testdir.runpytest("--collectonly") + s = result.stdout.str() + assert "test_notfound" not in s + assert "test_found" in s + + def test_custom_norecursedirs(self, testdir): + testdir.makeini(""" + [pytest] + norecursedirs = mydir xyz* + """) + tmpdir = testdir.tmpdir + tmpdir.ensure("mydir", "test_hello.py").write("def test_1(): pass") + tmpdir.ensure("xyz123", "test_2.py").write("def test_2(): 0/0") + tmpdir.ensure("xy", "test_ok.py").write("def test_3(): pass") + rec = testdir.inline_run() + rec.assertoutcome(passed=1) + rec = testdir.inline_run("xyz123/test_2.py") + rec.assertoutcome(failed=1) + +class TestCollectPluginHookRelay: + def test_pytest_collect_file(self, testdir): + wascalled = [] + class Plugin: + def pytest_collect_file(self, path, parent): + wascalled.append(path) + testdir.makefile(".abc", "xyz") + testdir.pytestmain([testdir.tmpdir], plugins=[Plugin()]) + assert len(wascalled) == 1 + assert wascalled[0].ext == '.abc' + + def test_pytest_collect_directory(self, testdir): + wascalled = [] + class Plugin: + def pytest_collect_directory(self, path, parent): + wascalled.append(path.basename) + testdir.mkdir("hello") + testdir.mkdir("world") + testdir.pytestmain(testdir.tmpdir, plugins=[Plugin()]) + assert "hello" in wascalled + assert "world" in wascalled + +class TestPrunetraceback: + def test_collection_error(self, testdir): + p = testdir.makepyfile(""" + import not_exists + """) + result = testdir.runpytest(p) + assert "__import__" not in result.stdout.str(), "too long traceback" + result.stdout.fnmatch_lines([ + "*ERROR collecting*", + "*mport*not_exists*" + ]) + + def test_custom_repr_failure(self, testdir): + p = testdir.makepyfile(""" + import not_exists + """) + testdir.makeconftest(""" + import py + def pytest_collect_file(path, parent): + return MyFile(path, parent) + class MyError(Exception): + pass + class MyFile(py.test.collect.File): + def collect(self): + raise MyError() + def repr_failure(self, excinfo): + if excinfo.errisinstance(MyError): + return "hello world" + return py.test.collect.File.repr_failure(self, excinfo) + """) + + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*ERROR collecting*", + "*hello world*", + ]) + + @py.test.mark.xfail(reason="other mechanism for adding to reporting needed") + def test_collect_report_postprocessing(self, testdir): + p = testdir.makepyfile(""" + import not_exists + """) + testdir.makeconftest(""" + import py + def pytest_make_collect_report(__multicall__): + rep = __multicall__.execute() + rep.headerlines += ["header1"] + return rep + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*ERROR collecting*", + "*header1*", + ]) + + +class TestCustomConftests: + def test_ignore_collect_path(self, testdir): + testdir.makeconftest(""" + def pytest_ignore_collect(path, config): + return path.basename.startswith("x") or \ + path.basename == "test_one.py" + """) + testdir.mkdir("xy123").ensure("test_hello.py").write( + "syntax error" + ) + testdir.makepyfile("def test_hello(): pass") + testdir.makepyfile(test_one="syntax error") + result = testdir.runpytest() + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed*"]) + + def test_collectignore_exclude_on_option(self, testdir): + testdir.makeconftest(""" + collect_ignore = ['hello', 'test_world.py'] + def pytest_addoption(parser): + parser.addoption("--XX", action="store_true", default=False) + def pytest_configure(config): + if config.getvalue("XX"): + collect_ignore[:] = [] + """) + testdir.mkdir("hello") + testdir.makepyfile(test_world="def test_hello(): pass") + result = testdir.runpytest() + assert result.ret == 0 + assert "passed" not in result.stdout.str() + result = testdir.runpytest("--XX") + assert result.ret == 0 + assert "passed" in result.stdout.str() + + def test_pytest_fs_collect_hooks_are_seen(self, testdir): + conf = testdir.makeconftest(""" + import py + class MyModule(py.test.collect.Module): + pass + def pytest_collect_file(path, parent): + if path.ext == ".py": + return MyModule(path, parent) + """) + sub = testdir.mkdir("sub") + p = testdir.makepyfile("def test_x(): pass") + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines([ + "*MyModule*", + "*test_x*" + ]) + + def test_pytest_collect_file_from_sister_dir(self, testdir): + sub1 = testdir.mkpydir("sub1") + sub2 = testdir.mkpydir("sub2") + conf1 = testdir.makeconftest(""" + import py + class MyModule1(py.test.collect.Module): + pass + def pytest_collect_file(path, parent): + if path.ext == ".py": + return MyModule1(path, parent) + """) + conf1.move(sub1.join(conf1.basename)) + conf2 = testdir.makeconftest(""" + import py + class MyModule2(py.test.collect.Module): + pass + def pytest_collect_file(path, parent): + if path.ext == ".py": + return MyModule2(path, parent) + """) + conf2.move(sub2.join(conf2.basename)) + p = testdir.makepyfile("def test_x(): pass") + p.copy(sub1.join(p.basename)) + p.copy(sub2.join(p.basename)) + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines([ + "*MyModule1*", + "*MyModule2*", + "*test_x*" + ]) class TestCollection: def test_parsearg(self, testdir): @@ -13,16 +290,15 @@ class TestCollection: subdir.chdir() config = testdir.parseconfig(p.basename) rcol = Collection(config=config) - assert rcol.topdir == testdir.tmpdir + assert rcol.fspath == subdir parts = rcol._parsearg(p.basename) - assert parts[0] == "sub" - assert parts[1] == p.basename + + assert parts[0] == target + assert len(parts) == 1 + parts = rcol._parsearg(p.basename + "::test_func") + assert parts[0] == target + assert parts[1] == "test_func" assert len(parts) == 2 - parts = rcol._parsearg(p.basename + "::test_func") - assert parts[0] == "sub" - assert parts[1] == p.basename - assert parts[2] == "test_func" - assert len(parts) == 3 def test_collect_topdir(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -30,14 +306,14 @@ class TestCollection: config = testdir.parseconfig(id) topdir = testdir.tmpdir rcol = Collection(config) - assert topdir == rcol.topdir - hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() - assert len(items) == 1 - root = items[0].listchain()[0] - root_id = rcol.getid(root) - root2 = rcol.getbyid(root_id)[0] - assert root2.fspath == root.fspath + assert topdir == rcol.fspath + rootid = rcol.nodeid + #root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0] + #assert root2 == rcol, rootid + colitems = rcol.perform_collect([rcol.nodeid], genitems=False) + assert len(colitems) == 1 + assert colitems[0].fspath == p + def test_collect_protocol_single_function(self, testdir): p = testdir.makepyfile("def test_func(): pass") @@ -45,13 +321,14 @@ class TestCollection: config = testdir.parseconfig(id) topdir = testdir.tmpdir rcol = Collection(config) - assert topdir == rcol.topdir + assert topdir == rcol.fspath hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items assert len(items) == 1 item = items[0] assert item.name == "test_func" - newid = rcol.getid(item) + newid = item.nodeid assert newid == id py.std.pprint.pprint(hookrec.hookrecorder.calls) hookrec.hookrecorder.contains([ @@ -60,8 +337,8 @@ class TestCollection: ("pytest_collectstart", "collector.fspath == p"), ("pytest_make_collect_report", "collector.fspath == p"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == p"), - ("pytest_collectreport", "report.fspath == topdir") + ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), + ("pytest_collectreport", "report.nodeid == '.'") ]) def test_collect_protocol_method(self, testdir): @@ -70,19 +347,19 @@ class TestCollection: def test_method(self): pass """) - normid = p.basename + "::TestClass::test_method" + normid = p.basename + "::TestClass::()::test_method" for id in [p.basename, p.basename + "::TestClass", p.basename + "::TestClass::()", - p.basename + "::TestClass::()::test_method", normid, ]: config = testdir.parseconfig(id) rcol = Collection(config=config) - nodes = rcol.perform_collect() - assert len(nodes) == 1 - assert nodes[0].name == "test_method" - newid = rcol.getid(nodes[0]) + rcol.perform_collect() + items = rcol.items + assert len(items) == 1 + assert items[0].name == "test_method" + newid = items[0].nodeid assert newid == normid def test_collect_custom_nodes_multi_id(self, testdir): @@ -104,20 +381,21 @@ class TestCollection: config = testdir.parseconfig(id) rcol = Collection(config) hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items py.std.pprint.pprint(hookrec.hookrecorder.calls) assert len(items) == 2 hookrec.hookrecorder.contains([ ("pytest_collectstart", - "collector.fspath == collector.collection.topdir"), + "collector.fspath == collector.collection.fspath"), ("pytest_collectstart", "collector.__class__.__name__ == 'SpecialFile'"), ("pytest_collectstart", "collector.__class__.__name__ == 'Module'"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == p"), - ("pytest_collectreport", - "report.fspath == %r" % str(rcol.topdir)), + ("pytest_collectreport", "report.nodeid.startswith(p.basename)"), + #("pytest_collectreport", + # "report.fspath == %r" % str(rcol.fspath)), ]) def test_collect_subdir_event_ordering(self, testdir): @@ -128,134 +406,87 @@ class TestCollection: config = testdir.parseconfig() rcol = Collection(config) hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items assert len(items) == 1 py.std.pprint.pprint(hookrec.hookrecorder.calls) hookrec.hookrecorder.contains([ - ("pytest_collectstart", "collector.fspath == aaa"), ("pytest_collectstart", "collector.fspath == test_aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == test_aaa"), - ("pytest_collectreport", "report.fspath == aaa"), + ("pytest_collectreport", + "report.nodeid.startswith('aaa/test_aaa.py')"), ]) def test_collect_two_commandline_args(self, testdir): p = testdir.makepyfile("def test_func(): pass") aaa = testdir.mkpydir("aaa") bbb = testdir.mkpydir("bbb") - p.copy(aaa.join("test_aaa.py")) - p.move(bbb.join("test_bbb.py")) + test_aaa = aaa.join("test_aaa.py") + p.copy(test_aaa) + test_bbb = bbb.join("test_bbb.py") + p.move(test_bbb) id = "." config = testdir.parseconfig(id) rcol = Collection(config) hookrec = testdir.getreportrecorder(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items assert len(items) == 2 py.std.pprint.pprint(hookrec.hookrecorder.calls) hookrec.hookrecorder.contains([ - ("pytest_collectstart", "collector.fspath == aaa"), + ("pytest_collectstart", "collector.fspath == test_aaa"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == aaa"), - ("pytest_collectstart", "collector.fspath == bbb"), + ("pytest_collectreport", "report.nodeid == 'aaa/test_aaa.py'"), + ("pytest_collectstart", "collector.fspath == test_bbb"), ("pytest_pycollect_makeitem", "name == 'test_func'"), - ("pytest_collectreport", "report.fspath == bbb"), + ("pytest_collectreport", "report.nodeid == 'bbb/test_bbb.py'"), ]) def test_serialization_byid(self, testdir): p = testdir.makepyfile("def test_func(): pass") config = testdir.parseconfig() rcol = Collection(config) - items = rcol.perform_collect() + rcol.perform_collect() + items = rcol.items assert len(items) == 1 item, = items - id = rcol.getid(item) newcol = Collection(config) - item2, = newcol.getbyid(id) + item2, = newcol.perform_collect([item.nodeid], genitems=False) assert item2.name == item.name assert item2.fspath == item.fspath - item2b, = newcol.getbyid(id) - assert item2b is item2 - -class Test_gettopdir: - def test_gettopdir(self, testdir): - tmp = testdir.tmpdir - assert gettopdir([tmp]) == tmp - topdir = gettopdir([tmp.join("hello"), tmp.join("world")]) - assert topdir == tmp - somefile = tmp.ensure("somefile.py") - assert gettopdir([somefile]) == tmp - - def test_gettopdir_pypkg(self, testdir): - tmp = testdir.tmpdir - a = tmp.ensure('a', dir=1) - b = tmp.ensure('a', 'b', '__init__.py') - c = tmp.ensure('a', 'b', 'c.py') - Z = tmp.ensure('Z', dir=1) - assert gettopdir([c]) == a - assert gettopdir([c, Z]) == tmp - assert gettopdir(["%s::xyc" % c]) == a - assert gettopdir(["%s::xyc::abc" % c]) == a - assert gettopdir(["%s::xyc" % c, "%s::abc" % Z]) == tmp + item2b, = newcol.perform_collect([item.nodeid], genitems=False) + assert item2b == item2 def getargnode(collection, arg): - return collection.getbyid(collection._normalizearg(str(arg)))[0] + argpath = arg.relto(collection.fspath) + return collection.perform_collect([argpath], genitems=False)[0] class Test_getinitialnodes: - def test_onedir(self, testdir): - config = testdir.reparseconfig([testdir.tmpdir]) - c = Collection(config) - col = getargnode(c, testdir.tmpdir) - assert isinstance(col, py.test.collect.Directory) - for col in col.listchain(): - assert col.config is config - t2 = getargnode(c, testdir.tmpdir) - assert col == t2 - - def test_curdir_and_subdir(self, testdir, tmpdir): - a = tmpdir.ensure("a", dir=1) - config = testdir.reparseconfig([tmpdir, a]) - c = Collection(config) - - col1 = getargnode(c, tmpdir) - col2 = getargnode(c, a) - assert col1.name == tmpdir.basename - assert col2.name == 'a' - for col in (col1, col2): - for subcol in col.listchain(): - assert col.config is config - def test_global_file(self, testdir, tmpdir): x = tmpdir.ensure("x.py") config = testdir.reparseconfig([x]) - col = getargnode(Collection(config), x) + col = testdir.getnode(config, x) assert isinstance(col, py.test.collect.Module) assert col.name == 'x.py' - assert col.parent.name == tmpdir.basename + assert col.parent.name == testdir.tmpdir.basename assert col.parent.parent is None for col in col.listchain(): assert col.config is config - def test_global_dir(self, testdir, tmpdir): - x = tmpdir.ensure("a", dir=1) + def test_pkgfile(self, testdir): + testdir.chdir() + tmpdir = testdir.tmpdir + subdir = tmpdir.join("subdir") + x = subdir.ensure("x.py") + subdir.ensure("__init__.py") config = testdir.reparseconfig([x]) - col = getargnode(Collection(config), x) - assert isinstance(col, py.test.collect.Directory) - print(col.listchain()) - assert col.name == 'a' - assert col.parent is None - assert col.config is config - - def test_pkgfile(self, testdir, tmpdir): - tmpdir = tmpdir.join("subdir") - x = tmpdir.ensure("x.py") - tmpdir.ensure("__init__.py") - config = testdir.reparseconfig([x]) - col = getargnode(Collection(config), x) + col = testdir.getnode(config, x) assert isinstance(col, py.test.collect.Module) - assert col.name == 'x.py' - assert col.parent.name == x.dirpath().basename - assert col.parent.parent.parent is None + print col.obj + print col.listchain() + assert col.name == 'subdir/x.py' + assert col.parent.parent is None for col in col.listchain(): assert col.config is config From commits-noreply at bitbucket.org Sat Nov 6 09:57:05 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:05 -0500 (CDT) Subject: [py-svn] pytest-xdist commit 922c00c8c2fa: adapt to pytest changes, add looponfailingdirs ini-option Message-ID: <20101106085705.20D6A241420@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1289033880 -3600 # Node ID 922c00c8c2fa0bebe693cab54301a01c43159e55 # Parent 724fe27731c76cf8a91472cf8cdacfb336365765 adapt to pytest changes, add looponfailingdirs ini-option --- a/tox.ini +++ b/tox.ini @@ -16,5 +16,5 @@ deps= pytest pypi pexpect -[pytest] -addopts = -rf +#[pytest] +#addopts = -rf --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -301,7 +301,7 @@ class DSession: runner = self.config.pluginmanager.getplugin("runner") fspath = nodeid.split("::")[0] msg = "Slave %r crashed while running %r" %(slave.gateway.id, nodeid) - rep = runner.TestReport(nodeid, (), fspath, (fspath, None, fspath), (), + rep = runner.TestReport(nodeid, (fspath, None, fspath), (), "failed", msg, "???") enrich_report_with_platform_data(rep, slave) self.config.hook.pytest_runtest_logreport(report=rep) @@ -350,6 +350,6 @@ def enrich_report_with_platform_data(rep ver = "%s.%s.%s" % d['version_info'][:3] infoline = "[%s] %s -- Python %s %s" % ( d['id'], d['sysplatform'], ver, d['executable']) - # XXX more structured longrepr? + # XXX more structured longrepr? rep.longrepr = infoline + "\n\n" + str(rep.longrepr) --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5a4' +__version__ = '1.5a5' --- a/testing/test_slavemanage.py +++ b/testing/test_slavemanage.py @@ -11,7 +11,7 @@ def pytest_funcarg__hookrecorder(request def pytest_funcarg__hook(request): from xdist import newhooks - from pytest._core import HookRelay, PluginManager + from pytest.main import HookRelay, PluginManager from pytest import hookspec return HookRelay([hookspec, newhooks], PluginManager()) --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -156,6 +156,8 @@ class TestSlaveInteractor: assert not ev.kwargs ev = slave.popevent() assert ev.name == "collectreport" + ev = slave.popevent() + assert ev.name == "collectreport" rep = unserialize_report(ev.name, ev.kwargs['data']) assert rep.skipped ev = slave.popevent("collectionfinish") @@ -168,6 +170,8 @@ class TestSlaveInteractor: assert not ev.kwargs ev = slave.popevent() assert ev.name == "collectreport" + ev = slave.popevent() + assert ev.name == "collectreport" rep = unserialize_report(ev.name, ev.kwargs['data']) assert rep.failed ev = slave.popevent("collectionfinish") --- a/xdist/plugin.py +++ b/xdist/plugin.py @@ -142,7 +142,7 @@ where the configuration file was found. """ import sys -import py +import py, pytest def pytest_addoption(parser): group = parser.getgroup("xdist", "distributed and subprocess testing") @@ -178,6 +178,8 @@ def pytest_addoption(parser): ' remote distributed testing.', type="pathlist") parser.addini('rsyncignore', 'list of (relative) paths to be ignored ' 'for rsyncing.', type="pathlist") + parser.addini("looponfailroots", type="pathlist", + help="directories to check for changes", default=[py.path.local()]) # ------------------------------------------------------------------------- # distributed testing hooks @@ -218,10 +220,10 @@ def check_options(config): usepdb = config.option.usepdb # a core option if val("looponfail"): if usepdb: - raise config.Error("--pdb incompatible with --looponfail.") + raise pytest.UsageError("--pdb incompatible with --looponfail.") elif val("dist") != "no": if usepdb: - raise config.Error("--pdb incompatible with distributing tests.") + raise pytest.UsageError("--pdb incompatible with distributing tests.") def pytest_runtest_protocol(item): --- a/xdist/looponfail.py +++ b/xdist/looponfail.py @@ -7,21 +7,22 @@ the controlling process which should best never happen. """ -import py +import py, pytest import sys import execnet def looponfail_main(config): remotecontrol = RemoteControl(config) - # XXX better configure rootdir - gettopdir = config.pluginmanager.getplugin("session").gettopdir - rootdirs = [gettopdir(config.args)] + rootdirs = config.getini("looponfailroots") statrecorder = StatRecorder(rootdirs) try: while 1: remotecontrol.loop_once() if not remotecontrol.failures and remotecontrol.wasfailing: continue # the last failures passed, let's immediately rerun all + repr_pytest_looponfailinfo( + failreports=remotecontrol.failures, + rootdirs=rootdirs) statrecorder.waitonchange(checkinterval=2.0) except KeyboardInterrupt: print() @@ -29,7 +30,6 @@ def looponfail_main(config): class RemoteControl(object): def __init__(self, config): self.config = config - self.remote_topdir = None self.failures = [] def trace(self, *args): @@ -70,8 +70,8 @@ class RemoteControl(object): def runsession(self): try: - self.trace("sending", (self.remote_topdir, self.failures)) - self.channel.send((self.remote_topdir, self.failures)) + self.trace("sending", self.failures) + self.channel.send(self.failures) try: return self.channel.receive() except self.channel.RemoteError: @@ -85,15 +85,11 @@ class RemoteControl(object): self.setup() self.wasfailing = self.failures and len(self.failures) result = self.runsession() - topdir, failures, reports, collection_failed = result + failures, reports, collection_failed = result if collection_failed: reports = ["Collection failed, keeping previous failure set"] else: - self.remote_topdir, self.failures = topdir, failures - - repr_pytest_looponfailinfo( - failreports=reports, - rootdirs=[self.remote_topdir],) + self.failures = failures def repr_pytest_looponfailinfo(failreports, rootdirs): tr = py.io.TerminalWriter() @@ -147,23 +143,29 @@ class SlaveFailSession: def pytest_collection(self, session): self.session = session - self.collection = session.collection - self.topdir, self.trails = self.current_command - if self.topdir and self.trails: - self.topdir = py.path.local(self.topdir) - self.collection.topdir = self.topdir + self.collection = collection = session.collection + self.trails = self.current_command + hook = self.collection.ihook + try: + items = collection.perform_collect(self.trails or None) + except pytest.UsageError: + items = collection.perform_collect(None) + hook.pytest_collection_modifyitems(config=session.config, items=items) + hook.pytest_collection_finish(collection=collection) + return True + + if self.trails: col = self.collection items = [] for trail in self.trails: - names = col._parsearg(trail, base=self.topdir) + names = col._parsearg(trail) try: for node in col.matchnodes([col._topcollector], names): items.extend(col.genitems(node)) - except self.config.Error: + except pytest.UsageError: pass # ignore collect errors / vanished tests self.collection.items = items return True - self.topdir = session.collection.topdir def pytest_runtest_logreport(self, report): if report.failed: @@ -189,8 +191,7 @@ class SlaveFailSession: loc = rep.longrepr loc = str(getattr(loc, 'reprcrash', loc)) failreports.append(loc) - topdir = str(self.topdir) - self.channel.send((topdir, trails, failreports, self.collection_failed)) + self.channel.send((trails, failreports, self.collection_failed)) class StatRecorder: def __init__(self, rootdirlist): --- a/xdist/remote.py +++ b/xdist/remote.py @@ -53,8 +53,8 @@ class SlaveInteractor: if name == "runtests": ids = kwargs['ids'] for nodeid in ids: - for item in self.collection.getbyid(nodeid): - self.config.hook.pytest_runtest_protocol(item=item) + item = self._id2item[nodeid] + self.config.hook.pytest_runtest_protocol(item=item) elif name == "runtests_all": for item in self.collection.items: self.config.hook.pytest_runtest_protocol(item=item) @@ -63,9 +63,13 @@ class SlaveInteractor: return True def pytest_collection_finish(self, collection): - ids = [collection.getid(item) for item in collection.items] + self._id2item = {} + ids = [] + for item in collection.items: + self._id2item[item.nodeid] = item + ids.append(item.nodeid) self.sendevent("collectionfinish", - topdir=str(collection.topdir), + topdir=str(collection.fspath), ids=ids) #def pytest_runtest_logstart(self, nodeid, location, fspath): --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.5a4', + version='1.5a5', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=__doc__, license='GPLv2 or later', From commits-noreply at bitbucket.org Sat Nov 6 09:57:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:01 -0500 (CDT) Subject: [py-svn] pytest commit e4e9f0f7c4dd: document and refine py.test.fail helper and strike superflous ExceptionFailure class Message-ID: <20101106085701.09AAD243F48@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996651 -3600 # Node ID e4e9f0f7c4dd2e8b39991b38db44f676802f1b99 # Parent 567a0d7b8fa2be3820a64b6a4d5ecb326b383f7a document and refine py.test.fail helper and strike superflous ExceptionFailure class refine builtin organisation and start a new doc --- /dev/null +++ b/doc/example/layout1/setup.cfg @@ -0,0 +1,4 @@ +[pytest] +testfilepatterns = + ${topdir}/tests/unit/test_${basename} + ${topdir}/tests/functional/*.py --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -1,4 +1,4 @@ -import py, sys +import pytest, py, sys from pytest.plugin import python as funcargs class TestModule: @@ -1118,3 +1118,60 @@ def test_show_funcarg(testdir): "*temporary directory*", ] ) + +class TestRaises: + def test_raises(self): + source = "int('qwe')" + excinfo = py.test.raises(ValueError, source) + code = excinfo.traceback[-1].frame.code + s = str(code.fullsource) + assert s == source + + def test_raises_exec(self): + py.test.raises(ValueError, "a,x = []") + + def test_raises_syntax_error(self): + py.test.raises(SyntaxError, "qwe qwe qwe") + + def test_raises_function(self): + py.test.raises(ValueError, int, 'hello') + + def test_raises_callable_no_exception(self): + class A: + def __call__(self): + pass + try: + py.test.raises(ValueError, A()) + except py.test.raises.Exception: + pass + + @py.test.mark.skipif('sys.version < "2.5"') + def test_raises_as_contextmanager(self, testdir): + testdir.makepyfile(""" + from __future__ import with_statement + import py + + def test_simple(): + with py.test.raises(ZeroDivisionError) as excinfo: + assert isinstance(excinfo, py.code.ExceptionInfo) + 1/0 + print (excinfo) + assert excinfo.type == ZeroDivisionError + + def test_noraise(): + with py.test.raises(py.test.raises.Exception): + with py.test.raises(ValueError): + int() + + def test_raise_wrong_exception_passes_by(): + with py.test.raises(ZeroDivisionError): + with py.test.raises(ValueError): + 1/0 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + '*3 passed*', + ]) + + + --- a/testing/plugin/test_skipping.py +++ b/testing/plugin/test_skipping.py @@ -417,7 +417,7 @@ def test_skipped_reasons_functional(test result.stdout.fnmatch_lines([ "*test_two.py S", "*test_one.py ss", - "*SKIP*3*conftest.py:3: 'test'", + "*SKIP*3*conftest.py:3: test", ]) assert result.ret == 0 --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -7,7 +7,6 @@ import sys import pytest from py._code.code import TerminalRepr -import pytest cutdir = py.path.local(pytest.__file__).dirpath() @@ -22,11 +21,16 @@ def pytest_cmdline_main(config): showfuncargs(config) return 0 -def pytest_namespace(): - return {'collect': { +def pytest_namespace(__multicall__): + __multicall__.execute() + raises.Exception = pytest.fail.Exception + return { + 'raises' : raises, + 'collect': { 'Module': Module, 'Class': Class, 'Instance': Instance, 'Function': Function, 'Generator': Generator, - '_fillfuncargs': fillfuncargs}} + '_fillfuncargs': fillfuncargs} + } def pytest_funcarg__pytestconfig(request): """ the pytest config object with access to command line opts.""" @@ -300,17 +304,17 @@ class FunctionMixin(PyobjMixin): if teardown_func_or_meth is not None: teardown_func_or_meth(self.obj) - def _prunetraceback(self, traceback): + def _prunetraceback(self, excinfo): if hasattr(self, '_obj') and not self.config.option.fulltrace: code = py.code.Code(self.obj) path, firstlineno = code.path, code.firstlineno + traceback = excinfo.traceback ntraceback = traceback.cut(path=path, firstlineno=firstlineno) if ntraceback == traceback: ntraceback = ntraceback.cut(path=path) if ntraceback == traceback: ntraceback = ntraceback.cut(excludepath=cutdir) - traceback = ntraceback.filter() - return traceback + excinfo.traceback = ntraceback.filter() def _repr_failure_py(self, excinfo, style="long"): if excinfo.errisinstance(FuncargRequest.LookupError): @@ -746,3 +750,71 @@ def getlocation(function, curdir): if fn.relto(curdir): fn = fn.relto(curdir) return "%s:%d" %(fn, lineno+1) + +# builtin pytest.raises helper + +def raises(ExpectedException, *args, **kwargs): + """ assert that a code block/function call raises an exception. + + If using Python 2.5 or above, you may use this function as a + context manager:: + + >>> with raises(ZeroDivisionError): + ... 1/0 + + Or you can one of two forms: + + if args[0] is callable: raise AssertionError if calling it with + the remaining arguments does not raise the expected exception. + if args[0] is a string: raise AssertionError if executing the + the string in the calling scope does not raise expected exception. + examples: + >>> x = 5 + >>> raises(TypeError, lambda x: x + 'hello', x=x) + >>> raises(TypeError, "x + 'hello'") + """ + __tracebackhide__ = True + + if not args: + return RaisesContext(ExpectedException) + elif isinstance(args[0], str): + code, = args + assert isinstance(code, str) + frame = sys._getframe(1) + loc = frame.f_locals.copy() + loc.update(kwargs) + #print "raises frame scope: %r" % frame.f_locals + try: + code = py.code.Source(code).compile() + py.builtin.exec_(code, frame.f_globals, loc) + # XXX didn'T mean f_globals == f_locals something special? + # this is destroyed here ... + except ExpectedException: + return py.code.ExceptionInfo() + else: + func = args[0] + try: + func(*args[1:], **kwargs) + except ExpectedException: + return py.code.ExceptionInfo() + k = ", ".join(["%s=%r" % x for x in kwargs.items()]) + if k: + k = ', ' + k + expr = '%s(%r%s)' %(getattr(func, '__name__', func), args, k) + pytest.fail("DID NOT RAISE") + +class RaisesContext(object): + def __init__(self, ExpectedException): + self.ExpectedException = ExpectedException + self.excinfo = None + + def __enter__(self): + self.excinfo = object.__new__(py.code.ExceptionInfo) + return self.excinfo + + def __exit__(self, *tp): + __tracebackhide__ = True + if tp[0] is None: + pytest.fail("DID NOT RAISE") + self.excinfo.__init__(tp) + return issubclass(self.excinfo.type, self.ExpectedException) --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -1,5 +1,5 @@ """ -py.test / pytest API for unit and functional testing with Python. +unit and functional testing with Python. see http://pytest.org for documentation and details --- /dev/null +++ b/doc/example/builtin.txt @@ -0,0 +1,36 @@ + +writing well integrated assertion helpers +======================================================== + +If you have a test helper function called from a test you can +use the ``pytest.fail``_ builtin to cleanly fail a test with a message. +The test support function will never itself show up in the traceback. +Example:: + + # content of test_checkconfig.py + import pytest + def checkconfig(x): + __tracebackhide__ = True + if not hasattr(x, "config"): + pytest.fail("not configured: %s" %(x,)) + + def test_something(): + checkconfig(42) + +The ``__tracebackhide__`` setting influences py.test showing +of tracebacks: the ``checkconfig`` function will not be shown +unless the ``--fulltrace`` command line option is specified. +Let's run our little function:: + + $ py.test -q + F + ================================= FAILURES ================================= + ______________________________ test_something ______________________________ + + def test_something(): + > checkconfig(42) + E Failed: not configured: 42 + + test_checkconfig.py:8: Failed + 1 failed in 0.02 seconds + --- /dev/null +++ b/doc/builtin.txt @@ -0,0 +1,70 @@ + +pytest builtin helpers +================================================ + + +builtin function arguments +----------------------------------------------------- + +You can ask for available builtin or project-custom +:ref:`function arguments` by typing:: + + $ py.test --funcargs + pytestconfig + the pytest config object with access to command line opts. + capsys + captures writes to sys.stdout/sys.stderr and makes + them available successively via a ``capsys.readouterr()`` method + which returns a ``(out, err)`` tuple of captured snapshot strings. + + capfd + captures writes to file descriptors 1 and 2 and makes + snapshotted ``(out, err)`` string tuples available + via the ``capsys.readouterr()`` method. If the underlying + platform does not have ``os.dup`` (e.g. Jython) tests using + this funcarg will automatically skip. + + tmpdir + return a temporary directory path object + unique to each test function invocation, + created as a sub directory of the base temporary + directory. The returned object is a `py.path.local`_ + path object. + + monkeypatch + The returned ``monkeypatch`` funcarg provides these + helper methods to modify objects, dictionaries or os.environ:: + + monkeypatch.setattr(obj, name, value, raising=True) + monkeypatch.delattr(obj, name, raising=True) + monkeypatch.setitem(mapping, name, value) + monkeypatch.delitem(obj, name, raising=True) + monkeypatch.setenv(name, value, prepend=False) + monkeypatch.delenv(name, value, raising=True) + monkeypatch.syspath_prepend(path) + + All modifications will be undone when the requesting + test function finished its execution. The ``raising`` + parameter determines if a KeyError or AttributeError + will be raised if the set/deletion operation has no target. + + recwarn + Return a WarningsRecorder instance that provides these methods: + + * ``pop(category=None)``: return last warning matching the category. + * ``clear()``: clear list of warnings + + +builtin py.test.* helpers +----------------------------------------------------- + +You can always use an interactive Python prompt and type:: + + import pytest + help(pytest) + +to get an overview on available globally available helpers. + +.. automodule:: pytest + :members: + --- a/doc/apiref.txt +++ b/doc/apiref.txt @@ -6,9 +6,10 @@ py.test reference documentation .. toctree:: :maxdepth: 2 - + + builtin.txt customize.txt - assert.txt + assert.txt funcargs.txt xunit_setup.txt capture.txt @@ -16,7 +17,7 @@ py.test reference documentation tmpdir.txt skipping.txt mark.txt - recwarn.txt + recwarn.txt unittest.txt doctest.txt - + --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -7,6 +7,7 @@ import py import pytest import os, sys +tracebackcutdir = py.path.local(pytest.__file__).dirpath() # exitcodes for the command line EXIT_OK = 0 @@ -403,14 +404,14 @@ class Node(object): current = current.parent return current - def _prunetraceback(self, traceback): - return traceback + def _prunetraceback(self, excinfo): + pass def _repr_failure_py(self, excinfo, style=None): if self.config.option.fulltrace: style="long" else: - excinfo.traceback = self._prunetraceback(excinfo.traceback) + self._prunetraceback(excinfo) # XXX should excinfo.getrepr record all data and toterminal() # process it? if style is None: @@ -448,14 +449,14 @@ class Collector(Node): """ internal helper method to cache results of calling collect(). """ return self._memoizedcall('_collected', self.collect) - def _prunetraceback(self, traceback): + def _prunetraceback(self, excinfo): if hasattr(self, 'fspath'): path = self.fspath + traceback = excinfo.traceback ntraceback = traceback.cut(path=self.fspath) if ntraceback == traceback: - ntraceback = ntraceback.cut(excludepath=py._pydir) - traceback = ntraceback.filter() - return traceback + ntraceback = ntraceback.cut(excludepath=tracebackcutdir) + excinfo.traceback = ntraceback.filter() class FSCollector(Collector): def __init__(self, fspath, parent=None, config=None, collection=None): --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -195,7 +195,7 @@ class TestCollectonly: assert len(cols) == 0 linecomp.assert_contains_lines(""" - !!! Skipped: 'nomod' !!! + !!! Skipped: nomod !!! """) def test_collectonly_failed_module(self, testdir, linecomp): --- a/pytest/plugin/runner.py +++ b/pytest/plugin/runner.py @@ -7,11 +7,9 @@ from py._code.code import TerminalRepr def pytest_namespace(): return { - 'raises' : raises, + 'fail' : fail, 'skip' : skip, 'importorskip' : importorskip, - 'fail' : fail, - 'xfail' : xfail, 'exit' : exit, } @@ -337,13 +335,12 @@ class OutcomeException(Exception): """ OutcomeException and its subclass instances indicate and contain info about test and collection outcomes. """ - def __init__(self, msg=None, excinfo=None): + def __init__(self, msg=None): self.msg = msg - self.excinfo = excinfo def __repr__(self): if self.msg: - return repr(self.msg) + return str(self.msg) return "<%s instance>" %(self.__class__.__name__,) __str__ = __repr__ @@ -356,19 +353,8 @@ class Failed(OutcomeException): """ raised from an explicit call to py.test.fail() """ __module__ = 'builtins' -class XFailed(OutcomeException): - """ raised from an explicit call to py.test.xfail() """ - __module__ = 'builtins' - -class ExceptionFailure(Failed): - """ raised by py.test.raises on an exception-assertion mismatch. """ - def __init__(self, expr, expected, msg=None, excinfo=None): - Failed.__init__(self, msg=msg, excinfo=excinfo) - self.expr = expr - self.expected = expected - class Exit(KeyboardInterrupt): - """ raised by py.test.exit for immediate program exits without tracebacks and reporter/summary. """ + """ raised for immediate program exits (no tracebacks/summaries)""" def __init__(self, msg="unknown reason"): self.msg = msg KeyboardInterrupt.__init__(self, msg) @@ -384,103 +370,20 @@ exit.Exception = Exit def skip(msg=""): """ skip an executing test with the given message. Note: it's usually - better use the py.test.mark.skipif marker to declare a test to be + better to use the py.test.mark.skipif marker to declare a test to be skipped under certain conditions like mismatching platforms or dependencies. See the pytest_skipping plugin for details. """ __tracebackhide__ = True raise Skipped(msg=msg) - skip.Exception = Skipped def fail(msg=""): """ explicitely fail an currently-executing test with the given Message. """ __tracebackhide__ = True raise Failed(msg=msg) - fail.Exception = Failed -def xfail(reason=""): - """ xfail an executing test or setup functions, taking an optional - reason string. - """ - __tracebackhide__ = True - raise XFailed(reason) -xfail.Exception = XFailed - -def raises(ExpectedException, *args, **kwargs): - """ assert that a code block/function call raises an exception. - - If using Python 2.5 or above, you may use this function as a - context manager:: - - >>> with raises(ZeroDivisionError): - ... 1/0 - - Or you can one of two forms: - - if args[0] is callable: raise AssertionError if calling it with - the remaining arguments does not raise the expected exception. - if args[0] is a string: raise AssertionError if executing the - the string in the calling scope does not raise expected exception. - examples: - >>> x = 5 - >>> raises(TypeError, lambda x: x + 'hello', x=x) - >>> raises(TypeError, "x + 'hello'") - """ - __tracebackhide__ = True - - if not args: - return RaisesContext(ExpectedException) - elif isinstance(args[0], str): - code, = args - assert isinstance(code, str) - frame = sys._getframe(1) - loc = frame.f_locals.copy() - loc.update(kwargs) - #print "raises frame scope: %r" % frame.f_locals - try: - code = py.code.Source(code).compile() - py.builtin.exec_(code, frame.f_globals, loc) - # XXX didn'T mean f_globals == f_locals something special? - # this is destroyed here ... - except ExpectedException: - return py.code.ExceptionInfo() - else: - func = args[0] - try: - func(*args[1:], **kwargs) - except ExpectedException: - return py.code.ExceptionInfo() - k = ", ".join(["%s=%r" % x for x in kwargs.items()]) - if k: - k = ', ' + k - expr = '%s(%r%s)' %(getattr(func, '__name__', func), args, k) - raise ExceptionFailure(msg="DID NOT RAISE", - expr=args, expected=ExpectedException) - - -class RaisesContext(object): - - def __init__(self, ExpectedException): - self.ExpectedException = ExpectedException - self.excinfo = None - - def __enter__(self): - self.excinfo = object.__new__(py.code.ExceptionInfo) - return self.excinfo - - def __exit__(self, *tp): - __tracebackhide__ = True - if tp[0] is None: - raise ExceptionFailure(msg="DID NOT RAISE", - expr=(), - expected=self.ExpectedException) - self.excinfo.__init__(tp) - return issubclass(self.excinfo.type, self.ExpectedException) - - -raises.Exception = ExceptionFailure def importorskip(modname, minversion=None): """ return imported module if it has a higher __version__ than the @@ -503,5 +406,3 @@ def importorskip(modname, minversion=Non py.test.skip("module %r has __version__ %r, required is: %r" %( modname, verattr, minversion)) return mod - - --- a/doc/example/index.txt +++ b/doc/example/index.txt @@ -7,6 +7,7 @@ Usages and Examples .. toctree:: :maxdepth: 2 + builtin.txt pythoncollection.txt controlskip.txt mysetup.txt --- a/testing/plugin/test_runner.py +++ b/testing/plugin/test_runner.py @@ -319,61 +319,6 @@ def test_runtest_in_module_ordering(test "*2 passed*" ]) -class TestRaises: - def test_raises(self): - source = "int('qwe')" - excinfo = py.test.raises(ValueError, source) - code = excinfo.traceback[-1].frame.code - s = str(code.fullsource) - assert s == source - - def test_raises_exec(self): - py.test.raises(ValueError, "a,x = []") - - def test_raises_syntax_error(self): - py.test.raises(SyntaxError, "qwe qwe qwe") - - def test_raises_function(self): - py.test.raises(ValueError, int, 'hello') - - def test_raises_callable_no_exception(self): - class A: - def __call__(self): - pass - try: - py.test.raises(ValueError, A()) - except py.test.raises.Exception: - pass - - @py.test.mark.skipif('sys.version < "2.5"') - def test_raises_as_contextmanager(self, testdir): - testdir.makepyfile(""" - from __future__ import with_statement - import py - - def test_simple(): - with py.test.raises(ZeroDivisionError) as excinfo: - assert isinstance(excinfo, py.code.ExceptionInfo) - 1/0 - print (excinfo) - assert excinfo.type == ZeroDivisionError - - def test_noraise(): - with py.test.raises(py.test.raises.Exception): - with py.test.raises(ValueError): - int() - - def test_raise_wrong_exception_passes_by(): - with py.test.raises(ZeroDivisionError): - with py.test.raises(ValueError): - 1/0 - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - '*3 passed*', - ]) - - def test_pytest_exit(): try: --- a/pytest/plugin/skipping.py +++ b/pytest/plugin/skipping.py @@ -1,154 +1,8 @@ """ -advanced skipping for python test functions, classes or modules. - -With this plugin you can mark test functions for conditional skipping -or as "xfail", expected-to-fail. Skipping a test will avoid running it -while xfail-marked tests will run and result in an inverted outcome: -a pass becomes a failure and a fail becomes a semi-passing one. - -The need for skipping a test is usually connected to a condition. -If a test fails under all conditions then it's probably better -to mark your test as 'xfail'. - -By passing ``-rxs`` to the terminal reporter you will see extra -summary information on skips and xfail-run tests at the end of a test run. - -.. _skipif: - -Skipping a single function -------------------------------------------- - -Here is an example for marking a test function to be skipped -when run on a Python3 interpreter:: - - @py.test.mark.skipif("sys.version_info >= (3,0)") - def test_function(): - ... - -During test function setup the skipif condition is -evaluated by calling ``eval(expr, namespace)``. The namespace -contains the ``sys`` and ``os`` modules and the test -``config`` object. The latter allows you to skip based -on a test configuration value e.g. like this:: - - @py.test.mark.skipif("not config.getvalue('db')") - def test_function(...): - ... - -Create a shortcut for your conditional skip decorator -at module level like this:: - - win32only = py.test.mark.skipif("sys.platform != 'win32'") - - @win32only - def test_function(): - ... - - -skip groups of test functions --------------------------------------- - -As with all metadata function marking you can do it at -`whole class- or module level`_. Here is an example -for skipping all methods of a test class based on platform:: - - class TestPosixCalls: - pytestmark = py.test.mark.skipif("sys.platform == 'win32'") - - def test_function(self): - # will not be setup or run under 'win32' platform - # - -The ``pytestmark`` decorator will be applied to each test function. -If your code targets python2.6 or above you can equivalently use -the skipif decorator on classes:: - - @py.test.mark.skipif("sys.platform == 'win32'") - class TestPosixCalls: - - def test_function(self): - # will not be setup or run under 'win32' platform - # - -It is fine in general to apply multiple "skipif" decorators -on a single function - this means that if any of the conditions -apply the function will be skipped. - -.. _`whole class- or module level`: mark.html#scoped-marking - -.. _xfail: - -mark a test function as **expected to fail** -------------------------------------------------------- - -You can use the ``xfail`` marker to indicate that you -expect the test to fail:: - - @py.test.mark.xfail - def test_function(): - ... - -This test will be run but no traceback will be reported -when it fails. Instead terminal reporting will list it in the -"expected to fail" or "unexpectedly passing" sections. - -Same as with skipif_ you can also selectively expect a failure -depending on platform:: - - @py.test.mark.xfail("sys.version_info >= (3,0)") - def test_function(): - ... - -To not run a test and still regard it as "xfailed":: - - @py.test.mark.xfail(..., run=False) - -To specify an explicit reason to be shown with xfailure detail:: - - @py.test.mark.xfail(..., reason="my reason") - -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:: - - def test_function(): - if not valid_config(): - py.test.xfail("unsuppored configuration") - - -skipping on a missing import dependency --------------------------------------------------- - -You can use the following import helper at module level -or within a test or test setup function:: - - docutils = py.test.importorskip("docutils") - -If ``docutils`` cannot be imported here, this will lead to a -skip outcome of the test. You can also skip dependeing if -if a library does not come with a high enough version:: - - docutils = py.test.importorskip("docutils", minversion="0.3") - -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:: - - def test_function(): - if not valid_config(): - py.test.skip("unsuppored configuration") - +plugin providing skip and xfail functionality. """ -import py +import py, pytest def pytest_addoption(parser): group = parser.getgroup("general") @@ -156,6 +10,18 @@ def pytest_addoption(parser): action="store_true", dest="runxfail", default=False, help="run tests even if they are marked xfail") +def pytest_namespace(): + return dict(xfail=xfail) + +class XFailed(pytest.fail.Exception): + """ raised from an explicit call to py.test.xfail() """ + +def xfail(reason=""): + """ xfail an executing test or setup functions with the given reason.""" + __tracebackhide__ = True + raise XFailed(reason) +xfail.Exception = XFailed + class MarkEvaluator: def __init__(self, item, name): self.item = item --- a/testing/plugin/test_resultlog.py +++ b/testing/plugin/test_resultlog.py @@ -89,7 +89,7 @@ class TestWithFunctionIntegration: assert lines[0].startswith("S ") assert lines[0].endswith("test_collection_skip.py") assert lines[1].startswith(" ") - assert lines[1].endswith("test_collection_skip.py:1: Skipped: 'hello'") + assert lines[1].endswith("test_collection_skip.py:1: Skipped: hello") lines = self.getresultlog(testdir, fail) assert lines --- a/pytest/_core.py +++ b/pytest/_core.py @@ -7,7 +7,7 @@ assert py.__version__.split(".")[:2] >= "%s is too old, remove or upgrade 'py'" % (py.__version__)) default_plugins = ( - "config session terminal python runner pdb capture unittest mark skipping " + "config session terminal runner python pdb capture unittest mark skipping " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " "junitxml doctest").split() From commits-noreply at bitbucket.org Sat Nov 6 09:57:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:00 -0500 (CDT) Subject: [py-svn] pytest commit 3253d770b03c: remove imperative xfail, this test passes Message-ID: <20101106085700.7C135241053@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288768153 -3600 # Node ID 3253d770b03c29b46f2d5c96f46b00b01f882551 # Parent 9a0938131cfa4fb581bfbe872bff1625dfbad4d2 remove imperative xfail, this test passes --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -424,7 +424,6 @@ class TestTerminalFunctional: "*test_verbose_reporting.py:10: test_gen*FAIL*", ]) assert result.ret == 1 - py.test.xfail("fix dist-testing") pytestconfig.pluginmanager.skipifmissing("xdist") result = testdir.runpytest(p1, '-v', '-n 1') result.stdout.fnmatch_lines([ From commits-noreply at bitbucket.org Sat Nov 6 09:57:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:00 -0500 (CDT) Subject: [py-svn] pytest commit 9ff5086b4e67: introduce norecursedirs config option, remove recfilter() Message-ID: <20101106085700.AEE4D24194E@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288909286 -3600 # Node ID 9ff5086b4e67207b3caf98c5554269e1f126557d # Parent 2dfb0db0864d075300712366a86cdf642c7c68d9 introduce norecursedirs config option, remove recfilter() --- a/pytest/plugin/helpconfig.py +++ b/pytest/plugin/helpconfig.py @@ -44,8 +44,11 @@ def showhelp(config): tw.line("setup.cfg or tox.ini options to be put into [pytest] section:") tw.line() - for name, (help, type) in sorted(config._parser._inidict.items()): - line = " %-15s %s" %(name, help) + for name, (help, type, default) in sorted(config._parser._inidict.items()): + if type is None: + type = "string" + spec = "%s (%s)" % (name, type) + line = " %-24s %s" %(spec, help) tw.line(line[:tw.fullwidth]) tw.line() ; tw.line() @@ -68,7 +71,7 @@ conftest_options = [ def pytest_report_header(config): lines = [] if config.option.debug or config.option.traceconfig: - lines.append("using: pytest-%s pylib-%s" % + lines.append("using: pytest-%s pylib-%s" % (pytest.__version__,py.__version__)) if config.option.traceconfig: --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -10,10 +10,6 @@ def pytest_cmdline_parse(pluginmanager, config.parse(args) return config -def pytest_addoption(parser): - parser.addini('addopts', 'default command line arguments') - parser.addini('minversion', 'minimally required pytest version') - class Parser: """ Parser for command line arguments. """ @@ -72,9 +68,10 @@ class Parser: setattr(option, name, value) return args - def addini(self, name, description, type=None): + def addini(self, name, help, type=None, default=None): """ add an ini-file option with the given name and description. """ - self._inidict[name] = (description, type) + assert type in (None, "pathlist", "args") + self._inidict[name] = (help, type, default) class OptionGroup: def __init__(self, name, description="", parser=None): @@ -293,13 +290,15 @@ class Config(object): sys.stderr.write(err) raise + def _initini(self, args): + self.inicfg = getcfg(args, ["setup.cfg", "tox.ini",]) + self._parser.addini('addopts', 'extra command line options', 'args') + self._parser.addini('minversion', 'minimally required pytest version') + def _preparse(self, args, addopts=True): - self.inicfg = {} - self.inicfg = getcfg(args, ["setup.cfg", "tox.ini",]) - if self.inicfg and addopts: - newargs = self.inicfg.get("addopts", None) - if newargs: - args[:] = py.std.shlex.split(newargs) + args + self._initini(args) + if addopts: + args[:] = self.getini("addopts") + args self._checkversion() self.pluginmanager.consider_setuptools_entrypoints() self.pluginmanager.consider_env() @@ -358,20 +357,25 @@ class Config(object): specified name hasn't been registered through a prior ``parse.addini`` call (usually from a plugin), a ValueError is raised. """ try: - description, type = self._parser._inidict[name] + description, type, default = self._parser._inidict[name] except KeyError: raise ValueError("unknown configuration value: %r" %(name,)) try: value = self.inicfg[name] except KeyError: - return # None indicates nothing found + if default is not None: + return default + return {'pathlist': [], 'args': [], None: ''}.get(type) if type == "pathlist": dp = py.path.local(self.inicfg.config.path).dirpath() l = [] for relpath in py.std.shlex.split(value): l.append(dp.join(relpath, abs=True)) return l + elif type == "args": + return py.std.shlex.split(value) else: + assert type is None return value def _getconftest_pathlist(self, name, path=None): --- a/testing/test_collect.py +++ b/testing/test_collect.py @@ -99,14 +99,25 @@ class TestCollectFS: tmpdir.ensure(".whatever", 'test_notfound.py') tmpdir.ensure(".bzr", 'test_notfound.py') tmpdir.ensure("normal", 'test_found.py') - tmpdir.ensure('test_found.py') - col = testdir.getnode(testdir.parseconfig(tmpdir), tmpdir) - items = col.collect() - names = [x.name for x in items] - assert len(items) == 2 - assert 'normal' in names - assert 'test_found.py' in names + result = testdir.runpytest("--collectonly") + s = result.stdout.str() + assert "test_notfound" not in s + assert "test_found" in s + + def test_custom_norecursedirs(self, testdir): + testdir.makeini(""" + [pytest] + norecursedirs = mydir xyz* + """) + tmpdir = testdir.tmpdir + tmpdir.ensure("mydir", "test_hello.py").write("def test_1(): pass") + tmpdir.ensure("xyz123", "test_2.py").write("def test_2(): 0/0") + tmpdir.ensure("xy", "test_ok.py").write("def test_3(): pass") + rec = testdir.inline_run() + rec.assertoutcome(passed=1) + rec = testdir.inline_run("xyz123/test_2.py") + rec.assertoutcome(failed=1) def test_found_certain_testfiles(self, testdir): p1 = testdir.makepyfile(test_found = "pass", found_test="pass") --- a/testing/test_config.py +++ b/testing/test_config.py @@ -152,6 +152,23 @@ class TestConfigAPI: assert l[1] == p.dirpath('world/sub.py') py.test.raises(ValueError, config.getini, 'other') + def test_addini_args(self, testdir): + testdir.makeconftest(""" + def pytest_addoption(parser): + parser.addini("args", "new args", type="args") + parser.addini("a2", "", "args", default="1 2 3".split()) + """) + p = testdir.makeini(""" + [pytest] + args=123 "123 hello" "this" + """) + config = testdir.parseconfig() + l = config.getini("args") + assert len(l) == 3 + assert l == ["123", "123 hello", "this"] + l = config.getini("a2") + assert l == list("123") + def test_options_on_small_file_do_not_blow_up(testdir): def runfiletest(opts): reprec = testdir.inline_run(*opts) --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -16,7 +16,8 @@ EXIT_INTERNALERROR = 3 EXIT_NOHOSTS = 4 def pytest_addoption(parser): - + parser.addini("norecursedirs", "directory patterns to avoid for recursion", + type="args", default=('.*', 'CVS', '_darcs', '{arch}')) group = parser.getgroup("general", "running and selection options") group._addoption('-x', '--exitfirst', action="store_true", default=False, dest="exitfirst", @@ -107,13 +108,15 @@ def pytest_ignore_collect(path, config): return path in ignore_paths def pytest_collect_directory(path, parent): - if not parent.recfilter(path): # by default special ".cvs", ... - # check if cmdline specified this dir or a subdir directly - for arg in parent.collection._argfspaths: - if path == arg or arg.relto(path): - break - else: - return + # check if cmdline specified this dir or a subdir directly + for arg in parent.collection._argfspaths: + if path == arg or arg.relto(path): + break + else: + patterns = parent.config.getini("norecursedirs") + for pat in patterns or []: + if path.check(fnmatch=pat): + return return Directory(path, parent=parent) class Session(object): @@ -465,10 +468,6 @@ class File(FSCollector): """ base class for collecting tests from a file. """ class Directory(FSCollector): - def recfilter(self, path): - if path.check(dir=1, dotfile=0): - return path.basename not in ('CVS', '_darcs', '{arch}') - def collect(self): l = [] for path in self.fspath.listdir(sort=True): From commits-noreply at bitbucket.org Sat Nov 6 09:57:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 03:57:00 -0500 (CDT) Subject: [py-svn] pytest commit 567a0d7b8fa2: some more refinements to docs Message-ID: <20101106085700.D8EB2241D22@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1288996645 -3600 # Node ID 567a0d7b8fa2be3820a64b6a4d5ecb326b383f7a # Parent 9ff5086b4e67207b3caf98c5554269e1f126557d some more refinements to docs --- a/doc/examples.txt +++ /dev/null @@ -1,13 +0,0 @@ - -.. _examples: - -Usages and Examples -=========================================== - -.. toctree:: - :maxdepth: 2 - - example/controlskip.txt - example/mysetup.txt - example/detectpytest.txt - example/nonpython.txt --- a/doc/example/test_collectonly.py +++ /dev/null @@ -1,11 +0,0 @@ - -# run this with $ py.test --collectonly test_collectonly.py -# -def test_function(): - pass - -class TestClass: - def test_method(self): - pass - def test_anothermethod(self): - pass --- /dev/null +++ b/doc/example/simple.txt @@ -0,0 +1,137 @@ + +.. highlightlang:: python + +simple patterns using hooks +========================================================== + +pass different values to a test function, depending on command line options +---------------------------------------------------------------------------- + +Suppose we want to write a test that depends on a command line option. +Here is a basic pattern how to achieve this:: + + # content of test_sample.py + def test_answer(cmdopt): + if cmdopt == "type1": + print ("first") + elif cmdopt == "type2": + print ("second") + assert 0 # to see what was printed + + +For this to work we need to add a command line option and +provide the ``cmdopt`` through a function argument factory:: + + # content of conftest.py + def pytest_addoption(parser): + parser.addoption("--cmdopt", action="store", default="type1", + help="my option: type1 or type2") + + def pytest_funcarg__cmdopt(request): + return request.config.option.cmdopt + +Let's run this without supplying our new command line option:: + + $ py.test -q + F + ================================= FAILURES ================================= + _______________________________ test_answer ________________________________ + + cmdopt = 'type1' + + def test_answer(cmdopt): + if cmdopt == "type1": + print ("first") + elif cmdopt == "type2": + print ("second") + > assert 0 # to see what was printed + E assert 0 + + test_sample.py:6: AssertionError + ----------------------------- Captured stdout ------------------------------ + first + 1 failed in 0.02 seconds + +And now with supplying a command line option:: + + $ py.test -q --cmdopt=type2 + F + ================================= FAILURES ================================= + _______________________________ test_answer ________________________________ + + cmdopt = 'type2' + + def test_answer(cmdopt): + if cmdopt == "type1": + print ("first") + elif cmdopt == "type2": + print ("second") + > assert 0 # to see what was printed + E assert 0 + + test_sample.py:6: AssertionError + ----------------------------- Captured stdout ------------------------------ + second + 1 failed in 0.02 seconds + +Ok, this completes the basic pattern. However, one often rather +wants to process command line options outside of the test and +rather pass in different or more complex objects. See the +next example or refer to :ref:`mysetup` for more information +on real-life examples. + +generating parameters combinations, depending on command line +---------------------------------------------------------------------------- + +Let's say we want to execute a test with different parameters +and the parameter range shall be determined by a command +line argument. Let's first write a simple computation test:: + + # content of test_compute.py + + def test_compute(param1): + assert param1 < 4 + +Now we add a test configuration like this:: + + # content of conftest.py + + def pytest_addoption(parser): + parser.addoption("--all", action="store_true", + help="run all combinations") + + def pytest_generate_tests(metafunc): + if 'param1' in metafunc.funcargnames: + if metafunc.config.option.all: + end = 5 + else: + end = 2 + for i in range(end): + metafunc.addcall(funcargs={'param1': i}) + +This means that we only run 2 tests if we do not pass ``--all``:: + + $ py.test -q test_compute.py + .. + 2 passed in 0.01 seconds + +We run only two computations, so we see two dots. +let's run the full monty:: + + $ py.test -q --all test_compute.py + ....F + ================================= FAILURES ================================= + _____________________________ test_compute[4] ______________________________ + + param1 = 4 + + def test_compute(param1): + > assert param1 < 4 + E assert 4 < 4 + + test_compute.py:3: AssertionError + 1 failed, 4 passed in 0.03 seconds + + +As expected when running the full range of ``param1`` values +we'll get an error on the last one. --- /dev/null +++ b/doc/example/index.txt @@ -0,0 +1,15 @@ + +.. _examples: + +Usages and Examples +=========================================== + +.. toctree:: + :maxdepth: 2 + + pythoncollection.txt + controlskip.txt + mysetup.txt + detectpytest.txt + nonpython.txt + simple.txt --- /dev/null +++ b/doc/example/pythoncollection.txt @@ -0,0 +1,29 @@ +Changing standard (Python) test discovery +=============================================== + +changing directory recursion +----------------------------------------------------- + +You can set the :confval:`norecursedirs` option in an ini-file, for example your ``setup.cfg`` in the project root directory:: + + # content of setup.cfg + [pytest] + norecursedirs = .svn _build tmp* + +This would tell py.test to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. + + +finding out what is collected +----------------------------------------------- + +You can always peek at the collection tree without running tests like this:: + + $ py.test --collectonly collectonly.py + + + + + + + + --- a/doc/conf.py +++ b/doc/conf.py @@ -258,7 +258,7 @@ epub_copyright = u'2010, holger krekel e # Example configuration for intersphinx: refer to the Python standard library. -intersphinx_mapping = {'http://docs.python.org/': None} +intersphinx_mapping = {} # 'http://docs.python.org/': None} def setup(app): #from sphinx.ext.autodoc import cut_lines #app.connect('autodoc-process-docstring', cut_lines(4, what=['module'])) --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -4,6 +4,8 @@ creating and managing test function argu .. currentmodule:: pytest.plugin.python + +.. _`funcargs`: .. _`funcarg mechanism`: Test function arguments and factories @@ -34,18 +36,18 @@ Running the test looks like this:: =========================== test session starts ============================ platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_simplefactory.py - + test_simplefactory.py F - + ================================= FAILURES ================================= ______________________________ test_function _______________________________ - + myfuncarg = 42 - + def test_function(myfuncarg): > assert myfuncarg == 17 E assert 42 == 17 - + test_simplefactory.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= @@ -118,7 +120,8 @@ example: Basic generated test example ---------------------------- -Let's consider this test module:: +Let's consider a test module which uses the ``pytest_generate_tests`` +hook to generate several calls to the same test function:: # content of test_example.py def pytest_generate_tests(metafunc): @@ -135,23 +138,24 @@ Running this:: =========================== test session starts ============================ platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 test path 1: test_example.py - + test_example.py .........F - + ================================= FAILURES ================================= _______________________________ test_func[9] _______________________________ - + numiter = 9 - + def test_func(numiter): > assert numiter < 9 E assert 9 < 9 - + test_example.py:7: AssertionError ==================== 1 failed, 9 passed in 0.03 seconds ==================== Note that the ``pytest_generate_tests(metafunc)`` hook is called during -the test collection phase. You can have a look at it with this:: +the test collection phase which is separate from the actual test running. +Let's just look at what is collected:: $ py.test --collectonly test_example.py @@ -173,7 +177,7 @@ If you want to select only the run with =========================== test session starts ============================ platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 -- /home/hpk/venv/0/bin/python test path 1: test_example.py - + test_example.py:6: test_func[0] PASSED test_example.py:6: test_func[1] PASSED test_example.py:6: test_func[2] PASSED @@ -184,16 +188,16 @@ If you want to select only the run with test_example.py:6: test_func[7] PASSED test_example.py:6: test_func[8] PASSED test_example.py:6: test_func[9] FAILED - + ================================= FAILURES ================================= _______________________________ test_func[9] _______________________________ - + numiter = 9 - + def test_func(numiter): > assert numiter < 9 E assert 9 < 9 - + test_example.py:7: AssertionError ==================== 1 failed, 9 passed in 0.04 seconds ==================== --- a/doc/customize.txt +++ b/doc/customize.txt @@ -35,13 +35,13 @@ If no path was provided at all the curre builtin configuration file options ---------------------------------------------- -.. confval:: minversion = VERSTRING +.. confval:: minversion - specifies the minimal pytest version that is needed for this test suite. + specifies a minimal pytest version needed for running tests. minversion = 2.1 # will fail if we run with pytest-2.0 -.. confval:: addopts = OPTS +.. confval:: addopts add the specified ``OPTS`` to the set of command line arguments as if they had been specified by the user. Example: if you have this ini file content:: @@ -53,5 +53,28 @@ builtin configuration file options py.test --maxfail=2 -rf test_hello.py -.. _`function arguments`: funcargs.html + Default is to add no options. +.. confval:: norecursedirs + + Set the directory basename patterns to avoid when recursing + for test discovery. The individual (fnmatch-style) patterns are + applied to the basename of a directory to decide if to recurse into it. + Pattern matching characters:: + + * matches everything + ? matches any single character + [seq] matches any character in seq + [!seq] matches any char not in seq + + Default patterns are ``.* _* CVS {args}``. Setting a ``norecurse`` + replaces the default. Here is a customizing example for avoiding + a different set of directories:: + + # content of setup.cfg + [pytest] + norecursedirs = .svn _build tmp* + + This would tell py.test to not recurse into typical subversion or + sphinx-build directories or into any ``tmp`` prefixed directory. + --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -1,8 +1,6 @@ Installation and Getting Started =================================== -.. _`easy_install`: - **Compatibility**: Python 2.4-3.2, Jython, PyPy on Unix/Posix and Windows Installation @@ -17,27 +15,26 @@ To check your installation has installed $ py.test --version -If you get an error, checkout :ref:`installation issues`. - +If you get an error checkout :ref:`installation issues`. Our first test run ---------------------------------------------------------- -Let's create a small file with a test function testing a function -computes a certain value:: +Let's create a first test file with a simple test function:: # content of test_sample.py def func(x): return x + 1 + def test_answer(): assert func(3) == 5 -You can execute the test function:: +That's it. You can execute the test function now:: - $ py.test test_sample.py + $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 - test path 1: test_sample.py + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev18 + test path 1: /tmp/doc-exec-211 test_sample.py F @@ -49,19 +46,26 @@ You can execute the test function:: E assert 4 == 5 E + where 4 = func(3) - test_sample.py:4: AssertionError + test_sample.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= -We told py.test to run the ``test_sample.py`` file and it :ref:`discovered` the -``test_answer`` function because of the ``test_`` prefix. We got a -failure because our little ``func(3)`` call did not return ``5``. +py.test found the ``test_answer`` function by following :ref:`standard test discovery rules `, basically detecting the ``test_`` prefixes. We got a failure report because our little ``func(3)`` call did not return ``5``. The report is formatted using the :ref:`standard traceback reporting`. .. note:: - You can simply use the `assert statement`_ for coding expectations because - intermediate values will be presented to you. Or to put it bluntly, - there is no need to learn all `the JUnit legacy methods`_ for expressing - assertions. + You can simply use the ``assert`` statement for coding expectations because + intermediate values will be presented to you. This is much easier than + learning all the `the JUnit legacy methods`_ which are even inconsistent + with Python's own coding guidelines (but consistent with + Java-style naming). + + There is only one seldomly hit caveat to using asserts: if your + assertion expression fails and has side effects then re-evaluating + it for presenting intermediate values can go wrong. It's easy to fix: + compute the value ahead of the assert and then do the + assertion or use the assert "message" syntax:: + + assert expr, "message" # show "message" if expr is not True .. _`the JUnit legacy methods`: http://docs.python.org/library/unittest.html#test-cases @@ -88,7 +92,7 @@ Running it with, this time in "quiet" re . 1 passed in 0.01 seconds -.. todo:: For further ways to assert exceptions see the :pyfunc:`raises` +.. todo:: For further ways to assert exceptions see the `raises` Grouping multiple tests in a class -------------------------------------------------------------- @@ -107,16 +111,16 @@ tests in a class like this:: x = "hello" assert hasattr(x, 'check') -The two tests will be discovered because of the default `automatic test -discovery`_. There is no need to subclass anything. If we now run -the module we'll see one passed and one failed test:: +The two tests are found because of the standard :ref:`test discovery`. +There is no need to subclass anything. We can simply +run the module by passing its filename:: $ py.test -q test_class.py .F ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - self = + self = def test_two(self): x = "hello" @@ -126,27 +130,74 @@ the module we'll see one passed and one test_class.py:8: AssertionError 1 failed, 1 passed in 0.02 seconds -where to go from here +The first test passed, the second failed. Again we can easily see +the intermediate values used in the assertion, helping us to +understand the reason for the failure. + +Going functional: requesting a unique temporary directory +-------------------------------------------------------------- + +For functional tests one often needs to create some files +and pass them to application objects. py.test provides +the versatile :ref:`funcarg mechanism` which allows to request +arbitrary resources, for example a unique temporary directory:: + + # content of test_tmpdir.py + def test_needsfiles(tmpdir): + print tmpdir + assert 0 + +We list the name ``tmpdir`` in the test function signature and +py.test will lookup and call a factory to create the resource +before performing the test function call. Let's just run it:: + + $ py.test -q test_tmpdir.py + F + ================================= FAILURES ================================= + _____________________________ test_needsfiles ______________________________ + + tmpdir = local('/tmp/pytest-1306/test_needsfiles0') + + def test_needsfiles(tmpdir): + print tmpdir + > assert 0 + E assert 0 + + test_tmpdir.py:3: AssertionError + ----------------------------- Captured stdout ------------------------------ + /tmp/pytest-1306/test_needsfiles0 + 1 failed in 0.04 seconds + +Before the test runs, a unique-per-test-invocation temporary directory +was created. More info at :ref:`tmpdir handling`. + +You can find out what kind of builtin :ref:`funcargs` exist by typing:: + + py.test --funcargs # shows builtin and custom function arguments + +where to go next ------------------------------------- Here are a few suggestions where to go next: * :ref:`cmdline` for command line invocation examples * :ref:`good practises` for virtualenv, test layout, genscript support -* :ref:`apiref` for documentation and examples on writing Python tests +* :ref:`apiref` for documentation and examples on using py.test +* :ref:`plugins` managing and writing plugins .. _`installation issues`: -Installation issues +Known Installation issues ------------------------------ easy_install or pip not found? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -Consult distribute_ to install the ``easy_install`` tool on your machine. -You may also use the original but somewhat older `setuptools`_ project -although we generally recommend to use ``distribute`` because it contains -more bug fixes and also works for Python3. +Consult `distribute docs `_ to install the ``easy_install`` +tool on your machine. You may also use the original but somewhat older +`setuptools`_ project although we generally recommend to use +``distribute`` because it contains more bug fixes and also works for +Python3. For Python2 you can also consult pip_ for the popular ``pip`` tool. --- a/doc/tmpdir.txt +++ b/doc/tmpdir.txt @@ -1,3 +1,5 @@ + +.. _`tmpdir handling`: temporary directories and files ================================================ @@ -18,7 +20,7 @@ and more. Here is an example test usage p = tmpdir.mkdir("sub").join("hello.txt") p.write("content") assert p.read() == "content" - assert len(os.listdir(str(tmpdir))) == 1 + assert tmpdir.listdir() == 1 assert 0 Running this would result in a passed test except for the last @@ -41,8 +43,6 @@ Running this would result in a passed te p.write("content") assert p.read() == "content" assert len(os.listdir(str(tmpdir))) == 1 - > assert 0 - E assert 0 test_tmpdir.py:7: AssertionError ========================= 1 failed in 0.04 seconds ========================= --- a/doc/unittest.txt +++ b/doc/unittest.txt @@ -1,3 +1,6 @@ + +.. _`unittest.TestCase`: + unittest.TestCase support ===================================================================== --- /dev/null +++ b/doc/extracol @@ -0,0 +1,32 @@ +changing Python test discovery patterns +-------------------------------------------------- + +You can influence python test file, function and class prefixes through +the :confval:`python_patterns` configuration valueto determine which +files are checked and which test functions are found. Example for using +a scheme that builds on ``check`` rather than on ``test`` prefixes:: + + + # content of setup.cfg + [pytest] + python_patterns = + files: check_*.py + functions: check_ + classes: Check + +See + :confval:`python_funcprefixes` and :confval:`python_classprefixes` + + + changing test file discovery + ----------------------------------------------------- + + You can specify patterns where python tests are found:: + + python_testfilepatterns = + testing/**/{purebasename}.py + testing/*.py + + .. note:: + + conftest.py files are never considered for test discovery --- a/doc/example/nonpython/conftest.py +++ b/doc/example/nonpython/conftest.py @@ -11,22 +11,22 @@ class YamlFile(py.test.collect.File): import yaml # we need a yaml parser, e.g. PyYAML raw = yaml.load(self.fspath.open()) for name, spec in raw.items(): - yield UsecaseItem(name, self, spec) + yield YamlItem(name, self, spec) -class UsecaseItem(py.test.collect.Item): +class YamlItem(py.test.collect.Item): def __init__(self, name, parent, spec): - super(UsecaseItem, self).__init__(name, parent) + super(YamlItem, self).__init__(name, parent) self.spec = spec def runtest(self): for name, value in self.spec.items(): # some custom test execution (dumb example follows) if name != value: - raise UsecaseException(self, name, value) + raise YamlException(self, name, value) def repr_failure(self, excinfo): """ called when self.runtest() raises an exception. """ - if excinfo.errisinstance(UsecaseException): + if isinstance(excinfo.value, YamlException): return "\n".join([ "usecase execution failed", " spec failed: %r: %r" % excinfo.value.args[1:3], @@ -36,5 +36,5 @@ class UsecaseItem(py.test.collect.Item): def reportinfo(self): return self.fspath, 0, "usecase: %s" % self.name -class UsecaseException(Exception): +class YamlException(Exception): """ custom exception for error reporting. """ --- a/doc/goodpractises.txt +++ b/doc/goodpractises.txt @@ -5,7 +5,7 @@ Good Integration Practises ================================================= -work with virtual environments +Work with virtual environments ----------------------------------------------------------- We recommend to work with virtualenv_ environments and use easy_install_ @@ -21,6 +21,24 @@ server Hudson_. .. _`buildout`: http://www.buildout.org/ .. _pip: http://pypi.python.org/pypi/pip +.. _`test discovery`: + +Conventions for Python test discovery +------------------------------------------------- + +``py.test`` implements the following standard test discovery: + +* collection starts from initial command line arguments + which may be directories, filenames or test ids. +* recurse into directories, unless they match :confval:`norecursedirs` +* ``test_*.py`` or ``*_test.py`` files, imported by their `package name`_. +* ``Test`` prefixed test classes (without an ``__init__`` method) +* ``test_`` prefixed test functions or methods are test items + +For changing and customization example, see :doc:`example/pythoncollection`. + +py.test additionally discovers tests using the standard +:ref:`unittest.TestCase ` subclassing technique. Choosing a test layout / import rules ------------------------------------------ @@ -57,6 +75,8 @@ You can always run your tests by pointin py.test # run all tests below current dir ... +.. _`package name`: + .. note:: Test modules are imported under their fully qualified name as follows: --- a/doc/example/nonpython.txt +++ b/doc/example/nonpython.txt @@ -4,6 +4,8 @@ Working with non-python tests ==================================================== +.. _`yaml plugin`: + a basic example for specifying tests in Yaml files -------------------------------------------------------------- @@ -39,7 +41,17 @@ now execute the test specification:: 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 -interesting interpretation of the yaml-values. Note that ``reportinfo()`` +interesting interpretation of the yaml-values. You can easily write +your own domain specific testing language this way. + +.. note:: + + ``repr_failure(excinfo)`` is called for representing test failures. + If you create custom collection nodes you can return an error + representation string of your choice. It + will be reported as a (red) string. + + ``reportinfo()`` is used for representing the test location and is also consulted for reporting in ``verbose`` mode:: --- a/doc/index.txt +++ b/doc/index.txt @@ -12,7 +12,7 @@ Welcome to ``py.test`` documentation: overview apiref plugins - examples + example/index talks develop --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -3,7 +3,7 @@ Writing, managing and understanding plug .. _`local plugin`: -py.test implements all aspects of configuration, collection, running and reporting by calling `well specified hooks`_. Virtually any Python module can be registered as a plugin. It can implement any number of hook functions (usually two or three) which all have a ``pytest_`` prefix, making hook functions easy to distinguish and find. There are three basic locations types:: +py.test implements all aspects of configuration, collection, running and reporting by calling `well specified hooks`_. Virtually any Python module can be registered as a plugin. It can implement any number of hook functions (usually two or three) which all have a ``pytest_`` prefix, making hook functions easy to distinguish and find. There are three basic locations types: * builtin plugins: loaded from py.test's own `pytest/plugin`_ directory. * `external plugins`_: modules discovered through `setuptools entry points`_ @@ -55,7 +55,7 @@ earlier than further away ones. .. _`installing plugins`: .. _`external plugins`: -Installing External Plugins +Installing External Plugins / Searching ------------------------------------------------------ Installing a plugin happens through any usual Python installation @@ -72,9 +72,7 @@ de-install it. You can find a list of v .. _`available installable plugins`: .. _`pytest- pypi.python.org search`: http://pypi.python.org/pypi?%3Aaction=search&term=pytest-&submit=search -.. _`setuptools entry points`: - -Writing an installable plugin +Writing a plugin by looking at examples ------------------------------------------------------ .. _`Distribute`: http://pypi.python.org/pypi/distribute @@ -83,9 +81,18 @@ Writing an installable plugin If you want to write a plugin, there are many real-life examples you can copy from: +* a custom collection example plugin: :ref:`yaml plugin` * around 20 `builtin plugins`_ which comprise py.test's own functionality * around 10 `external plugins`_ providing additional features +All of these plugins are using the documented `well specified hooks`_ +to implement their wide-ranging functionality. + +.. _`setuptools entry points`: + +Making your plugin installable by others +----------------------------------------------- + If you want to make your plugin externally available, you may define a so called entry point for your distribution so that ``py.test`` finds your plugin module. Entry points are @@ -149,9 +156,6 @@ will be loaded as well. You can also us which will import the specified module as a py.test plugin. -.. _`setuptools entry points`: -.. _registered: - Accessing another plugin by name -------------------------------------------- --- /dev/null +++ b/doc/example/collectonly.py @@ -0,0 +1,11 @@ + +# run this with $ py.test --collectonly test_collectonly.py +# +def test_function(): + pass + +class TestClass: + def test_method(self): + pass + def test_anothermethod(self): + pass --- a/doc/discovery.txt +++ /dev/null @@ -1,45 +0,0 @@ - -Test collection and discovery -====================================================== - -.. _`discovered`: - -Default filesystem test discovery ------------------------------------------------ - -Test collection starts from paths specified at the command line or from -the current directory. Tests are collected ahead of running the first test. -(This used to be different in earlier versions of ``py.test`` where -collection and running was interweaved which made test randomization -and distributed testing harder). - -Collection nodes which have children are called "Collectors" and otherwise -they are called "Items" or "test items". Here is an example of such a -tree:: - - example $ py.test --collectonly test_collectonly.py - - - - - - - - -By default all directories not starting with a dot are traversed, -looking for ``test_*.py`` and ``*_test.py`` files. Those Python -files are imported under their `package name`_. - -The Module collector looks for test functions -and test classes and methods. Test functions and methods -are prefixed ``test`` by default. Test classes must -start with a capitalized ``Test`` prefix. - -Customizing error messages -------------------------------------------------- - -On test and collection nodes ``py.test`` will invoke -the ``node.repr_failure(excinfo)`` function which -you may override and make it return an error -representation string of your choice. It -will be reported as a (red) string. From commits-noreply at bitbucket.org Sat Nov 6 11:37:26 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 05:37:26 -0500 (CDT) Subject: [py-svn] pytest commit 5f89191fcd0a: some more improvements and updates to docs, add release announcements Message-ID: <20101106103726.6B9261E135A@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289039933 -3600 # Node ID 5f89191fcd0a77727f9c8e5a3a73f8cc2e964e31 # Parent 3da7c9990c7de697f8a395956d68a277fff5c2fb some more improvements and updates to docs, add release announcements --- a/pytest/plugin/monkeypatch.py +++ b/pytest/plugin/monkeypatch.py @@ -66,7 +66,7 @@ class monkeypatch: def setenv(self, name, value, prepend=None): """ set environment variable ``name`` to ``value``. if ``prepend`` - is a character, read the current environment variable value + is a character, read the current environment variable value and prepend the ``value`` adjoined with the ``prepend`` character.""" value = str(value) if prepend and name in os.environ: --- a/doc/builtin.txt +++ b/doc/builtin.txt @@ -28,25 +28,25 @@ You can ask for available builtin or pro captures writes to sys.stdout/sys.stderr and makes them available successively via a ``capsys.readouterr()`` method which returns a ``(out, err)`` tuple of captured snapshot strings. - + capfd captures writes to file descriptors 1 and 2 and makes snapshotted ``(out, err)`` string tuples available via the ``capsys.readouterr()`` method. If the underlying platform does not have ``os.dup`` (e.g. Jython) tests using this funcarg will automatically skip. - + tmpdir return a temporary directory path object unique to each test function invocation, created as a sub directory of the base temporary directory. The returned object is a `py.path.local`_ path object. - + monkeypatch The returned ``monkeypatch`` funcarg provides these helper methods to modify objects, dictionaries or os.environ:: - + monkeypatch.setattr(obj, name, value, raising=True) monkeypatch.delattr(obj, name, raising=True) monkeypatch.setitem(mapping, name, value) @@ -54,15 +54,15 @@ You can ask for available builtin or pro monkeypatch.setenv(name, value, prepend=False) monkeypatch.delenv(name, value, raising=True) monkeypatch.syspath_prepend(path) - + All modifications will be undone when the requesting test function finished its execution. The ``raising`` parameter determines if a KeyError or AttributeError will be raised if the set/deletion operation has no target. - + recwarn Return a WarningsRecorder instance that provides these methods: - + * ``pop(category=None)``: return last warning matching the category. * ``clear()``: clear list of warnings - + --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -1,6 +1,4 @@ -""" -funcargs and support code for testing py.test's own functionality. -""" +""" (disabled by default) support for testing py.test and py.test plugins. """ import py, pytest import sys, os --- a/doc/example/builtin.txt +++ b/doc/example/builtin.txt @@ -3,8 +3,9 @@ writing well integrated assertion helper ======================================================== If you have a test helper function called from a test you can -use the ``pytest.fail``_ builtin to cleanly fail a test with a message. -The test support function will never itself show up in the traceback. +use the ``pytest.fail`` marker to fail a test with a certain message. +The test support function will not show up in the traceback if you +set the ``__tracebackhide__`` option somewhere in the helper function. Example:: # content of test_checkconfig.py @@ -33,4 +34,3 @@ Let's run our little function:: test_checkconfig.py:8: Failed 1 failed in 0.02 seconds - --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -34,20 +34,20 @@ Running the test looks like this:: $ py.test test_simplefactory.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_simplefactory.py - + test_simplefactory.py F - + ================================= FAILURES ================================= ______________________________ test_function _______________________________ - + myfuncarg = 42 - + def test_function(myfuncarg): > assert myfuncarg == 17 E assert 42 == 17 - + test_simplefactory.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= @@ -136,70 +136,52 @@ Running this:: $ py.test test_example.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_example.py - + test_example.py .........F - + ================================= FAILURES ================================= _______________________________ test_func[9] _______________________________ - + numiter = 9 - + def test_func(numiter): > assert numiter < 9 E assert 9 < 9 - + test_example.py:7: AssertionError - ==================== 1 failed, 9 passed in 0.03 seconds ==================== + ==================== 1 failed, 9 passed in 0.04 seconds ==================== Note that the ``pytest_generate_tests(metafunc)`` hook is called during the test collection phase which is separate from the actual test running. Let's just look at what is collected:: $ py.test --collectonly test_example.py - - - - - - - - - - - - + + + + + + + + + + + + If you want to select only the run with the value ``7`` you could do:: $ py.test -v -k 7 test_example.py # or -k test_func[7] =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 -- /home/hpk/venv/0/bin/python test path 1: test_example.py - - test_example.py:6: test_func[0] PASSED - test_example.py:6: test_func[1] PASSED - test_example.py:6: test_func[2] PASSED - test_example.py:6: test_func[3] PASSED - test_example.py:6: test_func[4] PASSED - test_example.py:6: test_func[5] PASSED - test_example.py:6: test_func[6] PASSED - test_example.py:6: test_func[7] PASSED - test_example.py:6: test_func[8] PASSED - test_example.py:6: test_func[9] FAILED - - ================================= FAILURES ================================= - _______________________________ test_func[9] _______________________________ - - numiter = 9 - - def test_func(numiter): - > assert numiter < 9 - E assert 9 < 9 - - test_example.py:7: AssertionError - ==================== 1 failed, 9 passed in 0.04 seconds ==================== + + test_example.py <- test_example.py:6: test_func[7] PASSED + + ======================== 9 tests deselected by '7' ========================= + ================== 1 passed, 9 deselected in 0.01 seconds ================== .. _`metafunc object`: --- a/pytest/plugin/junitxml.py +++ b/pytest/plugin/junitxml.py @@ -1,5 +1,6 @@ -""" logging of test results in JUnit-XML format, for use with Hudson - and build integration servers. Based on initial code from Ross Lawley. +""" report test results in JUnit-XML format, for use with Hudson and build integration servers. + +Based on initial code from Ross Lawley. """ import py --- a/doc/mark.txt +++ b/doc/mark.txt @@ -88,8 +88,8 @@ You can use the ``-k`` command line opti $ py.test -k webtest # running with the above defined examples yields =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 - test path 1: /tmp/doc-exec-171 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + test path 1: /tmp/doc-exec-407 test_mark.py .. test_mark_classlevel.py .. @@ -100,8 +100,8 @@ And you can also run all tests except th $ py.test -k-webtest =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 - test path 1: /tmp/doc-exec-171 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + test path 1: /tmp/doc-exec-407 ===================== 4 tests deselected by '-webtest' ===================== ======================= 4 deselected in 0.01 seconds ======================= @@ -110,8 +110,8 @@ Or to only select the class:: $ py.test -kTestClass =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 - test path 1: /tmp/doc-exec-171 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + test path 1: /tmp/doc-exec-407 test_mark_classlevel.py .. --- a/doc/unittest.txt +++ b/doc/unittest.txt @@ -24,7 +24,7 @@ Running it yields:: $ py.test test_unittest.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_unittest.py test_unittest.py F @@ -32,7 +32,7 @@ Running it yields:: ================================= FAILURES ================================= ____________________________ MyTest.test_method ____________________________ - self = + self = def test_method(self): x = 1 @@ -41,7 +41,7 @@ Running it yields:: test_unittest.py:8: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - self = , first = 1, second = 3 + self = , first = 1, second = 3 msg = None def failUnlessEqual(self, first, second, msg=None): --- a/pytest/plugin/doctest.py +++ b/pytest/plugin/doctest.py @@ -1,4 +1,4 @@ -""" collect and execute doctests from modules and test files.""" +""" discover and run doctests in modules and test files.""" import py from py._code.code import TerminalRepr, ReprFileLocation --- a/pytest/plugin/capture.py +++ b/pytest/plugin/capture.py @@ -1,6 +1,4 @@ -""" plugin for configurable per-test stdout/stderr capturing mechanisms and -``capsys`` and ``capfd`` function arguments. -""" +""" per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """ import py import os --- a/doc/tmpdir.txt +++ b/doc/tmpdir.txt @@ -20,15 +20,15 @@ and more. Here is an example test usage p = tmpdir.mkdir("sub").join("hello.txt") p.write("content") assert p.read() == "content" - assert tmpdir.listdir() == 1 + assert len(tmpdir.listdir()) == 1 assert 0 -Running this would result in a passed test except for the last +Running this would result in a passed test except for the last ``assert 0`` line which we use to look at values:: - + $ py.test test_tmpdir.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_tmpdir.py test_tmpdir.py F @@ -36,13 +36,15 @@ Running this would result in a passed te ================================= FAILURES ================================= _____________________________ test_create_file _____________________________ - tmpdir = local('/tmp/pytest-1248/test_create_file0') + tmpdir = local('/tmp/pytest-243/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") p.write("content") assert p.read() == "content" - assert len(os.listdir(str(tmpdir))) == 1 + assert len(tmpdir.listdir()) == 1 + > assert 0 + E assert 0 test_tmpdir.py:7: AssertionError ========================= 1 failed in 0.04 seconds ========================= @@ -52,7 +54,7 @@ Running this would result in a passed te the default base temporary directory ----------------------------------------------- -.. +.. You can create directories by calling one of two methods on the config object: - ``config.mktemp(basename)``: create and return a new tempdir @@ -61,14 +63,14 @@ the default base temporary directory Temporary directories are by default created as sub directories of the system temporary directory. The name will be ``pytest-NUM`` where ``NUM`` will be incremenated with each test run. Moreover, entries older -than 3 temporary directories will be removed. +than 3 temporary directories will be removed. You can override the default temporary directory logic and set it like this:: py.test --basetemp=mydir -When distributing tests on the local machine, ``py.test`` takes care to -configure a basetemp directory for the sub processes such that all +When distributing tests on the local machine, ``py.test`` takes care to +configure a basetemp directory for the sub processes such that all temporary data lands below below a single per-test run basetemp directory. .. _`py.path.local`: http://pylib.org/path.html --- /dev/null +++ b/doc/announce/release-2.0.0.txt @@ -0,0 +1,50 @@ +py.test 2.0.0: standalone, features++, implementation++, docs++ +=========================================================================== + +XXX PENDING + +Welcome to pytest-2.0.0! With this release py.test becomes its own standalone +PyPI distribution, named ``pytest``, installing the ``py.test`` command line +tool. Apart from a great internal cleanup this release comes with tons +of improvements and new features and a completely revamped extensive +documentation, including many continously tested examples. See + + http://pytest.org + +New Features +----------------------- + +- new invocations through Python interpreter and from Python:: + + python -m pytest # on all pythons >= 2.7 + python -m pytest.main # on all pythons >= 2.5 + import pytest ; pytest.main(args, plugins) + + see http://pytest.org/2.0.0/invoke.html for details. + +- new configuration through ini-files (setup.cfg or tox.ini recognized), + for example:: + + [pytest] + norecursedirs = .hg _build + python_collect_funcprefix = test_ + python_collect_classprefix = Test + + see http://pytest.org/2.0.0/customize.html + +- + +Thanks to issue reporters, people asking questions, complaining and +generally to Ronny Pfannschmidt for his awesome help on many issues. + +cheers, +holger krekel + +Changes between 1.3.3 and 1.3.4 +================================================== + +- fix issue111: improve install documentation for windows +- fix issue119: fix custom collectability of __init__.py as a module +- fix issue116: --doctestmodules work with __init__.py files as well +- fix issue115: unify internal exception passthrough/catching/GeneratorExit +- fix issue118: new --tb=native for presenting cpython-standard exceptions --- /dev/null +++ b/doc/nose.txt @@ -0,0 +1,42 @@ +Running test written for nose +======================================= + +.. include:: links.inc + +py.test has basic support for running tests written for nose_. +This is implemented in :pymod:`pytest.plugin.nose`. + +Usage +------------- + +type:: + + py.test # instead of 'nosetests' + +and you should be able to run your nose style tests and at the same +make full use of py.test's capabilities. + +Supported nose Idioms +---------------------- + +* setup and teardown at module/class/method level +* SkipTest exceptions and markers +* setup/teardown decorators +* yield-based tests and their setup +* general usage of nose utilities + +Unsupported idioms / issues +---------------------------------- + +- nose-style doctests are not collected and executed correctly, + also fixtures don't work. + +- no nose-configuration is recognized + +If you find other issues or have suggestions please run:: + + py.test --pastebin=all + +and send the resulting URL to a py.test contact channel, +at best to the mailing list. +""" --- a/pytest/plugin/mark.py +++ b/pytest/plugin/mark.py @@ -70,7 +70,7 @@ def matchonekeyword(key, itemkeywords): return True class MarkGenerator: - """ Factory for :class:`MarkDecorator` objects - exposed as + """ Factory for :class:`MarkDecorator` objects - exposed as a ``py.test.mark`` singleton instance. Example:: import py @@ -88,8 +88,8 @@ class MarkGenerator: class MarkDecorator: """ A decorator for test functions and test classes. When applied - it will create :class:`MarkInfo` objects which may be - :ref:`retrieved by hooks as item keywords` MarkDecorator instances + it will create :class:`MarkInfo` objects which may be + :ref:`retrieved by hooks as item keywords` MarkDecorator instances are usually created by writing:: mark1 = py.test.mark.NAME # simple MarkDecorator --- a/doc/doctest.txt +++ b/doc/doctest.txt @@ -44,7 +44,7 @@ then you can just invoke ``py.test`` wit $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 - test path 1: /tmp/doc-exec-197 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + test path 1: /tmp/doc-exec-400 ============================= in 0.00 seconds ============================= --- a/doc/example/xunit_setup.txt +++ b/doc/example/xunit_setup.txt @@ -19,8 +19,8 @@ custom options:: .. _optparse: http://docs.python.org/library/optparse.html -Working Examples -================ +order of setup/teardown module/class/item methods +==================================================== managing state at module, class and method level ------------------------------------------------------------ --- a/pytest/plugin/recwarn.py +++ b/pytest/plugin/recwarn.py @@ -1,4 +1,4 @@ -""" record warnings to allow assertions about them. """ +""" recording warnings during test function execution. """ import py import sys, os --- a/doc/example/mysetup.txt +++ b/doc/example/mysetup.txt @@ -49,7 +49,7 @@ You can now run the test:: $ py.test test_sample.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_sample.py test_sample.py F @@ -57,7 +57,7 @@ You can now run the test:: ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - mysetup = + mysetup = def test_answer(mysetup): app = mysetup.myapp() @@ -122,12 +122,12 @@ Running it yields:: $ py.test test_ssh.py -rs =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_ssh.py test_ssh.py s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-198/conftest.py:22: 'specify ssh host with --ssh' + SKIP [1] /tmp/doc-exec-438/conftest.py:22: specify ssh host with --ssh ======================== 1 skipped in 0.02 seconds ========================= --- a/doc/index.txt +++ b/doc/index.txt @@ -19,6 +19,7 @@ Welcome to ``py.test`` documentation: example/index talks develop + announce/index .. toctree:: :hidden: --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -5,7 +5,7 @@ Writing, managing and understanding plug py.test implements all aspects of configuration, collection, running and reporting by calling `well specified hooks`_. Virtually any Python module can be registered as a plugin. It can implement any number of hook functions (usually two or three) which all have a ``pytest_`` prefix, making hook functions easy to distinguish and find. There are three basic locations types: -* builtin plugins: loaded from py.test's own `pytest/plugin`_ directory. +* `builtin plugins`_: loaded from py.test's own ``pytest/plugin`` directory. * `external plugins`_: modules discovered through `setuptools entry points`_ * `conftest.py plugins`_: modules auto-discovered in test directories @@ -48,8 +48,8 @@ earlier than further away ones. python package directory (i.e. one containing an ``__init__.py``) then "import conftest" can be ambigous because there might be other ``conftest.py`` files as well on your PYTHONPATH or ``sys.path``. - It is thus good practise for projects to either put ``conftest.py`` - under a package scope or to never import anything from a + It is thus good practise for projects to either put ``conftest.py`` + under a package scope or to never import anything from a conftest.py file. .. _`installing plugins`: @@ -90,6 +90,7 @@ to implement their wide-ranging function .. _`setuptools entry points`: + Making your plugin installable by others ----------------------------------------------- @@ -97,8 +98,8 @@ If you want to make your plugin external may define a so called entry point for your distribution so that ``py.test`` finds your plugin module. Entry points are a feature that is provided by `setuptools`_ or `Distribute`_. -The concrete entry point is ``pytest11``. To make your plugin -available you can insert the following lines in your +The concrete entry point is ``pytest11``. To make your plugin +available you can insert the following lines in your setuptools/distribute-based setup-invocation: .. sourcecode:: python @@ -171,6 +172,37 @@ the plugin manager like this: If you want to look at the names of existing plugins, use the ``--traceconfig`` option. + +.. _`builtin plugins`: + +py.test default plugin reference +==================================== + +.. autosummary:: + + pytest.plugin.assertion + pytest.plugin.capture + pytest.plugin.config + pytest.plugin.doctest + pytest.plugin.genscript + pytest.plugin.helpconfig + pytest.plugin.junitxml + pytest.plugin.mark + pytest.plugin.monkeypatch + pytest.plugin.nose + pytest.plugin.pastebin + pytest.plugin.pdb + pytest.plugin.pytester + pytest.plugin.python + pytest.plugin.recwarn + pytest.plugin.resultlog + pytest.plugin.runner + pytest.plugin.session + pytest.plugin.skipping + pytest.plugin.terminal + pytest.plugin.tmpdir + pytest.plugin.unittest + .. _`well specified hooks`: py.test hook reference @@ -183,7 +215,7 @@ py.test calls hook functions to implemen test execution and reporting. When py.test loads a plugin it validates that all hook functions conform to their respective hook specification. Each hook function name and its argument names need to match a hook -specification exactly but it is allowed for a hook function to accept +specification exactly but it is allowed for a hook function to accept *less* parameters than specified. If you mistype argument names or the hook name itself you get useful errors. @@ -261,7 +293,7 @@ Reference of important objects involved .. autoclass:: pytest.plugin.session.Node(name, parent) :members: -.. +.. .. autoclass:: pytest.plugin.session.File(fspath, parent) :members: --- a/pytest/plugin/runner.py +++ b/pytest/plugin/runner.py @@ -1,6 +1,4 @@ -""" -collect and run test items and create reports. -""" +""" basic collect and runtest protocol implementations """ import py, sys from py._code.code import TerminalRepr @@ -92,11 +90,12 @@ def call_runtest_hook(item, when): return CallInfo(lambda: ihook(item=item), when=when) class CallInfo: - """ Call Information about a hook call. """ + """ Result/Exception info a function invocation. """ #: None or ExceptionInfo object. excinfo = None def __init__(self, func, when): - #: one of "setup", "call", "teardown" specifying the runtest phase. + #: context of invocation: one of "setup", "call", + #: "teardown", "memocollect" self.when = when try: self.result = func() --- a/pytest/plugin/pdb.py +++ b/pytest/plugin/pdb.py @@ -1,6 +1,5 @@ -""" -interactive debugging with the Python Debugger. -""" +""" interactive debugging with PDB, the Python Debugger. """ + import py import sys --- a/pytest/plugin/assertion.py +++ b/pytest/plugin/assertion.py @@ -1,3 +1,6 @@ +""" +support for presented detailed information in failing assertions. +""" import py import sys --- a/doc/monkeypatch.txt +++ b/doc/monkeypatch.txt @@ -39,8 +39,8 @@ will be undone. .. background check: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 - test path 1: /tmp/doc-exec-172 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + test path 1: /tmp/doc-exec-408 ============================= in 0.00 seconds ============================= --- a/pytest/plugin/tmpdir.py +++ b/pytest/plugin/tmpdir.py @@ -1,13 +1,4 @@ -"""provide temporary directories to test functions. - -usage example:: - - def test_plugin(tmpdir): - tmpdir.join("hello").write("hello") - -.. _`py.path.local`: ../../path.html - -""" +""" support for providing temporary directories to test functions. """ import pytest, py def pytest_configure(config): --- a/pytest/plugin/nose.py +++ b/pytest/plugin/nose.py @@ -1,42 +1,5 @@ -"""nose-compatibility plugin: allow to run nose test suites natively. +"""run test suites written for nose. """ -This is an experimental plugin for allowing to run tests written -in 'nosetests style with py.test. - -Usage -------------- - -type:: - - py.test # instead of 'nosetests' - -and you should be able to run nose style tests and at the same -time can make full use of py.test's capabilities. - -Supported nose Idioms ----------------------- - -* setup and teardown at module/class/method level -* SkipTest exceptions and markers -* setup/teardown decorators -* yield-based tests and their setup -* general usage of nose utilities - -Unsupported idioms / issues ----------------------------------- - -- nose-style doctests are not collected and executed correctly, - also fixtures don't work. - -- no nose-configuration is recognized - -If you find other issues or have suggestions please run:: - - py.test --pastebin=all - -and send the resulting URL to a py.test contact channel, -at best to the mailing list. -""" import py import inspect import sys --- a/doc/example/simple.txt +++ b/doc/example/simple.txt @@ -36,9 +36,9 @@ Let's run this without supplying our new F ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - + cmdopt = 'type1' - + def test_answer(cmdopt): if cmdopt == "type1": print ("first") @@ -46,7 +46,7 @@ Let's run this without supplying our new print ("second") > assert 0 # to see what was printed E assert 0 - + test_sample.py:6: AssertionError ----------------------------- Captured stdout ------------------------------ first @@ -58,9 +58,9 @@ And now with supplying a command line op F ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - + cmdopt = 'type2' - + def test_answer(cmdopt): if cmdopt == "type1": print ("first") @@ -68,7 +68,7 @@ And now with supplying a command line op print ("second") > assert 0 # to see what was printed E assert 0 - + test_sample.py:6: AssertionError ----------------------------- Captured stdout ------------------------------ second @@ -122,16 +122,15 @@ let's run the full monty:: ....F ================================= FAILURES ================================= _____________________________ test_compute[4] ______________________________ - + param1 = 4 - + def test_compute(param1): > assert param1 < 4 E assert 4 < 4 - + test_compute.py:3: AssertionError - 1 failed, 4 passed in 0.03 seconds - + 1 failed, 4 passed in 0.02 seconds As expected when running the full range of ``param1`` values we'll get an error on the last one. --- a/pytest/plugin/resultlog.py +++ b/pytest/plugin/resultlog.py @@ -1,4 +1,4 @@ -""" create machine readable plain text file with results. """ +""" (disabled by default) create result information in a plain text file. """ import py from py.builtin import print_ --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -1,10 +1,10 @@ +""" command line configuration, ini-file and conftest.py processing. """ import py import sys, os from pytest.main import PluginManager import pytest - def pytest_cmdline_parse(pluginmanager, args): config = Config(pluginmanager) config.parse(args) --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -14,6 +14,7 @@ Installation options:: To check your installation has installed the correct version:: $ py.test --version + This is py.test version 2.0.0.dev19, imported from /home/hpk/p/pytest/pytest If you get an error checkout :ref:`installation issues`. @@ -33,8 +34,8 @@ That's it. You can execute the test func $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev18 - test path 1: /tmp/doc-exec-211 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + test path 1: /tmp/doc-exec-404 test_sample.py F @@ -120,7 +121,7 @@ run the module by passing its filename:: ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - self = + self = def test_two(self): x = "hello" @@ -156,7 +157,7 @@ before performing the test function call ================================= FAILURES ================================= _____________________________ test_needsfiles ______________________________ - tmpdir = local('/tmp/pytest-1306/test_needsfiles0') + tmpdir = local('/tmp/pytest-240/test_needsfiles0') def test_needsfiles(tmpdir): print tmpdir @@ -165,7 +166,7 @@ before performing the test function call test_tmpdir.py:3: AssertionError ----------------------------- Captured stdout ------------------------------ - /tmp/pytest-1306/test_needsfiles0 + /tmp/pytest-240/test_needsfiles0 1 failed in 0.04 seconds Before the test runs, a unique-per-test-invocation temporary directory --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -1,8 +1,4 @@ -""" basic test session implementation. - -* drives collection of tests -* triggers executions of tests -""" +""" core implementation of testing process: init, collection, runtest loop. """ import py import pytest @@ -174,10 +170,10 @@ class Node(object): #: the test config object self.config = config or parent.config - #: the collection this node is part of. + #: the collection this node is part of self.collection = collection or parent.collection - #: the file where this item is contained/collected from. + #: filesystem path where this node was collected from self.fspath = getattr(parent, 'fspath', None) self.ihook = self.collection.gethookproxy(self.fspath) self.keywords = {self.name: True} --- a/pytest/plugin/unittest.py +++ b/pytest/plugin/unittest.py @@ -1,6 +1,4 @@ -""" -automatically discover and run traditional "unittest.py" style tests. -""" +""" support discovery and running of traditional "unittest.py" style tests. """ import py import sys --- a/doc/apiref.txt +++ b/doc/apiref.txt @@ -20,5 +20,6 @@ py.test reference documentation mark.txt recwarn.txt unittest.txt + nose.txt doctest.txt --- a/doc/announce/releases.txt +++ /dev/null @@ -1,16 +0,0 @@ -============= -Release notes -============= - -Contents: - -.. toctree:: - :maxdepth: 2 - -.. include: release-1.1.0 -.. include: release-1.0.2 - - release-1.0.1 - release-1.0.0 - release-0.9.2 - release-0.9.0 --- a/pytest/plugin/genscript.py +++ b/pytest/plugin/genscript.py @@ -1,3 +1,4 @@ +""" generate a single-file self-contained version of py.test """ import py import pickle import zlib --- a/pytest/plugin/skipping.py +++ b/pytest/plugin/skipping.py @@ -1,6 +1,4 @@ -""" -plugin providing skip and xfail functionality. -""" +""" support for skip/xfail functions and markers. """ import py, pytest --- a/pytest/plugin/terminal.py +++ b/pytest/plugin/terminal.py @@ -1,5 +1,4 @@ -""" -Implements terminal reporting of the full testing process. +""" terminal reporting of the full testing process. This is a good source for looking at the various reporting hooks. """ --- a/doc/contact.txt +++ b/doc/contact.txt @@ -1,11 +1,13 @@ .. _`contact channels`: +.. _`contact`: Contact channels =================================== -- `new issue tracker`_ to report bugs or suggest features. - See also the `old issue tracker`_ but don't submit bugs there. +- `new issue tracker`_ to report bugs or suggest features (for version + 2.0 and above). You may also peek at the `old issue tracker`_ but please + don't submit bugs there anymore. - `Testing In Python`_: a mailing list for Python testing tools and discussion. --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -1,6 +1,4 @@ -""" -Python related collection nodes. -""" +""" Python test discovery, setup and run of test functions. """ import py import inspect import sys --- a/doc/example/index.txt +++ b/doc/example/index.txt @@ -4,6 +4,9 @@ Usages and Examples =========================================== +This is a (growing) list of examples. :ref:`Contact ` us if you +need more examples or have questions. + .. toctree:: :maxdepth: 2 @@ -14,3 +17,4 @@ Usages and Examples detectpytest.txt nonpython.txt simple.txt + xunit_setup.txt --- a/doc/example/pythoncollection.txt +++ b/doc/example/pythoncollection.txt @@ -18,12 +18,11 @@ finding out what is collected You can always peek at the collection tree without running tests like this:: - $ py.test --collectonly collectonly.py - - - - - - - - + . $ py.test --collectonly collectonly.py + + + + + + + --- a/doc/conf.py +++ b/doc/conf.py @@ -25,7 +25,7 @@ import sys, os # Add any Sphinx extension module names here, as strings. They can be extensions # coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.todo', 'sphinx.ext.autosummary', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] # Add any paths that contain templates here, relative to this directory. @@ -65,7 +65,7 @@ release = '2.0.0dev0' # List of patterns, relative to source directory, that match files and # directories to ignore when looking for source files. -exclude_patterns = ['links.inc', '_build', 'test', 'announce'] # XXX +exclude_patterns = ['links.inc', '_build', 'test', ] # XXX # The reST default role (used for this markup: `text`) to use for all documents. #default_role = None --- a/doc/example/controlskip.txt +++ b/doc/example/controlskip.txt @@ -36,12 +36,12 @@ and when running it will see a skipped " $ py.test test_module.py -rs # "-rs" means report on the little 's' =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_module.py test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-195/conftest.py:9: 'need --runslow option to run' + SKIP [1] /tmp/doc-exec-435/conftest.py:9: need --runslow option to run =================== 1 passed, 1 skipped in 0.02 seconds ==================== @@ -49,7 +49,7 @@ Or run it including the ``slow`` marked $ py.test test_module.py --runslow =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_module.py test_module.py .. --- a/doc/assert.txt +++ b/doc/assert.txt @@ -21,7 +21,7 @@ assertion fails you will see the value o $ py.test test_assert1.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_assert1.py test_assert1.py F @@ -101,7 +101,7 @@ if you run this module:: $ py.test test_assert2.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_assert2.py test_assert2.py F --- a/pytest/plugin/helpconfig.py +++ b/pytest/plugin/helpconfig.py @@ -1,5 +1,4 @@ -""" provide version info, conftest/environment config names. -""" +""" version info, help messages, tracing configuration. """ import py import pytest import inspect, sys --- a/doc/example/nonpython.txt +++ b/doc/example/nonpython.txt @@ -27,17 +27,17 @@ now execute the test specification:: nonpython $ py.test test_simple.yml =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 test path 1: test_simple.yml - + test_simple.yml .F - + ================================= FAILURES ================================= ______________________________ usecase: hello ______________________________ usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.37 seconds ==================== + ==================== 1 failed, 1 passed in 0.42 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 @@ -47,22 +47,21 @@ your own domain specific testing languag .. note:: ``repr_failure(excinfo)`` is called for representing test failures. - If you create custom collection nodes you can return an error + If you create custom collection nodes you can return an error representation string of your choice. It will be reported as a (red) string. - ``reportinfo()`` -is used for representing the test location and is also consulted for +``reportinfo()`` is used for representing the test location and is also consulted for reporting in ``verbose`` mode:: nonpython $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev17 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 -- /home/hpk/venv/0/bin/python test path 1: /home/hpk/p/pytest/doc/example/nonpython - - test_simple.yml:1: usecase: ok PASSED - test_simple.yml:1: usecase: hello FAILED - + + test_simple.yml <- test_simple.yml:1: usecase: ok PASSED + test_simple.yml <- test_simple.yml:1: usecase: hello FAILED + ================================= FAILURES ================================= ______________________________ usecase: hello ______________________________ usecase execution failed @@ -74,7 +73,7 @@ While developing your custom test collec interesting to just look at the collection tree:: nonpython $ py.test --collectonly - - - - + + + + --- /dev/null +++ b/doc/announce/index.txt @@ -0,0 +1,22 @@ + +Release announcements +=========================================== + +.. toctree:: + :maxdepth: 2 + + release-2.0.0 + release-1.3.4 + release-1.3.3 + release-1.3.2 + release-1.3.1 + release-1.3.0 + release-1.2.1 + release-1.2.0 + release-1.1.1 + release-1.1.0 + release-1.0.2 + release-1.0.1 + release-1.0.0 + release-0.9.2 + release-0.9.0 From commits-noreply at bitbucket.org Sat Nov 6 19:44:53 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 13:44:53 -0500 (CDT) Subject: [py-svn] pytest commit e7083ff9d8ef: show traces for all hook invocations not just "path/node" based ones Message-ID: <20101106184453.0D9DD2419C5@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289069184 -3600 # Node ID e7083ff9d8efeb354f4b5bdfff77158802a1aa06 # Parent 5f89191fcd0a77727f9c8e5a3a73f8cc2e964e31 show traces for all hook invocations not just "path/node" based ones --- a/pytest/main.py +++ b/pytest/main.py @@ -94,7 +94,6 @@ class PluginManager(object): self._name2plugin[name] = plugin self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) self.hook.pytest_plugin_registered(manager=self, plugin=plugin) - self.trace("registered", plugin) if not prepend: self._plugins.append(plugin) else: @@ -380,13 +379,15 @@ class HookCaller: def __call__(self, **kwargs): methods = self.hookrelay._pm.listattr(self.name) - mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - return mc.execute() + return self._docall(methods, kwargs) def pcall(self, plugins, **kwargs): + methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) + return self._docall(methods, kwargs) + + def _docall(self, methods, kwargs): self.trace(self.name, kwargs) self.trace.root.indent += 1 - methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) mc = MultiCall(methods, kwargs, firstresult=self.firstresult) res = mc.execute() if res: From commits-noreply at bitbucket.org Sat Nov 6 20:05:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 14:05:02 -0500 (CDT) Subject: [py-svn] pytest commit 3986b64bdce8: test and fix tracing indentation in case of exceptions Message-ID: <20101106190502.EF3396C111B@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289070392 -3600 # Node ID 3986b64bdce8659f16e115c086b9c6dc075fe58c # Parent e7083ff9d8efeb354f4b5bdfff77158802a1aa06 test and fix tracing indentation in case of exceptions --- a/testing/test_main.py +++ b/testing/test_main.py @@ -266,18 +266,26 @@ class TestBootstrapping: l = list(plugins.listattr('x')) assert l == [41, 42, 43] - def test_register_trace(self): + def test_hook_tracing(self): pm = PluginManager() + saveindent = [] class api1: x = 41 + def pytest_plugin_registered(self, plugin): + saveindent.append(pm.trace.root.indent) + raise ValueError(42) l = [] - pm.trace.setmyprocessor(lambda kw, args: l.append((kw, args))) + pm.trace.root.setwriter(l.append) + indent = pm.trace.root.indent p = api1() pm.register(p) + + assert pm.trace.root.indent == indent assert len(l) == 1 - kw, args = l[0] - assert args[0] == "registered" - assert args[1] == p + assert 'pytest_plugin_registered' in l[0] + py.test.raises(ValueError, lambda: pm.register(api1())) + assert pm.trace.root.indent == indent + assert saveindent[0] > indent class TestPytestPluginInteractions: --- a/pytest/main.py +++ b/pytest/main.py @@ -389,10 +389,12 @@ class HookCaller: self.trace(self.name, kwargs) self.trace.root.indent += 1 mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - res = mc.execute() - if res: - self.trace(res) - self.trace.root.indent -= 1 + try: + res = mc.execute() + if res: + self.trace(res) + finally: + self.trace.root.indent -= 1 return res _preinit = [PluginManager(load=True)] # triggers default plugin importing From commits-noreply at bitbucket.org Sat Nov 6 23:44:50 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 17:44:50 -0500 (CDT) Subject: [py-svn] pytest commit c6e8dde3a8da: show return values of hooks more explicitely Message-ID: <20101106224450.D08866C1425@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289070765 -3600 # Node ID c6e8dde3a8da522120ea8259772da3d5e3f27217 # Parent 3986b64bdce8659f16e115c086b9c6dc075fe58c show return values of hooks more explicitely --- a/pytest/main.py +++ b/pytest/main.py @@ -392,7 +392,7 @@ class HookCaller: try: res = mc.execute() if res: - self.trace(res) + self.trace("finish", self.name, "-->", res) finally: self.trace.root.indent -= 1 return res From commits-noreply at bitbucket.org Sat Nov 6 23:44:50 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 17:44:50 -0500 (CDT) Subject: [py-svn] pytest commit 809c2914a73d: introduce an option that avoids discovery of classes other than unittest.TestCase in modules Message-ID: <20101106224450.C3E8F6C133E@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289083548 -3600 # Node ID 809c2914a73dd1604f9188842f13f9cb8f42a2ff # Parent c75f441489d31835abeeabd20c0f6cfd7441b269 introduce an option that avoids discovery of classes other than unittest.TestCase in modules importing unittest. --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -294,6 +294,7 @@ class TestInvocationVariants: assert not retcode out, err = capsys.readouterr() assert "--help" in out + pytest.raises(ValueError, lambda: pytest.main(retcode)) def test_invoke_with_path(self, testdir, capsys): retcode = testdir.pytestmain(testdir.tmpdir) @@ -330,3 +331,18 @@ class TestInvocationVariants: result.stderr.fnmatch_lines([ "ERROR*file*or*package*not*found*", ]) + + + def test_noclass_discovery_if_not_testcase(self, testdir): + testpath = testdir.makepyfile(""" + import unittest + import py + class TestHello(object): + def test_hello(self): + assert self.attr + + class RealTest(TestHello, unittest.TestCase): + attr = 42 + """) + reprec = testdir.inline_run(testpath) + reprec.assertoutcome(passed=1) --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -59,6 +59,8 @@ def pytest_pycollect_makeitem(__multical if res is not None: return res if collector._istestclasscandidate(name, obj): + if hasattr(collector.obj, 'unittest'): + return # we assume it's a mixin class for a TestCase derived one return Class(name, parent=collector) elif collector.funcnamefilter(name) and hasattr(obj, '__call__'): if is_generator(obj): @@ -124,6 +126,7 @@ class PyobjMixin(object): return self._fslineno def reportinfo(self): + # XXX caching? obj = self.obj if hasattr(obj, 'compat_co_firstlineno'): # nose compatibility --- a/testing/plugin/test_assertion.py +++ b/testing/plugin/test_assertion.py @@ -161,6 +161,15 @@ def test_functional(testdir): result = testdir.runpytest("--no-assert") assert "3 == 4" not in result.stdout.str() +def test_AssertionErrorIdentity(testdir): + testdir.makepyfile(""" + def test_hello(): + import exceptions + assert AssertionError is exceptions.AssertionError + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines(["*1 passed*"]) + def test_triple_quoted_string_issue113(testdir): testdir.makepyfile(""" def test_hello(): --- a/tox.ini +++ b/tox.ini @@ -52,5 +52,6 @@ commands= [pytest] minversion=2.0 plugins=pytester -addargs=-rfx +addopts=-rfx --pyargs rsyncdirs=pytest testing + --- a/pytest/plugin/unittest.py +++ b/pytest/plugin/unittest.py @@ -1,12 +1,13 @@ -""" support discovery and running of traditional "unittest.py" style tests. """ +""" discovery and running of std-library "unittest" style tests. """ import py import sys def pytest_pycollect_makeitem(collector, name, obj): - if 'unittest' not in sys.modules: - return # nobody derived unittest.TestCase + unittest = sys.modules.get('unittest') + if unittest is None: + return # nobody can have derived unittest.TestCase try: - isunit = issubclass(obj, py.std.unittest.TestCase) + isunit = issubclass(obj, unittest.TestCase) except KeyboardInterrupt: raise except Exception: --- a/pytest/plugin/assertion.py +++ b/pytest/plugin/assertion.py @@ -3,6 +3,7 @@ support for presented detailed informati """ import py import sys +from pytest.plugin.monkeypatch import monkeypatch def pytest_addoption(parser): group = parser.getgroup("debugconfig") @@ -15,25 +16,21 @@ def pytest_configure(config): # py._code._assertionnew to detect this plugin was loaded and in # turn call the hooks defined here as part of the # DebugInterpreter. + config._monkeypatch = m = monkeypatch() if not config.getvalue("noassert") and not config.getvalue("nomagic"): warn_about_missing_assertion() - config._oldassertion = py.builtin.builtins.AssertionError - config._oldbinrepr = py.code._reprcompare - py.builtin.builtins.AssertionError = py.code._AssertionError def callbinrepr(op, left, right): hook_result = config.hook.pytest_assertrepr_compare( config=config, op=op, left=left, right=right) for new_expl in hook_result: if new_expl: return '\n~'.join(new_expl) - py.code._reprcompare = callbinrepr + m.setattr(py.builtin.builtins, + 'AssertionError', py.code._AssertionError) + m.setattr(py.code, '_reprcompare', callbinrepr) def pytest_unconfigure(config): - if hasattr(config, '_oldassertion'): - py.builtin.builtins.AssertionError = config._oldassertion - py.code._reprcompare = config._oldbinrepr - del config._oldassertion - del config._oldbinrepr + config._monkeypatch.undo() def warn_about_missing_assertion(): try: --- a/pytest/main.py +++ b/pytest/main.py @@ -403,7 +403,11 @@ def main(args=None, plugins=None): if args is None: args = sys.argv[1:] elif not isinstance(args, (tuple, list)): - args = py.std.shlex.split(str(args)) + if isinstance(args, py.path.local): + args = str(args) + if not isinstance(args, str): + raise ValueError("not a string or argument list: %r" % (args,)) + args = py.std.shlex.split(args) if _preinit: _pluginmanager = _preinit.pop(0) else: # subsequent calls to main will create a fresh instance From commits-noreply at bitbucket.org Sat Nov 6 23:44:50 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 17:44:50 -0500 (CDT) Subject: [py-svn] pytest commit c75f441489d3: introduce new --testpkg importpath option, add more meat to draft release announcement Message-ID: <20101106224450.B089C6C111B@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289078253 -3600 # Node ID c75f441489d31835abeeabd20c0f6cfd7441b269 # Parent c6e8dde3a8da522120ea8259772da3d5e3f27217 introduce new --testpkg importpath option, add more meat to draft release announcement --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -309,3 +309,24 @@ class TestInvocationVariants: out, err = capsys.readouterr() assert "--myopt" in out + def test_cmdline_python_package(self, testdir): + path = testdir.mkpydir("tpkg") + path.join("test_hello.py").write("def test_hello(): pass") + path.join("test_world.py").write("def test_world(): pass") + result = testdir.runpytest("--pyargs", "tpkg") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) + result = testdir.runpytest("--pyargs", "tpkg.test_hello") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + + def test_cmdline_python_package_not_exists(self, testdir): + result = testdir.runpytest("--pyargs", "tpkgwhatv") + assert result.ret + result.stderr.fnmatch_lines([ + "ERROR*file*or*package*not*found*", + ]) --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -9,8 +9,8 @@ cutdir = py.path.local(pytest.__file__). def pytest_addoption(parser): - group = parser.getgroup("terminal reporting") - group._addoption('--funcargs', + group = parser.getgroup("general") + group.addoption('--funcargs', action="store_true", dest="showfuncargs", default=False, help="show available function arguments, sorted by plugin") --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev19' +__version__ = '2.0.0.dev20' __all__ = ['config', 'cmdline'] --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -31,6 +31,8 @@ def pytest_addoption(parser): group.addoption('--collectonly', action="store_true", dest="collectonly", help="only collect tests, don't execute them."), + group.addoption('--pyargs', action="store_true", + help="try to interpret all arguments as python packages.") group.addoption("--ignore", action="append", metavar="path", help="ignore path during collection (multi-allowed).") group.addoption('--confcutdir', dest="confcutdir", default=None, @@ -429,12 +431,29 @@ class Collection(FSCollector): ihook.pytest_collect_directory(path=path, parent=self) return True + def _tryconvertpyarg(self, x): + try: + mod = __import__(x, None, None, ['__doc__']) + except ImportError: + return x + p = py.path.local(mod.__file__) + if p.purebasename == "__init__": + p = p.dirpath() + return p + def _parsearg(self, arg): """ return (fspath, names) tuple after checking the file exists. """ + arg = str(arg) + if self.config.option.pyargs: + arg = self._tryconvertpyarg(arg) parts = str(arg).split("::") path = self.fspath.join(parts[0], abs=True) if not path.check(): - raise pytest.UsageError("file not found: %s" %(path,)) + if self.config.option.pyargs: + msg = "file or package not found: " + else: + msg = "file not found: " + raise pytest.UsageError(msg + arg) parts[0] = path return parts --- a/doc/usage.txt +++ b/doc/usage.txt @@ -25,39 +25,18 @@ To stop the testing process after the fi py.test -x # stop after first failure py.test -maxfail=2 # stop after two failures -calling pytest from Python code ----------------------------------------------------- +specifying tests / selecting tests +--------------------------------------------------- -.. versionadded: 2.0 +Several test run options:: -You can invoke ``py.test`` from Python code directly:: + py.test test_mod.py # run tests in module + py.test somepath # run all tests below path + py.test -k string # only run tests whose names contain a string - pytest.main() +Import 'pkg' and use its filesystem location to find and run tests:: -this acts as if you would call "py.test" from the command line. -It will not raise ``SystemExit`` but return the exitcode instead. -You can pass in options and arguments:: - - pytest.main(['x', 'mytestdir']) - -or pass in a string:: - - pytest.main("-x mytestdir") - -You can specify additional plugins to ``pytest.main``:: - - # content of myinvoke.py - import pytest - class MyPlugin: - def pytest_addoption(self, parser): - raise pytest.UsageError("hi from our plugin") - - pytest.main(plugins=[MyPlugin()]) - -Running it will exit quickly:: - - $ python myinvoke.py - ERROR: hi from our plugin + py.test --testpkg=pypkg # run all tests found below directory of pypkg calling pytest through ``python -m pytest`` ----------------------------------------------------- @@ -162,4 +141,39 @@ for example ``-x`` if you only want to s Currently only pasting to the http://paste.pocoo.org service is implemented. +calling pytest from Python code +---------------------------------------------------- + +.. versionadded: 2.0 + +You can invoke ``py.test`` from Python code directly:: + + pytest.main() + +this acts as if you would call "py.test" from the command line. +It will not raise ``SystemExit`` but return the exitcode instead. +You can pass in options and arguments:: + + pytest.main(['x', 'mytestdir']) + +or pass in a string:: + + pytest.main("-x mytestdir") + +You can specify additional plugins to ``pytest.main``:: + + # content of myinvoke.py + import pytest + class MyPlugin: + def pytest_addoption(self, parser): + raise pytest.UsageError("hi from our plugin") + + pytest.main(plugins=[MyPlugin()]) + +Running it will exit quickly:: + + $ python myinvoke.py + ERROR: hi from our plugin + + .. include:: links.inc --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev19', + version='2.0.0.dev20', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/doc/announce/release-2.0.0.txt +++ b/doc/announce/release-2.0.0.txt @@ -3,14 +3,24 @@ py.test 2.0.0: standalone, features++, i XXX PENDING -Welcome to pytest-2.0.0! With this release py.test becomes its own standalone -PyPI distribution, named ``pytest``, installing the ``py.test`` command line -tool. Apart from a great internal cleanup this release comes with tons -of improvements and new features and a completely revamped extensive -documentation, including many continously tested examples. See +Welcome to pytest-2.0.0, rapid and easy testing for and with Python. +py.test now comes as its own PyPI distribution named ``pytest`` which +installs the ``py.test`` tool. It removes most long-deprecated code, +providing for a much smaller and easier to understand code base. There +are also many new features and much improved documentation. See http://pytest.org +for details or below for some more information. + +Thanks to all issue reporters and people asking questions or +complaining. Particular thanks to Floris Bruynooghe and Ronny Pfannschmidt +for their great coding contributions. + +best, +holger + + New Features ----------------------- @@ -20,31 +30,117 @@ New Features python -m pytest.main # on all pythons >= 2.5 import pytest ; pytest.main(args, plugins) - see http://pytest.org/2.0.0/invoke.html for details. + see http://pytest.org/2.0.0/usage.html for details. + +- new and better reporting information in assert expressions + which compare lists, sequences or strings. + + see http://pytest.org/2.0.0/assert.html for details. - new configuration through ini-files (setup.cfg or tox.ini recognized), for example:: [pytest] - norecursedirs = .hg _build - python_collect_funcprefix = test_ - python_collect_classprefix = Test + norecursedirs = .hg data* # don't ever recurse in such dirs + addopts = -x --pyargs # add these options by default see http://pytest.org/2.0.0/customize.html -- +- improved standard unittest support. For example you can now run + the tests of an installed 'unittest' package with py.test:: -Thanks to issue reporters, people asking questions, complaining and -generally to Ronny Pfannschmidt for his awesome help on many issues. + py.test --pyargs unittest -cheers, -holger krekel +- add a new "-q" option which decreases verbosity and prints a more + nose/unittest-style "dot" output. -Changes between 1.3.3 and 1.3.4 +Fixes +----------------------- + +- fix issue126 - introduce py.test.set_trace() to trace execution via + PDB during the running of tests even if capturing is ongoing. +- fix issue124 - make reporting more resilient against tests opening + files on filedescriptor 1 (stdout). +- fix issue109 - sibling conftest.py files will not be loaded. + (and Directory collectors cannot be customized anymore from a Directory's + conftest.py - this needs to happen at least one level up). +- fix issue88 (finding custom test nodes from command line arg) +- fix issue93 stdout/stderr is captured while importing conftest.py +- fix bug: unittest collected functions now also can have "pytestmark" + applied at class/module level + +Important Note on importing "pytest" versus "py.test" +------------------------------------------------------- + +The usual way in pre-2.0 times to use py.test in python code was +to import "py" and then e.g. use "py.test.raises" for the helper. +This remains valid and is not planned to be deprecated. However, +in most examples and internal code you'll find "import pytest" +and "pytest.raises" used as the recommended default way. + +(Incompatible) Removals +----------------------------- + +- py.test.config is now only available if you are in a test run. + +- the following (mostly already deprecated) functionality was removed: + - removed support for Module/Class/... collection node definitions + in conftest.py files. They will cause nothing special. + - removed support for calling the pre-1.0 collection API of "run()" and "join" + - removed reading option values from conftest.py files or env variables. + This can now be done much much better and easier through the ini-file + mechanism and the "addopts" entry in particular. + - removed the "disabled" attribute in test classes. Use the skipping + and pytestmark mechanism to skip or xfail a test class. + +- py.test.collect.Directory does not exist anymore and it + is not possible to provide an own "Directory" object. + If you have used this and don#t know what to do, get + in contact. We'll figure someting out. + + Note that pytest_collect_directory() is still called but + any return value will be ignored. This allows to keep + old code working that performed for example "py.test.skip()" + in collect() to prevent recursion into directory trees + if a certain dependency or command line option is missing. + + +More Detailed Changes between 1.3.4 and 2.0.0 ================================================== -- fix issue111: improve install documentation for windows -- fix issue119: fix custom collectability of __init__.py as a module -- fix issue116: --doctestmodules work with __init__.py files as well -- fix issue115: unify internal exception passthrough/catching/GeneratorExit -- fix issue118: new --tb=native for presenting cpython-standard exceptions +- pytest-2.0 is now its own package and depends on pylib-2.0 +- new ability: python -m pytest / python -m pytest.main ability +- new python invcation: pytest.main(args, plugins) to load + some custom plugins early. +- try harder to run unittest test suites in a more compatible manner + by deferring setup/teardown semantics to the unittest package. +- introduce a new way to set config options via ini-style files, + by default setup.cfg and tox.ini files are searched. The old + ways (certain environment variables, dynamic conftest.py reading + is removed). +- add a new "-q" option which decreases verbosity and prints a more + nose/unittest-style "dot" output. +- fix issue126 - introduce py.test.set_trace() to trace execution via + PDB during the running of tests even if capturing is ongoing. +- fix issue123 - new "python -m py.test" invocation for py.test + (requires Python 2.5 or above) +- fix issue124 - make reporting more resilient against tests opening + files on filedescriptor 1 (stdout). +- fix issue109 - sibling conftest.py files will not be loaded. + (and Directory collectors cannot be customized anymore from a Directory's + conftest.py - this needs to happen at least one level up). +- introduce (customizable) assertion failure representations and enhance + output on assertion failures for comparisons and other cases (Floris Bruynooghe) +- nose-plugin: pass through type-signature failures in setup/teardown + functions instead of not calling them (Ed Singleton) +- remove py.test.collect.Directory (follows from a major refactoring + and simplification of the collection process) +- majorly reduce py.test core code, shift function/python testing to own plugin +- fix issue88 (finding custom test nodes from command line arg) +- refine 'tmpdir' creation, will now create basenames better associated + with test names (thanks Ronny) +- "xpass" (unexpected pass) tests don't cause exitcode!=0 +- fix issue131 / issue60 - importing doctests in __init__ files used as namespace packages +- fix issue93 stdout/stderr is captured while importing conftest.py +- fix bug: unittest collected functions now also can have "pytestmark" + applied at class/module level From commits-noreply at bitbucket.org Sun Nov 7 00:21:06 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 18:21:06 -0500 (CDT) Subject: [py-svn] pytest commit 1df9ce97afeb: some fixes for --pyargs situations and the docs, remove wrongly added test Message-ID: <20101106232106.EF0961E1178@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289085736 -3600 # Node ID 1df9ce97afeb503b2114f2de77684f7e88297537 # Parent 809c2914a73dd1604f9188842f13f9cb8f42a2ff some fixes for --pyargs situations and the docs, remove wrongly added test --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -310,7 +310,8 @@ class TestInvocationVariants: out, err = capsys.readouterr() assert "--myopt" in out - def test_cmdline_python_package(self, testdir): + def test_cmdline_python_package(self, testdir, monkeypatch): + monkeypatch.delenv('PYTHONDONTWRITEBYTECODE') path = testdir.mkpydir("tpkg") path.join("test_hello.py").write("def test_hello(): pass") path.join("test_world.py").write("def test_world(): pass") --- a/doc/example/pythoncollection.txt +++ b/doc/example/pythoncollection.txt @@ -12,6 +12,18 @@ You can set the :confval:`norecursedirs` This would tell py.test to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. +always try to interpret arguments as Python packages +----------------------------------------------------- + +You can use the ``--pyargs`` option to make py.test try +interpreting arguments as python package names, deriving +their file system path and then running the test. Through +an ini-file and the :confval:`addopts` option you can make +this change more permanently:: + + # content of setup.cfg or tox.ini + [pytest] + addopts = --pyargs finding out what is collected ----------------------------------------------- --- a/testing/plugin/test_assertion.py +++ b/testing/plugin/test_assertion.py @@ -161,15 +161,6 @@ def test_functional(testdir): result = testdir.runpytest("--no-assert") assert "3 == 4" not in result.stdout.str() -def test_AssertionErrorIdentity(testdir): - testdir.makepyfile(""" - def test_hello(): - import exceptions - assert AssertionError is exceptions.AssertionError - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines(["*1 passed*"]) - def test_triple_quoted_string_issue113(testdir): testdir.makepyfile(""" def test_hello(): --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -366,9 +366,10 @@ class Collection(FSCollector): self.trace.root.indent += 1 self._notfound = [] self._initialpaths = set() - self._initialargs = args + self._initialparts = [] for arg in args: parts = self._parsearg(arg) + self._initialparts.append(parts) self._initialpaths.add(parts[0]) self.ihook.pytest_collectstart(collector=self) rep = self.ihook.pytest_make_collect_report(collector=self) @@ -376,7 +377,7 @@ class Collection(FSCollector): self.trace.root.indent -= 1 if self._notfound: for arg, exc in self._notfound: - line = "no name %r in any of %r" % (exc.args[1], exc.args[0]) + line = "(no name %r in any of %r)" % (arg, exc.args[0]) raise pytest.UsageError("not found: %s\n%s" %(arg, line)) if not genitems: return rep.result @@ -388,8 +389,9 @@ class Collection(FSCollector): return items def collect(self): - for arg in self._initialargs: - self.trace("processing arg", arg) + for parts in self._initialparts: + arg = "::".join(map(str, parts)) + self.trace("processing argument", arg) self.trace.root.indent += 1 try: for x in self._collect(arg): @@ -417,8 +419,9 @@ class Collection(FSCollector): def _collectfile(self, path): ihook = self.gethookproxy(path) - if ihook.pytest_ignore_collect(path=path, config=self.config): - return () + if not self.isinitpath(path): + if ihook.pytest_ignore_collect(path=path, config=self.config): + return () return ihook.pytest_collect_file(path=path, parent=self) def _recurse(self, path): @@ -439,6 +442,8 @@ class Collection(FSCollector): p = py.path.local(mod.__file__) if p.purebasename == "__init__": p = p.dirpath() + else: + p = p.new(basename=p.purebasename+".py") return p def _parsearg(self, arg): --- a/doc/usage.txt +++ b/doc/usage.txt @@ -36,7 +36,7 @@ Several test run options:: Import 'pkg' and use its filesystem location to find and run tests:: - py.test --testpkg=pypkg # run all tests found below directory of pypkg + py.test --pyargs pkg # run all tests found below directory of pypkg calling pytest through ``python -m pytest`` ----------------------------------------------------- --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -213,6 +213,19 @@ class TestCustomConftests: assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) + def test_ignore_collect_not_called_on_argument(self, testdir): + testdir.makeconftest(""" + def pytest_ignore_collect(path, config): + return True + """) + p = testdir.makepyfile("def test_hello(): pass") + result = testdir.runpytest(p) + assert result.ret == 0 + assert "1 passed" in result.stdout.str() + result = testdir.runpytest() + assert result.ret == 0 + assert "1 passed" not in result.stdout.str() + def test_collectignore_exclude_on_option(self, testdir): testdir.makeconftest(""" collect_ignore = ['hello', 'test_world.py'] From commits-noreply at bitbucket.org Sun Nov 7 00:33:42 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 18:33:42 -0500 (CDT) Subject: [py-svn] pytest commit 43dccff20598: replace with list comp Message-ID: <20101106233342.7EA136C1304@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1289086440 18000 # Node ID 43dccff205987b4737e5d8c4fa077224f38b78c0 # Parent 1df9ce97afeb503b2114f2de77684f7e88297537 replace with list comp --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -376,7 +376,7 @@ class Config(object): elif type == "args": return py.std.shlex.split(value) elif type == "linelist": - return filter(None, map(lambda x: x.strip(), value.split("\n"))) + return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] else: assert type is None return value From commits-noreply at bitbucket.org Sun Nov 7 00:36:05 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 18:36:05 -0500 (CDT) Subject: [py-svn] pytest commit 8dad8f0e7649: PYTHONDONTWRITEBYTECODE might not be set Message-ID: <20101106233605.007956C1304@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1289086584 18000 # Node ID 8dad8f0e7649d22b6ffae74c9a806324460c871b # Parent 43dccff205987b4737e5d8c4fa077224f38b78c0 PYTHONDONTWRITEBYTECODE might not be set --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -311,7 +311,7 @@ class TestInvocationVariants: assert "--myopt" in out def test_cmdline_python_package(self, testdir, monkeypatch): - monkeypatch.delenv('PYTHONDONTWRITEBYTECODE') + monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', False) path = testdir.mkpydir("tpkg") path.join("test_hello.py").write("def test_hello(): pass") path.join("test_world.py").write("def test_world(): pass") From commits-noreply at bitbucket.org Sun Nov 7 01:08:44 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 19:08:44 -0500 (CDT) Subject: [py-svn] pytest commit 0ce9db200a51: some python3 related fixes Message-ID: <20101107000844.E0DC72410C8@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289088615 -3600 # Node ID 0ce9db200a51ce2de73ba3b2a009c08b975a058d # Parent 8dad8f0e7649d22b6ffae74c9a806324460c871b some python3 related fixes --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -325,6 +325,11 @@ class TestInvocationVariants: result.stdout.fnmatch_lines([ "*1 passed*" ]) + result = testdir.runpytest("--pyargs", ".") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) def test_cmdline_python_package_not_exists(self, testdir): result = testdir.runpytest("--pyargs", "tpkgwhatv") @@ -342,7 +347,7 @@ class TestInvocationVariants: def test_hello(self): assert self.attr - class RealTest(TestHello, unittest.TestCase): + class RealTest(unittest.TestCase, TestHello): attr = 42 """) reprec = testdir.inline_run(testpath) --- a/testing/test_config.py +++ b/testing/test_config.py @@ -40,6 +40,17 @@ class TestParseIni: "*tox.ini:2*requires*9.0*actual*" ]) + @py.test.mark.xfail(reason="probably not needed") + def test_confcutdir(self, testdir): + sub = testdir.mkdir("sub") + sub.chdir() + testdir.makeini(""" + [pytest] + addopts = --qwe + """) + result = testdir.runpytest("--confcutdir=.") + assert result.ret == 0 + class TestConfigCmdlineParsing: def test_parsing_again_fails(self, testdir): config = testdir.reparseconfig([testdir.tmpdir]) --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -437,7 +437,7 @@ class Collection(FSCollector): def _tryconvertpyarg(self, x): try: mod = __import__(x, None, None, ['__doc__']) - except ImportError: + except (ValueError, ImportError): return x p = py.path.local(mod.__file__) if p.purebasename == "__init__": --- a/pytest/plugin/unittest.py +++ b/pytest/plugin/unittest.py @@ -33,6 +33,10 @@ class UnitTestCase(py.test.collect.Class meth() class TestCaseFunction(py.test.collect.Function): + def setup(self): + pass + def teardown(self): + pass def startTest(self, testcase): pass def addError(self, testcase, rawexcinfo): --- a/pytest/main.py +++ b/pytest/main.py @@ -68,8 +68,11 @@ class PluginManager(object): self.trace = TagTracer().get("pluginmanage") if os.environ.get('PYTEST_DEBUG'): err = sys.stderr - if hasattr(os, 'dup'): - err = py.io.dupfile(err) + encoding = getattr(err, 'encoding', 'utf8') + try: + err = py.io.dupfile(err, encoding=encoding) + except Exception: + pass self.trace.root.setwriter(err.write) self.hook = HookRelay([hookspec], pm=self) self.register(self) --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -496,8 +496,6 @@ class Test_getinitialnodes: config = testdir.reparseconfig([x]) col = testdir.getnode(config, x) assert isinstance(col, py.test.collect.Module) - print col.obj - print col.listchain() assert col.name == 'subdir/x.py' assert col.parent.parent is None for col in col.listchain(): From commits-noreply at bitbucket.org Sun Nov 7 01:12:07 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 6 Nov 2010 19:12:07 -0500 (CDT) Subject: [py-svn] pytest commit 98575d7ad93e: fix bug showing up on windows Message-ID: <20101107001207.94CEF1E12AA@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289088820 -3600 # Node ID 98575d7ad93e5bce342be4f64089798b73a61ed1 # Parent 0ce9db200a51ce2de73ba3b2a009c08b975a058d fix bug showing up on windows --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -323,7 +323,7 @@ class FSCollector(Collector): return "." relpath = self.collection.fspath.bestrelpath(self.fspath) if os.sep != "/": - relpath = str(path).replace(os.sep, "/") + relpath = relpath.replace(os.sep, "/") return relpath class File(FSCollector): From commits-noreply at bitbucket.org Sun Nov 7 07:13:18 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 01:13:18 -0500 (CDT) Subject: [py-svn] pytest commit c6eebf5ff629: fix test, bump version Message-ID: <20101107061318.E2BAD2410C8@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289110490 -3600 # Node ID c6eebf5ff629d2eb8f5ad3e1192f90f5ff1e834f # Parent 98575d7ad93e5bce342be4f64089798b73a61ed1 fix test, bump version --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev20', + version='2.0.0.dev21', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -273,6 +273,7 @@ class TestInvocationVariants: res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) assert res.ret == 1 + @py.test.mark.skipif("sys.version_info < (2,5)") def test_python_pytest_main(self, testdir): p1 = testdir.makepyfile("def test_pass(): pass") res = testdir.run(py.std.sys.executable, "-m", "pytest.main", str(p1)) --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev20' +__version__ = '2.0.0.dev21' __all__ = ['config', 'cmdline'] From commits-noreply at bitbucket.org Sun Nov 7 09:04:24 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 02:04:24 -0600 (CST) Subject: [py-svn] pytest commit 2bffb78b3c42: bump version Message-ID: <20101107080424.3517A6C1304@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289117154 -3600 # Node ID 2bffb78b3c42f40f963bc62f5095f01b75465b68 # Parent c6eebf5ff629d2eb8f5ad3e1192f90f5ff1e834f bump version --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev21', + version='2.0.0.dev22', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev21' +__version__ = '2.0.0.dev22' __all__ = ['config', 'cmdline'] From commits-noreply at bitbucket.org Sun Nov 7 10:23:55 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 03:23:55 -0600 (CST) Subject: [py-svn] pytest commit 6a5bbc78bad9: probably the last major internal cleanup action: rename collection to Message-ID: <20101107092355.1F7E71E135A@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289121598 -3600 # Node ID 6a5bbc78bad973e969317db8b86f151e6539c42e # Parent 2bffb78b3c42f40f963bc62f5095f01b75465b68 probably the last major internal cleanup action: rename collection to session which now is the root collection node. This means that session, collection and config objects have a more defined relationship (previously there was no way to get from a collection node or even from a runtest hook to the session object which was strange). --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev22' +__version__ = '2.0.0.dev23' __all__ = ['config', 'cmdline'] --- a/doc/example/builtin.txt +++ b/doc/example/builtin.txt @@ -27,10 +27,10 @@ Let's run our little function:: F ================================= FAILURES ================================= ______________________________ test_something ______________________________ - + def test_something(): > checkconfig(42) E Failed: not configured: 42 - + test_checkconfig.py:8: Failed 1 failed in 0.02 seconds --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -34,7 +34,7 @@ Running the test looks like this:: $ py.test test_simplefactory.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_simplefactory.py test_simplefactory.py F @@ -136,7 +136,7 @@ Running this:: $ py.test test_example.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_example.py test_example.py .........F @@ -158,7 +158,6 @@ the test collection phase which is separ Let's just look at what is collected:: $ py.test --collectonly test_example.py - @@ -175,7 +174,7 @@ If you want to select only the run with $ py.test -v -k 7 test_example.py # or -k test_func[7] =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 -- /home/hpk/venv/0/bin/python test path 1: test_example.py test_example.py <- test_example.py:6: test_func[7] PASSED --- a/doc/mark.txt +++ b/doc/mark.txt @@ -88,20 +88,20 @@ You can use the ``-k`` command line opti $ py.test -k webtest # running with the above defined examples yields =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 - test path 1: /tmp/doc-exec-407 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + test path 1: /tmp/doc-exec-527 test_mark.py .. test_mark_classlevel.py .. - ========================= 4 passed in 0.01 seconds ========================= + ========================= 4 passed in 0.02 seconds ========================= And you can also run all tests except the ones that match the keyword:: $ py.test -k-webtest =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 - test path 1: /tmp/doc-exec-407 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + test path 1: /tmp/doc-exec-527 ===================== 4 tests deselected by '-webtest' ===================== ======================= 4 deselected in 0.01 seconds ======================= @@ -110,8 +110,8 @@ Or to only select the class:: $ py.test -kTestClass =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 - test path 1: /tmp/doc-exec-407 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + test path 1: /tmp/doc-exec-527 test_mark_classlevel.py .. --- a/doc/unittest.txt +++ b/doc/unittest.txt @@ -24,7 +24,7 @@ Running it yields:: $ py.test test_unittest.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_unittest.py test_unittest.py F @@ -56,7 +56,7 @@ Running it yields:: /usr/lib/python2.6/unittest.py:350: AssertionError ----------------------------- Captured stdout ------------------------------ hello - ========================= 1 failed in 0.02 seconds ========================= + ========================= 1 failed in 0.12 seconds ========================= .. _`unittest.py style`: http://docs.python.org/library/unittest.html --- a/doc/tmpdir.txt +++ b/doc/tmpdir.txt @@ -28,7 +28,7 @@ Running this would result in a passed te $ py.test test_tmpdir.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_tmpdir.py test_tmpdir.py F @@ -36,7 +36,7 @@ Running this would result in a passed te ================================= FAILURES ================================= _____________________________ test_create_file _____________________________ - tmpdir = local('/tmp/pytest-243/test_create_file0') + tmpdir = local('/tmp/pytest-447/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") @@ -47,7 +47,7 @@ Running this would result in a passed te E assert 0 test_tmpdir.py:7: AssertionError - ========================= 1 failed in 0.04 seconds ========================= + ========================= 1 failed in 0.15 seconds ========================= .. _`base temporary directory`: --- a/testing/plugin/test_runner.py +++ b/testing/plugin/test_runner.py @@ -244,7 +244,7 @@ class TestExecutionForked(BaseFunctional assert rep.failed assert rep.when == "???" -class TestCollectionReports: +class TestSessionReports: def test_collect_result(self, testdir): col = testdir.getmodulecol(""" def test_func1(): --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -516,7 +516,7 @@ def test_traceconfig(testdir, monkeypatc def test_debug(testdir, monkeypatch): result = testdir.runpytest("--debug") result.stderr.fnmatch_lines([ - "*registered*session*", + "*pytest_sessionstart*session*", ]) assert result.ret == 0 --- a/doc/example/simple.txt +++ b/doc/example/simple.txt @@ -130,7 +130,7 @@ let's run the full monty:: E assert 4 < 4 test_compute.py:3: AssertionError - 1 failed, 4 passed in 0.02 seconds + 1 failed, 4 passed in 0.03 seconds As expected when running the full range of ``param1`` values we'll get an error on the last one. --- a/doc/doctest.txt +++ b/doc/doctest.txt @@ -44,7 +44,7 @@ then you can just invoke ``py.test`` wit $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 - test path 1: /tmp/doc-exec-400 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + test path 1: /tmp/doc-exec-519 ============================= in 0.00 seconds ============================= --- a/doc/example/mysetup.txt +++ b/doc/example/mysetup.txt @@ -49,7 +49,7 @@ You can now run the test:: $ py.test test_sample.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_sample.py test_sample.py F @@ -57,7 +57,7 @@ You can now run the test:: ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - mysetup = + mysetup = def test_answer(mysetup): app = mysetup.myapp() @@ -122,14 +122,14 @@ Running it yields:: $ py.test test_ssh.py -rs =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_ssh.py test_ssh.py s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-438/conftest.py:22: specify ssh host with --ssh + SKIP [1] /tmp/doc-exec-560/conftest.py:22: specify ssh host with --ssh - ======================== 1 skipped in 0.02 seconds ========================= + ======================== 1 skipped in 0.03 seconds ========================= If you specify a command line option like ``py.test --ssh=python.org`` the test will execute as expected. --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -16,7 +16,7 @@ conftest.py: local per-directory plugins -------------------------------------------------------------- local ``conftest.py`` plugins contain directory-specific hook -implementations. Collection and test running activities will +implementations. Session and test running activities will invoke all hooks defined in "higher up" ``conftest.py`` files. Example: Assume the following layout and content of files:: @@ -268,7 +268,7 @@ you can use the following hook: reporting hooks ------------------------------ -Collection related reporting hooks: +Session related reporting hooks: .. autofunction: pytest_collectstart .. autofunction: pytest_itemcollected --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev22', + version='2.0.0.dev23', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/doc/monkeypatch.txt +++ b/doc/monkeypatch.txt @@ -39,8 +39,8 @@ will be undone. .. background check: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 - test path 1: /tmp/doc-exec-408 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + test path 1: /tmp/doc-exec-528 ============================= in 0.00 seconds ============================= --- a/doc/faq.txt +++ b/doc/faq.txt @@ -12,11 +12,11 @@ On naming, nosetests, licensing and magi Why a ``py.test`` instead of a ``pytest`` command? ++++++++++++++++++++++++++++++++++++++++++++++++++ -Some historic, some practical reasons: ``py.test`` used to be part of +Some historic, some practical reasons: ``py.test`` used to be part of the ``py`` package which provided several developer utitilities, all starting with ``py.``, providing nice TAB-completion. If you install ``pip install pycmd`` you get these tools from a separate -package. These days the command line tool could be ``pytest`` +package. These days the command line tool could be ``pytest`` but then many people have gotten used to the old name and there also is another tool with this same which would lead to some clashes. @@ -37,11 +37,11 @@ What's this "magic" with py.test? Around 2007 (version ``0.8``) some several people claimed that py.test was using too much "magic". It has been refactored a lot. It is today -probably one of the smallest, most universally runnable and most -customizable testing frameworks for Python. It remains true -that ``py.test`` uses metaprogramming techniques, i.e. it views -test code similar to how compilers view programs, using a -somewhat abstract internal model. +probably one of the smallest, most universally runnable and most +customizable testing frameworks for Python. It remains true +that ``py.test`` uses metaprogramming techniques, i.e. it views +test code similar to how compilers view programs, using a +somewhat abstract internal model. It's also true that the no-boilerplate testing is implemented by making use of the Python assert statement through "re-interpretation": @@ -64,7 +64,7 @@ function arguments, parametrized tests a Is using funcarg- versus xUnit setup a style question? +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -For simple applications and for people experienced with nose_ or +For simple applications and for people experienced with nose_ or unittest-style test setup using `xUnit style setup`_ feels natural. For larger test suites, parametrized testing or setup of complex test resources using funcargs_ is recommended. --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -14,7 +14,7 @@ Installation options:: To check your installation has installed the correct version:: $ py.test --version - This is py.test version 2.0.0.dev19, imported from /home/hpk/p/pytest/pytest + This is py.test version 2.0.0.dev22, imported from /home/hpk/p/pytest/pytest If you get an error checkout :ref:`installation issues`. @@ -34,8 +34,8 @@ That's it. You can execute the test func $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 - test path 1: /tmp/doc-exec-404 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + test path 1: /tmp/doc-exec-523 test_sample.py F @@ -121,7 +121,7 @@ run the module by passing its filename:: ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - self = + self = def test_two(self): x = "hello" @@ -129,7 +129,7 @@ run the module by passing its filename:: E assert hasattr('hello', 'check') test_class.py:8: AssertionError - 1 failed, 1 passed in 0.02 seconds + 1 failed, 1 passed in 0.03 seconds The first test passed, the second failed. Again we can easily see the intermediate values used in the assertion, helping us to @@ -157,7 +157,7 @@ before performing the test function call ================================= FAILURES ================================= _____________________________ test_needsfiles ______________________________ - tmpdir = local('/tmp/pytest-240/test_needsfiles0') + tmpdir = local('/tmp/pytest-446/test_needsfiles0') def test_needsfiles(tmpdir): print tmpdir @@ -166,8 +166,8 @@ before performing the test function call test_tmpdir.py:3: AssertionError ----------------------------- Captured stdout ------------------------------ - /tmp/pytest-240/test_needsfiles0 - 1 failed in 0.04 seconds + /tmp/pytest-446/test_needsfiles0 + 1 failed in 0.07 seconds Before the test runs, a unique-per-test-invocation temporary directory was created. More info at :ref:`tmpdir handling`. --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -1,4 +1,4 @@ -""" core implementation of testing process: init, collection, runtest loop. """ +""" core implementation of testing process: init, session, runtest loop. """ import py import pytest @@ -54,7 +54,7 @@ def pytest_configure(config): config.option.maxfail = 1 def pytest_cmdline_main(config): - """ default command line protocol for initialization, collection, + """ default command line protocol for initialization, session, running tests and reporting. """ session = Session(config) session.exitstatus = EXIT_OK @@ -83,20 +83,17 @@ def pytest_cmdline_main(config): return session.exitstatus def pytest_collection(session): - collection = session.collection - assert not hasattr(collection, 'items') - - collection.perform_collect() + session.perform_collect() hook = session.config.hook - items = collection.items - hook.pytest_collection_modifyitems(config=session.config, items=items) - hook.pytest_collection_finish(collection=collection) + hook.pytest_collection_modifyitems(session=session, + config=session.config, items=session.items) + hook.pytest_collection_finish(session=session) return True def pytest_runtestloop(session): if session.config.option.collectonly: return True - for item in session.collection.items: + for item in session.session.items: item.config.hook.pytest_runtest_protocol(item=item) if session.shouldstop: raise session.Interrupted(session.shouldstop) @@ -121,7 +118,7 @@ class Session(object): self.config.pluginmanager.register(self, name="session", prepend=True) self._testsfailed = 0 self.shouldstop = False - self.collection = Collection(config) # XXX move elswehre + self.session = Session(config) # XXX move elswehre def pytest_collectstart(self): if self.shouldstop: @@ -154,7 +151,7 @@ def compatproperty(name): def fget(self): #print "retrieving %r property from %s" %(name, self.fspath) py.log._apiwarn("2.0", "use py.test.collect.%s for " - "Collection classes" % name) + "Session classes" % name) return getattr(pytest.collect, name) return property(fget) @@ -162,7 +159,7 @@ class Node(object): """ base class for all Nodes in the collection tree. Collector subclasses have children, Items are terminal nodes.""" - def __init__(self, name, parent=None, config=None, collection=None): + def __init__(self, name, parent=None, config=None, session=None): #: a unique name with the scope of the parent self.name = name @@ -173,11 +170,11 @@ class Node(object): self.config = config or parent.config #: the collection this node is part of - self.collection = collection or parent.collection + self.session = session or parent.session #: filesystem path where this node was collected from self.fspath = getattr(parent, 'fspath', None) - self.ihook = self.collection.gethookproxy(self.fspath) + self.ihook = self.session.gethookproxy(self.fspath) self.keywords = {self.name: True} Module = compatproperty("Module") @@ -312,16 +309,16 @@ class Collector(Node): excinfo.traceback = ntraceback.filter() class FSCollector(Collector): - def __init__(self, fspath, parent=None, config=None, collection=None): + def __init__(self, fspath, parent=None, config=None, session=None): fspath = py.path.local(fspath) # xxx only for test_resultlog.py? name = parent and fspath.relto(parent.fspath) or fspath.basename - super(FSCollector, self).__init__(name, parent, config, collection) + super(FSCollector, self).__init__(name, parent, config, session) self.fspath = fspath def _makeid(self): - if self == self.collection: + if self == self.session: return "." - relpath = self.collection.fspath.bestrelpath(self.fspath) + relpath = self.session.fspath.bestrelpath(self.fspath) if os.sep != "/": relpath = relpath.replace(os.sep, "/") return relpath @@ -346,13 +343,33 @@ class Item(Node): self._location = location return location -class Collection(FSCollector): +class Session(FSCollector): + class Interrupted(KeyboardInterrupt): + """ signals an interrupted test run. """ + __module__ = 'builtins' # for py3 + def __init__(self, config): - super(Collection, self).__init__(py.path.local(), parent=None, - config=config, collection=self) + super(Session, self).__init__(py.path.local(), parent=None, + config=config, session=self) + self.config.pluginmanager.register(self, name="session", prepend=True) + self._testsfailed = 0 + self.shouldstop = False self.trace = config.trace.root.get("collection") self._norecursepatterns = config.getini("norecursedirs") + def pytest_collectstart(self): + if self.shouldstop: + raise self.Interrupted(self.shouldstop) + + def pytest_runtest_logreport(self, report): + if report.failed and 'xfail' not in getattr(report, 'keywords', []): + self._testsfailed += 1 + maxfail = self.config.getvalue("maxfail") + if maxfail and self._testsfailed >= maxfail: + self.shouldstop = "stopping after %d failures" % ( + self._testsfailed) + pytest_collectreport = pytest_runtest_logreport + def isinitpath(self, path): return path in self._initialpaths @@ -509,3 +526,4 @@ class Collection(FSCollector): yield x node.ihook.pytest_collectreport(report=rep) +Session = Session --- a/doc/usage.txt +++ b/doc/usage.txt @@ -175,5 +175,4 @@ Running it will exit quickly:: $ python myinvoke.py ERROR: hi from our plugin - .. include:: links.inc --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -6,7 +6,7 @@ import re import inspect import time from fnmatch import fnmatch -from pytest.plugin.session import Collection +from pytest.plugin.session import Session from py.builtin import print_ from pytest.main import HookRelay @@ -273,34 +273,34 @@ class TmpTestdir: p.ensure("__init__.py") return p - Collection = Collection + Session = Session def getnode(self, config, arg): - collection = Collection(config) + session = Session(config) assert '::' not in str(arg) p = py.path.local(arg) - x = collection.fspath.bestrelpath(p) - return collection.perform_collect([x], genitems=False)[0] + x = session.fspath.bestrelpath(p) + return session.perform_collect([x], genitems=False)[0] def getpathnode(self, path): config = self.parseconfig(path) - collection = Collection(config) - x = collection.fspath.bestrelpath(path) - return collection.perform_collect([x], genitems=False)[0] + session = Session(config) + x = session.fspath.bestrelpath(path) + return session.perform_collect([x], genitems=False)[0] def genitems(self, colitems): - collection = colitems[0].collection + session = colitems[0].session result = [] for colitem in colitems: - result.extend(collection.genitems(colitem)) + result.extend(session.genitems(colitem)) return result def inline_genitems(self, *args): #config = self.parseconfig(*args) config = self.parseconfigure(*args) rec = self.getreportrecorder(config) - collection = Collection(config) - collection.perform_collect() - return collection.items, rec + session = Session(config) + session.perform_collect() + return session.items, rec def runitem(self, source): # used from runner functional tests --- a/pytest/plugin/terminal.py +++ b/pytest/plugin/terminal.py @@ -376,8 +376,9 @@ class CollectonlyReporter: self._tw.line("INTERNALERROR> " + line) def pytest_collectstart(self, collector): - self.outindent(collector) - self.indent += self.INDENT + if collector.session != collector: + self.outindent(collector) + self.indent += self.INDENT def pytest_itemcollected(self, item): self.outindent(item) --- a/testing/plugin/test_resultlog.py +++ b/testing/plugin/test_resultlog.py @@ -5,10 +5,10 @@ from pytest.plugin.resultlog import gene from pytest.plugin.session import Node, Item, FSCollector def test_generic_path(testdir): - from pytest.plugin.session import Collection + from pytest.plugin.session import Session config = testdir.parseconfig() - collection = Collection(config) - p1 = Node('a', config=config, collection=collection) + session = Session(config) + p1 = Node('a', config=config, session=session) #assert p1.fspath is None p2 = Node('B', parent=p1) p3 = Node('()', parent = p2) @@ -17,7 +17,7 @@ def test_generic_path(testdir): res = generic_path(item) assert res == 'a.B().c' - p0 = FSCollector('proj/test', config=config, collection=collection) + p0 = FSCollector('proj/test', config=config, session=session) p1 = FSCollector('proj/test/a', parent=p0) p2 = Node('B', parent=p1) p3 = Node('()', parent = p2) --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -205,15 +205,15 @@ class TestFunction: def test_function_equality(self, testdir, tmpdir): config = testdir.reparseconfig() - collection = testdir.Collection(config) + session = testdir.Session(config) f1 = py.test.collect.Function(name="name", config=config, - args=(1,), callobj=isinstance, collection=collection) + args=(1,), callobj=isinstance, session=session) f2 = py.test.collect.Function(name="name",config=config, - args=(1,), callobj=py.builtin.callable, collection=collection) + args=(1,), callobj=py.builtin.callable, session=session) assert not f1 == f2 assert f1 != f2 f3 = py.test.collect.Function(name="name", config=config, - args=(1,2), callobj=py.builtin.callable, collection=collection) + args=(1,2), callobj=py.builtin.callable, session=session) assert not f3 == f2 assert f3 != f2 @@ -221,7 +221,7 @@ class TestFunction: assert f3 != f1 f1_b = py.test.collect.Function(name="name", config=config, - args=(1,), callobj=isinstance, collection=collection) + args=(1,), callobj=isinstance, session=session) assert f1 == f1_b assert not f1 != f1_b @@ -235,11 +235,11 @@ class TestFunction: param = 1 funcargs = {} id = "world" - collection = testdir.Collection(config) + session = testdir.Session(config) f5 = py.test.collect.Function(name="name", config=config, - callspec=callspec1, callobj=isinstance, collection=collection) + callspec=callspec1, callobj=isinstance, session=session) f5b = py.test.collect.Function(name="name", config=config, - callspec=callspec2, callobj=isinstance, collection=collection) + callspec=callspec2, callobj=isinstance, session=session) assert f5 != f5b assert not (f5 == f5b) @@ -396,7 +396,7 @@ def test_generate_tests_only_done_in_sub def test_modulecol_roundtrip(testdir): modcol = testdir.getmodulecol("pass", withinit=True) trail = modcol.nodeid - newcol = modcol.collection.perform_collect([trail], genitems=0)[0] + newcol = modcol.session.perform_collect([trail], genitems=0)[0] assert modcol.name == newcol.name --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -47,7 +47,7 @@ def pytest_collect_file(path, parent): ext = path.ext pb = path.purebasename if ext == ".py" and (pb.startswith("test_") or pb.endswith("_test") or - parent.collection.isinitpath(path)): + parent.session.isinitpath(path)): return parent.ihook.pytest_pycollect_makemodule( path=path, parent=parent) @@ -393,9 +393,9 @@ class Function(FunctionMixin, pytest.col """ _genid = None def __init__(self, name, parent=None, args=None, config=None, - callspec=None, callobj=_dummy, keywords=None, collection=None): + callspec=None, callobj=_dummy, keywords=None, session=None): super(Function, self).__init__(name, parent, - config=config, collection=collection) + config=config, session=session) self._args = args if self._isyieldedfunction(): assert not callspec, ( @@ -711,13 +711,13 @@ class FuncargRequest: raise self.LookupError(msg) def showfuncargs(config): - from pytest.plugin.session import Collection - collection = Collection(config) - collection.perform_collect() - if collection.items: - plugins = getplugins(collection.items[0]) + from pytest.plugin.session import Session + session = Session(config) + session.perform_collect() + if session.items: + plugins = getplugins(session.items[0]) else: - plugins = getplugins(collection) + plugins = getplugins(session) curdir = py.path.local() tw = py.io.TerminalWriter() verbose = config.getvalue("verbose") --- a/doc/example/pythoncollection.txt +++ b/doc/example/pythoncollection.txt @@ -31,7 +31,6 @@ finding out what is collected You can always peek at the collection tree without running tests like this:: . $ py.test --collectonly collectonly.py - --- a/doc/example/controlskip.txt +++ b/doc/example/controlskip.txt @@ -36,12 +36,12 @@ and when running it will see a skipped " $ py.test test_module.py -rs # "-rs" means report on the little 's' =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_module.py test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-435/conftest.py:9: need --runslow option to run + SKIP [1] /tmp/doc-exec-557/conftest.py:9: need --runslow option to run =================== 1 passed, 1 skipped in 0.02 seconds ==================== @@ -49,7 +49,7 @@ Or run it including the ``slow`` marked $ py.test test_module.py --runslow =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_module.py test_module.py .. --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -38,7 +38,8 @@ def pytest_unconfigure(config): """ called before test process is exited. """ def pytest_runtestloop(session): - """ called for performing the main runtest loop (after collection. """ + """ called for performing the main runtest loop + (after collection finished). """ pytest_runtestloop.firstresult = True # ------------------------------------------------------------------------- @@ -49,11 +50,11 @@ def pytest_collection(session): """ perform the collection protocol for the given session. """ pytest_collection.firstresult = True -def pytest_collection_modifyitems(config, items): +def pytest_collection_modifyitems(session, config, items): """ called after collection has been performed, may filter or re-order the items in-place.""" -def pytest_collection_finish(collection): +def pytest_collection_finish(session): """ called after collection has been performed and modified. """ def pytest_ignore_collect(path, config): @@ -64,8 +65,7 @@ def pytest_ignore_collect(path, config): pytest_ignore_collect.firstresult = True def pytest_collect_directory(path, parent): - """ return collection Node or None for the given path. Any new node - needs to have the specified ``parent`` as a parent.""" + """ called before traversing a directory for collection files. """ pytest_collect_directory.firstresult = True def pytest_collect_file(path, parent): --- a/doc/assert.txt +++ b/doc/assert.txt @@ -21,7 +21,7 @@ assertion fails you will see the value o $ py.test test_assert1.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_assert1.py test_assert1.py F @@ -35,7 +35,7 @@ assertion fails you will see the value o E + where 3 = f() test_assert1.py:5: AssertionError - ========================= 1 failed in 0.02 seconds ========================= + ========================= 1 failed in 0.05 seconds ========================= Reporting details about the failing assertion is achieved by re-evaluating the assert expression and recording intermediate values. @@ -101,7 +101,7 @@ if you run this module:: $ py.test test_assert2.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_assert2.py test_assert2.py F --- a/doc/example/nonpython.txt +++ b/doc/example/nonpython.txt @@ -27,17 +27,19 @@ now execute the test specification:: nonpython $ py.test test_simple.yml =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 test path 1: test_simple.yml - + test_simple.yml .F - + ================================= FAILURES ================================= ______________________________ usecase: hello ______________________________ usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ==================== 1 failed, 1 passed in 0.42 seconds ==================== + ========================= short test summary info ========================== + FAIL test_simple.yml::hello + ==================== 1 failed, 1 passed in 0.43 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,24 +58,25 @@ reporting in ``verbose`` mode:: nonpython $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev19 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 -- /home/hpk/venv/0/bin/python test path 1: /home/hpk/p/pytest/doc/example/nonpython - + test_simple.yml <- test_simple.yml:1: usecase: ok PASSED test_simple.yml <- test_simple.yml:1: usecase: hello FAILED - + ================================= FAILURES ================================= ______________________________ usecase: hello ______________________________ usecase execution failed spec failed: 'some': 'other' no further details known at this point. + ========================= short test summary info ========================== + FAIL test_simple.yml::hello ==================== 1 failed, 1 passed in 0.07 seconds ==================== While developing your custom test collection and execution it's also interesting to just look at the collection tree:: nonpython $ py.test --collectonly - --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,6 +1,6 @@ import py -from pytest.plugin.session import Collection +from pytest.plugin.session import Session class TestCollector: def test_collect_versus_item(self): @@ -86,7 +86,7 @@ class TestCollector: node = testdir.getpathnode(hello) assert isinstance(node, py.test.collect.File) assert node.name == "hello.xxx" - nodes = node.collection.perform_collect([node.nodeid], genitems=False) + nodes = node.session.perform_collect([node.nodeid], genitems=False) assert len(nodes) == 1 assert isinstance(nodes[0], py.test.collect.File) @@ -292,7 +292,7 @@ class TestCustomConftests: "*test_x*" ]) -class TestCollection: +class TestSession: def test_parsearg(self, testdir): p = testdir.makepyfile("def test_func(): pass") subdir = testdir.mkdir("sub") @@ -302,7 +302,7 @@ class TestCollection: testdir.chdir() subdir.chdir() config = testdir.parseconfig(p.basename) - rcol = Collection(config=config) + rcol = Session(config=config) assert rcol.fspath == subdir parts = rcol._parsearg(p.basename) @@ -318,7 +318,7 @@ class TestCollection: id = "::".join([p.basename, "test_func"]) config = testdir.parseconfig(id) topdir = testdir.tmpdir - rcol = Collection(config) + rcol = Session(config) assert topdir == rcol.fspath rootid = rcol.nodeid #root2 = rcol.perform_collect([rcol.nodeid], genitems=False)[0] @@ -333,7 +333,7 @@ class TestCollection: id = "::".join([p.basename, "test_func"]) config = testdir.parseconfig(id) topdir = testdir.tmpdir - rcol = Collection(config) + rcol = Session(config) assert topdir == rcol.fspath hookrec = testdir.getreportrecorder(config) rcol.perform_collect() @@ -367,7 +367,7 @@ class TestCollection: normid, ]: config = testdir.parseconfig(id) - rcol = Collection(config=config) + rcol = Session(config=config) rcol.perform_collect() items = rcol.items assert len(items) == 1 @@ -392,7 +392,7 @@ class TestCollection: id = p.basename config = testdir.parseconfig(id) - rcol = Collection(config) + rcol = Session(config) hookrec = testdir.getreportrecorder(config) rcol.perform_collect() items = rcol.items @@ -400,7 +400,7 @@ class TestCollection: assert len(items) == 2 hookrec.hookrecorder.contains([ ("pytest_collectstart", - "collector.fspath == collector.collection.fspath"), + "collector.fspath == collector.session.fspath"), ("pytest_collectstart", "collector.__class__.__name__ == 'SpecialFile'"), ("pytest_collectstart", @@ -417,7 +417,7 @@ class TestCollection: test_aaa = aaa.join("test_aaa.py") p.move(test_aaa) config = testdir.parseconfig() - rcol = Collection(config) + rcol = Session(config) hookrec = testdir.getreportrecorder(config) rcol.perform_collect() items = rcol.items @@ -441,7 +441,7 @@ class TestCollection: id = "." config = testdir.parseconfig(id) - rcol = Collection(config) + rcol = Session(config) hookrec = testdir.getreportrecorder(config) rcol.perform_collect() items = rcol.items @@ -459,12 +459,13 @@ class TestCollection: def test_serialization_byid(self, testdir): p = testdir.makepyfile("def test_func(): pass") config = testdir.parseconfig() - rcol = Collection(config) + rcol = Session(config) rcol.perform_collect() items = rcol.items assert len(items) == 1 item, = items - newcol = Collection(config) + rcol.config.pluginmanager.unregister(name="session") + newcol = Session(config) item2, = newcol.perform_collect([item.nodeid], genitems=False) assert item2.name == item.name assert item2.fspath == item.fspath From commits-noreply at bitbucket.org Sun Nov 7 10:24:51 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 03:24:51 -0600 (CST) Subject: [py-svn] pytest-xdist commit 8cea75c22706: adapt to collection -> session renaming of pytest core Message-ID: <20101107092451.B1DE52419C5@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1289121964 -3600 # Node ID 8cea75c227061419aa3044cad31e756596716bec # Parent 922c00c8c2fa0bebe693cab54301a01c43159e55 adapt to collection -> session renaming of pytest core --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.5a5', + version='1.5a6', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=__doc__, license='GPLv2 or later', --- a/xdist/remote.py +++ b/xdist/remote.py @@ -32,7 +32,6 @@ class SlaveInteractor: def pytest_sessionstart(self, session): self.session = session - self.collection = session.collection slaveinfo = getinfodict() self.sendevent("slaveready", slaveinfo=slaveinfo) @@ -56,20 +55,20 @@ class SlaveInteractor: item = self._id2item[nodeid] self.config.hook.pytest_runtest_protocol(item=item) elif name == "runtests_all": - for item in self.collection.items: + for item in session.items: self.config.hook.pytest_runtest_protocol(item=item) elif name == "shutdown": break return True - def pytest_collection_finish(self, collection): + def pytest_collection_finish(self, session): self._id2item = {} ids = [] - for item in collection.items: + for item in session.items: self._id2item[item.nodeid] = item ids.append(item.nodeid) self.sendevent("collectionfinish", - topdir=str(collection.fspath), + topdir=str(session.fspath), ids=ids) #def pytest_runtest_logstart(self, nodeid, location, fspath): --- a/xdist/looponfail.py +++ b/xdist/looponfail.py @@ -143,30 +143,16 @@ class SlaveFailSession: def pytest_collection(self, session): self.session = session - self.collection = collection = session.collection self.trails = self.current_command - hook = self.collection.ihook + hook = self.session.ihook try: - items = collection.perform_collect(self.trails or None) + items = session.perform_collect(self.trails or None) except pytest.UsageError: - items = collection.perform_collect(None) - hook.pytest_collection_modifyitems(config=session.config, items=items) - hook.pytest_collection_finish(collection=collection) + items = session.perform_collect(None) + hook.pytest_collection_modifyitems(session=session, config=session.config, items=items) + hook.pytest_collection_finish(session=session) return True - if self.trails: - col = self.collection - items = [] - for trail in self.trails: - names = col._parsearg(trail) - try: - for node in col.matchnodes([col._topcollector], names): - items.extend(col.genitems(node)) - except pytest.UsageError: - pass # ignore collect errors / vanished tests - self.collection.items = items - return True - def pytest_runtest_logreport(self, report): if report.failed: self.recorded_failures.append(report) --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5a5' +__version__ = '1.5a6' From commits-noreply at bitbucket.org Sun Nov 7 12:04:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 05:04:00 -0600 (CST) Subject: [py-svn] pytest commit ea0185ce5bed: remove duplicate code, normalize relative path names to fix windows running tests Message-ID: <20101107110400.BCDB91E1394@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289127932 -3600 # Node ID ea0185ce5bed6006371bb2b878203aa075b5d45e # Parent 6a5bbc78bad973e969317db8b86f151e6539c42e remove duplicate code, normalize relative path names to fix windows running tests --- a/pytest/plugin/junitxml.py +++ b/pytest/plugin/junitxml.py @@ -39,7 +39,7 @@ class LogXML(object): def _opentestcase(self, report): names = report.nodeid.split("::") - names[0] = names[0].replace(os.sep, '.') + names[0] = names[0].replace("/", '.') names = tuple(names) d = {'time': self._durations.pop(names, "0")} names = [x.replace(".py", "") for x in names if x != "()"] --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -354,6 +354,7 @@ class Generator(FunctionMixin, PyCollect # invoke setup/teardown on popular request # (induced by the common "test_*" naming shared with normal tests) self.config._setupstate.prepare(self) + l = [] seen = {} for i, x in enumerate(self.obj()): --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -108,34 +108,6 @@ def pytest_ignore_collect(path, config): ignore_paths.extend([py.path.local(x) for x in excludeopt]) return path in ignore_paths -class Session(object): - class Interrupted(KeyboardInterrupt): - """ signals an interrupted test run. """ - __module__ = 'builtins' # for py3 - - def __init__(self, config): - self.config = config - self.config.pluginmanager.register(self, name="session", prepend=True) - self._testsfailed = 0 - self.shouldstop = False - self.session = Session(config) # XXX move elswehre - - def pytest_collectstart(self): - if self.shouldstop: - raise self.Interrupted(self.shouldstop) - - def pytest_runtest_logreport(self, report): - if report.failed and 'xfail' not in getattr(report, 'keywords', []): - self._testsfailed += 1 - maxfail = self.config.getvalue("maxfail") - if maxfail and self._testsfailed >= maxfail: - self.shouldstop = "stopping after %d failures" % ( - self._testsfailed) - pytest_collectreport = pytest_runtest_logreport - -class NoMatch(Exception): - """ raised if matching cannot locate a matching names. """ - class HookProxy: def __init__(self, fspath, config): self.fspath = fspath @@ -311,7 +283,12 @@ class Collector(Node): class FSCollector(Collector): def __init__(self, fspath, parent=None, config=None, session=None): fspath = py.path.local(fspath) # xxx only for test_resultlog.py? - name = parent and fspath.relto(parent.fspath) or fspath.basename + name = fspath.basename + if parent is not None: + rel = fspath.relto(parent.fspath) + if rel: + name = rel + name = name.replace(os.sep, "/") super(FSCollector, self).__init__(name, parent, config, session) self.fspath = fspath @@ -343,6 +320,9 @@ class Item(Node): self._location = location return location +class NoMatch(Exception): + """ raised if matching cannot locate a matching names. """ + class Session(FSCollector): class Interrupted(KeyboardInterrupt): """ signals an interrupted test run. """ @@ -469,7 +449,8 @@ class Session(FSCollector): if self.config.option.pyargs: arg = self._tryconvertpyarg(arg) parts = str(arg).split("::") - path = self.fspath.join(parts[0], abs=True) + relpath = parts[0].replace("/", os.sep) + path = self.fspath.join(relpath, abs=True) if not path.check(): if self.config.option.pyargs: msg = "file or package not found: " From commits-noreply at bitbucket.org Sun Nov 7 16:08:58 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 09:08:58 -0600 (CST) Subject: [py-svn] pytest commit 6996b4fa53a1: refine initilization: read config also from a "pytest.ini" file if exists Message-ID: <20101107150858.BC03E2410C8@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289142622 -3600 # Node ID 6996b4fa53a19dfa17558c38c870e43b485d0df9 # Parent ea0185ce5bed6006371bb2b878203aa075b5d45e refine initilization: read config also from a "pytest.ini" file if exists and revert earlier commandline option and group ordering change. --- a/pytest/plugin/config.py +++ b/pytest/plugin/config.py @@ -21,6 +21,7 @@ class Parser: self._processopt = processopt self._usage = usage self._inidict = {} + self._ininames = [] self.hints = [] def processoption(self, option): @@ -55,12 +56,12 @@ class Parser: def parse(self, args): self.optparser = optparser = MyOptionParser(self) - groups = list(reversed(self._groups)) + [self._anonymous] + groups = self._groups + [self._anonymous] for group in groups: if group.options: desc = group.description or group.name optgroup = py.std.optparse.OptionGroup(optparser, desc) - optgroup.add_options(reversed(group.options)) + optgroup.add_options(group.options) optparser.add_option_group(optgroup) return self.optparser.parse_args([str(x) for x in args]) @@ -74,6 +75,7 @@ class Parser: """ add an ini-file option with the given name and description. """ assert type in (None, "pathlist", "args", "linelist") self._inidict[name] = (help, type, default) + self._ininames.append(name) class OptionGroup: def __init__(self, name, description="", parser=None): @@ -290,7 +292,7 @@ class Config(object): raise def _initini(self, args): - self.inicfg = getcfg(args, ["setup.cfg", "tox.ini",]) + self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"]) self._parser.addini('addopts', 'extra command line options', 'args') self._parser.addini('minversion', 'minimally required pytest version') @@ -303,7 +305,7 @@ class Config(object): self.pluginmanager.consider_env() self.pluginmanager.consider_preparse(args) self._setinitialconftest(args) - self.hook.pytest_addoption(parser=self._parser) + self.pluginmanager.do_addoption(self._parser) def _checkversion(self): minver = self.inicfg.get('minversion', None) @@ -426,13 +428,16 @@ def getcfg(args, inibasenames): args = [x for x in args if str(x)[0] != "-"] if not args: args = [py.path.local()] - for inibasename in inibasenames: - for p in args: - x = findupwards(p, inibasename) - if x is not None: - iniconfig = py.iniconfig.IniConfig(x) - if 'pytest' in iniconfig.sections: - return iniconfig['pytest'] + for arg in args: + arg = py.path.local(arg) + if arg.check(): + for base in arg.parts(reverse=True): + for inibasename in inibasenames: + p = base.join(inibasename) + if p.check(): + iniconfig = py.iniconfig.IniConfig(p) + if 'pytest' in iniconfig.sections: + return iniconfig['pytest'] return {} def findupwards(current, basename): --- a/doc/customize.txt +++ b/doc/customize.txt @@ -4,7 +4,7 @@ basic test configuration Command line options and configuration file settings ----------------------------------------------------------------- -You can get help on options and configuration options by running:: +You can get help on options and ini-config values by running:: py.test -h # prints options _and_ config file settings @@ -14,23 +14,31 @@ which were registered by installed plugi how test configuration is read from setup/tox ini-files -------------------------------------------------------- -py.test looks for the first ``[pytest]`` section in either the first ``setup.cfg`` or the first ``tox.ini`` file found upwards from the arguments. Example:: +py.test searched for the first matching ini-style configuration file +in the directories of command line argument and the directories above. +It looks for filenames in this order:: + + pytest.ini + tox.ini + setup.cfg + +Searching stops when the first ``[pytest]`` section is found. +There is no merging of configuration values from multiple files. Example:: py.test path/to/testdir will look in the following dirs for a config file:: + path/to/testdir/pytest.ini + path/to/testdir/tox.ini path/to/testdir/setup.cfg + path/to/pytest.ini + path/to/tox.ini path/to/setup.cfg - path/setup.cfg - setup.cfg - ... # up until root of filesystem - path/to/testdir/tox.ini - path/to/tox.ini - path/tox.ini ... # up until root of filesystem -If no path was provided at all the current working directory is used for the lookup. +If argument is provided to a py.test run, the current working directory +is used to start the search. builtin configuration file options ---------------------------------------------- --- a/testing/plugin/test_genscript.py +++ b/testing/plugin/test_genscript.py @@ -28,7 +28,6 @@ def test_gen(testdir, anypython, standal result = standalone.run(anypython, testdir, p) assert result.ret != 0 - at py.test.mark.xfail(reason="fix-dist", run=False) def test_rundist(testdir, pytestconfig, standalone): pytestconfig.pluginmanager.skipifmissing("xdist") testdir.makepyfile(""" --- a/testing/test_config.py +++ b/testing/test_config.py @@ -40,6 +40,30 @@ class TestParseIni: "*tox.ini:2*requires*9.0*actual*" ]) + @py.test.mark.multi(name="setup.cfg tox.ini pytest.ini".split()) + def test_ini_names(self, testdir, name): + testdir.tmpdir.join(name).write(py.std.textwrap.dedent(""" + [pytest] + minversion = 1.0 + """)) + config = Config() + config.parse([testdir.tmpdir]) + assert config.getini("minversion") == "1.0" + + def test_toxini_before_lower_pytestini(self, testdir): + sub = testdir.tmpdir.mkdir("sub") + sub.join("tox.ini").write(py.std.textwrap.dedent(""" + [pytest] + minversion = 2.0 + """)) + testdir.tmpdir.join("pytest.ini").write(py.std.textwrap.dedent(""" + [pytest] + minversion = 1.5 + """)) + config = Config() + config.parse([sub]) + assert config.getini("minversion") == "2.0" + @py.test.mark.xfail(reason="probably not needed") def test_confcutdir(self, testdir): sub = testdir.mkdir("sub") --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -418,7 +418,6 @@ class TestTerminalFunctional: "*test_verbose_reporting.py:10: test_gen*FAIL*", ]) assert result.ret == 1 - pytest.xfail("repair xdist") pytestconfig.pluginmanager.skipifmissing("xdist") result = testdir.runpytest(p1, '-v', '-n 1') result.stdout.fnmatch_lines([ --- a/pytest/plugin/helpconfig.py +++ b/pytest/plugin/helpconfig.py @@ -43,7 +43,8 @@ def showhelp(config): tw.line("setup.cfg or tox.ini options to be put into [pytest] section:") tw.line() - for name, (help, type, default) in sorted(config._parser._inidict.items()): + for name in config._parser._ininames: + help, type, default = config._parser._inidict[name] if type is None: type = "string" spec = "%s (%s)" % (name, type) --- a/pytest/main.py +++ b/pytest/main.py @@ -236,6 +236,11 @@ class PluginManager(object): for hint in self._hints: tw.line("hint: %s" % hint) + def do_addoption(self, parser): + mname = "pytest_addoption" + methods = reversed(self.listattr(mname)) + MultiCall(methods, {'parser': parser}).execute() + def do_configure(self, config): assert not hasattr(self, '_config') self._config = config --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] distshare={homedir}/.tox/distshare -envlist=py26,py27,py31,py27-xdist,py25,py24 +envlist=py26,py27,py31,py32,py27-xdist,py25,py24 indexserver= default http://pypi.testrun.org pypi http://pypi.python.org/simple From commits-noreply at bitbucket.org Sun Nov 7 16:25:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 09:25:01 -0600 (CST) Subject: [py-svn] pylib commit cc6128f129a0: bump version Message-ID: <20101107152501.5F54624140D@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289143596 -3600 # Node ID cc6128f129a0d2723c998efc521b2158f91762aa # Parent 5367bea2f47738bdefa9eb55752e5a2a4b0e841b bump version --- a/setup.py +++ b/setup.py @@ -32,7 +32,7 @@ def main(): long_description = long_description, install_requires=['py>=1.3.9', ], # force newer py version which removes 'py' namespace # # so we can occupy it - version='2.0.0.dev5', + version='2.0.0.dev6', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev5' +__version__ = '2.0.0.dev6' from py import _apipkg From commits-noreply at bitbucket.org Sun Nov 7 16:25:09 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 09:25:09 -0600 (CST) Subject: [py-svn] pytest commit 2203d078dd48: bump version Message-ID: <20101107152509.5A5C424140D@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289143604 -3600 # Node ID 2203d078dd485640a5be1d6e8c8782e15e33065f # Parent 6996b4fa53a19dfa17558c38c870e43b485d0df9 bump version --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev23', + version='2.0.0.dev24', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev23' +__version__ = '2.0.0.dev24' __all__ = ['config', 'cmdline'] From commits-noreply at bitbucket.org Sun Nov 7 17:19:54 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 10:19:54 -0600 (CST) Subject: [py-svn] pylib commit 50db633de97b: redo documentation for pylib: convert to sphinx, add an enhance. Message-ID: <20101107161954.B96561E1347@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289146873 -3600 # Node ID 50db633de97b3626c0a07fbe8911856e2c14988b # Parent cc6128f129a0d2723c998efc521b2158f91762aa redo documentation for pylib: convert to sphinx, add an enhance. --- /dev/null +++ b/doc/Makefile @@ -0,0 +1,130 @@ +# Makefile for Sphinx documentation +# + +# You can set these variables from the command line. +SPHINXOPTS = +SPHINXBUILD = sphinx-build +PAPER = +BUILDDIR = _build + +# Internal variables. +PAPEROPT_a4 = -D latex_paper_size=a4 +PAPEROPT_letter = -D latex_paper_size=letter +ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) . + +.PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest + +help: + @echo "Please use \`make ' where is one of" + @echo " html to make standalone HTML files" + @echo " dirhtml to make HTML files named index.html in directories" + @echo " singlehtml to make a single large HTML file" + @echo " pickle to make pickle files" + @echo " json to make JSON files" + @echo " htmlhelp to make HTML files and a HTML help project" + @echo " qthelp to make HTML files and a qthelp project" + @echo " devhelp to make HTML files and a Devhelp project" + @echo " epub to make an epub" + @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter" + @echo " latexpdf to make LaTeX files and run them through pdflatex" + @echo " text to make text files" + @echo " man to make manual pages" + @echo " changes to make an overview of all changed/added/deprecated items" + @echo " linkcheck to check all external links for integrity" + @echo " doctest to run all doctests embedded in the documentation (if enabled)" + +clean: + -rm -rf $(BUILDDIR)/* + +html: + $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/html." + +dirhtml: + $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml + @echo + @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml." + +singlehtml: + $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml + @echo + @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml." + +pickle: + $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle + @echo + @echo "Build finished; now you can process the pickle files." + +json: + $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json + @echo + @echo "Build finished; now you can process the JSON files." + +htmlhelp: + $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp + @echo + @echo "Build finished; now you can run HTML Help Workshop with the" \ + ".hhp project file in $(BUILDDIR)/htmlhelp." + +qthelp: + $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp + @echo + @echo "Build finished; now you can run "qcollectiongenerator" with the" \ + ".qhcp project file in $(BUILDDIR)/qthelp, like this:" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pylib.qhcp" + @echo "To view the help file:" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pylib.qhc" + +devhelp: + $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp + @echo + @echo "Build finished." + @echo "To view the help file:" + @echo "# mkdir -p $$HOME/.local/share/devhelp/pylib" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pylib" + @echo "# devhelp" + +epub: + $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub + @echo + @echo "Build finished. The epub file is in $(BUILDDIR)/epub." + +latex: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo + @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex." + @echo "Run \`make' in that directory to run these through (pdf)latex" \ + "(use \`make latexpdf' here to do that automatically)." + +latexpdf: + $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex + @echo "Running LaTeX files through pdflatex..." + make -C $(BUILDDIR)/latex all-pdf + @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex." + +text: + $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text + @echo + @echo "Build finished. The text files are in $(BUILDDIR)/text." + +man: + $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man + @echo + @echo "Build finished. The manual pages are in $(BUILDDIR)/man." + +changes: + $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes + @echo + @echo "The overview file is in $(BUILDDIR)/changes." + +linkcheck: + $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck + @echo + @echo "Link check complete; look for any errors in the above output " \ + "or in $(BUILDDIR)/linkcheck/output.txt." + +doctest: + $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest + @echo "Testing of doctests in the sources finished, look at the " \ + "results in $(BUILDDIR)/doctest/output.txt." --- a/doc/install.txt +++ b/doc/install.txt @@ -1,128 +1,50 @@ -.. _`index page`: http://pypi.python.org/pypi/py/ +.. _`pylib`: +.. _`index page`: http://pypi.python.org/pypi/pylib/ -py.test/pylib installation info in a nutshell +installation info in a nutshell =================================================== -**Pythons**: 2.4, 2.5, 2.6, 2.7, 3.0, 3.1.x, Jython-2.5.1, PyPy-1.2 +**PyPI name**: pylib_ + +**Pythons**: 2.4, 2.5, 2.6, 2.7, 3.0, 3.1.x, Jython-2.5.1, PyPy-1.3 **Operating systems**: Linux, Windows, OSX, Unix **Requirements**: setuptools_ or Distribute_ -**Installers**: easy_install_ and pip_ or `standalone`_ (new for 1.2) +**Installers**: ``easy_install`` and ``pip`` -**Distribution names**: +**hg repository**: https://bitbucket.org/hpk42/pylib -* PyPI name: ``py`` (see `index page`_ for versions) -* redhat fedora: ``python-py`` -* debian: ``python-codespeak-lib`` -* gentoo: ``pylib`` -**Installed scripts**: see `bin`_ for which and how scripts are installed. - -**hg repository**: https://bitbucket.org/hpk42/py-trunk - -.. _`bin`: bin.html - - -.. _`easy_install`: - -Installation using easy_install -=================================================== +easy install +----------------------------- Both `Distribute`_ and setuptools_ provide the ``easy_install`` installation tool with which you can type into a command line window:: - easy_install -U py + easy_install -U pylib -to install the latest release of the py lib and py.test. The ``-U`` switch +to install the latest release of the py lib. The ``-U`` switch will trigger an upgrade if you already have an older version installed. Note that setuptools works ok with Python2 interpreters while `Distribute`_ additionally works with Python3 and also avoid some issues on Windows. -Known issues: - -- **Windows**: If "easy_install" or "py.test" are not found - please see here for preparing your environment for running - command line tools: `Python for Windows`_. You may alternatively - use an `ActivePython install`_ which makes command line tools - automatically available under Windows. - -.. _`ActivePython install`: http://www.activestate.com/activepython/downloads - -.. _`Jython does not create command line launchers`: http://bugs.jython.org/issue1491 - -- **Jython2.5.1 on Windows XP**: `Jython does not create command line launchers`_ - so ``py.test`` will not work correctly. You may install py.test on - CPython and type ``py.test --genscript=mytest`` and then use - ``jython mytest`` to run py.test for your tests to run in Jython. - -- **On Linux**: If ``easy_install`` fails because it needs to run - as the superuser you are trying to install things globally - and need to put ``sudo`` in front of the command. - - -.. _quickstart: test/quickstart.html - - -Recommendation: install tool and dependencies virtually -=========================================================== - -It is recommended to work with virtual environments -(e.g. virtualenv_ or buildout_ based) and use easy_install_ -(or pip_) for installing py.test/pylib and any dependencies -you need to run your tests. Local virtual Python environments -(as opposed to system-wide "global" environments) make for a more -reproducible and reliable test environment. - -.. _`virtualenv`: http://pypi.python.org/pypi/virtualenv -.. _`buildout`: http://www.buildout.org/ -.. _pip: http://pypi.python.org/pypi/pip - -.. _standalone: - -Generating a py.test standalone Script -============================================ - -If you are a maintainer or application developer and want users -to run tests you can use a facility to generate a standalone -"py.test" script that you can tell users to run:: - - py.test --genscript=mytest - -will generate a ``mytest`` script that is, in fact, a ``py.test`` under -disguise. You can tell people to download and then e.g. run it like this:: - - python mytest --pastebin=all - -and ask them to send you the resulting URL. The resulting script has -all core features and runs unchanged under Python2 and Python3 interpreters. - -.. _`Python for Windows`: http://www.imladris.com/Scripts/PythonForWindows.html - -.. _mercurial: http://mercurial.selenic.com/wiki/ -.. _`Distribute`: -.. _`Distribute for installation`: http://pypi.python.org/pypi/distribute#installation-instructions -.. _`distribute installation`: http://pypi.python.org/pypi/distribute -.. _checkout: -.. _tarball: - Working from version control or a tarball -================================================= +----------------------------------------------- To follow development or start experiments, checkout the complete code and documentation source with mercurial_:: - hg clone https://bitbucket.org/hpk42/py-trunk/ + hg clone https://bitbucket.org/hpk42/pylib Development takes place on the 'trunk' branch. You can also go to the python package index and download and unpack a TAR file:: - http://pypi.python.org/pypi/py/ - + http://pypi.python.org/pypi/pylib/ activating a checkout with setuptools -------------------------------------------- @@ -137,52 +59,26 @@ in order to work inline with the tools a .. _`directly use a checkout`: -directly use a checkout or tarball / avoid setuptools -------------------------------------------------------------- - -Get a checkout_ or tarball_ and add paths to your environment variables: - -* ``PYTHONPATH`` needs to contain the root directory (where ``py`` resides) -* ``PATH`` needs to contain ``ROOT/bin`` or ``ROOT\bin\win32`` respectively. - -There also are helper scripts that set the variables accordingly. On windows -execute:: - - # inside autoexec.bat or shell startup - c:\\path\to\checkout\bin\env.cmd - -on linux/OSX add this to your shell initialization:: - - # inside e.g. .bashrc - eval `python ~/path/to/checkout/bin/env.py` - -both of which which will get you good settings. If you install -the pylib this way you can easily ``hg pull && hg up`` or download -a new tarball_ to follow the development tree. - - -Note that the scripts manually added like this will look for -py libs in the chain of parent directories of the current working dir. -For example, if you have a layout like this:: - - mypkg/ - subpkg1/ - tests/ - tests/ - py/ - -issuing ``py.test subpkg1`` will use the py lib -from that projects root directory. If in doubt over where -the pylib comes from you can always do:: - - py.test --version - -to see where py.test is imported from. - -.. _`command line scripts`: bin.html -.. _contact: contact.html - -.. _`RPM`: http://translate.sourceforge.net/releases/testing/fedora/pylib-0.9.2-1.fc9.noarch.rpm - .. _`setuptools`: http://pypi.python.org/pypi/setuptools + +Contact and Communication points +-------------------------------------- + +- `py-dev developers list`_ and `commit mailing list`_. + +- #pylib on irc.freenode.net IRC channel for random questions. + +- `bitbucket issue tracker`_ use this bitbucket issue tracker to report + bugs or request features. + +.. _`bitbucket issue tracker`: http://bitbucket.org/hpk42/pylib/issues/ + +.. _codespeak: http://codespeak.net/ +.. _`py-dev`: +.. _`development mailing list`: +.. _`py-dev developers list`: http://codespeak.net/mailman/listinfo/py-dev +.. _`py-svn`: +.. _`commit mailing list`: http://codespeak.net/mailman/listinfo/py-svn + +.. include:: links.inc --- a/py/_path/common.py +++ b/py/_path/common.py @@ -36,7 +36,7 @@ class Checkers: return self.path.relto(arg) def fnmatch(self, arg): - return FNMatcher(arg)(self.path) + return self.path.fnmatch(arg) def endswith(self, arg): return str(self.path).endswith(arg) @@ -161,22 +161,45 @@ newline will be removed from the end of return repr(str(self)) def check(self, **kw): - """ check a path for existence, or query its properties + """ check a path for existence and properties. - without arguments, this returns True if the path exists (on the - filesystem), False if not + Without arguments, return True if the path exists, otherwise False. - with (keyword only) arguments, the object compares the value - of the argument with the value of a property with the same name - (if it has one, else it raises a TypeError) + valid checkers:: - when for example the keyword argument 'ext' is '.py', this will - return True if self.ext == '.py', False otherwise + file=1 # is a file + file=0 # is not a file (may not even exist) + dir=1 # is a dir + link=1 # is a link + exists=1 # exists + + You can specify multiple checker definitions, for example:: + + path.check(file=1, link=1) # a link pointing to a file """ if not kw: kw = {'exists' : 1} return self.Checkers(self)._evaluate(kw) + def fnmatch(self, pattern): + """return true if the basename/fullname matches the glob-'pattern'. + + valid pattern characters:: + + * matches everything + ? matches any single character + [seq] matches any character in seq + [!seq] matches any char not in seq + + If the pattern contains a path-separator then the full path + is used for pattern matching and a '*' is prepended to the + pattern. + + if the pattern doesn't contain a path-separator the pattern + is only matched against the basename. + """ + return FNMatcher(pattern)(self) + def relto(self, relpath): """ return a string which is the relative part of the path to the given 'relpath'. @@ -339,20 +362,6 @@ class FNMatcher: def __init__(self, pattern): self.pattern = pattern def __call__(self, path): - """return true if the basename/fullname matches the glob-'pattern'. - - * matches everything - ? matches any single character - [seq] matches any character in seq - [!seq] matches any char not in seq - - if the pattern contains a path-separator then the full path - is used for pattern matching and a '*' is prepended to the - pattern. - - if the pattern doesn't contain a path-separator the pattern - is only matched against the basename. - """ pattern = self.pattern if pattern.find(path.sep) == -1: name = path.basename --- a/doc/io.txt +++ b/doc/io.txt @@ -41,3 +41,19 @@ filedescriptors you may invoke:: >>> out,err = capture.reset() >>> err 'world' + +py.io object reference +============================ + +.. autoclass:: py.io.StdCaptureFD + :members: + :inherited-members: + +.. autoclass:: py.io.StdCapture + :members: + :inherited-members: + +.. autoclass:: py.io.TerminalWriter + :members: + :inherited-members: + --- a/CHANGELOG +++ b/CHANGELOG @@ -5,8 +5,8 @@ Changes between 1.3.4 and 2.0.0dev0 - py.test was moved to a separate "pytest" package. What remains is a stub hook which will proxy ``import py.test`` to ``pytest``. - all command line tools ("py.cleanup/lookup/countloc/..." moved - to "pytools" package) -- removed the old deprecated "py.magic" namespace + to "pycmd" package) +- removed the old and deprecated "py.magic" namespace - use apipkg-1.1 and make py.apipkg.initpkg|ApiModule available - add py.iniconfig module for brain-dead easy ini-config file parsing - introduce py.builtin.any() --- a/doc/code.txt +++ b/doc/code.txt @@ -46,6 +46,11 @@ A quick example:: >>> str(c.source()).split('\n')[0] "def read(self, mode='r'):" +.. autoclass:: py.code.Code + :members: + :inherited-members: + + ``py.code.Source`` --------------------- @@ -67,6 +72,9 @@ Example:: >>> str(sub).strip() # XXX why is the strip() required?!? 'print "foo"' +.. autoclass:: py.code.Source + :members: + ``py.code.Traceback`` ------------------------ @@ -92,6 +100,9 @@ Example:: >>> str(first.statement).strip().startswith('raise ValueError') True +.. autoclass:: py.code.Traceback + :members: + ``py.code.Frame`` -------------------- @@ -111,6 +122,9 @@ Example (using the 'first' TracebackItem >>> [namevalue[0] for namevalue in frame.getargs()] ['cls', 'path'] +.. autoclass:: py.code.Frame + :members: + ``py.code.ExceptionInfo`` ---------------------------- @@ -132,3 +146,6 @@ Example:: >>> excinfo.exconly() "NameError: name 'foobar' is not defined" +.. autoclass:: py.code.ExceptionInfo + :members: + --- /dev/null +++ b/doc/conf.py @@ -0,0 +1,262 @@ +# -*- coding: utf-8 -*- +# +# pylib documentation build configuration file, created by +# sphinx-quickstart on Thu Oct 21 08:30:10 2010. +# +# This file is execfile()d with the current directory set to its containing dir. +# +# Note that not all possible configuration values are present in this +# autogenerated file. +# +# All configuration values have a default; values that are commented out +# serve to show the default. + +import sys, os + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +#sys.path.insert(0, os.path.abspath('.')) + +# -- General configuration ----------------------------------------------------- + +# If your documentation needs a minimal Sphinx version, state it here. +#needs_sphinx = '1.0' + +# Add any Sphinx extension module names here, as strings. They can be extensions +# coming with Sphinx (named 'sphinx.ext.*') or your custom ones. +extensions = ['sphinx.ext.autodoc', 'sphinx.ext.doctest', 'sphinx.ext.intersphinx', 'sphinx.ext.viewcode'] + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# The suffix of source filenames. +source_suffix = '.txt' + +# The encoding of source files. +#source_encoding = 'utf-8-sig' + +# The master toctree document. +master_doc = 'index' + +# General information about the project. +project = u'pylib' +copyright = u'2010, holger krekel et. al.' + +# The version info for the project you're documenting, acts as replacement for +# |version| and |release|, also used in various other places throughout the +# built documents. +# +# The short X.Y version. +version = '2.0' +# The full version, including alpha/beta/rc tags. +release = '2.0.0.dev0' + +# The language for content autogenerated by Sphinx. Refer to documentation +# for a list of supported languages. +#language = None + +# There are two options for replacing |today|: either, you set today to some +# non-false value, then it is used: +#today = '' +# Else, today_fmt is used as the format for a strftime call. +#today_fmt = '%B %d, %Y' + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +exclude_patterns = ['_build'] + +# The reST default role (used for this markup: `text`) to use for all documents. +#default_role = None + +# If true, '()' will be appended to :func: etc. cross-reference text. +#add_function_parentheses = True + +# If true, the current module name will be prepended to all description +# unit titles (such as .. function::). +#add_module_names = True + +# If true, sectionauthor and moduleauthor directives will be shown in the +# output. They are ignored by default. +#show_authors = False + +# The name of the Pygments (syntax highlighting) style to use. +pygments_style = 'sphinx' + +# A list of ignored prefixes for module index sorting. +#modindex_common_prefix = [] + + +# -- Options for HTML output --------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +html_theme = 'default' + +# Theme options are theme-specific and customize the look and feel of a theme +# further. For a list of options available for each theme, see the +# documentation. +#html_theme_options = {} + +# Add any paths that contain custom themes here, relative to this directory. +#html_theme_path = [] + +# The name for this set of Sphinx documents. If None, it defaults to +# " v documentation". +#html_title = None + +# A shorter title for the navigation bar. Default is the same as html_title. +#html_short_title = None + +# The name of an image file (relative to this directory) to place at the top +# of the sidebar. +#html_logo = None + +# The name of an image file (within the static path) to use as favicon of the +# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 +# pixels large. +#html_favicon = None + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] + +# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, +# using the given strftime format. +#html_last_updated_fmt = '%b %d, %Y' + +# If true, SmartyPants will be used to convert quotes and dashes to +# typographically correct entities. +#html_use_smartypants = True + +# Custom sidebar templates, maps document names to template names. +#html_sidebars = {} + +# Additional templates that should be rendered to pages, maps page names to +# template names. +#html_additional_pages = {} + +# If false, no module index is generated. +#html_domain_indices = True + +# If false, no index is generated. +#html_use_index = True + +# If true, the index is split into individual pages for each letter. +#html_split_index = False + +# If true, links to the reST sources are added to the pages. +#html_show_sourcelink = True + +# If true, "Created using Sphinx" is shown in the HTML footer. Default is True. +#html_show_sphinx = True + +# If true, "(C) Copyright ..." is shown in the HTML footer. Default is True. +#html_show_copyright = True + +# If true, an OpenSearch description file will be output, and all pages will +# contain a tag referring to it. The value of this option must be the +# base URL from which the finished HTML is served. +#html_use_opensearch = '' + +# This is the file name suffix for HTML files (e.g. ".xhtml"). +#html_file_suffix = None + +# Output file base name for HTML help builder. +htmlhelp_basename = 'pylibdoc' + + +# -- Options for LaTeX output -------------------------------------------------- + +# The paper size ('letter' or 'a4'). +#latex_paper_size = 'letter' + +# The font size ('10pt', '11pt' or '12pt'). +#latex_font_size = '10pt' + +# Grouping the document tree into LaTeX files. List of tuples +# (source start file, target name, title, author, documentclass [howto/manual]). +latex_documents = [ + ('index', 'pylib.tex', u'pylib Documentation', + u'holger krekel et. al.', 'manual'), +] + +# The name of an image file (relative to this directory) to place at the top of +# the title page. +#latex_logo = None + +# For "manual" documents, if this is true, then toplevel headings are parts, +# not chapters. +#latex_use_parts = False + +# If true, show page references after internal links. +#latex_show_pagerefs = False + +# If true, show URL addresses after external links. +#latex_show_urls = False + +# Additional stuff for the LaTeX preamble. +#latex_preamble = '' + +# Documents to append as an appendix to all manuals. +#latex_appendices = [] + +# If false, no module index is generated. +#latex_domain_indices = True + + +# -- Options for manual page output -------------------------------------------- + +# One entry per manual page. List of tuples +# (source start file, name, description, authors, manual section). +man_pages = [ + ('index', 'pylib', u'pylib Documentation', + [u'holger krekel et. al.'], 1) +] + +autodoc_member_order = "bysource" +autodoc_default_flags = "inherited-members" + +# -- Options for Epub output --------------------------------------------------- + +# Bibliographic Dublin Core info. +epub_title = u'pylib' +epub_author = u'holger krekel et. al.' +epub_publisher = u'holger krekel et. al.' +epub_copyright = u'2010, holger krekel et. al.' + +# The language of the text. It defaults to the language option +# or en if the language is not set. +#epub_language = '' + +# The scheme of the identifier. Typical schemes are ISBN or URL. +#epub_scheme = '' + +# The unique identifier of the text. This can be a ISBN number +# or the project homepage. +#epub_identifier = '' + +# A unique identification for the text. +#epub_uid = '' + +# HTML files that should be inserted before the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_pre_files = [] + +# HTML files shat should be inserted after the pages created by sphinx. +# The format is a list of tuples containing the path and title. +#epub_post_files = [] + +# A list of files that should not be packed into the epub file. +#epub_exclude_files = [] + +# The depth of the table of contents in toc.ncx. +#epub_tocdepth = 3 + +# Allow duplicate toc entries. +#epub_tocdup = True + + +# Example configuration for intersphinx: refer to the Python standard library. +intersphinx_mapping = {'http://docs.python.org/': None} --- a/doc/path.txt +++ b/doc/path.txt @@ -8,13 +8,13 @@ object to fs-like object trees (reading files/directories, examining the types and structure, etc.), and out-of-the-box provides a number of implementations of this API. -Path implementations provided by ``py.path`` +py.test.local - local file system path =============================================== .. _`local`: -``py.path.local`` --------------------- +basic interactive example +------------------------------------- The first and most obvious of the implementations is a wrapper around a local filesystem. It's just a bit nicer in usage than the regular Python APIs, and @@ -40,8 +40,15 @@ a ``py.path.local`` object for us (which >>> foofile.read(1) 'b' +reference documentation +--------------------------------- + +.. autoclass:: py._path.local.LocalPath + :members: + :inherited-members: + ``py.path.svnurl`` and ``py.path.svnwc`` ----------------------------------------------- +================================================== Two other ``py.path`` implementations that the py lib provides wrap the popular `Subversion`_ revision control system: the first (called 'svnurl') @@ -78,8 +85,23 @@ Example usage of ``py.path.svnwc``: .. _`Subversion`: http://subversion.tigris.org/ -Common vs. specific API -======================= +svn path related API reference +----------------------------------------- + +.. autoclass:: py._path.svnwc.SvnWCCommandPath + :members: + :inherited-members: + +.. autoclass:: py._path.svnurl.SvnCommandPath + :members: + :inherited-members: + +.. autoclass:: py._path.svnwc.SvnAuth + :members: + :inherited-members: + +Common vs. specific API, Examples +======================================== All Path objects support a common set of operations, suitable for many use cases and allowing to transparently switch the @@ -93,15 +115,12 @@ children, etc. Only things that are not such as handling metadata (e.g. the Subversion "properties") require using specific APIs. -Examples ---------------------------------- - A quick 'cookbook' of small examples that will be useful 'in real life', which also presents parts of the 'common' API, and shows some non-common methods: Searching `.txt` files -.......................... +-------------------------------- Search for a particular string inside all files with a .txt extension in a specific directory. @@ -123,7 +142,7 @@ specific directory. ['textfile1.txt', 'textfile2.txt', 'textfile2.txt'] Working with Paths -....................... +---------------------------- This example shows the ``py.path`` features to deal with filesystem paths Note that the filesystem is never touched, @@ -160,7 +179,7 @@ one, or a database or object tree, these with their own notion of path seperators and dealing with conversions, etc.). Checking path types -....................... +------------------------------- Now we will show a bit about the powerful 'check()' method on paths, which allows you to check whether a file exists, what type it is, etc.: @@ -186,7 +205,7 @@ allows you to check whether a file exist True Setting svn-properties -....................... +-------------------------------- As an example of 'uncommon' methods, we'll show how to read and write properties in an ``py.path.svnwc`` instance: @@ -206,7 +225,7 @@ properties in an ``py.path.svnwc`` insta 0 SVN authentication -....................... +---------------------------- Some uncommon functionality can also be provided as extensions, such as SVN authentication: @@ -238,36 +257,4 @@ Known problems / limitations there is no attention yet on making unicode paths work or deal with the famous "8.3" filename issues. -Future plans -============ -The Subversion path implementations are based -on the `svn` command line, not on the bindings. -It makes sense now to directly use the bindings. - -Moreover, it would be good, also considering -`execnet`_ distribution of programs, to -be able to manipulate Windows Paths on Linux -and vice versa. So we'd like to consider -refactoring the path implementations -to provide this choice (and getting rid -of platform-dependencies as much as possible). - -There is some experimental small approach -(``py/path/gateway/``) aiming at having -a convenient Remote Path implementation. - -There are various hacks out there to have -Memory-Filesystems and even path objects -being directly mountable under Linux (via `fuse`). -However, the Path object implementations -do not internally have a clean abstraction -of going to the filesystem - so with some -refactoring it should become easier to -have very custom Path objects, still offering -the quite full interface without requiring -to know about all details of the full path -implementation. - -.. _`execnet`: execnet.html - --- /dev/null +++ b/doc/links.inc @@ -0,0 +1,16 @@ + +.. _`skipping plugin`: plugin/skipping.html +.. _`funcargs mechanism`: funcargs.html +.. _`doctest.py`: http://docs.python.org/library/doctest.html +.. _`xUnit style setup`: xunit_setup.html +.. _`pytest_nose`: plugin/nose.html +.. _`reStructured Text`: http://docutils.sourceforge.net +.. _`Python debugger`: http://docs.python.org/lib/module-pdb.html +.. _nose: http://somethingaboutorange.com/mrl/projects/nose/ +.. _pytest: http://pypi.python.org/pypi/pytest +.. _mercurial: http://mercurial.selenic.com/wiki/ +.. _`setuptools`: http://pypi.python.org/pypi/setuptools +.. _`distribute`: http://pypi.python.org/pypi/distribute +.. _`pip`: http://pypi.python.org/pypi/pip +.. _`virtualenv`: http://pypi.python.org/pypi/virtualenv +.. _hudson: http://hudson-ci.org/ --- a/doc/conftest.py +++ /dev/null @@ -1,8 +0,0 @@ -#XXX make work: excludedirs = ['_build'] -import py -pytest_plugins = ['pytest_restdoc'] -collect_ignore = ['test/attic.txt'] - -def pytest_runtest_setup(item): - if item.fspath.ext == ".txt": - py.test.importorskip("pygments") # for raising an error --- a/py/_path/local.py +++ b/py/_path/local.py @@ -203,14 +203,14 @@ class LocalPath(FSBase): def new(self, **kw): """ create a modified version of this path. - the following keyword arguments modify various path parts: + the following keyword arguments modify various path parts:: a:/some/path/to/a/file.ext - || drive - |-------------| dirname - |------| basename - |--| purebasename - |--| ext + xx drive + xxxxxxxxxxxxxxxxx dirname + xxxxxxxx basename + xxxx purebasename + xxx ext """ obj = object.__new__(self.__class__) drive, dirname, basename, purebasename,ext = self._getbyspec( @@ -461,8 +461,8 @@ class LocalPath(FSBase): return self.strpath def pypkgpath(self, pkgname=None): - """ return the path's package path by looking for the given - pkgname. If pkgname is None then look for the last + """ return the Python package path by looking for a + pkgname. If pkgname is None look for the last directory upwards which still contains an __init__.py and whose basename is python-importable. Return None if a pkgpath can not be determined. --- a/doc/index.txt +++ b/doc/index.txt @@ -1,29 +1,37 @@ -py lib: tested useful cross-Python abstractions -=============================================================== +.. pylib documentation master file, created by + sphinx-quickstart on Thu Oct 21 08:30:10 2010. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. -The ``py`` lib has several namespaces: +Welcome to pylib's documentation! +================================= -`py.path`_: use path objects to transparently access local and svn filesystems. +:ref:`2.0.0 release announcement ` and :ref:`CHANGELOG ` -`py.code`_: generate code and use advanced introspection/traceback support. +Contents: -`py.io`_: Helper Classes for Capturing of Input/Output on FD/sys.std* level +.. toctree:: -`py.xml`_ for generating in-memory xml/html object trees + install + path + code + io + log + xml + misc -`py.log`_: an alpha document about the ad-hoc logging facilities + :maxdepth: 2 -`miscellaneous features`_ describes some small but nice py lib features. +.. toctree:: + :hidden: -.. _`PyPI project page`: http://pypi.python.org/pypi/py/ + announce/release-2.0.0 + changelog -For the latest Release, see `PyPI project page`_ +Indices and tables +================== -.. _`py-dev at codespeak net`: http://codespeak.net/mailman/listinfo/py-dev -.. _`py.log`: log.html -.. _`py.io`: io.html -.. _`py.path`: path.html -.. _`py.code`: code.html -.. _`py.xml`: xml.html -.. _`miscellaneous features`: misc.html +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` --- a/testing/path/common.py +++ b/testing/path/common.py @@ -102,7 +102,8 @@ class CommonFSTests(object): def test_fnmatch_file(self, path1): assert path1.join("samplefile").check(fnmatch='s*e') - assert path1.join("samplefile").check(notfnmatch='s*x') + assert path1.join("samplefile").fnmatch('s*e') + assert not path1.join("samplefile").fnmatch('s*x') assert not path1.join("samplefile").check(fnmatch='s*x') #def test_fnmatch_dir(self, path1): --- a/setup.py +++ b/setup.py @@ -9,8 +9,6 @@ pylib: cross-python development utils py.path.local: local path objects py.path.svnwc: local subversion WC paths -py.iniconfig: parse for .ini / .cfg files -py.apipkg: package API export control + lazy import py.io: io-capturing on filedescriptor or sys.* level Platforms: Linux, Win32, OSX --- a/doc/xml.txt +++ b/doc/xml.txt @@ -1,8 +1,7 @@ ==================================================== -py.xml: Lightweight and flexible xml/html generation +py.xml: simple pythonic xml/html file generation ==================================================== - Motivation ========== @@ -11,9 +10,6 @@ xml and html trees. However, many of th steep learning curve and are often hard to debug. Not to speak of the fact that they are frameworks to begin with. -The py lib strives to offer enough functionality to represent -itself and especially its API in html or xml. - .. _xist: http://www.livinglogic.de/Python/xist/index.html a pythonic object model , please --- a/.hgignore +++ b/.hgignore @@ -15,6 +15,7 @@ syntax:glob *.orig *~ +doc/_build build/ dist/ *.egg-info --- a/doc/confrest.py +++ /dev/null @@ -1,289 +0,0 @@ -import py - -from pytest.plugin.restdoc import convert_rest_html, strip_html_header - -html = py.xml.html - -class css: - #pagetitle = "pagetitle" - contentspace = "contentspace" - menubar = "menubar" - navspace = "navspace" - versioninfo = "versioninfo" - -class Page(object): - doctype = ('\n') - googlefragment = """ - - -""" - - def __init__(self, project, title, targetpath, stylesheeturl=None, - type="text/html", encoding="ISO-8859-1"): - self.project = project - self.title = project.prefix_title + title - self.targetpath = targetpath - self.stylesheeturl = stylesheeturl - self.type = type - self.encoding = encoding - - self.body = html.body() - self.head = html.head() - self._root = html.html(self.head, self.body) - self.fill() - - def a_href(self, name, url, **kwargs): - return html.a(name, class_="menu", href=url, **kwargs) - - def a_docref(self, name, relhtmlpath): - docpath = self.project.docpath - return html.div(html.a(name, class_="menu", - href=relpath(self.targetpath.strpath, - docpath.join(relhtmlpath).strpath))) - - def a_apigenref(self, name, relhtmlpath): - apipath = self.project.apigenpath - return html.a(name, class_="menu", - href=relpath(self.targetpath.strpath, - apipath.join(relhtmlpath).strpath)) - - def fill_menubar(self): - items = [ - self.a_docref("INSTALL", "install.html"), - self.a_docref("CONTACT", "contact.html"), - self.a_docref("CHANGELOG", "changelog.html"), - self.a_docref("FAQ", "faq.html"), - html.div( - html.h3("py.test:"), - self.a_docref("Index", "test/index.html"), - self.a_docref("Quickstart", "test/quickstart.html"), - self.a_docref("Features", "test/features.html"), - self.a_docref("Funcargs", "test/funcargs.html"), - self.a_docref("Plugins", "test/plugin/index.html"), - self.a_docref("Customize", "test/customize.html"), - self.a_docref("Tutorials", "test/talks.html"), - self.a_href("hudson-tests", "http://hudson.testrun.org") - ), - html.div( - html.h3("supporting APIs:"), - self.a_docref("Index", "index.html"), - self.a_docref("py.path", "path.html"), - self.a_docref("py.code", "code.html"), - ) - #self.a_docref("py.code", "code.html"), - #self.a_apigenref("api", "api/index.html"), - #self.a_apigenref("source", "source/index.html"), - #self.a_href("source", "http://bitbucket.org/hpk42/py-trunk/src/"), - ] - self.menubar = html.div(id=css.menubar, *[ - html.div(item) for item in items]) - version = py.version - announcelink = self.a_docref("%s ANN" % version, - "announce/release-%s.html" %(version,)) - self.menubar.insert(0, - html.div(announcelink)) - #self.a_href("%s-%s" % (self.title, py.version), - # "http://pypi.python.org/pypi/py/%s" % version, - #id="versioninfo", - - def fill(self): - content_type = "%s;charset=%s" %(self.type, self.encoding) - self.head.append(html.title(self.title)) - self.head.append(html.meta(name="Content-Type", content=content_type)) - if self.stylesheeturl: - self.head.append( - html.link(href=self.stylesheeturl, - media="screen", rel="stylesheet", - type="text/css")) - self.fill_menubar() - - self.body.append(html.div( - self.project.logo, - self.menubar, - id=css.navspace, - )) - - #self.body.append(html.div(self.title, id=css.pagetitle)) - self.contentspace = html.div(id=css.contentspace) - self.body.append(self.contentspace) - - def unicode(self, doctype=True): - page = self._root.unicode() - page = page.replace("", self.googlefragment + "") - if doctype: - return self.doctype + page - else: - return page - -class PyPage(Page): - def get_menubar(self): - menubar = super(PyPage, self).get_menubar() - # base layout - menubar.append( - html.a("issue", href="https://codespeak.net/issue/py-dev/", - class_="menu"), - ) - return menubar - - -def getrealname(username): - try: - import uconf - except ImportError: - return username - try: - user = uconf.system.User(username) - except KeyboardInterrupt: - raise - try: - return user.realname or username - except KeyError: - return username - - -class Project: - mydir = py.path.local(__file__).dirpath() - title = "py lib" - prefix_title = "" # we have a logo already containing "py lib" - encoding = 'latin1' - logo = html.div( - html.a( - html.img(alt="py lib", id='pyimg', height=114/2, width=154/2, - src="http://codespeak.net/img/pylib.png"), - href="http://pylib.org")) - Page = PyPage - - def __init__(self, sourcepath=None): - if sourcepath is None: - sourcepath = self.mydir - self.setpath(sourcepath) - - def setpath(self, sourcepath, docpath=None, - apigenpath=None, stylesheet=None): - self.sourcepath = sourcepath - if docpath is None: - docpath = sourcepath - self.docpath = docpath - if apigenpath is None: - apigenpath = docpath - self.apigenpath = apigenpath - if stylesheet is None: - p = sourcepath.join("style.css") - if p.check(): - self.stylesheet = p - else: - self.stylesheet = None - else: - p = sourcepath.join(stylesheet) - if p.check(): - stylesheet = p - self.stylesheet = stylesheet - #assert self.stylesheet - self.apigen_relpath = relpath( - self.docpath.strpath + '/', self.apigenpath.strpath + '/') - - def get_content(self, txtpath, encoding): - return unicode(txtpath.read(), encoding) - - def get_htmloutputpath(self, txtpath): - reloutputpath = txtpath.new(ext='.html').relto(self.sourcepath) - return self.docpath.join(reloutputpath) - - def process(self, txtpath): - encoding = self.encoding - content = self.get_content(txtpath, encoding) - outputpath = self.get_htmloutputpath(txtpath) - - stylesheet = self.stylesheet - if isinstance(stylesheet, py.path.local): - if not self.docpath.join(stylesheet.basename).check(): - docpath.ensure(dir=True) - stylesheet.copy(docpath) - stylesheet = relpath(outputpath.strpath, - self.docpath.join(stylesheet.basename).strpath) - - content = convert_rest_html(content, txtpath, - stylesheet=stylesheet, encoding=encoding) - content = strip_html_header(content, encoding=encoding) - - title = txtpath.purebasename - if txtpath.dirpath().basename == "test": - title = "py.test " + title - # title = "[%s] %s" % (txtpath.purebasename, py.version) - page = self.Page(self, title, - outputpath, stylesheeturl=stylesheet) - - try: - modified = py.process.cmdexec( - "hg tip --template 'modified {date|shortdate}'" - ) - except py.process.cmdexec.Error: - modified = " " - - #page.body.append(html.div(modified, id="docinfoline")) - - page.contentspace.append(py.xml.raw(content)) - outputpath.ensure().write(page.unicode().encode(encoding)) - -# XXX this function comes from apigen/linker.py, put it -# somewhere in py lib -import os -def relpath(p1, p2, sep=os.path.sep, back='..', normalize=True): - """ create a relative path from p1 to p2 - - sep is the seperator used for input and (depending - on the setting of 'normalize', see below) output - - back is the string used to indicate the parent directory - - when 'normalize' is True, any backslashes (\) in the path - will be replaced with forward slashes, resulting in a consistent - output on Windows and the rest of the world - - paths to directories must end on a / (URL style) - """ - if normalize: - p1 = p1.replace(sep, '/') - p2 = p2.replace(sep, '/') - sep = '/' - # XXX would be cool to be able to do long filename - # expansion and drive - # letter fixes here, and such... iow: windows sucks :( - if (p1.startswith(sep) ^ p2.startswith(sep)): - raise ValueError("mixed absolute relative path: %r -> %r" %(p1, p2)) - fromlist = p1.split(sep) - tolist = p2.split(sep) - - # AA - # AA BB -> AA/BB - # - # AA BB - # AA CC -> CC - # - # AA BB - # AA -> ../AA - - diffindex = 0 - for x1, x2 in zip(fromlist, tolist): - if x1 != x2: - break - diffindex += 1 - commonindex = diffindex - 1 - - fromlist_diff = fromlist[diffindex:] - tolist_diff = tolist[diffindex:] - - if not fromlist_diff: - return sep.join(tolist[commonindex:]) - backcount = len(fromlist_diff) - if tolist_diff: - return sep.join([back,]*(backcount-1) + tolist_diff) - return sep.join([back,]*(backcount) + tolist[commonindex:]) --- a/doc/changelog.txt +++ b/doc/changelog.txt @@ -1,2 +1,3 @@ +.. _`changelog`: .. include:: ../CHANGELOG --- a/doc/contact.txt +++ /dev/null @@ -1,30 +0,0 @@ -Contact and Communication points -=================================== - -- `py-dev developers list`_ announcements and discussions. - -- #pylib on irc.freenode.net IRC channel for random questions. - -- `tetamap`_: Holger Krekel's blog, main author of pylib code. - -- `commit mailing list`_ or `@pylibcommit`_ to follow development commits, - -- `bitbucket issue tracker`_ use this bitbucket issue tracker to report - bugs or request features. - -.. _`bitbucket issue tracker`: http://bitbucket.org/hpk42/pylib/issues/ - -.. _`get an account`: - -.. _tetamap: http://tetamap.wordpress.com - -.. _`@pylibcommit`: http://twitter.com/pylibcommit - - -.. _codespeak: http://codespeak.net/ -.. _`py-dev`: -.. _`development mailing list`: -.. _`py-dev developers list`: http://codespeak.net/mailman/listinfo/py-dev -.. _`py-svn`: -.. _`commit mailing list`: http://codespeak.net/mailman/listinfo/py-svn - --- /dev/null +++ b/doc/announce/release-2.0.0.txt @@ -0,0 +1,23 @@ + +.. _`release-2.0.0`: + +pylib 2.0.0: cross-platform library for path, code, io, ... manipulations +=========================================================================== + +"pylib" is a library comprising APIs for filesystem and svn path manipulations, +dynamic code construction, IO capturing and a Python2/Python3 compatibility +namespace. It runs unmodified on all Python interpreters compatible to +Python2.4 up until Python 3.2. "pylib" functionality used to be contained in a +PyPI distribution named "py" and then contained "py.test" and other +command line tools. This is now history. "pytest" is its own distribution +and "pylib" can be and is used completely separately from py.test. +The other "py.*" command line tools are installed with the new +separate "pycmd" distribution. + +The general idea for "pylib" is to place high value on providing +some basic APIs that are continously tested against many Python +interpreters and thus also to help transition. + +cheers, +holger + From commits-noreply at bitbucket.org Sun Nov 7 17:33:24 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 10:33:24 -0600 (CST) Subject: [py-svn] pylib commit 64c4901145ff: fix recursion handling (failed on windows) Message-ID: <20101107163324.99D6A6C1325@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289147700 -3600 # Node ID 64c4901145ffb29356f00cbdd4aea35fce9474c2 # Parent 50db633de97b3626c0a07fbe8911856e2c14988b fix recursion handling (failed on windows) --- a/py/_path/common.py +++ b/py/_path/common.py @@ -327,13 +327,13 @@ class Visitor: def __init__(self, fil, rec, ignore, bf, sort): if isinstance(fil, str): fil = FNMatcher(fil) - if rec: - if isinstance(rec, str): - rec = fnmatch(fil) - else: - assert hasattr(rec, '__call__') + if isinstance(rec, str): + self.rec = fnmatch(fil) + elif not hasattr(rec, '__call__') and rec: + self.rec = lambda path: True + else: + self.rec = rec self.fil = fil - self.rec = rec self.ignore = ignore self.breadthfirst = bf self.optsort = sort and sorted or (lambda x: x) From commits-noreply at bitbucket.org Sun Nov 7 17:36:09 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 7 Nov 2010 10:36:09 -0600 (CST) Subject: [py-svn] pytest commit 360498cc71f6: avoid parsing of path objects when pytest.main(path) is called. Message-ID: <20101107163609.993816C131E@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289147860 -3600 # Node ID 360498cc71f600f48b0a23e1b130c7117fd7b861 # Parent 2203d078dd485640a5be1d6e8c8782e15e33065f avoid parsing of path objects when pytest.main(path) is called. --- a/pytest/main.py +++ b/pytest/main.py @@ -410,9 +410,9 @@ _preinit = [PluginManager(load=True)] # def main(args=None, plugins=None): if args is None: args = sys.argv[1:] + elif isinstance(args, py.path.local): + args = [str(args)] elif not isinstance(args, (tuple, list)): - if isinstance(args, py.path.local): - args = str(args) if not isinstance(args, str): raise ValueError("not a string or argument list: %r" % (args,)) args = py.std.shlex.split(args) From commits-noreply at bitbucket.org Mon Nov 8 09:20:47 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 02:20:47 -0600 (CST) Subject: [py-svn] pytest commit 3bfe3a961247: install dependency from pytest distribution, not prior. Message-ID: <20101108082047.2132D24108B@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289204534 -3600 # Node ID 3bfe3a9612475789b02d7ac6496cc3def1319760 # Parent 360498cc71f600f48b0a23e1b130c7117fd7b861 install dependency from pytest distribution, not prior. --- a/tox.ini +++ b/tox.ini @@ -9,7 +9,6 @@ indexserver= changedir=testing commands= py.test -rfsxX --junitxml={envlogdir}/junit-{envname}.xml [] deps= - pylib pypi pexpect pypi nose From commits-noreply at bitbucket.org Mon Nov 8 17:42:48 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 10:42:48 -0600 (CST) Subject: [py-svn] pylib commit 646d50096ca9: adapt to new tox indexserver syntax Message-ID: <20101108164248.DE7FE6C1311@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289234669 -3600 # Node ID 646d50096ca96073a81e02e085f3e5891a73192c # Parent 64c4901145ffb29356f00cbdd4aea35fce9474c2 adapt to new tox indexserver syntax --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,8 @@ [tox] envlist=py26,py27,py31,py27-xdist,py25,py24 -indexserver= - default http://pypi.testrun.org + +[indexserver] +url = http://pypi.testrun.org [testenv] changedir=testing From commits-noreply at bitbucket.org Mon Nov 8 17:43:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 10:43:00 -0600 (CST) Subject: [py-svn] pytest commit 6da16e7e805f: adapt to new tox indexserver syntax Message-ID: <20101108164300.6E2F9240FC9@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289234205 -3600 # Node ID 6da16e7e805f4a339749e969ee537545018cc8ba # Parent 3bfe3a9612475789b02d7ac6496cc3def1319760 adapt to new tox indexserver syntax --- a/tox.ini +++ b/tox.ini @@ -1,16 +1,19 @@ [tox] distshare={homedir}/.tox/distshare envlist=py26,py27,py31,py32,py27-xdist,py25,py24 -indexserver= - default http://pypi.testrun.org - pypi http://pypi.python.org/simple + +[indexserver:default] +url = http://pypi.testrun.org + +[indexserver:pypi] +url = http://pypi.python.org/simple [testenv] changedir=testing commands= py.test -rfsxX --junitxml={envlogdir}/junit-{envname}.xml [] deps= - pypi pexpect - pypi nose + :pypi:pexpect + :pypi:nose [testenv:genscript] changedir=. @@ -27,7 +30,7 @@ commands= [testenv:doc] basepython=python changedir=doc -deps=pypi sphinx +deps=:pypi:sphinx pytest commands= From commits-noreply at bitbucket.org Mon Nov 8 21:11:58 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 14:11:58 -0600 (CST) Subject: [py-svn] pylib commit 4ce4019bb0a9: adapt to simpler indexserver def Message-ID: <20101108201158.D118F6C1311@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289247220 -3600 # Node ID 4ce4019bb0a929796cacb251eb1505001cdaacee # Parent 646d50096ca96073a81e02e085f3e5891a73192c adapt to simpler indexserver def --- a/tox.ini +++ b/tox.ini @@ -1,8 +1,6 @@ [tox] envlist=py26,py27,py31,py27-xdist,py25,py24 - -[indexserver] -url = http://pypi.testrun.org +indexserver= default=http://pypi.testrun.org [testenv] changedir=testing From commits-noreply at bitbucket.org Mon Nov 8 21:11:58 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 14:11:58 -0600 (CST) Subject: [py-svn] pytest commit 0e827d0237a4: adapt to simplified tox indexserver definition Message-ID: <20101108201158.CA0271E107A@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289247204 -3600 # Node ID 0e827d0237a44bce95a59df4dea41f82546bb4b0 # Parent 6da16e7e805f4a339749e969ee537545018cc8ba adapt to simplified tox indexserver definition --- a/tox.ini +++ b/tox.ini @@ -1,12 +1,9 @@ [tox] distshare={homedir}/.tox/distshare envlist=py26,py27,py31,py32,py27-xdist,py25,py24 - -[indexserver:default] -url = http://pypi.testrun.org - -[indexserver:pypi] -url = http://pypi.python.org/simple +indexserver= + default = http://pypi.testrun.org + pypi = http://pypi.python.org/simple [testenv] changedir=testing From commits-noreply at bitbucket.org Tue Nov 9 00:24:35 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 17:24:35 -0600 (CST) Subject: [py-svn] pytest commit 64e7dded2022: add coding for py3 Message-ID: <20101108232435.3F626241238@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1289256495 21600 # Node ID 64e7dded2022a6f8cd9078539ebec395857e266b # Parent 0e827d0237a44bce95a59df4dea41f82546bb4b0 add coding for py3 --- a/testing/plugin/test_junitxml.py +++ b/testing/plugin/test_junitxml.py @@ -222,6 +222,7 @@ class TestPython: def test_unicode(self, testdir): value = 'hx\xc4\x85\xc4\x87\n' testdir.makepyfile(""" + # coding: utf-8 def test_hello(): print (%r) assert 0 From commits-noreply at bitbucket.org Tue Nov 9 00:24:35 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 8 Nov 2010 17:24:35 -0600 (CST) Subject: [py-svn] pytest commit f52a7803cac8: run subprocess py.test scripts with the python version we're testing on Message-ID: <20101108232435.4CA85241420@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1289258702 21600 # Node ID f52a7803cac89ab3cbcceeca38b8f218ad58ffbc # Parent 64e7dded2022a6f8cd9078539ebec395857e266b run subprocess py.test scripts with the python version we're testing on --- a/pytest/plugin/pytester.py +++ b/pytest/plugin/pytester.py @@ -451,7 +451,7 @@ class TmpTestdir: if not self.request.config.getvalue("notoolsonpath"): script = py.path.local.sysfind(scriptname) assert script, "script %r not found" % scriptname - return (script,) + return (py.std.sys.executable, script,) else: py.test.skip("cannot run %r with --no-tools-on-path" % scriptname) From commits-noreply at bitbucket.org Sat Nov 13 09:09:12 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 02:09:12 -0600 (CST) Subject: [py-svn] pylib commit 1371dfb06957: use new apipkg with aliasmodule support Message-ID: <20101113080912.54FEE2419E2@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289635306 -3600 # Node ID 1371dfb06957338aef2066da0af04888f7e2af55 # Parent 4ce4019bb0a929796cacb251eb1505001cdaacee use new apipkg with aliasmodule support --- a/py/_apipkg.py +++ b/py/_apipkg.py @@ -9,11 +9,11 @@ import os import sys from types import ModuleType -__version__ = "1.1" +__version__ = '1.2.dev5' def initpkg(pkgname, exportdefs, attr=dict()): """ initialize given package from the export definitions. """ - oldmod = sys.modules[pkgname] + oldmod = sys.modules.get(pkgname) d = {} f = getattr(oldmod, '__file__', None) if f: @@ -25,10 +25,11 @@ def initpkg(pkgname, exportdefs, attr=di d['__loader__'] = oldmod.__loader__ if hasattr(oldmod, '__path__'): d['__path__'] = [os.path.abspath(p) for p in oldmod.__path__] - if hasattr(oldmod, '__doc__'): + if '__doc__' not in exportdefs and getattr(oldmod, '__doc__', None): d['__doc__'] = oldmod.__doc__ d.update(attr) - oldmod.__dict__.update(d) + if hasattr(oldmod, "__dict__"): + oldmod.__dict__.update(d) mod = ApiModule(pkgname, exportdefs, implprefix=pkgname, attr=d) sys.modules[pkgname] = mod @@ -44,6 +45,16 @@ def importobj(modpath, attrname): return retval class ApiModule(ModuleType): + def __docget(self): + try: + return self.__doc + except AttributeError: + if '__doc__' in self.__map__: + return self.__makeattr('__doc__') + def __docset(self, value): + self.__doc = value + __doc__ = property(__docget, __docset) + def __init__(self, name, importspec, implprefix=None, attr=None): self.__name__ = name self.__all__ = [x for x in importspec if x != '__onfirstaccess__'] @@ -65,8 +76,13 @@ class ApiModule(ModuleType): attrname = parts and parts[0] or "" if modpath[0] == '.': modpath = implprefix + modpath - if name == '__doc__': - self.__doc__ = importobj(modpath, attrname) + + if not attrname: + subname = '%s.%s'%(self.__name__, name) + apimod = AliasModule(subname, modpath) + sys.modules[subname] = apimod + if '.' not in name: + setattr(self, name, apimod) else: self.__map__[name] = (modpath, attrname) @@ -118,3 +134,28 @@ class ApiModule(ModuleType): pass return dict __dict__ = property(__dict__) + + +def AliasModule(modname, modpath): + mod = [] + + def getmod(): + if not mod: + mod.append(importobj(modpath, None)) + return mod[0] + + class AliasModule(ModuleType): + + def __repr__(self): + return '' % (modname, modpath) + + def __getattribute__(self, name): + return getattr(getmod(), name) + + def __setattr__(self, name, value): + setattr(getmod(), name, value) + + def __delattr__(self, name): + delattr(getmod(), name) + + return AliasModule(modname) --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def main(): long_description = long_description, install_requires=['py>=1.3.9', ], # force newer py version which removes 'py' namespace # # so we can occupy it - version='2.0.0.dev6', + version='2.0.0.dev7', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/test_apipkg.py +++ b/testing/test_apipkg.py @@ -1,7 +1,7 @@ import types import sys import py -from py import apipkg +import py._apipkg as apipkg import subprocess # # test support for importing modules @@ -16,7 +16,7 @@ class TestRealModule: tfile = pkgdir.join('__init__.py') tfile.write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, { 'x': { 'module': { @@ -89,7 +89,7 @@ class TestScenarios: def test_relative_import(self, monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("mymodule") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, exportdefs={ '__doc__': '.submod:maindoc', 'x': '.submod:x', @@ -109,32 +109,45 @@ class TestScenarios: def test_recursive_import(self, monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("recmodule") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, exportdefs={ 'some': '.submod:someclass', }) """)) pkgdir.join('submod.py').write(py.code.Source(""" - import recmodule + import recmodule class someclass: pass print (recmodule.__dict__) """)) monkeypatch.syspath_prepend(tmpdir) - import recmodule + import recmodule assert isinstance(recmodule, apipkg.ApiModule) assert recmodule.some.__name__ == "someclass" def test_module_alias_import(self, monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("aliasimport") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, exportdefs={ 'some': 'os.path', }) """)) monkeypatch.syspath_prepend(tmpdir) import aliasimport - assert aliasimport.some is py.std.os.path + for k, v in py.std.os.path.__dict__.items(): + assert getattr(aliasimport.some, k) == v + + def test_from_module_alias_import(self, monkeypatch, tmpdir): + pkgdir = tmpdir.mkdir("fromaliasimport") + pkgdir.join('__init__.py').write(py.code.Source(""" + import py._apipkg as apipkg + apipkg.initpkg(__name__, exportdefs={ + 'some': 'os.path', + }) + """)) + monkeypatch.syspath_prepend(tmpdir) + from fromaliasimport.some import join + assert join is py.std.os.path.join def xtest_nested_absolute_imports(): import email @@ -211,14 +224,22 @@ def test_initpkg_transfers_attrs(monkeyp assert newmod.__loader__ == mod.__loader__ assert newmod.__doc__ == mod.__doc__ -def test_initpkg_not_overwrite_exportdefs(monkeypatch): +def test_initpkg_nodoc(monkeypatch): mod = type(sys)('hello') - mod.__doc__ = "this is the documentation" + mod.__file__ = "hello.py" monkeypatch.setitem(sys.modules, 'hello', mod) + apipkg.initpkg('hello', {}) + newmod = sys.modules['hello'] + assert not newmod.__doc__ + +def test_initpkg_overwrite_doc(monkeypatch): + hello = type(sys)('hello') + hello.__doc__ = "this is the documentation" + monkeypatch.setitem(sys.modules, 'hello', hello) apipkg.initpkg('hello', {"__doc__": "sys:__doc__"}) - newmod = sys.modules['hello'] - assert newmod != mod - assert newmod.__doc__ == sys.__doc__ + newhello = sys.modules['hello'] + assert newhello != hello + assert newhello.__doc__ == sys.__doc__ def test_initpkg_not_transfers_not_existing_attrs(monkeypatch): mod = type(sys)('hello') @@ -249,7 +270,7 @@ def test_name_attribute(): def test_error_loading_one_element(monkeypatch, tmpdir): pkgdir = tmpdir.mkdir("errorloading1") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, exportdefs={ 'x': '.notexists:x', 'y': '.submod:y' @@ -267,7 +288,7 @@ def test_error_loading_one_element(monke def test_onfirstaccess(tmpdir, monkeypatch): pkgdir = tmpdir.mkdir("firstaccess") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, exportdefs={ '__onfirstaccess__': '.submod:init', 'l': '.submod:l', @@ -276,7 +297,7 @@ def test_onfirstaccess(tmpdir, monkeypat """)) pkgdir.join('submod.py').write(py.code.Source(""" l = [] - def init(): + def init(): l.append(1) """)) monkeypatch.syspath_prepend(tmpdir) @@ -288,19 +309,19 @@ def test_onfirstaccess(tmpdir, monkeypat @py.test.mark.multi(mode=['attr', 'dict', 'onfirst']) def test_onfirstaccess_setsnewattr(tmpdir, monkeypatch, mode): - pkgname = 'mode_' + mode + pkgname = tmpdir.basename.replace("-", "") pkgdir = tmpdir.mkdir(pkgname) pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, exportdefs={ '__onfirstaccess__': '.submod:init', }, ) """)) pkgdir.join('submod.py').write(py.code.Source(""" - def init(): + def init(): import %s as pkg - pkg.newattr = 42 + pkg.newattr = 42 """ % pkgname)) monkeypatch.syspath_prepend(tmpdir) mod = __import__(pkgname) @@ -329,12 +350,11 @@ def test_bpython_getattr_override(tmpdir def test_chdir_with_relative_imports_shouldnt_break_lazy_loading(tmpdir): - from py import _apipkg # cause py.apipkg is a apimodule - tmpdir.join('apipkg.py').write(py.code.Source(_apipkg)) + tmpdir.join('apipkg.py').write(py.code.Source(apipkg)) pkg = tmpdir.mkdir('pkg') messy = tmpdir.mkdir('messy') pkg.join('__init__.py').write(py.code.Source(""" - import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, { 'test': '.sub:test', }) @@ -365,7 +385,7 @@ def test_chdir_with_relative_imports_sho def test_dotted_name_lookup(tmpdir, monkeypatch): pkgdir = tmpdir.mkdir("dotted_name_lookup") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, dict(abs='os:path.abspath')) """)) monkeypatch.syspath_prepend(tmpdir) @@ -375,9 +395,76 @@ def test_dotted_name_lookup(tmpdir, monk def test_extra_attributes(tmpdir, monkeypatch): pkgdir = tmpdir.mkdir("extra_attributes") pkgdir.join('__init__.py').write(py.code.Source(""" - from py import apipkg + import py._apipkg as apipkg apipkg.initpkg(__name__, dict(abs='os:path.abspath'), dict(foo='bar')) """)) monkeypatch.syspath_prepend(tmpdir) import extra_attributes assert extra_attributes.foo == 'bar' + +def test_aliasmodule_repr(): + am = apipkg.AliasModule("mymod", "sys") + r = repr(am) + assert "" == r + am.version + assert repr(am) == r + +def test_aliasmodule_proxy_methods(tmpdir, monkeypatch): + pkgdir = tmpdir + pkgdir.join('aliasmodule_proxy.py').write(py.code.Source(""" + def doit(): + return 42 + """)) + + pkgdir.join('my_aliasmodule_proxy.py').write(py.code.Source(""" + import py._apipkg as apipkg + apipkg.initpkg(__name__, dict(proxy='aliasmodule_proxy')) + + def doit(): + return 42 + """)) + + monkeypatch.syspath_prepend(tmpdir) + import aliasmodule_proxy as orig + from my_aliasmodule_proxy import proxy + + doit = proxy.doit + assert doit is orig.doit + + del proxy.doit + py.test.raises(AttributeError, "orig.doit") + + proxy.doit = doit + assert orig.doit is doit + +def test_aliasmodule_nested_import_with_from(tmpdir, monkeypatch): + import os + pkgdir = tmpdir.mkdir("api1") + pkgdir.ensure("__init__.py").write(py.std.textwrap.dedent(""" + import py._apipkg as apipkg + apipkg.initpkg(__name__, { + 'os2': 'api2', + 'os2.path': 'api2.path2', + }) + """)) + tmpdir.join("api2.py").write(py.std.textwrap.dedent(""" + import os, sys + from os import path + sys.modules['api2.path2'] = path + x = 3 + """)) + monkeypatch.syspath_prepend(tmpdir) + from api1 import os2 + from api1.os2.path import abspath + assert abspath == os.path.abspath + # check that api1.os2 mirrors os.* + assert os2.x == 3 + import api1 + assert 'os2.path' not in api1.__dict__ + + +def test_initpkg_without_old_module(): + apipkg.initpkg("initpkg_without_old_module", + dict(modules="sys:modules")) + from initpkg_without_old_module import modules + assert modules is sys.modules --- a/py/__init__.py +++ b/py/__init__.py @@ -8,40 +8,44 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev6' +__version__ = '2.0.0.dev7' from py import _apipkg -_apipkg.initpkg(__name__, attr={'_apipkg': _apipkg}, exportdefs=dict( +_apipkg.initpkg(__name__, attr={'_apipkg': _apipkg}, exportdefs={ # access to all standard lib modules - std = '._std:std', + 'std': '._std:std', # access to all posix errno's as classes - error = '._error:error', + 'error': '._error:error', - _pydir = '.__metainfo:pydir', - version = 'py:__version__', # backward compatibility + '_pydir' : '.__metainfo:pydir', + 'version': 'py:__version__', # backward compatibility - test = 'pytest', # defer to pytest package + # pytest-2.0 has a flat namespace, we use alias modules + # to keep old references compatible + 'test' : 'pytest', + 'test.collect' : 'pytest', + 'test.cmdline' : 'pytest', # hook into the top-level standard library - process = { + 'process' : { '__doc__' : '._process:__doc__', 'cmdexec' : '._process.cmdexec:cmdexec', 'kill' : '._process.killproc:kill', 'ForkedFunc' : '._process.forkedfunc:ForkedFunc', }, - apipkg = { + 'apipkg' : { 'initpkg' : '._apipkg:initpkg', 'ApiModule' : '._apipkg:ApiModule', }, - iniconfig = { + 'iniconfig' : { 'IniConfig' : '._iniconfig:IniConfig', 'ParseError' : '._iniconfig:ParseError', }, - path = { + 'path' : { '__doc__' : '._path:__doc__', 'svnwc' : '._path.svnwc:SvnWCCommandPath', 'svnurl' : '._path.svnurl:SvnCommandPath', @@ -50,7 +54,7 @@ _apipkg.initpkg(__name__, attr={'_apipkg }, # python inspection/code-generation API - code = { + 'code' : { '__doc__' : '._code:__doc__', 'compile' : '._code.source:compile_', 'Source' : '._code.source:Source', @@ -69,7 +73,7 @@ _apipkg.initpkg(__name__, attr={'_apipkg }, # backports and additions of builtins - builtin = { + 'builtin' : { '__doc__' : '._builtin:__doc__', 'enumerate' : '._builtin:enumerate', 'reversed' : '._builtin:reversed', @@ -98,7 +102,7 @@ _apipkg.initpkg(__name__, attr={'_apipkg }, # input-output helping - io = { + 'io' : { '__doc__' : '._io:__doc__', 'dupfile' : '._io.capture:dupfile', 'TextIO' : '._io.capture:TextIO', @@ -113,7 +117,7 @@ _apipkg.initpkg(__name__, attr={'_apipkg }, # small and mean xml/html generation - xml = { + 'xml' : { '__doc__' : '._xmlgen:__doc__', 'html' : '._xmlgen:html', 'Tag' : '._xmlgen:Tag', @@ -122,7 +126,7 @@ _apipkg.initpkg(__name__, attr={'_apipkg 'escape' : '._xmlgen:escape', }, - log = { + 'log' : { # logging API ('producers' and 'consumers' connected via keywords) '__doc__' : '._log:__doc__', '_apiwarn' : '._log.warning:_apiwarn', @@ -137,11 +141,11 @@ _apipkg.initpkg(__name__, attr={'_apipkg }, # compatibility modules (deprecated) - compat = { + 'compat' : { '__doc__' : '._compat:__doc__', 'doctest' : '._compat.dep_doctest:doctest', 'optparse' : '._compat.dep_optparse:optparse', 'textwrap' : '._compat.dep_textwrap:textwrap', 'subprocess' : '._compat.dep_subprocess:subprocess', }, -)) +}) From commits-noreply at bitbucket.org Sat Nov 13 09:13:41 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 02:13:41 -0600 (CST) Subject: [py-svn] pytest commit d3c75ce89f7d: internally use pytest.* instead of ``py.test.*`` in many places. Message-ID: <20101113081341.514156C112C@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289635511 -3600 # Node ID d3c75ce89f7d8c69468c3f96a3bd1473600895f6 # Parent f52a7803cac89ab3cbcceeca38b8f218ad58ffbc internally use pytest.* instead of ``py.test.*`` in many places. make sub namespace names 'collect' and 'cmdline' available on pytest directly --- a/pytest/__init__.py +++ b/pytest/__init__.py @@ -5,9 +5,9 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev24' +__version__ = '2.0.0.dev25' -__all__ = ['config', 'cmdline'] +__all__ = ['cmdline', 'collect', 'main'] from pytest import main as cmdline UsageError = cmdline.UsageError --- a/pytest/hookspec.py +++ b/pytest/hookspec.py @@ -140,7 +140,7 @@ def pytest_runtest_teardown(item): def pytest_runtest_makereport(item, call): """ return a :py:class:`pytest.plugin.runner.TestReport` object - for the given :py:class:`pytest.collect.Item` and + for the given :py:class:`pytest.Item` and :py:class:`pytest.plugin.runner.CallInfo`. """ pytest_runtest_makereport.firstresult = True --- a/doc/example/nonpython/conftest.py +++ b/doc/example/nonpython/conftest.py @@ -6,14 +6,14 @@ def pytest_collect_file(path, parent): if path.ext == ".yml" and path.basename.startswith("test"): return YamlFile(path, parent) -class YamlFile(py.test.collect.File): +class YamlFile(pytest.File): def collect(self): import yaml # we need a yaml parser, e.g. PyYAML raw = yaml.load(self.fspath.open()) for name, spec in raw.items(): yield YamlItem(name, self, spec) -class YamlItem(py.test.collect.Item): +class YamlItem(pytest.Item): def __init__(self, name, parent, spec): super(YamlItem, self).__init__(name, parent) self.spec = spec --- a/pytest/plugin/doctest.py +++ b/pytest/plugin/doctest.py @@ -1,6 +1,6 @@ """ discover and run doctests in modules and test files.""" -import py +import pytest, py from py._code.code import TerminalRepr, ReprFileLocation def pytest_addoption(parser): @@ -31,7 +31,7 @@ class ReprFailDoctest(TerminalRepr): tw.line(line) self.reprlocation.toterminal(tw) -class DoctestItem(py.test.collect.Item): +class DoctestItem(pytest.Item): def __init__(self, path, parent): name = self.__class__.__name__ + ":" + path.basename super(DoctestItem, self).__init__(name=name, parent=parent) --- a/testing/plugin/test_runner.py +++ b/testing/plugin/test_runner.py @@ -141,8 +141,8 @@ class BaseFunctionalTests: def test_custom_failure_repr(self, testdir): testdir.makepyfile(conftest=""" - import py - class Function(py.test.collect.Function): + import pytest + class Function(pytest.Function): def repr_failure(self, excinfo): return "hello" """) @@ -162,13 +162,12 @@ class BaseFunctionalTests: def test_failure_in_setup_function_ignores_custom_repr(self, testdir): testdir.makepyfile(conftest=""" - import py - class Function(py.test.collect.Function): + import pytest + class Function(pytest.Function): def repr_failure(self, excinfo): assert 0 """) reports = testdir.runitem(""" - import py def setup_function(func): raise ValueError(42) def test_func(): @@ -200,9 +199,9 @@ class BaseFunctionalTests: def test_exit_propagates(self, testdir): try: testdir.runitem(""" - import py + import pytest def test_func(): - raise py.test.exit.Exception() + raise pytest.exit.Exception() """) except py.test.exit.Exception: pass @@ -267,8 +266,8 @@ class TestSessionReports: def test_skip_at_module_scope(self, testdir): col = testdir.getmodulecol(""" - import py - py.test.skip("hello") + import pytest + pytest.skip("hello") def test_func(): pass """) --- a/pytest/plugin/mark.py +++ b/pytest/plugin/mark.py @@ -1,5 +1,5 @@ """ generic mechanism for marking and selecting python functions. """ -import py +import pytest, py def pytest_namespace(): return {'mark': MarkGenerator()} @@ -156,13 +156,13 @@ class MarkInfo: self._name, self.args, self.kwargs) def pytest_itemcollected(item): - if not isinstance(item, py.test.collect.Function): + if not isinstance(item, pytest.Function): return try: func = item.obj.__func__ except AttributeError: func = getattr(item.obj, 'im_func', item.obj) - pyclasses = (py.test.collect.Class, py.test.collect.Module) + pyclasses = (pytest.Class, pytest.Module) for node in item.listchain(): if isinstance(node, pyclasses): marker = getattr(node.obj, 'pytestmark', None) --- a/testing/plugin/test_junitxml.py +++ b/testing/plugin/test_junitxml.py @@ -237,11 +237,11 @@ class TestPython: class TestNonPython: def test_summing_simple(self, testdir): testdir.makeconftest(""" - import py + import pytest def pytest_collect_file(path, parent): if path.ext == ".xyz": return MyItem(path, parent) - class MyItem(py.test.collect.Item): + class MyItem(pytest.Item): def __init__(self, path, parent): super(MyItem, self).__init__(path.basename, parent) self.fspath = path --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -234,7 +234,7 @@ initialisation, command line and configu generic "runtest" hooks ------------------------------ -All all runtest related hooks receive a :py:class:`pytest.collect.Item` object. +All all runtest related hooks receive a :py:class:`pytest.Item` object. .. autofunction:: pytest_runtest_protocol .. autofunction:: pytest_runtest_setup --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -52,7 +52,7 @@ class TestConftestValueAccessGlobal: def test_default_has_lower_prio(self, basedir): conftest = ConftestWithSetinitial(basedir.join("adir")) assert conftest.rget('Directory') == 3 - #assert conftest.lget('Directory') == py.test.collect.Directory + #assert conftest.lget('Directory') == pytest.Directory def test_value_access_not_existing(self, basedir): conftest = ConftestWithSetinitial(basedir) --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev24', + version='2.0.0.dev25', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/test_main.py +++ b/testing/test_main.py @@ -352,8 +352,8 @@ class TestPytestPluginInteractions: def test_namespace_early_from_import(self, testdir): p = testdir.makepyfile(""" - from py.test.collect import Item - from pytest.collect import Item as Item2 + from pytest import Item + from pytest import Item as Item2 assert Item is Item2 """) result = testdir.runpython(p) --- a/pytest/plugin/nose.py +++ b/pytest/plugin/nose.py @@ -1,6 +1,6 @@ """run test suites written for nose. """ -import py +import pytest, py import inspect import sys @@ -14,12 +14,12 @@ def pytest_runtest_makereport(__multical def pytest_runtest_setup(item): - if isinstance(item, (py.test.collect.Function)): - if isinstance(item.parent, py.test.collect.Generator): + if isinstance(item, (pytest.Function)): + if isinstance(item.parent, pytest.Generator): gen = item.parent if not hasattr(gen, '_nosegensetup'): call_optional(gen.obj, 'setup') - if isinstance(gen.parent, py.test.collect.Instance): + if isinstance(gen.parent, pytest.Instance): call_optional(gen.parent.obj, 'setup') gen._nosegensetup = True if not call_optional(item.obj, 'setup'): @@ -27,7 +27,7 @@ def pytest_runtest_setup(item): call_optional(item.parent.obj, 'setup') def pytest_runtest_teardown(item): - if isinstance(item, py.test.collect.Function): + if isinstance(item, pytest.Function): if not call_optional(item.obj, 'teardown'): call_optional(item.parent.obj, 'teardown') #if hasattr(item.parent, '_nosegensetup'): @@ -35,7 +35,7 @@ def pytest_runtest_teardown(item): # del item.parent._nosegensetup def pytest_make_collect_report(collector): - if isinstance(collector, py.test.collect.Generator): + if isinstance(collector, pytest.Generator): call_optional(collector.obj, 'setup') def call_optional(obj, name): --- a/pytest/plugin/session.py +++ b/pytest/plugin/session.py @@ -122,9 +122,9 @@ class HookProxy: def compatproperty(name): def fget(self): #print "retrieving %r property from %s" %(name, self.fspath) - py.log._apiwarn("2.0", "use py.test.collect.%s for " - "Session classes" % name) - return getattr(pytest.collect, name) + py.log._apiwarn("2.0", "use pytest.%s for " + "test collection and item classes" % name) + return getattr(pytest, name) return property(fget) class Node(object): @@ -479,10 +479,10 @@ class Session(FSCollector): nextnames = names[1:] resultnodes = [] for node in matching: - if isinstance(node, pytest.collect.Item): + if isinstance(node, pytest.Item): resultnodes.append(node) continue - assert isinstance(node, pytest.collect.Collector) + assert isinstance(node, pytest.Collector) node.ihook.pytest_collectstart(collector=node) rep = node.ihook.pytest_make_collect_report(collector=node) if rep.passed: @@ -494,11 +494,11 @@ class Session(FSCollector): def genitems(self, node): self.trace("genitems", node) - if isinstance(node, pytest.collect.Item): + if isinstance(node, pytest.Item): node.ihook.pytest_itemcollected(item=node) yield node else: - assert isinstance(node, pytest.collect.Collector) + assert isinstance(node, pytest.Collector) node.ihook.pytest_collectstart(collector=node) rep = node.ihook.pytest_make_collect_report(collector=node) if rep.passed: --- a/pytest/plugin/unittest.py +++ b/pytest/plugin/unittest.py @@ -1,5 +1,5 @@ """ discovery and running of std-library "unittest" style tests. """ -import py +import pytest, py import sys def pytest_pycollect_makeitem(collector, name, obj): @@ -16,7 +16,7 @@ def pytest_pycollect_makeitem(collector, if isunit: return UnitTestCase(name, parent=collector) -class UnitTestCase(py.test.collect.Class): +class UnitTestCase(pytest.Class): def collect(self): loader = py.std.unittest.TestLoader() for name in loader.getTestCaseNames(self.obj): @@ -32,7 +32,7 @@ class UnitTestCase(py.test.collect.Class if meth is not None: meth() -class TestCaseFunction(py.test.collect.Function): +class TestCaseFunction(pytest.Function): def setup(self): pass def teardown(self): --- a/pytest/plugin/skipping.py +++ b/pytest/plugin/skipping.py @@ -65,7 +65,7 @@ class MarkEvaluator: def pytest_runtest_setup(item): - if not isinstance(item, py.test.collect.Function): + if not isinstance(item, pytest.Function): return evalskip = MarkEvaluator(item, 'skipif') if evalskip.istrue(): @@ -84,7 +84,7 @@ def check_xfail_no_run(item): py.test.xfail("[NOTRUN] " + evalxfail.getexplanation()) def pytest_runtest_makereport(__multicall__, item, call): - if not isinstance(item, py.test.collect.Function): + if not isinstance(item, pytest.Function): return if not (call.excinfo and call.excinfo.errisinstance(py.test.xfail.Exception)): --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -59,11 +59,11 @@ class TestGenerator: colitems = modcol.collect() assert len(colitems) == 1 gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) + assert isinstance(gencol, pytest.Generator) gencolitems = gencol.collect() assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) + assert isinstance(gencolitems[0], pytest.Function) + assert isinstance(gencolitems[1], pytest.Function) assert gencolitems[0].name == '[0]' assert gencolitems[0].obj.__name__ == 'func1' @@ -77,11 +77,11 @@ class TestGenerator: yield func1, 42, 6*7 """) gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, py.test.collect.Generator) + assert isinstance(gencol, pytest.Generator) gencolitems = gencol.collect() assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) + assert isinstance(gencolitems[0], pytest.Function) + assert isinstance(gencolitems[1], pytest.Function) assert gencolitems[0].name == '[0]' assert gencolitems[0].obj.__name__ == 'func1' @@ -97,11 +97,11 @@ class TestGenerator: colitems = modcol.collect() assert len(colitems) == 1 gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) + assert isinstance(gencol, pytest.Generator) gencolitems = gencol.collect() assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) + assert isinstance(gencolitems[0], pytest.Function) + assert isinstance(gencolitems[1], pytest.Function) assert gencolitems[0].name == "['seventeen']" assert gencolitems[0].obj.__name__ == 'func1' assert gencolitems[1].name == "['fortytwo']" @@ -118,7 +118,7 @@ class TestGenerator: colitems = modcol.collect() assert len(colitems) == 1 gencol = colitems[0] - assert isinstance(gencol, py.test.collect.Generator) + assert isinstance(gencol, pytest.Generator) py.test.raises(ValueError, "gencol.collect()") def test_generative_methods_with_explicit_names(self, testdir): @@ -131,11 +131,11 @@ class TestGenerator: yield "m2", func1, 42, 6*7 """) gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, py.test.collect.Generator) + assert isinstance(gencol, pytest.Generator) gencolitems = gencol.collect() assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], py.test.collect.Function) - assert isinstance(gencolitems[1], py.test.collect.Function) + assert isinstance(gencolitems[0], pytest.Function) + assert isinstance(gencolitems[1], pytest.Function) assert gencolitems[0].name == "['m1']" assert gencolitems[0].obj.__name__ == 'func1' assert gencolitems[1].name == "['m2']" @@ -199,20 +199,20 @@ class TestGenerator: class TestFunction: def test_getmodulecollector(self, testdir): item = testdir.getitem("def test_func(): pass") - modcol = item.getparent(py.test.collect.Module) - assert isinstance(modcol, py.test.collect.Module) + modcol = item.getparent(pytest.Module) + assert isinstance(modcol, pytest.Module) assert hasattr(modcol.obj, 'test_func') def test_function_equality(self, testdir, tmpdir): config = testdir.reparseconfig() session = testdir.Session(config) - f1 = py.test.collect.Function(name="name", config=config, + f1 = pytest.Function(name="name", config=config, args=(1,), callobj=isinstance, session=session) - f2 = py.test.collect.Function(name="name",config=config, + f2 = pytest.Function(name="name",config=config, args=(1,), callobj=py.builtin.callable, session=session) assert not f1 == f2 assert f1 != f2 - f3 = py.test.collect.Function(name="name", config=config, + f3 = pytest.Function(name="name", config=config, args=(1,2), callobj=py.builtin.callable, session=session) assert not f3 == f2 assert f3 != f2 @@ -220,7 +220,7 @@ class TestFunction: assert not f3 == f1 assert f3 != f1 - f1_b = py.test.collect.Function(name="name", config=config, + f1_b = pytest.Function(name="name", config=config, args=(1,), callobj=isinstance, session=session) assert f1 == f1_b assert not f1 != f1_b @@ -236,9 +236,9 @@ class TestFunction: funcargs = {} id = "world" session = testdir.Session(config) - f5 = py.test.collect.Function(name="name", config=config, + f5 = pytest.Function(name="name", config=config, callspec=callspec1, callobj=isinstance, session=session) - f5b = py.test.collect.Function(name="name", config=config, + f5b = pytest.Function(name="name", config=config, callspec=callspec2, callobj=isinstance, session=session) assert f5 != f5b assert not (f5 == f5b) @@ -263,9 +263,9 @@ class TestSorting: def test_fail(): assert 0 """) fn1 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn1, py.test.collect.Function) + assert isinstance(fn1, pytest.Function) fn2 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn2, py.test.collect.Function) + assert isinstance(fn2, pytest.Function) assert fn1 == fn2 assert fn1 != modcol @@ -274,7 +274,7 @@ class TestSorting: assert hash(fn1) == hash(fn2) fn3 = testdir.collect_by_name(modcol, "test_fail") - assert isinstance(fn3, py.test.collect.Function) + assert isinstance(fn3, pytest.Function) assert not (fn1 == fn3) assert fn1 != fn3 @@ -309,8 +309,8 @@ class TestSorting: class TestConftestCustomization: def test_pytest_pycollect_module(self, testdir): testdir.makeconftest(""" - import py - class MyModule(py.test.collect.Module): + import pytest + class MyModule(pytest.Module): pass def pytest_pycollect_makemodule(path, parent): if path.basename == "test_xyz.py": @@ -326,8 +326,8 @@ class TestConftestCustomization: def test_pytest_pycollect_makeitem(self, testdir): testdir.makeconftest(""" - import py - class MyFunction(py.test.collect.Function): + import pytest + class MyFunction(pytest.Function): pass def pytest_pycollect_makeitem(collector, name, obj): if name == "some": @@ -342,7 +342,7 @@ class TestConftestCustomization: def test_makeitem_non_underscore(self, testdir, monkeypatch): modcol = testdir.getmodulecol("def _hello(): pass") l = [] - monkeypatch.setattr(py.test.collect.Module, 'makeitem', + monkeypatch.setattr(pytest.Module, 'makeitem', lambda self, name, obj: l.append(name)) l = modcol.collect() assert '_hello' not in l @@ -545,7 +545,7 @@ class TestFillFuncArgs: item.config.pluginmanager.register(Provider()) if hasattr(item, '_args'): del item._args - py.test.collect._fillfuncargs(item) + pytest._fillfuncargs(item) assert len(item.funcargs) == 1 class TestRequest: @@ -634,7 +634,7 @@ class TestRequest: req.config._setupstate.prepare(item) # XXX req._fillfuncargs() # successively check finalization calls - teardownlist = item.getparent(py.test.collect.Module).obj.teardownlist + teardownlist = item.getparent(pytest.Module).obj.teardownlist ss = item.config._setupstate assert not teardownlist ss.teardown_exact(item) @@ -1009,11 +1009,11 @@ def test_conftest_funcargs_only_availabl def test_funcarg_non_pycollectobj(testdir): # rough jstests usage testdir.makeconftest(""" - import py + import pytest def pytest_pycollect_makeitem(collector, name, obj): if name == "MyClass": return MyCollector(name, parent=collector) - class MyCollector(py.test.collect.Collector): + class MyCollector(pytest.Collector): def reportinfo(self): return self.fspath, 3, "xyz" """) @@ -1048,8 +1048,8 @@ def test_funcarg_lookup_error(testdir): class TestReportInfo: def test_itemreport_reportinfo(self, testdir, linecomp): testdir.makeconftest(""" - import py - class MyFunction(py.test.collect.Function): + import pytest + class MyFunction(pytest.Function): def reportinfo(self): return "ABCDE", 42, "custom" def pytest_pycollect_makeitem(collector, name, obj): --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -24,7 +24,6 @@ class TestGeneralUsage: parser.addoption("--xyz", dest="xyz", action="store") """) testdir.makepyfile(test_one=""" - import py def test_option(pytestconfig): assert pytestconfig.option.xyz == "123" """) @@ -37,7 +36,6 @@ class TestGeneralUsage: def test_basetemp(self, testdir): mytemp = testdir.tmpdir.mkdir("mytemp") p = testdir.makepyfile(""" - import py def test_1(pytestconfig): pytestconfig.getbasetemp().ensure("hello") """) @@ -86,9 +84,9 @@ class TestGeneralUsage: def test_early_skip(self, testdir): testdir.mkdir("xyz") testdir.makeconftest(""" - import py + import pytest def pytest_collect_directory(): - py.test.skip("early") + pytest.skip("early") """) result = testdir.runpytest() assert result.ret == 0 @@ -98,8 +96,8 @@ class TestGeneralUsage: def test_issue88_initial_file_multinodes(self, testdir): testdir.makeconftest(""" - import py - class MyFile(py.test.collect.File): + import pytest + class MyFile(pytest.File): def collect(self): return def pytest_collect_file(path, parent): @@ -163,9 +161,9 @@ class TestGeneralUsage: def test_directory_skipped(self, testdir): testdir.makeconftest(""" - import py + import pytest def pytest_ignore_collect(): - py.test.skip("intentional") + pytest.skip("intentional") """) testdir.makepyfile("def test_hello(): pass") result = testdir.runpytest() @@ -176,11 +174,11 @@ class TestGeneralUsage: def test_multiple_items_per_collector_byid(self, testdir): c = testdir.makeconftest(""" - import py - class MyItem(py.test.collect.Item): + import pytest + class MyItem(pytest.Item): def runtest(self): pass - class MyCollector(py.test.collect.File): + class MyCollector(pytest.File): def collect(self): return [MyItem(name="xyz", parent=self)] def pytest_collect_file(path, parent): @@ -195,13 +193,13 @@ class TestGeneralUsage: def test_skip_on_generated_funcarg_id(self, testdir): testdir.makeconftest(""" - import py + import pytest def pytest_generate_tests(metafunc): metafunc.addcall({'x': 3}, id='hello-123') def pytest_runtest_setup(item): print (item.keywords) if 'hello-123' in item.keywords: - py.test.skip("hello") + pytest.skip("hello") assert 0 """) p = testdir.makepyfile("""def test_func(x): pass""") @@ -233,23 +231,37 @@ class TestGeneralUsage: class TestInvocationVariants: def test_earlyinit(self, testdir): p = testdir.makepyfile(""" - import py - assert hasattr(py.test, 'mark') + import pytest + assert hasattr(pytest, 'mark') """) result = testdir.runpython(p) assert result.ret == 0 def test_pydoc(self, testdir): - result = testdir.runpython_c("import py;help(py.test)") + for name in ('py.test', 'pytest'): + result = testdir.runpython_c("import %s;help(%s)" % (name,name)) + assert result.ret == 0 + s = result.stdout.str() + assert 'MarkGenerator' in s + + @pytest.mark.multi(source=['py.test', 'pytest']) + def test_import_star(self, testdir, source): + p = testdir.makepyfile(""" + from %s import * + collect + cmdline + main + skip + xfail + """ % source) + result = testdir.runpython(p) assert result.ret == 0 - s = result.stdout.str() - assert 'MarkGenerator' in s def test_double_pytestcmdline(self, testdir): p = testdir.makepyfile(run=""" - import py - py.test.cmdline.main() - py.test.cmdline.main() + import py, pytest + pytest.main() + pytest.main() """) testdir.makepyfile(""" def test_hello(): @@ -343,7 +355,6 @@ class TestInvocationVariants: def test_noclass_discovery_if_not_testcase(self, testdir): testpath = testdir.makepyfile(""" import unittest - import py class TestHello(object): def test_hello(self): assert self.attr --- a/testing/plugin/test_session.py +++ b/testing/plugin/test_session.py @@ -1,4 +1,4 @@ -import py +import pytest, py class SessionTests: def test_basic_testitem_events(self, testdir): @@ -26,7 +26,7 @@ class SessionTests: colstarted = reprec.getcalls("pytest_collectstart") assert len(colstarted) == 1 + 1 col = colstarted[1].collector - assert isinstance(col, py.test.collect.Module) + assert isinstance(col, pytest.Module) def test_nested_import_error(self, testdir): tfile = testdir.makepyfile(""" @@ -94,7 +94,7 @@ class SessionTests: def test_broken_repr(self, testdir): p = testdir.makepyfile(""" - import py + import pytest class BrokenRepr1: foo=0 def __repr__(self): @@ -103,7 +103,7 @@ class SessionTests: class TestBrokenClass: def test_explicit_bad_repr(self): t = BrokenRepr1() - py.test.raises(Exception, 'repr(t)') + pytest.raises(Exception, 'repr(t)') def test_implicit_bad_repr1(self): t = BrokenRepr1() --- a/pytest/plugin/python.py +++ b/pytest/plugin/python.py @@ -142,7 +142,7 @@ class PyobjMixin(object): modpath = self.getmodpath() return fspath, lineno, modpath -class PyCollectorMixin(PyobjMixin, pytest.collect.Collector): +class PyCollectorMixin(PyobjMixin, pytest.Collector): def funcnamefilter(self, name): return name.startswith('test') @@ -203,7 +203,7 @@ class PyCollectorMixin(PyobjMixin, pytes l.append(function) return l -class Module(pytest.collect.File, PyCollectorMixin): +class Module(pytest.File, PyCollectorMixin): def _getobj(self): return self._memoizedcall('_obj', self._importtestmodule) @@ -249,7 +249,7 @@ class Module(pytest.collect.File, PyColl else: self.obj.teardown_module() -class Class(PyCollectorMixin, pytest.collect.Collector): +class Class(PyCollectorMixin, pytest.Collector): def collect(self): return [Instance(name="()", parent=self)] @@ -266,7 +266,7 @@ class Class(PyCollectorMixin, pytest.col teardown_class = getattr(teardown_class, 'im_func', teardown_class) teardown_class(self.obj) -class Instance(PyCollectorMixin, pytest.collect.Collector): +class Instance(PyCollectorMixin, pytest.Collector): def _getobj(self): return self.parent.obj() @@ -348,7 +348,7 @@ class FuncargLookupErrorRepr(TerminalRep tw.line() tw.line("%s:%d" % (self.filename, self.firstlineno+1)) -class Generator(FunctionMixin, PyCollectorMixin, pytest.collect.Collector): +class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector): def collect(self): # test generators are seen as collectors but they also # invoke setup/teardown on popular request @@ -388,7 +388,7 @@ class Generator(FunctionMixin, PyCollect # Test Items # _dummy = object() -class Function(FunctionMixin, pytest.collect.Item): +class Function(FunctionMixin, pytest.Item): """ a Function Item is responsible for setting up and executing a Python callable test object. """ @@ -480,10 +480,10 @@ def fillfuncargs(function): def getplugins(node, withpy=False): # might by any node plugins = node.config._getmatchingplugins(node.fspath) if withpy: - mod = node.getparent(pytest.collect.Module) + mod = node.getparent(pytest.Module) if mod is not None: plugins.append(mod.obj) - inst = node.getparent(pytest.collect.Instance) + inst = node.getparent(pytest.Instance) if inst is not None: plugins.append(inst.obj) return plugins @@ -573,12 +573,12 @@ class FuncargRequest: @property def module(self): """ module where the test function was collected. """ - return self._pyfuncitem.getparent(pytest.collect.Module).obj + return self._pyfuncitem.getparent(pytest.Module).obj @property def cls(self): """ class (can be None) where the test function was collected. """ - clscol = self._pyfuncitem.getparent(pytest.collect.Class) + clscol = self._pyfuncitem.getparent(pytest.Class) if clscol: return clscol.obj @property @@ -679,7 +679,7 @@ class FuncargRequest: if scope == "function": return self._pyfuncitem elif scope == "module": - return self._pyfuncitem.getparent(py.test.collect.Module) + return self._pyfuncitem.getparent(pytest.Module) elif scope == "session": return None raise ValueError("unknown finalization scope %r" %(scope,)) --- a/example/assertion/global_testmodule_config/conftest.py +++ b/example/assertion/global_testmodule_config/conftest.py @@ -2,9 +2,9 @@ import py mydir = py.path.local(__file__).dirpath() def pytest_runtest_setup(item): - if isinstance(item, py.test.collect.Function): + if isinstance(item, pytest.Function): if not item.fspath.relto(mydir): return - mod = item.getparent(py.test.collect.Module).obj + mod = item.getparent(pytest.Module).obj if hasattr(mod, 'hello'): py.builtin.print_("mod.hello", mod.hello) --- a/doc/test/attic.txt +++ b/doc/test/attic.txt @@ -60,7 +60,7 @@ modules to determine collectors and item Customizing execution of Items and Functions ---------------------------------------------------- -- ``py.test.collect.Function`` test items control execution +- ``pytest.Function`` test items control execution of a test function through its ``function.runtest()`` method. This method is responsible for performing setup and teardown ("Test Fixtures") for a test Function. --- a/pytest/main.py +++ b/pytest/main.py @@ -6,6 +6,7 @@ All else is in pytest/plugin. import sys, os import inspect import py +import pytest from pytest import hookspec # the extension point definitions assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " @@ -207,7 +208,7 @@ class PluginManager(object): def pytest_plugin_registered(self, plugin): dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} if dic: - self._setns(py.test, dic) + self._setns(pytest, dic) if hasattr(self, '_config'): self.call_plugin(plugin, "pytest_addoption", {'parser': self._config._parser}) @@ -219,16 +220,20 @@ class PluginManager(object): if isinstance(value, dict): mod = getattr(obj, name, None) if mod is None: - mod = py.std.types.ModuleType(name) - sys.modules['pytest.%s' % name] = mod - sys.modules['py.test.%s' % name] = mod + modname = "pytest.%s" % name + mod = py.std.types.ModuleType(modname) + sys.modules[modname] = mod mod.__all__ = [] setattr(obj, name, mod) + #print "setns", mod, value self._setns(mod, value) else: #print "setting", name, value, "on", obj setattr(obj, name, value) obj.__all__.append(name) + #print "appending", name, "to", obj + #pytest.__all__.append(name) # don't show in help(py.test) + setattr(pytest, name, value) def pytest_terminal_summary(self, terminalreporter): tw = terminalreporter._tw @@ -284,6 +289,7 @@ def canonical_importname(name): return name def importplugin(importspec): + #print "importing", importspec try: return __import__(importspec, None, None, '__doc__') except ImportError: @@ -408,6 +414,9 @@ class HookCaller: _preinit = [PluginManager(load=True)] # triggers default plugin importing def main(args=None, plugins=None): + """ returned exit code integer, after an in-process testing run + with the given command line arguments, preloading an optional list + of passed in plugin objects. """ if args is None: args = sys.argv[1:] elif isinstance(args, py.path.local): --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,10 +1,10 @@ -import py +import pytest, py from pytest.plugin.session import Session class TestCollector: def test_collect_versus_item(self): - from pytest.collect import Collector, Item + from pytest import Collector, Item assert not issubclass(Collector, Item) assert not issubclass(Item, Collector) @@ -14,15 +14,15 @@ class TestCollector: def test_fail(): assert 0 """) recwarn.clear() - assert modcol.Module == py.test.collect.Module + assert modcol.Module == pytest.Module recwarn.pop(DeprecationWarning) - assert modcol.Class == py.test.collect.Class + assert modcol.Class == pytest.Class recwarn.pop(DeprecationWarning) - assert modcol.Item == py.test.collect.Item + assert modcol.Item == pytest.Item recwarn.pop(DeprecationWarning) - assert modcol.File == py.test.collect.File + assert modcol.File == pytest.File recwarn.pop(DeprecationWarning) - assert modcol.Function == py.test.collect.Function + assert modcol.Function == pytest.Function recwarn.pop(DeprecationWarning) def test_check_equality(self, testdir): @@ -31,9 +31,9 @@ class TestCollector: def test_fail(): assert 0 """) fn1 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn1, py.test.collect.Function) + assert isinstance(fn1, pytest.Function) fn2 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn2, py.test.collect.Function) + assert isinstance(fn2, pytest.Function) assert fn1 == fn2 assert fn1 != modcol @@ -42,7 +42,7 @@ class TestCollector: assert hash(fn1) == hash(fn2) fn3 = testdir.collect_by_name(modcol, "test_fail") - assert isinstance(fn3, py.test.collect.Function) + assert isinstance(fn3, pytest.Function) assert not (fn1 == fn3) assert fn1 != fn3 @@ -63,32 +63,32 @@ class TestCollector: fn = testdir.collect_by_name( testdir.collect_by_name(cls, "()"), "test_foo") - parent = fn.getparent(py.test.collect.Module) + parent = fn.getparent(pytest.Module) assert parent is modcol - parent = fn.getparent(py.test.collect.Function) + parent = fn.getparent(pytest.Function) assert parent is fn - parent = fn.getparent(py.test.collect.Class) + parent = fn.getparent(pytest.Class) assert parent is cls def test_getcustomfile_roundtrip(self, testdir): hello = testdir.makefile(".xxx", hello="world") testdir.makepyfile(conftest=""" - import py - class CustomFile(py.test.collect.File): + import pytest + class CustomFile(pytest.File): pass def pytest_collect_file(path, parent): if path.ext == ".xxx": return CustomFile(path, parent=parent) """) node = testdir.getpathnode(hello) - assert isinstance(node, py.test.collect.File) + assert isinstance(node, pytest.File) assert node.name == "hello.xxx" nodes = node.session.perform_collect([node.nodeid], genitems=False) assert len(nodes) == 1 - assert isinstance(nodes[0], py.test.collect.File) + assert isinstance(nodes[0], pytest.File) class TestCollectFS: def test_ignored_certain_directories(self, testdir): @@ -158,18 +158,18 @@ class TestPrunetraceback: import not_exists """) testdir.makeconftest(""" - import py + import pytest def pytest_collect_file(path, parent): return MyFile(path, parent) class MyError(Exception): pass - class MyFile(py.test.collect.File): + class MyFile(pytest.File): def collect(self): raise MyError() def repr_failure(self, excinfo): if excinfo.errisinstance(MyError): return "hello world" - return py.test.collect.File.repr_failure(self, excinfo) + return pytest.File.repr_failure(self, excinfo) """) result = testdir.runpytest(p) @@ -184,7 +184,7 @@ class TestPrunetraceback: import not_exists """) testdir.makeconftest(""" - import py + import pytest def pytest_make_collect_report(__multicall__): rep = __multicall__.execute() rep.headerlines += ["header1"] @@ -246,8 +246,8 @@ class TestCustomConftests: def test_pytest_fs_collect_hooks_are_seen(self, testdir): conf = testdir.makeconftest(""" - import py - class MyModule(py.test.collect.Module): + import pytest + class MyModule(pytest.Module): pass def pytest_collect_file(path, parent): if path.ext == ".py": @@ -265,8 +265,8 @@ class TestCustomConftests: sub1 = testdir.mkpydir("sub1") sub2 = testdir.mkpydir("sub2") conf1 = testdir.makeconftest(""" - import py - class MyModule1(py.test.collect.Module): + import pytest + class MyModule1(pytest.Module): pass def pytest_collect_file(path, parent): if path.ext == ".py": @@ -274,8 +274,8 @@ class TestCustomConftests: """) conf1.move(sub1.join(conf1.basename)) conf2 = testdir.makeconftest(""" - import py - class MyModule2(py.test.collect.Module): + import pytest + class MyModule2(pytest.Module): pass def pytest_collect_file(path, parent): if path.ext == ".py": @@ -378,11 +378,11 @@ class TestSession: def test_collect_custom_nodes_multi_id(self, testdir): p = testdir.makepyfile("def test_func(): pass") testdir.makeconftest(""" - import py - class SpecialItem(py.test.collect.Item): + import pytest + class SpecialItem(pytest.Item): def runtest(self): return # ok - class SpecialFile(py.test.collect.File): + class SpecialFile(pytest.File): def collect(self): return [SpecialItem(name="check", parent=self)] def pytest_collect_file(path, parent): @@ -481,7 +481,7 @@ class Test_getinitialnodes: x = tmpdir.ensure("x.py") config = testdir.reparseconfig([x]) col = testdir.getnode(config, x) - assert isinstance(col, py.test.collect.Module) + assert isinstance(col, pytest.Module) assert col.name == 'x.py' assert col.parent.name == testdir.tmpdir.basename assert col.parent.parent is None @@ -496,7 +496,7 @@ class Test_getinitialnodes: subdir.ensure("__init__.py") config = testdir.reparseconfig([x]) col = testdir.getnode(config, x) - assert isinstance(col, py.test.collect.Module) + assert isinstance(col, pytest.Module) assert col.name == 'subdir/x.py' assert col.parent.parent is None for col in col.listchain(): From commits-noreply at bitbucket.org Sat Nov 13 11:12:10 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 04:12:10 -0600 (CST) Subject: [py-svn] pytest commit 4e2a0da2c7df: flat is better than nested (cont'd): Message-ID: <20101113101210.AC144241099@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289643045 -3600 # Node ID 4e2a0da2c7df834b6789db857af49440a11f98af # Parent d3c75ce89f7d8c69468c3f96a3bd1473600895f6 flat is better than nested (cont'd): - pytest.py is new module, making "python -m pytest" work always - _pytest/*.py now contains core.py, hookspec and the plugins, no sub packages --- a/pytest/plugin/monkeypatch.py +++ /dev/null @@ -1,103 +0,0 @@ -""" monkeypatching and mocking functionality. """ - -import os, sys - -def pytest_funcarg__monkeypatch(request): - """The returned ``monkeypatch`` funcarg provides these - helper methods to modify objects, dictionaries or os.environ:: - - monkeypatch.setattr(obj, name, value, raising=True) - monkeypatch.delattr(obj, name, raising=True) - monkeypatch.setitem(mapping, name, value) - monkeypatch.delitem(obj, name, raising=True) - monkeypatch.setenv(name, value, prepend=False) - monkeypatch.delenv(name, value, raising=True) - monkeypatch.syspath_prepend(path) - - All modifications will be undone when the requesting - test function finished its execution. The ``raising`` - parameter determines if a KeyError or AttributeError - will be raised if the set/deletion operation has no target. - """ - mpatch = monkeypatch() - request.addfinalizer(mpatch.undo) - return mpatch - -notset = object() - -class monkeypatch: - """ object keeping a record of setattr/item/env/syspath changes. """ - def __init__(self): - self._setattr = [] - self._setitem = [] - - def setattr(self, obj, name, value, raising=True): - """ set attribute ``name`` on ``obj`` to ``value``, by default - raise AttributeEror if the attribute did not exist. """ - oldval = getattr(obj, name, notset) - if raising and oldval is notset: - raise AttributeError("%r has no attribute %r" %(obj, name)) - self._setattr.insert(0, (obj, name, oldval)) - setattr(obj, name, value) - - def delattr(self, obj, name, raising=True): - """ delete attribute ``name`` from ``obj``, by default raise - AttributeError it the attribute did not previously exist. """ - if not hasattr(obj, name): - if raising: - raise AttributeError(name) - else: - self._setattr.insert(0, (obj, name, getattr(obj, name, notset))) - delattr(obj, name) - - def setitem(self, dic, name, value): - """ set dictionary entry ``name`` to value. """ - self._setitem.insert(0, (dic, name, dic.get(name, notset))) - dic[name] = value - - def delitem(self, dic, name, raising=True): - """ delete ``name`` from dict, raise KeyError if it doesn't exist.""" - if name not in dic: - if raising: - raise KeyError(name) - else: - self._setitem.insert(0, (dic, name, dic.get(name, notset))) - del dic[name] - - def setenv(self, name, value, prepend=None): - """ set environment variable ``name`` to ``value``. if ``prepend`` - is a character, read the current environment variable value - and prepend the ``value`` adjoined with the ``prepend`` character.""" - value = str(value) - if prepend and name in os.environ: - value = value + prepend + os.environ[name] - self.setitem(os.environ, name, value) - - def delenv(self, name, raising=True): - """ delete ``name`` from environment, raise KeyError it not exists.""" - self.delitem(os.environ, name, raising=raising) - - def syspath_prepend(self, path): - """ prepend ``path`` to ``sys.path`` list of import locations. """ - if not hasattr(self, '_savesyspath'): - self._savesyspath = sys.path[:] - sys.path.insert(0, str(path)) - - def undo(self): - """ undo previous changes. This call consumes the - undo stack. Calling it a second time has no effect unless - you do more monkeypatching after the undo call.""" - for obj, name, value in self._setattr: - if value is not notset: - setattr(obj, name, value) - else: - delattr(obj, name) - self._setattr[:] = [] - for dictionary, name, value in self._setitem: - if value is notset: - del dictionary[name] - else: - dictionary[name] = value - self._setitem[:] = [] - if hasattr(self, '_savesyspath'): - sys.path[:] = self._savesyspath --- /dev/null +++ b/_pytest/monkeypatch.py @@ -0,0 +1,103 @@ +""" monkeypatching and mocking functionality. """ + +import os, sys + +def pytest_funcarg__monkeypatch(request): + """The returned ``monkeypatch`` funcarg provides these + helper methods to modify objects, dictionaries or os.environ:: + + monkeypatch.setattr(obj, name, value, raising=True) + monkeypatch.delattr(obj, name, raising=True) + monkeypatch.setitem(mapping, name, value) + monkeypatch.delitem(obj, name, raising=True) + monkeypatch.setenv(name, value, prepend=False) + monkeypatch.delenv(name, value, raising=True) + monkeypatch.syspath_prepend(path) + + All modifications will be undone when the requesting + test function finished its execution. The ``raising`` + parameter determines if a KeyError or AttributeError + will be raised if the set/deletion operation has no target. + """ + mpatch = monkeypatch() + request.addfinalizer(mpatch.undo) + return mpatch + +notset = object() + +class monkeypatch: + """ object keeping a record of setattr/item/env/syspath changes. """ + def __init__(self): + self._setattr = [] + self._setitem = [] + + def setattr(self, obj, name, value, raising=True): + """ set attribute ``name`` on ``obj`` to ``value``, by default + raise AttributeEror if the attribute did not exist. """ + oldval = getattr(obj, name, notset) + if raising and oldval is notset: + raise AttributeError("%r has no attribute %r" %(obj, name)) + self._setattr.insert(0, (obj, name, oldval)) + setattr(obj, name, value) + + def delattr(self, obj, name, raising=True): + """ delete attribute ``name`` from ``obj``, by default raise + AttributeError it the attribute did not previously exist. """ + if not hasattr(obj, name): + if raising: + raise AttributeError(name) + else: + self._setattr.insert(0, (obj, name, getattr(obj, name, notset))) + delattr(obj, name) + + def setitem(self, dic, name, value): + """ set dictionary entry ``name`` to value. """ + self._setitem.insert(0, (dic, name, dic.get(name, notset))) + dic[name] = value + + def delitem(self, dic, name, raising=True): + """ delete ``name`` from dict, raise KeyError if it doesn't exist.""" + if name not in dic: + if raising: + raise KeyError(name) + else: + self._setitem.insert(0, (dic, name, dic.get(name, notset))) + del dic[name] + + def setenv(self, name, value, prepend=None): + """ set environment variable ``name`` to ``value``. if ``prepend`` + is a character, read the current environment variable value + and prepend the ``value`` adjoined with the ``prepend`` character.""" + value = str(value) + if prepend and name in os.environ: + value = value + prepend + os.environ[name] + self.setitem(os.environ, name, value) + + def delenv(self, name, raising=True): + """ delete ``name`` from environment, raise KeyError it not exists.""" + self.delitem(os.environ, name, raising=raising) + + def syspath_prepend(self, path): + """ prepend ``path`` to ``sys.path`` list of import locations. """ + if not hasattr(self, '_savesyspath'): + self._savesyspath = sys.path[:] + sys.path.insert(0, str(path)) + + def undo(self): + """ undo previous changes. This call consumes the + undo stack. Calling it a second time has no effect unless + you do more monkeypatching after the undo call.""" + for obj, name, value in self._setattr: + if value is not notset: + setattr(obj, name, value) + else: + delattr(obj, name) + self._setattr[:] = [] + for dictionary, name, value in self._setitem: + if value is notset: + del dictionary[name] + else: + dictionary[name] = value + self._setitem[:] = [] + if hasattr(self, '_savesyspath'): + sys.path[:] = self._savesyspath --- /dev/null +++ b/_pytest/pastebin.py @@ -0,0 +1,63 @@ +""" submit failure or test session information to a pastebin service. """ +import py, sys + +class url: + base = "http://paste.pocoo.org" + xmlrpc = base + "/xmlrpc/" + show = base + "/show/" + +def pytest_addoption(parser): + group = parser.getgroup("terminal reporting") + group._addoption('--pastebin', metavar="mode", + action='store', dest="pastebin", default=None, + type="choice", choices=['failed', 'all'], + help="send failed|all info to Pocoo pastebin service.") + +def pytest_configure(__multicall__, config): + import tempfile + __multicall__.execute() + if config.option.pastebin == "all": + config._pastebinfile = tempfile.TemporaryFile('w+') + tr = config.pluginmanager.getplugin('terminalreporter') + oldwrite = tr._tw.write + def tee_write(s, **kwargs): + oldwrite(s, **kwargs) + config._pastebinfile.write(str(s)) + tr._tw.write = tee_write + +def pytest_unconfigure(config): + if hasattr(config, '_pastebinfile'): + config._pastebinfile.seek(0) + sessionlog = config._pastebinfile.read() + config._pastebinfile.close() + del config._pastebinfile + proxyid = getproxy().newPaste("python", sessionlog) + pastebinurl = "%s%s" % (url.show, proxyid) + sys.stderr.write("pastebin session-log: %s\n" % pastebinurl) + tr = config.pluginmanager.getplugin('terminalreporter') + del tr._tw.__dict__['write'] + +def getproxy(): + return py.std.xmlrpclib.ServerProxy(url.xmlrpc).pastes + +def pytest_terminal_summary(terminalreporter): + if terminalreporter.config.option.pastebin != "failed": + return + tr = terminalreporter + if 'failed' in tr.stats: + terminalreporter.write_sep("=", "Sending information to Paste Service") + if tr.config.option.debug: + terminalreporter.write_line("xmlrpcurl: %s" %(url.xmlrpc,)) + serverproxy = getproxy() + for rep in terminalreporter.stats.get('failed'): + try: + msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc + except AttributeError: + msg = tr._getfailureheadline(rep) + tw = py.io.TerminalWriter(stringio=True) + rep.toterminal(tw) + s = tw.stringio.getvalue() + assert len(s) + proxyid = serverproxy.newPaste("python", s) + pastebinurl = "%s%s" % (url.show, proxyid) + tr.write_line("%s --> %s" %(msg, pastebinurl)) --- a/pytest/plugin/capture.py +++ /dev/null @@ -1,216 +0,0 @@ -""" per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """ - -import py -import os - -def pytest_addoption(parser): - group = parser.getgroup("general") - group._addoption('--capture', action="store", default=None, - metavar="method", type="choice", choices=['fd', 'sys', 'no'], - help="per-test capturing method: one of fd (default)|sys|no.") - group._addoption('-s', action="store_const", const="no", dest="capture", - help="shortcut for --capture=no.") - -def addouterr(rep, outerr): - repr = getattr(rep, 'longrepr', None) - if not hasattr(repr, 'addsection'): - return - for secname, content in zip(["out", "err"], outerr): - if content: - repr.addsection("Captured std%s" % secname, content.rstrip()) - -def pytest_configure(config): - config.pluginmanager.register(CaptureManager(), 'capturemanager') - -def pytest_unconfigure(config): - capman = config.pluginmanager.getplugin('capturemanager') - while capman._method2capture: - name, cap = capman._method2capture.popitem() - cap.reset() - -class NoCapture: - def startall(self): - pass - def resume(self): - pass - def reset(self): - pass - def suspend(self): - return "", "" - -class CaptureManager: - def __init__(self): - self._method2capture = {} - - def _maketempfile(self): - f = py.std.tempfile.TemporaryFile() - newf = py.io.dupfile(f, encoding="UTF-8") - f.close() - return newf - - def _makestringio(self): - return py.io.TextIO() - - def _getcapture(self, method): - if method == "fd": - return py.io.StdCaptureFD(now=False, - out=self._maketempfile(), err=self._maketempfile() - ) - elif method == "sys": - return py.io.StdCapture(now=False, - out=self._makestringio(), err=self._makestringio() - ) - elif method == "no": - return NoCapture() - else: - raise ValueError("unknown capturing method: %r" % method) - - def _getmethod(self, config, fspath): - if config.option.capture: - method = config.option.capture - else: - try: - method = config._conftest.rget("option_capture", path=fspath) - except KeyError: - method = "fd" - if method == "fd" and not hasattr(os, 'dup'): # e.g. jython - method = "sys" - return method - - def resumecapture_item(self, item): - method = self._getmethod(item.config, item.fspath) - if not hasattr(item, 'outerr'): - item.outerr = ('', '') # we accumulate outerr on the item - return self.resumecapture(method) - - def resumecapture(self, method): - if hasattr(self, '_capturing'): - raise ValueError("cannot resume, already capturing with %r" % - (self._capturing,)) - cap = self._method2capture.get(method) - self._capturing = method - if cap is None: - self._method2capture[method] = cap = self._getcapture(method) - cap.startall() - else: - cap.resume() - - def suspendcapture(self, item=None): - self.deactivate_funcargs() - if hasattr(self, '_capturing'): - method = self._capturing - cap = self._method2capture.get(method) - if cap is not None: - outerr = cap.suspend() - del self._capturing - if item: - outerr = (item.outerr[0] + outerr[0], - item.outerr[1] + outerr[1]) - return outerr - if hasattr(item, 'outerr'): - return item.outerr - return "", "" - - def activate_funcargs(self, pyfuncitem): - if not hasattr(pyfuncitem, 'funcargs'): - return - assert not hasattr(self, '_capturing_funcargs') - self._capturing_funcargs = capturing_funcargs = [] - for name, capfuncarg in pyfuncitem.funcargs.items(): - if name in ('capsys', 'capfd'): - capturing_funcargs.append(capfuncarg) - capfuncarg._start() - - def deactivate_funcargs(self): - capturing_funcargs = getattr(self, '_capturing_funcargs', None) - if capturing_funcargs is not None: - while capturing_funcargs: - capfuncarg = capturing_funcargs.pop() - capfuncarg._finalize() - del self._capturing_funcargs - - def pytest_make_collect_report(self, __multicall__, collector): - method = self._getmethod(collector.config, collector.fspath) - try: - self.resumecapture(method) - except ValueError: - return # recursive collect, XXX refactor capturing - # to allow for more lightweight recursive capturing - try: - rep = __multicall__.execute() - finally: - outerr = self.suspendcapture() - addouterr(rep, outerr) - return rep - - def pytest_runtest_setup(self, item): - self.resumecapture_item(item) - - def pytest_runtest_call(self, item): - self.resumecapture_item(item) - self.activate_funcargs(item) - - def pytest_runtest_teardown(self, item): - self.resumecapture_item(item) - - def pytest__teardown_final(self, __multicall__, session): - method = self._getmethod(session.config, None) - self.resumecapture(method) - try: - rep = __multicall__.execute() - finally: - outerr = self.suspendcapture() - if rep: - addouterr(rep, outerr) - return rep - - def pytest_keyboard_interrupt(self, excinfo): - if hasattr(self, '_capturing'): - self.suspendcapture() - - def pytest_runtest_makereport(self, __multicall__, item, call): - self.deactivate_funcargs() - rep = __multicall__.execute() - outerr = self.suspendcapture(item) - if not rep.passed: - addouterr(rep, outerr) - if not rep.passed or rep.when == "teardown": - outerr = ('', '') - item.outerr = outerr - return rep - -def pytest_funcarg__capsys(request): - """captures writes to sys.stdout/sys.stderr and makes - them available successively via a ``capsys.readouterr()`` method - which returns a ``(out, err)`` tuple of captured snapshot strings. - """ - return CaptureFuncarg(py.io.StdCapture) - -def pytest_funcarg__capfd(request): - """captures writes to file descriptors 1 and 2 and makes - snapshotted ``(out, err)`` string tuples available - via the ``capsys.readouterr()`` method. If the underlying - platform does not have ``os.dup`` (e.g. Jython) tests using - this funcarg will automatically skip. - """ - if not hasattr(os, 'dup'): - py.test.skip("capfd funcarg needs os.dup") - return CaptureFuncarg(py.io.StdCaptureFD) - -class CaptureFuncarg: - def __init__(self, captureclass): - self.capture = captureclass(now=False) - - def _start(self): - self.capture.startall() - - def _finalize(self): - if hasattr(self, 'capture'): - self.capture.reset() - del self.capture - - def readouterr(self): - return self.capture.readouterr() - - def close(self): - self._finalize() --- a/testing/plugin/test_runner.py +++ b/testing/plugin/test_runner.py @@ -1,5 +1,5 @@ import py, sys -from pytest.plugin import runner +from _pytest import runner from py._code.code import ReprExceptionInfo class TestSetupState: --- a/doc/nose.txt +++ b/doc/nose.txt @@ -4,7 +4,7 @@ Running test written for nose .. include:: links.inc py.test has basic support for running tests written for nose_. -This is implemented in :pymod:`pytest.plugin.nose`. +This is implemented in :pymod:`_pytest.nose`. Usage ------------- --- /dev/null +++ b/_pytest/resultlog.py @@ -0,0 +1,92 @@ +""" (disabled by default) create result information in a plain text file. """ + +import py +from py.builtin import print_ + +def pytest_addoption(parser): + group = parser.getgroup("resultlog", "resultlog plugin options") + group.addoption('--resultlog', action="store", dest="resultlog", metavar="path", default=None, + help="path for machine-readable result log.") + +def pytest_configure(config): + resultlog = config.option.resultlog + # prevent opening resultlog on slave nodes (xdist) + if resultlog and not hasattr(config, 'slaveinput'): + logfile = open(resultlog, 'w', 1) # line buffered + config._resultlog = ResultLog(config, logfile) + config.pluginmanager.register(config._resultlog) + +def pytest_unconfigure(config): + resultlog = getattr(config, '_resultlog', None) + if resultlog: + resultlog.logfile.close() + del config._resultlog + config.pluginmanager.unregister(resultlog) + +def generic_path(item): + chain = item.listchain() + gpath = [chain[0].name] + fspath = chain[0].fspath + fspart = False + for node in chain[1:]: + newfspath = node.fspath + if newfspath == fspath: + if fspart: + gpath.append(':') + fspart = False + else: + gpath.append('.') + else: + gpath.append('/') + fspart = True + name = node.name + if name[0] in '([': + gpath.pop() + gpath.append(name) + fspath = newfspath + return ''.join(gpath) + +class ResultLog(object): + def __init__(self, config, logfile): + self.config = config + self.logfile = logfile # preferably line buffered + + def write_log_entry(self, testpath, lettercode, longrepr): + print_("%s %s" % (lettercode, testpath), file=self.logfile) + for line in longrepr.splitlines(): + print_(" %s" % line, file=self.logfile) + + def log_outcome(self, report, lettercode, longrepr): + testpath = getattr(report, 'nodeid', None) + if testpath is None: + testpath = report.fspath + self.write_log_entry(testpath, lettercode, longrepr) + + def pytest_runtest_logreport(self, report): + res = self.config.hook.pytest_report_teststatus(report=report) + code = res[1] + if code == 'x': + longrepr = str(report.longrepr) + elif code == 'X': + longrepr = '' + elif report.passed: + longrepr = "" + elif report.failed: + longrepr = str(report.longrepr) + elif report.skipped: + longrepr = str(report.longrepr.reprcrash.message) + self.log_outcome(report, code, longrepr) + + def pytest_collectreport(self, report): + if not report.passed: + if report.failed: + code = "F" + else: + assert report.skipped + code = "S" + longrepr = str(report.longrepr.reprcrash) + self.log_outcome(report, code, longrepr) + + def pytest_internalerror(self, excrepr): + path = excrepr.reprcrash.path + self.write_log_entry(path, '!', str(excrepr)) --- a/testing/plugin/test_terminal.py +++ b/testing/plugin/test_terminal.py @@ -4,9 +4,9 @@ terminal reporting of the full testing p import pytest,py import sys -from pytest.plugin.terminal import TerminalReporter, \ +from _pytest.terminal import TerminalReporter, \ CollectonlyReporter, repr_pythonversion, getreportopt -from pytest.plugin import runner +from _pytest import runner def basic_run_report(item): runner.call_and_report(item, "setup", log=False) --- a/testing/plugin/test_tmpdir.py +++ b/testing/plugin/test_tmpdir.py @@ -1,7 +1,7 @@ import py -from pytest.plugin.tmpdir import pytest_funcarg__tmpdir -from pytest.plugin.python import FuncargRequest +from _pytest.tmpdir import pytest_funcarg__tmpdir +from _pytest.python import FuncargRequest def test_funcarg(testdir): item = testdir.getitem(""" --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -1,5 +1,5 @@ import py -from pytest.plugin import config as parseopt +from _pytest import config as parseopt from textwrap import dedent class TestParser: --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev25', + version='2.0.0.dev26', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], @@ -41,7 +41,8 @@ def main(): 'Topic :: Utilities', 'Programming Language :: Python', 'Programming Language :: Python :: 3'], - packages=['pytest', 'pytest.plugin', ], + packages=['_pytest', ], + py_modules=['pytest'], zip_safe=False, ) --- /dev/null +++ b/_pytest/genscript.py @@ -0,0 +1,73 @@ +""" generate a single-file self-contained version of py.test """ +import py +import pickle +import zlib +import base64 + +def find_toplevel(name): + for syspath in py.std.sys.path: + base = py.path.local(syspath) + lib = base/name + if lib.check(dir=1): + return lib + mod = base.join("%s.py" % name) + if mod.check(file=1): + return mod + raise LookupError(name) + +def pkgname(toplevel, rootpath, path): + parts = path.parts()[len(rootpath.parts()):] + return '.'.join([toplevel] + [x.purebasename for x in parts]) + +def pkg_to_mapping(name): + toplevel = find_toplevel(name) + name2src = {} + if toplevel.check(file=1): # module + name2src[toplevel.purebasename] = toplevel.read() + else: # package + for pyfile in toplevel.visit('*.py'): + pkg = pkgname(name, toplevel, pyfile) + name2src[pkg] = pyfile.read() + return name2src + +def compress_mapping(mapping): + data = pickle.dumps(mapping, 2) + data = zlib.compress(data, 9) + data = base64.encodestring(data) + data = data.decode('ascii') + return data + + +def compress_packages(names): + mapping = {} + for name in names: + mapping.update(pkg_to_mapping(name)) + return compress_mapping(mapping) + + +def generate_script(entry, packages): + data = compress_packages(packages) + tmpl = py.path.local(__file__).dirpath().join('standalonetemplate.py') + exe = tmpl.read() + exe = exe.replace('@SOURCES@', data) + exe = exe.replace('@ENTRY@', entry) + return exe + + +def pytest_addoption(parser): + group = parser.getgroup("debugconfig") + group.addoption("--genscript", action="store", default=None, + dest="genscript", metavar="path", + help="create standalone py.test script at given target path.") + +def pytest_cmdline_main(config): + genscript = config.getvalue("genscript") + if genscript: + script = generate_script( + 'import py; raise SystemExit(py.test.cmdline.main())', + ['py', '_pytest', 'pytest'], + ) + + genscript = py.path.local(genscript) + genscript.write(script) + return 0 --- /dev/null +++ b/_pytest/tmpdir.py @@ -0,0 +1,26 @@ +""" support for providing temporary directories to test functions. """ +import pytest, py + +def pytest_configure(config): + def ensuretemp(string, dir=1): + """ (deprecated) return temporary directory path with + the given string as the trailing part. It is usually + better to use the 'tmpdir' function argument which will + take care to provide empty unique directories for each + test call even if the test is called multiple times. + """ + #py.log._apiwarn(">1.1", "use tmpdir function argument") + return config.ensuretemp(string, dir=dir) + pytest.ensuretemp = ensuretemp + +def pytest_funcarg__tmpdir(request): + """return a temporary directory path object + unique to each test function invocation, + created as a sub directory of the base temporary + directory. The returned object is a `py.path.local`_ + path object. + """ + name = request._pyfuncitem.name + name = py.std.re.sub("[\W]", "_", name) + x = request.config.mktemp(name, numbered=True) + return x.realpath() --- a/pytest/plugin/standalonetemplate.py +++ /dev/null @@ -1,63 +0,0 @@ -#! /usr/bin/env python - -sources = """ - at SOURCES@""" - -import sys -import base64 -import zlib -import imp - -class DictImporter(object): - def __init__(self, sources): - self.sources = sources - - def find_module(self, fullname, path=None): - if fullname in self.sources: - return self - if fullname+'.__init__' in self.sources: - return self - return None - - def load_module(self, fullname): - # print "load_module:", fullname - from types import ModuleType - try: - s = self.sources[fullname] - is_pkg = False - except KeyError: - s = self.sources[fullname+'.__init__'] - is_pkg = True - - co = compile(s, fullname, 'exec') - module = sys.modules.setdefault(fullname, ModuleType(fullname)) - module.__file__ = "%s/%s" % (__file__, fullname) - module.__loader__ = self - if is_pkg: - module.__path__ = [fullname] - - do_exec(co, module.__dict__) - return sys.modules[fullname] - - def get_source(self, name): - res = self.sources.get(name) - if res is None: - res = self.sources.get(name+'.__init__') - return res - -if __name__ == "__main__": - if sys.version_info >= (3,0): - exec("def do_exec(co, loc): exec(co, loc)\n") - import pickle - sources = sources.encode("ascii") # ensure bytes - sources = pickle.loads(zlib.decompress(base64.decodebytes(sources))) - else: - import cPickle as pickle - exec("def do_exec(co, loc): exec co in loc\n") - sources = pickle.loads(zlib.decompress(base64.decodestring(sources))) - - importer = DictImporter(sources) - sys.meta_path.append(importer) - - entry = "@ENTRY@" - do_exec(entry, locals()) --- a/doc/usage.txt +++ b/doc/usage.txt @@ -7,6 +7,19 @@ Usage and Invocations .. _cmdline: +calling pytest through ``python -m pytest`` +----------------------------------------------------- + +.. versionadded: 2.0 + +If you use Python-2.5 or above you can invoke testing through the +Python interpreter from the command line:: + + python -m pytest [...] + +This is equivalent to invoking the command line script ``py.test [...]`` +directly. + Getting help on version, option names, environment vars ----------------------------------------------------------- @@ -38,23 +51,6 @@ Import 'pkg' and use its filesystem loca py.test --pyargs pkg # run all tests found below directory of pypkg -calling pytest through ``python -m pytest`` ------------------------------------------------------ - -.. versionadded: 2.0 - -You can invoke testing through the Python interpreter from the command line:: - - python -m pytest.main [...] - -Python2.7 and Python3 introduced specifying packages to "-m" so there -you can also type:: - - python -m pytest [...] - -All of these invocations are equivalent to the ``py.test [...]`` command line invocation. - - Modifying Python traceback printing ---------------------------------------------- --- /dev/null +++ b/_pytest/junitxml.py @@ -0,0 +1,174 @@ +""" report test results in JUnit-XML format, for use with Hudson and build integration servers. + +Based on initial code from Ross Lawley. +""" + +import py +import os +import time + +def pytest_addoption(parser): + group = parser.getgroup("terminal reporting") + group.addoption('--junitxml', action="store", dest="xmlpath", + metavar="path", default=None, + help="create junit-xml style report file at given path.") + group.addoption('--junitprefix', action="store", dest="junitprefix", + metavar="str", default=None, + help="prepend prefix to classnames in junit-xml output") + +def pytest_configure(config): + xmlpath = config.option.xmlpath + if xmlpath: + config._xml = LogXML(xmlpath, config.option.junitprefix) + config.pluginmanager.register(config._xml) + +def pytest_unconfigure(config): + xml = getattr(config, '_xml', None) + if xml: + del config._xml + config.pluginmanager.unregister(xml) + +class LogXML(object): + def __init__(self, logfile, prefix): + self.logfile = logfile + self.prefix = prefix + self.test_logs = [] + self.passed = self.skipped = 0 + self.failed = self.errors = 0 + self._durations = {} + + def _opentestcase(self, report): + names = report.nodeid.split("::") + names[0] = names[0].replace("/", '.') + names = tuple(names) + d = {'time': self._durations.pop(names, "0")} + names = [x.replace(".py", "") for x in names if x != "()"] + classnames = names[:-1] + if self.prefix: + classnames.insert(0, self.prefix) + d['classname'] = ".".join(classnames) + d['name'] = py.xml.escape(names[-1]) + attrs = ['%s="%s"' % item for item in sorted(d.items())] + self.test_logs.append("\n" % " ".join(attrs)) + + def _closetestcase(self): + self.test_logs.append("") + + def appendlog(self, fmt, *args): + args = tuple([py.xml.escape(arg) for arg in args]) + self.test_logs.append(fmt % args) + + def append_pass(self, report): + self.passed += 1 + self._opentestcase(report) + self._closetestcase() + + def append_failure(self, report): + self._opentestcase(report) + #msg = str(report.longrepr.reprtraceback.extraline) + if "xfail" in report.keywords: + self.appendlog( + '') + self.skipped += 1 + else: + self.appendlog('%s', + report.longrepr) + self.failed += 1 + self._closetestcase() + + def append_collect_failure(self, report): + self._opentestcase(report) + #msg = str(report.longrepr.reprtraceback.extraline) + self.appendlog('%s', + report.longrepr) + self._closetestcase() + self.errors += 1 + + def append_collect_skipped(self, report): + self._opentestcase(report) + #msg = str(report.longrepr.reprtraceback.extraline) + self.appendlog('%s', + report.longrepr) + self._closetestcase() + self.skipped += 1 + + def append_error(self, report): + self._opentestcase(report) + self.appendlog('%s', + report.longrepr) + self._closetestcase() + self.errors += 1 + + def append_skipped(self, report): + self._opentestcase(report) + if "xfail" in report.keywords: + self.appendlog( + '%s', + report.keywords['xfail']) + else: + self.appendlog("") + self._closetestcase() + self.skipped += 1 + + def pytest_runtest_logreport(self, report): + if report.passed: + self.append_pass(report) + elif report.failed: + if report.when != "call": + self.append_error(report) + else: + self.append_failure(report) + elif report.skipped: + self.append_skipped(report) + + def pytest_runtest_call(self, item, __multicall__): + names = tuple(item.listnames()) + start = time.time() + try: + return __multicall__.execute() + finally: + self._durations[names] = time.time() - start + + def pytest_collectreport(self, report): + if not report.passed: + if report.failed: + self.append_collect_failure(report) + else: + self.append_collect_skipped(report) + + def pytest_internalerror(self, excrepr): + self.errors += 1 + data = py.xml.escape(excrepr) + self.test_logs.append( + '\n' + ' ' + '%s' % data) + + def pytest_sessionstart(self, session): + self.suite_start_time = time.time() + + def pytest_sessionfinish(self, session, exitstatus, __multicall__): + if py.std.sys.version_info[0] < 3: + logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8') + else: + logfile = open(self.logfile, 'w', encoding='utf-8') + + suite_stop_time = time.time() + suite_time_delta = suite_stop_time - self.suite_start_time + numtests = self.passed + self.failed + logfile.write('') + logfile.write('') + logfile.writelines(self.test_logs) + logfile.write('') + logfile.close() + + def pytest_terminal_summary(self, terminalreporter): + tw = terminalreporter._tw + terminalreporter.write_sep("-", "generated xml file: %s" %(self.logfile)) --- a/pytest/plugin/runner.py +++ /dev/null @@ -1,373 +0,0 @@ -""" basic collect and runtest protocol implementations """ - -import py, sys -from py._code.code import TerminalRepr - -def pytest_namespace(): - return { - 'fail' : fail, - 'skip' : skip, - 'importorskip' : importorskip, - 'exit' : exit, - } - -# -# pytest plugin hooks - -# XXX move to pytest_sessionstart and fix py.test owns tests -def pytest_configure(config): - config._setupstate = SetupState() - -def pytest_sessionfinish(session, exitstatus): - if hasattr(session.config, '_setupstate'): - hook = session.config.hook - rep = hook.pytest__teardown_final(session=session) - if rep: - hook.pytest__teardown_final_logerror(session=session, report=rep) - session.exitstatus = 1 - -class NodeInfo: - def __init__(self, location): - self.location = location - -def pytest_runtest_protocol(item): - item.ihook.pytest_runtest_logstart( - nodeid=item.nodeid, location=item.location, - ) - runtestprotocol(item) - return True - -def runtestprotocol(item, log=True): - 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)) - return reports - -def pytest_runtest_setup(item): - item.config._setupstate.prepare(item) - -def pytest_runtest_call(item): - item.runtest() - -def pytest_runtest_teardown(item): - item.config._setupstate.teardown_exact(item) - -def pytest__teardown_final(session): - call = CallInfo(session.config._setupstate.teardown_all, when="teardown") - if call.excinfo: - ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) - call.excinfo.traceback = ntraceback.filter() - longrepr = call.excinfo.getrepr(funcargs=True) - return TeardownErrorReport(longrepr) - -def pytest_report_teststatus(report): - if report.when in ("setup", "teardown"): - if report.failed: - # category, shortletter, verbose-word - return "error", "E", "ERROR" - elif report.skipped: - return "skipped", "s", "SKIPPED" - else: - return "", "", "" - - -# -# Implementation - -def call_and_report(item, when, log=True): - call = call_runtest_hook(item, when) - hook = item.ihook - report = hook.pytest_runtest_makereport(item=item, call=call) - if log and (when == "call" or not report.passed): - hook.pytest_runtest_logreport(report=report) - return report - -def call_runtest_hook(item, when): - hookname = "pytest_runtest_" + when - ihook = getattr(item.ihook, hookname) - return CallInfo(lambda: ihook(item=item), when=when) - -class CallInfo: - """ Result/Exception info a function invocation. """ - #: None or ExceptionInfo object. - excinfo = None - def __init__(self, func, when): - #: context of invocation: one of "setup", "call", - #: "teardown", "memocollect" - self.when = when - try: - self.result = func() - except KeyboardInterrupt: - raise - except: - self.excinfo = py.code.ExceptionInfo() - - def __repr__(self): - if self.excinfo: - status = "exception: %s" % str(self.excinfo.value) - else: - status = "result: %r" % (self.result,) - return "" % (self.when, status) - -class BaseReport(object): - def toterminal(self, out): - longrepr = self.longrepr - if hasattr(longrepr, 'toterminal'): - longrepr.toterminal(out) - else: - out.line(str(longrepr)) - - passed = property(lambda x: x.outcome == "passed") - failed = property(lambda x: x.outcome == "failed") - skipped = property(lambda x: x.outcome == "skipped") - - @property - def fspath(self): - return self.nodeid.split("::")[0] - -def pytest_runtest_makereport(item, call): - when = call.when - keywords = dict([(x,1) for x in item.keywords]) - if not call.excinfo: - outcome = "passed" - longrepr = None - else: - excinfo = call.excinfo - if not isinstance(excinfo, py.code.ExceptionInfo): - outcome = "failed" - longrepr = excinfo - elif excinfo.errisinstance(py.test.skip.Exception): - outcome = "skipped" - longrepr = item._repr_failure_py(excinfo) - else: - outcome = "failed" - if call.when == "call": - longrepr = item.repr_failure(excinfo) - else: # exception in setup or teardown - longrepr = item._repr_failure_py(excinfo) - return TestReport(item.nodeid, item.location, - keywords, outcome, longrepr, when) - -class TestReport(BaseReport): - """ Basic test report object (also used for setup and teardown calls if - they fail). - """ - def __init__(self, nodeid, location, - keywords, outcome, longrepr, when): - #: normalized collection node id - self.nodeid = nodeid - - #: a (filesystempath, lineno, domaininfo) tuple indicating the - #: actual location of a test item - it might be different from the - #: collected one e.g. if a method is inherited from a different module. - self.location = location - - #: a name -> value dictionary containing all keywords and - #: markers associated with a test invocation. - self.keywords = keywords - - #: test outcome, always one of "passed", "failed", "skipped". - self.outcome = outcome - - #: None or a failure representation. - self.longrepr = longrepr - - #: one of 'setup', 'call', 'teardown' to indicate runtest phase. - self.when = when - - def __repr__(self): - return "" % ( - self.nodeid, self.when, self.outcome) - -class TeardownErrorReport(BaseReport): - outcome = "failed" - when = "teardown" - def __init__(self, longrepr): - self.longrepr = longrepr - -def pytest_make_collect_report(collector): - call = CallInfo(collector._memocollect, "memocollect") - reason = longrepr = None - if not call.excinfo: - outcome = "passed" - else: - if call.excinfo.errisinstance(py.test.skip.Exception): - outcome = "skipped" - reason = str(call.excinfo.value) - longrepr = collector._repr_failure_py(call.excinfo, "line") - else: - outcome = "failed" - errorinfo = collector.repr_failure(call.excinfo) - if not hasattr(errorinfo, "toterminal"): - errorinfo = CollectErrorRepr(errorinfo) - longrepr = errorinfo - return CollectReport(collector.nodeid, outcome, longrepr, - getattr(call, 'result', None), reason) - -class CollectReport(BaseReport): - def __init__(self, nodeid, outcome, longrepr, result, reason): - self.nodeid = nodeid - self.outcome = outcome - self.longrepr = longrepr - self.result = result or [] - self.reason = reason - - @property - def location(self): - return (self.fspath, None, self.fspath) - - def __repr__(self): - return "" % ( - self.nodeid, len(self.result), self.outcome) - -class CollectErrorRepr(TerminalRepr): - def __init__(self, msg): - self.longrepr = msg - def toterminal(self, out): - out.line(str(self.longrepr), red=True) - -class SetupState(object): - """ shared state for setting up/tearing down test items or collectors. """ - def __init__(self): - self.stack = [] - self._finalizers = {} - - def addfinalizer(self, finalizer, colitem): - """ attach a finalizer to the given colitem. - if colitem is None, this will add a finalizer that - is called at the end of teardown_all(). - """ - assert hasattr(finalizer, '__call__') - #assert colitem in self.stack - self._finalizers.setdefault(colitem, []).append(finalizer) - - def _pop_and_teardown(self): - colitem = self.stack.pop() - self._teardown_with_finalization(colitem) - - def _callfinalizers(self, colitem): - finalizers = self._finalizers.pop(colitem, None) - while finalizers: - fin = finalizers.pop() - fin() - - def _teardown_with_finalization(self, colitem): - self._callfinalizers(colitem) - if colitem: - colitem.teardown() - for colitem in self._finalizers: - assert colitem is None or colitem in self.stack - - def teardown_all(self): - while self.stack: - self._pop_and_teardown() - self._teardown_with_finalization(None) - assert not self._finalizers - - def teardown_exact(self, item): - if self.stack and item == self.stack[-1]: - self._pop_and_teardown() - else: - self._callfinalizers(item) - - def prepare(self, colitem): - """ setup objects along the collector chain to the test-method - and teardown previously setup objects.""" - needed_collectors = colitem.listchain() - while self.stack: - if self.stack == needed_collectors[:len(self.stack)]: - break - self._pop_and_teardown() - # check if the last collection node has raised an error - for col in self.stack: - if hasattr(col, '_prepare_exc'): - py.builtin._reraise(*col._prepare_exc) - for col in needed_collectors[len(self.stack):]: - self.stack.append(col) - try: - col.setup() - except Exception: - col._prepare_exc = sys.exc_info() - raise - -# ============================================================= -# Test OutcomeExceptions and helpers for creating them. - - -class OutcomeException(Exception): - """ OutcomeException and its subclass instances indicate and - contain info about test and collection outcomes. - """ - def __init__(self, msg=None): - self.msg = msg - - def __repr__(self): - if self.msg: - return str(self.msg) - return "<%s instance>" %(self.__class__.__name__,) - __str__ = __repr__ - -class Skipped(OutcomeException): - # XXX hackish: on 3k we fake to live in the builtins - # in order to have Skipped exception printing shorter/nicer - __module__ = 'builtins' - -class Failed(OutcomeException): - """ raised from an explicit call to py.test.fail() """ - __module__ = 'builtins' - -class Exit(KeyboardInterrupt): - """ raised for immediate program exits (no tracebacks/summaries)""" - def __init__(self, msg="unknown reason"): - self.msg = msg - KeyboardInterrupt.__init__(self, msg) - -# exposed helper methods - -def exit(msg): - """ exit testing process as if KeyboardInterrupt was triggered. """ - __tracebackhide__ = True - raise Exit(msg) - -exit.Exception = Exit - -def skip(msg=""): - """ skip an executing test with the given message. Note: it's usually - better to use the py.test.mark.skipif marker to declare a test to be - skipped under certain conditions like mismatching platforms or - dependencies. See the pytest_skipping plugin for details. - """ - __tracebackhide__ = True - raise Skipped(msg=msg) -skip.Exception = Skipped - -def fail(msg=""): - """ explicitely fail an currently-executing test with the given Message. """ - __tracebackhide__ = True - raise Failed(msg=msg) -fail.Exception = Failed - - -def importorskip(modname, minversion=None): - """ return imported module if it has a higher __version__ than the - optionally specified 'minversion' - otherwise call py.test.skip() - with a message detailing the mismatch. - """ - compile(modname, '', 'eval') # to catch syntaxerrors - try: - mod = __import__(modname, None, None, ['__doc__']) - except ImportError: - py.test.skip("could not import %r" %(modname,)) - if minversion is None: - return mod - verattr = getattr(mod, '__version__', None) - if isinstance(minversion, str): - minver = minversion.split(".") - else: - minver = list(minversion) - if verattr is None or verattr.split(".") < minver: - py.test.skip("module %r has __version__ %r, required is: %r" %( - modname, verattr, minversion)) - return mod --- a/testing/plugin/test_doctest.py +++ b/testing/plugin/test_doctest.py @@ -1,4 +1,4 @@ -from pytest.plugin.doctest import DoctestModule, DoctestTextfile +from _pytest.doctest import DoctestModule, DoctestTextfile import py pytest_plugins = ["pytest_doctest"] --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -244,16 +244,30 @@ class TestInvocationVariants: s = result.stdout.str() assert 'MarkGenerator' in s - @pytest.mark.multi(source=['py.test', 'pytest']) - def test_import_star(self, testdir, source): + def test_import_star_py_dot_test(self, testdir): p = testdir.makepyfile(""" - from %s import * - collect - cmdline + from py.test import * + #collect + #cmdline + #Item + #assert collect.Item is Item + #assert collect.Collector is Collector main skip xfail - """ % source) + """) + result = testdir.runpython(p) + assert result.ret == 0 + + def test_import_star_pytest(self, testdir): + p = testdir.makepyfile(""" + from pytest import * + #Item + #File + main + skip + xfail + """) result = testdir.runpython(p) assert result.ret == 0 @@ -286,13 +300,6 @@ class TestInvocationVariants: assert res.ret == 1 @py.test.mark.skipif("sys.version_info < (2,5)") - def test_python_pytest_main(self, testdir): - p1 = testdir.makepyfile("def test_pass(): pass") - res = testdir.run(py.std.sys.executable, "-m", "pytest.main", str(p1)) - assert res.ret == 0 - res.stdout.fnmatch_lines(["*1 passed*"]) - - @py.test.mark.skipif("sys.version_info < (2,7)") def test_python_pytest_package(self, testdir): p1 = testdir.makepyfile("def test_pass(): pass") res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1)) --- a/pytest/hookspec.py +++ /dev/null @@ -1,223 +0,0 @@ -""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ - -# ------------------------------------------------------------------------- -# Initialization -# ------------------------------------------------------------------------- - -def pytest_addhooks(pluginmanager): - """called at plugin load time to allow adding new hooks via a call to - pluginmanager.registerhooks(module).""" - - -def pytest_namespace(): - """return dict of name->object to be made globally available in - the py.test/pytest namespace. This hook is called before command - line options are parsed. - """ - -def pytest_cmdline_parse(pluginmanager, args): - """return initialized config object, parsing the specified args. """ -pytest_cmdline_parse.firstresult = True - -def pytest_addoption(parser): - """add optparse-style options and ini-style config values via calls - to ``parser.addoption`` and ``parser.addini(...)``. - """ - -def pytest_cmdline_main(config): - """ called for performing the main command line action. The default - implementation will invoke the configure hooks and runtest_mainloop. """ -pytest_cmdline_main.firstresult = True - -def pytest_configure(config): - """ called after command line options have been parsed. - and all plugins and initial conftest files been loaded. - """ - -def pytest_unconfigure(config): - """ called before test process is exited. """ - -def pytest_runtestloop(session): - """ called for performing the main runtest loop - (after collection finished). """ -pytest_runtestloop.firstresult = True - -# ------------------------------------------------------------------------- -# collection hooks -# ------------------------------------------------------------------------- - -def pytest_collection(session): - """ perform the collection protocol for the given session. """ -pytest_collection.firstresult = True - -def pytest_collection_modifyitems(session, config, items): - """ called after collection has been performed, may filter or re-order - the items in-place.""" - -def pytest_collection_finish(session): - """ called after collection has been performed and modified. """ - -def pytest_ignore_collect(path, config): - """ return True to prevent considering this path for collection. - This hook is consulted for all files and directories prior to calling - more specific hooks. - """ -pytest_ignore_collect.firstresult = True - -def pytest_collect_directory(path, parent): - """ called before traversing a directory for collection files. """ -pytest_collect_directory.firstresult = True - -def pytest_collect_file(path, parent): - """ return collection Node or None for the given path. Any new node - needs to have the specified ``parent`` as a parent.""" - -# logging hooks for collection -def pytest_collectstart(collector): - """ collector starts collecting. """ - -def pytest_itemcollected(item): - """ we just collected a test item. """ - -def pytest_collectreport(report): - """ collector finished collecting. """ - -def pytest_deselected(items): - """ called for test items deselected by keyword. """ - -def pytest_make_collect_report(collector): - """ perform ``collector.collect()`` and return a CollectReport. """ -pytest_make_collect_report.firstresult = True - -# ------------------------------------------------------------------------- -# Python test function related hooks -# ------------------------------------------------------------------------- - -def pytest_pycollect_makemodule(path, parent): - """ return a Module collector or None for the given path. - This hook will be called for each matching test module path. - The pytest_collect_file hook needs to be used if you want to - create test modules for files that do not match as a test module. - """ -pytest_pycollect_makemodule.firstresult = True - -def pytest_pycollect_makeitem(collector, name, obj): - """ return custom item/collector for a python object in a module, or None. """ -pytest_pycollect_makeitem.firstresult = True - -def pytest_pyfunc_call(pyfuncitem): - """ call underlying test function. """ -pytest_pyfunc_call.firstresult = True - -def pytest_generate_tests(metafunc): - """ generate (multiple) parametrized calls to a test function.""" - -# ------------------------------------------------------------------------- -# generic runtest related hooks -# ------------------------------------------------------------------------- -def pytest_itemstart(item, node=None): - """ (deprecated, use pytest_runtest_logstart). """ - -def pytest_runtest_protocol(item): - """ implements the standard runtest_setup/call/teardown protocol including - capturing exceptions and calling reporting hooks on the results accordingly. - - :return boolean: True if no further hook implementations should be invoked. - """ -pytest_runtest_protocol.firstresult = True - -def pytest_runtest_logstart(nodeid, location): - """ signal the start of a test run. """ - -def pytest_runtest_setup(item): - """ called before ``pytest_runtest_call(item)``. """ - -def pytest_runtest_call(item): - """ called to execute the test ``item``. """ - -def pytest_runtest_teardown(item): - """ called after ``pytest_runtest_call``. """ - -def pytest_runtest_makereport(item, call): - """ return a :py:class:`pytest.plugin.runner.TestReport` object - for the given :py:class:`pytest.Item` and - :py:class:`pytest.plugin.runner.CallInfo`. - """ -pytest_runtest_makereport.firstresult = True - -def pytest_runtest_logreport(report): - """ process item test report. """ - -# special handling for final teardown - somewhat internal for now -def pytest__teardown_final(session): - """ called before test session finishes. """ -pytest__teardown_final.firstresult = True - -def pytest__teardown_final_logerror(report, session): - """ called if runtest_teardown_final failed. """ - -# ------------------------------------------------------------------------- -# test session related hooks -# ------------------------------------------------------------------------- - -def pytest_sessionstart(session): - """ before session.main() is called. """ - -def pytest_sessionfinish(session, exitstatus): - """ whole test run finishes. """ - - -# ------------------------------------------------------------------------- -# hooks for customising the assert methods -# ------------------------------------------------------------------------- - -def pytest_assertrepr_compare(config, op, left, right): - """return explanation for comparisons in failing assert expressions. - - Return None for no custom explanation, otherwise return a list - of strings. The strings will be joined by newlines but any newlines - *in* a string will be escaped. Note that all but the first line will - be indented sligthly, the intention is for the first line to be a summary. - """ - -# ------------------------------------------------------------------------- -# hooks for influencing reporting (invoked from pytest_terminal) -# ------------------------------------------------------------------------- - -def pytest_report_header(config): - """ return a string to be displayed as header info for terminal reporting.""" - -def pytest_report_teststatus(report): - """ return result-category, shortletter and verbose word for reporting.""" -pytest_report_teststatus.firstresult = True - -def pytest_terminal_summary(terminalreporter): - """ add additional section in terminal summary reporting. """ - -# ------------------------------------------------------------------------- -# doctest hooks -# ------------------------------------------------------------------------- - -def pytest_doctest_prepare_content(content): - """ return processed content for a given doctest""" -pytest_doctest_prepare_content.firstresult = True - - -# ------------------------------------------------------------------------- -# error handling and internal debugging hooks -# ------------------------------------------------------------------------- - -def pytest_plugin_registered(plugin, manager): - """ a new py lib plugin got registered. """ - -def pytest_plugin_unregistered(plugin): - """ a py lib plugin got unregistered. """ - -def pytest_internalerror(excrepr): - """ called for internal errors. """ - -def pytest_keyboard_interrupt(excinfo): - """ called for keyboard interrupt. """ - -def pytest_trace(category, msg): - """ called for debug info. """ --- a/testing/plugin/test_monkeypatch.py +++ b/testing/plugin/test_monkeypatch.py @@ -1,6 +1,6 @@ import os, sys import py -from pytest.plugin.monkeypatch import monkeypatch as MonkeyPatch +from _pytest.monkeypatch import monkeypatch as MonkeyPatch def test_setattr(): class A: --- a/testing/plugin/test_recwarn.py +++ b/testing/plugin/test_recwarn.py @@ -1,5 +1,5 @@ import py -from pytest.plugin.recwarn import WarningsRecorder +from _pytest.recwarn import WarningsRecorder def test_WarningRecorder(recwarn): showwarning = py.std.warnings.showwarning --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -1,6 +1,6 @@ import pytest, py -from pytest.plugin.session import Session +from _pytest.session import Session class TestCollector: def test_collect_versus_item(self): --- a/pytest/__main__.py +++ /dev/null @@ -1,4 +0,0 @@ -import pytest - -if __name__ == '__main__': - raise SystemExit(pytest.main()) --- /dev/null +++ b/_pytest/__init__.py @@ -0,0 +1,1 @@ +# --- a/pytest/__init__.py +++ /dev/null @@ -1,14 +0,0 @@ -""" -unit and functional testing with Python. - -see http://pytest.org for documentation and details - -(c) Holger Krekel and others, 2004-2010 -""" -__version__ = '2.0.0.dev25' - -__all__ = ['cmdline', 'collect', 'main'] - -from pytest import main as cmdline -UsageError = cmdline.UsageError -main = cmdline.main --- a/doc/index.txt +++ b/doc/index.txt @@ -5,8 +5,8 @@ py.test: no-boilerplate testing with Pyt .. note:: version 2.0 introduces ``pytest`` as the main Python import name - but for historic reasons ``py.test`` remains fully valid and - represents the same package. + but for compatibility reasons you may continue to use ``py.test`` + in your test code. Welcome to ``py.test`` documentation: --- a/pytest/plugin/pastebin.py +++ /dev/null @@ -1,63 +0,0 @@ -""" submit failure or test session information to a pastebin service. """ -import py, sys - -class url: - base = "http://paste.pocoo.org" - xmlrpc = base + "/xmlrpc/" - show = base + "/show/" - -def pytest_addoption(parser): - group = parser.getgroup("terminal reporting") - group._addoption('--pastebin', metavar="mode", - action='store', dest="pastebin", default=None, - type="choice", choices=['failed', 'all'], - help="send failed|all info to Pocoo pastebin service.") - -def pytest_configure(__multicall__, config): - import tempfile - __multicall__.execute() - if config.option.pastebin == "all": - config._pastebinfile = tempfile.TemporaryFile('w+') - tr = config.pluginmanager.getplugin('terminalreporter') - oldwrite = tr._tw.write - def tee_write(s, **kwargs): - oldwrite(s, **kwargs) - config._pastebinfile.write(str(s)) - tr._tw.write = tee_write - -def pytest_unconfigure(config): - if hasattr(config, '_pastebinfile'): - config._pastebinfile.seek(0) - sessionlog = config._pastebinfile.read() - config._pastebinfile.close() - del config._pastebinfile - proxyid = getproxy().newPaste("python", sessionlog) - pastebinurl = "%s%s" % (url.show, proxyid) - sys.stderr.write("pastebin session-log: %s\n" % pastebinurl) - tr = config.pluginmanager.getplugin('terminalreporter') - del tr._tw.__dict__['write'] - -def getproxy(): - return py.std.xmlrpclib.ServerProxy(url.xmlrpc).pastes - -def pytest_terminal_summary(terminalreporter): - if terminalreporter.config.option.pastebin != "failed": - return - tr = terminalreporter - if 'failed' in tr.stats: - terminalreporter.write_sep("=", "Sending information to Paste Service") - if tr.config.option.debug: - terminalreporter.write_line("xmlrpcurl: %s" %(url.xmlrpc,)) - serverproxy = getproxy() - for rep in terminalreporter.stats.get('failed'): - try: - msg = rep.longrepr.reprtraceback.reprentries[-1].reprfileloc - except AttributeError: - msg = tr._getfailureheadline(rep) - tw = py.io.TerminalWriter(stringio=True) - rep.toterminal(tw) - s = tw.stringio.getvalue() - assert len(s) - proxyid = serverproxy.newPaste("python", s) - pastebinurl = "%s%s" % (url.show, proxyid) - tr.write_line("%s --> %s" %(msg, pastebinurl)) --- /dev/null +++ b/_pytest/runner.py @@ -0,0 +1,373 @@ +""" basic collect and runtest protocol implementations """ + +import py, sys +from py._code.code import TerminalRepr + +def pytest_namespace(): + return { + 'fail' : fail, + 'skip' : skip, + 'importorskip' : importorskip, + 'exit' : exit, + } + +# +# pytest plugin hooks + +# XXX move to pytest_sessionstart and fix py.test owns tests +def pytest_configure(config): + config._setupstate = SetupState() + +def pytest_sessionfinish(session, exitstatus): + if hasattr(session.config, '_setupstate'): + hook = session.config.hook + rep = hook.pytest__teardown_final(session=session) + if rep: + hook.pytest__teardown_final_logerror(session=session, report=rep) + session.exitstatus = 1 + +class NodeInfo: + def __init__(self, location): + self.location = location + +def pytest_runtest_protocol(item): + item.ihook.pytest_runtest_logstart( + nodeid=item.nodeid, location=item.location, + ) + runtestprotocol(item) + return True + +def runtestprotocol(item, log=True): + 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)) + return reports + +def pytest_runtest_setup(item): + item.config._setupstate.prepare(item) + +def pytest_runtest_call(item): + item.runtest() + +def pytest_runtest_teardown(item): + item.config._setupstate.teardown_exact(item) + +def pytest__teardown_final(session): + call = CallInfo(session.config._setupstate.teardown_all, when="teardown") + if call.excinfo: + ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir) + call.excinfo.traceback = ntraceback.filter() + longrepr = call.excinfo.getrepr(funcargs=True) + return TeardownErrorReport(longrepr) + +def pytest_report_teststatus(report): + if report.when in ("setup", "teardown"): + if report.failed: + # category, shortletter, verbose-word + return "error", "E", "ERROR" + elif report.skipped: + return "skipped", "s", "SKIPPED" + else: + return "", "", "" + + +# +# Implementation + +def call_and_report(item, when, log=True): + call = call_runtest_hook(item, when) + hook = item.ihook + report = hook.pytest_runtest_makereport(item=item, call=call) + if log and (when == "call" or not report.passed): + hook.pytest_runtest_logreport(report=report) + return report + +def call_runtest_hook(item, when): + hookname = "pytest_runtest_" + when + ihook = getattr(item.ihook, hookname) + return CallInfo(lambda: ihook(item=item), when=when) + +class CallInfo: + """ Result/Exception info a function invocation. """ + #: None or ExceptionInfo object. + excinfo = None + def __init__(self, func, when): + #: context of invocation: one of "setup", "call", + #: "teardown", "memocollect" + self.when = when + try: + self.result = func() + except KeyboardInterrupt: + raise + except: + self.excinfo = py.code.ExceptionInfo() + + def __repr__(self): + if self.excinfo: + status = "exception: %s" % str(self.excinfo.value) + else: + status = "result: %r" % (self.result,) + return "" % (self.when, status) + +class BaseReport(object): + def toterminal(self, out): + longrepr = self.longrepr + if hasattr(longrepr, 'toterminal'): + longrepr.toterminal(out) + else: + out.line(str(longrepr)) + + passed = property(lambda x: x.outcome == "passed") + failed = property(lambda x: x.outcome == "failed") + skipped = property(lambda x: x.outcome == "skipped") + + @property + def fspath(self): + return self.nodeid.split("::")[0] + +def pytest_runtest_makereport(item, call): + when = call.when + keywords = dict([(x,1) for x in item.keywords]) + if not call.excinfo: + outcome = "passed" + longrepr = None + else: + excinfo = call.excinfo + if not isinstance(excinfo, py.code.ExceptionInfo): + outcome = "failed" + longrepr = excinfo + elif excinfo.errisinstance(py.test.skip.Exception): + outcome = "skipped" + longrepr = item._repr_failure_py(excinfo) + else: + outcome = "failed" + if call.when == "call": + longrepr = item.repr_failure(excinfo) + else: # exception in setup or teardown + longrepr = item._repr_failure_py(excinfo) + return TestReport(item.nodeid, item.location, + keywords, outcome, longrepr, when) + +class TestReport(BaseReport): + """ Basic test report object (also used for setup and teardown calls if + they fail). + """ + def __init__(self, nodeid, location, + keywords, outcome, longrepr, when): + #: normalized collection node id + self.nodeid = nodeid + + #: a (filesystempath, lineno, domaininfo) tuple indicating the + #: actual location of a test item - it might be different from the + #: collected one e.g. if a method is inherited from a different module. + self.location = location + + #: a name -> value dictionary containing all keywords and + #: markers associated with a test invocation. + self.keywords = keywords + + #: test outcome, always one of "passed", "failed", "skipped". + self.outcome = outcome + + #: None or a failure representation. + self.longrepr = longrepr + + #: one of 'setup', 'call', 'teardown' to indicate runtest phase. + self.when = when + + def __repr__(self): + return "" % ( + self.nodeid, self.when, self.outcome) + +class TeardownErrorReport(BaseReport): + outcome = "failed" + when = "teardown" + def __init__(self, longrepr): + self.longrepr = longrepr + +def pytest_make_collect_report(collector): + call = CallInfo(collector._memocollect, "memocollect") + reason = longrepr = None + if not call.excinfo: + outcome = "passed" + else: + if call.excinfo.errisinstance(py.test.skip.Exception): + outcome = "skipped" + reason = str(call.excinfo.value) + longrepr = collector._repr_failure_py(call.excinfo, "line") + else: + outcome = "failed" + errorinfo = collector.repr_failure(call.excinfo) + if not hasattr(errorinfo, "toterminal"): + errorinfo = CollectErrorRepr(errorinfo) + longrepr = errorinfo + return CollectReport(collector.nodeid, outcome, longrepr, + getattr(call, 'result', None), reason) + +class CollectReport(BaseReport): + def __init__(self, nodeid, outcome, longrepr, result, reason): + self.nodeid = nodeid + self.outcome = outcome + self.longrepr = longrepr + self.result = result or [] + self.reason = reason + + @property + def location(self): + return (self.fspath, None, self.fspath) + + def __repr__(self): + return "" % ( + self.nodeid, len(self.result), self.outcome) + +class CollectErrorRepr(TerminalRepr): + def __init__(self, msg): + self.longrepr = msg + def toterminal(self, out): + out.line(str(self.longrepr), red=True) + +class SetupState(object): + """ shared state for setting up/tearing down test items or collectors. """ + def __init__(self): + self.stack = [] + self._finalizers = {} + + def addfinalizer(self, finalizer, colitem): + """ attach a finalizer to the given colitem. + if colitem is None, this will add a finalizer that + is called at the end of teardown_all(). + """ + assert hasattr(finalizer, '__call__') + #assert colitem in self.stack + self._finalizers.setdefault(colitem, []).append(finalizer) + + def _pop_and_teardown(self): + colitem = self.stack.pop() + self._teardown_with_finalization(colitem) + + def _callfinalizers(self, colitem): + finalizers = self._finalizers.pop(colitem, None) + while finalizers: + fin = finalizers.pop() + fin() + + def _teardown_with_finalization(self, colitem): + self._callfinalizers(colitem) + if colitem: + colitem.teardown() + for colitem in self._finalizers: + assert colitem is None or colitem in self.stack + + def teardown_all(self): + while self.stack: + self._pop_and_teardown() + self._teardown_with_finalization(None) + assert not self._finalizers + + def teardown_exact(self, item): + if self.stack and item == self.stack[-1]: + self._pop_and_teardown() + else: + self._callfinalizers(item) + + def prepare(self, colitem): + """ setup objects along the collector chain to the test-method + and teardown previously setup objects.""" + needed_collectors = colitem.listchain() + while self.stack: + if self.stack == needed_collectors[:len(self.stack)]: + break + self._pop_and_teardown() + # check if the last collection node has raised an error + for col in self.stack: + if hasattr(col, '_prepare_exc'): + py.builtin._reraise(*col._prepare_exc) + for col in needed_collectors[len(self.stack):]: + self.stack.append(col) + try: + col.setup() + except Exception: + col._prepare_exc = sys.exc_info() + raise + +# ============================================================= +# Test OutcomeExceptions and helpers for creating them. + + +class OutcomeException(Exception): + """ OutcomeException and its subclass instances indicate and + contain info about test and collection outcomes. + """ + def __init__(self, msg=None): + self.msg = msg + + def __repr__(self): + if self.msg: + return str(self.msg) + return "<%s instance>" %(self.__class__.__name__,) + __str__ = __repr__ + +class Skipped(OutcomeException): + # XXX hackish: on 3k we fake to live in the builtins + # in order to have Skipped exception printing shorter/nicer + __module__ = 'builtins' + +class Failed(OutcomeException): + """ raised from an explicit call to py.test.fail() """ + __module__ = 'builtins' + +class Exit(KeyboardInterrupt): + """ raised for immediate program exits (no tracebacks/summaries)""" + def __init__(self, msg="unknown reason"): + self.msg = msg + KeyboardInterrupt.__init__(self, msg) + +# exposed helper methods + +def exit(msg): + """ exit testing process as if KeyboardInterrupt was triggered. """ + __tracebackhide__ = True + raise Exit(msg) + +exit.Exception = Exit + +def skip(msg=""): + """ skip an executing test with the given message. Note: it's usually + better to use the py.test.mark.skipif marker to declare a test to be + skipped under certain conditions like mismatching platforms or + dependencies. See the pytest_skipping plugin for details. + """ + __tracebackhide__ = True + raise Skipped(msg=msg) +skip.Exception = Skipped + +def fail(msg=""): + """ explicitely fail an currently-executing test with the given Message. """ + __tracebackhide__ = True + raise Failed(msg=msg) +fail.Exception = Failed + + +def importorskip(modname, minversion=None): + """ return imported module if it has a higher __version__ than the + optionally specified 'minversion' - otherwise call py.test.skip() + with a message detailing the mismatch. + """ + compile(modname, '', 'eval') # to catch syntaxerrors + try: + mod = __import__(modname, None, None, ['__doc__']) + except ImportError: + py.test.skip("could not import %r" %(modname,)) + if minversion is None: + return mod + verattr = getattr(mod, '__version__', None) + if isinstance(minversion, str): + minver = minversion.split(".") + else: + minver = list(minversion) + if verattr is None or verattr.split(".") < minver: + py.test.skip("module %r has __version__ %r, required is: %r" %( + modname, verattr, minversion)) + return mod --- /dev/null +++ b/testing/test_core.py @@ -0,0 +1,634 @@ +import py, os +from _pytest.core import PluginManager, canonical_importname +from _pytest.core import MultiCall, HookRelay, varnames + + +class TestBootstrapping: + def test_consider_env_fails_to_import(self, monkeypatch): + pluginmanager = PluginManager() + monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") + py.test.raises(ImportError, "pluginmanager.consider_env()") + + def test_preparse_args(self): + pluginmanager = PluginManager() + py.test.raises(ImportError, """ + pluginmanager.consider_preparse(["xyz", "-p", "hello123"]) + """) + + def test_plugin_skip(self, testdir, monkeypatch): + p = testdir.makepyfile(pytest_skipping1=""" + import py + py.test.skip("hello") + """) + p.copy(p.dirpath("pytest_skipping2.py")) + monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") + result = testdir.runpytest("-p", "skipping1", "--traceconfig") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*hint*skipping2*hello*", + "*hint*skipping1*hello*", + ]) + + def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): + pluginmanager = PluginManager() + testdir.syspathinsert() + testdir.makepyfile(pytest_xy123="#") + monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') + l1 = len(pluginmanager.getplugins()) + pluginmanager.consider_env() + l2 = len(pluginmanager.getplugins()) + assert l2 == l1 + 1 + assert pluginmanager.getplugin('pytest_xy123') + pluginmanager.consider_env() + l3 = len(pluginmanager.getplugins()) + assert l2 == l3 + + def test_consider_setuptools_instantiation(self, monkeypatch): + pkg_resources = py.test.importorskip("pkg_resources") + def my_iter(name): + assert name == "pytest11" + class EntryPoint: + name = "mytestplugin" + def load(self): + class PseudoPlugin: + x = 42 + return PseudoPlugin() + return iter([EntryPoint()]) + + monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) + pluginmanager = PluginManager() + pluginmanager.consider_setuptools_entrypoints() + plugin = pluginmanager.getplugin("mytestplugin") + assert plugin.x == 42 + plugin2 = pluginmanager.getplugin("pytest_mytestplugin") + assert plugin2 == plugin + + def test_consider_setuptools_not_installed(self, monkeypatch): + monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', + py.std.types.ModuleType("pkg_resources")) + pluginmanager = PluginManager() + pluginmanager.consider_setuptools_entrypoints() + # ok, we did not explode + + def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): + x500 = testdir.makepyfile(pytest_x500="#") + p = testdir.makepyfile(""" + import py + def test_hello(pytestconfig): + plugin = pytestconfig.pluginmanager.getplugin('x500') + assert plugin is not None + """) + monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") + result = testdir.runpytest(p) + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed in*"]) + + def test_import_plugin_importname(self, testdir): + pluginmanager = PluginManager() + py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') + + reset = testdir.syspathinsert() + pluginname = "pytest_hello" + testdir.makepyfile(**{pluginname: ""}) + pluginmanager.import_plugin("hello") + len1 = len(pluginmanager.getplugins()) + pluginmanager.import_plugin("pytest_hello") + len2 = len(pluginmanager.getplugins()) + assert len1 == len2 + plugin1 = pluginmanager.getplugin("pytest_hello") + assert plugin1.__name__.endswith('pytest_hello') + plugin2 = pluginmanager.getplugin("hello") + assert plugin2 is plugin1 + + def test_import_plugin_dotted_name(self, testdir): + pluginmanager = PluginManager() + py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') + + reset = testdir.syspathinsert() + testdir.mkpydir("pkg").join("plug.py").write("x=3") + pluginname = "pkg.plug" + pluginmanager.import_plugin(pluginname) + mod = pluginmanager.getplugin("pkg.plug") + assert mod.x == 3 + + def test_consider_module(self, testdir): + pluginmanager = PluginManager() + testdir.syspathinsert() + testdir.makepyfile(pytest_plug1="#") + testdir.makepyfile(pytest_plug2="#") + mod = py.std.types.ModuleType("temp") + mod.pytest_plugins = ["pytest_plug1", "pytest_plug2"] + pluginmanager.consider_module(mod) + assert pluginmanager.getplugin("plug1").__name__ == "pytest_plug1" + assert pluginmanager.getplugin("plug2").__name__ == "pytest_plug2" + + def test_consider_module_import_module(self, testdir): + mod = py.std.types.ModuleType("x") + mod.pytest_plugins = "pytest_a" + aplugin = testdir.makepyfile(pytest_a="#") + pluginmanager = PluginManager() + reprec = testdir.getreportrecorder(pluginmanager) + #syspath.prepend(aplugin.dirpath()) + py.std.sys.path.insert(0, str(aplugin.dirpath())) + pluginmanager.consider_module(mod) + call = reprec.getcall(pluginmanager.hook.pytest_plugin_registered.name) + assert call.plugin.__name__ == "pytest_a" + + # check that it is not registered twice + pluginmanager.consider_module(mod) + l = reprec.getcalls("pytest_plugin_registered") + assert len(l) == 1 + + def test_config_sets_conftesthandle_onimport(self, testdir): + config = testdir.parseconfig([]) + assert config._conftest._onimport == config._onimportconftest + + def test_consider_conftest_deps(self, testdir): + mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() + pp = PluginManager() + py.test.raises(ImportError, "pp.consider_conftest(mod)") + + def test_pm(self): + pp = PluginManager() + class A: pass + a1, a2 = A(), A() + pp.register(a1) + assert pp.isregistered(a1) + pp.register(a2, "hello") + assert pp.isregistered(a2) + l = pp.getplugins() + assert a1 in l + assert a2 in l + assert pp.getplugin('hello') == a2 + pp.unregister(a1) + assert not pp.isregistered(a1) + pp.unregister(name="hello") + assert not pp.isregistered(a2) + + def test_pm_ordering(self): + pp = PluginManager() + class A: pass + a1, a2 = A(), A() + pp.register(a1) + pp.register(a2, "hello") + l = pp.getplugins() + assert l.index(a1) < l.index(a2) + a3 = A() + pp.register(a3, prepend=True) + l = pp.getplugins() + assert l.index(a3) == 0 + + def test_register_imported_modules(self): + pp = PluginManager() + mod = py.std.types.ModuleType("x.y.pytest_hello") + pp.register(mod) + assert pp.isregistered(mod) + l = pp.getplugins() + assert mod in l + py.test.raises(AssertionError, "pp.register(mod)") + mod2 = py.std.types.ModuleType("pytest_hello") + #pp.register(mod2) # double pm + py.test.raises(AssertionError, "pp.register(mod)") + #assert not pp.isregistered(mod2) + assert pp.getplugins() == l + + def test_canonical_import(self, monkeypatch): + mod = py.std.types.ModuleType("pytest_xyz") + monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) + pp = PluginManager() + pp.import_plugin('xyz') + assert pp.getplugin('xyz') == mod + assert pp.getplugin('pytest_xyz') == mod + assert pp.isregistered(mod) + + def test_register_mismatch_method(self): + pp = PluginManager(load=True) + class hello: + def pytest_gurgel(self): + pass + py.test.raises(Exception, "pp.register(hello())") + + def test_register_mismatch_arg(self): + pp = PluginManager(load=True) + class hello: + def pytest_configure(self, asd): + pass + excinfo = py.test.raises(Exception, "pp.register(hello())") + + def test_canonical_importname(self): + for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': + impname = canonical_importname(name) + + def test_notify_exception(self, capfd): + pp = PluginManager() + excinfo = py.test.raises(ValueError, "raise ValueError(1)") + pp.notify_exception(excinfo) + out, err = capfd.readouterr() + assert "ValueError" in err + class A: + def pytest_internalerror(self, excrepr): + return True + pp.register(A()) + pp.notify_exception(excinfo) + out, err = capfd.readouterr() + assert not err + + def test_register(self): + pm = PluginManager(load=False) + class MyPlugin: + pass + my = MyPlugin() + pm.register(my) + assert pm.getplugins() + my2 = MyPlugin() + pm.register(my2) + assert pm.getplugins()[1:] == [my, my2] + + assert pm.isregistered(my) + assert pm.isregistered(my2) + pm.unregister(my) + assert not pm.isregistered(my) + assert pm.getplugins()[1:] == [my2] + + def test_listattr(self): + plugins = PluginManager() + class api1: + x = 41 + class api2: + x = 42 + class api3: + x = 43 + plugins.register(api1()) + plugins.register(api2()) + plugins.register(api3()) + l = list(plugins.listattr('x')) + assert l == [41, 42, 43] + + def test_hook_tracing(self): + pm = PluginManager() + saveindent = [] + class api1: + x = 41 + def pytest_plugin_registered(self, plugin): + saveindent.append(pm.trace.root.indent) + raise ValueError(42) + l = [] + pm.trace.root.setwriter(l.append) + indent = pm.trace.root.indent + p = api1() + pm.register(p) + + assert pm.trace.root.indent == indent + assert len(l) == 1 + assert 'pytest_plugin_registered' in l[0] + py.test.raises(ValueError, lambda: pm.register(api1())) + assert pm.trace.root.indent == indent + assert saveindent[0] > indent + +class TestPytestPluginInteractions: + + def test_addhooks_conftestplugin(self, testdir): + newhooks = testdir.makepyfile(newhooks=""" + def pytest_myhook(xyz): + "new hook" + """) + conf = testdir.makeconftest(""" + import sys ; sys.path.insert(0, '.') + import newhooks + def pytest_addhooks(pluginmanager): + pluginmanager.addhooks(newhooks) + def pytest_myhook(xyz): + return xyz + 1 + """) + config = testdir.Config() + config._conftest.importconftest(conf) + print(config.pluginmanager.getplugins()) + res = config.hook.pytest_myhook(xyz=10) + assert res == [11] + + def test_addhooks_docstring_error(self, testdir): + newhooks = testdir.makepyfile(newhooks=""" + class A: # no pytest_ prefix + pass + def pytest_myhook(xyz): + pass + """) + conf = testdir.makeconftest(""" + import sys ; sys.path.insert(0, '.') + import newhooks + def pytest_addhooks(pluginmanager): + pluginmanager.addhooks(newhooks) + """) + res = testdir.runpytest() + assert res.ret != 0 + res.stderr.fnmatch_lines([ + "*docstring*pytest_myhook*newhooks*" + ]) + + def test_addhooks_nohooks(self, testdir): + conf = testdir.makeconftest(""" + import sys + def pytest_addhooks(pluginmanager): + pluginmanager.addhooks(sys) + """) + res = testdir.runpytest() + assert res.ret != 0 + res.stderr.fnmatch_lines([ + "*did not find*sys*" + ]) + + def test_do_option_conftestplugin(self, testdir): + p = testdir.makepyfile(""" + def pytest_addoption(parser): + parser.addoption('--test123', action="store_true") + """) + config = testdir.Config() + config._conftest.importconftest(p) + print(config.pluginmanager.getplugins()) + config.parse([]) + assert not config.option.test123 + + def test_namespace_early_from_import(self, testdir): + p = testdir.makepyfile(""" + from pytest import Item + from pytest import Item as Item2 + assert Item is Item2 + """) + result = testdir.runpython(p) + assert result.ret == 0 + + def test_do_ext_namespace(self, testdir): + testdir.makeconftest(""" + def pytest_namespace(): + return {'hello': 'world'} + """) + p = testdir.makepyfile(""" + from py.test import hello + import py + def test_hello(): + assert hello == "world" + assert 'hello' in py.test.__all__ + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + + def test_do_option_postinitialize(self, testdir): + config = testdir.Config() + config.parse([]) + config.pluginmanager.do_configure(config=config) + assert not hasattr(config.option, 'test123') + p = testdir.makepyfile(""" + def pytest_addoption(parser): + parser.addoption('--test123', action="store_true", + default=True) + """) + config._conftest.importconftest(p) + assert config.option.test123 + + def test_configure(self, testdir): + config = testdir.parseconfig() + l = [] + class A: + def pytest_configure(self, config): + l.append(self) + + config.pluginmanager.register(A()) + assert len(l) == 0 + config.pluginmanager.do_configure(config=config) + assert len(l) == 1 + config.pluginmanager.register(A()) # this should lead to a configured() plugin + assert len(l) == 2 + assert l[0] != l[1] + + config.pluginmanager.do_unconfigure(config=config) + config.pluginmanager.register(A()) + assert len(l) == 2 + + # lower level API + + def test_listattr(self): + pluginmanager = PluginManager() + class My2: + x = 42 + pluginmanager.register(My2()) + assert not pluginmanager.listattr("hello") + assert pluginmanager.listattr("x") == [42] + +def test_namespace_has_default_and_env_plugins(testdir): + p = testdir.makepyfile(""" + import py + py.test.mark + """) + result = testdir.runpython(p) + assert result.ret == 0 + +def test_varnames(): + def f(x): + i = 3 + class A: + def f(self, y): + pass + class B(object): + def __call__(self, z): + pass + assert varnames(f) == ("x",) + assert varnames(A().f) == ('y',) + assert varnames(B()) == ('z',) + +class TestMultiCall: + def test_uses_copy_of_methods(self): + l = [lambda: 42] + mc = MultiCall(l, {}) + repr(mc) + l[:] = [] + res = mc.execute() + return res == 42 + + def test_call_passing(self): + class P1: + def m(self, __multicall__, x): + assert len(__multicall__.results) == 1 + assert not __multicall__.methods + return 17 + + class P2: + def m(self, __multicall__, x): + assert __multicall__.results == [] + assert __multicall__.methods + return 23 + + p1 = P1() + p2 = P2() + multicall = MultiCall([p1.m, p2.m], {'x': 23}) + assert "23" in repr(multicall) + reslist = multicall.execute() + assert len(reslist) == 2 + # ensure reversed order + assert reslist == [23, 17] + + def test_keyword_args(self): + def f(x): + return x + 1 + class A: + def f(self, x, y): + return x + y + multicall = MultiCall([f, A().f], dict(x=23, y=24)) + assert "'x': 23" in repr(multicall) + assert "'y': 24" in repr(multicall) + reslist = multicall.execute() + assert reslist == [24+23, 24] + assert "2 results" in repr(multicall) + + def test_keyword_args_with_defaultargs(self): + def f(x, z=1): + return x + z + reslist = MultiCall([f], dict(x=23, y=24)).execute() + assert reslist == [24] + reslist = MultiCall([f], dict(x=23, z=2)).execute() + assert reslist == [25] + + def test_tags_call_error(self): + multicall = MultiCall([lambda x: x], {}) + py.test.raises(TypeError, "multicall.execute()") + + def test_call_subexecute(self): + def m(__multicall__): + subresult = __multicall__.execute() + return subresult + 1 + + def n(): + return 1 + + call = MultiCall([n, m], {}, firstresult=True) + res = call.execute() + assert res == 2 + + def test_call_none_is_no_result(self): + def m1(): + return 1 + def m2(): + return None + res = MultiCall([m1, m2], {}, firstresult=True).execute() + assert res == 1 + res = MultiCall([m1, m2], {}).execute() + assert res == [1] + +class TestHookRelay: + def test_happypath(self): + pm = PluginManager() + class Api: + def hello(self, arg): + "api hook 1" + + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") + assert hasattr(mcm, 'hello') + assert repr(mcm.hello).find("hello") != -1 + class Plugin: + def hello(self, arg): + return arg + 1 + pm.register(Plugin()) + l = mcm.hello(arg=3) + assert l == [4] + assert not hasattr(mcm, 'world') + + def test_only_kwargs(self): + pm = PluginManager() + class Api: + def hello(self, arg): + "api hook 1" + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") + py.test.raises(TypeError, "mcm.hello(3)") + + def test_firstresult_definition(self): + pm = PluginManager() + class Api: + def hello(self, arg): + "api hook 1" + hello.firstresult = True + + mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") + class Plugin: + def hello(self, arg): + return arg + 1 + pm.register(Plugin()) + res = mcm.hello(arg=3) + assert res == 4 + +class TestTracer: + def test_simple(self): + from _pytest.core import TagTracer + rootlogger = TagTracer("[my] ") + log = rootlogger.get("pytest") + log("hello") + l = [] + rootlogger.setwriter(l.append) + log("world") + assert len(l) == 1 + assert l[0] == "[my] world\n" + sublog = log.get("collection") + sublog("hello") + assert l[1] == "[my] hello\n" + + def test_indent(self): + from _pytest.core import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + l = [] + log.root.setwriter(lambda arg: l.append(arg)) + log("hello") + log.root.indent += 1 + log("line1") + log("line2") + log.root.indent += 1 + log("line3") + log("line4") + log.root.indent -= 1 + log("line5") + log.root.indent -= 1 + log("last") + assert len(l) == 7 + names = [x.rstrip()[len(rootlogger.prefix):] for x in l] + assert names == ['hello', ' line1', ' line2', + ' line3', ' line4', ' line5', 'last'] + + def test_setprocessor(self): + from _pytest.core import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + assert log2.tags == tuple("12") + l = [] + rootlogger.setprocessor(tuple("12"), lambda *args: l.append(args)) + log("not seen") + log2("seen") + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == ("seen",) + l2 = [] + rootlogger.setprocessor("1:2", lambda *args: l2.append(args)) + log2("seen") + tags, args = l2[0] + assert args == ("seen",) + + + def test_setmyprocessor(self): + from _pytest.core import TagTracer + rootlogger = TagTracer() + log = rootlogger.get("1") + log2 = log.get("2") + l = [] + log2.setmyprocessor(lambda *args: l.append(args)) + log("not seen") + assert not l + log2(42) + assert len(l) == 1 + tags, args = l[0] + assert "1" in tags + assert "2" in tags + assert args == (42,) --- /dev/null +++ b/_pytest/recwarn.py @@ -0,0 +1,96 @@ +""" recording warnings during test function execution. """ + +import py +import sys, os + +def pytest_funcarg__recwarn(request): + """Return a WarningsRecorder instance that provides these methods: + + * ``pop(category=None)``: return last warning matching the category. + * ``clear()``: clear list of warnings + """ + if sys.version_info >= (2,7): + import warnings + oldfilters = warnings.filters[:] + warnings.simplefilter('default') + def reset_filters(): + warnings.filters[:] = oldfilters + request.addfinalizer(reset_filters) + wrec = WarningsRecorder() + request.addfinalizer(wrec.finalize) + return wrec + +def pytest_namespace(): + return {'deprecated_call': deprecated_call} + +def deprecated_call(func, *args, **kwargs): + """ assert that calling func(*args, **kwargs) + triggers a DeprecationWarning. + """ + warningmodule = py.std.warnings + l = [] + oldwarn_explicit = getattr(warningmodule, 'warn_explicit') + def warn_explicit(*args, **kwargs): + l.append(args) + oldwarn_explicit(*args, **kwargs) + oldwarn = getattr(warningmodule, 'warn') + def warn(*args, **kwargs): + l.append(args) + oldwarn(*args, **kwargs) + + warningmodule.warn_explicit = warn_explicit + warningmodule.warn = warn + try: + ret = func(*args, **kwargs) + finally: + warningmodule.warn_explicit = warn_explicit + warningmodule.warn = warn + if not l: + #print warningmodule + __tracebackhide__ = True + raise AssertionError("%r did not produce DeprecationWarning" %(func,)) + return ret + + +class RecordedWarning: + def __init__(self, message, category, filename, lineno, line): + self.message = message + self.category = category + self.filename = filename + self.lineno = lineno + self.line = line + +class WarningsRecorder: + def __init__(self): + warningmodule = py.std.warnings + self.list = [] + def showwarning(message, category, filename, lineno, line=0): + self.list.append(RecordedWarning( + message, category, filename, lineno, line)) + try: + self.old_showwarning(message, category, + filename, lineno, line=line) + except TypeError: + # < python2.6 + self.old_showwarning(message, category, filename, lineno) + self.old_showwarning = warningmodule.showwarning + warningmodule.showwarning = showwarning + + def pop(self, cls=Warning): + """ pop the first recorded warning, raise exception if not exists.""" + for i, w in enumerate(self.list): + if issubclass(w.category, cls): + return self.list.pop(i) + __tracebackhide__ = True + assert 0, "%r not found in %r" %(cls, self.list) + + #def resetregistry(self): + # import warnings + # warnings.onceregistry.clear() + # warnings.__warningregistry__.clear() + + def clear(self): + self.list[:] = [] + + def finalize(self): + py.std.warnings.showwarning = self.old_showwarning --- /dev/null +++ b/_pytest/session.py @@ -0,0 +1,511 @@ +""" core implementation of testing process: init, session, runtest loop. """ + +import py +import pytest, _pytest +import os, sys +tracebackcutdir = py.path.local(_pytest.__file__).dirpath() + +# exitcodes for the command line +EXIT_OK = 0 +EXIT_TESTSFAILED = 1 +EXIT_INTERRUPTED = 2 +EXIT_INTERNALERROR = 3 + +def pytest_addoption(parser): + parser.addini("norecursedirs", "directory patterns to avoid for recursion", + type="args", default=('.*', 'CVS', '_darcs', '{arch}')) + #parser.addini("dirpatterns", + # "patterns specifying possible locations of test files", + # type="linelist", default=["**/test_*.txt", + # "**/test_*.py", "**/*_test.py"] + #) + group = parser.getgroup("general", "running and selection options") + group._addoption('-x', '--exitfirst', action="store_true", default=False, + dest="exitfirst", + help="exit instantly on first error or failed test."), + group._addoption('--maxfail', metavar="num", + action="store", type="int", dest="maxfail", default=0, + help="exit after first num failures or errors.") + + group = parser.getgroup("collect", "collection") + group.addoption('--collectonly', + action="store_true", dest="collectonly", + help="only collect tests, don't execute them."), + group.addoption('--pyargs', action="store_true", + help="try to interpret all arguments as python packages.") + group.addoption("--ignore", action="append", metavar="path", + help="ignore path during collection (multi-allowed).") + group.addoption('--confcutdir', dest="confcutdir", default=None, + metavar="dir", + help="only load conftest.py's relative to specified dir.") + + group = parser.getgroup("debugconfig", + "test process debugging and configuration") + group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir", + help="base temporary directory for this test run.") + + +def pytest_namespace(): + return dict(collect=dict(Item=Item, Collector=Collector, File=File)) + +def pytest_configure(config): + py.test.config = config # compatibiltiy + if config.option.exitfirst: + config.option.maxfail = 1 + +def pytest_cmdline_main(config): + """ default command line protocol for initialization, session, + running tests and reporting. """ + session = Session(config) + session.exitstatus = EXIT_OK + try: + config.pluginmanager.do_configure(config) + config.hook.pytest_sessionstart(session=session) + config.hook.pytest_collection(session=session) + config.hook.pytest_runtestloop(session=session) + except pytest.UsageError: + raise + except KeyboardInterrupt: + excinfo = py.code.ExceptionInfo() + config.hook.pytest_keyboard_interrupt(excinfo=excinfo) + session.exitstatus = EXIT_INTERRUPTED + except: + excinfo = py.code.ExceptionInfo() + config.pluginmanager.notify_exception(excinfo) + session.exitstatus = EXIT_INTERNALERROR + if excinfo.errisinstance(SystemExit): + sys.stderr.write("mainloop: caught Spurious SystemExit!\n") + if not session.exitstatus and session._testsfailed: + session.exitstatus = EXIT_TESTSFAILED + config.hook.pytest_sessionfinish(session=session, + exitstatus=session.exitstatus) + config.pluginmanager.do_unconfigure(config) + return session.exitstatus + +def pytest_collection(session): + session.perform_collect() + hook = session.config.hook + hook.pytest_collection_modifyitems(session=session, + config=session.config, items=session.items) + hook.pytest_collection_finish(session=session) + return True + +def pytest_runtestloop(session): + if session.config.option.collectonly: + return True + for item in session.session.items: + item.config.hook.pytest_runtest_protocol(item=item) + if session.shouldstop: + raise session.Interrupted(session.shouldstop) + return True + +def pytest_ignore_collect(path, config): + p = path.dirpath() + ignore_paths = config._getconftest_pathlist("collect_ignore", path=p) + ignore_paths = ignore_paths or [] + excludeopt = config.getvalue("ignore") + if excludeopt: + ignore_paths.extend([py.path.local(x) for x in excludeopt]) + return path in ignore_paths + +class HookProxy: + def __init__(self, fspath, config): + self.fspath = fspath + self.config = config + def __getattr__(self, name): + hookmethod = getattr(self.config.hook, name) + def call_matching_hooks(**kwargs): + plugins = self.config._getmatchingplugins(self.fspath) + return hookmethod.pcall(plugins, **kwargs) + return call_matching_hooks + +def compatproperty(name): + def fget(self): + #print "retrieving %r property from %s" %(name, self.fspath) + py.log._apiwarn("2.0", "use pytest.%s for " + "test collection and item classes" % name) + return getattr(pytest, name) + return property(fget, None, None, + "deprecated attribute %r, use pytest.%s" % (name,name)) + +class Node(object): + """ base class for all Nodes in the collection tree. + Collector subclasses have children, Items are terminal nodes.""" + + def __init__(self, name, parent=None, config=None, session=None): + #: a unique name with the scope of the parent + self.name = name + + #: the parent collector node. + self.parent = parent + + #: the test config object + self.config = config or parent.config + + #: the collection this node is part of + self.session = session or parent.session + + #: filesystem path where this node was collected from + self.fspath = getattr(parent, 'fspath', None) + self.ihook = self.session.gethookproxy(self.fspath) + self.keywords = {self.name: True} + + Module = compatproperty("Module") + Class = compatproperty("Class") + Function = compatproperty("Function") + File = compatproperty("File") + Item = compatproperty("Item") + + def __repr__(self): + return "<%s %r>" %(self.__class__.__name__, getattr(self, 'name', None)) + + # methods for ordering nodes + @property + def nodeid(self): + try: + return self._nodeid + except AttributeError: + self._nodeid = x = self._makeid() + return x + + def _makeid(self): + return self.parent.nodeid + "::" + self.name + + def __eq__(self, other): + if not isinstance(other, Node): + return False + return self.__class__ == other.__class__ and \ + self.name == other.name and self.parent == other.parent + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((self.name, self.parent)) + + def setup(self): + pass + + def teardown(self): + pass + + def _memoizedcall(self, attrname, function): + exattrname = "_ex_" + attrname + failure = getattr(self, exattrname, None) + if failure is not None: + py.builtin._reraise(failure[0], failure[1], failure[2]) + if hasattr(self, attrname): + return getattr(self, attrname) + try: + res = function() + except py.builtin._sysex: + raise + except: + failure = py.std.sys.exc_info() + setattr(self, exattrname, failure) + raise + setattr(self, attrname, res) + return res + + def listchain(self): + """ return list of all parent collectors up to self, + starting from root of collection tree. """ + l = [self] + while 1: + x = l[0] + if x.parent is not None: # and x.parent.parent is not None: + l.insert(0, x.parent) + else: + return l + + def listnames(self): + return [x.name for x in self.listchain()] + + def getparent(self, cls): + current = self + while current and not isinstance(current, cls): + current = current.parent + return current + + def _prunetraceback(self, excinfo): + pass + + def _repr_failure_py(self, excinfo, style=None): + if self.config.option.fulltrace: + style="long" + else: + self._prunetraceback(excinfo) + # XXX should excinfo.getrepr record all data and toterminal() + # process it? + if style is None: + if self.config.option.tbstyle == "short": + style = "short" + else: + style = "long" + return excinfo.getrepr(funcargs=True, + showlocals=self.config.option.showlocals, + style=style) + + repr_failure = _repr_failure_py + +class Collector(Node): + """ Collector instances create children through collect() + and thus iteratively build a tree. + """ + class CollectError(Exception): + """ an error during collection, contains a custom message. """ + + def collect(self): + """ returns a list of children (items and collectors) + for this collection node. + """ + raise NotImplementedError("abstract") + + def repr_failure(self, excinfo): + """ represent a collection failure. """ + if excinfo.errisinstance(self.CollectError): + exc = excinfo.value + return str(exc.args[0]) + return self._repr_failure_py(excinfo, style="short") + + def _memocollect(self): + """ internal helper method to cache results of calling collect(). """ + return self._memoizedcall('_collected', lambda: list(self.collect())) + + def _prunetraceback(self, excinfo): + if hasattr(self, 'fspath'): + path = self.fspath + traceback = excinfo.traceback + ntraceback = traceback.cut(path=self.fspath) + if ntraceback == traceback: + ntraceback = ntraceback.cut(excludepath=tracebackcutdir) + excinfo.traceback = ntraceback.filter() + +class FSCollector(Collector): + def __init__(self, fspath, parent=None, config=None, session=None): + fspath = py.path.local(fspath) # xxx only for test_resultlog.py? + name = fspath.basename + if parent is not None: + rel = fspath.relto(parent.fspath) + if rel: + name = rel + name = name.replace(os.sep, "/") + super(FSCollector, self).__init__(name, parent, config, session) + self.fspath = fspath + + def _makeid(self): + if self == self.session: + return "." + relpath = self.session.fspath.bestrelpath(self.fspath) + if os.sep != "/": + relpath = relpath.replace(os.sep, "/") + return relpath + +class File(FSCollector): + """ base class for collecting tests from a file. """ + +class Item(Node): + """ a basic test invocation item. Note that for a single function + there might be multiple test invocation items. + """ + def reportinfo(self): + return self.fspath, None, "" + + @property + def location(self): + try: + return self._location + except AttributeError: + location = self.reportinfo() + location = (str(location[0]), location[1], str(location[2])) + self._location = location + return location + +class NoMatch(Exception): + """ raised if matching cannot locate a matching names. """ + +class Session(FSCollector): + class Interrupted(KeyboardInterrupt): + """ signals an interrupted test run. """ + __module__ = 'builtins' # for py3 + + def __init__(self, config): + super(Session, self).__init__(py.path.local(), parent=None, + config=config, session=self) + self.config.pluginmanager.register(self, name="session", prepend=True) + self._testsfailed = 0 + self.shouldstop = False + self.trace = config.trace.root.get("collection") + self._norecursepatterns = config.getini("norecursedirs") + + def pytest_collectstart(self): + if self.shouldstop: + raise self.Interrupted(self.shouldstop) + + def pytest_runtest_logreport(self, report): + if report.failed and 'xfail' not in getattr(report, 'keywords', []): + self._testsfailed += 1 + maxfail = self.config.getvalue("maxfail") + if maxfail and self._testsfailed >= maxfail: + self.shouldstop = "stopping after %d failures" % ( + self._testsfailed) + pytest_collectreport = pytest_runtest_logreport + + def isinitpath(self, path): + return path in self._initialpaths + + def gethookproxy(self, fspath): + return HookProxy(fspath, self.config) + + def perform_collect(self, args=None, genitems=True): + if args is None: + args = self.config.args + self.trace("perform_collect", self, args) + self.trace.root.indent += 1 + self._notfound = [] + self._initialpaths = set() + self._initialparts = [] + for arg in args: + parts = self._parsearg(arg) + self._initialparts.append(parts) + self._initialpaths.add(parts[0]) + self.ihook.pytest_collectstart(collector=self) + rep = self.ihook.pytest_make_collect_report(collector=self) + self.ihook.pytest_collectreport(report=rep) + self.trace.root.indent -= 1 + if self._notfound: + for arg, exc in self._notfound: + line = "(no name %r in any of %r)" % (arg, exc.args[0]) + raise pytest.UsageError("not found: %s\n%s" %(arg, line)) + if not genitems: + return rep.result + else: + self.items = items = [] + if rep.passed: + for node in rep.result: + self.items.extend(self.genitems(node)) + return items + + def collect(self): + for parts in self._initialparts: + arg = "::".join(map(str, parts)) + self.trace("processing argument", arg) + self.trace.root.indent += 1 + try: + for x in self._collect(arg): + yield x + except NoMatch: + # we are inside a make_report hook so + # we cannot directly pass through the exception + self._notfound.append((arg, sys.exc_info()[1])) + self.trace.root.indent -= 1 + break + self.trace.root.indent -= 1 + + def _collect(self, arg): + names = self._parsearg(arg) + path = names.pop(0) + if path.check(dir=1): + assert not names, "invalid arg %r" %(arg,) + for path in path.visit(rec=self._recurse, bf=True, sort=True): + for x in self._collectfile(path): + yield x + else: + assert path.check(file=1) + for x in self.matchnodes(self._collectfile(path), names): + yield x + + def _collectfile(self, path): + ihook = self.gethookproxy(path) + if not self.isinitpath(path): + if ihook.pytest_ignore_collect(path=path, config=self.config): + return () + return ihook.pytest_collect_file(path=path, parent=self) + + def _recurse(self, path): + ihook = self.gethookproxy(path) + if ihook.pytest_ignore_collect(path=path, config=self.config): + return + for pat in self._norecursepatterns: + if path.check(fnmatch=pat): + return False + ihook.pytest_collect_directory(path=path, parent=self) + return True + + def _tryconvertpyarg(self, x): + try: + mod = __import__(x, None, None, ['__doc__']) + except (ValueError, ImportError): + return x + p = py.path.local(mod.__file__) + if p.purebasename == "__init__": + p = p.dirpath() + else: + p = p.new(basename=p.purebasename+".py") + return p + + def _parsearg(self, arg): + """ return (fspath, names) tuple after checking the file exists. """ + arg = str(arg) + if self.config.option.pyargs: + arg = self._tryconvertpyarg(arg) + parts = str(arg).split("::") + relpath = parts[0].replace("/", os.sep) + path = self.fspath.join(relpath, abs=True) + if not path.check(): + if self.config.option.pyargs: + msg = "file or package not found: " + else: + msg = "file not found: " + raise pytest.UsageError(msg + arg) + parts[0] = path + return parts + + def matchnodes(self, matching, names): + self.trace("matchnodes", matching, names) + self.trace.root.indent += 1 + nodes = self._matchnodes(matching, names) + num = len(nodes) + self.trace("matchnodes finished -> ", num, "nodes") + self.trace.root.indent -= 1 + if num == 0: + raise NoMatch(matching, names[:1]) + return nodes + + def _matchnodes(self, matching, names): + if not matching or not names: + return matching + name = names[0] + assert name + nextnames = names[1:] + resultnodes = [] + for node in matching: + if isinstance(node, pytest.Item): + resultnodes.append(node) + continue + assert isinstance(node, pytest.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + if rep.passed: + for x in rep.result: + if x.name == name: + resultnodes.extend(self.matchnodes([x], nextnames)) + node.ihook.pytest_collectreport(report=rep) + return resultnodes + + def genitems(self, node): + self.trace("genitems", node) + if isinstance(node, pytest.Item): + node.ihook.pytest_itemcollected(item=node) + yield node + else: + assert isinstance(node, pytest.Collector) + node.ihook.pytest_collectstart(collector=node) + rep = node.ihook.pytest_make_collect_report(collector=node) + if rep.passed: + for subnode in rep.result: + for x in self.genitems(subnode): + yield x + node.ihook.pytest_collectreport(report=rep) + +Session = Session --- /dev/null +++ b/pytest.py @@ -0,0 +1,17 @@ +""" +unit and functional testing with Python. + +see http://pytest.org for documentation and details + +(c) Holger Krekel and others, 2004-2010 +""" +__version__ = '2.0.0.dev26' +__all__ = ['main'] + +from _pytest.core import main, UsageError, _preloadplugins +from _pytest import core as cmdline + +if __name__ == '__main__': # if run as a script or by 'python -m pytest' + raise SystemExit(main()) +else: + _preloadplugins() # to populate pytest.* namespace so help(pytest) works --- a/testing/plugin/test_capture.py +++ b/testing/plugin/test_capture.py @@ -1,5 +1,5 @@ import py, os, sys -from pytest.plugin.capture import CaptureManager +from _pytest.capture import CaptureManager needsosdup = py.test.mark.xfail("not hasattr(os, 'dup')") --- a/doc/announce/release-2.0.0.txt +++ b/doc/announce/release-2.0.0.txt @@ -26,14 +26,16 @@ New Features - new invocations through Python interpreter and from Python:: - python -m pytest # on all pythons >= 2.7 - python -m pytest.main # on all pythons >= 2.5 + python -m pytest # on all pythons >= 2.5 + + or from a python program:: + import pytest ; pytest.main(args, plugins) see http://pytest.org/2.0.0/usage.html for details. - new and better reporting information in assert expressions - which compare lists, sequences or strings. + if comparing lists, sequences or strings. see http://pytest.org/2.0.0/assert.html for details. @@ -42,7 +44,7 @@ New Features [pytest] norecursedirs = .hg data* # don't ever recurse in such dirs - addopts = -x --pyargs # add these options by default + addopts = -x --pyargs # add these command line options by default see http://pytest.org/2.0.0/customize.html @@ -51,9 +53,10 @@ New Features py.test --pyargs unittest -- add a new "-q" option which decreases verbosity and prints a more +- new "-q" option which decreases verbosity and prints a more nose/unittest-style "dot" output. + Fixes ----------------------- --- a/testing/plugin/test_assertion.py +++ b/testing/plugin/test_assertion.py @@ -1,7 +1,7 @@ import sys import py -import pytest.plugin.assertion as plugin +import _pytest.assertion as plugin needsnewassert = py.test.mark.skipif("sys.version_info < (2,6)") --- /dev/null +++ b/_pytest/core.py @@ -0,0 +1,451 @@ +""" +pytest PluginManager, basic initialization and tracing. +All else is in pytest/plugin. +(c) Holger Krekel 2004-2010 +""" +import sys, os +import inspect +import py +from _pytest import hookspec # the extension point definitions + +assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " + "%s is too old, remove or upgrade 'py'" % (py.__version__)) + +default_plugins = ( + "config session terminal runner python pdb capture unittest mark skipping " + "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " + "junitxml doctest").split() + +IMPORTPREFIX = "pytest_" + +class TagTracer: + def __init__(self, prefix="[pytest] "): + self._tag2proc = {} + self.writer = None + self.indent = 0 + self.prefix = prefix + + def get(self, name): + return TagTracerSub(self, (name,)) + + def processmessage(self, tags, args): + if self.writer is not None: + if args: + indent = " " * self.indent + content = " ".join(map(str, args)) + self.writer("%s%s%s\n" %(self.prefix, indent, content)) + try: + self._tag2proc[tags](tags, args) + except KeyError: + pass + + def setwriter(self, writer): + self.writer = writer + + def setprocessor(self, tags, processor): + if isinstance(tags, str): + tags = tuple(tags.split(":")) + else: + assert isinstance(tags, tuple) + self._tag2proc[tags] = processor + +class TagTracerSub: + def __init__(self, root, tags): + self.root = root + self.tags = tags + def __call__(self, *args): + self.root.processmessage(self.tags, args) + def setmyprocessor(self, processor): + self.root.setprocessor(self.tags, processor) + def get(self, name): + return self.__class__(self.root, self.tags + (name,)) + +class PluginManager(object): + def __init__(self, load=False): + self._name2plugin = {} + self._plugins = [] + self._hints = [] + self.trace = TagTracer().get("pluginmanage") + if os.environ.get('PYTEST_DEBUG'): + err = sys.stderr + encoding = getattr(err, 'encoding', 'utf8') + try: + err = py.io.dupfile(err, encoding=encoding) + except Exception: + pass + self.trace.root.setwriter(err.write) + self.hook = HookRelay([hookspec], pm=self) + self.register(self) + if load: + for spec in default_plugins: + self.import_plugin(spec) + + def _getpluginname(self, plugin, name): + if name is None: + if hasattr(plugin, '__name__'): + name = plugin.__name__.split(".")[-1] + else: + name = id(plugin) + return name + + def register(self, plugin, name=None, prepend=False): + assert not self.isregistered(plugin), plugin + assert not self.isregistered(plugin), plugin + name = self._getpluginname(plugin, name) + if name in self._name2plugin: + return False + self._name2plugin[name] = plugin + self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) + self.hook.pytest_plugin_registered(manager=self, plugin=plugin) + if not prepend: + self._plugins.append(plugin) + else: + self._plugins.insert(0, plugin) + return True + + def unregister(self, plugin=None, name=None): + if plugin is None: + plugin = self.getplugin(name=name) + self._plugins.remove(plugin) + self.hook.pytest_plugin_unregistered(plugin=plugin) + for name, value in list(self._name2plugin.items()): + if value == plugin: + del self._name2plugin[name] + + def isregistered(self, plugin, name=None): + if self._getpluginname(plugin, name) in self._name2plugin: + return True + for val in self._name2plugin.values(): + if plugin == val: + return True + + def addhooks(self, spec): + self.hook._addhooks(spec, prefix="pytest_") + + def getplugins(self): + return list(self._plugins) + + def skipifmissing(self, name): + if not self.hasplugin(name): + py.test.skip("plugin %r is missing" % name) + + def hasplugin(self, name): + try: + self.getplugin(name) + return True + except KeyError: + return False + + def getplugin(self, name): + try: + return self._name2plugin[name] + except KeyError: + impname = canonical_importname(name) + return self._name2plugin[impname] + + # API for bootstrapping + # + def _envlist(self, varname): + val = py.std.os.environ.get(varname, None) + if val is not None: + return val.split(',') + return () + + def consider_env(self): + for spec in self._envlist("PYTEST_PLUGINS"): + self.import_plugin(spec) + + def consider_setuptools_entrypoints(self): + try: + from pkg_resources import iter_entry_points + except ImportError: + return # XXX issue a warning + for ep in iter_entry_points('pytest11'): + name = canonical_importname(ep.name) + if name in self._name2plugin: + continue + plugin = ep.load() + self.register(plugin, name=name) + + def consider_preparse(self, args): + for opt1,opt2 in zip(args, args[1:]): + if opt1 == "-p": + self.import_plugin(opt2) + + def consider_conftest(self, conftestmodule): + if self.register(conftestmodule, name=conftestmodule.__file__): + self.consider_module(conftestmodule) + + def consider_module(self, mod): + attr = getattr(mod, "pytest_plugins", ()) + if attr: + if not isinstance(attr, (list, tuple)): + attr = (attr,) + for spec in attr: + self.import_plugin(spec) + + def import_plugin(self, spec): + assert isinstance(spec, str) + modname = canonical_importname(spec) + if modname in self._name2plugin: + return + try: + mod = importplugin(modname) + except KeyboardInterrupt: + raise + except: + e = py.std.sys.exc_info()[1] + if not hasattr(py.test, 'skip'): + raise + elif not isinstance(e, py.test.skip.Exception): + raise + self._hints.append("skipped plugin %r: %s" %((modname, e.msg))) + else: + self.register(mod, modname) + self.consider_module(mod) + + def pytest_plugin_registered(self, plugin): + import pytest + dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} + if dic: + self._setns(pytest, dic) + if hasattr(self, '_config'): + self.call_plugin(plugin, "pytest_addoption", + {'parser': self._config._parser}) + self.call_plugin(plugin, "pytest_configure", + {'config': self._config}) + + def _setns(self, obj, dic): + import pytest + for name, value in dic.items(): + if isinstance(value, dict): + mod = getattr(obj, name, None) + if mod is None: + modname = "pytest.%s" % name + mod = py.std.types.ModuleType(modname) + sys.modules[modname] = mod + mod.__all__ = [] + setattr(obj, name, mod) + obj.__all__.append(name) + self._setns(mod, value) + else: + setattr(obj, name, value) + obj.__all__.append(name) + #if obj != pytest: + # pytest.__all__.append(name) + setattr(pytest, name, value) + + def pytest_terminal_summary(self, terminalreporter): + tw = terminalreporter._tw + if terminalreporter.config.option.traceconfig: + for hint in self._hints: + tw.line("hint: %s" % hint) + + def do_addoption(self, parser): + mname = "pytest_addoption" + methods = reversed(self.listattr(mname)) + MultiCall(methods, {'parser': parser}).execute() + + def do_configure(self, config): + assert not hasattr(self, '_config') + self._config = config + config.hook.pytest_configure(config=self._config) + + def do_unconfigure(self, config): + config = self._config + del self._config + config.hook.pytest_unconfigure(config=config) + config.pluginmanager.unregister(self) + + def notify_exception(self, excinfo): + excrepr = excinfo.getrepr(funcargs=True, showlocals=True) + res = self.hook.pytest_internalerror(excrepr=excrepr) + if not py.builtin.any(res): + for line in str(excrepr).split("\n"): + sys.stderr.write("INTERNALERROR> %s\n" %line) + sys.stderr.flush() + + def listattr(self, attrname, plugins=None): + if plugins is None: + plugins = self._plugins + l = [] + for plugin in plugins: + try: + l.append(getattr(plugin, attrname)) + except AttributeError: + continue + return l + + def call_plugin(self, plugin, methname, kwargs): + return MultiCall(methods=self.listattr(methname, plugins=[plugin]), + kwargs=kwargs, firstresult=True).execute() + +def canonical_importname(name): + if '.' in name: + return name + name = name.lower() + if not name.startswith(IMPORTPREFIX): + name = IMPORTPREFIX + name + return name + +def importplugin(importspec): + #print "importing", importspec + try: + return __import__(importspec, None, None, '__doc__') + except ImportError: + e = py.std.sys.exc_info()[1] + if str(e).find(importspec) == -1: + raise + name = importspec + try: + if name.startswith("pytest_"): + name = importspec[7:] + return __import__("_pytest.%s" %(name), None, None, '__doc__') + except ImportError: + e = py.std.sys.exc_info()[1] + if str(e).find(name) == -1: + raise + # show the original exception, not the failing internal one + return __import__(importspec, None, None, '__doc__') + + +class MultiCall: + """ execute a call into multiple python functions/methods. """ + def __init__(self, methods, kwargs, firstresult=False): + self.methods = list(methods) + self.kwargs = kwargs + self.results = [] + self.firstresult = firstresult + + def __repr__(self): + status = "%d results, %d meths" % (len(self.results), len(self.methods)) + return "" %(status, self.kwargs) + + def execute(self): + while self.methods: + method = self.methods.pop() + kwargs = self.getkwargs(method) + res = method(**kwargs) + if res is not None: + self.results.append(res) + if self.firstresult: + return res + if not self.firstresult: + return self.results + + def getkwargs(self, method): + kwargs = {} + for argname in varnames(method): + try: + kwargs[argname] = self.kwargs[argname] + except KeyError: + if argname == "__multicall__": + kwargs[argname] = self + return kwargs + +def varnames(func): + if not inspect.isfunction(func) and not inspect.ismethod(func): + func = getattr(func, '__call__', func) + ismethod = inspect.ismethod(func) + rawcode = py.code.getrawcode(func) + try: + return rawcode.co_varnames[ismethod:rawcode.co_argcount] + except AttributeError: + return () + +class HookRelay: + def __init__(self, hookspecs, pm, prefix="pytest_"): + if not isinstance(hookspecs, list): + hookspecs = [hookspecs] + self._hookspecs = [] + self._pm = pm + self.trace = pm.trace.root.get("hook") + for hookspec in hookspecs: + self._addhooks(hookspec, prefix) + + def _addhooks(self, hookspecs, prefix): + self._hookspecs.append(hookspecs) + added = False + for name, method in vars(hookspecs).items(): + if name.startswith(prefix): + if not method.__doc__: + raise ValueError("docstring required for hook %r, in %r" + % (method, hookspecs)) + firstresult = getattr(method, 'firstresult', False) + hc = HookCaller(self, name, firstresult=firstresult) + setattr(self, name, hc) + added = True + #print ("setting new hook", name) + if not added: + raise ValueError("did not find new %r hooks in %r" %( + prefix, hookspecs,)) + + +class HookCaller: + def __init__(self, hookrelay, name, firstresult): + self.hookrelay = hookrelay + self.name = name + self.firstresult = firstresult + self.trace = self.hookrelay.trace + + def __repr__(self): + return "" %(self.name,) + + def __call__(self, **kwargs): + methods = self.hookrelay._pm.listattr(self.name) + return self._docall(methods, kwargs) + + def pcall(self, plugins, **kwargs): + methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) + return self._docall(methods, kwargs) + + def _docall(self, methods, kwargs): + self.trace(self.name, kwargs) + self.trace.root.indent += 1 + mc = MultiCall(methods, kwargs, firstresult=self.firstresult) + try: + res = mc.execute() + if res: + self.trace("finish", self.name, "-->", res) + finally: + self.trace.root.indent -= 1 + return res + +_preinit = [] + +def _preloadplugins(): + _preinit.append(PluginManager(load=True)) + +def main(args=None, plugins=None): + """ returned exit code integer, after an in-process testing run + with the given command line arguments, preloading an optional list + of passed in plugin objects. """ + if args is None: + args = sys.argv[1:] + elif isinstance(args, py.path.local): + args = [str(args)] + elif not isinstance(args, (tuple, list)): + if not isinstance(args, str): + raise ValueError("not a string or argument list: %r" % (args,)) + args = py.std.shlex.split(args) + if _preinit: + _pluginmanager = _preinit.pop(0) + else: # subsequent calls to main will create a fresh instance + _pluginmanager = PluginManager(load=True) + hook = _pluginmanager.hook + try: + if plugins: + for plugin in plugins: + _pluginmanager.register(plugin) + config = hook.pytest_cmdline_parse( + pluginmanager=_pluginmanager, args=args) + exitstatus = hook.pytest_cmdline_main(config=config) + except UsageError: + e = sys.exc_info()[1] + sys.stderr.write("ERROR: %s\n" %(e.args[0],)) + exitstatus = 3 + return exitstatus + +class UsageError(Exception): + """ error in py.test usage or invocation""" + --- a/pytest/plugin/recwarn.py +++ /dev/null @@ -1,96 +0,0 @@ -""" recording warnings during test function execution. """ - -import py -import sys, os - -def pytest_funcarg__recwarn(request): - """Return a WarningsRecorder instance that provides these methods: - - * ``pop(category=None)``: return last warning matching the category. - * ``clear()``: clear list of warnings - """ - if sys.version_info >= (2,7): - import warnings - oldfilters = warnings.filters[:] - warnings.simplefilter('default') - def reset_filters(): - warnings.filters[:] = oldfilters - request.addfinalizer(reset_filters) - wrec = WarningsRecorder() - request.addfinalizer(wrec.finalize) - return wrec - -def pytest_namespace(): - return {'deprecated_call': deprecated_call} - -def deprecated_call(func, *args, **kwargs): - """ assert that calling func(*args, **kwargs) - triggers a DeprecationWarning. - """ - warningmodule = py.std.warnings - l = [] - oldwarn_explicit = getattr(warningmodule, 'warn_explicit') - def warn_explicit(*args, **kwargs): - l.append(args) - oldwarn_explicit(*args, **kwargs) - oldwarn = getattr(warningmodule, 'warn') - def warn(*args, **kwargs): - l.append(args) - oldwarn(*args, **kwargs) - - warningmodule.warn_explicit = warn_explicit - warningmodule.warn = warn - try: - ret = func(*args, **kwargs) - finally: - warningmodule.warn_explicit = warn_explicit - warningmodule.warn = warn - if not l: - #print warningmodule - __tracebackhide__ = True - raise AssertionError("%r did not produce DeprecationWarning" %(func,)) - return ret - - -class RecordedWarning: - def __init__(self, message, category, filename, lineno, line): - self.message = message - self.category = category - self.filename = filename - self.lineno = lineno - self.line = line - -class WarningsRecorder: - def __init__(self): - warningmodule = py.std.warnings - self.list = [] - def showwarning(message, category, filename, lineno, line=0): - self.list.append(RecordedWarning( - message, category, filename, lineno, line)) - try: - self.old_showwarning(message, category, - filename, lineno, line=line) - except TypeError: - # < python2.6 - self.old_showwarning(message, category, filename, lineno) - self.old_showwarning = warningmodule.showwarning - warningmodule.showwarning = showwarning - - def pop(self, cls=Warning): - """ pop the first recorded warning, raise exception if not exists.""" - for i, w in enumerate(self.list): - if issubclass(w.category, cls): - return self.list.pop(i) - __tracebackhide__ = True - assert 0, "%r not found in %r" %(cls, self.list) - - #def resetregistry(self): - # import warnings - # warnings.onceregistry.clear() - # warnings.__warningregistry__.clear() - - def clear(self): - self.list[:] = [] - - def finalize(self): - py.std.warnings.showwarning = self.old_showwarning --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -180,28 +180,28 @@ py.test default plugin reference .. autosummary:: - pytest.plugin.assertion - pytest.plugin.capture - pytest.plugin.config - pytest.plugin.doctest - pytest.plugin.genscript - pytest.plugin.helpconfig - pytest.plugin.junitxml - pytest.plugin.mark - pytest.plugin.monkeypatch - pytest.plugin.nose - pytest.plugin.pastebin - pytest.plugin.pdb - pytest.plugin.pytester - pytest.plugin.python - pytest.plugin.recwarn - pytest.plugin.resultlog - pytest.plugin.runner - pytest.plugin.session - pytest.plugin.skipping - pytest.plugin.terminal - pytest.plugin.tmpdir - pytest.plugin.unittest + _pytest.assertion + _pytest.capture + _pytest.config + _pytest.doctest + _pytest.genscript + _pytest.helpconfig + _pytest.junitxml + _pytest.mark + _pytest.monkeypatch + _pytest.nose + _pytest.pastebin + _pytest.pdb + _pytest.pytester + _pytest.python + _pytest.recwarn + _pytest.resultlog + _pytest.runner + _pytest.session + _pytest.skipping + _pytest.terminal + _pytest.tmpdir + _pytest.unittest .. _`well specified hooks`: @@ -222,7 +222,7 @@ hook name itself you get useful errors. initialisation, command line and configuration hooks -------------------------------------------------------------------- -.. currentmodule:: pytest.hookspec +.. currentmodule:: _pytest.hookspec .. autofunction:: pytest_cmdline_parse .. autofunction:: pytest_namespace @@ -243,11 +243,11 @@ All all runtest related hooks receive a .. autofunction:: pytest_runtest_makereport For deeper understanding you may look at the default implementation of -these hooks in :py:mod:`pytest.plugin.runner` and maybe also -in :py:mod:`pytest.plugin.pdb` which intercepts creation +these hooks in :py:mod:`_pytest.runner` and maybe also +in :py:mod:`_pytest.pdb` which intercepts creation of reports in order to drop to interactive debugging. -The :py:mod:`pytest.plugin.terminal` reported specifically uses +The :py:mod:`_pytest.terminal` reported specifically uses the reporting hook to print information about a test run. collection hooks @@ -284,35 +284,35 @@ test execution: Reference of important objects involved in hooks =========================================================== -.. autoclass:: pytest.plugin.config.Config +.. autoclass:: _pytest.config.Config :members: -.. autoclass:: pytest.plugin.config.Parser +.. autoclass:: _pytest.config.Parser :members: -.. autoclass:: pytest.plugin.session.Node(name, parent) +.. autoclass:: _pytest.session.Node(name, parent) :members: .. - .. autoclass:: pytest.plugin.session.File(fspath, parent) + .. autoclass:: _pytest.session.File(fspath, parent) :members: - .. autoclass:: pytest.plugin.session.Item(name, parent) + .. autoclass:: _pytest.session.Item(name, parent) :members: - .. autoclass:: pytest.plugin.python.Module(name, parent) + .. autoclass:: _pytest.python.Module(name, parent) :members: - .. autoclass:: pytest.plugin.python.Class(name, parent) + .. autoclass:: _pytest.python.Class(name, parent) :members: - .. autoclass:: pytest.plugin.python.Function(name, parent) + .. autoclass:: _pytest.python.Function(name, parent) :members: -.. autoclass:: pytest.plugin.runner.CallInfo +.. autoclass:: _pytest.runner.CallInfo :members: -.. autoclass:: pytest.plugin.runner.TestReport +.. autoclass:: _pytest.runner.TestReport :members: --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,5 +1,5 @@ import py -from pytest.plugin.config import Conftest +from _pytest.config import Conftest def pytest_generate_tests(metafunc): if "basedir" in metafunc.funcargnames: @@ -110,7 +110,7 @@ def test_conftest_global_import(testdir) testdir.makeconftest("x=3") p = testdir.makepyfile(""" import py - from pytest.plugin.config import Conftest + from _pytest.config import Conftest conf = Conftest() mod = conf.importconftest(py.path.local("conftest.py")) assert mod.x == 3 --- a/testing/plugin/test_resultlog.py +++ b/testing/plugin/test_resultlog.py @@ -1,11 +1,11 @@ import py import os -from pytest.plugin.resultlog import generic_path, ResultLog, \ +from _pytest.resultlog import generic_path, ResultLog, \ pytest_configure, pytest_unconfigure -from pytest.plugin.session import Node, Item, FSCollector +from _pytest.session import Node, Item, FSCollector def test_generic_path(testdir): - from pytest.plugin.session import Session + from _pytest.session import Session config = testdir.parseconfig() session = Session(config) p1 = Node('a', config=config, session=session) --- a/testing/plugin/test_skipping.py +++ b/testing/plugin/test_skipping.py @@ -1,8 +1,8 @@ import py -from pytest.plugin.skipping import MarkEvaluator, folded_skips -from pytest.plugin.skipping import pytest_runtest_setup -from pytest.plugin.runner import runtestprotocol +from _pytest.skipping import MarkEvaluator, folded_skips +from _pytest.skipping import pytest_runtest_setup +from _pytest.runner import runtestprotocol class TestEvaluator: def test_no_marker(self, testdir): --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,6 +1,6 @@ import py -from pytest.plugin.config import getcfg, Config +from _pytest.config import getcfg, Config class TestParseIni: def test_getcfg_and_config(self, tmpdir): --- /dev/null +++ b/_pytest/capture.py @@ -0,0 +1,216 @@ +""" per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """ + +import py +import os + +def pytest_addoption(parser): + group = parser.getgroup("general") + group._addoption('--capture', action="store", default=None, + metavar="method", type="choice", choices=['fd', 'sys', 'no'], + help="per-test capturing method: one of fd (default)|sys|no.") + group._addoption('-s', action="store_const", const="no", dest="capture", + help="shortcut for --capture=no.") + +def addouterr(rep, outerr): + repr = getattr(rep, 'longrepr', None) + if not hasattr(repr, 'addsection'): + return + for secname, content in zip(["out", "err"], outerr): + if content: + repr.addsection("Captured std%s" % secname, content.rstrip()) + +def pytest_configure(config): + config.pluginmanager.register(CaptureManager(), 'capturemanager') + +def pytest_unconfigure(config): + capman = config.pluginmanager.getplugin('capturemanager') + while capman._method2capture: + name, cap = capman._method2capture.popitem() + cap.reset() + +class NoCapture: + def startall(self): + pass + def resume(self): + pass + def reset(self): + pass + def suspend(self): + return "", "" + +class CaptureManager: + def __init__(self): + self._method2capture = {} + + def _maketempfile(self): + f = py.std.tempfile.TemporaryFile() + newf = py.io.dupfile(f, encoding="UTF-8") + f.close() + return newf + + def _makestringio(self): + return py.io.TextIO() + + def _getcapture(self, method): + if method == "fd": + return py.io.StdCaptureFD(now=False, + out=self._maketempfile(), err=self._maketempfile() + ) + elif method == "sys": + return py.io.StdCapture(now=False, + out=self._makestringio(), err=self._makestringio() + ) + elif method == "no": + return NoCapture() + else: + raise ValueError("unknown capturing method: %r" % method) + + def _getmethod(self, config, fspath): + if config.option.capture: + method = config.option.capture + else: + try: + method = config._conftest.rget("option_capture", path=fspath) + except KeyError: + method = "fd" + if method == "fd" and not hasattr(os, 'dup'): # e.g. jython + method = "sys" + return method + + def resumecapture_item(self, item): + method = self._getmethod(item.config, item.fspath) + if not hasattr(item, 'outerr'): + item.outerr = ('', '') # we accumulate outerr on the item + return self.resumecapture(method) + + def resumecapture(self, method): + if hasattr(self, '_capturing'): + raise ValueError("cannot resume, already capturing with %r" % + (self._capturing,)) + cap = self._method2capture.get(method) + self._capturing = method + if cap is None: + self._method2capture[method] = cap = self._getcapture(method) + cap.startall() + else: + cap.resume() + + def suspendcapture(self, item=None): + self.deactivate_funcargs() + if hasattr(self, '_capturing'): + method = self._capturing + cap = self._method2capture.get(method) + if cap is not None: + outerr = cap.suspend() + del self._capturing + if item: + outerr = (item.outerr[0] + outerr[0], + item.outerr[1] + outerr[1]) + return outerr + if hasattr(item, 'outerr'): + return item.outerr + return "", "" + + def activate_funcargs(self, pyfuncitem): + if not hasattr(pyfuncitem, 'funcargs'): + return + assert not hasattr(self, '_capturing_funcargs') + self._capturing_funcargs = capturing_funcargs = [] + for name, capfuncarg in pyfuncitem.funcargs.items(): + if name in ('capsys', 'capfd'): + capturing_funcargs.append(capfuncarg) + capfuncarg._start() + + def deactivate_funcargs(self): + capturing_funcargs = getattr(self, '_capturing_funcargs', None) + if capturing_funcargs is not None: + while capturing_funcargs: + capfuncarg = capturing_funcargs.pop() + capfuncarg._finalize() + del self._capturing_funcargs + + def pytest_make_collect_report(self, __multicall__, collector): + method = self._getmethod(collector.config, collector.fspath) + try: + self.resumecapture(method) + except ValueError: + return # recursive collect, XXX refactor capturing + # to allow for more lightweight recursive capturing + try: + rep = __multicall__.execute() + finally: + outerr = self.suspendcapture() + addouterr(rep, outerr) + return rep + + def pytest_runtest_setup(self, item): + self.resumecapture_item(item) + + def pytest_runtest_call(self, item): + self.resumecapture_item(item) + self.activate_funcargs(item) + + def pytest_runtest_teardown(self, item): + self.resumecapture_item(item) + + def pytest__teardown_final(self, __multicall__, session): + method = self._getmethod(session.config, None) + self.resumecapture(method) + try: + rep = __multicall__.execute() + finally: + outerr = self.suspendcapture() + if rep: + addouterr(rep, outerr) + return rep + + def pytest_keyboard_interrupt(self, excinfo): + if hasattr(self, '_capturing'): + self.suspendcapture() + + def pytest_runtest_makereport(self, __multicall__, item, call): + self.deactivate_funcargs() + rep = __multicall__.execute() + outerr = self.suspendcapture(item) + if not rep.passed: + addouterr(rep, outerr) + if not rep.passed or rep.when == "teardown": + outerr = ('', '') + item.outerr = outerr + return rep + +def pytest_funcarg__capsys(request): + """captures writes to sys.stdout/sys.stderr and makes + them available successively via a ``capsys.readouterr()`` method + which returns a ``(out, err)`` tuple of captured snapshot strings. + """ + return CaptureFuncarg(py.io.StdCapture) + +def pytest_funcarg__capfd(request): + """captures writes to file descriptors 1 and 2 and makes + snapshotted ``(out, err)`` string tuples available + via the ``capsys.readouterr()`` method. If the underlying + platform does not have ``os.dup`` (e.g. Jython) tests using + this funcarg will automatically skip. + """ + if not hasattr(os, 'dup'): + py.test.skip("capfd funcarg needs os.dup") + return CaptureFuncarg(py.io.StdCaptureFD) + +class CaptureFuncarg: + def __init__(self, captureclass): + self.capture = captureclass(now=False) + + def _start(self): + self.capture.startall() + + def _finalize(self): + if hasattr(self, 'capture'): + self.capture.reset() + del self.capture + + def readouterr(self): + return self.capture.readouterr() + + def close(self): + self._finalize() --- a/testing/plugin/test_pytester.py +++ b/testing/plugin/test_pytester.py @@ -1,7 +1,7 @@ import py import os, sys -from pytest.plugin.pytester import LineMatcher, LineComp, HookRecorder -from pytest.main import PluginManager +from _pytest.pytester import LineMatcher, LineComp, HookRecorder +from _pytest.core import PluginManager def test_reportrecorder(testdir): item = testdir.getitem("def test_func(): pass") @@ -97,7 +97,7 @@ def test_hookrecorder_basic_no_args_hook def test_functional(testdir, linecomp): reprec = testdir.inline_runsource(""" import py - from pytest.main import HookRelay, PluginManager + from _pytest.core import HookRelay, PluginManager pytest_plugins="pytester" def test_func(_pytest): class ApiClass: --- a/pytest/plugin/helpconfig.py +++ /dev/null @@ -1,158 +0,0 @@ -""" version info, help messages, tracing configuration. """ -import py -import pytest -import inspect, sys - -def pytest_addoption(parser): - group = parser.getgroup('debugconfig') - group.addoption('--version', action="store_true", - help="display pytest lib version and import information.") - group._addoption("-h", "--help", action="store_true", dest="help", - help="show help message and configuration info") - group._addoption('-p', action="append", dest="plugins", default = [], - metavar="name", - help="early-load given plugin (multi-allowed).") - group.addoption('--traceconfig', - action="store_true", dest="traceconfig", default=False, - help="trace considerations of conftest.py files."), - group._addoption('--nomagic', - action="store_true", dest="nomagic", default=False, - help="don't reinterpret asserts, no traceback cutting. ") - group.addoption('--debug', - action="store_true", dest="debug", default=False, - help="generate and show internal debugging information.") - - -def pytest_cmdline_main(config): - if config.option.version: - p = py.path.local(pytest.__file__).dirpath() - sys.stderr.write("This is py.test version %s, imported from %s\n" % - (pytest.__version__, p)) - return 0 - elif config.option.help: - config.pluginmanager.do_configure(config) - showhelp(config) - return 0 - -def showhelp(config): - tw = py.io.TerminalWriter() - tw.write(config._parser.optparser.format_help()) - tw.line() - tw.line() - #tw.sep( "=", "config file settings") - tw.line("setup.cfg or tox.ini options to be put into [pytest] section:") - tw.line() - - for name in config._parser._ininames: - help, type, default = config._parser._inidict[name] - if type is None: - type = "string" - spec = "%s (%s)" % (name, type) - line = " %-24s %s" %(spec, help) - tw.line(line[:tw.fullwidth]) - - tw.line() ; tw.line() - #tw.sep("=") - return - - tw.line("conftest.py options:") - tw.line() - conftestitems = sorted(config._parser._conftestdict.items()) - for name, help in conftest_options + conftestitems: - line = " %-15s %s" %(name, help) - tw.line(line[:tw.fullwidth]) - tw.line() - #tw.sep( "=") - -conftest_options = [ - ('pytest_plugins', 'list of plugin names to load'), -] - -def pytest_report_header(config): - lines = [] - if config.option.debug or config.option.traceconfig: - lines.append("using: pytest-%s pylib-%s" % - (pytest.__version__,py.__version__)) - - if config.option.traceconfig: - lines.append("active plugins:") - plugins = [] - items = config.pluginmanager._name2plugin.items() - for name, plugin in items: - lines.append(" %-20s: %s" %(name, repr(plugin))) - return lines - - -# ===================================================== -# validate plugin syntax and hooks -# ===================================================== - -def pytest_plugin_registered(manager, plugin): - methods = collectattr(plugin) - hooks = {} - for hookspec in manager.hook._hookspecs: - hooks.update(collectattr(hookspec)) - - stringio = py.io.TextIO() - def Print(*args): - if args: - stringio.write(" ".join(map(str, args))) - stringio.write("\n") - - fail = False - while methods: - name, method = methods.popitem() - #print "checking", name - if isgenerichook(name): - continue - if name not in hooks: - if not getattr(method, 'optionalhook', False): - Print("found unknown hook:", name) - fail = True - else: - #print "checking", method - method_args = getargs(method) - #print "method_args", method_args - if '__multicall__' in method_args: - method_args.remove('__multicall__') - hook = hooks[name] - hookargs = getargs(hook) - for arg in method_args: - if arg not in hookargs: - Print("argument %r not available" %(arg, )) - Print("actual definition: %s" %(formatdef(method))) - Print("available hook arguments: %s" % - ", ".join(hookargs)) - fail = True - break - #if not fail: - # print "matching hook:", formatdef(method) - if fail: - name = getattr(plugin, '__name__', plugin) - raise PluginValidationError("%s:\n%s" %(name, stringio.getvalue())) - -class PluginValidationError(Exception): - """ plugin failed validation. """ - -def isgenerichook(name): - return name == "pytest_plugins" or \ - name.startswith("pytest_funcarg__") - -def getargs(func): - args = inspect.getargs(py.code.getrawcode(func))[0] - startindex = inspect.ismethod(func) and 1 or 0 - return args[startindex:] - -def collectattr(obj): - methods = {} - for apiname in dir(obj): - if apiname.startswith("pytest_"): - methods[apiname] = getattr(obj, apiname) - return methods - -def formatdef(func): - return "%s%s" %( - func.__name__, - inspect.formatargspec(*inspect.getargspec(func)) - ) - --- a/pytest/plugin/assertion.py +++ /dev/null @@ -1,157 +0,0 @@ -""" -support for presented detailed information in failing assertions. -""" -import py -import sys -from pytest.plugin.monkeypatch import monkeypatch - -def pytest_addoption(parser): - group = parser.getgroup("debugconfig") - group._addoption('--no-assert', action="store_true", default=False, - dest="noassert", - help="disable python assert expression reinterpretation."), - -def pytest_configure(config): - # The _pytesthook attribute on the AssertionError is used by - # py._code._assertionnew to detect this plugin was loaded and in - # turn call the hooks defined here as part of the - # DebugInterpreter. - config._monkeypatch = m = monkeypatch() - if not config.getvalue("noassert") and not config.getvalue("nomagic"): - warn_about_missing_assertion() - def callbinrepr(op, left, right): - hook_result = config.hook.pytest_assertrepr_compare( - config=config, op=op, left=left, right=right) - for new_expl in hook_result: - if new_expl: - return '\n~'.join(new_expl) - m.setattr(py.builtin.builtins, - 'AssertionError', py.code._AssertionError) - m.setattr(py.code, '_reprcompare', callbinrepr) - -def pytest_unconfigure(config): - config._monkeypatch.undo() - -def warn_about_missing_assertion(): - try: - assert False - except AssertionError: - pass - else: - py.std.warnings.warn("Assertions are turned off!" - " (are you using python -O?)") - - -# Provide basestring in python3 -try: - basestring = basestring -except NameError: - basestring = str - - -def pytest_assertrepr_compare(op, left, right): - """return specialised explanations for some operators/operands""" - left_repr = py.io.saferepr(left, maxsize=30) - right_repr = py.io.saferepr(right, maxsize=30) - summary = '%s %s %s' % (left_repr, op, right_repr) - - 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) - - explanation = None - try: - if op == '==': - if istext(left) and istext(right): - explanation = _diff_text(left, right) - elif issequence(left) and issequence(right): - explanation = _compare_eq_sequence(left, right) - elif isset(left) and isset(right): - explanation = _compare_eq_set(left, right) - elif isdict(left) and isdict(right): - explanation = _diff_text(py.std.pprint.pformat(left), - py.std.pprint.pformat(right)) - except py.builtin._sysex: - raise - except: - excinfo = py.code.ExceptionInfo() - explanation = ['(pytest_assertion plugin: representation of ' - 'details failed. Probably an object has a faulty __repr__.)', - str(excinfo) - ] - - - if not explanation: - return None - - # Don't include pageloads of data, should be configurable - if len(''.join(explanation)) > 80*8: - explanation = ['Detailed information too verbose, truncated'] - - return [summary] + explanation - - -def _diff_text(left, right): - """Return the explanation for the diff between text - - This will skip leading and trailing characters which are - identical to keep the diff minimal. - """ - explanation = [] - 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 - if i > 42: - i -= 10 # Provide some context - explanation = ['Skipping %s identical ' - 'leading characters in diff' % i] - left = left[i:] - right = right[i:] - if len(left) == len(right): - for i in range(len(left)): - if left[-i] != right[-i]: - break - if i > 42: - i -= 10 # Provide some context - explanation += ['Skipping %s identical ' - 'trailing characters in diff' % i] - left = left[:-i] - right = right[:-i] - explanation += [line.strip('\n') - for line in py.std.difflib.ndiff(left.splitlines(), - right.splitlines())] - return explanation - - -def _compare_eq_sequence(left, right): - explanation = [] - for i in range(min(len(left), len(right))): - if left[i] != right[i]: - explanation += ['At index %s diff: %r != %r' % - (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)],)] - 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)) - - -def _compare_eq_set(left, right): - explanation = [] - diff_left = left - right - diff_right = right - left - if diff_left: - explanation.append('Extra items in the left set:') - for item in diff_left: - explanation.append(py.io.saferepr(item)) - if diff_right: - explanation.append('Extra items in the right set:') - for item in diff_right: - explanation.append(py.io.saferepr(item)) - return explanation --- /dev/null +++ b/_pytest/standalonetemplate.py @@ -0,0 +1,63 @@ +#! /usr/bin/env python + +sources = """ + at SOURCES@""" + +import sys +import base64 +import zlib +import imp + +class DictImporter(object): + def __init__(self, sources): + self.sources = sources + + def find_module(self, fullname, path=None): + if fullname in self.sources: + return self + if fullname+'.__init__' in self.sources: + return self + return None + + def load_module(self, fullname): + # print "load_module:", fullname + from types import ModuleType + try: + s = self.sources[fullname] + is_pkg = False + except KeyError: + s = self.sources[fullname+'.__init__'] + is_pkg = True + + co = compile(s, fullname, 'exec') + module = sys.modules.setdefault(fullname, ModuleType(fullname)) + module.__file__ = "%s/%s" % (__file__, fullname) + module.__loader__ = self + if is_pkg: + module.__path__ = [fullname] + + do_exec(co, module.__dict__) + return sys.modules[fullname] + + def get_source(self, name): + res = self.sources.get(name) + if res is None: + res = self.sources.get(name+'.__init__') + return res + +if __name__ == "__main__": + if sys.version_info >= (3,0): + exec("def do_exec(co, loc): exec(co, loc)\n") + import pickle + sources = sources.encode("ascii") # ensure bytes + sources = pickle.loads(zlib.decompress(base64.decodebytes(sources))) + else: + import cPickle as pickle + exec("def do_exec(co, loc): exec co in loc\n") + sources = pickle.loads(zlib.decompress(base64.decodestring(sources))) + + importer = DictImporter(sources) + sys.meta_path.append(importer) + + entry = "@ENTRY@" + do_exec(entry, locals()) --- a/doc/example/nonpython/conftest.py +++ b/doc/example/nonpython/conftest.py @@ -1,6 +1,6 @@ # content of conftest.py -import py +import pytest def pytest_collect_file(path, parent): if path.ext == ".yml" and path.basename.startswith("test"): --- a/pytest/plugin/doctest.py +++ /dev/null @@ -1,79 +0,0 @@ -""" discover and run doctests in modules and test files.""" - -import pytest, py -from py._code.code import TerminalRepr, ReprFileLocation - -def pytest_addoption(parser): - group = parser.getgroup("collect") - group.addoption("--doctest-modules", - action="store_true", default=False, - help="run doctests in all .py modules", - dest="doctestmodules") - group.addoption("--doctest-glob", - action="store", default="test*.txt", metavar="pat", - help="doctests file matching pattern, default: test*.txt", - dest="doctestglob") - -def pytest_collect_file(path, parent): - config = parent.config - if path.ext == ".py": - if config.option.doctestmodules: - return DoctestModule(path, parent) - elif path.check(fnmatch=config.getvalue("doctestglob")): - return DoctestTextfile(path, parent) - -class ReprFailDoctest(TerminalRepr): - def __init__(self, reprlocation, lines): - self.reprlocation = reprlocation - self.lines = lines - def toterminal(self, tw): - for line in self.lines: - tw.line(line) - self.reprlocation.toterminal(tw) - -class DoctestItem(pytest.Item): - def __init__(self, path, parent): - name = self.__class__.__name__ + ":" + path.basename - super(DoctestItem, self).__init__(name=name, parent=parent) - self.fspath = path - - def repr_failure(self, excinfo): - if excinfo.errisinstance(py.std.doctest.DocTestFailure): - doctestfailure = excinfo.value - example = doctestfailure.example - test = doctestfailure.test - filename = test.filename - lineno = test.lineno + example.lineno + 1 - message = excinfo.type.__name__ - reprlocation = ReprFileLocation(filename, lineno, message) - checker = py.std.doctest.OutputChecker() - REPORT_UDIFF = py.std.doctest.REPORT_UDIFF - filelines = py.path.local(filename).readlines(cr=0) - i = max(test.lineno, max(0, lineno - 10)) # XXX? - lines = [] - for line in filelines[i:lineno]: - lines.append("%03d %s" % (i+1, line)) - i += 1 - lines += checker.output_difference(example, - doctestfailure.got, REPORT_UDIFF).split("\n") - return ReprFailDoctest(reprlocation, lines) - elif excinfo.errisinstance(py.std.doctest.UnexpectedException): - excinfo = py.code.ExceptionInfo(excinfo.value.exc_info) - return super(DoctestItem, self).repr_failure(excinfo) - else: - return super(DoctestItem, self).repr_failure(excinfo) - - def reportinfo(self): - return self.fspath, None, "[doctest]" - -class DoctestTextfile(DoctestItem): - def runtest(self): - failed, tot = py.std.doctest.testfile( - str(self.fspath), module_relative=False, - raise_on_error=True, verbose=0) - -class DoctestModule(DoctestItem): - def runtest(self): - module = self.fspath.pyimport() - failed, tot = py.std.doctest.testmod( - module, raise_on_error=True, verbose=0) --- /dev/null +++ b/_pytest/python.py @@ -0,0 +1,823 @@ +""" Python test discovery, setup and run of test functions. """ +import py +import inspect +import sys +import pytest +from py._code.code import TerminalRepr + +import _pytest +cutdir = py.path.local(_pytest.__file__).dirpath() + +def pytest_addoption(parser): + group = parser.getgroup("general") + group.addoption('--funcargs', + action="store_true", dest="showfuncargs", default=False, + help="show available function arguments, sorted by plugin") + +def pytest_cmdline_main(config): + if config.option.showfuncargs: + showfuncargs(config) + return 0 + +def pytest_namespace(__multicall__): + __multicall__.execute() + raises.Exception = pytest.fail.Exception + return { + 'raises' : raises, + 'collect': { + 'Module': Module, 'Class': Class, 'Instance': Instance, + 'Function': Function, 'Generator': Generator, + '_fillfuncargs': fillfuncargs} + } + +def pytest_funcarg__pytestconfig(request): + """ the pytest config object with access to command line opts.""" + return request.config + +def pytest_pyfunc_call(__multicall__, pyfuncitem): + if not __multicall__.execute(): + testfunction = pyfuncitem.obj + if pyfuncitem._isyieldedfunction(): + testfunction(*pyfuncitem._args) + else: + funcargs = pyfuncitem.funcargs + testfunction(**funcargs) + +def pytest_collect_file(path, parent): + ext = path.ext + pb = path.purebasename + if ext == ".py" and (pb.startswith("test_") or pb.endswith("_test") or + parent.session.isinitpath(path)): + return parent.ihook.pytest_pycollect_makemodule( + path=path, parent=parent) + +def pytest_pycollect_makemodule(path, parent): + return Module(path, parent) + +def pytest_pycollect_makeitem(__multicall__, collector, name, obj): + res = __multicall__.execute() + if res is not None: + return res + if collector._istestclasscandidate(name, obj): + if hasattr(collector.obj, 'unittest'): + return # we assume it's a mixin class for a TestCase derived one + return Class(name, parent=collector) + elif collector.funcnamefilter(name) and hasattr(obj, '__call__'): + if is_generator(obj): + return Generator(name, parent=collector) + else: + return collector._genfunctions(name, obj) + +def is_generator(func): + try: + return py.code.getrawcode(func).co_flags & 32 # generator function + except AttributeError: # builtin functions have no bytecode + # assume them to not be generators + return False + +class PyobjMixin(object): + def obj(): + def fget(self): + try: + return self._obj + except AttributeError: + self._obj = obj = self._getobj() + return obj + def fset(self, value): + self._obj = value + return property(fget, fset, None, "underlying python object") + obj = obj() + + def _getobj(self): + return getattr(self.parent.obj, self.name) + + def getmodpath(self, stopatmodule=True, includemodule=False): + """ return python path relative to the containing module. """ + chain = self.listchain() + chain.reverse() + parts = [] + for node in chain: + if isinstance(node, Instance): + continue + name = node.name + if isinstance(node, Module): + assert name.endswith(".py") + name = name[:-3] + if stopatmodule: + if includemodule: + parts.append(name) + break + parts.append(name) + parts.reverse() + s = ".".join(parts) + return s.replace(".[", "[") + + def _getfslineno(self): + try: + return self._fslineno + except AttributeError: + pass + obj = self.obj + # xxx let decorators etc specify a sane ordering + if hasattr(obj, 'place_as'): + obj = obj.place_as + + self._fslineno = py.code.getfslineno(obj) + return self._fslineno + + def reportinfo(self): + # XXX caching? + obj = self.obj + if hasattr(obj, 'compat_co_firstlineno'): + # nose compatibility + fspath = sys.modules[obj.__module__].__file__ + if fspath.endswith(".pyc"): + fspath = fspath[:-1] + #assert 0 + #fn = inspect.getsourcefile(obj) or inspect.getfile(obj) + lineno = obj.compat_co_firstlineno + modpath = obj.__module__ + else: + fspath, lineno = self._getfslineno() + modpath = self.getmodpath() + return fspath, lineno, modpath + +class PyCollectorMixin(PyobjMixin, pytest.Collector): + + def funcnamefilter(self, name): + return name.startswith('test') + def classnamefilter(self, name): + return name.startswith('Test') + + def collect(self): + # NB. we avoid random getattrs and peek in the __dict__ instead + # (XXX originally introduced from a PyPy need, still true?) + dicts = [getattr(self.obj, '__dict__', {})] + for basecls in inspect.getmro(self.obj.__class__): + dicts.append(basecls.__dict__) + seen = {} + l = [] + for dic in dicts: + for name, obj in dic.items(): + if name in seen: + continue + seen[name] = True + if name[0] != "_": + res = self.makeitem(name, obj) + if res is None: + continue + if not isinstance(res, list): + res = [res] + l.extend(res) + l.sort(key=lambda item: item.reportinfo()[:2]) + return l + + def makeitem(self, name, obj): + return self.ihook.pytest_pycollect_makeitem( + collector=self, name=name, obj=obj) + + def _istestclasscandidate(self, name, obj): + if self.classnamefilter(name) and \ + inspect.isclass(obj): + if hasinit(obj): + # XXX WARN + return False + return True + + def _genfunctions(self, name, funcobj): + module = self.getparent(Module).obj + clscol = self.getparent(Class) + cls = clscol and clscol.obj or None + metafunc = Metafunc(funcobj, config=self.config, + cls=cls, module=module) + gentesthook = self.config.hook.pytest_generate_tests + plugins = getplugins(self, withpy=True) + gentesthook.pcall(plugins, metafunc=metafunc) + if not metafunc._calls: + return Function(name, parent=self) + l = [] + for callspec in metafunc._calls: + subname = "%s[%s]" %(name, callspec.id) + function = Function(name=subname, parent=self, + callspec=callspec, callobj=funcobj, keywords={callspec.id:True}) + l.append(function) + return l + +class Module(pytest.File, PyCollectorMixin): + def _getobj(self): + return self._memoizedcall('_obj', self._importtestmodule) + + def _importtestmodule(self): + # we assume we are only called once per module + try: + mod = self.fspath.pyimport(ensuresyspath=True) + except SyntaxError: + excinfo = py.code.ExceptionInfo() + raise self.CollectError(excinfo.getrepr(style="short")) + except self.fspath.ImportMismatchError: + e = sys.exc_info()[1] + raise self.CollectError( + "import file mismatch:\n" + "imported module %r has this __file__ attribute:\n" + " %s\n" + "which is not the same as the test file we want to collect:\n" + " %s\n" + "HINT: use a unique basename for your test file modules" + % e.args + ) + #print "imported test module", mod + self.config.pluginmanager.consider_module(mod) + return mod + + def setup(self): + if hasattr(self.obj, 'setup_module'): + #XXX: nose compat hack, move to nose plugin + # if it takes a positional arg, its probably a pytest style one + # so we pass the current module object + if inspect.getargspec(self.obj.setup_module)[0]: + self.obj.setup_module(self.obj) + else: + self.obj.setup_module() + + def teardown(self): + if hasattr(self.obj, 'teardown_module'): + #XXX: nose compat hack, move to nose plugin + # if it takes a positional arg, its probably a py.test style one + # so we pass the current module object + if inspect.getargspec(self.obj.teardown_module)[0]: + self.obj.teardown_module(self.obj) + else: + self.obj.teardown_module() + +class Class(PyCollectorMixin, pytest.Collector): + + def collect(self): + return [Instance(name="()", parent=self)] + + def setup(self): + setup_class = getattr(self.obj, 'setup_class', None) + if setup_class is not None: + setup_class = getattr(setup_class, 'im_func', setup_class) + setup_class(self.obj) + + def teardown(self): + teardown_class = getattr(self.obj, 'teardown_class', None) + if teardown_class is not None: + teardown_class = getattr(teardown_class, 'im_func', teardown_class) + teardown_class(self.obj) + +class Instance(PyCollectorMixin, pytest.Collector): + def _getobj(self): + return self.parent.obj() + + def newinstance(self): + self.obj = self._getobj() + return self.obj + +class FunctionMixin(PyobjMixin): + """ mixin for the code common to Function and Generator. + """ + + def setup(self): + """ perform setup for this test function. """ + if inspect.ismethod(self.obj): + name = 'setup_method' + else: + name = 'setup_function' + if isinstance(self.parent, Instance): + obj = self.parent.newinstance() + self.obj = self._getobj() + else: + obj = self.parent.obj + setup_func_or_method = getattr(obj, name, None) + if setup_func_or_method is not None: + setup_func_or_method(self.obj) + + def teardown(self): + """ perform teardown for this test function. """ + if inspect.ismethod(self.obj): + name = 'teardown_method' + else: + name = 'teardown_function' + obj = self.parent.obj + teardown_func_or_meth = getattr(obj, name, None) + if teardown_func_or_meth is not None: + teardown_func_or_meth(self.obj) + + def _prunetraceback(self, excinfo): + if hasattr(self, '_obj') and not self.config.option.fulltrace: + code = py.code.Code(self.obj) + path, firstlineno = code.path, code.firstlineno + traceback = excinfo.traceback + ntraceback = traceback.cut(path=path, firstlineno=firstlineno) + if ntraceback == traceback: + ntraceback = ntraceback.cut(path=path) + if ntraceback == traceback: + ntraceback = ntraceback.cut(excludepath=cutdir) + excinfo.traceback = ntraceback.filter() + + def _repr_failure_py(self, excinfo, style="long"): + if excinfo.errisinstance(FuncargRequest.LookupError): + fspath, lineno, msg = self.reportinfo() + lines, _ = inspect.getsourcelines(self.obj) + for i, line in enumerate(lines): + if line.strip().startswith('def'): + return FuncargLookupErrorRepr(fspath, lineno, + lines[:i+1], str(excinfo.value)) + return super(FunctionMixin, self)._repr_failure_py(excinfo, + style=style) + + def repr_failure(self, excinfo, outerr=None): + assert outerr is None, "XXX outerr usage is deprecated" + return self._repr_failure_py(excinfo, + style=self.config.option.tbstyle) + +class FuncargLookupErrorRepr(TerminalRepr): + def __init__(self, filename, firstlineno, deflines, errorstring): + self.deflines = deflines + self.errorstring = errorstring + self.filename = filename + self.firstlineno = firstlineno + + def toterminal(self, tw): + tw.line() + for line in self.deflines: + tw.line(" " + line.strip()) + for line in self.errorstring.split("\n"): + tw.line(" " + line.strip(), red=True) + tw.line() + tw.line("%s:%d" % (self.filename, self.firstlineno+1)) + +class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector): + def collect(self): + # test generators are seen as collectors but they also + # invoke setup/teardown on popular request + # (induced by the common "test_*" naming shared with normal tests) + self.config._setupstate.prepare(self) + + l = [] + seen = {} + for i, x in enumerate(self.obj()): + name, call, args = self.getcallargs(x) + if not py.builtin.callable(call): + raise TypeError("%r yielded non callable test %r" %(self.obj, call,)) + if name is None: + name = "[%d]" % i + else: + name = "['%s']" % name + if name in seen: + raise ValueError("%r generated tests with non-unique name %r" %(self, name)) + seen[name] = True + l.append(Function(name, self, args=args, callobj=call)) + return l + + def getcallargs(self, obj): + if not isinstance(obj, (tuple, list)): + obj = (obj,) + # explict naming + if isinstance(obj[0], py.builtin._basestring): + name = obj[0] + obj = obj[1:] + else: + name = None + call, args = obj[0], obj[1:] + return name, call, args + + +# +# Test Items +# +_dummy = object() +class Function(FunctionMixin, pytest.Item): + """ a Function Item is responsible for setting up + and executing a Python callable test object. + """ + _genid = None + def __init__(self, name, parent=None, args=None, config=None, + callspec=None, callobj=_dummy, keywords=None, session=None): + super(Function, self).__init__(name, parent, + config=config, session=session) + self._args = args + if self._isyieldedfunction(): + assert not callspec, ( + "yielded functions (deprecated) cannot have funcargs") + else: + if callspec is not None: + self.funcargs = callspec.funcargs or {} + self._genid = callspec.id + if hasattr(callspec, "param"): + self._requestparam = callspec.param + else: + self.funcargs = {} + if callobj is not _dummy: + self._obj = callobj + self.function = getattr(self.obj, 'im_func', self.obj) + self.keywords.update(py.builtin._getfuncdict(self.obj) or {}) + if keywords: + self.keywords.update(keywords) + + def _getobj(self): + name = self.name + i = name.find("[") # parametrization + if i != -1: + name = name[:i] + return getattr(self.parent.obj, name) + + def _isyieldedfunction(self): + return self._args is not None + + def runtest(self): + """ execute the underlying test function. """ + self.ihook.pytest_pyfunc_call(pyfuncitem=self) + + def setup(self): + super(Function, self).setup() + if hasattr(self, 'funcargs'): + fillfuncargs(self) + + def __eq__(self, other): + try: + return (self.name == other.name and + self._args == other._args and + self.parent == other.parent and + self.obj == other.obj and + getattr(self, '_genid', None) == + getattr(other, '_genid', None) + ) + except AttributeError: + pass + return False + + def __ne__(self, other): + return not self == other + + def __hash__(self): + return hash((self.parent, self.name)) + +def hasinit(obj): + init = getattr(obj, '__init__', None) + if init: + if init != object.__init__: + return True + + +def getfuncargnames(function): + # XXX merge with main.py's varnames + argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] + startindex = py.std.inspect.ismethod(function) and 1 or 0 + defaults = getattr(function, 'func_defaults', + getattr(function, '__defaults__', None)) or () + numdefaults = len(defaults) + if numdefaults: + return argnames[startindex:-numdefaults] + return argnames[startindex:] + +def fillfuncargs(function): + """ fill missing funcargs. """ + request = FuncargRequest(pyfuncitem=function) + request._fillfuncargs() + +def getplugins(node, withpy=False): # might by any node + plugins = node.config._getmatchingplugins(node.fspath) + if withpy: + mod = node.getparent(pytest.Module) + if mod is not None: + plugins.append(mod.obj) + inst = node.getparent(pytest.Instance) + if inst is not None: + plugins.append(inst.obj) + return plugins + +_notexists = object() +class CallSpec: + def __init__(self, funcargs, id, param): + self.funcargs = funcargs + self.id = id + if param is not _notexists: + self.param = param + def __repr__(self): + return "" %( + self.id, getattr(self, 'param', '?'), self.funcargs) + +class Metafunc: + def __init__(self, function, config=None, cls=None, module=None): + self.config = config + self.module = module + self.function = function + self.funcargnames = getfuncargnames(function) + self.cls = cls + self.module = module + self._calls = [] + self._ids = py.builtin.set() + + def addcall(self, funcargs=None, id=_notexists, param=_notexists): + """ add a new call to the underlying test function during the + collection phase of a test run. + + :arg funcargs: argument keyword dictionary used when invoking + the test function. + + :arg id: used for reporting and identification purposes. If you + don't supply an `id` the length of the currently + list of calls to the test function will be used. + + :arg param: will be exposed to a later funcarg factory invocation + through the ``request.param`` attribute. Setting it (instead of + directly providing a ``funcargs`` ditionary) is called + *indirect parametrization*. Indirect parametrization is + preferable if test values are expensive to setup or can + only be created after certain fixtures or test-run related + initialization code has been run. + """ + assert funcargs is None or isinstance(funcargs, dict) + if id is None: + raise ValueError("id=None not allowed") + if id is _notexists: + id = len(self._calls) + id = str(id) + if id in self._ids: + raise ValueError("duplicate id %r" % id) + self._ids.add(id) + self._calls.append(CallSpec(funcargs, id, param)) + +class FuncargRequest: + """ A request for function arguments from a test function. """ + _argprefix = "pytest_funcarg__" + _argname = None + + class LookupError(LookupError): + """ error on performing funcarg request. """ + + def __init__(self, pyfuncitem): + self._pyfuncitem = pyfuncitem + if hasattr(pyfuncitem, '_requestparam'): + self.param = pyfuncitem._requestparam + self._plugins = getplugins(pyfuncitem, withpy=True) + self._funcargs = self._pyfuncitem.funcargs.copy() + self._name2factory = {} + self._currentarg = None + + @property + def function(self): + """ function object of the test invocation. """ + return self._pyfuncitem.obj + + @property + def keywords(self): + """ keywords of the test function item. + + .. versionadded:: 2.0 + """ + return self._pyfuncitem.keywords + + @property + def module(self): + """ module where the test function was collected. """ + return self._pyfuncitem.getparent(pytest.Module).obj + + @property + def cls(self): + """ class (can be None) where the test function was collected. """ + clscol = self._pyfuncitem.getparent(pytest.Class) + if clscol: + return clscol.obj + @property + def instance(self): + """ instance (can be None) on which test function was collected. """ + return py.builtin._getimself(self.function) + + @property + def config(self): + """ the pytest config object associated with this request. """ + return self._pyfuncitem.config + + @property + def fspath(self): + """ the file system path of the test module which collected this test. """ + return self._pyfuncitem.fspath + + def _fillfuncargs(self): + argnames = getfuncargnames(self.function) + if argnames: + assert not getattr(self._pyfuncitem, '_args', None), ( + "yielded functions cannot have funcargs") + for argname in argnames: + if argname not in self._pyfuncitem.funcargs: + self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname) + + + def applymarker(self, marker): + """ apply a marker to a single test function invocation. + This method is useful if you don't want to have a keyword/marker + on all function invocations. + + :arg marker: a :py:class:`_pytest.mark.MarkDecorator` object + created by a call to ``py.test.mark.NAME(...)``. + """ + if not isinstance(marker, py.test.mark.XYZ.__class__): + raise ValueError("%r is not a py.test.mark.* object") + self._pyfuncitem.keywords[marker.markname] = marker + + def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): + """ return a testing resource managed by ``setup`` & + ``teardown`` calls. ``scope`` and ``extrakey`` determine when the + ``teardown`` function will be called so that subsequent calls to + ``setup`` would recreate the resource. + + :arg teardown: function receiving a previously setup resource. + :arg setup: a no-argument function creating a resource. + :arg scope: a string value out of ``function``, ``module`` or + ``session`` indicating the caching lifecycle of the resource. + :arg extrakey: added to internal caching key of (funcargname, scope). + """ + if not hasattr(self.config, '_setupcache'): + self.config._setupcache = {} # XXX weakref? + cachekey = (self._currentarg, self._getscopeitem(scope), extrakey) + cache = self.config._setupcache + try: + val = cache[cachekey] + except KeyError: + val = setup() + cache[cachekey] = val + if teardown is not None: + def finalizer(): + del cache[cachekey] + teardown(val) + self._addfinalizer(finalizer, scope=scope) + return val + + def getfuncargvalue(self, argname): + """ Retrieve a function argument by name for this test + function invocation. This allows one function argument factory + to call another function argument factory. If there are two + funcarg factories for the same test function argument the first + factory may use ``getfuncargvalue`` to call the second one and + do something additional with the resource. + """ + try: + return self._funcargs[argname] + except KeyError: + pass + if argname not in self._name2factory: + self._name2factory[argname] = self.config.pluginmanager.listattr( + plugins=self._plugins, + attrname=self._argprefix + str(argname) + ) + #else: we are called recursively + if not self._name2factory[argname]: + self._raiselookupfailed(argname) + funcargfactory = self._name2factory[argname].pop() + oldarg = self._currentarg + self._currentarg = argname + try: + self._funcargs[argname] = res = funcargfactory(request=self) + finally: + self._currentarg = oldarg + return res + + def _getscopeitem(self, scope): + if scope == "function": + return self._pyfuncitem + elif scope == "module": + return self._pyfuncitem.getparent(pytest.Module) + elif scope == "session": + return None + raise ValueError("unknown finalization scope %r" %(scope,)) + + def addfinalizer(self, finalizer): + """add finalizer function to be called after test function + finished execution. """ + self._addfinalizer(finalizer, scope="function") + + def _addfinalizer(self, finalizer, scope): + colitem = self._getscopeitem(scope) + self.config._setupstate.addfinalizer( + finalizer=finalizer, colitem=colitem) + + def __repr__(self): + return "" %(self._pyfuncitem) + + def _raiselookupfailed(self, argname): + available = [] + for plugin in self._plugins: + for name in vars(plugin): + if name.startswith(self._argprefix): + name = name[len(self._argprefix):] + if name not in available: + available.append(name) + fspath, lineno, msg = self._pyfuncitem.reportinfo() + msg = "LookupError: no factory found for function argument %r" % (argname,) + msg += "\n available funcargs: %s" %(", ".join(available),) + msg += "\n use 'py.test --funcargs [testpath]' for help on them." + raise self.LookupError(msg) + +def showfuncargs(config): + from _pytest.session import Session + session = Session(config) + session.perform_collect() + if session.items: + plugins = getplugins(session.items[0]) + else: + plugins = getplugins(session) + curdir = py.path.local() + tw = py.io.TerminalWriter() + verbose = config.getvalue("verbose") + for plugin in plugins: + available = [] + for name, factory in vars(plugin).items(): + if name.startswith(FuncargRequest._argprefix): + name = name[len(FuncargRequest._argprefix):] + if name not in available: + available.append([name, factory]) + if available: + pluginname = plugin.__name__ + for name, factory in available: + loc = getlocation(factory, curdir) + if verbose: + funcargspec = "%s -- %s" %(name, loc,) + else: + funcargspec = name + tw.line(funcargspec, green=True) + doc = factory.__doc__ or "" + if doc: + for line in doc.split("\n"): + tw.line(" " + line.strip()) + else: + tw.line(" %s: no docstring available" %(loc,), + red=True) + +def getlocation(function, curdir): + import inspect + fn = py.path.local(inspect.getfile(function)) + lineno = py.builtin._getcode(function).co_firstlineno + if fn.relto(curdir): + fn = fn.relto(curdir) + return "%s:%d" %(fn, lineno+1) + +# builtin pytest.raises helper + +def raises(ExpectedException, *args, **kwargs): + """ assert that a code block/function call raises an exception. + + If using Python 2.5 or above, you may use this function as a + context manager:: + + >>> with raises(ZeroDivisionError): + ... 1/0 + + Or you can one of two forms: + + if args[0] is callable: raise AssertionError if calling it with + the remaining arguments does not raise the expected exception. + if args[0] is a string: raise AssertionError if executing the + the string in the calling scope does not raise expected exception. + examples: + >>> x = 5 + >>> raises(TypeError, lambda x: x + 'hello', x=x) + >>> raises(TypeError, "x + 'hello'") + """ + __tracebackhide__ = True + + if not args: + return RaisesContext(ExpectedException) + elif isinstance(args[0], str): + code, = args + assert isinstance(code, str) + frame = sys._getframe(1) + loc = frame.f_locals.copy() + loc.update(kwargs) + #print "raises frame scope: %r" % frame.f_locals + try: + code = py.code.Source(code).compile() + py.builtin.exec_(code, frame.f_globals, loc) + # XXX didn'T mean f_globals == f_locals something special? + # this is destroyed here ... + except ExpectedException: + return py.code.ExceptionInfo() + else: + func = args[0] + try: + func(*args[1:], **kwargs) + except ExpectedException: + return py.code.ExceptionInfo() + k = ", ".join(["%s=%r" % x for x in kwargs.items()]) + if k: + k = ', ' + k + expr = '%s(%r%s)' %(getattr(func, '__name__', func), args, k) + pytest.fail("DID NOT RAISE") + +class RaisesContext(object): + def __init__(self, ExpectedException): + self.ExpectedException = ExpectedException + self.excinfo = None + + def __enter__(self): + self.excinfo = object.__new__(py.code.ExceptionInfo) + return self.excinfo + + def __exit__(self, *tp): + __tracebackhide__ = True + if tp[0] is None: + pytest.fail("DID NOT RAISE") + self.excinfo.__init__(tp) + return issubclass(self.excinfo.type, self.ExpectedException) --- /dev/null +++ b/_pytest/unittest.py @@ -0,0 +1,52 @@ +""" discovery and running of std-library "unittest" style tests. """ +import pytest, py +import sys + +def pytest_pycollect_makeitem(collector, name, obj): + unittest = sys.modules.get('unittest') + if unittest is None: + return # nobody can have derived unittest.TestCase + try: + isunit = issubclass(obj, unittest.TestCase) + except KeyboardInterrupt: + raise + except Exception: + pass + else: + if isunit: + return UnitTestCase(name, parent=collector) + +class UnitTestCase(pytest.Class): + def collect(self): + loader = py.std.unittest.TestLoader() + for name in loader.getTestCaseNames(self.obj): + yield TestCaseFunction(name, parent=self) + + def setup(self): + meth = getattr(self.obj, 'setUpClass', None) + if meth is not None: + meth() + + def teardown(self): + meth = getattr(self.obj, 'tearDownClass', None) + if meth is not None: + meth() + +class TestCaseFunction(pytest.Function): + def setup(self): + pass + def teardown(self): + pass + def startTest(self, testcase): + pass + def addError(self, testcase, rawexcinfo): + py.builtin._reraise(*rawexcinfo) + def addFailure(self, testcase, rawexcinfo): + py.builtin._reraise(*rawexcinfo) + def addSuccess(self, testcase): + pass + def stopTest(self, testcase): + pass + def runtest(self): + testcase = self.parent.obj(self.name) + testcase(result=self) --- /dev/null +++ b/_pytest/pytester.py @@ -0,0 +1,653 @@ +""" (disabled by default) support for testing py.test and py.test plugins. """ + +import py, pytest +import sys, os +import re +import inspect +import time +from fnmatch import fnmatch +from _pytest.session import Session +from py.builtin import print_ +from _pytest.core import HookRelay + +def pytest_addoption(parser): + group = parser.getgroup("pylib") + group.addoption('--no-tools-on-path', + action="store_true", dest="notoolsonpath", default=False, + help=("discover tools on PATH instead of going through py.cmdline.") + ) + +def pytest_funcarg___pytest(request): + return PytestArg(request) + +class PytestArg: + def __init__(self, request): + self.request = request + + def gethookrecorder(self, hook): + hookrecorder = HookRecorder(hook._pm) + hookrecorder.start_recording(hook._hookspecs) + self.request.addfinalizer(hookrecorder.finish_recording) + return hookrecorder + +class ParsedCall: + def __init__(self, name, locals): + assert '_name' not in locals + self.__dict__.update(locals) + self.__dict__.pop('self') + self._name = name + + def __repr__(self): + d = self.__dict__.copy() + del d['_name'] + return "" %(self._name, d) + +class HookRecorder: + def __init__(self, pluginmanager): + self._pluginmanager = pluginmanager + self.calls = [] + self._recorders = {} + + def start_recording(self, hookspecs): + if not isinstance(hookspecs, (list, tuple)): + hookspecs = [hookspecs] + for hookspec in hookspecs: + assert hookspec not in self._recorders + class RecordCalls: + _recorder = self + for name, method in vars(hookspec).items(): + if name[0] != "_": + setattr(RecordCalls, name, self._makecallparser(method)) + recorder = RecordCalls() + self._recorders[hookspec] = recorder + self._pluginmanager.register(recorder) + self.hook = HookRelay(hookspecs, pm=self._pluginmanager, + prefix="pytest_") + + def finish_recording(self): + for recorder in self._recorders.values(): + self._pluginmanager.unregister(recorder) + self._recorders.clear() + + def _makecallparser(self, method): + name = method.__name__ + args, varargs, varkw, default = py.std.inspect.getargspec(method) + if not args or args[0] != "self": + args.insert(0, 'self') + fspec = py.std.inspect.formatargspec(args, varargs, varkw, default) + # we use exec because we want to have early type + # errors on wrong input arguments, using + # *args/**kwargs delays this and gives errors + # elsewhere + exec (py.code.compile(""" + def %(name)s%(fspec)s: + self._recorder.calls.append( + ParsedCall(%(name)r, locals())) + """ % locals())) + return locals()[name] + + def getcalls(self, names): + if isinstance(names, str): + names = names.split() + for name in names: + for cls in self._recorders: + if name in vars(cls): + break + else: + raise ValueError("callname %r not found in %r" %( + name, self._recorders.keys())) + l = [] + for call in self.calls: + if call._name in names: + l.append(call) + return l + + def contains(self, entries): + __tracebackhide__ = True + from py.builtin import print_ + i = 0 + entries = list(entries) + backlocals = py.std.sys._getframe(1).f_locals + while entries: + name, check = entries.pop(0) + for ind, call in enumerate(self.calls[i:]): + if call._name == name: + print_("NAMEMATCH", name, call) + if eval(check, backlocals, call.__dict__): + print_("CHECKERMATCH", repr(check), "->", call) + else: + print_("NOCHECKERMATCH", repr(check), "-", call) + continue + i += ind + 1 + break + print_("NONAMEMATCH", name, "with", call) + else: + py.test.fail("could not find %r check %r" % (name, check)) + + def popcall(self, name): + for i, call in enumerate(self.calls): + if call._name == name: + del self.calls[i] + return call + raise ValueError("could not find call %r" %(name, )) + + def getcall(self, name): + l = self.getcalls(name) + assert len(l) == 1, (name, l) + return l[0] + + +def pytest_funcarg__linecomp(request): + return LineComp() + +def pytest_funcarg__LineMatcher(request): + return LineMatcher + +def pytest_funcarg__testdir(request): + tmptestdir = TmpTestdir(request) + return tmptestdir + +rex_outcome = re.compile("(\d+) (\w+)") +class RunResult: + def __init__(self, ret, outlines, errlines, duration): + self.ret = ret + self.outlines = outlines + self.errlines = errlines + self.stdout = LineMatcher(outlines) + self.stderr = LineMatcher(errlines) + self.duration = duration + + def parseoutcomes(self): + for line in reversed(self.outlines): + if 'seconds' in line: + outcomes = rex_outcome.findall(line) + if outcomes: + d = {} + for num, cat in outcomes: + d[cat] = int(num) + return d + +class TmpTestdir: + def __init__(self, request): + self.request = request + self.Config = request.config.__class__ + self._pytest = request.getfuncargvalue("_pytest") + # XXX remove duplication with tmpdir plugin + basetmp = request.config.ensuretemp("testdir") + name = request.function.__name__ + for i in range(100): + try: + tmpdir = basetmp.mkdir(name + str(i)) + except py.error.EEXIST: + continue + break + # we need to create another subdir + # because Directory.collect() currently loads + # conftest.py from sibling directories + self.tmpdir = tmpdir.mkdir(name) + self.plugins = [] + self._syspathremove = [] + self.chdir() # always chdir + self.request.addfinalizer(self.finalize) + + def __repr__(self): + return "" % (self.tmpdir,) + + def finalize(self): + for p in self._syspathremove: + py.std.sys.path.remove(p) + if hasattr(self, '_olddir'): + self._olddir.chdir() + # delete modules that have been loaded from tmpdir + for name, mod in list(sys.modules.items()): + if mod: + fn = getattr(mod, '__file__', None) + if fn and fn.startswith(str(self.tmpdir)): + del sys.modules[name] + + def getreportrecorder(self, obj): + if hasattr(obj, 'config'): + obj = obj.config + if hasattr(obj, 'hook'): + obj = obj.hook + assert hasattr(obj, '_hookspecs'), obj + reprec = ReportRecorder(obj) + reprec.hookrecorder = self._pytest.gethookrecorder(obj) + reprec.hook = reprec.hookrecorder.hook + return reprec + + def chdir(self): + old = self.tmpdir.chdir() + if not hasattr(self, '_olddir'): + self._olddir = old + + def _makefile(self, ext, args, kwargs): + items = list(kwargs.items()) + if args: + source = "\n".join(map(str, args)) + "\n" + basename = self.request.function.__name__ + items.insert(0, (basename, source)) + ret = None + for name, value in items: + p = self.tmpdir.join(name).new(ext=ext) + source = str(py.code.Source(value)).lstrip() + p.write(source.encode("utf-8"), "wb") + if ret is None: + ret = p + return ret + + + def makefile(self, ext, *args, **kwargs): + return self._makefile(ext, args, kwargs) + + def makeini(self, source): + return self.makefile('cfg', setup=source) + + def makeconftest(self, source): + return self.makepyfile(conftest=source) + + def makeini(self, source): + return self.makefile('.ini', tox=source) + + def getinicfg(self, source): + p = self.makeini(source) + return py.iniconfig.IniConfig(p)['pytest'] + + def makepyfile(self, *args, **kwargs): + return self._makefile('.py', args, kwargs) + + def maketxtfile(self, *args, **kwargs): + return self._makefile('.txt', args, kwargs) + + def syspathinsert(self, path=None): + if path is None: + path = self.tmpdir + py.std.sys.path.insert(0, str(path)) + self._syspathremove.append(str(path)) + + def mkdir(self, name): + return self.tmpdir.mkdir(name) + + def mkpydir(self, name): + p = self.mkdir(name) + p.ensure("__init__.py") + return p + + Session = Session + def getnode(self, config, arg): + session = Session(config) + assert '::' not in str(arg) + p = py.path.local(arg) + x = session.fspath.bestrelpath(p) + return session.perform_collect([x], genitems=False)[0] + + def getpathnode(self, path): + config = self.parseconfig(path) + session = Session(config) + x = session.fspath.bestrelpath(path) + return session.perform_collect([x], genitems=False)[0] + + def genitems(self, colitems): + session = colitems[0].session + result = [] + for colitem in colitems: + result.extend(session.genitems(colitem)) + return result + + def inline_genitems(self, *args): + #config = self.parseconfig(*args) + config = self.parseconfigure(*args) + rec = self.getreportrecorder(config) + session = Session(config) + session.perform_collect() + return session.items, rec + + def runitem(self, source): + # used from runner functional tests + item = self.getitem(source) + # the test class where we are called from wants to provide the runner + testclassinstance = py.builtin._getimself(self.request.function) + runner = testclassinstance.getrunner() + return runner(item) + + def inline_runsource(self, source, *cmdlineargs): + p = self.makepyfile(source) + l = list(cmdlineargs) + [p] + return self.inline_run(*l) + + def inline_runsource1(self, *args): + args = list(args) + source = args.pop() + p = self.makepyfile(source) + l = list(args) + [p] + reprec = self.inline_run(*l) + reports = reprec.getreports("pytest_runtest_logreport") + assert len(reports) == 1, reports + return reports[0] + + def inline_run(self, *args): + args = ("-s", ) + args # otherwise FD leakage + config = self.parseconfig(*args) + reprec = self.getreportrecorder(config) + #config.pluginmanager.do_configure(config) + config.hook.pytest_cmdline_main(config=config) + #config.pluginmanager.do_unconfigure(config) + return reprec + + def config_preparse(self): + config = self.Config() + for plugin in self.plugins: + if isinstance(plugin, str): + config.pluginmanager.import_plugin(plugin) + else: + if isinstance(plugin, dict): + plugin = PseudoPlugin(plugin) + if not config.pluginmanager.isregistered(plugin): + config.pluginmanager.register(plugin) + return config + + def parseconfig(self, *args): + if not args: + args = (self.tmpdir,) + config = self.config_preparse() + args = list(args) + ["--basetemp=%s" % self.tmpdir.dirpath('basetemp')] + config.parse(args) + return config + + def reparseconfig(self, args=None): + """ this is used from tests that want to re-invoke parse(). """ + if not args: + args = [self.tmpdir] + oldconfig = getattr(py.test, 'config', None) + try: + c = py.test.config = self.Config() + c.basetemp = oldconfig.mktemp("reparse", numbered=True) + c.parse(args) + return c + finally: + py.test.config = oldconfig + + def parseconfigure(self, *args): + config = self.parseconfig(*args) + config.pluginmanager.do_configure(config) + self.request.addfinalizer(lambda: + config.pluginmanager.do_unconfigure(config)) + return config + + def getitem(self, source, funcname="test_func"): + for item in self.getitems(source): + if item.name == funcname: + return item + assert 0, "%r item not found in module:\n%s" %(funcname, source) + + def getitems(self, source): + modcol = self.getmodulecol(source) + return self.genitems([modcol]) + + def getmodulecol(self, source, configargs=(), withinit=False): + kw = {self.request.function.__name__: py.code.Source(source).strip()} + path = self.makepyfile(**kw) + if withinit: + self.makepyfile(__init__ = "#") + self.config = config = self.parseconfigure(path, *configargs) + node = self.getnode(config, path) + #config.pluginmanager.do_unconfigure(config) + return node + + def collect_by_name(self, modcol, name): + for colitem in modcol._memocollect(): + if colitem.name == name: + return colitem + + def popen(self, cmdargs, stdout, stderr, **kw): + if not hasattr(py.std, 'subprocess'): + py.test.skip("no subprocess module") + env = os.environ.copy() + env['PYTHONPATH'] = ":".join(filter(None, [ + str(os.getcwd()), env.get('PYTHONPATH', '')])) + kw['env'] = env + #print "env", env + return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) + + def pytestmain(self, *args, **kwargs): + ret = pytest.main(*args, **kwargs) + if ret == 2: + raise KeyboardInterrupt() + def run(self, *cmdargs): + return self._run(*cmdargs) + + def _run(self, *cmdargs): + cmdargs = [str(x) for x in cmdargs] + p1 = self.tmpdir.join("stdout") + p2 = self.tmpdir.join("stderr") + print_("running", cmdargs, "curdir=", py.path.local()) + f1 = p1.open("wb") + f2 = p2.open("wb") + now = time.time() + popen = self.popen(cmdargs, stdout=f1, stderr=f2, + close_fds=(sys.platform != "win32")) + ret = popen.wait() + f1.close() + f2.close() + out = p1.read("rb") + out = getdecoded(out).splitlines() + err = p2.read("rb") + err = getdecoded(err).splitlines() + def dump_lines(lines, fp): + try: + for line in lines: + py.builtin.print_(line, file=fp) + except UnicodeEncodeError: + print("couldn't print to %s because of encoding" % (fp,)) + dump_lines(out, sys.stdout) + dump_lines(err, sys.stderr) + return RunResult(ret, out, err, time.time()-now) + + def runpybin(self, scriptname, *args): + fullargs = self._getpybinargs(scriptname) + args + return self.run(*fullargs) + + def _getpybinargs(self, scriptname): + if not self.request.config.getvalue("notoolsonpath"): + script = py.path.local.sysfind(scriptname) + assert script, "script %r not found" % scriptname + return (py.std.sys.executable, script,) + else: + py.test.skip("cannot run %r with --no-tools-on-path" % scriptname) + + def runpython(self, script, prepend=True): + if prepend: + s = self._getsysprepend() + if s: + script.write(s + "\n" + script.read()) + return self.run(sys.executable, script) + + def _getsysprepend(self): + if self.request.config.getvalue("notoolsonpath"): + s = "import sys;sys.path.insert(0,%r);" % str(py._pydir.dirpath()) + else: + s = "" + return s + + def runpython_c(self, command): + command = self._getsysprepend() + command + return self.run(py.std.sys.executable, "-c", command) + + def runpytest(self, *args): + p = py.path.local.make_numbered_dir(prefix="runpytest-", + keep=None, rootdir=self.tmpdir) + args = ('--basetemp=%s' % p, ) + args + #for x in args: + # if '--confcutdir' in str(x): + # break + #else: + # pass + # args = ('--confcutdir=.',) + args + plugins = [x for x in self.plugins if isinstance(x, str)] + if plugins: + args = ('-p', plugins[0]) + args + return self.runpybin("py.test", *args) + + def spawn_pytest(self, string, expect_timeout=10.0): + if self.request.config.getvalue("notoolsonpath"): + py.test.skip("--no-tools-on-path prevents running pexpect-spawn tests") + basetemp = self.tmpdir.mkdir("pexpect") + invoke = " ".join(map(str, self._getpybinargs("py.test"))) + cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) + return self.spawn(cmd, expect_timeout=expect_timeout) + + def spawn(self, cmd, expect_timeout=10.0): + pexpect = py.test.importorskip("pexpect", "2.4") + logfile = self.tmpdir.join("spawn.out") + child = pexpect.spawn(cmd, logfile=logfile.open("w")) + child.timeout = expect_timeout + return child + +def getdecoded(out): + try: + return out.decode("utf-8") + except UnicodeDecodeError: + return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % ( + py.io.saferepr(out),) + +class PseudoPlugin: + def __init__(self, vars): + self.__dict__.update(vars) + +class ReportRecorder(object): + def __init__(self, hook): + self.hook = hook + self.pluginmanager = hook._pm + self.pluginmanager.register(self) + + def getcall(self, name): + return self.hookrecorder.getcall(name) + + def popcall(self, name): + return self.hookrecorder.popcall(name) + + def getcalls(self, names): + """ return list of ParsedCall instances matching the given eventname. """ + return self.hookrecorder.getcalls(names) + + # functionality for test reports + + def getreports(self, names="pytest_runtest_logreport pytest_collectreport"): + return [x.report for x in self.getcalls(names)] + + def matchreport(self, inamepart="", names="pytest_runtest_logreport pytest_collectreport"): + """ return a testreport whose dotted import path matches """ + l = [] + for rep in self.getreports(names=names): + if not inamepart or inamepart in rep.nodeid.split("::"): + l.append(rep) + if not l: + raise ValueError("could not find test report matching %r: no test reports at all!" % + (inamepart,)) + if len(l) > 1: + raise ValueError("found more than one testreport matching %r: %s" %( + inamepart, l)) + return l[0] + + def getfailures(self, names='pytest_runtest_logreport pytest_collectreport'): + return [rep for rep in self.getreports(names) if rep.failed] + + def getfailedcollections(self): + return self.getfailures('pytest_collectreport') + + def listoutcomes(self): + passed = [] + skipped = [] + failed = [] + for rep in self.getreports("pytest_runtest_logreport"): + if rep.passed: + if rep.when == "call": + passed.append(rep) + elif rep.skipped: + skipped.append(rep) + elif rep.failed: + failed.append(rep) + return passed, skipped, failed + + def countoutcomes(self): + return [len(x) for x in self.listoutcomes()] + + def assertoutcome(self, passed=0, skipped=0, failed=0): + realpassed, realskipped, realfailed = self.listoutcomes() + assert passed == len(realpassed) + assert skipped == len(realskipped) + assert failed == len(realfailed) + + def clear(self): + self.hookrecorder.calls[:] = [] + + def unregister(self): + self.pluginmanager.unregister(self) + self.hookrecorder.finish_recording() + +class LineComp: + def __init__(self): + self.stringio = py.io.TextIO() + + def assert_contains_lines(self, lines2): + """ assert that lines2 are contained (linearly) in lines1. + return a list of extralines found. + """ + __tracebackhide__ = True + val = self.stringio.getvalue() + self.stringio.truncate(0) + self.stringio.seek(0) + lines1 = val.split("\n") + return LineMatcher(lines1).fnmatch_lines(lines2) + +class LineMatcher: + def __init__(self, lines): + self.lines = lines + + def str(self): + return "\n".join(self.lines) + + def _getlines(self, lines2): + if isinstance(lines2, str): + lines2 = py.code.Source(lines2) + if isinstance(lines2, py.code.Source): + lines2 = lines2.strip().lines + return lines2 + + def fnmatch_lines_random(self, lines2): + lines2 = self._getlines(lines2) + for line in lines2: + for x in self.lines: + if line == x or fnmatch(x, line): + print_("matched: ", repr(line)) + break + else: + raise ValueError("line %r not found in output" % line) + + def fnmatch_lines(self, lines2): + def show(arg1, arg2): + py.builtin.print_(arg1, arg2, file=py.std.sys.stderr) + lines2 = self._getlines(lines2) + lines1 = self.lines[:] + nextline = None + extralines = [] + __tracebackhide__ = True + for line in lines2: + nomatchprinted = False + while lines1: + nextline = lines1.pop(0) + if line == nextline: + show("exact match:", repr(line)) + break + elif fnmatch(nextline, line): + show("fnmatch:", repr(line)) + show(" with:", repr(nextline)) + break + else: + if not nomatchprinted: + show("nomatch:", repr(line)) + nomatchprinted = True + show(" and:", repr(nextline)) + extralines.append(nextline) + else: + py.test.fail("remains unmatched: %r, see stderr" % (line,)) --- a/pytest/plugin/pdb.py +++ /dev/null @@ -1,77 +0,0 @@ -""" interactive debugging with PDB, the Python Debugger. """ - -import py -import sys - -def pytest_addoption(parser): - group = parser.getgroup("general") - group._addoption('--pdb', - action="store_true", dest="usepdb", default=False, - help="start the interactive Python debugger on errors.") - -def pytest_namespace(): - return {'set_trace': pytestPDB().set_trace} - -def pytest_configure(config): - if config.getvalue("usepdb"): - config.pluginmanager.register(PdbInvoke(), 'pdbinvoke') - -class pytestPDB: - """ Pseudo PDB that defers to the real pdb. """ - item = None - - def set_trace(self): - """ invoke PDB set_trace debugging, dropping any IO capturing. """ - frame = sys._getframe().f_back - item = getattr(self, 'item', None) - if item is not None: - capman = item.config.pluginmanager.getplugin("capturemanager") - out, err = capman.suspendcapture() - if hasattr(item, 'outerr'): - item.outerr = (item.outerr[0] + out, item.outerr[1] + err) - tw = py.io.TerminalWriter() - tw.line() - tw.sep(">", "PDB set_trace (IO-capturing turned off)") - py.std.pdb.Pdb().set_trace(frame) - -def pdbitem(item): - pytestPDB.item = item -pytest_runtest_setup = pytest_runtest_call = pytest_runtest_teardown = pdbitem - -def pytest_runtest_makereport(): - pytestPDB.item = None - -class PdbInvoke: - def pytest_sessionfinish(self, session): - # don't display failures again at the end - session.config.option.tbstyle = "no" - def pytest_runtest_makereport(self, item, call, __multicall__): - if not call.excinfo or \ - call.excinfo.errisinstance(py.test.skip.Exception) or \ - call.excinfo.errisinstance(py.std.bdb.BdbQuit): - return - rep = __multicall__.execute() - if "xfail" in rep.keywords: - return rep - # we assume that the above execute() suspended capturing - tw = py.io.TerminalWriter() - tw.line() - tw.sep(">", "traceback") - rep.toterminal(tw) - tw.sep(">", "entering PDB") - post_mortem(call.excinfo._excinfo[2]) - return rep - -def post_mortem(t): - pdb = py.std.pdb - class Pdb(pdb.Pdb): - def get_stack(self, f, t): - stack, i = pdb.Pdb.get_stack(self, f, t) - if f is None: - i = max(0, len(stack) - 1) - while i and stack[i][0].f_locals.get("__tracebackhide__", False): - i-=1 - return stack, i - p = Pdb() - p.reset() - p.interaction(None, t) --- a/testing/plugin/test_helpconfig.py +++ b/testing/plugin/test_helpconfig.py @@ -1,5 +1,5 @@ import py, pytest,os -from pytest.plugin.helpconfig import collectattr +from _pytest.helpconfig import collectattr def test_version(testdir): result = testdir.runpytest("--version") --- a/pytest/plugin/tmpdir.py +++ /dev/null @@ -1,26 +0,0 @@ -""" support for providing temporary directories to test functions. """ -import pytest, py - -def pytest_configure(config): - def ensuretemp(string, dir=1): - """ (deprecated) return temporary directory path with - the given string as the trailing part. It is usually - better to use the 'tmpdir' function argument which will - take care to provide empty unique directories for each - test call even if the test is called multiple times. - """ - #py.log._apiwarn(">1.1", "use tmpdir function argument") - return config.ensuretemp(string, dir=dir) - pytest.ensuretemp = ensuretemp - -def pytest_funcarg__tmpdir(request): - """return a temporary directory path object - unique to each test function invocation, - created as a sub directory of the base temporary - directory. The returned object is a `py.path.local`_ - path object. - """ - name = request._pyfuncitem.name - name = py.std.re.sub("[\W]", "_", name) - x = request.config.mktemp(name, numbered=True) - return x.realpath() --- a/pytest/plugin/resultlog.py +++ /dev/null @@ -1,92 +0,0 @@ -""" (disabled by default) create result information in a plain text file. """ - -import py -from py.builtin import print_ - -def pytest_addoption(parser): - group = parser.getgroup("resultlog", "resultlog plugin options") - group.addoption('--resultlog', action="store", dest="resultlog", metavar="path", default=None, - help="path for machine-readable result log.") - -def pytest_configure(config): - resultlog = config.option.resultlog - # prevent opening resultlog on slave nodes (xdist) - if resultlog and not hasattr(config, 'slaveinput'): - logfile = open(resultlog, 'w', 1) # line buffered - config._resultlog = ResultLog(config, logfile) - config.pluginmanager.register(config._resultlog) - -def pytest_unconfigure(config): - resultlog = getattr(config, '_resultlog', None) - if resultlog: - resultlog.logfile.close() - del config._resultlog - config.pluginmanager.unregister(resultlog) - -def generic_path(item): - chain = item.listchain() - gpath = [chain[0].name] - fspath = chain[0].fspath - fspart = False - for node in chain[1:]: - newfspath = node.fspath - if newfspath == fspath: - if fspart: - gpath.append(':') - fspart = False - else: - gpath.append('.') - else: - gpath.append('/') - fspart = True - name = node.name - if name[0] in '([': - gpath.pop() - gpath.append(name) - fspath = newfspath - return ''.join(gpath) - -class ResultLog(object): - def __init__(self, config, logfile): - self.config = config - self.logfile = logfile # preferably line buffered - - def write_log_entry(self, testpath, lettercode, longrepr): - print_("%s %s" % (lettercode, testpath), file=self.logfile) - for line in longrepr.splitlines(): - print_(" %s" % line, file=self.logfile) - - def log_outcome(self, report, lettercode, longrepr): - testpath = getattr(report, 'nodeid', None) - if testpath is None: - testpath = report.fspath - self.write_log_entry(testpath, lettercode, longrepr) - - def pytest_runtest_logreport(self, report): - res = self.config.hook.pytest_report_teststatus(report=report) - code = res[1] - if code == 'x': - longrepr = str(report.longrepr) - elif code == 'X': - longrepr = '' - elif report.passed: - longrepr = "" - elif report.failed: - longrepr = str(report.longrepr) - elif report.skipped: - longrepr = str(report.longrepr.reprcrash.message) - self.log_outcome(report, code, longrepr) - - def pytest_collectreport(self, report): - if not report.passed: - if report.failed: - code = "F" - else: - assert report.skipped - code = "S" - longrepr = str(report.longrepr.reprcrash) - self.log_outcome(report, code, longrepr) - - def pytest_internalerror(self, excrepr): - path = excrepr.reprcrash.path - self.write_log_entry(path, '!', str(excrepr)) --- /dev/null +++ b/_pytest/terminal.py @@ -0,0 +1,420 @@ +""" terminal reporting of the full testing process. + +This is a good source for looking at the various reporting hooks. +""" +import py +import sys +import os + +def pytest_addoption(parser): + group = parser.getgroup("terminal reporting", "reporting", after="general") + group._addoption('-v', '--verbose', action="count", + dest="verbose", default=0, help="increase verbosity."), + group._addoption('-q', '--quiet', action="count", + dest="quiet", default=0, help="decreate verbosity."), + group._addoption('-r', + action="store", dest="reportchars", default=None, metavar="chars", + help="show extra test summary info as specified by chars (f)ailed, " + "(s)skipped, (x)failed, (X)passed.") + group._addoption('-l', '--showlocals', + action="store_true", dest="showlocals", default=False, + help="show locals in tracebacks (disabled by default).") + group._addoption('--report', + action="store", dest="report", default=None, metavar="opts", + help="(deprecated, use -r)") + group._addoption('--tb', metavar="style", + action="store", dest="tbstyle", default='long', + type="choice", choices=['long', 'short', 'no', 'line', 'native'], + help="traceback print mode (long/short/line/no).") + group._addoption('--fulltrace', + action="store_true", dest="fulltrace", default=False, + help="don't cut any tracebacks (default is to cut).") + +def pytest_configure(config): + config.option.verbose -= config.option.quiet + if config.option.collectonly: + reporter = CollectonlyReporter(config) + else: + # we try hard to make printing resilient against + # later changes on FD level. + stdout = py.std.sys.stdout + if hasattr(os, 'dup') and hasattr(stdout, 'fileno'): + try: + newfd = os.dup(stdout.fileno()) + #print "got newfd", newfd + except ValueError: + pass + else: + stdout = os.fdopen(newfd, stdout.mode, 1) + config._toclose = stdout + reporter = TerminalReporter(config, stdout) + config.pluginmanager.register(reporter, 'terminalreporter') + if config.option.debug or config.option.traceconfig: + def mywriter(tags, args): + msg = " ".join(map(str, args)) + reporter.write_line("[traceconfig] " + msg) + config.trace.root.setprocessor("pytest:config", mywriter) + +def pytest_unconfigure(config): + if hasattr(config, '_toclose'): + #print "closing", config._toclose, config._toclose.fileno() + config._toclose.close() + +def getreportopt(config): + reportopts = "" + optvalue = config.option.report + if optvalue: + py.builtin.print_("DEPRECATED: use -r instead of --report option.", + file=py.std.sys.stderr) + if optvalue: + for setting in optvalue.split(","): + setting = setting.strip() + if setting == "skipped": + reportopts += "s" + elif setting == "xfailed": + reportopts += "x" + reportchars = config.option.reportchars + if reportchars: + for char in reportchars: + if char not in reportopts: + reportopts += char + return reportopts + +def pytest_report_teststatus(report): + if report.passed: + letter = "." + elif report.skipped: + letter = "s" + elif report.failed: + letter = "F" + if report.when != "call": + letter = "f" + return report.outcome, letter, report.outcome.upper() + +class TerminalReporter: + def __init__(self, config, file=None): + self.config = config + self.verbosity = self.config.option.verbose + self.showheader = self.verbosity >= 0 + self.showfspath = self.verbosity >= 0 + self.showlongtestinfo = self.verbosity > 0 + + self.stats = {} + self.curdir = py.path.local() + if file is None: + file = py.std.sys.stdout + self._tw = py.io.TerminalWriter(file) + self.currentfspath = None + self.reportchars = getreportopt(config) + + def hasopt(self, char): + char = {'xfailed': 'x', 'skipped': 's'}.get(char,char) + return char in self.reportchars + + def write_fspath_result(self, fspath, res): + if fspath != self.currentfspath: + self.currentfspath = fspath + #fspath = self.curdir.bestrelpath(fspath) + self._tw.line() + #relpath = self.curdir.bestrelpath(fspath) + self._tw.write(fspath + " ") + self._tw.write(res) + + def write_ensure_prefix(self, prefix, extra="", **kwargs): + if self.currentfspath != prefix: + self._tw.line() + self.currentfspath = prefix + self._tw.write(prefix) + if extra: + self._tw.write(extra, **kwargs) + self.currentfspath = -2 + + def ensure_newline(self): + if self.currentfspath: + self._tw.line() + self.currentfspath = None + + def write_line(self, line, **markup): + line = str(line) + self.ensure_newline() + self._tw.line(line, **markup) + + def write_sep(self, sep, title=None, **markup): + self.ensure_newline() + self._tw.sep(sep, title, **markup) + + def pytest_internalerror(self, excrepr): + for line in str(excrepr).split("\n"): + self.write_line("INTERNALERROR> " + line) + return 1 + + def pytest_plugin_registered(self, plugin): + if self.config.option.traceconfig: + msg = "PLUGIN registered: %s" %(plugin,) + # XXX this event may happen during setup/teardown time + # which unfortunately captures our output here + # which garbles our output if we use self.write_line + self.write_line(msg) + + def pytest_deselected(self, items): + self.stats.setdefault('deselected', []).extend(items) + + def pytest__teardown_final_logerror(self, report): + self.stats.setdefault("error", []).append(report) + + def pytest_runtest_logstart(self, nodeid, location): + # ensure that the path is printed before the + # 1st test of a module starts running + fspath = nodeid.split("::")[0] + if self.showlongtestinfo: + line = self._locationline(fspath, *location) + self.write_ensure_prefix(line, "") + elif self.showfspath: + self.write_fspath_result(fspath, "") + + def pytest_runtest_logreport(self, report): + rep = report + res = self.config.hook.pytest_report_teststatus(report=rep) + cat, letter, word = res + self.stats.setdefault(cat, []).append(rep) + if not letter and not word: + # probably passed setup/teardown + return + if self.verbosity <= 0: + if not hasattr(rep, 'node') and self.showfspath: + self.write_fspath_result(rep.fspath, letter) + else: + self._tw.write(letter) + else: + if isinstance(word, tuple): + word, markup = word + else: + if rep.passed: + markup = {'green':True} + elif rep.failed: + markup = {'red':True} + elif rep.skipped: + markup = {'yellow':True} + line = self._locationline(str(rep.fspath), *rep.location) + if not hasattr(rep, 'node'): + self.write_ensure_prefix(line, word, **markup) + #self._tw.write(word, **markup) + else: + self.ensure_newline() + if hasattr(rep, 'node'): + self._tw.write("[%s] " % rep.node.gateway.id) + self._tw.write(word, **markup) + self._tw.write(" " + line) + self.currentfspath = -2 + + def pytest_collectreport(self, report): + if not report.passed: + if report.failed: + self.stats.setdefault("error", []).append(report) + self.write_fspath_result(report.fspath, "E") + elif report.skipped: + self.stats.setdefault("skipped", []).append(report) + self.write_fspath_result(report.fspath, "S") + + def pytest_sessionstart(self, session): + self._sessionstarttime = py.std.time.time() + if not self.showheader: + return + self.write_sep("=", "test session starts", bold=True) + verinfo = ".".join(map(str, sys.version_info[:3])) + msg = "platform %s -- Python %s" % (sys.platform, verinfo) + msg += " -- pytest-%s" % (py.test.__version__) + if self.verbosity > 0 or self.config.option.debug or \ + getattr(self.config.option, 'pastebin', None): + msg += " -- " + str(sys.executable) + self.write_line(msg) + lines = self.config.hook.pytest_report_header(config=self.config) + lines.reverse() + for line in flatten(lines): + self.write_line(line) + + def pytest_collection_finish(self): + if not self.showheader: + return + for i, testarg in enumerate(self.config.args): + self.write_line("test path %d: %s" %(i+1, testarg)) + + def pytest_sessionfinish(self, exitstatus, __multicall__): + __multicall__.execute() + self._tw.line("") + if exitstatus in (0, 1, 2): + self.summary_errors() + self.summary_failures() + self.config.hook.pytest_terminal_summary(terminalreporter=self) + if exitstatus == 2: + self._report_keyboardinterrupt() + self.summary_deselected() + self.summary_stats() + + def pytest_keyboard_interrupt(self, excinfo): + self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) + + def _report_keyboardinterrupt(self): + excrepr = self._keyboardinterrupt_memo + msg = excrepr.reprcrash.message + self.write_sep("!", msg) + if "KeyboardInterrupt" in msg: + if self.config.option.fulltrace: + excrepr.toterminal(self._tw) + else: + excrepr.reprcrash.toterminal(self._tw) + + def _locationline(self, collect_fspath, fspath, lineno, domain): + if fspath and fspath != collect_fspath: + fspath = "%s <- %s" % ( + self.curdir.bestrelpath(py.path.local(collect_fspath)), + self.curdir.bestrelpath(py.path.local(fspath))) + elif fspath: + fspath = self.curdir.bestrelpath(py.path.local(fspath)) + if lineno is not None: + lineno += 1 + if fspath and lineno and domain: + line = "%(fspath)s:%(lineno)s: %(domain)s" + elif fspath and domain: + line = "%(fspath)s: %(domain)s" + elif fspath and lineno: + line = "%(fspath)s:%(lineno)s %(extrapath)s" + else: + line = "[nolocation]" + return line % locals() + " " + + def _getfailureheadline(self, rep): + if hasattr(rep, 'location'): + fspath, lineno, domain = rep.location + return domain + else: + return "test session" # XXX? + + def _getcrashline(self, rep): + try: + return str(rep.longrepr.reprcrash) + except AttributeError: + try: + return str(rep.longrepr)[:50] + except AttributeError: + return "" + + # + # summaries for sessionfinish + # + + def summary_failures(self): + tbstyle = self.config.option.tbstyle + if 'failed' in self.stats and tbstyle != "no": + self.write_sep("=", "FAILURES") + for rep in self.stats['failed']: + if tbstyle == "line": + line = self._getcrashline(rep) + self.write_line(line) + else: + msg = self._getfailureheadline(rep) + self.write_sep("_", msg) + rep.toterminal(self._tw) + + def summary_errors(self): + if 'error' in self.stats and self.config.option.tbstyle != "no": + self.write_sep("=", "ERRORS") + for rep in self.stats['error']: + msg = self._getfailureheadline(rep) + if not hasattr(rep, 'when'): + # collect + msg = "ERROR collecting " + msg + elif rep.when == "setup": + msg = "ERROR at setup of " + msg + elif rep.when == "teardown": + msg = "ERROR at teardown of " + msg + self.write_sep("_", msg) + rep.toterminal(self._tw) + + def summary_stats(self): + session_duration = py.std.time.time() - self._sessionstarttime + + keys = "failed passed skipped deselected".split() + for key in self.stats.keys(): + if key not in keys: + keys.append(key) + parts = [] + for key in keys: + val = self.stats.get(key, None) + 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) + else: + self.write_line(msg, bold=True) + + def summary_deselected(self): + if 'deselected' in self.stats: + self.write_sep("=", "%d tests deselected by %r" %( + len(self.stats['deselected']), self.config.option.keyword), bold=True) + + +class CollectonlyReporter: + INDENT = " " + + def __init__(self, config, out=None): + self.config = config + if out is None: + out = py.std.sys.stdout + self._tw = py.io.TerminalWriter(out) + self.indent = "" + self._failed = [] + + def outindent(self, line): + self._tw.line(self.indent + str(line)) + + def pytest_internalerror(self, excrepr): + for line in str(excrepr).split("\n"): + self._tw.line("INTERNALERROR> " + line) + + def pytest_collectstart(self, collector): + if collector.session != collector: + self.outindent(collector) + self.indent += self.INDENT + + def pytest_itemcollected(self, item): + self.outindent(item) + + def pytest_collectreport(self, report): + if not report.passed: + if hasattr(report.longrepr, 'reprcrash'): + msg = report.longrepr.reprcrash.message + else: + # XXX unify (we have CollectErrorRepr here) + msg = str(report.longrepr.longrepr) + self.outindent("!!! %s !!!" % msg) + #self.outindent("!!! error !!!") + self._failed.append(report) + self.indent = self.indent[:-len(self.INDENT)] + + def pytest_collection_finish(self): + if self._failed: + self._tw.sep("!", "collection failures") + for rep in self._failed: + rep.toterminal(self._tw) + return self._failed and 1 or 0 + +def repr_pythonversion(v=None): + if v is None: + v = sys.version_info + try: + return "%s.%s.%s-%s-%s" % v + except (TypeError, ValueError): + return str(v) + +def flatten(l): + for x in l: + if isinstance(x, (list, tuple)): + for y in flatten(x): + yield y + else: + yield x + --- a/pytest/plugin/unittest.py +++ /dev/null @@ -1,52 +0,0 @@ -""" discovery and running of std-library "unittest" style tests. """ -import pytest, py -import sys - -def pytest_pycollect_makeitem(collector, name, obj): - unittest = sys.modules.get('unittest') - if unittest is None: - return # nobody can have derived unittest.TestCase - try: - isunit = issubclass(obj, unittest.TestCase) - except KeyboardInterrupt: - raise - except Exception: - pass - else: - if isunit: - return UnitTestCase(name, parent=collector) - -class UnitTestCase(pytest.Class): - def collect(self): - loader = py.std.unittest.TestLoader() - for name in loader.getTestCaseNames(self.obj): - yield TestCaseFunction(name, parent=self) - - def setup(self): - meth = getattr(self.obj, 'setUpClass', None) - if meth is not None: - meth() - - def teardown(self): - meth = getattr(self.obj, 'tearDownClass', None) - if meth is not None: - meth() - -class TestCaseFunction(pytest.Function): - def setup(self): - pass - def teardown(self): - pass - def startTest(self, testcase): - pass - def addError(self, testcase, rawexcinfo): - py.builtin._reraise(*rawexcinfo) - def addFailure(self, testcase, rawexcinfo): - py.builtin._reraise(*rawexcinfo) - def addSuccess(self, testcase): - pass - def stopTest(self, testcase): - pass - def runtest(self): - testcase = self.parent.obj(self.name) - testcase(result=self) --- a/pytest/plugin/pytester.py +++ /dev/null @@ -1,653 +0,0 @@ -""" (disabled by default) support for testing py.test and py.test plugins. """ - -import py, pytest -import sys, os -import re -import inspect -import time -from fnmatch import fnmatch -from pytest.plugin.session import Session -from py.builtin import print_ -from pytest.main import HookRelay - -def pytest_addoption(parser): - group = parser.getgroup("pylib") - group.addoption('--no-tools-on-path', - action="store_true", dest="notoolsonpath", default=False, - help=("discover tools on PATH instead of going through py.cmdline.") - ) - -def pytest_funcarg___pytest(request): - return PytestArg(request) - -class PytestArg: - def __init__(self, request): - self.request = request - - def gethookrecorder(self, hook): - hookrecorder = HookRecorder(hook._pm) - hookrecorder.start_recording(hook._hookspecs) - self.request.addfinalizer(hookrecorder.finish_recording) - return hookrecorder - -class ParsedCall: - def __init__(self, name, locals): - assert '_name' not in locals - self.__dict__.update(locals) - self.__dict__.pop('self') - self._name = name - - def __repr__(self): - d = self.__dict__.copy() - del d['_name'] - return "" %(self._name, d) - -class HookRecorder: - def __init__(self, pluginmanager): - self._pluginmanager = pluginmanager - self.calls = [] - self._recorders = {} - - def start_recording(self, hookspecs): - if not isinstance(hookspecs, (list, tuple)): - hookspecs = [hookspecs] - for hookspec in hookspecs: - assert hookspec not in self._recorders - class RecordCalls: - _recorder = self - for name, method in vars(hookspec).items(): - if name[0] != "_": - setattr(RecordCalls, name, self._makecallparser(method)) - recorder = RecordCalls() - self._recorders[hookspec] = recorder - self._pluginmanager.register(recorder) - self.hook = HookRelay(hookspecs, pm=self._pluginmanager, - prefix="pytest_") - - def finish_recording(self): - for recorder in self._recorders.values(): - self._pluginmanager.unregister(recorder) - self._recorders.clear() - - def _makecallparser(self, method): - name = method.__name__ - args, varargs, varkw, default = py.std.inspect.getargspec(method) - if not args or args[0] != "self": - args.insert(0, 'self') - fspec = py.std.inspect.formatargspec(args, varargs, varkw, default) - # we use exec because we want to have early type - # errors on wrong input arguments, using - # *args/**kwargs delays this and gives errors - # elsewhere - exec (py.code.compile(""" - def %(name)s%(fspec)s: - self._recorder.calls.append( - ParsedCall(%(name)r, locals())) - """ % locals())) - return locals()[name] - - def getcalls(self, names): - if isinstance(names, str): - names = names.split() - for name in names: - for cls in self._recorders: - if name in vars(cls): - break - else: - raise ValueError("callname %r not found in %r" %( - name, self._recorders.keys())) - l = [] - for call in self.calls: - if call._name in names: - l.append(call) - return l - - def contains(self, entries): - __tracebackhide__ = True - from py.builtin import print_ - i = 0 - entries = list(entries) - backlocals = py.std.sys._getframe(1).f_locals - while entries: - name, check = entries.pop(0) - for ind, call in enumerate(self.calls[i:]): - if call._name == name: - print_("NAMEMATCH", name, call) - if eval(check, backlocals, call.__dict__): - print_("CHECKERMATCH", repr(check), "->", call) - else: - print_("NOCHECKERMATCH", repr(check), "-", call) - continue - i += ind + 1 - break - print_("NONAMEMATCH", name, "with", call) - else: - py.test.fail("could not find %r check %r" % (name, check)) - - def popcall(self, name): - for i, call in enumerate(self.calls): - if call._name == name: - del self.calls[i] - return call - raise ValueError("could not find call %r" %(name, )) - - def getcall(self, name): - l = self.getcalls(name) - assert len(l) == 1, (name, l) - return l[0] - - -def pytest_funcarg__linecomp(request): - return LineComp() - -def pytest_funcarg__LineMatcher(request): - return LineMatcher - -def pytest_funcarg__testdir(request): - tmptestdir = TmpTestdir(request) - return tmptestdir - -rex_outcome = re.compile("(\d+) (\w+)") -class RunResult: - def __init__(self, ret, outlines, errlines, duration): - self.ret = ret - self.outlines = outlines - self.errlines = errlines - self.stdout = LineMatcher(outlines) - self.stderr = LineMatcher(errlines) - self.duration = duration - - def parseoutcomes(self): - for line in reversed(self.outlines): - if 'seconds' in line: - outcomes = rex_outcome.findall(line) - if outcomes: - d = {} - for num, cat in outcomes: - d[cat] = int(num) - return d - -class TmpTestdir: - def __init__(self, request): - self.request = request - self.Config = request.config.__class__ - self._pytest = request.getfuncargvalue("_pytest") - # XXX remove duplication with tmpdir plugin - basetmp = request.config.ensuretemp("testdir") - name = request.function.__name__ - for i in range(100): - try: - tmpdir = basetmp.mkdir(name + str(i)) - except py.error.EEXIST: - continue - break - # we need to create another subdir - # because Directory.collect() currently loads - # conftest.py from sibling directories - self.tmpdir = tmpdir.mkdir(name) - self.plugins = [] - self._syspathremove = [] - self.chdir() # always chdir - self.request.addfinalizer(self.finalize) - - def __repr__(self): - return "" % (self.tmpdir,) - - def finalize(self): - for p in self._syspathremove: - py.std.sys.path.remove(p) - if hasattr(self, '_olddir'): - self._olddir.chdir() - # delete modules that have been loaded from tmpdir - for name, mod in list(sys.modules.items()): - if mod: - fn = getattr(mod, '__file__', None) - if fn and fn.startswith(str(self.tmpdir)): - del sys.modules[name] - - def getreportrecorder(self, obj): - if hasattr(obj, 'config'): - obj = obj.config - if hasattr(obj, 'hook'): - obj = obj.hook - assert hasattr(obj, '_hookspecs'), obj - reprec = ReportRecorder(obj) - reprec.hookrecorder = self._pytest.gethookrecorder(obj) - reprec.hook = reprec.hookrecorder.hook - return reprec - - def chdir(self): - old = self.tmpdir.chdir() - if not hasattr(self, '_olddir'): - self._olddir = old - - def _makefile(self, ext, args, kwargs): - items = list(kwargs.items()) - if args: - source = "\n".join(map(str, args)) + "\n" - basename = self.request.function.__name__ - items.insert(0, (basename, source)) - ret = None - for name, value in items: - p = self.tmpdir.join(name).new(ext=ext) - source = str(py.code.Source(value)).lstrip() - p.write(source.encode("utf-8"), "wb") - if ret is None: - ret = p - return ret - - - def makefile(self, ext, *args, **kwargs): - return self._makefile(ext, args, kwargs) - - def makeini(self, source): - return self.makefile('cfg', setup=source) - - def makeconftest(self, source): - return self.makepyfile(conftest=source) - - def makeini(self, source): - return self.makefile('.ini', tox=source) - - def getinicfg(self, source): - p = self.makeini(source) - return py.iniconfig.IniConfig(p)['pytest'] - - def makepyfile(self, *args, **kwargs): - return self._makefile('.py', args, kwargs) - - def maketxtfile(self, *args, **kwargs): - return self._makefile('.txt', args, kwargs) - - def syspathinsert(self, path=None): - if path is None: - path = self.tmpdir - py.std.sys.path.insert(0, str(path)) - self._syspathremove.append(str(path)) - - def mkdir(self, name): - return self.tmpdir.mkdir(name) - - def mkpydir(self, name): - p = self.mkdir(name) - p.ensure("__init__.py") - return p - - Session = Session - def getnode(self, config, arg): - session = Session(config) - assert '::' not in str(arg) - p = py.path.local(arg) - x = session.fspath.bestrelpath(p) - return session.perform_collect([x], genitems=False)[0] - - def getpathnode(self, path): - config = self.parseconfig(path) - session = Session(config) - x = session.fspath.bestrelpath(path) - return session.perform_collect([x], genitems=False)[0] - - def genitems(self, colitems): - session = colitems[0].session - result = [] - for colitem in colitems: - result.extend(session.genitems(colitem)) - return result - - def inline_genitems(self, *args): - #config = self.parseconfig(*args) - config = self.parseconfigure(*args) - rec = self.getreportrecorder(config) - session = Session(config) - session.perform_collect() - return session.items, rec - - def runitem(self, source): - # used from runner functional tests - item = self.getitem(source) - # the test class where we are called from wants to provide the runner - testclassinstance = py.builtin._getimself(self.request.function) - runner = testclassinstance.getrunner() - return runner(item) - - def inline_runsource(self, source, *cmdlineargs): - p = self.makepyfile(source) - l = list(cmdlineargs) + [p] - return self.inline_run(*l) - - def inline_runsource1(self, *args): - args = list(args) - source = args.pop() - p = self.makepyfile(source) - l = list(args) + [p] - reprec = self.inline_run(*l) - reports = reprec.getreports("pytest_runtest_logreport") - assert len(reports) == 1, reports - return reports[0] - - def inline_run(self, *args): - args = ("-s", ) + args # otherwise FD leakage - config = self.parseconfig(*args) - reprec = self.getreportrecorder(config) - #config.pluginmanager.do_configure(config) - config.hook.pytest_cmdline_main(config=config) - #config.pluginmanager.do_unconfigure(config) - return reprec - - def config_preparse(self): - config = self.Config() - for plugin in self.plugins: - if isinstance(plugin, str): - config.pluginmanager.import_plugin(plugin) - else: - if isinstance(plugin, dict): - plugin = PseudoPlugin(plugin) - if not config.pluginmanager.isregistered(plugin): - config.pluginmanager.register(plugin) - return config - - def parseconfig(self, *args): - if not args: - args = (self.tmpdir,) - config = self.config_preparse() - args = list(args) + ["--basetemp=%s" % self.tmpdir.dirpath('basetemp')] - config.parse(args) - return config - - def reparseconfig(self, args=None): - """ this is used from tests that want to re-invoke parse(). """ - if not args: - args = [self.tmpdir] - oldconfig = getattr(py.test, 'config', None) - try: - c = py.test.config = self.Config() - c.basetemp = oldconfig.mktemp("reparse", numbered=True) - c.parse(args) - return c - finally: - py.test.config = oldconfig - - def parseconfigure(self, *args): - config = self.parseconfig(*args) - config.pluginmanager.do_configure(config) - self.request.addfinalizer(lambda: - config.pluginmanager.do_unconfigure(config)) - return config - - def getitem(self, source, funcname="test_func"): - for item in self.getitems(source): - if item.name == funcname: - return item - assert 0, "%r item not found in module:\n%s" %(funcname, source) - - def getitems(self, source): - modcol = self.getmodulecol(source) - return self.genitems([modcol]) - - def getmodulecol(self, source, configargs=(), withinit=False): - kw = {self.request.function.__name__: py.code.Source(source).strip()} - path = self.makepyfile(**kw) - if withinit: - self.makepyfile(__init__ = "#") - self.config = config = self.parseconfigure(path, *configargs) - node = self.getnode(config, path) - #config.pluginmanager.do_unconfigure(config) - return node - - def collect_by_name(self, modcol, name): - for colitem in modcol._memocollect(): - if colitem.name == name: - return colitem - - def popen(self, cmdargs, stdout, stderr, **kw): - if not hasattr(py.std, 'subprocess'): - py.test.skip("no subprocess module") - env = os.environ.copy() - env['PYTHONPATH'] = ":".join(filter(None, [ - str(os.getcwd()), env.get('PYTHONPATH', '')])) - kw['env'] = env - #print "env", env - return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw) - - def pytestmain(self, *args, **kwargs): - ret = pytest.main(*args, **kwargs) - if ret == 2: - raise KeyboardInterrupt() - def run(self, *cmdargs): - return self._run(*cmdargs) - - def _run(self, *cmdargs): - cmdargs = [str(x) for x in cmdargs] - p1 = self.tmpdir.join("stdout") - p2 = self.tmpdir.join("stderr") - print_("running", cmdargs, "curdir=", py.path.local()) - f1 = p1.open("wb") - f2 = p2.open("wb") - now = time.time() - popen = self.popen(cmdargs, stdout=f1, stderr=f2, - close_fds=(sys.platform != "win32")) - ret = popen.wait() - f1.close() - f2.close() - out = p1.read("rb") - out = getdecoded(out).splitlines() - err = p2.read("rb") - err = getdecoded(err).splitlines() - def dump_lines(lines, fp): - try: - for line in lines: - py.builtin.print_(line, file=fp) - except UnicodeEncodeError: - print("couldn't print to %s because of encoding" % (fp,)) - dump_lines(out, sys.stdout) - dump_lines(err, sys.stderr) - return RunResult(ret, out, err, time.time()-now) - - def runpybin(self, scriptname, *args): - fullargs = self._getpybinargs(scriptname) + args - return self.run(*fullargs) - - def _getpybinargs(self, scriptname): - if not self.request.config.getvalue("notoolsonpath"): - script = py.path.local.sysfind(scriptname) - assert script, "script %r not found" % scriptname - return (py.std.sys.executable, script,) - else: - py.test.skip("cannot run %r with --no-tools-on-path" % scriptname) - - def runpython(self, script, prepend=True): - if prepend: - s = self._getsysprepend() - if s: - script.write(s + "\n" + script.read()) - return self.run(sys.executable, script) - - def _getsysprepend(self): - if self.request.config.getvalue("notoolsonpath"): - s = "import sys;sys.path.insert(0,%r);" % str(py._pydir.dirpath()) - else: - s = "" - return s - - def runpython_c(self, command): - command = self._getsysprepend() + command - return self.run(py.std.sys.executable, "-c", command) - - def runpytest(self, *args): - p = py.path.local.make_numbered_dir(prefix="runpytest-", - keep=None, rootdir=self.tmpdir) - args = ('--basetemp=%s' % p, ) + args - #for x in args: - # if '--confcutdir' in str(x): - # break - #else: - # pass - # args = ('--confcutdir=.',) + args - plugins = [x for x in self.plugins if isinstance(x, str)] - if plugins: - args = ('-p', plugins[0]) + args - return self.runpybin("py.test", *args) - - def spawn_pytest(self, string, expect_timeout=10.0): - if self.request.config.getvalue("notoolsonpath"): - py.test.skip("--no-tools-on-path prevents running pexpect-spawn tests") - basetemp = self.tmpdir.mkdir("pexpect") - invoke = self._getpybinargs("py.test")[0] - cmd = "%s --basetemp=%s %s" % (invoke, basetemp, string) - return self.spawn(cmd, expect_timeout=expect_timeout) - - def spawn(self, cmd, expect_timeout=10.0): - pexpect = py.test.importorskip("pexpect", "2.4") - logfile = self.tmpdir.join("spawn.out") - child = pexpect.spawn(cmd, logfile=logfile.open("w")) - child.timeout = expect_timeout - return child - -def getdecoded(out): - try: - return out.decode("utf-8") - except UnicodeDecodeError: - return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % ( - py.io.saferepr(out),) - -class PseudoPlugin: - def __init__(self, vars): - self.__dict__.update(vars) - -class ReportRecorder(object): - def __init__(self, hook): - self.hook = hook - self.pluginmanager = hook._pm - self.pluginmanager.register(self) - - def getcall(self, name): - return self.hookrecorder.getcall(name) - - def popcall(self, name): - return self.hookrecorder.popcall(name) - - def getcalls(self, names): - """ return list of ParsedCall instances matching the given eventname. """ - return self.hookrecorder.getcalls(names) - - # functionality for test reports - - def getreports(self, names="pytest_runtest_logreport pytest_collectreport"): - return [x.report for x in self.getcalls(names)] - - def matchreport(self, inamepart="", names="pytest_runtest_logreport pytest_collectreport"): - """ return a testreport whose dotted import path matches """ - l = [] - for rep in self.getreports(names=names): - if not inamepart or inamepart in rep.nodeid.split("::"): - l.append(rep) - if not l: - raise ValueError("could not find test report matching %r: no test reports at all!" % - (inamepart,)) - if len(l) > 1: - raise ValueError("found more than one testreport matching %r: %s" %( - inamepart, l)) - return l[0] - - def getfailures(self, names='pytest_runtest_logreport pytest_collectreport'): - return [rep for rep in self.getreports(names) if rep.failed] - - def getfailedcollections(self): - return self.getfailures('pytest_collectreport') - - def listoutcomes(self): - passed = [] - skipped = [] - failed = [] - for rep in self.getreports("pytest_runtest_logreport"): - if rep.passed: - if rep.when == "call": - passed.append(rep) - elif rep.skipped: - skipped.append(rep) - elif rep.failed: - failed.append(rep) - return passed, skipped, failed - - def countoutcomes(self): - return [len(x) for x in self.listoutcomes()] - - def assertoutcome(self, passed=0, skipped=0, failed=0): - realpassed, realskipped, realfailed = self.listoutcomes() - assert passed == len(realpassed) - assert skipped == len(realskipped) - assert failed == len(realfailed) - - def clear(self): - self.hookrecorder.calls[:] = [] - - def unregister(self): - self.pluginmanager.unregister(self) - self.hookrecorder.finish_recording() - -class LineComp: - def __init__(self): - self.stringio = py.io.TextIO() - - def assert_contains_lines(self, lines2): - """ assert that lines2 are contained (linearly) in lines1. - return a list of extralines found. - """ - __tracebackhide__ = True - val = self.stringio.getvalue() - self.stringio.truncate(0) - self.stringio.seek(0) - lines1 = val.split("\n") - return LineMatcher(lines1).fnmatch_lines(lines2) - -class LineMatcher: - def __init__(self, lines): - self.lines = lines - - def str(self): - return "\n".join(self.lines) - - def _getlines(self, lines2): - if isinstance(lines2, str): - lines2 = py.code.Source(lines2) - if isinstance(lines2, py.code.Source): - lines2 = lines2.strip().lines - return lines2 - - def fnmatch_lines_random(self, lines2): - lines2 = self._getlines(lines2) - for line in lines2: - for x in self.lines: - if line == x or fnmatch(x, line): - print_("matched: ", repr(line)) - break - else: - raise ValueError("line %r not found in output" % line) - - def fnmatch_lines(self, lines2): - def show(arg1, arg2): - py.builtin.print_(arg1, arg2, file=py.std.sys.stderr) - lines2 = self._getlines(lines2) - lines1 = self.lines[:] - nextline = None - extralines = [] - __tracebackhide__ = True - for line in lines2: - nomatchprinted = False - while lines1: - nextline = lines1.pop(0) - if line == nextline: - show("exact match:", repr(line)) - break - elif fnmatch(nextline, line): - show("fnmatch:", repr(line)) - show(" with:", repr(nextline)) - break - else: - if not nomatchprinted: - show("nomatch:", repr(line)) - nomatchprinted = True - show(" and:", repr(nextline)) - extralines.append(nextline) - else: - py.test.fail("remains unmatched: %r, see stderr" % (line,)) --- a/pytest/plugin/genscript.py +++ /dev/null @@ -1,68 +0,0 @@ -""" generate a single-file self-contained version of py.test """ -import py -import pickle -import zlib -import base64 - -def find_toplevel(name): - for syspath in py.std.sys.path: - base = py.path.local(syspath) - lib = base/name - if lib.check(dir=1): - return lib - raise LookupError(name) - -def pkgname(toplevel, rootpath, path): - parts = path.parts()[len(rootpath.parts()):] - return '.'.join([toplevel] + [x.purebasename for x in parts]) - -def pkg_to_mapping(name): - toplevel = find_toplevel(name) - name2src = {} - for pyfile in toplevel.visit('*.py'): - pkg = pkgname(name, toplevel, pyfile) - name2src[pkg] = pyfile.read() - return name2src - - -def compress_mapping(mapping): - data = pickle.dumps(mapping, 2) - data = zlib.compress(data, 9) - data = base64.encodestring(data) - data = data.decode('ascii') - return data - - -def compress_packages(names): - mapping = {} - for name in names: - mapping.update(pkg_to_mapping(name)) - return compress_mapping(mapping) - - -def generate_script(entry, packages): - data = compress_packages(packages) - tmpl = py.path.local(__file__).dirpath().join('standalonetemplate.py') - exe = tmpl.read() - exe = exe.replace('@SOURCES@', data) - exe = exe.replace('@ENTRY@', entry) - return exe - - -def pytest_addoption(parser): - group = parser.getgroup("debugconfig") - group.addoption("--genscript", action="store", default=None, - dest="genscript", metavar="path", - help="create standalone py.test script at given target path.") - -def pytest_cmdline_main(config): - genscript = config.getvalue("genscript") - if genscript: - script = generate_script( - 'import py; raise SystemExit(py.test.cmdline.main())', - ['py', 'pytest'], - ) - - genscript = py.path.local(genscript) - genscript.write(script) - return 0 --- a/pytest/plugin/skipping.py +++ /dev/null @@ -1,213 +0,0 @@ -""" support for skip/xfail functions and markers. """ - -import py, pytest - -def pytest_addoption(parser): - group = parser.getgroup("general") - group.addoption('--runxfail', - action="store_true", dest="runxfail", default=False, - help="run tests even if they are marked xfail") - -def pytest_namespace(): - return dict(xfail=xfail) - -class XFailed(pytest.fail.Exception): - """ raised from an explicit call to py.test.xfail() """ - -def xfail(reason=""): - """ xfail an executing test or setup functions with the given reason.""" - __tracebackhide__ = True - raise XFailed(reason) -xfail.Exception = XFailed - -class MarkEvaluator: - def __init__(self, item, name): - self.item = item - self.name = name - - @property - def holder(self): - return self.item.keywords.get(self.name, None) - def __bool__(self): - return bool(self.holder) - __nonzero__ = __bool__ - - def istrue(self): - if self.holder: - d = {'os': py.std.os, 'sys': py.std.sys, 'config': self.item.config} - if self.holder.args: - self.result = False - for expr in self.holder.args: - self.expr = expr - if isinstance(expr, str): - result = cached_eval(self.item.config, expr, d) - else: - result = expr - if result: - self.result = True - self.expr = expr - break - else: - self.result = True - return getattr(self, 'result', False) - - def get(self, attr, default=None): - return self.holder.kwargs.get(attr, default) - - def getexplanation(self): - expl = self.get('reason', None) - if not expl: - if not hasattr(self, 'expr'): - return "" - else: - return "condition: " + self.expr - return expl - - -def pytest_runtest_setup(item): - if not isinstance(item, pytest.Function): - return - evalskip = MarkEvaluator(item, 'skipif') - if evalskip.istrue(): - py.test.skip(evalskip.getexplanation()) - item._evalxfail = MarkEvaluator(item, 'xfail') - check_xfail_no_run(item) - -def pytest_pyfunc_call(pyfuncitem): - check_xfail_no_run(pyfuncitem) - -def check_xfail_no_run(item): - if not item.config.option.runxfail: - evalxfail = item._evalxfail - if evalxfail.istrue(): - if not evalxfail.get('run', True): - py.test.xfail("[NOTRUN] " + evalxfail.getexplanation()) - -def pytest_runtest_makereport(__multicall__, item, call): - if not isinstance(item, pytest.Function): - return - if not (call.excinfo and - call.excinfo.errisinstance(py.test.xfail.Exception)): - evalxfail = getattr(item, '_evalxfail', None) - if not evalxfail: - return - if call.excinfo and call.excinfo.errisinstance(py.test.xfail.Exception): - if not item.config.getvalue("runxfail"): - rep = __multicall__.execute() - rep.keywords['xfail'] = "reason: " + call.excinfo.value.msg - rep.outcome = "skipped" - return rep - if call.when == "call": - rep = __multicall__.execute() - evalxfail = getattr(item, '_evalxfail') - if not item.config.getvalue("runxfail") and evalxfail.istrue(): - if call.excinfo: - rep.outcome = "skipped" - else: - rep.outcome = "failed" - rep.keywords['xfail'] = evalxfail.getexplanation() - else: - if 'xfail' in rep.keywords: - del rep.keywords['xfail'] - return rep - -# called by terminalreporter progress reporting -def pytest_report_teststatus(report): - if 'xfail' in report.keywords: - if report.skipped: - return "xfailed", "x", "xfail" - elif report.failed: - return "xpassed", "X", "XPASS" - -# called by the terminalreporter instance/plugin -def pytest_terminal_summary(terminalreporter): - tr = terminalreporter - if not tr.reportchars: - #for name in "xfailed skipped failed xpassed": - # if not tr.stats.get(name, 0): - # tr.write_line("HINT: use '-r' option to see extra " - # "summary info about tests") - # break - return - - lines = [] - for char in tr.reportchars: - if char == "x": - show_xfailed(terminalreporter, lines) - elif char == "X": - show_xpassed(terminalreporter, lines) - elif char in "fF": - show_failed(terminalreporter, lines) - elif char in "sS": - show_skipped(terminalreporter, lines) - if lines: - tr._tw.sep("=", "short test summary info") - for line in lines: - tr._tw.line(line) - -def show_failed(terminalreporter, lines): - tw = terminalreporter._tw - failed = terminalreporter.stats.get("failed") - if failed: - for rep in failed: - pos = rep.nodeid - lines.append("FAIL %s" %(pos, )) - -def show_xfailed(terminalreporter, lines): - xfailed = terminalreporter.stats.get("xfailed") - if xfailed: - for rep in xfailed: - pos = rep.nodeid - reason = rep.keywords['xfail'] - lines.append("XFAIL %s" % (pos,)) - if reason: - lines.append(" " + str(reason)) - -def show_xpassed(terminalreporter, lines): - xpassed = terminalreporter.stats.get("xpassed") - if xpassed: - for rep in xpassed: - pos = rep.nodeid - reason = rep.keywords['xfail'] - lines.append("XPASS %s %s" %(pos, reason)) - -def cached_eval(config, expr, d): - if not hasattr(config, '_evalcache'): - config._evalcache = {} - try: - return config._evalcache[expr] - except KeyError: - #import sys - #print >>sys.stderr, ("cache-miss: %r" % expr) - config._evalcache[expr] = x = eval(expr, d) - return x - - -def folded_skips(skipped): - d = {} - for event in skipped: - entry = event.longrepr.reprcrash - key = entry.path, entry.lineno, entry.message - d.setdefault(key, []).append(event) - l = [] - for key, events in d.items(): - l.append((len(events),) + key) - return l - -def show_skipped(terminalreporter, lines): - tr = terminalreporter - skipped = tr.stats.get('skipped', []) - if skipped: - #if not tr.hasopt('skipped'): - # tr.write_line( - # "%d skipped tests, specify -rs for more info" % - # len(skipped)) - # return - fskips = folded_skips(skipped) - if fskips: - #tr.write_sep("_", "skipped test summary") - for num, fspath, lineno, reason in fskips: - if reason.startswith("Skipped: "): - reason = reason[9:] - lines.append("SKIP [%d] %s:%d: %s" % - (num, fspath, lineno, reason)) --- a/pytest/plugin/terminal.py +++ /dev/null @@ -1,420 +0,0 @@ -""" terminal reporting of the full testing process. - -This is a good source for looking at the various reporting hooks. -""" -import py -import sys -import os - -def pytest_addoption(parser): - group = parser.getgroup("terminal reporting", "reporting", after="general") - group._addoption('-v', '--verbose', action="count", - dest="verbose", default=0, help="increase verbosity."), - group._addoption('-q', '--quiet', action="count", - dest="quiet", default=0, help="decreate verbosity."), - group._addoption('-r', - action="store", dest="reportchars", default=None, metavar="chars", - help="show extra test summary info as specified by chars (f)ailed, " - "(s)skipped, (x)failed, (X)passed.") - group._addoption('-l', '--showlocals', - action="store_true", dest="showlocals", default=False, - help="show locals in tracebacks (disabled by default).") - group._addoption('--report', - action="store", dest="report", default=None, metavar="opts", - help="(deprecated, use -r)") - group._addoption('--tb', metavar="style", - action="store", dest="tbstyle", default='long', - type="choice", choices=['long', 'short', 'no', 'line', 'native'], - help="traceback print mode (long/short/line/no).") - group._addoption('--fulltrace', - action="store_true", dest="fulltrace", default=False, - help="don't cut any tracebacks (default is to cut).") - -def pytest_configure(config): - config.option.verbose -= config.option.quiet - if config.option.collectonly: - reporter = CollectonlyReporter(config) - else: - # we try hard to make printing resilient against - # later changes on FD level. - stdout = py.std.sys.stdout - if hasattr(os, 'dup') and hasattr(stdout, 'fileno'): - try: - newfd = os.dup(stdout.fileno()) - #print "got newfd", newfd - except ValueError: - pass - else: - stdout = os.fdopen(newfd, stdout.mode, 1) - config._toclose = stdout - reporter = TerminalReporter(config, stdout) - config.pluginmanager.register(reporter, 'terminalreporter') - if config.option.debug or config.option.traceconfig: - def mywriter(tags, args): - msg = " ".join(map(str, args)) - reporter.write_line("[traceconfig] " + msg) - config.trace.root.setprocessor("pytest:config", mywriter) - -def pytest_unconfigure(config): - if hasattr(config, '_toclose'): - #print "closing", config._toclose, config._toclose.fileno() - config._toclose.close() - -def getreportopt(config): - reportopts = "" - optvalue = config.option.report - if optvalue: - py.builtin.print_("DEPRECATED: use -r instead of --report option.", - file=py.std.sys.stderr) - if optvalue: - for setting in optvalue.split(","): - setting = setting.strip() - if setting == "skipped": - reportopts += "s" - elif setting == "xfailed": - reportopts += "x" - reportchars = config.option.reportchars - if reportchars: - for char in reportchars: - if char not in reportopts: - reportopts += char - return reportopts - -def pytest_report_teststatus(report): - if report.passed: - letter = "." - elif report.skipped: - letter = "s" - elif report.failed: - letter = "F" - if report.when != "call": - letter = "f" - return report.outcome, letter, report.outcome.upper() - -class TerminalReporter: - def __init__(self, config, file=None): - self.config = config - self.verbosity = self.config.option.verbose - self.showheader = self.verbosity >= 0 - self.showfspath = self.verbosity >= 0 - self.showlongtestinfo = self.verbosity > 0 - - self.stats = {} - self.curdir = py.path.local() - if file is None: - file = py.std.sys.stdout - self._tw = py.io.TerminalWriter(file) - self.currentfspath = None - self.reportchars = getreportopt(config) - - def hasopt(self, char): - char = {'xfailed': 'x', 'skipped': 's'}.get(char,char) - return char in self.reportchars - - def write_fspath_result(self, fspath, res): - if fspath != self.currentfspath: - self.currentfspath = fspath - #fspath = self.curdir.bestrelpath(fspath) - self._tw.line() - #relpath = self.curdir.bestrelpath(fspath) - self._tw.write(fspath + " ") - self._tw.write(res) - - def write_ensure_prefix(self, prefix, extra="", **kwargs): - if self.currentfspath != prefix: - self._tw.line() - self.currentfspath = prefix - self._tw.write(prefix) - if extra: - self._tw.write(extra, **kwargs) - self.currentfspath = -2 - - def ensure_newline(self): - if self.currentfspath: - self._tw.line() - self.currentfspath = None - - def write_line(self, line, **markup): - line = str(line) - self.ensure_newline() - self._tw.line(line, **markup) - - def write_sep(self, sep, title=None, **markup): - self.ensure_newline() - self._tw.sep(sep, title, **markup) - - def pytest_internalerror(self, excrepr): - for line in str(excrepr).split("\n"): - self.write_line("INTERNALERROR> " + line) - return 1 - - def pytest_plugin_registered(self, plugin): - if self.config.option.traceconfig: - msg = "PLUGIN registered: %s" %(plugin,) - # XXX this event may happen during setup/teardown time - # which unfortunately captures our output here - # which garbles our output if we use self.write_line - self.write_line(msg) - - def pytest_deselected(self, items): - self.stats.setdefault('deselected', []).extend(items) - - def pytest__teardown_final_logerror(self, report): - self.stats.setdefault("error", []).append(report) - - def pytest_runtest_logstart(self, nodeid, location): - # ensure that the path is printed before the - # 1st test of a module starts running - fspath = nodeid.split("::")[0] - if self.showlongtestinfo: - line = self._locationline(fspath, *location) - self.write_ensure_prefix(line, "") - elif self.showfspath: - self.write_fspath_result(fspath, "") - - def pytest_runtest_logreport(self, report): - rep = report - res = self.config.hook.pytest_report_teststatus(report=rep) - cat, letter, word = res - self.stats.setdefault(cat, []).append(rep) - if not letter and not word: - # probably passed setup/teardown - return - if self.verbosity <= 0: - if not hasattr(rep, 'node') and self.showfspath: - self.write_fspath_result(rep.fspath, letter) - else: - self._tw.write(letter) - else: - if isinstance(word, tuple): - word, markup = word - else: - if rep.passed: - markup = {'green':True} - elif rep.failed: - markup = {'red':True} - elif rep.skipped: - markup = {'yellow':True} - line = self._locationline(str(rep.fspath), *rep.location) - if not hasattr(rep, 'node'): - self.write_ensure_prefix(line, word, **markup) - #self._tw.write(word, **markup) - else: - self.ensure_newline() - if hasattr(rep, 'node'): - self._tw.write("[%s] " % rep.node.gateway.id) - self._tw.write(word, **markup) - self._tw.write(" " + line) - self.currentfspath = -2 - - def pytest_collectreport(self, report): - if not report.passed: - if report.failed: - self.stats.setdefault("error", []).append(report) - self.write_fspath_result(report.fspath, "E") - elif report.skipped: - self.stats.setdefault("skipped", []).append(report) - self.write_fspath_result(report.fspath, "S") - - def pytest_sessionstart(self, session): - self._sessionstarttime = py.std.time.time() - if not self.showheader: - return - self.write_sep("=", "test session starts", bold=True) - verinfo = ".".join(map(str, sys.version_info[:3])) - msg = "platform %s -- Python %s" % (sys.platform, verinfo) - msg += " -- pytest-%s" % (py.test.__version__) - if self.verbosity > 0 or self.config.option.debug or \ - getattr(self.config.option, 'pastebin', None): - msg += " -- " + str(sys.executable) - self.write_line(msg) - lines = self.config.hook.pytest_report_header(config=self.config) - lines.reverse() - for line in flatten(lines): - self.write_line(line) - - def pytest_collection_finish(self): - if not self.showheader: - return - for i, testarg in enumerate(self.config.args): - self.write_line("test path %d: %s" %(i+1, testarg)) - - def pytest_sessionfinish(self, exitstatus, __multicall__): - __multicall__.execute() - self._tw.line("") - if exitstatus in (0, 1, 2): - self.summary_errors() - self.summary_failures() - self.config.hook.pytest_terminal_summary(terminalreporter=self) - if exitstatus == 2: - self._report_keyboardinterrupt() - self.summary_deselected() - self.summary_stats() - - def pytest_keyboard_interrupt(self, excinfo): - self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True) - - def _report_keyboardinterrupt(self): - excrepr = self._keyboardinterrupt_memo - msg = excrepr.reprcrash.message - self.write_sep("!", msg) - if "KeyboardInterrupt" in msg: - if self.config.option.fulltrace: - excrepr.toterminal(self._tw) - else: - excrepr.reprcrash.toterminal(self._tw) - - def _locationline(self, collect_fspath, fspath, lineno, domain): - if fspath and fspath != collect_fspath: - fspath = "%s <- %s" % ( - self.curdir.bestrelpath(py.path.local(collect_fspath)), - self.curdir.bestrelpath(py.path.local(fspath))) - elif fspath: - fspath = self.curdir.bestrelpath(py.path.local(fspath)) - if lineno is not None: - lineno += 1 - if fspath and lineno and domain: - line = "%(fspath)s:%(lineno)s: %(domain)s" - elif fspath and domain: - line = "%(fspath)s: %(domain)s" - elif fspath and lineno: - line = "%(fspath)s:%(lineno)s %(extrapath)s" - else: - line = "[nolocation]" - return line % locals() + " " - - def _getfailureheadline(self, rep): - if hasattr(rep, 'location'): - fspath, lineno, domain = rep.location - return domain - else: - return "test session" # XXX? - - def _getcrashline(self, rep): - try: - return str(rep.longrepr.reprcrash) - except AttributeError: - try: - return str(rep.longrepr)[:50] - except AttributeError: - return "" - - # - # summaries for sessionfinish - # - - def summary_failures(self): - tbstyle = self.config.option.tbstyle - if 'failed' in self.stats and tbstyle != "no": - self.write_sep("=", "FAILURES") - for rep in self.stats['failed']: - if tbstyle == "line": - line = self._getcrashline(rep) - self.write_line(line) - else: - msg = self._getfailureheadline(rep) - self.write_sep("_", msg) - rep.toterminal(self._tw) - - def summary_errors(self): - if 'error' in self.stats and self.config.option.tbstyle != "no": - self.write_sep("=", "ERRORS") - for rep in self.stats['error']: - msg = self._getfailureheadline(rep) - if not hasattr(rep, 'when'): - # collect - msg = "ERROR collecting " + msg - elif rep.when == "setup": - msg = "ERROR at setup of " + msg - elif rep.when == "teardown": - msg = "ERROR at teardown of " + msg - self.write_sep("_", msg) - rep.toterminal(self._tw) - - def summary_stats(self): - session_duration = py.std.time.time() - self._sessionstarttime - - keys = "failed passed skipped deselected".split() - for key in self.stats.keys(): - if key not in keys: - keys.append(key) - parts = [] - for key in keys: - val = self.stats.get(key, None) - 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) - else: - self.write_line(msg, bold=True) - - def summary_deselected(self): - if 'deselected' in self.stats: - self.write_sep("=", "%d tests deselected by %r" %( - len(self.stats['deselected']), self.config.option.keyword), bold=True) - - -class CollectonlyReporter: - INDENT = " " - - def __init__(self, config, out=None): - self.config = config - if out is None: - out = py.std.sys.stdout - self._tw = py.io.TerminalWriter(out) - self.indent = "" - self._failed = [] - - def outindent(self, line): - self._tw.line(self.indent + str(line)) - - def pytest_internalerror(self, excrepr): - for line in str(excrepr).split("\n"): - self._tw.line("INTERNALERROR> " + line) - - def pytest_collectstart(self, collector): - if collector.session != collector: - self.outindent(collector) - self.indent += self.INDENT - - def pytest_itemcollected(self, item): - self.outindent(item) - - def pytest_collectreport(self, report): - if not report.passed: - if hasattr(report.longrepr, 'reprcrash'): - msg = report.longrepr.reprcrash.message - else: - # XXX unify (we have CollectErrorRepr here) - msg = str(report.longrepr.longrepr) - self.outindent("!!! %s !!!" % msg) - #self.outindent("!!! error !!!") - self._failed.append(report) - self.indent = self.indent[:-len(self.INDENT)] - - def pytest_collection_finish(self): - if self._failed: - self._tw.sep("!", "collection failures") - for rep in self._failed: - rep.toterminal(self._tw) - return self._failed and 1 or 0 - -def repr_pythonversion(v=None): - if v is None: - v = sys.version_info - try: - return "%s.%s.%s-%s-%s" % v - except (TypeError, ValueError): - return str(v) - -def flatten(l): - for x in l: - if isinstance(x, (list, tuple)): - for y in flatten(x): - yield y - else: - yield x - --- a/testing/plugin/test_python.py +++ b/testing/plugin/test_python.py @@ -1,5 +1,5 @@ import pytest, py, sys -from pytest.plugin import python as funcargs +from _pytest import python as funcargs class TestModule: def test_failing_import(self, testdir): --- a/testing/plugin/test_mark.py +++ b/testing/plugin/test_mark.py @@ -1,5 +1,5 @@ import py -from pytest.plugin.mark import MarkGenerator as Mark +from _pytest.mark import MarkGenerator as Mark class TestMark: def test_pytest_mark_notcallable(self): --- /dev/null +++ b/_pytest/helpconfig.py @@ -0,0 +1,158 @@ +""" version info, help messages, tracing configuration. """ +import py +import pytest +import inspect, sys + +def pytest_addoption(parser): + group = parser.getgroup('debugconfig') + group.addoption('--version', action="store_true", + help="display pytest lib version and import information.") + group._addoption("-h", "--help", action="store_true", dest="help", + help="show help message and configuration info") + group._addoption('-p', action="append", dest="plugins", default = [], + metavar="name", + help="early-load given plugin (multi-allowed).") + group.addoption('--traceconfig', + action="store_true", dest="traceconfig", default=False, + help="trace considerations of conftest.py files."), + group._addoption('--nomagic', + action="store_true", dest="nomagic", default=False, + help="don't reinterpret asserts, no traceback cutting. ") + group.addoption('--debug', + action="store_true", dest="debug", default=False, + help="generate and show internal debugging information.") + + +def pytest_cmdline_main(config): + if config.option.version: + p = py.path.local(pytest.__file__) + sys.stderr.write("This is py.test version %s, imported from %s\n" % + (pytest.__version__, p)) + return 0 + elif config.option.help: + config.pluginmanager.do_configure(config) + showhelp(config) + return 0 + +def showhelp(config): + tw = py.io.TerminalWriter() + tw.write(config._parser.optparser.format_help()) + tw.line() + tw.line() + #tw.sep( "=", "config file settings") + tw.line("setup.cfg or tox.ini options to be put into [pytest] section:") + tw.line() + + for name in config._parser._ininames: + help, type, default = config._parser._inidict[name] + if type is None: + type = "string" + spec = "%s (%s)" % (name, type) + line = " %-24s %s" %(spec, help) + tw.line(line[:tw.fullwidth]) + + tw.line() ; tw.line() + #tw.sep("=") + return + + tw.line("conftest.py options:") + tw.line() + conftestitems = sorted(config._parser._conftestdict.items()) + for name, help in conftest_options + conftestitems: + line = " %-15s %s" %(name, help) + tw.line(line[:tw.fullwidth]) + tw.line() + #tw.sep( "=") + +conftest_options = [ + ('pytest_plugins', 'list of plugin names to load'), +] + +def pytest_report_header(config): + lines = [] + if config.option.debug or config.option.traceconfig: + lines.append("using: pytest-%s pylib-%s" % + (pytest.__version__,py.__version__)) + + if config.option.traceconfig: + lines.append("active plugins:") + plugins = [] + items = config.pluginmanager._name2plugin.items() + for name, plugin in items: + lines.append(" %-20s: %s" %(name, repr(plugin))) + return lines + + +# ===================================================== +# validate plugin syntax and hooks +# ===================================================== + +def pytest_plugin_registered(manager, plugin): + methods = collectattr(plugin) + hooks = {} + for hookspec in manager.hook._hookspecs: + hooks.update(collectattr(hookspec)) + + stringio = py.io.TextIO() + def Print(*args): + if args: + stringio.write(" ".join(map(str, args))) + stringio.write("\n") + + fail = False + while methods: + name, method = methods.popitem() + #print "checking", name + if isgenerichook(name): + continue + if name not in hooks: + if not getattr(method, 'optionalhook', False): + Print("found unknown hook:", name) + fail = True + else: + #print "checking", method + method_args = getargs(method) + #print "method_args", method_args + if '__multicall__' in method_args: + method_args.remove('__multicall__') + hook = hooks[name] + hookargs = getargs(hook) + for arg in method_args: + if arg not in hookargs: + Print("argument %r not available" %(arg, )) + Print("actual definition: %s" %(formatdef(method))) + Print("available hook arguments: %s" % + ", ".join(hookargs)) + fail = True + break + #if not fail: + # print "matching hook:", formatdef(method) + if fail: + name = getattr(plugin, '__name__', plugin) + raise PluginValidationError("%s:\n%s" %(name, stringio.getvalue())) + +class PluginValidationError(Exception): + """ plugin failed validation. """ + +def isgenerichook(name): + return name == "pytest_plugins" or \ + name.startswith("pytest_funcarg__") + +def getargs(func): + args = inspect.getargs(py.code.getrawcode(func))[0] + startindex = inspect.ismethod(func) and 1 or 0 + return args[startindex:] + +def collectattr(obj): + methods = {} + for apiname in dir(obj): + if apiname.startswith("pytest_"): + methods[apiname] = getattr(obj, apiname) + return methods + +def formatdef(func): + return "%s%s" %( + func.__name__, + inspect.formatargspec(*inspect.getargspec(func)) + ) + --- a/testing/plugin/test_genscript.py +++ b/testing/plugin/test_genscript.py @@ -22,7 +22,7 @@ def test_gen(testdir, anypython, standal result = standalone.run(anypython, testdir, '--version') assert result.ret == 0 result.stderr.fnmatch_lines([ - "*imported from*mypytest" + "*imported from*mypytest*" ]) p = testdir.makepyfile("def test_func(): assert 0") result = standalone.run(anypython, testdir, p) --- a/pytest/main.py +++ /dev/null @@ -1,450 +0,0 @@ -""" -pytest PluginManager, basic initialization and tracing. -All else is in pytest/plugin. -(c) Holger Krekel 2004-2010 -""" -import sys, os -import inspect -import py -import pytest -from pytest import hookspec # the extension point definitions - -assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " - "%s is too old, remove or upgrade 'py'" % (py.__version__)) - -default_plugins = ( - "config session terminal runner python pdb capture unittest mark skipping " - "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " - "junitxml doctest").split() - -IMPORTPREFIX = "pytest_" - -class TagTracer: - def __init__(self, prefix="[pytest] "): - self._tag2proc = {} - self.writer = None - self.indent = 0 - self.prefix = prefix - - def get(self, name): - return TagTracerSub(self, (name,)) - - def processmessage(self, tags, args): - if self.writer is not None: - if args: - indent = " " * self.indent - content = " ".join(map(str, args)) - self.writer("%s%s%s\n" %(self.prefix, indent, content)) - try: - self._tag2proc[tags](tags, args) - except KeyError: - pass - - def setwriter(self, writer): - self.writer = writer - - def setprocessor(self, tags, processor): - if isinstance(tags, str): - tags = tuple(tags.split(":")) - else: - assert isinstance(tags, tuple) - self._tag2proc[tags] = processor - -class TagTracerSub: - def __init__(self, root, tags): - self.root = root - self.tags = tags - def __call__(self, *args): - self.root.processmessage(self.tags, args) - def setmyprocessor(self, processor): - self.root.setprocessor(self.tags, processor) - def get(self, name): - return self.__class__(self.root, self.tags + (name,)) - -class PluginManager(object): - def __init__(self, load=False): - self._name2plugin = {} - self._plugins = [] - self._hints = [] - self.trace = TagTracer().get("pluginmanage") - if os.environ.get('PYTEST_DEBUG'): - err = sys.stderr - encoding = getattr(err, 'encoding', 'utf8') - try: - err = py.io.dupfile(err, encoding=encoding) - except Exception: - pass - self.trace.root.setwriter(err.write) - self.hook = HookRelay([hookspec], pm=self) - self.register(self) - if load: - for spec in default_plugins: - self.import_plugin(spec) - - def _getpluginname(self, plugin, name): - if name is None: - if hasattr(plugin, '__name__'): - name = plugin.__name__.split(".")[-1] - else: - name = id(plugin) - return name - - def register(self, plugin, name=None, prepend=False): - assert not self.isregistered(plugin), plugin - assert not self.isregistered(plugin), plugin - name = self._getpluginname(plugin, name) - if name in self._name2plugin: - return False - self._name2plugin[name] = plugin - self.call_plugin(plugin, "pytest_addhooks", {'pluginmanager': self}) - self.hook.pytest_plugin_registered(manager=self, plugin=plugin) - if not prepend: - self._plugins.append(plugin) - else: - self._plugins.insert(0, plugin) - return True - - def unregister(self, plugin=None, name=None): - if plugin is None: - plugin = self.getplugin(name=name) - self._plugins.remove(plugin) - self.hook.pytest_plugin_unregistered(plugin=plugin) - for name, value in list(self._name2plugin.items()): - if value == plugin: - del self._name2plugin[name] - - def isregistered(self, plugin, name=None): - if self._getpluginname(plugin, name) in self._name2plugin: - return True - for val in self._name2plugin.values(): - if plugin == val: - return True - - def addhooks(self, spec): - self.hook._addhooks(spec, prefix="pytest_") - - def getplugins(self): - return list(self._plugins) - - def skipifmissing(self, name): - if not self.hasplugin(name): - py.test.skip("plugin %r is missing" % name) - - def hasplugin(self, name): - try: - self.getplugin(name) - return True - except KeyError: - return False - - def getplugin(self, name): - try: - return self._name2plugin[name] - except KeyError: - impname = canonical_importname(name) - return self._name2plugin[impname] - - # API for bootstrapping - # - def _envlist(self, varname): - val = py.std.os.environ.get(varname, None) - if val is not None: - return val.split(',') - return () - - def consider_env(self): - for spec in self._envlist("PYTEST_PLUGINS"): - self.import_plugin(spec) - - def consider_setuptools_entrypoints(self): - try: - from pkg_resources import iter_entry_points - except ImportError: - return # XXX issue a warning - for ep in iter_entry_points('pytest11'): - name = canonical_importname(ep.name) - if name in self._name2plugin: - continue - plugin = ep.load() - self.register(plugin, name=name) - - def consider_preparse(self, args): - for opt1,opt2 in zip(args, args[1:]): - if opt1 == "-p": - self.import_plugin(opt2) - - def consider_conftest(self, conftestmodule): - if self.register(conftestmodule, name=conftestmodule.__file__): - self.consider_module(conftestmodule) - - def consider_module(self, mod): - attr = getattr(mod, "pytest_plugins", ()) - if attr: - if not isinstance(attr, (list, tuple)): - attr = (attr,) - for spec in attr: - self.import_plugin(spec) - - def import_plugin(self, spec): - assert isinstance(spec, str) - modname = canonical_importname(spec) - if modname in self._name2plugin: - return - try: - mod = importplugin(modname) - except KeyboardInterrupt: - raise - except: - e = py.std.sys.exc_info()[1] - if not hasattr(py.test, 'skip'): - raise - elif not isinstance(e, py.test.skip.Exception): - raise - self._hints.append("skipped plugin %r: %s" %((modname, e.msg))) - else: - self.register(mod, modname) - self.consider_module(mod) - - def pytest_plugin_registered(self, plugin): - dic = self.call_plugin(plugin, "pytest_namespace", {}) or {} - if dic: - self._setns(pytest, dic) - if hasattr(self, '_config'): - self.call_plugin(plugin, "pytest_addoption", - {'parser': self._config._parser}) - self.call_plugin(plugin, "pytest_configure", - {'config': self._config}) - - def _setns(self, obj, dic): - for name, value in dic.items(): - if isinstance(value, dict): - mod = getattr(obj, name, None) - if mod is None: - modname = "pytest.%s" % name - mod = py.std.types.ModuleType(modname) - sys.modules[modname] = mod - mod.__all__ = [] - setattr(obj, name, mod) - #print "setns", mod, value - self._setns(mod, value) - else: - #print "setting", name, value, "on", obj - setattr(obj, name, value) - obj.__all__.append(name) - #print "appending", name, "to", obj - #pytest.__all__.append(name) # don't show in help(py.test) - setattr(pytest, name, value) - - def pytest_terminal_summary(self, terminalreporter): - tw = terminalreporter._tw - if terminalreporter.config.option.traceconfig: - for hint in self._hints: - tw.line("hint: %s" % hint) - - def do_addoption(self, parser): - mname = "pytest_addoption" - methods = reversed(self.listattr(mname)) - MultiCall(methods, {'parser': parser}).execute() - - def do_configure(self, config): - assert not hasattr(self, '_config') - self._config = config - config.hook.pytest_configure(config=self._config) - - def do_unconfigure(self, config): - config = self._config - del self._config - config.hook.pytest_unconfigure(config=config) - config.pluginmanager.unregister(self) - - def notify_exception(self, excinfo): - excrepr = excinfo.getrepr(funcargs=True, showlocals=True) - res = self.hook.pytest_internalerror(excrepr=excrepr) - if not py.builtin.any(res): - for line in str(excrepr).split("\n"): - sys.stderr.write("INTERNALERROR> %s\n" %line) - sys.stderr.flush() - - def listattr(self, attrname, plugins=None): - if plugins is None: - plugins = self._plugins - l = [] - for plugin in plugins: - try: - l.append(getattr(plugin, attrname)) - except AttributeError: - continue - return l - - def call_plugin(self, plugin, methname, kwargs): - return MultiCall(methods=self.listattr(methname, plugins=[plugin]), - kwargs=kwargs, firstresult=True).execute() - -def canonical_importname(name): - if '.' in name: - return name - name = name.lower() - if not name.startswith(IMPORTPREFIX): - name = IMPORTPREFIX + name - return name - -def importplugin(importspec): - #print "importing", importspec - try: - return __import__(importspec, None, None, '__doc__') - except ImportError: - e = py.std.sys.exc_info()[1] - if str(e).find(importspec) == -1: - raise - name = importspec - try: - if name.startswith("pytest_"): - name = importspec[7:] - return __import__("pytest.plugin.%s" %(name), None, None, '__doc__') - except ImportError: - e = py.std.sys.exc_info()[1] - if str(e).find(name) == -1: - raise - # show the original exception, not the failing internal one - return __import__(importspec, None, None, '__doc__') - - -class MultiCall: - """ execute a call into multiple python functions/methods. """ - def __init__(self, methods, kwargs, firstresult=False): - self.methods = list(methods) - self.kwargs = kwargs - self.results = [] - self.firstresult = firstresult - - def __repr__(self): - status = "%d results, %d meths" % (len(self.results), len(self.methods)) - return "" %(status, self.kwargs) - - def execute(self): - while self.methods: - method = self.methods.pop() - kwargs = self.getkwargs(method) - res = method(**kwargs) - if res is not None: - self.results.append(res) - if self.firstresult: - return res - if not self.firstresult: - return self.results - - def getkwargs(self, method): - kwargs = {} - for argname in varnames(method): - try: - kwargs[argname] = self.kwargs[argname] - except KeyError: - if argname == "__multicall__": - kwargs[argname] = self - return kwargs - -def varnames(func): - if not inspect.isfunction(func) and not inspect.ismethod(func): - func = getattr(func, '__call__', func) - ismethod = inspect.ismethod(func) - rawcode = py.code.getrawcode(func) - try: - return rawcode.co_varnames[ismethod:rawcode.co_argcount] - except AttributeError: - return () - -class HookRelay: - def __init__(self, hookspecs, pm, prefix="pytest_"): - if not isinstance(hookspecs, list): - hookspecs = [hookspecs] - self._hookspecs = [] - self._pm = pm - self.trace = pm.trace.root.get("hook") - for hookspec in hookspecs: - self._addhooks(hookspec, prefix) - - def _addhooks(self, hookspecs, prefix): - self._hookspecs.append(hookspecs) - added = False - for name, method in vars(hookspecs).items(): - if name.startswith(prefix): - if not method.__doc__: - raise ValueError("docstring required for hook %r, in %r" - % (method, hookspecs)) - firstresult = getattr(method, 'firstresult', False) - hc = HookCaller(self, name, firstresult=firstresult) - setattr(self, name, hc) - added = True - #print ("setting new hook", name) - if not added: - raise ValueError("did not find new %r hooks in %r" %( - prefix, hookspecs,)) - - -class HookCaller: - def __init__(self, hookrelay, name, firstresult): - self.hookrelay = hookrelay - self.name = name - self.firstresult = firstresult - self.trace = self.hookrelay.trace - - def __repr__(self): - return "" %(self.name,) - - def __call__(self, **kwargs): - methods = self.hookrelay._pm.listattr(self.name) - return self._docall(methods, kwargs) - - def pcall(self, plugins, **kwargs): - methods = self.hookrelay._pm.listattr(self.name, plugins=plugins) - return self._docall(methods, kwargs) - - def _docall(self, methods, kwargs): - self.trace(self.name, kwargs) - self.trace.root.indent += 1 - mc = MultiCall(methods, kwargs, firstresult=self.firstresult) - try: - res = mc.execute() - if res: - self.trace("finish", self.name, "-->", res) - finally: - self.trace.root.indent -= 1 - return res - -_preinit = [PluginManager(load=True)] # triggers default plugin importing - -def main(args=None, plugins=None): - """ returned exit code integer, after an in-process testing run - with the given command line arguments, preloading an optional list - of passed in plugin objects. """ - if args is None: - args = sys.argv[1:] - elif isinstance(args, py.path.local): - args = [str(args)] - elif not isinstance(args, (tuple, list)): - if not isinstance(args, str): - raise ValueError("not a string or argument list: %r" % (args,)) - args = py.std.shlex.split(args) - if _preinit: - _pluginmanager = _preinit.pop(0) - else: # subsequent calls to main will create a fresh instance - _pluginmanager = PluginManager(load=True) - hook = _pluginmanager.hook - try: - if plugins: - for plugin in plugins: - _pluginmanager.register(plugin) - config = hook.pytest_cmdline_parse( - pluginmanager=_pluginmanager, args=args) - exitstatus = hook.pytest_cmdline_main(config=config) - except UsageError: - e = sys.exc_info()[1] - sys.stderr.write("ERROR: %s\n" %(e.args[0],)) - exitstatus = 3 - return exitstatus - -class UsageError(Exception): - """ error in py.test usage or invocation""" - -if __name__ == '__main__': - raise SystemExit(main()) --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -2,7 +2,7 @@ creating and managing test function arguments ============================================================== -.. currentmodule:: pytest.plugin.python +.. currentmodule:: _pytest.python .. _`funcargs`: @@ -92,7 +92,7 @@ Each funcarg factory receives a **reques specific test function call. A request object is passed to a funcarg factory and provides access to test configuration and context: -.. autoclass:: pytest.plugin.python.FuncargRequest() +.. autoclass:: _pytest.python.FuncargRequest() :members: function,cls,module,keywords,config .. _`useful caching and finalization helpers`: --- a/pytest/plugin/junitxml.py +++ /dev/null @@ -1,174 +0,0 @@ -""" report test results in JUnit-XML format, for use with Hudson and build integration servers. - -Based on initial code from Ross Lawley. -""" - -import py -import os -import time - -def pytest_addoption(parser): - group = parser.getgroup("terminal reporting") - group.addoption('--junitxml', action="store", dest="xmlpath", - metavar="path", default=None, - help="create junit-xml style report file at given path.") - group.addoption('--junitprefix', action="store", dest="junitprefix", - metavar="str", default=None, - help="prepend prefix to classnames in junit-xml output") - -def pytest_configure(config): - xmlpath = config.option.xmlpath - if xmlpath: - config._xml = LogXML(xmlpath, config.option.junitprefix) - config.pluginmanager.register(config._xml) - -def pytest_unconfigure(config): - xml = getattr(config, '_xml', None) - if xml: - del config._xml - config.pluginmanager.unregister(xml) - -class LogXML(object): - def __init__(self, logfile, prefix): - self.logfile = logfile - self.prefix = prefix - self.test_logs = [] - self.passed = self.skipped = 0 - self.failed = self.errors = 0 - self._durations = {} - - def _opentestcase(self, report): - names = report.nodeid.split("::") - names[0] = names[0].replace("/", '.') - names = tuple(names) - d = {'time': self._durations.pop(names, "0")} - names = [x.replace(".py", "") for x in names if x != "()"] - classnames = names[:-1] - if self.prefix: - classnames.insert(0, self.prefix) - d['classname'] = ".".join(classnames) - d['name'] = py.xml.escape(names[-1]) - attrs = ['%s="%s"' % item for item in sorted(d.items())] - self.test_logs.append("\n" % " ".join(attrs)) - - def _closetestcase(self): - self.test_logs.append("") - - def appendlog(self, fmt, *args): - args = tuple([py.xml.escape(arg) for arg in args]) - self.test_logs.append(fmt % args) - - def append_pass(self, report): - self.passed += 1 - self._opentestcase(report) - self._closetestcase() - - def append_failure(self, report): - self._opentestcase(report) - #msg = str(report.longrepr.reprtraceback.extraline) - if "xfail" in report.keywords: - self.appendlog( - '') - self.skipped += 1 - else: - self.appendlog('%s', - report.longrepr) - self.failed += 1 - self._closetestcase() - - def append_collect_failure(self, report): - self._opentestcase(report) - #msg = str(report.longrepr.reprtraceback.extraline) - self.appendlog('%s', - report.longrepr) - self._closetestcase() - self.errors += 1 - - def append_collect_skipped(self, report): - self._opentestcase(report) - #msg = str(report.longrepr.reprtraceback.extraline) - self.appendlog('%s', - report.longrepr) - self._closetestcase() - self.skipped += 1 - - def append_error(self, report): - self._opentestcase(report) - self.appendlog('%s', - report.longrepr) - self._closetestcase() - self.errors += 1 - - def append_skipped(self, report): - self._opentestcase(report) - if "xfail" in report.keywords: - self.appendlog( - '%s', - report.keywords['xfail']) - else: - self.appendlog("") - self._closetestcase() - self.skipped += 1 - - def pytest_runtest_logreport(self, report): - if report.passed: - self.append_pass(report) - elif report.failed: - if report.when != "call": - self.append_error(report) - else: - self.append_failure(report) - elif report.skipped: - self.append_skipped(report) - - def pytest_runtest_call(self, item, __multicall__): - names = tuple(item.listnames()) - start = time.time() - try: - return __multicall__.execute() - finally: - self._durations[names] = time.time() - start - - def pytest_collectreport(self, report): - if not report.passed: - if report.failed: - self.append_collect_failure(report) - else: - self.append_collect_skipped(report) - - def pytest_internalerror(self, excrepr): - self.errors += 1 - data = py.xml.escape(excrepr) - self.test_logs.append( - '\n' - ' ' - '%s' % data) - - def pytest_sessionstart(self, session): - self.suite_start_time = time.time() - - def pytest_sessionfinish(self, session, exitstatus, __multicall__): - if py.std.sys.version_info[0] < 3: - logfile = py.std.codecs.open(self.logfile, 'w', encoding='utf-8') - else: - logfile = open(self.logfile, 'w', encoding='utf-8') - - suite_stop_time = time.time() - suite_time_delta = suite_stop_time - self.suite_start_time - numtests = self.passed + self.failed - logfile.write('') - logfile.write('') - logfile.writelines(self.test_logs) - logfile.write('') - logfile.close() - - def pytest_terminal_summary(self, terminalreporter): - tw = terminalreporter._tw - terminalreporter.write_sep("-", "generated xml file: %s" %(self.logfile)) --- a/doc/mark.txt +++ b/doc/mark.txt @@ -4,7 +4,7 @@ mark test functions with attributes ================================================================= -.. currentmodule:: pytest.plugin.mark +.. currentmodule:: _pytest.mark By using the ``py.test.mark`` helper you can instantiate decorators that will set named meta data on test functions. --- /dev/null +++ b/_pytest/nose.py @@ -0,0 +1,47 @@ +"""run test suites written for nose. """ + +import pytest, py +import inspect +import sys + +def pytest_runtest_makereport(__multicall__, item, call): + SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None) + if SkipTest: + if call.excinfo and call.excinfo.errisinstance(SkipTest): + # let's substitute the excinfo with a py.test.skip one + call2 = call.__class__(lambda: py.test.skip(str(call.excinfo.value)), call.when) + call.excinfo = call2.excinfo + + +def pytest_runtest_setup(item): + if isinstance(item, (pytest.Function)): + if isinstance(item.parent, pytest.Generator): + gen = item.parent + if not hasattr(gen, '_nosegensetup'): + call_optional(gen.obj, 'setup') + if isinstance(gen.parent, pytest.Instance): + call_optional(gen.parent.obj, 'setup') + gen._nosegensetup = True + if not call_optional(item.obj, 'setup'): + # call module level setup if there is no object level one + call_optional(item.parent.obj, 'setup') + +def pytest_runtest_teardown(item): + if isinstance(item, pytest.Function): + if not call_optional(item.obj, 'teardown'): + call_optional(item.parent.obj, 'teardown') + #if hasattr(item.parent, '_nosegensetup'): + # #call_optional(item._nosegensetup, 'teardown') + # del item.parent._nosegensetup + +def pytest_make_collect_report(collector): + if isinstance(collector, pytest.Generator): + call_optional(collector.obj, 'setup') + +def call_optional(obj, name): + method = getattr(obj, name, None) + if method: + # If there's any problems allow the exception to raise rather than + # silently ignoring them + method() + return True --- a/pytest/plugin/config.py +++ /dev/null @@ -1,453 +0,0 @@ -""" command line configuration, ini-file and conftest.py processing. """ - -import py -import sys, os -from pytest.main import PluginManager -import pytest - -def pytest_cmdline_parse(pluginmanager, args): - config = Config(pluginmanager) - config.parse(args) - if config.option.debug: - config.trace.root.setwriter(sys.stderr.write) - return config - -class Parser: - """ Parser for command line arguments. """ - - def __init__(self, usage=None, processopt=None): - self._anonymous = OptionGroup("custom options", parser=self) - self._groups = [] - self._processopt = processopt - self._usage = usage - self._inidict = {} - self._ininames = [] - self.hints = [] - - def processoption(self, option): - if self._processopt: - if option.dest: - self._processopt(option) - - def addnote(self, note): - self._notes.append(note) - - def getgroup(self, name, description="", after=None): - """ get (or create) a named option Group. - - :name: unique name of the option group. - :description: long description for --help output. - :after: name of other group, used for ordering --help output. - """ - for group in self._groups: - if group.name == name: - return group - group = OptionGroup(name, description, parser=self) - i = 0 - for i, grp in enumerate(self._groups): - if grp.name == after: - break - self._groups.insert(i+1, group) - return group - - def addoption(self, *opts, **attrs): - """ add an optparse-style option. """ - self._anonymous.addoption(*opts, **attrs) - - def parse(self, args): - self.optparser = optparser = MyOptionParser(self) - groups = self._groups + [self._anonymous] - for group in groups: - if group.options: - desc = group.description or group.name - optgroup = py.std.optparse.OptionGroup(optparser, desc) - optgroup.add_options(group.options) - optparser.add_option_group(optgroup) - return self.optparser.parse_args([str(x) for x in args]) - - def parse_setoption(self, args, option): - parsedoption, args = self.parse(args) - for name, value in parsedoption.__dict__.items(): - setattr(option, name, value) - return args - - def addini(self, name, help, type=None, default=None): - """ add an ini-file option with the given name and description. """ - assert type in (None, "pathlist", "args", "linelist") - self._inidict[name] = (help, type, default) - self._ininames.append(name) - -class OptionGroup: - def __init__(self, name, description="", parser=None): - self.name = name - self.description = description - self.options = [] - self.parser = parser - - def addoption(self, *optnames, **attrs): - """ add an option to this group. """ - option = py.std.optparse.Option(*optnames, **attrs) - self._addoption_instance(option, shortupper=False) - - def _addoption(self, *optnames, **attrs): - option = py.std.optparse.Option(*optnames, **attrs) - self._addoption_instance(option, shortupper=True) - - def _addoption_instance(self, option, shortupper=False): - if not shortupper: - for opt in option._short_opts: - if opt[0] == '-' and opt[1].islower(): - raise ValueError("lowercase shortoptions reserved") - if self.parser: - self.parser.processoption(option) - self.options.append(option) - - -class MyOptionParser(py.std.optparse.OptionParser): - def __init__(self, parser): - self._parser = parser - py.std.optparse.OptionParser.__init__(self, usage=parser._usage, - add_help_option=False) - def format_epilog(self, formatter): - hints = self._parser.hints - if hints: - s = "\n".join(["hint: " + x for x in hints]) + "\n" - s = "\n" + s + "\n" - return s - return "" - -class Conftest(object): - """ the single place for accessing values and interacting - towards conftest modules from py.test objects. - """ - def __init__(self, onimport=None, confcutdir=None): - self._path2confmods = {} - self._onimport = onimport - self._conftestpath2mod = {} - self._confcutdir = confcutdir - self._md5cache = {} - - def setinitial(self, args): - """ try to find a first anchor path for looking up global values - from conftests. This function is usually called _before_ - argument parsing. conftest files may add command line options - and we thus have no completely safe way of determining - which parts of the arguments are actually related to options - and which are file system paths. We just try here to get - bootstrapped ... - """ - current = py.path.local() - opt = '--confcutdir' - for i in range(len(args)): - opt1 = str(args[i]) - if opt1.startswith(opt): - if opt1 == opt: - if len(args) > i: - p = current.join(args[i+1], abs=True) - elif opt1.startswith(opt + "="): - p = current.join(opt1[len(opt)+1:], abs=1) - self._confcutdir = p - break - for arg in args + [current]: - if hasattr(arg, 'startswith') and arg.startswith("--"): - continue - anchor = current.join(arg, abs=1) - if anchor.check(): # we found some file object - self._path2confmods[None] = self.getconftestmodules(anchor) - # let's also consider test* dirs - if anchor.check(dir=1): - for x in anchor.listdir("test*"): - if x.check(dir=1): - self.getconftestmodules(x) - break - else: - assert 0, "no root of filesystem?" - - def getconftestmodules(self, path): - """ return a list of imported conftest modules for the given path. """ - try: - clist = self._path2confmods[path] - except KeyError: - if path is None: - raise ValueError("missing default confest.") - dp = path.dirpath() - clist = [] - if dp != path: - cutdir = self._confcutdir - if cutdir and path != cutdir and not path.relto(cutdir): - pass - else: - conftestpath = path.join("conftest.py") - if conftestpath.check(file=1): - key = conftestpath.computehash() - # XXX logging about conftest loading - if key not in self._md5cache: - clist.append(self.importconftest(conftestpath)) - self._md5cache[key] = conftestpath - else: - # use some kind of logging - print ("WARN: not loading %s" % conftestpath) - clist[:0] = self.getconftestmodules(dp) - self._path2confmods[path] = clist - # be defensive: avoid changes from caller side to - # affect us by always returning a copy of the actual list - return clist[:] - - def rget(self, name, path=None): - mod, value = self.rget_with_confmod(name, path) - return value - - def rget_with_confmod(self, name, path=None): - modules = self.getconftestmodules(path) - modules.reverse() - for mod in modules: - try: - return mod, getattr(mod, name) - except AttributeError: - continue - raise KeyError(name) - - def importconftest(self, conftestpath): - assert conftestpath.check(), conftestpath - try: - return self._conftestpath2mod[conftestpath] - except KeyError: - pkgpath = conftestpath.pypkgpath() - if pkgpath is None: - _ensure_removed_sysmodule(conftestpath.purebasename) - self._conftestpath2mod[conftestpath] = mod = conftestpath.pyimport() - dirpath = conftestpath.dirpath() - if dirpath in self._path2confmods: - for path, mods in self._path2confmods.items(): - if path and path.relto(dirpath) or path == dirpath: - assert mod not in mods - mods.append(mod) - self._postimport(mod) - return mod - - def _postimport(self, mod): - if self._onimport: - self._onimport(mod) - return mod - -def _ensure_removed_sysmodule(modname): - try: - del sys.modules[modname] - except KeyError: - pass - -class CmdOptions(object): - """ holds cmdline options as attributes.""" - def __init__(self, **kwargs): - self.__dict__.update(kwargs) - def __repr__(self): - return "" %(self.__dict__,) - -class Config(object): - """ access to configuration values, pluginmanager and plugin hooks. """ - basetemp = None - - def __init__(self, pluginmanager=None): - #: command line option values, usually added via parser.addoption(...) - #: or parser.getgroup(...).addoption(...) calls - self.option = CmdOptions() - self._parser = Parser( - usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]", - processopt=self._processopt, - ) - #: a pluginmanager instance - self.pluginmanager = pluginmanager or PluginManager(load=True) - self.trace = self.pluginmanager.trace.root.get("config") - self._conftest = Conftest(onimport=self._onimportconftest) - self.hook = self.pluginmanager.hook - - def _onimportconftest(self, conftestmodule): - self.trace("loaded conftestmodule %r" %(conftestmodule,)) - self.pluginmanager.consider_conftest(conftestmodule) - - def _processopt(self, opt): - if hasattr(opt, 'default') and opt.dest: - if not hasattr(self.option, opt.dest): - setattr(self.option, opt.dest, opt.default) - - def _getmatchingplugins(self, fspath): - allconftests = self._conftest._conftestpath2mod.values() - plugins = [x for x in self.pluginmanager.getplugins() - if x not in allconftests] - plugins += self._conftest.getconftestmodules(fspath) - return plugins - - def _setinitialconftest(self, args): - # capture output during conftest init (#issue93) - name = hasattr(os, 'dup') and 'StdCaptureFD' or 'StdCapture' - cap = getattr(py.io, name)() - try: - try: - self._conftest.setinitial(args) - finally: - out, err = cap.reset() - except: - sys.stdout.write(out) - sys.stderr.write(err) - raise - - def _initini(self, args): - self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"]) - self._parser.addini('addopts', 'extra command line options', 'args') - self._parser.addini('minversion', 'minimally required pytest version') - - def _preparse(self, args, addopts=True): - self._initini(args) - if addopts: - args[:] = self.getini("addopts") + args - self._checkversion() - self.pluginmanager.consider_setuptools_entrypoints() - self.pluginmanager.consider_env() - self.pluginmanager.consider_preparse(args) - self._setinitialconftest(args) - self.pluginmanager.do_addoption(self._parser) - - def _checkversion(self): - minver = self.inicfg.get('minversion', None) - if minver: - ver = minver.split(".") - myver = pytest.__version__.split(".") - if myver < ver: - raise pytest.UsageError( - "%s:%d: requires pytest-%s, actual pytest-%s'" %( - self.inicfg.config.path, self.inicfg.lineof('minversion'), - minver, pytest.__version__)) - - def parse(self, args): - # parse given cmdline arguments into this config object. - # Note that this can only be called once per testing process. - assert not hasattr(self, 'args'), ( - "can only parse cmdline args at most once per Config object") - self._preparse(args) - self._parser.hints.extend(self.pluginmanager._hints) - args = self._parser.parse_setoption(args, self.option) - if not args: - args.append(py.std.os.getcwd()) - self.args = args - - def ensuretemp(self, string, dir=True): - return self.getbasetemp().ensure(string, dir=dir) - - def getbasetemp(self): - if self.basetemp is None: - basetemp = self.option.basetemp - if basetemp: - basetemp = py.path.local(basetemp) - if not basetemp.check(dir=1): - basetemp.mkdir() - else: - basetemp = py.path.local.make_numbered_dir(prefix='pytest-') - self.basetemp = basetemp - return self.basetemp - - def mktemp(self, basename, numbered=False): - basetemp = self.getbasetemp() - if not numbered: - return basetemp.mkdir(basename) - else: - return py.path.local.make_numbered_dir(prefix=basename, - keep=0, rootdir=basetemp, lock_timeout=None) - - def getini(self, name): - """ return configuration value from an ini file. If the - specified name hasn't been registered through a prior ``parse.addini`` - call (usually from a plugin), a ValueError is raised. """ - try: - description, type, default = self._parser._inidict[name] - except KeyError: - raise ValueError("unknown configuration value: %r" %(name,)) - try: - value = self.inicfg[name] - except KeyError: - if default is not None: - return default - if type is None: - return '' - return [] - if type == "pathlist": - dp = py.path.local(self.inicfg.config.path).dirpath() - l = [] - for relpath in py.std.shlex.split(value): - l.append(dp.join(relpath, abs=True)) - return l - elif type == "args": - return py.std.shlex.split(value) - elif type == "linelist": - return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] - else: - assert type is None - return value - - def _getconftest_pathlist(self, name, path=None): - try: - mod, relroots = self._conftest.rget_with_confmod(name, path) - except KeyError: - return None - modpath = py.path.local(mod.__file__).dirpath() - l = [] - for relroot in relroots: - if not isinstance(relroot, py.path.local): - relroot = relroot.replace("/", py.path.local.sep) - relroot = modpath.join(relroot, abs=True) - l.append(relroot) - return l - - def _getconftest(self, name, path=None, check=False): - if check: - self._checkconftest(name) - return self._conftest.rget(name, path) - - def getvalue(self, name, path=None): - """ return ``name`` value looked set from command line options. - - (deprecated) if we can't find the option also lookup - the name in a matching conftest file. - """ - try: - return getattr(self.option, name) - except AttributeError: - return self._getconftest(name, path, check=False) - - def getvalueorskip(self, name, path=None): - """ (deprecated) return getvalue(name) or call py.test.skip if no value exists. """ - try: - val = self.getvalue(name, path) - if val is None: - raise KeyError(name) - return val - except KeyError: - py.test.skip("no %r value found" %(name,)) - - -def getcfg(args, inibasenames): - args = [x for x in args if str(x)[0] != "-"] - if not args: - args = [py.path.local()] - for arg in args: - arg = py.path.local(arg) - if arg.check(): - for base in arg.parts(reverse=True): - for inibasename in inibasenames: - p = base.join(inibasename) - if p.check(): - iniconfig = py.iniconfig.IniConfig(p) - if 'pytest' in iniconfig.sections: - return iniconfig['pytest'] - return {} - -def findupwards(current, basename): - current = py.path.local(current) - while 1: - p = current.join(basename) - if p.check(): - return p - p = current.dirpath() - if p == current: - return - current = p - --- a/pytest/plugin/mark.py +++ /dev/null @@ -1,176 +0,0 @@ -""" 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', - action="store", dest="keyword", default='', metavar="KEYWORDEXPR", - help="only run tests which match given keyword expression. " - "An expression consists of space-separated terms. " - "Each term must match. Precede a term with '-' to negate. " - "Terminate expression with ':' to make the first match match " - "all subsequent tests (usually file-order). ") - -def pytest_collection_modifyitems(items, config): - keywordexpr = config.option.keyword - if not keywordexpr: - return - selectuntil = False - if keywordexpr[-1] == ":": - selectuntil = True - keywordexpr = keywordexpr[:-1] - - remaining = [] - deselected = [] - for colitem in items: - if keywordexpr and skipbykeyword(colitem, keywordexpr): - deselected.append(colitem) - else: - remaining.append(colitem) - if selectuntil: - keywordexpr = None - - if deselected: - config.hook.pytest_deselected(items=deselected) - items[:] = remaining - -def skipbykeyword(colitem, keywordexpr): - """ return True if they given keyword expression means to - skip this collector/item. - """ - if not keywordexpr: - return - - itemkeywords = getkeywords(colitem) - for key in filter(None, keywordexpr.split()): - eor = key[:1] == '-' - if eor: - key = key[1:] - if not (eor ^ matchonekeyword(key, itemkeywords)): - return True - -def getkeywords(node): - keywords = {} - while node is not None: - keywords.update(node.keywords) - node = node.parent - return keywords - - -def matchonekeyword(key, itemkeywords): - for elem in key.split("."): - for kw in itemkeywords: - if elem in kw: - break - else: - return False - return True - -class MarkGenerator: - """ Factory for :class:`MarkDecorator` objects - exposed as - a ``py.test.mark`` singleton instance. Example:: - - import py - @py.test.mark.slowtest - def test_function(): - pass - - will set a 'slowtest' :class:`MarkInfo` object - on the ``test_function`` object. """ - - def __getattr__(self, name): - if name[0] == "_": - raise AttributeError(name) - return MarkDecorator(name) - -class MarkDecorator: - """ A decorator for test functions and test classes. When applied - it will create :class:`MarkInfo` objects which may be - :ref:`retrieved by hooks as item keywords` MarkDecorator instances - are usually created by writing:: - - mark1 = py.test.mark.NAME # simple MarkDecorator - mark2 = py.test.mark.NAME(name1=value) # parametrized MarkDecorator - - and can then be applied as decorators to test functions:: - - @mark2 - def test_function(): - pass - """ - def __init__(self, name): - self.markname = name - self.kwargs = {} - self.args = [] - - def __repr__(self): - d = self.__dict__.copy() - name = d.pop('markname') - return "" %(name, d) - - def __call__(self, *args, **kwargs): - """ if passed a single callable argument: decorate it with mark info. - otherwise add *args/**kwargs in-place to mark information. """ - if args: - func = args[0] - if len(args) == 1 and hasattr(func, '__call__') or \ - hasattr(func, '__bases__'): - if hasattr(func, '__bases__'): - if hasattr(func, 'pytestmark'): - l = func.pytestmark - if not isinstance(l, list): - func.pytestmark = [l, self] - else: - l.append(self) - else: - func.pytestmark = [self] - else: - holder = getattr(func, self.markname, None) - if holder is None: - holder = MarkInfo(self.markname, self.args, self.kwargs) - setattr(func, self.markname, holder) - else: - holder.kwargs.update(self.kwargs) - holder.args.extend(self.args) - return func - else: - self.args.extend(args) - self.kwargs.update(kwargs) - return self - -class MarkInfo: - """ Marking object created by :class:`MarkDecorator` instances. """ - def __init__(self, name, args, kwargs): - #: name of attribute - self.name = name - #: positional argument list, empty if none specified - self.args = args - #: keyword argument dictionary, empty if nothing specified - self.kwargs = kwargs - - def __repr__(self): - return "" % ( - self._name, self.args, self.kwargs) - -def pytest_itemcollected(item): - if not isinstance(item, pytest.Function): - return - try: - func = item.obj.__func__ - except AttributeError: - func = getattr(item.obj, 'im_func', item.obj) - pyclasses = (pytest.Class, pytest.Module) - for node in item.listchain(): - if isinstance(node, pyclasses): - marker = getattr(node.obj, 'pytestmark', None) - if marker is not None: - if isinstance(marker, list): - for mark in marker: - mark(func) - else: - marker(func) - node = node.parent - item.keywords.update(py.builtin._getfuncdict(func)) --- a/pytest/plugin/__init__.py +++ /dev/null @@ -1,1 +0,0 @@ -# --- /dev/null +++ b/_pytest/config.py @@ -0,0 +1,453 @@ +""" command line configuration, ini-file and conftest.py processing. """ + +import py +import sys, os +from _pytest.core import PluginManager +import pytest + +def pytest_cmdline_parse(pluginmanager, args): + config = Config(pluginmanager) + config.parse(args) + if config.option.debug: + config.trace.root.setwriter(sys.stderr.write) + return config + +class Parser: + """ Parser for command line arguments. """ + + def __init__(self, usage=None, processopt=None): + self._anonymous = OptionGroup("custom options", parser=self) + self._groups = [] + self._processopt = processopt + self._usage = usage + self._inidict = {} + self._ininames = [] + self.hints = [] + + def processoption(self, option): + if self._processopt: + if option.dest: + self._processopt(option) + + def addnote(self, note): + self._notes.append(note) + + def getgroup(self, name, description="", after=None): + """ get (or create) a named option Group. + + :name: unique name of the option group. + :description: long description for --help output. + :after: name of other group, used for ordering --help output. + """ + for group in self._groups: + if group.name == name: + return group + group = OptionGroup(name, description, parser=self) + i = 0 + for i, grp in enumerate(self._groups): + if grp.name == after: + break + self._groups.insert(i+1, group) + return group + + def addoption(self, *opts, **attrs): + """ add an optparse-style option. """ + self._anonymous.addoption(*opts, **attrs) + + def parse(self, args): + self.optparser = optparser = MyOptionParser(self) + groups = self._groups + [self._anonymous] + for group in groups: + if group.options: + desc = group.description or group.name + optgroup = py.std.optparse.OptionGroup(optparser, desc) + optgroup.add_options(group.options) + optparser.add_option_group(optgroup) + return self.optparser.parse_args([str(x) for x in args]) + + def parse_setoption(self, args, option): + parsedoption, args = self.parse(args) + for name, value in parsedoption.__dict__.items(): + setattr(option, name, value) + return args + + def addini(self, name, help, type=None, default=None): + """ add an ini-file option with the given name and description. """ + assert type in (None, "pathlist", "args", "linelist") + self._inidict[name] = (help, type, default) + self._ininames.append(name) + +class OptionGroup: + def __init__(self, name, description="", parser=None): + self.name = name + self.description = description + self.options = [] + self.parser = parser + + def addoption(self, *optnames, **attrs): + """ add an option to this group. """ + option = py.std.optparse.Option(*optnames, **attrs) + self._addoption_instance(option, shortupper=False) + + def _addoption(self, *optnames, **attrs): + option = py.std.optparse.Option(*optnames, **attrs) + self._addoption_instance(option, shortupper=True) + + def _addoption_instance(self, option, shortupper=False): + if not shortupper: + for opt in option._short_opts: + if opt[0] == '-' and opt[1].islower(): + raise ValueError("lowercase shortoptions reserved") + if self.parser: + self.parser.processoption(option) + self.options.append(option) + + +class MyOptionParser(py.std.optparse.OptionParser): + def __init__(self, parser): + self._parser = parser + py.std.optparse.OptionParser.__init__(self, usage=parser._usage, + add_help_option=False) + def format_epilog(self, formatter): + hints = self._parser.hints + if hints: + s = "\n".join(["hint: " + x for x in hints]) + "\n" + s = "\n" + s + "\n" + return s + return "" + +class Conftest(object): + """ the single place for accessing values and interacting + towards conftest modules from py.test objects. + """ + def __init__(self, onimport=None, confcutdir=None): + self._path2confmods = {} + self._onimport = onimport + self._conftestpath2mod = {} + self._confcutdir = confcutdir + self._md5cache = {} + + def setinitial(self, args): + """ try to find a first anchor path for looking up global values + from conftests. This function is usually called _before_ + argument parsing. conftest files may add command line options + and we thus have no completely safe way of determining + which parts of the arguments are actually related to options + and which are file system paths. We just try here to get + bootstrapped ... + """ + current = py.path.local() + opt = '--confcutdir' + for i in range(len(args)): + opt1 = str(args[i]) + if opt1.startswith(opt): + if opt1 == opt: + if len(args) > i: + p = current.join(args[i+1], abs=True) + elif opt1.startswith(opt + "="): + p = current.join(opt1[len(opt)+1:], abs=1) + self._confcutdir = p + break + for arg in args + [current]: + if hasattr(arg, 'startswith') and arg.startswith("--"): + continue + anchor = current.join(arg, abs=1) + if anchor.check(): # we found some file object + self._path2confmods[None] = self.getconftestmodules(anchor) + # let's also consider test* dirs + if anchor.check(dir=1): + for x in anchor.listdir("test*"): + if x.check(dir=1): + self.getconftestmodules(x) + break + else: + assert 0, "no root of filesystem?" + + def getconftestmodules(self, path): + """ return a list of imported conftest modules for the given path. """ + try: + clist = self._path2confmods[path] + except KeyError: + if path is None: + raise ValueError("missing default confest.") + dp = path.dirpath() + clist = [] + if dp != path: + cutdir = self._confcutdir + if cutdir and path != cutdir and not path.relto(cutdir): + pass + else: + conftestpath = path.join("conftest.py") + if conftestpath.check(file=1): + key = conftestpath.computehash() + # XXX logging about conftest loading + if key not in self._md5cache: + clist.append(self.importconftest(conftestpath)) + self._md5cache[key] = conftestpath + else: + # use some kind of logging + print ("WARN: not loading %s" % conftestpath) + clist[:0] = self.getconftestmodules(dp) + self._path2confmods[path] = clist + # be defensive: avoid changes from caller side to + # affect us by always returning a copy of the actual list + return clist[:] + + def rget(self, name, path=None): + mod, value = self.rget_with_confmod(name, path) + return value + + def rget_with_confmod(self, name, path=None): + modules = self.getconftestmodules(path) + modules.reverse() + for mod in modules: + try: + return mod, getattr(mod, name) + except AttributeError: + continue + raise KeyError(name) + + def importconftest(self, conftestpath): + assert conftestpath.check(), conftestpath + try: + return self._conftestpath2mod[conftestpath] + except KeyError: + pkgpath = conftestpath.pypkgpath() + if pkgpath is None: + _ensure_removed_sysmodule(conftestpath.purebasename) + self._conftestpath2mod[conftestpath] = mod = conftestpath.pyimport() + dirpath = conftestpath.dirpath() + if dirpath in self._path2confmods: + for path, mods in self._path2confmods.items(): + if path and path.relto(dirpath) or path == dirpath: + assert mod not in mods + mods.append(mod) + self._postimport(mod) + return mod + + def _postimport(self, mod): + if self._onimport: + self._onimport(mod) + return mod + +def _ensure_removed_sysmodule(modname): + try: + del sys.modules[modname] + except KeyError: + pass + +class CmdOptions(object): + """ holds cmdline options as attributes.""" + def __init__(self, **kwargs): + self.__dict__.update(kwargs) + def __repr__(self): + return "" %(self.__dict__,) + +class Config(object): + """ access to configuration values, pluginmanager and plugin hooks. """ + basetemp = None + + def __init__(self, pluginmanager=None): + #: command line option values, usually added via parser.addoption(...) + #: or parser.getgroup(...).addoption(...) calls + self.option = CmdOptions() + self._parser = Parser( + usage="usage: %prog [options] [file_or_dir] [file_or_dir] [...]", + processopt=self._processopt, + ) + #: a pluginmanager instance + self.pluginmanager = pluginmanager or PluginManager(load=True) + self.trace = self.pluginmanager.trace.root.get("config") + self._conftest = Conftest(onimport=self._onimportconftest) + self.hook = self.pluginmanager.hook + + def _onimportconftest(self, conftestmodule): + self.trace("loaded conftestmodule %r" %(conftestmodule,)) + self.pluginmanager.consider_conftest(conftestmodule) + + def _processopt(self, opt): + if hasattr(opt, 'default') and opt.dest: + if not hasattr(self.option, opt.dest): + setattr(self.option, opt.dest, opt.default) + + def _getmatchingplugins(self, fspath): + allconftests = self._conftest._conftestpath2mod.values() + plugins = [x for x in self.pluginmanager.getplugins() + if x not in allconftests] + plugins += self._conftest.getconftestmodules(fspath) + return plugins + + def _setinitialconftest(self, args): + # capture output during conftest init (#issue93) + name = hasattr(os, 'dup') and 'StdCaptureFD' or 'StdCapture' + cap = getattr(py.io, name)() + try: + try: + self._conftest.setinitial(args) + finally: + out, err = cap.reset() + except: + sys.stdout.write(out) + sys.stderr.write(err) + raise + + def _initini(self, args): + self.inicfg = getcfg(args, ["pytest.ini", "tox.ini", "setup.cfg"]) + self._parser.addini('addopts', 'extra command line options', 'args') + self._parser.addini('minversion', 'minimally required pytest version') + + def _preparse(self, args, addopts=True): + self._initini(args) + if addopts: + args[:] = self.getini("addopts") + args + self._checkversion() + self.pluginmanager.consider_setuptools_entrypoints() + self.pluginmanager.consider_env() + self.pluginmanager.consider_preparse(args) + self._setinitialconftest(args) + self.pluginmanager.do_addoption(self._parser) + + def _checkversion(self): + minver = self.inicfg.get('minversion', None) + if minver: + ver = minver.split(".") + myver = pytest.__version__.split(".") + if myver < ver: + raise pytest.UsageError( + "%s:%d: requires pytest-%s, actual pytest-%s'" %( + self.inicfg.config.path, self.inicfg.lineof('minversion'), + minver, pytest.__version__)) + + def parse(self, args): + # parse given cmdline arguments into this config object. + # Note that this can only be called once per testing process. + assert not hasattr(self, 'args'), ( + "can only parse cmdline args at most once per Config object") + self._preparse(args) + self._parser.hints.extend(self.pluginmanager._hints) + args = self._parser.parse_setoption(args, self.option) + if not args: + args.append(py.std.os.getcwd()) + self.args = args + + def ensuretemp(self, string, dir=True): + return self.getbasetemp().ensure(string, dir=dir) + + def getbasetemp(self): + if self.basetemp is None: + basetemp = self.option.basetemp + if basetemp: + basetemp = py.path.local(basetemp) + if not basetemp.check(dir=1): + basetemp.mkdir() + else: + basetemp = py.path.local.make_numbered_dir(prefix='pytest-') + self.basetemp = basetemp + return self.basetemp + + def mktemp(self, basename, numbered=False): + basetemp = self.getbasetemp() + if not numbered: + return basetemp.mkdir(basename) + else: + return py.path.local.make_numbered_dir(prefix=basename, + keep=0, rootdir=basetemp, lock_timeout=None) + + def getini(self, name): + """ return configuration value from an ini file. If the + specified name hasn't been registered through a prior ``parse.addini`` + call (usually from a plugin), a ValueError is raised. """ + try: + description, type, default = self._parser._inidict[name] + except KeyError: + raise ValueError("unknown configuration value: %r" %(name,)) + try: + value = self.inicfg[name] + except KeyError: + if default is not None: + return default + if type is None: + return '' + return [] + if type == "pathlist": + dp = py.path.local(self.inicfg.config.path).dirpath() + l = [] + for relpath in py.std.shlex.split(value): + l.append(dp.join(relpath, abs=True)) + return l + elif type == "args": + return py.std.shlex.split(value) + elif type == "linelist": + return [t for t in map(lambda x: x.strip(), value.split("\n")) if t] + else: + assert type is None + return value + + def _getconftest_pathlist(self, name, path=None): + try: + mod, relroots = self._conftest.rget_with_confmod(name, path) + except KeyError: + return None + modpath = py.path.local(mod.__file__).dirpath() + l = [] + for relroot in relroots: + if not isinstance(relroot, py.path.local): + relroot = relroot.replace("/", py.path.local.sep) + relroot = modpath.join(relroot, abs=True) + l.append(relroot) + return l + + def _getconftest(self, name, path=None, check=False): + if check: + self._checkconftest(name) + return self._conftest.rget(name, path) + + def getvalue(self, name, path=None): + """ return ``name`` value looked set from command line options. + + (deprecated) if we can't find the option also lookup + the name in a matching conftest file. + """ + try: + return getattr(self.option, name) + except AttributeError: + return self._getconftest(name, path, check=False) + + def getvalueorskip(self, name, path=None): + """ (deprecated) return getvalue(name) or call py.test.skip if no value exists. """ + try: + val = self.getvalue(name, path) + if val is None: + raise KeyError(name) + return val + except KeyError: + py.test.skip("no %r value found" %(name,)) + + +def getcfg(args, inibasenames): + args = [x for x in args if str(x)[0] != "-"] + if not args: + args = [py.path.local()] + for arg in args: + arg = py.path.local(arg) + if arg.check(): + for base in arg.parts(reverse=True): + for inibasename in inibasenames: + p = base.join(inibasename) + if p.check(): + iniconfig = py.iniconfig.IniConfig(p) + if 'pytest' in iniconfig.sections: + return iniconfig['pytest'] + return {} + +def findupwards(current, basename): + current = py.path.local(current) + while 1: + p = current.join(basename) + if p.check(): + return p + p = current.dirpath() + if p == current: + return + current = p + --- /dev/null +++ b/_pytest/skipping.py @@ -0,0 +1,213 @@ +""" support for skip/xfail functions and markers. """ + +import py, pytest + +def pytest_addoption(parser): + group = parser.getgroup("general") + group.addoption('--runxfail', + action="store_true", dest="runxfail", default=False, + help="run tests even if they are marked xfail") + +def pytest_namespace(): + return dict(xfail=xfail) + +class XFailed(pytest.fail.Exception): + """ raised from an explicit call to py.test.xfail() """ + +def xfail(reason=""): + """ xfail an executing test or setup functions with the given reason.""" + __tracebackhide__ = True + raise XFailed(reason) +xfail.Exception = XFailed + +class MarkEvaluator: + def __init__(self, item, name): + self.item = item + self.name = name + + @property + def holder(self): + return self.item.keywords.get(self.name, None) + def __bool__(self): + return bool(self.holder) + __nonzero__ = __bool__ + + def istrue(self): + if self.holder: + d = {'os': py.std.os, 'sys': py.std.sys, 'config': self.item.config} + if self.holder.args: + self.result = False + for expr in self.holder.args: + self.expr = expr + if isinstance(expr, str): + result = cached_eval(self.item.config, expr, d) + else: + result = expr + if result: + self.result = True + self.expr = expr + break + else: + self.result = True + return getattr(self, 'result', False) + + def get(self, attr, default=None): + return self.holder.kwargs.get(attr, default) + + def getexplanation(self): + expl = self.get('reason', None) + if not expl: + if not hasattr(self, 'expr'): + return "" + else: + return "condition: " + self.expr + return expl + + +def pytest_runtest_setup(item): + if not isinstance(item, pytest.Function): + return + evalskip = MarkEvaluator(item, 'skipif') + if evalskip.istrue(): + py.test.skip(evalskip.getexplanation()) + item._evalxfail = MarkEvaluator(item, 'xfail') + check_xfail_no_run(item) + +def pytest_pyfunc_call(pyfuncitem): + check_xfail_no_run(pyfuncitem) + +def check_xfail_no_run(item): + if not item.config.option.runxfail: + evalxfail = item._evalxfail + if evalxfail.istrue(): + if not evalxfail.get('run', True): + py.test.xfail("[NOTRUN] " + evalxfail.getexplanation()) + +def pytest_runtest_makereport(__multicall__, item, call): + if not isinstance(item, pytest.Function): + return + if not (call.excinfo and + call.excinfo.errisinstance(py.test.xfail.Exception)): + evalxfail = getattr(item, '_evalxfail', None) + if not evalxfail: + return + if call.excinfo and call.excinfo.errisinstance(py.test.xfail.Exception): + if not item.config.getvalue("runxfail"): + rep = __multicall__.execute() + rep.keywords['xfail'] = "reason: " + call.excinfo.value.msg + rep.outcome = "skipped" + return rep + if call.when == "call": + rep = __multicall__.execute() + evalxfail = getattr(item, '_evalxfail') + if not item.config.getvalue("runxfail") and evalxfail.istrue(): + if call.excinfo: + rep.outcome = "skipped" + else: + rep.outcome = "failed" + rep.keywords['xfail'] = evalxfail.getexplanation() + else: + if 'xfail' in rep.keywords: + del rep.keywords['xfail'] + return rep + +# called by terminalreporter progress reporting +def pytest_report_teststatus(report): + if 'xfail' in report.keywords: + if report.skipped: + return "xfailed", "x", "xfail" + elif report.failed: + return "xpassed", "X", "XPASS" + +# called by the terminalreporter instance/plugin +def pytest_terminal_summary(terminalreporter): + tr = terminalreporter + if not tr.reportchars: + #for name in "xfailed skipped failed xpassed": + # if not tr.stats.get(name, 0): + # tr.write_line("HINT: use '-r' option to see extra " + # "summary info about tests") + # break + return + + lines = [] + for char in tr.reportchars: + if char == "x": + show_xfailed(terminalreporter, lines) + elif char == "X": + show_xpassed(terminalreporter, lines) + elif char in "fF": + show_failed(terminalreporter, lines) + elif char in "sS": + show_skipped(terminalreporter, lines) + if lines: + tr._tw.sep("=", "short test summary info") + for line in lines: + tr._tw.line(line) + +def show_failed(terminalreporter, lines): + tw = terminalreporter._tw + failed = terminalreporter.stats.get("failed") + if failed: + for rep in failed: + pos = rep.nodeid + lines.append("FAIL %s" %(pos, )) + +def show_xfailed(terminalreporter, lines): + xfailed = terminalreporter.stats.get("xfailed") + if xfailed: + for rep in xfailed: + pos = rep.nodeid + reason = rep.keywords['xfail'] + lines.append("XFAIL %s" % (pos,)) + if reason: + lines.append(" " + str(reason)) + +def show_xpassed(terminalreporter, lines): + xpassed = terminalreporter.stats.get("xpassed") + if xpassed: + for rep in xpassed: + pos = rep.nodeid + reason = rep.keywords['xfail'] + lines.append("XPASS %s %s" %(pos, reason)) + +def cached_eval(config, expr, d): + if not hasattr(config, '_evalcache'): + config._evalcache = {} + try: + return config._evalcache[expr] + except KeyError: + #import sys + #print >>sys.stderr, ("cache-miss: %r" % expr) + config._evalcache[expr] = x = eval(expr, d) + return x + + +def folded_skips(skipped): + d = {} + for event in skipped: + entry = event.longrepr.reprcrash + key = entry.path, entry.lineno, entry.message + d.setdefault(key, []).append(event) + l = [] + for key, events in d.items(): + l.append((len(events),) + key) + return l + +def show_skipped(terminalreporter, lines): + tr = terminalreporter + skipped = tr.stats.get('skipped', []) + if skipped: + #if not tr.hasopt('skipped'): + # tr.write_line( + # "%d skipped tests, specify -rs for more info" % + # len(skipped)) + # return + fskips = folded_skips(skipped) + if fskips: + #tr.write_sep("_", "skipped test summary") + for num, fspath, lineno, reason in fskips: + if reason.startswith("Skipped: "): + reason = reason[9:] + lines.append("SKIP [%d] %s:%d: %s" % + (num, fspath, lineno, reason)) --- a/testing/test_main.py +++ /dev/null @@ -1,634 +0,0 @@ -import py, os -from pytest.main import PluginManager, canonical_importname -from pytest.main import MultiCall, HookRelay, varnames - - -class TestBootstrapping: - def test_consider_env_fails_to_import(self, monkeypatch): - pluginmanager = PluginManager() - monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") - py.test.raises(ImportError, "pluginmanager.consider_env()") - - def test_preparse_args(self): - pluginmanager = PluginManager() - py.test.raises(ImportError, """ - pluginmanager.consider_preparse(["xyz", "-p", "hello123"]) - """) - - def test_plugin_skip(self, testdir, monkeypatch): - p = testdir.makepyfile(pytest_skipping1=""" - import py - py.test.skip("hello") - """) - p.copy(p.dirpath("pytest_skipping2.py")) - monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") - result = testdir.runpytest("-p", "skipping1", "--traceconfig") - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*hint*skipping2*hello*", - "*hint*skipping1*hello*", - ]) - - def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(pytest_xy123="#") - monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') - l1 = len(pluginmanager.getplugins()) - pluginmanager.consider_env() - l2 = len(pluginmanager.getplugins()) - assert l2 == l1 + 1 - assert pluginmanager.getplugin('pytest_xy123') - pluginmanager.consider_env() - l3 = len(pluginmanager.getplugins()) - assert l2 == l3 - - def test_consider_setuptools_instantiation(self, monkeypatch): - pkg_resources = py.test.importorskip("pkg_resources") - def my_iter(name): - assert name == "pytest11" - class EntryPoint: - name = "mytestplugin" - def load(self): - class PseudoPlugin: - x = 42 - return PseudoPlugin() - return iter([EntryPoint()]) - - monkeypatch.setattr(pkg_resources, 'iter_entry_points', my_iter) - pluginmanager = PluginManager() - pluginmanager.consider_setuptools_entrypoints() - plugin = pluginmanager.getplugin("mytestplugin") - assert plugin.x == 42 - plugin2 = pluginmanager.getplugin("pytest_mytestplugin") - assert plugin2 == plugin - - def test_consider_setuptools_not_installed(self, monkeypatch): - monkeypatch.setitem(py.std.sys.modules, 'pkg_resources', - py.std.types.ModuleType("pkg_resources")) - pluginmanager = PluginManager() - pluginmanager.consider_setuptools_entrypoints() - # ok, we did not explode - - def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): - x500 = testdir.makepyfile(pytest_x500="#") - p = testdir.makepyfile(""" - import py - def test_hello(pytestconfig): - plugin = pytestconfig.pluginmanager.getplugin('x500') - assert plugin is not None - """) - monkeypatch.setenv('PYTEST_PLUGINS', 'pytest_x500', prepend=",") - result = testdir.runpytest(p) - assert result.ret == 0 - result.stdout.fnmatch_lines(["*1 passed in*"]) - - def test_import_plugin_importname(self, testdir): - pluginmanager = PluginManager() - py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') - - reset = testdir.syspathinsert() - pluginname = "pytest_hello" - testdir.makepyfile(**{pluginname: ""}) - pluginmanager.import_plugin("hello") - len1 = len(pluginmanager.getplugins()) - pluginmanager.import_plugin("pytest_hello") - len2 = len(pluginmanager.getplugins()) - assert len1 == len2 - plugin1 = pluginmanager.getplugin("pytest_hello") - assert plugin1.__name__.endswith('pytest_hello') - plugin2 = pluginmanager.getplugin("hello") - assert plugin2 is plugin1 - - def test_import_plugin_dotted_name(self, testdir): - pluginmanager = PluginManager() - py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') - - reset = testdir.syspathinsert() - testdir.mkpydir("pkg").join("plug.py").write("x=3") - pluginname = "pkg.plug" - pluginmanager.import_plugin(pluginname) - mod = pluginmanager.getplugin("pkg.plug") - assert mod.x == 3 - - def test_consider_module(self, testdir): - pluginmanager = PluginManager() - testdir.syspathinsert() - testdir.makepyfile(pytest_plug1="#") - testdir.makepyfile(pytest_plug2="#") - mod = py.std.types.ModuleType("temp") - mod.pytest_plugins = ["pytest_plug1", "pytest_plug2"] - pluginmanager.consider_module(mod) - assert pluginmanager.getplugin("plug1").__name__ == "pytest_plug1" - assert pluginmanager.getplugin("plug2").__name__ == "pytest_plug2" - - def test_consider_module_import_module(self, testdir): - mod = py.std.types.ModuleType("x") - mod.pytest_plugins = "pytest_a" - aplugin = testdir.makepyfile(pytest_a="#") - pluginmanager = PluginManager() - reprec = testdir.getreportrecorder(pluginmanager) - #syspath.prepend(aplugin.dirpath()) - py.std.sys.path.insert(0, str(aplugin.dirpath())) - pluginmanager.consider_module(mod) - call = reprec.getcall(pluginmanager.hook.pytest_plugin_registered.name) - assert call.plugin.__name__ == "pytest_a" - - # check that it is not registered twice - pluginmanager.consider_module(mod) - l = reprec.getcalls("pytest_plugin_registered") - assert len(l) == 1 - - def test_config_sets_conftesthandle_onimport(self, testdir): - config = testdir.parseconfig([]) - assert config._conftest._onimport == config._onimportconftest - - def test_consider_conftest_deps(self, testdir): - mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() - pp = PluginManager() - py.test.raises(ImportError, "pp.consider_conftest(mod)") - - def test_pm(self): - pp = PluginManager() - class A: pass - a1, a2 = A(), A() - pp.register(a1) - assert pp.isregistered(a1) - pp.register(a2, "hello") - assert pp.isregistered(a2) - l = pp.getplugins() - assert a1 in l - assert a2 in l - assert pp.getplugin('hello') == a2 - pp.unregister(a1) - assert not pp.isregistered(a1) - pp.unregister(name="hello") - assert not pp.isregistered(a2) - - def test_pm_ordering(self): - pp = PluginManager() - class A: pass - a1, a2 = A(), A() - pp.register(a1) - pp.register(a2, "hello") - l = pp.getplugins() - assert l.index(a1) < l.index(a2) - a3 = A() - pp.register(a3, prepend=True) - l = pp.getplugins() - assert l.index(a3) == 0 - - def test_register_imported_modules(self): - pp = PluginManager() - mod = py.std.types.ModuleType("x.y.pytest_hello") - pp.register(mod) - assert pp.isregistered(mod) - l = pp.getplugins() - assert mod in l - py.test.raises(AssertionError, "pp.register(mod)") - mod2 = py.std.types.ModuleType("pytest_hello") - #pp.register(mod2) # double pm - py.test.raises(AssertionError, "pp.register(mod)") - #assert not pp.isregistered(mod2) - assert pp.getplugins() == l - - def test_canonical_import(self, monkeypatch): - mod = py.std.types.ModuleType("pytest_xyz") - monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod) - pp = PluginManager() - pp.import_plugin('xyz') - assert pp.getplugin('xyz') == mod - assert pp.getplugin('pytest_xyz') == mod - assert pp.isregistered(mod) - - def test_register_mismatch_method(self): - pp = PluginManager(load=True) - class hello: - def pytest_gurgel(self): - pass - py.test.raises(Exception, "pp.register(hello())") - - def test_register_mismatch_arg(self): - pp = PluginManager(load=True) - class hello: - def pytest_configure(self, asd): - pass - excinfo = py.test.raises(Exception, "pp.register(hello())") - - def test_canonical_importname(self): - for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': - impname = canonical_importname(name) - - def test_notify_exception(self, capfd): - pp = PluginManager() - excinfo = py.test.raises(ValueError, "raise ValueError(1)") - pp.notify_exception(excinfo) - out, err = capfd.readouterr() - assert "ValueError" in err - class A: - def pytest_internalerror(self, excrepr): - return True - pp.register(A()) - pp.notify_exception(excinfo) - out, err = capfd.readouterr() - assert not err - - def test_register(self): - pm = PluginManager(load=False) - class MyPlugin: - pass - my = MyPlugin() - pm.register(my) - assert pm.getplugins() - my2 = MyPlugin() - pm.register(my2) - assert pm.getplugins()[1:] == [my, my2] - - assert pm.isregistered(my) - assert pm.isregistered(my2) - pm.unregister(my) - assert not pm.isregistered(my) - assert pm.getplugins()[1:] == [my2] - - def test_listattr(self): - plugins = PluginManager() - class api1: - x = 41 - class api2: - x = 42 - class api3: - x = 43 - plugins.register(api1()) - plugins.register(api2()) - plugins.register(api3()) - l = list(plugins.listattr('x')) - assert l == [41, 42, 43] - - def test_hook_tracing(self): - pm = PluginManager() - saveindent = [] - class api1: - x = 41 - def pytest_plugin_registered(self, plugin): - saveindent.append(pm.trace.root.indent) - raise ValueError(42) - l = [] - pm.trace.root.setwriter(l.append) - indent = pm.trace.root.indent - p = api1() - pm.register(p) - - assert pm.trace.root.indent == indent - assert len(l) == 1 - assert 'pytest_plugin_registered' in l[0] - py.test.raises(ValueError, lambda: pm.register(api1())) - assert pm.trace.root.indent == indent - assert saveindent[0] > indent - -class TestPytestPluginInteractions: - - def test_addhooks_conftestplugin(self, testdir): - newhooks = testdir.makepyfile(newhooks=""" - def pytest_myhook(xyz): - "new hook" - """) - conf = testdir.makeconftest(""" - import sys ; sys.path.insert(0, '.') - import newhooks - def pytest_addhooks(pluginmanager): - pluginmanager.addhooks(newhooks) - def pytest_myhook(xyz): - return xyz + 1 - """) - config = testdir.Config() - config._conftest.importconftest(conf) - print(config.pluginmanager.getplugins()) - res = config.hook.pytest_myhook(xyz=10) - assert res == [11] - - def test_addhooks_docstring_error(self, testdir): - newhooks = testdir.makepyfile(newhooks=""" - class A: # no pytest_ prefix - pass - def pytest_myhook(xyz): - pass - """) - conf = testdir.makeconftest(""" - import sys ; sys.path.insert(0, '.') - import newhooks - def pytest_addhooks(pluginmanager): - pluginmanager.addhooks(newhooks) - """) - res = testdir.runpytest() - assert res.ret != 0 - res.stderr.fnmatch_lines([ - "*docstring*pytest_myhook*newhooks*" - ]) - - def test_addhooks_nohooks(self, testdir): - conf = testdir.makeconftest(""" - import sys - def pytest_addhooks(pluginmanager): - pluginmanager.addhooks(sys) - """) - res = testdir.runpytest() - assert res.ret != 0 - res.stderr.fnmatch_lines([ - "*did not find*sys*" - ]) - - def test_do_option_conftestplugin(self, testdir): - p = testdir.makepyfile(""" - def pytest_addoption(parser): - parser.addoption('--test123', action="store_true") - """) - config = testdir.Config() - config._conftest.importconftest(p) - print(config.pluginmanager.getplugins()) - config.parse([]) - assert not config.option.test123 - - def test_namespace_early_from_import(self, testdir): - p = testdir.makepyfile(""" - from pytest import Item - from pytest import Item as Item2 - assert Item is Item2 - """) - result = testdir.runpython(p) - assert result.ret == 0 - - def test_do_ext_namespace(self, testdir): - testdir.makeconftest(""" - def pytest_namespace(): - return {'hello': 'world'} - """) - p = testdir.makepyfile(""" - from py.test import hello - import py - def test_hello(): - assert hello == "world" - assert 'hello' in py.test.__all__ - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) - - def test_do_option_postinitialize(self, testdir): - config = testdir.Config() - config.parse([]) - config.pluginmanager.do_configure(config=config) - assert not hasattr(config.option, 'test123') - p = testdir.makepyfile(""" - def pytest_addoption(parser): - parser.addoption('--test123', action="store_true", - default=True) - """) - config._conftest.importconftest(p) - assert config.option.test123 - - def test_configure(self, testdir): - config = testdir.parseconfig() - l = [] - class A: - def pytest_configure(self, config): - l.append(self) - - config.pluginmanager.register(A()) - assert len(l) == 0 - config.pluginmanager.do_configure(config=config) - assert len(l) == 1 - config.pluginmanager.register(A()) # this should lead to a configured() plugin - assert len(l) == 2 - assert l[0] != l[1] - - config.pluginmanager.do_unconfigure(config=config) - config.pluginmanager.register(A()) - assert len(l) == 2 - - # lower level API - - def test_listattr(self): - pluginmanager = PluginManager() - class My2: - x = 42 - pluginmanager.register(My2()) - assert not pluginmanager.listattr("hello") - assert pluginmanager.listattr("x") == [42] - -def test_namespace_has_default_and_env_plugins(testdir): - p = testdir.makepyfile(""" - import py - py.test.mark - """) - result = testdir.runpython(p) - assert result.ret == 0 - -def test_varnames(): - def f(x): - i = 3 - class A: - def f(self, y): - pass - class B(object): - def __call__(self, z): - pass - assert varnames(f) == ("x",) - assert varnames(A().f) == ('y',) - assert varnames(B()) == ('z',) - -class TestMultiCall: - def test_uses_copy_of_methods(self): - l = [lambda: 42] - mc = MultiCall(l, {}) - repr(mc) - l[:] = [] - res = mc.execute() - return res == 42 - - def test_call_passing(self): - class P1: - def m(self, __multicall__, x): - assert len(__multicall__.results) == 1 - assert not __multicall__.methods - return 17 - - class P2: - def m(self, __multicall__, x): - assert __multicall__.results == [] - assert __multicall__.methods - return 23 - - p1 = P1() - p2 = P2() - multicall = MultiCall([p1.m, p2.m], {'x': 23}) - assert "23" in repr(multicall) - reslist = multicall.execute() - assert len(reslist) == 2 - # ensure reversed order - assert reslist == [23, 17] - - def test_keyword_args(self): - def f(x): - return x + 1 - class A: - def f(self, x, y): - return x + y - multicall = MultiCall([f, A().f], dict(x=23, y=24)) - assert "'x': 23" in repr(multicall) - assert "'y': 24" in repr(multicall) - reslist = multicall.execute() - assert reslist == [24+23, 24] - assert "2 results" in repr(multicall) - - def test_keyword_args_with_defaultargs(self): - def f(x, z=1): - return x + z - reslist = MultiCall([f], dict(x=23, y=24)).execute() - assert reslist == [24] - reslist = MultiCall([f], dict(x=23, z=2)).execute() - assert reslist == [25] - - def test_tags_call_error(self): - multicall = MultiCall([lambda x: x], {}) - py.test.raises(TypeError, "multicall.execute()") - - def test_call_subexecute(self): - def m(__multicall__): - subresult = __multicall__.execute() - return subresult + 1 - - def n(): - return 1 - - call = MultiCall([n, m], {}, firstresult=True) - res = call.execute() - assert res == 2 - - def test_call_none_is_no_result(self): - def m1(): - return 1 - def m2(): - return None - res = MultiCall([m1, m2], {}, firstresult=True).execute() - assert res == 1 - res = MultiCall([m1, m2], {}).execute() - assert res == [1] - -class TestHookRelay: - def test_happypath(self): - pm = PluginManager() - class Api: - def hello(self, arg): - "api hook 1" - - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - assert hasattr(mcm, 'hello') - assert repr(mcm.hello).find("hello") != -1 - class Plugin: - def hello(self, arg): - return arg + 1 - pm.register(Plugin()) - l = mcm.hello(arg=3) - assert l == [4] - assert not hasattr(mcm, 'world') - - def test_only_kwargs(self): - pm = PluginManager() - class Api: - def hello(self, arg): - "api hook 1" - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - py.test.raises(TypeError, "mcm.hello(3)") - - def test_firstresult_definition(self): - pm = PluginManager() - class Api: - def hello(self, arg): - "api hook 1" - hello.firstresult = True - - mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - class Plugin: - def hello(self, arg): - return arg + 1 - pm.register(Plugin()) - res = mcm.hello(arg=3) - assert res == 4 - -class TestTracer: - def test_simple(self): - from pytest.main import TagTracer - rootlogger = TagTracer("[my] ") - log = rootlogger.get("pytest") - log("hello") - l = [] - rootlogger.setwriter(l.append) - log("world") - assert len(l) == 1 - assert l[0] == "[my] world\n" - sublog = log.get("collection") - sublog("hello") - assert l[1] == "[my] hello\n" - - def test_indent(self): - from pytest.main import TagTracer - rootlogger = TagTracer() - log = rootlogger.get("1") - l = [] - log.root.setwriter(lambda arg: l.append(arg)) - log("hello") - log.root.indent += 1 - log("line1") - log("line2") - log.root.indent += 1 - log("line3") - log("line4") - log.root.indent -= 1 - log("line5") - log.root.indent -= 1 - log("last") - assert len(l) == 7 - names = [x.rstrip()[len(rootlogger.prefix):] for x in l] - assert names == ['hello', ' line1', ' line2', - ' line3', ' line4', ' line5', 'last'] - - def test_setprocessor(self): - from pytest.main import TagTracer - rootlogger = TagTracer() - log = rootlogger.get("1") - log2 = log.get("2") - assert log2.tags == tuple("12") - l = [] - rootlogger.setprocessor(tuple("12"), lambda *args: l.append(args)) - log("not seen") - log2("seen") - assert len(l) == 1 - tags, args = l[0] - assert "1" in tags - assert "2" in tags - assert args == ("seen",) - l2 = [] - rootlogger.setprocessor("1:2", lambda *args: l2.append(args)) - log2("seen") - tags, args = l2[0] - assert args == ("seen",) - - - def test_setmyprocessor(self): - from pytest.main import TagTracer - rootlogger = TagTracer() - log = rootlogger.get("1") - log2 = log.get("2") - l = [] - log2.setmyprocessor(lambda *args: l.append(args)) - log("not seen") - assert not l - log2(42) - assert len(l) == 1 - tags, args = l[0] - assert "1" in tags - assert "2" in tags - assert args == (42,) --- a/doc/monkeypatch.txt +++ b/doc/monkeypatch.txt @@ -2,7 +2,7 @@ monkeypatching/mocking modules and environments ================================================================ -.. currentmodule:: pytest.plugin.monkeypatch +.. currentmodule:: _pytest.monkeypatch Sometimes tests need to invoke functionality which depends on global settings or which invokes code which cannot be easily --- a/pytest/plugin/nose.py +++ /dev/null @@ -1,47 +0,0 @@ -"""run test suites written for nose. """ - -import pytest, py -import inspect -import sys - -def pytest_runtest_makereport(__multicall__, item, call): - SkipTest = getattr(sys.modules.get('nose', None), 'SkipTest', None) - if SkipTest: - if call.excinfo and call.excinfo.errisinstance(SkipTest): - # let's substitute the excinfo with a py.test.skip one - call2 = call.__class__(lambda: py.test.skip(str(call.excinfo.value)), call.when) - call.excinfo = call2.excinfo - - -def pytest_runtest_setup(item): - if isinstance(item, (pytest.Function)): - if isinstance(item.parent, pytest.Generator): - gen = item.parent - if not hasattr(gen, '_nosegensetup'): - call_optional(gen.obj, 'setup') - if isinstance(gen.parent, pytest.Instance): - call_optional(gen.parent.obj, 'setup') - gen._nosegensetup = True - if not call_optional(item.obj, 'setup'): - # call module level setup if there is no object level one - call_optional(item.parent.obj, 'setup') - -def pytest_runtest_teardown(item): - if isinstance(item, pytest.Function): - if not call_optional(item.obj, 'teardown'): - call_optional(item.parent.obj, 'teardown') - #if hasattr(item.parent, '_nosegensetup'): - # #call_optional(item._nosegensetup, 'teardown') - # del item.parent._nosegensetup - -def pytest_make_collect_report(collector): - if isinstance(collector, pytest.Generator): - call_optional(collector.obj, 'setup') - -def call_optional(obj, name): - method = getattr(obj, name, None) - if method: - # If there's any problems allow the exception to raise rather than - # silently ignoring them - method() - return True --- a/tox.ini +++ b/tox.ini @@ -52,5 +52,5 @@ commands= minversion=2.0 plugins=pytester addopts=-rfx --pyargs -rsyncdirs=pytest testing +rsyncdirs=pytest.py _pytest testing --- a/pytest/plugin/session.py +++ /dev/null @@ -1,510 +0,0 @@ -""" core implementation of testing process: init, session, runtest loop. """ - -import py -import pytest -import os, sys -tracebackcutdir = py.path.local(pytest.__file__).dirpath() - -# exitcodes for the command line -EXIT_OK = 0 -EXIT_TESTSFAILED = 1 -EXIT_INTERRUPTED = 2 -EXIT_INTERNALERROR = 3 - -def pytest_addoption(parser): - parser.addini("norecursedirs", "directory patterns to avoid for recursion", - type="args", default=('.*', 'CVS', '_darcs', '{arch}')) - #parser.addini("dirpatterns", - # "patterns specifying possible locations of test files", - # type="linelist", default=["**/test_*.txt", - # "**/test_*.py", "**/*_test.py"] - #) - group = parser.getgroup("general", "running and selection options") - group._addoption('-x', '--exitfirst', action="store_true", default=False, - dest="exitfirst", - help="exit instantly on first error or failed test."), - group._addoption('--maxfail', metavar="num", - action="store", type="int", dest="maxfail", default=0, - help="exit after first num failures or errors.") - - group = parser.getgroup("collect", "collection") - group.addoption('--collectonly', - action="store_true", dest="collectonly", - help="only collect tests, don't execute them."), - group.addoption('--pyargs', action="store_true", - help="try to interpret all arguments as python packages.") - group.addoption("--ignore", action="append", metavar="path", - help="ignore path during collection (multi-allowed).") - group.addoption('--confcutdir', dest="confcutdir", default=None, - metavar="dir", - help="only load conftest.py's relative to specified dir.") - - group = parser.getgroup("debugconfig", - "test process debugging and configuration") - group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir", - help="base temporary directory for this test run.") - - -def pytest_namespace(): - return dict(collect=dict(Item=Item, Collector=Collector, File=File)) - -def pytest_configure(config): - py.test.config = config # compatibiltiy - if config.option.exitfirst: - config.option.maxfail = 1 - -def pytest_cmdline_main(config): - """ default command line protocol for initialization, session, - running tests and reporting. """ - session = Session(config) - session.exitstatus = EXIT_OK - try: - config.pluginmanager.do_configure(config) - config.hook.pytest_sessionstart(session=session) - config.hook.pytest_collection(session=session) - config.hook.pytest_runtestloop(session=session) - except pytest.UsageError: - raise - except KeyboardInterrupt: - excinfo = py.code.ExceptionInfo() - config.hook.pytest_keyboard_interrupt(excinfo=excinfo) - session.exitstatus = EXIT_INTERRUPTED - except: - excinfo = py.code.ExceptionInfo() - config.pluginmanager.notify_exception(excinfo) - session.exitstatus = EXIT_INTERNALERROR - if excinfo.errisinstance(SystemExit): - sys.stderr.write("mainloop: caught Spurious SystemExit!\n") - if not session.exitstatus and session._testsfailed: - session.exitstatus = EXIT_TESTSFAILED - config.hook.pytest_sessionfinish(session=session, - exitstatus=session.exitstatus) - config.pluginmanager.do_unconfigure(config) - return session.exitstatus - -def pytest_collection(session): - session.perform_collect() - hook = session.config.hook - hook.pytest_collection_modifyitems(session=session, - config=session.config, items=session.items) - hook.pytest_collection_finish(session=session) - return True - -def pytest_runtestloop(session): - if session.config.option.collectonly: - return True - for item in session.session.items: - item.config.hook.pytest_runtest_protocol(item=item) - if session.shouldstop: - raise session.Interrupted(session.shouldstop) - return True - -def pytest_ignore_collect(path, config): - p = path.dirpath() - ignore_paths = config._getconftest_pathlist("collect_ignore", path=p) - ignore_paths = ignore_paths or [] - excludeopt = config.getvalue("ignore") - if excludeopt: - ignore_paths.extend([py.path.local(x) for x in excludeopt]) - return path in ignore_paths - -class HookProxy: - def __init__(self, fspath, config): - self.fspath = fspath - self.config = config - def __getattr__(self, name): - hookmethod = getattr(self.config.hook, name) - def call_matching_hooks(**kwargs): - plugins = self.config._getmatchingplugins(self.fspath) - return hookmethod.pcall(plugins, **kwargs) - return call_matching_hooks - -def compatproperty(name): - def fget(self): - #print "retrieving %r property from %s" %(name, self.fspath) - py.log._apiwarn("2.0", "use pytest.%s for " - "test collection and item classes" % name) - return getattr(pytest, name) - return property(fget) - -class Node(object): - """ base class for all Nodes in the collection tree. - Collector subclasses have children, Items are terminal nodes.""" - - def __init__(self, name, parent=None, config=None, session=None): - #: a unique name with the scope of the parent - self.name = name - - #: the parent collector node. - self.parent = parent - - #: the test config object - self.config = config or parent.config - - #: the collection this node is part of - self.session = session or parent.session - - #: filesystem path where this node was collected from - self.fspath = getattr(parent, 'fspath', None) - self.ihook = self.session.gethookproxy(self.fspath) - self.keywords = {self.name: True} - - Module = compatproperty("Module") - Class = compatproperty("Class") - Function = compatproperty("Function") - File = compatproperty("File") - Item = compatproperty("Item") - - def __repr__(self): - return "<%s %r>" %(self.__class__.__name__, getattr(self, 'name', None)) - - # methods for ordering nodes - @property - def nodeid(self): - try: - return self._nodeid - except AttributeError: - self._nodeid = x = self._makeid() - return x - - def _makeid(self): - return self.parent.nodeid + "::" + self.name - - def __eq__(self, other): - if not isinstance(other, Node): - return False - return self.__class__ == other.__class__ and \ - self.name == other.name and self.parent == other.parent - - def __ne__(self, other): - return not self == other - - def __hash__(self): - return hash((self.name, self.parent)) - - def setup(self): - pass - - def teardown(self): - pass - - def _memoizedcall(self, attrname, function): - exattrname = "_ex_" + attrname - failure = getattr(self, exattrname, None) - if failure is not None: - py.builtin._reraise(failure[0], failure[1], failure[2]) - if hasattr(self, attrname): - return getattr(self, attrname) - try: - res = function() - except py.builtin._sysex: - raise - except: - failure = py.std.sys.exc_info() - setattr(self, exattrname, failure) - raise - setattr(self, attrname, res) - return res - - def listchain(self): - """ return list of all parent collectors up to self, - starting from root of collection tree. """ - l = [self] - while 1: - x = l[0] - if x.parent is not None: # and x.parent.parent is not None: - l.insert(0, x.parent) - else: - return l - - def listnames(self): - return [x.name for x in self.listchain()] - - def getparent(self, cls): - current = self - while current and not isinstance(current, cls): - current = current.parent - return current - - def _prunetraceback(self, excinfo): - pass - - def _repr_failure_py(self, excinfo, style=None): - if self.config.option.fulltrace: - style="long" - else: - self._prunetraceback(excinfo) - # XXX should excinfo.getrepr record all data and toterminal() - # process it? - if style is None: - if self.config.option.tbstyle == "short": - style = "short" - else: - style = "long" - return excinfo.getrepr(funcargs=True, - showlocals=self.config.option.showlocals, - style=style) - - repr_failure = _repr_failure_py - -class Collector(Node): - """ Collector instances create children through collect() - and thus iteratively build a tree. - """ - class CollectError(Exception): - """ an error during collection, contains a custom message. """ - - def collect(self): - """ returns a list of children (items and collectors) - for this collection node. - """ - raise NotImplementedError("abstract") - - def repr_failure(self, excinfo): - """ represent a collection failure. """ - if excinfo.errisinstance(self.CollectError): - exc = excinfo.value - return str(exc.args[0]) - return self._repr_failure_py(excinfo, style="short") - - def _memocollect(self): - """ internal helper method to cache results of calling collect(). """ - return self._memoizedcall('_collected', lambda: list(self.collect())) - - def _prunetraceback(self, excinfo): - if hasattr(self, 'fspath'): - path = self.fspath - traceback = excinfo.traceback - ntraceback = traceback.cut(path=self.fspath) - if ntraceback == traceback: - ntraceback = ntraceback.cut(excludepath=tracebackcutdir) - excinfo.traceback = ntraceback.filter() - -class FSCollector(Collector): - def __init__(self, fspath, parent=None, config=None, session=None): - fspath = py.path.local(fspath) # xxx only for test_resultlog.py? - name = fspath.basename - if parent is not None: - rel = fspath.relto(parent.fspath) - if rel: - name = rel - name = name.replace(os.sep, "/") - super(FSCollector, self).__init__(name, parent, config, session) - self.fspath = fspath - - def _makeid(self): - if self == self.session: - return "." - relpath = self.session.fspath.bestrelpath(self.fspath) - if os.sep != "/": - relpath = relpath.replace(os.sep, "/") - return relpath - -class File(FSCollector): - """ base class for collecting tests from a file. """ - -class Item(Node): - """ a basic test invocation item. Note that for a single function - there might be multiple test invocation items. - """ - def reportinfo(self): - return self.fspath, None, "" - - @property - def location(self): - try: - return self._location - except AttributeError: - location = self.reportinfo() - location = (str(location[0]), location[1], str(location[2])) - self._location = location - return location - -class NoMatch(Exception): - """ raised if matching cannot locate a matching names. """ - -class Session(FSCollector): - class Interrupted(KeyboardInterrupt): - """ signals an interrupted test run. """ - __module__ = 'builtins' # for py3 - - def __init__(self, config): - super(Session, self).__init__(py.path.local(), parent=None, - config=config, session=self) - self.config.pluginmanager.register(self, name="session", prepend=True) - self._testsfailed = 0 - self.shouldstop = False - self.trace = config.trace.root.get("collection") - self._norecursepatterns = config.getini("norecursedirs") - - def pytest_collectstart(self): - if self.shouldstop: - raise self.Interrupted(self.shouldstop) - - def pytest_runtest_logreport(self, report): - if report.failed and 'xfail' not in getattr(report, 'keywords', []): - self._testsfailed += 1 - maxfail = self.config.getvalue("maxfail") - if maxfail and self._testsfailed >= maxfail: - self.shouldstop = "stopping after %d failures" % ( - self._testsfailed) - pytest_collectreport = pytest_runtest_logreport - - def isinitpath(self, path): - return path in self._initialpaths - - def gethookproxy(self, fspath): - return HookProxy(fspath, self.config) - - def perform_collect(self, args=None, genitems=True): - if args is None: - args = self.config.args - self.trace("perform_collect", self, args) - self.trace.root.indent += 1 - self._notfound = [] - self._initialpaths = set() - self._initialparts = [] - for arg in args: - parts = self._parsearg(arg) - self._initialparts.append(parts) - self._initialpaths.add(parts[0]) - self.ihook.pytest_collectstart(collector=self) - rep = self.ihook.pytest_make_collect_report(collector=self) - self.ihook.pytest_collectreport(report=rep) - self.trace.root.indent -= 1 - if self._notfound: - for arg, exc in self._notfound: - line = "(no name %r in any of %r)" % (arg, exc.args[0]) - raise pytest.UsageError("not found: %s\n%s" %(arg, line)) - if not genitems: - return rep.result - else: - self.items = items = [] - if rep.passed: - for node in rep.result: - self.items.extend(self.genitems(node)) - return items - - def collect(self): - for parts in self._initialparts: - arg = "::".join(map(str, parts)) - self.trace("processing argument", arg) - self.trace.root.indent += 1 - try: - for x in self._collect(arg): - yield x - except NoMatch: - # we are inside a make_report hook so - # we cannot directly pass through the exception - self._notfound.append((arg, sys.exc_info()[1])) - self.trace.root.indent -= 1 - break - self.trace.root.indent -= 1 - - def _collect(self, arg): - names = self._parsearg(arg) - path = names.pop(0) - if path.check(dir=1): - assert not names, "invalid arg %r" %(arg,) - for path in path.visit(rec=self._recurse, bf=True, sort=True): - for x in self._collectfile(path): - yield x - else: - assert path.check(file=1) - for x in self.matchnodes(self._collectfile(path), names): - yield x - - def _collectfile(self, path): - ihook = self.gethookproxy(path) - if not self.isinitpath(path): - if ihook.pytest_ignore_collect(path=path, config=self.config): - return () - return ihook.pytest_collect_file(path=path, parent=self) - - def _recurse(self, path): - ihook = self.gethookproxy(path) - if ihook.pytest_ignore_collect(path=path, config=self.config): - return - for pat in self._norecursepatterns: - if path.check(fnmatch=pat): - return False - ihook.pytest_collect_directory(path=path, parent=self) - return True - - def _tryconvertpyarg(self, x): - try: - mod = __import__(x, None, None, ['__doc__']) - except (ValueError, ImportError): - return x - p = py.path.local(mod.__file__) - if p.purebasename == "__init__": - p = p.dirpath() - else: - p = p.new(basename=p.purebasename+".py") - return p - - def _parsearg(self, arg): - """ return (fspath, names) tuple after checking the file exists. """ - arg = str(arg) - if self.config.option.pyargs: - arg = self._tryconvertpyarg(arg) - parts = str(arg).split("::") - relpath = parts[0].replace("/", os.sep) - path = self.fspath.join(relpath, abs=True) - if not path.check(): - if self.config.option.pyargs: - msg = "file or package not found: " - else: - msg = "file not found: " - raise pytest.UsageError(msg + arg) - parts[0] = path - return parts - - def matchnodes(self, matching, names): - self.trace("matchnodes", matching, names) - self.trace.root.indent += 1 - nodes = self._matchnodes(matching, names) - num = len(nodes) - self.trace("matchnodes finished -> ", num, "nodes") - self.trace.root.indent -= 1 - if num == 0: - raise NoMatch(matching, names[:1]) - return nodes - - def _matchnodes(self, matching, names): - if not matching or not names: - return matching - name = names[0] - assert name - nextnames = names[1:] - resultnodes = [] - for node in matching: - if isinstance(node, pytest.Item): - resultnodes.append(node) - continue - assert isinstance(node, pytest.Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) - if rep.passed: - for x in rep.result: - if x.name == name: - resultnodes.extend(self.matchnodes([x], nextnames)) - node.ihook.pytest_collectreport(report=rep) - return resultnodes - - def genitems(self, node): - self.trace("genitems", node) - if isinstance(node, pytest.Item): - node.ihook.pytest_itemcollected(item=node) - yield node - else: - assert isinstance(node, pytest.Collector) - node.ihook.pytest_collectstart(collector=node) - rep = node.ihook.pytest_make_collect_report(collector=node) - if rep.passed: - for subnode in rep.result: - for x in self.genitems(subnode): - yield x - node.ihook.pytest_collectreport(report=rep) - -Session = Session --- /dev/null +++ b/_pytest/mark.py @@ -0,0 +1,176 @@ +""" 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', + action="store", dest="keyword", default='', metavar="KEYWORDEXPR", + help="only run tests which match given keyword expression. " + "An expression consists of space-separated terms. " + "Each term must match. Precede a term with '-' to negate. " + "Terminate expression with ':' to make the first match match " + "all subsequent tests (usually file-order). ") + +def pytest_collection_modifyitems(items, config): + keywordexpr = config.option.keyword + if not keywordexpr: + return + selectuntil = False + if keywordexpr[-1] == ":": + selectuntil = True + keywordexpr = keywordexpr[:-1] + + remaining = [] + deselected = [] + for colitem in items: + if keywordexpr and skipbykeyword(colitem, keywordexpr): + deselected.append(colitem) + else: + remaining.append(colitem) + if selectuntil: + keywordexpr = None + + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = remaining + +def skipbykeyword(colitem, keywordexpr): + """ return True if they given keyword expression means to + skip this collector/item. + """ + if not keywordexpr: + return + + itemkeywords = getkeywords(colitem) + for key in filter(None, keywordexpr.split()): + eor = key[:1] == '-' + if eor: + key = key[1:] + if not (eor ^ matchonekeyword(key, itemkeywords)): + return True + +def getkeywords(node): + keywords = {} + while node is not None: + keywords.update(node.keywords) + node = node.parent + return keywords + + +def matchonekeyword(key, itemkeywords): + for elem in key.split("."): + for kw in itemkeywords: + if elem in kw: + break + else: + return False + return True + +class MarkGenerator: + """ Factory for :class:`MarkDecorator` objects - exposed as + a ``py.test.mark`` singleton instance. Example:: + + import py + @py.test.mark.slowtest + def test_function(): + pass + + will set a 'slowtest' :class:`MarkInfo` object + on the ``test_function`` object. """ + + def __getattr__(self, name): + if name[0] == "_": + raise AttributeError(name) + return MarkDecorator(name) + +class MarkDecorator: + """ A decorator for test functions and test classes. When applied + it will create :class:`MarkInfo` objects which may be + :ref:`retrieved by hooks as item keywords` MarkDecorator instances + are usually created by writing:: + + mark1 = py.test.mark.NAME # simple MarkDecorator + mark2 = py.test.mark.NAME(name1=value) # parametrized MarkDecorator + + and can then be applied as decorators to test functions:: + + @mark2 + def test_function(): + pass + """ + def __init__(self, name): + self.markname = name + self.kwargs = {} + self.args = [] + + def __repr__(self): + d = self.__dict__.copy() + name = d.pop('markname') + return "" %(name, d) + + def __call__(self, *args, **kwargs): + """ if passed a single callable argument: decorate it with mark info. + otherwise add *args/**kwargs in-place to mark information. """ + if args: + func = args[0] + if len(args) == 1 and hasattr(func, '__call__') or \ + hasattr(func, '__bases__'): + if hasattr(func, '__bases__'): + if hasattr(func, 'pytestmark'): + l = func.pytestmark + if not isinstance(l, list): + func.pytestmark = [l, self] + else: + l.append(self) + else: + func.pytestmark = [self] + else: + holder = getattr(func, self.markname, None) + if holder is None: + holder = MarkInfo(self.markname, self.args, self.kwargs) + setattr(func, self.markname, holder) + else: + holder.kwargs.update(self.kwargs) + holder.args.extend(self.args) + return func + else: + self.args.extend(args) + self.kwargs.update(kwargs) + return self + +class MarkInfo: + """ Marking object created by :class:`MarkDecorator` instances. """ + def __init__(self, name, args, kwargs): + #: name of attribute + self.name = name + #: positional argument list, empty if none specified + self.args = args + #: keyword argument dictionary, empty if nothing specified + self.kwargs = kwargs + + def __repr__(self): + return "" % ( + self._name, self.args, self.kwargs) + +def pytest_itemcollected(item): + if not isinstance(item, pytest.Function): + return + try: + func = item.obj.__func__ + except AttributeError: + func = getattr(item.obj, 'im_func', item.obj) + pyclasses = (pytest.Class, pytest.Module) + for node in item.listchain(): + if isinstance(node, pyclasses): + marker = getattr(node.obj, 'pytestmark', None) + if marker is not None: + if isinstance(marker, list): + for mark in marker: + mark(func) + else: + marker(func) + node = node.parent + item.keywords.update(py.builtin._getfuncdict(func)) --- /dev/null +++ b/_pytest/pdb.py @@ -0,0 +1,77 @@ +""" interactive debugging with PDB, the Python Debugger. """ + +import py +import sys + +def pytest_addoption(parser): + group = parser.getgroup("general") + group._addoption('--pdb', + action="store_true", dest="usepdb", default=False, + help="start the interactive Python debugger on errors.") + +def pytest_namespace(): + return {'set_trace': pytestPDB().set_trace} + +def pytest_configure(config): + if config.getvalue("usepdb"): + config.pluginmanager.register(PdbInvoke(), 'pdbinvoke') + +class pytestPDB: + """ Pseudo PDB that defers to the real pdb. """ + item = None + + def set_trace(self): + """ invoke PDB set_trace debugging, dropping any IO capturing. """ + frame = sys._getframe().f_back + item = getattr(self, 'item', None) + if item is not None: + capman = item.config.pluginmanager.getplugin("capturemanager") + out, err = capman.suspendcapture() + if hasattr(item, 'outerr'): + item.outerr = (item.outerr[0] + out, item.outerr[1] + err) + tw = py.io.TerminalWriter() + tw.line() + tw.sep(">", "PDB set_trace (IO-capturing turned off)") + py.std.pdb.Pdb().set_trace(frame) + +def pdbitem(item): + pytestPDB.item = item +pytest_runtest_setup = pytest_runtest_call = pytest_runtest_teardown = pdbitem + +def pytest_runtest_makereport(): + pytestPDB.item = None + +class PdbInvoke: + def pytest_sessionfinish(self, session): + # don't display failures again at the end + session.config.option.tbstyle = "no" + def pytest_runtest_makereport(self, item, call, __multicall__): + if not call.excinfo or \ + call.excinfo.errisinstance(py.test.skip.Exception) or \ + call.excinfo.errisinstance(py.std.bdb.BdbQuit): + return + rep = __multicall__.execute() + if "xfail" in rep.keywords: + return rep + # we assume that the above execute() suspended capturing + tw = py.io.TerminalWriter() + tw.line() + tw.sep(">", "traceback") + rep.toterminal(tw) + tw.sep(">", "entering PDB") + post_mortem(call.excinfo._excinfo[2]) + return rep + +def post_mortem(t): + pdb = py.std.pdb + class Pdb(pdb.Pdb): + def get_stack(self, f, t): + stack, i = pdb.Pdb.get_stack(self, f, t) + if f is None: + i = max(0, len(stack) - 1) + while i and stack[i][0].f_locals.get("__tracebackhide__", False): + i-=1 + return stack, i + p = Pdb() + p.reset() + p.interaction(None, t) --- /dev/null +++ b/_pytest/doctest.py @@ -0,0 +1,79 @@ +""" discover and run doctests in modules and test files.""" + +import pytest, py +from py._code.code import TerminalRepr, ReprFileLocation + +def pytest_addoption(parser): + group = parser.getgroup("collect") + group.addoption("--doctest-modules", + action="store_true", default=False, + help="run doctests in all .py modules", + dest="doctestmodules") + group.addoption("--doctest-glob", + action="store", default="test*.txt", metavar="pat", + help="doctests file matching pattern, default: test*.txt", + dest="doctestglob") + +def pytest_collect_file(path, parent): + config = parent.config + if path.ext == ".py": + if config.option.doctestmodules: + return DoctestModule(path, parent) + elif path.check(fnmatch=config.getvalue("doctestglob")): + return DoctestTextfile(path, parent) + +class ReprFailDoctest(TerminalRepr): + def __init__(self, reprlocation, lines): + self.reprlocation = reprlocation + self.lines = lines + def toterminal(self, tw): + for line in self.lines: + tw.line(line) + self.reprlocation.toterminal(tw) + +class DoctestItem(pytest.Item): + def __init__(self, path, parent): + name = self.__class__.__name__ + ":" + path.basename + super(DoctestItem, self).__init__(name=name, parent=parent) + self.fspath = path + + def repr_failure(self, excinfo): + if excinfo.errisinstance(py.std.doctest.DocTestFailure): + doctestfailure = excinfo.value + example = doctestfailure.example + test = doctestfailure.test + filename = test.filename + lineno = test.lineno + example.lineno + 1 + message = excinfo.type.__name__ + reprlocation = ReprFileLocation(filename, lineno, message) + checker = py.std.doctest.OutputChecker() + REPORT_UDIFF = py.std.doctest.REPORT_UDIFF + filelines = py.path.local(filename).readlines(cr=0) + i = max(test.lineno, max(0, lineno - 10)) # XXX? + lines = [] + for line in filelines[i:lineno]: + lines.append("%03d %s" % (i+1, line)) + i += 1 + lines += checker.output_difference(example, + doctestfailure.got, REPORT_UDIFF).split("\n") + return ReprFailDoctest(reprlocation, lines) + elif excinfo.errisinstance(py.std.doctest.UnexpectedException): + excinfo = py.code.ExceptionInfo(excinfo.value.exc_info) + return super(DoctestItem, self).repr_failure(excinfo) + else: + return super(DoctestItem, self).repr_failure(excinfo) + + def reportinfo(self): + return self.fspath, None, "[doctest]" + +class DoctestTextfile(DoctestItem): + def runtest(self): + failed, tot = py.std.doctest.testfile( + str(self.fspath), module_relative=False, + raise_on_error=True, verbose=0) + +class DoctestModule(DoctestItem): + def runtest(self): + module = self.fspath.pyimport() + failed, tot = py.std.doctest.testmod( + module, raise_on_error=True, verbose=0) --- a/pytest/plugin/python.py +++ /dev/null @@ -1,823 +0,0 @@ -""" Python test discovery, setup and run of test functions. """ -import py -import inspect -import sys -import pytest -from py._code.code import TerminalRepr - -cutdir = py.path.local(pytest.__file__).dirpath() - - -def pytest_addoption(parser): - group = parser.getgroup("general") - group.addoption('--funcargs', - action="store_true", dest="showfuncargs", default=False, - help="show available function arguments, sorted by plugin") - -def pytest_cmdline_main(config): - if config.option.showfuncargs: - showfuncargs(config) - return 0 - -def pytest_namespace(__multicall__): - __multicall__.execute() - raises.Exception = pytest.fail.Exception - return { - 'raises' : raises, - 'collect': { - 'Module': Module, 'Class': Class, 'Instance': Instance, - 'Function': Function, 'Generator': Generator, - '_fillfuncargs': fillfuncargs} - } - -def pytest_funcarg__pytestconfig(request): - """ the pytest config object with access to command line opts.""" - return request.config - -def pytest_pyfunc_call(__multicall__, pyfuncitem): - if not __multicall__.execute(): - testfunction = pyfuncitem.obj - if pyfuncitem._isyieldedfunction(): - testfunction(*pyfuncitem._args) - else: - funcargs = pyfuncitem.funcargs - testfunction(**funcargs) - -def pytest_collect_file(path, parent): - ext = path.ext - pb = path.purebasename - if ext == ".py" and (pb.startswith("test_") or pb.endswith("_test") or - parent.session.isinitpath(path)): - return parent.ihook.pytest_pycollect_makemodule( - path=path, parent=parent) - -def pytest_pycollect_makemodule(path, parent): - return Module(path, parent) - -def pytest_pycollect_makeitem(__multicall__, collector, name, obj): - res = __multicall__.execute() - if res is not None: - return res - if collector._istestclasscandidate(name, obj): - if hasattr(collector.obj, 'unittest'): - return # we assume it's a mixin class for a TestCase derived one - return Class(name, parent=collector) - elif collector.funcnamefilter(name) and hasattr(obj, '__call__'): - if is_generator(obj): - return Generator(name, parent=collector) - else: - return collector._genfunctions(name, obj) - -def is_generator(func): - try: - return py.code.getrawcode(func).co_flags & 32 # generator function - except AttributeError: # builtin functions have no bytecode - # assume them to not be generators - return False - -class PyobjMixin(object): - def obj(): - def fget(self): - try: - return self._obj - except AttributeError: - self._obj = obj = self._getobj() - return obj - def fset(self, value): - self._obj = value - return property(fget, fset, None, "underlying python object") - obj = obj() - - def _getobj(self): - return getattr(self.parent.obj, self.name) - - def getmodpath(self, stopatmodule=True, includemodule=False): - """ return python path relative to the containing module. """ - chain = self.listchain() - chain.reverse() - parts = [] - for node in chain: - if isinstance(node, Instance): - continue - name = node.name - if isinstance(node, Module): - assert name.endswith(".py") - name = name[:-3] - if stopatmodule: - if includemodule: - parts.append(name) - break - parts.append(name) - parts.reverse() - s = ".".join(parts) - return s.replace(".[", "[") - - def _getfslineno(self): - try: - return self._fslineno - except AttributeError: - pass - obj = self.obj - # xxx let decorators etc specify a sane ordering - if hasattr(obj, 'place_as'): - obj = obj.place_as - - self._fslineno = py.code.getfslineno(obj) - return self._fslineno - - def reportinfo(self): - # XXX caching? - obj = self.obj - if hasattr(obj, 'compat_co_firstlineno'): - # nose compatibility - fspath = sys.modules[obj.__module__].__file__ - if fspath.endswith(".pyc"): - fspath = fspath[:-1] - #assert 0 - #fn = inspect.getsourcefile(obj) or inspect.getfile(obj) - lineno = obj.compat_co_firstlineno - modpath = obj.__module__ - else: - fspath, lineno = self._getfslineno() - modpath = self.getmodpath() - return fspath, lineno, modpath - -class PyCollectorMixin(PyobjMixin, pytest.Collector): - - def funcnamefilter(self, name): - return name.startswith('test') - def classnamefilter(self, name): - return name.startswith('Test') - - def collect(self): - # NB. we avoid random getattrs and peek in the __dict__ instead - # (XXX originally introduced from a PyPy need, still true?) - dicts = [getattr(self.obj, '__dict__', {})] - for basecls in inspect.getmro(self.obj.__class__): - dicts.append(basecls.__dict__) - seen = {} - l = [] - for dic in dicts: - for name, obj in dic.items(): - if name in seen: - continue - seen[name] = True - if name[0] != "_": - res = self.makeitem(name, obj) - if res is None: - continue - if not isinstance(res, list): - res = [res] - l.extend(res) - l.sort(key=lambda item: item.reportinfo()[:2]) - return l - - def makeitem(self, name, obj): - return self.ihook.pytest_pycollect_makeitem( - collector=self, name=name, obj=obj) - - def _istestclasscandidate(self, name, obj): - if self.classnamefilter(name) and \ - inspect.isclass(obj): - if hasinit(obj): - # XXX WARN - return False - return True - - def _genfunctions(self, name, funcobj): - module = self.getparent(Module).obj - clscol = self.getparent(Class) - cls = clscol and clscol.obj or None - metafunc = Metafunc(funcobj, config=self.config, - cls=cls, module=module) - gentesthook = self.config.hook.pytest_generate_tests - plugins = getplugins(self, withpy=True) - gentesthook.pcall(plugins, metafunc=metafunc) - if not metafunc._calls: - return Function(name, parent=self) - l = [] - for callspec in metafunc._calls: - subname = "%s[%s]" %(name, callspec.id) - function = Function(name=subname, parent=self, - callspec=callspec, callobj=funcobj, keywords={callspec.id:True}) - l.append(function) - return l - -class Module(pytest.File, PyCollectorMixin): - def _getobj(self): - return self._memoizedcall('_obj', self._importtestmodule) - - def _importtestmodule(self): - # we assume we are only called once per module - try: - mod = self.fspath.pyimport(ensuresyspath=True) - except SyntaxError: - excinfo = py.code.ExceptionInfo() - raise self.CollectError(excinfo.getrepr(style="short")) - except self.fspath.ImportMismatchError: - e = sys.exc_info()[1] - raise self.CollectError( - "import file mismatch:\n" - "imported module %r has this __file__ attribute:\n" - " %s\n" - "which is not the same as the test file we want to collect:\n" - " %s\n" - "HINT: use a unique basename for your test file modules" - % e.args - ) - #print "imported test module", mod - self.config.pluginmanager.consider_module(mod) - return mod - - def setup(self): - if hasattr(self.obj, 'setup_module'): - #XXX: nose compat hack, move to nose plugin - # if it takes a positional arg, its probably a pytest style one - # so we pass the current module object - if inspect.getargspec(self.obj.setup_module)[0]: - self.obj.setup_module(self.obj) - else: - self.obj.setup_module() - - def teardown(self): - if hasattr(self.obj, 'teardown_module'): - #XXX: nose compat hack, move to nose plugin - # if it takes a positional arg, its probably a py.test style one - # so we pass the current module object - if inspect.getargspec(self.obj.teardown_module)[0]: - self.obj.teardown_module(self.obj) - else: - self.obj.teardown_module() - -class Class(PyCollectorMixin, pytest.Collector): - - def collect(self): - return [Instance(name="()", parent=self)] - - def setup(self): - setup_class = getattr(self.obj, 'setup_class', None) - if setup_class is not None: - setup_class = getattr(setup_class, 'im_func', setup_class) - setup_class(self.obj) - - def teardown(self): - teardown_class = getattr(self.obj, 'teardown_class', None) - if teardown_class is not None: - teardown_class = getattr(teardown_class, 'im_func', teardown_class) - teardown_class(self.obj) - -class Instance(PyCollectorMixin, pytest.Collector): - def _getobj(self): - return self.parent.obj() - - def newinstance(self): - self.obj = self._getobj() - return self.obj - -class FunctionMixin(PyobjMixin): - """ mixin for the code common to Function and Generator. - """ - - def setup(self): - """ perform setup for this test function. """ - if inspect.ismethod(self.obj): - name = 'setup_method' - else: - name = 'setup_function' - if isinstance(self.parent, Instance): - obj = self.parent.newinstance() - self.obj = self._getobj() - else: - obj = self.parent.obj - setup_func_or_method = getattr(obj, name, None) - if setup_func_or_method is not None: - setup_func_or_method(self.obj) - - def teardown(self): - """ perform teardown for this test function. """ - if inspect.ismethod(self.obj): - name = 'teardown_method' - else: - name = 'teardown_function' - obj = self.parent.obj - teardown_func_or_meth = getattr(obj, name, None) - if teardown_func_or_meth is not None: - teardown_func_or_meth(self.obj) - - def _prunetraceback(self, excinfo): - if hasattr(self, '_obj') and not self.config.option.fulltrace: - code = py.code.Code(self.obj) - path, firstlineno = code.path, code.firstlineno - traceback = excinfo.traceback - ntraceback = traceback.cut(path=path, firstlineno=firstlineno) - if ntraceback == traceback: - ntraceback = ntraceback.cut(path=path) - if ntraceback == traceback: - ntraceback = ntraceback.cut(excludepath=cutdir) - excinfo.traceback = ntraceback.filter() - - def _repr_failure_py(self, excinfo, style="long"): - if excinfo.errisinstance(FuncargRequest.LookupError): - fspath, lineno, msg = self.reportinfo() - lines, _ = inspect.getsourcelines(self.obj) - for i, line in enumerate(lines): - if line.strip().startswith('def'): - return FuncargLookupErrorRepr(fspath, lineno, - lines[:i+1], str(excinfo.value)) - return super(FunctionMixin, self)._repr_failure_py(excinfo, - style=style) - - def repr_failure(self, excinfo, outerr=None): - assert outerr is None, "XXX outerr usage is deprecated" - return self._repr_failure_py(excinfo, - style=self.config.option.tbstyle) - -class FuncargLookupErrorRepr(TerminalRepr): - def __init__(self, filename, firstlineno, deflines, errorstring): - self.deflines = deflines - self.errorstring = errorstring - self.filename = filename - self.firstlineno = firstlineno - - def toterminal(self, tw): - tw.line() - for line in self.deflines: - tw.line(" " + line.strip()) - for line in self.errorstring.split("\n"): - tw.line(" " + line.strip(), red=True) - tw.line() - tw.line("%s:%d" % (self.filename, self.firstlineno+1)) - -class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector): - def collect(self): - # test generators are seen as collectors but they also - # invoke setup/teardown on popular request - # (induced by the common "test_*" naming shared with normal tests) - self.config._setupstate.prepare(self) - - l = [] - seen = {} - for i, x in enumerate(self.obj()): - name, call, args = self.getcallargs(x) - if not py.builtin.callable(call): - raise TypeError("%r yielded non callable test %r" %(self.obj, call,)) - if name is None: - name = "[%d]" % i - else: - name = "['%s']" % name - if name in seen: - raise ValueError("%r generated tests with non-unique name %r" %(self, name)) - seen[name] = True - l.append(Function(name, self, args=args, callobj=call)) - return l - - def getcallargs(self, obj): - if not isinstance(obj, (tuple, list)): - obj = (obj,) - # explict naming - if isinstance(obj[0], py.builtin._basestring): - name = obj[0] - obj = obj[1:] - else: - name = None - call, args = obj[0], obj[1:] - return name, call, args - - -# -# Test Items -# -_dummy = object() -class Function(FunctionMixin, pytest.Item): - """ a Function Item is responsible for setting up - and executing a Python callable test object. - """ - _genid = None - def __init__(self, name, parent=None, args=None, config=None, - callspec=None, callobj=_dummy, keywords=None, session=None): - super(Function, self).__init__(name, parent, - config=config, session=session) - self._args = args - if self._isyieldedfunction(): - assert not callspec, ( - "yielded functions (deprecated) cannot have funcargs") - else: - if callspec is not None: - self.funcargs = callspec.funcargs or {} - self._genid = callspec.id - if hasattr(callspec, "param"): - self._requestparam = callspec.param - else: - self.funcargs = {} - if callobj is not _dummy: - self._obj = callobj - self.function = getattr(self.obj, 'im_func', self.obj) - self.keywords.update(py.builtin._getfuncdict(self.obj) or {}) - if keywords: - self.keywords.update(keywords) - - def _getobj(self): - name = self.name - i = name.find("[") # parametrization - if i != -1: - name = name[:i] - return getattr(self.parent.obj, name) - - def _isyieldedfunction(self): - return self._args is not None - - def runtest(self): - """ execute the underlying test function. """ - self.ihook.pytest_pyfunc_call(pyfuncitem=self) - - def setup(self): - super(Function, self).setup() - if hasattr(self, 'funcargs'): - fillfuncargs(self) - - def __eq__(self, other): - try: - return (self.name == other.name and - self._args == other._args and - self.parent == other.parent and - self.obj == other.obj and - getattr(self, '_genid', None) == - getattr(other, '_genid', None) - ) - except AttributeError: - pass - return False - - def __ne__(self, other): - return not self == other - - def __hash__(self): - return hash((self.parent, self.name)) - -def hasinit(obj): - init = getattr(obj, '__init__', None) - if init: - if init != object.__init__: - return True - - -def getfuncargnames(function): - # XXX merge with main.py's varnames - argnames = py.std.inspect.getargs(py.code.getrawcode(function))[0] - startindex = py.std.inspect.ismethod(function) and 1 or 0 - defaults = getattr(function, 'func_defaults', - getattr(function, '__defaults__', None)) or () - numdefaults = len(defaults) - if numdefaults: - return argnames[startindex:-numdefaults] - return argnames[startindex:] - -def fillfuncargs(function): - """ fill missing funcargs. """ - request = FuncargRequest(pyfuncitem=function) - request._fillfuncargs() - -def getplugins(node, withpy=False): # might by any node - plugins = node.config._getmatchingplugins(node.fspath) - if withpy: - mod = node.getparent(pytest.Module) - if mod is not None: - plugins.append(mod.obj) - inst = node.getparent(pytest.Instance) - if inst is not None: - plugins.append(inst.obj) - return plugins - -_notexists = object() -class CallSpec: - def __init__(self, funcargs, id, param): - self.funcargs = funcargs - self.id = id - if param is not _notexists: - self.param = param - def __repr__(self): - return "" %( - self.id, getattr(self, 'param', '?'), self.funcargs) - -class Metafunc: - def __init__(self, function, config=None, cls=None, module=None): - self.config = config - self.module = module - self.function = function - self.funcargnames = getfuncargnames(function) - self.cls = cls - self.module = module - self._calls = [] - self._ids = py.builtin.set() - - def addcall(self, funcargs=None, id=_notexists, param=_notexists): - """ add a new call to the underlying test function during the - collection phase of a test run. - - :arg funcargs: argument keyword dictionary used when invoking - the test function. - - :arg id: used for reporting and identification purposes. If you - don't supply an `id` the length of the currently - list of calls to the test function will be used. - - :arg param: will be exposed to a later funcarg factory invocation - through the ``request.param`` attribute. Setting it (instead of - directly providing a ``funcargs`` ditionary) is called - *indirect parametrization*. Indirect parametrization is - preferable if test values are expensive to setup or can - only be created after certain fixtures or test-run related - initialization code has been run. - """ - assert funcargs is None or isinstance(funcargs, dict) - if id is None: - raise ValueError("id=None not allowed") - if id is _notexists: - id = len(self._calls) - id = str(id) - if id in self._ids: - raise ValueError("duplicate id %r" % id) - self._ids.add(id) - self._calls.append(CallSpec(funcargs, id, param)) - -class FuncargRequest: - """ A request for function arguments from a test function. """ - _argprefix = "pytest_funcarg__" - _argname = None - - class LookupError(LookupError): - """ error on performing funcarg request. """ - - def __init__(self, pyfuncitem): - self._pyfuncitem = pyfuncitem - if hasattr(pyfuncitem, '_requestparam'): - self.param = pyfuncitem._requestparam - self._plugins = getplugins(pyfuncitem, withpy=True) - self._funcargs = self._pyfuncitem.funcargs.copy() - self._name2factory = {} - self._currentarg = None - - @property - def function(self): - """ function object of the test invocation. """ - return self._pyfuncitem.obj - - @property - def keywords(self): - """ keywords of the test function item. - - .. versionadded:: 2.0 - """ - return self._pyfuncitem.keywords - - @property - def module(self): - """ module where the test function was collected. """ - return self._pyfuncitem.getparent(pytest.Module).obj - - @property - def cls(self): - """ class (can be None) where the test function was collected. """ - clscol = self._pyfuncitem.getparent(pytest.Class) - if clscol: - return clscol.obj - @property - def instance(self): - """ instance (can be None) on which test function was collected. """ - return py.builtin._getimself(self.function) - - @property - def config(self): - """ the pytest config object associated with this request. """ - return self._pyfuncitem.config - - @property - def fspath(self): - """ the file system path of the test module which collected this test. """ - return self._pyfuncitem.fspath - - def _fillfuncargs(self): - argnames = getfuncargnames(self.function) - if argnames: - assert not getattr(self._pyfuncitem, '_args', None), ( - "yielded functions cannot have funcargs") - for argname in argnames: - if argname not in self._pyfuncitem.funcargs: - self._pyfuncitem.funcargs[argname] = self.getfuncargvalue(argname) - - - def applymarker(self, marker): - """ apply a marker to a single test function invocation. - This method is useful if you don't want to have a keyword/marker - on all function invocations. - - :arg marker: a :py:class:`pytest.plugin.mark.MarkDecorator` object - created by a call to ``py.test.mark.NAME(...)``. - """ - if not isinstance(marker, py.test.mark.XYZ.__class__): - raise ValueError("%r is not a py.test.mark.* object") - self._pyfuncitem.keywords[marker.markname] = marker - - def cached_setup(self, setup, teardown=None, scope="module", extrakey=None): - """ return a testing resource managed by ``setup`` & - ``teardown`` calls. ``scope`` and ``extrakey`` determine when the - ``teardown`` function will be called so that subsequent calls to - ``setup`` would recreate the resource. - - :arg teardown: function receiving a previously setup resource. - :arg setup: a no-argument function creating a resource. - :arg scope: a string value out of ``function``, ``module`` or - ``session`` indicating the caching lifecycle of the resource. - :arg extrakey: added to internal caching key of (funcargname, scope). - """ - if not hasattr(self.config, '_setupcache'): - self.config._setupcache = {} # XXX weakref? - cachekey = (self._currentarg, self._getscopeitem(scope), extrakey) - cache = self.config._setupcache - try: - val = cache[cachekey] - except KeyError: - val = setup() - cache[cachekey] = val - if teardown is not None: - def finalizer(): - del cache[cachekey] - teardown(val) - self._addfinalizer(finalizer, scope=scope) - return val - - def getfuncargvalue(self, argname): - """ Retrieve a function argument by name for this test - function invocation. This allows one function argument factory - to call another function argument factory. If there are two - funcarg factories for the same test function argument the first - factory may use ``getfuncargvalue`` to call the second one and - do something additional with the resource. - """ - try: - return self._funcargs[argname] - except KeyError: - pass - if argname not in self._name2factory: - self._name2factory[argname] = self.config.pluginmanager.listattr( - plugins=self._plugins, - attrname=self._argprefix + str(argname) - ) - #else: we are called recursively - if not self._name2factory[argname]: - self._raiselookupfailed(argname) - funcargfactory = self._name2factory[argname].pop() - oldarg = self._currentarg - self._currentarg = argname - try: - self._funcargs[argname] = res = funcargfactory(request=self) - finally: - self._currentarg = oldarg - return res - - def _getscopeitem(self, scope): - if scope == "function": - return self._pyfuncitem - elif scope == "module": - return self._pyfuncitem.getparent(pytest.Module) - elif scope == "session": - return None - raise ValueError("unknown finalization scope %r" %(scope,)) - - def addfinalizer(self, finalizer): - """add finalizer function to be called after test function - finished execution. """ - self._addfinalizer(finalizer, scope="function") - - def _addfinalizer(self, finalizer, scope): - colitem = self._getscopeitem(scope) - self.config._setupstate.addfinalizer( - finalizer=finalizer, colitem=colitem) - - def __repr__(self): - return "" %(self._pyfuncitem) - - def _raiselookupfailed(self, argname): - available = [] - for plugin in self._plugins: - for name in vars(plugin): - if name.startswith(self._argprefix): - name = name[len(self._argprefix):] - if name not in available: - available.append(name) - fspath, lineno, msg = self._pyfuncitem.reportinfo() - msg = "LookupError: no factory found for function argument %r" % (argname,) - msg += "\n available funcargs: %s" %(", ".join(available),) - msg += "\n use 'py.test --funcargs [testpath]' for help on them." - raise self.LookupError(msg) - -def showfuncargs(config): - from pytest.plugin.session import Session - session = Session(config) - session.perform_collect() - if session.items: - plugins = getplugins(session.items[0]) - else: - plugins = getplugins(session) - curdir = py.path.local() - tw = py.io.TerminalWriter() - verbose = config.getvalue("verbose") - for plugin in plugins: - available = [] - for name, factory in vars(plugin).items(): - if name.startswith(FuncargRequest._argprefix): - name = name[len(FuncargRequest._argprefix):] - if name not in available: - available.append([name, factory]) - if available: - pluginname = plugin.__name__ - for name, factory in available: - loc = getlocation(factory, curdir) - if verbose: - funcargspec = "%s -- %s" %(name, loc,) - else: - funcargspec = name - tw.line(funcargspec, green=True) - doc = factory.__doc__ or "" - if doc: - for line in doc.split("\n"): - tw.line(" " + line.strip()) - else: - tw.line(" %s: no docstring available" %(loc,), - red=True) - -def getlocation(function, curdir): - import inspect - fn = py.path.local(inspect.getfile(function)) - lineno = py.builtin._getcode(function).co_firstlineno - if fn.relto(curdir): - fn = fn.relto(curdir) - return "%s:%d" %(fn, lineno+1) - -# builtin pytest.raises helper - -def raises(ExpectedException, *args, **kwargs): - """ assert that a code block/function call raises an exception. - - If using Python 2.5 or above, you may use this function as a - context manager:: - - >>> with raises(ZeroDivisionError): - ... 1/0 - - Or you can one of two forms: - - if args[0] is callable: raise AssertionError if calling it with - the remaining arguments does not raise the expected exception. - if args[0] is a string: raise AssertionError if executing the - the string in the calling scope does not raise expected exception. - examples: - >>> x = 5 - >>> raises(TypeError, lambda x: x + 'hello', x=x) - >>> raises(TypeError, "x + 'hello'") - """ - __tracebackhide__ = True - - if not args: - return RaisesContext(ExpectedException) - elif isinstance(args[0], str): - code, = args - assert isinstance(code, str) - frame = sys._getframe(1) - loc = frame.f_locals.copy() - loc.update(kwargs) - #print "raises frame scope: %r" % frame.f_locals - try: - code = py.code.Source(code).compile() - py.builtin.exec_(code, frame.f_globals, loc) - # XXX didn'T mean f_globals == f_locals something special? - # this is destroyed here ... - except ExpectedException: - return py.code.ExceptionInfo() - else: - func = args[0] - try: - func(*args[1:], **kwargs) - except ExpectedException: - return py.code.ExceptionInfo() - k = ", ".join(["%s=%r" % x for x in kwargs.items()]) - if k: - k = ', ' + k - expr = '%s(%r%s)' %(getattr(func, '__name__', func), args, k) - pytest.fail("DID NOT RAISE") - -class RaisesContext(object): - def __init__(self, ExpectedException): - self.ExpectedException = ExpectedException - self.excinfo = None - - def __enter__(self): - self.excinfo = object.__new__(py.code.ExceptionInfo) - return self.excinfo - - def __exit__(self, *tp): - __tracebackhide__ = True - if tp[0] is None: - pytest.fail("DID NOT RAISE") - self.excinfo.__init__(tp) - return issubclass(self.excinfo.type, self.ExpectedException) --- /dev/null +++ b/_pytest/hookspec.py @@ -0,0 +1,223 @@ +""" hook specifications for pytest plugins, invoked from main.py and builtin plugins. """ + +# ------------------------------------------------------------------------- +# Initialization +# ------------------------------------------------------------------------- + +def pytest_addhooks(pluginmanager): + """called at plugin load time to allow adding new hooks via a call to + pluginmanager.registerhooks(module).""" + + +def pytest_namespace(): + """return dict of name->object to be made globally available in + the py.test/pytest namespace. This hook is called before command + line options are parsed. + """ + +def pytest_cmdline_parse(pluginmanager, args): + """return initialized config object, parsing the specified args. """ +pytest_cmdline_parse.firstresult = True + +def pytest_addoption(parser): + """add optparse-style options and ini-style config values via calls + to ``parser.addoption`` and ``parser.addini(...)``. + """ + +def pytest_cmdline_main(config): + """ called for performing the main command line action. The default + implementation will invoke the configure hooks and runtest_mainloop. """ +pytest_cmdline_main.firstresult = True + +def pytest_configure(config): + """ called after command line options have been parsed. + and all plugins and initial conftest files been loaded. + """ + +def pytest_unconfigure(config): + """ called before test process is exited. """ + +def pytest_runtestloop(session): + """ called for performing the main runtest loop + (after collection finished). """ +pytest_runtestloop.firstresult = True + +# ------------------------------------------------------------------------- +# collection hooks +# ------------------------------------------------------------------------- + +def pytest_collection(session): + """ perform the collection protocol for the given session. """ +pytest_collection.firstresult = True + +def pytest_collection_modifyitems(session, config, items): + """ called after collection has been performed, may filter or re-order + the items in-place.""" + +def pytest_collection_finish(session): + """ called after collection has been performed and modified. """ + +def pytest_ignore_collect(path, config): + """ return True to prevent considering this path for collection. + This hook is consulted for all files and directories prior to calling + more specific hooks. + """ +pytest_ignore_collect.firstresult = True + +def pytest_collect_directory(path, parent): + """ called before traversing a directory for collection files. """ +pytest_collect_directory.firstresult = True + +def pytest_collect_file(path, parent): + """ return collection Node or None for the given path. Any new node + needs to have the specified ``parent`` as a parent.""" + +# logging hooks for collection +def pytest_collectstart(collector): + """ collector starts collecting. """ + +def pytest_itemcollected(item): + """ we just collected a test item. """ + +def pytest_collectreport(report): + """ collector finished collecting. """ + +def pytest_deselected(items): + """ called for test items deselected by keyword. """ + +def pytest_make_collect_report(collector): + """ perform ``collector.collect()`` and return a CollectReport. """ +pytest_make_collect_report.firstresult = True + +# ------------------------------------------------------------------------- +# Python test function related hooks +# ------------------------------------------------------------------------- + +def pytest_pycollect_makemodule(path, parent): + """ return a Module collector or None for the given path. + This hook will be called for each matching test module path. + The pytest_collect_file hook needs to be used if you want to + create test modules for files that do not match as a test module. + """ +pytest_pycollect_makemodule.firstresult = True + +def pytest_pycollect_makeitem(collector, name, obj): + """ return custom item/collector for a python object in a module, or None. """ +pytest_pycollect_makeitem.firstresult = True + +def pytest_pyfunc_call(pyfuncitem): + """ call underlying test function. """ +pytest_pyfunc_call.firstresult = True + +def pytest_generate_tests(metafunc): + """ generate (multiple) parametrized calls to a test function.""" + +# ------------------------------------------------------------------------- +# generic runtest related hooks +# ------------------------------------------------------------------------- +def pytest_itemstart(item, node=None): + """ (deprecated, use pytest_runtest_logstart). """ + +def pytest_runtest_protocol(item): + """ implements the standard runtest_setup/call/teardown protocol including + capturing exceptions and calling reporting hooks on the results accordingly. + + :return boolean: True if no further hook implementations should be invoked. + """ +pytest_runtest_protocol.firstresult = True + +def pytest_runtest_logstart(nodeid, location): + """ signal the start of a test run. """ + +def pytest_runtest_setup(item): + """ called before ``pytest_runtest_call(item)``. """ + +def pytest_runtest_call(item): + """ called to execute the test ``item``. """ + +def pytest_runtest_teardown(item): + """ called after ``pytest_runtest_call``. """ + +def pytest_runtest_makereport(item, call): + """ return a :py:class:`_pytest.runner.TestReport` object + for the given :py:class:`pytest.Item` and + :py:class:`_pytest.runner.CallInfo`. + """ +pytest_runtest_makereport.firstresult = True + +def pytest_runtest_logreport(report): + """ process item test report. """ + +# special handling for final teardown - somewhat internal for now +def pytest__teardown_final(session): + """ called before test session finishes. """ +pytest__teardown_final.firstresult = True + +def pytest__teardown_final_logerror(report, session): + """ called if runtest_teardown_final failed. """ + +# ------------------------------------------------------------------------- +# test session related hooks +# ------------------------------------------------------------------------- + +def pytest_sessionstart(session): + """ before session.main() is called. """ + +def pytest_sessionfinish(session, exitstatus): + """ whole test run finishes. """ + + +# ------------------------------------------------------------------------- +# hooks for customising the assert methods +# ------------------------------------------------------------------------- + +def pytest_assertrepr_compare(config, op, left, right): + """return explanation for comparisons in failing assert expressions. + + Return None for no custom explanation, otherwise return a list + of strings. The strings will be joined by newlines but any newlines + *in* a string will be escaped. Note that all but the first line will + be indented sligthly, the intention is for the first line to be a summary. + """ + +# ------------------------------------------------------------------------- +# hooks for influencing reporting (invoked from _pytest_terminal) +# ------------------------------------------------------------------------- + +def pytest_report_header(config): + """ return a string to be displayed as header info for terminal reporting.""" + +def pytest_report_teststatus(report): + """ return result-category, shortletter and verbose word for reporting.""" +pytest_report_teststatus.firstresult = True + +def pytest_terminal_summary(terminalreporter): + """ add additional section in terminal summary reporting. """ + +# ------------------------------------------------------------------------- +# doctest hooks +# ------------------------------------------------------------------------- + +def pytest_doctest_prepare_content(content): + """ return processed content for a given doctest""" +pytest_doctest_prepare_content.firstresult = True + + +# ------------------------------------------------------------------------- +# error handling and internal debugging hooks +# ------------------------------------------------------------------------- + +def pytest_plugin_registered(plugin, manager): + """ a new py lib plugin got registered. """ + +def pytest_plugin_unregistered(plugin): + """ a py lib plugin got unregistered. """ + +def pytest_internalerror(excrepr): + """ called for internal errors. """ + +def pytest_keyboard_interrupt(excinfo): + """ called for keyboard interrupt. """ + +def pytest_trace(category, msg): + """ called for debug info. """ --- a/testing/plugin/conftest.py +++ b/testing/plugin/conftest.py @@ -1,8 +1,8 @@ import py -import pytest.plugin -plugindir = py.path.local(pytest.plugin.__file__).dirpath() -from pytest.main import default_plugins +import _pytest +plugindir = py.path.local(_pytest.__file__).dirpath() +from _pytest.core import default_plugins def pytest_collect_file(path, parent): if path.basename.startswith("pytest_") and path.ext == ".py": --- /dev/null +++ b/_pytest/assertion.py @@ -0,0 +1,157 @@ +""" +support for presented detailed information in failing assertions. +""" +import py +import sys +from _pytest.monkeypatch import monkeypatch + +def pytest_addoption(parser): + group = parser.getgroup("debugconfig") + group._addoption('--no-assert', action="store_true", default=False, + dest="noassert", + help="disable python assert expression reinterpretation."), + +def pytest_configure(config): + # The _pytesthook attribute on the AssertionError is used by + # py._code._assertionnew to detect this plugin was loaded and in + # turn call the hooks defined here as part of the + # DebugInterpreter. + config._monkeypatch = m = monkeypatch() + if not config.getvalue("noassert") and not config.getvalue("nomagic"): + warn_about_missing_assertion() + def callbinrepr(op, left, right): + hook_result = config.hook.pytest_assertrepr_compare( + config=config, op=op, left=left, right=right) + for new_expl in hook_result: + if new_expl: + return '\n~'.join(new_expl) + m.setattr(py.builtin.builtins, + 'AssertionError', py.code._AssertionError) + m.setattr(py.code, '_reprcompare', callbinrepr) + +def pytest_unconfigure(config): + config._monkeypatch.undo() + +def warn_about_missing_assertion(): + try: + assert False + except AssertionError: + pass + else: + py.std.warnings.warn("Assertions are turned off!" + " (are you using python -O?)") + + +# Provide basestring in python3 +try: + basestring = basestring +except NameError: + basestring = str + + +def pytest_assertrepr_compare(op, left, right): + """return specialised explanations for some operators/operands""" + left_repr = py.io.saferepr(left, maxsize=30) + right_repr = py.io.saferepr(right, maxsize=30) + summary = '%s %s %s' % (left_repr, op, right_repr) + + 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) + + explanation = None + try: + if op == '==': + if istext(left) and istext(right): + explanation = _diff_text(left, right) + elif issequence(left) and issequence(right): + explanation = _compare_eq_sequence(left, right) + elif isset(left) and isset(right): + explanation = _compare_eq_set(left, right) + elif isdict(left) and isdict(right): + explanation = _diff_text(py.std.pprint.pformat(left), + py.std.pprint.pformat(right)) + except py.builtin._sysex: + raise + except: + excinfo = py.code.ExceptionInfo() + explanation = ['(pytest_assertion plugin: representation of ' + 'details failed. Probably an object has a faulty __repr__.)', + str(excinfo) + ] + + + if not explanation: + return None + + # Don't include pageloads of data, should be configurable + if len(''.join(explanation)) > 80*8: + explanation = ['Detailed information too verbose, truncated'] + + return [summary] + explanation + + +def _diff_text(left, right): + """Return the explanation for the diff between text + + This will skip leading and trailing characters which are + identical to keep the diff minimal. + """ + explanation = [] + 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 + if i > 42: + i -= 10 # Provide some context + explanation = ['Skipping %s identical ' + 'leading characters in diff' % i] + left = left[i:] + right = right[i:] + if len(left) == len(right): + for i in range(len(left)): + if left[-i] != right[-i]: + break + if i > 42: + i -= 10 # Provide some context + explanation += ['Skipping %s identical ' + 'trailing characters in diff' % i] + left = left[:-i] + right = right[:-i] + explanation += [line.strip('\n') + for line in py.std.difflib.ndiff(left.splitlines(), + right.splitlines())] + return explanation + + +def _compare_eq_sequence(left, right): + explanation = [] + for i in range(min(len(left), len(right))): + if left[i] != right[i]: + explanation += ['At index %s diff: %r != %r' % + (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)],)] + 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)) + + +def _compare_eq_set(left, right): + explanation = [] + diff_left = left - right + diff_right = right - left + if diff_left: + explanation.append('Extra items in the left set:') + for item in diff_left: + explanation.append(py.io.saferepr(item)) + if diff_right: + explanation.append('Extra items in the right set:') + for item in diff_right: + explanation.append(py.io.saferepr(item)) + return explanation From commits-noreply at bitbucket.org Sat Nov 13 11:12:31 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 04:12:31 -0600 (CST) Subject: [py-svn] pytest-xdist commit 2bda8b5eecd3: adapt to new import locations Message-ID: <20101113101231.670E56C0F76@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1289643139 -3600 # Node ID 2bda8b5eecd3cc2b9f4eb991826a88f3821ab17b # Parent 8cea75c227061419aa3044cad31e756596716bec adapt to new import locations --- a/ISSUES.txt +++ b/ISSUES.txt @@ -2,12 +2,11 @@ rename / hooks ----------------------------------------------- tag: bug -node -> slave +node -> slave transition for hooks? configure_node -> configure_slave - -allow to run xdist tests with xdist +allow to remotely run xdist tests with xdist ----------------------------------------------- tag: feature @@ -16,3 +15,10 @@ currently this doesn't work because the has no py.test plugin. How to configure/do register "xdist.plugin" on the remote side? +see to avoid any "from _pytest" internal imports +----------------------------------------------- +tag: feature + +currently tests and even xdist core code imports +names from the internal _pytest namespace. +See to avoid it. --- a/xdist/plugin.py +++ b/xdist/plugin.py @@ -236,7 +236,7 @@ def pytest_runtest_protocol(item): def forked_run_report(item): # for now, we run setup/teardown in the subprocess # XXX optionally allow sharing of setup/teardown - from pytest.plugin.runner import runtestprotocol + from _pytest.runner import runtestprotocol EXITSTATUS_TESTEXIT = 4 import marshal from xdist.remote import serialize_report @@ -262,7 +262,7 @@ def report_process_crash(item, result): path, lineno = item._getfslineno() info = "%s:%s: running the test CRASHED with signal %d" %( path, lineno, result.signal) - from pytest.plugin import runner + from _pytest import runner call = runner.CallInfo(lambda: 0/0, "???") call.excinfo = info rep = runner.pytest_runtest_makereport(item, call) --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5a6' +__version__ = '1.5a7' --- a/testing/test_slavemanage.py +++ b/testing/test_slavemanage.py @@ -11,8 +11,8 @@ def pytest_funcarg__hookrecorder(request def pytest_funcarg__hook(request): from xdist import newhooks - from pytest.main import HookRelay, PluginManager - from pytest import hookspec + from _pytest.core import HookRelay, PluginManager + from _pytest import hookspec return HookRelay([hookspec, newhooks], PluginManager()) class pytest_funcarg__mysetup: --- a/xdist/slavemanage.py +++ b/xdist/slavemanage.py @@ -3,7 +3,7 @@ import sys, os import execnet import xdist.remote -from pytest.plugin import runner # XXX load dynamically +from _pytest import runner # XXX load dynamically class NodeManager(object): def __init__(self, config, specs=None): --- a/xdist/looponfail.py +++ b/xdist/looponfail.py @@ -119,7 +119,7 @@ def init_slave_session(channel, args, op sys.path[:] = newpaths #fullwidth, hasmarkup = channel.receive() - from pytest.plugin.config import Config + from _pytest.config import Config config = Config() config.option.__dict__.update(option_dict) config._preparse(args) --- a/xdist/remote.py +++ b/xdist/remote.py @@ -105,7 +105,7 @@ def getinfodict(): ) def remote_initconfig(option_dict, args): - from pytest.plugin.config import Config + from _pytest.config import Config config = Config() config.pluginmanager.unregister(name="terminal") config._preparse(args, addopts=False) --- a/testing/test_dsession.py +++ b/testing/test_dsession.py @@ -1,5 +1,5 @@ from xdist.dsession import DSession, LoadScheduling, EachScheduling -from pytest.plugin import session as outcome +from _pytest import session as outcome import py import execnet @@ -140,7 +140,7 @@ class TestDistReporter: @py.test.mark.xfail def test_rsync_printing(self, testdir, linecomp): config = testdir.parseconfig() - from pytest.plugin.pytest_terminal import TerminalReporter + from _pytest.pytest_terminal import TerminalReporter rep = TerminalReporter(config, file=linecomp.stringio) config.pluginmanager.register(rep, "terminalreporter") dsession = DSession(config) --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.5a6', + version='1.5a7', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=__doc__, license='GPLv2 or later', From commits-noreply at bitbucket.org Sat Nov 13 11:45:07 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 04:45:07 -0600 (CST) Subject: [py-svn] pytest commit 54e78a7ce498: revert benjamin's change: script could be py.test.exe so we cannot Message-ID: <20101113104507.EA1261E117D@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289645098 -3600 # Node ID 54e78a7ce4980b1cab0ed40ca0e16dbea31394c2 # Parent 755d6ca9b91b6716f55663edee404269d107f1de revert benjamin's change: script could be py.test.exe so we cannot just return "python,script". When was the actual problem occuring? --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -451,7 +451,10 @@ class TmpTestdir: if not self.request.config.getvalue("notoolsonpath"): script = py.path.local.sysfind(scriptname) assert script, "script %r not found" % scriptname - return (py.std.sys.executable, script,) + # XXX we rely on script refering to the correct environment + # we cannot use "(py.std.sys.executable,script)" + # becaue on windows the script is e.g. a py.test.exe + return (script,) else: py.test.skip("cannot run %r with --no-tools-on-path" % scriptname) From commits-noreply at bitbucket.org Sat Nov 13 11:45:07 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 04:45:07 -0600 (CST) Subject: [py-svn] pytest commit 755d6ca9b91b: also un-nest test directory Message-ID: <20101113104507.D526A1E1032@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289644240 -3600 # Node ID 755d6ca9b91b6716f55663edee404269d107f1de # Parent 4e2a0da2c7df834b6789db857af49440a11f98af also un-nest test directory --- /dev/null +++ b/testing/test_pastebin.py @@ -0,0 +1,47 @@ + +class TestPasting: + def pytest_funcarg__pastebinlist(self, request): + mp = request.getfuncargvalue("monkeypatch") + pastebinlist = [] + class MockProxy: + def newPaste(self, language, code): + pastebinlist.append((language, code)) + plugin = request.config.pluginmanager.getplugin('pastebin') + mp.setattr(plugin, 'getproxy', MockProxy) + return pastebinlist + + def test_failed(self, testdir, pastebinlist): + testpath = testdir.makepyfile(""" + import py + def test_pass(): + pass + def test_fail(): + assert 0 + def test_skip(): + py.test.skip("") + """) + reprec = testdir.inline_run(testpath, "--paste=failed") + assert len(pastebinlist) == 1 + assert pastebinlist[0][0] == "python" + s = pastebinlist[0][1] + assert s.find("def test_fail") != -1 + assert reprec.countoutcomes() == [1,1,1] + + def test_all(self, testdir, pastebinlist): + testpath = testdir.makepyfile(""" + import py + def test_pass(): + pass + def test_fail(): + assert 0 + def test_skip(): + py.test.skip("") + """) + reprec = testdir.inline_run(testpath, "--pastebin=all") + assert reprec.countoutcomes() == [1,1,1] + assert len(pastebinlist) == 1 + assert pastebinlist[0][0] == "python" + s = pastebinlist[0][1] + for x in 'test_fail test_skip skipped'.split(): + assert s.find(x), (s, x) + --- /dev/null +++ b/testing/test_terminal.py @@ -0,0 +1,639 @@ +""" +terminal reporting of the full testing process. +""" +import pytest,py +import sys + +from _pytest.terminal import TerminalReporter, \ + CollectonlyReporter, repr_pythonversion, getreportopt +from _pytest import runner + +def basic_run_report(item): + runner.call_and_report(item, "setup", log=False) + return runner.call_and_report(item, "call", log=False) + +class Option: + def __init__(self, verbose=False, fulltrace=False): + self.verbose = verbose + self.fulltrace = fulltrace + + @property + def args(self): + l = [] + if self.verbose: + l.append('-v') + if self.fulltrace: + l.append('--fulltrace') + return l + +def pytest_generate_tests(metafunc): + if "option" in metafunc.funcargnames: + metafunc.addcall(id="default", + funcargs={'option': Option(verbose=False)}) + metafunc.addcall(id="verbose", + funcargs={'option': Option(verbose=True)}) + metafunc.addcall(id="quiet", + funcargs={'option': Option(verbose=-1)}) + metafunc.addcall(id="fulltrace", + funcargs={'option': Option(fulltrace=True)}) + + +class TestTerminal: + def test_pass_skip_fail(self, testdir, option): + p = testdir.makepyfile(""" + import py + def test_ok(): + pass + def test_skip(): + py.test.skip("xx") + def test_func(): + assert 0 + """) + result = testdir.runpytest(*option.args) + if option.verbose: + result.stdout.fnmatch_lines([ + "*test_pass_skip_fail.py:2: *test_ok*PASS*", + "*test_pass_skip_fail.py:4: *test_skip*SKIP*", + "*test_pass_skip_fail.py:6: *test_func*FAIL*", + ]) + else: + result.stdout.fnmatch_lines([ + "*test_pass_skip_fail.py .sF" + ]) + result.stdout.fnmatch_lines([ + " def test_func():", + "> assert 0", + "E assert 0", + ]) + + def test_internalerror(self, testdir, linecomp): + modcol = testdir.getmodulecol("def test_one(): pass") + rep = TerminalReporter(modcol.config, file=linecomp.stringio) + excinfo = py.test.raises(ValueError, "raise ValueError('hello')") + rep.pytest_internalerror(excinfo.getrepr()) + linecomp.assert_contains_lines([ + "INTERNALERROR> *ValueError*hello*" + ]) + + def test_writeline(self, testdir, linecomp): + modcol = testdir.getmodulecol("def test_one(): pass") + stringio = py.io.TextIO() + rep = TerminalReporter(modcol.config, file=linecomp.stringio) + rep.write_fspath_result(py.path.local("xy.py"), '.') + rep.write_line("hello world") + lines = linecomp.stringio.getvalue().split('\n') + assert not lines[0] + assert lines[1].endswith("xy.py .") + assert lines[2] == "hello world" + + def test_show_runtest_logstart(self, testdir, linecomp): + item = testdir.getitem("def test_func(): pass") + tr = TerminalReporter(item.config, file=linecomp.stringio) + item.config.pluginmanager.register(tr) + location = item.reportinfo() + tr.config.hook.pytest_runtest_logstart(nodeid=item.nodeid, + location=location, fspath=str(item.fspath)) + linecomp.assert_contains_lines([ + "*test_show_runtest_logstart.py*" + ]) + + def test_runtest_location_shown_before_test_starts(self, testdir): + p1 = testdir.makepyfile(""" + def test_1(): + import time + time.sleep(20) + """) + child = testdir.spawn_pytest("") + child.expect(".*test_runtest_location.*py") + child.sendeof() + child.kill(15) + + def test_itemreport_subclasses_show_subclassed_file(self, testdir): + p1 = testdir.makepyfile(test_p1=""" + class BaseTests: + def test_p1(self): + pass + class TestClass(BaseTests): + pass + """) + p2 = testdir.makepyfile(test_p2=""" + from test_p1 import BaseTests + class TestMore(BaseTests): + pass + """) + result = testdir.runpytest(p2) + result.stdout.fnmatch_lines([ + "*test_p2.py .", + "*1 passed*", + ]) + result = testdir.runpytest("-v", p2) + result.stdout.fnmatch_lines([ + "*test_p2.py <- *test_p1.py:2: TestMore.test_p1*", + ]) + + def test_keyboard_interrupt(self, testdir, option): + p = testdir.makepyfile(""" + def test_foobar(): + assert 0 + def test_spamegg(): + import py; py.test.skip('skip me please!') + def test_interrupt_me(): + raise KeyboardInterrupt # simulating the user + """) + + result = testdir.runpytest(*option.args) + result.stdout.fnmatch_lines([ + " def test_foobar():", + "> assert 0", + "E assert 0", + "*_keyboard_interrupt.py:6: KeyboardInterrupt*", + ]) + if option.fulltrace: + result.stdout.fnmatch_lines([ + "*raise KeyboardInterrupt # simulating the user*", + ]) + result.stdout.fnmatch_lines(['*KeyboardInterrupt*']) + + + +class TestCollectonly: + def test_collectonly_basic(self, testdir, linecomp): + modcol = testdir.getmodulecol(configargs=['--collectonly'], source=""" + def test_func(): + pass + """) + rep = CollectonlyReporter(modcol.config, out=linecomp.stringio) + modcol.config.pluginmanager.register(rep) + indent = rep.indent + rep.config.hook.pytest_collectstart(collector=modcol) + linecomp.assert_contains_lines([ + "" + ]) + item = modcol.collect()[0] + rep.config.hook.pytest_itemcollected(item=item) + linecomp.assert_contains_lines([ + " ", + ]) + report = rep.config.hook.pytest_make_collect_report(collector=modcol) + rep.config.hook.pytest_collectreport(report=report) + assert rep.indent == indent + + def test_collectonly_skipped_module(self, testdir, linecomp): + modcol = testdir.getmodulecol(configargs=['--collectonly'], source=""" + import py + py.test.skip("nomod") + """) + rep = CollectonlyReporter(modcol.config, out=linecomp.stringio) + modcol.config.pluginmanager.register(rep) + cols = list(testdir.genitems([modcol])) + assert len(cols) == 0 + linecomp.assert_contains_lines(""" + + !!! Skipped: nomod !!! + """) + + def test_collectonly_failed_module(self, testdir, linecomp): + modcol = testdir.getmodulecol(configargs=['--collectonly'], source=""" + raise ValueError(0) + """) + rep = CollectonlyReporter(modcol.config, out=linecomp.stringio) + modcol.config.pluginmanager.register(rep) + cols = list(testdir.genitems([modcol])) + assert len(cols) == 0 + linecomp.assert_contains_lines(""" + + !!! ValueError: 0 !!! + """) + + def test_collectonly_fatal(self, testdir): + p1 = testdir.makeconftest(""" + def pytest_collectstart(collector): + assert 0, "urgs" + """) + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines([ + "*INTERNAL*args*" + ]) + assert result.ret == 3 + + def test_collectonly_simple(self, testdir): + p = testdir.makepyfile(""" + def test_func1(): + pass + class TestClass: + def test_method(self): + pass + """) + result = testdir.runpytest("--collectonly", p) + stderr = result.stderr.str().strip() + #assert stderr.startswith("inserting into sys.path") + assert result.ret == 0 + extra = result.stdout.fnmatch_lines([ + "*", + "* ", + "* ", + "* ", + "* ", + ]) + + def test_collectonly_error(self, testdir): + p = testdir.makepyfile("import Errlkjqweqwe") + result = testdir.runpytest("--collectonly", p) + stderr = result.stderr.str().strip() + assert result.ret == 1 + extra = result.stdout.fnmatch_lines(py.code.Source(""" + * + *ImportError* + *!!!*failures*!!! + *test_collectonly_error.py:1* + """).strip()) + + +def test_repr_python_version(monkeypatch): + monkeypatch.setattr(sys, 'version_info', (2, 5, 1, 'final', 0)) + assert repr_pythonversion() == "2.5.1-final-0" + py.std.sys.version_info = x = (2,3) + assert repr_pythonversion() == str(x) + +class TestFixtureReporting: + def test_setup_fixture_error(self, testdir): + p = testdir.makepyfile(""" + def setup_function(function): + print ("setup func") + assert 0 + def test_nada(): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*ERROR at setup of test_nada*", + "*setup_function(function):*", + "*setup func*", + "*assert 0*", + "*1 error*", + ]) + assert result.ret != 0 + + def test_teardown_fixture_error(self, testdir): + p = testdir.makepyfile(""" + def test_nada(): + pass + def teardown_function(function): + print ("teardown func") + assert 0 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*ERROR at teardown*", + "*teardown_function(function):*", + "*assert 0*", + "*Captured stdout*", + "*teardown func*", + "*1 passed*1 error*", + ]) + + def test_teardown_fixture_error_and_test_failure(self, testdir): + p = testdir.makepyfile(""" + def test_fail(): + assert 0, "failingfunc" + + def teardown_function(function): + print ("teardown func") + assert False + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*ERROR at teardown of test_fail*", + "*teardown_function(function):*", + "*assert False*", + "*Captured stdout*", + "*teardown func*", + + "*test_fail*", + "*def test_fail():", + "*failingfunc*", + "*1 failed*1 error*", + ]) + +class TestTerminalFunctional: + def test_deselected(self, testdir): + testpath = testdir.makepyfile(""" + def test_one(): + pass + def test_two(): + pass + def test_three(): + pass + """ + ) + result = testdir.runpytest("-k", "test_two:", testpath) + result.stdout.fnmatch_lines([ + "*test_deselected.py ..", + "=* 1 test*deselected by 'test_two:'*=", + ]) + assert result.ret == 0 + + def test_no_skip_summary_if_failure(self, testdir): + testdir.makepyfile(""" + import py + def test_ok(): + pass + def test_fail(): + assert 0 + def test_skip(): + py.test.skip("dontshow") + """) + result = testdir.runpytest() + assert result.stdout.str().find("skip test summary") == -1 + assert result.ret == 1 + + def test_passes(self, testdir): + p1 = testdir.makepyfile(""" + def test_passes(): + pass + class TestClass: + def test_method(self): + pass + """) + old = p1.dirpath().chdir() + try: + result = testdir.runpytest() + finally: + old.chdir() + result.stdout.fnmatch_lines([ + "test_passes.py ..", + "* 2 pass*", + ]) + assert result.ret == 0 + + def test_header_trailer_info(self, testdir): + p1 = testdir.makepyfile(""" + def test_passes(): + pass + """) + result = testdir.runpytest() + verinfo = ".".join(map(str, py.std.sys.version_info[:3])) + result.stdout.fnmatch_lines([ + "*===== test session starts ====*", + "platform %s -- Python %s*" %( + py.std.sys.platform, verinfo), # , py.std.sys.executable), + "*test_header_trailer_info.py .", + "=* 1 passed in *.[0-9][0-9] seconds *=", + ]) + + def test_showlocals(self, testdir): + p1 = testdir.makepyfile(""" + def test_showlocals(): + x = 3 + y = "x" * 5000 + assert 0 + """) + result = testdir.runpytest(p1, '-l') + result.stdout.fnmatch_lines([ + #"_ _ * Locals *", + "x* = 3", + "y* = 'xxxxxx*" + ]) + + def test_verbose_reporting(self, testdir, pytestconfig): + p1 = testdir.makepyfile(""" + import py + def test_fail(): + raise ValueError() + def test_pass(): + pass + class TestClass: + def test_skip(self): + py.test.skip("hello") + def test_gen(): + def check(x): + assert x == 1 + yield check, 0 + """) + result = testdir.runpytest(p1, '-v') + result.stdout.fnmatch_lines([ + "*test_verbose_reporting.py:2: test_fail*FAIL*", + "*test_verbose_reporting.py:4: test_pass*PASS*", + "*test_verbose_reporting.py:7: TestClass.test_skip*SKIP*", + "*test_verbose_reporting.py:10: test_gen*FAIL*", + ]) + assert result.ret == 1 + pytestconfig.pluginmanager.skipifmissing("xdist") + result = testdir.runpytest(p1, '-v', '-n 1') + result.stdout.fnmatch_lines([ + "*FAIL*test_verbose_reporting.py:2: test_fail*", + ]) + assert result.ret == 1 + + def test_quiet_reporting(self, testdir): + p1 = testdir.makepyfile("def test_pass(): pass") + result = testdir.runpytest(p1, '-q') + s = result.stdout.str() + assert 'test session starts' not in s + assert p1.basename not in s + assert "===" not in s + +def test_fail_extra_reporting(testdir): + p = testdir.makepyfile("def test_this(): assert 0") + result = testdir.runpytest(p) + assert 'short test summary' not in result.stdout.str() + result = testdir.runpytest(p, '-rf') + result.stdout.fnmatch_lines([ + "*test summary*", + "FAIL*test_fail_extra_reporting*", + ]) + +def test_fail_reporting_on_pass(testdir): + p = testdir.makepyfile("def test_this(): assert 1") + result = testdir.runpytest(p, '-rf') + assert 'short test summary' not in result.stdout.str() + +def test_getreportopt(): + class config: + class option: + reportchars = "" + config.option.report = "xfailed" + assert getreportopt(config) == "x" + + config.option.report = "xfailed,skipped" + assert getreportopt(config) == "xs" + + config.option.report = "skipped,xfailed" + assert getreportopt(config) == "sx" + + config.option.report = "skipped" + config.option.reportchars = "sf" + assert getreportopt(config) == "sf" + + config.option.reportchars = "sfx" + assert getreportopt(config) == "sfx" + +def test_terminalreporter_reportopt_addopts(testdir): + testdir.makeini("[pytest]\naddopts=-rs") + p = testdir.makepyfile(""" + def pytest_funcarg__tr(request): + tr = request.config.pluginmanager.getplugin("terminalreporter") + return tr + def test_opt(tr): + assert tr.hasopt('skipped') + assert not tr.hasopt('qwe') + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + +def test_tbstyle_short(testdir): + p = testdir.makepyfile(""" + def pytest_funcarg__arg(request): + return 42 + def test_opt(arg): + x = 0 + assert x + """) + result = testdir.runpytest("--tb=short") + s = result.stdout.str() + assert 'arg = 42' not in s + assert 'x = 0' not in s + result.stdout.fnmatch_lines([ + "*%s:5*" % p.basename, + ">*assert x", + "E*assert*", + ]) + result = testdir.runpytest() + s = result.stdout.str() + assert 'x = 0' in s + assert 'assert x' in s + +def test_traceconfig(testdir, monkeypatch): + result = testdir.runpytest("--traceconfig") + result.stdout.fnmatch_lines([ + "*active plugins*" + ]) + assert result.ret == 0 + +def test_debug(testdir, monkeypatch): + result = testdir.runpytest("--debug") + result.stderr.fnmatch_lines([ + "*pytest_sessionstart*session*", + ]) + assert result.ret == 0 + +def test_PYTEST_DEBUG(testdir, monkeypatch): + monkeypatch.setenv("PYTEST_DEBUG", "1") + result = testdir.runpytest() + assert result.ret == 0 + result.stderr.fnmatch_lines([ + "*registered*PluginManager*" + ]) + + +class TestGenericReporting: + """ this test class can be subclassed with a different option + provider to run e.g. distributed tests. + """ + def test_collect_fail(self, testdir, option): + p = testdir.makepyfile("import xyz\n") + result = testdir.runpytest(*option.args) + result.stdout.fnmatch_lines([ + "*test_collect_fail.py E*", + "> import xyz", + "E ImportError: No module named xyz", + "*1 error*", + ]) + + def test_maxfailures(self, testdir, option): + p = testdir.makepyfile(""" + def test_1(): + assert 0 + def test_2(): + assert 0 + def test_3(): + assert 0 + """) + result = testdir.runpytest("--maxfail=2", *option.args) + result.stdout.fnmatch_lines([ + "*def test_1():*", + "*def test_2():*", + "*!! Interrupted: stopping after 2 failures*!!*", + "*2 failed*", + ]) + + + def test_tb_option(self, testdir, option): + p = testdir.makepyfile(""" + import py + def g(): + raise IndexError + def test_func(): + print (6*7) + g() # --calling-- + """) + for tbopt in ["long", "short", "no"]: + print('testing --tb=%s...' % tbopt) + result = testdir.runpytest('--tb=%s' % tbopt) + s = result.stdout.str() + if tbopt == "long": + assert 'print (6*7)' in s + else: + assert 'print (6*7)' not in s + if tbopt != "no": + assert '--calling--' in s + assert 'IndexError' in s + else: + assert 'FAILURES' not in s + assert '--calling--' not in s + assert 'IndexError' not in s + + def test_tb_crashline(self, testdir, option): + p = testdir.makepyfile(""" + import py + def g(): + raise IndexError + def test_func1(): + print (6*7) + g() # --calling-- + def test_func2(): + assert 0, "hello" + """) + result = testdir.runpytest("--tb=line") + bn = p.basename + result.stdout.fnmatch_lines([ + "*%s:3: IndexError*" % bn, + "*%s:8: AssertionError: hello*" % bn, + ]) + s = result.stdout.str() + assert "def test_func2" not in s + + def test_pytest_report_header(self, testdir, option): + testdir.makeconftest(""" + def pytest_report_header(config): + return "hello: info" + """) + testdir.mkdir("a").join("conftest.py").write(""" +def pytest_report_header(config): + return ["line1", "line2"]""") + result = testdir.runpytest("a") + result.stdout.fnmatch_lines([ + "line1", + "line2", + "*hello: info*", + ]) + + at py.test.mark.xfail("not hasattr(os, 'dup')") +def test_fdopen_kept_alive_issue124(testdir): + testdir.makepyfile(""" + import os, sys + k = [] + def test_open_file_and_keep_alive(capfd): + stdout = os.fdopen(1, 'w', 1) + k.append(stdout) + + def test_close_kept_alive_file(): + stdout = k.pop() + stdout.close() + """) + result = testdir.runpytest("-s") + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) --- a/testing/plugin/test_doctest.py +++ /dev/null @@ -1,114 +0,0 @@ -from _pytest.doctest import DoctestModule, DoctestTextfile -import py - -pytest_plugins = ["pytest_doctest"] - -class TestDoctests: - - def test_collect_testtextfile(self, testdir): - testdir.maketxtfile(whatever="") - checkfile = testdir.maketxtfile(test_something=""" - alskdjalsdk - >>> i = 5 - >>> i-1 - 4 - """) - for x in (testdir.tmpdir, checkfile): - #print "checking that %s returns custom items" % (x,) - items, reprec = testdir.inline_genitems(x) - assert len(items) == 1 - assert isinstance(items[0], DoctestTextfile) - - def test_collect_module(self, testdir): - path = testdir.makepyfile(whatever="#") - for p in (path, testdir.tmpdir): - items, reprec = testdir.inline_genitems(p, - '--doctest-modules') - assert len(items) == 1 - assert isinstance(items[0], DoctestModule) - - def test_simple_doctestfile(self, testdir): - p = testdir.maketxtfile(test_doc=""" - >>> x = 1 - >>> x == 1 - False - """) - reprec = testdir.inline_run(p, ) - reprec.assertoutcome(failed=1) - - def test_new_pattern(self, testdir): - p = testdir.maketxtfile(xdoc =""" - >>> x = 1 - >>> x == 1 - False - """) - reprec = testdir.inline_run(p, "--doctest-glob=x*.txt") - reprec.assertoutcome(failed=1) - - def test_doctest_unexpected_exception(self, testdir): - p = testdir.maketxtfile(""" - >>> i = 0 - >>> i = 1 - >>> x - 2 - """) - reprec = testdir.inline_run(p) - call = reprec.getcall("pytest_runtest_logreport") - assert call.report.failed - assert call.report.longrepr - # XXX - #testitem, = items - #excinfo = py.test.raises(Failed, "testitem.runtest()") - #repr = testitem.repr_failure(excinfo, ("", "")) - #assert repr.reprlocation - - def test_doctestmodule(self, testdir): - p = testdir.makepyfile(""" - ''' - >>> x = 1 - >>> x == 1 - False - - ''' - """) - reprec = testdir.inline_run(p, "--doctest-modules") - reprec.assertoutcome(failed=1) - - def test_doctestmodule_external_and_issue116(self, testdir): - p = testdir.mkpydir("hello") - p.join("__init__.py").write(py.code.Source(""" - def somefunc(): - ''' - >>> i = 0 - >>> i + 1 - 2 - ''' - """)) - result = testdir.runpytest(p, "--doctest-modules") - result.stdout.fnmatch_lines([ - '004 *>>> i = 0', - '005 *>>> i + 1', - '*Expected:', - "* 2", - "*Got:", - "* 1", - "*:5: DocTestFailure" - ]) - - - def test_txtfile_failing(self, testdir): - p = testdir.maketxtfile(""" - >>> i = 0 - >>> i + 1 - 2 - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - '001 >>> i = 0', - '002 >>> i + 1', - 'Expected:', - " 2", - "Got:", - " 1", - "*test_txtfile_failing.txt:2: DocTestFailure" - ]) --- a/testing/plugin/test_unittest.py +++ /dev/null @@ -1,105 +0,0 @@ -import py - -def test_simple_unittest(testdir): - testpath = testdir.makepyfile(""" - import unittest - pytest_plugins = "pytest_unittest" - class MyTestCase(unittest.TestCase): - def testpassing(self): - self.assertEquals('foo', 'foo') - def test_failing(self): - self.assertEquals('foo', 'bar') - """) - reprec = testdir.inline_run(testpath) - assert reprec.matchreport("testpassing").passed - assert reprec.matchreport("test_failing").failed - -def test_isclasscheck_issue53(testdir): - testpath = testdir.makepyfile(""" - import unittest - class _E(object): - def __getattr__(self, tag): - pass - E = _E() - """) - result = testdir.runpytest(testpath) - assert result.ret == 0 - -def test_setup(testdir): - testpath = testdir.makepyfile(test_two=""" - import unittest - class MyTestCase(unittest.TestCase): - def setUp(self): - self.foo = 1 - def test_setUp(self): - self.assertEquals(1, self.foo) - """) - reprec = testdir.inline_run(testpath) - rep = reprec.matchreport("test_setUp") - assert rep.passed - -def test_new_instances(testdir): - testpath = testdir.makepyfile(""" - import unittest - class MyTestCase(unittest.TestCase): - def test_func1(self): - self.x = 2 - def test_func2(self): - assert not hasattr(self, 'x') - """) - reprec = testdir.inline_run(testpath) - reprec.assertoutcome(passed=2) - -def test_teardown(testdir): - testpath = testdir.makepyfile(""" - import unittest - pytest_plugins = "pytest_unittest" # XXX - class MyTestCase(unittest.TestCase): - l = [] - def test_one(self): - pass - def tearDown(self): - self.l.append(None) - class Second(unittest.TestCase): - def test_check(self): - self.assertEquals(MyTestCase.l, [None]) - """) - reprec = testdir.inline_run(testpath) - passed, skipped, failed = reprec.countoutcomes() - assert failed == 0, failed - assert passed == 2 - assert passed + skipped + failed == 2 - -def test_module_level_pytestmark(testdir): - testpath = testdir.makepyfile(""" - import unittest - import py - pytestmark = py.test.mark.xfail - class MyTestCase(unittest.TestCase): - def test_func1(self): - assert 0 - """) - reprec = testdir.inline_run(testpath, "-s") - reprec.assertoutcome(skipped=1) - -def test_class_setup(testdir): - testpath = testdir.makepyfile(""" - import unittest - import py - class MyTestCase(unittest.TestCase): - x = 0 - @classmethod - def setUpClass(cls): - cls.x += 1 - def test_func1(self): - assert self.x == 1 - def test_func2(self): - assert self.x == 1 - @classmethod - def tearDownClass(cls): - cls.x -= 1 - def test_teareddown(): - assert MyTestCase.x == 0 - """) - reprec = testdir.inline_run(testpath) - reprec.assertoutcome(passed=3) --- a/testing/plugin/test_pdb.py +++ /dev/null @@ -1,145 +0,0 @@ -import py -import sys - -class TestPDB: - def pytest_funcarg__pdblist(self, request): - monkeypatch = request.getfuncargvalue("monkeypatch") - pdblist = [] - def mypdb(*args): - pdblist.append(args) - plugin = request.config.pluginmanager.getplugin('pdb') - monkeypatch.setattr(plugin, 'post_mortem', mypdb) - return pdblist - - def test_pdb_on_fail(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ - def test_func(): - assert 0 - """) - assert rep.failed - assert len(pdblist) == 1 - tb = py.code.Traceback(pdblist[0][0]) - assert tb[-1].name == "test_func" - - def test_pdb_on_xfail(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ - import py - @py.test.mark.xfail - def test_func(): - assert 0 - """) - assert "xfail" in rep.keywords - assert not pdblist - - def test_pdb_on_skip(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ - import py - def test_func(): - py.test.skip("hello") - """) - assert rep.skipped - assert len(pdblist) == 0 - - def test_pdb_on_BdbQuit(self, testdir, pdblist): - rep = testdir.inline_runsource1('--pdb', """ - import py, bdb - def test_func(): - raise bdb.BdbQuit - """) - assert rep.failed - assert len(pdblist) == 0 - - def test_pdb_interaction(self, testdir): - p1 = testdir.makepyfile(""" - def test_1(): - i = 0 - assert i == 1 - """) - child = testdir.spawn_pytest("--pdb %s" % p1) - child.expect(".*def test_1") - child.expect(".*i = 0") - child.expect("(Pdb)") - child.sendeof() - rest = child.read() - assert "1 failed" in rest - assert "def test_1" not in rest - if child.isalive(): - child.wait() - - def test_pdb_interaction_exception(self, testdir): - p1 = testdir.makepyfile(""" - import py - def globalfunc(): - pass - def test_1(): - py.test.raises(ValueError, globalfunc) - """) - child = testdir.spawn_pytest("--pdb %s" % p1) - child.expect(".*def test_1") - child.expect(".*py.test.raises.*globalfunc") - child.expect("(Pdb)") - child.sendline("globalfunc") - child.expect(".*function") - child.sendeof() - child.expect("1 failed") - if child.isalive(): - child.wait() - - def test_pdb_interaction_capturing_simple(self, testdir): - p1 = testdir.makepyfile(""" - import py - def test_1(): - i = 0 - print ("hello17") - py.test.set_trace() - x = 3 - """) - child = testdir.spawn_pytest(str(p1)) - child.expect("test_1") - child.expect("x = 3") - child.expect("(Pdb)") - child.sendeof() - rest = child.read() - assert "1 failed" in rest - assert "def test_1" in rest - assert "hello17" in rest # out is captured - if child.isalive(): - child.wait() - - def test_pdb_interaction_capturing_twice(self, testdir): - p1 = testdir.makepyfile(""" - import py - def test_1(): - i = 0 - print ("hello17") - py.test.set_trace() - x = 3 - print ("hello18") - py.test.set_trace() - x = 4 - """) - child = testdir.spawn_pytest(str(p1)) - child.expect("test_1") - child.expect("x = 3") - child.expect("(Pdb)") - child.sendline('c') - child.expect("x = 4") - child.sendeof() - rest = child.read() - assert "1 failed" in rest - assert "def test_1" in rest - assert "hello17" in rest # out is captured - assert "hello18" in rest # out is captured - if child.isalive(): - child.wait() - - def test_pdb_used_outside_test(self, testdir): - p1 = testdir.makepyfile(""" - import py - py.test.set_trace() - x = 5 - """) - child = testdir.spawn("%s %s" %(sys.executable, p1)) - child.expect("x = 5") - child.sendeof() - child.wait() --- /dev/null +++ b/testing/test_helpconfig.py @@ -0,0 +1,53 @@ +import py, pytest,os +from _pytest.helpconfig import collectattr + +def test_version(testdir): + result = testdir.runpytest("--version") + assert result.ret == 0 + #p = py.path.local(py.__file__).dirpath() + result.stderr.fnmatch_lines([ + '*py.test*%s*imported from*' % (pytest.__version__, ) + ]) + +def test_help(testdir): + result = testdir.runpytest("--help") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*-v*verbose*", + "*setup.cfg*", + "*minversion*", + ]) + +def test_collectattr(): + class A: + def pytest_hello(self): + pass + class B(A): + def pytest_world(self): + pass + methods = py.builtin.sorted(collectattr(B)) + assert list(methods) == ['pytest_hello', 'pytest_world'] + methods = py.builtin.sorted(collectattr(B())) + assert list(methods) == ['pytest_hello', 'pytest_world'] + +def test_hookvalidation_unknown(testdir): + testdir.makeconftest(""" + def pytest_hello(xyz): + pass + """) + result = testdir.runpytest() + assert result.ret != 0 + result.stderr.fnmatch_lines([ + '*unknown hook*pytest_hello*' + ]) + +def test_hookvalidation_optional(testdir): + testdir.makeconftest(""" + import py + @py.test.mark.optionalhook + def pytest_hello(xyz): + pass + """) + result = testdir.runpytest() + assert result.ret == 0 + --- a/testing/plugin/test_capture.py +++ /dev/null @@ -1,379 +0,0 @@ -import py, os, sys -from _pytest.capture import CaptureManager - -needsosdup = py.test.mark.xfail("not hasattr(os, 'dup')") - -class TestCaptureManager: - def test_getmethod_default_no_fd(self, testdir, monkeypatch): - config = testdir.parseconfig(testdir.tmpdir) - assert config.getvalue("capture") is None - capman = CaptureManager() - monkeypatch.delattr(os, 'dup', raising=False) - try: - assert capman._getmethod(config, None) == "sys" - finally: - monkeypatch.undo() - - def test_configure_per_fspath(self, testdir): - config = testdir.parseconfig(testdir.tmpdir) - assert config.getvalue("capture") is None - capman = CaptureManager() - hasfd = hasattr(os, 'dup') - if hasfd: - assert capman._getmethod(config, None) == "fd" - else: - assert capman._getmethod(config, None) == "sys" - - for name in ('no', 'fd', 'sys'): - if not hasfd and name == 'fd': - continue - sub = testdir.tmpdir.mkdir("dir" + name) - sub.ensure("__init__.py") - sub.join("conftest.py").write('option_capture = %r' % name) - assert capman._getmethod(config, sub.join("test_hello.py")) == name - - @needsosdup - @py.test.mark.multi(method=['no', 'fd', 'sys']) - def test_capturing_basic_api(self, method): - capouter = py.io.StdCaptureFD() - old = sys.stdout, sys.stderr, sys.stdin - try: - capman = CaptureManager() - # call suspend without resume or start - outerr = capman.suspendcapture() - outerr = capman.suspendcapture() - assert outerr == ("", "") - capman.resumecapture(method) - print ("hello") - out, err = capman.suspendcapture() - if method == "no": - assert old == (sys.stdout, sys.stderr, sys.stdin) - else: - assert out == "hello\n" - capman.resumecapture(method) - out, err = capman.suspendcapture() - assert not out and not err - finally: - capouter.reset() - - @needsosdup - def test_juggle_capturings(self, testdir): - capouter = py.io.StdCaptureFD() - try: - config = testdir.parseconfig(testdir.tmpdir) - capman = CaptureManager() - capman.resumecapture("fd") - py.test.raises(ValueError, 'capman.resumecapture("fd")') - py.test.raises(ValueError, 'capman.resumecapture("sys")') - os.write(1, "hello\n".encode('ascii')) - out, err = capman.suspendcapture() - assert out == "hello\n" - capman.resumecapture("sys") - os.write(1, "hello\n".encode('ascii')) - py.builtin.print_("world", file=sys.stderr) - out, err = capman.suspendcapture() - assert not out - assert err == "world\n" - finally: - capouter.reset() - - at py.test.mark.multi(method=['fd', 'sys']) -def test_capturing_unicode(testdir, method): - if sys.version_info >= (3,0): - obj = "'b\u00f6y'" - else: - obj = "u'\u00f6y'" - testdir.makepyfile(""" - # coding=utf8 - # taken from issue 227 from nosetests - def test_unicode(): - import sys - print (sys.stdout) - print (%s) - """ % obj) - result = testdir.runpytest("--capture=%s" % method) - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) - - at py.test.mark.multi(method=['fd', 'sys']) -def test_capturing_bytes_in_utf8_encoding(testdir, method): - testdir.makepyfile(""" - def test_unicode(): - print ('b\\u00f6y') - """) - result = testdir.runpytest("--capture=%s" % method) - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) - -def test_collect_capturing(testdir): - p = testdir.makepyfile(""" - print ("collect %s failure" % 13) - import xyz42123 - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*Captured stdout*", - "*collect 13 failure*", - ]) - -class TestPerTestCapturing: - def test_capture_and_fixtures(self, testdir): - p = testdir.makepyfile(""" - def setup_module(mod): - print ("setup module") - def setup_function(function): - print ("setup " + function.__name__) - def test_func1(): - print ("in func1") - assert 0 - def test_func2(): - print ("in func2") - assert 0 - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "setup module*", - "setup test_func1*", - "in func1*", - "setup test_func2*", - "in func2*", - ]) - - @py.test.mark.xfail - def test_capture_scope_cache(self, testdir): - p = testdir.makepyfile(""" - import sys - def setup_module(func): - print ("module-setup") - def setup_function(func): - print ("function-setup") - def test_func(): - print ("in function") - assert 0 - def teardown_function(func): - print ("in teardown") - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*test_func():*", - "*Captured stdout during setup*", - "module-setup*", - "function-setup*", - "*Captured stdout*", - "in teardown*", - ]) - - - def test_no_carry_over(self, testdir): - p = testdir.makepyfile(""" - def test_func1(): - print ("in func1") - def test_func2(): - print ("in func2") - assert 0 - """) - result = testdir.runpytest(p) - s = result.stdout.str() - assert "in func1" not in s - assert "in func2" in s - - - def test_teardown_capturing(self, testdir): - p = testdir.makepyfile(""" - def setup_function(function): - print ("setup func1") - def teardown_function(function): - print ("teardown func1") - assert 0 - def test_func1(): - print ("in func1") - pass - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - '*teardown_function*', - '*Captured stdout*', - "setup func1*", - "in func1*", - "teardown func1*", - #"*1 fixture failure*" - ]) - - def test_teardown_final_capturing(self, testdir): - p = testdir.makepyfile(""" - def teardown_module(mod): - print ("teardown module") - assert 0 - def test_func(): - pass - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*def teardown_module(mod):*", - "*Captured stdout*", - "*teardown module*", - "*1 error*", - ]) - - def test_capturing_outerr(self, testdir): - p1 = testdir.makepyfile(""" - import sys - def test_capturing(): - print (42) - sys.stderr.write(str(23)) - def test_capturing_error(): - print (1) - sys.stderr.write(str(2)) - raise ValueError - """) - result = testdir.runpytest(p1) - result.stdout.fnmatch_lines([ - "*test_capturing_outerr.py .F", - "====* FAILURES *====", - "____*____", - "*test_capturing_outerr.py:8: ValueError", - "*--- Captured stdout ---*", - "1", - "*--- Captured stderr ---*", - "2", - ]) - -class TestLoggingInteraction: - def test_logging_stream_ownership(self, testdir): - p = testdir.makepyfile(""" - def test_logging(): - import logging - import py - stream = py.io.TextIO() - logging.basicConfig(stream=stream) - stream.close() # to free memory/release resources - """) - result = testdir.runpytest(p) - result.stderr.str().find("atexit") == -1 - - def test_logging_and_immediate_setupteardown(self, testdir): - p = testdir.makepyfile(""" - import logging - def setup_function(function): - logging.warn("hello1") - - def test_logging(): - logging.warn("hello2") - assert 0 - - def teardown_function(function): - logging.warn("hello3") - assert 0 - """) - for optargs in (('--capture=sys',), ('--capture=fd',)): - print (optargs) - result = testdir.runpytest(p, *optargs) - s = result.stdout.str() - result.stdout.fnmatch_lines([ - "*WARN*hello3", # errors show first! - "*WARN*hello1", - "*WARN*hello2", - ]) - # verify proper termination - assert "closed" not in s - - def test_logging_and_crossscope_fixtures(self, testdir): - p = testdir.makepyfile(""" - import logging - def setup_module(function): - logging.warn("hello1") - - def test_logging(): - logging.warn("hello2") - assert 0 - - def teardown_module(function): - logging.warn("hello3") - assert 0 - """) - for optargs in (('--capture=sys',), ('--capture=fd',)): - print (optargs) - result = testdir.runpytest(p, *optargs) - s = result.stdout.str() - result.stdout.fnmatch_lines([ - "*WARN*hello3", # errors come first - "*WARN*hello1", - "*WARN*hello2", - ]) - # verify proper termination - assert "closed" not in s - -class TestCaptureFuncarg: - def test_std_functional(self, testdir): - reprec = testdir.inline_runsource(""" - def test_hello(capsys): - print (42) - out, err = capsys.readouterr() - assert out.startswith("42") - """) - reprec.assertoutcome(passed=1) - - @needsosdup - def test_stdfd_functional(self, testdir): - reprec = testdir.inline_runsource(""" - def test_hello(capfd): - import os - os.write(1, "42".encode('ascii')) - out, err = capfd.readouterr() - assert out.startswith("42") - capfd.close() - """) - reprec.assertoutcome(passed=1) - - def test_partial_setup_failure(self, testdir): - p = testdir.makepyfile(""" - def test_hello(capsys, missingarg): - pass - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*test_partial_setup_failure*", - "*1 error*", - ]) - - @needsosdup - def test_keyboardinterrupt_disables_capturing(self, testdir): - p = testdir.makepyfile(""" - def test_hello(capfd): - import os - os.write(1, str(42).encode('ascii')) - raise KeyboardInterrupt() - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*KeyboardInterrupt*" - ]) - assert result.ret == 2 - -def test_setup_failure_does_not_kill_capturing(testdir): - sub1 = testdir.mkpydir("sub1") - sub1.join("conftest.py").write(py.code.Source(""" - def pytest_runtest_setup(item): - raise ValueError(42) - """)) - sub1.join("test_mod.py").write("def test_func1(): pass") - result = testdir.runpytest(testdir.tmpdir, '--traceconfig') - result.stdout.fnmatch_lines([ - "*ValueError(42)*", - "*1 error*" - ]) - -def test_fdfuncarg_skips_on_no_osdup(testdir): - testdir.makepyfile(""" - import os - if hasattr(os, 'dup'): - del os.dup - def test_hello(capfd): - pass - """) - result = testdir.runpytest("--capture=no") - result.stdout.fnmatch_lines([ - "*1 skipped*" - ]) --- /dev/null +++ b/testing/test_resultlog.py @@ -0,0 +1,193 @@ +import py +import os +from _pytest.resultlog import generic_path, ResultLog, \ + pytest_configure, pytest_unconfigure +from _pytest.session import Node, Item, FSCollector + +def test_generic_path(testdir): + from _pytest.session import Session + config = testdir.parseconfig() + session = Session(config) + p1 = Node('a', config=config, session=session) + #assert p1.fspath is None + p2 = Node('B', parent=p1) + p3 = Node('()', parent = p2) + item = Item('c', parent = p3) + + res = generic_path(item) + assert res == 'a.B().c' + + p0 = FSCollector('proj/test', config=config, session=session) + p1 = FSCollector('proj/test/a', parent=p0) + p2 = Node('B', parent=p1) + p3 = Node('()', parent = p2) + p4 = Node('c', parent=p3) + item = Item('[1]', parent = p4) + + res = generic_path(item) + assert res == 'test/a:B().c[1]' + +def test_write_log_entry(): + reslog = ResultLog(None, None) + reslog.logfile = py.io.TextIO() + reslog.write_log_entry('name', '.', '') + entry = reslog.logfile.getvalue() + assert entry[-1] == '\n' + entry_lines = entry.splitlines() + assert len(entry_lines) == 1 + assert entry_lines[0] == '. name' + + reslog.logfile = py.io.TextIO() + reslog.write_log_entry('name', 's', 'Skipped') + entry = reslog.logfile.getvalue() + assert entry[-1] == '\n' + entry_lines = entry.splitlines() + assert len(entry_lines) == 2 + assert entry_lines[0] == 's name' + assert entry_lines[1] == ' Skipped' + + reslog.logfile = py.io.TextIO() + reslog.write_log_entry('name', 's', 'Skipped\n') + entry = reslog.logfile.getvalue() + assert entry[-1] == '\n' + entry_lines = entry.splitlines() + assert len(entry_lines) == 2 + assert entry_lines[0] == 's name' + assert entry_lines[1] == ' Skipped' + + reslog.logfile = py.io.TextIO() + longrepr = ' tb1\n tb 2\nE tb3\nSome Error' + reslog.write_log_entry('name', 'F', longrepr) + entry = reslog.logfile.getvalue() + assert entry[-1] == '\n' + entry_lines = entry.splitlines() + assert len(entry_lines) == 5 + assert entry_lines[0] == 'F name' + assert entry_lines[1:] == [' '+line for line in longrepr.splitlines()] + + +class TestWithFunctionIntegration: + # XXX (hpk) i think that the resultlog plugin should + # provide a Parser object so that one can remain + # ignorant regarding formatting details. + def getresultlog(self, testdir, arg): + resultlog = testdir.tmpdir.join("resultlog") + testdir.plugins.append("resultlog") + args = ["--resultlog=%s" % resultlog] + [arg] + testdir.runpytest(*args) + return [x for x in resultlog.readlines(cr=0) if x] + + def test_collection_report(self, testdir): + ok = testdir.makepyfile(test_collection_ok="") + skip = testdir.makepyfile(test_collection_skip="import py ; py.test.skip('hello')") + fail = testdir.makepyfile(test_collection_fail="XXX") + lines = self.getresultlog(testdir, ok) + assert not lines + + lines = self.getresultlog(testdir, skip) + assert len(lines) == 2 + assert lines[0].startswith("S ") + assert lines[0].endswith("test_collection_skip.py") + assert lines[1].startswith(" ") + assert lines[1].endswith("test_collection_skip.py:1: Skipped: hello") + + lines = self.getresultlog(testdir, fail) + assert lines + assert lines[0].startswith("F ") + assert lines[0].endswith("test_collection_fail.py"), lines[0] + for x in lines[1:]: + assert x.startswith(" ") + assert "XXX" in "".join(lines[1:]) + + def test_log_test_outcomes(self, testdir): + mod = testdir.makepyfile(test_mod=""" + import py + def test_pass(): pass + def test_skip(): py.test.skip("hello") + def test_fail(): raise ValueError("FAIL") + + @py.test.mark.xfail + def test_xfail(): raise ValueError("XFAIL") + @py.test.mark.xfail + def test_xpass(): pass + + """) + lines = self.getresultlog(testdir, mod) + assert len(lines) >= 3 + assert lines[0].startswith(". ") + assert lines[0].endswith("test_pass") + assert lines[1].startswith("s "), lines[1] + assert lines[1].endswith("test_skip") + assert lines[2].find("hello") != -1 + + assert lines[3].startswith("F ") + assert lines[3].endswith("test_fail") + tb = "".join(lines[4:8]) + assert tb.find('raise ValueError("FAIL")') != -1 + + assert lines[8].startswith('x ') + tb = "".join(lines[8:14]) + assert tb.find('raise ValueError("XFAIL")') != -1 + + assert lines[14].startswith('X ') + assert len(lines) == 15 + + def test_internal_exception(self): + # they are produced for example by a teardown failing + # at the end of the run + try: + raise ValueError + except ValueError: + excinfo = py.code.ExceptionInfo() + reslog = ResultLog(None, py.io.TextIO()) + reslog.pytest_internalerror(excinfo.getrepr()) + entry = reslog.logfile.getvalue() + entry_lines = entry.splitlines() + + assert entry_lines[0].startswith('! ') + assert os.path.basename(__file__)[:-9] in entry_lines[0] #.pyc/class + assert entry_lines[-1][0] == ' ' + assert 'ValueError' in entry + +def test_generic(testdir, LineMatcher): + testdir.plugins.append("resultlog") + testdir.makepyfile(""" + import py + def test_pass(): + pass + def test_fail(): + assert 0 + def test_skip(): + py.test.skip("") + @py.test.mark.xfail + def test_xfail(): + assert 0 + @py.test.mark.xfail(run=False) + def test_xfail_norun(): + assert 0 + """) + testdir.runpytest("--resultlog=result.log") + lines = testdir.tmpdir.join("result.log").readlines(cr=0) + LineMatcher(lines).fnmatch_lines([ + ". *:test_pass", + "F *:test_fail", + "s *:test_skip", + "x *:test_xfail", + "x *:test_xfail_norun", + ]) + +def test_no_resultlog_on_slaves(testdir): + config = testdir.parseconfig("-p", "resultlog", "--resultlog=resultlog") + + assert not hasattr(config, '_resultlog') + pytest_configure(config) + assert hasattr(config, '_resultlog') + pytest_unconfigure(config) + assert not hasattr(config, '_resultlog') + + config.slaveinput = {} + pytest_configure(config) + assert not hasattr(config, '_resultlog') + pytest_unconfigure(config) + assert not hasattr(config, '_resultlog') + --- a/testing/plugin/test_runner.py +++ /dev/null @@ -1,387 +0,0 @@ -import py, sys -from _pytest import runner -from py._code.code import ReprExceptionInfo - -class TestSetupState: - def test_setup(self, testdir): - ss = runner.SetupState() - item = testdir.getitem("def test_func(): pass") - l = [1] - ss.prepare(item) - ss.addfinalizer(l.pop, colitem=item) - assert l - ss._pop_and_teardown() - assert not l - - def test_setup_scope_None(self, testdir): - item = testdir.getitem("def test_func(): pass") - ss = runner.SetupState() - l = [1] - ss.prepare(item) - ss.addfinalizer(l.pop, colitem=None) - assert l - ss._pop_and_teardown() - assert l - ss._pop_and_teardown() - assert l - ss.teardown_all() - assert not l - - def test_teardown_exact_stack_empty(self, testdir): - item = testdir.getitem("def test_func(): pass") - ss = runner.SetupState() - ss.teardown_exact(item) - ss.teardown_exact(item) - ss.teardown_exact(item) - - def test_setup_fails_and_failure_is_cached(self, testdir): - item = testdir.getitem(""" - def setup_module(mod): - raise ValueError(42) - def test_func(): pass - """) - ss = runner.SetupState() - py.test.raises(ValueError, "ss.prepare(item)") - py.test.raises(ValueError, "ss.prepare(item)") - -class BaseFunctionalTests: - def test_passfunction(self, testdir): - reports = testdir.runitem(""" - def test_func(): - pass - """) - rep = reports[1] - assert rep.passed - assert not rep.failed - assert rep.outcome == "passed" - assert not rep.longrepr - - def test_failfunction(self, testdir): - reports = testdir.runitem(""" - def test_func(): - assert 0 - """) - rep = reports[1] - assert not rep.passed - assert not rep.skipped - assert rep.failed - assert rep.when == "call" - assert rep.outcome == "failed" - #assert isinstance(rep.longrepr, ReprExceptionInfo) - - def test_skipfunction(self, testdir): - reports = testdir.runitem(""" - import py - def test_func(): - py.test.skip("hello") - """) - rep = reports[1] - assert not rep.failed - assert not rep.passed - assert rep.skipped - assert rep.outcome == "skipped" - #assert rep.skipped.when == "call" - #assert rep.skipped.when == "call" - #assert rep.skipped == "%sreason == "hello" - #assert rep.skipped.location.lineno == 3 - #assert rep.skipped.location.path - #assert not rep.skipped.failurerepr - - def test_skip_in_setup_function(self, testdir): - reports = testdir.runitem(""" - import py - def setup_function(func): - py.test.skip("hello") - def test_func(): - pass - """) - print(reports) - rep = reports[0] - assert not rep.failed - assert not rep.passed - assert rep.skipped - #assert rep.skipped.reason == "hello" - #assert rep.skipped.location.lineno == 3 - #assert rep.skipped.location.lineno == 3 - assert len(reports) == 2 - assert reports[1].passed # teardown - - def test_failure_in_setup_function(self, testdir): - reports = testdir.runitem(""" - import py - def setup_function(func): - raise ValueError(42) - def test_func(): - pass - """) - rep = reports[0] - assert not rep.skipped - assert not rep.passed - assert rep.failed - assert rep.when == "setup" - assert len(reports) == 2 - - def test_failure_in_teardown_function(self, testdir): - reports = testdir.runitem(""" - import py - def teardown_function(func): - raise ValueError(42) - def test_func(): - pass - """) - print(reports) - assert len(reports) == 3 - rep = reports[2] - assert not rep.skipped - assert not rep.passed - assert rep.failed - assert rep.when == "teardown" - #assert rep.longrepr.reprcrash.lineno == 3 - #assert rep.longrepr.reprtraceback.reprentries - - def test_custom_failure_repr(self, testdir): - testdir.makepyfile(conftest=""" - import pytest - class Function(pytest.Function): - def repr_failure(self, excinfo): - return "hello" - """) - reports = testdir.runitem(""" - import py - def test_func(): - assert 0 - """) - rep = reports[1] - assert not rep.skipped - assert not rep.passed - assert rep.failed - #assert rep.outcome.when == "call" - #assert rep.failed.where.lineno == 3 - #assert rep.failed.where.path.basename == "test_func.py" - #assert rep.failed.failurerepr == "hello" - - def test_failure_in_setup_function_ignores_custom_repr(self, testdir): - testdir.makepyfile(conftest=""" - import pytest - class Function(pytest.Function): - def repr_failure(self, excinfo): - assert 0 - """) - reports = testdir.runitem(""" - def setup_function(func): - raise ValueError(42) - def test_func(): - pass - """) - assert len(reports) == 2 - rep = reports[0] - print(rep) - assert not rep.skipped - assert not rep.passed - assert rep.failed - #assert rep.outcome.when == "setup" - #assert rep.outcome.where.lineno == 3 - #assert rep.outcome.where.path.basename == "test_func.py" - #assert instanace(rep.failed.failurerepr, PythonFailureRepr) - - def test_systemexit_does_not_bail_out(self, testdir): - try: - reports = testdir.runitem(""" - def test_func(): - raise SystemExit(42) - """) - except SystemExit: - py.test.fail("runner did not catch SystemExit") - rep = reports[1] - assert rep.failed - assert rep.when == "call" - - def test_exit_propagates(self, testdir): - try: - testdir.runitem(""" - import pytest - def test_func(): - raise pytest.exit.Exception() - """) - except py.test.exit.Exception: - pass - else: - py.test.fail("did not raise") - -class TestExecutionNonForked(BaseFunctionalTests): - def getrunner(self): - def f(item): - return runner.runtestprotocol(item, log=False) - return f - - def test_keyboardinterrupt_propagates(self, testdir): - try: - testdir.runitem(""" - def test_func(): - raise KeyboardInterrupt("fake") - """) - except KeyboardInterrupt: - pass - else: - py.test.fail("did not raise") - -class TestExecutionForked(BaseFunctionalTests): - pytestmark = py.test.mark.skipif("not hasattr(os, 'fork')") - - def getrunner(self): - # XXX re-arrange this test to live in pytest-xdist - xplugin = py.test.importorskip("xdist.plugin") - return xplugin.forked_run_report - - def test_suicide(self, testdir): - reports = testdir.runitem(""" - def test_func(): - import os - os.kill(os.getpid(), 15) - """) - rep = reports[0] - assert rep.failed - assert rep.when == "???" - -class TestSessionReports: - def test_collect_result(self, testdir): - col = testdir.getmodulecol(""" - def test_func1(): - pass - class TestClass: - pass - """) - rep = runner.pytest_make_collect_report(col) - assert not rep.failed - assert not rep.skipped - assert rep.passed - locinfo = rep.location - assert locinfo[0] == col.fspath.basename - assert not locinfo[1] - assert locinfo[2] == col.fspath.basename - res = rep.result - assert len(res) == 2 - assert res[0].name == "test_func1" - assert res[1].name == "TestClass" - - def test_skip_at_module_scope(self, testdir): - col = testdir.getmodulecol(""" - import pytest - pytest.skip("hello") - def test_func(): - pass - """) - rep = runner.pytest_make_collect_report(col) - assert not rep.failed - assert not rep.passed - assert rep.skipped - -def test_callinfo(): - ci = runner.CallInfo(lambda: 0, '123') - assert ci.when == "123" - assert ci.result == 0 - assert "result" in repr(ci) - ci = runner.CallInfo(lambda: 0/0, '123') - assert ci.when == "123" - assert not hasattr(ci, 'result') - assert ci.excinfo - assert "exc" in repr(ci) - -# design question: do we want general hooks in python files? -# then something like the following functional tests makes sense - at py.test.mark.xfail -def test_runtest_in_module_ordering(testdir): - p1 = testdir.makepyfile(""" - def pytest_runtest_setup(item): # runs after class-level! - item.function.mylist.append("module") - class TestClass: - def pytest_runtest_setup(self, item): - assert not hasattr(item.function, 'mylist') - item.function.mylist = ['class'] - def pytest_funcarg__mylist(self, request): - return request.function.mylist - def pytest_runtest_call(self, item, __multicall__): - try: - __multicall__.execute() - except ValueError: - pass - def test_hello1(self, mylist): - assert mylist == ['class', 'module'], mylist - raise ValueError() - def test_hello2(self, mylist): - assert mylist == ['class', 'module'], mylist - def pytest_runtest_teardown(item): - del item.function.mylist - """) - result = testdir.runpytest(p1) - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) - - -def test_pytest_exit(): - try: - py.test.exit("hello") - except py.test.exit.Exception: - excinfo = py.code.ExceptionInfo() - assert excinfo.errisinstance(KeyboardInterrupt) - -def test_pytest_fail(): - try: - py.test.fail("hello") - except py.test.fail.Exception: - excinfo = py.code.ExceptionInfo() - s = excinfo.exconly(tryshort=True) - assert s.startswith("Failed") - -def test_exception_printing_skip(): - try: - py.test.skip("hello") - except py.test.skip.Exception: - excinfo = py.code.ExceptionInfo() - s = excinfo.exconly(tryshort=True) - assert s.startswith("Skipped") - -def test_importorskip(): - importorskip = py.test.importorskip - try: - sys = importorskip("sys") - assert sys == py.std.sys - #path = py.test.importorskip("os.path") - #assert path == py.std.os.path - py.test.raises(py.test.skip.Exception, - "py.test.importorskip('alskdj')") - py.test.raises(SyntaxError, "py.test.importorskip('x y z')") - py.test.raises(SyntaxError, "py.test.importorskip('x=y')") - path = importorskip("py", minversion=".".join(py.__version__)) - mod = py.std.types.ModuleType("hello123") - mod.__version__ = "1.3" - py.test.raises(py.test.skip.Exception, """ - py.test.importorskip("hello123", minversion="5.0") - """) - except py.test.skip.Exception: - print(py.code.ExceptionInfo()) - py.test.fail("spurious skip") - -def test_importorskip_imports_last_module_part(): - import os - ospath = py.test.importorskip("os.path") - assert os.path == ospath - - -def test_pytest_cmdline_main(testdir): - p = testdir.makepyfile(""" - import sys - sys.path.insert(0, %r) - import py - def test_hello(): - assert 1 - if __name__ == '__main__': - py.test.cmdline.main([__file__]) - """ % (str(py._pydir.dirpath()))) - import subprocess - popen = subprocess.Popen([sys.executable, str(p)], stdout=subprocess.PIPE) - s = popen.stdout.read() - ret = popen.wait() - assert ret == 0 - --- a/testing/plugin/test_terminal.py +++ /dev/null @@ -1,639 +0,0 @@ -""" -terminal reporting of the full testing process. -""" -import pytest,py -import sys - -from _pytest.terminal import TerminalReporter, \ - CollectonlyReporter, repr_pythonversion, getreportopt -from _pytest import runner - -def basic_run_report(item): - runner.call_and_report(item, "setup", log=False) - return runner.call_and_report(item, "call", log=False) - -class Option: - def __init__(self, verbose=False, fulltrace=False): - self.verbose = verbose - self.fulltrace = fulltrace - - @property - def args(self): - l = [] - if self.verbose: - l.append('-v') - if self.fulltrace: - l.append('--fulltrace') - return l - -def pytest_generate_tests(metafunc): - if "option" in metafunc.funcargnames: - metafunc.addcall(id="default", - funcargs={'option': Option(verbose=False)}) - metafunc.addcall(id="verbose", - funcargs={'option': Option(verbose=True)}) - metafunc.addcall(id="quiet", - funcargs={'option': Option(verbose=-1)}) - metafunc.addcall(id="fulltrace", - funcargs={'option': Option(fulltrace=True)}) - - -class TestTerminal: - def test_pass_skip_fail(self, testdir, option): - p = testdir.makepyfile(""" - import py - def test_ok(): - pass - def test_skip(): - py.test.skip("xx") - def test_func(): - assert 0 - """) - result = testdir.runpytest(*option.args) - if option.verbose: - result.stdout.fnmatch_lines([ - "*test_pass_skip_fail.py:2: *test_ok*PASS*", - "*test_pass_skip_fail.py:4: *test_skip*SKIP*", - "*test_pass_skip_fail.py:6: *test_func*FAIL*", - ]) - else: - result.stdout.fnmatch_lines([ - "*test_pass_skip_fail.py .sF" - ]) - result.stdout.fnmatch_lines([ - " def test_func():", - "> assert 0", - "E assert 0", - ]) - - def test_internalerror(self, testdir, linecomp): - modcol = testdir.getmodulecol("def test_one(): pass") - rep = TerminalReporter(modcol.config, file=linecomp.stringio) - excinfo = py.test.raises(ValueError, "raise ValueError('hello')") - rep.pytest_internalerror(excinfo.getrepr()) - linecomp.assert_contains_lines([ - "INTERNALERROR> *ValueError*hello*" - ]) - - def test_writeline(self, testdir, linecomp): - modcol = testdir.getmodulecol("def test_one(): pass") - stringio = py.io.TextIO() - rep = TerminalReporter(modcol.config, file=linecomp.stringio) - rep.write_fspath_result(py.path.local("xy.py"), '.') - rep.write_line("hello world") - lines = linecomp.stringio.getvalue().split('\n') - assert not lines[0] - assert lines[1].endswith("xy.py .") - assert lines[2] == "hello world" - - def test_show_runtest_logstart(self, testdir, linecomp): - item = testdir.getitem("def test_func(): pass") - tr = TerminalReporter(item.config, file=linecomp.stringio) - item.config.pluginmanager.register(tr) - location = item.reportinfo() - tr.config.hook.pytest_runtest_logstart(nodeid=item.nodeid, - location=location, fspath=str(item.fspath)) - linecomp.assert_contains_lines([ - "*test_show_runtest_logstart.py*" - ]) - - def test_runtest_location_shown_before_test_starts(self, testdir): - p1 = testdir.makepyfile(""" - def test_1(): - import time - time.sleep(20) - """) - child = testdir.spawn_pytest("") - child.expect(".*test_runtest_location.*py") - child.sendeof() - child.kill(15) - - def test_itemreport_subclasses_show_subclassed_file(self, testdir): - p1 = testdir.makepyfile(test_p1=""" - class BaseTests: - def test_p1(self): - pass - class TestClass(BaseTests): - pass - """) - p2 = testdir.makepyfile(test_p2=""" - from test_p1 import BaseTests - class TestMore(BaseTests): - pass - """) - result = testdir.runpytest(p2) - result.stdout.fnmatch_lines([ - "*test_p2.py .", - "*1 passed*", - ]) - result = testdir.runpytest("-v", p2) - result.stdout.fnmatch_lines([ - "*test_p2.py <- *test_p1.py:2: TestMore.test_p1*", - ]) - - def test_keyboard_interrupt(self, testdir, option): - p = testdir.makepyfile(""" - def test_foobar(): - assert 0 - def test_spamegg(): - import py; py.test.skip('skip me please!') - def test_interrupt_me(): - raise KeyboardInterrupt # simulating the user - """) - - result = testdir.runpytest(*option.args) - result.stdout.fnmatch_lines([ - " def test_foobar():", - "> assert 0", - "E assert 0", - "*_keyboard_interrupt.py:6: KeyboardInterrupt*", - ]) - if option.fulltrace: - result.stdout.fnmatch_lines([ - "*raise KeyboardInterrupt # simulating the user*", - ]) - result.stdout.fnmatch_lines(['*KeyboardInterrupt*']) - - - -class TestCollectonly: - def test_collectonly_basic(self, testdir, linecomp): - modcol = testdir.getmodulecol(configargs=['--collectonly'], source=""" - def test_func(): - pass - """) - rep = CollectonlyReporter(modcol.config, out=linecomp.stringio) - modcol.config.pluginmanager.register(rep) - indent = rep.indent - rep.config.hook.pytest_collectstart(collector=modcol) - linecomp.assert_contains_lines([ - "" - ]) - item = modcol.collect()[0] - rep.config.hook.pytest_itemcollected(item=item) - linecomp.assert_contains_lines([ - " ", - ]) - report = rep.config.hook.pytest_make_collect_report(collector=modcol) - rep.config.hook.pytest_collectreport(report=report) - assert rep.indent == indent - - def test_collectonly_skipped_module(self, testdir, linecomp): - modcol = testdir.getmodulecol(configargs=['--collectonly'], source=""" - import py - py.test.skip("nomod") - """) - rep = CollectonlyReporter(modcol.config, out=linecomp.stringio) - modcol.config.pluginmanager.register(rep) - cols = list(testdir.genitems([modcol])) - assert len(cols) == 0 - linecomp.assert_contains_lines(""" - - !!! Skipped: nomod !!! - """) - - def test_collectonly_failed_module(self, testdir, linecomp): - modcol = testdir.getmodulecol(configargs=['--collectonly'], source=""" - raise ValueError(0) - """) - rep = CollectonlyReporter(modcol.config, out=linecomp.stringio) - modcol.config.pluginmanager.register(rep) - cols = list(testdir.genitems([modcol])) - assert len(cols) == 0 - linecomp.assert_contains_lines(""" - - !!! ValueError: 0 !!! - """) - - def test_collectonly_fatal(self, testdir): - p1 = testdir.makeconftest(""" - def pytest_collectstart(collector): - assert 0, "urgs" - """) - result = testdir.runpytest("--collectonly") - result.stdout.fnmatch_lines([ - "*INTERNAL*args*" - ]) - assert result.ret == 3 - - def test_collectonly_simple(self, testdir): - p = testdir.makepyfile(""" - def test_func1(): - pass - class TestClass: - def test_method(self): - pass - """) - result = testdir.runpytest("--collectonly", p) - stderr = result.stderr.str().strip() - #assert stderr.startswith("inserting into sys.path") - assert result.ret == 0 - extra = result.stdout.fnmatch_lines([ - "*", - "* ", - "* ", - "* ", - "* ", - ]) - - def test_collectonly_error(self, testdir): - p = testdir.makepyfile("import Errlkjqweqwe") - result = testdir.runpytest("--collectonly", p) - stderr = result.stderr.str().strip() - assert result.ret == 1 - extra = result.stdout.fnmatch_lines(py.code.Source(""" - * - *ImportError* - *!!!*failures*!!! - *test_collectonly_error.py:1* - """).strip()) - - -def test_repr_python_version(monkeypatch): - monkeypatch.setattr(sys, 'version_info', (2, 5, 1, 'final', 0)) - assert repr_pythonversion() == "2.5.1-final-0" - py.std.sys.version_info = x = (2,3) - assert repr_pythonversion() == str(x) - -class TestFixtureReporting: - def test_setup_fixture_error(self, testdir): - p = testdir.makepyfile(""" - def setup_function(function): - print ("setup func") - assert 0 - def test_nada(): - pass - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*ERROR at setup of test_nada*", - "*setup_function(function):*", - "*setup func*", - "*assert 0*", - "*1 error*", - ]) - assert result.ret != 0 - - def test_teardown_fixture_error(self, testdir): - p = testdir.makepyfile(""" - def test_nada(): - pass - def teardown_function(function): - print ("teardown func") - assert 0 - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*ERROR at teardown*", - "*teardown_function(function):*", - "*assert 0*", - "*Captured stdout*", - "*teardown func*", - "*1 passed*1 error*", - ]) - - def test_teardown_fixture_error_and_test_failure(self, testdir): - p = testdir.makepyfile(""" - def test_fail(): - assert 0, "failingfunc" - - def teardown_function(function): - print ("teardown func") - assert False - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*ERROR at teardown of test_fail*", - "*teardown_function(function):*", - "*assert False*", - "*Captured stdout*", - "*teardown func*", - - "*test_fail*", - "*def test_fail():", - "*failingfunc*", - "*1 failed*1 error*", - ]) - -class TestTerminalFunctional: - def test_deselected(self, testdir): - testpath = testdir.makepyfile(""" - def test_one(): - pass - def test_two(): - pass - def test_three(): - pass - """ - ) - result = testdir.runpytest("-k", "test_two:", testpath) - result.stdout.fnmatch_lines([ - "*test_deselected.py ..", - "=* 1 test*deselected by 'test_two:'*=", - ]) - assert result.ret == 0 - - def test_no_skip_summary_if_failure(self, testdir): - testdir.makepyfile(""" - import py - def test_ok(): - pass - def test_fail(): - assert 0 - def test_skip(): - py.test.skip("dontshow") - """) - result = testdir.runpytest() - assert result.stdout.str().find("skip test summary") == -1 - assert result.ret == 1 - - def test_passes(self, testdir): - p1 = testdir.makepyfile(""" - def test_passes(): - pass - class TestClass: - def test_method(self): - pass - """) - old = p1.dirpath().chdir() - try: - result = testdir.runpytest() - finally: - old.chdir() - result.stdout.fnmatch_lines([ - "test_passes.py ..", - "* 2 pass*", - ]) - assert result.ret == 0 - - def test_header_trailer_info(self, testdir): - p1 = testdir.makepyfile(""" - def test_passes(): - pass - """) - result = testdir.runpytest() - verinfo = ".".join(map(str, py.std.sys.version_info[:3])) - result.stdout.fnmatch_lines([ - "*===== test session starts ====*", - "platform %s -- Python %s*" %( - py.std.sys.platform, verinfo), # , py.std.sys.executable), - "*test_header_trailer_info.py .", - "=* 1 passed in *.[0-9][0-9] seconds *=", - ]) - - def test_showlocals(self, testdir): - p1 = testdir.makepyfile(""" - def test_showlocals(): - x = 3 - y = "x" * 5000 - assert 0 - """) - result = testdir.runpytest(p1, '-l') - result.stdout.fnmatch_lines([ - #"_ _ * Locals *", - "x* = 3", - "y* = 'xxxxxx*" - ]) - - def test_verbose_reporting(self, testdir, pytestconfig): - p1 = testdir.makepyfile(""" - import py - def test_fail(): - raise ValueError() - def test_pass(): - pass - class TestClass: - def test_skip(self): - py.test.skip("hello") - def test_gen(): - def check(x): - assert x == 1 - yield check, 0 - """) - result = testdir.runpytest(p1, '-v') - result.stdout.fnmatch_lines([ - "*test_verbose_reporting.py:2: test_fail*FAIL*", - "*test_verbose_reporting.py:4: test_pass*PASS*", - "*test_verbose_reporting.py:7: TestClass.test_skip*SKIP*", - "*test_verbose_reporting.py:10: test_gen*FAIL*", - ]) - assert result.ret == 1 - pytestconfig.pluginmanager.skipifmissing("xdist") - result = testdir.runpytest(p1, '-v', '-n 1') - result.stdout.fnmatch_lines([ - "*FAIL*test_verbose_reporting.py:2: test_fail*", - ]) - assert result.ret == 1 - - def test_quiet_reporting(self, testdir): - p1 = testdir.makepyfile("def test_pass(): pass") - result = testdir.runpytest(p1, '-q') - s = result.stdout.str() - assert 'test session starts' not in s - assert p1.basename not in s - assert "===" not in s - -def test_fail_extra_reporting(testdir): - p = testdir.makepyfile("def test_this(): assert 0") - result = testdir.runpytest(p) - assert 'short test summary' not in result.stdout.str() - result = testdir.runpytest(p, '-rf') - result.stdout.fnmatch_lines([ - "*test summary*", - "FAIL*test_fail_extra_reporting*", - ]) - -def test_fail_reporting_on_pass(testdir): - p = testdir.makepyfile("def test_this(): assert 1") - result = testdir.runpytest(p, '-rf') - assert 'short test summary' not in result.stdout.str() - -def test_getreportopt(): - class config: - class option: - reportchars = "" - config.option.report = "xfailed" - assert getreportopt(config) == "x" - - config.option.report = "xfailed,skipped" - assert getreportopt(config) == "xs" - - config.option.report = "skipped,xfailed" - assert getreportopt(config) == "sx" - - config.option.report = "skipped" - config.option.reportchars = "sf" - assert getreportopt(config) == "sf" - - config.option.reportchars = "sfx" - assert getreportopt(config) == "sfx" - -def test_terminalreporter_reportopt_addopts(testdir): - testdir.makeini("[pytest]\naddopts=-rs") - p = testdir.makepyfile(""" - def pytest_funcarg__tr(request): - tr = request.config.pluginmanager.getplugin("terminalreporter") - return tr - def test_opt(tr): - assert tr.hasopt('skipped') - assert not tr.hasopt('qwe') - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) - -def test_tbstyle_short(testdir): - p = testdir.makepyfile(""" - def pytest_funcarg__arg(request): - return 42 - def test_opt(arg): - x = 0 - assert x - """) - result = testdir.runpytest("--tb=short") - s = result.stdout.str() - assert 'arg = 42' not in s - assert 'x = 0' not in s - result.stdout.fnmatch_lines([ - "*%s:5*" % p.basename, - ">*assert x", - "E*assert*", - ]) - result = testdir.runpytest() - s = result.stdout.str() - assert 'x = 0' in s - assert 'assert x' in s - -def test_traceconfig(testdir, monkeypatch): - result = testdir.runpytest("--traceconfig") - result.stdout.fnmatch_lines([ - "*active plugins*" - ]) - assert result.ret == 0 - -def test_debug(testdir, monkeypatch): - result = testdir.runpytest("--debug") - result.stderr.fnmatch_lines([ - "*pytest_sessionstart*session*", - ]) - assert result.ret == 0 - -def test_PYTEST_DEBUG(testdir, monkeypatch): - monkeypatch.setenv("PYTEST_DEBUG", "1") - result = testdir.runpytest() - assert result.ret == 0 - result.stderr.fnmatch_lines([ - "*registered*PluginManager*" - ]) - - -class TestGenericReporting: - """ this test class can be subclassed with a different option - provider to run e.g. distributed tests. - """ - def test_collect_fail(self, testdir, option): - p = testdir.makepyfile("import xyz\n") - result = testdir.runpytest(*option.args) - result.stdout.fnmatch_lines([ - "*test_collect_fail.py E*", - "> import xyz", - "E ImportError: No module named xyz", - "*1 error*", - ]) - - def test_maxfailures(self, testdir, option): - p = testdir.makepyfile(""" - def test_1(): - assert 0 - def test_2(): - assert 0 - def test_3(): - assert 0 - """) - result = testdir.runpytest("--maxfail=2", *option.args) - result.stdout.fnmatch_lines([ - "*def test_1():*", - "*def test_2():*", - "*!! Interrupted: stopping after 2 failures*!!*", - "*2 failed*", - ]) - - - def test_tb_option(self, testdir, option): - p = testdir.makepyfile(""" - import py - def g(): - raise IndexError - def test_func(): - print (6*7) - g() # --calling-- - """) - for tbopt in ["long", "short", "no"]: - print('testing --tb=%s...' % tbopt) - result = testdir.runpytest('--tb=%s' % tbopt) - s = result.stdout.str() - if tbopt == "long": - assert 'print (6*7)' in s - else: - assert 'print (6*7)' not in s - if tbopt != "no": - assert '--calling--' in s - assert 'IndexError' in s - else: - assert 'FAILURES' not in s - assert '--calling--' not in s - assert 'IndexError' not in s - - def test_tb_crashline(self, testdir, option): - p = testdir.makepyfile(""" - import py - def g(): - raise IndexError - def test_func1(): - print (6*7) - g() # --calling-- - def test_func2(): - assert 0, "hello" - """) - result = testdir.runpytest("--tb=line") - bn = p.basename - result.stdout.fnmatch_lines([ - "*%s:3: IndexError*" % bn, - "*%s:8: AssertionError: hello*" % bn, - ]) - s = result.stdout.str() - assert "def test_func2" not in s - - def test_pytest_report_header(self, testdir, option): - testdir.makeconftest(""" - def pytest_report_header(config): - return "hello: info" - """) - testdir.mkdir("a").join("conftest.py").write(""" -def pytest_report_header(config): - return ["line1", "line2"]""") - result = testdir.runpytest("a") - result.stdout.fnmatch_lines([ - "line1", - "line2", - "*hello: info*", - ]) - - at py.test.mark.xfail("not hasattr(os, 'dup')") -def test_fdopen_kept_alive_issue124(testdir): - testdir.makepyfile(""" - import os, sys - k = [] - def test_open_file_and_keep_alive(capfd): - stdout = os.fdopen(1, 'w', 1) - k.append(stdout) - - def test_close_kept_alive_file(): - stdout = k.pop() - stdout.close() - """) - result = testdir.runpytest("-s") - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) --- /dev/null +++ b/testing/test_skipping.py @@ -0,0 +1,444 @@ +import py + +from _pytest.skipping import MarkEvaluator, folded_skips +from _pytest.skipping import pytest_runtest_setup +from _pytest.runner import runtestprotocol + +class TestEvaluator: + def test_no_marker(self, testdir): + item = testdir.getitem("def test_func(): pass") + evalskipif = MarkEvaluator(item, 'skipif') + assert not evalskipif + assert not evalskipif.istrue() + + def test_marked_no_args(self, testdir): + item = testdir.getitem(""" + import py + @py.test.mark.xyz + def test_func(): + pass + """) + ev = MarkEvaluator(item, 'xyz') + assert ev + assert ev.istrue() + expl = ev.getexplanation() + assert expl == "" + assert not ev.get("run", False) + + def test_marked_one_arg(self, testdir): + item = testdir.getitem(""" + import py + @py.test.mark.xyz("hasattr(os, 'sep')") + def test_func(): + pass + """) + ev = MarkEvaluator(item, 'xyz') + assert ev + assert ev.istrue() + expl = ev.getexplanation() + assert expl == "condition: hasattr(os, 'sep')" + + def test_marked_one_arg_with_reason(self, testdir): + item = testdir.getitem(""" + import py + @py.test.mark.xyz("hasattr(os, 'sep')", attr=2, reason="hello world") + def test_func(): + pass + """) + ev = MarkEvaluator(item, 'xyz') + assert ev + assert ev.istrue() + expl = ev.getexplanation() + assert expl == "hello world" + assert ev.get("attr") == 2 + + def test_marked_one_arg_twice(self, testdir): + lines = [ + '''@py.test.mark.skipif("not hasattr(os, 'murks')")''', + '''@py.test.mark.skipif("hasattr(os, 'murks')")''' + ] + for i in range(0, 2): + item = testdir.getitem(""" + import py + %s + %s + def test_func(): + pass + """ % (lines[i], lines[(i+1) %2])) + ev = MarkEvaluator(item, 'skipif') + assert ev + assert ev.istrue() + expl = ev.getexplanation() + assert expl == "condition: not hasattr(os, 'murks')" + + def test_marked_one_arg_twice2(self, testdir): + item = testdir.getitem(""" + import py + @py.test.mark.skipif("hasattr(os, 'murks')") + @py.test.mark.skipif("not hasattr(os, 'murks')") + def test_func(): + pass + """) + ev = MarkEvaluator(item, 'skipif') + assert ev + assert ev.istrue() + expl = ev.getexplanation() + assert expl == "condition: not hasattr(os, 'murks')" + + def test_skipif_class(self, testdir): + item, = testdir.getitems(""" + import py + class TestClass: + pytestmark = py.test.mark.skipif("config._hackxyz") + def test_func(self): + pass + """) + item.config._hackxyz = 3 + ev = MarkEvaluator(item, 'skipif') + assert ev.istrue() + expl = ev.getexplanation() + assert expl == "condition: config._hackxyz" + + +class TestXFail: + def test_xfail_simple(self, testdir): + item = testdir.getitem(""" + import py + @py.test.mark.xfail + def test_func(): + assert 0 + """) + reports = runtestprotocol(item, log=False) + assert len(reports) == 3 + callreport = reports[1] + assert callreport.skipped + expl = callreport.keywords['xfail'] + assert expl == "" + + def test_xfail_xpassed(self, testdir): + item = testdir.getitem(""" + import py + @py.test.mark.xfail + def test_func(): + assert 1 + """) + reports = runtestprotocol(item, log=False) + assert len(reports) == 3 + callreport = reports[1] + assert callreport.failed + expl = callreport.keywords['xfail'] + assert expl == "" + + def test_xfail_run_anyway(self, testdir): + testdir.makepyfile(""" + import py + @py.test.mark.xfail + def test_func(): + assert 0 + """) + result = testdir.runpytest("--runxfail") + assert result.ret == 1 + result.stdout.fnmatch_lines([ + "*def test_func():*", + "*assert 0*", + "*1 failed*", + ]) + + def test_xfail_evalfalse_but_fails(self, testdir): + item = testdir.getitem(""" + import py + @py.test.mark.xfail('False') + def test_func(): + assert 0 + """) + reports = runtestprotocol(item, log=False) + callreport = reports[1] + assert callreport.failed + assert 'xfail' not in callreport.keywords + + def test_xfail_not_report_default(self, testdir): + p = testdir.makepyfile(test_one=""" + import py + @py.test.mark.xfail + def test_this(): + assert 0 + """) + result = testdir.runpytest(p, '-v') + #result.stdout.fnmatch_lines([ + # "*HINT*use*-r*" + #]) + + def test_xfail_not_run_xfail_reporting(self, testdir): + p = testdir.makepyfile(test_one=""" + import py + @py.test.mark.xfail(run=False, reason="noway") + def test_this(): + assert 0 + @py.test.mark.xfail("True", run=False) + def test_this_true(): + assert 0 + @py.test.mark.xfail("False", run=False, reason="huh") + def test_this_false(): + assert 1 + """) + result = testdir.runpytest(p, '--report=xfailed', ) + result.stdout.fnmatch_lines([ + "*test_one*test_this*", + "*NOTRUN*noway", + "*test_one*test_this_true*", + "*NOTRUN*condition:*True*", + "*1 passed*", + ]) + + def test_xfail_not_run_no_setup_run(self, testdir): + p = testdir.makepyfile(test_one=""" + import py + @py.test.mark.xfail(run=False, reason="hello") + def test_this(): + assert 0 + def setup_module(mod): + raise ValueError(42) + """) + result = testdir.runpytest(p, '--report=xfailed', ) + result.stdout.fnmatch_lines([ + "*test_one*test_this*", + "*NOTRUN*hello", + "*1 xfailed*", + ]) + + def test_xfail_xpass(self, testdir): + p = testdir.makepyfile(test_one=""" + import py + @py.test.mark.xfail + def test_that(): + assert 1 + """) + result = testdir.runpytest(p, '-rX') + result.stdout.fnmatch_lines([ + "*XPASS*test_that*", + "*1 xpassed*" + ]) + assert result.ret == 0 + + def test_xfail_imperative(self, testdir): + p = testdir.makepyfile(""" + import py + def test_this(): + py.test.xfail("hello") + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*1 xfailed*", + ]) + result = testdir.runpytest(p, "-rx") + result.stdout.fnmatch_lines([ + "*XFAIL*test_this*", + "*reason:*hello*", + ]) + result = testdir.runpytest(p, "--runxfail") + result.stdout.fnmatch_lines([ + "*def test_this():*", + "*py.test.xfail*", + ]) + + def test_xfail_imperative_in_setup_function(self, testdir): + p = testdir.makepyfile(""" + import py + def setup_function(function): + py.test.xfail("hello") + + def test_this(): + assert 0 + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*1 xfailed*", + ]) + result = testdir.runpytest(p, "-rx") + result.stdout.fnmatch_lines([ + "*XFAIL*test_this*", + "*reason:*hello*", + ]) + result = testdir.runpytest(p, "--runxfail") + result.stdout.fnmatch_lines([ + "*def setup_function(function):*", + "*py.test.xfail*", + ]) + + def xtest_dynamic_xfail_set_during_setup(self, testdir): + p = testdir.makepyfile(""" + import py + def setup_function(function): + py.test.mark.xfail(function) + def test_this(): + assert 0 + def test_that(): + assert 1 + """) + result = testdir.runpytest(p, '-rxX') + result.stdout.fnmatch_lines([ + "*XFAIL*test_this*", + "*XPASS*test_that*", + ]) + + def test_dynamic_xfail_no_run(self, testdir): + p = testdir.makepyfile(""" + import py + def pytest_funcarg__arg(request): + request.applymarker(py.test.mark.xfail(run=False)) + def test_this(arg): + assert 0 + """) + result = testdir.runpytest(p, '-rxX') + result.stdout.fnmatch_lines([ + "*XFAIL*test_this*", + "*NOTRUN*", + ]) + + def test_dynamic_xfail_set_during_funcarg_setup(self, testdir): + p = testdir.makepyfile(""" + import py + def pytest_funcarg__arg(request): + request.applymarker(py.test.mark.xfail) + def test_this2(arg): + assert 0 + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*1 xfailed*", + ]) + + +class TestSkipif: + def test_skipif_conditional(self, testdir): + item = testdir.getitem(""" + import py + @py.test.mark.skipif("hasattr(os, 'sep')") + def test_func(): + pass + """) + x = py.test.raises(py.test.skip.Exception, "pytest_runtest_setup(item)") + assert x.value.msg == "condition: hasattr(os, 'sep')" + + + def test_skipif_reporting(self, testdir): + p = testdir.makepyfile(""" + import py + @py.test.mark.skipif("hasattr(sys, 'platform')") + def test_that(): + assert 0 + """) + result = testdir.runpytest(p, '-s', '-rs') + result.stdout.fnmatch_lines([ + "*SKIP*1*platform*", + "*1 skipped*" + ]) + assert result.ret == 0 + +def test_skip_not_report_default(testdir): + p = testdir.makepyfile(test_one=""" + import py + def test_this(): + py.test.skip("hello") + """) + result = testdir.runpytest(p, '-v') + result.stdout.fnmatch_lines([ + #"*HINT*use*-r*", + "*1 skipped*", + ]) + + +def test_skipif_class(testdir): + p = testdir.makepyfile(""" + import py + + class TestClass: + pytestmark = py.test.mark.skipif("True") + def test_that(self): + assert 0 + def test_though(self): + assert 0 + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*2 skipped*" + ]) + + +def test_skip_reasons_folding(): + class longrepr: + class reprcrash: + path = 'xyz' + lineno = 3 + message = "justso" + + class X: + pass + ev1 = X() + ev1.when = "execute" + ev1.skipped = True + ev1.longrepr = longrepr + + ev2 = X() + ev2.longrepr = longrepr + ev2.skipped = True + + l = folded_skips([ev1, ev2]) + assert len(l) == 1 + num, fspath, lineno, reason = l[0] + assert num == 2 + assert fspath == longrepr.reprcrash.path + assert lineno == longrepr.reprcrash.lineno + assert reason == longrepr.reprcrash.message + +def test_skipped_reasons_functional(testdir): + testdir.makepyfile( + test_one=""" + from conftest import doskip + def setup_function(func): + doskip() + def test_func(): + pass + class TestClass: + def test_method(self): + doskip() + """, + test_two = """ + from conftest import doskip + doskip() + """, + conftest = """ + import py + def doskip(): + py.test.skip('test') + """ + ) + result = testdir.runpytest('--report=skipped') + result.stdout.fnmatch_lines([ + "*test_two.py S", + "*test_one.py ss", + "*SKIP*3*conftest.py:3: test", + ]) + assert result.ret == 0 + +def test_reportchars(testdir): + testdir.makepyfile(""" + import py + def test_1(): + assert 0 + @py.test.mark.xfail + def test_2(): + assert 0 + @py.test.mark.xfail + def test_3(): + pass + def test_4(): + py.test.skip("four") + """) + result = testdir.runpytest("-rfxXs") + result.stdout.fnmatch_lines([ + "FAIL*test_1*", + "XFAIL*test_2*", + "XPASS*test_3*", + "SKIP*four*", + ]) --- a/testing/plugin/test_assertion.py +++ /dev/null @@ -1,203 +0,0 @@ -import sys - -import py -import _pytest.assertion as plugin - -needsnewassert = py.test.mark.skipif("sys.version_info < (2,6)") - -def interpret(expr): - return py.code._reinterpret(expr, py.code.Frame(sys._getframe(1))) - -class TestBinReprIntegration: - pytestmark = needsnewassert - - def pytest_funcarg__hook(self, request): - class MockHook(object): - def __init__(self): - self.called = False - self.args = tuple() - self.kwargs = dict() - - def __call__(self, op, left, right): - self.called = True - self.op = op - self.left = left - self.right = right - mockhook = MockHook() - monkeypatch = request.getfuncargvalue("monkeypatch") - monkeypatch.setattr(py.code, '_reprcompare', mockhook) - return mockhook - - def test_pytest_assertrepr_compare_called(self, hook): - interpret('assert 0 == 1') - assert hook.called - - - def test_pytest_assertrepr_compare_args(self, hook): - interpret('assert [0, 1] == [0, 2]') - assert hook.op == '==' - assert hook.left == [0, 1] - assert hook.right == [0, 2] - - def test_configure_unconfigure(self, testdir, hook): - assert hook == py.code._reprcompare - config = testdir.parseconfig() - plugin.pytest_configure(config) - assert hook != py.code._reprcompare - plugin.pytest_unconfigure(config) - assert hook == py.code._reprcompare - -def callequal(left, right): - return plugin.pytest_assertrepr_compare('==', left, right) - -class TestAssert_reprcompare: - def test_different_types(self): - assert callequal([0, 1], 'foo') is None - - def test_summary(self): - summary = callequal([0, 1], [0, 2])[0] - assert len(summary) < 65 - - def test_text_diff(self): - diff = callequal('spam', 'eggs')[1:] - assert '- spam' in diff - assert '+ eggs' in diff - - def test_multiline_text_diff(self): - left = 'foo\nspam\nbar' - right = 'foo\neggs\nbar' - diff = callequal(left, right) - assert '- spam' in diff - assert '+ eggs' in diff - - def test_list(self): - expl = callequal([0, 1], [0, 2]) - assert len(expl) > 1 - - def test_list_different_lenghts(self): - expl = callequal([0, 1], [0, 1, 2]) - assert len(expl) > 1 - expl = callequal([0, 1, 2], [0, 1]) - assert len(expl) > 1 - - def test_dict(self): - expl = callequal({'a': 0}, {'a': 1}) - assert len(expl) > 1 - - def test_set(self): - expl = callequal(set([0, 1]), set([0, 2])) - assert len(expl) > 1 - - def test_list_tuples(self): - expl = callequal([], [(1,2)]) - assert len(expl) > 1 - expl = callequal([(1,2)], []) - assert len(expl) > 1 - - def test_list_bad_repr(self): - class A: - def __repr__(self): - raise ValueError(42) - expl = callequal([], [A()]) - assert 'ValueError' in "".join(expl) - expl = callequal({}, {'1': A()}) - assert 'faulty' in "".join(expl) - - def test_one_repr_empty(self): - """ - the faulty empty string repr did trigger - a unbound local error in _diff_text - """ - class A(str): - def __repr__(self): - return '' - expl = callequal(A(), '') - assert not expl - - at needsnewassert -def test_pytest_assertrepr_compare_integration(testdir): - testdir.makepyfile(""" - def test_hello(): - x = set(range(100)) - y = x.copy() - y.remove(50) - assert x == y - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*def test_hello():*", - "*assert x == y*", - "*E*Extra items*left*", - "*E*50*", - ]) - - at needsnewassert -def test_sequence_comparison_uses_repr(testdir): - testdir.makepyfile(""" - def test_hello(): - x = set("hello x") - y = set("hello y") - assert x == y - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*def test_hello():*", - "*assert x == y*", - "*E*Extra items*left*", - "*E*'x'*", - "*E*Extra items*right*", - "*E*'y'*", - ]) - - -def test_functional(testdir): - testdir.makepyfile(""" - def test_hello(): - x = 3 - assert x == 4 - """) - result = testdir.runpytest() - assert "3 == 4" in result.stdout.str() - result = testdir.runpytest("--no-assert") - assert "3 == 4" not in result.stdout.str() - -def test_triple_quoted_string_issue113(testdir): - testdir.makepyfile(""" - def test_hello(): - assert "" == ''' - '''""") - result = testdir.runpytest("--fulltrace") - result.stdout.fnmatch_lines([ - "*1 failed*", - ]) - assert 'SyntaxError' not in result.stdout.str() - -def test_traceback_failure(testdir): - p1 = testdir.makepyfile(""" - def g(): - return 2 - def f(x): - assert x == g() - def test_onefails(): - f(3) - """) - result = testdir.runpytest(p1) - result.stdout.fnmatch_lines([ - "*test_traceback_failure.py F", - "====* FAILURES *====", - "____*____", - "", - " def test_onefails():", - "> f(3)", - "", - "*test_*.py:6: ", - "_ _ _ *", - #"", - " def f(x):", - "> assert x == g()", - "E assert 3 == 2", - "E + where 2 = g()", - "", - "*test_traceback_failure.py:4: AssertionError" - ]) - --- /dev/null +++ b/testing/test_junitxml.py @@ -0,0 +1,265 @@ + +from xml.dom import minidom +import py, sys + +def runandparse(testdir, *args): + resultpath = testdir.tmpdir.join("junit.xml") + result = testdir.runpytest("--junitxml=%s" % resultpath, *args) + xmldoc = minidom.parse(str(resultpath)) + return result, xmldoc + +def assert_attr(node, **kwargs): + __tracebackhide__ = True + for name, expected in kwargs.items(): + anode = node.getAttributeNode(name) + assert anode, "node %r has no attribute %r" %(node, name) + val = anode.value + if val != str(expected): + py.test.fail("%r != %r" %(str(val), str(expected))) + +class TestPython: + def test_summing_simple(self, testdir): + testdir.makepyfile(""" + import py + def test_pass(): + pass + def test_fail(): + assert 0 + def test_skip(): + py.test.skip("") + @py.test.mark.xfail + def test_xfail(): + assert 0 + @py.test.mark.xfail + def test_xpass(): + assert 1 + """) + result, dom = runandparse(testdir) + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, errors=0, failures=1, skips=3, tests=2) + + def test_setup_error(self, testdir): + testdir.makepyfile(""" + def pytest_funcarg__arg(request): + raise ValueError() + def test_function(arg): + pass + """) + result, dom = runandparse(testdir) + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, errors=1, tests=0) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + classname="test_setup_error", + name="test_function") + fnode = tnode.getElementsByTagName("error")[0] + assert_attr(fnode, message="test setup failure") + assert "ValueError" in fnode.toxml() + + def test_classname_instance(self, testdir): + testdir.makepyfile(""" + class TestClass: + def test_method(self): + assert 0 + """) + result, dom = runandparse(testdir) + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, failures=1) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + classname="test_classname_instance.TestClass", + name="test_method") + + def test_classname_nested_dir(self, testdir): + p = testdir.tmpdir.ensure("sub", "test_hello.py") + p.write("def test_func(): 0/0") + result, dom = runandparse(testdir) + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, failures=1) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + classname="sub.test_hello", + name="test_func") + + def test_internal_error(self, testdir): + testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0") + testdir.makepyfile("def test_function(): pass") + result, dom = runandparse(testdir) + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, errors=1, tests=0) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, classname="pytest", name="internal") + fnode = tnode.getElementsByTagName("error")[0] + assert_attr(fnode, message="internal error") + assert "Division" in fnode.toxml() + + def test_failure_function(self, testdir): + testdir.makepyfile("def test_fail(): raise ValueError(42)") + result, dom = runandparse(testdir) + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, failures=1, tests=1) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + classname="test_failure_function", + name="test_fail") + fnode = tnode.getElementsByTagName("failure")[0] + assert_attr(fnode, message="test failure") + assert "ValueError" in fnode.toxml() + + def test_failure_escape(self, testdir): + testdir.makepyfile(""" + def pytest_generate_tests(metafunc): + metafunc.addcall(id="<", funcargs=dict(arg1=42)) + metafunc.addcall(id="&", funcargs=dict(arg1=44)) + def test_func(arg1): + assert 0 + """) + result, dom = runandparse(testdir) + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, failures=2, tests=2) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + classname="test_failure_escape", + name="test_func[<]") + tnode = node.getElementsByTagName("testcase")[1] + assert_attr(tnode, + classname="test_failure_escape", + name="test_func[&]") + + def test_junit_prefixing(self, testdir): + testdir.makepyfile(""" + def test_func(): + assert 0 + class TestHello: + def test_hello(self): + pass + """) + result, dom = runandparse(testdir, "--junitprefix=xyz") + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, failures=1, tests=2) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + classname="xyz.test_junit_prefixing", + name="test_func") + tnode = node.getElementsByTagName("testcase")[1] + assert_attr(tnode, + classname="xyz.test_junit_prefixing." + "TestHello", + name="test_hello") + + def test_xfailure_function(self, testdir): + testdir.makepyfile(""" + import py + def test_xfail(): + py.test.xfail("42") + """) + result, dom = runandparse(testdir) + assert not result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, skips=1, tests=0) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + classname="test_xfailure_function", + name="test_xfail") + fnode = tnode.getElementsByTagName("skipped")[0] + assert_attr(fnode, message="expected test failure") + #assert "ValueError" in fnode.toxml() + + def test_xfailure_xpass(self, testdir): + testdir.makepyfile(""" + import py + @py.test.mark.xfail + def test_xpass(): + pass + """) + result, dom = runandparse(testdir) + #assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, skips=1, tests=0) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + classname="test_xfailure_xpass", + name="test_xpass") + fnode = tnode.getElementsByTagName("skipped")[0] + assert_attr(fnode, message="xfail-marked test passes unexpectedly") + #assert "ValueError" in fnode.toxml() + + def test_collect_error(self, testdir): + testdir.makepyfile("syntax error") + result, dom = runandparse(testdir) + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, errors=1, tests=0) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + #classname="test_collect_error", + name="test_collect_error") + fnode = tnode.getElementsByTagName("failure")[0] + assert_attr(fnode, message="collection failure") + assert "SyntaxError" in fnode.toxml() + + def test_collect_skipped(self, testdir): + testdir.makepyfile("import py ; py.test.skip('xyz')") + result, dom = runandparse(testdir) + assert not result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, skips=1, tests=0) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + #classname="test_collect_error", + name="test_collect_skipped") + fnode = tnode.getElementsByTagName("skipped")[0] + assert_attr(fnode, message="collection skipped") + + def test_unicode(self, testdir): + value = 'hx\xc4\x85\xc4\x87\n' + testdir.makepyfile(""" + # coding: utf-8 + def test_hello(): + print (%r) + assert 0 + """ % value) + result, dom = runandparse(testdir) + assert result.ret == 1 + tnode = dom.getElementsByTagName("testcase")[0] + fnode = tnode.getElementsByTagName("failure")[0] + if not sys.platform.startswith("java"): + assert "hx" in fnode.toxml() + +class TestNonPython: + def test_summing_simple(self, testdir): + testdir.makeconftest(""" + import pytest + def pytest_collect_file(path, parent): + if path.ext == ".xyz": + return MyItem(path, parent) + class MyItem(pytest.Item): + def __init__(self, path, parent): + super(MyItem, self).__init__(path.basename, parent) + self.fspath = path + def runtest(self): + raise ValueError(42) + def repr_failure(self, excinfo): + return "custom item runtest failed" + """) + testdir.tmpdir.join("myfile.xyz").write("hello") + result, dom = runandparse(testdir) + assert result.ret + node = dom.getElementsByTagName("testsuite")[0] + assert_attr(node, errors=0, failures=1, skips=0, tests=1) + tnode = node.getElementsByTagName("testcase")[0] + assert_attr(tnode, + #classname="test_collect_error", + name="myfile.xyz") + fnode = tnode.getElementsByTagName("failure")[0] + assert_attr(fnode, message="test failure") + assert "custom item runtest failed" in fnode.toxml() + --- a/testing/plugin/test_pastebin.py +++ /dev/null @@ -1,47 +0,0 @@ - -class TestPasting: - def pytest_funcarg__pastebinlist(self, request): - mp = request.getfuncargvalue("monkeypatch") - pastebinlist = [] - class MockProxy: - def newPaste(self, language, code): - pastebinlist.append((language, code)) - plugin = request.config.pluginmanager.getplugin('pastebin') - mp.setattr(plugin, 'getproxy', MockProxy) - return pastebinlist - - def test_failed(self, testdir, pastebinlist): - testpath = testdir.makepyfile(""" - import py - def test_pass(): - pass - def test_fail(): - assert 0 - def test_skip(): - py.test.skip("") - """) - reprec = testdir.inline_run(testpath, "--paste=failed") - assert len(pastebinlist) == 1 - assert pastebinlist[0][0] == "python" - s = pastebinlist[0][1] - assert s.find("def test_fail") != -1 - assert reprec.countoutcomes() == [1,1,1] - - def test_all(self, testdir, pastebinlist): - testpath = testdir.makepyfile(""" - import py - def test_pass(): - pass - def test_fail(): - assert 0 - def test_skip(): - py.test.skip("") - """) - reprec = testdir.inline_run(testpath, "--pastebin=all") - assert reprec.countoutcomes() == [1,1,1] - assert len(pastebinlist) == 1 - assert pastebinlist[0][0] == "python" - s = pastebinlist[0][1] - for x in 'test_fail test_skip skipped'.split(): - assert s.find(x), (s, x) - --- a/testing/plugin/test_junitxml.py +++ /dev/null @@ -1,265 +0,0 @@ - -from xml.dom import minidom -import py, sys - -def runandparse(testdir, *args): - resultpath = testdir.tmpdir.join("junit.xml") - result = testdir.runpytest("--junitxml=%s" % resultpath, *args) - xmldoc = minidom.parse(str(resultpath)) - return result, xmldoc - -def assert_attr(node, **kwargs): - __tracebackhide__ = True - for name, expected in kwargs.items(): - anode = node.getAttributeNode(name) - assert anode, "node %r has no attribute %r" %(node, name) - val = anode.value - if val != str(expected): - py.test.fail("%r != %r" %(str(val), str(expected))) - -class TestPython: - def test_summing_simple(self, testdir): - testdir.makepyfile(""" - import py - def test_pass(): - pass - def test_fail(): - assert 0 - def test_skip(): - py.test.skip("") - @py.test.mark.xfail - def test_xfail(): - assert 0 - @py.test.mark.xfail - def test_xpass(): - assert 1 - """) - result, dom = runandparse(testdir) - assert result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, errors=0, failures=1, skips=3, tests=2) - - def test_setup_error(self, testdir): - testdir.makepyfile(""" - def pytest_funcarg__arg(request): - raise ValueError() - def test_function(arg): - pass - """) - result, dom = runandparse(testdir) - assert result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, errors=1, tests=0) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, - classname="test_setup_error", - name="test_function") - fnode = tnode.getElementsByTagName("error")[0] - assert_attr(fnode, message="test setup failure") - assert "ValueError" in fnode.toxml() - - def test_classname_instance(self, testdir): - testdir.makepyfile(""" - class TestClass: - def test_method(self): - assert 0 - """) - result, dom = runandparse(testdir) - assert result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, failures=1) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, - classname="test_classname_instance.TestClass", - name="test_method") - - def test_classname_nested_dir(self, testdir): - p = testdir.tmpdir.ensure("sub", "test_hello.py") - p.write("def test_func(): 0/0") - result, dom = runandparse(testdir) - assert result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, failures=1) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, - classname="sub.test_hello", - name="test_func") - - def test_internal_error(self, testdir): - testdir.makeconftest("def pytest_runtest_protocol(): 0 / 0") - testdir.makepyfile("def test_function(): pass") - result, dom = runandparse(testdir) - assert result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, errors=1, tests=0) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, classname="pytest", name="internal") - fnode = tnode.getElementsByTagName("error")[0] - assert_attr(fnode, message="internal error") - assert "Division" in fnode.toxml() - - def test_failure_function(self, testdir): - testdir.makepyfile("def test_fail(): raise ValueError(42)") - result, dom = runandparse(testdir) - assert result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, failures=1, tests=1) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, - classname="test_failure_function", - name="test_fail") - fnode = tnode.getElementsByTagName("failure")[0] - assert_attr(fnode, message="test failure") - assert "ValueError" in fnode.toxml() - - def test_failure_escape(self, testdir): - testdir.makepyfile(""" - def pytest_generate_tests(metafunc): - metafunc.addcall(id="<", funcargs=dict(arg1=42)) - metafunc.addcall(id="&", funcargs=dict(arg1=44)) - def test_func(arg1): - assert 0 - """) - result, dom = runandparse(testdir) - assert result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, failures=2, tests=2) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, - classname="test_failure_escape", - name="test_func[<]") - tnode = node.getElementsByTagName("testcase")[1] - assert_attr(tnode, - classname="test_failure_escape", - name="test_func[&]") - - def test_junit_prefixing(self, testdir): - testdir.makepyfile(""" - def test_func(): - assert 0 - class TestHello: - def test_hello(self): - pass - """) - result, dom = runandparse(testdir, "--junitprefix=xyz") - assert result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, failures=1, tests=2) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, - classname="xyz.test_junit_prefixing", - name="test_func") - tnode = node.getElementsByTagName("testcase")[1] - assert_attr(tnode, - classname="xyz.test_junit_prefixing." - "TestHello", - name="test_hello") - - def test_xfailure_function(self, testdir): - testdir.makepyfile(""" - import py - def test_xfail(): - py.test.xfail("42") - """) - result, dom = runandparse(testdir) - assert not result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, skips=1, tests=0) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, - classname="test_xfailure_function", - name="test_xfail") - fnode = tnode.getElementsByTagName("skipped")[0] - assert_attr(fnode, message="expected test failure") - #assert "ValueError" in fnode.toxml() - - def test_xfailure_xpass(self, testdir): - testdir.makepyfile(""" - import py - @py.test.mark.xfail - def test_xpass(): - pass - """) - result, dom = runandparse(testdir) - #assert result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, skips=1, tests=0) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, - classname="test_xfailure_xpass", - name="test_xpass") - fnode = tnode.getElementsByTagName("skipped")[0] - assert_attr(fnode, message="xfail-marked test passes unexpectedly") - #assert "ValueError" in fnode.toxml() - - def test_collect_error(self, testdir): - testdir.makepyfile("syntax error") - result, dom = runandparse(testdir) - assert result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, errors=1, tests=0) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, - #classname="test_collect_error", - name="test_collect_error") - fnode = tnode.getElementsByTagName("failure")[0] - assert_attr(fnode, message="collection failure") - assert "SyntaxError" in fnode.toxml() - - def test_collect_skipped(self, testdir): - testdir.makepyfile("import py ; py.test.skip('xyz')") - result, dom = runandparse(testdir) - assert not result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, skips=1, tests=0) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, - #classname="test_collect_error", - name="test_collect_skipped") - fnode = tnode.getElementsByTagName("skipped")[0] - assert_attr(fnode, message="collection skipped") - - def test_unicode(self, testdir): - value = 'hx\xc4\x85\xc4\x87\n' - testdir.makepyfile(""" - # coding: utf-8 - def test_hello(): - print (%r) - assert 0 - """ % value) - result, dom = runandparse(testdir) - assert result.ret == 1 - tnode = dom.getElementsByTagName("testcase")[0] - fnode = tnode.getElementsByTagName("failure")[0] - if not sys.platform.startswith("java"): - assert "hx" in fnode.toxml() - -class TestNonPython: - def test_summing_simple(self, testdir): - testdir.makeconftest(""" - import pytest - def pytest_collect_file(path, parent): - if path.ext == ".xyz": - return MyItem(path, parent) - class MyItem(pytest.Item): - def __init__(self, path, parent): - super(MyItem, self).__init__(path.basename, parent) - self.fspath = path - def runtest(self): - raise ValueError(42) - def repr_failure(self, excinfo): - return "custom item runtest failed" - """) - testdir.tmpdir.join("myfile.xyz").write("hello") - result, dom = runandparse(testdir) - assert result.ret - node = dom.getElementsByTagName("testsuite")[0] - assert_attr(node, errors=0, failures=1, skips=0, tests=1) - tnode = node.getElementsByTagName("testcase")[0] - assert_attr(tnode, - #classname="test_collect_error", - name="myfile.xyz") - fnode = tnode.getElementsByTagName("failure")[0] - assert_attr(fnode, message="test failure") - assert "custom item runtest failed" in fnode.toxml() - --- a/testing/plugin/test_tmpdir.py +++ /dev/null @@ -1,29 +0,0 @@ -import py - -from _pytest.tmpdir import pytest_funcarg__tmpdir -from _pytest.python import FuncargRequest - -def test_funcarg(testdir): - item = testdir.getitem(""" - def pytest_generate_tests(metafunc): - metafunc.addcall(id='a') - metafunc.addcall(id='b') - def test_func(tmpdir): pass - """, 'test_func[a]') - p = pytest_funcarg__tmpdir(FuncargRequest(item)) - assert p.check() - bn = p.basename.strip("0123456789") - assert bn.endswith("test_func_a_") - item.name = "qwe/\\abc" - p = pytest_funcarg__tmpdir(FuncargRequest(item)) - assert p.check() - bn = p.basename.strip("0123456789") - assert bn == "qwe__abc" - -def test_ensuretemp(recwarn): - #py.test.deprecated_call(py.test.ensuretemp, 'hello') - d1 = py.test.ensuretemp('hello') - d2 = py.test.ensuretemp('hello') - assert d1 == d2 - assert d1.check(dir=1) - --- a/testing/plugin/test_resultlog.py +++ /dev/null @@ -1,193 +0,0 @@ -import py -import os -from _pytest.resultlog import generic_path, ResultLog, \ - pytest_configure, pytest_unconfigure -from _pytest.session import Node, Item, FSCollector - -def test_generic_path(testdir): - from _pytest.session import Session - config = testdir.parseconfig() - session = Session(config) - p1 = Node('a', config=config, session=session) - #assert p1.fspath is None - p2 = Node('B', parent=p1) - p3 = Node('()', parent = p2) - item = Item('c', parent = p3) - - res = generic_path(item) - assert res == 'a.B().c' - - p0 = FSCollector('proj/test', config=config, session=session) - p1 = FSCollector('proj/test/a', parent=p0) - p2 = Node('B', parent=p1) - p3 = Node('()', parent = p2) - p4 = Node('c', parent=p3) - item = Item('[1]', parent = p4) - - res = generic_path(item) - assert res == 'test/a:B().c[1]' - -def test_write_log_entry(): - reslog = ResultLog(None, None) - reslog.logfile = py.io.TextIO() - reslog.write_log_entry('name', '.', '') - entry = reslog.logfile.getvalue() - assert entry[-1] == '\n' - entry_lines = entry.splitlines() - assert len(entry_lines) == 1 - assert entry_lines[0] == '. name' - - reslog.logfile = py.io.TextIO() - reslog.write_log_entry('name', 's', 'Skipped') - entry = reslog.logfile.getvalue() - assert entry[-1] == '\n' - entry_lines = entry.splitlines() - assert len(entry_lines) == 2 - assert entry_lines[0] == 's name' - assert entry_lines[1] == ' Skipped' - - reslog.logfile = py.io.TextIO() - reslog.write_log_entry('name', 's', 'Skipped\n') - entry = reslog.logfile.getvalue() - assert entry[-1] == '\n' - entry_lines = entry.splitlines() - assert len(entry_lines) == 2 - assert entry_lines[0] == 's name' - assert entry_lines[1] == ' Skipped' - - reslog.logfile = py.io.TextIO() - longrepr = ' tb1\n tb 2\nE tb3\nSome Error' - reslog.write_log_entry('name', 'F', longrepr) - entry = reslog.logfile.getvalue() - assert entry[-1] == '\n' - entry_lines = entry.splitlines() - assert len(entry_lines) == 5 - assert entry_lines[0] == 'F name' - assert entry_lines[1:] == [' '+line for line in longrepr.splitlines()] - - -class TestWithFunctionIntegration: - # XXX (hpk) i think that the resultlog plugin should - # provide a Parser object so that one can remain - # ignorant regarding formatting details. - def getresultlog(self, testdir, arg): - resultlog = testdir.tmpdir.join("resultlog") - testdir.plugins.append("resultlog") - args = ["--resultlog=%s" % resultlog] + [arg] - testdir.runpytest(*args) - return [x for x in resultlog.readlines(cr=0) if x] - - def test_collection_report(self, testdir): - ok = testdir.makepyfile(test_collection_ok="") - skip = testdir.makepyfile(test_collection_skip="import py ; py.test.skip('hello')") - fail = testdir.makepyfile(test_collection_fail="XXX") - lines = self.getresultlog(testdir, ok) - assert not lines - - lines = self.getresultlog(testdir, skip) - assert len(lines) == 2 - assert lines[0].startswith("S ") - assert lines[0].endswith("test_collection_skip.py") - assert lines[1].startswith(" ") - assert lines[1].endswith("test_collection_skip.py:1: Skipped: hello") - - lines = self.getresultlog(testdir, fail) - assert lines - assert lines[0].startswith("F ") - assert lines[0].endswith("test_collection_fail.py"), lines[0] - for x in lines[1:]: - assert x.startswith(" ") - assert "XXX" in "".join(lines[1:]) - - def test_log_test_outcomes(self, testdir): - mod = testdir.makepyfile(test_mod=""" - import py - def test_pass(): pass - def test_skip(): py.test.skip("hello") - def test_fail(): raise ValueError("FAIL") - - @py.test.mark.xfail - def test_xfail(): raise ValueError("XFAIL") - @py.test.mark.xfail - def test_xpass(): pass - - """) - lines = self.getresultlog(testdir, mod) - assert len(lines) >= 3 - assert lines[0].startswith(". ") - assert lines[0].endswith("test_pass") - assert lines[1].startswith("s "), lines[1] - assert lines[1].endswith("test_skip") - assert lines[2].find("hello") != -1 - - assert lines[3].startswith("F ") - assert lines[3].endswith("test_fail") - tb = "".join(lines[4:8]) - assert tb.find('raise ValueError("FAIL")') != -1 - - assert lines[8].startswith('x ') - tb = "".join(lines[8:14]) - assert tb.find('raise ValueError("XFAIL")') != -1 - - assert lines[14].startswith('X ') - assert len(lines) == 15 - - def test_internal_exception(self): - # they are produced for example by a teardown failing - # at the end of the run - try: - raise ValueError - except ValueError: - excinfo = py.code.ExceptionInfo() - reslog = ResultLog(None, py.io.TextIO()) - reslog.pytest_internalerror(excinfo.getrepr()) - entry = reslog.logfile.getvalue() - entry_lines = entry.splitlines() - - assert entry_lines[0].startswith('! ') - assert os.path.basename(__file__)[:-9] in entry_lines[0] #.pyc/class - assert entry_lines[-1][0] == ' ' - assert 'ValueError' in entry - -def test_generic(testdir, LineMatcher): - testdir.plugins.append("resultlog") - testdir.makepyfile(""" - import py - def test_pass(): - pass - def test_fail(): - assert 0 - def test_skip(): - py.test.skip("") - @py.test.mark.xfail - def test_xfail(): - assert 0 - @py.test.mark.xfail(run=False) - def test_xfail_norun(): - assert 0 - """) - testdir.runpytest("--resultlog=result.log") - lines = testdir.tmpdir.join("result.log").readlines(cr=0) - LineMatcher(lines).fnmatch_lines([ - ". *:test_pass", - "F *:test_fail", - "s *:test_skip", - "x *:test_xfail", - "x *:test_xfail_norun", - ]) - -def test_no_resultlog_on_slaves(testdir): - config = testdir.parseconfig("-p", "resultlog", "--resultlog=resultlog") - - assert not hasattr(config, '_resultlog') - pytest_configure(config) - assert hasattr(config, '_resultlog') - pytest_unconfigure(config) - assert not hasattr(config, '_resultlog') - - config.slaveinput = {} - pytest_configure(config) - assert not hasattr(config, '_resultlog') - pytest_unconfigure(config) - assert not hasattr(config, '_resultlog') - --- a/testing/plugin/test_runner_xunit.py +++ /dev/null @@ -1,212 +0,0 @@ -# -# test correct setup/teardowns at -# module, class, and instance level - -def test_module_and_function_setup(testdir): - reprec = testdir.inline_runsource(""" - modlevel = [] - def setup_module(module): - assert not modlevel - module.modlevel.append(42) - - def teardown_module(module): - modlevel.pop() - - def setup_function(function): - function.answer = 17 - - def teardown_function(function): - del function.answer - - def test_modlevel(): - assert modlevel[0] == 42 - assert test_modlevel.answer == 17 - - class TestFromClass: - def test_module(self): - assert modlevel[0] == 42 - assert not hasattr(test_modlevel, 'answer') - """) - rep = reprec.matchreport("test_modlevel") - assert rep.passed - rep = reprec.matchreport("test_module") - assert rep.passed - -def test_class_setup(testdir): - reprec = testdir.inline_runsource(""" - class TestSimpleClassSetup: - clslevel = [] - def setup_class(cls): - cls.clslevel.append(23) - - def teardown_class(cls): - cls.clslevel.pop() - - def test_classlevel(self): - assert self.clslevel[0] == 23 - - class TestInheritedClassSetupStillWorks(TestSimpleClassSetup): - def test_classlevel_anothertime(self): - assert self.clslevel == [23] - - def test_cleanup(): - assert not TestSimpleClassSetup.clslevel - assert not TestInheritedClassSetupStillWorks.clslevel - """) - reprec.assertoutcome(passed=1+2+1) - - -def test_method_setup(testdir): - reprec = testdir.inline_runsource(""" - class TestSetupMethod: - def setup_method(self, meth): - self.methsetup = meth - def teardown_method(self, meth): - del self.methsetup - - def test_some(self): - assert self.methsetup == self.test_some - - def test_other(self): - assert self.methsetup == self.test_other - """) - reprec.assertoutcome(passed=2) - -def test_method_generator_setup(testdir): - reprec = testdir.inline_runsource(""" - class TestSetupTeardownOnInstance: - def setup_class(cls): - cls.classsetup = True - - def setup_method(self, method): - self.methsetup = method - - def test_generate(self): - assert self.classsetup - assert self.methsetup == self.test_generate - yield self.generated, 5 - yield self.generated, 2 - - def generated(self, value): - assert self.classsetup - assert self.methsetup == self.test_generate - assert value == 5 - """) - reprec.assertoutcome(passed=1, failed=1) - -def test_func_generator_setup(testdir): - reprec = testdir.inline_runsource(""" - import sys - - def setup_module(mod): - print ("setup_module") - mod.x = [] - - def setup_function(fun): - print ("setup_function") - x.append(1) - - def teardown_function(fun): - print ("teardown_function") - x.pop() - - def test_one(): - assert x == [1] - def check(): - print ("check") - sys.stderr.write("e\\n") - assert x == [1] - yield check - assert x == [1] - """) - rep = reprec.matchreport("test_one", names="pytest_runtest_logreport") - assert rep.passed - -def test_method_setup_uses_fresh_instances(testdir): - reprec = testdir.inline_runsource(""" - class TestSelfState1: - memory = [] - def test_hello(self): - self.memory.append(self) - - def test_afterhello(self): - assert self != self.memory[0] - """) - reprec.assertoutcome(passed=2, failed=0) - -def test_failing_setup_calls_teardown(testdir): - p = testdir.makepyfile(""" - def setup_module(mod): - raise ValueError(42) - def test_function(): - assert 0 - def teardown_module(mod): - raise ValueError(43) - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*42*", - "*43*", - "*2 error*" - ]) - -def test_setup_that_skips_calledagain_and_teardown(testdir): - p = testdir.makepyfile(""" - import py - def setup_module(mod): - py.test.skip("x") - def test_function1(): - pass - def test_function2(): - pass - def teardown_module(mod): - raise ValueError(43) - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*ValueError*43*", - "*2 skipped*1 error*", - ]) - -def test_setup_fails_again_on_all_tests(testdir): - p = testdir.makepyfile(""" - import py - def setup_module(mod): - raise ValueError(42) - def test_function1(): - pass - def test_function2(): - pass - def teardown_module(mod): - raise ValueError(43) - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*3 error*" - ]) - assert "passed" not in result.stdout.str() - -def test_setup_funcarg_setup_not_called_if_outer_scope_fails(testdir): - p = testdir.makepyfile(""" - import py - def setup_module(mod): - raise ValueError(42) - def pytest_funcarg__hello(request): - raise ValueError("xyz43") - def test_function1(hello): - pass - def test_function2(hello): - pass - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*function1*", - "*ValueError*42*", - "*function2*", - "*ValueError*42*", - "*2 error*" - ]) - assert "xyz43" not in result.stdout.str() - - - --- /dev/null +++ b/testing/test_recwarn.py @@ -0,0 +1,81 @@ +import py +from _pytest.recwarn import WarningsRecorder + +def test_WarningRecorder(recwarn): + showwarning = py.std.warnings.showwarning + rec = WarningsRecorder() + assert py.std.warnings.showwarning != showwarning + assert not rec.list + py.std.warnings.warn_explicit("hello", UserWarning, "xyz", 13) + assert len(rec.list) == 1 + py.std.warnings.warn(DeprecationWarning("hello")) + assert len(rec.list) == 2 + warn = rec.pop() + assert str(warn.message) == "hello" + l = rec.list + rec.clear() + assert len(rec.list) == 0 + assert l is rec.list + py.test.raises(AssertionError, "rec.pop()") + rec.finalize() + assert showwarning == py.std.warnings.showwarning + +def test_recwarn_functional(testdir): + reprec = testdir.inline_runsource(""" + pytest_plugins = 'pytest_recwarn', + import warnings + oldwarn = warnings.showwarning + def test_method(recwarn): + assert warnings.showwarning != oldwarn + warnings.warn("hello") + warn = recwarn.pop() + assert isinstance(warn.message, UserWarning) + def test_finalized(): + assert warnings.showwarning == oldwarn + """) + res = reprec.countoutcomes() + assert tuple(res) == (2, 0, 0), res + +# +# ============ test py.test.deprecated_call() ============== +# + +def dep(i): + if i == 0: + py.std.warnings.warn("is deprecated", DeprecationWarning) + return 42 + +reg = {} +def dep_explicit(i): + if i == 0: + py.std.warnings.warn_explicit("dep_explicit", category=DeprecationWarning, + filename="hello", lineno=3) + +def test_deprecated_call_raises(): + excinfo = py.test.raises(AssertionError, + "py.test.deprecated_call(dep, 3)") + assert str(excinfo).find("did not produce") != -1 + +def test_deprecated_call(): + py.test.deprecated_call(dep, 0) + +def test_deprecated_call_ret(): + ret = py.test.deprecated_call(dep, 0) + assert ret == 42 + +def test_deprecated_call_preserves(): + r = py.std.warnings.onceregistry.copy() + f = py.std.warnings.filters[:] + test_deprecated_call_raises() + test_deprecated_call() + assert r == py.std.warnings.onceregistry + assert f == py.std.warnings.filters + +def test_deprecated_explicit_call_raises(): + py.test.raises(AssertionError, + "py.test.deprecated_call(dep_explicit, 3)") + +def test_deprecated_explicit_call(): + py.test.deprecated_call(dep_explicit, 0) + py.test.deprecated_call(dep_explicit, 0) + --- /dev/null +++ b/testing/test_runner.py @@ -0,0 +1,387 @@ +import py, sys +from _pytest import runner +from py._code.code import ReprExceptionInfo + +class TestSetupState: + def test_setup(self, testdir): + ss = runner.SetupState() + item = testdir.getitem("def test_func(): pass") + l = [1] + ss.prepare(item) + ss.addfinalizer(l.pop, colitem=item) + assert l + ss._pop_and_teardown() + assert not l + + def test_setup_scope_None(self, testdir): + item = testdir.getitem("def test_func(): pass") + ss = runner.SetupState() + l = [1] + ss.prepare(item) + ss.addfinalizer(l.pop, colitem=None) + assert l + ss._pop_and_teardown() + assert l + ss._pop_and_teardown() + assert l + ss.teardown_all() + assert not l + + def test_teardown_exact_stack_empty(self, testdir): + item = testdir.getitem("def test_func(): pass") + ss = runner.SetupState() + ss.teardown_exact(item) + ss.teardown_exact(item) + ss.teardown_exact(item) + + def test_setup_fails_and_failure_is_cached(self, testdir): + item = testdir.getitem(""" + def setup_module(mod): + raise ValueError(42) + def test_func(): pass + """) + ss = runner.SetupState() + py.test.raises(ValueError, "ss.prepare(item)") + py.test.raises(ValueError, "ss.prepare(item)") + +class BaseFunctionalTests: + def test_passfunction(self, testdir): + reports = testdir.runitem(""" + def test_func(): + pass + """) + rep = reports[1] + assert rep.passed + assert not rep.failed + assert rep.outcome == "passed" + assert not rep.longrepr + + def test_failfunction(self, testdir): + reports = testdir.runitem(""" + def test_func(): + assert 0 + """) + rep = reports[1] + assert not rep.passed + assert not rep.skipped + assert rep.failed + assert rep.when == "call" + assert rep.outcome == "failed" + #assert isinstance(rep.longrepr, ReprExceptionInfo) + + def test_skipfunction(self, testdir): + reports = testdir.runitem(""" + import py + def test_func(): + py.test.skip("hello") + """) + rep = reports[1] + assert not rep.failed + assert not rep.passed + assert rep.skipped + assert rep.outcome == "skipped" + #assert rep.skipped.when == "call" + #assert rep.skipped.when == "call" + #assert rep.skipped == "%sreason == "hello" + #assert rep.skipped.location.lineno == 3 + #assert rep.skipped.location.path + #assert not rep.skipped.failurerepr + + def test_skip_in_setup_function(self, testdir): + reports = testdir.runitem(""" + import py + def setup_function(func): + py.test.skip("hello") + def test_func(): + pass + """) + print(reports) + rep = reports[0] + assert not rep.failed + assert not rep.passed + assert rep.skipped + #assert rep.skipped.reason == "hello" + #assert rep.skipped.location.lineno == 3 + #assert rep.skipped.location.lineno == 3 + assert len(reports) == 2 + assert reports[1].passed # teardown + + def test_failure_in_setup_function(self, testdir): + reports = testdir.runitem(""" + import py + def setup_function(func): + raise ValueError(42) + def test_func(): + pass + """) + rep = reports[0] + assert not rep.skipped + assert not rep.passed + assert rep.failed + assert rep.when == "setup" + assert len(reports) == 2 + + def test_failure_in_teardown_function(self, testdir): + reports = testdir.runitem(""" + import py + def teardown_function(func): + raise ValueError(42) + def test_func(): + pass + """) + print(reports) + assert len(reports) == 3 + rep = reports[2] + assert not rep.skipped + assert not rep.passed + assert rep.failed + assert rep.when == "teardown" + #assert rep.longrepr.reprcrash.lineno == 3 + #assert rep.longrepr.reprtraceback.reprentries + + def test_custom_failure_repr(self, testdir): + testdir.makepyfile(conftest=""" + import pytest + class Function(pytest.Function): + def repr_failure(self, excinfo): + return "hello" + """) + reports = testdir.runitem(""" + import py + def test_func(): + assert 0 + """) + rep = reports[1] + assert not rep.skipped + assert not rep.passed + assert rep.failed + #assert rep.outcome.when == "call" + #assert rep.failed.where.lineno == 3 + #assert rep.failed.where.path.basename == "test_func.py" + #assert rep.failed.failurerepr == "hello" + + def test_failure_in_setup_function_ignores_custom_repr(self, testdir): + testdir.makepyfile(conftest=""" + import pytest + class Function(pytest.Function): + def repr_failure(self, excinfo): + assert 0 + """) + reports = testdir.runitem(""" + def setup_function(func): + raise ValueError(42) + def test_func(): + pass + """) + assert len(reports) == 2 + rep = reports[0] + print(rep) + assert not rep.skipped + assert not rep.passed + assert rep.failed + #assert rep.outcome.when == "setup" + #assert rep.outcome.where.lineno == 3 + #assert rep.outcome.where.path.basename == "test_func.py" + #assert instanace(rep.failed.failurerepr, PythonFailureRepr) + + def test_systemexit_does_not_bail_out(self, testdir): + try: + reports = testdir.runitem(""" + def test_func(): + raise SystemExit(42) + """) + except SystemExit: + py.test.fail("runner did not catch SystemExit") + rep = reports[1] + assert rep.failed + assert rep.when == "call" + + def test_exit_propagates(self, testdir): + try: + testdir.runitem(""" + import pytest + def test_func(): + raise pytest.exit.Exception() + """) + except py.test.exit.Exception: + pass + else: + py.test.fail("did not raise") + +class TestExecutionNonForked(BaseFunctionalTests): + def getrunner(self): + def f(item): + return runner.runtestprotocol(item, log=False) + return f + + def test_keyboardinterrupt_propagates(self, testdir): + try: + testdir.runitem(""" + def test_func(): + raise KeyboardInterrupt("fake") + """) + except KeyboardInterrupt: + pass + else: + py.test.fail("did not raise") + +class TestExecutionForked(BaseFunctionalTests): + pytestmark = py.test.mark.skipif("not hasattr(os, 'fork')") + + def getrunner(self): + # XXX re-arrange this test to live in pytest-xdist + xplugin = py.test.importorskip("xdist.plugin") + return xplugin.forked_run_report + + def test_suicide(self, testdir): + reports = testdir.runitem(""" + def test_func(): + import os + os.kill(os.getpid(), 15) + """) + rep = reports[0] + assert rep.failed + assert rep.when == "???" + +class TestSessionReports: + def test_collect_result(self, testdir): + col = testdir.getmodulecol(""" + def test_func1(): + pass + class TestClass: + pass + """) + rep = runner.pytest_make_collect_report(col) + assert not rep.failed + assert not rep.skipped + assert rep.passed + locinfo = rep.location + assert locinfo[0] == col.fspath.basename + assert not locinfo[1] + assert locinfo[2] == col.fspath.basename + res = rep.result + assert len(res) == 2 + assert res[0].name == "test_func1" + assert res[1].name == "TestClass" + + def test_skip_at_module_scope(self, testdir): + col = testdir.getmodulecol(""" + import pytest + pytest.skip("hello") + def test_func(): + pass + """) + rep = runner.pytest_make_collect_report(col) + assert not rep.failed + assert not rep.passed + assert rep.skipped + +def test_callinfo(): + ci = runner.CallInfo(lambda: 0, '123') + assert ci.when == "123" + assert ci.result == 0 + assert "result" in repr(ci) + ci = runner.CallInfo(lambda: 0/0, '123') + assert ci.when == "123" + assert not hasattr(ci, 'result') + assert ci.excinfo + assert "exc" in repr(ci) + +# design question: do we want general hooks in python files? +# then something like the following functional tests makes sense + at py.test.mark.xfail +def test_runtest_in_module_ordering(testdir): + p1 = testdir.makepyfile(""" + def pytest_runtest_setup(item): # runs after class-level! + item.function.mylist.append("module") + class TestClass: + def pytest_runtest_setup(self, item): + assert not hasattr(item.function, 'mylist') + item.function.mylist = ['class'] + def pytest_funcarg__mylist(self, request): + return request.function.mylist + def pytest_runtest_call(self, item, __multicall__): + try: + __multicall__.execute() + except ValueError: + pass + def test_hello1(self, mylist): + assert mylist == ['class', 'module'], mylist + raise ValueError() + def test_hello2(self, mylist): + assert mylist == ['class', 'module'], mylist + def pytest_runtest_teardown(item): + del item.function.mylist + """) + result = testdir.runpytest(p1) + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) + + +def test_pytest_exit(): + try: + py.test.exit("hello") + except py.test.exit.Exception: + excinfo = py.code.ExceptionInfo() + assert excinfo.errisinstance(KeyboardInterrupt) + +def test_pytest_fail(): + try: + py.test.fail("hello") + except py.test.fail.Exception: + excinfo = py.code.ExceptionInfo() + s = excinfo.exconly(tryshort=True) + assert s.startswith("Failed") + +def test_exception_printing_skip(): + try: + py.test.skip("hello") + except py.test.skip.Exception: + excinfo = py.code.ExceptionInfo() + s = excinfo.exconly(tryshort=True) + assert s.startswith("Skipped") + +def test_importorskip(): + importorskip = py.test.importorskip + try: + sys = importorskip("sys") + assert sys == py.std.sys + #path = py.test.importorskip("os.path") + #assert path == py.std.os.path + py.test.raises(py.test.skip.Exception, + "py.test.importorskip('alskdj')") + py.test.raises(SyntaxError, "py.test.importorskip('x y z')") + py.test.raises(SyntaxError, "py.test.importorskip('x=y')") + path = importorskip("py", minversion=".".join(py.__version__)) + mod = py.std.types.ModuleType("hello123") + mod.__version__ = "1.3" + py.test.raises(py.test.skip.Exception, """ + py.test.importorskip("hello123", minversion="5.0") + """) + except py.test.skip.Exception: + print(py.code.ExceptionInfo()) + py.test.fail("spurious skip") + +def test_importorskip_imports_last_module_part(): + import os + ospath = py.test.importorskip("os.path") + assert os.path == ospath + + +def test_pytest_cmdline_main(testdir): + p = testdir.makepyfile(""" + import sys + sys.path.insert(0, %r) + import py + def test_hello(): + assert 1 + if __name__ == '__main__': + py.test.cmdline.main([__file__]) + """ % (str(py._pydir.dirpath()))) + import subprocess + popen = subprocess.Popen([sys.executable, str(p)], stdout=subprocess.PIPE) + s = popen.stdout.read() + ret = popen.wait() + assert ret == 0 + --- a/testing/plugin/test_helpconfig.py +++ /dev/null @@ -1,53 +0,0 @@ -import py, pytest,os -from _pytest.helpconfig import collectattr - -def test_version(testdir): - result = testdir.runpytest("--version") - assert result.ret == 0 - #p = py.path.local(py.__file__).dirpath() - result.stderr.fnmatch_lines([ - '*py.test*%s*imported from*' % (pytest.__version__, ) - ]) - -def test_help(testdir): - result = testdir.runpytest("--help") - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*-v*verbose*", - "*setup.cfg*", - "*minversion*", - ]) - -def test_collectattr(): - class A: - def pytest_hello(self): - pass - class B(A): - def pytest_world(self): - pass - methods = py.builtin.sorted(collectattr(B)) - assert list(methods) == ['pytest_hello', 'pytest_world'] - methods = py.builtin.sorted(collectattr(B())) - assert list(methods) == ['pytest_hello', 'pytest_world'] - -def test_hookvalidation_unknown(testdir): - testdir.makeconftest(""" - def pytest_hello(xyz): - pass - """) - result = testdir.runpytest() - assert result.ret != 0 - result.stderr.fnmatch_lines([ - '*unknown hook*pytest_hello*' - ]) - -def test_hookvalidation_optional(testdir): - testdir.makeconftest(""" - import py - @py.test.mark.optionalhook - def pytest_hello(xyz): - pass - """) - result = testdir.runpytest() - assert result.ret == 0 - --- a/testing/plugin/__init__.py +++ /dev/null @@ -1,1 +0,0 @@ -# --- /dev/null +++ b/testing/test_assertion.py @@ -0,0 +1,203 @@ +import sys + +import py +import _pytest.assertion as plugin + +needsnewassert = py.test.mark.skipif("sys.version_info < (2,6)") + +def interpret(expr): + return py.code._reinterpret(expr, py.code.Frame(sys._getframe(1))) + +class TestBinReprIntegration: + pytestmark = needsnewassert + + def pytest_funcarg__hook(self, request): + class MockHook(object): + def __init__(self): + self.called = False + self.args = tuple() + self.kwargs = dict() + + def __call__(self, op, left, right): + self.called = True + self.op = op + self.left = left + self.right = right + mockhook = MockHook() + monkeypatch = request.getfuncargvalue("monkeypatch") + monkeypatch.setattr(py.code, '_reprcompare', mockhook) + return mockhook + + def test_pytest_assertrepr_compare_called(self, hook): + interpret('assert 0 == 1') + assert hook.called + + + def test_pytest_assertrepr_compare_args(self, hook): + interpret('assert [0, 1] == [0, 2]') + assert hook.op == '==' + assert hook.left == [0, 1] + assert hook.right == [0, 2] + + def test_configure_unconfigure(self, testdir, hook): + assert hook == py.code._reprcompare + config = testdir.parseconfig() + plugin.pytest_configure(config) + assert hook != py.code._reprcompare + plugin.pytest_unconfigure(config) + assert hook == py.code._reprcompare + +def callequal(left, right): + return plugin.pytest_assertrepr_compare('==', left, right) + +class TestAssert_reprcompare: + def test_different_types(self): + assert callequal([0, 1], 'foo') is None + + def test_summary(self): + summary = callequal([0, 1], [0, 2])[0] + assert len(summary) < 65 + + def test_text_diff(self): + diff = callequal('spam', 'eggs')[1:] + assert '- spam' in diff + assert '+ eggs' in diff + + def test_multiline_text_diff(self): + left = 'foo\nspam\nbar' + right = 'foo\neggs\nbar' + diff = callequal(left, right) + assert '- spam' in diff + assert '+ eggs' in diff + + def test_list(self): + expl = callequal([0, 1], [0, 2]) + assert len(expl) > 1 + + def test_list_different_lenghts(self): + expl = callequal([0, 1], [0, 1, 2]) + assert len(expl) > 1 + expl = callequal([0, 1, 2], [0, 1]) + assert len(expl) > 1 + + def test_dict(self): + expl = callequal({'a': 0}, {'a': 1}) + assert len(expl) > 1 + + def test_set(self): + expl = callequal(set([0, 1]), set([0, 2])) + assert len(expl) > 1 + + def test_list_tuples(self): + expl = callequal([], [(1,2)]) + assert len(expl) > 1 + expl = callequal([(1,2)], []) + assert len(expl) > 1 + + def test_list_bad_repr(self): + class A: + def __repr__(self): + raise ValueError(42) + expl = callequal([], [A()]) + assert 'ValueError' in "".join(expl) + expl = callequal({}, {'1': A()}) + assert 'faulty' in "".join(expl) + + def test_one_repr_empty(self): + """ + the faulty empty string repr did trigger + a unbound local error in _diff_text + """ + class A(str): + def __repr__(self): + return '' + expl = callequal(A(), '') + assert not expl + + at needsnewassert +def test_pytest_assertrepr_compare_integration(testdir): + testdir.makepyfile(""" + def test_hello(): + x = set(range(100)) + y = x.copy() + y.remove(50) + assert x == y + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*def test_hello():*", + "*assert x == y*", + "*E*Extra items*left*", + "*E*50*", + ]) + + at needsnewassert +def test_sequence_comparison_uses_repr(testdir): + testdir.makepyfile(""" + def test_hello(): + x = set("hello x") + y = set("hello y") + assert x == y + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*def test_hello():*", + "*assert x == y*", + "*E*Extra items*left*", + "*E*'x'*", + "*E*Extra items*right*", + "*E*'y'*", + ]) + + +def test_functional(testdir): + testdir.makepyfile(""" + def test_hello(): + x = 3 + assert x == 4 + """) + result = testdir.runpytest() + assert "3 == 4" in result.stdout.str() + result = testdir.runpytest("--no-assert") + assert "3 == 4" not in result.stdout.str() + +def test_triple_quoted_string_issue113(testdir): + testdir.makepyfile(""" + def test_hello(): + assert "" == ''' + '''""") + result = testdir.runpytest("--fulltrace") + result.stdout.fnmatch_lines([ + "*1 failed*", + ]) + assert 'SyntaxError' not in result.stdout.str() + +def test_traceback_failure(testdir): + p1 = testdir.makepyfile(""" + def g(): + return 2 + def f(x): + assert x == g() + def test_onefails(): + f(3) + """) + result = testdir.runpytest(p1) + result.stdout.fnmatch_lines([ + "*test_traceback_failure.py F", + "====* FAILURES *====", + "____*____", + "", + " def test_onefails():", + "> f(3)", + "", + "*test_*.py:6: ", + "_ _ _ *", + #"", + " def f(x):", + "> assert x == g()", + "E assert 3 == 2", + "E + where 2 = g()", + "", + "*test_traceback_failure.py:4: AssertionError" + ]) + --- a/testing/plugin/conftest.py +++ /dev/null @@ -1,36 +0,0 @@ -import py - -import _pytest -plugindir = py.path.local(_pytest.__file__).dirpath() -from _pytest.core import default_plugins - -def pytest_collect_file(path, parent): - if path.basename.startswith("pytest_") and path.ext == ".py": - mod = parent.Module(path, parent=parent) - return mod - -# for plugin test we try to automatically make sure that -# the according plugin is loaded -def pytest_funcarg__testdir(request): - testdir = request.getfuncargvalue("testdir") - #for obj in (request.cls, request.module): - # if hasattr(obj, 'testplugin'): - # testdir.plugins.append(obj.testplugin) - # break - #else: - modname = request.module.__name__.split(".")[-1] - if modname.startswith("test_pytest_"): - modname = modname[5:] - if plugindir.join("%s.py" % modname).check(): - if modname[7:] not in default_plugins: - testdir.plugins.append(vars(request.module)) - testdir.plugins.append(modname) - #elif modname.startswith("test_pytest"): - # pname = modname[5:] - # assert pname not in testdir.plugins - # testdir.plugins.append(pname) - # #testdir.plugins.append(vars(request.module)) - else: - pass # raise ValueError("need better support code") - return testdir - --- /dev/null +++ b/testing/test_session.py @@ -0,0 +1,227 @@ +import pytest, py + +class SessionTests: + def test_basic_testitem_events(self, testdir): + tfile = testdir.makepyfile(""" + def test_one(): + pass + def test_one_one(): + assert 0 + def test_other(): + raise ValueError(23) + def test_two(someargs): + pass + """) + reprec = testdir.inline_run(tfile) + passed, skipped, failed = reprec.listoutcomes() + assert len(skipped) == 0 + assert len(passed) == 1 + assert len(failed) == 3 + end = lambda x: x.nodeid.split("::")[-1] + assert end(failed[0]) == "test_one_one" + assert end(failed[1]) == "test_other" + assert end(failed[2]) == "test_two" + itemstarted = reprec.getcalls("pytest_itemcollected") + assert len(itemstarted) == 4 + colstarted = reprec.getcalls("pytest_collectstart") + assert len(colstarted) == 1 + 1 + col = colstarted[1].collector + assert isinstance(col, pytest.Module) + + def test_nested_import_error(self, testdir): + tfile = testdir.makepyfile(""" + import import_fails + def test_this(): + assert import_fails.a == 1 + """, import_fails=""" + import does_not_work + a = 1 + """) + reprec = testdir.inline_run(tfile) + l = reprec.getfailedcollections() + assert len(l) == 1 + out = l[0].longrepr.reprcrash.message + assert out.find('does_not_work') != -1 + + def test_raises_output(self, testdir): + reprec = testdir.inline_runsource(""" + import py + def test_raises_doesnt(): + py.test.raises(ValueError, int, "3") + """) + passed, skipped, failed = reprec.listoutcomes() + assert len(failed) == 1 + out = failed[0].longrepr.reprcrash.message + if not out.find("DID NOT RAISE") != -1: + print(out) + py.test.fail("incorrect raises() output") + + def test_generator_yields_None(self, testdir): + reprec = testdir.inline_runsource(""" + def test_1(): + yield None + """) + failures = reprec.getfailedcollections() + out = failures[0].longrepr.reprcrash.message + i = out.find('TypeError') + assert i != -1 + + def test_syntax_error_module(self, testdir): + reprec = testdir.inline_runsource("this is really not python") + l = reprec.getfailedcollections() + assert len(l) == 1 + out = str(l[0].longrepr) + assert out.find(str('not python')) != -1 + + def test_exit_first_problem(self, testdir): + reprec = testdir.inline_runsource(""" + def test_one(): assert 0 + def test_two(): assert 0 + """, '--exitfirst') + passed, skipped, failed = reprec.countoutcomes() + assert failed == 1 + assert passed == skipped == 0 + + def test_maxfail(self, testdir): + reprec = testdir.inline_runsource(""" + def test_one(): assert 0 + def test_two(): assert 0 + def test_three(): assert 0 + """, '--maxfail=2') + passed, skipped, failed = reprec.countoutcomes() + assert failed == 2 + assert passed == skipped == 0 + + def test_broken_repr(self, testdir): + p = testdir.makepyfile(""" + import pytest + class BrokenRepr1: + foo=0 + def __repr__(self): + raise Exception("Ha Ha fooled you, I'm a broken repr().") + + class TestBrokenClass: + def test_explicit_bad_repr(self): + t = BrokenRepr1() + pytest.raises(Exception, 'repr(t)') + + def test_implicit_bad_repr1(self): + t = BrokenRepr1() + assert t.foo == 1 + + """) + reprec = testdir.inline_run(p) + passed, skipped, failed = reprec.listoutcomes() + assert len(failed) == 1 + out = failed[0].longrepr.reprcrash.message + assert out.find("""[Exception("Ha Ha fooled you, I'm a broken repr().") raised in repr()]""") != -1 #' + + def test_skip_file_by_conftest(self, testdir): + testdir.makepyfile(conftest=""" + import py + def pytest_collect_file(): + py.test.skip("intentional") + """, test_file=""" + def test_one(): pass + """) + try: + reprec = testdir.inline_run(testdir.tmpdir) + except py.test.skip.Exception: + py.test.fail("wrong skipped caught") + reports = reprec.getreports("pytest_collectreport") + assert len(reports) == 1 + assert reports[0].skipped + +class TestNewSession(SessionTests): + + def test_order_of_execution(self, testdir): + reprec = testdir.inline_runsource(""" + l = [] + def test_1(): + l.append(1) + def test_2(): + l.append(2) + def test_3(): + assert l == [1,2] + class Testmygroup: + reslist = l + def test_1(self): + self.reslist.append(1) + def test_2(self): + self.reslist.append(2) + def test_3(self): + self.reslist.append(3) + def test_4(self): + assert self.reslist == [1,2,1,2,3] + """) + passed, skipped, failed = reprec.countoutcomes() + assert failed == skipped == 0 + assert passed == 7 + # also test listnames() here ... + + def test_collect_only_with_various_situations(self, testdir): + p = testdir.makepyfile( + test_one=""" + def test_one(): + raise ValueError() + + class TestX: + def test_method_one(self): + pass + + class TestY(TestX): + pass + """, + test_two=""" + import py + py.test.skip('xxx') + """, + test_three="xxxdsadsadsadsa", + __init__="" + ) + reprec = testdir.inline_run('--collectonly', p.dirpath()) + + itemstarted = reprec.getcalls("pytest_itemcollected") + assert len(itemstarted) == 3 + assert not reprec.getreports("pytest_runtest_logreport") + started = reprec.getcalls("pytest_collectstart") + finished = reprec.getreports("pytest_collectreport") + assert len(started) == len(finished) + assert len(started) == 8 # XXX extra TopCollector + colfail = [x for x in finished if x.failed] + colskipped = [x for x in finished if x.skipped] + assert len(colfail) == 1 + assert len(colskipped) == 1 + + def test_minus_x_import_error(self, testdir): + testdir.makepyfile(__init__="") + testdir.makepyfile(test_one="xxxx", test_two="yyyy") + reprec = testdir.inline_run("-x", testdir.tmpdir) + finished = reprec.getreports("pytest_collectreport") + colfail = [x for x in finished if x.failed] + assert len(colfail) == 1 + + +def test_plugin_specify(testdir): + testdir.chdir() + config = py.test.raises(ImportError, """ + testdir.parseconfig("-p", "nqweotexistent") + """) + #py.test.raises(ImportError, + # "config.pluginmanager.do_configure(config)" + #) + +def test_plugin_already_exists(testdir): + config = testdir.parseconfig("-p", "session") + assert config.option.plugins == ['session'] + config.pluginmanager.do_configure(config) + +def test_exclude(testdir): + hellodir = testdir.mkdir("hello") + hellodir.join("test_hello.py").write("x y syntaxerror") + hello2dir = testdir.mkdir("hello2") + hello2dir.join("test_hello2.py").write("x y syntaxerror") + testdir.makepyfile(test_ok="def test_pass(): pass") + result = testdir.runpytest("--ignore=hello", "--ignore=hello2") + assert result.ret == 0 + result.stdout.fnmatch_lines(["*1 passed*"]) --- a/testing/plugin/test_nose.py +++ /dev/null @@ -1,254 +0,0 @@ -import py - -def setup_module(mod): - mod.nose = py.test.importorskip("nose") - -def test_nose_setup(testdir): - p = testdir.makepyfile(""" - l = [] - - def test_hello(): - assert l == [1] - - def test_world(): - assert l == [1,2] - - test_hello.setup = lambda: l.append(1) - test_hello.teardown = lambda: l.append(2) - """) - result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) - - -def test_nose_setup_func(testdir): - p = testdir.makepyfile(""" - l = [] - - def my_setup(): - a = 1 - l.append(a) - - def my_teardown(): - b = 2 - l.append(b) - - def test_hello(): - print l - assert l == [1] - - def test_world(): - print l - assert l == [1,2] - - test_hello.setup = my_setup - test_hello.teardown = my_teardown - """) - result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) - - -def test_nose_setup_func_failure(testdir): - p = testdir.makepyfile(""" - l = [] - - my_setup = lambda x: 1 - my_teardown = lambda x: 2 - - def test_hello(): - print l - assert l == [1] - - def test_world(): - print l - assert l == [1,2] - - test_hello.setup = my_setup - test_hello.teardown = my_teardown - """) - result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*TypeError: () takes exactly 1 argument (0 given)*" - ]) - - -def test_nose_setup_func_failure_2(testdir): - p = testdir.makepyfile(""" - l = [] - - my_setup = 1 - my_teardown = 2 - - def test_hello(): - print l - assert l == [1] - - def test_world(): - print l - assert l == [1,2] - - test_hello.setup = my_setup - test_hello.teardown = my_teardown - """) - result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*TypeError: 'int' object is not callable*" - ]) - - -def test_nose_setup_partial(testdir): - py.test.importorskip("functools") - p = testdir.makepyfile(""" - from functools import partial - - l = [] - - def my_setup(x): - a = x - l.append(a) - - def my_teardown(x): - b = x - l.append(b) - - my_setup_partial = partial(my_setup, 1) - my_teardown_partial = partial(my_teardown, 2) - - def test_hello(): - print l - assert l == [1] - - def test_world(): - print l - assert l == [1,2] - - test_hello.setup = my_setup_partial - test_hello.teardown = my_teardown_partial - """) - result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) - - -def test_nose_test_generator_fixtures(testdir): - p = testdir.makepyfile(""" - # taken from nose-0.11.1 unit_tests/test_generator_fixtures.py - from nose.tools import eq_ - called = [] - - def outer_setup(): - called.append('outer_setup') - - def outer_teardown(): - called.append('outer_teardown') - - def inner_setup(): - called.append('inner_setup') - - def inner_teardown(): - called.append('inner_teardown') - - def test_gen(): - called[:] = [] - for i in range(0, 5): - yield check, i - - def check(i): - expect = ['outer_setup'] - for x in range(0, i): - expect.append('inner_setup') - expect.append('inner_teardown') - expect.append('inner_setup') - eq_(called, expect) - - - test_gen.setup = outer_setup - test_gen.teardown = outer_teardown - check.setup = inner_setup - check.teardown = inner_teardown - - class TestClass(object): - def setup(self): - print "setup called in", self - self.called = ['setup'] - - def teardown(self): - print "teardown called in", self - eq_(self.called, ['setup']) - self.called.append('teardown') - - def test(self): - print "test called in", self - for i in range(0, 5): - yield self.check, i - - def check(self, i): - print "check called in", self - expect = ['setup'] - #for x in range(0, i): - # expect.append('setup') - # expect.append('teardown') - #expect.append('setup') - eq_(self.called, expect) - """) - result = testdir.runpytest(p, '-p', 'nose') - result.stdout.fnmatch_lines([ - "*10 passed*" - ]) - - -def test_module_level_setup(testdir): - testdir.makepyfile(""" - from nose.tools import with_setup - items = {} - - def setup(): - items[1]=1 - - def teardown(): - del items[1] - - def setup2(): - items[2] = 2 - - def teardown2(): - del items[2] - - def test_setup_module_setup(): - assert items[1] == 1 - - @with_setup(setup2, teardown2) - def test_local_setup(): - assert items[2] == 2 - assert 1 not in items - """) - result = testdir.runpytest('-p', 'nose') - result.stdout.fnmatch_lines([ - "*2 passed*", - ]) - - -def test_nose_style_setup_teardown(testdir): - testdir.makepyfile(""" - l = [] - - def setup_module(): - l.append(1) - - def teardown_module(): - 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*", - ]) --- a/testing/plugin/test_skipping.py +++ /dev/null @@ -1,444 +0,0 @@ -import py - -from _pytest.skipping import MarkEvaluator, folded_skips -from _pytest.skipping import pytest_runtest_setup -from _pytest.runner import runtestprotocol - -class TestEvaluator: - def test_no_marker(self, testdir): - item = testdir.getitem("def test_func(): pass") - evalskipif = MarkEvaluator(item, 'skipif') - assert not evalskipif - assert not evalskipif.istrue() - - def test_marked_no_args(self, testdir): - item = testdir.getitem(""" - import py - @py.test.mark.xyz - def test_func(): - pass - """) - ev = MarkEvaluator(item, 'xyz') - assert ev - assert ev.istrue() - expl = ev.getexplanation() - assert expl == "" - assert not ev.get("run", False) - - def test_marked_one_arg(self, testdir): - item = testdir.getitem(""" - import py - @py.test.mark.xyz("hasattr(os, 'sep')") - def test_func(): - pass - """) - ev = MarkEvaluator(item, 'xyz') - assert ev - assert ev.istrue() - expl = ev.getexplanation() - assert expl == "condition: hasattr(os, 'sep')" - - def test_marked_one_arg_with_reason(self, testdir): - item = testdir.getitem(""" - import py - @py.test.mark.xyz("hasattr(os, 'sep')", attr=2, reason="hello world") - def test_func(): - pass - """) - ev = MarkEvaluator(item, 'xyz') - assert ev - assert ev.istrue() - expl = ev.getexplanation() - assert expl == "hello world" - assert ev.get("attr") == 2 - - def test_marked_one_arg_twice(self, testdir): - lines = [ - '''@py.test.mark.skipif("not hasattr(os, 'murks')")''', - '''@py.test.mark.skipif("hasattr(os, 'murks')")''' - ] - for i in range(0, 2): - item = testdir.getitem(""" - import py - %s - %s - def test_func(): - pass - """ % (lines[i], lines[(i+1) %2])) - ev = MarkEvaluator(item, 'skipif') - assert ev - assert ev.istrue() - expl = ev.getexplanation() - assert expl == "condition: not hasattr(os, 'murks')" - - def test_marked_one_arg_twice2(self, testdir): - item = testdir.getitem(""" - import py - @py.test.mark.skipif("hasattr(os, 'murks')") - @py.test.mark.skipif("not hasattr(os, 'murks')") - def test_func(): - pass - """) - ev = MarkEvaluator(item, 'skipif') - assert ev - assert ev.istrue() - expl = ev.getexplanation() - assert expl == "condition: not hasattr(os, 'murks')" - - def test_skipif_class(self, testdir): - item, = testdir.getitems(""" - import py - class TestClass: - pytestmark = py.test.mark.skipif("config._hackxyz") - def test_func(self): - pass - """) - item.config._hackxyz = 3 - ev = MarkEvaluator(item, 'skipif') - assert ev.istrue() - expl = ev.getexplanation() - assert expl == "condition: config._hackxyz" - - -class TestXFail: - def test_xfail_simple(self, testdir): - item = testdir.getitem(""" - import py - @py.test.mark.xfail - def test_func(): - assert 0 - """) - reports = runtestprotocol(item, log=False) - assert len(reports) == 3 - callreport = reports[1] - assert callreport.skipped - expl = callreport.keywords['xfail'] - assert expl == "" - - def test_xfail_xpassed(self, testdir): - item = testdir.getitem(""" - import py - @py.test.mark.xfail - def test_func(): - assert 1 - """) - reports = runtestprotocol(item, log=False) - assert len(reports) == 3 - callreport = reports[1] - assert callreport.failed - expl = callreport.keywords['xfail'] - assert expl == "" - - def test_xfail_run_anyway(self, testdir): - testdir.makepyfile(""" - import py - @py.test.mark.xfail - def test_func(): - assert 0 - """) - result = testdir.runpytest("--runxfail") - assert result.ret == 1 - result.stdout.fnmatch_lines([ - "*def test_func():*", - "*assert 0*", - "*1 failed*", - ]) - - def test_xfail_evalfalse_but_fails(self, testdir): - item = testdir.getitem(""" - import py - @py.test.mark.xfail('False') - def test_func(): - assert 0 - """) - reports = runtestprotocol(item, log=False) - callreport = reports[1] - assert callreport.failed - assert 'xfail' not in callreport.keywords - - def test_xfail_not_report_default(self, testdir): - p = testdir.makepyfile(test_one=""" - import py - @py.test.mark.xfail - def test_this(): - assert 0 - """) - result = testdir.runpytest(p, '-v') - #result.stdout.fnmatch_lines([ - # "*HINT*use*-r*" - #]) - - def test_xfail_not_run_xfail_reporting(self, testdir): - p = testdir.makepyfile(test_one=""" - import py - @py.test.mark.xfail(run=False, reason="noway") - def test_this(): - assert 0 - @py.test.mark.xfail("True", run=False) - def test_this_true(): - assert 0 - @py.test.mark.xfail("False", run=False, reason="huh") - def test_this_false(): - assert 1 - """) - result = testdir.runpytest(p, '--report=xfailed', ) - result.stdout.fnmatch_lines([ - "*test_one*test_this*", - "*NOTRUN*noway", - "*test_one*test_this_true*", - "*NOTRUN*condition:*True*", - "*1 passed*", - ]) - - def test_xfail_not_run_no_setup_run(self, testdir): - p = testdir.makepyfile(test_one=""" - import py - @py.test.mark.xfail(run=False, reason="hello") - def test_this(): - assert 0 - def setup_module(mod): - raise ValueError(42) - """) - result = testdir.runpytest(p, '--report=xfailed', ) - result.stdout.fnmatch_lines([ - "*test_one*test_this*", - "*NOTRUN*hello", - "*1 xfailed*", - ]) - - def test_xfail_xpass(self, testdir): - p = testdir.makepyfile(test_one=""" - import py - @py.test.mark.xfail - def test_that(): - assert 1 - """) - result = testdir.runpytest(p, '-rX') - result.stdout.fnmatch_lines([ - "*XPASS*test_that*", - "*1 xpassed*" - ]) - assert result.ret == 0 - - def test_xfail_imperative(self, testdir): - p = testdir.makepyfile(""" - import py - def test_this(): - py.test.xfail("hello") - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 xfailed*", - ]) - result = testdir.runpytest(p, "-rx") - result.stdout.fnmatch_lines([ - "*XFAIL*test_this*", - "*reason:*hello*", - ]) - result = testdir.runpytest(p, "--runxfail") - result.stdout.fnmatch_lines([ - "*def test_this():*", - "*py.test.xfail*", - ]) - - def test_xfail_imperative_in_setup_function(self, testdir): - p = testdir.makepyfile(""" - import py - def setup_function(function): - py.test.xfail("hello") - - def test_this(): - assert 0 - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 xfailed*", - ]) - result = testdir.runpytest(p, "-rx") - result.stdout.fnmatch_lines([ - "*XFAIL*test_this*", - "*reason:*hello*", - ]) - result = testdir.runpytest(p, "--runxfail") - result.stdout.fnmatch_lines([ - "*def setup_function(function):*", - "*py.test.xfail*", - ]) - - def xtest_dynamic_xfail_set_during_setup(self, testdir): - p = testdir.makepyfile(""" - import py - def setup_function(function): - py.test.mark.xfail(function) - def test_this(): - assert 0 - def test_that(): - assert 1 - """) - result = testdir.runpytest(p, '-rxX') - result.stdout.fnmatch_lines([ - "*XFAIL*test_this*", - "*XPASS*test_that*", - ]) - - def test_dynamic_xfail_no_run(self, testdir): - p = testdir.makepyfile(""" - import py - def pytest_funcarg__arg(request): - request.applymarker(py.test.mark.xfail(run=False)) - def test_this(arg): - assert 0 - """) - result = testdir.runpytest(p, '-rxX') - result.stdout.fnmatch_lines([ - "*XFAIL*test_this*", - "*NOTRUN*", - ]) - - def test_dynamic_xfail_set_during_funcarg_setup(self, testdir): - p = testdir.makepyfile(""" - import py - def pytest_funcarg__arg(request): - request.applymarker(py.test.mark.xfail) - def test_this2(arg): - assert 0 - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 xfailed*", - ]) - - -class TestSkipif: - def test_skipif_conditional(self, testdir): - item = testdir.getitem(""" - import py - @py.test.mark.skipif("hasattr(os, 'sep')") - def test_func(): - pass - """) - x = py.test.raises(py.test.skip.Exception, "pytest_runtest_setup(item)") - assert x.value.msg == "condition: hasattr(os, 'sep')" - - - def test_skipif_reporting(self, testdir): - p = testdir.makepyfile(""" - import py - @py.test.mark.skipif("hasattr(sys, 'platform')") - def test_that(): - assert 0 - """) - result = testdir.runpytest(p, '-s', '-rs') - result.stdout.fnmatch_lines([ - "*SKIP*1*platform*", - "*1 skipped*" - ]) - assert result.ret == 0 - -def test_skip_not_report_default(testdir): - p = testdir.makepyfile(test_one=""" - import py - def test_this(): - py.test.skip("hello") - """) - result = testdir.runpytest(p, '-v') - result.stdout.fnmatch_lines([ - #"*HINT*use*-r*", - "*1 skipped*", - ]) - - -def test_skipif_class(testdir): - p = testdir.makepyfile(""" - import py - - class TestClass: - pytestmark = py.test.mark.skipif("True") - def test_that(self): - assert 0 - def test_though(self): - assert 0 - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*2 skipped*" - ]) - - -def test_skip_reasons_folding(): - class longrepr: - class reprcrash: - path = 'xyz' - lineno = 3 - message = "justso" - - class X: - pass - ev1 = X() - ev1.when = "execute" - ev1.skipped = True - ev1.longrepr = longrepr - - ev2 = X() - ev2.longrepr = longrepr - ev2.skipped = True - - l = folded_skips([ev1, ev2]) - assert len(l) == 1 - num, fspath, lineno, reason = l[0] - assert num == 2 - assert fspath == longrepr.reprcrash.path - assert lineno == longrepr.reprcrash.lineno - assert reason == longrepr.reprcrash.message - -def test_skipped_reasons_functional(testdir): - testdir.makepyfile( - test_one=""" - from conftest import doskip - def setup_function(func): - doskip() - def test_func(): - pass - class TestClass: - def test_method(self): - doskip() - """, - test_two = """ - from conftest import doskip - doskip() - """, - conftest = """ - import py - def doskip(): - py.test.skip('test') - """ - ) - result = testdir.runpytest('--report=skipped') - result.stdout.fnmatch_lines([ - "*test_two.py S", - "*test_one.py ss", - "*SKIP*3*conftest.py:3: test", - ]) - assert result.ret == 0 - -def test_reportchars(testdir): - testdir.makepyfile(""" - import py - def test_1(): - assert 0 - @py.test.mark.xfail - def test_2(): - assert 0 - @py.test.mark.xfail - def test_3(): - pass - def test_4(): - py.test.skip("four") - """) - result = testdir.runpytest("-rfxXs") - result.stdout.fnmatch_lines([ - "FAIL*test_1*", - "XFAIL*test_2*", - "XPASS*test_3*", - "SKIP*four*", - ]) --- /dev/null +++ b/testing/test_mark.py @@ -0,0 +1,295 @@ +import py +from _pytest.mark import MarkGenerator as Mark + +class TestMark: + def test_pytest_mark_notcallable(self): + mark = Mark() + py.test.raises((AttributeError, TypeError), "mark()") + + def test_pytest_mark_bare(self): + mark = Mark() + def f(): + pass + mark.hello(f) + assert f.hello + + def test_pytest_mark_keywords(self): + mark = Mark() + def f(): + pass + mark.world(x=3, y=4)(f) + assert f.world + assert f.world.kwargs['x'] == 3 + assert f.world.kwargs['y'] == 4 + + def test_apply_multiple_and_merge(self): + mark = Mark() + def f(): + pass + marker = mark.world + mark.world(x=3)(f) + assert f.world.kwargs['x'] == 3 + mark.world(y=4)(f) + assert f.world.kwargs['x'] == 3 + assert f.world.kwargs['y'] == 4 + mark.world(y=1)(f) + assert f.world.kwargs['y'] == 1 + assert len(f.world.args) == 0 + + def test_pytest_mark_positional(self): + mark = Mark() + def f(): + pass + mark.world("hello")(f) + assert f.world.args[0] == "hello" + mark.world("world")(f) + +class TestFunctional: + def test_mark_per_function(self, testdir): + p = testdir.makepyfile(""" + import py + @py.test.mark.hello + def test_hello(): + assert hasattr(test_hello, 'hello') + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines(["*passed*"]) + + def test_mark_per_module(self, testdir): + item = testdir.getitem(""" + import py + pytestmark = py.test.mark.hello + def test_func(): + pass + """) + keywords = item.keywords + assert 'hello' in keywords + + def test_marklist_per_class(self, testdir): + item = testdir.getitem(""" + import py + class TestClass: + pytestmark = [py.test.mark.hello, py.test.mark.world] + def test_func(self): + assert TestClass.test_func.hello + assert TestClass.test_func.world + """) + keywords = item.keywords + assert 'hello' in keywords + + def test_marklist_per_module(self, testdir): + item = testdir.getitem(""" + import py + pytestmark = [py.test.mark.hello, py.test.mark.world] + class TestClass: + def test_func(self): + assert TestClass.test_func.hello + assert TestClass.test_func.world + """) + keywords = item.keywords + assert 'hello' in keywords + assert 'world' in keywords + + @py.test.mark.skipif("sys.version_info < (2,6)") + def test_mark_per_class_decorator(self, testdir): + item = testdir.getitem(""" + import py + @py.test.mark.hello + class TestClass: + def test_func(self): + assert TestClass.test_func.hello + """) + keywords = item.keywords + assert 'hello' in keywords + + @py.test.mark.skipif("sys.version_info < (2,6)") + def test_mark_per_class_decorator_plus_existing_dec(self, testdir): + item = testdir.getitem(""" + import py + @py.test.mark.hello + class TestClass: + pytestmark = py.test.mark.world + def test_func(self): + assert TestClass.test_func.hello + assert TestClass.test_func.world + """) + keywords = item.keywords + assert 'hello' in keywords + assert 'world' in keywords + + def test_merging_markers(self, testdir): + p = testdir.makepyfile(""" + import py + pytestmark = py.test.mark.hello("pos1", x=1, y=2) + class TestClass: + # classlevel overrides module level + pytestmark = py.test.mark.hello(x=3) + @py.test.mark.hello("pos0", z=4) + def test_func(self): + pass + """) + items, rec = testdir.inline_genitems(p) + item, = items + keywords = item.keywords + marker = keywords['hello'] + assert marker.args == ["pos0", "pos1"] + assert marker.kwargs == {'x': 3, 'y': 2, 'z': 4} + + def test_mark_other(self, testdir): + py.test.raises(TypeError, ''' + testdir.getitem(""" + import py + class pytestmark: + pass + def test_func(): + pass + """) + ''') + + def test_mark_dynamically_in_funcarg(self, testdir): + testdir.makeconftest(""" + import py + def pytest_funcarg__arg(request): + request.applymarker(py.test.mark.hello) + def pytest_terminal_summary(terminalreporter): + l = terminalreporter.stats['passed'] + terminalreporter._tw.line("keyword: %s" % l[0].keywords) + """) + testdir.makepyfile(""" + def test_func(arg): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "keyword: *hello*" + ]) + + +class Test_genitems: + def test_check_collect_hashes(self, testdir): + p = testdir.makepyfile(""" + def test_1(): + pass + + def test_2(): + pass + """) + p.copy(p.dirpath(p.purebasename + "2" + ".py")) + items, reprec = testdir.inline_genitems(p.dirpath()) + assert len(items) == 4 + for numi, i in enumerate(items): + for numj, j in enumerate(items): + if numj != numi: + assert hash(i) != hash(j) + assert i != j + + def test_root_conftest_syntax_error(self, testdir): + # do we want to unify behaviour with + # test_subdir_conftest_error? + p = testdir.makepyfile(conftest="raise SyntaxError\n") + py.test.raises(SyntaxError, testdir.inline_genitems, p.dirpath()) + + def test_example_items1(self, testdir): + p = testdir.makepyfile(''' + def testone(): + pass + + class TestX: + def testmethod_one(self): + pass + + class TestY(TestX): + pass + ''') + items, reprec = testdir.inline_genitems(p) + assert len(items) == 3 + assert items[0].name == 'testone' + assert items[1].name == 'testmethod_one' + assert items[2].name == 'testmethod_one' + + # let's also test getmodpath here + assert items[0].getmodpath() == "testone" + assert items[1].getmodpath() == "TestX.testmethod_one" + assert items[2].getmodpath() == "TestY.testmethod_one" + + s = items[0].getmodpath(stopatmodule=False) + assert s.endswith("test_example_items1.testone") + print(s) + + +class TestKeywordSelection: + def test_select_simple(self, testdir): + file_test = testdir.makepyfile(""" + def test_one(): + assert 0 + class TestClass(object): + def test_method_one(self): + assert 42 == 43 + """) + def check(keyword, name): + reprec = testdir.inline_run("-s", "-k", keyword, file_test) + passed, skipped, failed = reprec.listoutcomes() + assert len(failed) == 1 + assert failed[0].nodeid.split("::")[-1] == name + 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.test', 'test_method_one') + + def test_select_extra_keywords(self, testdir): + p = testdir.makepyfile(test_select=""" + def test_1(): + pass + class TestClass: + def test_2(self): + pass + """) + testdir.makepyfile(conftest=""" + import py + def pytest_pycollect_makeitem(__multicall__, name): + if name == "TestClass": + item = __multicall__.execute() + item.keywords['xxx'] = True + return item + """) + for keyword in ('xxx', 'xxx test_2', 'TestClass', 'xxx -test_1', + 'TestClass test_2', 'xxx TestClass test_2',): + reprec = testdir.inline_run(p.dirpath(), '-s', '-k', keyword) + py.builtin.print_("keyword", repr(keyword)) + passed, skipped, failed = reprec.listoutcomes() + assert len(passed) == 1 + assert passed[0].nodeid.endswith("test_2") + dlist = reprec.getcalls("pytest_deselected") + assert len(dlist) == 1 + assert dlist[0].items[0].name == 'test_1' + + def test_select_starton(self, testdir): + threepass = testdir.makepyfile(test_threepass=""" + def test_one(): assert 1 + def test_two(): assert 1 + def test_three(): assert 1 + """) + reprec = testdir.inline_run("-k", "test_two:", threepass) + passed, skipped, failed = reprec.listoutcomes() + assert len(passed) == 2 + assert not failed + dlist = reprec.getcalls("pytest_deselected") + assert len(dlist) == 1 + item = dlist[0].items[0] + assert item.name == "test_one" + + + def test_keyword_extra(self, testdir): + p = testdir.makepyfile(""" + def test_one(): + assert 0 + test_one.mykeyword = True + """) + reprec = testdir.inline_run("-k", "-mykeyword", p) + passed, skipped, failed = reprec.countoutcomes() + assert passed + skipped + failed == 0 + reprec = testdir.inline_run("-k", "mykeyword", p) + passed, skipped, failed = reprec.countoutcomes() + assert failed == 1 --- /dev/null +++ b/testing/test_monkeypatch.py @@ -0,0 +1,139 @@ +import os, sys +import py +from _pytest.monkeypatch import monkeypatch as MonkeyPatch + +def test_setattr(): + class A: + x = 1 + monkeypatch = MonkeyPatch() + py.test.raises(AttributeError, "monkeypatch.setattr(A, 'notexists', 2)") + monkeypatch.setattr(A, 'y', 2, raising=False) + assert A.y == 2 + monkeypatch.undo() + assert not hasattr(A, 'y') + + monkeypatch = MonkeyPatch() + monkeypatch.setattr(A, 'x', 2) + assert A.x == 2 + monkeypatch.setattr(A, 'x', 3) + assert A.x == 3 + monkeypatch.undo() + assert A.x == 1 + + A.x = 5 + monkeypatch.undo() # double-undo makes no modification + assert A.x == 5 + +def test_delattr(): + class A: + x = 1 + monkeypatch = MonkeyPatch() + monkeypatch.delattr(A, 'x') + assert not hasattr(A, 'x') + monkeypatch.undo() + assert A.x == 1 + + monkeypatch = MonkeyPatch() + monkeypatch.delattr(A, 'x') + py.test.raises(AttributeError, "monkeypatch.delattr(A, 'y')") + monkeypatch.delattr(A, 'y', raising=False) + monkeypatch.setattr(A, 'x', 5, raising=False) + assert A.x == 5 + monkeypatch.undo() + assert A.x == 1 + +def test_setitem(): + d = {'x': 1} + monkeypatch = MonkeyPatch() + monkeypatch.setitem(d, 'x', 2) + monkeypatch.setitem(d, 'y', 1700) + monkeypatch.setitem(d, 'y', 1700) + assert d['x'] == 2 + assert d['y'] == 1700 + monkeypatch.setitem(d, 'x', 3) + assert d['x'] == 3 + monkeypatch.undo() + assert d['x'] == 1 + assert 'y' not in d + d['x'] = 5 + monkeypatch.undo() + assert d['x'] == 5 + +def test_delitem(): + d = {'x': 1} + monkeypatch = MonkeyPatch() + monkeypatch.delitem(d, 'x') + assert 'x' not in d + monkeypatch.delitem(d, 'y', raising=False) + py.test.raises(KeyError, "monkeypatch.delitem(d, 'y')") + assert not d + monkeypatch.setitem(d, 'y', 1700) + assert d['y'] == 1700 + d['hello'] = 'world' + monkeypatch.setitem(d, 'x', 1500) + assert d['x'] == 1500 + monkeypatch.undo() + assert d == {'hello': 'world', 'x': 1} + +def test_setenv(): + monkeypatch = MonkeyPatch() + monkeypatch.setenv('XYZ123', 2) + import os + assert os.environ['XYZ123'] == "2" + monkeypatch.undo() + assert 'XYZ123' not in os.environ + +def test_delenv(): + name = 'xyz1234' + assert name not in os.environ + monkeypatch = MonkeyPatch() + py.test.raises(KeyError, "monkeypatch.delenv(%r, raising=True)" % name) + monkeypatch.delenv(name, raising=False) + monkeypatch.undo() + os.environ[name] = "1" + try: + monkeypatch = MonkeyPatch() + monkeypatch.delenv(name) + assert name not in os.environ + monkeypatch.setenv(name, "3") + assert os.environ[name] == "3" + monkeypatch.undo() + assert os.environ[name] == "1" + finally: + if name in os.environ: + del os.environ[name] + +def test_setenv_prepend(): + import os + monkeypatch = MonkeyPatch() + monkeypatch.setenv('XYZ123', 2, prepend="-") + assert os.environ['XYZ123'] == "2" + monkeypatch.setenv('XYZ123', 3, prepend="-") + assert os.environ['XYZ123'] == "3-2" + monkeypatch.undo() + assert 'XYZ123' not in os.environ + +def test_monkeypatch_plugin(testdir): + reprec = testdir.inline_runsource(""" + def test_method(monkeypatch): + assert monkeypatch.__class__.__name__ == "monkeypatch" + """) + res = reprec.countoutcomes() + assert tuple(res) == (1, 0, 0), res + +def test_syspath_prepend(): + old = list(sys.path) + try: + monkeypatch = MonkeyPatch() + monkeypatch.syspath_prepend('world') + monkeypatch.syspath_prepend('hello') + assert sys.path[0] == "hello" + assert sys.path[1] == "world" + monkeypatch.undo() + assert sys.path == old + monkeypatch.undo() + assert sys.path == old + finally: + sys.path[:] = old + + --- /dev/null +++ b/testing/test_nose.py @@ -0,0 +1,254 @@ +import py + +def setup_module(mod): + mod.nose = py.test.importorskip("nose") + +def test_nose_setup(testdir): + p = testdir.makepyfile(""" + l = [] + + def test_hello(): + assert l == [1] + + def test_world(): + assert l == [1,2] + + test_hello.setup = lambda: l.append(1) + test_hello.teardown = lambda: l.append(2) + """) + result = testdir.runpytest(p, '-p', 'nose') + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) + + +def test_nose_setup_func(testdir): + p = testdir.makepyfile(""" + l = [] + + def my_setup(): + a = 1 + l.append(a) + + def my_teardown(): + b = 2 + l.append(b) + + def test_hello(): + print l + assert l == [1] + + def test_world(): + print l + assert l == [1,2] + + test_hello.setup = my_setup + test_hello.teardown = my_teardown + """) + result = testdir.runpytest(p, '-p', 'nose') + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) + + +def test_nose_setup_func_failure(testdir): + p = testdir.makepyfile(""" + l = [] + + my_setup = lambda x: 1 + my_teardown = lambda x: 2 + + def test_hello(): + print l + assert l == [1] + + def test_world(): + print l + assert l == [1,2] + + test_hello.setup = my_setup + test_hello.teardown = my_teardown + """) + result = testdir.runpytest(p, '-p', 'nose') + result.stdout.fnmatch_lines([ + "*TypeError: () takes exactly 1 argument (0 given)*" + ]) + + +def test_nose_setup_func_failure_2(testdir): + p = testdir.makepyfile(""" + l = [] + + my_setup = 1 + my_teardown = 2 + + def test_hello(): + print l + assert l == [1] + + def test_world(): + print l + assert l == [1,2] + + test_hello.setup = my_setup + test_hello.teardown = my_teardown + """) + result = testdir.runpytest(p, '-p', 'nose') + result.stdout.fnmatch_lines([ + "*TypeError: 'int' object is not callable*" + ]) + + +def test_nose_setup_partial(testdir): + py.test.importorskip("functools") + p = testdir.makepyfile(""" + from functools import partial + + l = [] + + def my_setup(x): + a = x + l.append(a) + + def my_teardown(x): + b = x + l.append(b) + + my_setup_partial = partial(my_setup, 1) + my_teardown_partial = partial(my_teardown, 2) + + def test_hello(): + print l + assert l == [1] + + def test_world(): + print l + assert l == [1,2] + + test_hello.setup = my_setup_partial + test_hello.teardown = my_teardown_partial + """) + result = testdir.runpytest(p, '-p', 'nose') + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) + + +def test_nose_test_generator_fixtures(testdir): + p = testdir.makepyfile(""" + # taken from nose-0.11.1 unit_tests/test_generator_fixtures.py + from nose.tools import eq_ + called = [] + + def outer_setup(): + called.append('outer_setup') + + def outer_teardown(): + called.append('outer_teardown') + + def inner_setup(): + called.append('inner_setup') + + def inner_teardown(): + called.append('inner_teardown') + + def test_gen(): + called[:] = [] + for i in range(0, 5): + yield check, i + + def check(i): + expect = ['outer_setup'] + for x in range(0, i): + expect.append('inner_setup') + expect.append('inner_teardown') + expect.append('inner_setup') + eq_(called, expect) + + + test_gen.setup = outer_setup + test_gen.teardown = outer_teardown + check.setup = inner_setup + check.teardown = inner_teardown + + class TestClass(object): + def setup(self): + print "setup called in", self + self.called = ['setup'] + + def teardown(self): + print "teardown called in", self + eq_(self.called, ['setup']) + self.called.append('teardown') + + def test(self): + print "test called in", self + for i in range(0, 5): + yield self.check, i + + def check(self, i): + print "check called in", self + expect = ['setup'] + #for x in range(0, i): + # expect.append('setup') + # expect.append('teardown') + #expect.append('setup') + eq_(self.called, expect) + """) + result = testdir.runpytest(p, '-p', 'nose') + result.stdout.fnmatch_lines([ + "*10 passed*" + ]) + + +def test_module_level_setup(testdir): + testdir.makepyfile(""" + from nose.tools import with_setup + items = {} + + def setup(): + items[1]=1 + + def teardown(): + del items[1] + + def setup2(): + items[2] = 2 + + def teardown2(): + del items[2] + + def test_setup_module_setup(): + assert items[1] == 1 + + @with_setup(setup2, teardown2) + def test_local_setup(): + assert items[2] == 2 + assert 1 not in items + """) + result = testdir.runpytest('-p', 'nose') + result.stdout.fnmatch_lines([ + "*2 passed*", + ]) + + +def test_nose_style_setup_teardown(testdir): + testdir.makepyfile(""" + l = [] + + def setup_module(): + l.append(1) + + def teardown_module(): + 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*", + ]) --- /dev/null +++ b/testing/test_genscript.py @@ -0,0 +1,41 @@ +import py, os, sys +import subprocess + + +def pytest_funcarg__standalone(request): + return request.cached_setup(scope="module", setup=lambda: Standalone(request)) + +class Standalone: + def __init__(self, request): + self.testdir = request.getfuncargvalue("testdir") + script = "mypytest" + result = self.testdir.runpytest("--genscript=%s" % script) + assert result.ret == 0 + self.script = self.testdir.tmpdir.join(script) + assert self.script.check() + + def run(self, anypython, testdir, *args): + testdir.chdir() + return testdir._run(anypython, self.script, *args) + +def test_gen(testdir, anypython, standalone): + result = standalone.run(anypython, testdir, '--version') + assert result.ret == 0 + result.stderr.fnmatch_lines([ + "*imported from*mypytest*" + ]) + p = testdir.makepyfile("def test_func(): assert 0") + result = standalone.run(anypython, testdir, p) + assert result.ret != 0 + +def test_rundist(testdir, pytestconfig, standalone): + pytestconfig.pluginmanager.skipifmissing("xdist") + testdir.makepyfile(""" + def test_one(): + pass + """) + result = standalone.run(sys.executable, testdir, '-n', '3') + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*1 passed*", + ]) --- /dev/null +++ b/testing/test_tmpdir.py @@ -0,0 +1,29 @@ +import py + +from _pytest.tmpdir import pytest_funcarg__tmpdir +from _pytest.python import FuncargRequest + +def test_funcarg(testdir): + item = testdir.getitem(""" + def pytest_generate_tests(metafunc): + metafunc.addcall(id='a') + metafunc.addcall(id='b') + def test_func(tmpdir): pass + """, 'test_func[a]') + p = pytest_funcarg__tmpdir(FuncargRequest(item)) + assert p.check() + bn = p.basename.strip("0123456789") + assert bn.endswith("test_func_a_") + item.name = "qwe/\\abc" + p = pytest_funcarg__tmpdir(FuncargRequest(item)) + assert p.check() + bn = p.basename.strip("0123456789") + assert bn == "qwe__abc" + +def test_ensuretemp(recwarn): + #py.test.deprecated_call(py.test.ensuretemp, 'hello') + d1 = py.test.ensuretemp('hello') + d2 = py.test.ensuretemp('hello') + assert d1 == d2 + assert d1.check(dir=1) + --- /dev/null +++ b/testing/test_pdb.py @@ -0,0 +1,145 @@ +import py +import sys + +class TestPDB: + def pytest_funcarg__pdblist(self, request): + monkeypatch = request.getfuncargvalue("monkeypatch") + pdblist = [] + def mypdb(*args): + pdblist.append(args) + plugin = request.config.pluginmanager.getplugin('pdb') + monkeypatch.setattr(plugin, 'post_mortem', mypdb) + return pdblist + + def test_pdb_on_fail(self, testdir, pdblist): + rep = testdir.inline_runsource1('--pdb', """ + def test_func(): + assert 0 + """) + assert rep.failed + assert len(pdblist) == 1 + tb = py.code.Traceback(pdblist[0][0]) + assert tb[-1].name == "test_func" + + def test_pdb_on_xfail(self, testdir, pdblist): + rep = testdir.inline_runsource1('--pdb', """ + import py + @py.test.mark.xfail + def test_func(): + assert 0 + """) + assert "xfail" in rep.keywords + assert not pdblist + + def test_pdb_on_skip(self, testdir, pdblist): + rep = testdir.inline_runsource1('--pdb', """ + import py + def test_func(): + py.test.skip("hello") + """) + assert rep.skipped + assert len(pdblist) == 0 + + def test_pdb_on_BdbQuit(self, testdir, pdblist): + rep = testdir.inline_runsource1('--pdb', """ + import py, bdb + def test_func(): + raise bdb.BdbQuit + """) + assert rep.failed + assert len(pdblist) == 0 + + def test_pdb_interaction(self, testdir): + p1 = testdir.makepyfile(""" + def test_1(): + i = 0 + assert i == 1 + """) + child = testdir.spawn_pytest("--pdb %s" % p1) + child.expect(".*def test_1") + child.expect(".*i = 0") + child.expect("(Pdb)") + child.sendeof() + rest = child.read() + assert "1 failed" in rest + assert "def test_1" not in rest + if child.isalive(): + child.wait() + + def test_pdb_interaction_exception(self, testdir): + p1 = testdir.makepyfile(""" + import py + def globalfunc(): + pass + def test_1(): + py.test.raises(ValueError, globalfunc) + """) + child = testdir.spawn_pytest("--pdb %s" % p1) + child.expect(".*def test_1") + child.expect(".*py.test.raises.*globalfunc") + child.expect("(Pdb)") + child.sendline("globalfunc") + child.expect(".*function") + child.sendeof() + child.expect("1 failed") + if child.isalive(): + child.wait() + + def test_pdb_interaction_capturing_simple(self, testdir): + p1 = testdir.makepyfile(""" + import py + def test_1(): + i = 0 + print ("hello17") + py.test.set_trace() + x = 3 + """) + child = testdir.spawn_pytest(str(p1)) + child.expect("test_1") + child.expect("x = 3") + child.expect("(Pdb)") + child.sendeof() + rest = child.read() + assert "1 failed" in rest + assert "def test_1" in rest + assert "hello17" in rest # out is captured + if child.isalive(): + child.wait() + + def test_pdb_interaction_capturing_twice(self, testdir): + p1 = testdir.makepyfile(""" + import py + def test_1(): + i = 0 + print ("hello17") + py.test.set_trace() + x = 3 + print ("hello18") + py.test.set_trace() + x = 4 + """) + child = testdir.spawn_pytest(str(p1)) + child.expect("test_1") + child.expect("x = 3") + child.expect("(Pdb)") + child.sendline('c') + child.expect("x = 4") + child.sendeof() + rest = child.read() + assert "1 failed" in rest + assert "def test_1" in rest + assert "hello17" in rest # out is captured + assert "hello18" in rest # out is captured + if child.isalive(): + child.wait() + + def test_pdb_used_outside_test(self, testdir): + p1 = testdir.makepyfile(""" + import py + py.test.set_trace() + x = 5 + """) + child = testdir.spawn("%s %s" %(sys.executable, p1)) + child.expect("x = 5") + child.sendeof() + child.wait() --- /dev/null +++ b/testing/test_runner_xunit.py @@ -0,0 +1,212 @@ +# +# test correct setup/teardowns at +# module, class, and instance level + +def test_module_and_function_setup(testdir): + reprec = testdir.inline_runsource(""" + modlevel = [] + def setup_module(module): + assert not modlevel + module.modlevel.append(42) + + def teardown_module(module): + modlevel.pop() + + def setup_function(function): + function.answer = 17 + + def teardown_function(function): + del function.answer + + def test_modlevel(): + assert modlevel[0] == 42 + assert test_modlevel.answer == 17 + + class TestFromClass: + def test_module(self): + assert modlevel[0] == 42 + assert not hasattr(test_modlevel, 'answer') + """) + rep = reprec.matchreport("test_modlevel") + assert rep.passed + rep = reprec.matchreport("test_module") + assert rep.passed + +def test_class_setup(testdir): + reprec = testdir.inline_runsource(""" + class TestSimpleClassSetup: + clslevel = [] + def setup_class(cls): + cls.clslevel.append(23) + + def teardown_class(cls): + cls.clslevel.pop() + + def test_classlevel(self): + assert self.clslevel[0] == 23 + + class TestInheritedClassSetupStillWorks(TestSimpleClassSetup): + def test_classlevel_anothertime(self): + assert self.clslevel == [23] + + def test_cleanup(): + assert not TestSimpleClassSetup.clslevel + assert not TestInheritedClassSetupStillWorks.clslevel + """) + reprec.assertoutcome(passed=1+2+1) + + +def test_method_setup(testdir): + reprec = testdir.inline_runsource(""" + class TestSetupMethod: + def setup_method(self, meth): + self.methsetup = meth + def teardown_method(self, meth): + del self.methsetup + + def test_some(self): + assert self.methsetup == self.test_some + + def test_other(self): + assert self.methsetup == self.test_other + """) + reprec.assertoutcome(passed=2) + +def test_method_generator_setup(testdir): + reprec = testdir.inline_runsource(""" + class TestSetupTeardownOnInstance: + def setup_class(cls): + cls.classsetup = True + + def setup_method(self, method): + self.methsetup = method + + def test_generate(self): + assert self.classsetup + assert self.methsetup == self.test_generate + yield self.generated, 5 + yield self.generated, 2 + + def generated(self, value): + assert self.classsetup + assert self.methsetup == self.test_generate + assert value == 5 + """) + reprec.assertoutcome(passed=1, failed=1) + +def test_func_generator_setup(testdir): + reprec = testdir.inline_runsource(""" + import sys + + def setup_module(mod): + print ("setup_module") + mod.x = [] + + def setup_function(fun): + print ("setup_function") + x.append(1) + + def teardown_function(fun): + print ("teardown_function") + x.pop() + + def test_one(): + assert x == [1] + def check(): + print ("check") + sys.stderr.write("e\\n") + assert x == [1] + yield check + assert x == [1] + """) + rep = reprec.matchreport("test_one", names="pytest_runtest_logreport") + assert rep.passed + +def test_method_setup_uses_fresh_instances(testdir): + reprec = testdir.inline_runsource(""" + class TestSelfState1: + memory = [] + def test_hello(self): + self.memory.append(self) + + def test_afterhello(self): + assert self != self.memory[0] + """) + reprec.assertoutcome(passed=2, failed=0) + +def test_failing_setup_calls_teardown(testdir): + p = testdir.makepyfile(""" + def setup_module(mod): + raise ValueError(42) + def test_function(): + assert 0 + def teardown_module(mod): + raise ValueError(43) + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*42*", + "*43*", + "*2 error*" + ]) + +def test_setup_that_skips_calledagain_and_teardown(testdir): + p = testdir.makepyfile(""" + import py + def setup_module(mod): + py.test.skip("x") + def test_function1(): + pass + def test_function2(): + pass + def teardown_module(mod): + raise ValueError(43) + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*ValueError*43*", + "*2 skipped*1 error*", + ]) + +def test_setup_fails_again_on_all_tests(testdir): + p = testdir.makepyfile(""" + import py + def setup_module(mod): + raise ValueError(42) + def test_function1(): + pass + def test_function2(): + pass + def teardown_module(mod): + raise ValueError(43) + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*3 error*" + ]) + assert "passed" not in result.stdout.str() + +def test_setup_funcarg_setup_not_called_if_outer_scope_fails(testdir): + p = testdir.makepyfile(""" + import py + def setup_module(mod): + raise ValueError(42) + def pytest_funcarg__hello(request): + raise ValueError("xyz43") + def test_function1(hello): + pass + def test_function2(hello): + pass + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*function1*", + "*ValueError*42*", + "*function2*", + "*ValueError*42*", + "*2 error*" + ]) + assert "xyz43" not in result.stdout.str() + + + --- a/testing/plugin/test_session.py +++ /dev/null @@ -1,227 +0,0 @@ -import pytest, py - -class SessionTests: - def test_basic_testitem_events(self, testdir): - tfile = testdir.makepyfile(""" - def test_one(): - pass - def test_one_one(): - assert 0 - def test_other(): - raise ValueError(23) - def test_two(someargs): - pass - """) - reprec = testdir.inline_run(tfile) - passed, skipped, failed = reprec.listoutcomes() - assert len(skipped) == 0 - assert len(passed) == 1 - assert len(failed) == 3 - end = lambda x: x.nodeid.split("::")[-1] - assert end(failed[0]) == "test_one_one" - assert end(failed[1]) == "test_other" - assert end(failed[2]) == "test_two" - itemstarted = reprec.getcalls("pytest_itemcollected") - assert len(itemstarted) == 4 - colstarted = reprec.getcalls("pytest_collectstart") - assert len(colstarted) == 1 + 1 - col = colstarted[1].collector - assert isinstance(col, pytest.Module) - - def test_nested_import_error(self, testdir): - tfile = testdir.makepyfile(""" - import import_fails - def test_this(): - assert import_fails.a == 1 - """, import_fails=""" - import does_not_work - a = 1 - """) - reprec = testdir.inline_run(tfile) - l = reprec.getfailedcollections() - assert len(l) == 1 - out = l[0].longrepr.reprcrash.message - assert out.find('does_not_work') != -1 - - def test_raises_output(self, testdir): - reprec = testdir.inline_runsource(""" - import py - def test_raises_doesnt(): - py.test.raises(ValueError, int, "3") - """) - passed, skipped, failed = reprec.listoutcomes() - assert len(failed) == 1 - out = failed[0].longrepr.reprcrash.message - if not out.find("DID NOT RAISE") != -1: - print(out) - py.test.fail("incorrect raises() output") - - def test_generator_yields_None(self, testdir): - reprec = testdir.inline_runsource(""" - def test_1(): - yield None - """) - failures = reprec.getfailedcollections() - out = failures[0].longrepr.reprcrash.message - i = out.find('TypeError') - assert i != -1 - - def test_syntax_error_module(self, testdir): - reprec = testdir.inline_runsource("this is really not python") - l = reprec.getfailedcollections() - assert len(l) == 1 - out = str(l[0].longrepr) - assert out.find(str('not python')) != -1 - - def test_exit_first_problem(self, testdir): - reprec = testdir.inline_runsource(""" - def test_one(): assert 0 - def test_two(): assert 0 - """, '--exitfirst') - passed, skipped, failed = reprec.countoutcomes() - assert failed == 1 - assert passed == skipped == 0 - - def test_maxfail(self, testdir): - reprec = testdir.inline_runsource(""" - def test_one(): assert 0 - def test_two(): assert 0 - def test_three(): assert 0 - """, '--maxfail=2') - passed, skipped, failed = reprec.countoutcomes() - assert failed == 2 - assert passed == skipped == 0 - - def test_broken_repr(self, testdir): - p = testdir.makepyfile(""" - import pytest - class BrokenRepr1: - foo=0 - def __repr__(self): - raise Exception("Ha Ha fooled you, I'm a broken repr().") - - class TestBrokenClass: - def test_explicit_bad_repr(self): - t = BrokenRepr1() - pytest.raises(Exception, 'repr(t)') - - def test_implicit_bad_repr1(self): - t = BrokenRepr1() - assert t.foo == 1 - - """) - reprec = testdir.inline_run(p) - passed, skipped, failed = reprec.listoutcomes() - assert len(failed) == 1 - out = failed[0].longrepr.reprcrash.message - assert out.find("""[Exception("Ha Ha fooled you, I'm a broken repr().") raised in repr()]""") != -1 #' - - def test_skip_file_by_conftest(self, testdir): - testdir.makepyfile(conftest=""" - import py - def pytest_collect_file(): - py.test.skip("intentional") - """, test_file=""" - def test_one(): pass - """) - try: - reprec = testdir.inline_run(testdir.tmpdir) - except py.test.skip.Exception: - py.test.fail("wrong skipped caught") - reports = reprec.getreports("pytest_collectreport") - assert len(reports) == 1 - assert reports[0].skipped - -class TestNewSession(SessionTests): - - def test_order_of_execution(self, testdir): - reprec = testdir.inline_runsource(""" - l = [] - def test_1(): - l.append(1) - def test_2(): - l.append(2) - def test_3(): - assert l == [1,2] - class Testmygroup: - reslist = l - def test_1(self): - self.reslist.append(1) - def test_2(self): - self.reslist.append(2) - def test_3(self): - self.reslist.append(3) - def test_4(self): - assert self.reslist == [1,2,1,2,3] - """) - passed, skipped, failed = reprec.countoutcomes() - assert failed == skipped == 0 - assert passed == 7 - # also test listnames() here ... - - def test_collect_only_with_various_situations(self, testdir): - p = testdir.makepyfile( - test_one=""" - def test_one(): - raise ValueError() - - class TestX: - def test_method_one(self): - pass - - class TestY(TestX): - pass - """, - test_two=""" - import py - py.test.skip('xxx') - """, - test_three="xxxdsadsadsadsa", - __init__="" - ) - reprec = testdir.inline_run('--collectonly', p.dirpath()) - - itemstarted = reprec.getcalls("pytest_itemcollected") - assert len(itemstarted) == 3 - assert not reprec.getreports("pytest_runtest_logreport") - started = reprec.getcalls("pytest_collectstart") - finished = reprec.getreports("pytest_collectreport") - assert len(started) == len(finished) - assert len(started) == 8 # XXX extra TopCollector - colfail = [x for x in finished if x.failed] - colskipped = [x for x in finished if x.skipped] - assert len(colfail) == 1 - assert len(colskipped) == 1 - - def test_minus_x_import_error(self, testdir): - testdir.makepyfile(__init__="") - testdir.makepyfile(test_one="xxxx", test_two="yyyy") - reprec = testdir.inline_run("-x", testdir.tmpdir) - finished = reprec.getreports("pytest_collectreport") - colfail = [x for x in finished if x.failed] - assert len(colfail) == 1 - - -def test_plugin_specify(testdir): - testdir.chdir() - config = py.test.raises(ImportError, """ - testdir.parseconfig("-p", "nqweotexistent") - """) - #py.test.raises(ImportError, - # "config.pluginmanager.do_configure(config)" - #) - -def test_plugin_already_exists(testdir): - config = testdir.parseconfig("-p", "session") - assert config.option.plugins == ['session'] - config.pluginmanager.do_configure(config) - -def test_exclude(testdir): - hellodir = testdir.mkdir("hello") - hellodir.join("test_hello.py").write("x y syntaxerror") - hello2dir = testdir.mkdir("hello2") - hello2dir.join("test_hello2.py").write("x y syntaxerror") - testdir.makepyfile(test_ok="def test_pass(): pass") - result = testdir.runpytest("--ignore=hello", "--ignore=hello2") - assert result.ret == 0 - result.stdout.fnmatch_lines(["*1 passed*"]) --- a/testing/plugin/test_python.py +++ /dev/null @@ -1,1176 +0,0 @@ -import pytest, py, sys -from _pytest import python as funcargs - -class TestModule: - def test_failing_import(self, testdir): - modcol = testdir.getmodulecol("import alksdjalskdjalkjals") - py.test.raises(ImportError, modcol.collect) - py.test.raises(ImportError, modcol.collect) - - def test_import_duplicate(self, testdir): - a = testdir.mkdir("a") - b = testdir.mkdir("b") - p = a.ensure("test_whatever.py") - p.pyimport() - del py.std.sys.modules['test_whatever'] - b.ensure("test_whatever.py") - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*import*mismatch*", - "*imported*test_whatever*", - "*%s*" % a.join("test_whatever.py"), - "*not the same*", - "*%s*" % b.join("test_whatever.py"), - "*HINT*", - ]) - - def test_syntax_error_in_module(self, testdir): - modcol = testdir.getmodulecol("this is a syntax error") - py.test.raises(modcol.CollectError, modcol.collect) - py.test.raises(modcol.CollectError, modcol.collect) - - def test_module_considers_pluginmanager_at_import(self, testdir): - modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") - py.test.raises(ImportError, "modcol.obj") - -class TestClass: - def test_class_with_init_not_collected(self, testdir): - modcol = testdir.getmodulecol(""" - class TestClass1: - def __init__(self): - pass - class TestClass2(object): - def __init__(self): - pass - """) - l = modcol.collect() - assert len(l) == 0 - -class TestGenerator: - def test_generative_functions(self, testdir): - modcol = testdir.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - - def test_gen(): - yield func1, 17, 3*5 - yield func1, 42, 6*7 - """) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, pytest.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], pytest.Function) - assert isinstance(gencolitems[1], pytest.Function) - assert gencolitems[0].name == '[0]' - assert gencolitems[0].obj.__name__ == 'func1' - - def test_generative_methods(self, testdir): - modcol = testdir.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - class TestGenMethods: - def test_gen(self): - yield func1, 17, 3*5 - yield func1, 42, 6*7 - """) - gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, pytest.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], pytest.Function) - assert isinstance(gencolitems[1], pytest.Function) - assert gencolitems[0].name == '[0]' - assert gencolitems[0].obj.__name__ == 'func1' - - def test_generative_functions_with_explicit_names(self, testdir): - modcol = testdir.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - - def test_gen(): - yield "seventeen", func1, 17, 3*5 - yield "fortytwo", func1, 42, 6*7 - """) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, pytest.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], pytest.Function) - assert isinstance(gencolitems[1], pytest.Function) - assert gencolitems[0].name == "['seventeen']" - assert gencolitems[0].obj.__name__ == 'func1' - assert gencolitems[1].name == "['fortytwo']" - assert gencolitems[1].obj.__name__ == 'func1' - - def test_generative_functions_unique_explicit_names(self, testdir): - # generative - modcol = testdir.getmodulecol(""" - def func(): pass - def test_gen(): - yield "name", func - yield "name", func - """) - colitems = modcol.collect() - assert len(colitems) == 1 - gencol = colitems[0] - assert isinstance(gencol, pytest.Generator) - py.test.raises(ValueError, "gencol.collect()") - - def test_generative_methods_with_explicit_names(self, testdir): - modcol = testdir.getmodulecol(""" - def func1(arg, arg2): - assert arg == arg2 - class TestGenMethods: - def test_gen(self): - yield "m1", func1, 17, 3*5 - yield "m2", func1, 42, 6*7 - """) - gencol = modcol.collect()[0].collect()[0].collect()[0] - assert isinstance(gencol, pytest.Generator) - gencolitems = gencol.collect() - assert len(gencolitems) == 2 - assert isinstance(gencolitems[0], pytest.Function) - assert isinstance(gencolitems[1], pytest.Function) - assert gencolitems[0].name == "['m1']" - assert gencolitems[0].obj.__name__ == 'func1' - assert gencolitems[1].name == "['m2']" - assert gencolitems[1].obj.__name__ == 'func1' - - def test_order_of_execution_generator_same_codeline(self, testdir, tmpdir): - o = testdir.makepyfile(""" - def test_generative_order_of_execution(): - import py - test_list = [] - expected_list = list(range(6)) - - def list_append(item): - test_list.append(item) - - def assert_order_of_execution(): - py.builtin.print_('expected order', expected_list) - py.builtin.print_('but got ', test_list) - assert test_list == expected_list - - for i in expected_list: - yield list_append, i - yield assert_order_of_execution - """) - reprec = testdir.inline_run(o) - passed, skipped, failed = reprec.countoutcomes() - assert passed == 7 - assert not skipped and not failed - - def test_order_of_execution_generator_different_codeline(self, testdir): - o = testdir.makepyfile(""" - def test_generative_tests_different_codeline(): - import py - test_list = [] - expected_list = list(range(3)) - - def list_append_2(): - test_list.append(2) - - def list_append_1(): - test_list.append(1) - - def list_append_0(): - test_list.append(0) - - def assert_order_of_execution(): - py.builtin.print_('expected order', expected_list) - py.builtin.print_('but got ', test_list) - assert test_list == expected_list - - yield list_append_0 - yield list_append_1 - yield list_append_2 - yield assert_order_of_execution - """) - reprec = testdir.inline_run(o) - passed, skipped, failed = reprec.countoutcomes() - assert passed == 4 - assert not skipped and not failed - -class TestFunction: - def test_getmodulecollector(self, testdir): - item = testdir.getitem("def test_func(): pass") - modcol = item.getparent(pytest.Module) - assert isinstance(modcol, pytest.Module) - assert hasattr(modcol.obj, 'test_func') - - def test_function_equality(self, testdir, tmpdir): - config = testdir.reparseconfig() - session = testdir.Session(config) - f1 = pytest.Function(name="name", config=config, - args=(1,), callobj=isinstance, session=session) - f2 = pytest.Function(name="name",config=config, - args=(1,), callobj=py.builtin.callable, session=session) - assert not f1 == f2 - assert f1 != f2 - f3 = pytest.Function(name="name", config=config, - args=(1,2), callobj=py.builtin.callable, session=session) - assert not f3 == f2 - assert f3 != f2 - - assert not f3 == f1 - assert f3 != f1 - - f1_b = pytest.Function(name="name", config=config, - args=(1,), callobj=isinstance, session=session) - assert f1 == f1_b - assert not f1 != f1_b - - def test_function_equality_with_callspec(self, testdir, tmpdir): - config = testdir.reparseconfig() - class callspec1: - param = 1 - funcargs = {} - id = "hello" - class callspec2: - param = 1 - funcargs = {} - id = "world" - session = testdir.Session(config) - f5 = pytest.Function(name="name", config=config, - callspec=callspec1, callobj=isinstance, session=session) - f5b = pytest.Function(name="name", config=config, - callspec=callspec2, callobj=isinstance, session=session) - assert f5 != f5b - assert not (f5 == f5b) - - def test_pyfunc_call(self, testdir): - item = testdir.getitem("def test_func(): raise ValueError") - config = item.config - class MyPlugin1: - def pytest_pyfunc_call(self, pyfuncitem): - raise ValueError - class MyPlugin2: - def pytest_pyfunc_call(self, pyfuncitem): - return True - config.pluginmanager.register(MyPlugin1()) - config.pluginmanager.register(MyPlugin2()) - config.hook.pytest_pyfunc_call(pyfuncitem=item) - -class TestSorting: - def test_check_equality(self, testdir): - modcol = testdir.getmodulecol(""" - def test_pass(): pass - def test_fail(): assert 0 - """) - fn1 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn1, pytest.Function) - fn2 = testdir.collect_by_name(modcol, "test_pass") - assert isinstance(fn2, pytest.Function) - - assert fn1 == fn2 - assert fn1 != modcol - if py.std.sys.version_info < (3, 0): - assert cmp(fn1, fn2) == 0 - assert hash(fn1) == hash(fn2) - - fn3 = testdir.collect_by_name(modcol, "test_fail") - assert isinstance(fn3, pytest.Function) - assert not (fn1 == fn3) - assert fn1 != fn3 - - for fn in fn1,fn2,fn3: - assert fn != 3 - assert fn != modcol - assert fn != [1,2,3] - assert [1,2,3] != fn - assert modcol != fn - - def test_allow_sane_sorting_for_decorators(self, testdir): - modcol = testdir.getmodulecol(""" - def dec(f): - g = lambda: f(2) - g.place_as = f - return g - - - def test_b(y): - pass - test_b = dec(test_b) - - def test_a(y): - pass - test_a = dec(test_a) - """) - colitems = modcol.collect() - assert len(colitems) == 2 - assert [item.name for item in colitems] == ['test_b', 'test_a'] - - -class TestConftestCustomization: - def test_pytest_pycollect_module(self, testdir): - testdir.makeconftest(""" - import pytest - class MyModule(pytest.Module): - pass - def pytest_pycollect_makemodule(path, parent): - if path.basename == "test_xyz.py": - return MyModule(path, parent) - """) - testdir.makepyfile("def some(): pass") - testdir.makepyfile(test_xyz="") - result = testdir.runpytest("--collectonly") - result.stdout.fnmatch_lines([ - "*3 - - def test_traceback_error_during_import(self, testdir): - testdir.makepyfile(""" - x = 1 - x = 2 - x = 17 - asd - """) - result = testdir.runpytest() - assert result.ret != 0 - out = result.stdout.str() - assert "x = 1" not in out - assert "x = 2" not in out - result.stdout.fnmatch_lines([ - ">*asd*", - "E*NameError*", - ]) - result = testdir.runpytest("--fulltrace") - out = result.stdout.str() - assert "x = 1" in out - assert "x = 2" in out - result.stdout.fnmatch_lines([ - ">*asd*", - "E*NameError*", - ]) - -def test_getfuncargnames(): - def f(): pass - assert not funcargs.getfuncargnames(f) - def g(arg): pass - assert funcargs.getfuncargnames(g) == ['arg'] - def h(arg1, arg2="hello"): pass - assert funcargs.getfuncargnames(h) == ['arg1'] - def h(arg1, arg2, arg3="hello"): pass - assert funcargs.getfuncargnames(h) == ['arg1', 'arg2'] - class A: - def f(self, arg1, arg2="hello"): - pass - assert funcargs.getfuncargnames(A().f) == ['arg1'] - if sys.version_info < (3,0): - assert funcargs.getfuncargnames(A.f) == ['arg1'] - -def test_callspec_repr(): - cs = funcargs.CallSpec({}, 'hello', 1) - repr(cs) - cs = funcargs.CallSpec({}, 'hello', funcargs._notexists) - repr(cs) - -class TestFillFuncArgs: - def test_funcarg_lookupfails(self, testdir): - testdir.makeconftest(""" - def pytest_funcarg__xyzsomething(request): - return 42 - """) - item = testdir.getitem("def test_func(some): pass") - exc = py.test.raises(funcargs.FuncargRequest.LookupError, - "funcargs.fillfuncargs(item)") - s = str(exc.value) - assert s.find("xyzsomething") != -1 - - def test_funcarg_lookup_default(self, testdir): - item = testdir.getitem("def test_func(some, other=42): pass") - class Provider: - def pytest_funcarg__some(self, request): - return request.function.__name__ - item.config.pluginmanager.register(Provider()) - funcargs.fillfuncargs(item) - assert len(item.funcargs) == 1 - - def test_funcarg_basic(self, testdir): - item = testdir.getitem("def test_func(some, other): pass") - class Provider: - def pytest_funcarg__some(self, request): - return request.function.__name__ - def pytest_funcarg__other(self, request): - return 42 - item.config.pluginmanager.register(Provider()) - funcargs.fillfuncargs(item) - assert len(item.funcargs) == 2 - assert item.funcargs['some'] == "test_func" - assert item.funcargs['other'] == 42 - - def test_funcarg_lookup_modulelevel(self, testdir): - modcol = testdir.getmodulecol(""" - def pytest_funcarg__something(request): - return request.function.__name__ - - class TestClass: - def test_method(self, something): - pass - def test_func(something): - pass - """) - item1, item2 = testdir.genitems([modcol]) - funcargs.fillfuncargs(item1) - assert item1.funcargs['something'] == "test_method" - funcargs.fillfuncargs(item2) - assert item2.funcargs['something'] == "test_func" - - def test_funcarg_lookup_classlevel(self, testdir): - p = testdir.makepyfile(""" - class TestClass: - def pytest_funcarg__something(self, request): - return request.instance - def test_method(self, something): - assert something is self - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) - - def test_fillfuncargs_exposed(self, testdir): - item = testdir.getitem("def test_func(some, other=42): pass") - class Provider: - def pytest_funcarg__some(self, request): - return request.function.__name__ - item.config.pluginmanager.register(Provider()) - if hasattr(item, '_args'): - del item._args - pytest._fillfuncargs(item) - assert len(item.funcargs) == 1 - -class TestRequest: - def test_request_attributes(self, testdir): - item = testdir.getitem(""" - def pytest_funcarg__something(request): pass - def test_func(something): pass - """) - req = funcargs.FuncargRequest(item) - assert req.function == item.obj - assert req.keywords is item.keywords - assert hasattr(req.module, 'test_func') - assert req.cls is None - assert req.function.__name__ == "test_func" - assert req.config == item.config - assert repr(req).find(req.function.__name__) != -1 - - def test_request_attributes_method(self, testdir): - item, = testdir.getitems(""" - class TestB: - def test_func(self, something): - pass - """) - req = funcargs.FuncargRequest(item) - assert req.cls.__name__ == "TestB" - assert req.instance.__class__ == req.cls - - def XXXtest_request_contains_funcarg_name2factory(self, testdir): - modcol = testdir.getmodulecol(""" - def pytest_funcarg__something(request): - pass - class TestClass: - def test_method(self, something): - pass - """) - item1, = testdir.genitems([modcol]) - assert item1.name == "test_method" - name2factory = funcargs.FuncargRequest(item1)._name2factory - assert len(name2factory) == 1 - assert name2factory[0].__name__ == "pytest_funcarg__something" - - def test_getfuncargvalue_recursive(self, testdir): - testdir.makeconftest(""" - def pytest_funcarg__something(request): - return 1 - """) - item = testdir.getitem(""" - def pytest_funcarg__something(request): - return request.getfuncargvalue("something") + 1 - def test_func(something): - assert something == 2 - """) - req = funcargs.FuncargRequest(item) - val = req.getfuncargvalue("something") - assert val == 2 - - def test_getfuncargvalue(self, testdir): - item = testdir.getitem(""" - l = [2] - def pytest_funcarg__something(request): return 1 - def pytest_funcarg__other(request): - return l.pop() - def test_func(something): pass - """) - req = funcargs.FuncargRequest(item) - py.test.raises(req.LookupError, req.getfuncargvalue, "notexists") - val = req.getfuncargvalue("something") - assert val == 1 - val = req.getfuncargvalue("something") - assert val == 1 - val2 = req.getfuncargvalue("other") - assert val2 == 2 - val2 = req.getfuncargvalue("other") # see about caching - assert val2 == 2 - req._fillfuncargs() - assert item.funcargs == {'something': 1} - - def test_request_addfinalizer(self, testdir): - item = testdir.getitem(""" - teardownlist = [] - def pytest_funcarg__something(request): - request.addfinalizer(lambda: teardownlist.append(1)) - def test_func(something): pass - """) - req = funcargs.FuncargRequest(item) - req.config._setupstate.prepare(item) # XXX - req._fillfuncargs() - # successively check finalization calls - teardownlist = item.getparent(pytest.Module).obj.teardownlist - ss = item.config._setupstate - assert not teardownlist - ss.teardown_exact(item) - print(ss.stack) - assert teardownlist == [1] - - def test_request_addfinalizer_partial_setup_failure(self, testdir): - p = testdir.makepyfile(""" - l = [] - def pytest_funcarg__something(request): - request.addfinalizer(lambda: l.append(None)) - def test_func(something, missingarg): - pass - def test_second(): - assert len(l) == 1 - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines([ - "*1 passed*1 error*" - ]) - - def test_request_getmodulepath(self, testdir): - modcol = testdir.getmodulecol("def test_somefunc(): pass") - item, = testdir.genitems([modcol]) - req = funcargs.FuncargRequest(item) - assert req.fspath == modcol.fspath - -def test_applymarker(testdir): - item1,item2 = testdir.getitems(""" - class TestClass: - def test_func1(self, something): - pass - def test_func2(self, something): - pass - """) - req1 = funcargs.FuncargRequest(item1) - assert 'xfail' not in item1.keywords - req1.applymarker(py.test.mark.xfail) - assert 'xfail' in item1.keywords - assert 'skipif' not in item1.keywords - req1.applymarker(py.test.mark.skipif) - assert 'skipif' in item1.keywords - py.test.raises(ValueError, "req1.applymarker(42)") - -class TestRequestCachedSetup: - def test_request_cachedsetup(self, testdir): - item1,item2 = testdir.getitems(""" - class TestClass: - def test_func1(self, something): - pass - def test_func2(self, something): - pass - """) - req1 = funcargs.FuncargRequest(item1) - l = ["hello"] - def setup(): - return l.pop() - ret1 = req1.cached_setup(setup) - assert ret1 == "hello" - ret1b = req1.cached_setup(setup) - assert ret1 == ret1b - req2 = funcargs.FuncargRequest(item2) - ret2 = req2.cached_setup(setup) - assert ret2 == ret1 - - def test_request_cachedsetup_extrakey(self, testdir): - item1 = testdir.getitem("def test_func(): pass") - req1 = funcargs.FuncargRequest(item1) - l = ["hello", "world"] - def setup(): - return l.pop() - ret1 = req1.cached_setup(setup, extrakey=1) - ret2 = req1.cached_setup(setup, extrakey=2) - assert ret2 == "hello" - assert ret1 == "world" - ret1b = req1.cached_setup(setup, extrakey=1) - ret2b = req1.cached_setup(setup, extrakey=2) - assert ret1 == ret1b - assert ret2 == ret2b - - def test_request_cachedsetup_cache_deletion(self, testdir): - item1 = testdir.getitem("def test_func(): pass") - req1 = funcargs.FuncargRequest(item1) - l = [] - def setup(): - l.append("setup") - def teardown(val): - l.append("teardown") - ret1 = req1.cached_setup(setup, teardown, scope="function") - assert l == ['setup'] - # artificial call of finalizer - req1.config._setupstate._callfinalizers(item1) - assert l == ["setup", "teardown"] - ret2 = req1.cached_setup(setup, teardown, scope="function") - assert l == ["setup", "teardown", "setup"] - req1.config._setupstate._callfinalizers(item1) - assert l == ["setup", "teardown", "setup", "teardown"] - - def test_request_cached_setup_two_args(self, testdir): - testdir.makepyfile(""" - def pytest_funcarg__arg1(request): - return request.cached_setup(lambda: 42) - def pytest_funcarg__arg2(request): - return request.cached_setup(lambda: 17) - def test_two_different_setups(arg1, arg2): - assert arg1 != arg2 - """) - result = testdir.runpytest("-v") - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) - - def test_request_cached_setup_getfuncargvalue(self, testdir): - testdir.makepyfile(""" - def pytest_funcarg__arg1(request): - arg1 = request.getfuncargvalue("arg2") - return request.cached_setup(lambda: arg1 + 1) - def pytest_funcarg__arg2(request): - return request.cached_setup(lambda: 10) - def test_two_funcarg(arg1): - assert arg1 == 11 - """) - result = testdir.runpytest("-v") - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) - - def test_request_cached_setup_functional(self, testdir): - testdir.makepyfile(test_0=""" - l = [] - def pytest_funcarg__something(request): - val = request.cached_setup(fsetup, fteardown) - return val - def fsetup(mycache=[1]): - l.append(mycache.pop()) - return l - def fteardown(something): - l.remove(something[0]) - l.append(2) - def test_list_once(something): - assert something == [1] - def test_list_twice(something): - assert something == [1] - """) - testdir.makepyfile(test_1=""" - import test_0 # should have run already - def test_check_test0_has_teardown_correct(): - assert test_0.l == [2] - """) - result = testdir.runpytest("-v") - result.stdout.fnmatch_lines([ - "*3 passed*" - ]) - -class TestMetafunc: - def test_no_funcargs(self, testdir): - def function(): pass - metafunc = funcargs.Metafunc(function) - assert not metafunc.funcargnames - - def test_function_basic(self): - def func(arg1, arg2="qwe"): pass - metafunc = funcargs.Metafunc(func) - assert len(metafunc.funcargnames) == 1 - assert 'arg1' in metafunc.funcargnames - assert metafunc.function is func - assert metafunc.cls is None - - def test_addcall_no_args(self): - def func(arg1): pass - metafunc = funcargs.Metafunc(func) - metafunc.addcall() - assert len(metafunc._calls) == 1 - call = metafunc._calls[0] - assert call.id == "0" - assert not hasattr(call, 'param') - - def test_addcall_id(self): - def func(arg1): pass - metafunc = funcargs.Metafunc(func) - py.test.raises(ValueError, "metafunc.addcall(id=None)") - - metafunc.addcall(id=1) - py.test.raises(ValueError, "metafunc.addcall(id=1)") - py.test.raises(ValueError, "metafunc.addcall(id='1')") - metafunc.addcall(id=2) - assert len(metafunc._calls) == 2 - assert metafunc._calls[0].id == "1" - assert metafunc._calls[1].id == "2" - - def test_addcall_param(self): - def func(arg1): pass - metafunc = funcargs.Metafunc(func) - class obj: pass - metafunc.addcall(param=obj) - metafunc.addcall(param=obj) - metafunc.addcall(param=1) - assert len(metafunc._calls) == 3 - assert metafunc._calls[0].param == obj - assert metafunc._calls[1].param == obj - assert metafunc._calls[2].param == 1 - - def test_addcall_funcargs(self): - def func(arg1): pass - metafunc = funcargs.Metafunc(func) - class obj: pass - metafunc.addcall(funcargs={"x": 2}) - metafunc.addcall(funcargs={"x": 3}) - assert len(metafunc._calls) == 2 - assert metafunc._calls[0].funcargs == {'x': 2} - assert metafunc._calls[1].funcargs == {'x': 3} - assert not hasattr(metafunc._calls[1], 'param') - -class TestGenfuncFunctional: - def test_attributes(self, testdir): - p = testdir.makepyfile(""" - # assumes that generate/provide runs in the same process - import py - def pytest_generate_tests(metafunc): - metafunc.addcall(param=metafunc) - - def pytest_funcarg__metafunc(request): - assert request._pyfuncitem._genid == "0" - return request.param - - def test_function(metafunc, pytestconfig): - assert metafunc.config == pytestconfig - assert metafunc.module.__name__ == __name__ - assert metafunc.function == test_function - assert metafunc.cls is None - - class TestClass: - def test_method(self, metafunc, pytestconfig): - assert metafunc.config == pytestconfig - assert metafunc.module.__name__ == __name__ - if py.std.sys.version_info > (3, 0): - unbound = TestClass.test_method - else: - unbound = TestClass.test_method.im_func - # XXX actually have an unbound test function here? - assert metafunc.function == unbound - assert metafunc.cls == TestClass - """) - result = testdir.runpytest(p, "-v") - result.stdout.fnmatch_lines([ - "*2 passed in*", - ]) - - def test_addcall_with_two_funcargs_generators(self, testdir): - testdir.makeconftest(""" - def pytest_generate_tests(metafunc): - assert "arg1" in metafunc.funcargnames - metafunc.addcall(funcargs=dict(arg1=1, arg2=2)) - """) - p = testdir.makepyfile(""" - def pytest_generate_tests(metafunc): - metafunc.addcall(funcargs=dict(arg1=1, arg2=1)) - - class TestClass: - def test_myfunc(self, arg1, arg2): - assert arg1 == arg2 - """) - result = testdir.runpytest("-v", p) - result.stdout.fnmatch_lines([ - "*test_myfunc*0*PASS*", - "*test_myfunc*1*FAIL*", - "*1 failed, 1 passed*" - ]) - - def test_two_functions(self, testdir): - p = testdir.makepyfile(""" - def pytest_generate_tests(metafunc): - metafunc.addcall(param=10) - metafunc.addcall(param=20) - - def pytest_funcarg__arg1(request): - return request.param - - def test_func1(arg1): - assert arg1 == 10 - def test_func2(arg1): - assert arg1 in (10, 20) - """) - result = testdir.runpytest("-v", p) - result.stdout.fnmatch_lines([ - "*test_func1*0*PASS*", - "*test_func1*1*FAIL*", - "*test_func2*PASS*", - "*1 failed, 3 passed*" - ]) - - def test_generate_plugin_and_module(self, testdir): - testdir.makeconftest(""" - def pytest_generate_tests(metafunc): - assert "arg1" in metafunc.funcargnames - metafunc.addcall(id="world", param=(2,100)) - """) - p = testdir.makepyfile(""" - def pytest_generate_tests(metafunc): - metafunc.addcall(param=(1,1), id="hello") - - def pytest_funcarg__arg1(request): - return request.param[0] - def pytest_funcarg__arg2(request): - return request.param[1] - - class TestClass: - def test_myfunc(self, arg1, arg2): - assert arg1 == arg2 - """) - result = testdir.runpytest("-v", p) - result.stdout.fnmatch_lines([ - "*test_myfunc*hello*PASS*", - "*test_myfunc*world*FAIL*", - "*1 failed, 1 passed*" - ]) - - def test_generate_tests_in_class(self, testdir): - p = testdir.makepyfile(""" - class TestClass: - def pytest_generate_tests(self, metafunc): - metafunc.addcall(funcargs={'hello': 'world'}, id="hello") - - def test_myfunc(self, hello): - assert hello == "world" - """) - result = testdir.runpytest("-v", p) - result.stdout.fnmatch_lines([ - "*test_myfunc*hello*PASS*", - "*1 passed*" - ]) - - def test_two_functions_not_same_instance(self, testdir): - p = testdir.makepyfile(""" - def pytest_generate_tests(metafunc): - metafunc.addcall({'arg1': 10}) - metafunc.addcall({'arg1': 20}) - - class TestClass: - def test_func(self, arg1): - assert not hasattr(self, 'x') - self.x = 1 - """) - result = testdir.runpytest("-v", p) - result.stdout.fnmatch_lines([ - "*test_func*0*PASS*", - "*test_func*1*PASS*", - "*2 pass*", - ]) - - -def test_conftest_funcargs_only_available_in_subdir(testdir): - sub1 = testdir.mkpydir("sub1") - sub2 = testdir.mkpydir("sub2") - sub1.join("conftest.py").write(py.code.Source(""" - import py - def pytest_funcarg__arg1(request): - py.test.raises(Exception, "request.getfuncargvalue('arg2')") - """)) - sub2.join("conftest.py").write(py.code.Source(""" - import py - def pytest_funcarg__arg2(request): - py.test.raises(Exception, "request.getfuncargvalue('arg1')") - """)) - - sub1.join("test_in_sub1.py").write("def test_1(arg1): pass") - sub2.join("test_in_sub2.py").write("def test_2(arg2): pass") - result = testdir.runpytest("-v") - result.stdout.fnmatch_lines([ - "*2 passed*" - ]) - -def test_funcarg_non_pycollectobj(testdir): # rough jstests usage - testdir.makeconftest(""" - import pytest - def pytest_pycollect_makeitem(collector, name, obj): - if name == "MyClass": - return MyCollector(name, parent=collector) - class MyCollector(pytest.Collector): - def reportinfo(self): - return self.fspath, 3, "xyz" - """) - modcol = testdir.getmodulecol(""" - def pytest_funcarg__arg1(request): - return 42 - class MyClass: - pass - """) - clscol = modcol.collect()[0] - clscol.obj = lambda arg1: None - clscol.funcargs = {} - funcargs.fillfuncargs(clscol) - assert clscol.funcargs['arg1'] == 42 - - -def test_funcarg_lookup_error(testdir): - p = testdir.makepyfile(""" - def test_lookup_error(unknown): - pass - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*ERROR at setup of test_lookup_error*", - "*def test_lookup_error(unknown):*", - "*LookupError: no factory found*unknown*", - "*available funcargs*", - "*1 error*", - ]) - assert "INTERNAL" not in result.stdout.str() - -class TestReportInfo: - def test_itemreport_reportinfo(self, testdir, linecomp): - testdir.makeconftest(""" - import pytest - class MyFunction(pytest.Function): - def reportinfo(self): - return "ABCDE", 42, "custom" - def pytest_pycollect_makeitem(collector, name, obj): - if name == "test_func": - return MyFunction(name, parent=collector) - """) - item = testdir.getitem("def test_func(): pass") - runner = item.config.pluginmanager.getplugin("runner") - assert item.location == ("ABCDE", 42, "custom") - - def test_func_reportinfo(self, testdir): - item = testdir.getitem("def test_func(): pass") - fspath, lineno, modpath = item.reportinfo() - assert fspath == item.fspath - assert lineno == 0 - assert modpath == "test_func" - - def test_class_reportinfo(self, testdir): - modcol = testdir.getmodulecol(""" - # lineno 0 - class TestClass: - def test_hello(self): pass - """) - classcol = testdir.collect_by_name(modcol, "TestClass") - fspath, lineno, msg = classcol.reportinfo() - assert fspath == modcol.fspath - assert lineno == 1 - assert msg == "TestClass" - - def test_generator_reportinfo(self, testdir): - modcol = testdir.getmodulecol(""" - # lineno 0 - def test_gen(): - def check(x): - assert x - yield check, 3 - """) - gencol = testdir.collect_by_name(modcol, "test_gen") - fspath, lineno, modpath = gencol.reportinfo() - assert fspath == modcol.fspath - assert lineno == 1 - assert modpath == "test_gen" - - genitem = gencol.collect()[0] - fspath, lineno, modpath = genitem.reportinfo() - assert fspath == modcol.fspath - assert lineno == 2 - assert modpath == "test_gen[0]" - """ - def test_func(): - pass - def test_genfunc(): - def check(x): - pass - yield check, 3 - class TestClass: - def test_method(self): - pass - """ - -def test_show_funcarg(testdir): - result = testdir.runpytest("--funcargs") - result.stdout.fnmatch_lines([ - "*tmpdir*", - "*temporary directory*", - ] - ) - -class TestRaises: - def test_raises(self): - source = "int('qwe')" - excinfo = py.test.raises(ValueError, source) - code = excinfo.traceback[-1].frame.code - s = str(code.fullsource) - assert s == source - - def test_raises_exec(self): - py.test.raises(ValueError, "a,x = []") - - def test_raises_syntax_error(self): - py.test.raises(SyntaxError, "qwe qwe qwe") - - def test_raises_function(self): - py.test.raises(ValueError, int, 'hello') - - def test_raises_callable_no_exception(self): - class A: - def __call__(self): - pass - try: - py.test.raises(ValueError, A()) - except py.test.raises.Exception: - pass - - @py.test.mark.skipif('sys.version < "2.5"') - def test_raises_as_contextmanager(self, testdir): - testdir.makepyfile(""" - from __future__ import with_statement - import py - - def test_simple(): - with py.test.raises(ZeroDivisionError) as excinfo: - assert isinstance(excinfo, py.code.ExceptionInfo) - 1/0 - print (excinfo) - assert excinfo.type == ZeroDivisionError - - def test_noraise(): - with py.test.raises(py.test.raises.Exception): - with py.test.raises(ValueError): - int() - - def test_raise_wrong_exception_passes_by(): - with py.test.raises(ZeroDivisionError): - with py.test.raises(ValueError): - 1/0 - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - '*3 passed*', - ]) - - - --- a/testing/plugin/test_mark.py +++ /dev/null @@ -1,295 +0,0 @@ -import py -from _pytest.mark import MarkGenerator as Mark - -class TestMark: - def test_pytest_mark_notcallable(self): - mark = Mark() - py.test.raises((AttributeError, TypeError), "mark()") - - def test_pytest_mark_bare(self): - mark = Mark() - def f(): - pass - mark.hello(f) - assert f.hello - - def test_pytest_mark_keywords(self): - mark = Mark() - def f(): - pass - mark.world(x=3, y=4)(f) - assert f.world - assert f.world.kwargs['x'] == 3 - assert f.world.kwargs['y'] == 4 - - def test_apply_multiple_and_merge(self): - mark = Mark() - def f(): - pass - marker = mark.world - mark.world(x=3)(f) - assert f.world.kwargs['x'] == 3 - mark.world(y=4)(f) - assert f.world.kwargs['x'] == 3 - assert f.world.kwargs['y'] == 4 - mark.world(y=1)(f) - assert f.world.kwargs['y'] == 1 - assert len(f.world.args) == 0 - - def test_pytest_mark_positional(self): - mark = Mark() - def f(): - pass - mark.world("hello")(f) - assert f.world.args[0] == "hello" - mark.world("world")(f) - -class TestFunctional: - def test_mark_per_function(self, testdir): - p = testdir.makepyfile(""" - import py - @py.test.mark.hello - def test_hello(): - assert hasattr(test_hello, 'hello') - """) - result = testdir.runpytest(p) - result.stdout.fnmatch_lines(["*passed*"]) - - def test_mark_per_module(self, testdir): - item = testdir.getitem(""" - import py - pytestmark = py.test.mark.hello - def test_func(): - pass - """) - keywords = item.keywords - assert 'hello' in keywords - - def test_marklist_per_class(self, testdir): - item = testdir.getitem(""" - import py - class TestClass: - pytestmark = [py.test.mark.hello, py.test.mark.world] - def test_func(self): - assert TestClass.test_func.hello - assert TestClass.test_func.world - """) - keywords = item.keywords - assert 'hello' in keywords - - def test_marklist_per_module(self, testdir): - item = testdir.getitem(""" - import py - pytestmark = [py.test.mark.hello, py.test.mark.world] - class TestClass: - def test_func(self): - assert TestClass.test_func.hello - assert TestClass.test_func.world - """) - keywords = item.keywords - assert 'hello' in keywords - assert 'world' in keywords - - @py.test.mark.skipif("sys.version_info < (2,6)") - def test_mark_per_class_decorator(self, testdir): - item = testdir.getitem(""" - import py - @py.test.mark.hello - class TestClass: - def test_func(self): - assert TestClass.test_func.hello - """) - keywords = item.keywords - assert 'hello' in keywords - - @py.test.mark.skipif("sys.version_info < (2,6)") - def test_mark_per_class_decorator_plus_existing_dec(self, testdir): - item = testdir.getitem(""" - import py - @py.test.mark.hello - class TestClass: - pytestmark = py.test.mark.world - def test_func(self): - assert TestClass.test_func.hello - assert TestClass.test_func.world - """) - keywords = item.keywords - assert 'hello' in keywords - assert 'world' in keywords - - def test_merging_markers(self, testdir): - p = testdir.makepyfile(""" - import py - pytestmark = py.test.mark.hello("pos1", x=1, y=2) - class TestClass: - # classlevel overrides module level - pytestmark = py.test.mark.hello(x=3) - @py.test.mark.hello("pos0", z=4) - def test_func(self): - pass - """) - items, rec = testdir.inline_genitems(p) - item, = items - keywords = item.keywords - marker = keywords['hello'] - assert marker.args == ["pos0", "pos1"] - assert marker.kwargs == {'x': 3, 'y': 2, 'z': 4} - - def test_mark_other(self, testdir): - py.test.raises(TypeError, ''' - testdir.getitem(""" - import py - class pytestmark: - pass - def test_func(): - pass - """) - ''') - - def test_mark_dynamically_in_funcarg(self, testdir): - testdir.makeconftest(""" - import py - def pytest_funcarg__arg(request): - request.applymarker(py.test.mark.hello) - def pytest_terminal_summary(terminalreporter): - l = terminalreporter.stats['passed'] - terminalreporter._tw.line("keyword: %s" % l[0].keywords) - """) - testdir.makepyfile(""" - def test_func(arg): - pass - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "keyword: *hello*" - ]) - - -class Test_genitems: - def test_check_collect_hashes(self, testdir): - p = testdir.makepyfile(""" - def test_1(): - pass - - def test_2(): - pass - """) - p.copy(p.dirpath(p.purebasename + "2" + ".py")) - items, reprec = testdir.inline_genitems(p.dirpath()) - assert len(items) == 4 - for numi, i in enumerate(items): - for numj, j in enumerate(items): - if numj != numi: - assert hash(i) != hash(j) - assert i != j - - def test_root_conftest_syntax_error(self, testdir): - # do we want to unify behaviour with - # test_subdir_conftest_error? - p = testdir.makepyfile(conftest="raise SyntaxError\n") - py.test.raises(SyntaxError, testdir.inline_genitems, p.dirpath()) - - def test_example_items1(self, testdir): - p = testdir.makepyfile(''' - def testone(): - pass - - class TestX: - def testmethod_one(self): - pass - - class TestY(TestX): - pass - ''') - items, reprec = testdir.inline_genitems(p) - assert len(items) == 3 - assert items[0].name == 'testone' - assert items[1].name == 'testmethod_one' - assert items[2].name == 'testmethod_one' - - # let's also test getmodpath here - assert items[0].getmodpath() == "testone" - assert items[1].getmodpath() == "TestX.testmethod_one" - assert items[2].getmodpath() == "TestY.testmethod_one" - - s = items[0].getmodpath(stopatmodule=False) - assert s.endswith("test_example_items1.testone") - print(s) - - -class TestKeywordSelection: - def test_select_simple(self, testdir): - file_test = testdir.makepyfile(""" - def test_one(): - assert 0 - class TestClass(object): - def test_method_one(self): - assert 42 == 43 - """) - def check(keyword, name): - reprec = testdir.inline_run("-s", "-k", keyword, file_test) - passed, skipped, failed = reprec.listoutcomes() - assert len(failed) == 1 - assert failed[0].nodeid.split("::")[-1] == name - 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.test', 'test_method_one') - - def test_select_extra_keywords(self, testdir): - p = testdir.makepyfile(test_select=""" - def test_1(): - pass - class TestClass: - def test_2(self): - pass - """) - testdir.makepyfile(conftest=""" - import py - def pytest_pycollect_makeitem(__multicall__, name): - if name == "TestClass": - item = __multicall__.execute() - item.keywords['xxx'] = True - return item - """) - for keyword in ('xxx', 'xxx test_2', 'TestClass', 'xxx -test_1', - 'TestClass test_2', 'xxx TestClass test_2',): - reprec = testdir.inline_run(p.dirpath(), '-s', '-k', keyword) - py.builtin.print_("keyword", repr(keyword)) - passed, skipped, failed = reprec.listoutcomes() - assert len(passed) == 1 - assert passed[0].nodeid.endswith("test_2") - dlist = reprec.getcalls("pytest_deselected") - assert len(dlist) == 1 - assert dlist[0].items[0].name == 'test_1' - - def test_select_starton(self, testdir): - threepass = testdir.makepyfile(test_threepass=""" - def test_one(): assert 1 - def test_two(): assert 1 - def test_three(): assert 1 - """) - reprec = testdir.inline_run("-k", "test_two:", threepass) - passed, skipped, failed = reprec.listoutcomes() - assert len(passed) == 2 - assert not failed - dlist = reprec.getcalls("pytest_deselected") - assert len(dlist) == 1 - item = dlist[0].items[0] - assert item.name == "test_one" - - - def test_keyword_extra(self, testdir): - p = testdir.makepyfile(""" - def test_one(): - assert 0 - test_one.mykeyword = True - """) - reprec = testdir.inline_run("-k", "-mykeyword", p) - passed, skipped, failed = reprec.countoutcomes() - assert passed + skipped + failed == 0 - reprec = testdir.inline_run("-k", "mykeyword", p) - passed, skipped, failed = reprec.countoutcomes() - assert failed == 1 --- /dev/null +++ b/testing/test_doctest.py @@ -0,0 +1,114 @@ +from _pytest.doctest import DoctestModule, DoctestTextfile +import py + +pytest_plugins = ["pytest_doctest"] + +class TestDoctests: + + def test_collect_testtextfile(self, testdir): + testdir.maketxtfile(whatever="") + checkfile = testdir.maketxtfile(test_something=""" + alskdjalsdk + >>> i = 5 + >>> i-1 + 4 + """) + for x in (testdir.tmpdir, checkfile): + #print "checking that %s returns custom items" % (x,) + items, reprec = testdir.inline_genitems(x) + assert len(items) == 1 + assert isinstance(items[0], DoctestTextfile) + + def test_collect_module(self, testdir): + path = testdir.makepyfile(whatever="#") + for p in (path, testdir.tmpdir): + items, reprec = testdir.inline_genitems(p, + '--doctest-modules') + assert len(items) == 1 + assert isinstance(items[0], DoctestModule) + + def test_simple_doctestfile(self, testdir): + p = testdir.maketxtfile(test_doc=""" + >>> x = 1 + >>> x == 1 + False + """) + reprec = testdir.inline_run(p, ) + reprec.assertoutcome(failed=1) + + def test_new_pattern(self, testdir): + p = testdir.maketxtfile(xdoc =""" + >>> x = 1 + >>> x == 1 + False + """) + reprec = testdir.inline_run(p, "--doctest-glob=x*.txt") + reprec.assertoutcome(failed=1) + + def test_doctest_unexpected_exception(self, testdir): + p = testdir.maketxtfile(""" + >>> i = 0 + >>> i = 1 + >>> x + 2 + """) + reprec = testdir.inline_run(p) + call = reprec.getcall("pytest_runtest_logreport") + assert call.report.failed + assert call.report.longrepr + # XXX + #testitem, = items + #excinfo = py.test.raises(Failed, "testitem.runtest()") + #repr = testitem.repr_failure(excinfo, ("", "")) + #assert repr.reprlocation + + def test_doctestmodule(self, testdir): + p = testdir.makepyfile(""" + ''' + >>> x = 1 + >>> x == 1 + False + + ''' + """) + reprec = testdir.inline_run(p, "--doctest-modules") + reprec.assertoutcome(failed=1) + + def test_doctestmodule_external_and_issue116(self, testdir): + p = testdir.mkpydir("hello") + p.join("__init__.py").write(py.code.Source(""" + def somefunc(): + ''' + >>> i = 0 + >>> i + 1 + 2 + ''' + """)) + result = testdir.runpytest(p, "--doctest-modules") + result.stdout.fnmatch_lines([ + '004 *>>> i = 0', + '005 *>>> i + 1', + '*Expected:', + "* 2", + "*Got:", + "* 1", + "*:5: DocTestFailure" + ]) + + + def test_txtfile_failing(self, testdir): + p = testdir.maketxtfile(""" + >>> i = 0 + >>> i + 1 + 2 + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + '001 >>> i = 0', + '002 >>> i + 1', + 'Expected:', + " 2", + "Got:", + " 1", + "*test_txtfile_failing.txt:2: DocTestFailure" + ]) --- a/testing/plugin/test_pytester.py +++ /dev/null @@ -1,114 +0,0 @@ -import py -import os, sys -from _pytest.pytester import LineMatcher, LineComp, HookRecorder -from _pytest.core import PluginManager - -def test_reportrecorder(testdir): - item = testdir.getitem("def test_func(): pass") - recorder = testdir.getreportrecorder(item.config) - assert not recorder.getfailures() - - py.test.xfail("internal reportrecorder tests need refactoring") - class rep: - excinfo = None - passed = False - failed = True - skipped = False - when = "call" - - recorder.hook.pytest_runtest_logreport(report=rep) - failures = recorder.getfailures() - assert failures == [rep] - failures = recorder.getfailures() - assert failures == [rep] - - class rep: - excinfo = None - passed = False - failed = False - skipped = True - when = "call" - rep.passed = False - rep.skipped = True - recorder.hook.pytest_runtest_logreport(report=rep) - - modcol = testdir.getmodulecol("") - rep = modcol.config.hook.pytest_make_collect_report(collector=modcol) - rep.passed = False - rep.failed = True - rep.skipped = False - recorder.hook.pytest_collectreport(report=rep) - - passed, skipped, failed = recorder.listoutcomes() - assert not passed and skipped and failed - - numpassed, numskipped, numfailed = recorder.countoutcomes() - assert numpassed == 0 - assert numskipped == 1 - assert numfailed == 1 - assert len(recorder.getfailedcollections()) == 1 - - recorder.unregister() - recorder.clear() - recorder.hook.pytest_runtest_logreport(report=rep) - py.test.raises(ValueError, "recorder.getfailures()") - - -def test_parseconfig(testdir): - config1 = testdir.parseconfig() - config2 = testdir.parseconfig() - assert config2 != config1 - assert config1 != py.test.config - -def test_testdir_runs_with_plugin(testdir): - testdir.makepyfile(""" - pytest_plugins = "pytest_pytester" - def test_hello(testdir): - assert 1 - """) - result = testdir.runpytest() - result.stdout.fnmatch_lines([ - "*1 passed*" - ]) - -def test_hookrecorder_basic(): - rec = HookRecorder(PluginManager()) - class ApiClass: - def pytest_xyz(self, arg): - "x" - rec.start_recording(ApiClass) - rec.hook.pytest_xyz(arg=123) - call = rec.popcall("pytest_xyz") - assert call.arg == 123 - assert call._name == "pytest_xyz" - py.test.raises(ValueError, "rec.popcall('abc')") - -def test_hookrecorder_basic_no_args_hook(): - rec = HookRecorder(PluginManager()) - apimod = type(os)('api') - def pytest_xyz(): - "x" - apimod.pytest_xyz = pytest_xyz - rec.start_recording(apimod) - rec.hook.pytest_xyz() - call = rec.popcall("pytest_xyz") - assert call._name == "pytest_xyz" - -def test_functional(testdir, linecomp): - reprec = testdir.inline_runsource(""" - import py - from _pytest.core import HookRelay, PluginManager - pytest_plugins="pytester" - def test_func(_pytest): - class ApiClass: - def pytest_xyz(self, arg): "x" - hook = HookRelay([ApiClass], PluginManager(load=False)) - rec = _pytest.gethookrecorder(hook) - class Plugin: - def pytest_xyz(self, arg): - return arg + 1 - rec._pluginmanager.register(Plugin()) - res = rec.hook.pytest_xyz(arg=41) - assert res == [42] - """) - reprec.assertoutcome(passed=1) --- a/testing/plugin/test_genscript.py +++ /dev/null @@ -1,41 +0,0 @@ -import py, os, sys -import subprocess - - -def pytest_funcarg__standalone(request): - return request.cached_setup(scope="module", setup=lambda: Standalone(request)) - -class Standalone: - def __init__(self, request): - self.testdir = request.getfuncargvalue("testdir") - script = "mypytest" - result = self.testdir.runpytest("--genscript=%s" % script) - assert result.ret == 0 - self.script = self.testdir.tmpdir.join(script) - assert self.script.check() - - def run(self, anypython, testdir, *args): - testdir.chdir() - return testdir._run(anypython, self.script, *args) - -def test_gen(testdir, anypython, standalone): - result = standalone.run(anypython, testdir, '--version') - assert result.ret == 0 - result.stderr.fnmatch_lines([ - "*imported from*mypytest*" - ]) - p = testdir.makepyfile("def test_func(): assert 0") - result = standalone.run(anypython, testdir, p) - assert result.ret != 0 - -def test_rundist(testdir, pytestconfig, standalone): - pytestconfig.pluginmanager.skipifmissing("xdist") - testdir.makepyfile(""" - def test_one(): - pass - """) - result = standalone.run(sys.executable, testdir, '-n', '3') - assert result.ret == 0 - result.stdout.fnmatch_lines([ - "*1 passed*", - ]) --- /dev/null +++ b/testing/test_capture.py @@ -0,0 +1,379 @@ +import py, os, sys +from _pytest.capture import CaptureManager + +needsosdup = py.test.mark.xfail("not hasattr(os, 'dup')") + +class TestCaptureManager: + def test_getmethod_default_no_fd(self, testdir, monkeypatch): + config = testdir.parseconfig(testdir.tmpdir) + assert config.getvalue("capture") is None + capman = CaptureManager() + monkeypatch.delattr(os, 'dup', raising=False) + try: + assert capman._getmethod(config, None) == "sys" + finally: + monkeypatch.undo() + + def test_configure_per_fspath(self, testdir): + config = testdir.parseconfig(testdir.tmpdir) + assert config.getvalue("capture") is None + capman = CaptureManager() + hasfd = hasattr(os, 'dup') + if hasfd: + assert capman._getmethod(config, None) == "fd" + else: + assert capman._getmethod(config, None) == "sys" + + for name in ('no', 'fd', 'sys'): + if not hasfd and name == 'fd': + continue + sub = testdir.tmpdir.mkdir("dir" + name) + sub.ensure("__init__.py") + sub.join("conftest.py").write('option_capture = %r' % name) + assert capman._getmethod(config, sub.join("test_hello.py")) == name + + @needsosdup + @py.test.mark.multi(method=['no', 'fd', 'sys']) + def test_capturing_basic_api(self, method): + capouter = py.io.StdCaptureFD() + old = sys.stdout, sys.stderr, sys.stdin + try: + capman = CaptureManager() + # call suspend without resume or start + outerr = capman.suspendcapture() + outerr = capman.suspendcapture() + assert outerr == ("", "") + capman.resumecapture(method) + print ("hello") + out, err = capman.suspendcapture() + if method == "no": + assert old == (sys.stdout, sys.stderr, sys.stdin) + else: + assert out == "hello\n" + capman.resumecapture(method) + out, err = capman.suspendcapture() + assert not out and not err + finally: + capouter.reset() + + @needsosdup + def test_juggle_capturings(self, testdir): + capouter = py.io.StdCaptureFD() + try: + config = testdir.parseconfig(testdir.tmpdir) + capman = CaptureManager() + capman.resumecapture("fd") + py.test.raises(ValueError, 'capman.resumecapture("fd")') + py.test.raises(ValueError, 'capman.resumecapture("sys")') + os.write(1, "hello\n".encode('ascii')) + out, err = capman.suspendcapture() + assert out == "hello\n" + capman.resumecapture("sys") + os.write(1, "hello\n".encode('ascii')) + py.builtin.print_("world", file=sys.stderr) + out, err = capman.suspendcapture() + assert not out + assert err == "world\n" + finally: + capouter.reset() + + at py.test.mark.multi(method=['fd', 'sys']) +def test_capturing_unicode(testdir, method): + if sys.version_info >= (3,0): + obj = "'b\u00f6y'" + else: + obj = "u'\u00f6y'" + testdir.makepyfile(""" + # coding=utf8 + # taken from issue 227 from nosetests + def test_unicode(): + import sys + print (sys.stdout) + print (%s) + """ % obj) + result = testdir.runpytest("--capture=%s" % method) + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + + at py.test.mark.multi(method=['fd', 'sys']) +def test_capturing_bytes_in_utf8_encoding(testdir, method): + testdir.makepyfile(""" + def test_unicode(): + print ('b\\u00f6y') + """) + result = testdir.runpytest("--capture=%s" % method) + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + +def test_collect_capturing(testdir): + p = testdir.makepyfile(""" + print ("collect %s failure" % 13) + import xyz42123 + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*Captured stdout*", + "*collect 13 failure*", + ]) + +class TestPerTestCapturing: + def test_capture_and_fixtures(self, testdir): + p = testdir.makepyfile(""" + def setup_module(mod): + print ("setup module") + def setup_function(function): + print ("setup " + function.__name__) + def test_func1(): + print ("in func1") + assert 0 + def test_func2(): + print ("in func2") + assert 0 + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "setup module*", + "setup test_func1*", + "in func1*", + "setup test_func2*", + "in func2*", + ]) + + @py.test.mark.xfail + def test_capture_scope_cache(self, testdir): + p = testdir.makepyfile(""" + import sys + def setup_module(func): + print ("module-setup") + def setup_function(func): + print ("function-setup") + def test_func(): + print ("in function") + assert 0 + def teardown_function(func): + print ("in teardown") + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*test_func():*", + "*Captured stdout during setup*", + "module-setup*", + "function-setup*", + "*Captured stdout*", + "in teardown*", + ]) + + + def test_no_carry_over(self, testdir): + p = testdir.makepyfile(""" + def test_func1(): + print ("in func1") + def test_func2(): + print ("in func2") + assert 0 + """) + result = testdir.runpytest(p) + s = result.stdout.str() + assert "in func1" not in s + assert "in func2" in s + + + def test_teardown_capturing(self, testdir): + p = testdir.makepyfile(""" + def setup_function(function): + print ("setup func1") + def teardown_function(function): + print ("teardown func1") + assert 0 + def test_func1(): + print ("in func1") + pass + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + '*teardown_function*', + '*Captured stdout*', + "setup func1*", + "in func1*", + "teardown func1*", + #"*1 fixture failure*" + ]) + + def test_teardown_final_capturing(self, testdir): + p = testdir.makepyfile(""" + def teardown_module(mod): + print ("teardown module") + assert 0 + def test_func(): + pass + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*def teardown_module(mod):*", + "*Captured stdout*", + "*teardown module*", + "*1 error*", + ]) + + def test_capturing_outerr(self, testdir): + p1 = testdir.makepyfile(""" + import sys + def test_capturing(): + print (42) + sys.stderr.write(str(23)) + def test_capturing_error(): + print (1) + sys.stderr.write(str(2)) + raise ValueError + """) + result = testdir.runpytest(p1) + result.stdout.fnmatch_lines([ + "*test_capturing_outerr.py .F", + "====* FAILURES *====", + "____*____", + "*test_capturing_outerr.py:8: ValueError", + "*--- Captured stdout ---*", + "1", + "*--- Captured stderr ---*", + "2", + ]) + +class TestLoggingInteraction: + def test_logging_stream_ownership(self, testdir): + p = testdir.makepyfile(""" + def test_logging(): + import logging + import py + stream = py.io.TextIO() + logging.basicConfig(stream=stream) + stream.close() # to free memory/release resources + """) + result = testdir.runpytest(p) + result.stderr.str().find("atexit") == -1 + + def test_logging_and_immediate_setupteardown(self, testdir): + p = testdir.makepyfile(""" + import logging + def setup_function(function): + logging.warn("hello1") + + def test_logging(): + logging.warn("hello2") + assert 0 + + def teardown_function(function): + logging.warn("hello3") + assert 0 + """) + for optargs in (('--capture=sys',), ('--capture=fd',)): + print (optargs) + result = testdir.runpytest(p, *optargs) + s = result.stdout.str() + result.stdout.fnmatch_lines([ + "*WARN*hello3", # errors show first! + "*WARN*hello1", + "*WARN*hello2", + ]) + # verify proper termination + assert "closed" not in s + + def test_logging_and_crossscope_fixtures(self, testdir): + p = testdir.makepyfile(""" + import logging + def setup_module(function): + logging.warn("hello1") + + def test_logging(): + logging.warn("hello2") + assert 0 + + def teardown_module(function): + logging.warn("hello3") + assert 0 + """) + for optargs in (('--capture=sys',), ('--capture=fd',)): + print (optargs) + result = testdir.runpytest(p, *optargs) + s = result.stdout.str() + result.stdout.fnmatch_lines([ + "*WARN*hello3", # errors come first + "*WARN*hello1", + "*WARN*hello2", + ]) + # verify proper termination + assert "closed" not in s + +class TestCaptureFuncarg: + def test_std_functional(self, testdir): + reprec = testdir.inline_runsource(""" + def test_hello(capsys): + print (42) + out, err = capsys.readouterr() + assert out.startswith("42") + """) + reprec.assertoutcome(passed=1) + + @needsosdup + def test_stdfd_functional(self, testdir): + reprec = testdir.inline_runsource(""" + def test_hello(capfd): + import os + os.write(1, "42".encode('ascii')) + out, err = capfd.readouterr() + assert out.startswith("42") + capfd.close() + """) + reprec.assertoutcome(passed=1) + + def test_partial_setup_failure(self, testdir): + p = testdir.makepyfile(""" + def test_hello(capsys, missingarg): + pass + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*test_partial_setup_failure*", + "*1 error*", + ]) + + @needsosdup + def test_keyboardinterrupt_disables_capturing(self, testdir): + p = testdir.makepyfile(""" + def test_hello(capfd): + import os + os.write(1, str(42).encode('ascii')) + raise KeyboardInterrupt() + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*KeyboardInterrupt*" + ]) + assert result.ret == 2 + +def test_setup_failure_does_not_kill_capturing(testdir): + sub1 = testdir.mkpydir("sub1") + sub1.join("conftest.py").write(py.code.Source(""" + def pytest_runtest_setup(item): + raise ValueError(42) + """)) + sub1.join("test_mod.py").write("def test_func1(): pass") + result = testdir.runpytest(testdir.tmpdir, '--traceconfig') + result.stdout.fnmatch_lines([ + "*ValueError(42)*", + "*1 error*" + ]) + +def test_fdfuncarg_skips_on_no_osdup(testdir): + testdir.makepyfile(""" + import os + if hasattr(os, 'dup'): + del os.dup + def test_hello(capfd): + pass + """) + result = testdir.runpytest("--capture=no") + result.stdout.fnmatch_lines([ + "*1 skipped*" + ]) --- a/testing/plugin/test_monkeypatch.py +++ /dev/null @@ -1,139 +0,0 @@ -import os, sys -import py -from _pytest.monkeypatch import monkeypatch as MonkeyPatch - -def test_setattr(): - class A: - x = 1 - monkeypatch = MonkeyPatch() - py.test.raises(AttributeError, "monkeypatch.setattr(A, 'notexists', 2)") - monkeypatch.setattr(A, 'y', 2, raising=False) - assert A.y == 2 - monkeypatch.undo() - assert not hasattr(A, 'y') - - monkeypatch = MonkeyPatch() - monkeypatch.setattr(A, 'x', 2) - assert A.x == 2 - monkeypatch.setattr(A, 'x', 3) - assert A.x == 3 - monkeypatch.undo() - assert A.x == 1 - - A.x = 5 - monkeypatch.undo() # double-undo makes no modification - assert A.x == 5 - -def test_delattr(): - class A: - x = 1 - monkeypatch = MonkeyPatch() - monkeypatch.delattr(A, 'x') - assert not hasattr(A, 'x') - monkeypatch.undo() - assert A.x == 1 - - monkeypatch = MonkeyPatch() - monkeypatch.delattr(A, 'x') - py.test.raises(AttributeError, "monkeypatch.delattr(A, 'y')") - monkeypatch.delattr(A, 'y', raising=False) - monkeypatch.setattr(A, 'x', 5, raising=False) - assert A.x == 5 - monkeypatch.undo() - assert A.x == 1 - -def test_setitem(): - d = {'x': 1} - monkeypatch = MonkeyPatch() - monkeypatch.setitem(d, 'x', 2) - monkeypatch.setitem(d, 'y', 1700) - monkeypatch.setitem(d, 'y', 1700) - assert d['x'] == 2 - assert d['y'] == 1700 - monkeypatch.setitem(d, 'x', 3) - assert d['x'] == 3 - monkeypatch.undo() - assert d['x'] == 1 - assert 'y' not in d - d['x'] = 5 - monkeypatch.undo() - assert d['x'] == 5 - -def test_delitem(): - d = {'x': 1} - monkeypatch = MonkeyPatch() - monkeypatch.delitem(d, 'x') - assert 'x' not in d - monkeypatch.delitem(d, 'y', raising=False) - py.test.raises(KeyError, "monkeypatch.delitem(d, 'y')") - assert not d - monkeypatch.setitem(d, 'y', 1700) - assert d['y'] == 1700 - d['hello'] = 'world' - monkeypatch.setitem(d, 'x', 1500) - assert d['x'] == 1500 - monkeypatch.undo() - assert d == {'hello': 'world', 'x': 1} - -def test_setenv(): - monkeypatch = MonkeyPatch() - monkeypatch.setenv('XYZ123', 2) - import os - assert os.environ['XYZ123'] == "2" - monkeypatch.undo() - assert 'XYZ123' not in os.environ - -def test_delenv(): - name = 'xyz1234' - assert name not in os.environ - monkeypatch = MonkeyPatch() - py.test.raises(KeyError, "monkeypatch.delenv(%r, raising=True)" % name) - monkeypatch.delenv(name, raising=False) - monkeypatch.undo() - os.environ[name] = "1" - try: - monkeypatch = MonkeyPatch() - monkeypatch.delenv(name) - assert name not in os.environ - monkeypatch.setenv(name, "3") - assert os.environ[name] == "3" - monkeypatch.undo() - assert os.environ[name] == "1" - finally: - if name in os.environ: - del os.environ[name] - -def test_setenv_prepend(): - import os - monkeypatch = MonkeyPatch() - monkeypatch.setenv('XYZ123', 2, prepend="-") - assert os.environ['XYZ123'] == "2" - monkeypatch.setenv('XYZ123', 3, prepend="-") - assert os.environ['XYZ123'] == "3-2" - monkeypatch.undo() - assert 'XYZ123' not in os.environ - -def test_monkeypatch_plugin(testdir): - reprec = testdir.inline_runsource(""" - def test_method(monkeypatch): - assert monkeypatch.__class__.__name__ == "monkeypatch" - """) - res = reprec.countoutcomes() - assert tuple(res) == (1, 0, 0), res - -def test_syspath_prepend(): - old = list(sys.path) - try: - monkeypatch = MonkeyPatch() - monkeypatch.syspath_prepend('world') - monkeypatch.syspath_prepend('hello') - assert sys.path[0] == "hello" - assert sys.path[1] == "world" - monkeypatch.undo() - assert sys.path == old - monkeypatch.undo() - assert sys.path == old - finally: - sys.path[:] = old - - --- /dev/null +++ b/testing/test_pytester.py @@ -0,0 +1,114 @@ +import py +import os, sys +from _pytest.pytester import LineMatcher, LineComp, HookRecorder +from _pytest.core import PluginManager + +def test_reportrecorder(testdir): + item = testdir.getitem("def test_func(): pass") + recorder = testdir.getreportrecorder(item.config) + assert not recorder.getfailures() + + py.test.xfail("internal reportrecorder tests need refactoring") + class rep: + excinfo = None + passed = False + failed = True + skipped = False + when = "call" + + recorder.hook.pytest_runtest_logreport(report=rep) + failures = recorder.getfailures() + assert failures == [rep] + failures = recorder.getfailures() + assert failures == [rep] + + class rep: + excinfo = None + passed = False + failed = False + skipped = True + when = "call" + rep.passed = False + rep.skipped = True + recorder.hook.pytest_runtest_logreport(report=rep) + + modcol = testdir.getmodulecol("") + rep = modcol.config.hook.pytest_make_collect_report(collector=modcol) + rep.passed = False + rep.failed = True + rep.skipped = False + recorder.hook.pytest_collectreport(report=rep) + + passed, skipped, failed = recorder.listoutcomes() + assert not passed and skipped and failed + + numpassed, numskipped, numfailed = recorder.countoutcomes() + assert numpassed == 0 + assert numskipped == 1 + assert numfailed == 1 + assert len(recorder.getfailedcollections()) == 1 + + recorder.unregister() + recorder.clear() + recorder.hook.pytest_runtest_logreport(report=rep) + py.test.raises(ValueError, "recorder.getfailures()") + + +def test_parseconfig(testdir): + config1 = testdir.parseconfig() + config2 = testdir.parseconfig() + assert config2 != config1 + assert config1 != py.test.config + +def test_testdir_runs_with_plugin(testdir): + testdir.makepyfile(""" + pytest_plugins = "pytest_pytester" + def test_hello(testdir): + assert 1 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + +def test_hookrecorder_basic(): + rec = HookRecorder(PluginManager()) + class ApiClass: + def pytest_xyz(self, arg): + "x" + rec.start_recording(ApiClass) + rec.hook.pytest_xyz(arg=123) + call = rec.popcall("pytest_xyz") + assert call.arg == 123 + assert call._name == "pytest_xyz" + py.test.raises(ValueError, "rec.popcall('abc')") + +def test_hookrecorder_basic_no_args_hook(): + rec = HookRecorder(PluginManager()) + apimod = type(os)('api') + def pytest_xyz(): + "x" + apimod.pytest_xyz = pytest_xyz + rec.start_recording(apimod) + rec.hook.pytest_xyz() + call = rec.popcall("pytest_xyz") + assert call._name == "pytest_xyz" + +def test_functional(testdir, linecomp): + reprec = testdir.inline_runsource(""" + import py + from _pytest.core import HookRelay, PluginManager + pytest_plugins="pytester" + def test_func(_pytest): + class ApiClass: + def pytest_xyz(self, arg): "x" + hook = HookRelay([ApiClass], PluginManager(load=False)) + rec = _pytest.gethookrecorder(hook) + class Plugin: + def pytest_xyz(self, arg): + return arg + 1 + rec._pluginmanager.register(Plugin()) + res = rec.hook.pytest_xyz(arg=41) + assert res == [42] + """) + reprec.assertoutcome(passed=1) --- /dev/null +++ b/testing/test_python.py @@ -0,0 +1,1176 @@ +import pytest, py, sys +from _pytest import python as funcargs + +class TestModule: + def test_failing_import(self, testdir): + modcol = testdir.getmodulecol("import alksdjalskdjalkjals") + py.test.raises(ImportError, modcol.collect) + py.test.raises(ImportError, modcol.collect) + + def test_import_duplicate(self, testdir): + a = testdir.mkdir("a") + b = testdir.mkdir("b") + p = a.ensure("test_whatever.py") + p.pyimport() + del py.std.sys.modules['test_whatever'] + b.ensure("test_whatever.py") + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*import*mismatch*", + "*imported*test_whatever*", + "*%s*" % a.join("test_whatever.py"), + "*not the same*", + "*%s*" % b.join("test_whatever.py"), + "*HINT*", + ]) + + def test_syntax_error_in_module(self, testdir): + modcol = testdir.getmodulecol("this is a syntax error") + py.test.raises(modcol.CollectError, modcol.collect) + py.test.raises(modcol.CollectError, modcol.collect) + + def test_module_considers_pluginmanager_at_import(self, testdir): + modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") + py.test.raises(ImportError, "modcol.obj") + +class TestClass: + def test_class_with_init_not_collected(self, testdir): + modcol = testdir.getmodulecol(""" + class TestClass1: + def __init__(self): + pass + class TestClass2(object): + def __init__(self): + pass + """) + l = modcol.collect() + assert len(l) == 0 + +class TestGenerator: + def test_generative_functions(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + + def test_gen(): + yield func1, 17, 3*5 + yield func1, 42, 6*7 + """) + colitems = modcol.collect() + assert len(colitems) == 1 + gencol = colitems[0] + assert isinstance(gencol, pytest.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], pytest.Function) + assert isinstance(gencolitems[1], pytest.Function) + assert gencolitems[0].name == '[0]' + assert gencolitems[0].obj.__name__ == 'func1' + + def test_generative_methods(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + class TestGenMethods: + def test_gen(self): + yield func1, 17, 3*5 + yield func1, 42, 6*7 + """) + gencol = modcol.collect()[0].collect()[0].collect()[0] + assert isinstance(gencol, pytest.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], pytest.Function) + assert isinstance(gencolitems[1], pytest.Function) + assert gencolitems[0].name == '[0]' + assert gencolitems[0].obj.__name__ == 'func1' + + def test_generative_functions_with_explicit_names(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + + def test_gen(): + yield "seventeen", func1, 17, 3*5 + yield "fortytwo", func1, 42, 6*7 + """) + colitems = modcol.collect() + assert len(colitems) == 1 + gencol = colitems[0] + assert isinstance(gencol, pytest.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], pytest.Function) + assert isinstance(gencolitems[1], pytest.Function) + assert gencolitems[0].name == "['seventeen']" + assert gencolitems[0].obj.__name__ == 'func1' + assert gencolitems[1].name == "['fortytwo']" + assert gencolitems[1].obj.__name__ == 'func1' + + def test_generative_functions_unique_explicit_names(self, testdir): + # generative + modcol = testdir.getmodulecol(""" + def func(): pass + def test_gen(): + yield "name", func + yield "name", func + """) + colitems = modcol.collect() + assert len(colitems) == 1 + gencol = colitems[0] + assert isinstance(gencol, pytest.Generator) + py.test.raises(ValueError, "gencol.collect()") + + def test_generative_methods_with_explicit_names(self, testdir): + modcol = testdir.getmodulecol(""" + def func1(arg, arg2): + assert arg == arg2 + class TestGenMethods: + def test_gen(self): + yield "m1", func1, 17, 3*5 + yield "m2", func1, 42, 6*7 + """) + gencol = modcol.collect()[0].collect()[0].collect()[0] + assert isinstance(gencol, pytest.Generator) + gencolitems = gencol.collect() + assert len(gencolitems) == 2 + assert isinstance(gencolitems[0], pytest.Function) + assert isinstance(gencolitems[1], pytest.Function) + assert gencolitems[0].name == "['m1']" + assert gencolitems[0].obj.__name__ == 'func1' + assert gencolitems[1].name == "['m2']" + assert gencolitems[1].obj.__name__ == 'func1' + + def test_order_of_execution_generator_same_codeline(self, testdir, tmpdir): + o = testdir.makepyfile(""" + def test_generative_order_of_execution(): + import py + test_list = [] + expected_list = list(range(6)) + + def list_append(item): + test_list.append(item) + + def assert_order_of_execution(): + py.builtin.print_('expected order', expected_list) + py.builtin.print_('but got ', test_list) + assert test_list == expected_list + + for i in expected_list: + yield list_append, i + yield assert_order_of_execution + """) + reprec = testdir.inline_run(o) + passed, skipped, failed = reprec.countoutcomes() + assert passed == 7 + assert not skipped and not failed + + def test_order_of_execution_generator_different_codeline(self, testdir): + o = testdir.makepyfile(""" + def test_generative_tests_different_codeline(): + import py + test_list = [] + expected_list = list(range(3)) + + def list_append_2(): + test_list.append(2) + + def list_append_1(): + test_list.append(1) + + def list_append_0(): + test_list.append(0) + + def assert_order_of_execution(): + py.builtin.print_('expected order', expected_list) + py.builtin.print_('but got ', test_list) + assert test_list == expected_list + + yield list_append_0 + yield list_append_1 + yield list_append_2 + yield assert_order_of_execution + """) + reprec = testdir.inline_run(o) + passed, skipped, failed = reprec.countoutcomes() + assert passed == 4 + assert not skipped and not failed + +class TestFunction: + def test_getmodulecollector(self, testdir): + item = testdir.getitem("def test_func(): pass") + modcol = item.getparent(pytest.Module) + assert isinstance(modcol, pytest.Module) + assert hasattr(modcol.obj, 'test_func') + + def test_function_equality(self, testdir, tmpdir): + config = testdir.reparseconfig() + session = testdir.Session(config) + f1 = pytest.Function(name="name", config=config, + args=(1,), callobj=isinstance, session=session) + f2 = pytest.Function(name="name",config=config, + args=(1,), callobj=py.builtin.callable, session=session) + assert not f1 == f2 + assert f1 != f2 + f3 = pytest.Function(name="name", config=config, + args=(1,2), callobj=py.builtin.callable, session=session) + assert not f3 == f2 + assert f3 != f2 + + assert not f3 == f1 + assert f3 != f1 + + f1_b = pytest.Function(name="name", config=config, + args=(1,), callobj=isinstance, session=session) + assert f1 == f1_b + assert not f1 != f1_b + + def test_function_equality_with_callspec(self, testdir, tmpdir): + config = testdir.reparseconfig() + class callspec1: + param = 1 + funcargs = {} + id = "hello" + class callspec2: + param = 1 + funcargs = {} + id = "world" + session = testdir.Session(config) + f5 = pytest.Function(name="name", config=config, + callspec=callspec1, callobj=isinstance, session=session) + f5b = pytest.Function(name="name", config=config, + callspec=callspec2, callobj=isinstance, session=session) + assert f5 != f5b + assert not (f5 == f5b) + + def test_pyfunc_call(self, testdir): + item = testdir.getitem("def test_func(): raise ValueError") + config = item.config + class MyPlugin1: + def pytest_pyfunc_call(self, pyfuncitem): + raise ValueError + class MyPlugin2: + def pytest_pyfunc_call(self, pyfuncitem): + return True + config.pluginmanager.register(MyPlugin1()) + config.pluginmanager.register(MyPlugin2()) + config.hook.pytest_pyfunc_call(pyfuncitem=item) + +class TestSorting: + def test_check_equality(self, testdir): + modcol = testdir.getmodulecol(""" + def test_pass(): pass + def test_fail(): assert 0 + """) + fn1 = testdir.collect_by_name(modcol, "test_pass") + assert isinstance(fn1, pytest.Function) + fn2 = testdir.collect_by_name(modcol, "test_pass") + assert isinstance(fn2, pytest.Function) + + assert fn1 == fn2 + assert fn1 != modcol + if py.std.sys.version_info < (3, 0): + assert cmp(fn1, fn2) == 0 + assert hash(fn1) == hash(fn2) + + fn3 = testdir.collect_by_name(modcol, "test_fail") + assert isinstance(fn3, pytest.Function) + assert not (fn1 == fn3) + assert fn1 != fn3 + + for fn in fn1,fn2,fn3: + assert fn != 3 + assert fn != modcol + assert fn != [1,2,3] + assert [1,2,3] != fn + assert modcol != fn + + def test_allow_sane_sorting_for_decorators(self, testdir): + modcol = testdir.getmodulecol(""" + def dec(f): + g = lambda: f(2) + g.place_as = f + return g + + + def test_b(y): + pass + test_b = dec(test_b) + + def test_a(y): + pass + test_a = dec(test_a) + """) + colitems = modcol.collect() + assert len(colitems) == 2 + assert [item.name for item in colitems] == ['test_b', 'test_a'] + + +class TestConftestCustomization: + def test_pytest_pycollect_module(self, testdir): + testdir.makeconftest(""" + import pytest + class MyModule(pytest.Module): + pass + def pytest_pycollect_makemodule(path, parent): + if path.basename == "test_xyz.py": + return MyModule(path, parent) + """) + testdir.makepyfile("def some(): pass") + testdir.makepyfile(test_xyz="") + result = testdir.runpytest("--collectonly") + result.stdout.fnmatch_lines([ + "*3 + + def test_traceback_error_during_import(self, testdir): + testdir.makepyfile(""" + x = 1 + x = 2 + x = 17 + asd + """) + result = testdir.runpytest() + assert result.ret != 0 + out = result.stdout.str() + assert "x = 1" not in out + assert "x = 2" not in out + result.stdout.fnmatch_lines([ + ">*asd*", + "E*NameError*", + ]) + result = testdir.runpytest("--fulltrace") + out = result.stdout.str() + assert "x = 1" in out + assert "x = 2" in out + result.stdout.fnmatch_lines([ + ">*asd*", + "E*NameError*", + ]) + +def test_getfuncargnames(): + def f(): pass + assert not funcargs.getfuncargnames(f) + def g(arg): pass + assert funcargs.getfuncargnames(g) == ['arg'] + def h(arg1, arg2="hello"): pass + assert funcargs.getfuncargnames(h) == ['arg1'] + def h(arg1, arg2, arg3="hello"): pass + assert funcargs.getfuncargnames(h) == ['arg1', 'arg2'] + class A: + def f(self, arg1, arg2="hello"): + pass + assert funcargs.getfuncargnames(A().f) == ['arg1'] + if sys.version_info < (3,0): + assert funcargs.getfuncargnames(A.f) == ['arg1'] + +def test_callspec_repr(): + cs = funcargs.CallSpec({}, 'hello', 1) + repr(cs) + cs = funcargs.CallSpec({}, 'hello', funcargs._notexists) + repr(cs) + +class TestFillFuncArgs: + def test_funcarg_lookupfails(self, testdir): + testdir.makeconftest(""" + def pytest_funcarg__xyzsomething(request): + return 42 + """) + item = testdir.getitem("def test_func(some): pass") + exc = py.test.raises(funcargs.FuncargRequest.LookupError, + "funcargs.fillfuncargs(item)") + s = str(exc.value) + assert s.find("xyzsomething") != -1 + + def test_funcarg_lookup_default(self, testdir): + item = testdir.getitem("def test_func(some, other=42): pass") + class Provider: + def pytest_funcarg__some(self, request): + return request.function.__name__ + item.config.pluginmanager.register(Provider()) + funcargs.fillfuncargs(item) + assert len(item.funcargs) == 1 + + def test_funcarg_basic(self, testdir): + item = testdir.getitem("def test_func(some, other): pass") + class Provider: + def pytest_funcarg__some(self, request): + return request.function.__name__ + def pytest_funcarg__other(self, request): + return 42 + item.config.pluginmanager.register(Provider()) + funcargs.fillfuncargs(item) + assert len(item.funcargs) == 2 + assert item.funcargs['some'] == "test_func" + assert item.funcargs['other'] == 42 + + def test_funcarg_lookup_modulelevel(self, testdir): + modcol = testdir.getmodulecol(""" + def pytest_funcarg__something(request): + return request.function.__name__ + + class TestClass: + def test_method(self, something): + pass + def test_func(something): + pass + """) + item1, item2 = testdir.genitems([modcol]) + funcargs.fillfuncargs(item1) + assert item1.funcargs['something'] == "test_method" + funcargs.fillfuncargs(item2) + assert item2.funcargs['something'] == "test_func" + + def test_funcarg_lookup_classlevel(self, testdir): + p = testdir.makepyfile(""" + class TestClass: + def pytest_funcarg__something(self, request): + return request.instance + def test_method(self, something): + assert something is self + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + + def test_fillfuncargs_exposed(self, testdir): + item = testdir.getitem("def test_func(some, other=42): pass") + class Provider: + def pytest_funcarg__some(self, request): + return request.function.__name__ + item.config.pluginmanager.register(Provider()) + if hasattr(item, '_args'): + del item._args + pytest._fillfuncargs(item) + assert len(item.funcargs) == 1 + +class TestRequest: + def test_request_attributes(self, testdir): + item = testdir.getitem(""" + def pytest_funcarg__something(request): pass + def test_func(something): pass + """) + req = funcargs.FuncargRequest(item) + assert req.function == item.obj + assert req.keywords is item.keywords + assert hasattr(req.module, 'test_func') + assert req.cls is None + assert req.function.__name__ == "test_func" + assert req.config == item.config + assert repr(req).find(req.function.__name__) != -1 + + def test_request_attributes_method(self, testdir): + item, = testdir.getitems(""" + class TestB: + def test_func(self, something): + pass + """) + req = funcargs.FuncargRequest(item) + assert req.cls.__name__ == "TestB" + assert req.instance.__class__ == req.cls + + def XXXtest_request_contains_funcarg_name2factory(self, testdir): + modcol = testdir.getmodulecol(""" + def pytest_funcarg__something(request): + pass + class TestClass: + def test_method(self, something): + pass + """) + item1, = testdir.genitems([modcol]) + assert item1.name == "test_method" + name2factory = funcargs.FuncargRequest(item1)._name2factory + assert len(name2factory) == 1 + assert name2factory[0].__name__ == "pytest_funcarg__something" + + def test_getfuncargvalue_recursive(self, testdir): + testdir.makeconftest(""" + def pytest_funcarg__something(request): + return 1 + """) + item = testdir.getitem(""" + def pytest_funcarg__something(request): + return request.getfuncargvalue("something") + 1 + def test_func(something): + assert something == 2 + """) + req = funcargs.FuncargRequest(item) + val = req.getfuncargvalue("something") + assert val == 2 + + def test_getfuncargvalue(self, testdir): + item = testdir.getitem(""" + l = [2] + def pytest_funcarg__something(request): return 1 + def pytest_funcarg__other(request): + return l.pop() + def test_func(something): pass + """) + req = funcargs.FuncargRequest(item) + py.test.raises(req.LookupError, req.getfuncargvalue, "notexists") + val = req.getfuncargvalue("something") + assert val == 1 + val = req.getfuncargvalue("something") + assert val == 1 + val2 = req.getfuncargvalue("other") + assert val2 == 2 + val2 = req.getfuncargvalue("other") # see about caching + assert val2 == 2 + req._fillfuncargs() + assert item.funcargs == {'something': 1} + + def test_request_addfinalizer(self, testdir): + item = testdir.getitem(""" + teardownlist = [] + def pytest_funcarg__something(request): + request.addfinalizer(lambda: teardownlist.append(1)) + def test_func(something): pass + """) + req = funcargs.FuncargRequest(item) + req.config._setupstate.prepare(item) # XXX + req._fillfuncargs() + # successively check finalization calls + teardownlist = item.getparent(pytest.Module).obj.teardownlist + ss = item.config._setupstate + assert not teardownlist + ss.teardown_exact(item) + print(ss.stack) + assert teardownlist == [1] + + def test_request_addfinalizer_partial_setup_failure(self, testdir): + p = testdir.makepyfile(""" + l = [] + def pytest_funcarg__something(request): + request.addfinalizer(lambda: l.append(None)) + def test_func(something, missingarg): + pass + def test_second(): + assert len(l) == 1 + """) + result = testdir.runpytest(p) + result.stdout.fnmatch_lines([ + "*1 passed*1 error*" + ]) + + def test_request_getmodulepath(self, testdir): + modcol = testdir.getmodulecol("def test_somefunc(): pass") + item, = testdir.genitems([modcol]) + req = funcargs.FuncargRequest(item) + assert req.fspath == modcol.fspath + +def test_applymarker(testdir): + item1,item2 = testdir.getitems(""" + class TestClass: + def test_func1(self, something): + pass + def test_func2(self, something): + pass + """) + req1 = funcargs.FuncargRequest(item1) + assert 'xfail' not in item1.keywords + req1.applymarker(py.test.mark.xfail) + assert 'xfail' in item1.keywords + assert 'skipif' not in item1.keywords + req1.applymarker(py.test.mark.skipif) + assert 'skipif' in item1.keywords + py.test.raises(ValueError, "req1.applymarker(42)") + +class TestRequestCachedSetup: + def test_request_cachedsetup(self, testdir): + item1,item2 = testdir.getitems(""" + class TestClass: + def test_func1(self, something): + pass + def test_func2(self, something): + pass + """) + req1 = funcargs.FuncargRequest(item1) + l = ["hello"] + def setup(): + return l.pop() + ret1 = req1.cached_setup(setup) + assert ret1 == "hello" + ret1b = req1.cached_setup(setup) + assert ret1 == ret1b + req2 = funcargs.FuncargRequest(item2) + ret2 = req2.cached_setup(setup) + assert ret2 == ret1 + + def test_request_cachedsetup_extrakey(self, testdir): + item1 = testdir.getitem("def test_func(): pass") + req1 = funcargs.FuncargRequest(item1) + l = ["hello", "world"] + def setup(): + return l.pop() + ret1 = req1.cached_setup(setup, extrakey=1) + ret2 = req1.cached_setup(setup, extrakey=2) + assert ret2 == "hello" + assert ret1 == "world" + ret1b = req1.cached_setup(setup, extrakey=1) + ret2b = req1.cached_setup(setup, extrakey=2) + assert ret1 == ret1b + assert ret2 == ret2b + + def test_request_cachedsetup_cache_deletion(self, testdir): + item1 = testdir.getitem("def test_func(): pass") + req1 = funcargs.FuncargRequest(item1) + l = [] + def setup(): + l.append("setup") + def teardown(val): + l.append("teardown") + ret1 = req1.cached_setup(setup, teardown, scope="function") + assert l == ['setup'] + # artificial call of finalizer + req1.config._setupstate._callfinalizers(item1) + assert l == ["setup", "teardown"] + ret2 = req1.cached_setup(setup, teardown, scope="function") + assert l == ["setup", "teardown", "setup"] + req1.config._setupstate._callfinalizers(item1) + assert l == ["setup", "teardown", "setup", "teardown"] + + def test_request_cached_setup_two_args(self, testdir): + testdir.makepyfile(""" + def pytest_funcarg__arg1(request): + return request.cached_setup(lambda: 42) + def pytest_funcarg__arg2(request): + return request.cached_setup(lambda: 17) + def test_two_different_setups(arg1, arg2): + assert arg1 != arg2 + """) + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + + def test_request_cached_setup_getfuncargvalue(self, testdir): + testdir.makepyfile(""" + def pytest_funcarg__arg1(request): + arg1 = request.getfuncargvalue("arg2") + return request.cached_setup(lambda: arg1 + 1) + def pytest_funcarg__arg2(request): + return request.cached_setup(lambda: 10) + def test_two_funcarg(arg1): + assert arg1 == 11 + """) + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + + def test_request_cached_setup_functional(self, testdir): + testdir.makepyfile(test_0=""" + l = [] + def pytest_funcarg__something(request): + val = request.cached_setup(fsetup, fteardown) + return val + def fsetup(mycache=[1]): + l.append(mycache.pop()) + return l + def fteardown(something): + l.remove(something[0]) + l.append(2) + def test_list_once(something): + assert something == [1] + def test_list_twice(something): + assert something == [1] + """) + testdir.makepyfile(test_1=""" + import test_0 # should have run already + def test_check_test0_has_teardown_correct(): + assert test_0.l == [2] + """) + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines([ + "*3 passed*" + ]) + +class TestMetafunc: + def test_no_funcargs(self, testdir): + def function(): pass + metafunc = funcargs.Metafunc(function) + assert not metafunc.funcargnames + + def test_function_basic(self): + def func(arg1, arg2="qwe"): pass + metafunc = funcargs.Metafunc(func) + assert len(metafunc.funcargnames) == 1 + assert 'arg1' in metafunc.funcargnames + assert metafunc.function is func + assert metafunc.cls is None + + def test_addcall_no_args(self): + def func(arg1): pass + metafunc = funcargs.Metafunc(func) + metafunc.addcall() + assert len(metafunc._calls) == 1 + call = metafunc._calls[0] + assert call.id == "0" + assert not hasattr(call, 'param') + + def test_addcall_id(self): + def func(arg1): pass + metafunc = funcargs.Metafunc(func) + py.test.raises(ValueError, "metafunc.addcall(id=None)") + + metafunc.addcall(id=1) + py.test.raises(ValueError, "metafunc.addcall(id=1)") + py.test.raises(ValueError, "metafunc.addcall(id='1')") + metafunc.addcall(id=2) + assert len(metafunc._calls) == 2 + assert metafunc._calls[0].id == "1" + assert metafunc._calls[1].id == "2" + + def test_addcall_param(self): + def func(arg1): pass + metafunc = funcargs.Metafunc(func) + class obj: pass + metafunc.addcall(param=obj) + metafunc.addcall(param=obj) + metafunc.addcall(param=1) + assert len(metafunc._calls) == 3 + assert metafunc._calls[0].param == obj + assert metafunc._calls[1].param == obj + assert metafunc._calls[2].param == 1 + + def test_addcall_funcargs(self): + def func(arg1): pass + metafunc = funcargs.Metafunc(func) + class obj: pass + metafunc.addcall(funcargs={"x": 2}) + metafunc.addcall(funcargs={"x": 3}) + assert len(metafunc._calls) == 2 + assert metafunc._calls[0].funcargs == {'x': 2} + assert metafunc._calls[1].funcargs == {'x': 3} + assert not hasattr(metafunc._calls[1], 'param') + +class TestGenfuncFunctional: + def test_attributes(self, testdir): + p = testdir.makepyfile(""" + # assumes that generate/provide runs in the same process + import py + def pytest_generate_tests(metafunc): + metafunc.addcall(param=metafunc) + + def pytest_funcarg__metafunc(request): + assert request._pyfuncitem._genid == "0" + return request.param + + def test_function(metafunc, pytestconfig): + assert metafunc.config == pytestconfig + assert metafunc.module.__name__ == __name__ + assert metafunc.function == test_function + assert metafunc.cls is None + + class TestClass: + def test_method(self, metafunc, pytestconfig): + assert metafunc.config == pytestconfig + assert metafunc.module.__name__ == __name__ + if py.std.sys.version_info > (3, 0): + unbound = TestClass.test_method + else: + unbound = TestClass.test_method.im_func + # XXX actually have an unbound test function here? + assert metafunc.function == unbound + assert metafunc.cls == TestClass + """) + result = testdir.runpytest(p, "-v") + result.stdout.fnmatch_lines([ + "*2 passed in*", + ]) + + def test_addcall_with_two_funcargs_generators(self, testdir): + testdir.makeconftest(""" + def pytest_generate_tests(metafunc): + assert "arg1" in metafunc.funcargnames + metafunc.addcall(funcargs=dict(arg1=1, arg2=2)) + """) + p = testdir.makepyfile(""" + def pytest_generate_tests(metafunc): + metafunc.addcall(funcargs=dict(arg1=1, arg2=1)) + + class TestClass: + def test_myfunc(self, arg1, arg2): + assert arg1 == arg2 + """) + result = testdir.runpytest("-v", p) + result.stdout.fnmatch_lines([ + "*test_myfunc*0*PASS*", + "*test_myfunc*1*FAIL*", + "*1 failed, 1 passed*" + ]) + + def test_two_functions(self, testdir): + p = testdir.makepyfile(""" + def pytest_generate_tests(metafunc): + metafunc.addcall(param=10) + metafunc.addcall(param=20) + + def pytest_funcarg__arg1(request): + return request.param + + def test_func1(arg1): + assert arg1 == 10 + def test_func2(arg1): + assert arg1 in (10, 20) + """) + result = testdir.runpytest("-v", p) + result.stdout.fnmatch_lines([ + "*test_func1*0*PASS*", + "*test_func1*1*FAIL*", + "*test_func2*PASS*", + "*1 failed, 3 passed*" + ]) + + def test_generate_plugin_and_module(self, testdir): + testdir.makeconftest(""" + def pytest_generate_tests(metafunc): + assert "arg1" in metafunc.funcargnames + metafunc.addcall(id="world", param=(2,100)) + """) + p = testdir.makepyfile(""" + def pytest_generate_tests(metafunc): + metafunc.addcall(param=(1,1), id="hello") + + def pytest_funcarg__arg1(request): + return request.param[0] + def pytest_funcarg__arg2(request): + return request.param[1] + + class TestClass: + def test_myfunc(self, arg1, arg2): + assert arg1 == arg2 + """) + result = testdir.runpytest("-v", p) + result.stdout.fnmatch_lines([ + "*test_myfunc*hello*PASS*", + "*test_myfunc*world*FAIL*", + "*1 failed, 1 passed*" + ]) + + def test_generate_tests_in_class(self, testdir): + p = testdir.makepyfile(""" + class TestClass: + def pytest_generate_tests(self, metafunc): + metafunc.addcall(funcargs={'hello': 'world'}, id="hello") + + def test_myfunc(self, hello): + assert hello == "world" + """) + result = testdir.runpytest("-v", p) + result.stdout.fnmatch_lines([ + "*test_myfunc*hello*PASS*", + "*1 passed*" + ]) + + def test_two_functions_not_same_instance(self, testdir): + p = testdir.makepyfile(""" + def pytest_generate_tests(metafunc): + metafunc.addcall({'arg1': 10}) + metafunc.addcall({'arg1': 20}) + + class TestClass: + def test_func(self, arg1): + assert not hasattr(self, 'x') + self.x = 1 + """) + result = testdir.runpytest("-v", p) + result.stdout.fnmatch_lines([ + "*test_func*0*PASS*", + "*test_func*1*PASS*", + "*2 pass*", + ]) + + +def test_conftest_funcargs_only_available_in_subdir(testdir): + sub1 = testdir.mkpydir("sub1") + sub2 = testdir.mkpydir("sub2") + sub1.join("conftest.py").write(py.code.Source(""" + import py + def pytest_funcarg__arg1(request): + py.test.raises(Exception, "request.getfuncargvalue('arg2')") + """)) + sub2.join("conftest.py").write(py.code.Source(""" + import py + def pytest_funcarg__arg2(request): + py.test.raises(Exception, "request.getfuncargvalue('arg1')") + """)) + + sub1.join("test_in_sub1.py").write("def test_1(arg1): pass") + sub2.join("test_in_sub2.py").write("def test_2(arg2): pass") + result = testdir.runpytest("-v") + result.stdout.fnmatch_lines([ + "*2 passed*" + ]) + +def test_funcarg_non_pycollectobj(testdir): # rough jstests usage + testdir.makeconftest(""" + import pytest + def pytest_pycollect_makeitem(collector, name, obj): + if name == "MyClass": + return MyCollector(name, parent=collector) + class MyCollector(pytest.Collector): + def reportinfo(self): + return self.fspath, 3, "xyz" + """) + modcol = testdir.getmodulecol(""" + def pytest_funcarg__arg1(request): + return 42 + class MyClass: + pass + """) + clscol = modcol.collect()[0] + clscol.obj = lambda arg1: None + clscol.funcargs = {} + funcargs.fillfuncargs(clscol) + assert clscol.funcargs['arg1'] == 42 + + +def test_funcarg_lookup_error(testdir): + p = testdir.makepyfile(""" + def test_lookup_error(unknown): + pass + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*ERROR at setup of test_lookup_error*", + "*def test_lookup_error(unknown):*", + "*LookupError: no factory found*unknown*", + "*available funcargs*", + "*1 error*", + ]) + assert "INTERNAL" not in result.stdout.str() + +class TestReportInfo: + def test_itemreport_reportinfo(self, testdir, linecomp): + testdir.makeconftest(""" + import pytest + class MyFunction(pytest.Function): + def reportinfo(self): + return "ABCDE", 42, "custom" + def pytest_pycollect_makeitem(collector, name, obj): + if name == "test_func": + return MyFunction(name, parent=collector) + """) + item = testdir.getitem("def test_func(): pass") + runner = item.config.pluginmanager.getplugin("runner") + assert item.location == ("ABCDE", 42, "custom") + + def test_func_reportinfo(self, testdir): + item = testdir.getitem("def test_func(): pass") + fspath, lineno, modpath = item.reportinfo() + assert fspath == item.fspath + assert lineno == 0 + assert modpath == "test_func" + + def test_class_reportinfo(self, testdir): + modcol = testdir.getmodulecol(""" + # lineno 0 + class TestClass: + def test_hello(self): pass + """) + classcol = testdir.collect_by_name(modcol, "TestClass") + fspath, lineno, msg = classcol.reportinfo() + assert fspath == modcol.fspath + assert lineno == 1 + assert msg == "TestClass" + + def test_generator_reportinfo(self, testdir): + modcol = testdir.getmodulecol(""" + # lineno 0 + def test_gen(): + def check(x): + assert x + yield check, 3 + """) + gencol = testdir.collect_by_name(modcol, "test_gen") + fspath, lineno, modpath = gencol.reportinfo() + assert fspath == modcol.fspath + assert lineno == 1 + assert modpath == "test_gen" + + genitem = gencol.collect()[0] + fspath, lineno, modpath = genitem.reportinfo() + assert fspath == modcol.fspath + assert lineno == 2 + assert modpath == "test_gen[0]" + """ + def test_func(): + pass + def test_genfunc(): + def check(x): + pass + yield check, 3 + class TestClass: + def test_method(self): + pass + """ + +def test_show_funcarg(testdir): + result = testdir.runpytest("--funcargs") + result.stdout.fnmatch_lines([ + "*tmpdir*", + "*temporary directory*", + ] + ) + +class TestRaises: + def test_raises(self): + source = "int('qwe')" + excinfo = py.test.raises(ValueError, source) + code = excinfo.traceback[-1].frame.code + s = str(code.fullsource) + assert s == source + + def test_raises_exec(self): + py.test.raises(ValueError, "a,x = []") + + def test_raises_syntax_error(self): + py.test.raises(SyntaxError, "qwe qwe qwe") + + def test_raises_function(self): + py.test.raises(ValueError, int, 'hello') + + def test_raises_callable_no_exception(self): + class A: + def __call__(self): + pass + try: + py.test.raises(ValueError, A()) + except py.test.raises.Exception: + pass + + @py.test.mark.skipif('sys.version < "2.5"') + def test_raises_as_contextmanager(self, testdir): + testdir.makepyfile(""" + from __future__ import with_statement + import py + + def test_simple(): + with py.test.raises(ZeroDivisionError) as excinfo: + assert isinstance(excinfo, py.code.ExceptionInfo) + 1/0 + print (excinfo) + assert excinfo.type == ZeroDivisionError + + def test_noraise(): + with py.test.raises(py.test.raises.Exception): + with py.test.raises(ValueError): + int() + + def test_raise_wrong_exception_passes_by(): + with py.test.raises(ZeroDivisionError): + with py.test.raises(ValueError): + 1/0 + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + '*3 passed*', + ]) + + + --- a/testing/plugin/test_recwarn.py +++ /dev/null @@ -1,81 +0,0 @@ -import py -from _pytest.recwarn import WarningsRecorder - -def test_WarningRecorder(recwarn): - showwarning = py.std.warnings.showwarning - rec = WarningsRecorder() - assert py.std.warnings.showwarning != showwarning - assert not rec.list - py.std.warnings.warn_explicit("hello", UserWarning, "xyz", 13) - assert len(rec.list) == 1 - py.std.warnings.warn(DeprecationWarning("hello")) - assert len(rec.list) == 2 - warn = rec.pop() - assert str(warn.message) == "hello" - l = rec.list - rec.clear() - assert len(rec.list) == 0 - assert l is rec.list - py.test.raises(AssertionError, "rec.pop()") - rec.finalize() - assert showwarning == py.std.warnings.showwarning - -def test_recwarn_functional(testdir): - reprec = testdir.inline_runsource(""" - pytest_plugins = 'pytest_recwarn', - import warnings - oldwarn = warnings.showwarning - def test_method(recwarn): - assert warnings.showwarning != oldwarn - warnings.warn("hello") - warn = recwarn.pop() - assert isinstance(warn.message, UserWarning) - def test_finalized(): - assert warnings.showwarning == oldwarn - """) - res = reprec.countoutcomes() - assert tuple(res) == (2, 0, 0), res - -# -# ============ test py.test.deprecated_call() ============== -# - -def dep(i): - if i == 0: - py.std.warnings.warn("is deprecated", DeprecationWarning) - return 42 - -reg = {} -def dep_explicit(i): - if i == 0: - py.std.warnings.warn_explicit("dep_explicit", category=DeprecationWarning, - filename="hello", lineno=3) - -def test_deprecated_call_raises(): - excinfo = py.test.raises(AssertionError, - "py.test.deprecated_call(dep, 3)") - assert str(excinfo).find("did not produce") != -1 - -def test_deprecated_call(): - py.test.deprecated_call(dep, 0) - -def test_deprecated_call_ret(): - ret = py.test.deprecated_call(dep, 0) - assert ret == 42 - -def test_deprecated_call_preserves(): - r = py.std.warnings.onceregistry.copy() - f = py.std.warnings.filters[:] - test_deprecated_call_raises() - test_deprecated_call() - assert r == py.std.warnings.onceregistry - assert f == py.std.warnings.filters - -def test_deprecated_explicit_call_raises(): - py.test.raises(AssertionError, - "py.test.deprecated_call(dep_explicit, 3)") - -def test_deprecated_explicit_call(): - py.test.deprecated_call(dep_explicit, 0) - py.test.deprecated_call(dep_explicit, 0) - --- /dev/null +++ b/testing/test_unittest.py @@ -0,0 +1,105 @@ +import py + +def test_simple_unittest(testdir): + testpath = testdir.makepyfile(""" + import unittest + pytest_plugins = "pytest_unittest" + class MyTestCase(unittest.TestCase): + def testpassing(self): + self.assertEquals('foo', 'foo') + def test_failing(self): + self.assertEquals('foo', 'bar') + """) + reprec = testdir.inline_run(testpath) + assert reprec.matchreport("testpassing").passed + assert reprec.matchreport("test_failing").failed + +def test_isclasscheck_issue53(testdir): + testpath = testdir.makepyfile(""" + import unittest + class _E(object): + def __getattr__(self, tag): + pass + E = _E() + """) + result = testdir.runpytest(testpath) + assert result.ret == 0 + +def test_setup(testdir): + testpath = testdir.makepyfile(test_two=""" + import unittest + class MyTestCase(unittest.TestCase): + def setUp(self): + self.foo = 1 + def test_setUp(self): + self.assertEquals(1, self.foo) + """) + reprec = testdir.inline_run(testpath) + rep = reprec.matchreport("test_setUp") + assert rep.passed + +def test_new_instances(testdir): + testpath = testdir.makepyfile(""" + import unittest + class MyTestCase(unittest.TestCase): + def test_func1(self): + self.x = 2 + def test_func2(self): + assert not hasattr(self, 'x') + """) + reprec = testdir.inline_run(testpath) + reprec.assertoutcome(passed=2) + +def test_teardown(testdir): + testpath = testdir.makepyfile(""" + import unittest + pytest_plugins = "pytest_unittest" # XXX + class MyTestCase(unittest.TestCase): + l = [] + def test_one(self): + pass + def tearDown(self): + self.l.append(None) + class Second(unittest.TestCase): + def test_check(self): + self.assertEquals(MyTestCase.l, [None]) + """) + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() + assert failed == 0, failed + assert passed == 2 + assert passed + skipped + failed == 2 + +def test_module_level_pytestmark(testdir): + testpath = testdir.makepyfile(""" + import unittest + import py + pytestmark = py.test.mark.xfail + class MyTestCase(unittest.TestCase): + def test_func1(self): + assert 0 + """) + reprec = testdir.inline_run(testpath, "-s") + reprec.assertoutcome(skipped=1) + +def test_class_setup(testdir): + testpath = testdir.makepyfile(""" + import unittest + import py + class MyTestCase(unittest.TestCase): + x = 0 + @classmethod + def setUpClass(cls): + cls.x += 1 + def test_func1(self): + assert self.x == 1 + def test_func2(self): + assert self.x == 1 + @classmethod + def tearDownClass(cls): + cls.x -= 1 + def test_teareddown(): + assert MyTestCase.x == 0 + """) + reprec = testdir.inline_run(testpath) + reprec.assertoutcome(passed=3) From commits-noreply at bitbucket.org Sat Nov 13 16:38:33 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 09:38:33 -0600 (CST) Subject: [py-svn] pylib commit f5b2ad2c9cb0: slight adjustments Message-ID: <20101113153833.53FF1241099@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289662689 -3600 # Node ID f5b2ad2c9cb0d7e048ff93ddaec8286786148005 # Parent 1371dfb06957338aef2066da0af04888f7e2af55 slight adjustments --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,7 @@ [tox] envlist=py26,py27,py31,py27-xdist,py25,py24 -indexserver= default=http://pypi.testrun.org +indexserver= + default=http://pypi.testrun.org [testenv] changedir=testing @@ -18,5 +19,4 @@ commands= [testenv:jython] changedir=testing commands= - {envpython} {envbindir}/py.test-jython \ - -rfsxX --junitxml={envlogdir}/junit-{envname}0.xml [io_ code] + {envpython} -m pytest -rfsxX --junitxml={envlogdir}/junit-{envname}0.xml [io_ code] --- a/py/test.py +++ b/py/test.py @@ -1,7 +1,7 @@ import sys if __name__ == '__main__': - import py - sys.exit(py.test.cmdline.main()) + import pytest + sys.exit(pytest.main()) else: import sys, pytest sys.modules['py.test'] = pytest From commits-noreply at bitbucket.org Sat Nov 13 19:46:38 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 12:46:38 -0600 (CST) Subject: [py-svn] pytest commit 01f97e561a1a: some fixes to make cross linux/windows remote testing work again Message-ID: <20101113184638.4F7266C1064@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289673988 -3600 # Node ID 01f97e561a1afcb664558a7fb6e5bb92fd6e9bcb # Parent 54e78a7ce4980b1cab0ed40ca0e16dbea31394c2 some fixes to make cross linux/windows remote testing work again --- a/pytest.py +++ b/pytest.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev26' +__version__ = '2.0.0.dev27' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev26', + version='2.0.0.dev27', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -400,10 +400,8 @@ class TmpTestdir: return colitem def popen(self, cmdargs, stdout, stderr, **kw): - if not hasattr(py.std, 'subprocess'): - py.test.skip("no subprocess module") env = os.environ.copy() - env['PYTHONPATH'] = ":".join(filter(None, [ + env['PYTHONPATH'] = os.pathsep.join(filter(None, [ str(os.getcwd()), env.get('PYTHONPATH', '')])) kw['env'] = env #print "env", env @@ -449,12 +447,13 @@ class TmpTestdir: def _getpybinargs(self, scriptname): if not self.request.config.getvalue("notoolsonpath"): - script = py.path.local.sysfind(scriptname) + import pytest + script = pytest.__file__.strip("co") assert script, "script %r not found" % scriptname # XXX we rely on script refering to the correct environment # we cannot use "(py.std.sys.executable,script)" # becaue on windows the script is e.g. a py.test.exe - return (script,) + return (py.std.sys.executable, script,) else: py.test.skip("cannot run %r with --no-tools-on-path" % scriptname) --- a/_pytest/core.py +++ b/_pytest/core.py @@ -157,14 +157,17 @@ class PluginManager(object): def consider_setuptools_entrypoints(self): try: - from pkg_resources import iter_entry_points + from pkg_resources import iter_entry_points, DistributionNotFound except ImportError: return # XXX issue a warning for ep in iter_entry_points('pytest11'): name = canonical_importname(ep.name) if name in self._name2plugin: continue - plugin = ep.load() + try: + plugin = ep.load() + except DistributionNotFound: + continue self.register(plugin, name=name) def consider_preparse(self, args): --- a/tox.ini +++ b/tox.ini @@ -36,6 +36,12 @@ commands= [testenv:py31] deps=pylib +[testenv:py31-xdist] +deps=pytest-xdist +commands= + py.test -n3 -rfsxX \ + --junitxml={envlogdir}/junit-{envname}.xml [] + [testenv:py32] deps=pylib @@ -52,5 +58,5 @@ commands= minversion=2.0 plugins=pytester addopts=-rfx --pyargs -rsyncdirs=pytest.py _pytest testing +rsyncdirs=tox.ini pytest.py _pytest testing From commits-noreply at bitbucket.org Sat Nov 13 19:46:39 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 12:46:39 -0600 (CST) Subject: [py-svn] pylib commit bd406471f394: fix path.dirname on windows, some detail fixes to make cross linux/windows testing work Message-ID: <20101113184639.4D32A6C132B@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1289673971 -3600 # Node ID bd406471f39498a5f9e99c53315378e83702becd # Parent f5b2ad2c9cb0d7e048ff93ddaec8286786148005 fix path.dirname on windows, some detail fixes to make cross linux/windows testing work --- a/py/_path/local.py +++ b/py/_path/local.py @@ -229,24 +229,17 @@ class LocalPath(FSBase): ext = '.' + ext kw['basename'] = pb + ext - kw.setdefault('drive', drive) - kw.setdefault('dirname', dirname) + if ('dirname' in kw and not kw['dirname']): + kw['dirname'] = drive + else: + kw.setdefault('dirname', dirname) kw.setdefault('sep', self.sep) obj.strpath = os.path.normpath( - "%(drive)s%(dirname)s%(sep)s%(basename)s" % kw) + "%(dirname)s%(sep)s%(basename)s" % kw) return obj def _getbyspec(self, spec): - """ return a sequence of specified path parts. 'spec' is - a comma separated string containing path part names. - according to the following convention: - a:/some/path/to/a/file.ext - || drive - |-------------| dirname - |------| basename - |--| purebasename - |--| ext - """ + """ see new for what 'spec' can be. """ res = [] parts = self.strpath.split(self.sep) @@ -256,7 +249,7 @@ class LocalPath(FSBase): if name == 'drive': append(parts[0]) elif name == 'dirname': - append(self.sep.join(['']+parts[1:-1])) + append(self.sep.join(parts[:-1])) else: basename = parts[-1] if name == 'basename': --- a/conftest.py +++ b/conftest.py @@ -5,7 +5,6 @@ pytest_plugins = 'doctest pytester'.spli collect_ignore = ['build', 'doc/_build'] -rsyncdirs = ['conftest.py', 'py', 'doc', 'testing'] import os, py pid = os.getpid() --- a/tox.ini +++ b/tox.ini @@ -20,3 +20,6 @@ commands= changedir=testing commands= {envpython} -m pytest -rfsxX --junitxml={envlogdir}/junit-{envname}0.xml [io_ code] + +[pytest] +rsyncdirs = conftest.py py doc testing --- a/CHANGELOG +++ b/CHANGELOG @@ -10,7 +10,8 @@ Changes between 1.3.4 and 2.0.0dev0 - use apipkg-1.1 and make py.apipkg.initpkg|ApiModule available - add py.iniconfig module for brain-dead easy ini-config file parsing - introduce py.builtin.any() -- path objects have a .dirname attribute now +- path objects have a .dirname attribute now (equivalent to + os.path.dirname(path)) - path.visit() accepts breadthfirst (bf) and sort options Changes between 1.3.3 and 1.3.4 From commits-noreply at bitbucket.org Sat Nov 13 19:47:09 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 12:47:09 -0600 (CST) Subject: [py-svn] pytest-xdist commit f724a1ad7e58: some changes to make remote testing work better - slightly hackish and will remain so until 'tox' is used to create environments and properly set things up. Message-ID: <20101113184709.5F7696C1064@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1289674021 -3600 # Node ID f724a1ad7e5828e2b7512d5a7413abfe867d2b9c # Parent 2bda8b5eecd3cc2b9f4eb991826a88f3821ab17b some changes to make remote testing work better - slightly hackish and will remain so until 'tox' is used to create environments and properly set things up. --- a/ISSUES.txt +++ b/ISSUES.txt @@ -1,3 +1,10 @@ +next release critical +----------------------------------------------- +tag: bug + +miserably fails: --dist=each --tx popen --tx socket=... + + rename / hooks ----------------------------------------------- tag: bug --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -247,10 +247,14 @@ class DSession: def slave_errordown(self, node, error): self.config.hook.pytest_testnodedown(node=node, error=error) - crashitem = self.sched.remove_node(node) - if crashitem: - self.handle_crashitem(crashitem, node) - #self.report_line("item crashed on node: %s" % crashitem) + try: + crashitem = self.sched.remove_node(node) + except KeyError: + pass + else: + if crashitem: + self.handle_crashitem(crashitem, node) + #self.report_line("item crashed on node: %s" % crashitem) if not self.sched.hasnodes(): self.session_finished = True --- a/testing/test_plugin.py +++ b/testing/test_plugin.py @@ -41,9 +41,10 @@ class TestDistOptions: def test_getrsyncdirs(self, testdir): config = testdir.parseconfigure('--rsyncdir=' + str(testdir.tmpdir)) nm = NodeManager(config, specs=[execnet.XSpec("popen")]) - roots = nm._getrsyncdirs() - assert len(roots) == 1 + 1 # pylib - assert testdir.tmpdir in roots + assert not nm._getrsyncdirs() + nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")]) + assert nm.roots + assert testdir.tmpdir in nm.roots def test_getrsyncdirs_with_conftest(self, testdir): p = py.path.local() @@ -55,7 +56,7 @@ class TestDistOptions: """) config = testdir.parseconfigure( testdir.tmpdir, '--rsyncdir=y', '--rsyncdir=z') - nm = NodeManager(config, specs=[execnet.XSpec("popen")]) + nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=xyz")]) roots = nm._getrsyncdirs() #assert len(roots) == 3 + 1 # pylib assert py.path.local('y') in roots --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -88,6 +88,8 @@ class TestReportSerialization: assert newrep.passed == rep.passed assert newrep.failed == rep.failed assert newrep.skipped == rep.skipped + if newrep.skipped: + assert len(newrep.reprcrash) == 3 assert newrep.outcome == rep.outcome assert newrep.when == rep.when assert newrep.keywords == rep.keywords --- a/xdist/slavemanage.py +++ b/xdist/slavemanage.py @@ -10,8 +10,9 @@ class NodeManager(object): self.config = config if specs is None: specs = self._getxspecs() + self.gwmanager = GatewayManager(specs, config.hook) + self.specs = self.gwmanager.specs self.roots = self._getrsyncdirs() - self.gwmanager = GatewayManager(specs, config.hook) self._nodesready = py.std.threading.Event() def trace(self, msg): @@ -34,12 +35,6 @@ class NodeManager(object): # send each rsync root for root in self.roots: self.gwmanager.rsync(root, **options) - else: - XXX # do we want to care for situations without explicit rsyncdirs? - # we transfer our topdir as the root - self.gwmanager.rsync(self.config.topdir, **options) - # and cd into it - self.gwmanager.multi_chdir(self.config.topdir.basename, inplacelocal=False) def makegateways(self): # we change to the topdir sot that @@ -83,8 +78,16 @@ class NodeManager(object): return [execnet.XSpec(x) for x in xspeclist] def _getrsyncdirs(self): + for spec in self.specs: + if not spec.popen or spec.chdir: + break + else: + return [] + import pytest, _pytest + pytestpath = pytest.__file__.rstrip("co") + pytestdir = py.path.local(_pytest.__file__).dirpath() config = self.config - candidates = [py._pydir] + candidates = [py._pydir,pytestpath,pytestdir] candidates += config.option.rsyncdir conftestroots = config.getini("rsyncdirs") if conftestroots: --- a/xdist/remote.py +++ b/xdist/remote.py @@ -121,6 +121,11 @@ def remote_initconfig(option_dict, args) if __name__ == '__channelexec__': slaveinput,args,option_dict = channel.receive() + importpath = os.getcwd() + sys.path.insert(0, importpath) # XXX only for remote situations + os.environ['PYTHONPATH'] = (importpath + os.pathsep + + os.environ.get('PYTHONPATH', '')) + #os.environ['PYTHONPATH'] = importpath import py config = remote_initconfig(option_dict, args) config.slaveinput = slaveinput From commits-noreply at bitbucket.org Sat Nov 13 21:03:59 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 14:03:59 -0600 (CST) Subject: [py-svn] pytest-xdist commit 9c6fb2a805e1: fix and unify remote skip test/collect reports Message-ID: <20101113200359.E77BF6C1029@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1289678015 -3600 # Node ID 9c6fb2a805e1a602b1b3e863e7b79c07beefa74c # Parent f724a1ad7e5828e2b7512d5a7413abfe867d2b9c fix and unify remote skip test/collect reports --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -144,7 +144,7 @@ class TestDistribution: subdir.ensure("__init__.py") p = subdir.join("test_one.py") p.write("def test_5():\n assert not __file__.startswith(%r)" % str(p)) - result = testdir.runpytest("-v", "-d", + result = testdir.runpytest("-v", "-d", "--rsyncdir=%(subdir)s" % locals(), "--tx=popen//chdir=%(dest)s" % locals(), p) assert result.ret == 0 @@ -394,3 +394,18 @@ def test_crashing_item(testdir): +def test_skipping(testdir): + p = testdir.makepyfile(""" + import pytest + def test_crash(): + pytest.skip("hello") + """) + result = testdir.runpytest("-n1", '-rs', p) + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*hello*", + "*1 skipped*" + ]) + + + --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -274,12 +274,12 @@ class DSession: if rep.when in ("setup", "call"): self.sched.remove_item(node, rep.nodeid) #self.report_line("testreport %s: %s" %(rep.id, rep.status)) - enrich_report_with_platform_data(rep, node) + rep.node = node self.config.hook.pytest_runtest_logreport(report=rep) self._handlefailures(rep) def slave_teardownreport(self, node, rep): - enrich_report_with_platform_data(rep, node) + rep.node = node self.config.hook.pytest__teardown_final_logerror(report=rep) def slave_collectreport(self, node, rep): @@ -307,7 +307,7 @@ class DSession: msg = "Slave %r crashed while running %r" %(slave.gateway.id, nodeid) rep = runner.TestReport(nodeid, (fspath, None, fspath), (), "failed", msg, "???") - enrich_report_with_platform_data(rep, slave) + rep.node = slave self.config.hook.pytest_runtest_logreport(report=rep) class TerminalDistReporter: @@ -346,14 +346,3 @@ class TerminalDistReporter: # targets = ", ".join(["[%s]" % gw.id for gw in gateways]) # self.write_line("rsyncfinish: %s -> %s" %(source, targets)) - -def enrich_report_with_platform_data(rep, node): - rep.node = node - if hasattr(rep, 'node') and rep.longrepr: - d = node.slaveinfo - ver = "%s.%s.%s" % d['version_info'][:3] - infoline = "[%s] %s -- Python %s %s" % ( - d['id'], d['sysplatform'], ver, d['executable']) - # XXX more structured longrepr? - rep.longrepr = infoline + "\n\n" + str(rep.longrepr) - --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5a7' +__version__ = '1.5a8' --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -88,8 +88,8 @@ class TestReportSerialization: assert newrep.passed == rep.passed assert newrep.failed == rep.failed assert newrep.skipped == rep.skipped - if newrep.skipped: - assert len(newrep.reprcrash) == 3 + if newrep.skipped and 'xfail' not in newrep.keywords: + assert len(newrep.longrepr) == 3 assert newrep.outcome == rep.outcome assert newrep.when == rep.when assert newrep.keywords == rep.keywords --- a/xdist/remote.py +++ b/xdist/remote.py @@ -85,7 +85,10 @@ class SlaveInteractor: def serialize_report(rep): import py d = rep.__dict__.copy() - d['longrepr'] = rep.longrepr and str(rep.longrepr) or None + if hasattr(rep.longrepr, 'toterminal'): + d['longrepr'] = str(rep.longrepr) + else: + d['longrepr'] = rep.longrepr for name in d: if isinstance(d[name], py.path.local): d[name] = str(d[name]) --- a/setup.py +++ b/setup.py @@ -10,7 +10,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.5a7', + version='1.5a8', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=__doc__, license='GPLv2 or later', From commits-noreply at bitbucket.org Sat Nov 13 21:04:24 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 14:04:24 -0600 (CST) Subject: [py-svn] pytest commit 057049589d52: fix skip reporting over distributed testing. if we have a "skip" report Message-ID: <20101113200424.1A06D1E1320@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289678608 -3600 # Node ID 057049589d52958d8561f45d88264ab3622ff70f # Parent 01f97e561a1afcb664558a7fb6e5bb92fd6e9bcb fix skip reporting over distributed testing. if we have a "skip" report rep.longrepr will now be a 3-tuple (path, lineno, message) --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -366,11 +366,10 @@ def test_skipif_class(testdir): def test_skip_reasons_folding(): - class longrepr: - class reprcrash: - path = 'xyz' - lineno = 3 - message = "justso" + path = 'xyz' + lineno = 3 + message = "justso" + longrepr = (path, lineno, message) class X: pass @@ -387,9 +386,9 @@ def test_skip_reasons_folding(): assert len(l) == 1 num, fspath, lineno, reason = l[0] assert num == 2 - assert fspath == longrepr.reprcrash.path - assert lineno == longrepr.reprcrash.lineno - assert reason == longrepr.reprcrash.message + assert fspath == path + assert lineno == lineno + assert reason == message def test_skipped_reasons_functional(testdir): testdir.makepyfile( --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -111,9 +111,21 @@ class CallInfo: status = "result: %r" % (self.result,) return "" % (self.when, status) +def getslaveinfoline(node): + try: + return node._slaveinfocache + except AttributeError: + d = node.slaveinfo + ver = "%s.%s.%s" % d['version_info'][:3] + node._slaveinfocache = s = "[%s] %s -- Python %s %s" % ( + d['id'], d['sysplatform'], ver, d['executable']) + return s + class BaseReport(object): def toterminal(self, out): longrepr = self.longrepr + if hasattr(self, 'node'): + out.line(getslaveinfoline(self.node)) if hasattr(longrepr, 'toterminal'): longrepr.toterminal(out) else: @@ -140,7 +152,8 @@ def pytest_runtest_makereport(item, call longrepr = excinfo elif excinfo.errisinstance(py.test.skip.Exception): outcome = "skipped" - longrepr = item._repr_failure_py(excinfo) + r = item._repr_failure_py(excinfo, "line").reprcrash + longrepr = (str(r.path), r.lineno, r.message) else: outcome = "failed" if call.when == "call": @@ -189,14 +202,14 @@ class TeardownErrorReport(BaseReport): def pytest_make_collect_report(collector): call = CallInfo(collector._memocollect, "memocollect") - reason = longrepr = None + longrepr = None if not call.excinfo: outcome = "passed" else: if call.excinfo.errisinstance(py.test.skip.Exception): outcome = "skipped" - reason = str(call.excinfo.value) - longrepr = collector._repr_failure_py(call.excinfo, "line") + r = collector._repr_failure_py(call.excinfo, "line").reprcrash + longrepr = (str(r.path), r.lineno, r.message) else: outcome = "failed" errorinfo = collector.repr_failure(call.excinfo) @@ -204,15 +217,14 @@ def pytest_make_collect_report(collector errorinfo = CollectErrorRepr(errorinfo) longrepr = errorinfo return CollectReport(collector.nodeid, outcome, longrepr, - getattr(call, 'result', None), reason) + getattr(call, 'result', None)) class CollectReport(BaseReport): - def __init__(self, nodeid, outcome, longrepr, result, reason): + def __init__(self, nodeid, outcome, longrepr, result): self.nodeid = nodeid self.outcome = outcome self.longrepr = longrepr self.result = result or [] - self.reason = reason @property def location(self): @@ -355,6 +367,7 @@ def importorskip(modname, minversion=Non optionally specified 'minversion' - otherwise call py.test.skip() with a message detailing the mismatch. """ + __tracebackhide__ = True compile(modname, '', 'eval') # to catch syntaxerrors try: mod = __import__(modname, None, None, ['__doc__']) --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -389,7 +389,7 @@ class CollectonlyReporter: msg = report.longrepr.reprcrash.message else: # XXX unify (we have CollectErrorRepr here) - msg = str(report.longrepr.longrepr) + msg = str(report.longrepr[2]) self.outindent("!!! %s !!!" % msg) #self.outindent("!!! error !!!") self._failed.append(report) --- a/_pytest/skipping.py +++ b/_pytest/skipping.py @@ -186,8 +186,8 @@ def cached_eval(config, expr, d): def folded_skips(skipped): d = {} for event in skipped: - entry = event.longrepr.reprcrash - key = entry.path, entry.lineno, entry.message + key = event.longrepr + assert len(key) == 3, (event, key) d.setdefault(key, []).append(event) l = [] for key, events in d.items(): --- a/pytest.py +++ b/pytest.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev27' +__version__ = '2.0.0.dev28' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev27', + version='2.0.0.dev28', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/_pytest/resultlog.py +++ b/_pytest/resultlog.py @@ -74,17 +74,18 @@ class ResultLog(object): elif report.failed: longrepr = str(report.longrepr) elif report.skipped: - longrepr = str(report.longrepr.reprcrash.message) + longrepr = str(report.longrepr[2]) self.log_outcome(report, code, longrepr) def pytest_collectreport(self, report): if not report.passed: if report.failed: code = "F" + longrepr = str(report.longrepr.reprcrash) else: assert report.skipped code = "S" - longrepr = str(report.longrepr.reprcrash) + longrepr = "%s:%d: %s" % report.longrepr self.log_outcome(report, code, longrepr) def pytest_internalerror(self, excrepr): --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -344,13 +344,18 @@ def test_exception_printing_skip(): def test_importorskip(): importorskip = py.test.importorskip + def f(): + importorskip("asdlkj") try: sys = importorskip("sys") assert sys == py.std.sys #path = py.test.importorskip("os.path") #assert path == py.std.os.path - py.test.raises(py.test.skip.Exception, - "py.test.importorskip('alskdj')") + excinfo = py.test.raises(py.test.skip.Exception, f) + path = py.path.local(excinfo.getrepr().reprcrash.path) + # check that importorskip reports the actual call + # in this test the test_runner.py file + assert path.purebasename == "test_runner" py.test.raises(SyntaxError, "py.test.importorskip('x y z')") py.test.raises(SyntaxError, "py.test.importorskip('x=y')") path = importorskip("py", minversion=".".join(py.__version__)) From commits-noreply at bitbucket.org Sat Nov 13 22:20:15 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 15:20:15 -0600 (CST) Subject: [py-svn] pytest-xdist commit b2582a5bd279: fox tox.ini file Message-ID: <20101113212015.928582411A8@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1289683205 -3600 # Node ID b2582a5bd27955f9a993a98d21f555be95e6cbb3 # Parent 9c6fb2a805e1a602b1b3e863e7b79c07beefa74c fox tox.ini file --- a/tox.ini +++ b/tox.ini @@ -1,20 +1,19 @@ [tox] envlist=py26,py31,py27,py25,py24 indexserver= - default http://pypi.testrun.org - pypi http://pypi.python.org/simple + default = http://pypi.testrun.org + pypi = http://pypi.python.org/simple [testenv] changedir=testing deps=pytest -commands= py.test -rsfxX \ - --junitxml={envlogdir}/junit-{envname}.xml [] +commands= py.test --junitxml={envlogdir}/junit-{envname}.xml [] [testenv:py26] deps= pytest pypi pexpect -#[pytest] -#addopts = -rf +[pytest] +addopts = -rsfxX From commits-noreply at bitbucket.org Sat Nov 13 22:25:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 15:25:01 -0600 (CST) Subject: [py-svn] pytest-xdist commit f4c1b7411063: another fix to tox.ini Message-ID: <20101113212501.042112411A8@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1289683493 -3600 # Node ID f4c1b7411063e811e9925056d3d532df44302f71 # Parent b2582a5bd27955f9a993a98d21f555be95e6cbb3 another fix to tox.ini --- a/tox.ini +++ b/tox.ini @@ -13,7 +13,7 @@ commands= py.test --junitxml={envlogdir} [testenv:py26] deps= pytest - pypi pexpect + :pypi:pexpect [pytest] addopts = -rsfxX From commits-noreply at bitbucket.org Sat Nov 13 23:34:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 16:34:02 -0600 (CST) Subject: [py-svn] pytest commit 8295dc9f529f: perform represenation of short paths at test execution site Message-ID: <20101113223402.A14FE1E1167@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289687618 -3600 # Node ID 8295dc9f529f957ad5e552d575b55bf36e2dbf91 # Parent 057049589d52958d8561f45d88264ab3622ff70f perform represenation of short paths at test execution site --- a/_pytest/session.py +++ b/_pytest/session.py @@ -317,7 +317,8 @@ class Item(Node): return self._location except AttributeError: location = self.reportinfo() - location = (str(location[0]), location[1], str(location[2])) + fspath = self.session.fspath.bestrelpath(location[0]) + location = (fspath, location[1], str(location[2])) self._location = location return location --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -266,11 +266,7 @@ class TerminalReporter: def _locationline(self, collect_fspath, fspath, lineno, domain): if fspath and fspath != collect_fspath: - fspath = "%s <- %s" % ( - self.curdir.bestrelpath(py.path.local(collect_fspath)), - self.curdir.bestrelpath(py.path.local(fspath))) - elif fspath: - fspath = self.curdir.bestrelpath(py.path.local(fspath)) + fspath = "%s <- %s" % (collect_fspath, fspath) if lineno is not None: lineno += 1 if fspath and lineno and domain: From commits-noreply at bitbucket.org Sat Nov 13 23:34:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 13 Nov 2010 16:34:02 -0600 (CST) Subject: [py-svn] pytest commit 0b0933c4cacd: some pep8 fixes Message-ID: <20101113223402.B0BAD1E12D9@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289687630 -3600 # Node ID 0b0933c4cacdc941cc23dd3114f171457186d0d1 # Parent 8295dc9f529f957ad5e552d575b55bf36e2dbf91 some pep8 fixes --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -129,7 +129,7 @@ def pytest_plugin_registered(manager, pl # print "matching hook:", formatdef(method) if fail: name = getattr(plugin, '__name__', plugin) - raise PluginValidationError("%s:\n%s" %(name, stringio.getvalue())) + raise PluginValidationError("%s:\n%s" % (name, stringio.getvalue())) class PluginValidationError(Exception): """ plugin failed validation. """ @@ -151,7 +151,7 @@ def collectattr(obj): return methods def formatdef(func): - return "%s%s" %( + return "%s%s" % ( func.__name__, inspect.formatargspec(*inspect.getargspec(func)) ) --- a/_pytest/standalonetemplate.py +++ b/_pytest/standalonetemplate.py @@ -15,7 +15,7 @@ class DictImporter(object): def find_module(self, fullname, path=None): if fullname in self.sources: return self - if fullname+'.__init__' in self.sources: + if fullname + '.__init__' in self.sources: return self return None @@ -26,7 +26,7 @@ class DictImporter(object): s = self.sources[fullname] is_pkg = False except KeyError: - s = self.sources[fullname+'.__init__'] + s = self.sources[fullname + '.__init__'] is_pkg = True co = compile(s, fullname, 'exec') @@ -42,11 +42,11 @@ class DictImporter(object): def get_source(self, name): res = self.sources.get(name) if res is None: - res = self.sources.get(name+'.__init__') + res = self.sources.get(name + '.__init__') return res if __name__ == "__main__": - if sys.version_info >= (3,0): + if sys.version_info >= (3, 0): exec("def do_exec(co, loc): exec(co, loc)\n") import pickle sources = sources.encode("ascii") # ensure bytes --- a/tox.ini +++ b/tox.ini @@ -57,6 +57,6 @@ commands= [pytest] minversion=2.0 plugins=pytester -addopts=-rfx --pyargs +#addopts= -rxf --pyargs rsyncdirs=tox.ini pytest.py _pytest testing --- a/example/assertion/global_testmodule_config/conftest.py +++ b/example/assertion/global_testmodule_config/conftest.py @@ -1,4 +1,4 @@ -import py +import pytest, py mydir = py.path.local(__file__).dirpath() def pytest_runtest_setup(item): From commits-noreply at bitbucket.org Wed Nov 17 12:19:32 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 17 Nov 2010 05:19:32 -0600 (CST) Subject: [py-svn] pytest commit 05df9e37023f: bump version and comment out ignore-testclass-if-unittest-module-feature Message-ID: <20101117111932.A4CCA6C129F@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1289992884 -3600 # Node ID 05df9e37023f7e9d27b5bffae50fe9ae42701d56 # Parent 0b0933c4cacdc941cc23dd3114f171457186d0d1 bump version and comment out ignore-testclass-if-unittest-module-feature --- a/pytest.py +++ b/pytest.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev28' +__version__ = '2.0.0.dev29' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev28', + version='2.0.0.dev29', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -359,6 +359,7 @@ class TestInvocationVariants: ]) + @py.test.mark.xfail(reason="decide: feature or bug") def test_noclass_discovery_if_not_testcase(self, testdir): testpath = testdir.makepyfile(""" import unittest --- a/_pytest/python.py +++ b/_pytest/python.py @@ -59,8 +59,8 @@ def pytest_pycollect_makeitem(__multical if res is not None: return res if collector._istestclasscandidate(name, obj): - if hasattr(collector.obj, 'unittest'): - return # we assume it's a mixin class for a TestCase derived one + #if hasattr(collector.obj, 'unittest'): + # return # we assume it's a mixin class for a TestCase derived one return Class(name, parent=collector) elif collector.funcnamefilter(name) and hasattr(obj, '__call__'): if is_generator(obj): From commits-noreply at bitbucket.org Wed Nov 17 18:26:03 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 17 Nov 2010 11:26:03 -0600 (CST) Subject: [py-svn] pytest commit 2dea0dd5657e: run doctests in .txt/.rst files directly specified on command line irrespective of "test*.txt" pattern. Message-ID: <20101117172603.EC1986C129F@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290000801 -3600 # Node ID 2dea0dd5657ef31ef68cad10a4b356a29db500c8 # Parent 05df9e37023f7e9d27b5bffae50fe9ae42701d56 run doctests in .txt/.rst files directly specified on command line irrespective of "test*.txt" pattern. --- a/doc/index.txt +++ b/doc/index.txt @@ -5,7 +5,7 @@ py.test: no-boilerplate testing with Pyt .. note:: version 2.0 introduces ``pytest`` as the main Python import name - but for compatibility reasons you may continue to use ``py.test`` + but for compatibility reasons you can continue to use ``py.test`` in your test code. Welcome to ``py.test`` documentation: --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,12 +1,10 @@ from _pytest.doctest import DoctestModule, DoctestTextfile import py -pytest_plugins = ["pytest_doctest"] - class TestDoctests: def test_collect_testtextfile(self, testdir): - testdir.maketxtfile(whatever="") + w = testdir.maketxtfile(whatever="") checkfile = testdir.maketxtfile(test_something=""" alskdjalsdk >>> i = 5 @@ -18,6 +16,8 @@ class TestDoctests: items, reprec = testdir.inline_genitems(x) assert len(items) == 1 assert isinstance(items[0], DoctestTextfile) + items, reprec = testdir.inline_genitems(w) + assert len(items) == 1 def test_collect_module(self, testdir): path = testdir.makepyfile(whatever="#") --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -19,7 +19,8 @@ def pytest_collect_file(path, parent): if path.ext == ".py": if config.option.doctestmodules: return DoctestModule(path, parent) - elif path.check(fnmatch=config.getvalue("doctestglob")): + elif (path.ext in ('.txt', '.rst') and parent.session.isinitpath(path)) or \ + path.check(fnmatch=config.getvalue("doctestglob")): return DoctestTextfile(path, parent) class ReprFailDoctest(TerminalRepr): --- a/tox.ini +++ b/tox.ini @@ -57,6 +57,6 @@ commands= [pytest] minversion=2.0 plugins=pytester -#addopts= -rxf --pyargs +addopts= -rxf --pyargs rsyncdirs=tox.ini pytest.py _pytest testing From commits-noreply at bitbucket.org Wed Nov 17 18:26:04 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 17 Nov 2010 11:26:04 -0600 (CST) Subject: [py-svn] pytest commit 73811fb299b8: fix doctest IDs, also fix tree traversal and remove dead code Message-ID: <20101117172604.124EE6C132B@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290014668 -3600 # Node ID 73811fb299b8711440299d096f576bbc7f344139 # Parent 2dea0dd5657ef31ef68cad10a4b356a29db500c8 fix doctest IDs, also fix tree traversal and remove dead code --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -33,11 +33,6 @@ class ReprFailDoctest(TerminalRepr): self.reprlocation.toterminal(tw) class DoctestItem(pytest.Item): - def __init__(self, path, parent): - name = self.__class__.__name__ + ":" + path.basename - super(DoctestItem, self).__init__(name=name, parent=parent) - self.fspath = path - def repr_failure(self, excinfo): if excinfo.errisinstance(py.std.doctest.DocTestFailure): doctestfailure = excinfo.value @@ -67,13 +62,13 @@ class DoctestItem(pytest.Item): def reportinfo(self): return self.fspath, None, "[doctest]" -class DoctestTextfile(DoctestItem): +class DoctestTextfile(DoctestItem, pytest.File): def runtest(self): failed, tot = py.std.doctest.testfile( str(self.fspath), module_relative=False, raise_on_error=True, verbose=0) -class DoctestModule(DoctestItem): +class DoctestModule(DoctestItem, pytest.File): def runtest(self): module = self.fspath.pyimport() failed, tot = py.std.doctest.testmod( --- a/_pytest/session.py +++ b/_pytest/session.py @@ -482,7 +482,8 @@ class Session(FSCollector): resultnodes = [] for node in matching: if isinstance(node, pytest.Item): - resultnodes.append(node) + if not names: + resultnodes.append(node) continue assert isinstance(node, pytest.Collector) node.ihook.pytest_collectstart(collector=node) @@ -508,5 +509,3 @@ class Session(FSCollector): for x in self.genitems(subnode): yield x node.ihook.pytest_collectreport(report=rep) - -Session = Session --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -372,3 +372,22 @@ class TestInvocationVariants: """) reprec = testdir.inline_run(testpath) reprec.assertoutcome(passed=1) + + def test_doctest_id(self, testdir): + testdir.makefile('.txt', """ + >>> x=3 + >>> x + 4 + """) + result = testdir.runpytest("-rf") + lines = result.stdout.str().splitlines() + for line in lines: + if line.startswith("FAIL "): + testid = line[5:].strip() + break + result = testdir.runpytest(testid, '-rf') + result.stdout.fnmatch_lines([ + line, + "*1 failed*", + ]) + --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -472,10 +472,6 @@ class TestSession: item2b, = newcol.perform_collect([item.nodeid], genitems=False) assert item2b == item2 -def getargnode(collection, arg): - argpath = arg.relto(collection.fspath) - return collection.perform_collect([argpath], genitems=False)[0] - class Test_getinitialnodes: def test_global_file(self, testdir, tmpdir): x = tmpdir.ensure("x.py") @@ -552,3 +548,43 @@ class Test_genitems: s = items[0].getmodpath(stopatmodule=False) assert s.endswith("test_example_items1.testone") print(s) + +def test_matchnodes_two_collections_same_file(testdir): + testdir.makeconftest(""" + import pytest + def pytest_configure(config): + config.pluginmanager.register(Plugin2()) + + class Plugin2: + def pytest_collect_file(self, path, parent): + if path.ext == ".abc": + return MyFile2(path, parent) + + def pytest_collect_file(path, parent): + if path.ext == ".abc": + return MyFile1(path, parent) + + class MyFile1(pytest.Item, pytest.File): + def runtest(self): + pass + class MyFile2(pytest.File): + def collect(self): + return [Item2("hello", parent=self)] + + class Item2(pytest.Item): + def runtest(self): + pass + """) + p = testdir.makefile(".abc", "") + result = testdir.runpytest() + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*2 passed*", + ]) + res = testdir.runpytest("%s::hello" % p.basename) + res.stdout.fnmatch_lines([ + "*1 passed*", + ]) + + + From commits-noreply at bitbucket.org Wed Nov 17 18:27:17 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 17 Nov 2010 11:27:17 -0600 (CST) Subject: [py-svn] pytest commit 9810db0ef538: bump version number Message-ID: <20101117172717.16A676C129F@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290014827 -3600 # Node ID 9810db0ef538c51439a9d359c4e6202ca6e04d17 # Parent 73811fb299b8711440299d096f576bbc7f344139 bump version number --- a/pytest.py +++ b/pytest.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev29' +__version__ = '2.0.0.dev30' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev29', + version='2.0.0.dev30', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From commits-noreply at bitbucket.org Wed Nov 17 22:13:07 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 17 Nov 2010 15:13:07 -0600 (CST) Subject: [py-svn] pytest-xdist commit 8e7b9712d0db: important for allowing addopts + looponfail Message-ID: <20101117211307.B93FD1E1477@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1290028368 -3600 # Node ID 8e7b9712d0dbe19a7577ef706293f9d2e24e24b0 # Parent f4c1b7411063e811e9925056d3d532df44302f71 important for allowing addopts + looponfail --- a/xdist/looponfail.py +++ b/xdist/looponfail.py @@ -122,7 +122,7 @@ def init_slave_session(channel, args, op from _pytest.config import Config config = Config() config.option.__dict__.update(option_dict) - config._preparse(args) + config._preparse(list(args)) config.args = args from xdist.looponfail import SlaveFailSession SlaveFailSession(config, channel).main() From commits-noreply at bitbucket.org Thu Nov 18 14:58:20 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 18 Nov 2010 07:58:20 -0600 (CST) Subject: [py-svn] pytest-xdist commit 917353071622: shift documentation, remove duplicate code Message-ID: <20101118135820.2D2B31E0FB4@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1290088675 -3600 # Node ID 917353071622e40941b1b1d290bdf48f848c8e7a # Parent 8e7b9712d0dbe19a7577ef706293f9d2e24e24b0 shift documentation, remove duplicate code --- a/xdist/plugin.py +++ b/xdist/plugin.py @@ -1,146 +1,3 @@ -"""loop on failing tests, distribute test runs to CPUs and hosts. - -The `pytest-xdist`_ plugin extends py.test with some unique -test execution modes: - -* Looponfail: run your tests repeatedly in a subprocess. After each run py.test - waits until a file in your project changes and then re-runs the previously - failing tests. This is repeated until all tests pass after which again - a full run is performed. - -* Load-balancing: if you have multiple CPUs or hosts you can use - those for a combined test run. This allows to speed up - development or to use special resources of remote machines. - -* Multi-Platform coverage: you can specify different Python interpreters - or different platforms and run tests in parallel on all of them. - -Before running tests remotely, ``py.test`` efficiently synchronizes your -program source code to the remote place. All test results -are reported back and displayed to your local test session. -You may specify different Python versions and interpreters. - -.. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist - -Usage examples ---------------------- - -Speed up test runs by sending tests to multiple CPUs -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -To send tests to multiple CPUs, type:: - - py.test -n NUM - -Especially for longer running tests or tests requiring -a lot of IO this can lead to considerable speed ups. - - -Running tests in a Python subprocess -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -To instantiate a python2.4 sub process and send tests to it, you may type:: - - py.test -d --tx popen//python=python2.4 - -This will start a subprocess which is run with the "python2.4" -Python interpreter, found in your system binary lookup path. - -If you prefix the --tx option value like this:: - - --tx 3*popen//python=python2.4 - -then three subprocesses would be created and tests -will be load-balanced across these three processes. - - -Sending tests to remote SSH accounts -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -Suppose you have a package ``mypkg`` which contains some -tests that you can successfully run locally. And you -have a ssh-reachable machine ``myhost``. Then -you can ad-hoc distribute your tests by typing:: - - py.test -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg - -This will synchronize your ``mypkg`` package directory -to an remote ssh account and then locally collect tests -and send them to remote places for execution. - -You can specify multiple ``--rsyncdir`` directories -to be sent to the remote side. - -**NOTE:** For py.test to collect and send tests correctly -you not only need to make sure all code and tests -directories are rsynced, but that any test (sub) directory -also has an ``__init__.py`` file because internally -py.test references tests as a fully qualified python -module path. **You will otherwise get strange errors** -during setup of the remote side. - -Sending tests to remote Socket Servers -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -Download the single-module `socketserver.py`_ Python program -and run it like this:: - - python socketserver.py - -It will tell you that it starts listening on the default -port. You can now on your home machine specify this -new socket host with something like this:: - - py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg - - -.. _`atonce`: - -Running tests on many platforms at once -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -The basic command to run tests on multiple platforms is:: - - py.test --dist=each --tx=spec1 --tx=spec2 - -If you specify a windows host, an OSX host and a Linux -environment this command will send each tests to all -platforms - and report back failures from all platforms -at once. The specifications strings use the `xspec syntax`_. - -.. _`xspec syntax`: http://codespeak.net/execnet/trunk/basics.html#xspec - -.. _`socketserver.py`: http://codespeak.net/svn/py/dist/py/execnet/script/socketserver.py - -.. _`execnet`: http://codespeak.net/execnet - -Specifying test exec environments in a conftest.py -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -Instead of specifying command line options, you can -put options values in a ``conftest.py`` file like this:: - - option_tx = ['ssh=myhost//python=python2.5', 'popen//python=python2.5'] - option_dist = True - -Any commandline ``--tx`` specifictions will add to the list of -available execution environments. - -Specifying "rsync" dirs in a setup.cfg|tox.ini -+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -In a ``tox.ini`` or ``setup.cfg`` file in your root project directory -you may specify directories to include or to exclude in synchronisation:: - - [pytest] - rsyncdirs = . mypkg helperpkg - rsyncignore = .hg - -These directory specifications are relative to the directory -where the configuration file was found. - -""" - import sys import py, pytest --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5a8' +__version__ = '1.5a9' --- a/xdist/gwmanage.py +++ /dev/null @@ -1,99 +0,0 @@ -""" - instantiating, managing and rsyncing to test hosts -""" - -import py -import sys, os.path -import execnet -from execnet.gateway_base import RemoteError - -class GatewayManager: - EXIT_TIMEOUT = 10 - RemoteError = RemoteError - def __init__(self, specs, hook, defaultchdir="pyexecnetcache"): - self.specs = [] - self.hook = hook - self.group = execnet.Group() - for spec in specs: - if not isinstance(spec, execnet.XSpec): - spec = execnet.XSpec(spec) - if not spec.chdir and not spec.popen: - spec.chdir = defaultchdir - self.specs.append(spec) - - def makegateways(self): - assert not list(self.group) - for spec in self.specs: - gw = self.group.makegateway(spec) - self.hook.pytest_gwmanage_newgateway(gateway=gw) - - def rsync(self, source, notify=None, verbose=False, ignores=None): - """ perform rsync to all remote hosts. - """ - rsync = HostRSync(source, verbose=verbose, ignores=ignores) - seen = py.builtin.set() - gateways = [] - for gateway in self.group: - spec = gateway.spec - if spec.popen and not spec.chdir: - # XXX this assumes that sources are python-packages - # and that adding the basedir does not hurt - gateway.remote_exec(""" - import sys ; sys.path.insert(0, %r) - """ % os.path.dirname(str(source))).waitclose() - continue - if spec not in seen: - def finished(): - if notify: - notify("rsyncrootready", spec, source) - rsync.add_target_host(gateway, finished=finished) - seen.add(spec) - gateways.append(gateway) - if seen: - self.hook.pytest_gwmanage_rsyncstart( - source=source, - gateways=gateways, - ) - rsync.send() - self.hook.pytest_gwmanage_rsyncfinish( - source=source, - gateways=gateways, - ) - - def exit(self): - self.group.terminate(self.EXIT_TIMEOUT) - -class HostRSync(execnet.RSync): - """ RSyncer that filters out common files - """ - def __init__(self, sourcedir, *args, **kwargs): - self._synced = {} - ignores= None - if 'ignores' in kwargs: - ignores = kwargs.pop('ignores') - self._ignores = ignores or [] - super(HostRSync, self).__init__(sourcedir=sourcedir, **kwargs) - - def filter(self, path): - path = py.path.local(path) - if not path.ext in ('.pyc', '.pyo'): - if not path.basename.endswith('~'): - if path.check(dotfile=0): - for x in self._ignores: - if path == x: - break - else: - return True - - def add_target_host(self, gateway, finished=None): - remotepath = os.path.basename(self._sourcedir) - super(HostRSync, self).add_target(gateway, remotepath, - finishedcallback=finished, - delete=True,) - - def _report_send_file(self, gateway, modified_rel_path): - if self._verbose: - path = os.path.basename(self._sourcedir) + "/" + modified_rel_path - remotepath = gateway.spec.chdir - py.builtin.print_('%s:%s <= %s' % - (gateway.spec, remotepath, path)) --- a/testing/conftest.py +++ b/testing/conftest.py @@ -27,7 +27,7 @@ def getspecssh(config): if spec.ssh: if not py.path.local.sysfind("ssh"): py.test.skip("command not found: ssh") - return spec + return str(spec) py.test.skip("need '--gx ssh=...'") def getsocketspec(config): --- a/setup.py +++ b/setup.py @@ -1,18 +1,10 @@ -""" -py.test 'xdist' plugin for distributed testing and loop-on-failing modes. - -See http://pytest.org/plugin/xdist.html for documentation and, after -installation of the ``pytest-xdist`` PyPI package, ``py.test -h`` -for the new options. -""" - from setuptools import setup setup( name="pytest-xdist", - version='1.5a8', + version='1.5a9', description='py.test xdist plugin for distributed testing and loop-on-failing modes', - long_description=__doc__, + long_description=open('README.txt').read(), license='GPLv2 or later', author='holger krekel and contributors', author_email='py-dev at codespeak.net,holger at merlinux.eu', --- a/README.txt +++ b/README.txt @@ -1,29 +1,170 @@ -py.test xdist plugin: distributed testing and loop failure +xdist: pytest distributed testing plugin =============================================================== -.. _`pytest-xdist respository`: http://bitbucket.org/hpk42/pytest-xdist -.. _`pytest`: http://pytest.org +The `pytest-xdist`_ plugin extends py.test with some unique +test execution modes: -The pytest-xdist plugin extends `py.test`_ to ad-hoc distribute test -runs to multiple CPUs or remote machines. It requires setuptools -or distribute which help to pull in the neccessary execnet and -pytest-core dependencies. +* Looponfail: run your tests repeatedly in a subprocess. After each run py.test + waits until a file in your project changes and then re-runs the previously + failing tests. This is repeated until all tests pass after which again + a full run is performed. -Install the plugin locally with:: +* multiprocess Load-balancing: if you have multiple CPUs or hosts you can use + those for a combined test run. This allows to speed up + development or to use special resources of remote machines. - python setup.py install +* Multi-Platform coverage: you can specify different Python interpreters + or different platforms and run tests in parallel on all of them. -or use the package in develope/in-place mode, particularly -useful with a checkout of the `pytest-xdist repository`_:: +Before running tests remotely, ``py.test`` efficiently "rsyncs" your +program source code to the remote place. All test results +are reported back and displayed to your local terminal. +You may specify different Python versions and interpreters. + + +Installation +----------------------- + +Install the plugin with:: + + easy_install pytest-xdist + + # or + + pip install pytest-xdist + +or use the package in develope/in-place mode with +a checkout of the `pytest-xdist repository`_ :: python setup.py develop -or use one of:: +Usage examples +--------------------- - easy_install pytest-xdist +Speed up test runs by sending tests to multiple CPUs ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - pip install pytest-xdist +To send tests to multiple CPUs, type:: -for downloading and installing it in one go. + py.test -n NUM -holger krekel +Especially for longer running tests or tests requiring +a lot of IO this can lead to considerable speed ups. + + +Running tests in a Python subprocess ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +To instantiate a python2.4 sub process and send tests to it, you may type:: + + py.test -d --tx popen//python=python2.4 + +This will start a subprocess which is run with the "python2.4" +Python interpreter, found in your system binary lookup path. + +If you prefix the --tx option value like this:: + + --tx 3*popen//python=python2.4 + +then three subprocesses would be created and tests +will be load-balanced across these three processes. + + +Sending tests to remote SSH accounts ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Suppose you have a package ``mypkg`` which contains some +tests that you can successfully run locally. And you +have a ssh-reachable machine ``myhost``. Then +you can ad-hoc distribute your tests by typing:: + + py.test -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg + +This will synchronize your ``mypkg`` package directory +to an remote ssh account and then locally collect tests +and send them to remote places for execution. + +You can specify multiple ``--rsyncdir`` directories +to be sent to the remote side. + +**NOTE:** For py.test to collect and send tests correctly +you not only need to make sure all code and tests +directories are rsynced, but that any test (sub) directory +also has an ``__init__.py`` file because internally +py.test references tests as a fully qualified python +module path. **You will otherwise get strange errors** +during setup of the remote side. + +Sending tests to remote Socket Servers ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Download the single-module `socketserver.py`_ Python program +and run it like this:: + + python socketserver.py + +It will tell you that it starts listening on the default +port. You can now on your home machine specify this +new socket host with something like this:: + + py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg + + +.. _`atonce`: + +Running tests on many platforms at once ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +The basic command to run tests on multiple platforms is:: + + py.test --dist=each --tx=spec1 --tx=spec2 + +If you specify a windows host, an OSX host and a Linux +environment this command will send each tests to all +platforms - and report back failures from all platforms +at once. The specifications strings use the `xspec syntax`_. + +.. _`xspec syntax`: http://codespeak.net/execnet/trunk/basics.html#xspec + +.. _`socketserver.py`: http://bitbucket.org/hpk42/execnet/raw/2af991418160/execnet/script/socketserver.py + +.. _`execnet`: http://codespeak.net/execnet + +Specifying test exec environments in an ini file ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +pytest (since version 2.0) supports ini-style cofiguration. +You can for example make running with three subprocesses +your default like this:: + + [pytest] + addopts = -n3 + +You can also add default environments like this:: + + [pytest] + addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python2.6 + +and then just type:: + + py.test --dist=each + +to run tests in each of the environments. + +Specifying "rsync" dirs in an ini-file ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +In a ``tox.ini`` or ``setup.cfg`` file in your root project directory +you may specify directories to include or to exclude in synchronisation:: + + [pytest] + rsyncdirs = . mypkg helperpkg + rsyncignore = .hg + +These directory specifications are relative to the directory +where the configuration file was found. + +.. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist +.. _`pytest-xdist repository`: http://bitbucket.org/hpk42/pytest-xdist +.. _`pytest`: http://pytest.org + From commits-noreply at bitbucket.org Thu Nov 18 14:58:31 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 18 Nov 2010 07:58:31 -0600 (CST) Subject: [py-svn] pytest commit 867df79243d4: refine docs and docstrings, fix some small bits here and there while doing that. Message-ID: <20101118135831.6945724121E@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290088576 -3600 # Node ID 867df79243d4ecc5607d1004e3ab96599c861dd5 # Parent 8431fad888a4d0fd556456a89198f2d47722a8b9 refine docs and docstrings, fix some small bits here and there while doing that. --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -2,6 +2,10 @@ import py, pytest from _pytest.mark import MarkGenerator as Mark class TestMark: + def test_pytest_exists_in_namespace_all(self): + assert 'mark' in py.test.__all__ + assert 'mark' in pytest.__all__ + def test_pytest_mark_notcallable(self): mark = Mark() pytest.raises((AttributeError, TypeError), "mark()") --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -64,12 +64,16 @@ class DoctestItem(pytest.Item): class DoctestTextfile(DoctestItem, pytest.File): def runtest(self): - failed, tot = py.std.doctest.testfile( + doctest = py.std.doctest + failed, tot = doctest.testfile( str(self.fspath), module_relative=False, + optionflags=doctest.ELLIPSIS, raise_on_error=True, verbose=0) class DoctestModule(DoctestItem, pytest.File): def runtest(self): + doctest = py.std.doctest module = self.fspath.pyimport() - failed, tot = py.std.doctest.testmod( - module, raise_on_error=True, verbose=0) + failed, tot = doctest.testmod( + module, raise_on_error=True, verbose=0, + optionflags=doctest.ELLIPSIS) --- a/doc/nose.txt +++ b/doc/nose.txt @@ -4,7 +4,6 @@ Running test written for nose .. include:: links.inc py.test has basic support for running tests written for nose_. -This is implemented in :pymod:`_pytest.nose`. Usage ------------- @@ -13,8 +12,8 @@ type:: py.test # instead of 'nosetests' -and you should be able to run your nose style tests and at the same -make full use of py.test's capabilities. +and you should be able to run your nose style tests and +make use of py.test's capabilities. Supported nose Idioms ---------------------- @@ -25,18 +24,12 @@ Supported nose Idioms * yield-based tests and their setup * general usage of nose utilities -Unsupported idioms / issues +Unsupported idioms / known issues ---------------------------------- - nose-style doctests are not collected and executed correctly, - also fixtures don't work. + also doctest fixtures don't work. - no nose-configuration is recognized -If you find other issues or have suggestions please run:: - py.test --pastebin=all - -and send the resulting URL to a py.test contact channel, -at best to the mailing list. -""" --- a/_pytest/python.py +++ b/_pytest/python.py @@ -762,19 +762,28 @@ def raises(ExpectedException, *args, **k If using Python 2.5 or above, you may use this function as a context manager:: - >>> with raises(ZeroDivisionError): - ... 1/0 + >>> with raises(ZeroDivisionError): + ... 1/0 - Or you can one of two forms: + Or you can specify a callable by passing a to-be-called lambda:: - if args[0] is callable: raise AssertionError if calling it with - the remaining arguments does not raise the expected exception. - if args[0] is a string: raise AssertionError if executing the - the string in the calling scope does not raise expected exception. - examples: - >>> x = 5 - >>> raises(TypeError, lambda x: x + 'hello', x=x) - >>> raises(TypeError, "x + 'hello'") + >>> raises(ZeroDivisionError, lambda: 1/0) + + + or you can specify an arbitrary callable with arguments:: + + >>> def f(x): return 1/x + ... + >>> raises(ZeroDivisionError, f, 0) + + >>> raises(ZeroDivisionError, f, x=0) + + + A third possibility is to use a string which which will + be executed:: + + >>> raises(ZeroDivisionError, "f(0)") + """ __tracebackhide__ = True --- a/doc/index.txt +++ b/doc/index.txt @@ -5,8 +5,8 @@ py.test: no-boilerplate testing with Pyt .. note:: version 2.0 introduces ``pytest`` as the main Python import name - but for compatibility reasons you can continue to use ``py.test`` - in your test code. + but for compatibility reasons you can continue to use ``import py`` + and ``py.test.XYZ`` to access :ref:`pytest helpers` in your test code. Welcome to ``py.test`` documentation: @@ -25,6 +25,7 @@ Welcome to ``py.test`` documentation: :hidden: changelog.txt + example/attic Indices and tables ================== --- a/doc/builtin.txt +++ b/doc/builtin.txt @@ -1,8 +1,10 @@ + +.. _`pytest helpers`: pytest builtin helpers ================================================ -builtin pytest.* helpers +builtin pytest.* functions and helping objects ----------------------------------------------------- You can always use an interactive Python prompt and type:: --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -39,7 +39,7 @@ class TestGeneralUsage: def test_1(pytestconfig): pytestconfig.getbasetemp().ensure("hello") """) - result = testdir.runpytest(p, '--basetemp=%s' %mytemp) + result = testdir.runpytest(p, '--basetemp=%s' % mytemp) assert result.ret == 0 assert mytemp.join('hello').check() @@ -239,7 +239,7 @@ class TestInvocationVariants: def test_pydoc(self, testdir): for name in ('py.test', 'pytest'): - result = testdir.runpython_c("import %s;help(%s)" % (name,name)) + result = testdir.runpython_c("import %s;help(%s)" % (name, name)) assert result.ret == 0 s = result.stdout.str() assert 'MarkGenerator' in s @@ -325,7 +325,7 @@ class TestInvocationVariants: class MyPlugin: def pytest_addoption(self, parser): parser.addoption("--myopt") - + pytest.main(["-h"], plugins=[MyPlugin()]) out, err = capsys.readouterr() assert "--myopt" in out @@ -358,7 +358,6 @@ class TestInvocationVariants: "ERROR*file*or*package*not*found*", ]) - @pytest.mark.xfail(reason="decide: feature or bug") def test_noclass_discovery_if_not_testcase(self, testdir): testpath = testdir.makepyfile(""" --- a/doc/features.txt +++ b/doc/features.txt @@ -5,12 +5,12 @@ no-boilerplate testing with Python ---------------------------------- - automatic, fully customizable Python test discovery -- allows fully :pep:`8` compliant coding style - write simple test functions and freely group tests - ``assert`` statement for your assertions - powerful parametrization of test functions -- rely on powerful traceback and assertion reporting +- rely on helpful traceback and failing assertion reporting - use ``print`` or ``pdb`` debugging on failures +- enables fully :pep:`8` compliant coding style extensive plugin and customization system ------------------------------------------------------ @@ -25,11 +25,9 @@ extensive plugin and customization syste mature command line testing tool -------------------------------------- -- powerful :ref:`usage` possibilities +- powerful :ref:`usage` possibilities, well sorted command line options - used in many projects, ranging from 10 to 10K tests -- simple well sorted command line options -- runs on Unix, Windows from Python 2.4 up to Python 3.1 and 3.2 -- is itself tested extensively on a CI server +- tested on Unix and Windows from Python 2.4 up to Python 3.1 and 3.2 - keyword/testname based selection of tests integrates well with CI systems @@ -41,7 +39,6 @@ integrates well with CI systems .. _`tox`: http://codespeak.net/tox - supports common testing practises and methods ----------------------------------------------------------- --- a/doc/apiref.txt +++ b/doc/apiref.txt @@ -15,6 +15,7 @@ py.test reference documentation xunit_setup.txt capture.txt monkeypatch.txt + xdist.txt tmpdir.txt skipping.txt mark.txt --- a/doc/links.inc +++ b/doc/links.inc @@ -12,6 +12,7 @@ .. _`setuptools`: http://pypi.python.org/pypi/setuptools .. _`easy_install`: +.. _`distribute docs`: .. _`distribute`: http://pypi.python.org/pypi/distribute .. _`pip`: http://pypi.python.org/pypi/pip .. _`virtualenv`: http://pypi.python.org/pypi/virtualenv --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -36,17 +36,17 @@ That's it. You can execute the test func =========================== test session starts ============================ platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 test path 1: /tmp/doc-exec-70 - + test_sample.py F - + ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - + def test_answer(): > assert func(3) == 5 E assert 4 == 5 E + where 4 = func(3) - + test_sample.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= @@ -120,14 +120,14 @@ run the module by passing its filename:: .F ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - + self = - + def test_two(self): x = "hello" > assert hasattr(x, 'check') E assert hasattr('hello', 'check') - + test_class.py:8: AssertionError 1 failed, 1 passed in 0.02 seconds @@ -156,14 +156,14 @@ before performing the test function call F ================================= FAILURES ================================= _____________________________ test_needsfiles ______________________________ - + tmpdir = local('/tmp/pytest-122/test_needsfiles0') - + def test_needsfiles(tmpdir): print tmpdir > assert 0 E assert 0 - + test_tmpdir.py:3: AssertionError ----------------------------- Captured stdout ------------------------------ /tmp/pytest-122/test_needsfiles0 @@ -194,29 +194,20 @@ Known Installation issues easy_install or pip not found? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -Consult `distribute docs `_ to install the ``easy_install`` -tool on your machine. You may also use the original but somewhat older -`setuptools`_ project although we generally recommend to use -``distribute`` because it contains more bug fixes and also works for -Python3. - -For Python2 you can also consult pip_ for the popular ``pip`` tool. - -However, If you want to install on Python3 you need to use Distribute_ which -provides the ``easy_install`` utility. - +Consult `distribute docs`_ to install the ``easy_install`` +tool on your machine. You may also use the older +`setuptools`_ project but it lacks bug fixes and does not +work on Python3. If you use Python2 you may also install pip_. py.test not found on Windows despite installation? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ .. _`Python for Windows`: http://www.imladris.com/Scripts/PythonForWindows.html - - **Windows**: If "easy_install" or "py.test" are not found - please see here for preparing your environment for running - command line tools: `Python for Windows`_. You may alternatively - use an `ActivePython install`_ which makes command line tools - automatically available under Windows. + you need to add the Python script path to your ``PATH``, see here: + `Python for Windows`_. You may alternatively use an `ActivePython install`_ + which does this for you automatically. .. _`ActivePython install`: http://www.activestate.com/activepython/downloads --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -204,12 +204,12 @@ class TestCustomConftests: return path.basename.startswith("x") or \ path.basename == "test_one.py" """) - testdir.mkdir("xy123").ensure("test_hello.py").write( - "syntax error" - ) + sub = testdir.mkdir("xy123") + sub.ensure("test_hello.py").write("syntax error") + sub.join("conftest.py").write("syntax error") testdir.makepyfile("def test_hello(): pass") testdir.makepyfile(test_one="syntax error") - result = testdir.runpytest() + result = testdir.runpytest("--fulltrace") assert result.ret == 0 result.stdout.fnmatch_lines(["*1 passed*"]) --- a/_pytest/recwarn.py +++ b/_pytest/recwarn.py @@ -24,8 +24,8 @@ def pytest_namespace(): return {'deprecated_call': deprecated_call} def deprecated_call(func, *args, **kwargs): - """ assert that calling func(*args, **kwargs) - triggers a DeprecationWarning. + """ assert that calling ``func(*args, **kwargs)`` + triggers a DeprecationWarning. """ warningmodule = py.std.warnings l = [] --- a/_pytest/session.py +++ b/_pytest/session.py @@ -408,7 +408,8 @@ class Session(FSCollector): path = names.pop(0) if path.check(dir=1): assert not names, "invalid arg %r" %(arg,) - for path in path.visit(rec=self._recurse, bf=True, sort=True): + for path in path.visit(fil=lambda x: x.check(file=1), + rec=self._recurse, bf=True, sort=True): for x in self._collectfile(path): yield x else: @@ -424,12 +425,13 @@ class Session(FSCollector): return ihook.pytest_collect_file(path=path, parent=self) def _recurse(self, path): - ihook = self.gethookproxy(path) + ihook = self.gethookproxy(path.dirpath()) if ihook.pytest_ignore_collect(path=path, config=self.config): return for pat in self._norecursepatterns: if path.check(fnmatch=pat): return False + ihook = self.gethookproxy(path) ihook.pytest_collect_directory(path=path, parent=self) return True --- a/tox.ini +++ b/tox.ini @@ -24,6 +24,12 @@ commands= py.test -n3 -rfsxX \ --junitxml={envlogdir}/junit-{envname}.xml [] +[testenv:doctest] +changedir=. +commands=py.test --doctest-modules _pytest +deps= + + [testenv:doc] basepython=python changedir=doc @@ -57,6 +63,6 @@ commands= [pytest] minversion=2.0 plugins=pytester -addopts= -rxf --pyargs +addopts= -rxf --pyargs --doctest-modules rsyncdirs=tox.ini pytest.py _pytest testing --- a/doc/example/x.txt +++ /dev/null @@ -1,123 +0,0 @@ -Looping on the failing test set ------------------------------------------ - -``py.test --looponfailing`` (implemented through the external -`pytest-xdist`_ plugin) allows to run a test suite, -memorize all failures and then loop over the failing set -of tests until they all pass. It will re-start running -the tests when it detects file changes in your project. - -select tests by keyword / test name search ------------------------------------------------------ - -.. _`selection by keyword`: - -You can selectively run tests by specifiying a keyword -on the command line. Examples:: - - py.test -k test_simple - py.test -k "-test_simple" - -will run all tests matching (or not matching) the -"test_simple" keyword. Note that you need to quote -the keyword if "-" is recognized as an indicator -for a commandline option. Lastly, you may use:: - - py.test. -k "test_simple:" - -which will run all tests after the expression has *matched once*, i.e. -all tests that are seen after a test that matches the "test_simple" -keyword. - -By default, all filename parts and -class/function names of a test function are put into the set -of keywords for a given test. You can specify additional -kewords like this: - -.. sourcecode:: python - - @py.test.mark.webtest - def test_send_http(): - ... - -and then use those keywords to select tests. See the `pytest_keyword`_ -plugin for more information. - -.. _`pytest_keyword`: plugin/mark.html -skip or expect-to-fail a test -------------------------------------------- - -py.test has a dedicated `skipping plugin`_ that allows to define - -* define "skip" outcomes indicating a platform or a - dependency mismatch. - -* "xfail" outcomes indicating an "expected failure" either with - with or without running a test. - -* skip and xfail outcomes can be applied at module, class or method - level or even only for certain argument sets of a parametrized function. - -.. _`skipping plugin`: plugin/skipping.html -.. _`funcargs mechanism`: funcargs.html -.. _`doctest.py`: http://docs.python.org/library/doctest.html -.. _`xUnit style setup`: xunit_setup.html -.. _`pytest_nose`: plugin/nose.html - - -no-boilerplate testing ----------------------------------- - -.. _`autocollect`: - -automatic Python test discovery -+++++++++++++++++++++++++++++++++++ - -By default, all python modules with a ``test_*.py`` -filename are inspected for finding tests: - -* functions with a name beginning with ``test_`` -* classes with a leading ``Test`` name and ``test`` prefixed methods. -* ``unittest.TestCase`` subclasses - -parametrizing test functions and functional testing -++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -py.test offers the unique `funcargs mechanism`_ for setting up -and passing project-specific objects to Python test functions. -Test Parametrization happens by triggering a call to the same test -function with different argument values. For doing fixtures -using the funcarg mechanism makes your test and setup code -more efficient and more readable. This is especially true -for functional tests which might depend on command line -options and a setup that needs to be shared across -a whole test run. - -per-test capturing of output, including subprocesses -++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -By default, ``py.test`` captures all writes to stdout/stderr. -Output from ``print`` statements as well as from subprocesses -is captured_. When a test fails, the associated captured outputs are shown. -This allows you to put debugging print statements in your code without -being overwhelmed by all the output that might be generated by tests -that do not fail. - -.. _captured: plugin/capture.html - - -information-rich tracebacks, PDB introspection -+++++++++++++++++++++++++++++++++++++++++++++++++++++++ - -.. _`example tracebacks`: http://paste.pocoo.org/show/134814/ - -A lot of care is taken to present useful failure information -and in particular nice and concise Python tracebacks. This -is especially useful if you need to regularly look at failures -from nightly runs, i.e. are detached from the actual test -running session. Here are `example tracebacks`_ for a number of failing -test functions. You can modify traceback printing styles through the -command line. Using the `--pdb`` option you can automatically activate -a PDB `Python debugger`_ when a test fails. - - --- a/_pytest/nose.py +++ b/_pytest/nose.py @@ -1,4 +1,4 @@ -"""run test suites written for nose. """ +""" run test suites written for nose. """ import pytest, py import inspect --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -52,7 +52,6 @@ earlier than further away ones. under a package scope or to never import anything from a conftest.py file. -.. _`installing plugins`: .. _`external plugins`: Installing External Plugins / Searching @@ -85,12 +84,11 @@ you can copy from: * around 20 `builtin plugins`_ which comprise py.test's own functionality * around 10 `external plugins`_ providing additional features -All of these plugins are using the documented `well specified hooks`_ -to implement their wide-ranging functionality. +All of these plugins implement the documented `well specified hooks`_ +to extend and add functionality. .. _`setuptools entry points`: - Making your plugin installable by others ----------------------------------------------- --- a/doc/announce/release-2.0.0.txt +++ b/doc/announce/release-2.0.0.txt @@ -87,6 +87,7 @@ and "pytest.raises" used as the recommen - py.test.config is now only available if you are in a test run. - the following (mostly already deprecated) functionality was removed: + - removed support for Module/Class/... collection node definitions in conftest.py files. They will cause nothing special. - removed support for calling the pre-1.0 collection API of "run()" and "join" From commits-noreply at bitbucket.org Thu Nov 18 14:58:31 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 18 Nov 2010 07:58:31 -0600 (CST) Subject: [py-svn] pytest commit 8431fad888a4: streamline docs, especially use "import pytest" and "pytest.*" in python code examples instead of "import py" and "py.test.*". Message-ID: <20101118135831.CB18B241BE7@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290028336 -3600 # Node ID 8431fad888a4d0fd556456a89198f2d47722a8b9 # Parent 9810db0ef538c51439a9d359c4e6202ca6e04d17 streamline docs, especially use "import pytest" and "pytest.*" in python code examples instead of "import py" and "py.test.*". --- a/testing/test_pastebin.py +++ b/testing/test_pastebin.py @@ -12,13 +12,13 @@ class TestPasting: def test_failed(self, testdir, pastebinlist): testpath = testdir.makepyfile(""" - import py + import pytest def test_pass(): pass def test_fail(): assert 0 def test_skip(): - py.test.skip("") + pytest.skip("") """) reprec = testdir.inline_run(testpath, "--paste=failed") assert len(pastebinlist) == 1 @@ -29,13 +29,13 @@ class TestPasting: def test_all(self, testdir, pastebinlist): testpath = testdir.makepyfile(""" - import py + import pytest def test_pass(): pass def test_fail(): assert 0 def test_skip(): - py.test.skip("") + pytest.skip("") """) reprec = testdir.inline_run(testpath, "--pastebin=all") assert reprec.countoutcomes() == [1,1,1] --- a/doc/builtin.txt +++ b/doc/builtin.txt @@ -1,8 +1,8 @@ -py.test builtin helpers +pytest builtin helpers ================================================ -builtin py.test.* helpers +builtin pytest.* helpers ----------------------------------------------------- You can always use an interactive Python prompt and type:: @@ -28,25 +28,25 @@ You can ask for available builtin or pro captures writes to sys.stdout/sys.stderr and makes them available successively via a ``capsys.readouterr()`` method which returns a ``(out, err)`` tuple of captured snapshot strings. - + capfd captures writes to file descriptors 1 and 2 and makes snapshotted ``(out, err)`` string tuples available via the ``capsys.readouterr()`` method. If the underlying platform does not have ``os.dup`` (e.g. Jython) tests using this funcarg will automatically skip. - + tmpdir return a temporary directory path object unique to each test function invocation, created as a sub directory of the base temporary directory. The returned object is a `py.path.local`_ path object. - + monkeypatch The returned ``monkeypatch`` funcarg provides these helper methods to modify objects, dictionaries or os.environ:: - + monkeypatch.setattr(obj, name, value, raising=True) monkeypatch.delattr(obj, name, raising=True) monkeypatch.setitem(mapping, name, value) @@ -54,15 +54,15 @@ You can ask for available builtin or pro monkeypatch.setenv(name, value, prepend=False) monkeypatch.delenv(name, value, raising=True) monkeypatch.syspath_prepend(path) - + All modifications will be undone when the requesting test function finished its execution. The ``raising`` parameter determines if a KeyError or AttributeError will be raised if the set/deletion operation has no target. - + recwarn Return a WarningsRecorder instance that provides these methods: - + * ``pop(category=None)``: return last warning matching the category. * ``clear()``: clear list of warnings - + --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -41,11 +41,11 @@ def pytest_generate_tests(metafunc): class TestTerminal: def test_pass_skip_fail(self, testdir, option): p = testdir.makepyfile(""" - import py + import pytest def test_ok(): pass def test_skip(): - py.test.skip("xx") + pytest.skip("xx") def test_func(): assert 0 """) @@ -69,7 +69,7 @@ class TestTerminal: def test_internalerror(self, testdir, linecomp): modcol = testdir.getmodulecol("def test_one(): pass") rep = TerminalReporter(modcol.config, file=linecomp.stringio) - excinfo = py.test.raises(ValueError, "raise ValueError('hello')") + excinfo = pytest.raises(ValueError, "raise ValueError('hello')") rep.pytest_internalerror(excinfo.getrepr()) linecomp.assert_contains_lines([ "INTERNALERROR> *ValueError*hello*" @@ -136,7 +136,7 @@ class TestTerminal: def test_foobar(): assert 0 def test_spamegg(): - import py; py.test.skip('skip me please!') + import py; pytest.skip('skip me please!') def test_interrupt_me(): raise KeyboardInterrupt # simulating the user """) @@ -180,8 +180,8 @@ class TestCollectonly: def test_collectonly_skipped_module(self, testdir, linecomp): modcol = testdir.getmodulecol(configargs=['--collectonly'], source=""" - import py - py.test.skip("nomod") + import pytest + pytest.skip("nomod") """) rep = CollectonlyReporter(modcol.config, out=linecomp.stringio) modcol.config.pluginmanager.register(rep) @@ -335,13 +335,13 @@ class TestTerminalFunctional: def test_no_skip_summary_if_failure(self, testdir): testdir.makepyfile(""" - import py + import pytest def test_ok(): pass def test_fail(): assert 0 def test_skip(): - py.test.skip("dontshow") + pytest.skip("dontshow") """) result = testdir.runpytest() assert result.stdout.str().find("skip test summary") == -1 @@ -397,14 +397,14 @@ class TestTerminalFunctional: def test_verbose_reporting(self, testdir, pytestconfig): p1 = testdir.makepyfile(""" - import py + import pytest def test_fail(): raise ValueError() def test_pass(): pass class TestClass: def test_skip(self): - py.test.skip("hello") + pytest.skip("hello") def test_gen(): def check(x): assert x == 1 @@ -562,7 +562,7 @@ class TestGenericReporting: def test_tb_option(self, testdir, option): p = testdir.makepyfile(""" - import py + import pytest def g(): raise IndexError def test_func(): @@ -587,7 +587,7 @@ class TestGenericReporting: def test_tb_crashline(self, testdir, option): p = testdir.makepyfile(""" - import py + import pytest def g(): raise IndexError def test_func1(): @@ -620,7 +620,7 @@ def pytest_report_header(config): "*hello: info*", ]) - at py.test.mark.xfail("not hasattr(os, 'dup')") + at pytest.mark.xfail("not hasattr(os, 'dup')") def test_fdopen_kept_alive_issue124(testdir): testdir.makepyfile(""" import os, sys --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -34,7 +34,7 @@ Running the test looks like this:: $ py.test test_simplefactory.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 test path 1: test_simplefactory.py test_simplefactory.py F @@ -136,7 +136,7 @@ Running this:: $ py.test test_example.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 test path 1: test_example.py test_example.py .........F @@ -151,7 +151,7 @@ Running this:: E assert 9 < 9 test_example.py:7: AssertionError - ==================== 1 failed, 9 passed in 0.04 seconds ==================== + ==================== 1 failed, 9 passed in 0.03 seconds ==================== Note that the ``pytest_generate_tests(metafunc)`` hook is called during the test collection phase which is separate from the actual test running. @@ -174,10 +174,10 @@ If you want to select only the run with $ py.test -v -k 7 test_example.py # or -k test_func[7] =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 -- /home/hpk/venv/0/bin/python test path 1: test_example.py - test_example.py <- test_example.py:6: test_func[7] PASSED + test_example.py:6: test_func[7] PASSED ======================== 9 tests deselected by '7' ========================= ================== 1 passed, 9 deselected in 0.01 seconds ================== --- a/doc/mark.txt +++ b/doc/mark.txt @@ -6,7 +6,7 @@ mark test functions with attributes .. currentmodule:: _pytest.mark -By using the ``py.test.mark`` helper you can instantiate +By using the ``pytest.mark`` helper you can instantiate decorators that will set named meta data on test functions. Marking a single function @@ -14,22 +14,22 @@ Marking a single function You can "mark" a test function with meta data like this:: - import py - @py.test.mark.webtest + import pytest + @pytest.mark.webtest def test_send_http(): ... -This will set the function attribute ``webtest`` to a :py:class:`MarkInfo` +This will set the function attribute ``webtest`` to a :py:class:`MarkInfo` instance. You can also specify parametrized meta data like this:: # content of test_mark.py - import py - @py.test.mark.webtest(firefox=30) + import pytest + @pytest.mark.webtest(firefox=30) def test_receive(): pass - @py.test.mark.webtest("functional", firefox=30) + @pytest.mark.webtest("functional", firefox=30) def test_run_and_look(): pass @@ -43,12 +43,12 @@ and access it from other places like thi Marking whole classes or modules ---------------------------------------------------- -If you are programming with Python2.6 you may use ``py.test.mark`` decorators +If you are programming with Python2.6 you may use ``pytest.mark`` decorators with classes to apply markers to all its test methods:: # content of test_mark_classlevel.py - import py - @py.test.mark.webtest + import pytest + @pytest.mark.webtest class TestClass: def test_startup(self): pass @@ -61,22 +61,22 @@ two test functions. To remain compatible with Python2.5 you can also set a ``pytestmark`` attribute on a TestClass like this:: - import py + import pytest class TestClass: - pytestmark = py.test.mark.webtest + pytestmark = pytest.mark.webtest or if you need to use multiple markers you can use a list:: - import py + import pytest class TestClass: - pytestmark = [py.test.mark.webtest, pytest.mark.slowtest] + pytestmark = [pytest.mark.webtest, pytest.mark.slowtest] You can also set a module level marker:: - import py - pytestmark = py.test.mark.webtest + import pytest + pytestmark = pytest.mark.webtest in which case it will be applied to all functions and methods defined in the module. @@ -88,20 +88,20 @@ You can use the ``-k`` command line opti $ py.test -k webtest # running with the above defined examples yields =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 - test path 1: /tmp/doc-exec-527 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 + test path 1: /tmp/doc-exec-74 test_mark.py .. test_mark_classlevel.py .. - ========================= 4 passed in 0.02 seconds ========================= + ========================= 4 passed in 0.01 seconds ========================= And you can also run all tests except the ones that match the keyword:: $ py.test -k-webtest =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 - test path 1: /tmp/doc-exec-527 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 + test path 1: /tmp/doc-exec-74 ===================== 4 tests deselected by '-webtest' ===================== ======================= 4 deselected in 0.01 seconds ======================= @@ -110,8 +110,8 @@ Or to only select the class:: $ py.test -kTestClass =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 - test path 1: /tmp/doc-exec-527 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 + test path 1: /tmp/doc-exec-74 test_mark_classlevel.py .. --- a/doc/unittest.txt +++ b/doc/unittest.txt @@ -24,7 +24,7 @@ Running it yields:: $ py.test test_unittest.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 test path 1: test_unittest.py test_unittest.py F @@ -56,7 +56,7 @@ Running it yields:: /usr/lib/python2.6/unittest.py:350: AssertionError ----------------------------- Captured stdout ------------------------------ hello - ========================= 1 failed in 0.12 seconds ========================= + ========================= 1 failed in 0.02 seconds ========================= .. _`unittest.py style`: http://docs.python.org/library/unittest.html --- a/testing/test_core.py +++ b/testing/test_core.py @@ -1,4 +1,4 @@ -import py, os +import pytest, py, os from _pytest.core import PluginManager, canonical_importname from _pytest.core import MultiCall, HookRelay, varnames @@ -7,18 +7,18 @@ class TestBootstrapping: def test_consider_env_fails_to_import(self, monkeypatch): pluginmanager = PluginManager() monkeypatch.setenv('PYTEST_PLUGINS', 'nonexisting', prepend=",") - py.test.raises(ImportError, "pluginmanager.consider_env()") + pytest.raises(ImportError, "pluginmanager.consider_env()") def test_preparse_args(self): pluginmanager = PluginManager() - py.test.raises(ImportError, """ + pytest.raises(ImportError, """ pluginmanager.consider_preparse(["xyz", "-p", "hello123"]) """) def test_plugin_skip(self, testdir, monkeypatch): p = testdir.makepyfile(pytest_skipping1=""" - import py - py.test.skip("hello") + import pytest + pytest.skip("hello") """) p.copy(p.dirpath("pytest_skipping2.py")) monkeypatch.setenv("PYTEST_PLUGINS", "skipping2") @@ -73,7 +73,7 @@ class TestBootstrapping: def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): x500 = testdir.makepyfile(pytest_x500="#") p = testdir.makepyfile(""" - import py + import pytest def test_hello(pytestconfig): plugin = pytestconfig.pluginmanager.getplugin('x500') assert plugin is not None @@ -85,8 +85,8 @@ class TestBootstrapping: def test_import_plugin_importname(self, testdir): pluginmanager = PluginManager() - py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') + pytest.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + pytest.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwx.y")') reset = testdir.syspathinsert() pluginname = "pytest_hello" @@ -103,8 +103,8 @@ class TestBootstrapping: def test_import_plugin_dotted_name(self, testdir): pluginmanager = PluginManager() - py.test.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') - py.test.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') + pytest.raises(ImportError, 'pluginmanager.import_plugin("qweqwex.y")') + pytest.raises(ImportError, 'pluginmanager.import_plugin("pytest_qweqwex.y")') reset = testdir.syspathinsert() testdir.mkpydir("pkg").join("plug.py").write("x=3") @@ -148,7 +148,7 @@ class TestBootstrapping: def test_consider_conftest_deps(self, testdir): mod = testdir.makepyfile("pytest_plugins='xyz'").pyimport() pp = PluginManager() - py.test.raises(ImportError, "pp.consider_conftest(mod)") + pytest.raises(ImportError, "pp.consider_conftest(mod)") def test_pm(self): pp = PluginManager() @@ -187,10 +187,10 @@ class TestBootstrapping: assert pp.isregistered(mod) l = pp.getplugins() assert mod in l - py.test.raises(AssertionError, "pp.register(mod)") + pytest.raises(AssertionError, "pp.register(mod)") mod2 = py.std.types.ModuleType("pytest_hello") #pp.register(mod2) # double pm - py.test.raises(AssertionError, "pp.register(mod)") + pytest.raises(AssertionError, "pp.register(mod)") #assert not pp.isregistered(mod2) assert pp.getplugins() == l @@ -208,14 +208,14 @@ class TestBootstrapping: class hello: def pytest_gurgel(self): pass - py.test.raises(Exception, "pp.register(hello())") + pytest.raises(Exception, "pp.register(hello())") def test_register_mismatch_arg(self): pp = PluginManager(load=True) class hello: def pytest_configure(self, asd): pass - excinfo = py.test.raises(Exception, "pp.register(hello())") + excinfo = pytest.raises(Exception, "pp.register(hello())") def test_canonical_importname(self): for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': @@ -223,7 +223,7 @@ class TestBootstrapping: def test_notify_exception(self, capfd): pp = PluginManager() - excinfo = py.test.raises(ValueError, "raise ValueError(1)") + excinfo = pytest.raises(ValueError, "raise ValueError(1)") pp.notify_exception(excinfo) out, err = capfd.readouterr() assert "ValueError" in err @@ -283,7 +283,7 @@ class TestBootstrapping: assert pm.trace.root.indent == indent assert len(l) == 1 assert 'pytest_plugin_registered' in l[0] - py.test.raises(ValueError, lambda: pm.register(api1())) + pytest.raises(ValueError, lambda: pm.register(api1())) assert pm.trace.root.indent == indent assert saveindent[0] > indent @@ -420,8 +420,8 @@ class TestPytestPluginInteractions: def test_namespace_has_default_and_env_plugins(testdir): p = testdir.makepyfile(""" - import py - py.test.mark + import pytest + pytest.mark """) result = testdir.runpython(p) assert result.ret == 0 @@ -493,7 +493,7 @@ class TestMultiCall: def test_tags_call_error(self): multicall = MultiCall([lambda x: x], {}) - py.test.raises(TypeError, "multicall.execute()") + pytest.raises(TypeError, "multicall.execute()") def test_call_subexecute(self): def m(__multicall__): @@ -541,7 +541,7 @@ class TestHookRelay: def hello(self, arg): "api hook 1" mcm = HookRelay(hookspecs=Api, pm=pm, prefix="he") - py.test.raises(TypeError, "mcm.hello(3)") + pytest.raises(TypeError, "mcm.hello(3)") def test_firstresult_definition(self): pm = PluginManager() --- a/doc/skipping.txt +++ b/doc/skipping.txt @@ -12,7 +12,7 @@ requirement without which considering or not make sense. If a test fails under all conditions then it's probably best to mark your test as 'xfail'. -By running ``py.test -rxs`` you will see extra reporting +By running ``py.test -rxs`` you will see extra reporting information on skips and xfail-run tests at the end of a test run. .. _skipif: @@ -23,7 +23,7 @@ Skipping a single function Here is an example for marking a test function to be skipped when run on a Python3 interpreter:: - @py.test.mark.skipif("sys.version_info >= (3,0)") + @pytest.mark.skipif("sys.version_info >= (3,0)") def test_function(): ... @@ -33,14 +33,14 @@ contains the ``sys`` and ``os`` modules ``config`` object. The latter allows you to skip based on a test configuration value e.g. like this:: - @py.test.mark.skipif("not config.getvalue('db')") + @pytest.mark.skipif("not config.getvalue('db')") def test_function(...): ... Create a shortcut for your conditional skip decorator at module level like this:: - win32only = py.test.mark.skipif("sys.platform != 'win32'") + win32only = pytest.mark.skipif("sys.platform != 'win32'") @win32only def test_function(): @@ -55,7 +55,7 @@ As with all function :ref:`marking` you for skipping all methods of a test class based on platform:: class TestPosixCalls: - pytestmark = py.test.mark.skipif("sys.platform == 'win32'") + pytestmark = pytest.mark.skipif("sys.platform == 'win32'") def test_function(self): # will not be setup or run under 'win32' platform @@ -65,7 +65,7 @@ The ``pytestmark`` decorator will be app If your code targets python2.6 or above you can equivalently use the skipif decorator on classes:: - @py.test.mark.skipif("sys.platform == 'win32'") + @pytest.mark.skipif("sys.platform == 'win32'") class TestPosixCalls: def test_function(self): @@ -86,7 +86,7 @@ mark a test function as expected to fail You can use the ``xfail`` marker to indicate that you expect the test to fail:: - @py.test.mark.xfail + @pytest.mark.xfail def test_function(): ... @@ -97,21 +97,21 @@ when it fails. Instead terminal reportin Same as with skipif_ you can also selectively expect a failure depending on platform:: - @py.test.mark.xfail("sys.version_info >= (3,0)") + @pytest.mark.xfail("sys.version_info >= (3,0)") def test_function(): ... To not run a test and still regard it as "xfailed":: - @py.test.mark.xfail(..., run=False) + @pytest.mark.xfail(..., run=False) To specify an explicit reason to be shown with xfailure detail:: - @py.test.mark.xfail(..., reason="my reason") + @pytest.mark.xfail(..., reason="my reason") By specifying on the commandline:: - py.test --runxfail + pytest --runxfail you can force the running and reporting of a runnable ``xfail`` marked test. @@ -124,7 +124,7 @@ within test or setup code. Example:: def test_function(): if not valid_config(): - py.test.xfail("unsuppored configuration") + pytest.xfail("unsuppored configuration") skipping on a missing import dependency @@ -133,13 +133,13 @@ skipping on a missing import dependency You can use the following import helper at module level or within a test or test setup function:: - docutils = py.test.importorskip("docutils") + docutils = pytest.importorskip("docutils") If ``docutils`` cannot be imported here, this will lead to a skip outcome of the test. You can also skip dependeing if if a library does not come with a high enough version:: - docutils = py.test.importorskip("docutils", minversion="0.3") + docutils = pytest.importorskip("docutils", minversion="0.3") The version will be read from the specified module's ``__version__`` attribute. @@ -152,5 +152,5 @@ within test or setup code. Example:: def test_function(): if not valid_config(): - py.test.skip("unsuppored configuration") + pytest.skip("unsuppored configuration") --- a/doc/assert.txt +++ b/doc/assert.txt @@ -21,27 +21,27 @@ assertion fails you will see the value o $ py.test test_assert1.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 test path 1: test_assert1.py - + test_assert1.py F - + ================================= FAILURES ================================= ______________________________ test_function _______________________________ - + def test_function(): > assert f() == 4 E assert 3 == 4 E + where 3 = f() - + test_assert1.py:5: AssertionError - ========================= 1 failed in 0.05 seconds ========================= + ========================= 1 failed in 0.03 seconds ========================= Reporting details about the failing assertion is achieved by re-evaluating the assert expression and recording intermediate values. Note: If evaluating the assert expression has side effects you may get a -warning that the intermediate values could not be determined safely. A +warning that the intermediate values could not be determined safely. A common example for this issue is reading from a file and comparing in one line:: @@ -57,14 +57,14 @@ assertions about expected exceptions ------------------------------------------ In order to write assertions about raised exceptions, you can use -``py.test.raises`` as a context manager like this:: +``pytest.raises`` as a context manager like this:: - with py.test.raises(ZeroDivisionError): + with pytest.raises(ZeroDivisionError): 1 / 0 and if you need to have access to the actual exception info you may use:: - with py.test.raises(RuntimeError) as excinfo: + with pytest.raises(RuntimeError) as excinfo: def f(): f() f() @@ -74,8 +74,8 @@ and if you need to have access to the ac If you want to write test code that works on Python2.4 as well, you may also use two other ways to test for an expected exception:: - py.test.raises(ExpectedException, func, *args, **kwargs) - py.test.raises(ExpectedException, "func(*args, **kwargs)") + pytest.raises(ExpectedException, func, *args, **kwargs) + pytest.raises(ExpectedException, "func(*args, **kwargs)") both of which execute the specified function with args and kwargs and asserts that the given ``ExpectedException`` is raised. The reporter will @@ -101,14 +101,14 @@ if you run this module:: $ py.test test_assert2.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 test path 1: test_assert2.py - + test_assert2.py F - + ================================= FAILURES ================================= ___________________________ test_set_comparison ____________________________ - + def test_set_comparison(): set1 = set("1308") set2 = set("8035") @@ -118,7 +118,7 @@ if you run this module:: E '1' E Extra items in the right set: E '5' - + test_assert2.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= @@ -128,7 +128,7 @@ Special comparisons are done for a numbe * comparing long sequences: first failing indices * comparing dicts: different entries -.. +.. Defining your own comparison ---------------------------------------------- --- a/doc/tmpdir.txt +++ b/doc/tmpdir.txt @@ -28,7 +28,7 @@ Running this would result in a passed te $ py.test test_tmpdir.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 test path 1: test_tmpdir.py test_tmpdir.py F @@ -36,7 +36,7 @@ Running this would result in a passed te ================================= FAILURES ================================= _____________________________ test_create_file _____________________________ - tmpdir = local('/tmp/pytest-447/test_create_file0') + tmpdir = local('/tmp/pytest-123/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") @@ -47,7 +47,7 @@ Running this would result in a passed te E assert 0 test_tmpdir.py:7: AssertionError - ========================= 1 failed in 0.15 seconds ========================= + ========================= 1 failed in 0.02 seconds ========================= .. _`base temporary directory`: --- a/testing/test_assertion.py +++ b/testing/test_assertion.py @@ -1,9 +1,9 @@ import sys -import py +import py, pytest import _pytest.assertion as plugin -needsnewassert = py.test.mark.skipif("sys.version_info < (2,6)") +needsnewassert = pytest.mark.skipif("sys.version_info < (2,6)") def interpret(expr): return py.code._reinterpret(expr, py.code.Frame(sys._getframe(1))) --- a/testing/test_session.py +++ b/testing/test_session.py @@ -45,9 +45,9 @@ class SessionTests: def test_raises_output(self, testdir): reprec = testdir.inline_runsource(""" - import py + import pytest def test_raises_doesnt(): - py.test.raises(ValueError, int, "3") + pytest.raises(ValueError, int, "3") """) passed, skipped, failed = reprec.listoutcomes() assert len(failed) == 1 @@ -118,15 +118,15 @@ class SessionTests: def test_skip_file_by_conftest(self, testdir): testdir.makepyfile(conftest=""" - import py + import pytest def pytest_collect_file(): - py.test.skip("intentional") + pytest.skip("intentional") """, test_file=""" def test_one(): pass """) try: reprec = testdir.inline_run(testdir.tmpdir) - except py.test.skip.Exception: + except pytest.skip.Exception: py.test.fail("wrong skipped caught") reports = reprec.getreports("pytest_collectreport") assert len(reports) == 1 @@ -173,8 +173,8 @@ class TestNewSession(SessionTests): pass """, test_two=""" - import py - py.test.skip('xxx') + import pytest + pytest.skip('xxx') """, test_three="xxxdsadsadsadsa", __init__="" @@ -204,10 +204,10 @@ class TestNewSession(SessionTests): def test_plugin_specify(testdir): testdir.chdir() - config = py.test.raises(ImportError, """ + config = pytest.raises(ImportError, """ testdir.parseconfig("-p", "nqweotexistent") """) - #py.test.raises(ImportError, + #pytest.raises(ImportError, # "config.pluginmanager.do_configure(config)" #) --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -1,4 +1,4 @@ -import py +import pytest import os, sys from _pytest.pytester import LineMatcher, LineComp, HookRecorder from _pytest.core import PluginManager @@ -8,7 +8,7 @@ def test_reportrecorder(testdir): recorder = testdir.getreportrecorder(item.config) assert not recorder.getfailures() - py.test.xfail("internal reportrecorder tests need refactoring") + pytest.xfail("internal reportrecorder tests need refactoring") class rep: excinfo = None passed = False @@ -51,10 +51,11 @@ def test_reportrecorder(testdir): recorder.unregister() recorder.clear() recorder.hook.pytest_runtest_logreport(report=rep) - py.test.raises(ValueError, "recorder.getfailures()") + pytest.raises(ValueError, "recorder.getfailures()") def test_parseconfig(testdir): + import py config1 = testdir.parseconfig() config2 = testdir.parseconfig() assert config2 != config1 @@ -81,7 +82,7 @@ def test_hookrecorder_basic(): call = rec.popcall("pytest_xyz") assert call.arg == 123 assert call._name == "pytest_xyz" - py.test.raises(ValueError, "rec.popcall('abc')") + pytest.raises(ValueError, "rec.popcall('abc')") def test_hookrecorder_basic_no_args_hook(): rec = HookRecorder(PluginManager()) @@ -96,7 +97,7 @@ def test_hookrecorder_basic_no_args_hook def test_functional(testdir, linecomp): reprec = testdir.inline_runsource(""" - import py + import pytest from _pytest.core import HookRelay, PluginManager pytest_plugins="pytester" def test_func(_pytest): --- a/doc/doctest.txt +++ b/doc/doctest.txt @@ -44,7 +44,7 @@ then you can just invoke ``py.test`` wit $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 - test path 1: /tmp/doc-exec-519 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 + test path 1: /tmp/doc-exec-66 ============================= in 0.00 seconds ============================= --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -1,4 +1,4 @@ -import py +import pytest from _pytest.skipping import MarkEvaluator, folded_skips from _pytest.skipping import pytest_runtest_setup @@ -13,8 +13,8 @@ class TestEvaluator: def test_marked_no_args(self, testdir): item = testdir.getitem(""" - import py - @py.test.mark.xyz + import pytest + @pytest.mark.xyz def test_func(): pass """) @@ -27,8 +27,8 @@ class TestEvaluator: def test_marked_one_arg(self, testdir): item = testdir.getitem(""" - import py - @py.test.mark.xyz("hasattr(os, 'sep')") + import pytest + @pytest.mark.xyz("hasattr(os, 'sep')") def test_func(): pass """) @@ -40,8 +40,8 @@ class TestEvaluator: def test_marked_one_arg_with_reason(self, testdir): item = testdir.getitem(""" - import py - @py.test.mark.xyz("hasattr(os, 'sep')", attr=2, reason="hello world") + import pytest + @pytest.mark.xyz("hasattr(os, 'sep')", attr=2, reason="hello world") def test_func(): pass """) @@ -54,12 +54,12 @@ class TestEvaluator: def test_marked_one_arg_twice(self, testdir): lines = [ - '''@py.test.mark.skipif("not hasattr(os, 'murks')")''', - '''@py.test.mark.skipif("hasattr(os, 'murks')")''' + '''@pytest.mark.skipif("not hasattr(os, 'murks')")''', + '''@pytest.mark.skipif("hasattr(os, 'murks')")''' ] for i in range(0, 2): item = testdir.getitem(""" - import py + import pytest %s %s def test_func(): @@ -73,9 +73,9 @@ class TestEvaluator: def test_marked_one_arg_twice2(self, testdir): item = testdir.getitem(""" - import py - @py.test.mark.skipif("hasattr(os, 'murks')") - @py.test.mark.skipif("not hasattr(os, 'murks')") + import pytest + @pytest.mark.skipif("hasattr(os, 'murks')") + @pytest.mark.skipif("not hasattr(os, 'murks')") def test_func(): pass """) @@ -87,9 +87,9 @@ class TestEvaluator: def test_skipif_class(self, testdir): item, = testdir.getitems(""" - import py + import pytest class TestClass: - pytestmark = py.test.mark.skipif("config._hackxyz") + pytestmark = pytest.mark.skipif("config._hackxyz") def test_func(self): pass """) @@ -103,8 +103,8 @@ class TestEvaluator: class TestXFail: def test_xfail_simple(self, testdir): item = testdir.getitem(""" - import py - @py.test.mark.xfail + import pytest + @pytest.mark.xfail def test_func(): assert 0 """) @@ -117,8 +117,8 @@ class TestXFail: def test_xfail_xpassed(self, testdir): item = testdir.getitem(""" - import py - @py.test.mark.xfail + import pytest + @pytest.mark.xfail def test_func(): assert 1 """) @@ -131,8 +131,8 @@ class TestXFail: def test_xfail_run_anyway(self, testdir): testdir.makepyfile(""" - import py - @py.test.mark.xfail + import pytest + @pytest.mark.xfail def test_func(): assert 0 """) @@ -146,8 +146,8 @@ class TestXFail: def test_xfail_evalfalse_but_fails(self, testdir): item = testdir.getitem(""" - import py - @py.test.mark.xfail('False') + import pytest + @pytest.mark.xfail('False') def test_func(): assert 0 """) @@ -158,8 +158,8 @@ class TestXFail: def test_xfail_not_report_default(self, testdir): p = testdir.makepyfile(test_one=""" - import py - @py.test.mark.xfail + import pytest + @pytest.mark.xfail def test_this(): assert 0 """) @@ -170,14 +170,14 @@ class TestXFail: def test_xfail_not_run_xfail_reporting(self, testdir): p = testdir.makepyfile(test_one=""" - import py - @py.test.mark.xfail(run=False, reason="noway") + import pytest + @pytest.mark.xfail(run=False, reason="noway") def test_this(): assert 0 - @py.test.mark.xfail("True", run=False) + @pytest.mark.xfail("True", run=False) def test_this_true(): assert 0 - @py.test.mark.xfail("False", run=False, reason="huh") + @pytest.mark.xfail("False", run=False, reason="huh") def test_this_false(): assert 1 """) @@ -192,8 +192,8 @@ class TestXFail: def test_xfail_not_run_no_setup_run(self, testdir): p = testdir.makepyfile(test_one=""" - import py - @py.test.mark.xfail(run=False, reason="hello") + import pytest + @pytest.mark.xfail(run=False, reason="hello") def test_this(): assert 0 def setup_module(mod): @@ -208,8 +208,8 @@ class TestXFail: def test_xfail_xpass(self, testdir): p = testdir.makepyfile(test_one=""" - import py - @py.test.mark.xfail + import pytest + @pytest.mark.xfail def test_that(): assert 1 """) @@ -222,9 +222,9 @@ class TestXFail: def test_xfail_imperative(self, testdir): p = testdir.makepyfile(""" - import py + import pytest def test_this(): - py.test.xfail("hello") + pytest.xfail("hello") """) result = testdir.runpytest(p) result.stdout.fnmatch_lines([ @@ -238,14 +238,14 @@ class TestXFail: result = testdir.runpytest(p, "--runxfail") result.stdout.fnmatch_lines([ "*def test_this():*", - "*py.test.xfail*", + "*pytest.xfail*", ]) def test_xfail_imperative_in_setup_function(self, testdir): p = testdir.makepyfile(""" - import py + import pytest def setup_function(function): - py.test.xfail("hello") + pytest.xfail("hello") def test_this(): assert 0 @@ -262,14 +262,14 @@ class TestXFail: result = testdir.runpytest(p, "--runxfail") result.stdout.fnmatch_lines([ "*def setup_function(function):*", - "*py.test.xfail*", + "*pytest.xfail*", ]) def xtest_dynamic_xfail_set_during_setup(self, testdir): p = testdir.makepyfile(""" - import py + import pytest def setup_function(function): - py.test.mark.xfail(function) + pytest.mark.xfail(function) def test_this(): assert 0 def test_that(): @@ -283,9 +283,9 @@ class TestXFail: def test_dynamic_xfail_no_run(self, testdir): p = testdir.makepyfile(""" - import py + import pytest def pytest_funcarg__arg(request): - request.applymarker(py.test.mark.xfail(run=False)) + request.applymarker(pytest.mark.xfail(run=False)) def test_this(arg): assert 0 """) @@ -297,9 +297,9 @@ class TestXFail: def test_dynamic_xfail_set_during_funcarg_setup(self, testdir): p = testdir.makepyfile(""" - import py + import pytest def pytest_funcarg__arg(request): - request.applymarker(py.test.mark.xfail) + request.applymarker(pytest.mark.xfail) def test_this2(arg): assert 0 """) @@ -312,19 +312,19 @@ class TestXFail: class TestSkipif: def test_skipif_conditional(self, testdir): item = testdir.getitem(""" - import py - @py.test.mark.skipif("hasattr(os, 'sep')") + import pytest + @pytest.mark.skipif("hasattr(os, 'sep')") def test_func(): pass """) - x = py.test.raises(py.test.skip.Exception, "pytest_runtest_setup(item)") + x = pytest.raises(pytest.skip.Exception, "pytest_runtest_setup(item)") assert x.value.msg == "condition: hasattr(os, 'sep')" def test_skipif_reporting(self, testdir): p = testdir.makepyfile(""" - import py - @py.test.mark.skipif("hasattr(sys, 'platform')") + import pytest + @pytest.mark.skipif("hasattr(sys, 'platform')") def test_that(): assert 0 """) @@ -337,9 +337,9 @@ class TestSkipif: def test_skip_not_report_default(testdir): p = testdir.makepyfile(test_one=""" - import py + import pytest def test_this(): - py.test.skip("hello") + pytest.skip("hello") """) result = testdir.runpytest(p, '-v') result.stdout.fnmatch_lines([ @@ -350,10 +350,10 @@ def test_skip_not_report_default(testdir def test_skipif_class(testdir): p = testdir.makepyfile(""" - import py + import pytest class TestClass: - pytestmark = py.test.mark.skipif("True") + pytestmark = pytest.mark.skipif("True") def test_that(self): assert 0 def test_though(self): @@ -407,9 +407,9 @@ def test_skipped_reasons_functional(test doskip() """, conftest = """ - import py + import pytest def doskip(): - py.test.skip('test') + pytest.skip('test') """ ) result = testdir.runpytest('--report=skipped') @@ -422,17 +422,17 @@ def test_skipped_reasons_functional(test def test_reportchars(testdir): testdir.makepyfile(""" - import py + import pytest def test_1(): assert 0 - @py.test.mark.xfail + @pytest.mark.xfail def test_2(): assert 0 - @py.test.mark.xfail + @pytest.mark.xfail def test_3(): pass def test_4(): - py.test.skip("four") + pytest.skip("four") """) result = testdir.runpytest("-rfxXs") result.stdout.fnmatch_lines([ --- a/testing/test_junitxml.py +++ b/testing/test_junitxml.py @@ -20,17 +20,17 @@ def assert_attr(node, **kwargs): class TestPython: def test_summing_simple(self, testdir): testdir.makepyfile(""" - import py + import pytest def test_pass(): pass def test_fail(): assert 0 def test_skip(): - py.test.skip("") - @py.test.mark.xfail + pytest.skip("") + @pytest.mark.xfail def test_xfail(): assert 0 - @py.test.mark.xfail + @pytest.mark.xfail def test_xpass(): assert 1 """) @@ -157,9 +157,9 @@ class TestPython: def test_xfailure_function(self, testdir): testdir.makepyfile(""" - import py + import pytest def test_xfail(): - py.test.xfail("42") + pytest.xfail("42") """) result, dom = runandparse(testdir) assert not result.ret @@ -175,8 +175,8 @@ class TestPython: def test_xfailure_xpass(self, testdir): testdir.makepyfile(""" - import py - @py.test.mark.xfail + import pytest + @pytest.mark.xfail def test_xpass(): pass """) @@ -207,7 +207,7 @@ class TestPython: assert "SyntaxError" in fnode.toxml() def test_collect_skipped(self, testdir): - testdir.makepyfile("import py ; py.test.skip('xyz')") + testdir.makepyfile("import pytest; pytest.skip('xyz')") result, dom = runandparse(testdir) assert not result.ret node = dom.getElementsByTagName("testsuite")[0] --- a/testing/test_resultlog.py +++ b/testing/test_resultlog.py @@ -1,4 +1,4 @@ -import py +import py, pytest import os from _pytest.resultlog import generic_path, ResultLog, \ pytest_configure, pytest_unconfigure @@ -79,7 +79,8 @@ class TestWithFunctionIntegration: def test_collection_report(self, testdir): ok = testdir.makepyfile(test_collection_ok="") - skip = testdir.makepyfile(test_collection_skip="import py ; py.test.skip('hello')") + skip = testdir.makepyfile(test_collection_skip= + "import pytest ; pytest.skip('hello')") fail = testdir.makepyfile(test_collection_fail="XXX") lines = self.getresultlog(testdir, ok) assert not lines @@ -101,14 +102,14 @@ class TestWithFunctionIntegration: def test_log_test_outcomes(self, testdir): mod = testdir.makepyfile(test_mod=""" - import py + import pytest def test_pass(): pass - def test_skip(): py.test.skip("hello") + def test_skip(): pytest.skip("hello") def test_fail(): raise ValueError("FAIL") - @py.test.mark.xfail + @pytest.mark.xfail def test_xfail(): raise ValueError("XFAIL") - @py.test.mark.xfail + @pytest.mark.xfail def test_xpass(): pass """) @@ -152,17 +153,17 @@ class TestWithFunctionIntegration: def test_generic(testdir, LineMatcher): testdir.plugins.append("resultlog") testdir.makepyfile(""" - import py + import pytest def test_pass(): pass def test_fail(): assert 0 def test_skip(): - py.test.skip("") - @py.test.mark.xfail + pytest.skip("") + @pytest.mark.xfail def test_xfail(): assert 0 - @py.test.mark.xfail(run=False) + @pytest.mark.xfail(run=False) def test_xfail_norun(): assert 0 """) --- a/doc/example/mysetup.txt +++ b/doc/example/mysetup.txt @@ -49,7 +49,7 @@ You can now run the test:: $ py.test test_sample.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 test path 1: test_sample.py test_sample.py F @@ -57,7 +57,7 @@ You can now run the test:: ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - mysetup = + mysetup = def test_answer(mysetup): app = mysetup.myapp() @@ -84,7 +84,7 @@ the previous example to add a command li and to offer a new mysetup method:: # content of ./conftest.py - import py + import pytest from myapp import MyApp def pytest_funcarg__mysetup(request): # "mysetup" factory function @@ -105,7 +105,7 @@ and to offer a new mysetup method:: def getsshconnection(self): host = self.config.option.ssh if host is None: - py.test.skip("specify ssh host with --ssh") + pytest.skip("specify ssh host with --ssh") return execnet.SshGateway(host) @@ -122,14 +122,14 @@ Running it yields:: $ py.test test_ssh.py -rs =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 test path 1: test_ssh.py test_ssh.py s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-560/conftest.py:22: specify ssh host with --ssh + SKIP [1] /tmp/doc-exec-107/conftest.py:22: specify ssh host with --ssh - ======================== 1 skipped in 0.03 seconds ========================= + ======================== 1 skipped in 0.02 seconds ========================= If you specify a command line option like ``py.test --ssh=python.org`` the test will execute as expected. --- a/testing/test_conftest.py +++ b/testing/test_conftest.py @@ -1,4 +1,4 @@ -import py +import py, pytest from _pytest.config import Conftest def pytest_generate_tests(metafunc): @@ -56,8 +56,8 @@ class TestConftestValueAccessGlobal: def test_value_access_not_existing(self, basedir): conftest = ConftestWithSetinitial(basedir) - py.test.raises(KeyError, "conftest.rget('a')") - #py.test.raises(KeyError, "conftest.lget('a')") + pytest.raises(KeyError, "conftest.rget('a')") + #pytest.raises(KeyError, "conftest.lget('a')") def test_value_access_by_path(self, basedir): conftest = ConftestWithSetinitial(basedir) @@ -66,7 +66,7 @@ class TestConftestValueAccessGlobal: assert conftest.rget("a", basedir.join('adir', 'b')) == 1.5 #assert conftest.lget("a", basedir.join('adir', 'b')) == 1 #assert conftest.lget("b", basedir.join('adir', 'b')) == 2 - #assert py.test.raises(KeyError, + #assert pytest.raises(KeyError, # 'conftest.lget("b", basedir.join("a"))' #) @@ -109,7 +109,7 @@ def test_doubledash_not_considered(testd def test_conftest_global_import(testdir): testdir.makeconftest("x=3") p = testdir.makepyfile(""" - import py + import py, pytest from _pytest.config import Conftest conf = Conftest() mod = conf.importconftest(py.path.local("conftest.py")) @@ -185,7 +185,7 @@ def test_conftest_samecontent_detection( assert len(l) == 1 assert l[0].__file__ == p.join("conftest.py") - at py.test.mark.multi(name='test tests whatever .dotdir'.split()) + at pytest.mark.multi(name='test tests whatever .dotdir'.split()) def test_setinitial_conftest_subdirs(testdir, name): sub = testdir.mkdir(name) subconftest = sub.ensure("conftest.py") --- a/testing/test_parseopt.py +++ b/testing/test_parseopt.py @@ -1,11 +1,11 @@ -import py +import py, pytest from _pytest import config as parseopt from textwrap import dedent class TestParser: def test_no_help_by_default(self, capsys): parser = parseopt.Parser(usage="xyz") - py.test.raises(SystemExit, 'parser.parse(["-h"])') + pytest.raises(SystemExit, 'parser.parse(["-h"])') out, err = capsys.readouterr() assert err.find("no such option") != -1 @@ -41,7 +41,7 @@ class TestParser: def test_group_shortopt_lowercase(self): parser = parseopt.Parser() group = parser.getgroup("hello") - py.test.raises(ValueError, """ + pytest.raises(ValueError, """ group.addoption("-x", action="store_true") """) assert len(group.options) == 0 @@ -102,7 +102,7 @@ class TestParser: assert option.this == 42 - at py.test.mark.skipif("sys.version_info < (2,5)") + at pytest.mark.skipif("sys.version_info < (2,5)") def test_addoption_parser_epilog(testdir): testdir.makeconftest(""" def pytest_addoption(parser): --- a/testing/test_runner_xunit.py +++ b/testing/test_runner_xunit.py @@ -152,9 +152,9 @@ def test_failing_setup_calls_teardown(te def test_setup_that_skips_calledagain_and_teardown(testdir): p = testdir.makepyfile(""" - import py + import pytest def setup_module(mod): - py.test.skip("x") + pytest.skip("x") def test_function1(): pass def test_function2(): @@ -170,7 +170,7 @@ def test_setup_that_skips_calledagain_an def test_setup_fails_again_on_all_tests(testdir): p = testdir.makepyfile(""" - import py + import pytest def setup_module(mod): raise ValueError(42) def test_function1(): @@ -188,7 +188,7 @@ def test_setup_fails_again_on_all_tests( def test_setup_funcarg_setup_not_called_if_outer_scope_fails(testdir): p = testdir.makepyfile(""" - import py + import pytest def setup_module(mod): raise ValueError(42) def pytest_funcarg__hello(request): --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -1,4 +1,4 @@ -import py, sys +import pytest, py, sys from _pytest import runner from py._code.code import ReprExceptionInfo @@ -41,8 +41,8 @@ class TestSetupState: def test_func(): pass """) ss = runner.SetupState() - py.test.raises(ValueError, "ss.prepare(item)") - py.test.raises(ValueError, "ss.prepare(item)") + pytest.raises(ValueError, "ss.prepare(item)") + pytest.raises(ValueError, "ss.prepare(item)") class BaseFunctionalTests: def test_passfunction(self, testdir): @@ -71,9 +71,9 @@ class BaseFunctionalTests: def test_skipfunction(self, testdir): reports = testdir.runitem(""" - import py + import pytest def test_func(): - py.test.skip("hello") + pytest.skip("hello") """) rep = reports[1] assert not rep.failed @@ -89,9 +89,9 @@ class BaseFunctionalTests: def test_skip_in_setup_function(self, testdir): reports = testdir.runitem(""" - import py + import pytest def setup_function(func): - py.test.skip("hello") + pytest.skip("hello") def test_func(): pass """) @@ -108,7 +108,7 @@ class BaseFunctionalTests: def test_failure_in_setup_function(self, testdir): reports = testdir.runitem(""" - import py + import pytest def setup_function(func): raise ValueError(42) def test_func(): @@ -123,7 +123,7 @@ class BaseFunctionalTests: def test_failure_in_teardown_function(self, testdir): reports = testdir.runitem(""" - import py + import pytest def teardown_function(func): raise ValueError(42) def test_func(): @@ -147,7 +147,7 @@ class BaseFunctionalTests: return "hello" """) reports = testdir.runitem(""" - import py + import pytest def test_func(): assert 0 """) @@ -226,7 +226,7 @@ class TestExecutionNonForked(BaseFunctio py.test.fail("did not raise") class TestExecutionForked(BaseFunctionalTests): - pytestmark = py.test.mark.skipif("not hasattr(os, 'fork')") + pytestmark = pytest.mark.skipif("not hasattr(os, 'fork')") def getrunner(self): # XXX re-arrange this test to live in pytest-xdist @@ -289,7 +289,7 @@ def test_callinfo(): # design question: do we want general hooks in python files? # then something like the following functional tests makes sense - at py.test.mark.xfail + at pytest.mark.xfail def test_runtest_in_module_ordering(testdir): p1 = testdir.makepyfile(""" def pytest_runtest_setup(item): # runs after class-level! @@ -336,8 +336,8 @@ def test_pytest_fail(): def test_exception_printing_skip(): try: - py.test.skip("hello") - except py.test.skip.Exception: + pytest.skip("hello") + except pytest.skip.Exception: excinfo = py.code.ExceptionInfo() s = excinfo.exconly(tryshort=True) assert s.startswith("Skipped") @@ -351,20 +351,20 @@ def test_importorskip(): assert sys == py.std.sys #path = py.test.importorskip("os.path") #assert path == py.std.os.path - excinfo = py.test.raises(py.test.skip.Exception, f) + excinfo = pytest.raises(pytest.skip.Exception, f) path = py.path.local(excinfo.getrepr().reprcrash.path) # check that importorskip reports the actual call # in this test the test_runner.py file assert path.purebasename == "test_runner" - py.test.raises(SyntaxError, "py.test.importorskip('x y z')") - py.test.raises(SyntaxError, "py.test.importorskip('x=y')") + pytest.raises(SyntaxError, "py.test.importorskip('x y z')") + pytest.raises(SyntaxError, "py.test.importorskip('x=y')") path = importorskip("py", minversion=".".join(py.__version__)) mod = py.std.types.ModuleType("hello123") mod.__version__ = "1.3" - py.test.raises(py.test.skip.Exception, """ + pytest.raises(pytest.skip.Exception, """ py.test.importorskip("hello123", minversion="5.0") """) - except py.test.skip.Exception: + except pytest.skip.Exception: print(py.code.ExceptionInfo()) py.test.fail("spurious skip") --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -1,4 +1,4 @@ -import py +import pytest def test_simple_unittest(testdir): testpath = testdir.makepyfile(""" @@ -73,8 +73,8 @@ def test_teardown(testdir): def test_module_level_pytestmark(testdir): testpath = testdir.makepyfile(""" import unittest - import py - pytestmark = py.test.mark.xfail + import pytest + pytestmark = pytest.mark.xfail class MyTestCase(unittest.TestCase): def test_func1(self): assert 0 @@ -85,7 +85,7 @@ def test_module_level_pytestmark(testdir def test_class_setup(testdir): testpath = testdir.makepyfile(""" import unittest - import py + import pytest class MyTestCase(unittest.TestCase): x = 0 @classmethod --- a/doc/monkeypatch.txt +++ b/doc/monkeypatch.txt @@ -39,8 +39,8 @@ will be undone. .. background check: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 - test path 1: /tmp/doc-exec-528 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 + test path 1: /tmp/doc-exec-75 ============================= in 0.00 seconds ============================= --- a/doc/faq.txt +++ b/doc/faq.txt @@ -65,9 +65,9 @@ Is using funcarg- versus xUnit setup a s +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ For simple applications and for people experienced with nose_ or -unittest-style test setup using `xUnit style setup`_ +unittest-style test setup using `xUnit style setup`_ often feels natural. For larger test suites, parametrized testing -or setup of complex test resources using funcargs_ is recommended. +or setup of complex test resources using funcargs_ may feel more natural. Moreover, funcargs are ideal for writing advanced test support code (like e.g. the monkeypatch_, the tmpdir_ or capture_ funcargs) because the support code can register setup/teardown functions @@ -82,18 +82,17 @@ in a managed class/module/function scope Why the ``pytest_funcarg__*`` name for funcarg factories? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ -When experimenting with funcargs an explicit registration mechanism -was considered. But lacking a good use case for this indirection and -flexibility we decided to go for `Convention over Configuration`_ and -allow to directly specify the factory. Besides removing the need -for an indirection it allows to "grep" for ``pytest_funcarg__MYARG`` -and will safely find all factory functions for the ``MYARG`` function -argument. It helps to alleviate the de-coupling of function -argument usage and creation. +We alternatively implemented an explicit registration mechanism for +function argument factories. But lacking a good use case for this +indirection and flexibility we decided to go for `Convention over +Configuration`_ and rather have factories specified by convention. +Besides removing the need for an registration indirection it allows to +"grep" for ``pytest_funcarg__MYARG`` and will safely find all factory +functions for the ``MYARG`` function argument. .. _`Convention over Configuration`: http://en.wikipedia.org/wiki/Convention_over_Configuration -Can I yield multiple values from a factory function? +Can I yield multiple values from a funcarg factory function? ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ There are two conceptual reasons why yielding from a factory function @@ -126,24 +125,16 @@ by pickling and thus implicitely re-impo Unfortuantely, setuptools-0.6.11 does not ``if __name__=='__main__'`` protect its generated command line script. This leads to infinite recursion when running a test that instantiates Processes. -There are these workarounds: -* `install Distribute`_ as a drop-in replacement for setuptools - and install py.test - -* `directly use a checkout`_ which avoids all setuptools/Distribute - installation - -If those options are not available to you, you may also manually +A good solution is to `install Distribute`_ as a drop-in replacement +for setuptools and then re-install ``pytest``. Otherwise you could fix the script that is created by setuptools by inserting an ``if __name__ == '__main__'``. Or you can create a "pytest.py" script with this content and invoke that with the python version:: - import py + import pytest if __name__ == '__main__': - py.cmdline.pytest() - -.. _`directly use a checkout`: install.html#directly-use-a-checkout + pytest.main() .. _`install distribute`: http://pypi.python.org/pypi/distribute#installation-instructions --- a/testing/test_config.py +++ b/testing/test_config.py @@ -1,4 +1,4 @@ -import py +import py, pytest from _pytest.config import getcfg, Config @@ -40,7 +40,7 @@ class TestParseIni: "*tox.ini:2*requires*9.0*actual*" ]) - @py.test.mark.multi(name="setup.cfg tox.ini pytest.ini".split()) + @pytest.mark.multi(name="setup.cfg tox.ini pytest.ini".split()) def test_ini_names(self, testdir, name): testdir.tmpdir.join(name).write(py.std.textwrap.dedent(""" [pytest] @@ -64,7 +64,7 @@ class TestParseIni: config.parse([sub]) assert config.getini("minversion") == "2.0" - @py.test.mark.xfail(reason="probably not needed") + @pytest.mark.xfail(reason="probably not needed") def test_confcutdir(self, testdir): sub = testdir.mkdir("sub") sub.chdir() @@ -78,7 +78,7 @@ class TestParseIni: class TestConfigCmdlineParsing: def test_parsing_again_fails(self, testdir): config = testdir.reparseconfig([testdir.tmpdir]) - py.test.raises(AssertionError, "config.parse([])") + pytest.raises(AssertionError, "config.parse([])") class TestConfigTmpdir: @@ -125,21 +125,21 @@ class TestConfigAPI: o = testdir.tmpdir assert config.getvalue("x") == 1 assert config.getvalue("x", o.join('sub')) == 2 - py.test.raises(KeyError, "config.getvalue('y')") + pytest.raises(KeyError, "config.getvalue('y')") config = testdir.reparseconfig([str(o.join('sub'))]) assert config.getvalue("x") == 2 assert config.getvalue("y") == 3 assert config.getvalue("x", o) == 1 - py.test.raises(KeyError, 'config.getvalue("y", o)') + pytest.raises(KeyError, 'config.getvalue("y", o)') def test_config_getvalueorskip(self, testdir): config = testdir.parseconfig() - py.test.raises(py.test.skip.Exception, + pytest.raises(pytest.skip.Exception, "config.getvalueorskip('hello')") verbose = config.getvalueorskip("verbose") assert verbose == config.option.verbose config.option.hello = None - py.test.raises(py.test.skip.Exception, + pytest.raises(pytest.skip.Exception, "config.getvalueorskip('hello')") def test_config_overwrite(self, testdir): @@ -176,7 +176,7 @@ class TestConfigAPI: config = testdir.parseconfig() val = config.getini("myname") assert val == "hello" - py.test.raises(ValueError, config.getini, 'other') + pytest.raises(ValueError, config.getini, 'other') def test_addini_pathlist(self, testdir): testdir.makeconftest(""" @@ -193,7 +193,7 @@ class TestConfigAPI: assert len(l) == 2 assert l[0] == p.dirpath('hello') assert l[1] == p.dirpath('world/sub.py') - py.test.raises(ValueError, config.getini, 'other') + pytest.raises(ValueError, config.getini, 'other') def test_addini_args(self, testdir): testdir.makeconftest(""" --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -14,7 +14,7 @@ Installation options:: To check your installation has installed the correct version:: $ py.test --version - This is py.test version 2.0.0.dev22, imported from /home/hpk/p/pytest/pytest + This is py.test version 2.0.0.dev30, imported from /home/hpk/p/pytest/pytest.py If you get an error checkout :ref:`installation issues`. @@ -34,8 +34,8 @@ That's it. You can execute the test func $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 - test path 1: /tmp/doc-exec-523 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 + test path 1: /tmp/doc-exec-70 test_sample.py F @@ -79,19 +79,19 @@ If you want to assert some code raises a use the ``raises`` helper:: # content of test_sysexit.py - import py + import pytest def f(): raise SystemExit(1) def test_mytest(): - with py.test.raises(SystemExit): + with pytest.raises(SystemExit): f() Running it with, this time in "quiet" reporting mode:: $ py.test -q test_sysexit.py . - 1 passed in 0.01 seconds + 1 passed in 0.00 seconds .. todo:: For further ways to assert exceptions see the `raises` @@ -121,7 +121,7 @@ run the module by passing its filename:: ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - self = + self = def test_two(self): x = "hello" @@ -129,7 +129,7 @@ run the module by passing its filename:: E assert hasattr('hello', 'check') test_class.py:8: AssertionError - 1 failed, 1 passed in 0.03 seconds + 1 failed, 1 passed in 0.02 seconds The first test passed, the second failed. Again we can easily see the intermediate values used in the assertion, helping us to @@ -149,7 +149,7 @@ arbitrary resources, for example a uniqu assert 0 We list the name ``tmpdir`` in the test function signature and -py.test will lookup and call a factory to create the resource +py.test will lookup and call a factory to create the resource before performing the test function call. Let's just run it:: $ py.test -q test_tmpdir.py @@ -157,7 +157,7 @@ before performing the test function call ================================= FAILURES ================================= _____________________________ test_needsfiles ______________________________ - tmpdir = local('/tmp/pytest-446/test_needsfiles0') + tmpdir = local('/tmp/pytest-122/test_needsfiles0') def test_needsfiles(tmpdir): print tmpdir @@ -166,8 +166,8 @@ before performing the test function call test_tmpdir.py:3: AssertionError ----------------------------- Captured stdout ------------------------------ - /tmp/pytest-446/test_needsfiles0 - 1 failed in 0.07 seconds + /tmp/pytest-122/test_needsfiles0 + 1 failed in 0.05 seconds Before the test runs, a unique-per-test-invocation temporary directory was created. More info at :ref:`tmpdir handling`. --- a/testing/test_monkeypatch.py +++ b/testing/test_monkeypatch.py @@ -1,12 +1,12 @@ import os, sys -import py +import pytest from _pytest.monkeypatch import monkeypatch as MonkeyPatch def test_setattr(): class A: x = 1 monkeypatch = MonkeyPatch() - py.test.raises(AttributeError, "monkeypatch.setattr(A, 'notexists', 2)") + pytest.raises(AttributeError, "monkeypatch.setattr(A, 'notexists', 2)") monkeypatch.setattr(A, 'y', 2, raising=False) assert A.y == 2 monkeypatch.undo() @@ -35,7 +35,7 @@ def test_delattr(): monkeypatch = MonkeyPatch() monkeypatch.delattr(A, 'x') - py.test.raises(AttributeError, "monkeypatch.delattr(A, 'y')") + pytest.raises(AttributeError, "monkeypatch.delattr(A, 'y')") monkeypatch.delattr(A, 'y', raising=False) monkeypatch.setattr(A, 'x', 5, raising=False) assert A.x == 5 @@ -65,7 +65,7 @@ def test_delitem(): monkeypatch.delitem(d, 'x') assert 'x' not in d monkeypatch.delitem(d, 'y', raising=False) - py.test.raises(KeyError, "monkeypatch.delitem(d, 'y')") + pytest.raises(KeyError, "monkeypatch.delitem(d, 'y')") assert not d monkeypatch.setitem(d, 'y', 1700) assert d['y'] == 1700 @@ -87,7 +87,7 @@ def test_delenv(): name = 'xyz1234' assert name not in os.environ monkeypatch = MonkeyPatch() - py.test.raises(KeyError, "monkeypatch.delenv(%r, raising=True)" % name) + pytest.raises(KeyError, "monkeypatch.delenv(%r, raising=True)" % name) monkeypatch.delenv(name, raising=False) monkeypatch.undo() os.environ[name] = "1" --- a/doc/usage.txt +++ b/doc/usage.txt @@ -10,7 +10,7 @@ Usage and Invocations calling pytest through ``python -m pytest`` ----------------------------------------------------- -.. versionadded: 2.0 +.. versionadded:: 2.0 If you use Python-2.5 or above you can invoke testing through the Python interpreter from the command line:: @@ -88,9 +88,10 @@ Setting a breakpoint / aka ``set_trace() If you want to set a breakpoint and enter the ``pdb.set_trace()`` you can use a helper:: + import pytest def test_function(): ... - py.test.set_trace() # invoke PDB debugger and tracing + pytest.set_trace() # invoke PDB debugger and tracing .. versionadded: 2.0.0 @@ -140,7 +141,7 @@ Currently only pasting to the http://pas calling pytest from Python code ---------------------------------------------------- -.. versionadded: 2.0 +.. versionadded:: 2.0 You can invoke ``py.test`` from Python code directly:: --- a/testing/test_nose.py +++ b/testing/test_nose.py @@ -1,4 +1,4 @@ -import py +import py, pytest def setup_module(mod): mod.nose = py.test.importorskip("nose") --- a/testing/conftest.py +++ b/testing/conftest.py @@ -1,4 +1,4 @@ -import py +import pytest import sys pytest_plugins = "pytester", @@ -91,5 +91,5 @@ def pytest_funcarg__anypython(request): executable = py.path.local(executable) if executable.check(): return executable - py.test.skip("no %s found" % (name,)) + pytest.skip("no %s found" % (name,)) return executable --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -1,4 +1,4 @@ -import py +import py, pytest from _pytest.tmpdir import pytest_funcarg__tmpdir from _pytest.python import FuncargRequest --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -1,4 +1,4 @@ -import py +import py, pytest import sys class TestPDB: @@ -23,8 +23,8 @@ class TestPDB: def test_pdb_on_xfail(self, testdir, pdblist): rep = testdir.inline_runsource1('--pdb', """ - import py - @py.test.mark.xfail + import pytest + @pytest.mark.xfail def test_func(): assert 0 """) @@ -33,16 +33,16 @@ class TestPDB: def test_pdb_on_skip(self, testdir, pdblist): rep = testdir.inline_runsource1('--pdb', """ - import py + import pytest def test_func(): - py.test.skip("hello") + pytest.skip("hello") """) assert rep.skipped assert len(pdblist) == 0 def test_pdb_on_BdbQuit(self, testdir, pdblist): rep = testdir.inline_runsource1('--pdb', """ - import py, bdb + import bdb def test_func(): raise bdb.BdbQuit """) @@ -68,15 +68,15 @@ class TestPDB: def test_pdb_interaction_exception(self, testdir): p1 = testdir.makepyfile(""" - import py + import pytest def globalfunc(): pass def test_1(): - py.test.raises(ValueError, globalfunc) + pytest.raises(ValueError, globalfunc) """) child = testdir.spawn_pytest("--pdb %s" % p1) child.expect(".*def test_1") - child.expect(".*py.test.raises.*globalfunc") + child.expect(".*pytest.raises.*globalfunc") child.expect("(Pdb)") child.sendline("globalfunc") child.expect(".*function") @@ -87,11 +87,11 @@ class TestPDB: def test_pdb_interaction_capturing_simple(self, testdir): p1 = testdir.makepyfile(""" - import py + import pytest def test_1(): i = 0 print ("hello17") - py.test.set_trace() + pytest.set_trace() x = 3 """) child = testdir.spawn_pytest(str(p1)) @@ -108,14 +108,14 @@ class TestPDB: def test_pdb_interaction_capturing_twice(self, testdir): p1 = testdir.makepyfile(""" - import py + import pytest def test_1(): i = 0 print ("hello17") - py.test.set_trace() + pytest.set_trace() x = 3 print ("hello18") - py.test.set_trace() + pytest.set_trace() x = 4 """) child = testdir.spawn_pytest(str(p1)) @@ -135,8 +135,8 @@ class TestPDB: def test_pdb_used_outside_test(self, testdir): p1 = testdir.makepyfile(""" - import py - py.test.set_trace() + import pytest + pytest.set_trace() x = 5 """) child = testdir.spawn("%s %s" %(sys.executable, p1)) --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -1,10 +1,10 @@ -import py +import py, pytest from _pytest.mark import MarkGenerator as Mark class TestMark: def test_pytest_mark_notcallable(self): mark = Mark() - py.test.raises((AttributeError, TypeError), "mark()") + pytest.raises((AttributeError, TypeError), "mark()") def test_pytest_mark_bare(self): mark = Mark() @@ -47,8 +47,8 @@ class TestMark: class TestFunctional: def test_mark_per_function(self, testdir): p = testdir.makepyfile(""" - import py - @py.test.mark.hello + import pytest + @pytest.mark.hello def test_hello(): assert hasattr(test_hello, 'hello') """) @@ -57,8 +57,8 @@ class TestFunctional: def test_mark_per_module(self, testdir): item = testdir.getitem(""" - import py - pytestmark = py.test.mark.hello + import pytest + pytestmark = pytest.mark.hello def test_func(): pass """) @@ -67,9 +67,9 @@ class TestFunctional: def test_marklist_per_class(self, testdir): item = testdir.getitem(""" - import py + import pytest class TestClass: - pytestmark = [py.test.mark.hello, py.test.mark.world] + pytestmark = [pytest.mark.hello, pytest.mark.world] def test_func(self): assert TestClass.test_func.hello assert TestClass.test_func.world @@ -79,8 +79,8 @@ class TestFunctional: def test_marklist_per_module(self, testdir): item = testdir.getitem(""" - import py - pytestmark = [py.test.mark.hello, py.test.mark.world] + import pytest + pytestmark = [pytest.mark.hello, pytest.mark.world] class TestClass: def test_func(self): assert TestClass.test_func.hello @@ -90,11 +90,11 @@ class TestFunctional: assert 'hello' in keywords assert 'world' in keywords - @py.test.mark.skipif("sys.version_info < (2,6)") + @pytest.mark.skipif("sys.version_info < (2,6)") def test_mark_per_class_decorator(self, testdir): item = testdir.getitem(""" - import py - @py.test.mark.hello + import pytest + @pytest.mark.hello class TestClass: def test_func(self): assert TestClass.test_func.hello @@ -102,13 +102,13 @@ class TestFunctional: keywords = item.keywords assert 'hello' in keywords - @py.test.mark.skipif("sys.version_info < (2,6)") + @pytest.mark.skipif("sys.version_info < (2,6)") def test_mark_per_class_decorator_plus_existing_dec(self, testdir): item = testdir.getitem(""" - import py - @py.test.mark.hello + import pytest + @pytest.mark.hello class TestClass: - pytestmark = py.test.mark.world + pytestmark = pytest.mark.world def test_func(self): assert TestClass.test_func.hello assert TestClass.test_func.world @@ -119,12 +119,12 @@ class TestFunctional: def test_merging_markers(self, testdir): p = testdir.makepyfile(""" - import py - pytestmark = py.test.mark.hello("pos1", x=1, y=2) + import pytest + pytestmark = pytest.mark.hello("pos1", x=1, y=2) class TestClass: # classlevel overrides module level - pytestmark = py.test.mark.hello(x=3) - @py.test.mark.hello("pos0", z=4) + pytestmark = pytest.mark.hello(x=3) + @pytest.mark.hello("pos0", z=4) def test_func(self): pass """) @@ -136,9 +136,9 @@ class TestFunctional: assert marker.kwargs == {'x': 3, 'y': 2, 'z': 4} def test_mark_other(self, testdir): - py.test.raises(TypeError, ''' + pytest.raises(TypeError, ''' testdir.getitem(""" - import py + import pytest class pytestmark: pass def test_func(): @@ -148,9 +148,9 @@ class TestFunctional: def test_mark_dynamically_in_funcarg(self, testdir): testdir.makeconftest(""" - import py + import pytest def pytest_funcarg__arg(request): - request.applymarker(py.test.mark.hello) + request.applymarker(pytest.mark.hello) def pytest_terminal_summary(terminalreporter): l = terminalreporter.stats['passed'] terminalreporter._tw.line("keyword: %s" % l[0].keywords) @@ -187,7 +187,7 @@ class Test_genitems: # do we want to unify behaviour with # test_subdir_conftest_error? p = testdir.makepyfile(conftest="raise SyntaxError\n") - py.test.raises(SyntaxError, testdir.inline_genitems, p.dirpath()) + pytest.raises(SyntaxError, testdir.inline_genitems, p.dirpath()) def test_example_items1(self, testdir): p = testdir.makepyfile(''' @@ -247,7 +247,6 @@ class TestKeywordSelection: pass """) testdir.makepyfile(conftest=""" - import py def pytest_pycollect_makeitem(__multicall__, name): if name == "TestClass": item = __multicall__.execute() --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -130,7 +130,7 @@ class TestGeneralUsage: assert result.ret != 0 assert "should be seen" in result.stdout.str() - @py.test.mark.skipif("not hasattr(os, 'symlink')") + @pytest.mark.skipif("not hasattr(os, 'symlink')") def test_chdir(self, testdir): testdir.tmpdir.join("py").mksymlinkto(py._pydir) p = testdir.tmpdir.join("main.py") @@ -273,7 +273,7 @@ class TestInvocationVariants: def test_double_pytestcmdline(self, testdir): p = testdir.makepyfile(run=""" - import py, pytest + import pytest pytest.main() pytest.main() """) @@ -287,19 +287,19 @@ class TestInvocationVariants: "*1 passed*", ]) - @py.test.mark.skipif("sys.version_info < (2,5)") + @pytest.mark.skipif("sys.version_info < (2,5)") def test_python_minus_m_invocation_ok(self, testdir): p1 = testdir.makepyfile("def test_hello(): pass") res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) assert res.ret == 0 - @py.test.mark.skipif("sys.version_info < (2,5)") + @pytest.mark.skipif("sys.version_info < (2,5)") def test_python_minus_m_invocation_fail(self, testdir): p1 = testdir.makepyfile("def test_fail(): 0/0") res = testdir.run(py.std.sys.executable, "-m", "py.test", str(p1)) assert res.ret == 1 - @py.test.mark.skipif("sys.version_info < (2,5)") + @pytest.mark.skipif("sys.version_info < (2,5)") def test_python_pytest_package(self, testdir): p1 = testdir.makepyfile("def test_pass(): pass") res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1)) @@ -359,7 +359,7 @@ class TestInvocationVariants: ]) - @py.test.mark.xfail(reason="decide: feature or bug") + @pytest.mark.xfail(reason="decide: feature or bug") def test_noclass_discovery_if_not_testcase(self, testdir): testpath = testdir.makepyfile(""" import unittest --- a/testing/test_doctest.py +++ b/testing/test_doctest.py @@ -1,5 +1,5 @@ from _pytest.doctest import DoctestModule, DoctestTextfile -import py +import py, pytest class TestDoctests: @@ -58,7 +58,7 @@ class TestDoctests: assert call.report.longrepr # XXX #testitem, = items - #excinfo = py.test.raises(Failed, "testitem.runtest()") + #excinfo = pytest.raises(Failed, "testitem.runtest()") #repr = testitem.repr_failure(excinfo, ("", "")) #assert repr.reprlocation --- a/doc/example/controlskip.txt +++ b/doc/example/controlskip.txt @@ -8,22 +8,22 @@ Here is a ``conftest.py`` file adding a line option to control skipping of ``slow`` marked tests:: # content of conftest.py - - import py + + import pytest def pytest_addoption(parser): parser.addoption("--runslow", action="store_true", help="run slow tests") def pytest_runtest_setup(item): if 'slow' in item.keywords and not item.config.getvalue("runslow"): - py.test.skip("need --runslow option to run") + pytest.skip("need --runslow option to run") We can now write a test module like this:: # content of test_module.py - - import py - slow = py.test.mark.slow + + import pytest + slow = pytest.mark.slow def test_func_fast(): pass @@ -34,14 +34,14 @@ We can now write a test module like this and when running it will see a skipped "slow" test:: - $ py.test test_module.py -rs # "-rs" means report on the little 's' + $ py.test test_module.py -rs # "-rs" means report details on the little 's' =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 test path 1: test_module.py test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-557/conftest.py:9: need --runslow option to run + SKIP [1] /tmp/doc-exec-104/conftest.py:9: need --runslow option to run =================== 1 passed, 1 skipped in 0.02 seconds ==================== @@ -49,7 +49,7 @@ Or run it including the ``slow`` marked $ py.test test_module.py --runslow =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 test path 1: test_module.py test_module.py .. --- a/testing/test_helpconfig.py +++ b/testing/test_helpconfig.py @@ -43,8 +43,8 @@ def test_hookvalidation_unknown(testdir) def test_hookvalidation_optional(testdir): testdir.makeconftest(""" - import py - @py.test.mark.optionalhook + import pytest + @pytest.mark.optionalhook def pytest_hello(xyz): pass """) --- a/doc/features.txt +++ b/doc/features.txt @@ -15,11 +15,11 @@ no-boilerplate testing with Python extensive plugin and customization system ------------------------------------------------------ -.. _`suprisingly easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html +.. _`easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html - all collection, reporting, running aspects are delegated to hook functions -- hook functions are defined per-directory, per-project or through PyPI released plugins -- it is `suprisingly easy`_ to add command line options or +- hook functions are implemented per-directory, per-project or through PyPI released plugins +- it is `easy`_ to add command line options or do other kind of add-ons and customizations. mature command line testing tool @@ -42,24 +42,20 @@ integrates well with CI systems .. _`tox`: http://codespeak.net/tox -supports several testing practises and methods +supports common testing practises and methods ----------------------------------------------------------- - supports extended `xUnit style setup`_ -- can integrate nose_, `unittest.py` and `doctest.py`_ style tests +- can integrate ``nose``, ``unittest.py`` and ``doctest.py`` style tests - supports generating testing coverage reports - supports :ref:`non-python tests` - `Javasript unit- and functional testing`_ -.. _`Javasript unit- and functional testing`: plugin/oejskit.html -.. _`coverage testing with figleaf`: plugin/figleaf.html -.. _`unittest.py`: http://docs.python.org/library/unittest.html +.. _`Javasript unit- and functional testing`: http://pypi.python.org/pypi/oejskit distributing tests to local/remote subprocesses -------------------------------------------------------- -.. _`pytest-xdist`: plugin/xdist.html - - distribute tests to multiple CPUs - distribute tests to remote ssh or socket connected machines - run tests in subprocess, re-run failing ones on file-change --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -1,7 +1,7 @@ -import py, os, sys +import pytest, py, os, sys from _pytest.capture import CaptureManager -needsosdup = py.test.mark.xfail("not hasattr(os, 'dup')") +needsosdup = pytest.mark.xfail("not hasattr(os, 'dup')") class TestCaptureManager: def test_getmethod_default_no_fd(self, testdir, monkeypatch): @@ -33,7 +33,7 @@ class TestCaptureManager: assert capman._getmethod(config, sub.join("test_hello.py")) == name @needsosdup - @py.test.mark.multi(method=['no', 'fd', 'sys']) + @pytest.mark.multi(method=['no', 'fd', 'sys']) def test_capturing_basic_api(self, method): capouter = py.io.StdCaptureFD() old = sys.stdout, sys.stderr, sys.stdin @@ -63,8 +63,8 @@ class TestCaptureManager: config = testdir.parseconfig(testdir.tmpdir) capman = CaptureManager() capman.resumecapture("fd") - py.test.raises(ValueError, 'capman.resumecapture("fd")') - py.test.raises(ValueError, 'capman.resumecapture("sys")') + pytest.raises(ValueError, 'capman.resumecapture("fd")') + pytest.raises(ValueError, 'capman.resumecapture("sys")') os.write(1, "hello\n".encode('ascii')) out, err = capman.suspendcapture() assert out == "hello\n" @@ -77,7 +77,7 @@ class TestCaptureManager: finally: capouter.reset() - at py.test.mark.multi(method=['fd', 'sys']) + at pytest.mark.multi(method=['fd', 'sys']) def test_capturing_unicode(testdir, method): if sys.version_info >= (3,0): obj = "'b\u00f6y'" @@ -96,7 +96,7 @@ def test_capturing_unicode(testdir, meth "*1 passed*" ]) - at py.test.mark.multi(method=['fd', 'sys']) + at pytest.mark.multi(method=['fd', 'sys']) def test_capturing_bytes_in_utf8_encoding(testdir, method): testdir.makepyfile(""" def test_unicode(): @@ -141,7 +141,7 @@ class TestPerTestCapturing: "in func2*", ]) - @py.test.mark.xfail + @pytest.mark.xfail def test_capture_scope_cache(self, testdir): p = testdir.makepyfile(""" import sys @@ -245,7 +245,7 @@ class TestLoggingInteraction: p = testdir.makepyfile(""" def test_logging(): import logging - import py + import pytest stream = py.io.TextIO() logging.basicConfig(stream=stream) stream.close() # to free memory/release resources --- a/testing/test_recwarn.py +++ b/testing/test_recwarn.py @@ -1,4 +1,4 @@ -import py +import py, pytest from _pytest.recwarn import WarningsRecorder def test_WarningRecorder(recwarn): @@ -16,7 +16,7 @@ def test_WarningRecorder(recwarn): rec.clear() assert len(rec.list) == 0 assert l is rec.list - py.test.raises(AssertionError, "rec.pop()") + pytest.raises(AssertionError, "rec.pop()") rec.finalize() assert showwarning == py.std.warnings.showwarning @@ -52,7 +52,7 @@ def dep_explicit(i): filename="hello", lineno=3) def test_deprecated_call_raises(): - excinfo = py.test.raises(AssertionError, + excinfo = pytest.raises(AssertionError, "py.test.deprecated_call(dep, 3)") assert str(excinfo).find("did not produce") != -1 @@ -72,7 +72,7 @@ def test_deprecated_call_preserves(): assert f == py.std.warnings.filters def test_deprecated_explicit_call_raises(): - py.test.raises(AssertionError, + pytest.raises(AssertionError, "py.test.deprecated_call(dep_explicit, 3)") def test_deprecated_explicit_call(): --- a/doc/recwarn.txt +++ b/doc/recwarn.txt @@ -31,8 +31,8 @@ You can also call a global helper for ch that a certain function call triggers a Deprecation warning:: - import py + import pytest def test_global(): - py.test.deprecated_call(myfunction, 17) + pytest.deprecated_call(myfunction, 17) --- a/doc/example/nonpython.txt +++ b/doc/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.6.5 -- pytest-2.0.0.dev22 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 test path 1: test_simple.yml test_simple.yml .F @@ -39,7 +39,7 @@ now execute the test specification:: no further details known at this point. ========================= short test summary info ========================== FAIL test_simple.yml::hello - ==================== 1 failed, 1 passed in 0.43 seconds ==================== + ==================== 1 failed, 1 passed in 0.06 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 @@ -58,11 +58,11 @@ reporting in ``verbose`` mode:: nonpython $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev22 -- /home/hpk/venv/0/bin/python + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 -- /home/hpk/venv/0/bin/python test path 1: /home/hpk/p/pytest/doc/example/nonpython - test_simple.yml <- test_simple.yml:1: usecase: ok PASSED - test_simple.yml <- test_simple.yml:1: usecase: hello FAILED + test_simple.yml:1: usecase: ok PASSED + test_simple.yml:1: usecase: hello FAILED ================================= FAILURES ================================= ______________________________ usecase: hello ______________________________ @@ -71,7 +71,7 @@ reporting in ``verbose`` mode:: no further details known at this point. ========================= short test summary info ========================== FAIL test_simple.yml::hello - ==================== 1 failed, 1 passed in 0.07 seconds ==================== + ==================== 1 failed, 1 passed in 0.06 seconds ==================== While developing your custom test collection and execution it's also interesting to just look at the collection tree:: --- a/testing/test_python.py +++ b/testing/test_python.py @@ -4,8 +4,8 @@ from _pytest import python as funcargs class TestModule: def test_failing_import(self, testdir): modcol = testdir.getmodulecol("import alksdjalskdjalkjals") - py.test.raises(ImportError, modcol.collect) - py.test.raises(ImportError, modcol.collect) + pytest.raises(ImportError, modcol.collect) + pytest.raises(ImportError, modcol.collect) def test_import_duplicate(self, testdir): a = testdir.mkdir("a") @@ -26,12 +26,12 @@ class TestModule: def test_syntax_error_in_module(self, testdir): modcol = testdir.getmodulecol("this is a syntax error") - py.test.raises(modcol.CollectError, modcol.collect) - py.test.raises(modcol.CollectError, modcol.collect) + pytest.raises(modcol.CollectError, modcol.collect) + pytest.raises(modcol.CollectError, modcol.collect) def test_module_considers_pluginmanager_at_import(self, testdir): modcol = testdir.getmodulecol("pytest_plugins='xasdlkj',") - py.test.raises(ImportError, "modcol.obj") + pytest.raises(ImportError, "modcol.obj") class TestClass: def test_class_with_init_not_collected(self, testdir): @@ -119,7 +119,7 @@ class TestGenerator: assert len(colitems) == 1 gencol = colitems[0] assert isinstance(gencol, pytest.Generator) - py.test.raises(ValueError, "gencol.collect()") + pytest.raises(ValueError, "gencol.collect()") def test_generative_methods_with_explicit_names(self, testdir): modcol = testdir.getmodulecol(""" @@ -144,7 +144,7 @@ class TestGenerator: def test_order_of_execution_generator_same_codeline(self, testdir, tmpdir): o = testdir.makepyfile(""" def test_generative_order_of_execution(): - import py + import py, pytest test_list = [] expected_list = list(range(6)) @@ -168,7 +168,7 @@ class TestGenerator: def test_order_of_execution_generator_different_codeline(self, testdir): o = testdir.makepyfile(""" def test_generative_tests_different_codeline(): - import py + import py, pytest test_list = [] expected_list = list(range(3)) @@ -351,7 +351,7 @@ def test_setup_only_available_in_subdir( sub1 = testdir.mkpydir("sub1") sub2 = testdir.mkpydir("sub2") sub1.join("conftest.py").write(py.code.Source(""" - import py + import pytest def pytest_runtest_setup(item): assert item.fspath.purebasename == "test_in_sub1" def pytest_runtest_call(item): @@ -360,7 +360,7 @@ def test_setup_only_available_in_subdir( assert item.fspath.purebasename == "test_in_sub1" """)) sub2.join("conftest.py").write(py.code.Source(""" - import py + import pytest def pytest_runtest_setup(item): assert item.fspath.purebasename == "test_in_sub2" def pytest_runtest_call(item): @@ -402,7 +402,7 @@ def test_modulecol_roundtrip(testdir): class TestTracebackCutting: def test_skip_simple(self): - excinfo = py.test.raises(py.test.skip.Exception, 'py.test.skip("xxx")') + excinfo = pytest.raises(pytest.skip.Exception, 'pytest.skip("xxx")') assert excinfo.traceback[-1].frame.code.name == "skip" assert excinfo.traceback[-1].ishidden() @@ -480,7 +480,7 @@ class TestFillFuncArgs: return 42 """) item = testdir.getitem("def test_func(some): pass") - exc = py.test.raises(funcargs.FuncargRequest.LookupError, + exc = pytest.raises(funcargs.FuncargRequest.LookupError, "funcargs.fillfuncargs(item)") s = str(exc.value) assert s.find("xyzsomething") != -1 @@ -611,7 +611,7 @@ class TestRequest: def test_func(something): pass """) req = funcargs.FuncargRequest(item) - py.test.raises(req.LookupError, req.getfuncargvalue, "notexists") + pytest.raises(req.LookupError, req.getfuncargvalue, "notexists") val = req.getfuncargvalue("something") assert val == 1 val = req.getfuncargvalue("something") @@ -672,12 +672,12 @@ def test_applymarker(testdir): """) req1 = funcargs.FuncargRequest(item1) assert 'xfail' not in item1.keywords - req1.applymarker(py.test.mark.xfail) + req1.applymarker(pytest.mark.xfail) assert 'xfail' in item1.keywords assert 'skipif' not in item1.keywords - req1.applymarker(py.test.mark.skipif) + req1.applymarker(pytest.mark.skipif) assert 'skipif' in item1.keywords - py.test.raises(ValueError, "req1.applymarker(42)") + pytest.raises(ValueError, "req1.applymarker(42)") class TestRequestCachedSetup: def test_request_cachedsetup(self, testdir): @@ -815,11 +815,11 @@ class TestMetafunc: def test_addcall_id(self): def func(arg1): pass metafunc = funcargs.Metafunc(func) - py.test.raises(ValueError, "metafunc.addcall(id=None)") + pytest.raises(ValueError, "metafunc.addcall(id=None)") metafunc.addcall(id=1) - py.test.raises(ValueError, "metafunc.addcall(id=1)") - py.test.raises(ValueError, "metafunc.addcall(id='1')") + pytest.raises(ValueError, "metafunc.addcall(id=1)") + pytest.raises(ValueError, "metafunc.addcall(id='1')") metafunc.addcall(id=2) assert len(metafunc._calls) == 2 assert metafunc._calls[0].id == "1" @@ -852,7 +852,7 @@ class TestGenfuncFunctional: def test_attributes(self, testdir): p = testdir.makepyfile(""" # assumes that generate/provide runs in the same process - import py + import py, pytest def pytest_generate_tests(metafunc): metafunc.addcall(param=metafunc) @@ -990,14 +990,14 @@ def test_conftest_funcargs_only_availabl sub1 = testdir.mkpydir("sub1") sub2 = testdir.mkpydir("sub2") sub1.join("conftest.py").write(py.code.Source(""" - import py + import pytest def pytest_funcarg__arg1(request): - py.test.raises(Exception, "request.getfuncargvalue('arg2')") + pytest.raises(Exception, "request.getfuncargvalue('arg2')") """)) sub2.join("conftest.py").write(py.code.Source(""" - import py + import pytest def pytest_funcarg__arg2(request): - py.test.raises(Exception, "request.getfuncargvalue('arg1')") + pytest.raises(Exception, "request.getfuncargvalue('arg1')") """)) sub1.join("test_in_sub1.py").write("def test_1(arg1): pass") @@ -1121,50 +1121,50 @@ def test_show_funcarg(testdir): class TestRaises: def test_raises(self): source = "int('qwe')" - excinfo = py.test.raises(ValueError, source) + excinfo = pytest.raises(ValueError, source) code = excinfo.traceback[-1].frame.code s = str(code.fullsource) assert s == source def test_raises_exec(self): - py.test.raises(ValueError, "a,x = []") + pytest.raises(ValueError, "a,x = []") def test_raises_syntax_error(self): - py.test.raises(SyntaxError, "qwe qwe qwe") + pytest.raises(SyntaxError, "qwe qwe qwe") def test_raises_function(self): - py.test.raises(ValueError, int, 'hello') + pytest.raises(ValueError, int, 'hello') def test_raises_callable_no_exception(self): class A: def __call__(self): pass try: - py.test.raises(ValueError, A()) - except py.test.raises.Exception: + pytest.raises(ValueError, A()) + except pytest.raises.Exception: pass - @py.test.mark.skipif('sys.version < "2.5"') + @pytest.mark.skipif('sys.version < "2.5"') def test_raises_as_contextmanager(self, testdir): testdir.makepyfile(""" from __future__ import with_statement - import py + import py, pytest def test_simple(): - with py.test.raises(ZeroDivisionError) as excinfo: + with pytest.raises(ZeroDivisionError) as excinfo: assert isinstance(excinfo, py.code.ExceptionInfo) 1/0 print (excinfo) assert excinfo.type == ZeroDivisionError def test_noraise(): - with py.test.raises(py.test.raises.Exception): - with py.test.raises(ValueError): + with pytest.raises(pytest.raises.Exception): + with pytest.raises(ValueError): int() def test_raise_wrong_exception_passes_by(): - with py.test.raises(ZeroDivisionError): - with py.test.raises(ValueError): + with pytest.raises(ZeroDivisionError): + with pytest.raises(ValueError): 1/0 """) result = testdir.runpytest() --- a/testing/test_collection.py +++ b/testing/test_collection.py @@ -178,7 +178,7 @@ class TestPrunetraceback: "*hello world*", ]) - @py.test.mark.xfail(reason="other mechanism for adding to reporting needed") + @pytest.mark.xfail(reason="other mechanism for adding to reporting needed") def test_collect_report_postprocessing(self, testdir): p = testdir.makepyfile(""" import not_exists @@ -520,7 +520,7 @@ class Test_genitems: # do we want to unify behaviour with # test_subdir_conftest_error? p = testdir.makepyfile(conftest="raise SyntaxError\n") - py.test.raises(SyntaxError, testdir.inline_genitems, p.dirpath()) + pytest.raises(SyntaxError, testdir.inline_genitems, p.dirpath()) def test_example_items1(self, testdir): p = testdir.makepyfile(''' From commits-noreply at bitbucket.org Thu Nov 18 15:05:12 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 18 Nov 2010 08:05:12 -0600 (CST) Subject: [py-svn] pytest commit 2bd8b95e0ccb: add some missing files Message-ID: <20101118140512.269D31E0FB4@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290089090 -3600 # Node ID 2bd8b95e0ccbc09e3857970821ab219a2401ca56 # Parent 867df79243d4ecc5607d1004e3ab96599c861dd5 add some missing files --- /dev/null +++ b/doc/check_sphinx.py @@ -0,0 +1,17 @@ +import py +import subprocess +def test_build_docs(tmpdir): + doctrees = tmpdir.join("doctrees") + htmldir = tmpdir.join("html") + subprocess.check_call([ + "sphinx-build", "-W", "-bhtml", + "-d", str(doctrees), ".", str(htmldir)]) + +def test_linkcheck(tmpdir): + doctrees = tmpdir.join("doctrees") + htmldir = tmpdir.join("html") + subprocess.check_call( + ["sphinx-build", "-blinkcheck", + "-d", str(doctrees), ".", str(htmldir)]) + + --- /dev/null +++ b/doc/example/attic.txt @@ -0,0 +1,79 @@ + +.. _`accept example`: + +example: specifying and selecting acceptance tests +-------------------------------------------------------------- + +.. sourcecode:: python + + # ./conftest.py + def pytest_option(parser): + group = parser.getgroup("myproject") + group.addoption("-A", dest="acceptance", action="store_true", + help="run (slow) acceptance tests") + + def pytest_funcarg__accept(request): + return AcceptFuncarg(request) + + class AcceptFuncarg: + def __init__(self, request): + if not request.config.option.acceptance: + pytest.skip("specify -A to run acceptance tests") + self.tmpdir = request.config.mktemp(request.function.__name__, numbered=True) + + def run(self, cmd): + """ called by test code to execute an acceptance test. """ + self.tmpdir.chdir() + return py.process.cmdexec(cmd) + + +and the actual test function example: + +.. sourcecode:: python + + def test_some_acceptance_aspect(accept): + accept.tmpdir.mkdir("somesub") + result = accept.run("ls -la") + assert "somesub" in result + +If you run this test without specifying a command line option +the test will get skipped with an appropriate message. Otherwise +you can start to add convenience and test support methods +to your AcceptFuncarg and drive running of tools or +applications and provide ways to do assertions about +the output. + +.. _`decorate a funcarg`: + +example: decorating a funcarg in a test module +-------------------------------------------------------------- + +For larger scale setups it's sometimes useful to decorare +a funcarg just for a particular test module. We can +extend the `accept example`_ by putting this in our test module: + +.. sourcecode:: python + + def pytest_funcarg__accept(request): + # call the next factory (living in our conftest.py) + arg = request.getfuncargvalue("accept") + # create a special layout in our tempdir + arg.tmpdir.mkdir("special") + return arg + + class TestSpecialAcceptance: + def test_sometest(self, accept): + assert accept.tmpdir.join("special").check() + +Our module level factory will be invoked first and it can +ask its request object to call the next factory and then +decorate its result. This mechanism allows us to stay +ignorant of how/where the function argument is provided - +in our example from a `conftest plugin`_. + +sidenote: the temporary directory used here are instances of +the `py.path.local`_ class which provides many of the os.path +methods in a convenient way. + +.. _`py.path.local`: ../path.html#local +.. _`conftest plugin`: customize.html#conftestplugin --- /dev/null +++ b/doc/xdist.txt @@ -0,0 +1,170 @@ +xdist: pytest distributed testing plugin +=============================================================== + +The `pytest-xdist`_ plugin extends py.test with some unique +test execution modes: + +* Looponfail: run your tests repeatedly in a subprocess. After each run py.test + waits until a file in your project changes and then re-runs the previously + failing tests. This is repeated until all tests pass after which again + a full run is performed. + +* multiprocess Load-balancing: if you have multiple CPUs or hosts you can use + those for a combined test run. This allows to speed up + development or to use special resources of remote machines. + +* Multi-Platform coverage: you can specify different Python interpreters + or different platforms and run tests in parallel on all of them. + +Before running tests remotely, ``py.test`` efficiently "rsyncs" your +program source code to the remote place. All test results +are reported back and displayed to your local terminal. +You may specify different Python versions and interpreters. + + +Installation +----------------------- + +Install the plugin with:: + + easy_install pytest-xdist + + # or + + pip install pytest-xdist + +or use the package in develope/in-place mode with +a checkout of the `pytest-xdist repository`_ :: + + python setup.py develop + +Usage examples +--------------------- + +Speed up test runs by sending tests to multiple CPUs ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +To send tests to multiple CPUs, type:: + + py.test -n NUM + +Especially for longer running tests or tests requiring +a lot of IO this can lead to considerable speed ups. + + +Running tests in a Python subprocess ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +To instantiate a python2.4 sub process and send tests to it, you may type:: + + py.test -d --tx popen//python=python2.4 + +This will start a subprocess which is run with the "python2.4" +Python interpreter, found in your system binary lookup path. + +If you prefix the --tx option value like this:: + + --tx 3*popen//python=python2.4 + +then three subprocesses would be created and tests +will be load-balanced across these three processes. + + +Sending tests to remote SSH accounts ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Suppose you have a package ``mypkg`` which contains some +tests that you can successfully run locally. And you +have a ssh-reachable machine ``myhost``. Then +you can ad-hoc distribute your tests by typing:: + + py.test -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg + +This will synchronize your ``mypkg`` package directory +to an remote ssh account and then locally collect tests +and send them to remote places for execution. + +You can specify multiple ``--rsyncdir`` directories +to be sent to the remote side. + +**NOTE:** For py.test to collect and send tests correctly +you not only need to make sure all code and tests +directories are rsynced, but that any test (sub) directory +also has an ``__init__.py`` file because internally +py.test references tests as a fully qualified python +module path. **You will otherwise get strange errors** +during setup of the remote side. + +Sending tests to remote Socket Servers ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +Download the single-module `socketserver.py`_ Python program +and run it like this:: + + python socketserver.py + +It will tell you that it starts listening on the default +port. You can now on your home machine specify this +new socket host with something like this:: + + py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg + + +.. _`atonce`: + +Running tests on many platforms at once ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +The basic command to run tests on multiple platforms is:: + + py.test --dist=each --tx=spec1 --tx=spec2 + +If you specify a windows host, an OSX host and a Linux +environment this command will send each tests to all +platforms - and report back failures from all platforms +at once. The specifications strings use the `xspec syntax`_. + +.. _`xspec syntax`: http://codespeak.net/execnet/trunk/basics.html#xspec + +.. _`socketserver.py`: http://bitbucket.org/hpk42/execnet/raw/2af991418160/execnet/script/socketserver.py + +.. _`execnet`: http://codespeak.net/execnet + +Specifying test exec environments in an ini file ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +pytest (since version 2.0) supports ini-style cofiguration. +You can for example make running with three subprocesses +your default like this:: + + [pytest] + addopts = -n3 + +You can also add default environments like this:: + + [pytest] + addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python2.6 + +and then just type:: + + py.test --dist=each + +to run tests in each of the environments. + +Specifying "rsync" dirs in an ini-file ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +In a ``tox.ini`` or ``setup.cfg`` file in your root project directory +you may specify directories to include or to exclude in synchronisation:: + + [pytest] + rsyncdirs = . mypkg helperpkg + rsyncignore = .hg + +These directory specifications are relative to the directory +where the configuration file was found. + +.. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist +.. _`pytest-xdist repository`: http://bitbucket.org/hpk42/pytest-xdist +.. _`pytest`: http://pytest.org + From commits-noreply at bitbucket.org Thu Nov 18 15:19:42 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 18 Nov 2010 08:19:42 -0600 (CST) Subject: [py-svn] pytest commit 1e1073e89e9f: fix bare "py.test" runs without a directory by not defaulting to --doctest-modules which will virtually import everything Message-ID: <20101118141942.52A511E0FB4@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290089960 -3600 # Node ID 1e1073e89e9fdaf7e317f887528d075401ad5f87 # Parent 2bd8b95e0ccbc09e3857970821ab219a2401ca56 fix bare "py.test" runs without a directory by not defaulting to --doctest-modules which will virtually import everything --- a/tox.ini +++ b/tox.ini @@ -63,6 +63,6 @@ commands= [pytest] minversion=2.0 plugins=pytester -addopts= -rxf --pyargs --doctest-modules +addopts= -rxf --pyargs rsyncdirs=tox.ini pytest.py _pytest testing --- a/bench/bench.py +++ b/bench/bench.py @@ -1,9 +1,10 @@ -import cProfile -import py -import pstats -stats = cProfile.run('py.test.cmdline.main(["empty.py", ])', 'prof') -p = pstats.Stats("prof") -p.strip_dirs() -p.sort_stats('cumulative') -print p.print_stats(30) +if __name__ == '__main__': + import cProfile + import py + import pstats + stats = cProfile.run('py.test.cmdline.main(["empty.py", ])', 'prof') + p = pstats.Stats("prof") + p.strip_dirs() + p.sort_stats('cumulative') + print p.print_stats(30) From commits-noreply at bitbucket.org Thu Nov 18 15:32:22 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 18 Nov 2010 08:32:22 -0600 (CST) Subject: [py-svn] pytest commit c359bd19375e: better deal with importing conftest.py with --doctest-modules and Message-ID: <20101118143222.5F3F66C131F@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290090718 -3600 # Node ID c359bd19375e6ce32534799d1981e9a59da627b2 # Parent 1e1073e89e9fdaf7e317f887528d075401ad5f87 better deal with importing conftest.py with --doctest-modules and re-enable default of "--doctest-modules" even if issued at root level --- a/_pytest/doctest.py +++ b/_pytest/doctest.py @@ -73,7 +73,10 @@ class DoctestTextfile(DoctestItem, pytes class DoctestModule(DoctestItem, pytest.File): def runtest(self): doctest = py.std.doctest - module = self.fspath.pyimport() + if self.fspath.basename == "conftest.py": + module = self.config._conftest.importconftest(self.fspath) + else: + module = self.fspath.pyimport() failed, tot = doctest.testmod( module, raise_on_error=True, verbose=0, optionflags=doctest.ELLIPSIS) --- a/tox.ini +++ b/tox.ini @@ -18,6 +18,7 @@ commands= py.test --genscript=pytest1 deps=pylib [testenv:py27-xdist] +changedir=. basepython=python2.7 deps=pytest-xdist commands= @@ -63,6 +64,6 @@ commands= [pytest] minversion=2.0 plugins=pytester -addopts= -rxf --pyargs +addopts= -rxf --pyargs --doctest-modules rsyncdirs=tox.ini pytest.py _pytest testing From commits-noreply at bitbucket.org Sat Nov 20 18:05:14 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 20 Nov 2010 11:05:14 -0600 (CST) Subject: [py-svn] pytest commit d633f6bed7af: add ability to use scope="class" in request.cached_setup() calls Message-ID: <20101120170514.C832B243F74@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290272598 -3600 # Node ID d633f6bed7afd820ddc0c6e560f5b63669ab1e60 # Parent ddcf34bc8c80790e66689a13ae1d23fb8461a4ac add ability to use scope="class" in request.cached_setup() calls --- a/pytest.py +++ b/pytest.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev30' +__version__ = '2.0.0.dev31' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev30', + version='2.0.0.dev31', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/_pytest/python.py +++ b/_pytest/python.py @@ -626,8 +626,8 @@ class FuncargRequest: :arg teardown: function receiving a previously setup resource. :arg setup: a no-argument function creating a resource. - :arg scope: a string value out of ``function``, ``module`` or - ``session`` indicating the caching lifecycle of the resource. + :arg scope: a string value out of ``function``, ``class``, ``module`` + or ``session`` indicating the caching lifecycle of the resource. :arg extrakey: added to internal caching key of (funcargname, scope). """ if not hasattr(self.config, '_setupcache'): @@ -678,10 +678,15 @@ class FuncargRequest: def _getscopeitem(self, scope): if scope == "function": return self._pyfuncitem - elif scope == "module": - return self._pyfuncitem.getparent(pytest.Module) elif scope == "session": return None + elif scope == "class": + x = self._pyfuncitem.getparent(pytest.Class) + if x is not None: + return x + scope = "module" + if scope == "module": + return self._pyfuncitem.getparent(pytest.Module) raise ValueError("unknown finalization scope %r" %(scope,)) def addfinalizer(self, finalizer): --- a/testing/test_python.py +++ b/testing/test_python.py @@ -682,9 +682,9 @@ def test_applymarker(testdir): class TestRequestCachedSetup: def test_request_cachedsetup(self, testdir): item1,item2 = testdir.getitems(""" + def test_func1(self, something): + pass class TestClass: - def test_func1(self, something): - pass def test_func2(self, something): pass """) @@ -692,6 +692,7 @@ class TestRequestCachedSetup: l = ["hello"] def setup(): return l.pop() + # cached_setup's scope defaults to 'module' ret1 = req1.cached_setup(setup) assert ret1 == "hello" ret1b = req1.cached_setup(setup) @@ -700,6 +701,39 @@ class TestRequestCachedSetup: ret2 = req2.cached_setup(setup) assert ret2 == ret1 + def test_request_cachedsetup_class(self, testdir): + item1, item2, item3, item4 = testdir.getitems(""" + def test_func1(self, something): + pass + def test_func2(self, something): + pass + class TestClass: + def test_func1a(self, something): + pass + def test_func2b(self, something): + pass + """) + req1 = funcargs.FuncargRequest(item2) + l = ["hello2", "hello"] + def setup(): + return l.pop() + + # module level functions setup with scope=class + # automatically turn "class" to "module" scope + ret1 = req1.cached_setup(setup, scope="class") + assert ret1 == "hello" + req2 = funcargs.FuncargRequest(item2) + ret2 = req2.cached_setup(setup, scope="class") + assert ret2 == "hello" + + req3 = funcargs.FuncargRequest(item3) + ret3a = req3.cached_setup(setup, scope="class") + ret3b = req3.cached_setup(setup, scope="class") + assert ret3a == ret3b == "hello2" + req4 = funcargs.FuncargRequest(item4) + ret4 = req4.cached_setup(setup, scope="class") + assert ret4 == ret3a + def test_request_cachedsetup_extrakey(self, testdir): item1 = testdir.getitem("def test_func(): pass") req1 = funcargs.FuncargRequest(item1) --- a/CHANGELOG +++ b/CHANGELOG @@ -37,6 +37,7 @@ Changes between 1.3.4 and 2.0.0dev0 - fix issue93 stdout/stderr is captured while importing conftest.py - fix bug: unittest collected functions now also can have "pytestmark" applied at class/module level +- add ability to use "class" level for cached_setup helper Changes between 1.3.3 and 1.3.4 From commits-noreply at bitbucket.org Sat Nov 20 18:05:14 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 20 Nov 2010 11:05:14 -0600 (CST) Subject: [py-svn] pytest commit ddcf34bc8c80: refine release announcement Message-ID: <20101120170514.B07662410BD@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290102153 -3600 # Node ID ddcf34bc8c80790e66689a13ae1d23fb8461a4ac # Parent c359bd19375e6ce32534799d1981e9a59da627b2 refine release announcement --- a/doc/Makefile +++ b/doc/Makefile @@ -39,8 +39,8 @@ help: clean: -rm -rf $(BUILDDIR)/* -install: html - rsync -avz _build/html/ code:public_html/pytest +install: clean html + rsync -avz _build/html/ code:www-pytest/2.0.0 html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html --- a/doc/conf.py +++ b/doc/conf.py @@ -51,7 +51,9 @@ copyright = u'2010, holger krekel et ali # The short X.Y version. version = '2.0.0' # The full version, including alpha/beta/rc tags. -release = '2.0.0dev0' +import py, pytest +assert py.path.local().relto(py.path.local(pytest.__file__).dirpath().dirpath()) +release = pytest.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. --- a/doc/announce/release-2.0.0.txt +++ b/doc/announce/release-2.0.0.txt @@ -1,24 +1,23 @@ -py.test 2.0.0: standalone, features++, implementation++, docs++ +py.test 2.0.0: improved asserts, unittest, reporting, config, docs =========================================================================== -XXX PENDING +Welcome to pytest-2.0.0, a major new release of "py.test", the rapid +easy Python testing tool. There are many new features and a lot of +long-deprecated code is removed, resulting in a much smaller and cleaner +implementation. If you had a test suite using non-deprecated +functionality 2.0.0 is very likely to continue to work for you without +requiring changes. -Welcome to pytest-2.0.0, rapid and easy testing for and with Python. -py.test now comes as its own PyPI distribution named ``pytest`` which -installs the ``py.test`` tool. It removes most long-deprecated code, -providing for a much smaller and easier to understand code base. There -are also many new features and much improved documentation. See +Check out the revised the improved documentation and examples: - http://pytest.org - -for details or below for some more information. + http://pytest.org/2.0.0/index.html Thanks to all issue reporters and people asking questions or complaining. Particular thanks to Floris Bruynooghe and Ronny Pfannschmidt -for their great coding contributions. +for their great coding contributions and may others for feedback and help. best, -holger +holger krekel New Features @@ -30,7 +29,7 @@ New Features or from a python program:: - import pytest ; pytest.main(args, plugins) + import pytest ; pytest.main(arglist, pluginlist) see http://pytest.org/2.0.0/usage.html for details. @@ -48,14 +47,17 @@ New Features see http://pytest.org/2.0.0/customize.html -- improved standard unittest support. For example you can now run - the tests of an installed 'unittest' package with py.test:: +- improved standard unittest support. In general py.test should now + better run custom TestCases like twisted trial or Django based + TestCases. Also you can now run the tests of an installed + 'unittest' package with py.test:: py.test --pyargs unittest - new "-q" option which decreases verbosity and prints a more nose/unittest-style "dot" output. +- many more improvements in details Fixes ----------------------- @@ -72,14 +74,24 @@ Fixes - fix bug: unittest collected functions now also can have "pytestmark" applied at class/module level -Important Note on importing "pytest" versus "py.test" -------------------------------------------------------- +Notes +-------------------- -The usual way in pre-2.0 times to use py.test in python code was -to import "py" and then e.g. use "py.test.raises" for the helper. -This remains valid and is not planned to be deprecated. However, -in most examples and internal code you'll find "import pytest" -and "pytest.raises" used as the recommended default way. +* The usual way in pre-2.0 times to use py.test in python code was + to import "py" and then e.g. use "py.test.raises" for the helper. + This remains valid and is not planned to be deprecated. However, + in most examples and internal code you'll find "import pytest" + and "pytest.raises" used as the recommended default way. + +* pytest now first performs collection of the complete test suite + before running any test. This changes for example the semantics of when + pytest_collectstart/pytest_collectreport are called. Some plugins may + need upgrading. + +* The pytest package consists of a 400 LOC core.py and about 20 builtin plugins, + summing up to roughly 5000 LOCs, including docstrings. To be fair, it also + uses generic code from the "pylib", and the new "py" package to help + with filesystem and introspection/code manipulation. (Incompatible) Removals ----------------------------- @@ -99,7 +111,7 @@ and "pytest.raises" used as the recommen - py.test.collect.Directory does not exist anymore and it is not possible to provide an own "Directory" object. - If you have used this and don#t know what to do, get + If you have used this and don't know what to do, get in contact. We'll figure someting out. Note that pytest_collect_directory() is still called but From commits-noreply at bitbucket.org Sat Nov 20 21:36:58 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 20 Nov 2010 14:36:58 -0600 (CST) Subject: [py-svn] pytest commit 98e74a494cb2: fix: mark.* objects are now immutable as long as they are not an attribute on a function, enables usage like this:: Message-ID: <20101120203658.98D291E12B7@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290280658 -3600 # Node ID 98e74a494cb2d96e45a4bbcf04e764229ccd0b30 # Parent d633f6bed7afd820ddc0c6e560f5b63669ab1e60 fix: mark.* objects are now immutable as long as they are not an attribute on a function, enables usage like this:: xfail = pytest.mark.xfail @xfail def test_func1(): pass @xfail(reason="123") def test_func2(): pass where previously test_func1 and test_func2 would wrongly share the same reason because the xfail object was modified in place. --- a/testing/test_mark.py +++ b/testing/test_mark.py @@ -48,6 +48,21 @@ class TestMark: assert f.world.args[0] == "hello" mark.world("world")(f) + def test_pytest_mark_reuse(self): + mark = Mark() + def f(): + pass + w = mark.some + w("hello", reason="123")(f) + assert f.some.args[0] == "hello" + assert f.some.kwargs['reason'] == "123" + def g(): + pass + w("world", reason2="456")(g) + assert g.some.args[0] == "world" + assert 'reason' not in g.some.kwargs + assert g.some.kwargs['reason2'] == "456" + class TestFunctional: def test_mark_per_function(self, testdir): p = testdir.makepyfile(""" @@ -136,7 +151,7 @@ class TestFunctional: item, = items keywords = item.keywords marker = keywords['hello'] - assert marker.args == ["pos0", "pos1"] + assert marker.args == ("pos0", "pos1") assert marker.kwargs == {'x': 3, 'y': 2, 'z': 4} def test_mark_other(self, testdir): --- a/_pytest/mark.py +++ b/_pytest/mark.py @@ -101,10 +101,10 @@ class MarkDecorator: def test_function(): pass """ - def __init__(self, name): + def __init__(self, name, args=None, kwargs=None): self.markname = name - self.kwargs = {} - self.args = [] + self.args = args or () + self.kwargs = kwargs or {} def __repr__(self): d = self.__dict__.copy() @@ -134,12 +134,12 @@ class MarkDecorator: setattr(func, self.markname, holder) else: holder.kwargs.update(self.kwargs) - holder.args.extend(self.args) + holder.args += self.args return func - else: - self.args.extend(args) - self.kwargs.update(kwargs) - return self + kw = self.kwargs.copy() + kw.update(kwargs) + args = self.args + args + return self.__class__(self.markname, args=args, kwargs=kw) class MarkInfo: """ Marking object created by :class:`MarkDecorator` instances. """ --- a/CHANGELOG +++ b/CHANGELOG @@ -38,7 +38,7 @@ Changes between 1.3.4 and 2.0.0dev0 - fix bug: unittest collected functions now also can have "pytestmark" applied at class/module level - add ability to use "class" level for cached_setup helper - +- fix strangeness: mark.* objects are now immutable, create new instances Changes between 1.3.3 and 1.3.4 ---------------------------------------------- From commits-noreply at bitbucket.org Sat Nov 20 21:36:59 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sat, 20 Nov 2010 14:36:59 -0600 (CST) Subject: [py-svn] pytest commit f6fe6e272106: merging and refining examples, also refining skipping documentation. Message-ID: <20101120203659.179381E133A@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290285355 -3600 # Node ID f6fe6e272106d390b43fc85a043cd7fa4cd2c449 # Parent 98e74a494cb2d96e45a4bbcf04e764229ccd0b30 merging and refining examples, also refining skipping documentation. --- a/example/genhtmlcss.py +++ /dev/null @@ -1,23 +0,0 @@ -import py -html = py.xml.html - -class my(html): - "a custom style" - class body(html.body): - style = html.Style(font_size = "120%") - - class h2(html.h2): - style = html.Style(background = "grey") - - class p(html.p): - style = html.Style(font_weight="bold") - -doc = my.html( - my.head(), - my.body( - my.h2("hello world"), - my.p("bold as bold can") - ) -) - -print doc.unicode(indent=2) --- /dev/null +++ b/doc/example/costlysetup/sub2/__init__.py @@ -0,0 +1,1 @@ +# --- a/example/assertion/test_failures.py +++ /dev/null @@ -1,13 +0,0 @@ - -import py -failure_demo = py.path.local(__file__).dirpath('failure_demo.py') - -def test_failure_demo_fails_properly(testdir): - target = testdir.tmpdir.join(failure_demo.basename) - failure_demo.copy(target) - failure_demo.copy(testdir.tmpdir.join(failure_demo.basename)) - result = testdir.runpytest(target) - result.stdout.fnmatch_lines([ - "*35 failed*" - ]) - assert result.ret != 0 --- /dev/null +++ b/doc/example/pythoncollection.py @@ -0,0 +1,11 @@ + +# run this with $ py.test --collectonly test_collectonly.py +# +def test_function(): + pass + +class TestClass: + def test_method(self): + pass + def test_anothermethod(self): + pass --- /dev/null +++ b/doc/example/costlysetup/sub1/test_quick.py @@ -0,0 +1,3 @@ + +def test_quick(setup): + pass --- a/doc/example/builtin.txt +++ /dev/null @@ -1,36 +0,0 @@ - -writing well integrated assertion helpers -======================================================== - -If you have a test helper function called from a test you can -use the ``pytest.fail`` marker to fail a test with a certain message. -The test support function will not show up in the traceback if you -set the ``__tracebackhide__`` option somewhere in the helper function. -Example:: - - # content of test_checkconfig.py - import pytest - def checkconfig(x): - __tracebackhide__ = True - if not hasattr(x, "config"): - pytest.fail("not configured: %s" %(x,)) - - def test_something(): - checkconfig(42) - -The ``__tracebackhide__`` setting influences py.test showing -of tracebacks: the ``checkconfig`` function will not be shown -unless the ``--fulltrace`` command line option is specified. -Let's run our little function:: - - $ py.test -q - F - ================================= FAILURES ================================= - ______________________________ test_something ______________________________ - - def test_something(): - > checkconfig(42) - E Failed: not configured: 42 - - test_checkconfig.py:8: Failed - 1 failed in 0.02 seconds --- a/example/assertion/global_testmodule_config/test_hello.py +++ /dev/null @@ -1,5 +0,0 @@ - -hello = "world" - -def test_func(): - pass --- a/example/genhtml.py +++ /dev/null @@ -1,13 +0,0 @@ -from py.xml import html - -paras = "First Para", "Second para" - -doc = html.html( - html.head( - html.meta(name="Content-Type", value="text/html; charset=latin1")), - html.body( - [html.p(p) for p in paras])) - -print unicode(doc).encode('latin1') - - --- /dev/null +++ b/doc/example/parametrize.txt @@ -0,0 +1,142 @@ + +parametrizing tests +================================================= + +py.test allows to easily implement your own custom +parametrization scheme for tests. Here we provide +some examples for inspiration and re-use. + +Parametrizing test methods through per-class configuration +-------------------------------------------------------------- + +.. _`unittest parameterizer`: http://code.google.com/p/unittest-ext/source/browse/trunk/params.py + +Here is an example ``pytest_generate_function`` function implementing a +parametrization scheme similar to Michael Foords `unittest +parameterizer`_ in a lot less code:: + + # content of ./test_parametrize.py + import pytest + + def pytest_generate_tests(metafunc): + # called once per each test function + for funcargs in metafunc.cls.params[metafunc.function.__name__]: + # schedule a new test function run with applied **funcargs + metafunc.addcall(funcargs=funcargs) + + class TestClass: + # a map specifying multiple argument sets for a test method + params = { + 'test_equals': [dict(a=1, b=2), dict(a=3, b=3), ], + 'test_zerodivision': [dict(a=1, b=0), dict(a=3, b=2)], + } + + def test_equals(self, a, b): + assert a == b + + def test_zerodivision(self, a, b): + pytest.raises(ZeroDivisionError, "a/b") + +Running it means we are two tests for each test functions, using +the respective settings:: + + $ py.test -q + F..F + ================================= FAILURES ================================= + _________________________ TestClass.test_equals[0] _________________________ + + self = , a = 1, b = 2 + + def test_equals(self, a, b): + > assert a == b + E assert 1 == 2 + + test_parametrize.py:17: AssertionError + ______________________ TestClass.test_zerodivision[1] ______________________ + + self = , a = 3, b = 2 + + def test_zerodivision(self, a, b): + > pytest.raises(ZeroDivisionError, "a/b") + E Failed: DID NOT RAISE + + test_parametrize.py:20: Failed + 2 failed, 2 passed in 0.03 seconds + +Parametrizing test methods through a decorator +-------------------------------------------------------------- + +Modifying the previous example we can also allow decorators +for parametrizing test methods:: + + # content of test_parametrize2.py + + import pytest + + # test support code + def params(funcarglist): + def wrapper(function): + function.funcarglist = funcarglist + return function + return wrapper + + def pytest_generate_tests(metafunc): + for funcargs in getattr(metafunc.function, 'funcarglist', ()): + metafunc.addcall(funcargs=funcargs) + + # actual test code + class TestClass: + @params([dict(a=1, b=2), dict(a=3, b=3), ]) + def test_equals(self, a, b): + assert a == b + + @params([dict(a=1, b=0), dict(a=3, b=2)]) + def test_zerodivision(self, a, b): + pytest.raises(ZeroDivisionError, "a/b") + +Running it gives similar results as before:: + + $ py.test -q test_parametrize2.py + F..F + ================================= FAILURES ================================= + _________________________ TestClass.test_equals[0] _________________________ + + self = , a = 1, b = 2 + + @params([dict(a=1, b=2), dict(a=3, b=3), ]) + def test_equals(self, a, b): + > assert a == b + E assert 1 == 2 + + test_parametrize2.py:19: AssertionError + ______________________ TestClass.test_zerodivision[1] ______________________ + + self = , a = 3, b = 2 + + @params([dict(a=1, b=0), dict(a=3, b=2)]) + def test_zerodivision(self, a, b): + > pytest.raises(ZeroDivisionError, "a/b") + E Failed: DID NOT RAISE + + test_parametrize2.py:23: Failed + 2 failed, 2 passed in 0.03 seconds + +checking serialization between Python interpreters +-------------------------------------------------------------- + +Here is a stripped down real-life example of using parametrized +testing for testing serialization betwee different interpreters. +We define a ``test_basic_objects`` function which is to be run +with different sets of arguments for its three arguments:: + +* ``python1``: first python interpreter +* ``python2``: second python interpreter +* ``obj``: object to be dumped from first interpreter and loaded into second interpreter + +.. literalinclude:: multipython.py + +Running it (with Python-2.4 through to Python2.7 installed):: + + . $ py.test -q multipython.py + ....s....s....s....ssssss....s....s....s....ssssss....s....s....s....ssssss + 48 passed, 27 skipped in 2.55 seconds --- a/example/funcarg/mysetup2/__init__.py +++ /dev/null @@ -1,1 +0,0 @@ -# XXX this file should not need to be here but is here for proper sys.path mangling --- a/doc/skipping.txt +++ b/doc/skipping.txt @@ -2,18 +2,19 @@ skip and xfail mechanisms ===================================================================== -You can mark test functions for a conditional *skip* or as *xfail*, -expected-to-fail. Skipping a test avoids running a test. -Whereas an xfail-marked test usually is run but if it fails it is -not reported in detail and counted separately. The latter allows -to keep track of real implementation problems whereas test skips -are normally tied to a condition, such as a platform or dependency -requirement without which considering or running the test does -not make sense. If a test fails under all conditions then it's -probably best to mark your test as 'xfail'. +You can skip or "xfail" test functions, either by marking functions +through a decorator or by calling the ``pytest.skip|xfail`` helpers. +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 you expect your test to fail because there is an +implementation problem. Counting and listing *xfailing* tests separately +helps to maintain a list of implementation problems and you can provide +info such as a bug number or a URL to provide a human readable problem context. -By running ``py.test -rxs`` you will see extra reporting -information on skips and xfail-run tests at the end of a test run. +Usually detailed information about skipped/xfailed tests is not shown +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 xfail tests .. _skipif: @@ -47,7 +48,7 @@ at module level like this:: ... -skip groups of test functions +skip test functions of a class -------------------------------------- As with all function :ref:`marking` you can do it at @@ -58,8 +59,7 @@ for skipping all methods of a test class pytestmark = pytest.mark.skipif("sys.platform == 'win32'") def test_function(self): - # will not be setup or run under 'win32' platform - # + "will not be setup or run under 'win32' platform" The ``pytestmark`` decorator will be applied to each test function. If your code targets python2.6 or above you can equivalently use @@ -69,8 +69,7 @@ the skipif decorator on classes:: class TestPosixCalls: def test_function(self): - # will not be setup or run under 'win32' platform - # + "will not be setup or run under 'win32' platform" It is fine in general to apply multiple "skipif" decorators on a single function - this means that if any of the conditions @@ -94,6 +93,13 @@ This test will be run but no traceback w when it fails. Instead terminal reporting will list it in the "expected to fail" or "unexpectedly passing" sections. +By specifying on the commandline:: + + pytest --runxfail + +you can force the running and reporting of an ``xfail`` marked test +as if it weren't marked at all. + Same as with skipif_ you can also selectively expect a failure depending on platform:: @@ -101,19 +107,32 @@ depending on platform:: def test_function(): ... -To not run a test and still regard it as "xfailed":: +You can also avoid running an "xfail" test at all or +specify a reason such as a bug ID or similar. Here is +a simple test file with usages: - @pytest.mark.xfail(..., run=False) +.. literalinclude:: example/xfail_demo.py -To specify an explicit reason to be shown with xfailure detail:: +Running it with the report-on-xfail option gives this output:: - @pytest.mark.xfail(..., reason="my reason") - -By specifying on the commandline:: - - pytest --runxfail - -you can force the running and reporting of a runnable ``xfail`` marked test. + example $ py.test -rx xfail_demo.py + =========================== test session starts ============================ + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev31 + test path 1: xfail_demo.py + + xfail_demo.py xxxxx + ========================= short test summary info ========================== + XFAIL xfail_demo.py::test_hello + XFAIL xfail_demo.py::test_hello2 + reason: [NOTRUN] + XFAIL xfail_demo.py::test_hello3 + condition: hasattr(os, 'sep') + XFAIL xfail_demo.py::test_hello4 + bug 110 + XFAIL xfail_demo.py::test_hello5 + reason: reason + + ======================== 5 xfailed in 0.04 seconds ========================= imperative xfail from within a test or setup function ------------------------------------------------------ --- a/example/funcarg/urloption/conftest.py +++ /dev/null @@ -1,15 +0,0 @@ -# conftest.py -import py - - -def pytest_addoption(parser): - grp = parser.getgroup("testserver options") - grp.addoption("--url", action="store", default=None, - help="url for testserver") - -def pytest_funcarg__url(request): - url = request.config.getvalue("url") - if url is None: - py.test.skip("need --url") - return url - --- a/example/funcarg/test_simpleprovider.py +++ /dev/null @@ -1,7 +0,0 @@ -# ./test_simpleprovider.py -def pytest_funcarg__myfuncarg(request): - return 42 - -def test_function(myfuncarg): - assert myfuncarg == 17 - --- a/example/funcarg/costlysetup/sub2/__init__.py +++ /dev/null @@ -1,1 +0,0 @@ -# --- /dev/null +++ b/doc/pytest.ini @@ -0,0 +1,2 @@ +[pytest] +# just defined to prevent the root level tox.ini to kick in --- a/example/funcarg/costlysetup/conftest.py +++ /dev/null @@ -1,16 +0,0 @@ - -def pytest_funcarg__setup(request): - return request.cached_setup( - setup=lambda: CostlySetup(), - teardown=lambda costlysetup: costlysetup.finalize(), - scope="session", - ) - -class CostlySetup: - def __init__(self): - import time - time.sleep(5) - self.timecostly = 1 - - def finalize(self): - del self.timecostly --- a/doc/example/xunit_setup.txt +++ /dev/null @@ -1,74 +0,0 @@ -Learning by examples -===================== - -adding custom options ----------------------- - -py.test supports adding of standard optparse_ Options. -A plugin may implement the ``addoption`` hook for registering -custom options:: - - def pytest_addoption(parser): - parser.addoption("-M", "--myopt", action="store", - help="specify string to set myopt") - - def pytest_configure(config): - if config.option.myopt: - # do action based on option value - # - -.. _optparse: http://docs.python.org/library/optparse.html - -order of setup/teardown module/class/item methods -==================================================== - -managing state at module, class and method level ------------------------------------------------------------- - -Here is a working example for what goes on when you setup modules, -classes and methods:: - - # [[from py/documentation/example/pytest/test_setup_flow_example.py]] - - def setup_module(module): - module.TestStateFullThing.classcount = 0 - - class TestStateFullThing: - def setup_class(cls): - cls.classcount += 1 - - def teardown_class(cls): - cls.classcount -= 1 - - def setup_method(self, method): - self.id = eval(method.func_name[5:]) - - def test_42(self): - assert self.classcount == 1 - assert self.id == 42 - - def test_23(self): - assert self.classcount == 1 - assert self.id == 23 - - def teardown_module(module): - assert module.TestStateFullThing.classcount == 0 - -For this example the control flow happens as follows:: - - import test_setup_flow_example - setup_module(test_setup_flow_example) - setup_class(TestStateFullThing) - instance = TestStateFullThing() - setup_method(instance, instance.test_42) - instance.test_42() - setup_method(instance, instance.test_23) - instance.test_23() - teardown_class(TestStateFullThing) - teardown_module(test_setup_flow_example) - -Note that ``setup_class(TestStateFullThing)`` is called and not -``TestStateFullThing.setup_class()`` which would require you -to insert ``setup_class = classmethod(setup_class)`` to make -your setup function callable. Did we mention that lazyness -is a virtue? --- a/example/funcarg/mysetup2/myapp.py +++ /dev/null @@ -1,5 +0,0 @@ - -class MyApp: - def question(self): - return 6 * 9 - --- a/example/funcarg/parametrize/test_parametrize.py +++ /dev/null @@ -1,17 +0,0 @@ -import py - -def pytest_generate_tests(metafunc): - for funcargs in metafunc.cls.params[metafunc.function.__name__]: - metafunc.addcall(funcargs=funcargs) - -class TestClass: - params = { - 'test_equals': [dict(a=1, b=2), dict(a=3, b=3), dict(a=5, b=4)], - 'test_zerodivision': [dict(a=1, b=0), dict(a=3, b=2)], - } - - def test_equals(self, a, b): - assert a == b - - def test_zerodivision(self, a, b): - py.test.raises(ZeroDivisionError, "a/b") --- /dev/null +++ b/doc/example/assertion/global_testmodule_config/test_hello.py @@ -0,0 +1,5 @@ + +hello = "world" + +def test_func(): + pass --- a/example/genxml.py +++ /dev/null @@ -1,17 +0,0 @@ - -import py -class ns(py.xml.Namespace): - pass - -doc = ns.books( - ns.book( - ns.author("May Day"), - ns.title("python for java programmers"),), - ns.book( - ns.author("why", class_="somecssclass"), - ns.title("Java for Python programmers"),), - publisher="N.N", - ) -print doc.unicode(indent=2).encode('utf8') - - --- a/doc/plugins.txt +++ b/doc/plugins.txt @@ -65,7 +65,7 @@ tool, for example:: If a plugin is installed, py.test automatically finds and integrates it, there is no need to activate it. If you don't need a plugin anymore simply -de-install it. You can find a list of valid plugins through a +de-install it. You can find a list of available plugins through a `pytest- pypi.python.org search`_. .. _`available installable plugins`: --- a/example/xfail_demo.py +++ /dev/null @@ -1,16 +0,0 @@ -import py - - at py.test.mark.xfail -def test_hello(): - assert 0 - - at py.test.mark.xfail(run=False) -def test_hello2(): - assert 0 - - at py.test.mark.xfail("hasattr(os, 'sep')") -def test_hello3(): - assert 0 - -def test_hello5(): - py.test.xfail("reason") --- a/example/funcarg/conftest.py +++ /dev/null @@ -1,3 +0,0 @@ -import py - -collect_ignore = 'mysetup', 'mysetup2', 'test_simpleprovider.py', 'parametrize' --- /dev/null +++ b/doc/example/assertion/test_setup_flow_example.py @@ -0,0 +1,42 @@ +def setup_module(module): + module.TestStateFullThing.classcount = 0 + +class TestStateFullThing: + def setup_class(cls): + cls.classcount += 1 + + def teardown_class(cls): + cls.classcount -= 1 + + def setup_method(self, method): + self.id = eval(method.__name__[5:]) + + def test_42(self): + assert self.classcount == 1 + assert self.id == 42 + + def test_23(self): + assert self.classcount == 1 + assert self.id == 23 + +def teardown_module(module): + assert module.TestStateFullThing.classcount == 0 + +""" For this example the control flow happens as follows:: + import test_setup_flow_example + setup_module(test_setup_flow_example) + setup_class(TestStateFullThing) + instance = TestStateFullThing() + setup_method(instance, instance.test_42) + instance.test_42() + setup_method(instance, instance.test_23) + instance.test_23() + teardown_class(TestStateFullThing) + teardown_module(test_setup_flow_example) + +Note that ``setup_class(TestStateFullThing)`` is called and not +``TestStateFullThing.setup_class()`` which would require you +to insert ``setup_class = classmethod(setup_class)`` to make +your setup function callable. +""" + --- a/example/funcarg/costlysetup/sub1/test_quick.py +++ /dev/null @@ -1,3 +0,0 @@ - -def test_quick(): - pass --- a/example/funcarg/mysetup/myapp.py +++ /dev/null @@ -1,5 +0,0 @@ - -class MyApp: - def question(self): - return 6 * 9 - --- a/example/funcarg/parametrize/test_parametrize2.py +++ /dev/null @@ -1,25 +0,0 @@ -import py - -# test support code -def params(funcarglist): - def wrapper(function): - function.funcarglist = funcarglist - return function - return wrapper - -def pytest_generate_tests(metafunc): - for funcargs in getattr(metafunc.function, 'funcarglist', ()): - metafunc.addcall(funcargs=funcargs) - - -# actual test code - -class TestClass: - @params([dict(a=1, b=2), dict(a=3, b=3), dict(a=5, b=4)], ) - def test_equals(self, a, b): - assert a == b - - @params([dict(a=1, b=0), dict(a=3, b=2)]) - def test_zerodivision(self, a, b): - py.test.raises(ZeroDivisionError, "a/b") - --- a/doc/example/detectpytest.txt +++ /dev/null @@ -1,29 +0,0 @@ - -Detect if running from within a py.test run --------------------------------------------------------------- - -Usually it is a bad idea to make application code -behave differently if called from a test. But if you -absolutely must find out if your application code is -running from a test you can do something like this:: - - # content of conftest.py in your testing directory - - def pytest_configure(config): - import sys - sys._called_from_test = True - - def pytest_unconfigure(config): - del sys._called_from_test - -and then check for the ``sys._called_from_test`` flag:: - - if hasattr(sys, '_called_from_test'): - # called from within a test run - else: - # called "normally" - -accordingly in your application. It's also a good idea -to rather use your own application module rather than ``sys`` -for handling flag. - --- a/example/funcarg/costlysetup/sub1/__init__.py +++ /dev/null @@ -1,1 +0,0 @@ -# --- a/example/funcarg/parametrize/test_parametrize3.py +++ /dev/null @@ -1,15 +0,0 @@ - -# following hook can be put unchanged into a local or global plugin -def pytest_generate_tests(metafunc): - for scenario in metafunc.cls.scenarios: - metafunc.addcall(id=scenario[0], funcargs=scenario[1]) - - -scenario1 = ('basic', {'attribute': 'value'}) -scenario2 = ('advanced', {'attribute': 'value2'}) - -class TestSampleWithScenarios: - scenarios = [scenario1, scenario2] - - def test_demo(self, attribute): - assert isinstance(attribute, str) --- a/doc/example/simple.txt +++ b/doc/example/simple.txt @@ -1,9 +1,26 @@ .. highlightlang:: python -simple patterns using hooks +simple hook using patterns ========================================================== +adding custom options +---------------------- + +py.test supports adding of standard optparse_ Options. +A plugin may implement the ``addoption`` hook for registering +custom options:: + + def pytest_addoption(parser): + parser.addoption("-M", "--myopt", action="store", + help="specify string to set myopt") + + def pytest_configure(config): + if config.option.myopt: + # do action based on option value + +.. _optparse: http://docs.python.org/library/optparse.html + pass different values to a test function, depending on command line options ---------------------------------------------------------------------------- @@ -134,3 +151,128 @@ let's run the full monty:: As expected when running the full range of ``param1`` values we'll get an error on the last one. + + +.. _`retrieved by hooks as item keywords`: + +control skipping of tests according to command line option +-------------------------------------------------------------- + +Here is a ``conftest.py`` file adding a ``--runslow`` command +line option to control skipping of ``slow`` marked tests:: + + # content of conftest.py + + import pytest + def pytest_addoption(parser): + parser.addoption("--runslow", action="store_true", + help="run slow tests") + + def pytest_runtest_setup(item): + if 'slow' in item.keywords and not item.config.getvalue("runslow"): + pytest.skip("need --runslow option to run") + +We can now write a test module like this:: + + # content of test_module.py + + import pytest + slow = pytest.mark.slow + + def test_func_fast(): + pass + + @slow + def test_func_slow(): + pass + +and when running it will see a skipped "slow" test:: + + $ py.test test_module.py -rs # "-rs" means report details on the little 's' + =========================== test session starts ============================ + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 + test path 1: test_module.py + + test_module.py .s + ========================= short test summary info ========================== + SKIP [1] /tmp/doc-exec-104/conftest.py:9: need --runslow option to run + + =================== 1 passed, 1 skipped in 0.02 seconds ==================== + +Or run it including the ``slow`` marked test:: + + $ py.test test_module.py --runslow + =========================== test session starts ============================ + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 + test path 1: test_module.py + + test_module.py .. + + ========================= 2 passed in 0.01 seconds ========================= + + +writing well integrated assertion helpers +-------------------------------------------------- + +If you have a test helper function called from a test you can +use the ``pytest.fail`` marker to fail a test with a certain message. +The test support function will not show up in the traceback if you +set the ``__tracebackhide__`` option somewhere in the helper function. +Example:: + + # content of test_checkconfig.py + import pytest + def checkconfig(x): + __tracebackhide__ = True + if not hasattr(x, "config"): + pytest.fail("not configured: %s" %(x,)) + + def test_something(): + checkconfig(42) + +The ``__tracebackhide__`` setting influences py.test showing +of tracebacks: the ``checkconfig`` function will not be shown +unless the ``--fulltrace`` command line option is specified. +Let's run our little function:: + + $ py.test -q + F + ================================= FAILURES ================================= + ______________________________ test_something ______________________________ + + def test_something(): + > checkconfig(42) + E Failed: not configured: 42 + + test_checkconfig.py:8: Failed + 1 failed in 0.02 seconds + + +Detect if running from within a py.test run +-------------------------------------------------------------- + +Usually it is a bad idea to make application code +behave differently if called from a test. But if you +absolutely must find out if your application code is +running from a test you can do something like this:: + + # content of conftest.py in your testing directory + + def pytest_configure(config): + import sys + sys._called_from_test = True + + def pytest_unconfigure(config): + del sys._called_from_test + +and then check for the ``sys._called_from_test`` flag:: + + if hasattr(sys, '_called_from_test'): + # called from within a test run + else: + # called "normally" + +accordingly in your application. It's also a good idea +to rather use your own application module rather than ``sys`` +for handling flag. + --- a/example/funcarg/costlysetup/sub2/test_two.py +++ /dev/null @@ -1,6 +0,0 @@ -def test_something(setup): - assert setup.timecostly == 1 - -def test_something_more(setup): - assert setup.timecostly == 1 - --- /dev/null +++ b/doc/example/xfail_demo.py @@ -0,0 +1,21 @@ +import pytest +xfail = pytest.mark.xfail + + at xfail +def test_hello(): + assert 0 + + at xfail(run=False) +def test_hello2(): + assert 0 + + at xfail("hasattr(os, 'sep')") +def test_hello3(): + assert 0 + + at xfail(reason="bug 110") +def test_hello4(): + assert 0 + +def test_hello5(): + pytest.xfail("reason") --- a/example/funcarg/mysetup/__init__.py +++ /dev/null @@ -1,1 +0,0 @@ -# XXX this file should not need to be here but is here for proper sys.path mangling --- /dev/null +++ b/doc/example/assertion/test_failures.py @@ -0,0 +1,14 @@ + +import py +failure_demo = py.path.local(__file__).dirpath('failure_demo.py') +pytest_plugins = 'pytester', + +def test_failure_demo_fails_properly(testdir): + target = testdir.tmpdir.join(failure_demo.basename) + failure_demo.copy(target) + failure_demo.copy(testdir.tmpdir.join(failure_demo.basename)) + result = testdir.runpytest(target) + result.stdout.fnmatch_lines([ + "*35 failed*" + ]) + assert result.ret != 0 --- /dev/null +++ b/doc/example/costlysetup/sub1/__init__.py @@ -0,0 +1,1 @@ +# --- a/example/funcarg/test_multi_python.py +++ /dev/null @@ -1,65 +0,0 @@ -""" - -module containing a parametrized tests testing cross-python -serialization via the pickle module. -""" -import py - -pythonlist = ['python2.3', 'python2.4', 'python2.5', 'python2.6'] -# 'jython' 'python3.1'] - -def pytest_generate_tests(metafunc): - if 'python1' in metafunc.funcargnames: - assert 'python2' in metafunc.funcargnames - for obj in metafunc.function.multiarg.kwargs['obj']: - for py1 in pythonlist: - for py2 in pythonlist: - metafunc.addcall(id="%s-%s-%s" % (py1, py2, obj), - param=(py1, py2, obj)) - - at py.test.mark.multiarg(obj=[42, {}, {1:3},]) -def test_basic_objects(python1, python2, obj): - python1.dumps(obj) - python2.load_and_is_true("obj == %s" % obj) - -def pytest_funcarg__python1(request): - tmpdir = request.getfuncargvalue("tmpdir") - picklefile = tmpdir.join("data.pickle") - return Python(request.param[0], picklefile) - -def pytest_funcarg__python2(request): - python1 = request.getfuncargvalue("python1") - return Python(request.param[1], python1.picklefile) - -def pytest_funcarg__obj(request): - return request.param[2] - -class Python: - def __init__(self, version, picklefile): - self.pythonpath = py.path.local.sysfind(version) - if not self.pythonpath: - py.test.skip("%r not found" %(version,)) - self.picklefile = picklefile - def dumps(self, obj): - dumpfile = self.picklefile.dirpath("dump.py") - dumpfile.write(py.code.Source(""" - import pickle - f = open(%r, 'wb') - s = pickle.dump(%r, f) - f.close() - """ % (str(self.picklefile), obj))) - py.process.cmdexec("%s %s" %(self.pythonpath, dumpfile)) - - def load_and_is_true(self, expression): - loadfile = self.picklefile.dirpath("load.py") - loadfile.write(py.code.Source(""" - import pickle - f = open(%r, 'rb') - obj = pickle.load(f) - f.close() - res = eval(%r) - if not res: - raise SystemExit(1) - """ % (str(self.picklefile), expression))) - print (loadfile) - py.process.cmdexec("%s %s" %(self.pythonpath, loadfile)) --- /dev/null +++ b/doc/example/assertion/failure_demo.py @@ -0,0 +1,193 @@ +from py.test import raises +import py + +def otherfunc(a,b): + assert a==b + +def somefunc(x,y): + otherfunc(x,y) + +def otherfunc_multi(a,b): + assert (a == + b) + +def test_generative(param1, param2): + assert param1 * 2 < param2 + +def pytest_generate_tests(metafunc): + if 'param1' in metafunc.funcargnames: + metafunc.addcall(funcargs=dict(param1=3, param2=6)) + +class TestFailing(object): + def test_simple(self): + def f(): + return 42 + def g(): + return 43 + + assert f() == g() + + def test_simple_multiline(self): + otherfunc_multi( + 42, + 6*9) + + def test_not(self): + def f(): + return 42 + assert not f() + + def test_complex_error(self): + def f(): + return 44 + def g(): + return 43 + somefunc(f(), g()) + + def test_z1_unpack_error(self): + l = [] + a,b = l + + def test_z2_type_error(self): + l = 3 + a,b = l + + def test_startswith(self): + s = "123" + g = "456" + assert s.startswith(g) + + def test_startswith_nested(self): + def f(): + return "123" + def g(): + return "456" + assert f().startswith(g()) + + def test_global_func(self): + assert isinstance(globf(42), float) + + def test_instance(self): + self.x = 6*7 + assert self.x != 42 + + def test_compare(self): + assert globf(10) < 5 + + def test_try_finally(self): + x = 1 + try: + assert x == 0 + finally: + x = 0 + + def test_raises(self): + s = 'qwe' + raises(TypeError, "int(s)") + + def test_raises_doesnt(self): + raises(IOError, "int('3')") + + def test_raise(self): + raise ValueError("demo error") + + def test_tupleerror(self): + a,b = [1] + + def test_reinterpret_fails_with_print_for_the_fun_of_it(self): + l = [1,2,3] + print ("l is %r" % l) + a,b = l.pop() + + def test_some_error(self): + if namenotexi: + pass + + def func1(self): + assert 41 == 42 + + +# thanks to Matthew Scott for this test +def test_dynamic_compile_shows_nicely(): + src = 'def foo():\n assert 1 == 0\n' + name = 'abc-123' + module = py.std.imp.new_module(name) + code = py.code.compile(src, name, 'exec') + py.builtin.exec_(code, module.__dict__) + py.std.sys.modules[name] = module + module.foo() + + +class TestSpecialisedExplanations(object): + def test_eq_text(self): + assert 'spam' == 'eggs' + + def test_eq_similar_text(self): + assert 'foo 1 bar' == 'foo 2 bar' + + def test_eq_multiline_text(self): + assert 'foo\nspam\nbar' == 'foo\neggs\nbar' + + def test_eq_long_text(self): + a = '1'*100 + 'a' + '2'*100 + b = '1'*100 + 'b' + '2'*100 + assert a == b + + 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 + + def test_eq_list(self): + assert [0, 1, 2] == [0, 1, 3] + + def test_eq_list_long(self): + a = [0]*100 + [1] + [3]*100 + b = [0]*100 + [2] + [3]*100 + assert a == b + + def test_eq_dict(self): + assert {'a': 0, 'b': 1} == {'a': 0, 'b': 2} + + def test_eq_set(self): + assert set([0, 10, 11, 12]) == set([0, 20, 21]) + + def test_eq_longer_list(self): + assert [1,2] == [1,2,3] + + def test_in_list(self): + assert 1 in [0, 2, 3, 4, 5] + + +def test_attribute(): + class Foo(object): + b = 1 + i = Foo() + assert i.b == 2 + + +def test_attribute_instance(): + class Foo(object): + b = 1 + assert Foo().b == 2 + + +def test_attribute_failure(): + class Foo(object): + def _get_b(self): + raise Exception('Failed to get attrib') + b = property(_get_b) + i = Foo() + assert i.b == 2 + + +def test_attribute_multiple(): + class Foo(object): + b = 1 + class Bar(object): + b = 2 + assert Foo().b == Bar().b + + +def globf(x): + return x+1 --- /dev/null +++ b/doc/example/costlysetup/sub2/test_two.py @@ -0,0 +1,6 @@ +def test_something(setup): + assert setup.timecostly == 1 + +def test_something_more(setup): + assert setup.timecostly == 1 + --- a/doc/example/collectonly.py +++ /dev/null @@ -1,11 +0,0 @@ - -# run this with $ py.test --collectonly test_collectonly.py -# -def test_function(): - pass - -class TestClass: - def test_method(self): - pass - def test_anothermethod(self): - pass --- a/example/funcarg/mysetup/conftest.py +++ /dev/null @@ -1,9 +0,0 @@ - -from mysetup.myapp import MyApp - -def pytest_funcarg__mysetup(request): - return MySetup() - -class MySetup: - def myapp(self): - return MyApp() --- a/example/funcarg/mysetup2/test_sample.py +++ /dev/null @@ -1,6 +0,0 @@ - -def test_answer(mysetup): - app = mysetup.myapp() - answer = app.question() - assert answer == 42 - --- /dev/null +++ b/doc/example/assertion/global_testmodule_config/conftest.py @@ -0,0 +1,10 @@ +import pytest, py +mydir = py.path.local(__file__).dirpath() + +def pytest_runtest_setup(item): + if isinstance(item, pytest.Function): + if not item.fspath.relto(mydir): + return + mod = item.getparent(pytest.Module).obj + if hasattr(mod, 'hello'): + py.builtin.print_("mod.hello", mod.hello) --- a/doc/example/index.txt +++ b/doc/example/index.txt @@ -10,11 +10,8 @@ need more examples or have questions. .. toctree:: :maxdepth: 2 - builtin.txt + simple.txt pythoncollection.txt - controlskip.txt mysetup.txt - detectpytest.txt + parametrize.txt nonpython.txt - simple.txt - xunit_setup.txt --- a/doc/example/pythoncollection.txt +++ b/doc/example/pythoncollection.txt @@ -21,7 +21,7 @@ their file system path and then running an ini-file and the :confval:`addopts` option you can make this change more permanently:: - # content of setup.cfg or tox.ini + # content of pytest.ini [pytest] addopts = --pyargs @@ -30,8 +30,8 @@ finding out what is collected You can always peek at the collection tree without running tests like this:: - . $ py.test --collectonly collectonly.py - + . $ py.test --collectonly pythoncollection.py + --- /dev/null +++ b/doc/example/costlysetup/conftest.py @@ -0,0 +1,17 @@ + +def pytest_funcarg__setup(request): + return request.cached_setup( + setup=lambda: CostlySetup(), + teardown=lambda costlysetup: costlysetup.finalize(), + scope="session", + ) + +class CostlySetup: + def __init__(self): + import time + print ("performing costly setup") + time.sleep(5) + self.timecostly = 1 + + def finalize(self): + del self.timecostly --- a/doc/example/controlskip.txt +++ /dev/null @@ -1,57 +0,0 @@ - -.. _`retrieved by hooks as item keywords`: - -control skipping of tests according to command line option --------------------------------------------------------------- - -Here is a ``conftest.py`` file adding a ``--runslow`` command -line option to control skipping of ``slow`` marked tests:: - - # content of conftest.py - - import pytest - def pytest_addoption(parser): - parser.addoption("--runslow", action="store_true", - help="run slow tests") - - def pytest_runtest_setup(item): - if 'slow' in item.keywords and not item.config.getvalue("runslow"): - pytest.skip("need --runslow option to run") - -We can now write a test module like this:: - - # content of test_module.py - - import pytest - slow = pytest.mark.slow - - def test_func_fast(): - pass - - @slow - def test_func_slow(): - pass - -and when running it will see a skipped "slow" test:: - - $ py.test test_module.py -rs # "-rs" means report details on the little 's' - =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_module.py - - test_module.py .s - ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-104/conftest.py:9: need --runslow option to run - - =================== 1 passed, 1 skipped in 0.02 seconds ==================== - -Or run it including the ``slow`` marked test:: - - $ py.test test_module.py --runslow - =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_module.py - - test_module.py .. - - ========================= 2 passed in 0.01 seconds ========================= --- a/example/assertion/global_testmodule_config/conftest.py +++ /dev/null @@ -1,10 +0,0 @@ -import pytest, py -mydir = py.path.local(__file__).dirpath() - -def pytest_runtest_setup(item): - if isinstance(item, pytest.Function): - if not item.fspath.relto(mydir): - return - mod = item.getparent(pytest.Module).obj - if hasattr(mod, 'hello'): - py.builtin.print_("mod.hello", mod.hello) --- /dev/null +++ b/doc/example/multipython.py @@ -0,0 +1,63 @@ +""" +module containing a parametrized tests testing cross-python +serialization via the pickle module. +""" +import py + +pythonlist = ['python2.4', 'python2.5', 'python2.6', 'python2.7', 'python2.8'] + +def pytest_generate_tests(metafunc): + if 'python1' in metafunc.funcargnames: + assert 'python2' in metafunc.funcargnames + for obj in metafunc.function.multiarg.kwargs['obj']: + for py1 in pythonlist: + for py2 in pythonlist: + metafunc.addcall(id="%s-%s-%s" % (py1, py2, obj), + param=(py1, py2, obj)) + + at py.test.mark.multiarg(obj=[42, {}, {1:3},]) +def test_basic_objects(python1, python2, obj): + python1.dumps(obj) + python2.load_and_is_true("obj == %s" % obj) + +def pytest_funcarg__python1(request): + tmpdir = request.getfuncargvalue("tmpdir") + picklefile = tmpdir.join("data.pickle") + return Python(request.param[0], picklefile) + +def pytest_funcarg__python2(request): + python1 = request.getfuncargvalue("python1") + return Python(request.param[1], python1.picklefile) + +def pytest_funcarg__obj(request): + return request.param[2] + +class Python: + def __init__(self, version, picklefile): + self.pythonpath = py.path.local.sysfind(version) + if not self.pythonpath: + py.test.skip("%r not found" %(version,)) + self.picklefile = picklefile + def dumps(self, obj): + dumpfile = self.picklefile.dirpath("dump.py") + dumpfile.write(py.code.Source(""" + import pickle + f = open(%r, 'wb') + s = pickle.dump(%r, f) + f.close() + """ % (str(self.picklefile), obj))) + py.process.cmdexec("%s %s" %(self.pythonpath, dumpfile)) + + def load_and_is_true(self, expression): + loadfile = self.picklefile.dirpath("load.py") + loadfile.write(py.code.Source(""" + import pickle + f = open(%r, 'rb') + obj = pickle.load(f) + f.close() + res = eval(%r) + if not res: + raise SystemExit(1) + """ % (str(self.picklefile), expression))) + print (loadfile) + py.process.cmdexec("%s %s" %(self.pythonpath, loadfile)) --- a/example/assertion/test_setup_flow_example.py +++ /dev/null @@ -1,42 +0,0 @@ -def setup_module(module): - module.TestStateFullThing.classcount = 0 - -class TestStateFullThing: - def setup_class(cls): - cls.classcount += 1 - - def teardown_class(cls): - cls.classcount -= 1 - - def setup_method(self, method): - self.id = eval(method.__name__[5:]) - - def test_42(self): - assert self.classcount == 1 - assert self.id == 42 - - def test_23(self): - assert self.classcount == 1 - assert self.id == 23 - -def teardown_module(module): - assert module.TestStateFullThing.classcount == 0 - -""" For this example the control flow happens as follows:: - import test_setup_flow_example - setup_module(test_setup_flow_example) - setup_class(TestStateFullThing) - instance = TestStateFullThing() - setup_method(instance, instance.test_42) - instance.test_42() - setup_method(instance, instance.test_23) - instance.test_23() - teardown_class(TestStateFullThing) - teardown_module(test_setup_flow_example) - -Note that ``setup_class(TestStateFullThing)`` is called and not -``TestStateFullThing.setup_class()`` which would require you -to insert ``setup_class = classmethod(setup_class)`` to make -your setup function callable. -""" - --- a/doc/announce/index.txt +++ b/doc/announce/index.txt @@ -6,6 +6,10 @@ Release announcements :maxdepth: 2 release-2.0.0 + +.. toctree:: + :hidden: + release-1.3.4 release-1.3.3 release-1.3.2 --- a/example/funcarg/mysetup2/conftest.py +++ /dev/null @@ -1,24 +0,0 @@ -import py -from mysetup2.myapp import MyApp - -def pytest_funcarg__mysetup(request): - return MySetup(request) - -def pytest_addoption(parser): - parser.addoption("--ssh", action="store", default=None, - help="specify ssh host to run tests with") - - -class MySetup: - def __init__(self, request): - self.config = request.config - - def myapp(self): - return MyApp() - - def getsshconnection(self): - host = self.config.option.ssh - if host is None: - py.test.skip("specify ssh host with --ssh") - return execnet.SshGateway(host) - --- a/example/funcarg/mysetup2/test_ssh.py +++ /dev/null @@ -1,5 +0,0 @@ - -class TestClass: - def test_function(self, mysetup): - conn = mysetup.getsshconnection() - # work with conn --- a/example/assertion/failure_demo.py +++ /dev/null @@ -1,193 +0,0 @@ -from py.test import raises -import py - -def otherfunc(a,b): - assert a==b - -def somefunc(x,y): - otherfunc(x,y) - -def otherfunc_multi(a,b): - assert (a == - b) - -def test_generative(param1, param2): - assert param1 * 2 < param2 - -def pytest_generate_tests(metafunc): - if 'param1' in metafunc.funcargnames: - metafunc.addcall(funcargs=dict(param1=3, param2=6)) - -class TestFailing(object): - def test_simple(self): - def f(): - return 42 - def g(): - return 43 - - assert f() == g() - - def test_simple_multiline(self): - otherfunc_multi( - 42, - 6*9) - - def test_not(self): - def f(): - return 42 - assert not f() - - def test_complex_error(self): - def f(): - return 44 - def g(): - return 43 - somefunc(f(), g()) - - def test_z1_unpack_error(self): - l = [] - a,b = l - - def test_z2_type_error(self): - l = 3 - a,b = l - - def test_startswith(self): - s = "123" - g = "456" - assert s.startswith(g) - - def test_startswith_nested(self): - def f(): - return "123" - def g(): - return "456" - assert f().startswith(g()) - - def test_global_func(self): - assert isinstance(globf(42), float) - - def test_instance(self): - self.x = 6*7 - assert self.x != 42 - - def test_compare(self): - assert globf(10) < 5 - - def test_try_finally(self): - x = 1 - try: - assert x == 0 - finally: - x = 0 - - def test_raises(self): - s = 'qwe' - raises(TypeError, "int(s)") - - def test_raises_doesnt(self): - raises(IOError, "int('3')") - - def test_raise(self): - raise ValueError("demo error") - - def test_tupleerror(self): - a,b = [1] - - def test_reinterpret_fails_with_print_for_the_fun_of_it(self): - l = [1,2,3] - print ("l is %r" % l) - a,b = l.pop() - - def test_some_error(self): - if namenotexi: - pass - - def func1(self): - assert 41 == 42 - - -# thanks to Matthew Scott for this test -def test_dynamic_compile_shows_nicely(): - src = 'def foo():\n assert 1 == 0\n' - name = 'abc-123' - module = py.std.imp.new_module(name) - code = py.code.compile(src, name, 'exec') - py.builtin.exec_(code, module.__dict__) - py.std.sys.modules[name] = module - module.foo() - - -class TestSpecialisedExplanations(object): - def test_eq_text(self): - assert 'spam' == 'eggs' - - def test_eq_similar_text(self): - assert 'foo 1 bar' == 'foo 2 bar' - - def test_eq_multiline_text(self): - assert 'foo\nspam\nbar' == 'foo\neggs\nbar' - - def test_eq_long_text(self): - a = '1'*100 + 'a' + '2'*100 - b = '1'*100 + 'b' + '2'*100 - assert a == b - - 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 - - def test_eq_list(self): - assert [0, 1, 2] == [0, 1, 3] - - def test_eq_list_long(self): - a = [0]*100 + [1] + [3]*100 - b = [0]*100 + [2] + [3]*100 - assert a == b - - def test_eq_dict(self): - assert {'a': 0, 'b': 1} == {'a': 0, 'b': 2} - - def test_eq_set(self): - assert set([0, 10, 11, 12]) == set([0, 20, 21]) - - def test_eq_longer_list(self): - assert [1,2] == [1,2,3] - - def test_in_list(self): - assert 1 in [0, 2, 3, 4, 5] - - -def test_attribute(): - class Foo(object): - b = 1 - i = Foo() - assert i.b == 2 - - -def test_attribute_instance(): - class Foo(object): - b = 1 - assert Foo().b == 2 - - -def test_attribute_failure(): - class Foo(object): - def _get_b(self): - raise Exception('Failed to get attrib') - b = property(_get_b) - i = Foo() - assert i.b == 2 - - -def test_attribute_multiple(): - class Foo(object): - b = 1 - class Bar(object): - b = 2 - assert Foo().b == Bar().b - - -def globf(x): - return x+1 --- a/example/funcarg/mysetup/test_sample.py +++ /dev/null @@ -1,5 +0,0 @@ - -def test_answer(mysetup): - app = mysetup.myapp() - answer = app.question() - assert answer == 42 From commits-noreply at bitbucket.org Sun Nov 21 17:55:19 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 21 Nov 2010 10:55:19 -0600 (CST) Subject: [py-svn] pytest commit 53915e1ea006: refine tmpdir handling and docs Message-ID: <20101121165519.5F6F91E1276@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290357798 -3600 # Node ID 53915e1ea00656d7685dc2a500b61b3fae350e41 # Parent f6fe6e272106d390b43fc85a043cd7fa4cd2c449 refine tmpdir handling and docs - clear tmpdir specified with --basetemp - remove config.mktmp and config.getbasetemp methods --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -33,16 +33,6 @@ class TestGeneralUsage: '*1 passed*', ]) - def test_basetemp(self, testdir): - mytemp = testdir.tmpdir.mkdir("mytemp") - p = testdir.makepyfile(""" - def test_1(pytestconfig): - pytestconfig.getbasetemp().ensure("hello") - """) - result = testdir.runpytest(p, '--basetemp=%s' % mytemp) - assert result.ret == 0 - assert mytemp.join('hello').check() - def test_assertion_magic(self, testdir): p = testdir.makepyfile(""" def test_this(): --- a/doc/index.txt +++ b/doc/index.txt @@ -5,8 +5,8 @@ py.test: no-boilerplate testing with Pyt .. note:: version 2.0 introduces ``pytest`` as the main Python import name - but for compatibility reasons you can continue to use ``import py`` - and ``py.test.XYZ`` to access :ref:`pytest helpers` in your test code. + in examples but you can continue to use ``import py`` and + ``py.test.XYZ`` to access :ref:`pytest helpers` in your test code. Welcome to ``py.test`` documentation: --- a/_pytest/tmpdir.py +++ b/_pytest/tmpdir.py @@ -1,17 +1,61 @@ """ support for providing temporary directories to test functions. """ import pytest, py +from _pytest.monkeypatch import monkeypatch -def pytest_configure(config): - def ensuretemp(string, dir=1): +class TempdirHandler: + def __init__(self, config): + self.config = config + self.trace = config.trace.get("tmpdir") + + def ensuretemp(self, string, dir=1): """ (deprecated) return temporary directory path with the given string as the trailing part. It is usually - better to use the 'tmpdir' function argument which will - take care to provide empty unique directories for each - test call even if the test is called multiple times. + better to use the 'tmpdir' function argument which + provides an empty unique-per-test-invocation directory + and is guaranteed to be empty. """ #py.log._apiwarn(">1.1", "use tmpdir function argument") - return config.ensuretemp(string, dir=dir) - pytest.ensuretemp = ensuretemp + return self.getbasetemp().ensure(string, dir=dir) + + def mktemp(self, basename, numbered=True): + basetemp = self.getbasetemp() + if not numbered: + p = basetemp.mkdir(basename) + else: + p = py.path.local.make_numbered_dir(prefix=basename, + keep=0, rootdir=basetemp, lock_timeout=None) + self.trace("mktemp", p) + return p + + def getbasetemp(self): + """ return base temporary directory. """ + try: + return self._basetemp + except AttributeError: + basetemp = self.config.option.basetemp + if basetemp: + basetemp = py.path.local(basetemp) + if basetemp.check(): + basetemp.remove() + basetemp.mkdir() + else: + basetemp = py.path.local.make_numbered_dir(prefix='pytest-') + self._basetemp = t = basetemp + self.trace("new basetemp", t) + return t + + def finish(self): + self.trace("finish") + +def pytest_configure(config): + config._mp = mp = monkeypatch() + t = TempdirHandler(config) + mp.setattr(config, '_tmpdirhandler', t, raising=False) + mp.setattr(pytest, 'ensuretemp', t.ensuretemp, raising=False) + +def pytest_unconfigure(config): + config._tmpdirhandler.finish() + config._mp.undo() def pytest_funcarg__tmpdir(request): """return a temporary directory path object @@ -22,5 +66,6 @@ def pytest_funcarg__tmpdir(request): """ name = request._pyfuncitem.name name = py.std.re.sub("[\W]", "_", name) - x = request.config.mktemp(name, numbered=True) + x = request.config._tmpdirhandler.mktemp(name, numbered=True) return x.realpath() + --- a/doc/tmpdir.txt +++ b/doc/tmpdir.txt @@ -54,18 +54,12 @@ Running this would result in a passed te the default base temporary directory ----------------------------------------------- -.. - You can create directories by calling one of two methods - on the config object: - - ``config.mktemp(basename)``: create and return a new tempdir - - ``config.ensuretemp(basename)``: create or return a new tempdir - Temporary directories are by default created as sub directories of -the system temporary directory. The name will be ``pytest-NUM`` where +the system temporary directory. The base name will be ``pytest-NUM`` where ``NUM`` will be incremenated with each test run. Moreover, entries older than 3 temporary directories will be removed. -You can override the default temporary directory logic and set it like this:: +You can override the default temporary directory setting like this:: py.test --basetemp=mydir --- a/testing/test_config.py +++ b/testing/test_config.py @@ -81,33 +81,6 @@ class TestConfigCmdlineParsing: pytest.raises(AssertionError, "config.parse([])") -class TestConfigTmpdir: - def test_getbasetemp(self, testdir): - config = testdir.Config() - config.basetemp = "hello" - config.getbasetemp() == "hello" - - def test_mktemp(self, testdir): - config = testdir.Config() - config.basetemp = testdir.mkdir("hello") - tmp = config.mktemp("world") - assert tmp.relto(config.basetemp) == "world" - tmp = config.mktemp("this", numbered=True) - assert tmp.relto(config.basetemp).startswith("this") - tmp2 = config.mktemp("this", numbered=True) - assert tmp2.relto(config.basetemp).startswith("this") - assert tmp2 != tmp - - def test_reparse(self, testdir): - config2 = testdir.reparseconfig([]) - config3 = testdir.reparseconfig([]) - assert config2.getbasetemp() != config3.getbasetemp() - assert not config2.getbasetemp().relto(config3.getbasetemp()) - assert not config3.getbasetemp().relto(config2.getbasetemp()) - - def test_reparse_filename_too_long(self, testdir): - config = testdir.reparseconfig(["--basetemp=%s" % ("123"*300)]) - class TestConfigAPI: def test_config_trace(self, testdir): --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -173,7 +173,7 @@ class TmpTestdir: self.Config = request.config.__class__ self._pytest = request.getfuncargvalue("_pytest") # XXX remove duplication with tmpdir plugin - basetmp = request.config.ensuretemp("testdir") + basetmp = request.config._tmpdirhandler.ensuretemp("testdir") name = request.function.__name__ for i in range(100): try: @@ -350,7 +350,12 @@ class TmpTestdir: if not args: args = (self.tmpdir,) config = self.config_preparse() - args = list(args) + ["--basetemp=%s" % self.tmpdir.dirpath('basetemp')] + args = list(args) + for x in args: + if str(x).startswith('--basetemp'): + break + else: + args.append("--basetemp=%s" % self.tmpdir.dirpath('basetemp')) config.parse(args) return config @@ -361,7 +366,8 @@ class TmpTestdir: oldconfig = getattr(py.test, 'config', None) try: c = py.test.config = self.Config() - c.basetemp = oldconfig.mktemp("reparse", numbered=True) + c.basetemp = py.path.local.make_numbered_dir(prefix="reparse", + keep=0, rootdir=self.tmpdir, lock_timeout=None) c.parse(args) return c finally: --- a/testing/test_tmpdir.py +++ b/testing/test_tmpdir.py @@ -1,6 +1,6 @@ import py, pytest -from _pytest.tmpdir import pytest_funcarg__tmpdir +from _pytest.tmpdir import pytest_funcarg__tmpdir, TempdirHandler from _pytest.python import FuncargRequest def test_funcarg(testdir): @@ -27,3 +27,51 @@ def test_ensuretemp(recwarn): assert d1 == d2 assert d1.check(dir=1) +class TestTempdirHandler: + def test_mktemp(self, testdir): + config = testdir.Config() + config.option.basetemp = testdir.mkdir("hello") + t = TempdirHandler(config) + tmp = t.mktemp("world") + assert tmp.relto(t.getbasetemp()) == "world0" + tmp = t.mktemp("this") + assert tmp.relto(t.getbasetemp()).startswith("this") + tmp2 = t.mktemp("this") + assert tmp2.relto(t.getbasetemp()).startswith("this") + assert tmp2 != tmp + +class TestConfigTmpdir: + def test_getbasetemp_custom_removes_old(self, testdir): + p = testdir.tmpdir.join("xyz") + config = testdir.parseconfigure("--basetemp=xyz") + b = config._tmpdirhandler.getbasetemp() + assert b == p + h = b.ensure("hello") + config._tmpdirhandler.getbasetemp() + assert h.check() + config = testdir.parseconfigure("--basetemp=xyz") + b2 = config._tmpdirhandler.getbasetemp() + assert b2.check() + assert not h.check() + + def test_reparse(self, testdir): + config2 = testdir.reparseconfig([]) + config3 = testdir.reparseconfig([]) + assert config2.basetemp != config3.basetemp + assert not config2.basetemp.relto(config3.basetemp) + assert not config3.basetemp.relto(config2.basetemp) + + def test_reparse_filename_too_long(self, testdir): + config = testdir.reparseconfig(["--basetemp=%s" % ("123"*300)]) + + +def test_basetemp(testdir): + mytemp = testdir.tmpdir.mkdir("mytemp") + p = testdir.makepyfile(""" + import pytest + def test_1(): + pytest.ensuretemp("hello") + """) + result = testdir.runpytest(p, '--basetemp=%s' % mytemp) + assert result.ret == 0 + assert mytemp.join('hello').check() --- a/_pytest/config.py +++ b/_pytest/config.py @@ -1,4 +1,4 @@ -""" command line configuration, ini-file and conftest.py processing. """ +""" command line options, ini-file and conftest.py processing. """ import py import sys, os @@ -245,8 +245,6 @@ class CmdOptions(object): class Config(object): """ access to configuration values, pluginmanager and plugin hooks. """ - basetemp = None - def __init__(self, pluginmanager=None): #: command line option values, usually added via parser.addoption(...) #: or parser.getgroup(...).addoption(...) calls @@ -330,29 +328,6 @@ class Config(object): args.append(py.std.os.getcwd()) self.args = args - def ensuretemp(self, string, dir=True): - return self.getbasetemp().ensure(string, dir=dir) - - def getbasetemp(self): - if self.basetemp is None: - basetemp = self.option.basetemp - if basetemp: - basetemp = py.path.local(basetemp) - if not basetemp.check(dir=1): - basetemp.mkdir() - else: - basetemp = py.path.local.make_numbered_dir(prefix='pytest-') - self.basetemp = basetemp - return self.basetemp - - def mktemp(self, basename, numbered=False): - basetemp = self.getbasetemp() - if not numbered: - return basetemp.mkdir(basename) - else: - return py.path.local.make_numbered_dir(prefix=basename, - keep=0, rootdir=basetemp, lock_timeout=None) - def getini(self, name): """ return configuration value from an ini file. If the specified name hasn't been registered through a prior ``parse.addini`` --- a/tox.ini +++ b/tox.ini @@ -66,4 +66,3 @@ minversion=2.0 plugins=pytester addopts= -rxf --pyargs --doctest-modules rsyncdirs=tox.ini pytest.py _pytest testing - From commits-noreply at bitbucket.org Sun Nov 21 18:43:43 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 21 Nov 2010 11:43:43 -0600 (CST) Subject: [py-svn] pylib commit 52d3b11bd299: refine docs, README -> setup.py Message-ID: <20101121174343.70B152419BA@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1290102170 -3600 # Node ID 52d3b11bd299c609a25fcf701a4a6bf132736a6a # Parent bd406471f39498a5f9e99c53315378e83702becd refine docs, README -> setup.py --- a/doc/Makefile +++ b/doc/Makefile @@ -36,6 +36,9 @@ help: clean: -rm -rf $(BUILDDIR)/* +install: clean html + rsync -avz _build/html/ code:www-pylib/2.0.0 + html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @echo --- a/setup.py +++ b/setup.py @@ -4,30 +4,11 @@ if sys.version_info >= (3,0): use_setuptools() from setuptools import setup -long_description = """ -pylib: cross-python development utils - -py.path.local: local path objects -py.path.svnwc: local subversion WC paths -py.io: io-capturing on filedescriptor or sys.* level - -Platforms: Linux, Win32, OSX - -Interpreters: Python versions 2.4 through to 3.2, Jython 2.5.1 and PyPy - -Web page: http://pylib.org - -Bugs and issues: http://bitbucket.org/hpk42/pylib/issues/ - -Mailing lists and more contact points: http://pylib.org/contact.html - -(c) Holger Krekel and others, 2004-2010 -""" def main(): setup( name='pylib', - description='pylib: cross-python path, io, code, log facilities', - long_description = long_description, + description='pylib: cross-python path, ini-parsing, io, code, log facilities', + long_description = open('README.txt').read(), install_requires=['py>=1.3.9', ], # force newer py version which removes 'py' namespace # # so we can occupy it version='2.0.0.dev7', --- a/README.txt +++ b/README.txt @@ -8,3 +8,10 @@ the following tools and modules: * py.path: uniform local and svn path objects For questions and more information please visit http://pylib.org + +Bugs and issues: http://bitbucket.org/hpk42/pylib/issues/ + +Mailing lists and more contact points: http://pylib.org/contact.html + +(c) Holger Krekel and others, 2004-2010 + --- a/doc/conf.py +++ b/doc/conf.py @@ -48,9 +48,11 @@ copyright = u'2010, holger krekel et. al # built documents. # # The short X.Y version. -version = '2.0' +version = '2.0.0' # The full version, including alpha/beta/rc tags. -release = '2.0.0.dev0' +import py, pytest +assert py.path.local().relto(py.path.local(py.__file__).dirpath().dirpath()) +release = py.__version__ # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From commits-noreply at bitbucket.org Sun Nov 21 18:43:43 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 21 Nov 2010 11:43:43 -0600 (CST) Subject: [py-svn] pylib commit 1fe1c8998862: * removed compat namespace Message-ID: <20101121174343.93B34241A0D@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1290361189 -3600 # Node ID 1fe1c89988622791b4a9103eae97fdd45c21cb69 # Parent 52d3b11bd299c609a25fcf701a4a6bf132736a6a * removed compat namespace * some bug fixes wrt tmpdirs --- a/py/_compat/dep_subprocess.py +++ /dev/null @@ -1,5 +0,0 @@ - -import py -py.log._apiwarn("1.1", "py.compat.subprocess deprecated, use standard library version.", -stacklevel="apipkg") -subprocess = py.std.subprocess --- a/testing/root/test_com.py +++ /dev/null @@ -1,3 +0,0 @@ - -import py -import os --- a/py/__init__.py +++ b/py/__init__.py @@ -140,12 +140,4 @@ _apipkg.initpkg(__name__, attr={'_apipkg 'Syslog' : '._log.log:Syslog', }, - # compatibility modules (deprecated) - 'compat' : { - '__doc__' : '._compat:__doc__', - 'doctest' : '._compat.dep_doctest:doctest', - 'optparse' : '._compat.dep_optparse:optparse', - 'textwrap' : '._compat.dep_textwrap:textwrap', - 'subprocess' : '._compat.dep_subprocess:subprocess', - }, }) --- a/py/_compat/dep_doctest.py +++ /dev/null @@ -1,5 +0,0 @@ -import py - -py.log._apiwarn("1.1", "py.compat.doctest deprecated, use standard library version.", -stacklevel="apipkg") -doctest = py.std.doctest --- a/py/_compat/dep_optparse.py +++ /dev/null @@ -1,4 +0,0 @@ -import py -py.log._apiwarn("1.1", "py.compat.optparse deprecated, use standard library version.", stacklevel="apipkg") - -optparse = py.std.optparse --- a/testing/path/test_local.py +++ b/testing/path/test_local.py @@ -14,7 +14,7 @@ skiponwin32 = py.test.mark.skipif( def pytest_funcarg__path1(request): def setup(): - path1 = request.config.mktemp("path1") + path1 = request.getfuncargvalue("tmpdir") common.setuptestfs(path1) return path1 def teardown(path1): --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -348,7 +348,8 @@ def test_deindent(): lines = deindent(source.splitlines()) assert lines == ['', 'def f():', ' def g():', ' pass', ' '] - at py.test.mark.xfail("sys.version_info[:2] != (2,7) and sys.version_info[:2]<(3,2)") + at py.test.mark.xfail("sys.version_info[:3] < (2,6,5) or " + "((3,0) <= sys.version_info[:2] < (3,2))") def test_source_of_class_at_eof_without_newline(tmpdir): # this test fails because the implicit inspect.getsource(A) below # does not return the "x = 1" last line. --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ Changes between 1.3.4 and 2.0.0dev0 - path objects have a .dirname attribute now (equivalent to os.path.dirname(path)) - path.visit() accepts breadthfirst (bf) and sort options +- remove deprecated py.compat namespace Changes between 1.3.3 and 1.3.4 ================================================== --- a/tox.ini +++ b/tox.ini @@ -23,3 +23,4 @@ commands= [pytest] rsyncdirs = conftest.py py doc testing +addopts = -rxXf --- a/py/_compat/dep_textwrap.py +++ /dev/null @@ -1,5 +0,0 @@ -import py - -py.log._apiwarn("1.1", "py.compat.textwrap deprecated, use standard library version.", - stacklevel="apipkg") -textwrap = py.std.textwrap --- a/setup.py +++ b/setup.py @@ -30,7 +30,6 @@ def main(): 'Programming Language :: Python :: 3'], packages=['py', 'py._code', - 'py._compat', 'py._io', 'py._log', 'py._path', --- a/py/_compat/__init__.py +++ /dev/null @@ -1,2 +0,0 @@ -""" compatibility modules (taken from 2.4.4) """ - From commits-noreply at bitbucket.org Sun Nov 21 19:13:40 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 21 Nov 2010 12:13:40 -0600 (CST) Subject: [py-svn] pylib commit eaeb7575fbcf: not all python2.6.5 pass this test it seems Message-ID: <20101121181340.5C29E241306@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1290363208 -3600 # Node ID eaeb7575fbcf599a10741528a3f000a01125e58a # Parent 1fe1c89988622791b4a9103eae97fdd45c21cb69 not all python2.6.5 pass this test it seems --- a/testing/code/test_source.py +++ b/testing/code/test_source.py @@ -348,7 +348,7 @@ def test_deindent(): lines = deindent(source.splitlines()) assert lines == ['', 'def f():', ' def g():', ' pass', ' '] - at py.test.mark.xfail("sys.version_info[:3] < (2,6,5) or " + at py.test.mark.xfail("sys.version_info[:3] < (2,7,0) or " "((3,0) <= sys.version_info[:2] < (3,2))") def test_source_of_class_at_eof_without_newline(tmpdir): # this test fails because the implicit inspect.getsource(A) below From commits-noreply at bitbucket.org Mon Nov 22 12:06:38 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 22 Nov 2010 05:06:38 -0600 (CST) Subject: [py-svn] pytest-xdist commit b6a80ccbb4aa: fixes for new internal pytest tmpdir handling Message-ID: <20101122110638.BEDA46C12B1@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1290422483 -3600 # Node ID b6a80ccbb4aa962384ce3998a29226f59243821c # Parent 917353071622e40941b1b1d290bdf48f848c8e7a fixes for new internal pytest tmpdir handling --- a/testing/test_remote.py +++ b/testing/test_remote.py @@ -34,7 +34,7 @@ class SlaveSetup: self.testdir.chdir() #import os ; os.environ['EXECNET_DEBUG'] = "2" self.gateway = execnet.makegateway() - self.config = config = self.testdir.parseconfig() + self.config = config = self.testdir.parseconfigure() putevent = self.use_callback and self.events.put or None self.slp = SlaveController(None, self.gateway, config, putevent) self.request.addfinalizer(self.slp.ensure_teardown) --- a/testing/acceptance_test.py +++ b/testing/acceptance_test.py @@ -72,10 +72,10 @@ class TestDistribution: def test_basetemp_in_subprocesses(self, testdir): p1 = testdir.makepyfile(""" - def test_send(pytestconfig): - bt = pytestconfig.getbasetemp() - assert bt.basename.startswith("popen-") - """) + def test_send(tmpdir): + import py + assert tmpdir.relto(py.path.local(%r)), tmpdir + """ % str(testdir.tmpdir)) result = testdir.runpytest(p1, "-n1") assert result.ret == 0 result.stdout.fnmatch_lines([ --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -1,4 +1,4 @@ -import py +import pytest, py import sys from xdist.slavemanage import NodeManager queue = py.builtin._tryimport('queue', 'Queue') @@ -171,8 +171,8 @@ class DSession: if self.terminal and self.config.option.verbose >= 0: self.terminal.write_line(line) - def pytest_sessionstart(self, session, __multicall__): - #print "remaining multicall methods", __multicall__.methods + @pytest.mark.trylast + def pytest_sessionstart(self, session): if self.config.option.verbose > 0: self.report_line("instantiating gateways (use -v for details): %s" % ",".join(self.config.option.tx)) @@ -181,11 +181,13 @@ class DSession: def pytest_sessionfinish(self, session): """ teardown any resources after a test run. """ - self.nodemanager.teardown_nodes() + nm = getattr(self, 'nodemanager', None) # if not fully initialized + if nm is not None: + nm.teardown_nodes() - def pytest_collection(self, __multicall__): + def pytest_collection(self): # prohibit collection of test items in master process - __multicall__.methods[:] = [] + return True def pytest_runtestloop(self): numnodes = len(self.nodemanager.gwmanager.specs) @@ -326,7 +328,7 @@ class TerminalDistReporter: gateway.id, rinfo.platform, version, rinfo.cwd)) def pytest_testnodeready(self, node): - if self.config.option.verbose >= 0: + if self.config.option.verbose > 0: d = node.slaveinfo infoline = "[%s] Python %s" %( d['id'], --- a/xdist/slavemanage.py +++ b/xdist/slavemanage.py @@ -14,9 +14,7 @@ class NodeManager(object): self.specs = self.gwmanager.specs self.roots = self._getrsyncdirs() self._nodesready = py.std.threading.Event() - - def trace(self, msg): - self.config.hook.pytest_trace(category="nodemanage", msg=msg) + self.trace = self.config.trace.get("nodemanager") def config_getignores(self): return self.config.getini("rsyncignore") @@ -89,9 +87,9 @@ class NodeManager(object): config = self.config candidates = [py._pydir,pytestpath,pytestdir] candidates += config.option.rsyncdir - conftestroots = config.getini("rsyncdirs") - if conftestroots: - candidates.extend(conftestroots) + rsyncroots = config.getini("rsyncdirs") + if rsyncroots: + candidates.extend(rsyncroots) roots = [] for root in candidates: root = py.path.local(root).realpath() @@ -101,7 +99,6 @@ class NodeManager(object): roots.append(root) return roots - class GatewayManager: """ instantiating, managing and rsyncing to test hosts @@ -239,7 +236,8 @@ class SlaveController(object): option_dict = vars(self.config.option) if spec.popen: name = "popen-%s" % self.gateway.id - option_dict['basetemp'] = str(self.config.getbasetemp().join(name)) + basetemp = self.config._tmpdirhandler.getbasetemp() + option_dict['basetemp'] = str(basetemp.join(name)) self.config.hook.pytest_configure_node(node=self) self.channel = self.gateway.remote_exec(xdist.remote) self.channel.send((self.slaveinput, args, option_dict)) --- a/testing/test_slavemanage.py +++ b/testing/test_slavemanage.py @@ -17,11 +17,9 @@ def pytest_funcarg__hook(request): class pytest_funcarg__mysetup: def __init__(self, request): - basetemp = request.config.mktemp( - "mysetup-%s" % request.function.__name__, - numbered=True) - self.source = basetemp.mkdir("source") - self.dest = basetemp.mkdir("dest") + temp = request.getfuncargvalue("tmpdir") + self.source = temp.mkdir("source") + self.dest = temp.mkdir("dest") request.getfuncargvalue("_pytest") class TestGatewayManagerPopen: @@ -220,7 +218,7 @@ class TestNodeManager: specs = ["popen"] * 2 source.join("conftest.py").write("rsyncdirs = ['a']") source.ensure('a', dir=1) - config = testdir.reparseconfig([source, '--debug']) + config = testdir.parseconfigure(source, '--debug') assert config.option.debug nodemanager = NodeManager(config, specs) reprec = testdir.getreportrecorder(config).hookrecorder From commits-noreply at bitbucket.org Mon Nov 22 12:06:38 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 22 Nov 2010 05:06:38 -0600 (CST) Subject: [py-svn] pytest commit ce993aec28cc: add a way to mark hooks as "tryfirst" or "trylast" to influence its position in a hook chain. Message-ID: <20101122110638.E80D31E126E@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290377879 -3600 # Node ID ce993aec28cc55b8edb513b4390397ff4508b1a2 # Parent 53915e1ea00656d7685dc2a500b61b3fae350e41 add a way to mark hooks as "tryfirst" or "trylast" to influence its position in a hook chain. Use 'tryfirst' for capturing hooks so they can start capturing as early as possible, including when conftests add output in runtest_setup hooks. --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -377,3 +377,13 @@ def test_fdfuncarg_skips_on_no_osdup(tes result.stdout.fnmatch_lines([ "*1 skipped*" ]) + +def test_capture_conftest_runtest_setup(testdir): + testdir.makeconftest(""" + def pytest_runtest_setup(): + print ("hello19") + """) + testdir.makepyfile("def test_func(): pass") + result = testdir.runpytest() + assert result.ret == 0 + assert 'hello19' not in result.stdout.str() --- a/_pytest/python.py +++ b/_pytest/python.py @@ -19,8 +19,8 @@ def pytest_cmdline_main(config): showfuncargs(config) return 0 -def pytest_namespace(__multicall__): - __multicall__.execute() + at pytest.mark.trylast +def pytest_namespace(): raises.Exception = pytest.fail.Exception return { 'raises' : raises, --- a/_pytest/capture.py +++ b/_pytest/capture.py @@ -1,6 +1,6 @@ """ per-test stdout/stderr capturing mechanisms, ``capsys`` and ``capfd`` function arguments. """ -import py +import pytest, py import os def pytest_addoption(parser): @@ -143,13 +143,16 @@ class CaptureManager: addouterr(rep, outerr) return rep + @pytest.mark.tryfirst def pytest_runtest_setup(self, item): self.resumecapture_item(item) + @pytest.mark.tryfirst def pytest_runtest_call(self, item): self.resumecapture_item(item) self.activate_funcargs(item) + @pytest.mark.tryfirst def pytest_runtest_teardown(self, item): self.resumecapture_item(item) @@ -168,6 +171,7 @@ class CaptureManager: if hasattr(self, '_capturing'): self.suspendcapture() + @pytest.mark.tryfirst def pytest_runtest_makereport(self, __multicall__, item, call): self.deactivate_funcargs() rep = __multicall__.execute() --- a/_pytest/core.py +++ b/_pytest/core.py @@ -12,7 +12,7 @@ assert py.__version__.split(".")[:2] >= "%s is too old, remove or upgrade 'py'" % (py.__version__)) default_plugins = ( - "config session terminal runner python pdb capture unittest mark skipping " + "config mark session terminal runner python pdb unittest capture skipping " "tmpdir monkeypatch recwarn pastebin helpconfig nose assertion genscript " "junitxml doctest").split() @@ -272,11 +272,19 @@ class PluginManager(object): if plugins is None: plugins = self._plugins l = [] + last = [] for plugin in plugins: try: - l.append(getattr(plugin, attrname)) + meth = getattr(plugin, attrname) + if hasattr(meth, 'tryfirst'): + last.append(meth) + elif hasattr(meth, 'trylast'): + l.insert(0, meth) + else: + l.append(meth) except AttributeError: continue + l.extend(last) return l def call_plugin(self, plugin, methname, kwargs): --- a/testing/test_core.py +++ b/testing/test_core.py @@ -418,6 +418,35 @@ class TestPytestPluginInteractions: assert not pluginmanager.listattr("hello") assert pluginmanager.listattr("x") == [42] + def test_listattr_tryfirst(self): + class P1: + @pytest.mark.tryfirst + def m(self): + return 17 + + class P2: + def m(self): + return 23 + class P3: + def m(self): + return 19 + + pluginmanager = PluginManager() + p1 = P1() + p2 = P2() + p3 = P3() + pluginmanager.register(p1) + pluginmanager.register(p2) + pluginmanager.register(p3) + methods = pluginmanager.listattr('m') + assert methods == [p2.m, p3.m, p1.m] + del P1.m.__dict__['tryfirst'] + + pytest.mark.trylast(getattr(P2.m, 'im_func', P2.m)) + methods = pluginmanager.listattr('m') + assert methods == [p2.m, p1.m, p3.m] + + def test_namespace_has_default_and_env_plugins(testdir): p = testdir.makepyfile(""" import pytest --- a/pytest.py +++ b/pytest.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev31' +__version__ = '2.0.0.dev32' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev31', + version='2.0.0.dev32', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/_pytest/pdb.py +++ b/_pytest/pdb.py @@ -1,6 +1,6 @@ """ interactive debugging with PDB, the Python Debugger. """ -import py +import pytest, py import sys def pytest_addoption(parser): @@ -45,9 +45,11 @@ class PdbInvoke: def pytest_sessionfinish(self, session): # don't display failures again at the end session.config.option.tbstyle = "no" + + @pytest.mark.tryfirst def pytest_runtest_makereport(self, item, call, __multicall__): if not call.excinfo or \ - call.excinfo.errisinstance(py.test.skip.Exception) or \ + call.excinfo.errisinstance(pytest.skip.Exception) or \ call.excinfo.errisinstance(py.std.bdb.BdbQuit): return rep = __multicall__.execute() From commits-noreply at bitbucket.org Mon Nov 22 12:06:39 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 22 Nov 2010 05:06:39 -0600 (CST) Subject: [py-svn] pytest commit e49e10c37aa0: refine initialization and collection reporting, introduce a progress bar Message-ID: <20101122110639.090091E128B@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290423596 -3600 # Node ID e49e10c37aa055752e225557de5df1692b1c1cc0 # Parent ce993aec28cc55b8edb513b4390397ff4508b1a2 refine initialization and collection reporting, introduce a progress bar --- a/doc/example/assertion/global_testmodule_config/conftest.py +++ b/doc/example/assertion/global_testmodule_config/conftest.py @@ -7,4 +7,4 @@ def pytest_runtest_setup(item): return mod = item.getparent(pytest.Module).obj if hasattr(mod, 'hello'): - py.builtin.print_("mod.hello", mod.hello) + print ("mod.hello %r" % (mod.hello,)) --- a/testing/test_terminal.py +++ b/testing/test_terminal.py @@ -536,7 +536,6 @@ class TestGenericReporting: p = testdir.makepyfile("import xyz\n") result = testdir.runpytest(*option.args) result.stdout.fnmatch_lines([ - "*test_collect_fail.py E*", "> import xyz", "E ImportError: No module named xyz", "*1 error*", --- a/testing/test_skipping.py +++ b/testing/test_skipping.py @@ -414,8 +414,6 @@ def test_skipped_reasons_functional(test ) result = testdir.runpytest('--report=skipped') result.stdout.fnmatch_lines([ - "*test_two.py S", - "*test_one.py ss", "*SKIP*3*conftest.py:3: test", ]) assert result.ret == 0 --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -2,7 +2,7 @@ This is a good source for looking at the various reporting hooks. """ -import py +import pytest, py import sys import os @@ -98,6 +98,7 @@ class TerminalReporter: self.showheader = self.verbosity >= 0 self.showfspath = self.verbosity >= 0 self.showlongtestinfo = self.verbosity > 0 + self._numcollected = 0 self.stats = {} self.curdir = py.path.local() @@ -106,6 +107,7 @@ class TerminalReporter: self._tw = py.io.TerminalWriter(file) self.currentfspath = None self.reportchars = getreportopt(config) + self.hasmarkup = self._tw.hasmarkup def hasopt(self, char): char = {'xfailed': 'x', 'skipped': 's'}.get(char,char) @@ -139,6 +141,10 @@ class TerminalReporter: self.ensure_newline() self._tw.line(line, **markup) + def rewrite(self, line, **markup): + line = str(line) + self._tw.write("\r" + line, **markup) + def write_sep(self, sep, title=None, **markup): self.ensure_newline() self._tw.sep(sep, title, **markup) @@ -207,14 +213,42 @@ class TerminalReporter: self._tw.write(" " + line) self.currentfspath = -2 + def pytest_collection(self): + if not self.hasmarkup: + self.write_line("collecting ...", bold=True) + def pytest_collectreport(self, report): - if not report.passed: - if report.failed: - self.stats.setdefault("error", []).append(report) - self.write_fspath_result(report.fspath, "E") - elif report.skipped: - self.stats.setdefault("skipped", []).append(report) - self.write_fspath_result(report.fspath, "S") + if report.failed: + self.stats.setdefault("error", []).append(report) + elif report.skipped: + self.stats.setdefault("skipped", []).append(report) + items = [x for x in report.result if isinstance(x, pytest.Item)] + self._numcollected += len(items) + if self.hasmarkup: + #self.write_fspath_result(report.fspath, 'E') + self.report_collect() + + def report_collect(self, final=False): + errors = len(self.stats.get('error', [])) + skipped = len(self.stats.get('skipped', [])) + if final: + line = "collected " + else: + line = "collecting " + line += str(self._numcollected) + " items" + if errors: + line += " / %d errors" % errors + if skipped: + line += " / %d skipped" % skipped + if self.hasmarkup: + if final: + line += " \n" + self.rewrite(line, bold=True) + else: + self.write_line(line) + + def pytest_collection_modifyitems(self): + self.report_collect(True) def pytest_sessionstart(self, session): self._sessionstarttime = py.std.time.time() @@ -236,8 +270,8 @@ class TerminalReporter: def pytest_collection_finish(self): if not self.showheader: return - for i, testarg in enumerate(self.config.args): - self.write_line("test path %d: %s" %(i+1, testarg)) + #for i, testarg in enumerate(self.config.args): + # self.write_line("test path %d: %s" %(i+1, testarg)) def pytest_sessionfinish(self, exitstatus, __multicall__): __multicall__.execute() --- a/_pytest/config.py +++ b/_pytest/config.py @@ -405,14 +405,13 @@ def getcfg(args, inibasenames): args = [py.path.local()] for arg in args: arg = py.path.local(arg) - if arg.check(): - for base in arg.parts(reverse=True): - for inibasename in inibasenames: - p = base.join(inibasename) - if p.check(): - iniconfig = py.iniconfig.IniConfig(p) - if 'pytest' in iniconfig.sections: - return iniconfig['pytest'] + for base in arg.parts(reverse=True): + for inibasename in inibasenames: + p = base.join(inibasename) + if p.check(): + iniconfig = py.iniconfig.IniConfig(p) + if 'pytest' in iniconfig.sections: + return iniconfig['pytest'] return {} def findupwards(current, basename): --- a/_pytest/hookspec.py +++ b/_pytest/hookspec.py @@ -218,6 +218,3 @@ def pytest_internalerror(excrepr): def pytest_keyboard_interrupt(excinfo): """ called for keyboard interrupt. """ - -def pytest_trace(category, msg): - """ called for debug info. """ From commits-noreply at bitbucket.org Mon Nov 22 12:06:38 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 22 Nov 2010 05:06:38 -0600 (CST) Subject: [py-svn] pytest-xdist commit 05e9a2aa1553: merge nodemanager and gwmanager Message-ID: <20101122110638.D014C6C1309@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1290423955 -3600 # Node ID 05e9a2aa1553750cb155b5e05e4b7eb3be1c73d7 # Parent b6a80ccbb4aa962384ce3998a29226f59243821c merge nodemanager and gwmanager --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.5a9', + version='1.5a10', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=open('README.txt').read(), license='GPLv2 or later', --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -190,7 +190,7 @@ class DSession: return True def pytest_runtestloop(self): - numnodes = len(self.nodemanager.gwmanager.specs) + numnodes = len(self.nodemanager.specs) dist = self.config.getvalue("dist") if dist == "load": self.sched = LoadScheduling(numnodes, log=self.log) --- a/xdist/slavemanage.py +++ b/xdist/slavemanage.py @@ -6,15 +6,22 @@ import xdist.remote from _pytest import runner # XXX load dynamically class NodeManager(object): - def __init__(self, config, specs=None): + EXIT_TIMEOUT = 10 + def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"): self.config = config + self._nodesready = py.std.threading.Event() + self.trace = self.config.trace.get("nodemanager") + self.group = execnet.Group() if specs is None: specs = self._getxspecs() - self.gwmanager = GatewayManager(specs, config.hook) - self.specs = self.gwmanager.specs + self.specs = [] + for spec in specs: + if not isinstance(spec, execnet.XSpec): + spec = execnet.XSpec(spec) + if not spec.chdir and not spec.popen: + spec.chdir = defaultchdir + self.specs.append(spec) self.roots = self._getrsyncdirs() - self._nodesready = py.std.threading.Event() - self.trace = self.config.trace.get("nodemanager") def config_getignores(self): return self.config.getini("rsyncignore") @@ -32,37 +39,30 @@ class NodeManager(object): if self.roots: # send each rsync root for root in self.roots: - self.gwmanager.rsync(root, **options) + self.rsync(root, **options) def makegateways(self): - # we change to the topdir sot that - # PopenGateways will have their cwd - # such that unpickling configs will - # pick it up as the right topdir - # (for other gateways this chdir is irrelevant) self.trace("making gateways") - #old = self.config.topdir.chdir() - #try: - self.gwmanager.makegateways() - #finally: - # old.chdir() + assert not list(self.group) + for spec in self.specs: + gw = self.group.makegateway(spec) + self.config.hook.pytest_gwmanage_newgateway(gateway=gw) def setup_nodes(self, putevent): self.rsync_roots() self.trace("setting up nodes") - for gateway in self.gwmanager.group: + for gateway in self.group: node = SlaveController(self, gateway, self.config, putevent) gateway.node = node # to keep node alive node.setup() self.trace("started node %r" % node) def teardown_nodes(self): - self.gwmanager.exit() + self.group.terminate(self.EXIT_TIMEOUT) def _getxspecs(self): - config = self.config xspeclist = [] - for xspec in config.getvalue("tx"): + for xspec in self.config.getvalue("tx"): i = xspec.find("*") try: num = int(xspec[:i]) @@ -99,28 +99,6 @@ class NodeManager(object): roots.append(root) return roots -class GatewayManager: - """ - instantiating, managing and rsyncing to test hosts - """ - EXIT_TIMEOUT = 10 - def __init__(self, specs, hook, defaultchdir="pyexecnetcache"): - self.specs = [] - self.hook = hook - self.group = execnet.Group() - for spec in specs: - if not isinstance(spec, execnet.XSpec): - spec = execnet.XSpec(spec) - if not spec.chdir and not spec.popen: - spec.chdir = defaultchdir - self.specs.append(spec) - - def makegateways(self): - assert not list(self.group) - for spec in self.specs: - gw = self.group.makegateway(spec) - self.hook.pytest_gwmanage_newgateway(gateway=gw) - def rsync(self, source, notify=None, verbose=False, ignores=None): """ perform rsync to all remote hosts. """ @@ -144,19 +122,16 @@ class GatewayManager: seen.add(spec) gateways.append(gateway) if seen: - self.hook.pytest_gwmanage_rsyncstart( + self.config.hook.pytest_gwmanage_rsyncstart( source=source, gateways=gateways, ) rsync.send() - self.hook.pytest_gwmanage_rsyncfinish( + self.config.hook.pytest_gwmanage_rsyncfinish( source=source, gateways=gateways, ) - def exit(self): - self.group.terminate(self.EXIT_TIMEOUT) - class HostRSync(execnet.RSync): """ RSyncer that filters out common files """ --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5a9' +__version__ = '1.5a10' --- a/testing/test_slavemanage.py +++ b/testing/test_slavemanage.py @@ -1,15 +1,20 @@ import py import os import execnet -from xdist.slavemanage import GatewayManager, HostRSync -from xdist.slavemanage import NodeManager +from xdist.slavemanage import HostRSync, NodeManager + +pytest_plugins = "pytester", def pytest_funcarg__hookrecorder(request): _pytest = request.getfuncargvalue('_pytest') - hook = request.getfuncargvalue('hook') - return _pytest.gethookrecorder(hook) + config = request.getfuncargvalue('config') + return _pytest.gethookrecorder(config.hook) -def pytest_funcarg__hook(request): +def pytest_funcarg__config(request): + testdir = request.getfuncargvalue("testdir") + config = testdir.parseconfig() + return config + from xdist import newhooks from _pytest.core import HookRelay, PluginManager from _pytest import hookspec @@ -22,20 +27,20 @@ class pytest_funcarg__mysetup: self.dest = temp.mkdir("dest") request.getfuncargvalue("_pytest") -class TestGatewayManagerPopen: - def test_popen_no_default_chdir(self, hook): - gm = GatewayManager(["popen"], hook) +class TestNodeManagerPopen: + def test_popen_no_default_chdir(self, config): + gm = NodeManager(config, ["popen"]) assert gm.specs[0].chdir is None - def test_default_chdir(self, hook): + def test_default_chdir(self, config): l = ["ssh=noco", "socket=xyz"] - for spec in GatewayManager(l, hook).specs: + for spec in NodeManager(config, l).specs: assert spec.chdir == "pyexecnetcache" - for spec in GatewayManager(l, hook, defaultchdir="abc").specs: + for spec in NodeManager(config, l, defaultchdir="abc").specs: assert spec.chdir == "abc" - def test_popen_makegateway_events(self, hook, hookrecorder, _pytest): - hm = GatewayManager(["popen"] * 2, hook) + def test_popen_makegateway_events(self, config, hookrecorder, _pytest): + hm = NodeManager(config, ["popen"] * 2) hm.makegateways() call = hookrecorder.popcall("pytest_gwmanage_newgateway") assert call.gateway.spec == execnet.XSpec("popen") @@ -43,12 +48,12 @@ class TestGatewayManagerPopen: call = hookrecorder.popcall("pytest_gwmanage_newgateway") assert call.gateway.id == "gw1" assert len(hm.group) == 2 - hm.exit() + hm.teardown_nodes() assert not len(hm.group) - def test_popens_rsync(self, hook, mysetup): + def test_popens_rsync(self, config, mysetup): source = mysetup.source - hm = GatewayManager(["popen"] * 2, hook) + hm = NodeManager(config, ["popen"] * 2) hm.makegateways() assert len(hm.group) == 2 for gw in hm.group: @@ -62,28 +67,28 @@ class TestGatewayManagerPopen: l = [] hm.rsync(source, notify=lambda *args: l.append(args)) assert not l - hm.exit() + hm.teardown_nodes() assert not len(hm.group) assert "sys.path.insert" in gw.remote_exec.args[0] - def test_rsync_popen_with_path(self, hook, mysetup): + def test_rsync_popen_with_path(self, config, mysetup): source, dest = mysetup.source, mysetup.dest - hm = GatewayManager(["popen//chdir=%s" %dest] * 1, hook) + hm = NodeManager(config, ["popen//chdir=%s" %dest] * 1) hm.makegateways() source.ensure("dir1", "dir2", "hello") l = [] hm.rsync(source, notify=lambda *args: l.append(args)) assert len(l) == 1 assert l[0] == ("rsyncrootready", hm.group['gw0'].spec, source) - hm.exit() + hm.teardown_nodes() dest = dest.join(source.basename) assert dest.join("dir1").check() assert dest.join("dir1", "dir2").check() assert dest.join("dir1", "dir2", 'hello').check() - def test_rsync_same_popen_twice(self, hook, mysetup, hookrecorder): + def test_rsync_same_popen_twice(self, config, mysetup, hookrecorder): source, dest = mysetup.source, mysetup.dest - hm = GatewayManager(["popen//chdir=%s" %dest] * 2, hook) + hm = NodeManager(config, ["popen//chdir=%s" %dest] * 2) hm.makegateways() source.ensure("dir1", "dir2", "hello") hm.rsync(source) @@ -156,14 +161,13 @@ class TestNodeManager: "--rsyncdir", rsyncroot, source, )) - #assert nodemanager.config.topdir == source nodemanager.rsync_roots() if rsyncroot == source: dest = dest.join("source") assert dest.join("dir1").check() assert dest.join("dir1", "dir2").check() assert dest.join("dir1", "dir2", 'hello').check() - nodemanager.gwmanager.exit() + nodemanager.teardown_nodes() def test_init_rsync_roots(self, testdir, mysetup): source, dest = mysetup.source, mysetup.dest @@ -209,25 +213,10 @@ class TestNodeManager: config = testdir.reparseconfig([source]) nodemanager = NodeManager(config, specs) nodemanager.rsync_roots() - for gwspec in nodemanager.gwmanager.specs: + for gwspec in nodemanager.specs: assert gwspec._samefilesystem() assert not gwspec.chdir - def test_setup_DEBUG(self, mysetup, testdir): - source = mysetup.source - specs = ["popen"] * 2 - source.join("conftest.py").write("rsyncdirs = ['a']") - source.ensure('a', dir=1) - config = testdir.parseconfigure(source, '--debug') - assert config.option.debug - nodemanager = NodeManager(config, specs) - reprec = testdir.getreportrecorder(config).hookrecorder - nodemanager.setup_nodes(putevent=[].append) - for spec in nodemanager.gwmanager.specs: - l = reprec.getcalls("pytest_trace") - assert l - nodemanager.teardown_nodes() - def test_ssh_setup_nodes(self, specssh, testdir): testdir.makepyfile(__init__="", test_x=""" def test_one(): From commits-noreply at bitbucket.org Mon Nov 22 12:34:17 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 22 Nov 2010 05:34:17 -0600 (CST) Subject: [py-svn] pylib commit 7799c5a50b52: fix samefile() impl on windows Message-ID: <20101122113417.5C60C241984@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pylib # URL http://bitbucket.org/hpk42/pylib/overview # User holger krekel # Date 1290425642 -3600 # Node ID 7799c5a50b52b28ebcf1eb543c97d790ec53db4f # Parent eaeb7575fbcf599a10741528a3f000a01125e58a fix samefile() impl on windows --- a/py/_path/local.py +++ b/py/_path/local.py @@ -68,10 +68,6 @@ class PosixPath(common.PathBase): target = self.sep.join(('..', )*n + (relsource, )) py.error.checked_call(os.symlink, target, self.strpath) - def samefile(self, other): - """ return True if other refers to the same stat object as self. """ - return py.error.checked_call(os.path.samefile, str(self), str(other)) - def getuserid(user): import pwd if not isinstance(user, int): @@ -160,6 +156,14 @@ class LocalPath(FSBase): def __lt__(self, other): return str(self) < str(other) + def samefile(self, other): + """ return True if 'other' references the same file as 'self'. """ + if self == other: + return True + if not iswin32: + return py.error.checked_call(os.path.samefile, str(self), str(other)) + return False + def remove(self, rec=1, ignore_errors=False): """ remove a file or directory (or a directory tree if rec=1). if ignore_errors is True, errors while removing directories will --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def main(): long_description = open('README.txt').read(), install_requires=['py>=1.3.9', ], # force newer py version which removes 'py' namespace # # so we can occupy it - version='2.0.0.dev7', + version='2.0.0.dev8', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/path/test_local.py +++ b/testing/path/test_local.py @@ -420,6 +420,10 @@ def test_samefile(tmpdir): assert tmpdir.samefile(tmpdir) p = tmpdir.ensure("hello") assert p.samefile(p) + if sys.platform == "win32": + p1 = p.__class__(str(p).lower()) + p2 = p.__class__(str(p).upper()) + assert p1.samefile(p2) def test_mkdtemp_rootdir(tmpdir): dtmp = local.mkdtemp(rootdir=tmpdir) --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev7' +__version__ = '2.0.0.dev8' from py import _apipkg From commits-noreply at bitbucket.org Mon Nov 22 12:42:57 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 22 Nov 2010 05:42:57 -0600 (CST) Subject: [py-svn] pytest commit 286116561e6f: fix bug on windows Message-ID: <20101122114257.4AD921E0FBF@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290426168 -3600 # Node ID 286116561e6feec6e9ce0590f66558c12fb3b338 # Parent e49e10c37aa055752e225557de5df1692b1c1cc0 fix bug on windows --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -454,7 +454,7 @@ class TmpTestdir: def _getpybinargs(self, scriptname): if not self.request.config.getvalue("notoolsonpath"): import pytest - script = pytest.__file__.strip("co") + script = pytest.__file__.rstrip("co") assert script, "script %r not found" % scriptname # XXX we rely on script refering to the correct environment # we cannot use "(py.std.sys.executable,script)" From commits-noreply at bitbucket.org Mon Nov 22 15:40:12 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 22 Nov 2010 08:40:12 -0600 (CST) Subject: [py-svn] pytest-xdist commit 1edfc8649ffc: * introduce progress-reporting Message-ID: <20101122144012.1FE966C1309@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1290436796 -3600 # Node ID 1edfc8649ffcaa06a9d05451361033bb6b2dcbcc # Parent 05e9a2aa1553750cb155b5e05e4b7eb3be1c73d7 * introduce progress-reporting * rename somewhat internal pytest_gwmanage_* events to pytest_xdist_* * make gateway setup a separate step --- a/xdist/newhooks.py +++ b/xdist/newhooks.py @@ -1,11 +1,14 @@ -def pytest_gwmanage_newgateway(gateway): +def pytest_xdist_setupnodes(config, specs): + """ called before any remote node is set up. """ + +def pytest_xdist_newgateway(gateway): """ called on new raw gateway creation. """ -def pytest_gwmanage_rsyncstart(source, gateways): +def pytest_xdist_rsyncstart(source, gateways): """ called before rsyncing a directory to remote gateways takes place. """ -def pytest_gwmanage_rsyncfinish(source, gateways): +def pytest_xdist_rsyncfinish(source, gateways): """ called after rsyncing a directory to remote gateways takes place. """ def pytest_configure_node(node): --- a/xdist/plugin.py +++ b/xdist/plugin.py @@ -59,12 +59,9 @@ def pytest_cmdline_main(config): def pytest_configure(config, __multicall__): __multicall__.execute() if config.getvalue("dist") != "no": - from xdist.dsession import DSession, TerminalDistReporter + from xdist.dsession import DSession session = DSession(config) config.pluginmanager.register(session, "dsession") - - trdist = TerminalDistReporter(config) - config.pluginmanager.register(trdist, "terminaldistreporter") def check_options(config): if config.option.numprocesses: --- a/xdist/dsession.py +++ b/xdist/dsession.py @@ -166,6 +166,9 @@ class DSession: self.terminal = config.pluginmanager.getplugin("terminalreporter") except KeyError: self.terminal = None + else: + self.trdist = TerminalDistReporter(config) + config.pluginmanager.register(self.trdist, "terminaldistreporter") def report_line(self, line): if self.terminal and self.config.option.verbose >= 0: @@ -173,9 +176,6 @@ class DSession: @pytest.mark.trylast def pytest_sessionstart(self, session): - if self.config.option.verbose > 0: - self.report_line("instantiating gateways (use -v for details): %s" % - ",".join(self.config.option.tx)) self.nodemanager = NodeManager(self.config) self.nodemanager.setup_nodes(putevent=self.queue.put) @@ -262,10 +262,15 @@ class DSession: def slave_collectionfinish(self, node, ids): self.sched.addnode_collection(node, ids) - self.report_line("[%s] collected %d test items" %( - node.gateway.id, len(ids))) + if self.terminal: + self.trdist.setstatus(node.gateway.spec, "[%d]" %(len(ids))) if self.sched.collection_is_completed: + if self.terminal: + self.terminal.write_line("") + self.terminal.write_line("scheduling tests via %s" %( + self.sched.__class__.__name__)) + self.sched.init_distribute() def slave_logstart(self, node, nodeid, location): @@ -316,16 +321,43 @@ class TerminalDistReporter: def __init__(self, config): self.config = config self.tr = config.pluginmanager.getplugin("terminalreporter") + self._status = {} + self._lastlen = 0 def write_line(self, msg): self.tr.write_line(msg) - def pytest_gwmanage_newgateway(self, gateway): - rinfo = gateway._rinfo() - if self.config.option.verbose >= 0: - version = "%s.%s.%s" %rinfo.version_info[:3] - self.write_line("[%s] %s Python %s cwd: %s" % ( - gateway.id, rinfo.platform, version, rinfo.cwd)) + def setstatus(self, spec, status, show=True): + self._status[spec.id] = status + if show: + parts = ["%s %s" %(spec.id, self._status[spec.id]) + for spec in self._specs] + line = " / ".join(parts) + self.rewrite(line) + + def rewrite(self, line, newline=False): + pline = line + " " * max(self._lastlen-len(line), 0) + if newline: + self._lastlen = 0 + pline += "\n" + else: + self._lastlen = len(line) + self.tr.rewrite(pline, bold=True) + + def pytest_xdist_setupnodes(self, specs): + self._specs = specs + for spec in specs: + self.setstatus(spec, "initializing", show=False) + self.setstatus(spec, "initializing", show=True) + + def pytest_xdist_newgateway(self, gateway): + if self.config.option.verbose > 0: + rinfo = gateway._rinfo() + version = "%s.%s.%s" % rinfo.version_info[:3] + self.rewrite("[%s] %s Python %s cwd: %s" % ( + gateway.id, rinfo.platform, version, rinfo.cwd), + newline=True) + self.setstatus(gateway.spec, "collecting") def pytest_testnodeready(self, node): if self.config.option.verbose > 0: @@ -333,18 +365,19 @@ class TerminalDistReporter: infoline = "[%s] Python %s" %( d['id'], d['version'].replace('\n', ' -- '),) - self.write_line(infoline) + self.rewrite(infoline, newline=True) + self.setstatus(node.gateway.spec, "ready") def pytest_testnodedown(self, node, error): if not error: return self.write_line("[%s] node down: %s" %(node.gateway.id, error)) - #def pytest_gwmanage_rsyncstart(self, source, gateways): + #def pytest_xdist_rsyncstart(self, source, gateways): # targets = ",".join([gw.id for gw in gateways]) # msg = "[%s] rsyncing: %s" %(targets, source) # self.write_line(msg) - #def pytest_gwmanage_rsyncfinish(self, source, gateways): + #def pytest_xdist_rsyncfinish(self, source, gateways): # targets = ", ".join(["[%s]" % gw.id for gw in gateways]) # self.write_line("rsyncfinish: %s -> %s" %(source, targets)) --- a/testing/test_slavemanage.py +++ b/testing/test_slavemanage.py @@ -15,11 +15,6 @@ def pytest_funcarg__config(request): config = testdir.parseconfig() return config - from xdist import newhooks - from _pytest.core import HookRelay, PluginManager - from _pytest import hookspec - return HookRelay([hookspec, newhooks], PluginManager()) - class pytest_funcarg__mysetup: def __init__(self, request): temp = request.getfuncargvalue("tmpdir") @@ -42,10 +37,13 @@ class TestNodeManagerPopen: def test_popen_makegateway_events(self, config, hookrecorder, _pytest): hm = NodeManager(config, ["popen"] * 2) hm.makegateways() - call = hookrecorder.popcall("pytest_gwmanage_newgateway") + call = hookrecorder.popcall("pytest_xdist_setupnodes") + assert len(call.specs) == 2 + + call = hookrecorder.popcall("pytest_xdist_newgateway") assert call.gateway.spec == execnet.XSpec("popen") assert call.gateway.id == "gw0" - call = hookrecorder.popcall("pytest_gwmanage_newgateway") + call = hookrecorder.popcall("pytest_xdist_newgateway") assert call.gateway.id == "gw1" assert len(hm.group) == 2 hm.teardown_nodes() @@ -92,11 +90,11 @@ class TestNodeManagerPopen: hm.makegateways() source.ensure("dir1", "dir2", "hello") hm.rsync(source) - call = hookrecorder.popcall("pytest_gwmanage_rsyncstart") + call = hookrecorder.popcall("pytest_xdist_rsyncstart") assert call.source == source assert len(call.gateways) == 1 assert call.gateways[0] in hm.group - call = hookrecorder.popcall("pytest_gwmanage_rsyncfinish") + call = hookrecorder.popcall("pytest_xdist_rsyncfinish") class TestHRSync: class pytest_funcarg__mysetup: @@ -140,6 +138,7 @@ class TestNodeManager: config = testdir.reparseconfig([source]) nodemanager = NodeManager(config, ["popen//chdir=%s" % mysetup.dest]) #assert nodemanager.config.topdir == source == config.topdir + nodemanager.makegateways() nodemanager.rsync_roots() p, = nodemanager.gwmanager.multi_exec( "import os ; channel.send(os.getcwd())").receive_each() @@ -161,6 +160,7 @@ class TestNodeManager: "--rsyncdir", rsyncroot, source, )) + nodemanager.makegateways() nodemanager.rsync_roots() if rsyncroot == source: dest = dest.join("source") @@ -181,6 +181,7 @@ class TestNodeManager: """)) config = testdir.reparseconfig([source]) nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) + nodemanager.makegateways() nodemanager.rsync_roots() assert dest.join("dir2").check() assert not dest.join("dir1").check() @@ -199,6 +200,7 @@ class TestNodeManager: """)) config = testdir.reparseconfig([source]) nodemanager = NodeManager(config, ["popen//chdir=%s" % dest]) + nodemanager.makegateways() nodemanager.rsync_roots() assert dest.join("dir1").check() assert not dest.join("dir1", "dir2").check() @@ -212,6 +214,7 @@ class TestNodeManager: source.ensure('a', dir=1) config = testdir.reparseconfig([source]) nodemanager = NodeManager(config, specs) + nodemanager.makegateways() nodemanager.rsync_roots() for gwspec in nodemanager.specs: assert gwspec._samefilesystem() --- a/CHANGELOG +++ b/CHANGELOG @@ -4,9 +4,10 @@ 1.5a1 - adapt to pytest-2.0 changes, rsyncdirs and rsyncignore can now only be specified in [pytest] sections of ini files, see "py.test -h" for details. -- major internal refactoring to match the py-1.4 event refactoring +- major internal refactoring to match the pytest-2.0 event refactoring - perform test collection always at slave side instead of at the master - make python2/python3 bridging work, remove usage of pickling +- improve initial reporting by using line-rewriting - remove all trailing whitespace from source 1.4 --- a/xdist/slavemanage.py +++ b/xdist/slavemanage.py @@ -20,20 +20,17 @@ class NodeManager(object): spec = execnet.XSpec(spec) if not spec.chdir and not spec.popen: spec.chdir = defaultchdir + self.group.allocate_id(spec) self.specs.append(spec) self.roots = self._getrsyncdirs() - def config_getignores(self): - return self.config.getini("rsyncignore") - def rsync_roots(self): """ make sure that all remote gateways have the same set of roots in their current directory. """ - self.makegateways() options = { - 'ignores': self.config_getignores(), + 'ignores': self.config.getini("rsyncignore"), 'verbose': self.config.option.verbose, } if self.roots: @@ -42,13 +39,15 @@ class NodeManager(object): self.rsync(root, **options) def makegateways(self): - self.trace("making gateways") assert not list(self.group) + self.config.hook.pytest_xdist_setupnodes(config=self.config, + specs=self.specs) for spec in self.specs: gw = self.group.makegateway(spec) - self.config.hook.pytest_gwmanage_newgateway(gateway=gw) + self.config.hook.pytest_xdist_newgateway(gateway=gw) def setup_nodes(self, putevent): + self.makegateways() self.rsync_roots() self.trace("setting up nodes") for gateway in self.group: @@ -122,12 +121,12 @@ class NodeManager(object): seen.add(spec) gateways.append(gateway) if seen: - self.config.hook.pytest_gwmanage_rsyncstart( + self.config.hook.pytest_xdist_rsyncstart( source=source, gateways=gateways, ) rsync.send() - self.config.hook.pytest_gwmanage_rsyncfinish( + self.config.hook.pytest_xdist_rsyncfinish( source=source, gateways=gateways, ) --- a/testing/test_dsession.py +++ b/testing/test_dsession.py @@ -156,11 +156,11 @@ class TestDistReporter: # platform = "xyz" # cwd = "qwe" - #dsession.pytest_gwmanage_newgateway(gw1, rinfo) + #dsession.pytest_xdist_newgateway(gw1, rinfo) #linecomp.assert_contains_lines([ # "*X1*popen*xyz*2.5*" #]) - dsession.pytest_gwmanage_rsyncstart(source="hello", gateways=[gw1, gw2]) + dsession.pytest_xdist_rsyncstart(source="hello", gateways=[gw1, gw2]) linecomp.assert_contains_lines([ "[X1,X2] rsyncing: hello", ]) From commits-noreply at bitbucket.org Tue Nov 23 15:45:33 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 08:45:33 -0600 (CST) Subject: [py-svn] pytest commit f34cf98d4fc9: nice-fy error reporting of self-tests Message-ID: <20101123144533.3A7DD1E1281@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290435618 -3600 # Node ID f34cf98d4fc9e2f326b0e946c4adf9c214ba569e # Parent 286116561e6feec6e9ce0590f66558c12fb3b338 nice-fy error reporting of self-tests --- a/testing/test_pytester.py +++ b/testing/test_pytester.py @@ -82,7 +82,7 @@ def test_hookrecorder_basic(): call = rec.popcall("pytest_xyz") assert call.arg == 123 assert call._name == "pytest_xyz" - pytest.raises(ValueError, "rec.popcall('abc')") + pytest.raises(pytest.fail.Exception, "rec.popcall('abc')") def test_hookrecorder_basic_no_args_hook(): rec = HookRecorder(PluginManager()) --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -125,11 +125,14 @@ class HookRecorder: py.test.fail("could not find %r check %r" % (name, check)) def popcall(self, name): + __tracebackhide__ = True for i, call in enumerate(self.calls): if call._name == name: del self.calls[i] return call - raise ValueError("could not find call %r" %(name, )) + lines = ["could not find call %r, in:" % (name,)] + lines.extend([" %s" % str(x) for x in self.calls]) + py.test.fail("\n".join(lines)) def getcall(self, name): l = self.getcalls(name) From commits-noreply at bitbucket.org Tue Nov 23 15:45:33 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 08:45:33 -0600 (CST) Subject: [py-svn] pytest commit a864bc3f8b16: refine unittest support to also work with twisted trial test cases better by Message-ID: <20101123144533.4E22F1E12AE@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290523343 -3600 # Node ID a864bc3f8b16d5d0895d5b093e202aed4652edfd # Parent f34cf98d4fc9e2f326b0e946c4adf9c214ba569e refine unittest support to also work with twisted trial test cases better by introducing a slightly hackish way to report a failure upstream --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -33,16 +33,40 @@ class UnitTestCase(pytest.Class): meth() class TestCaseFunction(pytest.Function): + _excinfo = None def setup(self): pass def teardown(self): pass def startTest(self, testcase): pass + + def _addexcinfo(self, rawexcinfo): + #__tracebackhide__ = True + assert rawexcinfo + try: + self._excinfo = py.code.ExceptionInfo(rawexcinfo) + except TypeError: + try: + try: + l = py.std.traceback.format_exception(*rawexcinfo) + l.insert(0, "NOTE: Incompatible Exception Representation, " + "displaying natively:\n\n") + pytest.fail("".join(l), pytrace=False) + except (pytest.fail.Exception, KeyboardInterrupt): + raise + except: + pytest.fail("ERROR: Unknown Incompatible Exception " + "representation:\n%r" %(rawexcinfo,), pytrace=False) + except pytest.fail.Exception: + self._excinfo = py.code.ExceptionInfo() + except KeyboardInterrupt: + raise + def addError(self, testcase, rawexcinfo): - py.builtin._reraise(*rawexcinfo) + self._addexcinfo(rawexcinfo) def addFailure(self, testcase, rawexcinfo): - py.builtin._reraise(*rawexcinfo) + self._addexcinfo(rawexcinfo) def addSuccess(self, testcase): pass def stopTest(self, testcase): @@ -50,3 +74,11 @@ class TestCaseFunction(pytest.Function): def runtest(self): testcase = self.parent.obj(self.name) testcase(result=self) + + at pytest.mark.tryfirst +def pytest_runtest_makereport(item, call): + if isinstance(item, TestCaseFunction): + if item._excinfo: + call.excinfo = item._excinfo + item._excinfo = None + del call.result --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -142,6 +142,7 @@ class BaseReport(object): def pytest_runtest_makereport(item, call): when = call.when keywords = dict([(x,1) for x in item.keywords]) + excinfo = call.excinfo if not call.excinfo: outcome = "passed" longrepr = None @@ -312,8 +313,9 @@ class OutcomeException(Exception): """ OutcomeException and its subclass instances indicate and contain info about test and collection outcomes. """ - def __init__(self, msg=None): + def __init__(self, msg=None, pytrace=True): self.msg = msg + self.pytrace = pytrace def __repr__(self): if self.msg: @@ -355,10 +357,10 @@ def skip(msg=""): raise Skipped(msg=msg) skip.Exception = Skipped -def fail(msg=""): +def fail(msg="", pytrace=True): """ explicitely fail an currently-executing test with the given Message. """ __tracebackhide__ = True - raise Failed(msg=msg) + raise Failed(msg=msg, pytrace=pytrace) fail.Exception = Failed --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -103,3 +103,65 @@ def test_class_setup(testdir): """) reprec = testdir.inline_run(testpath) reprec.assertoutcome(passed=3) + + + at pytest.mark.multi(type=['Error', 'Failure']) +def test_testcase_adderrorandfailure_defers(testdir, type): + testdir.makepyfile(""" + from unittest import TestCase + import pytest + class MyTestCase(TestCase): + def run(self, result): + excinfo = pytest.raises(ZeroDivisionError, lambda: 0/0) + try: + result.add%s(self, excinfo._excinfo) + except KeyboardInterrupt: + raise + except: + pytest.fail("add%s should not raise") + def test_hello(self): + pass + """ % (type, type)) + result = testdir.runpytest() + assert 'should not raise' not in result.stdout.str() + + at pytest.mark.multi(type=['Error', 'Failure']) +def test_testcase_custom_exception_info(testdir, type): + testdir.makepyfile(""" + from unittest import TestCase + import py, pytest + class MyTestCase(TestCase): + def run(self, result): + excinfo = pytest.raises(ZeroDivisionError, lambda: 0/0) + # we fake an incompatible exception info + from _pytest.monkeypatch import monkeypatch + mp = monkeypatch() + def t(*args): + mp.undo() + raise TypeError() + mp.setattr(py.code, 'ExceptionInfo', t) + try: + excinfo = excinfo._excinfo + result.add%(type)s(self, excinfo) + finally: + mp.undo() + def test_hello(self): + pass + """ % locals()) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "NOTE: Incompatible Exception Representation*", + "*ZeroDivisionError*", + "*1 failed*", + ]) + +def test_testcase_totally_incompatible_exception_info(testdir): + item, = testdir.getitems(""" + from unittest import TestCase + class MyTestCase(TestCase): + def test_hello(self): + pass + """) + item.addError(None, 42) + excinfo = item._excinfo + assert 'ERROR: Unknown Incompatible' in str(excinfo.getrepr()) --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -334,6 +334,17 @@ def test_pytest_fail(): s = excinfo.exconly(tryshort=True) assert s.startswith("Failed") +def test_pytest_fail_notrace(testdir): + testdir.makepyfile(""" + import pytest + def test_hello(): + pytest.fail("hello", pytrace=False) + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "hello" + ]) + def test_exception_printing_skip(): try: pytest.skip("hello") --- a/_pytest/python.py +++ b/_pytest/python.py @@ -329,6 +329,9 @@ class FunctionMixin(PyobjMixin): def repr_failure(self, excinfo, outerr=None): assert outerr is None, "XXX outerr usage is deprecated" + if excinfo.errisinstance(pytest.fail.Exception): + if not excinfo.value.pytrace: + return str(excinfo.value) return self._repr_failure_py(excinfo, style=self.config.option.tbstyle) From commits-noreply at bitbucket.org Tue Nov 23 16:11:20 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 09:11:20 -0600 (CST) Subject: [py-svn] pytest commit d31c01dbd371: fix #128 show tracebacks for all failures and errors that haven't beed PDB-debugged Message-ID: <20101123151120.2C440241210@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290525047 -3600 # Node ID d31c01dbd371b5a99b25eee092812a0494dc2e49 # Parent a864bc3f8b16d5d0895d5b093e202aed4652edfd fix #128 show tracebacks for all failures and errors that haven't beed PDB-debugged --- a/CHANGELOG +++ b/CHANGELOG @@ -13,6 +13,7 @@ Changes between 1.3.4 and 2.0.0dev0 is removed). - add a new "-q" option which decreases verbosity and prints a more nose/unittest-style "dot" output. +- fix issue135 - marks now work with unittest test cases as well - fix issue126 - introduce py.test.set_trace() to trace execution via PDB during the running of tests even if capturing is ongoing. - fix issue123 - new "python -m py.test" invocation for py.test --- a/_pytest/runner.py +++ b/_pytest/runner.py @@ -358,7 +358,9 @@ def skip(msg=""): skip.Exception = Skipped def fail(msg="", pytrace=True): - """ explicitely fail an currently-executing test with the given Message. """ + """ explicitely fail an currently-executing test with the given Message. + if @pytrace is not True the msg represents the full failure information. + """ __tracebackhide__ = True raise Failed(msg=msg, pytrace=pytrace) fail.Exception = Failed --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -332,13 +332,21 @@ class TerminalReporter: # # summaries for sessionfinish # + def getreports(self, name): + l = [] + for x in self.stats.get(name, []): + if not hasattr(x, '_pdbshown'): + l.append(x) + return l def summary_failures(self): - tbstyle = self.config.option.tbstyle - if 'failed' in self.stats and tbstyle != "no": + if self.config.option.tbstyle != "no": + reports = self.getreports('failed') + if not reports: + return self.write_sep("=", "FAILURES") - for rep in self.stats['failed']: - if tbstyle == "line": + for rep in reports: + if self.config.option.tbstyle == "line": line = self._getcrashline(rep) self.write_line(line) else: @@ -347,7 +355,10 @@ class TerminalReporter: rep.toterminal(self._tw) def summary_errors(self): - if 'error' in self.stats and self.config.option.tbstyle != "no": + if self.config.option.tbstyle != "no": + reports = self.getreports('error') + if not reports: + return self.write_sep("=", "ERRORS") for rep in self.stats['error']: msg = self._getfailureheadline(rep) --- a/testing/test_pdb.py +++ b/testing/test_pdb.py @@ -143,3 +143,11 @@ class TestPDB: child.expect("x = 5") child.sendeof() child.wait() + + def test_pdb_collection_failure_is_shown(self, testdir): + p1 = testdir.makepyfile("""xxx """) + result = testdir.runpytest("--pdb", p1) + result.stdout.fnmatch_lines([ + "*NameError*xxx*", + "*1 error*", + ]) --- a/pytest.py +++ b/pytest.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev32' +__version__ = '2.0.0.dev33' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/_pytest/pdb.py +++ b/_pytest/pdb.py @@ -42,10 +42,6 @@ def pytest_runtest_makereport(): pytestPDB.item = None class PdbInvoke: - def pytest_sessionfinish(self, session): - # don't display failures again at the end - session.config.option.tbstyle = "no" - @pytest.mark.tryfirst def pytest_runtest_makereport(self, item, call, __multicall__): if not call.excinfo or \ @@ -62,6 +58,7 @@ class PdbInvoke: rep.toterminal(tw) tw.sep(">", "entering PDB") post_mortem(call.excinfo._excinfo[2]) + rep._pdbshown = True return rep def post_mortem(t): --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev32', + version='2.0.0.dev33', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From commits-noreply at bitbucket.org Tue Nov 23 17:12:42 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 10:12:42 -0600 (CST) Subject: [py-svn] py commit c4f9141c8ef5: make this package be the prime "py" package again to simplify release and packaging Message-ID: <20101123161242.F4230240F75@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project py # URL http://bitbucket.org/hpk42/py/overview # User holger krekel # Date 1290528144 -3600 # Node ID c4f9141c8ef5678cc4909f31faac048ce928c9d6 # Parent 7799c5a50b52b28ebcf1eb543c97d790ec53db4f make this package be the prime "py" package again to simplify release and packaging --- a/setup.py +++ b/setup.py @@ -6,12 +6,10 @@ from setuptools import setup def main(): setup( - name='pylib', - description='pylib: cross-python path, ini-parsing, io, code, log facilities', + name='py', + description='library with cross-python path, ini-parsing, io, code, log facilities', long_description = open('README.txt').read(), - install_requires=['py>=1.3.9', ], # force newer py version which removes 'py' namespace - # # so we can occupy it - version='2.0.0.dev8', + version='1.4.0a2', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From commits-noreply at bitbucket.org Tue Nov 23 17:19:05 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 10:19:05 -0600 (CST) Subject: [py-svn] py commit 572f3f908bce: depend on correct pytest version Message-ID: <20101123161905.C87D01E1167@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project py # URL http://bitbucket.org/hpk42/py/overview # User holger krekel # Date 1290529133 -3600 # Node ID 572f3f908bceffa7bfe1590846e711a41b7f9524 # Parent c4f9141c8ef5678cc4909f31faac048ce928c9d6 depend on correct pytest version --- a/tox.ini +++ b/tox.ini @@ -6,6 +6,7 @@ indexserver= [testenv] changedir=testing commands=py.test -rfsxX --junitxml={envlogdir}/junit-{envname}.xml [] +deps=pytest>=2.0.0.dev34 [testenv:py27-xdist] basepython=python2.7 From commits-noreply at bitbucket.org Tue Nov 23 17:23:22 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 10:23:22 -0600 (CST) Subject: [py-svn] pytest commit d5ce992fb510: depend on py, not pylib distro Message-ID: <20101123162322.47FD66C125F@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290529294 -3600 # Node ID d5ce992fb51029853daa89561c00fd24a881f793 # Parent d31c01dbd371b5a99b25eee092812a0494dc2e49 depend on py, not pylib distro --- a/pytest.py +++ b/pytest.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev33' +__version__ = '2.0.0.dev34' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,14 +22,14 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev33', + version='2.0.0.dev34', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], author='holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others', author_email='holger at merlinux.eu', entry_points= make_entry_points(), - install_requires=['pylib>=1.9.9'], + install_requires=['py>=1.4.0a2'], classifiers=['Development Status :: 5 - Production', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', --- a/tox.ini +++ b/tox.ini @@ -15,7 +15,7 @@ deps= [testenv:genscript] changedir=. commands= py.test --genscript=pytest1 -deps=pylib +deps=py>=1.4.0a2 [testenv:py27-xdist] changedir=. @@ -41,7 +41,7 @@ commands= make html [testenv:py31] -deps=pylib +deps=py>=1.4.0a2 [testenv:py31-xdist] deps=pytest-xdist @@ -50,7 +50,7 @@ commands= --junitxml={envlogdir}/junit-{envname}.xml [] [testenv:py32] -deps=pylib +deps=py>=1.4.0a2 [testenv:pypy] basepython=pypy-c From commits-noreply at bitbucket.org Tue Nov 23 17:54:59 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 10:54:59 -0600 (CST) Subject: [py-svn] py commit acc532fe1fca: fix docs to point to 1.4.0 Message-ID: <20101123165459.C4CF16C125F@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project py # URL http://bitbucket.org/hpk42/py/overview # User holger krekel # Date 1290531288 -3600 # Node ID acc532fe1fcaa6effcc820afa7b8a80502cd00e8 # Parent 572f3f908bceffa7bfe1590846e711a41b7f9524 fix docs to point to 1.4.0 --- a/doc/conf.py +++ b/doc/conf.py @@ -48,11 +48,11 @@ copyright = u'2010, holger krekel et. al # built documents. # # The short X.Y version. -version = '2.0.0' # The full version, including alpha/beta/rc tags. import py, pytest assert py.path.local().relto(py.path.local(py.__file__).dirpath().dirpath()) release = py.__version__ +version = ".".join(release.split(".")[:2]) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. --- a/doc/announce/release-2.0.0.txt +++ /dev/null @@ -1,23 +0,0 @@ - -.. _`release-2.0.0`: - -pylib 2.0.0: cross-platform library for path, code, io, ... manipulations -=========================================================================== - -"pylib" is a library comprising APIs for filesystem and svn path manipulations, -dynamic code construction, IO capturing and a Python2/Python3 compatibility -namespace. It runs unmodified on all Python interpreters compatible to -Python2.4 up until Python 3.2. "pylib" functionality used to be contained in a -PyPI distribution named "py" and then contained "py.test" and other -command line tools. This is now history. "pytest" is its own distribution -and "pylib" can be and is used completely separately from py.test. -The other "py.*" command line tools are installed with the new -separate "pycmd" distribution. - -The general idea for "pylib" is to place high value on providing -some basic APIs that are continously tested against many Python -interpreters and thus also to help transition. - -cheers, -holger - --- /dev/null +++ b/doc/announce/release-1.4.0.txt @@ -0,0 +1,21 @@ + +.. _`release-1.4.0`: + +pylib 1.4.0: cross-platform library for path, code, io, ... manipulations +=========================================================================== + +"py" is a small library comprising APIs for filesystem and svn path manipulations, +dynamic code construction and introspection, IO capturing and a Python2/Python3 +compatibility namespace. It runs unmodified on all Python interpreters compatible +to Python2.4 up until Python 3.2. + +The py distribution prior to 1.4.0 used to contain "py.test" which now +comes as its own "pytest" distribution. Also, the "py.cleanup|py.lookup|py.countloc" +etc. helpers are now part of pycmd. This makes "py-1.4.0" a simple library +not installing any command line utilities. The general idea for "py" is to +place high value on providing some basic APIs that are continously tested +against many Python interpreters and thus also to help transition. + +cheers, +holger + --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,5 @@ -Changes between 1.3.4 and 2.0.0dev0 +Changes between 1.3.4 and 1.4.0dev0 ================================================== - py.test was moved to a separate "pytest" package. What remains is From commits-noreply at bitbucket.org Tue Nov 23 19:06:29 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 12:06:29 -0600 (CST) Subject: [py-svn] py commit 85a43322235d: improve assertion heuristics - the starting line needs to contain "assert " now Message-ID: <20101123180629.2B39F6C12F5@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project py # URL http://bitbucket.org/hpk42/py/overview # User holger krekel # Date 1290535574 -3600 # Node ID 85a43322235d4d67a4e1d41226847fa977544291 # Parent acc532fe1fcaa6effcc820afa7b8a80502cd00e8 improve assertion heuristics - the starting line needs to contain "assert " now --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ def main(): name='py', description='library with cross-python path, ini-parsing, io, code, log facilities', long_description = open('README.txt').read(), - version='1.4.0a2', + version='1.4.0a3', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/code/test_assertion.py +++ b/testing/code/test_assertion.py @@ -251,3 +251,15 @@ def test_assert_customizable_reprcompare e = exvalue() s = str(e) assert "hello" in s + +def test_assert_long_source(): + result = None + try: + assert result == [ + (None, ['somet text', 'more text']), + ] + except AssertionError: + e = exvalue() + s = str(e) + assert 're-run' not in s + assert 'somet text' in s --- a/py/_code/assertion.py +++ b/py/_code/assertion.py @@ -63,8 +63,10 @@ class AssertionError(BuiltinAssertionErr else: f = py.code.Frame(sys._getframe(1)) try: - source = f.statement - source = str(source.deindent()).strip() + source = f.code.fullsource + if source is not None: + source = source.getstatement(f.lineno, assertion=True) + source = str(source.deindent()).strip() except py.error.ENOENT: source = None # this can also occur during reinterpretation, when the --- a/py/_code/source.py +++ b/py/_code/source.py @@ -98,14 +98,14 @@ class Source(object): newsource.lines = [(indent+line) for line in self.lines] return newsource - def getstatement(self, lineno): + def getstatement(self, lineno, assertion=False): """ return Source statement which contains the given linenumber (counted from 0). """ - start, end = self.getstatementrange(lineno) + start, end = self.getstatementrange(lineno, assertion) return self[start:end] - def getstatementrange(self, lineno): + def getstatementrange(self, lineno, assertion=False): """ return (start, end) tuple which spans the minimal statement region which containing the given lineno. """ @@ -117,6 +117,10 @@ class Source(object): # 1. find the start of the statement from codeop import compile_command for start in range(lineno, -1, -1): + if assertion: + if "assert " not in self.lines[start]: + continue + trylines = self.lines[start:lineno+1] # quick hack to indent the source and get it as a string in one go trylines.insert(0, 'def xxx():') --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev8' +__version__ = '1.4.0a3' from py import _apipkg From commits-noreply at bitbucket.org Tue Nov 23 19:12:57 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 12:12:57 -0600 (CST) Subject: [py-svn] pytest commit 39967d4a7b5d: fix the py version check Message-ID: <20101123181257.4BC976C12F5@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290535881 -3600 # Node ID 39967d4a7b5d6d4b85f3cec58cd2f831dcaa4192 # Parent d5ce992fb51029853daa89561c00fd24a881f793 fix the py version check --- a/pytest.py +++ b/pytest.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev34' +__version__ = '2.0.0.dev35' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev34', + version='2.0.0.dev35', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/_pytest/core.py +++ b/_pytest/core.py @@ -8,7 +8,7 @@ import inspect import py from _pytest import hookspec # the extension point definitions -assert py.__version__.split(".")[:2] >= ['2', '0'], ("installation problem: " +assert py.__version__.split(".")[:2] >= ['1', '4'], ("installation problem: " "%s is too old, remove or upgrade 'py'" % (py.__version__)) default_plugins = ( From commits-noreply at bitbucket.org Tue Nov 23 19:25:35 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 12:25:35 -0600 (CST) Subject: [py-svn] py commit beee8a9b2fe5: some doc fixes Message-ID: <20101123182535.EC04C1E1280@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project py # URL http://bitbucket.org/hpk42/py/overview # User holger krekel # Date 1290536723 -3600 # Node ID beee8a9b2fe566759dd381b1e4ce5b83731eaa90 # Parent 85a43322235d4d67a4e1d41226847fa977544291 some doc fixes --- a/doc/Makefile +++ b/doc/Makefile @@ -37,7 +37,7 @@ clean: -rm -rf $(BUILDDIR)/* install: clean html - rsync -avz _build/html/ code:www-pylib/2.0.0 + rsync -avz _build/html/ code:www-pylib/1.4.0 html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html @@ -75,17 +75,17 @@ qthelp: @echo @echo "Build finished; now you can run "qcollectiongenerator" with the" \ ".qhcp project file in $(BUILDDIR)/qthelp, like this:" - @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/pylib.qhcp" + @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/py.qhcp" @echo "To view the help file:" - @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/pylib.qhc" + @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/py.qhc" devhelp: $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp @echo @echo "Build finished." @echo "To view the help file:" - @echo "# mkdir -p $$HOME/.local/share/devhelp/pylib" - @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/pylib" + @echo "# mkdir -p $$HOME/.local/share/devhelp/py" + @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/py" @echo "# devhelp" epub: --- a/doc/install.txt +++ b/doc/install.txt @@ -5,7 +5,7 @@ installation info in a nutshell =================================================== -**PyPI name**: pylib_ +**PyPI name**: py_ **Pythons**: 2.4, 2.5, 2.6, 2.7, 3.0, 3.1.x, Jython-2.5.1, PyPy-1.3 @@ -15,8 +15,7 @@ installation info in a nutshell **Installers**: ``easy_install`` and ``pip`` -**hg repository**: https://bitbucket.org/hpk42/pylib - +**hg repository**: https://bitbucket.org/hpk42/py easy install ----------------------------- @@ -24,12 +23,17 @@ easy install Both `Distribute`_ and setuptools_ provide the ``easy_install`` installation tool with which you can type into a command line window:: - easy_install -U pylib + easy_install -U py to install the latest release of the py lib. The ``-U`` switch will trigger an upgrade if you already have an older version installed. -Note that setuptools works ok with Python2 interpreters while `Distribute`_ -additionally works with Python3 and also avoid some issues on Windows. + +.. note:: + + As of version 1.4 py does not contain py.test anymore - you + need to install the new `pytest`_ distribution. + +.. _pytest: http://pytest.org Working from version control or a tarball ----------------------------------------------- --- a/doc/code.txt +++ b/doc/code.txt @@ -2,15 +2,11 @@ py.code: higher level python code and introspection objects ================================================================================ -The ``py.code`` part of the pylib contains some functionality to help -dealing with Python code objects. Even though working with Python's internal -code objects (as found on frames and callables) can be very powerful, it's -usually also quite cumbersome, because the API provided by core Python is -relatively low level and not very accessible. - -The ``py.code`` library tries to simplify accessing the code objects as well -as creating them. There is a small set of interfaces a user needs to deal with, -all nicely bundled together, and with a rich set of 'Pythonic' functionality. +``py.code`` provides higher level APIs and objects for Code, Frame, Traceback, +ExceptionInfo and source code construction. The ``py.code`` library +tries to simplify accessing the code objects as well as creating them. +There is a small set of interfaces a user needs to deal with, all nicely +bundled together, and with a rich set of 'Pythonic' functionality. Contents of the library ======================= @@ -47,7 +43,7 @@ A quick example:: "def read(self, mode='r'):" .. autoclass:: py.code.Code - :members: + :members: :inherited-members: @@ -73,7 +69,7 @@ Example:: 'print "foo"' .. autoclass:: py.code.Source - :members: + :members: ``py.code.Traceback`` @@ -101,7 +97,7 @@ Example:: True .. autoclass:: py.code.Traceback - :members: + :members: ``py.code.Frame`` -------------------- @@ -123,7 +119,7 @@ Example (using the 'first' TracebackItem ['cls', 'path'] .. autoclass:: py.code.Frame - :members: + :members: ``py.code.ExceptionInfo`` ---------------------------- @@ -147,5 +143,5 @@ Example:: "NameError: name 'foobar' is not defined" .. autoclass:: py.code.ExceptionInfo - :members: + :members: --- a/doc/conf.py +++ b/doc/conf.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- # -# pylib documentation build configuration file, created by +# py documentation build configuration file, created by # sphinx-quickstart on Thu Oct 21 08:30:10 2010. # # This file is execfile()d with the current directory set to its containing dir. @@ -40,7 +40,7 @@ source_suffix = '.txt' master_doc = 'index' # General information about the project. -project = u'pylib' +project = u'py' copyright = u'2010, holger krekel et. al.' # The version info for the project you're documenting, acts as replacement for @@ -166,7 +166,7 @@ html_static_path = ['_static'] #html_file_suffix = None # Output file base name for HTML help builder. -htmlhelp_basename = 'pylibdoc' +htmlhelp_basename = 'py' # -- Options for LaTeX output -------------------------------------------------- @@ -180,7 +180,7 @@ htmlhelp_basename = 'pylibdoc' # Grouping the document tree into LaTeX files. List of tuples # (source start file, target name, title, author, documentclass [howto/manual]). latex_documents = [ - ('index', 'pylib.tex', u'pylib Documentation', + ('index', 'py.tex', u'py Documentation', u'holger krekel et. al.', 'manual'), ] @@ -213,7 +213,7 @@ latex_documents = [ # One entry per manual page. List of tuples # (source start file, name, description, authors, manual section). man_pages = [ - ('index', 'pylib', u'pylib Documentation', + ('index', 'py', u'py Documentation', [u'holger krekel et. al.'], 1) ] @@ -223,7 +223,7 @@ autodoc_default_flags = "inherited-membe # -- Options for Epub output --------------------------------------------------- # Bibliographic Dublin Core info. -epub_title = u'pylib' +epub_title = u'py' epub_author = u'holger krekel et. al.' epub_publisher = u'holger krekel et. al.' epub_copyright = u'2010, holger krekel et. al.' --- a/doc/index.txt +++ b/doc/index.txt @@ -1,12 +1,18 @@ -.. pylib documentation master file, created by +.. py documentation master file, created by sphinx-quickstart on Thu Oct 21 08:30:10 2010. You can adapt this file completely to your liking, but it should at least contain the root `toctree` directive. -Welcome to pylib's documentation! +Welcome to py's documentation! ================================= -:ref:`2.0.0 release announcement ` and :ref:`CHANGELOG ` +:ref:`1.4.0 release announcement ` and :ref:`CHANGELOG ` + +.. note:: + + py.test now comes from the `pytest distribtion`_ + +.. _`pytest distribution`: http://pypi.python.org/pypi/pytest Contents: --- a/doc/announce/release-1.4.0.txt +++ b/doc/announce/release-1.4.0.txt @@ -1,7 +1,7 @@ .. _`release-1.4.0`: -pylib 1.4.0: cross-platform library for path, code, io, ... manipulations +py-1.4.0: lib for path, code, io, ... manipulations =========================================================================== "py" is a small library comprising APIs for filesystem and svn path manipulations, From commits-noreply at bitbucket.org Wed Nov 24 00:28:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 17:28:01 -0600 (CST) Subject: [py-svn] pytest commit 3b14dfd0a106: improve docs Message-ID: <20101123232801.F3ED21E126F@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290554602 -3600 # Node ID 3b14dfd0a106c184022acc1e8cda36c42ae9ec61 # Parent 39967d4a7b5d6d4b85f3cec58cd2f831dcaa4192 improve docs --- a/doc/faq.txt +++ b/doc/faq.txt @@ -16,9 +16,10 @@ Some historic, some practical reasons: ` the ``py`` package which provided several developer utitilities, all starting with ``py.``, providing nice TAB-completion. If you install ``pip install pycmd`` you get these tools from a separate -package. These days the command line tool could be ``pytest`` -but then many people have gotten used to the old name and there -also is another tool with this same which would lead to some clashes. +package. These days the command line tool could be called ``pytest`` +but then again many people have gotten used to the old name and there +is another tool named "pytest" so we just decided to stick with +``py.test``. What's py.test's relation to ``nosetests``? +++++++++++++++++++++++++++++++++++++++++++++++++ @@ -35,11 +36,12 @@ cycle. What's this "magic" with py.test? ++++++++++++++++++++++++++++++++++++++++++ -Around 2007 (version ``0.8``) some several people claimed that py.test -was using too much "magic". It has been refactored a lot. It is today +Around 2007 (version ``0.8``) some people claimed that py.test +was using too much "magic". It has been refactored a lot. Thrown +out old code. Deprecated unused approaches and code. And it is today probably one of the smallest, most universally runnable and most -customizable testing frameworks for Python. It remains true -that ``py.test`` uses metaprogramming techniques, i.e. it views +customizable testing frameworks for Python. It's true that +``py.test`` uses metaprogramming techniques, i.e. it views test code similar to how compilers view programs, using a somewhat abstract internal model. --- a/doc/_static/sphinxdoc.css +++ b/doc/_static/sphinxdoc.css @@ -17,7 +17,7 @@ body { font-family: 'Lucida Grande', 'Lucida Sans Unicode', 'Geneva', 'Verdana', sans-serif; - font-size: 14px; + font-size: 1.1em; letter-spacing: -0.01em; line-height: 150%; text-align: center; @@ -38,7 +38,7 @@ div.document { } div.bodywrapper { - margin: 0 360px 0 0; + margin: 0 240px 0 0; border-right: 1px solid #ccc; } @@ -88,7 +88,7 @@ div.sphinxsidebarwrapper { div.sphinxsidebar { margin: 0; padding: 0.5em 15px 15px 0; - width: 310px; + width: 210px; float: right; font-size: 1em; text-align: left; @@ -99,7 +99,7 @@ div.sphinxsidebar h3, div.sphinxsidebar font-size: 1em; padding: 0.1em 0 0.1em 0.5em; color: white; - border: 2px solid #86989B; + border: 1px solid #86989B; background-color: #AFC1C4; } --- /dev/null +++ b/doc/_templates/layout.html @@ -0,0 +1,41 @@ +{% extends "!layout.html" %} + +{% block relbar1 %} +{% endblock %} +{% block relbar2 %} +{% endblock %} + +{% block rootrellink %} +{% endblock %} +{% block sidebarrel %} +{% endblock %} + +{% block header %} +
+

tox: virtualenv-based automation of test activities

+
+ home |  + install |  + examples |  + customize |  + contact  +
+
+{% endblock %} + +{% block footer %} +{{ super() }} + +{% endblock %} --- a/doc/conf.py +++ b/doc/conf.py @@ -135,6 +135,7 @@ html_static_path = ['_static'] # Custom sidebar templates, maps document names to template names. #html_sidebars = {} +html_sidebars = {'index': 'indexsidebar.html'} # Additional templates that should be rendered to pages, maps page names to # template names. --- /dev/null +++ b/doc/_templates/indexsidebar.html @@ -0,0 +1,21 @@ +

Download

+{% if version.endswith('(hg)') %} +

This documentation is for version {{ version }}, which is + not released yet.

+

You can use it from the + Bitbucket Repo or look for + released versions in the Python + Package Index.

+{% else %} +

Current: {{ version }} +[Changes]

+

+pytest on PyPI +

+
pip install pytest
+{% endif %} + +

Questions? Suggestions?

+ +

Checkout support channels +

--- a/doc/announce/release-2.0.0.txt +++ b/doc/announce/release-2.0.0.txt @@ -2,13 +2,18 @@ py.test 2.0.0: improved asserts, unittes =========================================================================== Welcome to pytest-2.0.0, a major new release of "py.test", the rapid -easy Python testing tool. There are many new features and a lot of -long-deprecated code is removed, resulting in a much smaller and cleaner -implementation. If you had a test suite using non-deprecated -functionality 2.0.0 is very likely to continue to work for you without -requiring changes. +easy Python testing tool. There are many new features and enhancements, +see below. Also a lot of long-deprecated code has been removed, +resulting in a much smaller and cleaner implementation. Note that +pytest used to part of the "py" distribution up until version py-1.3.4 +but this has changed now: pytest-2.0.0 only contains py.test related +code and is expected to be backward-compatible to existing test code. If +you want to install it, type one of:: -Check out the revised the improved documentation and examples: + pip install -U pytest + easy_install -U pytest + +and check out the much improved documentation and examples: http://pytest.org/2.0.0/index.html @@ -136,6 +141,7 @@ More Detailed Changes between 1.3.4 and is removed). - add a new "-q" option which decreases verbosity and prints a more nose/unittest-style "dot" output. +- fix issue135 - marks now work with unittest test cases as well - fix issue126 - introduce py.test.set_trace() to trace execution via PDB during the running of tests even if capturing is ongoing. - fix issue123 - new "python -m py.test" invocation for py.test @@ -160,3 +166,6 @@ More Detailed Changes between 1.3.4 and - fix issue93 stdout/stderr is captured while importing conftest.py - fix bug: unittest collected functions now also can have "pytestmark" applied at class/module level +- add ability to use "class" level for cached_setup helper +- fix strangeness: mark.* objects are now immutable, create new instances + From commits-noreply at bitbucket.org Wed Nov 24 00:28:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 17:28:02 -0600 (CST) Subject: [py-svn] pytest commit 079d3f4bb7b3: allow setup_class in unittest test cases Message-ID: <20101123232802.1965C1E128B@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290554619 -3600 # Node ID 079d3f4bb7b3757e1d3a42cde8f2dad997523953 # Parent 3b14dfd0a106c184022acc1e8cda36c42ae9ec61 allow setup_class in unittest test cases --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -26,11 +26,13 @@ class UnitTestCase(pytest.Class): meth = getattr(self.obj, 'setUpClass', None) if meth is not None: meth() + super(UnitTestCase, self).setup() def teardown(self): meth = getattr(self.obj, 'tearDownClass', None) if meth is not None: meth() + super(UnitTestCase, self).teardown() class TestCaseFunction(pytest.Function): _excinfo = None --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev35', + version='2.0.0.dev36', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -82,7 +82,7 @@ def test_module_level_pytestmark(testdir reprec = testdir.inline_run(testpath, "-s") reprec.assertoutcome(skipped=1) -def test_class_setup(testdir): +def test_setup_setUpClass(testdir): testpath = testdir.makepyfile(""" import unittest import pytest @@ -104,6 +104,26 @@ def test_class_setup(testdir): reprec = testdir.inline_run(testpath) reprec.assertoutcome(passed=3) +def test_setup_class(testdir): + testpath = testdir.makepyfile(""" + import unittest + import pytest + class MyTestCase(unittest.TestCase): + x = 0 + def setup_class(cls): + cls.x += 1 + def test_func1(self): + assert self.x == 1 + def test_func2(self): + assert self.x == 1 + def teardown_class(cls): + cls.x -= 1 + def test_teareddown(): + assert MyTestCase.x == 0 + """) + reprec = testdir.inline_run(testpath) + reprec.assertoutcome(passed=3) + @pytest.mark.multi(type=['Error', 'Failure']) def test_testcase_adderrorandfailure_defers(testdir, type): --- a/pytest.py +++ b/pytest.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev35' +__version__ = '2.0.0.dev36' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins From commits-noreply at bitbucket.org Wed Nov 24 03:05:20 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 20:05:20 -0600 (CST) Subject: [py-svn] pytest commit af0ac4661ead: tw is unused here Message-ID: <20101124020520.949E9241210@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1290564340 21600 # Node ID af0ac4661ead95d62747f34939a1e449a022200d # Parent 079d3f4bb7b3757e1d3a42cde8f2dad997523953 tw is unused here --- a/_pytest/junitxml.py +++ b/_pytest/junitxml.py @@ -170,5 +170,4 @@ class LogXML(object): logfile.close() def pytest_terminal_summary(self, terminalreporter): - tw = terminalreporter._tw - terminalreporter.write_sep("-", "generated xml file: %s" %(self.logfile)) + terminalreporter.write_sep("-", "generated xml file: %s" % (self.logfile)) From commits-noreply at bitbucket.org Wed Nov 24 03:26:52 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 20:26:52 -0600 (CST) Subject: [py-svn] pytest commit 0fb3d4517695: some cajoling to get pytest.py to be found when it's not on path Message-ID: <20101124022652.D1AFB6C12F5@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1290565632 21600 # Node ID 0fb3d4517695c142c743c1702efd32015a158d02 # Parent af0ac4661ead95d62747f34939a1e449a022200d some cajoling to get pytest.py to be found when it's not on path --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -17,6 +17,15 @@ def pytest_addoption(parser): help=("discover tools on PATH instead of going through py.cmdline.") ) +def pytest_configure(config): + # This might be called multiple times. Only take the first. + global _pytest_fullpath + import pytest + try: + _pytest_fullpath + except NameError: + _pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc")) + def pytest_funcarg___pytest(request): return PytestArg(request) @@ -456,13 +465,10 @@ class TmpTestdir: def _getpybinargs(self, scriptname): if not self.request.config.getvalue("notoolsonpath"): - import pytest - script = pytest.__file__.rstrip("co") - assert script, "script %r not found" % scriptname # XXX we rely on script refering to the correct environment # we cannot use "(py.std.sys.executable,script)" # becaue on windows the script is e.g. a py.test.exe - return (py.std.sys.executable, script,) + return (py.std.sys.executable, _pytest_fullpath,) else: py.test.skip("cannot run %r with --no-tools-on-path" % scriptname) From commits-noreply at bitbucket.org Wed Nov 24 03:31:46 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Nov 2010 20:31:46 -0600 (CST) Subject: [py-svn] pytest commit 9d430445abb6: python3 fixes Message-ID: <20101124023146.C6F801E126F@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1290565927 21600 # Node ID 9d430445abb6f3235910b3d3d679a16135588e39 # Parent 0fb3d4517695c142c743c1702efd32015a158d02 python3 fixes --- a/bench/empty.py +++ b/bench/empty.py @@ -1,3 +1,3 @@ - +import py for i in range(1000): - exec "def test_func_%d(): pass" % i + py.builtin.exec_("def test_func_%d(): pass" % i) --- a/bench/bench.py +++ b/bench/bench.py @@ -7,4 +7,4 @@ if __name__ == '__main__': p = pstats.Stats("prof") p.strip_dirs() p.sort_stats('cumulative') - print p.print_stats(30) + print(p.print_stats(30)) From commits-noreply at bitbucket.org Wed Nov 24 11:50:10 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 04:50:10 -0600 (CST) Subject: [py-svn] pytest commit 22f3981d50c8: fix #6 : allow skip/xfail/pdb with trial by hacking the raw exception info out from trial Message-ID: <20101124105010.BCEF6241053@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290595735 -3600 # Node ID 22f3981d50c80bea055c39d0e1310dc45e0d5f75 # Parent 9d430445abb6f3235910b3d3d679a16135588e39 fix #6 : allow skip/xfail/pdb with trial by hacking the raw exception info out from trial --- a/tox.ini +++ b/tox.ini @@ -25,6 +25,13 @@ commands= py.test -n3 -rfsxX \ --junitxml={envlogdir}/junit-{envname}.xml [] +[testenv:trial] +changedir=. +basepython=python2.6 +deps=:pypi:twisted +commands= + py.test -rsxf \ + --junitxml={envlogdir}/junit-{envname}.xml [testing/test_unittest.py] [testenv:doctest] changedir=. commands=py.test --doctest-modules _pytest --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -44,8 +44,8 @@ class TestCaseFunction(pytest.Function): pass def _addexcinfo(self, rawexcinfo): - #__tracebackhide__ = True - assert rawexcinfo + # unwrap potential exception info (see twisted trial support below) + rawexcinfo = getattr(rawexcinfo, '_rawexcinfo', rawexcinfo) try: self._excinfo = py.code.ExceptionInfo(rawexcinfo) except TypeError: @@ -60,10 +60,10 @@ class TestCaseFunction(pytest.Function): except: pytest.fail("ERROR: Unknown Incompatible Exception " "representation:\n%r" %(rawexcinfo,), pytrace=False) + except KeyboardInterrupt: + raise except pytest.fail.Exception: self._excinfo = py.code.ExceptionInfo() - except KeyboardInterrupt: - raise def addError(self, testcase, rawexcinfo): self._addexcinfo(rawexcinfo) @@ -84,3 +84,30 @@ def pytest_runtest_makereport(item, call call.excinfo = item._excinfo item._excinfo = None del call.result + +# twisted trial support +def pytest_runtest_protocol(item, __multicall__): + if isinstance(item, TestCaseFunction): + if 'twisted.trial.unittest' in sys.modules: + ut = sys.modules['twisted.python.failure'] + Failure__init__ = ut.Failure.__init__.im_func + check_testcase_implements_trial_reporter() + def excstore(self, exc_value=None, exc_type=None, exc_tb=None): + if exc_value is None: + self._rawexcinfo = sys.exc_info() + else: + self._rawexcinfo = (exc_value, exc_type, exc_tb) + Failure__init__(self, exc_value, exc_type, exc_tb) + ut.Failure.__init__ = excstore + try: + return __multicall__.execute() + finally: + ut.Failure.__init__ = Failure__init__ + +def check_testcase_implements_trial_reporter(done=[]): + if done: + return + from zope.interface import classImplements + from twisted.trial.itrial import IReporter + classImplements(TestCaseFunction, IReporter) + done.append(1) --- a/pytest.py +++ b/pytest.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev36' +__version__ = '2.0.0.dev37' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/_pytest/pdb.py +++ b/_pytest/pdb.py @@ -44,11 +44,11 @@ def pytest_runtest_makereport(): class PdbInvoke: @pytest.mark.tryfirst def pytest_runtest_makereport(self, item, call, __multicall__): + rep = __multicall__.execute() if not call.excinfo or \ call.excinfo.errisinstance(pytest.skip.Exception) or \ call.excinfo.errisinstance(py.std.bdb.BdbQuit): - return - rep = __multicall__.execute() + return rep if "xfail" in rep.keywords: return rep # we assume that the above execute() suspended capturing --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev36', + version='2.0.0.dev37', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -185,3 +185,107 @@ def test_testcase_totally_incompatible_e item.addError(None, 42) excinfo = item._excinfo assert 'ERROR: Unknown Incompatible' in str(excinfo.getrepr()) + + + +class TestTrialUnittest: + def setup_class(cls): + pytest.importorskip("twisted.trial.unittest") + + def test_trial_exceptions_with_skips(self, testdir): + testdir.makepyfile(""" + from twisted.trial import unittest + import pytest + class TC(unittest.TestCase): + def test_hello(self): + pytest.skip("skip_in_method") + @pytest.mark.skipif("sys.version_info != 1") + def test_hello2(self): + pass + @pytest.mark.xfail(reason="iwanto") + def test_hello3(self): + assert 0 + def test_hello4(self): + pytest.xfail("i2wanto") + + class TC2(unittest.TestCase): + def setup_class(cls): + pytest.skip("skip_in_setup_class") + def test_method(self): + pass + """) + result = testdir.runpytest("-rxs") + assert result.ret == 0 + result.stdout.fnmatch_lines_random([ + "*skip_in_setup_class*", + "*iwanto*", + "*i2wanto*", + "*sys.version_info*", + "*skip_in_method*", + "*3 skipped*2 xfail*", + ]) + + def test_trial_pdb(self, testdir): + p = testdir.makepyfile(""" + from twisted.trial import unittest + import pytest + class TC(unittest.TestCase): + def test_hello(self): + assert 0, "hellopdb" + """) + child = testdir.spawn_pytest(p) + child.expect("hellopdb") + child.sendeof() + +def test_djangolike_testcase(testdir): + # contributed from Morten Breekevold + testdir.makepyfile(""" + from unittest import TestCase, main + + class DjangoLikeTestCase(TestCase): + + def setUp(self): + print ("setUp()") + + def test_presetup_has_been_run(self): + print ("test_thing()") + self.assertTrue(hasattr(self, 'was_presetup')) + + def tearDown(self): + print ("tearDown()") + + def __call__(self, result=None): + try: + self._pre_setup() + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + import sys + result.addError(self, sys.exc_info()) + return + super(DjangoLikeTestCase, self).__call__(result) + try: + self._post_teardown() + except (KeyboardInterrupt, SystemExit): + raise + except Exception: + import sys + result.addError(self, sys.exc_info()) + return + + def _pre_setup(self): + print ("_pre_setup()") + self.was_presetup = True + + def _post_teardown(self): + print ("_post_teardown()") + """) + result = testdir.runpytest("-s") + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*_pre_setup()*", + "*setUp()*", + "*test_thing()*", + "*tearDown()*", + "*_post_teardown()*", + ]) From commits-noreply at bitbucket.org Wed Nov 24 14:35:55 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 07:35:55 -0600 (CST) Subject: [py-svn] pytest commit 19df6d3f871f: Finish the test Message-ID: <20101124133555.277461E126B@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Maciej Fijalkowski # Date 1290604000 -7200 # Node ID 19df6d3f871fce3005bbef752f28249669233410 # Parent 48c7827675cc8159dd86b71aee4c29fbb8e20cd5 Finish the test --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -228,7 +228,7 @@ class TestTrialUnittest: def test_trial_error(self, testdir): testdir.makepyfile(""" from twisted.trial.unittest import TestCase - from twisted.internet.defer import inlineCallbacks + from twisted.internet.defer import Deferred from twisted.internet import reactor class TC(TestCase): @@ -238,13 +238,16 @@ class TestTrialUnittest: def test_two(self): def f(_): crash - - return reactor.callLater(0.3, f) + + d = Deferred() + d.addCallback(f) + reactor.callLater(0.3, d.callback, None) + return d def test_three(self): def f(): pass # will never get called - return reactor.callLater(0.3, f) + reactor.callLater(0.3, f) # will crash at teardown def test_four(self): @@ -252,7 +255,10 @@ class TestTrialUnittest: reactor.callLater(0.3, f) crash - return reactor.callLater(0.3, f) + d = Deferred() + d.addCallback(f) + reactor.callLater(0.3, d.callback, None) + return d # will crash both at test time and at teardown """) result = testdir.runpytest() From commits-noreply at bitbucket.org Wed Nov 24 14:35:55 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 07:35:55 -0600 (CST) Subject: [py-svn] pytest commit 84ce63c04007: teach trial support code to throw separate errors/failures for setup/call/teardown Message-ID: <20101124133555.359001E129F@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290605704 -3600 # Node ID 84ce63c04007afe18d1491742bc3c56794a22658 # Parent 19df6d3f871fce3005bbef752f28249669233410 teach trial support code to throw separate errors/failures for setup/call/teardown --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -47,7 +47,7 @@ class TestCaseFunction(pytest.Function): # unwrap potential exception info (see twisted trial support below) rawexcinfo = getattr(rawexcinfo, '_rawexcinfo', rawexcinfo) try: - self._excinfo = py.code.ExceptionInfo(rawexcinfo) + excinfo = py.code.ExceptionInfo(rawexcinfo) except TypeError: try: try: @@ -63,7 +63,8 @@ class TestCaseFunction(pytest.Function): except KeyboardInterrupt: raise except pytest.fail.Exception: - self._excinfo = py.code.ExceptionInfo() + excinfo = py.code.ExceptionInfo() + self.__dict__.setdefault('_excinfo', []).append(excinfo) def addError(self, testcase, rawexcinfo): self._addexcinfo(rawexcinfo) @@ -80,9 +81,8 @@ class TestCaseFunction(pytest.Function): @pytest.mark.tryfirst def pytest_runtest_makereport(item, call): if isinstance(item, TestCaseFunction): - if item._excinfo: - call.excinfo = item._excinfo - item._excinfo = None + if hasattr(item, '_excinfo') and item._excinfo: + call.excinfo = item._excinfo.pop(0) del call.result # twisted trial support --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -183,11 +183,9 @@ def test_testcase_totally_incompatible_e pass """) item.addError(None, 42) - excinfo = item._excinfo + excinfo = item._excinfo.pop(0) assert 'ERROR: Unknown Incompatible' in str(excinfo.getrepr()) - - class TestTrialUnittest: def setup_class(cls): pytest.importorskip("twisted.trial.unittest") @@ -225,6 +223,7 @@ class TestTrialUnittest: "*3 skipped*2 xfail*", ]) + @pytest.mark.xfail(reason="fijal needs add checks") def test_trial_error(self, testdir): testdir.makepyfile(""" from twisted.trial.unittest import TestCase @@ -262,6 +261,7 @@ class TestTrialUnittest: # will crash both at test time and at teardown """) result = testdir.runpytest() + assert 0 def test_trial_pdb(self, testdir): p = testdir.makepyfile(""" @@ -275,6 +275,46 @@ class TestTrialUnittest: child.expect("hellopdb") child.sendeof() + def test_trial_setup_failure_is_shown(self, testdir): + testdir.makepyfile(""" + from twisted.trial import unittest + import pytest + class TC(unittest.TestCase): + def setUp(self): + assert 0, "down1" + def test_method(self): + print ("never42") + xyz + """) + result = testdir.runpytest("-s") + assert result.ret == 1 + result.stdout.fnmatch_lines([ + "*setUp*", + "*assert 0*down1*", + "*1 failed*", + ]) + assert 'never42' not in result.stdout.str() + + def test_trial_teardown_and_test_failure(self, testdir): + testdir.makepyfile(""" + from twisted.trial import unittest + import pytest + class TC(unittest.TestCase): + def tearDown(self): + assert 0, "down1" + def test_method(self): + assert False, "down2" + """) + result = testdir.runpytest("-s") + assert result.ret == 1 + result.stdout.fnmatch_lines([ + "*tearDown*", + "*assert 0*", + "*test_method*", + "*assert False*", + "*1 failed*1 error*", + ]) + def test_djangolike_testcase(testdir): # contributed from Morten Breekevold testdir.makepyfile(""" From commits-noreply at bitbucket.org Wed Nov 24 14:35:55 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 07:35:55 -0600 (CST) Subject: [py-svn] pytest commit 48c7827675cc: A test for trial Message-ID: <20101124133555.4177C1E12B6@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Maciej Fijalkowski # Date 1290603296 -7200 # Node ID 48c7827675cc8159dd86b71aee4c29fbb8e20cd5 # Parent 22f3981d50c80bea055c39d0e1310dc45e0d5f75 A test for trial --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -1,6 +1,6 @@ """ discovery and running of std-library "unittest" style tests. """ import pytest, py -import sys +import sys, pdb def pytest_pycollect_makeitem(collector, name, obj): unittest = sys.modules.get('unittest') @@ -96,7 +96,9 @@ def pytest_runtest_protocol(item, __mult if exc_value is None: self._rawexcinfo = sys.exc_info() else: - self._rawexcinfo = (exc_value, exc_type, exc_tb) + if exc_type is None: + exc_type = type(exc_value) + self._rawexcinfo = (exc_type, exc_value, exc_tb) Failure__init__(self, exc_value, exc_type, exc_tb) ut.Failure.__init__ = excstore try: --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -225,6 +225,38 @@ class TestTrialUnittest: "*3 skipped*2 xfail*", ]) + def test_trial_error(self, testdir): + testdir.makepyfile(""" + from twisted.trial.unittest import TestCase + from twisted.internet.defer import inlineCallbacks + from twisted.internet import reactor + + class TC(TestCase): + def test_one(self): + crash + + def test_two(self): + def f(_): + crash + + return reactor.callLater(0.3, f) + + def test_three(self): + def f(): + pass # will never get called + return reactor.callLater(0.3, f) + # will crash at teardown + + def test_four(self): + def f(_): + reactor.callLater(0.3, f) + crash + + return reactor.callLater(0.3, f) + # will crash both at test time and at teardown + """) + result = testdir.runpytest() + def test_trial_pdb(self, testdir): p = testdir.makepyfile(""" from twisted.trial import unittest From commits-noreply at bitbucket.org Wed Nov 24 16:18:03 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 09:18:03 -0600 (CST) Subject: [py-svn] pytest commit 06270cb47dad: allow setup_method/teardown_method to be mixed into unittest cases, reshuffle tests a bit Message-ID: <20101124151803.562CE1E10AC@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290611869 -3600 # Node ID 06270cb47dadcbff1bb45fb3da8f7d0cb877ae0a # Parent 84ce63c04007afe18d1491742bc3c56794a22658 allow setup_method/teardown_method to be mixed into unittest cases, reshuffle tests a bit --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -36,10 +36,17 @@ class UnitTestCase(pytest.Class): class TestCaseFunction(pytest.Function): _excinfo = None + def setup(self): - pass + self._testcase = self.parent.obj(self.name) + self._obj = getattr(self._testcase, self.name) + if hasattr(self._testcase, 'setup_method'): + self._testcase.setup_method(self._obj) + def teardown(self): - pass + if hasattr(self._testcase, 'teardown_method'): + self._testcase.teardown_method(self._obj) + def startTest(self, testcase): pass @@ -75,13 +82,12 @@ class TestCaseFunction(pytest.Function): def stopTest(self, testcase): pass def runtest(self): - testcase = self.parent.obj(self.name) - testcase(result=self) + self._testcase(result=self) @pytest.mark.tryfirst def pytest_runtest_makereport(item, call): if isinstance(item, TestCaseFunction): - if hasattr(item, '_excinfo') and item._excinfo: + if item._excinfo: call.excinfo = item._excinfo.pop(0) del call.result --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev37', + version='2.0.0.dev38', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/_pytest/pytester.py +++ b/_pytest/pytester.py @@ -552,10 +552,12 @@ class ReportRecorder(object): def getreports(self, names="pytest_runtest_logreport pytest_collectreport"): return [x.report for x in self.getcalls(names)] - def matchreport(self, inamepart="", names="pytest_runtest_logreport pytest_collectreport"): + def matchreport(self, inamepart="", names="pytest_runtest_logreport pytest_collectreport", when=None): """ return a testreport whose dotted import path matches """ l = [] for rep in self.getreports(names=names): + if when and getattr(rep, 'when', None) != when: + continue if not inamepart or inamepart in rep.nodeid.split("::"): l.append(rep) if not l: --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -26,17 +26,24 @@ def test_isclasscheck_issue53(testdir): assert result.ret == 0 def test_setup(testdir): - testpath = testdir.makepyfile(test_two=""" + testpath = testdir.makepyfile(""" import unittest class MyTestCase(unittest.TestCase): def setUp(self): self.foo = 1 - def test_setUp(self): + def setup_method(self, method): + self.foo2 = 1 + def test_both(self): self.assertEquals(1, self.foo) + assert self.foo2 == 1 + def teardown_method(self, method): + assert 0, "42" + """) - reprec = testdir.inline_run(testpath) - rep = reprec.matchreport("test_setUp") - assert rep.passed + reprec = testdir.inline_run("-s", testpath) + assert reprec.matchreport("test_both", when="call").passed + rep = reprec.matchreport("test_both", when="teardown") + assert rep.failed and '42' in str(rep.longrepr) def test_new_instances(testdir): testpath = testdir.makepyfile(""" @@ -53,7 +60,6 @@ def test_new_instances(testdir): def test_teardown(testdir): testpath = testdir.makepyfile(""" import unittest - pytest_plugins = "pytest_unittest" # XXX class MyTestCase(unittest.TestCase): l = [] def test_one(self): @@ -70,17 +76,44 @@ def test_teardown(testdir): assert passed == 2 assert passed + skipped + failed == 2 -def test_module_level_pytestmark(testdir): - testpath = testdir.makepyfile(""" +def test_method_and_teardown_failing_reporting(testdir): + testdir.makepyfile(""" + import unittest, pytest + class TC(unittest.TestCase): + def tearDown(self): + assert 0, "down1" + def test_method(self): + assert False, "down2" + """) + result = testdir.runpytest("-s") + assert result.ret == 1 + result.stdout.fnmatch_lines([ + "*tearDown*", + "*assert 0*", + "*test_method*", + "*assert False*", + "*1 failed*1 error*", + ]) + +def test_setup_failure_is_shown(testdir): + testdir.makepyfile(""" import unittest import pytest - pytestmark = pytest.mark.xfail - class MyTestCase(unittest.TestCase): - def test_func1(self): - assert 0 + class TC(unittest.TestCase): + def setUp(self): + assert 0, "down1" + def test_method(self): + print ("never42") + xyz """) - reprec = testdir.inline_run(testpath, "-s") - reprec.assertoutcome(skipped=1) + result = testdir.runpytest("-s") + assert result.ret == 1 + result.stdout.fnmatch_lines([ + "*setUp*", + "*assert 0*down1*", + "*1 failed*", + ]) + assert 'never42' not in result.stdout.str() def test_setup_setUpClass(testdir): testpath = testdir.makepyfile(""" @@ -186,6 +219,19 @@ def test_testcase_totally_incompatible_e excinfo = item._excinfo.pop(0) assert 'ERROR: Unknown Incompatible' in str(excinfo.getrepr()) +def test_module_level_pytestmark(testdir): + testpath = testdir.makepyfile(""" + import unittest + import pytest + pytestmark = pytest.mark.xfail + class MyTestCase(unittest.TestCase): + def test_func1(self): + assert 0 + """) + reprec = testdir.inline_run(testpath, "-s") + reprec.assertoutcome(skipped=1) + + class TestTrialUnittest: def setup_class(cls): pytest.importorskip("twisted.trial.unittest") @@ -275,46 +321,6 @@ class TestTrialUnittest: child.expect("hellopdb") child.sendeof() - def test_trial_setup_failure_is_shown(self, testdir): - testdir.makepyfile(""" - from twisted.trial import unittest - import pytest - class TC(unittest.TestCase): - def setUp(self): - assert 0, "down1" - def test_method(self): - print ("never42") - xyz - """) - result = testdir.runpytest("-s") - assert result.ret == 1 - result.stdout.fnmatch_lines([ - "*setUp*", - "*assert 0*down1*", - "*1 failed*", - ]) - assert 'never42' not in result.stdout.str() - - def test_trial_teardown_and_test_failure(self, testdir): - testdir.makepyfile(""" - from twisted.trial import unittest - import pytest - class TC(unittest.TestCase): - def tearDown(self): - assert 0, "down1" - def test_method(self): - assert False, "down2" - """) - result = testdir.runpytest("-s") - assert result.ret == 1 - result.stdout.fnmatch_lines([ - "*tearDown*", - "*assert 0*", - "*test_method*", - "*assert False*", - "*1 failed*1 error*", - ]) - def test_djangolike_testcase(testdir): # contributed from Morten Breekevold testdir.makepyfile(""" --- a/pytest.py +++ b/pytest.py @@ -5,7 +5,7 @@ see http://pytest.org for documentation (c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev37' +__version__ = '2.0.0.dev38' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins From commits-noreply at bitbucket.org Wed Nov 24 16:44:15 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 09:44:15 -0600 (CST) Subject: [py-svn] pytest commit 2df60ca00849: also accept non-pytrace pytest.fail() call in setup/teardown methods Message-ID: <20101124154415.B58656C1064@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290613435 -3600 # Node ID 2df60ca008494643bb61267977d3eb66e3be7894 # Parent 06270cb47dadcbff1bb45fb3da8f7d0cb877ae0a also accept non-pytrace pytest.fail() call in setup/teardown methods --- a/testing/test_runner.py +++ b/testing/test_runner.py @@ -339,11 +339,15 @@ def test_pytest_fail_notrace(testdir): import pytest def test_hello(): pytest.fail("hello", pytrace=False) + def teardown_function(function): + pytest.fail("world", pytrace=False) """) result = testdir.runpytest() result.stdout.fnmatch_lines([ - "hello" + "world", + "hello", ]) + assert 'def teardown_function' not in result.stdout.str() def test_exception_printing_skip(): try: --- a/_pytest/python.py +++ b/_pytest/python.py @@ -324,14 +324,14 @@ class FunctionMixin(PyobjMixin): if line.strip().startswith('def'): return FuncargLookupErrorRepr(fspath, lineno, lines[:i+1], str(excinfo.value)) + if excinfo.errisinstance(pytest.fail.Exception): + if not excinfo.value.pytrace: + return str(excinfo.value) return super(FunctionMixin, self)._repr_failure_py(excinfo, style=style) def repr_failure(self, excinfo, outerr=None): assert outerr is None, "XXX outerr usage is deprecated" - if excinfo.errisinstance(pytest.fail.Exception): - if not excinfo.value.pytrace: - return str(excinfo.value) return self._repr_failure_py(excinfo, style=self.config.option.tbstyle) From commits-noreply at bitbucket.org Wed Nov 24 16:48:52 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 09:48:52 -0600 (CST) Subject: [py-svn] py commit ceb0875f32dc: slightly refine assertion heuristic Message-ID: <20101124154852.E81791E10AC@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project py # URL http://bitbucket.org/hpk42/py/overview # User holger krekel # Date 1290613692 -3600 # Node ID ceb0875f32dcd662c4ca8991b7bb6a4363bcb4ee # Parent beee8a9b2fe566759dd381b1e4ce5b83731eaa90 slightly refine assertion heuristic --- a/testing/code/test_assertion.py +++ b/testing/code/test_assertion.py @@ -252,10 +252,9 @@ def test_assert_customizable_reprcompare s = str(e) assert "hello" in s -def test_assert_long_source(): - result = None +def test_assert_long_source_1(): try: - assert result == [ + assert len == [ (None, ['somet text', 'more text']), ] except AssertionError: @@ -263,3 +262,14 @@ def test_assert_long_source(): s = str(e) assert 're-run' not in s assert 'somet text' in s + +def test_assert_long_source_2(): + try: + assert(len == [ + (None, ['somet text', 'more text']), + ]) + except AssertionError: + e = exvalue() + s = str(e) + assert 're-run' not in s + assert 'somet text' in s --- a/py/_code/source.py +++ b/py/_code/source.py @@ -118,7 +118,7 @@ class Source(object): from codeop import compile_command for start in range(lineno, -1, -1): if assertion: - if "assert " not in self.lines[start]: + if "assert" not in self.lines[start]: continue trylines = self.lines[start:lineno+1] From commits-noreply at bitbucket.org Wed Nov 24 22:26:16 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 15:26:16 -0600 (CST) Subject: [py-svn] pytest commit cfcf15d0d1e4: simplify pluginlist computation Message-ID: <20101124212616.48F07241346@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290633772 -3600 # Node ID cfcf15d0d1e4aa25a9d6d45f59f5b6e92a19ed1e # Parent ae80b0f9d8ed34cc84e96d4175beb89e81541120 simplify pluginlist computation --- a/_pytest/session.py +++ b/_pytest/session.py @@ -221,6 +221,9 @@ class Node(object): def listnames(self): return [x.name for x in self.listchain()] + def getplugins(self): + return self.config._getmatchingplugins(self.fspath) + def getparent(self, cls): current = self while current and not isinstance(current, cls): --- a/_pytest/python.py +++ b/_pytest/python.py @@ -208,7 +208,10 @@ class PyCollectorMixin(PyobjMixin, pytes metafunc = Metafunc(funcobj, config=self.config, cls=cls, module=module) gentesthook = self.config.hook.pytest_generate_tests - plugins = getplugins(self, withpy=True) + extra = [module] + if cls is not None: + extra.append(cls()) + plugins = self.getplugins() + extra gentesthook.pcall(plugins, metafunc=metafunc) if not metafunc._calls: return Function(name, parent=self) @@ -497,17 +500,6 @@ def fillfuncargs(function): request = FuncargRequest(pyfuncitem=function) request._fillfuncargs() -def getplugins(node, withpy=False): # might by any node - plugins = node.config._getmatchingplugins(node.fspath) - if withpy: - mod = node.getparent(pytest.Module) - if mod is not None: - plugins.append(mod.obj) - inst = node.getparent(pytest.Instance) - if inst is not None: - plugins.append(inst.obj) - return plugins - _notexists = object() class CallSpec: def __init__(self, funcargs, id, param): @@ -572,7 +564,8 @@ class FuncargRequest: self._pyfuncitem = pyfuncitem if hasattr(pyfuncitem, '_requestparam'): self.param = pyfuncitem._requestparam - self._plugins = getplugins(pyfuncitem, withpy=True) + extra = filter(None, [self.module, self.instance]) + self._plugins = pyfuncitem.getplugins() + extra self._funcargs = self._pyfuncitem.funcargs.copy() self._name2factory = {} self._currentarg = None @@ -741,9 +734,9 @@ def showfuncargs(config): session = Session(config) session.perform_collect() if session.items: - plugins = getplugins(session.items[0]) + plugins = session.items[0].getplugins() else: - plugins = getplugins(session) + plugins = session.getplugins() curdir = py.path.local() tw = py.io.TerminalWriter() verbose = config.getvalue("verbose") From commits-noreply at bitbucket.org Wed Nov 24 22:26:16 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 15:26:16 -0600 (CST) Subject: [py-svn] pytest commit 189bbc321107: [mq]: doc Message-ID: <20101124212616.26185241118@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290621728 -3600 # Node ID 189bbc321107f9186042a3674efd34cda62f36b9 # Parent 2df60ca008494643bb61267977d3eb66e3be7894 [mq]: doc --- a/doc/goodpractises.txt +++ b/doc/goodpractises.txt @@ -22,6 +22,7 @@ server Hudson_. .. _pip: http://pypi.python.org/pypi/pip .. _`test discovery`: +.. _`Python test discovery`: Conventions for Python test discovery ------------------------------------------------- --- a/doc/features.txt +++ b/doc/features.txt @@ -1,16 +1,39 @@ py.test Features ================== -no-boilerplate testing with Python ----------------------------------- +mature no-boilerplate testing with Python +------------------------------------------------ -- automatic, fully customizable Python test discovery -- write simple test functions and freely group tests -- ``assert`` statement for your assertions +- automatic, customizable :ref:`Python test discovery` +- simple test functions and freely groupable tests +- simple ``assert`` statements for assertions - powerful parametrization of test functions -- rely on helpful traceback and failing assertion reporting +- very helpful traceback and failing assertion reporting - use ``print`` or ``pdb`` debugging on failures - enables fully :pep:`8` compliant coding style +- powerful :ref:`usage` possibilities, well sorted command line options +- used in many projects, ranging from 10 to 10K tests +- tested on Unix and Windows from Python 2.4 up to Python 3.1 and 3.2 +- keyword/testname based selection of tests + +supports common testing practises and methods +----------------------------------------------------------- + +- can integrate ``nose``, ``unittest.py`` and ``doctest.py`` style + tests, including running testcases made for Django and trial +- supports extended `xUnit style setup`_ +- supports domain-specific :ref:`non-python tests` +- supports generating testing coverage reports +- `Javasript unit- and functional testing`_ + +.. _`Javasript unit- and functional testing`: http://pypi.python.org/pypi/oejskit + +distributing tests to local/remote subprocesses +-------------------------------------------------------- + +- can distribute tests to multiple CPUs +- can distribute tests to remote ssh or socket connected machines +- can run tests in subprocess, re-run failing ones on file-change extensive plugin and customization system ------------------------------------------------------ @@ -22,14 +45,6 @@ extensive plugin and customization syste - it is `easy`_ to add command line options or do other kind of add-ons and customizations. -mature command line testing tool --------------------------------------- - -- powerful :ref:`usage` possibilities, well sorted command line options -- used in many projects, ranging from 10 to 10K tests -- tested on Unix and Windows from Python 2.4 up to Python 3.1 and 3.2 -- keyword/testname based selection of tests - integrates well with CI systems ---------------------------------------- @@ -39,24 +54,6 @@ integrates well with CI systems .. _`tox`: http://codespeak.net/tox -supports common testing practises and methods ------------------------------------------------------------ - -- supports extended `xUnit style setup`_ -- can integrate ``nose``, ``unittest.py`` and ``doctest.py`` style tests -- supports generating testing coverage reports -- supports :ref:`non-python tests` -- `Javasript unit- and functional testing`_ - -.. _`Javasript unit- and functional testing`: http://pypi.python.org/pypi/oejskit - -distributing tests to local/remote subprocesses --------------------------------------------------------- - -- distribute tests to multiple CPUs -- distribute tests to remote ssh or socket connected machines -- run tests in subprocess, re-run failing ones on file-change - skip or expect-to-fail a test ------------------------------------------- --- a/doc/announce/release-2.0.0.txt +++ b/doc/announce/release-2.0.0.txt @@ -1,25 +1,25 @@ -py.test 2.0.0: improved asserts, unittest, reporting, config, docs +py.test 2.0.0: asserts++, unittest++, reporting++, config++, docs++ =========================================================================== Welcome to pytest-2.0.0, a major new release of "py.test", the rapid easy Python testing tool. There are many new features and enhancements, -see below. Also a lot of long-deprecated code has been removed, -resulting in a much smaller and cleaner implementation. Note that -pytest used to part of the "py" distribution up until version py-1.3.4 -but this has changed now: pytest-2.0.0 only contains py.test related -code and is expected to be backward-compatible to existing test code. If -you want to install it, type one of:: +see below for summary and detailed lists. A lot of long-deprecated code +has been removed, resulting in a much smaller and cleaner +implementation. See the new docs with examples here: + + http://pytest.org/2.0.0/index.html + +A note on packaging: pytest used to part of the "py" distribution up +until version py-1.3.4 but this has changed now: pytest-2.0.0 only +contains py.test related code and is expected to be backward-compatible +to existing test code. If you want to install pytest, just type one of:: pip install -U pytest easy_install -U pytest -and check out the much improved documentation and examples: - - http://pytest.org/2.0.0/index.html - -Thanks to all issue reporters and people asking questions or +Many thanks to all issue reporters and people asking questions or complaining. Particular thanks to Floris Bruynooghe and Ronny Pfannschmidt -for their great coding contributions and may others for feedback and help. +for their great coding contributions and many others for feedback and help. best, holger krekel @@ -53,16 +53,16 @@ New Features see http://pytest.org/2.0.0/customize.html - improved standard unittest support. In general py.test should now - better run custom TestCases like twisted trial or Django based - TestCases. Also you can now run the tests of an installed - 'unittest' package with py.test:: + better be able to run custom unittest.TestCases like twisted trial + or Django based TestCases. Also you can now run the tests of an + installed 'unittest' package with py.test:: py.test --pyargs unittest - new "-q" option which decreases verbosity and prints a more nose/unittest-style "dot" output. -- many more improvements in details +- many many more detailed improvements details Fixes ----------------------- @@ -79,7 +79,7 @@ Fixes - fix bug: unittest collected functions now also can have "pytestmark" applied at class/module level -Notes +Important Notes -------------------- * The usual way in pre-2.0 times to use py.test in python code was From commits-noreply at bitbucket.org Wed Nov 24 22:26:16 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 15:26:16 -0600 (CST) Subject: [py-svn] pytest commit ae80b0f9d8ed: introduce new discovery mechanism Message-ID: <20101124212616.3CB32241291@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290632464 -3600 # Node ID ae80b0f9d8ed34cc84e96d4175beb89e81541120 # Parent 189bbc321107f9186042a3674efd34cda62f36b9 introduce new discovery mechanism XXX experiment with using it before introducing it or wait for feature request --- a/tox.ini +++ b/tox.ini @@ -73,3 +73,6 @@ minversion=2.0 plugins=pytester addopts= -rxf --pyargs --doctest-modules rsyncdirs=tox.ini pytest.py _pytest testing +python_files=test_*.py *_test.py +python_classes=Test Acceptance +python_functions=test --- a/_pytest/python.py +++ b/_pytest/python.py @@ -13,6 +13,13 @@ def pytest_addoption(parser): group.addoption('--funcargs', action="store_true", dest="showfuncargs", default=False, help="show available function arguments, sorted by plugin") + parser.addini("python_files", type="args", + default=('test_*.py', '*_test.py'), + help="glob-style file patterns for Python test module discovery") + parser.addini("python_classes", type="args", default=("Test",), + help="prefixes for Python test class discovery") + parser.addini("python_functions", type="args", default=("test",), + help="prefixes for Python test function and method discovery") def pytest_cmdline_main(config): if config.option.showfuncargs: @@ -46,8 +53,13 @@ def pytest_pyfunc_call(__multicall__, py def pytest_collect_file(path, parent): ext = path.ext pb = path.purebasename - if ext == ".py" and (pb.startswith("test_") or pb.endswith("_test") or - parent.session.isinitpath(path)): + if ext == ".py": + if not parent.session.isinitpath(path): + for pat in parent.config.getini('python_files'): + if path.fnmatch(pat): + break + else: + return return parent.ihook.pytest_pycollect_makemodule( path=path, parent=parent) @@ -145,9 +157,14 @@ class PyobjMixin(object): class PyCollectorMixin(PyobjMixin, pytest.Collector): def funcnamefilter(self, name): - return name.startswith('test') + for prefix in self.config.getini("python_functions"): + if name.startswith(prefix): + return True + def classnamefilter(self, name): - return name.startswith('Test') + for prefix in self.config.getini("python_classes"): + if name.startswith(prefix): + return True def collect(self): # NB. we avoid random getattrs and peek in the __dict__ instead --- a/doc/example/pythoncollection.txt +++ b/doc/example/pythoncollection.txt @@ -12,19 +12,63 @@ You can set the :confval:`norecursedirs` This would tell py.test to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. -always try to interpret arguments as Python packages +.. _`change naming conventions`: + +change naming conventions +----------------------------------------------------- + +You can configure different naming conventions by setting +the :confval:`pytest_pycollect` configuration option. Example:: + + # content of setup.cfg + # can also be defined in in tox.ini or pytest.ini file + [pytest] + python_files=check_*.py + python_classes=Check + python_functions=check + +This would make py.test look for ``check_`` prefixes in +Python filenames, ``Check`` prefixes in classes and ``check`` prefixes +in functions and classes. For example, if we have:: + + # content of check_myapp.py + class CheckMyApp: + def check_simple(self): + pass + def check_complex(self): + pass + +then the test collection looks like this:: + + $ py.test --collectonly + + + + + + +interpret cmdline arguments as Python packages ----------------------------------------------------- You can use the ``--pyargs`` option to make py.test try interpreting arguments as python package names, deriving -their file system path and then running the test. Through -an ini-file and the :confval:`addopts` option you can make -this change more permanently:: +their file system path and then running the test. For +example if you have unittest2 installed you can type:: + + py.test --pyargs unittest2.test.test_skipping -q + +which will run the respective test module. Like with +other options, through an ini-file and the :confval:`addopts` option you +can make this change more permanently:: # content of pytest.ini [pytest] addopts = --pyargs +Now a simple invocation of ``py.test NAME`` will check +if NAME exists as an importable package/module and otherwise +treat it as a filesystem path. + finding out what is collected ----------------------------------------------- --- a/doc/customize.txt +++ b/doc/customize.txt @@ -86,3 +86,18 @@ builtin configuration file options This would tell py.test to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. +.. confval:: python_discovery + + Determines names and patterns for finding Python tests, specified + through indendent ``name = value`` settings with these possible names:: + + [pytest] + python_discovery = + file = + func = + class = + + See :ref:`change naming conventions` for examples. the ``class`` + to be empty in which case all non-underscore empty classes + will be considered. + --- a/_pytest/config.py +++ b/_pytest/config.py @@ -258,6 +258,7 @@ class Config(object): self.trace = self.pluginmanager.trace.root.get("config") self._conftest = Conftest(onimport=self._onimportconftest) self.hook = self.pluginmanager.hook + self._inicache = {} def _onimportconftest(self, conftestmodule): self.trace("loaded conftestmodule %r" %(conftestmodule,)) @@ -333,6 +334,13 @@ class Config(object): specified name hasn't been registered through a prior ``parse.addini`` call (usually from a plugin), a ValueError is raised. """ try: + return self._inicache[name] + except KeyError: + self._inicache[name] = val = self._getini(name) + return val + + def _getini(self, name): + try: description, type, default = self._parser._inidict[name] except KeyError: raise ValueError("unknown configuration value: %r" %(name,)) --- a/testing/test_python.py +++ b/testing/test_python.py @@ -1208,3 +1208,32 @@ class TestRaises: +def test_customized_python_discovery(testdir): + testdir.makeini(""" + [pytest] + python_files=check_*.py + python_classes=Check + python_functions=check + """) + p = testdir.makepyfile(""" + def check_simple(): + pass + class CheckMyApp: + def check_meth(self): + pass + """) + p2 = p.new(basename=p.basename.replace("test", "check")) + p.move(p2) + result = testdir.runpytest("--collectonly", "-s") + result.stdout.fnmatch_lines([ + "*check_customized*", + "*check_simple*", + "*CheckMyApp*", + "*check_meth*", + ]) + + result = testdir.runpytest() + assert result.ret == 0 + result.stdout.fnmatch_lines([ + "*2 passed*", + ]) From commits-noreply at bitbucket.org Thu Nov 25 02:18:14 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 19:18:14 -0600 (CST) Subject: [py-svn] pytest commit b8b7749ce018: express filter as a listcomp Message-ID: <20101125011814.165691E126B@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1290647922 21600 # Node ID b8b7749ce0183b539a50a174575562307b86ca88 # Parent cfcf15d0d1e4aa25a9d6d45f59f5b6e92a19ed1e express filter as a listcomp --- a/_pytest/python.py +++ b/_pytest/python.py @@ -564,7 +564,7 @@ class FuncargRequest: self._pyfuncitem = pyfuncitem if hasattr(pyfuncitem, '_requestparam'): self.param = pyfuncitem._requestparam - extra = filter(None, [self.module, self.instance]) + extra = [obj for obj in (self.module, self.instance) if obj] self._plugins = pyfuncitem.getplugins() + extra self._funcargs = self._pyfuncitem.funcargs.copy() self._name2factory = {} From commits-noreply at bitbucket.org Thu Nov 25 04:26:47 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 21:26:47 -0600 (CST) Subject: [py-svn] pytest commit bb8861d816ce: don't try to load conf.py Message-ID: <20101125032647.40918241210@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1290655630 21600 # Node ID bb8861d816ce6e5f5c9bcad5944b3c9a3920ccec # Parent b8b7749ce0183b539a50a174575562307b86ca88 don't try to load conf.py --- /dev/null +++ b/doc/conftest.py @@ -0,0 +1,1 @@ +collect_ignore = ["conf.py"] From commits-noreply at bitbucket.org Thu Nov 25 04:35:12 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Nov 2010 21:35:12 -0600 (CST) Subject: [py-svn] pytest commit f8c5a510fd00: remove invalid comment Message-ID: <20101125033512.E1F9D1E126B@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1290656136 21600 # Node ID f8c5a510fd00047fa81b5c4d624c0e6e25ebfd81 # Parent bb8861d816ce6e5f5c9bcad5944b3c9a3920ccec remove invalid comment --- a/_pytest/core.py +++ b/_pytest/core.py @@ -1,6 +1,5 @@ """ pytest PluginManager, basic initialization and tracing. -All else is in pytest/plugin. (c) Holger Krekel 2004-2010 """ import sys, os From commits-noreply at bitbucket.org Thu Nov 25 12:44:37 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 05:44:37 -0600 (CST) Subject: [py-svn] pytest commit 4853ee1ad7d5: tons and tons of refinements and additions to docs Message-ID: <20101125114437.0B8C12410BD@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290683470 -3600 # Node ID 4853ee1ad7d54469c45e9158b05d8320df0669b0 # Parent f8c5a510fd00047fa81b5c4d624c0e6e25ebfd81 tons and tons of refinements and additions to docs --- /dev/null +++ b/doc/projects.txt @@ -0,0 +1,40 @@ +.. _projects: + +Project Examples +========================== + +Here are some examples of projects using py.test: + +* `PyPy `_, Python with a JIT compiler, running over `16000 tests `_ +* the `MoinMoin `_ Wiki Engine +* `tox `_, virtualenv/Hudson integration tool +* `PIDA `_ framework for integrated development +* `Fom `_ a fluid object mapper for FluidDB +* `six `_ Python 2 and 3 compatibility utilities +* `The Translate Toolkit `_ for localization and conversion +* `execnet `_ rapid multi-Python deployment +* `py-s3fuse `_ Amazon S3 FUSE based filesystem +* `Circuits `_ lightweight Event Driven Framework +* `pygtk-helpers `_ easy interaction with PyGTK +* `QuantumCore `_ statusmessage and repoze openid plugin +* `pydataportability `_ libraries for managing the open web +* `XIST `_ extensible HTML/XML generator +* `tiddlyweb `_ optionally headless, extensible RESTful datastore +* `Paludis `_ tools for Gentoo Paludis package manager +* `Gerald `_ schema comparison tool +* `abjad `_ Python API for Formalized Score control +* `bu `_ a microscopic build system +* `katcp `_ Telescope communication protocol over Twisted +* `kss plugin timer `_ +* many more ... (please send notes via the :ref:`contact`) + +Some examples of organisations using py.test +---------------------------------------------- + +* `Square Kilometre Array `_ +* `Tandberg `_ +* `Stups department of Heinrich Heine University `_ +* `Open End `_ +* `Laboraratory of Bioinformatics `_ +* `merlinux `_ +* many more ... (please send a note via the :ref:`contact`) --- a/doc/index.txt +++ b/doc/index.txt @@ -1,35 +1,59 @@ -py.test: no-boilerplate testing with Python -============================================== -.. todolist:: -.. note:: - version 2.0 introduces ``pytest`` as the main Python import name - in examples but you can continue to use ``import py`` and - ``py.test.XYZ`` to access :ref:`pytest helpers` in your test code. +Welcome to ``py.test``! +============================================= -Welcome to ``py.test`` documentation: -.. toctree:: - :maxdepth: 2 +- **a mature fully featured testing tool** - overview - apiref - plugins - example/index - talks - develop - announce/index + - runs on Posix/Windows, Python 2.4-3.2, PyPy and Jython + - continously `tested on many Python interpreters `_ + - used in :ref:`many projects `, ranging from 10 to 10000 tests + - has :ref:`comprehensive documentation ` + - comes with :ref:`tested examples ` + - supports :ref:`good integration practises ` + +- **provides no-boilerplate testing** + + - makes it :ref:`easy to get started `, refined :ref:`usage options ` + - :ref:`assert with the assert statement` + - helpful :ref:`traceback and failing assertion reporting ` + - allows `print debugging `_ and `generic output capturing `_ + - supports :pep:`8` compliant coding style in tests + +- **supports functional testing and complex test setups** + + - advanced :ref:`skip and xfail` + - generic :ref:`marking and test selection ` + - can :ref:`distribute tests to multiple CPUs ` through :ref:`xdist plugin ` + - can :ref:`continously re-run failing tests ` + - many :ref:`builtin helpers ` + - flexible :ref:`Python test discovery` + - unique :ref:`dependency injection through funcargs ` + - :ref:`parametrized test functions ` + +- **integrates many common testing methods** + + - can integrate ``nose``, ``unittest.py`` and ``doctest.py`` style + tests, including running testcases made for Django and trial + - supports extended :ref:`xUnit style setup ` + - supports domain-specific :ref:`non-python tests` + - supports generating testing coverage reports + - `Javasript unit- and functional testing`_ + +- **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. + +.. _`Javasript 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 .. toctree:: :hidden: - changelog.txt - example/attic + contact.txt + contents.txt -Indices and tables -================== - -* :ref:`genindex` -* :ref:`modindex` -* :ref:`search` --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -8,14 +8,25 @@ creating and managing test function argu .. _`funcargs`: .. _`funcarg mechanism`: -Test function arguments and factories +Dependency injection through function arguments ================================================= -A *funcarg* is the short name for "test function argument". Like -any other Python function a test function can receive one or multiple -arguments. When ``py.test`` prepares running a test function -it looks at the neccessary function arguments, locates and calls -factories which then provide the values to be passed to the function. +py.test allows to inject values into test functions through the *funcarg +mechanism*: For each argument name in a test function signature a factory is +looked up and called to create the value. The factory can live in the +same test class, test module, in a per-directory ``confest.py`` file or +in an external plugin. It has full access to the requesting test +function, can register finalizers and invoke lifecycle-caching +helpers. As can be expected from a systematic dependency +injection mechanism, this allows full de-coupling of resource and +fixture setup from test code, enabling more maintainable and +easy-to-modify test suites. + +A test function may be invoked multiple times in which case we +speak of :ref:`parametrized testing `. This can be +very useful if you want to test e.g. against different database backends +or with multiple numerical arguments sets and want to reuse the same set +of test functions. Basic funcarg example ----------------------- @@ -53,17 +64,19 @@ Running the test looks like this:: This means that the test function was called with a ``myfuncarg`` value of ``42`` and the assert fails accordingly. Here is how py.test -calls the test function: +comes to call the test function this way: -1. py.test discovers the ``test_function`` because of the ``test_`` prefix. - The test function needs a function argument named ``myfuncarg``. - A matching factory function is discovered by looking for the - name ``pytest_funcarg__myfuncarg``. +1. py.test :ref:`finds ` the ``test_function`` because + of the ``test_`` prefix. The test function needs a function argument + named ``myfuncarg``. A matching factory function is discovered by + looking for the name ``pytest_funcarg__myfuncarg``. 2. ``pytest_funcarg__myfuncarg(request)`` is called and returns the value for ``myfuncarg``. -3. ``test_function(42)`` call is executed. +3. the test function can now be called: ``test_function(42)`` + and results in the above exception because of the assertion + mismatch. Note that if you misspell a function argument or want to use one that isn't available, you'll see an error @@ -108,6 +121,7 @@ factory and provides access to test conf .. _`test generators`: .. _`parametrizing-tests`: +.. _`parametrized test functions`: Parametrizing multiple calls to a test function =========================================================== --- a/doc/_templates/layout.html +++ b/doc/_templates/layout.html @@ -12,9 +12,10 @@ {% block header %}
-

tox: virtualenv-based automation of test activities

+

pytest: rapid no-boilerplate testing with Python

home |  + all docs |  install |  examples |  customize |  --- a/doc/announce/release-1.0.0.txt +++ /dev/null @@ -1,63 +0,0 @@ - -pylib 1.0.0 released: testing-with-python innovations continue --------------------------------------------------------------------- - -Took a few betas but finally i uploaded a `1.0.0 py lib release`_, -featuring the mature and powerful py.test tool and "execnet-style" -*elastic* distributed programming. With the new release, there are -many new advanced automated testing features - here is a quick summary: - -* funcargs_ - pythonic zero-boilerplate fixtures for Python test functions : - - - totally separates test code, test configuration and test setup - - ideal for integration and functional tests - - allows for flexible and natural test parametrization schemes - -* new `plugin architecture`_, allowing easy-to-write project-specific and cross-project single-file plugins. The most notable new external plugin is `oejskit`_ which naturally enables **running and reporting of javascript-unittests in real-life browsers**. - -* many new features done in easy-to-improve `default plugins`_, highlights: - - * xfail: mark tests as "expected to fail" and report separately. - * pastebin: automatically send tracebacks to pocoo paste service - * capture: flexibly capture stdout/stderr of subprocesses, per-test ... - * monkeypatch: safely monkeypatch modules/classes from within tests - * unittest: run and integrate traditional unittest.py tests - * figleaf: generate html coverage reports with the figleaf module - * resultlog: generate buildbot-friendly reporting output - * ... - -* `distributed testing`_ and `elastic distributed execution`_: - - - new unified "TX" URL scheme for specifying remote processes - - new distribution modes "--dist=each" and "--dist=load" - - new sync/async ways to handle 1:N communication - - improved documentation - -The py lib continues to offer most of the functionality used by -the testing tool in `independent namespaces`_. - -Some non-test related code, notably greenlets/co-routines and -api-generation now live as their own projects which simplifies the -installation procedure because no C-Extensions are required anymore. - -The whole package should work well with Linux, Win32 and OSX, on Python -2.3, 2.4, 2.5 and 2.6. (Expect Python3 compatibility soon!) - -For more info, see the py.test and py lib documentation: - - http://pytest.org - - http://pylib.org - -have fun, -holger - -.. _`independent namespaces`: http://pylib.org -.. _`funcargs`: http://codespeak.net/py/dist/test/funcargs.html -.. _`plugin architecture`: http://codespeak.net/py/dist/test/extend.html -.. _`default plugins`: http://codespeak.net/py/dist/test/plugin/index.html -.. _`distributed testing`: http://codespeak.net/py/dist/test/dist.html -.. _`elastic distributed execution`: http://codespeak.net/py/dist/execnet.html -.. _`1.0.0 py lib release`: http://pypi.python.org/pypi/py -.. _`oejskit`: http://codespeak.net/py/dist/test/plugin/oejskit.html - --- a/doc/announce/release-1.0.2.txt +++ /dev/null @@ -1,5 +0,0 @@ -1.0.2: packaging fixes ------------------------------------------------------------------------ - -this release is purely a release for fixing packaging issues. - --- a/doc/features.txt +++ /dev/null @@ -1,64 +0,0 @@ -py.test Features -================== - -mature no-boilerplate testing with Python ------------------------------------------------- - -- automatic, customizable :ref:`Python test discovery` -- simple test functions and freely groupable tests -- simple ``assert`` statements for assertions -- powerful parametrization of test functions -- very helpful traceback and failing assertion reporting -- use ``print`` or ``pdb`` debugging on failures -- enables fully :pep:`8` compliant coding style -- powerful :ref:`usage` possibilities, well sorted command line options -- used in many projects, ranging from 10 to 10K tests -- tested on Unix and Windows from Python 2.4 up to Python 3.1 and 3.2 -- keyword/testname based selection of tests - -supports common testing practises and methods ------------------------------------------------------------ - -- can integrate ``nose``, ``unittest.py`` and ``doctest.py`` style - tests, including running testcases made for Django and trial -- supports extended `xUnit style setup`_ -- supports domain-specific :ref:`non-python tests` -- supports generating testing coverage reports -- `Javasript unit- and functional testing`_ - -.. _`Javasript unit- and functional testing`: http://pypi.python.org/pypi/oejskit - -distributing tests to local/remote subprocesses --------------------------------------------------------- - -- can distribute tests to multiple CPUs -- can distribute tests to remote ssh or socket connected machines -- can run tests in subprocess, re-run failing ones on file-change - -extensive plugin and customization system ------------------------------------------------------- - -.. _`easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html - -- all collection, reporting, running aspects are delegated to hook functions -- hook functions are implemented per-directory, per-project or through PyPI released plugins -- it is `easy`_ to add command line options or - do other kind of add-ons and customizations. - -integrates well with CI systems ----------------------------------------- - -- produces compatible JunitXML output for Hudson or other CI servers -- produces "resultlog" text files for easy parsing -- integrates well with tox_ - -.. _`tox`: http://codespeak.net/tox - -skip or expect-to-fail a test -------------------------------------------- - -- skip tests if there are platform or dependency mismatches -- xfail a test (on certain platforms)indicating an implementation problem -- you can use skip and xfail imperatively or as a decorating marker - -.. include:: links.inc --- a/doc/develop.txt +++ b/doc/develop.txt @@ -15,15 +15,12 @@ Working from version control or a tarbal To follow development or start experiments, checkout the complete code and documentation source with mercurial_:: - hg clone https://bitbucket.org/hpk42/py-trunk/ - -Development takes place on the 'trunk' branch. + hg clone https://bitbucket.org/hpk42/pytest/ You can also go to the python package index and download and unpack a TAR file:: - http://pypi.python.org/pypi/py/ - + http://pypi.python.org/pypi/pytest/ activating a checkout with setuptools -------------------------------------------- --- a/doc/announce/release-1.1.0.txt +++ /dev/null @@ -1,115 +0,0 @@ -py.test/pylib 1.1.0: Python3, Jython, advanced skipping, cleanups ... --------------------------------------------------------------------------------- - -Features: - -* compatible to Python3 (single py2/py3 source), `easy to install`_ -* conditional skipping_: skip/xfail based on platform/dependencies -* generalized marking_: mark tests one a whole-class or whole-module basis - -Fixes: - -* code reduction and "de-magification" (e.g. 23 KLoc -> 11 KLOC) -* distribute testing requires the now separately released execnet_ package -* funcarg-setup/caching, "same-name" test modules now cause an exlicit error -* de-cluttered reporting options, --report for skipped/xfail details - -Compatibilities - -1.1.0 should allow running test code that already worked well with 1.0.2 -plus some more due to improved unittest/nose compatibility. - -More information: http://pytest.org - -thanks and have fun, - -holger (http://twitter.com/hpk42) - -.. _execnet: http://codespeak.net/execnet -.. _`easy to install`: ../install.html -.. _marking: ../test/plugin/mark.html -.. _skipping: ../test/plugin/skipping.html - - -Changelog 1.0.2 -> 1.1.0 ------------------------------------------------------------------------ - -* remove py.rest tool and internal namespace - it was - never really advertised and can still be used with - the old release if needed. If there is interest - it could be revived into its own tool i guess. - -* fix issue48 and issue59: raise an Error if the module - from an imported test file does not seem to come from - the filepath - avoids "same-name" confusion that has - been reported repeatedly - -* merged Ronny's nose-compatibility hacks: now - nose-style setup_module() and setup() functions are - supported - -* introduce generalized py.test.mark function marking - -* reshuffle / refine command line grouping - -* deprecate parser.addgroup in favour of getgroup which creates option group - -* add --report command line option that allows to control showing of skipped/xfailed sections - -* generalized skipping: a new way to mark python functions with skipif or xfail - at function, class and modules level based on platform or sys-module attributes. - -* extend py.test.mark decorator to allow for positional args - -* introduce and test "py.cleanup -d" to remove empty directories - -* fix issue #59 - robustify unittest test collection - -* make bpython/help interaction work by adding an __all__ attribute - to ApiModule, cleanup initpkg - -* use MIT license for pylib, add some contributors - -* remove py.execnet code and substitute all usages with 'execnet' proper - -* fix issue50 - cached_setup now caches more to expectations - for test functions with multiple arguments. - -* merge Jarko's fixes, issue #45 and #46 - -* add the ability to specify a path for py.lookup to search in - -* fix a funcarg cached_setup bug probably only occuring - in distributed testing and "module" scope with teardown. - -* many fixes and changes for making the code base python3 compatible, - many thanks to Benjamin Peterson for helping with this. - -* consolidate builtins implementation to be compatible with >=2.3, - add helpers to ease keeping 2 and 3k compatible code - -* deprecate py.compat.doctest|subprocess|textwrap|optparse - -* deprecate py.magic.autopath, remove py/magic directory - -* move pytest assertion handling to py/code and a pytest_assertion - plugin, add "--no-assert" option, deprecate py.magic namespaces - in favour of (less) py.code ones. - -* consolidate and cleanup py/code classes and files - -* cleanup py/misc, move tests to bin-for-dist - -* introduce delattr/delitem/delenv methods to py.test's monkeypatch funcarg - -* consolidate py.log implementation, remove old approach. - -* introduce py.io.TextIO and py.io.BytesIO for distinguishing between - text/unicode and byte-streams (uses underlying standard lib io.* - if available) - -* make py.unittest_convert helper script available which converts "unittest.py" - style files into the simpler assert/direct-test-classes py.test/nosetests - style. The script was written by Laura Creighton. - -* simplified internal localpath implementation --- a/doc/announce/release-1.2.1.txt +++ /dev/null @@ -1,66 +0,0 @@ -py.test/pylib 1.2.1: little fixes and improvements --------------------------------------------------------------------------------- - -py.test is an advanced automated testing tool working with -Python2, Python3 and Jython versions on all major operating -systems. It has a simple plugin architecture and can run many -existing common Python test suites without modification. It offers -some unique features not found in other testing tools. -See http://pytest.org for more info. - -py.test 1.2.1 brings bug fixes and some new options and abilities triggered -by user feedback: - -* --funcargs [testpath] will show available builtin- and project funcargs. -* display a short and concise traceback if funcarg lookup fails. -* early-load "conftest.py" files in non-dot first-level sub directories. -* --tb=line will print a single line for each failing test (issue67) -* py.cleanup has a number of new options, cleanups up setup.py related files -* fix issue78: always call python-level teardown functions even if the - according setup failed. - -For more detailed information see the changelog below. - -cheers and have fun, - -holger - - -Changes between 1.2.1 and 1.2.0 -===================================== - -- refined usage and options for "py.cleanup":: - - py.cleanup # remove "*.pyc" and "*$py.class" (jython) files - py.cleanup -e .swp -e .cache # also remove files with these extensions - py.cleanup -s # remove "build" and "dist" directory next to setup.py files - py.cleanup -d # also remove empty directories - py.cleanup -a # synonym for "-s -d -e 'pip-log.txt'" - py.cleanup -n # dry run, only show what would be removed - -- add a new option "py.test --funcargs" which shows available funcargs - and their help strings (docstrings on their respective factory function) - for a given test path - -- display a short and concise traceback if a funcarg lookup fails - -- early-load "conftest.py" files in non-dot first-level sub directories. - allows to conveniently keep and access test-related options in a ``test`` - subdir and still add command line options. - -- fix issue67: new super-short traceback-printing option: "--tb=line" will print a single line for each failing (python) test indicating its filename, lineno and the failure value - -- fix issue78: always call python-level teardown functions even if the - according setup failed. This includes refinements for calling setup_module/class functions - which will now only be called once instead of the previous behaviour where they'd be called - multiple times if they raise an exception (including a Skipped exception). Any exception - will be re-corded and associated with all tests in the according module/class scope. - -- fix issue63: assume <40 columns to be a bogus terminal width, default to 80 - -- fix pdb debugging to be in the correct frame on raises-related errors - -- update apipkg.py to fix an issue where recursive imports might - unnecessarily break importing - -- fix plugin links --- a/doc/announce/release-0.9.0.txt +++ /dev/null @@ -1,7 +0,0 @@ -py lib 1.0.0: XXX -====================================================================== - -Welcome to the 1.0.0 py lib release - a library aiming to -support agile and test-driven python development on various levels. - -XXX --- a/doc/announce/release-2.0.0.txt +++ b/doc/announce/release-2.0.0.txt @@ -126,46 +126,4 @@ Important Notes if a certain dependency or command line option is missing. -More Detailed Changes between 1.3.4 and 2.0.0 -================================================== - -- pytest-2.0 is now its own package and depends on pylib-2.0 -- new ability: python -m pytest / python -m pytest.main ability -- new python invcation: pytest.main(args, plugins) to load - some custom plugins early. -- try harder to run unittest test suites in a more compatible manner - by deferring setup/teardown semantics to the unittest package. -- introduce a new way to set config options via ini-style files, - by default setup.cfg and tox.ini files are searched. The old - ways (certain environment variables, dynamic conftest.py reading - is removed). -- add a new "-q" option which decreases verbosity and prints a more - nose/unittest-style "dot" output. -- fix issue135 - marks now work with unittest test cases as well -- fix issue126 - introduce py.test.set_trace() to trace execution via - PDB during the running of tests even if capturing is ongoing. -- fix issue123 - new "python -m py.test" invocation for py.test - (requires Python 2.5 or above) -- fix issue124 - make reporting more resilient against tests opening - files on filedescriptor 1 (stdout). -- fix issue109 - sibling conftest.py files will not be loaded. - (and Directory collectors cannot be customized anymore from a Directory's - conftest.py - this needs to happen at least one level up). -- introduce (customizable) assertion failure representations and enhance - output on assertion failures for comparisons and other cases (Floris Bruynooghe) -- nose-plugin: pass through type-signature failures in setup/teardown - functions instead of not calling them (Ed Singleton) -- remove py.test.collect.Directory (follows from a major refactoring - and simplification of the collection process) -- majorly reduce py.test core code, shift function/python testing to own plugin -- fix issue88 (finding custom test nodes from command line arg) -- refine 'tmpdir' creation, will now create basenames better associated - with test names (thanks Ronny) -- "xpass" (unexpected pass) tests don't cause exitcode!=0 -- fix issue131 / issue60 - importing doctests in __init__ files used as namespace packages -- fix issue93 stdout/stderr is captured while importing conftest.py -- fix bug: unittest collected functions now also can have "pytestmark" - applied at class/module level -- add ability to use "class" level for cached_setup helper -- fix strangeness: mark.* objects are now immutable, create new instances - +see :ref:`changelog` for more detailed changes. --- a/doc/capture.txt +++ b/doc/capture.txt @@ -17,14 +17,14 @@ Setting capturing methods or disabling c There are two ways in which ``py.test`` can perform capturing: -* ``fd`` level capturing (default): All writes going to the operating - system file descriptors 1 and 2 will be captured, for example writes such - as ``os.write(1, 'hello')``. Capturing on ``fd``-level also includes +* ``fd`` level capturing (default): All writes going to the operating + system file descriptors 1 and 2 will be captured, for example writes such + as ``os.write(1, 'hello')``. Capturing on ``fd``-level also includes **output from subprocesses**. -* ``sys`` level capturing: The ``sys.stdout`` and ``sys.stderr`` will +* ``sys`` level capturing: The ``sys.stdout`` and ``sys.stderr`` will will be replaced with in-memory files and the ``print`` builtin or - output from code like ``sys.stderr.write(...)`` will be captured with + output from code like ``sys.stderr.write(...)`` will be captured with this method. .. _`disable capturing`: @@ -42,6 +42,8 @@ If you set capturing values in a conftes then all tests in that directory will execute with "fd" style capturing. +_ `printdebugging`: + Accessing captured output from a test function --------------------------------------------------- --- a/doc/announce/release-1.2.0.txt +++ /dev/null @@ -1,116 +0,0 @@ -py.test/pylib 1.2.0: junitxml, standalone test scripts, pluginization --------------------------------------------------------------------------------- - -py.test is an advanced automated testing tool working with -Python2, Python3 and Jython versions on all major operating -systems. It has a simple plugin architecture and can run many -existing common Python test suites without modification. It offers -some unique features not found in other testing tools. -See http://pytest.org for more info. - -py.test 1.2.0 brings many bug fixes and interesting new abilities: - -* --junitxml=path will create an XML file for use with CI processing -* --genscript=path creates a standalone py.test-equivalent test-script -* --ignore=path prevents collection of anything below that path -* --confcutdir=path only lookup conftest.py test configs below that path -* a 'pytest_report_header' hook to add info to the terminal report header -* a 'pytestconfig' function argument gives direct access to option values -* 'pytest_generate_tests' can now be put into a class as well -* on CPython py.test additionally installs as "py.test-VERSION", on - Jython as py.test-jython and on PyPy as py.test-pypy-XYZ - -Apart from many bug fixes 1.2.0 also has better pluginization: -Distributed testing and looponfailing testing now live in the -separately installable 'pytest-xdist' plugin. The same is true for -'pytest-figleaf' for doing coverage reporting. Those two plugins -can serve well now as blue prints for doing your own. - -thanks to all who helped and gave feedback, -have fun, - -holger krekel, January 2010 - -Changes between 1.2.0 and 1.1.1 -===================================== - -- moved dist/looponfailing from py.test core into a new - separately released pytest-xdist plugin. - -- new junitxml plugin: --junitxml=path will generate a junit style xml file - which is processable e.g. by the Hudson CI system. - -- new option: --genscript=path will generate a standalone py.test script - which will not need any libraries installed. thanks to Ralf Schmitt. - -- new option: --ignore will prevent specified path from collection. - Can be specified multiple times. - -- new option: --confcutdir=dir will make py.test only consider conftest - files that are relative to the specified dir. - -- new funcarg: "pytestconfig" is the pytest config object for access - to command line args and can now be easily used in a test. - -- install 'py.test' and `py.which` with a ``-$VERSION`` suffix to - disambiguate between Python3, python2.X, Jython and PyPy installed versions. - -- new "pytestconfig" funcarg allows access to test config object - -- new "pytest_report_header" hook can return additional lines - to be displayed at the header of a test run. - -- (experimental) allow "py.test path::name1::name2::..." for pointing - to a test within a test collection directly. This might eventually - evolve as a full substitute to "-k" specifications. - -- streamlined plugin loading: order is now as documented in - customize.html: setuptools, ENV, commandline, conftest. - also setuptools entry point names are turned to canonical namees ("pytest_*") - -- automatically skip tests that need 'capfd' but have no os.dup - -- allow pytest_generate_tests to be defined in classes as well - -- deprecate usage of 'disabled' attribute in favour of pytestmark -- deprecate definition of Directory, Module, Class and Function nodes - in conftest.py files. Use pytest collect hooks instead. - -- collection/item node specific runtest/collect hooks are only called exactly - on matching conftest.py files, i.e. ones which are exactly below - the filesystem path of an item - -- change: the first pytest_collect_directory hook to return something - will now prevent further hooks to be called. - -- change: figleaf plugin now requires --figleaf to run. Also - change its long command line options to be a bit shorter (see py.test -h). - -- change: pytest doctest plugin is now enabled by default and has a - new option --doctest-glob to set a pattern for file matches. - -- change: remove internal py._* helper vars, only keep py._pydir - -- robustify capturing to survive if custom pytest_runtest_setup - code failed and prevented the capturing setup code from running. - -- make py.test.* helpers provided by default plugins visible early - - works transparently both for pydoc and for interactive sessions - which will regularly see e.g. py.test.mark and py.test.importorskip. - -- simplify internal plugin manager machinery -- simplify internal collection tree by introducing a RootCollector node - -- fix assert reinterpreation that sees a call containing "keyword=..." - -- fix issue66: invoke pytest_sessionstart and pytest_sessionfinish - hooks on slaves during dist-testing, report module/session teardown - hooks correctly. - -- fix issue65: properly handle dist-testing if no - execnet/py lib installed remotely. - -- skip some install-tests if no execnet is available - -- fix docs, fix internal bin/ script generation - --- a/doc/goodpractises.txt +++ b/doc/goodpractises.txt @@ -1,6 +1,6 @@ .. highlightlang:: python -.. _`good practises`: +.. _`goodpractises`: Good Integration Practises ================================================= @@ -21,6 +21,78 @@ server Hudson_. .. _`buildout`: http://www.buildout.org/ .. _pip: http://pypi.python.org/pypi/pip +Use tox and Continous Integration servers +------------------------------------------------- + +If you are (often) releasing code to the public you +may want to look into `tox`_, the virtualenv test automation +tool and its `pytest support `_. +The basic idea is to generate a JUnitXML file through the ``--junitxml=PATH`` option and have a continous integration server like Hudson_ pick it up. + +.. _standalone: +.. _`genscript method`: + +Create a py.test standalone Script +------------------------------------------- + +If you are a maintainer or application developer and want others +to easily run tests you can generate a completely standalone "py.test" +script:: + + py.test --genscript=runtests.py + +generates a ``runtests.py`` script which is a fully functional basic +``py.test`` script, running unchanged under Python2 and Python3. +You can tell people to download the script and then e.g. run it like this:: + + python runtests.py + + +.. _`Distribute for installation`: http://pypi.python.org/pypi/distribute#installation-instructions +.. _`distribute installation`: http://pypi.python.org/pypi/distribute + + +Integrating with distutils / ``python setup.py test`` +-------------------------------------------------------- + +You can easily integrate test runs into your distutils or +setuptools based project. Use the `genscript method`_ +to generate a standalone py.test script:: + + py.test --genscript=runtests.py + +and make this script part of your distribution and then add +this to your ``setup.py`` file:: + + from distutils.core import setup, Command + # you can also import from setuptools + + class PyTest(Command): + user_options = [] + def initialize_options(self): + pass + def finalize_options(self): + pass + def run(self): + import sys,subprocess + errno = subprocess.call([sys.executable, 'runtest.py']) + raise SystemExit(errno) + setup( + #..., + cmdclass = {'test': PyTest}, + #..., + ) + +If you now type:: + + python setup.py test + +this will execute your tests using ``runtest.py``. As this is a +standalone version of ``py.test`` no prior installation whatsoever is +required for calling the test command. You can also pass additional +arguments to the subprocess-calls like your test directory or other +options. + .. _`test discovery`: .. _`Python test discovery`: @@ -93,70 +165,5 @@ You can always run your tests by pointin you must follow the convention of having directory and file names map to the import names. -.. _standalone: -.. _`genscript method`: - -Generating a py.test standalone Script -------------------------------------------- - -If you are a maintainer or application developer and want others -to easily run tests you can generate a completely standalone "py.test" -script:: - - py.test --genscript=runtests.py - -generates a ``runtests.py`` script which is a fully functional basic -``py.test`` script, running unchanged under Python2 and Python3. -You can tell people to download and then e.g. run it like this to -produce a Paste URL:: - - python runtests.py --pastebin=all - -and ask them to send you the resulting URL. - -.. _`Distribute for installation`: http://pypi.python.org/pypi/distribute#installation-instructions -.. _`distribute installation`: http://pypi.python.org/pypi/distribute - - -Integrating with distutils / ``python setup.py test`` --------------------------------------------------------- - -You can easily integrate test runs into your distutils or -setuptools based project. Use the `genscript method`_ -to generate a standalone py.test script:: - - py.test --genscript=runtests.py - -and make this script part of your distribution and then add -this to your ``setup.py`` file:: - - from distutils.core import setup, Command - # you can also import from setuptools - - class PyTest(Command): - user_options = [] - def initialize_options(self): - pass - def finalize_options(self): - pass - def run(self): - import sys,subprocess - errno = subprocess.call([sys.executable, 'runtest.py']) - raise SystemExit(errno) - setup( - #..., - cmdclass = {'test': PyTest}, - #..., - ) - -If you now type:: - - python setup.py test - -this will execute your tests using ``runtest.py``. As this is a -standalone version of ``py.test`` no prior installation whatsoever is -required for calling the test command. You can also pass additional -arguments to the subprocess-calls like your test directory or other -options. .. include:: links.inc --- a/pytest.py +++ b/pytest.py @@ -1,11 +1,7 @@ """ unit and functional testing with Python. - -see http://pytest.org for documentation and details - -(c) Holger Krekel and others, 2004-2010 """ -__version__ = '2.0.0.dev38' +__version__ = '2.0.0.dev39' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/doc/faq.txt +++ b/doc/faq.txt @@ -6,7 +6,7 @@ Some Issues and Questions If you don't find an answer here, checkout the :ref:`contact channels` to get help. -On naming, nosetests, licensing and magic XXX +On naming, nosetests, licensing and magic ------------------------------------------------ Why a ``py.test`` instead of a ``pytest`` command? @@ -21,14 +21,16 @@ but then again many people have gotten u is another tool named "pytest" so we just decided to stick with ``py.test``. -What's py.test's relation to ``nosetests``? +What's the relation to nose and unittest? +++++++++++++++++++++++++++++++++++++++++++++++++ py.test and nose_ share basic philosophy when it comes to running Python tests. In fact, you can run many tests -written for unittest or nose with py.test. nose_ was originally created +written nose with py.test. nose_ was originally created as a clone of ``py.test`` when py.test was in the ``0.8`` release -cycle. +cycle. As of version 2.0 support for running unittest test +suites is majorly improved and you should be able to run +many Django and Twisted test suites. .. _features: test/features.html --- a/_pytest/python.py +++ b/_pytest/python.py @@ -775,33 +775,34 @@ def getlocation(function, curdir): # builtin pytest.raises helper def raises(ExpectedException, *args, **kwargs): - """ assert that a code block/function call raises an exception. + """ assert that a code block/function call raises @ExpectedException + and raise a failure exception otherwise. + + If using Python 2.5 or above, you may use this function as a + context manager:: - If using Python 2.5 or above, you may use this function as a - context manager:: + >>> with raises(ZeroDivisionError): + ... 1/0 - >>> with raises(ZeroDivisionError): - ... 1/0 + Or you can specify a callable by passing a to-be-called lambda:: - Or you can specify a callable by passing a to-be-called lambda:: + >>> raises(ZeroDivisionError, lambda: 1/0) + - >>> raises(ZeroDivisionError, lambda: 1/0) - + or you can specify an arbitrary callable with arguments:: - or you can specify an arbitrary callable with arguments:: + >>> def f(x): return 1/x + ... + >>> raises(ZeroDivisionError, f, 0) + + >>> raises(ZeroDivisionError, f, x=0) + - >>> def f(x): return 1/x - ... - >>> raises(ZeroDivisionError, f, 0) - - >>> raises(ZeroDivisionError, f, x=0) - - - A third possibility is to use a string which which will - be executed:: - - >>> raises(ZeroDivisionError, "f(0)") - + A third possibility is to use a string which which will + be executed:: + + >>> raises(ZeroDivisionError, "f(0)") + """ __tracebackhide__ = True --- a/doc/example/pythoncollection.txt +++ b/doc/example/pythoncollection.txt @@ -18,7 +18,8 @@ change naming conventions ----------------------------------------------------- You can configure different naming conventions by setting -the :confval:`pytest_pycollect` configuration option. Example:: +the :confval:`python_files`, :confval:`python_classes` and +:confval:`python_functions` configuration options. Example:: # content of setup.cfg # can also be defined in in tox.ini or pytest.ini file @@ -57,7 +58,7 @@ example if you have unittest2 installed py.test --pyargs unittest2.test.test_skipping -q -which will run the respective test module. Like with +which would run the respective test module. Like with other options, through an ini-file and the :confval:`addopts` option you can make this change more permanently:: --- a/doc/announce/release-1.0.1.txt +++ /dev/null @@ -1,48 +0,0 @@ -1.0.1: improved reporting, nose/unittest.py support, bug fixes ------------------------------------------------------------------------ - -This is a bugfix release of pylib/py.test also coming with: - -* improved documentation, improved navigation -* test failure reporting improvements -* support for directly running existing nose/unittest.py style tests - -visit here for more info, including quickstart and tutorials: - - http://pytest.org and http://pylib.org - - -Changelog 1.0.0 to 1.0.1 ------------------------- - -* added a default 'pytest_nose' plugin which handles nose.SkipTest, - nose-style function/method/generator setup/teardown and - tries to report functions correctly. - -* improved documentation, better navigation: see http://pytest.org - -* added a "--help-config" option to show conftest.py / ENV-var names for - all longopt cmdline options, and some special conftest.py variables. - renamed 'conf_capture' conftest setting to 'option_capture' accordingly. - -* unicode fixes: capturing and unicode writes to sys.stdout - (through e.g a print statement) now work within tests, - they are encoded as "utf8" by default, also terminalwriting - was adapted and somewhat unified between windows and linux - -* fix issue #27: better reporting on non-collectable items given on commandline - (e.g. pyc files) - -* fix issue #33: added --version flag (thanks Benjamin Peterson) - -* fix issue #32: adding support for "incomplete" paths to wcpath.status() - -* "Test" prefixed classes are *not* collected by default anymore if they - have an __init__ method - -* monkeypatch setenv() now accepts a "prepend" parameter - -* improved reporting of collection error tracebacks - -* simplified multicall mechanism and plugin architecture, - renamed some internal methods and argnames --- a/doc/announce/release-1.3.4.txt +++ /dev/null @@ -1,22 +0,0 @@ -py.test/pylib 1.3.4: fixes and new native traceback option -=========================================================================== - -pylib/py.test 1.3.4 is a minor maintenance release mostly containing bug fixes -and a new "--tb=native" traceback option to show "normal" Python standard -tracebacks instead of the py.test enhanced tracebacks. See below for more -change info and http://pytest.org for more general information on features -and configuration of the testing tool. - -Thanks to the issue reporters and generally to Ronny Pfannschmidt for help. - -cheers, -holger krekel - -Changes between 1.3.3 and 1.3.4 -================================================== - -- fix issue111: improve install documentation for windows -- fix issue119: fix custom collectability of __init__.py as a module -- fix issue116: --doctestmodules work with __init__.py files as well -- fix issue115: unify internal exception passthrough/catching/GeneratorExit -- fix issue118: new --tb=native for presenting cpython-standard exceptions --- a/doc/contact.txt +++ b/doc/contact.txt @@ -15,7 +15,8 @@ Contact channels - #pylib on irc.freenode.net IRC channel for random questions. -- `tetamap`_: Holger Krekel's blog, often about testing and py.test related news. +- private mail to Holger.Krekel at gmail com if you have sensitive issues to communicate + - `commit mailing list`_ - `merlinux.eu`_ offers on-site teaching and consulting services. --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev38', + version='2.0.0.dev39', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/doc/_templates/indexsidebar.html +++ b/doc/_templates/indexsidebar.html @@ -12,10 +12,11 @@

pytest on PyPI

+
easy_install pytest
pip install pytest
{% endif %}

Questions? Suggestions?

-

Checkout support channels +

contact channels

--- /dev/null +++ b/doc/naming20.txt @@ -0,0 +1,20 @@ + +.. _naming20: + +New pytest names in 2.0 (flat is better than nested) +---------------------------------------------------- + +If you used older version of the ``py`` distribution (which +included the py.test command line tool and Python name space) +you accessed helpers and possibly collection classes through +the ``py.test`` Python namespaces. The new ``pytest`` +Python module flaty provides the same objects, following +these renaming rules:: + + py.test.XYZ -> pytest.XYZ + py.test.collect.XYZ -> pytest.XYZ + py.test.cmdline.main -> pytest.main + +The old ``py.test.*`` ways to access functionality remain +valid but you are encouraged to do global renames according +to the above rules in your test code. --- a/doc/example/simple.txt +++ b/doc/example/simple.txt @@ -1,26 +1,9 @@ .. highlightlang:: python -simple hook using patterns +basic patterns and examples ========================================================== -adding custom options ----------------------- - -py.test supports adding of standard optparse_ Options. -A plugin may implement the ``addoption`` hook for registering -custom options:: - - def pytest_addoption(parser): - parser.addoption("-M", "--myopt", action="store", - help="specify string to set myopt") - - def pytest_configure(config): - if config.option.myopt: - # do action based on option value - -.. _optparse: http://docs.python.org/library/optparse.html - pass different values to a test function, depending on command line options ---------------------------------------------------------------------------- @@ -37,7 +20,7 @@ Here is a basic pattern how to achieve t For this to work we need to add a command line option and -provide the ``cmdopt`` through a function argument factory:: +provide the ``cmdopt`` through a :ref:`function argument ` factory:: # content of conftest.py def pytest_addoption(parser): --- a/CHANGELOG +++ b/CHANGELOG @@ -7,6 +7,8 @@ Changes between 1.3.4 and 2.0.0dev0 some custom plugins early. - try harder to run unittest test suites in a more compatible manner by deferring setup/teardown semantics to the unittest package. + also work harder to run twisted/trial and Django tests which + should now basically work by default. - introduce a new way to set config options via ini-style files, by default setup.cfg and tox.ini files are searched. The old ways (certain environment variables, dynamic conftest.py reading --- a/doc/overview.txt +++ b/doc/overview.txt @@ -1,13 +1,13 @@ ================================================== -Overview and Introduction +Getting started basics ================================================== .. toctree:: :maxdepth: 2 - features.txt getting-started.txt usage.txt goodpractises.txt + projects.txt faq.txt --- a/doc/announce/release-1.3.3.txt +++ /dev/null @@ -1,26 +0,0 @@ -py.test/pylib 1.3.3: windows and other fixes -=========================================================================== - -pylib/py.test 1.3.3 is a minor bugfix release featuring some improvements -and fixes. See changelog_ for full history. - -have fun, -holger krekel - -.. _changelog: ../changelog.html - -Changes between 1.3.2 and 1.3.3 -================================================== - -- fix issue113: assertion representation problem with triple-quoted strings - (and possibly other cases) -- make conftest loading detect that a conftest file with the same - content was already loaded, avoids surprises in nested directory structures - which can be produced e.g. by Hudson. It probably removes the need to use - --confcutdir in most cases. -- fix terminal coloring for win32 - (thanks Michael Foord for reporting) -- fix weirdness: make terminal width detection work on stdout instead of stdin - (thanks Armin Ronacher for reporting) -- remove trailing whitespace in all py/text distribution files - --- a/doc/customize.txt +++ b/doc/customize.txt @@ -11,6 +11,7 @@ You can get help on options and ini-conf This will display command line and configuration file settings which were registered by installed plugins. + how test configuration is read from setup/tox ini-files -------------------------------------------------------- @@ -40,6 +41,26 @@ will look in the following dirs for a co If argument is provided to a py.test run, the current working directory is used to start the search. +.. _`how to change command line options defaults`: +.. _`adding default options`: + +how to change command line options defaults +------------------------------------------------ + +py.test provides a simple way to set some default +command line options. For example, if you want +to always see detailed info on skipped and xfailed +tests, as well as have terser "dot progress output", +you can add this to your root directory:: + + # content of pytest.ini + # (or tox.ini or setup.cfg) + [pytest] + addopts = -rsxX -q + +From now on, running ``py.test`` will implicitely add +the specified options. + builtin configuration file options ---------------------------------------------- @@ -86,18 +107,20 @@ builtin configuration file options This would tell py.test to not recurse into typical subversion or sphinx-build directories or into any ``tmp`` prefixed directory. -.. confval:: python_discovery +.. confval:: python_files - Determines names and patterns for finding Python tests, specified - through indendent ``name = value`` settings with these possible names:: + One or more Glob-style file patterns determining which python files + are considered as test modules. - [pytest] - python_discovery = - file = - func = - class = +.. confval:: python_classes - See :ref:`change naming conventions` for examples. the ``class`` - to be empty in which case all non-underscore empty classes - will be considered. + One or more name prefixes determining which test classes + are considered as test modules. +.. confval:: python_functions + + One or more name prefixes determining which test functions + and methods are considered as test modules. + + See :ref:`change naming conventions` for examples. + --- a/doc/apiref.txt +++ b/doc/apiref.txt @@ -4,7 +4,6 @@ py.test reference documentation ================================================ - .. toctree:: :maxdepth: 2 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,6 +6,7 @@ include LICENSE #include conftest.py graft doc graft testing +exclude doc/_build include testing #exclude *.orig #exclude *.orig --- /dev/null +++ b/doc/example/reportingdemo.txt @@ -0,0 +1,486 @@ + +.. _`tbreportdemo`: + +Demo of Python failure reports with py.test +================================================== + +Here is a nice run of several tens of failures +and how py.test presents things (unfortunately +not showing the nice colors here in the HTML that you +get on the terminal - we are working on that):: + + assertion $ py.test failure_demo.py + =========================== test session starts ============================ + platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev38 + collecting ... + collected 35 items + + failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF + + ================================= FAILURES ================================= + ____________________________ test_generative[0] ____________________________ + + param1 = 3, param2 = 6 + + def test_generative(param1, param2): + > assert param1 * 2 < param2 + E assert (3 * 2) < 6 + + failure_demo.py:15: AssertionError + _________________________ TestFailing.test_simple __________________________ + + self = + + def test_simple(self): + def f(): + return 42 + def g(): + return 43 + + > assert f() == g() + E assert 42 == 43 + E + where 42 = () + E + and 43 = () + + failure_demo.py:28: AssertionError + ____________________ TestFailing.test_simple_multiline _____________________ + + self = + + def test_simple_multiline(self): + otherfunc_multi( + 42, + > 6*9) + + failure_demo.py:33: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + a = 42, b = 54 + + def otherfunc_multi(a,b): + assert (a == + > b) + E assert 42 == 54 + + failure_demo.py:12: AssertionError + ___________________________ TestFailing.test_not ___________________________ + + self = + + def test_not(self): + def f(): + return 42 + > assert not f() + E assert not 42 + E + where 42 = () + + failure_demo.py:38: AssertionError + _________________ TestSpecialisedExplanations.test_eq_text _________________ + + self = + + def test_eq_text(self): + > assert 'spam' == 'eggs' + E assert 'spam' == 'eggs' + E - spam + E + eggs + + failure_demo.py:42: AssertionError + _____________ TestSpecialisedExplanations.test_eq_similar_text _____________ + + self = + + def test_eq_similar_text(self): + > assert 'foo 1 bar' == 'foo 2 bar' + E assert 'foo 1 bar' == 'foo 2 bar' + E - foo 1 bar + E ? ^ + E + foo 2 bar + E ? ^ + + failure_demo.py:45: AssertionError + ____________ TestSpecialisedExplanations.test_eq_multiline_text ____________ + + self = + + def test_eq_multiline_text(self): + > assert 'foo\nspam\nbar' == 'foo\neggs\nbar' + E assert 'foo\nspam\nbar' == 'foo\neggs\nbar' + E foo + E - spam + E + eggs + E bar + + failure_demo.py:48: AssertionError + ______________ TestSpecialisedExplanations.test_eq_long_text _______________ + + 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' == '111111111111...2222222222222' + E Skipping 90 identical leading characters in diff + E Skipping 91 identical trailing characters in diff + E - 1111111111a222222222 + E ? ^ + E + 1111111111b222222222 + E ? ^ + + failure_demo.py:53: AssertionError + _________ TestSpecialisedExplanations.test_eq_long_text_multiline __________ + + 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\n...n2\n2\n2\n2\n' + E Skipping 190 identical leading characters in diff + E Skipping 191 identical trailing characters in diff + E 1 + E 1 + E 1 + E 1 + E 1 + E - a2 + E + b2 + E 2 + E 2 + E 2 + E 2 + + failure_demo.py:58: AssertionError + _________________ TestSpecialisedExplanations.test_eq_list _________________ + + self = + + def test_eq_list(self): + > assert [0, 1, 2] == [0, 1, 3] + E assert [0, 1, 2] == [0, 1, 3] + E At index 2 diff: 2 != 3 + + failure_demo.py:61: AssertionError + ______________ TestSpecialisedExplanations.test_eq_list_long _______________ + + self = + + def test_eq_list_long(self): + a = [0]*100 + [1] + [3]*100 + b = [0]*100 + [2] + [3]*100 + > assert a == b + E assert [0, 0, 0, 0, 0, 0, ...] == [0, 0, 0, 0, 0, 0, ...] + E At index 100 diff: 1 != 2 + + failure_demo.py:66: AssertionError + _________________ TestSpecialisedExplanations.test_eq_dict _________________ + + 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 ? ^ + + failure_demo.py:69: AssertionError + _________________ TestSpecialisedExplanations.test_eq_set __________________ + + self = + + def test_eq_set(self): + > assert set([0, 10, 11, 12]) == set([0, 20, 21]) + E assert set([0, 10, 11, 12]) == set([0, 20, 21]) + E Extra items in the left set: + E 10 + E 11 + E 12 + E Extra items in the right set: + E 20 + E 21 + + failure_demo.py:72: AssertionError + _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ + + self = + + def test_eq_longer_list(self): + > assert [1,2] == [1,2,3] + E assert [1, 2] == [1, 2, 3] + E Right contains more items, first extra item: 3 + + failure_demo.py:75: AssertionError + _________________ TestSpecialisedExplanations.test_in_list _________________ + + self = + + def test_in_list(self): + > assert 1 in [0, 2, 3, 4, 5] + E assert 1 in [0, 2, 3, 4, 5] + + failure_demo.py:78: AssertionError + ______________________________ test_attribute ______________________________ + + def test_attribute(): + class Foo(object): + b = 1 + i = Foo() + > assert i.b == 2 + E assert 1 == 2 + E + where 1 = .b + + failure_demo.py:85: AssertionError + _________________________ test_attribute_instance __________________________ + + def test_attribute_instance(): + class Foo(object): + b = 1 + > assert Foo().b == 2 + E assert 1 == 2 + E + where 1 = .b + E + where = () + + failure_demo.py:91: AssertionError + __________________________ test_attribute_failure __________________________ + + def test_attribute_failure(): + class Foo(object): + def _get_b(self): + raise Exception('Failed to get attrib') + b = property(_get_b) + i = Foo() + > assert i.b == 2 + + failure_demo.py:100: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + self = + + def _get_b(self): + > raise Exception('Failed to get attrib') + E Exception: Failed to get attrib + + failure_demo.py:97: Exception + _________________________ test_attribute_multiple __________________________ + + def test_attribute_multiple(): + class Foo(object): + b = 1 + class Bar(object): + b = 2 + > assert Foo().b == Bar().b + E assert 1 == 2 + E + where 1 = .b + E + where = () + E + and 2 = .b + E + where = () + + failure_demo.py:108: AssertionError + __________________________ TestRaises.test_raises __________________________ + + self = + + def test_raises(self): + s = 'qwe' + > raises(TypeError, "int(s)") + + failure_demo.py:117: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + > int(s) + E ValueError: invalid literal for int() with base 10: 'qwe' + + <0-codegen /home/hpk/p/pytest/_pytest/python.py:819>:1: ValueError + ______________________ TestRaises.test_raises_doesnt _______________________ + + self = + + def test_raises_doesnt(self): + > raises(IOError, "int('3')") + E Failed: DID NOT RAISE + + failure_demo.py:120: Failed + __________________________ TestRaises.test_raise ___________________________ + + self = + + def test_raise(self): + > raise ValueError("demo error") + E ValueError: demo error + + failure_demo.py:123: ValueError + ________________________ TestRaises.test_tupleerror ________________________ + + self = + + def test_tupleerror(self): + > a,b = [1] + E ValueError: need more than 1 value to unpack + + failure_demo.py:126: ValueError + ______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______ + + self = + + def test_reinterpret_fails_with_print_for_the_fun_of_it(self): + l = [1,2,3] + print ("l is %r" % l) + > a,b = l.pop() + E TypeError: 'int' object is not iterable + + failure_demo.py:131: TypeError + ----------------------------- Captured stdout ------------------------------ + l is [1, 2, 3] + ________________________ TestRaises.test_some_error ________________________ + + self = + + def test_some_error(self): + > if namenotexi: + E NameError: global name 'namenotexi' is not defined + + failure_demo.py:134: NameError + ____________________ test_dynamic_compile_shows_nicely _____________________ + + def test_dynamic_compile_shows_nicely(): + src = 'def foo():\n assert 1 == 0\n' + name = 'abc-123' + module = py.std.imp.new_module(name) + code = py.code.compile(src, name, 'exec') + py.builtin.exec_(code, module.__dict__) + py.std.sys.modules[name] = module + > module.foo() + + failure_demo.py:149: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + def foo(): + > assert 1 == 0 + E assert 1 == 0 + + <2-codegen 'abc-123' /home/hpk/p/pytest/doc/example/assertion/failure_demo.py:146>:2: AssertionError + ____________________ TestMoreErrors.test_complex_error _____________________ + + self = + + def test_complex_error(self): + def f(): + return 44 + def g(): + return 43 + > somefunc(f(), g()) + + failure_demo.py:159: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + x = 44, y = 43 + + def somefunc(x,y): + > otherfunc(x,y) + + failure_demo.py:8: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + + a = 44, b = 43 + + def otherfunc(a,b): + > assert a==b + E assert 44 == 43 + + failure_demo.py:5: AssertionError + ___________________ TestMoreErrors.test_z1_unpack_error ____________________ + + self = + + def test_z1_unpack_error(self): + l = [] + > a,b = l + E ValueError: need more than 0 values to unpack + + failure_demo.py:163: ValueError + ____________________ TestMoreErrors.test_z2_type_error _____________________ + + self = + + def test_z2_type_error(self): + l = 3 + > a,b = l + E TypeError: 'int' object is not iterable + + failure_demo.py:167: TypeError + ______________________ TestMoreErrors.test_startswith ______________________ + + self = + + def test_startswith(self): + s = "123" + g = "456" + > assert s.startswith(g) + E assert ('456') + E + where = '123'.startswith + + failure_demo.py:172: AssertionError + __________________ TestMoreErrors.test_startswith_nested ___________________ + + self = + + def test_startswith_nested(self): + def f(): + return "123" + def g(): + return "456" + > assert f().startswith(g()) + E assert ('456') + E + where = '123'.startswith + E + where '123' = () + E + and '456' = () + + failure_demo.py:179: AssertionError + _____________________ TestMoreErrors.test_global_func ______________________ + + self = + + def test_global_func(self): + > assert isinstance(globf(42), float) + E assert isinstance(43, float) + E + where 43 = globf(42) + + failure_demo.py:182: AssertionError + _______________________ TestMoreErrors.test_instance _______________________ + + self = + + def test_instance(self): + self.x = 6*7 + > assert self.x != 42 + E assert 42 != 42 + E + where 42 = 42 + E + where 42 = .x + + failure_demo.py:186: AssertionError + _______________________ TestMoreErrors.test_compare ________________________ + + self = + + def test_compare(self): + > assert globf(10) < 5 + E assert 11 < 5 + E + where 11 = globf(10) + + failure_demo.py:189: AssertionError + _____________________ TestMoreErrors.test_try_finally ______________________ + + self = + + def test_try_finally(self): + x = 1 + try: + > assert x == 0 + E assert 1 == 0 + + failure_demo.py:194: AssertionError + ======================== 35 failed in 0.19 seconds ========================= --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -3,6 +3,8 @@ Installation and Getting Started **Compatibility**: Python 2.4-3.2, Jython, PyPy on Unix/Posix and Windows +.. _`getstarted`: + Installation ---------------------------------------- @@ -18,6 +20,8 @@ To check your installation has installed If you get an error checkout :ref:`installation issues`. +.. _`simpletest`: + Our first test run ---------------------------------------------------------- @@ -55,16 +59,16 @@ py.test found the ``test_answer`` functi .. note:: You can simply use the ``assert`` statement for coding expectations because - intermediate values will be presented to you. This is much easier than - learning all the `the JUnit legacy methods`_ which are even inconsistent - with Python's own coding guidelines (but consistent with - Java-style naming). + intermediate values will be presented to you. This is arguably easier than + learning all the `the JUnit legacy methods`_. - There is only one seldomly hit caveat to using asserts: if your - assertion expression fails and has side effects then re-evaluating - it for presenting intermediate values can go wrong. It's easy to fix: - compute the value ahead of the assert and then do the - assertion or use the assert "message" syntax:: + However, there remains one caveat to using simple asserts: your + assertion expression should better be side-effect free. Because + after an assertion failed py.test will re-evaluate the expression + in order to present intermediate values. You will get a nice warning + and you can easily fix it: compute the value ahead of the assert and + then do the assertion. Or maybe just use the assert "explicit message" + syntax:: assert expr, "message" # show "message" if expr is not True --- a/doc/example/assertion/failure_demo.py +++ b/doc/example/assertion/failure_demo.py @@ -37,6 +37,120 @@ class TestFailing(object): return 42 assert not f() +class TestSpecialisedExplanations(object): + def test_eq_text(self): + assert 'spam' == 'eggs' + + def test_eq_similar_text(self): + assert 'foo 1 bar' == 'foo 2 bar' + + def test_eq_multiline_text(self): + assert 'foo\nspam\nbar' == 'foo\neggs\nbar' + + def test_eq_long_text(self): + a = '1'*100 + 'a' + '2'*100 + b = '1'*100 + 'b' + '2'*100 + assert a == b + + 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 + + def test_eq_list(self): + assert [0, 1, 2] == [0, 1, 3] + + def test_eq_list_long(self): + a = [0]*100 + [1] + [3]*100 + b = [0]*100 + [2] + [3]*100 + assert a == b + + def test_eq_dict(self): + assert {'a': 0, 'b': 1} == {'a': 0, 'b': 2} + + def test_eq_set(self): + assert set([0, 10, 11, 12]) == set([0, 20, 21]) + + def test_eq_longer_list(self): + assert [1,2] == [1,2,3] + + def test_in_list(self): + assert 1 in [0, 2, 3, 4, 5] + + +def test_attribute(): + class Foo(object): + b = 1 + i = Foo() + assert i.b == 2 + + +def test_attribute_instance(): + class Foo(object): + b = 1 + assert Foo().b == 2 + + +def test_attribute_failure(): + class Foo(object): + def _get_b(self): + raise Exception('Failed to get attrib') + b = property(_get_b) + i = Foo() + assert i.b == 2 + + +def test_attribute_multiple(): + class Foo(object): + b = 1 + class Bar(object): + b = 2 + assert Foo().b == Bar().b + + +def globf(x): + return x+1 + +class TestRaises: + def test_raises(self): + s = 'qwe' + raises(TypeError, "int(s)") + + def test_raises_doesnt(self): + raises(IOError, "int('3')") + + def test_raise(self): + raise ValueError("demo error") + + def test_tupleerror(self): + a,b = [1] + + def test_reinterpret_fails_with_print_for_the_fun_of_it(self): + l = [1,2,3] + print ("l is %r" % l) + a,b = l.pop() + + def test_some_error(self): + if namenotexi: + pass + + def func1(self): + assert 41 == 42 + + +# thanks to Matthew Scott for this test +def test_dynamic_compile_shows_nicely(): + src = 'def foo():\n assert 1 == 0\n' + name = 'abc-123' + module = py.std.imp.new_module(name) + code = py.code.compile(src, name, 'exec') + py.builtin.exec_(code, module.__dict__) + py.std.sys.modules[name] = module + module.foo() + + + +class TestMoreErrors: def test_complex_error(self): def f(): return 44 @@ -81,113 +195,3 @@ class TestFailing(object): finally: x = 0 - def test_raises(self): - s = 'qwe' - raises(TypeError, "int(s)") - - def test_raises_doesnt(self): - raises(IOError, "int('3')") - - def test_raise(self): - raise ValueError("demo error") - - def test_tupleerror(self): - a,b = [1] - - def test_reinterpret_fails_with_print_for_the_fun_of_it(self): - l = [1,2,3] - print ("l is %r" % l) - a,b = l.pop() - - def test_some_error(self): - if namenotexi: - pass - - def func1(self): - assert 41 == 42 - - -# thanks to Matthew Scott for this test -def test_dynamic_compile_shows_nicely(): - src = 'def foo():\n assert 1 == 0\n' - name = 'abc-123' - module = py.std.imp.new_module(name) - code = py.code.compile(src, name, 'exec') - py.builtin.exec_(code, module.__dict__) - py.std.sys.modules[name] = module - module.foo() - - -class TestSpecialisedExplanations(object): - def test_eq_text(self): - assert 'spam' == 'eggs' - - def test_eq_similar_text(self): - assert 'foo 1 bar' == 'foo 2 bar' - - def test_eq_multiline_text(self): - assert 'foo\nspam\nbar' == 'foo\neggs\nbar' - - def test_eq_long_text(self): - a = '1'*100 + 'a' + '2'*100 - b = '1'*100 + 'b' + '2'*100 - assert a == b - - 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 - - def test_eq_list(self): - assert [0, 1, 2] == [0, 1, 3] - - def test_eq_list_long(self): - a = [0]*100 + [1] + [3]*100 - b = [0]*100 + [2] + [3]*100 - assert a == b - - def test_eq_dict(self): - assert {'a': 0, 'b': 1} == {'a': 0, 'b': 2} - - def test_eq_set(self): - assert set([0, 10, 11, 12]) == set([0, 20, 21]) - - def test_eq_longer_list(self): - assert [1,2] == [1,2,3] - - def test_in_list(self): - assert 1 in [0, 2, 3, 4, 5] - - -def test_attribute(): - class Foo(object): - b = 1 - i = Foo() - assert i.b == 2 - - -def test_attribute_instance(): - class Foo(object): - b = 1 - assert Foo().b == 2 - - -def test_attribute_failure(): - class Foo(object): - def _get_b(self): - raise Exception('Failed to get attrib') - b = property(_get_b) - i = Foo() - assert i.b == 2 - - -def test_attribute_multiple(): - class Foo(object): - b = 1 - class Bar(object): - b = 2 - assert Foo().b == Bar().b - - -def globf(x): - return x+1 --- a/doc/changelog.txt +++ b/doc/changelog.txt @@ -1,3 +1,5 @@ + +.. _changelog: Changelog history ================================= --- a/doc/announce/release-1.3.1.txt +++ /dev/null @@ -1,104 +0,0 @@ -py.test/pylib 1.3.1: new py.test.xfail, --maxfail, better reporting -=========================================================================== - -The pylib/py.test 1.3.1 release brings: - -- the new imperative ``py.test.xfail()`` helper in order to have a test or - setup function result in an "expected failure" -- a new option ``--maxfail=NUM`` to stop the test run after some failures -- markers/decorators are now applicable to test classes (>=Python2.6) -- improved reporting, shorter tracebacks in several cases -- some simplified internals, more compatibility with Jython and PyPy -- bug fixes and various refinements - -See the below CHANGELOG entry below for more details and -http://pylib.org/install.html for installation instructions. - -If you used older versions of py.test you should be able to upgrade -to 1.3.1 without changes to your test source code. - -py.test is an automated testing tool working with Python2, -Python3, Jython and PyPy versions on all major operating systems. It -offers a no-boilerplate testing approach and has inspired other testing -tools and enhancements in the standard Python library for more than five -years. It has a simple and extensive plugin architecture, configurable -reporting and provides unique ways to make it fit to your testing -process and needs. - -See http://pytest.org for more info. - -cheers and have fun, - -holger krekel - -Changes between 1.3.0 and 1.3.1 -================================================== - -New features -++++++++++++++++++ - -- issue91: introduce new py.test.xfail(reason) helper - to imperatively mark a test as expected to fail. Can - be used from within setup and test functions. This is - useful especially for parametrized tests when certain - configurations are expected-to-fail. In this case the - declarative approach with the @py.test.mark.xfail cannot - be used as it would mark all configurations as xfail. - -- issue102: introduce new --maxfail=NUM option to stop - test runs after NUM failures. This is a generalization - of the '-x' or '--exitfirst' option which is now equivalent - to '--maxfail=1'. Both '-x' and '--maxfail' will - now also print a line near the end indicating the Interruption. - -- issue89: allow py.test.mark decorators to be used on classes - (class decorators were introduced with python2.6) and - also allow to have multiple markers applied at class/module level - by specifying a list. - -- improve and refine letter reporting in the progress bar: - . pass - f failed test - s skipped tests (reminder: use for dependency/platform mismatch only) - x xfailed test (test that was expected to fail) - X xpassed test (test that was expected to fail but passed) - - You can use any combination of 'fsxX' with the '-r' extended - reporting option. The xfail/xpass results will show up as - skipped tests in the junitxml output - which also fixes - issue99. - -- make py.test.cmdline.main() return the exitstatus instead of raising - SystemExit and also allow it to be called multiple times. This of - course requires that your application and tests are properly teared - down and don't have global state. - -Fixes / Maintenance -++++++++++++++++++++++ - -- improved traceback presentation: - - improved and unified reporting for "--tb=short" option - - Errors during test module imports are much shorter, (using --tb=short style) - - raises shows shorter more relevant tracebacks - - --fulltrace now more systematically makes traces longer / inhibits cutting - -- improve support for raises and other dynamically compiled code by - manipulating python's linecache.cache instead of the previous - rather hacky way of creating custom code objects. This makes - it seemlessly work on Jython and PyPy where it previously didn't. - -- fix issue96: make capturing more resilient against Control-C - interruptions (involved somewhat substantial refactoring - to the underlying capturing functionality to avoid race - conditions). - -- fix chaining of conditional skipif/xfail decorators - so it works now - as expected to use multiple @py.test.mark.skipif(condition) decorators, - including specific reporting which of the conditions lead to skipping. - -- fix issue95: late-import zlib so that it's not required - for general py.test startup. - -- fix issue94: make reporting more robust against bogus source code - (and internally be more careful when presenting unexpected byte sequences) - --- a/doc/xunit_setup.txt +++ b/doc/xunit_setup.txt @@ -1,3 +1,5 @@ +.. _xunitsetup: + ==================================== extended xUnit style setup fixtures ==================================== --- a/doc/example/index.txt +++ b/doc/example/index.txt @@ -4,12 +4,13 @@ Usages and Examples =========================================== -This is a (growing) list of examples. :ref:`Contact ` us if you -need more examples or have questions. +Here is a (growing) list of examples. :ref:`Contact ` us if you +need more examples or have questions. Also take a look at the :ref:`comprehensive documentation ` which contains many example snippets as well. .. toctree:: :maxdepth: 2 + reportingdemo.txt simple.txt pythoncollection.txt mysetup.txt --- a/_pytest/helpconfig.py +++ b/_pytest/helpconfig.py @@ -40,7 +40,8 @@ def showhelp(config): tw.line() tw.line() #tw.sep( "=", "config file settings") - tw.line("setup.cfg or tox.ini options to be put into [pytest] section:") + tw.line("[pytest] ini-options in the next " + "pytest.ini|tox.ini|setup.cfg file:") tw.line() for name in config._parser._ininames: --- a/doc/announce/release-1.3.2.txt +++ /dev/null @@ -1,720 +0,0 @@ -py.test/pylib 1.3.2: API and reporting refinements, many fixes -=========================================================================== - -The pylib/py.test 1.3.2 release brings many bug fixes and some new -features. It was refined for and tested against the recently released -Python2.7 and remains compatibile to the usual armada of interpreters -(Python2.4 through to Python3.1.2, Jython and PyPy). Note that for using -distributed testing features you'll need to upgrade to the jointly released -pytest-xdist-1.4 because of some internal refactorings. - -See http://pytest.org for general documentation and below for -a detailed CHANGELOG. - -cheers & particular thanks to Benjamin Peterson, Ronny Pfannschmidt -and all issue and patch contributors, - -holger krekel - -Changes between 1.3.1 and 1.3.2 -================================================== - -New features -++++++++++++++++++ - -- fix issue103: introduce py.test.raises as context manager, examples:: - - with py.test.raises(ZeroDivisionError): - x = 0 - 1 / x - - with py.test.raises(RuntimeError) as excinfo: - call_something() - - # you may do extra checks on excinfo.value|type|traceback here - - (thanks Ronny Pfannschmidt) - -- Funcarg factories can now dynamically apply a marker to a - test invocation. This is for example useful if a factory - provides parameters to a test which are expected-to-fail:: - - def pytest_funcarg__arg(request): - request.applymarker(py.test.mark.xfail(reason="flaky config")) - ... - - def test_function(arg): - ... - -- improved error reporting on collection and import errors. This makes - use of a more general mechanism, namely that for custom test item/collect - nodes ``node.repr_failure(excinfo)`` is now uniformly called so that you can - override it to return a string error representation of your choice - which is going to be reported as a (red) string. - -- introduce '--junitprefix=STR' option to prepend a prefix - to all reports in the junitxml file. - -Bug fixes / Maintenance -++++++++++++++++++++++++++ - -- make tests and the ``pytest_recwarn`` plugin in particular fully compatible - to Python2.7 (if you use the ``recwarn`` funcarg warnings will be enabled so that - you can properly check for their existence in a cross-python manner). -- refine --pdb: ignore xfailed tests, unify its TB-reporting and - don't display failures again at the end. -- fix assertion interpretation with the ** operator (thanks Benjamin Peterson) -- fix issue105 assignment on the same line as a failing assertion (thanks Benjamin Peterson) -- fix issue104 proper escaping for test names in junitxml plugin (thanks anonymous) -- fix issue57 -f|--looponfail to work with xpassing tests (thanks Ronny) -- fix issue92 collectonly reporter and --pastebin (thanks Benjamin Peterson) -- fix py.code.compile(source) to generate unique filenames -- fix assertion re-interp problems on PyPy, by defering code - compilation to the (overridable) Frame.eval class. (thanks Amaury Forgeot) -- fix py.path.local.pyimport() to work with directories -- streamline py.path.local.mkdtemp implementation and usage -- don't print empty lines when showing junitxml-filename -- add optional boolean ignore_errors parameter to py.path.local.remove -- fix terminal writing on win32/python2.4 -- py.process.cmdexec() now tries harder to return properly encoded unicode objects - on all python versions -- install plain py.test/py.which scripts also for Jython, this helps to - get canonical script paths in virtualenv situations -- make path.bestrelpath(path) return ".", note that when calling - X.bestrelpath the assumption is that X is a directory. -- make initial conftest discovery ignore "--" prefixed arguments -- fix resultlog plugin when used in an multicpu/multihost xdist situation - (thanks Jakub Gustak) -- perform distributed testing related reporting in the xdist-plugin - rather than having dist-related code in the generic py.test - distribution -- fix homedir detection on Windows -- ship distribute_setup.py version 0.6.13 - -Changes between 1.3.0 and 1.3.1 -================================================== - -New features -++++++++++++++++++ - -- issue91: introduce new py.test.xfail(reason) helper - to imperatively mark a test as expected to fail. Can - be used from within setup and test functions. This is - useful especially for parametrized tests when certain - configurations are expected-to-fail. In this case the - declarative approach with the @py.test.mark.xfail cannot - be used as it would mark all configurations as xfail. - -- issue102: introduce new --maxfail=NUM option to stop - test runs after NUM failures. This is a generalization - of the '-x' or '--exitfirst' option which is now equivalent - to '--maxfail=1'. Both '-x' and '--maxfail' will - now also print a line near the end indicating the Interruption. - -- issue89: allow py.test.mark decorators to be used on classes - (class decorators were introduced with python2.6) and - also allow to have multiple markers applied at class/module level - by specifying a list. - -- improve and refine letter reporting in the progress bar: - . pass - f failed test - s skipped tests (reminder: use for dependency/platform mismatch only) - x xfailed test (test that was expected to fail) - X xpassed test (test that was expected to fail but passed) - - You can use any combination of 'fsxX' with the '-r' extended - reporting option. The xfail/xpass results will show up as - skipped tests in the junitxml output - which also fixes - issue99. - -- make py.test.cmdline.main() return the exitstatus instead of raising - SystemExit and also allow it to be called multiple times. This of - course requires that your application and tests are properly teared - down and don't have global state. - -Fixes / Maintenance -++++++++++++++++++++++ - -- improved traceback presentation: - - improved and unified reporting for "--tb=short" option - - Errors during test module imports are much shorter, (using --tb=short style) - - raises shows shorter more relevant tracebacks - - --fulltrace now more systematically makes traces longer / inhibits cutting - -- improve support for raises and other dynamically compiled code by - manipulating python's linecache.cache instead of the previous - rather hacky way of creating custom code objects. This makes - it seemlessly work on Jython and PyPy where it previously didn't. - -- fix issue96: make capturing more resilient against Control-C - interruptions (involved somewhat substantial refactoring - to the underlying capturing functionality to avoid race - conditions). - -- fix chaining of conditional skipif/xfail decorators - so it works now - as expected to use multiple @py.test.mark.skipif(condition) decorators, - including specific reporting which of the conditions lead to skipping. - -- fix issue95: late-import zlib so that it's not required - for general py.test startup. - -- fix issue94: make reporting more robust against bogus source code - (and internally be more careful when presenting unexpected byte sequences) - - -Changes between 1.2.1 and 1.3.0 -================================================== - -- deprecate --report option in favour of a new shorter and easier to - remember -r option: it takes a string argument consisting of any - combination of 'xfsX' characters. They relate to the single chars - you see during the dotted progress printing and will print an extra line - per test at the end of the test run. This extra line indicates the exact - position or test ID that you directly paste to the py.test cmdline in order - to re-run a particular test. - -- allow external plugins to register new hooks via the new - pytest_addhooks(pluginmanager) hook. The new release of - the pytest-xdist plugin for distributed and looponfailing - testing requires this feature. - -- add a new pytest_ignore_collect(path, config) hook to allow projects and - plugins to define exclusion behaviour for their directory structure - - for example you may define in a conftest.py this method:: - - def pytest_ignore_collect(path): - return path.check(link=1) - - to prevent even a collection try of any tests in symlinked dirs. - -- new pytest_pycollect_makemodule(path, parent) hook for - allowing customization of the Module collection object for a - matching test module. - -- extend and refine xfail mechanism: - ``@py.test.mark.xfail(run=False)`` do not run the decorated test - ``@py.test.mark.xfail(reason="...")`` prints the reason string in xfail summaries - specifiying ``--runxfail`` on command line virtually ignores xfail markers - -- expose (previously internal) commonly useful methods: - py.io.get_terminal_with() -> return terminal width - py.io.ansi_print(...) -> print colored/bold text on linux/win32 - py.io.saferepr(obj) -> return limited representation string - -- expose test outcome related exceptions as py.test.skip.Exception, - py.test.raises.Exception etc., useful mostly for plugins - doing special outcome interpretation/tweaking - -- (issue85) fix junitxml plugin to handle tests with non-ascii output - -- fix/refine python3 compatibility (thanks Benjamin Peterson) - -- fixes for making the jython/win32 combination work, note however: - jython2.5.1/win32 does not provide a command line launcher, see - http://bugs.jython.org/issue1491 . See pylib install documentation - for how to work around. - -- fixes for handling of unicode exception values and unprintable objects - -- (issue87) fix unboundlocal error in assertionold code - -- (issue86) improve documentation for looponfailing - -- refine IO capturing: stdin-redirect pseudo-file now has a NOP close() method - -- ship distribute_setup.py version 0.6.10 - -- added links to the new capturelog and coverage plugins - - -Changes between 1.2.1 and 1.2.0 -===================================== - -- refined usage and options for "py.cleanup":: - - py.cleanup # remove "*.pyc" and "*$py.class" (jython) files - py.cleanup -e .swp -e .cache # also remove files with these extensions - py.cleanup -s # remove "build" and "dist" directory next to setup.py files - py.cleanup -d # also remove empty directories - py.cleanup -a # synonym for "-s -d -e 'pip-log.txt'" - py.cleanup -n # dry run, only show what would be removed - -- add a new option "py.test --funcargs" which shows available funcargs - and their help strings (docstrings on their respective factory function) - for a given test path - -- display a short and concise traceback if a funcarg lookup fails - -- early-load "conftest.py" files in non-dot first-level sub directories. - allows to conveniently keep and access test-related options in a ``test`` - subdir and still add command line options. - -- fix issue67: new super-short traceback-printing option: "--tb=line" will print a single line for each failing (python) test indicating its filename, lineno and the failure value - -- fix issue78: always call python-level teardown functions even if the - according setup failed. This includes refinements for calling setup_module/class functions - which will now only be called once instead of the previous behaviour where they'd be called - multiple times if they raise an exception (including a Skipped exception). Any exception - will be re-corded and associated with all tests in the according module/class scope. - -- fix issue63: assume <40 columns to be a bogus terminal width, default to 80 - -- fix pdb debugging to be in the correct frame on raises-related errors - -- update apipkg.py to fix an issue where recursive imports might - unnecessarily break importing - -- fix plugin links - -Changes between 1.2 and 1.1.1 -===================================== - -- moved dist/looponfailing from py.test core into a new - separately released pytest-xdist plugin. - -- new junitxml plugin: --junitxml=path will generate a junit style xml file - which is processable e.g. by the Hudson CI system. - -- new option: --genscript=path will generate a standalone py.test script - which will not need any libraries installed. thanks to Ralf Schmitt. - -- new option: --ignore will prevent specified path from collection. - Can be specified multiple times. - -- new option: --confcutdir=dir will make py.test only consider conftest - files that are relative to the specified dir. - -- new funcarg: "pytestconfig" is the pytest config object for access - to command line args and can now be easily used in a test. - -- install 'py.test' and `py.which` with a ``-$VERSION`` suffix to - disambiguate between Python3, python2.X, Jython and PyPy installed versions. - -- new "pytestconfig" funcarg allows access to test config object - -- new "pytest_report_header" hook can return additional lines - to be displayed at the header of a test run. - -- (experimental) allow "py.test path::name1::name2::..." for pointing - to a test within a test collection directly. This might eventually - evolve as a full substitute to "-k" specifications. - -- streamlined plugin loading: order is now as documented in - customize.html: setuptools, ENV, commandline, conftest. - also setuptools entry point names are turned to canonical namees ("pytest_*") - -- automatically skip tests that need 'capfd' but have no os.dup - -- allow pytest_generate_tests to be defined in classes as well - -- deprecate usage of 'disabled' attribute in favour of pytestmark -- deprecate definition of Directory, Module, Class and Function nodes - in conftest.py files. Use pytest collect hooks instead. - -- collection/item node specific runtest/collect hooks are only called exactly - on matching conftest.py files, i.e. ones which are exactly below - the filesystem path of an item - -- change: the first pytest_collect_directory hook to return something - will now prevent further hooks to be called. - -- change: figleaf plugin now requires --figleaf to run. Also - change its long command line options to be a bit shorter (see py.test -h). - -- change: pytest doctest plugin is now enabled by default and has a - new option --doctest-glob to set a pattern for file matches. - -- change: remove internal py._* helper vars, only keep py._pydir - -- robustify capturing to survive if custom pytest_runtest_setup - code failed and prevented the capturing setup code from running. - -- make py.test.* helpers provided by default plugins visible early - - works transparently both for pydoc and for interactive sessions - which will regularly see e.g. py.test.mark and py.test.importorskip. - -- simplify internal plugin manager machinery -- simplify internal collection tree by introducing a RootCollector node - -- fix assert reinterpreation that sees a call containing "keyword=..." - -- fix issue66: invoke pytest_sessionstart and pytest_sessionfinish - hooks on slaves during dist-testing, report module/session teardown - hooks correctly. - -- fix issue65: properly handle dist-testing if no - execnet/py lib installed remotely. - -- skip some install-tests if no execnet is available - -- fix docs, fix internal bin/ script generation - - -Changes between 1.1.1 and 1.1.0 -===================================== - -- introduce automatic plugin registration via 'pytest11' - entrypoints via setuptools' pkg_resources.iter_entry_points - -- fix py.test dist-testing to work with execnet >= 1.0.0b4 - -- re-introduce py.test.cmdline.main() for better backward compatibility - -- svn paths: fix a bug with path.check(versioned=True) for svn paths, - allow '%' in svn paths, make svnwc.update() default to interactive mode - like in 1.0.x and add svnwc.update(interactive=False) to inhibit interaction. - -- refine distributed tarball to contain test and no pyc files - -- try harder to have deprecation warnings for py.compat.* accesses - report a correct location - -Changes between 1.1.0 and 1.0.2 -===================================== - -* adjust and improve docs - -* remove py.rest tool and internal namespace - it was - never really advertised and can still be used with - the old release if needed. If there is interest - it could be revived into its own tool i guess. - -* fix issue48 and issue59: raise an Error if the module - from an imported test file does not seem to come from - the filepath - avoids "same-name" confusion that has - been reported repeatedly - -* merged Ronny's nose-compatibility hacks: now - nose-style setup_module() and setup() functions are - supported - -* introduce generalized py.test.mark function marking - -* reshuffle / refine command line grouping - -* deprecate parser.addgroup in favour of getgroup which creates option group - -* add --report command line option that allows to control showing of skipped/xfailed sections - -* generalized skipping: a new way to mark python functions with skipif or xfail - at function, class and modules level based on platform or sys-module attributes. - -* extend py.test.mark decorator to allow for positional args - -* introduce and test "py.cleanup -d" to remove empty directories - -* fix issue #59 - robustify unittest test collection - -* make bpython/help interaction work by adding an __all__ attribute - to ApiModule, cleanup initpkg - -* use MIT license for pylib, add some contributors - -* remove py.execnet code and substitute all usages with 'execnet' proper - -* fix issue50 - cached_setup now caches more to expectations - for test functions with multiple arguments. - -* merge Jarko's fixes, issue #45 and #46 - -* add the ability to specify a path for py.lookup to search in - -* fix a funcarg cached_setup bug probably only occuring - in distributed testing and "module" scope with teardown. - -* many fixes and changes for making the code base python3 compatible, - many thanks to Benjamin Peterson for helping with this. - -* consolidate builtins implementation to be compatible with >=2.3, - add helpers to ease keeping 2 and 3k compatible code - -* deprecate py.compat.doctest|subprocess|textwrap|optparse - -* deprecate py.magic.autopath, remove py/magic directory - -* move pytest assertion handling to py/code and a pytest_assertion - plugin, add "--no-assert" option, deprecate py.magic namespaces - in favour of (less) py.code ones. - -* consolidate and cleanup py/code classes and files - -* cleanup py/misc, move tests to bin-for-dist - -* introduce delattr/delitem/delenv methods to py.test's monkeypatch funcarg - -* consolidate py.log implementation, remove old approach. - -* introduce py.io.TextIO and py.io.BytesIO for distinguishing between - text/unicode and byte-streams (uses underlying standard lib io.* - if available) - -* make py.unittest_convert helper script available which converts "unittest.py" - style files into the simpler assert/direct-test-classes py.test/nosetests - style. The script was written by Laura Creighton. - -* simplified internal localpath implementation - -Changes between 1.0.1 and 1.0.2 -===================================== - -* fixing packaging issues, triggered by fedora redhat packaging, - also added doc, examples and contrib dirs to the tarball. - -* added a documentation link to the new django plugin. - -Changes between 1.0.0 and 1.0.1 -===================================== - -* added a 'pytest_nose' plugin which handles nose.SkipTest, - nose-style function/method/generator setup/teardown and - tries to report functions correctly. - -* capturing of unicode writes or encoded strings to sys.stdout/err - work better, also terminalwriting was adapted and somewhat - unified between windows and linux. - -* improved documentation layout and content a lot - -* added a "--help-config" option to show conftest.py / ENV-var names for - all longopt cmdline options, and some special conftest.py variables. - renamed 'conf_capture' conftest setting to 'option_capture' accordingly. - -* fix issue #27: better reporting on non-collectable items given on commandline - (e.g. pyc files) - -* fix issue #33: added --version flag (thanks Benjamin Peterson) - -* fix issue #32: adding support for "incomplete" paths to wcpath.status() - -* "Test" prefixed classes are *not* collected by default anymore if they - have an __init__ method - -* monkeypatch setenv() now accepts a "prepend" parameter - -* improved reporting of collection error tracebacks - -* simplified multicall mechanism and plugin architecture, - renamed some internal methods and argnames - -Changes between 1.0.0b9 and 1.0.0 -===================================== - -* more terse reporting try to show filesystem path relatively to current dir -* improve xfail output a bit - -Changes between 1.0.0b8 and 1.0.0b9 -===================================== - -* cleanly handle and report final teardown of test setup - -* fix svn-1.6 compat issue with py.path.svnwc().versioned() - (thanks Wouter Vanden Hove) - -* setup/teardown or collection problems now show as ERRORs - or with big "E"'s in the progress lines. they are reported - and counted separately. - -* dist-testing: properly handle test items that get locally - collected but cannot be collected on the remote side - often - due to platform/dependency reasons - -* simplified py.test.mark API - see keyword plugin documentation - -* integrate better with logging: capturing now by default captures - test functions and their immediate setup/teardown in a single stream - -* capsys and capfd funcargs now have a readouterr() and a close() method - (underlyingly py.io.StdCapture/FD objects are used which grew a - readouterr() method as well to return snapshots of captured out/err) - -* make assert-reinterpretation work better with comparisons not - returning bools (reported with numpy from thanks maciej fijalkowski) - -* reworked per-test output capturing into the pytest_iocapture.py plugin - and thus removed capturing code from config object - -* item.repr_failure(excinfo) instead of item.repr_failure(excinfo, outerr) - - -Changes between 1.0.0b7 and 1.0.0b8 -===================================== - -* pytest_unittest-plugin is now enabled by default - -* introduced pytest_keyboardinterrupt hook and - refined pytest_sessionfinish hooked, added tests. - -* workaround a buggy logging module interaction ("closing already closed - files"). Thanks to Sridhar Ratnakumar for triggering. - -* if plugins use "py.test.importorskip" for importing - a dependency only a warning will be issued instead - of exiting the testing process. - -* many improvements to docs: - - refined funcargs doc , use the term "factory" instead of "provider" - - added a new talk/tutorial doc page - - better download page - - better plugin docstrings - - added new plugins page and automatic doc generation script - -* fixed teardown problem related to partially failing funcarg setups - (thanks MrTopf for reporting), "pytest_runtest_teardown" is now - always invoked even if the "pytest_runtest_setup" failed. - -* tweaked doctest output for docstrings in py modules, - thanks Radomir. - -Changes between 1.0.0b3 and 1.0.0b7 -============================================= - -* renamed py.test.xfail back to py.test.mark.xfail to avoid - two ways to decorate for xfail - -* re-added py.test.mark decorator for setting keywords on functions - (it was actually documented so removing it was not nice) - -* remove scope-argument from request.addfinalizer() because - request.cached_setup has the scope arg. TOOWTDI. - -* perform setup finalization before reporting failures - -* apply modified patches from Andreas Kloeckner to allow - test functions to have no func_code (#22) and to make - "-k" and function keywords work (#20) - -* apply patch from Daniel Peolzleithner (issue #23) - -* resolve issue #18, multiprocessing.Manager() and - redirection clash - -* make __name__ == "__channelexec__" for remote_exec code - -Changes between 1.0.0b1 and 1.0.0b3 -============================================= - -* plugin classes are removed: one now defines - hooks directly in conftest.py or global pytest_*.py - files. - -* added new pytest_namespace(config) hook that allows - to inject helpers directly to the py.test.* namespace. - -* documented and refined many hooks - -* added new style of generative tests via - pytest_generate_tests hook that integrates - well with function arguments. - - -Changes between 0.9.2 and 1.0.0b1 -============================================= - -* introduced new "funcarg" setup method, - see doc/test/funcarg.txt - -* introduced plugin architecuture and many - new py.test plugins, see - doc/test/plugins.txt - -* teardown_method is now guaranteed to get - called after a test method has run. - -* new method: py.test.importorskip(mod,minversion) - will either import or call py.test.skip() - -* completely revised internal py.test architecture - -* new py.process.ForkedFunc object allowing to - fork execution of a function to a sub process - and getting a result back. - -XXX lots of things missing here XXX - -Changes between 0.9.1 and 0.9.2 -=============================== - -* refined installation and metadata, created new setup.py, - now based on setuptools/ez_setup (thanks to Ralf Schmitt - for his support). - -* improved the way of making py.* scripts available in - windows environments, they are now added to the - Scripts directory as ".cmd" files. - -* py.path.svnwc.status() now is more complete and - uses xml output from the 'svn' command if available - (Guido Wesdorp) - -* fix for py.path.svn* to work with svn 1.5 - (Chris Lamb) - -* fix path.relto(otherpath) method on windows to - use normcase for checking if a path is relative. - -* py.test's traceback is better parseable from editors - (follows the filenames:LINENO: MSG convention) - (thanks to Osmo Salomaa) - -* fix to javascript-generation, "py.test --runbrowser" - should work more reliably now - -* removed previously accidentally added - py.test.broken and py.test.notimplemented helpers. - -* there now is a py.__version__ attribute - -Changes between 0.9.0 and 0.9.1 -=============================== - -This is a fairly complete list of changes between 0.9 and 0.9.1, which can -serve as a reference for developers. - -* allowing + signs in py.path.svn urls [39106] -* fixed support for Failed exceptions without excinfo in py.test [39340] -* added support for killing processes for Windows (as well as platforms that - support os.kill) in py.misc.killproc [39655] -* added setup/teardown for generative tests to py.test [40702] -* added detection of FAILED TO LOAD MODULE to py.test [40703, 40738, 40739] -* fixed problem with calling .remove() on wcpaths of non-versioned files in - py.path [44248] -* fixed some import and inheritance issues in py.test [41480, 44648, 44655] -* fail to run greenlet tests when pypy is available, but without stackless - [45294] -* small fixes in rsession tests [45295] -* fixed issue with 2.5 type representations in py.test [45483, 45484] -* made that internal reporting issues displaying is done atomically in py.test - [45518] -* made that non-existing files are igored by the py.lookup script [45519] -* improved exception name creation in py.test [45535] -* made that less threads are used in execnet [merge in 45539] -* removed lock required for atomical reporting issue displaying in py.test - [45545] -* removed globals from execnet [45541, 45547] -* refactored cleanup mechanics, made that setDaemon is set to 1 to make atexit - get called in 2.5 (py.execnet) [45548] -* fixed bug in joining threads in py.execnet's servemain [45549] -* refactored py.test.rsession tests to not rely on exact output format anymore - [45646] -* using repr() on test outcome [45647] -* added 'Reason' classes for py.test.skip() [45648, 45649] -* killed some unnecessary sanity check in py.test.collect [45655] -* avoid using os.tmpfile() in py.io.fdcapture because on Windows it's only - usable by Administrators [45901] -* added support for locking and non-recursive commits to py.path.svnwc [45994] -* locking files in py.execnet to prevent CPython from segfaulting [46010] -* added export() method to py.path.svnurl -* fixed -d -x in py.test [47277] -* fixed argument concatenation problem in py.path.svnwc [49423] -* restore py.test behaviour that it exits with code 1 when there are failures - [49974] -* don't fail on html files that don't have an accompanying .txt file [50606] -* fixed 'utestconvert.py < input' [50645] -* small fix for code indentation in py.code.source [50755] -* fix _docgen.py documentation building [51285] -* improved checks for source representation of code blocks in py.test [51292] -* added support for passing authentication to py.path.svn* objects [52000, - 52001] -* removed sorted() call for py.apigen tests in favour of [].sort() to support - Python 2.3 [52481] --- a/doc/builtin.txt +++ b/doc/builtin.txt @@ -21,7 +21,7 @@ builtin function arguments ----------------------------------------------------- You can ask for available builtin or project-custom -:ref:`function arguments` by typing:: +:ref:`function arguments ` by typing:: $ py.test --funcargs pytestconfig --- a/doc/xdist.txt +++ b/doc/xdist.txt @@ -1,3 +1,6 @@ + +.. _`xdist`: + xdist: pytest distributed testing plugin =============================================================== @@ -38,9 +41,12 @@ a checkout of the `pytest-xdist reposito python setup.py develop + Usage examples --------------------- +.. _`xdistcpu`: + Speed up test runs by sending tests to multiple CPUs +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ @@ -69,6 +75,24 @@ If you prefix the --tx option value like then three subprocesses would be created and tests will be load-balanced across these three processes. +.. _looponfailing: + + +Running tests in looponfailing mode ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ + +For refactoring a project with a medium or large test suite +you can use the looponfailing mode, simply add the ``--f`` option:: + + py.test -f + +and py.test will run your tests, then wait for file changes and re-run the failing test set. Of course you can pass in more options to select tests or test files. File changes are detected by looking at the root directory - you can override this automatic default by an ini-file setting:: + + # content of a pytest.ini, setup.cfg or tox.ini file + [pytest] + looponfailroots = mypkg testdir + +This would lead to only looking for file changes in the respective directories, specified relatively to the ini-file's directory. Sending tests to remote SSH accounts +++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ --- a/doc/skipping.txt +++ b/doc/skipping.txt @@ -1,3 +1,5 @@ + +.. _`skip and xfail`: skip and xfail mechanisms ===================================================================== @@ -5,9 +7,9 @@ skip and xfail mechanisms You can skip or "xfail" test functions, either by marking functions through a decorator or by calling the ``pytest.skip|xfail`` helpers. 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 you expect your test to fail because there is an -implementation problem. Counting and listing *xfailing* tests separately -helps to maintain a list of implementation problems and you can provide -info such as a bug number or a URL to provide a human readable problem context. +implementation problem. py.test counts and lists *xfailing* tests separately +and you can provide info such as a bug number or a URL to provide a +human readable problem context. Usually detailed information about skipped/xfailed tests is not shown to avoid cluttering the output. You can use the ``-r`` option to @@ -16,6 +18,8 @@ test progress:: py.test -rxs # show extra info on skips and xfail tests +(See :ref:`how to change command line options defaults`) + .. _skipif: Skipping a single function --- a/doc/announce/release-0.9.2.txt +++ /dev/null @@ -1,27 +0,0 @@ -py lib 0.9.2: bugfix release -============================= - -Welcome to the 0.9.2 py lib and py.test release - -mainly fixing Windows issues, providing better -packaging and integration with setuptools. - -Here is a quick summary of what the py lib provides: - -* py.test: cross-project testing tool with many advanced features -* py.execnet: ad-hoc code distribution to SSH, Socket and local sub processes -* py.magic.greenlet: micro-threads on standard CPython ("stackless-light") -* py.path: path abstractions over local and subversion files -* rich documentation of py's exported API -* tested against Linux, Win32, OSX, works on python 2.3-2.6 - -See here for more information: - -Pypi pages: http://pypi.python.org/pypi/py/ - -Download/Install: http://codespeak.net/py/0.9.2/download.html - -Documentation/API: http://codespeak.net/py/0.9.2/index.html - -best and have fun, - -holger krekel --- a/doc/assert.txt +++ b/doc/assert.txt @@ -2,6 +2,8 @@ Writing and reporting of assertions in tests ============================================ +.. _`assert with the assert statement`: + assert with the ``assert`` statement --------------------------------------------------------- --- /dev/null +++ b/doc/contents.txt @@ -0,0 +1,33 @@ + +.. _toc: + +Table of Contents +======================== + +.. note:: + version 2.0 introduces :ref:`pytest as the main Python import name ` + +.. toctree:: + :maxdepth: 2 + + overview + apiref + plugins + example/index + talks + develop + announce/index + +.. toctree:: + :hidden: + + changelog.txt + naming20.txt + example/attic + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` --- a/doc/announce/index.txt +++ b/doc/announce/index.txt @@ -7,20 +7,3 @@ Release announcements release-2.0.0 -.. toctree:: - :hidden: - - release-1.3.4 - release-1.3.3 - release-1.3.2 - release-1.3.1 - release-1.3.0 - release-1.2.1 - release-1.2.0 - release-1.1.1 - release-1.1.0 - release-1.0.2 - release-1.0.1 - release-1.0.0 - release-0.9.2 - release-0.9.0 --- a/doc/conf.py +++ b/doc/conf.py @@ -140,6 +140,8 @@ html_sidebars = {'index': 'indexsidebar. # Additional templates that should be rendered to pages, maps page names to # template names. #html_additional_pages = {} +#html_additional_pages = {'index': 'index.html'} + # If false, no module index is generated. #html_domain_indices = True --- a/doc/announce/release-1.3.0.txt +++ /dev/null @@ -1,580 +0,0 @@ -py.test/pylib 1.3.0: new options, per-plugin hooks, fixes ... -=========================================================================== - -The 1.3.0 release introduces new options, bug fixes and improved compatibility -with Python3 and Jython-2.5.1 on Windows. If you already use py-1.2 chances -are you can use py-1.3.0. See the below CHANGELOG for more details and -http://pylib.org/install.html for installation instructions. - -py.test is an advanced automated testing tool working with Python2, -Python3, Jython and PyPy versions on all major operating systems. It -offers a no-boilerplate testing approach and has inspired other testing -tools and enhancements in the standard Python library for more than five -years. It has a simple and extensive plugin architecture, configurable -reporting and provides unique ways to make it fit to your testing -process and needs. - -See http://pytest.org for more info. - -cheers and have fun, - -holger krekel - -Changes between 1.2.1 and 1.3.0 -================================================== - -- deprecate --report option in favour of a new shorter and easier to - remember -r option: it takes a string argument consisting of any - combination of 'xfsX' characters. They relate to the single chars - you see during the dotted progress printing and will print an extra line - per test at the end of the test run. This extra line indicates the exact - position or test ID that you directly paste to the py.test cmdline in order - to re-run a particular test. - -- allow external plugins to register new hooks via the new - pytest_addhooks(pluginmanager) hook. The new release of - the pytest-xdist plugin for distributed and looponfailing - testing requires this feature. - -- add a new pytest_ignore_collect(path, config) hook to allow projects and - plugins to define exclusion behaviour for their directory structure - - for example you may define in a conftest.py this method:: - - def pytest_ignore_collect(path): - return path.check(link=1) - - to prevent even collection of any tests in symlinked dirs. - -- new pytest_pycollect_makemodule(path, parent) hook for - allowing customization of the Module collection object for a - matching test module. - -- extend and refine xfail mechanism:: - - @py.test.mark.xfail(run=False) do not run the decorated test - @py.test.mark.xfail(reason="...") prints the reason string in xfail summaries - - specifiying ``--runxfail`` on command line ignores xfail markers to show - you the underlying traceback. - -- expose (previously internal) commonly useful methods: - py.io.get_terminal_with() -> return terminal width - py.io.ansi_print(...) -> print colored/bold text on linux/win32 - py.io.saferepr(obj) -> return limited representation string - -- expose test outcome related exceptions as py.test.skip.Exception, - py.test.raises.Exception etc., useful mostly for plugins - doing special outcome interpretation/tweaking - -- (issue85) fix junitxml plugin to handle tests with non-ascii output - -- fix/refine python3 compatibility (thanks Benjamin Peterson) - -- fixes for making the jython/win32 combination work, note however: - jython2.5.1/win32 does not provide a command line launcher, see - http://bugs.jython.org/issue1491 . See pylib install documentation - for how to work around. - -- fixes for handling of unicode exception values and unprintable objects - -- (issue87) fix unboundlocal error in assertionold code - -- (issue86) improve documentation for looponfailing - -- refine IO capturing: stdin-redirect pseudo-file now has a NOP close() method - -- ship distribute_setup.py version 0.6.10 - -- added links to the new capturelog and coverage plugins - - -Changes between 1.2.1 and 1.2.0 -===================================== - -- refined usage and options for "py.cleanup":: - - py.cleanup # remove "*.pyc" and "*$py.class" (jython) files - py.cleanup -e .swp -e .cache # also remove files with these extensions - py.cleanup -s # remove "build" and "dist" directory next to setup.py files - py.cleanup -d # also remove empty directories - py.cleanup -a # synonym for "-s -d -e 'pip-log.txt'" - py.cleanup -n # dry run, only show what would be removed - -- add a new option "py.test --funcargs" which shows available funcargs - and their help strings (docstrings on their respective factory function) - for a given test path - -- display a short and concise traceback if a funcarg lookup fails - -- early-load "conftest.py" files in non-dot first-level sub directories. - allows to conveniently keep and access test-related options in a ``test`` - subdir and still add command line options. - -- fix issue67: new super-short traceback-printing option: "--tb=line" will print a single line for each failing (python) test indicating its filename, lineno and the failure value - -- fix issue78: always call python-level teardown functions even if the - according setup failed. This includes refinements for calling setup_module/class functions - which will now only be called once instead of the previous behaviour where they'd be called - multiple times if they raise an exception (including a Skipped exception). Any exception - will be re-corded and associated with all tests in the according module/class scope. - -- fix issue63: assume <40 columns to be a bogus terminal width, default to 80 - -- fix pdb debugging to be in the correct frame on raises-related errors - -- update apipkg.py to fix an issue where recursive imports might - unnecessarily break importing - -- fix plugin links - -Changes between 1.2 and 1.1.1 -===================================== - -- moved dist/looponfailing from py.test core into a new - separately released pytest-xdist plugin. - -- new junitxml plugin: --junitxml=path will generate a junit style xml file - which is processable e.g. by the Hudson CI system. - -- new option: --genscript=path will generate a standalone py.test script - which will not need any libraries installed. thanks to Ralf Schmitt. - -- new option: --ignore will prevent specified path from collection. - Can be specified multiple times. - -- new option: --confcutdir=dir will make py.test only consider conftest - files that are relative to the specified dir. - -- new funcarg: "pytestconfig" is the pytest config object for access - to command line args and can now be easily used in a test. - -- install 'py.test' and `py.which` with a ``-$VERSION`` suffix to - disambiguate between Python3, python2.X, Jython and PyPy installed versions. - -- new "pytestconfig" funcarg allows access to test config object - -- new "pytest_report_header" hook can return additional lines - to be displayed at the header of a test run. - -- (experimental) allow "py.test path::name1::name2::..." for pointing - to a test within a test collection directly. This might eventually - evolve as a full substitute to "-k" specifications. - -- streamlined plugin loading: order is now as documented in - customize.html: setuptools, ENV, commandline, conftest. - also setuptools entry point names are turned to canonical namees ("pytest_*") - -- automatically skip tests that need 'capfd' but have no os.dup - -- allow pytest_generate_tests to be defined in classes as well - -- deprecate usage of 'disabled' attribute in favour of pytestmark -- deprecate definition of Directory, Module, Class and Function nodes - in conftest.py files. Use pytest collect hooks instead. - -- collection/item node specific runtest/collect hooks are only called exactly - on matching conftest.py files, i.e. ones which are exactly below - the filesystem path of an item - -- change: the first pytest_collect_directory hook to return something - will now prevent further hooks to be called. - -- change: figleaf plugin now requires --figleaf to run. Also - change its long command line options to be a bit shorter (see py.test -h). - -- change: pytest doctest plugin is now enabled by default and has a - new option --doctest-glob to set a pattern for file matches. - -- change: remove internal py._* helper vars, only keep py._pydir - -- robustify capturing to survive if custom pytest_runtest_setup - code failed and prevented the capturing setup code from running. - -- make py.test.* helpers provided by default plugins visible early - - works transparently both for pydoc and for interactive sessions - which will regularly see e.g. py.test.mark and py.test.importorskip. - -- simplify internal plugin manager machinery -- simplify internal collection tree by introducing a RootCollector node - -- fix assert reinterpreation that sees a call containing "keyword=..." - -- fix issue66: invoke pytest_sessionstart and pytest_sessionfinish - hooks on slaves during dist-testing, report module/session teardown - hooks correctly. - -- fix issue65: properly handle dist-testing if no - execnet/py lib installed remotely. - -- skip some install-tests if no execnet is available - -- fix docs, fix internal bin/ script generation - - -Changes between 1.1.1 and 1.1.0 -===================================== - -- introduce automatic plugin registration via 'pytest11' - entrypoints via setuptools' pkg_resources.iter_entry_points - -- fix py.test dist-testing to work with execnet >= 1.0.0b4 - -- re-introduce py.test.cmdline.main() for better backward compatibility - -- svn paths: fix a bug with path.check(versioned=True) for svn paths, - allow '%' in svn paths, make svnwc.update() default to interactive mode - like in 1.0.x and add svnwc.update(interactive=False) to inhibit interaction. - -- refine distributed tarball to contain test and no pyc files - -- try harder to have deprecation warnings for py.compat.* accesses - report a correct location - -Changes between 1.1.0 and 1.0.2 -===================================== - -* adjust and improve docs - -* remove py.rest tool and internal namespace - it was - never really advertised and can still be used with - the old release if needed. If there is interest - it could be revived into its own tool i guess. - -* fix issue48 and issue59: raise an Error if the module - from an imported test file does not seem to come from - the filepath - avoids "same-name" confusion that has - been reported repeatedly - -* merged Ronny's nose-compatibility hacks: now - nose-style setup_module() and setup() functions are - supported - -* introduce generalized py.test.mark function marking - -* reshuffle / refine command line grouping - -* deprecate parser.addgroup in favour of getgroup which creates option group - -* add --report command line option that allows to control showing of skipped/xfailed sections - -* generalized skipping: a new way to mark python functions with skipif or xfail - at function, class and modules level based on platform or sys-module attributes. - -* extend py.test.mark decorator to allow for positional args - -* introduce and test "py.cleanup -d" to remove empty directories - -* fix issue #59 - robustify unittest test collection - -* make bpython/help interaction work by adding an __all__ attribute - to ApiModule, cleanup initpkg - -* use MIT license for pylib, add some contributors - -* remove py.execnet code and substitute all usages with 'execnet' proper - -* fix issue50 - cached_setup now caches more to expectations - for test functions with multiple arguments. - -* merge Jarko's fixes, issue #45 and #46 - -* add the ability to specify a path for py.lookup to search in - -* fix a funcarg cached_setup bug probably only occuring - in distributed testing and "module" scope with teardown. - -* many fixes and changes for making the code base python3 compatible, - many thanks to Benjamin Peterson for helping with this. - -* consolidate builtins implementation to be compatible with >=2.3, - add helpers to ease keeping 2 and 3k compatible code - -* deprecate py.compat.doctest|subprocess|textwrap|optparse - -* deprecate py.magic.autopath, remove py/magic directory - -* move pytest assertion handling to py/code and a pytest_assertion - plugin, add "--no-assert" option, deprecate py.magic namespaces - in favour of (less) py.code ones. - -* consolidate and cleanup py/code classes and files - -* cleanup py/misc, move tests to bin-for-dist - -* introduce delattr/delitem/delenv methods to py.test's monkeypatch funcarg - -* consolidate py.log implementation, remove old approach. - -* introduce py.io.TextIO and py.io.BytesIO for distinguishing between - text/unicode and byte-streams (uses underlying standard lib io.* - if available) - -* make py.unittest_convert helper script available which converts "unittest.py" - style files into the simpler assert/direct-test-classes py.test/nosetests - style. The script was written by Laura Creighton. - -* simplified internal localpath implementation - -Changes between 1.0.1 and 1.0.2 -===================================== - -* fixing packaging issues, triggered by fedora redhat packaging, - also added doc, examples and contrib dirs to the tarball. - -* added a documentation link to the new django plugin. - -Changes between 1.0.0 and 1.0.1 -===================================== - -* added a 'pytest_nose' plugin which handles nose.SkipTest, - nose-style function/method/generator setup/teardown and - tries to report functions correctly. - -* capturing of unicode writes or encoded strings to sys.stdout/err - work better, also terminalwriting was adapted and somewhat - unified between windows and linux. - -* improved documentation layout and content a lot - -* added a "--help-config" option to show conftest.py / ENV-var names for - all longopt cmdline options, and some special conftest.py variables. - renamed 'conf_capture' conftest setting to 'option_capture' accordingly. - -* fix issue #27: better reporting on non-collectable items given on commandline - (e.g. pyc files) - -* fix issue #33: added --version flag (thanks Benjamin Peterson) - -* fix issue #32: adding support for "incomplete" paths to wcpath.status() - -* "Test" prefixed classes are *not* collected by default anymore if they - have an __init__ method - -* monkeypatch setenv() now accepts a "prepend" parameter - -* improved reporting of collection error tracebacks - -* simplified multicall mechanism and plugin architecture, - renamed some internal methods and argnames - -Changes between 1.0.0b9 and 1.0.0 -===================================== - -* more terse reporting try to show filesystem path relatively to current dir -* improve xfail output a bit - -Changes between 1.0.0b8 and 1.0.0b9 -===================================== - -* cleanly handle and report final teardown of test setup - -* fix svn-1.6 compat issue with py.path.svnwc().versioned() - (thanks Wouter Vanden Hove) - -* setup/teardown or collection problems now show as ERRORs - or with big "E"'s in the progress lines. they are reported - and counted separately. - -* dist-testing: properly handle test items that get locally - collected but cannot be collected on the remote side - often - due to platform/dependency reasons - -* simplified py.test.mark API - see keyword plugin documentation - -* integrate better with logging: capturing now by default captures - test functions and their immediate setup/teardown in a single stream - -* capsys and capfd funcargs now have a readouterr() and a close() method - (underlyingly py.io.StdCapture/FD objects are used which grew a - readouterr() method as well to return snapshots of captured out/err) - -* make assert-reinterpretation work better with comparisons not - returning bools (reported with numpy from thanks maciej fijalkowski) - -* reworked per-test output capturing into the pytest_iocapture.py plugin - and thus removed capturing code from config object - -* item.repr_failure(excinfo) instead of item.repr_failure(excinfo, outerr) - - -Changes between 1.0.0b7 and 1.0.0b8 -===================================== - -* pytest_unittest-plugin is now enabled by default - -* introduced pytest_keyboardinterrupt hook and - refined pytest_sessionfinish hooked, added tests. - -* workaround a buggy logging module interaction ("closing already closed - files"). Thanks to Sridhar Ratnakumar for triggering. - -* if plugins use "py.test.importorskip" for importing - a dependency only a warning will be issued instead - of exiting the testing process. - -* many improvements to docs: - - refined funcargs doc , use the term "factory" instead of "provider" - - added a new talk/tutorial doc page - - better download page - - better plugin docstrings - - added new plugins page and automatic doc generation script - -* fixed teardown problem related to partially failing funcarg setups - (thanks MrTopf for reporting), "pytest_runtest_teardown" is now - always invoked even if the "pytest_runtest_setup" failed. - -* tweaked doctest output for docstrings in py modules, - thanks Radomir. - -Changes between 1.0.0b3 and 1.0.0b7 -============================================= - -* renamed py.test.xfail back to py.test.mark.xfail to avoid - two ways to decorate for xfail - -* re-added py.test.mark decorator for setting keywords on functions - (it was actually documented so removing it was not nice) - -* remove scope-argument from request.addfinalizer() because - request.cached_setup has the scope arg. TOOWTDI. - -* perform setup finalization before reporting failures - -* apply modified patches from Andreas Kloeckner to allow - test functions to have no func_code (#22) and to make - "-k" and function keywords work (#20) - -* apply patch from Daniel Peolzleithner (issue #23) - -* resolve issue #18, multiprocessing.Manager() and - redirection clash - -* make __name__ == "__channelexec__" for remote_exec code - -Changes between 1.0.0b1 and 1.0.0b3 -============================================= - -* plugin classes are removed: one now defines - hooks directly in conftest.py or global pytest_*.py - files. - -* added new pytest_namespace(config) hook that allows - to inject helpers directly to the py.test.* namespace. - -* documented and refined many hooks - -* added new style of generative tests via - pytest_generate_tests hook that integrates - well with function arguments. - - -Changes between 0.9.2 and 1.0.0b1 -============================================= - -* introduced new "funcarg" setup method, - see doc/test/funcarg.txt - -* introduced plugin architecuture and many - new py.test plugins, see - doc/test/plugins.txt - -* teardown_method is now guaranteed to get - called after a test method has run. - -* new method: py.test.importorskip(mod,minversion) - will either import or call py.test.skip() - -* completely revised internal py.test architecture - -* new py.process.ForkedFunc object allowing to - fork execution of a function to a sub process - and getting a result back. - -XXX lots of things missing here XXX - -Changes between 0.9.1 and 0.9.2 -=============================== - -* refined installation and metadata, created new setup.py, - now based on setuptools/ez_setup (thanks to Ralf Schmitt - for his support). - -* improved the way of making py.* scripts available in - windows environments, they are now added to the - Scripts directory as ".cmd" files. - -* py.path.svnwc.status() now is more complete and - uses xml output from the 'svn' command if available - (Guido Wesdorp) - -* fix for py.path.svn* to work with svn 1.5 - (Chris Lamb) - -* fix path.relto(otherpath) method on windows to - use normcase for checking if a path is relative. - -* py.test's traceback is better parseable from editors - (follows the filenames:LINENO: MSG convention) - (thanks to Osmo Salomaa) - -* fix to javascript-generation, "py.test --runbrowser" - should work more reliably now - -* removed previously accidentally added - py.test.broken and py.test.notimplemented helpers. - -* there now is a py.__version__ attribute - -Changes between 0.9.0 and 0.9.1 -=============================== - -This is a fairly complete list of changes between 0.9 and 0.9.1, which can -serve as a reference for developers. - -* allowing + signs in py.path.svn urls [39106] -* fixed support for Failed exceptions without excinfo in py.test [39340] -* added support for killing processes for Windows (as well as platforms that - support os.kill) in py.misc.killproc [39655] -* added setup/teardown for generative tests to py.test [40702] -* added detection of FAILED TO LOAD MODULE to py.test [40703, 40738, 40739] -* fixed problem with calling .remove() on wcpaths of non-versioned files in - py.path [44248] -* fixed some import and inheritance issues in py.test [41480, 44648, 44655] -* fail to run greenlet tests when pypy is available, but without stackless - [45294] -* small fixes in rsession tests [45295] -* fixed issue with 2.5 type representations in py.test [45483, 45484] -* made that internal reporting issues displaying is done atomically in py.test - [45518] -* made that non-existing files are igored by the py.lookup script [45519] -* improved exception name creation in py.test [45535] -* made that less threads are used in execnet [merge in 45539] -* removed lock required for atomical reporting issue displaying in py.test - [45545] -* removed globals from execnet [45541, 45547] -* refactored cleanup mechanics, made that setDaemon is set to 1 to make atexit - get called in 2.5 (py.execnet) [45548] -* fixed bug in joining threads in py.execnet's servemain [45549] -* refactored py.test.rsession tests to not rely on exact output format anymore - [45646] -* using repr() on test outcome [45647] -* added 'Reason' classes for py.test.skip() [45648, 45649] -* killed some unnecessary sanity check in py.test.collect [45655] -* avoid using os.tmpfile() in py.io.fdcapture because on Windows it's only - usable by Administrators [45901] -* added support for locking and non-recursive commits to py.path.svnwc [45994] -* locking files in py.execnet to prevent CPython from segfaulting [46010] -* added export() method to py.path.svnurl -* fixed -d -x in py.test [47277] -* fixed argument concatenation problem in py.path.svnwc [49423] -* restore py.test behaviour that it exits with code 1 when there are failures - [49974] -* don't fail on html files that don't have an accompanying .txt file [50606] -* fixed 'utestconvert.py < input' [50645] -* small fix for code indentation in py.code.source [50755] -* fix _docgen.py documentation building [51285] -* improved checks for source representation of code blocks in py.test [51292] -* added support for passing authentication to py.path.svn* objects [52000, - 52001] -* removed sorted() call for py.apigen tests in favour of [].sort() to support - Python 2.3 [52481] --- a/doc/announce/release-1.1.1.txt +++ /dev/null @@ -1,48 +0,0 @@ -py.test/pylib 1.1.1: bugfix release, setuptools plugin registration --------------------------------------------------------------------------------- - -This is a compatibility fixing release of pylib/py.test to work -better with previous 1.0.x test code bases. It also contains fixes -and changes to work with `execnet>=1.0.0`_ to provide distributed -testing and looponfailing testing modes. py-1.1.1 also introduces -a new mechanism for registering plugins via setuptools. - -What is pylib/py.test? ------------------------ - -py.test is an advanced automated testing tool working with -Python2, Python3 and Jython versions on all major operating -systems. It has an extensive plugin architecture and can run many -existing common Python test suites without modification. Moreover, -it offers some unique features not found in other -testing tools. See http://pytest.org for more info. - -The pylib also contains a localpath and svnpath implementation -and some developer-oriented command line tools. See -http://pylib.org for more info. - -thanks to all who helped and gave feedback, -have fun, - -holger (http://twitter.com/hpk42) - -.. _`execnet>=1.0.0`: http://codespeak.net/execnet - -Changes between 1.1.1 and 1.1.0 -===================================== - -- introduce automatic plugin registration via 'pytest11' - entrypoints via setuptools' pkg_resources.iter_entry_points - -- fix py.test dist-testing to work with execnet >= 1.0.0b4 - -- re-introduce py.test.cmdline.main() for better backward compatibility - -- svn paths: fix a bug with path.check(versioned=True) for svn paths, - allow '%' in svn paths, make svnwc.update() default to interactive mode - like in 1.0.x and add svnwc.update(interactive=False) to inhibit interaction. - -- refine distributed tarball to contain test and no pyc files - -- try harder to have deprecation warnings for py.compat.* accesses - report a correct location From commits-noreply at bitbucket.org Thu Nov 25 14:46:36 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 07:46:36 -0600 (CST) Subject: [py-svn] py commit 8d9ddabb8c3f: fix assertion source finding raising some more Message-ID: <20101125134636.CC6BF1E12AF@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project py # URL http://bitbucket.org/hpk42/py/overview # User holger krekel # Date 1290692265 -3600 # Node ID 8d9ddabb8c3f60367341ce56344d60b9685a9013 # Parent ceb0875f32dcd662c4ca8991b7bb6a4363bcb4ee fix assertion source finding raising some more --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ def main(): name='py', description='library with cross-python path, ini-parsing, io, code, log facilities', long_description = open('README.txt').read(), - version='1.4.0a3', + version='1.4.0a5', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/testing/code/test_assertion.py +++ b/testing/code/test_assertion.py @@ -273,3 +273,32 @@ def test_assert_long_source_2(): s = str(e) assert 're-run' not in s assert 'somet text' in s + +def test_assert_raise_alias(testdir): + testdir.makepyfile(""" + import sys + EX = AssertionError + def test_hello(): + raise EX("hello" + "multi" + "line") + """) + result = testdir.runpytest() + result.stdout.fnmatch_lines([ + "*def test_hello*", + "*raise EX*", + "*1 failed*", + ]) + + +def test_assert_raise_subclass(): + class SomeEx(AssertionError): + def __init__(self, *args): + super(SomeEx, self).__init__() + try: + raise SomeEx("hello") + except AssertionError: + s = str(exvalue()) + assert 're-run' not in s + assert 'could not determine' in s + --- a/py/_code/assertion.py +++ b/py/_code/assertion.py @@ -65,18 +65,22 @@ class AssertionError(BuiltinAssertionErr try: source = f.code.fullsource if source is not None: - source = source.getstatement(f.lineno, assertion=True) - source = str(source.deindent()).strip() + try: + source = source.getstatement(f.lineno, assertion=True) + except IndexError: + source = None + else: + source = str(source.deindent()).strip() except py.error.ENOENT: source = None # this can also occur during reinterpretation, when the # co_filename is set to "". if source: self.msg = reinterpret(source, f, should_fail=True) - if not self.args: - self.args = (self.msg,) else: - self.msg = None + self.msg = "" + if not self.args: + self.args = (self.msg,) if sys.version_info > (3, 0): AssertionError.__module__ = "builtins" --- a/py/_code/source.py +++ b/py/_code/source.py @@ -118,9 +118,12 @@ class Source(object): from codeop import compile_command for start in range(lineno, -1, -1): if assertion: - if "assert" not in self.lines[start]: + line = self.lines[start] + # the following lines are not fully tested, change with care + if 'super' in line and 'self' in line and '__init__' in line: + raise IndexError("likely a subclass") + if "assert" not in line and "raise" not in line: continue - trylines = self.lines[start:lineno+1] # quick hack to indent the source and get it as a string in one go trylines.insert(0, 'def xxx():') --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = '1.4.0a3' +__version__ = '1.4.0a5' from py import _apipkg From commits-noreply at bitbucket.org Thu Nov 25 15:55:53 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 08:55:53 -0600 (CST) Subject: [py-svn] pytest commit 64fe9da75233: adding anto's projects Message-ID: <20101125145553.A24B56C1313@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290686401 -3600 # Node ID 64fe9da752332c8b36afdbc439e1cf693aa54393 # Parent 8ee073e2945d44af241148a947144965fcbbcda7 adding anto's projects --- a/doc/projects.txt +++ b/doc/projects.txt @@ -1,6 +1,6 @@ .. _projects: -Project Examples +Project examples ========================== Here are some examples of projects using py.test: @@ -11,6 +11,7 @@ Here are some examples of projects using * `PIDA `_ framework for integrated development * `Fom `_ a fluid object mapper for FluidDB * `six `_ Python 2 and 3 compatibility utilities +* `pdb++ `_ a fancier version of PDB * `The Translate Toolkit `_ for localization and conversion * `execnet `_ rapid multi-Python deployment * `py-s3fuse `_ Amazon S3 FUSE based filesystem @@ -20,6 +21,7 @@ Here are some examples of projects using * `pydataportability `_ libraries for managing the open web * `XIST `_ extensible HTML/XML generator * `tiddlyweb `_ optionally headless, extensible RESTful datastore +* `fancycompleter `_ for colorful tab-completion * `Paludis `_ tools for Gentoo Paludis package manager * `Gerald `_ schema comparison tool * `abjad `_ Python API for Formalized Score control @@ -28,8 +30,8 @@ Here are some examples of projects using * `kss plugin timer `_ * many more ... (please send notes via the :ref:`contact`) -Some examples of organisations using py.test ----------------------------------------------- +Some organisations using py.test +----------------------------------- * `Square Kilometre Array `_ * `Tandberg `_ --- a/doc/_templates/indexsidebar.html +++ b/doc/_templates/indexsidebar.html @@ -7,8 +7,8 @@ released versions in the Python Package Index.

{% else %} -

Current: {{ version }} -[Changes]

+

{{ version }} release +[Changelog]

pytest on PyPI

--- a/doc/assert.txt +++ b/doc/assert.txt @@ -130,6 +130,8 @@ Special comparisons are done for a numbe * comparing long sequences: first failing indices * comparing dicts: different entries +See the :ref:`reporting demo ` for examples. + .. Defining your own comparison ---------------------------------------------- From commits-noreply at bitbucket.org Thu Nov 25 15:55:53 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 08:55:53 -0600 (CST) Subject: [py-svn] pytest commit 8ee073e2945d: fix some more trial/unittest related bits, particularly allow todo/skip items, Message-ID: <20101125145553.925356C1070@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290696539 -3600 # Node ID 8ee073e2945d44af241148a947144965fcbbcda7 # Parent 4853ee1ad7d54469c45e9158b05d8320df0669b0 fix some more trial/unittest related bits, particularly allow todo/skip items, now we can run a large fraction of twisted's own test suite, mostly not those that depend on the exact Failure() semantics (e.g. frame objects not being around after gc.collect() but py.test kills them only slightly later anyway) --- a/_pytest/unittest.py +++ b/_pytest/unittest.py @@ -37,6 +37,12 @@ class UnitTestCase(pytest.Class): class TestCaseFunction(pytest.Function): _excinfo = None + def __init__(self, name, parent): + super(TestCaseFunction, self).__init__(name, parent) + if hasattr(self._obj, 'todo'): + getattr(self._obj, 'im_func', self._obj).xfail = \ + pytest.mark.xfail(reason=str(self._obj.todo)) + def setup(self): self._testcase = self.parent.obj(self.name) self._obj = getattr(self._testcase, self.name) @@ -77,6 +83,18 @@ class TestCaseFunction(pytest.Function): self._addexcinfo(rawexcinfo) def addFailure(self, testcase, rawexcinfo): self._addexcinfo(rawexcinfo) + def addSkip(self, testcase, reason): + try: + pytest.skip(reason) + except pytest.skip.Exception: + self._addexcinfo(sys.exc_info()) + def addExpectedFailure(self, testcase, rawexcinfo, reason): + try: + pytest.xfail(str(reason)) + except pytest.xfail.Exception: + self._addexcinfo(sys.exc_info()) + def addUnexpectedSuccess(self, testcase, reason): + pass def addSuccess(self, testcase): pass def stopTest(self, testcase): --- a/testing/test_unittest.py +++ b/testing/test_unittest.py @@ -251,6 +251,17 @@ class TestTrialUnittest: assert 0 def test_hello4(self): pytest.xfail("i2wanto") + def test_trial_skip(self): + pass + test_trial_skip.skip = "trialselfskip" + + def test_trial_todo(self): + assert 0 + test_trial_todo.todo = "mytodo" + + def test_trial_todo_success(self): + pass + test_trial_todo_success.todo = "mytodo" class TC2(unittest.TestCase): def setup_class(cls): @@ -261,15 +272,16 @@ class TestTrialUnittest: result = testdir.runpytest("-rxs") assert result.ret == 0 result.stdout.fnmatch_lines_random([ + "*XFAIL*test_trial_todo*", + "*trialselfskip*", "*skip_in_setup_class*", "*iwanto*", "*i2wanto*", "*sys.version_info*", "*skip_in_method*", - "*3 skipped*2 xfail*", + "*4 skipped*3 xfail*1 xpass*", ]) - @pytest.mark.xfail(reason="fijal needs add checks") def test_trial_error(self, testdir): testdir.makepyfile(""" from twisted.trial.unittest import TestCase @@ -307,7 +319,18 @@ class TestTrialUnittest: # will crash both at test time and at teardown """) result = testdir.runpytest() - assert 0 + result.stdout.fnmatch_lines([ + "*ERRORS*", + "*DelayedCalls*", + "*test_four*", + "*NameError*crash*", + "*test_one*", + "*NameError*crash*", + "*test_three*", + "*DelayedCalls*", + "*test_two*", + "*crash*", + ]) def test_trial_pdb(self, testdir): p = testdir.makepyfile(""" From commits-noreply at bitbucket.org Thu Nov 25 16:36:39 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 09:36:39 -0600 (CST) Subject: [py-svn] pytest commit 83cd00f43c89: some small pre-release updates Message-ID: <20101125153639.496326C1064@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290699385 -3600 # Node ID 83cd00f43c899b8754faccf03dd3fafd33992729 # Parent 64fe9da752332c8b36afdbc439e1cf693aa54393 some small pre-release updates --- a/pytest.py +++ b/pytest.py @@ -1,7 +1,7 @@ """ unit and functional testing with Python. """ -__version__ = '2.0.0.dev39' +__version__ = '2.0.0.dev42' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev39', + version='2.0.0.dev42', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/MANIFEST.in +++ b/MANIFEST.in @@ -3,17 +3,5 @@ include README.txt include setup.py include distribute_setup.py include LICENSE -#include conftest.py graft doc graft testing -exclude doc/_build -include testing -#exclude *.orig -#exclude *.orig -exclude *.rej -exclude .hgignore -exclude *.pyc -#recursive-exclude testing *.pyc *.orig *.rej *$py.class -#prune .pyc -#prune .svn -#prune .hg --- a/doc/projects.txt +++ b/doc/projects.txt @@ -11,11 +11,14 @@ Here are some examples of projects using * `PIDA `_ framework for integrated development * `Fom `_ a fluid object mapper for FluidDB * `six `_ Python 2 and 3 compatibility utilities -* `pdb++ `_ a fancier version of PDB +* `pediapress `_ MediaWiki articles +* `mwlib `_ mediawiki parser and utility library * `The Translate Toolkit `_ for localization and conversion * `execnet `_ rapid multi-Python deployment +* `bbfreeze `_ create standalone executables from Python scripts +* `pdb++ `_ a fancier version of PDB * `py-s3fuse `_ Amazon S3 FUSE based filesystem -* `Circuits `_ lightweight Event Driven Framework +* `Circuits `_ lightweight Event Driven Framework * `pygtk-helpers `_ easy interaction with PyGTK * `QuantumCore `_ statusmessage and repoze openid plugin * `pydataportability `_ libraries for managing the open web @@ -39,4 +42,4 @@ Some organisations using py.test * `Open End `_ * `Laboraratory of Bioinformatics `_ * `merlinux `_ -* many more ... (please send a note via the :ref:`contact`) +* many more ... (please be so kind to send a note via :ref:`contact`) From commits-noreply at bitbucket.org Thu Nov 25 16:39:01 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 09:39:01 -0600 (CST) Subject: [py-svn] py commit d8a7846892df: release prep Message-ID: <20101125153901.6E7B01E0404@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project py # URL http://bitbucket.org/hpk42/py/overview # User holger krekel # Date 1290699394 -3600 # Node ID d8a7846892df17a93ad268f8c86c0c7e5f3fdb78 # Parent 8d9ddabb8c3f60367341ce56344d60b9685a9013 release prep --- a/doc/install.txt +++ b/doc/install.txt @@ -1,6 +1,6 @@ -.. _`pylib`: -.. _`index page`: http://pypi.python.org/pypi/pylib/ +.. _`py`: +.. _`index page`: http://pypi.python.org/pypi/py/ installation info in a nutshell =================================================== @@ -41,14 +41,14 @@ Working from version control or a tarbal To follow development or start experiments, checkout the complete code and documentation source with mercurial_:: - hg clone https://bitbucket.org/hpk42/pylib + hg clone https://bitbucket.org/hpk42/py Development takes place on the 'trunk' branch. You can also go to the python package index and download and unpack a TAR file:: - http://pypi.python.org/pypi/pylib/ + http://pypi.python.org/pypi/py/ activating a checkout with setuptools -------------------------------------------- @@ -76,7 +76,7 @@ Contact and Communication points - `bitbucket issue tracker`_ use this bitbucket issue tracker to report bugs or request features. -.. _`bitbucket issue tracker`: http://bitbucket.org/hpk42/pylib/issues/ +.. _`bitbucket issue tracker`: http://bitbucket.org/hpk42/py/issues/ .. _codespeak: http://codespeak.net/ .. _`py-dev`: --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = '1.4.0a5' +__version__ = '1.4.0.a7' from py import _apipkg --- a/doc/index.txt +++ b/doc/index.txt @@ -10,9 +10,9 @@ Welcome to py's documentation! .. note:: - py.test now comes from the `pytest distribtion`_ + py.test now comes from the `pytest distribution`_ -.. _`pytest distribution`: http://pypi.python.org/pypi/pytest +.. _`pytest distribution`: http://pytest.org Contents: --- a/MANIFEST.in +++ b/MANIFEST.in @@ -6,11 +6,3 @@ include LICENSE include conftest.py graft doc graft testing -#exclude *.orig -#exclude *.orig -#exclude *.rej -#exclude *.pyc -#recursive-exclude testing *.pyc *.orig *.rej *$py.class -#prune .pyc -#prune .svn -#prune .hg --- a/py/_path/svnwc.py +++ b/py/_path/svnwc.py @@ -138,7 +138,7 @@ class SvnPathBase(common.PathBase): def new(self, **kw): """ create a modified version of this path. A 'rev' argument indicates a new revision. - the following keyword arguments modify various path parts: + the following keyword arguments modify various path parts:: http://host.com/repo/path/file.ext |-----------------------| dirname --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ def main(): name='py', description='library with cross-python path, ini-parsing, io, code, log facilities', long_description = open('README.txt').read(), - version='1.4.0a5', + version='1.4.0.a7', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/README.txt +++ b/README.txt @@ -7,6 +7,9 @@ the following tools and modules: * py.code: dynamic code generation and introspection * py.path: uniform local and svn path objects +NOTE: prior to the 1.4 release this distribution used to +contain py.test which is now its own package, see http://pytest.org + For questions and more information please visit http://pylib.org Bugs and issues: http://bitbucket.org/hpk42/pylib/issues/ From commits-noreply at bitbucket.org Thu Nov 25 16:41:29 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 09:41:29 -0600 (CST) Subject: [py-svn] pytest-xdist commit 5f81d2757754: rel preps Message-ID: <20101125154129.F07506C1064@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1290699676 -3600 # Node ID 5f81d275775449132c497cef31d72bc5e1f7eeb8 # Parent 1edfc8649ffcaa06a9d05451361033bb6b2dcbcc rel preps --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.5a10', + version='1.5a11', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=open('README.txt').read(), license='GPLv2 or later', @@ -13,7 +13,7 @@ setup( packages = ['xdist'], entry_points = {'pytest11': ['xdist = xdist.plugin'],}, zip_safe=False, - install_requires = ['execnet>=1.0.7', 'pytest>1.9.9'], + install_requires = ['execnet>=1.0.8', 'pytest>1.9.9'], classifiers=[ 'Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5a10' +__version__ = '1.5a11' From commits-noreply at bitbucket.org Thu Nov 25 17:09:20 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 10:09:20 -0600 (CST) Subject: [py-svn] py commit 33d061c0c279: super does not work with exceptions in python2.4 Message-ID: <20101125160920.39BD66C1064@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project py # URL http://bitbucket.org/hpk42/py/overview # User holger krekel # Date 1290701348 -3600 # Node ID 33d061c0c2790378ff0171022516ea2b2c9350ca # Parent d8a7846892df17a93ad268f8c86c0c7e5f3fdb78 super does not work with exceptions in python2.4 --- a/testing/code/test_assertion.py +++ b/testing/code/test_assertion.py @@ -1,4 +1,4 @@ -import py +import pytest, py def exvalue(): return py.std.sys.exc_info()[1] @@ -291,6 +291,7 @@ def test_assert_raise_alias(testdir): ]) + at pytest.mark.skipif("sys.version_info < (2,5)") def test_assert_raise_subclass(): class SomeEx(AssertionError): def __init__(self, *args): From commits-noreply at bitbucket.org Thu Nov 25 18:15:19 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 11:15:19 -0600 (CST) Subject: [py-svn] py commit 5346ab41b059: setting version to 1.4.0 refining release announce Message-ID: <20101125171519.06C876C1070@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project py # URL http://bitbucket.org/hpk42/py/overview # User holger krekel # Date 1290705284 -3600 # Node ID 5346ab41b059c95a48cbe1e8a7bae96ce6e0da27 # Parent 33d061c0c2790378ff0171022516ea2b2c9350ca setting version to 1.4.0 refining release announce --- a/setup.py +++ b/setup.py @@ -9,7 +9,7 @@ def main(): name='py', description='library with cross-python path, ini-parsing, io, code, log facilities', long_description = open('README.txt').read(), - version='1.4.0.a7', + version='1.4.0', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/py/__init__.py +++ b/py/__init__.py @@ -8,7 +8,7 @@ dictionary or an import path. (c) Holger Krekel and others, 2004-2010 """ -__version__ = '1.4.0.a7' +__version__ = '1.4.0' from py import _apipkg --- a/doc/announce/release-1.4.0.txt +++ b/doc/announce/release-1.4.0.txt @@ -4,18 +4,35 @@ py-1.4.0: lib for path, code, io, ... manipulations =========================================================================== -"py" is a small library comprising APIs for filesystem and svn path manipulations, -dynamic code construction and introspection, IO capturing and a Python2/Python3 -compatibility namespace. It runs unmodified on all Python interpreters compatible -to Python2.4 up until Python 3.2. +"py" is a small library comprising APIs for filesystem and svn path +manipulations, dynamic code construction and introspection, IO capturing +and a Python2/Python3 compatibility namespace. It runs unmodified on +all Python interpreters compatible to Python2.4 up until Python 3.2. +The general goal with "py" is to provide stable APIs that are +continously tested against many Python interpreters and thus also to +help transition. The py distribution prior to 1.4.0 used to contain "py.test" which now -comes as its own "pytest" distribution. Also, the "py.cleanup|py.lookup|py.countloc" -etc. helpers are now part of pycmd. This makes "py-1.4.0" a simple library -not installing any command line utilities. The general idea for "py" is to -place high value on providing some basic APIs that are continously tested -against many Python interpreters and thus also to help transition. +comes as its own "pytest" distribution. Also, the +"py.cleanup|py.lookup|py.countloc" etc. helpers are now part of pycmd. +This makes "py-1.4.0" a simple library which does not nstall any command +line utilities anymore. cheers, holger +Changes between 1.3.4 and 1.4.0 +------------------------------------- + +- py.test was moved to a separate "pytest" package. What remains is + a stub hook which will proxy ``import py.test`` to ``pytest``. +- all command line tools ("py.cleanup/lookup/countloc/..." moved + to "pycmd" package) +- removed the old and deprecated "py.magic" namespace +- use apipkg-1.1 and make py.apipkg.initpkg|ApiModule available +- add py.iniconfig module for brain-dead easy ini-config file parsing +- introduce py.builtin.any() +- path objects have a .dirname attribute now (equivalent to + os.path.dirname(path)) +- path.visit() accepts breadthfirst (bf) and sort options +- remove deprecated py.compat namespace --- a/CHANGELOG +++ b/CHANGELOG @@ -1,5 +1,5 @@ -Changes between 1.3.4 and 1.4.0dev0 +Changes between 1.3.4 and 1.4.0 ================================================== - py.test was moved to a separate "pytest" package. What remains is From commits-noreply at bitbucket.org Thu Nov 25 18:15:19 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 11:15:19 -0600 (CST) Subject: [py-svn] py commit f10f6036eee5: Added tag 1.4.0 for changeset 5346ab41b059 Message-ID: <20101125171519.126716C1313@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project py # URL http://bitbucket.org/hpk42/py/overview # User holger krekel # Date 1290705290 -3600 # Node ID f10f6036eee5ba4470ec2855755d7e0dba54940e # Parent 5346ab41b059c95a48cbe1e8a7bae96ce6e0da27 Added tag 1.4.0 for changeset 5346ab41b059 --- a/.hgtags +++ b/.hgtags @@ -31,3 +31,4 @@ c59d3fa8681a5b5966b8375b16fccd64a3a8dbeb 79ef6377705184c55633d456832eea318fedcf61 1.3.4 79ef6377705184c55633d456832eea318fedcf61 1.3.4 90fffd35373e9f125af233f78b19416f0938d841 1.3.4 +5346ab41b059c95a48cbe1e8a7bae96ce6e0da27 1.4.0 From commits-noreply at bitbucket.org Thu Nov 25 20:07:04 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 13:07:04 -0600 (CST) Subject: [py-svn] pytest commit 57f312d11228: last changes, preparing 2.0.0 Message-ID: <20101125190704.959C26C1414@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290712002 -3600 # Node ID 57f312d1122860af1b0642541d6082b540322777 # Parent 83cd00f43c899b8754faccf03dd3fafd33992729 last changes, preparing 2.0.0 --- a/testing/test_capture.py +++ b/testing/test_capture.py @@ -77,6 +77,7 @@ class TestCaptureManager: finally: capouter.reset() + at pytest.mark.xfail("hasattr(sys, 'pypy_version_info')") @pytest.mark.multi(method=['fd', 'sys']) def test_capturing_unicode(testdir, method): if sys.version_info >= (3,0): --- a/doc/assert.txt +++ b/doc/assert.txt @@ -84,6 +84,8 @@ asserts that the given ``ExpectedExcepti provide you with helpful output in case of failures such as *no exception* or *wrong exception*. +.. _newreport: + Making use of context-sensitive comparisons ------------------------------------------------- @@ -130,7 +132,7 @@ Special comparisons are done for a numbe * comparing long sequences: first failing indices * comparing dicts: different entries -See the :ref:`reporting demo ` for examples. +See the :ref:`reporting demo ` for many more examples. .. Defining your own comparison --- a/doc/example/reportingdemo.txt +++ b/doc/example/reportingdemo.txt @@ -7,7 +7,9 @@ Demo of Python failure reports with py.t Here is a nice run of several tens of failures and how py.test presents things (unfortunately not showing the nice colors here in the HTML that you -get on the terminal - we are working on that):: +get on the terminal - we are working on that): + +.. code-block:: python assertion $ py.test failure_demo.py =========================== test session starts ============================ --- a/pytest.py +++ b/pytest.py @@ -1,7 +1,7 @@ """ unit and functional testing with Python. """ -__version__ = '2.0.0.dev42' +__version__ = '2.0.0' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,7 +22,7 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0.dev42', + version='2.0.0', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -257,6 +257,9 @@ class TerminalReporter: self.write_sep("=", "test session starts", bold=True) verinfo = ".".join(map(str, sys.version_info[:3])) msg = "platform %s -- Python %s" % (sys.platform, verinfo) + if hasattr(sys, 'pypy_version_info'): + verinfo = ".".join(map(str, sys.pypy_version_info[:3])) + msg += "[pypy-%s]" % verinfo msg += " -- pytest-%s" % (py.test.__version__) if self.verbosity > 0 or self.config.option.debug or \ getattr(self.config.option, 'pastebin', None): --- a/doc/announce/release-2.0.0.txt +++ b/doc/announce/release-2.0.0.txt @@ -41,7 +41,7 @@ New Features - new and better reporting information in assert expressions if comparing lists, sequences or strings. - see http://pytest.org/2.0.0/assert.html for details. + see http://pytest.org/2.0.0/assert.html#newreport - new configuration through ini-files (setup.cfg or tox.ini recognized), for example:: From commits-noreply at bitbucket.org Thu Nov 25 20:12:12 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 13:12:12 -0600 (CST) Subject: [py-svn] pytest-xdist commit cd44a941c833: upping version to 1.5 Message-ID: <20101125191212.031996C140B@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1290712319 -3600 # Node ID cd44a941c833c098e4899fe3d42a96703754d0d5 # Parent 5f81d275775449132c497cef31d72bc5e1f7eeb8 upping version to 1.5 --- a/setup.py +++ b/setup.py @@ -2,7 +2,7 @@ from setuptools import setup setup( name="pytest-xdist", - version='1.5a11', + version='1.5', description='py.test xdist plugin for distributed testing and loop-on-failing modes', long_description=open('README.txt').read(), license='GPLv2 or later', --- a/xdist/__init__.py +++ b/xdist/__init__.py @@ -1,2 +1,2 @@ # -__version__ = '1.5a11' +__version__ = '1.5' --- a/CHANGELOG +++ b/CHANGELOG @@ -1,7 +1,7 @@ -1.5a1 +1.5 ------------------------- -- adapt to pytest-2.0 changes, rsyncdirs and rsyncignore can now +- adapt to and require pytest-2.0 changes, rsyncdirs and rsyncignore can now only be specified in [pytest] sections of ini files, see "py.test -h" for details. - major internal refactoring to match the pytest-2.0 event refactoring From commits-noreply at bitbucket.org Thu Nov 25 21:03:59 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 14:03:59 -0600 (CST) Subject: [py-svn] pytest-xdist commit 94be523cbf10: Added tag 1.5 for changeset cd44a941c833 Message-ID: <20101125200359.C01251E0404@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest-xdist # URL http://bitbucket.org/hpk42/pytest-xdist/overview # User holger krekel # Date 1290715410 -3600 # Node ID 94be523cbf10d0df4ae903ba9b575b4ed6c5725a # Parent cd44a941c833c098e4899fe3d42a96703754d0d5 Added tag 1.5 for changeset cd44a941c833 --- a/.hgtags +++ b/.hgtags @@ -5,3 +5,4 @@ e6c4ce20db4bf65086ff55807a3c306cad7ca393 e6c4ce20db4bf65086ff55807a3c306cad7ca393 1.3 eaf8b1cb7c312883598677231be5bbeea3b5c127 1.3 a423748bf17ee778a37853225210257699cad9c1 1.4 +cd44a941c833c098e4899fe3d42a96703754d0d5 1.5 From commits-noreply at bitbucket.org Thu Nov 25 21:04:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 14:04:02 -0600 (CST) Subject: [py-svn] pytest commit 06e3242d34b2: Added tag 2.0.0 for changeset e9e127acd6f0 Message-ID: <20101125200402.D2BF4241972@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290715388 -3600 # Node ID 06e3242d34b290b20ea336f7d220da45bd2759eb # Parent e9e127acd6f0497324ef7f40cfb997cad4c4cd17 Added tag 2.0.0 for changeset e9e127acd6f0 --- a/.hgtags +++ b/.hgtags @@ -31,3 +31,4 @@ c59d3fa8681a5b5966b8375b16fccd64a3a8dbeb 79ef6377705184c55633d456832eea318fedcf61 1.3.4 79ef6377705184c55633d456832eea318fedcf61 1.3.4 90fffd35373e9f125af233f78b19416f0938d841 1.3.4 +e9e127acd6f0497324ef7f40cfb997cad4c4cd17 2.0.0 From commits-noreply at bitbucket.org Thu Nov 25 21:04:02 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 14:04:02 -0600 (CST) Subject: [py-svn] pytest commit e9e127acd6f0: fix trove classifier Message-ID: <20101125200402.E1DCE2419B7@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290715329 -3600 # Node ID e9e127acd6f0497324ef7f40cfb997cad4c4cd17 # Parent 57f312d1122860af1b0642541d6082b540322777 fix trove classifier --- a/setup.py +++ b/setup.py @@ -30,7 +30,7 @@ def main(): author_email='holger at merlinux.eu', entry_points= make_entry_points(), install_requires=['py>=1.4.0a2'], - classifiers=['Development Status :: 5 - Production', + classifiers=['Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', 'Operating System :: POSIX', From commits-noreply at bitbucket.org Fri Nov 26 03:55:00 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 25 Nov 2010 20:55:00 -0600 (CST) Subject: [py-svn] pytest commit 66a7de5f7a3e: need double colon here Message-ID: <20101126025500.7BCBE6C140B@bitbucket03.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User Benjamin Peterson # Date 1290740132 21600 # Node ID 66a7de5f7a3e6778cf0424423dbfde94d4297576 # Parent 06e3242d34b290b20ea336f7d220da45bd2759eb need double colon here --- a/doc/assert.txt +++ b/doc/assert.txt @@ -49,8 +49,8 @@ line:: assert f.read() != '...' -This might fail but when re-interpretation comes along it might pass. -You can rewrite this (and any other expression with side effects) easily, though: +This might fail but when re-interpretation comes along it might pass. You can +rewrite this (and any other expression with side effects) easily, though:: content = f.read() assert content != '...' From commits-noreply at bitbucket.org Fri Nov 26 15:43:09 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 26 Nov 2010 08:43:09 -0600 (CST) Subject: [py-svn] pytest commit 2e4c9cdd531a: regenerating examples Message-ID: <20101126144309.B8D142410BD@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290774416 -3600 # Node ID 2e4c9cdd531abbc69a90cd8615ea0d72ab479e58 # Parent 66a7de5f7a3e6778cf0424423dbfde94d4297576 regenerating examples --- a/doc/Makefile +++ b/doc/Makefile @@ -15,7 +15,7 @@ ALLSPHINXOPTS = -d $(BUILDDIR)/doctree .PHONY: help clean html dirhtml singlehtml pickle json htmlhelp qthelp devhelp epub latex latexpdf text man changes linkcheck doctest regen: - COLUMNS=76 regendoc --update *.txt */*.txt + PYTHONDONTWRITEBYTECODE=1 COLUMNS=76 regendoc --update *.txt */*.txt help: @echo "Please use \`make ' where is one of" --- a/doc/unittest.txt +++ b/doc/unittest.txt @@ -24,8 +24,8 @@ Running it yields:: $ py.test test_unittest.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_unittest.py + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 1 items test_unittest.py F @@ -56,7 +56,7 @@ Running it yields:: /usr/lib/python2.6/unittest.py:350: AssertionError ----------------------------- Captured stdout ------------------------------ hello - ========================= 1 failed in 0.02 seconds ========================= + ========================= 1 failed in 0.03 seconds ========================= .. _`unittest.py style`: http://docs.python.org/library/unittest.html --- a/doc/projects.txt +++ b/doc/projects.txt @@ -15,9 +15,12 @@ Here are some examples of projects using * `mwlib `_ mediawiki parser and utility library * `The Translate Toolkit `_ for localization and conversion * `execnet `_ rapid multi-Python deployment +* `Pacha `_ configuration management in five minutes * `bbfreeze `_ create standalone executables from Python scripts * `pdb++ `_ a fancier version of PDB * `py-s3fuse `_ Amazon S3 FUSE based filesystem +* `waskr `_ WSGI Stats Middleware +* `guachi `_ global persistent configs for Python modules * `Circuits `_ lightweight Event Driven Framework * `pygtk-helpers `_ easy interaction with PyGTK * `QuantumCore `_ statusmessage and repoze openid plugin --- a/doc/example/simple.txt +++ b/doc/example/simple.txt @@ -7,6 +7,8 @@ basic patterns and examples pass different values to a test function, depending on command line options ---------------------------------------------------------------------------- +.. regendoc:wipe + Suppose we want to write a test that depends on a command line option. Here is a basic pattern how to achieve this:: @@ -32,7 +34,8 @@ provide the ``cmdopt`` through a :ref:`f Let's run this without supplying our new command line option:: - $ py.test -q + $ py.test -q test_sample.py + collecting ... collected 1 items F ================================= FAILURES ================================= _______________________________ test_answer ________________________________ @@ -55,6 +58,7 @@ Let's run this without supplying our new And now with supplying a command line option:: $ py.test -q --cmdopt=type2 + collecting ... collected 1 items F ================================= FAILURES ================================= _______________________________ test_answer ________________________________ @@ -83,6 +87,8 @@ on real-life examples. generating parameters combinations, depending on command line ---------------------------------------------------------------------------- +.. regendoc:wipe + Let's say we want to execute a test with different parameters and the parameter range shall be determined by a command line argument. Let's first write a simple computation test:: @@ -112,13 +118,15 @@ Now we add a test configuration like thi This means that we only run 2 tests if we do not pass ``--all``:: $ py.test -q test_compute.py + collecting ... collected 2 items .. 2 passed in 0.01 seconds We run only two computations, so we see two dots. let's run the full monty:: - $ py.test -q --all test_compute.py + $ py.test -q --all + collecting ... collected 5 items ....F ================================= FAILURES ================================= _____________________________ test_compute[4] ______________________________ @@ -141,6 +149,8 @@ we'll get an error on the last one. control skipping of tests according to command line option -------------------------------------------------------------- +.. regendoc:wipe + Here is a ``conftest.py`` file adding a ``--runslow`` command line option to control skipping of ``slow`` marked tests:: @@ -171,32 +181,33 @@ We can now write a test module like this and when running it will see a skipped "slow" test:: - $ py.test test_module.py -rs # "-rs" means report details on the little 's' + $ py.test -rs # "-rs" means report details on the little 's' =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_module.py + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 2 items test_module.py .s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-104/conftest.py:9: need --runslow option to run + SKIP [1] /tmp/doc-exec-479/conftest.py:9: need --runslow option to run =================== 1 passed, 1 skipped in 0.02 seconds ==================== Or run it including the ``slow`` marked test:: - $ py.test test_module.py --runslow + $ py.test --runslow =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_module.py + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 2 items test_module.py .. ========================= 2 passed in 0.01 seconds ========================= - writing well integrated assertion helpers -------------------------------------------------- +.. regendoc:wipe + If you have a test helper function called from a test you can use the ``pytest.fail`` marker to fail a test with a certain message. The test support function will not show up in the traceback if you @@ -218,7 +229,8 @@ of tracebacks: the ``checkconfig`` funct unless the ``--fulltrace`` command line option is specified. Let's run our little function:: - $ py.test -q + $ py.test -q test_checkconfig.py + collecting ... collected 1 items F ================================= FAILURES ================================= ______________________________ test_something ______________________________ @@ -230,16 +242,17 @@ Let's run our little function:: test_checkconfig.py:8: Failed 1 failed in 0.02 seconds - Detect if running from within a py.test run -------------------------------------------------------------- +.. regendoc:wipe + Usually it is a bad idea to make application code behave differently if called from a test. But if you absolutely must find out if your application code is running from a test you can do something like this:: - # content of conftest.py in your testing directory + # content of conftest.py def pytest_configure(config): import sys --- a/doc/monkeypatch.txt +++ b/doc/monkeypatch.txt @@ -39,8 +39,8 @@ will be undone. .. background check: $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: /tmp/doc-exec-75 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 0 items ============================= in 0.00 seconds ============================= --- a/doc/doctest.txt +++ b/doc/doctest.txt @@ -15,11 +15,11 @@ python test modules):: py.test --doctest-modules You can make these changes permanent in your project by -putting them into a conftest.py file like this:: +putting them into a pytest.ini file like this:: - # content of conftest.py - option_doctestmodules = True - option_doctestglob = "*.rst" + # content of pytest.ini + [pytest] + addopts = --doctest-modules If you then have a text file like this:: @@ -35,7 +35,7 @@ and another like this:: # content of mymodule.py def something(): """ a doctest in a docstring - >>> something() + >>> something() 42 """ return 42 @@ -44,7 +44,9 @@ then you can just invoke ``py.test`` wit $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: /tmp/doc-exec-66 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 1 items - ============================= in 0.00 seconds ============================= + mymodule.py . + + ========================= 1 passed in 0.03 seconds ========================= --- a/doc/funcargs.txt +++ b/doc/funcargs.txt @@ -45,8 +45,8 @@ Running the test looks like this:: $ py.test test_simplefactory.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_simplefactory.py + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 1 items test_simplefactory.py F @@ -150,8 +150,8 @@ Running this:: $ py.test test_example.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_example.py + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 10 items test_example.py .........F @@ -188,8 +188,8 @@ If you want to select only the run with $ py.test -v -k 7 test_example.py # or -k test_func[7] =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 -- /home/hpk/venv/0/bin/python - test path 1: test_example.py + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 -- /home/hpk/venv/0/bin/python + collecting ... collected 10 items test_example.py:6: test_func[7] PASSED --- a/doc/builtin.txt +++ b/doc/builtin.txt @@ -30,25 +30,25 @@ You can ask for available builtin or pro captures writes to sys.stdout/sys.stderr and makes them available successively via a ``capsys.readouterr()`` method which returns a ``(out, err)`` tuple of captured snapshot strings. - + capfd captures writes to file descriptors 1 and 2 and makes snapshotted ``(out, err)`` string tuples available via the ``capsys.readouterr()`` method. If the underlying platform does not have ``os.dup`` (e.g. Jython) tests using this funcarg will automatically skip. - + tmpdir return a temporary directory path object unique to each test function invocation, created as a sub directory of the base temporary directory. The returned object is a `py.path.local`_ path object. - + monkeypatch The returned ``monkeypatch`` funcarg provides these helper methods to modify objects, dictionaries or os.environ:: - + monkeypatch.setattr(obj, name, value, raising=True) monkeypatch.delattr(obj, name, raising=True) monkeypatch.setitem(mapping, name, value) @@ -56,15 +56,15 @@ You can ask for available builtin or pro monkeypatch.setenv(name, value, prepend=False) monkeypatch.delenv(name, value, raising=True) monkeypatch.syspath_prepend(path) - + All modifications will be undone when the requesting test function finished its execution. The ``raising`` parameter determines if a KeyError or AttributeError will be raised if the set/deletion operation has no target. - + recwarn Return a WarningsRecorder instance that provides these methods: - + * ``pop(category=None)``: return last warning matching the category. * ``clear()``: clear list of warnings - + --- a/doc/mark.txt +++ b/doc/mark.txt @@ -88,8 +88,8 @@ You can use the ``-k`` command line opti $ py.test -k webtest # running with the above defined examples yields =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: /tmp/doc-exec-74 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 4 items test_mark.py .. test_mark_classlevel.py .. @@ -100,8 +100,8 @@ And you can also run all tests except th $ py.test -k-webtest =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: /tmp/doc-exec-74 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 4 items ===================== 4 tests deselected by '-webtest' ===================== ======================= 4 deselected in 0.01 seconds ======================= @@ -110,8 +110,8 @@ Or to only select the class:: $ py.test -kTestClass =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: /tmp/doc-exec-74 + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 4 items test_mark_classlevel.py .. --- a/doc/example/parametrize.txt +++ b/doc/example/parametrize.txt @@ -41,11 +41,12 @@ Running it means we are two tests for ea the respective settings:: $ py.test -q + collecting ... collected 4 items F..F ================================= FAILURES ================================= _________________________ TestClass.test_equals[0] _________________________ - self = , a = 1, b = 2 + self = , a = 1, b = 2 def test_equals(self, a, b): > assert a == b @@ -54,7 +55,7 @@ the respective settings:: test_parametrize.py:17: AssertionError ______________________ TestClass.test_zerodivision[1] ______________________ - self = , a = 3, b = 2 + self = , a = 3, b = 2 def test_zerodivision(self, a, b): > pytest.raises(ZeroDivisionError, "a/b") @@ -97,11 +98,12 @@ for parametrizing test methods:: Running it gives similar results as before:: $ py.test -q test_parametrize2.py + collecting ... collected 4 items F..F ================================= FAILURES ================================= _________________________ TestClass.test_equals[0] _________________________ - self = , a = 1, b = 2 + self = , a = 1, b = 2 @params([dict(a=1, b=2), dict(a=3, b=3), ]) def test_equals(self, a, b): @@ -111,7 +113,7 @@ Running it gives similar results as befo test_parametrize2.py:19: AssertionError ______________________ TestClass.test_zerodivision[1] ______________________ - self = , a = 3, b = 2 + self = , a = 3, b = 2 @params([dict(a=1, b=0), dict(a=3, b=2)]) def test_zerodivision(self, a, b): @@ -138,5 +140,6 @@ with different sets of arguments for its Running it (with Python-2.4 through to Python2.7 installed):: . $ py.test -q multipython.py + collecting ... collected 75 items ....s....s....s....ssssss....s....s....s....ssssss....s....s....s....ssssss - 48 passed, 27 skipped in 2.55 seconds + 48 passed, 27 skipped in 2.74 seconds --- a/doc/getting-started.txt +++ b/doc/getting-started.txt @@ -16,7 +16,7 @@ Installation options:: To check your installation has installed the correct version:: $ py.test --version - This is py.test version 2.0.0.dev30, imported from /home/hpk/p/pytest/pytest.py + This is py.test version 2.0.0, imported from /home/hpk/p/pytest/pytest.pyc If you get an error checkout :ref:`installation issues`. @@ -38,19 +38,19 @@ That's it. You can execute the test func $ py.test =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: /tmp/doc-exec-70 - + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 1 items + test_sample.py F - + ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - + def test_answer(): > assert func(3) == 5 E assert 4 == 5 E + where 4 = func(3) - + test_sample.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= @@ -58,9 +58,10 @@ py.test found the ``test_answer`` functi .. note:: - You can simply use the ``assert`` statement for coding expectations because - intermediate values will be presented to you. This is arguably easier than - learning all the `the JUnit legacy methods`_. + You can simply use the ``assert`` statement for asserting + expectations because intermediate values will be presented to you. + This is arguably easier than learning all the `the JUnit legacy + methods`_. However, there remains one caveat to using simple asserts: your assertion expression should better be side-effect free. Because @@ -94,6 +95,7 @@ use the ``raises`` helper:: Running it with, this time in "quiet" reporting mode:: $ py.test -q test_sysexit.py + collecting ... collected 1 items . 1 passed in 0.00 seconds @@ -121,17 +123,18 @@ There is no need to subclass anything. run the module by passing its filename:: $ py.test -q test_class.py + collecting ... collected 2 items .F ================================= FAILURES ================================= ____________________________ TestClass.test_two ____________________________ - - self = - + + self = + def test_two(self): x = "hello" > assert hasattr(x, 'check') E assert hasattr('hello', 'check') - + test_class.py:8: AssertionError 1 failed, 1 passed in 0.02 seconds @@ -157,21 +160,22 @@ py.test will lookup and call a factory t before performing the test function call. Let's just run it:: $ py.test -q test_tmpdir.py + collecting ... collected 1 items F ================================= FAILURES ================================= _____________________________ test_needsfiles ______________________________ - - tmpdir = local('/tmp/pytest-122/test_needsfiles0') - + + tmpdir = local('/tmp/pytest-7/test_needsfiles0') + def test_needsfiles(tmpdir): print tmpdir > assert 0 E assert 0 - + test_tmpdir.py:3: AssertionError ----------------------------- Captured stdout ------------------------------ - /tmp/pytest-122/test_needsfiles0 - 1 failed in 0.05 seconds + /tmp/pytest-7/test_needsfiles0 + 1 failed in 0.04 seconds Before the test runs, a unique-per-test-invocation temporary directory was created. More info at :ref:`tmpdir handling`. --- a/doc/assert.txt +++ b/doc/assert.txt @@ -23,21 +23,21 @@ assertion fails you will see the value o $ py.test test_assert1.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_assert1.py - + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 1 items + test_assert1.py F - + ================================= FAILURES ================================= ______________________________ test_function _______________________________ - + def test_function(): > assert f() == 4 E assert 3 == 4 E + where 3 = f() - + test_assert1.py:5: AssertionError - ========================= 1 failed in 0.03 seconds ========================= + ========================= 1 failed in 0.02 seconds ========================= Reporting details about the failing assertion is achieved by re-evaluating the assert expression and recording intermediate values. @@ -105,14 +105,14 @@ if you run this module:: $ py.test test_assert2.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_assert2.py - + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 1 items + test_assert2.py F - + ================================= FAILURES ================================= ___________________________ test_set_comparison ____________________________ - + def test_set_comparison(): set1 = set("1308") set2 = set("8035") @@ -122,7 +122,7 @@ if you run this module:: E '1' E Extra items in the right set: E '5' - + test_assert2.py:5: AssertionError ========================= 1 failed in 0.02 seconds ========================= --- a/doc/example/mysetup.txt +++ b/doc/example/mysetup.txt @@ -49,15 +49,15 @@ You can now run the test:: $ py.test test_sample.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_sample.py + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 1 items test_sample.py F ================================= FAILURES ================================= _______________________________ test_answer ________________________________ - mysetup = + mysetup = def test_answer(mysetup): app = mysetup.myapp() @@ -122,12 +122,12 @@ Running it yields:: $ py.test test_ssh.py -rs =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_ssh.py + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 1 items test_ssh.py s ========================= short test summary info ========================== - SKIP [1] /tmp/doc-exec-107/conftest.py:22: specify ssh host with --ssh + SKIP [1] /tmp/doc-exec-474/conftest.py:22: specify ssh host with --ssh ======================== 1 skipped in 0.02 seconds ========================= --- a/doc/skipping.txt +++ b/doc/skipping.txt @@ -121,14 +121,14 @@ Running it with the report-on-xfail opti example $ py.test -rx xfail_demo.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev31 - test path 1: xfail_demo.py + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 5 items xfail_demo.py xxxxx ========================= short test summary info ========================== XFAIL xfail_demo.py::test_hello XFAIL xfail_demo.py::test_hello2 - reason: [NOTRUN] + reason: [NOTRUN] XFAIL xfail_demo.py::test_hello3 condition: hasattr(os, 'sep') XFAIL xfail_demo.py::test_hello4 --- a/doc/example/nonpython.txt +++ b/doc/example/nonpython.txt @@ -27,8 +27,8 @@ now execute the test specification:: nonpython $ py.test test_simple.yml =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_simple.yml + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 2 items test_simple.yml .F @@ -37,9 +37,7 @@ now execute the test specification:: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ========================= short test summary info ========================== - FAIL test_simple.yml::hello - ==================== 1 failed, 1 passed in 0.06 seconds ==================== + ==================== 1 failed, 1 passed in 0.03 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 @@ -58,8 +56,8 @@ reporting in ``verbose`` mode:: nonpython $ py.test -v =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 -- /home/hpk/venv/0/bin/python - test path 1: /home/hpk/p/pytest/doc/example/nonpython + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 -- /home/hpk/venv/0/bin/python + collecting ... collected 2 items test_simple.yml:1: usecase: ok PASSED test_simple.yml:1: usecase: hello FAILED @@ -69,9 +67,7 @@ reporting in ``verbose`` mode:: usecase execution failed spec failed: 'some': 'other' no further details known at this point. - ========================= short test summary info ========================== - FAIL test_simple.yml::hello - ==================== 1 failed, 1 passed in 0.06 seconds ==================== + ==================== 1 failed, 1 passed in 0.03 seconds ==================== While developing your custom test collection and execution it's also interesting to just look at the collection tree:: --- a/doc/example/reportingdemo.txt +++ b/doc/example/reportingdemo.txt @@ -13,9 +13,8 @@ get on the terminal - we are working on assertion $ py.test failure_demo.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev38 - collecting ... - collected 35 items + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 35 items failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF @@ -31,7 +30,7 @@ get on the terminal - we are working on failure_demo.py:15: AssertionError _________________________ TestFailing.test_simple __________________________ - self = + self = def test_simple(self): def f(): @@ -41,21 +40,21 @@ get on the terminal - we are working on > 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( 42, > 6*9) - failure_demo.py:33: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + failure_demo.py:33: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ a = 42, b = 54 @@ -67,19 +66,19 @@ get on the terminal - we are working on failure_demo.py:12: 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' @@ -90,7 +89,7 @@ get on the terminal - we are working on 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' @@ -103,7 +102,7 @@ get on the terminal - we are working on 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' @@ -116,7 +115,7 @@ get on the terminal - we are working on failure_demo.py:48: AssertionError ______________ TestSpecialisedExplanations.test_eq_long_text _______________ - self = + self = def test_eq_long_text(self): a = '1'*100 + 'a' + '2'*100 @@ -133,7 +132,7 @@ get on the terminal - we are working on 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 @@ -157,7 +156,7 @@ get on the terminal - we are working on failure_demo.py:58: AssertionError _________________ TestSpecialisedExplanations.test_eq_list _________________ - self = + self = def test_eq_list(self): > assert [0, 1, 2] == [0, 1, 3] @@ -167,7 +166,7 @@ get on the terminal - we are working on failure_demo.py:61: AssertionError ______________ TestSpecialisedExplanations.test_eq_list_long _______________ - self = + self = def test_eq_list_long(self): a = [0]*100 + [1] + [3]*100 @@ -179,7 +178,7 @@ get on the terminal - we are working on 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} @@ -192,7 +191,7 @@ get on the terminal - we are working on 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]) @@ -208,7 +207,7 @@ get on the terminal - we are working on failure_demo.py:72: AssertionError _____________ TestSpecialisedExplanations.test_eq_longer_list ______________ - self = + self = def test_eq_longer_list(self): > assert [1,2] == [1,2,3] @@ -218,7 +217,7 @@ get on the terminal - we are working on failure_demo.py:75: AssertionError _________________ TestSpecialisedExplanations.test_in_list _________________ - self = + self = def test_in_list(self): > assert 1 in [0, 2, 3, 4, 5] @@ -233,7 +232,7 @@ get on the terminal - we are working on i = Foo() > assert i.b == 2 E assert 1 == 2 - E + where 1 = .b + E + where 1 = .b failure_demo.py:85: AssertionError _________________________ test_attribute_instance __________________________ @@ -243,8 +242,8 @@ get on the terminal - we are working on 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:91: AssertionError __________________________ test_attribute_failure __________________________ @@ -257,10 +256,10 @@ get on the terminal - we are working on i = Foo() > assert i.b == 2 - failure_demo.py:100: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + failure_demo.py:100: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ - self = + self = def _get_b(self): > raise Exception('Failed to get attrib') @@ -276,22 +275,22 @@ get on the terminal - we are working on 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:108: AssertionError __________________________ TestRaises.test_raises __________________________ - self = + self = def test_raises(self): s = 'qwe' > raises(TypeError, "int(s)") - failure_demo.py:117: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + failure_demo.py:117: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ > int(s) E ValueError: invalid literal for int() with base 10: 'qwe' @@ -299,7 +298,7 @@ get on the terminal - we are working on <0-codegen /home/hpk/p/pytest/_pytest/python.py:819>:1: ValueError ______________________ TestRaises.test_raises_doesnt _______________________ - self = + self = def test_raises_doesnt(self): > raises(IOError, "int('3')") @@ -308,7 +307,7 @@ get on the terminal - we are working on failure_demo.py:120: Failed __________________________ TestRaises.test_raise ___________________________ - self = + self = def test_raise(self): > raise ValueError("demo error") @@ -317,7 +316,7 @@ get on the terminal - we are working on failure_demo.py:123: ValueError ________________________ TestRaises.test_tupleerror ________________________ - self = + self = def test_tupleerror(self): > a,b = [1] @@ -326,7 +325,7 @@ get on the terminal - we are working on failure_demo.py:126: 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] @@ -339,7 +338,7 @@ get on the terminal - we are working on l is [1, 2, 3] ________________________ TestRaises.test_some_error ________________________ - self = + self = def test_some_error(self): > if namenotexi: @@ -357,8 +356,8 @@ get on the terminal - we are working on py.std.sys.modules[name] = module > module.foo() - failure_demo.py:149: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + failure_demo.py:149: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ def foo(): > assert 1 == 0 @@ -367,7 +366,7 @@ get on the terminal - we are working on <2-codegen 'abc-123' /home/hpk/p/pytest/doc/example/assertion/failure_demo.py:146>:2: AssertionError ____________________ TestMoreErrors.test_complex_error _____________________ - self = + self = def test_complex_error(self): def f(): @@ -376,16 +375,16 @@ get on the terminal - we are working on return 43 > somefunc(f(), g()) - failure_demo.py:159: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + failure_demo.py:159: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ x = 44, y = 43 def somefunc(x,y): > otherfunc(x,y) - failure_demo.py:8: - _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ + failure_demo.py:8: + _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ a = 44, b = 43 @@ -396,7 +395,7 @@ get on the terminal - we are working on failure_demo.py:5: AssertionError ___________________ TestMoreErrors.test_z1_unpack_error ____________________ - self = + self = def test_z1_unpack_error(self): l = [] @@ -406,7 +405,7 @@ get on the terminal - we are working on failure_demo.py:163: ValueError ____________________ TestMoreErrors.test_z2_type_error _____________________ - self = + self = def test_z2_type_error(self): l = 3 @@ -416,19 +415,19 @@ get on the terminal - we are working on failure_demo.py:167: 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:172: AssertionError __________________ TestMoreErrors.test_startswith_nested ___________________ - self = + self = def test_startswith_nested(self): def f(): @@ -436,15 +435,15 @@ get on the terminal - we are working on 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:179: AssertionError _____________________ TestMoreErrors.test_global_func ______________________ - self = + self = def test_global_func(self): > assert isinstance(globf(42), float) @@ -454,19 +453,19 @@ get on the terminal - we are working on failure_demo.py:182: 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 = 42 - E + where 42 = .x + E + where 42 = .x failure_demo.py:186: AssertionError _______________________ TestMoreErrors.test_compare ________________________ - self = + self = def test_compare(self): > assert globf(10) < 5 @@ -476,7 +475,7 @@ get on the terminal - we are working on failure_demo.py:189: AssertionError _____________________ TestMoreErrors.test_try_finally ______________________ - self = + self = def test_try_finally(self): x = 1 --- a/doc/tmpdir.txt +++ b/doc/tmpdir.txt @@ -28,15 +28,15 @@ Running this would result in a passed te $ py.test test_tmpdir.py =========================== test session starts ============================ - platform linux2 -- Python 2.6.5 -- pytest-2.0.0.dev30 - test path 1: test_tmpdir.py + platform linux2 -- Python 2.6.5 -- pytest-2.0.0 + collecting ... collected 1 items test_tmpdir.py F ================================= FAILURES ================================= _____________________________ test_create_file _____________________________ - tmpdir = local('/tmp/pytest-123/test_create_file0') + tmpdir = local('/tmp/pytest-8/test_create_file0') def test_create_file(tmpdir): p = tmpdir.mkdir("sub").join("hello.txt") @@ -47,7 +47,7 @@ Running this would result in a passed te E assert 0 test_tmpdir.py:7: AssertionError - ========================= 1 failed in 0.02 seconds ========================= + ========================= 1 failed in 0.04 seconds ========================= .. _`base temporary directory`: From commits-noreply at bitbucket.org Fri Nov 26 15:43:09 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Fri, 26 Nov 2010 08:43:09 -0600 (CST) Subject: [py-svn] pytest commit 026cb4b8eebd: bumping version to a dev version, run tests by using python PyPI by default Message-ID: <20101126144309.CD323241972@bitbucket01.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project pytest # URL http://bitbucket.org/hpk42/pytest/overview # User holger krekel # Date 1290775020 -3600 # Node ID 026cb4b8eebd6b30617475bf9ccdba76999ad86a # Parent 2e4c9cdd531abbc69a90cd8615ea0d72ab479e58 bumping version to a dev version, run tests by using python PyPI by default --- a/doc/Makefile +++ b/doc/Makefile @@ -40,7 +40,7 @@ clean: -rm -rf $(BUILDDIR)/* install: clean html - rsync -avz _build/html/ code:www-pytest/2.0.0 + rsync -avz _build/html/ code:www-pytest/ html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html --- a/tox.ini +++ b/tox.ini @@ -2,20 +2,20 @@ distshare={homedir}/.tox/distshare envlist=py26,py27,py31,py32,py27-xdist,py25,py24 indexserver= - default = http://pypi.testrun.org - pypi = http://pypi.python.org/simple + default = http://pypi.python.org/simple + testrun = http://pypi.testrun.org [testenv] changedir=testing commands= py.test -rfsxX --junitxml={envlogdir}/junit-{envname}.xml [] deps= - :pypi:pexpect - :pypi:nose + pexpect + nose [testenv:genscript] changedir=. commands= py.test --genscript=pytest1 -deps=py>=1.4.0a2 +deps=py>=1.4.0 [testenv:py27-xdist] changedir=. @@ -28,7 +28,7 @@ commands= [testenv:trial] changedir=. basepython=python2.6 -deps=:pypi:twisted +deps=twisted commands= py.test -rsxf \ --junitxml={envlogdir}/junit-{envname}.xml [testing/test_unittest.py] @@ -41,14 +41,14 @@ deps= [testenv:doc] basepython=python changedir=doc -deps=:pypi:sphinx +deps=sphinx pytest commands= make html [testenv:py31] -deps=py>=1.4.0a2 +deps=py>=1.4.0 [testenv:py31-xdist] deps=pytest-xdist @@ -57,7 +57,7 @@ commands= --junitxml={envlogdir}/junit-{envname}.xml [] [testenv:py32] -deps=py>=1.4.0a2 +deps=py>=1.4.0 [testenv:pypy] basepython=pypy-c --- a/CHANGELOG +++ b/CHANGELOG @@ -1,4 +1,9 @@ -Changes between 1.3.4 and 2.0.0dev0 +Changes between 2.0.0 and 2.0.1.dev1 +---------------------------------------------- + +- refinements to terminal output + +Changes between 1.3.4 and 2.0.0 ---------------------------------------------- - pytest-2.0 is now its own package and depends on pylib-2.0 --- a/doc/conf.py +++ b/doc/conf.py @@ -49,7 +49,7 @@ copyright = u'2010, holger krekel et ali # built documents. # # The short X.Y version. -version = '2.0.0' +version = '2.0' # The full version, including alpha/beta/rc tags. import py, pytest assert py.path.local().relto(py.path.local(pytest.__file__).dirpath().dirpath()) --- a/_pytest/terminal.py +++ b/_pytest/terminal.py @@ -136,6 +136,9 @@ class TerminalReporter: self._tw.line() self.currentfspath = None + def write(self, content, **markup): + self._tw.write(content, **markup) + def write_line(self, line, **markup): line = str(line) self.ensure_newline() @@ -215,7 +218,7 @@ class TerminalReporter: def pytest_collection(self): if not self.hasmarkup: - self.write_line("collecting ...", bold=True) + self.write("collecting ... ", bold=True) def pytest_collectreport(self, report): if report.failed: --- a/pytest.py +++ b/pytest.py @@ -1,7 +1,7 @@ """ unit and functional testing with Python. """ -__version__ = '2.0.0' +__version__ = '2.0.1.dev1' __all__ = ['main'] from _pytest.core import main, UsageError, _preloadplugins --- a/setup.py +++ b/setup.py @@ -22,14 +22,14 @@ def main(): name='pytest', description='py.test: simple powerful testing with Python', long_description = long_description, - version='2.0.0', + version='2.0.1.dev1', url='http://pytest.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], author='holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others', author_email='holger at merlinux.eu', entry_points= make_entry_points(), - install_requires=['py>=1.4.0a2'], + install_requires=['py>=1.4.0'], classifiers=['Development Status :: 5 - Production/Stable', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', From commits-noreply at bitbucket.org Mon Nov 29 16:58:03 2010 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 29 Nov 2010 09:58:03 -0600 (CST) Subject: [py-svn] py commit 9265aa1ddd34: fixed and sent release announcement Message-ID: <20101129155803.256E11E12DB@bitbucket02.managed.contegix.com> # HG changeset patch -- Bitbucket.org # Project py # URL http://bitbucket.org/hpk42/py/overview # User holger krekel # Date 1291046273 -3600 # Node ID 9265aa1ddd34db756f6dc6d2de0ca7a5a0622032 # Parent f10f6036eee5ba4470ec2855755d7e0dba54940e fixed and sent release announcement --- a/doc/Makefile +++ b/doc/Makefile @@ -37,7 +37,7 @@ clean: -rm -rf $(BUILDDIR)/* install: clean html - rsync -avz _build/html/ code:www-pylib/1.4.0 + rsync -avz _build/html/ code:www-pylib/ html: $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html --- a/doc/install.txt +++ b/doc/install.txt @@ -17,7 +17,7 @@ installation info in a nutshell **hg repository**: https://bitbucket.org/hpk42/py -easy install +easy install or pip ``py`` ----------------------------- Both `Distribute`_ and setuptools_ provide the ``easy_install`` --- a/doc/announce/release-1.4.0.txt +++ b/doc/announce/release-1.4.0.txt @@ -1,22 +1,31 @@ .. _`release-1.4.0`: -py-1.4.0: lib for path, code, io, ... manipulations +py-1.4.0: cross-python lib for path, code, io, ... manipulations =========================================================================== "py" is a small library comprising APIs for filesystem and svn path -manipulations, dynamic code construction and introspection, IO capturing -and a Python2/Python3 compatibility namespace. It runs unmodified on -all Python interpreters compatible to Python2.4 up until Python 3.2. -The general goal with "py" is to provide stable APIs that are -continously tested against many Python interpreters and thus also to -help transition. +manipulations, dynamic code construction and introspection, a Py2/Py3 +compatibility namespace ("py.builtin"), IO capturing, terminal colored printing +(on windows and linux), ini-file parsing and a lazy import mechanism. +It runs unmodified on all Python interpreters compatible to Python2.4 up +until Python 3.2. The general goal with "py" is to provide stable APIs +for some common tasks that are continously tested against many Python +interpreters and thus also to help transition. Here are some docs: -The py distribution prior to 1.4.0 used to contain "py.test" which now -comes as its own "pytest" distribution. Also, the -"py.cleanup|py.lookup|py.countloc" etc. helpers are now part of pycmd. -This makes "py-1.4.0" a simple library which does not nstall any command -line utilities anymore. + http://pylib.org + +NOTE: The prior py-1.3.X versions contained "py.test" which now comes +as its own separate "pytest" distribution and was just released +as "pytest-2.0.0", see here for the revamped docs: + + http://pytest.org + +And "py.cleanup|py.lookup|py.countloc" etc. helpers are now part of +the pycmd distribution, see http://pypi.python.org/pypi/pycmd + +This makes "py-1.4.0" a simple library which does not install +any command line utilities anymore. cheers, holger --- /dev/null +++ b/doc/_templates/layout.html @@ -0,0 +1,18 @@ +{% extends "!layout.html" %} + +{% block footer %} +{{ super() }} + +{% endblock %}