Compare commits

...

137 Commits
1.2 ... 1.10

Author SHA1 Message Date
holger krekel
b0732f99a6 final 2014-01-29 13:10:20 +01:00
holger krekel
ab987d512c make pexpect specific environments so that the canonical ones also run on windows. Also add a little sleep to a pexpect-related tests in the hope it helps the test consistently passing (Bah). 2014-01-27 19:47:37 +01:00
holger krekel
ffa156ced5 refine distribution fractions and make it dependend on the duration
of the last test in a node's set.
2014-01-27 12:39:43 +01:00
holger krekel
e26d4486fc send multiple "to test" indices in one network message to a slave
and improve heuristics for sending chunks where the chunksize
depends on the number of remaining tests rather than fixed numbers.
This reduces the number of master -> node messages (but not the
reverse direction)
2014-01-27 11:37:40 +01:00
holger krekel
18a30fab7d fix issue419: work with collection indices instead of node ids.
This reduces network message size.
2014-01-27 11:37:33 +01:00
holger krekel
dd73d132b1 remove unneccesary item2nodes data structure for load scheduling 2014-01-27 11:37:27 +01:00
holger krekel
a22e0384a1 a failing test for pytest issue419 2014-01-27 11:37:26 +01:00
holger krekel
8eabc7d5df Merged in lukaszb/pytest-xdist/lukaszb/readmetxt-typo-1388766852691 (pull request #7)
README.txt typo
2014-01-04 21:41:34 +01:00
Lukasz Balcerzak
dbf7932514 README.txt typo 2014-01-03 16:34:15 +00:00
holger krekel
6355c3e8d0 add glob support for rsyncignores, add command line option to pass
additional rsyncignores. Thanks Anatoly Bubenkov.
2013-12-06 13:26:09 +01:00
holger krekel
c505f00320 Merged in paylogic/pytest-xdist/rsyncignore-option (pull request #5)
add glob support for rsyncignore. add command line option for rsyncignore
2013-12-06 13:25:19 +01:00
Anatoly Bubenkov
7e066be35e docs fixed 2013-12-06 10:21:32 +01:00
Anatoly Bubenkov
27f1d8049b merge with mainline 2013-12-06 10:20:03 +01:00
Anatoly Bubenkov
bcf1c85f44 docs fixed 2013-12-06 10:19:18 +01:00
Anatoly Bubenkov
a3fac52e85 more readability for return 2013-12-05 23:22:49 +01:00
Anatoly Bubenkov
152f965a70 fix ignores mixing 2013-12-05 16:35:56 +01:00
Anatoly Bubenkov
027b3f51ed change metavars 2013-12-05 15:31:52 +01:00
Anatoly Bubenkov
f4b86f0127 add glob support for rsyncignore. add command line option for rsyncignore 2013-12-05 15:19:31 +01:00
holger krekel
8146b27671 now that pytest_runtest_logstart is sent to the master again,
disable showing of filenames explicitely with the terminalreporter
2013-11-21 15:00:06 +01:00
holger krekel
a488cc5add merge 2013-11-19 12:21:31 +01:00
holger krekel
aad1aace52 merge fix pytest issue382 - produce "pytest_runtest_logstart" event again
in master. Thanks Aron Curzon.
2013-11-19 12:08:55 +01:00
curzona
82d357ab1a Uncomment logstart hook in remote.py and slavemanage.py 2013-11-09 12:10:13 -08:00
holger krekel
ca54911481 ignore directories and pyc files for changes (editors write tmp/swap
files etc., which also affects mtime of directory)
2013-10-07 10:28:08 +02:00
holger krekel
8e88261c15 ignore dot files for file changes (editors write tmp/swap files etc.) 2013-10-07 10:20:24 +02:00
holger krekel
6045195f16 Added tag 1.9 for changeset 5c5cb6d59e12 2013-10-04 14:35:34 +02:00
holger krekel
b88b23e358 some test fixes and configure tox-testing against pytest==2.3.5 and pytest>=2.4.2 2013-10-04 10:34:24 +02:00
holger krekel
a9aca9a3ae fix pytest issue347: slaves running on top of Python3.2
will set PYTHONDONTWRITEYBTECODE to 1 to avoid import concurrency
bugs.
2013-09-05 18:52:59 +02:00
holger krekel
45dd5975f0 Merged in dimazest/pytest-xdist/collection_diff_reporting (pull request #3)
Give more context when nodes collected different tests.
2013-09-05 16:23:12 +02:00
Dmitrijs Milajevs
fa96143aeb Cleanup 2013-09-05 15:30:37 +02:00
Dmitrijs Milajevs
9a50d9de17 Give more context when nodes collected different tests. 2013-09-05 15:24:27 +02:00
holger krekel
f144ef88e3 fix pytest issue41: re-run tests on all file changes, not just
randomly select ones like .py/.c.
2013-09-05 14:21:43 +02:00
holger krekel
013b2fc6bb streamline tox.ini 2013-09-05 14:21:28 +02:00
holger krekel
df66687eae bump version 2013-04-30 17:08:06 +02:00
holger krekel
75c125e04e Merged in jjh/pytest-xdist/catch-import-errors (pull request #2)
Patch for issue #180
2013-04-30 17:07:09 +02:00
Jonathan Hunt
26016aefd1 Use the error longrepr rather than the file path to check
for duplicate errors.
2013-04-28 16:14:52 -07:00
Jonathan Hunt
6c1587a157 Added an additional test for import errors in --tx mode. 2013-04-28 16:06:41 -07:00
Jonathan Hunt
10c9c34c75 Passing tests, avoiding duplicate errors. 2013-04-25 18:20:40 -07:00
Jonathan Hunt
46b94c1c81 Report ImportErrors in distributed mode. 2013-04-25 18:04:24 -07:00
Jonathan Hunt
6038ec21ca Added test to catch import failure in distributed mode. 2013-04-25 17:45:42 -07:00
holger krekel
0be3d4b077 fix py3 issue 2013-04-02 10:48:26 +02:00
holger krekel
34eec1ef17 - changed LICENSE to MIT
- attribute Jeremy's change
2013-04-02 10:33:36 +02:00
holger krekel
c44fe4e234 merge again 2013-04-02 10:30:52 +02:00
holger krekel
a771352f01 merge 2013-04-02 10:29:51 +02:00
holger krekel
6369c8c7ce Merged in jerith/pytest-xdist/dedupe-failures (pull request #1)
Avoid collecting duplicate failures when using --looponfail.
2013-04-02 10:29:21 +02:00
Jeremy Thurgood
587d1501a7 Avoid collecting duplicate failures when using --looponfail. 2013-03-27 17:34:58 +02:00
holger krekel
53c4025774 add boxed example 2012-11-20 13:26:29 +01:00
holger krekel
8ed3788992 mention that --boxed is not available on windows 2012-09-27 15:09:55 +02:00
holger krekel
60f035bcae improve README to cover --boxed option and contain some more links 2012-08-04 09:54:51 +02:00
holger krekel
d766c0777a add a note where to find the issue tracker 2012-04-30 18:31:13 -06:00
holger krekel
b235db879c merge 2012-01-22 06:14:22 +01:00
Ronny Pfannschmidt
663cd81117 add a test to check deserialization of nonstandard test report attributes 2012-01-20 19:53:41 +01:00
holger krekel
f56c316645 Added tag 1.8 for changeset 1d27987c2675 2011-12-16 11:59:21 +00:00
holger krekel
0387c57b76 merge 2011-12-16 11:59:17 +00:00
holger krekel
bca7a5c35b Added tag 1.8 for changeset 0d1c00018008 2011-12-16 11:57:23 +00:00
holger krekel
689bf7262e streamline MANIFEST file 2011-12-16 11:47:28 +00:00
holger krekel
5488b6d9a1 bump versions 2011-12-16 10:47:33 +00:00
holger krekel
aed7a5a07e fix tests by properly removing __pycache__ files 2011-12-15 07:41:09 +01:00
holger krekel
927e95176f try py31/py32 environments in tox.ini 2011-12-12 21:12:25 +00:00
holger krekel
8c6fea9895 remove usage of final_logerrors now that we have better teardown information 2011-12-02 21:03:58 +00:00
holger krekel
1f6d410481 fix issue 93 of pytest - avoid delayed teardowns 2011-12-02 21:03:29 +00:00
holger krekel
d5ff8b066a tag 1.7 2011-11-18 19:08:05 +00:00
holger krekel
750ada16d1 Added tag 1.7 for changeset 29c38e195526 2011-11-18 19:07:56 +00:00
holger krekel
c1f27bf87e depend on 2.2.0 2011-11-18 18:50:26 +00:00
holger krekel
c19f5e4bfd Added tag 1.7 for changeset 20875fed94e7 2011-11-18 18:49:29 +00:00
holger krekel
8a923ea2f4 remove unncessary hack here as well 2011-11-18 17:01:39 +00:00
holger krekel
e8ae2f9090 small fixes to test suite 2011-11-18 16:03:02 +00:00
holger krekel
f946ab1835 adapt to pytest-2.2.0.dev2 2011-11-08 17:54:26 +00:00
holger krekel
1c2563d864 pull pytest from testrun for py26 2011-07-07 23:14:34 +02:00
holger krekel
b04616419e Added tag 1.6 for changeset 4815040bdad8 2011-04-17 23:10:37 +02:00
holger krekel
722006f7f6 bump version to 1.6 2011-04-17 23:10:20 +02:00
holger krekel
d9c5f18478 fix race condition as reported by Ralf Schmitt 2011-04-14 21:35:27 +02:00
holger krekel
bd30e96ab0 fix issue34 2011-03-16 17:59:14 +01:00
holger krekel
eb09e22b62 fix test 2010-12-07 12:47:36 +01:00
holger krekel
319e247de5 bump version 2010-12-07 12:38:45 +01:00
holger krekel
1c712fe6a0 fix and shorten collection reporting and fix a test wrt internal pytest api 2010-12-07 12:00:22 +01:00
holger krekel
a6a4158b78 Added tag 1.5 for changeset cd44a941c833 2010-11-25 21:03:30 +01:00
holger krekel
1167186f65 upping version to 1.5 2010-11-25 20:11:59 +01:00
holger krekel
125991f180 rel preps 2010-11-25 16:41:16 +01:00
holger krekel
fc1f29db68 * introduce progress-reporting
* rename somewhat internal pytest_gwmanage_* events to pytest_xdist_*
* make gateway setup a separate step
2010-11-22 15:39:56 +01:00
holger krekel
9108790801 merge nodemanager and gwmanager 2010-11-22 12:05:55 +01:00
holger krekel
3fc03c8c8e fixes for new internal pytest tmpdir handling 2010-11-22 11:41:23 +01:00
holger krekel
c924dda30f shift documentation, remove duplicate code 2010-11-18 14:57:55 +01:00
holger krekel
9e982f0f73 important for allowing addopts + looponfail 2010-11-17 22:12:48 +01:00
holger krekel
705fe5962b another fix to tox.ini 2010-11-13 22:24:53 +01:00
holger krekel
d1f799064b fox tox.ini file 2010-11-13 22:20:05 +01:00
holger krekel
47d3e99083 fix and unify remote skip test/collect reports 2010-11-13 20:53:35 +01:00
holger krekel
d81ffaa980 some changes to make remote testing work better - slightly hackish and will remain so until 'tox' is used to create environments and properly set things up. 2010-11-13 19:47:01 +01:00
holger krekel
a0652e9d99 adapt to new import locations 2010-11-13 11:12:19 +01:00
holger krekel
8272a669a4 adapt to collection -> session renaming of pytest core 2010-11-07 10:26:04 +01:00
holger krekel
9d55d3c831 adapt to pytest changes, add looponfailingdirs ini-option 2010-11-06 09:58:00 +01:00
holger krekel
985c9163b7 unregister terminal plugin in subprocesses before it gets configured 2010-11-01 09:22:49 +01:00
holger krekel
4661db0fef don't add opts in sub processes, add -rf by default 2010-11-01 08:56:47 +01:00
holger krekel
4cd76363f6 adapt to new pytest changes, use new addini/getini methods for rsyncdirs / rsyncignore options 2010-11-01 00:27:43 +01:00
holger krekel
f1a77e80e8 fix for collection unification changes on pytest dev 2010-10-26 00:04:36 +02:00
holger krekel
e4bd57b670 fix tox.ini config to pull package deps from testrun.org 2010-10-23 17:24:00 +02:00
holger krekel
516b5cfe6a remove hudson artifact handling in favour of indexserver 2010-10-15 21:15:48 +02:00
holger krekel
9675d754db require pytest instead of pylib 2010-10-12 22:26:13 +02:00
holger krekel
7b73e081fe adapt to pytest trunk changes - config moved to plugin 2010-10-12 15:00:37 +02:00
holger krekel
d83af5fdcd adapt to shifted plugin names 2010-10-11 01:15:31 +02:00
holger krekel
025f1d577e adapt xdist plugin - it still uses some internal imports 2010-10-10 15:33:32 +02:00
holger krekel
dc67e46d59 another python3 syntax error, gah. 2010-10-07 11:07:32 +02:00
holger krekel
51815940c2 increase TIMEOUT for waits in tests, fix python3 syntax 2010-10-07 09:00:07 +02:00
holger krekel
9df55b1c60 refactor and port dsession tests, re-introduce --dist testing along
with a simple scheduler.
2010-09-28 16:32:42 +02:00
holger krekel
3767ce3269 move some code around, ending up with fewer files logically grouped 2010-09-28 16:08:31 +02:00
holger krekel
f7923c692e move code to fewer more sensible modules. 2010-09-27 16:13:57 +02:00
holger krekel
3ad58ae21f fix distributed testing reporting details 2010-09-27 16:13:56 +02:00
holger krekel
749faf5e7c re-introduce slaveinput/slaveoutput configure protocol 2010-09-27 16:13:55 +02:00
holger krekel
c90c4a254c make rsyncing work again, some streamlining in termination detection 2010-09-27 16:13:55 +02:00
holger krekel
2efb059d75 adapt to new refactored py baseline.
remove pickling in distributed testing
refactor internal mechanism enough to work for -n1
* * *
fix handling of crashing items and slave down events
2010-09-27 16:13:54 +02:00
holger krekel
e03f6d76b6 make looponfailing work with the new py-trunk developments, refactor internal sesssion handling 2010-09-11 18:34:35 +02:00
holger krekel
4d0422a548 remove trailing whitespace from sources 2010-09-07 10:46:58 +02:00
holger krekel
198a2c2d38 improved and simpler tox.ini for pytest-xdist desting 2010-07-14 23:18:32 +02:00
holger krekel
518fae1f21 Added tag 1.4 for changeset a423748bf17e 2010-07-08 17:22:56 +02:00
holger krekel
feaf840fab just use the released version fo execnet 2010-07-07 18:33:27 +02:00
holger krekel
f112ad6cfb typo 2010-07-07 18:14:28 +02:00
holger krekel
71771fbdc6 improved versioing 2010-07-07 18:12:43 +02:00
holger krekel
c0b2df1a77 show detailed gateway setup info only with "--verbose" or "-v" 2010-07-07 16:44:12 +02:00
holger krekel
9aa1df89ee bump to xdist 1.4 2010-07-07 14:52:40 +02:00
holger krekel
67e0cd463c add a test created from jgustak (but add the related env feature to execnet where it
is separately tested)
2010-07-07 14:33:24 +02:00
holger krekel
fb31aa882d integrate xdist related reporting into the plugin 2010-07-07 12:43:19 +02:00
holger krekel
51b0ba55e9 don't test deprecation warnings on python2.7 2010-07-06 15:01:40 +02:00
holger krekel
d0ccb5743d refining tests for jython and python2.7 2010-07-06 13:50:03 +02:00
holger krekel
fa7da18d9b using proper distshare dir for hudson 2010-07-06 12:56:15 +02:00
holger krekel
cf36bf8bf2 bumping version 2010-07-06 12:51:09 +02:00
holger krekel
55e3d27820 adding a tox.ini file 2010-07-06 12:50:06 +02:00
holger krekel
a17bc05c5a merge 2010-06-27 14:00:01 +02:00
holger krekel
e5e23fd278 add a test for issue57 which currently needs to be fixed on py-trunk though 2010-05-26 18:52:58 +02:00
holger krekel
43c0591c55 Added tag 1.3 for changeset e6c4ce20db4b 2010-05-25 21:26:09 +02:00
holger krekel
7bf3c7f029 bump version number 2010-05-25 21:26:06 +02:00
holger krekel
e48dcda834 Added tag 1.3 for changeset eaf8b1cb7c31 2010-05-25 21:23:29 +02:00
holger krekel
faa03ad601 merge 2010-05-25 21:23:23 +02:00
holger krekel
e0f61e4fa2 merge 2010-05-25 21:00:42 +02:00
holger krekel
f7be994848 adapt for py-trunk/1.3.1 changes wrt --exitfirst -> --maxfailures 2010-05-25 16:59:08 +02:00
holger krekel
7b19de5450 Added tag 1.2 for changeset 56d8e5280be2 2010-05-25 12:02:29 +02:00
holger krekel
0c4f5eced2 fix conftest related documentation 2010-05-18 18:40:48 +02:00
holger krekel
e593841a70 fix looponfailing issue to always run against the newest version of the source 2010-05-17 17:32:34 +02:00
holger krekel
43b693258f xdist is now somewhat stable after all the fixes, i'd say. 2010-05-05 21:21:39 +02:00
38 changed files with 2880 additions and 2788 deletions

View File

@@ -14,8 +14,15 @@ syntax:glob
*.class
*.orig
*.sublime-*
.Python
build/
dist/
include/
lib/
bin/
pytest_xdist.egg-info
issue/
3rdparty/
.tox

14
.hgtags
View File

@@ -1,2 +1,16 @@
42c6503ee48fae9c4c96d406afb12bfc86f15803 1.0
eca7ce17eabf296983c36812c8b8be901e7055a3 1.1
56d8e5280be224a0ad3220a9deed55334710bd23 1.2
e6c4ce20db4bf65086ff55807a3c306cad7ca393 1.3
e6c4ce20db4bf65086ff55807a3c306cad7ca393 1.3
eaf8b1cb7c312883598677231be5bbeea3b5c127 1.3
a423748bf17ee778a37853225210257699cad9c1 1.4
cd44a941c833c098e4899fe3d42a96703754d0d5 1.5
4815040bdad8f182a5487f57a9da385483836e75 1.6
20875fed94e7f3dff50bdf762df91153b15ceca6 1.7
20875fed94e7f3dff50bdf762df91153b15ceca6 1.7
29c38e195526f5f0fdd651fb51f59d6efaaafbb0 1.7
0d1c00018008433956aa7d93007bab6ea7de96e4 1.8
0d1c00018008433956aa7d93007bab6ea7de96e4 1.8
1d27987c267577899350a25ba5828d55d87083ad 1.8
5c5cb6d59e12e566fbb0217aea718dc31578bee1 1.9

122
CHANGELOG
View File

@@ -1,33 +1,123 @@
1.10
-------------------------
- add glob support for rsyncignores, add command line option to pass
additional rsyncignores. Thanks Anatoly Bubenkov.
- fix pytest issue382 - produce "pytest_runtest_logstart" event again
in master. Thanks Aron Curzon.
- fix pytest issue419 by sending/receiving indices into the test
collection instead of node ids (which are not neccessarily unique
for functions parametrized with duplicate values)
- send multiple "to test" indices in one network message to a slave
and improve heuristics for sending chunks where the chunksize
depends on the number of remaining tests rather than fixed numbers.
This reduces the number of master -> node messages (but not the
reverse direction)
1.9
-------------------------
- changed LICENSE to MIT
- fix duplicate reported test ids with --looponfailing
(thanks Jeremy Thurgood)
- fix pytest issue41: re-run tests on all file changes, not just
randomly select ones like .py/.c.
- fix pytest issue347: slaves running on top of Python3.2
will set PYTHONDONTWRITEYBTECODE to 1 to avoid import concurrency
bugs.
1.8
-------------------------
- fix pytest-issue93 - use the refined pytest-2.2.1 runtestprotocol
interface to perform eager teardowns for test items.
1.7
-------------------------
- fix incompatibilities with pytest-2.2.0 (allow multiple
pytest_runtest_logreport reports for a test item)
1.6
-------------------------
- terser collection reporting
- fix issue34 - distributed testing with -p plugin now works correctly
- fix race condition in looponfail mode where a concurrent file removal
could cause a crash
1.5
-------------------------
- adapt to and require pytest-2.0 changes, rsyncdirs and rsyncignore can now
only be specified in [pytest] sections of ini files, see "py.test -h"
for details.
- major internal refactoring to match the pytest-2.0 event refactoring
- perform test collection always at slave side instead of at the master
- make python2/python3 bridging work, remove usage of pickling
- improve initial reporting by using line-rewriting
- remove all trailing whitespace from source
1.4
-------------------------
- perform distributed testing related reporting in the plugin
rather than having dist-related code in the generic py.test
distribution
- depend on execnet-1.0.7 which adds "env1:NAME=value" keys to
gateway specification strings.
- show detailed gateway setup and platform information only when
"-v" or "--verbose" is specified.
1.3
-------------------------
- fix --looponfailing - it would not actually run against the fully changed
source tree when initial conftest files load application state.
- adapt for py-1.3.1's new --maxfailure option
1.2
-------------------------
- fix issue79: sessionfinish/teardown hooks are now called systematically
on the slave side
- introduce a new data input/output mechanism to allow the master side
to send and receive data from a slave.
- fix race condition in underlying pickling/unpickling handling
- use and require new register hooks facility of py.test>=1.3.0
- require improved execnet>=1.0.6 because of various race conditions
that can arise in xdist testing modes.
- fix some python3 related pickling related race conditions
- fix PyPI description
- fix issue79: sessionfinish/teardown hooks are now called systematically
on the slave side
- introduce a new data input/output mechanism to allow the master side
to send and receive data from a slave.
- fix race condition in underlying pickling/unpickling handling
- use and require new register hooks facility of py.test>=1.3.0
- require improved execnet>=1.0.6 because of various race conditions
that can arise in xdist testing modes.
- fix some python3 related pickling related race conditions
- fix PyPI description
1.1
-------------------------
- fix an indefinite hang which would wait for events although no events
are pending - this happened if items arrive very quickly while
are pending - this happened if items arrive very quickly while
the "reschedule-event" tried unconditionally avoiding a busy-loop
and not schedule new work.
and not schedule new work.
1.0
-------------------------
- moved code out of py-1.1.1 into its own plugin
- use a new, faster and more sensible model to do load-balancing
- use a new, faster and more sensible model to do load-balancing
of tests - now no magic "MAXITEMSPERHOST" is needed and load-testing
works effectively even with very few tests.
- cleaned up termination handling
- make -x cause hard killing of test nodes to decrease wait time
works effectively even with very few tests.
- cleaned up termination handling
- make -x cause hard killing of test nodes to decrease wait time
until the traceback shows up on first failure

View File

@@ -1,10 +1,31 @@
allow to run xdist tests with xdist
next release critical
-----------------------------------------------
tag: feature
tag: bug
miserably fails: --dist=each --tx popen --tx socket=...
rename / hooks
-----------------------------------------------
tag: bug
node -> slave
transition for hooks?
configure_node -> configure_slave
allow to remotely run xdist tests with xdist
-----------------------------------------------
tag: feature
allow to run xdist own tests using its own mechanism.
currently this doesn't work because the remote side
has no py.test plugin. How to configure/do
register "xdist.plugin" on the remote side?
has no py.test plugin. How to configure/do
register "xdist.plugin" on the remote side?
see to avoid any "from _pytest" internal imports
-----------------------------------------------
tag: feature
currently tests and even xdist core code imports
names from the internal _pytest namespace.
See to avoid it.

28
LICENSE
View File

@@ -1,13 +1,19 @@
The execnet package is released under the provisions of the Gnu Public
License (GPL), version 2 or later.
See http://www.fsf.org/licensing/licenses/ for more information.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
This package also contains some minor parts which
which are useable under the MIT license.
If you have questions and/or want to use parts of
the code under a different license than the GPL
please contact me.
holger krekel, January 2010, holger at merlinux eu

View File

@@ -2,9 +2,6 @@ include CHANGELOG
include LICENSE
include README.txt
include setup.py
include tox.ini
graft testing
exclude *.orig
exclude *.rej
recursive-exclude testing *.pyc *$py.class
prune .svn
prune .hg

View File

@@ -1,29 +1,212 @@
py.test xdist plugin: distributed testing and loop failure
xdist: pytest distributed testing plugin
===============================================================
.. _`pytest-xdist respository`: http://bitbucket.org/hpk42/pytest-xdist
.. _`pytest`: http://pytest.org
The `pytest-xdist`_ plugin extends py.test with some unique
test execution modes:
The pytest-xdist plugin extends `py.test`_ to ad-hoc distribute test
runs to multiple CPUs or remote machines. It requires setuptools
or distribute which help to pull in the neccessary execnet and
pytest-core dependencies.
* test run parallelization_: if you have multiple CPUs or hosts you can use
those for a combined test run. This allows to speed up
development or to use special resources of `remote machines`_.
Install the plugin locally with::
* ``--boxed``: (not available on Windows) run each test in a boxed_
subprocess to survive ``SEGFAULTS`` or otherwise dying processes
python setup.py install
* ``--looponfail``: run your tests repeatedly in a subprocess. After each run
py.test waits until a file in your project changes and then re-runs
the previously failing tests. This is repeated until all tests pass
after which again a full run is performed.
or use the package in develope/in-place mode, particularly
useful with a checkout of the `pytest-xdist repository`_::
* `Multi-Platform`_ coverage: you can specify different Python interpreters
or different platforms and run tests in parallel on all of them.
Before running tests remotely, ``py.test`` efficiently "rsyncs" your
program source code to the remote place. All test results
are reported back and displayed to your local terminal.
You may specify different Python versions and interpreters.
Installation
-----------------------
Install the plugin with::
easy_install pytest-xdist
# or
pip install pytest-xdist
or use the package in develope/in-place mode with
a checkout of the `pytest-xdist repository`_ ::
python setup.py develop
or use one of::
Usage examples
---------------------
easy_install pytest-xdist
.. _parallelization:
pip install pytest-xdist
Speed up test runs by sending tests to multiple CPUs
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
To send tests to multiple CPUs, type::
py.test -n NUM
Especially for longer running tests or tests requiring
a lot of IO this can lead to considerable speed ups.
Running tests in a Python subprocess
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
To instantiate a python2.5 sub process and send tests to it, you may type::
py.test -d --tx popen//python=python2.5
This will start a subprocess which is run with the "python2.5"
Python interpreter, found in your system binary lookup path.
If you prefix the --tx option value like this::
--tx 3*popen//python=python2.5
then three subprocesses would be created and tests
will be load-balanced across these three processes.
.. _boxed:
Running tests in a boxed subprocess
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
If you have tests involving C or C++ libraries you might have to deal
with tests crashing the process. For this case you may use the boxing
options::
py.test --boxed
which will run each test in a subprocess and will report if a test
crashed the process. You can also combine this option with
running multiple processes to speed up the test run and use your CPU cores::
py.test -n3 --boxed
this would run 3 testing subprocesses in parallel which each
create new boxed subprocesses for each test.
.. _`remote machines`:
Sending tests to remote SSH accounts
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Suppose you have a package ``mypkg`` which contains some
tests that you can successfully run locally. And you
have a ssh-reachable machine ``myhost``. Then
you can ad-hoc distribute your tests by typing::
py.test -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg
This will synchronize your ``mypkg`` package directory
to an remote ssh account and then locally collect tests
and send them to remote places for execution.
You can specify multiple ``--rsyncdir`` directories
to be sent to the remote side.
**NOTE:** For py.test to collect and send tests correctly
you not only need to make sure all code and tests
directories are rsynced, but that any test (sub) directory
also has an ``__init__.py`` file because internally
py.test references tests as a fully qualified python
module path. **You will otherwise get strange errors**
during setup of the remote side.
You can specify multiple ``--rsyncignore`` glob-patterns
to be ignored when file are sent to the remote side.
There are also internal ignores: .*, *.pyc, *.pyo, *~
Those you cannot override using rsyncignore command-line or
ini-file option(s).
Sending tests to remote Socket Servers
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Download the single-module `socketserver.py`_ Python program
and run it like this::
python socketserver.py
It will tell you that it starts listening on the default
port. You can now on your home machine specify this
new socket host with something like this::
py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg
.. _`atonce`:
.. _`Multi-Platform`:
Running tests on many platforms at once
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
The basic command to run tests on multiple platforms is::
py.test --dist=each --tx=spec1 --tx=spec2
If you specify a windows host, an OSX host and a Linux
environment this command will send each tests to all
platforms - and report back failures from all platforms
at once. The specifications strings use the `xspec syntax`_.
.. _`xspec syntax`: http://codespeak.net/execnet/trunk/basics.html#xspec
.. _`socketserver.py`: http://bitbucket.org/hpk42/execnet/raw/2af991418160/execnet/script/socketserver.py
.. _`execnet`: http://codespeak.net/execnet
Specifying test exec environments in an ini file
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
pytest (since version 2.0) supports ini-style cofiguration.
You can for example make running with three subprocesses
your default like this::
[pytest]
addopts = -n3
You can also add default environments like this::
[pytest]
addopts = --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python2.6
and then just type::
py.test --dist=each
to run tests in each of the environments.
Specifying "rsync" dirs in an ini-file
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
In a ``tox.ini`` or ``setup.cfg`` file in your root project directory
you may specify directories to include or to exclude in synchronisation::
[pytest]
rsyncdirs = . mypkg helperpkg
rsyncignore = .hg
These directory specifications are relative to the directory
where the configuration file was found.
.. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist
.. _`pytest-xdist repository`: http://bitbucket.org/hpk42/pytest-xdist
.. _`pytest`: http://pytest.org
Issue and Bug Tracker
------------------------
Please use the pytest issue tracker for bugs in this plugin, see https://bitbucket.org/hpk42/pytest/issues .
for downloading and installing it in one go.
holger krekel <holger at merlinux eu>

62
example/boxed.txt Normal file
View File

@@ -0,0 +1,62 @@
If your testing involves C or C++ libraries you might have to deal
with crashing processes. The xdist-plugin provides the ``--boxed`` option
to run each test in a controled subprocess. Here is a basic example::
# content of test_module.py
import pytest
import os
import time
# run test function 50 times with different argument
@pytest.mark.parametrize("arg", range(50))
def test_func(arg):
time.sleep(0.05) # each tests takes a while
if arg % 19 == 0:
os.kill(os.getpid(), 15)
If you run this with::
$ py.test --boxed
=========================== test session starts ============================
platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8
plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov
collecting ... collected 50 items
test_module.py f..................f..................f...........
================================= FAILURES =================================
_______________________________ test_func[0] _______________________________
/home/hpk/tmp/doc-exec-420/test_module.py:6: running the test CRASHED with signal 15
______________________________ test_func[19] _______________________________
/home/hpk/tmp/doc-exec-420/test_module.py:6: running the test CRASHED with signal 15
______________________________ test_func[38] _______________________________
/home/hpk/tmp/doc-exec-420/test_module.py:6: running the test CRASHED with signal 15
=================== 3 failed, 47 passed in 3.41 seconds ====================
You'll see that a couple of tests are reported as crashing, indicated
by lower-case ``f`` and the respective failure summary. You can also use
the xdist-provided parallelization feature to speed up your testing::
$ py.test --boxed -n3
=========================== test session starts ============================
platform linux2 -- Python 2.7.3 -- pytest-2.3.0.dev8
plugins: xdist, bugzilla, cache, oejskit, cli, pep8, cov
gw0 I / gw1 I / gw2 I
gw0 [50] / gw1 [50] / gw2 [50]
scheduling tests via LoadScheduling
..f...............f..................f............
================================= FAILURES =================================
_______________________________ test_func[0] _______________________________
[gw0] linux2 -- Python 2.7.3 /home/hpk/venv/1/bin/python
/home/hpk/tmp/doc-exec-420/test_module.py:6: running the test CRASHED with signal 15
______________________________ test_func[19] _______________________________
[gw2] linux2 -- Python 2.7.3 /home/hpk/venv/1/bin/python
/home/hpk/tmp/doc-exec-420/test_module.py:6: running the test CRASHED with signal 15
______________________________ test_func[38] _______________________________
[gw2] linux2 -- Python 2.7.3 /home/hpk/venv/1/bin/python
/home/hpk/tmp/doc-exec-420/test_module.py:6: running the test CRASHED with signal 15
=================== 3 failed, 47 passed in 2.03 seconds ====================

View File

@@ -1,32 +1,23 @@
"""
py.test 'xdist' plugin for distributed testing and loop-on-failing modes.
See http://pytest.org/plugin/xdist.html for documentation and, after
installation of the ``pytest-xdist`` PyPI package, ``py.test -h``
for the new options.
"""
from setuptools import setup
from xdist import __version__
setup(
name="pytest-xdist",
version=__version__,
version='1.10',
description='py.test xdist plugin for distributed testing and loop-on-failing modes',
long_description=__doc__,
license='GPLv2 or later',
long_description=open('README.txt').read(),
license='MIT',
author='holger krekel and contributors',
author_email='py-dev@codespeak.net,holger@merlinux.eu',
author_email='pytest-dev@python.org,holger@merlinux.eu',
url='http://bitbucket.org/hpk42/pytest-xdist',
platforms=['linux', 'osx', 'win32'],
packages = ['xdist'],
entry_points = {'pytest11': ['xdist = xdist.plugin'],},
zip_safe=False,
install_requires = ['execnet>=1.0.6', 'py>=1.3.0'],
install_requires = ['execnet>=1.1', 'pytest>=2.4.2'],
classifiers=[
'Development Status :: 4 - Beta',
'Development Status :: 5 - Production/Stable',
'Intended Audience :: Developers',
'License :: OSI Approved :: GNU General Public License (GPL)',
'License :: OSI Approved :: MIT License',
'Operating System :: POSIX',
'Operating System :: Microsoft :: Windows',
'Operating System :: MacOS :: MacOS X',

View File

@@ -1 +0,0 @@
#

View File

@@ -1,6 +1,77 @@
import py
import sys
class TestDistribution:
def test_n1_pass(self, testdir):
p1 = testdir.makepyfile("""
def test_ok():
pass
""")
result = testdir.runpytest(p1, "-n1")
assert result.ret == 0
result.stdout.fnmatch_lines([
"*1 passed*",
])
def test_n1_fail(self, testdir):
p1 = testdir.makepyfile("""
def test_fail():
assert 0
""")
result = testdir.runpytest(p1, "-n1")
assert result.ret == 1
result.stdout.fnmatch_lines([
"*1 failed*",
])
def test_n1_import_error(self, testdir):
p1 = testdir.makepyfile("""
import __import_of_missing_module
def test_import():
pass
""")
result = testdir.runpytest(p1, "-n1")
assert result.ret == 1
result.stdout.fnmatch_lines([
"E ImportError: *__import_of_missing_module*",
])
def test_n2_import_error(self, testdir):
"""Check that we don't report the same import error multiple times
in distributed mode."""
p1 = testdir.makepyfile("""
import __import_of_missing_module
def test_import():
pass
""")
result1 = testdir.runpytest(p1, "-n2")
result2 = testdir.runpytest(p1, "-n1")
assert len(result1.stdout.lines) == len(result2.stdout.lines)
def test_n1_skip(self, testdir):
p1 = testdir.makepyfile("""
def test_skip():
import py
py.test.skip("myreason")
""")
result = testdir.runpytest(p1, "-n1")
assert result.ret == 0
result.stdout.fnmatch_lines([
"*1 skipped*",
])
def test_manytests_to_one_import_error(self, testdir):
p1 = testdir.makepyfile("""
import __import_of_missing_module
def test_import():
pass
""")
result = testdir.runpytest(p1, '--tx=popen', '--tx=popen')
assert result.ret == 1
result.stdout.fnmatch_lines([
"E ImportError: *__import_of_missing_module*",
])
def test_manytests_to_one_popen(self, testdir):
p1 = testdir.makepyfile("""
import py
@@ -12,17 +83,42 @@ class TestDistribution:
pass
def test_skip():
py.test.skip("hello")
""",
""",
)
result = testdir.runpytest(p1, '-d', '--tx=popen', '--tx=popen')
result = testdir.runpytest(p1, "-v", '-d', '--tx=popen', '--tx=popen')
result.stdout.fnmatch_lines([
"*0*popen*Python*",
"*1*popen*Python*",
"*1*Python*",
"*2 failed, 1 passed, 1 skipped*",
])
assert result.ret == 1
def test_dist_conftest_specified(self, testdir):
def test_n1_fail_minus_x(self, testdir):
p1 = testdir.makepyfile("""
def test_fail1():
assert 0
def test_fail2():
assert 0
""")
result = testdir.runpytest(p1, "-x", "-v", "-n1")
assert result.ret == 2
result.stdout.fnmatch_lines([
"*Interrupted: stopping*1*",
"*1 failed*",
])
def test_basetemp_in_subprocesses(self, testdir):
p1 = testdir.makepyfile("""
def test_send(tmpdir):
import py
assert tmpdir.relto(py.path.local(%r)), tmpdir
""" % str(testdir.tmpdir))
result = testdir.runpytest(p1, "-n1")
assert result.ret == 0
result.stdout.fnmatch_lines([
"*1 passed*",
])
def test_dist_ini_specified(self, testdir):
p1 = testdir.makepyfile("""
import py
def test_fail0():
@@ -33,24 +129,24 @@ class TestDistribution:
pass
def test_skip():
py.test.skip("hello")
""",
""",
)
testdir.makeconftest("""
option_tx = 'popen popen popen'.split()
testdir.makeini("""
[pytest]
addopts = --tx=3*popen
""")
result = testdir.runpytest(p1, '-d')
result = testdir.runpytest(p1, '-d', "-v")
result.stdout.fnmatch_lines([
"*0*popen*Python*",
"*1*popen*Python*",
"*2*popen*Python*",
"*2*Python*",
"*2 failed, 1 passed, 1 skipped*",
])
assert result.ret == 1
@py.test.mark.xfail("sys.platform.startswith('java')", run=False)
def test_dist_tests_with_crash(self, testdir):
if not hasattr(py.std.os, 'kill'):
py.test.skip("no os.kill")
p1 = testdir.makepyfile("""
import py
def test_fail0():
@@ -68,12 +164,11 @@ class TestDistribution:
os.kill(os.getpid(), 15)
"""
)
result = testdir.runpytest(p1, '-d', '--tx=3*popen')
result = testdir.runpytest(p1, "-v", '-d', '-n1')
result.stdout.fnmatch_lines([
"*popen*Python*",
"*popen*Python*",
"*popen*Python*",
"*node down*",
"*Python*",
"*PASS**test_ok*",
"*node*down*",
"*3 failed, 1 passed, 1 skipped*"
])
assert result.ret == 1
@@ -84,40 +179,19 @@ class TestDistribution:
subdir = source.mkdir("example_pkg")
subdir.ensure("__init__.py")
p = subdir.join("test_one.py")
p.write("def test_5(): assert not __file__.startswith(%r)" % str(p))
result = testdir.runpytest("-d", "--rsyncdir=%(subdir)s" % locals(),
p.write("def test_5():\n assert not __file__.startswith(%r)" % str(p))
result = testdir.runpytest("-v", "-d",
"--rsyncdir=%(subdir)s" % locals(),
"--tx=popen//chdir=%(dest)s" % locals(), p)
assert result.ret == 0
result.stdout.fnmatch_lines([
"*0* *popen*platform*",
"*0* *cwd*",
#"RSyncStart: [G1]",
#"RSyncFinished: [G1]",
"*1 passed*"
])
assert dest.join(subdir.basename).check(dir=1)
def test_dist_each(self, testdir):
interpreters = []
for name in ("python2.4", "python2.5"):
interp = py.path.local.sysfind(name)
if interp is None:
py.test.skip("%s not found" % name)
interpreters.append(interp)
testdir.makepyfile(__init__="", test_one="""
import sys
def test_hello():
print("%s...%s" % sys.version_info[:2])
assert 0
""")
args = ["--dist=each"]
args += ["--tx", "popen//python=%s" % interpreters[0]]
args += ["--tx", "popen//python=%s" % interpreters[1]]
result = testdir.runpytest(*args)
s = result.stdout.str()
assert "2.4" in s
assert "2.5" in s
def test_data_exchange(self, testdir):
c1 = testdir.makeconftest("""
# This hook only called on master.
@@ -125,8 +199,8 @@ class TestDistribution:
node.slaveinput['a'] = 42
node.slaveinput['b'] = 7
# This hook only takes action on slave.
def pytest_configure(config):
# this attribute is only set on slaves
if hasattr(config, 'slaveinput'):
a = config.slaveinput['a']
b = config.slaveinput['b']
@@ -137,17 +211,16 @@ class TestDistribution:
def pytest_testnodedown(node, error):
node.config.calc_result = node.slaveoutput['r']
# This hook only takes action on master.
def pytest_terminal_summary(terminalreporter):
if not hasattr(terminalreporter.config, 'slaveinput'):
calc_result = terminalreporter.config.calc_result
terminalreporter._tw.sep('-',
terminalreporter._tw.sep('-',
'calculated result is %s' % calc_result)
""")
p1 = testdir.makepyfile("def test_func(): pass")
result = testdir.runpytest(p1, '-d', '--tx=popen')
result = testdir.runpytest("-v", p1, '-d', '--tx=popen')
result.stdout.fnmatch_lines([
"*popen*Python*",
"*0*Python*",
"*calculated result is 49*",
"*1 passed*"
])
@@ -160,15 +233,229 @@ class TestDistribution:
""")
testdir.makeconftest("""
def pytest_sessionfinish(session):
# on the slave
if hasattr(session.config, 'slaveoutput'):
session.config.slaveoutput['s2'] = 42
# on the master
def pytest_testnodedown(node, error):
assert node.slaveoutput['s2'] == 42
print ("s2call-finished")
""")
args = ["-n1"]
args = ["-n1", "--debug"]
result = testdir.runpytest(*args)
s = result.stdout.str()
assert result.ret
assert 'SIGINT' in s
assert result.ret == 2
assert 's2call' in s
assert "Interrupted" in s
def test_keyboard_interrupt_dist(self, testdir):
# xxx could be refined to check for return code
p = testdir.makepyfile("""
def test_sleep():
import time
time.sleep(10)
""")
child = testdir.spawn_pytest("-n1")
py.std.time.sleep(0.1)
child.expect(".*test session starts.*")
child.kill(2) # keyboard interrupt
child.expect(".*KeyboardInterrupt.*")
#child.expect(".*seconds.*")
child.close()
#assert ret == 2
class TestDistEach:
def test_simple(self, testdir):
testdir.makepyfile("""
def test_hello():
pass
""")
result = testdir.runpytest("--debug", "--dist=each", "--tx=2*popen")
assert not result.ret
result.stdout.fnmatch_lines(["*2 pass*"])
@py.test.mark.xfail(run=False,
reason="other python versions might not have py.test installed")
def test_simple_diffoutput(self, testdir):
interpreters = []
for name in ("python2.5", "python2.6"):
interp = py.path.local.sysfind(name)
if interp is None:
py.test.skip("%s not found" % name)
interpreters.append(interp)
testdir.makepyfile(__init__="", test_one="""
import sys
def test_hello():
print("%s...%s" % sys.version_info[:2])
assert 0
""")
args = ["--dist=each", "-v"]
args += ["--tx", "popen//python=%s" % interpreters[0]]
args += ["--tx", "popen//python=%s" % interpreters[1]]
result = testdir.runpytest(*args)
s = result.stdout.str()
assert "2...5" in s
assert "2...6" in s
class TestTerminalReporting:
def test_pass_skip_fail(self, testdir):
p = testdir.makepyfile("""
import py
def test_ok():
pass
def test_skip():
py.test.skip("xx")
def test_func():
assert 0
""")
result = testdir.runpytest("-n1", "-v")
result.stdout.fnmatch_lines_random([
"*PASS*test_pass_skip_fail.py:2: *test_ok*",
"*SKIP*test_pass_skip_fail.py:4: *test_skip*",
"*FAIL*test_pass_skip_fail.py:6: *test_func*",
])
result.stdout.fnmatch_lines([
"*def test_func():",
"> assert 0",
"E assert 0",
])
def test_fail_platinfo(self, testdir):
p = testdir.makepyfile("""
def test_func():
assert 0
""")
result = testdir.runpytest("-n1", "-v")
result.stdout.fnmatch_lines([
"*FAIL*test_fail_platinfo.py:1: *test_func*",
"*0*Python*",
"*def test_func():",
"> assert 0",
"E assert 0",
])
def test_teardownfails_one_function(testdir):
p = testdir.makepyfile("""
def test_func():
pass
def teardown_function(function):
assert 0
""")
result = testdir.runpytest(p, '-n1', '--tx=popen')
result.stdout.fnmatch_lines([
"*def teardown_function(function):*",
"*1 passed*1 error*"
])
@py.test.mark.xfail
def test_terminate_on_hangingnode(testdir):
p = testdir.makeconftest("""
def pytest_sessionfinishes(session):
if session.nodeid == "my": # running on slave
import time
time.sleep(3)
""")
result = testdir.runpytest(p, '--dist=each', '--tx=popen//id=my')
assert result.duration < 2.0
result.stdout.fnmatch_lines([
"*killed*my*",
])
def test_session_hooks(testdir):
testdir.makeconftest("""
import sys
def pytest_sessionstart(session):
sys.pytestsessionhooks = session
def pytest_sessionfinish(session):
if hasattr(session.config, 'slaveinput'):
name = "slave"
else:
name = "master"
f = open(name, "w")
f.write("xy")
f.close()
# let's fail on the slave
if name == "slave":
raise ValueError(42)
""")
p = testdir.makepyfile("""
import sys
def test_hello():
assert hasattr(sys, 'pytestsessionhooks')
""")
result = testdir.runpytest(p, "--dist=each", "--tx=popen")
result.stdout.fnmatch_lines([
"*ValueError*",
"*1 passed*",
])
assert not result.ret
d = result.parseoutcomes()
assert d['passed'] == 1
assert testdir.tmpdir.join("slave").check()
assert testdir.tmpdir.join("master").check()
def test_funcarg_teardown_failure(testdir):
p = testdir.makepyfile("""
def pytest_funcarg__myarg(request):
def teardown(val):
raise ValueError(val)
return request.cached_setup(setup=lambda: 42, teardown=teardown,
scope="module")
def test_hello(myarg):
pass
""")
result = testdir.runpytest("--debug", p) # , "-n1")
result.stdout.fnmatch_lines([
"*ValueError*42*",
"*1 passed*1 error*",
])
assert result.ret
def test_crashing_item(testdir):
p = testdir.makepyfile("""
import py
import os
def test_crash():
py.process.kill(os.getpid())
def test_noncrash():
pass
""")
result = testdir.runpytest("-n2", p)
result.stdout.fnmatch_lines([
"*crashed*test_crash*",
"*1 failed*1 passed*"
])
def test_skipping(testdir):
p = testdir.makepyfile("""
import pytest
def test_crash():
pytest.skip("hello")
""")
result = testdir.runpytest("-n1", '-rs', p)
assert result.ret == 0
result.stdout.fnmatch_lines([
"*hello*",
"*1 skipped*"
])
def test_issue34_pluginloading_in_subprocess(testdir):
testdir.tmpdir.join("plugin123.py").write(py.code.Source("""
def pytest_namespace():
return {'sample_variable': 'testing'}
"""))
testdir.makepyfile("""
import pytest
def test_hello():
assert pytest.sample_variable == "testing"
""")
result = testdir.runpytest("-n1", "-p", "plugin123")
assert result.ret == 0
result.stdout.fnmatch_lines([
"*1 passed*",
])

View File

@@ -2,12 +2,11 @@ import py
import execnet
pytest_plugins = "pytester"
option_report = 'skipped'
#rsyncdirs = ['.', '../xdist', py.path.local(execnet.__file__).dirpath()]
def pytest_addoption(parser):
parser.addoption('--gx',
parser.addoption('--gx',
action="append", dest="gspecs", default=None,
help=("add a global test environment, XSpec-syntax. "))
@@ -17,9 +16,9 @@ def getgspecs(config):
return [execnet.XSpec(spec)
for spec in config.getvalueorskip("gspecs")]
# configuration information for tests
# configuration information for tests
def getgspecs(config):
return [execnet.XSpec(spec)
return [execnet.XSpec(spec)
for spec in config.getvalueorskip("gspecs")]
def getspecssh(config):
@@ -28,7 +27,7 @@ def getspecssh(config):
if spec.ssh:
if not py.path.local.sysfind("ssh"):
py.test.skip("command not found: ssh")
return spec
return str(spec)
py.test.skip("need '--gx ssh=...'")
def getsocketspec(config):

View File

@@ -16,15 +16,13 @@ def test_functional_boxed(testdir):
class TestOptionEffects:
def test_boxed_option_default(self, testdir):
tmpdir = testdir.tmpdir.ensure("subdir", dir=1)
config = testdir.reparseconfig()
config.initsession()
config = testdir.parseconfig()
assert not config.option.boxed
py.test.importorskip("execnet")
config = testdir.reparseconfig(['-d', tmpdir])
config.initsession()
config = testdir.parseconfig('-d', tmpdir)
assert not config.option.boxed
def test_is_not_boxed_by_default(self, testdir):
config = testdir.reparseconfig([testdir.tmpdir])
config = testdir.parseconfig(testdir.tmpdir)
assert not config.option.boxed

View File

@@ -1,37 +0,0 @@
import py
def test_dist_conftest_options(testdir):
p1 = testdir.tmpdir.ensure("dir", 'p1.py')
p1.dirpath("__init__.py").write("")
p1.dirpath("conftest.py").write(py.code.Source("""
import py
from py.builtin import print_
print_("importing conftest", __file__)
Option = py.test.config.Option
option = py.test.config.addoptions("someopt",
Option('--someopt', action="store_true",
dest="someopt", default=False))
dist_rsync_roots = ['../dir']
print_("added options", option)
print_("config file seen from conftest", py.test.config)
"""))
p1.write(py.code.Source("""
import py
from %s import conftest
from py.builtin import print_
def test_1():
print_("config from test_1", py.test.config)
print_("conftest from test_1", conftest.__file__)
print_("test_1: py.test.config.option.someopt", py.test.config.option.someopt)
print_("test_1: conftest", conftest)
print_("test_1: conftest.option.someopt", conftest.option.someopt)
assert conftest.option.someopt
""" % p1.dirpath().purebasename ))
result = testdir.runpytest('-d', '--tx=popen', p1, '--someopt')
assert result.ret == 0
result.stderr.fnmatch_lines([
"*Deprecation*pytest_addoptions*",
])
result.stdout.fnmatch_lines([
"*1 passed*",
])

View File

@@ -1,26 +1,39 @@
from xdist.dsession import DSession
from py._test import session as outcome
from xdist.dsession import (
DSession,
LoadScheduling,
EachScheduling,
report_collection_diff,
)
from _pytest import main as outcome
import py
import pytest
import execnet
XSpec = execnet.XSpec
def run(item, node, excinfo=None):
runner = item.config.pluginmanager.getplugin("runner")
rep = runner.ItemTestReport(item=item,
rep = runner.ItemTestReport(item=item,
excinfo=excinfo, when="call")
rep.node = node
return rep
return rep
class MockGateway:
_count = 0
def __init__(self):
self.id = str(self._count)
self._count += 1
class MockNode:
def __init__(self):
self.sent = []
self.gateway = MockGateway()
def send(self, item):
self.sent.append(item)
def send_runtest_some(self, indices):
self.sent.extend(indices)
def sendlist(self, items):
self.sent.extend(items)
def send_runtest_all(self):
self.sent.append("ALL")
def shutdown(self):
self._shutdown=True
@@ -29,373 +42,115 @@ def dumpqueue(queue):
while queue.qsize():
print(queue.get())
class TestDSession:
def test_add_remove_node(self, testdir):
item = testdir.getitem("def test_func(): pass")
node = MockNode()
rep = run(item, node)
session = DSession(item.config)
assert not session.node2pending
session.addnode(node)
assert len(session.node2pending) == 1
session.senditems_load([item])
pending = session.removenode(node)
assert pending == [item]
assert item not in session.item2nodes
l = session.removenode(node)
assert not l
def test_senditems_each_and_receive_with_two_nodes(self, testdir):
item = testdir.getitem("def test_func(): pass")
class TestEachScheduling:
def test_schedule_load_simple(self):
node1 = MockNode()
node2 = MockNode()
session = DSession(item.config)
session.addnode(node1)
session.addnode(node2)
session.senditems_each([item])
assert session.node2pending[node1] == [item]
assert session.node2pending[node2] == [item]
assert node1 in session.item2nodes[item]
assert node2 in session.item2nodes[item]
session.removeitem(item, node1)
assert session.item2nodes[item] == [node2]
session.removeitem(item, node2)
assert not session.node2pending[node1]
assert not session.item2nodes
sched = EachScheduling(2)
sched.addnode(node1)
sched.addnode(node2)
collection = ["a.py::test_1", ]
assert not sched.collection_is_completed
sched.addnode_collection(node1, collection)
assert not sched.collection_is_completed
sched.addnode_collection(node2, collection)
assert sched.collection_is_completed
assert sched.node2collection[node1] == collection
assert sched.node2collection[node2] == collection
sched.init_distribute()
assert sched.tests_finished()
assert node1.sent == ['ALL']
assert node2.sent == ['ALL']
sched.remove_item(node1, 0)
assert sched.tests_finished()
sched.remove_item(node2, 0)
assert sched.tests_finished()
def test_senditems_load_and_receive_one_node(self, testdir):
item = testdir.getitem("def test_func(): pass")
node = MockNode()
rep = run(item, node)
session = DSession(item.config)
session.addnode(node)
session.senditems_load([item])
assert session.node2pending[node] == [item]
assert session.item2nodes[item] == [node]
session.removeitem(item, node)
assert not session.node2pending[node]
assert not session.item2nodes
def test_schedule_remove_node(self):
node1 = MockNode()
sched = EachScheduling(1)
sched.addnode(node1)
collection = ["a.py::test_1", ]
assert not sched.collection_is_completed
sched.addnode_collection(node1, collection)
assert sched.collection_is_completed
assert sched.node2collection[node1] == collection
sched.init_distribute()
assert sched.tests_finished()
crashitem = sched.remove_node(node1)
assert crashitem
assert sched.tests_finished()
assert not sched.hasnodes()
def test_triggertesting_collect(self, testdir):
modcol = testdir.getmodulecol("""
def test_func():
pass
""")
session = DSession(modcol.config)
reprec = testdir.getreportrecorder(session)
items = session.collect_all_items([modcol])
assert len(items) == 1
calls= reprec.getcalls("pytest_collectreport")
assert len(calls) == 1
call = calls[0]
assert len(call.report.result) == 1
def test_senditems_load(self, testdir, monkeypatch):
item = testdir.getitem("def test_func(): pass")
session = DSession(item.config)
class TestLoadScheduling:
def test_schedule_load_simple(self):
node1 = MockNode()
node2 = MockNode()
session.addnode(node1)
session.addnode(node2)
monkeypatch.setattr(session, 'ITEM_CHUNKSIZE', 3)
session.senditems_load([item] * (2*session.ITEM_CHUNKSIZE +1))
sched = LoadScheduling(2)
sched.addnode(node1)
sched.addnode(node2)
collection = ["a.py::test_1", "a.py::test_2"]
assert not sched.collection_is_completed
sched.addnode_collection(node1, collection)
assert not sched.collection_is_completed
sched.addnode_collection(node2, collection)
assert sched.collection_is_completed
assert sched.node2collection[node1] == collection
assert sched.node2collection[node2] == collection
sched.init_distribute()
assert sched.tests_finished()
assert len(node1.sent) == 1
assert len(node2.sent) == 1
x = sorted(node1.sent + node2.sent)
assert x == [0, 1]
sched.remove_item(node1, node1.sent[0])
sched.remove_item(node2, node2.sent[0])
assert sched.tests_finished()
assert not sched.pending
def test_init_distribute_chunksize(self):
sched = LoadScheduling(2)
node1 = MockNode()
node2 = MockNode()
sched.addnode(node1)
sched.addnode(node2)
col = ["xyz"] * (3)
sched.addnode_collection(node1, col)
sched.addnode_collection(node2, col)
sched.init_distribute()
#assert not sched.tests_finished()
sent1 = node1.sent
sent2 = node2.sent
chunkitems = [item] * session.ITEM_CHUNKSIZE
assert sent1 == chunkitems
assert sent2 == chunkitems
assert session.node2pending[node1] == sent1
assert session.node2pending[node2] == sent2
name, args, kwargs = session.queue.get(block=False)
assert name == "pytest_rescheduleitems"
assert kwargs['items'] == [item]
chunkitems = col[:1]
assert (sent1 == [0] and sent2 == [1]) or (
sent1 == [1] and sent2 == [0])
assert sched.node2pending[node1] == sent1
assert sched.node2pending[node2] == sent2
assert len(sched.pending) == 1
for node in (node1, node2):
for i in sched.node2pending[node]:
sched.remove_item(node, i)
assert not sched.pending
def test_keyboardinterrupt(self, testdir):
item = testdir.getitem("def test_func(): pass")
session = DSession(item.config)
def raise_(timeout=None): raise KeyboardInterrupt()
session.queue.get = raise_
exitstatus = session.loop([])
assert exitstatus == outcome.EXIT_INTERRUPTED
def test_internalerror(self, testdir):
item = testdir.getitem("def test_func(): pass")
session = DSession(item.config)
def raise_(): raise ValueError()
session.queue.get = raise_
exitstatus = session.loop([])
assert exitstatus == outcome.EXIT_INTERNALERROR
def test_rescheduleevent(self, testdir):
item = testdir.getitem("def test_func(): pass")
session = DSession(item.config)
def test_add_remove_node(self):
node = MockNode()
session.addnode(node)
loopstate = session._initloopstate([])
session.queueevent("pytest_rescheduleitems", items=[item])
session.loop_once(loopstate)
# we need to do work because nothing is pending / we would not wake up
assert loopstate.dowork == True
sched = LoadScheduling(1)
sched.addnode(node)
collection = ["test_file.py::test_func"]
sched.addnode_collection(node, collection)
assert sched.collection_is_completed
sched.init_distribute()
assert not sched.pending
crashitem = sched.remove_node(node)
assert crashitem == collection[0]
session.node2pending[node].append(item)
session.queueevent("pytest_rescheduleitems", items=[item])
session.loop_once(loopstate)
# now we want to not directly trigger work again to avoid busy-wait
assert loopstate.dowork == False
session.queueevent(None)
session.loop_once(loopstate)
session.queueevent(None)
session.loop_once(loopstate)
assert node.sent == [item, item]
session.queueevent("pytest_runtest_logreport", report=run(item, node))
session.loop_once(loopstate)
session.queueevent("pytest_runtest_logreport", report=run(item, node))
session.loop_once(loopstate)
assert loopstate.shuttingdown
assert not loopstate.testsfailed
def test_no_node_remaining_for_tests(self, testdir):
item = testdir.getitem("def test_func(): pass")
# setup a session with one node
session = DSession(item.config)
node = MockNode()
session.addnode(node)
# setup a HostDown event
session.queueevent("pytest_testnodedown", node=node, error=None)
loopstate = session._initloopstate([item])
loopstate.dowork = False
session.loop_once(loopstate)
dumpqueue(session.queue)
assert loopstate.exitstatus == outcome.EXIT_NOHOSTS
def test_removeitem_from_failing_teardown(self, testdir):
# teardown reports only come in when they signal a failure
# internal session-management should basically ignore them
# XXX probably it'S best to invent a new error hook for
# teardown/setup related failures
modcol = testdir.getmodulecol("""
def test_one():
pass
def teardown_function(function):
assert 0
""")
item1, = modcol.collect()
# setup a session with two nodes
session = DSession(item1.config)
node1, node2 = MockNode(), MockNode()
session.addnode(node1)
session.addnode(node2)
# have one test pending for a node that goes down
session.senditems_each([item1])
nodes = session.item2nodes[item1]
class rep:
failed = True
item = item1
node = nodes[0]
when = "call"
session.queueevent("pytest_runtest_logreport", report=rep)
reprec = testdir.getreportrecorder(session)
print(session.item2nodes)
loopstate = session._initloopstate([])
assert len(session.item2nodes[item1]) == 2
session.loop_once(loopstate)
assert len(session.item2nodes[item1]) == 1
rep.when = "teardown"
session.queueevent("pytest_runtest_logreport", report=rep)
session.loop_once(loopstate)
assert len(session.item2nodes[item1]) == 1
def test_testnodeready_adds_to_available(self, testdir):
item = testdir.getitem("def test_func(): pass")
# setup a session with two nodes
session = DSession(item.config)
node1 = MockNode()
session.queueevent("pytest_testnodeready", node=node1)
loopstate = session._initloopstate([item])
loopstate.dowork = False
assert len(session.node2pending) == 0
session.loop_once(loopstate)
assert len(session.node2pending) == 1
def runthrough(self, item, excinfo=None):
session = DSession(item.config)
node = MockNode()
session.addnode(node)
loopstate = session._initloopstate([item])
session.queueevent(None)
session.loop_once(loopstate)
assert node.sent == [item]
ev = run(item, node, excinfo=excinfo)
session.queueevent("pytest_runtest_logreport", report=ev)
session.loop_once(loopstate)
assert loopstate.shuttingdown
session.queueevent("pytest_testnodedown", node=node, error=None)
session.loop_once(loopstate)
dumpqueue(session.queue)
return session, loopstate.exitstatus
def test_exit_completed_tests_ok(self, testdir):
item = testdir.getitem("def test_func(): pass")
session, exitstatus = self.runthrough(item)
assert exitstatus == outcome.EXIT_OK
def test_exit_completed_tests_fail(self, testdir):
item = testdir.getitem("def test_func(): 0/0")
session, exitstatus = self.runthrough(item, excinfo="fail")
assert exitstatus == outcome.EXIT_TESTSFAILED
def test_exit_on_first_failing(self, testdir):
modcol = testdir.getmodulecol("""
def test_fail():
assert 0
def test_pass():
pass
""")
modcol.config.option.exitfirst = True
session = DSession(modcol.config)
node = MockNode()
session.addnode(node)
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, "fail")
ev2 = run(items[1], node, None)
session.queueevent("pytest_runtest_logreport", report=ev1) # a failing one
session.queueevent("pytest_runtest_logreport", report=ev2)
# now call the loop
loopstate = session._initloopstate(items)
from xdist.dsession import ExitFirstInterrupt
py.test.raises(ExitFirstInterrupt, "session.loop_once(loopstate)")
assert loopstate.testsfailed
#assert loopstate.shuttingdown
def test_shuttingdown_filters(self, testdir):
item = testdir.getitem("def test_func(): pass")
session = DSession(item.config)
node = MockNode()
session.addnode(node)
loopstate = session._initloopstate([])
loopstate.shuttingdown = True
reprec = testdir.getreportrecorder(session)
session.queueevent("pytest_runtest_logreport", report=run(item, node))
session.loop_once(loopstate)
assert not reprec.getcalls("pytest_testnodedown")
session.queueevent("pytest_testnodedown", node=node, error=None)
session.loop_once(loopstate)
assert reprec.getcall('pytest_testnodedown').node == node
def test_filteritems(self, testdir):
modcol = testdir.getmodulecol("""
def test_fail():
assert 0
def test_pass():
pass
""")
session = DSession(modcol.config)
modcol.config.option.keyword = "nothing"
dsel = session.filteritems([modcol])
assert dsel == [modcol]
items = modcol.collect()
hookrecorder = testdir.getreportrecorder(session).hookrecorder
remaining = session.filteritems(items)
assert remaining == []
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 = hookrecorder.getcalls("pytest_deselected")[-1]
assert event.items == [items[1]]
def test_testnodedown_shutdown_after_completion(self, testdir):
item = testdir.getitem("def test_func(): pass")
session = DSession(item.config)
node = MockNode()
session.addnode(node)
session.senditems_load([item])
session.queueevent("pytest_runtest_logreport", report=run(item, node))
loopstate = session._initloopstate([])
session.loop_once(loopstate)
assert node._shutdown is True
assert loopstate.exitstatus is None, "loop did not wait for testnodedown"
assert loopstate.shuttingdown
session.queueevent("pytest_testnodedown", node=node, error=None)
session.loop_once(loopstate)
assert loopstate.exitstatus == 0
def test_nopending_but_collection_remains(self, testdir):
modcol = testdir.getmodulecol("""
def test_fail():
assert 0
def test_pass():
pass
""")
session = DSession(modcol.config)
node = MockNode()
session.addnode(node)
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_runtest_logreport", report=run(item1, node))
# but we have a collection pending
session.queueevent("pytest_collectreport", report=colreport)
loopstate = session._initloopstate([])
session.loop_once(loopstate)
assert loopstate.exitstatus is None, "loop did not care for collection report"
assert not loopstate.colitems
session.loop_once(loopstate)
assert loopstate.colitems == colreport.result
assert loopstate.exitstatus is None, "loop did not care for colitems"
def test_dist_some_tests(self, testdir):
p1 = testdir.makepyfile(test_one="""
def test_1():
pass
def test_x():
import py
py.test.skip("aaa")
def test_fail():
assert 0
""")
config = testdir.parseconfig('-d', p1, '--tx=popen')
dsession = DSession(config)
hookrecorder = testdir.getreportrecorder(config).hookrecorder
dsession.main([config.getnode(p1)])
rep = hookrecorder.popcall("pytest_runtest_logreport").report
assert rep.passed
rep = hookrecorder.popcall("pytest_runtest_logreport").report
assert rep.skipped
rep = hookrecorder.popcall("pytest_runtest_logreport").report
assert rep.failed
# see that the node is really down
node = hookrecorder.popcall("pytest_testnodedown").node
assert node.gateway.spec.popen
#XXX eq.geteventargs("pytest_sessionfinish")
class TestDistReporter:
@py.test.mark.xfail
def test_rsync_printing(self, testdir, linecomp):
config = testdir.parseconfig()
from py._plugin.pytest_terminal import TerminalReporter
from _pytest.pytest_terminal import TerminalReporter
rep = TerminalReporter(config, file=linecomp.stringio)
config.pluginmanager.register(rep, "terminalreporter")
dsession = DSession(config)
@@ -410,124 +165,57 @@ class TestDSession:
# executable = "hello"
# platform = "xyz"
# cwd = "qwe"
#dsession.pytest_gwmanage_newgateway(gw1, rinfo)
#dsession.pytest_xdist_newgateway(gw1, rinfo)
#linecomp.assert_contains_lines([
# "*X1*popen*xyz*2.5*"
#])
dsession.pytest_gwmanage_rsyncstart(source="hello", gateways=[gw1, gw2])
dsession.pytest_xdist_rsyncstart(source="hello", gateways=[gw1, gw2])
linecomp.assert_contains_lines([
"[X1,X2] rsyncing: hello",
])
def test_collected_function_causes_remote_skip(testdir):
sub = testdir.mkpydir("testing")
sub.join("test_module.py").write(py.code.Source("""
import py
path = py.path.local(%r)
if path.check():
path.remove()
else:
py.test.skip("remote skip")
def test_func():
pass
def test_func2():
pass
""" % str(sub.ensure("somefile"))))
result = testdir.runpytest('-v', '--dist=each', '--tx=popen')
result.stdout.fnmatch_lines([
"*2 skipped*"
])
def test_teardownfails_one_function(testdir):
p = testdir.makepyfile("""
def test_func():
pass
def teardown_function(function):
assert 0
""")
result = testdir.runpytest(p, '--dist=each', '--tx=popen')
result.stdout.fnmatch_lines([
"*def teardown_function(function):*",
"*1 passed*1 error*"
])
@py.test.mark.xfail
def test_terminate_on_hangingnode(testdir):
p = testdir.makeconftest("""
def pytest__teardown_final(session):
if session.nodeid == "my": # running on slave
import time
time.sleep(3)
""")
result = testdir.runpytest(p, '--dist=each', '--tx=popen//id=my')
assert result.duration < 2.0
result.stdout.fnmatch_lines([
"*killed*my*",
])
def test_report_collection_diff_equal():
"""Test reporting of equal collections."""
from_collection = to_collection = ['aaa', 'bbb', 'ccc']
assert report_collection_diff(from_collection, to_collection, 1, 2)
def test_report_collection_diff_different():
"""Test reporting of different collections."""
from_collection = ['aaa', 'bbb', 'ccc', 'YYY']
to_collection = ['aZa', 'bbb', 'XXX', 'ccc']
error_message = (
'Different tests were collected between 1 and 2. The difference is:\n'
'--- 1\n'
'\n'
'+++ 2\n'
'\n'
'@@ -1,4 +1,4 @@\n'
'\n'
'-aaa\n'
'+aZa\n'
' bbb\n'
'+XXX\n'
' ccc\n'
'-YYY'
)
def test_session_hooks(testdir):
testdir.makeconftest("""
import sys
def pytest_sessionstart(session):
sys.pytestsessionhooks = session
def pytest_sessionfinish(session):
f = open(session.nodeid or "master", 'w')
f.write("xy")
f.close()
# let's fail on the slave
if session.nodeid:
raise ValueError(42)
""")
p = testdir.makepyfile("""
import sys
def test_hello():
assert hasattr(sys, 'pytestsessionhooks')
""")
result = testdir.runpytest(p, "--dist=each", "--tx=popen//id=my1")
result.stdout.fnmatch_lines([
"*ValueError*",
"*1 passed*",
])
assert result.ret
d = result.parseoutcomes()
assert d['passed'] == 1
assert testdir.tmpdir.join("my1").check()
assert testdir.tmpdir.join("master").check()
try:
report_collection_diff(from_collection, to_collection, 1, 2)
except AssertionError as e:
assert py.builtin._totext(e) == error_message
def test_funcarg_teardown_failure(testdir):
p = testdir.makepyfile("""
def pytest_funcarg__myarg(request):
def teardown(val):
raise ValueError(val)
return request.cached_setup(setup=lambda: 42, teardown=teardown,
scope="module")
def test_hello(myarg):
@pytest.mark.xfail(reason="duplicate test ids not supported yet")
def test_pytest_issue419(testdir):
testdir.makepyfile("""
import pytest
@pytest.mark.parametrize('birth_year', [1988, 1988, ])
def test_2011_table(birth_year):
pass
""")
result = testdir.runpytest(p, "-n1")
assert result.ret
result.stdout.fnmatch_lines([
"*ValueError*42*",
"*1 passed*1 error*",
])
def test_crashing_item(testdir):
p = testdir.makepyfile("""
import py
import os
def test_crash():
py.process.kill(os.getpid())
def test_noncrash():
pass
""")
result = testdir.runpytest("-n2", p)
result.stdout.fnmatch_lines([
"*crashed*test_crash*",
"*1 failed*1 passed*"
])
reprec = testdir.inline_run("-n1")
reprec.assertoutcome(passed=2)
assert 0

View File

@@ -1,121 +0,0 @@
import py
import os
from xdist.gwmanage import GatewayManager, HostRSync
from py._test.pluginmanager import HookRelay, Registry
from py._plugin import hookspec
from xdist import newhooks
import execnet
def pytest_funcarg__hookrecorder(request):
_pytest = request.getfuncargvalue('_pytest')
hook = request.getfuncargvalue('hook')
return _pytest.gethookrecorder(hook)
def pytest_funcarg__hook(request):
return HookRelay([hookspec, newhooks], Registry())
class TestGatewayManagerPopen:
def test_popen_no_default_chdir(self, hook):
gm = GatewayManager(["popen"], hook)
assert gm.specs[0].chdir is None
def test_default_chdir(self, hook):
l = ["ssh=noco", "socket=xyz"]
for spec in GatewayManager(l, hook).specs:
assert spec.chdir == "pyexecnetcache"
for spec in GatewayManager(l, hook, defaultchdir="abc").specs:
assert spec.chdir == "abc"
def test_popen_makegateway_events(self, hook, hookrecorder, _pytest):
hm = GatewayManager(["popen"] * 2, hook)
hm.makegateways()
call = hookrecorder.popcall("pytest_gwmanage_newgateway")
assert call.gateway.spec == execnet.XSpec("popen")
assert call.gateway.id == "gw0"
assert call.platinfo.executable == call.gateway._rinfo().executable
call = hookrecorder.popcall("pytest_gwmanage_newgateway")
assert call.gateway.id == "gw1"
assert len(hm.group) == 2
hm.exit()
assert not len(hm.group)
def test_popens_rsync(self, hook, mysetup):
source = mysetup.source
hm = GatewayManager(["popen"] * 2, hook)
hm.makegateways()
assert len(hm.group) == 2
for gw in hm.group:
class pseudoexec:
args = []
def __init__(self, *args):
self.args.extend(args)
def waitclose(self):
pass
gw.remote_exec = pseudoexec
l = []
hm.rsync(source, notify=lambda *args: l.append(args))
assert not l
hm.exit()
assert not len(hm.group)
assert "sys.path.insert" in gw.remote_exec.args[0]
def test_rsync_popen_with_path(self, hook, mysetup):
source, dest = mysetup.source, mysetup.dest
hm = GatewayManager(["popen//chdir=%s" %dest] * 1, hook)
hm.makegateways()
source.ensure("dir1", "dir2", "hello")
l = []
hm.rsync(source, notify=lambda *args: l.append(args))
assert len(l) == 1
assert l[0] == ("rsyncrootready", hm.group['gw0'].spec, source)
hm.exit()
dest = dest.join(source.basename)
assert dest.join("dir1").check()
assert dest.join("dir1", "dir2").check()
assert dest.join("dir1", "dir2", 'hello').check()
def test_rsync_same_popen_twice(self, hook, mysetup, hookrecorder):
source, dest = mysetup.source, mysetup.dest
hm = GatewayManager(["popen//chdir=%s" %dest] * 2, hook)
hm.makegateways()
source.ensure("dir1", "dir2", "hello")
hm.rsync(source)
call = hookrecorder.popcall("pytest_gwmanage_rsyncstart")
assert call.source == source
assert len(call.gateways) == 1
assert call.gateways[0] in hm.group
call = hookrecorder.popcall("pytest_gwmanage_rsyncfinish")
class pytest_funcarg__mysetup:
def __init__(self, request):
tmp = request.getfuncargvalue('tmpdir')
self.source = tmp.mkdir("source")
self.dest = tmp.mkdir("dest")
class TestHRSync:
def test_hrsync_filter(self, mysetup):
source, dest = mysetup.source, mysetup.dest
source.ensure("dir", "file.txt")
source.ensure(".svn", "entries")
source.ensure(".somedotfile", "moreentries")
source.ensure("somedir", "editfile~")
syncer = HostRSync(source)
l = list(source.visit(rec=syncer.filter,
fil=syncer.filter))
assert len(l) == 3
basenames = [x.basename for x in l]
assert 'dir' in basenames
assert 'file.txt' in basenames
assert 'somedir' in basenames
def test_hrsync_one_host(self, mysetup):
source, dest = mysetup.source, mysetup.dest
gw = execnet.makegateway("popen//chdir=%s" % dest)
finished = []
rsync = HostRSync(source)
rsync.add_target_host(gw, finished=lambda: finished.append(1))
source.join("hello.py").write("world")
rsync.send()
gw.exit()
assert dest.join(source.basename, "hello.py").check()
assert len(finished) == 1

280
testing/test_looponfail.py Normal file
View File

@@ -0,0 +1,280 @@
import py
from xdist.looponfail import RemoteControl
from xdist.looponfail import StatRecorder
class TestStatRecorder:
def test_filechange(self, tmpdir):
tmp = tmpdir
hello = tmp.ensure("hello.py")
sd = StatRecorder([tmp])
changed = sd.check()
assert not changed
hello.write("world")
changed = sd.check()
assert changed
(hello + "c").write("hello")
changed = sd.check()
assert not changed
p = tmp.ensure("new.py")
changed = sd.check()
assert changed
p.remove()
changed = sd.check()
assert changed
tmp.join("a", "b", "c.py").ensure()
changed = sd.check()
assert changed
tmp.join("a", "c.txt").ensure()
changed = sd.check()
assert changed
changed = sd.check()
assert not changed
tmp.join("a").remove()
changed = sd.check()
assert changed
def test_dirchange(self, tmpdir):
tmp = tmpdir
hello = tmp.ensure("dir", "hello.py")
sd = StatRecorder([tmp])
assert not sd.fil(tmp.join("dir"))
def test_filechange_deletion_race(self, tmpdir, monkeypatch):
tmp = tmpdir
sd = StatRecorder([tmp])
changed = sd.check()
assert not changed
p = tmp.ensure("new.py")
changed = sd.check()
assert changed
p.remove()
# make check()'s visit() call return our just removed
# path as if we were in a race condition
monkeypatch.setattr(tmp, 'visit', lambda *args: [p])
changed = sd.check()
assert changed
def test_pycremoval(self, tmpdir):
tmp = tmpdir
hello = tmp.ensure("hello.py")
sd = StatRecorder([tmp])
changed = sd.check()
assert not changed
pycfile = hello + "c"
pycfile.ensure()
hello.write("world")
changed = sd.check()
assert changed
assert not pycfile.check()
def test_waitonchange(self, tmpdir, monkeypatch):
tmp = tmpdir
sd = StatRecorder([tmp])
l = [True, False]
monkeypatch.setattr(StatRecorder, 'check', lambda self: l.pop())
sd.waitonchange(checkinterval=0.2)
assert not l
class TestRemoteControl:
def test_nofailures(self, testdir):
item = testdir.getitem("def test_func(): pass\n")
control = RemoteControl(item.config)
control.setup()
topdir, failures = control.runsession()[:2]
assert not failures
def test_failures_somewhere(self, testdir):
item = testdir.getitem("def test_func():\n assert 0\n")
control = RemoteControl(item.config)
control.setup()
failures = control.runsession()
assert failures
control.setup()
item.fspath.write("def test_func():\n assert 1\n")
removepyc(item.fspath)
topdir, failures = control.runsession()[:2]
assert not failures
def test_failure_change(self, testdir):
modcol = testdir.getitem("""
def test_func():
assert 0
""")
control = RemoteControl(modcol.config)
control.loop_once()
assert control.failures
modcol.fspath.write(py.code.Source("""
def test_func():
assert 1
def test_new():
assert 0
"""))
removepyc(modcol.fspath)
control.loop_once()
assert not control.failures
control.loop_once()
assert control.failures
assert str(control.failures).find("test_new") != -1
def test_failure_subdir_no_init(self, testdir):
modcol = testdir.getitem("""
def test_func():
assert 0
""")
parent = modcol.fspath.dirpath().dirpath()
parent.chdir()
modcol.config.args = [py.path.local(x).relto(parent)
for x in modcol.config.args]
control = RemoteControl(modcol.config)
control.loop_once()
assert control.failures
control.loop_once()
assert control.failures
class TestLooponFailing:
def test_looponfail_from_fail_to_ok(self, testdir):
modcol = testdir.getmodulecol("""
def test_one():
x = 0
assert x == 1
def test_two():
assert 1
""")
remotecontrol = RemoteControl(modcol.config)
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1
modcol.fspath.write(py.code.Source("""
def test_one():
assert 1
def test_two():
assert 1
"""))
removepyc(modcol.fspath)
remotecontrol.loop_once()
assert not remotecontrol.failures
def test_looponfail_from_one_to_two_tests(self, testdir):
modcol = testdir.getmodulecol("""
def test_one():
assert 0
""")
remotecontrol = RemoteControl(modcol.config)
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1
assert 'test_one' in remotecontrol.failures[0]
modcol.fspath.write(py.code.Source("""
def test_one():
assert 1 # passes now
def test_two():
assert 0 # new and fails
"""))
removepyc(modcol.fspath)
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 0
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1
assert 'test_one' not in remotecontrol.failures[0]
assert 'test_two' in remotecontrol.failures[0]
def test_looponfail_removed_test(self, testdir):
modcol = testdir.getmodulecol("""
def test_one():
assert 0
def test_two():
assert 0
""")
remotecontrol = RemoteControl(modcol.config)
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 2
modcol.fspath.write(py.code.Source("""
def test_xxx(): # renamed test
assert 0
def test_two():
assert 1 # pass now
"""))
removepyc(modcol.fspath)
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 0
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1
def test_looponfail_multiple_errors(self, testdir, monkeypatch):
modcol = testdir.getmodulecol("""
def test_one():
assert 0
""")
remotecontrol = RemoteControl(modcol.config)
orig_runsession = remotecontrol.runsession
def runsession_dups():
# twisted.trial test cases may report multiple errors.
failures, reports, collection_failed = orig_runsession()
print (failures)
return failures * 2, reports, collection_failed
monkeypatch.setattr(remotecontrol, 'runsession', runsession_dups)
remotecontrol.loop_once()
assert len(remotecontrol.failures) == 1
class TestFunctional:
def test_fail_to_ok(self, testdir):
p = testdir.makepyfile("""
def test_one():
x = 0
assert x == 1
""")
#p = testdir.mkdir("sub").join(p1.basename)
#p1.move(p)
child = testdir.spawn_pytest("-f %s --traceconfig" % p)
child.expect("def test_one")
child.expect("x == 1")
child.expect("1 failed")
child.expect("### LOOPONFAILING ####")
child.expect("waiting for changes")
p.write(py.code.Source("""
def test_one():
x = 1
assert x == 1
"""))
child.expect(".*1 passed.*")
child.kill(15)
def test_xfail_passes(self, testdir):
p = testdir.makepyfile("""
import py
@py.test.mark.xfail
def test_one():
pass
""")
child = testdir.spawn_pytest("-f %s" % p)
child.expect("1 xpass")
child.expect("### LOOPONFAILING ####")
child.expect("waiting for changes")
child.kill(15)
def removepyc(path):
# XXX damn those pyc files
pyc = path + "c"
if pyc.check():
pyc.remove()
c = path.dirpath("__pycache__")
if c.check():
c.remove()

View File

@@ -1,254 +0,0 @@
import py
import sys
import execnet
Queue = py.builtin._tryimport('queue', 'Queue').Queue
from xdist.mypickle import ImmutablePickler, PickleChannel
from xdist.mypickle import UnpickleError, makekey
# first let's test some basic functionality
def pytest_generate_tests(metafunc):
if 'picklemod' in metafunc.funcargnames:
import pickle
metafunc.addcall(funcargs={'picklemod': pickle})
try:
import cPickle
except ImportError:
pass
else:
metafunc.addcall(funcargs={'picklemod': cPickle})
elif "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_underlying_basic_pickling_mechanisms(picklemod):
f1 = py.io.BytesIO()
f2 = py.io.BytesIO()
pickler1 = picklemod.Pickler(f1)
unpickler1 = picklemod.Unpickler(f2)
pickler2 = picklemod.Pickler(f2)
unpickler2 = picklemod.Unpickler(f1)
#pickler1.memo = unpickler1.memo = {}
#pickler2.memo = unpickler2.memo = {}
d = {}
pickler1.dump(d)
f1.seek(0)
d_other = unpickler2.load()
# translate unpickler2 memo to pickler2
pickler2.memo = dict([(id(obj), (int(x), obj))
for x, obj in unpickler2.memo.items()])
pickler2.dump(d_other)
f2.seek(0)
unpickler1.memo = dict([(makekey(x), y)
for x, y in pickler1.memo.values()])
d_back = unpickler1.load()
assert d is d_back
class A:
pass
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)
p2 = ImmutablePickler(uneven=True)
a1 = A()
a2 = A()
a3 = A()
a3.a1 = a1
a2.a1 = a1
s1 = p1.dumps(a1)
a1.a3 = a3
s2 = p1.dumps(a2)
other_a1 = p2.loads(s1)
other_a2 = p2.loads(s2)
back_a1 = p1.loads(p2.dumps(other_a1))
other_a3 = p2.loads(p1.dumps(a3))
back_a3 = p1.loads(p2.dumps(other_a3))
back_a2 = p1.loads(p2.dumps(other_a2))
back_a1 = p1.loads(p2.dumps(other_a1))
assert back_a1 is a1
assert back_a2 is a2
def test_pickling_concurrently():
p1 = ImmutablePickler(uneven=False)
p2 = ImmutablePickler(uneven=True)
a1 = A()
a1.hasattr = 42
a2 = A()
s1 = p1.dumps(a1)
s2 = p2.dumps(a2)
other_a1 = p2.loads(s1)
other_a2 = p1.loads(s2)
a1_back = p1.loads(p2.dumps(other_a1))
def test_self_memoize():
p1 = ImmutablePickler(uneven=False)
a1 = A()
p1.selfmemoize(a1)
x = p1.loads(p1.dumps(a1))
assert x is a1
TESTTIMEOUT = 2.0
class TestPickleChannelFunctional:
def setup_class(cls):
cls.gw = execnet.PopenGateway()
cls.gw.remote_exec(
"import py ; py.path.local(%r).pyimport()" %(__file__)
)
cls.gw.remote_init_threads(5)
# we need the remote test code to import
# the same test module here
def test_popen_send_instance(self):
channel = self.gw.remote_exec("""
from xdist.mypickle import PickleChannel
channel = PickleChannel(channel)
from testing.test_mypickle import A
a1 = A()
a1.hello = 10
channel.send(a1)
a2 = channel.receive()
channel.send(a2 is a1)
""")
channel = PickleChannel(channel)
a_received = channel.receive()
assert isinstance(a_received, A)
assert a_received.hello == 10
channel.send(a_received)
remote_a2_is_a1 = channel.receive()
assert remote_a2_is_a1
def test_send_concurrent(self):
channel = self.gw.remote_exec("""
from xdist.mypickle import PickleChannel
channel = PickleChannel(channel)
from testing.test_mypickle import A
l = [A() for i in range(10)]
channel.send(l)
other_l = channel.receive()
channel.send((l, other_l))
channel.send(channel.receive())
channel.receive()
""")
channel = PickleChannel(channel)
l = [A() for i in range(10)]
channel.send(l)
other_l = channel.receive()
channel.send(other_l)
ret = channel.receive()
assert ret[0] is other_l
assert ret[1] is l
back = channel.receive()
assert other_l is other_l
channel.send(None)
#s1 = p1.dumps(a1)
#s2 = p2.dumps(a2)
#other_a1 = p2.loads(s1)
#other_a2 = p1.loads(s2)
#a1_back = p1.loads(p2.dumps(other_a1))
def test_popen_with_callback(self):
channel = self.gw.remote_exec("""
from xdist.mypickle import PickleChannel
channel = PickleChannel(channel)
from testing.test_mypickle import A
a1 = A()
a1.hello = 10
channel.send(a1)
a2 = channel.receive()
channel.send(a2 is a1)
""")
channel = PickleChannel(channel)
queue = Queue()
channel.setcallback(queue.put)
a_received = queue.get(timeout=TESTTIMEOUT)
assert isinstance(a_received, A)
assert a_received.hello == 10
channel.send(a_received)
#remote_a2_is_a1 = queue.get(timeout=TESTTIMEOUT)
#assert remote_a2_is_a1
def test_popen_with_callback_with_endmarker(self):
channel = self.gw.remote_exec("""
from xdist.mypickle import PickleChannel
channel = PickleChannel(channel)
from testing.test_mypickle import A
a1 = A()
a1.hello = 10
channel.send(a1)
a2 = channel.receive()
channel.send(a2 is a1)
""")
channel = PickleChannel(channel)
queue = Queue()
channel.setcallback(queue.put, endmarker=-1)
a_received = queue.get(timeout=TESTTIMEOUT)
assert isinstance(a_received, A)
assert a_received.hello == 10
channel.send(a_received)
remote_a2_is_a1 = queue.get(timeout=TESTTIMEOUT)
assert remote_a2_is_a1
endmarker = queue.get(timeout=TESTTIMEOUT)
assert endmarker == -1
def test_popen_with_callback_with_endmarker_and_unpickling_error(self):
channel = self.gw.remote_exec("""
from xdist.mypickle import PickleChannel
channel = PickleChannel(channel)
from testing.test_mypickle import A
a1 = A()
channel.send(a1)
channel.send(a1)
""")
channel = PickleChannel(channel)
queue = Queue()
a = channel.receive()
channel._ipickle._unpicklememo.clear()
channel.setcallback(queue.put, endmarker=-1)
next = queue.get(timeout=TESTTIMEOUT)
assert next == -1
error = channel._getremoteerror()
assert isinstance(error, UnpickleError)
def test_popen_with_various_methods(self):
channel = self.gw.remote_exec("""
from xdist.mypickle import PickleChannel
channel = PickleChannel(channel)
channel.receive()
""")
channel = PickleChannel(channel)
assert not channel.isclosed()
assert not channel._getremoteerror()
channel.send(2)
channel.waitclose(timeout=2)

View File

@@ -1,120 +0,0 @@
import py
from xdist.nodemanage import NodeManager
class pytest_funcarg__mysetup:
def __init__(self, request):
basetemp = request.config.mktemp(
"mysetup-%s" % request.function.__name__,
numbered=True)
self.source = basetemp.mkdir("source")
self.dest = basetemp.mkdir("dest")
request.getfuncargvalue("_pytest")
class TestNodeManager:
@py.test.mark.xfail
def test_rsync_roots_no_roots(self, testdir, mysetup):
mysetup.source.ensure("dir1", "file1").write("hello")
config = testdir.reparseconfig([source])
nodemanager = NodeManager(config, ["popen//chdir=%s" % mysetup.dest])
assert nodemanager.config.topdir == source == config.topdir
nodemanager.rsync_roots()
p, = nodemanager.gwmanager.multi_exec("import os ; channel.send(os.getcwd())").receive_each()
p = py.path.local(p)
py.builtin.print_("remote curdir", p)
assert p == mysetup.dest.join(config.topdir.basename)
assert p.join("dir1").check()
assert p.join("dir1", "file1").check()
def test_popen_rsync_subdir(self, testdir, mysetup):
source, dest = mysetup.source, mysetup.dest
dir1 = mysetup.source.mkdir("dir1")
dir2 = dir1.mkdir("dir2")
dir2.ensure("hello")
for rsyncroot in (dir1, source):
dest.remove()
nodemanager = NodeManager(testdir.parseconfig(
"--tx", "popen//chdir=%s" % dest,
"--rsyncdir", rsyncroot,
source,
))
assert nodemanager.config.topdir == source
nodemanager.rsync_roots()
if rsyncroot == source:
dest = dest.join("source")
assert dest.join("dir1").check()
assert dest.join("dir1", "dir2").check()
assert dest.join("dir1", "dir2", 'hello').check()
nodemanager.gwmanager.exit()
def test_init_rsync_roots(self, testdir, mysetup):
source, dest = mysetup.source, mysetup.dest
dir2 = source.ensure("dir1", "dir2", dir=1)
source.ensure("dir1", "somefile", dir=1)
dir2.ensure("hello")
source.ensure("bogusdir", "file")
source.join("conftest.py").write(py.code.Source("""
rsyncdirs = ['dir1/dir2']
"""))
session = testdir.reparseconfig([source]).initsession()
nodemanager = NodeManager(session.config, ["popen//chdir=%s" % dest])
nodemanager.rsync_roots()
assert dest.join("dir2").check()
assert not dest.join("dir1").check()
assert not dest.join("bogus").check()
def test_rsyncignore(self, testdir, mysetup):
source, dest = mysetup.source, mysetup.dest
dir2 = source.ensure("dir1", "dir2", dir=1)
dir5 = source.ensure("dir5", "dir6", "bogus")
dirf = source.ensure("dir5", "file")
dir2.ensure("hello")
source.join("conftest.py").write(py.code.Source("""
rsyncdirs = ['dir1', 'dir5']
rsyncignore = ['dir1/dir2', 'dir5/dir6']
"""))
session = testdir.reparseconfig([source]).initsession()
nodemanager = NodeManager(session.config,
["popen//chdir=%s" % dest])
nodemanager.rsync_roots()
assert dest.join("dir1").check()
assert not dest.join("dir1", "dir2").check()
assert dest.join("dir5","file").check()
assert not dest.join("dir6").check()
def test_optimise_popen(self, testdir, mysetup):
source, dest = mysetup.source, mysetup.dest
specs = ["popen"] * 3
source.join("conftest.py").write("rsyncdirs = ['a']")
source.ensure('a', dir=1)
config = testdir.reparseconfig([source])
nodemanager = NodeManager(config, specs)
nodemanager.rsync_roots()
for gwspec in nodemanager.gwmanager.specs:
assert gwspec._samefilesystem()
assert not gwspec.chdir
def test_setup_DEBUG(self, mysetup, testdir):
source = mysetup.source
specs = ["popen"] * 2
source.join("conftest.py").write("rsyncdirs = ['a']")
source.ensure('a', dir=1)
config = testdir.reparseconfig([source, '--debug'])
assert config.option.debug
nodemanager = NodeManager(config, specs)
reprec = testdir.getreportrecorder(config).hookrecorder
nodemanager.setup_nodes(putevent=[].append)
for spec in nodemanager.gwmanager.specs:
l = reprec.getcalls("pytest_trace")
assert l
nodemanager.teardown_nodes()
def test_ssh_setup_nodes(self, specssh, testdir):
testdir.makepyfile(__init__="", test_x="""
def test_one():
pass
""")
reprec = testdir.inline_run("-d", "--rsyncdir=%s" % testdir.tmpdir,
"--tx", specssh, testdir.tmpdir)
rep, = reprec.getreports("pytest_runtest_logreport")
assert rep.passed

View File

@@ -1,82 +0,0 @@
import py
import pickle
def setglobals(request):
oldconfig = py.test.config
print("setting py.test.config to None")
py.test.config = None
def resetglobals():
py.builtin.print_("setting py.test.config to", oldconfig)
py.test.config = oldconfig
request.addfinalizer(resetglobals)
def pytest_funcarg__testdir(request):
setglobals(request)
return request.getfuncargvalue("testdir")
class ImmutablePickleTransport:
def __init__(self, request):
from xdist.mypickle import ImmutablePickler
self.p1 = ImmutablePickler(uneven=0)
self.p2 = ImmutablePickler(uneven=1)
setglobals(request)
def p1_to_p2(self, obj):
return self.p2.loads(self.p1.dumps(obj))
def p2_to_p1(self, obj):
return self.p1.loads(self.p2.dumps(obj))
def unifyconfig(self, config):
p2config = self.p1_to_p2(config)
p2config._initafterpickle(config.topdir)
return p2config
pytest_funcarg__pickletransport = ImmutablePickleTransport
class TestImmutablePickling:
def test_pickle_config(self, testdir, pickletransport):
config1 = testdir.parseconfig()
assert config1.topdir == testdir.tmpdir
testdir.chdir()
p2config = pickletransport.p1_to_p2(config1)
assert p2config.topdir.realpath() == config1.topdir.realpath()
config_back = pickletransport.p2_to_p1(p2config)
assert config_back is config1
def test_pickle_modcol(self, testdir, pickletransport):
modcol1 = testdir.getmodulecol("def test_one(): pass")
modcol2a = pickletransport.p1_to_p2(modcol1)
modcol2b = pickletransport.p1_to_p2(modcol1)
assert modcol2a is modcol2b
modcol1_back = pickletransport.p2_to_p1(modcol2a)
assert modcol1_back
def test_pickle_func(self, testdir, pickletransport):
modcol1 = testdir.getmodulecol("def test_one(): pass")
item = modcol1.collect_by_name("test_one")
testdir.chdir()
item2a = pickletransport.p1_to_p2(item)
assert item is not item2a # of course
assert item2a.name == item.name
modback = pickletransport.p2_to_p1(item2a.parent)
assert modback is modcol1
def test_config__setstate__wired_correctly_in_childprocess(testdir):
execnet = py.test.importorskip("execnet")
from xdist.mypickle import PickleChannel
gw = execnet.makegateway()
channel = gw.remote_exec("""
import py
from xdist.mypickle import PickleChannel
channel = PickleChannel(channel)
config = channel.receive()
assert py.test.config == config
""")
channel = PickleChannel(channel)
config = testdir.parseconfig()
channel.send(config)
channel.waitclose() # this will potentially raise
gw.exit()

View File

@@ -1,11 +1,10 @@
import py
import execnet
from xdist.nodemanage import NodeManager
from xdist.slavemanage import NodeManager
def test_dist_incompatibility_messages(testdir):
Error = py.test.config.Error
py.test.raises(Error, "testdir.parseconfigure('--pdb', '--looponfail')")
result = testdir.runpytest("--pdb", "--looponfail")
assert result.ret != 0
result = testdir.runpytest("--pdb", "-n", "3")
assert result.ret != 0
assert "incompatible" in result.stderr.str()
@@ -14,10 +13,13 @@ def test_dist_incompatibility_messages(testdir):
assert "incompatible" in result.stderr.str()
def test_dist_options(testdir):
from xdist.plugin import check_options
config = testdir.parseconfigure("-n 2")
check_options(config)
assert config.option.dist == "load"
assert config.option.tx == ['popen'] * 2
config = testdir.parseconfigure("-d")
check_options(config)
assert config.option.dist == "load"
class TestDistOptions:
@@ -27,35 +29,42 @@ class TestDistOptions:
xspecs = nodemanager._getxspecs()
assert len(xspecs) == 2
print(xspecs)
assert xspecs[0].popen
assert xspecs[0].popen
assert xspecs[1].ssh == "xyz"
def test_xspecs_multiplied(self, testdir):
config = testdir.parseconfigure("--tx=3*popen",)
xspecs = NodeManager(config)._getxspecs()
assert len(xspecs) == 3
assert xspecs[1].popen
assert xspecs[1].popen
def test_getrsyncdirs(self, testdir):
config = testdir.parseconfigure('--rsyncdir=' + str(testdir.tmpdir))
nm = NodeManager(config, specs=[execnet.XSpec("popen")])
roots = nm._getrsyncdirs()
assert len(roots) == 1 + 2 # pylib + xdist
assert testdir.tmpdir in roots
assert not nm._getrsyncdirs()
nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")])
assert nm.roots
assert testdir.tmpdir in nm.roots
def test_getrsyncignore(self, testdir):
config = testdir.parseconfigure('--rsyncignore=fo*')
nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=qwe")])
assert 'fo*' in nm.rsyncoptions['ignores']
def test_getrsyncdirs_with_conftest(self, testdir):
p = py.path.local()
for bn in 'x y z'.split():
p.mkdir(bn)
testdir.makeconftest("""
rsyncdirs= 'x',
testdir.makeini("""
[pytest]
rsyncdirs= x
""")
config = testdir.parseconfigure(
testdir.tmpdir, '--rsyncdir=y', '--rsyncdir=z')
nm = NodeManager(config, specs=[execnet.XSpec("popen")])
nm = NodeManager(config, specs=[execnet.XSpec("popen//chdir=xyz")])
roots = nm._getrsyncdirs()
assert len(roots) == 3 + 2 # pylib + xdist
assert py.path.local('y') in roots
assert py.path.local('z') in roots
assert testdir.tmpdir.join('x') in roots
#assert len(roots) == 3 + 1 # pylib
assert py.path.local('y') in roots
assert py.path.local('z') in roots
assert testdir.tmpdir.join('x') in roots

View File

@@ -1,151 +1,249 @@
import py
py.test.importorskip("execnet")
from xdist.remote import LooponfailingSession, LoopState, RemoteControl
from xdist.slavemanage import SlaveController, unserialize_report
from xdist.remote import serialize_report
import execnet
queue = py.builtin._tryimport("queue", "Queue")
from py.builtin import print_
import marshal
class TestRemoteControl:
def test_nofailures(self, testdir):
item = testdir.getitem("def test_func(): pass\n")
control = RemoteControl(item.config)
control.setup()
failures = control.runsession()
assert not failures
WAIT_TIMEOUT = 10.0
def test_failures_somewhere(self, testdir):
item = testdir.getitem("def test_func(): assert 0\n")
control = RemoteControl(item.config)
control.setup()
failures = control.runsession()
assert failures
control.setup()
item.fspath.write("def test_func(): assert 1\n")
pyc = item.fspath.new(ext=".pyc")
if pyc.check():
pyc.remove()
failures = control.runsession(failures)
assert not failures
def check_marshallable(d):
try:
marshal.dumps(d)
except ValueError:
py.std.pprint.pprint(d)
raise ValueError("not marshallable")
def test_failure_change(self, testdir):
modcol = testdir.getitem("""
def test_func():
assert 0
class EventCall:
def __init__(self, eventcall):
self.name, self.kwargs = eventcall
def __str__(self):
return "<EventCall %s(**%s)>" %(self.name, self.kwargs)
class SlaveSetup:
use_callback = False
def __init__(self, request):
self.testdir = testdir = request.getfuncargvalue("testdir")
self.request = request
self.events = queue.Queue()
def setup(self, ):
self.testdir.chdir()
#import os ; os.environ['EXECNET_DEBUG'] = "2"
self.gateway = execnet.makegateway()
self.config = config = self.testdir.parseconfigure()
putevent = self.use_callback and self.events.put or None
self.slp = SlaveController(None, self.gateway, config, putevent)
self.request.addfinalizer(self.slp.ensure_teardown)
self.slp.setup()
def popevent(self, name=None):
while 1:
if self.use_callback:
data = self.events.get(timeout=WAIT_TIMEOUT)
else:
data = self.slp.channel.receive(timeout=WAIT_TIMEOUT)
ev = EventCall(data)
if name is None or ev.name == name:
return ev
print("skipping %s" % (ev,))
def sendcommand(self, name, **kwargs):
self.slp.sendcommand(name, **kwargs)
def pytest_funcarg__slave(request):
return SlaveSetup(request)
def test_remoteinitconfig(testdir):
from xdist.remote import remote_initconfig
config1 = testdir.parseconfig()
config2 = remote_initconfig(config1.option.__dict__, config1.args)
assert config2.option.__dict__ == config1.option.__dict__
assert config2.pluginmanager.getplugin("terminal") in (-1, None)
class TestReportSerialization:
def test_itemreport_outcomes(self, testdir):
reprec = testdir.inline_runsource("""
import py
def test_pass(): pass
def test_fail(): 0/0
@py.test.mark.skipif("True")
def test_skip(): pass
def test_skip_imperative():
py.test.skip("hello")
@py.test.mark.xfail("True")
def test_xfail(): 0/0
def test_xfail_imperative():
py.test.xfail("hello")
""")
control = RemoteControl(modcol.config)
control.setup()
failures = control.runsession()
assert failures
control.setup()
modcol.fspath.write(py.code.Source("""
reports = reprec.getreports("pytest_runtest_logreport")
assert len(reports) == 17 # with setup/teardown "passed" reports
for rep in reports:
d = serialize_report(rep)
check_marshallable(d)
newrep = unserialize_report("testreport", d)
assert newrep.passed == rep.passed
assert newrep.failed == rep.failed
assert newrep.skipped == rep.skipped
if newrep.skipped and not hasattr(newrep, "wasxfail"):
assert len(newrep.longrepr) == 3
assert newrep.outcome == rep.outcome
assert newrep.when == rep.when
assert newrep.keywords == rep.keywords
if rep.failed:
assert newrep.longrepr == str(rep.longrepr)
def test_collectreport_passed(self, testdir):
reprec = testdir.inline_runsource("def test_func(): pass")
reports = reprec.getreports("pytest_collectreport")
for rep in reports:
d = serialize_report(rep)
check_marshallable(d)
newrep = unserialize_report("collectreport", d)
assert newrep.passed == rep.passed
assert newrep.failed == rep.failed
assert newrep.skipped == rep.skipped
def test_collectreport_fail(self, testdir):
reprec = testdir.inline_runsource("qwe abc")
reports = reprec.getreports("pytest_collectreport")
assert reports
for rep in reports:
d = serialize_report(rep)
check_marshallable(d)
newrep = unserialize_report("collectreport", d)
assert newrep.passed == rep.passed
assert newrep.failed == rep.failed
assert newrep.skipped == rep.skipped
if rep.failed:
assert newrep.longrepr == str(rep.longrepr)
def test_extended_report_deserialization(self, testdir):
reprec = testdir.inline_runsource("qwe abc")
reports = reprec.getreports("pytest_collectreport")
assert reports
for rep in reports:
rep.extra = True
d = serialize_report(rep)
check_marshallable(d)
newrep = unserialize_report("collectreport", d)
assert newrep.extra
assert newrep.passed == rep.passed
assert newrep.failed == rep.failed
assert newrep.skipped == rep.skipped
if rep.failed:
assert newrep.longrepr == str(rep.longrepr)
class TestSlaveInteractor:
def test_basic_collect_and_runtests(self, slave):
p = slave.testdir.makepyfile("""
def test_func():
assert 1
def test_new():
assert 0
"""))
pyc = modcol.fspath.new(ext=".pyc")
if pyc.check():
pyc.remove()
failures = control.runsession(failures)
assert not failures
control.setup()
failures = control.runsession()
assert failures
assert str(failures).find("test_new") != -1
class TestLooponFailing:
def test_looponfail_from_fail_to_ok(self, testdir):
modcol = testdir.getmodulecol("""
def test_one():
x = 0
assert x == 1
def test_two():
assert 1
pass
""")
session = LooponfailingSession(modcol.config)
loopstate = LoopState()
session.remotecontrol.setup()
session.loop_once(loopstate)
assert len(loopstate.colitems) == 1
modcol.fspath.write(py.code.Source("""
def test_one():
x = 15
assert x == 15
def test_two():
assert 1
"""))
assert session.statrecorder.check()
session.loop_once(loopstate)
assert not loopstate.colitems
slave.setup()
ev = slave.popevent()
assert ev.name == "slaveready"
ev = slave.popevent()
assert ev.name == "collectionstart"
assert not ev.kwargs
ev = slave.popevent("collectionfinish")
assert ev.kwargs['topdir'] == slave.testdir.tmpdir
ids = ev.kwargs['ids']
assert len(ids) == 1
slave.sendcommand("runtests", indices=list(range(len(ids))))
slave.sendcommand("shutdown")
ev = slave.popevent("logstart")
assert ev.kwargs["nodeid"].endswith("test_func")
assert len(ev.kwargs["location"]) == 3
ev = slave.popevent("testreport") # setup
ev = slave.popevent("testreport")
assert ev.name == "testreport"
rep = unserialize_report(ev.name, ev.kwargs['data'])
assert rep.nodeid.endswith("::test_func")
assert rep.passed
assert rep.when == "call"
ev = slave.popevent("slavefinished")
assert 'slaveoutput' in ev.kwargs
def test_looponfail_from_one_to_two_tests(self, testdir):
modcol = testdir.getmodulecol("""
def test_one():
assert 0
def test_remote_collect_skip(self, slave):
p = slave.testdir.makepyfile("""
import py
py.test.skip("hello")
""")
session = LooponfailingSession(modcol.config)
loopstate = LoopState()
session.remotecontrol.setup()
loopstate.colitems = []
session.loop_once(loopstate)
assert len(loopstate.colitems) == 1
slave.setup()
ev = slave.popevent("collectionstart")
assert not ev.kwargs
ev = slave.popevent()
assert ev.name == "collectreport"
ev = slave.popevent()
assert ev.name == "collectreport"
rep = unserialize_report(ev.name, ev.kwargs['data'])
assert rep.skipped
ev = slave.popevent("collectionfinish")
assert not ev.kwargs['ids']
modcol.fspath.write(py.code.Source("""
def test_one():
assert 1 # passes now
def test_two():
assert 0 # new and fails
"""))
assert session.statrecorder.check()
session.loop_once(loopstate)
assert len(loopstate.colitems) == 0
def test_remote_collect_fail(self, slave):
p = slave.testdir.makepyfile("""aasd qwe""")
slave.setup()
ev = slave.popevent("collectionstart")
assert not ev.kwargs
ev = slave.popevent()
assert ev.name == "collectreport"
ev = slave.popevent()
assert ev.name == "collectreport"
rep = unserialize_report(ev.name, ev.kwargs['data'])
assert rep.failed
ev = slave.popevent("collectionfinish")
assert not ev.kwargs['ids']
session.loop_once(loopstate)
assert len(loopstate.colitems) == 1
def test_looponfail_removed_test(self, testdir):
modcol = testdir.getmodulecol("""
def test_one():
assert 0
def test_two():
assert 0
def test_runtests_all(self, slave):
p = slave.testdir.makepyfile("""
def test_func(): pass
def test_func2(): pass
""")
session = LooponfailingSession(modcol.config)
loopstate = LoopState()
session.remotecontrol.setup()
loopstate.colitems = []
session.loop_once(loopstate)
assert len(loopstate.colitems) == 2
slave.setup()
ev = slave.popevent()
assert ev.name == "slaveready"
ev = slave.popevent()
assert ev.name == "collectionstart"
assert not ev.kwargs
ev = slave.popevent("collectionfinish")
ids = ev.kwargs['ids']
assert len(ids) == 2
slave.sendcommand("runtests_all", )
slave.sendcommand("shutdown", )
for func in "::test_func", "::test_func2":
for i in range(3): # setup/call/teardown
ev = slave.popevent("testreport")
assert ev.name == "testreport"
rep = unserialize_report(ev.name, ev.kwargs['data'])
assert rep.nodeid.endswith(func)
ev = slave.popevent("slavefinished")
assert 'slaveoutput' in ev.kwargs
modcol.fspath.write(py.code.Source("""
def test_xxx(): # renamed test
assert 0
def test_two():
assert 1 # pass now
"""))
assert session.statrecorder.check()
session.loop_once(loopstate)
assert len(loopstate.colitems) == 0
session.loop_once(loopstate)
assert len(loopstate.colitems) == 1
def test_looponfail_functional_fail_to_ok(self, testdir):
p = testdir.makepyfile("""
def test_one():
x = 0
assert x == 1
def test_happy_run_events_converted(self, testdir, slave):
py.test.xfail("implement a simple test for event production")
assert not slave.use_callback
p = slave.testdir.makepyfile("""
def test_func():
pass
""")
child = testdir.spawn_pytest("-f %s" % p)
child.expect("def test_one")
child.expect("x == 1")
child.expect("1 failed")
child.expect("### LOOPONFAILING ####")
child.expect("waiting for changes")
p.write(py.code.Source("""
def test_one():
x = 1
assert x == 1
"""))
child.expect(".*1 passed.*")
child.kill(15)
slave.setup()
hookrec = testdir.getreportrecorder(slave.config)
for data in slave.slp.channel:
slave.slp.process_from_remote(data)
slave.slp.process_from_remote(slave.slp.ENDMARK)
py.std.pprint.pprint(hookrec.hookrecorder.calls)
hookrec.hookrecorder.contains([
("pytest_collectstart", "collector.fspath == aaa"),
("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.collector.fspath == aaa"),
("pytest_collectstart", "collector.fspath == bbb"),
("pytest_pycollect_makeitem", "name == 'test_func'"),
("pytest_collectreport", "report.collector.fspath == bbb"),
])

242
testing/test_slavemanage.py Normal file
View File

@@ -0,0 +1,242 @@
import py
import os
import execnet
from xdist.slavemanage import HostRSync, NodeManager
pytest_plugins = "pytester",
def pytest_funcarg__hookrecorder(request):
_pytest = request.getfuncargvalue('_pytest')
config = request.getfuncargvalue('config')
return _pytest.gethookrecorder(config.hook)
def pytest_funcarg__config(request):
testdir = request.getfuncargvalue("testdir")
config = testdir.parseconfig()
return config
def pytest_funcarg__mysetup(request):
class mysetup:
def __init__(self, request):
temp = request.getfuncargvalue("tmpdir")
self.source = temp.mkdir("source")
self.dest = temp.mkdir("dest")
request.getfuncargvalue("_pytest")
return mysetup(request)
class TestNodeManagerPopen:
def test_popen_no_default_chdir(self, config):
gm = NodeManager(config, ["popen"])
assert gm.specs[0].chdir is None
def test_default_chdir(self, config):
l = ["ssh=noco", "socket=xyz"]
for spec in NodeManager(config, l).specs:
assert spec.chdir == "pyexecnetcache"
for spec in NodeManager(config, l, defaultchdir="abc").specs:
assert spec.chdir == "abc"
def test_popen_makegateway_events(self, config, hookrecorder, _pytest):
hm = NodeManager(config, ["popen"] * 2)
hm.makegateways()
call = hookrecorder.popcall("pytest_xdist_setupnodes")
assert len(call.specs) == 2
call = hookrecorder.popcall("pytest_xdist_newgateway")
assert call.gateway.spec == execnet.XSpec("popen")
assert call.gateway.id == "gw0"
call = hookrecorder.popcall("pytest_xdist_newgateway")
assert call.gateway.id == "gw1"
assert len(hm.group) == 2
hm.teardown_nodes()
assert not len(hm.group)
def test_popens_rsync(self, config, mysetup):
source = mysetup.source
hm = NodeManager(config, ["popen"] * 2)
hm.makegateways()
assert len(hm.group) == 2
for gw in hm.group:
class pseudoexec:
args = []
def __init__(self, *args):
self.args.extend(args)
def waitclose(self):
pass
gw.remote_exec = pseudoexec
l = []
hm.rsync(source, notify=lambda *args: l.append(args))
assert not l
hm.teardown_nodes()
assert not len(hm.group)
assert "sys.path.insert" in gw.remote_exec.args[0]
def test_rsync_popen_with_path(self, config, mysetup):
source, dest = mysetup.source, mysetup.dest
hm = NodeManager(config, ["popen//chdir=%s" %dest] * 1)
hm.makegateways()
source.ensure("dir1", "dir2", "hello")
l = []
hm.rsync(source, notify=lambda *args: l.append(args))
assert len(l) == 1
assert l[0] == ("rsyncrootready", hm.group['gw0'].spec, source)
hm.teardown_nodes()
dest = dest.join(source.basename)
assert dest.join("dir1").check()
assert dest.join("dir1", "dir2").check()
assert dest.join("dir1", "dir2", 'hello').check()
def test_rsync_same_popen_twice(self, config, mysetup, hookrecorder):
source, dest = mysetup.source, mysetup.dest
hm = NodeManager(config, ["popen//chdir=%s" %dest] * 2)
hm.makegateways()
source.ensure("dir1", "dir2", "hello")
hm.rsync(source)
call = hookrecorder.popcall("pytest_xdist_rsyncstart")
assert call.source == source
assert len(call.gateways) == 1
assert call.gateways[0] in hm.group
call = hookrecorder.popcall("pytest_xdist_rsyncfinish")
class TestHRSync:
def pytest_funcarg__mysetup(self, request):
class mysetup:
def __init__(self, request):
tmp = request.getfuncargvalue('tmpdir')
self.source = tmp.mkdir("source")
self.dest = tmp.mkdir("dest")
return mysetup(request)
def test_hrsync_filter(self, mysetup):
source, dest = mysetup.source, mysetup.dest
source.ensure("dir", "file.txt")
source.ensure(".svn", "entries")
source.ensure(".somedotfile", "moreentries")
source.ensure("somedir", "editfile~")
syncer = HostRSync(source, ignores=NodeManager.DEFAULT_IGNORES)
l = list(source.visit(rec=syncer.filter,
fil=syncer.filter))
assert len(l) == 3
basenames = [x.basename for x in l]
assert 'dir' in basenames
assert 'file.txt' in basenames
assert 'somedir' in basenames
def test_hrsync_one_host(self, mysetup):
source, dest = mysetup.source, mysetup.dest
gw = execnet.makegateway("popen//chdir=%s" % dest)
finished = []
rsync = HostRSync(source)
rsync.add_target_host(gw, finished=lambda: finished.append(1))
source.join("hello.py").write("world")
rsync.send()
gw.exit()
assert dest.join(source.basename, "hello.py").check()
assert len(finished) == 1
class TestNodeManager:
@py.test.mark.xfail
def test_rsync_roots_no_roots(self, testdir, mysetup):
mysetup.source.ensure("dir1", "file1").write("hello")
config = testdir.parseconfig(source)
nodemanager = NodeManager(config, ["popen//chdir=%s" % mysetup.dest])
#assert nodemanager.config.topdir == source == config.topdir
nodemanager.makegateways()
nodemanager.rsync_roots()
p, = nodemanager.gwmanager.multi_exec(
"import os ; channel.send(os.getcwd())").receive_each()
p = py.path.local(p)
py.builtin.print_("remote curdir", p)
assert p == mysetup.dest.join(config.topdir.basename)
assert p.join("dir1").check()
assert p.join("dir1", "file1").check()
def test_popen_rsync_subdir(self, testdir, mysetup):
source, dest = mysetup.source, mysetup.dest
dir1 = mysetup.source.mkdir("dir1")
dir2 = dir1.mkdir("dir2")
dir2.ensure("hello")
for rsyncroot in (dir1, source):
dest.remove()
nodemanager = NodeManager(testdir.parseconfig(
"--tx", "popen//chdir=%s" % dest,
"--rsyncdir", rsyncroot,
source,
))
nodemanager.makegateways()
nodemanager.rsync_roots()
if rsyncroot == source:
dest = dest.join("source")
assert dest.join("dir1").check()
assert dest.join("dir1", "dir2").check()
assert dest.join("dir1", "dir2", 'hello').check()
nodemanager.teardown_nodes()
def test_init_rsync_roots(self, testdir, mysetup):
source, dest = mysetup.source, mysetup.dest
dir2 = source.ensure("dir1", "dir2", dir=1)
source.ensure("dir1", "somefile", dir=1)
dir2.ensure("hello")
source.ensure("bogusdir", "file")
source.join("tox.ini").write(py.std.textwrap.dedent("""
[pytest]
rsyncdirs=dir1/dir2
"""))
config = testdir.parseconfig(source)
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
nodemanager.makegateways()
nodemanager.rsync_roots()
assert dest.join("dir2").check()
assert not dest.join("dir1").check()
assert not dest.join("bogus").check()
def test_rsyncignore(self, testdir, mysetup):
source, dest = mysetup.source, mysetup.dest
dir2 = source.ensure("dir1", "dir2", dir=1)
dir5 = source.ensure("dir5", "dir6", "bogus")
dirf = source.ensure("dir5", "file")
dir2.ensure("hello")
dirfoo = source.ensure("foo", "bar")
dirbar = source.ensure("bar", "foo")
source.join("tox.ini").write(py.std.textwrap.dedent("""
[pytest]
rsyncdirs = dir1 dir5
rsyncignore = dir1/dir2 dir5/dir6 foo*
"""))
config = testdir.parseconfig(source)
config.option.rsyncignore = ['bar']
nodemanager = NodeManager(config, ["popen//chdir=%s" % dest])
nodemanager.makegateways()
nodemanager.rsync_roots()
assert dest.join("dir1").check()
assert not dest.join("dir1", "dir2").check()
assert dest.join("dir5","file").check()
assert not dest.join("dir6").check()
assert not dest.join('foo').check()
assert not dest.join('bar').check()
def test_optimise_popen(self, testdir, mysetup):
source, dest = mysetup.source, mysetup.dest
specs = ["popen"] * 3
source.join("conftest.py").write("rsyncdirs = ['a']")
source.ensure('a', dir=1)
config = testdir.parseconfig(source)
nodemanager = NodeManager(config, specs)
nodemanager.makegateways()
nodemanager.rsync_roots()
for gwspec in nodemanager.specs:
assert gwspec._samefilesystem()
assert not gwspec.chdir
def test_ssh_setup_nodes(self, specssh, testdir):
testdir.makepyfile(__init__="", test_x="""
def test_one():
pass
""")
reprec = testdir.inline_run("-d", "--rsyncdir=%s" % testdir.tmpdir,
"--tx", specssh, testdir.tmpdir)
rep, = reprec.getreports("pytest_runtest_logreport")
assert rep.passed

View File

@@ -1,149 +0,0 @@
import py
import execnet
from xdist.txnode import TXNode
queue = py.builtin._tryimport("queue", "Queue")
Queue = queue.Queue
class EventQueue:
def __init__(self, registry, queue=None):
if queue is None:
queue = Queue()
self.queue = queue
registry.register(self)
def geteventargs(self, eventname, timeout=10.0):
events = []
while 1:
try:
eventcall = self.queue.get(timeout=timeout)
except queue.Empty:
#print "node channel", self.node.channel
#print "remoteerror", self.node.channel._getremoteerror()
py.builtin.print_("seen events", events)
raise IOError("did not see %r events" % (eventname))
else:
name, args, kwargs = eventcall
assert isinstance(name, str)
if name == eventname:
if args:
return args
return kwargs
events.append(name)
if name == "pytest_internalerror":
py.builtin.print_(str(kwargs["excrepr"]))
class MySetup:
def __init__(self, request):
self.id = 0
self.request = request
def geteventargs(self, eventname, timeout=10.0):
eq = EventQueue(self.config.pluginmanager, self.queue)
return eq.geteventargs(eventname, timeout=timeout)
def makenode(self, config=None):
if config is None:
testdir = self.request.getfuncargvalue("testdir")
config = testdir.reparseconfig([])
self.config = config
self.queue = Queue()
self.xspec = execnet.XSpec("popen")
self.gateway = execnet.makegateway(self.xspec)
self.id += 1
self.gateway.id = str(self.id)
self.nodemanager = None
self.node = TXNode(self.nodemanager, self.gateway, self.config, putevent=self.queue.put)
assert not self.node.channel.isclosed()
return self.node
def xfinalize(self):
if hasattr(self, 'node'):
gw = self.node.gateway
py.builtin.print_("exiting:", gw)
gw.exit()
def pytest_funcarg__mysetup(request):
mysetup = MySetup(request)
#pyfuncitem.addfinalizer(mysetup.finalize)
return mysetup
def test_node_hash_equality(mysetup):
node = mysetup.makenode()
node2 = mysetup.makenode()
assert node != node2
assert node == node
assert not (node != node)
class TestMasterSlaveConnection:
def test_crash_invalid_item(self, mysetup):
node = mysetup.makenode()
node.send(123) # invalid item
kwargs = mysetup.geteventargs("pytest_testnodedown")
assert kwargs['node'] is node
#assert isinstance(kwargs['error'], execnet.RemoteError)
def test_crash_killed(self, testdir, mysetup):
if not hasattr(py.std.os, 'kill'):
py.test.skip("no os.kill")
item = testdir.getitem("""
def test_func():
import os
os.kill(os.getpid(), 9)
""")
node = mysetup.makenode(item.config)
node.send(item)
kwargs = mysetup.geteventargs("pytest_testnodedown")
assert kwargs['node'] is node
assert "Not properly terminated" in str(kwargs['error'])
def test_node_down(self, mysetup):
node = mysetup.makenode()
node.shutdown()
kwargs = mysetup.geteventargs("pytest_testnodedown")
assert kwargs['node'] is node
assert not kwargs['error']
node.callback(node.ENDMARK)
excinfo = py.test.raises(IOError,
"mysetup.geteventargs('testnodedown', timeout=0.01)")
def test_send_on_closed_channel(self, testdir, mysetup):
item = testdir.getitem("def test_func(): pass")
node = mysetup.makenode(item.config)
node.channel.close()
py.test.raises(IOError, "node.send(item)")
#ev = self.getcalls(pytest_internalerror)
#assert ev.excinfo.errisinstance(IOError)
def test_send_one(self, testdir, mysetup):
item = testdir.getitem("def test_func(): pass")
node = mysetup.makenode(item.config)
node.send(item)
kwargs = mysetup.geteventargs("pytest_runtest_logreport")
rep = kwargs['report']
assert rep.passed
py.builtin.print_(rep)
assert rep.item == item
def test_send_some(self, testdir, mysetup):
items = testdir.getitems("""
def test_pass():
pass
def test_fail():
assert 0
def test_skip():
import py
py.test.skip("x")
""")
node = mysetup.makenode(items[0].config)
for item in items:
node.send(item)
for outcome in "passed failed skipped".split():
kwargs = mysetup.geteventargs("pytest_runtest_logreport")
report = kwargs['report']
assert getattr(report, outcome)
node.sendlist(items)
for outcome in "passed failed skipped".split():
rep = mysetup.geteventargs("pytest_runtest_logreport")['report']
assert getattr(rep, outcome)

View File

@@ -1,61 +0,0 @@
import py
from xdist.util import StatRecorder
def test_filechange(tmpdir):
tmp = tmpdir
hello = tmp.ensure("hello.py")
sd = StatRecorder([tmp])
changed = sd.check()
assert not changed
hello.write("world")
changed = sd.check()
assert changed
tmp.ensure("new.py")
changed = sd.check()
assert changed
tmp.join("new.py").remove()
changed = sd.check()
assert changed
tmp.join("a", "b", "c.py").ensure()
changed = sd.check()
assert changed
tmp.join("a", "c.txt").ensure()
changed = sd.check()
assert changed
changed = sd.check()
assert not changed
tmp.join("a").remove()
changed = sd.check()
assert changed
def test_pycremoval(tmpdir):
tmp = tmpdir
hello = tmp.ensure("hello.py")
sd = StatRecorder([tmp])
changed = sd.check()
assert not changed
pycfile = hello + "c"
pycfile.ensure()
changed = sd.check()
assert not changed
hello.write("world")
changed = sd.check()
assert not pycfile.check()
def test_waitonchange(tmpdir, monkeypatch):
tmp = tmpdir
sd = StatRecorder([tmp])
l = [True, False]
monkeypatch.setattr(StatRecorder, 'check', lambda self: l.pop())
sd.waitonchange(checkinterval=0.2)
assert not l

27
tox.ini Normal file
View File

@@ -0,0 +1,27 @@
[tox]
envlist=py26,py32,py33,py27,py27-pexpect,py33-pexpect,py26,py26-old,py33-old
[testenv]
changedir=testing
deps=pytest>=2.5.1
commands= py.test --junitxml={envlogdir}/junit-{envname}.xml []
[testenv:py27-pexpect]
deps={[testenv]deps}
pexpect
[testenv:py33-pexpect]
deps={[testenv]deps}
pexpect
[testenv:py26-old]
deps=
pytest==2.4.2
[testenv:py33-old]
basepython = python3.3
deps=
pytest==2.4.2
[pytest]
addopts = -rsfxX
;; hello

View File

@@ -1,3 +1,2 @@
#
__version__ = "1.2"
__version__ = '1.10'

View File

@@ -1,297 +1,435 @@
import sys
import difflib
import pytest
import py
from py._test import session
from xdist.nodemanage import NodeManager
from xdist.slavemanage import NodeManager
queue = py.builtin._tryimport('queue', 'Queue')
debug_file = None # open('/tmp/loop.log', 'w')
def debug(*args):
if debug_file is not None:
s = " ".join(map(str, args))
debug_file.write(s+"\n")
debug_file.flush()
class LoopState(object):
def __init__(self, dsession, colitems):
self.dsession = dsession
self.colitems = colitems
self.exitstatus = None
# loopstate.dowork is False after reschedule events
# because otherwise we might very busily loop
# waiting for a host to become ready.
self.dowork = True
self.shuttingdown = False
self.testsfailed = False
class EachScheduling:
def __repr__(self):
return "<LoopState exitstatus=%r shuttingdown=%r len(colitems)=%d>" % (
self.exitstatus, self.shuttingdown, len(self.colitems))
def pytest_runtest_logreport(self, report):
if report.item in self.dsession.item2nodes:
if report.when != "teardown": # otherwise we already managed it
self.dsession.removeitem(report.item, report.node)
if report.failed:
self.testsfailed = True
def pytest_collectreport(self, report):
if report.passed:
self.colitems.extend(report.result)
def pytest_testnodeready(self, node):
self.dsession.addnode(node)
def pytest_testnodedown(self, node, error=None):
pending = self.dsession.removenode(node)
if pending:
if error:
crashitem = pending[0]
debug("determined crashitem", crashitem)
self.dsession.handle_crashitem(crashitem, node)
# XXX recovery handling for "each"?
# currently pending items are not retried
if self.dsession.config.option.dist == "load":
self.colitems.extend(pending[1:])
def pytest_rescheduleitems(self, items):
self.colitems[:] = items + self.colitems
for pending in self.dsession.node2pending.values():
if pending:
self.dowork = False # avoid busywait, nodes still have work
class ExitFirstInterrupt(KeyboardInterrupt):
pass
class DSession(session.Session):
"""
Session drives the collection and running of tests
and generates test events for reporters.
"""
LOAD_THRESHOLD_NEWITEMS = 5
ITEM_CHUNKSIZE = 10
def __init__(self, config):
self.queue = queue.Queue()
def __init__(self, numnodes, log=None):
self.numnodes = numnodes
self.node2collection = {}
self.node2pending = {}
self.item2nodes = {}
super(DSession, self).__init__(config=config)
if log is None:
self.log = py.log.Producer("eachsched")
else:
self.log = log.loadsched
self.collection_is_completed = False
def hasnodes(self):
return bool(self.node2pending)
def addnode(self, node):
self.node2collection[node] = None
def tests_finished(self):
if not self.collection_is_completed:
return False
return True
def addnode_collection(self, node, collection):
assert not self.collection_is_completed
assert self.node2collection[node] is None
self.node2collection[node] = list(collection)
self.node2pending[node] = []
if len(self.node2pending) >= self.numnodes:
self.collection_is_completed = True
def remove_item(self, node, item_index, duration=0):
self.node2pending[node].remove(item_index)
def remove_node(self, node):
# KeyError if we didn't get an addnode() yet
pending = self.node2pending.pop(node)
if not pending:
return
crashitem = self.node2collection[node][pending.pop(0)]
# XXX what about the rest of pending?
return crashitem
def init_distribute(self):
assert self.collection_is_completed
for node, pending in self.node2pending.items():
node.send_runtest_all()
pending[:] = range(len(self.node2collection[node]))
class LoadScheduling:
def __init__(self, numnodes, log=None):
self.numnodes = numnodes
self.node2pending = {}
self.node2collection = {}
self.pending = []
if log is None:
self.log = py.log.Producer("loadsched")
else:
self.log = log.loadsched
self.collection_is_completed = False
def hasnodes(self):
return bool(self.node2pending)
def addnode(self, node):
self.node2pending[node] = []
def tests_finished(self):
if not self.collection_is_completed or self.pending:
return False
#for items in self.node2pending.values():
# if items:
# return False
return True
def addnode_collection(self, node, collection):
assert not self.collection_is_completed
assert node in self.node2pending
self.node2collection[node] = list(collection)
if len(self.node2collection) >= self.numnodes:
self.collection_is_completed = True
def remove_item(self, node, item_index, duration=0):
node_pending = self.node2pending[node]
node_pending.remove(item_index)
# pre-load items-to-test if the node may become ready
if self.pending:
if duration >= 0.1 and node_pending:
# seems the node is doing long-running tests
# so let's rather wait with sending new items
return
# how many nodes do we have remaining per node roughly?
num_nodes = len(self.node2pending)
# if our node goes below a heuristic minimum, fill it out to
# heuristic maximum
items_per_node_min = max(
1, len(self.pending) // num_nodes // 4)
items_per_node_max = max(
1, len(self.pending) // num_nodes // 2)
if len(node_pending) <= items_per_node_min:
num_send = items_per_node_max - len(node_pending) + 1
self._send_tests(node, num_send)
self.log("num items waiting for node:", len(self.pending))
#self.log("node2pending:", self.node2pending)
def remove_node(self, node):
pending = self.node2pending.pop(node)
if not pending:
return
# the node must have crashed on the item if there are pending ones
crashitem = self.collection[pending.pop(0)]
self.pending.extend(pending)
return crashitem
def init_distribute(self):
assert self.collection_is_completed
# XXX allow nodes to have different collections
node_collection_items = list(self.node2collection.items())
first_node, col = node_collection_items[0]
for node, collection in node_collection_items[1:]:
report_collection_diff(
col,
collection,
first_node.gateway.id,
node.gateway.id,
)
# all collections are the same, good.
# we now create an index
self.collection = col
self.pending[:] = range(len(col))
if not col:
return
# how many items per node do we have about?
items_per_node = len(self.collection) // len(self.node2pending)
# take a fraction of tests for initial distribution
node_chunksize = max(items_per_node // 4, 1)
# and initialize each node with a chunk of tests
for node in self.node2pending:
self._send_tests(node, node_chunksize)
#f = open("/tmp/sent", "w")
def _send_tests(self, node, num):
tests_per_node = self.pending[:num]
#print >>self.f, "sent", node, tests_per_node
if tests_per_node:
del self.pending[:num]
self.node2pending[node].extend(tests_per_node)
node.send_runtest_some(tests_per_node)
def report_collection_diff(from_collection, to_collection, from_id, to_id):
"""Report the collected test difference between two nodes.
:returns: True if collections are equal.
:raises: AssertionError with a detailed error message describing the
difference between the collections.
"""
if from_collection == to_collection:
return True
diff = difflib.unified_diff(
from_collection,
to_collection,
fromfile=from_id,
tofile=to_id,
)
error_message = py.builtin._totext(
'Different tests were collected between {from_id} and {to_id}. '
'The difference is:\n'
'{diff}'
).format(from_id=from_id, to_id=to_id, diff='\n'.join(diff))
msg = "\n".join([x.rstrip() for x in error_message.split("\n")])
raise AssertionError(msg)
class Interrupted(KeyboardInterrupt):
""" signals an immediate interruption. """
class DSession:
def __init__(self, config):
self.config = config
self.log = py.log.Producer("dsession")
if not config.option.debug:
py.log.setconsumer(self.log._keywords, None)
self.shuttingdown = False
self.countfailures = 0
self.maxfail = config.getvalue("maxfail")
self.queue = queue.Queue()
self._failed_collection_errors = {}
try:
self.terminal = config.pluginmanager.getplugin("terminalreporter")
except KeyError:
self.terminal = None
self._nodesready = py.std.threading.Event()
else:
self.trdist = TerminalDistReporter(config)
config.pluginmanager.register(self.trdist, "terminaldistreporter")
def report_line(self, line):
if self.terminal:
if self.terminal and self.config.option.verbose >= 0:
self.terminal.write_line(line)
def pytest_gwmanage_rsyncstart(self, source, gateways):
targets = ",".join([gw.id for gw in gateways])
msg = "[%s] rsyncing: %s" %(targets, source)
self.report_line(msg)
@pytest.mark.trylast
def pytest_sessionstart(self, session):
self.nodemanager = NodeManager(self.config)
self.nodemanager.setup_nodes(putevent=self.queue.put)
#def pytest_gwmanage_rsyncfinish(self, source, gateways):
# targets = ", ".join(["[%s]" % gw.id for gw in gateways])
# self.write_line("rsyncfinish: %s -> %s" %(source, targets))
def pytest_sessionfinish(self, session):
""" teardown any resources after a test run. """
nm = getattr(self, 'nodemanager', None) # if not fully initialized
if nm is not None:
nm.teardown_nodes()
def main(self, colitems):
self.sessionstarts()
self.setup()
allitems = self.collect_all_items(colitems)
exitstatus = self.loop(allitems)
self.teardown()
self.sessionfinishes(exitstatus=exitstatus)
return exitstatus
def pytest_collection(self):
# prohibit collection of test items in master process
return True
def collect_all_items(self, colitems):
self.report_line("[master] starting full item collection ...")
allitems = list(self.collect(colitems))
self.report_line("[master] collected %d items" %(len(allitems)))
return allitems
def pytest_runtestloop(self):
numnodes = len(self.nodemanager.specs)
dist = self.config.getvalue("dist")
if dist == "load":
self.sched = LoadScheduling(numnodes, log=self.log)
elif dist == "each":
self.sched = EachScheduling(numnodes, log=self.log)
else:
assert 0, dist
self.shouldstop = False
self.session_finished = False
while not self.session_finished:
self.loop_once()
if self.shouldstop:
raise Interrupted(str(self.shouldstop))
return True
def loop_once(self, loopstate):
if loopstate.shuttingdown:
return self.loop_once_shutdown(loopstate)
colitems = loopstate.colitems
if self._nodesready.isSet() and loopstate.dowork and colitems:
self.triggertesting(loopstate.colitems)
colitems[:] = []
# we use a timeout here so that control-C gets through
def loop_once(self):
""" process one callback from one of the slaves. """
while 1:
try:
eventcall = self.queue.get(timeout=2.0)
break
except queue.Empty:
continue
loopstate.dowork = True
callname, args, kwargs = eventcall
if callname is not None:
call = getattr(self.config.hook, callname)
assert not args
call(**kwargs)
# termination conditions
if (not self.node2pending or
(loopstate.testsfailed and self.config.option.exitfirst) or
(not self.item2nodes and not colitems and not self.queue.qsize())):
if self.config.option.exitfirst:
raise ExitFirstInterrupt()
callname, kwargs = eventcall
assert callname, kwargs
method = "slave_" + callname
call = getattr(self, method)
self.log("calling method", method, kwargs)
call(**kwargs)
if self.sched.tests_finished():
self.triggershutdown()
loopstate.shuttingdown = True
if not self.node2pending:
loopstate.exitstatus = session.EXIT_NOHOSTS
def loop_once_shutdown(self, loopstate):
# once we are in shutdown mode we dont send
# events other than HostDown upstream
eventname, args, kwargs = self.queue.get()
if eventname == "pytest_testnodedown":
self.config.hook.pytest_testnodedown(**kwargs)
self.removenode(kwargs['node'])
elif eventname == "pytest_runtest_logreport":
# might be some teardown report
self.config.hook.pytest_runtest_logreport(**kwargs)
elif eventname == "pytest_internalerror":
self.config.hook.pytest_internalerror(**kwargs)
loopstate.exitstatus = session.EXIT_INTERNALERROR
elif eventname == "pytest__teardown_final_logerror":
self.config.hook.pytest__teardown_final_logerror(**kwargs)
loopstate.exitstatus = session.EXIT_TESTSFAILED
if not self.node2pending:
# finished
if loopstate.testsfailed:
loopstate.exitstatus = session.EXIT_TESTSFAILED
else:
loopstate.exitstatus = session.EXIT_OK
#self.config.pluginmanager.unregister(loopstate)
def _initloopstate(self, colitems):
loopstate = LoopState(self, colitems)
self.config.pluginmanager.register(loopstate)
return loopstate
#
# callbacks for processing events from slaves
#
def loop(self, colitems):
try:
loopstate = self._initloopstate(colitems)
loopstate.dowork = False # first receive at least one HostUp events
while 1:
self.loop_once(loopstate)
if loopstate.exitstatus is not None:
exitstatus = loopstate.exitstatus
break
except KeyboardInterrupt:
excinfo = py.code.ExceptionInfo()
if excinfo.errisinstance(ExitFirstInterrupt):
exitstatus = session.EXIT_TESTSFAILED
else:
self.config.hook.pytest_keyboard_interrupt(excinfo=excinfo)
exitstatus = session.EXIT_INTERRUPTED
except:
self.config.pluginmanager.notify_exception()
exitstatus = session.EXIT_INTERNALERROR
self.config.pluginmanager.unregister(loopstate)
if exitstatus == 0 and self._testsfailed:
exitstatus = session.EXIT_TESTSFAILED
return exitstatus
def triggershutdown(self):
for node in self.node2pending:
def slave_slaveready(self, node, slaveinfo):
node.slaveinfo = slaveinfo
node.slaveinfo['id'] = node.gateway.id
node.slaveinfo['spec'] = node.gateway.spec
self.config.hook.pytest_testnodeready(node=node)
self.sched.addnode(node)
if self.shuttingdown:
node.shutdown()
def addnode(self, node):
assert node not in self.node2pending
self.node2pending[node] = []
if (not hasattr(self, 'nodemanager') or
len(self.node2pending) == len(self.nodemanager.gwmanager.group)):
self._nodesready.set()
def slave_slavefinished(self, node):
self.config.hook.pytest_testnodedown(node=node, error=None)
if node.slaveoutput['exitstatus'] == 2: # keyboard-interrupt
self.shouldstop = "%s received keyboard-interrupt" % (node,)
self.slave_errordown(node, "keyboard-interrupt")
return
crashitem = self.sched.remove_node(node)
#assert not crashitem, (crashitem, node)
if self.shuttingdown and not self.sched.hasnodes():
self.session_finished = True
def removenode(self, node):
def slave_errordown(self, node, error):
self.config.hook.pytest_testnodedown(node=node, error=error)
try:
pending = self.node2pending.pop(node)
crashitem = self.sched.remove_node(node)
except KeyError:
# this happens if we didn't receive a testnodeready event yet
return []
for item in pending:
l = self.item2nodes[item]
l.remove(node)
if not l:
del self.item2nodes[item]
return pending
def triggertesting(self, colitems):
# for now we don't allow sending collectors
for next in colitems:
assert isinstance(next, py.test.collect.Item), next
senditems = list(colitems)
if self.config.option.dist == "each":
self.senditems_each(senditems)
pass
else:
# XXX assert self.config.option.dist == "load"
self.senditems_load(senditems)
if crashitem:
self.handle_crashitem(crashitem, node)
#self.report_line("item crashed on node: %s" % crashitem)
if not self.sched.hasnodes():
self.session_finished = True
def queueevent(self, eventname, **kwargs):
self.queue.put((eventname, (), kwargs))
def slave_collectionfinish(self, node, ids):
self.sched.addnode_collection(node, ids)
if self.terminal:
self.trdist.setstatus(node.gateway.spec, "[%d]" %(len(ids)))
def senditems_each(self, tosend):
if not tosend:
return
for node, pending in self.node2pending.items():
node.sendlist(tosend)
pending.extend(tosend)
for item in tosend:
nodes = self.item2nodes.setdefault(item, [])
assert node not in nodes
nodes.append(node)
item.ihook.pytest_itemstart(item=item, node=node)
tosend[:] = []
if self.sched.collection_is_completed:
if self.terminal:
self.trdist.ensure_show_status()
self.terminal.write_line("")
self.terminal.write_line("scheduling tests via %s" %(
self.sched.__class__.__name__))
def senditems_load(self, tosend):
if not tosend:
return
available = []
for node, pending in self.node2pending.items():
if len(pending) < self.LOAD_THRESHOLD_NEWITEMS:
available.append((node, pending))
num_available = len(available)
max_one_round = num_available * self.ITEM_CHUNKSIZE -1
if num_available:
for i, item in enumerate(tosend):
nodeindex = i % num_available
node, pending = available[nodeindex]
node.send(item)
self.item2nodes.setdefault(item, []).append(node)
item.ihook.pytest_itemstart(item=item, node=node)
pending.append(item)
if i >= max_one_round:
break
del tosend[:i+1]
if tosend:
# we have some left, give it to the main loop
self.queueevent("pytest_rescheduleitems", items=tosend)
self.sched.init_distribute()
def removeitem(self, item, node):
if item not in self.item2nodes:
raise AssertionError(item, self.item2nodes)
nodes = self.item2nodes[item]
if node in nodes: # the node might have gone down already
nodes.remove(node)
if not nodes:
del self.item2nodes[item]
pending = self.node2pending[node]
pending.remove(item)
def slave_logstart(self, node, nodeid, location):
self.config.hook.pytest_runtest_logstart(
nodeid=nodeid, location=location)
def handle_crashitem(self, item, node):
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="???")
def slave_testreport(self, node, rep):
if not (rep.passed and rep.when != "call"):
if rep.when in ("setup", "call"):
self.sched.remove_item(node, rep.item_index, rep.duration)
#self.report_line("testreport %s: %s" %(rep.id, rep.status))
rep.node = node
item.ihook.pytest_runtest_logreport(report=rep)
self.config.hook.pytest_runtest_logreport(report=rep)
self._handlefailures(rep)
def setup(self):
""" setup any neccessary resources ahead of the test run. """
self.nodemanager = NodeManager(self.config)
self.nodemanager.setup_nodes(putevent=self.queue.put)
def slave_collectreport(self, node, rep):
if rep.failed:
self._failed_slave_collectreport(node, rep)
def _failed_slave_collectreport(self, node, rep):
# Check we haven't already seen this report (from
# another slave).
if rep.longrepr not in self._failed_collection_errors:
self._failed_collection_errors[rep.longrepr] = True
self.config.hook.pytest_collectreport(report=rep)
self._handlefailures(rep)
def _handlefailures(self, rep):
if rep.failed:
self.countfailures += 1
if self.maxfail and self.countfailures >= self.maxfail:
self.shouldstop = "stopping after %d failures" % (
self.countfailures)
def triggershutdown(self):
self.log("triggering shutdown")
self.shuttingdown = True
for node in self.sched.node2pending:
node.shutdown()
def handle_crashitem(self, nodeid, slave):
# XXX get more reporting info by recording pytest_runtest_logstart?
runner = self.config.pluginmanager.getplugin("runner")
fspath = nodeid.split("::")[0]
msg = "Slave %r crashed while running %r" %(slave.gateway.id, nodeid)
rep = runner.TestReport(nodeid, (fspath, None, fspath), (),
"failed", msg, "???")
rep.node = slave
self.config.hook.pytest_runtest_logreport(report=rep)
class TerminalDistReporter:
def __init__(self, config):
self.config = config
self.tr = config.pluginmanager.getplugin("terminalreporter")
self._status = {}
self._lastlen = 0
def write_line(self, msg):
self.tr.write_line(msg)
def ensure_show_status(self):
if not self.tr.hasmarkup:
self.write_line(self.getstatus())
def setstatus(self, spec, status, show=True):
self._status[spec.id] = status
if show and self.tr.hasmarkup:
self.rewrite(self.getstatus())
def getstatus(self):
parts = ["%s %s" %(spec.id, self._status[spec.id])
for spec in self._specs]
return " / ".join(parts)
def rewrite(self, line, newline=False):
pline = line + " " * max(self._lastlen-len(line), 0)
if newline:
self._lastlen = 0
pline += "\n"
else:
self._lastlen = len(line)
self.tr.rewrite(pline, bold=True)
def pytest_xdist_setupnodes(self, specs):
self._specs = specs
for spec in specs:
self.setstatus(spec, "I", show=False)
self.setstatus(spec, "I", show=True)
self.ensure_show_status()
def pytest_xdist_newgateway(self, gateway):
if self.config.option.verbose > 0:
rinfo = gateway._rinfo()
version = "%s.%s.%s" % rinfo.version_info[:3]
self.rewrite("[%s] %s Python %s cwd: %s" % (
gateway.id, rinfo.platform, version, rinfo.cwd),
newline=True)
self.setstatus(gateway.spec, "C")
def pytest_testnodeready(self, node):
if self.config.option.verbose > 0:
d = node.slaveinfo
infoline = "[%s] Python %s" %(
d['id'],
d['version'].replace('\n', ' -- '),)
self.rewrite(infoline, newline=True)
self.setstatus(node.gateway.spec, "ok")
def pytest_testnodedown(self, node, error):
if not error:
return
self.write_line("[%s] node down: %s" %(node.gateway.id, error))
#def pytest_xdist_rsyncstart(self, source, gateways):
# targets = ",".join([gw.id for gw in gateways])
# msg = "[%s] rsyncing: %s" %(targets, source)
# self.write_line(msg)
#def pytest_xdist_rsyncfinish(self, source, gateways):
# targets = ", ".join(["[%s]" % gw.id for gw in gateways])
# self.write_line("rsyncfinish: %s -> %s" %(source, targets))
def teardown(self):
""" teardown any resources after a test run. """
self.nodemanager.teardown_nodes()

View File

@@ -1,100 +0,0 @@
"""
instantiating, managing and rsyncing to test hosts
"""
import py
import sys, os.path
import execnet
from execnet.gateway_base import RemoteError
class GatewayManager:
EXIT_TIMEOUT = 10
RemoteError = RemoteError
def __init__(self, specs, hook, defaultchdir="pyexecnetcache"):
self.specs = []
self.hook = hook
self.group = execnet.Group()
for spec in specs:
if not isinstance(spec, execnet.XSpec):
spec = execnet.XSpec(spec)
if not spec.chdir and not spec.popen:
spec.chdir = defaultchdir
self.specs.append(spec)
def makegateways(self):
assert not list(self.group)
for spec in self.specs:
gw = self.group.makegateway(spec)
self.hook.pytest_gwmanage_newgateway(
gateway=gw, platinfo=gw._rinfo())
def rsync(self, source, notify=None, verbose=False, ignores=None):
""" perform rsync to all remote hosts.
"""
rsync = HostRSync(source, verbose=verbose, ignores=ignores)
seen = py.builtin.set()
gateways = []
for gateway in self.group:
spec = gateway.spec
if spec.popen and not spec.chdir:
# XXX this assumes that sources are python-packages
# and that adding the basedir does not hurt
gateway.remote_exec("""
import sys ; sys.path.insert(0, %r)
""" % os.path.dirname(str(source))).waitclose()
continue
if spec not in seen:
def finished():
if notify:
notify("rsyncrootready", spec, source)
rsync.add_target_host(gateway, finished=finished)
seen.add(spec)
gateways.append(gateway)
if seen:
self.hook.pytest_gwmanage_rsyncstart(
source=source,
gateways=gateways,
)
rsync.send()
self.hook.pytest_gwmanage_rsyncfinish(
source=source,
gateways=gateways,
)
def exit(self):
self.group.terminate(self.EXIT_TIMEOUT)
class HostRSync(execnet.RSync):
""" RSyncer that filters out common files
"""
def __init__(self, sourcedir, *args, **kwargs):
self._synced = {}
ignores= None
if 'ignores' in kwargs:
ignores = kwargs.pop('ignores')
self._ignores = ignores or []
super(HostRSync, self).__init__(sourcedir=sourcedir, **kwargs)
def filter(self, path):
path = py.path.local(path)
if not path.ext in ('.pyc', '.pyo'):
if not path.basename.endswith('~'):
if path.check(dotfile=0):
for x in self._ignores:
if path == x:
break
else:
return True
def add_target_host(self, gateway, finished=None):
remotepath = os.path.basename(self._sourcedir)
super(HostRSync, self).add_target(gateway, remotepath,
finishedcallback=finished,
delete=True,)
def _report_send_file(self, gateway, modified_rel_path):
if self._verbose:
path = os.path.basename(self._sourcedir) + "/" + modified_rel_path
remotepath = gateway.spec.chdir
py.builtin.print_('%s:%s <= %s' %
(gateway.spec, remotepath, path))

231
xdist/looponfail.py Normal file
View File

@@ -0,0 +1,231 @@
"""
Implement -f aka looponfailing for py.test.
NOTE that we try to avoid loading and depending on application modules
within the controlling process (the one that starts repeatedly test
processes) otherwise changes to source code can crash
the controlling process which should best never happen.
"""
import py, pytest
import sys
import execnet
def looponfail_main(config):
remotecontrol = RemoteControl(config)
rootdirs = config.getini("looponfailroots")
statrecorder = StatRecorder(rootdirs)
try:
while 1:
remotecontrol.loop_once()
if not remotecontrol.failures and remotecontrol.wasfailing:
continue # the last failures passed, let's immediately rerun all
repr_pytest_looponfailinfo(
failreports=remotecontrol.failures,
rootdirs=rootdirs)
statrecorder.waitonchange(checkinterval=2.0)
except KeyboardInterrupt:
print()
class RemoteControl(object):
def __init__(self, config):
self.config = config
self.failures = []
def trace(self, *args):
if self.config.option.debug:
msg = " ".join([str(x) for x in args])
py.builtin.print_("RemoteControl:", msg)
def initgateway(self):
return execnet.makegateway("popen")
def setup(self, out=None):
if out is None:
out = py.io.TerminalWriter()
if hasattr(self, 'gateway'):
raise ValueError("already have gateway %r" % self.gateway)
self.trace("setting up slave session")
self.gateway = self.initgateway()
self.channel = channel = self.gateway.remote_exec(init_slave_session,
args=self.config.args,
option_dict=vars(self.config.option),
)
remote_outchannel = channel.receive()
def write(s):
out._file.write(s)
out._file.flush()
remote_outchannel.setcallback(write)
def ensure_teardown(self):
if hasattr(self, 'channel'):
if not self.channel.isclosed():
self.trace("closing", self.channel)
self.channel.close()
del self.channel
if hasattr(self, 'gateway'):
self.trace("exiting", self.gateway)
self.gateway.exit()
del self.gateway
def runsession(self):
try:
self.trace("sending", self.failures)
self.channel.send(self.failures)
try:
return self.channel.receive()
except self.channel.RemoteError:
e = sys.exc_info()[1]
self.trace("ERROR", e)
raise
finally:
self.ensure_teardown()
def loop_once(self):
self.setup()
self.wasfailing = self.failures and len(self.failures)
result = self.runsession()
failures, reports, collection_failed = result
if collection_failed:
reports = ["Collection failed, keeping previous failure set"]
else:
uniq_failures = []
for failure in failures:
if failure not in uniq_failures:
uniq_failures.append(failure)
self.failures = uniq_failures
def repr_pytest_looponfailinfo(failreports, rootdirs):
tr = py.io.TerminalWriter()
if failreports:
tr.sep("#", "LOOPONFAILING", bold=True)
for report in failreports:
if report:
tr.line(report, red=True)
tr.sep("#", "waiting for changes", bold=True)
for rootdir in rootdirs:
tr.line("### Watching: %s" %(rootdir,), bold=True)
def init_slave_session(channel, args, option_dict):
import os, sys
import py
outchannel = channel.gateway.newchannel()
sys.stdout = sys.stderr = outchannel.makefile('w')
channel.send(outchannel)
# prune sys.path to not contain relative paths
newpaths = []
for p in sys.path:
if p:
if not os.path.isabs(p):
p = os.path.abspath(p)
newpaths.append(p)
sys.path[:] = newpaths
#fullwidth, hasmarkup = channel.receive()
from _pytest.config import Config
config = Config.fromdictargs(option_dict, list(args))
config.args = args
from xdist.looponfail import SlaveFailSession
SlaveFailSession(config, channel).main()
class SlaveFailSession:
def __init__(self, config, channel):
self.config = config
self.channel = channel
self.recorded_failures = []
self.collection_failed = False
config.pluginmanager.register(self)
config.option.looponfail = False
config.option.usepdb = False
def DEBUG(self, *args):
if self.config.option.debug:
print(" ".join(map(str, args)))
def pytest_collection(self, session):
self.session = session
self.trails = self.current_command
hook = self.session.ihook
try:
items = session.perform_collect(self.trails or None)
except pytest.UsageError:
items = session.perform_collect(None)
hook.pytest_collection_modifyitems(session=session, config=session.config, items=items)
hook.pytest_collection_finish(session=session)
return True
def pytest_runtest_logreport(self, report):
if report.failed:
self.recorded_failures.append(report)
def pytest_collectreport(self, report):
if report.failed:
self.recorded_failures.append(report)
self.collection_failed = True
def main(self):
self.DEBUG("SLAVE: received configuration, waiting for command trails")
try:
command = self.channel.receive()
except KeyboardInterrupt:
return # in the slave we can't do much about this
self.DEBUG("received", command)
self.current_command = command
self.config.hook.pytest_cmdline_main(config=self.config)
trails, failreports = [], []
for rep in self.recorded_failures:
trails.append(rep.nodeid)
loc = rep.longrepr
loc = str(getattr(loc, 'reprcrash', loc))
failreports.append(loc)
self.channel.send((trails, failreports, self.collection_failed))
class StatRecorder:
def __init__(self, rootdirlist):
self.rootdirlist = rootdirlist
self.statcache = {}
self.check() # snapshot state
def fil(self, p):
return p.check(file=1, dotfile=0) and p.ext != ".pyc"
def rec(self, p):
return p.check(dotfile=0)
def waitonchange(self, checkinterval=1.0):
while 1:
changed = self.check()
if changed:
return
py.std.time.sleep(checkinterval)
def check(self, removepycfiles=True):
changed = False
statcache = self.statcache
newstat = {}
for rootdir in self.rootdirlist:
for path in rootdir.visit(self.fil, self.rec):
oldstat = statcache.pop(path, None)
try:
newstat[path] = curstat = path.stat()
except py.error.ENOENT:
if oldstat:
changed = True
else:
if oldstat:
if oldstat.mtime != curstat.mtime or \
oldstat.size != curstat.size:
changed = True
py.builtin.print_("# MODIFIED", path)
if removepycfiles and path.ext == ".py":
pycfile = path + "c"
if pycfile.check():
pycfile.remove()
else:
changed = True
if statcache:
changed = True
self.statcache = newstat
return changed

View File

@@ -1,183 +0,0 @@
"""
Pickling support for two processes that want to exchange
*immutable* object instances. Immutable in the sense
that the receiving side of an object can modify its
copy but when it sends it back the original sending
side will continue to see its unmodified version
(and no actual state will go over the wire).
This module also implements an experimental
execnet pickling channel using this idea.
"""
import py
import sys, os, struct
#debug = open("log-mypickle-%d" % os.getpid(), 'w')
if sys.version_info >= (3,0):
makekey = lambda x: x
fromkey = lambda x: x
from pickle import _Pickler as Pickler
from pickle import _Unpickler as Unpickler
else:
makekey = str
fromkey = int
from pickle import Pickler, Unpickler
class MyPickler(Pickler):
""" Pickler with a custom memoize()
to take care of unique ID creation.
See the usage in ImmutablePickler
"""
def __init__(self, immo, file, protocol, uneven):
Pickler.__init__(self, file, protocol)
self.uneven = uneven
self._unpicklememo = immo._unpicklememo
self.memo = immo._picklememo
def memoize(self, obj):
if self.fast:
return
assert id(obj) not in self.memo
memo_len = len(self.memo)
key = memo_len * 2 + self.uneven
self.write(self.put(key))
self.memo[id(obj)] = key, obj
key = makekey(key)
if key in self._unpicklememo:
assert self._unpicklememo[key] is obj
dict.__setitem__(self._unpicklememo, key, obj)
#if sys.version_info < (3,0):
# def save_string(self, obj, pack=struct.pack):
# obj = unicode(obj)
# self.save_unicode(obj, pack=pack)
# Pickler.dispatch[str] = save_string
class UnpicklingDict(dict):
def __init__(self, picklememo):
super(UnpicklingDict, self).__init__()
self._picklememo = picklememo
def __setitem__(self, key, obj):
super(UnpicklingDict, self).__setitem__(key, obj)
self._picklememo[id(obj)] = (fromkey(key), obj)
class ImmutablePickler:
def __init__(self, uneven, protocol=0):
""" ImmutablePicklers are instantiated in Pairs.
The two sides need to create unique IDs
while pickling their objects. This is
done by using either even or uneven
numbers, depending on the instantiation
parameter.
"""
self._picklememo = {}
self._unpicklememo = UnpicklingDict(self._picklememo)
self._protocol = protocol
self.uneven = uneven and 1 or 0
def selfmemoize(self, obj):
# this is for feeding objects to ourselfes
# which be the case e.g. if you want to pickle
# from a forked process back to the original
f = py.io.BytesIO()
pickler = MyPickler(self, f, self._protocol, uneven=self.uneven)
pickler.memoize(obj)
def dumps(self, obj):
f = py.io.BytesIO()
pickler = MyPickler(self, f, self._protocol, uneven=self.uneven)
pickler.dump(obj)
#print >>debug, "dumped", obj
#print >>debug, "picklememo", self._picklememo
return f.getvalue()
def loads(self, string):
f = py.io.BytesIO(string)
unpickler = Unpickler(f)
unpickler.memo = self._unpicklememo
res = unpickler.load()
#print >>debug, "loaded", res
#print >>debug, "unpicklememo", self._unpicklememo
return res
NO_ENDMARKER_WANTED = object()
class UnpickleError(Exception):
""" Problems while unpickling. """
def __init__(self, formatted):
self.formatted = formatted
Exception.__init__(self, formatted)
def __str__(self):
return self.formatted
class PickleChannel(object):
""" PickleChannels wrap execnet channels
and allow to send/receive by using
"immutable pickling".
"""
_unpicklingerror = None
def __init__(self, channel):
self._channel = channel
# we use the fact that each side of a
# gateway connection counts with uneven
# or even numbers depending on which
# side it is (for the purpose of creating
# unique ids - which is what we need it here for)
uneven = channel.gateway._channelfactory.count % 2
self._ipickle = ImmutablePickler(uneven=uneven)
self.RemoteError = channel.RemoteError
def send(self, obj):
pickled_obj = self._ipickle.dumps(obj)
self._channel.send(pickled_obj)
def receive(self):
pickled_obj = self._channel.receive()
return self._unpickle(pickled_obj)
def _unpickle(self, pickled_obj):
if isinstance(pickled_obj, self._channel.__class__):
return pickled_obj
return self._ipickle.loads(pickled_obj)
def _getremoteerror(self):
return self._unpicklingerror or self._channel._getremoteerror()
def close(self):
return self._channel.close()
def isclosed(self):
return self._channel.isclosed()
def waitclose(self, timeout=None):
return self._channel.waitclose(timeout=timeout)
def setcallback(self, callback, endmarker=NO_ENDMARKER_WANTED):
if endmarker is NO_ENDMARKER_WANTED:
def unpickle_callback(pickled_obj):
obj = self._unpickle(pickled_obj)
callback(obj)
self._channel.setcallback(unpickle_callback)
return
uniqueendmarker = object()
def unpickle_callback(pickled_obj):
if pickled_obj is uniqueendmarker:
return callback(endmarker)
try:
obj = self._unpickle(pickled_obj)
except KeyboardInterrupt:
raise
except:
excinfo = py.code.ExceptionInfo()
formatted = str(excinfo.getrepr(showlocals=True,funcargs=True))
self._unpicklingerror = UnpickleError(formatted)
callback(endmarker)
else:
callback(obj)
self._channel.setcallback(unpickle_callback, uniqueendmarker)

View File

@@ -1,11 +1,14 @@
def pytest_gwmanage_newgateway(gateway, platinfo):
""" called on new raw gateway creation. """
def pytest_xdist_setupnodes(config, specs):
""" called before any remote node is set up. """
def pytest_gwmanage_rsyncstart(source, gateways):
def pytest_xdist_newgateway(gateway):
""" called on new raw gateway creation. """
def pytest_xdist_rsyncstart(source, gateways):
""" called before rsyncing a directory to remote gateways takes place. """
def pytest_gwmanage_rsyncfinish(source, gateways):
def pytest_xdist_rsyncfinish(source, gateways):
""" called after rsyncing a directory to remote gateways takes place. """
def pytest_configure_node(node):
@@ -16,10 +19,3 @@ def pytest_testnodeready(node):
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. """

View File

@@ -1,99 +0,0 @@
import py
import sys, os
import xdist
from xdist.txnode import TXNode
from xdist.gwmanage import GatewayManager
import execnet
class NodeManager(object):
def __init__(self, config, specs=None):
self.config = config
if specs is None:
specs = self._getxspecs()
self.roots = self._getrsyncdirs()
self.gwmanager = GatewayManager(specs, config.hook)
self._nodesready = py.std.threading.Event()
def trace(self, msg):
self.config.hook.pytest_trace(category="nodemanage", msg=msg)
def config_getignores(self):
return self.config.getconftest_pathlist("rsyncignore")
def rsync_roots(self):
""" make sure that all remote gateways
have the same set of roots in their
current directory.
"""
self.makegateways()
options = {
'ignores': self.config_getignores(),
'verbose': self.config.option.verbose,
}
if self.roots:
# send each rsync root
for root in self.roots:
self.gwmanager.rsync(root, **options)
else:
XXX # do we want to care for situations without explicit rsyncdirs?
# we transfer our topdir as the root
self.gwmanager.rsync(self.config.topdir, **options)
# and cd into it
self.gwmanager.multi_chdir(self.config.topdir.basename, inplacelocal=False)
def makegateways(self):
# we change to the topdir sot that
# PopenGateways will have their cwd
# such that unpickling configs will
# pick it up as the right topdir
# (for other gateways this chdir is irrelevant)
self.trace("making gateways")
old = self.config.topdir.chdir()
try:
self.gwmanager.makegateways()
finally:
old.chdir()
def setup_nodes(self, putevent):
self.rsync_roots()
self.trace("setting up nodes")
for gateway in self.gwmanager.group:
node = TXNode(self, gateway, self.config, putevent)
gateway.node = node # to keep node alive
self.trace("started node %r" % node)
def teardown_nodes(self):
self.gwmanager.exit()
def _getxspecs(self):
config = self.config
xspeclist = []
for xspec in config.getvalue("tx"):
i = xspec.find("*")
try:
num = int(xspec[:i])
except ValueError:
xspeclist.append(xspec)
else:
xspeclist.extend([xspec[i+1:]] * num)
if not xspeclist:
raise config.Error(
"MISSING test execution (tx) nodes: please specify --tx")
return [execnet.XSpec(x) for x in xspeclist]
def _getrsyncdirs(self):
config = self.config
candidates = [py._pydir]
candidates += [py.path.local(xdist.__file__).dirpath()]
candidates += config.option.rsyncdir
conftestroots = config.getconftest_pathlist("rsyncdirs")
if conftestroots:
candidates.extend(conftestroots)
roots = []
for root in candidates:
root = py.path.local(root).realpath()
if not root.check():
raise config.Error("rsyncdir doesn't exist: %r" %(root,))
if root not in roots:
roots.append(root)
return roots

View File

@@ -1,177 +1,44 @@
"""loop on failing tests, distribute test runs to CPUs and hosts.
The `pytest-xdist`_ plugin extends py.test with some unique
test execution modes:
* Looponfail: run your tests repeatedly in a subprocess. After each run py.test
waits until a file in your project changes and then re-runs the previously
failing tests. This is repeated until all tests pass after which again
a full run is performed.
* Load-balancing: if you have multiple CPUs or hosts you can use
those for a combined test run. This allows to speed up
development or to use special resources of remote machines.
* Multi-Platform coverage: you can specify different Python interpreters
or different platforms and run tests in parallel on all of them.
Before running tests remotely, ``py.test`` efficiently synchronizes your
program source code to the remote place. All test results
are reported back and displayed to your local test session.
You may specify different Python versions and interpreters.
.. _`pytest-xdist`: http://pypi.python.org/pypi/pytest-xdist
Usage examples
---------------------
Speed up test runs by sending tests to multiple CPUs
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
To send tests to multiple CPUs, type::
py.test -n NUM
Especially for longer running tests or tests requiring
a lot of IO this can lead to considerable speed ups.
Running tests in a Python subprocess
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
To instantiate a python2.4 sub process and send tests to it, you may type::
py.test -d --tx popen//python=python2.4
This will start a subprocess which is run with the "python2.4"
Python interpreter, found in your system binary lookup path.
If you prefix the --tx option value like this::
--tx 3*popen//python=python2.4
then three subprocesses would be created and tests
will be load-balanced across these three processes.
Sending tests to remote SSH accounts
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Suppose you have a package ``mypkg`` which contains some
tests that you can successfully run locally. And you
have a ssh-reachable machine ``myhost``. Then
you can ad-hoc distribute your tests by typing::
py.test -d --tx ssh=myhostpopen --rsyncdir mypkg mypkg
This will synchronize your ``mypkg`` package directory
to an remote ssh account and then locally collect tests
and send them to remote places for execution.
You can specify multiple ``--rsyncdir`` directories
to be sent to the remote side.
**NOTE:** For py.test to collect and send tests correctly
you not only need to make sure all code and tests
directories are rsynced, but that any test (sub) directory
also has an ``__init__.py`` file because internally
py.test references tests as a fully qualified python
module path. **You will otherwise get strange errors**
during setup of the remote side.
Sending tests to remote Socket Servers
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Download the single-module `socketserver.py`_ Python program
and run it like this::
python socketserver.py
It will tell you that it starts listening on the default
port. You can now on your home machine specify this
new socket host with something like this::
py.test -d --tx socket=192.168.1.102:8888 --rsyncdir mypkg mypkg
.. _`atonce`:
Running tests on many platforms at once
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
The basic command to run tests on multiple platforms is::
py.test --dist=each --tx=spec1 --tx=spec2
If you specify a windows host, an OSX host and a Linux
environment this command will send each tests to all
platforms - and report back failures from all platforms
at once. The specifications strings use the `xspec syntax`_.
.. _`xspec syntax`: http://codespeak.net/execnet/trunk/basics.html#xspec
.. _`socketserver.py`: http://codespeak.net/svn/py/dist/py/execnet/script/socketserver.py
.. _`execnet`: http://codespeak.net/execnet
Specifying test exec environments in a conftest.py
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Instead of specifying command line options, you can
put options values in a ``conftest.py`` file like this::
pytest_option_tx = ['ssh=myhost//python=python2.5', 'popen//python=python2.5']
pytest_option_dist = True
Any commandline ``--tx`` specifictions will add to the list of available execution
environments.
Specifying "rsync" dirs in a conftest.py
+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
In your ``mypkg/conftest.py`` you may specify directories to synchronise
or to exclude::
rsyncdirs = ['.', '../plugins']
rsyncignore = ['_cache']
These directory specifications are relative to the directory
where the ``conftest.py`` is found.
"""
import sys
import py
import pytest
def pytest_addoption(parser):
group = parser.getgroup("xdist", "distributed and subprocess testing")
group = parser.getgroup("xdist", "distributed and subprocess testing")
group._addoption('-f', '--looponfail',
action="store_true", dest="looponfail", default=False,
help="run tests in subprocess, wait for modified files "
"and re-run failing test set until all pass.")
group._addoption('-n', dest="numprocesses", metavar="numprocesses",
action="store", type="int",
group._addoption('-n', dest="numprocesses", metavar="numprocesses",
action="store", type="int",
help="shortcut for '--dist=load --tx=NUM*popen'")
group.addoption('--boxed',
action="store_true", dest="boxed", default=False,
help="box each test run in a separate process (unix)")
group._addoption('--dist', metavar="distmode",
action="store", choices=['load', 'each', 'no'],
type="choice", dest="dist", default="no",
help="box each test run in a separate process (unix)")
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=[],
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@codespeak.net//chdir=testcache"))
group._addoption('-d',
group._addoption('-d',
action="store_true", dest="distload", default=False,
help="load-balance tests. shortcut for '--dist=load'")
group.addoption('--rsyncdir', action="append", default=[], metavar="dir1",
group.addoption('--rsyncdir', action="append", default=[], metavar="DIR",
help="add directory for rsyncing to remote tx nodes.")
group.addoption('--rsyncignore', action="append", default=[], metavar="GLOB",
help="add expression for ignores when rsyncing to remote tx nodes.")
parser.addini('rsyncdirs', 'list of (relative) paths to be rsynced for'
' remote distributed testing.', type="pathlist")
parser.addini('rsyncignore', 'list of (relative) glob-style paths to be ignored '
'for rsyncing.', type="pathlist")
parser.addini("looponfailroots", type="pathlist",
help="directories to check for changes", default=[py.path.local()])
# -------------------------------------------------------------------------
# distributed testing hooks
@@ -183,7 +50,24 @@ def pytest_addhooks(pluginmanager):
# -------------------------------------------------------------------------
# distributed testing initialization
# -------------------------------------------------------------------------
def pytest_configure(config):
def pytest_cmdline_main(config):
check_options(config)
if config.getvalue("looponfail"):
from xdist.looponfail import looponfail_main
looponfail_main(config)
return 2 # looponfail only can get stop with ctrl-C anyway
def pytest_configure(config, __multicall__):
__multicall__.execute()
if config.getvalue("dist") != "no":
from xdist.dsession import DSession
session = DSession(config)
config.pluginmanager.register(session, "dsession")
tr = config.pluginmanager.getplugin("terminalreporter")
tr.showfspath = False
def check_options(config):
if config.option.numprocesses:
config.option.dist = "load"
config.option.tx = ['popen'] * int(config.option.numprocesses)
@@ -194,14 +78,11 @@ def pytest_configure(config):
usepdb = config.option.usepdb # a core option
if val("looponfail"):
if usepdb:
raise config.Error("--pdb incompatible with --looponfail.")
from xdist.remote import LooponfailingSession
config.setsessionclass(LooponfailingSession)
raise pytest.UsageError("--pdb incompatible with --looponfail.")
elif val("dist") != "no":
if usepdb:
raise config.Error("--pdb incompatible with distributing tests.")
from xdist.dsession import DSession
config.setsessionclass(DSession)
raise pytest.UsageError("--pdb incompatible with distributing tests.")
def pytest_runtest_protocol(item):
if item.config.getvalue("boxed"):
@@ -211,27 +92,25 @@ def pytest_runtest_protocol(item):
return True
def forked_run_report(item):
# for now, we run setup/teardown in the subprocess
# XXX optionally allow sharing of setup/teardown
from py._plugin.pytest_runner import runtestprotocol
# for now, we run setup/teardown in the subprocess
# XXX optionally allow sharing of setup/teardown
from _pytest.runner import runtestprotocol
EXITSTATUS_TESTEXIT = 4
from xdist.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)
import marshal
from xdist.remote import serialize_report
from xdist.slavemanage import unserialize_report
def runforked():
try:
reports = runtestprotocol(item, log=False)
except KeyboardInterrupt:
except KeyboardInterrupt:
py.std.os._exit(EXITSTATUS_TESTEXIT)
return ipickle.dumps(reports)
return marshal.dumps([serialize_report(x) for x in reports])
ff = py.process.ForkedFunc(runforked)
result = ff.waitfinish()
if result.retval is not None:
return ipickle.loads(result.retval)
report_dumps = marshal.loads(result.retval)
return [unserialize_report("testreport", x) for x in report_dumps]
else:
if result.exitstatus == EXITSTATUS_TESTEXIT:
py.test.exit("forked test item %s raised Exit" %(item,))
@@ -241,6 +120,8 @@ def report_process_crash(item, result):
path, lineno = item._getfslineno()
info = "%s:%s: running the test CRASHED with signal %d" %(
path, lineno, result.signal)
from py._plugin.pytest_runner import ItemTestReport
return ItemTestReport(item, excinfo=info, when="???")
from _pytest import runner
call = runner.CallInfo(lambda: 0/0, "???")
call.excinfo = info
rep = runner.pytest_runtest_makereport(item, call)
return rep

View File

@@ -1,177 +1,143 @@
"""
LooponfailingSession and Helpers.
NOTE that one really has to avoid loading and depending on
application modules within the controlling process
(the one that starts repeatedly test processes)
otherwise changes to source code can crash
the controlling process which should never happen.
This module is executed in remote subprocesses and helps to
control a remote testing session and relay back information.
It assumes that 'py' is importable and does not have dependencies
on the rest of the xdist code. This means that the xdist-plugin
needs not to be installed in remote environments.
"""
import py
import sys
import execnet
from py._test.session import Session
from xdist import util
class LooponfailingSession(Session):
def __init__(self, config):
super(LooponfailingSession, self).__init__(config=config)
self.rootdirs = [self.config.topdir] # xxx dist_rsync_roots?
self.statrecorder = util.StatRecorder(self.rootdirs)
self.remotecontrol = RemoteControl(self.config)
self.out = py.io.TerminalWriter()
import sys, os
def main(self, initialitems):
try:
self.loopstate = loopstate = LoopState([])
self.remotecontrol.setup()
while 1:
self.loop_once(loopstate)
if not loopstate.colitems and loopstate.wasfailing:
continue # the last failures passed, let's rerun all
self.statrecorder.waitonchange(checkinterval=2.0)
except KeyboardInterrupt:
print
def loop_once(self, loopstate):
colitems = loopstate.colitems
loopstate.wasfailing = colitems and len(colitems)
loopstate.colitems = self.remotecontrol.runsession(colitems or ())
self.remotecontrol.setup()
class LoopState:
def __init__(self, colitems=None):
self.colitems = colitems
class RemoteControl(object):
def __init__(self, config):
class SlaveInteractor:
def __init__(self, config, channel):
self.config = config
self.slaveid = config.slaveinput.get('slaveid', "?")
self.log = py.log.Producer("slave-%s" % self.slaveid)
if not config.option.debug:
py.log.setconsumer(self.log._keywords, None)
self.channel = channel
config.pluginmanager.register(self)
def trace(self, *args):
if self.config.option.debug:
msg = " ".join([str(x) for x in args])
py.builtin.print_("RemoteControl:", msg)
def sendevent(self, name, **kwargs):
self.log("sending", name, kwargs)
self.channel.send((name, kwargs))
def initgateway(self):
return execnet.makegateway("popen")
def pytest_internalerror(self, excrepr):
for line in str(excrepr).split("\n"):
self.log("IERROR>", line)
def setup(self, out=None):
if out is None:
out = py.io.TerminalWriter()
if hasattr(self, 'gateway'):
raise ValueError("already have gateway %r" % self.gateway)
self.trace("setting up slave session")
self.gateway = self.initgateway()
self.channel = channel = self.gateway.remote_exec("""
import os, sys
import py
chdir = channel.receive()
outchannel = channel.gateway.newchannel()
channel.send(outchannel)
# prune sys.path to not contain relative paths
newpaths = []
for p in sys.path:
if p:
if not os.path.isabs(p):
p = os.path.abspath(p)
newpaths.append(p)
sys.path[:] = newpaths
os.chdir(chdir) # unpickling config uses cwd as topdir
config_state = channel.receive()
fullwidth, hasmarkup = channel.receive()
py.test.config.__setstate__(config_state)
def pytest_sessionstart(self, session):
self.session = session
slaveinfo = getinfodict()
self.sendevent("slaveready", slaveinfo=slaveinfo)
import sys
sys.stdout = sys.stderr = outchannel.makefile('w')
def pytest_sessionfinish(self, __multicall__, exitstatus):
self.config.slaveoutput['exitstatus'] = exitstatus
res = __multicall__.execute()
self.sendevent("slavefinished", slaveoutput=self.config.slaveoutput)
return res
from xdist.remote import slave_runsession
slave_runsession(channel, py.test.config, fullwidth, hasmarkup)
""")
channel.send(str(self.config.topdir))
remote_outchannel = channel.receive()
def write(s):
out._file.write(s)
out._file.flush()
remote_outchannel.setcallback(write)
channel.send(self.config.__getstate__())
channel.send((out.fullwidth, out.hasmarkup))
self.trace("set up of slave session complete")
def pytest_collection(self, session):
self.sendevent("collectionstart")
def ensure_teardown(self):
if hasattr(self, 'channel'):
if not self.channel.isclosed():
self.trace("closing", self.channel)
self.channel.close()
del self.channel
if hasattr(self, 'gateway'):
self.trace("exiting", self.gateway)
self.gateway.exit()
del self.gateway
def pytest_runtestloop(self, session):
self.log("entering main loop")
torun = []
while 1:
name, kwargs = self.channel.receive()
self.log("received command", name, kwargs)
if name == "runtests":
torun.extend(kwargs['indices'])
elif name == "runtests_all":
torun.extend(range(len(session.items)))
self.log("items to run:", torun)
while torun:
self.run_tests(torun)
if name == "shutdown":
break
return True
def runsession(self, colitems=()):
try:
self.trace("sending", colitems)
trails = colitems
self.channel.send(trails)
try:
return self.channel.receive()
except self.channel.RemoteError:
e = sys.exc_info()[1]
self.trace("ERROR", e)
raise
finally:
self.ensure_teardown()
def run_tests(self, torun):
items = self.session.items
self.item_index = torun.pop(0)
if torun:
nextitem = items[torun[0]]
else:
nextitem = None
self.config.hook.pytest_runtest_protocol(
item=items[self.item_index],
nextitem=nextitem)
def slave_runsession(channel, config, fullwidth, hasmarkup):
""" we run this on the other side. """
if config.option.debug:
def DEBUG(*args):
print(" ".join(map(str, args)))
def pytest_collection_finish(self, session):
self.sendevent("collectionfinish",
topdir=str(session.fspath),
ids=[item.nodeid for item in session.items])
def pytest_runtest_logstart(self, nodeid, location):
self.sendevent("logstart", nodeid=nodeid, location=location)
def pytest_runtest_logreport(self, report):
data = serialize_report(report)
data["item_index"] = self.item_index
assert self.session.items[self.item_index].nodeid == report.nodeid
self.sendevent("testreport", data=data)
def pytest_collectreport(self, report):
data = serialize_report(report)
self.sendevent("collectreport", data=data)
def serialize_report(rep):
import py
d = rep.__dict__.copy()
if hasattr(rep.longrepr, 'toterminal'):
d['longrepr'] = str(rep.longrepr)
else:
def DEBUG(*args): pass
d['longrepr'] = rep.longrepr
for name in d:
if isinstance(d[name], py.path.local):
d[name] = str(d[name])
elif name == "result":
d[name] = None # for now
return d
DEBUG("SLAVE: received configuration, using topdir:", config.topdir)
#config.option.session = None
config.option.looponfail = False
config.option.usepdb = False
try:
trails = channel.receive()
except KeyboardInterrupt:
return # in the slave we can't do much about this
config.pluginmanager.do_configure(config)
DEBUG("SLAVE: initsession()")
session = config.initsession()
# XXX configure the reporter object's terminal writer more directly
# XXX and write a test for this remote-terminal setting logic
config.pytest_terminal_hasmarkup = hasmarkup
config.pytest_terminal_fullwidth = fullwidth
if trails:
colitems = []
for trail in trails:
try:
colitem = config._rootcol.fromtrail(trail)
except ValueError:
#XXX send info for "test disappeared" or so
continue
colitems.append(colitem)
else:
colitems = config.getinitialnodes()
session.shouldclose = channel.isclosed
class Failures(list):
def pytest_runtest_logreport(self, report):
if report.failed:
self.append(report)
pytest_collectreport = pytest_runtest_logreport
failreports = Failures()
session.pluginmanager.register(failreports)
def getinfodict():
import platform
return dict(
version = sys.version,
version_info = tuple(sys.version_info),
sysplatform = sys.platform,
platform = platform.platform(),
executable = sys.executable,
cwd = os.getcwd(),
)
DEBUG("SLAVE: starting session.main()")
session.main(colitems)
session.config.hook.pytest_looponfailinfo(
failreports=list(failreports),
rootdirs=[config.topdir])
rootcol = session.config._rootcol
channel.send([rootcol.totrail(rep.getnode()) for rep in failreports])
def remote_initconfig(option_dict, args):
from _pytest.config import Config
option_dict['plugins'].append("no:terminal")
config = Config.fromdictargs(option_dict, args)
config.option.looponfail = False
config.option.usepdb = False
config.option.dist = "no"
config.option.distload = False
config.option.numprocesses = None
config.args = args
return config
if __name__ == '__channelexec__':
# python3.2 is not concurrent import safe, so let's play it safe
# https://bitbucket.org/hpk42/pytest/issue/347/pytest-xdist-and-python-32
if sys.version_info[:2] == (3,2):
os.environ["PYTHONDONTWRITEBYTECODE"] = "1"
slaveinput,args,option_dict = channel.receive()
importpath = os.getcwd()
sys.path.insert(0, importpath) # XXX only for remote situations
os.environ['PYTHONPATH'] = (importpath + os.pathsep +
os.environ.get('PYTHONPATH', ''))
#os.environ['PYTHONPATH'] = importpath
import py
config = remote_initconfig(option_dict, args)
config.slaveinput = slaveinput
config.slaveoutput = {}
interactor = SlaveInteractor(config, channel)
config.hook.pytest_cmdline_main(config=config)

316
xdist/slavemanage.py Normal file
View File

@@ -0,0 +1,316 @@
import fnmatch
import os
import py
import pytest
import execnet
import xdist.remote
from _pytest import runner # XXX load dynamically
class NodeManager(object):
EXIT_TIMEOUT = 10
DEFAULT_IGNORES = ['.*', '*.pyc', '*.pyo', '*~']
def __init__(self, config, specs=None, defaultchdir="pyexecnetcache"):
self.config = config
self._nodesready = py.std.threading.Event()
self.trace = self.config.trace.get("nodemanager")
self.group = execnet.Group()
if specs is None:
specs = self._getxspecs()
self.specs = []
for spec in specs:
if not isinstance(spec, execnet.XSpec):
spec = execnet.XSpec(spec)
if not spec.chdir and not spec.popen:
spec.chdir = defaultchdir
self.group.allocate_id(spec)
self.specs.append(spec)
self.roots = self._getrsyncdirs()
self.rsyncoptions = self._getrsyncoptions()
def rsync_roots(self):
""" make sure that all remote gateways
have the same set of roots in their
current directory.
"""
if self.roots:
# send each rsync root
for root in self.roots:
self.rsync(root, **self.rsyncoptions)
def makegateways(self):
assert not list(self.group)
self.config.hook.pytest_xdist_setupnodes(config=self.config,
specs=self.specs)
for spec in self.specs:
gw = self.group.makegateway(spec)
self.config.hook.pytest_xdist_newgateway(gateway=gw)
def setup_nodes(self, putevent):
self.makegateways()
self.rsync_roots()
self.trace("setting up nodes")
for gateway in self.group:
node = SlaveController(self, gateway, self.config, putevent)
gateway.node = node # to keep node alive
node.setup()
self.trace("started node %r" % node)
def teardown_nodes(self):
self.group.terminate(self.EXIT_TIMEOUT)
def _getxspecs(self):
xspeclist = []
for xspec in self.config.getvalue("tx"):
i = xspec.find("*")
try:
num = int(xspec[:i])
except ValueError:
xspeclist.append(xspec)
else:
xspeclist.extend([xspec[i+1:]] * num)
if not xspeclist:
raise pytest.UsageError(
"MISSING test execution (tx) nodes: please specify --tx")
return [execnet.XSpec(x) for x in xspeclist]
def _getrsyncdirs(self):
for spec in self.specs:
if not spec.popen or spec.chdir:
break
else:
return []
import pytest, _pytest
pytestpath = pytest.__file__.rstrip("co")
pytestdir = py.path.local(_pytest.__file__).dirpath()
config = self.config
candidates = [py._pydir,pytestpath,pytestdir]
candidates += config.option.rsyncdir
rsyncroots = config.getini("rsyncdirs")
if rsyncroots:
candidates.extend(rsyncroots)
roots = []
for root in candidates:
root = py.path.local(root).realpath()
if not root.check():
raise pytest.UsageError("rsyncdir doesn't exist: %r" %(root,))
if root not in roots:
roots.append(root)
return roots
def _getrsyncoptions(self):
"""Get options to be passed for rsync."""
ignores = list(self.DEFAULT_IGNORES)
ignores += self.config.option.rsyncignore
ignores += self.config.getini("rsyncignore")
return {
'ignores': ignores,
'verbose': self.config.option.verbose,
}
def rsync(self, source, notify=None, verbose=False, ignores=None):
""" perform rsync to all remote hosts.
"""
rsync = HostRSync(source, verbose=verbose, ignores=ignores)
seen = py.builtin.set()
gateways = []
for gateway in self.group:
spec = gateway.spec
if spec.popen and not spec.chdir:
# XXX this assumes that sources are python-packages
# and that adding the basedir does not hurt
gateway.remote_exec("""
import sys ; sys.path.insert(0, %r)
""" % os.path.dirname(str(source))).waitclose()
continue
if spec not in seen:
def finished():
if notify:
notify("rsyncrootready", spec, source)
rsync.add_target_host(gateway, finished=finished)
seen.add(spec)
gateways.append(gateway)
if seen:
self.config.hook.pytest_xdist_rsyncstart(
source=source,
gateways=gateways,
)
rsync.send()
self.config.hook.pytest_xdist_rsyncfinish(
source=source,
gateways=gateways,
)
class HostRSync(execnet.RSync):
""" RSyncer that filters out common files
"""
def __init__(self, sourcedir, *args, **kwargs):
self._synced = {}
ignores= None
if 'ignores' in kwargs:
ignores = kwargs.pop('ignores')
self._ignores = ignores or []
super(HostRSync, self).__init__(sourcedir=sourcedir, **kwargs)
def filter(self, path):
path = py.path.local(path)
for x in self._ignores:
x = getattr(x, 'strpath', x)
if fnmatch.fnmatch(path.basename, x) or fnmatch.fnmatch(path.strpath, x):
return False
else:
return True
def add_target_host(self, gateway, finished=None):
remotepath = os.path.basename(self._sourcedir)
super(HostRSync, self).add_target(gateway, remotepath,
finishedcallback=finished,
delete=True,)
def _report_send_file(self, gateway, modified_rel_path):
if self._verbose:
path = os.path.basename(self._sourcedir) + "/" + modified_rel_path
remotepath = gateway.spec.chdir
py.builtin.print_('%s:%s <= %s' %
(gateway.spec, remotepath, path))
def make_reltoroot(roots, args):
# XXX introduce/use public API for splitting py.test args
splitcode = "::"
l = []
for arg in args:
parts = arg.split(splitcode)
fspath = py.path.local(parts[0])
for root in roots:
x = fspath.relto(root)
if x or fspath == root:
parts[0] = root.basename + "/" + x
break
else:
raise ValueError("arg %s not relative to an rsync root" % (arg,))
l.append(splitcode.join(parts))
return l
class SlaveController(object):
ENDMARK = -1
def __init__(self, nodemanager, gateway, config, putevent):
self.nodemanager = nodemanager
self.putevent = putevent
self.gateway = gateway
self.config = config
self.slaveinput = {'slaveid': gateway.id}
self._down = False
self.log = py.log.Producer("slavectl-%s" % gateway.id)
if not self.config.option.debug:
py.log.setconsumer(self.log._keywords, None)
def __repr__(self):
return "<%s %s>" %(self.__class__.__name__, self.gateway.id,)
def setup(self):
self.log("setting up slave session")
spec = self.gateway.spec
args = self.config.args
if not spec.popen or spec.chdir:
args = make_reltoroot(self.nodemanager.roots, args)
option_dict = vars(self.config.option)
if spec.popen:
name = "popen-%s" % self.gateway.id
basetemp = self.config._tmpdirhandler.getbasetemp()
option_dict['basetemp'] = str(basetemp.join(name))
self.config.hook.pytest_configure_node(node=self)
self.channel = self.gateway.remote_exec(xdist.remote)
self.channel.send((self.slaveinput, args, option_dict))
if self.putevent:
self.channel.setcallback(self.process_from_remote,
endmarker=self.ENDMARK)
def ensure_teardown(self):
if hasattr(self, 'channel'):
if not self.channel.isclosed():
self.log("closing", self.channel)
self.channel.close()
#del self.channel
if hasattr(self, 'gateway'):
self.log("exiting", self.gateway)
self.gateway.exit()
#del self.gateway
def send_runtest_some(self, indices):
self.sendcommand("runtests", indices=indices)
def send_runtest_all(self):
self.sendcommand("runtests_all",)
def shutdown(self):
if not self._down:
try:
self.sendcommand("shutdown")
except IOError:
pass
def sendcommand(self, name, **kwargs):
""" send a named parametrized command to the other side. """
self.log("sending command %s(**%s)" % (name, kwargs))
self.channel.send((name, kwargs))
def notify_inproc(self, eventname, **kwargs):
self.log("queuing %s(**%s)" % (eventname, kwargs))
self.putevent((eventname, kwargs))
def process_from_remote(self, eventcall):
""" this gets called for each object we receive from
the other side and if the channel closes.
Note that channel callbacks run in the receiver
thread of execnet gateways - we need to
avoid raising exceptions or doing heavy work.
"""
try:
if eventcall == self.ENDMARK:
err = self.channel._getremoteerror()
if not self._down:
if not err or isinstance(err, EOFError):
err = "Not properly terminated" # lost connection?
self.notify_inproc("errordown", node=self, error=err)
self._down = True
return
eventname, kwargs = eventcall
if eventname in ("collectionstart"):
self.log("ignoring %s(%s)" %(eventname, kwargs))
elif eventname == "slaveready":
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname == "slavefinished":
self._down = True
self.slaveoutput = kwargs['slaveoutput']
self.notify_inproc("slavefinished", node=self)
elif eventname == "logstart":
self.notify_inproc(eventname, node=self, **kwargs)
elif eventname in ("testreport", "collectreport", "teardownreport"):
item_index = kwargs.pop("item_index", None)
rep = unserialize_report(eventname, kwargs['data'])
if item_index is not None:
rep.item_index = item_index
self.notify_inproc(eventname, node=self, rep=rep)
elif eventname == "collectionfinish":
self.notify_inproc(eventname, node=self, ids=kwargs['ids'])
else:
raise ValueError("unknown event: %s" %(eventname,))
except KeyboardInterrupt:
# should not land in receiver-thread
raise
except:
excinfo = py.code.ExceptionInfo()
py.builtin.print_("!" * 20, excinfo)
self.config.pluginmanager.notify_exception(excinfo)
def unserialize_report(name, reportdict):
if name == "testreport":
return runner.TestReport(**reportdict)
elif name == "collectreport":
return runner.CollectReport(**reportdict)

View File

@@ -1,174 +0,0 @@
"""
Manage setup, running and local representation of remote nodes/processes.
"""
import py
from xdist.mypickle import PickleChannel
from py._test.session import Session
class TXNode(object):
""" Represents a Test Execution environment in the controlling process.
- sets up a slave node through an execnet gateway
- manages sending of test-items and receival of results and events
- creates events when the remote side crashes
"""
ENDMARK = -1
def __init__(self, nodemanager, gateway, config, putevent):
self.nodemanager = nodemanager
self.config = config
self.putevent = putevent
self.gateway = gateway
self.slaveinput = {}
self.channel = install_slave(self)
self.channel.setcallback(self.callback, endmarker=self.ENDMARK)
self._down = False
def __repr__(self):
id = self.gateway.id
status = self._down and 'true' or 'false'
return "<TXNode %r down=%s>" %(id, status)
def notify(self, eventname, *args, **kwargs):
assert not args
self.putevent((eventname, args, kwargs))
def callback(self, eventcall):
""" this gets called for each object we receive from
the other side and if the channel closes.
Note that channel callbacks run in the receiver
thread of execnet gateways - we need to
avoid raising exceptions or doing heavy work.
"""
try:
if eventcall == self.ENDMARK:
err = self.channel._getremoteerror()
if not self._down:
if not err or isinstance(err, EOFError):
err = "Not properly terminated" # lost connection?
self.notify("pytest_testnodedown", node=self, error=err)
self._down = True
return
eventname, args, kwargs = eventcall
if eventname == "slaveready":
self.notify("pytest_testnodeready", node=self)
elif eventname == "slavefinished":
self._down = True
self.slaveoutput = kwargs['slaveoutput']
error = kwargs['error']
self.notify("pytest_testnodedown", error=error, node=self)
elif eventname in ("pytest_runtest_logreport",
"pytest__teardown_final_logerror"):
kwargs['report'].node = self
self.notify(eventname, **kwargs)
else:
self.notify(eventname, **kwargs)
except KeyboardInterrupt:
# should not land in receiver-thread
raise
except:
excinfo = py.code.ExceptionInfo()
py.builtin.print_("!" * 20, excinfo)
self.config.pluginmanager.notify_exception(excinfo)
def send(self, item):
assert item is not None
self.channel.send(item)
def sendlist(self, itemlist):
self.channel.send(itemlist)
def shutdown(self, kill=False):
if kill:
self.gateway.exit()
else:
self.channel.send(None)
# configuring and setting up slave node
def install_slave(node):
channel = node.gateway.remote_exec(source="""
import os, sys
sys.path.insert(0, os.getcwd())
from xdist.mypickle import PickleChannel
from xdist.txnode import SlaveSession
channel.send("basicimport")
channel = PickleChannel(channel)
import py
config, slaveinput, basetemp, nodeid = channel.receive()
config.slaveinput = slaveinput
config.slaveoutput = {}
if basetemp:
config.basetemp = py.path.local(basetemp)
config.nodeid = nodeid
config.pluginmanager.do_configure(config)
session = SlaveSession(config, channel, nodeid)
session.dist_main()
""")
channel.receive()
channel = PickleChannel(channel)
basetemp = None
config = node.config
config.hook.pytest_configure_node(node=node)
if node.gateway.spec.popen:
popenbase = config.ensuretemp("popen")
basetemp = py.path.local.make_numbered_dir(prefix="slave-",
keep=0, rootdir=popenbase)
basetemp = str(basetemp)
channel.send((config, node.slaveinput, basetemp, node.gateway.id))
return channel
class SlaveSession(Session):
def __init__(self, config, channel, nodeid):
self.channel = channel
self.nodeid = nodeid
super(SlaveSession, self).__init__(config=config)
def __repr__(self):
return "<%s channel=%s>" %(self.__class__.__name__, self.channel)
def sendevent(self, eventname, *args, **kwargs):
self.channel.send((eventname, args, kwargs))
def pytest_runtest_logreport(self, report):
self.sendevent("pytest_runtest_logreport", report=report)
def pytest__teardown_final_logerror(self, report):
self.sendevent("pytest__teardown_final_logerror", report=report)
def pytest_keyboard_interrupt(self, excinfo):
self._slaveerror = "SIGINT"
def pytest_internalerror(self, excrepr):
self._slaveerror = "internal-error"
self.sendevent("pytest_internalerror", excrepr=excrepr)
def dist_main(self):
self.runner = self.config.pluginmanager.getplugin("pytest_runner")
self.sendevent("slaveready")
self.main(None)
error = getattr(self, '_slaveerror', None)
self.sendevent("slavefinished", error=error,
slaveoutput=self.config.slaveoutput)
def _mainloop(self, colitems):
while 1:
task = self.channel.receive()
if task is None:
break
if isinstance(task, list):
for item in task:
self.run_single(item=item)
else:
self.run_single(item=task)
def run_single(self, item):
call = self.runner.CallInfo(item._reraiseunpicklingproblem, when='setup')
if call.excinfo:
# likely it is not collectable here because of
# platform/import-dependency induced skips
# we fake a setup-error report with the obtained exception
# and do not care about capturing or non-runner hooks
rep = self.runner.pytest_runtest_makereport(item=item, call=call)
self.pytest_runtest_logreport(rep)
return
item.config.hook.pytest_runtest_protocol(item=item)

View File

@@ -1,53 +0,0 @@
import py
class StatRecorder:
def __init__(self, rootdirlist):
self.rootdirlist = rootdirlist
self.statcache = {}
self.check() # snapshot state
def fil(self, p):
return p.ext in ('.py', '.txt', '.c', '.h')
def rec(self, p):
return p.check(dotfile=0)
def waitonchange(self, checkinterval=1.0):
while 1:
changed = self.check()
if changed:
return
py.std.time.sleep(checkinterval)
def check(self, removepycfiles=True):
changed = False
statcache = self.statcache
newstat = {}
for rootdir in self.rootdirlist:
for path in rootdir.visit(self.fil, self.rec):
oldstat = statcache.get(path, None)
if oldstat is not None:
del statcache[path]
try:
newstat[path] = curstat = path.stat()
except py.error.ENOENT:
if oldstat:
del statcache[path]
changed = True
else:
if oldstat:
if oldstat.mtime != curstat.mtime or \
oldstat.size != curstat.size:
changed = True
py.builtin.print_("# MODIFIED", path)
if removepycfiles and path.ext == ".py":
pycfile = path + "c"
if pycfile.check():
pycfile.remove()
else:
changed = True
if statcache:
changed = True
self.statcache = newstat
return changed