From py-svn at codespeak.net Mon Jun 1 16:39:14 2009 From: py-svn at codespeak.net (py-svn at codespeak.net) Date: Mon, 1 Jun 2009 16:39:14 +0200 (CEST) Subject: [py-svn] For next week Message-ID: <20090601143914.8AC47169F7D@codespeak.net> An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Tue Jun 2 12:06:25 2009 From: py-svn at codespeak.net (py-svn at codespeak.net) Date: Tue, 2 Jun 2009 12:06:25 +0200 (CEST) Subject: [py-svn] BestBuy.com Deal of the Day Message-ID: <20090602100625.3AFEC169F32@codespeak.net> An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Tue Jun 2 18:19:08 2009 From: py-svn at codespeak.net (Alida) Date: Tue, 2 Jun 2009 18:19:08 +0200 (CEST) Subject: [py-svn] Why not in messenger? Message-ID: <522790959449287.DQAYAWYCCOZVIIY@75.112.166.190.f.sta.codetel.net.do> An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Wed Jun 3 10:01:56 2009 From: py-svn at codespeak.net (Jean) Date: Wed, 3 Jun 2009 10:01:56 +0200 (CEST) Subject: [py-svn] Avril's thin panties Message-ID: <126461323209813.UHGTPECSAXROPHW@cust979-89-31-8-170.isp.mcnet.cz> An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Thu Jun 4 04:54:28 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 04 Jun 2009 02:54:28 -0000 Subject: [py-svn] commit/py-3k: yangyan5: remove test_doctest_copy.py and revise test_support.py Message-ID: <20090604025428.13696.86008@domU-12-31-39-00-D4-C1.compute-1.internal> 1 new changeset in py-3k: http://www.bitbucket.org/yangyan5/py-3k/changeset/241ac6eb3864/ changeset: r1154:241ac6eb3864 user: yangyan5 date: 2009-06-04 04:53:43 summary: remove test_doctest_copy.py and revise test_support.py affected #: 2 files (114 bytes) Repository URL: http://bitbucket.org/yangyan5/py-3k/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Jun 4 20:36:59 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 04 Jun 2009 18:36:59 -0000 Subject: [py-svn] commit/py-3k: yangyan5: eliminate all warnings except True False warning when running 2.6 -3 Message-ID: <20090604183659.13696.20031@domU-12-31-39-00-D4-C1.compute-1.internal> 1 new changeset in py-3k: http://www.bitbucket.org/yangyan5/py-3k/changeset/d9ce4b2cc132/ changeset: r1155:d9ce4b2cc132 user: yangyan5 date: 2009-06-04 20:32:24 summary: eliminate all warnings except True False warning when running 2.6 -3 affected #: 8 files (287 bytes) Repository URL: http://bitbucket.org/yangyan5/py-3k/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Thu Jun 4 23:03:56 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 04 Jun 2009 21:03:56 -0000 Subject: [py-svn] commit/py-3k: yangyan5: eliminate True False warning when running 2.6 -3 Message-ID: <20090604210356.13696.49959@domU-12-31-39-00-D4-C1.compute-1.internal> 1 new changeset in py-3k: http://www.bitbucket.org/yangyan5/py-3k/changeset/ca150e887898/ changeset: r1156:ca150e887898 user: yangyan5 date: 2009-06-04 22:59:20 summary: eliminate True False warning when running 2.6 -3 affected #: 3 files (13 bytes) Repository URL: http://bitbucket.org/yangyan5/py-3k/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From py-svn at codespeak.net Fri Jun 5 15:27:26 2009 From: py-svn at codespeak.net (Phoebe) Date: Fri, 5 Jun 2009 15:27:26 +0200 (CEST) Subject: [py-svn] Excuse me for writing Message-ID: <154380398635641.RLAUBHFCBRNYGPE@45.Red-217-125-28.staticIP.rima-tde.net> An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Sat Jun 6 15:58:43 2009 From: py-svn at codespeak.net (Szumigala Alita) Date: Sat, 6 Jun 2009 15:58:43 +0200 (CEST) Subject: [py-svn] Exams results Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Sun Jun 7 10:36:00 2009 From: py-svn at codespeak.net (Rayna) Date: Sun, 7 Jun 2009 10:36:00 +0200 (CEST) Subject: [py-svn] Details of actors' death Message-ID: <725692866046184.EOEIMBGLQRJXJQU@[124.50.55.147]> An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Mon Jun 8 01:06:46 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Sun, 07 Jun 2009 23:06:46 -0000 Subject: [py-svn] commit/py-3k: yangyan5: make some modules 2to3able and add compiler package into compat Message-ID: <20090607230646.13696.6853@domU-12-31-39-00-D4-C1.compute-1.internal> 1 new changeset in py-3k: http://www.bitbucket.org/yangyan5/py-3k/changeset/17cdc5ffd6ff/ changeset: r1157:17cdc5ffd6ff user: yangyan5 date: 2009-06-08 01:06:18 summary: make some modules 2to3able and add compiler package into compat affected #: 21 files (201.8 KB) Repository URL: http://bitbucket.org/yangyan5/py-3k/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From py-svn at codespeak.net Mon Jun 8 11:03:10 2009 From: py-svn at codespeak.net (Madeleine) Date: Mon, 8 Jun 2009 11:03:10 +0200 (CEST) Subject: [py-svn] Complaint on you Message-ID: <331733065872398.SWLVCPOQKZOLCWO@[59.97.211.7]> An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Mon Jun 8 16:24:10 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 08 Jun 2009 14:24:10 -0000 Subject: [py-svn] commit/py-trunk: 4 new changesets Message-ID: <20090608142410.13696.38224@domU-12-31-39-00-D4-C1.compute-1.internal> 4 new changesets in py-trunk: http://www.bitbucket.org/hpk42/py-trunk/changeset/b65184087a6f/ changeset: r1144:b65184087a6f user: ra... at brainbot.com date: 2009-06-05 12:05:23 summary: adapt to new plugin architecture and misc cleanups: - exit with non-zero value on import errors - remove unused imports - make --twisted-logging a boolean option affected #: 1 file (751 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/626678034305/ changeset: r1145:626678034305 user: ra... at brainbot.com date: 2009-06-08 15:14:40 summary: restart system calls on SIGCHLD signals (needed for OS X) this only works in python >= 2.6. affected #: 1 file (287 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/4796f75ef7c5/ changeset: r1142:4796f75ef7c5 user: ra... at brainbot.com date: 2009-06-03 15:16:34 summary: fix typos in comments affected #: 1 file (2 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/20e64b3893a0/ changeset: r1143:20e64b3893a0 user: ra... at brainbot.com date: 2009-06-03 15:22:14 summary: fix generator tests affected #: 1 file (63 bytes) Repository URL: http://bitbucket.org/hpk42/py-trunk/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue Jun 9 16:14:22 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 09 Jun 2009 14:14:22 -0000 Subject: [py-svn] commit/py-trunk: 2 new changesets Message-ID: <20090609141422.13696.73201@domU-12-31-39-00-D4-C1.compute-1.internal> 2 new changesets in py-trunk: http://www.bitbucket.org/hpk42/py-trunk/changeset/2bd3da8d16c4/ changeset: r1146:2bd3da8d16c4 user: hpk date: 2009-06-08 18:31:10 summary: * rename, cleanup and document runtest hooks * factor runner code into pytest_runner plugin * cleanup setupstate handling affected #: 26 files (18.3 KB) http://www.bitbucket.org/hpk42/py-trunk/changeset/f09e4fc85c1f/ changeset: r1147:f09e4fc85c1f user: hpk date: 2009-06-09 16:08:34 summary: document and refine/rename item **runtest** hook invocations capture output separately for fixture and actual test run affected #: 13 files (3.3 KB) Repository URL: http://bitbucket.org/hpk42/py-trunk/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue Jun 9 17:06:15 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 09 Jun 2009 15:06:15 -0000 Subject: [py-svn] commit/py-trunk: hpk: * fix too-many-open files issue with fd based io-capturing Message-ID: <20090609150615.13696.94969@domU-12-31-39-00-D4-C1.compute-1.internal> 1 new changeset in py-trunk: http://www.bitbucket.org/hpk42/py-trunk/changeset/b5c750c0b558/ changeset: r1148:b5c750c0b558 user: hpk date: 2009-06-09 17:06:31 summary: * fix too-many-open files issue with fd based io-capturing * workaround 2.6 issue with pickling of objects from global conftest.py files affected #: 2 files (176 bytes) Repository URL: http://bitbucket.org/hpk42/py-trunk/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From py-svn at codespeak.net Wed Jun 10 16:45:21 2009 From: py-svn at codespeak.net (Zacate Micheal) Date: Wed, 10 Jun 2009 16:45:21 +0200 (CEST) Subject: [py-svn] You are disqualified Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Wed Jun 10 23:45:35 2009 From: py-svn at codespeak.net (Delaine) Date: Wed, 10 Jun 2009 23:45:35 +0200 (CEST) Subject: [py-svn] Contact me via cell Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Thu Jun 11 12:50:45 2009 From: py-svn at codespeak.net (Ramiro) Date: Thu, 11 Jun 2009 12:50:45 +0200 (CEST) Subject: [py-svn] Account was deleted Message-ID: An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Thu Jun 11 19:49:49 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 11 Jun 2009 17:49:49 -0000 Subject: [py-svn] commit/py-trunk: 6 new changesets Message-ID: <20090611174949.13696.62246@domU-12-31-39-00-D4-C1.compute-1.internal> 6 new changesets in py-trunk: http://www.bitbucket.org/hpk42/py-trunk/changeset/6e0a81cd31c7/ changeset: r1152:6e0a81cd31c7 user: hpk date: 2009-06-11 14:49:49 summary: remove unused function affected #: 2 files (355 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/906196b26e43/ changeset: r1153:906196b26e43 user: hpk date: 2009-06-11 18:23:32 summary: striking config from Node signature affected #: 5 files (284 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/6d297bccb45e/ changeset: r1154:6d297bccb45e user: hpk date: 2009-06-11 19:49:25 summary: * remove redundant call_next_provider method, thus generalizing Request object * refine according documentation affected #: 6 files (888 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/123b4f9c50eb/ changeset: r1149:123b4f9c50eb user: hpk date: 2009-06-09 17:28:50 summary: fix threading io-redirection tests affected #: 1 file (277 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/3928237c27da/ changeset: r1150:3928237c27da user: hpk date: 2009-06-11 14:48:42 summary: consolidate extension docs into a single file affected #: 7 files (6.9 KB) http://www.bitbucket.org/hpk42/py-trunk/changeset/4005b6d8d886/ changeset: r1151:4005b6d8d886 user: hpk date: 2009-06-11 14:48:53 summary: rename api to hookspec and use direct modules instead of class affected #: 7 files (5.3 KB) Repository URL: http://bitbucket.org/hpk42/py-trunk/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From py-svn at codespeak.net Fri Jun 12 03:32:48 2009 From: py-svn at codespeak.net (Lacy) Date: Fri, 12 Jun 2009 03:32:48 +0200 (CEST) Subject: [py-svn] Salary cut scheme Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Fri Jun 12 15:25:50 2009 From: py-svn at codespeak.net (py-svn at codespeak.net) Date: Fri, 12 Jun 2009 15:25:50 +0200 (CEST) Subject: [py-svn] Invitation: 06 June Message-ID: <20090612132550.DA799168539@codespeak.net> An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Sat Jun 13 13:12:17 2009 From: py-svn at codespeak.net (Donny Fleischhacker) Date: Sat, 13 Jun 2009 13:12:17 +0200 (CEST) Subject: [py-svn] Panties of Jolie Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Sun Jun 14 17:58:46 2009 From: py-svn at codespeak.net (Cesena Mickey) Date: Sun, 14 Jun 2009 17:58:46 +0200 (CEST) Subject: [py-svn] Please, leave comments Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Mon Jun 15 00:35:34 2009 From: py-svn at codespeak.net (Jadwiga Brutsch) Date: Mon, 15 Jun 2009 00:35:34 +0200 (CEST) Subject: [py-svn] This is administrator's mail Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Mon Jun 15 04:27:33 2009 From: py-svn at codespeak.net (Morton Roffe) Date: Mon, 15 Jun 2009 04:27:33 +0200 (CEST) Subject: [py-svn] Problems with computer Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Mon Jun 15 14:22:32 2009 From: py-svn at codespeak.net (Gorecki Trudi) Date: Mon, 15 Jun 2009 14:22:32 +0200 (CEST) Subject: [py-svn] Be incredible in performance Message-ID: An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Mon Jun 15 17:29:54 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 15 Jun 2009 15:29:54 -0000 Subject: [py-svn] commit/py-trunk: 2 new changesets Message-ID: <20090615152954.13696.60133@domU-12-31-39-00-D4-C1.compute-1.internal> 2 new changesets in py-trunk: http://www.bitbucket.org/hpk42/py-trunk/changeset/c6774f591e74/ changeset: r1155:c6774f591e74 user: hpk date: 2009-06-15 15:15:40 summary: * refine and rename pycollect related hooks * refine runtest/test function call protocol affected #: 11 files (1.2 KB) http://www.bitbucket.org/hpk42/py-trunk/changeset/f403575189c4/ changeset: r1156:f403575189c4 user: hpk date: 2009-06-15 17:28:55 summary: * refine collect hooks and docs, remove pytest_collect_recurse * write and extend extension docs affected #: 9 files (5.0 KB) Repository URL: http://bitbucket.org/hpk42/py-trunk/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From commits-noreply at bitbucket.org Tue Jun 16 12:04:18 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 16 Jun 2009 10:04:18 -0000 Subject: [py-svn] commit/py-3k: 2 new changesets Message-ID: <20090616100418.13696.61305@domU-12-31-39-00-D4-C1.compute-1.internal> 2 new changesets in py-3k: http://www.bitbucket.org/yangyan5/py-3k/changeset/b549a00d9cd6/ changeset: r1157:b549a00d9cd6 user: yangyan5 date: 2009-06-16 11:39:50 summary: keep py.test compatible with 2.4, 2.6, 3.0. still some failures in 3.0 affected #: 51 files (11.8 KB) http://www.bitbucket.org/yangyan5/py-3k/changeset/4fb7154d3d3f/ changeset: r1158:4fb7154d3d3f user: yangyan5 date: 2009-06-16 12:04:10 summary: merge with py-trunk affected #: 8 files (2.0 KB) Repository URL: http://bitbucket.org/yangyan5/py-3k/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From py-svn at codespeak.net Tue Jun 16 14:22:59 2009 From: py-svn at codespeak.net (Demetria Yxuvoben) Date: Tue, 16 Jun 2009 15:22:59 +0300 Subject: [py-svn] Saw that wiki thing? Message-ID: An HTML attachment was scrubbed... URL: From hpk at codespeak.net Tue Jun 16 18:23:22 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Tue, 16 Jun 2009 18:23:22 +0200 (CEST) Subject: [py-svn] r65791 - in py/trunk: . contrib/pytest_coverage contrib/pytest_twisted doc/test example/funcarg example/funcarg/costlysetup example/funcarg/costlysetup/sub1 example/funcarg/costlysetup/sub2 example/funcarg/lazysetup example/funcarg/lazysetup/sub1 example/funcarg/lazysetup/sub2 example/funcarg/mysetup example/funcarg/mysetup2 example/funcarg/parametrize example/pytest py py/code py/code/testing py/execnet py/execnet/testing py/io py/io/testing py/misc/testing py/test py/test/dist py/test/dist/testing py/test/looponfail py/test/plugin py/test/testing py/thread/testing Message-ID: <20090616162322.6FD3116847B@codespeak.net> Author: hpk Date: Tue Jun 16 18:23:18 2009 New Revision: 65791 Added: py/trunk/doc/test/extend.txt py/trunk/example/funcarg/costlysetup/ py/trunk/example/funcarg/costlysetup/conftest.py py/trunk/example/funcarg/costlysetup/sub1/ py/trunk/example/funcarg/costlysetup/sub1/test_quick.py py/trunk/example/funcarg/costlysetup/sub2/ py/trunk/example/funcarg/costlysetup/sub2/test_two.py py/trunk/example/funcarg/lazysetup/ py/trunk/example/funcarg/lazysetup/sub1/ py/trunk/example/funcarg/lazysetup/sub2/ py/trunk/example/funcarg/mysetup/__init__.py py/trunk/example/funcarg/mysetup2/ py/trunk/example/funcarg/mysetup2/__init__.py py/trunk/example/funcarg/mysetup2/conftest.py py/trunk/example/funcarg/mysetup2/myapp.py py/trunk/example/funcarg/mysetup2/test_sample.py py/trunk/example/funcarg/mysetup2/test_ssh.py py/trunk/example/funcarg/parametrize/ py/trunk/example/funcarg/parametrize/test_parametrize.py py/trunk/example/funcarg/parametrize/test_parametrize2.py py/trunk/example/funcarg/parametrize/test_parametrize3.py py/trunk/example/funcarg/test_simpleprovider.py py/trunk/py/test/plugin/hookspec.py py/trunk/py/test/plugin/pytest_hooklog.py py/trunk/py/test/plugin/test_pytest_runner.py Removed: py/trunk/doc/test/ext.txt py/trunk/doc/test/plugins.txt py/trunk/py/test/custompdb.py py/trunk/py/test/plugin/api.py py/trunk/py/test/plugin/pytest_eventlog.py py/trunk/py/test/plugin/pytest_plugintester.py py/trunk/py/test/runner.py py/trunk/py/test/testing/test_api.py py/trunk/py/test/testing/test_runner.py py/trunk/py/test/testing/test_runner_functional.py Modified: py/trunk/CHANGELOG py/trunk/MANIFEST py/trunk/TODO.txt py/trunk/contrib/pytest_coverage/__init__.py py/trunk/contrib/pytest_twisted/__init__.py py/trunk/doc/test/config.txt py/trunk/doc/test/examples.txt py/trunk/doc/test/features.txt py/trunk/doc/test/funcargs.txt py/trunk/doc/test/test.txt py/trunk/example/funcarg/mysetup/conftest.py py/trunk/example/pytest/test_failures.py py/trunk/py/LICENSE py/trunk/py/__init__.py py/trunk/py/_com.py py/trunk/py/code/source.py py/trunk/py/code/testing/test_source.py py/trunk/py/conftest.py py/trunk/py/execnet/testing/test_event.py py/trunk/py/execnet/testing/test_gwmanage.py py/trunk/py/execnet/testing/test_xspec.py py/trunk/py/execnet/xspec.py py/trunk/py/io/stdcapture.py py/trunk/py/io/terminalwriter.py py/trunk/py/io/testing/test_terminalwriter.py py/trunk/py/misc/testing/test_com.py py/trunk/py/test/collect.py py/trunk/py/test/config.py py/trunk/py/test/defaultconftest.py py/trunk/py/test/dist/dsession.py py/trunk/py/test/dist/testing/test_dsession.py py/trunk/py/test/dist/testing/test_mypickle.py py/trunk/py/test/dist/testing/test_nodemanage.py py/trunk/py/test/dist/testing/test_txnode.py py/trunk/py/test/dist/txnode.py py/trunk/py/test/funcargs.py py/trunk/py/test/looponfail/remote.py py/trunk/py/test/outcome.py py/trunk/py/test/plugin/conftest.py py/trunk/py/test/plugin/pytest__pytest.py py/trunk/py/test/plugin/pytest_default.py py/trunk/py/test/plugin/pytest_doctest.py py/trunk/py/test/plugin/pytest_execnetcleanup.py py/trunk/py/test/plugin/pytest_figleaf.py py/trunk/py/test/plugin/pytest_iocapture.py py/trunk/py/test/plugin/pytest_monkeypatch.py py/trunk/py/test/plugin/pytest_pdb.py py/trunk/py/test/plugin/pytest_pocoo.py py/trunk/py/test/plugin/pytest_pylint.py py/trunk/py/test/plugin/pytest_pytester.py py/trunk/py/test/plugin/pytest_recwarn.py py/trunk/py/test/plugin/pytest_restdoc.py py/trunk/py/test/plugin/pytest_resultdb.py py/trunk/py/test/plugin/pytest_resultlog.py py/trunk/py/test/plugin/pytest_runner.py py/trunk/py/test/plugin/pytest_terminal.py py/trunk/py/test/plugin/pytest_tmpdir.py py/trunk/py/test/plugin/pytest_unittest.py py/trunk/py/test/plugin/pytest_xfail.py py/trunk/py/test/pluginmanager.py py/trunk/py/test/pycollect.py py/trunk/py/test/session.py py/trunk/py/test/testing/acceptance_test.py py/trunk/py/test/testing/test_collect.py py/trunk/py/test/testing/test_config.py py/trunk/py/test/testing/test_conftesthandle.py py/trunk/py/test/testing/test_funcargs.py py/trunk/py/test/testing/test_genitems.py py/trunk/py/test/testing/test_outcome.py py/trunk/py/test/testing/test_parseopt.py py/trunk/py/test/testing/test_pickling.py py/trunk/py/test/testing/test_pluginmanager.py py/trunk/py/test/testing/test_pycollect.py py/trunk/py/test/testing/test_session.py py/trunk/py/test/testing/test_setup_functional.py py/trunk/py/test/testing/test_traceback.py py/trunk/py/thread/testing/test_io.py py/trunk/setup.py Log: merge mainline py-trunk to svn py/trunk for setup.py experiments Modified: py/trunk/CHANGELOG ============================================================================== --- py/trunk/CHANGELOG (original) +++ py/trunk/CHANGELOG Tue Jun 16 18:23:18 2009 @@ -1,6 +1,18 @@ $Id$ -Changes between 0.9.2 and 1.0 (UNRELEASED) +Changes between 1.0.0b1 and 1.0.0b2 +============================================= + +* plugin classes are removed: one now defines + hooks directly in conftest.py or global pytest_*.py + files. + +* documented and refined various hooks + +* added new style of generative tests via pytest_generate_tests + hook + +Changes between 0.9.2 and 1.0.0b1 ============================================= * introduced new "funcarg" setup method, Modified: py/trunk/MANIFEST ============================================================================== --- py/trunk/MANIFEST (original) +++ py/trunk/MANIFEST Tue Jun 16 18:23:18 2009 @@ -248,6 +248,7 @@ py/test/dist/testing/test_nodemanage.py py/test/dist/testing/test_txnode.py py/test/dist/txnode.py +py/test/funcargs.py py/test/looponfail/__init__.py py/test/looponfail/remote.py py/test/looponfail/testing/__init__.py @@ -259,12 +260,14 @@ py/test/plugin/__init__.py py/test/plugin/api.py py/test/plugin/conftest.py +py/test/plugin/hookspec.py py/test/plugin/pytest__pytest.py py/test/plugin/pytest_default.py py/test/plugin/pytest_doctest.py py/test/plugin/pytest_eventlog.py py/test/plugin/pytest_execnetcleanup.py py/test/plugin/pytest_figleaf.py +py/test/plugin/pytest_hooklog.py py/test/plugin/pytest_iocapture.py py/test/plugin/pytest_monkeypatch.py py/test/plugin/pytest_pdb.py @@ -272,6 +275,7 @@ py/test/plugin/pytest_pocoo.py py/test/plugin/pytest_pylint.py py/test/plugin/pytest_pytester.py +py/test/plugin/pytest_recwarn.py py/test/plugin/pytest_restdoc.py py/test/plugin/pytest_resultdb.py py/test/plugin/pytest_resultlog.py @@ -280,6 +284,7 @@ py/test/plugin/pytest_tmpdir.py py/test/plugin/pytest_unittest.py py/test/plugin/pytest_xfail.py +py/test/plugin/test_pytest_runner.py py/test/pluginmanager.py py/test/pycollect.py py/test/runner.py @@ -292,6 +297,7 @@ py/test/testing/import_test/package/module_that_imports_shared_lib.py py/test/testing/import_test/package/shared_lib.py py/test/testing/import_test/package/test_import.py +py/test/testing/test_api.py py/test/testing/test_collect.py py/test/testing/test_compat.py py/test/testing/test_config.py Modified: py/trunk/TODO.txt ============================================================================== --- py/trunk/TODO.txt (original) +++ py/trunk/TODO.txt Tue Jun 16 18:23:18 2009 @@ -4,111 +4,39 @@ py.test -------------- -- compatilibity: honour/warn item.run() method of test items (so far - probably only execute() is warned about or the other way round) +- clarify setup/run events and runner.py versus pytest_runner.py, + introduce a general pytest_item_setup(item, setupstate) + and always isolate py._com.comregistry when py lib's own tests are run -- introduce plugin arch, port existing things to plugins: - - importorskip - - filelog - - chtmpdir per method - - apigen - - xfail - - acceptance/assertlines - - dist-testing? - -- introduce setuptools-style version checking, at least - for py lib itself, maybe also for other packages: - - py.checkversion("py>=1.0") - -- generative tests: it is somewhat misleading for - the classical htmlviews that generated tests are identified - by numbers rather than by its parameters. unclear how - to fix this because we may not always be able to assume - that we can identify a generated tests by using its parameters - (it might not be hashable, doesn't have a sensical repr ...?) +- hook review and hook docs - turn deprecation / apiwarnings into events, report them at the end? -- get APIGEN back to work - -- get web reporter back to work - -- introduce decorator "shouldfail" or "xfail" - as to mark a test as "expected to fail", - report specially if it surprisingly passes - - nightly test runs on multiple platforms -- review and refactor architecture of py.test with particular - respect to: - - writing (stacked) extensions / plugins (compared to Nose) - - porting existing extensions (htmlconftest / buildbot / PyPy's conftest's ...) - - fast and stable distributed testing - - reliable cross-platform testing - -- improve py.test documentation to reflect new - event architecture - -- review and optimize skip-handling (it can be quite slow in - certain situations because e.g. setup/teardown is fully performed - although we have "skip by keyword" and could detect this early) - py.execnet -------------- - cross-python version (2.2/2.3-2.5/6) and cross-platform testing of setup/teardown semantics -- optimize general setup and rsync timing? +py.test apigen plugin +--------------------------- -py.apigen ----------------- +- make it work again with the new plugin arch -- make it work again +packaging / svn-mercurial interaction +-------------------------------------------- -see apigen_refactorings.txt +- decide if to go with or without setuptools, check windows 2.6 + availability -- check out CodeInvestigator - http://codeinvestigator.googlepages.com/main - - or other code that collects data from running a program - (in our case running the tests) - -Criticism and solutions --------------------------------- -"too big": - - too much code, you need entire py lib, hard to include into app - + have a small pytest boostrap that loads pylib.zip from net - + provide smaller script ala simpy - - lots of cmdline options, possibilities, documentation - rather unsorted - -"needless differences between py.test and nosetests": - - py.test.skip - - raises - -"tutorial structure missing", e.g.: - - "how to get started" in a minimal way, also how to use - existing conftests/plugins - - how to configure py.test - - how to write plugins/extensions - -"too much magic" - - re-execution of assert expressions - + rename "--nomagic" to something that turns off "superassertions" - + hint at --tb=... - - get rid of py/magic directory - -has a good ui but could be better - - support developer communication, e.g. py.test - --sendfailures=freenode-pypy - --sendfailures=pocoo # prints out paste.pocoo.url with traceback - - graphical interface, probably QT - - generally store test results and use them for subsequent calls +- open a mercurial branch for releases? +- write a script to dumb-bridge the mercurial repo to svn + (i.e. forget about svn history) -ld (review and shift to above) +1.1 and beyond ================================= refactorings @@ -119,16 +47,6 @@ - check if it works on win32 - refine error reporting (don't show python tracebacks) -- generalization of "host specifications" for execnet and - py.test --dist usages in particular (see also revision 37500 which - contained a draft for that). The goal is to have cross-platform - testing and dist-testing and other usages of py.execnet all - use a common syntax for specifiying connection methods and - be able to instantiate gateways/connections through it. - -- unification of "gateway"/host setup and teardown, including - rsyncing, i.e. cross-platform and dist-testing. - - py.log: unify API, possibly deprecate duplicate ones, base things on a Config object (hte latter almost a feature though) (M988) @@ -139,7 +57,9 @@ -------------- - (Harald Armin Massa): make py2exe work with py lib + - optimize file checking with --looponfailing (harald has code for win32) + - have a py.test scan/run database for results and test names etc. (to allow quicker selection of tests and post-run information on failures etc.) (M760) @@ -168,116 +88,8 @@ - references from ReST docs to modules, functions and classes of apigen generated html docs (M960) -- review svn-testing (and escape characters), consider - svn-bindings (M634) - - py.test.pdb - there is my hack for a while now, which integrates rlcompleter2 with pdb. First of all it requires some strange changes to rlcompleter itself, which has no tests. Long-term plan would be to have pyrepl+rlcompleter2+pdb fixes integrated into pylib and have it tested. This requires work though. - -xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx ---- below neeeds more review --- -xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx - -More random notes, goals --------------------------------- -- REDUCE "MAGICALNESS", from an IRC discussion with ronny: - - integration into IDEs - - python2.6/python3 compat? - - initpkg exports - - assert reinterpretation - - greenlet compiles at runtime only in "dev-mode" - - conftest's are "scary", hum, what about - -nicefications: -looponfailing shoudl nicely signal tests that failed but now PASS tests -rename Node to itemtestloop -refactor config and session tests to go into test_distsession.py -raises DID NOT RAISE: report the return value -have node.shutdown perform out-of-band so that shutdowns happens more quickly -tracebacks of importerrors of test modules should start with the test module file - -- fix hostmanage to care for setting PYTHONPATH properly - -- BRANCH: - adding of options - -- BRANCH: - TEMPDIR handling - syspath handling (notify on changes, restore for each test?) - -- COMPLETE REPORTING FOR MERGE! - - remove ItemStart and CollectionStart - which are only needed for collectonly. - implement it some other way. - - - pre-counting of test items - - - move assert reinterp back to session? - - - merge "--tb" and "--fulltrace" option, --tb=full - - implement --showouterr, don't show outerr by default - - - domainpath? - - - translate remote filenames to local filenames so that, - probably based on option - - - review safe_repr - - - move OutcomeRepr.where/exconly attr to ReprExceptionInfo - or substitute as LocationRepr? - -- reprcrash rename message to exconly - -- test terminal reporter - ACCEPTANCE test for eventlog writing - ACCEPTANCE test for acceptance custom reporting :) - - ACCEPTANCE showing nice Collection Errors - - ACCEPTANCE test for "py.test2" exit signals - ACCEPTANCE test for "py.test2 --traceconfig" - ACCEPTANCE test for nice reprsentation of failures during Collection - ACCEPTANCE test for nice reprsentation of failures during Generator Collection - ACCEPTANCE test for "py.test2" conftest containing syntax errors - - ACCEPTANCE test for "py.test2" honouring conftest specifying "extrainfo" - ACCEPTANCE test for "py.test2" on a simple example project - -- merge CollectionFinish and ItemTestReport - maybe with base class: BaseReport and some common attrs/methods - -- merge terminal/remote and dist-testing - and make allocation of tests to hosts more dynamic - -- time setup/teardown and the actual test runs separately - ACCEPTANCE: - py.test2 -- - py.test2 --repeat=10 - -- expect failing tests - py.test2.expectfail(feature=138) - - -while killing a process: -Exception in thread receiver: -Traceback (most recent call last): - File "threading.py", line 460, in __bootstrap - self.run() - File "threading.py", line 440, in run - self.__target(*self.__args, **self.__kwargs) - File "/home/hpk/py/branch/event/py/execnet/gateway.py", line 140, in _thread_receiver - self._stopsend() - File "/home/hpk/py/branch/event/py/execnet/gateway.py", line 329, in _stopsend - self._send(None) - File "/home/hpk/py/branch/event/py/execnet/gateway.py", line 147, in _send - self._io.close_write() - File "/home/hpk/py/branch/event/py/execnet/inputoutput.py", line 106, in close_write - self.outfile.close() -IOError: [Errno 32] Broken pipe - - - Modified: py/trunk/contrib/pytest_coverage/__init__.py ============================================================================== --- py/trunk/contrib/pytest_coverage/__init__.py (original) +++ py/trunk/contrib/pytest_coverage/__init__.py Tue Jun 16 18:23:18 2009 @@ -327,22 +327,3 @@ self.coverage.start() -# =============================================================================== -# plugin tests -# =============================================================================== -# XXX -''' -def test_generic(plugintester): - plugintester.apicheck(EventlogPlugin) - - testdir = plugintester.testdir() - testdir.makepyfile(""" - def test_pass(): - pass - """) - testdir.runpytest("--eventlog=event.log") - s = testdir.tmpdir.join("event.log").read() - assert s.find("TestrunStart") != -1 - assert s.find("ItemTestReport") != -1 - assert s.find("TestrunFinish") != -1 -''' Modified: py/trunk/contrib/pytest_twisted/__init__.py ============================================================================== --- py/trunk/contrib/pytest_twisted/__init__.py (original) +++ py/trunk/contrib/pytest_twisted/__init__.py Tue Jun 16 18:23:18 2009 @@ -1,27 +1,27 @@ """ -Notes: twisted's asynchrone behavior may have influence on the order of test-functions +Allows to test twisted applications with pytest. + +Notes: twisted's asynchronous behavior may have influence on the order of test-functions TODO: + credits to Ralf Schmitt See: http://twistedmatrix.com/pipermail/twisted-python/2007-February/014872.html + get test to work """ -import os import sys -import py - try: - from twisted.internet.defer import Deferred + from twisted.internet import reactor, defer + from twisted.python import failure, log except ImportError: print "To use the twisted option you have to install twisted." - sys.exit(0) + sys.exit(10) try: from greenlet import greenlet except ImportError: print "Since pylib 1.0 greenlet are removed and separately packaged: " \ "http://pypi.python.org/pypi/greenlet" - sys.exit(0) + sys.exit(10) def _start_twisted_logging(): @@ -35,18 +35,26 @@ def flush(self): sys.stdout.flush() # sys.stdout will be changed by py.test later. - import twisted.python.log - twisted.python.log.startLogging(Logger(), setStdout=0) + log.startLogging(Logger(), setStdout=0) def _run_twisted(logging=False): """Start twisted mainloop and initialize recursive calling of doit().""" - from twisted.internet import reactor, defer - from twisted.python import log, failure # make twisted copy traceback... failure.Failure.cleanFailure = lambda *args: None if logging: _start_twisted_logging() + + def fix_signal_handling(): + # see http://twistedmatrix.com/trac/ticket/733 + import signal + if hasattr(signal, "siginterrupt"): + signal.siginterrupt(signal.SIGCHLD, False) + + def start(): + fix_signal_handling() + doit(None) + # recursively called for each test-function/method due done() def doit(val): # val always None # switch context to wait that wrapper() passes back to test-method @@ -63,50 +71,34 @@ # the test-function *may* return a deferred # here the test-function will actually been called - # done() is finalizing a test-process by assureing recursive envoking + # done() is finalizing a test-process by assuring recursive invoking # of doit() defer.maybeDeferred(res).addCallback(done).addErrback(err) - # initialy preparing the calling of doit() and starting the reactor - reactor.callLater(0.0, doit, None) + # initially preparing the calling of doit() and starting the reactor + reactor.callLater(0.0, start) reactor.run() - -class TwistedPlugin: - """Allows to test twisted applications with pytest.""" - - def pytest_addoption(self, parser): - #parser.addoption("--twisted", dest="twisted", - # help="Allows to test twisted applications with pytest.") - - group = parser.addgroup('twisted options') - group.addoption('-T', action='store_true', default=False, - dest = 'twisted', - help="Allows to test twisted applications.") - group.addoption('--twisted-logging', action='store', default=False, - dest='twisted_logging', - help="switch on twisted internal logging") - self.twisted = False - - def pytest_configure(self, config): - twisted = config.getvalue("twisted") - twisted_logging = config.getvalue("twisted_logging") - if twisted: - self.twisted = True - gr_twisted.switch(twisted_logging) - - def pytest_unconfigure(self, config): - if self.twisted: - gr_twisted.switch(None) - - def pytest_pyfunc_call(self, pyfuncitem, *args, **kwargs): - if self.twisted: - # XXX1 kwargs? - # XXX2 we want to delegate actual call to next plugin - # (which may want to produce test coverage, etc.) - res = gr_twisted.switch(lambda: pyfuncitem.obj(*args)) - if res: - res.raiseException() - return True # indicates that we performed the function call +def pytest_addoption(parser): + group = parser.addgroup('twisted options') + group.addoption('--twisted-logging', action='store_true', default=False, + dest='twisted_logging', + help="switch on twisted internal logging") + +def pytest_configure(config): + twisted_logging = config.getvalue("twisted_logging") + gr_twisted.switch(twisted_logging) + +def pytest_unconfigure(config): + gr_twisted.switch(None) + +def pytest_pyfunc_call(pyfuncitem): + # XXX1 kwargs? + # XXX2 we want to delegate actual call to next plugin + # (which may want to produce test coverage, etc.) + res = gr_twisted.switch(lambda: pyfuncitem.call()) + if res: + res.raiseException() + return True # indicates that we performed the function call gr_twisted = greenlet(_run_twisted) gr_tests = greenlet.getcurrent() @@ -115,10 +107,7 @@ # plugin tests # =============================================================================== -def test_generic(plugintester): - plugintester.apicheck(TwistedPlugin) - - testdir = plugintester.testdir() +def test_generic(testdir): testdir.makepyfile(''' def test_pass(): pass Modified: py/trunk/doc/test/config.txt ============================================================================== --- py/trunk/doc/test/config.txt (original) +++ py/trunk/doc/test/config.txt Tue Jun 16 18:23:18 2009 @@ -1,13 +1,58 @@ Test configuration ======================== -test options and values +available test options ----------------------------- -You can see all available command line options by running:: +You can see command line options by running:: py.test -h +This will display all available command line options +including the ones added by plugins `loaded at tool startup`_. + +.. _`loaded at tool startup`: extend.html#tool-startup + +.. _conftestpy: +.. _collectignore: + +conftest.py: project specific test configuration +-------------------------------------------------------- + +A unique feature of py.test are its powerful ``conftest.py`` files which +allow to `set option defaults`_, `implement hooks`_, `specify funcargs`_ +or set particular variables to influence the testing process: + +* ``pytest_plugins``: list of named plugins to load + +* ``collect_ignore``: list of paths to ignore during test collection (relative to the containing + ``conftest.py`` file) + +* ``rsyncdirs``: list of to-be-rsynced directories for distributed + testing + +You may put a conftest.py files in your project root directory or into +your package directory if you want to add project-specific test options. + +``py.test`` loads all ``conftest.py`` files upwards from the command +line specified test files. It will lookup configuration values +right-to-left, i.e. the closer conftest files will be checked first. +You may have a ``conftest.py`` in your very home directory to have some +global configuration values. + +There is a flag that may help you debugging your conftest.py +configuration:: + + py.test --traceconfig + +.. _`implement hooks`: extend.html#conftest.py-plugin +.. _`specify funcargs`: funcargs.html#application-setup-tutorial-example + +.. _`set option defaults`: + +setting option defaults +------------------------------- + py.test will lookup values of options in this order: * option value supplied at command line @@ -16,12 +61,11 @@ The name of an option usually is the one you find in the longform of the option, i.e. the name -behind the ``--`` double-dash. +behind the ``--`` double-dash that you get with ``py.test -h``. IOW, you can set default values for options per project, per home-directoray, per shell session or per test-run. - .. _`basetemp`: per-testrun temporary directories Modified: py/trunk/doc/test/examples.txt ============================================================================== --- py/trunk/doc/test/examples.txt (original) +++ py/trunk/doc/test/examples.txt Tue Jun 16 18:23:18 2009 @@ -1,3 +1,23 @@ +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 Working Examples ================ Deleted: /py/trunk/doc/test/ext.txt ============================================================================== --- /py/trunk/doc/test/ext.txt Tue Jun 16 18:23:18 2009 +++ (empty file) @@ -1,126 +0,0 @@ -====================================== -Writing plugins and extensions -====================================== - - -.. _`local plugin`: - -Local Plugins -================================== - -You can easily specify a project-specific or "local" -plugin by defining a ``ConftestPlugin`` in a ``conftest.py`` -file like this:: - - class ConftestPlugin: - """ my local plugin. """ - - -Learning by examples -===================== - -XXX - -adding custom options ----------------------- - -py.test supports adding of standard optparse_ Options. -A plugin may implement the ``addoption`` hook for registering -custom options:: - - class ConftestPlugin: - def pytest_addoption(self, parser): - parser.addoption("-M", "--myopt", action="store", - help="specify string to set myopt") - - def pytest_configure(self, config): - if config.option.myopt: - # do action based on option value - -.. _optparse: http://docs.python.org/library/optparse.html - -Setting default values for test options -======================================= - -You can see all available command line options by running:: - - py.test -h - -py.test will lookup values of options in this order: - -* option value supplied at command line -* content of environment variable ``PYTEST_OPTION_NAME=...`` -* ``name = ...`` setting in the nearest ``conftest.py`` file. - -The name of an option usually is the one you find -in the longform of the option, i.e. the name -behind the ``--`` double-dash. - -IOW, you can set default values for options per project, per -home-directoray, per shell session or per test-run. - -.. _`collection process`: - -Test Collection process -====================================================== - -The collecting process is iterative so that distribution -and execution of tests can start as soon as the first test -item is collected. Collection nodes with children are -called "Collectors" and terminal nodes are called "Items". -Here is an example of such a tree, generated with the -command ``py.test --collectonly py/xmlobj``:: - - - - - - - - - - - - - - - - -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. - -.. _`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`` - -* determine the fully qualified name for ``fspath`` by either: - - * calling ``root.__pkg__.getimportname(fspath)`` if the - ``__pkg__`` exists.` or - - * otherwise use the relative path of the module path to - the base dir and turn slashes into dots and strike - the trailing ``.py``. - - -Plugin hooks and events -======================================= - -XXX Added: py/trunk/doc/test/extend.txt ============================================================================== --- (empty file) +++ py/trunk/doc/test/extend.txt Tue Jun 16 18:23:18 2009 @@ -0,0 +1,275 @@ +================================================ +Extending and customizing py.test +================================================ + +.. _`local plugin`: + +py.test implements much of its functionality by calling `well specified +hooks`_. Python modules which contain such hook functions are called +plugins. Hook functions are discovered in ``conftest.py`` files or +in **named** plugins. ``conftest.py`` files are sometimes called "anonymous" +or "local" plugins if they define hooks. Named plugins are python modules +or packages that have an all lowercase ``pytest_`` prefixed name and who +are imported during tool startup or the testing process. + +.. _`tool startup`: + +Plugin discovery at tool startup +-------------------------------------------- + +py.test loads plugin modules at tool startup in the following way: + +* by reading the ``PYTEST_PLUGINS`` environment variable + and importing the comma-separated list of named plugins. + +* by pre-scanning the command line for the ``-p name`` option + and loading the specified plugin before actual command line parsing. + +* by loading all `conftest.py plugin`_ files as inferred by the command line + invocation + +* by recursively loading all plugins specified by the + ``pytest_plugins`` variable in a ``conftest.py`` file + +Note that at tool startup only ``conftest.py`` files in +the directory of the specified test modules (or the current dir if None) +or any of the parent directories are found. There is no try to +pre-scan all subdirectories to find ``conftest.py`` files or test +modules. + +Specifying plugins in a test module or plugin +----------------------------------------------- + +You can specify plugins in a test module or a plugin like this: + +.. sourcecode:: python + + pytest_plugins = "name1", "name2", + +When the test module or plugin is loaded the specified plugins +will be loaded. If you specify plugins without the ``pytest_`` +prefix it will be automatically added. All plugin names +must be lowercase. + +.. _`conftest.py plugin`: +.. _`conftestplugin`: + +conftest.py as anonymous per-project plugins +-------------------------------------------------- + +The purpose of ``conftest.py`` files is to allow `project-specific +test configuration`_. But they also make for a good place to implement +project-specific test related features through hooks. For example you may +set the `collect_ignore`_ variable depending on a command line option +by defining the following hook in a ``conftest.py`` file: + +.. _`exclude-file-example`: + +.. sourcecode:: python + + # ./conftest.py in your root or package dir + collect_ignore = ['hello', 'test_world.py'] + def pytest_addoption(parser): + parser.addoption("--runall", action="store_true", default=False) + def pytest_configure(config): + if config.getvalue("runall"): + collect_ignore[:] = [] + +.. _`project-specific test configuration`: config.html#conftestpy +.. _`collect_ignore`: config.html#collectignore + +.. _`well specified hooks`: + +Available py.test hooks +==================================== + +py.test calls hooks functions to implement its `test collection`_, running and +reporting process. Upon loading of a plugin py.test performs +strict checking on contained hook functions. Function and argument names +need to match exactly the `original definition of the hook`_. It thus +provides useful error reporting on mistyped hook or argument names +and minimizes version incompatibilites. + +.. _`original definition of the hook`: http://bitbucket.org/hpk42/py-trunk/src/tip/py/test/plugin/hookspec.py + +generic "runtest" hooks +------------------------------ + +Each test item is usually executed by calling the following three hooks: + +.. sourcecode:: python + + pytest_runtest_setup(item) + pytest_runtest_call(item) + pytest_runtest_teardown(item) + +For each of the three invocations a `call object`_ encapsulates +information about the outcome of the call and is subsequently used +to make a report object: + +.. sourcecode:: python + + report = hook.pytest_runtest_makereport(item, call) + +For example, the `pytest_pdb plugin`_ uses this hook to activate +interactive debugging on failures when ``--pdb`` is specified on the +command line. + +Usually three reports will be generated for a single test item. However, +if the ``pytest_runtest_setup`` fails no call or teardown hooks +will be called and only one report will be created. + +Each of the up to three reports is eventually fed to the logreport hook: + +.. sourcecode:: python + + pytest_runtest_logreport(report) + +A ``report`` object contains status and reporting information: + +.. sourcecode:: python + + report.longrepr = string/lines/object to print + report.when = "setup", "call" or "teardown" + report.shortrepr = letter for progress-report + report.passed = True or False + report.failed = True or False + report.skipped = True or False + +The `pytest_terminal plugin`_ uses this hook to print information +about a test run. + +The protocol described here is implemented via this hook: + +.. sourcecode:: python + + pytest_runtest_protocol(item) -> True + +.. _`call object`: + +The call object contains information about a performed call: + +.. sourcecode:: python + + call.excinfo = ExceptionInfo object or None + call.when = "setup", "call" or "teardown" + call.outerr = None or tuple of strings representing captured stdout/stderr + +.. _`pytest_pdb plugin`: http://bitbucket.org/hpk42/py-trunk/src/tip/py/test/plugin/pytest_pdb.py +.. _`pytest_terminal plugin`: http://bitbucket.org/hpk42/py-trunk/src/tip/py/test/plugin/pytest_terminal.py + + +generic collection hooks +------------------------------ + +py.test calls the following two fundamental hooks for collecting files and directories: + +.. sourcecode:: python + + def pytest_collect_directory(path, parent): + """ return Collection node or None for the given path. """ + + def pytest_collect_file(path, parent): + """ return Collection node or None for the given path. """ + +Both return a `collection node`_ for a given path. All returned +nodes from all hook implementations will participate in the +collection and running protocol. The ``parent`` object is +the parent node and may be used to access command line +options via the ``parent.config`` object. + + +Python specific test function and module hooks +---------------------------------------------------- + +For influencing the collection of objects in Python modules +you can use the following hook: + +.. sourcecode:: python + + pytest_pycollect_makeitem(collector, name, obj) + +This hook will be called for each Python object in a collected +Python module. The return value is a custom `collection node`_. + + + +.. XXX or ``False`` if you want to indicate that the given item should not be collected. + + + +Included default plugins +============================= + +You can find the source code of all default plugins in + + http://bitbucket.org/hpk42/py-trunk/src/tip/py/test/plugin/ + +Additionally you can check out some more contributed plugins here + + http://bitbucket.org/hpk42/py-trunk/src/tip/contrib/ + + +.. _`collection process`: +.. _`collection node`: +.. _`test collection`: + + +Test Collection process +====================================================== + +The collecting process is iterative so that distribution +and execution of tests can start as soon as the first test +item is collected. Collection nodes with children are +called "Collectors" and terminal nodes are called "Items". +Here is an example of such a tree, generated with the +command ``py.test --collectonly py/xmlobj``:: + + + + + + + + + + + + + + + + +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. + +.. _`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`` + +* determine the fully qualified name for ``fspath`` by either: + + * calling ``root.__pkg__.getimportname(fspath)`` if the + ``__pkg__`` exists.` or + + * otherwise use the relative path of the module path to + the base dir and turn slashes into dots and strike + the trailing ``.py``. + Modified: py/trunk/doc/test/features.txt ============================================================================== --- py/trunk/doc/test/features.txt (original) +++ py/trunk/doc/test/features.txt Tue Jun 16 18:23:18 2009 @@ -30,13 +30,8 @@ From each test module every function with a leading ``test_`` or class with a leading ``Test`` name is collected. -.. _`generative tests`: -.. _`collection process`: ext.html#collection-process - -Rapidly write integration, functional, unit tests -=================================================== +.. _`collection process`: extend.html#collection-process -XXX funcargs and xUnit style setups =================================================== @@ -96,17 +91,20 @@ ---------------------------------- By default, ``py.test`` catches text written to stdout/stderr during -the execution of each individual test. This output will only be +the execution of each individual test. This output will only be displayed however if the test fails; you will not see it otherwise. 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. Each failing test that produced output during the running of the test -will have its output displayed in the ``recorded stdout`` section. +function will have its output displayed in the ``recorded stdout`` section. + +During Setup and Teardown ("Fixture") capturing is performed separately so +that you will only see this output if the actual fixture functions fail. The catching of stdout/stderr output can be disabled using the -``--nocapture`` option to the ``py.test`` tool. Any output will +``--nocapture`` or ``-s`` option to the ``py.test`` tool. Any output will in this case be displayed as soon as it is generated. test execution order @@ -259,11 +257,17 @@ def test_xxx(self): ... +.. _`test generators`: funcargs.html#test-generators + +.. _`generative tests`: + generative tests: yielding parametrized tests ==================================================== +Deprecated since 1.0 in favour of `test generators`_. + *Generative tests* are test methods that are *generator functions* which -``yield`` callables and their arguments. This is most useful for running a +``yield`` callables and their arguments. This is useful for running a test function multiple times against different parameters. Example:: def test_generative(): @@ -283,19 +287,19 @@ for x in (42,17,49): yield "case %d" % x, check, x -extensible plugin system +easy to extend ========================================= -py.test itself consists of many plugins -and you can easily write new `py.test plugins`_ -for these purposes: +Since 1.0 py.test has advanced `extension mechanisms`_. +One can can easily modify or add aspects for for +purposes such as: * reporting extensions -* customizing collection and run of tests +* customizing collection and execution of tests * running non-python tests -* managing test state setup +* managing custom test state setup -.. _`py.test plugins`: plugins.html +.. _`extension mechanisms`: extend.html .. _`reStructured Text`: http://docutils.sourceforge.net .. _`Python debugger`: http://docs.python.org/lib/module-pdb.html Modified: py/trunk/doc/test/funcargs.txt ============================================================================== --- py/trunk/doc/test/funcargs.txt (original) +++ py/trunk/doc/test/funcargs.txt Tue Jun 16 18:23:18 2009 @@ -1,209 +1,314 @@ ====================================================== -**funcargs**: powerful test setup and parametrization +**funcargs**: test setup and parametrization ====================================================== -Since version 1.0 it is possible to provide arguments to test functions, -often called "funcargs". The funcarg mechanisms were developed with -these goals in mind: - -* **no boilerplate**: cleanly encapsulate test setup and fixtures -* **flexibility**: easily setup test state depending on command line options or environment -* **readability**: write simple to read and debug test functions -* **parametrizing tests**: run a test function multiple times with different parameters - - -.. contents:: Contents: - :depth: 2 - -Basic mechanisms by example -============================================= - -providing single function arguments as needed ---------------------------------------------------------- - -Let's look at a simple example of using funcargs within a test module: +Since version 1.0 py.test introduces test function arguments, +in short "funcargs" for your Python test functions. The basic idea +that your unit-, functional- or acceptance test functions can name +arguments and py.test will discover a matching provider from your +test configuration. The mechanism complements the automatic +discovery of test files, classes and functions which follows +the `Convention over Configuration`_ strategy. By discovering and +calling functions ("funcarg providers") that provide values for your +actual test functions it becomes easy to: + +* separate test function code from test state setup/fixtures +* manage test value setup and teardown depending on + command line options or configuration +* parametrize multiple runs of the same test functions +* present useful debug info if setting up test state goes wrong + +Using funcargs, test functions become more expressive, +more "templaty" and more test-aspect oriented. In fact, +funcarg mechanisms are meant to be complete and +convenient enough to + +* substitute and improve on most usages of `xUnit style`_ setup. + For a simple example of how funcargs compare + to xUnit setup, see the `blog post about + the monkeypatch funcarg`_. + +* substitute and improve on all usages of `old-style generative tests`_, + i.e. test functions that use the "yield" statement. + Using yield in test functions is deprecated since 1.0. + + +.. _`blog post about the monkeypatch funcarg`: http://tetamap.wordpress.com/2009/03/03/monkeypatching-in-unit-tests-done-right/ +.. _`xUnit style`: xunit_setup.html +.. _`old-style generative tests`: features.html#generative-tests + +.. _`funcarg provider`: + +funcarg providers: setting up test function arguments +============================================================== + +Test functions can specify one ore more arguments ("funcargs") +and a test module or plugin can define functions that provide +the function argument. Let's look at a simple self-contained +example that you can put into a test module: .. sourcecode:: python + # ./test_simpleprovider.py def pytest_funcarg__myfuncarg(request): return 42 def test_function(myfuncarg): - assert myfuncarg == 42 + assert myfuncarg == 17 -1. To setup the running of the ``test_function()`` call, py.test - looks up a provider for the ``myfuncarg`` argument. - The provider method is recognized by its ``pytest_funcarg__`` prefix - followed by the requested function argument name. - The `request object`_ gives access to test context. +If you run this with ``py.test test_simpleprovider.py`` you see something like this: -2. A ``test_function(42)`` call is executed. If the test fails - one can see the original provided value. +.. sourcecode:: python + ============================ test session starts ============================ + python: platform linux2 -- Python 2.6.2 + test object 1: /home/hpk/hg/py/trunk/example/funcarg/test_simpleprovider.py -generating test runs with multiple function argument values ----------------------------------------------------------------------- + test_simpleprovider.py F -You can parametrize multiple runs of a test function by -providing multiple values for function arguments. Here -is an example for running the same test function three times. + ================================= FAILURES ================================== + _______________________________ test_function _______________________________ -.. sourcecode:: python + myfuncarg = 42 - def pytest_genfuncruns(runspec): - if "arg1" in runspec.funcargnames: - runspec.addfuncarg("arg1", 10) - runspec.addfuncarg("arg1", 20) - runspec.addfuncarg("arg1", 30) + def test_function(myfuncarg): + > assert myfuncarg == 17 + E assert 42 == 17 - def test_function(arg1): - assert myfuncarg in (10, 20, 30) + test_simpleprovider.py:6: AssertionError + ========================= 1 failed in 0.11 seconds ========================== -Here is what happens: -1. The ``pytest_genfuncruns()`` hook will be called once for each test - function. The if-statement makes sure that we only add function - arguments (and runs) for functions that need it. The `runspec object`_ - provides access to context information. +This means that the test function got executed and the assertion failed. +Here is how py.test comes to execute this test function: -2. Subsequently the ``test_function()`` will be called three times - with three different values for ``arg1``. +1. py.test discovers the ``test_function`` because of the ``test_`` prefix. + The test function needs a function argument named ``myfuncarg``. + A matching provider function is discovered by looking for the special + name ``pytest_funcarg__myfuncarg``. -Funcarg rules and support objects -==================================== +2. ``pytest_funcarg__myfuncarg(request)`` is called and + returns the value for ``myfuncarg``. -.. _`request object`: +3. ``test_function(42)`` call is executed. -funcarg request objects ------------------------- +Note that if you misspell a function argument or want +to use one that isn't available, an error with a list of +available function argument is provided. + +For more interesting provider functions that make good use of the +`request object`_ please see the `application setup tutorial example`_. -Request objects encapsulate a request for a function argument from a -specific test function. Request objects provide access to command line -options, the underlying python function and allow interaction -with other providers and the test running process. +.. _`request object`: -Attributes of request objects -++++++++++++++++++++++++++++++++++++++++ +funcarg request objects +------------------------------------------ -``request.argname``: name of the requested function argument +Request objects are passed to funcarg providers. They +encapsulate a request for a function argument for a +specific test function. Request objects allow providers +to access test configuration and test context: ``request.function``: python function object requesting the argument ``request.cls``: class object where the test function is defined in or None. -``runspec.module``: module object where the test function is defined in. +``request.module``: module object where the test function is defined in. ``request.config``: access to command line opts and general config +``request.param``: if exists was passed by a `parametrizing test generator`_ + + +perform scoped setup and teardown +--------------------------------------------- + +.. sourcecode:: python + + def cached_setup(setup, teardown=None, scope="module", keyextra=None): + """ cache and return result of calling setup(). + + The scope determines the cache key and ``keyextra`` adds to the cachekey. + The scope also determines when teardown(result) will be called. + valid scopes: + scope == 'function': when the single test function run finishes. + scope == 'module': when tests in a different module are run + scope == 'session': when tests of the session have run. + """ + +example for providing a value that is to be setup only once during a test run: + +.. sourcecode:: python + + def pytest_funcarg__db(request): + return request.cached_setup( + lambda: ExpensiveSetup(request.config.option.db), + lambda val: val.close(), + scope="run" + ) + + cleanup after test function execution -++++++++++++++++++++++++++++++++++++++++ +--------------------------------------------- + +.. sourcecode:: python -Request objects allow to **register a finalizer method** which is -called after a test function has finished running. -This is useful for tearing down or cleaning up -test state. Here is a basic example for providing -a ``myfile`` object that will be closed upon test -function finish: + def addfinalizer(func, scope="function"): + """ register calling a a finalizer function. + scope == 'function': when the single test function run finishes. + scope == 'module': when tests in a different module are run + scope == 'session': when tests of the session have run. + """ + +Calling ``request.addfinalizer()`` is useful for scheduling teardown +functions. The given scope determines when the teardown function +will be called. Here is a basic example for providing a ``myfile`` +object that is to be closed when the test function finishes. .. sourcecode:: python def pytest_funcarg__myfile(self, request): - # ... create and open a "myfile" object ... + # ... create and open a unique per-function "myfile" object ... request.addfinalizer(lambda: myfile.close()) return myfile +requesting values of other funcargs +--------------------------------------------- + +Inside a funcarg provider, you sometimes may want to use a +different function argument which may be specified with +the test function or not. For such purposes you can +dynamically request a funcarg value: + +.. sourcecode:: python + + def getfuncargvalue(name): + """ Lookup and call function argument provider for the given name. + Each function argument is only requested once per function setup. + """ + +You can also use this function if you want to `decorate a funcarg`_ +locally, i.e. you want to provide the normal value but add/do something +extra. If a provider cannot be found a ``request.Error`` exception will be +raised. -decorating other funcarg providers -++++++++++++++++++++++++++++++++++++++++ -If you want to **decorate a function argument** that is -provided elsewhere you can ask the request object -to provide the "next" value: +.. _`test generators`: +.. _`parametrizing test generator`: + +generating parametrized tests with funcargs +=========================================================== + +You can directly parametrize multiple runs of the same test +function by adding new test function calls with different +function argument values. Let's look at a simple self-contained +example: .. sourcecode:: python - def pytest_funcarg__myfile(self, request): - myfile = request.call_next_provider() - # do something extra - return myfile + # ./test_example.py + def pytest_generate_tests(metafunc): + if "numiter" in metafunc.funcargnames: + for i in range(10): + metafunc.addcall(funcargs=dict(numiter=i)) + + def test_func(numiter): + assert numiter < 9 + +If you run this with ``py.test test_example.py`` you'll get: -This will raise a ``request.Error`` exception if there -is no next provider left. See the `decorator example`_ -for a use of this method. +.. sourcecode:: python + + ================================= test session starts ================================= + python: platform linux2 -- Python 2.6.2 + test object 1: /home/hpk/hg/py/trunk/test_example.py + + test_example.py .........F + + ====================================== FAILURES ======================================= + _______________________________ test_func.test_func[9] ________________________________ + + numiter = 9 + + def test_func(numiter): + > assert numiter < 9 + E assert 9 < 9 + /home/hpk/hg/py/trunk/test_example.py:10: AssertionError -.. _`lookup order`: -Order of provider and test generator lookup ----------------------------------------------- +Here is what happens in detail: -Both test generators as well as funcarg providers -are looked up in the following three scopes: +1. ``pytest_generate_tests(metafunc)`` hook is called once for each test + function. It adds ten new function calls with explicit function arguments. -1. test module -2. local plugins -3. global plugins +2. **execute tests**: ``test_func(numiter)`` is called ten times with + ten different arguments. -Using multiple funcargs ----------------------------------------- +.. _`metafunc object`: -Test functions can have multiple arguments -which can either come from a test generator -or from a provider. +test generators and metafunc objects +------------------------------------------- -.. _`runspec object`: +metafunc objects are passed to the ``pytest_generate_tests`` hook. +They help to inspect a testfunction and to generate tests +according to test configuration or values specified +in the class or module where a test function is defined: -runspec objects ------------------------- +``metafunc.funcargnames``: set of required function arguments for given function -Runspecs help to inspect a testfunction and -to generate tests with combinations of function argument values. +``metafunc.function``: underlying python test function -generating and combining funcargs -+++++++++++++++++++++++++++++++++++++++++++++++++++ +``metafunc.cls``: class object where the test function is defined in or None. -Calling ``runspec.addfuncarg(argname, value)`` will trigger -tests function calls with the given function -argument value. For each already existing -funcarg combination, the added funcarg value will +``metafunc.module``: the module object where the test function is defined in. -* be merged to the existing funcarg combination if the - new argument name isn't part of the funcarg combination yet. +``metafunc.config``: access to command line opts and general config -* otherwise generate a new test call where the existing - funcarg combination is copied and updated - with the newly added funcarg value. -For simple usage, e.g. test functions with a single -generated function argument, each call to ``addfuncarg`` -will just trigger a new call. +the ``metafunc.addcall()`` method +----------------------------------------------- -This scheme allows two sources to generate -function arguments independently from each other. +.. sourcecode:: python -Attributes of runspec objects -++++++++++++++++++++++++++++++++++++++++ + def addcall(funcargs={}, id=None, param=None): + """ trigger a new test function call. """ -``runspec.funcargnames``: set of required function arguments for given function +``funcargs`` can be a dictionary of argument names +mapped to values - providing it is called *direct parametrization*. -``runspec.function``: underlying python test function +If you provide an `id`` it will be used for reporting +and identification purposes. If you don't supply an `id` +the stringified counter of the list of added calls will be used. +``id`` values needs to be unique between all +invocations for a given test function. -``runspec.cls``: class object where the test function is defined in or None. +``param`` if specified will be seen by any +`funcarg provider`_ as a ``request.param`` attribute. +Setting it is called *indirect parametrization*. -``runspec.module``: the module object where the test function is defined in. +Indirect parametrization is preferable if test values are +expensive to setup or can only be created in certain environments. +Test generators and thus ``addcall()`` invocations are performed +during test collection which is separate from the actual test +setup and test run phase. With distributed testing collection +and test setup/run happens in different process. -``runspec.config``: access to command line opts and general config +.. _`tutorial examples`: -Useful Funcarg Tutorial Examples +Funcarg Tutorial Examples ======================================= + +.. _`application setup tutorial example`: + application specific test setup --------------------------------------------------------- Here is a basic useful step-wise example for handling application specific test setup. The goal is to have one place where we have the -glue code for bootstrapping and configuring application objects and allow +glue and test support code for bootstrapping and configuring application objects and allow test modules and test functions to stay ignorant of involved details. step 1: use and implement a test/app-specific "mysetup" @@ -232,20 +337,17 @@ .. sourcecode:: python # ./conftest.py + from myapp import MyApp - class ConftestPlugin: - def pytest_funcarg__mysetup(self, request): - return MySetup() + def pytest_funcarg__mysetup(request): + return MySetup() class MySetup: def myapp(self): return MyApp() - -py.test finds the ``pytest_funcarg__mysetup`` method by -name, see also `lookup order`_. - -To run the example we put a pseudo MyApp object into ``myapp.py``: + +To run the example we represent our application by putting a pseudo MyApp object into ``myapp.py``: .. sourcecode:: python @@ -257,7 +359,18 @@ You can now run the test with ``py.test test_sample.py`` which will show this failure: -.. sourcecode:: python +.. sourcecode:: python + + ========================= test session starts ========================= + python: platform linux2 -- Python 2.6.2 + test object 1: /home/hpk/hg/py/trunk/example/funcarg/mysetup + + test_sample.py F + + ============================== FAILURES =============================== + _____________________________ test_answer _____________________________ + + mysetup = def test_answer(mysetup): app = mysetup.myapp() @@ -265,49 +378,83 @@ > assert answer == 42 E assert 54 == 42 -If you are confused as to what the concrete question or answers -mean actually, please visit here_ :) + test_sample.py:5: AssertionError + ====================== 1 failed in 0.11 seconds ======================= + +This means that our ``mysetup`` object was successfully instantiated, +we asked it to provide an application instance and checking +its ``question`` method resulted in the wrong answer. If you are +confused as to what the concrete question or answers actually mean, +please see here_ :) Otherwise proceed to step 2. .. _here: http://uncyclopedia.wikia.com/wiki/The_Hitchhiker's_Guide_to_the_Galaxy -.. _`local plugin`: ext.html#local-plugin +.. _`local plugin`: extend.html#local-plugin step 2: adding a command line option ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++ If you provide a "funcarg" from a plugin you can easily make methods -depend on command line options or environment settings. Let's write a -local plugin that adds a command line option to ``py.test`` invocations: +depend on command line options or environment settings. +To add a command line option we update the conftest.py of +the previous example to add a command line option +and to offer a new mysetup method: .. sourcecode:: python - class ConftestPlugin: - def pytest_addoption(self, parser): - parser.addoption("--ssh", action="store", default=None, - help="specify ssh host to run tests with") + # ./conftest.py + import py + from myapp import MyApp + + def pytest_funcarg__mysetup(request): + return MySetup(request) - pytest_funcarg__mysetup = MySetupFuncarg + def pytest_addoption(parser): + parser.addoption("--ssh", action="store", default=None, + help="specify ssh host to run tests with") + - class MySetupFuncarg: + class MySetup: def __init__(self, request): - self.request = request + self.config = request.config + + def myapp(self): + return MyApp() + def getsshconnection(self): - host = self.request.config.option.ssh + host = self.config.option.ssh if host is None: - py.test.skip("specify ssh host with --ssh to run this test") + py.test.skip("specify ssh host with --ssh") return py.execnet.SshGateway(host) -Now any test functions can use the ``mysetup.getsshconnection()`` method like this: + +Now any test function can use the ``mysetup.getsshconnection()`` method like this: .. sourcecode:: python + # ./test_ssh.py class TestClass: def test_function(self, mysetup): conn = mysetup.getsshconnection() # work with conn -Running this without specifying a command line option will result in a skipped -test_function. +Running ``py.test test_ssh.py`` without specifying a command line option will result in a skipped test_function: + +.. sourcecode:: python + + ========================= test session starts ========================= + python: platform linux2 -- Python 2.6.2 + test object 1: test_ssh.py + + test_ssh.py s + + ________________________ skipped test summary _________________________ + conftest.py:23: [1] Skipped: 'specify ssh host with --ssh' + ====================== 1 skipped in 0.11 seconds ====================== + +Note especially how the test function could stay clear knowing about how to construct test state values or when to skip and with what message. The test function can concentrate on actual test code and test state providers can interact with execution of tests. + +If you specify a command line option like ``py.test --ssh=python.org`` the test will get un-skipped and actually execute. .. _`accept example`: @@ -316,14 +463,14 @@ .. sourcecode:: python - class ConftestPlugin: - def pytest_option(self, parser): - group = parser.getgroup("myproject") - group.addoption("-A", dest="acceptance", action="store_true", - help="run (slow) acceptance tests") + # ./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(self, request): - return AcceptFuncarg(request) + def pytest_funcarg__accept(request): + return AcceptFuncarg(request) class AcceptFuncarg: def __init__(self, request): @@ -353,7 +500,7 @@ applications and provide ways to do assertions about the output. -.. _`decorator example`: +.. _`decorate a funcarg`: example: decorating a funcarg in a test module -------------------------------------------------------------- @@ -365,7 +512,7 @@ .. sourcecode:: python def pytest_funcarg__accept(self, request): - arg = request.call_next_provider() + arg = request.getfuncargvalue("accept") # call the next provider # create a special layout in our tempdir arg.tmpdir.mkdir("special") return arg @@ -374,83 +521,18 @@ def test_sometest(self, accept): assert accept.tmpdir.join("special").check() -According to the the `lookup order`_ our module level provider -will be invoked first and it can ask ask its request object to -call the next provider and then decorate its result. This -mechanism allows us to stay ignorant of how/where the -function argument is provided. +Our module level provider will be invoked first and it can +ask its request object to call the next provider 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 - -.. _`combine multiple funcarg values`: - - -parametrize test functions by combining generated funcargs --------------------------------------------------------------------------- - -Adding different funcargs will generate test calls with -all combinations of added funcargs. Consider this example: - -.. sourcecode:: python - - def makearg1(runspec): - runspec.addfuncarg("arg1", 10) - runspec.addfuncarg("arg1", 11) - - def makearg2(runspec): - runspec.addfuncarg("arg2", 20) - runspec.addfuncarg("arg2", 21) - - def pytest_genfuncruns(runspec): - makearg1(runspec) - makearg2(runspec) - - # the actual test function - - def test_function(arg1, arg2): - assert arg1 in (10, 20) - assert arg2 in (20, 30) - -Running this test module will result in ``test_function`` -being called four times, in the following order:: - - test_function(10, 20) - test_function(10, 21) - test_function(11, 20) - test_function(11, 21) - - -example: test functions with generated and provided funcargs -------------------------------------------------------------------- - -You can mix generated function arguments and normally -provided ones. Consider this module: - -.. sourcecode:: python - - def pytest_genfuncruns(runspec): - if "arg1" in runspec.funcargnames: # test_function2 does not have it - runspec.addfuncarg("arg1", 10) - runspec.addfuncarg("arg1", 20) - - def pytest_funcarg__arg2(request): - return [10, 20] - - def test_function(arg1, arg2): - assert arg1 in arg2 - - def test_function2(arg2): - assert args2 == [10, 20] - -Running this test module will result in ``test_function`` -being called twice, with these arguments:: - - test_function(10, [10, 20]) - test_function(20, [10, 20]) +.. _`conftest plugin`: extend.html#conftestplugin Questions and Answers Deleted: /py/trunk/doc/test/plugins.txt ============================================================================== --- /py/trunk/doc/test/plugins.txt Tue Jun 16 18:23:18 2009 +++ (empty file) @@ -1,71 +0,0 @@ -================ -Included plugins -================ - -Many of py.test's features are implemented as a plugin. - -Included plugins -================ - -You can find the source code of all default plugins in -http://codespeak.net/svn/py/trunk/py/test/plugin/ - -plugins that add reporting asepcts ------------------------------------ - -pytest_terminal: default reporter for writing info to terminals - -pytest_resultlog: log test results in machine-readable form to a file - -pytest_eventlog: log all internal pytest events to a file - -plugins for adding new test types ------------------------------------ - -pytest_unittest: run traditional unittest TestCase instances - -pytest_doctest: run doctests in python modules or .txt files - -pytest_restdoc: provide RestructuredText syntax and link checking - -plugins for python test functions ------------------------------------ - -pytest_xfail: provides "expected to fail" test marker - -pytest_tmpdir: provide temporary directories to test functions - -pytest_plugintester: generic plugin apichecks, support for functional plugin tests - -pytest_apigen: tracing values of function/method calls when running tests - -Loading plugins and specifying dependencies -============================================ - -py.test loads and configures plugins at tool startup: - -* by reading the ``PYTEST_PLUGINS`` environment variable - and importing the comma-separated list of plugin names. - -* by loading all plugins specified via one or more ``-p name`` - command line options. - -* by loading all plugins specified via a ``pytest_plugins`` - variable in ``conftest.py`` files or test modules. - -example: ensure a plugin is loaded ------------------------------------ - -If you create a ``conftest.py`` file with the following content:: - - pytest_plugins = "pytest_myextension", - -then all tests in that directory and below it will run with -an instantiated "pytest_myextension". Here is how instantiation -takes place: - -* the module ``pytest_extension`` will be imported and - and its contained `ExtensionPlugin`` class will - be instantiated. A plugin module may specify its - dependencies via another ``pytest_plugins`` definition. - Modified: py/trunk/doc/test/test.txt ============================================================================== --- py/trunk/doc/test/test.txt (original) +++ py/trunk/doc/test/test.txt Tue Jun 16 18:23:18 2009 @@ -15,16 +15,15 @@ `distributed testing`_: distribute test runs to other machines and platforms. -plugins_: using available plugins. - -extend_: writing plugins and advanced configuration. +extend_: intro to extend and customize py.test runs +config_: ``conftest.py`` files and general configuration .. _quickstart: quickstart.html .. _features: features.html .. _funcargs: funcargs.html -.. _plugins: plugins.html -.. _extend: ext.html +.. _extend: extend.html +.. _config: config.html .. _`distributed testing`: dist.html Added: py/trunk/example/funcarg/costlysetup/conftest.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/costlysetup/conftest.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,16 @@ + +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 Added: py/trunk/example/funcarg/costlysetup/sub1/test_quick.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/costlysetup/sub1/test_quick.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,3 @@ + +def test_quick(): + pass Added: py/trunk/example/funcarg/costlysetup/sub2/test_two.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/costlysetup/sub2/test_two.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,6 @@ +def test_something(setup): + assert setup.timecostly == 1 + +def test_something_more(setup): + assert setup.timecostly == 1 + Added: py/trunk/example/funcarg/mysetup/__init__.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/mysetup/__init__.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1 @@ +# XXX this file should not need to be here but is here for proper sys.path mangling Modified: py/trunk/example/funcarg/mysetup/conftest.py ============================================================================== --- py/trunk/example/funcarg/mysetup/conftest.py (original) +++ py/trunk/example/funcarg/mysetup/conftest.py Tue Jun 16 18:23:18 2009 @@ -1,9 +1,8 @@ from myapp import MyApp -class ConftestPlugin: - def pytest_funcarg__mysetup(self, request): - return MySetup() +def pytest_funcarg__mysetup(request): + return MySetup() class MySetup: def myapp(self): Added: py/trunk/example/funcarg/mysetup2/__init__.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/mysetup2/__init__.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1 @@ +# XXX this file should not need to be here but is here for proper sys.path mangling Added: py/trunk/example/funcarg/mysetup2/conftest.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/mysetup2/conftest.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,24 @@ +import py +from 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 py.execnet.SshGateway(host) + Added: py/trunk/example/funcarg/mysetup2/myapp.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/mysetup2/myapp.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,5 @@ + +class MyApp: + def question(self): + return 6 * 9 + Added: py/trunk/example/funcarg/mysetup2/test_sample.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/mysetup2/test_sample.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,6 @@ + +def test_answer(mysetup): + app = mysetup.myapp() + answer = app.question() + assert answer == 42 + Added: py/trunk/example/funcarg/mysetup2/test_ssh.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/mysetup2/test_ssh.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,5 @@ + +class TestClass: + def test_function(self, mysetup): + conn = mysetup.getsshconnection() + # work with conn Added: py/trunk/example/funcarg/parametrize/test_parametrize.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/parametrize/test_parametrize.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,17 @@ +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") Added: py/trunk/example/funcarg/parametrize/test_parametrize2.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/parametrize/test_parametrize2.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,25 @@ +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") + Added: py/trunk/example/funcarg/parametrize/test_parametrize3.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/parametrize/test_parametrize3.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,15 @@ + +# 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) Added: py/trunk/example/funcarg/test_simpleprovider.py ============================================================================== --- (empty file) +++ py/trunk/example/funcarg/test_simpleprovider.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,7 @@ +# ./test_simpleprovider.py +def pytest_funcarg__myfuncarg(request): + return 42 + +def test_function(myfuncarg): + assert myfuncarg == 17 + Modified: py/trunk/example/pytest/test_failures.py ============================================================================== --- py/trunk/example/pytest/test_failures.py (original) +++ py/trunk/example/pytest/test_failures.py Tue Jun 16 18:23:18 2009 @@ -5,10 +5,10 @@ pytest_plugins = "pytest_pytester" def test_failure_demo_fails_properly(testdir): - sorter = testdir.inline_run(failure_demo) - passed, skipped, failed = sorter.countoutcomes() + reprec = testdir.inline_run(failure_demo) + passed, skipped, failed = reprec.countoutcomes() assert passed == 0 assert failed == 20, failed - colreports = sorter.getnamed("collectionreport") + colreports = reprec.getnamed("collectionreport") failed = len([x.failed for x in colreports]) assert failed == 5 Modified: py/trunk/py/LICENSE ============================================================================== --- py/trunk/py/LICENSE (original) +++ py/trunk/py/LICENSE Tue Jun 16 18:23:18 2009 @@ -6,6 +6,7 @@ Holger Krekel, holger at merlinux eu Guido Wesdorp, johnny at johnnydebris net + Samuele Pedroni, pedronis at openend se Carl Friedrich Bolz, cfbolz at gmx de Armin Rigo, arigo at tunes org Maciek Fijalkowski, fijal at genesilico.pl @@ -14,7 +15,6 @@ Contributors include:: - Samuele Pedroni Chris Lamb Harald Armin Massa Ralf Schmitt Modified: py/trunk/py/__init__.py ============================================================================== --- py/trunk/py/__init__.py (original) +++ py/trunk/py/__init__.py Tue Jun 16 18:23:18 2009 @@ -1,17 +1,15 @@ # -*- coding: utf-8 -*- """ -The py lib is an extensible library for testing, distributed processing and -interacting with filesystems. +advanced testing and development support library: - `py.test`_: cross-project testing tool with many advanced features - `py.execnet`_: ad-hoc code distribution to SSH, Socket and local sub processes - `py.path`_: path abstractions over local and subversion files - `py.code`_: dynamic code compile and traceback printing support -The py lib and its tools should work well on Linux, Win32, -OSX, Python versions 2.3-2.6. For questions please go to -http://pylib.org/contact.html +Compatibility: Linux, Win32, OSX, Python versions 2.3-2.6. +For questions please check out http://pylib.org/contact.html .. _`py.test`: http://pylib.org/test.html .. _`py.execnet`: http://pylib.org/execnet.html @@ -24,12 +22,9 @@ version = "1.0.0b2" initpkg(__name__, - description = "pylib and py.test: agile development and test support library", - revision = int('$LastChangedRevision$'.split(':')[1][:-1]), - lastchangedate = '$LastChangedDate$', + description = "py.test and pylib: advanced testing tool and networking lib", version = version, url = "http://pylib.org", - download_url = "http://codespeak.net/py/%s/download.html" % version, license = "MIT license", platforms = ['unix', 'linux', 'osx', 'cygwin', 'win32'], author = "holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others", @@ -49,7 +44,6 @@ "Programming Language :: Python", ], - # EXPORTED API exportdefs = { @@ -95,6 +89,7 @@ 'test.collect.Instance' : ('./test/pycollect.py', 'Instance'), 'test.collect.Generator' : ('./test/pycollect.py', 'Generator'), 'test.collect.Function' : ('./test/pycollect.py', 'Function'), + 'test.collect._fillfuncargs' : ('./test/funcargs.py', 'fillfuncargs'), # thread related API (still in early design phase) '_thread.WorkerPool' : ('./thread/pool.py', 'WorkerPool'), @@ -133,6 +128,7 @@ 'code.Frame' : ('./code/frame.py', 'Frame'), 'code.ExceptionInfo' : ('./code/excinfo.py', 'ExceptionInfo'), 'code.Traceback' : ('./code/traceback2.py', 'Traceback'), + 'code.getfslineno' : ('./code/source.py', 'getfslineno'), # backports and additions of builtins 'builtin.__doc__' : ('./builtin/__init__.py', '__doc__'), Modified: py/trunk/py/_com.py ============================================================================== --- py/trunk/py/_com.py (original) +++ py/trunk/py/_com.py Tue Jun 16 18:23:18 2009 @@ -10,7 +10,6 @@ Simple example: MultiCall([list1.append, list2.append], 42).execute() """ - NONEASRESULT = object() def __init__(self, methods, *args, **kwargs): self.methods = methods[:] @@ -26,8 +25,6 @@ self.results = [res] break if res is not None: - if res is self.NONEASRESULT: - res = None self.results.append(res) if firstresult: break @@ -66,6 +63,7 @@ """ Manage Plugins: Load plugins and manage calls to plugins. """ + logfile = None MultiCall = MultiCall def __init__(self, plugins=None): @@ -99,23 +97,14 @@ l.reverse() return l - def call_firstresult(self, methname, *args, **kwargs): - """ return first non-None result of a plugin method. """ - return MultiCall(self.listattr(methname), *args, **kwargs).execute(firstresult=True) - - def call_plugin(self, plugin, methname, *args, **kwargs): - return MultiCall(self.listattr(methname, plugins=[plugin]), - *args, **kwargs).execute(firstresult=True) - - class Hooks: def __init__(self, hookspecs, registry=None): self._hookspecs = hookspecs if registry is None: - registry = comregistry + registry = py._com.comregistry self.registry = registry for name, method in vars(hookspecs).items(): - if name[:2] != "__": + if name[:1] != "_": firstresult = getattr(method, 'firstresult', False) mm = HookCall(registry, name, firstresult=firstresult) setattr(self, name, mm) @@ -142,6 +131,11 @@ "for api call to %r" % self.name) attr = self.registry.listattr(self.name, extra=self.extralookup) mc = MultiCall(attr, **kwargs) + # XXX this should be doable from a hook impl: + if self.registry.logfile: + self.registry.logfile.write("%s(**%s) # firstresult=%s\n" % + (self.name, kwargs, self.firstresult)) + self.registry.logfile.flush() return mc.execute(firstresult=self.firstresult) comregistry = Registry() Modified: py/trunk/py/code/source.py ============================================================================== --- py/trunk/py/code/source.py (original) +++ py/trunk/py/code/source.py Tue Jun 16 18:23:18 2009 @@ -217,6 +217,26 @@ return co +def getfslineno(obj): + try: + code = py.code.Code(obj) + except TypeError: + # fallback to + fn = (py.std.inspect.getsourcefile(obj) or + py.std.inspect.getfile(obj)) + fspath = fn and py.path.local(fn) or None + if fspath: + try: + _, lineno = findsource(obj) + except IOError: + lineno = None + else: + lineno = None + else: + fspath = code.path + lineno = code.firstlineno + return fspath, lineno + # # helper functions # Modified: py/trunk/py/code/testing/test_source.py ============================================================================== --- py/trunk/py/code/testing/test_source.py (original) +++ py/trunk/py/code/testing/test_source.py Tue Jun 16 18:23:18 2009 @@ -355,3 +355,27 @@ assert 'if 1:' in str(src) assert src[lineno] == " def x():" + +def test_getfslineno(): + from py.code import getfslineno + + def f(x): + pass + + fspath, lineno = getfslineno(f) + + fname = __file__ + if fname.lower().endswith('.pyc'): + fname = fname[:-1] + + assert fspath == py.path.local(fname) + assert lineno == f.func_code.co_firstlineno-1 # see findsource + + class A(object): + pass + + fspath, lineno = getfslineno(A) + + _, A_lineno = py.std.inspect.findsource(A) + assert fspath == py.path.local(fname) + assert lineno == A_lineno Modified: py/trunk/py/conftest.py ============================================================================== --- py/trunk/py/conftest.py (original) +++ py/trunk/py/conftest.py Tue Jun 16 18:23:18 2009 @@ -3,25 +3,23 @@ rsyncdirs = ['../doc'] import py -class PylibTestconfigPlugin: - def pytest_funcarg__specssh(self, request): - return getspecssh(request.config) - def pytest_funcarg__specsocket(self, request): - return getsocketspec(request.config) - - def pytest_addoption(self, parser): - group = parser.addgroup("pylib", "py lib testing options") - group.addoption('--sshhost', - action="store", dest="sshhost", default=None, - help=("ssh xspec for ssh functional tests. ")) - group.addoption('--gx', - action="append", dest="gspecs", default=None, - help=("add a global test environment, XSpec-syntax. ")) - group.addoption('--runslowtests', - action="store_true", dest="runslowtests", default=False, - help=("run slow tests")) +def pytest_addoption(parser): + group = parser.addgroup("pylib", "py lib testing options") + group.addoption('--sshhost', + action="store", dest="sshhost", default=None, + help=("ssh xspec for ssh functional tests. ")) + group.addoption('--gx', + action="append", dest="gspecs", default=None, + help=("add a global test environment, XSpec-syntax. ")) + group.addoption('--runslowtests', + action="store_true", dest="runslowtests", default=False, + help=("run slow tests")) + +def pytest_funcarg__specssh(request): + return getspecssh(request.config) +def pytest_funcarg__specsocket(request): + return getsocketspec(request.config) -ConftestPlugin = PylibTestconfigPlugin # configuration information for tests def getgspecs(config=None): Modified: py/trunk/py/execnet/testing/test_event.py ============================================================================== --- py/trunk/py/execnet/testing/test_event.py (original) +++ py/trunk/py/execnet/testing/test_event.py Tue Jun 16 18:23:18 2009 @@ -4,7 +4,7 @@ class TestExecnetEvents: def test_popengateway(self, _pytest): - rec = _pytest.getcallrecorder(ExecnetAPI) + rec = _pytest.gethookrecorder(ExecnetAPI) gw = py.execnet.PopenGateway() call = rec.popcall("pyexecnet_gateway_init") assert call.gateway == gw Modified: py/trunk/py/execnet/testing/test_gwmanage.py ============================================================================== --- py/trunk/py/execnet/testing/test_gwmanage.py (original) +++ py/trunk/py/execnet/testing/test_gwmanage.py Tue Jun 16 18:23:18 2009 @@ -21,7 +21,7 @@ assert spec.chdir == "abc" def test_popen_makegateway_events(self, _pytest): - rec = _pytest.getcallrecorder(py.execnet._HookSpecs) + rec = _pytest.gethookrecorder(py.execnet._HookSpecs) hm = GatewayManager(["popen"] * 2) hm.makegateways() call = rec.popcall("pyexecnet_gwmanage_newgateway") @@ -63,7 +63,7 @@ def test_hostmanage_rsync_same_popen_twice(self, mysetup, _pytest): source, dest = mysetup.source, mysetup.dest - rec = _pytest.getcallrecorder(py.execnet._HookSpecs) + rec = _pytest.gethookrecorder(py.execnet._HookSpecs) hm = GatewayManager(["popen//chdir=%s" %dest] * 2) hm.makegateways() source.ensure("dir1", "dir2", "hello") @@ -117,6 +117,7 @@ tmp = request.config.mktemp(request.function.__name__, numbered=True) self.source = tmp.mkdir("source") self.dest = tmp.mkdir("dest") + request.getfuncargvalue("_pytest") # to have patching of py._com.comregistry class TestHRSync: def test_hrsync_filter(self, mysetup): Modified: py/trunk/py/execnet/testing/test_xspec.py ============================================================================== --- py/trunk/py/execnet/testing/test_xspec.py (original) +++ py/trunk/py/execnet/testing/test_xspec.py Tue Jun 16 18:23:18 2009 @@ -36,9 +36,10 @@ for x in ("popen", "popen//python=this"): assert XSpec(x)._spec == x - def test_repr(self): + def test_repr_and_string(self): for x in ("popen", "popen//python=this"): assert repr(XSpec(x)).find("popen") != -1 + assert str(XSpec(x)) == x def test_hash_equality(self): assert XSpec("popen") == XSpec("popen") Modified: py/trunk/py/execnet/xspec.py ============================================================================== --- py/trunk/py/execnet/xspec.py (original) +++ py/trunk/py/execnet/xspec.py Tue Jun 16 18:23:18 2009 @@ -27,6 +27,8 @@ def __repr__(self): return "" %(self._spec,) + def __str__(self): + return self._spec def __hash__(self): return hash(self._spec) Modified: py/trunk/py/io/stdcapture.py ============================================================================== --- py/trunk/py/io/stdcapture.py (original) +++ py/trunk/py/io/stdcapture.py Tue Jun 16 18:23:18 2009 @@ -22,14 +22,14 @@ call = classmethod(call) def reset(self): - """ reset sys.stdout and sys.stderr - - returns a tuple of file objects (out, err) for the captured - data + """ reset sys.stdout and sys.stderr and return captured output + as strings and restore sys.stdout/err. """ - outfile, errfile = self.done() - return outfile.read(), errfile.read() - + x, y = self.done() + outerr = x.read(), y.read() + x.close() + y.close() + return outerr class StdCaptureFD(Capture): """ This class allows to capture writes to FD1 and FD2 @@ -58,11 +58,14 @@ def done(self): """ return (outfile, errfile) and stop capturing. """ - outfile = errfile = emptyfile if hasattr(self, 'out'): outfile = self.out.done() + else: + outfile = StringIO() if hasattr(self, 'err'): errfile = self.err.done() + else: + errfile = StringIO() if hasattr(self, '_oldin'): oldsys, oldfd = self._oldin os.dup2(oldfd, 0) @@ -94,15 +97,9 @@ self.oldin = sys.stdin sys.stdin = self.newin = DontReadFromInput() - def reset(self): - """ return captured output as strings and restore sys.stdout/err.""" - x, y = self.done() - return x.read(), y.read() - def done(self): """ return (outfile, errfile) and stop capturing. """ o,e = sys.stdout, sys.stderr - outfile = errfile = emptyfile if self._out: try: sys.stdout = self.oldout @@ -111,6 +108,8 @@ del self.oldout outfile = self.newout outfile.seek(0) + else: + outfile = StringIO() if self._err: try: sys.stderr = self.olderr @@ -119,6 +118,8 @@ del self.olderr errfile = self.newerr errfile.seek(0) + else: + errfile = StringIO() if self._in: sys.stdin = self.oldin return outfile, errfile @@ -144,5 +145,4 @@ else: devnullpath = '/dev/null' -emptyfile = StringIO() Modified: py/trunk/py/io/terminalwriter.py ============================================================================== --- py/trunk/py/io/terminalwriter.py (original) +++ py/trunk/py/io/terminalwriter.py Tue Jun 16 18:23:18 2009 @@ -129,6 +129,10 @@ if flush: file.flush() +def should_do_markup(file): + return hasattr(file, 'isatty') and file.isatty() \ + and os.environ.get('TERM') != 'dumb' + class TerminalWriter(object): _esctable = dict(black=30, red=31, green=32, yellow=33, blue=34, purple=35, cyan=36, white=37, @@ -146,7 +150,7 @@ file = WriteFile(file) self._file = file self.fullwidth = get_terminal_width() - self.hasmarkup = hasattr(file, 'isatty') and file.isatty() + self.hasmarkup = should_do_markup(file) def _escaped(self, text, esc): if esc and self.hasmarkup: @@ -217,7 +221,7 @@ file = WriteFile(file) self._file = file self.fullwidth = get_terminal_width() - self.hasmarkup = hasattr(file, 'isatty') and file.isatty() + self.hasmarkup = should_do_markup(file) def sep(self, sepchar, title=None, fullwidth=None, **kw): if fullwidth is None: Modified: py/trunk/py/io/testing/test_terminalwriter.py ============================================================================== --- py/trunk/py/io/testing/test_terminalwriter.py (original) +++ py/trunk/py/io/testing/test_terminalwriter.py Tue Jun 16 18:23:18 2009 @@ -1,6 +1,7 @@ import py import os, sys from py.__.io import terminalwriter +import StringIO def skip_win32(): if sys.platform == 'win32': @@ -20,6 +21,14 @@ tw = py.io.TerminalWriter(stringio=True) assert hasattr(tw, 'stringio') +def test_terminalwriter_dumb_term_no_markup(monkeypatch): + monkeypatch.setattr(os, 'environ', {'TERM': 'dumb', 'PATH': ''}) + monkeypatch.setattr(sys, 'stdout', StringIO.StringIO()) + monkeypatch.setattr(sys.stdout, 'isatty', lambda:True) + assert sys.stdout.isatty() + tw = py.io.TerminalWriter() + assert not tw.hasmarkup + class BaseTests: def test_line(self): tw = self.getwriter() Modified: py/trunk/py/misc/testing/test_com.py ============================================================================== --- py/trunk/py/misc/testing/test_com.py (original) +++ py/trunk/py/misc/testing/test_com.py Tue Jun 16 18:23:18 2009 @@ -73,7 +73,15 @@ # we might not have had a chance to run at all. #res = call.execute(firstresult=True) #assert res == 10 - + + def test_call_none_is_no_result(self): + def m1(): + return 1 + def m2(): + return None + mc = MultiCall([m1, m2]) + res = mc.execute(firstresult=True) + assert res == 1 class TestRegistry: def test_MultiCall(self): @@ -97,28 +105,6 @@ assert not registry.isregistered(my) assert list(registry) == [my2] - def test_call_none_is_no_result(self): - plugins = Registry() - class api1: - def m(self): - return None - class api2: - def m(self, __call__): - return 41 - plugins.register(api1()) - plugins.register(api1()) - plugins.register(api2()) - assert plugins.call_firstresult('m') == 41 - - def test_call_noneasresult(self): - plugins = Registry() - class api1: - def m(self, __call__): - return __call__.NONEASRESULT - plugins.register(api1()) - plugins.register(api1()) - assert plugins.call_firstresult('m') is None - def test_listattr(self): plugins = Registry() class api1: Modified: py/trunk/py/test/collect.py ============================================================================== --- py/trunk/py/test/collect.py (original) +++ py/trunk/py/test/collect.py Tue Jun 16 18:23:18 2009 @@ -25,15 +25,12 @@ - configuration/options for setup/teardown stdout/stderr capturing and execution of test items """ - def __init__(self, name, parent=None, config=None): + def __init__(self, name, parent=None): self.name = name self.parent = parent - if config is None: - config = parent.config - self.config = config + self.config = getattr(parent, 'config', None) self.fspath = getattr(parent, 'fspath', None) - # # note to myself: Pickling is uh. # @@ -79,13 +76,6 @@ def __hash__(self): return hash((self.name, self.parent)) - - def __cmp__(self, other): - if not isinstance(other, Node): - return -1 - s1 = self._getsortvalue() - s2 = other._getsortvalue() - return cmp(s1, s2) def setup(self): pass @@ -127,6 +117,12 @@ 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 _getitembynames(self, namelist): cur = self for name in namelist: @@ -224,9 +220,6 @@ return True return False - def _getsortvalue(self): - return self.name - def _prunetraceback(self, traceback): return traceback @@ -285,7 +278,6 @@ """ Directory = configproperty('Directory') Module = configproperty('Module') - #DoctestFile = configproperty('DoctestFile') def collect(self): """ returns a list of children (items and collectors) @@ -339,9 +331,9 @@ return self.collect_by_name(name) class FSCollector(Collector): - def __init__(self, fspath, parent=None, config=None): + def __init__(self, fspath, parent=None): fspath = py.path.local(fspath) - super(FSCollector, self).__init__(fspath.basename, parent, config=config) + super(FSCollector, self).__init__(fspath.basename, parent) self.fspath = fspath def __getstate__(self): @@ -392,7 +384,13 @@ l.append(res) return l + def _ignore(self, path): + ignore_paths = self.config.getconftest_pathlist("collect_ignore", path=path) + return ignore_paths and path in ignore_paths + def consider(self, path): + if self._ignore(path): + return if path.check(file=1): res = self.consider_file(path) elif path.check(dir=1): @@ -400,10 +398,11 @@ else: res = None if isinstance(res, list): - # throw out identical modules + # throw out identical results l = [] for x in res: if x not in l: + assert x.parent == self, "wrong collection tree construction" l.append(x) res = l return res @@ -414,10 +413,8 @@ def consider_dir(self, path, usefilters=None): if usefilters is not None: py.log._apiwarn("0.99", "usefilters argument not needed") - res = self.config.hook.pytest_collect_recurse(path=path, parent=self) - if res is None or res: - return self.config.hook.pytest_collect_directory( - path=path, parent=self) + return self.config.hook.pytest_collect_directory( + path=path, parent=self) class Item(Node): """ a basic test item. """ @@ -433,13 +430,13 @@ def run(self): """ deprecated, here because subclasses might call it. """ - return self.execute(self.obj, *self._args) + return self.execute(self.obj) - def execute(self, obj, *args): + def execute(self, obj): """ deprecated, here because subclasses might call it. """ - return obj(*args) + return obj() - def metainfo(self): + def reportinfo(self): return self.fspath, None, "" def warnoldcollect(function=None): Modified: py/trunk/py/test/config.py ============================================================================== --- py/trunk/py/test/config.py (original) +++ py/trunk/py/test/config.py Tue Jun 16 18:23:18 2009 @@ -2,7 +2,6 @@ from conftesthandle import Conftest from py.__.test import parseopt -from py.__.test.runner import SetupState def ensuretemp(string, dir=1): """ return temporary directory path with @@ -41,7 +40,6 @@ assert isinstance(pluginmanager, py.test._PluginManager) self.pluginmanager = pluginmanager self._conftest = Conftest(onimport=self._onimportconftest) - self._setupstate = SetupState() self.hook = pluginmanager.hook def _onimportconftest(self, conftestmodule): @@ -157,7 +155,8 @@ if pkgpath is None: pkgpath = path.check(file=1) and path.dirpath() or path Dir = self._conftest.rget("Directory", pkgpath) - col = Dir(pkgpath, config=self) + col = Dir(pkgpath) + col.config = self return col._getfsnode(path) def getconftest_pathlist(self, name, path=None): Deleted: /py/trunk/py/test/custompdb.py ============================================================================== --- /py/trunk/py/test/custompdb.py Tue Jun 16 18:23:18 2009 +++ (empty file) @@ -1,77 +0,0 @@ -import pdb, sys, linecache - -class Pdb(pdb.Pdb): - def do_list(self, arg): - self.lastcmd = 'list' - last = None - if arg: - try: - x = eval(arg, {}, {}) - if type(x) == type(()): - first, last = x - first = int(first) - last = int(last) - if last < first: - # Assume it's a count - last = first + last - else: - first = max(1, int(x) - 5) - except: - print '*** Error in argument:', repr(arg) - return - elif self.lineno is None: - first = max(1, self.curframe.f_lineno - 5) - else: - first = self.lineno + 1 - if last is None: - last = first + 10 - filename = self.curframe.f_code.co_filename - breaklist = self.get_file_breaks(filename) - try: - for lineno in range(first, last+1): - # start difference from normal do_line - line = self._getline(filename, lineno) - # end difference from normal do_line - if not line: - print '[EOF]' - break - else: - s = repr(lineno).rjust(3) - if len(s) < 4: s = s + ' ' - if lineno in breaklist: s = s + 'B' - else: s = s + ' ' - if lineno == self.curframe.f_lineno: - s = s + '->' - print s + '\t' + line, - self.lineno = lineno - except KeyboardInterrupt: - pass - do_l = do_list - - def _getline(self, filename, lineno): - if hasattr(filename, "__source__"): - try: - return filename.__source__.lines[lineno - 1] + "\n" - except IndexError: - return None - return linecache.getline(filename, lineno) - - def get_stack(self, f, t): - # Modified from bdb.py to be able to walk the stack beyond generators, - # which does not work in the normal pdb :-( - stack, i = pdb.Pdb.get_stack(self, f, t) - if f is None: - i = max(0, len(stack) - 1) - return stack, i - -def post_mortem(t): - # modified from pdb.py for the new get_stack() implementation - p = Pdb() - p.reset() - p.interaction(None, t) - -def set_trace(): - # again, a copy of the version in pdb.py - Pdb().set_trace(sys._getframe().f_back) - - Modified: py/trunk/py/test/defaultconftest.py ============================================================================== --- py/trunk/py/test/defaultconftest.py (original) +++ py/trunk/py/test/defaultconftest.py Tue Jun 16 18:23:18 2009 @@ -10,5 +10,5 @@ Function = py.test.collect.Function Instance = py.test.collect.Instance -pytest_plugins = "default terminal xfail tmpdir execnetcleanup monkeypatch recwarn pdb".split() +pytest_plugins = "default runner terminal xfail tmpdir execnetcleanup monkeypatch recwarn pdb".split() Modified: py/trunk/py/test/dist/dsession.py ============================================================================== --- py/trunk/py/test/dist/dsession.py (original) +++ py/trunk/py/test/dist/dsession.py Tue Jun 16 18:23:18 2009 @@ -5,7 +5,6 @@ """ import py -from py.__.test.runner import basic_run_report, basic_collect_report, ItemTestReport from py.__.test.session import Session from py.__.test import outcome from py.__.test.dist.nodemanage import NodeManager @@ -24,9 +23,9 @@ self.shuttingdown = False self.testsfailed = False - def pytest_itemtestreport(self, rep): - if rep.colitem in self.dsession.item2nodes: - self.dsession.removeitem(rep.colitem, rep.node) + def pytest_runtest_logreport(self, rep): + if rep.item in self.dsession.item2nodes: + self.dsession.removeitem(rep.item, rep.node) if rep.failed: self.testsfailed = True @@ -61,14 +60,14 @@ self.item2nodes = {} super(DSession, self).__init__(config=config) - def pytest_configure(self, __call__, config): - __call__.execute() - try: - config.getxspecs() - except config.Error: - print - raise config.Error("dist mode %r needs test execution environments, " - "none found." %(config.option.dist)) + #def pytest_configure(self, __call__, config): + # __call__.execute() + # try: + # config.getxspecs() + # except config.Error: + # print + # raise config.Error("dist mode %r needs test execution environments, " + # "none found." %(config.option.dist)) def main(self, colitems=None): colitems = self.getinitialitems(colitems) @@ -177,7 +176,8 @@ senditems.append(next) else: self.config.hook.pytest_collectstart(collector=next) - self.queueevent("pytest_collectreport", rep=basic_collect_report(next)) + colrep = self.config.hook.pytest_make_collect_report(collector=next) + self.queueevent("pytest_collectreport", rep=colrep) if self.config.option.dist == "each": self.senditems_each(senditems) else: @@ -238,10 +238,11 @@ self.node2pending[node].remove(item) def handle_crashitem(self, item, node): - longrepr = "!!! Node %r crashed during running of test %r" %(node, item) - rep = ItemTestReport(item, when="???", excinfo=longrepr) + runner = item.config.pluginmanager.getplugin("runner") + info = "!!! Node %r crashed during running of test %r" %(node, item) + rep = runner.ItemTestReport(item=item, excinfo=info, when="???") rep.node = node - self.config.hook.pytest_itemtestreport(rep=rep) + self.config.hook.pytest_runtest_logreport(rep=rep) def setup(self): """ setup any neccessary resources ahead of the test run. """ Modified: py/trunk/py/test/dist/testing/test_dsession.py ============================================================================== --- py/trunk/py/test/dist/testing/test_dsession.py (original) +++ py/trunk/py/test/dist/testing/test_dsession.py Tue Jun 16 18:23:18 2009 @@ -1,13 +1,13 @@ from py.__.test.dist.dsession import DSession -from py.__.test.runner import basic_collect_report from py.__.test import outcome import py XSpec = py.execnet.XSpec -def run(item, node): - from py.__.test.runner import basic_run_report - rep = basic_run_report(item) +def run(item, node, excinfo=None): + runner = item.config.pluginmanager.getplugin("runner") + rep = runner.ItemTestReport(item=item, + excinfo=excinfo, when="call", outerr=("", "")) rep.node = node return rep @@ -134,7 +134,7 @@ session.queueevent(None) session.loop_once(loopstate) assert node.sent == [[item]] - session.queueevent("pytest_itemtestreport", rep=run(item, node)) + session.queueevent("pytest_runtest_logreport", rep=run(item, node)) session.loop_once(loopstate) assert loopstate.shuttingdown assert not loopstate.testsfailed @@ -174,16 +174,16 @@ session.senditems_load([item1, item2]) node = session.item2nodes[item1] [0] session.queueevent("pytest_testnodedown", node=node, error=None) - evrec = testdir.geteventrecorder(session.pluginmanager) + reprec = testdir.getreportrecorder(session) print session.item2nodes loopstate = session._initloopstate([]) session.loop_once(loopstate) assert loopstate.colitems == [item2] # do not reschedule crash item - testrep = evrec.matchreport(names="itemtestreport") - assert testrep.failed - assert testrep.colitem == item1 - assert str(testrep.longrepr).find("crashed") != -1 + rep = reprec.matchreport(names="pytest_runtest_logreport") + assert rep.failed + assert rep.item == item1 + assert str(rep.longrepr).find("crashed") != -1 #assert str(testrep.longrepr).find(node.gateway.spec) != -1 def test_testnodeready_adds_to_available(self, testdir): @@ -198,7 +198,7 @@ session.loop_once(loopstate) assert len(session.node2pending) == 1 - def runthrough(self, item): + def runthrough(self, item, excinfo=None): session = DSession(item.config) node = MockNode() session.addnode(node) @@ -208,8 +208,8 @@ session.loop_once(loopstate) assert node.sent == [[item]] - ev = run(item, node) - session.queueevent("pytest_itemtestreport", rep=ev) + ev = run(item, node, excinfo=excinfo) + session.queueevent("pytest_runtest_logreport", rep=ev) session.loop_once(loopstate) assert loopstate.shuttingdown session.queueevent("pytest_testnodedown", node=node, error=None) @@ -224,7 +224,7 @@ def test_exit_completed_tests_fail(self, testdir): item = testdir.getitem("def test_func(): 0/0") - session, exitstatus = self.runthrough(item) + session, exitstatus = self.runthrough(item, excinfo="fail") assert exitstatus == outcome.EXIT_TESTSFAILED def test_exit_on_first_failing(self, testdir): @@ -238,16 +238,16 @@ session = DSession(modcol.config) node = MockNode() session.addnode(node) - items = basic_collect_report(modcol).result + items = modcol.config.hook.pytest_make_collect_report(collector=modcol).result # trigger testing - this sends tests to the node session.triggertesting(items) # run tests ourselves and produce reports - ev1 = run(items[0], node) - ev2 = run(items[1], node) - session.queueevent("pytest_itemtestreport", rep=ev1) # a failing one - session.queueevent("pytest_itemtestreport", rep=ev2) + ev1 = run(items[0], node, "fail") + ev2 = run(items[1], node, None) + session.queueevent("pytest_runtest_logreport", rep=ev1) # a failing one + session.queueevent("pytest_runtest_logreport", rep=ev2) # now call the loop loopstate = session._initloopstate(items) session.loop_once(loopstate) @@ -261,13 +261,13 @@ session.addnode(node) loopstate = session._initloopstate([]) loopstate.shuttingdown = True - evrec = testdir.geteventrecorder(session.pluginmanager) - session.queueevent("pytest_itemtestreport", rep=run(item, node)) + reprec = testdir.getreportrecorder(session) + session.queueevent("pytest_runtest_logreport", rep=run(item, node)) session.loop_once(loopstate) - assert not evrec.getcalls("pytest_testnodedown") + assert not reprec.getcalls("pytest_testnodedown") session.queueevent("pytest_testnodedown", node=node, error=None) session.loop_once(loopstate) - assert evrec.getcall('pytest_testnodedown').node == node + assert reprec.getcall('pytest_testnodedown').node == node def test_filteritems(self, testdir): modcol = testdir.getmodulecol(""" @@ -282,18 +282,18 @@ dsel = session.filteritems([modcol]) assert dsel == [modcol] items = modcol.collect() - callrecorder = testdir.geteventrecorder(session.pluginmanager).callrecorder + hookrecorder = testdir.getreportrecorder(session).hookrecorder remaining = session.filteritems(items) assert remaining == [] - event = callrecorder.getcalls("pytest_deselected")[-1] + event = hookrecorder.getcalls("pytest_deselected")[-1] assert event.items == items modcol.config.option.keyword = "test_fail" remaining = session.filteritems(items) assert remaining == [items[0]] - event = callrecorder.getcalls("pytest_deselected")[-1] + event = hookrecorder.getcalls("pytest_deselected")[-1] assert event.items == [items[1]] def test_testnodedown_shutdown_after_completion(self, testdir): @@ -303,7 +303,7 @@ node = MockNode() session.addnode(node) session.senditems_load([item]) - session.queueevent("pytest_itemtestreport", rep=run(item, node)) + session.queueevent("pytest_runtest_logreport", rep=run(item, node)) loopstate = session._initloopstate([]) session.loop_once(loopstate) assert node._shutdown is True @@ -324,13 +324,12 @@ node = MockNode() session.addnode(node) - colreport = basic_collect_report(modcol) + colreport = modcol.config.hook.pytest_make_collect_report(collector=modcol) item1, item2 = colreport.result session.senditems_load([item1]) # node2pending will become empty when the loop sees the report rep = run(item1, node) - - session.queueevent("pytest_itemtestreport", rep=run(item1, node)) + session.queueevent("pytest_runtest_logreport", rep=run(item1, node)) # but we have a collection pending session.queueevent("pytest_collectreport", rep=colreport) @@ -355,16 +354,26 @@ """) config = testdir.parseconfig('-d', p1, '--tx=popen') dsession = DSession(config) - callrecorder = testdir.geteventrecorder(config.pluginmanager).callrecorder + hookrecorder = testdir.getreportrecorder(config).hookrecorder dsession.main([config.getfsnode(p1)]) - rep = callrecorder.popcall("pytest_itemtestreport").rep + rep = hookrecorder.popcall("pytest_runtest_logreport").rep assert rep.passed - rep = callrecorder.popcall("pytest_itemtestreport").rep + rep = hookrecorder.popcall("pytest_runtest_logreport").rep assert rep.skipped - rep = callrecorder.popcall("pytest_itemtestreport").rep + rep = hookrecorder.popcall("pytest_runtest_logreport").rep assert rep.failed # see that the node is really down - node = callrecorder.popcall("pytest_testnodedown").node + node = hookrecorder.popcall("pytest_testnodedown").node assert node.gateway.spec.popen - #XXX eq.geteventargs("pytest_testrunfinish") + #XXX eq.geteventargs("pytest_sessionfinish") + @py.test.mark.xfail("test implementation missing") + def test_collected_function_causes_remote_skip_at_module_level(self, testdir): + p = testdir.makepyfile(""" + import py + py.test.importorskip("xyz") + def test_func(): + pass + """) + # we need to be able to collect test_func locally but not in the subprocess + XXX Modified: py/trunk/py/test/dist/testing/test_mypickle.py ============================================================================== --- py/trunk/py/test/dist/testing/test_mypickle.py (original) +++ py/trunk/py/test/dist/testing/test_mypickle.py Tue Jun 16 18:23:18 2009 @@ -4,22 +4,24 @@ class A: pass -def test_pickle_and_back_IS_same(): - def pickle_band_back_IS_same(obj, proto): - p1 = ImmutablePickler(uneven=False, protocol=proto) - p2 = ImmutablePickler(uneven=True, protocol=proto) - s1 = p1.dumps(obj) - d2 = p2.loads(s1) - s2 = p2.dumps(d2) - obj_back = p1.loads(s2) - assert obj is obj_back - a1 = A() - a2 = A() - a2.a1 = a1 - for proto in (0,1,2, -1): - for obj in {1:2}, [1,2,3], a1, a2: - yield pickle_band_back_IS_same, obj, proto +def pytest_generate_tests(metafunc): + if "obj" in metafunc.funcargnames and "proto" in metafunc.funcargnames: + a1 = A() + a2 = A() + a2.a1 = a1 + for proto in (0,1,2, -1): + for obj in {1:2}, [1,2,3], a1, a2: + metafunc.addcall(funcargs=dict(obj=obj, proto=proto)) + +def test_pickle_and_back_IS_same(obj, proto): + p1 = ImmutablePickler(uneven=False, protocol=proto) + p2 = ImmutablePickler(uneven=True, protocol=proto) + s1 = p1.dumps(obj) + d2 = p2.loads(s1) + s2 = p2.dumps(d2) + obj_back = p1.loads(s2) + assert obj is obj_back def test_pickling_twice_before_unpickling(): p1 = ImmutablePickler(uneven=False) Modified: py/trunk/py/test/dist/testing/test_nodemanage.py ============================================================================== --- py/trunk/py/test/dist/testing/test_nodemanage.py (original) +++ py/trunk/py/test/dist/testing/test_nodemanage.py Tue Jun 16 18:23:18 2009 @@ -8,6 +8,7 @@ numbered=True) self.source = basetemp.mkdir("source") self.dest = basetemp.mkdir("dest") + request.getfuncargvalue("_pytest") class TestNodeManager: @py.test.mark.xfail("consider / forbid implicit rsyncdirs?") @@ -107,10 +108,10 @@ config = py.test.config._reparse([source, '--debug']) assert config.option.debug nodemanager = NodeManager(config, specs) - sorter = testdir.geteventrecorder(config.pluginmanager).callrecorder + reprec = testdir.getreportrecorder(config).hookrecorder nodemanager.setup_nodes(putevent=[].append) for spec in nodemanager.gwmanager.specs: - l = sorter.getcalls("pytest_trace") + l = reprec.getcalls("pytest_trace") assert l nodemanager.teardown_nodes() @@ -119,8 +120,8 @@ def test_one(): pass """) - sorter = testdir.inline_run("-d", "--rsyncdir=%s" % testdir.tmpdir, - "--tx=%s" % specssh, testdir.tmpdir) - ev = sorter.getfirstnamed(pytest_itemtestreport) - assert ev.passed + reprec = testdir.inline_run("-d", "--rsyncdir=%s" % testdir.tmpdir, + "--tx", specssh, testdir.tmpdir) + rep, = reprec.getreports("pytest_runtest_logreport") + assert rep.passed Modified: py/trunk/py/test/dist/testing/test_txnode.py ============================================================================== --- py/trunk/py/test/dist/testing/test_txnode.py (original) +++ py/trunk/py/test/dist/testing/test_txnode.py Tue Jun 16 18:23:18 2009 @@ -111,11 +111,11 @@ item = testdir.getitem("def test_func(): pass") node = mysetup.makenode(item.config) node.send(item) - kwargs = mysetup.geteventargs("pytest_itemtestreport") + kwargs = mysetup.geteventargs("pytest_runtest_logreport") rep = kwargs['rep'] assert rep.passed print rep - assert rep.colitem == item + assert rep.item == item def test_send_some(self, testdir, mysetup): items = testdir.getitems(""" @@ -131,11 +131,11 @@ for item in items: node.send(item) for outcome in "passed failed skipped".split(): - kwargs = mysetup.geteventargs("pytest_itemtestreport") + kwargs = mysetup.geteventargs("pytest_runtest_logreport") rep = kwargs['rep'] assert getattr(rep, outcome) node.sendlist(items) for outcome in "passed failed skipped".split(): - rep = mysetup.geteventargs("pytest_itemtestreport")['rep'] + rep = mysetup.geteventargs("pytest_runtest_logreport")['rep'] assert getattr(rep, outcome) Modified: py/trunk/py/test/dist/txnode.py ============================================================================== --- py/trunk/py/test/dist/txnode.py (original) +++ py/trunk/py/test/dist/txnode.py Tue Jun 16 18:23:18 2009 @@ -50,10 +50,10 @@ elif eventname == "slavefinished": self._down = True self.notify("pytest_testnodedown", error=None, node=self) - elif eventname == "pytest_itemtestreport": + elif eventname == "pytest_runtest_logreport": rep = kwargs['rep'] rep.node = self - self.notify("pytest_itemtestreport", rep=rep) + self.notify("pytest_runtest_logreport", rep=rep) else: self.notify(eventname, *args, **kwargs) except KeyboardInterrupt: @@ -105,8 +105,8 @@ def sendevent(self, eventname, *args, **kwargs): self.channel.send((eventname, args, kwargs)) - def pytest_itemtestreport(self, rep): - self.sendevent("pytest_itemtestreport", rep=rep) + def pytest_runtest_logreport(self, rep): + self.sendevent("pytest_runtest_logreport", rep=rep) def run(self): channel = self.channel @@ -124,9 +124,9 @@ break if isinstance(task, list): for item in task: - item.config.pluginmanager.do_itemrun(item) + item.config.hook.pytest_runtest_protocol(item=item) else: - task.config.pluginmanager.do_itemrun(item=task) + task.config.hook.pytest_runtest_protocol(item=task) except KeyboardInterrupt: raise except: Modified: py/trunk/py/test/funcargs.py ============================================================================== --- py/trunk/py/test/funcargs.py (original) +++ py/trunk/py/test/funcargs.py Tue Jun 16 18:23:18 2009 @@ -10,22 +10,19 @@ def fillfuncargs(function): """ fill missing funcargs. """ - if function._args: - # functions yielded from a generator: we don't want - # to support that because we want to go here anyway: - # http://bitbucket.org/hpk42/py-trunk/issue/2/next-generation-generative-tests - pass - else: - # standard Python Test function/method case - for argname in getfuncargnames(function.obj): - if argname not in function.funcargs: - request = FuncargRequest(pyfuncitem=function, argname=argname) - try: - function.funcargs[argname] = request.call_next_provider() - except request.Error: - request._raiselookupfailed() + request = FuncargRequest(pyfuncitem=function) + request._fillfuncargs() -class RunSpecs: + +_notexists = object() +class CallSpec: + def __init__(self, funcargs, id, param): + self.funcargs = funcargs + self.id = id + if param is not _notexists: + self.param = param + +class Metafunc: def __init__(self, function, config=None, cls=None, module=None): self.config = config self.module = module @@ -33,81 +30,136 @@ self.funcargnames = getfuncargnames(function) self.cls = cls self.module = module - self._combinations = [] + self._calls = [] + self._ids = py.builtin.set() - def addfuncarg(self, argname, value): - if argname not in self.funcargnames: - raise ValueError("function %r has no funcarg %r" %( - self.function, argname)) - newcombi = [] - if not self._combinations: - newcombi.append({argname:value}) - else: - for combi in self._combinations: - if argname in combi: - combi = combi.copy() - newcombi.append(combi) - combi[argname] = value - self._combinations.extend(newcombi) + def addcall(self, funcargs=None, id=_notexists, param=_notexists): + 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 FunctionCollector(py.test.collect.Collector): - def __init__(self, name, parent, combinations): + def __init__(self, name, parent, calls): super(FunctionCollector, self).__init__(name, parent) - self.combinations = combinations + self.calls = calls self.obj = getattr(self.parent.obj, name) def collect(self): l = [] - for i, funcargs in py.builtin.enumerate(self.combinations): - function = self.parent.Function(name="%s[%s]" %(self.name, i), - parent=self, funcargs=funcargs, callobj=self.obj) + for callspec in self.calls: + name = "%s[%s]" %(self.name, callspec.id) + function = self.parent.Function(name=name, parent=self, + callspec=callspec, callobj=self.obj) l.append(function) - return l + return l + + def reportinfo(self): + try: + return self._fslineno, self.name + except AttributeError: + pass + fspath, lineno = py.code.getfslineno(self.obj) + self._fslineno = fspath, lineno + return fspath, lineno, self.name + class FuncargRequest: _argprefix = "pytest_funcarg__" + _argname = None class Error(LookupError): """ error on performing funcarg request. """ - - def __init__(self, pyfuncitem, argname): + + def __init__(self, pyfuncitem): self._pyfuncitem = pyfuncitem - self.argname = argname self.function = pyfuncitem.obj - self.module = pyfuncitem.getmodulecollector().obj + self.module = pyfuncitem.getparent(py.test.collect.Module).obj self.cls = getattr(self.function, 'im_class', None) + self.instance = getattr(self.function, 'im_self', None) self.config = pyfuncitem.config self.fspath = pyfuncitem.fspath + if hasattr(pyfuncitem, '_requestparam'): + self.param = pyfuncitem._requestparam self._plugins = self.config.pluginmanager.getplugins() - self._plugins.append(pyfuncitem.getmodulecollector().obj) - self._provider = self.config.pluginmanager.listattr( - plugins=self._plugins, - attrname=self._argprefix + str(argname) - ) + self._plugins.append(self.module) + if self.instance is not None: + self._plugins.append(self.instance) + self._funcargs = self._pyfuncitem.funcargs.copy() + self._provider = {} + + 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 cached_setup(self, setup, teardown=None, scope="module", extrakey=None): + if not hasattr(self.config, '_setupcache'): + self.config._setupcache = {} # XXX weakref? + cachekey = (self._getscopeitem(scope), extrakey) + cache = self.config._setupcache + try: + val = cache[cachekey] + except KeyError: + val = setup() + cache[cachekey] = val + if teardown is not None: + self.addfinalizer(lambda: teardown(val), scope=scope) + return val + + def getfuncargvalue(self, argname): + try: + return self._funcargs[argname] + except KeyError: + pass + if argname not in self._provider: + self._provider[argname] = self.config.pluginmanager.listattr( + plugins=self._plugins, + attrname=self._argprefix + str(argname) + ) + #else: we are called recursively + if not self._provider[argname]: + self._raiselookupfailed(argname) + funcargprovider = self._provider[argname].pop() + self._funcargs[argname] = res = funcargprovider(request=self) + return res + + def _getscopeitem(self, scope): + if scope == "function": + return self._pyfuncitem + elif scope == "module": + return self._pyfuncitem.getparent(py.test.collect.Module) + elif scope == "session": + return None + raise ValueError("unknown finalization scope %r" %(scope,)) + + def addfinalizer(self, finalizer, scope="function"): + colitem = self._getscopeitem(scope) + self.config._setupstate.addfinalizer(finalizer=finalizer, colitem=colitem) def __repr__(self): - return "" %(self.argname, self._pyfuncitem) - - def call_next_provider(self): - if not self._provider: - raise self.Error("no provider methods left") - next_provider = self._provider.pop() - return next_provider(request=self) - - def addfinalizer(self, finalizer): - self._pyfuncitem.addfinalizer(finalizer) + return "" %(self._pyfuncitem) - def _raiselookupfailed(self): + def _raiselookupfailed(self, argname): available = [] for plugin in self._plugins: - for name in vars(plugin.__class__): + 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.metainfo() + fspath, lineno, msg = self._pyfuncitem.reportinfo() line = "%s:%s" %(fspath, lineno) - msg = "funcargument %r not found for: %s" %(self.argname, line) + msg = "funcargument %r not found for: %s" %(argname, line) msg += "\n available funcargs: %s" %(", ".join(available),) raise LookupError(msg) Modified: py/trunk/py/test/looponfail/remote.py ============================================================================== --- py/trunk/py/test/looponfail/remote.py (original) +++ py/trunk/py/test/looponfail/remote.py Tue Jun 16 18:23:18 2009 @@ -137,10 +137,10 @@ session.shouldclose = channel.isclosed class Failures(list): - def pytest_itemtestreport(self, rep): + def pytest_runtest_logreport(self, rep): if rep.failed: self.append(rep) - pytest_collectreport = pytest_itemtestreport + pytest_collectreport = pytest_runtest_logreport failreports = Failures() session.pluginmanager.register(failreports) @@ -150,4 +150,4 @@ session.config.hook.pytest_looponfailinfo( failreports=list(failreports), rootdirs=[config.topdir]) - channel.send([x.colitem._totrail() for x in failreports]) + channel.send([rep.getnode()._totrail() for rep in failreports]) Modified: py/trunk/py/test/outcome.py ============================================================================== --- py/trunk/py/test/outcome.py (original) +++ py/trunk/py/test/outcome.py Tue Jun 16 18:23:18 2009 @@ -36,11 +36,11 @@ self.expr = expr self.expected = expected -class Exit(Exception): +class Exit(KeyboardInterrupt): """ for immediate program exits without tracebacks and reporter/summary. """ def __init__(self, msg="unknown reason"): self.msg = msg - Exception.__init__(self, msg) + KeyboardInterrupt.__init__(self, msg) # exposed helper methods Deleted: /py/trunk/py/test/plugin/api.py ============================================================================== --- /py/trunk/py/test/plugin/api.py Tue Jun 16 18:23:18 2009 +++ (empty file) @@ -1,142 +0,0 @@ -""" -API definitions for pytest plugin hooks -""" - -class PluginHooks: - # ------------------------------------------------------------------------------ - # Command line and configuration hooks - # ------------------------------------------------------------------------------ - def pytest_addoption(self, parser): - """ called before commandline parsing. """ - - def pytest_configure(self, config): - """ called after command line options have been parsed. - and all plugins and initial conftest files been loaded. - ``config`` provides access to all such configuration values. - """ - - def pytest_unconfigure(self, config): - """ called before test process is exited. - """ - - # ------------------------------------------------------------------------------ - # test Session related hooks - # ------------------------------------------------------------------------------ - - def pytest_testrunstart(self): - """ whole test run starts. """ - - def pytest_testrunfinish(self, exitstatus, excrepr=None): - """ whole test run finishes. """ - - def pytest_deselected(self, items): - """ collected items that were deselected (by keyword). """ - - - - # ------------------------------------------------------------------------------ - # collection hooks - # ------------------------------------------------------------------------------ - def pytest_collect_file(self, path, parent): - """ return Collection node or None. """ - - def pytest_collect_recurse(self, path, parent): - """ return True/False to cause/prevent recursion into given directory. - return None if you do not want to make the decision. - """ - pytest_collect_recurse.firstresult = True - - def pytest_collect_directory(self, path, parent): - """ return Collection node or None. """ - - def pytest_pycollect_obj(self, collector, name, obj): - """ return custom item/collector for a python object in a module, or None. """ - pytest_pycollect_obj.firstresult = True - - def pytest_genfuncruns(self, runspec): - """ generate (multiple) parametrized calls to a test function.""" - - def pytest_collectstart(self, collector): - """ collector starts collecting. """ - - def pytest_collectreport(self, rep): - """ collector finished collecting. """ - - # ------------------------------------------------------------------------------ - # runtest related hooks - # ------------------------------------------------------------------------------ - # - def pytest_itemrun(self, item, pdb=None): - """ run given test item and return test report. """ - - def pytest_pyfunc_call(self, pyfuncitem, args, kwargs): - """ return True if we consumed/did the call to the python function item. """ - pytest_pyfunc_call.firstresult = True - - def pytest_item_makereport(self, item, excinfo, when, outerr): - """ return ItemTestReport for the given test outcome. """ - pytest_item_makereport.firstresult = True - - def pytest_itemstart(self, item, node=None): - """ test item gets collected. """ - - def pytest_itemtestreport(self, rep): - """ test has been run. """ - - def pytest_item_runtest_finished(self, item, excinfo, outerr): - """ test has been run. """ - - def pytest_itemsetupreport(self, rep): - """ a report on running a fixture function. """ - - # ------------------------------------------------------------------------------ - # reporting hooks (invoked from pytest_terminal.py) - # ------------------------------------------------------------------------------ - def pytest_report_teststatus(self, rep): - """ return shortletter and verbose word. """ - pytest_report_teststatus.firstresult = True - - def pytest_terminal_summary(self, terminalreporter): - """ add additional section in terminal summary reporting. """ - - # ------------------------------------------------------------------------------ - # doctest hooks - # ------------------------------------------------------------------------------ - def pytest_doctest_prepare_content(self, content): - """ return processed content for a given doctest""" - pytest_doctest_prepare_content.firstresult = True - - # ------------------------------------------------------------------------------ - # misc hooks - # ------------------------------------------------------------------------------ - - def pytest_plugin_registered(self, plugin): - """ a new py lib plugin got registered. """ - - def pytest_plugin_unregistered(self, plugin): - """ a py lib plugin got unregistered. """ - - def pytest_internalerror(self, excrepr): - """ called for internal errors. """ - - def pytest_trace(self, category, msg): - """ called for debug info. """ - - - - # ------------------------------------------------------------------------------ - # distributed testing - # ------------------------------------------------------------------------------ - - def pytest_testnodeready(self, node): - """ Test Node is ready to operate. """ - - def pytest_testnodedown(self, node, error): - """ Test Node is down. """ - - def pytest_rescheduleitems(self, items): - """ reschedule Items from a node that went down. """ - - def pytest_looponfailinfo(self, failreports, rootdirs): - """ info for repeating failing tests. """ - Modified: py/trunk/py/test/plugin/conftest.py ============================================================================== --- py/trunk/py/test/plugin/conftest.py (original) +++ py/trunk/py/test/plugin/conftest.py Tue Jun 16 18:23:18 2009 @@ -1,10 +1,25 @@ import py -pytest_plugins = "pytester", "plugintester" +pytest_plugins = "pytester" -class ConftestPlugin: - def pytest_collect_file(self, path, parent): - if path.basename.startswith("pytest_") and path.ext == ".py": - mod = parent.Module(path, parent=parent) - return mod +def pytest_collect_file(path, parent): + if path.basename.startswith("pytest_") and path.ext == ".py": + mod = parent.Module(path, parent=parent) + return mod + +# decorate testdir to contain plugin under test +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: + basename = request.module.__name__.split(".")[-1] + if basename.startswith("pytest_"): + testdir.plugins.append(vars(request.module)) + testdir.plugins.append(basename) + else: + pass # raise ValueError("need better support code") + return testdir Added: py/trunk/py/test/plugin/hookspec.py ============================================================================== --- (empty file) +++ py/trunk/py/test/plugin/hookspec.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,150 @@ +""" +py.test hooks / extension points +""" + +# ------------------------------------------------------------------------------ +# Command line and configuration hooks +# ------------------------------------------------------------------------------ +def pytest_addoption(parser): + """ called before commandline parsing. """ + +def pytest_configure(config): + """ called after command line options have been parsed. + and all plugins and initial conftest files been loaded. + ``config`` provides access to all such configuration values. + """ + +def pytest_unconfigure(config): + """ called before test process is exited. """ + +# ------------------------------------------------------------------------------ +# test Session related hooks +# ------------------------------------------------------------------------------ + +def pytest_sessionstart(session): + """ before session.main() is called. """ + +def pytest_sessionfinish(session, exitstatus, excrepr=None): + """ whole test run finishes. """ + +def pytest_deselected(items): + """ repeatedly called for test items deselected by keyword. """ + +# ------------------------------------------------------------------------------ +# collection hooks +# ------------------------------------------------------------------------------ + +def pytest_collect_directory(path, parent): + """ return Collection node or None for the given path. """ + +def pytest_collect_file(path, parent): + """ return Collection node or None for the given path. """ + +def pytest_collectstart(collector): + """ collector starts collecting. """ + +def pytest_collectreport(rep): + """ collector finished collecting. """ + +def pytest_make_collect_report(collector): + """ perform a collection and return a collection. """ +pytest_make_collect_report.firstresult = True + +# XXX rename to item_collected()? meaning in distribution context? +def pytest_itemstart(item, node=None): + """ test item gets collected. """ + +# ------------------------------------------------------------------------------ +# Python test function related hooks +# ------------------------------------------------------------------------------ + +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): + """ perform function call to the with the given function arguments. """ +pytest_pyfunc_call.firstresult = True + +def pytest_generate_tests(metafunc): + """ generate (multiple) parametrized calls to a test function.""" + +# ------------------------------------------------------------------------------ +# generic runtest related hooks +# ------------------------------------------------------------------------------ +def pytest_runtest_setup(item): + """ called before pytest_runtest_call(). """ + +def pytest_runtest_call(item): + """ execute test item. """ + +def pytest_runtest_teardown(item): + """ called after pytest_runtest_call(). """ + +def pytest_runtest_protocol(item): + """ run given test item and return test report. """ +pytest_runtest_protocol.firstresult = True + +def pytest_runtest_makereport(item, call): + """ make ItemTestReport for the specified test outcome. """ +pytest_runtest_makereport.firstresult = True + +def pytest_runtest_logreport(rep): + """ process item test report. """ + +# ------------------------------------------------------------------------------ +# generic reporting hooks (invoked from pytest_terminal.py) +# ------------------------------------------------------------------------------ +def pytest_report_teststatus(rep): + """ return shortletter and verbose word. """ +pytest_report_teststatus.firstresult = True + +def pytest_terminal_summary(terminalreporter): + """ add additional section in terminal summary reporting. """ + +def pytest_report_iteminfo(item): + """ return (fspath, lineno, name) for the item. + the information is used for result display and to sort tests + """ +pytest_report_iteminfo.firstresult = True + +# ------------------------------------------------------------------------------ +# doctest hooks +# ------------------------------------------------------------------------------ +def pytest_doctest_prepare_content(content): + """ return processed content for a given doctest""" +pytest_doctest_prepare_content.firstresult = True + + +# ------------------------------------------------------------------------------ +# misc hooks +# ------------------------------------------------------------------------------ + +def pytest_plugin_registered(plugin): + """ 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_trace(category, msg): + """ called for debug info. """ + +# ------------------------------------------------------------------------------ +# distributed testing +# ------------------------------------------------------------------------------ + +def pytest_testnodeready(node): + """ Test Node is ready to operate. """ + +def pytest_testnodedown(node, error): + """ Test Node is down. """ + +def pytest_rescheduleitems(items): + """ reschedule Items from a node that went down. """ + +def pytest_looponfailinfo(failreports, rootdirs): + """ info for repeating failing tests. """ + Modified: py/trunk/py/test/plugin/pytest__pytest.py ============================================================================== --- py/trunk/py/test/plugin/pytest__pytest.py (original) +++ py/trunk/py/test/plugin/pytest__pytest.py Tue Jun 16 18:23:18 2009 @@ -1,21 +1,23 @@ import py -class _pytestPlugin: - def pytest_funcarg___pytest(self, request): - return PytestArg(request) +def pytest_funcarg___pytest(request): + return PytestArg(request) class PytestArg: def __init__(self, request): self.request = request - - def getcallrecorder(self, hookspecs, comregistry=None): - if comregistry is None: - comregistry = self.request.config.pluginmanager.comregistry - callrecorder = CallRecorder(comregistry) - callrecorder.start_recording(hookspecs) - self.request.addfinalizer(callrecorder.finalize) - return callrecorder - + self.monkeypatch = self.request.getfuncargvalue("monkeypatch") + self.comregistry = py._com.Registry() + self.monkeypatch.setattr(py._com, 'comregistry', self.comregistry) + + def gethookrecorder(self, hookspecs, registry=None): + if registry is not None: + self.monkeypatch.setattr(py._com, 'comregistry', registry) + self.comregistry = registry + hookrecorder = HookRecorder(self.comregistry) + hookrecorder.start_recording(hookspecs) + self.request.addfinalizer(hookrecorder.finish_recording) + return hookrecorder class ParsedCall: def __init__(self, name, locals): @@ -29,7 +31,7 @@ del d['_name'] return "" %(self._name, d) -class CallRecorder: +class HookRecorder: def __init__(self, comregistry): self._comregistry = comregistry self.calls = [] @@ -41,26 +43,22 @@ _recorder = self for name, method in vars(hookspecs).items(): if name[0] != "_": - setattr(RecordCalls, name, self._getcallparser(method)) + setattr(RecordCalls, name, self._makecallparser(method)) recorder = RecordCalls() self._recorders[hookspecs] = recorder self._comregistry.register(recorder) self.hook = py._com.Hooks(hookspecs, registry=self._comregistry) - def finalize(self): + def finish_recording(self): for recorder in self._recorders.values(): self._comregistry.unregister(recorder) self._recorders.clear() - def recordsmethod(self, name): - for hookspecs in self._recorders: - if hasattr(hookspecs, name): - return True - - def _getcallparser(self, method): + def _makecallparser(self, method): name = method.__name__ args, varargs, varkw, default = py.std.inspect.getargspec(method) - assert args[0] == "self" + if 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 @@ -73,13 +71,6 @@ """ % locals()) return locals()[name] - def popcall(self, name): - for i, call in py.builtin.enumerate(self.calls): - if call._name == name: - del self.calls[i] - return call - raise ValueError("could not find call %r in %r" %(name, self.calls)) - def getcalls(self, names): if isinstance(names, str): names = names.split() @@ -96,12 +87,22 @@ l.append(call) return l -def test_generic(plugintester): - plugintester.hookcheck(_pytestPlugin) + def popcall(self, name): + for i, call in py.builtin.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 test_callrecorder_basic(): + +def test_hookrecorder_basic(): comregistry = py._com.Registry() - rec = CallRecorder(comregistry) + rec = HookRecorder(comregistry) class ApiClass: def xyz(self, arg): pass @@ -112,14 +113,19 @@ assert call._name == "xyz" py.test.raises(ValueError, "rec.popcall('abc')") +reg = py._com.comregistry +def test_functional_default(testdir, _pytest): + assert _pytest.comregistry == py._com.comregistry + assert _pytest.comregistry != reg + def test_functional(testdir, linecomp): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" import py pytest_plugins="_pytest" def test_func(_pytest): class ApiClass: def xyz(self, arg): pass - rec = _pytest.getcallrecorder(ApiClass) + rec = _pytest.gethookrecorder(ApiClass) class Plugin: def xyz(self, arg): return arg + 1 @@ -127,4 +133,4 @@ res = rec.hook.xyz(arg=41) assert res == [42] """) - sorter.assertoutcome(passed=1) + reprec.assertoutcome(passed=1) Modified: py/trunk/py/test/plugin/pytest_default.py ============================================================================== --- py/trunk/py/test/plugin/pytest_default.py (original) +++ py/trunk/py/test/plugin/pytest_default.py Tue Jun 16 18:23:18 2009 @@ -1,176 +1,146 @@ -import py +""" Plugin implementing defaults and general options. """ -class DefaultPlugin: - """ Plugin implementing defaults and general options. """ +import py - def pytest_itemrun(self, item, pdb=None): - from py.__.test.runner import basic_run_report, forked_run_report - if item.config.option.boxed: - runner = forked_run_report +def pytest_pyfunc_call(__call__, pyfuncitem): + if not __call__.execute(firstresult=True): + testfunction = pyfuncitem.obj + if pyfuncitem._isyieldedfunction(): + testfunction(*pyfuncitem._args) else: - runner = basic_run_report - report = runner(item, pdb=pdb) - item.config.hook.pytest_itemtestreport(rep=report) - return True - - def pytest_item_makereport(self, item, excinfo, when, outerr): - from py.__.test import runner - return runner.ItemTestReport(item, excinfo, when, outerr) - - def pytest_item_runtest_finished(self, item, excinfo, outerr): - from py.__.test import runner - rep = runner.ItemTestReport(item, excinfo, "execute", outerr) - item.config.hook.pytest_itemtestreport(rep=rep) - - def pytest_pyfunc_call(self, pyfuncitem, args, kwargs): - pyfuncitem.obj(*args, **kwargs) - - def pytest_collect_file(self, path, parent): - ext = path.ext - pb = path.purebasename - if pb.startswith("test_") or pb.endswith("_test") or \ - path in parent.config.args: - if ext == ".py": - return parent.Module(path, parent=parent) - - def pytest_collect_recurse(self, path, parent): - #excludelist = parent._config.getvalue_pathlist('dir_exclude', path) - #if excludelist and path in excludelist: - # return - if not parent.recfilter(path): - # check if cmdline specified this dir or a subdir directly - for arg in parent.config.args: - if path == arg or arg.relto(path): - break - else: - return False - return True - - def pytest_collect_directory(self, path, parent): - # XXX reconsider the following comment - # not use parent.Directory here as we generally - # want dir/conftest.py to be able to - # define Directory(dir) already - Directory = parent.config.getvalue('Directory', path) - return Directory(path, parent=parent) - - def pytest_addoption(self, parser): - group = parser.addgroup("general", "test collection and failure interaction options") - group._addoption('-v', '--verbose', action="count", - dest="verbose", default=0, help="increase verbosity."), - group._addoption('-x', '--exitfirst', - action="store_true", dest="exitfirst", default=False, - help="exit instantly on first error or failed test."), - group._addoption('-k', - action="store", dest="keyword", default='', - help="only run test items matching the given " - "space separated keywords. precede a keyword with '-' to negate. " - "Terminate the expression with ':' to treat a match as a signal " - "to run all subsequent tests. ") - group._addoption('-l', '--showlocals', - action="store_true", dest="showlocals", default=False, - help="show locals in tracebacks (disabled by default).") - #group._addoption('--showskipsummary', - # action="store_true", dest="showskipsummary", default=False, - # help="always show summary of skipped tests") - group._addoption('--pdb', - action="store_true", dest="usepdb", default=False, - help="start pdb (the Python debugger) on errors.") - group._addoption('--tb', metavar="style", - action="store", dest="tbstyle", default='long', - type="choice", choices=['long', 'short', 'no'], - help="traceback verboseness (long/short/no).") - group._addoption('-s', - action="store_true", dest="nocapture", default=False, - help="disable catching of stdout/stderr during test run.") - group.addoption('--boxed', - action="store_true", dest="boxed", default=False, - help="box each test run in a separate process") - group._addoption('-p', action="append", dest="plugin", default = [], - help=("load the specified plugin after command line parsing. " - "Example: '-p hello' will trigger 'import pytest_hello' " - "and instantiate 'HelloPlugin' from the module.")) - group._addoption('-f', '--looponfail', - action="store_true", dest="looponfail", default=False, - help="run tests, re-run failing test set until all pass.") - - group = parser.addgroup("test process debugging") - group.addoption('--collectonly', - action="store_true", dest="collectonly", - help="only collect tests, don't execute them."), - 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('--fulltrace', - action="store_true", dest="fulltrace", default=False, - help="don't cut any tracebacks (default is to cut).") - group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir", - help="base temporary directory for this test run.") - group._addoption('--iocapture', action="store", default="fd", metavar="method", - type="choice", choices=['fd', 'sys', 'no'], - help="set iocapturing method: fd|sys|no.") - group.addoption('--debug', - action="store_true", dest="debug", default=False, - help="generate and show debugging information.") - - group = parser.addgroup("dist", "distributed testing") # see http://pytest.org/help/dist") - group._addoption('--dist', metavar="distmode", - action="store", choices=['load', 'each', 'no'], - type="choice", dest="dist", default="no", - help=("set mode for distributing tests to exec environments.\n\n" - "each: send each test to each available environment.\n\n" - "load: send each test to available environment.\n\n" - "(default) no: run tests inprocess, don't distribute.")) - group._addoption('--tx', dest="tx", action="append", default=[], metavar="xspec", - help=("add a test execution environment. some examples: " - "--tx popen//python=python2.5 --tx socket=192.168.1.102:8888 " - "--tx ssh=user at codespeak.net//chdir=testcache")) - group._addoption('-d', - action="store_true", dest="distload", default=False, - help="load-balance tests. shortcut for '--dist=load'") - group._addoption('-n', dest="numprocesses", metavar="numprocesses", - action="store", type="int", - help="shortcut for '--dist=load --tx=NUM*popen'") - group.addoption('--rsyncdir', action="append", default=[], metavar="dir1", - help="add directory for rsyncing to remote tx nodes.") - - def pytest_configure(self, config): - self.fixoptions(config) - self.setsession(config) - self.loadplugins(config) - - def fixoptions(self, config): - if config.option.numprocesses: - config.option.dist = "load" - config.option.tx = ['popen'] * int(config.option.numprocesses) - if config.option.distload: - config.option.dist = "load" - if config.getvalue("usepdb"): - if config.getvalue("looponfail"): - raise config.Error("--pdb incompatible with --looponfail.") - if config.option.dist != "no": - raise config.Error("--pdb incomptaible with distributing tests.") - - def loadplugins(self, config): - for name in config.getvalue("plugin"): - print "importing", name - config.pluginmanager.import_plugin(name) - - def setsession(self, config): - val = config.getvalue - if val("collectonly"): - from py.__.test.session import Session - config.setsessionclass(Session) + funcargs = pyfuncitem.funcargs + testfunction(**funcargs) + +def pytest_collect_file(path, parent): + ext = path.ext + pb = path.purebasename + if pb.startswith("test_") or pb.endswith("_test") or \ + path in parent.config.args: + if ext == ".py": + return parent.Module(path, parent=parent) + +def pytest_collect_directory(path, parent): + # XXX reconsider the following comment + # not use parent.Directory here as we generally + # want dir/conftest.py to be able to + # define Directory(dir) already + if not parent.recfilter(path): # by default special ".cvs", ... + # check if cmdline specified this dir or a subdir directly + for arg in parent.config.args: + if path == arg or arg.relto(path): + break else: - if val("looponfail"): - from py.__.test.looponfail.remote import LooponfailingSession - config.setsessionclass(LooponfailingSession) - elif val("dist") != "no": - from py.__.test.dist.dsession import DSession - config.setsessionclass(DSession) + return + Directory = parent.config.getvalue('Directory', path) + return Directory(path, parent=parent) + +def pytest_report_iteminfo(item): + return item.reportinfo() + +def pytest_addoption(parser): + group = parser.getgroup("general", "test collection and failure interaction options") + group._addoption('-v', '--verbose', action="count", + dest="verbose", default=0, help="increase verbosity."), + group._addoption('-x', '--exitfirst', + action="store_true", dest="exitfirst", default=False, + help="exit instantly on first error or failed test."), + group._addoption('-k', + action="store", dest="keyword", default='', + help="only run test items matching the given " + "space separated keywords. precede a keyword with '-' to negate. " + "Terminate the expression with ':' to treat a match as a signal " + "to run all subsequent tests. ") + group._addoption('-l', '--showlocals', + action="store_true", dest="showlocals", default=False, + help="show locals in tracebacks (disabled by default).") + #group._addoption('--showskipsummary', + # action="store_true", dest="showskipsummary", default=False, + # help="always show summary of skipped tests") + group._addoption('--tb', metavar="style", + action="store", dest="tbstyle", default='long', + type="choice", choices=['long', 'short', 'no'], + help="traceback verboseness (long/short/no).") + group._addoption('-s', + action="store_true", dest="nocapture", default=False, + help="disable catching of stdout/stderr during test run.") + group._addoption('-p', action="append", dest="plugin", default = [], + help=("load the specified plugin after command line parsing. ")) + group._addoption('-f', '--looponfail', + action="store_true", dest="looponfail", default=False, + help="run tests, re-run failing test set until all pass.") + + group = parser.addgroup("test process debugging") + group.addoption('--collectonly', + action="store_true", dest="collectonly", + help="only collect tests, don't execute them."), + 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('--fulltrace', + action="store_true", dest="fulltrace", default=False, + help="don't cut any tracebacks (default is to cut).") + group.addoption('--basetemp', dest="basetemp", default=None, metavar="dir", + help="base temporary directory for this test run.") + group._addoption('--iocapture', action="store", default="fd", metavar="method", + type="choice", choices=['fd', 'sys', 'no'], + help="set iocapturing method: fd|sys|no.") + group.addoption('--debug', + action="store_true", dest="debug", default=False, + help="generate and show debugging information.") + + group = parser.addgroup("dist", "distributed testing") # see http://pytest.org/help/dist") + group._addoption('--dist', metavar="distmode", + action="store", choices=['load', 'each', 'no'], + type="choice", dest="dist", default="no", + help=("set mode for distributing tests to exec environments.\n\n" + "each: send each test to each available environment.\n\n" + "load: send each test to available environment.\n\n" + "(default) no: run tests inprocess, don't distribute.")) + group._addoption('--tx', dest="tx", action="append", default=[], metavar="xspec", + help=("add a test execution environment. some examples: " + "--tx popen//python=python2.5 --tx socket=192.168.1.102:8888 " + "--tx ssh=user at codespeak.net//chdir=testcache")) + group._addoption('-d', + action="store_true", dest="distload", default=False, + help="load-balance tests. shortcut for '--dist=load'") + group._addoption('-n', dest="numprocesses", metavar="numprocesses", + action="store", type="int", + help="shortcut for '--dist=load --tx=NUM*popen'") + group.addoption('--rsyncdir', action="append", default=[], metavar="dir1", + help="add directory for rsyncing to remote tx nodes.") + +def pytest_configure(config): + fixoptions(config) + setsession(config) + loadplugins(config) + +def fixoptions(config): + if config.option.numprocesses: + config.option.dist = "load" + config.option.tx = ['popen'] * int(config.option.numprocesses) + if config.option.distload: + config.option.dist = "load" + +def loadplugins(config): + for name in config.getvalue("plugin"): + print "importing", name + config.pluginmanager.import_plugin(name) + +def setsession(config): + val = config.getvalue + if val("collectonly"): + from py.__.test.session import Session + config.setsessionclass(Session) + else: + if val("looponfail"): + from py.__.test.looponfail.remote import LooponfailingSession + config.setsessionclass(LooponfailingSession) + elif val("dist") != "no": + from py.__.test.dist.dsession import DSession + config.setsessionclass(DSession) def test_implied_different_sessions(tmpdir): def x(*args): @@ -186,9 +156,6 @@ assert x('-n3') == 'DSession' assert x('-f') == 'LooponfailingSession' -def test_generic(plugintester): - plugintester.hookcheck(DefaultPlugin) - def test_plugin_specify(testdir): testdir.chdir() config = py.test.raises(ImportError, """ @@ -239,12 +206,18 @@ assert testdir.tmpdir.join('x') in roots def test_dist_options(testdir): - py.test.raises(Exception, "testdir.parseconfigure('--pdb', '--looponfail')") - py.test.raises(Exception, "testdir.parseconfigure('--pdb', '-n 3')") - py.test.raises(Exception, "testdir.parseconfigure('--pdb', '-d')") config = testdir.parseconfigure("-n 2") assert config.option.dist == "load" assert config.option.tx == ['popen'] * 2 config = testdir.parseconfigure("-d") assert config.option.dist == "load" + +def test_pytest_report_iteminfo(): + class FakeItem(object): + + def reportinfo(self): + return "-reportinfo-" + + res = pytest_report_iteminfo(FakeItem()) + assert res == "-reportinfo-" Modified: py/trunk/py/test/plugin/pytest_doctest.py ============================================================================== --- py/trunk/py/test/plugin/pytest_doctest.py (original) +++ py/trunk/py/test/plugin/pytest_doctest.py Tue Jun 16 18:23:18 2009 @@ -1,20 +1,22 @@ +""" +automatically collect and execute doctests. +""" + import py +from py.__.code.excinfo import Repr, ReprFileLocation -class DoctestPlugin: - def pytest_addoption(self, parser): - group = parser.addgroup("doctest options") - group.addoption("--doctest-modules", - action="store_true", default=False, - dest="doctestmodules") +def pytest_addoption(parser): + group = parser.addgroup("doctest options") + group.addoption("--doctest-modules", + action="store_true", default=False, + dest="doctestmodules") - def pytest_collect_file(self, path, parent): - if path.ext == ".py": - if parent.config.getvalue("doctestmodules"): - return DoctestModule(path, parent) - if path.check(fnmatch="test_*.txt"): - return DoctestTextfile(path, parent) - -from py.__.code.excinfo import Repr, ReprFileLocation +def pytest_collect_file(path, parent): + if path.ext == ".py": + if parent.config.getvalue("doctestmodules"): + return DoctestModule(path, parent) + if path.check(fnmatch="test_*.txt"): + return DoctestTextfile(path, parent) class ReprFailDoctest(Repr): def __init__(self, reprlocation, lines): @@ -76,8 +78,8 @@ # class TestDoctests: + def test_collect_testtextfile(self, testdir): - testdir.plugins.append(DoctestPlugin()) testdir.maketxtfile(whatever="") checkfile = testdir.maketxtfile(test_something=""" alskdjalsdk @@ -87,40 +89,37 @@ """) for x in (testdir.tmpdir, checkfile): #print "checking that %s returns custom items" % (x,) - items, sorter = testdir.inline_genitems(x) + items, reprec = testdir.inline_genitems(x) assert len(items) == 1 assert isinstance(items[0], DoctestTextfile) def test_collect_module(self, testdir): - testdir.plugins.append(DoctestPlugin()) path = testdir.makepyfile(whatever="#") for p in (path, testdir.tmpdir): - items, evrec = testdir.inline_genitems(p, '--doctest-modules') + items, reprec = testdir.inline_genitems(p, '--doctest-modules') assert len(items) == 1 assert isinstance(items[0], DoctestModule) def test_simple_doctestfile(self, testdir): - testdir.plugins.append(DoctestPlugin()) p = testdir.maketxtfile(test_doc=""" >>> x = 1 >>> x == 1 False """) - sorter = testdir.inline_run(p) - sorter.assertoutcome(failed=1) + reprec = testdir.inline_run(p) + reprec.assertoutcome(failed=1) def test_doctest_unexpected_exception(self, testdir): from py.__.test.outcome import Failed - testdir.plugins.append(DoctestPlugin()) p = testdir.maketxtfile(""" >>> i = 0 >>> i = 1 >>> x 2 """) - sorter = testdir.inline_run(p) - call = sorter.getcall("pytest_itemtestreport") + reprec = testdir.inline_run(p) + call = reprec.getcall("pytest_runtest_logreport") assert call.rep.failed assert call.rep.longrepr # XXX @@ -130,7 +129,6 @@ #assert repr.reprlocation def test_doctestmodule(self, testdir): - testdir.plugins.append(DoctestPlugin()) p = testdir.makepyfile(""" ''' >>> x = 1 @@ -139,11 +137,10 @@ ''' """) - sorter = testdir.inline_run(p, "--doctest-modules") - sorter.assertoutcome(failed=1) + reprec = testdir.inline_run(p, "--doctest-modules") + reprec.assertoutcome(failed=1) def test_txtfile_failing(self, testdir): - testdir.plugins.append('pytest_doctest') p = testdir.maketxtfile(""" >>> i = 0 >>> i + 1 @@ -159,8 +156,3 @@ " 1", "*test_txtfile_failing.txt:2: DocTestFailure" ]) - - -def test_generic(plugintester): - plugintester.hookcheck(DoctestPlugin) - Deleted: /py/trunk/py/test/plugin/pytest_eventlog.py ============================================================================== --- /py/trunk/py/test/plugin/pytest_eventlog.py Tue Jun 16 18:23:18 2009 +++ (empty file) @@ -1,42 +0,0 @@ -import py - -class EventlogPlugin: - """ log pytest events to a file. """ - def pytest_addoption(self, parser): - parser.addoption("--eventlog", dest="eventlog", - help="write all pytest events to the given file.") - - def pytest_configure(self, config): - eventlog = config.getvalue("eventlog") - if eventlog: - self.eventlogfile = open(eventlog, 'w') - - def pytest_unconfigure(self, config): - if hasattr(self, 'eventlogfile'): - self.eventlogfile.close() - del self.eventlogfile - - def pyevent(self, eventname, args, kwargs): - if hasattr(self, 'eventlogfile'): - f = self.eventlogfile - print >>f, eventname, args, kwargs - f.flush() - -# =============================================================================== -# plugin tests -# =============================================================================== - - at py.test.mark.xfail -def test_generic(plugintester): - plugintester.hookcheck(EventlogPlugin) - - testdir = plugintester.testdir() - testdir.makepyfile(""" - def test_pass(): - pass - """) - testdir.runpytest("--eventlog=event.log") - s = testdir.tmpdir.join("event.log").read() - assert s.find("testrunstart") != -1 - assert s.find("ItemTestReport") != -1 - assert s.find("testrunfinish") != -1 Modified: py/trunk/py/test/plugin/pytest_execnetcleanup.py ============================================================================== --- py/trunk/py/test/plugin/pytest_execnetcleanup.py (original) +++ py/trunk/py/test/plugin/pytest_execnetcleanup.py Tue Jun 16 18:23:18 2009 @@ -1,40 +1,36 @@ +""" +cleanup gateways that were instantiated during a test function run. +""" import py -class ExecnetcleanupPlugin: - _gateways = None - _debug = None +def pytest_configure(config): + config.pluginmanager.register(Execnetcleanup()) - def pytest_configure(self, config): - self._debug = config.option.debug +class Execnetcleanup: + _gateways = None + def __init__(self, debug=False): + self._debug = debug - def trace(self, msg, *args): - if self._debug: - print "[execnetcleanup %0x] %s %s" %(id(self), msg, args) - def pyexecnet_gateway_init(self, gateway): - self.trace("init", gateway) if self._gateways is not None: self._gateways.append(gateway) def pyexecnet_gateway_exit(self, gateway): - self.trace("exit", gateway) if self._gateways is not None: self._gateways.remove(gateway) - def pytest_testrunstart(self): - self.trace("testrunstart") + def pytest_sessionstart(self, session): self._gateways = [] - def pytest_testrunfinish(self, exitstatus, excrepr=None): - self.trace("testrunfinish", exitstatus) + def pytest_sessionfinish(self, session, exitstatus, excrepr=None): l = [] for gw in self._gateways: gw.exit() l.append(gw) #for gw in l: # gw.join() - # - def pytest_pyfunc_call(self, __call__, pyfuncitem, args, kwargs): + + def pytest_pyfunc_call(self, __call__, pyfuncitem): if self._gateways is not None: gateways = self._gateways[:] res = __call__.execute(firstresult=True) @@ -42,9 +38,6 @@ self._gateways[-1].exit() return res -def test_generic(plugintester): - plugintester.hookcheck(ExecnetcleanupPlugin) - @py.test.mark.xfail("clarify plugin registration/unregistration") def test_execnetplugin(testdir): p = ExecnetcleanupPlugin() Modified: py/trunk/py/test/plugin/pytest_figleaf.py ============================================================================== --- py/trunk/py/test/plugin/pytest_figleaf.py (original) +++ py/trunk/py/test/plugin/pytest_figleaf.py Tue Jun 16 18:23:18 2009 @@ -1,65 +1,58 @@ +""" +write and report coverage data using the 'figleaf' module. +""" import py -class FigleafPlugin: - def pytest_addoption(self, parser): - group = parser.addgroup('figleaf options') - group.addoption('-F', action='store_true', default=False, - dest = 'figleaf', - help=('trace coverage with figleaf and write HTML ' - 'for files below the current working dir')) - group.addoption('--figleaf-data', action='store', default='.figleaf', - dest='figleafdata', - help='path coverage tracing file.') - group.addoption('--figleaf-html', action='store', default='html', - dest='figleafhtml', - help='path to the coverage html dir.') - - def pytest_configure(self, config): - if config.getvalue('figleaf'): - try: - import figleaf - import figleaf.annotate_html - except ImportError: - raise config.Error('Could not import figleaf module') - self.figleaf = figleaf - self.figleaf.start() - - def pytest_terminal_summary(self, terminalreporter): - if hasattr(self, 'figleaf'): - config = terminalreporter.config - datafile = py.path.local(config.getvalue('figleafdata')) - tw = terminalreporter._tw - tw.sep('-', 'figleaf') - tw.line('Writing figleaf data to %s' % (datafile)) - self.figleaf.stop() - self.figleaf.write_coverage(str(datafile)) - coverage = self.get_coverage(datafile, config) - - reportdir = py.path.local(config.getvalue('figleafhtml')) - tw.line('Writing figleaf html to file://%s' % (reportdir)) - self.figleaf.annotate_html.prepare_reportdir(str(reportdir)) - exclude = [] - self.figleaf.annotate_html.report_as_html(coverage, - str(reportdir), exclude, {}) - - def get_coverage(self, datafile, config): - # basepath = config.topdir - basepath = py.path.local() - data = self.figleaf.read_coverage(str(datafile)) - d = {} - coverage = self.figleaf.combine_coverage(d, data) - for path in coverage.keys(): - if not py.path.local(path).relto(basepath): - del coverage[path] - return coverage +figleaf = py.test.importorskip("figleaf") +import figleaf.annotate_html +def pytest_addoption(parser): + group = parser.addgroup('figleaf options') + group.addoption('-F', action='store_true', default=False, + dest = 'figleaf', + help=('trace coverage with figleaf and write HTML ' + 'for files below the current working dir')) + group.addoption('--figleaf-data', action='store', default='.figleaf', + dest='figleafdata', + help='path coverage tracing file.') + group.addoption('--figleaf-html', action='store', default='html', + dest='figleafhtml', + help='path to the coverage html dir.') + +def pytest_configure(config): + figleaf.start() + +def pytest_terminal_summary(terminalreporter): + config = terminalreporter.config + datafile = py.path.local(config.getvalue('figleafdata')) + tw = terminalreporter._tw + tw.sep('-', 'figleaf') + tw.line('Writing figleaf data to %s' % (datafile)) + figleaf.stop() + figleaf.write_coverage(str(datafile)) + coverage = get_coverage(datafile, config) + reportdir = py.path.local(config.getvalue('figleafhtml')) + tw.line('Writing figleaf html to file://%s' % (reportdir)) + figleaf.annotate_html.prepare_reportdir(str(reportdir)) + exclude = [] + figleaf.annotate_html.report_as_html(coverage, + str(reportdir), exclude, {}) + +def get_coverage(datafile, config): + # basepath = config.topdir + basepath = py.path.local() + data = figleaf.read_coverage(str(datafile)) + d = {} + coverage = figleaf.combine_coverage(d, data) + for path in coverage.keys(): + if not py.path.local(path).relto(basepath): + del coverage[path] + return coverage -def test_generic(plugintester): - plugintester.hookcheck(FigleafPlugin) def test_functional(testdir): py.test.importorskip("figleaf") - testdir.plugins.append('figleaf') + testdir.plugins.append("figleaf") testdir.makepyfile(""" def f(): x = 42 Added: py/trunk/py/test/plugin/pytest_hooklog.py ============================================================================== --- (empty file) +++ py/trunk/py/test/plugin/pytest_hooklog.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,33 @@ +""" log calling of plugin hooks to a file. """ +import py + +def pytest_addoption(parser): + parser.addoption("--hooklog", dest="hooklog", default=None, + help="write hook calls to the given file.") + +def pytest_configure(config): + hooklog = config.getvalue("hooklog") + if hooklog: + assert not config.pluginmanager.comregistry.logfile + config.pluginmanager.comregistry.logfile = open(hooklog, 'w') + +def pytest_unconfigure(config): + f = config.pluginmanager.comregistry.logfile + if f: + f.close() + config.pluginmanager.comregistry.logfile = None + +# =============================================================================== +# plugin tests +# =============================================================================== + +def test_functional(testdir): + testdir.makepyfile(""" + def test_pass(): + pass + """) + testdir.runpytest("--hooklog=hook.log") + s = testdir.tmpdir.join("hook.log").read() + assert s.find("pytest_sessionstart") != -1 + assert s.find("ItemTestReport") != -1 + assert s.find("sessionfinish") != -1 Modified: py/trunk/py/test/plugin/pytest_iocapture.py ============================================================================== --- py/trunk/py/test/plugin/pytest_iocapture.py (original) +++ py/trunk/py/test/plugin/pytest_iocapture.py Tue Jun 16 18:23:18 2009 @@ -1,51 +1,64 @@ +""" +'capsys' and 'capfd' funcargs for capturing stdout/stderror either +by intercepting sys.stdout/stderr or File Descriptors 1/2. + +Calling the reset() method of the capture funcargs gives +a out/err tuple of strings representing the captured streams. +You can call reset() multiple times each time getting +the chunk of output that was captured between the invocations. + +""" import py -class IocapturePlugin: - """ capture sys.stdout/sys.stderr / fd1/fd2. """ - def pytest_funcarg__stdcapture(self, request): - capture = Capture(py.io.StdCapture) - request.addfinalizer(capture.finalize) - return capture - - def pytest_funcarg__stdcapturefd(self, request): - capture = Capture(py.io.StdCaptureFD) - request.addfinalizer(capture.finalize) - return capture +def pytest_funcarg__capsys(request): + """ capture writes to sys.stdout/sys.stderr. """ + capture = Capture(py.io.StdCapture) + request.addfinalizer(capture.finalize) + return capture + +def pytest_funcarg__capfd(request): + """ capture writes to filedescriptors 1 and 2""" + capture = Capture(py.io.StdCaptureFD) + request.addfinalizer(capture.finalize) + return capture + +def pytest_pyfunc_call(pyfuncitem): + for funcarg, value in pyfuncitem.funcargs.items(): + if funcarg == "capsys" or funcarg == "capfd": + value.reset() class Capture: + _capture = None def __init__(self, captureclass): self._captureclass = captureclass - self._capture = self._captureclass() def finalize(self): - self._capture.reset() + if self._capture: + self._capture.reset() def reset(self): - res = self._capture.reset() + res = None + if self._capture: + res = self._capture.reset() self._capture = self._captureclass() return res -def test_generic(plugintester): - plugintester.hookcheck(IocapturePlugin) - class TestCapture: def test_std_functional(self, testdir): - testdir.plugins.append(IocapturePlugin()) - evrec = testdir.inline_runsource(""" - def test_hello(stdcapture): + reprec = testdir.inline_runsource(""" + def test_hello(capsys): print 42 - out, err = stdcapture.reset() + out, err = capsys.reset() assert out.startswith("42") """) - evrec.assertoutcome(passed=1) + reprec.assertoutcome(passed=1) def test_stdfd_functional(self, testdir): - testdir.plugins.append(IocapturePlugin()) - evrec = testdir.inline_runsource(""" - def test_hello(stdcapturefd): + reprec = testdir.inline_runsource(""" + def test_hello(capfd): import os os.write(1, "42") - out, err = stdcapturefd.reset() + out, err = capfd.reset() assert out.startswith("42") """) - evrec.assertoutcome(passed=1) + reprec.assertoutcome(passed=1) Modified: py/trunk/py/test/plugin/pytest_monkeypatch.py ============================================================================== --- py/trunk/py/test/plugin/pytest_monkeypatch.py (original) +++ py/trunk/py/test/plugin/pytest_monkeypatch.py Tue Jun 16 18:23:18 2009 @@ -1,11 +1,20 @@ +""" + "monkeypatch" funcarg for safely patching objects, + dictionaries and environment variables during the execution of + a test. "monkeypatch" has three helper functions: + + monkeypatch.setattr(obj, name, value) + monkeypatch.setitem(obj, name, value) + monkeypatch.setenv(name, value) + + After the test has run modifications will be undone. +""" import os -class MonkeypatchPlugin: - """ setattr-monkeypatching with automatical reversal after test. """ - def pytest_funcarg__monkeypatch(self, request): - monkeypatch = MonkeyPatch() - request.addfinalizer(monkeypatch.finalize) - return monkeypatch +def pytest_funcarg__monkeypatch(request): + monkeypatch = MonkeyPatch() + request.addfinalizer(monkeypatch.finalize) + return monkeypatch notset = object() @@ -77,11 +86,11 @@ assert 'XYZ123' not in os.environ def test_monkeypatch_plugin(testdir): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" pytest_plugins = 'pytest_monkeypatch', def test_method(monkeypatch): assert monkeypatch.__class__.__name__ == "MonkeyPatch" """) - res = sorter.countoutcomes() + res = reprec.countoutcomes() assert tuple(res) == (1, 0, 0), res Modified: py/trunk/py/test/plugin/pytest_pdb.py ============================================================================== --- py/trunk/py/test/plugin/pytest_pdb.py (original) +++ py/trunk/py/test/plugin/pytest_pdb.py Tue Jun 16 18:23:18 2009 @@ -1,9 +1,155 @@ -from py.__.test.custompdb import post_mortem +""" +interactive debugging with a PDB prompt. -class PdbPlugin: - def pytest_item_runtest_finished(self, item, excinfo, outerr): - if excinfo and item.config.option.usepdb: +""" +import py +import pdb, sys, linecache +from py.__.test.outcome import Skipped + +def pytest_addoption(parser): + group = parser.getgroup("general") + group._addoption('--pdb', + action="store_true", dest="usepdb", default=False, + help="start pdb (the Python debugger) on errors.") + + +def pytest_configure(config): + if config.option.usepdb: + if config.getvalue("looponfail"): + raise config.Error("--pdb incompatible with --looponfail.") + if config.option.dist != "no": + raise config.Error("--pdb incomptaible with distributing tests.") + config.pluginmanager.register(PdbInvoke()) + +class PdbInvoke: + def pytest_runtest_makereport(self, item, call): + if call.excinfo and not call.excinfo.errisinstance(Skipped): tw = py.io.TerminalWriter() - repr = excinfo.getrepr() + repr = call.excinfo.getrepr() repr.toterminal(tw) - post_mortem(excinfo._excinfo[2]) + post_mortem(call.excinfo._excinfo[2]) + +class Pdb(py.std.pdb.Pdb): + def do_list(self, arg): + self.lastcmd = 'list' + last = None + if arg: + try: + x = eval(arg, {}, {}) + if type(x) == type(()): + first, last = x + first = int(first) + last = int(last) + if last < first: + # Assume it's a count + last = first + last + else: + first = max(1, int(x) - 5) + except: + print '*** Error in argument:', repr(arg) + return + elif self.lineno is None: + first = max(1, self.curframe.f_lineno - 5) + else: + first = self.lineno + 1 + if last is None: + last = first + 10 + filename = self.curframe.f_code.co_filename + breaklist = self.get_file_breaks(filename) + try: + for lineno in range(first, last+1): + # start difference from normal do_line + line = self._getline(filename, lineno) + # end difference from normal do_line + if not line: + print '[EOF]' + break + else: + s = repr(lineno).rjust(3) + if len(s) < 4: s = s + ' ' + if lineno in breaklist: s = s + 'B' + else: s = s + ' ' + if lineno == self.curframe.f_lineno: + s = s + '->' + print s + '\t' + line, + self.lineno = lineno + except KeyboardInterrupt: + pass + do_l = do_list + + def _getline(self, filename, lineno): + if hasattr(filename, "__source__"): + try: + return filename.__source__.lines[lineno - 1] + "\n" + except IndexError: + return None + return linecache.getline(filename, lineno) + + def get_stack(self, f, t): + # Modified from bdb.py to be able to walk the stack beyond generators, + # which does not work in the normal pdb :-( + stack, i = pdb.Pdb.get_stack(self, f, t) + if f is None: + i = max(0, len(stack) - 1) + return stack, i + +def post_mortem(t): + # modified from pdb.py for the new get_stack() implementation + p = Pdb() + p.reset() + p.interaction(None, t) + +def set_trace(): + # again, a copy of the version in pdb.py + Pdb().set_trace(sys._getframe().f_back) + + +class TestPDB: + def pytest_funcarg__pdblist(self, request): + monkeypatch = request.getfuncargvalue("monkeypatch") + pdblist = [] + def mypdb(*args): + pdblist.append(args) + monkeypatch.setitem(globals(), 'post_mortem', mypdb) + return pdblist + + def test_incompatibility_messages(self, testdir): + Error = py.test.config.Error + py.test.raises(Error, "testdir.parseconfigure('--pdb', '--looponfail')") + py.test.raises(Error, "testdir.parseconfigure('--pdb', '-n 3')") + py.test.raises(Error, "testdir.parseconfigure('--pdb', '-d')") + + 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_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_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() + child.expect("1 failed") + if child.isalive(): + child.wait() + Deleted: /py/trunk/py/test/plugin/pytest_plugintester.py ============================================================================== --- /py/trunk/py/test/plugin/pytest_plugintester.py Tue Jun 16 18:23:18 2009 +++ (empty file) @@ -1,87 +0,0 @@ -""" -plugin with support classes and functions for testing pytest functionality -""" -import py -from py.__.test.plugin import api - -class PlugintesterPlugin: - """ test support code for testing pytest plugins. """ - def pytest_funcarg__plugintester(self, request): - return PluginTester(request) - -class PluginTester: - def __init__(self, request): - self.request = request - - def testdir(self): - from pytest_pytester import TmpTestdir - crunner = TmpTestdir(self.request) - self.request.addfinalizer(crunner.finalize) - # - #for colitem in self.request.listchain(): - # if isinstance(colitem, py.test.collect.Module) and \ - # colitem.name.startswith("pytest_"): - # crunner.plugins.append(colitem.fspath.purebasename) - # break - return crunner - - def hookcheck(self, pluginclass): - print "loading and checking", pluginclass - fail = False - pm = py.test._PluginManager() - methods = collectattr(pluginclass) - hooks = collectattr(api.PluginHooks) - getargs = py.std.inspect.getargs - - def isgenerichook(name): - return name.startswith("pytest_funcarg__") - - while methods: - name, method = methods.popitem() - if isgenerichook(name): - continue - if name not in hooks: - print "found unknown hook: %s" % name - fail = True - else: - hook = hooks[name] - if not hasattr(hook, 'func_code'): - continue # XXX do some checks on attributes as well? - method_args = getargs(method.func_code) - if '__call__' in method_args[0]: - method_args[0].remove('__call__') - hookargs = getargs(hook.func_code) - for arg, hookarg in zip(method_args[0], hookargs[0]): - if arg != hookarg: - print "argument mismatch:" - print "actual : %s.%s" %(pluginclass.__name__, formatdef(method)) - print "required:", formatdef(hook) - fail = True - break - if not fail: - print "matching hook:", formatdef(method) - if fail: - py.test.fail("Plugin API error") - -def collectattr(obj, prefixes=("pytest_",)): - methods = {} - for apiname in vars(obj): - for prefix in prefixes: - if apiname.startswith(prefix): - methods[apiname] = getattr(obj, apiname) - return methods - -def formatdef(func): - formatargspec = py.std.inspect.formatargspec - getargspec = py.std.inspect.formatargspec - return "%s%s" %( - func.func_name, - py.std.inspect.formatargspec(*py.std.inspect.getargspec(func)) - ) - -# =============================================================================== -# plugin tests -# =============================================================================== - -def test_generic(plugintester): - plugintester.hookcheck(PlugintesterPlugin) Modified: py/trunk/py/test/plugin/pytest_pocoo.py ============================================================================== --- py/trunk/py/test/plugin/pytest_pocoo.py (original) +++ py/trunk/py/test/plugin/pytest_pocoo.py Tue Jun 16 18:23:18 2009 @@ -8,49 +8,43 @@ xmlrpc = base + "/xmlrpc/" show = base + "/show/" -class PocooPlugin: - """ report URLs from sending test failures to the pocoo paste service. """ +def pytest_addoption(parser): + group = parser.addgroup("pocoo plugin") + group.addoption('-P', '--pocoo-sendfailures', + action='store_true', dest="pocoo_sendfailures", + help="send failures to %s paste service" %(url.base,)) + +def getproxy(): + return py.std.xmlrpclib.ServerProxy(url.xmlrpc).pastes + +def pytest_terminal_summary(terminalreporter): + if terminalreporter.config.option.pocoo_sendfailures: + tr = terminalreporter + if 'failed' in tr.stats and tr.config.option.tbstyle != "no": + terminalreporter.write_sep("=", "Sending failures to %s" %(url.base,)) + terminalreporter.write_line("xmlrpcurl: %s" %(url.xmlrpc,)) + #print self.__class__.getproxy + #print self.__class__, id(self.__class__) + serverproxy = getproxy() + for ev in terminalreporter.stats.get('failed'): + tw = py.io.TerminalWriter(stringio=True) + ev.toterminal(tw) + s = tw.stringio.getvalue() + # XXX add failure summary + assert len(s) + terminalreporter.write_line("newpaste() ...") + proxyid = serverproxy.newPaste("python", s) + terminalreporter.write_line("%s%s\n" % (url.show, proxyid)) + break - def pytest_addoption(self, parser): - parser.addoption('-P', '--pocoo-sendfailures', - action='store_true', dest="pocoo_sendfailures", - help="send failures to %s" %(url.base,)) - - def getproxy(self): - return py.std.xmlrpclib.ServerProxy(url.xmlrpc).pastes - - def pytest_terminal_summary(self, terminalreporter): - if terminalreporter.config.option.pocoo_sendfailures: - tr = terminalreporter - if 'failed' in tr.stats and tr.config.option.tbstyle != "no": - terminalreporter.write_sep("=", "Sending failures to %s" %(url.base,)) - terminalreporter.write_line("xmlrpcurl: %s" %(url.xmlrpc,)) - print self.__class__.getproxy - print self.__class__, id(self.__class__) - serverproxy = self.getproxy() - for ev in terminalreporter.stats.get('failed'): - tw = py.io.TerminalWriter(stringio=True) - ev.toterminal(tw) - s = tw.stringio.getvalue() - # XXX add failure summary - assert len(s) - terminalreporter.write_line("newpaste() ...") - proxyid = serverproxy.newPaste("python", s) - terminalreporter.write_line("%s%s\n" % (url.show, proxyid)) - break - - -def test_apicheck(plugintester): - plugintester.hookcheck(PocooPlugin) def test_toproxy(testdir, monkeypatch): l = [] class MockProxy: def newPaste(self, language, code): l.append((language, code)) - monkeypatch.setattr(PocooPlugin, 'getproxy', MockProxy) - testdir.plugins.insert(0, PocooPlugin()) - testdir.chdir() + monkeypatch.setitem(globals(), 'getproxy', MockProxy) + testdir.plugins.insert(0, globals()) testpath = testdir.makepyfile(""" import py def test_pass(): @@ -60,10 +54,10 @@ def test_skip(): py.test.skip("") """) - evrec = testdir.inline_run(testpath, "-P") + reprec = testdir.inline_run(testpath, "-P") assert len(l) == 1 assert l[0][0] == "python" s = l[0][1] assert s.find("def test_fail") != -1 - assert evrec.countoutcomes() == [1,1,1] + assert reprec.countoutcomes() == [1,1,1] Modified: py/trunk/py/test/plugin/pytest_pylint.py ============================================================================== --- py/trunk/py/test/plugin/pytest_pylint.py (original) +++ py/trunk/py/test/plugin/pytest_pylint.py Tue Jun 16 18:23:18 2009 @@ -1,36 +1,24 @@ """pylint plugin - XXX: Currently in progress, NOT IN WORKING STATE. """ import py -class PylintPlugin: - def pytest_addoption(self, parser): - group = parser.addgroup('pylint options') - group.addoption('--pylint', action='store_true', - default=False, dest='pylint', - help='Pylint coverate of test files.') - - def pytest_configure(self, config): - if config.getvalue('pylint'): - try: - from pylint import lint - self.lint = lint - except ImportError: - raise config.Error('Could not import pylint module') - print "trying to configure pytest" - - def pytest_collect_file(self, path, parent): - if path.ext == ".py": - if parent.config.getvalue('pylint'): - return PylintItem(path, parent, self.lint) - - def pytest_terminal_summary(self, terminalreporter): - if hasattr(self, 'lint'): - print 'placeholder for pylint output' +lint = py.test.importorskip("pylint") +def pytest_addoption(parser): + group = parser.addgroup('pylint options') + group.addoption('--pylint', action='store_true', + default=False, dest='pylint', + help='Pylint coverate of test files.') + +def pytest_collect_file(path, parent): + if path.ext == ".py": + if parent.config.getvalue('pylint'): + return PylintItem(path, parent, self.lint) +def pytest_terminal_summary(terminalreporter): + print 'placeholder for pylint output' class PylintItem(py.test.collect.Item): def __init__(self, path, parent, lintlib): @@ -50,8 +38,4 @@ print ">>>", print rating -def test_generic(plugintester): - plugintester.hookcheck(PylintPlugin) - -#def test_functional Modified: py/trunk/py/test/plugin/pytest_pytester.py ============================================================================== --- py/trunk/py/test/plugin/pytest_pytester.py (original) +++ py/trunk/py/test/plugin/pytest_pytester.py Tue Jun 16 18:23:18 2009 @@ -1,34 +1,29 @@ """ -pytes plugin for easing testing of pytest runs themselves. +funcargs and support code for testing py.test functionality. """ import py import os import inspect -from py.__.test import runner from py.__.test.config import Config as pytestConfig -from pytest__pytest import CallRecorder -import api +import hookspec +pytest_plugins = '_pytest' -class PytesterPlugin: - def pytest_funcarg__linecomp(self, request): - return LineComp() - - def pytest_funcarg__LineMatcher(self, request): - return LineMatcher - - def pytest_funcarg__testdir(self, request): - tmptestdir = TmpTestdir(request) - return tmptestdir - - def pytest_funcarg__eventrecorder(self, request): - evrec = EventRecorder(py._com.comregistry) - request.addfinalizer(lambda: evrec.comregistry.unregister(evrec)) - return evrec +def pytest_funcarg__linecomp(request): + return LineComp() -def test_generic(plugintester): - plugintester.hookcheck(PytesterPlugin) +def pytest_funcarg__LineMatcher(request): + return LineMatcher + +def pytest_funcarg__testdir(request): + tmptestdir = TmpTestdir(request) + return tmptestdir + +def pytest_funcarg__reportrecorder(request): + reprec = ReportRecorder(py._com.comregistry) + request.addfinalizer(lambda: reprec.comregistry.unregister(reprec)) + return reprec class RunResult: def __init__(self, ret, outlines, errlines): @@ -41,6 +36,7 @@ class TmpTestdir: def __init__(self, request): self.request = request + self._pytest = request.getfuncargvalue("_pytest") # XXX remove duplication with tmpdir plugin basetmp = request.config.ensuretemp("testdir") name = request.function.__name__ @@ -74,13 +70,22 @@ if hasattr(self, '_olddir'): self._olddir.chdir() - def geteventrecorder(self, registry): - sorter = EventRecorder(registry) - sorter.callrecorder = CallRecorder(registry) - sorter.callrecorder.start_recording(api.PluginHooks) - sorter.hook = sorter.callrecorder.hook - self.request.addfinalizer(sorter.callrecorder.finalize) - return sorter + def getreportrecorder(self, obj): + if isinstance(obj, py._com.Registry): + registry = obj + elif hasattr(obj, 'comregistry'): + registry = obj.comregistry + elif hasattr(obj, 'pluginmanager'): + registry = obj.pluginmanager.comregistry + elif hasattr(obj, 'config'): + registry = obj.config.pluginmanager.comregistry + else: + raise ValueError("obj %r provides no comregistry" %(obj,)) + assert isinstance(registry, py._com.Registry) + reprec = ReportRecorder(registry) + reprec.hookrecorder = self._pytest.gethookrecorder(hookspec, registry) + reprec.hook = reprec.hookrecorder.hook + return reprec def chdir(self): old = self.tmpdir.chdir() @@ -131,32 +136,42 @@ #config = self.parseconfig(*args) config = self.parseconfig(*args) session = config.initsession() - rec = self.geteventrecorder(config.pluginmanager) + rec = self.getreportrecorder(config) colitems = [config.getfsnode(arg) for arg in config.args] items = list(session.genitems(colitems)) return items, rec - def runitem(self, source, **runnerargs): + 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 = self.request.function.im_self runner = testclassinstance.getrunner() - return runner(item, **runnerargs) + 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): config = self.parseconfig(*args) config.pluginmanager.do_configure(config) session = config.initsession() - sorter = self.geteventrecorder(config.pluginmanager) + reprec = self.getreportrecorder(config) session.main() config.pluginmanager.do_unconfigure(config) - return sorter + return reprec def config_preparse(self): config = self.Config() @@ -164,7 +179,11 @@ if isinstance(plugin, str): config.pluginmanager.import_plugin(plugin) else: - config.pluginmanager.register(plugin) + if isinstance(plugin, dict): + plugin = PseudoPlugin(plugin) + if not config.pluginmanager.isregistered(plugin): + config.pluginmanager.register(plugin) + #print "config.pluginmanager.impname2plugin", config.pluginmanager.impname2plugin return config def parseconfig(self, *args): @@ -207,6 +226,12 @@ self.makepyfile(__init__ = "#") self.config = self.parseconfig(path, *configargs) self.session = self.config.initsession() + #self.config.pluginmanager.do_configure(config=self.config) + # XXX + self.config.pluginmanager.import_plugin("runner") + plugin = self.config.pluginmanager.getplugin("runner") + plugin.pytest_configure(config=self.config) + return self.config.getfsnode(path) def prepare(self): @@ -244,8 +269,12 @@ p1 = py.path.local("stdout") p2 = py.path.local("stderr") print "running", cmdargs, "curdir=", py.path.local() - popen = self.popen(cmdargs, stdout=p1.open("w"), stderr=p2.open("w")) + f1 = p1.open("w") + f2 = p2.open("w") + popen = self.popen(cmdargs, stdout=f1, stderr=f2, close_fds=True) ret = popen.wait() + f1.close() + f2.close() out, err = p1.readlines(cr=0), p2.readlines(cr=0) if err: for line in err: @@ -280,58 +309,36 @@ child.timeout = expect_timeout return child -class Event: - def __init__(self, name, args, kwargs): - self.name = name - self.args = args - self.kwargs = kwargs - def __repr__(self): - return "" %(self.name, self.args) - -class ParsedCall: - def __init__(self, locals): - self.__dict__ = locals.copy() - - def __repr__(self): - return "" %(self.__dict__,) +class PseudoPlugin: + def __init__(self, vars): + self.__dict__.update(vars) -class EventRecorder(object): - def __init__(self, comregistry, debug=False): # True): +class ReportRecorder(object): + def __init__(self, comregistry): self.comregistry = comregistry - self.debug = debug comregistry.register(self) def getcall(self, name): - l = self.getcalls(name) - assert len(l) == 1, (name, l) - return l[0] + return self.hookrecorder.getcall(name) + + def popcall(self, name): + return self.hookrecorder.popcall(name) - def getcalls(self, *names): + def getcalls(self, names): """ return list of ParsedCall instances matching the given eventname. """ - if len(names) == 1 and isinstance(names, str): - names = names.split() - l = [] - for name in names: - if self.callrecorder.recordsmethod("pytest_" + name): - name = "pytest_" + name - if self.callrecorder.recordsmethod(name): - l.extend(self.callrecorder.getcalls(name)) - return l + return self.hookrecorder.getcalls(names) # functionality for test reports - def getreports(self, names="itemtestreport collectreport"): - names = [("pytest_" + x) for x in names.split()] - l = [] - for call in self.getcalls(*names): - l.append(call.rep) - return l + def getreports(self, names="pytest_runtest_logreport pytest_collectreport"): + return [x.rep for x in self.getcalls(names)] - def matchreport(self, inamepart="", names="itemtestreport collectreport"): + 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.colitem.listnames(): + colitem = rep.getnode() + if not inamepart or inamepart in colitem.listnames(): l.append(rep) if not l: raise ValueError("could not find test report matching %r: no test reports at all!" % @@ -341,23 +348,20 @@ inamepart, l)) return l[0] - def getfailures(self, names='itemtestreport collectreport'): - l = [] - for call in self.getcalls(*names.split()): - if call.rep.failed: - l.append(call.rep) - return l + 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('collectreport') + return self.getfailures('pytest_collectreport') def listoutcomes(self): passed = [] skipped = [] failed = [] - for rep in self.getreports("itemtestreport"): + for rep in self.getreports("pytest_runtest_logreport"): if rep.passed: - passed.append(rep) + if rep.when == "call": + passed.append(rep) elif rep.skipped: skipped.append(rep) elif rep.failed: @@ -374,34 +378,46 @@ assert failed == len(realfailed) def clear(self): - self.callrecorder.calls[:] = [] + self.hookrecorder.calls[:] = [] def unregister(self): self.comregistry.unregister(self) - self.callrecorder.finalize() + self.hookrecorder.finish_recording() -def test_eventrecorder(testdir): +def test_reportrecorder(testdir): registry = py._com.Registry() - recorder = testdir.geteventrecorder(registry) + recorder = testdir.getreportrecorder(registry) assert not recorder.getfailures() - rep = runner.ItemTestReport(None, None) - rep.passed = False - rep.failed = True - recorder.hook.pytest_itemtestreport(rep=rep) + item = testdir.getitem("def test_func(): pass") + class rep: + excinfo = None + passed = False + failed = True + skipped = False + when = "call" + + recorder.hook.pytest_runtest_logreport(rep=rep) failures = recorder.getfailures() assert failures == [rep] failures = recorder.getfailures() assert failures == [rep] - rep = runner.ItemTestReport(None, None) + class rep: + excinfo = None + passed = False + failed = False + skipped = True + when = "call" rep.passed = False rep.skipped = True - recorder.hook.pytest_itemtestreport(rep=rep) + recorder.hook.pytest_runtest_logreport(rep=rep) - rep = runner.CollectReport(None, None) + modcol = testdir.getmodulecol("") + rep = modcol.config.hook.pytest_make_collect_report(collector=modcol) rep.passed = False rep.failed = True - recorder.hook.pytest_itemtestreport(rep=rep) + rep.skipped = False + recorder.hook.pytest_collectreport(rep=rep) passed, skipped, failed = recorder.listoutcomes() assert not passed and skipped and failed @@ -409,13 +425,13 @@ numpassed, numskipped, numfailed = recorder.countoutcomes() assert numpassed == 0 assert numskipped == 1 - assert numfailed == 2 + assert numfailed == 1 + assert len(recorder.getfailedcollections()) == 1 recorder.unregister() recorder.clear() - assert not recorder.getfailures() - recorder.hook.pytest_itemtestreport(rep=rep) - assert not recorder.getfailures() + recorder.hook.pytest_runtest_logreport(rep=rep) + py.test.raises(ValueError, "recorder.getfailures()") class LineComp: def __init__(self): @@ -473,11 +489,19 @@ extralines.extend(lines1) return extralines - - - 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() + assert result.stdout.fnmatch_lines([ + "*1 passed*" + ]) Modified: py/trunk/py/test/plugin/pytest_recwarn.py ============================================================================== --- py/trunk/py/test/plugin/pytest_recwarn.py (original) +++ py/trunk/py/test/plugin/pytest_recwarn.py Tue Jun 16 18:23:18 2009 @@ -1,18 +1,14 @@ """ - -provides "recwarn" funcarg for asserting warnings to be shown -to a user. See the test at the bottom for an example. - +"recwarn" funcarg for asserting that warnings are shown to a user. """ import py import os -class RecwarnPlugin: - def pytest_funcarg__recwarn(self, request): - """ check that warnings have been raised. """ - warnings = WarningsRecorder() - request.addfinalizer(warnings.finalize) - return warnings +def pytest_funcarg__recwarn(request): + """ check that warnings have been raised. """ + warnings = WarningsRecorder() + request.addfinalizer(warnings.finalize) + return warnings class RecordedWarning: def __init__(self, message, category, filename, lineno, line): @@ -77,7 +73,7 @@ assert showwarning == py.std.warnings.showwarning def test_recwarn_functional(testdir): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" pytest_plugins = 'pytest_recwarn', import warnings oldwarn = warnings.showwarning @@ -89,6 +85,6 @@ def test_finalized(): assert warnings.showwarning == oldwarn """) - res = sorter.countoutcomes() + res = reprec.countoutcomes() assert tuple(res) == (2, 0, 0), res Modified: py/trunk/py/test/plugin/pytest_restdoc.py ============================================================================== --- py/trunk/py/test/plugin/pytest_restdoc.py (original) +++ py/trunk/py/test/plugin/pytest_restdoc.py Tue Jun 16 18:23:18 2009 @@ -1,23 +1,26 @@ +""" +perform ReST specific tests on .txt files, including +linkchecks and remote URL checks. +""" import py -class RestdocPlugin: - def pytest_addoption(self, parser): - group = parser.addgroup("ReST", "ReST documentation check options") - group.addoption('-R', '--urlcheck', - action="store_true", dest="urlcheck", default=False, - help="urlopen() remote links found in ReST text files.") - group.addoption('--urltimeout', action="store", metavar="secs", - type="int", dest="urlcheck_timeout", default=5, - help="timeout in seconds for remote urlchecks") - group.addoption('--forcegen', - action="store_true", dest="forcegen", default=False, - help="force generation of html files.") - - def pytest_collect_file(self, path, parent): - if path.ext == ".txt": - project = getproject(path) - if project is not None: - return ReSTFile(path, parent=parent, project=project) +def pytest_addoption(parser): + group = parser.addgroup("ReST", "ReST documentation check options") + group.addoption('-R', '--urlcheck', + action="store_true", dest="urlcheck", default=False, + help="urlopen() remote links found in ReST text files.") + group.addoption('--urltimeout', action="store", metavar="secs", + type="int", dest="urlcheck_timeout", default=5, + help="timeout in seconds for remote urlchecks") + group.addoption('--forcegen', + action="store_true", dest="forcegen", default=False, + help="force generation of html files.") + +def pytest_collect_file(path, parent): + if path.ext == ".txt": + project = getproject(path) + if project is not None: + return ReSTFile(path, parent=parent, project=project) def getproject(path): for parent in path.parts(reverse=True): @@ -64,7 +67,7 @@ super(ReSTSyntaxTest, self).__init__(*args, **kwargs) self.project = project - def metainfo(self): + def reportinfo(self): return self.fspath, None, "syntax check" def runtest(self): @@ -196,7 +199,7 @@ #return [] # no need to rebuild class DoctestText(py.test.collect.Item): - def metainfo(self): + def reportinfo(self): return self.fspath, None, "doctest" def runtest(self): @@ -268,7 +271,7 @@ if tryfn.startswith('http:') or tryfn.startswith('https'): if self.config.getvalue("urlcheck"): yield CheckLink(name, parent=self, - args=(tryfn, path, lineno, timeout), callobj=urlcheck) + args=(tryfn, path, lineno, timeout), checkfunc=urlcheck) elif tryfn.startswith('webcal:'): continue else: @@ -279,16 +282,19 @@ checkfn = tryfn if checkfn.strip() and (1 or checkfn.endswith('.html')): yield CheckLink(name, parent=self, - args=(tryfn, path, lineno), callobj=localrefcheck) + args=(tryfn, path, lineno), checkfunc=localrefcheck) -class CheckLink(py.test.collect.Function): - def metainfo(self, basedir=None): - return (self.fspath, self._args[2], "checklink: %s" % self._args[0]) - - def setup(self): - pass - def teardown(self): - pass +class CheckLink(py.test.collect.Item): + def __init__(self, name, parent, args, checkfunc): + super(CheckLink, self).__init__(name, parent) + self.args = args + self.checkfunc = checkfunc + + def runtest(self): + return self.checkfunc(*self.args) + + def reportinfo(self, basedir=None): + return (self.fspath, self.args[2], "checklink: %s" % self.args[0]) def urlcheck(tryfn, path, lineno, TIMEOUT_URLOPEN): old = py.std.socket.getdefaulttimeout() @@ -345,8 +351,6 @@ # # PLUGIN tests # -def test_generic(plugintester): - plugintester.hookcheck(RestdocPlugin) def test_deindent(): assert deindent('foo') == 'foo' @@ -388,26 +392,26 @@ "resolve_linkrole('source', 'py/foo/bar.py')") -def pytest_funcarg__testdir(request): - testdir = request.call_next_provider() - testdir.makepyfile(confrest="from py.__.misc.rest import Project") - testdir.plugins.append(RestdocPlugin()) - count = 0 - for p in testdir.plugins: - if isinstance(p, RestdocPlugin): - count += 1 - assert count < 2 - return testdir - class TestDoctest: + def pytest_funcarg__testdir(self, request): + testdir = request.getfuncargvalue("testdir") + assert request.module.__name__ == __name__ + testdir.makepyfile(confrest="from py.__.misc.rest import Project") + for p in testdir.plugins: + if p == globals(): + break + else: + testdir.plugins.append(globals()) + return testdir + def test_doctest_extra_exec(self, testdir): xtxt = testdir.maketxtfile(x=""" hello:: .. >>> raise ValueError >>> None """) - sorter = testdir.inline_run(xtxt) - passed, skipped, failed = sorter.countoutcomes() + reprec = testdir.inline_run(xtxt) + passed, skipped, failed = reprec.countoutcomes() assert failed == 1 def test_doctest_basic(self, testdir): @@ -429,23 +433,23 @@ end """) - sorter = testdir.inline_run(xtxt) - passed, skipped, failed = sorter.countoutcomes() + reprec = testdir.inline_run(xtxt) + passed, skipped, failed = reprec.countoutcomes() assert failed == 0 assert passed + skipped == 2 def test_doctest_eol(self, testdir): ytxt = testdir.maketxtfile(y=".. >>> 1 + 1\r\n 2\r\n\r\n") - sorter = testdir.inline_run(ytxt) - passed, skipped, failed = sorter.countoutcomes() + reprec = testdir.inline_run(ytxt) + passed, skipped, failed = reprec.countoutcomes() assert failed == 0 assert passed + skipped == 2 def test_doctest_indentation(self, testdir): footxt = testdir.maketxtfile(foo= '..\n >>> print "foo\\n bar"\n foo\n bar\n') - sorter = testdir.inline_run(footxt) - passed, skipped, failed = sorter.countoutcomes() + reprec = testdir.inline_run(footxt) + passed, skipped, failed = reprec.countoutcomes() assert failed == 0 assert skipped + passed == 2 @@ -455,8 +459,8 @@ .. _`blah`: javascript:some_function() """) - sorter = testdir.inline_run(xtxt) - passed, skipped, failed = sorter.countoutcomes() + reprec = testdir.inline_run(xtxt) + passed, skipped, failed = reprec.countoutcomes() assert failed == 0 assert skipped + passed == 3 @@ -476,9 +480,9 @@ False """) - sorter = testdir.inline_run(xtxt) + reprec = testdir.inline_run(xtxt) assert len(l) == 1 - passed, skipped, failed = sorter.countoutcomes() + passed, skipped, failed = reprec.countoutcomes() assert passed >= 1 assert not failed assert skipped <= 1 Modified: py/trunk/py/test/plugin/pytest_resultdb.py ============================================================================== --- py/trunk/py/test/plugin/pytest_resultdb.py (original) +++ py/trunk/py/test/plugin/pytest_resultdb.py Tue Jun 16 18:23:18 2009 @@ -1,54 +1,51 @@ -import py -from pytest_resultlog import ResultLog +"""XXX in progress: resultdb plugin for database logging of test results. -class ResultdbPlugin: - """XXX in progress: resultdb plugin for database logging of test results. +Saves test results to a datastore. - Saves test results to a datastore. +XXX this needs to be merged with resultlog plugin - XXX this needs to be merged with resultlog plugin +Also mixes in some early ideas about an archive abstraction for test +results. +""" +import py - Also mixes in some early ideas about an archive abstraction for test - results. - """ - def pytest_addoption(self, parser): - group = parser.addgroup("resultdb", "resultdb plugin options") - group.addoption('--resultdb', action="store", dest="resultdb", - metavar="path", - help="path to the file to store test results.") - group.addoption('--resultdb_format', action="store", - dest="resultdbformat", default='json', - help="data format (json, sqlite)") - - def pytest_configure(self, config): - if config.getvalue('resultdb'): - if config.option.resultdb: - # local import so missing module won't crash py.test - try: - import sqlite3 - except ImportError: - raise config.Error('Could not import sqlite3 module') - try: - import simplejson - except ImportError: - raise config.Error('Could not import simplejson module') - if config.option.resultdbformat.lower() == 'json': - self.resultdb = ResultDB(JSONResultArchive, - config.option.resultdb) - elif config.option.resultdbformat.lower() == 'sqlite': - self.resultdb = ResultDB(SQLiteResultArchive, - config.option.resultdb) - else: - raise config.Error('Unknown --resultdb_format: %s' % - config.option.resultdbformat) - - config.pluginmanager.register(self.resultdb) - - def pytest_unconfigure(self, config): - if hasattr(self, 'resultdb'): - del self.resultdb - #config.pluginmanager.unregister(self.resultdb) +py.test.skip("XXX needs to be merged with resultlog") + +from pytest_resultlog import ResultLog + +def pytest_addoption(parser): + group = parser.addgroup("resultdb", "resultdb plugin options") + group.addoption('--resultdb', action="store", dest="resultdb", + metavar="path", + help="path to the file to store test results.") + group.addoption('--resultdb_format', action="store", + dest="resultdbformat", default='json', + help="data format (json, sqlite)") + +def pytest_configure(config): + # XXX using config.XYZ is not good + if config.getvalue('resultdb'): + if config.option.resultdb: + # local import so missing module won't crash py.test + try: + import sqlite3 + except ImportError: + raise config.Error('Could not import sqlite3 module') + try: + import simplejson + except ImportError: + raise config.Error('Could not import simplejson module') + if config.option.resultdbformat.lower() == 'json': + resultdb = ResultDB(JSONResultArchive, + config.option.resultdb) + elif config.option.resultdbformat.lower() == 'sqlite': + resultdb = ResultDB(SQLiteResultArchive, + config.option.resultdb) + else: + raise config.Error('Unknown --resultdb_format: %s' % + config.option.resultdbformat) + config.pluginmanager.register(resultdb) class JSONResultArchive(object): def __init__(self, archive_path): @@ -303,9 +300,8 @@ archive.init_db() return archive - def test_collection_report(self, plugintester): + def test_collection_report(self, testdir): py.test.skip("Needs a rewrite for db version.") - testdir = plugintester.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") @@ -328,8 +324,7 @@ assert x.startswith(" ") assert "XXX" in "".join(lines[1:]) - def test_log_test_outcomes(self, plugintester): - testdir = plugintester.testdir() + def test_log_test_outcomes(self, testdir): mod = testdir.makepyfile(test_mod=""" import py def test_pass(): pass @@ -364,9 +359,7 @@ assert entry_lines[-1][0] == ' ' assert 'ValueError' in entry -def test_generic(plugintester): - plugintester.hookcheck(ResultdbPlugin) - testdir = plugintester.testdir() +def test_generic(testdir): testdir.makepyfile(""" import py def test_pass(): Modified: py/trunk/py/test/plugin/pytest_resultlog.py ============================================================================== --- py/trunk/py/test/plugin/pytest_resultlog.py (original) +++ py/trunk/py/test/plugin/pytest_resultlog.py Tue Jun 16 18:23:18 2009 @@ -1,26 +1,27 @@ +"""resultlog plugin for machine-readable logging of test results. + Useful for buildbot integration code. +""" + import py -class ResultlogPlugin: - """resultlog plugin for machine-readable logging of test results. - Useful for buildbot integration code. - """ - def pytest_addoption(self, parser): - group = parser.addgroup("resultlog", "resultlog plugin options") - group.addoption('--resultlog', action="store", dest="resultlog", metavar="path", - help="path for machine-readable result log.") - - def pytest_configure(self, config): - resultlog = config.option.resultlog - if resultlog: - logfile = open(resultlog, 'w', 1) # line buffered - self.resultlog = ResultLog(logfile) - config.pluginmanager.register(self.resultlog) - - def pytest_unconfigure(self, config): - if hasattr(self, 'resultlog'): - self.resultlog.logfile.close() - del self.resultlog - #config.pluginmanager.unregister(self.resultlog) +def pytest_addoption(parser): + group = parser.addgroup("resultlog", "resultlog plugin options") + group.addoption('--resultlog', action="store", dest="resultlog", metavar="path", + help="path for machine-readable result log.") + +def pytest_configure(config): + resultlog = config.option.resultlog + if resultlog: + logfile = open(resultlog, 'w', 1) # line buffered + config._resultlog = ResultLog(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() @@ -54,11 +55,11 @@ for line in longrepr.splitlines(): print >>self.logfile, " %s" % line - def log_outcome(self, event, shortrepr, longrepr): - testpath = generic_path(event.colitem) + def log_outcome(self, node, shortrepr, longrepr): + testpath = generic_path(node) self.write_log_entry(testpath, shortrepr, longrepr) - def pytest_itemtestreport(self, rep): + def pytest_runtest_logreport(self, rep): code = rep.shortrepr if rep.passed: longrepr = "" @@ -66,7 +67,7 @@ longrepr = str(rep.longrepr) elif rep.skipped: longrepr = str(rep.longrepr.reprcrash.message) - self.log_outcome(rep, code, longrepr) + self.log_outcome(rep.item, code, longrepr) def pytest_collectreport(self, rep): if not rep.passed: @@ -76,7 +77,7 @@ assert rep.skipped code = "S" longrepr = str(rep.longrepr.reprcrash) - self.log_outcome(rep, code, longrepr) + self.log_outcome(rep.collector, code, longrepr) def pytest_internalerror(self, excrepr): path = excrepr.reprcrash.path @@ -93,7 +94,7 @@ def test_generic_path(): from py.__.test.collect import Node, Item, FSCollector - p1 = Node('a', config='dummy') + p1 = Node('a') assert p1.fspath is None p2 = Node('B', parent=p1) p3 = Node('()', parent = p2) @@ -102,7 +103,7 @@ res = generic_path(item) assert res == 'a.B().c' - p0 = FSCollector('proj/test', config='dummy') + p0 = FSCollector('proj/test') p1 = FSCollector('proj/test/a', parent=p0) p2 = Node('B', parent=p1) p3 = Node('()', parent = p2) @@ -162,8 +163,7 @@ testdir.runpytest(*args) return filter(None, resultlog.readlines(cr=0)) - def test_collection_report(self, plugintester): - testdir = plugintester.testdir() + 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") @@ -185,8 +185,7 @@ assert x.startswith(" ") assert "XXX" in "".join(lines[1:]) - def test_log_test_outcomes(self, plugintester): - testdir = plugintester.testdir() + def test_log_test_outcomes(self, testdir): mod = testdir.makepyfile(test_mod=""" import py def test_pass(): pass @@ -223,9 +222,7 @@ assert entry_lines[-1][0] == ' ' assert 'ValueError' in entry -def test_generic(plugintester, LineMatcher): - plugintester.hookcheck(ResultlogPlugin) - testdir = plugintester.testdir() +def test_generic(testdir, LineMatcher): testdir.plugins.append("resultlog") testdir.makepyfile(""" import py Modified: py/trunk/py/test/plugin/pytest_runner.py ============================================================================== --- py/trunk/py/test/plugin/pytest_runner.py (original) +++ py/trunk/py/test/plugin/pytest_runner.py Tue Jun 16 18:23:18 2009 @@ -1,31 +1,132 @@ +""" + collect and run test items. + + * executing test items + * running collectors + * and generating report events about it +""" + import py + from py.__.test.outcome import Skipped -class RunnerPlugin: - def pytest_configure(self, config): - config._setupstate = SetupState() - - def pytest_unconfigure(self, config): - config._setupstate.teardown_all() - - def pytest_item_setup_and_runtest(self, item): - setupstate = item.config._setupstate - call = item.config.guardedcall(lambda: setupstate.prepare(item)) - if call.excinfo: - rep = ItemSetupReport(item, call.excinfo, call.outerr) - item.config.hook.pytest_itemsetupreport(rep=rep) - else: - call = item.config.guardedcall(lambda: item.runtest()) - item.config.hook.pytest_item_runtest_finished( - item=item, excinfo=call.excinfo, outerr=call.outerr) - call = item.config.guardedcall(lambda: self.teardown_exact(item)) - if call.excinfo: - rep = ItemSetupReport(item, call.excinfo, call.outerr) - item.config.hook.pytest_itemsetupreport(rep=rep) - - def pytest_collector_collect(self, collector): - call = item.config.guardedcall(lambda x: collector._memocollect()) - return CollectReport(collector, res, excinfo, outerr) +# +# pytest plugin hooks + +def pytest_addoption(parser): + group = parser.getgroup("general") + group.addoption('--boxed', + action="store_true", dest="boxed", default=False, + help="box each test run in a separate process") + +def pytest_configure(config): + config._setupstate = SetupState() + +def pytest_unconfigure(config): + config._setupstate.teardown_all() + +def pytest_make_collect_report(collector): + call = collector.config.guardedcall( + lambda: collector._memocollect() + ) + result = None + if not call.excinfo: + result = call.result + return CollectReport(collector, result, call.excinfo, call.outerr) + + return report + +def pytest_runtest_protocol(item): + if item.config.getvalue("boxed"): + reports = forked_run_report(item) + for rep in reports: + item.config.hook.pytest_runtest_logreport(rep=rep) + else: + 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): + if not item._deprecated_testexecution(): + item.runtest() + +def pytest_runtest_makereport(item, call): + return ItemTestReport(item, call.excinfo, call.when, call.outerr) + +def pytest_runtest_teardown(item): + item.config._setupstate.teardown_exact(item) + +# +# Implementation + +def call_and_report(item, when, log=True): + call = RuntestHookCall(item, when) + hook = item.config.hook + report = hook.pytest_runtest_makereport(item=item, call=call) + if log and (when == "call" or not report.passed): + hook.pytest_runtest_logreport(rep=report) + return report + + +class RuntestHookCall: + excinfo = None + _prefix = "pytest_runtest_" + def __init__(self, item, when): + self.when = when + hookname = self._prefix + when + hook = getattr(item.config.hook, hookname) + capture = item.config._getcapture() + try: + try: + self.result = hook(item=item) + except KeyboardInterrupt: + raise + except: + self.excinfo = py.code.ExceptionInfo() + finally: + self.outerr = capture.reset() + +def forked_run_report(item): + # for now, we run setup/teardown in the subprocess + # XXX optionally allow sharing of setup/teardown + EXITSTATUS_TESTEXIT = 4 + from py.__.test.dist.mypickle import ImmutablePickler + ipickle = ImmutablePickler(uneven=0) + ipickle.selfmemoize(item.config) + # XXX workaround the issue that 2.6 cannot pickle + # instances of classes defined in global conftest.py files + ipickle.selfmemoize(item) + def runforked(): + try: + reports = runtestprotocol(item, log=False) + except KeyboardInterrupt: + py.std.os._exit(EXITSTATUS_TESTEXIT) + return ipickle.dumps(reports) + + ff = py.process.ForkedFunc(runforked) + result = ff.waitfinish() + if result.retval is not None: + return ipickle.loads(result.retval) + else: + if result.exitstatus == EXITSTATUS_TESTEXIT: + py.test.exit("forked test item %s raised Exit" %(item,)) + return [report_process_crash(item, result)] + +def report_process_crash(item, result): + path, lineno = item._getfslineno() + info = "%s:%s: running the test CRASHED with signal %d" %( + path, lineno, result.signal) + return ItemTestReport(item, excinfo=info, when="???") class BaseReport(object): def __repr__(self): @@ -43,10 +144,12 @@ class ItemTestReport(BaseReport): failed = passed = skipped = False - def __init__(self, colitem, excinfo=None, when=None, outerr=None): - self.colitem = colitem - if colitem and when != "setup": - self.keywords = colitem.readkeywords() + def __init__(self, item, excinfo=None, when=None, outerr=None): + self.item = item + self.when = when + self.outerr = outerr + if item and when != "setup": + self.keywords = item.readkeywords() else: # if we fail during setup it might mean # we are not able to access the underlying object @@ -58,7 +161,6 @@ self.passed = True self.shortrepr = "." else: - self.when = when if not isinstance(excinfo, py.code.ExceptionInfo): self.failed = True shortrepr = "?" @@ -66,65 +168,79 @@ elif excinfo.errisinstance(Skipped): self.skipped = True shortrepr = "s" - longrepr = self.colitem._repr_failure_py(excinfo, outerr) + longrepr = self.item._repr_failure_py(excinfo, outerr) else: self.failed = True - shortrepr = self.colitem.shortfailurerepr - if self.when == "execute": - longrepr = self.colitem.repr_failure(excinfo, outerr) + shortrepr = self.item.shortfailurerepr + if self.when == "call": + longrepr = self.item.repr_failure(excinfo, outerr) else: # exception in setup or teardown - longrepr = self.colitem._repr_failure_py(excinfo, outerr) + longrepr = self.item._repr_failure_py(excinfo, outerr) shortrepr = shortrepr.lower() self.shortrepr = shortrepr self.longrepr = longrepr - + + def getnode(self): + return self.item class CollectReport(BaseReport): skipped = failed = passed = False - def __init__(self, colitem, result, excinfo=None, outerr=None): - # XXX rename to collector - self.colitem = colitem + def __init__(self, collector, result, excinfo=None, outerr=None): + self.collector = collector if not excinfo: self.passed = True self.result = result else: self.outerr = outerr - self.longrepr = self.colitem._repr_failure_py(excinfo, outerr) + self.longrepr = self.collector._repr_failure_py(excinfo, outerr) if excinfo.errisinstance(Skipped): self.skipped = True self.reason = str(excinfo.value) else: self.failed = True -class ItemSetupReport(BaseReport): - failed = passed = skipped = False - def __init__(self, item, excinfo=None, outerr=None): - self.item = item - self.outerr = outerr - if not excinfo: - self.passed = True - else: - if excinfo.errisinstance(Skipped): - self.skipped = True - else: - self.failed = True - self.excrepr = item._repr_failure_py(excinfo, []) + def getnode(self): + return self.collector 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 callable(finalizer) + #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 _teardown_with_finalization(self, colitem): + finalizers = self._finalizers.pop(colitem, None) + while finalizers: + fin = finalizers.pop() + fin() + 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: - col = self.stack.pop() - col.teardown() + self._pop_and_teardown() + self._teardown_with_finalization(None) + assert not self._finalizers def teardown_exact(self, item): - if self.stack and self.stack[-1] == item: - col = self.stack.pop() - col.teardown() + assert self.stack and self.stack[-1] == item + self._pop_and_teardown() def prepare(self, colitem): """ setup objects along the collector chain to the test-method @@ -133,140 +249,7 @@ while self.stack: if self.stack == needed_collectors[:len(self.stack)]: break - col = self.stack.pop() - col.teardown() + self._pop_and_teardown() for col in needed_collectors[len(self.stack):]: col.setup() self.stack.append(col) - -# =============================================================================== -# -# plugin tests -# -# =============================================================================== - -def test_generic(plugintester): - plugintester.hookcheck(RunnerPlugin()) - -class TestSetupState: - def test_setup_prepare(self, testdir): - modcol = testdir.getmodulecol(""" - def setup_function(function): - function.myfunc = function - def test_func1(): - pass - def test_func2(): - pass - def teardown_function(function): - del function.myfunc - """) - item1, item2 = modcol.collect() - setup = SetupState() - setup.prepare(item1) - assert item1.obj.myfunc == item1.obj - assert not hasattr(item2, 'myfunc') - - setup.prepare(item2) - assert item2.obj.myfunc == item2.obj - assert not hasattr(item1, 'myfunc') - - def test_setup_teardown_exact(self, testdir): - item = testdir.getitem(""" - def test_func(): - pass - def teardown_function(function): - function.tear = "down" - """) - setup = SetupState() - setup.prepare(item) - setup.teardown_exact(item) - assert item.obj.tear == "down" - - def test_setup_teardown_exact(self, testdir): - item = testdir.getitem(""" - def setup_module(mod): - mod.x = 1 - def setup_function(function): - function.y = 2 - def test_func(): - pass - def teardown_function(function): - del function.y - def teardown_module(mod): - del mod.x - """) - setup = SetupState() - setup.prepare(item) - assert item.obj.y == 2 - assert item.parent.obj.x == 1 - setup.teardown_all() - assert not hasattr(item.obj, 'y') - assert not hasattr(item.parent.obj, 'x') - -class TestRunnerPlugin: - def test_pytest_item_setup_and_runtest(self, testdir): - item = testdir.getitem("""def test_func(): pass""") - plugin = RunnerPlugin() - plugin.pytest_configure(item.config) - sorter = testdir.geteventrecorder(item.config.pluginmanager) - plugin.pytest_item_setup_and_runtest(item) - rep = sorter.getcall("pytest_itemtestreport").rep - assert rep.passed - -class TestSetupEvents: - disabled = True - - def test_setup_runtest_ok(self, testdir): - sorter = testdir.inline_runsource(""" - def setup_module(mod): - pass - def test_func(): - pass - """) - assert not sorter.getcalls(pytest_itemsetupreport) - - def test_setup_fails(self, testdir): - sorter = testdir.inline_runsource(""" - def setup_module(mod): - print "world" - raise ValueError(42) - def test_func(): - pass - """) - rep = sorter.popcall(pytest_itemsetupreport).rep - assert rep.failed - assert not rep.skipped - assert rep.excrepr - assert "42" in str(rep.excrepr) - assert rep.outerr[0].find("world") != -1 - - def test_teardown_fails(self, testdir): - sorter = testdir.inline_runsource(""" - def test_func(): - pass - def teardown_function(func): - print "13" - raise ValueError(25) - """) - rep = evrec.popcall(pytest_itemsetupreport).rep - assert rep.failed - assert rep.item == item - assert not rep.passed - assert "13" in rep.outerr[0] - assert "25" in str(rep.excrepr) - - def test_setup_skips(self, testdir): - sorter = testdir.inline_runsource(""" - import py - def setup_module(mod): - py.test.skip("17") - def test_func(): - pass - """) - rep = sorter.popcall(pytest_itemsetupreport) - assert not rep.failed - assert rep.skipped - assert rep.excrepr - assert "17" in str(rep.excrepr) - - Modified: py/trunk/py/test/plugin/pytest_terminal.py ============================================================================== --- py/trunk/py/test/plugin/pytest_terminal.py (original) +++ py/trunk/py/test/plugin/pytest_terminal.py Tue Jun 16 18:23:18 2009 @@ -1,21 +1,19 @@ import py import sys -class TerminalPlugin(object): - """ Report a test run to a terminal. """ - def pytest_configure(self, config): - if config.option.collectonly: - self.reporter = CollectonlyReporter(config) - else: - self.reporter = TerminalReporter(config) - # XXX see remote.py's XXX - for attr in 'pytest_terminal_hasmarkup', 'pytest_terminal_fullwidth': - if hasattr(config, attr): - #print "SETTING TERMINAL OPTIONS", attr, getattr(config, attr) - name = attr.split("_")[-1] - assert hasattr(self.reporter._tw, name), name - setattr(self.reporter._tw, name, getattr(config, attr)) - config.pluginmanager.register(self.reporter) +def pytest_configure(config): + if config.option.collectonly: + reporter = CollectonlyReporter(config) + else: + reporter = TerminalReporter(config) + # XXX see remote.py's XXX + for attr in 'pytest_terminal_hasmarkup', 'pytest_terminal_fullwidth': + if hasattr(config, attr): + #print "SETTING TERMINAL OPTIONS", attr, getattr(config, attr) + name = attr.split("_")[-1] + assert hasattr(self.reporter._tw, name), name + setattr(reporter._tw, name, getattr(config, attr)) + config.pluginmanager.register(reporter) class TerminalReporter: def __init__(self, config, file=None): @@ -142,30 +140,31 @@ def pytest_deselected(self, items): self.stats.setdefault('deselected', []).append(items) - def pytest_itemstart(self, item, node=None): if self.config.option.dist != "no": # for dist-testing situations itemstart means we # queued the item for sending, not interesting (unless debugging) if self.config.option.debug: - line = self._metainfoline(item) + line = self._reportinfoline(item) extra = "" if node: extra = "-> " + str(node.gateway.id) self.write_ensure_prefix(line, extra) else: if self.config.option.verbose: - line = self._metainfoline(item) + line = self._reportinfoline(item) self.write_ensure_prefix(line, "") else: # ensure that the path is printed before the # 1st test of a module starts running - fspath, lineno, msg = item.metainfo() + fspath, lineno, msg = self._getreportinfo(item) self.write_fspath_result(fspath, "") - def pytest_itemtestreport(self, rep): - fspath = rep.colitem.fspath + def pytest_runtest_logreport(self, rep): + if rep.passed and rep.when in ("setup", "teardown"): + return + fspath = rep.item.fspath cat, letter, word = self.getcategoryletterword(rep) if isinstance(word, tuple): word, markup = word @@ -173,10 +172,10 @@ markup = {} self.stats.setdefault(cat, []).append(rep) if not self.config.option.verbose: - fspath, lineno, msg = rep.colitem.metainfo() + fspath, lineno, msg = self._getreportinfo(rep.item) self.write_fspath_result(fspath, letter) else: - line = self._metainfoline(rep.colitem) + line = self._reportinfoline(rep.item) if not hasattr(rep, 'node'): self.write_ensure_prefix(line, word, **markup) else: @@ -192,12 +191,12 @@ if rep.failed: self.stats.setdefault("failed", []).append(rep) msg = rep.longrepr.reprcrash.message - self.write_fspath_result(rep.colitem.fspath, "F") + self.write_fspath_result(rep.collector.fspath, "F") elif rep.skipped: self.stats.setdefault("skipped", []).append(rep) - self.write_fspath_result(rep.colitem.fspath, "S") + self.write_fspath_result(rep.collector.fspath, "S") - def pytest_testrunstart(self): + def pytest_sessionstart(self, session): self.write_sep("=", "test session starts", bold=True) self._sessionstarttime = py.std.time.time() @@ -228,7 +227,7 @@ for i, testarg in py.builtin.enumerate(self.config.args): self.write_line("test object %d: %s" %(i+1, testarg)) - def pytest_testrunfinish(self, exitstatus, excrepr=None): + def pytest_sessionfinish(self, session, exitstatus, excrepr=None): self._tw.line("") if exitstatus in (0, 1, 2): self.summary_failures() @@ -254,8 +253,8 @@ for rootdir in rootdirs: self.write_line("### Watching: %s" %(rootdir,), bold=True) - def _metainfoline(self, item): - fspath, lineno, msg = item.metainfo() + def _reportinfoline(self, item): + fspath, lineno, msg = self._getreportinfo(item) if fspath: fspath = self.curdir.bestrelpath(fspath) if lineno is not None: @@ -267,23 +266,41 @@ elif fspath and lineno: line = "%(fspath)s:%(lineno)s" else: - line = "[nometainfo]" + line = "[noreportinfo]" return line % locals() + " " + + def _getfailureheadline(self, rep): + if hasattr(rep, "collector"): + return str(rep.collector.fspath) + else: + fspath, lineno, msg = self._getreportinfo(rep.item) + return msg + + def _getreportinfo(self, item): + try: + return item.__reportinfo + except AttributeError: + pass + reportinfo = item.config.hook.pytest_report_iteminfo(item=item) + # cache on item + item.__reportinfo = reportinfo + return reportinfo # - # summaries for testrunfinish + # summaries for sessionfinish # def summary_failures(self): if 'failed' in self.stats and self.config.option.tbstyle != "no": self.write_sep("=", "FAILURES") - for ev in self.stats['failed']: - self.write_sep("_", "FAILURES") - if hasattr(ev, 'node'): + for rep in self.stats['failed']: + msg = self._getfailureheadline(rep) + self.write_sep("_", msg) + if hasattr(rep, 'node'): self.write_line(self.gateway2info.get( - ev.node.gateway, "node %r (platinfo not found? strange)") + rep.node.gateway, "node %r (platinfo not found? strange)") [:self._tw.fullwidth-1]) - ev.toterminal(self._tw) + rep.toterminal(self._tw) def summary_stats(self): session_duration = py.std.time.time() - self._sessionstarttime @@ -334,6 +351,10 @@ def outindent(self, line): self.out.line(self.indent + str(line)) + def pytest_internalerror(self, excrepr): + for line in str(excrepr).split("\n"): + self.out.line("INTERNALERROR> " + line) + def pytest_collectstart(self, collector): self.outindent(collector) self.indent += self.INDENT @@ -347,7 +368,7 @@ self._failed.append(rep) self.indent = self.indent[:-len(self.INDENT)] - def pytest_testrunfinish(self, exitstatus, excrepr=None): + def pytest_sessionfinish(self, session, exitstatus, excrepr=None): if self._failed: self.out.sep("!", "collection failures") for rep in self._failed: @@ -378,10 +399,12 @@ # # =============================================================================== -from py.__.test import runner +import pytest_runner as runner # XXX -class TestTerminal: +def basic_run_report(item): + return runner.call_and_report(item, "call", log=False) +class TestTerminal: def test_pass_skip_fail(self, testdir, linecomp): modcol = testdir.getmodulecol(""" import py @@ -394,15 +417,15 @@ """) rep = TerminalReporter(modcol.config, file=linecomp.stringio) rep.config.pluginmanager.register(rep) - rep.config.hook.pytest_testrunstart() - + rep.config.hook.pytest_sessionstart(session=testdir.session) + for item in testdir.genitems([modcol]): - ev = runner.basic_run_report(item) - rep.config.hook.pytest_itemtestreport(rep=ev) + ev = basic_run_report(item) + rep.config.hook.pytest_runtest_logreport(rep=ev) linecomp.assert_contains_lines([ "*test_pass_skip_fail.py .sF" ]) - rep.config.hook.pytest_testrunfinish(exitstatus=1) + rep.config.hook.pytest_sessionfinish(session=testdir.session, exitstatus=1) linecomp.assert_contains_lines([ " def test_func():", "> assert 0", @@ -421,21 +444,21 @@ """, configargs=("-v",)) rep = TerminalReporter(modcol.config, file=linecomp.stringio) rep.config.pluginmanager.register(rep) - rep.config.hook.pytest_testrunstart() + rep.config.hook.pytest_sessionstart(session=testdir.session) items = modcol.collect() rep.config.option.debug = True # for item in items: rep.config.hook.pytest_itemstart(item=item, node=None) s = linecomp.stringio.getvalue().strip() assert s.endswith(item.name) - rep.config.hook.pytest_itemtestreport(rep=runner.basic_run_report(item)) + rep.config.hook.pytest_runtest_logreport(rep=basic_run_report(item)) linecomp.assert_contains_lines([ "*test_pass_skip_fail_verbose.py:2: *test_ok*PASS*", "*test_pass_skip_fail_verbose.py:4: *test_skip*SKIP*", "*test_pass_skip_fail_verbose.py:6: *test_func*FAIL*", ]) - rep.config.hook.pytest_testrunfinish(exitstatus=1) + rep.config.hook.pytest_sessionfinish(session=testdir.session, exitstatus=1) linecomp.assert_contains_lines([ " def test_func():", "> assert 0", @@ -446,13 +469,13 @@ modcol = testdir.getmodulecol("import xyz") rep = TerminalReporter(modcol.config, file=linecomp.stringio) rep.config.pluginmanager.register(rep) - rep.config.hook.pytest_testrunstart() + rep.config.hook.pytest_sessionstart(session=testdir.session) l = list(testdir.genitems([modcol])) assert len(l) == 0 linecomp.assert_contains_lines([ "*test_collect_fail.py F*" ]) - rep.config.hook.pytest_testrunfinish(exitstatus=1) + rep.config.hook.pytest_sessionfinish(session=testdir.session, exitstatus=1) linecomp.assert_contains_lines([ "> import xyz", "E ImportError: No module named xyz" @@ -519,7 +542,7 @@ raise ValueError() """) rep = TerminalReporter(modcol.config, file=linecomp.stringio) - reports = [runner.basic_run_report(x) for x in modcol.collect()] + reports = [basic_run_report(x) for x in modcol.collect()] rep.pytest_looponfailinfo(reports, [modcol.config.topdir]) linecomp.assert_contains_lines([ "*test_looponfailreport.py:2: assert 0", @@ -542,11 +565,11 @@ """, configargs=("--tb=%s" % tbopt,)) rep = TerminalReporter(modcol.config, file=linecomp.stringio) rep.config.pluginmanager.register(rep) - rep.config.hook.pytest_testrunstart() + rep.config.hook.pytest_sessionstart(session=testdir.session) for item in testdir.genitems([modcol]): - rep.config.hook.pytest_itemtestreport( - rep=runner.basic_run_report(item)) - rep.config.hook.pytest_testrunfinish(exitstatus=1) + rep.config.hook.pytest_runtest_logreport( + rep=basic_run_report(item)) + rep.config.hook.pytest_sessionfinish(session=testdir.session, exitstatus=1) s = linecomp.stringio.getvalue() if tbopt == "long": print s @@ -571,11 +594,11 @@ "*test_show_path_before_running_test.py*" ]) - def test_itemreport_metainfo(self, testdir, linecomp): + def test_itemreport_reportinfo(self, testdir, linecomp): testdir.makeconftest(""" import py class Function(py.test.collect.Function): - def metainfo(self): + def reportinfo(self): return "ABCDE", 42, "custom" """) item = testdir.getitem("def test_func(): pass") @@ -591,6 +614,25 @@ "*ABCDE:43: custom*" ]) + def test_itemreport_pytest_report_iteminfo(self, testdir, linecomp): + item = testdir.getitem("def test_func(): pass") + class Plugin: + def pytest_report_iteminfo(self, item): + return "FGHJ", 42, "custom" + item.config.pluginmanager.register(Plugin()) + tr = TerminalReporter(item.config, file=linecomp.stringio) + item.config.pluginmanager.register(tr) + tr.config.hook.pytest_itemstart(item=item) + linecomp.assert_contains_lines([ + "*FGHJ " + ]) + tr.config.option.verbose = True + tr.config.hook.pytest_itemstart(item=item) + linecomp.assert_contains_lines([ + "*FGHJ:43: custom*" + ]) + + def pseudo_keyboard_interrupt(self, testdir, linecomp, verbose=False): modcol = testdir.getmodulecol(""" def test_foobar(): @@ -603,11 +645,11 @@ #""", configargs=("--showskipsummary",) + ("-v",)*verbose) rep = TerminalReporter(modcol.config, file=linecomp.stringio) modcol.config.pluginmanager.register(rep) - modcol.config.hook.pytest_testrunstart() + modcol.config.hook.pytest_sessionstart(session=testdir.session) try: for item in testdir.genitems([modcol]): - modcol.config.hook.pytest_itemtestreport( - rep=runner.basic_run_report(item)) + modcol.config.hook.pytest_runtest_logreport( + rep=basic_run_report(item)) except KeyboardInterrupt: excinfo = py.code.ExceptionInfo() else: @@ -615,7 +657,8 @@ s = linecomp.stringio.getvalue() if not verbose: assert s.find("_keyboard_interrupt.py Fs") != -1 - modcol.config.hook.pytest_testrunfinish(exitstatus=2, excrepr=excinfo.getrepr()) + modcol.config.hook.pytest_sessionfinish( + session=testdir.session, exitstatus=2, excrepr=excinfo.getrepr()) text = linecomp.stringio.getvalue() linecomp.assert_contains_lines([ " def test_foobar():", @@ -705,13 +748,20 @@ !!! 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_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) -def test_generic(plugintester): - plugintester.hookcheck(TerminalPlugin) - plugintester.hookcheck(TerminalReporter) - plugintester.hookcheck(CollectonlyReporter) Modified: py/trunk/py/test/plugin/pytest_tmpdir.py ============================================================================== --- py/trunk/py/test/plugin/pytest_tmpdir.py (original) +++ py/trunk/py/test/plugin/pytest_tmpdir.py Tue Jun 16 18:23:18 2009 @@ -1,4 +1,6 @@ """ + provide temporary directories to test functions and methods. + example: pytest_plugins = "pytest_tmpdir" @@ -9,13 +11,9 @@ """ import py -class TmpdirPlugin: - """ provide temporary directories to test functions and methods. - """ - - def pytest_funcarg__tmpdir(self, request): - name = request.function.__name__ - return request.config.mktemp(name, numbered=True) +def pytest_funcarg__tmpdir(request): + name = request.function.__name__ + return request.config.mktemp(name, numbered=True) # =============================================================================== # @@ -23,14 +21,11 @@ # # =============================================================================== # -def test_generic(plugintester): - plugintester.hookcheck(TmpdirPlugin) def test_funcarg(testdir): from py.__.test.funcargs import FuncargRequest item = testdir.getitem("def test_func(tmpdir): pass") - plugin = TmpdirPlugin() - p = plugin.pytest_funcarg__tmpdir(FuncargRequest(item, "tmpdir")) + p = pytest_funcarg__tmpdir(FuncargRequest(item)) assert p.check() bn = p.basename.strip("0123456789-") assert bn.endswith("test_func") Modified: py/trunk/py/test/plugin/pytest_unittest.py ============================================================================== --- py/trunk/py/test/plugin/pytest_unittest.py (original) +++ py/trunk/py/test/plugin/pytest_unittest.py Tue Jun 16 18:23:18 2009 @@ -1,5 +1,5 @@ """ -automatically collect and run traditional "unittest.py" style tests. +automatically discover and run traditional "unittest.py" style tests. you can mix unittest TestCase subclasses and py.test style tests in one test module. @@ -10,17 +10,12 @@ http://johnnydebris.net/svn/projects/py_unittest -$HeadURL: https://codespeak.net/svn/py/branch/pytestplugin/contrib/py_unittest/conftest.py $ -$Id: conftest.py 60979 2009-01-14 22:29:32Z hpk $ """ import py -class UnittestPlugin: - """ discover and integrate traditional ``unittest.py`` tests. - """ - def pytest_pycollect_obj(self, collector, name, obj): - if py.std.inspect.isclass(obj) and issubclass(obj, py.std.unittest.TestCase): - return UnitTestCase(name, parent=collector) +def pytest_pycollect_makeitem(collector, name, obj): + if py.std.inspect.isclass(obj) and issubclass(obj, py.std.unittest.TestCase): + return UnitTestCase(name, parent=collector) class UnitTestCase(py.test.collect.Class): def collect(self): @@ -70,9 +65,6 @@ instance.tearDown() -def test_generic(plugintester): - plugintester.hookcheck(UnittestPlugin) - def test_simple_unittest(testdir): testpath = testdir.makepyfile(""" import unittest @@ -83,9 +75,9 @@ def test_failing(self): self.assertEquals('foo', 'bar') """) - sorter = testdir.inline_run(testpath) - assert sorter.matchreport("testpassing").passed - assert sorter.matchreport("test_failing").failed + reprec = testdir.inline_run(testpath) + assert reprec.matchreport("testpassing").passed + assert reprec.matchreport("test_failing").failed def test_setup(testdir): testpath = testdir.makepyfile(test_two=""" @@ -97,8 +89,8 @@ def test_setUp(self): self.assertEquals(1, self.foo) """) - sorter = testdir.inline_run(testpath) - rep = sorter.matchreport("test_setUp") + reprec = testdir.inline_run(testpath) + rep = reprec.matchreport("test_setUp") assert rep.passed def test_teardown(testdir): @@ -115,8 +107,8 @@ def test_check(self): self.assertEquals(MyTestCase.l, [None]) """) - sorter = testdir.inline_run(testpath) - passed, skipped, failed = sorter.countoutcomes() + reprec = testdir.inline_run(testpath) + passed, skipped, failed = reprec.countoutcomes() print "COUNTS", passed, skipped, failed assert failed == 0, failed assert passed == 2 Modified: py/trunk/py/test/plugin/pytest_xfail.py ============================================================================== --- py/trunk/py/test/plugin/pytest_xfail.py (original) +++ py/trunk/py/test/plugin/pytest_xfail.py Tue Jun 16 18:23:18 2009 @@ -1,5 +1,8 @@ """ -for marking and reporting "expected to fail" tests. +mark tests as expected-to-fail and report them separately. + +example: + @py.test.mark.xfail("needs refactoring") def test_hello(): ... @@ -7,48 +10,47 @@ """ import py -class XfailPlugin(object): - """ mark and report specially about "expected to fail" tests. """ - - def pytest_item_makereport(self, __call__, item, excinfo, when, outerr): - if hasattr(item, 'obj') and hasattr(item.obj, 'func_dict'): - if 'xfail' in item.obj.func_dict: - res = __call__.execute(firstresult=True) - if excinfo: - res.skipped = True - res.failed = res.passed = False - else: - res.skipped = res.passed = False - res.failed = True - return res - - def pytest_report_teststatus(self, rep): - """ return shortletter and verbose word. """ - if 'xfail' in rep.keywords: - if rep.skipped: - return "xfailed", "x", "xfail" - elif rep.failed: - return "xpassed", "P", "xpass" - - # a hook implemented called by the terminalreporter instance/plugin - def pytest_terminal_summary(self, terminalreporter): - tr = terminalreporter - xfailed = tr.stats.get("xfailed") - if xfailed: - tr.write_sep("_", "expected failures") - for event in xfailed: - entry = event.longrepr.reprcrash - key = entry.path, entry.lineno, entry.message - reason = event.longrepr.reprcrash.message - modpath = event.colitem.getmodpath(includemodule=True) - #tr._tw.line("%s %s:%d: %s" %(modpath, entry.path, entry.lineno, entry.message)) - tr._tw.line("%s %s:%d: " %(modpath, entry.path, entry.lineno)) - - xpassed = terminalreporter.stats.get("xpassed") - if xpassed: - tr.write_sep("_", "UNEXPECTEDLY PASSING TESTS") - for event in xpassed: - tr._tw.line("%s: xpassed" %(event.colitem,)) +def pytest_runtest_makereport(__call__, item, call): + if call.when != "call": + return + if hasattr(item, 'obj') and hasattr(item.obj, 'func_dict'): + if 'xfail' in item.obj.func_dict: + res = __call__.execute(firstresult=True) + if call.excinfo: + res.skipped = True + res.failed = res.passed = False + else: + res.skipped = res.passed = False + res.failed = True + return res + +def pytest_report_teststatus(rep): + """ return shortletter and verbose word. """ + if 'xfail' in rep.keywords: + if rep.skipped: + return "xfailed", "x", "xfail" + elif rep.failed: + return "xpassed", "P", "xpass" + +# called by the terminalreporter instance/plugin +def pytest_terminal_summary(terminalreporter): + tr = terminalreporter + xfailed = tr.stats.get("xfailed") + if xfailed: + tr.write_sep("_", "expected failures") + for event in xfailed: + entry = event.longrepr.reprcrash + key = entry.path, entry.lineno, entry.message + reason = event.longrepr.reprcrash.message + modpath = event.item.getmodpath(includemodule=True) + #tr._tw.line("%s %s:%d: %s" %(modpath, entry.path, entry.lineno, entry.message)) + tr._tw.line("%s %s:%d: " %(modpath, entry.path, entry.lineno)) + + xpassed = terminalreporter.stats.get("xpassed") + if xpassed: + tr.write_sep("_", "UNEXPECTEDLY PASSING TESTS") + for event in xpassed: + tr._tw.line("%s: xpassed" %(event.item,)) # =============================================================================== # @@ -56,11 +58,8 @@ # # =============================================================================== -def test_generic(plugintester): - plugintester.hookcheck(XfailPlugin) -def test_xfail(plugintester, linecomp): - testdir = plugintester.testdir() +def test_xfail(testdir, linecomp): p = testdir.makepyfile(test_one=""" import py pytest_plugins="pytest_xfail", Added: py/trunk/py/test/plugin/test_pytest_runner.py ============================================================================== --- (empty file) +++ py/trunk/py/test/plugin/test_pytest_runner.py Tue Jun 16 18:23:18 2009 @@ -0,0 +1,297 @@ +import py +from py.__.test.plugin import pytest_runner as runner +from py.__.code.excinfo 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 + + +class BaseFunctionalTests: + def test_funcattr(self, testdir): + reports = testdir.runitem(""" + import py + @py.test.mark(xfail="needs refactoring") + def test_func(): + raise Exit() + """) + rep = reports[1] + assert rep.keywords['xfail'] == "needs refactoring" + + def test_passfunction(self, testdir): + reports = testdir.runitem(""" + def test_func(): + pass + """) + rep = reports[1] + assert rep.passed + assert not rep.failed + assert rep.shortrepr == "." + assert not hasattr(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 isinstance(rep.longrepr, ReprExceptionInfo) + assert str(rep.shortrepr) == "F" + + 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.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) == 1 + + 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) == 1 + + 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 py + class Function(py.test.collect.Function): + def repr_failure(self, excinfo, outerr): + 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_failure_repr(self, testdir): + testdir.makepyfile(conftest=""" + import py + class Function(py.test.collect.Function): + def repr_failure(self, excinfo): + assert 0 + """) + reports = testdir.runitem(""" + import py + def setup_function(func): + raise ValueError(42) + def test_func(): + pass + """) + assert len(reports) == 1 + 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_capture_in_func(self, testdir): + reports = testdir.runitem(""" + import sys + def setup_function(func): + print "in setup" + def test_func(): + print "in function" + assert 0 + def teardown_function(func): + print "in teardown" + """) + assert reports[0].outerr[0] == "in setup\n" + assert reports[1].outerr[0] == "in function\n" + assert reports[2].outerr[0] == "in teardown\n" + + 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): + from py.__.test.outcome import Exit + try: + testdir.runitem(""" + from py.__.test.outcome import Exit + def test_func(): + raise Exit() + """) + except Exit: + 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): + from py.__.test.outcome import Exit + try: + testdir.runitem(""" + def test_func(): + raise KeyboardInterrupt("fake") + """) + except KeyboardInterrupt, e: + pass + else: + py.test.fail("did not raise") + +class TestExecutionForked(BaseFunctionalTests): + def getrunner(self): + if not hasattr(py.std.os, 'fork'): + py.test.skip("no os.fork available") + return runner.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 TestCollectionReports: + 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 + 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 py + py.test.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_functional_boxed(testdir): + if not hasattr(py.std.os, 'fork'): + py.test.skip("needs os.fork") + p1 = testdir.makepyfile(""" + import os + def test_function(): + os.kill(os.getpid(), 15) + """) + result = testdir.runpytest(p1, "--boxed") + assert result.stdout.fnmatch_lines([ + "*CRASHED*", + "*1 failed*" + ]) Modified: py/trunk/py/test/pluginmanager.py ============================================================================== --- py/trunk/py/test/pluginmanager.py (original) +++ py/trunk/py/test/pluginmanager.py Tue Jun 16 18:23:18 2009 @@ -2,9 +2,15 @@ managing loading and interacting with pytest plugins. """ import py -from py.__.test.plugin import api +from py.__.test.plugin import hookspec + +def check_old_use(mod, modname): + clsname = modname[len('pytest_'):].capitalize() + "Plugin" + assert not hasattr(mod, clsname), (mod, clsname) class PluginManager(object): + class Error(Exception): + """signals a plugin specific error.""" def __init__(self, comregistry=None): if comregistry is None: comregistry = py._com.Registry() @@ -13,30 +19,46 @@ self.impname2plugin = {} self.hook = py._com.Hooks( - hookspecs=api.PluginHooks, + hookspecs=hookspec, registry=self.comregistry) - def register(self, plugin): + 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): + assert not self.isregistered(plugin) + name = self._getpluginname(plugin, name) + assert name not in self.impname2plugin + self.impname2plugin[name] = plugin self.hook.pytest_plugin_registered(plugin=plugin) - import types + self._checkplugin(plugin) self.comregistry.register(plugin) + return True def unregister(self, plugin): self.hook.pytest_plugin_unregistered(plugin=plugin) self.comregistry.unregister(plugin) + for name, value in self.impname2plugin.items(): + if value == plugin: + del self.impname2plugin[name] - def isregistered(self, plugin): - return self.comregistry.isregistered(plugin) + def isregistered(self, plugin, name=None): + return self._getpluginname(plugin, name) in self.impname2plugin def getplugins(self): return list(self.comregistry) - # API for bootstrapping - # def getplugin(self, importname): - impname, clsname = canonical_names(importname) + impname = canonical_importname(importname) return self.impname2plugin[impname] + # API for bootstrapping + # def _envlist(self, varname): val = py.std.os.environ.get(varname, None) if val is not None: @@ -54,10 +76,11 @@ def consider_conftest(self, conftestmodule): cls = getattr(conftestmodule, 'ConftestPlugin', None) - if cls is not None and cls not in self.impname2plugin: - self.impname2plugin[cls] = True - self.register(cls()) - self.consider_module(conftestmodule) + if cls is not None: + raise ValueError("%r: 'ConftestPlugins' only existed till 1.0.0b1, " + "were removed in 1.0.0b2" % (cls,)) + if self.register(conftestmodule, name=conftestmodule.__file__): + self.consider_module(conftestmodule) def consider_module(self, mod): attr = getattr(mod, "pytest_plugins", ()) @@ -69,28 +92,61 @@ def import_plugin(self, spec): assert isinstance(spec, str) - modname, clsname = canonical_names(spec) + modname = canonical_importname(spec) if modname in self.impname2plugin: return mod = importplugin(modname) - plugin = registerplugin(self.register, mod, clsname) - self.impname2plugin[modname] = plugin + check_old_use(mod, modname) + self.register(mod) self.consider_module(mod) + + def _checkplugin(self, plugin): + # ===================================================== + # check plugin hooks + # ===================================================== + methods = collectattr(plugin) + hooks = collectattr(hookspec) + stringio = py.std.StringIO.StringIO() + 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: + Print("found unknown hook:", name) + fail = True + else: + method_args = getargs(method) + if '__call__' in method_args: + method_args.remove('__call__') + hook = hooks[name] + hookargs = getargs(hook) + for arg, hookarg in zip(method_args, hookargs): + if arg != hookarg: + Print("argument mismatch: %r != %r" %(arg, hookarg)) + Print("actual : %s" %(formatdef(method))) + Print("required:", formatdef(hook)) + fail = True + break + #if not fail: + # print "matching hook:", formatdef(method) + if fail: + name = getattr(plugin, '__name__', plugin) + raise self.Error("%s:\n%s" %(name, stringio.getvalue())) # # # API for interacting with registered and instantiated plugin objects # # - def getfirst(self, attrname): - for x in self.comregistry.listattr(attrname): - return x - def listattr(self, attrname, plugins=None, extra=()): return self.comregistry.listattr(attrname, plugins=plugins, extra=extra) - def call_firstresult(self, *args, **kwargs): - return self.comregistry.call_firstresult(*args, **kwargs) - def notify_exception(self, excinfo=None): if excinfo is None: excinfo = py.code.ExceptionInfo() @@ -123,27 +179,15 @@ config.hook.pytest_unconfigure(config=config) config.pluginmanager.unregister(self) - def do_itemrun(self, item, pdb=None): - res = self.comregistry.call_firstresult("pytest_itemrun", item=item, pdb=pdb) - if res is None: - raise ValueError("could not run %r" %(item,)) - # # XXX old code to automatically load classes # -def canonical_names(importspec): - importspec = importspec.lower() +def canonical_importname(name): + name = name.lower() modprefix = "pytest_" - if not importspec.startswith(modprefix): - importspec = modprefix + importspec - clsname = importspec[len(modprefix):].capitalize() + "Plugin" - return importspec, clsname - -def registerplugin(registerfunc, mod, clsname): - pluginclass = getattr(mod, clsname) - plugin = pluginclass() - registerfunc(plugin) - return plugin + if not name.startswith(modprefix): + name = modprefix + name + return name def importplugin(importspec): try: @@ -159,3 +203,30 @@ #print "syspath:", py.std.sys.path #print "curdir:", py.std.os.getcwd() return __import__(importspec) # show the original exception + + + +def isgenerichook(name): + return name == "pytest_plugins" or \ + name.startswith("pytest_funcarg__") or \ + name.startswith("pytest_option_") + +def getargs(func): + args = py.std.inspect.getargs(func.func_code)[0] + startindex = hasattr(func, 'im_self') and 1 or 0 + return args[startindex:] + +def collectattr(obj, prefixes=("pytest_",)): + methods = {} + for apiname in dir(obj): + for prefix in prefixes: + if apiname.startswith(prefix): + methods[apiname] = getattr(obj, apiname) + return methods + +def formatdef(func): + return "%s%s" %( + func.func_name, + py.std.inspect.formatargspec(*py.std.inspect.getargspec(func)) + ) + Modified: py/trunk/py/test/pycollect.py ============================================================================== --- py/trunk/py/test/pycollect.py (original) +++ py/trunk/py/test/pycollect.py Tue Jun 16 18:23:18 2009 @@ -18,7 +18,6 @@ """ import py from py.__.test.collect import configproperty, warnoldcollect -from py.__.code.source import findsource pydir = py.path.local(py.__file__).dirpath() from py.__.test import funcargs @@ -38,16 +37,6 @@ def _getobj(self): return getattr(self.parent.obj, self.name) - def getmodulecollector(self): - return self._getparent(Module) - def getclasscollector(self): - return self._getparent(Class) - def _getparent(self, cls): - current = self - while current and not isinstance(current, cls): - current = current.parent - return current - def getmodpath(self, stopatmodule=True, includemodule=False): """ return python path relative to the containing module. """ chain = self.listchain() @@ -69,37 +58,21 @@ s = ".".join(parts) return s.replace(".[", "[") - def getfslineno(self): + def _getfslineno(self): try: return self._fslineno except AttributeError: pass obj = self.obj - # let decorators etc specify a sane ordering + # xxx let decorators etc specify a sane ordering if hasattr(obj, 'place_as'): obj = obj.place_as - try: - code = py.code.Code(obj) - except TypeError: - # fallback to - fn = (py.std.inspect.getsourcefile(obj) or - py.std.inspect.getfile(obj)) - fspath = fn and py.path.local(fn) or None - if fspath: - try: - _, lineno = findsource(obj) - except IOError: - lineno = None - else: - lineno = None - else: - fspath = code.path - lineno = code.firstlineno - self._fslineno = fspath, lineno - return fspath, lineno - def metainfo(self): - fspath, lineno = self.getfslineno() + self._fslineno = py.code.getfslineno(obj) + return self._fslineno + + def reportinfo(self): + fspath, lineno = self._getfslineno() modpath = self.getmodpath() return fspath, lineno, modpath @@ -120,7 +93,7 @@ return l name2items = self._buildname2items() colitems = name2items.values() - colitems.sort() + colitems.sort(key=lambda item: item.reportinfo()[:2]) return colitems def _buildname2items(self): @@ -146,9 +119,9 @@ return self.join(name) def makeitem(self, name, obj): - res = self.config.hook.pytest_pycollect_obj( + res = self.config.hook.pytest_pycollect_makeitem( collector=self, name=name, obj=obj) - if res: + if res is not None: return res if (self.classnamefilter(name)) and \ py.std.inspect.isclass(obj): @@ -167,18 +140,18 @@ return self._genfunctions(name, obj) def _genfunctions(self, name, funcobj): - module = self.getmodulecollector().obj + module = self.getparent(Module).obj # due to _buildname2items funcobj is the raw function, we need # to work to get at the class - clscol = self.getclasscollector() + clscol = self.getparent(Class) cls = clscol and clscol.obj or None - runspec = funcargs.RunSpecs(funcobj, config=self.config, cls=cls, module=module) - gentesthook = self.config.hook.pytest_genfuncruns.clone(extralookup=module) - gentesthook(runspec=runspec) - if not runspec._combinations: + metafunc = funcargs.Metafunc(funcobj, config=self.config, cls=cls, module=module) + gentesthook = self.config.hook.pytest_generate_tests.clone(extralookup=module) + gentesthook(metafunc=metafunc) + if not metafunc._calls: return self.Function(name, parent=self) return funcargs.FunctionCollector(name=name, - parent=self, combinations=runspec._combinations) + parent=self, calls=metafunc._calls) class Module(py.test.collect.File, PyCollectorMixin): def _getobj(self): @@ -232,9 +205,6 @@ teardown_class = getattr(teardown_class, 'im_func', teardown_class) teardown_class(self.obj) - def _getsortvalue(self): - return self.getfslineno() - class Instance(PyCollectorMixin, py.test.collect.Collector): def _getobj(self): return self.parent.obj() @@ -257,9 +227,6 @@ """ mixin for the code common to Function and Generator. """ - def _getsortvalue(self): - return self.getfslineno() - def setup(self): """ perform setup for this test function. """ if hasattr(self.obj, 'im_self'): @@ -346,25 +313,26 @@ """ a Function Item is responsible for setting up and executing a Python callable test object. """ - def __init__(self, name, parent=None, config=None, args=(), funcargs=None, callobj=_dummy): - super(Function, self).__init__(name, parent, config=config) - self._finalizers = [] - self._args = args - if funcargs is None: - funcargs = {} - self.funcargs = funcargs + _genid = None + def __init__(self, name, parent=None, args=None, + callspec=None, callobj=_dummy): + super(Function, self).__init__(name, parent) + 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 - def addfinalizer(self, func): - self._finalizers.append(func) - - def teardown(self): - finalizers = self._finalizers - while finalizers: - call = finalizers.pop() - call() - super(Function, self).teardown() + def _isyieldedfunction(self): + return self._args is not None def readkeywords(self): d = super(Function, self).readkeywords() @@ -372,20 +340,23 @@ return d def runtest(self): - """ execute the given test function. """ - self.config.hook.pytest_pyfunc_call(pyfuncitem=self, - args=self._args, kwargs=self.funcargs) + """ execute the underlying test function. """ + self.config.hook.pytest_pyfunc_call(pyfuncitem=self) def setup(self): super(Function, self).setup() - funcargs.fillfuncargs(self) + if hasattr(self, 'funcargs'): + 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) + self.obj == other.obj and + getattr(self, '_genid', None) == + getattr(other, '_genid', None) + ) except AttributeError: pass return False Deleted: /py/trunk/py/test/runner.py ============================================================================== --- /py/trunk/py/test/runner.py Tue Jun 16 18:23:18 2009 +++ (empty file) @@ -1,197 +0,0 @@ -""" - internal classes for - - * executing test items - * running collectors - * and generating report events about it -""" - -import py - -from py.__.test.outcome import Exit, Skipped - -def basic_run_report(item, pdb=None): - """ return report about setting up and running a test item. """ - excinfo = None - capture = item.config._getcapture() - try: - try: - when = "setup" - item.config._setupstate.prepare(item) - try: - when = "execute" - if not item._deprecated_testexecution(): - item.runtest() - finally: - when = "teardown" - item.config._setupstate.teardown_exact(item) - when = "execute" - finally: - outerr = capture.reset() - except (Exit, KeyboardInterrupt): - raise - except: - excinfo = py.code.ExceptionInfo() - testrep = item.config.hook.pytest_item_makereport( - item=item, excinfo=excinfo, when=when, outerr=outerr) - if pdb and testrep.failed: - tw = py.io.TerminalWriter() - testrep.toterminal(tw) - pdb(excinfo) - return testrep - -def basic_collect_report(collector): - excinfo = res = None - try: - capture = collector.config._getcapture() - try: - res = collector._memocollect() - finally: - outerr = capture.reset() - except (Exit, KeyboardInterrupt): - raise - except: - excinfo = py.code.ExceptionInfo() - return CollectReport(collector, res, excinfo, outerr) - -def forked_run_report(item, pdb=None): - EXITSTATUS_TESTEXIT = 4 - from py.__.test.dist.mypickle import ImmutablePickler - ipickle = ImmutablePickler(uneven=0) - ipickle.selfmemoize(item.config) - def runforked(): - try: - testrep = basic_run_report(item) - except (KeyboardInterrupt, Exit): - py.std.os._exit(EXITSTATUS_TESTEXIT) - return ipickle.dumps(testrep) - - ff = py.process.ForkedFunc(runforked) - result = ff.waitfinish() - if result.retval is not None: - return ipickle.loads(result.retval) - else: - if result.exitstatus == EXITSTATUS_TESTEXIT: - raise Exit("forked test item %s raised Exit" %(item,)) - return report_process_crash(item, result) - -def report_process_crash(item, result): - path, lineno = item.getfslineno() - longrepr = [ - ("X", "CRASHED"), - ("%s:%s: CRASHED with signal %d" %(path, lineno, result.signal)), - ] - return ItemTestReport(item, excinfo=longrepr, when="???") - -class BaseReport(object): - def __repr__(self): - l = ["%s=%s" %(key, value) - for key, value in self.__dict__.items()] - return "<%s %s>" %(self.__class__.__name__, " ".join(l),) - - def toterminal(self, out): - longrepr = self.longrepr - if hasattr(longrepr, 'toterminal'): - longrepr.toterminal(out) - else: - out.line(str(longrepr)) - -class ItemTestReport(BaseReport): - failed = passed = skipped = False - - # XXX rename colitem to item here - def __init__(self, colitem, excinfo=None, when=None, outerr=None): - self.colitem = colitem - if colitem and when != "setup": - self.keywords = colitem.readkeywords() - else: - # if we fail during setup it might mean - # we are not able to access the underlying object - # this might e.g. happen if we are unpickled - # and our parent collector did not collect us - # (because it e.g. skipped for platform reasons) - self.keywords = {} - if not excinfo: - self.passed = True - self.shortrepr = "." - else: - self.when = when - if not isinstance(excinfo, py.code.ExceptionInfo): - self.failed = True - shortrepr = "?" - longrepr = excinfo - elif excinfo.errisinstance(Skipped): - self.skipped = True - shortrepr = "s" - longrepr = self.colitem._repr_failure_py(excinfo, outerr) - else: - self.failed = True - shortrepr = self.colitem.shortfailurerepr - if self.when == "execute": - longrepr = self.colitem.repr_failure(excinfo, outerr) - else: # exception in setup or teardown - longrepr = self.colitem._repr_failure_py(excinfo, outerr) - shortrepr = shortrepr.lower() - self.shortrepr = shortrepr - self.longrepr = longrepr - - -class CollectReport(BaseReport): - skipped = failed = passed = False - - def __init__(self, colitem, result, excinfo=None, outerr=None): - # XXX rename to collector - self.colitem = colitem - if not excinfo: - self.passed = True - self.result = result - else: - self.outerr = outerr - self.longrepr = self.colitem._repr_failure_py(excinfo, outerr) - if excinfo.errisinstance(Skipped): - self.skipped = True - self.reason = str(excinfo.value) - else: - self.failed = True - -class ItemSetupReport(BaseReport): - failed = passed = skipped = False - def __init__(self, item, excinfo=None, outerr=None): - self.item = item - self.outerr = outerr - if not excinfo: - self.passed = True - else: - if excinfo.errisinstance(Skipped): - self.skipped = True - else: - self.failed = True - self.excrepr = item._repr_failure_py(excinfo, []) - -class SetupState(object): - """ shared state for setting up/tearing down test items or collectors. """ - def __init__(self): - self.stack = [] - - def teardown_all(self): - while self.stack: - col = self.stack.pop() - col.teardown() - - def teardown_exact(self, item): - if self.stack and self.stack[-1] == item: - col = self.stack.pop() - col.teardown() - - def prepare(self, colitem): - """ setup objects along the collector chain to the test-method - Teardown any unneccessary previously setup objects.""" - needed_collectors = colitem.listchain() - while self.stack: - if self.stack == needed_collectors[:len(self.stack)]: - break - col = self.stack.pop() - col.teardown() - for col in needed_collectors[len(self.stack):]: - col.setup() - self.stack.append(col) Modified: py/trunk/py/test/session.py ============================================================================== --- py/trunk/py/test/session.py (original) +++ py/trunk/py/test/session.py Tue Jun 16 18:23:18 2009 @@ -11,7 +11,6 @@ # imports used for genitems() Item = py.test.collect.Item Collector = py.test.collect.Collector -from runner import basic_collect_report class Session(object): """ @@ -42,7 +41,7 @@ else: assert isinstance(next, Collector) self.config.hook.pytest_collectstart(collector=next) - rep = basic_collect_report(next) + rep = self.config.hook.pytest_make_collect_report(collector=next) if rep.passed: for x in self.genitems(rep.result, keywordexpr): yield x @@ -78,18 +77,19 @@ def sessionstarts(self): """ setup any neccessary resources ahead of the test run. """ - self.config.hook.pytest_testrunstart() + self.config.hook.pytest_sessionstart(session=self) - def pytest_itemtestreport(self, rep): + def pytest_runtest_logreport(self, rep): if rep.failed: self._testsfailed = True if self.config.option.exitfirst: self.shouldstop = True - pytest_collectreport = pytest_itemtestreport + pytest_collectreport = pytest_runtest_logreport def sessionfinishes(self, exitstatus=0, excinfo=None): """ teardown any resources after a test run. """ - self.config.hook.pytest_testrunfinish( + self.config.hook.pytest_sessionfinish( + session=self, exitstatus=exitstatus, excrepr=excinfo and excinfo.getrepr() or None ) @@ -112,9 +112,7 @@ if self.shouldstop: break if not self.config.option.collectonly: - self.runtest(item) - - self.config._setupstate.teardown_all() + item.config.hook.pytest_runtest_protocol(item=item) except KeyboardInterrupt: captured_excinfo = py.code.ExceptionInfo() exitstatus = outcome.EXIT_INTERRUPTED @@ -126,11 +124,3 @@ exitstatus = outcome.EXIT_TESTSFAILED self.sessionfinishes(exitstatus=exitstatus, excinfo=captured_excinfo) return exitstatus - - def runpdb(self, excinfo): - from py.__.test.custompdb import post_mortem - post_mortem(excinfo._excinfo[2]) - - def runtest(self, item): - pdb = self.config.option.usepdb and self.runpdb or None - item.config.pluginmanager.do_itemrun(item, pdb=pdb) Modified: py/trunk/py/test/testing/acceptance_test.py ============================================================================== --- py/trunk/py/test/testing/acceptance_test.py (original) +++ py/trunk/py/test/testing/acceptance_test.py Tue Jun 16 18:23:18 2009 @@ -5,9 +5,8 @@ class TestGeneralUsage: def test_config_error(self, testdir): testdir.makeconftest(""" - class ConftestPlugin: - def pytest_configure(self, config): - raise config.Error("hello") + def pytest_configure(config): + raise config.Error("hello") """) result = testdir.runpytest(testdir.tmpdir) assert result.ret != 0 @@ -17,9 +16,8 @@ def test_config_preparse_plugin_option(self, testdir): testdir.makepyfile(pytest_xyz=""" - class XyzPlugin: - def pytest_addoption(self, parser): - parser.addoption("--xyz", dest="xyz", action="store") + def pytest_addoption(parser): + parser.addoption("--xyz", dest="xyz", action="store") """) testdir.makepyfile(test_one=""" import py @@ -443,21 +441,6 @@ class TestInteractive: - 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() - child.expect("1 failed") - if child.isalive(): - child.wait() - def test_simple_looponfail_interaction(self, testdir): p1 = testdir.makepyfile(""" def test_1(): Deleted: /py/trunk/py/test/testing/test_api.py ============================================================================== --- /py/trunk/py/test/testing/test_api.py Tue Jun 16 18:23:18 2009 +++ (empty file) @@ -1,14 +0,0 @@ - -class TestPyfuncHooks: - 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, *args, **kwargs): - raise ValueError - class MyPlugin2: - def pytest_pyfunc_call(self, pyfuncitem, *args, **kwargs): - return True - config.pluginmanager.register(MyPlugin1()) - config.pluginmanager.register(MyPlugin2()) - config.hook.pytest_pyfunc_call(pyfuncitem=item) Modified: py/trunk/py/test/testing/test_collect.py ============================================================================== --- py/trunk/py/test/testing/test_collect.py (original) +++ py/trunk/py/test/testing/test_collect.py Tue Jun 16 18:23:18 2009 @@ -6,7 +6,7 @@ assert not issubclass(Collector, Item) assert not issubclass(Item, Collector) - def test_check_equality_and_cmp_basic(self, testdir): + def test_check_equality(self, testdir): modcol = testdir.getmodulecol(""" def test_pass(): pass def test_fail(): assert 0 @@ -25,11 +25,7 @@ assert isinstance(fn3, py.test.collect.Function) assert not (fn1 == fn3) assert fn1 != fn3 - assert cmp(fn1, fn3) == -1 - assert cmp(fn1, 10) == -1 - assert cmp(fn2, 10) == -1 - assert cmp(fn3, 10) == -1 for fn in fn1,fn2,fn3: assert fn != 3 assert fn != modcol @@ -37,6 +33,24 @@ assert [1,2,3] != fn assert modcol != fn + def test_getparent(self, testdir): + modcol = testdir.getmodulecol(""" + class TestClass: + def test_foo(): + pass + """) + cls = modcol.collect_by_name("TestClass") + fn = cls.collect_by_name("()").collect_by_name("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_totrail_and_back(self, tmpdir): a = tmpdir.ensure("a", dir=1) tmpdir.ensure("a", "__init__.py") @@ -157,12 +171,12 @@ testdir.plugins.append(Plugin()) testdir.mkdir("hello") testdir.mkdir("world") - sorter = testdir.inline_run() + reprec = testdir.inline_run() assert "hello" in wascalled assert "world" in wascalled # make sure the directories do not get double-appended - colreports = sorter.getreports("collectreport") - names = [rep.colitem.name for rep in colreports] + colreports = reprec.getreports("pytest_collectreport") + names = [rep.collector.name for rep in colreports] assert names.count("hello") == 1 class TestCustomConftests: @@ -191,18 +205,22 @@ assert item.name == "hello.xxx" assert item.__class__.__name__ == "CustomItem" - def test_avoid_directory_on_option(self, testdir): + def test_collectignore_exclude_on_option(self, testdir): testdir.makeconftest(""" - class ConftestPlugin: - def pytest_addoption(self, parser): - parser.addoption("--XX", action="store_true", default=False) - def pytest_collect_recurse(self, path, parent): - return parent.config.getvalue("XX") + 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") - sorter = testdir.inline_run(testdir.tmpdir) - names = [rep.colitem.name for rep in sorter.getreports("collectreport")] + testdir.makepyfile(test_world="#") + reprec = testdir.inline_run(testdir.tmpdir) + names = [rep.collector.name for rep in reprec.getreports("pytest_collectreport")] assert 'hello' not in names - evrec = testdir.inline_run(testdir.tmpdir, "--XX") - names = [rep.colitem.name for rep in evrec.getreports("collectreport")] + assert 'test_world.py' not in names + reprec = testdir.inline_run(testdir.tmpdir, "--XX") + names = [rep.collector.name for rep in reprec.getreports("pytest_collectreport")] assert 'hello' in names + assert 'test_world.py' in names Modified: py/trunk/py/test/testing/test_config.py ============================================================================== --- py/trunk/py/test/testing/test_config.py (original) +++ py/trunk/py/test/testing/test_config.py Tue Jun 16 18:23:18 2009 @@ -303,8 +303,8 @@ def test_options_on_small_file_do_not_blow_up(testdir): def runfiletest(opts): - sorter = testdir.inline_run(*opts) - passed, skipped, failed = sorter.countoutcomes() + reprec = testdir.inline_run(*opts) + passed, skipped, failed = reprec.countoutcomes() assert failed == 2 assert skipped == passed == 0 path = testdir.makepyfile(""" Modified: py/trunk/py/test/testing/test_conftesthandle.py ============================================================================== --- py/trunk/py/test/testing/test_conftesthandle.py (original) +++ py/trunk/py/test/testing/test_conftesthandle.py Tue Jun 16 18:23:18 2009 @@ -1,98 +1,92 @@ import py from py.__.test.conftesthandle import Conftest -class TestConftestValueAccessGlobal: - def setup_class(cls): - # if we have "global" conftests (i.e. no __init__.py - # and thus no further import scope) it should still all work - # because "global" conftests are imported with a - # mangled module name (related to their actual path) - cls.basedir = d = py.test.ensuretemp(cls.__name__) +def pytest_generate_tests(metafunc): + if "basedir" in metafunc.funcargnames: + metafunc.addcall(param="global") + metafunc.addcall(param="inpackage") + +def pytest_funcarg__basedir(request): + def basedirmaker(request): + basedir = d = request.config.ensuretemp(request.param) d.ensure("adir/conftest.py").write("a=1 ; Directory = 3") d.ensure("adir/b/conftest.py").write("b=2 ; a = 1.5") + if request.param == "inpackage": + d.ensure("adir/__init__.py") + d.ensure("adir/b/__init__.py") + return d + return request.cached_setup(lambda: basedirmaker(request), extrakey=request.param) - def test_basic_init(self): +class TestConftestValueAccessGlobal: + def test_basic_init(self, basedir): conftest = Conftest() - conftest.setinitial([self.basedir.join("adir")]) + conftest.setinitial([basedir.join("adir")]) assert conftest.rget("a") == 1 - def test_onimport(self): + def test_onimport(self, basedir): l = [] conftest = Conftest(onimport=l.append) - conftest.setinitial([self.basedir.join("adir")]) + conftest.setinitial([basedir.join("adir")]) assert len(l) == 2 # default + the one assert conftest.rget("a") == 1 - assert conftest.rget("b", self.basedir.join("adir", "b")) == 2 + assert conftest.rget("b", basedir.join("adir", "b")) == 2 assert len(l) == 3 - def test_immediate_initialiation_and_incremental_are_the_same(self): + def test_immediate_initialiation_and_incremental_are_the_same(self, basedir): conftest = Conftest() snap0 = len(conftest._path2confmods) - conftest.getconftestmodules(self.basedir) + conftest.getconftestmodules(basedir) snap1 = len(conftest._path2confmods) #assert len(conftest._path2confmods) == snap1 + 1 - conftest.getconftestmodules(self.basedir.join('adir')) + conftest.getconftestmodules(basedir.join('adir')) assert len(conftest._path2confmods) == snap1 + 1 - conftest.getconftestmodules(self.basedir.join('b')) + conftest.getconftestmodules(basedir.join('b')) assert len(conftest._path2confmods) == snap1 + 2 - def test_default_Module_setting_is_visible_always(self): - for path in self.basedir.parts(): + def test_default_Module_setting_is_visible_always(self, basedir): + for path in basedir.parts(): conftest = Conftest(path) #assert conftest.lget("Module") == py.test.collect.Module assert conftest.rget("Module") == py.test.collect.Module - def test_default_has_lower_prio(self): - conftest = Conftest(self.basedir.join("adir")) + def test_default_has_lower_prio(self, basedir): + conftest = Conftest(basedir.join("adir")) assert conftest.rget('Directory') == 3 #assert conftest.lget('Directory') == py.test.collect.Directory - def test_value_access_not_existing(self): - conftest = Conftest(self.basedir) + def test_value_access_not_existing(self, basedir): + conftest = Conftest(basedir) py.test.raises(KeyError, "conftest.rget('a')") #py.test.raises(KeyError, "conftest.lget('a')") - def test_value_access_by_path(self): - conftest = Conftest(self.basedir) - assert conftest.rget("a", self.basedir.join('adir')) == 1 - #assert conftest.lget("a", self.basedir.join('adir')) == 1 - assert conftest.rget("a", self.basedir.join('adir', 'b')) == 1.5 - #assert conftest.lget("a", self.basedir.join('adir', 'b')) == 1 - #assert conftest.lget("b", self.basedir.join('adir', 'b')) == 2 + def test_value_access_by_path(self, basedir): + conftest = Conftest(basedir) + assert conftest.rget("a", basedir.join('adir')) == 1 + #assert conftest.lget("a", basedir.join('adir')) == 1 + 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, - # 'conftest.lget("b", self.basedir.join("a"))' + # 'conftest.lget("b", basedir.join("a"))' #) - def test_value_access_with_init_one_conftest(self): - conftest = Conftest(self.basedir.join('adir')) + def test_value_access_with_init_one_conftest(self, basedir): + conftest = Conftest(basedir.join('adir')) assert conftest.rget("a") == 1 #assert conftest.lget("a") == 1 - def test_value_access_with_init_two_conftests(self): - conftest = Conftest(self.basedir.join("adir", "b")) + def test_value_access_with_init_two_conftests(self, basedir): + conftest = Conftest(basedir.join("adir", "b")) conftest.rget("a") == 1.5 #conftest.lget("a") == 1 #conftest.lget("b") == 1 - def test_value_access_with_confmod(self): - topdir = self.basedir.join("adir", "b") + def test_value_access_with_confmod(self, basedir): + topdir = basedir.join("adir", "b") topdir.ensure("xx", dir=True) conftest = Conftest(topdir) mod, value = conftest.rget_with_confmod("a", topdir) assert value == 1.5 path = py.path.local(mod.__file__) - assert path.dirpath() == self.basedir.join("adir", "b") + assert path.dirpath() == basedir.join("adir", "b") assert path.purebasename == "conftest" - -class TestConftestValueAccessInPackage(TestConftestValueAccessGlobal): - def setup_class(cls): - TestConftestValueAccessGlobal.__dict__['setup_class'](cls) - d = cls.basedir - d.ensure("adir/__init__.py") - d.ensure("adir/b/__init__.py") - - - - - - Modified: py/trunk/py/test/testing/test_funcargs.py ============================================================================== --- py/trunk/py/test/testing/test_funcargs.py (original) +++ py/trunk/py/test/testing/test_funcargs.py Tue Jun 16 18:23:18 2009 @@ -19,9 +19,8 @@ class TestFillFuncArgs: def test_funcarg_lookupfails(self, testdir): testdir.makeconftest(""" - class ConftestPlugin: - def pytest_funcarg__xyzsomething(self, request): - return 42 + def pytest_funcarg__xyzsomething(request): + return 42 """) item = testdir.getitem("def test_func(some): pass") exc = py.test.raises(LookupError, "funcargs.fillfuncargs(item)") @@ -67,14 +66,37 @@ 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) + assert 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 + py.test.collect._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, argname="other") - assert req.argname == "other" + req = funcargs.FuncargRequest(item) assert req.function == item.obj assert hasattr(req.module, 'test_func') assert req.cls is None @@ -88,144 +110,292 @@ def test_func(self, something): pass """) - req = funcargs.FuncargRequest(item, argname="something") + req = funcargs.FuncargRequest(item) assert req.cls.__name__ == "TestB" - - def test_request_contains_funcargs_provider(self, testdir): + assert req.instance.__class__ == req.cls + + def XXXtest_request_contains_funcargs_provider(self, testdir): modcol = testdir.getmodulecol(""" def pytest_funcarg__something(request): pass class TestClass: def test_method(self, something): - pass + pass """) item1, = testdir.genitems([modcol]) assert item1.name == "test_method" - provider = funcargs.FuncargRequest(item1, "something")._provider + provider = funcargs.FuncargRequest(item1)._provider assert len(provider) == 1 assert provider[0].__name__ == "pytest_funcarg__something" - def test_request_call_next_provider(self, testdir): + def test_getfuncargvalue_recursive(self, testdir): + testdir.makeconftest(""" + def pytest_funcarg__something(request): + return 1 + """) item = testdir.getitem(""" - def pytest_funcarg__something(request): pass + 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, "something") - val = req.call_next_provider() - assert val is None - py.test.raises(req.Error, "req.call_next_provider()") + req = funcargs.FuncargRequest(item) + 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): + def test_request_addfinalizer_scopes(self, testdir): item = testdir.getitem(""" - def pytest_funcarg__something(request): pass + teardownlist = [] + def pytest_funcarg__something(request): + for scope in ("function", "module", "session"): + request.addfinalizer( + lambda x=scope: teardownlist.append(x), + scope=scope) + def test_func(something): pass """) - req = funcargs.FuncargRequest(item, "something") - l = [1] - req.addfinalizer(l.pop) - item.teardown() + req = funcargs.FuncargRequest(item) + req.config._setupstate.prepare(item) # XXX + req._fillfuncargs() + # successively check finalization calls + teardownlist = item.getparent(py.test.collect.Module).obj.teardownlist + ss = item.config._setupstate + assert not teardownlist + ss.teardown_exact(item) + print ss.stack + assert teardownlist == ['function'] + ss.teardown_exact(item.parent) + assert teardownlist == ['function', 'module'] + ss.teardown_all() + assert teardownlist == ['function', 'module', 'session'] + + def test_request_addfinalizer_unknown_scope(self, testdir): + item = testdir.getitem("def test_func(): pass") + req = funcargs.FuncargRequest(item) + py.test.raises(ValueError, "req.addfinalizer(None, scope='xyz')") def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") item, = testdir.genitems([modcol]) - req = funcargs.FuncargRequest(item, "xxx") + req = funcargs.FuncargRequest(item) assert req.fspath == modcol.fspath -class TestRunSpecs: +class TestRequestProtocol: + @py.test.mark.xfail + def test_protocol(self, testdir): + item = testdir.getitem(""" + def pytest_funcarg_arg1(request): return 1 + def pytest_funcarg_arg2(request): return 2 + def test_func(arg1, arg2): pass + """) + req = funcargs.FuncargRequest(item) + req._fillargs() + #assert item.funcreq. + + +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_cached_setup_functional(self, testdir): + testdir.makepyfile(test_0=""" + l = [] + def pytest_funcarg__something(request): + val = request.cached_setup(setup, teardown) + return val + def setup(mycache=[1]): + l.append(mycache.pop()) + return l + def teardown(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 - runspec = funcargs.RunSpecs(function) - assert not runspec.funcargnames + metafunc = funcargs.Metafunc(function) + assert not metafunc.funcargnames def test_function_basic(self): def func(arg1, arg2="qwe"): pass - runspec = funcargs.RunSpecs(func) - assert len(runspec.funcargnames) == 1 - assert 'arg1' in runspec.funcargnames - assert runspec.function is func - assert runspec.cls is None + 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_addfuncarg_basic(self): + def test_addcall_id(self): def func(arg1): pass - runspec = funcargs.RunSpecs(func) - py.test.raises(ValueError, """ - runspec.addfuncarg("notexists", 100) - """) - runspec.addfuncarg("arg1", 100) - assert len(runspec._combinations) == 1 - assert runspec._combinations[0] == {'arg1': 100} + metafunc = funcargs.Metafunc(func) + py.test.raises(ValueError, "metafunc.addcall(id=None)") - def test_addfuncarg_two(self): + 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 - runspec = funcargs.RunSpecs(func) - runspec.addfuncarg("arg1", 100) - runspec.addfuncarg("arg1", 101) - assert len(runspec._combinations) == 2 - assert runspec._combinations[0] == {'arg1': 100} - assert runspec._combinations[1] == {'arg1': 101} - - def test_addfuncarg_combined(self): - runspec = funcargs.RunSpecs(lambda arg1, arg2: 0) - runspec.addfuncarg('arg1', 1) - runspec.addfuncarg('arg1', 2) - runspec.addfuncarg('arg2', 100) - combinations = runspec._combinations - assert len(combinations) == 2 - assert combinations[0] == {'arg1': 1, 'arg2': 100} - assert combinations[1] == {'arg1': 2, 'arg2': 100} - runspec.addfuncarg('arg2', 101) - assert len(combinations) == 4 - assert combinations[-1] == {'arg1': 2, 'arg2': 101} + 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_genfuncruns(runspec): - runspec.addfuncarg("runspec", runspec) + 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): + assert metafunc.config == py.test.config + assert metafunc.module.__name__ == __name__ + assert metafunc.function == test_function + assert metafunc.cls is None - def test_function(runspec): - assert runspec.config == py.test.config - assert runspec.module.__name__ == __name__ - assert runspec.function == test_function - assert runspec.cls is None - class TestClass: - def test_method(self, runspec): - assert runspec.config == py.test.config - assert runspec.module.__name__ == __name__ - # XXX actually have the unbound test function here? - assert runspec.function == TestClass.test_method.im_func - assert runspec.cls == TestClass + class TestClass: + def test_method(self, metafunc): + assert metafunc.config == py.test.config + assert metafunc.module.__name__ == __name__ + # XXX actually have an unbound test function here? + assert metafunc.function == TestClass.test_method.im_func + assert metafunc.cls == TestClass """) result = testdir.runpytest(p, "-v") result.stdout.fnmatch_lines([ "*2 passed in*", ]) - def test_arg_twice(self, testdir): + def test_addcall_with_funcargs_two(self, testdir): testdir.makeconftest(""" - class ConftestPlugin: - def pytest_genfuncruns(self, runspec): - assert "arg" in runspec.funcargnames - runspec.addfuncarg("arg", 10) - runspec.addfuncarg("arg", 20) + def pytest_generate_tests(metafunc): + assert "arg1" in metafunc.funcargnames + metafunc.addcall(funcargs=dict(arg1=1, arg2=2)) """) p = testdir.makepyfile(""" - def test_myfunc(arg): - assert arg == 10 + 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) assert result.stdout.fnmatch_lines([ - "*test_myfunc*PASS*", # case for 10 - "*test_myfunc*FAIL*", # case for 20 + "*test_myfunc*0*PASS*", + "*test_myfunc*1*FAIL*", "*1 failed, 1 passed*" ]) def test_two_functions(self, testdir): p = testdir.makepyfile(""" - def pytest_genfuncruns(runspec): - runspec.addfuncarg("arg1", 10) - runspec.addfuncarg("arg1", 20) + 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 @@ -240,27 +410,28 @@ "*1 failed, 3 passed*" ]) - def test_genfuncarg_inmodule(self, testdir): + def test_generate_plugin_and_module(self, testdir): testdir.makeconftest(""" - class ConftestPlugin: - def pytest_genfuncruns(self, runspec): - assert "arg" in runspec.funcargnames - runspec.addfuncarg("arg", 10) + def pytest_generate_tests(metafunc): + assert "arg1" in metafunc.funcargnames + metafunc.addcall(id="world", param=(2,100)) """) p = testdir.makepyfile(""" - def pytest_genfuncruns(runspec): - runspec.addfuncarg("arg2", 10) - runspec.addfuncarg("arg2", 20) - runspec.addfuncarg("classarg", 17) + 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, arg, arg2, classarg): - assert classarg == 17 - assert arg == arg2 + def test_myfunc(self, arg1, arg2): + assert arg1 == arg2 """) result = testdir.runpytest("-v", p) assert result.stdout.fnmatch_lines([ - "*test_myfunc*0*PASS*", - "*test_myfunc*1*FAIL*", + "*test_myfunc*hello*PASS*", + "*test_myfunc*world*FAIL*", "*1 failed, 1 passed*" ]) Modified: py/trunk/py/test/testing/test_genitems.py ============================================================================== --- py/trunk/py/test/testing/test_genitems.py (original) +++ py/trunk/py/test/testing/test_genitems.py Tue Jun 16 18:23:18 2009 @@ -10,7 +10,7 @@ pass """) p.copy(p.dirpath(p.purebasename + "2" + ".py")) - items, sorter = testdir.inline_genitems(p.dirpath()) + items, reprec = testdir.inline_genitems(p.dirpath()) assert len(items) == 4 for numi, i in enumerate(items): for numj, j in enumerate(items): @@ -27,8 +27,8 @@ def test_subdir_conftest_error(self, testdir): tmp = testdir.tmpdir tmp.ensure("sub", "conftest.py").write("raise SyntaxError\n") - items, sorter = testdir.inline_genitems(tmp) - collectionfailures = sorter.getfailedcollections() + items, reprec = testdir.inline_genitems(tmp) + collectionfailures = reprec.getfailedcollections() assert len(collectionfailures) == 1 ev = collectionfailures[0] assert ev.longrepr.reprcrash.message.startswith("SyntaxError") @@ -45,7 +45,7 @@ class TestY(TestX): pass ''') - items, sorter = testdir.inline_genitems(p) + items, reprec = testdir.inline_genitems(p) assert len(items) == 3 assert items[0].name == 'testone' assert items[1].name == 'testmethod_one' @@ -70,11 +70,11 @@ assert 42 == 43 """) def check(keyword, name): - sorter = testdir.inline_run("-s", "-k", keyword, file_test) - passed, skipped, failed = sorter.listoutcomes() + reprec = testdir.inline_run("-s", "-k", keyword, file_test) + passed, skipped, failed = reprec.listoutcomes() assert len(failed) == 1 - assert failed[0].colitem.name == name - assert len(sorter.getcalls('deselected')) == 1 + assert failed[0].item.name == name + assert len(reprec.getcalls('pytest_deselected')) == 1 for keyword in ['test_one', 'est_on']: #yield check, keyword, 'test_one' @@ -97,12 +97,12 @@ """) for keyword in ('xxx', 'xxx test_2', 'TestClass', 'xxx -test_1', 'TestClass test_2', 'xxx TestClass test_2',): - sorter = testdir.inline_run(p.dirpath(), '-s', '-k', keyword) + reprec = testdir.inline_run(p.dirpath(), '-s', '-k', keyword) print "keyword", repr(keyword) - passed, skipped, failed = sorter.listoutcomes() + passed, skipped, failed = reprec.listoutcomes() assert len(passed) == 1 - assert passed[0].colitem.name == "test_2" - dlist = sorter.getcalls("pytest_deselected") + assert passed[0].item.name == "test_2" + dlist = reprec.getcalls("pytest_deselected") assert len(dlist) == 1 assert dlist[0].items[0].name == 'test_1' @@ -112,11 +112,11 @@ def test_two(): assert 1 def test_three(): assert 1 """) - sorter = testdir.inline_run("-k", "test_two:", threepass) - passed, skipped, failed = sorter.listoutcomes() + reprec = testdir.inline_run("-k", "test_two:", threepass) + passed, skipped, failed = reprec.listoutcomes() assert len(passed) == 2 assert not failed - dlist = sorter.getcalls("pytest_deselected") + dlist = reprec.getcalls("pytest_deselected") assert len(dlist) == 1 item = dlist[0].items[0] assert item.name == "test_one" Modified: py/trunk/py/test/testing/test_outcome.py ============================================================================== --- py/trunk/py/test/testing/test_outcome.py (original) +++ py/trunk/py/test/testing/test_outcome.py Tue Jun 16 18:23:18 2009 @@ -76,6 +76,13 @@ print py.code.ExceptionInfo() py.test.fail("spurious skip") +def test_pytest_exit(): + try: + py.test.exit("hello") + except: + excinfo = py.code.ExceptionInfo() + assert excinfo.errisinstance(KeyboardInterrupt) + def test_pytest_mark_getattr(): from py.__.test.outcome import mark def f(): pass Modified: py/trunk/py/test/testing/test_parseopt.py ============================================================================== --- py/trunk/py/test/testing/test_parseopt.py (original) +++ py/trunk/py/test/testing/test_parseopt.py Tue Jun 16 18:23:18 2009 @@ -4,10 +4,10 @@ pytest_plugins = 'pytest_iocapture' class TestParser: - def test_init(self, stdcapture): + def test_init(self, capsys): parser = parseopt.Parser(usage="xyz") py.test.raises(SystemExit, 'parser.parse(["-h"])') - out, err = stdcapture.reset() + out, err = capsys.reset() assert out.find("xyz") != -1 def test_group_add_and_get(self): Modified: py/trunk/py/test/testing/test_pickling.py ============================================================================== --- py/trunk/py/test/testing/test_pickling.py (original) +++ py/trunk/py/test/testing/test_pickling.py Tue Jun 16 18:23:18 2009 @@ -14,7 +14,7 @@ def pytest_funcarg__testdir(request): setglobals(request) - return request.call_next_provider() + return request.getfuncargvalue("testdir") class ImmutablePickleTransport: def __init__(self, request): @@ -87,11 +87,10 @@ def test_config_pickling_customoption(self, testdir): testdir.makeconftest(""" - class ConftestPlugin: - def pytest_addoption(self, parser): - group = parser.addgroup("testing group") - group.addoption('-G', '--glong', action="store", default=42, - type="int", dest="gdest", help="g value.") + def pytest_addoption(parser): + group = parser.addgroup("testing group") + group.addoption('-G', '--glong', action="store", default=42, + type="int", dest="gdest", help="g value.") """) config = testdir.parseconfig("-G", "11") assert config.option.gdest == 11 @@ -108,11 +107,10 @@ tmp = testdir.tmpdir.ensure("w1", "w2", dir=1) tmp.ensure("__init__.py") tmp.join("conftest.py").write(py.code.Source(""" - class ConftestPlugin: - def pytest_addoption(self, parser): - group = parser.addgroup("testing group") - group.addoption('-G', '--glong', action="store", default=42, - type="int", dest="gdest", help="g value.") + def pytest_addoption(parser): + group = parser.addgroup("testing group") + group.addoption('-G', '--glong', action="store", default=42, + type="int", dest="gdest", help="g value.") """)) config = testdir.parseconfig(tmp, "-G", "11") assert config.option.gdest == 11 Modified: py/trunk/py/test/testing/test_pluginmanager.py ============================================================================== --- py/trunk/py/test/testing/test_pluginmanager.py (original) +++ py/trunk/py/test/testing/test_pluginmanager.py Tue Jun 16 18:23:18 2009 @@ -1,6 +1,5 @@ import py, os -from py.__.test.pluginmanager import PluginManager, canonical_names -from py.__.test.pluginmanager import registerplugin, importplugin +from py.__.test.pluginmanager import PluginManager, canonical_importname, collectattr class TestBootstrapping: def test_consider_env_fails_to_import(self, monkeypatch): @@ -17,7 +16,7 @@ def test_consider_env_plugin_instantiation(self, testdir, monkeypatch): pluginmanager = PluginManager() testdir.syspathinsert() - testdir.makepyfile(pytest_xy123="class Xy123Plugin: pass") + testdir.makepyfile(pytest_xy123="#") monkeypatch.setitem(os.environ, 'PYTEST_PLUGINS', 'xy123') l1 = len(pluginmanager.getplugins()) pluginmanager.consider_env() @@ -29,7 +28,7 @@ assert l2 == l3 def test_pluginmanager_ENV_startup(self, testdir, monkeypatch): - x500 = testdir.makepyfile(pytest_x500="class X500Plugin: pass") + x500 = testdir.makepyfile(pytest_x500="#") p = testdir.makepyfile(""" import py def test_hello(): @@ -48,59 +47,49 @@ reset = testdir.syspathinsert() pluginname = "pytest_hello" - testdir.makepyfile(**{pluginname: """ - class HelloPlugin: - pass - """}) + 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.__class__.__name__ == 'HelloPlugin' + assert plugin1.__name__.endswith('pytest_hello') plugin2 = pluginmanager.getplugin("hello") assert plugin2 is plugin1 def test_consider_module(self, testdir): pluginmanager = PluginManager() testdir.syspathinsert() - testdir.makepyfile(pytest_plug1="class Plug1Plugin: pass") - testdir.makepyfile(pytest_plug2="class Plug2Plugin: pass") + testdir.makepyfile(pytest_plug1="#") + testdir.makepyfile(pytest_plug2="#") mod = py.std.new.module("temp") mod.pytest_plugins = ["pytest_plug1", "pytest_plug2"] pluginmanager.consider_module(mod) - assert pluginmanager.getplugin("plug1").__class__.__name__ == "Plug1Plugin" - assert pluginmanager.getplugin("plug2").__class__.__name__ == "Plug2Plugin" + 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.new.module("x") mod.pytest_plugins = "pytest_a" - aplugin = testdir.makepyfile(pytest_a="""class APlugin: pass""") + aplugin = testdir.makepyfile(pytest_a="#") pluginmanager = PluginManager() - sorter = testdir.geteventrecorder(pluginmanager) + reprec = testdir.getreportrecorder(pluginmanager) #syspath.prepend(aplugin.dirpath()) py.std.sys.path.insert(0, str(aplugin.dirpath())) pluginmanager.consider_module(mod) - call = sorter.getcall(pluginmanager.hook.pytest_plugin_registered.name) - assert call.plugin.__class__.__name__ == "APlugin" + 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 = sorter.getcalls("plugin_registered") + l = reprec.getcalls("pytest_plugin_registered") assert len(l) == 1 - def test_consider_conftest(self, testdir): + def test_consider_conftest_deprecated(self, testdir): pp = PluginManager() - mod = testdir.makepyfile("class ConftestPlugin: hello = 1").pyimport() - pp.consider_conftest(mod) - l = [x for x in pp.getplugins() if isinstance(x, mod.ConftestPlugin)] - assert len(l) == 1 - assert l[0].hello == 1 - - pp.consider_conftest(mod) - l = [x for x in pp.getplugins() if isinstance(x, mod.ConftestPlugin)] - assert len(l) == 1 + mod = testdir.makepyfile("class ConftestPlugin: pass").pyimport() + call = py.test.raises(ValueError, pp.consider_conftest, mod) def test_config_sets_conftesthandle_onimport(self, testdir): config = testdir.parseconfig([]) @@ -113,7 +102,8 @@ def test_registry(self): pp = PluginManager() - a1, a2 = object(), object() + class A: pass + a1, a2 = A(), A() pp.register(a1) assert pp.isregistered(a1) pp.register(a2) @@ -124,33 +114,43 @@ pp.unregister(a2) assert not pp.isregistered(a2) - def test_canonical_names(self): - for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': - impname, clsname = canonical_names(name) - assert impname == "pytest_xyz" - assert clsname == "XyzPlugin" + def test_register_imported_modules(self): + pp = PluginManager() + mod = py.std.new.module("x.y.pytest_hello") + pp.register(mod) + assert pp.isregistered(mod) + assert pp.getplugins() == [mod] + py.test.raises(AssertionError, "pp.register(mod)") + mod2 = py.std.new.module("pytest_hello") + #pp.register(mod2) # double registry + py.test.raises(AssertionError, "pp.register(mod)") + #assert not pp.isregistered(mod2) + assert pp.getplugins() == [mod] # does not actually modify plugins - def test_registerplugin(self): - l = [] - registerfunc = l.append - registerplugin(registerfunc, py.io, "TerminalWriter") - assert len(l) == 1 - assert isinstance(l[0], py.io.TerminalWriter) + def test_register_mismatch_method(self): + pp = PluginManager() + class hello: + def pytest_gurgel(self): + pass + py.test.raises(pp.Error, "pp.register(hello())") - def test_importplugin(self): - assert importplugin("py") == py - py.test.raises(ImportError, "importplugin('laksjd.qwe')") - mod = importplugin("pytest_terminal") - assert mod is py.__.test.plugin.pytest_terminal + def test_register_mismatch_arg(self): + pp = PluginManager() + class hello: + def pytest_configure(self, asd): + pass + excinfo = py.test.raises(pp.Error, "pp.register(hello())") + def test_canonical_importname(self): + for name in 'xyz', 'pytest_xyz', 'pytest_Xyz', 'Xyz': + impname = canonical_importname(name) class TestPytestPluginInteractions: def test_do_option_conftestplugin(self, testdir): from py.__.test.config import Config p = testdir.makepyfile(""" - class ConftestPlugin: - def pytest_addoption(self, parser): - parser.addoption('--test123', action="store_true") + def pytest_addoption(parser): + parser.addoption('--test123', action="store_true") """) config = Config() config._conftest.importconftest(p) @@ -165,10 +165,9 @@ config.pluginmanager.do_configure(config=config) assert not hasattr(config.option, 'test123') p = testdir.makepyfile(""" - class ConftestPlugin: - def pytest_addoption(self, parser): - parser.addoption('--test123', action="store_true", - default=True) + def pytest_addoption(parser): + parser.addoption('--test123', action="store_true", + default=True) """) config._conftest.importconftest(p) assert config.option.test123 @@ -198,35 +197,6 @@ # lower level API - def test_getfirst(self): - pluginmanager = PluginManager() - class My1: - x = 1 - assert pluginmanager.getfirst("x") is None - pluginmanager.register(My1()) - assert pluginmanager.getfirst("x") == 1 - - - def test_call_firstresult(self): - pluginmanager = PluginManager() - class My1: - def method(self): - pass - class My2: - def method(self): - return True - class My3: - def method(self): - return None - assert pluginmanager.call_firstresult("method") is None - assert pluginmanager.call_firstresult("methodnotexists") is None - pluginmanager.register(My1()) - assert pluginmanager.call_firstresult("method") is None - pluginmanager.register(My2()) - assert pluginmanager.call_firstresult("method") == True - pluginmanager.register(My3()) - assert pluginmanager.call_firstresult("method") == True - def test_listattr(self): pluginmanager = PluginManager() class My2: @@ -259,3 +229,14 @@ results = call.execute() assert results == [1,2,2] +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'] Modified: py/trunk/py/test/testing/test_pycollect.py ============================================================================== --- py/trunk/py/test/testing/test_pycollect.py (original) +++ py/trunk/py/test/testing/test_pycollect.py Tue Jun 16 18:23:18 2009 @@ -6,7 +6,8 @@ def test_module_file_not_found(self, testdir): tmpdir = testdir.tmpdir fn = tmpdir.join('nada','no') - col = py.test.collect.Module(fn, config=testdir.parseconfig(tmpdir)) + col = py.test.collect.Module(fn) + col.config = testdir.parseconfig(tmpdir) py.test.raises(py.error.ENOENT, col.collect) def test_failing_import(self, testdir): @@ -179,8 +180,8 @@ yield list_append, i yield assert_order_of_execution """) - sorter = testdir.inline_run(o) - passed, skipped, failed = sorter.countoutcomes() + reprec = testdir.inline_run(o) + passed, skipped, failed = reprec.countoutcomes() assert passed == 7 assert not skipped and not failed @@ -209,27 +210,27 @@ yield list_append_2 yield assert_order_of_execution """) - sorter = testdir.inline_run(o) - passed, skipped, failed = sorter.countoutcomes() + 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.getmodulecollector() + modcol = item.getparent(py.test.collect.Module) assert isinstance(modcol, py.test.collect.Module) assert hasattr(modcol.obj, 'test_func') def test_function_equality(self, tmpdir): config = py.test.config._reparse([tmpdir]) - f1 = py.test.collect.Function(name="name", config=config, + f1 = py.test.collect.Function(name="name", args=(1,), callobj=isinstance) - f2 = py.test.collect.Function(name="name", config=config, + f2 = py.test.collect.Function(name="name", args=(1,), callobj=callable) assert not f1 == f2 assert f1 != f2 - f3 = py.test.collect.Function(name="name", config=config, + f3 = py.test.collect.Function(name="name", args=(1,2), callobj=callable) assert not f3 == f2 assert f3 != f2 @@ -237,13 +238,43 @@ assert not f3 == f1 assert f3 != f1 - f1_b = py.test.collect.Function(name="name", config=config, + f1_b = py.test.collect.Function(name="name", args=(1,), callobj=isinstance) assert f1 == f1_b assert not f1 != f1_b + def test_function_equality_with_callspec(self, tmpdir): + config = py.test.config._reparse([tmpdir]) + class callspec1: + param = 1 + funcargs = {} + id = "hello" + class callspec2: + param = 1 + funcargs = {} + id = "world" + f5 = py.test.collect.Function(name="name", + callspec=callspec1, callobj=isinstance) + f5b = py.test.collect.Function(name="name", + callspec=callspec2, callobj=isinstance) + 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_and_cmp_basic(self, testdir): + def test_check_equality(self, testdir): modcol = testdir.getmodulecol(""" def test_pass(): pass def test_fail(): assert 0 @@ -262,11 +293,7 @@ assert isinstance(fn3, py.test.collect.Function) assert not (fn1 == fn3) assert fn1 != fn3 - assert cmp(fn1, fn3) == -1 - assert cmp(fn1, 10) == -1 - assert cmp(fn2, 10) == -1 - assert cmp(fn3, 10) == -1 for fn in fn1,fn2,fn3: assert fn != 3 assert fn != modcol @@ -282,18 +309,18 @@ return g - def test_a(y): - pass - test_a = dec(test_a) - 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 - f1, f2 = colitems - assert cmp(f2, f1) > 0 + assert [item.name for item in colitems] == ['test_b', 'test_a'] + class TestConftestCustomization: def test_extra_python_files_and_functions(self, testdir): @@ -346,28 +373,28 @@ assert colitems[0].name == "check_method" -class TestMetaInfo: +class TestReportinfo: - def test_func_metainfo(self, testdir): + def test_func_reportinfo(self, testdir): item = testdir.getitem("def test_func(): pass") - fspath, lineno, modpath = item.metainfo() + fspath, lineno, modpath = item.reportinfo() assert fspath == item.fspath assert lineno == 0 assert modpath == "test_func" - def test_class_metainfo(self, testdir): + def test_class_reportinfo(self, testdir): modcol = testdir.getmodulecol(""" # lineno 0 class TestClass: def test_hello(self): pass """) classcol = modcol.collect_by_name("TestClass") - fspath, lineno, msg = classcol.metainfo() + fspath, lineno, msg = classcol.reportinfo() assert fspath == modcol.fspath assert lineno == 1 assert msg == "TestClass" - def test_generator_metainfo(self, testdir): + def test_generator_reportinfo(self, testdir): modcol = testdir.getmodulecol(""" # lineno 0 def test_gen(): @@ -376,13 +403,13 @@ yield check, 3 """) gencol = modcol.collect_by_name("test_gen") - fspath, lineno, modpath = gencol.metainfo() + fspath, lineno, modpath = gencol.reportinfo() assert fspath == modcol.fspath assert lineno == 1 assert modpath == "test_gen" genitem = gencol.collect()[0] - fspath, lineno, modpath = genitem.metainfo() + fspath, lineno, modpath = genitem.reportinfo() assert fspath == modcol.fspath assert lineno == 2 assert modpath == "test_gen[0]" Deleted: /py/trunk/py/test/testing/test_runner.py ============================================================================== --- /py/trunk/py/test/testing/test_runner.py Tue Jun 16 18:23:18 2009 +++ (empty file) @@ -1,92 +0,0 @@ - -from py.__.test.config import SetupState - -class TestSetupState: - disabled = True - def test_setup_ok(self, testdir): - item = testdir.getitem(""" - def setup_module(mod): - pass - def test_func(): - pass - """) - evrec = testdir.geteventrecorder(item.config) - setup = SetupState() - res = setup.do_setup(item) - assert res - - def test_setup_fails(self, testdir): - item = testdir.getitem(""" - def setup_module(mod): - print "world" - raise ValueError(42) - def test_func(): - pass - """) - evrec = testdir.geteventrecorder(item.config) - setup = SetupState() - res = setup.do_setup(item) - assert not res - rep = evrec.popcall(pytest_itemsetupreport).rep - assert rep.failed - assert not rep.skipped - assert rep.excrepr - assert "42" in str(rep.excrepr) - assert rep.outerr[0].find("world") != -1 - - def test_teardown_fails(self, testdir): - item = testdir.getitem(""" - def test_func(): - pass - def teardown_function(func): - print "13" - raise ValueError(25) - """) - evrec = testdir.geteventrecorder(item.config) - setup = SetupState() - res = setup.do_setup(item) - assert res - rep = evrec.popcall(pytest_itemsetupreport).rep - assert rep.passed - setup.do_teardown(item) - rep = evrec.popcall(pytest_itemsetupreport).rep - assert rep.item == item - assert rep.failed - assert not rep.passed - assert "13" in rep.outerr[0] - assert "25" in str(rep.excrepr) - - def test_setupitem_skips(self, testdir): - item = testdir.getitem(""" - import py - def setup_module(mod): - py.test.skip("17") - def test_func(): - pass - """) - evrec = testdir.geteventrecorder(item.config) - setup = SetupState() - setup.do_setup(item) - rep = evrec.popcall(pytest_itemsetupreport).rep - assert not rep.failed - assert rep.skipped - assert rep.excrepr - assert "17" in str(rep.excrepr) - - def test_runtest_ok(self, testdir): - item = testdir.getitem("def test_func(): pass") - evrec = testdir.geteventrecorder(item.config) - setup = SetupState() - setup.do_fixture_and_runtest(item) - rep = evrec.popcall(pytest_itemtestreport).rep - assert rep.passed - - def test_runtest_fails(self, testdir): - item = testdir.getitem("def test_func(): assert 0") - evrec = testdir.geteventrecorder(item.config) - setup = SetupState() - setup.do_fixture_and_runtest(item) - event = evrec.popcall(pytest_item_runtest_finished) - assert event.excinfo - - Deleted: /py/trunk/py/test/testing/test_runner_functional.py ============================================================================== --- /py/trunk/py/test/testing/test_runner_functional.py Tue Jun 16 18:23:18 2009 +++ (empty file) @@ -1,263 +0,0 @@ -import py -from py.__.test.runner import basic_run_report, forked_run_report, basic_collect_report -from py.__.code.excinfo import ReprExceptionInfo - -class BaseTests: - def test_funcattr(self, testdir): - ev = testdir.runitem(""" - import py - @py.test.mark(xfail="needs refactoring") - def test_func(): - raise Exit() - """) - assert ev.keywords['xfail'] == "needs refactoring" - - def test_passfunction(self, testdir): - ev = testdir.runitem(""" - def test_func(): - pass - """) - assert ev.passed - assert not ev.failed - assert ev.shortrepr == "." - assert not hasattr(ev, 'longrepr') - - def test_failfunction(self, testdir): - ev = testdir.runitem(""" - def test_func(): - assert 0 - """) - assert not ev.passed - assert not ev.skipped - assert ev.failed - assert ev.when == "execute" - assert isinstance(ev.longrepr, ReprExceptionInfo) - assert str(ev.shortrepr) == "F" - - def test_skipfunction(self, testdir): - ev = testdir.runitem(""" - import py - def test_func(): - py.test.skip("hello") - """) - assert not ev.failed - assert not ev.passed - assert ev.skipped - #assert ev.skipped.when == "execute" - #assert ev.skipped.when == "execute" - #assert ev.skipped == "%sreason == "hello" - #assert ev.skipped.location.lineno == 3 - #assert ev.skipped.location.path - #assert not ev.skipped.failurerepr - - def test_skip_in_setup_function(self, testdir): - ev = testdir.runitem(""" - import py - def setup_function(func): - py.test.skip("hello") - def test_func(): - pass - """) - print ev - assert not ev.failed - assert not ev.passed - assert ev.skipped - #assert ev.skipped.reason == "hello" - #assert ev.skipped.location.lineno == 3 - #assert ev.skipped.location.lineno == 3 - - def test_failure_in_setup_function(self, testdir): - ev = testdir.runitem(""" - import py - def setup_function(func): - raise ValueError(42) - def test_func(): - pass - """) - print ev - assert not ev.skipped - assert not ev.passed - assert ev.failed - assert ev.when == "setup" - - def test_failure_in_teardown_function(self, testdir): - ev = testdir.runitem(""" - import py - def teardown_function(func): - raise ValueError(42) - def test_func(): - pass - """) - print ev - assert not ev.skipped - assert not ev.passed - assert ev.failed - assert ev.when == "teardown" - assert ev.longrepr.reprcrash.lineno == 3 - assert ev.longrepr.reprtraceback.reprentries - - def test_custom_failure_repr(self, testdir): - testdir.makepyfile(conftest=""" - import py - class Function(py.test.collect.Function): - def repr_failure(self, excinfo, outerr): - return "hello" - """) - ev = testdir.runitem(""" - import py - def test_func(): - assert 0 - """) - assert not ev.skipped - assert not ev.passed - assert ev.failed - #assert ev.outcome.when == "execute" - #assert ev.failed.where.lineno == 3 - #assert ev.failed.where.path.basename == "test_func.py" - #assert ev.failed.failurerepr == "hello" - - def test_failure_in_setup_function_ignores_custom_failure_repr(self, testdir): - testdir.makepyfile(conftest=""" - import py - class Function(py.test.collect.Function): - def repr_failure(self, excinfo): - assert 0 - """) - ev = testdir.runitem(""" - import py - def setup_function(func): - raise ValueError(42) - def test_func(): - pass - """) - print ev - assert not ev.skipped - assert not ev.passed - assert ev.failed - #assert ev.outcome.when == "setup" - #assert ev.outcome.where.lineno == 3 - #assert ev.outcome.where.path.basename == "test_func.py" - #assert instanace(ev.failed.failurerepr, PythonFailureRepr) - - def test_capture_in_func(self, testdir): - ev = testdir.runitem(""" - import py - def setup_function(func): - print >>py.std.sys.stderr, "in setup" - def test_func(): - print "in function" - assert 0 - def teardown_func(func): - print "in teardown" - """) - assert ev.failed - # out, err = ev.failed.outerr - # assert out == ['in function\nin teardown\n'] - # assert err == ['in setup\n'] - - def test_systemexit_does_not_bail_out(self, testdir): - try: - ev = testdir.runitem(""" - def test_func(): - raise SystemExit(42) - """) - except SystemExit: - py.test.fail("runner did not catch SystemExit") - assert ev.failed - assert ev.when == "execute" - - def test_exit_propagates(self, testdir): - from py.__.test.outcome import Exit - try: - testdir.runitem(""" - from py.__.test.outcome import Exit - def test_func(): - raise Exit() - """) - except Exit: - pass - else: - py.test.fail("did not raise") - - -class TestExecutionNonForked(BaseTests): - def getrunner(self): - return basic_run_report - - def test_keyboardinterrupt_propagates(self, testdir): - from py.__.test.outcome import Exit - try: - testdir.runitem(""" - def test_func(): - raise KeyboardInterrupt("fake") - """) - except KeyboardInterrupt, e: - pass - else: - py.test.fail("did not raise") - - def test_pdb_on_fail(self, testdir): - l = [] - ev = testdir.runitem(""" - def test_func(): - assert 0 - """, pdb=l.append) - assert ev.failed - assert ev.when == "execute" - assert len(l) == 1 - - def test_pdb_on_skip(self, testdir): - l = [] - ev = testdir.runitem(""" - import py - def test_func(): - py.test.skip("hello") - """, pdb=l.append) - assert len(l) == 0 - assert ev.skipped - -class TestExecutionForked(BaseTests): - def getrunner(self): - if not hasattr(py.std.os, 'fork'): - py.test.skip("no os.fork available") - return forked_run_report - - def test_suicide(self, testdir): - ev = testdir.runitem(""" - def test_func(): - import os - os.kill(os.getpid(), 15) - """) - assert ev.failed - assert ev.when == "???" - -class TestCollectionEvent: - def test_collect_result(self, testdir): - col = testdir.getmodulecol(""" - def test_func1(): - pass - class TestClass: - pass - """) - ev = basic_collect_report(col) - assert not ev.failed - assert not ev.skipped - assert ev.passed - res = ev.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 py - py.test.skip("hello") - def test_func(): - pass - """) - ev = basic_collect_report(col) - assert not ev.failed - assert not ev.passed - assert ev.skipped - - Modified: py/trunk/py/test/testing/test_session.py ============================================================================== --- py/trunk/py/test/testing/test_session.py (original) +++ py/trunk/py/test/testing/test_session.py Tue Jun 16 18:23:18 2009 @@ -17,17 +17,17 @@ def test_two(someargs): pass """) - sorter = testdir.inline_run(tfile) - passed, skipped, failed = sorter.listoutcomes() + reprec = testdir.inline_run(tfile) + passed, skipped, failed = reprec.listoutcomes() assert len(skipped) == 0 assert len(passed) == 1 assert len(failed) == 3 - assert failed[0].colitem.name == "test_one_one" - assert failed[1].colitem.name == "test_other" - assert failed[2].colitem.name == "test_two" - itemstarted = sorter.getcalls("itemstart") + assert failed[0].item.name == "test_one_one" + assert failed[1].item.name == "test_other" + assert failed[2].item.name == "test_two" + itemstarted = reprec.getcalls("pytest_itemstart") assert len(itemstarted) == 4 - colstarted = sorter.getcalls("collectstart") + colstarted = reprec.getcalls("pytest_collectstart") assert len(colstarted) == 1 col = colstarted[0].collector assert isinstance(col, py.test.collect.Module) @@ -41,19 +41,19 @@ import does_not_work a = 1 """) - sorter = testdir.inline_run(tfile) - l = sorter.getfailedcollections() + 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): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" import py def test_raises_doesnt(): py.test.raises(ValueError, int, "3") """) - passed, skipped, failed = sorter.listoutcomes() + passed, skipped, failed = reprec.listoutcomes() assert len(failed) == 1 out = failed[0].longrepr.reprcrash.message if not out.find("DID NOT RAISE") != -1: @@ -61,28 +61,28 @@ py.test.fail("incorrect raises() output") def test_generator_yields_None(self, testdir): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" def test_1(): yield None """) - failures = sorter.getfailedcollections() + failures = reprec.getfailedcollections() out = failures[0].longrepr.reprcrash.message i = out.find('TypeError') assert i != -1 def test_syntax_error_module(self, testdir): - sorter = testdir.inline_runsource("this is really not python") - l = sorter.getfailedcollections() + reprec = testdir.inline_runsource("this is really not python") + l = reprec.getfailedcollections() assert len(l) == 1 out = l[0].longrepr.reprcrash.message assert out.find(str('not python')) != -1 def test_exit_first_problem(self, testdir): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" def test_one(): assert 0 def test_two(): assert 0 """, '--exitfirst') - passed, skipped, failed = sorter.countoutcomes() + passed, skipped, failed = reprec.countoutcomes() assert failed == 1 assert passed == skipped == 0 @@ -111,8 +111,8 @@ t = BrokenRepr2() assert t.foo == 1 """) - sorter = testdir.inline_run(p) - passed, skipped, failed = sorter.listoutcomes() + reprec = testdir.inline_run(p) + passed, skipped, failed = reprec.listoutcomes() assert len(failed) == 2 out = failed[0].longrepr.reprcrash.message assert out.find("""[Exception("Ha Ha fooled you, I'm a broken repr().") raised in repr()]""") != -1 #' @@ -129,31 +129,15 @@ """, test_file=""" def test_one(): pass """) - sorter = testdir.inline_run(testdir.tmpdir) - reports = sorter.getreports("collectreport") + reprec = testdir.inline_run(testdir.tmpdir) + reports = reprec.getreports("pytest_collectreport") assert len(reports) == 1 assert reports[0].skipped class TestNewSession(SessionTests): - def test_pdb_run(self, testdir, monkeypatch): - import py.__.test.custompdb - tfile = testdir.makepyfile(""" - def test_usepdb(): - assert 0 - """) - l = [] - def mypdb(*args): - l.append(args) - monkeypatch.setattr(py.__.test.custompdb, 'post_mortem', mypdb) - sorter = testdir.inline_run('--pdb', tfile) - rep = sorter.matchreport("test_usepdb") - assert rep.failed - assert len(l) == 1 - tb = py.code.Traceback(l[0][0]) - assert tb[-1].name == "test_usepdb" def test_order_of_execution(self, testdir): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" l = [] def test_1(): l.append(1) @@ -172,7 +156,7 @@ def test_4(self): assert self.reslist == [1,2,1,2,3] """) - passed, skipped, failed = sorter.countoutcomes() + passed, skipped, failed = reprec.countoutcomes() assert failed == skipped == 0 assert passed == 7 # also test listnames() here ... @@ -197,13 +181,13 @@ test_three="xxxdsadsadsadsa", __init__="" ) - sorter = testdir.inline_run('--collectonly', p.dirpath()) + reprec = testdir.inline_run('--collectonly', p.dirpath()) - itemstarted = sorter.getcalls("pytest_itemstart") + itemstarted = reprec.getcalls("pytest_itemstart") assert len(itemstarted) == 3 - assert not sorter.getreports("itemtestreport") - started = sorter.getcalls("pytest_collectstart") - finished = sorter.getreports("collectreport") + 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 colfail = [x for x in finished if x.failed] @@ -214,8 +198,8 @@ def test_minus_x_import_error(self, testdir): testdir.makepyfile(__init__="") testdir.makepyfile(test_one="xxxx", test_two="yyyy") - sorter = testdir.inline_run("-x", testdir.tmpdir) - finished = sorter.getreports("collectreport") + 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 Modified: py/trunk/py/test/testing/test_setup_functional.py ============================================================================== --- py/trunk/py/test/testing/test_setup_functional.py (original) +++ py/trunk/py/test/testing/test_setup_functional.py Tue Jun 16 18:23:18 2009 @@ -3,7 +3,7 @@ # module, class, and instance level def test_module_and_function_setup(testdir): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" modlevel = [] def setup_module(module): assert not modlevel @@ -27,13 +27,13 @@ assert modlevel[0] == 42 assert not hasattr(test_modlevel, 'answer') """) - rep = sorter.matchreport("test_modlevel") + rep = reprec.matchreport("test_modlevel") assert rep.passed - rep = sorter.matchreport("test_module") + rep = reprec.matchreport("test_module") assert rep.passed def test_class_setup(testdir): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" class TestSimpleClassSetup: clslevel = [] def setup_class(cls): @@ -53,10 +53,10 @@ assert not TestSimpleClassSetup.clslevel assert not TestInheritedClassSetupStillWorks.clslevel """) - sorter.assertoutcome(passed=1+2+1) + reprec.assertoutcome(passed=1+2+1) def test_method_setup(testdir): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" class TestSetupMethod: def setup_method(self, meth): self.methsetup = meth @@ -69,10 +69,10 @@ def test_other(self): assert self.methsetup == self.test_other """) - sorter.assertoutcome(passed=2) + reprec.assertoutcome(passed=2) def test_method_generator_setup(testdir): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" class TestSetupTeardownOnInstance: def setup_class(cls): cls.classsetup = True @@ -91,10 +91,10 @@ assert self.methsetup == self.test_generate assert value == 5 """) - sorter.assertoutcome(passed=1, failed=1) + reprec.assertoutcome(passed=1, failed=1) def test_func_generator_setup(testdir): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" import sys def setup_module(mod): @@ -118,11 +118,11 @@ yield check assert x == [1] """) - rep = sorter.matchreport("test_one", names="itemtestreport") + rep = reprec.matchreport("test_one", names="pytest_runtest_logreport") assert rep.passed def test_method_setup_uses_fresh_instances(testdir): - sorter = testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" class TestSelfState1: def __init__(self): self.hello = 42 @@ -137,5 +137,5 @@ def test_world(self): assert not hasattr(self, 'world') """) - sorter.assertoutcome(passed=4, failed=0) + reprec.assertoutcome(passed=4, failed=0) Modified: py/trunk/py/test/testing/test_traceback.py ============================================================================== --- py/trunk/py/test/testing/test_traceback.py (original) +++ py/trunk/py/test/testing/test_traceback.py Tue Jun 16 18:23:18 2009 @@ -9,21 +9,20 @@ def test_traceback_argsetup(self, testdir): testdir.makeconftest(""" - class ConftestPlugin: - def pytest_funcarg__hello(self, request): - raise ValueError("xyz") + def pytest_funcarg__hello(request): + raise ValueError("xyz") """) p = testdir.makepyfile("def test(hello): pass") result = testdir.runpytest(p) assert result.ret != 0 out = result.stdout.str() assert out.find("xyz") != -1 - assert out.find("conftest.py:3: ValueError") != -1 + assert out.find("conftest.py:2: ValueError") != -1 numentries = out.count("_ _ _") # separator for traceback entries assert numentries == 0 result = testdir.runpytest("--fulltrace", p) out = result.stdout.str() - assert out.find("conftest.py:3: ValueError") != -1 + assert out.find("conftest.py:2: ValueError") != -1 numentries = out.count("_ _ _ _") # separator for traceback entries assert numentries >3 Modified: py/trunk/py/thread/testing/test_io.py ============================================================================== --- py/trunk/py/thread/testing/test_io.py (original) +++ py/trunk/py/thread/testing/test_io.py Tue Jun 16 18:23:18 2009 @@ -12,55 +12,61 @@ assert old == sys.stdout class TestThreadOut: - def setup_method(self, method): - self.out = ThreadOut(sys, 'stdout') - def teardown_method(self, method): - self.out.deinstall() - def test_threadout_one(self): - l = [] - self.out.setwritefunc(l.append) - print 42,13, - x = l.pop(0) - assert x == '42' - x = l.pop(0) - assert x == ' ' - x = l.pop(0) - assert x == '13' - + out = ThreadOut(sys, 'stdout') + try: + l = [] + out.setwritefunc(l.append) + print 42,13, + x = l.pop(0) + assert x == '42' + x = l.pop(0) + assert x == ' ' + x = l.pop(0) + assert x == '13' + finally: + out.deinstall() def test_threadout_multi_and_default(self): - num = 3 - defaults = [] - def f(l): - self.out.setwritefunc(l.append) - print id(l), - self.out.delwritefunc() - print 1 - self.out.setdefaultwriter(defaults.append) - pool = WorkerPool() - listlist = [] - for x in range(num): - l = [] - listlist.append(l) - pool.dispatch(f, l) - pool.shutdown() - for name, value in self.out.__dict__.items(): - print >>sys.stderr, "%s: %s" %(name, value) - pool.join(2.0) - for i in range(num): - item = listlist[i] - assert item ==[str(id(item))] - assert not self.out._tid2out - assert defaults - expect = ['1' for x in range(num)] - defaults = [x for x in defaults if x.strip()] - assert defaults == expect + out = ThreadOut(sys, 'stdout') + try: + num = 3 + defaults = [] + def f(l): + out.setwritefunc(l.append) + print id(l), + out.delwritefunc() + print 1 + out.setdefaultwriter(defaults.append) + pool = WorkerPool() + listlist = [] + for x in range(num): + l = [] + listlist.append(l) + pool.dispatch(f, l) + pool.shutdown() + for name, value in out.__dict__.items(): + print >>sys.stderr, "%s: %s" %(name, value) + pool.join(2.0) + for i in range(num): + item = listlist[i] + assert item ==[str(id(item))] + assert not out._tid2out + assert defaults + expect = ['1' for x in range(num)] + defaults = [x for x in defaults if x.strip()] + assert defaults == expect + finally: + out.deinstall() def test_threadout_nested(self): - # we want ThreadOuts to coexist - last = sys.stdout - out = ThreadOut(sys, 'stdout') - assert last == sys.stdout - out.deinstall() - assert last == sys.stdout + out1 = ThreadOut(sys, 'stdout') + try: + # we want ThreadOuts to coexist + last = sys.stdout + out = ThreadOut(sys, 'stdout') + assert last == sys.stdout + out.deinstall() + assert last == sys.stdout + finally: + out1.deinstall() Modified: py/trunk/setup.py ============================================================================== --- py/trunk/setup.py (original) +++ py/trunk/setup.py Tue Jun 16 18:23:18 2009 @@ -1,27 +1,25 @@ """ setup file for 'py' package based on: - https://codespeak.net/svn/py/trunk, revision=64088 + https://codespeak.net/svn/py/trunk, revision=65224 autogenerated by gensetup.py """ import os, sys -from distutils.core import setup, Extension +from distutils.core import setup long_description = """ -The py lib is an extensible library for testing, distributed processing and -interacting with filesystems. +advanced testing and development support library: - `py.test`_: cross-project testing tool with many advanced features - `py.execnet`_: ad-hoc code distribution to SSH, Socket and local sub processes - `py.path`_: path abstractions over local and subversion files - `py.code`_: dynamic code compile and traceback printing support -The py lib and its tools should work well on Linux, Win32, -OSX, Python versions 2.3-2.6. For questions please go to -http://pylib.org/contact.html +Compatibility: Linux, Win32, OSX, Python versions 2.3-2.6. +For questions please check out http://pylib.org/contact.html .. _`py.test`: http://pylib.org/test.html .. _`py.execnet`: http://pylib.org/execnet.html @@ -33,11 +31,10 @@ def main(): setup( name='py', - description='pylib and py.test: agile development and test support library', + description='py.test and pylib: advanced testing tool and networking lib', long_description = long_description, version='1.0.0b2', url='http://pylib.org', - download_url='http://codespeak.net/py/1.0.0b2/download.html', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], author='holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others', @@ -148,6 +145,45 @@ 'rest/testing/data/tocdepth.rst2pdfconfig']}, ) +def getscripts(): + if sys.platform == "win32": + base = "py/bin/win32/" + ext = ".cmd" + else: + base = "py/bin/" + ext = "" + l = [] + for name in ['py.countloc', 'py.svnwcrevert', 'py.rest', 'py.test', 'py.cleanup', 'py.lookup', 'py.which']: + l.append(base + name + ext) + return l + +# scripts for windows: turn "py.SCRIPT" into "py_SCRIPT" and create +# "py.SCRIPT.cmd" files invoking "py_SCRIPT" +from distutils.command.install_scripts import install_scripts +class my_install_scripts(install_scripts): + def run(self): + install_scripts.run(self) + #print self.outfiles + for fn in self.outfiles: + basename = os.path.basename(fn) + if basename.startswith("py.") and not basename.endswith(".cmd"): + newbasename = basename.replace(".", "_") + newfn = os.path.join(os.path.dirname(fn), newbasename) + if os.path.exists(newfn): + os.remove(newfn) + os.rename(fn, newfn) + fncmd = fn + ".cmd" + if os.path.exists(fncmd): + os.remove(fncmd) + f = open(fncmd, 'w') + f.write("@echo off\n") + f.write('python "%%~dp0\%s" %%*' %(newbasename)) + f.close() +if sys.platform == "win32": + cmdclass = {'install_scripts': my_install_scripts} +else: + cmdclass = {} + if __name__ == '__main__': main() \ No newline at end of file From py-svn at codespeak.net Thu Jun 18 06:51:50 2009 From: py-svn at codespeak.net (py-svn at codespeak.net) Date: Thu, 18 Jun 2009 06:51:50 +0200 (CEST) Subject: [py-svn] Last time... Message-ID: <20090618045150.65A19168569@codespeak.net> An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Thu Jun 18 10:41:56 2009 From: py-svn at codespeak.net (Frieda Jpuu) Date: Thu, 18 Jun 2009 10:41:56 +0200 Subject: [py-svn] Guess who! Take my ICQ Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Thu Jun 18 12:40:38 2009 From: py-svn at codespeak.net (Caravello Polly) Date: Thu, 18 Jun 2009 12:40:38 +0200 (CEST) Subject: [py-svn] Photos of the place Message-ID: An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Thu Jun 18 19:04:02 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 18 Jun 2009 17:04:02 -0000 Subject: [py-svn] commit/py-trunk: 4 new changesets Message-ID: <20090618170402.13696.37844@domU-12-31-39-00-D4-C1.compute-1.internal> 4 new changesets in py-trunk: http://www.bitbucket.org/hpk42/py-trunk/changeset/b689e82cf777/ changeset: r1160:b689e82cf777 user: hpk date: 2009-06-18 19:02:59 summary: fixing docs, adding newest ez_setup.py, fixing a few win32 failures, fixing iocapture plugin affected #: 6 files (627 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/2d162885c157/ changeset: r1157:2d162885c157 user: hpk date: 2009-06-17 21:08:19 summary: bah - i guess i rather keep using setuptools for 1.0 because of the issue of installing non-setuptools mediated windows scripts on previously installed with setuptools ones affected #: 4 files (10.7 KB) http://www.bitbucket.org/hpk42/py-trunk/changeset/4f159498f15f/ changeset: r1158:4f159498f15f user: hpk date: 2009-06-18 17:19:12 summary: * introduce and document new pytest_namespace hook * remove py.test.mark helper * move xfail to work directly on py.test namespace, simplified affected #: 16 files (5.3 KB) http://www.bitbucket.org/hpk42/py-trunk/changeset/d3fb68de9b56/ changeset: r1159:d3fb68de9b56 user: hpk date: 2009-06-18 17:26:40 summary: shift deprecated_call helper into pytest_recwarn plugin affected #: 3 files (4.5 KB) Repository URL: http://bitbucket.org/hpk42/py-trunk/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From hpk at codespeak.net Thu Jun 18 20:10:00 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Thu, 18 Jun 2009 20:10:00 +0200 (CEST) Subject: [py-svn] r65819 - in py/trunk: . doc doc/test py py/bin py/execnet/testing py/misc/testing py/test py/test/dist/testing py/test/plugin py/test/testing Message-ID: <20090618181000.641B416855C@codespeak.net> Author: hpk Date: Thu Jun 18 20:09:57 2009 New Revision: 65819 Added: py/trunk/ez_setup.py Modified: py/trunk/CHANGELOG py/trunk/MANIFEST py/trunk/doc/download.txt py/trunk/doc/test/extend.txt py/trunk/py/__init__.py py/trunk/py/_com.py py/trunk/py/bin/gendoc.py py/trunk/py/execnet/testing/test_gateway.py py/trunk/py/misc/testing/test_api.py py/trunk/py/test/dist/testing/test_dsession.py py/trunk/py/test/dist/testing/test_nodemanage.py py/trunk/py/test/outcome.py py/trunk/py/test/plugin/hookspec.py py/trunk/py/test/plugin/pytest_execnetcleanup.py py/trunk/py/test/plugin/pytest_iocapture.py py/trunk/py/test/plugin/pytest_pytester.py py/trunk/py/test/plugin/pytest_recwarn.py py/trunk/py/test/plugin/pytest_xfail.py py/trunk/py/test/plugin/test_pytest_runner.py py/trunk/py/test/pluginmanager.py py/trunk/py/test/testing/test_config.py py/trunk/py/test/testing/test_funcargs.py py/trunk/py/test/testing/test_outcome.py py/trunk/py/test/testing/test_pluginmanager.py py/trunk/setup.py Log: merging hg py-trunk to svn/trunk Modified: py/trunk/CHANGELOG ============================================================================== --- py/trunk/CHANGELOG (original) +++ py/trunk/CHANGELOG Thu Jun 18 20:09:57 2009 @@ -7,10 +7,15 @@ hooks directly in conftest.py or global pytest_*.py files. -* documented and refined various hooks +* added new pytest_namespace(config) hook that allows + to inject helpers directly to the py.test.* namespace. -* added new style of generative tests via pytest_generate_tests - hook +* 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 ============================================= Modified: py/trunk/MANIFEST ============================================================================== --- py/trunk/MANIFEST (original) +++ py/trunk/MANIFEST Thu Jun 18 20:09:57 2009 @@ -5,6 +5,58 @@ README.txt TODO.txt _findpy.py +doc/announce/release-0.9.0.txt +doc/announce/release-0.9.2.txt +doc/announce/release-1.0.0.txt +doc/announce/releases.txt +doc/bin.txt +doc/code.txt +doc/confrest.py +doc/conftest.py +doc/contact.txt +doc/download.txt +doc/execnet.txt +doc/img/pylib.png +doc/index.txt +doc/io.txt +doc/log.txt +doc/misc.txt +doc/path.txt +doc/style.css +doc/test/attic.txt +doc/test/config.txt +doc/test/dist.txt +doc/test/examples.txt +doc/test/extend.txt +doc/test/features.txt +doc/test/funcargs.txt +doc/test/quickstart.txt +doc/test/test.txt +doc/test/xunit_setup.txt +doc/xml.txt +example/execnet/popen_read_multiple.py +example/funcarg/costlysetup/conftest.py +example/funcarg/costlysetup/sub1/test_quick.py +example/funcarg/costlysetup/sub2/test_two.py +example/funcarg/mysetup/__init__.py +example/funcarg/mysetup/conftest.py +example/funcarg/mysetup/myapp.py +example/funcarg/mysetup/test_sample.py +example/funcarg/mysetup2/__init__.py +example/funcarg/mysetup2/conftest.py +example/funcarg/mysetup2/myapp.py +example/funcarg/mysetup2/test_sample.py +example/funcarg/mysetup2/test_ssh.py +example/funcarg/parametrize/test_parametrize.py +example/funcarg/parametrize/test_parametrize2.py +example/funcarg/parametrize/test_parametrize3.py +example/funcarg/test_simpleprovider.py +example/genhtml.py +example/genhtmlcss.py +example/genxml.py +example/pytest/failure_demo.py +example/pytest/test_failures.py +example/pytest/test_setup_flow_example.py py/LICENSE py/__init__.py py/_com.py @@ -236,7 +288,6 @@ py/test/compat.py py/test/config.py py/test/conftesthandle.py -py/test/custompdb.py py/test/defaultconftest.py py/test/dist/__init__.py py/test/dist/dsession.py @@ -258,20 +309,17 @@ py/test/outcome.py py/test/parseopt.py py/test/plugin/__init__.py -py/test/plugin/api.py py/test/plugin/conftest.py py/test/plugin/hookspec.py py/test/plugin/pytest__pytest.py py/test/plugin/pytest_default.py py/test/plugin/pytest_doctest.py -py/test/plugin/pytest_eventlog.py py/test/plugin/pytest_execnetcleanup.py py/test/plugin/pytest_figleaf.py py/test/plugin/pytest_hooklog.py py/test/plugin/pytest_iocapture.py py/test/plugin/pytest_monkeypatch.py py/test/plugin/pytest_pdb.py -py/test/plugin/pytest_plugintester.py py/test/plugin/pytest_pocoo.py py/test/plugin/pytest_pylint.py py/test/plugin/pytest_pytester.py @@ -287,7 +335,6 @@ py/test/plugin/test_pytest_runner.py py/test/pluginmanager.py py/test/pycollect.py -py/test/runner.py py/test/session.py py/test/testing/__init__.py py/test/testing/acceptance_test.py @@ -297,7 +344,6 @@ py/test/testing/import_test/package/module_that_imports_shared_lib.py py/test/testing/import_test/package/shared_lib.py py/test/testing/import_test/package/test_import.py -py/test/testing/test_api.py py/test/testing/test_collect.py py/test/testing/test_compat.py py/test/testing/test_config.py @@ -311,8 +357,6 @@ py/test/testing/test_pluginmanager.py py/test/testing/test_pycollect.py py/test/testing/test_recording.py -py/test/testing/test_runner.py -py/test/testing/test_runner_functional.py py/test/testing/test_session.py py/test/testing/test_setup_functional.py py/test/testing/test_traceback.py Modified: py/trunk/doc/download.txt ============================================================================== --- py/trunk/doc/download.txt (original) +++ py/trunk/doc/download.txt Thu Jun 18 20:09:57 2009 @@ -5,7 +5,7 @@ "easy_install py" =================================================== -With a working `setuptools installation`_ you can install from the command line:: +If you have a working `setuptools installation`_ you can install from the command line:: easy_install -U py Modified: py/trunk/doc/test/extend.txt ============================================================================== --- py/trunk/doc/test/extend.txt (original) +++ py/trunk/doc/test/extend.txt Thu Jun 18 20:09:57 2009 @@ -8,11 +8,14 @@ hooks`_. Python modules which contain such hook functions are called plugins. Hook functions are discovered in ``conftest.py`` files or in **named** plugins. ``conftest.py`` files are sometimes called "anonymous" -or "local" plugins if they define hooks. Named plugins are python modules -or packages that have an all lowercase ``pytest_`` prefixed name and who -are imported during tool startup or the testing process. +or "local" plugins if they contain hooks. They allow to write and distribute +some extensions along with the test suite or the application package easily. +Named plugins are python modules or packages that have an all lowercase +``pytest_`` prefixed name and who are imported during tool startup or +the testing process. .. _`tool startup`: +.. _`test tool starts up`: Plugin discovery at tool startup -------------------------------------------- @@ -86,11 +89,58 @@ py.test calls hooks functions to implement its `test collection`_, running and reporting process. Upon loading of a plugin py.test performs strict checking on contained hook functions. Function and argument names -need to match exactly the `original definition of the hook`_. It thus +need to match exactly one of `hook definition specification`_. It thus provides useful error reporting on mistyped hook or argument names and minimizes version incompatibilites. -.. _`original definition of the hook`: http://bitbucket.org/hpk42/py-trunk/src/tip/py/test/plugin/hookspec.py +.. _`hook definition specification`: http://bitbucket.org/hpk42/py-trunk/src/tip/py/test/plugin/hookspec.py + +.. _`configuration hooks`: + +command line parsing and configuration hooks +-------------------------------------------------------------------- + +When the `test tool starts up`_ it will invoke all hooks that add +command line options in the python standard optparse style. + +.. sourcecode:: python + + def pytest_addoption(parser): + """ add command line options. """" + parser.addoption("--myopt", dest="myopt", action="store_true") + +After all these hooks have been called, the command line is parser +and a ``config`` object is created and another hook is invoked, +for example: + +.. sourcecode:: python + + def pytest_configure(config): + config.getvalue("myopt") + +When the test run finishes this corresponding finalizer hook is called: + + def pytest_unconfigure(config): + ... + + +adding global py.test helpers and functionality +-------------------------------------------------------------------- + +If you want to make global helper functions or objects available +to your test code you can implement: + + def pytest_namespace(config): + """ return dictionary with items to be made available on py.test. """ + +All such returned items will be made available directly on +the ``py.test`` namespace. + +If you want to provide helpers that are specific to a test function run or need +to be setup per test function run, please refer to the `funcargs mechanism`_. + +.. _`funcargs mechanism`: funcargs.html + generic "runtest" hooks ------------------------------ @@ -179,7 +229,7 @@ options via the ``parent.config`` object. -Python specific test function and module hooks +Python test function and module hooks ---------------------------------------------------- For influencing the collection of objects in Python modules @@ -187,12 +237,11 @@ .. sourcecode:: python - pytest_pycollect_makeitem(collector, name, obj) + def pytest_pycollect_makeitem(collector, name, obj): + """ return custom item/collector for a python object in a module, or None. """ This hook will be called for each Python object in a collected -Python module. The return value is a custom `collection node`_. - - +Python module. The return value is a custom `collection node`_ or None. .. XXX or ``False`` if you want to indicate that the given item should not be collected. Added: py/trunk/ez_setup.py ============================================================================== --- (empty file) +++ py/trunk/ez_setup.py Thu Jun 18 20:09:57 2009 @@ -0,0 +1,276 @@ +#!python +"""Bootstrap setuptools installation + +If you want to use setuptools in your package's setup.py, just include this +file in the same directory with it, and add this to the top of your setup.py:: + + from ez_setup import use_setuptools + use_setuptools() + +If you want to require a specific version of setuptools, set a download +mirror, or use an alternate download directory, you can do so by supplying +the appropriate options to ``use_setuptools()``. + +This file can also be run as a script to install or upgrade setuptools. +""" +import sys +DEFAULT_VERSION = "0.6c9" +DEFAULT_URL = "http://pypi.python.org/packages/%s/s/setuptools/" % sys.version[:3] + +md5_data = { + 'setuptools-0.6b1-py2.3.egg': '8822caf901250d848b996b7f25c6e6ca', + 'setuptools-0.6b1-py2.4.egg': 'b79a8a403e4502fbb85ee3f1941735cb', + 'setuptools-0.6b2-py2.3.egg': '5657759d8a6d8fc44070a9d07272d99b', + 'setuptools-0.6b2-py2.4.egg': '4996a8d169d2be661fa32a6e52e4f82a', + 'setuptools-0.6b3-py2.3.egg': 'bb31c0fc7399a63579975cad9f5a0618', + 'setuptools-0.6b3-py2.4.egg': '38a8c6b3d6ecd22247f179f7da669fac', + 'setuptools-0.6b4-py2.3.egg': '62045a24ed4e1ebc77fe039aa4e6f7e5', + 'setuptools-0.6b4-py2.4.egg': '4cb2a185d228dacffb2d17f103b3b1c4', + 'setuptools-0.6c1-py2.3.egg': 'b3f2b5539d65cb7f74ad79127f1a908c', + 'setuptools-0.6c1-py2.4.egg': 'b45adeda0667d2d2ffe14009364f2a4b', + 'setuptools-0.6c2-py2.3.egg': 'f0064bf6aa2b7d0f3ba0b43f20817c27', + 'setuptools-0.6c2-py2.4.egg': '616192eec35f47e8ea16cd6a122b7277', + 'setuptools-0.6c3-py2.3.egg': 'f181fa125dfe85a259c9cd6f1d7b78fa', + 'setuptools-0.6c3-py2.4.egg': 'e0ed74682c998bfb73bf803a50e7b71e', + 'setuptools-0.6c3-py2.5.egg': 'abef16fdd61955514841c7c6bd98965e', + 'setuptools-0.6c4-py2.3.egg': 'b0b9131acab32022bfac7f44c5d7971f', + 'setuptools-0.6c4-py2.4.egg': '2a1f9656d4fbf3c97bf946c0a124e6e2', + 'setuptools-0.6c4-py2.5.egg': '8f5a052e32cdb9c72bcf4b5526f28afc', + 'setuptools-0.6c5-py2.3.egg': 'ee9fd80965da04f2f3e6b3576e9d8167', + 'setuptools-0.6c5-py2.4.egg': 'afe2adf1c01701ee841761f5bcd8aa64', + 'setuptools-0.6c5-py2.5.egg': 'a8d3f61494ccaa8714dfed37bccd3d5d', + 'setuptools-0.6c6-py2.3.egg': '35686b78116a668847237b69d549ec20', + 'setuptools-0.6c6-py2.4.egg': '3c56af57be3225019260a644430065ab', + 'setuptools-0.6c6-py2.5.egg': 'b2f8a7520709a5b34f80946de5f02f53', + 'setuptools-0.6c7-py2.3.egg': '209fdf9adc3a615e5115b725658e13e2', + 'setuptools-0.6c7-py2.4.egg': '5a8f954807d46a0fb67cf1f26c55a82e', + 'setuptools-0.6c7-py2.5.egg': '45d2ad28f9750e7434111fde831e8372', + 'setuptools-0.6c8-py2.3.egg': '50759d29b349db8cfd807ba8303f1902', + 'setuptools-0.6c8-py2.4.egg': 'cba38d74f7d483c06e9daa6070cce6de', + 'setuptools-0.6c8-py2.5.egg': '1721747ee329dc150590a58b3e1ac95b', + 'setuptools-0.6c9-py2.3.egg': 'a83c4020414807b496e4cfbe08507c03', + 'setuptools-0.6c9-py2.4.egg': '260a2be2e5388d66bdaee06abec6342a', + 'setuptools-0.6c9-py2.5.egg': 'fe67c3e5a17b12c0e7c541b7ea43a8e6', + 'setuptools-0.6c9-py2.6.egg': 'ca37b1ff16fa2ede6e19383e7b59245a', +} + +import sys, os +try: from hashlib import md5 +except ImportError: from md5 import md5 + +def _validate_md5(egg_name, data): + if egg_name in md5_data: + digest = md5(data).hexdigest() + if digest != md5_data[egg_name]: + print >>sys.stderr, ( + "md5 validation of %s failed! (Possible download problem?)" + % egg_name + ) + sys.exit(2) + return data + +def use_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + download_delay=15 +): + """Automatically find/download setuptools and make it available on sys.path + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end with + a '/'). `to_dir` is the directory where setuptools will be downloaded, if + it is not already available. If `download_delay` is specified, it should + be the number of seconds that will be paused before initiating a download, + should one be required. If an older version of setuptools is installed, + this routine will print a message to ``sys.stderr`` and raise SystemExit in + an attempt to abort the calling script. + """ + was_imported = 'pkg_resources' in sys.modules or 'setuptools' in sys.modules + def do_download(): + egg = download_setuptools(version, download_base, to_dir, download_delay) + sys.path.insert(0, egg) + import setuptools; setuptools.bootstrap_install_from = egg + try: + import pkg_resources + except ImportError: + return do_download() + try: + pkg_resources.require("setuptools>="+version); return + except pkg_resources.VersionConflict, e: + if was_imported: + print >>sys.stderr, ( + "The required version of setuptools (>=%s) is not available, and\n" + "can't be installed while this script is running. Please install\n" + " a more recent version first, using 'easy_install -U setuptools'." + "\n\n(Currently using %r)" + ) % (version, e.args[0]) + sys.exit(2) + else: + del pkg_resources, sys.modules['pkg_resources'] # reload ok + return do_download() + except pkg_resources.DistributionNotFound: + return do_download() + +def download_setuptools( + version=DEFAULT_VERSION, download_base=DEFAULT_URL, to_dir=os.curdir, + delay = 15 +): + """Download setuptools from a specified location and return its filename + + `version` should be a valid setuptools version number that is available + as an egg for download under the `download_base` URL (which should end + with a '/'). `to_dir` is the directory where the egg will be downloaded. + `delay` is the number of seconds to pause before an actual download attempt. + """ + import urllib2, shutil + egg_name = "setuptools-%s-py%s.egg" % (version,sys.version[:3]) + url = download_base + egg_name + saveto = os.path.join(to_dir, egg_name) + src = dst = None + if not os.path.exists(saveto): # Avoid repeated downloads + try: + from distutils import log + if delay: + log.warn(""" +--------------------------------------------------------------------------- +This script requires setuptools version %s to run (even to display +help). I will attempt to download it for you (from +%s), but +you may need to enable firewall access for this script first. +I will start the download in %d seconds. + +(Note: if this machine does not have network access, please obtain the file + + %s + +and place it in this directory before rerunning this script.) +---------------------------------------------------------------------------""", + version, download_base, delay, url + ); from time import sleep; sleep(delay) + log.warn("Downloading %s", url) + src = urllib2.urlopen(url) + # Read/write all in one block, so we don't create a corrupt file + # if the download is interrupted. + data = _validate_md5(egg_name, src.read()) + dst = open(saveto,"wb"); dst.write(data) + finally: + if src: src.close() + if dst: dst.close() + return os.path.realpath(saveto) + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +def main(argv, version=DEFAULT_VERSION): + """Install or upgrade setuptools and EasyInstall""" + try: + import setuptools + except ImportError: + egg = None + try: + egg = download_setuptools(version, delay=0) + sys.path.insert(0,egg) + from setuptools.command.easy_install import main + return main(list(argv)+[egg]) # we're done here + finally: + if egg and os.path.exists(egg): + os.unlink(egg) + else: + if setuptools.__version__ == '0.0.1': + print >>sys.stderr, ( + "You have an obsolete version of setuptools installed. Please\n" + "remove it from your system entirely before rerunning this script." + ) + sys.exit(2) + + req = "setuptools>="+version + import pkg_resources + try: + pkg_resources.require(req) + except pkg_resources.VersionConflict: + try: + from setuptools.command.easy_install import main + except ImportError: + from easy_install import main + main(list(argv)+[download_setuptools(delay=0)]) + sys.exit(0) # try to force an exit + else: + if argv: + from setuptools.command.easy_install import main + main(argv) + else: + print "Setuptools version",version,"or greater has been installed." + print '(Run "ez_setup.py -U setuptools" to reinstall or upgrade.)' + +def update_md5(filenames): + """Update our built-in md5 registry""" + + import re + + for name in filenames: + base = os.path.basename(name) + f = open(name,'rb') + md5_data[base] = md5(f.read()).hexdigest() + f.close() + + data = [" %r: %r,\n" % it for it in md5_data.items()] + data.sort() + repl = "".join(data) + + import inspect + srcfile = inspect.getsourcefile(sys.modules[__name__]) + f = open(srcfile, 'rb'); src = f.read(); f.close() + + match = re.search("\nmd5_data = {\n([^}]+)}", src) + if not match: + print >>sys.stderr, "Internal error!" + sys.exit(2) + + src = src[:match.start(1)] + repl + src[match.end(1):] + f = open(srcfile,'w') + f.write(src) + f.close() + + +if __name__=='__main__': + if len(sys.argv)>2 and sys.argv[1]=='--md5update': + update_md5(sys.argv[2:]) + else: + main(sys.argv[1:]) + + + + + + Modified: py/trunk/py/__init__.py ============================================================================== --- py/trunk/py/__init__.py (original) +++ py/trunk/py/__init__.py Thu Jun 18 20:09:57 2009 @@ -66,13 +66,10 @@ 'test.__doc__' : ('./test/__init__.py', '__doc__'), 'test._PluginManager' : ('./test/pluginmanager.py', 'PluginManager'), 'test.raises' : ('./test/outcome.py', 'raises'), - 'test.mark' : ('./test/outcome.py', 'mark',), - 'test.deprecated_call' : ('./test/outcome.py', 'deprecated_call'), 'test.skip' : ('./test/outcome.py', 'skip'), 'test.importorskip' : ('./test/outcome.py', 'importorskip'), 'test.fail' : ('./test/outcome.py', 'fail'), 'test.exit' : ('./test/outcome.py', 'exit'), - 'test.pdb' : ('./test/custompdb.py', 'set_trace'), # configuration/initialization related test api 'test.config' : ('./test/config.py', 'config_per_process'), Modified: py/trunk/py/_com.py ============================================================================== --- py/trunk/py/_com.py (original) +++ py/trunk/py/_com.py Thu Jun 18 20:09:57 2009 @@ -109,7 +109,7 @@ mm = HookCall(registry, name, firstresult=firstresult) setattr(self, name, mm) def __repr__(self): - return "" %(self._hookspecs, self._plugins) + return "" %(self._hookspecs, self.registry) class HookCall: def __init__(self, registry, name, firstresult, extralookup=None): Modified: py/trunk/py/bin/gendoc.py ============================================================================== --- py/trunk/py/bin/gendoc.py (original) +++ py/trunk/py/bin/gendoc.py Thu Jun 18 20:09:57 2009 @@ -10,6 +10,7 @@ py.std.sys.exit() else: args = list(sys.argv[1:]) + args.extend(['-p', 'apigen']) argkeys = [a.split('=')[0] for a in args] if '--apigen' not in argkeys: args.append('--apigen') @@ -18,5 +19,5 @@ os.path.dirname(apigen.__file__), 'tool', 'py_build', 'build.py') args.append('--apigenscript=%s' % (fpath,)) if '--apigenpath' not in argkeys: - args.append('--apigenpath=/tmp/pylib-api') + args.append('--apigenpath=api') py.test.cmdline.main(args) Modified: py/trunk/py/execnet/testing/test_gateway.py ============================================================================== --- py/trunk/py/execnet/testing/test_gateway.py (original) +++ py/trunk/py/execnet/testing/test_gateway.py Thu Jun 18 20:09:57 2009 @@ -543,7 +543,7 @@ ret = channel.receive() assert ret == 42 - @py.test.mark.xfail("fix needed: dying remote process does not cause waitclose() to fail") + @py.test.xfail # "fix needed: dying remote process does not cause waitclose() to fail" def test_waitclose_on_remote_killed(self): gw = py.execnet.PopenGateway() channel = gw.remote_exec(""" @@ -616,12 +616,12 @@ def test_sshaddress(self): assert self.gw.remoteaddress == self.sshhost - @py.test.mark.xfail("XXX ssh-gateway error handling") + @py.test.xfail # XXX ssh-gateway error handling def test_connexion_failes_on_non_existing_hosts(self): py.test.raises(IOError, "py.execnet.SshGateway('nowhere.codespeak.net')") - @py.test.mark.xfail("XXX ssh-gateway error handling") + @py.test.xfail # "XXX ssh-gateway error handling" def test_deprecated_identity(self): py.test.deprecated_call( py.test.raises, IOError, Modified: py/trunk/py/misc/testing/test_api.py ============================================================================== --- py/trunk/py/misc/testing/test_api.py (original) +++ py/trunk/py/misc/testing/test_api.py Thu Jun 18 20:09:57 2009 @@ -4,6 +4,17 @@ import sys import inspect +def test_all_resolves(): + seen = py.builtin.set([py]) + lastlength = None + while len(seen) != lastlength: + lastlength = len(seen) + for item in py.builtin.frozenset(seen): + for value in item.__dict__.values(): + if isinstance(value, type(py.test)): + seen.add(value) + + class TestAPI_V0_namespace_consistence: def test_path_entrypoints(self): assert inspect.ismodule(py.path) Modified: py/trunk/py/test/dist/testing/test_dsession.py ============================================================================== --- py/trunk/py/test/dist/testing/test_dsession.py (original) +++ py/trunk/py/test/dist/testing/test_dsession.py Thu Jun 18 20:09:57 2009 @@ -367,7 +367,7 @@ assert node.gateway.spec.popen #XXX eq.geteventargs("pytest_sessionfinish") - @py.test.mark.xfail("test implementation missing") + @py.test.xfail def test_collected_function_causes_remote_skip_at_module_level(self, testdir): p = testdir.makepyfile(""" import py Modified: py/trunk/py/test/dist/testing/test_nodemanage.py ============================================================================== --- py/trunk/py/test/dist/testing/test_nodemanage.py (original) +++ py/trunk/py/test/dist/testing/test_nodemanage.py Thu Jun 18 20:09:57 2009 @@ -4,14 +4,14 @@ class pytest_funcarg__mysetup: def __init__(self, request): basetemp = request.config.mktemp( - "mysetup:%s" % request.function.__name__, + "mysetup-%s" % request.function.__name__, numbered=True) self.source = basetemp.mkdir("source") self.dest = basetemp.mkdir("dest") request.getfuncargvalue("_pytest") class TestNodeManager: - @py.test.mark.xfail("consider / forbid implicit rsyncdirs?") + @py.test.xfail def test_rsync_roots_no_roots(self, mysetup): mysetup.source.ensure("dir1", "file1").write("hello") config = py.test.config._reparse([source]) Modified: py/trunk/py/test/outcome.py ============================================================================== --- py/trunk/py/test/outcome.py (original) +++ py/trunk/py/test/outcome.py Thu Jun 18 20:09:57 2009 @@ -112,62 +112,6 @@ raise ExceptionFailure(msg="DID NOT RAISE", expr=args, expected=ExpectedException) -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 - raise AssertionError("%r did not produce DeprecationWarning" %(func,)) - return ret - -class KeywordDecorator: - """ decorator for setting function attributes. """ - def __init__(self, keywords, lastname=None): - self._keywords = keywords - self._lastname = lastname - - def __call__(self, func=None, **kwargs): - if func is None: - kw = self._keywords.copy() - kw.update(kwargs) - return KeywordDecorator(kw) - elif not hasattr(func, 'func_dict'): - kw = self._keywords.copy() - name = self._lastname - if name is None: - name = "mark" - kw[name] = func - return KeywordDecorator(kw) - func.func_dict.update(self._keywords) - return func - - def __getattr__(self, name): - if name[0] == "_": - raise AttributeError(name) - kw = self._keywords.copy() - kw[name] = True - return self.__class__(kw, lastname=name) - -mark = KeywordDecorator({}) # exitcodes for the command line EXIT_OK = 0 Modified: py/trunk/py/test/plugin/hookspec.py ============================================================================== --- py/trunk/py/test/plugin/hookspec.py (original) +++ py/trunk/py/test/plugin/hookspec.py Thu Jun 18 20:09:57 2009 @@ -14,6 +14,10 @@ ``config`` provides access to all such configuration values. """ +def pytest_namespace(config): + """ return dict of name->object to become available at py.test.*""" + + def pytest_unconfigure(config): """ called before test process is exited. """ Modified: py/trunk/py/test/plugin/pytest_execnetcleanup.py ============================================================================== --- py/trunk/py/test/plugin/pytest_execnetcleanup.py (original) +++ py/trunk/py/test/plugin/pytest_execnetcleanup.py Thu Jun 18 20:09:57 2009 @@ -3,6 +3,8 @@ """ import py +pytest_plugins = "xfail" + def pytest_configure(config): config.pluginmanager.register(Execnetcleanup()) @@ -37,18 +39,16 @@ while len(self._gateways) > len(gateways): self._gateways[-1].exit() return res - - at py.test.mark.xfail("clarify plugin registration/unregistration") + def test_execnetplugin(testdir): - p = ExecnetcleanupPlugin() - testdir.plugins.append(p) - testdir.inline_runsource(""" + reprec = testdir.inline_runsource(""" import py import sys def test_hello(): sys._gw = py.execnet.PopenGateway() + def test_world(): + assert hasattr(sys, '_gw') + py.test.raises(KeyError, "sys._gw.exit()") # already closed + """, "-s", "--debug") - assert not p._gateways - assert py.std.sys._gw - py.test.raises(KeyError, "py.std.sys._gw.exit()") # already closed - + reprec.assertoutcome(passed=2) Modified: py/trunk/py/test/plugin/pytest_iocapture.py ============================================================================== --- py/trunk/py/test/plugin/pytest_iocapture.py (original) +++ py/trunk/py/test/plugin/pytest_iocapture.py Thu Jun 18 20:09:57 2009 @@ -23,9 +23,10 @@ return capture def pytest_pyfunc_call(pyfuncitem): - for funcarg, value in pyfuncitem.funcargs.items(): - if funcarg == "capsys" or funcarg == "capfd": - value.reset() + if hasattr(pyfuncitem, 'funcargs'): + for funcarg, value in pyfuncitem.funcargs.items(): + if funcarg == "capsys" or funcarg == "capfd": + value.reset() class Capture: _capture = None @@ -62,3 +63,11 @@ assert out.startswith("42") """) reprec.assertoutcome(passed=1) + + def test_funcall_yielded_no_funcargs(self, testdir): + reprec = testdir.inline_runsource(""" + def test_hello(): + yield lambda: None + """) + reprec.assertoutcome(passed=1) + Modified: py/trunk/py/test/plugin/pytest_pytester.py ============================================================================== --- py/trunk/py/test/plugin/pytest_pytester.py (original) +++ py/trunk/py/test/plugin/pytest_pytester.py Thu Jun 18 20:09:57 2009 @@ -3,7 +3,7 @@ """ import py -import os +import sys, os import inspect from py.__.test.config import Config as pytestConfig import hookspec @@ -271,7 +271,8 @@ print "running", cmdargs, "curdir=", py.path.local() f1 = p1.open("w") f2 = p2.open("w") - popen = self.popen(cmdargs, stdout=f1, stderr=f2, close_fds=True) + popen = self.popen(cmdargs, stdout=f1, stderr=f2, + close_fds=(sys.platform != "win32")) ret = popen.wait() f1.close() f2.close() Modified: py/trunk/py/test/plugin/pytest_recwarn.py ============================================================================== --- py/trunk/py/test/plugin/pytest_recwarn.py (original) +++ py/trunk/py/test/plugin/pytest_recwarn.py Thu Jun 18 20:09:57 2009 @@ -1,6 +1,13 @@ """ -"recwarn" funcarg for asserting that warnings are shown to a user. +help performing checks for deprecation and other warnings. Provides: + + recwarn: function argument where one can call recwarn.pop() to get + the last warning that would have been shown. + + py.test.deprecated_call(func, *args, **kwargs): + assert that a function call triggers a deprecation warning. """ + import py import os @@ -10,6 +17,37 @@ request.addfinalizer(warnings.finalize) return warnings +def pytest_namespace(config): + 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 + raise AssertionError("%r did not produce DeprecationWarning" %(func,)) + return ret + + class RecordedWarning: def __init__(self, message, category, filename, lineno, line): self.message = message @@ -88,3 +126,46 @@ 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) + Modified: py/trunk/py/test/plugin/pytest_xfail.py ============================================================================== --- py/trunk/py/test/plugin/pytest_xfail.py (original) +++ py/trunk/py/test/plugin/pytest_xfail.py Thu Jun 18 20:09:57 2009 @@ -3,7 +3,7 @@ example: - @py.test.mark.xfail("needs refactoring") + @py.test.xfail def test_hello(): ... assert 0 @@ -52,29 +52,34 @@ for event in xpassed: tr._tw.line("%s: xpassed" %(event.item,)) +def xfail_decorator(func): + func.xfail = True + return func + +def pytest_namespace(config): + return dict(xfail=xfail_decorator) + # =============================================================================== # # plugin tests # # =============================================================================== - def test_xfail(testdir, linecomp): p = testdir.makepyfile(test_one=""" import py - pytest_plugins="pytest_xfail", - @py.test.mark.xfail + @py.test.xfail def test_this(): assert 0 - @py.test.mark.xfail + @py.test.xfail def test_that(): assert 1 """) result = testdir.runpytest(p) extra = result.stdout.fnmatch_lines([ "*expected failures*", - "*test_one.test_this*test_one.py:5*", + "*test_one.test_this*test_one.py:4*", "*UNEXPECTEDLY PASSING*", "*test_that*", ]) Modified: py/trunk/py/test/plugin/test_pytest_runner.py ============================================================================== --- py/trunk/py/test/plugin/test_pytest_runner.py (original) +++ py/trunk/py/test/plugin/test_pytest_runner.py Thu Jun 18 20:09:57 2009 @@ -29,16 +29,6 @@ class BaseFunctionalTests: - def test_funcattr(self, testdir): - reports = testdir.runitem(""" - import py - @py.test.mark(xfail="needs refactoring") - def test_func(): - raise Exit() - """) - rep = reports[1] - assert rep.keywords['xfail'] == "needs refactoring" - def test_passfunction(self, testdir): reports = testdir.runitem(""" def test_func(): Modified: py/trunk/py/test/pluginmanager.py ============================================================================== --- py/trunk/py/test/pluginmanager.py (original) +++ py/trunk/py/test/pluginmanager.py Thu Jun 18 20:09:57 2009 @@ -162,16 +162,25 @@ if hasattr(self, '_config'): self.call_plugin(plugin, "pytest_addoption", parser=self._config._parser) self.call_plugin(plugin, "pytest_configure", config=self._config) + #dic = self.call_plugin(plugin, "pytest_namespace", config=self._config) + #self._updateext(dic) def call_plugin(self, plugin, methname, **kwargs): return self.MultiCall(self.listattr(methname, plugins=[plugin]), **kwargs).execute(firstresult=True) + def _updateext(self, dic): + if dic: + for name, value in dic.items(): + setattr(py.test, name, value) + def do_configure(self, config): assert not hasattr(self, '_config') config.pluginmanager.register(self) self._config = config config.hook.pytest_configure(config=self._config) + for dic in config.hook.pytest_namespace(config=config) or []: + self._updateext(dic) def do_unconfigure(self, config): config = self._config @@ -179,6 +188,9 @@ config.hook.pytest_unconfigure(config=config) config.pluginmanager.unregister(self) +class Ext: + """ namespace for extension objects. """ + # # XXX old code to automatically load classes # Modified: py/trunk/py/test/testing/test_config.py ============================================================================== --- py/trunk/py/test/testing/test_config.py (original) +++ py/trunk/py/test/testing/test_config.py Thu Jun 18 20:09:57 2009 @@ -91,6 +91,7 @@ assert config2.basetemp != config3.basetemp class TestConfigAPI: + def test_config_getvalue_honours_conftest(self, testdir): testdir.makepyfile(conftest="x=1") testdir.mkdir("sub").join("conftest.py").write("x=2 ; y = 3") @@ -320,8 +321,8 @@ def test_default_registry(): assert py.test.config.pluginmanager.comregistry is py._com.comregistry - at py.test.mark.todo("test for deprecation") def test_ensuretemp(): + # XXX test for deprecation d1 = py.test.ensuretemp('hello') d2 = py.test.ensuretemp('hello') assert d1 == d2 Modified: py/trunk/py/test/testing/test_funcargs.py ============================================================================== --- py/trunk/py/test/testing/test_funcargs.py (original) +++ py/trunk/py/test/testing/test_funcargs.py Thu Jun 18 20:09:57 2009 @@ -200,18 +200,6 @@ req = funcargs.FuncargRequest(item) assert req.fspath == modcol.fspath -class TestRequestProtocol: - @py.test.mark.xfail - def test_protocol(self, testdir): - item = testdir.getitem(""" - def pytest_funcarg_arg1(request): return 1 - def pytest_funcarg_arg2(request): return 2 - def test_func(arg1, arg2): pass - """) - req = funcargs.FuncargRequest(item) - req._fillargs() - #assert item.funcreq. - class TestRequestCachedSetup: def test_request_cachedsetup(self, testdir): Modified: py/trunk/py/test/testing/test_outcome.py ============================================================================== --- py/trunk/py/test/testing/test_outcome.py (original) +++ py/trunk/py/test/testing/test_outcome.py Thu Jun 18 20:09:57 2009 @@ -15,49 +15,6 @@ def test_raises_function(self): py.test.raises(ValueError, int, 'hello') -# -# ============ 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) - def test_importorskip(): from py.__.test.outcome import Skipped try: @@ -83,34 +40,3 @@ excinfo = py.code.ExceptionInfo() assert excinfo.errisinstance(KeyboardInterrupt) -def test_pytest_mark_getattr(): - from py.__.test.outcome import mark - def f(): pass - - mark.hello(f) - assert f.hello == True - - mark.hello("test")(f) - assert f.hello == "test" - - py.test.raises(AttributeError, "mark._hello") - py.test.raises(AttributeError, "mark.__str__") - -def test_pytest_mark_call(): - from py.__.test.outcome import mark - def f(): pass - mark(x=3)(f) - assert f.x == 3 - def g(): pass - mark(g) - assert not g.func_dict - - mark.hello(f) - assert f.hello == True - - mark.hello("test")(f) - assert f.hello == "test" - - mark("x1")(f) - assert f.mark == "x1" - Modified: py/trunk/py/test/testing/test_pluginmanager.py ============================================================================== --- py/trunk/py/test/testing/test_pluginmanager.py (original) +++ py/trunk/py/test/testing/test_pluginmanager.py Thu Jun 18 20:09:57 2009 @@ -158,6 +158,22 @@ config.parse([]) assert not config.option.test123 + def test_do_ext_namespace(self, testdir): + testdir.makeconftest(""" + def pytest_namespace(config): + return {'hello': 'world'} + """) + p = testdir.makepyfile(""" + from py.test import hello + import py + def test_hello(): + assert hello == "world" + """) + result = testdir.runpytest(p) + assert result.stdout.fnmatch_lines([ + "*1 passed*" + ]) + def test_do_option_postinitialize(self, testdir): from py.__.test.config import Config config = Config() @@ -205,7 +221,7 @@ assert not pluginmanager.listattr("hello") assert pluginmanager.listattr("x") == [42] - @py.test.mark(xfail="implement setupcall") + @py.test.xfail # setup call methods def test_call_setup_participants(self, testdir): testdir.makepyfile( conftest=""" Modified: py/trunk/setup.py ============================================================================== --- py/trunk/setup.py (original) +++ py/trunk/setup.py Thu Jun 18 20:09:57 2009 @@ -7,8 +7,10 @@ """ import os, sys -from distutils.core import setup - +import ez_setup +ez_setup.use_setuptools() +from setuptools import setup, Extension + long_description = """ advanced testing and development support library: @@ -40,14 +42,13 @@ author='holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others', author_email='holger at merlinux.eu, py-dev at codespeak.net', - py_modules=['_findpy'], - scripts=['py/bin/py.cleanup', - 'py/bin/py.countloc', - 'py/bin/py.lookup', - 'py/bin/py.rest', - 'py/bin/py.svnwcrevert', - 'py/bin/py.test', - 'py/bin/py.which'], + entry_points={'console_scripts': ['py.cleanup = py.cmdline:pycleanup', + 'py.countloc = py.cmdline:pycountloc', + 'py.lookup = py.cmdline:pylookup', + 'py.rest = py.cmdline:pyrest', + 'py.svnwcrevert = py.cmdline:pysvnwcrevert', + 'py.test = py.cmdline:pytest', + 'py.which = py.cmdline:pywhich']}, classifiers=['Development Status :: 4 - Beta', 'Intended Audience :: Developers', 'License :: OSI Approved :: MIT License', @@ -59,7 +60,9 @@ 'Topic :: System :: Distributed Computing', 'Topic :: Utilities', 'Programming Language :: Python'], - packages=['py', + packages=['example.funcarg.mysetup', + 'example.funcarg.mysetup2', + 'py', 'py.builtin', 'py.builtin.testing', 'py.cmdline', @@ -143,47 +146,9 @@ 'rest/testing/data/part1.txt', 'rest/testing/data/part2.txt', 'rest/testing/data/tocdepth.rst2pdfconfig']}, + zip_safe=False, ) -def getscripts(): - if sys.platform == "win32": - base = "py/bin/win32/" - ext = ".cmd" - else: - base = "py/bin/" - ext = "" - l = [] - for name in ['py.countloc', 'py.svnwcrevert', 'py.rest', 'py.test', 'py.cleanup', 'py.lookup', 'py.which']: - l.append(base + name + ext) - return l - -# scripts for windows: turn "py.SCRIPT" into "py_SCRIPT" and create -# "py.SCRIPT.cmd" files invoking "py_SCRIPT" -from distutils.command.install_scripts import install_scripts -class my_install_scripts(install_scripts): - def run(self): - install_scripts.run(self) - #print self.outfiles - for fn in self.outfiles: - basename = os.path.basename(fn) - if basename.startswith("py.") and not basename.endswith(".cmd"): - newbasename = basename.replace(".", "_") - newfn = os.path.join(os.path.dirname(fn), newbasename) - if os.path.exists(newfn): - os.remove(newfn) - os.rename(fn, newfn) - fncmd = fn + ".cmd" - if os.path.exists(fncmd): - os.remove(fncmd) - f = open(fncmd, 'w') - f.write("@echo off\n") - f.write('python "%%~dp0\%s" %%*' %(newbasename)) - f.close() -if sys.platform == "win32": - cmdclass = {'install_scripts': my_install_scripts} -else: - cmdclass = {} - if __name__ == '__main__': main() \ No newline at end of file From commits-noreply at bitbucket.org Thu Jun 18 22:22:25 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Thu, 18 Jun 2009 20:22:25 -0000 Subject: [py-svn] commit/py-3k: yangyan5: tests of io/, builtin/, log/, and cmdline/ pass for python3.1 Message-ID: <20090618202225.13696.33335@domU-12-31-39-00-D4-C1.compute-1.internal> 1 new changeset in py-3k: http://www.bitbucket.org/yangyan5/py-3k/changeset/7be75141c343/ changeset: r1159:7be75141c343 user: yangyan5 date: 2009-06-18 22:21:58 summary: tests of io/, builtin/, log/, and cmdline/ pass for python3.1 affected #: 7 files (339 bytes) Repository URL: http://bitbucket.org/yangyan5/py-3k/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From hpk at codespeak.net Fri Jun 19 12:24:12 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Fri, 19 Jun 2009 12:24:12 +0200 (CEST) Subject: [py-svn] r65822 - in py/trunk/doc: . announce test Message-ID: <20090619102412.0CE1B169E9C@codespeak.net> Author: hpk Date: Fri Jun 19 12:24:11 2009 New Revision: 65822 Modified: py/trunk/doc/announce/release-1.0.0.txt py/trunk/doc/download.txt py/trunk/doc/index.txt py/trunk/doc/test/extend.txt Log: merge to svn Modified: py/trunk/doc/announce/release-1.0.0.txt ============================================================================== --- py/trunk/doc/announce/release-1.0.0.txt (original) +++ py/trunk/doc/announce/release-1.0.0.txt Fri Jun 19 12:24:11 2009 @@ -6,8 +6,7 @@ Welcome to the 1.0.0 py lib release - a python library aiming to support agile and test-driven development. -It passes tests against Linux, OSX and Win32, on Python -2.3, 2.4, 2.5 and 2.6. +It works with Linux, OSX and Win32, on Python 2.3, 2.4, 2.5 and 2.6. Main API/Tool Features: Modified: py/trunk/doc/download.txt ============================================================================== --- py/trunk/doc/download.txt (original) +++ py/trunk/doc/download.txt Fri Jun 19 12:24:11 2009 @@ -2,6 +2,8 @@ Downloading ============== +Latest Release: 1.0.0b2 + "easy_install py" =================================================== Modified: py/trunk/doc/index.txt ============================================================================== --- py/trunk/doc/index.txt (original) +++ py/trunk/doc/index.txt Fri Jun 19 12:24:11 2009 @@ -1,6 +1,8 @@ py lib: Main tools and APIs =================================== +Latest Release: 1.0.0b2 + `py.test`_ write and deploy unit- and functional tests to multiple machines. `py.execnet`_ rapidly deploy local or remote processes from your program. Modified: py/trunk/doc/test/extend.txt ============================================================================== --- py/trunk/doc/test/extend.txt (original) +++ py/trunk/doc/test/extend.txt Fri Jun 19 12:24:11 2009 @@ -8,11 +8,11 @@ hooks`_. Python modules which contain such hook functions are called plugins. Hook functions are discovered in ``conftest.py`` files or in **named** plugins. ``conftest.py`` files are sometimes called "anonymous" -or "local" plugins if they contain hooks. They allow to write and distribute -some extensions along with the test suite or the application package easily. -Named plugins are python modules or packages that have an all lowercase -``pytest_`` prefixed name and who are imported during tool startup or -the testing process. +or conftest plugins. They are useful for keeping test extensions close +to the application package. Named plugins are normal python modules or packages +that can be distributed separately. Named plugins need to follow a naming pattern; +they have an all lowercase ``pytest_`` prefixed name. While conftest plugins are +discovered automatically, named plugins must be explicitely specified. .. _`tool startup`: .. _`test tool starts up`: @@ -61,7 +61,7 @@ -------------------------------------------------- The purpose of ``conftest.py`` files is to allow `project-specific -test configuration`_. But they also make for a good place to implement +test configuration`_. They thus make for a good place to implement project-specific test related features through hooks. For example you may set the `collect_ignore`_ variable depending on a command line option by defining the following hook in a ``conftest.py`` file: @@ -91,7 +91,9 @@ strict checking on contained hook functions. Function and argument names need to match exactly one of `hook definition specification`_. It thus provides useful error reporting on mistyped hook or argument names -and minimizes version incompatibilites. +and minimizes version incompatibilites. Below you find some introductory +information on particular hooks. It's sensible to look at existing +plugins so see example usages and start off with your own plugin. .. _`hook definition specification`: http://bitbucket.org/hpk42/py-trunk/src/tip/py/test/plugin/hookspec.py From hpk at codespeak.net Fri Jun 19 13:09:59 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Fri, 19 Jun 2009 13:09:59 +0200 (CEST) Subject: [py-svn] r65823 - in py/trunk: . doc py py/test Message-ID: <20090619110959.0FCA2169EA9@codespeak.net> Author: hpk Date: Fri Jun 19 13:09:57 2009 New Revision: 65823 Modified: py/trunk/CHANGELOG py/trunk/MANIFEST py/trunk/doc/download.txt py/trunk/doc/index.txt py/trunk/py/__init__.py py/trunk/py/test/pluginmanager.py py/trunk/setup.py Log: merge hg-trunk b3 Modified: py/trunk/CHANGELOG ============================================================================== --- py/trunk/CHANGELOG (original) +++ py/trunk/CHANGELOG Fri Jun 19 13:09:57 2009 @@ -1,6 +1,6 @@ $Id$ -Changes between 1.0.0b1 and 1.0.0b2 +Changes between 1.0.0b1 and 1.0.0b3 ============================================= * plugin classes are removed: one now defines Modified: py/trunk/MANIFEST ============================================================================== --- py/trunk/MANIFEST (original) +++ py/trunk/MANIFEST Fri Jun 19 13:09:57 2009 @@ -57,6 +57,7 @@ example/pytest/failure_demo.py example/pytest/test_failures.py example/pytest/test_setup_flow_example.py +ez_setup.py py/LICENSE py/__init__.py py/_com.py Modified: py/trunk/doc/download.txt ============================================================================== --- py/trunk/doc/download.txt (original) +++ py/trunk/doc/download.txt Fri Jun 19 13:09:57 2009 @@ -2,7 +2,9 @@ Downloading ============== -Latest Release: 1.0.0b2 +.. _`PyPI project page`: http://pypi.python.org/pypi?%3Aaction=pkg_edit&name=py + +Latest Release, see `PyPI project page`_ "easy_install py" =================================================== Modified: py/trunk/doc/index.txt ============================================================================== --- py/trunk/doc/index.txt (original) +++ py/trunk/doc/index.txt Fri Jun 19 13:09:57 2009 @@ -1,7 +1,9 @@ py lib: Main tools and APIs =================================== -Latest Release: 1.0.0b2 +.. _`PyPI project page`: http://pypi.python.org/pypi?%3Aaction=pkg_edit&name=py + +Latest Release, see `PyPI project page`_ `py.test`_ write and deploy unit- and functional tests to multiple machines. Modified: py/trunk/py/__init__.py ============================================================================== --- py/trunk/py/__init__.py (original) +++ py/trunk/py/__init__.py Fri Jun 19 13:09:57 2009 @@ -19,7 +19,7 @@ """ from initpkg import initpkg -version = "1.0.0b2" +version = "1.0.0b3" initpkg(__name__, description = "py.test and pylib: advanced testing tool and networking lib", Modified: py/trunk/py/test/pluginmanager.py ============================================================================== --- py/trunk/py/test/pluginmanager.py (original) +++ py/trunk/py/test/pluginmanager.py Fri Jun 19 13:09:57 2009 @@ -33,7 +33,8 @@ def register(self, plugin, name=None): assert not self.isregistered(plugin) name = self._getpluginname(plugin, name) - assert name not in self.impname2plugin + if name in self.impname2plugin: + return False self.impname2plugin[name] = plugin self.hook.pytest_plugin_registered(plugin=plugin) self._checkplugin(plugin) Modified: py/trunk/setup.py ============================================================================== --- py/trunk/setup.py (original) +++ py/trunk/setup.py Fri Jun 19 13:09:57 2009 @@ -35,7 +35,7 @@ name='py', description='py.test and pylib: advanced testing tool and networking lib', long_description = long_description, - version='1.0.0b2', + version='1.0.0b3', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], From py-svn at codespeak.net Sat Jun 20 06:25:10 2009 From: py-svn at codespeak.net (py-svn at codespeak.net) Date: Sat, 20 Jun 2009 06:25:10 +0200 (CEST) Subject: [py-svn] BestBuy.com Deal of the Day Message-ID: <20090620042510.D48F916857D@codespeak.net> An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Sun Jun 21 17:28:42 2009 From: py-svn at codespeak.net (Pickette Melania) Date: Sun, 21 Jun 2009 17:28:42 +0200 (CEST) Subject: [py-svn] Can't open your file Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Mon Jun 22 15:40:57 2009 From: py-svn at codespeak.net (Christiane Koozy) Date: Mon, 22 Jun 2009 10:40:57 -0300 Subject: [py-svn] Faster youtube analog Message-ID: An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Tue Jun 23 01:03:46 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 22 Jun 2009 23:03:46 -0000 Subject: [py-svn] commit/py-3k: yangyan5: builtin/, cmdline/, io/, log/, path/, process/, rest/ pass under python3.1 Message-ID: <20090622230346.13696.93020@domU-12-31-39-00-D4-C1.compute-1.internal> 1 new changeset in py-3k: http://www.bitbucket.org/yangyan5/py-3k/changeset/5ae8113102e2/ changeset: r1160:5ae8113102e2 user: yangyan5 date: 2009-06-23 01:03:12 summary: builtin/, cmdline/, io/, log/, path/, process/, rest/ pass under python3.1 affected #: 25 files (3.6 KB) Repository URL: http://bitbucket.org/yangyan5/py-3k/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From py-svn at codespeak.net Tue Jun 23 07:12:01 2009 From: py-svn at codespeak.net (Lesley Aeif) Date: Tue, 23 Jun 2009 07:12:01 +05-30 Subject: [py-svn] Timetable, autumn Message-ID: An HTML attachment was scrubbed... URL: From hpk at codespeak.net Tue Jun 23 13:17:27 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Tue, 23 Jun 2009 13:17:27 +0200 (CEST) Subject: [py-svn] r65880 - py/extradoc/talk/ep2009/pytest-advanced Message-ID: <20090623111727.46160169ECF@codespeak.net> Author: hpk Date: Tue Jun 23 13:17:26 2009 New Revision: 65880 Added: py/extradoc/talk/ep2009/pytest-advanced/ (props changed) - copied from r65320, py/extradoc/talk/pycon-us-2009/pytest-advanced/ Modified: py/extradoc/talk/ep2009/pytest-advanced/author.latex py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt Log: working on ep 2009 slides ... Modified: py/extradoc/talk/ep2009/pytest-advanced/author.latex ============================================================================== --- py/extradoc/talk/pycon-us-2009/pytest-advanced/author.latex (original) +++ py/extradoc/talk/ep2009/pytest-advanced/author.latex Tue Jun 23 13:17:26 2009 @@ -1,7 +1,7 @@ \definecolor{rrblitbackground}{rgb}{0.0, 0.0, 0.0} -\title[pytest advanced testing tutorial] {pytest advanced testing tutorial} +\title[rapid testing with py.test] {rapid testing with py.test} \author[H. Krekel]{Holger Krekel \\ http://merlinux.eu} -\institute[Pycon US 2009, Chicago]{Pycon US 2009, Chicago} -\date{March 26, 2009} +\institute[EuroPython 2009, Birmingham]{EuroPython 2009, Birmingham} +\date{June 29, 2009} Modified: py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt ============================================================================== --- py/extradoc/talk/pycon-us-2009/pytest-advanced/pytest-advanced.txt (original) +++ py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt Tue Jun 23 13:17:26 2009 @@ -2,19 +2,19 @@ .. include:: ================================================================= -Advanced cross platform testing +Rapid testing with py.test ================================================================= -If you have time, please install +Install ======================================== - svn checkout http://codespeak.net/svn/py/dist +- hg clone https://hpk42 at bitbucket.org/hpk42/py-trunk/ - run "python setup.py" with "install" or "develop" - if need be: easy_install "py" -- slides: http://tinyurl.com/c76gve my technical background =========================== @@ -26,15 +26,6 @@ - merlinux GmbH since 2004 - PyPy EU-project 2004-2007 -my current background -======================== - -.. image:: img/little_red_riding_hood_by_marikaz.jpg - :scale: 100 - :align: left - -http://marikaz.deviantart.com/ CC 3.0 AN-ND - my testing background ======================= @@ -42,7 +33,7 @@ - "test-driven developer" (TDD) since 2002 - founded PyPy, based on TDD principles - developed utest/stdtest, now py.test -- consulted to help with testing +- consultancies on testing What's **your** background? ============================== @@ -171,7 +162,7 @@ merge with real-life deployment. -pytest advanced features (30 minutes) +Walkthrough Python test functions (30 minutes) ============================================================================ .. image:: img/new_color_in_dark_old_city_by_marikaz.jpg @@ -180,7 +171,44 @@ http://marikaz.deviantart.com/ CC 3.0 AN-ND -Python test function viewed abstractly +A Typical Python test layout +========================================== +:: + + app/__init__.py + ... + app/tests/test_module.py + ... + +py.test invocation: ``py.test app`` + +Another typical test layout +========================================== +:: + + app/__init__.py + tests/test_module.py + +py.test invocation: ``py.test tests``. + +Mind the the `__init__.py` files! +========================================== + +wherever your "tests" directories are, +always provide a ``__init__.py`` + +automatic test discovery +=================================== + +py.test walks over your source tree and: + +- discovers ``test_*.py`` test files +- discovers ``test_`` functions or ``Test`` classes + +**automatic discovery avoids boilerplate** + + +Typical test function - viewed abstractly ========================================== :: @@ -190,14 +218,18 @@ ... assert "things are ok" -observations +observations and the setup question ========================================== -* test configuration mixes with code instantiation +* test values / configuration mixes with test code * importing 'app' may fail -* the "app.pkg.SomeClass" reference may change +* the ``app.pkg.SomeClass`` reference may change + +what if multiple test functions setup the same +classes, maybe with slightly different values? + -setting up state close to code +x-unit style setup / fixtures ========================================== :: @@ -214,61 +246,72 @@ observations ========================================== -* test configuration mixes with code instantiation +* test values / configuration mixes with test code * importing 'app' may fail -* the "app.pkg.SomeClass" reference may change -* **functions tend to group by setup methods** +* the ``app.pkg.SomeClass`` reference may change +* **functions now group by setup/fixture code** * **multiple methods can reuse the same setup** -meet py.test "funcargs" +meet "funcargs" - test function arguments ========================================== -goals: +:: -* fully separate test setup from test code -* setup app bootstraps in one place -* allow to group tests in classes or files logically + def test_something(inst1): + assert inst1.call() == "result" -basic funcarg mechanism +observations ========================================== -- have a test function specify its needed setup -- lookup and call a function that provides setup +* test values are simply used in test code +* no imports or app.pkg.SomeClass references here +* freedom to group tests logically +* multiple functions can re-use the same funcarg setup -Example test function +How to setup the funcarg value? ========================================== -:: +:: + + from app.pkg import SomeClass + def pytest_funcarg__inst1(request): + return SomeClass("somevalue") - def test_somefunction(myarg): - assert myarg == 'test_somefunction' +defined in: test module, ``conftest.py`` or named plugin -Example conftest.py -========================================== -:: +observations +=================================================== - class ConftestPlugin: - def pytest_funcarg__myarg(self, pyfuncitem): - return pyfuncitem.name +* funcarg provider automatically discovered +* app bootstraps for testing in one place +* funcarg can be used from different test functions + +introducing a command line option +=================================================== +:: + # ./tests/conftest.py + def pytest_addoption(parser): + parser.addoption("--val", action="store") + def pytest_funcarg_inst1(request): + return SomeClass(request.config.getvalue("val")) observations -===================== +=================================================== -funcargs: +* test configuration separated from test code +* app bootstraps for testing in one place +* can be used from any test function anywhere -* test functions receive value by specifying a name -* makers are registered after command line parsing -* makers have meta-access to "collected function item" +``request`` object attributes +=================================================== -notes on "named based lookup" -=================================== +``request.function``: python test function -py.test often looks up names: +``request.cls``: containing test class -- discover ``test_*.py`` test files -- discover ``test_`` functions or ``Test`` classes -- and now: discover test function arguments +``request.module``: test module + +``request.config``: access to options and general config -**automatic discovery avoids boilerplate** Exercise ========================================== From commits-noreply at bitbucket.org Tue Jun 23 16:56:53 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 23 Jun 2009 14:56:53 -0000 Subject: [py-svn] commit/py-trunk: 5 new changesets Message-ID: <20090623145653.13696.83014@domU-12-31-39-00-D4-C1.compute-1.internal> 5 new changesets in py-trunk: http://www.bitbucket.org/hpk42/py-trunk/changeset/f41bfd9094d1/ changeset: r1161:f41bfd9094d1 user: hpk date: 2009-06-18 20:04:36 summary: fixing some release/apigen related issues affected #: 4 files (451 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/822271c98933/ changeset: r1162:822271c98933 user: hpk date: 2009-06-19 12:19:59 summary: snapshot 1.0.0b2 affected #: 5 files (296 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/52c6d9e78777/ changeset: r1163:52c6d9e78777 user: hpk date: 2009-06-19 13:05:44 summary: b3, fix (likelY) windows-path-comparison related issue at plugin registration link to PyPI affected #: 7 files (226 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/506f4c5f741b/ changeset: r1164:506f4c5f741b user: hpk date: 2009-06-23 11:33:20 summary: Added tag 1.0.0b3 for changeset 52c6d9e78777 affected #: 1 file (49 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/0516498708b6/ changeset: r1165:0516498708b6 user: hpk date: 2009-06-23 12:12:45 summary: remove TODO file (mostly done, superflous or in the issue tracker), remove keyword subsitution in text files. regen setup with hg info instead of svn. affected #: 4 files (274 bytes) Repository URL: http://bitbucket.org/hpk42/py-trunk/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From hpk at codespeak.net Wed Jun 24 14:29:27 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Wed, 24 Jun 2009 14:29:27 +0200 (CEST) Subject: [py-svn] r65909 - py/extradoc/talk/ep2009/pytest-advanced Message-ID: <20090624122927.E076E169F4D@codespeak.net> Author: hpk Date: Wed Jun 24 14:29:27 2009 New Revision: 65909 Added: py/extradoc/talk/ep2009/pytest-advanced/test_addfinalizer.py py/extradoc/talk/ep2009/pytest-advanced/test_cached_setup.py py/extradoc/talk/ep2009/pytest-advanced/test_generate_tests.py Removed: py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.pdf Modified: py/extradoc/talk/ep2009/pytest-advanced/makepdf py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt Log: snapshot of slides, examples Modified: py/extradoc/talk/ep2009/pytest-advanced/makepdf ============================================================================== --- py/extradoc/talk/ep2009/pytest-advanced/makepdf (original) +++ py/extradoc/talk/ep2009/pytest-advanced/makepdf Wed Jun 24 14:29:27 2009 @@ -7,7 +7,7 @@ # https://sourceforge.net/tracker/?func=detail&atid=422032&aid=1459707&group_id=38414 BASE=pytest-advanced -python rst2beamer.py --stylesheet=stylesheet.latex --documentoptions=14pt $BASE.txt $BASE.latex || exit +python rst2beamer.py --stylesheet=stylesheet.latex --documentoptions=13pt $BASE.txt $BASE.latex || exit sed 's/\\date{}/\\input{author.latex}/' $BASE.latex >tmpfile || exit sed 's/\\maketitle/\\input{title.latex}/' tmpfile >$BASE.latex || exit pdflatex $BASE.latex || exit Deleted: /py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.pdf ============================================================================== Files /py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.pdf Wed Jun 24 14:29:27 2009 and (empty file) differ Modified: py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt ============================================================================== --- py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt (original) +++ py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt Wed Jun 24 14:29:27 2009 @@ -9,11 +9,12 @@ ======================================== - svn checkout http://codespeak.net/svn/py/dist -- hg clone https://hpk42 at bitbucket.org/hpk42/py-trunk/ + +- hg clone https://bitbucket.org/hpk42/py-trunk/ - run "python setup.py" with "install" or "develop" -- if need be: easy_install "py" +- or try: easy_install "py" my technical background @@ -44,16 +45,6 @@ - need/want to run tests on >1 platforms? - have "non-python" tests? -Python -================================== - -Python has no compile-time type security. - - -Testing to the rescue! -================================== - -automated tests are better than declaring types. The test tool question ======================================== @@ -63,8 +54,8 @@ my current answer ======================================== -* verify that my code changes work out -* be helpful when test scenarios fail +* verify that code changes work out +* be helpful when tests fail If failures are not helpful ... ======================================== @@ -73,96 +64,77 @@ write more (different) tests -The Automated Test question -======================================== - -what are automated tests there for? - -my current answer -======================================== - -to make sure that +Test terminology +========================================= -* units react well to input. -* components co-operate nicely -* code changes work out in the end +developer oriented tests: + +- unit tests +- integration tests +- functional tests -What does "code changes work out" mean? -======================================== +user/customer oriented tests: -.. image:: img/rails_in_the_city_by_marikaz.jpg - :scale: 90 - :align: center +- acceptance tests +- useability tests +- GUI/Web functional tests -http://marikaz.deviantart.com/ CC 3.0 AN-ND -my current answer +Interesting developer test types ======================================== -- various operating systems -- various Python Interpreters -- cloud environments -- web browsers etc. - -Common Test terminology -============================== +* **unit testing**: units react well to input. +* **integration**: units work well together +* **functional**: software works in target systems -- developer and customer tests -- unit tests -- functional tests -- acceptance tests -- integration tests - -you may discuss a long time -about categorizations ... - -py.test strives to test it all +Unit testing ================================= -current focus: +* individual pieces or units are fit for use +* a unit is the smallest testable part of an app +* unit tests often check function or class behaviour +* often relatively simple to setup, quick to perfrom -- unittesting -- functional -- integration tests +.. image:: img/small.png + :align: center + :scale: 60 -A pragmatic view on test types +Integration testing ================================= -let's talk about small, medium or large tests. - -Small Tests: one aspect -============================== +* units work well together +* more black-boxy +* more efforts for managing setup -.. image:: img/small.png +.. image:: img/medium.png :align: center - :scale: 70 + :scale: 60 -Medium Tests: two aspects -============================== -.. image:: img/medium.png - :align: center - :scale: 70 +functional: fitness for target system +======================================== -Large Tests: end-to-end -============================== +example system parameters: + +- operating systems, versions of required software +- browser/server combinations +- often expensive/complex to setup .. image:: img/large.png :align: center - :scale: 70 + :scale: 60 -generally speaking -============================================ -what is the vision of automated testing? +py.test strives to test it all +================================= -my current answer -======================================== +- unit tests +- integration tests +- functional tests -merge with real-life deployment. -Walkthrough Python test functions (30 minutes) +Walkthrough Python test functions (60 minutes) ============================================================================ .. image:: img/new_color_in_dark_old_city_by_marikaz.jpg @@ -187,28 +159,111 @@ :: app/__init__.py + ... tests/test_module.py + ... + py.test invocation: ``py.test tests``. Mind the the `__init__.py` files! ========================================== -wherever your "tests" directories are, -always provide a ``__init__.py`` +* wherever your "tests" directories are, + always provide a ``__init__.py`` + +* py.test will make sure that your package root + directory is added to the Module import path + automatic test discovery =================================== py.test walks over your source tree and: -- discovers ``test_*.py`` test files -- discovers ``test_`` functions or ``Test`` classes +- discovers all ``test_*.py`` test files +- discovers all ``test_`` functions or ``Test`` classes **automatic discovery avoids boilerplate** -Typical test function - viewed abstractly +test function / test classes +======================================== + +:: + + def test_something(): + assert True + + class TestSomething: + def test_something(self): + assert True + def test_other(self): + ... + + +assert introspection +==================== + +:: + + def test_assert_introspection(): + assert result # assertTrue(result) + assert x == 1 # assertEqual(x, 1) + assert x != y # assertNotEqual(x, y) + assert not result # assertFalse(result) + + +print() debugging +================= + +:: + + def test_something1(): + print "parent", node.parent + assert node.children + +testing exceptions +================== + +:: + + import py + + def test_raises_one(): + py.test.raises(ValueError, int, 'foo') + + def test_raises_two(): + py.test.raises(ValueError, "int('foo')") + + +get-going exercise (max 10 min) +============================================ + +install with one of: + +- easy_install "py" +- svn checkout http://codespeak.net/svn/py/trunk +- hg clone https://bitbucket.org/hpk42/py-trunk/ +- run "python setup.py" with "install" or "develop" +- create a ``test_file.py`` and run ``py.test test_file.py`` +- help your neighbours! + +selected options +============================== + +among others, ``py.test --help`` yields these useful options: + +- -s - disable catching of stdout/stderr during test run. +- -x/--exitfirst - exit instantly on first error or failed test. +- -l/--showlocals - show locals in tracebacks. +- --pdb - start pdb (the Python debugger) on errors. +- --tb/--fulltrace - different options to control traceback generation. +- --nomagic - turn of assert reinterpretation + +.. _`function arguments`: + +Typical test function - abstract view ========================================== :: @@ -221,36 +276,56 @@ observations and the setup question ========================================== -* test values / configuration mixes with test code +* test values and configuration mixes with test code * importing 'app' may fail * the ``app.pkg.SomeClass`` reference may change -what if multiple test functions setup the same -classes, maybe with slightly different values? +what if multiple test functions have similar setup, +maybe with slightly different configuration for +class instantiation? -x-unit style setup / fixtures +"unittest.py" style setup / abstract view ========================================== :: - + import unittest from app.pkg import SomeClass - class TestGroup: - def setup_method(self, method): + + class TestGroup(unittest.TestCase): + def setUp(self): self.inst1 = SomeClass("somevalue") def test_something(self): ... use self.inst1 ... - assert "things are ok" + assert self.inst1.method() == "ok" observations ========================================== +* **multiple methods can reuse the same setup** +* ``self.inst1`` instantiated anew for each test invocation * test values / configuration mixes with test code -* importing 'app' may fail * the ``app.pkg.SomeClass`` reference may change * **functions now group by setup/fixture code** -* **multiple methods can reuse the same setup** + +old-style xUnit py.test extensions +========================================================== + +to help with managing test state across modules, classes, +and methods, py.test invented: + + setup_module(module) / teardown_module(module) + + setup_class(cls) / teardown_class(cls) + + setup_method(self, method) / teardown_method(self, method) + +but: +- even increases need for grouping of functions by setup +- test values / configuration still mix with test code +- ... mostly deprecated! + meet "funcargs" - test function arguments ========================================== @@ -258,36 +333,47 @@ :: def test_something(inst1): - assert inst1.call() == "result" + assert inst1.method() == "ok" + +or:: + + class TestGroup: + def test_other(self, inst1): + assert inst1.method() == "ok" observations ========================================== * test values are simply used in test code -* no imports or app.pkg.SomeClass references here +* no imports or app.pkg.SomeClass references * freedom to group tests logically -* multiple functions can re-use the same funcarg setup +* multiple functions re-use same funcarg setup How to setup the funcarg value? ========================================== -:: - + +write down in test module, ``conftest.py`` (or a plugin):: + from app.pkg import SomeClass def pytest_funcarg__inst1(request): return SomeClass("somevalue") -defined in: test module, ``conftest.py`` or named plugin - -observations +observations / notes =================================================== -* funcarg provider automatically discovered -* app bootstraps for testing in one place -* funcarg can be used from different test functions +* test value setup separated from test code + +* funcarg setup function automatically discovered by looking + for ``pytest_funcarg__`` prefixed functions + +* app specific bootstraping contained in one place + +* re-useable from test functions across a whole project -introducing a command line option +let's introduce a command line option =================================================== :: + # ./tests/conftest.py def pytest_addoption(parser): parser.addoption("--val", action="store") @@ -312,17 +398,107 @@ ``request.config``: access to options and general config - -Exercise +Exercise (max 10 minutes) ========================================== * write a new package "mypkg" * add mypkg/__init__ and mypkg/test_url.py -* add a test_path function that needs an "url" argument +* add a ``test_open(url)`` function checking + if URL can be opened * write the provider in mypkg/conftest.py * run your test +* optional: add a command line option "--url" + +Bonus: add more tests, play with wrongly named args, introduce options. + +invoking a test function with different values +================================================ + +:: + + def pytest_generate_tests(metafunc): + if "url" in metafunc.funcargnames: + metafunc.addcall(funcargs=dict(url="http://testrun.org")) + metafunc.addcall(funcargs=dict(url="https://codespeak.net")) + +``metafunc`` attributes and abilities +================================================ + +``metafunc.funcargnames``: set of required function arguments for given function + +``function, cls, module, config``: same as request object + +means: you can easily implement a decorator or per-module +data for specifiying argument sets. see + +deprecation sidenote on yield-generated tests +================================================ + +py.test pioneered 'yield' based testing + +deprecated since 1.0 because it's a very limited form of the +new ``pytest_generate_tests()`` mechanism. + + +Calling a finalizer for a funcarg +============================================= + +a funcarg provider can use its ``request`` object to +register a finalization call:: + + myfile = open(...) + request.addfinalizer(lambda: myfile.close()) + + +Managing more complex scope setup +============================================= + +- some funcargs are costly to setup +- some funcargs need explicit teardown +- managing this correctly can be complex + +py.test provides a powerful high-level helper:: + + ``request.cached_setup(...)`` + + +Example for caching a Database object Setup +============================================= + +:: + + def pytest_funcarg__db(request): + return request.cached_setup( + setup=lambda: Database(request.config.getvalue("db")), + teardown=lambda db: db.close(), + scope="session", + ) + +* the test code does not need to know about this optimization +* easy to change to per-module or to per-function setup/teardown! +* **interactive demo** + + +summary of py.test funcargs +================================ + +* test function is fully separated from test setup/bootstrap +* test function can focus on specific testing aspects +* test functions can be called multiple times +* manage simple and complex "funcarg" values +* re-use test functions with generated calls +* ideal for functional testing + + +Break +===== + +.. image:: img/flying_lady_by_marikaz.jpg + :scale: 100 + :align: left + +http://marikaz.deviantart.com/ CC 3.0 AN-ND -Bonus: add more tests, play with wrongly named args. Using Plugins and Extensions ========================================= @@ -394,15 +570,6 @@ - let's play with "pytest_unittest" plugin -Break -===== - -.. image:: img/flying_lady_by_marikaz.jpg - :scale: 100 - :align: left - -http://marikaz.deviantart.com/ CC 3.0 AN-ND - Writing Plugins (30 minutes) ============================================================================ @@ -419,22 +586,27 @@ * collectors, ``collect()`` returns list of "colitems" * items, implement ``runtest()`` for running a test + test collection tree ======================== -- collection tree is built iteratively -- test collection starts from directories or files (via cmdline) +* collection tree is built iteratively +* test collection starts from directories or files (via cmdline) + + -py.test.collect.* filesystem objects +py.test.collect filesystem objects ====================================== -- **Directory** -- **File** +* Directory +* File + + always available Attributes ======================================= -**parent** a reference to the collector that produced us +**parent**: a reference to the collector that produced us **name**: a name that is unique within the scope of our parent Added: py/extradoc/talk/ep2009/pytest-advanced/test_addfinalizer.py ============================================================================== --- (empty file) +++ py/extradoc/talk/ep2009/pytest-advanced/test_addfinalizer.py Wed Jun 24 14:29:27 2009 @@ -0,0 +1,12 @@ + +def pytest_funcarg__myarg(request): + print "setup", + def teardown(): print "teardown", + request.addfinalizer(teardown) + return 42 + +def test_hello(myarg): + assert myarg == 42 + +def test_world(myarg): + assert myarg == 23 Added: py/extradoc/talk/ep2009/pytest-advanced/test_cached_setup.py ============================================================================== --- (empty file) +++ py/extradoc/talk/ep2009/pytest-advanced/test_cached_setup.py Wed Jun 24 14:29:27 2009 @@ -0,0 +1,20 @@ + +class Myarg: + def __init__(self): + print "setup", + + def finalize(self): + print "teardown", + +def pytest_funcarg__myarg(request): + return request.cached_setup( + setup=Myarg, + teardown=lambda arg: arg.finalize(), + scope="module", + ) + +def test_hello(myarg): + assert myarg == 42 + +def test_world(myarg): + assert myarg == 23 Added: py/extradoc/talk/ep2009/pytest-advanced/test_generate_tests.py ============================================================================== --- (empty file) +++ py/extradoc/talk/ep2009/pytest-advanced/test_generate_tests.py Wed Jun 24 14:29:27 2009 @@ -0,0 +1,12 @@ + +def pytest_generate_tests(metafunc): + if "url" in metafunc.funcargnames: + for val in ('testrun.org', 'codespeak.net'): + metafunc.addcall(id=val, funcargs={'url': 'http://' + val}) + + +import urllib +def test_open(url): + f = urllib.urlopen(url) + assert f + f.close() From commits-noreply at bitbucket.org Wed Jun 24 18:10:02 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Wed, 24 Jun 2009 16:10:02 -0000 Subject: [py-svn] commit/py-trunk: 18 new changesets Message-ID: <20090624161002.13696.69777@domU-12-31-39-00-D4-C1.compute-1.internal> 18 new changesets in py-trunk: http://www.bitbucket.org/hpk42/py-trunk/changeset/2d14b6bbff19/ changeset: r1166:2d14b6bbff19 user: hpk date: 2009-06-23 12:19:55 summary: bumping version to "1.0.x" for now, regen setup, opening 1.0.x branch affected #: 3 files (14.4 KB) http://www.bitbucket.org/hpk42/py-trunk/changeset/9859a60a0035/ changeset: r1167:9859a60a0035 user: hpk date: 2009-06-23 17:10:52 summary: remove scope argument from request.addfinalizer affected #: 4 files (1.2 KB) http://www.bitbucket.org/hpk42/py-trunk/changeset/eb9c80c07f10/ changeset: r1168:eb9c80c07f10 user: hpk date: 2009-06-24 15:43:37 summary: perform state setup finalization earlier affected #: 3 files (264 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/f5bc2d1aed00/ changeset: r1169:f5bc2d1aed00 user: hpk date: 2009-06-24 15:55:57 summary: addresses issue #22 allow test functions to have no func_code affected #: 2 files (251 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/4fa7081c2758/ changeset: r1170:4fa7081c2758 user: hpk date: 2009-06-24 16:04:42 summary: add test and fix keyword recogniation, thanks Andreas Kloeckner affected #: 3 files (558 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/a887c6d1d9c2/ changeset: r1171:a887c6d1d9c2 user: hpk date: 2009-06-24 16:20:14 summary: regen setup.py, already bump trove classifier affected #: 3 files (12.7 KB) http://www.bitbucket.org/hpk42/py-trunk/changeset/34ac8ec26545/ changeset: r1172:34ac8ec26545 user: hpk date: 2009-06-24 16:24:20 summary: resolve issue 23 affected #: 2 files (93 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/33348724fd55/ changeset: r1173:33348724fd55 user: hpk date: 2009-06-24 16:35:01 summary: resolves issue 18 multiprocessing py.test co-existence add fileno() method and test DontReadFromInput redirection some more affected #: 3 files (479 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/1c7aaa8c61f3/ changeset: r1174:1c7aaa8c61f3 user: hpk date: 2009-06-24 16:57:55 summary: add py.test version to verbose reporting correctly regen setup affected #: 3 files (1.5 KB) http://www.bitbucket.org/hpk42/py-trunk/changeset/f203b8287def/ changeset: r1175:f203b8287def user: hpk date: 2009-06-24 17:13:04 summary: Added tag 1.0.0b4 for changeset 1c7aaa8c61f3 affected #: 1 file (49 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/8cd6eb91eba3/ changeset: r1176:8cd6eb91eba3 user: hpk date: 2009-06-24 17:13:54 summary: Removed tag 1.0.0b4 affected #: 1 file (98 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/a0d05417421b/ changeset: r1177:a0d05417421b user: hpk date: 2009-06-24 17:14:26 summary: Added tag 1.0.0b4 for changeset 8cd6eb91eba3 affected #: 1 file (98 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/93772595f042/ changeset: r1178:93772595f042 user: hpk date: 2009-06-24 17:23:09 summary: tag revision 1.0.0b4 for upload affected #: 3 files (10 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/b1781bbfef8f/ changeset: r1179:b1781bbfef8f user: hpk date: 2009-06-24 17:25:11 summary: Removed tag 1.0.0b4 affected #: 1 file (98 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/548548a177f3/ changeset: r1180:548548a177f3 user: hpk date: 2009-06-24 17:47:29 summary: another attempt at a full tarball affected #: 4 files (673 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/8a8203ee5eb8/ changeset: r1181:8a8203ee5eb8 user: hpk date: 2009-06-24 17:49:08 summary: merge 1.0.x release branch changes affected #: 3 files (284 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/c51fbfd5ba69/ changeset: r1182:c51fbfd5ba69 user: hpk date: 2009-06-24 18:02:41 summary: tagging 1.0.0b5 affected #: 3 files (10 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/938a880ea406/ changeset: r1183:938a880ea406 user: hpk date: 2009-06-24 18:08:43 summary: merge 1.0.x changes / fixed setup.py affected #: 3 files (2 bytes) Repository URL: http://bitbucket.org/hpk42/py-trunk/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From hpk at codespeak.net Wed Jun 24 21:22:15 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Wed, 24 Jun 2009 21:22:15 +0200 (CEST) Subject: [py-svn] r65934 - py/extradoc/talk/ep2009/pytest-advanced Message-ID: <20090624192215.C9E76169EF3@codespeak.net> Author: hpk Date: Wed Jun 24 21:22:12 2009 New Revision: 65934 Modified: py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt Log: mostly finalize the full tutorial draft Modified: py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt ============================================================================== --- py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt (original) +++ py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt Wed Jun 24 21:22:12 2009 @@ -240,19 +240,23 @@ get-going exercise (max 10 min) ============================================ -install with one of: +- install with one of: + + - ``easy_install -U py`` + - svn checkout http://codespeak.net/svn/py/trunk + - hg clone https://bitbucket.org/hpk42/py-trunk/ + - run "python setup.py" with "install" or "develop" + +- create a ``test_file.py`` with some ``test_function`` + +- execute ``py.test test_file.py`` -- easy_install "py" -- svn checkout http://codespeak.net/svn/py/trunk -- hg clone https://bitbucket.org/hpk42/py-trunk/ -- run "python setup.py" with "install" or "develop" -- create a ``test_file.py`` and run ``py.test test_file.py`` - help your neighbours! -selected options +some important basic options ============================== -among others, ``py.test --help`` yields these useful options: +among others, ``py.test --help`` shows these options: - -s - disable catching of stdout/stderr during test run. - -x/--exitfirst - exit instantly on first error or failed test. @@ -276,19 +280,18 @@ observations and the setup question ========================================== -* test values and configuration mixes with test code +* test configuration mixes with test code * importing 'app' may fail * the ``app.pkg.SomeClass`` reference may change -what if multiple test functions have similar setup, -maybe with slightly different configuration for -class instantiation? +what if multiple test functions have class setup? "unittest.py" style setup / abstract view ========================================== :: + import unittest from app.pkg import SomeClass @@ -304,27 +307,28 @@ ========================================== * **multiple methods can reuse the same setup** -* ``self.inst1`` instantiated anew for each test invocation +* ``self.inst1`` instantiated a new for each test invocation * test values / configuration mixes with test code * the ``app.pkg.SomeClass`` reference may change * **functions now group by setup/fixture code** -old-style xUnit py.test extensions +sidenote: old-style xUnit py.test extensions ========================================================== -to help with managing test state across modules, classes, -and methods, py.test invented: +py.test was the first to introduce extensions: - setup_module(module) / teardown_module(module) +- setup_module / teardown_module - setup_class(cls) / teardown_class(cls) +- setup_class / teardown_class - setup_method(self, method) / teardown_method(self, method) +- setup_method / teardown_method -but: -- even increases need for grouping of functions by setup -- test values / configuration still mix with test code -- ... mostly deprecated! +but it didn't change the issues: + +- test configuration still mixes with test code +- cements grouping of functions per-file and setup + +**not recommended anymore** meet "funcargs" - test function arguments @@ -335,57 +339,34 @@ def test_something(inst1): assert inst1.method() == "ok" -or:: - class TestGroup: - def test_other(self, inst1): - assert inst1.method() == "ok" + def test_method_equal(self, inst1, inst2): + assert inst1.method() == inst2.othermethod() observations ========================================== -* test values are simply used in test code +* test code uses already setup ``inst1`` instance * no imports or app.pkg.SomeClass references * freedom to group tests logically * multiple functions re-use same funcarg setup -How to setup the funcarg value? +How do funcarg values get setup? ========================================== -write down in test module, ``conftest.py`` (or a plugin):: - - from app.pkg import SomeClass - def pytest_funcarg__inst1(request): - return SomeClass("somevalue") +write down in test module, ``conftest.py`` or registered plugin:: -observations / notes -=================================================== + from app.pkg import SomeClass -* test value setup separated from test code + def pytest_funcarg__inst1(request): + return SomeClass("somevalue") -* funcarg setup function automatically discovered by looking - for ``pytest_funcarg__`` prefixed functions +* automatically discovered by naming convention * app specific bootstraping contained in one place * re-useable from test functions across a whole project -let's introduce a command line option -=================================================== -:: - - # ./tests/conftest.py - def pytest_addoption(parser): - parser.addoption("--val", action="store") - def pytest_funcarg_inst1(request): - return SomeClass(request.config.getvalue("val")) - -observations -=================================================== - -* test configuration separated from test code -* app bootstraps for testing in one place -* can be used from any test function anywhere ``request`` object attributes =================================================== @@ -401,54 +382,26 @@ Exercise (max 10 minutes) ========================================== -* write a new package "mypkg" +* create a new package directory "mypkg" * add mypkg/__init__ and mypkg/test_url.py * add a ``test_open(url)`` function checking if URL can be opened -* write the provider in mypkg/conftest.py +* write ``pytest_funcarg__url`` provider in mypkg/conftest.py * run your test -* optional: add a command line option "--url" - -Bonus: add more tests, play with wrongly named args, introduce options. - -invoking a test function with different values -================================================ - -:: - - def pytest_generate_tests(metafunc): - if "url" in metafunc.funcargnames: - metafunc.addcall(funcargs=dict(url="http://testrun.org")) - metafunc.addcall(funcargs=dict(url="https://codespeak.net")) - -``metafunc`` attributes and abilities -================================================ - -``metafunc.funcargnames``: set of required function arguments for given function - -``function, cls, module, config``: same as request object - -means: you can easily implement a decorator or per-module -data for specifiying argument sets. see - -deprecation sidenote on yield-generated tests -================================================ +* optional: add a second funcarg, misspell funcarg names -py.test pioneered 'yield' based testing -deprecated since 1.0 because it's a very limited form of the -new ``pytest_generate_tests()`` mechanism. - - -Calling a finalizer for a funcarg +registering a finalizer for a funcarg ============================================= -a funcarg provider can use its ``request`` object to -register a finalization call:: +:: - myfile = open(...) - request.addfinalizer(lambda: myfile.close()) + def pytest_funcarg__myfile(request): + myfile = open(...) + request.addfinalizer(lambda: myfile.close()) + return myfile +[interactive demo] Managing more complex scope setup ============================================= @@ -457,9 +410,10 @@ - some funcargs need explicit teardown - managing this correctly can be complex -py.test provides a powerful high-level helper:: +py.test provides a powerful high-level helper function:: - ``request.cached_setup(...)`` + def pytest_funcarg__arg(request): + return request.cached_setup(setup, teardown, scope) Example for caching a Database object Setup @@ -474,21 +428,90 @@ scope="session", ) -* the test code does not need to know about this optimization -* easy to change to per-module or to per-function setup/teardown! +* test code simply uses ``db`` function argument +* test code does not know about setup/teardown details +* test code does not know about caching scopes +* easy to change caching scope +* **ideal for functional testing** * **interactive demo** +the mysetup funcarg pattern +============================================ + +avoid the need to have one funcarg for each app object:: + + def pytest_funcarg__mysetup(request): + return MySetup(request) + + class MySetup: + def __init__(self, request): + self.config = request.config + + def obj1(self): + return ... + def obj2(self): + return ... + +invoking a test function with different values +================================================ + +:: + + def pytest_generate_tests(metafunc): + if "url" not in metafunc.funcargnames: + return + metafunc.addcall(funcargs={'url': "http://testrun.org"}) + metafunc.addcall(funcargs={'url': "http://xyz.net"]) + + +[interactive demo] + +``metafunc`` attributes +================================================ + +``metafunc.funcargnames``: funcarg names for given test function + +``metafunc.function``: python test function + +``metafunc.cls``: containing test class + +``metafunc.module``: test module + +``metafunc.config``: access to options and general config + +pytest_generate_tests for custom parametrization +================================================== + +see blog post: + + http://tetamap.wordpress.com/2009/05/13/parametrizing-python-tests + +or in short: + + http://tinyurl.com/ofry94 + + +deprecation sidenote on yield-generated tests +================================================ + +- py.test pioneered 'yield' test generators + +- deprecated since 1.0 because: + - limited value + - was hackish to implement + - ``pytest_generate_tests()`` is vastly superior + + summary of py.test funcargs ================================ -* test function is fully separated from test setup/bootstrap -* test function can focus on specific testing aspects -* test functions can be called multiple times +* test function separated from configuration and setup +* **cleaner, more focused test functions** +* test functions can be called with multiple parameter sets * manage simple and complex "funcarg" values * re-use test functions with generated calls -* ideal for functional testing - +* ideal for functional or system testing Break ===== @@ -500,7 +523,7 @@ http://marikaz.deviantart.com/ CC 3.0 AN-ND -Using Plugins and Extensions +Hooks and Extensions ========================================= .. image:: img/end_of_a_age_by_marikaz.jpg @@ -509,101 +532,102 @@ http://marikaz.deviantart.com/ CC 3.0 AN-ND -Historic view -========================== +py.test 1.0 hooks +=========================== + +py.test calls more than 30 hooks to implement -- 0.9.x uses conftest's for extension and configuration -- 1.0 uses "plugins" for extending and conftest.py for configuration -- we do "smooth" transition because of existing test code base +- options and configuration +- test collection +- running of tests +- reporting +- (distribution of tests) -Customizing py.test +what are hooks? =========================== -- configuration values go to conftest.py files -- write local or global plugins -- provide funcargs - -Conftest.py -=============== - -- can be put into test directory or higher up -- contains test configuration values -- specifies plugins to use -- can provide default values for command line options - -Specifying plugins -======================== - -- ``-p NAME``: load comma-separated list of plugins -- plugins are always named "pytest_NAME" and can be - anywhere in your import path +* hooks are python functions +* hook functions alway start with ``pytest_`` +* strict hook signature checking when hooks are loaded +* hooks are usually attributes of a **plugin** object +* ``pytest_funcarg_*`` providers are hooks as well -Writing a local conftest plugin -==================================== +what are plugins? +=========================== -you can write a local conftest.py based plugin:: +- ``conftest.py`` modules +- ``pytest_*`` modules or packages. - class ConftestPlugin: - def pytest_addoption(self, parser): - parser.addoption("--myworld", action="store_true") - ... +conftest.py modules +======================================= -Exercise -========================================== +- are automatically discovered +- can be put in a test directory or higher up +- can contain hook functions -* add an option for specifying url on the command line -* look at "py.test -h" +**conftest.py: project specific config and extensions** +named pytest_* plugins +============================== -other Plugin Examples -========================================= +- normal python modules or packages, normally imported +- always named "pytest_*" all lowercase +- ``-p NAME`` to load plugin(s) from the command line -- integrate collection/run of traditional unit-tests -- run functions in their own tempdir -- testing ReST documents -- running Prolog tests (old: conftest based) -- running Javascript tests (old: conftest based) -- html reporting for nightly runs (old: conftest-based) -if time permits ... -========================================= +example: implementing a new py.test.* helper +================================================= -- let's play with "pytest_unittest" plugin +:: + # ./conftest.py or pytest_myname.py + def pytest_namespace(config): + return {'hello': 'world'} -Writing Plugins (30 minutes) -============================================================================ +makes ``py.test.hello`` available with value ``'hello'`` -- test collection objects -- plugin hooks and events -- event system for custom reporting -- test collection hooks -- test running hooks -py.test collection objects -============================= +Plugin Examples +========================================= -* collectors, ``collect()`` returns list of "colitems" -* items, implement ``runtest()`` for running a test +- pytest_unittest.py: mark tests as "expected to fail" +- pytest_xfail.py: mark test as "expected to fail" with py.test.xfail -test collection tree -======================== +- pytest_pocoo.py: send failure tracebacks to pocoo paste service -* collection tree is built iteratively -* test collection starts from directories or files (via cmdline) +- pytest_monkeypatch.py: safely patch objects/environment + +- pytest_figleaf.py: generate html coverage reports +- pytest_resultlog.py: generate buildbot-friendly output -py.test.collect filesystem objects -====================================== +Basic working of py.test +============================== + +- collect tests +- (distribute tests to remote processes) +- run test +- generate test report +- (send basic report info) +- report -* Directory -* File +py.test collection objects +============================= +* collection tree is built iteratively +* test collection starts from directories or files (via cmdline) +* ``collector.collect()`` returns list of collected children +* ``item.runtest()`` runs a test +easily allows for non-python tests, examples: -always available Attributes +- pytest_restdoc.py for ReST syntax and remote URL checking + +- pytest_doctest.py for running doctests + +collection node Attributes ======================================= **parent**: a reference to the collector that produced us @@ -612,31 +636,14 @@ **config**: test configuration -Python object collectors -======================================= - -**obj** points to the underlying python object. -- **Module** -- **Class**/**Instance** -- **Generator** -- **Function** - -Exercise "collection tree" +Quick Exercise "collection tree" ==================================== -inspecting the test collection tree:: +inspect your test collection tree:: py.test --collectonly -Hooks and Events -=================== - -- Plugin hook methods implement interaction - with configuration/collection/running of tests - -- Events are called for notification purposes, - are not asked for return values. Configuration Hooks ====================== @@ -651,8 +658,8 @@ def pytest_unconfigure(self, config): """ called before test process is exited. """ -Collection Hooks -====================== +Filesystem Collection Hooks +=================================== :: def pytest_collect_file(self, path, parent): @@ -661,62 +668,42 @@ def pytest_collect_directory(self, path, parent): """ return Collection node or None. """ - def pytest_collect_recurse(self, path, parent): - """ return True/False to cause/prevent recursion. """ - Python collection hooks ===================================== :: - def pytest_pymodule_makeitem(self, modcol, name, obj): - """ return custom item/collector for a python object in a module, or None. """ + + def pytest_pycollect_makeitem(collector, name, obj): + """ return custom item/collector for a python object in a module, or None. """ function test run hooks ========================== :: - def pytest_pyfunc_call(self, pyfuncitem, args, kwargs): - """ return True if we consumed/did the call to the python function item. """ - def pytest_item_makereport(self, item, excinfo, when, outerr): - """ return ItemTestReport event for the given test outcome. """ + def pytest_runtest_setup(item): + def pytest_runtest_call(item): + def pytest_runtest_teardown(item): -Reporting hooks +reporting hooks ========================== -:: - def pytest_report_teststatus(self, event): - """ return shortletter and verbose word. """ - - def pytest_terminal_summary(self, terminalreporter): - """ add additional section in terminal summary reporting. """ -Event methods -=============== - -are only called, no interaction. - -see ``py/test/plugin/pytest_terminal.py`` -for a full selection of events. - -Warning -=============== +:: -naming changes might take place before 1.0 final + report = hook.pytest_runtest_makereport(item, call) + pytest_runtest_logreport(report) -Writing cross-project plugins -================================== -- put plugin into pytest_name.py or package -- make a "NamePlugin" class available -- release or copy to somewhere importable +Terminal reporting hooks +========================== +:: -Exercise -================================== + def pytest_report_teststatus(self, event): + """ return shortletter and verbose word. """ -* port conftest plugin to global "pytest_myapp" plugin. -* put pytest_myapp somewhere where it's importable (or release it :) -* put "pytest_plugins = 'pytest_myapp'" into your test module + def pytest_terminal_summary(self, terminalreporter): + """ add additional section in terminal summary reporting. """ -Distributed Testing (45 minutes) +Distributed Testing (30 minutes) ==================================== .. image:: img/rails_in_the_city_by_marikaz.jpg @@ -785,17 +772,16 @@ Assuming an IP address, you can now tell py.test to distribute: : - py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg + py.test -d --tx socket=IPADDRESS:8888 --rsyncdir mypkg mypkg Exercise: Move settings into conftest ======================================== -mypkg/conftest.py:: +conftest.py:: + rsyncdirs = ['.'] pytest_option_tx = ['ssh=myhost', 'socket=...'] -mypkg/conftest.py:: - rsyncdirs = ['.'] Distribution modes ======================================== @@ -819,6 +805,9 @@ * asynchronously send and receive data between processes through channels * completely avoid manual installation steps on remote places +**see my thursday morning talk during EuroPython2009 :)** + + xspecs: Exec environment specs ================================= @@ -840,6 +829,7 @@ ================================= :: + ssh=wyvern//python=python2.4//chdir=mycache popen//python=2.5//nice=20 socket=192.168.1.4:8888 From hpk at codespeak.net Wed Jun 24 21:25:54 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Wed, 24 Jun 2009 21:25:54 +0200 (CEST) Subject: [py-svn] r65935 - in py/extradoc/talk/ep2009: pytest-advanced rapid-testing Message-ID: <20090624192554.DC234169F22@codespeak.net> Author: hpk Date: Wed Jun 24 21:25:54 2009 New Revision: 65935 Added: py/extradoc/talk/ep2009/rapid-testing/ - copied from r65880, py/extradoc/talk/ep2009/pytest-advanced/ py/extradoc/talk/ep2009/rapid-testing/makepdf - copied, changed from r65909, py/extradoc/talk/ep2009/pytest-advanced/makepdf py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt (props changed) - copied unchanged from r65880, py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt py/extradoc/talk/ep2009/rapid-testing/test_addfinalizer.py - copied unchanged from r65909, py/extradoc/talk/ep2009/pytest-advanced/test_addfinalizer.py py/extradoc/talk/ep2009/rapid-testing/test_cached_setup.py - copied unchanged from r65909, py/extradoc/talk/ep2009/pytest-advanced/test_cached_setup.py py/extradoc/talk/ep2009/rapid-testing/test_generate_tests.py - copied unchanged from r65909, py/extradoc/talk/ep2009/pytest-advanced/test_generate_tests.py Removed: py/extradoc/talk/ep2009/pytest-advanced/ py/extradoc/talk/ep2009/rapid-testing/pytest-advanced.pdf py/extradoc/talk/ep2009/rapid-testing/pytest-advanced.txt Log: rename to rapid-testing Copied: py/extradoc/talk/ep2009/rapid-testing/makepdf (from r65909, py/extradoc/talk/ep2009/pytest-advanced/makepdf) ============================================================================== --- py/extradoc/talk/ep2009/pytest-advanced/makepdf (original) +++ py/extradoc/talk/ep2009/rapid-testing/makepdf Wed Jun 24 21:25:54 2009 @@ -6,7 +6,7 @@ # WARNING: to work, it needs this patch for docutils # https://sourceforge.net/tracker/?func=detail&atid=422032&aid=1459707&group_id=38414 -BASE=pytest-advanced +BASE=rapid-testing python rst2beamer.py --stylesheet=stylesheet.latex --documentoptions=13pt $BASE.txt $BASE.latex || exit sed 's/\\date{}/\\input{author.latex}/' $BASE.latex >tmpfile || exit sed 's/\\maketitle/\\input{title.latex}/' tmpfile >$BASE.latex || exit Deleted: /py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.pdf ============================================================================== Files /py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.pdf Wed Jun 24 21:25:54 2009 and (empty file) differ Deleted: /py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt ============================================================================== --- /py/extradoc/talk/ep2009/pytest-advanced/pytest-advanced.txt Wed Jun 24 21:25:54 2009 +++ (empty file) @@ -1,691 +0,0 @@ -.. include:: beamerdefs.txt -.. include:: - -================================================================= -Rapid testing with py.test -================================================================= - -Install -======================================== - -- svn checkout http://codespeak.net/svn/py/dist -- hg clone https://hpk42 at bitbucket.org/hpk42/py-trunk/ - -- run "python setup.py" with "install" or "develop" - -- if need be: easy_install "py" - - -my technical background -=========================== - -- programming since 20 years -- Python since around 2000 -- released projects: pypy, py.test/py lib, rlcompleter2 -- other: mailwitness, shpy, vadm, codespeak, ... -- merlinux GmbH since 2004 -- PyPy EU-project 2004-2007 - -my testing background -======================= - -- learned Python 2001 -- "test-driven developer" (TDD) since 2002 -- founded PyPy, based on TDD principles -- developed utest/stdtest, now py.test -- consultancies on testing - -What's **your** background? -============================== - -- what test tools do you use? -- work test-driven? -- have test-suites with >1000 tests? -- need/want to run tests on >1 platforms? -- have "non-python" tests? - -Python -================================== - -Python has no compile-time type security. - - -Testing to the rescue! -================================== - -automated tests are better than declaring types. - -The test tool question -======================================== - -what is the job of automated testing tools? - -my current answer -======================================== - -* verify that my code changes work out -* be helpful when test scenarios fail - -If failures are not helpful ... -======================================== - -improve the test tool or - -write more (different) tests - -The Automated Test question -======================================== - -what are automated tests there for? - -my current answer -======================================== - -to make sure that - -* units react well to input. -* components co-operate nicely -* code changes work out in the end - -What does "code changes work out" mean? -======================================== - -.. image:: img/rails_in_the_city_by_marikaz.jpg - :scale: 90 - :align: center - -http://marikaz.deviantart.com/ CC 3.0 AN-ND - -my current answer -======================================== - -- various operating systems -- various Python Interpreters -- cloud environments -- web browsers etc. - -Common Test terminology -============================== - -- developer and customer tests -- unit tests -- functional tests -- acceptance tests -- integration tests - -you may discuss a long time -about categorizations ... - -py.test strives to test it all -================================= - -current focus: - -- unittesting -- functional -- integration tests - -A pragmatic view on test types -================================= - -let's talk about small, medium or large tests. - -Small Tests: one aspect -============================== - -.. image:: img/small.png - :align: center - :scale: 70 - -Medium Tests: two aspects -============================== - -.. image:: img/medium.png - :align: center - :scale: 70 - -Large Tests: end-to-end -============================== - -.. image:: img/large.png - :align: center - :scale: 70 - -generally speaking -============================================ - -what is the vision of automated testing? - -my current answer -======================================== - -merge with real-life deployment. - - -Walkthrough Python test functions (30 minutes) -============================================================================ - -.. image:: img/new_color_in_dark_old_city_by_marikaz.jpg - :scale: 100 - :align: left - -http://marikaz.deviantart.com/ CC 3.0 AN-ND - -A Typical Python test layout -========================================== -:: - - app/__init__.py - ... - app/tests/test_module.py - ... - -py.test invocation: ``py.test app`` - -Another typical test layout -========================================== -:: - - app/__init__.py - tests/test_module.py - -py.test invocation: ``py.test tests``. - -Mind the the `__init__.py` files! -========================================== - -wherever your "tests" directories are, -always provide a ``__init__.py`` - -automatic test discovery -=================================== - -py.test walks over your source tree and: - -- discovers ``test_*.py`` test files -- discovers ``test_`` functions or ``Test`` classes - -**automatic discovery avoids boilerplate** - - -Typical test function - viewed abstractly -========================================== -:: - - from app.pkg import SomeClass - def test_something(): - inst1 = SomeClass("somevalue") - ... - assert "things are ok" - -observations and the setup question -========================================== - -* test values / configuration mixes with test code -* importing 'app' may fail -* the ``app.pkg.SomeClass`` reference may change - -what if multiple test functions setup the same -classes, maybe with slightly different values? - - -x-unit style setup / fixtures -========================================== - -:: - - from app.pkg import SomeClass - class TestGroup: - def setup_method(self, method): - self.inst1 = SomeClass("somevalue") - - def test_something(self): - ... use self.inst1 ... - assert "things are ok" - -observations -========================================== - -* test values / configuration mixes with test code -* importing 'app' may fail -* the ``app.pkg.SomeClass`` reference may change -* **functions now group by setup/fixture code** -* **multiple methods can reuse the same setup** - -meet "funcargs" - test function arguments -========================================== - -:: - - def test_something(inst1): - assert inst1.call() == "result" - -observations -========================================== - -* test values are simply used in test code -* no imports or app.pkg.SomeClass references here -* freedom to group tests logically -* multiple functions can re-use the same funcarg setup - -How to setup the funcarg value? -========================================== -:: - - from app.pkg import SomeClass - def pytest_funcarg__inst1(request): - return SomeClass("somevalue") - -defined in: test module, ``conftest.py`` or named plugin - -observations -=================================================== - -* funcarg provider automatically discovered -* app bootstraps for testing in one place -* funcarg can be used from different test functions - -introducing a command line option -=================================================== -:: - # ./tests/conftest.py - def pytest_addoption(parser): - parser.addoption("--val", action="store") - def pytest_funcarg_inst1(request): - return SomeClass(request.config.getvalue("val")) - -observations -=================================================== - -* test configuration separated from test code -* app bootstraps for testing in one place -* can be used from any test function anywhere - -``request`` object attributes -=================================================== - -``request.function``: python test function - -``request.cls``: containing test class - -``request.module``: test module - -``request.config``: access to options and general config - - -Exercise -========================================== - -* write a new package "mypkg" -* add mypkg/__init__ and mypkg/test_url.py -* add a test_path function that needs an "url" argument -* write the provider in mypkg/conftest.py -* run your test - -Bonus: add more tests, play with wrongly named args. - -Using Plugins and Extensions -========================================= - -.. image:: img/end_of_a_age_by_marikaz.jpg - :scale: 100 - :align: left - -http://marikaz.deviantart.com/ CC 3.0 AN-ND - -Historic view -========================== - -- 0.9.x uses conftest's for extension and configuration -- 1.0 uses "plugins" for extending and conftest.py for configuration -- we do "smooth" transition because of existing test code base - -Customizing py.test -=========================== - -- configuration values go to conftest.py files -- write local or global plugins -- provide funcargs - -Conftest.py -=============== - -- can be put into test directory or higher up -- contains test configuration values -- specifies plugins to use -- can provide default values for command line options - -Specifying plugins -======================== - -- ``-p NAME``: load comma-separated list of plugins -- plugins are always named "pytest_NAME" and can be - anywhere in your import path - -Writing a local conftest plugin -==================================== - -you can write a local conftest.py based plugin:: - - class ConftestPlugin: - def pytest_addoption(self, parser): - parser.addoption("--myworld", action="store_true") - ... - -Exercise -========================================== - -* add an option for specifying url on the command line -* look at "py.test -h" - - -other Plugin Examples -========================================= - -- integrate collection/run of traditional unit-tests -- run functions in their own tempdir -- testing ReST documents -- running Prolog tests (old: conftest based) -- running Javascript tests (old: conftest based) -- html reporting for nightly runs (old: conftest-based) - -if time permits ... -========================================= - -- let's play with "pytest_unittest" plugin - -Break -===== - -.. image:: img/flying_lady_by_marikaz.jpg - :scale: 100 - :align: left - -http://marikaz.deviantart.com/ CC 3.0 AN-ND - - -Writing Plugins (30 minutes) -============================================================================ - -- test collection objects -- plugin hooks and events -- event system for custom reporting -- test collection hooks -- test running hooks - -py.test collection objects -============================= - -* collectors, ``collect()`` returns list of "colitems" -* items, implement ``runtest()`` for running a test - -test collection tree -======================== - -- collection tree is built iteratively -- test collection starts from directories or files (via cmdline) - -py.test.collect.* filesystem objects -====================================== - -- **Directory** -- **File** - -always available Attributes -======================================= - -**parent** a reference to the collector that produced us - -**name**: a name that is unique within the scope of our parent - -**config**: test configuration - -Python object collectors -======================================= - -**obj** points to the underlying python object. - -- **Module** -- **Class**/**Instance** -- **Generator** -- **Function** - -Exercise "collection tree" -==================================== - -inspecting the test collection tree:: - - py.test --collectonly - -Hooks and Events -=================== - -- Plugin hook methods implement interaction - with configuration/collection/running of tests - -- Events are called for notification purposes, - are not asked for return values. - -Configuration Hooks -====================== -:: - - def pytest_addoption(self, parser): - """ called before commandline parsing. """ - - def pytest_configure(self, config): - """ called after command line options have been parsed. " - - def pytest_unconfigure(self, config): - """ called before test process is exited. """ - -Collection Hooks -====================== -:: - - def pytest_collect_file(self, path, parent): - """ return Collection node or None. """ - - def pytest_collect_directory(self, path, parent): - """ return Collection node or None. """ - - def pytest_collect_recurse(self, path, parent): - """ return True/False to cause/prevent recursion. """ - -Python collection hooks -===================================== -:: - def pytest_pymodule_makeitem(self, modcol, name, obj): - """ return custom item/collector for a python object in a module, or None. """ - -function test run hooks -========================== -:: - def pytest_pyfunc_call(self, pyfuncitem, args, kwargs): - """ return True if we consumed/did the call to the python function item. """ - - def pytest_item_makereport(self, item, excinfo, when, outerr): - """ return ItemTestReport event for the given test outcome. """ - -Reporting hooks -========================== -:: - def pytest_report_teststatus(self, event): - """ return shortletter and verbose word. """ - - def pytest_terminal_summary(self, terminalreporter): - """ add additional section in terminal summary reporting. """ - -Event methods -=============== - -are only called, no interaction. - -see ``py/test/plugin/pytest_terminal.py`` -for a full selection of events. - -Warning -=============== - -naming changes might take place before 1.0 final - -Writing cross-project plugins -================================== - -- put plugin into pytest_name.py or package -- make a "NamePlugin" class available -- release or copy to somewhere importable - -Exercise -================================== - -* port conftest plugin to global "pytest_myapp" plugin. -* put pytest_myapp somewhere where it's importable (or release it :) -* put "pytest_plugins = 'pytest_myapp'" into your test module - - -Distributed Testing (45 minutes) -==================================== - -.. image:: img/rails_in_the_city_by_marikaz.jpg - :scale: 100 - :align: left - -http://marikaz.deviantart.com/ CC 3.0 AN-ND - - -the basic idea -==================================== - -collect tests locally, -run tests in separated test execution processes. - -test distribution modes -==================================== - ---dist=load # load-balance tests to exec environments ---dist=each # send each test to each exec environment - -send test to one other python interpreter -================================================== - -:: - - py.test --dist=each --tx popen//python=python2.4 - -send test to three different interpreters -================================================== - -:: - - py.test --dist=each \ - --tx=popen//python=python2.4 \ - --tx=popen//python=python2.5 \ - --tx=popen//python=python2.6 - -Example: Speed up test runs -==================================== - -To send tests to multiple CPUs, type:: - - py.test --dist=load --tx popen --tx popen --tx popen - -or in short:: - - py.test -n 3 - -Especially for longer running tests or tests requiring -a lot of IO this can lead to considerable speed ups! - -send tests to remote SSH accout -======================================== - -:: - - py.test --d --tx ssh=myhostpopen --rsyncdir mypkg mypkg - -Sending tests to remote socket servers -======================================== - -Download and start - -http://codespeak.net/svn/py/dist/py/execnet/script/socketserver.py - -Assuming an IP address, you can now tell py.test to distribute: : - - py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg - -Exercise: Move settings into conftest -======================================== - -mypkg/conftest.py:: - rsyncdirs = ['.'] - pytest_option_tx = ['ssh=myhost', 'socket=...'] - -mypkg/conftest.py:: - rsyncdirs = ['.'] - -Distribution modes -======================================== - -:: - - py.test --dist=each mypkg - - py.test --dist=load mypkg - -how does py.test do all this? -======================================== - -it uses py.execnet! - -``py.execnet`` -======================================== - -* instantiates local or remote Python Processes -* send code for execution in one or many processes -* asynchronously send and receive data between processes through channels -* completely avoid manual installation steps on remote places - -xspecs: Exec environment specs -================================= - -general form:: - - key1=value1//key2=value2//key3=value3 - -xspec key meanings -================================= - -* ``popen`` for a PopenGateway -* ``ssh=host`` for a SshGateway -* ``socket=address:port`` for a SocketGateway -* ``python=executable`` for specifying Python executables -* ``chdir=path`` change remote working dir to given relative or absolute path -* ``nice=value`` decrease remote nice level if platforms supports it - -xspec examples -================================= - -:: - ssh=wyvern//python=python2.4//chdir=mycache - popen//python=2.5//nice=20 - socket=192.168.1.4:8888 - -Exercise -================================= - -* Play with runing your tests in multiple interpreters or processes -* see if you can get access to your neighbour's PC - - -Feedback round -================== - -How did you like the tutorial? - -Thanks for taking part! - -holger krekel at merlinux eu - - From py-svn at codespeak.net Wed Jun 24 22:39:56 2009 From: py-svn at codespeak.net (Nori Janise) Date: Wed, 24 Jun 2009 22:39:56 +0200 (CEST) Subject: [py-svn] License near expiration Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Thu Jun 25 17:13:21 2009 From: py-svn at codespeak.net (Sadie Racav) Date: Thu, 25 Jun 2009 17:13:21 +0200 Subject: [py-svn] Senator showed pride Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Thu Jun 25 23:22:25 2009 From: py-svn at codespeak.net (py-svn at codespeak.net) Date: Thu, 25 Jun 2009 23:22:25 +0200 (CEST) Subject: [py-svn] Your iTunes Account #558146 Message-ID: <20090625212225.0B2E6169F5C@codespeak.net> An HTML attachment was scrubbed... URL: From hpk at codespeak.net Fri Jun 26 12:48:40 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Fri, 26 Jun 2009 12:48:40 +0200 (CEST) Subject: [py-svn] r65993 - py/extradoc/talk/ep2009/execnet Message-ID: <20090626104840.DA0191684C7@codespeak.net> Author: hpk Date: Fri Jun 26 12:48:40 2009 New Revision: 65993 Added: py/extradoc/talk/ep2009/execnet/ py/extradoc/talk/ep2009/execnet/author.latex py/extradoc/talk/ep2009/execnet/beamerdefs.txt py/extradoc/talk/ep2009/execnet/execnet.txt py/extradoc/talk/ep2009/execnet/makepdf (contents, props changed) py/extradoc/talk/ep2009/execnet/rst2beamer.py (contents, props changed) py/extradoc/talk/ep2009/execnet/stylesheet.latex py/extradoc/talk/ep2009/execnet/title.latex Log: skeleton for execnet talk Added: py/extradoc/talk/ep2009/execnet/author.latex ============================================================================== --- (empty file) +++ py/extradoc/talk/ep2009/execnet/author.latex Fri Jun 26 12:48:40 2009 @@ -0,0 +1,7 @@ +\definecolor{rrblitbackground}{rgb}{0.0, 0.0, 0.0} + +\title[Disitributed programming with py.execnet] {distributed programming with py.execnet} +\author[H. Krekel]{Holger Krekel \\ http://merlinux.eu} + +\institute[EuroPython 2009, Birmingham]{EuroPython 2009, Birmingham} +\date{July 2nd, 2009} Added: py/extradoc/talk/ep2009/execnet/beamerdefs.txt ============================================================================== --- (empty file) +++ py/extradoc/talk/ep2009/execnet/beamerdefs.txt Fri Jun 26 12:48:40 2009 @@ -0,0 +1,77 @@ +.. colors +.. =========================== + +.. role:: green +.. role:: red + + +.. general useful commands +.. =========================== + +.. |pause| raw:: latex + + \pause + +.. |small| raw:: latex + + {\small + +.. |end_small| raw:: latex + + } + + +.. closed bracket +.. =========================== + +.. |>| raw:: latex + + } + + +.. example block +.. =========================== + +.. |example<| raw:: latex + + \begin{exampleblock}{ + + +.. |end_example| raw:: latex + + \end{exampleblock} + + + +.. alert block +.. =========================== + +.. |alert<| raw:: latex + + \begin{alertblock}{ + + +.. |end_alert| raw:: latex + + \end{alertblock} + + + +.. columns +.. =========================== + +.. |column1| raw:: latex + + \begin{columns} + \begin{column}{0.45\textwidth} + +.. |column2| raw:: latex + + \end{column} + \begin{column}{0.45\textwidth} + + +.. |end_columns| raw:: latex + + \end{column} + \end{columns} Added: py/extradoc/talk/ep2009/execnet/execnet.txt ============================================================================== --- (empty file) +++ py/extradoc/talk/ep2009/execnet/execnet.txt Fri Jun 26 12:48:40 2009 @@ -0,0 +1,87 @@ +.. include:: beamerdefs.txt +.. include:: + +================================================================= +Distributed Programming with py.execnet +================================================================= + +my technical background +=========================== + +- programming since 20 years +- Python since around 2000 +- released projects: pypy, py.test/py lib, rlcompleter2 +- other: mailwitness, shpy, vadm, codespeak, ... +- merlinux GmbH since 2004 +- PyPy EU-project 2004-2007 + +my distributed systems background +============================================= + +- Adaptive Communication Environment (ACE) +- Corba ORB TAO +- Corba Transaction Service (XOTS) +- automated system maintenance (service hosting) +- py.test distribution of tests +- consultancies on distributed systems + + +This talk +======================================== + +* intro to py.execnet, examples, demos + +* comparison with multiprocessing and RMI + +* discussion about challenges / promises of + execnet-style deployment models + + +Intro to py.execnet +============================ + +execnet is about ad-hoc deployment of code + +The basic model +============================ + + + + +Example +============================ + +:: + + >>> gw = py.execnet.PopenGateway() + >>> channel = gw.remote_exec("import os ; channel.send(os.getpid())") + >>> pid = channel.getpid() + >>> assert pid != os.getpid() + + +Data protocol +===================== + +chat/text based protocols + +examples: SMTP, HTTP, DNS, ... + +**allows many implementation (languages)** + +Data format +=================== + +open or reverse engeneered specifications + +pdf, xml, html, ODS, doc, ... + +**allows many implementation (languages)** + +**agnostic to transport mechanism** + +Remote method invocation +============================== + +- SOAP/XMLRPC + + Added: py/extradoc/talk/ep2009/execnet/makepdf ============================================================================== --- (empty file) +++ py/extradoc/talk/ep2009/execnet/makepdf Fri Jun 26 12:48:40 2009 @@ -0,0 +1,14 @@ +#!/bin/bash + +# you can find rst2beamer.py here: +# http://codespeak.net/svn/user/antocuni/bin/rst2beamer.py + +# WARNING: to work, it needs this patch for docutils +# https://sourceforge.net/tracker/?func=detail&atid=422032&aid=1459707&group_id=38414 + +BASE=execnet +python rst2beamer.py --stylesheet=stylesheet.latex --documentoptions=13pt $BASE.txt $BASE.latex || exit +sed 's/\\date{}/\\input{author.latex}/' $BASE.latex >tmpfile || exit +sed 's/\\maketitle/\\input{title.latex}/' tmpfile >$BASE.latex || exit +pdflatex $BASE.latex || exit + Added: py/extradoc/talk/ep2009/execnet/rst2beamer.py ============================================================================== --- (empty file) +++ py/extradoc/talk/ep2009/execnet/rst2beamer.py Fri Jun 26 12:48:40 2009 @@ -0,0 +1,170 @@ +#!/usr/bin/env python +# encoding: utf-8 +""" +A docutils script converting restructured text into Beamer-flavoured LaTeX. + +Beamer is a LaTeX document class for presentations. Via this script, ReST can +be used to prepare slides. It can be called:: + + rst2beamer.py infile.txt > outfile.tex + +where ``infile.tex`` contains the produced Beamer LaTeX. + +See for more details. + +""" +# TODO: modifications for handout sections? +# TOOD: sections and subsections? +# TODO: enable beamer themes? +# TODO: convert document metadata to front page fields? +# TODO: toc-conversion? +# TODO: fix descriptions + +# Unless otherwise stated, created by P-M Agapow on 2007-08-21 +# and open for academic & non-commercial use and modification . + +__docformat__ = 'restructuredtext en' +__author__ = "Paul-Michael Agapow " +__version__ = "0.2" + + +### IMPORTS ### + +import locale +from docutils.core import publish_cmdline, default_description +from docutils.writers.latex2e import Writer as Latex2eWriter +from docutils.writers.latex2e import LaTeXTranslator, DocumentClass +from docutils import nodes + +## CONSTANTS & DEFINES: ### + +BEAMER_SPEC = ( + 'Beamer options', + 'These are derived almost entirely from the LaTeX2e options', + tuple ( + [ + ( + 'Specify theme.', + ['--theme'], + {'default': '', } + ), + ( + 'Specify document options. Multiple options can be given, ' + 'separated by commas. Default is "10pt,a4paper".', + ['--documentoptions'], + {'default': '', } + ), + ] + list (Latex2eWriter.settings_spec[2][2:]) + ), +) + +BEAMER_DEFAULTS = { + 'output_encoding': 'latin-1', + 'documentclass': 'beamer', +} + + +### IMPLEMENTATION ### + +try: + locale.setlocale (locale.LC_ALL, '') +except: + pass + +class BeamerTranslator (LaTeXTranslator): + """ + A converter for docutils elements to beamer-flavoured latex. + """ + + def __init__ (self, document): + LaTeXTranslator.__init__ (self, document) + self.head_prefix = [x for x in self.head_prefix if ('{typearea}' not in x)] + hyperref_posn = [i for i in range (len (self.head_prefix)) if ('{hyperref}' in self.head_prefix[i])] + self.head_prefix[hyperref_posn[0]] = '\\usepackage{hyperref}\n' + self.head_prefix.extend ([ + '\\definecolor{rrblitbackground}{rgb}{0.55, 0.3, 0.1}\n', + '\\newenvironment{rtbliteral}{\n', + '\\begin{ttfamily}\n', + '\\color{rrblitbackground}\n', + '}{\n', + '\\end{ttfamily}\n', + '}\n', + ]) + # this fixes the hardcoded section titles in docutils 0.4 + self.d_class = DocumentClass ('article') + + def begin_frametag (self): + return '\\begin{frame}\n' + + def end_frametag (self): + return '\\end{frame}\n' + + def visit_section (self, node): + if (self.section_level == 0): + self.body.append (self.begin_frametag()) + LaTeXTranslator.visit_section (self, node) + + def depart_section (self, node): + # Remove counter for potential subsections: + LaTeXTranslator.depart_section (self, node) + if (self.section_level == 0): + self.body.append (self.end_frametag()) + + def visit_title (self, node): + if (self.section_level == 1): + self.body.append ('\\frametitle{%s}\n\n' % self.encode(node.astext())) + raise nodes.SkipNode + else: + LaTeXTranslator.visit_title (self, node) + + def depart_title (self, node): + if (self.section_level != 1): + LaTeXTranslator.depart_title (self, node) + + def visit_literal_block(self, node): + if not self.active_table.is_open(): + self.body.append('\n\n\\smallskip\n\\begin{rtbliteral}\n') + self.context.append('\\end{rtbliteral}\n\\smallskip\n\n') + else: + self.body.append('\n') + self.context.append('\n') + if (self.settings.use_verbatim_when_possible and (len(node) == 1) + # in case of a parsed-literal containing just a "**bold**" word: + and isinstance(node[0], nodes.Text)): + self.verbatim = 1 + self.body.append('\\begin{verbatim}\n') + else: + self.literal_block = 1 + self.insert_none_breaking_blanks = 1 + + def depart_literal_block(self, node): + if self.verbatim: + self.body.append('\n\\end{verbatim}\n') + self.verbatim = 0 + else: + self.body.append('\n') + self.insert_none_breaking_blanks = 0 + self.literal_block = 0 + self.body.append(self.context.pop()) + + +class BeamerWriter (Latex2eWriter): + """ + A docutils writer that modifies the translator and settings for beamer. + """ + settings_spec = BEAMER_SPEC + settings_defaults = BEAMER_DEFAULTS + + def __init__(self): + Latex2eWriter.__init__(self) + self.translator_class = BeamerTranslator + + +if __name__ == '__main__': + description = ( + "Generates Beamer-flavoured LaTeX for PDF-based presentations." + default_description) + publish_cmdline (writer=BeamerWriter(), description=description) + + +### END ###################################################################### + Added: py/extradoc/talk/ep2009/execnet/stylesheet.latex ============================================================================== --- (empty file) +++ py/extradoc/talk/ep2009/execnet/stylesheet.latex Fri Jun 26 12:48:40 2009 @@ -0,0 +1,10 @@ +\usetheme{Boadilla} +\setbeamercovered{transparent} +\setbeamertemplate{navigation symbols}{} + +\definecolor{darkgreen}{rgb}{0, 0.5, 0.0} +\newcommand{\docutilsrolegreen}[1]{\color{darkgreen}#1\normalcolor} +\newcommand{\docutilsrolered}[1]{\color{red}#1\normalcolor} + +\newcommand{\green}[1]{\color{darkgreen}#1\normalcolor} +\newcommand{\red}[1]{\color{red}#1\normalcolor} Added: py/extradoc/talk/ep2009/execnet/title.latex ============================================================================== --- (empty file) +++ py/extradoc/talk/ep2009/execnet/title.latex Fri Jun 26 12:48:40 2009 @@ -0,0 +1,7 @@ +\begin{titlepage} +\begin{figure}[h] +\includegraphics[width=64px,height=64px]{img/merlinux-logo.jpg} +\qquad +\includegraphics[width=80px]{img/py-web.png} +\end{figure} +\end{titlepage} From hpk at codespeak.net Fri Jun 26 12:57:04 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Fri, 26 Jun 2009 12:57:04 +0200 (CEST) Subject: [py-svn] r65994 - py/extradoc/talk/ep2009/execnet Message-ID: <20090626105704.D26A31684DB@codespeak.net> Author: hpk Date: Fri Jun 26 12:57:04 2009 New Revision: 65994 Added: py/extradoc/talk/ep2009/execnet/model1.graffle py/extradoc/talk/ep2009/execnet/model1.png (contents, props changed) Modified: py/extradoc/talk/ep2009/execnet/execnet.txt Log: adding a graphic Modified: py/extradoc/talk/ep2009/execnet/execnet.txt ============================================================================== --- py/extradoc/talk/ep2009/execnet/execnet.txt (original) +++ py/extradoc/talk/ep2009/execnet/execnet.txt Fri Jun 26 12:57:04 2009 @@ -45,6 +45,9 @@ The basic model ============================ +.. image:: model1.png + :scale: 90 + :align: center Added: py/extradoc/talk/ep2009/execnet/model1.graffle ============================================================================== --- (empty file) +++ py/extradoc/talk/ep2009/execnet/model1.graffle Fri Jun 26 12:57:04 2009 @@ -0,0 +1,636 @@ + + + + + ActiveLayerIndex + 0 + ApplicationVersion + + com.omnigroup.OmniGraffle + 129.22 + + AutoAdjust + + CanvasColor + + w + 1 + + CanvasOrigin + {0, 0} + CanvasScale + 1 + ColumnAlign + 1 + ColumnSpacing + 36 + CreationDate + 2009-06-26 12:14:11 +0200 + Creator + holger krekel + DisplayScale + 1 cm = 1 cm + GraphDocumentVersion + 5 + GraphicsList + + + Class + LineGraphic + Head + + ID + 46 + Info + 2 + + ID + 55 + Points + + {302.863, 112.386} + {394.37, 162.425} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 51 + + + + Class + LineGraphic + Head + + ID + 1 + Info + 2 + + ID + 54 + Points + + {255.688, 112.386} + {158.386, 162.425} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + 0 + + + Tail + + ID + 51 + + + + Class + LineGraphic + Head + + ID + 46 + Info + 4 + + ID + 49 + Points + + {189.213, 180.425} + {283.465, 170.079} + {363.543, 180.425} + + Style + + stroke + + HeadArrow + FilledArrow + LineType + 1 + TailArrow + FilledArrow + + + Tail + + ID + 1 + Info + 3 + + + + Bounds + {{363.543, 162.425}, {61.6536, 36}} + Class + ShapedGraphic + ID + 46 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + RoundRect + Text + + Text + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf480 +{\fonttbl\f0\fswiss\fcharset77 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Channel} + + + + Bounds + {{127.559, 162.425}, {61.6535, 36}} + Class + ShapedGraphic + ID + 1 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + RoundRect + Text + + Text + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf480 +{\fonttbl\f0\fswiss\fcharset77 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Channel} + + + + Class + LineGraphic + Head + + ID + 41 + Info + 4 + + ID + 39 + Points + + {198.425, 95.3858} + {363.543, 95.3858} + + Style + + stroke + + HeadArrow + FilledDoubleArrow + LineType + 1 + Pattern + 2 + TailArrow + 0 + + + Tail + + ID + 44 + Info + 3 + + + + Bounds + {{363.543, 77.3858}, {90, 36}} + Class + ShapedGraphic + ID + 41 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + RoundRect + Text + + Text + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf480 +{\fonttbl\f0\fswiss\fcharset77 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Gateway} + + + + Bounds + {{354.331, 56.6929}, {99.2126, 184.252}} + Class + ShapedGraphic + ID + 42 + Shape + Rectangle + TextPlacement + 0 + + + Bounds + {{108.425, 77.3858}, {90, 36}} + Class + ShapedGraphic + ID + 44 + Magnets + + {0, 1} + {0, -1} + {1, 0} + {-1, 0} + + Shape + RoundRect + Text + + Text + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf480 +{\fonttbl\f0\fswiss\fcharset77 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc + +\f0\fs20 \cf0 Gateway} + + + + Bounds + {{99.2126, 56.6929}, {113.386, 184.252}} + Class + ShapedGraphic + FitText + Clip + Flow + Clip + ID + 45 + Shape + Rectangle + TextPlacement + 0 + + + Bounds + {{256.237, 136.108}, {61, 68}} + Class + ShapedGraphic + FitText + YES + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 14 + + ID + 50 + Line + + ID + 49 + Position + 0.55674099922180176 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf480 +{\fonttbl\f0\fswiss\fcharset77 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural + +\f0\fs28 \cf0 \ +Send\ +Receive\ +data} + + + + Bounds + {{226, 78.3858}, {108, 34}} + Class + ShapedGraphic + FitText + YES + FontInfo + + Color + + w + 0 + + Font + Helvetica + Size + 14 + + ID + 51 + Line + + ID + 39 + Position + 0.49403908848762512 + RotationType + 0 + + Shape + Rectangle + Style + + shadow + + Draws + NO + + stroke + + Draws + NO + + + Text + + Text + {\rtf1\mac\ansicpg10000\cocoartf824\cocoasubrtf480 +{\fonttbl\f0\fswiss\fcharset77 Helvetica;} +{\colortbl;\red255\green255\blue255;} +\pard\tx560\tx1120\tx1680\tx2240\tx2800\tx3360\tx3920\tx4480\tx5040\tx5600\tx6160\tx6720\qc\pardirnatural + +\f0\fs28 \cf0 remote-execute\ +code} + + + + GridInfo + + GridSpacing + 14.17322826385498 + MajorGridSpacing + 10 + ShowsGrid + YES + SnapsToGrid + YES + + GuidesLocked + NO + GuidesVisible + YES + HPages + 1 + ImageCounter + 1 + IsPalette + NO + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + LinksVisible + NO + MagnetsVisible + NO + MasterSheet + Master 1 + MasterSheets + + + ActiveLayerIndex + 0 + AutoAdjust + + CanvasColor + + w + 1 + + CanvasOrigin + {0, 0} + CanvasScale + 1 + ColumnAlign + 1 + ColumnSpacing + 36 + DisplayScale + 1 cm = 1 cm + GraphicsList + + GridInfo + + GridSpacing + 14.17322826385498 + MajorGridSpacing + 10 + ShowsGrid + YES + SnapsToGrid + YES + + HPages + 1 + IsPalette + NO + KeepToScale + + Layers + + + Lock + NO + Name + Layer 1 + Print + YES + View + YES + + + LayoutInfo + + Orientation + 2 + OutlineStyle + Basic + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Master 1 + UniqueID + 1 + VPages + 1 + + + ModificationDate + 2009-06-26 12:50:42 +0200 + Modifier + holger krekel + NotesVisible + NO + Orientation + 2 + OriginVisible + NO + OutlineStyle + Basic + PageBreaks + YES + PrintInfo + + NSBottomMargin + + float + 0 + + NSLeftMargin + + float + 0 + + NSPaperName + + string + iso-a4 + + NSPaperSize + + size + {595, 842} + + NSRightMargin + + float + 0 + + NSTopMargin + + float + 0 + + + ReadOnly + NO + RowAlign + 1 + RowSpacing + 36 + SheetTitle + Canvas 1 + SmartAlignmentGuidesActive + NO + SmartDistanceGuidesActive + NO + UniqueID + 1 + UseEntirePage + + VPages + 1 + WindowInfo + + CurrentSheet + 0 + DrawerOpen + + DrawerTab + Outline + DrawerWidth + 209 + Frame + {{1499, 82}, {574, 742}} + VisibleRegion + {{0, 0}, {559, 628}} + Zoom + 1 + + + Added: py/extradoc/talk/ep2009/execnet/model1.png ============================================================================== Binary file. No diff available. From hpk at codespeak.net Fri Jun 26 13:02:19 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Fri, 26 Jun 2009 13:02:19 +0200 (CEST) Subject: [py-svn] r65995 - py/extradoc/talk/ep2009/execnet/img Message-ID: <20090626110219.4D5461684DB@codespeak.net> Author: hpk Date: Fri Jun 26 13:02:18 2009 New Revision: 65995 Added: py/extradoc/talk/ep2009/execnet/img/ py/extradoc/talk/ep2009/execnet/img/merlinux-logo.jpg (contents, props changed) py/extradoc/talk/ep2009/execnet/img/py-web.png (contents, props changed) Log: also adding missing images Added: py/extradoc/talk/ep2009/execnet/img/merlinux-logo.jpg ============================================================================== Binary file. No diff available. Added: py/extradoc/talk/ep2009/execnet/img/py-web.png ============================================================================== Binary file. No diff available. From py-svn at codespeak.net Fri Jun 26 19:59:42 2009 From: py-svn at codespeak.net (Donn Azqsocjk) Date: Fri, 26 Jun 2009 19:59:42 +05-30 Subject: [py-svn] Your membership expired Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Fri Jun 26 17:35:02 2009 From: py-svn at codespeak.net (Pfizer Inc.) Date: Fri, 26 Jun 2009 17:35:02 +0200 (CEST) Subject: [py-svn] Dear py-svn@codespeak.net 26.6.2009 82% 0FF Message-ID: <019501c9f683$ec74dc40$cdaade54@1043442803166> An HTML attachment was scrubbed... URL: From hpk at codespeak.net Fri Jun 26 18:59:50 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Fri, 26 Jun 2009 18:59:50 +0200 (CEST) Subject: [py-svn] r65998 - py/extradoc/talk/ep2009/execnet Message-ID: <20090626165950.68F0716854B@codespeak.net> Author: hpk Date: Fri Jun 26 18:59:47 2009 New Revision: 65998 Modified: py/extradoc/talk/ep2009/execnet/execnet.txt Log: snapshot of execnet talk Modified: py/extradoc/talk/ep2009/execnet/execnet.txt ============================================================================== --- py/extradoc/talk/ep2009/execnet/execnet.txt (original) +++ py/extradoc/talk/ep2009/execnet/execnet.txt Fri Jun 26 18:59:47 2009 @@ -1,5 +1,5 @@ -.. include:: beamerdefs.txt -.. include:: +.. .. include:: beamerdefs.txt +.. .. include:: ================================================================= Distributed Programming with py.execnet @@ -40,26 +40,261 @@ Intro to py.execnet ============================ -execnet is about ad-hoc deployment of code +execnet does: -The basic model -============================ +1. instantiating remote Python processes +2. sending code for remote execution +3. send/receive of data between remote and local code +4. (closing down remote process) + +The basic model / terminology +=============================== .. image:: model1.png - :scale: 90 + :scale: 100 :align: center - - -Example +Interactive Example ============================ :: >>> gw = py.execnet.PopenGateway() - >>> channel = gw.remote_exec("import os ; channel.send(os.getpid())") - >>> pid = channel.getpid() - >>> assert pid != os.getpid() + >>> src = "import os ; channel.send(os.getpid())" + >>> channel = gw.remote_exec(src) + >>> remote_pid = channel.getpid() + + +gateway's remote_exec +================================== + +:: + + def remote_exec(source): + """return channel object for communicating with the + asynchronously executing 'source' code which will + have a corresponding 'channel' object in its + executing namespace.""" + +Channel objects, send() and receive() +======================================== + +symmetric send/receive, example:: + + >>> src = "channel.send(channel.receive() + 1 )" + >>> ch = gw.remote_exec(src) + >>> ch.send(41) + >>> result = ch.receive() + +**channels work with builtin basic python data structures**: +dictionaries, lists, tuples, numbers, floats, ... + +Channel objects, waitclose() +===================================== + +waiting for creation of a file:: + + >>> ch = gw.remote_exec(""" + filename, content = channel.receive() + f = open(filename, 'w') + f.write(content) + f.close() + """) # channel.close() is called automatically + >>> ch.send(("hello.txt", "world") + >>> ch.waitclose() + +sync-receive data from sub processes +============================================= + +Use MultiChannels for receiving multiple results from remote code:: + + >>> from py.execnet import PopenGateway, MultiChannel + >>> src = "import os ; channel.send(os.getpid())" + >>> ch1 = PopenGateway().remote_exec(src) + >>> ch2 = PopenGateway().remote_exec(src) + >>> mch = MultiChannel([ch1, ch2]) + >>> l = mch.receive_each() + >>> assert len(l) == 2 + +async-receive data from sub processes +=============================================== + +Use ``MultiChannel.make_receive_queue()`` for asynchronously receiving +data from remote code. The returned queue yield ``(channel, result)`` +tuples allowing to determine where a result comes from:: + + >>> ch1 = PopenGateway().remote_exec("channel.send(1)") + >>> ch2 = PopenGateway().remote_exec("channel.send(2)") + >>> mch = MultiChannel([ch1, ch2]) + >>> queue = mch.make_receive_queue() + >>> chan1, res1 = queue.get() # optional timeout + >>> chan2, res2 = queue.get() + >>> res1 + res2 + 3 + +remote_exec is single threaded by default +======================================================== + +:: + + ch1 = gw.remote_exec("channel.send(channel.receive()+1)") + ch2 = gw.remote_exec("channel.send(channel.receive()+1)") + ch2.send(1) + ch2.receive() # XXX BLOCKS of single remote thread ! + + +concurrent remote execution +======================================================== + +:: + + gw.remote_init_threads(num=2) + ch1 = gw.remote_exec("channel.send(channel.receive()+1)") + ch2 = gw.remote_exec("channel.send(channel.receive()+1)") + ch2.send(1) + ch2.receive() # now works! + ch1.send(3) + ch1.receive() + +[quick look into remote_init_threads implementation] + + +Gateway / process instantiation +======================================= + +currently three gateway types: + +* PopenGateway opens a local Subprocess + +* SshGateway opens a ssh-mediated remote subprocess + +* SocketGateway opens a socket-mediated remote subprocess + +Gateways can cross Python versions, for example: +a Python-2.4 process can connect with a Python-2.6 subprocess + + +SshGateway +======================================= + +- uses underlying "ssh" command line tool + +- configurable: identity, ssh_config file, + remote python interpreter + +- **zero-install ad-hoc deployment**: + + 1. instantiate (remote) python interpreter process + 2. send dispatcher bootstrap code + 3. dispatcher waits for remote_exec commands, + manages channel communication + +SocketGateway +======================================= + +- uses standard socket connections +- needs a simple socket listen/accept/read loop on the remote side + +- **zero-install deployment**: + +1. **establish connection to existing remote-python interpreter process** +2. send it bootstrap code that installs a gateway dispatcher +3. dispatcher executes remote_exec commands, manages channel communication + + +XSpecs and py.execnet.makegateway() +=============================================== + +1.0 introduces a string-scheme for specifying gateways:: + + key1[=value1]//key2[=value2]//... + +Examples:: + + popen//python=2.5//nice=20 + socket=192.168.1.4:8888 + ssh=wyvern//python=python2.4//chdir=mycache + +so you can do:: + + >>> gateway = py.execnet.makegateway(xspec) + +e.g. used by ``py.test --tx ssh=remote_host``. + + +zero-install / rsyncing dependencies +======================================= + +**zero-install** means: no prior remote installation of code + +use ``py.execnet.RSync`` to transfer dependencies:: + + >>> rsyncer = py.execnet.RSync("sourcedir") + >>> gw = py.execnet.SshGateway("codespeak.net") + >>> rsyncer.addtarget(gw) + >>> rsyncer.send() + + +detecting if we are run through remote_exec +============================================= + +:: + + if __name__ == '__channelexec__': + # we will also have a 'channel' object + + +real life: system admin examples +=================================== + +- getting resource information from multiple remote linux machines + +- hot-syncing an SVN repository + +- py.test test-distribution + +- (other scripting) + +(demo + source code looking) + + +Execnet Summary +=============================== + + +* ad-hoc instantiate remote Python processes, represent as ``Gateway`` + +* send and execute code remotely via ``gateway.remote_exec()`` + +* communication through symmetric ``channel.receive()/channel.send()`` + +-> easy deployment and communication model + +multiprocessing +============================================ + +- part of standard lib since CPython 2.6 +- tries to make thread/process distintinction transparent +- needs **pickling** to work for resources to send/receive +- recommends to use Pipes (one-to-one) and Queues (many-to-many) for communication +- (also recommends to keep shared state minimal) + +XXX +multiprocessing and ssh +============================== + +- requires compatible "multiprocessing" package on the "remote" side +- requires to-be-executed code to pre-exist/be importable on other side +- multiprocessing.distributing helps with dispatching/managing cluster tasks +- Python version agnostic? there are backported multiprocessing packages + +XXX + + + +Pyro / RMI +=========== + Data protocol @@ -85,6 +320,5 @@ Remote method invocation ============================== -- SOAP/XMLRPC From py-svn at codespeak.net Sat Jun 27 04:12:22 2009 From: py-svn at codespeak.net (Marcelle Qufopyc) Date: Sat, 27 Jun 2009 09:12:22 +0700 Subject: [py-svn] Your membership expired Message-ID: <9268GR.5296ABA.51307990290QVLQZZUNAJHKUMJ1491@[123.24.128.139]> An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Sat Jun 27 16:15:00 2009 From: py-svn at codespeak.net (Glynis Qkjcez) Date: Sat, 27 Jun 2009 11:15:00 -0300 Subject: [py-svn] Interesting facts Message-ID: <6793MZA.4585D5206.5035044240EJAOWCYITSKWVQQ56@189-18-76-151.dsl.telesp.net.br> An HTML attachment was scrubbed... URL: From hpk at codespeak.net Sun Jun 28 11:51:43 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Sun, 28 Jun 2009 11:51:43 +0200 (CEST) Subject: [py-svn] r66011 - in py/trunk: . doc doc/announce doc/test py py/execnet py/execnet/testing py/io py/io/testing py/test py/test/plugin py/test/testing Message-ID: <20090628095143.9EF0C168478@codespeak.net> Author: hpk Date: Sun Jun 28 11:51:40 2009 New Revision: 66011 Removed: py/trunk/TODO.txt Modified: py/trunk/CHANGELOG py/trunk/MANIFEST py/trunk/README.txt (contents, props changed) py/trunk/doc/announce/release-1.0.0.txt py/trunk/doc/download.txt py/trunk/doc/execnet.txt py/trunk/doc/test/funcargs.txt py/trunk/py/__init__.py py/trunk/py/execnet/gateway.py py/trunk/py/execnet/testing/test_gateway.py py/trunk/py/io/stdcapture.py py/trunk/py/io/testing/test_stdcapture.py py/trunk/py/test/collect.py py/trunk/py/test/funcargs.py py/trunk/py/test/plugin/pytest_runner.py py/trunk/py/test/plugin/pytest_terminal.py py/trunk/py/test/pycollect.py py/trunk/py/test/testing/test_funcargs.py py/trunk/py/test/testing/test_genitems.py py/trunk/setup.py Log: updating trunk Modified: py/trunk/CHANGELOG ============================================================================== --- py/trunk/CHANGELOG (original) +++ py/trunk/CHANGELOG Sun Jun 28 11:51:40 2009 @@ -1,4 +1,21 @@ -$Id$ +Changes between 1.0.0b3 and 1.0.0 +============================================= + +* 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 ============================================= Modified: py/trunk/MANIFEST ============================================================================== --- py/trunk/MANIFEST (original) +++ py/trunk/MANIFEST Sun Jun 28 11:51:40 2009 @@ -1,9 +1,11 @@ +MANIFEST +py/__init__.py +setup.py .hgignore +.hgtags CHANGELOG LICENSE -MANIFEST README.txt -TODO.txt _findpy.py doc/announce/release-0.9.0.txt doc/announce/release-0.9.2.txt @@ -59,7 +61,6 @@ example/pytest/test_setup_flow_example.py ez_setup.py py/LICENSE -py/__init__.py py/_com.py py/bin/_findpy.py py/bin/_genscripts.py @@ -382,5 +383,4 @@ py/xmlobj/testing/test_html.py py/xmlobj/testing/test_xml.py py/xmlobj/visit.py -py/xmlobj/xml.py -setup.py \ No newline at end of file +py/xmlobj/xml.py \ No newline at end of file Modified: py/trunk/README.txt ============================================================================== --- py/trunk/README.txt (original) +++ py/trunk/README.txt Sun Jun 28 11:51:40 2009 @@ -17,4 +17,3 @@ holger krekel, holger at merlinux eu -$Id$ Deleted: /py/trunk/TODO.txt ============================================================================== --- /py/trunk/TODO.txt Sun Jun 28 11:51:40 2009 +++ (empty file) @@ -1,95 +0,0 @@ -Things to do for 1.0.0 -========================= - -py.test --------------- - -- clarify setup/run events and runner.py versus pytest_runner.py, - introduce a general pytest_item_setup(item, setupstate) - and always isolate py._com.comregistry when py lib's own tests are run - -- hook review and hook docs - -- turn deprecation / apiwarnings into events, report them at the end? - -- nightly test runs on multiple platforms - -py.execnet --------------- - -- cross-python version (2.2/2.3-2.5/6) and cross-platform testing of - setup/teardown semantics - -py.test apigen plugin ---------------------------- - -- make it work again with the new plugin arch - -packaging / svn-mercurial interaction --------------------------------------------- - -- decide if to go with or without setuptools, check windows 2.6 - availability - -- open a mercurial branch for releases? - -- write a script to dumb-bridge the mercurial repo to svn - (i.e. forget about svn history) - -1.1 and beyond -================================= - -refactorings ------------------- - -- refine doctests usage (particularly skips of doctests if - some imports/conditions are not satisfied) - - check if it works on win32 - - refine error reporting (don't show python tracebacks) - -- py.log: unify API, possibly deprecate duplicate ones, - base things on a Config object (hte latter almost a feature though) - (M988) - -- see to teardown more eagerly - -features --------------- - -- (Harald Armin Massa): make py2exe work with py lib - -- optimize file checking with --looponfailing (harald has code for win32) - -- have a py.test scan/run database for results and test names - etc. (to allow quicker selection of tests and post-run - information on failures etc.) (M760) - -- have config options from environment, command line or conftest's - - have py/doc/config/"OPTNAME".txt for each option pypy-style - - py.test --showconfig shows current configuration according - to envvars, cmdlineopts and conftests considered for your dir location. - - py.test --help-conftest lists all possible environment envs - py.test --help-env lists all possible environment envs - -- consider features of py.apigen (recheck closed "M1016") - -- integrate rlcompleter2 (make it remotely workable) - and maybe integrate with "pdb" / pdbplus (M975) - -- integrate native collecting of unittest.py tests from py.test - (along the PyPy lib-python tests) (M987) - -- provide an automated conversion script helper for converting - unittest.py based tests to py.test ones. (M987) - -- references from ReST docs to modules, functions and classes - of apigen generated html docs (M960) - -- py.test.pdb - there is my hack for a while now, which integrates - rlcompleter2 with pdb. First of all it requires some strange changes - to rlcompleter itself, which has no tests. Long-term plan would be - to have pyrepl+rlcompleter2+pdb fixes integrated into pylib and - have it tested. This requires work though. Modified: py/trunk/doc/announce/release-1.0.0.txt ============================================================================== --- py/trunk/doc/announce/release-1.0.0.txt (original) +++ py/trunk/doc/announce/release-1.0.0.txt Sun Jun 28 11:51:40 2009 @@ -1,22 +1,53 @@ -py lib 1.0.0: distributed testing and dynamic code deployment -=============================================================== +py.test / py lib 1.0.0: new test plugins, funcargs and cleanups +============================================================================ -XXX draft +Welcome to the 1.0 release bringing new flexibility and +power to testing with Python. Main news: -Welcome to the 1.0.0 py lib release - a python library aiming -to support agile and test-driven development. +* improved test architecture, featuring super-simple project + specific or cross-project single-file plugins, e.g: -It works with Linux, OSX and Win32, on Python 2.3, 2.4, 2.5 and 2.6. + * pytest_unittest.py: run traditional unittest.py tests + * pytest_xfail.py: mark tests as "expected to fail" + * pytest_pocoo.py: automatically send tracebacks to pocoo paste service + * pytest_monkeypatch.py: safely patch parts of your environment in a test function + * pytest_figleaf.py: generate html coverage reports + * pytest_resultlog.py: generate buildbot-friendly output -Main API/Tool Features: + and many more! + +* funcargs - bringing new flexibilty and zero-boilerplate to Python testing: + + - cleanly separated test code and test configuration and test value setup + - ideal for integration and functional tests + - new generative tests -> deprecation of yield-generated tests + +* distributed testing and distributed execution (py.execnet): + + - new unified "TX" URL scheme for specifying remote resources + - new sync/async ways to handle multiple remote processes + - much improved documentation + + +See the py.test documentation for more info: + + http://pytest.org + +The py lib also got smaller and focuses on offering much of the +well-tested py.test code in independent namespaces: -* py.test: cross-project testing tool with many advanced features * py.execnet: ad-hoc code distribution to SSH, Socket and local sub processes -* py.code: support for dynamically running and debugging python code +* py.code: higher-level introspection and dynamic generation of python code * py.path: path abstractions over local and subversion files -Download/Install: http://codespeak.net/py/1.0.0/download.html -Documentation/API: http://codespeak.net/py/1.0.0/index.html +Some non-strictly-test related code, notably greenlets/co-routines +and apigen now live on their own and have been removed, also simplifying +the installation procedures. + +The whole package works well with Linux, OSX and Win32, on +Python 2.3, 2.4, 2.5 and 2.6. (Expect Python3 compatibility soon!) + +Download/Install: http://codespeak.net/py/dist/download.html best, holger Modified: py/trunk/doc/download.txt ============================================================================== --- py/trunk/doc/download.txt (original) +++ py/trunk/doc/download.txt Sun Jun 28 11:51:40 2009 @@ -2,7 +2,7 @@ Downloading ============== -.. _`PyPI project page`: http://pypi.python.org/pypi?%3Aaction=pkg_edit&name=py +.. _`PyPI project page`: http://pypi.python.org/pypi/py/ Latest Release, see `PyPI project page`_ Modified: py/trunk/doc/execnet.txt ============================================================================== --- py/trunk/doc/execnet.txt (original) +++ py/trunk/doc/execnet.txt Sun Jun 28 11:51:40 2009 @@ -233,3 +233,19 @@ socketgw = py.execnet.SocketGateway.new_remote(popengw, ("127.0.0.1", 0)) print socketgw._rinfo() # print some info about the remote environment + + +Sending a module / checking if run through remote_exec +-------------------------------------------------------------- + +You can pass a module object to ``remote_exec`` in which case +its source code will be sent. No dependencies will be transferred +so the module must be self-contained or only use modules that are +installed on the "other" side. Module code can detect if it is +running in a remote_exec situation by checking for the special +``__name__`` attribute like this:: + + if __name__ == '__channelexec__': + # ... call module functions ... + + Modified: py/trunk/doc/test/funcargs.txt ============================================================================== --- py/trunk/doc/test/funcargs.txt (original) +++ py/trunk/doc/test/funcargs.txt Sun Jun 28 11:51:40 2009 @@ -39,7 +39,7 @@ .. _`funcarg provider`: -funcarg providers: setting up test function arguments +funcarg providers: instantiating test function arguments ============================================================== Test functions can specify one ore more arguments ("funcargs") @@ -120,7 +120,27 @@ ``request.param``: if exists was passed by a `parametrizing test generator`_ -perform scoped setup and teardown +teardown/cleanup after test function execution +------------------------------------------------ + +.. sourcecode:: python + + def addfinalizer(func): + """ call a finalizer function when test function finishes. """ + +Calling ``request.addfinalizer()`` is useful for scheduling teardown +functions. Here is an example for providing a ``myfile`` +object that is to be closed when the test function finishes. + +.. sourcecode:: python + + def pytest_funcarg__myfile(self, request): + # ... create and open a unique per-function "myfile" object ... + request.addfinalizer(lambda: myfile.close()) + return myfile + + +perform scope-specific setup and cleanup --------------------------------------------- .. sourcecode:: python @@ -148,30 +168,6 @@ ) -cleanup after test function execution ---------------------------------------------- - -.. sourcecode:: python - - def addfinalizer(func, scope="function"): - """ register calling a a finalizer function. - scope == 'function': when the single test function run finishes. - scope == 'module': when tests in a different module are run - scope == 'session': when tests of the session have run. - """ - -Calling ``request.addfinalizer()`` is useful for scheduling teardown -functions. The given scope determines when the teardown function -will be called. Here is a basic example for providing a ``myfile`` -object that is to be closed when the test function finishes. - -.. sourcecode:: python - - def pytest_funcarg__myfile(self, request): - # ... create and open a unique per-function "myfile" object ... - request.addfinalizer(lambda: myfile.close()) - return myfile - requesting values of other funcargs --------------------------------------------- Modified: py/trunk/py/__init__.py ============================================================================== --- py/trunk/py/__init__.py (original) +++ py/trunk/py/__init__.py Sun Jun 28 11:51:40 2009 @@ -19,7 +19,7 @@ """ from initpkg import initpkg -version = "1.0.0b3" +version = "1.0.0b6" initpkg(__name__, description = "py.test and pylib: advanced testing tool and networking lib", Modified: py/trunk/py/execnet/gateway.py ============================================================================== --- py/trunk/py/execnet/gateway.py (original) +++ py/trunk/py/execnet/gateway.py Sun Jun 28 11:51:40 2009 @@ -230,7 +230,7 @@ from sys import exc_info channel, (source, outid, errid) = item try: - loc = { 'channel' : channel } + loc = { 'channel' : channel, '__name__': '__channelexec__'} self._trace("execution starts:", repr(source)[:50]) close = self._local_redirect_thread_output(outid, errid) try: Modified: py/trunk/py/execnet/testing/test_gateway.py ============================================================================== --- py/trunk/py/execnet/testing/test_gateway.py (original) +++ py/trunk/py/execnet/testing/test_gateway.py Sun Jun 28 11:51:40 2009 @@ -92,6 +92,11 @@ def test_repr_doesnt_crash(self): assert isinstance(repr(self), str) + def test_attribute__name__(self): + channel = self.gw.remote_exec("channel.send(__name__)") + name = channel.receive() + assert name == "__channelexec__" + def test_correct_setup_no_py(self): channel = self.gw.remote_exec(""" import sys Modified: py/trunk/py/io/stdcapture.py ============================================================================== --- py/trunk/py/io/stdcapture.py (original) +++ py/trunk/py/io/stdcapture.py Sun Jun 28 11:51:40 2009 @@ -136,6 +136,11 @@ readline = read readlines = read __iter__ = read + + def fileno(self): + raise ValueError("redirected Stdin is pseudofile, has no fileno()") + def isatty(self): + return False try: devnullpath = os.devnull Modified: py/trunk/py/io/testing/test_stdcapture.py ============================================================================== --- py/trunk/py/io/testing/test_stdcapture.py (original) +++ py/trunk/py/io/testing/test_stdcapture.py Sun Jun 28 11:51:40 2009 @@ -1,6 +1,15 @@ import os, sys import py +def test_dontreadfrominput(): + from py.__.io.stdcapture import DontReadFromInput + f = DontReadFromInput() + assert not f.isatty() + py.test.raises(IOError, f.read) + py.test.raises(IOError, f.readlines) + py.test.raises(IOError, iter, f) + py.test.raises(ValueError, f.fileno) + class TestStdCapture: def getcapture(self, **kw): return py.io.StdCapture(**kw) Modified: py/trunk/py/test/collect.py ============================================================================== --- py/trunk/py/test/collect.py (original) +++ py/trunk/py/test/collect.py Sun Jun 28 11:51:40 2009 @@ -204,7 +204,7 @@ def _matchonekeyword(self, key, chain): elems = key.split(".") # XXX O(n^2), anyone cares? - chain = [item._keywords() for item in chain if item._keywords()] + chain = [item.readkeywords() for item in chain if item._keywords()] for start, _ in enumerate(chain): if start + len(elems) > len(chain): return False Modified: py/trunk/py/test/funcargs.py ============================================================================== --- py/trunk/py/test/funcargs.py (original) +++ py/trunk/py/test/funcargs.py Sun Jun 28 11:51:40 2009 @@ -113,7 +113,7 @@ val = setup() cache[cachekey] = val if teardown is not None: - self.addfinalizer(lambda: teardown(val), scope=scope) + self._addfinalizer(lambda: teardown(val), scope=scope) return val def getfuncargvalue(self, argname): @@ -142,10 +142,14 @@ return None raise ValueError("unknown finalization scope %r" %(scope,)) - def addfinalizer(self, finalizer, scope="function"): + def _addfinalizer(self, finalizer, scope): colitem = self._getscopeitem(scope) self.config._setupstate.addfinalizer(finalizer=finalizer, colitem=colitem) + def addfinalizer(self, finalizer): + """ call the given finalizer after test function finished execution. """ + self._addfinalizer(finalizer, scope="function") + def __repr__(self): return "" %(self._pyfuncitem) Modified: py/trunk/py/test/plugin/pytest_runner.py ============================================================================== --- py/trunk/py/test/plugin/pytest_runner.py (original) +++ py/trunk/py/test/plugin/pytest_runner.py Sun Jun 28 11:51:40 2009 @@ -19,11 +19,14 @@ action="store_true", dest="boxed", default=False, help="box each test run in a separate process") +# XXX move to pytest_sessionstart and fix py.test owns tests def pytest_configure(config): config._setupstate = SetupState() -def pytest_unconfigure(config): - config._setupstate.teardown_all() +def pytest_sessionfinish(session, exitstatus, excrepr=None): + # XXX see above + if hasattr(session.config, '_setupstate'): + session.config._setupstate.teardown_all() def pytest_make_collect_report(collector): call = collector.config.guardedcall( Modified: py/trunk/py/test/plugin/pytest_terminal.py ============================================================================== --- py/trunk/py/test/plugin/pytest_terminal.py (original) +++ py/trunk/py/test/plugin/pytest_terminal.py Sun Jun 28 11:51:40 2009 @@ -204,6 +204,7 @@ msg = "python: platform %s -- Python %s" % (sys.platform, verinfo) if self.config.option.verbose or self.config.option.debug: msg += " -- " + str(sys.executable) + msg += " -- pytest-%s" % (py.__version__) self.write_line(msg) if self.config.option.debug or self.config.option.traceconfig: @@ -227,7 +228,8 @@ for i, testarg in py.builtin.enumerate(self.config.args): self.write_line("test object %d: %s" %(i+1, testarg)) - def pytest_sessionfinish(self, session, exitstatus, excrepr=None): + def pytest_sessionfinish(self, __call__, session, exitstatus, excrepr=None): + __call__.execute() self._tw.line("") if exitstatus in (0, 1, 2): self.summary_failures() Modified: py/trunk/py/test/pycollect.py ============================================================================== --- py/trunk/py/test/pycollect.py (original) +++ py/trunk/py/test/pycollect.py Sun Jun 28 11:51:40 2009 @@ -133,11 +133,11 @@ res = self._deprecated_join(name) if res is not None: return res - if obj.func_code.co_flags & 32: # generator function + if is_generator(obj): # XXX deprecation warning return self.Generator(name, parent=self) - else: - return self._genfunctions(name, obj) + else: + return self._genfunctions(name, obj) def _genfunctions(self, name, funcobj): module = self.getparent(Module).obj @@ -152,6 +152,12 @@ return self.Function(name, parent=self) return funcargs.FunctionCollector(name=name, parent=self, calls=metafunc._calls) + +def is_generator(func): + try: + return (func.func_code.co_flags & 32) # generator function + except AttributeError: # c / builtin functions have no func_code + return False class Module(py.test.collect.File, PyCollectorMixin): def _getobj(self): Modified: py/trunk/py/test/testing/test_funcargs.py ============================================================================== --- py/trunk/py/test/testing/test_funcargs.py (original) +++ py/trunk/py/test/testing/test_funcargs.py Sun Jun 28 11:51:40 2009 @@ -163,15 +163,11 @@ req._fillfuncargs() assert item.funcargs == {'something': 1} - def test_request_addfinalizer_scopes(self, testdir): + def test_request_addfinalizer(self, testdir): item = testdir.getitem(""" teardownlist = [] def pytest_funcarg__something(request): - for scope in ("function", "module", "session"): - request.addfinalizer( - lambda x=scope: teardownlist.append(x), - scope=scope) - + request.addfinalizer(lambda: teardownlist.append(1)) def test_func(something): pass """) req = funcargs.FuncargRequest(item) @@ -183,16 +179,7 @@ assert not teardownlist ss.teardown_exact(item) print ss.stack - assert teardownlist == ['function'] - ss.teardown_exact(item.parent) - assert teardownlist == ['function', 'module'] - ss.teardown_all() - assert teardownlist == ['function', 'module', 'session'] - - def test_request_addfinalizer_unknown_scope(self, testdir): - item = testdir.getitem("def test_func(): pass") - req = funcargs.FuncargRequest(item) - py.test.raises(ValueError, "req.addfinalizer(None, scope='xyz')") + assert teardownlist == [1] def test_request_getmodulepath(self, testdir): modcol = testdir.getmodulecol("def test_somefunc(): pass") @@ -200,7 +187,6 @@ req = funcargs.FuncargRequest(item) assert req.fspath == modcol.fspath - class TestRequestCachedSetup: def test_request_cachedsetup(self, testdir): item1,item2 = testdir.getitems(""" Modified: py/trunk/py/test/testing/test_genitems.py ============================================================================== --- py/trunk/py/test/testing/test_genitems.py (original) +++ py/trunk/py/test/testing/test_genitems.py Sun Jun 28 11:51:40 2009 @@ -121,3 +121,16 @@ 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 Modified: py/trunk/setup.py ============================================================================== --- py/trunk/setup.py (original) +++ py/trunk/setup.py Sun Jun 28 11:51:40 2009 @@ -1,16 +1,16 @@ """ - setup file for 'py' package based on: - - https://codespeak.net/svn/py/trunk, revision=65224 - - autogenerated by gensetup.py +autogenerated by gensetup.py +setup file for 'py' package based on: + +revision: 1181:8a8203ee5eb85837b6a40d95d861af42008d1a4c + """ import os, sys import ez_setup ez_setup.use_setuptools() -from setuptools import setup, Extension - +from setuptools import setup + long_description = """ advanced testing and development support library: @@ -35,7 +35,7 @@ name='py', description='py.test and pylib: advanced testing tool and networking lib', long_description = long_description, - version='1.0.0b3', + version='1.0.0b5', url='http://pylib.org', license='MIT license', platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'], @@ -60,9 +60,7 @@ 'Topic :: System :: Distributed Computing', 'Topic :: Utilities', 'Programming Language :: Python'], - packages=['example.funcarg.mysetup', - 'example.funcarg.mysetup2', - 'py', + packages=['py', 'py.builtin', 'py.builtin.testing', 'py.cmdline', From hpk at codespeak.net Sun Jun 28 15:49:19 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Sun, 28 Jun 2009 15:49:19 +0200 (CEST) Subject: [py-svn] r66012 - py/extradoc/talk/ep2009/rapid-testing Message-ID: <20090628134919.8205716849F@codespeak.net> Author: hpk Date: Sun Jun 28 15:49:17 2009 New Revision: 66012 Added: py/extradoc/talk/ep2009/rapid-testing/beamerdefs.txt (contents, props changed) Modified: py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt Log: redoing many changes i had done already but managed to loose during notebook/desktop syncing :( Added: py/extradoc/talk/ep2009/rapid-testing/beamerdefs.txt ============================================================================== --- (empty file) +++ py/extradoc/talk/ep2009/rapid-testing/beamerdefs.txt Sun Jun 28 15:49:17 2009 @@ -0,0 +1,77 @@ +.. colors +.. =========================== + +.. role:: green +.. role:: red + + +.. general useful commands +.. =========================== + +.. |pause| raw:: latex + + \pause + +.. |small| raw:: latex + + {\small + +.. |end_small| raw:: latex + + } + + +.. closed bracket +.. =========================== + +.. |>| raw:: latex + + } + + +.. example block +.. =========================== + +.. |example<| raw:: latex + + \begin{exampleblock}{ + + +.. |end_example| raw:: latex + + \end{exampleblock} + + + +.. alert block +.. =========================== + +.. |alert<| raw:: latex + + \begin{alertblock}{ + + +.. |end_alert| raw:: latex + + \end{alertblock} + + + +.. columns +.. =========================== + +.. |column1| raw:: latex + + \begin{columns} + \begin{column}{0.45\textwidth} + +.. |column2| raw:: latex + + \end{column} + \begin{column}{0.45\textwidth} + + +.. |end_columns| raw:: latex + + \end{column} + \end{columns} Modified: py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt ============================================================================== --- py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt (original) +++ py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt Sun Jun 28 15:49:17 2009 @@ -5,16 +5,6 @@ Rapid testing with py.test ================================================================= -Install -======================================== - -- svn checkout http://codespeak.net/svn/py/dist -- hg clone https://hpk42 at bitbucket.org/hpk42/py-trunk/ - -- run "python setup.py" with "install" or "develop" - -- if need be: easy_install "py" - my technical background =========================== @@ -44,12 +34,11 @@ - need/want to run tests on >1 platforms? - have "non-python" tests? -Python +Basic consideration ================================== Python has no compile-time type security. - Testing to the rescue! ================================== @@ -60,18 +49,19 @@ what is the job of automated testing tools? + my current answer ======================================== -* verify that my code changes work out -* be helpful when test scenarios fail +* verify code changes work out +* be helpful when tests fail If failures are not helpful ... ======================================== improve the test tool or -write more (different) tests +write more or write different tests The Automated Test question ======================================== @@ -83,86 +73,52 @@ to make sure that -* units react well to input. -* components co-operate nicely -* code changes work out in the end - -What does "code changes work out" mean? -======================================== - -.. image:: img/rails_in_the_city_by_marikaz.jpg - :scale: 90 - :align: center - -http://marikaz.deviantart.com/ CC 3.0 AN-ND - -my current answer -======================================== - -- various operating systems -- various Python Interpreters -- cloud environments -- web browsers etc. - -Common Test terminology -============================== - -- developer and customer tests -- unit tests -- functional tests -- acceptance tests -- integration tests - -you may discuss a long time -about categorizations ... - -py.test strives to test it all -================================= +* unittest: units react well to input. +* integration: components co-operate nicely +* functional: code changes work out in the end -current focus: +unittest +============ -- unittesting -- functional -- integration tests +assert that functions, classes behave as expected -A pragmatic view on test types -================================= - -let's talk about small, medium or large tests. - -Small Tests: one aspect -============================== +low level / white-boxy .. image:: img/small.png :align: center :scale: 70 -Medium Tests: two aspects -============================== +integration +============== + +assert units/components co-operate nicely + +slightly higher level / more black boxy .. image:: img/medium.png :align: center :scale: 70 -Large Tests: end-to-end -============================== + +functional / system +======================== + +things work in target environment .. image:: img/large.png :align: center :scale: 70 -generally speaking -============================================ - -what is the vision of automated testing? - -my current answer -======================================== - -merge with real-life deployment. +Common Test terminology +============================== +- developer and customer tests +- unit tests +- functional tests +- acceptance tests +- integration tests -Walkthrough Python test functions (30 minutes) +Walkthrough Python test functions ============================================================================ .. image:: img/new_color_in_dark_old_city_by_marikaz.jpg @@ -175,9 +131,9 @@ ========================================== :: - app/__init__.py + mypkg/__init__.py ... - app/tests/test_module.py + mypkg/tests/test_module.py ... py.test invocation: ``py.test app`` @@ -186,16 +142,17 @@ ========================================== :: - app/__init__.py - tests/test_module.py + mypkg/__init__.py + test/test_module.py -py.test invocation: ``py.test tests``. +py.test invocation: ``py.test test``. -Mind the the `__init__.py` files! +create `__init__.py` files ========================================== wherever your "tests" directories are, -always provide a ``__init__.py`` +always provide a ``__init__.py``! + automatic test discovery =================================== @@ -207,49 +164,145 @@ **automatic discovery avoids boilerplate** +test function and test classes +=================================== + +:: + + def test_something(): + assert True + + class TestSomething: + def test_something(self): + assert True + +assert introspection +==================== + +:: + + def test_assert_introspection(): + assert result # assertTrue() + assert result == 1 # assertEqual(result, 1) + assert result != 2 # assertNotEqual(result, 2) + assert not result # assertFalse(result) + +print() debugging +================= + +:: + + import py + + def test_something1(): + print "Useful debugging information." + assert False + +testing for exceptions +================================ + +:: + + import py + + def test_raises_one(): + py.test.raises(ValueError, int, 'foo') + + def test_raises_two(): + py.test.raises(ValueError, "int('foo')") + + +Skipping tests +================================= + +If you need to skip a test due to platform mismatch:: + + def test_function_win32(): + if sys.platform != "win32": + py.test.skip("win32 needed") + ... + + +"XFailing" tests +================================= + +If you want to mark a test function as "expected to fail":: + + import py + @py.test.mark.xfail + def test_function(): + assert f() == "ok" + + +selected options +================ + +(see ``py.test -h``) + +- -s - disable catching of stdout/stderr during test run. +- -x/--exitfirst - exit instantly on first error or failed test. +- --showlocals - show locals in tracebacks. +- --pdb - start pdb (the Python debugger) on errors. +- -f/--looponfail - loop on failing test set. +- -k - only run test items matching the given keyword. +- --tb/fulltrace - different options to control traceback generation. + +Exercise 1 +======================= + +``easy_install -U py`` or: + + - svn checkout http://codespeak.net/svn/py/dist + - hg clone https://bitbucket.org/hpk42/py-trunk/ + - run "python setup.py" with "install" or "develop" + +- create ``mypkg/test/test_hello.py`` (plus ``__init__.py``'s) +- write a simple test function +- run ``py.test mypkg``, play around with options +- help your neighbour + Typical test function - viewed abstractly ========================================== :: - from app.pkg import SomeClass + from mypkg.mod import SomeClass def test_something(): inst1 = SomeClass("somevalue") ... - assert "things are ok" + assert self.inst1.method() == "ok" + observations and the setup question ========================================== -* test values / configuration mixes with test code -* importing 'app' may fail -* the ``app.pkg.SomeClass`` reference may change +* test value setup mixes with test code +* the ``mypkg.mod.SomeClass`` reference may change what if multiple test functions setup the same classes, maybe with slightly different values? -x-unit style setup / fixtures +old-style: xUnit unit setup ========================================== :: - from app.pkg import SomeClass + from mypkg.mod import SomeClass class TestGroup: def setup_method(self, method): self.inst1 = SomeClass("somevalue") def test_something(self): ... use self.inst1 ... - assert "things are ok" + assert self.inst1.method() == "ok" observations ========================================== -* test values / configuration mixes with test code -* importing 'app' may fail -* the ``app.pkg.SomeClass`` reference may change -* **functions now group by setup/fixture code** +* test value setup mixes with test code +* ``mypkg.mod.SomeClass`` reference may change +* **test functions now group by setup code** * **multiple methods can reuse the same setup** meet "funcargs" - test function arguments @@ -258,21 +311,25 @@ :: def test_something(inst1): - assert inst1.call() == "result" + assert inst1.method() == "ok" + + class TestClass: + def test_something(self, inst1): + assert inst1.method() == "ok" observations ========================================== * test values are simply used in test code -* no imports or app.pkg.SomeClass references here -* freedom to group tests logically +* no imports or mypkg.mod.SomeClass references here +* grouping in test classes independent of setup * multiple functions can re-use the same funcarg setup How to setup the funcarg value? ========================================== :: - from app.pkg import SomeClass + from mypkg.mod import SomeClass def pytest_funcarg__inst1(request): return SomeClass("somevalue") @@ -281,48 +338,95 @@ observations =================================================== +* funcarg provider separated from test code * funcarg provider automatically discovered -* app bootstraps for testing in one place * funcarg can be used from different test functions -introducing a command line option -=================================================== + +Exercise +========================================== + +* write a new package "mypkg" +* add mypkg/__init__ and mypkg/test_url.py +* add a test_path function that needs an "url" argument +* write the provider in mypkg/conftest.py +* run your test + +Bonus: add more tests, play with wrongly named args. + +Adding funcarg finalizers / teardown +====================================== + :: - # ./tests/conftest.py - def pytest_addoption(parser): - parser.addoption("--val", action="store") - def pytest_funcarg_inst1(request): - return SomeClass(request.config.getvalue("val")) -observations -=================================================== + def pytest_funcarg__inst1(request): + f = open(...) + request.addfinalizer(lambda: f.close()) + return f -* test configuration separated from test code -* app bootstraps for testing in one place -* can be used from any test function anywhere +Managing longer-living funcargs +====================================== -``request`` object attributes -=================================================== +:: -``request.function``: python test function + def pytest_funcarg__db(request): + return request.cached_setup( + setup=lambda: Database(request.config.getvalue("...")), + teardown=lambda db: db.close(), + scope="session" + """) -``request.cls``: containing test class +**setup/teardown of function arguments can be changed independently +from test code!** -``request.module``: test module +request attributes +====================================== + +``request.function``: python function object requesting the argument + +``request.cls``: class object where the test function is defined in or None. -``request.config``: access to options and general config +``request.module``: module object where the test function is defined in. +``request.config``: module object where the test function is defined in. Exercise -========================================== +====================================== -* write a new package "mypkg" -* add mypkg/__init__ and mypkg/test_url.py -* add a test_path function that needs an "url" argument -* write the provider in mypkg/conftest.py -* run your test +have your test module and/or test function specify +a parameter for instantiating an application class +that is used in your test function. + + +Parametrizing test function calls +====================================== + +:: + + def test_function(param): + assert param < 3 + + def pytest_generate_tests(metafunc): + if "param" not in metafunc.funcargnames: + return + for i in range(3): + metafunc.addcall(funcargs={'param': i}) + +notes +======================= + +- test code remains unmodified +- pytest_generate_tests can live in conftest.py or plugin +- pytest_generate_tests can implement all kinds of parametrization schemes + + +funcargs for augmenting testing techniques +============================================= + + +summary funcargs +====================== -Bonus: add more tests, play with wrongly named args. Using Plugins and Extensions ========================================= From hpk at codespeak.net Sun Jun 28 16:27:48 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Sun, 28 Jun 2009 16:27:48 +0200 (CEST) Subject: [py-svn] r66013 - py/extradoc/talk/ep2009/rapid-testing Message-ID: <20090628142748.6626D168042@codespeak.net> Author: hpk Date: Sun Jun 28 16:27:48 2009 New Revision: 66013 Modified: py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt Log: catching up with lost changes (2) Modified: py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt ============================================================================== --- py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt (original) +++ py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt Sun Jun 28 16:27:48 2009 @@ -1,5 +1,5 @@ -.. include:: beamerdefs.txt -.. include:: +.. .. include:: beamerdefs.txt +.. .. include:: ================================================================= Rapid testing with py.test @@ -342,7 +342,6 @@ * funcarg provider automatically discovered * funcarg can be used from different test functions - Exercise ========================================== @@ -376,8 +375,7 @@ scope="session" """) -**setup/teardown of function arguments can be changed independently -from test code!** +**independent from actual test code which just uses the instance** request attributes ====================================== @@ -417,105 +415,139 @@ - test code remains unmodified - pytest_generate_tests can live in conftest.py or plugin -- pytest_generate_tests can implement all kinds of parametrization schemes +- pytest_generate_tests can implement various parametrization schemes + +see blog post: http://tinyurl.com/ofry94 -funcargs for augmenting testing techniques +funcargs for testing helpers ============================================= +example: -summary funcargs -====================== + def test_envreading(inst1, monkeypatch): + monkeypatch.setenv('PREFIX', 'myval') + val = inst1.readenv() + assert val == "myval" + +see post http://tinyurl.com/ob87o6 + +summary py.test funcargs +=========================== + +- makes for no-boilerplate and readable test code +- one place to manage fixtures for tests +- nice per-test-function helpers -Using Plugins and Extensions +(questions / demo'ing as time permits) ========================================= -.. image:: img/end_of_a_age_by_marikaz.jpg +- showcase py.test's ``testdir`` usage +- showcase request.getfuncargvalue() +- showcase/discuss ``mysetup`` in the docs + +Break +===== + +.. image:: img/flying_lady_by_marikaz.jpg :scale: 100 :align: left http://marikaz.deviantart.com/ CC 3.0 AN-ND -Historic view -========================== +Hooks and Extensions (45 minutes) +========================================= -- 0.9.x uses conftest's for extension and configuration -- 1.0 uses "plugins" for extending and conftest.py for configuration -- we do "smooth" transition because of existing test code base +.. image:: img/end_of_a_age_by_marikaz.jpg + :scale: 100 + :align: left + +http://marikaz.deviantart.com/ CC 3.0 AN-ND Customizing py.test =========================== -- configuration values go to conftest.py files -- write local or global plugins -- provide funcargs +- implement hooks (in conftest.py or named plugin) +- implement **new test items** +- providing funcargs Conftest.py =============== -- can be put into test directory or higher up -- contains test configuration values -- specifies plugins to use +- is auto-discovered in test directory or higher up +- can be used to implement hooks - can provide default values for command line options +- specifies plugins to use -Specifying plugins -======================== +a simple hook: adding a command line opt +=========================================== -- ``-p NAME``: load comma-separated list of plugins -- plugins are always named "pytest_NAME" and can be - anywhere in your import path +:: -Writing a local conftest plugin -==================================== + #./conftest.py + def pytest_addoption(parser): + parser.addoption("--url", action="store", default="http://testrun.org") + def pytest_funcarg__url(request): + return request.config.getvalue("url") -you can write a local conftest.py based plugin:: +adding helpers to py.test.* namespace +====================================== - class ConftestPlugin: - def pytest_addoption(self, parser): - parser.addoption("--myworld", action="store_true") - ... +:: -Exercise -========================================== + #./conftest.py + def mycheck(val): + assert val == 42 + def pytest_namespace(config): + return {'mycheck': mycheck} -* add an option for specifying url on the command line -* look at "py.test -h" + #./test_module.py + def test_something(): + py.test.mycheck(41) -other Plugin Examples -========================================= +Named Plugins +======================== -- integrate collection/run of traditional unit-tests -- run functions in their own tempdir -- testing ReST documents -- running Prolog tests (old: conftest based) -- running Javascript tests (old: conftest based) -- html reporting for nightly runs (old: conftest-based) +- named plugins are regular python modules or packages +- fixed "pytest_NAME" naming scheme -if time permits ... -========================================= +- ``py.test -p NAME``: load comma-separated list of plugins +- ``PYTEST_PLUGINS=NAME`` environment variable is recognized +- ``pytest_plugins=[...]`` in test module or conftest.py -- let's play with "pytest_unittest" plugin -Break -===== +Hooks +========== -.. image:: img/flying_lady_by_marikaz.jpg - :scale: 100 - :align: left +- hooks are python functions with a ``pytest_`` prefix +- hook implementation need to use *exact argument names* +- no classes let alone sublclasses neccessary! -http://marikaz.deviantart.com/ CC 3.0 AN-ND +Hook categories +================== +- configuration hooks +- collection hooks +- runtest hooks +- reporting hooks +- configuration / testing process ("event") hooks -Writing Plugins (30 minutes) -============================================================================ +see py/test/plugin/hookspec.py + +Configuration Hooks +====================== +:: -- test collection objects -- plugin hooks and events -- event system for custom reporting -- test collection hooks -- test running hooks + def pytest_addoption(parser): + """ called before commandline parsing. """ + + def pytest_configure(config): + """ called after command line options have been parsed. " + + def pytest_unconfigure(config): + """ called before test process is exited. """ py.test collection objects ============================= @@ -535,7 +567,15 @@ - **Directory** - **File** -always available Attributes +Demo/Exercise "collection tree" +==================================== + +inspecting the test collection tree:: + + py.test --collectonly + + +collection node Attributes ======================================= **parent** a reference to the collector that produced us @@ -551,38 +591,8 @@ - **Module** - **Class**/**Instance** -- **Generator** - **Function** -Exercise "collection tree" -==================================== - -inspecting the test collection tree:: - - py.test --collectonly - -Hooks and Events -=================== - -- Plugin hook methods implement interaction - with configuration/collection/running of tests - -- Events are called for notification purposes, - are not asked for return values. - -Configuration Hooks -====================== -:: - - def pytest_addoption(self, parser): - """ called before commandline parsing. """ - - def pytest_configure(self, config): - """ called after command line options have been parsed. " - - def pytest_unconfigure(self, config): - """ called before test process is exited. """ - Collection Hooks ====================== :: From py-svn at codespeak.net Sun Jun 28 20:27:14 2009 From: py-svn at codespeak.net (py-svn at codespeak.net) Date: Sun, 28 Jun 2009 20:27:14 +0200 (CEST) Subject: [py-svn] 0rder #222743 Message-ID: <20090628182714.C12A4168436@codespeak.net> An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Mon Jun 29 06:40:19 2009 From: py-svn at codespeak.net (Teodoro Aguv) Date: Mon, 29 Jun 2009 10:40:19 +0600 Subject: [py-svn] Something with your cell Message-ID: <5006CW.65413D8.60564447091MEALCERPHCKBHLB643@[92.46.0.100]> An HTML attachment was scrubbed... URL: From hpk at codespeak.net Mon Jun 29 13:23:31 2009 From: hpk at codespeak.net (hpk at codespeak.net) Date: Mon, 29 Jun 2009 13:23:31 +0200 (CEST) Subject: [py-svn] r66027 - py/extradoc/talk/ep2009/rapid-testing Message-ID: <20090629112331.10EE1168449@codespeak.net> Author: hpk Date: Mon Jun 29 13:23:30 2009 New Revision: 66027 Modified: py/extradoc/talk/ep2009/rapid-testing/makepdf py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt py/extradoc/talk/ep2009/rapid-testing/test_cached_setup.py Log: kind of last changes Modified: py/extradoc/talk/ep2009/rapid-testing/makepdf ============================================================================== --- py/extradoc/talk/ep2009/rapid-testing/makepdf (original) +++ py/extradoc/talk/ep2009/rapid-testing/makepdf Mon Jun 29 13:23:30 2009 @@ -7,7 +7,7 @@ # https://sourceforge.net/tracker/?func=detail&atid=422032&aid=1459707&group_id=38414 BASE=rapid-testing -python rst2beamer.py --stylesheet=stylesheet.latex --documentoptions=13pt $BASE.txt $BASE.latex || exit +python rst2beamer.py --stylesheet=stylesheet.latex --documentoptions=14pt $BASE.txt $BASE.latex || exit sed 's/\\date{}/\\input{author.latex}/' $BASE.latex >tmpfile || exit sed 's/\\maketitle/\\input{title.latex}/' tmpfile >$BASE.latex || exit pdflatex $BASE.latex || exit Modified: py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt ============================================================================== --- py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt (original) +++ py/extradoc/talk/ep2009/rapid-testing/rapid-testing.txt Mon Jun 29 13:23:30 2009 @@ -1,28 +1,19 @@ -.. .. include:: beamerdefs.txt -.. .. include:: +.. include:: beamerdefs.txt +.. include:: ================================================================= Rapid testing with py.test ================================================================= -my technical background +my testing background =========================== - programming since 20 years - Python since around 2000 -- released projects: pypy, py.test/py lib, rlcompleter2 -- other: mailwitness, shpy, vadm, codespeak, ... -- merlinux GmbH since 2004 -- PyPy EU-project 2004-2007 - -my testing background -======================= - -- learned Python 2001 -- "test-driven developer" (TDD) since 2002 -- founded PyPy, based on TDD principles -- developed utest/stdtest, now py.test +- founded PyPy on TDD principles +- py.test since 2003 +- couple of merlinux projects with TDD - consultancies on testing What's **your** background? @@ -34,16 +25,6 @@ - need/want to run tests on >1 platforms? - have "non-python" tests? -Basic consideration -================================== - -Python has no compile-time type security. - -Testing to the rescue! -================================== - -automated tests are better than declaring types. - The test tool question ======================================== @@ -63,38 +44,28 @@ write more or write different tests -The Automated Test question -======================================== - -what are automated tests there for? - -my current answer +Developer oriented automated tests ======================================== -to make sure that - * unittest: units react well to input. * integration: components co-operate nicely -* functional: code changes work out in the end +* functional: code changes work out in user environments unittest ============ -assert that functions, classes behave as expected - -low level / white-boxy +assert that functions and classes behave as expected .. image:: img/small.png :align: center :scale: 70 + integration ============== assert units/components co-operate nicely -slightly higher level / more black boxy - .. image:: img/medium.png :align: center :scale: 70 @@ -103,22 +74,15 @@ functional / system ======================== -things work in target environment +things work in user target environment .. image:: img/large.png :align: center :scale: 70 -Common Test terminology -============================== -- developer and customer tests -- unit tests -- functional tests -- acceptance tests -- integration tests -Walkthrough Python test functions +py.test basics ============================================================================ .. image:: img/new_color_in_dark_old_city_by_marikaz.jpg @@ -127,6 +91,30 @@ http://marikaz.deviantart.com/ CC 3.0 AN-ND +py.test fundamental features +=============================== + +- cross-project testing tool +- no-boilerplate test code +- useful information when a test fails +- deep extensibility +- distribute tests to multiple hosts + +cross-project test tool +=========================== + +- tests are run via ``py.test`` command line tool. + +- project specific ``conftest.py`` files + +- testing starts from files/dirs or current dir + +examples:: + + py.test test_file.py + py.test path/to/tests + + A Typical Python test layout ========================================== :: @@ -136,67 +124,67 @@ mypkg/tests/test_module.py ... -py.test invocation: ``py.test app`` +example invocation:: + + py.test mypkg Another typical test layout ========================================== :: mypkg/__init__.py + ... test/test_module.py -py.test invocation: ``py.test test``. +example invocations:: -create `__init__.py` files -========================================== + py.test test + py.test # curdir == parent of mypkg -wherever your "tests" directories are, -always provide a ``__init__.py``! +mind the `tests/__init__.py` files +========================================== + +- provide ``__init__.py`` with your tests +- test files are imported as normal python modules +- sys.path is amended to contain the first non-``__init__.py`` dir automatic test discovery =================================== -py.test walks over your source tree and: +py.test walks over your **whole** source tree and: - discovers ``test_*.py`` test files -- discovers ``test_`` functions or ``Test`` classes +- discovers ``test_`` functions and ``Test`` classes **automatic discovery avoids boilerplate** -test function and test classes -=================================== +no boilerplate python test code +================================= :: def test_something(): - assert True + x = 3 + assert x == 4 class TestSomething: def test_something(self): - assert True + x = 1 + assert x == 5 assert introspection -==================== +======================= :: def test_assert_introspection(): - assert result # assertTrue() - assert result == 1 # assertEqual(result, 1) - assert result != 2 # assertNotEqual(result, 2) - assert not result # assertFalse(result) - -print() debugging -================= - -:: + # unittest.py + assert x # assertTrue() + assert x == 1 # assertEqual(x, 1) + assert x != 2 # assertNotEqual(x, 2) + assert not x # assertFalse(x) - import py - - def test_something1(): - print "Useful debugging information." - assert False testing for exceptions ================================ @@ -211,17 +199,38 @@ def test_raises_two(): py.test.raises(ValueError, "int('foo')") +Failure / Traceback Demo +=========================== + +interactive demo: ``py.test failure_demo.py`` + + +print() debugging +================= + +:: + + def test_something1(): + print "Useful debugging information." + assert False + +output captured per function run, only shown on failure. Skipping tests ================================= -If you need to skip a test due to platform mismatch:: +Skipping a test because of platform mismatch:: def test_function_win32(): if sys.platform != "win32": py.test.skip("win32 needed") ... +or because of missing dependency:: + + # ./test_module.py + docutils = py.test.importorskip("docutils") + "XFailing" tests ================================= @@ -229,39 +238,46 @@ If you want to mark a test function as "expected to fail":: import py - @py.test.mark.xfail + @py.test.mark.xfail def test_function(): - assert f() == "ok" + assert f() == "thisresult" +no traceback shown / reports specially if it passes. -selected options -================ +some important options +======================== -(see ``py.test -h``) +see also ``py.test -h``: -- -s - disable catching of stdout/stderr during test run. -- -x/--exitfirst - exit instantly on first error or failed test. -- --showlocals - show locals in tracebacks. -- --pdb - start pdb (the Python debugger) on errors. -- -f/--looponfail - loop on failing test set. -- -k - only run test items matching the given keyword. -- --tb/fulltrace - different options to control traceback generation. +- -s disable catching of stdout/stderr +- -x exit instantly on first failure +- -k only run tests matching the given keyword. +- -l show locals in tracebacks. +- --pdb start Python debugger on errors +- --tb/fulltrace - control traceback generation. +- -f/--looponfail - loop on failing test set. -Exercise 1 -======================= +Exercise 1 (max 15 minutes) +============================= ``easy_install -U py`` or: - - svn checkout http://codespeak.net/svn/py/dist - - hg clone https://bitbucket.org/hpk42/py-trunk/ - - run "python setup.py" with "install" or "develop" + - svn co http://codespeak.net/svn/py/trunk OR + - hg clone https://bitbucket.org/hpk42/py-trunk/ + - run "python setup.py" with "install" or "develop" -- create ``mypkg/test/test_hello.py`` (plus ``__init__.py``'s) +- create ``mypkg/test/test_hello.py`` (plus ``__init__.py`` files) - write a simple test function - run ``py.test mypkg``, play around with options -- help your neighbour +test functions and funcargs +==================================================== + +**funcargs** are new since 1.0, unique py.test feature + +let's first look at the motivation ... + Typical test function - viewed abstractly ========================================== :: @@ -270,40 +286,47 @@ def test_something(): inst1 = SomeClass("somevalue") ... - assert self.inst1.method() == "ok" + assert inst1.method() == "ok" -observations and the setup question +observations and the fixture question ========================================== -* test value setup mixes with test code +* test value for Instantiation mixes with test code * the ``mypkg.mod.SomeClass`` reference may change -what if multiple test functions setup the same -classes, maybe with slightly different values? +What if multiple test functions need to create +an instance as well to perform their tests? -old-style: xUnit unit setup +old-style strategy: xUnit-style fixtures ========================================== :: from mypkg.mod import SomeClass - class TestGroup: - def setup_method(self, method): + import unittest + class TestGroup(unittest.TestCase): + def setUp(self): self.inst1 = SomeClass("somevalue") def test_something(self): ... use self.inst1 ... assert self.inst1.method() == "ok" -observations + + +observations / notes ========================================== -* test value setup mixes with test code +* unittest-runner automatically invokes ``self.setUp()`` + for each test function invocation. +* test value for Instantiation mixes with test code * ``mypkg.mod.SomeClass`` reference may change -* **test functions now group by setup code** -* **multiple methods can reuse the same setup** +* **test module has 7 LOCs for one test function** +* **multiple methods reuse the same setup** +* **test functions group by setup code** + meet "funcargs" - test function arguments ========================================== @@ -313,93 +336,108 @@ def test_something(inst1): assert inst1.method() == "ok" - class TestClass: - def test_something(self, inst1): - assert inst1.method() == "ok" + class TestClass: + def test_something(self, inst1): + assert inst1.method() == "ok" observations -========================================== +============== -* test values are simply used in test code -* no imports or mypkg.mod.SomeClass references here + +* test function "requests" arguments it needs * grouping in test classes independent of setup -* multiple functions can re-use the same funcarg setup +* functions/methods re-use same funcarg setup -How to setup the funcarg value? +How to provide the function argument? ========================================== :: - + + #./conftest.py (or test module) + from mypkg.mod import SomeClass def pytest_funcarg__inst1(request): return SomeClass("somevalue") -defined in: test module, ``conftest.py`` or named plugin -observations +funcarg provider observations =================================================== -* funcarg provider separated from test code -* funcarg provider automatically discovered -* funcarg can be used from different test functions +* discovered through naming convention +* loosely coupled, separated from test code +* will be called for each test function +* has one mypkg.mod.SomeClass reference Exercise ========================================== -* write a new package "mypkg" +* write a package "mypkg" * add mypkg/__init__ and mypkg/test_url.py -* add a test_path function that needs an "url" argument -* write the provider in mypkg/conftest.py +* add a ``def test_path(url)`` function +* write a ``pytest_funcarg__url`` provider in mypkg/conftest.py * run your test -Bonus: add more tests, play with wrongly named args. +Bonus: play with wrong funcarg names + Adding funcarg finalizers / teardown ====================================== :: - def pytest_funcarg__inst1(request): + def pytest_funcarg__file(request): f = open(...) request.addfinalizer(lambda: f.close()) return f -Managing longer-living funcargs -====================================== +DEMO ``py.test -s test_addfinalizer.py`` + +cached_setup(): long-living funcargs +================================================= :: def pytest_funcarg__db(request): return request.cached_setup( - setup=lambda: Database(request.config.getvalue("...")), - teardown=lambda db: db.close(), - scope="session" + setup=lambda: Myarg() + teardown=lambda arg: arg.finalize(), + scope="module" """) -**independent from actual test code which just uses the instance** +DEMO ``py.test -s test_cachedsetup.py`` + -request attributes +funcarg request object attributes ====================================== -``request.function``: python function object requesting the argument +funcarg providers always receive a request object: -``request.cls``: class object where the test function is defined in or None. +``request.function``: python test function -``request.module``: module object where the test function is defined in. +``request.cls``: class of test function or None -``request.config``: module object where the test function is defined in. +``request.module``: module where test function lives -Exercise -====================================== +``request.config``: config object (*) + + +mysetup pattern: avoiding too-many-funcargs +=================================================== + +:: + + # ./test_sample.py + def test_answer(mysetup): + app = mysetup.myapp() + answer = app.question() + assert answer == 42 -have your test module and/or test function specify -a parameter for instantiating an application class -that is used in your test function. +-> ``mysetup`` funcarg can easily grow more objects -Parametrizing test function calls +calling test functions multiple times ====================================== -:: +self-contained example:: def test_function(param): assert param < 3 @@ -408,26 +446,28 @@ if "param" not in metafunc.funcargnames: return for i in range(3): - metafunc.addcall(funcargs={'param': i}) + funcargs = {'param': i}) + metafunc.addcall(funcargs=funcargs) -notes -======================= +pytest_generate_tests notes +=============================== -- test code remains unmodified -- pytest_generate_tests can live in conftest.py or plugin -- pytest_generate_tests can implement various parametrization schemes +- test function just takes funcargs +- can live in conftest.py or plugin +- enables various parametrization schemes +- "generative" yield-tests now deprecated see blog post: http://tinyurl.com/ofry94 -funcargs for testing helpers -============================================= +test-function aware helpers ... +=================================================== -example: +:: def test_envreading(inst1, monkeypatch): - monkeypatch.setenv('PREFIX', 'myval') - val = inst1.readenv() + monkeypatch.setenv('INST1', 'myval') + val = inst1._readenv() assert val == "myval" see post http://tinyurl.com/ob87o6 @@ -435,10 +475,11 @@ summary py.test funcargs =========================== -- makes for no-boilerplate and readable test code +- readable no-boilerplate test code +- complexity of fixtures does not affect test code +- can easily be configured - one place to manage fixtures for tests -- nice per-test-function helpers - +- also enable "test-function aware" helpers (questions / demo'ing as time permits) ========================================= @@ -465,74 +506,88 @@ http://marikaz.deviantart.com/ CC 3.0 AN-ND -Customizing py.test +Customizing py.test runs =========================== -- implement hooks (in conftest.py or named plugin) -- implement **new test items** -- providing funcargs +- implement hooks in ``conftest.py`` or named plugin +- implement new funcargs +- invent new test items +- specify plugins / command line opts in conftest.py + +conftest.py +=============================== + +- project specific test configuration(s) +- auto-discovered in test directory or higher up +- can contain hook implementations +- can provide default values for command line options +- can specify plugins to load + +a simple config: ignoring directories +========================================== + +:: -Conftest.py -=============== + # conftest.py + collect_ignore = ['data', 'test_myfile.py'] + +paths are relative to directory of conftest.py + +a simple default option: verbose +========================================== + +:: + + # conftest.py + pytest_option_verbose = True + pytest_option_exitfirst = True -- is auto-discovered in test directory or higher up -- can be used to implement hooks -- can provide default values for command line options -- specifies plugins to use a simple hook: adding a command line opt =========================================== :: - #./conftest.py + # conftest.py def pytest_addoption(parser): - parser.addoption("--url", action="store", default="http://testrun.org") - def pytest_funcarg__url(request): - return request.config.getvalue("url") - -adding helpers to py.test.* namespace -====================================== + parser.addoption("--url", + action="store", default=None) -:: + def pytest_funcarg__url(request): + url = request.getvalue("url") + if url is None: + py.test.skip("need --url") + return url - #./conftest.py - def mycheck(val): - assert val == 42 - def pytest_namespace(config): - return {'mycheck': mycheck} +Hooks are Python functions +=================================== - #./test_module.py - def test_something(): - py.test.mycheck(41) +- hooks are python functions with a ``pytest_`` prefix +- hook functions need to use *exact argument names* Named Plugins ======================== -- named plugins are regular python modules or packages +- normal python modules or packages - fixed "pytest_NAME" naming scheme +- can contain hooks -- ``py.test -p NAME``: load comma-separated list of plugins -- ``PYTEST_PLUGINS=NAME`` environment variable is recognized -- ``pytest_plugins=[...]`` in test module or conftest.py +specifying plugins +=============================== +- command line: ``py.test -p NAME`` +- env variable: ``PYTEST_PLUGINS=NAME`` +- test module / conftest.py: ``pytest_plugins=[...]`` -Hooks -========== - -- hooks are python functions with a ``pytest_`` prefix -- hook implementation need to use *exact argument names* -- no classes let alone sublclasses neccessary! - -Hook categories +hook categories ================== - configuration hooks - collection hooks - runtest hooks - reporting hooks -- configuration / testing process ("event") hooks +- testing process ("event") hooks see py/test/plugin/hookspec.py @@ -541,124 +596,198 @@ :: def pytest_addoption(parser): - """ called before commandline parsing. """ + "called before commandline parsing." def pytest_configure(config): - """ called after command line options have been parsed. " + "called after commandline parsing." def pytest_unconfigure(config): - """ called before test process is exited. """ + "called before test process is exited." -py.test collection objects -============================= +config object / tempdirs +========================== + +- ``config.getvalue(optname)``: return cmdline option value + +- ``config.mktemp(basename)``: create and returns a new tempdir + +- ``config.ensuretemp(basename)``: create or return a new tempdir + +tempdirs are created as subdirs of a per-session testdir + +sidenote: py.path objects +================================= + +py.test often uses ``py.path.local`` objects +for accessing the filesystem:: + + >>> p = py.path.local() # curdir + >>> newp = p.join("hello.txt") + >>> newp.write("file content of hello.txt") + >>> s = newp.read() + >>> ... + + +exercise +============== -* collectors, ``collect()`` returns list of "colitems" -* items, implement ``runtest()`` for running a test +write a named plugin ``pytest_tmpfs.py`` which contains a +``pytest_funcarg__tmpfs`` provider which creates +a TmpFS instance. The TmpFS class should contain +this method: -test collection tree + makefile(basename, content) -> py.path.local object + +and your test module should contain a simple test function +that uses ``tmpfs`` to create a file and checks that +the content has been written. + + +py.test collection tree ======================== - collection tree is built iteratively -- test collection starts from directories or files (via cmdline) +- test collection starts from directories or files +- ``collector.collect()`` returns "colitem" list -py.test.collect.* filesystem objects +py.test.collect.* objects ====================================== - **Directory** - **File** +- Python specific collection objects: + + - Module + - Class + - Instance + - Function Demo/Exercise "collection tree" ==================================== -inspecting the test collection tree:: +inspecting the test collection tree of your own examples:: py.test --collectonly +Collection Hooks +====================== +:: -collection node Attributes -======================================= + def pytest_collect_directory(path, parent): + "return Collection node or None. " -**parent** a reference to the collector that produced us + def pytest_collect_file(path, parent): + "return Collection node or None." -**name**: a name that is unique within the scope of our parent + def pytest_pycollect_makeitem(collector, name, obj): + "return custom item/collector or none." -**config**: test configuration +Example "new item" plugins +===================================== -Python object collectors -======================================= +pytest_unittest: run traditional ``unittest.py`` modules -**obj** points to the underlying python object. +pytest_doctest: run doctests -- **Module** -- **Class**/**Instance** -- **Function** +pytest_restdoc: check ReST syntax and remote URLs for ``.txt`` files -Collection Hooks -====================== -:: +testing javascript in live browsers: see talk tuesday afternoon! - def pytest_collect_file(self, path, parent): - """ return Collection node or None. """ - def pytest_collect_directory(self, path, parent): - """ return Collection node or None. """ +runtest hooks +==================== - def pytest_collect_recurse(self, path, parent): - """ return True/False to cause/prevent recursion. """ +:: -Python collection hooks -===================================== -:: - def pytest_pymodule_makeitem(self, modcol, name, obj): - """ return custom item/collector for a python object in a module, or None. """ + def pytest_runtest_setup(item): + "called before pytest_runtest_call()." -function test run hooks -========================== -:: - def pytest_pyfunc_call(self, pyfuncitem, args, kwargs): - """ return True if we consumed/did the call to the python function item. """ + def pytest_runtest_call(item): + "execute test item." + + def pytest_runtest_teardown(item): + "called after pytest_runtest_call()." - def pytest_item_makereport(self, item, excinfo, when, outerr): - """ return ItemTestReport event for the given test outcome. """ + def pytest_pyfunc_call(pyfuncitem): + "perform python function call." Reporting hooks ========================== :: - def pytest_report_teststatus(self, event): - """ return shortletter and verbose word. """ - def pytest_terminal_summary(self, terminalreporter): - """ add additional section in terminal summary reporting. """ + def pytest_runtest_makereport(item, call): + "make report for call object." -Event methods -=============== + def pytest_runtest_logreport(rep): + "process report for logging." -are only called, no interaction. + def pytest_report_teststatus(self, event): + "return cat, shortletter, verbose word." -see ``py/test/plugin/pytest_terminal.py`` -for a full selection of events. + def pytest_terminal_summary(self, terminalreporter): + "add section to summary reporting." -Warning -=============== -naming changes might take place before 1.0 final - -Writing cross-project plugins +runtest/report related examples ================================== -- put plugin into pytest_name.py or package -- make a "NamePlugin" class available -- release or copy to somewhere importable +- pytest_figleaf.py: generate coverage report at end -Exercise -================================== +- pytest_xfail.py: display xpassed/xfailed specially + +- pytest_pdb.py: invoke PDB on python test failure + +- pytest_resultlog.py: produce buildbot friendly output + + +the py.test.* namespace hook +====================================== + +if you need a global helper:: -* port conftest plugin to global "pytest_myapp" plugin. -* put pytest_myapp somewhere where it's importable (or release it :) -* put "pytest_plugins = 'pytest_myapp'" into your test module + #./conftest.py or named plugin + def mycheck(val): + assert val == 42 + + def pytest_namespace(config): + return {'mycheck': mycheck} + + #./test_module.py + def test_something(): + py.test.mycheck(41) + + +pytest_namespace examples +=================================== + +``py.test.mark`` for settings keywords + +``py.test.deprecated_call`` for checking for deprecation calls +doing helpers in py.test +=============================== -Distributed Testing (45 minutes) +**funcargs**: test-function run aware + +**py.test.NAME**: globally available + +test code does not need to +import/know implementation location + + +Summary customization +========================= + +- conftest.py for per-project test config and hooks +- many hooks for interacting with py.test +- named plugins for general test support code + +hook specs: py/test/plugin/hookspec.py + +default plugin examples: py/test/plugin/pytest_* + + +Distributed Testing (30 minutes) ==================================== .. image:: img/rails_in_the_city_by_marikaz.jpg @@ -671,8 +800,8 @@ the basic idea ==================================== -collect tests locally, -run tests in separated test execution processes. +1. collect tests locally +2. run tests in one/more execution processes. test distribution modes ==================================== @@ -702,7 +831,8 @@ To send tests to multiple CPUs, type:: - py.test --dist=load --tx popen --tx popen --tx popen + py.test --dist=load \ + --tx popen --tx popen --tx popen or in short:: @@ -716,28 +846,32 @@ :: - py.test --d --tx ssh=myhostpopen --rsyncdir mypkg mypkg + py.test --d --tx ssh=myhostpopen \ + --rsyncdir mypkg mypkg Sending tests to remote socket servers ======================================== -Download and start +Download and start py/execnet/script/socketserver.py + +or -http://codespeak.net/svn/py/dist/py/execnet/script/socketserver.py +http://codespeak.net/~hpk/socketserver.py -Assuming an IP address, you can now tell py.test to distribute: : - py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg +Assuming an IP address, you can now tell py.test to distribute:: + + py.test --dist=each --tx socket=192.168.1.102:8888 \ + --rsyncdir mypkg mypkg Exercise: Move settings into conftest ======================================== mypkg/conftest.py:: + rsyncdirs = ['.'] pytest_option_tx = ['ssh=myhost', 'socket=...'] -mypkg/conftest.py:: - rsyncdirs = ['.'] Distribution modes ======================================== @@ -748,10 +882,10 @@ py.test --dist=load mypkg -how does py.test do all this? +how does py.test distribute tests? ======================================== -it uses py.execnet! +it uses **py.execnet** ``py.execnet`` ======================================== @@ -761,6 +895,9 @@ * asynchronously send and receive data between processes through channels * completely avoid manual installation steps on remote places +see talk Thursday morning + + xspecs: Exec environment specs ================================= @@ -778,20 +915,36 @@ * ``chdir=path`` change remote working dir to given relative or absolute path * ``nice=value`` decrease remote nice level if platforms supports it -xspec examples +Exercise ================================= -:: - ssh=wyvern//python=python2.4//chdir=mycache +* Play with runing your tests in multiple interpreters or processes +* see if you can get access to your neighbour's PC + +xspec examples:: + popen//python=2.5//nice=20 + ssh=wyvern//python=python2.4//chdir=mycache socket=192.168.1.4:8888 -Exercise -================================= -* Play with runing your tests in multiple interpreters or processes -* see if you can get access to your neighbour's PC +"running tests until all pass" +================================= + +:: + + py.test --looponfailing + +"running tests even if they crash" +==================================== + +:: + + py.test --boxed + +(requires ``os.fork``) + Feedback round ================== @@ -802,4 +955,5 @@ holger krekel at merlinux eu +http://pytest.org Modified: py/extradoc/talk/ep2009/rapid-testing/test_cached_setup.py ============================================================================== --- py/extradoc/talk/ep2009/rapid-testing/test_cached_setup.py (original) +++ py/extradoc/talk/ep2009/rapid-testing/test_cached_setup.py Mon Jun 29 13:23:30 2009 @@ -10,7 +10,7 @@ return request.cached_setup( setup=Myarg, teardown=lambda arg: arg.finalize(), - scope="module", + scope="function", ) def test_hello(myarg): From commits-noreply at bitbucket.org Mon Jun 29 13:29:19 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Mon, 29 Jun 2009 11:29:19 -0000 Subject: [py-svn] commit/py-trunk: 5 new changesets Message-ID: <20090629112919.13696.92440@domU-12-31-39-00-D4-C1.compute-1.internal> 5 new changesets in py-trunk: http://www.bitbucket.org/hpk42/py-trunk/changeset/333781021aa2/ changeset: r1184:333781021aa2 user: hpk date: 2009-06-28 13:19:43 summary: re-adding py.test.mark as documented (!) by adding pytest_keyword plugin affected #: 7 files (2.2 KB) http://www.bitbucket.org/hpk42/py-trunk/changeset/94cad81e8807/ changeset: r1185:94cad81e8807 user: hpk date: 2009-06-28 13:27:34 summary: re-introduce py.test.mark and move py.test.xfail to py.test.mark.xfail affected #: 6 files (278 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/2cc0507f117f/ changeset: r1186:2cc0507f117f user: hpk date: 2009-06-29 08:32:33 summary: fix typo affected #: 1 file (6 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/d65932865163/ changeset: r1187:d65932865163 user: hpk date: 2009-06-29 12:40:24 summary: Added tag 1.0.0b6 for changeset 2cc0507f117f affected #: 1 file (49 bytes) http://www.bitbucket.org/hpk42/py-trunk/changeset/4675c07dba4c/ changeset: r1188:4675c07dba4c user: hpk date: 2009-06-29 12:50:02 summary: merge 1.0.x branch affected #: 3 files (33 bytes) Repository URL: http://bitbucket.org/hpk42/py-trunk/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email. From py-svn at codespeak.net Mon Jun 29 16:45:15 2009 From: py-svn at codespeak.net (Pendon Jina) Date: Mon, 29 Jun 2009 16:45:15 +0200 (CEST) Subject: [py-svn] Professionals' meeting Message-ID: An HTML attachment was scrubbed... URL: From py-svn at codespeak.net Mon Jun 29 23:40:28 2009 From: py-svn at codespeak.net (Delta Etekar) Date: Tue, 30 Jun 2009 00:40:28 +0300 Subject: [py-svn] Let's perform a trick Message-ID: <2603TXB.18410EB.7453128421828QYRNNHVNGHFKTZR58@[94.120.47.142]> An HTML attachment was scrubbed... URL: From commits-noreply at bitbucket.org Tue Jun 30 06:28:23 2009 From: commits-noreply at bitbucket.org (commits-noreply at bitbucket.org) Date: Tue, 30 Jun 2009 04:28:23 -0000 Subject: [py-svn] commit/py-3k: yangyan5: tests of builtin/, cmdline/, io/, log/, path/, process/, rest/, thread/, tool/, and xmlobj/ pass under python3.1 Message-ID: <20090630042823.13696.8400@domU-12-31-39-00-D4-C1.compute-1.internal> 1 new changeset in py-3k: http://www.bitbucket.org/yangyan5/py-3k/changeset/d081651e4027/ changeset: r1161:d081651e4027 user: yangyan5 date: 2009-06-30 06:25:28 summary: tests of builtin/, cmdline/, io/, log/, path/, process/, rest/, thread/, tool/, and xmlobj/ pass under python3.1 affected #: 29 files (15.1 KB) Repository URL: http://bitbucket.org/yangyan5/py-3k/ -- This is a commit notification from bitbucket.org. You are receiving this because you have the service enabled, addressing the recipient of this email.