Compare commits
241 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
48e6823c7a | ||
|
|
6b4e6eee09 | ||
|
|
f7648e11d8 | ||
|
|
7bb7d1205c | ||
|
|
a1d41c6811 | ||
|
|
58e0301f87 | ||
|
|
a5e7b2760d | ||
|
|
efe438d3e8 | ||
|
|
ec0565fac5 | ||
|
|
48a6a504b6 | ||
|
|
8f55425898 | ||
|
|
a51e52aee3 | ||
|
|
69dfc75572 | ||
|
|
9d3e51af9f | ||
|
|
f7c1b9087a | ||
|
|
36c42b5c15 | ||
|
|
bc8ee95e72 | ||
|
|
979dfd20f2 | ||
|
|
67fbd24ebf | ||
|
|
7f7589afa9 | ||
|
|
4f01cda2a7 | ||
|
|
bd296c796f | ||
|
|
7144cec580 | ||
|
|
99a1188287 | ||
|
|
0b18b6094e | ||
|
|
ae53d04780 | ||
|
|
a324826dfd | ||
|
|
29bf205f3a | ||
|
|
3b9fd3abd8 | ||
|
|
974e4e3a9d | ||
|
|
369b7709f7 | ||
|
|
78438db752 | ||
|
|
a2f4a11301 | ||
|
|
077c468589 | ||
|
|
d4fe273b2f | ||
|
|
761a95e542 | ||
|
|
5ae04397bd | ||
|
|
2c230f910d | ||
|
|
ae54151467 | ||
|
|
05af53d160 | ||
|
|
448f1c0d9c | ||
|
|
346da57a8a | ||
|
|
9d92b19ed1 | ||
|
|
e2201fe3a9 | ||
|
|
45b98d6e70 | ||
|
|
29b4082b00 | ||
|
|
6ac638ba87 | ||
|
|
f2512017ea | ||
|
|
3bd3ba133f | ||
|
|
be249dcfe5 | ||
|
|
45afb1b7d1 | ||
|
|
922a283f99 | ||
|
|
7e857e9068 | ||
|
|
172d46abd0 | ||
|
|
ac9192e4f8 | ||
|
|
b490047b1c | ||
|
|
ad785a476c | ||
|
|
d37af98db3 | ||
|
|
4316cf2121 | ||
|
|
fb6fc673b8 | ||
|
|
eaec527a60 | ||
|
|
2bc4065a00 | ||
|
|
5c32421f2e | ||
|
|
fab7615c8a | ||
|
|
2315de8321 | ||
|
|
25711a0879 | ||
|
|
0e05a4fbcf | ||
|
|
8675cf640d | ||
|
|
8b211983ff | ||
|
|
661a8a4a92 | ||
|
|
abe080c6b4 | ||
|
|
574d230c22 | ||
|
|
88c5299a94 | ||
|
|
09933b8b04 | ||
|
|
fb1b1d9aae | ||
|
|
68a08840e1 | ||
|
|
41b8a03b05 | ||
|
|
fba2079292 | ||
|
|
9675b0f65c | ||
|
|
c426a67b0e | ||
|
|
6ca3c980bf | ||
|
|
5bd34f8ecc | ||
|
|
7636dc76e0 | ||
|
|
c5dee7b549 | ||
|
|
643ab120f4 | ||
|
|
f86c8469f5 | ||
|
|
22335acd09 | ||
|
|
8b866aa065 | ||
|
|
2c4964d290 | ||
|
|
a70293fdb7 | ||
|
|
43113f9a9d | ||
|
|
650c3bcfde | ||
|
|
ade9b9aa8e | ||
|
|
7576b3c7d0 | ||
|
|
85415135a4 | ||
|
|
3cc8697744 | ||
|
|
703da22831 | ||
|
|
14ceaf2459 | ||
|
|
f3bc197afb | ||
|
|
aafe6a8e34 | ||
|
|
46b1348b79 | ||
|
|
709da3fe84 | ||
|
|
a59c2c9e17 | ||
|
|
6096aeca53 | ||
|
|
8cd68494bf | ||
|
|
dd4b252749 | ||
|
|
59067684cd | ||
|
|
81f4a07548 | ||
|
|
50c8218501 | ||
|
|
814c6c70f1 | ||
|
|
85118e9019 | ||
|
|
c2cdc66eca | ||
|
|
bc66cd85b1 | ||
|
|
639f35bbc4 | ||
|
|
8c683acad1 | ||
|
|
dc8225afea | ||
|
|
8713f4ba60 | ||
|
|
c40dc9f779 | ||
|
|
d1684e8052 | ||
|
|
c25ea2cbe2 | ||
|
|
6a523b4f59 | ||
|
|
fb043c355e | ||
|
|
0ef23dd31f | ||
|
|
c13fa886d9 | ||
|
|
79ac8c6681 | ||
|
|
418cd482b1 | ||
|
|
491f58ab26 | ||
|
|
df85ddf0d2 | ||
|
|
e7c8fc7db9 | ||
|
|
92f8eef836 | ||
|
|
758b5e3511 | ||
|
|
e91dc7c895 | ||
|
|
4e8b9fab3c | ||
|
|
d105e75d87 | ||
|
|
46950ef19a | ||
|
|
407ca5b120 | ||
|
|
a4fe63c08d | ||
|
|
fefdca5787 | ||
|
|
c7d120ec1c | ||
|
|
ae8ee08ac0 | ||
|
|
1707168b62 | ||
|
|
4fcd346838 | ||
|
|
aa7f7a1c71 | ||
|
|
48b76c7544 | ||
|
|
d52ff3e2b9 | ||
|
|
f286a02582 | ||
|
|
c6e3606c6b | ||
|
|
f4eb15632d | ||
|
|
d027f9d546 | ||
|
|
8de50347fb | ||
|
|
4b4a2c8162 | ||
|
|
29d58ffdf2 | ||
|
|
9ea58242d4 | ||
|
|
8772b8c928 | ||
|
|
8e81ed693a | ||
|
|
d853d9a9af | ||
|
|
57a3d4d6d8 | ||
|
|
8f6477f695 | ||
|
|
2618e3640f | ||
|
|
43de6c270f | ||
|
|
ce1b456762 | ||
|
|
332bceeb7a | ||
|
|
e3b2792677 | ||
|
|
67859158d4 | ||
|
|
5dfce4a0ca | ||
|
|
6c90059342 | ||
|
|
5690beab5a | ||
|
|
8bc9fdc8d3 | ||
|
|
00dee742b0 | ||
|
|
5e31624315 | ||
|
|
5e311d3bfc | ||
|
|
4b7293428b | ||
|
|
6fdcecb864 | ||
|
|
f63ff5267c | ||
|
|
5498fe960f | ||
|
|
4c885cf0d2 | ||
|
|
326b63adf8 | ||
|
|
70dc7a976d | ||
|
|
89a98e3276 | ||
|
|
410438f187 | ||
|
|
8dc4e732f0 | ||
|
|
e98057130d | ||
|
|
70d22fbe9a | ||
|
|
56b40ebd75 | ||
|
|
5f75c5851f | ||
|
|
606ea870f0 | ||
|
|
e56838cb6c | ||
|
|
e22d3e03fe | ||
|
|
d53feaf6f0 | ||
|
|
914f689ee8 | ||
|
|
971f34147a | ||
|
|
16b4f54545 | ||
|
|
abb07fc732 | ||
|
|
cf6949c9a3 | ||
|
|
2f984e0c23 | ||
|
|
0a7237b72f | ||
|
|
f684a9ed56 | ||
|
|
196cece338 | ||
|
|
241ff0b43a | ||
|
|
411e9b136b | ||
|
|
96521ada68 | ||
|
|
7cf8afef47 | ||
|
|
657522b629 | ||
|
|
dd199d255c | ||
|
|
89d6defd68 | ||
|
|
c4d761fe99 | ||
|
|
bf3d9f3737 | ||
|
|
32a67f9622 | ||
|
|
d438a0bd83 | ||
|
|
d3645758ea | ||
|
|
15b9e8ed7d | ||
|
|
ee64da4bad | ||
|
|
4fe13e59a7 | ||
|
|
250160b4b0 | ||
|
|
f423ce9c01 | ||
|
|
491c05cea7 | ||
|
|
e02d22aa4f | ||
|
|
c0910abf2f | ||
|
|
b061e71da9 | ||
|
|
fa412675fc | ||
|
|
0bb84abca7 | ||
|
|
f5decc90ca | ||
|
|
7fc2f8786f | ||
|
|
76cede83c0 | ||
|
|
993efe927b | ||
|
|
9c4f6791e5 | ||
|
|
7ba8fee3dc | ||
|
|
265b7458cb | ||
|
|
dc3e39e95c | ||
|
|
4f2166c997 | ||
|
|
e0c128beec | ||
|
|
bf039fea74 | ||
|
|
78be3db9bb | ||
|
|
aae89cd021 | ||
|
|
9ac818fb5c | ||
|
|
9e6dfaefd9 | ||
|
|
c742e47de0 | ||
|
|
95ddd5059f | ||
|
|
b6815538c5 | ||
|
|
ea936213bc | ||
|
|
7e65f346f4 |
5
.hgtags
5
.hgtags
@@ -39,3 +39,8 @@ e4497c2aed358c1988cf7be83ca9394c3c707fa2 2.0.1
|
||||
c777dcad166548b7499564cb49ae5c8b4b07f935 2.0.3
|
||||
c777dcad166548b7499564cb49ae5c8b4b07f935 2.0.3
|
||||
49f11dbff725acdcc5fe3657cbcdf9ae04e25bbc 2.0.3
|
||||
49f11dbff725acdcc5fe3657cbcdf9ae04e25bbc 2.0.3
|
||||
363e5a5a59c803e6bc176a6f9cc4bf1a1ca2dab0 2.0.3
|
||||
e5e1746a197f0398356a43fbe2eebac9690f795d 2.1.0
|
||||
5864412c6f3c903384243bd315639d101d7ebc67 2.1.2
|
||||
12a05d59249f80276e25fd8b96e8e545b1332b7a 2.1.3
|
||||
|
||||
18
AUTHORS
18
AUTHORS
@@ -1,16 +1,17 @@
|
||||
Holger Krekel, holger at merlinux eu
|
||||
Benjamin Peterson, benjamin at python org
|
||||
Ronny Pfannschmidt, Ronny.Pfannschmidt at gmx de
|
||||
Guido Wesdorp, johnny at johnnydebris net
|
||||
Samuele Pedroni, pedronis at openend se
|
||||
Carl Friedrich Bolz, cfbolz at gmx de
|
||||
Armin Rigo, arigo at tunes org
|
||||
Maciek Fijalkowski, fijal at genesilico pl
|
||||
Brian Dorsey, briandorsey at gmail com
|
||||
merlinux GmbH, Germany, office at merlinux eu
|
||||
|
||||
Contributors include::
|
||||
|
||||
Ronny Pfannschmidt
|
||||
Benjamin Peterson
|
||||
Floris Bruynooghe
|
||||
Samuele Pedroni
|
||||
Carl Friedrich Bolz
|
||||
Armin Rigo
|
||||
Maciek Fijalkowski
|
||||
Guido Wesdorp
|
||||
Brian Dorsey
|
||||
Ross Lawley
|
||||
Ralf Schmitt
|
||||
Chris Lamb
|
||||
@@ -21,3 +22,4 @@ Jan Balster
|
||||
Grig Gheorghiu
|
||||
Bob Ippolito
|
||||
Christian Tismer
|
||||
Daniel Nuri
|
||||
|
||||
84
CHANGELOG
84
CHANGELOG
@@ -1,3 +1,87 @@
|
||||
Changes between 2.1.3 and 2.2.0
|
||||
----------------------------------------
|
||||
|
||||
- fix issue90: introduce eager tearing down of test items so that
|
||||
teardown function are called earlier.
|
||||
- add an all-powerful metafunc.parametrize function which allows to
|
||||
parametrize test function arguments in multiple steps and therefore
|
||||
from indepdenent plugins and palces.
|
||||
- add a @pytest.mark.parametrize helper which allows to easily
|
||||
call a test function with different argument values
|
||||
- Add examples to the "parametrize" example page, including a quick port
|
||||
of Test scenarios and the new parametrize function and decorator.
|
||||
- introduce registration for "pytest.mark.*" helpers via ini-files
|
||||
or through plugin hooks. Also introduce a "--strict" option which
|
||||
will treat unregistered markers as errors
|
||||
allowing to avoid typos and maintain a well described set of markers
|
||||
for your test suite. See exaples at http://pytest.org/latest/mark.html
|
||||
and its links.
|
||||
- issue50: introduce "-m marker" option to select tests based on markers
|
||||
(this is a stricter and more predictable version of '-k' in that "-m"
|
||||
only matches complete markers and has more obvious rules for and/or
|
||||
semantics.
|
||||
- new feature to help optimizing the speed of your tests:
|
||||
--durations=N option for displaying N slowest test calls
|
||||
and setup/teardown methods.
|
||||
- fix issue87: --pastebin now works with python3
|
||||
- fix issue89: --pdb with unexpected exceptions in doctest work more sensibly
|
||||
- fix and cleanup pytest's own test suite to not leak FDs
|
||||
- fix issue83: link to generated funcarg list
|
||||
- fix issue74: pyarg module names are now checked against imp.find_module false positives
|
||||
- fix compatibility with twisted/trial-11.1.0 use cases
|
||||
|
||||
Changes between 2.1.2 and 2.1.3
|
||||
----------------------------------------
|
||||
|
||||
- fix issue79: assertion rewriting failed on some comparisons in boolops
|
||||
- correctly handle zero length arguments (a la pytest '')
|
||||
- fix issue67 / junitxml now contains correct test durations, thanks ronny
|
||||
- fix issue75 / skipping test failure on jython
|
||||
- fix issue77 / Allow assertrepr_compare hook to apply to a subset of tests
|
||||
|
||||
Changes between 2.1.1 and 2.1.2
|
||||
----------------------------------------
|
||||
|
||||
- fix assertion rewriting on files with windows newlines on some Python versions
|
||||
- refine test discovery by package/module name (--pyargs), thanks Florian Mayer
|
||||
- fix issue69 / assertion rewriting fixed on some boolean operations
|
||||
- fix issue68 / packages now work with assertion rewriting
|
||||
- fix issue66: use different assertion rewriting caches when the -O option is passed
|
||||
- don't try assertion rewriting on Jython, use reinterp
|
||||
|
||||
Changes between 2.1.0 and 2.1.1
|
||||
----------------------------------------------
|
||||
|
||||
- fix issue64 / pytest.set_trace now works within pytest_generate_tests hooks
|
||||
- fix issue60 / fix error conditions involving the creation of __pycache__
|
||||
- fix issue63 / assertion rewriting on inserts involving strings containing '%'
|
||||
- fix assertion rewriting on calls with a ** arg
|
||||
- don't cache rewritten modules if bytecode generation is disabled
|
||||
- fix assertion rewriting in read-only directories
|
||||
- fix issue59: provide system-out/err tags for junitxml output
|
||||
- fix issue61: assertion rewriting on boolean operations with 3 or more operands
|
||||
- you can now build a man page with "cd doc ; make man"
|
||||
|
||||
Changes between 2.0.3 and 2.1.0.DEV
|
||||
----------------------------------------------
|
||||
|
||||
- fix issue53 call nosestyle setup functions with correct ordering
|
||||
- fix issue58 and issue59: new assertion code fixes
|
||||
- merge Benjamin's assertionrewrite branch: now assertions
|
||||
for test modules on python 2.6 and above are done by rewriting
|
||||
the AST and saving the pyc file before the test module is imported.
|
||||
see doc/assert.txt for more info.
|
||||
- fix issue43: improve doctests with better traceback reporting on
|
||||
unexpected exceptions
|
||||
- fix issue47: timing output in junitxml for test cases is now correct
|
||||
- fix issue48: typo in MarkInfo repr leading to exception
|
||||
- fix issue49: avoid confusing error when initizaliation partially fails
|
||||
- fix issue44: env/username expansion for junitxml file path
|
||||
- show releaselevel information in test runs for pypy
|
||||
- reworked doc pages for better navigation and PDF generation
|
||||
- report KeyboardInterrupt even if interrupted during session startup
|
||||
- fix issue 35 - provide PDF doc version and download link from index page
|
||||
|
||||
Changes between 2.0.2 and 2.0.3
|
||||
----------------------------------------------
|
||||
|
||||
|
||||
77
ISSUES.txt
77
ISSUES.txt
@@ -7,17 +7,27 @@ tags: bug 2.4 core xdist
|
||||
the protocol now - setup/teardown is called at module level.
|
||||
consider making calling of setup/teardown configurable
|
||||
|
||||
profiling / hook call optimization
|
||||
-------------------------------------
|
||||
tags: enhancement 2.1
|
||||
optimizations
|
||||
---------------------------------------------------------------
|
||||
tags: 2.4 core
|
||||
|
||||
bench/bench.py reveals that for very quick running
|
||||
unit tests the hook architecture is a bit slow.
|
||||
Profile and improve hook calls.
|
||||
- look at ihook optimization such that all lookups for
|
||||
hooks relating to the same fspath are cached.
|
||||
|
||||
fix start/finish partial finailization problem
|
||||
---------------------------------------------------------------
|
||||
tags: bug core
|
||||
|
||||
if a configure/runtest_setup/sessionstart/... hook invocation partially
|
||||
fails the sessionfinishes is not called. Each hook implementation
|
||||
should better be repsonsible for registering a cleanup/finalizer
|
||||
appropriately to avoid this issue. Moreover/Alternatively, we could
|
||||
record which implementations of a hook succeeded and only call their
|
||||
teardown.
|
||||
|
||||
do early-teardown of test modules
|
||||
-----------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
currently teardowns are called when the next tests is setup
|
||||
except for the function/method level where interally
|
||||
@@ -29,7 +39,7 @@ prints of teardown-code appear in the setup of the next test.
|
||||
|
||||
consider and document __init__ file usage in test directories
|
||||
---------------------------------------------------------------
|
||||
tags: bug 2.1 core
|
||||
tags: bug 2.2 core
|
||||
|
||||
Currently, a test module is imported with its fully qualified
|
||||
package path, determined by checking __init__ files upwards.
|
||||
@@ -44,7 +54,7 @@ certain scenarios makes sense.
|
||||
|
||||
relax requirement to have tests/testing contain an __init__
|
||||
----------------------------------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
bb: http://bitbucket.org/hpk42/py-trunk/issue/64
|
||||
|
||||
A local test run of a "tests" directory may work
|
||||
@@ -55,25 +65,24 @@ i.e. port the nose-logic of unloading a test module.
|
||||
|
||||
customize test function collection
|
||||
-------------------------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
- introduce py.test.mark.nocollect for not considering a function for
|
||||
test collection at all. maybe also introduce a py.test.mark.test to
|
||||
explicitely mark a function to become a tested one. Lookup JUnit ways
|
||||
of tagging tests.
|
||||
|
||||
- allow an easy way to customize "test_", "Test" prefixes for file paths
|
||||
and test function/class names. the current customizable Item requires
|
||||
too much code/concepts to influence this collection matching.
|
||||
maybe introduce pytest_pycollect_filters = {
|
||||
'file': 'test*.py',
|
||||
'function': 'test*',
|
||||
'class': 'Test*',
|
||||
}
|
||||
introduce pytest.mark.importorskip
|
||||
-------------------------------------------------------
|
||||
tags: feature 2.2
|
||||
|
||||
in addition to the imperative pytest.importorskip also introduce
|
||||
a pytest.mark.importorskip so that the test count is more correct.
|
||||
|
||||
|
||||
introduce py.test.mark.platform
|
||||
-------------------------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
Introduce nice-to-spell platform-skipping, examples:
|
||||
|
||||
@@ -90,7 +99,7 @@ interpreter versions.
|
||||
|
||||
pytest.mark.xfail signature change
|
||||
-------------------------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
change to pytest.mark.xfail(reason, (optional)condition)
|
||||
to better implement the word meaning. It also signals
|
||||
@@ -100,7 +109,7 @@ Compatibility? Maybe rename to "pytest.mark.xfail"?
|
||||
|
||||
introduce py.test.mark registration
|
||||
-----------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
introduce a hook that allows to register a named mark decorator
|
||||
with documentation and add "py.test --marks" to get
|
||||
@@ -109,7 +118,7 @@ definitions.
|
||||
|
||||
allow to non-intrusively apply skipfs/xfail/marks
|
||||
---------------------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
use case: mark a module or directory structures
|
||||
to be skipped on certain platforms (i.e. no import
|
||||
@@ -120,14 +129,14 @@ from conftests or plugins.
|
||||
|
||||
explicit referencing of conftest.py files
|
||||
-----------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
allow to name conftest.py files (in sub directories) that should
|
||||
be imported early, as to include command line options.
|
||||
|
||||
improve central py.test ini file
|
||||
----------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
introduce more declarative configuration options:
|
||||
- (to-be-collected test directories)
|
||||
@@ -138,7 +147,7 @@ introduce more declarative configuration options:
|
||||
|
||||
new documentation
|
||||
----------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
- logo py.test
|
||||
- examples for unittest or functional testing
|
||||
@@ -149,7 +158,7 @@ tags: feature 2.1
|
||||
|
||||
generalize parametrized testing to generate combinations
|
||||
-------------------------------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
think about extending metafunc.addcall or add a new method to allow to
|
||||
generate tests with combinations of all generated versions - what to do
|
||||
@@ -164,7 +173,7 @@ of values for a given function argument.
|
||||
|
||||
have imported module mismatch honour relative paths
|
||||
--------------------------------------------------------
|
||||
tags: bug 2.1
|
||||
tags: bug 2.2
|
||||
|
||||
With 1.1.1 py.test fails at least on windows if an import
|
||||
is relative and compared against an absolute conftest.py
|
||||
@@ -172,7 +181,7 @@ path. Normalize.
|
||||
|
||||
call termination with small timeout
|
||||
-------------------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
test: testing/pytest/dist/test_dsession.py - test_terminate_on_hanging_node
|
||||
|
||||
Call gateway group termination with a small timeout if available.
|
||||
@@ -180,7 +189,7 @@ Should make dist-testing less likely to leave lost processes.
|
||||
|
||||
consider globals: py.test.ensuretemp and config
|
||||
--------------------------------------------------------------
|
||||
tags: experimental-wish 2.1
|
||||
tags: experimental-wish 2.2
|
||||
|
||||
consider deprecating py.test.ensuretemp and py.test.config
|
||||
to further reduce py.test globality. Also consider
|
||||
@@ -189,7 +198,7 @@ a plugin rather than being there from the start.
|
||||
|
||||
consider allowing funcargs for setup methods
|
||||
--------------------------------------------------------------
|
||||
tags: experimental-wish 2.1
|
||||
tags: experimental-wish 2.2
|
||||
|
||||
Users have expressed the wish to have funcargs available to setup
|
||||
functions. Experiment with allowing funcargs there - it might
|
||||
@@ -205,7 +214,7 @@ setup_module -> request has no request.cls
|
||||
|
||||
consider pytest_addsyspath hook
|
||||
-----------------------------------------
|
||||
tags: 2.1
|
||||
tags: 2.2
|
||||
|
||||
py.test could call a new pytest_addsyspath() in order to systematically
|
||||
allow manipulation of sys.path and to inhibit it via --no-addsyspath
|
||||
@@ -217,7 +226,7 @@ and pytest_configure.
|
||||
|
||||
show plugin information in test header
|
||||
----------------------------------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
Now that external plugins are becoming more numerous
|
||||
it would be useful to have external plugins along with
|
||||
@@ -225,7 +234,7 @@ their versions displayed as a header line.
|
||||
|
||||
deprecate global py.test.config usage
|
||||
----------------------------------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
py.test.ensuretemp and py.test.config are probably the last
|
||||
objects containing global state. Often using them is not
|
||||
@@ -235,7 +244,7 @@ as others.
|
||||
|
||||
remove deprecated bits in collect.py
|
||||
-------------------------------------------------------------------
|
||||
tags: feature 2.1
|
||||
tags: feature 2.2
|
||||
|
||||
In an effort to further simplify code, review and remove deprecated bits
|
||||
in collect.py. Probably good:
|
||||
|
||||
@@ -1,2 +1,2 @@
|
||||
#
|
||||
__version__ = '2.0.3'
|
||||
__version__ = '2.2.0'
|
||||
|
||||
119
_pytest/assertion/__init__.py
Normal file
119
_pytest/assertion/__init__.py
Normal file
@@ -0,0 +1,119 @@
|
||||
"""
|
||||
support for presenting detailed information in failing assertions.
|
||||
"""
|
||||
import py
|
||||
import sys
|
||||
import pytest
|
||||
from _pytest.monkeypatch import monkeypatch
|
||||
from _pytest.assertion import util
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group.addoption('--assert', action="store", dest="assertmode",
|
||||
choices=("rewrite", "reinterp", "plain",),
|
||||
default="rewrite", metavar="MODE",
|
||||
help="""control assertion debugging tools.
|
||||
'plain' performs no assertion debugging.
|
||||
'reinterp' reinterprets assert statements after they failed to provide assertion expression information.
|
||||
'rewrite' (the default) rewrites assert statements in test modules on import
|
||||
to provide assert expression information. """)
|
||||
group.addoption('--no-assert', action="store_true", default=False,
|
||||
dest="noassert", help="DEPRECATED equivalent to --assert=plain")
|
||||
group.addoption('--nomagic', action="store_true", default=False,
|
||||
dest="nomagic", help="DEPRECATED equivalent to --assert=plain")
|
||||
|
||||
class AssertionState:
|
||||
"""State for the assertion plugin."""
|
||||
|
||||
def __init__(self, config, mode):
|
||||
self.mode = mode
|
||||
self.trace = config.trace.root.get("assertion")
|
||||
|
||||
def pytest_configure(config):
|
||||
mode = config.getvalue("assertmode")
|
||||
if config.getvalue("noassert") or config.getvalue("nomagic"):
|
||||
mode = "plain"
|
||||
if mode == "rewrite":
|
||||
try:
|
||||
import ast
|
||||
except ImportError:
|
||||
mode = "reinterp"
|
||||
else:
|
||||
if sys.platform.startswith('java'):
|
||||
mode = "reinterp"
|
||||
if mode != "plain":
|
||||
_load_modules(mode)
|
||||
m = monkeypatch()
|
||||
config._cleanup.append(m.undo)
|
||||
m.setattr(py.builtin.builtins, 'AssertionError',
|
||||
reinterpret.AssertionError)
|
||||
hook = None
|
||||
if mode == "rewrite":
|
||||
hook = rewrite.AssertionRewritingHook()
|
||||
sys.meta_path.append(hook)
|
||||
warn_about_missing_assertion(mode)
|
||||
config._assertstate = AssertionState(config, mode)
|
||||
config._assertstate.hook = hook
|
||||
config._assertstate.trace("configured with mode set to %r" % (mode,))
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
hook = config._assertstate.hook
|
||||
if hook is not None:
|
||||
sys.meta_path.remove(hook)
|
||||
|
||||
def pytest_collection(session):
|
||||
# this hook is only called when test modules are collected
|
||||
# so for example not in the master process of pytest-xdist
|
||||
# (which does not collect test modules)
|
||||
hook = session.config._assertstate.hook
|
||||
if hook is not None:
|
||||
hook.set_session(session)
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
def callbinrepr(op, left, right):
|
||||
hook_result = item.ihook.pytest_assertrepr_compare(
|
||||
config=item.config, op=op, left=left, right=right)
|
||||
for new_expl in hook_result:
|
||||
if new_expl:
|
||||
res = '\n~'.join(new_expl)
|
||||
if item.config.getvalue("assertmode") == "rewrite":
|
||||
# The result will be fed back a python % formatting
|
||||
# operation, which will fail if there are extraneous
|
||||
# '%'s in the string. Escape them here.
|
||||
res = res.replace("%", "%%")
|
||||
return res
|
||||
util._reprcompare = callbinrepr
|
||||
|
||||
def pytest_runtest_teardown(item):
|
||||
util._reprcompare = None
|
||||
|
||||
def pytest_sessionfinish(session):
|
||||
hook = session.config._assertstate.hook
|
||||
if hook is not None:
|
||||
hook.session = None
|
||||
|
||||
def _load_modules(mode):
|
||||
"""Lazily import assertion related code."""
|
||||
global rewrite, reinterpret
|
||||
from _pytest.assertion import reinterpret
|
||||
if mode == "rewrite":
|
||||
from _pytest.assertion import rewrite
|
||||
|
||||
def warn_about_missing_assertion(mode):
|
||||
try:
|
||||
assert False
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
if mode == "rewrite":
|
||||
specifically = ("assertions which are not in test modules "
|
||||
"will be ignored")
|
||||
else:
|
||||
specifically = "failing tests may report as passing"
|
||||
|
||||
sys.stderr.write("WARNING: " + specifically +
|
||||
" because assert statements are not executed "
|
||||
"by the underlying Python interpreter "
|
||||
"(are you using python -O?)\n")
|
||||
|
||||
pytest_assertrepr_compare = util.assertrepr_compare
|
||||
333
_pytest/assertion/newinterpret.py
Normal file
333
_pytest/assertion/newinterpret.py
Normal file
@@ -0,0 +1,333 @@
|
||||
"""
|
||||
Find intermediate evalutation results in assert statements through builtin AST.
|
||||
This should replace oldinterpret.py eventually.
|
||||
"""
|
||||
|
||||
import sys
|
||||
import ast
|
||||
|
||||
import py
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion.reinterpret import BuiltinAssertionError
|
||||
|
||||
|
||||
if sys.platform.startswith("java") and sys.version_info < (2, 5, 2):
|
||||
# See http://bugs.jython.org/issue1497
|
||||
_exprs = ("BoolOp", "BinOp", "UnaryOp", "Lambda", "IfExp", "Dict",
|
||||
"ListComp", "GeneratorExp", "Yield", "Compare", "Call",
|
||||
"Repr", "Num", "Str", "Attribute", "Subscript", "Name",
|
||||
"List", "Tuple")
|
||||
_stmts = ("FunctionDef", "ClassDef", "Return", "Delete", "Assign",
|
||||
"AugAssign", "Print", "For", "While", "If", "With", "Raise",
|
||||
"TryExcept", "TryFinally", "Assert", "Import", "ImportFrom",
|
||||
"Exec", "Global", "Expr", "Pass", "Break", "Continue")
|
||||
_expr_nodes = set(getattr(ast, name) for name in _exprs)
|
||||
_stmt_nodes = set(getattr(ast, name) for name in _stmts)
|
||||
def _is_ast_expr(node):
|
||||
return node.__class__ in _expr_nodes
|
||||
def _is_ast_stmt(node):
|
||||
return node.__class__ in _stmt_nodes
|
||||
else:
|
||||
def _is_ast_expr(node):
|
||||
return isinstance(node, ast.expr)
|
||||
def _is_ast_stmt(node):
|
||||
return isinstance(node, ast.stmt)
|
||||
|
||||
|
||||
class Failure(Exception):
|
||||
"""Error found while interpreting AST."""
|
||||
|
||||
def __init__(self, explanation=""):
|
||||
self.cause = sys.exc_info()
|
||||
self.explanation = explanation
|
||||
|
||||
|
||||
def interpret(source, frame, should_fail=False):
|
||||
mod = ast.parse(source)
|
||||
visitor = DebugInterpreter(frame)
|
||||
try:
|
||||
visitor.visit(mod)
|
||||
except Failure:
|
||||
failure = sys.exc_info()[1]
|
||||
return getfailure(failure)
|
||||
if should_fail:
|
||||
return ("(assertion failed, but when it was re-run for "
|
||||
"printing intermediate values, it did not fail. Suggestions: "
|
||||
"compute assert expression before the assert or use --assert=plain)")
|
||||
|
||||
def run(offending_line, frame=None):
|
||||
if frame is None:
|
||||
frame = py.code.Frame(sys._getframe(1))
|
||||
return interpret(offending_line, frame)
|
||||
|
||||
def getfailure(e):
|
||||
explanation = util.format_explanation(e.explanation)
|
||||
value = e.cause[1]
|
||||
if str(value):
|
||||
lines = explanation.split('\n')
|
||||
lines[0] += " << %s" % (value,)
|
||||
explanation = '\n'.join(lines)
|
||||
text = "%s: %s" % (e.cause[0].__name__, explanation)
|
||||
if text.startswith('AssertionError: assert '):
|
||||
text = text[16:]
|
||||
return text
|
||||
|
||||
operator_map = {
|
||||
ast.BitOr : "|",
|
||||
ast.BitXor : "^",
|
||||
ast.BitAnd : "&",
|
||||
ast.LShift : "<<",
|
||||
ast.RShift : ">>",
|
||||
ast.Add : "+",
|
||||
ast.Sub : "-",
|
||||
ast.Mult : "*",
|
||||
ast.Div : "/",
|
||||
ast.FloorDiv : "//",
|
||||
ast.Mod : "%",
|
||||
ast.Eq : "==",
|
||||
ast.NotEq : "!=",
|
||||
ast.Lt : "<",
|
||||
ast.LtE : "<=",
|
||||
ast.Gt : ">",
|
||||
ast.GtE : ">=",
|
||||
ast.Pow : "**",
|
||||
ast.Is : "is",
|
||||
ast.IsNot : "is not",
|
||||
ast.In : "in",
|
||||
ast.NotIn : "not in"
|
||||
}
|
||||
|
||||
unary_map = {
|
||||
ast.Not : "not %s",
|
||||
ast.Invert : "~%s",
|
||||
ast.USub : "-%s",
|
||||
ast.UAdd : "+%s"
|
||||
}
|
||||
|
||||
|
||||
class DebugInterpreter(ast.NodeVisitor):
|
||||
"""Interpret AST nodes to gleam useful debugging information. """
|
||||
|
||||
def __init__(self, frame):
|
||||
self.frame = frame
|
||||
|
||||
def generic_visit(self, node):
|
||||
# Fallback when we don't have a special implementation.
|
||||
if _is_ast_expr(node):
|
||||
mod = ast.Expression(node)
|
||||
co = self._compile(mod)
|
||||
try:
|
||||
result = self.frame.eval(co)
|
||||
except Exception:
|
||||
raise Failure()
|
||||
explanation = self.frame.repr(result)
|
||||
return explanation, result
|
||||
elif _is_ast_stmt(node):
|
||||
mod = ast.Module([node])
|
||||
co = self._compile(mod, "exec")
|
||||
try:
|
||||
self.frame.exec_(co)
|
||||
except Exception:
|
||||
raise Failure()
|
||||
return None, None
|
||||
else:
|
||||
raise AssertionError("can't handle %s" %(node,))
|
||||
|
||||
def _compile(self, source, mode="eval"):
|
||||
return compile(source, "<assertion interpretation>", mode)
|
||||
|
||||
def visit_Expr(self, expr):
|
||||
return self.visit(expr.value)
|
||||
|
||||
def visit_Module(self, mod):
|
||||
for stmt in mod.body:
|
||||
self.visit(stmt)
|
||||
|
||||
def visit_Name(self, name):
|
||||
explanation, result = self.generic_visit(name)
|
||||
# See if the name is local.
|
||||
source = "%r in locals() is not globals()" % (name.id,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
local = self.frame.eval(co)
|
||||
except Exception:
|
||||
# have to assume it isn't
|
||||
local = None
|
||||
if local is None or not self.frame.is_true(local):
|
||||
return name.id, result
|
||||
return explanation, result
|
||||
|
||||
def visit_Compare(self, comp):
|
||||
left = comp.left
|
||||
left_explanation, left_result = self.visit(left)
|
||||
for op, next_op in zip(comp.ops, comp.comparators):
|
||||
next_explanation, next_result = self.visit(next_op)
|
||||
op_symbol = operator_map[op.__class__]
|
||||
explanation = "%s %s %s" % (left_explanation, op_symbol,
|
||||
next_explanation)
|
||||
source = "__exprinfo_left %s __exprinfo_right" % (op_symbol,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_left=left_result,
|
||||
__exprinfo_right=next_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
try:
|
||||
if not self.frame.is_true(result):
|
||||
break
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
break
|
||||
left_explanation, left_result = next_explanation, next_result
|
||||
|
||||
if util._reprcompare is not None:
|
||||
res = util._reprcompare(op_symbol, left_result, next_result)
|
||||
if res:
|
||||
explanation = res
|
||||
return explanation, result
|
||||
|
||||
def visit_BoolOp(self, boolop):
|
||||
is_or = isinstance(boolop.op, ast.Or)
|
||||
explanations = []
|
||||
for operand in boolop.values:
|
||||
explanation, result = self.visit(operand)
|
||||
explanations.append(explanation)
|
||||
if result == is_or:
|
||||
break
|
||||
name = is_or and " or " or " and "
|
||||
explanation = "(" + name.join(explanations) + ")"
|
||||
return explanation, result
|
||||
|
||||
def visit_UnaryOp(self, unary):
|
||||
pattern = unary_map[unary.op.__class__]
|
||||
operand_explanation, operand_result = self.visit(unary.operand)
|
||||
explanation = pattern % (operand_explanation,)
|
||||
co = self._compile(pattern % ("__exprinfo_expr",))
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_expr=operand_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, result
|
||||
|
||||
def visit_BinOp(self, binop):
|
||||
left_explanation, left_result = self.visit(binop.left)
|
||||
right_explanation, right_result = self.visit(binop.right)
|
||||
symbol = operator_map[binop.op.__class__]
|
||||
explanation = "(%s %s %s)" % (left_explanation, symbol,
|
||||
right_explanation)
|
||||
source = "__exprinfo_left %s __exprinfo_right" % (symbol,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_left=left_result,
|
||||
__exprinfo_right=right_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, result
|
||||
|
||||
def visit_Call(self, call):
|
||||
func_explanation, func = self.visit(call.func)
|
||||
arg_explanations = []
|
||||
ns = {"__exprinfo_func" : func}
|
||||
arguments = []
|
||||
for arg in call.args:
|
||||
arg_explanation, arg_result = self.visit(arg)
|
||||
arg_name = "__exprinfo_%s" % (len(ns),)
|
||||
ns[arg_name] = arg_result
|
||||
arguments.append(arg_name)
|
||||
arg_explanations.append(arg_explanation)
|
||||
for keyword in call.keywords:
|
||||
arg_explanation, arg_result = self.visit(keyword.value)
|
||||
arg_name = "__exprinfo_%s" % (len(ns),)
|
||||
ns[arg_name] = arg_result
|
||||
keyword_source = "%s=%%s" % (keyword.arg)
|
||||
arguments.append(keyword_source % (arg_name,))
|
||||
arg_explanations.append(keyword_source % (arg_explanation,))
|
||||
if call.starargs:
|
||||
arg_explanation, arg_result = self.visit(call.starargs)
|
||||
arg_name = "__exprinfo_star"
|
||||
ns[arg_name] = arg_result
|
||||
arguments.append("*%s" % (arg_name,))
|
||||
arg_explanations.append("*%s" % (arg_explanation,))
|
||||
if call.kwargs:
|
||||
arg_explanation, arg_result = self.visit(call.kwargs)
|
||||
arg_name = "__exprinfo_kwds"
|
||||
ns[arg_name] = arg_result
|
||||
arguments.append("**%s" % (arg_name,))
|
||||
arg_explanations.append("**%s" % (arg_explanation,))
|
||||
args_explained = ", ".join(arg_explanations)
|
||||
explanation = "%s(%s)" % (func_explanation, args_explained)
|
||||
args = ", ".join(arguments)
|
||||
source = "__exprinfo_func(%s)" % (args,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, **ns)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
pattern = "%s\n{%s = %s\n}"
|
||||
rep = self.frame.repr(result)
|
||||
explanation = pattern % (rep, rep, explanation)
|
||||
return explanation, result
|
||||
|
||||
def _is_builtin_name(self, name):
|
||||
pattern = "%r not in globals() and %r not in locals()"
|
||||
source = pattern % (name.id, name.id)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
return self.frame.eval(co)
|
||||
except Exception:
|
||||
return False
|
||||
|
||||
def visit_Attribute(self, attr):
|
||||
if not isinstance(attr.ctx, ast.Load):
|
||||
return self.generic_visit(attr)
|
||||
source_explanation, source_result = self.visit(attr.value)
|
||||
explanation = "%s.%s" % (source_explanation, attr.attr)
|
||||
source = "__exprinfo_expr.%s" % (attr.attr,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
result = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
explanation = "%s\n{%s = %s.%s\n}" % (self.frame.repr(result),
|
||||
self.frame.repr(result),
|
||||
source_explanation, attr.attr)
|
||||
# Check if the attr is from an instance.
|
||||
source = "%r in getattr(__exprinfo_expr, '__dict__', {})"
|
||||
source = source % (attr.attr,)
|
||||
co = self._compile(source)
|
||||
try:
|
||||
from_instance = self.frame.eval(co, __exprinfo_expr=source_result)
|
||||
except Exception:
|
||||
from_instance = None
|
||||
if from_instance is None or self.frame.is_true(from_instance):
|
||||
rep = self.frame.repr(result)
|
||||
pattern = "%s\n{%s = %s\n}"
|
||||
explanation = pattern % (rep, rep, explanation)
|
||||
return explanation, result
|
||||
|
||||
def visit_Assert(self, assrt):
|
||||
test_explanation, test_result = self.visit(assrt.test)
|
||||
explanation = "assert %s" % (test_explanation,)
|
||||
if not self.frame.is_true(test_result):
|
||||
try:
|
||||
raise BuiltinAssertionError
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, test_result
|
||||
|
||||
def visit_Assign(self, assign):
|
||||
value_explanation, value_result = self.visit(assign.value)
|
||||
explanation = "... = %s" % (value_explanation,)
|
||||
name = ast.Name("__exprinfo_expr", ast.Load(),
|
||||
lineno=assign.value.lineno,
|
||||
col_offset=assign.value.col_offset)
|
||||
new_assign = ast.Assign(assign.targets, name, lineno=assign.lineno,
|
||||
col_offset=assign.col_offset)
|
||||
mod = ast.Module([new_assign])
|
||||
co = self._compile(mod, "exec")
|
||||
try:
|
||||
self.frame.exec_(co, __exprinfo_expr=value_result)
|
||||
except Exception:
|
||||
raise Failure(explanation)
|
||||
return explanation, value_result
|
||||
552
_pytest/assertion/oldinterpret.py
Normal file
552
_pytest/assertion/oldinterpret.py
Normal file
@@ -0,0 +1,552 @@
|
||||
import py
|
||||
import sys, inspect
|
||||
from compiler import parse, ast, pycodegen
|
||||
from _pytest.assertion.util import format_explanation
|
||||
from _pytest.assertion.reinterpret import BuiltinAssertionError
|
||||
|
||||
passthroughex = py.builtin._sysex
|
||||
|
||||
class Failure:
|
||||
def __init__(self, node):
|
||||
self.exc, self.value, self.tb = sys.exc_info()
|
||||
self.node = node
|
||||
|
||||
class View(object):
|
||||
"""View base class.
|
||||
|
||||
If C is a subclass of View, then C(x) creates a proxy object around
|
||||
the object x. The actual class of the proxy is not C in general,
|
||||
but a *subclass* of C determined by the rules below. To avoid confusion
|
||||
we call view class the class of the proxy (a subclass of C, so of View)
|
||||
and object class the class of x.
|
||||
|
||||
Attributes and methods not found in the proxy are automatically read on x.
|
||||
Other operations like setting attributes are performed on the proxy, as
|
||||
determined by its view class. The object x is available from the proxy
|
||||
as its __obj__ attribute.
|
||||
|
||||
The view class selection is determined by the __view__ tuples and the
|
||||
optional __viewkey__ method. By default, the selected view class is the
|
||||
most specific subclass of C whose __view__ mentions the class of x.
|
||||
If no such subclass is found, the search proceeds with the parent
|
||||
object classes. For example, C(True) will first look for a subclass
|
||||
of C with __view__ = (..., bool, ...) and only if it doesn't find any
|
||||
look for one with __view__ = (..., int, ...), and then ..., object,...
|
||||
If everything fails the class C itself is considered to be the default.
|
||||
|
||||
Alternatively, the view class selection can be driven by another aspect
|
||||
of the object x, instead of the class of x, by overriding __viewkey__.
|
||||
See last example at the end of this module.
|
||||
"""
|
||||
|
||||
_viewcache = {}
|
||||
__view__ = ()
|
||||
|
||||
def __new__(rootclass, obj, *args, **kwds):
|
||||
self = object.__new__(rootclass)
|
||||
self.__obj__ = obj
|
||||
self.__rootclass__ = rootclass
|
||||
key = self.__viewkey__()
|
||||
try:
|
||||
self.__class__ = self._viewcache[key]
|
||||
except KeyError:
|
||||
self.__class__ = self._selectsubclass(key)
|
||||
return self
|
||||
|
||||
def __getattr__(self, attr):
|
||||
# attributes not found in the normal hierarchy rooted on View
|
||||
# are looked up in the object's real class
|
||||
return getattr(self.__obj__, attr)
|
||||
|
||||
def __viewkey__(self):
|
||||
return self.__obj__.__class__
|
||||
|
||||
def __matchkey__(self, key, subclasses):
|
||||
if inspect.isclass(key):
|
||||
keys = inspect.getmro(key)
|
||||
else:
|
||||
keys = [key]
|
||||
for key in keys:
|
||||
result = [C for C in subclasses if key in C.__view__]
|
||||
if result:
|
||||
return result
|
||||
return []
|
||||
|
||||
def _selectsubclass(self, key):
|
||||
subclasses = list(enumsubclasses(self.__rootclass__))
|
||||
for C in subclasses:
|
||||
if not isinstance(C.__view__, tuple):
|
||||
C.__view__ = (C.__view__,)
|
||||
choices = self.__matchkey__(key, subclasses)
|
||||
if not choices:
|
||||
return self.__rootclass__
|
||||
elif len(choices) == 1:
|
||||
return choices[0]
|
||||
else:
|
||||
# combine the multiple choices
|
||||
return type('?', tuple(choices), {})
|
||||
|
||||
def __repr__(self):
|
||||
return '%s(%r)' % (self.__rootclass__.__name__, self.__obj__)
|
||||
|
||||
|
||||
def enumsubclasses(cls):
|
||||
for subcls in cls.__subclasses__():
|
||||
for subsubclass in enumsubclasses(subcls):
|
||||
yield subsubclass
|
||||
yield cls
|
||||
|
||||
|
||||
class Interpretable(View):
|
||||
"""A parse tree node with a few extra methods."""
|
||||
explanation = None
|
||||
|
||||
def is_builtin(self, frame):
|
||||
return False
|
||||
|
||||
def eval(self, frame):
|
||||
# fall-back for unknown expression nodes
|
||||
try:
|
||||
expr = ast.Expression(self.__obj__)
|
||||
expr.filename = '<eval>'
|
||||
self.__obj__.filename = '<eval>'
|
||||
co = pycodegen.ExpressionCodeGenerator(expr).getCode()
|
||||
result = frame.eval(co)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
self.result = result
|
||||
self.explanation = self.explanation or frame.repr(self.result)
|
||||
|
||||
def run(self, frame):
|
||||
# fall-back for unknown statement nodes
|
||||
try:
|
||||
expr = ast.Module(None, ast.Stmt([self.__obj__]))
|
||||
expr.filename = '<run>'
|
||||
co = pycodegen.ModuleCodeGenerator(expr).getCode()
|
||||
frame.exec_(co)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
def nice_explanation(self):
|
||||
return format_explanation(self.explanation)
|
||||
|
||||
|
||||
class Name(Interpretable):
|
||||
__view__ = ast.Name
|
||||
|
||||
def is_local(self, frame):
|
||||
source = '%r in locals() is not globals()' % self.name
|
||||
try:
|
||||
return frame.is_true(frame.eval(source))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def is_global(self, frame):
|
||||
source = '%r in globals()' % self.name
|
||||
try:
|
||||
return frame.is_true(frame.eval(source))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def is_builtin(self, frame):
|
||||
source = '%r not in locals() and %r not in globals()' % (
|
||||
self.name, self.name)
|
||||
try:
|
||||
return frame.is_true(frame.eval(source))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def eval(self, frame):
|
||||
super(Name, self).eval(frame)
|
||||
if not self.is_local(frame):
|
||||
self.explanation = self.name
|
||||
|
||||
class Compare(Interpretable):
|
||||
__view__ = ast.Compare
|
||||
|
||||
def eval(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
for operation, expr2 in self.ops:
|
||||
if hasattr(self, 'result'):
|
||||
# shortcutting in chained expressions
|
||||
if not frame.is_true(self.result):
|
||||
break
|
||||
expr2 = Interpretable(expr2)
|
||||
expr2.eval(frame)
|
||||
self.explanation = "%s %s %s" % (
|
||||
expr.explanation, operation, expr2.explanation)
|
||||
source = "__exprinfo_left %s __exprinfo_right" % operation
|
||||
try:
|
||||
self.result = frame.eval(source,
|
||||
__exprinfo_left=expr.result,
|
||||
__exprinfo_right=expr2.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
expr = expr2
|
||||
|
||||
class And(Interpretable):
|
||||
__view__ = ast.And
|
||||
|
||||
def eval(self, frame):
|
||||
explanations = []
|
||||
for expr in self.nodes:
|
||||
expr = Interpretable(expr)
|
||||
expr.eval(frame)
|
||||
explanations.append(expr.explanation)
|
||||
self.result = expr.result
|
||||
if not frame.is_true(expr.result):
|
||||
break
|
||||
self.explanation = '(' + ' and '.join(explanations) + ')'
|
||||
|
||||
class Or(Interpretable):
|
||||
__view__ = ast.Or
|
||||
|
||||
def eval(self, frame):
|
||||
explanations = []
|
||||
for expr in self.nodes:
|
||||
expr = Interpretable(expr)
|
||||
expr.eval(frame)
|
||||
explanations.append(expr.explanation)
|
||||
self.result = expr.result
|
||||
if frame.is_true(expr.result):
|
||||
break
|
||||
self.explanation = '(' + ' or '.join(explanations) + ')'
|
||||
|
||||
|
||||
# == Unary operations ==
|
||||
keepalive = []
|
||||
for astclass, astpattern in {
|
||||
ast.Not : 'not __exprinfo_expr',
|
||||
ast.Invert : '(~__exprinfo_expr)',
|
||||
}.items():
|
||||
|
||||
class UnaryArith(Interpretable):
|
||||
__view__ = astclass
|
||||
|
||||
def eval(self, frame, astpattern=astpattern):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
self.explanation = astpattern.replace('__exprinfo_expr',
|
||||
expr.explanation)
|
||||
try:
|
||||
self.result = frame.eval(astpattern,
|
||||
__exprinfo_expr=expr.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
keepalive.append(UnaryArith)
|
||||
|
||||
# == Binary operations ==
|
||||
for astclass, astpattern in {
|
||||
ast.Add : '(__exprinfo_left + __exprinfo_right)',
|
||||
ast.Sub : '(__exprinfo_left - __exprinfo_right)',
|
||||
ast.Mul : '(__exprinfo_left * __exprinfo_right)',
|
||||
ast.Div : '(__exprinfo_left / __exprinfo_right)',
|
||||
ast.Mod : '(__exprinfo_left % __exprinfo_right)',
|
||||
ast.Power : '(__exprinfo_left ** __exprinfo_right)',
|
||||
}.items():
|
||||
|
||||
class BinaryArith(Interpretable):
|
||||
__view__ = astclass
|
||||
|
||||
def eval(self, frame, astpattern=astpattern):
|
||||
left = Interpretable(self.left)
|
||||
left.eval(frame)
|
||||
right = Interpretable(self.right)
|
||||
right.eval(frame)
|
||||
self.explanation = (astpattern
|
||||
.replace('__exprinfo_left', left .explanation)
|
||||
.replace('__exprinfo_right', right.explanation))
|
||||
try:
|
||||
self.result = frame.eval(astpattern,
|
||||
__exprinfo_left=left.result,
|
||||
__exprinfo_right=right.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
keepalive.append(BinaryArith)
|
||||
|
||||
|
||||
class CallFunc(Interpretable):
|
||||
__view__ = ast.CallFunc
|
||||
|
||||
def is_bool(self, frame):
|
||||
source = 'isinstance(__exprinfo_value, bool)'
|
||||
try:
|
||||
return frame.is_true(frame.eval(source,
|
||||
__exprinfo_value=self.result))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
return False
|
||||
|
||||
def eval(self, frame):
|
||||
node = Interpretable(self.node)
|
||||
node.eval(frame)
|
||||
explanations = []
|
||||
vars = {'__exprinfo_fn': node.result}
|
||||
source = '__exprinfo_fn('
|
||||
for a in self.args:
|
||||
if isinstance(a, ast.Keyword):
|
||||
keyword = a.name
|
||||
a = a.expr
|
||||
else:
|
||||
keyword = None
|
||||
a = Interpretable(a)
|
||||
a.eval(frame)
|
||||
argname = '__exprinfo_%d' % len(vars)
|
||||
vars[argname] = a.result
|
||||
if keyword is None:
|
||||
source += argname + ','
|
||||
explanations.append(a.explanation)
|
||||
else:
|
||||
source += '%s=%s,' % (keyword, argname)
|
||||
explanations.append('%s=%s' % (keyword, a.explanation))
|
||||
if self.star_args:
|
||||
star_args = Interpretable(self.star_args)
|
||||
star_args.eval(frame)
|
||||
argname = '__exprinfo_star'
|
||||
vars[argname] = star_args.result
|
||||
source += '*' + argname + ','
|
||||
explanations.append('*' + star_args.explanation)
|
||||
if self.dstar_args:
|
||||
dstar_args = Interpretable(self.dstar_args)
|
||||
dstar_args.eval(frame)
|
||||
argname = '__exprinfo_kwds'
|
||||
vars[argname] = dstar_args.result
|
||||
source += '**' + argname + ','
|
||||
explanations.append('**' + dstar_args.explanation)
|
||||
self.explanation = "%s(%s)" % (
|
||||
node.explanation, ', '.join(explanations))
|
||||
if source.endswith(','):
|
||||
source = source[:-1]
|
||||
source += ')'
|
||||
try:
|
||||
self.result = frame.eval(source, **vars)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
if not node.is_builtin(frame) or not self.is_bool(frame):
|
||||
r = frame.repr(self.result)
|
||||
self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation)
|
||||
|
||||
class Getattr(Interpretable):
|
||||
__view__ = ast.Getattr
|
||||
|
||||
def eval(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
source = '__exprinfo_expr.%s' % self.attrname
|
||||
try:
|
||||
self.result = frame.eval(source, __exprinfo_expr=expr.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
self.explanation = '%s.%s' % (expr.explanation, self.attrname)
|
||||
# if the attribute comes from the instance, its value is interesting
|
||||
source = ('hasattr(__exprinfo_expr, "__dict__") and '
|
||||
'%r in __exprinfo_expr.__dict__' % self.attrname)
|
||||
try:
|
||||
from_instance = frame.is_true(
|
||||
frame.eval(source, __exprinfo_expr=expr.result))
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
from_instance = True
|
||||
if from_instance:
|
||||
r = frame.repr(self.result)
|
||||
self.explanation = '%s\n{%s = %s\n}' % (r, r, self.explanation)
|
||||
|
||||
# == Re-interpretation of full statements ==
|
||||
|
||||
class Assert(Interpretable):
|
||||
__view__ = ast.Assert
|
||||
|
||||
def run(self, frame):
|
||||
test = Interpretable(self.test)
|
||||
test.eval(frame)
|
||||
# print the result as 'assert <explanation>'
|
||||
self.result = test.result
|
||||
self.explanation = 'assert ' + test.explanation
|
||||
if not frame.is_true(test.result):
|
||||
try:
|
||||
raise BuiltinAssertionError
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
class Assign(Interpretable):
|
||||
__view__ = ast.Assign
|
||||
|
||||
def run(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
self.result = expr.result
|
||||
self.explanation = '... = ' + expr.explanation
|
||||
# fall-back-run the rest of the assignment
|
||||
ass = ast.Assign(self.nodes, ast.Name('__exprinfo_expr'))
|
||||
mod = ast.Module(None, ast.Stmt([ass]))
|
||||
mod.filename = '<run>'
|
||||
co = pycodegen.ModuleCodeGenerator(mod).getCode()
|
||||
try:
|
||||
frame.exec_(co, __exprinfo_expr=expr.result)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
raise Failure(self)
|
||||
|
||||
class Discard(Interpretable):
|
||||
__view__ = ast.Discard
|
||||
|
||||
def run(self, frame):
|
||||
expr = Interpretable(self.expr)
|
||||
expr.eval(frame)
|
||||
self.result = expr.result
|
||||
self.explanation = expr.explanation
|
||||
|
||||
class Stmt(Interpretable):
|
||||
__view__ = ast.Stmt
|
||||
|
||||
def run(self, frame):
|
||||
for stmt in self.nodes:
|
||||
stmt = Interpretable(stmt)
|
||||
stmt.run(frame)
|
||||
|
||||
|
||||
def report_failure(e):
|
||||
explanation = e.node.nice_explanation()
|
||||
if explanation:
|
||||
explanation = ", in: " + explanation
|
||||
else:
|
||||
explanation = ""
|
||||
sys.stdout.write("%s: %s%s\n" % (e.exc.__name__, e.value, explanation))
|
||||
|
||||
def check(s, frame=None):
|
||||
if frame is None:
|
||||
frame = sys._getframe(1)
|
||||
frame = py.code.Frame(frame)
|
||||
expr = parse(s, 'eval')
|
||||
assert isinstance(expr, ast.Expression)
|
||||
node = Interpretable(expr.node)
|
||||
try:
|
||||
node.eval(frame)
|
||||
except passthroughex:
|
||||
raise
|
||||
except Failure:
|
||||
e = sys.exc_info()[1]
|
||||
report_failure(e)
|
||||
else:
|
||||
if not frame.is_true(node.result):
|
||||
sys.stderr.write("assertion failed: %s\n" % node.nice_explanation())
|
||||
|
||||
|
||||
###########################################################
|
||||
# API / Entry points
|
||||
# #########################################################
|
||||
|
||||
def interpret(source, frame, should_fail=False):
|
||||
module = Interpretable(parse(source, 'exec').node)
|
||||
#print "got module", module
|
||||
if isinstance(frame, py.std.types.FrameType):
|
||||
frame = py.code.Frame(frame)
|
||||
try:
|
||||
module.run(frame)
|
||||
except Failure:
|
||||
e = sys.exc_info()[1]
|
||||
return getfailure(e)
|
||||
except passthroughex:
|
||||
raise
|
||||
except:
|
||||
import traceback
|
||||
traceback.print_exc()
|
||||
if should_fail:
|
||||
return ("(assertion failed, but when it was re-run for "
|
||||
"printing intermediate values, it did not fail. Suggestions: "
|
||||
"compute assert expression before the assert or use --assert=plain)")
|
||||
else:
|
||||
return None
|
||||
|
||||
def getmsg(excinfo):
|
||||
if isinstance(excinfo, tuple):
|
||||
excinfo = py.code.ExceptionInfo(excinfo)
|
||||
#frame, line = gettbline(tb)
|
||||
#frame = py.code.Frame(frame)
|
||||
#return interpret(line, frame)
|
||||
|
||||
tb = excinfo.traceback[-1]
|
||||
source = str(tb.statement).strip()
|
||||
x = interpret(source, tb.frame, should_fail=True)
|
||||
if not isinstance(x, str):
|
||||
raise TypeError("interpret returned non-string %r" % (x,))
|
||||
return x
|
||||
|
||||
def getfailure(e):
|
||||
explanation = e.node.nice_explanation()
|
||||
if str(e.value):
|
||||
lines = explanation.split('\n')
|
||||
lines[0] += " << %s" % (e.value,)
|
||||
explanation = '\n'.join(lines)
|
||||
text = "%s: %s" % (e.exc.__name__, explanation)
|
||||
if text.startswith('AssertionError: assert '):
|
||||
text = text[16:]
|
||||
return text
|
||||
|
||||
def run(s, frame=None):
|
||||
if frame is None:
|
||||
frame = sys._getframe(1)
|
||||
frame = py.code.Frame(frame)
|
||||
module = Interpretable(parse(s, 'exec').node)
|
||||
try:
|
||||
module.run(frame)
|
||||
except Failure:
|
||||
e = sys.exc_info()[1]
|
||||
report_failure(e)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# example:
|
||||
def f():
|
||||
return 5
|
||||
def g():
|
||||
return 3
|
||||
def h(x):
|
||||
return 'never'
|
||||
check("f() * g() == 5")
|
||||
check("not f()")
|
||||
check("not (f() and g() or 0)")
|
||||
check("f() == g()")
|
||||
i = 4
|
||||
check("i == f()")
|
||||
check("len(f()) == 0")
|
||||
check("isinstance(2+3+4, float)")
|
||||
|
||||
run("x = i")
|
||||
check("x == 5")
|
||||
|
||||
run("assert not f(), 'oops'")
|
||||
run("a, b, c = 1, 2")
|
||||
run("a, b, c = f()")
|
||||
|
||||
check("max([f(),g()]) == 4")
|
||||
check("'hello'[g()] == 'h'")
|
||||
run("'guk%d' % h(f())")
|
||||
48
_pytest/assertion/reinterpret.py
Normal file
48
_pytest/assertion/reinterpret.py
Normal file
@@ -0,0 +1,48 @@
|
||||
import sys
|
||||
import py
|
||||
|
||||
BuiltinAssertionError = py.builtin.builtins.AssertionError
|
||||
|
||||
class AssertionError(BuiltinAssertionError):
|
||||
def __init__(self, *args):
|
||||
BuiltinAssertionError.__init__(self, *args)
|
||||
if args:
|
||||
try:
|
||||
self.msg = str(args[0])
|
||||
except py.builtin._sysex:
|
||||
raise
|
||||
except:
|
||||
self.msg = "<[broken __repr__] %s at %0xd>" %(
|
||||
args[0].__class__, id(args[0]))
|
||||
else:
|
||||
f = py.code.Frame(sys._getframe(1))
|
||||
try:
|
||||
source = f.code.fullsource
|
||||
if source is not None:
|
||||
try:
|
||||
source = source.getstatement(f.lineno, assertion=True)
|
||||
except IndexError:
|
||||
source = None
|
||||
else:
|
||||
source = str(source.deindent()).strip()
|
||||
except py.error.ENOENT:
|
||||
source = None
|
||||
# this can also occur during reinterpretation, when the
|
||||
# co_filename is set to "<run>".
|
||||
if source:
|
||||
self.msg = reinterpret(source, f, should_fail=True)
|
||||
else:
|
||||
self.msg = "<could not determine information>"
|
||||
if not self.args:
|
||||
self.args = (self.msg,)
|
||||
|
||||
if sys.version_info > (3, 0):
|
||||
AssertionError.__module__ = "builtins"
|
||||
reinterpret_old = "old reinterpretation not available for py3"
|
||||
else:
|
||||
from _pytest.assertion.oldinterpret import interpret as reinterpret_old
|
||||
if sys.version_info >= (2, 6) or (sys.platform.startswith("java")):
|
||||
from _pytest.assertion.newinterpret import interpret as reinterpret
|
||||
else:
|
||||
reinterpret = reinterpret_old
|
||||
|
||||
598
_pytest/assertion/rewrite.py
Normal file
598
_pytest/assertion/rewrite.py
Normal file
@@ -0,0 +1,598 @@
|
||||
"""Rewrite assertion AST to produce nice error messages"""
|
||||
|
||||
import ast
|
||||
import collections
|
||||
import errno
|
||||
import itertools
|
||||
import imp
|
||||
import marshal
|
||||
import os
|
||||
import struct
|
||||
import sys
|
||||
import types
|
||||
|
||||
import py
|
||||
from _pytest.assertion import util
|
||||
|
||||
|
||||
# Windows gives ENOENT in places *nix gives ENOTDIR.
|
||||
if sys.platform.startswith("win"):
|
||||
PATH_COMPONENT_NOT_DIR = errno.ENOENT
|
||||
else:
|
||||
PATH_COMPONENT_NOT_DIR = errno.ENOTDIR
|
||||
|
||||
# py.test caches rewritten pycs in __pycache__.
|
||||
if hasattr(imp, "get_tag"):
|
||||
PYTEST_TAG = imp.get_tag() + "-PYTEST"
|
||||
else:
|
||||
if hasattr(sys, "pypy_version_info"):
|
||||
impl = "pypy"
|
||||
elif sys.platform == "java":
|
||||
impl = "jython"
|
||||
else:
|
||||
impl = "cpython"
|
||||
ver = sys.version_info
|
||||
PYTEST_TAG = "%s-%s%s-PYTEST" % (impl, ver[0], ver[1])
|
||||
del ver, impl
|
||||
|
||||
PYC_EXT = ".py" + "c" if __debug__ else "o"
|
||||
PYC_TAIL = "." + PYTEST_TAG + PYC_EXT
|
||||
|
||||
REWRITE_NEWLINES = sys.version_info[:2] != (2, 7) and sys.version_info < (3, 2)
|
||||
|
||||
class AssertionRewritingHook(object):
|
||||
"""Import hook which rewrites asserts."""
|
||||
|
||||
def __init__(self):
|
||||
self.session = None
|
||||
self.modules = {}
|
||||
|
||||
def set_session(self, session):
|
||||
self.fnpats = session.config.getini("python_files")
|
||||
self.session = session
|
||||
|
||||
def find_module(self, name, path=None):
|
||||
if self.session is None:
|
||||
return None
|
||||
sess = self.session
|
||||
state = sess.config._assertstate
|
||||
state.trace("find_module called for: %s" % name)
|
||||
names = name.rsplit(".", 1)
|
||||
lastname = names[-1]
|
||||
pth = None
|
||||
if path is not None and len(path) == 1:
|
||||
pth = path[0]
|
||||
if pth is None:
|
||||
try:
|
||||
fd, fn, desc = imp.find_module(lastname, path)
|
||||
except ImportError:
|
||||
return None
|
||||
if fd is not None:
|
||||
fd.close()
|
||||
tp = desc[2]
|
||||
if tp == imp.PY_COMPILED:
|
||||
if hasattr(imp, "source_from_cache"):
|
||||
fn = imp.source_from_cache(fn)
|
||||
else:
|
||||
fn = fn[:-1]
|
||||
elif tp != imp.PY_SOURCE:
|
||||
# Don't know what this is.
|
||||
return None
|
||||
else:
|
||||
fn = os.path.join(pth, name.rpartition(".")[2] + ".py")
|
||||
fn_pypath = py.path.local(fn)
|
||||
# Is this a test file?
|
||||
if not sess.isinitpath(fn):
|
||||
# We have to be very careful here because imports in this code can
|
||||
# trigger a cycle.
|
||||
self.session = None
|
||||
try:
|
||||
for pat in self.fnpats:
|
||||
if fn_pypath.fnmatch(pat):
|
||||
state.trace("matched test file %r" % (fn,))
|
||||
break
|
||||
else:
|
||||
return None
|
||||
finally:
|
||||
self.session = sess
|
||||
else:
|
||||
state.trace("matched test file (was specified on cmdline): %r" % (fn,))
|
||||
# The requested module looks like a test file, so rewrite it. This is
|
||||
# the most magical part of the process: load the source, rewrite the
|
||||
# asserts, and load the rewritten source. We also cache the rewritten
|
||||
# module code in a special pyc. We must be aware of the possibility of
|
||||
# concurrent py.test processes rewriting and loading pycs. To avoid
|
||||
# tricky race conditions, we maintain the following invariant: The
|
||||
# cached pyc is always a complete, valid pyc. Operations on it must be
|
||||
# atomic. POSIX's atomic rename comes in handy.
|
||||
write = not sys.dont_write_bytecode
|
||||
cache_dir = os.path.join(fn_pypath.dirname, "__pycache__")
|
||||
if write:
|
||||
try:
|
||||
os.mkdir(cache_dir)
|
||||
except OSError:
|
||||
e = sys.exc_info()[1].errno
|
||||
if e == errno.EEXIST:
|
||||
# Either the __pycache__ directory already exists (the
|
||||
# common case) or it's blocked by a non-dir node. In the
|
||||
# latter case, we'll ignore it in _write_pyc.
|
||||
pass
|
||||
elif e == PATH_COMPONENT_NOT_DIR:
|
||||
# One of the path components was not a directory, likely
|
||||
# because we're in a zip file.
|
||||
write = False
|
||||
elif e == errno.EACCES:
|
||||
state.trace("read only directory: %r" % (fn_pypath.dirname,))
|
||||
write = False
|
||||
else:
|
||||
raise
|
||||
cache_name = fn_pypath.basename[:-3] + PYC_TAIL
|
||||
pyc = os.path.join(cache_dir, cache_name)
|
||||
# Notice that even if we're in a read-only directory, I'm going to check
|
||||
# for a cached pyc. This may not be optimal...
|
||||
co = _read_pyc(fn_pypath, pyc)
|
||||
if co is None:
|
||||
state.trace("rewriting %r" % (fn,))
|
||||
co = _rewrite_test(state, fn_pypath)
|
||||
if co is None:
|
||||
# Probably a SyntaxError in the test.
|
||||
return None
|
||||
if write:
|
||||
_make_rewritten_pyc(state, fn_pypath, pyc, co)
|
||||
else:
|
||||
state.trace("found cached rewritten pyc for %r" % (fn,))
|
||||
self.modules[name] = co, pyc
|
||||
return self
|
||||
|
||||
def load_module(self, name):
|
||||
co, pyc = self.modules.pop(name)
|
||||
# I wish I could just call imp.load_compiled here, but __file__ has to
|
||||
# be set properly. In Python 3.2+, this all would be handled correctly
|
||||
# by load_compiled.
|
||||
mod = sys.modules[name] = imp.new_module(name)
|
||||
try:
|
||||
mod.__file__ = co.co_filename
|
||||
# Normally, this attribute is 3.2+.
|
||||
mod.__cached__ = pyc
|
||||
py.builtin.exec_(co, mod.__dict__)
|
||||
except:
|
||||
del sys.modules[name]
|
||||
raise
|
||||
return sys.modules[name]
|
||||
|
||||
def _write_pyc(co, source_path, pyc):
|
||||
# Technically, we don't have to have the same pyc format as (C)Python, since
|
||||
# these "pycs" should never be seen by builtin import. However, there's
|
||||
# little reason deviate, and I hope sometime to be able to use
|
||||
# imp.load_compiled to load them. (See the comment in load_module above.)
|
||||
mtime = int(source_path.mtime())
|
||||
try:
|
||||
fp = open(pyc, "wb")
|
||||
except IOError:
|
||||
err = sys.exc_info()[1].errno
|
||||
if err == PATH_COMPONENT_NOT_DIR:
|
||||
# This happens when we get a EEXIST in find_module creating the
|
||||
# __pycache__ directory and __pycache__ is by some non-dir node.
|
||||
return False
|
||||
raise
|
||||
try:
|
||||
fp.write(imp.get_magic())
|
||||
fp.write(struct.pack("<l", mtime))
|
||||
marshal.dump(co, fp)
|
||||
finally:
|
||||
fp.close()
|
||||
return True
|
||||
|
||||
RN = "\r\n".encode("utf-8")
|
||||
N = "\n".encode("utf-8")
|
||||
|
||||
def _rewrite_test(state, fn):
|
||||
"""Try to read and rewrite *fn* and return the code object."""
|
||||
try:
|
||||
source = fn.read("rb")
|
||||
except EnvironmentError:
|
||||
return None
|
||||
# On Python versions which are not 2.7 and less than or equal to 3.1, the
|
||||
# parser expects *nix newlines.
|
||||
if REWRITE_NEWLINES:
|
||||
source = source.replace(RN, N) + N
|
||||
try:
|
||||
tree = ast.parse(source)
|
||||
except SyntaxError:
|
||||
# Let this pop up again in the real import.
|
||||
state.trace("failed to parse: %r" % (fn,))
|
||||
return None
|
||||
rewrite_asserts(tree)
|
||||
try:
|
||||
co = compile(tree, fn.strpath, "exec")
|
||||
except SyntaxError:
|
||||
# It's possible that this error is from some bug in the
|
||||
# assertion rewriting, but I don't know of a fast way to tell.
|
||||
state.trace("failed to compile: %r" % (fn,))
|
||||
return None
|
||||
return co
|
||||
|
||||
def _make_rewritten_pyc(state, fn, pyc, co):
|
||||
"""Try to dump rewritten code to *pyc*."""
|
||||
if sys.platform.startswith("win"):
|
||||
# Windows grants exclusive access to open files and doesn't have atomic
|
||||
# rename, so just write into the final file.
|
||||
_write_pyc(co, fn, pyc)
|
||||
else:
|
||||
# When not on windows, assume rename is atomic. Dump the code object
|
||||
# into a file specific to this process and atomically replace it.
|
||||
proc_pyc = pyc + "." + str(os.getpid())
|
||||
if _write_pyc(co, fn, proc_pyc):
|
||||
os.rename(proc_pyc, pyc)
|
||||
|
||||
def _read_pyc(source, pyc):
|
||||
"""Possibly read a py.test pyc containing rewritten code.
|
||||
|
||||
Return rewritten code if successful or None if not.
|
||||
"""
|
||||
try:
|
||||
fp = open(pyc, "rb")
|
||||
except IOError:
|
||||
return None
|
||||
try:
|
||||
try:
|
||||
mtime = int(source.mtime())
|
||||
data = fp.read(8)
|
||||
except EnvironmentError:
|
||||
return None
|
||||
# Check for invalid or out of date pyc file.
|
||||
if (len(data) != 8 or
|
||||
data[:4] != imp.get_magic() or
|
||||
struct.unpack("<l", data[4:])[0] != mtime):
|
||||
return None
|
||||
co = marshal.load(fp)
|
||||
if not isinstance(co, types.CodeType):
|
||||
# That's interesting....
|
||||
return None
|
||||
return co
|
||||
finally:
|
||||
fp.close()
|
||||
|
||||
|
||||
def rewrite_asserts(mod):
|
||||
"""Rewrite the assert statements in mod."""
|
||||
AssertionRewriter().run(mod)
|
||||
|
||||
|
||||
_saferepr = py.io.saferepr
|
||||
from _pytest.assertion.util import format_explanation as _format_explanation
|
||||
|
||||
def _format_boolop(explanations, is_or):
|
||||
return "(" + (is_or and " or " or " and ").join(explanations) + ")"
|
||||
|
||||
def _call_reprcompare(ops, results, expls, each_obj):
|
||||
for i, res, expl in zip(range(len(ops)), results, expls):
|
||||
try:
|
||||
done = not res
|
||||
except Exception:
|
||||
done = True
|
||||
if done:
|
||||
break
|
||||
if util._reprcompare is not None:
|
||||
custom = util._reprcompare(ops[i], each_obj[i], each_obj[i + 1])
|
||||
if custom is not None:
|
||||
return custom
|
||||
return expl
|
||||
|
||||
|
||||
unary_map = {
|
||||
ast.Not : "not %s",
|
||||
ast.Invert : "~%s",
|
||||
ast.USub : "-%s",
|
||||
ast.UAdd : "+%s"
|
||||
}
|
||||
|
||||
binop_map = {
|
||||
ast.BitOr : "|",
|
||||
ast.BitXor : "^",
|
||||
ast.BitAnd : "&",
|
||||
ast.LShift : "<<",
|
||||
ast.RShift : ">>",
|
||||
ast.Add : "+",
|
||||
ast.Sub : "-",
|
||||
ast.Mult : "*",
|
||||
ast.Div : "/",
|
||||
ast.FloorDiv : "//",
|
||||
ast.Mod : "%",
|
||||
ast.Eq : "==",
|
||||
ast.NotEq : "!=",
|
||||
ast.Lt : "<",
|
||||
ast.LtE : "<=",
|
||||
ast.Gt : ">",
|
||||
ast.GtE : ">=",
|
||||
ast.Pow : "**",
|
||||
ast.Is : "is",
|
||||
ast.IsNot : "is not",
|
||||
ast.In : "in",
|
||||
ast.NotIn : "not in"
|
||||
}
|
||||
|
||||
|
||||
def set_location(node, lineno, col_offset):
|
||||
"""Set node location information recursively."""
|
||||
def _fix(node, lineno, col_offset):
|
||||
if "lineno" in node._attributes:
|
||||
node.lineno = lineno
|
||||
if "col_offset" in node._attributes:
|
||||
node.col_offset = col_offset
|
||||
for child in ast.iter_child_nodes(node):
|
||||
_fix(child, lineno, col_offset)
|
||||
_fix(node, lineno, col_offset)
|
||||
return node
|
||||
|
||||
|
||||
class AssertionRewriter(ast.NodeVisitor):
|
||||
|
||||
def run(self, mod):
|
||||
"""Find all assert statements in *mod* and rewrite them."""
|
||||
if not mod.body:
|
||||
# Nothing to do.
|
||||
return
|
||||
# Insert some special imports at the top of the module but after any
|
||||
# docstrings and __future__ imports.
|
||||
aliases = [ast.alias(py.builtin.builtins.__name__, "@py_builtins"),
|
||||
ast.alias("_pytest.assertion.rewrite", "@pytest_ar")]
|
||||
expect_docstring = True
|
||||
pos = 0
|
||||
lineno = 0
|
||||
for item in mod.body:
|
||||
if (expect_docstring and isinstance(item, ast.Expr) and
|
||||
isinstance(item.value, ast.Str)):
|
||||
doc = item.value.s
|
||||
if "PYTEST_DONT_REWRITE" in doc:
|
||||
# The module has disabled assertion rewriting.
|
||||
return
|
||||
lineno += len(doc) - 1
|
||||
expect_docstring = False
|
||||
elif (not isinstance(item, ast.ImportFrom) or item.level > 0 or
|
||||
item.module != "__future__"):
|
||||
lineno = item.lineno
|
||||
break
|
||||
pos += 1
|
||||
imports = [ast.Import([alias], lineno=lineno, col_offset=0)
|
||||
for alias in aliases]
|
||||
mod.body[pos:pos] = imports
|
||||
# Collect asserts.
|
||||
nodes = [mod]
|
||||
while nodes:
|
||||
node = nodes.pop()
|
||||
for name, field in ast.iter_fields(node):
|
||||
if isinstance(field, list):
|
||||
new = []
|
||||
for i, child in enumerate(field):
|
||||
if isinstance(child, ast.Assert):
|
||||
# Transform assert.
|
||||
new.extend(self.visit(child))
|
||||
else:
|
||||
new.append(child)
|
||||
if isinstance(child, ast.AST):
|
||||
nodes.append(child)
|
||||
setattr(node, name, new)
|
||||
elif (isinstance(field, ast.AST) and
|
||||
# Don't recurse into expressions as they can't contain
|
||||
# asserts.
|
||||
not isinstance(field, ast.expr)):
|
||||
nodes.append(field)
|
||||
|
||||
def variable(self):
|
||||
"""Get a new variable."""
|
||||
# Use a character invalid in python identifiers to avoid clashing.
|
||||
name = "@py_assert" + str(next(self.variable_counter))
|
||||
self.variables.append(name)
|
||||
return name
|
||||
|
||||
def assign(self, expr):
|
||||
"""Give *expr* a name."""
|
||||
name = self.variable()
|
||||
self.statements.append(ast.Assign([ast.Name(name, ast.Store())], expr))
|
||||
return ast.Name(name, ast.Load())
|
||||
|
||||
def display(self, expr):
|
||||
"""Call py.io.saferepr on the expression."""
|
||||
return self.helper("saferepr", expr)
|
||||
|
||||
def helper(self, name, *args):
|
||||
"""Call a helper in this module."""
|
||||
py_name = ast.Name("@pytest_ar", ast.Load())
|
||||
attr = ast.Attribute(py_name, "_" + name, ast.Load())
|
||||
return ast.Call(attr, list(args), [], None, None)
|
||||
|
||||
def builtin(self, name):
|
||||
"""Return the builtin called *name*."""
|
||||
builtin_name = ast.Name("@py_builtins", ast.Load())
|
||||
return ast.Attribute(builtin_name, name, ast.Load())
|
||||
|
||||
def explanation_param(self, expr):
|
||||
specifier = "py" + str(next(self.variable_counter))
|
||||
self.explanation_specifiers[specifier] = expr
|
||||
return "%(" + specifier + ")s"
|
||||
|
||||
def push_format_context(self):
|
||||
self.explanation_specifiers = {}
|
||||
self.stack.append(self.explanation_specifiers)
|
||||
|
||||
def pop_format_context(self, expl_expr):
|
||||
current = self.stack.pop()
|
||||
if self.stack:
|
||||
self.explanation_specifiers = self.stack[-1]
|
||||
keys = [ast.Str(key) for key in current.keys()]
|
||||
format_dict = ast.Dict(keys, list(current.values()))
|
||||
form = ast.BinOp(expl_expr, ast.Mod(), format_dict)
|
||||
name = "@py_format" + str(next(self.variable_counter))
|
||||
self.on_failure.append(ast.Assign([ast.Name(name, ast.Store())], form))
|
||||
return ast.Name(name, ast.Load())
|
||||
|
||||
def generic_visit(self, node):
|
||||
"""Handle expressions we don't have custom code for."""
|
||||
assert isinstance(node, ast.expr)
|
||||
res = self.assign(node)
|
||||
return res, self.explanation_param(self.display(res))
|
||||
|
||||
def visit_Assert(self, assert_):
|
||||
if assert_.msg:
|
||||
# There's already a message. Don't mess with it.
|
||||
return [assert_]
|
||||
self.statements = []
|
||||
self.cond_chain = ()
|
||||
self.variables = []
|
||||
self.variable_counter = itertools.count()
|
||||
self.stack = []
|
||||
self.on_failure = []
|
||||
self.push_format_context()
|
||||
# Rewrite assert into a bunch of statements.
|
||||
top_condition, explanation = self.visit(assert_.test)
|
||||
# Create failure message.
|
||||
body = self.on_failure
|
||||
negation = ast.UnaryOp(ast.Not(), top_condition)
|
||||
self.statements.append(ast.If(negation, body, []))
|
||||
explanation = "assert " + explanation
|
||||
template = ast.Str(explanation)
|
||||
msg = self.pop_format_context(template)
|
||||
fmt = self.helper("format_explanation", msg)
|
||||
err_name = ast.Name("AssertionError", ast.Load())
|
||||
exc = ast.Call(err_name, [fmt], [], None, None)
|
||||
if sys.version_info[0] >= 3:
|
||||
raise_ = ast.Raise(exc, None)
|
||||
else:
|
||||
raise_ = ast.Raise(exc, None, None)
|
||||
body.append(raise_)
|
||||
# Clear temporary variables by setting them to None.
|
||||
if self.variables:
|
||||
variables = [ast.Name(name, ast.Store()) for name in self.variables]
|
||||
clear = ast.Assign(variables, ast.Name("None", ast.Load()))
|
||||
self.statements.append(clear)
|
||||
# Fix line numbers.
|
||||
for stmt in self.statements:
|
||||
set_location(stmt, assert_.lineno, assert_.col_offset)
|
||||
return self.statements
|
||||
|
||||
def visit_Name(self, name):
|
||||
# Check if the name is local or not.
|
||||
locs = ast.Call(self.builtin("locals"), [], [], None, None)
|
||||
globs = ast.Call(self.builtin("globals"), [], [], None, None)
|
||||
ops = [ast.In(), ast.IsNot()]
|
||||
test = ast.Compare(ast.Str(name.id), ops, [locs, globs])
|
||||
expr = ast.IfExp(test, self.display(name), ast.Str(name.id))
|
||||
return name, self.explanation_param(expr)
|
||||
|
||||
def visit_BoolOp(self, boolop):
|
||||
res_var = self.variable()
|
||||
expl_list = self.assign(ast.List([], ast.Load()))
|
||||
app = ast.Attribute(expl_list, "append", ast.Load())
|
||||
is_or = int(isinstance(boolop.op, ast.Or))
|
||||
body = save = self.statements
|
||||
fail_save = self.on_failure
|
||||
levels = len(boolop.values) - 1
|
||||
self.push_format_context()
|
||||
# Process each operand, short-circuting if needed.
|
||||
for i, v in enumerate(boolop.values):
|
||||
if i:
|
||||
fail_inner = []
|
||||
self.on_failure.append(ast.If(cond, fail_inner, []))
|
||||
self.on_failure = fail_inner
|
||||
self.push_format_context()
|
||||
res, expl = self.visit(v)
|
||||
body.append(ast.Assign([ast.Name(res_var, ast.Store())], res))
|
||||
expl_format = self.pop_format_context(ast.Str(expl))
|
||||
call = ast.Call(app, [expl_format], [], None, None)
|
||||
self.on_failure.append(ast.Expr(call))
|
||||
if i < levels:
|
||||
cond = res
|
||||
if is_or:
|
||||
cond = ast.UnaryOp(ast.Not(), cond)
|
||||
inner = []
|
||||
self.statements.append(ast.If(cond, inner, []))
|
||||
self.statements = body = inner
|
||||
self.statements = save
|
||||
self.on_failure = fail_save
|
||||
expl_template = self.helper("format_boolop", expl_list, ast.Num(is_or))
|
||||
expl = self.pop_format_context(expl_template)
|
||||
return ast.Name(res_var, ast.Load()), self.explanation_param(expl)
|
||||
|
||||
def visit_UnaryOp(self, unary):
|
||||
pattern = unary_map[unary.op.__class__]
|
||||
operand_res, operand_expl = self.visit(unary.operand)
|
||||
res = self.assign(ast.UnaryOp(unary.op, operand_res))
|
||||
return res, pattern % (operand_expl,)
|
||||
|
||||
def visit_BinOp(self, binop):
|
||||
symbol = binop_map[binop.op.__class__]
|
||||
left_expr, left_expl = self.visit(binop.left)
|
||||
right_expr, right_expl = self.visit(binop.right)
|
||||
explanation = "(%s %s %s)" % (left_expl, symbol, right_expl)
|
||||
res = self.assign(ast.BinOp(left_expr, binop.op, right_expr))
|
||||
return res, explanation
|
||||
|
||||
def visit_Call(self, call):
|
||||
new_func, func_expl = self.visit(call.func)
|
||||
arg_expls = []
|
||||
new_args = []
|
||||
new_kwargs = []
|
||||
new_star = new_kwarg = None
|
||||
for arg in call.args:
|
||||
res, expl = self.visit(arg)
|
||||
new_args.append(res)
|
||||
arg_expls.append(expl)
|
||||
for keyword in call.keywords:
|
||||
res, expl = self.visit(keyword.value)
|
||||
new_kwargs.append(ast.keyword(keyword.arg, res))
|
||||
arg_expls.append(keyword.arg + "=" + expl)
|
||||
if call.starargs:
|
||||
new_star, expl = self.visit(call.starargs)
|
||||
arg_expls.append("*" + expl)
|
||||
if call.kwargs:
|
||||
new_kwarg, expl = self.visit(call.kwargs)
|
||||
arg_expls.append("**" + expl)
|
||||
expl = "%s(%s)" % (func_expl, ', '.join(arg_expls))
|
||||
new_call = ast.Call(new_func, new_args, new_kwargs, new_star, new_kwarg)
|
||||
res = self.assign(new_call)
|
||||
res_expl = self.explanation_param(self.display(res))
|
||||
outer_expl = "%s\n{%s = %s\n}" % (res_expl, res_expl, expl)
|
||||
return res, outer_expl
|
||||
|
||||
def visit_Attribute(self, attr):
|
||||
if not isinstance(attr.ctx, ast.Load):
|
||||
return self.generic_visit(attr)
|
||||
value, value_expl = self.visit(attr.value)
|
||||
res = self.assign(ast.Attribute(value, attr.attr, ast.Load()))
|
||||
res_expl = self.explanation_param(self.display(res))
|
||||
pat = "%s\n{%s = %s.%s\n}"
|
||||
expl = pat % (res_expl, res_expl, value_expl, attr.attr)
|
||||
return res, expl
|
||||
|
||||
def visit_Compare(self, comp):
|
||||
self.push_format_context()
|
||||
left_res, left_expl = self.visit(comp.left)
|
||||
res_variables = [self.variable() for i in range(len(comp.ops))]
|
||||
load_names = [ast.Name(v, ast.Load()) for v in res_variables]
|
||||
store_names = [ast.Name(v, ast.Store()) for v in res_variables]
|
||||
it = zip(range(len(comp.ops)), comp.ops, comp.comparators)
|
||||
expls = []
|
||||
syms = []
|
||||
results = [left_res]
|
||||
for i, op, next_operand in it:
|
||||
next_res, next_expl = self.visit(next_operand)
|
||||
results.append(next_res)
|
||||
sym = binop_map[op.__class__]
|
||||
syms.append(ast.Str(sym))
|
||||
expl = "%s %s %s" % (left_expl, sym, next_expl)
|
||||
expls.append(ast.Str(expl))
|
||||
res_expr = ast.Compare(left_res, [op], [next_res])
|
||||
self.statements.append(ast.Assign([store_names[i]], res_expr))
|
||||
left_res, left_expl = next_res, next_expl
|
||||
# Use py.code._reprcompare if that's available.
|
||||
expl_call = self.helper("call_reprcompare",
|
||||
ast.Tuple(syms, ast.Load()),
|
||||
ast.Tuple(load_names, ast.Load()),
|
||||
ast.Tuple(expls, ast.Load()),
|
||||
ast.Tuple(results, ast.Load()))
|
||||
if len(comp.ops) > 1:
|
||||
res = ast.BoolOp(ast.And(), load_names)
|
||||
else:
|
||||
res = load_names[0]
|
||||
return res, self.explanation_param(self.pop_format_context(expl_call))
|
||||
@@ -1,43 +1,79 @@
|
||||
"""
|
||||
support for presented detailed information in failing assertions.
|
||||
"""
|
||||
"""Utilities for assertion debugging"""
|
||||
|
||||
import py
|
||||
import sys
|
||||
from _pytest.monkeypatch import monkeypatch
|
||||
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("debugconfig")
|
||||
group._addoption('--no-assert', action="store_true", default=False,
|
||||
dest="noassert",
|
||||
help="disable python assert expression reinterpretation."),
|
||||
|
||||
def pytest_configure(config):
|
||||
# The _reprcompare attribute on the py.code module is used by
|
||||
# py._code._assertionnew to detect this plugin was loaded and in
|
||||
# turn call the hooks defined here as part of the
|
||||
# DebugInterpreter.
|
||||
m = monkeypatch()
|
||||
config._cleanup.append(m.undo)
|
||||
warn_about_missing_assertion()
|
||||
if not config.getvalue("noassert") and not config.getvalue("nomagic"):
|
||||
def callbinrepr(op, left, right):
|
||||
hook_result = config.hook.pytest_assertrepr_compare(
|
||||
config=config, op=op, left=left, right=right)
|
||||
for new_expl in hook_result:
|
||||
if new_expl:
|
||||
return '\n~'.join(new_expl)
|
||||
m.setattr(py.builtin.builtins,
|
||||
'AssertionError', py.code._AssertionError)
|
||||
m.setattr(py.code, '_reprcompare', callbinrepr)
|
||||
# The _reprcompare attribute on the util module is used by the new assertion
|
||||
# interpretation code and assertion rewriter to detect this plugin was
|
||||
# loaded and in turn call the hooks defined here as part of the
|
||||
# DebugInterpreter.
|
||||
_reprcompare = None
|
||||
|
||||
def format_explanation(explanation):
|
||||
"""This formats an explanation
|
||||
|
||||
Normally all embedded newlines are escaped, however there are
|
||||
three exceptions: \n{, \n} and \n~. The first two are intended
|
||||
cover nested explanations, see function and attribute explanations
|
||||
for examples (.visit_Call(), visit_Attribute()). The last one is
|
||||
for when one explanation needs to span multiple lines, e.g. when
|
||||
displaying diffs.
|
||||
"""
|
||||
# simplify 'assert False where False = ...'
|
||||
where = 0
|
||||
while True:
|
||||
start = where = explanation.find("False\n{False = ", where)
|
||||
if where == -1:
|
||||
break
|
||||
level = 0
|
||||
for i, c in enumerate(explanation[start:]):
|
||||
if c == "{":
|
||||
level += 1
|
||||
elif c == "}":
|
||||
level -= 1
|
||||
if not level:
|
||||
break
|
||||
else:
|
||||
raise AssertionError("unbalanced braces: %r" % (explanation,))
|
||||
end = start + i
|
||||
where = end
|
||||
if explanation[end - 1] == '\n':
|
||||
explanation = (explanation[:start] + explanation[start+15:end-1] +
|
||||
explanation[end+1:])
|
||||
where -= 17
|
||||
raw_lines = (explanation or '').split('\n')
|
||||
# escape newlines not followed by {, } and ~
|
||||
lines = [raw_lines[0]]
|
||||
for l in raw_lines[1:]:
|
||||
if l.startswith('{') or l.startswith('}') or l.startswith('~'):
|
||||
lines.append(l)
|
||||
else:
|
||||
lines[-1] += '\\n' + l
|
||||
|
||||
result = lines[:1]
|
||||
stack = [0]
|
||||
stackcnt = [0]
|
||||
for line in lines[1:]:
|
||||
if line.startswith('{'):
|
||||
if stackcnt[-1]:
|
||||
s = 'and '
|
||||
else:
|
||||
s = 'where '
|
||||
stack.append(len(result))
|
||||
stackcnt[-1] += 1
|
||||
stackcnt.append(0)
|
||||
result.append(' +' + ' '*(len(stack)-1) + s + line[1:])
|
||||
elif line.startswith('}'):
|
||||
assert line.startswith('}')
|
||||
stack.pop()
|
||||
stackcnt.pop()
|
||||
result[stack[-1]] += line[1:]
|
||||
else:
|
||||
assert line.startswith('~')
|
||||
result.append(' '*len(stack) + line[1:])
|
||||
assert len(stack) == 1
|
||||
return '\n'.join(result)
|
||||
|
||||
def warn_about_missing_assertion():
|
||||
try:
|
||||
assert False
|
||||
except AssertionError:
|
||||
pass
|
||||
else:
|
||||
sys.stderr.write("WARNING: failing tests may report as passing because "
|
||||
"assertions are turned off! (are you using python -O?)\n")
|
||||
|
||||
# Provide basestring in python3
|
||||
try:
|
||||
@@ -46,7 +82,7 @@ except NameError:
|
||||
basestring = str
|
||||
|
||||
|
||||
def pytest_assertrepr_compare(op, left, right):
|
||||
def assertrepr_compare(op, left, right):
|
||||
"""return specialised explanations for some operators/operands"""
|
||||
width = 80 - 15 - len(op) - 2 # 15 chars indentation, 1 space around op
|
||||
left_repr = py.io.saferepr(left, maxsize=int(width/2))
|
||||
@@ -11,22 +11,22 @@ def pytest_addoption(parser):
|
||||
group._addoption('-s', action="store_const", const="no", dest="capture",
|
||||
help="shortcut for --capture=no.")
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_cmdline_parse(pluginmanager, args):
|
||||
# we want to perform capturing already for plugin/conftest loading
|
||||
if '-s' in args or "--capture=no" in args:
|
||||
method = "no"
|
||||
elif hasattr(os, 'dup') and '--capture=sys' not in args:
|
||||
method = "fd"
|
||||
else:
|
||||
method = "sys"
|
||||
capman = CaptureManager(method)
|
||||
pluginmanager.register(capman, "capturemanager")
|
||||
|
||||
def addouterr(rep, outerr):
|
||||
repr = getattr(rep, 'longrepr', None)
|
||||
if not hasattr(repr, 'addsection'):
|
||||
return
|
||||
for secname, content in zip(["out", "err"], outerr):
|
||||
if content:
|
||||
repr.addsection("Captured std%s" % secname, content.rstrip())
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
# registered in config.py during early conftest.py loading
|
||||
capman = config.pluginmanager.getplugin('capturemanager')
|
||||
while capman._method2capture:
|
||||
name, cap = capman._method2capture.popitem()
|
||||
# XXX logging module may wants to close it itself on process exit
|
||||
# otherwise we could do finalization here and call "reset()".
|
||||
cap.suspend()
|
||||
rep.sections.append(("Captured std%s" % secname, content))
|
||||
|
||||
class NoCapture:
|
||||
def startall(self):
|
||||
@@ -39,8 +39,9 @@ class NoCapture:
|
||||
return "", ""
|
||||
|
||||
class CaptureManager:
|
||||
def __init__(self):
|
||||
def __init__(self, defaultmethod=None):
|
||||
self._method2capture = {}
|
||||
self._defaultmethod = defaultmethod
|
||||
|
||||
def _maketempfile(self):
|
||||
f = py.std.tempfile.TemporaryFile()
|
||||
@@ -65,14 +66,6 @@ class CaptureManager:
|
||||
else:
|
||||
raise ValueError("unknown capturing method: %r" % method)
|
||||
|
||||
def _getmethod_preoptionparse(self, args):
|
||||
if '-s' in args or "--capture=no" in args:
|
||||
return "no"
|
||||
elif hasattr(os, 'dup') and '--capture=sys' not in args:
|
||||
return "fd"
|
||||
else:
|
||||
return "sys"
|
||||
|
||||
def _getmethod(self, config, fspath):
|
||||
if config.option.capture:
|
||||
method = config.option.capture
|
||||
@@ -85,16 +78,22 @@ class CaptureManager:
|
||||
method = "sys"
|
||||
return method
|
||||
|
||||
def reset_capturings(self):
|
||||
for name, cap in self._method2capture.items():
|
||||
cap.reset()
|
||||
|
||||
def resumecapture_item(self, item):
|
||||
method = self._getmethod(item.config, item.fspath)
|
||||
if not hasattr(item, 'outerr'):
|
||||
item.outerr = ('', '') # we accumulate outerr on the item
|
||||
return self.resumecapture(method)
|
||||
|
||||
def resumecapture(self, method):
|
||||
def resumecapture(self, method=None):
|
||||
if hasattr(self, '_capturing'):
|
||||
raise ValueError("cannot resume, already capturing with %r" %
|
||||
(self._capturing,))
|
||||
if method is None:
|
||||
method = self._defaultmethod
|
||||
cap = self._method2capture.get(method)
|
||||
self._capturing = method
|
||||
if cap is None:
|
||||
@@ -164,17 +163,6 @@ class CaptureManager:
|
||||
def pytest_runtest_teardown(self, item):
|
||||
self.resumecapture_item(item)
|
||||
|
||||
def pytest__teardown_final(self, __multicall__, session):
|
||||
method = self._getmethod(session.config, None)
|
||||
self.resumecapture(method)
|
||||
try:
|
||||
rep = __multicall__.execute()
|
||||
finally:
|
||||
outerr = self.suspendcapture()
|
||||
if rep:
|
||||
addouterr(rep, outerr)
|
||||
return rep
|
||||
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
if hasattr(self, '_capturing'):
|
||||
self.suspendcapture()
|
||||
|
||||
@@ -8,13 +8,15 @@ import pytest
|
||||
def pytest_cmdline_parse(pluginmanager, args):
|
||||
config = Config(pluginmanager)
|
||||
config.parse(args)
|
||||
if config.option.debug:
|
||||
config.trace.root.setwriter(sys.stderr.write)
|
||||
return config
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
for func in config._cleanup:
|
||||
func()
|
||||
while 1:
|
||||
try:
|
||||
fin = config._cleanup.pop()
|
||||
except IndexError:
|
||||
break
|
||||
fin()
|
||||
|
||||
class Parser:
|
||||
""" Parser for command line arguments. """
|
||||
@@ -81,6 +83,7 @@ class Parser:
|
||||
self._inidict[name] = (help, type, default)
|
||||
self._ininames.append(name)
|
||||
|
||||
|
||||
class OptionGroup:
|
||||
def __init__(self, name, description="", parser=None):
|
||||
self.name = name
|
||||
@@ -256,11 +259,14 @@ class Config(object):
|
||||
self.hook = self.pluginmanager.hook
|
||||
self._inicache = {}
|
||||
self._cleanup = []
|
||||
|
||||
|
||||
@classmethod
|
||||
def fromdictargs(cls, option_dict, args):
|
||||
""" constructor useable for subprocesses. """
|
||||
config = cls()
|
||||
# XXX slightly crude way to initialize capturing
|
||||
import _pytest.capture
|
||||
_pytest.capture.pytest_cmdline_parse(config.pluginmanager, args)
|
||||
config._preparse(args, addopts=False)
|
||||
config.option.__dict__.update(option_dict)
|
||||
for x in config.option.plugins:
|
||||
@@ -285,11 +291,10 @@ class Config(object):
|
||||
|
||||
def _setinitialconftest(self, args):
|
||||
# capture output during conftest init (#issue93)
|
||||
from _pytest.capture import CaptureManager
|
||||
capman = CaptureManager()
|
||||
self.pluginmanager.register(capman, 'capturemanager')
|
||||
# will be unregistered in capture.py's unconfigure()
|
||||
capman.resumecapture(capman._getmethod_preoptionparse(args))
|
||||
# XXX introduce load_conftest hook to avoid needing to know
|
||||
# about capturing plugin here
|
||||
capman = self.pluginmanager.getplugin("capturemanager")
|
||||
capman.resumecapture()
|
||||
try:
|
||||
try:
|
||||
self._conftest.setinitial(args)
|
||||
@@ -334,6 +339,7 @@ class Config(object):
|
||||
# Note that this can only be called once per testing process.
|
||||
assert not hasattr(self, 'args'), (
|
||||
"can only parse cmdline args at most once per Config object")
|
||||
self._origargs = args
|
||||
self._preparse(args)
|
||||
self._parser.hints.extend(self.pluginmanager._hints)
|
||||
args = self._parser.parse_setoption(args, self.option)
|
||||
@@ -341,6 +347,14 @@ class Config(object):
|
||||
args.append(py.std.os.getcwd())
|
||||
self.args = args
|
||||
|
||||
def addinivalue_line(self, name, line):
|
||||
""" add a line to an ini-file option. The option must have been
|
||||
declared but might not yet be set in which case the line becomes the
|
||||
the first line in its value. """
|
||||
x = self.getini(name)
|
||||
assert isinstance(x, list)
|
||||
x.append(line) # modifies the cached list inline
|
||||
|
||||
def getini(self, name):
|
||||
""" return configuration value from an ini file. If the
|
||||
specified name hasn't been registered through a prior ``parse.addini``
|
||||
@@ -422,7 +436,7 @@ class Config(object):
|
||||
|
||||
|
||||
def getcfg(args, inibasenames):
|
||||
args = [x for x in args if str(x)[0] != "-"]
|
||||
args = [x for x in args if not str(x).startswith("-")]
|
||||
if not args:
|
||||
args = [py.path.local()]
|
||||
for arg in args:
|
||||
|
||||
@@ -16,11 +16,10 @@ default_plugins = (
|
||||
"junitxml resultlog doctest").split()
|
||||
|
||||
class TagTracer:
|
||||
def __init__(self, prefix="[pytest] "):
|
||||
def __init__(self):
|
||||
self._tag2proc = {}
|
||||
self.writer = None
|
||||
self.indent = 0
|
||||
self.prefix = prefix
|
||||
|
||||
def get(self, name):
|
||||
return TagTracerSub(self, (name,))
|
||||
@@ -30,7 +29,7 @@ class TagTracer:
|
||||
if args:
|
||||
indent = " " * self.indent
|
||||
content = " ".join(map(str, args))
|
||||
self.writer("%s%s%s\n" %(self.prefix, indent, content))
|
||||
self.writer("%s%s [%s]\n" %(indent, content, ":".join(tags)))
|
||||
try:
|
||||
self._tag2proc[tags](tags, args)
|
||||
except KeyError:
|
||||
@@ -212,6 +211,14 @@ class PluginManager(object):
|
||||
self.register(mod, modname)
|
||||
self.consider_module(mod)
|
||||
|
||||
def pytest_configure(self, config):
|
||||
config.addinivalue_line("markers",
|
||||
"tryfirst: mark a hook implementation function such that the "
|
||||
"plugin machinery will try to call it first/as early as possible.")
|
||||
config.addinivalue_line("markers",
|
||||
"trylast: mark a hook implementation function such that the "
|
||||
"plugin machinery will try to call it last/as late as possible.")
|
||||
|
||||
def pytest_plugin_registered(self, plugin):
|
||||
import pytest
|
||||
dic = self.call_plugin(plugin, "pytest_namespace", {}) or {}
|
||||
@@ -432,10 +439,7 @@ _preinit = []
|
||||
def _preloadplugins():
|
||||
_preinit.append(PluginManager(load=True))
|
||||
|
||||
def main(args=None, plugins=None):
|
||||
""" returned exit code integer, after an in-process testing run
|
||||
with the given command line arguments, preloading an optional list
|
||||
of passed in plugin objects. """
|
||||
def _prepareconfig(args=None, plugins=None):
|
||||
if args is None:
|
||||
args = sys.argv[1:]
|
||||
elif isinstance(args, py.path.local):
|
||||
@@ -449,13 +453,19 @@ def main(args=None, plugins=None):
|
||||
else: # subsequent calls to main will create a fresh instance
|
||||
_pluginmanager = PluginManager(load=True)
|
||||
hook = _pluginmanager.hook
|
||||
if plugins:
|
||||
for plugin in plugins:
|
||||
_pluginmanager.register(plugin)
|
||||
return hook.pytest_cmdline_parse(
|
||||
pluginmanager=_pluginmanager, args=args)
|
||||
|
||||
def main(args=None, plugins=None):
|
||||
""" returned exit code integer, after an in-process testing run
|
||||
with the given command line arguments, preloading an optional list
|
||||
of passed in plugin objects. """
|
||||
try:
|
||||
if plugins:
|
||||
for plugin in plugins:
|
||||
_pluginmanager.register(plugin)
|
||||
config = hook.pytest_cmdline_parse(
|
||||
pluginmanager=_pluginmanager, args=args)
|
||||
exitstatus = hook.pytest_cmdline_main(config=config)
|
||||
config = _prepareconfig(args, plugins)
|
||||
exitstatus = config.hook.pytest_cmdline_main(config=config)
|
||||
except UsageError:
|
||||
e = sys.exc_info()[1]
|
||||
sys.stderr.write("ERROR: %s\n" %(e.args[0],))
|
||||
|
||||
@@ -59,7 +59,7 @@ class DoctestItem(pytest.Item):
|
||||
inner_excinfo = py.code.ExceptionInfo(excinfo.value.exc_info)
|
||||
lines += ["UNEXPECTED EXCEPTION: %s" %
|
||||
repr(inner_excinfo.value)]
|
||||
|
||||
lines += py.std.traceback.format_exception(*excinfo.value.exc_info)
|
||||
return ReprFailDoctest(reprlocation, lines)
|
||||
else:
|
||||
return super(DoctestItem, self).repr_failure(excinfo)
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
""" version info, help messages, tracing configuration. """
|
||||
import py
|
||||
import pytest
|
||||
import inspect, sys
|
||||
import os, inspect, sys
|
||||
from _pytest.core import varnames
|
||||
|
||||
def pytest_addoption(parser):
|
||||
@@ -16,12 +16,31 @@ def pytest_addoption(parser):
|
||||
group.addoption('--traceconfig',
|
||||
action="store_true", dest="traceconfig", default=False,
|
||||
help="trace considerations of conftest.py files."),
|
||||
group._addoption('--nomagic',
|
||||
action="store_true", dest="nomagic", default=False,
|
||||
help="don't reinterpret asserts, no traceback cutting. ")
|
||||
group.addoption('--debug',
|
||||
action="store_true", dest="debug", default=False,
|
||||
help="generate and show internal debugging information.")
|
||||
help="store internal tracing debug information in 'pytestdebug.log'.")
|
||||
|
||||
|
||||
def pytest_cmdline_parse(__multicall__):
|
||||
config = __multicall__.execute()
|
||||
if config.option.debug:
|
||||
path = os.path.abspath("pytestdebug.log")
|
||||
f = open(path, 'w')
|
||||
config._debugfile = f
|
||||
f.write("versions pytest-%s, py-%s, python-%s\ncwd=%s\nargs=%s\n\n" %(
|
||||
pytest.__version__, py.__version__, ".".join(map(str, sys.version_info)),
|
||||
os.getcwd(), config._origargs))
|
||||
config.trace.root.setwriter(f.write)
|
||||
sys.stderr.write("writing pytestdebug information to %s\n" % path)
|
||||
return config
|
||||
|
||||
@pytest.mark.trylast
|
||||
def pytest_unconfigure(config):
|
||||
if hasattr(config, '_debugfile'):
|
||||
config._debugfile.close()
|
||||
sys.stderr.write("wrote pytestdebug information to %s\n" %
|
||||
config._debugfile.name)
|
||||
config.trace.root.setwriter(None)
|
||||
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
@@ -37,6 +56,7 @@ def pytest_cmdline_main(config):
|
||||
elif config.option.help:
|
||||
config.pluginmanager.do_configure(config)
|
||||
showhelp(config)
|
||||
config.pluginmanager.do_unconfigure(config)
|
||||
return 0
|
||||
|
||||
def showhelp(config):
|
||||
@@ -94,7 +114,7 @@ def pytest_report_header(config):
|
||||
verinfo = getpluginversioninfo(config)
|
||||
if verinfo:
|
||||
lines.extend(verinfo)
|
||||
|
||||
|
||||
if config.option.traceconfig:
|
||||
lines.append("active plugins:")
|
||||
plugins = []
|
||||
|
||||
@@ -149,7 +149,8 @@ def pytest_runtest_makereport(item, call):
|
||||
pytest_runtest_makereport.firstresult = True
|
||||
|
||||
def pytest_runtest_logreport(report):
|
||||
""" process item test report. """
|
||||
""" process a test setup/call/teardown report relating to
|
||||
the respective phase of executing a test. """
|
||||
|
||||
# special handling for final teardown - somewhat internal for now
|
||||
def pytest__teardown_final(session):
|
||||
|
||||
@@ -65,18 +65,18 @@ def pytest_unconfigure(config):
|
||||
|
||||
class LogXML(object):
|
||||
def __init__(self, logfile, prefix):
|
||||
self.logfile = logfile
|
||||
logfile = os.path.expanduser(os.path.expandvars(logfile))
|
||||
self.logfile = os.path.normpath(logfile)
|
||||
self.prefix = prefix
|
||||
self.test_logs = []
|
||||
self.passed = self.skipped = 0
|
||||
self.failed = self.errors = 0
|
||||
self._durations = {}
|
||||
|
||||
def _opentestcase(self, report):
|
||||
names = report.nodeid.split("::")
|
||||
names[0] = names[0].replace("/", '.')
|
||||
names = tuple(names)
|
||||
d = {'time': self._durations.pop(names, "0")}
|
||||
d = {'time': getattr(report, 'duration', 0)}
|
||||
names = [x.replace(".py", "") for x in names if x != "()"]
|
||||
classnames = names[:-1]
|
||||
if self.prefix:
|
||||
@@ -113,8 +113,14 @@ class LogXML(object):
|
||||
'<skipped message="xfail-marked test passes unexpectedly"/>')
|
||||
self.skipped += 1
|
||||
else:
|
||||
sec = dict(report.sections)
|
||||
self.appendlog('<failure message="test failure">%s</failure>',
|
||||
report.longrepr)
|
||||
for name in ('out', 'err'):
|
||||
content = sec.get("Captured std%s" % name)
|
||||
if content:
|
||||
self.appendlog(
|
||||
"<system-%s>%%s</system-%s>" % (name, name), content)
|
||||
self.failed += 1
|
||||
self._closetestcase()
|
||||
|
||||
@@ -160,7 +166,8 @@ class LogXML(object):
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
if report.passed:
|
||||
self.append_pass(report)
|
||||
if report.when == "call": # ignore setup/teardown
|
||||
self.append_pass(report)
|
||||
elif report.failed:
|
||||
if report.when != "call":
|
||||
self.append_error(report)
|
||||
@@ -169,14 +176,6 @@ class LogXML(object):
|
||||
elif report.skipped:
|
||||
self.append_skipped(report)
|
||||
|
||||
def pytest_runtest_call(self, item, __multicall__):
|
||||
names = tuple(item.listnames())
|
||||
start = time.time()
|
||||
try:
|
||||
return __multicall__.execute()
|
||||
finally:
|
||||
self._durations[names] = time.time() - start
|
||||
|
||||
def pytest_collectreport(self, report):
|
||||
if not report.passed:
|
||||
if report.failed:
|
||||
|
||||
102
_pytest/main.py
102
_pytest/main.py
@@ -2,7 +2,7 @@
|
||||
|
||||
import py
|
||||
import pytest, _pytest
|
||||
import os, sys
|
||||
import os, sys, imp
|
||||
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
|
||||
# exitcodes for the command line
|
||||
@@ -11,6 +11,8 @@ EXIT_TESTSFAILED = 1
|
||||
EXIT_INTERRUPTED = 2
|
||||
EXIT_INTERNALERROR = 3
|
||||
|
||||
name_re = py.std.re.compile("^[a-zA-Z_]\w*$")
|
||||
|
||||
def pytest_addoption(parser):
|
||||
parser.addini("norecursedirs", "directory patterns to avoid for recursion",
|
||||
type="args", default=('.*', 'CVS', '_darcs', '{arch}'))
|
||||
@@ -27,6 +29,9 @@ def pytest_addoption(parser):
|
||||
action="store", type="int", dest="maxfail", default=0,
|
||||
help="exit after first num failures or errors.")
|
||||
|
||||
group._addoption('--strict', action="store_true",
|
||||
help="run pytest in strict mode, warnings become errors.")
|
||||
|
||||
group = parser.getgroup("collect", "collection")
|
||||
group.addoption('--collectonly',
|
||||
action="store_true", dest="collectonly",
|
||||
@@ -46,23 +51,25 @@ def pytest_addoption(parser):
|
||||
|
||||
|
||||
def pytest_namespace():
|
||||
return dict(collect=dict(Item=Item, Collector=Collector, File=File))
|
||||
|
||||
collect = dict(Item=Item, Collector=Collector, File=File, Session=Session)
|
||||
return dict(collect=collect)
|
||||
|
||||
def pytest_configure(config):
|
||||
py.test.config = config # compatibiltiy
|
||||
if config.option.exitfirst:
|
||||
config.option.maxfail = 1
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
""" default command line protocol for initialization, session,
|
||||
running tests and reporting. """
|
||||
def wrap_session(config, doit):
|
||||
"""Skeleton command line program"""
|
||||
session = Session(config)
|
||||
session.exitstatus = EXIT_OK
|
||||
initstate = 0
|
||||
try:
|
||||
config.pluginmanager.do_configure(config)
|
||||
initstate = 1
|
||||
config.hook.pytest_sessionstart(session=session)
|
||||
config.hook.pytest_collection(session=session)
|
||||
config.hook.pytest_runtestloop(session=session)
|
||||
initstate = 2
|
||||
doit(config, session)
|
||||
except pytest.UsageError:
|
||||
raise
|
||||
except KeyboardInterrupt:
|
||||
@@ -75,25 +82,31 @@ def pytest_cmdline_main(config):
|
||||
session.exitstatus = EXIT_INTERNALERROR
|
||||
if excinfo.errisinstance(SystemExit):
|
||||
sys.stderr.write("mainloop: caught Spurious SystemExit!\n")
|
||||
if initstate >= 2:
|
||||
config.hook.pytest_sessionfinish(session=session,
|
||||
exitstatus=session.exitstatus or (session._testsfailed and 1))
|
||||
if not session.exitstatus and session._testsfailed:
|
||||
session.exitstatus = EXIT_TESTSFAILED
|
||||
config.hook.pytest_sessionfinish(session=session,
|
||||
exitstatus=session.exitstatus)
|
||||
config.pluginmanager.do_unconfigure(config)
|
||||
if initstate >= 1:
|
||||
config.pluginmanager.do_unconfigure(config)
|
||||
return session.exitstatus
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
return wrap_session(config, _main)
|
||||
|
||||
def _main(config, session):
|
||||
""" default command line protocol for initialization, session,
|
||||
running tests and reporting. """
|
||||
config.hook.pytest_collection(session=session)
|
||||
config.hook.pytest_runtestloop(session=session)
|
||||
|
||||
def pytest_collection(session):
|
||||
session.perform_collect()
|
||||
hook = session.config.hook
|
||||
hook.pytest_collection_modifyitems(session=session,
|
||||
config=session.config, items=session.items)
|
||||
hook.pytest_collection_finish(session=session)
|
||||
return True
|
||||
return session.perform_collect()
|
||||
|
||||
def pytest_runtestloop(session):
|
||||
if session.config.option.collectonly:
|
||||
return True
|
||||
for item in session.session.items:
|
||||
for item in session.items:
|
||||
item.config.hook.pytest_runtest_protocol(item=item)
|
||||
if session.shouldstop:
|
||||
raise session.Interrupted(session.shouldstop)
|
||||
@@ -124,7 +137,7 @@ def compatproperty(name):
|
||||
return getattr(pytest, name)
|
||||
return property(fget, None, None,
|
||||
"deprecated attribute %r, use pytest.%s" % (name,name))
|
||||
|
||||
|
||||
class Node(object):
|
||||
""" base class for all Nodes in the collection tree.
|
||||
Collector subclasses have children, Items are terminal nodes."""
|
||||
@@ -135,13 +148,13 @@ class Node(object):
|
||||
|
||||
#: the parent collector node.
|
||||
self.parent = parent
|
||||
|
||||
|
||||
#: the test config object
|
||||
self.config = config or parent.config
|
||||
|
||||
#: the collection this node is part of
|
||||
self.session = session or parent.session
|
||||
|
||||
|
||||
#: filesystem path where this node was collected from
|
||||
self.fspath = getattr(parent, 'fspath', None)
|
||||
self.ihook = self.session.gethookproxy(self.fspath)
|
||||
@@ -317,6 +330,8 @@ class Item(Node):
|
||||
""" a basic test invocation item. Note that for a single function
|
||||
there might be multiple test invocation items.
|
||||
"""
|
||||
nextitem = None
|
||||
|
||||
def reportinfo(self):
|
||||
return self.fspath, None, ""
|
||||
|
||||
@@ -374,6 +389,16 @@ class Session(FSCollector):
|
||||
return HookProxy(fspath, self.config)
|
||||
|
||||
def perform_collect(self, args=None, genitems=True):
|
||||
hook = self.config.hook
|
||||
try:
|
||||
items = self._perform_collect(args, genitems)
|
||||
hook.pytest_collection_modifyitems(session=self,
|
||||
config=self.config, items=items)
|
||||
finally:
|
||||
hook.pytest_collection_finish(session=self)
|
||||
return items
|
||||
|
||||
def _perform_collect(self, args, genitems):
|
||||
if args is None:
|
||||
args = self.config.args
|
||||
self.trace("perform_collect", self, args)
|
||||
@@ -451,16 +476,29 @@ class Session(FSCollector):
|
||||
return True
|
||||
|
||||
def _tryconvertpyarg(self, x):
|
||||
try:
|
||||
mod = __import__(x, None, None, ['__doc__'])
|
||||
except (ValueError, ImportError):
|
||||
return x
|
||||
p = py.path.local(mod.__file__)
|
||||
if p.purebasename == "__init__":
|
||||
p = p.dirpath()
|
||||
else:
|
||||
p = p.new(basename=p.purebasename+".py")
|
||||
return str(p)
|
||||
mod = None
|
||||
path = [os.path.abspath('.')] + sys.path
|
||||
for name in x.split('.'):
|
||||
# ignore anything that's not a proper name here
|
||||
# else something like --pyargs will mess up '.'
|
||||
# since imp.find_module will actually sometimes work for it
|
||||
# but it's supposed to be considered a filesystem path
|
||||
# not a package
|
||||
if name_re.match(name) is None:
|
||||
return x
|
||||
try:
|
||||
fd, mod, type_ = imp.find_module(name, path)
|
||||
except ImportError:
|
||||
return x
|
||||
else:
|
||||
if fd is not None:
|
||||
fd.close()
|
||||
|
||||
if type_[2] != imp.PKG_DIRECTORY:
|
||||
path = [os.path.dirname(mod)]
|
||||
else:
|
||||
path = [mod]
|
||||
return mod
|
||||
|
||||
def _parsearg(self, arg):
|
||||
""" return (fspath, names) tuple after checking the file exists. """
|
||||
@@ -478,7 +516,7 @@ class Session(FSCollector):
|
||||
raise pytest.UsageError(msg + arg)
|
||||
parts[0] = path
|
||||
return parts
|
||||
|
||||
|
||||
def matchnodes(self, matching, names):
|
||||
self.trace("matchnodes", matching, names)
|
||||
self.trace.root.indent += 1
|
||||
|
||||
@@ -14,12 +14,37 @@ def pytest_addoption(parser):
|
||||
"Terminate expression with ':' to make the first match match "
|
||||
"all subsequent tests (usually file-order). ")
|
||||
|
||||
group._addoption("-m",
|
||||
action="store", dest="markexpr", default="", metavar="MARKEXPR",
|
||||
help="only run tests matching given mark expression. "
|
||||
"example: -m 'mark1 and not mark2'."
|
||||
)
|
||||
|
||||
group.addoption("--markers", action="store_true", help=
|
||||
"show markers (builtin, plugin and per-project ones).")
|
||||
|
||||
parser.addini("markers", "markers for test functions", 'linelist')
|
||||
|
||||
def pytest_cmdline_main(config):
|
||||
if config.option.markers:
|
||||
config.pluginmanager.do_configure(config)
|
||||
tw = py.io.TerminalWriter()
|
||||
for line in config.getini("markers"):
|
||||
name, rest = line.split(":", 1)
|
||||
tw.write("@pytest.mark.%s:" % name, bold=True)
|
||||
tw.line(rest)
|
||||
tw.line()
|
||||
config.pluginmanager.do_unconfigure(config)
|
||||
return 0
|
||||
pytest_cmdline_main.tryfirst = True
|
||||
|
||||
def pytest_collection_modifyitems(items, config):
|
||||
keywordexpr = config.option.keyword
|
||||
if not keywordexpr:
|
||||
matchexpr = config.option.markexpr
|
||||
if not keywordexpr and not matchexpr:
|
||||
return
|
||||
selectuntil = False
|
||||
if keywordexpr[-1] == ":":
|
||||
if keywordexpr[-1:] == ":":
|
||||
selectuntil = True
|
||||
keywordexpr = keywordexpr[:-1]
|
||||
|
||||
@@ -29,21 +54,38 @@ def pytest_collection_modifyitems(items, config):
|
||||
if keywordexpr and skipbykeyword(colitem, keywordexpr):
|
||||
deselected.append(colitem)
|
||||
else:
|
||||
remaining.append(colitem)
|
||||
if selectuntil:
|
||||
keywordexpr = None
|
||||
if matchexpr:
|
||||
if not matchmark(colitem, matchexpr):
|
||||
deselected.append(colitem)
|
||||
continue
|
||||
remaining.append(colitem)
|
||||
|
||||
if deselected:
|
||||
config.hook.pytest_deselected(items=deselected)
|
||||
items[:] = remaining
|
||||
|
||||
class BoolDict:
|
||||
def __init__(self, mydict):
|
||||
self._mydict = mydict
|
||||
def __getitem__(self, name):
|
||||
return name in self._mydict
|
||||
|
||||
def matchmark(colitem, matchexpr):
|
||||
return eval(matchexpr, {}, BoolDict(colitem.obj.__dict__))
|
||||
|
||||
def pytest_configure(config):
|
||||
if config.option.strict:
|
||||
pytest.mark._config = config
|
||||
|
||||
def skipbykeyword(colitem, keywordexpr):
|
||||
""" return True if they given keyword expression means to
|
||||
skip this collector/item.
|
||||
"""
|
||||
if not keywordexpr:
|
||||
return
|
||||
|
||||
|
||||
itemkeywords = getkeywords(colitem)
|
||||
for key in filter(None, keywordexpr.split()):
|
||||
eor = key[:1] == '-'
|
||||
@@ -77,15 +119,31 @@ class MarkGenerator:
|
||||
@py.test.mark.slowtest
|
||||
def test_function():
|
||||
pass
|
||||
|
||||
|
||||
will set a 'slowtest' :class:`MarkInfo` object
|
||||
on the ``test_function`` object. """
|
||||
|
||||
def __getattr__(self, name):
|
||||
if name[0] == "_":
|
||||
raise AttributeError(name)
|
||||
if hasattr(self, '_config'):
|
||||
self._check(name)
|
||||
return MarkDecorator(name)
|
||||
|
||||
def _check(self, name):
|
||||
try:
|
||||
if name in self._markers:
|
||||
return
|
||||
except AttributeError:
|
||||
pass
|
||||
self._markers = l = set()
|
||||
for line in self._config.getini("markers"):
|
||||
beginning = line.split(":", 1)
|
||||
x = beginning[0].split("(", 1)[0]
|
||||
l.add(x)
|
||||
if name not in self._markers:
|
||||
raise AttributeError("%r not a registered marker" % (name,))
|
||||
|
||||
class MarkDecorator:
|
||||
""" A decorator for test functions and test classes. When applied
|
||||
it will create :class:`MarkInfo` objects which may be
|
||||
@@ -153,7 +211,7 @@ class MarkInfo:
|
||||
|
||||
def __repr__(self):
|
||||
return "<MarkInfo %r args=%r kwargs=%r>" % (
|
||||
self._name, self.args, self.kwargs)
|
||||
self.name, self.args, self.kwargs)
|
||||
|
||||
def pytest_itemcollected(item):
|
||||
if not isinstance(item, pytest.Function):
|
||||
|
||||
@@ -13,6 +13,7 @@ def pytest_runtest_makereport(__multicall__, item, call):
|
||||
call.excinfo = call2.excinfo
|
||||
|
||||
|
||||
@pytest.mark.trylast
|
||||
def pytest_runtest_setup(item):
|
||||
if isinstance(item, (pytest.Function)):
|
||||
if isinstance(item.parent, pytest.Generator):
|
||||
|
||||
@@ -38,7 +38,11 @@ def pytest_unconfigure(config):
|
||||
del tr._tw.__dict__['write']
|
||||
|
||||
def getproxy():
|
||||
return py.std.xmlrpclib.ServerProxy(url.xmlrpc).pastes
|
||||
if sys.version_info < (3, 0):
|
||||
from xmlrpclib import ServerProxy
|
||||
else:
|
||||
from xmlrpc.client import ServerProxy
|
||||
return ServerProxy(url.xmlrpc).pastes
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
if terminalreporter.config.option.pastebin != "failed":
|
||||
|
||||
@@ -19,11 +19,13 @@ def pytest_configure(config):
|
||||
class pytestPDB:
|
||||
""" Pseudo PDB that defers to the real pdb. """
|
||||
item = None
|
||||
collector = None
|
||||
|
||||
def set_trace(self):
|
||||
""" invoke PDB set_trace debugging, dropping any IO capturing. """
|
||||
frame = sys._getframe().f_back
|
||||
item = getattr(self, 'item', None)
|
||||
item = self.item or self.collector
|
||||
|
||||
if item is not None:
|
||||
capman = item.config.pluginmanager.getplugin("capturemanager")
|
||||
out, err = capman.suspendcapture()
|
||||
@@ -38,6 +40,14 @@ def pdbitem(item):
|
||||
pytestPDB.item = item
|
||||
pytest_runtest_setup = pytest_runtest_call = pytest_runtest_teardown = pdbitem
|
||||
|
||||
@pytest.mark.tryfirst
|
||||
def pytest_make_collect_report(__multicall__, collector):
|
||||
try:
|
||||
pytestPDB.collector = collector
|
||||
return __multicall__.execute()
|
||||
finally:
|
||||
pytestPDB.collector = None
|
||||
|
||||
def pytest_runtest_makereport():
|
||||
pytestPDB.item = None
|
||||
|
||||
@@ -60,7 +70,13 @@ class PdbInvoke:
|
||||
tw.sep(">", "traceback")
|
||||
rep.toterminal(tw)
|
||||
tw.sep(">", "entering PDB")
|
||||
post_mortem(call.excinfo._excinfo[2])
|
||||
# A doctest.UnexpectedException is not useful for post_mortem.
|
||||
# Use the underlying exception instead:
|
||||
if isinstance(call.excinfo.value, py.std.doctest.UnexpectedException):
|
||||
tb = call.excinfo.value.exc_info[2]
|
||||
else:
|
||||
tb = call.excinfo._excinfo[2]
|
||||
post_mortem(tb)
|
||||
rep._pdbshown = True
|
||||
return rep
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ import re
|
||||
import inspect
|
||||
import time
|
||||
from fnmatch import fnmatch
|
||||
from _pytest.main import Session
|
||||
from _pytest.main import Session, EXIT_OK
|
||||
from py.builtin import print_
|
||||
from _pytest.core import HookRelay
|
||||
|
||||
@@ -25,6 +25,7 @@ def pytest_configure(config):
|
||||
_pytest_fullpath
|
||||
except NameError:
|
||||
_pytest_fullpath = os.path.abspath(pytest.__file__.rstrip("oc"))
|
||||
_pytest_fullpath = _pytest_fullpath.replace("$py.class", ".py")
|
||||
|
||||
def pytest_funcarg___pytest(request):
|
||||
return PytestArg(request)
|
||||
@@ -292,13 +293,19 @@ class TmpTestdir:
|
||||
assert '::' not in str(arg)
|
||||
p = py.path.local(arg)
|
||||
x = session.fspath.bestrelpath(p)
|
||||
return session.perform_collect([x], genitems=False)[0]
|
||||
config.hook.pytest_sessionstart(session=session)
|
||||
res = session.perform_collect([x], genitems=False)[0]
|
||||
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
|
||||
return res
|
||||
|
||||
def getpathnode(self, path):
|
||||
config = self.parseconfig(path)
|
||||
config = self.parseconfigure(path)
|
||||
session = Session(config)
|
||||
x = session.fspath.bestrelpath(path)
|
||||
return session.perform_collect([x], genitems=False)[0]
|
||||
config.hook.pytest_sessionstart(session=session)
|
||||
res = session.perform_collect([x], genitems=False)[0]
|
||||
config.hook.pytest_sessionfinish(session=session, exitstatus=EXIT_OK)
|
||||
return res
|
||||
|
||||
def genitems(self, colitems):
|
||||
session = colitems[0].session
|
||||
@@ -307,14 +314,6 @@ class TmpTestdir:
|
||||
result.extend(session.genitems(colitem))
|
||||
return result
|
||||
|
||||
def inline_genitems(self, *args):
|
||||
#config = self.parseconfig(*args)
|
||||
config = self.parseconfigure(*args)
|
||||
rec = self.getreportrecorder(config)
|
||||
session = Session(config)
|
||||
session.perform_collect()
|
||||
return session.items, rec
|
||||
|
||||
def runitem(self, source):
|
||||
# used from runner functional tests
|
||||
item = self.getitem(source)
|
||||
@@ -335,62 +334,57 @@ class TmpTestdir:
|
||||
l = list(args) + [p]
|
||||
reprec = self.inline_run(*l)
|
||||
reports = reprec.getreports("pytest_runtest_logreport")
|
||||
assert len(reports) == 1, reports
|
||||
return reports[0]
|
||||
assert len(reports) == 3, reports # setup/call/teardown
|
||||
return reports[1]
|
||||
|
||||
def inline_genitems(self, *args):
|
||||
return self.inprocess_run(list(args) + ['--collectonly'])
|
||||
|
||||
def inline_run(self, *args):
|
||||
args = ("-s", ) + args # otherwise FD leakage
|
||||
config = self.parseconfig(*args)
|
||||
reprec = self.getreportrecorder(config)
|
||||
#config.pluginmanager.do_configure(config)
|
||||
config.hook.pytest_cmdline_main(config=config)
|
||||
#config.pluginmanager.do_unconfigure(config)
|
||||
return reprec
|
||||
items, rec = self.inprocess_run(args)
|
||||
return rec
|
||||
|
||||
def config_preparse(self):
|
||||
config = self.Config()
|
||||
for plugin in self.plugins:
|
||||
if isinstance(plugin, str):
|
||||
config.pluginmanager.import_plugin(plugin)
|
||||
else:
|
||||
if isinstance(plugin, dict):
|
||||
plugin = PseudoPlugin(plugin)
|
||||
if not config.pluginmanager.isregistered(plugin):
|
||||
config.pluginmanager.register(plugin)
|
||||
return config
|
||||
def inprocess_run(self, args, plugins=None):
|
||||
rec = []
|
||||
items = []
|
||||
class Collect:
|
||||
def pytest_configure(x, config):
|
||||
rec.append(self.getreportrecorder(config))
|
||||
def pytest_itemcollected(self, item):
|
||||
items.append(item)
|
||||
if not plugins:
|
||||
plugins = []
|
||||
plugins.append(Collect())
|
||||
ret = self.pytestmain(list(args), plugins=[Collect()])
|
||||
reprec = rec[0]
|
||||
reprec.ret = ret
|
||||
assert len(rec) == 1
|
||||
return items, reprec
|
||||
|
||||
def parseconfig(self, *args):
|
||||
if not args:
|
||||
args = (self.tmpdir,)
|
||||
config = self.config_preparse()
|
||||
args = list(args)
|
||||
args = [str(x) for x in args]
|
||||
for x in args:
|
||||
if str(x).startswith('--basetemp'):
|
||||
break
|
||||
else:
|
||||
args.append("--basetemp=%s" % self.tmpdir.dirpath('basetemp'))
|
||||
config.parse(args)
|
||||
import _pytest.core
|
||||
config = _pytest.core._prepareconfig(args, self.plugins)
|
||||
# the in-process pytest invocation needs to avoid leaking FDs
|
||||
# so we register a "reset_capturings" callmon the capturing manager
|
||||
# and make sure it gets called
|
||||
config._cleanup.append(
|
||||
config.pluginmanager.getplugin("capturemanager").reset_capturings)
|
||||
import _pytest.config
|
||||
self.request.addfinalizer(
|
||||
lambda: _pytest.config.pytest_unconfigure(config))
|
||||
return config
|
||||
|
||||
def reparseconfig(self, args=None):
|
||||
""" this is used from tests that want to re-invoke parse(). """
|
||||
if not args:
|
||||
args = [self.tmpdir]
|
||||
oldconfig = getattr(py.test, 'config', None)
|
||||
try:
|
||||
c = py.test.config = self.Config()
|
||||
c.basetemp = py.path.local.make_numbered_dir(prefix="reparse",
|
||||
keep=0, rootdir=self.tmpdir, lock_timeout=None)
|
||||
c.parse(args)
|
||||
return c
|
||||
finally:
|
||||
py.test.config = oldconfig
|
||||
|
||||
def parseconfigure(self, *args):
|
||||
config = self.parseconfig(*args)
|
||||
config.pluginmanager.do_configure(config)
|
||||
self.request.addfinalizer(lambda:
|
||||
config.pluginmanager.do_unconfigure(config))
|
||||
config.pluginmanager.do_unconfigure(config))
|
||||
return config
|
||||
|
||||
def getitem(self, source, funcname="test_func"):
|
||||
@@ -410,7 +404,6 @@ class TmpTestdir:
|
||||
self.makepyfile(__init__ = "#")
|
||||
self.config = config = self.parseconfigure(path, *configargs)
|
||||
node = self.getnode(config, path)
|
||||
#config.pluginmanager.do_unconfigure(config)
|
||||
return node
|
||||
|
||||
def collect_by_name(self, modcol, name):
|
||||
@@ -427,9 +420,16 @@ class TmpTestdir:
|
||||
return py.std.subprocess.Popen(cmdargs, stdout=stdout, stderr=stderr, **kw)
|
||||
|
||||
def pytestmain(self, *args, **kwargs):
|
||||
ret = pytest.main(*args, **kwargs)
|
||||
if ret == 2:
|
||||
raise KeyboardInterrupt()
|
||||
class ResetCapturing:
|
||||
@pytest.mark.trylast
|
||||
def pytest_unconfigure(self, config):
|
||||
capman = config.pluginmanager.getplugin("capturemanager")
|
||||
capman.reset_capturings()
|
||||
plugins = kwargs.setdefault("plugins", [])
|
||||
rc = ResetCapturing()
|
||||
plugins.append(rc)
|
||||
return pytest.main(*args, **kwargs)
|
||||
|
||||
def run(self, *cmdargs):
|
||||
return self._run(*cmdargs)
|
||||
|
||||
@@ -518,6 +518,8 @@ class TmpTestdir:
|
||||
pexpect = py.test.importorskip("pexpect", "2.4")
|
||||
if hasattr(sys, 'pypy_version_info') and '64' in py.std.platform.machine():
|
||||
pytest.skip("pypy-64 bit not supported")
|
||||
if sys.platform == "darwin":
|
||||
pytest.xfail("pexpect does not work reliably on darwin?!")
|
||||
logfile = self.tmpdir.join("spawn.out")
|
||||
child = pexpect.spawn(cmd, logfile=logfile.open("w"))
|
||||
child.timeout = expect_timeout
|
||||
@@ -530,10 +532,6 @@ def getdecoded(out):
|
||||
return "INTERNAL not-utf8-decodeable, truncated string:\n%s" % (
|
||||
py.io.saferepr(out),)
|
||||
|
||||
class PseudoPlugin:
|
||||
def __init__(self, vars):
|
||||
self.__dict__.update(vars)
|
||||
|
||||
class ReportRecorder(object):
|
||||
def __init__(self, hook):
|
||||
self.hook = hook
|
||||
@@ -555,10 +553,17 @@ class ReportRecorder(object):
|
||||
def getreports(self, names="pytest_runtest_logreport pytest_collectreport"):
|
||||
return [x.report for x in self.getcalls(names)]
|
||||
|
||||
def matchreport(self, inamepart="", names="pytest_runtest_logreport pytest_collectreport", when=None):
|
||||
def matchreport(self, inamepart="",
|
||||
names="pytest_runtest_logreport pytest_collectreport", when=None):
|
||||
""" return a testreport whose dotted import path matches """
|
||||
l = []
|
||||
for rep in self.getreports(names=names):
|
||||
try:
|
||||
if not when and rep.when != "call" and rep.passed:
|
||||
# setup/teardown passing reports - let's ignore those
|
||||
continue
|
||||
except AttributeError:
|
||||
pass
|
||||
if when and getattr(rep, 'when', None) != when:
|
||||
continue
|
||||
if not inamepart or inamepart in rep.nodeid.split("::"):
|
||||
|
||||
@@ -4,6 +4,7 @@ import inspect
|
||||
import sys
|
||||
import pytest
|
||||
from py._code.code import TerminalRepr
|
||||
from _pytest.monkeypatch import monkeypatch
|
||||
|
||||
import _pytest
|
||||
cutdir = py.path.local(_pytest.__file__).dirpath()
|
||||
@@ -26,6 +27,23 @@ def pytest_cmdline_main(config):
|
||||
showfuncargs(config)
|
||||
return 0
|
||||
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
try:
|
||||
param = metafunc.function.parametrize
|
||||
except AttributeError:
|
||||
return
|
||||
metafunc.parametrize(*param.args, **param.kwargs)
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers",
|
||||
"parametrize(argnames, argvalues): call a test function multiple "
|
||||
"times passing in multiple different argument value sets. Example: "
|
||||
"@parametrize('arg1', [1,2]) would lead to two calls of the decorated "
|
||||
"test function, one with arg1=1 and another with arg1=2."
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.trylast
|
||||
def pytest_namespace():
|
||||
raises.Exception = pytest.fail.Exception
|
||||
@@ -369,12 +387,13 @@ class FuncargLookupErrorRepr(TerminalRepr):
|
||||
tw.line()
|
||||
tw.line("%s:%d" % (self.filename, self.firstlineno+1))
|
||||
|
||||
|
||||
class Generator(FunctionMixin, PyCollectorMixin, pytest.Collector):
|
||||
def collect(self):
|
||||
# test generators are seen as collectors but they also
|
||||
# invoke setup/teardown on popular request
|
||||
# (induced by the common "test_*" naming shared with normal tests)
|
||||
self.config._setupstate.prepare(self)
|
||||
self.session._setupstate.prepare(self)
|
||||
# see FunctionMixin.setup and test_setupstate_is_preserved_134
|
||||
self._preservedparent = self.parent.obj
|
||||
l = []
|
||||
@@ -425,6 +444,7 @@ class Function(FunctionMixin, pytest.Item):
|
||||
"yielded functions (deprecated) cannot have funcargs")
|
||||
else:
|
||||
if callspec is not None:
|
||||
self.callspec = callspec
|
||||
self.funcargs = callspec.funcargs or {}
|
||||
self._genid = callspec.id
|
||||
if hasattr(callspec, "param"):
|
||||
@@ -501,15 +521,59 @@ def fillfuncargs(function):
|
||||
request._fillfuncargs()
|
||||
|
||||
_notexists = object()
|
||||
class CallSpec:
|
||||
def __init__(self, funcargs, id, param):
|
||||
self.funcargs = funcargs
|
||||
self.id = id
|
||||
|
||||
class CallSpec2(object):
|
||||
def __init__(self, metafunc):
|
||||
self.metafunc = metafunc
|
||||
self.funcargs = {}
|
||||
self._idlist = []
|
||||
self.params = {}
|
||||
self._globalid = _notexists
|
||||
self._globalid_args = set()
|
||||
self._globalparam = _notexists
|
||||
|
||||
def copy(self, metafunc):
|
||||
cs = CallSpec2(self.metafunc)
|
||||
cs.funcargs.update(self.funcargs)
|
||||
cs.params.update(self.params)
|
||||
cs._idlist = list(self._idlist)
|
||||
cs._globalid = self._globalid
|
||||
cs._globalid_args = self._globalid_args
|
||||
cs._globalparam = self._globalparam
|
||||
return cs
|
||||
|
||||
def _checkargnotcontained(self, arg):
|
||||
if arg in self.params or arg in self.funcargs:
|
||||
raise ValueError("duplicate %r" %(arg,))
|
||||
|
||||
def getparam(self, name):
|
||||
try:
|
||||
return self.params[name]
|
||||
except KeyError:
|
||||
if self._globalparam is _notexists:
|
||||
raise ValueError(name)
|
||||
return self._globalparam
|
||||
|
||||
@property
|
||||
def id(self):
|
||||
return "-".join(filter(None, self._idlist))
|
||||
|
||||
def setmulti(self, valtype, argnames, valset, id):
|
||||
for arg,val in zip(argnames, valset):
|
||||
self._checkargnotcontained(arg)
|
||||
getattr(self, valtype)[arg] = val
|
||||
self._idlist.append(id)
|
||||
|
||||
def setall(self, funcargs, id, param):
|
||||
for x in funcargs:
|
||||
self._checkargnotcontained(x)
|
||||
self.funcargs.update(funcargs)
|
||||
if id is not _notexists:
|
||||
self._idlist.append(id)
|
||||
if param is not _notexists:
|
||||
self.param = param
|
||||
def __repr__(self):
|
||||
return "<CallSpec id=%r param=%r funcargs=%r>" %(
|
||||
self.id, getattr(self, 'param', '?'), self.funcargs)
|
||||
assert self._globalparam is _notexists
|
||||
self._globalparam = param
|
||||
|
||||
|
||||
class Metafunc:
|
||||
def __init__(self, function, config=None, cls=None, module=None):
|
||||
@@ -523,31 +587,69 @@ class Metafunc:
|
||||
self._calls = []
|
||||
self._ids = py.builtin.set()
|
||||
|
||||
def parametrize(self, argnames, argvalues, indirect=False, ids=None):
|
||||
""" parametrize calls to the underlying test function during
|
||||
the collection phase of a test run. parametrize may be called
|
||||
multiple times for disjunct argnames sets.
|
||||
|
||||
:arg argnames: an argument name or a list of argument names
|
||||
|
||||
:arg argvalues: a list of values for a single argument if argnames
|
||||
specified a single argument only or a list of tuples which specify
|
||||
values for the multiple argument names.
|
||||
|
||||
:arg indirect: if True each argvalue corresponding to an argument will be
|
||||
passed as request.param to the respective funcarg factory so that
|
||||
it can perform more expensive setups during the setup phase of
|
||||
a test rather than at collection time (which is the default).
|
||||
|
||||
:arg ids: list of string ids corresponding to the (list of) argvalues
|
||||
so that they are part of the test id. If no ids are provided
|
||||
they will be generated automatically from the argvalues.
|
||||
"""
|
||||
if not isinstance(argnames, (tuple, list)):
|
||||
argnames = (argnames,)
|
||||
argvalues = [(val,) for val in argvalues]
|
||||
for arg in argnames:
|
||||
if arg not in self.funcargnames:
|
||||
raise ValueError("%r has no argument %r" %(self.function, arg))
|
||||
valtype = indirect and "params" or "funcargs"
|
||||
if not ids:
|
||||
idmaker = IDMaker()
|
||||
ids = list(map(idmaker, argvalues))
|
||||
newcalls = []
|
||||
for callspec in self._calls or [CallSpec2(self)]:
|
||||
for i, valset in enumerate(argvalues):
|
||||
assert len(valset) == len(argnames)
|
||||
newcallspec = callspec.copy(self)
|
||||
newcallspec.setmulti(valtype, argnames, valset, ids[i])
|
||||
newcalls.append(newcallspec)
|
||||
self._calls = newcalls
|
||||
|
||||
def addcall(self, funcargs=None, id=_notexists, param=_notexists):
|
||||
""" add a new call to the underlying test function during the
|
||||
collection phase of a test run. Note that request.addcall() is
|
||||
""" (deprecated, use parametrize) add a new call to the underlying
|
||||
test function during
|
||||
the collection phase of a test run. Note that request.addcall() is
|
||||
called during the test collection phase prior and independently
|
||||
to actual test execution. Therefore you should perform setup
|
||||
of resources in a funcarg factory which can be instrumented
|
||||
with the ``param``.
|
||||
to actual test execution. You should only use addcall()
|
||||
if you need to specify multiple arguments of a test function
|
||||
|
||||
:arg funcargs: argument keyword dictionary used when invoking
|
||||
the test function.
|
||||
|
||||
:arg id: used for reporting and identification purposes. If you
|
||||
don't supply an `id` the length of the currently
|
||||
list of calls to the test function will be used.
|
||||
don't supply an `id` an automatic unique id will be generated.
|
||||
|
||||
:arg param: will be exposed to a later funcarg factory invocation
|
||||
through the ``request.param`` attribute. It allows to
|
||||
defer test fixture setup activities to when an actual
|
||||
test is run.
|
||||
:arg param: a parameter which will be exposed to a later funcarg factory
|
||||
invocation through the ``request.param`` attribute.
|
||||
"""
|
||||
assert funcargs is None or isinstance(funcargs, dict)
|
||||
if funcargs is not None:
|
||||
for name in funcargs:
|
||||
if name not in self.funcargnames:
|
||||
pytest.fail("funcarg %r not used in this function." % name)
|
||||
else:
|
||||
funcargs = {}
|
||||
if id is None:
|
||||
raise ValueError("id=None not allowed")
|
||||
if id is _notexists:
|
||||
@@ -556,11 +658,26 @@ class Metafunc:
|
||||
if id in self._ids:
|
||||
raise ValueError("duplicate id %r" % id)
|
||||
self._ids.add(id)
|
||||
self._calls.append(CallSpec(funcargs, id, param))
|
||||
|
||||
cs = CallSpec2(self)
|
||||
cs.setall(funcargs, id, param)
|
||||
self._calls.append(cs)
|
||||
|
||||
class IDMaker:
|
||||
def __init__(self):
|
||||
self.counter = 0
|
||||
def __call__(self, valset):
|
||||
l = []
|
||||
for val in valset:
|
||||
if not isinstance(val, (int, str)):
|
||||
val = "."+str(self.counter)
|
||||
self.counter += 1
|
||||
l.append(str(val))
|
||||
return "-".join(l)
|
||||
|
||||
class FuncargRequest:
|
||||
""" A request for function arguments from a test function.
|
||||
|
||||
|
||||
Note that there is an optional ``param`` attribute in case
|
||||
there was an invocation to metafunc.addcall(param=...).
|
||||
If no such call was done in a ``pytest_generate_tests``
|
||||
@@ -693,11 +810,18 @@ class FuncargRequest:
|
||||
self._raiselookupfailed(argname)
|
||||
funcargfactory = self._name2factory[argname].pop()
|
||||
oldarg = self._currentarg
|
||||
self._currentarg = argname
|
||||
mp = monkeypatch()
|
||||
mp.setattr(self, '_currentarg', argname)
|
||||
try:
|
||||
param = self._pyfuncitem.callspec.getparam(argname)
|
||||
except (AttributeError, ValueError):
|
||||
pass
|
||||
else:
|
||||
mp.setattr(self, 'param', param, raising=False)
|
||||
try:
|
||||
self._funcargs[argname] = res = funcargfactory(request=self)
|
||||
finally:
|
||||
self._currentarg = oldarg
|
||||
mp.undo()
|
||||
return res
|
||||
|
||||
def _getscopeitem(self, scope):
|
||||
@@ -721,7 +845,7 @@ class FuncargRequest:
|
||||
|
||||
def _addfinalizer(self, finalizer, scope):
|
||||
colitem = self._getscopeitem(scope)
|
||||
self.config._setupstate.addfinalizer(
|
||||
self._pyfuncitem.session._setupstate.addfinalizer(
|
||||
finalizer=finalizer, colitem=colitem)
|
||||
|
||||
def __repr__(self):
|
||||
@@ -742,8 +866,10 @@ class FuncargRequest:
|
||||
raise self.LookupError(msg)
|
||||
|
||||
def showfuncargs(config):
|
||||
from _pytest.main import Session
|
||||
session = Session(config)
|
||||
from _pytest.main import wrap_session
|
||||
return wrap_session(config, _showfuncargs_main)
|
||||
|
||||
def _showfuncargs_main(config, session):
|
||||
session.perform_collect()
|
||||
if session.items:
|
||||
plugins = session.items[0].getplugins()
|
||||
|
||||
@@ -63,6 +63,8 @@ class ResultLog(object):
|
||||
self.write_log_entry(testpath, lettercode, longrepr)
|
||||
|
||||
def pytest_runtest_logreport(self, report):
|
||||
if report.when != "call" and report.passed:
|
||||
return
|
||||
res = self.config.hook.pytest_report_teststatus(report=report)
|
||||
code = res[1]
|
||||
if code == 'x':
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
""" basic collect and runtest protocol implementations """
|
||||
|
||||
import py, sys
|
||||
import py, sys, time
|
||||
from py._code.code import TerminalRepr
|
||||
|
||||
def pytest_namespace():
|
||||
@@ -14,53 +14,97 @@ def pytest_namespace():
|
||||
#
|
||||
# pytest plugin hooks
|
||||
|
||||
# XXX move to pytest_sessionstart and fix py.test owns tests
|
||||
def pytest_configure(config):
|
||||
config._setupstate = SetupState()
|
||||
def pytest_addoption(parser):
|
||||
group = parser.getgroup("terminal reporting", "reporting", after="general")
|
||||
group.addoption('--durations',
|
||||
action="store", type="int", default=None, metavar="N",
|
||||
help="show N slowest setup/test durations (N=0 for all)."),
|
||||
|
||||
def pytest_terminal_summary(terminalreporter):
|
||||
durations = terminalreporter.config.option.durations
|
||||
if durations is None:
|
||||
return
|
||||
tr = terminalreporter
|
||||
dlist = []
|
||||
for replist in tr.stats.values():
|
||||
for rep in replist:
|
||||
if hasattr(rep, 'duration'):
|
||||
dlist.append(rep)
|
||||
if not dlist:
|
||||
return
|
||||
dlist.sort(key=lambda x: x.duration)
|
||||
dlist.reverse()
|
||||
if not durations:
|
||||
tr.write_sep("=", "slowest test durations")
|
||||
else:
|
||||
tr.write_sep("=", "slowest %s test durations" % durations)
|
||||
dlist = dlist[:durations]
|
||||
|
||||
for rep in dlist:
|
||||
nodeid = rep.nodeid.replace("::()::", "::")
|
||||
tr.write_line("%02.2fs %-8s %s" %
|
||||
(rep.duration, rep.when, nodeid))
|
||||
|
||||
def pytest_sessionstart(session):
|
||||
session._setupstate = SetupState()
|
||||
|
||||
def pytest_sessionfinish(session, exitstatus):
|
||||
if hasattr(session.config, '_setupstate'):
|
||||
hook = session.config.hook
|
||||
rep = hook.pytest__teardown_final(session=session)
|
||||
if rep:
|
||||
hook.pytest__teardown_final_logerror(session=session, report=rep)
|
||||
session.exitstatus = 1
|
||||
hook = session.config.hook
|
||||
rep = hook.pytest__teardown_final(session=session)
|
||||
if rep:
|
||||
hook.pytest__teardown_final_logerror(session=session, report=rep)
|
||||
session.exitstatus = 1
|
||||
|
||||
class NodeInfo:
|
||||
def __init__(self, location):
|
||||
self.location = location
|
||||
|
||||
def perform_pending_teardown(config, nextitem):
|
||||
try:
|
||||
olditem, log = config._pendingteardown
|
||||
except AttributeError:
|
||||
pass
|
||||
else:
|
||||
del config._pendingteardown
|
||||
olditem.nextitem = nextitem
|
||||
call_and_report(olditem, "teardown", log)
|
||||
|
||||
def pytest_runtest_protocol(item):
|
||||
perform_pending_teardown(item.config, item)
|
||||
item.ihook.pytest_runtest_logstart(
|
||||
nodeid=item.nodeid, location=item.location,
|
||||
)
|
||||
runtestprotocol(item)
|
||||
runtestprotocol(item, teardowndelayed=True)
|
||||
return True
|
||||
|
||||
def runtestprotocol(item, log=True):
|
||||
def runtestprotocol(item, log=True, teardowndelayed=False):
|
||||
rep = call_and_report(item, "setup", log)
|
||||
reports = [rep]
|
||||
if rep.passed:
|
||||
reports.append(call_and_report(item, "call", log))
|
||||
reports.append(call_and_report(item, "teardown", log))
|
||||
if teardowndelayed:
|
||||
item.config._pendingteardown = item, log
|
||||
else:
|
||||
reports.append(call_and_report(item, "teardown", log))
|
||||
return reports
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
item.config._setupstate.prepare(item)
|
||||
item.session._setupstate.prepare(item)
|
||||
|
||||
def pytest_runtest_call(item):
|
||||
item.runtest()
|
||||
|
||||
def pytest_runtest_teardown(item):
|
||||
item.config._setupstate.teardown_exact(item)
|
||||
item.session._setupstate.teardown_exact(item)
|
||||
|
||||
def pytest__teardown_final(session):
|
||||
call = CallInfo(session.config._setupstate.teardown_all, when="teardown")
|
||||
if call.excinfo:
|
||||
ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir)
|
||||
call.excinfo.traceback = ntraceback.filter()
|
||||
longrepr = call.excinfo.getrepr(funcargs=True)
|
||||
return TeardownErrorReport(longrepr)
|
||||
perform_pending_teardown(session.config, None)
|
||||
#call = CallInfo(session._setupstate.teardown_all, when="teardown")
|
||||
#if call.excinfo:
|
||||
# ntraceback = call.excinfo.traceback .cut(excludepath=py._pydir)
|
||||
# call.excinfo.traceback = ntraceback.filter()
|
||||
# longrepr = call.excinfo.getrepr(funcargs=True)
|
||||
# return TeardownErrorReport(longrepr)
|
||||
|
||||
def pytest_report_teststatus(report):
|
||||
if report.when in ("setup", "teardown"):
|
||||
@@ -80,7 +124,7 @@ def call_and_report(item, when, log=True):
|
||||
call = call_runtest_hook(item, when)
|
||||
hook = item.ihook
|
||||
report = hook.pytest_runtest_makereport(item=item, call=call)
|
||||
if log and (when == "call" or not report.passed):
|
||||
if log:
|
||||
hook.pytest_runtest_logreport(report=report)
|
||||
return report
|
||||
|
||||
@@ -97,12 +141,16 @@ class CallInfo:
|
||||
#: context of invocation: one of "setup", "call",
|
||||
#: "teardown", "memocollect"
|
||||
self.when = when
|
||||
self.start = time.time()
|
||||
try:
|
||||
self.result = func()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
self.excinfo = py.code.ExceptionInfo()
|
||||
try:
|
||||
self.result = func()
|
||||
except KeyboardInterrupt:
|
||||
raise
|
||||
except:
|
||||
self.excinfo = py.code.ExceptionInfo()
|
||||
finally:
|
||||
self.stop = time.time()
|
||||
|
||||
def __repr__(self):
|
||||
if self.excinfo:
|
||||
@@ -141,6 +189,7 @@ class BaseReport(object):
|
||||
|
||||
def pytest_runtest_makereport(item, call):
|
||||
when = call.when
|
||||
duration = call.stop-call.start
|
||||
keywords = dict([(x,1) for x in item.keywords])
|
||||
excinfo = call.excinfo
|
||||
if not call.excinfo:
|
||||
@@ -162,14 +211,15 @@ def pytest_runtest_makereport(item, call):
|
||||
else: # exception in setup or teardown
|
||||
longrepr = item._repr_failure_py(excinfo)
|
||||
return TestReport(item.nodeid, item.location,
|
||||
keywords, outcome, longrepr, when)
|
||||
keywords, outcome, longrepr, when,
|
||||
duration=duration)
|
||||
|
||||
class TestReport(BaseReport):
|
||||
""" Basic test report object (also used for setup and teardown calls if
|
||||
they fail).
|
||||
"""
|
||||
def __init__(self, nodeid, location,
|
||||
keywords, outcome, longrepr, when):
|
||||
keywords, outcome, longrepr, when, sections=(), duration=0):
|
||||
#: normalized collection node id
|
||||
self.nodeid = nodeid
|
||||
|
||||
@@ -181,16 +231,23 @@ class TestReport(BaseReport):
|
||||
#: a name -> value dictionary containing all keywords and
|
||||
#: markers associated with a test invocation.
|
||||
self.keywords = keywords
|
||||
|
||||
|
||||
#: test outcome, always one of "passed", "failed", "skipped".
|
||||
self.outcome = outcome
|
||||
|
||||
#: None or a failure representation.
|
||||
self.longrepr = longrepr
|
||||
|
||||
|
||||
#: one of 'setup', 'call', 'teardown' to indicate runtest phase.
|
||||
self.when = when
|
||||
|
||||
#: list of (secname, data) extra information which needs to
|
||||
#: marshallable
|
||||
self.sections = list(sections)
|
||||
|
||||
#: time it took to run just the test
|
||||
self.duration = duration
|
||||
|
||||
def __repr__(self):
|
||||
return "<TestReport %r when=%r outcome=%r>" % (
|
||||
self.nodeid, self.when, self.outcome)
|
||||
@@ -200,6 +257,7 @@ class TeardownErrorReport(BaseReport):
|
||||
when = "teardown"
|
||||
def __init__(self, longrepr):
|
||||
self.longrepr = longrepr
|
||||
self.sections = []
|
||||
|
||||
def pytest_make_collect_report(collector):
|
||||
call = CallInfo(collector._memocollect, "memocollect")
|
||||
@@ -221,11 +279,12 @@ def pytest_make_collect_report(collector):
|
||||
getattr(call, 'result', None))
|
||||
|
||||
class CollectReport(BaseReport):
|
||||
def __init__(self, nodeid, outcome, longrepr, result):
|
||||
def __init__(self, nodeid, outcome, longrepr, result, sections=()):
|
||||
self.nodeid = nodeid
|
||||
self.outcome = outcome
|
||||
self.longrepr = longrepr
|
||||
self.result = result or []
|
||||
self.sections = list(sections)
|
||||
|
||||
@property
|
||||
def location(self):
|
||||
@@ -280,19 +339,22 @@ class SetupState(object):
|
||||
assert not self._finalizers
|
||||
|
||||
def teardown_exact(self, item):
|
||||
if self.stack and item == self.stack[-1]:
|
||||
colitem = item.nextitem
|
||||
needed_collectors = colitem and colitem.listchain() or []
|
||||
self._teardown_towards(needed_collectors)
|
||||
|
||||
def _teardown_towards(self, needed_collectors):
|
||||
while self.stack:
|
||||
if self.stack == needed_collectors[:len(self.stack)]:
|
||||
break
|
||||
self._pop_and_teardown()
|
||||
else:
|
||||
self._callfinalizers(item)
|
||||
|
||||
def prepare(self, colitem):
|
||||
""" setup objects along the collector chain to the test-method
|
||||
and teardown previously setup objects."""
|
||||
needed_collectors = colitem.listchain()
|
||||
while self.stack:
|
||||
if self.stack == needed_collectors[:len(self.stack)]:
|
||||
break
|
||||
self._pop_and_teardown()
|
||||
self._teardown_towards(needed_collectors)
|
||||
|
||||
# check if the last collection node has raised an error
|
||||
for col in self.stack:
|
||||
if hasattr(col, '_prepare_exc'):
|
||||
|
||||
@@ -9,6 +9,21 @@ def pytest_addoption(parser):
|
||||
action="store_true", dest="runxfail", default=False,
|
||||
help="run tests even if they are marked xfail")
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers",
|
||||
"skipif(*conditions): skip the given test function if evaluation "
|
||||
"of all conditions has a True value. Evaluation happens within the "
|
||||
"module global context. Example: skipif('sys.platform == \"win32\"') "
|
||||
"skips the test if we are on the win32 platform. "
|
||||
)
|
||||
config.addinivalue_line("markers",
|
||||
"xfail(*conditions, reason=None, run=True): mark the the test function "
|
||||
"as an expected failure. Optionally specify a reason and run=False "
|
||||
"if you don't even want to execute the test function. Any positional "
|
||||
"condition strings will be evaluated (like with skipif) and if one is "
|
||||
"False the marker will not be applied."
|
||||
)
|
||||
|
||||
def pytest_namespace():
|
||||
return dict(xfail=xfail)
|
||||
|
||||
@@ -169,21 +184,23 @@ def pytest_terminal_summary(terminalreporter):
|
||||
elif char == "X":
|
||||
show_xpassed(terminalreporter, lines)
|
||||
elif char in "fF":
|
||||
show_failed(terminalreporter, lines)
|
||||
show_simple(terminalreporter, lines, 'failed', "FAIL %s")
|
||||
elif char in "sS":
|
||||
show_skipped(terminalreporter, lines)
|
||||
elif char == "E":
|
||||
show_simple(terminalreporter, lines, 'error', "ERROR %s")
|
||||
if lines:
|
||||
tr._tw.sep("=", "short test summary info")
|
||||
for line in lines:
|
||||
tr._tw.line(line)
|
||||
|
||||
def show_failed(terminalreporter, lines):
|
||||
def show_simple(terminalreporter, lines, stat, format):
|
||||
tw = terminalreporter._tw
|
||||
failed = terminalreporter.stats.get("failed")
|
||||
failed = terminalreporter.stats.get(stat)
|
||||
if failed:
|
||||
for rep in failed:
|
||||
pos = rep.nodeid
|
||||
lines.append("FAIL %s" %(pos, ))
|
||||
lines.append(format %(pos, ))
|
||||
|
||||
def show_xfailed(terminalreporter, lines):
|
||||
xfailed = terminalreporter.stats.get("xfailed")
|
||||
|
||||
@@ -15,7 +15,7 @@ def pytest_addoption(parser):
|
||||
group._addoption('-r',
|
||||
action="store", dest="reportchars", default=None, metavar="chars",
|
||||
help="show extra test summary info as specified by chars (f)ailed, "
|
||||
"(s)skipped, (x)failed, (X)passed.")
|
||||
"(E)error, (s)skipped, (x)failed, (X)passed.")
|
||||
group._addoption('-l', '--showlocals',
|
||||
action="store_true", dest="showlocals", default=False,
|
||||
help="show locals in tracebacks (disabled by default).")
|
||||
@@ -43,7 +43,8 @@ def pytest_configure(config):
|
||||
pass
|
||||
else:
|
||||
stdout = os.fdopen(newfd, stdout.mode, 1)
|
||||
config._toclose = stdout
|
||||
config._cleanup.append(lambda: stdout.close())
|
||||
|
||||
reporter = TerminalReporter(config, stdout)
|
||||
config.pluginmanager.register(reporter, 'terminalreporter')
|
||||
if config.option.debug or config.option.traceconfig:
|
||||
@@ -52,11 +53,6 @@ def pytest_configure(config):
|
||||
reporter.write_line("[traceconfig] " + msg)
|
||||
config.trace.root.setprocessor("pytest:config", mywriter)
|
||||
|
||||
def pytest_unconfigure(config):
|
||||
if hasattr(config, '_toclose'):
|
||||
#print "closing", config._toclose, config._toclose.fileno()
|
||||
config._toclose.close()
|
||||
|
||||
def getreportopt(config):
|
||||
reportopts = ""
|
||||
optvalue = config.option.report
|
||||
@@ -259,7 +255,7 @@ class TerminalReporter:
|
||||
msg = "platform %s -- Python %s" % (sys.platform, verinfo)
|
||||
if hasattr(sys, 'pypy_version_info'):
|
||||
verinfo = ".".join(map(str, sys.pypy_version_info[:3]))
|
||||
msg += "[pypy-%s]" % verinfo
|
||||
msg += "[pypy-%s-%s]" % (verinfo, sys.pypy_version_info[3])
|
||||
msg += " -- pytest-%s" % (py.test.__version__)
|
||||
if self.verbosity > 0 or self.config.option.debug or \
|
||||
getattr(self.config.option, 'pastebin', None):
|
||||
@@ -318,12 +314,17 @@ class TerminalReporter:
|
||||
self.config.hook.pytest_terminal_summary(terminalreporter=self)
|
||||
if exitstatus == 2:
|
||||
self._report_keyboardinterrupt()
|
||||
del self._keyboardinterrupt_memo
|
||||
self.summary_deselected()
|
||||
self.summary_stats()
|
||||
|
||||
def pytest_keyboard_interrupt(self, excinfo):
|
||||
self._keyboardinterrupt_memo = excinfo.getrepr(funcargs=True)
|
||||
|
||||
def pytest_unconfigure(self):
|
||||
if hasattr(self, '_keyboardinterrupt_memo'):
|
||||
self._report_keyboardinterrupt()
|
||||
|
||||
def _report_keyboardinterrupt(self):
|
||||
excrepr = self._keyboardinterrupt_memo
|
||||
msg = excrepr.reprcrash.message
|
||||
@@ -388,7 +389,7 @@ class TerminalReporter:
|
||||
else:
|
||||
msg = self._getfailureheadline(rep)
|
||||
self.write_sep("_", msg)
|
||||
rep.toterminal(self._tw)
|
||||
self._outrep_summary(rep)
|
||||
|
||||
def summary_errors(self):
|
||||
if self.config.option.tbstyle != "no":
|
||||
@@ -406,7 +407,15 @@ class TerminalReporter:
|
||||
elif rep.when == "teardown":
|
||||
msg = "ERROR at teardown of " + msg
|
||||
self.write_sep("_", msg)
|
||||
rep.toterminal(self._tw)
|
||||
self._outrep_summary(rep)
|
||||
|
||||
def _outrep_summary(self, rep):
|
||||
rep.toterminal(self._tw)
|
||||
for secname, content in rep.sections:
|
||||
self._tw.sep("-", secname)
|
||||
if content[-1:] == "\n":
|
||||
content = content[:-1]
|
||||
self._tw.line(content)
|
||||
|
||||
def summary_stats(self):
|
||||
session_duration = py.std.time.time() - self._sessionstarttime
|
||||
@@ -417,9 +426,10 @@ class TerminalReporter:
|
||||
keys.append(key)
|
||||
parts = []
|
||||
for key in keys:
|
||||
val = self.stats.get(key, None)
|
||||
if val:
|
||||
parts.append("%d %s" %(len(val), key))
|
||||
if key: # setup/teardown reports have an empty key, ignore them
|
||||
val = self.stats.get(key, None)
|
||||
if val:
|
||||
parts.append("%d %s" %(len(val), key))
|
||||
line = ", ".join(parts)
|
||||
# XXX coloring
|
||||
msg = "%s in %.2f seconds" %(line, session_duration)
|
||||
@@ -430,8 +440,15 @@ class TerminalReporter:
|
||||
|
||||
def summary_deselected(self):
|
||||
if 'deselected' in self.stats:
|
||||
l = []
|
||||
k = self.config.option.keyword
|
||||
if k:
|
||||
l.append("-k%s" % k)
|
||||
m = self.config.option.markexpr
|
||||
if m:
|
||||
l.append("-m %r" % m)
|
||||
self.write_sep("=", "%d tests deselected by %r" %(
|
||||
len(self.stats['deselected']), self.config.option.keyword), bold=True)
|
||||
len(self.stats['deselected']), " ".join(l)), bold=True)
|
||||
|
||||
def repr_pythonversion(v=None):
|
||||
if v is None:
|
||||
|
||||
@@ -46,7 +46,7 @@ class TempdirHandler:
|
||||
|
||||
def finish(self):
|
||||
self.trace("finish")
|
||||
|
||||
|
||||
def pytest_configure(config):
|
||||
mp = monkeypatch()
|
||||
t = TempdirHandler(config)
|
||||
@@ -64,5 +64,5 @@ def pytest_funcarg__tmpdir(request):
|
||||
name = request._pyfuncitem.name
|
||||
name = py.std.re.sub("[\W]", "_", name)
|
||||
x = request.config._tmpdirhandler.mktemp(name, numbered=True)
|
||||
return x.realpath()
|
||||
return x
|
||||
|
||||
|
||||
@@ -120,14 +120,19 @@ def pytest_runtest_protocol(item, __multicall__):
|
||||
ut = sys.modules['twisted.python.failure']
|
||||
Failure__init__ = ut.Failure.__init__.im_func
|
||||
check_testcase_implements_trial_reporter()
|
||||
def excstore(self, exc_value=None, exc_type=None, exc_tb=None):
|
||||
def excstore(self, exc_value=None, exc_type=None, exc_tb=None,
|
||||
captureVars=None):
|
||||
if exc_value is None:
|
||||
self._rawexcinfo = sys.exc_info()
|
||||
else:
|
||||
if exc_type is None:
|
||||
exc_type = type(exc_value)
|
||||
self._rawexcinfo = (exc_type, exc_value, exc_tb)
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb)
|
||||
try:
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb,
|
||||
captureVars=captureVars)
|
||||
except TypeError:
|
||||
Failure__init__(self, exc_value, exc_type, exc_tb)
|
||||
ut.Failure.__init__ = excstore
|
||||
try:
|
||||
return __multicall__.execute()
|
||||
|
||||
@@ -46,7 +46,7 @@ except ImportError:
|
||||
args = [quote(arg) for arg in args]
|
||||
return os.spawnl(os.P_WAIT, sys.executable, *args) == 0
|
||||
|
||||
DEFAULT_VERSION = "0.6.14"
|
||||
DEFAULT_VERSION = "0.6.19"
|
||||
DEFAULT_URL = "http://pypi.python.org/packages/source/d/distribute/"
|
||||
SETUPTOOLS_FAKED_VERSION = "0.6c11"
|
||||
|
||||
|
||||
10
doc/Makefile
10
doc/Makefile
@@ -39,8 +39,14 @@ help:
|
||||
clean:
|
||||
-rm -rf $(BUILDDIR)/*
|
||||
|
||||
install: clean html
|
||||
rsync -avz _build/html/ code:www-pytest/
|
||||
install: html
|
||||
@rsync -avz _build/html/ pytest.org:/www/pytest.org/2.2.0
|
||||
|
||||
installpdf: latexpdf
|
||||
@scp $(BUILDDIR)/latex/pytest.pdf pytest.org:/www/pytest.org/latest
|
||||
|
||||
installall: clean install installpdf
|
||||
@echo "done"
|
||||
|
||||
html:
|
||||
$(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
|
||||
|
||||
2
doc/_static/sphinxdoc.css
vendored
2
doc/_static/sphinxdoc.css
vendored
@@ -48,7 +48,7 @@ div.body {
|
||||
}
|
||||
|
||||
div.related {
|
||||
font-size: 1em;
|
||||
font-size: 0.8em;
|
||||
}
|
||||
|
||||
div.related ul {
|
||||
|
||||
2
doc/_templates/indexsidebar.html
vendored
2
doc/_templates/indexsidebar.html
vendored
@@ -10,7 +10,7 @@
|
||||
<p><b><a href="{{ pathto('announce/index')}}">{{ release }} release</a></b>
|
||||
[<a href="{{ pathto('changelog') }}">Changelog</a>]</p>
|
||||
<p>
|
||||
<a href="http://pypi.python.org/pypi/pytest">pytest on PyPI</a>
|
||||
<a href="http://pypi.python.org/pypi/pytest">pytest/PyPI</a>
|
||||
</p>
|
||||
<pre>easy_install pytest</pre>
|
||||
<pre>pip install pytest</pre>
|
||||
|
||||
27
doc/_templates/layout.html
vendored
27
doc/_templates/layout.html
vendored
@@ -1,28 +1,8 @@
|
||||
{% extends "!layout.html" %}
|
||||
|
||||
{% block relbar1 %}
|
||||
{% endblock %}
|
||||
{% block relbar2 %}
|
||||
{% endblock %}
|
||||
|
||||
{% block rootrellink %}
|
||||
{% endblock %}
|
||||
{% block sidebarrel %}
|
||||
{% endblock %}
|
||||
|
||||
{% block header %}
|
||||
<div style="background-color: white; text-align: left; padding: 10px 10px 15px 15px">
|
||||
<h1>pytest: rapid no-boilerplate testing with Python</h1>
|
||||
<div style="text-align: left; font-size: 130%; vertical-align: middle;">
|
||||
<a href="{{ pathto('index') }}">home</a> |
|
||||
<a href="{{ pathto('contents') }}">all docs</a> |
|
||||
<a href="{{ pathto('getting-started') }}">install</a> |
|
||||
<a href="{{ pathto('example/index') }}">examples</a> |
|
||||
<a href="{{ pathto('customize') }}">customize</a> |
|
||||
<a href="https://bitbucket.org/hpk42/pytest/issues?status=new&status=open">issues</a>|
|
||||
<a href="{{ pathto('contact') }}">contact</a>
|
||||
</div>
|
||||
</div>
|
||||
{% block relbaritems %}
|
||||
{{ super() }}
|
||||
<g:plusone></g:plusone>
|
||||
{% endblock %}
|
||||
|
||||
{% block footer %}
|
||||
@@ -40,4 +20,5 @@
|
||||
})();
|
||||
|
||||
</script>
|
||||
<script type="text/javascript" src="https://apis.google.com/js/plusone.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
37
doc/_templates/localtoc.html
vendored
Normal file
37
doc/_templates/localtoc.html
vendored
Normal file
@@ -0,0 +1,37 @@
|
||||
|
||||
{%- if pagename != "search" %}
|
||||
<div id="searchbox" style="display: none">
|
||||
<form class="search" action="{{ pathto('search') }}" method="get">
|
||||
<input type="text" name="q" size="18" />
|
||||
<input type="submit" value="{{ _('Search') }}" />
|
||||
<input type="hidden" name="check_keywords" value="yes" />
|
||||
<input type="hidden" name="area" value="default" />
|
||||
</form>
|
||||
</div>
|
||||
<script type="text/javascript">$('#searchbox').show(0);</script>
|
||||
{%- endif %}
|
||||
|
||||
<h3>quicklinks</h3>
|
||||
<div style="text-align: left; font-size: 100%; vertical-align: middle;">
|
||||
<table>
|
||||
<tr>
|
||||
<td>
|
||||
<a href="{{ pathto('index') }}">home</a>
|
||||
</td><td>
|
||||
<a href="{{ pathto('contents') }}">TOC/contents</a>
|
||||
</td></tr><tr><td>
|
||||
<a href="{{ pathto('getting-started') }}">install</a>
|
||||
</td><td>
|
||||
<a href="{{ pathto('changelog') }}">changelog</a>
|
||||
</td></tr><tr><td>
|
||||
<a href="{{ pathto('example/index') }}">examples</a>
|
||||
</td><td>
|
||||
<a href="{{ pathto('customize') }}">customize</a>
|
||||
</td></tr><tr><td>
|
||||
<a href="https://bitbucket.org/hpk42/pytest/issues?status=new&status=open">issues[bb]</a>
|
||||
</td><td>
|
||||
<a href="{{ pathto('contact') }}">contact</a>
|
||||
</td></tr></table>
|
||||
</div>
|
||||
{% extends "basic/localtoc.html" %}
|
||||
|
||||
0
doc/_templates/searchbox.html
vendored
Normal file
0
doc/_templates/searchbox.html
vendored
Normal file
@@ -5,6 +5,11 @@ Release announcements
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
release-2.2.0
|
||||
release-2.1.3
|
||||
release-2.1.2
|
||||
release-2.1.1
|
||||
release-2.1.0
|
||||
release-2.0.3
|
||||
release-2.0.2
|
||||
release-2.0.1
|
||||
|
||||
48
doc/announce/release-2.1.0.txt
Normal file
48
doc/announce/release-2.1.0.txt
Normal file
@@ -0,0 +1,48 @@
|
||||
py.test 2.1.0: perfected assertions and bug fixes
|
||||
===========================================================================
|
||||
|
||||
Welcome to the relase of pytest-2.1, a mature testing tool for Python,
|
||||
supporting CPython 2.4-3.2, Jython and latest PyPy interpreters. See
|
||||
the improved extensive docs (now also as PDF!) with tested examples here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
The single biggest news about this release are **perfected assertions**
|
||||
courtesy of Benjamin Peterson. You can now safely use ``assert``
|
||||
statements in test modules without having to worry about side effects
|
||||
or python optimization ("-OO") options. This is achieved by rewriting
|
||||
assert statements in test modules upon import, using a PEP302 hook.
|
||||
See http://pytest.org/assert.html#advanced-assertion-introspection for
|
||||
detailed information. The work has been partly sponsored by my company,
|
||||
merlinux GmbH.
|
||||
|
||||
For further details on bug fixes and smaller enhancements see below.
|
||||
|
||||
If you want to install or upgrade pytest, just type one of::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
best,
|
||||
holger krekel / http://merlinux.eu
|
||||
|
||||
Changes between 2.0.3 and 2.1.0
|
||||
----------------------------------------------
|
||||
|
||||
- fix issue53 call nosestyle setup functions with correct ordering
|
||||
- fix issue58 and issue59: new assertion code fixes
|
||||
- merge Benjamin's assertionrewrite branch: now assertions
|
||||
for test modules on python 2.6 and above are done by rewriting
|
||||
the AST and saving the pyc file before the test module is imported.
|
||||
see doc/assert.txt for more info.
|
||||
- fix issue43: improve doctests with better traceback reporting on
|
||||
unexpected exceptions
|
||||
- fix issue47: timing output in junitxml for test cases is now correct
|
||||
- fix issue48: typo in MarkInfo repr leading to exception
|
||||
- fix issue49: avoid confusing error when initizaliation partially fails
|
||||
- fix issue44: env/username expansion for junitxml file path
|
||||
- show releaselevel information in test runs for pypy
|
||||
- reworked doc pages for better navigation and PDF generation
|
||||
- report KeyboardInterrupt even if interrupted during session startup
|
||||
- fix issue 35 - provide PDF doc version and download link from index page
|
||||
|
||||
37
doc/announce/release-2.1.1.txt
Normal file
37
doc/announce/release-2.1.1.txt
Normal file
@@ -0,0 +1,37 @@
|
||||
py.test 2.1.1: assertion fixes and improved junitxml output
|
||||
===========================================================================
|
||||
|
||||
pytest-2.1.1 is a backward compatible maintenance release of the
|
||||
popular py.test testing tool. See extensive docs with examples here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
Most bug fixes address remaining issues with the perfected assertions
|
||||
introduced with 2.1.0 - many thanks to the bug reporters and to Benjamin
|
||||
Peterson for helping to fix them. Also, junitxml output now produces
|
||||
system-out/err tags which lead to better displays of tracebacks with Jenkins.
|
||||
|
||||
Also a quick note to package maintainers and others interested: there now
|
||||
is a "pytest" man page which can be generated with "make man" in doc/.
|
||||
|
||||
If you want to install or upgrade pytest, just type one of::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
best,
|
||||
holger krekel / http://merlinux.eu
|
||||
|
||||
Changes between 2.1.0 and 2.1.1
|
||||
----------------------------------------------
|
||||
|
||||
- fix issue64 / pytest.set_trace now works within pytest_generate_tests hooks
|
||||
- fix issue60 / fix error conditions involving the creation of __pycache__
|
||||
- fix issue63 / assertion rewriting on inserts involving strings containing '%'
|
||||
- fix assertion rewriting on calls with a ** arg
|
||||
- don't cache rewritten modules if bytecode generation is disabled
|
||||
- fix assertion rewriting in read-only directories
|
||||
- fix issue59: provide system-out/err tags for junitxml output
|
||||
- fix issue61: assertion rewriting on boolean operations with 3 or more operands
|
||||
- you can now build a man page with "cd doc ; make man"
|
||||
|
||||
33
doc/announce/release-2.1.2.txt
Normal file
33
doc/announce/release-2.1.2.txt
Normal file
@@ -0,0 +1,33 @@
|
||||
py.test 2.1.2: bug fixes and fixes for jython
|
||||
===========================================================================
|
||||
|
||||
pytest-2.1.2 is a minor backward compatible maintenance release of the
|
||||
popular py.test testing tool. pytest is commonly used for unit,
|
||||
functional- and integration testing. See extensive docs with examples
|
||||
here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
Most bug fixes address remaining issues with the perfected assertions
|
||||
introduced in the 2.1 series - many thanks to the bug reporters and to Benjamin
|
||||
Peterson for helping to fix them. pytest should also work better with
|
||||
Jython-2.5.1 (and Jython trunk).
|
||||
|
||||
If you want to install or upgrade pytest, just type one of::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
best,
|
||||
holger krekel / http://merlinux.eu
|
||||
|
||||
Changes between 2.1.1 and 2.1.2
|
||||
----------------------------------------
|
||||
|
||||
- fix assertion rewriting on files with windows newlines on some Python versions
|
||||
- refine test discovery by package/module name (--pyargs), thanks Florian Mayer
|
||||
- fix issue69 / assertion rewriting fixed on some boolean operations
|
||||
- fix issue68 / packages now work with assertion rewriting
|
||||
- fix issue66: use different assertion rewriting caches when the -O option is passed
|
||||
- don't try assertion rewriting on Jython, use reinterp
|
||||
|
||||
32
doc/announce/release-2.1.3.txt
Normal file
32
doc/announce/release-2.1.3.txt
Normal file
@@ -0,0 +1,32 @@
|
||||
py.test 2.1.3: just some more fixes
|
||||
===========================================================================
|
||||
|
||||
pytest-2.1.3 is a minor backward compatible maintenance release of the
|
||||
popular py.test testing tool. It is commonly used for unit, functional-
|
||||
and integration testing. See extensive docs with examples here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
The release contains another fix to the perfected assertions introduced
|
||||
with the 2.1 series as well as the new possibility to customize reporting
|
||||
for assertion expressions on a per-directory level.
|
||||
|
||||
If you want to install or upgrade pytest, just type one of::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
Thanks to the bug reporters and to Ronny Pfannschmidt, Benjamin Peterson
|
||||
and Floris Bruynooghe who implemented the fixes.
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
Changes between 2.1.2 and 2.1.3
|
||||
----------------------------------------
|
||||
|
||||
- fix issue79: assertion rewriting failed on some comparisons in boolops,
|
||||
- correctly handle zero length arguments (a la pytest '')
|
||||
- fix issue67 / junitxml now contains correct test durations
|
||||
- fix issue75 / skipping test failure on jython
|
||||
- fix issue77 / Allow assertrepr_compare hook to apply to a subset of tests
|
||||
96
doc/announce/release-2.2.0.txt
Normal file
96
doc/announce/release-2.2.0.txt
Normal file
@@ -0,0 +1,96 @@
|
||||
py.test 2.2.0: test marking++, parametrization++ and duration profiling
|
||||
===========================================================================
|
||||
|
||||
pytest-2.2.0 is a test-suite compatible release of the popular
|
||||
py.test testing tool. Plugins might need upgrades. It comes
|
||||
with these improvements:
|
||||
|
||||
* easier and more powerful parametrization of tests:
|
||||
|
||||
- new @pytest.mark.parametrize decorator to run tests with different arguments
|
||||
- new metafunc.parametrize() API for parametrizing arguments independently
|
||||
- see examples at http://pytest.org/latest/example/parametrize.html
|
||||
- NOTE that parametrize() related APIs are still a bit experimental
|
||||
and might change in future releases.
|
||||
|
||||
* improved handling of test markers and refined marking mechanism:
|
||||
|
||||
- "-m markexpr" option for selecting tests according to their mark
|
||||
- a new "markers" ini-variable for registering test markers for your project
|
||||
- the new "--strict" bails out with an error if using unregistered markers.
|
||||
- see examples at http://pytest.org/latest/example/markers.html
|
||||
|
||||
* duration profiling: new "--duration=N" option showing the N slowest test
|
||||
execution or setup/teardown calls. This is most useful if you want to
|
||||
find out where your slowest test code is.
|
||||
|
||||
* also 2.2.0 performs more eager calling of teardown/finalizers functions
|
||||
resulting in better and more accurate reporting when they fail
|
||||
|
||||
Besides there is the usual set of bug fixes along with a cleanup of
|
||||
pytest's own test suite allowing it to run on a wider range of environments.
|
||||
|
||||
For general information, see extensive docs with examples here:
|
||||
|
||||
http://pytest.org/
|
||||
|
||||
If you want to install or upgrade pytest you might just type::
|
||||
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
Thanks to Ronny Pfannschmidt, David Burns, Jeff Donner, Daniel Nouri,
|
||||
Alfredo Doza and all who gave feedback or sent bug reports.
|
||||
|
||||
best,
|
||||
holger krekel
|
||||
|
||||
|
||||
notes on incompatibility
|
||||
------------------------------
|
||||
|
||||
While test suites should work unchanged you might need to upgrade plugins:
|
||||
|
||||
* You need a new version of the pytest-xdist plugin (1.7) for distributing
|
||||
test runs.
|
||||
|
||||
* Other plugins might need an upgrade if they implement
|
||||
the ``pytest_runtest_logreport`` hook which now is called unconditionally
|
||||
for the setup/teardown fixture phases of a test. You may choose to
|
||||
ignore setup/teardown failures by inserting "if rep.when != 'call': return"
|
||||
or something similar. Note that most code probably "just" works because
|
||||
the hook was already called for failing setup/teardown phases of a test
|
||||
so a plugin should have been ready to grok such reports already.
|
||||
|
||||
|
||||
Changes between 2.1.3 and 2.2.0
|
||||
----------------------------------------
|
||||
|
||||
- fix issue90: introduce eager tearing down of test items so that
|
||||
teardown function are called earlier.
|
||||
- add an all-powerful metafunc.parametrize function which allows to
|
||||
parametrize test function arguments in multiple steps and therefore
|
||||
from indepdenent plugins and palces.
|
||||
- add a @pytest.mark.parametrize helper which allows to easily
|
||||
call a test function with different argument values
|
||||
- Add examples to the "parametrize" example page, including a quick port
|
||||
of Test scenarios and the new parametrize function and decorator.
|
||||
- introduce registration for "pytest.mark.*" helpers via ini-files
|
||||
or through plugin hooks. Also introduce a "--strict" option which
|
||||
will treat unregistered markers as errors
|
||||
allowing to avoid typos and maintain a well described set of markers
|
||||
for your test suite. See exaples at http://pytest.org/latest/mark.html
|
||||
and its links.
|
||||
- issue50: introduce "-m marker" option to select tests based on markers
|
||||
(this is a stricter and more predictable version of '-k' in that "-m"
|
||||
only matches complete markers and has more obvious rules for and/or
|
||||
semantics.
|
||||
- new feature to help optimizing the speed of your tests:
|
||||
--durations=N option for displaying N slowest test calls
|
||||
and setup/teardown methods.
|
||||
- fix issue87: --pastebin now works with python3
|
||||
- fix issue89: --pdb with unexpected exceptions in doctest work more sensibly
|
||||
- fix and cleanup pytest's own test suite to not leak FDs
|
||||
- fix issue83: link to generated funcarg list
|
||||
- fix issue74: pyarg module names are now checked against imp.find_module false positives
|
||||
- fix compatibility with twisted/trial-11.1.0 use cases
|
||||
152
doc/assert.txt
152
doc/assert.txt
@@ -4,7 +4,7 @@ The writing and reporting of assertions in tests
|
||||
|
||||
.. _`assert with the assert statement`:
|
||||
|
||||
assert with the ``assert`` statement
|
||||
Asserting with the ``assert`` statement
|
||||
---------------------------------------------------------
|
||||
|
||||
``py.test`` allows you to use the standard python ``assert`` for verifying
|
||||
@@ -18,12 +18,12 @@ following::
|
||||
def test_function():
|
||||
assert f() == 4
|
||||
|
||||
to assert that your object returns a certain value. If this
|
||||
assertion fails you will see the value of ``x``::
|
||||
to assert that your function returns a certain value. If this assertion fails
|
||||
you will see the return value of the function call::
|
||||
|
||||
$ py.test test_assert1.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 1 items
|
||||
|
||||
test_assert1.py F
|
||||
@@ -39,25 +39,22 @@ assertion fails you will see the value of ``x``::
|
||||
test_assert1.py:5: AssertionError
|
||||
========================= 1 failed in 0.02 seconds =========================
|
||||
|
||||
Reporting details about the failing assertion is achieved by re-evaluating
|
||||
the assert expression and recording the intermediate values.
|
||||
py.test has support for showing the values of the most common subexpressions
|
||||
including calls, attributes, comparisons, and binary and unary
|
||||
operators. (See :ref:`tbreportdemo`). This allows you to use the
|
||||
idiomatic python constructs without boilerplate code while not losing
|
||||
introspection information.
|
||||
|
||||
Note: If evaluating the assert expression has side effects you may get a
|
||||
warning that the intermediate values could not be determined safely. A
|
||||
common example of this issue is an assertion which reads from a file::
|
||||
However, if you specify a message with the assertion like this::
|
||||
|
||||
assert f.read() != '...'
|
||||
assert a % 2 == 0, "value was odd, should be even"
|
||||
|
||||
If this assertion fails then the re-evaluation will probably succeed!
|
||||
This is because ``f.read()`` will return an empty string when it is
|
||||
called the second time during the re-evaluation. However, it is
|
||||
easy to rewrite the assertion and avoid any trouble::
|
||||
then no assertion introspection takes places at all and the message
|
||||
will be simply shown in the traceback.
|
||||
|
||||
content = f.read()
|
||||
assert content != '...'
|
||||
See :ref:`assert-details` for more information on assertion introspection.
|
||||
|
||||
|
||||
assertions about expected exceptions
|
||||
Assertions about expected exceptions
|
||||
------------------------------------------
|
||||
|
||||
In order to write assertions about raised exceptions, you can use
|
||||
@@ -108,7 +105,7 @@ if you run this module::
|
||||
|
||||
$ py.test test_assert2.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 1 items
|
||||
|
||||
test_assert2.py F
|
||||
@@ -127,7 +124,7 @@ if you run this module::
|
||||
E '5'
|
||||
|
||||
test_assert2.py:5: AssertionError
|
||||
========================= 1 failed in 0.03 seconds =========================
|
||||
========================= 1 failed in 0.02 seconds =========================
|
||||
|
||||
Special comparisons are done for a number of cases:
|
||||
|
||||
@@ -137,8 +134,117 @@ Special comparisons are done for a number of cases:
|
||||
|
||||
See the :ref:`reporting demo <tbreportdemo>` for many more examples.
|
||||
|
||||
..
|
||||
Defining your own comparison
|
||||
----------------------------------------------
|
||||
Defining your own assertion comparison
|
||||
----------------------------------------------
|
||||
|
||||
It is possible to add your own detailed explanations by implementing
|
||||
the ``pytest_assertrepr_compare`` hook.
|
||||
|
||||
.. autofunction:: _pytest.hookspec.pytest_assertrepr_compare
|
||||
|
||||
As an example consider adding the following hook in a conftest.py which
|
||||
provides an alternative explanation for ``Foo`` objects::
|
||||
|
||||
# content of conftest.py
|
||||
from test_foocompare import Foo
|
||||
def pytest_assertrepr_compare(op, left, right):
|
||||
if isinstance(left, Foo) and isinstance(right, Foo) and op == "==":
|
||||
return ['Comparing Foo instances:',
|
||||
' vals: %s != %s' % (left.val, right.val)]
|
||||
|
||||
now, given this test module::
|
||||
|
||||
# content of test_foocompare.py
|
||||
class Foo:
|
||||
def __init__(self, val):
|
||||
self.val = val
|
||||
|
||||
def test_compare():
|
||||
f1 = Foo(1)
|
||||
f2 = Foo(2)
|
||||
assert f1 == f2
|
||||
|
||||
you can run the test module and get the custom output defined in
|
||||
the conftest file::
|
||||
|
||||
$ py.test -q test_foocompare.py
|
||||
collecting ... collected 1 items
|
||||
F
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_compare _______________________________
|
||||
|
||||
def test_compare():
|
||||
f1 = Foo(1)
|
||||
f2 = Foo(2)
|
||||
> assert f1 == f2
|
||||
E assert Comparing Foo instances:
|
||||
E vals: 1 != 2
|
||||
|
||||
test_foocompare.py:8: AssertionError
|
||||
1 failed in 0.02 seconds
|
||||
|
||||
.. _assert-details:
|
||||
.. _`assert introspection`:
|
||||
|
||||
Advanced assertion introspection
|
||||
----------------------------------
|
||||
|
||||
.. versionadded:: 2.1
|
||||
|
||||
|
||||
Reporting details about a failing assertion is achieved either by rewriting
|
||||
assert statements before they are run or re-evaluating the assert expression and
|
||||
recording the intermediate values. Which technique is used depends on the
|
||||
location of the assert, py.test's configuration, and Python version being used
|
||||
to run py.test. Note that for assert statements with a manually provided
|
||||
message, i.e. ``assert expr, message``, no assertion introspection takes place
|
||||
and the manually provided message will be rendered in tracebacks.
|
||||
|
||||
By default, if the Python version is greater than or equal to 2.6, py.test
|
||||
rewrites assert statements in test modules. Rewritten assert statements put
|
||||
introspection information into the assertion failure message. py.test only
|
||||
rewrites test modules directly discovered by its test collection process, so
|
||||
asserts in supporting modules which are not themselves test modules will not be
|
||||
rewritten.
|
||||
|
||||
.. note::
|
||||
|
||||
py.test rewrites test modules on import. It does this by using an import hook
|
||||
to write a new pyc files. Most of the time this works transparently. However,
|
||||
if you are messing with import yourself, the import hook may interfere. If
|
||||
this is the case, simply use ``--assert=reinterp`` or
|
||||
``--assert=plain``. Additionally, rewriting will fail silently if it cannot
|
||||
write new pycs, i.e. in a read-only filesystem or a zipfile.
|
||||
|
||||
If an assert statement has not been rewritten or the Python version is less than
|
||||
2.6, py.test falls back on assert reinterpretation. In assert reinterpretation,
|
||||
py.test walks the frame of the function containing the assert statement to
|
||||
discover sub-expression results of the failing assert statement. You can force
|
||||
py.test to always use assertion reinterpretation by passing the
|
||||
``--assert=reinterp`` option.
|
||||
|
||||
Assert reinterpretation has a caveat not present with assert rewriting: If
|
||||
evaluating the assert expression has side effects you may get a warning that the
|
||||
intermediate values could not be determined safely. A common example of this
|
||||
issue is an assertion which reads from a file::
|
||||
|
||||
assert f.read() != '...'
|
||||
|
||||
If this assertion fails then the re-evaluation will probably succeed!
|
||||
This is because ``f.read()`` will return an empty string when it is
|
||||
called the second time during the re-evaluation. However, it is
|
||||
easy to rewrite the assertion and avoid any trouble::
|
||||
|
||||
content = f.read()
|
||||
assert content != '...'
|
||||
|
||||
All assert introspection can be turned off by passing ``--assert=plain``.
|
||||
|
||||
For further information, Benjamin Peterson wrote up `Behind the scenes of py.test's new assertion rewriting <http://pybites.blogspot.com/2011/07/behind-scenes-of-pytests-new-assertion.html>`_.
|
||||
|
||||
.. versionadded:: 2.1
|
||||
Add assert rewriting as an alternate introspection technique.
|
||||
|
||||
.. versionchanged:: 2.1
|
||||
Introduce the ``--assert`` option. Deprecate ``--no-assert`` and
|
||||
``--nomagic``.
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
.. _`pytest helpers`:
|
||||
|
||||
pytest builtin helpers
|
||||
Pytest builtin helpers
|
||||
================================================
|
||||
|
||||
builtin pytest.* functions and helping objects
|
||||
@@ -17,13 +17,19 @@ to get an overview on the globally available helpers.
|
||||
.. automodule:: pytest
|
||||
:members:
|
||||
|
||||
builtin function arguments
|
||||
|
||||
.. _builtinfuncargs:
|
||||
|
||||
Builtin function arguments
|
||||
-----------------------------------------------------
|
||||
|
||||
You can ask for available builtin or project-custom
|
||||
:ref:`function arguments <funcargs>` by typing::
|
||||
|
||||
$ py.test --funcargs
|
||||
=========================== test session starts ============================
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collected 0 items
|
||||
pytestconfig
|
||||
the pytest config object with access to command line opts.
|
||||
capsys
|
||||
@@ -69,3 +75,5 @@ You can ask for available builtin or project-custom
|
||||
See http://docs.python.org/library/warnings.html for information
|
||||
on warning categories.
|
||||
|
||||
|
||||
============================= in 0.00 seconds =============================
|
||||
|
||||
@@ -64,7 +64,7 @@ of the failing function and hide the other one::
|
||||
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_module.py .F
|
||||
@@ -78,7 +78,7 @@ of the failing function and hide the other one::
|
||||
|
||||
test_module.py:9: AssertionError
|
||||
----------------------------- Captured stdout ------------------------------
|
||||
setting up <function test_func2 at 0x238c410>
|
||||
setting up <function test_func2 at 0x101353a28>
|
||||
==================== 1 failed, 1 passed in 0.02 seconds ====================
|
||||
|
||||
Accessing captured output from a test function
|
||||
|
||||
40
doc/conf.py
40
doc/conf.py
@@ -38,7 +38,7 @@ source_suffix = '.txt'
|
||||
#source_encoding = 'utf-8-sig'
|
||||
|
||||
# The master toctree document.
|
||||
master_doc = 'index'
|
||||
master_doc = 'contents'
|
||||
|
||||
# General information about the project.
|
||||
project = u'pytest'
|
||||
@@ -48,13 +48,11 @@ copyright = u'2011, holger krekel et alii'
|
||||
# |version| and |release|, also used in various other places throughout the
|
||||
# built documents.
|
||||
#
|
||||
# The short X.Y version.
|
||||
version = '2.0'
|
||||
# The full version, including alpha/beta/rc tags.
|
||||
import py, pytest
|
||||
import pytest
|
||||
release = pytest.__version__
|
||||
# The short X.Y version.
|
||||
version = ".".join(release.split(".")[:2])
|
||||
#assert py.path.local().relto(py.path.local(pytest.__file__).dirpath().dirpath())
|
||||
|
||||
# The language for content autogenerated by Sphinx. Refer to documentation
|
||||
# for a list of supported languages.
|
||||
@@ -68,7 +66,10 @@ version = ".".join(release.split(".")[:2])
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
exclude_patterns = ['links.inc', '_build', 'test', ] # XXX
|
||||
exclude_patterns = ['links.inc', '_build', 'naming20.txt', 'test/*',
|
||||
'example/attic.txt',
|
||||
]
|
||||
|
||||
|
||||
# The reST default role (used for this markup: `text`) to use for all documents.
|
||||
#default_role = None
|
||||
@@ -107,10 +108,10 @@ html_theme_options = {}
|
||||
|
||||
# The name for this set of Sphinx documents. If None, it defaults to
|
||||
# "<project> v<release> documentation".
|
||||
#html_title = None
|
||||
html_title = None
|
||||
|
||||
# A shorter title for the navigation bar. Default is the same as html_title.
|
||||
#html_short_title = None
|
||||
html_short_title = "pytest-%s" % release
|
||||
|
||||
# The name of an image file (relative to this directory) to place at the top
|
||||
# of the sidebar.
|
||||
@@ -136,7 +137,7 @@ html_static_path = ['_static']
|
||||
|
||||
# Custom sidebar templates, maps document names to template names.
|
||||
#html_sidebars = {}
|
||||
html_sidebars = {'index': 'indexsidebar.html'}
|
||||
#html_sidebars = {'index': 'indexsidebar.html'}
|
||||
|
||||
# Additional templates that should be rendered to pages, maps page names to
|
||||
# template names.
|
||||
@@ -145,16 +146,16 @@ html_sidebars = {'index': 'indexsidebar.html'}
|
||||
|
||||
|
||||
# If false, no module index is generated.
|
||||
#html_domain_indices = True
|
||||
html_domain_indices = True
|
||||
|
||||
# If false, no index is generated.
|
||||
#html_use_index = True
|
||||
html_use_index = False
|
||||
|
||||
# If true, the index is split into individual pages for each letter.
|
||||
#html_split_index = False
|
||||
|
||||
# If true, links to the reST sources are added to the pages.
|
||||
#html_show_sourcelink = True
|
||||
html_show_sourcelink = False
|
||||
|
||||
# If true, "Created using Sphinx" is shown in the HTML footer. Default is True.
|
||||
#html_show_sphinx = True
|
||||
@@ -185,7 +186,7 @@ htmlhelp_basename = 'pytestdoc'
|
||||
# Grouping the document tree into LaTeX files. List of tuples
|
||||
# (source start file, target name, title, author, documentclass [howto/manual]).
|
||||
latex_documents = [
|
||||
('index', 'pytest.tex', u'pytest Documentation',
|
||||
('contents', 'pytest.tex', u'pytest Documentation',
|
||||
u'holger krekel et alii', 'manual'),
|
||||
]
|
||||
|
||||
@@ -210,16 +211,15 @@ latex_documents = [
|
||||
#latex_appendices = []
|
||||
|
||||
# If false, no module index is generated.
|
||||
#latex_domain_indices = True
|
||||
|
||||
latex_domain_indices = False
|
||||
|
||||
# -- Options for manual page output --------------------------------------------
|
||||
|
||||
# One entry per manual page. List of tuples
|
||||
# (source start file, name, description, authors, manual section).
|
||||
man_pages = [
|
||||
('index', 'pytest', u'pytest Documentation',
|
||||
[u'holger krekel et alii'], 1)
|
||||
('usage', 'pytest', u'pytest usage',
|
||||
[u'holger krekel at merlinux eu'], 1)
|
||||
]
|
||||
|
||||
|
||||
@@ -227,9 +227,9 @@ man_pages = [
|
||||
|
||||
# Bibliographic Dublin Core info.
|
||||
epub_title = u'pytest'
|
||||
epub_author = u'holger krekel et alii'
|
||||
epub_publisher = u'holger krekel et alii'
|
||||
epub_copyright = u'2010, holger krekel et alii'
|
||||
epub_author = u'holger krekel at merlinux eu'
|
||||
epub_publisher = u'holger krekel at merlinux eu'
|
||||
epub_copyright = u'2011, holger krekel et alii'
|
||||
|
||||
# The language of the text. It defaults to the language option
|
||||
# or en if the language is not set.
|
||||
|
||||
@@ -1,19 +1,20 @@
|
||||
|
||||
.. _toc:
|
||||
|
||||
Table of Contents
|
||||
Full pytest documenation
|
||||
========================
|
||||
|
||||
.. note::
|
||||
version 2.0 introduces :ref:`pytest as the main Python import name <naming20>`
|
||||
`Download latest version as PDF <http://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_
|
||||
|
||||
.. `Download latest version as EPUB <http://media.readthedocs.org/epub/pytest/latest/pytest.epub>`_
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
overview
|
||||
example/index
|
||||
apiref
|
||||
plugins
|
||||
example/index
|
||||
talks
|
||||
develop
|
||||
announce/index
|
||||
@@ -22,12 +23,4 @@ Table of Contents
|
||||
:hidden:
|
||||
|
||||
changelog.txt
|
||||
naming20.txt
|
||||
example/attic
|
||||
|
||||
Indices and tables
|
||||
==================
|
||||
|
||||
* :ref:`genindex`
|
||||
* :ref:`modindex`
|
||||
* :ref:`search`
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
basic test configuration
|
||||
Basic test configuration
|
||||
===================================
|
||||
|
||||
Command line options and configuration file settings
|
||||
@@ -59,7 +59,7 @@ progress output, you can write it into a configuration file::
|
||||
|
||||
From now on, running ``py.test`` will add the specified options.
|
||||
|
||||
builtin configuration file options
|
||||
Builtin configuration file options
|
||||
----------------------------------------------
|
||||
|
||||
.. confval:: minversion
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
doctest integration for modules and test files
|
||||
Doctest integration for modules and test files
|
||||
=========================================================
|
||||
|
||||
By default all files matching the ``test*.txt`` pattern will
|
||||
@@ -44,9 +44,9 @@ then you can just invoke ``py.test`` without command line options::
|
||||
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 1 items
|
||||
|
||||
mymodule.py .
|
||||
|
||||
========================= 1 passed in 0.40 seconds =========================
|
||||
========================= 1 passed in 0.05 seconds =========================
|
||||
|
||||
@@ -7,6 +7,10 @@ Usages and Examples
|
||||
Here is a (growing) list of examples. :ref:`Contact <contact>` us if you
|
||||
need more examples or have questions. Also take a look at the :ref:`comprehensive documentation <toc>` which contains many example snippets as well.
|
||||
|
||||
.. note::
|
||||
|
||||
see :doc:`../getting-started` for basic introductionary examples
|
||||
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
@@ -14,5 +18,6 @@ need more examples or have questions. Also take a look at the :ref:`comprehensiv
|
||||
simple.txt
|
||||
mysetup.txt
|
||||
parametrize.txt
|
||||
markers.txt
|
||||
pythoncollection.txt
|
||||
nonpython.txt
|
||||
|
||||
260
doc/example/markers.txt
Normal file
260
doc/example/markers.txt
Normal file
@@ -0,0 +1,260 @@
|
||||
|
||||
.. _`mark examples`:
|
||||
|
||||
Working with custom markers
|
||||
=================================================
|
||||
|
||||
Here are some example using the :ref:`mark` mechanism.
|
||||
|
||||
marking test functions and selecting them for a run
|
||||
----------------------------------------------------
|
||||
|
||||
You can "mark" a test function with custom metadata like this::
|
||||
|
||||
# content of test_server.py
|
||||
|
||||
import pytest
|
||||
@pytest.mark.webtest
|
||||
def test_send_http():
|
||||
pass # perform some webtest test for your app
|
||||
def test_something_quick():
|
||||
pass
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
You can then restrict a test run to only run tests marked with ``webtest``::
|
||||
|
||||
$ py.test -v -m webtest
|
||||
=========================== test session starts ============================
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0 -- /Users/hpk/venv/1/bin/python
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_server.py:3: test_send_http PASSED
|
||||
|
||||
=================== 1 tests deselected by "-m 'webtest'" ===================
|
||||
================== 1 passed, 1 deselected in 0.01 seconds ==================
|
||||
|
||||
Or the inverse, running all tests except the webtest ones::
|
||||
|
||||
$ py.test -v -m "not webtest"
|
||||
=========================== test session starts ============================
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0 -- /Users/hpk/venv/1/bin/python
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_server.py:6: test_something_quick PASSED
|
||||
|
||||
================= 1 tests deselected by "-m 'not webtest'" =================
|
||||
================== 1 passed, 1 deselected in 0.01 seconds ==================
|
||||
|
||||
Registering markers
|
||||
-------------------------------------
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
.. ini-syntax for custom markers:
|
||||
|
||||
Registering markers for your test suite is simple::
|
||||
|
||||
# content of pytest.ini
|
||||
[pytest]
|
||||
markers =
|
||||
webtest: mark a test as a webtest.
|
||||
|
||||
You can ask which markers exist for your test suite - the list includes our just defined ``webtest`` markers::
|
||||
|
||||
$ py.test --markers
|
||||
@pytest.mark.webtest: mark a test as a webtest.
|
||||
|
||||
@pytest.mark.skipif(*conditions): skip the given test function if evaluation of all conditions has a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform.
|
||||
|
||||
@pytest.mark.xfail(*conditions, reason=None, run=True): mark the the test function as an expected failure. Optionally specify a reason and run=False if you don't even want to execute the test function. Any positional condition strings will be evaluated (like with skipif) and if one is False the marker will not be applied.
|
||||
|
||||
@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in multiple different argument value sets. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.
|
||||
|
||||
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.
|
||||
|
||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
|
||||
|
||||
|
||||
For an example on how to add and work with markers from a plugin, see
|
||||
:ref:`adding a custom marker from a plugin`.
|
||||
|
||||
.. note::
|
||||
|
||||
It is recommended to explicitely register markers so that:
|
||||
|
||||
* there is one place in your test suite defining your markers
|
||||
|
||||
* asking for existing markers via ``py.test --markers`` gives good output
|
||||
|
||||
* typos in function markers are treated as an error if you use
|
||||
the ``--strict`` option. Later versions of py.test are probably
|
||||
going to treat non-registered markers as an error.
|
||||
|
||||
.. _`scoped-marking`:
|
||||
|
||||
Marking whole classes or modules
|
||||
----------------------------------------------------
|
||||
|
||||
If you are programming with Python2.6 you may use ``pytest.mark`` decorators
|
||||
with classes to apply markers to all of its test methods::
|
||||
|
||||
# content of test_mark_classlevel.py
|
||||
import pytest
|
||||
@pytest.mark.webtest
|
||||
class TestClass:
|
||||
def test_startup(self):
|
||||
pass
|
||||
def test_startup_and_more(self):
|
||||
pass
|
||||
|
||||
This is equivalent to directly applying the decorator to the
|
||||
two test functions.
|
||||
|
||||
To remain backward-compatible with Python2.4 you can also set a
|
||||
``pytestmark`` attribute on a TestClass like this::
|
||||
|
||||
import pytest
|
||||
|
||||
class TestClass:
|
||||
pytestmark = pytest.mark.webtest
|
||||
|
||||
or if you need to use multiple markers you can use a list::
|
||||
|
||||
import pytest
|
||||
|
||||
class TestClass:
|
||||
pytestmark = [pytest.mark.webtest, pytest.mark.slowtest]
|
||||
|
||||
You can also set a module level marker::
|
||||
|
||||
import pytest
|
||||
pytestmark = pytest.mark.webtest
|
||||
|
||||
in which case it will be applied to all functions and
|
||||
methods defined in the module.
|
||||
|
||||
Using ``-k TEXT`` to select tests
|
||||
----------------------------------------------------
|
||||
|
||||
You can use the ``-k`` command line option to only run tests with names that match the given argument::
|
||||
|
||||
$ py.test -k send_http # running with the above defined examples
|
||||
=========================== test session starts ============================
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_server.py .
|
||||
|
||||
=================== 3 tests deselected by '-ksend_http' ====================
|
||||
================== 1 passed, 3 deselected in 0.02 seconds ==================
|
||||
|
||||
And you can also run all tests except the ones that match the keyword::
|
||||
|
||||
$ py.test -k-send_http
|
||||
=========================== test session starts ============================
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_mark_classlevel.py ..
|
||||
test_server.py .
|
||||
|
||||
=================== 1 tests deselected by '-k-send_http' ===================
|
||||
================== 3 passed, 1 deselected in 0.03 seconds ==================
|
||||
|
||||
Or to only select the class::
|
||||
|
||||
$ py.test -kTestClass
|
||||
=========================== test session starts ============================
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_mark_classlevel.py ..
|
||||
|
||||
=================== 2 tests deselected by '-kTestClass' ====================
|
||||
================== 2 passed, 2 deselected in 0.02 seconds ==================
|
||||
|
||||
.. _`adding a custom marker from a plugin`:
|
||||
|
||||
custom marker and command line option to control test runs
|
||||
----------------------------------------------------------
|
||||
|
||||
Plugins can provide custom markers and implement specific behaviour
|
||||
based on it. This is a self-contained example which adds a command
|
||||
line option and a parametrized test function marker to run tests
|
||||
specifies via named environments::
|
||||
|
||||
# content of conftest.py
|
||||
|
||||
import pytest
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption("-E", dest="env", action="store", metavar="NAME",
|
||||
help="only run tests matching the environment NAME.")
|
||||
|
||||
def pytest_configure(config):
|
||||
# register an additional marker
|
||||
config.addinivalue_line("markers",
|
||||
"env(name): mark test to run only on named environment")
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
if not isinstance(item, item.Function):
|
||||
return
|
||||
if hasattr(item.obj, 'env'):
|
||||
envmarker = getattr(item.obj, 'env')
|
||||
envname = envmarker.args[0]
|
||||
if envname != item.config.option.env:
|
||||
pytest.skip("test requires env %r" % envname)
|
||||
|
||||
A test file using this local plugin::
|
||||
|
||||
# content of test_someenv.py
|
||||
|
||||
import pytest
|
||||
@pytest.mark.env("stage1")
|
||||
def test_basic_db_operation():
|
||||
pass
|
||||
|
||||
and an example invocations specifying a different environment than what
|
||||
the test needs::
|
||||
|
||||
$ py.test -E stage2
|
||||
=========================== test session starts ============================
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 5 items
|
||||
|
||||
test_mark_classlevel.py ..
|
||||
test_server.py ..
|
||||
test_someenv.py s
|
||||
|
||||
=================== 4 passed, 1 skipped in 0.04 seconds ====================
|
||||
|
||||
and here is one that specifies exactly the environment needed::
|
||||
|
||||
$ py.test -E stage1
|
||||
=========================== test session starts ============================
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 5 items
|
||||
|
||||
test_mark_classlevel.py ..
|
||||
test_server.py ..
|
||||
test_someenv.py .
|
||||
|
||||
========================= 5 passed in 0.04 seconds =========================
|
||||
|
||||
The ``--markers`` option always gives you a list of available markers::
|
||||
|
||||
$ py.test --markers
|
||||
@pytest.mark.webtest: mark a test as a webtest.
|
||||
|
||||
@pytest.mark.env(name): mark test to run only on named environment
|
||||
|
||||
@pytest.mark.skipif(*conditions): skip the given test function if evaluation of all conditions has a True value. Evaluation happens within the module global context. Example: skipif('sys.platform == "win32"') skips the test if we are on the win32 platform.
|
||||
|
||||
@pytest.mark.xfail(*conditions, reason=None, run=True): mark the the test function as an expected failure. Optionally specify a reason and run=False if you don't even want to execute the test function. Any positional condition strings will be evaluated (like with skipif) and if one is False the marker will not be applied.
|
||||
|
||||
@pytest.mark.parametrize(argnames, argvalues): call a test function multiple times passing in multiple different argument value sets. Example: @parametrize('arg1', [1,2]) would lead to two calls of the decorated test function, one with arg1=1 and another with arg1=2.
|
||||
|
||||
@pytest.mark.tryfirst: mark a hook implementation function such that the plugin machinery will try to call it first/as early as possible.
|
||||
|
||||
@pytest.mark.trylast: mark a hook implementation function such that the plugin machinery will try to call it last/as late as possible.
|
||||
|
||||
@@ -2,20 +2,20 @@
|
||||
module containing a parametrized tests testing cross-python
|
||||
serialization via the pickle module.
|
||||
"""
|
||||
import py
|
||||
import py, pytest
|
||||
|
||||
pythonlist = ['python2.4', 'python2.5', 'python2.6', 'python2.7', 'python2.8']
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
if 'python1' in metafunc.funcargnames:
|
||||
assert 'python2' in metafunc.funcargnames
|
||||
for obj in metafunc.function.multiarg.kwargs['obj']:
|
||||
for py1 in pythonlist:
|
||||
for py2 in pythonlist:
|
||||
metafunc.addcall(id="%s-%s-%s" % (py1, py2, obj),
|
||||
param=(py1, py2, obj))
|
||||
# we parametrize all "python1" and "python2" arguments to iterate
|
||||
# over the python interpreters of our list above - the actual
|
||||
# setup and lookup of interpreters in the python1/python2 factories
|
||||
# respectively.
|
||||
for arg in metafunc.funcargnames:
|
||||
if arg in ("python1", "python2"):
|
||||
metafunc.parametrize(arg, pythonlist, indirect=True)
|
||||
|
||||
@py.test.mark.multiarg(obj=[42, {}, {1:3},])
|
||||
@pytest.mark.parametrize("obj", [42, {}, {1:3},])
|
||||
def test_basic_objects(python1, python2, obj):
|
||||
python1.dumps(obj)
|
||||
python2.load_and_is_true("obj == %s" % obj)
|
||||
@@ -23,14 +23,11 @@ def test_basic_objects(python1, python2, obj):
|
||||
def pytest_funcarg__python1(request):
|
||||
tmpdir = request.getfuncargvalue("tmpdir")
|
||||
picklefile = tmpdir.join("data.pickle")
|
||||
return Python(request.param[0], picklefile)
|
||||
return Python(request.param, picklefile)
|
||||
|
||||
def pytest_funcarg__python2(request):
|
||||
python1 = request.getfuncargvalue("python1")
|
||||
return Python(request.param[1], python1.picklefile)
|
||||
|
||||
def pytest_funcarg__obj(request):
|
||||
return request.param[2]
|
||||
return Python(request.param, python1.picklefile)
|
||||
|
||||
class Python:
|
||||
def __init__(self, version, picklefile):
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
|
||||
.. _mysetup:
|
||||
|
||||
mysetup pattern: application specific test fixtures
|
||||
Mysetup pattern: application specific test fixtures
|
||||
==========================================================
|
||||
|
||||
Here is a basic useful step-by-step example for managing and interacting
|
||||
@@ -12,8 +12,8 @@ where we have the glue and test support code for bootstrapping and
|
||||
configuring application objects and allow test modules and test
|
||||
functions to stay ignorant of involved details.
|
||||
|
||||
step1: implementing the test/app-specific ``mysetup`` pattern
|
||||
-------------------------------------------------------------
|
||||
Step 1: Implementing the test/app-specific ``mysetup`` pattern
|
||||
--------------------------------------------------------------
|
||||
|
||||
Let's write a simple test function using a ``mysetup`` funcarg::
|
||||
|
||||
@@ -49,7 +49,7 @@ You can now run the test::
|
||||
|
||||
$ py.test test_sample.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 1 items
|
||||
|
||||
test_sample.py F
|
||||
@@ -57,7 +57,7 @@ You can now run the test::
|
||||
================================= FAILURES =================================
|
||||
_______________________________ test_answer ________________________________
|
||||
|
||||
mysetup = <conftest.MySetup instance at 0x2c1b128>
|
||||
mysetup = <conftest.MySetup instance at 0x1012b2bd8>
|
||||
|
||||
def test_answer(mysetup):
|
||||
app = mysetup.myapp()
|
||||
@@ -76,7 +76,7 @@ the concrete question or answers actually mean, please see here_.
|
||||
.. _here: http://uncyclopedia.wikia.com/wiki/The_Hitchhiker's_Guide_to_the_Galaxy
|
||||
.. _`tut-cmdlineoption`:
|
||||
|
||||
step 2: checking a command line option and skipping tests
|
||||
Step 2: Checking a command line option and skipping tests
|
||||
-----------------------------------------------------------
|
||||
|
||||
To add a command line option we update the ``conftest.py`` of
|
||||
@@ -122,14 +122,14 @@ Running it yields::
|
||||
|
||||
$ py.test test_ssh.py -rs
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 1 items
|
||||
|
||||
test_ssh.py s
|
||||
========================= short test summary info ==========================
|
||||
SKIP [1] /tmp/doc-exec-37/conftest.py:22: specify ssh host with --ssh
|
||||
SKIP [1] /Users/hpk/tmp/doc-exec-625/conftest.py:22: specify ssh host with --ssh
|
||||
|
||||
======================== 1 skipped in 0.01 seconds =========================
|
||||
======================== 1 skipped in 0.02 seconds =========================
|
||||
|
||||
If you specify a command line option like ``py.test --ssh=python.org`` the test will execute as expected.
|
||||
|
||||
|
||||
@@ -6,7 +6,7 @@ Working with non-python tests
|
||||
|
||||
.. _`yaml plugin`:
|
||||
|
||||
a basic example for specifying tests in Yaml files
|
||||
A basic example for specifying tests in Yaml files
|
||||
--------------------------------------------------------------
|
||||
|
||||
.. _`pytest-yamlwsgi`: http://bitbucket.org/aafshar/pytest-yamlwsgi/src/tip/pytest_yamlwsgi.py
|
||||
@@ -27,7 +27,7 @@ now execute the test specification::
|
||||
|
||||
nonpython $ py.test test_simple.yml
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_simple.yml .F
|
||||
@@ -37,7 +37,7 @@ now execute the test specification::
|
||||
usecase execution failed
|
||||
spec failed: 'some': 'other'
|
||||
no further details known at this point.
|
||||
==================== 1 failed, 1 passed in 0.24 seconds ====================
|
||||
==================== 1 failed, 1 passed in 0.10 seconds ====================
|
||||
|
||||
You get one dot for the passing ``sub1: sub1`` check and one failure.
|
||||
Obviously in the above ``conftest.py`` you'll want to implement a more
|
||||
@@ -56,7 +56,7 @@ reporting in ``verbose`` mode::
|
||||
|
||||
nonpython $ py.test -v
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3 -- /home/hpk/venv/0/bin/python
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0 -- /Users/hpk/venv/1/bin/python
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_simple.yml:1: usecase: ok PASSED
|
||||
@@ -67,17 +67,17 @@ reporting in ``verbose`` mode::
|
||||
usecase execution failed
|
||||
spec failed: 'some': 'other'
|
||||
no further details known at this point.
|
||||
==================== 1 failed, 1 passed in 0.07 seconds ====================
|
||||
==================== 1 failed, 1 passed in 0.09 seconds ====================
|
||||
|
||||
While developing your custom test collection and execution it's also
|
||||
interesting to just look at the collection tree::
|
||||
|
||||
nonpython $ py.test --collectonly
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 2 items
|
||||
<YamlFile 'test_simple.yml'>
|
||||
<YamlItem 'ok'>
|
||||
<YamlItem 'hello'>
|
||||
|
||||
============================= in 0.07 seconds =============================
|
||||
============================= in 0.08 seconds =============================
|
||||
|
||||
@@ -1,21 +1,74 @@
|
||||
|
||||
.. _paramexamples:
|
||||
|
||||
parametrizing tests
|
||||
Parametrizing tests
|
||||
=================================================
|
||||
|
||||
py.test allows to easily implement your own custom
|
||||
parametrization scheme for tests. Here we provide
|
||||
some examples for inspiration and re-use.
|
||||
.. currentmodule:: _pytest.python
|
||||
|
||||
generating parameters combinations, depending on command line
|
||||
py.test allows to easily parametrize test functions.
|
||||
In the following we provide some examples using
|
||||
the builtin mechanisms.
|
||||
|
||||
.. _parametrizemark:
|
||||
|
||||
simple "decorator" parametrization of a test function
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
.. versionadded:: 2.2
|
||||
|
||||
The builtin ``pytest.mark.parametrize`` decorator directly enables
|
||||
parametrization of arguments for a test function. Here is an example
|
||||
of a test function that wants to compare that processing some input
|
||||
results in expected output::
|
||||
|
||||
# content of test_expectation.py
|
||||
import pytest
|
||||
@pytest.mark.parametrize(("input", "expected"), [
|
||||
("3+5", 8),
|
||||
("2+4", 6),
|
||||
("6*9", 42),
|
||||
])
|
||||
def test_eval(input, expected):
|
||||
assert eval(input) == expected
|
||||
|
||||
we parametrize two arguments of the test function so that the test
|
||||
function is called three times. Let's run it::
|
||||
|
||||
$ py.test -q
|
||||
collecting ... collected 3 items
|
||||
..F
|
||||
================================= FAILURES =================================
|
||||
____________________________ test_eval[6*9-42] _____________________________
|
||||
|
||||
input = '6*9', expected = 42
|
||||
|
||||
@pytest.mark.parametrize(("input", "expected"), [
|
||||
("3+5", 8),
|
||||
("2+4", 6),
|
||||
("6*9", 42),
|
||||
])
|
||||
def test_eval(input, expected):
|
||||
> assert eval(input) == expected
|
||||
E assert 54 == 42
|
||||
E + where 54 = eval('6*9')
|
||||
|
||||
test_expectation.py:8: AssertionError
|
||||
1 failed, 2 passed in 0.03 seconds
|
||||
|
||||
As expected only one pair of input/output values fails the simple test function.
|
||||
|
||||
Note that there are various ways how you can mark groups of functions,
|
||||
see :ref:`mark`.
|
||||
|
||||
Generating parameters combinations, depending on command line
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
Let's say we want to execute a test with different parameters
|
||||
and the parameter range shall be determined by a command
|
||||
line argument. Let's first write a simple computation test::
|
||||
Let's say we want to execute a test with different computation
|
||||
parameters and the parameter range shall be determined by a command
|
||||
line argument. Let's first write a simple (do-nothing) computation test::
|
||||
|
||||
# content of test_compute.py
|
||||
|
||||
@@ -36,15 +89,14 @@ Now we add a test configuration like this::
|
||||
end = 5
|
||||
else:
|
||||
end = 2
|
||||
for i in range(end):
|
||||
metafunc.addcall(funcargs={'param1': i})
|
||||
metafunc.parametrize("param1", range(end))
|
||||
|
||||
This means that we only run 2 tests if we do not pass ``--all``::
|
||||
|
||||
$ py.test -q test_compute.py
|
||||
collecting ... collected 2 items
|
||||
..
|
||||
2 passed in 0.01 seconds
|
||||
2 passed in 0.02 seconds
|
||||
|
||||
We run only two computations, so we see two dots.
|
||||
let's run the full monty::
|
||||
@@ -67,15 +119,73 @@ let's run the full monty::
|
||||
As expected when running the full range of ``param1`` values
|
||||
we'll get an error on the last one.
|
||||
|
||||
Deferring the setup of parametrizing resources
|
||||
a quick port of "testscenarios"
|
||||
------------------------------------
|
||||
|
||||
.. _`test scenarios`: http://bazaar.launchpad.net/~lifeless/testscenarios/trunk/annotate/head%3A/doc/example.py
|
||||
|
||||
Here is a quick port of to run tests configured with `test scenarios`_,
|
||||
an add-on from Robert Collins for the standard unittest framework. We
|
||||
only have to work a bit to construct the correct arguments for pytest's
|
||||
:py:func:`Metafunc.parametrize`::
|
||||
|
||||
# content of test_scenarios.py
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
idlist = []
|
||||
argvalues = []
|
||||
for scenario in metafunc.cls.scenarios:
|
||||
idlist.append(scenario[0])
|
||||
items = scenario[1].items()
|
||||
argnames = [x[0] for x in items]
|
||||
argvalues.append(([x[1] for x in items]))
|
||||
metafunc.parametrize(argnames, argvalues, ids=idlist)
|
||||
|
||||
scenario1 = ('basic', {'attribute': 'value'})
|
||||
scenario2 = ('advanced', {'attribute': 'value2'})
|
||||
|
||||
class TestSampleWithScenarios:
|
||||
scenarios = [scenario1, scenario2]
|
||||
|
||||
def test_demo(self, attribute):
|
||||
assert isinstance(attribute, str)
|
||||
|
||||
this is a fully self-contained example which you can run with::
|
||||
|
||||
$ py.test test_scenarios.py
|
||||
=========================== test session starts ============================
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_scenarios.py ..
|
||||
|
||||
========================= 2 passed in 0.02 seconds =========================
|
||||
|
||||
If you just collect tests you'll also nicely see 'advanced' and 'basic' as variants for the test function::
|
||||
|
||||
|
||||
$ py.test --collectonly test_scenarios.py
|
||||
=========================== test session starts ============================
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 2 items
|
||||
<Module 'test_scenarios.py'>
|
||||
<Class 'TestSampleWithScenarios'>
|
||||
<Instance '()'>
|
||||
<Function 'test_demo[basic]'>
|
||||
<Function 'test_demo[advanced]'>
|
||||
|
||||
============================= in 0.01 seconds =============================
|
||||
|
||||
Deferring the setup of parametrized resources
|
||||
---------------------------------------------------
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
The parametrization of test functions happens at collection
|
||||
time. It is often a good idea to setup possibly expensive
|
||||
resources only when the actual test is run. Here is a simple
|
||||
example how you can achieve that::
|
||||
time. It is a good idea to setup expensive resources like DB
|
||||
connections or subprocess only when the actual test is run.
|
||||
Here is a simple example how you can achieve that, first
|
||||
the actual test requiring a ``db`` object::
|
||||
|
||||
# content of test_backends.py
|
||||
|
||||
@@ -85,17 +195,15 @@ example how you can achieve that::
|
||||
if db.__class__.__name__ == "DB2":
|
||||
pytest.fail("deliberately failing for demo purposes")
|
||||
|
||||
Now we add a test configuration that takes care to generate
|
||||
two invocations of the ``test_db_initialized`` function and
|
||||
furthermore a factory that creates a database object when
|
||||
each test is actually run::
|
||||
We can now add a test configuration that generates two invocations of
|
||||
the ``test_db_initialized`` function and also implements a factory that
|
||||
creates a database object for the actual test invocations::
|
||||
|
||||
# content of conftest.py
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
if 'db' in metafunc.funcargnames:
|
||||
metafunc.addcall(param="d1")
|
||||
metafunc.addcall(param="d2")
|
||||
metafunc.parametrize("db", ['d1', 'd2'], indirect=True)
|
||||
|
||||
class DB1:
|
||||
"one database object"
|
||||
@@ -114,11 +222,11 @@ Let's first see how it looks like at collection time::
|
||||
|
||||
$ py.test test_backends.py --collectonly
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 2 items
|
||||
<Module 'test_backends.py'>
|
||||
<Function 'test_db_initialized[0]'>
|
||||
<Function 'test_db_initialized[1]'>
|
||||
<Function 'test_db_initialized[d1]'>
|
||||
<Function 'test_db_initialized[d2]'>
|
||||
|
||||
============================= in 0.01 seconds =============================
|
||||
|
||||
@@ -128,9 +236,9 @@ And then when we run the test::
|
||||
collecting ... collected 2 items
|
||||
.F
|
||||
================================= FAILURES =================================
|
||||
__________________________ test_db_initialized[1] __________________________
|
||||
_________________________ test_db_initialized[d2] __________________________
|
||||
|
||||
db = <conftest.DB2 instance at 0x2bf7bd8>
|
||||
db = <conftest.DB2 instance at 0x10150ab90>
|
||||
|
||||
def test_db_initialized(db):
|
||||
# a dummy test
|
||||
@@ -139,34 +247,37 @@ And then when we run the test::
|
||||
E Failed: deliberately failing for demo purposes
|
||||
|
||||
test_backends.py:6: Failed
|
||||
1 failed, 1 passed in 0.03 seconds
|
||||
1 failed, 1 passed in 0.02 seconds
|
||||
|
||||
Now you see that one invocation of the test passes and another fails,
|
||||
as it to be expected.
|
||||
The first invocation with ``db == "DB1"`` passed while the second with ``db == "DB2"`` failed. Our ``pytest_funcarg__db`` factory has instantiated each of the DB values during the setup phase while the ``pytest_generate_tests`` generated two according calls to the ``test_db_initialized`` during the collection phase.
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
Parametrizing test methods through per-class configuration
|
||||
--------------------------------------------------------------
|
||||
|
||||
.. _`unittest parameterizer`: http://code.google.com/p/unittest-ext/source/browse/trunk/params.py
|
||||
|
||||
|
||||
Here is an example ``pytest_generate_function`` function implementing a
|
||||
parametrization scheme similar to Michael Foords `unittest
|
||||
parameterizer`_ in a lot less code::
|
||||
parameterizer`_ but in a lot less code::
|
||||
|
||||
# content of ./test_parametrize.py
|
||||
import pytest
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
# called once per each test function
|
||||
for funcargs in metafunc.cls.params[metafunc.function.__name__]:
|
||||
# schedule a new test function run with applied **funcargs
|
||||
metafunc.addcall(funcargs=funcargs)
|
||||
funcarglist = metafunc.cls.params[metafunc.function.__name__]
|
||||
argnames = list(funcarglist[0])
|
||||
metafunc.parametrize(argnames, [[funcargs[name] for name in argnames]
|
||||
for funcargs in funcarglist])
|
||||
|
||||
class TestClass:
|
||||
# a map specifying multiple argument sets for a test method
|
||||
params = {
|
||||
'test_equals': [dict(a=1, b=2), dict(a=3, b=3), ],
|
||||
'test_zerodivision': [dict(a=1, b=0), dict(a=3, b=2)],
|
||||
'test_zerodivision': [dict(a=1, b=0), ],
|
||||
}
|
||||
|
||||
def test_equals(self, a, b):
|
||||
@@ -175,120 +286,44 @@ parameterizer`_ in a lot less code::
|
||||
def test_zerodivision(self, a, b):
|
||||
pytest.raises(ZeroDivisionError, "a/b")
|
||||
|
||||
Running it means we are two tests for each test functions, using
|
||||
the respective settings::
|
||||
Our test generator looks up a class-level definition which specifies which
|
||||
argument sets to use for each test function. Let's run it::
|
||||
|
||||
$ py.test -q
|
||||
collecting ... collected 6 items
|
||||
.FF..F
|
||||
collecting ... collected 3 items
|
||||
F..
|
||||
================================= FAILURES =================================
|
||||
__________________________ test_db_initialized[1] __________________________
|
||||
________________________ TestClass.test_equals[1-2] ________________________
|
||||
|
||||
db = <conftest.DB2 instance at 0x19bcb90>
|
||||
|
||||
def test_db_initialized(db):
|
||||
# a dummy test
|
||||
if db.__class__.__name__ == "DB2":
|
||||
> pytest.fail("deliberately failing for demo purposes")
|
||||
E Failed: deliberately failing for demo purposes
|
||||
|
||||
test_backends.py:6: Failed
|
||||
_________________________ TestClass.test_equals[0] _________________________
|
||||
|
||||
self = <test_parametrize.TestClass instance at 0x19ca8c0>, a = 1, b = 2
|
||||
self = <test_parametrize.TestClass instance at 0x101509638>, a = 1, b = 2
|
||||
|
||||
def test_equals(self, a, b):
|
||||
> assert a == b
|
||||
E assert 1 == 2
|
||||
|
||||
test_parametrize.py:17: AssertionError
|
||||
______________________ TestClass.test_zerodivision[1] ______________________
|
||||
|
||||
self = <test_parametrize.TestClass instance at 0x19cd4d0>, a = 3, b = 2
|
||||
|
||||
def test_zerodivision(self, a, b):
|
||||
> pytest.raises(ZeroDivisionError, "a/b")
|
||||
E Failed: DID NOT RAISE
|
||||
|
||||
test_parametrize.py:20: Failed
|
||||
3 failed, 3 passed in 0.05 seconds
|
||||
test_parametrize.py:18: AssertionError
|
||||
1 failed, 2 passed in 0.03 seconds
|
||||
|
||||
Parametrizing test methods through a decorator
|
||||
--------------------------------------------------------------
|
||||
|
||||
Modifying the previous example we can also allow decorators
|
||||
for parametrizing test methods::
|
||||
|
||||
# content of test_parametrize2.py
|
||||
|
||||
import pytest
|
||||
|
||||
# test support code
|
||||
def params(funcarglist):
|
||||
def wrapper(function):
|
||||
function.funcarglist = funcarglist
|
||||
return function
|
||||
return wrapper
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
for funcargs in getattr(metafunc.function, 'funcarglist', ()):
|
||||
metafunc.addcall(funcargs=funcargs)
|
||||
|
||||
# actual test code
|
||||
class TestClass:
|
||||
@params([dict(a=1, b=2), dict(a=3, b=3), ])
|
||||
def test_equals(self, a, b):
|
||||
assert a == b
|
||||
|
||||
@params([dict(a=1, b=0), dict(a=3, b=2)])
|
||||
def test_zerodivision(self, a, b):
|
||||
pytest.raises(ZeroDivisionError, "a/b")
|
||||
|
||||
Running it gives similar results as before::
|
||||
|
||||
$ py.test -q test_parametrize2.py
|
||||
collecting ... collected 4 items
|
||||
F..F
|
||||
================================= FAILURES =================================
|
||||
_________________________ TestClass.test_equals[0] _________________________
|
||||
|
||||
self = <test_parametrize2.TestClass instance at 0x1cf1170>, a = 1, b = 2
|
||||
|
||||
@params([dict(a=1, b=2), dict(a=3, b=3), ])
|
||||
def test_equals(self, a, b):
|
||||
> assert a == b
|
||||
E assert 1 == 2
|
||||
|
||||
test_parametrize2.py:19: AssertionError
|
||||
______________________ TestClass.test_zerodivision[1] ______________________
|
||||
|
||||
self = <test_parametrize2.TestClass instance at 0x1d02170>, a = 3, b = 2
|
||||
|
||||
@params([dict(a=1, b=0), dict(a=3, b=2)])
|
||||
def test_zerodivision(self, a, b):
|
||||
> pytest.raises(ZeroDivisionError, "a/b")
|
||||
E Failed: DID NOT RAISE
|
||||
|
||||
test_parametrize2.py:23: Failed
|
||||
2 failed, 2 passed in 0.03 seconds
|
||||
|
||||
checking serialization between Python interpreters
|
||||
Indirect parametrization with multiple resources
|
||||
--------------------------------------------------------------
|
||||
|
||||
Here is a stripped down real-life example of using parametrized
|
||||
testing for testing serialization between different interpreters.
|
||||
testing for testing serialization, invoking different python interpreters.
|
||||
We define a ``test_basic_objects`` function which is to be run
|
||||
with different sets of arguments for its three arguments::
|
||||
with different sets of arguments for its three arguments:
|
||||
|
||||
* ``python1``: first python interpreter
|
||||
* ``python2``: second python interpreter
|
||||
* ``obj``: object to be dumped from first interpreter and loaded into second interpreter
|
||||
* ``python1``: first python interpreter, run to pickle-dump an object to a file
|
||||
* ``python2``: second interpreter, run to pickle-load an object from a file
|
||||
* ``obj``: object to be dumped/loaded
|
||||
|
||||
.. literalinclude:: multipython.py
|
||||
|
||||
Running it (with Python-2.4 through to Python2.7 installed)::
|
||||
Running it results in some skips if we don't have all the python interpreters installed and otherwise runs all combinations (5 interpreters times 5 interpreters times 3 objects to serialize/deserialize)::
|
||||
|
||||
. $ py.test -q multipython.py
|
||||
. $ py.test -rs -q multipython.py
|
||||
collecting ... collected 75 items
|
||||
....s....s....s....ssssss....s....s....s....ssssss....s....s....s....ssssss
|
||||
48 passed, 27 skipped in 2.04 seconds
|
||||
ssssssssssssssssss.........ssssss.........ssssss.........ssssssssssssssssss
|
||||
========================= short test summary info ==========================
|
||||
SKIP [24] /Users/hpk/p/pytest/doc/example/multipython.py:36: 'python2.8' not found
|
||||
SKIP [24] /Users/hpk/p/pytest/doc/example/multipython.py:36: 'python2.4' not found
|
||||
27 passed, 48 skipped in 3.03 seconds
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
Changing standard (Python) test discovery
|
||||
===============================================
|
||||
|
||||
changing directory recursion
|
||||
Changing directory recursion
|
||||
-----------------------------------------------------
|
||||
|
||||
You can set the :confval:`norecursedirs` option in an ini-file, for example your ``setup.cfg`` in the project root directory::
|
||||
@@ -14,7 +14,7 @@ This would tell py.test to not recurse into typical subversion or sphinx-build d
|
||||
|
||||
.. _`change naming conventions`:
|
||||
|
||||
change naming conventions
|
||||
Changing naming conventions
|
||||
-----------------------------------------------------
|
||||
|
||||
You can configure different naming conventions by setting
|
||||
@@ -43,7 +43,7 @@ then the test collection looks like this::
|
||||
|
||||
$ py.test --collectonly
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 2 items
|
||||
<Module 'check_myapp.py'>
|
||||
<Class 'CheckMyApp'>
|
||||
@@ -51,9 +51,9 @@ then the test collection looks like this::
|
||||
<Function 'check_simple'>
|
||||
<Function 'check_complex'>
|
||||
|
||||
============================= in 0.01 seconds =============================
|
||||
============================= in 0.02 seconds =============================
|
||||
|
||||
interpret cmdline arguments as Python packages
|
||||
Interpreting cmdline arguments as Python packages
|
||||
-----------------------------------------------------
|
||||
|
||||
You can use the ``--pyargs`` option to make py.test try
|
||||
@@ -75,14 +75,14 @@ Now a simple invocation of ``py.test NAME`` will check
|
||||
if NAME exists as an importable package/module and otherwise
|
||||
treat it as a filesystem path.
|
||||
|
||||
finding out what is collected
|
||||
Finding out what is collected
|
||||
-----------------------------------------------
|
||||
|
||||
You can always peek at the collection tree without running tests like this::
|
||||
|
||||
. $ py.test --collectonly pythoncollection.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 3 items
|
||||
<Module 'pythoncollection.py'>
|
||||
<Function 'test_function'>
|
||||
|
||||
@@ -13,7 +13,7 @@ get on the terminal - we are working on that):
|
||||
|
||||
assertion $ py.test failure_demo.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 39 items
|
||||
|
||||
failure_demo.py FFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFFF
|
||||
@@ -30,7 +30,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:15: AssertionError
|
||||
_________________________ TestFailing.test_simple __________________________
|
||||
|
||||
self = <failure_demo.TestFailing object at 0x14b9890>
|
||||
self = <failure_demo.TestFailing object at 0x1013552d0>
|
||||
|
||||
def test_simple(self):
|
||||
def f():
|
||||
@@ -40,13 +40,13 @@ get on the terminal - we are working on that):
|
||||
|
||||
> assert f() == g()
|
||||
E assert 42 == 43
|
||||
E + where 42 = <function f at 0x14a5e60>()
|
||||
E + and 43 = <function g at 0x14bc1b8>()
|
||||
E + where 42 = <function f at 0x101514f50>()
|
||||
E + and 43 = <function g at 0x101516050>()
|
||||
|
||||
failure_demo.py:28: AssertionError
|
||||
____________________ TestFailing.test_simple_multiline _____________________
|
||||
|
||||
self = <failure_demo.TestFailing object at 0x14b9b50>
|
||||
self = <failure_demo.TestFailing object at 0x101355950>
|
||||
|
||||
def test_simple_multiline(self):
|
||||
otherfunc_multi(
|
||||
@@ -59,26 +59,26 @@ get on the terminal - we are working on that):
|
||||
a = 42, b = 54
|
||||
|
||||
def otherfunc_multi(a,b):
|
||||
assert (a ==
|
||||
> b)
|
||||
> assert (a ==
|
||||
b)
|
||||
E assert 42 == 54
|
||||
|
||||
failure_demo.py:12: AssertionError
|
||||
failure_demo.py:11: AssertionError
|
||||
___________________________ TestFailing.test_not ___________________________
|
||||
|
||||
self = <failure_demo.TestFailing object at 0x14b9790>
|
||||
self = <failure_demo.TestFailing object at 0x101355ad0>
|
||||
|
||||
def test_not(self):
|
||||
def f():
|
||||
return 42
|
||||
> assert not f()
|
||||
E assert not 42
|
||||
E + where 42 = <function f at 0x14bc398>()
|
||||
E + where 42 = <function f at 0x101514f50>()
|
||||
|
||||
failure_demo.py:38: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_text _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x14aa810>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x1013559d0>
|
||||
|
||||
def test_eq_text(self):
|
||||
> assert 'spam' == 'eggs'
|
||||
@@ -89,7 +89,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:42: AssertionError
|
||||
_____________ TestSpecialisedExplanations.test_eq_similar_text _____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x1576190>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x101350dd0>
|
||||
|
||||
def test_eq_similar_text(self):
|
||||
> assert 'foo 1 bar' == 'foo 2 bar'
|
||||
@@ -102,7 +102,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:45: AssertionError
|
||||
____________ TestSpecialisedExplanations.test_eq_multiline_text ____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x14a7450>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x101350d10>
|
||||
|
||||
def test_eq_multiline_text(self):
|
||||
> assert 'foo\nspam\nbar' == 'foo\neggs\nbar'
|
||||
@@ -115,7 +115,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:48: AssertionError
|
||||
______________ TestSpecialisedExplanations.test_eq_long_text _______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x14b9350>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x101350cd0>
|
||||
|
||||
def test_eq_long_text(self):
|
||||
a = '1'*100 + 'a' + '2'*100
|
||||
@@ -132,7 +132,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:53: AssertionError
|
||||
_________ TestSpecialisedExplanations.test_eq_long_text_multiline __________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x15764d0>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x101350f50>
|
||||
|
||||
def test_eq_long_text_multiline(self):
|
||||
a = '1\n'*100 + 'a' + '2\n'*100
|
||||
@@ -156,7 +156,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:58: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_list _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x1576350>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x10134f350>
|
||||
|
||||
def test_eq_list(self):
|
||||
> assert [0, 1, 2] == [0, 1, 3]
|
||||
@@ -166,7 +166,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:61: AssertionError
|
||||
______________ TestSpecialisedExplanations.test_eq_list_long _______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x1576f10>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x10134fc10>
|
||||
|
||||
def test_eq_list_long(self):
|
||||
a = [0]*100 + [1] + [3]*100
|
||||
@@ -178,7 +178,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:66: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_dict _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x1576390>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x10134f2d0>
|
||||
|
||||
def test_eq_dict(self):
|
||||
> assert {'a': 0, 'b': 1} == {'a': 0, 'b': 2}
|
||||
@@ -191,7 +191,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:69: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_eq_set __________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x14bd790>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x10134f110>
|
||||
|
||||
def test_eq_set(self):
|
||||
> assert set([0, 10, 11, 12]) == set([0, 20, 21])
|
||||
@@ -207,7 +207,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:72: AssertionError
|
||||
_____________ TestSpecialisedExplanations.test_eq_longer_list ______________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x157a7d0>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x10134f510>
|
||||
|
||||
def test_eq_longer_list(self):
|
||||
> assert [1,2] == [1,2,3]
|
||||
@@ -217,7 +217,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:75: AssertionError
|
||||
_________________ TestSpecialisedExplanations.test_in_list _________________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x157ab50>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x10134f6d0>
|
||||
|
||||
def test_in_list(self):
|
||||
> assert 1 in [0, 2, 3, 4, 5]
|
||||
@@ -226,7 +226,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:78: AssertionError
|
||||
__________ TestSpecialisedExplanations.test_not_in_text_multiline __________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x157a090>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x10152c490>
|
||||
|
||||
def test_not_in_text_multiline(self):
|
||||
text = 'some multiline\ntext\nwhich\nincludes foo\nand a\ntail'
|
||||
@@ -244,7 +244,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:82: AssertionError
|
||||
___________ TestSpecialisedExplanations.test_not_in_text_single ____________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x14aaa50>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x10152cfd0>
|
||||
|
||||
def test_not_in_text_single(self):
|
||||
text = 'single foo line'
|
||||
@@ -257,7 +257,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:86: AssertionError
|
||||
_________ TestSpecialisedExplanations.test_not_in_text_single_long _________
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x157ab90>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x10152c090>
|
||||
|
||||
def test_not_in_text_single_long(self):
|
||||
text = 'head ' * 50 + 'foo ' + 'tail ' * 20
|
||||
@@ -270,7 +270,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:90: AssertionError
|
||||
______ TestSpecialisedExplanations.test_not_in_text_single_long_term _______
|
||||
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x1576ed0>
|
||||
self = <failure_demo.TestSpecialisedExplanations object at 0x10152cb90>
|
||||
|
||||
def test_not_in_text_single_long_term(self):
|
||||
text = 'head ' * 50 + 'f'*70 + 'tail ' * 20
|
||||
@@ -289,7 +289,7 @@ get on the terminal - we are working on that):
|
||||
i = Foo()
|
||||
> assert i.b == 2
|
||||
E assert 1 == 2
|
||||
E + where 1 = <failure_demo.Foo object at 0x157a910>.b
|
||||
E + where 1 = <failure_demo.Foo object at 0x10152c350>.b
|
||||
|
||||
failure_demo.py:101: AssertionError
|
||||
_________________________ test_attribute_instance __________________________
|
||||
@@ -299,8 +299,8 @@ get on the terminal - we are working on that):
|
||||
b = 1
|
||||
> assert Foo().b == 2
|
||||
E assert 1 == 2
|
||||
E + where 1 = <failure_demo.Foo object at 0x1584610>.b
|
||||
E + where <failure_demo.Foo object at 0x1584610> = <class 'failure_demo.Foo'>()
|
||||
E + where 1 = <failure_demo.Foo object at 0x10134fe90>.b
|
||||
E + where <failure_demo.Foo object at 0x10134fe90> = <class 'failure_demo.Foo'>()
|
||||
|
||||
failure_demo.py:107: AssertionError
|
||||
__________________________ test_attribute_failure __________________________
|
||||
@@ -316,7 +316,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:116:
|
||||
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
|
||||
|
||||
self = <failure_demo.Foo object at 0x157a3d0>
|
||||
self = <failure_demo.Foo object at 0x10152c610>
|
||||
|
||||
def _get_b(self):
|
||||
> raise Exception('Failed to get attrib')
|
||||
@@ -332,15 +332,15 @@ get on the terminal - we are working on that):
|
||||
b = 2
|
||||
> assert Foo().b == Bar().b
|
||||
E assert 1 == 2
|
||||
E + where 1 = <failure_demo.Foo object at 0x157a1d0>.b
|
||||
E + where <failure_demo.Foo object at 0x157a1d0> = <class 'failure_demo.Foo'>()
|
||||
E + and 2 = <failure_demo.Bar object at 0x157a9d0>.b
|
||||
E + where <failure_demo.Bar object at 0x157a9d0> = <class 'failure_demo.Bar'>()
|
||||
E + where 1 = <failure_demo.Foo object at 0x10152c950>.b
|
||||
E + where <failure_demo.Foo object at 0x10152c950> = <class 'failure_demo.Foo'>()
|
||||
E + and 2 = <failure_demo.Bar object at 0x10152c250>.b
|
||||
E + where <failure_demo.Bar object at 0x10152c250> = <class 'failure_demo.Bar'>()
|
||||
|
||||
failure_demo.py:124: AssertionError
|
||||
__________________________ TestRaises.test_raises __________________________
|
||||
|
||||
self = <failure_demo.TestRaises instance at 0x157d7e8>
|
||||
self = <failure_demo.TestRaises instance at 0x1015219e0>
|
||||
|
||||
def test_raises(self):
|
||||
s = 'qwe'
|
||||
@@ -352,10 +352,10 @@ get on the terminal - we are working on that):
|
||||
> int(s)
|
||||
E ValueError: invalid literal for int() with base 10: 'qwe'
|
||||
|
||||
<0-codegen /home/hpk/p/pytest/_pytest/python.py:831>:1: ValueError
|
||||
<0-codegen /Users/hpk/p/pytest/_pytest/python.py:957>:1: ValueError
|
||||
______________________ TestRaises.test_raises_doesnt _______________________
|
||||
|
||||
self = <failure_demo.TestRaises instance at 0x158ae60>
|
||||
self = <failure_demo.TestRaises instance at 0x1013794d0>
|
||||
|
||||
def test_raises_doesnt(self):
|
||||
> raises(IOError, "int('3')")
|
||||
@@ -364,7 +364,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:136: Failed
|
||||
__________________________ TestRaises.test_raise ___________________________
|
||||
|
||||
self = <failure_demo.TestRaises instance at 0x158bb90>
|
||||
self = <failure_demo.TestRaises instance at 0x10151f6c8>
|
||||
|
||||
def test_raise(self):
|
||||
> raise ValueError("demo error")
|
||||
@@ -373,7 +373,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:139: ValueError
|
||||
________________________ TestRaises.test_tupleerror ________________________
|
||||
|
||||
self = <failure_demo.TestRaises instance at 0x157cd40>
|
||||
self = <failure_demo.TestRaises instance at 0x1013733f8>
|
||||
|
||||
def test_tupleerror(self):
|
||||
> a,b = [1]
|
||||
@@ -382,7 +382,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:142: ValueError
|
||||
______ TestRaises.test_reinterpret_fails_with_print_for_the_fun_of_it ______
|
||||
|
||||
self = <failure_demo.TestRaises instance at 0x157d488>
|
||||
self = <failure_demo.TestRaises instance at 0x10136e170>
|
||||
|
||||
def test_reinterpret_fails_with_print_for_the_fun_of_it(self):
|
||||
l = [1,2,3]
|
||||
@@ -395,7 +395,7 @@ get on the terminal - we are working on that):
|
||||
l is [1, 2, 3]
|
||||
________________________ TestRaises.test_some_error ________________________
|
||||
|
||||
self = <failure_demo.TestRaises instance at 0x158a7e8>
|
||||
self = <failure_demo.TestRaises instance at 0x10136ef38>
|
||||
|
||||
def test_some_error(self):
|
||||
> if namenotexi:
|
||||
@@ -420,10 +420,10 @@ get on the terminal - we are working on that):
|
||||
> assert 1 == 0
|
||||
E assert 1 == 0
|
||||
|
||||
<2-codegen 'abc-123' /home/hpk/p/pytest/doc/example/assertion/failure_demo.py:162>:2: AssertionError
|
||||
<2-codegen 'abc-123' /Users/hpk/p/pytest/doc/example/assertion/failure_demo.py:162>:2: AssertionError
|
||||
____________________ TestMoreErrors.test_complex_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x158f8c0>
|
||||
self = <failure_demo.TestMoreErrors instance at 0x101520638>
|
||||
|
||||
def test_complex_error(self):
|
||||
def f():
|
||||
@@ -452,7 +452,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:5: AssertionError
|
||||
___________________ TestMoreErrors.test_z1_unpack_error ____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x158c998>
|
||||
self = <failure_demo.TestMoreErrors instance at 0x10136bcb0>
|
||||
|
||||
def test_z1_unpack_error(self):
|
||||
l = []
|
||||
@@ -462,7 +462,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:179: ValueError
|
||||
____________________ TestMoreErrors.test_z2_type_error _____________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x15854d0>
|
||||
self = <failure_demo.TestMoreErrors instance at 0x10136a440>
|
||||
|
||||
def test_z2_type_error(self):
|
||||
l = 3
|
||||
@@ -472,20 +472,19 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:183: TypeError
|
||||
______________________ TestMoreErrors.test_startswith ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x14b65a8>
|
||||
self = <failure_demo.TestMoreErrors instance at 0x101368290>
|
||||
|
||||
def test_startswith(self):
|
||||
s = "123"
|
||||
g = "456"
|
||||
> assert s.startswith(g)
|
||||
E assert False
|
||||
E + where False = <built-in method startswith of str object at 0x14902a0>('456')
|
||||
E + where <built-in method startswith of str object at 0x14902a0> = '123'.startswith
|
||||
E assert <built-in method startswith of str object at 0x101354030>('456')
|
||||
E + where <built-in method startswith of str object at 0x101354030> = '123'.startswith
|
||||
|
||||
failure_demo.py:188: AssertionError
|
||||
__________________ TestMoreErrors.test_startswith_nested ___________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x158d518>
|
||||
self = <failure_demo.TestMoreErrors instance at 0x101368f38>
|
||||
|
||||
def test_startswith_nested(self):
|
||||
def f():
|
||||
@@ -493,39 +492,36 @@ get on the terminal - we are working on that):
|
||||
def g():
|
||||
return "456"
|
||||
> assert f().startswith(g())
|
||||
E assert False
|
||||
E + where False = <built-in method startswith of str object at 0x14902a0>('456')
|
||||
E + where <built-in method startswith of str object at 0x14902a0> = '123'.startswith
|
||||
E + where '123' = <function f at 0x15806e0>()
|
||||
E + and '456' = <function g at 0x1580aa0>()
|
||||
E assert <built-in method startswith of str object at 0x101354030>('456')
|
||||
E + where <built-in method startswith of str object at 0x101354030> = '123'.startswith
|
||||
E + where '123' = <function f at 0x10136c578>()
|
||||
E + and '456' = <function g at 0x10136c5f0>()
|
||||
|
||||
failure_demo.py:195: AssertionError
|
||||
_____________________ TestMoreErrors.test_global_func ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x1593440>
|
||||
self = <failure_demo.TestMoreErrors instance at 0x10136aef0>
|
||||
|
||||
def test_global_func(self):
|
||||
> assert isinstance(globf(42), float)
|
||||
E assert False
|
||||
E + where False = isinstance(43, float)
|
||||
E + where 43 = globf(42)
|
||||
E assert isinstance(43, float)
|
||||
E + where 43 = globf(42)
|
||||
|
||||
failure_demo.py:198: AssertionError
|
||||
_______________________ TestMoreErrors.test_instance _______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x15952d8>
|
||||
self = <failure_demo.TestMoreErrors instance at 0x10151c440>
|
||||
|
||||
def test_instance(self):
|
||||
self.x = 6*7
|
||||
> assert self.x != 42
|
||||
E assert 42 != 42
|
||||
E + where 42 = 42
|
||||
E + where 42 = <failure_demo.TestMoreErrors instance at 0x15952d8>.x
|
||||
E + where 42 = <failure_demo.TestMoreErrors instance at 0x10151c440>.x
|
||||
|
||||
failure_demo.py:202: AssertionError
|
||||
_______________________ TestMoreErrors.test_compare ________________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x1593758>
|
||||
self = <failure_demo.TestMoreErrors instance at 0x101373a70>
|
||||
|
||||
def test_compare(self):
|
||||
> assert globf(10) < 5
|
||||
@@ -535,7 +531,7 @@ get on the terminal - we are working on that):
|
||||
failure_demo.py:205: AssertionError
|
||||
_____________________ TestMoreErrors.test_try_finally ______________________
|
||||
|
||||
self = <failure_demo.TestMoreErrors instance at 0x157cd88>
|
||||
self = <failure_demo.TestMoreErrors instance at 0x101363c68>
|
||||
|
||||
def test_try_finally(self):
|
||||
x = 1
|
||||
@@ -544,4 +540,4 @@ get on the terminal - we are working on that):
|
||||
E assert 1 == 0
|
||||
|
||||
failure_demo.py:210: AssertionError
|
||||
======================== 39 failed in 0.23 seconds =========================
|
||||
======================== 39 failed in 0.41 seconds =========================
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
.. highlightlang:: python
|
||||
|
||||
basic patterns and examples
|
||||
Basic patterns and examples
|
||||
==========================================================
|
||||
|
||||
pass different values to a test function, depending on command line options
|
||||
Pass different values to a test function, depending on command line options
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
.. regendoc:wipe
|
||||
@@ -53,7 +53,7 @@ Let's run this without supplying our new command line option::
|
||||
test_sample.py:6: AssertionError
|
||||
----------------------------- Captured stdout ------------------------------
|
||||
first
|
||||
1 failed in 0.03 seconds
|
||||
1 failed in 0.02 seconds
|
||||
|
||||
And now with supplying a command line option::
|
||||
|
||||
@@ -85,7 +85,7 @@ next example or refer to :ref:`mysetup` for more information
|
||||
on real-life examples.
|
||||
|
||||
|
||||
dynamically adding command line options
|
||||
Dynamically adding command line options
|
||||
--------------------------------------------------------------
|
||||
|
||||
.. regendoc:wipe
|
||||
@@ -109,17 +109,17 @@ directory with the above conftest.py::
|
||||
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
gw0 I / gw1 I / gw2 I / gw3 I
|
||||
gw0 [0] / gw1 [0] / gw2 [0] / gw3 [0]
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
gw0 I
|
||||
gw0 [0]
|
||||
|
||||
scheduling tests via LoadScheduling
|
||||
|
||||
============================= in 0.52 seconds =============================
|
||||
============================= in 0.71 seconds =============================
|
||||
|
||||
.. _`excontrolskip`:
|
||||
|
||||
control skipping of tests according to command line option
|
||||
Control skipping of tests according to command line option
|
||||
--------------------------------------------------------------
|
||||
|
||||
.. regendoc:wipe
|
||||
@@ -156,27 +156,27 @@ and when running it will see a skipped "slow" test::
|
||||
|
||||
$ py.test -rs # "-rs" means report details on the little 's'
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_module.py .s
|
||||
========================= short test summary info ==========================
|
||||
SKIP [1] /tmp/doc-exec-42/conftest.py:9: need --runslow option to run
|
||||
SKIP [1] /Users/hpk/tmp/doc-exec-630/conftest.py:9: need --runslow option to run
|
||||
|
||||
=================== 1 passed, 1 skipped in 0.01 seconds ====================
|
||||
=================== 1 passed, 1 skipped in 0.02 seconds ====================
|
||||
|
||||
Or run it including the ``slow`` marked test::
|
||||
|
||||
$ py.test --runslow
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 2 items
|
||||
|
||||
test_module.py ..
|
||||
|
||||
========================= 2 passed in 0.01 seconds =========================
|
||||
========================= 2 passed in 0.62 seconds =========================
|
||||
|
||||
writing well integrated assertion helpers
|
||||
Writing well integrated assertion helpers
|
||||
--------------------------------------------------
|
||||
|
||||
.. regendoc:wipe
|
||||
@@ -261,7 +261,7 @@ which will add the string to the test header accordingly::
|
||||
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
project deps: mylib-1.1
|
||||
collecting ... collected 0 items
|
||||
|
||||
@@ -284,7 +284,7 @@ which will add info only when run with "--v"::
|
||||
|
||||
$ py.test -v
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3 -- /home/hpk/venv/0/bin/python
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0 -- /Users/hpk/venv/1/bin/python
|
||||
info1: did you know that ...
|
||||
did you?
|
||||
collecting ... collected 0 items
|
||||
@@ -295,7 +295,45 @@ and nothing when run plainly::
|
||||
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 0 items
|
||||
|
||||
============================= in 0.00 seconds =============================
|
||||
|
||||
profiling test duration
|
||||
--------------------------
|
||||
|
||||
.. regendoc:wipe
|
||||
|
||||
.. versionadded: 2.2
|
||||
|
||||
If you have a slow running large test suite you might want to find
|
||||
out which tests are slowest. Let's make an artifical test suite::
|
||||
|
||||
# content of test_some_are_slow.py
|
||||
|
||||
import time
|
||||
|
||||
def test_funcfast():
|
||||
pass
|
||||
|
||||
def test_funcslow1():
|
||||
time.sleep(0.1)
|
||||
|
||||
def test_funcslow2():
|
||||
time.sleep(0.2)
|
||||
|
||||
Now we can profile which test functions execute slowest::
|
||||
|
||||
$ py.test --durations=3
|
||||
=========================== test session starts ============================
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 3 items
|
||||
|
||||
test_some_are_slow.py ...
|
||||
|
||||
========================= slowest 3 test durations =========================
|
||||
0.20s call test_some_are_slow.py::test_funcslow2
|
||||
0.10s call test_some_are_slow.py::test_funcslow1
|
||||
0.00s setup test_some_are_slow.py::test_funcfast
|
||||
========================= 3 passed in 0.32 seconds =========================
|
||||
|
||||
20
doc/faq.txt
20
doc/faq.txt
@@ -47,19 +47,23 @@ customizable testing frameworks for Python. However,
|
||||
``py.test`` still uses many metaprogramming techniques and
|
||||
reading its source is thus likely not something for Python beginners.
|
||||
|
||||
A second "magic" issue arguably the assert statement re-intepreation:
|
||||
When an ``assert`` statement fails, py.test re-interprets the expression
|
||||
to show intermediate values if a test fails. If your expression
|
||||
has side effects (better to avoid them anyway!) the intermediate values
|
||||
may not be the same, obfuscating the initial error (this is also
|
||||
explained at the command line if it happens).
|
||||
``py.test --no-assert`` turns off assert re-interpretation.
|
||||
A second "magic" issue arguably the assert statement debugging feature. When
|
||||
loading test modules py.test rewrites the source code of assert statements. When
|
||||
a rewritten assert statement fails, its error message has more information than
|
||||
the original. py.test also has a second assert debugging technique. When an
|
||||
``assert`` statement that was missed by the rewriter fails, py.test
|
||||
re-interprets the expression to show intermediate values if a test fails. This
|
||||
second technique suffers from caveat that the rewriting does not: If your
|
||||
expression has side effects (better to avoid them anyway!) the intermediate
|
||||
values may not be the same, confusing the reinterpreter and obfuscating the
|
||||
initial error (this is also explained at the command line if it happens).
|
||||
You can turn off all assertion debugging with ``py.test --assertmode=off``.
|
||||
|
||||
.. _`py namespaces`: index.html
|
||||
.. _`py/__init__.py`: http://bitbucket.org/hpk42/py-trunk/src/trunk/py/__init__.py
|
||||
|
||||
|
||||
function arguments, parametrized tests and setup
|
||||
Function arguments, parametrized tests and setup
|
||||
-------------------------------------------------------
|
||||
|
||||
.. _funcargs: test/funcargs.html
|
||||
|
||||
@@ -38,6 +38,7 @@ very useful if you want to test e.g. against different database backends
|
||||
or with multiple numerical arguments sets and want to reuse the same set
|
||||
of test functions.
|
||||
|
||||
py.test comes with :ref:`builtinfuncargs` and there are some refined usages in the examples section.
|
||||
|
||||
.. _funcarg:
|
||||
|
||||
@@ -61,7 +62,7 @@ Running the test looks like this::
|
||||
|
||||
$ py.test test_simplefactory.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 1 items
|
||||
|
||||
test_simplefactory.py F
|
||||
@@ -76,7 +77,7 @@ Running the test looks like this::
|
||||
E assert 42 == 17
|
||||
|
||||
test_simplefactory.py:5: AssertionError
|
||||
========================= 1 failed in 0.02 seconds =========================
|
||||
========================= 1 failed in 0.03 seconds =========================
|
||||
|
||||
This means that indeed the test function was called with a ``myfuncarg``
|
||||
argument value of ``42`` and the assert fails. Here is how py.test
|
||||
@@ -157,17 +158,16 @@ hook to generate several calls to the same test function::
|
||||
# content of test_example.py
|
||||
def pytest_generate_tests(metafunc):
|
||||
if "numiter" in metafunc.funcargnames:
|
||||
for i in range(10):
|
||||
metafunc.addcall(funcargs=dict(numiter=i))
|
||||
metafunc.parametrize("numiter", range(10))
|
||||
|
||||
def test_func(numiter):
|
||||
assert numiter < 9
|
||||
|
||||
Running this::
|
||||
Running this will generate ten invocations of ``test_func`` passing in each of the items in the list of ``range(10)``::
|
||||
|
||||
$ py.test test_example.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 10 items
|
||||
|
||||
test_example.py .........F
|
||||
@@ -181,16 +181,16 @@ Running this::
|
||||
> assert numiter < 9
|
||||
E assert 9 < 9
|
||||
|
||||
test_example.py:7: AssertionError
|
||||
==================== 1 failed, 9 passed in 0.03 seconds ====================
|
||||
test_example.py:6: AssertionError
|
||||
==================== 1 failed, 9 passed in 0.05 seconds ====================
|
||||
|
||||
Note that the ``pytest_generate_tests(metafunc)`` hook is called during
|
||||
Obviously, only when ``numiter`` has the value of ``9`` does the test fail. Note that the ``pytest_generate_tests(metafunc)`` hook is called during
|
||||
the test collection phase which is separate from the actual test running.
|
||||
Let's just look at what is collected::
|
||||
|
||||
$ py.test --collectonly test_example.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 10 items
|
||||
<Module 'test_example.py'>
|
||||
<Function 'test_func[0]'>
|
||||
@@ -204,19 +204,19 @@ Let's just look at what is collected::
|
||||
<Function 'test_func[8]'>
|
||||
<Function 'test_func[9]'>
|
||||
|
||||
============================= in 0.00 seconds =============================
|
||||
============================= in 0.01 seconds =============================
|
||||
|
||||
If you want to select only the run with the value ``7`` you could do::
|
||||
|
||||
$ py.test -v -k 7 test_example.py # or -k test_func[7]
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3 -- /home/hpk/venv/0/bin/python
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0 -- /Users/hpk/venv/1/bin/python
|
||||
collecting ... collected 10 items
|
||||
|
||||
test_example.py:6: test_func[7] PASSED
|
||||
test_example.py:5: test_func[7] PASSED
|
||||
|
||||
======================== 9 tests deselected by '7' =========================
|
||||
================== 1 passed, 9 deselected in 0.01 seconds ==================
|
||||
======================= 9 tests deselected by '-k7' ========================
|
||||
================== 1 passed, 9 deselected in 0.02 seconds ==================
|
||||
|
||||
You might want to look at :ref:`more parametrization examples <paramexamples>`.
|
||||
|
||||
@@ -240,4 +240,5 @@ in the class or module where a test function is defined:
|
||||
|
||||
``metafunc.config``: access to command line opts and general config
|
||||
|
||||
.. automethod:: Metafunc.parametrize(name, values, idmaker=None)
|
||||
.. automethod:: Metafunc.addcall(funcargs=None, id=_notexists, param=_notexists)
|
||||
|
||||
@@ -1,7 +1,13 @@
|
||||
Installation and Getting Started
|
||||
===================================
|
||||
|
||||
**Compatibility**: Python 2.4-3.2, Jython, PyPy on Unix/Posix and Windows
|
||||
**Pythons**: Python 2.4-3.2, Jython, PyPy
|
||||
|
||||
**Platforms**: Unix/Posix and Windows
|
||||
|
||||
**PyPI package name**: `pytest <http://pypi.python.org/pypi/pytest>`_
|
||||
|
||||
**documentation as PDF**: `download latest <http://pytest.org/latest/pytest.pdf>`_
|
||||
|
||||
.. _`getstarted`:
|
||||
|
||||
@@ -10,16 +16,15 @@ Installation
|
||||
|
||||
Installation options::
|
||||
|
||||
easy_install -U pytest # or
|
||||
pip install -U pytest
|
||||
pip install -U pytest # or
|
||||
easy_install -U pytest
|
||||
|
||||
To check your installation has installed the correct version::
|
||||
|
||||
$ py.test --version
|
||||
This is py.test version 2.0.3, imported from /home/hpk/p/pytest/pytest.pyc
|
||||
This is py.test version 2.2.0, imported from /Users/hpk/p/pytest/pytest.pyc
|
||||
setuptools registered plugins:
|
||||
pytest-xdist-1.6.dev3 at /home/hpk/p/pytest-xdist/xdist/plugin.pyc
|
||||
pytest-incremental-0.1.0 at /home/hpk/venv/0/lib/python2.6/site-packages/pytest_incremental.pyc
|
||||
pytest-xdist-1.7.dev1 at /Users/hpk/p/pytest-xdist/xdist/plugin.pyc
|
||||
|
||||
If you get an error checkout :ref:`installation issues`.
|
||||
|
||||
@@ -41,7 +46,7 @@ That's it. You can execute the test function now::
|
||||
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 1 items
|
||||
|
||||
test_sample.py F
|
||||
@@ -55,28 +60,18 @@ That's it. You can execute the test function now::
|
||||
E + where 4 = func(3)
|
||||
|
||||
test_sample.py:5: AssertionError
|
||||
========================= 1 failed in 0.02 seconds =========================
|
||||
========================= 1 failed in 0.04 seconds =========================
|
||||
|
||||
py.test found the ``test_answer`` function by following :ref:`standard test discovery rules <test discovery>`, basically detecting the ``test_`` prefixes. We got a failure report because our little ``func(3)`` call did not return ``5``.
|
||||
|
||||
.. note::
|
||||
|
||||
You can simply use the ``assert`` statement for asserting
|
||||
expectations because intermediate values will be presented to you.
|
||||
This is arguably easier than learning all the `the JUnit legacy
|
||||
methods`_.
|
||||
You can simply use the ``assert`` statement for asserting test
|
||||
expectations. pytest's :ref:`assert introspection` will intelligently
|
||||
report intermediate values of the assert expression freeing
|
||||
you from the need to learn the many names of `JUnit legacy methods`_.
|
||||
|
||||
However, there remains one caveat to using simple asserts: your
|
||||
assertion expression should better be side-effect free. Because
|
||||
after an assertion failed py.test will re-evaluate the expression
|
||||
in order to present intermediate values. You will get a nice warning
|
||||
and you can easily fix it: compute the value ahead of the assert and
|
||||
then do the assertion. Or maybe just use the assert "explicit message"
|
||||
syntax::
|
||||
|
||||
assert expr, "message" # show "message" if expr is not True
|
||||
|
||||
.. _`the JUnit legacy methods`: http://docs.python.org/library/unittest.html#test-cases
|
||||
.. _`JUnit legacy methods`: http://docs.python.org/library/unittest.html#test-cases
|
||||
|
||||
.. _`assert statement`: http://docs.python.org/reference/simple_stmts.html#the-assert-statement
|
||||
|
||||
@@ -131,13 +126,12 @@ run the module by passing its filename::
|
||||
================================= FAILURES =================================
|
||||
____________________________ TestClass.test_two ____________________________
|
||||
|
||||
self = <test_class.TestClass instance at 0x142c320>
|
||||
self = <test_class.TestClass instance at 0x10150a170>
|
||||
|
||||
def test_two(self):
|
||||
x = "hello"
|
||||
> assert hasattr(x, 'check')
|
||||
E assert False
|
||||
E + where False = hasattr('hello', 'check')
|
||||
E assert hasattr('hello', 'check')
|
||||
|
||||
test_class.py:8: AssertionError
|
||||
1 failed, 1 passed in 0.03 seconds
|
||||
@@ -169,7 +163,7 @@ before performing the test function call. Let's just run it::
|
||||
================================= FAILURES =================================
|
||||
_____________________________ test_needsfiles ______________________________
|
||||
|
||||
tmpdir = local('/tmp/pytest-10/test_needsfiles0')
|
||||
tmpdir = local('/Users/hpk/tmp/pytest-1595/test_needsfiles0')
|
||||
|
||||
def test_needsfiles(tmpdir):
|
||||
print tmpdir
|
||||
@@ -178,8 +172,8 @@ before performing the test function call. Let's just run it::
|
||||
|
||||
test_tmpdir.py:3: AssertionError
|
||||
----------------------------- Captured stdout ------------------------------
|
||||
/tmp/pytest-10/test_needsfiles0
|
||||
1 failed in 0.13 seconds
|
||||
/Users/hpk/tmp/pytest-1595/test_needsfiles0
|
||||
1 failed in 0.15 seconds
|
||||
|
||||
Before the test runs, a unique-per-test-invocation temporary directory
|
||||
was created. More info at :ref:`tmpdir handling`.
|
||||
@@ -188,7 +182,7 @@ You can find out what kind of builtin :ref:`funcargs` exist by typing::
|
||||
|
||||
py.test --funcargs # shows builtin and custom function arguments
|
||||
|
||||
where to go next
|
||||
Where to go next
|
||||
-------------------------------------
|
||||
|
||||
Here are a few suggestions where to go next:
|
||||
@@ -206,10 +200,15 @@ Known Installation issues
|
||||
easy_install or pip not found?
|
||||
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
Consult `distribute docs`_ to install the ``easy_install``
|
||||
tool on your machine. You may also use the older
|
||||
`setuptools`_ project but it lacks bug fixes and does not
|
||||
work on Python3. If you use Python2 you may also install pip_.
|
||||
.. _`install pip`: http://www.pip-installer.org/en/latest/index.html
|
||||
|
||||
`Install pip`_ for a state of the art python package installer.
|
||||
|
||||
Or consult `distribute docs`_ to install the ``easy_install``
|
||||
tool on your machine.
|
||||
|
||||
You may also use the older `setuptools`_ project but it lacks bug fixes
|
||||
and does not work on Python3.
|
||||
|
||||
py.test not found on Windows despite installation?
|
||||
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
|
||||
|
||||
@@ -53,7 +53,7 @@ You can tell people to download the script and then e.g. run it like this::
|
||||
Integrating with distutils / ``python setup.py test``
|
||||
--------------------------------------------------------
|
||||
|
||||
You can easily integrate test runs into your distutils or
|
||||
You can integrate test runs into your distutils or
|
||||
setuptools based project. Use the `genscript method`_
|
||||
to generate a standalone py.test script::
|
||||
|
||||
@@ -106,9 +106,9 @@ Conventions for Python test discovery
|
||||
* ``Test`` prefixed test classes (without an ``__init__`` method)
|
||||
* ``test_`` prefixed test functions or methods are test items
|
||||
|
||||
For examples of how to cnd cusotmize your test discovery :doc:`example/pythoncollection`.
|
||||
For examples of how to customize your test discovery :doc:`example/pythoncollection`.
|
||||
|
||||
py.test additionally discovers tests using the standard
|
||||
Within Python modules, py.test also discovers tests using the standard
|
||||
:ref:`unittest.TestCase <unittest.TestCase>` subclassing technique.
|
||||
|
||||
Choosing a test layout / import rules
|
||||
@@ -138,7 +138,10 @@ py.test supports common test layouts:
|
||||
test_app.py
|
||||
...
|
||||
|
||||
You can always run your tests by pointing to it::
|
||||
In both cases you usually need to make sure that ``mypkg`` is importable,
|
||||
for example by using the setuptools ``python setup.py develop`` method.
|
||||
|
||||
You can run your tests by pointing to it::
|
||||
|
||||
py.test tests/test_app.py # for external test dirs
|
||||
py.test mypkg/test/test_app.py # for inlined test dirs
|
||||
@@ -150,18 +153,27 @@ You can always run your tests by pointing to it::
|
||||
|
||||
.. note::
|
||||
|
||||
Test modules are imported under their fully qualified name as follows:
|
||||
If py.test finds a "a/b/test_module.py" test file while
|
||||
recursing into the filesystem it determines the import name
|
||||
as follows:
|
||||
|
||||
* find ``basedir`` -- this is the first "upward" (towards the root)
|
||||
directory not containing an ``__init__.py``
|
||||
directory not containing an ``__init__.py``. If both the ``a``
|
||||
and ``b`` directories contain an ``__init__.py`` the basedir will
|
||||
be the parent dir of ``a``.
|
||||
|
||||
* perform ``sys.path.insert(0, basedir)`` to make the fully
|
||||
qualified test module path importable.
|
||||
* perform ``sys.path.insert(0, basedir)`` to make the test module
|
||||
importable under the fully qualified import name.
|
||||
|
||||
* ``import path.to.test_module`` where the path is determined
|
||||
by converting path separators into "." files. This means
|
||||
* ``import a.b.test_module`` where the path is determined
|
||||
by converting path separators ``/`` into "." characters. This means
|
||||
you must follow the convention of having directory and file
|
||||
names map to the import names.
|
||||
names map directly to the import names.
|
||||
|
||||
The reason for this somewhat evolved importing technique is
|
||||
that in larger projects multiple test modules might import
|
||||
from each other and thus deriving a canonical import name helps
|
||||
to avoid surprises such as a test modules getting imported twice.
|
||||
|
||||
|
||||
.. include:: links.inc
|
||||
|
||||
@@ -1,17 +1,16 @@
|
||||
|
||||
|
||||
Welcome to ``py.test``!
|
||||
Welcome to pytest!
|
||||
=============================================
|
||||
|
||||
|
||||
- **a mature full-featured testing tool**
|
||||
|
||||
- runs on Posix/Windows, Python 2.4-3.2, PyPy and Jython
|
||||
- runs on Posix/Windows, Python 2.4-3.2, PyPy and Jython-2.5.1
|
||||
- :ref:`comprehensive online <toc>` and `PDF documentation <pytest.pdf>`_
|
||||
- continuously `tested on many Python interpreters <http://hudson.testrun.org/view/pytest/job/pytest/>`_
|
||||
- used in :ref:`many projects and organisations <projects>`, in test
|
||||
suites ranging from 10 to 10s of thousands of tests
|
||||
- has :ref:`comprehensive documentation <toc>`
|
||||
- comes with :ref:`tested examples <examples>`
|
||||
- comes with many :ref:`tested examples <examples>`
|
||||
- supports :ref:`good integration practises <goodpractises>`
|
||||
|
||||
- **provides no-boilerplate testing**
|
||||
@@ -26,14 +25,15 @@ Welcome to ``py.test``!
|
||||
|
||||
- **supports functional testing and complex test setups**
|
||||
|
||||
- (new in 2.2) :ref:`durations`
|
||||
- (much improved in 2.2) :ref:`marking and test selection <mark>`
|
||||
- (improved in 2.2) :ref:`parametrized test functions <parametrized test functions>`
|
||||
- advanced :ref:`skip and xfail`
|
||||
- generic :ref:`marking and test selection <mark>`
|
||||
- unique :ref:`dependency injection through funcargs <funcargs>`
|
||||
- can :ref:`distribute tests to multiple CPUs <xdistcpu>` through :ref:`xdist plugin <xdist>`
|
||||
- can :ref:`continuously re-run failing tests <looponfailing>`
|
||||
- many :ref:`builtin helpers <pytest helpers>`
|
||||
- flexible :ref:`Python test discovery`
|
||||
- unique :ref:`dependency injection through funcargs <funcargs>`
|
||||
- :ref:`parametrized test functions <parametrized test functions>`
|
||||
|
||||
- **integrates many common testing methods**
|
||||
|
||||
@@ -54,9 +54,4 @@ Welcome to ``py.test``!
|
||||
|
||||
.. _`easy`: http://bruynooghe.blogspot.com/2009/12/skipping-slow-test-by-default-in-pytest.html
|
||||
|
||||
.. toctree::
|
||||
:hidden:
|
||||
|
||||
contact.txt
|
||||
contents.txt
|
||||
|
||||
|
||||
121
doc/mark.txt
121
doc/mark.txt
@@ -1,122 +1,25 @@
|
||||
|
||||
.. _mark:
|
||||
|
||||
mark test functions with attributes
|
||||
Marking test functions with attributes
|
||||
=================================================================
|
||||
|
||||
.. currentmodule:: _pytest.mark
|
||||
|
||||
By using the ``pytest.mark`` helper you can instantiate
|
||||
decorators that will set named metadata on test functions.
|
||||
By using the ``pytest.mark`` helper you can easily set
|
||||
metadata on your test functions. There are
|
||||
some builtin markers, for example:
|
||||
|
||||
Marking a single function
|
||||
----------------------------------------------------
|
||||
* :ref:`skipif <skipif>` - skip a test function if a certain condition is met
|
||||
* :ref:`xfail <xfail>` - produce an "expected failure" outcome if a certain
|
||||
condition is met
|
||||
* :ref:`parametrize <parametrizemark>` to perform multiple calls
|
||||
to the same test function.
|
||||
|
||||
You can "mark" a test function with metadata like this::
|
||||
It's easy to create custom markers or to apply markers
|
||||
to whole test classes or modules. See :ref:`mark examples` for examples
|
||||
which also serve as documentation.
|
||||
|
||||
import pytest
|
||||
@pytest.mark.webtest
|
||||
def test_send_http():
|
||||
...
|
||||
|
||||
This will set the function attribute ``webtest`` to a :py:class:`MarkInfo`
|
||||
instance. You can also specify parametrized metadata like this::
|
||||
|
||||
# content of test_mark.py
|
||||
|
||||
import pytest
|
||||
@pytest.mark.webtest(firefox=30)
|
||||
def test_receive():
|
||||
pass
|
||||
|
||||
@pytest.mark.webtest("functional", firefox=30)
|
||||
def test_run_and_look():
|
||||
pass
|
||||
|
||||
and access it from other places like this::
|
||||
|
||||
test_receive.webtest.kwargs['firefox'] == 30
|
||||
test_run_and_look.webtest.args[0] == "functional"
|
||||
|
||||
.. _`scoped-marking`:
|
||||
|
||||
Marking whole classes or modules
|
||||
----------------------------------------------------
|
||||
|
||||
If you are programming with Python2.6 you may use ``pytest.mark`` decorators
|
||||
with classes to apply markers to all of its test methods::
|
||||
|
||||
# content of test_mark_classlevel.py
|
||||
import pytest
|
||||
@pytest.mark.webtest
|
||||
class TestClass:
|
||||
def test_startup(self):
|
||||
pass
|
||||
def test_startup_and_more(self):
|
||||
pass
|
||||
|
||||
This is equivalent to directly applying the decorator to the
|
||||
two test functions.
|
||||
|
||||
To remain compatible with Python2.5 you can also set a
|
||||
``pytestmark`` attribute on a TestClass like this::
|
||||
|
||||
import pytest
|
||||
|
||||
class TestClass:
|
||||
pytestmark = pytest.mark.webtest
|
||||
|
||||
or if you need to use multiple markers you can use a list::
|
||||
|
||||
import pytest
|
||||
|
||||
class TestClass:
|
||||
pytestmark = [pytest.mark.webtest, pytest.mark.slowtest]
|
||||
|
||||
You can also set a module level marker::
|
||||
|
||||
import pytest
|
||||
pytestmark = pytest.mark.webtest
|
||||
|
||||
in which case it will be applied to all functions and
|
||||
methods defined in the module.
|
||||
|
||||
Using ``-k TEXT`` to select tests
|
||||
----------------------------------------------------
|
||||
|
||||
You can use the ``-k`` command line option to select tests::
|
||||
|
||||
$ py.test -k webtest # running with the above defined examples yields
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_mark.py ..
|
||||
test_mark_classlevel.py ..
|
||||
|
||||
========================= 4 passed in 0.01 seconds =========================
|
||||
|
||||
And you can also run all tests except the ones that match the keyword::
|
||||
|
||||
$ py.test -k-webtest
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
collecting ... collected 4 items
|
||||
|
||||
===================== 4 tests deselected by '-webtest' =====================
|
||||
======================= 4 deselected in 0.01 seconds =======================
|
||||
|
||||
Or to only select the class::
|
||||
|
||||
$ py.test -kTestClass
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
collecting ... collected 4 items
|
||||
|
||||
test_mark_classlevel.py ..
|
||||
|
||||
==================== 2 tests deselected by 'TestClass' =====================
|
||||
================== 2 passed, 2 deselected in 0.01 seconds ==================
|
||||
|
||||
API reference for mark related objects
|
||||
------------------------------------------------
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
|
||||
monkeypatching/mocking modules and environments
|
||||
Monkeypatching/mocking modules and environments
|
||||
================================================================
|
||||
|
||||
.. currentmodule:: _pytest.monkeypatch
|
||||
@@ -39,10 +39,10 @@ will be undone.
|
||||
.. background check:
|
||||
$ py.test
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 0 items
|
||||
|
||||
============================= in 0.00 seconds =============================
|
||||
============================= in 0.20 seconds =============================
|
||||
|
||||
Method reference of the monkeypatch function argument
|
||||
-----------------------------------------------------
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
Running test written for nose
|
||||
Running tests written for nose
|
||||
=======================================
|
||||
|
||||
.. include:: links.inc
|
||||
|
||||
@@ -5,6 +5,7 @@ Getting started basics
|
||||
.. toctree::
|
||||
:maxdepth: 2
|
||||
|
||||
index.txt
|
||||
getting-started.txt
|
||||
usage.txt
|
||||
goodpractises.txt
|
||||
|
||||
@@ -203,7 +203,7 @@ and their names. It will also print local plugins aka
|
||||
|
||||
.. _`cmdunregister`:
|
||||
|
||||
deactivate / unregister a plugin by name
|
||||
Deactivating / unregistering a plugin by name
|
||||
----------------------------------------------------------------------------
|
||||
|
||||
You can prevent plugins from loading or unregister them::
|
||||
@@ -253,7 +253,7 @@ in the `pytest repository <http://bitbucket.org/hpk42/pytest/>`_.
|
||||
py.test hook reference
|
||||
====================================
|
||||
|
||||
hook specification and validation
|
||||
Hook specification and validation
|
||||
-----------------------------------------
|
||||
|
||||
py.test calls hook functions to implement initialization, running,
|
||||
@@ -264,7 +264,7 @@ specification. However, a hook function may accept *fewer* parameters
|
||||
by simply not specifying them. If you mistype argument names or the
|
||||
hook name itself you get an error showing the available arguments.
|
||||
|
||||
initialisation, command line and configuration hooks
|
||||
Initialisation, command line and configuration hooks
|
||||
--------------------------------------------------------------------
|
||||
|
||||
.. currentmodule:: _pytest.hookspec
|
||||
@@ -277,7 +277,7 @@ initialisation, command line and configuration hooks
|
||||
.. autofunction:: pytest_configure
|
||||
.. autofunction:: pytest_unconfigure
|
||||
|
||||
generic "runtest" hooks
|
||||
Generic "runtest" hooks
|
||||
------------------------------
|
||||
|
||||
All all runtest related hooks receive a :py:class:`pytest.Item` object.
|
||||
@@ -297,7 +297,7 @@ into interactive debugging when a test failure occurs.
|
||||
The :py:mod:`_pytest.terminal` reported specifically uses
|
||||
the reporting hook to print information about a test run.
|
||||
|
||||
collection hooks
|
||||
Collection hooks
|
||||
------------------------------
|
||||
|
||||
py.test calls the following hooks for collecting files and directories:
|
||||
@@ -312,7 +312,7 @@ you can use the following hook:
|
||||
.. autofunction:: pytest_pycollect_makeitem
|
||||
|
||||
|
||||
reporting hooks
|
||||
Reporting hooks
|
||||
------------------------------
|
||||
|
||||
Session related reporting hooks:
|
||||
@@ -327,7 +327,6 @@ test execution:
|
||||
|
||||
.. autofunction: pytest_runtest_logreport
|
||||
|
||||
|
||||
Reference of important objects involved in hooks
|
||||
===========================================================
|
||||
|
||||
|
||||
@@ -5,7 +5,8 @@ Project examples
|
||||
|
||||
Here are some examples of projects using py.test (please send notes via :ref:`contact`):
|
||||
|
||||
* `PyPy <http://pypy.org>`_, Python with a JIT compiler, running over `16000 tests <http://test.pypy.org>`_
|
||||
* `PyPy <http://pypy.org>`_, Python with a JIT compiler, running over
|
||||
`16000 tests <http://buildbot.pypy.org/summary?branch=%3Ctrunk%3E>`_
|
||||
* the `MoinMoin <http://moinmo.in>`_ Wiki Engine
|
||||
* `tox <http://codespeak.net/tox>`_, virtualenv/Hudson integration tool
|
||||
* `PIDA <http://pida.co.uk>`_ framework for integrated development
|
||||
@@ -42,9 +43,10 @@ Some organisations using py.test
|
||||
-----------------------------------
|
||||
|
||||
* `Square Kilometre Array, Cape Town <http://ska.ac.za/>`_
|
||||
* `Some Mozilla QA people <http://www.theautomatedtester.co.uk/blog/2011/pytest_and_xdist_plugin.html>`_ use pytest to distribute their Selenium tests
|
||||
* `Tandberg <http://www.tandberg.com/>`_
|
||||
* `Shootq <http://web.shootq.com/>`_
|
||||
* `Stups department of Heinrich Heine University Düsseldorf <http://www.stups.uni-duesseldorf.de/projects.php>`_
|
||||
* `Stups department of Heinrich Heine University Duesseldorf <http://www.stups.uni-duesseldorf.de/projects.php>`_
|
||||
* `cellzome <http://www.cellzome.com/>`_
|
||||
* `Open End, Gothenborg <http://www.openend.se>`_
|
||||
* `Laboraratory of Bioinformatics, Warsaw <http://genesilico.pl/>`_
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
|
||||
asserting deprecation and other warnings.
|
||||
Asserting deprecation and other warnings.
|
||||
=====================================================
|
||||
|
||||
recwarn function argument
|
||||
The recwarn function argument
|
||||
------------------------------------
|
||||
|
||||
You can use the ``recwarn`` funcarg to assert that code triggers
|
||||
@@ -24,7 +24,7 @@ The ``recwarn`` function argument provides these methods:
|
||||
* ``pop(category=None)``: return last warning matching the category.
|
||||
* ``clear()``: clear list of warnings
|
||||
|
||||
ensuring a function triggers a deprecation warning
|
||||
Ensuring a function triggers a deprecation warning
|
||||
-------------------------------------------------------
|
||||
|
||||
You can also call a global helper for checking
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.. _`skip and xfail`:
|
||||
|
||||
skip and xfail: dealing with tests that can not succeed
|
||||
Skip and xfail: dealing with tests that can not succeed
|
||||
=====================================================================
|
||||
|
||||
If you have test functions that cannot be run on certain platforms
|
||||
@@ -62,7 +62,7 @@ decorator at module level like this::
|
||||
def test_function():
|
||||
...
|
||||
|
||||
skip all test functions of a class
|
||||
Skip all test functions of a class
|
||||
--------------------------------------
|
||||
|
||||
As with all function :ref:`marking <mark>` you can skip test functions at the
|
||||
@@ -92,7 +92,7 @@ Using multiple "skipif" decorators on a single function is generally fine - it m
|
||||
|
||||
.. _xfail:
|
||||
|
||||
mark a test function as expected to fail
|
||||
Mark a test function as expected to fail
|
||||
-------------------------------------------------------
|
||||
|
||||
You can use the ``xfail`` marker to indicate that you
|
||||
@@ -130,7 +130,7 @@ Running it with the report-on-xfail option gives this output::
|
||||
|
||||
example $ py.test -rx xfail_demo.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 6 items
|
||||
|
||||
xfail_demo.py xxxxxx
|
||||
@@ -147,11 +147,11 @@ Running it with the report-on-xfail option gives this output::
|
||||
XFAIL xfail_demo.py::test_hello6
|
||||
reason: reason
|
||||
|
||||
======================== 6 xfailed in 0.05 seconds =========================
|
||||
======================== 6 xfailed in 0.08 seconds =========================
|
||||
|
||||
.. _`evaluation of skipif/xfail conditions`:
|
||||
|
||||
evaluation of skipif/xfail expressions
|
||||
Evaluation of skipif/xfail expressions
|
||||
----------------------------------------------------
|
||||
|
||||
.. versionadded:: 2.0.2
|
||||
@@ -174,7 +174,7 @@ which you might have added::
|
||||
...
|
||||
|
||||
|
||||
imperative xfail from within a test or setup function
|
||||
Imperative xfail from within a test or setup function
|
||||
------------------------------------------------------
|
||||
|
||||
If you cannot declare xfail-conditions at import time
|
||||
@@ -186,7 +186,7 @@ within test or setup code. Example::
|
||||
pytest.xfail("unsupported configuration")
|
||||
|
||||
|
||||
skipping on a missing import dependency
|
||||
Skipping on a missing import dependency
|
||||
--------------------------------------------------
|
||||
|
||||
You can use the following import helper at module level
|
||||
@@ -202,7 +202,7 @@ version number of a library::
|
||||
|
||||
The version will be read from the specified module's ``__version__`` attribute.
|
||||
|
||||
imperative skip from within a test or setup function
|
||||
Imperative skip from within a test or setup function
|
||||
------------------------------------------------------
|
||||
|
||||
If for some reason you cannot declare skip-conditions
|
||||
|
||||
@@ -4,35 +4,40 @@ Talks and Tutorials
|
||||
|
||||
.. _`funcargs`: funcargs.html
|
||||
|
||||
tutorial examples and blog postings
|
||||
Tutorial examples and blog postings
|
||||
---------------------------------------------
|
||||
|
||||
.. _`tutorial1 repository`: http://bitbucket.org/hpk42/pytest-tutorial1/
|
||||
.. _`pycon 2010 tutorial PDF`: http://bitbucket.org/hpk42/pytest-tutorial1/raw/tip/pytest-basic.pdf
|
||||
|
||||
basic usage and funcargs:
|
||||
Basic usage and funcargs:
|
||||
|
||||
- `pycon 2010 tutorial PDF`_ and `tutorial1 repository`_
|
||||
|
||||
function arguments:
|
||||
Function arguments:
|
||||
|
||||
- :ref:`mysetup`
|
||||
- `application setup in test functions with funcargs`_
|
||||
- `monkey patching done right`_ (blog post, consult `monkeypatch
|
||||
plugin`_ for actual 1.0 API)
|
||||
|
||||
test parametrization:
|
||||
Test parametrization:
|
||||
|
||||
- `generating parametrized tests with funcargs`_
|
||||
- `generating parametrized tests with funcargs`_ (uses deprecated ``addcall()`` API.
|
||||
- `test generators and cached setup`_
|
||||
- `parametrizing tests, generalized`_ (blog post)
|
||||
- `putting test-hooks into local or global plugins`_ (blog post)
|
||||
|
||||
distributed testing:
|
||||
Assertion introspection:
|
||||
|
||||
- `(07/2011) Behind the scenes of py.test's new assertion rewriting
|
||||
<http://pybites.blogspot.com/2011/07/behind-scenes-of-pytests-new-assertion.html>`_
|
||||
|
||||
Distributed testing:
|
||||
|
||||
- `simultaneously test your code on all platforms`_ (blog entry)
|
||||
|
||||
plugin specific examples:
|
||||
Plugin specific examples:
|
||||
|
||||
- `skipping slow tests by default in py.test`_ (blog entry)
|
||||
|
||||
@@ -49,7 +54,7 @@ plugin specific examples:
|
||||
.. _`generating parametrized tests with funcargs`: funcargs.html#test-generators
|
||||
.. _`test generators and cached setup`: http://bruynooghe.blogspot.com/2010/06/pytest-test-generators-and-cached-setup.html
|
||||
|
||||
conference talks and tutorials
|
||||
Conference talks and tutorials
|
||||
----------------------------------------
|
||||
|
||||
- `ep2009-rapidtesting.pdf`_ tutorial slides (July 2009):
|
||||
|
||||
@@ -1,10 +1,10 @@
|
||||
|
||||
.. _`tmpdir handling`:
|
||||
|
||||
temporary directories and files
|
||||
Temporary directories and files
|
||||
================================================
|
||||
|
||||
the 'tmpdir' test function argument
|
||||
The 'tmpdir' test function argument
|
||||
-----------------------------------
|
||||
|
||||
You can use the ``tmpdir`` function argument which will
|
||||
@@ -28,7 +28,7 @@ Running this would result in a passed test except for the last
|
||||
|
||||
$ py.test test_tmpdir.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 1 items
|
||||
|
||||
test_tmpdir.py F
|
||||
@@ -36,7 +36,7 @@ Running this would result in a passed test except for the last
|
||||
================================= FAILURES =================================
|
||||
_____________________________ test_create_file _____________________________
|
||||
|
||||
tmpdir = local('/tmp/pytest-11/test_create_file0')
|
||||
tmpdir = local('/Users/hpk/tmp/pytest-1596/test_create_file0')
|
||||
|
||||
def test_create_file(tmpdir):
|
||||
p = tmpdir.mkdir("sub").join("hello.txt")
|
||||
@@ -47,11 +47,11 @@ Running this would result in a passed test except for the last
|
||||
E assert 0
|
||||
|
||||
test_tmpdir.py:7: AssertionError
|
||||
========================= 1 failed in 0.06 seconds =========================
|
||||
========================= 1 failed in 0.20 seconds =========================
|
||||
|
||||
.. _`base temporary directory`:
|
||||
|
||||
the default base temporary directory
|
||||
The default base temporary directory
|
||||
-----------------------------------------------
|
||||
|
||||
Temporary directories are by default created as sub-directories of
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
|
||||
.. _`unittest.TestCase`:
|
||||
|
||||
unittest.TestCase support
|
||||
Support for unittest.TestCase
|
||||
=====================================================================
|
||||
|
||||
py.test has limited support for running Python `unittest.py style`_ tests.
|
||||
@@ -24,7 +24,7 @@ Running it yields::
|
||||
|
||||
$ py.test test_unittest.py
|
||||
=========================== test session starts ============================
|
||||
platform linux2 -- Python 2.6.6 -- pytest-2.0.3
|
||||
platform darwin -- Python 2.7.1 -- pytest-2.2.0
|
||||
collecting ... collected 1 items
|
||||
|
||||
test_unittest.py F
|
||||
@@ -42,7 +42,7 @@ Running it yields::
|
||||
test_unittest.py:8: AssertionError
|
||||
----------------------------- Captured stdout ------------------------------
|
||||
hello
|
||||
========================= 1 failed in 0.02 seconds =========================
|
||||
========================= 1 failed in 0.23 seconds =========================
|
||||
|
||||
.. _`unittest.py style`: http://docs.python.org/library/unittest.html
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ Usage and Invocations
|
||||
|
||||
.. _cmdline:
|
||||
|
||||
calling pytest through ``python -m pytest``
|
||||
Calling pytest through ``python -m pytest``
|
||||
-----------------------------------------------------
|
||||
|
||||
.. versionadded:: 2.0
|
||||
@@ -38,7 +38,7 @@ To stop the testing process after the first (N) failures::
|
||||
py.test -x # stop after first failure
|
||||
py.test -maxfail=2 # stop after two failures
|
||||
|
||||
specifying tests / selecting tests
|
||||
Specifying tests / selecting tests
|
||||
---------------------------------------------------
|
||||
|
||||
Several test run options::
|
||||
@@ -98,7 +98,19 @@ can use a helper::
|
||||
In previous versions you could only enter PDB tracing if
|
||||
you disable capturing on the command line via ``py.test -s``.
|
||||
|
||||
creating JUnitXML format files
|
||||
.. _durations:
|
||||
|
||||
Profiling test execution duration
|
||||
-------------------------------------
|
||||
|
||||
.. versionadded: 2.2
|
||||
|
||||
To get a list of the slowest 10 test durations::
|
||||
|
||||
py.test --durations=10
|
||||
|
||||
|
||||
Creating JUnitXML format files
|
||||
----------------------------------------------------
|
||||
|
||||
To create result files which can be read by Hudson_ or other Continuous
|
||||
@@ -108,7 +120,7 @@ integration servers, use this invocation::
|
||||
|
||||
to create an XML file at ``path``.
|
||||
|
||||
creating resultlog format files
|
||||
Creating resultlog format files
|
||||
----------------------------------------------------
|
||||
|
||||
To create plain-text machine-readable result files you can issue::
|
||||
@@ -121,7 +133,7 @@ by the `PyPy-test`_ web page to show test results over several revisions.
|
||||
.. _`PyPy-test`: http://codespeak.net:8099/summary
|
||||
|
||||
|
||||
send test report to pocoo pastebin service
|
||||
Sending test report to pocoo pastebin service
|
||||
-----------------------------------------------------
|
||||
|
||||
**Creating a URL for each test failure**::
|
||||
@@ -138,7 +150,7 @@ for example ``-x`` if you only want to send one particular failure.
|
||||
|
||||
Currently only pasting to the http://paste.pocoo.org service is implemented.
|
||||
|
||||
calling pytest from Python code
|
||||
Calling pytest from Python code
|
||||
----------------------------------------------------
|
||||
|
||||
.. versionadded:: 2.0
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
.. _xunitsetup:
|
||||
|
||||
====================================
|
||||
extended xUnit style setup fixtures
|
||||
Extended xUnit style setup fixtures
|
||||
====================================
|
||||
|
||||
.. _`funcargs`: funcargs.html
|
||||
@@ -16,7 +16,7 @@ before running a test function and ``teardown`` after it has finished.
|
||||
handling by optionally calling per-module and per-class hooks.
|
||||
|
||||
|
||||
module level setup/teardown
|
||||
Module level setup/teardown
|
||||
=============================================
|
||||
|
||||
If you have multiple test functions and test classes in a single
|
||||
@@ -33,7 +33,7 @@ which will usually be called once for all the functions::
|
||||
with a setup_module method.
|
||||
"""
|
||||
|
||||
class level setup/teardown
|
||||
Class level setup/teardown
|
||||
=============================================
|
||||
|
||||
Similarly, the following methods are called at class level before
|
||||
@@ -51,7 +51,7 @@ and after all test methods of the class are called::
|
||||
with a call to setup_class.
|
||||
"""
|
||||
|
||||
method and function level setup/teardown
|
||||
Method and function level setup/teardown
|
||||
=============================================
|
||||
|
||||
Similarly, the following methods are called around each method invocation::
|
||||
|
||||
21
setup.py
21
setup.py
@@ -1,36 +1,39 @@
|
||||
import os, sys
|
||||
if sys.version_info >= (3,0):
|
||||
try:
|
||||
from setuptools import setup
|
||||
except ImportError:
|
||||
from distribute_setup import use_setuptools
|
||||
use_setuptools()
|
||||
from setuptools import setup
|
||||
from setuptools import setup
|
||||
|
||||
long_description = """
|
||||
cross-project testing tool for Python.
|
||||
|
||||
Platforms: Linux, Win32, OSX
|
||||
|
||||
Interpreters: Python versions 2.4 through to 3.2, Jython 2.5.1 and PyPy
|
||||
Interpreters: Python versions 2.4 through to 3.2, Jython 2.5.1 and PyPy-1.5
|
||||
|
||||
Bugs and issues: http://bitbucket.org/hpk42/pytest/issues/
|
||||
|
||||
Web page: http://pytest.org
|
||||
|
||||
(c) Holger Krekel and others, 2004-2010
|
||||
(c) Holger Krekel and others, 2004-2011
|
||||
"""
|
||||
def main():
|
||||
setup(
|
||||
name='pytest',
|
||||
description='py.test: simple powerful testing with Python',
|
||||
long_description = long_description,
|
||||
version='2.0.3',
|
||||
version='2.2.0',
|
||||
url='http://pytest.org',
|
||||
license='MIT license',
|
||||
platforms=['unix', 'linux', 'osx', 'cygwin', 'win32'],
|
||||
author='holger krekel, Guido Wesdorp, Carl Friedrich Bolz, Armin Rigo, Maciej Fijalkowski & others',
|
||||
author='Holger Krekel, Benjamin Peterson, Ronny Pfannschmidt, Floris Bruynooghe and others',
|
||||
author_email='holger at merlinux.eu',
|
||||
entry_points= make_entry_points(),
|
||||
install_requires=['py>1.4.1'],
|
||||
classifiers=['Development Status :: 5 - Production/Stable',
|
||||
# the following should be enabled for release
|
||||
install_requires=['py>=1.4.5'],
|
||||
classifiers=['Development Status :: 6 - Mature',
|
||||
'Intended Audience :: Developers',
|
||||
'License :: OSI Approved :: MIT License',
|
||||
'Operating System :: POSIX',
|
||||
@@ -41,7 +44,7 @@ def main():
|
||||
'Topic :: Utilities',
|
||||
'Programming Language :: Python',
|
||||
'Programming Language :: Python :: 3'],
|
||||
packages=['_pytest', ],
|
||||
packages=['_pytest', '_pytest.assertion'],
|
||||
py_modules=['pytest'],
|
||||
zip_safe=False,
|
||||
)
|
||||
|
||||
@@ -13,6 +13,12 @@ class TestGeneralUsage:
|
||||
'*ERROR: hello'
|
||||
])
|
||||
|
||||
def test_root_conftest_syntax_error(self, testdir):
|
||||
p = testdir.makepyfile(conftest="raise SyntaxError\n")
|
||||
result = testdir.runpytest()
|
||||
result.stderr.fnmatch_lines(["*raise SyntaxError*"])
|
||||
assert result.ret != 0
|
||||
|
||||
def test_early_hook_error_issue38_1(self, testdir):
|
||||
testdir.makeconftest("""
|
||||
def pytest_sessionstart():
|
||||
@@ -89,7 +95,7 @@ class TestGeneralUsage:
|
||||
result = testdir.runpytest(p)
|
||||
result.stdout.fnmatch_lines([
|
||||
#XXX on jython this fails: "> import import_fails",
|
||||
"E ImportError: No module named does_not_work",
|
||||
"E ImportError: No module named *does_not_work*",
|
||||
])
|
||||
assert result.ret == 1
|
||||
|
||||
@@ -155,7 +161,7 @@ class TestGeneralUsage:
|
||||
assert result.ret != 0
|
||||
assert "should be seen" in result.stdout.str()
|
||||
|
||||
@pytest.mark.skipif("not hasattr(os, 'symlink')")
|
||||
@pytest.mark.skipif("not hasattr(py.path.local, 'mksymlinkto')")
|
||||
def test_chdir(self, testdir):
|
||||
testdir.tmpdir.join("py").mksymlinkto(py._pydir)
|
||||
p = testdir.tmpdir.join("main.py")
|
||||
@@ -231,7 +237,7 @@ class TestGeneralUsage:
|
||||
res = testdir.runpytest(p)
|
||||
assert res.ret == 0
|
||||
res.stdout.fnmatch_lines(["*1 skipped*"])
|
||||
|
||||
|
||||
def test_direct_addressing_selects(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
def pytest_generate_tests(metafunc):
|
||||
@@ -259,6 +265,19 @@ class TestGeneralUsage:
|
||||
if name.startswith("pytest_"):
|
||||
assert value.__doc__, "no docstring for %s" % name
|
||||
|
||||
def test_initialization_error_issue49(self, testdir):
|
||||
testdir.makeconftest("""
|
||||
def pytest_configure():
|
||||
x
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
assert result.ret == 3 # internal error
|
||||
result.stderr.fnmatch_lines([
|
||||
"INTERNAL*pytest_configure*",
|
||||
"INTERNAL*x*",
|
||||
])
|
||||
assert 'sessionstarttime' not in result.stderr.str()
|
||||
|
||||
class TestInvocationVariants:
|
||||
def test_earlyinit(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
@@ -268,6 +287,7 @@ class TestInvocationVariants:
|
||||
result = testdir.runpython(p)
|
||||
assert result.ret == 0
|
||||
|
||||
@pytest.mark.xfail("sys.platform.startswith('java')")
|
||||
def test_pydoc(self, testdir):
|
||||
for name in ('py.test', 'pytest'):
|
||||
result = testdir.runpython_c("import %s;help(%s)" % (name, name))
|
||||
@@ -340,27 +360,40 @@ class TestInvocationVariants:
|
||||
def test_equivalence_pytest_pytest(self):
|
||||
assert pytest.main == py.test.cmdline.main
|
||||
|
||||
def test_invoke_with_string(self, capsys):
|
||||
retcode = pytest.main("-h")
|
||||
def test_invoke_with_string(self, testdir, capsys):
|
||||
retcode = testdir.pytestmain("-h")
|
||||
assert not retcode
|
||||
out, err = capsys.readouterr()
|
||||
assert "--help" in out
|
||||
pytest.raises(ValueError, lambda: pytest.main(retcode))
|
||||
pytest.raises(ValueError, lambda: pytest.main(0))
|
||||
|
||||
def test_invoke_with_path(self, testdir, capsys):
|
||||
retcode = testdir.pytestmain(testdir.tmpdir)
|
||||
assert not retcode
|
||||
out, err = capsys.readouterr()
|
||||
|
||||
def test_invoke_plugin_api(self, capsys):
|
||||
|
||||
def test_invoke_plugin_api(self, testdir, capsys):
|
||||
class MyPlugin:
|
||||
def pytest_addoption(self, parser):
|
||||
parser.addoption("--myopt")
|
||||
|
||||
pytest.main(["-h"], plugins=[MyPlugin()])
|
||||
testdir.pytestmain(["-h"], plugins=[MyPlugin()])
|
||||
out, err = capsys.readouterr()
|
||||
assert "--myopt" in out
|
||||
|
||||
def test_pyargs_importerror(self, testdir, monkeypatch):
|
||||
monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', False)
|
||||
path = testdir.mkpydir("tpkg")
|
||||
path.join("test_hello.py").write('raise ImportError')
|
||||
|
||||
result = testdir.runpytest("--pyargs", "tpkg.test_hello")
|
||||
assert result.ret != 0
|
||||
# FIXME: It would be more natural to match NOT
|
||||
# "ERROR*file*or*package*not*found*".
|
||||
result.stdout.fnmatch_lines([
|
||||
"*collected 0 items*"
|
||||
])
|
||||
|
||||
def test_cmdline_python_package(self, testdir, monkeypatch):
|
||||
monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', False)
|
||||
path = testdir.mkpydir("tpkg")
|
||||
@@ -376,16 +409,21 @@ class TestInvocationVariants:
|
||||
result.stdout.fnmatch_lines([
|
||||
"*1 passed*"
|
||||
])
|
||||
|
||||
empty_package = testdir.mkpydir("empty_package")
|
||||
monkeypatch.setenv('PYTHONPATH', empty_package)
|
||||
result = testdir.runpytest("--pyargs", ".")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines([
|
||||
"*2 passed*"
|
||||
])
|
||||
|
||||
monkeypatch.setenv('PYTHONPATH', testdir)
|
||||
path.join('test_hello.py').remove()
|
||||
result = testdir.runpytest("--pyargs", "tpkg.test_hello")
|
||||
assert result.ret != 0
|
||||
result.stderr.fnmatch_lines([
|
||||
"*file*not*found*test_hello*",
|
||||
"*not*found*test_hello*",
|
||||
])
|
||||
|
||||
def test_cmdline_python_package_not_exists(self, testdir):
|
||||
@@ -427,3 +465,92 @@ class TestInvocationVariants:
|
||||
"*1 failed*",
|
||||
])
|
||||
|
||||
class TestDurations:
|
||||
source = """
|
||||
import time
|
||||
frag = 0.02
|
||||
def test_2():
|
||||
time.sleep(frag*2)
|
||||
def test_1():
|
||||
time.sleep(frag)
|
||||
def test_3():
|
||||
time.sleep(frag*3)
|
||||
"""
|
||||
|
||||
def test_calls(self, testdir):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=10")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines([
|
||||
"*durations*",
|
||||
"*call*test_3*",
|
||||
"*call*test_2*",
|
||||
"*call*test_1*",
|
||||
])
|
||||
|
||||
def test_calls_show_2(self, testdir):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=2")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines([
|
||||
"*durations*",
|
||||
"*call*test_3*",
|
||||
"*call*test_2*",
|
||||
])
|
||||
assert "test_1" not in result.stdout.str()
|
||||
|
||||
def test_calls_showall(self, testdir):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=0")
|
||||
assert result.ret == 0
|
||||
for x in "123":
|
||||
for y in 'call',: #'setup', 'call', 'teardown':
|
||||
l = []
|
||||
for line in result.stdout.lines:
|
||||
if ("test_%s" % x) in line and y in line:
|
||||
break
|
||||
else:
|
||||
raise AssertionError("not found %s %s" % (x,y))
|
||||
|
||||
def test_with_deselected(self, testdir):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=2", "-k test_1")
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines([
|
||||
"*durations*",
|
||||
"*call*test_1*",
|
||||
])
|
||||
|
||||
def test_with_failing_collection(self, testdir):
|
||||
testdir.makepyfile(self.source)
|
||||
testdir.makepyfile(test_collecterror="""xyz""")
|
||||
result = testdir.runpytest("--durations=2", "-k test_1")
|
||||
assert result.ret != 0
|
||||
result.stdout.fnmatch_lines([
|
||||
"*durations*",
|
||||
"*call*test_1*",
|
||||
])
|
||||
|
||||
|
||||
class TestDurationWithFixture:
|
||||
source = """
|
||||
import time
|
||||
frag = 0.01
|
||||
def setup_function(func):
|
||||
time.sleep(frag * 3)
|
||||
def test_1():
|
||||
time.sleep(frag*2)
|
||||
def test_2():
|
||||
time.sleep(frag)
|
||||
"""
|
||||
def test_setup_function(self, testdir):
|
||||
testdir.makepyfile(self.source)
|
||||
result = testdir.runpytest("--durations=10")
|
||||
assert result.ret == 0
|
||||
|
||||
result.stdout.fnmatch_lines([
|
||||
"*durations*",
|
||||
"* setup *test_1*",
|
||||
"* call *test_1*",
|
||||
])
|
||||
|
||||
|
||||
@@ -12,13 +12,17 @@ def pytest_addoption(parser):
|
||||
help=("run FD checks if lsof is available"))
|
||||
|
||||
def pytest_configure(config):
|
||||
config.addinivalue_line("markers",
|
||||
"multi(arg=[value1,value2, ...]): call the test function "
|
||||
"multiple times with arg=value1, then with arg=value2, ... "
|
||||
)
|
||||
if config.getvalue("lsof"):
|
||||
try:
|
||||
out = py.process.cmdexec("lsof -p %d" % pid)
|
||||
except py.process.cmdexec.Error:
|
||||
pass
|
||||
else:
|
||||
config._numfiles = getopenfiles(out)
|
||||
config._numfiles = len(getopenfiles(out))
|
||||
|
||||
#def pytest_report_header():
|
||||
# return "pid: %s" % os.getpid()
|
||||
@@ -26,23 +30,31 @@ def pytest_configure(config):
|
||||
def getopenfiles(out):
|
||||
def isopen(line):
|
||||
return ("REG" in line or "CHR" in line) and (
|
||||
"deleted" not in line and 'mem' not in line)
|
||||
return len([x for x in out.split("\n") if isopen(x)])
|
||||
"deleted" not in line and 'mem' not in line and "txt" not in line)
|
||||
return [x for x in out.split("\n") if isopen(x)]
|
||||
|
||||
def pytest_unconfigure(config, __multicall__):
|
||||
if not hasattr(config, '_numfiles'):
|
||||
return
|
||||
__multicall__.execute()
|
||||
def check_open_files(config):
|
||||
out2 = py.process.cmdexec("lsof -p %d" % pid)
|
||||
len2 = getopenfiles(out2)
|
||||
assert len2 < config._numfiles + 7, out2
|
||||
|
||||
lines2 = getopenfiles(out2)
|
||||
if len(lines2) > config._numfiles + 1:
|
||||
error = []
|
||||
error.append("***** %s FD leackage detected" %
|
||||
(len(lines2)-config._numfiles))
|
||||
error.extend(lines2)
|
||||
error.append(error[0])
|
||||
# update numfile so that the overall test run continuess
|
||||
config._numfiles = len(lines2)
|
||||
raise AssertionError("\n".join(error))
|
||||
|
||||
def pytest_runtest_setup(item):
|
||||
item._oldir = py.path.local()
|
||||
|
||||
def pytest_runtest_teardown(item):
|
||||
def pytest_runtest_teardown(item, __multicall__):
|
||||
item._oldir.chdir()
|
||||
if hasattr(item.config, '_numfiles'):
|
||||
x = __multicall__.execute()
|
||||
check_open_files(item.config)
|
||||
return x
|
||||
|
||||
def pytest_generate_tests(metafunc):
|
||||
multi = getattr(metafunc.function, 'multi', None)
|
||||
@@ -53,7 +65,7 @@ def pytest_generate_tests(metafunc):
|
||||
metafunc.addcall(funcargs={name: val})
|
||||
elif 'anypython' in metafunc.funcargnames:
|
||||
for name in ('python2.4', 'python2.5', 'python2.6',
|
||||
'python2.7', 'python3.1', 'pypy-c', 'jython'):
|
||||
'python2.7', 'python3.1', 'pypy', 'jython'):
|
||||
metafunc.addcall(id=name, param=name)
|
||||
|
||||
# XXX copied from execnet's conftest.py - needs to be merged
|
||||
@@ -78,6 +90,8 @@ def getexecutable(name, cache={}):
|
||||
out, err = popen.communicate()
|
||||
if not err or "2.5" not in err:
|
||||
executable = None
|
||||
if "2.5.2" in err:
|
||||
executable = None # http://bugs.jython.org/issue1790
|
||||
cache[name] = executable
|
||||
return executable
|
||||
|
||||
@@ -91,5 +105,5 @@ def pytest_funcarg__anypython(request):
|
||||
executable = py.path.local(executable)
|
||||
if executable.check():
|
||||
return executable
|
||||
pytest.skip("no %s found" % (name,))
|
||||
pytest.skip("no suitable %s found" % (name,))
|
||||
return executable
|
||||
|
||||
327
testing/test_assertinterpret.py
Normal file
327
testing/test_assertinterpret.py
Normal file
@@ -0,0 +1,327 @@
|
||||
"PYTEST_DONT_REWRITE"
|
||||
import pytest, py
|
||||
|
||||
from _pytest.assertion import util
|
||||
|
||||
def exvalue():
|
||||
return py.std.sys.exc_info()[1]
|
||||
|
||||
def f():
|
||||
return 2
|
||||
|
||||
def test_not_being_rewritten():
|
||||
assert "@py_builtins" not in globals()
|
||||
|
||||
def test_assert():
|
||||
try:
|
||||
assert f() == 3
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith('assert 2 == 3\n')
|
||||
|
||||
def test_assert_with_explicit_message():
|
||||
try:
|
||||
assert f() == 3, "hello"
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert e.msg == 'hello'
|
||||
|
||||
def test_assert_within_finally():
|
||||
class A:
|
||||
def f():
|
||||
pass
|
||||
excinfo = py.test.raises(TypeError, """
|
||||
try:
|
||||
A().f()
|
||||
finally:
|
||||
i = 42
|
||||
""")
|
||||
s = excinfo.exconly()
|
||||
assert s.find("takes no argument") != -1
|
||||
|
||||
#def g():
|
||||
# A.f()
|
||||
#excinfo = getexcinfo(TypeError, g)
|
||||
#msg = getmsg(excinfo)
|
||||
#assert msg.find("must be called with A") != -1
|
||||
|
||||
|
||||
def test_assert_multiline_1():
|
||||
try:
|
||||
assert (f() ==
|
||||
3)
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith('assert 2 == 3\n')
|
||||
|
||||
def test_assert_multiline_2():
|
||||
try:
|
||||
assert (f() == (4,
|
||||
3)[-1])
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith('assert 2 ==')
|
||||
|
||||
def test_in():
|
||||
try:
|
||||
assert "hi" in [1, 2]
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith("assert 'hi' in")
|
||||
|
||||
def test_is():
|
||||
try:
|
||||
assert 1 is 2
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith("assert 1 is 2")
|
||||
|
||||
|
||||
@py.test.mark.skipif("sys.version_info < (2,6)")
|
||||
def test_attrib():
|
||||
class Foo(object):
|
||||
b = 1
|
||||
i = Foo()
|
||||
try:
|
||||
assert i.b == 2
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith("assert 1 == 2")
|
||||
|
||||
@py.test.mark.skipif("sys.version_info < (2,6)")
|
||||
def test_attrib_inst():
|
||||
class Foo(object):
|
||||
b = 1
|
||||
try:
|
||||
assert Foo().b == 2
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith("assert 1 == 2")
|
||||
|
||||
def test_len():
|
||||
l = list(range(42))
|
||||
try:
|
||||
assert len(l) == 100
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert s.startswith("assert 42 == 100")
|
||||
assert "where 42 = len([" in s
|
||||
|
||||
def test_assert_non_string_message():
|
||||
class A:
|
||||
def __str__(self):
|
||||
return "hello"
|
||||
try:
|
||||
assert 0 == 1, A()
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert e.msg == "hello"
|
||||
|
||||
def test_assert_keyword_arg():
|
||||
def f(x=3):
|
||||
return False
|
||||
try:
|
||||
assert f(x=5)
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert "x=5" in e.msg
|
||||
|
||||
# These tests should both fail, but should fail nicely...
|
||||
class WeirdRepr:
|
||||
def __repr__(self):
|
||||
return '<WeirdRepr\nsecond line>'
|
||||
|
||||
def bug_test_assert_repr():
|
||||
v = WeirdRepr()
|
||||
try:
|
||||
assert v == 1
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert e.msg.find('WeirdRepr') != -1
|
||||
assert e.msg.find('second line') != -1
|
||||
assert 0
|
||||
|
||||
def test_assert_non_string():
|
||||
try:
|
||||
assert 0, ['list']
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert e.msg.find("list") != -1
|
||||
|
||||
def test_assert_implicit_multiline():
|
||||
try:
|
||||
x = [1,2,3]
|
||||
assert x != [1,
|
||||
2, 3]
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert e.msg.find('assert [1, 2, 3] !=') != -1
|
||||
|
||||
|
||||
def test_assert_with_brokenrepr_arg():
|
||||
class BrokenRepr:
|
||||
def __repr__(self): 0 / 0
|
||||
e = AssertionError(BrokenRepr())
|
||||
if e.msg.find("broken __repr__") == -1:
|
||||
py.test.fail("broken __repr__ not handle correctly")
|
||||
|
||||
def test_multiple_statements_per_line():
|
||||
try:
|
||||
a = 1; assert a == 2
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert "assert 1 == 2" in e.msg
|
||||
|
||||
def test_power():
|
||||
try:
|
||||
assert 2**3 == 7
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
assert "assert (2 ** 3) == 7" in e.msg
|
||||
|
||||
|
||||
class TestView:
|
||||
|
||||
def setup_class(cls):
|
||||
cls.View = pytest.importorskip("_pytest.assertion.oldinterpret").View
|
||||
|
||||
def test_class_dispatch(self):
|
||||
### Use a custom class hierarchy with existing instances
|
||||
|
||||
class Picklable(self.View):
|
||||
pass
|
||||
|
||||
class Simple(Picklable):
|
||||
__view__ = object
|
||||
def pickle(self):
|
||||
return repr(self.__obj__)
|
||||
|
||||
class Seq(Picklable):
|
||||
__view__ = list, tuple, dict
|
||||
def pickle(self):
|
||||
return ';'.join(
|
||||
[Picklable(item).pickle() for item in self.__obj__])
|
||||
|
||||
class Dict(Seq):
|
||||
__view__ = dict
|
||||
def pickle(self):
|
||||
return Seq.pickle(self) + '!' + Seq(self.values()).pickle()
|
||||
|
||||
assert Picklable(123).pickle() == '123'
|
||||
assert Picklable([1,[2,3],4]).pickle() == '1;2;3;4'
|
||||
assert Picklable({1:2}).pickle() == '1!2'
|
||||
|
||||
def test_viewtype_class_hierarchy(self):
|
||||
# Use a custom class hierarchy based on attributes of existing instances
|
||||
class Operation:
|
||||
"Existing class that I don't want to change."
|
||||
def __init__(self, opname, *args):
|
||||
self.opname = opname
|
||||
self.args = args
|
||||
|
||||
existing = [Operation('+', 4, 5),
|
||||
Operation('getitem', '', 'join'),
|
||||
Operation('setattr', 'x', 'y', 3),
|
||||
Operation('-', 12, 1)]
|
||||
|
||||
class PyOp(self.View):
|
||||
def __viewkey__(self):
|
||||
return self.opname
|
||||
def generate(self):
|
||||
return '%s(%s)' % (self.opname, ', '.join(map(repr, self.args)))
|
||||
|
||||
class PyBinaryOp(PyOp):
|
||||
__view__ = ('+', '-', '*', '/')
|
||||
def generate(self):
|
||||
return '%s %s %s' % (self.args[0], self.opname, self.args[1])
|
||||
|
||||
codelines = [PyOp(op).generate() for op in existing]
|
||||
assert codelines == ["4 + 5", "getitem('', 'join')",
|
||||
"setattr('x', 'y', 3)", "12 - 1"]
|
||||
|
||||
@py.test.mark.skipif("sys.version_info < (2,6)")
|
||||
def test_assert_customizable_reprcompare(monkeypatch):
|
||||
monkeypatch.setattr(util, '_reprcompare', lambda *args: 'hello')
|
||||
try:
|
||||
assert 3 == 4
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert "hello" in s
|
||||
|
||||
def test_assert_long_source_1():
|
||||
try:
|
||||
assert len == [
|
||||
(None, ['somet text', 'more text']),
|
||||
]
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert 're-run' not in s
|
||||
assert 'somet text' in s
|
||||
|
||||
def test_assert_long_source_2():
|
||||
try:
|
||||
assert(len == [
|
||||
(None, ['somet text', 'more text']),
|
||||
])
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert 're-run' not in s
|
||||
assert 'somet text' in s
|
||||
|
||||
def test_assert_raise_alias(testdir):
|
||||
testdir.makepyfile("""
|
||||
"PYTEST_DONT_REWRITE"
|
||||
import sys
|
||||
EX = AssertionError
|
||||
def test_hello():
|
||||
raise EX("hello"
|
||||
"multi"
|
||||
"line")
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
"*def test_hello*",
|
||||
"*raise EX*",
|
||||
"*1 failed*",
|
||||
])
|
||||
|
||||
|
||||
@pytest.mark.skipif("sys.version_info < (2,5)")
|
||||
def test_assert_raise_subclass():
|
||||
class SomeEx(AssertionError):
|
||||
def __init__(self, *args):
|
||||
super(SomeEx, self).__init__()
|
||||
try:
|
||||
raise SomeEx("hello")
|
||||
except AssertionError:
|
||||
s = str(exvalue())
|
||||
assert 're-run' not in s
|
||||
assert 'could not determine' in s
|
||||
|
||||
def test_assert_raises_in_nonzero_of_object_pytest_issue10():
|
||||
class A(object):
|
||||
def __nonzero__(self):
|
||||
raise ValueError(42)
|
||||
def __lt__(self, other):
|
||||
return A()
|
||||
def __repr__(self):
|
||||
return "<MY42 object>"
|
||||
def myany(x):
|
||||
return True
|
||||
try:
|
||||
assert not(myany(A() < 0))
|
||||
except AssertionError:
|
||||
e = exvalue()
|
||||
s = str(e)
|
||||
assert "<MY42 object> < 0" in s
|
||||
@@ -2,11 +2,12 @@ import sys
|
||||
|
||||
import py, pytest
|
||||
import _pytest.assertion as plugin
|
||||
from _pytest.assertion import reinterpret, util
|
||||
|
||||
needsnewassert = pytest.mark.skipif("sys.version_info < (2,6)")
|
||||
|
||||
def interpret(expr):
|
||||
return py.code._reinterpret(expr, py.code.Frame(sys._getframe(1)))
|
||||
return reinterpret.reinterpret(expr, py.code.Frame(sys._getframe(1)))
|
||||
|
||||
class TestBinReprIntegration:
|
||||
pytestmark = needsnewassert
|
||||
@@ -25,7 +26,7 @@ class TestBinReprIntegration:
|
||||
self.right = right
|
||||
mockhook = MockHook()
|
||||
monkeypatch = request.getfuncargvalue("monkeypatch")
|
||||
monkeypatch.setattr(py.code, '_reprcompare', mockhook)
|
||||
monkeypatch.setattr(util, '_reprcompare', mockhook)
|
||||
return mockhook
|
||||
|
||||
def test_pytest_assertrepr_compare_called(self, hook):
|
||||
@@ -39,15 +40,6 @@ class TestBinReprIntegration:
|
||||
assert hook.left == [0, 1]
|
||||
assert hook.right == [0, 2]
|
||||
|
||||
def test_configure_unconfigure(self, testdir, hook):
|
||||
assert hook == py.code._reprcompare
|
||||
config = testdir.parseconfig()
|
||||
plugin.pytest_configure(config)
|
||||
assert hook != py.code._reprcompare
|
||||
from _pytest.config import pytest_unconfigure
|
||||
pytest_unconfigure(config)
|
||||
assert hook == py.code._reprcompare
|
||||
|
||||
def callequal(left, right):
|
||||
return plugin.pytest_assertrepr_compare('==', left, right)
|
||||
|
||||
@@ -119,6 +111,14 @@ class TestAssert_reprcompare:
|
||||
expl = ' '.join(callequal('foo', 'bar'))
|
||||
assert 'raised in repr()' not in expl
|
||||
|
||||
@needsnewassert
|
||||
def test_rewritten(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_rewritten():
|
||||
assert "@py_builtins" in globals()
|
||||
""")
|
||||
assert testdir.runpytest().ret == 0
|
||||
|
||||
def test_reprcompare_notin():
|
||||
detail = plugin.pytest_assertrepr_compare('not in', 'foo', 'aaafoobbb')[1:]
|
||||
assert detail == ["'foo' is contained here:", ' aaafoobbb', '? +++']
|
||||
@@ -158,8 +158,30 @@ def test_sequence_comparison_uses_repr(testdir):
|
||||
"*E*'y'*",
|
||||
])
|
||||
|
||||
@needsnewassert
|
||||
def test_assertrepr_loaded_per_dir(testdir):
|
||||
testdir.makepyfile(test_base=['def test_base(): assert 1 == 2'])
|
||||
a = testdir.mkdir('a')
|
||||
a_test = a.join('test_a.py')
|
||||
a_test.write('def test_a(): assert 1 == 2')
|
||||
a_conftest = a.join('conftest.py')
|
||||
a_conftest.write('def pytest_assertrepr_compare(): return ["summary a"]')
|
||||
b = testdir.mkdir('b')
|
||||
b_test = b.join('test_b.py')
|
||||
b_test.write('def test_b(): assert 1 == 2')
|
||||
b_conftest = b.join('conftest.py')
|
||||
b_conftest.write('def pytest_assertrepr_compare(): return ["summary b"]')
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
'*def test_base():*',
|
||||
'*E*assert 1 == 2*',
|
||||
'*def test_a():*',
|
||||
'*E*assert summary a*',
|
||||
'*def test_b():*',
|
||||
'*E*assert summary b*'])
|
||||
|
||||
def test_functional(testdir):
|
||||
|
||||
def test_assertion_options(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_hello():
|
||||
x = 3
|
||||
@@ -167,8 +189,24 @@ def test_functional(testdir):
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
assert "3 == 4" in result.stdout.str()
|
||||
result = testdir.runpytest("--no-assert")
|
||||
assert "3 == 4" not in result.stdout.str()
|
||||
off_options = (("--no-assert",),
|
||||
("--nomagic",),
|
||||
("--no-assert", "--nomagic"),
|
||||
("--assert=plain",),
|
||||
("--assert=plain", "--no-assert"),
|
||||
("--assert=plain", "--nomagic"),
|
||||
("--assert=plain", "--no-assert", "--nomagic"))
|
||||
for opt in off_options:
|
||||
result = testdir.runpytest(*opt)
|
||||
assert "3 == 4" not in result.stdout.str()
|
||||
|
||||
def test_old_assert_mode(testdir):
|
||||
testdir.makepyfile("""
|
||||
def test_in_old_mode():
|
||||
assert "@py_builtins" not in globals()
|
||||
""")
|
||||
result = testdir.runpytest("--assert=reinterp")
|
||||
assert result.ret == 0
|
||||
|
||||
def test_triple_quoted_string_issue113(testdir):
|
||||
testdir.makepyfile("""
|
||||
@@ -210,14 +248,14 @@ def test_traceback_failure(testdir):
|
||||
"*test_traceback_failure.py:4: AssertionError"
|
||||
])
|
||||
|
||||
@pytest.mark.skipif("sys.version_info < (2,5) or '__pypy__' in sys.builtin_module_names")
|
||||
@pytest.mark.skipif("sys.version_info < (2,5) or '__pypy__' in sys.builtin_module_names or sys.platform.startswith('java')" )
|
||||
def test_warn_missing(testdir):
|
||||
p1 = testdir.makepyfile("")
|
||||
result = testdir.run(sys.executable, "-OO", "-m", "pytest", "-h")
|
||||
result.stderr.fnmatch_lines([
|
||||
"*WARNING*assertion*",
|
||||
"*WARNING*assert statements are not executed*",
|
||||
])
|
||||
result = testdir.run(sys.executable, "-OO", "-m", "pytest", "--no-assert")
|
||||
result.stderr.fnmatch_lines([
|
||||
"*WARNING*assertion*",
|
||||
"*WARNING*assert statements are not executed*",
|
||||
])
|
||||
|
||||
376
testing/test_assertrewrite.py
Normal file
376
testing/test_assertrewrite.py
Normal file
@@ -0,0 +1,376 @@
|
||||
import sys
|
||||
import zipfile
|
||||
import py
|
||||
import pytest
|
||||
|
||||
ast = pytest.importorskip("ast")
|
||||
if sys.platform.startswith("java"):
|
||||
# XXX should be xfail
|
||||
pytest.skip("assert rewrite does currently not work on jython")
|
||||
|
||||
from _pytest.assertion import util
|
||||
from _pytest.assertion.rewrite import rewrite_asserts
|
||||
|
||||
|
||||
def setup_module(mod):
|
||||
mod._old_reprcompare = util._reprcompare
|
||||
py.code._reprcompare = None
|
||||
|
||||
def teardown_module(mod):
|
||||
util._reprcompare = mod._old_reprcompare
|
||||
del mod._old_reprcompare
|
||||
|
||||
|
||||
def rewrite(src):
|
||||
tree = ast.parse(src)
|
||||
rewrite_asserts(tree)
|
||||
return tree
|
||||
|
||||
def getmsg(f, extra_ns=None, must_pass=False):
|
||||
"""Rewrite the assertions in f, run it, and get the failure message."""
|
||||
src = '\n'.join(py.code.Code(f).source().lines)
|
||||
mod = rewrite(src)
|
||||
code = compile(mod, "<test>", "exec")
|
||||
ns = {}
|
||||
if extra_ns is not None:
|
||||
ns.update(extra_ns)
|
||||
py.builtin.exec_(code, ns)
|
||||
func = ns[f.__name__]
|
||||
try:
|
||||
func()
|
||||
except AssertionError:
|
||||
if must_pass:
|
||||
pytest.fail("shouldn't have raised")
|
||||
s = str(sys.exc_info()[1])
|
||||
if not s.startswith("assert"):
|
||||
return "AssertionError: " + s
|
||||
return s
|
||||
else:
|
||||
if not must_pass:
|
||||
pytest.fail("function didn't raise at all")
|
||||
|
||||
|
||||
class TestAssertionRewrite:
|
||||
|
||||
def test_place_initial_imports(self):
|
||||
s = """'Doc string'\nother = stuff"""
|
||||
m = rewrite(s)
|
||||
assert isinstance(m.body[0], ast.Expr)
|
||||
assert isinstance(m.body[0].value, ast.Str)
|
||||
for imp in m.body[1:3]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 2
|
||||
assert imp.col_offset == 0
|
||||
assert isinstance(m.body[3], ast.Assign)
|
||||
s = """from __future__ import with_statement\nother_stuff"""
|
||||
m = rewrite(s)
|
||||
assert isinstance(m.body[0], ast.ImportFrom)
|
||||
for imp in m.body[1:3]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 2
|
||||
assert imp.col_offset == 0
|
||||
assert isinstance(m.body[3], ast.Expr)
|
||||
s = """'doc string'\nfrom __future__ import with_statement\nother"""
|
||||
m = rewrite(s)
|
||||
assert isinstance(m.body[0], ast.Expr)
|
||||
assert isinstance(m.body[0].value, ast.Str)
|
||||
assert isinstance(m.body[1], ast.ImportFrom)
|
||||
for imp in m.body[2:4]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 3
|
||||
assert imp.col_offset == 0
|
||||
assert isinstance(m.body[4], ast.Expr)
|
||||
s = """from . import relative\nother_stuff"""
|
||||
m = rewrite(s)
|
||||
for imp in m.body[0:2]:
|
||||
assert isinstance(imp, ast.Import)
|
||||
assert imp.lineno == 1
|
||||
assert imp.col_offset == 0
|
||||
assert isinstance(m.body[3], ast.Expr)
|
||||
|
||||
def test_dont_rewrite(self):
|
||||
s = """'PYTEST_DONT_REWRITE'\nassert 14"""
|
||||
m = rewrite(s)
|
||||
assert len(m.body) == 2
|
||||
assert isinstance(m.body[0].value, ast.Str)
|
||||
assert isinstance(m.body[1], ast.Assert)
|
||||
assert m.body[1].msg is None
|
||||
|
||||
def test_name(self):
|
||||
def f():
|
||||
assert False
|
||||
assert getmsg(f) == "assert False"
|
||||
def f():
|
||||
f = False
|
||||
assert f
|
||||
assert getmsg(f) == "assert False"
|
||||
def f():
|
||||
assert a_global
|
||||
assert getmsg(f, {"a_global" : False}) == "assert a_global"
|
||||
|
||||
def test_assert_already_has_message(self):
|
||||
def f():
|
||||
assert False, "something bad!"
|
||||
assert getmsg(f) == "AssertionError: something bad!"
|
||||
|
||||
def test_boolop(self):
|
||||
def f():
|
||||
f = g = False
|
||||
assert f and g
|
||||
assert getmsg(f) == "assert (False)"
|
||||
def f():
|
||||
f = True
|
||||
g = False
|
||||
assert f and g
|
||||
assert getmsg(f) == "assert (True and False)"
|
||||
def f():
|
||||
f = False
|
||||
g = True
|
||||
assert f and g
|
||||
assert getmsg(f) == "assert (False)"
|
||||
def f():
|
||||
f = g = False
|
||||
assert f or g
|
||||
assert getmsg(f) == "assert (False or False)"
|
||||
def f():
|
||||
f = g = False
|
||||
assert not f and not g
|
||||
getmsg(f, must_pass=True)
|
||||
def x():
|
||||
return False
|
||||
def f():
|
||||
assert x() and x()
|
||||
assert getmsg(f, {"x" : x}) == "assert (x())"
|
||||
def f():
|
||||
assert False or x()
|
||||
assert getmsg(f, {"x" : x}) == "assert (False or x())"
|
||||
def f():
|
||||
assert 1 in {} and 2 in {}
|
||||
assert getmsg(f) == "assert (1 in {})"
|
||||
def f():
|
||||
x = 1
|
||||
y = 2
|
||||
assert x in {1 : None} and y in {}
|
||||
assert getmsg(f) == "assert (1 in {1: None} and 2 in {})"
|
||||
def f():
|
||||
f = True
|
||||
g = False
|
||||
assert f or g
|
||||
getmsg(f, must_pass=True)
|
||||
def f():
|
||||
f = g = h = lambda: True
|
||||
assert f() and g() and h()
|
||||
getmsg(f, must_pass=True)
|
||||
|
||||
def test_short_circut_evaluation(self):
|
||||
def f():
|
||||
assert True or explode
|
||||
getmsg(f, must_pass=True)
|
||||
def f():
|
||||
x = 1
|
||||
assert x == 1 or x == 2
|
||||
getmsg(f, must_pass=True)
|
||||
|
||||
def test_unary_op(self):
|
||||
def f():
|
||||
x = True
|
||||
assert not x
|
||||
assert getmsg(f) == "assert not True"
|
||||
def f():
|
||||
x = 0
|
||||
assert ~x + 1
|
||||
assert getmsg(f) == "assert (~0 + 1)"
|
||||
def f():
|
||||
x = 3
|
||||
assert -x + x
|
||||
assert getmsg(f) == "assert (-3 + 3)"
|
||||
def f():
|
||||
x = 0
|
||||
assert +x + x
|
||||
assert getmsg(f) == "assert (+0 + 0)"
|
||||
|
||||
def test_binary_op(self):
|
||||
def f():
|
||||
x = 1
|
||||
y = -1
|
||||
assert x + y
|
||||
assert getmsg(f) == "assert (1 + -1)"
|
||||
|
||||
def test_call(self):
|
||||
def g(a=42, *args, **kwargs):
|
||||
return False
|
||||
ns = {"g" : g}
|
||||
def f():
|
||||
assert g()
|
||||
assert getmsg(f, ns) == """assert g()"""
|
||||
def f():
|
||||
assert g(1)
|
||||
assert getmsg(f, ns) == """assert g(1)"""
|
||||
def f():
|
||||
assert g(1, 2)
|
||||
assert getmsg(f, ns) == """assert g(1, 2)"""
|
||||
def f():
|
||||
assert g(1, g=42)
|
||||
assert getmsg(f, ns) == """assert g(1, g=42)"""
|
||||
def f():
|
||||
assert g(1, 3, g=23)
|
||||
assert getmsg(f, ns) == """assert g(1, 3, g=23)"""
|
||||
def f():
|
||||
seq = [1, 2, 3]
|
||||
assert g(*seq)
|
||||
assert getmsg(f, ns) == """assert g(*[1, 2, 3])"""
|
||||
def f():
|
||||
x = "a"
|
||||
assert g(**{x : 2})
|
||||
assert getmsg(f, ns) == """assert g(**{'a': 2})"""
|
||||
|
||||
def test_attribute(self):
|
||||
class X(object):
|
||||
g = 3
|
||||
ns = {"X" : X, "x" : X()}
|
||||
def f():
|
||||
assert not x.g
|
||||
assert getmsg(f, ns) == """assert not 3
|
||||
+ where 3 = x.g"""
|
||||
def f():
|
||||
x.a = False
|
||||
assert x.a
|
||||
assert getmsg(f, ns) == """assert x.a"""
|
||||
|
||||
def test_comparisons(self):
|
||||
def f():
|
||||
a, b = range(2)
|
||||
assert b < a
|
||||
assert getmsg(f) == """assert 1 < 0"""
|
||||
def f():
|
||||
a, b, c = range(3)
|
||||
assert a > b > c
|
||||
assert getmsg(f) == """assert 0 > 1"""
|
||||
def f():
|
||||
a, b, c = range(3)
|
||||
assert a < b > c
|
||||
assert getmsg(f) == """assert 1 > 2"""
|
||||
def f():
|
||||
a, b, c = range(3)
|
||||
assert a < b <= c
|
||||
getmsg(f, must_pass=True)
|
||||
def f():
|
||||
a, b, c = range(3)
|
||||
assert a < b
|
||||
assert b < c
|
||||
getmsg(f, must_pass=True)
|
||||
|
||||
def test_len(self):
|
||||
def f():
|
||||
l = list(range(10))
|
||||
assert len(l) == 11
|
||||
assert getmsg(f).startswith("""assert 10 == 11
|
||||
+ where 10 = len([""")
|
||||
|
||||
def test_custom_reprcompare(self, monkeypatch):
|
||||
def my_reprcompare(op, left, right):
|
||||
return "42"
|
||||
monkeypatch.setattr(util, "_reprcompare", my_reprcompare)
|
||||
def f():
|
||||
assert 42 < 3
|
||||
assert getmsg(f) == "assert 42"
|
||||
def my_reprcompare(op, left, right):
|
||||
return "%s %s %s" % (left, op, right)
|
||||
monkeypatch.setattr(util, "_reprcompare", my_reprcompare)
|
||||
def f():
|
||||
assert 1 < 3 < 5 <= 4 < 7
|
||||
assert getmsg(f) == "assert 5 <= 4"
|
||||
|
||||
def test_assert_raising_nonzero_in_comparison(self):
|
||||
def f():
|
||||
class A(object):
|
||||
def __nonzero__(self):
|
||||
raise ValueError(42)
|
||||
def __lt__(self, other):
|
||||
return A()
|
||||
def __repr__(self):
|
||||
return "<MY42 object>"
|
||||
def myany(x):
|
||||
return False
|
||||
assert myany(A() < 0)
|
||||
assert "<MY42 object> < 0" in getmsg(f)
|
||||
|
||||
def test_formatchar(self):
|
||||
def f():
|
||||
assert "%test" == "test"
|
||||
assert getmsg(f).startswith("assert '%test' == 'test'")
|
||||
|
||||
|
||||
class TestRewriteOnImport:
|
||||
|
||||
def test_pycache_is_a_file(self, testdir):
|
||||
testdir.tmpdir.join("__pycache__").write("Hello")
|
||||
testdir.makepyfile("""
|
||||
def test_rewritten():
|
||||
assert "@py_builtins" in globals()""")
|
||||
assert testdir.runpytest().ret == 0
|
||||
|
||||
def test_zipfile(self, testdir):
|
||||
z = testdir.tmpdir.join("myzip.zip")
|
||||
z_fn = str(z)
|
||||
f = zipfile.ZipFile(z_fn, "w")
|
||||
try:
|
||||
f.writestr("test_gum/__init__.py", "")
|
||||
f.writestr("test_gum/test_lizard.py", "")
|
||||
finally:
|
||||
f.close()
|
||||
z.chmod(256)
|
||||
testdir.makepyfile("""
|
||||
import sys
|
||||
sys.path.append(%r)
|
||||
import test_gum.test_lizard""" % (z_fn,))
|
||||
assert testdir.runpytest().ret == 0
|
||||
|
||||
def test_readonly(self, testdir):
|
||||
sub = testdir.mkdir("testing")
|
||||
sub.join("test_readonly.py").write(
|
||||
py.builtin._totext("""
|
||||
def test_rewritten():
|
||||
assert "@py_builtins" in globals()
|
||||
""").encode("utf-8"), "wb")
|
||||
sub.chmod(320)
|
||||
assert testdir.runpytest().ret == 0
|
||||
|
||||
def test_dont_write_bytecode(self, testdir, monkeypatch):
|
||||
testdir.makepyfile("""
|
||||
import os
|
||||
def test_no_bytecode():
|
||||
assert "__pycache__" in __cached__
|
||||
assert not os.path.exists(__cached__)
|
||||
assert not os.path.exists(os.path.dirname(__cached__))""")
|
||||
monkeypatch.setenv("PYTHONDONTWRITEBYTECODE", "1")
|
||||
assert testdir.runpytest().ret == 0
|
||||
|
||||
def test_pyc_vs_pyo(self, testdir, monkeypatch):
|
||||
testdir.makepyfile("""
|
||||
import pytest
|
||||
def test_optimized():
|
||||
"hello"
|
||||
assert test_optimized.__doc__ is None""")
|
||||
p = py.path.local.make_numbered_dir(prefix="runpytest-", keep=None,
|
||||
rootdir=testdir.tmpdir)
|
||||
tmp = "--basetemp=%s" % p
|
||||
monkeypatch.setenv("PYTHONOPTIMIZE", "2")
|
||||
assert testdir.runpybin("py.test", tmp).ret == 0
|
||||
monkeypatch.undo()
|
||||
assert testdir.runpybin("py.test", tmp).ret == 1
|
||||
|
||||
def test_package(self, testdir):
|
||||
pkg = testdir.tmpdir.join("pkg")
|
||||
pkg.mkdir()
|
||||
pkg.join("__init__.py").ensure()
|
||||
pkg.join("test_blah.py").write("""
|
||||
def test_rewritten():
|
||||
assert "@py_builtins" in globals()""")
|
||||
assert testdir.runpytest().ret == 0
|
||||
|
||||
def test_translate_newlines(self, testdir):
|
||||
content = "def test_rewritten():\r\n assert '@py_builtins' in globals()"
|
||||
b = content.encode("utf-8")
|
||||
testdir.tmpdir.join("test_newlines.py").write(b, "wb")
|
||||
assert testdir.runpytest().ret == 0
|
||||
@@ -16,7 +16,6 @@ class TestCaptureManager:
|
||||
|
||||
def test_configure_per_fspath(self, testdir):
|
||||
config = testdir.parseconfig(testdir.tmpdir)
|
||||
assert config.getvalue("capture") is None
|
||||
capman = CaptureManager()
|
||||
hasfd = hasattr(os, 'dup')
|
||||
if hasfd:
|
||||
@@ -53,6 +52,7 @@ class TestCaptureManager:
|
||||
capman.resumecapture(method)
|
||||
out, err = capman.suspendcapture()
|
||||
assert not out and not err
|
||||
capman.reset_capturings()
|
||||
finally:
|
||||
capouter.reset()
|
||||
|
||||
@@ -60,20 +60,23 @@ class TestCaptureManager:
|
||||
def test_juggle_capturings(self, testdir):
|
||||
capouter = py.io.StdCaptureFD()
|
||||
try:
|
||||
config = testdir.parseconfig(testdir.tmpdir)
|
||||
#config = testdir.parseconfig(testdir.tmpdir)
|
||||
capman = CaptureManager()
|
||||
capman.resumecapture("fd")
|
||||
pytest.raises(ValueError, 'capman.resumecapture("fd")')
|
||||
pytest.raises(ValueError, 'capman.resumecapture("sys")')
|
||||
os.write(1, "hello\n".encode('ascii'))
|
||||
out, err = capman.suspendcapture()
|
||||
assert out == "hello\n"
|
||||
capman.resumecapture("sys")
|
||||
os.write(1, "hello\n".encode('ascii'))
|
||||
py.builtin.print_("world", file=sys.stderr)
|
||||
out, err = capman.suspendcapture()
|
||||
assert not out
|
||||
assert err == "world\n"
|
||||
try:
|
||||
capman.resumecapture("fd")
|
||||
pytest.raises(ValueError, 'capman.resumecapture("fd")')
|
||||
pytest.raises(ValueError, 'capman.resumecapture("sys")')
|
||||
os.write(1, "hello\n".encode('ascii'))
|
||||
out, err = capman.suspendcapture()
|
||||
assert out == "hello\n"
|
||||
capman.resumecapture("sys")
|
||||
os.write(1, "hello\n".encode('ascii'))
|
||||
py.builtin.print_("world", file=sys.stderr)
|
||||
out, err = capman.suspendcapture()
|
||||
assert not out
|
||||
assert err == "world\n"
|
||||
finally:
|
||||
capman.reset_capturings()
|
||||
finally:
|
||||
capouter.reset()
|
||||
|
||||
|
||||
@@ -313,6 +313,7 @@ class TestSession:
|
||||
def test_collect_topdir(self, testdir):
|
||||
p = testdir.makepyfile("def test_func(): pass")
|
||||
id = "::".join([p.basename, "test_func"])
|
||||
# XXX migrate to inline_genitems? (see below)
|
||||
config = testdir.parseconfig(id)
|
||||
topdir = testdir.tmpdir
|
||||
rcol = Session(config)
|
||||
@@ -328,15 +329,9 @@ class TestSession:
|
||||
def test_collect_protocol_single_function(self, testdir):
|
||||
p = testdir.makepyfile("def test_func(): pass")
|
||||
id = "::".join([p.basename, "test_func"])
|
||||
config = testdir.parseconfig(id)
|
||||
topdir = testdir.tmpdir
|
||||
rcol = Session(config)
|
||||
assert topdir == rcol.fspath
|
||||
hookrec = testdir.getreportrecorder(config)
|
||||
rcol.perform_collect()
|
||||
items = rcol.items
|
||||
assert len(items) == 1
|
||||
item = items[0]
|
||||
items, hookrec = testdir.inline_genitems(id)
|
||||
item, = items
|
||||
assert item.name == "test_func"
|
||||
newid = item.nodeid
|
||||
assert newid == id
|
||||
@@ -363,10 +358,7 @@ class TestSession:
|
||||
p.basename + "::TestClass::()",
|
||||
normid,
|
||||
]:
|
||||
config = testdir.parseconfig(id)
|
||||
rcol = Session(config=config)
|
||||
rcol.perform_collect()
|
||||
items = rcol.items
|
||||
items, hookrec = testdir.inline_genitems(id)
|
||||
assert len(items) == 1
|
||||
assert items[0].name == "test_method"
|
||||
newid = items[0].nodeid
|
||||
@@ -388,11 +380,7 @@ class TestSession:
|
||||
""" % p.basename)
|
||||
id = p.basename
|
||||
|
||||
config = testdir.parseconfig(id)
|
||||
rcol = Session(config)
|
||||
hookrec = testdir.getreportrecorder(config)
|
||||
rcol.perform_collect()
|
||||
items = rcol.items
|
||||
items, hookrec = testdir.inline_genitems(id)
|
||||
py.std.pprint.pprint(hookrec.hookrecorder.calls)
|
||||
assert len(items) == 2
|
||||
hookrec.hookrecorder.contains([
|
||||
@@ -413,11 +401,8 @@ class TestSession:
|
||||
aaa = testdir.mkpydir("aaa")
|
||||
test_aaa = aaa.join("test_aaa.py")
|
||||
p.move(test_aaa)
|
||||
config = testdir.parseconfig()
|
||||
rcol = Session(config)
|
||||
hookrec = testdir.getreportrecorder(config)
|
||||
rcol.perform_collect()
|
||||
items = rcol.items
|
||||
|
||||
items, hookrec = testdir.inline_genitems()
|
||||
assert len(items) == 1
|
||||
py.std.pprint.pprint(hookrec.hookrecorder.calls)
|
||||
hookrec.hookrecorder.contains([
|
||||
@@ -437,11 +422,8 @@ class TestSession:
|
||||
p.move(test_bbb)
|
||||
|
||||
id = "."
|
||||
config = testdir.parseconfig(id)
|
||||
rcol = Session(config)
|
||||
hookrec = testdir.getreportrecorder(config)
|
||||
rcol.perform_collect()
|
||||
items = rcol.items
|
||||
|
||||
items, hookrec = testdir.inline_genitems(id)
|
||||
assert len(items) == 2
|
||||
py.std.pprint.pprint(hookrec.hookrecorder.calls)
|
||||
hookrec.hookrecorder.contains([
|
||||
@@ -455,19 +437,13 @@ class TestSession:
|
||||
|
||||
def test_serialization_byid(self, testdir):
|
||||
p = testdir.makepyfile("def test_func(): pass")
|
||||
config = testdir.parseconfig()
|
||||
rcol = Session(config)
|
||||
rcol.perform_collect()
|
||||
items = rcol.items
|
||||
items, hookrec = testdir.inline_genitems()
|
||||
assert len(items) == 1
|
||||
item, = items
|
||||
rcol.config.pluginmanager.unregister(name="session")
|
||||
newcol = Session(config)
|
||||
item2, = newcol.perform_collect([item.nodeid], genitems=False)
|
||||
items2, hookrec = testdir.inline_genitems(item.nodeid)
|
||||
item2, = items2
|
||||
assert item2.name == item.name
|
||||
assert item2.fspath == item.fspath
|
||||
item2b, = newcol.perform_collect([item.nodeid], genitems=False)
|
||||
assert item2b == item2
|
||||
|
||||
def test_find_byid_without_instance_parents(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
@@ -476,10 +452,7 @@ class TestSession:
|
||||
pass
|
||||
""")
|
||||
arg = p.basename + ("::TestClass::test_method")
|
||||
config = testdir.parseconfig(arg)
|
||||
rcol = Session(config)
|
||||
rcol.perform_collect()
|
||||
items = rcol.items
|
||||
items, hookrec = testdir.inline_genitems(arg)
|
||||
assert len(items) == 1
|
||||
item, = items
|
||||
assert item.nodeid.endswith("TestClass::()::test_method")
|
||||
@@ -487,7 +460,7 @@ class TestSession:
|
||||
class Test_getinitialnodes:
|
||||
def test_global_file(self, testdir, tmpdir):
|
||||
x = tmpdir.ensure("x.py")
|
||||
config = testdir.reparseconfig([x])
|
||||
config = testdir.parseconfigure(x)
|
||||
col = testdir.getnode(config, x)
|
||||
assert isinstance(col, pytest.Module)
|
||||
assert col.name == 'x.py'
|
||||
@@ -502,7 +475,7 @@ class Test_getinitialnodes:
|
||||
subdir = tmpdir.join("subdir")
|
||||
x = subdir.ensure("x.py")
|
||||
subdir.ensure("__init__.py")
|
||||
config = testdir.reparseconfig([x])
|
||||
config = testdir.parseconfigure(x)
|
||||
col = testdir.getnode(config, x)
|
||||
assert isinstance(col, pytest.Module)
|
||||
assert col.name == 'subdir/x.py'
|
||||
@@ -528,12 +501,6 @@ class Test_genitems:
|
||||
assert hash(i) != hash(j)
|
||||
assert i != j
|
||||
|
||||
def test_root_conftest_syntax_error(self, testdir):
|
||||
# do we want to unify behaviour with
|
||||
# test_subdir_conftest_error?
|
||||
p = testdir.makepyfile(conftest="raise SyntaxError\n")
|
||||
pytest.raises(SyntaxError, testdir.inline_genitems, p.dirpath())
|
||||
|
||||
def test_example_items1(self, testdir):
|
||||
p = testdir.makepyfile('''
|
||||
def testone():
|
||||
@@ -597,6 +564,6 @@ def test_matchnodes_two_collections_same_file(testdir):
|
||||
res.stdout.fnmatch_lines([
|
||||
"*1 passed*",
|
||||
])
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import py, pytest
|
||||
|
||||
from _pytest.config import getcfg, Config
|
||||
from _pytest.config import getcfg
|
||||
|
||||
class TestParseIni:
|
||||
def test_getcfg_and_config(self, tmpdir):
|
||||
def test_getcfg_and_config(self, testdir, tmpdir):
|
||||
sub = tmpdir.mkdir("sub")
|
||||
sub.chdir()
|
||||
tmpdir.join("setup.cfg").write(py.code.Source("""
|
||||
@@ -12,22 +12,23 @@ class TestParseIni:
|
||||
"""))
|
||||
cfg = getcfg([sub], ["setup.cfg"])
|
||||
assert cfg['name'] == "value"
|
||||
config = Config()
|
||||
config._preparse([sub])
|
||||
config = testdir.parseconfigure(sub)
|
||||
assert config.inicfg['name'] == 'value'
|
||||
|
||||
def test_append_parse_args(self, tmpdir):
|
||||
def test_getcfg_empty_path(self, tmpdir):
|
||||
cfg = getcfg([''], ['setup.cfg']) #happens on py.test ""
|
||||
|
||||
def test_append_parse_args(self, testdir, tmpdir):
|
||||
tmpdir.join("setup.cfg").write(py.code.Source("""
|
||||
[pytest]
|
||||
addopts = --verbose
|
||||
"""))
|
||||
config = Config()
|
||||
config.parse([tmpdir])
|
||||
config = testdir.parseconfig(tmpdir)
|
||||
assert config.option.verbose
|
||||
config = Config()
|
||||
args = [tmpdir,]
|
||||
config._preparse(args, addopts=False)
|
||||
assert len(args) == 1
|
||||
#config = testdir.Config()
|
||||
#args = [tmpdir,]
|
||||
#config._preparse(args, addopts=False)
|
||||
#assert len(args) == 1
|
||||
|
||||
def test_tox_ini_wrong_version(self, testdir):
|
||||
p = testdir.makefile('.ini', tox="""
|
||||
@@ -46,8 +47,7 @@ class TestParseIni:
|
||||
[pytest]
|
||||
minversion = 1.0
|
||||
"""))
|
||||
config = Config()
|
||||
config.parse([testdir.tmpdir])
|
||||
config = testdir.parseconfig()
|
||||
assert config.getini("minversion") == "1.0"
|
||||
|
||||
def test_toxini_before_lower_pytestini(self, testdir):
|
||||
@@ -60,8 +60,7 @@ class TestParseIni:
|
||||
[pytest]
|
||||
minversion = 1.5
|
||||
"""))
|
||||
config = Config()
|
||||
config.parse([sub])
|
||||
config = testdir.parseconfigure(sub)
|
||||
assert config.getini("minversion") == "2.0"
|
||||
|
||||
@pytest.mark.xfail(reason="probably not needed")
|
||||
@@ -74,10 +73,10 @@ class TestParseIni:
|
||||
""")
|
||||
result = testdir.runpytest("--confcutdir=.")
|
||||
assert result.ret == 0
|
||||
|
||||
|
||||
class TestConfigCmdlineParsing:
|
||||
def test_parsing_again_fails(self, testdir):
|
||||
config = testdir.reparseconfig([testdir.tmpdir])
|
||||
config = testdir.parseconfig()
|
||||
pytest.raises(AssertionError, "config.parse([])")
|
||||
|
||||
|
||||
@@ -88,7 +87,7 @@ class TestConfigAPI:
|
||||
config.trace.root.setwriter(l.append)
|
||||
config.trace("hello")
|
||||
assert len(l) == 1
|
||||
assert l[0] == "[pytest] hello\n"
|
||||
assert l[0] == "hello [config]\n"
|
||||
|
||||
def test_config_getvalue_honours_conftest(self, testdir):
|
||||
testdir.makepyfile(conftest="x=1")
|
||||
@@ -98,7 +97,7 @@ class TestConfigAPI:
|
||||
assert config.getvalue("x") == 1
|
||||
assert config.getvalue("x", o.join('sub')) == 2
|
||||
pytest.raises(KeyError, "config.getvalue('y')")
|
||||
config = testdir.reparseconfig([str(o.join('sub'))])
|
||||
config = testdir.parseconfigure(str(o.join('sub')))
|
||||
assert config.getvalue("x") == 2
|
||||
assert config.getvalue("y") == 3
|
||||
assert config.getvalue("x", o) == 1
|
||||
@@ -124,18 +123,18 @@ class TestConfigAPI:
|
||||
def test_config_overwrite(self, testdir):
|
||||
o = testdir.tmpdir
|
||||
o.ensure("conftest.py").write("x=1")
|
||||
config = testdir.reparseconfig([str(o)])
|
||||
config = testdir.parseconfig(str(o))
|
||||
assert config.getvalue('x') == 1
|
||||
config.option.x = 2
|
||||
assert config.getvalue('x') == 2
|
||||
config = testdir.reparseconfig([str(o)])
|
||||
config = testdir.parseconfig([str(o)])
|
||||
assert config.getvalue('x') == 1
|
||||
|
||||
def test_getconftest_pathlist(self, testdir, tmpdir):
|
||||
somepath = tmpdir.join("x", "y", "z")
|
||||
p = tmpdir.join("conftest.py")
|
||||
p.write("pathlist = ['.', %r]" % str(somepath))
|
||||
config = testdir.reparseconfig([p])
|
||||
config = testdir.parseconfigure(p)
|
||||
assert config._getconftest_pathlist('notexist') is None
|
||||
pl = config._getconftest_pathlist('pathlist')
|
||||
print(pl)
|
||||
@@ -209,6 +208,40 @@ class TestConfigAPI:
|
||||
l = config.getini("a2")
|
||||
assert l == []
|
||||
|
||||
def test_addinivalue_line_existing(self, testdir):
|
||||
testdir.makeconftest("""
|
||||
def pytest_addoption(parser):
|
||||
parser.addini("xy", "", type="linelist")
|
||||
""")
|
||||
p = testdir.makeini("""
|
||||
[pytest]
|
||||
xy= 123
|
||||
""")
|
||||
config = testdir.parseconfig()
|
||||
l = config.getini("xy")
|
||||
assert len(l) == 1
|
||||
assert l == ["123"]
|
||||
config.addinivalue_line("xy", "456")
|
||||
l = config.getini("xy")
|
||||
assert len(l) == 2
|
||||
assert l == ["123", "456"]
|
||||
|
||||
def test_addinivalue_line_new(self, testdir):
|
||||
testdir.makeconftest("""
|
||||
def pytest_addoption(parser):
|
||||
parser.addini("xy", "", type="linelist")
|
||||
""")
|
||||
config = testdir.parseconfig()
|
||||
assert not config.getini("xy")
|
||||
config.addinivalue_line("xy", "456")
|
||||
l = config.getini("xy")
|
||||
assert len(l) == 1
|
||||
assert l == ["456"]
|
||||
config.addinivalue_line("xy", "123")
|
||||
l = config.getini("xy")
|
||||
assert len(l) == 2
|
||||
assert l == ["456", "123"]
|
||||
|
||||
def test_options_on_small_file_do_not_blow_up(testdir):
|
||||
def runfiletest(opts):
|
||||
reprec = testdir.inline_run(*opts)
|
||||
|
||||
@@ -89,7 +89,7 @@ class TestConftestValueAccessGlobal:
|
||||
assert value == 1.5
|
||||
path = py.path.local(mod.__file__)
|
||||
assert path.dirpath() == basedir.join("adir", "b")
|
||||
assert path.purebasename == "conftest"
|
||||
assert path.purebasename.startswith("conftest")
|
||||
|
||||
def test_conftest_in_nonpkg_with_init(tmpdir):
|
||||
tmpdir.ensure("adir-1.0/conftest.py").write("a=1 ; Directory = 3")
|
||||
|
||||
@@ -332,17 +332,6 @@ class TestPytestPluginInteractions:
|
||||
"*did not find*sys*"
|
||||
])
|
||||
|
||||
def test_do_option_conftestplugin(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
def pytest_addoption(parser):
|
||||
parser.addoption('--test123', action="store_true")
|
||||
""")
|
||||
config = testdir.Config()
|
||||
config._conftest.importconftest(p)
|
||||
print(config.pluginmanager.getplugins())
|
||||
config.parse([])
|
||||
assert not config.option.test123
|
||||
|
||||
def test_namespace_early_from_import(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
from pytest import Item
|
||||
@@ -370,9 +359,7 @@ class TestPytestPluginInteractions:
|
||||
])
|
||||
|
||||
def test_do_option_postinitialize(self, testdir):
|
||||
config = testdir.Config()
|
||||
config.parse([])
|
||||
config.pluginmanager.do_configure(config=config)
|
||||
config = testdir.parseconfigure()
|
||||
assert not hasattr(config.option, 'test123')
|
||||
p = testdir.makepyfile("""
|
||||
def pytest_addoption(parser):
|
||||
@@ -586,17 +573,17 @@ class TestHookRelay:
|
||||
class TestTracer:
|
||||
def test_simple(self):
|
||||
from _pytest.core import TagTracer
|
||||
rootlogger = TagTracer("[my] ")
|
||||
rootlogger = TagTracer()
|
||||
log = rootlogger.get("pytest")
|
||||
log("hello")
|
||||
l = []
|
||||
rootlogger.setwriter(l.append)
|
||||
log("world")
|
||||
assert len(l) == 1
|
||||
assert l[0] == "[my] world\n"
|
||||
assert l[0] == "world [pytest]\n"
|
||||
sublog = log.get("collection")
|
||||
sublog("hello")
|
||||
assert l[1] == "[my] hello\n"
|
||||
assert l[1] == "hello [pytest:collection]\n"
|
||||
|
||||
def test_indent(self):
|
||||
from _pytest.core import TagTracer
|
||||
@@ -616,7 +603,7 @@ class TestTracer:
|
||||
log.root.indent -= 1
|
||||
log("last")
|
||||
assert len(l) == 7
|
||||
names = [x.rstrip()[len(rootlogger.prefix):] for x in l]
|
||||
names = [x[:x.rfind(' [')] for x in l]
|
||||
assert names == ['hello', ' line1', ' line2',
|
||||
' line3', ' line4', ' line5', 'last']
|
||||
|
||||
@@ -640,7 +627,7 @@ class TestTracer:
|
||||
log2("seen")
|
||||
tags, args = l2[0]
|
||||
assert args == ("seen",)
|
||||
|
||||
|
||||
|
||||
def test_setmyprocessor(self):
|
||||
from _pytest.core import TagTracer
|
||||
@@ -657,3 +644,10 @@ class TestTracer:
|
||||
assert "1" in tags
|
||||
assert "2" in tags
|
||||
assert args == (42,)
|
||||
|
||||
def test_default_markers(testdir):
|
||||
result = testdir.runpytest("--markers")
|
||||
result.stdout.fnmatch_lines([
|
||||
"*tryfirst*first*",
|
||||
"*trylast*last*",
|
||||
])
|
||||
|
||||
@@ -59,6 +59,21 @@ class TestDoctests:
|
||||
"*UNEXPECTED*ZeroDivision*",
|
||||
])
|
||||
|
||||
def test_doctest_unex_importerror(self, testdir):
|
||||
testdir.tmpdir.join("hello.py").write(py.code.Source("""
|
||||
import asdalsdkjaslkdjasd
|
||||
"""))
|
||||
p = testdir.maketxtfile("""
|
||||
>>> import hello
|
||||
>>>
|
||||
""")
|
||||
result = testdir.runpytest("--doctest-modules")
|
||||
result.stdout.fnmatch_lines([
|
||||
"*>>> import hello",
|
||||
"*UNEXPECTED*ImportError*",
|
||||
"*import asdals*",
|
||||
])
|
||||
|
||||
def test_doctestmodule(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
'''
|
||||
@@ -99,7 +114,7 @@ class TestDoctests:
|
||||
>>> i + 1
|
||||
2
|
||||
""")
|
||||
result = testdir.runpytest(p)
|
||||
result = testdir.runpytest(p, "-s")
|
||||
result.stdout.fnmatch_lines([
|
||||
'001 >>> i = 0',
|
||||
'002 >>> i + 1',
|
||||
|
||||
@@ -62,3 +62,17 @@ def test_traceconfig(testdir):
|
||||
"*using*pytest*py*",
|
||||
"*active plugins*",
|
||||
])
|
||||
|
||||
def test_debug(testdir, monkeypatch):
|
||||
result = testdir.runpytest("--debug")
|
||||
assert result.ret == 0
|
||||
p = testdir.tmpdir.join("pytestdebug.log")
|
||||
assert "pytest_sessionstart" in p.read()
|
||||
|
||||
def test_PYTEST_DEBUG(testdir, monkeypatch):
|
||||
monkeypatch.setenv("PYTEST_DEBUG", "1")
|
||||
result = testdir.runpytest()
|
||||
assert result.ret == 0
|
||||
result.stderr.fnmatch_lines([
|
||||
"*registered*PluginManager*"
|
||||
])
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
|
||||
from xml.dom import minidom
|
||||
import py, sys
|
||||
import py, sys, os
|
||||
|
||||
def runandparse(testdir, *args):
|
||||
resultpath = testdir.tmpdir.join("junit.xml")
|
||||
@@ -39,6 +39,18 @@ class TestPython:
|
||||
node = dom.getElementsByTagName("testsuite")[0]
|
||||
assert_attr(node, errors=0, failures=1, skips=3, tests=2)
|
||||
|
||||
def test_timing_function(self, testdir):
|
||||
testdir.makepyfile("""
|
||||
import time, pytest
|
||||
def test_sleep():
|
||||
time.sleep(0.01)
|
||||
""")
|
||||
result, dom = runandparse(testdir)
|
||||
node = dom.getElementsByTagName("testsuite")[0]
|
||||
tnode = node.getElementsByTagName("testcase")[0]
|
||||
val = tnode.getAttributeNode("time").value
|
||||
assert float(val) >= 0.001
|
||||
|
||||
def test_setup_error(self, testdir):
|
||||
testdir.makepyfile("""
|
||||
def pytest_funcarg__arg(request):
|
||||
@@ -119,7 +131,14 @@ class TestPython:
|
||||
assert "Division" in fnode.toxml()
|
||||
|
||||
def test_failure_function(self, testdir):
|
||||
testdir.makepyfile("def test_fail(): raise ValueError(42)")
|
||||
testdir.makepyfile("""
|
||||
import sys
|
||||
def test_fail():
|
||||
print ("hello-stdout")
|
||||
sys.stderr.write("hello-stderr\\n")
|
||||
raise ValueError(42)
|
||||
""")
|
||||
|
||||
result, dom = runandparse(testdir)
|
||||
assert result.ret
|
||||
node = dom.getElementsByTagName("testsuite")[0]
|
||||
@@ -131,6 +150,12 @@ class TestPython:
|
||||
fnode = tnode.getElementsByTagName("failure")[0]
|
||||
assert_attr(fnode, message="test failure")
|
||||
assert "ValueError" in fnode.toxml()
|
||||
systemout = fnode.nextSibling
|
||||
assert systemout.tagName == "system-out"
|
||||
assert "hello-stdout" in systemout.toxml()
|
||||
systemerr = systemout.nextSibling
|
||||
assert systemerr.tagName == "system-err"
|
||||
assert "hello-stderr" in systemerr.toxml()
|
||||
|
||||
def test_failure_escape(self, testdir):
|
||||
testdir.makepyfile("""
|
||||
@@ -351,3 +376,16 @@ def test_invalid_xml_escape(testdir):
|
||||
assert '#x%04X' % i in text
|
||||
for i in valid:
|
||||
assert chr(i) in text
|
||||
|
||||
def test_logxml_path_expansion():
|
||||
from _pytest.junitxml import LogXML
|
||||
|
||||
home_tilde = os.path.normpath(os.path.expanduser('~/test.xml'))
|
||||
# this is here for when $HOME is not set correct
|
||||
home_var = os.path.normpath(os.path.expandvars('$HOME/test.xml'))
|
||||
|
||||
xml_tilde = LogXML('~/test.xml', None)
|
||||
assert xml_tilde.logfile == home_tilde
|
||||
|
||||
xml_var = LogXML('$HOME/test.xml', None)
|
||||
assert xml_var.logfile == home_var
|
||||
|
||||
@@ -2,6 +2,11 @@ import py, pytest
|
||||
from _pytest.mark import MarkGenerator as Mark
|
||||
|
||||
class TestMark:
|
||||
def test_markinfo_repr(self):
|
||||
from _pytest.mark import MarkInfo
|
||||
m = MarkInfo("hello", (1,2), {})
|
||||
repr(m)
|
||||
|
||||
def test_pytest_exists_in_namespace_all(self):
|
||||
assert 'mark' in py.test.__all__
|
||||
assert 'mark' in pytest.__all__
|
||||
@@ -63,7 +68,78 @@ class TestMark:
|
||||
assert 'reason' not in g.some.kwargs
|
||||
assert g.some.kwargs['reason2'] == "456"
|
||||
|
||||
|
||||
def test_ini_markers(testdir):
|
||||
testdir.makeini("""
|
||||
[pytest]
|
||||
markers =
|
||||
a1: this is a webtest marker
|
||||
a2: this is a smoke marker
|
||||
""")
|
||||
testdir.makepyfile("""
|
||||
def test_markers(pytestconfig):
|
||||
markers = pytestconfig.getini("markers")
|
||||
print (markers)
|
||||
assert len(markers) >= 2
|
||||
assert markers[0].startswith("a1:")
|
||||
assert markers[1].startswith("a2:")
|
||||
""")
|
||||
rec = testdir.inline_run()
|
||||
rec.assertoutcome(passed=1)
|
||||
|
||||
def test_markers_option(testdir):
|
||||
testdir.makeini("""
|
||||
[pytest]
|
||||
markers =
|
||||
a1: this is a webtest marker
|
||||
a1some: another marker
|
||||
""")
|
||||
result = testdir.runpytest("--markers", )
|
||||
result.stdout.fnmatch_lines([
|
||||
"*a1*this is a webtest*",
|
||||
"*a1some*another marker",
|
||||
])
|
||||
|
||||
|
||||
def test_strict_prohibits_unregistered_markers(testdir):
|
||||
testdir.makepyfile("""
|
||||
import pytest
|
||||
@pytest.mark.unregisteredmark
|
||||
def test_hello():
|
||||
pass
|
||||
""")
|
||||
result = testdir.runpytest("--strict")
|
||||
assert result.ret != 0
|
||||
result.stdout.fnmatch_lines([
|
||||
"*unregisteredmark*not*registered*",
|
||||
])
|
||||
|
||||
@pytest.mark.multi(spec=[
|
||||
("xyz", ("test_one",)),
|
||||
("xyz and xyz2", ()),
|
||||
("xyz2", ("test_two",)),
|
||||
("xyz or xyz2", ("test_one", "test_two"),)
|
||||
])
|
||||
def test_mark_option(spec, testdir):
|
||||
testdir.makepyfile("""
|
||||
import pytest
|
||||
@pytest.mark.xyz
|
||||
def test_one():
|
||||
pass
|
||||
@pytest.mark.xyz2
|
||||
def test_two():
|
||||
pass
|
||||
""")
|
||||
opt, passed_result = spec
|
||||
rec = testdir.inline_run("-m", opt)
|
||||
passed, skipped, fail = rec.listoutcomes()
|
||||
passed = [x.nodeid.split("::")[-1] for x in passed]
|
||||
assert len(passed) == len(passed_result)
|
||||
assert list(passed) == list(passed_result)
|
||||
|
||||
|
||||
class TestFunctional:
|
||||
|
||||
def test_mark_per_function(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
import pytest
|
||||
@@ -72,7 +148,7 @@ class TestFunctional:
|
||||
assert hasattr(test_hello, 'hello')
|
||||
""")
|
||||
result = testdir.runpytest(p)
|
||||
result.stdout.fnmatch_lines(["*passed*"])
|
||||
result.stdout.fnmatch_lines(["*1 passed*"])
|
||||
|
||||
def test_mark_per_module(self, testdir):
|
||||
item = testdir.getitem("""
|
||||
@@ -184,58 +260,6 @@ class TestFunctional:
|
||||
])
|
||||
|
||||
|
||||
class Test_genitems:
|
||||
def test_check_collect_hashes(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
def test_1():
|
||||
pass
|
||||
|
||||
def test_2():
|
||||
pass
|
||||
""")
|
||||
p.copy(p.dirpath(p.purebasename + "2" + ".py"))
|
||||
items, reprec = testdir.inline_genitems(p.dirpath())
|
||||
assert len(items) == 4
|
||||
for numi, i in enumerate(items):
|
||||
for numj, j in enumerate(items):
|
||||
if numj != numi:
|
||||
assert hash(i) != hash(j)
|
||||
assert i != j
|
||||
|
||||
def test_root_conftest_syntax_error(self, testdir):
|
||||
# do we want to unify behaviour with
|
||||
# test_subdir_conftest_error?
|
||||
p = testdir.makepyfile(conftest="raise SyntaxError\n")
|
||||
pytest.raises(SyntaxError, testdir.inline_genitems, p.dirpath())
|
||||
|
||||
def test_example_items1(self, testdir):
|
||||
p = testdir.makepyfile('''
|
||||
def testone():
|
||||
pass
|
||||
|
||||
class TestX:
|
||||
def testmethod_one(self):
|
||||
pass
|
||||
|
||||
class TestY(TestX):
|
||||
pass
|
||||
''')
|
||||
items, reprec = testdir.inline_genitems(p)
|
||||
assert len(items) == 3
|
||||
assert items[0].name == 'testone'
|
||||
assert items[1].name == 'testmethod_one'
|
||||
assert items[2].name == 'testmethod_one'
|
||||
|
||||
# let's also test getmodpath here
|
||||
assert items[0].getmodpath() == "testone"
|
||||
assert items[1].getmodpath() == "TestX.testmethod_one"
|
||||
assert items[2].getmodpath() == "TestY.testmethod_one"
|
||||
|
||||
s = items[0].getmodpath(stopatmodule=False)
|
||||
assert s.endswith("test_example_items1.testone")
|
||||
print(s)
|
||||
|
||||
|
||||
class TestKeywordSelection:
|
||||
def test_select_simple(self, testdir):
|
||||
file_test = testdir.makepyfile("""
|
||||
|
||||
@@ -255,3 +255,19 @@ def test_nose_style_setup_teardown(testdir):
|
||||
result.stdout.fnmatch_lines([
|
||||
"*2 passed*",
|
||||
])
|
||||
|
||||
def test_nose_setup_ordering(testdir):
|
||||
testdir.makepyfile("""
|
||||
def setup_module(mod):
|
||||
mod.visited = True
|
||||
|
||||
class TestClass:
|
||||
def setup(self):
|
||||
assert visited
|
||||
def test_first(self):
|
||||
pass
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
"*1 passed*",
|
||||
])
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import pytest
|
||||
|
||||
class TestPasting:
|
||||
def pytest_funcarg__pastebinlist(self, request):
|
||||
@@ -45,3 +46,14 @@ class TestPasting:
|
||||
for x in 'test_fail test_skip skipped'.split():
|
||||
assert s.find(x), (s, x)
|
||||
|
||||
|
||||
class TestRPCClient:
|
||||
def pytest_funcarg__pastebin(self, request):
|
||||
return request.config.pluginmanager.getplugin('pastebin')
|
||||
|
||||
def test_getproxy(self, pastebin):
|
||||
proxy = pastebin.getproxy()
|
||||
assert proxy is not None
|
||||
assert proxy.__class__.__module__.startswith('xmlrpc')
|
||||
|
||||
|
||||
|
||||
@@ -106,6 +106,26 @@ class TestPDB:
|
||||
if child.isalive():
|
||||
child.wait()
|
||||
|
||||
def test_pdb_interaction_doctest(self, testdir):
|
||||
p1 = testdir.makepyfile("""
|
||||
import pytest
|
||||
def function_1():
|
||||
'''
|
||||
>>> i = 0
|
||||
>>> assert i == 1
|
||||
'''
|
||||
""")
|
||||
child = testdir.spawn_pytest("--doctest-modules --pdb %s" % p1)
|
||||
child.expect("(Pdb)")
|
||||
child.sendline('i')
|
||||
child.expect("0")
|
||||
child.expect("(Pdb)")
|
||||
child.sendeof()
|
||||
rest = child.read()
|
||||
assert "1 failed" in rest
|
||||
if child.isalive():
|
||||
child.wait()
|
||||
|
||||
def test_pdb_interaction_capturing_twice(self, testdir):
|
||||
p1 = testdir.makepyfile("""
|
||||
import pytest
|
||||
@@ -144,6 +164,19 @@ class TestPDB:
|
||||
child.sendeof()
|
||||
child.wait()
|
||||
|
||||
def test_pdb_used_in_generate_tests(self, testdir):
|
||||
p1 = testdir.makepyfile("""
|
||||
import pytest
|
||||
def pytest_generate_tests(metafunc):
|
||||
pytest.set_trace()
|
||||
x = 5
|
||||
def test_foo(a):
|
||||
pass
|
||||
""")
|
||||
child = testdir.spawn_pytest(str(p1))
|
||||
child.expect("x = 5")
|
||||
child.sendeof()
|
||||
child.wait()
|
||||
def test_pdb_collection_failure_is_shown(self, testdir):
|
||||
p1 = testdir.makepyfile("""xxx """)
|
||||
result = testdir.runpytest("--pdb", p1)
|
||||
|
||||
@@ -257,7 +257,7 @@ class TestFunction:
|
||||
assert hasattr(modcol.obj, 'test_func')
|
||||
|
||||
def test_function_equality(self, testdir, tmpdir):
|
||||
config = testdir.reparseconfig()
|
||||
config = testdir.parseconfigure()
|
||||
session = testdir.Session(config)
|
||||
f1 = pytest.Function(name="name", config=config,
|
||||
args=(1,), callobj=isinstance, session=session)
|
||||
@@ -279,7 +279,7 @@ class TestFunction:
|
||||
assert not f1 != f1_b
|
||||
|
||||
def test_function_equality_with_callspec(self, testdir, tmpdir):
|
||||
config = testdir.reparseconfig()
|
||||
config = testdir.parseconfigure()
|
||||
class callspec1:
|
||||
param = 1
|
||||
funcargs = {}
|
||||
@@ -520,12 +520,6 @@ def test_getfuncargnames():
|
||||
if sys.version_info < (3,0):
|
||||
assert funcargs.getfuncargnames(A.f) == ['arg1']
|
||||
|
||||
def test_callspec_repr():
|
||||
cs = funcargs.CallSpec({}, 'hello', 1)
|
||||
repr(cs)
|
||||
cs = funcargs.CallSpec({}, 'hello', funcargs._notexists)
|
||||
repr(cs)
|
||||
|
||||
class TestFillFuncArgs:
|
||||
def test_fillfuncargs_exposed(self):
|
||||
# used by oejskit
|
||||
@@ -689,11 +683,11 @@ class TestRequest:
|
||||
def test_func(something): pass
|
||||
""")
|
||||
req = funcargs.FuncargRequest(item)
|
||||
req.config._setupstate.prepare(item) # XXX
|
||||
req._pyfuncitem.session._setupstate.prepare(item) # XXX
|
||||
req._fillfuncargs()
|
||||
# successively check finalization calls
|
||||
teardownlist = item.getparent(pytest.Module).obj.teardownlist
|
||||
ss = item.config._setupstate
|
||||
ss = item.session._setupstate
|
||||
assert not teardownlist
|
||||
ss.teardown_exact(item)
|
||||
print(ss.stack)
|
||||
@@ -783,7 +777,7 @@ class TestRequestCachedSetup:
|
||||
req2 = funcargs.FuncargRequest(item2)
|
||||
ret2 = req2.cached_setup(setup, scope="class")
|
||||
assert ret2 == "hello"
|
||||
|
||||
|
||||
req3 = funcargs.FuncargRequest(item3)
|
||||
ret3a = req3.cached_setup(setup, scope="class")
|
||||
ret3b = req3.cached_setup(setup, scope="class")
|
||||
@@ -818,11 +812,11 @@ class TestRequestCachedSetup:
|
||||
ret1 = req1.cached_setup(setup, teardown, scope="function")
|
||||
assert l == ['setup']
|
||||
# artificial call of finalizer
|
||||
req1.config._setupstate._callfinalizers(item1)
|
||||
req1._pyfuncitem.session._setupstate._callfinalizers(item1)
|
||||
assert l == ["setup", "teardown"]
|
||||
ret2 = req1.cached_setup(setup, teardown, scope="function")
|
||||
assert l == ["setup", "teardown", "setup"]
|
||||
req1.config._setupstate._callfinalizers(item1)
|
||||
req1._pyfuncitem.session._setupstate._callfinalizers(item1)
|
||||
assert l == ["setup", "teardown", "setup", "teardown"]
|
||||
|
||||
def test_request_cached_setup_two_args(self, testdir):
|
||||
@@ -886,6 +880,7 @@ class TestMetafunc:
|
||||
def function(): pass
|
||||
metafunc = funcargs.Metafunc(function)
|
||||
assert not metafunc.funcargnames
|
||||
repr(metafunc._calls)
|
||||
|
||||
def test_function_basic(self):
|
||||
def func(arg1, arg2="qwe"): pass
|
||||
@@ -925,9 +920,9 @@ class TestMetafunc:
|
||||
metafunc.addcall(param=obj)
|
||||
metafunc.addcall(param=1)
|
||||
assert len(metafunc._calls) == 3
|
||||
assert metafunc._calls[0].param == obj
|
||||
assert metafunc._calls[1].param == obj
|
||||
assert metafunc._calls[2].param == 1
|
||||
assert metafunc._calls[0].getparam("arg1") == obj
|
||||
assert metafunc._calls[1].getparam("arg1") == obj
|
||||
assert metafunc._calls[2].getparam("arg1") == 1
|
||||
|
||||
def test_addcall_funcargs(self):
|
||||
def func(x): pass
|
||||
@@ -941,7 +936,119 @@ class TestMetafunc:
|
||||
assert metafunc._calls[1].funcargs == {'x': 3}
|
||||
assert not hasattr(metafunc._calls[1], 'param')
|
||||
|
||||
class TestGenfuncFunctional:
|
||||
def test_parametrize_error(self):
|
||||
def func(x, y): pass
|
||||
metafunc = funcargs.Metafunc(func)
|
||||
metafunc.parametrize("x", [1,2])
|
||||
pytest.raises(ValueError, lambda: metafunc.parametrize("x", [5,6]))
|
||||
pytest.raises(ValueError, lambda: metafunc.parametrize("x", [5,6]))
|
||||
metafunc.parametrize("y", [1,2])
|
||||
pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5,6]))
|
||||
pytest.raises(ValueError, lambda: metafunc.parametrize("y", [5,6]))
|
||||
|
||||
def test_parametrize_and_id(self):
|
||||
def func(x, y): pass
|
||||
metafunc = funcargs.Metafunc(func)
|
||||
|
||||
metafunc.parametrize("x", [1,2], ids=['basic', 'advanced'])
|
||||
metafunc.parametrize("y", ["abc", "def"])
|
||||
ids = [x.id for x in metafunc._calls]
|
||||
assert ids == ["basic-abc", "basic-def", "advanced-abc", "advanced-def"]
|
||||
|
||||
def test_parametrize_with_userobjects(self):
|
||||
def func(x, y): pass
|
||||
metafunc = funcargs.Metafunc(func)
|
||||
class A:
|
||||
pass
|
||||
metafunc.parametrize("x", [A(), A()])
|
||||
metafunc.parametrize("y", list("ab"))
|
||||
assert metafunc._calls[0].id == ".0-a"
|
||||
assert metafunc._calls[1].id == ".0-b"
|
||||
assert metafunc._calls[2].id == ".1-a"
|
||||
assert metafunc._calls[3].id == ".1-b"
|
||||
|
||||
def test_addcall_and_parametrize(self):
|
||||
def func(x, y): pass
|
||||
metafunc = funcargs.Metafunc(func)
|
||||
metafunc.addcall({'x': 1})
|
||||
metafunc.parametrize('y', [2,3])
|
||||
assert len(metafunc._calls) == 2
|
||||
assert metafunc._calls[0].funcargs == {'x': 1, 'y': 2}
|
||||
assert metafunc._calls[1].funcargs == {'x': 1, 'y': 3}
|
||||
assert metafunc._calls[0].id == "0-2"
|
||||
assert metafunc._calls[1].id == "0-3"
|
||||
|
||||
def test_parametrize_indirect(self):
|
||||
def func(x, y): pass
|
||||
metafunc = funcargs.Metafunc(func)
|
||||
metafunc.parametrize('x', [1], indirect=True)
|
||||
metafunc.parametrize('y', [2,3], indirect=True)
|
||||
assert len(metafunc._calls) == 2
|
||||
assert metafunc._calls[0].funcargs == {}
|
||||
assert metafunc._calls[1].funcargs == {}
|
||||
assert metafunc._calls[0].params == dict(x=1,y=2)
|
||||
assert metafunc._calls[1].params == dict(x=1,y=3)
|
||||
|
||||
def test_addcalls_and_parametrize_indirect(self):
|
||||
def func(x, y): pass
|
||||
metafunc = funcargs.Metafunc(func)
|
||||
metafunc.addcall(param="123")
|
||||
metafunc.parametrize('x', [1], indirect=True)
|
||||
metafunc.parametrize('y', [2,3], indirect=True)
|
||||
assert len(metafunc._calls) == 2
|
||||
assert metafunc._calls[0].funcargs == {}
|
||||
assert metafunc._calls[1].funcargs == {}
|
||||
assert metafunc._calls[0].params == dict(x=1,y=2)
|
||||
assert metafunc._calls[1].params == dict(x=1,y=3)
|
||||
|
||||
def test_parametrize_functional(self, testdir):
|
||||
testdir.makepyfile("""
|
||||
def pytest_generate_tests(metafunc):
|
||||
metafunc.parametrize('x', [1,2], indirect=True)
|
||||
metafunc.parametrize('y', [2])
|
||||
def pytest_funcarg__x(request):
|
||||
return request.param * 10
|
||||
def pytest_funcarg__y(request):
|
||||
return request.param
|
||||
|
||||
def test_simple(x,y):
|
||||
assert x in (10,20)
|
||||
assert y == 2
|
||||
""")
|
||||
result = testdir.runpytest("-v")
|
||||
result.stdout.fnmatch_lines([
|
||||
"*test_simple*1-2*",
|
||||
"*test_simple*2-2*",
|
||||
"*2 passed*",
|
||||
])
|
||||
|
||||
def test_parametrize_onearg(self):
|
||||
metafunc = funcargs.Metafunc(lambda x: None)
|
||||
metafunc.parametrize("x", [1,2])
|
||||
assert len(metafunc._calls) == 2
|
||||
assert metafunc._calls[0].funcargs == dict(x=1)
|
||||
assert metafunc._calls[0].id == "1"
|
||||
assert metafunc._calls[1].funcargs == dict(x=2)
|
||||
assert metafunc._calls[1].id == "2"
|
||||
|
||||
def test_parametrize_onearg_indirect(self):
|
||||
metafunc = funcargs.Metafunc(lambda x: None)
|
||||
metafunc.parametrize("x", [1,2], indirect=True)
|
||||
assert metafunc._calls[0].params == dict(x=1)
|
||||
assert metafunc._calls[0].id == "1"
|
||||
assert metafunc._calls[1].params == dict(x=2)
|
||||
assert metafunc._calls[1].id == "2"
|
||||
|
||||
def test_parametrize_twoargs(self):
|
||||
metafunc = funcargs.Metafunc(lambda x,y: None)
|
||||
metafunc.parametrize(("x", "y"), [(1,2), (3,4)])
|
||||
assert len(metafunc._calls) == 2
|
||||
assert metafunc._calls[0].funcargs == dict(x=1, y=2)
|
||||
assert metafunc._calls[0].id == "1-2"
|
||||
assert metafunc._calls[1].funcargs == dict(x=3, y=4)
|
||||
assert metafunc._calls[1].id == "3-4"
|
||||
|
||||
class TestMetafuncFunctional:
|
||||
def test_attributes(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
# assumes that generate/provide runs in the same process
|
||||
@@ -1109,6 +1216,46 @@ class TestGenfuncFunctional:
|
||||
"*1 pass*",
|
||||
])
|
||||
|
||||
def test_parametrize_functional2(self, testdir):
|
||||
testdir.makepyfile("""
|
||||
def pytest_generate_tests(metafunc):
|
||||
metafunc.parametrize("arg1", [1,2])
|
||||
metafunc.parametrize("arg2", [4,5])
|
||||
def test_hello(arg1, arg2):
|
||||
assert 0, (arg1, arg2)
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
result.stdout.fnmatch_lines([
|
||||
"*(1, 4)*",
|
||||
"*(1, 5)*",
|
||||
"*(2, 4)*",
|
||||
"*(2, 5)*",
|
||||
"*4 failed*",
|
||||
])
|
||||
|
||||
def test_parametrize_and_inner_getfuncargvalue(self, testdir):
|
||||
p = testdir.makepyfile("""
|
||||
def pytest_generate_tests(metafunc):
|
||||
metafunc.parametrize("arg1", [1], indirect=True)
|
||||
metafunc.parametrize("arg2", [10], indirect=True)
|
||||
|
||||
def pytest_funcarg__arg1(request):
|
||||
x = request.getfuncargvalue("arg2")
|
||||
return x + request.param
|
||||
|
||||
def pytest_funcarg__arg2(request):
|
||||
return request.param
|
||||
|
||||
def test_func1(arg1, arg2):
|
||||
assert arg1 == 11
|
||||
""")
|
||||
result = testdir.runpytest("-v", p)
|
||||
result.stdout.fnmatch_lines([
|
||||
"*test_func1*1*PASS*",
|
||||
"*1 passed*"
|
||||
])
|
||||
|
||||
|
||||
def test_conftest_funcargs_only_available_in_subdir(testdir):
|
||||
sub1 = testdir.mkpydir("sub1")
|
||||
sub2 = testdir.mkpydir("sub2")
|
||||
@@ -1320,7 +1467,7 @@ def test_customized_python_discovery(testdir):
|
||||
"*CheckMyApp*",
|
||||
"*check_meth*",
|
||||
])
|
||||
|
||||
|
||||
result = testdir.runpytest()
|
||||
assert result.ret == 0
|
||||
result.stdout.fnmatch_lines([
|
||||
@@ -1354,7 +1501,7 @@ def test_customize_through_attributes(testdir):
|
||||
Function = MyFunction
|
||||
class MyClass(pytest.Class):
|
||||
Instance = MyInstance
|
||||
|
||||
|
||||
def pytest_pycollect_makeitem(collector, name, obj):
|
||||
if name.startswith("MyTestClass"):
|
||||
return MyClass(name, parent=collector)
|
||||
|
||||
@@ -160,6 +160,45 @@ class BaseFunctionalTests:
|
||||
#assert rep.failed.where.path.basename == "test_func.py"
|
||||
#assert rep.failed.failurerepr == "hello"
|
||||
|
||||
def test_teardown_final_returncode(self, testdir):
|
||||
rec = testdir.inline_runsource("""
|
||||
def test_func():
|
||||
pass
|
||||
def teardown_function(func):
|
||||
raise ValueError(42)
|
||||
""")
|
||||
assert rec.ret == 1
|
||||
|
||||
def test_exact_teardown_issue90(self, testdir):
|
||||
rec = testdir.inline_runsource("""
|
||||
import pytest
|
||||
|
||||
class TestClass:
|
||||
def test_method(self):
|
||||
pass
|
||||
def teardown_class(cls):
|
||||
raise Exception()
|
||||
|
||||
def test_func():
|
||||
pass
|
||||
def teardown_function(func):
|
||||
raise ValueError(42)
|
||||
""")
|
||||
reps = rec.getreports("pytest_runtest_logreport")
|
||||
print (reps)
|
||||
for i in range(2):
|
||||
assert reps[i].nodeid.endswith("test_method")
|
||||
assert reps[i].passed
|
||||
assert reps[2].when == "teardown"
|
||||
assert reps[2].failed
|
||||
assert len(reps) == 6
|
||||
for i in range(3,5):
|
||||
assert reps[i].nodeid.endswith("test_func")
|
||||
assert reps[i].passed
|
||||
assert reps[5].when == "teardown"
|
||||
assert reps[5].nodeid.endswith("test_func")
|
||||
assert reps[5].failed
|
||||
|
||||
def test_failure_in_setup_function_ignores_custom_repr(self, testdir):
|
||||
testdir.makepyfile(conftest="""
|
||||
import pytest
|
||||
|
||||
@@ -186,7 +186,7 @@ def test_setup_fails_again_on_all_tests(testdir):
|
||||
])
|
||||
assert "passed" not in result.stdout.str()
|
||||
|
||||
def test_setup_funcarg_setup_not_called_if_outer_scope_fails(testdir):
|
||||
def test_setup_funcarg_setup_when_outer_scope_fails(testdir):
|
||||
p = testdir.makepyfile("""
|
||||
import pytest
|
||||
def setup_module(mod):
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import pytest
|
||||
import sys
|
||||
|
||||
from _pytest.skipping import MarkEvaluator, folded_skips
|
||||
from _pytest.skipping import pytest_runtest_setup
|
||||
@@ -471,6 +472,21 @@ def test_reportchars(testdir):
|
||||
"SKIP*four*",
|
||||
])
|
||||
|
||||
def test_reportchars_error(testdir):
|
||||
testdir.makepyfile(
|
||||
conftest="""
|
||||
def pytest_runtest_teardown():
|
||||
assert 0
|
||||
""",
|
||||
test_simple="""
|
||||
def test_foo():
|
||||
pass
|
||||
""")
|
||||
result = testdir.runpytest('-rE')
|
||||
result.stdout.fnmatch_lines([
|
||||
'ERROR*test_foo*',
|
||||
])
|
||||
|
||||
@pytest.mark.xfail("hasattr(sys, 'pypy_version_info')")
|
||||
def test_errors_in_xfail_skip_expressions(testdir):
|
||||
testdir.makepyfile("""
|
||||
@@ -486,6 +502,10 @@ def test_errors_in_xfail_skip_expressions(testdir):
|
||||
pass
|
||||
""")
|
||||
result = testdir.runpytest()
|
||||
markline = " ^"
|
||||
if sys.platform.startswith("java"):
|
||||
# XXX report this to java
|
||||
markline = "*" + markline[8:]
|
||||
result.stdout.fnmatch_lines([
|
||||
"*ERROR*test_nameerror*",
|
||||
"*evaluating*skipif*expression*",
|
||||
@@ -493,7 +513,7 @@ def test_errors_in_xfail_skip_expressions(testdir):
|
||||
"*ERROR*test_syntax*",
|
||||
"*evaluating*xfail*expression*",
|
||||
" syntax error",
|
||||
" ^",
|
||||
markline,
|
||||
"SyntaxError: invalid syntax",
|
||||
"*1 pass*2 error*",
|
||||
])
|
||||
@@ -529,3 +549,10 @@ def test_direct_gives_error(testdir):
|
||||
])
|
||||
|
||||
|
||||
def test_default_markers(testdir):
|
||||
result = testdir.runpytest("--markers")
|
||||
result.stdout.fnmatch_lines([
|
||||
"*skipif(*conditions)*skip*",
|
||||
"*xfail(*conditions, reason=None, run=True)*expected failure*",
|
||||
])
|
||||
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user