Compare commits

...

221 Commits
3.3.1 ... 3.4.0

Author SHA1 Message Date
Bruno Oliveira
527845ef29 Changelog adjustments suggested during review 2018-01-30 19:49:08 +00:00
Bruno Oliveira
489e638b4e Preparing release version 3.4.0 2018-01-30 19:47:56 +00:00
Bruno Oliveira
13ee1cffed Suggest to update all dependencies when preparing releases 2018-01-30 19:44:06 +00:00
Bruno Oliveira
4c148bd0ef Fix imports in failure_demo.py 2018-01-30 19:44:06 +00:00
Bruno Oliveira
71a7b3c062 Merge pull request #3142 from thisch/only_colorize_levelname
Colorize live-log levelnames
2018-01-30 17:41:18 -02:00
Thomas Hisch
ebab1b6c69 live-logging: Colorize levelname 2018-01-30 18:21:12 +01:00
Bruno Oliveira
49773b573f Merge pull request #3132 from raphaelcastaneda/feature/add-record-xml-attribute
implement #3130 - add record_xml_attribute fixture
2018-01-29 15:42:55 -02:00
Ronny Pfannschmidt
32979def7d Merge pull request #3158 from nicoddemus/cache-getvalue
Replace deprecated option.getvalue by option.getoption in cache plugin
2018-01-29 13:57:45 +01:00
Bruno Oliveira
ab00c3e911 Add .pytest_cache directory to gitignore 2018-01-29 08:44:11 -02:00
Bruno Oliveira
6e4efccc38 Rename test_cache to test_cacheprovider for consistency with cacheprovider 2018-01-29 08:40:29 -02:00
Bruno Oliveira
269eeec702 Replace deprecated option.getvalue by option.getoption in cacheprovider 2018-01-29 08:40:29 -02:00
Bruno Oliveira
0e1be01b7a Merge pull request #3108 from cheezman34/features
Optimize reorder_items in fixtures.py
2018-01-27 23:21:09 -02:00
Bruno Oliveira
aff463a3c4 Merge remote-tracking branch 'upstream/master' into features 2018-01-27 12:28:43 -02:00
Bruno Oliveira
b3247c1d03 Merge pull request #3044 from RonnyPfannschmidt/parameterset-empty-enable-xfail
empty parameterset - enable opt to xfail
2018-01-27 12:23:49 -02:00
Bruno Oliveira
169635e889 Move example of empty_parameter_set_mark closer to the options 2018-01-27 11:02:32 -02:00
Bruno Oliveira
cd0b2ace67 Merge pull request #3150 from alanbato/pytest_cache
Change cache directory structure to include `pytest`
2018-01-27 10:08:56 -02:00
Ronny Pfannschmidt
17a1ed5edf use a constant to sort out repeated use of the EMPTY_PARAMETERSET_OPTION 2018-01-26 12:12:26 +01:00
Ronny Pfannschmidt
a54cd4c2fd correct testing and usage of the empty_parameter_set_mark config option 2018-01-26 12:05:52 +01:00
Ronny Pfannschmidt
77de45cce3 enhance docs for empty_parameter_set_mark according to review comments 2018-01-26 12:01:27 +01:00
Ronny Pfannschmidt
d550c33cd0 s/empty_parameterset/empty_parameter_set_mark 2018-01-26 11:56:24 +01:00
Bruno Oliveira
a58099022a Merge pull request #3124 from nicoddemus/logging-3013
Changes in the logging plugin for 3.4
2018-01-26 08:52:43 -02:00
Ronny Pfannschmidt
3bc7ced97a Merge pull request #3153 from brianmaissy/document_not_to_call_main_multiple_times
Added note that calling pytest.main multiple times from the same process is not recommended because of import caching
2018-01-26 11:47:16 +01:00
Ronny Pfannschmidt
8979b2a9d7 document empty_parameterset in customize.rst 2018-01-26 11:26:48 +01:00
Ronny Pfannschmidt
d4c11e58aa exted empty parameterset check with reason test 2018-01-26 11:18:50 +01:00
Ronny Pfannschmidt
7f83605c81 fix empty parameterset tests by mocking a config object 2018-01-26 11:10:00 +01:00
Ronny Pfannschmidt
37b41de779 fix #2527 - introduce a option to pic the empty parameterset action 2018-01-26 11:10:00 +01:00
Ronny Pfannschmidt
bf2c10c810 parameterset: refactor marking empty parametersets 2018-01-26 11:10:00 +01:00
Brian Maissy
4285325cb8 Added note that calling pytest.main multiple times from the same process is not recommended because of import caching 2018-01-25 23:07:34 +02:00
Bruno Oliveira
cbbd606b6c Reword changelog 2018-01-25 17:21:54 -02:00
Alan Velasco
a24ca9872f Change cache directory name to include pytest 2018-01-25 07:50:08 -06:00
Bruno Oliveira
b8be339632 Merge pull request #3148 from nicoddemus/deprecate-old-style-classes-2147
All classes now subclass object for better py3 compatibility
2018-01-25 10:41:28 -02:00
Bruno Oliveira
2aad8c0fce Merge pull request #3147 from nicoddemus/issue-3103
'-o' option no longer swallows all other non-options after it
2018-01-25 10:41:18 -02:00
Bruno Oliveira
15cbd61159 Change caplog.get_handler(when) to caplog.get_records(when)
While updating the docs I noticed that caplog.get_handler() exposes
the underlying Handler object, which I think it is a bit too much
detail at this stage. Update to return the records directly instead.
2018-01-24 19:08:49 -02:00
Bruno Oliveira
2f955e0c99 Update documentation: rewording and move things for better reading flow 2018-01-24 18:42:59 -02:00
Bruno Oliveira
af37778b0d All classes now subclass object for better py3 compatibility
Fix #2147
2018-01-24 18:23:42 -02:00
Bruno Oliveira
3f5e9ea71e Fix -o behavior to no longer swallow all remaining options
The current behavior was too error-prone because a "-o" option would
swallow all the following non-option parameters:

  pytest -o foo=bar path/to/test.py

path/to/test.py would be captured by the -o option, and would fail
because "path/to/test.py" is not in the format "key=value".
2018-01-23 21:19:16 -02:00
Bruno Oliveira
443275f025 Reword changelog a bit 2018-01-23 21:19:04 -02:00
Bruno Oliveira
8426c57a9e Ensure changes in the message in the future do not make the test pass by accident 2018-01-23 21:18:59 -02:00
Aron Coyle
30ca9f9d38 Add endline 2018-01-23 21:18:51 -02:00
Aron Coyle
46d87deb5d Add changelog update authors 2018-01-23 21:18:45 -02:00
Aron Coyle
203508d9f3 cleanup test cases 2018-01-23 21:18:37 -02:00
Andrew Toolan
2c7f94fdb9 Added basic fix and test 2018-01-23 21:18:10 -02:00
Bruno Oliveira
ff90c9e237 Merge pull request #3116 from brianmaissy/pexpect_test_hangs_on_mac
use flush in order to avoid hanging on mac
2018-01-23 19:32:19 -02:00
Bruno Oliveira
b4e8861aa5 Fix typos 2018-01-23 19:02:32 -02:00
Bruno Oliveira
baa189f5a3 Merge pull request #3131 from soyrice/master
Improve readability of Getting Started guide
2018-01-23 17:48:11 -02:00
Bruno Oliveira
113bfb6be8 Report 'call' phase as 'live log call'
As commented in review, this makes it consistent with the headers shown
by stdout/stderr capturing ("Captured log call")
2018-01-22 21:43:35 -02:00
Bruno Oliveira
9f4688e549 Remove unnecessary -s from test_log_cli_enabled_disabled 2018-01-22 21:26:14 -02:00
Bruno Oliveira
3a9d0b26d5 Use pytest_runtest_logstart to signal the start of a new test
This also simplifies the code a bit because we don't need to keep
a set of ids anymore
2018-01-22 21:20:48 -02:00
Raphael Castaneda
a5e60b6a2d implement #3130 - adding record_xml_attribute fixture
update incorrect expected attribute value in test_record_attribute

attr names must be strings

Update CHANGELOG formatting

update usage documentation

Fix versionadded for record_xml_attribute

Indent the xml schema properly inside the warning box in the docs
2018-01-22 15:14:53 -08:00
Bruno Oliveira
0df42b4426 Show a header for each testing phase during live logging
As suggested during review
2018-01-22 21:00:52 -02:00
Bruno Oliveira
0d96a5bf90 Merge pull request #3135 from brianmaissy/doc/clarify_pytest_fixture_setup_with_fixture_that_returns_None
Clarify a possible confusion when using pytest_fixture_setup with fixture functions that return None
2018-01-22 18:02:33 -02:00
Bruno Oliveira
060f68bd90 Merge pull request #3137 from brianmaissy/doc/note_that_certain_hooks_cant_be_used_with_hookwrapper
Document hooks (defined with historic=True) which cannot be used with hookwrapper=True
2018-01-22 18:00:35 -02:00
Bruno Oliveira
e5739a3115 Merge pull request #3136 from brianmaissy/doc/document_warning_filtering_behavior
Clarify that warning capturing doesn't change the warning filter by d…
2018-01-22 17:45:36 -02:00
Bruno Oliveira
8a8797df80 Small changelog formatting 2018-01-21 23:21:21 -02:00
Brian Maissy
8994603d46 Document hooks (defined with historic=True) which cannot be used with hookwrapper=True 2018-01-21 23:17:16 +02:00
Brian Maissy
5c0b340a4b Clarify that warning capturing doesn't change the warning filter by default 2018-01-21 22:43:00 +02:00
Brian Maissy
196dcc37a8 Clarify a possible confusion when using pytest_fixture_setup with fixture functions that return None 2018-01-21 21:50:26 +02:00
Cyrus Maden
0ab57c4139 Typo fix: "handeling" --> "handling" 2018-01-20 11:12:59 -08:00
Bruno Oliveira
29a7b5e064 Initialize log_cli_handler to None during LoggingPlugin init
Some of testdir's functionality bypasses pytest_runtestloop so this
attribute needs to be set early
2018-01-20 14:19:45 -02:00
Bruno Oliveira
27ae270159 Mention in docs that log messages of level WARNING or above are captured 2018-01-20 12:08:51 -02:00
Bruno Oliveira
2e40a8b3ca Fix test_caplog_captures_for_all_stages by setting log level 2018-01-20 12:04:28 -02:00
Bruno Oliveira
18e053546c Use six.StringIO and __name__ in test_live_logging_suspends_capture 2018-01-20 12:02:55 -02:00
Bruno Oliveira
9dbcac9af3 Suspend stdout/stderr capturing when emitting live logging messages 2018-01-20 12:02:55 -02:00
Bruno Oliveira
4a436572a8 Simplify test assertions a bit 2018-01-20 12:02:55 -02:00
Bruno Oliveira
97a4967b03 Improve code formatting 2018-01-20 12:02:55 -02:00
Bruno Oliveira
8f6a5928f7 Add newline before log messages and enable -v output when log_cli is enabled 2018-01-20 12:02:55 -02:00
Bruno Oliveira
5d89a93977 Small improvements to tests suggested during review 2018-01-20 12:02:54 -02:00
Bruno Oliveira
c53b72fd7b Add CHANGELOG for 3013 2018-01-20 12:02:54 -02:00
Bruno Oliveira
6bb739516f Update logging docs with the new changes in 3.4
Ref: #3013
2018-01-20 12:02:54 -02:00
Bruno Oliveira
8d735f3e1d Live log option now writes to the terminal reporter
Ref: #3013
2018-01-20 12:01:42 -02:00
Bruno Oliveira
aca1b06747 Undo log level set by caplog.set_level at the end of the test
Otherwise this leaks the log level information to other tests

Ref: #3013
2018-01-20 12:01:42 -02:00
Bruno Oliveira
8dcd2718aa No longer change the level of any logger unless requested explicitly
Ref: #3013
2018-01-20 12:01:42 -02:00
Bruno Oliveira
5ad1313b8a log_cli must now be enabled explicitly
Ref: #3013
2018-01-20 12:00:46 -02:00
Bruno Oliveira
3b3d237f07 Merge pull request #3117 from boxed/access_logs_in_teardown
Access captures logs in teardown
2018-01-20 11:21:17 -02:00
Bruno Oliveira
c4c968fe69 Reword CHANGELOG after introduction of caplog.get_handler() 2018-01-20 11:14:09 -02:00
Cyrus Maden
0b6df94b12 Delete .DS_Store 2018-01-19 18:45:02 -08:00
Cyrus Maden
c3d420bf75 Delete .DS_Store 2018-01-19 18:44:46 -08:00
Cyrus Maden
ebb4c47155 Delete .DS_Store 2018-01-19 18:44:33 -08:00
Anders Hovmöller
7ea5a22657 Access captures logs in teardown 2018-01-19 12:42:35 +01:00
Cyrus Maden
cd76366d87 Rename pr.doc to 3131.doc 2018-01-18 15:59:37 -08:00
Cyrus Maden
931e8830ba Update changelog
Not issue ID. Will update with pr ID after submitting pr
2018-01-18 15:54:31 -08:00
Bruno Oliveira
621374679b Merge pull request #3129 from kimberlythegeek/typo_in_logging_doc
Typo in logging doc
2018-01-18 16:40:09 -02:00
Bruno Oliveira
8be1136d03 Small changelog formatting 2018-01-18 16:40:00 -02:00
Kimberly
e0b63e34fa fixed typo in logging doc and added fix to changelog 2018-01-18 10:40:18 -07:00
Florian Bruhin
1fd67c9000 Merge pull request #3121 from feuillemorte/2953-keyword-expressions-error
#2953 show a simple and easy error when keyword expressions trigger a syntax error
2018-01-17 19:00:22 +01:00
Bruno Oliveira
e3406e0818 Show usage errors in red 2018-01-16 19:35:32 -02:00
feuillemorte
dc79116de3 Merge branch '2953-keyword-expressions-error' of github.com:feuillemorte/pytest into 2953-keyword-expressions-error 2018-01-16 23:36:40 +03:00
feuillemorte
8433e2ba04 #2953 fix comments: fix exception type 2018-01-16 23:35:57 +03:00
Bruno Oliveira
86e1b44230 Improve changelog formatting 2018-01-16 18:10:15 -02:00
feuillemorte
5d3f7d7142 Merge branch '2953-keyword-expressions-error' of github.com:feuillemorte/pytest into 2953-keyword-expressions-error 2018-01-16 22:56:23 +03:00
feuillemorte
648d5d0c6b #2953 fix comments: use keyword module 2018-01-16 22:55:24 +03:00
feuillemorte
dff597dcd0 Add changelog entry 2018-01-16 21:34:13 +03:00
feuillemorte
076fb56f85 show a simple and easy error when keyword expressions trigger a syntax error 2018-01-16 21:30:44 +03:00
Bruno Oliveira
150537d5e0 Merge pull request #3119 from PoppyBagel/fix-formatting
[doc] Fix wrong formatting in parametrize.rst
2018-01-16 08:33:48 -02:00
Kate
d8c23fd39b Fix wrong formatting 2018-01-16 12:36:28 +03:00
Cyrus Maden
b748576358 Add name 2018-01-15 13:36:44 -08:00
Cyrus Maden
f555a3a76c Update getting started guide
Proofread; added intro paragraph under first header to orient new users; fixed grammar errors (switched to active voice, actionable directions, etc) to improve readability
2018-01-15 13:27:10 -08:00
Cyrus Maden
1f4831a23f Update getting-started.rst 2018-01-15 12:28:21 -08:00
Brian Maissy
0a0d97aeb5 added changelog news fragment 2018-01-14 23:14:59 +02:00
Brian Maissy
4a3863c2e2 use flush in order to avoid hanging on mac 2018-01-14 23:00:23 +02:00
Aaron
d314691fd3 more descriptive changelog message 2018-01-12 10:29:18 -08:00
Ronny Pfannschmidt
01e37fe892 Merge pull request #3110 from nicoddemus/progress-teardown-3088
Fix progress report when tests fail during teardown
2018-01-12 11:09:01 +01:00
Bruno Oliveira
abbdb60051 Move logic determining if progress should be displayed to a function 2018-01-12 07:04:43 -02:00
Bruno Oliveira
5939b336cd Fix progress report when tests fail during teardown
Fix #3088
2018-01-11 20:42:05 -02:00
Aaron
3d289b803d update AUTHORS and changelog 2018-01-11 12:25:41 -08:00
Aaron
4a704bbb55 fix reorder_items_atscope ordering 2018-01-11 11:30:52 -08:00
Aaron
ee6c9f50a2 optimize fixtures.reorder_items 2018-01-11 11:30:52 -08:00
Bruno Oliveira
3181718fe0 Merge pull request #3087 from nicoddemus/deprecation-roadmap
Update deprecation/removal docs to point to labels/milestones instead
2018-01-11 11:14:47 -02:00
Bruno Oliveira
2674f352e8 Merge pull request #3091 from nicoddemus/conftest-hooks
Document bootstrap and initialization hooks
2018-01-11 11:14:20 -02:00
Bruno Oliveira
6fb46a0e79 Merge pull request #3104 from pbrod/patch-1
Update Freezing pytest description in simple.rst
2018-01-10 14:19:25 -02:00
Per A. Brodtkorb
820ea6d68f Update Freezing pytest description in simple.rst
I have trouble using third party plugins in my frozen program and discovered 
that other people have experienced it as well:

    https://mail.python.org/pipermail//pytest-dev/2015-June/003015.html

The problem is that the mechanism for plugin discovery used by pytest
(setupttools entry points) doesn't work with frozen executables so pytest
can't find any plugins. The solution seems to be to import the third party 
plugins explicitly as shown here:

    https://mail.python.org/pipermail//pytest-dev/2015-June/003018.html

This is not mentioned anywhere in the documentaion.
2018-01-10 16:44:26 +01:00
Ronny Pfannschmidt
b0032ba2b3 Merge pull request #3102 from nicoddemus/logfinish-hook-3101
Add new pytest_runtest_logfinish hook
2018-01-10 07:57:35 +01:00
Ronny Pfannschmidt
cf9b31bd5a Merge pull request #3068 from asottile/pystd
Replace py.std with stdlib imports
2018-01-10 07:52:55 +01:00
Bruno Oliveira
b68b80aec9 Add new pytest_runtest_logfinish hook
Fix #3101
2018-01-09 22:17:39 -02:00
Anthony Sottile
bd1d17e8de Replace py.std with stdlib imports 2018-01-09 12:44:10 -08:00
Bruno Oliveira
93306f6a5e Merge remote-tracking branch 'upstream/master' into features 2018-01-09 18:41:00 -02:00
Bruno Oliveira
962aede290 Merge pull request #3096 from nicoddemus/import-warnings
Ignore ImportWarnings regarding package resolution
2018-01-09 18:40:33 -02:00
Ronny Pfannschmidt
a8d3d329ec Merge pull request #3098 from uranusjr/pep426-setuptools-36.2.1
Add support for new environment marker usages
2018-01-09 18:21:06 +01:00
Tzu-ping Chung
b256cd2a6a Add support for new environment marker usages 2018-01-09 21:34:11 +08:00
Bruno Oliveira
b6b36bc167 Handle pluggy package or module for traceback filtering
Since 0.6.1 pluggy has been turned into a package
2018-01-08 21:29:15 -02:00
Bruno Oliveira
3dd24f8d21 Ignore ImportWarnings regarding package resolution
The problem is described/discussed in #3061

Ideally this should be a temporary solution until we find a proper one
which gets rid of the warning
2018-01-08 21:11:08 -02:00
Bruno Oliveira
794fb193ba Merge pull request #3093 from cryvate/fix-issue-2985-blurb
Fix issue 2985 blurb in changelog
2018-01-07 01:18:18 -02:00
Henk-Jaap Wagenaar
d7e1f037d9 Fix issue 2985 blurb in changelog 2018-01-06 22:15:06 +00:00
Bruno Oliveira
29ff9301d8 Merge pull request #3092 from jdufresne/readthedocs
Prefer https://*.readthedocs.io over http://*.rtfd.org
2018-01-06 15:53:10 -02:00
Bruno Oliveira
f3c666db3c Small formatting update in CHANGELOG 2018-01-06 14:08:15 -02:00
Jon Dufresne
b93aa5e35f Prefer https://*.readthedocs.io over http://*.rtfd.org
- Requests URL using https instead of http
- Avoids unnecessary redirect of *.rtfd.org -> *.readthedocs.io

*.rtfd.org exists as a means for pasting short URLs, which doesn't much
 apply for links in documentation.
2018-01-06 08:05:42 -08:00
Bruno Oliveira
bc66f7e43f Merge pull request #3075 from elliterate/bugs/fix-skipping-plugin-reporting
Assume not skipped by mark if attribute missing
2018-01-06 13:56:07 -02:00
Bruno Oliveira
cb6b851780 Update deprecation/removal docs to point to labels/milestones instead
Using milestones and proper issues are a much saner way to handle these topics
than keeping them in sync in a separate document
2018-01-06 13:48:39 -02:00
Bruno Oliveira
afb8a4e35d Document bootstrap and initialization hooks
Fix #2616
2018-01-06 13:31:38 -02:00
Bruno Oliveira
06a182386b Merge pull request #3073 from nicoddemus/pytest_runtestloop-docs
Add pytest_runtestloop to the docs
2018-01-05 20:35:51 -02:00
Bruno Oliveira
fac07c1b3f Merge pull request #3076 from pafonta/patch-1
[doc] Reword strange sentence on doctest flags
2018-01-05 09:49:54 -02:00
Bruno Oliveira
e8c0ca4f08 Merge pull request #3064 from nicoddemus/release-3.3.2
Preparing release version 3.3.2
2018-01-04 20:42:51 -02:00
Pierre-Alexandre Fonta
ac6f257efc Create news fragment for the pull request 2018-01-04 14:58:30 +01:00
Pierre-Alexandre Fonta
554cb8d09c Reword strange sentence on doctest flags 2018-01-04 14:47:05 +01:00
Ian Lesperance
25b504b4f0 Add self to authors list 2018-01-03 18:54:49 -05:00
Ian Lesperance
0a6e086f9d Add changelog entry 2018-01-03 18:53:32 -05:00
Ian Lesperance
f24c470403 Assume not skipped by mark if attribute missing
Fixes #3074.
2018-01-03 18:47:18 -05:00
Bruno Oliveira
52a7ccef57 Add pytest_runtestloop to the docs 2018-01-03 16:58:57 -02:00
Bruno Oliveira
bd2d0d2c3c Preparing release version 3.3.2 2017-12-25 20:38:27 +00:00
Bruno Oliveira
08997279f4 Merge pull request #3059 from asottile/yesqa
Remove unnecessary `# noqa` comments.
2017-12-22 17:03:02 -02:00
Anthony Sottile
205e29d843 Remove unnecessary # noqa comments.
Commit automated with https://github.com/asottile/yesqa
2017-12-22 08:29:48 -08:00
Ronny Pfannschmidt
672c901c70 Merge pull request #2784 from RonnyPfannschmidt/fix-fdleaks
pytester: ignore files used to obtain current user metadata
2017-12-19 17:08:53 +01:00
Bruno Oliveira
c70efaa0fb Merge pull request #3047 from RonnyPfannschmidt/mark-newapi
move node base classes from main to nodes
2017-12-19 07:59:25 -02:00
Ronny Pfannschmidt
d370e7788d Merge pull request #2068 from flub/commitaccess
[proposal] Add section about how you become a committer
2017-12-18 18:20:49 +01:00
Ronny Pfannschmidt
0d83dd1b31 Merge pull request #3016 from jurko-gospodnetic/clean-up-state-after-in-process-pytest-runs
Clean up state after in process pytest runs
2017-12-18 17:56:24 +01:00
Ronny Pfannschmidt
ed293ec3e9 pytester: no ignore files used to obtain current user metadata in the fd leak detector 2017-12-18 17:52:55 +01:00
Ronny Pfannschmidt
2f8427bb4e Merge pull request #3043 from awbdallas/parameterize_documentation_fix
Parameterize documentation fix
2017-12-18 17:48:41 +01:00
Ronny Pfannschmidt
94608c6110 port _Compatproperty to attrs 2017-12-18 11:08:20 +01:00
Ronny Pfannschmidt
afc607cfd8 move node base classes from main to nodes 2017-12-18 11:08:20 +01:00
Florian Bruhin
30729b7c3c Merge pull request #3045 from thisch/revert_accidental_push
Revert accidental push
2017-12-17 22:06:38 +01:00
Thomas Hisch
dfc5399cd7 Revert "WIP #3013"
This reverts commit 28a93b9eeb.
2017-12-17 21:50:17 +01:00
Thomas Hisch
76489d30f7 Revert "WIP"
This reverts commit d6f75d2836.
2017-12-17 21:49:57 +01:00
Thomas Hisch
d6f75d2836 WIP 2017-12-17 20:11:24 +01:00
Jurko Gospodnetić
d85a3ca19a add changelog entry 2017-12-17 12:47:50 +01:00
Jurko Gospodnetić
f3c9c6e8a8 fix restoring Python state after in-process pytest runs
Now each in-process pytest run saves a snapshot of important global Python
state and restores it after the test completes, including the list of loaded
modules & the Python path settings.

Previously only the loaded package data was getting restored, but that was
also reverting any loaded package changes done in the test triggering the
pytest runs, and not only those done by the pytest runs themselves.

Updated acceptance tests broken by this change, which were only passing before
by accident as they were making multiple pytest runs with later ones depending
on sys.path changes left behind by the initial one.
2017-12-17 12:47:50 +01:00
Jurko Gospodnetić
67bd60d5c6 clean up Testdir taking snapshots & restoring global Python state
Now extracted to new CwdSnapshot, SysModulesSnapshot & SysPathsSnapshot
classes, each saving the state they are interested in on instantiation
and restoring it in its `restore()` method.
2017-12-17 12:47:50 +01:00
Thomas Hisch
28a93b9eeb WIP #3013 2017-12-17 06:54:57 +01:00
Austin
70461d1ead Update parametrize.rst 2017-12-16 09:31:48 -06:00
Austin
73eccb4c36 Adding news fragment 2017-12-16 09:21:27 -06:00
Austin
d7a76a4d07 Updated for more clarity
- Changed original wording for parameterize. Should help be a little more clear
2017-12-16 09:04:18 -06:00
Bruno Oliveira
924b5e2e3d Merge pull request #3031 from nicoddemus/symlink-privileges
Check if symlink does have privileges on Windows for test_cmdline_python_package_symlink
2017-12-16 12:36:11 -02:00
Bruno Oliveira
d87279115d Merge pull request #3041 from segevfiner/capture-no-disable-progress
Use classic console output when -s is used
2017-12-16 12:34:34 -02:00
Bruno Oliveira
0a2735a275 Reword changelog entry 2017-12-16 12:33:34 -02:00
Austin
c90e76c371 Clarifiyng documentation for parameterize
- Added snippet dealing with parameterize run order of tests
2017-12-16 08:25:02 -06:00
Segev Finer
370daf0441 Use classic console output when -s is used
Fixes #3038
2017-12-16 15:00:23 +02:00
Bruno Oliveira
586ecea6f2 Check if symlink does not privileges on Windows for test_cmdline_python_package_symlink
os.symlink might fail on Windows because users require a special policy
to create symlinks (argh).

This is not a problem on AppVeyor because it is logged in as an admin,
but would be surprising for Windows users running pytest's test
suite on their computer.
2017-12-14 19:23:15 -02:00
Bruno Oliveira
db4df5833a Merge pull request #3030 from nicoddemus/leak
Fix memory leak caused by fixture values never been garbage collected
2017-12-14 19:20:34 -02:00
Bruno Oliveira
b17c6e5f89 Merge pull request #3028 from thisch/removelogger
Remove logger parameter from catching_logs
2017-12-13 20:56:48 -02:00
Bruno Oliveira
c3f63ac143 Fix memory leak caused by fixture values never been garbage collected
The leak was caused by the (unused) `FixtureRequest._fixture_values`
cache.

This was introduced because the `partial` object (created to call
FixtureDef.finish() bound with the Request) is kept alive
through the entire session when a function-scoped fixture depends
on a session-scoped (or higher) fixture because of the nested
`addfinalizer` calls.

FixtureDef.finish() started receiving a request object in order to
obtain the proper hook proxy object (#2127), but this does not seem
useful at all in practice because `pytest_fixture_post_finalizer`
will be called with the `request` object of the moment the fixture value
was *created*, not the request object active when the fixture value
is being destroyed. We should probably deprecate/remove the request
parameter from `pytest_fixture_post_finalizer`.

Fix #2981
2017-12-13 19:49:06 -02:00
Thomas Hisch
3862b0b28d Remove logger parameter from catching_logs
The logger parameter of catching_logs is not used anywhere. The main
motivation for removing the logger parameter is that it removes the

logger = logger or logging.getLogger(logger)

line. IMO there are too many occurences of the string 'logg' ;)
2017-12-13 21:13:59 +01:00
Ronny Pfannschmidt
476d4df1b7 Merge pull request #3010 from cryvate/fix-issue-2985
Improve handling of pyargs
2017-12-13 13:56:42 +01:00
Ronny Pfannschmidt
52449903c3 Merge pull request #3023 from nicoddemus/preparse-deprecated
Add param annotations and types to hookspec
2017-12-13 13:51:20 +01:00
Ronny Pfannschmidt
506c9c91c0 Merge pull request #3025 from anntzer/dont-rewrite-plugin
Respect PYTEST_DONT_REWRITE for plugins too.

closes #2995
2017-12-13 12:34:51 +01:00
Bruno Oliveira
38f34e2fa1 Merge pull request #3012 from thisch/merge_contextmanagers
Integrate logging_using_handler into catching_logs
2017-12-13 06:59:48 -02:00
Bruno Oliveira
ba41015ef6 Merge pull request #3021 from jurko-gospodnetic/cleanup
Code cleanup
2017-12-13 06:58:18 -02:00
Bruno Oliveira
ebfc1c49d1 Fix changelog formatting 2017-12-13 06:58:07 -02:00
Antony Lee
45e7734b1a Change set ops to use methods instead of operators. 2017-12-13 00:54:57 -08:00
Bruno Oliveira
8ce6e39b1c Small formatting to CHANGELOG 2017-12-13 06:52:37 -02:00
Antony Lee
c8e7d1ae34 Respect PYTEST_DONT_REWRITE for plugins too. 2017-12-12 17:43:17 -08:00
Bruno Oliveira
7b5d4d01ed Add param annotations and types to hookspec
Also mention which hook to use instead of the deprecated
pytest_cmdline_preparse

Fix #3022
2017-12-12 18:01:31 -02:00
Jurko Gospodnetić
a4f4579f19 add changelog entry 2017-12-12 13:41:31 +01:00
Jurko Gospodnetić
e4da9bacdf fix actial --> actual typo 2017-12-12 12:55:32 +01:00
Henk-Jaap Wagenaar
1e295535c3 Move _patched_find_module to module namespace. 2017-12-12 09:53:06 +00:00
Henk-Jaap Wagenaar
3ca1e4b7f0 Make patch for issue in pkgutil.ImpImporter local by using contextmanager. 2017-12-12 08:43:01 +00:00
Henk-Jaap Wagenaar
dc19624248 Improve testing comments and code of issue 2985 (symlink in pyargs). 2017-12-12 08:26:40 +00:00
Bruno Oliveira
f8f1a52ea0 Merge remote-tracking branch 'upstream/master' into features 2017-12-11 22:18:50 -02:00
Bruno Oliveira
5c6d7739bc Merge pull request #3015 from jurko-gospodnetic/code-cleanup
Code cleanup
2017-12-11 22:15:50 -02:00
Bruno Oliveira
771b5c8852 Merge pull request #3019 from srinivasreddy/rm_ast
remove '_ast' module; and redirect '_ast' references to 'ast'
2017-12-11 22:12:40 -02:00
Bruno Oliveira
fc5ec5807e Add changelog for 3018 2017-12-11 17:40:35 -02:00
Srinivas Reddy Thatiparthy
fc544dc660 remove redundant checking because
* isinstance([], Sequence) is True

* isinstance((), Sequence) is True
2017-12-12 00:10:59 +05:30
Srinivas Reddy Thatiparthy
7792587b3f remove unnecessary import 2017-12-12 00:03:38 +05:30
Srinivas Reddy Thatiparthy
c2cd239d35 remove _ast reference from test cases as well 2017-12-11 23:24:47 +05:30
Srinivas Reddy Thatiparthy
cb0ba18f53 remove '_ast' module; and redirect '_ast' references to 'ast' 2017-12-11 23:14:17 +05:30
Jurko Gospodnetić
3b85e0c3a9 simplify test_conftest_found_with_double_dash() test code 2017-12-11 15:10:04 +01:00
Jurko Gospodnetić
73bc6bacfa add changelog entry 2017-12-09 13:34:58 +01:00
Jurko Gospodnetić
8e8a953ac6 fix test name typo 2017-12-09 13:34:58 +01:00
Jurko Gospodnetić
852b96714e use spaces consistently instead of some of them being   2017-12-09 13:34:57 +01:00
Jurko Gospodnetić
dd64f1a4a9 fix tox.ini comment typos 2017-12-09 13:34:57 +01:00
Jurko Gospodnetić
41a6ec6f31 touch up test_cmdline_python_namespace_package() test code 2017-12-09 13:34:56 +01:00
Jurko Gospodnetić
7feab7391d simplify test_pdb_collection_failure_is_shown test data setup code 2017-12-09 13:34:56 +01:00
Jurko Gospodnetić
f0bfe9de3d shorten some test code 2017-12-09 13:34:56 +01:00
Jurko Gospodnetić
596937e610 remove extra whitespace 2017-12-09 13:34:55 +01:00
Jurko Gospodnetić
57fcd3f57e remove corpse code comments 2017-12-09 13:34:55 +01:00
Jurko Gospodnetić
65f5383106 fix comment & docstring typos, line wrapping & wording 2017-12-09 13:34:54 +01:00
Ronny Pfannschmidt
964c29cb93 Merge pull request #3014 from nicoddemus/cap-named-tuple
Change capture docs to use namedtuple
2017-12-09 09:38:39 +01:00
Bruno Oliveira
38fb6aae78 Change capture docs to use namedtuple 2017-12-08 18:34:29 -02:00
Bruno Oliveira
1c5b887dfd Fix spelling of pytest in CHANGELOG 2017-12-08 09:04:42 -02:00
Thomas Hisch
ba209b5230 Integrate logging_using_handler into catching_logs
logging_using_handler is only used in catching_logs. Therefore it makes
sense to remove one of the many context managers from the logging
plugin.
2017-12-07 16:34:53 +01:00
Henk-Jaap Wagenaar
b62fd79c0c Fix issue 2985. 2017-12-07 15:12:44 +00:00
Bruno Oliveira
655146e522 Merge remote-tracking branch 'upstream/master' into features 2017-12-05 22:30:35 -02:00
Bruno Oliveira
88f2cc9b64 Small formatting fixes in CHANGELOG 2017-12-05 22:28:56 -02:00
Bruno Oliveira
ed2bb9d723 Merge pull request #2997 from nicoddemus/release-3.3.1
Preparing release version 3.3.1
2017-12-05 22:21:41 -02:00
Bruno Oliveira
294729962d Merge pull request #2976 from st--/master
Extend _pytest.python._idval to return __name__ of functions as well
2017-11-30 18:01:21 -02:00
ST John
652936f47f make linter happier 2017-11-30 10:29:05 +00:00
ST John
1fe2e2cb03 Merge branch 'master' of https://github.com/st--/pytest 2017-11-30 10:19:38 +00:00
ST John
e66473853c add test 2017-11-30 10:19:29 +00:00
Bruno Oliveira
fdd4abb88a Small rewording of the CHANGELOG 2017-11-29 21:11:34 -02:00
ST John
5085aa2bce add changelog file 2017-11-29 16:30:34 +00:00
ST John
912330a7e2 Extend _pytest.python._idval to return __name__ of functions as well, not just for classes 2017-11-29 16:17:49 +00:00
Floris Bruynooghe
1f0d06641a Update text to only give access when wanted
Also clarify merging perms given the PR approval now available.
2017-02-19 15:16:34 -03:00
Floris Bruynooghe
4c62cd451a Add section about how you become a committer 2016-11-17 21:25:22 +00:00
87 changed files with 2990 additions and 1355 deletions

1
.gitignore vendored
View File

@@ -33,6 +33,7 @@ env/
3rdparty/
.tox
.cache
.pytest_cache
.coverage
.ropeproject
.idea

View File

@@ -3,12 +3,15 @@ merlinux GmbH, Germany, office at merlinux eu
Contributors include::
Aaron Coleman
Abdeali JK
Abhijeet Kasurde
Ahn Ki-Wook
Alan Velasco
Alexander Johnson
Alexei Kozlenok
Anatoly Bubenkoff
Anders Hovmöller
Andras Tim
Andreas Zeidler
Andrzej Ostrowski
@@ -17,6 +20,7 @@ Anthon van der Neut
Anthony Sottile
Antony Lee
Armin Rigo
Aron Coyle
Aron Curzon
Aviv Palivoda
Barney Gale
@@ -38,6 +42,7 @@ Christian Boelsen
Christian Theunert
Christian Tismer
Christopher Gilling
Cyrus Maden
Daniel Grana
Daniel Hahler
Daniel Nuri
@@ -74,9 +79,11 @@ Grig Gheorghiu
Grigorii Eremeev (budulianin)
Guido Wesdorp
Harald Armin Massa
Henk-Jaap Wagenaar
Hugo van Kemenade
Hui Wang (coldnight)
Ian Bicking
Ian Lesperance
Jaap Broekhuizen
Jan Balster
Janne Vanhala
@@ -147,6 +154,7 @@ Punyashloka Biswal
Quentin Pradet
Ralf Schmitt
Ran Benita
Raphael Castaneda
Raphael Pierzina
Raquel Alegre
Ravi Chandra
@@ -177,6 +185,7 @@ Tom Dalton
Tom Viner
Trevor Bekolay
Tyler Goodlet
Tzu-ping Chung
Vasily Kuznetsov
Victor Uriarte
Vidar T. Fauske

View File

@@ -8,6 +8,175 @@
.. towncrier release notes start
Pytest 3.4.0 (2018-01-30)
=========================
Deprecations and Removals
-------------------------
- All pytest classes now subclass ``object`` for better Python 2/3 compatibility.
This should not affect user code except in very rare edge cases. (`#2147
<https://github.com/pytest-dev/pytest/issues/2147>`_)
Features
--------
- Introduce ``empty_parameter_set_mark`` ini option to select which mark to
apply when ``@pytest.mark.parametrize`` is given an empty set of parameters.
Valid options are ``skip`` (default) and ``xfail``. Note that it is planned
to change the default to ``xfail`` in future releases as this is considered
less error prone. (`#2527
<https://github.com/pytest-dev/pytest/issues/2527>`_)
- **Incompatible change**: after community feedback the `logging
<https://docs.pytest.org/en/latest/logging.html>`_ functionality has
undergone some changes. Please consult the `logging documentation
<https://docs.pytest.org/en/latest/logging.html#incompatible-changes-in-pytest-3-4>`_
for details. (`#3013 <https://github.com/pytest-dev/pytest/issues/3013>`_)
- Console output falls back to "classic" mode when capturing is disabled (``-s``),
otherwise the output gets garbled to the point of being useless. (`#3038
<https://github.com/pytest-dev/pytest/issues/3038>`_)
- New `pytest_runtest_logfinish
<https://docs.pytest.org/en/latest/writing_plugins.html#_pytest.hookspec.pytest_runtest_logfinish>`_
hook which is called when a test item has finished executing, analogous to
`pytest_runtest_logstart
<https://docs.pytest.org/en/latest/writing_plugins.html#_pytest.hookspec.pytest_runtest_start>`_.
(`#3101 <https://github.com/pytest-dev/pytest/issues/3101>`_)
- Improve performance when collecting tests using many fixtures. (`#3107
<https://github.com/pytest-dev/pytest/issues/3107>`_)
- New ``caplog.get_records(when)`` method which provides access to the captured
records for the ``"setup"``, ``"call"`` and ``"teardown"``
testing stages. (`#3117 <https://github.com/pytest-dev/pytest/issues/3117>`_)
- New fixture ``record_xml_attribute`` that allows modifying and inserting
attributes on the ``<testcase>`` xml node in JUnit reports. (`#3130
<https://github.com/pytest-dev/pytest/issues/3130>`_)
- The default cache directory has been renamed from ``.cache`` to
``.pytest_cache`` after community feedback that the name ``.cache`` did not
make it clear that it was used by pytest. (`#3138
<https://github.com/pytest-dev/pytest/issues/3138>`_)
- Colorize the levelname column in the live-log output. (`#3142
<https://github.com/pytest-dev/pytest/issues/3142>`_)
Bug Fixes
---------
- Fix hanging pexpect test on MacOS by using flush() instead of wait().
(`#2022 <https://github.com/pytest-dev/pytest/issues/2022>`_)
- Fix restoring Python state after in-process pytest runs with the
``pytester`` plugin; this may break tests using multiple inprocess
pytest runs if later ones depend on earlier ones leaking global interpreter
changes. (`#3016 <https://github.com/pytest-dev/pytest/issues/3016>`_)
- Fix skipping plugin reporting hook when test aborted before plugin setup
hook. (`#3074 <https://github.com/pytest-dev/pytest/issues/3074>`_)
- Fix progress percentage reported when tests fail during teardown. (`#3088
<https://github.com/pytest-dev/pytest/issues/3088>`_)
- **Incompatible change**: ``-o/--override`` option no longer eats all the
remaining options, which can lead to surprising behavior: for example,
``pytest -o foo=1 /path/to/test.py`` would fail because ``/path/to/test.py``
would be considered as part of the ``-o`` command-line argument. One
consequence of this is that now multiple configuration overrides need
multiple ``-o`` flags: ``pytest -o foo=1 -o bar=2``. (`#3103
<https://github.com/pytest-dev/pytest/issues/3103>`_)
Improved Documentation
----------------------
- Document hooks (defined with ``historic=True``) which cannot be used with
``hookwrapper=True``. (`#2423
<https://github.com/pytest-dev/pytest/issues/2423>`_)
- Clarify that warning capturing doesn't change the warning filter by default.
(`#2457 <https://github.com/pytest-dev/pytest/issues/2457>`_)
- Clarify a possible confusion when using pytest_fixture_setup with fixture
functions that return None. (`#2698
<https://github.com/pytest-dev/pytest/issues/2698>`_)
- Fix the wording of a sentence on doctest flags used in pytest. (`#3076
<https://github.com/pytest-dev/pytest/issues/3076>`_)
- Prefer ``https://*.readthedocs.io`` over ``http://*.rtfd.org`` for links in
the documentation. (`#3092
<https://github.com/pytest-dev/pytest/issues/3092>`_)
- Improve readability (wording, grammar) of Getting Started guide (`#3131
<https://github.com/pytest-dev/pytest/issues/3131>`_)
- Added note that calling pytest.main multiple times from the same process is
not recommended because of import caching. (`#3143
<https://github.com/pytest-dev/pytest/issues/3143>`_)
Trivial/Internal Changes
------------------------
- Show a simple and easy error when keyword expressions trigger a syntax error
(for example, ``"-k foo and import"`` will show an error that you can not use
the ``import`` keyword in expressions). (`#2953
<https://github.com/pytest-dev/pytest/issues/2953>`_)
- Change parametrized automatic test id generation to use the ``__name__``
attribute of functions instead of the fallback argument name plus counter.
(`#2976 <https://github.com/pytest-dev/pytest/issues/2976>`_)
- Replace py.std with stdlib imports. (`#3067
<https://github.com/pytest-dev/pytest/issues/3067>`_)
- Corrected 'you' to 'your' in logging docs. (`#3129
<https://github.com/pytest-dev/pytest/issues/3129>`_)
Pytest 3.3.2 (2017-12-25)
=========================
Bug Fixes
---------
- pytester: ignore files used to obtain current user metadata in the fd leak
detector. (`#2784 <https://github.com/pytest-dev/pytest/issues/2784>`_)
- Fix **memory leak** where objects returned by fixtures were never destructed
by the garbage collector. (`#2981
<https://github.com/pytest-dev/pytest/issues/2981>`_)
- Fix conversion of pyargs to filename to not convert symlinks on Python 2. (`#2985
<https://github.com/pytest-dev/pytest/issues/2985>`_)
- ``PYTEST_DONT_REWRITE`` is now checked for plugins too rather than only for
test modules. (`#2995 <https://github.com/pytest-dev/pytest/issues/2995>`_)
Improved Documentation
----------------------
- Add clarifying note about behavior of multiple parametrized arguments (`#3001
<https://github.com/pytest-dev/pytest/issues/3001>`_)
Trivial/Internal Changes
------------------------
- Code cleanup. (`#3015 <https://github.com/pytest-dev/pytest/issues/3015>`_,
`#3021 <https://github.com/pytest-dev/pytest/issues/3021>`_)
- Clean up code by replacing imports and references of `_ast` to `ast`. (`#3018
<https://github.com/pytest-dev/pytest/issues/3018>`_)
Pytest 3.3.1 (2017-12-05)
=========================
@@ -31,7 +200,7 @@ Bug Fixes
``TerminalReporter._tw``. This alias was removed by accident in the ``3.3.0``
release. (`#2984 <https://github.com/pytest-dev/pytest/issues/2984>`_)
- The pytest-capturelog plugin is now also blacklisted, avoiding errors when
- The ``pytest-capturelog`` plugin is now also blacklisted, avoiding errors when
running pytest with it still installed. (`#3004
<https://github.com/pytest-dev/pytest/issues/3004>`_)
@@ -39,14 +208,14 @@ Bug Fixes
Improved Documentation
----------------------
- Fix broken link to plugin pytest-localserver. (`#2963
- Fix broken link to plugin ``pytest-localserver``. (`#2963
<https://github.com/pytest-dev/pytest/issues/2963>`_)
Trivial/Internal Changes
------------------------
- Update github "bugs" link in CONTRIBUTING.rst (`#2949
- Update github "bugs" link in ``CONTRIBUTING.rst`` (`#2949
<https://github.com/pytest-dev/pytest/issues/2949>`_)
@@ -157,10 +326,10 @@ Bug Fixes
<https://github.com/pytest-dev/pytest/issues/2124>`_)
- If an exception happens while loading a plugin, pytest no longer hides the
original traceback. In python2 it will show the original traceback with a new
message that explains in which plugin. In python3 it will show 2 canonized
original traceback. In Python 2 it will show the original traceback with a new
message that explains in which plugin. In Python 3 it will show 2 canonized
exceptions, the original exception while loading the plugin in addition to an
exception that PyTest throws about loading a plugin. (`#2491
exception that pytest throws about loading a plugin. (`#2491
<https://github.com/pytest-dev/pytest/issues/2491>`_)
- ``capsys`` and ``capfd`` can now be used by other fixtures. (`#2709

View File

@@ -276,3 +276,15 @@ Here is a simple overview, with pytest-specific bits:
base: features # if it's a feature
Joining the Development Team
----------------------------
Anyone who has successfully seen through a pull request which did not
require any extra work from the development team to merge will
themselves gain commit access if they so wish (if we forget to ask please send a friendly
reminder). This does not mean your workflow to contribute changes,
everyone goes through the same pull-request-and-review process and
no-one merges their own pull requests unless already approved. It does however mean you can
participate in the development process more fully since you can merge
pull requests from other contributors yourself after having reviewed
them.

View File

@@ -12,7 +12,7 @@ taking a lot of time to make a new one.
#. Install development dependencies in a virtual environment with::
pip3 install -r tasks/requirements.txt
pip3 install -U -r tasks/requirements.txt
#. Create a branch ``release-X.Y.Z`` with the version for the release.

View File

@@ -60,7 +60,7 @@ import os
from glob import glob
class FastFilesCompleter:
class FastFilesCompleter(object):
'Fast file completer class'
def __init__(self, directories=True):

View File

@@ -1,5 +1,7 @@
from __future__ import absolute_import, division, print_function
import inspect
import sys
import traceback
from inspect import CO_VARARGS, CO_VARKEYWORDS
import re
from weakref import ref
@@ -422,7 +424,7 @@ class ExceptionInfo(object):
"""
if style == 'native':
return ReprExceptionInfo(ReprTracebackNative(
py.std.traceback.format_exception(
traceback.format_exception(
self.type,
self.value,
self.traceback[0]._rawentry,
@@ -556,7 +558,7 @@ class FormattedExcinfo(object):
# else:
# self._line("%-10s =\\" % (name,))
# # XXX
# py.std.pprint.pprint(value, stream=self.excinfowriter)
# pprint.pprint(value, stream=self.excinfowriter)
return ReprLocals(lines)
def repr_traceback_entry(self, entry, excinfo=None):
@@ -669,7 +671,7 @@ class FormattedExcinfo(object):
else:
# fallback to native repr if the exception doesn't have a traceback:
# ExceptionInfo objects require a full traceback to work
reprtraceback = ReprTracebackNative(py.std.traceback.format_exception(type(e), e, None))
reprtraceback = ReprTracebackNative(traceback.format_exception(type(e), e, None))
reprcrash = None
repr_chain += [(reprtraceback, reprcrash, descr)]
@@ -886,7 +888,7 @@ def getrawcode(obj, trycall=True):
obj = getattr(obj, 'f_code', obj)
obj = getattr(obj, '__code__', obj)
if trycall and not hasattr(obj, 'co_firstlineno'):
if hasattr(obj, '__call__') and not py.std.inspect.isclass(obj):
if hasattr(obj, '__call__') and not inspect.isclass(obj):
x = getrawcode(obj.__call__, trycall=False)
if hasattr(x, 'co_firstlineno'):
return x

View File

@@ -1,19 +1,16 @@
from __future__ import absolute_import, division, generators, print_function
import ast
from ast import PyCF_ONLY_AST as _AST_FLAG
from bisect import bisect_right
import linecache
import sys
import six
import inspect
import tokenize
import py
cpy_compile = compile
try:
import _ast
from _ast import PyCF_ONLY_AST as _AST_FLAG
except ImportError:
_AST_FLAG = 0
_ast = None
cpy_compile = compile
class Source(object):
@@ -195,7 +192,7 @@ class Source(object):
if flag & _AST_FLAG:
return co
lines = [(x + "\n") for x in self.lines]
py.std.linecache.cache[filename] = (1, None, lines, filename)
linecache.cache[filename] = (1, None, lines, filename)
return co
#
@@ -209,7 +206,7 @@ def compile_(source, filename=None, mode='exec', flags=generators.compiler_flag,
retrieval of the source code for the code object
and any recursively created code objects.
"""
if _ast is not None and isinstance(source, _ast.AST):
if isinstance(source, ast.AST):
# XXX should Source support having AST?
return cpy_compile(source, filename, mode, flags, dont_inherit)
_genframe = sys._getframe(1) # the caller
@@ -227,8 +224,7 @@ def getfslineno(obj):
code = _pytest._code.Code(obj)
except TypeError:
try:
fn = (py.std.inspect.getsourcefile(obj) or
py.std.inspect.getfile(obj))
fn = inspect.getsourcefile(obj) or inspect.getfile(obj)
except TypeError:
return "", -1
@@ -252,7 +248,7 @@ def getfslineno(obj):
def findsource(obj):
try:
sourcelines, lineno = py.std.inspect.findsource(obj)
sourcelines, lineno = inspect.findsource(obj)
except py.builtin._sysex:
raise
except: # noqa
@@ -322,7 +318,7 @@ def get_statement_startend2(lineno, node):
# AST's line numbers start indexing at 1
values = []
for x in ast.walk(node):
if isinstance(x, _ast.stmt) or isinstance(x, _ast.ExceptHandler):
if isinstance(x, ast.stmt) or isinstance(x, ast.ExceptHandler):
values.append(x.lineno - 1)
for name in "finalbody", "orelse":
val = getattr(x, name, None)

View File

@@ -56,7 +56,7 @@ class DummyRewriteHook(object):
pass
class AssertionState:
class AssertionState(object):
"""State for the assertion plugin."""
def __init__(self, config, mode):

View File

@@ -1,7 +1,6 @@
"""Rewrite assertion AST to produce nice error messages"""
from __future__ import absolute_import, division, print_function
import ast
import _ast
import errno
import itertools
import imp
@@ -180,11 +179,13 @@ class AssertionRewritingHook(object):
The named module or package as well as any nested modules will
be rewritten on import.
"""
already_imported = set(names).intersection(set(sys.modules))
if already_imported:
for name in already_imported:
if name not in self._rewritten_names:
self._warn_already_imported(name)
already_imported = (set(names)
.intersection(sys.modules)
.difference(self._rewritten_names))
for name in already_imported:
if not AssertionRewriter.is_rewrite_disabled(
sys.modules[name].__doc__ or ""):
self._warn_already_imported(name)
self._must_rewrite.update(names)
def _warn_already_imported(self, name):
@@ -636,7 +637,8 @@ class AssertionRewriter(ast.NodeVisitor):
not isinstance(field, ast.expr)):
nodes.append(field)
def is_rewrite_disabled(self, docstring):
@staticmethod
def is_rewrite_disabled(docstring):
return "PYTEST_DONT_REWRITE" in docstring
def variable(self):
@@ -914,7 +916,7 @@ class AssertionRewriter(ast.NodeVisitor):
def visit_Compare(self, comp):
self.push_format_context()
left_res, left_expl = self.visit(comp.left)
if isinstance(comp.left, (_ast.Compare, _ast.BoolOp)):
if isinstance(comp.left, (ast.Compare, ast.BoolOp)):
left_expl = "({0})".format(left_expl)
res_variables = [self.variable() for i in range(len(comp.ops))]
load_names = [ast.Name(v, ast.Load()) for v in res_variables]
@@ -925,7 +927,7 @@ class AssertionRewriter(ast.NodeVisitor):
results = [left_res]
for i, op, next_operand in it:
next_res, next_expl = self.visit(next_operand)
if isinstance(next_operand, (_ast.Compare, _ast.BoolOp)):
if isinstance(next_operand, (ast.Compare, ast.BoolOp)):
next_expl = "({0})".format(next_expl)
results.append(next_res)
sym = binop_map[op.__class__]

View File

@@ -5,11 +5,7 @@ import pprint
import _pytest._code
import py
import six
try:
from collections import Sequence
except ImportError:
Sequence = list
from collections import Sequence
u = six.text_type
@@ -113,7 +109,7 @@ def assertrepr_compare(config, op, left, right):
summary = u('%s %s %s') % (ecu(left_repr), op, ecu(right_repr))
def issequence(x):
return (isinstance(x, (list, tuple, Sequence)) and not isinstance(x, basestring))
return isinstance(x, Sequence) and not isinstance(x, basestring)
def istext(x):
return isinstance(x, basestring)

View File

@@ -17,7 +17,7 @@ class Cache(object):
self.config = config
self._cachedir = Cache.cache_dir_from_config(config)
self.trace = config.trace.root.get("cache")
if config.getvalue("cacheclear"):
if config.getoption("cacheclear"):
self.trace("clearing cachedir")
if self._cachedir.check():
self._cachedir.remove()
@@ -98,13 +98,13 @@ class Cache(object):
json.dump(value, f, indent=2, sort_keys=True)
class LFPlugin:
class LFPlugin(object):
""" Plugin which implements the --lf (run last-failing) option """
def __init__(self, config):
self.config = config
active_keys = 'lf', 'failedfirst'
self.active = any(config.getvalue(key) for key in active_keys)
self.active = any(config.getoption(key) for key in active_keys)
self.lastfailed = config.cache.get("cache/lastfailed", {})
self._previously_failed_count = None
@@ -114,7 +114,8 @@ class LFPlugin:
mode = "run all (no recorded failures)"
else:
noun = 'failure' if self._previously_failed_count == 1 else 'failures'
suffix = " first" if self.config.getvalue("failedfirst") else ""
suffix = " first" if self.config.getoption(
"failedfirst") else ""
mode = "rerun previous {count} {noun}{suffix}".format(
count=self._previously_failed_count, suffix=suffix, noun=noun
)
@@ -151,7 +152,7 @@ class LFPlugin:
# running a subset of all tests with recorded failures outside
# of the set of tests currently executing
return
if self.config.getvalue("lf"):
if self.config.getoption("lf"):
items[:] = previously_failed
config.hook.pytest_deselected(items=previously_passed)
else:
@@ -159,7 +160,7 @@ class LFPlugin:
def pytest_sessionfinish(self, session):
config = self.config
if config.getvalue("cacheshow") or hasattr(config, "slaveinput"):
if config.getoption("cacheshow") or hasattr(config, "slaveinput"):
return
saved_lastfailed = config.cache.get("cache/lastfailed", {})
@@ -185,7 +186,7 @@ def pytest_addoption(parser):
'--cache-clear', action='store_true', dest="cacheclear",
help="remove all cache contents at start of test run.")
parser.addini(
"cache_dir", default='.cache',
"cache_dir", default='.pytest_cache',
help="cache directory path.")

View File

@@ -61,7 +61,7 @@ def pytest_load_initial_conftests(early_config, parser, args):
sys.stderr.write(err)
class CaptureManager:
class CaptureManager(object):
"""
Capture plugin, manages that the appropriate capture method is enabled/disabled during collection and each
test phase (setup, call, teardown). After each of those points, the captured output is obtained and
@@ -271,7 +271,7 @@ def _install_capture_fixture_on_item(request, capture_class):
del request.node._capture_fixture
class CaptureFixture:
class CaptureFixture(object):
def __init__(self, captureclass, request):
self.captureclass = captureclass
self.request = request
@@ -416,11 +416,11 @@ class MultiCapture(object):
self.err.snap() if self.err is not None else "")
class NoCapture:
class NoCapture(object):
__init__ = start = done = suspend = resume = lambda *args: None
class FDCaptureBinary:
class FDCaptureBinary(object):
"""Capture IO to/from a given os-level filedescriptor.
snap() produces `bytes`
@@ -506,7 +506,7 @@ class FDCapture(FDCaptureBinary):
return res
class SysCapture:
class SysCapture(object):
def __init__(self, fd, tmpfile=None):
name = patchsysdict[fd]
self._old = getattr(sys, name)
@@ -551,7 +551,7 @@ class SysCaptureBinary(SysCapture):
return res
class DontReadFromInput:
class DontReadFromInput(object):
"""Temporary stub class. Ideally when stdin is accessed, the
capturing should be turned off, with possibly all data captured
so far sent to the screen. This should be configurable, though,

View File

@@ -60,12 +60,13 @@ def main(args=None, plugins=None):
finally:
config._ensure_unconfigure()
except UsageError as e:
tw = py.io.TerminalWriter(sys.stderr)
for msg in e.args:
sys.stderr.write("ERROR: %s\n" % (msg,))
tw.line("ERROR: {}\n".format(msg), red=True)
return 4
class cmdline: # compatibility namespace
class cmdline(object): # compatibility namespace
main = staticmethod(main)
@@ -462,7 +463,7 @@ def _get_plugin_specs_as_list(specs):
return []
class Parser:
class Parser(object):
""" Parser for command line arguments and ini-file values.
:ivar extra_info: dict of generic param -> value to display in case
@@ -597,7 +598,7 @@ class ArgumentError(Exception):
return self.msg
class Argument:
class Argument(object):
"""class that mimics the necessary behaviour of optparse.Option
its currently a least effort implementation
@@ -727,7 +728,7 @@ class Argument:
return 'Argument({0})'.format(', '.join(args))
class OptionGroup:
class OptionGroup(object):
def __init__(self, name, description="", parser=None):
self.name = name
self.description = description
@@ -858,7 +859,7 @@ class CmdOptions(object):
return CmdOptions(self.__dict__)
class Notset:
class Notset(object):
def __repr__(self):
return "<NOTSET>"
@@ -1187,16 +1188,15 @@ class Config(object):
def _get_override_ini_value(self, name):
value = None
# override_ini is a list of list, to support both -o foo1=bar1 foo2=bar2 and
# and -o foo1=bar1 -o foo2=bar2 options
# always use the last item if multiple value set for same ini-name,
# override_ini is a list of "ini=value" options
# always use the last item if multiple values are set for same ini-name,
# e.g. -o foo=bar1 -o foo=bar2 will set foo to bar2
for ini_config_list in self._override_ini:
for ini_config in ini_config_list:
try:
(key, user_ini_value) = ini_config.split("=", 1)
except ValueError:
raise UsageError("-o/--override-ini expects option=value style.")
for ini_config in self._override_ini:
try:
key, user_ini_value = ini_config.split("=", 1)
except ValueError:
raise UsageError("-o/--override-ini expects option=value style.")
else:
if key == name:
value = user_ini_value
return value

View File

@@ -40,7 +40,7 @@ def pytest_configure(config):
config._cleanup.append(fin)
class pytestPDB:
class pytestPDB(object):
""" Pseudo PDB that defers to the real pdb. """
_pluginmanager = None
_config = None
@@ -62,7 +62,7 @@ class pytestPDB:
cls._pdb_cls().set_trace(frame)
class PdbInvoke:
class PdbInvoke(object):
def pytest_exception_interact(self, node, call, report):
capman = node.config.pluginmanager.getplugin("capturemanager")
if capman:

View File

@@ -4,7 +4,7 @@ import functools
import inspect
import sys
import warnings
from collections import OrderedDict
from collections import OrderedDict, deque, defaultdict
import attr
import py
@@ -26,11 +26,12 @@ from _pytest.outcomes import fail, TEST_OUTCOME
def pytest_sessionstart(session):
import _pytest.python
import _pytest.nodes
scopename2class.update({
'class': _pytest.python.Class,
'module': _pytest.python.Module,
'function': _pytest.main.Item,
'function': _pytest.nodes.Item,
'session': _pytest.main.Session,
})
session._fixturemanager = FixtureManager(session)
@@ -162,62 +163,51 @@ def get_parametrized_fixture_keys(item, scopenum):
def reorder_items(items):
argkeys_cache = {}
items_by_argkey = {}
for scopenum in range(0, scopenum_function):
argkeys_cache[scopenum] = d = {}
items_by_argkey[scopenum] = item_d = defaultdict(list)
for item in items:
keys = OrderedDict.fromkeys(get_parametrized_fixture_keys(item, scopenum))
if keys:
d[item] = keys
return reorder_items_atscope(items, set(), argkeys_cache, 0)
for key in keys:
item_d[key].append(item)
items = OrderedDict.fromkeys(items)
return list(reorder_items_atscope(items, set(), argkeys_cache, items_by_argkey, 0))
def reorder_items_atscope(items, ignore, argkeys_cache, scopenum):
def reorder_items_atscope(items, ignore, argkeys_cache, items_by_argkey, scopenum):
if scopenum >= scopenum_function or len(items) < 3:
return items
items_done = []
while 1:
items_before, items_same, items_other, newignore = \
slice_items(items, ignore, argkeys_cache[scopenum])
items_before = reorder_items_atscope(
items_before, ignore, argkeys_cache, scopenum + 1)
if items_same is None:
# nothing to reorder in this scope
assert items_other is None
return items_done + items_before
items_done.extend(items_before)
items = items_same + items_other
ignore = newignore
def slice_items(items, ignore, scoped_argkeys_cache):
# we pick the first item which uses a fixture instance in the
# requested scope and which we haven't seen yet. We slice the input
# items list into a list of items_nomatch, items_same and
# items_other
if scoped_argkeys_cache: # do we need to do work at all?
it = iter(items)
# first find a slicing key
for i, item in enumerate(it):
argkeys = scoped_argkeys_cache.get(item)
if argkeys is not None:
newargkeys = OrderedDict.fromkeys(k for k in argkeys if k not in ignore)
if newargkeys: # found a slicing key
slicing_argkey, _ = newargkeys.popitem()
items_before = items[:i]
items_same = [item]
items_other = []
# now slice the remainder of the list
for item in it:
argkeys = scoped_argkeys_cache.get(item)
if argkeys and slicing_argkey in argkeys and \
slicing_argkey not in ignore:
items_same.append(item)
else:
items_other.append(item)
newignore = ignore.copy()
newignore.add(slicing_argkey)
return (items_before, items_same, items_other, newignore)
return items, None, None, None
items_deque = deque(items)
items_done = OrderedDict()
scoped_items_by_argkey = items_by_argkey[scopenum]
scoped_argkeys_cache = argkeys_cache[scopenum]
while items_deque:
no_argkey_group = OrderedDict()
slicing_argkey = None
while items_deque:
item = items_deque.popleft()
if item in items_done or item in no_argkey_group:
continue
argkeys = OrderedDict.fromkeys(k for k in scoped_argkeys_cache.get(item, []) if k not in ignore)
if not argkeys:
no_argkey_group[item] = None
else:
slicing_argkey, _ = argkeys.popitem()
# we don't have to remove relevant items from later in the deque because they'll just be ignored
for i in reversed(scoped_items_by_argkey[slicing_argkey]):
if i in items:
items_deque.appendleft(i)
break
if no_argkey_group:
no_argkey_group = reorder_items_atscope(
no_argkey_group, set(), argkeys_cache, items_by_argkey, scopenum + 1)
for item in no_argkey_group:
items_done[item] = None
ignore.add(slicing_argkey)
return items_done
def fillfixtures(function):
@@ -246,7 +236,7 @@ def get_direct_param_fixture_func(request):
return request.param
class FuncFixtureInfo:
class FuncFixtureInfo(object):
def __init__(self, argnames, names_closure, name2fixturedefs):
self.argnames = argnames
self.names_closure = names_closure
@@ -267,7 +257,6 @@ class FixtureRequest(FuncargnamesCompatAttr):
self.fixturename = None
#: Scope string, one of "function", "class", "module", "session"
self.scope = "function"
self._fixture_values = {} # argname -> fixture value
self._fixture_defs = {} # argname -> FixtureDef
fixtureinfo = pyfuncitem._fixtureinfo
self._arg2fixturedefs = fixtureinfo.name2fixturedefs.copy()
@@ -443,15 +432,14 @@ class FixtureRequest(FuncargnamesCompatAttr):
fixturedef = self._getnextfixturedef(argname)
except FixtureLookupError:
if argname == "request":
class PseudoFixtureDef:
class PseudoFixtureDef(object):
cached_result = (self, [0], None)
scope = "function"
return PseudoFixtureDef
raise
# remove indent to prevent the python3 exception
# from leaking into the call
result = self._getfixturevalue(fixturedef)
self._fixture_values[argname] = result
self._compute_fixture_value(fixturedef)
self._fixture_defs[argname] = fixturedef
return fixturedef
@@ -466,7 +454,14 @@ class FixtureRequest(FuncargnamesCompatAttr):
values.append(fixturedef)
current = current._parent_request
def _getfixturevalue(self, fixturedef):
def _compute_fixture_value(self, fixturedef):
"""
Creates a SubRequest based on "self" and calls the execute method of the given fixturedef object. This will
force the FixtureDef object to throw away any previous results and compute a new fixture value, which
will be stored into the FixtureDef object itself.
:param FixtureDef fixturedef:
"""
# prepare a subrequest object before calling fixture function
# (latter managed by fixturedef)
argname = fixturedef.argname
@@ -515,12 +510,11 @@ class FixtureRequest(FuncargnamesCompatAttr):
exc_clear()
try:
# call the fixture function
val = fixturedef.execute(request=subrequest)
fixturedef.execute(request=subrequest)
finally:
# if fixture function failed it might have registered finalizers
self.session._setupstate.addfinalizer(functools.partial(fixturedef.finish, request=subrequest),
subrequest.node)
return val
def _check_scope(self, argname, invoking_scope, requested_scope):
if argname == "request":
@@ -573,7 +567,6 @@ class SubRequest(FixtureRequest):
self.scope = scope
self._fixturedef = fixturedef
self._pyfuncitem = request._pyfuncitem
self._fixture_values = request._fixture_values
self._fixture_defs = request._fixture_defs
self._arg2fixturedefs = request._arg2fixturedefs
self._arg2index = request._arg2index
@@ -715,7 +708,7 @@ def call_fixture_func(fixturefunc, request, kwargs):
return res
class FixtureDef:
class FixtureDef(object):
""" A container for a factory definition. """
def __init__(self, fixturemanager, baseid, argname, func, scope, params,
@@ -921,7 +914,7 @@ def pytestconfig(request):
return request.config
class FixtureManager:
class FixtureManager(object):
"""
pytest fixtures definitions and information is stored and managed
from this class.

View File

@@ -57,9 +57,9 @@ def pytest_addoption(parser):
action="store_true", dest="debug", default=False,
help="store internal tracing debug information in 'pytestdebug.log'.")
group._addoption(
'-o', '--override-ini', nargs='*', dest="override_ini",
'-o', '--override-ini', dest="override_ini",
action="append",
help="override config option with option=value style, e.g. `-o xfail_strict=True`.")
help='override ini option with "option=value" style, e.g. `-o xfail_strict=True -o cache_dir=cache`.')
@pytest.hookimpl(hookwrapper=True)

View File

@@ -12,22 +12,40 @@ hookspec = HookspecMarker("pytest")
@hookspec(historic=True)
def pytest_addhooks(pluginmanager):
"""called at plugin registration time to allow adding new hooks via a call to
pluginmanager.add_hookspecs(module_or_class, prefix)."""
``pluginmanager.add_hookspecs(module_or_class, prefix)``.
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
.. note::
This hook is incompatible with ``hookwrapper=True``.
"""
@hookspec(historic=True)
def pytest_namespace():
"""
DEPRECATED: this hook causes direct monkeypatching on pytest, its use is strongly discouraged
(**Deprecated**) this hook causes direct monkeypatching on pytest, its use is strongly discouraged
return dict of name->object to be made globally available in
the pytest namespace. This hook is called at plugin registration
time.
the pytest namespace.
This hook is called at plugin registration time.
.. note::
This hook is incompatible with ``hookwrapper=True``.
"""
@hookspec(historic=True)
def pytest_plugin_registered(plugin, manager):
""" a new pytest plugin got registered. """
""" a new pytest plugin got registered.
:param plugin: the plugin module or instance
:param _pytest.config.PytestPluginManager manager: pytest plugin manager
.. note::
This hook is incompatible with ``hookwrapper=True``.
"""
@hookspec(historic=True)
@@ -41,7 +59,7 @@ def pytest_addoption(parser):
files situated at the tests root directory due to how pytest
:ref:`discovers plugins during startup <pluginorder>`.
:arg parser: To add command line options, call
:arg _pytest.config.Parser parser: To add command line options, call
:py:func:`parser.addoption(...) <_pytest.config.Parser.addoption>`.
To add ini-file values call :py:func:`parser.addini(...)
<_pytest.config.Parser.addini>`.
@@ -56,8 +74,10 @@ def pytest_addoption(parser):
a value read from an ini-style file.
The config object is passed around on many internal objects via the ``.config``
attribute or can be retrieved as the ``pytestconfig`` fixture or accessed
via (deprecated) ``pytest.config``.
attribute or can be retrieved as the ``pytestconfig`` fixture.
.. note::
This hook is incompatible with ``hookwrapper=True``.
"""
@@ -72,14 +92,15 @@ def pytest_configure(config):
After that, the hook is called for other conftest files as they are
imported.
:arg config: pytest config object
:type config: _pytest.config.Config
.. note::
This hook is incompatible with ``hookwrapper=True``.
:arg _pytest.config.Config config: pytest config object
"""
# -------------------------------------------------------------------------
# Bootstrapping hooks called for plugins registered early enough:
# internal and 3rd party plugins as well as directly
# discoverable conftest.py local plugins.
# internal and 3rd party plugins.
# -------------------------------------------------------------------------
@@ -87,11 +108,28 @@ def pytest_configure(config):
def pytest_cmdline_parse(pluginmanager, args):
"""return initialized config object, parsing the specified args.
Stops at first non-None result, see :ref:`firstresult` """
Stops at first non-None result, see :ref:`firstresult`
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
:param _pytest.config.PytestPluginManager pluginmanager: pytest plugin manager
:param list[str] args: list of arguments passed on the command line
"""
def pytest_cmdline_preparse(config, args):
"""(deprecated) modify command line arguments before option parsing. """
"""(**Deprecated**) modify command line arguments before option parsing.
This hook is considered deprecated and will be removed in a future pytest version. Consider
using :func:`pytest_load_initial_conftests` instead.
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
:param _pytest.config.Config config: pytest config object
:param list[str] args: list of arguments passed on the command line
"""
@hookspec(firstresult=True)
@@ -99,12 +137,26 @@ def pytest_cmdline_main(config):
""" called for performing the main command line action. The default
implementation will invoke the configure hooks and runtest_mainloop.
Stops at first non-None result, see :ref:`firstresult` """
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
Stops at first non-None result, see :ref:`firstresult`
:param _pytest.config.Config config: pytest config object
"""
def pytest_load_initial_conftests(early_config, parser, args):
""" implements the loading of initial conftest files ahead
of command line option parsing. """
of command line option parsing.
.. note::
This hook will not be called for ``conftest.py`` files, only for setuptools plugins.
:param _pytest.config.Config early_config: pytest config object
:param list[str] args: list of arguments passed on the command line
:param _pytest.config.Parser parser: to add command line options
"""
# -------------------------------------------------------------------------
@@ -113,18 +165,29 @@ def pytest_load_initial_conftests(early_config, parser, args):
@hookspec(firstresult=True)
def pytest_collection(session):
""" perform the collection protocol for the given session.
"""Perform the collection protocol for the given session.
Stops at first non-None result, see :ref:`firstresult` """
Stops at first non-None result, see :ref:`firstresult`.
:param _pytest.main.Session session: the pytest session object
"""
def pytest_collection_modifyitems(session, config, items):
""" called after collection has been performed, may filter or re-order
the items in-place."""
the items in-place.
:param _pytest.main.Session session: the pytest session object
:param _pytest.config.Config config: pytest config object
:param List[_pytest.nodes.Item] items: list of item objects
"""
def pytest_collection_finish(session):
""" called after collection has been performed and modified. """
""" called after collection has been performed and modified.
:param _pytest.main.Session session: the pytest session object
"""
@hookspec(firstresult=True)
@@ -134,6 +197,9 @@ def pytest_ignore_collect(path, config):
more specific hooks.
Stops at first non-None result, see :ref:`firstresult`
:param str path: the path to analyze
:param _pytest.config.Config config: pytest config object
"""
@@ -141,12 +207,18 @@ def pytest_ignore_collect(path, config):
def pytest_collect_directory(path, parent):
""" called before traversing a directory for collection files.
Stops at first non-None result, see :ref:`firstresult` """
Stops at first non-None result, see :ref:`firstresult`
:param str path: the path to analyze
"""
def pytest_collect_file(path, parent):
""" return collection Node or None for the given path. Any new node
needs to have the specified ``parent`` as a parent."""
needs to have the specified ``parent`` as a parent.
:param str path: the path to collect
"""
# logging hooks for collection
@@ -212,7 +284,12 @@ def pytest_make_parametrize_id(config, val, argname):
by @pytest.mark.parametrize calls. Return None if the hook doesn't know about ``val``.
The parameter name is available as ``argname``, if required.
Stops at first non-None result, see :ref:`firstresult` """
Stops at first non-None result, see :ref:`firstresult`
:param _pytest.config.Config config: pytest config object
:param val: the parametrized value
:param str argname: the automatic parameter name produced by pytest
"""
# -------------------------------------------------------------------------
# generic runtest related hooks
@@ -224,11 +301,14 @@ def pytest_runtestloop(session):
""" called for performing the main runtest loop
(after collection finished).
Stops at first non-None result, see :ref:`firstresult` """
Stops at first non-None result, see :ref:`firstresult`
:param _pytest.main.Session session: the pytest session object
"""
def pytest_itemstart(item, node):
""" (deprecated, use pytest_runtest_logstart). """
"""(**Deprecated**) use pytest_runtest_logstart. """
@hookspec(firstresult=True)
@@ -250,7 +330,25 @@ def pytest_runtest_protocol(item, nextitem):
def pytest_runtest_logstart(nodeid, location):
""" signal the start of running a single test item. """
""" signal the start of running a single test item.
This hook will be called **before** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and
:func:`pytest_runtest_teardown` hooks.
:param str nodeid: full id of the item
:param location: a triple of ``(filename, linenum, testname)``
"""
def pytest_runtest_logfinish(nodeid, location):
""" signal the complete finish of running a single test item.
This hook will be called **after** :func:`pytest_runtest_setup`, :func:`pytest_runtest_call` and
:func:`pytest_runtest_teardown` hooks.
:param str nodeid: full id of the item
:param location: a triple of ``(filename, linenum, testname)``
"""
def pytest_runtest_setup(item):
@@ -293,7 +391,15 @@ def pytest_runtest_logreport(report):
def pytest_fixture_setup(fixturedef, request):
""" performs fixture setup execution.
Stops at first non-None result, see :ref:`firstresult` """
:return: The return value of the call to the fixture function
Stops at first non-None result, see :ref:`firstresult`
.. note::
If the fixture function returns None, other implementations of
this hook function will continue to be called, according to the
behavior of the :ref:`firstresult` option.
"""
def pytest_fixture_post_finalizer(fixturedef, request):
@@ -307,15 +413,25 @@ def pytest_fixture_post_finalizer(fixturedef, request):
def pytest_sessionstart(session):
""" before session.main() is called. """
""" before session.main() is called.
:param _pytest.main.Session session: the pytest session object
"""
def pytest_sessionfinish(session, exitstatus):
""" whole test run finishes. """
""" whole test run finishes.
:param _pytest.main.Session session: the pytest session object
:param int exitstatus: the status which pytest will return to the system
"""
def pytest_unconfigure(config):
""" called before test process is exited. """
""" called before test process is exited.
:param _pytest.config.Config config: pytest config object
"""
# -------------------------------------------------------------------------
@@ -329,6 +445,8 @@ def pytest_assertrepr_compare(config, op, left, right):
of strings. The strings will be joined by newlines but any newlines
*in* a string will be escaped. Note that all but the first line will
be indented slightly, the intention is for the first line to be a summary.
:param _pytest.config.Config config: pytest config object
"""
# -------------------------------------------------------------------------
@@ -339,7 +457,7 @@ def pytest_assertrepr_compare(config, op, left, right):
def pytest_report_header(config, startdir):
""" return a string or list of strings to be displayed as header info for terminal reporting.
:param config: the pytest config object.
:param _pytest.config.Config config: pytest config object
:param startdir: py.path object with the starting dir
.. note::
@@ -358,7 +476,7 @@ def pytest_report_collectionfinish(config, startdir, items):
This strings will be displayed after the standard "collected X items" message.
:param config: the pytest config object.
:param _pytest.config.Config config: pytest config object
:param startdir: py.path object with the starting dir
:param items: list of pytest items that are going to be executed; this list should not be modified.
"""
@@ -379,7 +497,11 @@ def pytest_terminal_summary(terminalreporter, exitstatus):
def pytest_logwarning(message, code, nodeid, fslocation):
""" process a warning specified by a message, a code string,
a nodeid and fslocation (both of which may be None
if the warning is not tied to a partilar node/location)."""
if the warning is not tied to a particular node/location).
.. note::
This hook is incompatible with ``hookwrapper=True``.
"""
# -------------------------------------------------------------------------
# doctest hooks
@@ -418,6 +540,5 @@ def pytest_enter_pdb(config):
""" called upon pdb.set_trace(), can be used by plugins to take special
action just before the python debugger enters in interactive mode.
:arg config: pytest config object
:type config: _pytest.config.Config
:param _pytest.config.Config config: pytest config object
"""

View File

@@ -85,6 +85,9 @@ class _NodeReporter(object):
def add_property(self, name, value):
self.properties.append((str(name), bin_xml_escape(value)))
def add_attribute(self, name, value):
self.attrs[str(name)] = bin_xml_escape(value)
def make_properties_node(self):
"""Return a Junit node containing custom properties, if any.
"""
@@ -98,6 +101,7 @@ class _NodeReporter(object):
def record_testreport(self, testreport):
assert not self.testcase
names = mangle_test_address(testreport.nodeid)
existing_attrs = self.attrs
classnames = names[:-1]
if self.xml.prefix:
classnames.insert(0, self.xml.prefix)
@@ -111,6 +115,7 @@ class _NodeReporter(object):
if hasattr(testreport, "url"):
attrs["url"] = testreport.url
self.attrs = attrs
self.attrs.update(existing_attrs) # restore any user-defined attributes
def to_xml(self):
testcase = Junit.testcase(time=self.duration, **self.attrs)
@@ -211,6 +216,27 @@ def record_xml_property(request):
return add_property_noop
@pytest.fixture
def record_xml_attribute(request):
"""Add extra xml attributes to the tag for the calling test.
The fixture is callable with ``(name, value)``, with value being automatically
xml-encoded
"""
request.node.warn(
code='C3',
message='record_xml_attribute is an experimental feature',
)
xml = getattr(request.config, "_xml", None)
if xml is not None:
node_reporter = xml.node_reporter(request.node.nodeid)
return node_reporter.add_attribute
else:
def add_attr_noop(name, value):
pass
return add_attr_noop
def pytest_addoption(parser):
group = parser.getgroup("terminal reporting")
group.addoption(

View File

@@ -2,9 +2,10 @@ from __future__ import absolute_import, division, print_function
import logging
from contextlib import closing, contextmanager
import sys
import re
import six
from _pytest.config import create_terminal_writer
import pytest
import py
@@ -13,6 +14,58 @@ DEFAULT_LOG_FORMAT = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s'
DEFAULT_LOG_DATE_FORMAT = '%H:%M:%S'
class ColoredLevelFormatter(logging.Formatter):
"""
Colorize the %(levelname)..s part of the log format passed to __init__.
"""
LOGLEVEL_COLOROPTS = {
logging.CRITICAL: {'red'},
logging.ERROR: {'red', 'bold'},
logging.WARNING: {'yellow'},
logging.WARN: {'yellow'},
logging.INFO: {'green'},
logging.DEBUG: {'purple'},
logging.NOTSET: set(),
}
LEVELNAME_FMT_REGEX = re.compile(r'%\(levelname\)([+-]?\d*s)')
def __init__(self, terminalwriter, *args, **kwargs):
super(ColoredLevelFormatter, self).__init__(
*args, **kwargs)
if six.PY2:
self._original_fmt = self._fmt
else:
self._original_fmt = self._style._fmt
self._level_to_fmt_mapping = {}
levelname_fmt_match = self.LEVELNAME_FMT_REGEX.search(self._fmt)
if not levelname_fmt_match:
return
levelname_fmt = levelname_fmt_match.group()
for level, color_opts in self.LOGLEVEL_COLOROPTS.items():
formatted_levelname = levelname_fmt % {
'levelname': logging.getLevelName(level)}
# add ANSI escape sequences around the formatted levelname
color_kwargs = {name: True for name in color_opts}
colorized_formatted_levelname = terminalwriter.markup(
formatted_levelname, **color_kwargs)
self._level_to_fmt_mapping[level] = self.LEVELNAME_FMT_REGEX.sub(
colorized_formatted_levelname,
self._fmt)
def format(self, record):
fmt = self._level_to_fmt_mapping.get(
record.levelno, self._original_fmt)
if six.PY2:
self._fmt = fmt
else:
self._style._fmt = fmt
return super(ColoredLevelFormatter, self).format(record)
def get_option_ini(config, *names):
for name in names:
ret = config.getoption(name) # 'default' arg won't work as expected
@@ -48,6 +101,9 @@ def pytest_addoption(parser):
'--log-date-format',
dest='log_date_format', default=DEFAULT_LOG_DATE_FORMAT,
help='log date format as used by the logging module.')
parser.addini(
'log_cli', default=False, type='bool',
help='enable log display during test run (also known as "live logging").')
add_option_ini(
'--log-cli-level',
dest='log_cli_level', default=None,
@@ -79,39 +135,31 @@ def pytest_addoption(parser):
@contextmanager
def logging_using_handler(handler, logger=None):
"""Context manager that safely registers a given handler."""
logger = logger or logging.getLogger(logger)
if handler in logger.handlers: # reentrancy
# Adding the same handler twice would confuse logging system.
# Just don't do that.
yield
else:
logger.addHandler(handler)
try:
yield
finally:
logger.removeHandler(handler)
@contextmanager
def catching_logs(handler, formatter=None,
level=logging.NOTSET, logger=None):
def catching_logs(handler, formatter=None, level=None):
"""Context manager that prepares the whole logging machinery properly."""
logger = logger or logging.getLogger(logger)
root_logger = logging.getLogger()
if formatter is not None:
handler.setFormatter(formatter)
handler.setLevel(level)
if level is not None:
handler.setLevel(level)
with logging_using_handler(handler, logger):
orig_level = logger.level
logger.setLevel(min(orig_level, level))
try:
yield handler
finally:
logger.setLevel(orig_level)
# Adding the same handler twice would confuse logging system.
# Just don't do that.
add_new_handler = handler not in root_logger.handlers
if add_new_handler:
root_logger.addHandler(handler)
if level is not None:
orig_level = root_logger.level
root_logger.setLevel(level)
try:
yield handler
finally:
if level is not None:
root_logger.setLevel(orig_level)
if add_new_handler:
root_logger.removeHandler(handler)
class LogCaptureHandler(logging.StreamHandler):
@@ -134,11 +182,40 @@ class LogCaptureFixture(object):
def __init__(self, item):
"""Creates a new funcarg."""
self._item = item
self._initial_log_levels = {} # type: Dict[str, int] # dict of log name -> log level
def _finalize(self):
"""Finalizes the fixture.
This restores the log levels changed by :meth:`set_level`.
"""
# restore log levels
for logger_name, level in self._initial_log_levels.items():
logger = logging.getLogger(logger_name)
logger.setLevel(level)
@property
def handler(self):
return self._item.catch_log_handler
def get_records(self, when):
"""
Get the logging records for one of the possible test phases.
:param str when:
Which test phase to obtain the records from. Valid values are: "setup", "call" and "teardown".
:rtype: List[logging.LogRecord]
:return: the list of captured records at the given stage
.. versionadded:: 3.4
"""
handler = self._item.catch_log_handlers.get(when)
if handler:
return handler.records
else:
return []
@property
def text(self):
"""Returns the log text."""
@@ -165,31 +242,31 @@ class LogCaptureFixture(object):
self.handler.records = []
def set_level(self, level, logger=None):
"""Sets the level for capturing of logs.
"""Sets the level for capturing of logs. The level will be restored to its previous value at the end of
the test.
By default, the level is set on the handler used to capture
logs. Specify a logger name to instead set the level of any
logger.
:param int level: the logger to level.
:param str logger: the logger to update the level. If not given, the root logger level is updated.
.. versionchanged:: 3.4
The levels of the loggers changed by this function will be restored to their initial values at the
end of the test.
"""
if logger is None:
logger = self.handler
else:
logger = logging.getLogger(logger)
logger_name = logger
logger = logging.getLogger(logger_name)
# save the original log-level to restore it during teardown
self._initial_log_levels.setdefault(logger_name, logger.level)
logger.setLevel(level)
@contextmanager
def at_level(self, level, logger=None):
"""Context manager that sets the level for capturing of logs.
"""Context manager that sets the level for capturing of logs. After the end of the 'with' statement the
level is restored to its original value.
By default, the level is set on the handler used to capture
logs. Specify a logger name to instead set the level of any
logger.
:param int level: the logger to level.
:param str logger: the logger to update the level. If not given, the root logger level is updated.
"""
if logger is None:
logger = self.handler
else:
logger = logging.getLogger(logger)
logger = logging.getLogger(logger)
orig_level = logger.level
logger.setLevel(level)
try:
@@ -208,7 +285,9 @@ def caplog(request):
* caplog.records() -> list of logging.LogRecord instances
* caplog.record_tuples() -> list of (logger_name, level, message) tuples
"""
return LogCaptureFixture(request.node)
result = LogCaptureFixture(request.node)
yield result
result._finalize()
def get_actual_log_level(config, *setting_names):
@@ -238,8 +317,12 @@ def get_actual_log_level(config, *setting_names):
def pytest_configure(config):
config.pluginmanager.register(LoggingPlugin(config),
'logging-plugin')
config.pluginmanager.register(LoggingPlugin(config), 'logging-plugin')
@contextmanager
def _dummy_context_manager():
yield
class LoggingPlugin(object):
@@ -252,57 +335,52 @@ class LoggingPlugin(object):
The formatter can be safely shared across all handlers so
create a single one for the entire test session here.
"""
self.log_cli_level = get_actual_log_level(
config, 'log_cli_level', 'log_level') or logging.WARNING
self._config = config
# enable verbose output automatically if live logging is enabled
if self._config.getini('log_cli') and not config.getoption('verbose'):
# sanity check: terminal reporter should not have been loaded at this point
assert self._config.pluginmanager.get_plugin('terminalreporter') is None
config.option.verbose = 1
self.print_logs = get_option_ini(config, 'log_print')
self.formatter = logging.Formatter(
get_option_ini(config, 'log_format'),
get_option_ini(config, 'log_date_format'))
log_cli_handler = logging.StreamHandler(sys.stderr)
log_cli_format = get_option_ini(
config, 'log_cli_format', 'log_format')
log_cli_date_format = get_option_ini(
config, 'log_cli_date_format', 'log_date_format')
log_cli_formatter = logging.Formatter(
log_cli_format,
datefmt=log_cli_date_format)
self.log_cli_handler = log_cli_handler # needed for a single unittest
self.live_logs = catching_logs(log_cli_handler,
formatter=log_cli_formatter,
level=self.log_cli_level)
self.formatter = logging.Formatter(get_option_ini(config, 'log_format'),
get_option_ini(config, 'log_date_format'))
self.log_level = get_actual_log_level(config, 'log_level')
log_file = get_option_ini(config, 'log_file')
if log_file:
self.log_file_level = get_actual_log_level(
config, 'log_file_level') or logging.WARNING
self.log_file_level = get_actual_log_level(config, 'log_file_level')
log_file_format = get_option_ini(
config, 'log_file_format', 'log_format')
log_file_date_format = get_option_ini(
config, 'log_file_date_format', 'log_date_format')
self.log_file_handler = logging.FileHandler(
log_file,
# Each pytest runtests session will write to a clean logfile
mode='w')
log_file_formatter = logging.Formatter(
log_file_format,
datefmt=log_file_date_format)
log_file_format = get_option_ini(config, 'log_file_format', 'log_format')
log_file_date_format = get_option_ini(config, 'log_file_date_format', 'log_date_format')
# Each pytest runtests session will write to a clean logfile
self.log_file_handler = logging.FileHandler(log_file, mode='w')
log_file_formatter = logging.Formatter(log_file_format, datefmt=log_file_date_format)
self.log_file_handler.setFormatter(log_file_formatter)
else:
self.log_file_handler = None
# initialized during pytest_runtestloop
self.log_cli_handler = None
@contextmanager
def _runtest_for(self, item, when):
"""Implements the internals of pytest_runtest_xxx() hook."""
with catching_logs(LogCaptureHandler(),
formatter=self.formatter) as log_handler:
formatter=self.formatter, level=self.log_level) as log_handler:
if self.log_cli_handler:
self.log_cli_handler.set_when(when)
if not hasattr(item, 'catch_log_handlers'):
item.catch_log_handlers = {}
item.catch_log_handlers[when] = log_handler
item.catch_log_handler = log_handler
try:
yield # run test
finally:
del item.catch_log_handler
if when == 'teardown':
del item.catch_log_handlers
if self.print_logs:
# Add a captured log section to the report.
@@ -324,10 +402,15 @@ class LoggingPlugin(object):
with self._runtest_for(item, 'teardown'):
yield
def pytest_runtest_logstart(self):
if self.log_cli_handler:
self.log_cli_handler.reset()
@pytest.hookimpl(hookwrapper=True)
def pytest_runtestloop(self, session):
"""Runs all collected test items."""
with self.live_logs:
self._setup_cli_logging()
with self.live_logs_context:
if self.log_file_handler is not None:
with closing(self.log_file_handler):
with catching_logs(self.log_file_handler,
@@ -335,3 +418,69 @@ class LoggingPlugin(object):
yield # run all the tests
else:
yield # run all the tests
def _setup_cli_logging(self):
"""Sets up the handler and logger for the Live Logs feature, if enabled.
This must be done right before starting the loop so we can access the terminal reporter plugin.
"""
terminal_reporter = self._config.pluginmanager.get_plugin('terminalreporter')
if self._config.getini('log_cli') and terminal_reporter is not None:
capture_manager = self._config.pluginmanager.get_plugin('capturemanager')
log_cli_handler = _LiveLoggingStreamHandler(terminal_reporter, capture_manager)
log_cli_format = get_option_ini(self._config, 'log_cli_format', 'log_format')
log_cli_date_format = get_option_ini(self._config, 'log_cli_date_format', 'log_date_format')
if self._config.option.color != 'no' and ColoredLevelFormatter.LEVELNAME_FMT_REGEX.search(log_cli_format):
log_cli_formatter = ColoredLevelFormatter(create_terminal_writer(self._config),
log_cli_format, datefmt=log_cli_date_format)
else:
log_cli_formatter = logging.Formatter(log_cli_format, datefmt=log_cli_date_format)
log_cli_level = get_actual_log_level(self._config, 'log_cli_level', 'log_level')
self.log_cli_handler = log_cli_handler
self.live_logs_context = catching_logs(log_cli_handler, formatter=log_cli_formatter, level=log_cli_level)
else:
self.live_logs_context = _dummy_context_manager()
class _LiveLoggingStreamHandler(logging.StreamHandler):
"""
Custom StreamHandler used by the live logging feature: it will write a newline before the first log message
in each test.
During live logging we must also explicitly disable stdout/stderr capturing otherwise it will get captured
and won't appear in the terminal.
"""
def __init__(self, terminal_reporter, capture_manager):
"""
:param _pytest.terminal.TerminalReporter terminal_reporter:
:param _pytest.capture.CaptureManager capture_manager:
"""
logging.StreamHandler.__init__(self, stream=terminal_reporter)
self.capture_manager = capture_manager
self.reset()
self.set_when(None)
def reset(self):
"""Reset the handler; should be called before the start of each test"""
self._first_record_emitted = False
def set_when(self, when):
"""Prepares for the given test phase (setup/call/teardown)"""
self._when = when
self._section_name_shown = False
def emit(self, record):
if self.capture_manager is not None:
self.capture_manager.suspend_global_capture()
try:
if not self._first_record_emitted or self._when == 'teardown':
self.stream.write('\n')
self._first_record_emitted = True
if not self._section_name_shown:
self.stream.section('live log ' + self._when, sep='-', bold=True)
self._section_name_shown = True
logging.StreamHandler.emit(self, record)
finally:
if self.capture_manager is not None:
self.capture_manager.resume_global_capture()

View File

@@ -1,8 +1,10 @@
""" core implementation of testing process: init, session, runtest loop. """
from __future__ import absolute_import, division, print_function
import contextlib
import functools
import os
import pkgutil
import six
import sys
@@ -10,16 +12,11 @@ import _pytest
from _pytest import nodes
import _pytest._code
import py
try:
from collections import MutableMapping as MappingMixin
except ImportError:
from UserDict import DictMixin as MappingMixin
from _pytest.config import directory_arg, UsageError, hookimpl
from _pytest.outcomes import exit
from _pytest.runner import collect_one_node
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
# exitcodes for the command line
EXIT_OK = 0
@@ -206,7 +203,47 @@ def pytest_ignore_collect(path, config):
return False
class FSHookProxy:
@contextlib.contextmanager
def _patched_find_module():
"""Patch bug in pkgutil.ImpImporter.find_module
When using pkgutil.find_loader on python<3.4 it removes symlinks
from the path due to a call to os.path.realpath. This is not consistent
with actually doing the import (in these versions, pkgutil and __import__
did not share the same underlying code). This can break conftest
discovery for pytest where symlinks are involved.
The only supported python<3.4 by pytest is python 2.7.
"""
if six.PY2: # python 3.4+ uses importlib instead
def find_module_patched(self, fullname, path=None):
# Note: we ignore 'path' argument since it is only used via meta_path
subname = fullname.split(".")[-1]
if subname != fullname and self.path is None:
return None
if self.path is None:
path = None
else:
# original: path = [os.path.realpath(self.path)]
path = [self.path]
try:
file, filename, etc = pkgutil.imp.find_module(subname,
path)
except ImportError:
return None
return pkgutil.ImpLoader(fullname, file, filename, etc)
old_find_module = pkgutil.ImpImporter.find_module
pkgutil.ImpImporter.find_module = find_module_patched
try:
yield
finally:
pkgutil.ImpImporter.find_module = old_find_module
else:
yield
class FSHookProxy(object):
def __init__(self, fspath, pm, remove_mods):
self.fspath = fspath
self.pm = pm
@@ -218,356 +255,6 @@ class FSHookProxy:
return x
class _CompatProperty(object):
def __init__(self, name):
self.name = name
def __get__(self, obj, owner):
if obj is None:
return self
# TODO: reenable in the features branch
# warnings.warn(
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
# name=self.name, owner=type(owner).__name__),
# PendingDeprecationWarning, stacklevel=2)
return getattr(__import__('pytest'), self.name)
class NodeKeywords(MappingMixin):
def __init__(self, node):
self.node = node
self.parent = node.parent
self._markers = {node.name: True}
def __getitem__(self, key):
try:
return self._markers[key]
except KeyError:
if self.parent is None:
raise
return self.parent.keywords[key]
def __setitem__(self, key, value):
self._markers[key] = value
def __delitem__(self, key):
raise ValueError("cannot delete key in keywords dict")
def __iter__(self):
seen = set(self._markers)
if self.parent is not None:
seen.update(self.parent.keywords)
return iter(seen)
def __len__(self):
return len(self.__iter__())
def keys(self):
return list(self)
def __repr__(self):
return "<NodeKeywords for node %s>" % (self.node, )
class Node(object):
""" base class for Collector and Item the test collection tree.
Collector subclasses have children, Items are terminal nodes."""
def __init__(self, name, parent=None, config=None, session=None):
#: a unique name within the scope of the parent node
self.name = name
#: the parent collector node.
self.parent = parent
#: the pytest config object
self.config = config or parent.config
#: the session this node is part of
self.session = session or parent.session
#: filesystem path where this node was collected from (can be None)
self.fspath = getattr(parent, 'fspath', None)
#: keywords/markers collected from all scopes
self.keywords = NodeKeywords(self)
#: allow adding of extra keywords to use for matching
self.extra_keyword_matches = set()
# used for storing artificial fixturedefs for direct parametrization
self._name2pseudofixturedef = {}
@property
def ihook(self):
""" fspath sensitive hook proxy used to call pytest hooks"""
return self.session.gethookproxy(self.fspath)
Module = _CompatProperty("Module")
Class = _CompatProperty("Class")
Instance = _CompatProperty("Instance")
Function = _CompatProperty("Function")
File = _CompatProperty("File")
Item = _CompatProperty("Item")
def _getcustomclass(self, name):
maybe_compatprop = getattr(type(self), name)
if isinstance(maybe_compatprop, _CompatProperty):
return getattr(__import__('pytest'), name)
else:
cls = getattr(self, name)
# TODO: reenable in the features branch
# warnings.warn("use of node.%s is deprecated, "
# "use pytest_pycollect_makeitem(...) to create custom "
# "collection nodes" % name, category=DeprecationWarning)
return cls
def __repr__(self):
return "<%s %r>" % (self.__class__.__name__,
getattr(self, 'name', None))
def warn(self, code, message):
""" generate a warning with the given code and message for this
item. """
assert isinstance(code, str)
fslocation = getattr(self, "location", None)
if fslocation is None:
fslocation = getattr(self, "fspath", None)
self.ihook.pytest_logwarning.call_historic(kwargs=dict(
code=code, message=message,
nodeid=self.nodeid, fslocation=fslocation))
# methods for ordering nodes
@property
def nodeid(self):
""" a ::-separated string denoting its collection tree address. """
try:
return self._nodeid
except AttributeError:
self._nodeid = x = self._makeid()
return x
def _makeid(self):
return self.parent.nodeid + "::" + self.name
def __hash__(self):
return hash(self.nodeid)
def setup(self):
pass
def teardown(self):
pass
def listchain(self):
""" return list of all parent collectors up to self,
starting from root of collection tree. """
chain = []
item = self
while item is not None:
chain.append(item)
item = item.parent
chain.reverse()
return chain
def add_marker(self, marker):
""" dynamically add a marker object to the node.
``marker`` can be a string or pytest.mark.* instance.
"""
from _pytest.mark import MarkDecorator, MARK_GEN
if isinstance(marker, six.string_types):
marker = getattr(MARK_GEN, marker)
elif not isinstance(marker, MarkDecorator):
raise ValueError("is not a string or pytest.mark.* Marker")
self.keywords[marker.name] = marker
def get_marker(self, name):
""" get a marker object from this node or None if
the node doesn't have a marker with that name. """
val = self.keywords.get(name, None)
if val is not None:
from _pytest.mark import MarkInfo, MarkDecorator
if isinstance(val, (MarkDecorator, MarkInfo)):
return val
def listextrakeywords(self):
""" Return a set of all extra keywords in self and any parents."""
extra_keywords = set()
item = self
for item in self.listchain():
extra_keywords.update(item.extra_keyword_matches)
return extra_keywords
def listnames(self):
return [x.name for x in self.listchain()]
def addfinalizer(self, fin):
""" register a function to be called when this node is finalized.
This method can only be called when this node is active
in a setup chain, for example during self.setup().
"""
self.session._setupstate.addfinalizer(fin, self)
def getparent(self, cls):
""" get the next parent node (including ourself)
which is an instance of the given class"""
current = self
while current and not isinstance(current, cls):
current = current.parent
return current
def _prunetraceback(self, excinfo):
pass
def _repr_failure_py(self, excinfo, style=None):
fm = self.session._fixturemanager
if excinfo.errisinstance(fm.FixtureLookupError):
return excinfo.value.formatrepr()
tbfilter = True
if self.config.option.fulltrace:
style = "long"
else:
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
self._prunetraceback(excinfo)
if len(excinfo.traceback) == 0:
excinfo.traceback = tb
tbfilter = False # prunetraceback already does it
if style == "auto":
style = "long"
# XXX should excinfo.getrepr record all data and toterminal() process it?
if style is None:
if self.config.option.tbstyle == "short":
style = "short"
else:
style = "long"
try:
os.getcwd()
abspath = False
except OSError:
abspath = True
return excinfo.getrepr(funcargs=True, abspath=abspath,
showlocals=self.config.option.showlocals,
style=style, tbfilter=tbfilter)
repr_failure = _repr_failure_py
class Collector(Node):
""" Collector instances create children through collect()
and thus iteratively build a tree.
"""
class CollectError(Exception):
""" an error during collection, contains a custom message. """
def collect(self):
""" returns a list of children (items and collectors)
for this collection node.
"""
raise NotImplementedError("abstract")
def repr_failure(self, excinfo):
""" represent a collection failure. """
if excinfo.errisinstance(self.CollectError):
exc = excinfo.value
return str(exc.args[0])
return self._repr_failure_py(excinfo, style="short")
def _prunetraceback(self, excinfo):
if hasattr(self, 'fspath'):
traceback = excinfo.traceback
ntraceback = traceback.cut(path=self.fspath)
if ntraceback == traceback:
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
excinfo.traceback = ntraceback.filter()
class FSCollector(Collector):
def __init__(self, fspath, parent=None, config=None, session=None):
fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
name = fspath.basename
if parent is not None:
rel = fspath.relto(parent.fspath)
if rel:
name = rel
name = name.replace(os.sep, nodes.SEP)
super(FSCollector, self).__init__(name, parent, config, session)
self.fspath = fspath
def _check_initialpaths_for_relpath(self):
for initialpath in self.session._initialpaths:
if self.fspath.common(initialpath) == initialpath:
return self.fspath.relto(initialpath.dirname)
def _makeid(self):
relpath = self.fspath.relto(self.config.rootdir)
if not relpath:
relpath = self._check_initialpaths_for_relpath()
if os.sep != nodes.SEP:
relpath = relpath.replace(os.sep, nodes.SEP)
return relpath
class File(FSCollector):
""" base class for collecting tests from a file. """
class Item(Node):
""" a basic test invocation item. Note that for a single function
there might be multiple test invocation items.
"""
nextitem = None
def __init__(self, name, parent=None, config=None, session=None):
super(Item, self).__init__(name, parent, config, session)
self._report_sections = []
def add_report_section(self, when, key, content):
"""
Adds a new report section, similar to what's done internally to add stdout and
stderr captured output::
item.add_report_section("call", "stdout", "report section contents")
:param str when:
One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
:param str key:
Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
``"stderr"`` internally.
:param str content:
The full contents as a string.
"""
if content:
self._report_sections.append((when, key, content))
def reportinfo(self):
return self.fspath, None, ""
@property
def location(self):
try:
return self._location
except AttributeError:
location = self.reportinfo()
# bestrelpath is a quite slow function
cache = self.config.__dict__.setdefault("_bestrelpathcache", {})
try:
fspath = cache[location[0]]
except KeyError:
fspath = self.session.fspath.bestrelpath(location[0])
cache[location[0]] = fspath
location = (fspath, location[1], str(location[2]))
self._location = location
return location
class NoMatch(Exception):
""" raised if matching cannot locate a matching names. """
@@ -581,13 +268,14 @@ class Failed(Exception):
""" signals an stop as failed test run. """
class Session(FSCollector):
class Session(nodes.FSCollector):
Interrupted = Interrupted
Failed = Failed
def __init__(self, config):
FSCollector.__init__(self, config.rootdir, parent=None,
config=config, session=self)
nodes.FSCollector.__init__(
self, config.rootdir, parent=None,
config=config, session=self)
self.testsfailed = 0
self.testscollected = 0
self.shouldstop = False
@@ -728,9 +416,10 @@ class Session(FSCollector):
"""Convert a dotted module name to path.
"""
import pkgutil
try:
loader = pkgutil.find_loader(x)
with _patched_find_module():
loader = pkgutil.find_loader(x)
except ImportError:
return x
if loader is None:
@@ -738,7 +427,8 @@ class Session(FSCollector):
# This method is sometimes invoked when AssertionRewritingHook, which
# does not define a get_filename method, is already in place:
try:
path = loader.get_filename(x)
with _patched_find_module():
path = loader.get_filename(x)
except AttributeError:
# Retrieve path from AssertionRewritingHook:
path = loader.modules[x][0].co_filename
@@ -782,11 +472,11 @@ class Session(FSCollector):
nextnames = names[1:]
resultnodes = []
for node in matching:
if isinstance(node, Item):
if isinstance(node, nodes.Item):
if not names:
resultnodes.append(node)
continue
assert isinstance(node, Collector)
assert isinstance(node, nodes.Collector)
rep = collect_one_node(node)
if rep.passed:
has_matched = False
@@ -808,11 +498,11 @@ class Session(FSCollector):
def genitems(self, node):
self.trace("genitems", node)
if isinstance(node, Item):
if isinstance(node, nodes.Item):
node.ihook.pytest_itemcollected(item=node)
yield node
else:
assert isinstance(node, Collector)
assert isinstance(node, nodes.Collector)
rep = collect_one_node(node)
if rep.passed:
for subnode in rep.result:

View File

@@ -2,14 +2,19 @@
from __future__ import absolute_import, division, print_function
import inspect
import keyword
import warnings
import attr
from collections import namedtuple
from operator import attrgetter
from six.moves import map
from _pytest.config import UsageError
from .deprecated import MARK_PARAMETERSET_UNPACKING
from .compat import NOTSET, getfslineno
EMPTY_PARAMETERSET_OPTION = "empty_parameter_set_mark"
def alias(name, warning=None):
getter = attrgetter(name)
@@ -70,7 +75,7 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
return cls(argval, marks=newmarks, id=None)
@classmethod
def _for_parameterize(cls, argnames, argvalues, function):
def _for_parameterize(cls, argnames, argvalues, function, config):
if not isinstance(argnames, (tuple, list)):
argnames = [x.strip() for x in argnames.split(",") if x.strip()]
force_tuple = len(argnames) == 1
@@ -82,10 +87,7 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
del argvalues
if not parameters:
fs, lineno = getfslineno(function)
reason = "got empty parameter set %r, function %s at %s:%d" % (
argnames, function.__name__, fs, lineno)
mark = MARK_GEN.skip(reason=reason)
mark = get_empty_parameterset_mark(config, argnames, function)
parameters.append(ParameterSet(
values=(NOTSET,) * len(argnames),
marks=[mark],
@@ -94,6 +96,20 @@ class ParameterSet(namedtuple('ParameterSet', 'values, marks, id')):
return argnames, parameters
def get_empty_parameterset_mark(config, argnames, function):
requested_mark = config.getini(EMPTY_PARAMETERSET_OPTION)
if requested_mark in ('', None, 'skip'):
mark = MARK_GEN.skip
elif requested_mark == 'xfail':
mark = MARK_GEN.xfail(run=False)
else:
raise LookupError(requested_mark)
fs, lineno = getfslineno(function)
reason = "got empty parameter set %r, function %s at %s:%d" % (
argnames, function.__name__, fs, lineno)
return mark(reason=reason)
class MarkerError(Exception):
"""Error in use of a pytest marker/attribute."""
@@ -133,6 +149,9 @@ def pytest_addoption(parser):
)
parser.addini("markers", "markers for test functions", 'linelist')
parser.addini(
EMPTY_PARAMETERSET_OPTION,
"default marker for empty parametersets")
def pytest_cmdline_main(config):
@@ -222,6 +241,9 @@ class KeywordMapping(object):
return False
python_keywords_allowed_list = ["or", "and", "not"]
def matchmark(colitem, markexpr):
"""Tries to match on any marker names, attached to the given colitem."""
return eval(markexpr, {}, MarkMapping.from_keywords(colitem.keywords))
@@ -259,7 +281,13 @@ def matchkeyword(colitem, keywordexpr):
return mapping[keywordexpr]
elif keywordexpr.startswith("not ") and " " not in keywordexpr[4:]:
return not mapping[keywordexpr[4:]]
return eval(keywordexpr, {}, mapping)
for kwd in keywordexpr.split():
if keyword.iskeyword(kwd) and kwd not in python_keywords_allowed_list:
raise UsageError("Python keyword '{}' not accepted in expressions passed to '-k'".format(kwd))
try:
return eval(keywordexpr, {}, mapping)
except SyntaxError:
raise UsageError("Wrong expression passed to '-k': {}".format(keywordexpr))
def pytest_configure(config):
@@ -267,12 +295,19 @@ def pytest_configure(config):
if config.option.strict:
MARK_GEN._config = config
empty_parameterset = config.getini(EMPTY_PARAMETERSET_OPTION)
if empty_parameterset not in ('skip', 'xfail', None, ''):
raise UsageError(
"{!s} must be one of skip and xfail,"
" but it is {!r}".format(EMPTY_PARAMETERSET_OPTION, empty_parameterset))
def pytest_unconfigure(config):
MARK_GEN._config = getattr(config, '_old_mark_config', None)
class MarkGenerator:
class MarkGenerator(object):
""" Factory for :class:`MarkDecorator` objects - exposed as
a ``pytest.mark`` singleton instance. Example::

View File

@@ -88,7 +88,7 @@ def derive_importpath(import_path, raising):
return attr, target
class Notset:
class Notset(object):
def __repr__(self):
return "<notset>"
@@ -96,7 +96,7 @@ class Notset:
notset = Notset()
class MonkeyPatch:
class MonkeyPatch(object):
""" Object returned by the ``monkeypatch`` fixture keeping a record of setattr/item/env/syspath changes.
"""

View File

@@ -1,5 +1,18 @@
from __future__ import absolute_import, division, print_function
from collections import MutableMapping as MappingMixin
import os
import six
import py
import attr
import _pytest
SEP = "/"
tracebackcutdir = py.path.local(_pytest.__file__).dirpath()
def _splitnode(nodeid):
"""Split a nodeid into constituent 'parts'.
@@ -35,3 +48,353 @@ def ischildnode(baseid, nodeid):
if len(node_parts) < len(base_parts):
return False
return node_parts[:len(base_parts)] == base_parts
@attr.s
class _CompatProperty(object):
name = attr.ib()
def __get__(self, obj, owner):
if obj is None:
return self
# TODO: reenable in the features branch
# warnings.warn(
# "usage of {owner!r}.{name} is deprecated, please use pytest.{name} instead".format(
# name=self.name, owner=type(owner).__name__),
# PendingDeprecationWarning, stacklevel=2)
return getattr(__import__('pytest'), self.name)
class NodeKeywords(MappingMixin):
def __init__(self, node):
self.node = node
self.parent = node.parent
self._markers = {node.name: True}
def __getitem__(self, key):
try:
return self._markers[key]
except KeyError:
if self.parent is None:
raise
return self.parent.keywords[key]
def __setitem__(self, key, value):
self._markers[key] = value
def __delitem__(self, key):
raise ValueError("cannot delete key in keywords dict")
def __iter__(self):
seen = set(self._markers)
if self.parent is not None:
seen.update(self.parent.keywords)
return iter(seen)
def __len__(self):
return len(self.__iter__())
def keys(self):
return list(self)
def __repr__(self):
return "<NodeKeywords for node %s>" % (self.node, )
class Node(object):
""" base class for Collector and Item the test collection tree.
Collector subclasses have children, Items are terminal nodes."""
def __init__(self, name, parent=None, config=None, session=None):
#: a unique name within the scope of the parent node
self.name = name
#: the parent collector node.
self.parent = parent
#: the pytest config object
self.config = config or parent.config
#: the session this node is part of
self.session = session or parent.session
#: filesystem path where this node was collected from (can be None)
self.fspath = getattr(parent, 'fspath', None)
#: keywords/markers collected from all scopes
self.keywords = NodeKeywords(self)
#: allow adding of extra keywords to use for matching
self.extra_keyword_matches = set()
# used for storing artificial fixturedefs for direct parametrization
self._name2pseudofixturedef = {}
@property
def ihook(self):
""" fspath sensitive hook proxy used to call pytest hooks"""
return self.session.gethookproxy(self.fspath)
Module = _CompatProperty("Module")
Class = _CompatProperty("Class")
Instance = _CompatProperty("Instance")
Function = _CompatProperty("Function")
File = _CompatProperty("File")
Item = _CompatProperty("Item")
def _getcustomclass(self, name):
maybe_compatprop = getattr(type(self), name)
if isinstance(maybe_compatprop, _CompatProperty):
return getattr(__import__('pytest'), name)
else:
cls = getattr(self, name)
# TODO: reenable in the features branch
# warnings.warn("use of node.%s is deprecated, "
# "use pytest_pycollect_makeitem(...) to create custom "
# "collection nodes" % name, category=DeprecationWarning)
return cls
def __repr__(self):
return "<%s %r>" % (self.__class__.__name__,
getattr(self, 'name', None))
def warn(self, code, message):
""" generate a warning with the given code and message for this
item. """
assert isinstance(code, str)
fslocation = getattr(self, "location", None)
if fslocation is None:
fslocation = getattr(self, "fspath", None)
self.ihook.pytest_logwarning.call_historic(kwargs=dict(
code=code, message=message,
nodeid=self.nodeid, fslocation=fslocation))
# methods for ordering nodes
@property
def nodeid(self):
""" a ::-separated string denoting its collection tree address. """
try:
return self._nodeid
except AttributeError:
self._nodeid = x = self._makeid()
return x
def _makeid(self):
return self.parent.nodeid + "::" + self.name
def __hash__(self):
return hash(self.nodeid)
def setup(self):
pass
def teardown(self):
pass
def listchain(self):
""" return list of all parent collectors up to self,
starting from root of collection tree. """
chain = []
item = self
while item is not None:
chain.append(item)
item = item.parent
chain.reverse()
return chain
def add_marker(self, marker):
""" dynamically add a marker object to the node.
``marker`` can be a string or pytest.mark.* instance.
"""
from _pytest.mark import MarkDecorator, MARK_GEN
if isinstance(marker, six.string_types):
marker = getattr(MARK_GEN, marker)
elif not isinstance(marker, MarkDecorator):
raise ValueError("is not a string or pytest.mark.* Marker")
self.keywords[marker.name] = marker
def get_marker(self, name):
""" get a marker object from this node or None if
the node doesn't have a marker with that name. """
val = self.keywords.get(name, None)
if val is not None:
from _pytest.mark import MarkInfo, MarkDecorator
if isinstance(val, (MarkDecorator, MarkInfo)):
return val
def listextrakeywords(self):
""" Return a set of all extra keywords in self and any parents."""
extra_keywords = set()
item = self
for item in self.listchain():
extra_keywords.update(item.extra_keyword_matches)
return extra_keywords
def listnames(self):
return [x.name for x in self.listchain()]
def addfinalizer(self, fin):
""" register a function to be called when this node is finalized.
This method can only be called when this node is active
in a setup chain, for example during self.setup().
"""
self.session._setupstate.addfinalizer(fin, self)
def getparent(self, cls):
""" get the next parent node (including ourself)
which is an instance of the given class"""
current = self
while current and not isinstance(current, cls):
current = current.parent
return current
def _prunetraceback(self, excinfo):
pass
def _repr_failure_py(self, excinfo, style=None):
fm = self.session._fixturemanager
if excinfo.errisinstance(fm.FixtureLookupError):
return excinfo.value.formatrepr()
tbfilter = True
if self.config.option.fulltrace:
style = "long"
else:
tb = _pytest._code.Traceback([excinfo.traceback[-1]])
self._prunetraceback(excinfo)
if len(excinfo.traceback) == 0:
excinfo.traceback = tb
tbfilter = False # prunetraceback already does it
if style == "auto":
style = "long"
# XXX should excinfo.getrepr record all data and toterminal() process it?
if style is None:
if self.config.option.tbstyle == "short":
style = "short"
else:
style = "long"
try:
os.getcwd()
abspath = False
except OSError:
abspath = True
return excinfo.getrepr(funcargs=True, abspath=abspath,
showlocals=self.config.option.showlocals,
style=style, tbfilter=tbfilter)
repr_failure = _repr_failure_py
class Collector(Node):
""" Collector instances create children through collect()
and thus iteratively build a tree.
"""
class CollectError(Exception):
""" an error during collection, contains a custom message. """
def collect(self):
""" returns a list of children (items and collectors)
for this collection node.
"""
raise NotImplementedError("abstract")
def repr_failure(self, excinfo):
""" represent a collection failure. """
if excinfo.errisinstance(self.CollectError):
exc = excinfo.value
return str(exc.args[0])
return self._repr_failure_py(excinfo, style="short")
def _prunetraceback(self, excinfo):
if hasattr(self, 'fspath'):
traceback = excinfo.traceback
ntraceback = traceback.cut(path=self.fspath)
if ntraceback == traceback:
ntraceback = ntraceback.cut(excludepath=tracebackcutdir)
excinfo.traceback = ntraceback.filter()
class FSCollector(Collector):
def __init__(self, fspath, parent=None, config=None, session=None):
fspath = py.path.local(fspath) # xxx only for test_resultlog.py?
name = fspath.basename
if parent is not None:
rel = fspath.relto(parent.fspath)
if rel:
name = rel
name = name.replace(os.sep, SEP)
super(FSCollector, self).__init__(name, parent, config, session)
self.fspath = fspath
def _check_initialpaths_for_relpath(self):
for initialpath in self.session._initialpaths:
if self.fspath.common(initialpath) == initialpath:
return self.fspath.relto(initialpath.dirname)
def _makeid(self):
relpath = self.fspath.relto(self.config.rootdir)
if not relpath:
relpath = self._check_initialpaths_for_relpath()
if os.sep != SEP:
relpath = relpath.replace(os.sep, SEP)
return relpath
class File(FSCollector):
""" base class for collecting tests from a file. """
class Item(Node):
""" a basic test invocation item. Note that for a single function
there might be multiple test invocation items.
"""
nextitem = None
def __init__(self, name, parent=None, config=None, session=None):
super(Item, self).__init__(name, parent, config, session)
self._report_sections = []
def add_report_section(self, when, key, content):
"""
Adds a new report section, similar to what's done internally to add stdout and
stderr captured output::
item.add_report_section("call", "stdout", "report section contents")
:param str when:
One of the possible capture states, ``"setup"``, ``"call"``, ``"teardown"``.
:param str key:
Name of the section, can be customized at will. Pytest uses ``"stdout"`` and
``"stderr"`` internally.
:param str content:
The full contents as a string.
"""
if content:
self._report_sections.append((when, key, content))
def reportinfo(self):
return self.fspath, None, ""
@property
def location(self):
try:
return self._location
except AttributeError:
location = self.reportinfo()
# bestrelpath is a quite slow function
cache = self.config.__dict__.setdefault("_bestrelpathcache", {})
try:
fspath = cache[location[0]]
except KeyError:
fspath = self.session.fspath.bestrelpath(location[0])
cache[location[0]] = fspath
location = (fspath, location[1], str(location[2]))
self._location = location
return location

View File

@@ -1,4 +1,4 @@
""" (disabled by default) support for testing pytest and pytest plugins. """
"""(disabled by default) support for testing pytest and pytest plugins."""
from __future__ import absolute_import, division, print_function
import codecs
@@ -26,14 +26,18 @@ from _pytest.assertion.rewrite import AssertionRewritingHook
PYTEST_FULLPATH = os.path.abspath(pytest.__file__.rstrip("oc")).replace("$py.class", ".py")
IGNORE_PAM = [ # filenames added when obtaining details about the current user
u'/var/lib/sss/mc/passwd'
]
def pytest_addoption(parser):
# group = parser.getgroup("pytester", "pytester (self-tests) options")
parser.addoption('--lsof',
action="store_true", dest="lsof", default=False,
help=("run FD checks if lsof is available"))
parser.addoption('--runpytest', default="inprocess", dest="runpytest",
choices=("inprocess", "subprocess", ),
choices=("inprocess", "subprocess"),
help=("run pytest sub runs in tests using an 'inprocess' "
"or 'subprocess' (python -m main) method"))
@@ -67,6 +71,8 @@ class LsofFdLeakChecker(object):
fields = line.split('\0')
fd = fields[0][1:]
filename = fields[1][1:]
if filename in IGNORE_PAM:
continue
if filename.startswith('/'):
open_files.append((fd, filename))
@@ -76,8 +82,8 @@ class LsofFdLeakChecker(object):
try:
py.process.cmdexec("lsof -v")
except (py.process.cmdexec.Error, UnicodeDecodeError):
# cmdexec may raise UnicodeDecodeError on Windows systems
# with locale other than english:
# cmdexec may raise UnicodeDecodeError on Windows systems with
# locale other than English:
# https://bitbucket.org/pytest-dev/py/issues/66
return False
else:
@@ -132,7 +138,7 @@ def getexecutable(name, cache={}):
if "2.5.2" in err:
executable = None # http://bugs.jython.org/issue1790
elif popen.returncode != 0:
# Handle pyenv's 127.
# handle pyenv's 127
executable = None
cache[name] = executable
return executable
@@ -157,14 +163,15 @@ def anypython(request):
@pytest.fixture
def _pytest(request):
""" Return a helper which offers a gethookrecorder(hook)
method which returns a HookRecorder instance which helps
to make assertions about called hooks.
"""Return a helper which offers a gethookrecorder(hook) method which
returns a HookRecorder instance which helps to make assertions about called
hooks.
"""
return PytestArg(request)
class PytestArg:
class PytestArg(object):
def __init__(self, request):
self.request = request
@@ -179,7 +186,7 @@ def get_public_names(values):
return [x for x in values if x[0] != "_"]
class ParsedCall:
class ParsedCall(object):
def __init__(self, name, kwargs):
self.__dict__.update(kwargs)
self._name = name
@@ -190,11 +197,11 @@ class ParsedCall:
return "<ParsedCall %r(**%r)>" % (self._name, d)
class HookRecorder:
class HookRecorder(object):
"""Record all hooks called in a plugin manager.
This wraps all the hook calls in the plugin manager, recording
each call before propagating the normal calls.
This wraps all the hook calls in the plugin manager, recording each call
before propagating the normal calls.
"""
@@ -262,7 +269,7 @@ class HookRecorder:
def matchreport(self, inamepart="",
names="pytest_runtest_logreport pytest_collectreport", when=None):
""" return a testreport whose dotted import path matches """
"""return a testreport whose dotted import path matches"""
values = []
for rep in self.getreports(names=names):
try:
@@ -336,19 +343,19 @@ def testdir(request, tmpdir_factory):
rex_outcome = re.compile(r"(\d+) ([\w-]+)")
class RunResult:
class RunResult(object):
"""The result of running a command.
Attributes:
:ret: The return value.
:outlines: List of lines captured from stdout.
:errlines: List of lines captures from stderr.
:ret: the return value
:outlines: list of lines captured from stdout
:errlines: list of lines captures from stderr
:stdout: :py:class:`LineMatcher` of stdout, use ``stdout.str()`` to
reconstruct stdout or the commonly used
``stdout.fnmatch_lines()`` method.
:stderrr: :py:class:`LineMatcher` of stderr.
:duration: Duration in seconds.
reconstruct stdout or the commonly used ``stdout.fnmatch_lines()``
method
:stderr: :py:class:`LineMatcher` of stderr
:duration: duration in seconds
"""
@@ -361,8 +368,10 @@ class RunResult:
self.duration = duration
def parseoutcomes(self):
""" Return a dictionary of outcomestring->num from parsing
the terminal output that the test process produced."""
"""Return a dictionary of outcomestring->num from parsing the terminal
output that the test process produced.
"""
for line in reversed(self.outlines):
if 'seconds' in line:
outcomes = rex_outcome.findall(line)
@@ -374,8 +383,10 @@ class RunResult:
raise ValueError("Pytest terminal report not found")
def assert_outcomes(self, passed=0, skipped=0, failed=0, error=0):
""" assert that the specified outcomes appear with the respective
numbers (0 means it didn't occur) in the text output from a test run."""
"""Assert that the specified outcomes appear with the respective
numbers (0 means it didn't occur) in the text output from a test run.
"""
d = self.parseoutcomes()
obtained = {
'passed': d.get('passed', 0),
@@ -386,24 +397,50 @@ class RunResult:
assert obtained == dict(passed=passed, skipped=skipped, failed=failed, error=error)
class Testdir:
class CwdSnapshot(object):
def __init__(self):
self.__saved = os.getcwd()
def restore(self):
os.chdir(self.__saved)
class SysModulesSnapshot(object):
def __init__(self, preserve=None):
self.__preserve = preserve
self.__saved = dict(sys.modules)
def restore(self):
if self.__preserve:
self.__saved.update(
(k, m) for k, m in sys.modules.items() if self.__preserve(k))
sys.modules.clear()
sys.modules.update(self.__saved)
class SysPathsSnapshot(object):
def __init__(self):
self.__saved = list(sys.path), list(sys.meta_path)
def restore(self):
sys.path[:], sys.meta_path[:] = self.__saved
class Testdir(object):
"""Temporary test directory with tools to test/run pytest itself.
This is based on the ``tmpdir`` fixture but provides a number of
methods which aid with testing pytest itself. Unless
:py:meth:`chdir` is used all methods will use :py:attr:`tmpdir` as
current working directory.
This is based on the ``tmpdir`` fixture but provides a number of methods
which aid with testing pytest itself. Unless :py:meth:`chdir` is used all
methods will use :py:attr:`tmpdir` as their current working directory.
Attributes:
:tmpdir: The :py:class:`py.path.local` instance of the temporary
directory.
:tmpdir: The :py:class:`py.path.local` instance of the temporary directory.
:plugins: A list of plugins to use with :py:meth:`parseconfig` and
:py:meth:`runpytest`. Initially this is an empty list but
plugins can be added to the list. The type of items to add to
the list depend on the method which uses them so refer to them
for details.
:py:meth:`runpytest`. Initially this is an empty list but plugins can
be added to the list. The type of items to add to the list depends on
the method using them so refer to them for details.
"""
@@ -413,9 +450,10 @@ class Testdir:
name = request.function.__name__
self.tmpdir = tmpdir_factory.mktemp(name, numbered=True)
self.plugins = []
self._savesyspath = (list(sys.path), list(sys.meta_path))
self._savemodulekeys = set(sys.modules)
self.chdir() # always chdir
self._cwd_snapshot = CwdSnapshot()
self._sys_path_snapshot = SysPathsSnapshot()
self._sys_modules_snapshot = self.__take_sys_modules_snapshot()
self.chdir()
self.request.addfinalizer(self.finalize)
method = self.request.config.getoption("--runpytest")
if method == "inprocess":
@@ -429,29 +467,22 @@ class Testdir:
def finalize(self):
"""Clean up global state artifacts.
Some methods modify the global interpreter state and this
tries to clean this up. It does not remove the temporary
directory however so it can be looked at after the test run
has finished.
Some methods modify the global interpreter state and this tries to
clean this up. It does not remove the temporary directory however so
it can be looked at after the test run has finished.
"""
sys.path[:], sys.meta_path[:] = self._savesyspath
if hasattr(self, '_olddir'):
self._olddir.chdir()
self.delete_loaded_modules()
self._sys_modules_snapshot.restore()
self._sys_path_snapshot.restore()
self._cwd_snapshot.restore()
def delete_loaded_modules(self):
"""Delete modules that have been loaded during a test.
This allows the interpreter to catch module changes in case
the module is re-imported.
"""
for name in set(sys.modules).difference(self._savemodulekeys):
# some zope modules used by twisted-related tests keeps internal
# state and can't be deleted; we had some trouble in the past
# with zope.interface for example
if not name.startswith("zope"):
del sys.modules[name]
def __take_sys_modules_snapshot(self):
# some zope modules used by twisted-related tests keep internal state
# and can't be deleted; we had some trouble in the past with
# `zope.interface` for example
def preserve_module(name):
return name.startswith("zope")
return SysModulesSnapshot(preserve=preserve_module)
def make_hook_recorder(self, pluginmanager):
"""Create a new :py:class:`HookRecorder` for a PluginManager."""
@@ -466,9 +497,7 @@ class Testdir:
This is done automatically upon instantiation.
"""
old = self.tmpdir.chdir()
if not hasattr(self, '_olddir'):
self._olddir = old
self.tmpdir.chdir()
def _makefile(self, ext, args, kwargs, encoding='utf-8'):
items = list(kwargs.items())
@@ -495,17 +524,15 @@ class Testdir:
def makefile(self, ext, *args, **kwargs):
"""Create a new file in the testdir.
ext: The extension the file should use, including the dot.
E.g. ".py".
ext: The extension the file should use, including the dot, e.g. `.py`.
args: All args will be treated as strings and joined using
newlines. The result will be written as contents to the
file. The name of the file will be based on the test
function requesting this fixture.
args: All args will be treated as strings and joined using newlines.
The result will be written as contents to the file. The name of the
file will be based on the test function requesting this fixture.
E.g. "testdir.makefile('.txt', 'line1', 'line2')"
kwargs: Each keyword is the name of a file, while the value of
it will be written as contents of the file.
kwargs: Each keyword is the name of a file, while the value of it will
be written as contents of the file.
E.g. "testdir.makefile('.ini', pytest='[pytest]\naddopts=-rs\n')"
"""
@@ -535,14 +562,16 @@ class Testdir:
def syspathinsert(self, path=None):
"""Prepend a directory to sys.path, defaults to :py:attr:`tmpdir`.
This is undone automatically after the test.
This is undone automatically when this object dies at the end of each
test.
"""
if path is None:
path = self.tmpdir
sys.path.insert(0, str(path))
# a call to syspathinsert() usually means that the caller
# wants to import some dynamically created files.
# with python3 we thus invalidate import caches.
# a call to syspathinsert() usually means that the caller wants to
# import some dynamically created files, thus with python3 we
# invalidate its import caches
self._possibly_invalidate_import_caches()
def _possibly_invalidate_import_caches(self):
@@ -562,8 +591,8 @@ class Testdir:
def mkpydir(self, name):
"""Create a new python package.
This creates a (sub)directory with an empty ``__init__.py``
file so that is recognised as a python package.
This creates a (sub)directory with an empty ``__init__.py`` file so it
gets recognised as a python package.
"""
p = self.mkdir(name)
@@ -576,10 +605,10 @@ class Testdir:
"""Return the collection node of a file.
:param config: :py:class:`_pytest.config.Config` instance, see
:py:meth:`parseconfig` and :py:meth:`parseconfigure` to
create the configuration.
:py:meth:`parseconfig` and :py:meth:`parseconfigure` to create the
configuration
:param arg: A :py:class:`py.path.local` instance of the file.
:param arg: a :py:class:`py.path.local` instance of the file
"""
session = Session(config)
@@ -593,11 +622,10 @@ class Testdir:
def getpathnode(self, path):
"""Return the collection node of a file.
This is like :py:meth:`getnode` but uses
:py:meth:`parseconfigure` to create the (configured) pytest
Config instance.
This is like :py:meth:`getnode` but uses :py:meth:`parseconfigure` to
create the (configured) pytest Config instance.
:param path: A :py:class:`py.path.local` instance of the file.
:param path: a :py:class:`py.path.local` instance of the file
"""
config = self.parseconfigure(path)
@@ -611,8 +639,8 @@ class Testdir:
def genitems(self, colitems):
"""Generate all test items from a collection node.
This recurses into the collection node and returns a list of
all the test items contained within.
This recurses into the collection node and returns a list of all the
test items contained within.
"""
session = colitems[0].session
@@ -624,10 +652,10 @@ class Testdir:
def runitem(self, source):
"""Run the "test_func" Item.
The calling test instance (the class which contains the test
method) must provide a ``.getrunner()`` method which should
return a runner which can run the test protocol for a single
item, like e.g. :py:func:`_pytest.runner.runtestprotocol`.
The calling test instance (class containing the test method) must
provide a ``.getrunner()`` method which should return a runner which
can run the test protocol for a single item, e.g.
:py:func:`_pytest.runner.runtestprotocol`.
"""
# used from runner functional tests
@@ -641,14 +669,14 @@ class Testdir:
"""Run a test module in process using ``pytest.main()``.
This run writes "source" into a temporary file and runs
``pytest.main()`` on it, returning a :py:class:`HookRecorder`
instance for the result.
``pytest.main()`` on it, returning a :py:class:`HookRecorder` instance
for the result.
:param source: The source code of the test module.
:param source: the source code of the test module
:param cmdlineargs: Any extra command line arguments to use.
:param cmdlineargs: any extra command line arguments to use
:return: :py:class:`HookRecorder` instance of the result.
:return: :py:class:`HookRecorder` instance of the result
"""
p = self.makepyfile(source)
@@ -658,13 +686,9 @@ class Testdir:
def inline_genitems(self, *args):
"""Run ``pytest.main(['--collectonly'])`` in-process.
Returns a tuple of the collected items and a
:py:class:`HookRecorder` instance.
This runs the :py:func:`pytest.main` function to run all of
pytest inside the test process itself like
:py:meth:`inline_run`. However the return value is a tuple of
the collection items and a :py:class:`HookRecorder` instance.
Runs the :py:func:`pytest.main` function to run all of pytest inside
the test process itself like :py:meth:`inline_run`, but returns a
tuple of the collected items and a :py:class:`HookRecorder` instance.
"""
rec = self.inline_run("--collect-only", *args)
@@ -674,60 +698,78 @@ class Testdir:
def inline_run(self, *args, **kwargs):
"""Run ``pytest.main()`` in-process, returning a HookRecorder.
This runs the :py:func:`pytest.main` function to run all of
pytest inside the test process itself. This means it can
return a :py:class:`HookRecorder` instance which gives more
detailed results from then run then can be done by matching
stdout/stderr from :py:meth:`runpytest`.
Runs the :py:func:`pytest.main` function to run all of pytest inside
the test process itself. This means it can return a
:py:class:`HookRecorder` instance which gives more detailed results
from that run than can be done by matching stdout/stderr from
:py:meth:`runpytest`.
:param args: Any command line arguments to pass to
:py:func:`pytest.main`.
:param args: command line arguments to pass to :py:func:`pytest.main`
:param plugin: (keyword-only) Extra plugin instances the
``pytest.main()`` instance should use.
:param plugin: (keyword-only) extra plugin instances the
``pytest.main()`` instance should use
:return: a :py:class:`HookRecorder` instance
:return: A :py:class:`HookRecorder` instance.
"""
# When running py.test inline any plugins active in the main
# test process are already imported. So this disables the
# warning which will trigger to say they can no longer be
# rewritten, which is fine as they are already rewritten.
orig_warn = AssertionRewritingHook._warn_already_imported
finalizers = []
try:
# When running py.test inline any plugins active in the main test
# process are already imported. So this disables the warning which
# will trigger to say they can no longer be rewritten, which is
# fine as they have already been rewritten.
orig_warn = AssertionRewritingHook._warn_already_imported
def revert():
AssertionRewritingHook._warn_already_imported = orig_warn
def revert_warn_already_imported():
AssertionRewritingHook._warn_already_imported = orig_warn
finalizers.append(revert_warn_already_imported)
AssertionRewritingHook._warn_already_imported = lambda *a: None
self.request.addfinalizer(revert)
AssertionRewritingHook._warn_already_imported = lambda *a: None
# Any sys.module or sys.path changes done while running py.test
# inline should be reverted after the test run completes to avoid
# clashing with later inline tests run within the same pytest test,
# e.g. just because they use matching test module names.
finalizers.append(self.__take_sys_modules_snapshot().restore)
finalizers.append(SysPathsSnapshot().restore)
rec = []
# Important note:
# - our tests should not leave any other references/registrations
# laying around other than possibly loaded test modules
# referenced from sys.modules, as nothing will clean those up
# automatically
class Collect:
def pytest_configure(x, config):
rec.append(self.make_hook_recorder(config.pluginmanager))
rec = []
plugins = kwargs.get("plugins") or []
plugins.append(Collect())
ret = pytest.main(list(args), plugins=plugins)
self.delete_loaded_modules()
if len(rec) == 1:
reprec = rec.pop()
else:
class reprec:
pass
reprec.ret = ret
class Collect(object):
def pytest_configure(x, config):
rec.append(self.make_hook_recorder(config.pluginmanager))
# typically we reraise keyboard interrupts from the child run
# because it's our user requesting interruption of the testing
if ret == 2 and not kwargs.get("no_reraise_ctrlc"):
calls = reprec.getcalls("pytest_keyboard_interrupt")
if calls and calls[-1].excinfo.type == KeyboardInterrupt:
raise KeyboardInterrupt()
return reprec
plugins = kwargs.get("plugins") or []
plugins.append(Collect())
ret = pytest.main(list(args), plugins=plugins)
if len(rec) == 1:
reprec = rec.pop()
else:
class reprec(object):
pass
reprec.ret = ret
# typically we reraise keyboard interrupts from the child run
# because it's our user requesting interruption of the testing
if ret == 2 and not kwargs.get("no_reraise_ctrlc"):
calls = reprec.getcalls("pytest_keyboard_interrupt")
if calls and calls[-1].excinfo.type == KeyboardInterrupt:
raise KeyboardInterrupt()
return reprec
finally:
for finalizer in finalizers:
finalizer()
def runpytest_inprocess(self, *args, **kwargs):
""" Return result of running pytest in-process, providing a similar
interface to what self.runpytest() provides. """
"""Return result of running pytest in-process, providing a similar
interface to what self.runpytest() provides.
"""
if kwargs.get("syspathinsert"):
self.syspathinsert()
now = time.time()
@@ -738,13 +780,13 @@ class Testdir:
reprec = self.inline_run(*args, **kwargs)
except SystemExit as e:
class reprec:
class reprec(object):
ret = e.args[0]
except Exception:
traceback.print_exc()
class reprec:
class reprec(object):
ret = 3
finally:
out, err = capture.readouterr()
@@ -759,7 +801,7 @@ class Testdir:
return res
def runpytest(self, *args, **kwargs):
""" Run pytest inline or in a subprocess, depending on the command line
"""Run pytest inline or in a subprocess, depending on the command line
option "--runpytest" and return a :py:class:`RunResult`.
"""
@@ -780,13 +822,13 @@ class Testdir:
def parseconfig(self, *args):
"""Return a new pytest Config instance from given commandline args.
This invokes the pytest bootstrapping code in _pytest.config
to create a new :py:class:`_pytest.core.PluginManager` and
call the pytest_cmdline_parse hook to create new
This invokes the pytest bootstrapping code in _pytest.config to create
a new :py:class:`_pytest.core.PluginManager` and call the
pytest_cmdline_parse hook to create a new
:py:class:`_pytest.config.Config` instance.
If :py:attr:`plugins` has been populated they should be plugin
modules which will be registered with the PluginManager.
If :py:attr:`plugins` has been populated they should be plugin modules
to be registered with the PluginManager.
"""
args = self._ensure_basetemp(args)
@@ -802,9 +844,8 @@ class Testdir:
def parseconfigure(self, *args):
"""Return a new pytest configured Config instance.
This returns a new :py:class:`_pytest.config.Config` instance
like :py:meth:`parseconfig`, but also calls the
pytest_configure hook.
This returns a new :py:class:`_pytest.config.Config` instance like
:py:meth:`parseconfig`, but also calls the pytest_configure hook.
"""
config = self.parseconfig(*args)
@@ -815,14 +856,14 @@ class Testdir:
def getitem(self, source, funcname="test_func"):
"""Return the test item for a test function.
This writes the source to a python file and runs pytest's
collection on the resulting module, returning the test item
for the requested function name.
This writes the source to a python file and runs pytest's collection on
the resulting module, returning the test item for the requested
function name.
:param source: The module source.
:param source: the module source
:param funcname: The name of the test function for which the
Item must be returned.
:param funcname: the name of the test function for which to return a
test item
"""
items = self.getitems(source)
@@ -835,9 +876,8 @@ class Testdir:
def getitems(self, source):
"""Return all test items collected from the module.
This writes the source to a python file and runs pytest's
collection on the resulting module, returning all test items
contained within.
This writes the source to a python file and runs pytest's collection on
the resulting module, returning all test items contained within.
"""
modcol = self.getmodulecol(source)
@@ -846,17 +886,17 @@ class Testdir:
def getmodulecol(self, source, configargs=(), withinit=False):
"""Return the module collection node for ``source``.
This writes ``source`` to a file using :py:meth:`makepyfile`
and then runs the pytest collection on it, returning the
collection node for the test module.
This writes ``source`` to a file using :py:meth:`makepyfile` and then
runs the pytest collection on it, returning the collection node for the
test module.
:param source: The source code of the module to collect.
:param source: the source code of the module to collect
:param configargs: Any extra arguments to pass to
:py:meth:`parseconfigure`.
:param configargs: any extra arguments to pass to
:py:meth:`parseconfigure`
:param withinit: Whether to also write a ``__init__.py`` file
to the temporary directory to ensure it is a package.
:param withinit: whether to also write an ``__init__.py`` file to the
same directory to ensure it is a package
"""
kw = {self.request.function.__name__: Source(source).strip()}
@@ -871,13 +911,12 @@ class Testdir:
def collect_by_name(self, modcol, name):
"""Return the collection node for name from the module collection.
This will search a module collection node for a collection
node matching the given name.
This will search a module collection node for a collection node
matching the given name.
:param modcol: A module collection node, see
:py:meth:`getmodulecol`.
:param modcol: a module collection node; see :py:meth:`getmodulecol`
:param name: The name of the node to return.
:param name: the name of the node to return
"""
if modcol not in self._mod_collections:
@@ -889,8 +928,8 @@ class Testdir:
def popen(self, cmdargs, stdout, stderr, **kw):
"""Invoke subprocess.Popen.
This calls subprocess.Popen making sure the current working
directory is the PYTHONPATH.
This calls subprocess.Popen making sure the current working directory
is in the PYTHONPATH.
You probably want to use :py:meth:`run` instead.
@@ -908,8 +947,7 @@ class Testdir:
def run(self, *cmdargs):
"""Run a command with arguments.
Run a process using subprocess.Popen saving the stdout and
stderr.
Run a process using subprocess.Popen saving the stdout and stderr.
Returns a :py:class:`RunResult`.
@@ -952,14 +990,15 @@ class Testdir:
print("couldn't print to %s because of encoding" % (fp,))
def _getpytestargs(self):
# we cannot use "(sys.executable,script)"
# because on windows the script is e.g. a pytest.exe
# we cannot use `(sys.executable, script)` because on Windows the
# script is e.g. `pytest.exe`
return (sys.executable, PYTEST_FULLPATH) # noqa
def runpython(self, script):
"""Run a python script using sys.executable as interpreter.
Returns a :py:class:`RunResult`.
"""
return self.run(sys.executable, script)
@@ -970,25 +1009,18 @@ class Testdir:
def runpytest_subprocess(self, *args, **kwargs):
"""Run pytest as a subprocess with given arguments.
Any plugins added to the :py:attr:`plugins` list will added
using the ``-p`` command line option. Addtionally
``--basetemp`` is used put any temporary files and directories
in a numbered directory prefixed with "runpytest-" so they do
not conflict with the normal numberd pytest location for
temporary files and directories.
Any plugins added to the :py:attr:`plugins` list will added using the
``-p`` command line option. Additionally ``--basetemp`` is used put
any temporary files and directories in a numbered directory prefixed
with "runpytest-" so they do not conflict with the normal numbered
pytest location for temporary files and directories.
Returns a :py:class:`RunResult`.
"""
p = py.path.local.make_numbered_dir(prefix="runpytest-",
keep=None, rootdir=self.tmpdir)
args = ('--basetemp=%s' % p, ) + args
# for x in args:
# if '--confcutdir' in str(x):
# break
# else:
# pass
# args = ('--confcutdir=.',) + args
args = ('--basetemp=%s' % p,) + args
plugins = [x for x in self.plugins if isinstance(x, str)]
if plugins:
args = ('-p', plugins[0]) + args
@@ -998,8 +1030,8 @@ class Testdir:
def spawn_pytest(self, string, expect_timeout=10.0):
"""Run pytest using pexpect.
This makes sure to use the right pytest and sets up the
temporary directory locations.
This makes sure to use the right pytest and sets up the temporary
directory locations.
The pexpect child is returned.
@@ -1013,6 +1045,7 @@ class Testdir:
"""Run a command using pexpect.
The pexpect child is returned.
"""
pexpect = pytest.importorskip("pexpect", "3.0")
if hasattr(sys, 'pypy_version_info') and '64' in platform.machine():
@@ -1034,13 +1067,15 @@ def getdecoded(out):
py.io.saferepr(out),)
class LineComp:
class LineComp(object):
def __init__(self):
self.stringio = py.io.TextIO()
def assert_contains_lines(self, lines2):
""" assert that lines2 are contained (linearly) in lines1.
return a list of extralines found.
"""Assert that lines2 are contained (linearly) in lines1.
Return a list of extralines found.
"""
__tracebackhide__ = True
val = self.stringio.getvalue()
@@ -1050,14 +1085,14 @@ class LineComp:
return LineMatcher(lines1).fnmatch_lines(lines2)
class LineMatcher:
class LineMatcher(object):
"""Flexible matching of text.
This is a convenience class to test large texts like the output of
commands.
The constructor takes a list of lines without their trailing
newlines, i.e. ``text.splitlines()``.
The constructor takes a list of lines without their trailing newlines, i.e.
``text.splitlines()``.
"""
@@ -1077,18 +1112,19 @@ class LineMatcher:
return lines2
def fnmatch_lines_random(self, lines2):
"""Check lines exist in the output using ``fnmatch.fnmatch``, in any order.
"""Check lines exist in the output using in any order.
Lines are checked using ``fnmatch.fnmatch``. The argument is a list of
lines which have to occur in the output, in any order.
The argument is a list of lines which have to occur in the
output, in any order.
"""
self._match_lines_random(lines2, fnmatch)
def re_match_lines_random(self, lines2):
"""Check lines exist in the output using ``re.match``, in any order.
The argument is a list of lines which have to occur in the
output, in any order.
The argument is a list of lines which have to occur in the output, in
any order.
"""
self._match_lines_random(lines2, lambda name, pat: re.match(pat, name))
@@ -1096,8 +1132,8 @@ class LineMatcher:
def _match_lines_random(self, lines2, match_func):
"""Check lines exist in the output.
The argument is a list of lines which have to occur in the
output, in any order. Each line can contain glob whildcards.
The argument is a list of lines which have to occur in the output, in
any order. Each line can contain glob whildcards.
"""
lines2 = self._getlines(lines2)
@@ -1114,6 +1150,7 @@ class LineMatcher:
"""Return all lines following the given line in the text.
The given line can contain glob wildcards.
"""
for i, line in enumerate(self.lines):
if fnline == line or fnmatch(line, fnline):
@@ -1130,10 +1167,9 @@ class LineMatcher:
def fnmatch_lines(self, lines2):
"""Search captured text for matching lines using ``fnmatch.fnmatch``.
The argument is a list of lines which have to match and can
use glob wildcards. If they do not match a pytest.fail() is
called. The matches and non-matches are also printed on
stdout.
The argument is a list of lines which have to match and can use glob
wildcards. If they do not match a pytest.fail() is called. The
matches and non-matches are also printed on stdout.
"""
self._match_lines(lines2, fnmatch, 'fnmatch')
@@ -1144,21 +1180,22 @@ class LineMatcher:
The argument is a list of lines which have to match using ``re.match``.
If they do not match a pytest.fail() is called.
The matches and non-matches are also printed on
stdout.
The matches and non-matches are also printed on stdout.
"""
self._match_lines(lines2, lambda name, pat: re.match(pat, name), 're.match')
def _match_lines(self, lines2, match_func, match_nickname):
"""Underlying implementation of ``fnmatch_lines`` and ``re_match_lines``.
:param list[str] lines2: list of string patterns to match. The actual format depends on
``match_func``.
:param match_func: a callable ``match_func(line, pattern)`` where line is the captured
line from stdout/stderr and pattern is the matching pattern.
:param list[str] lines2: list of string patterns to match. The actual
format depends on ``match_func``
:param match_func: a callable ``match_func(line, pattern)`` where line
is the captured line from stdout/stderr and pattern is the matching
pattern
:param str match_nickname: the nickname for the match function that
will be logged to stdout when a match occurs
:param str match_nickname: the nickname for the match function that will be logged
to stdout when a match occurs.
"""
lines2 = self._getlines(lines2)
lines1 = self.lines[:]

View File

@@ -19,7 +19,7 @@ from _pytest.config import hookimpl
import _pytest
import pluggy
from _pytest import fixtures
from _pytest import main
from _pytest import nodes
from _pytest import deprecated
from _pytest.compat import (
isclass, isfunction, is_generator, ascii_escaped,
@@ -30,9 +30,17 @@ from _pytest.compat import (
from _pytest.outcomes import fail
from _pytest.mark import transfer_markers
cutdir1 = py.path.local(pluggy.__file__.rstrip("oc"))
cutdir2 = py.path.local(_pytest.__file__).dirpath()
cutdir3 = py.path.local(py.__file__).dirpath()
# relative paths that we use to filter traceback entries from appearing to the user;
# see filter_traceback
# note: if we need to add more paths than what we have now we should probably use a list
# for better maintenance
_pluggy_dir = py.path.local(pluggy.__file__.rstrip("oc"))
# pluggy is either a package or a single module depending on the version
if _pluggy_dir.basename == '__init__.py':
_pluggy_dir = _pluggy_dir.dirpath()
_pytest_dir = py.path.local(_pytest.__file__).dirpath()
_py_dir = py.path.local(py.__file__).dirpath()
def filter_traceback(entry):
@@ -47,10 +55,10 @@ def filter_traceback(entry):
is_generated = '<' in raw_filename and '>' in raw_filename
if is_generated:
return False
# entry.path might point to an inexisting file, in which case it will
# alsso return a str object. see #1133
# entry.path might point to an non-existing file, in which case it will
# also return a str object. see #1133
p = py.path.local(entry.path)
return p != cutdir1 and not p.relto(cutdir2) and not p.relto(cutdir3)
return not p.relto(_pluggy_dir) and not p.relto(_pytest_dir) and not p.relto(_py_dir)
def pyobj_property(name):
@@ -261,7 +269,7 @@ class PyobjMixin(PyobjContext):
return fspath, lineno, modpath
class PyCollector(PyobjMixin, main.Collector):
class PyCollector(PyobjMixin, nodes.Collector):
def funcnamefilter(self, name):
return self._matches_prefix_or_glob_option('python_functions', name)
@@ -386,7 +394,7 @@ class PyCollector(PyobjMixin, main.Collector):
)
class Module(main.File, PyCollector):
class Module(nodes.File, PyCollector):
""" Collector for test classes and functions. """
def _getobj(self):
@@ -563,7 +571,6 @@ class FunctionMixin(PyobjMixin):
if ntraceback == traceback:
ntraceback = ntraceback.cut(path=path)
if ntraceback == traceback:
# ntraceback = ntraceback.cut(excludepath=cutdir2)
ntraceback = ntraceback.filter(filter_traceback)
if not ntraceback:
ntraceback = traceback
@@ -779,7 +786,7 @@ class Metafunc(fixtures.FuncargnamesCompatAttr):
from _pytest.mark import ParameterSet
from py.io import saferepr
argnames, parameters = ParameterSet._for_parameterize(
argnames, argvalues, self.function)
argnames, argvalues, self.function, self.config)
del argvalues
if scope is None:
@@ -933,7 +940,7 @@ def _idval(val, argname, idx, idfn, config=None):
return ascii_escaped(val.pattern)
elif enum is not None and isinstance(val, enum.Enum):
return str(val)
elif isclass(val) and hasattr(val, '__name__'):
elif (isclass(val) or isfunction(val)) and hasattr(val, '__name__'):
return val.__name__
return str(argname) + str(idx)
@@ -1090,7 +1097,7 @@ def write_docstring(tw, doc):
tw.write(INDENT + line + "\n")
class Function(FunctionMixin, main.Item, fixtures.FuncargnamesCompatAttr):
class Function(FunctionMixin, nodes.Item, fixtures.FuncargnamesCompatAttr):
""" a Function Item is responsible for setting up and executing a
Python test function.
"""

View File

@@ -60,6 +60,9 @@ def pytest_runtest_protocol(item, nextitem):
nodeid=item.nodeid, location=item.location,
)
runtestprotocol(item, nextitem=nextitem)
item.ihook.pytest_runtest_logfinish(
nodeid=item.nodeid, location=item.location,
)
return True
@@ -175,7 +178,7 @@ def call_runtest_hook(item, when, **kwds):
return CallInfo(lambda: ihook(item=item, **kwds), when=when)
class CallInfo:
class CallInfo(object):
""" Result/Exception info a function invocation. """
#: None or ExceptionInfo object.
excinfo = None

View File

@@ -261,7 +261,7 @@ def pytest_runtest_makereport(item, call):
else:
rep.outcome = "passed"
rep.wasxfail = explanation
elif item._skipped_by_mark and rep.skipped and type(rep.longrepr) is tuple:
elif getattr(item, '_skipped_by_mark', False) and rep.skipped and type(rep.longrepr) is tuple:
# skipped by mark.skipif; change the location of the failure
# to point to the item definition, otherwise it will display
# the location of where the skip exception was raised within pytest

View File

@@ -94,7 +94,7 @@ def pytest_report_teststatus(report):
return report.outcome, letter, report.outcome.upper()
class WarningReport:
class WarningReport(object):
"""
Simple structure to hold warnings information captured by ``pytest_logwarning``.
"""
@@ -129,7 +129,7 @@ class WarningReport:
return None
class TerminalReporter:
class TerminalReporter(object):
def __init__(self, config, file=None):
import _pytest.config
self.config = config
@@ -152,8 +152,18 @@ class TerminalReporter:
self.reportchars = getreportopt(config)
self.hasmarkup = self._tw.hasmarkup
self.isatty = file.isatty()
self._progress_items_reported = 0
self._show_progress_info = self.config.getini('console_output_style') == 'progress'
self._progress_nodeids_reported = set()
self._show_progress_info = self._determine_show_progress_info()
def _determine_show_progress_info(self):
"""Return True if we should display progress information based on the current config"""
# do not show progress if we are not capturing output (#3038)
if self.config.getoption('capture') == 'no':
return False
# do not show progress if we are showing fixture setup/teardown
if self.config.getoption('setupshow'):
return False
return self.config.getini('console_output_style') == 'progress'
def hasopt(self, char):
char = {'xfailed': 'x', 'skipped': 's'}.get(char, char)
@@ -178,7 +188,6 @@ class TerminalReporter:
if extra:
self._tw.write(extra, **kwargs)
self.currentfspath = -2
self._write_progress_information_filling_space()
def ensure_newline(self):
if self.currentfspath:
@@ -268,14 +277,13 @@ class TerminalReporter:
# probably passed setup/teardown
return
running_xdist = hasattr(rep, 'node')
self._progress_items_reported += 1
if self.verbosity <= 0:
if not running_xdist and self.showfspath:
self.write_fspath_result(rep.nodeid, letter)
else:
self._tw.write(letter)
self._write_progress_if_past_edge()
else:
self._progress_nodeids_reported.add(rep.nodeid)
if markup is None:
if rep.passed:
markup = {'green': True}
@@ -288,6 +296,8 @@ class TerminalReporter:
line = self._locationline(rep.nodeid, *rep.location)
if not running_xdist:
self.write_ensure_prefix(line, word, **markup)
if self._show_progress_info:
self._write_progress_information_filling_space()
else:
self.ensure_newline()
self._tw.write("[%s]" % rep.node.gateway.id)
@@ -299,31 +309,28 @@ class TerminalReporter:
self._tw.write(" " + line)
self.currentfspath = -2
def _write_progress_if_past_edge(self):
if not self._show_progress_info:
return
last_item = self._progress_items_reported == self._session.testscollected
if last_item:
self._write_progress_information_filling_space()
return
past_edge = self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1 >= self._screen_width
if past_edge:
msg = self._get_progress_information_message()
self._tw.write(msg + '\n', cyan=True)
def pytest_runtest_logfinish(self, nodeid):
if self.verbosity <= 0 and self._show_progress_info:
self._progress_nodeids_reported.add(nodeid)
last_item = len(self._progress_nodeids_reported) == self._session.testscollected
if last_item:
self._write_progress_information_filling_space()
else:
past_edge = self._tw.chars_on_current_line + self._PROGRESS_LENGTH + 1 >= self._screen_width
if past_edge:
msg = self._get_progress_information_message()
self._tw.write(msg + '\n', cyan=True)
_PROGRESS_LENGTH = len(' [100%]')
def _get_progress_information_message(self):
collected = self._session.testscollected
if collected:
progress = self._progress_items_reported * 100 // collected
progress = len(self._progress_nodeids_reported) * 100 // collected
return ' [{:3d}%]'.format(progress)
return ' [100%]'
def _write_progress_information_filling_space(self):
if not self._show_progress_info:
return
msg = self._get_progress_information_message()
fill = ' ' * (self._tw.fullwidth - self._tw.chars_on_current_line - len(msg) - 1)
self.write(fill + msg, cyan=True)

View File

@@ -8,7 +8,7 @@ import py
from _pytest.monkeypatch import MonkeyPatch
class TempdirFactory:
class TempdirFactory(object):
"""Factory for temporary directories under the common base temp directory.
The base directory can be configured using the ``--basetemp`` option.

View File

@@ -6,6 +6,8 @@ Release announcements
:maxdepth: 2
release-3.4.0
release-3.3.2
release-3.3.1
release-3.3.0
release-3.2.5

View File

@@ -0,0 +1,28 @@
pytest-3.3.2
=======================================
pytest 3.3.2 has just been released to PyPI.
This is a bug-fix release, being a drop-in replacement. To upgrade::
pip install --upgrade pytest
The full changelog is available at http://doc.pytest.org/en/latest/changelog.html.
Thanks to all who contributed to this release, among them:
* Anthony Sottile
* Antony Lee
* Austin
* Bruno Oliveira
* Florian Bruhin
* Floris Bruynooghe
* Henk-Jaap Wagenaar
* Jurko Gospodnetić
* Ronny Pfannschmidt
* Srinivas Reddy Thatiparthy
* Thomas Hisch
Happy testing,
The pytest Development Team

View File

@@ -0,0 +1,52 @@
pytest-3.4.0
=======================================
The pytest team is proud to announce the 3.4.0 release!
pytest is a mature Python testing tool with more than a 1600 tests
against itself, passing on many different interpreters and platforms.
This release contains a number of bugs fixes and improvements, so users are encouraged
to take a look at the CHANGELOG:
http://doc.pytest.org/en/latest/changelog.html
For complete documentation, please visit:
http://docs.pytest.org
As usual, you can upgrade from pypi via:
pip install -U pytest
Thanks to all who contributed to this release, among them:
* Aaron
* Alan Velasco
* Anders Hovmöller
* Andrew Toolan
* Anthony Sottile
* Aron Coyle
* Brian Maissy
* Bruno Oliveira
* Cyrus Maden
* Florian Bruhin
* Henk-Jaap Wagenaar
* Ian Lesperance
* Jon Dufresne
* Jurko Gospodnetić
* Kate
* Kimberly
* Per A. Brodtkorb
* Pierre-Alexandre Fonta
* Raphael Castaneda
* Ronny Pfannschmidt
* ST John
* Segev Finer
* Thomas Hisch
* Tzu-ping Chung
* feuillemorte
Happy testing,
The Pytest Development Team

View File

@@ -15,91 +15,6 @@ We will only remove deprecated functionality in major releases (e.g. if we depre
Deprecation Roadmap
-------------------
This page lists deprecated features and when we plan to remove them. It is important to list the feature, the version where it got deprecated and the version we plan to remove it.
We track deprecation and removal of features using milestones and the `deprecation <https://github.com/pytest-dev/pytest/issues?q=label%3A%22type%3A+deprecation%22>`_ and `removal <https://github.com/pytest-dev/pytest/labels/type%3A%20removal>`_ labels on GitHub.
Following our deprecation policy, we should aim to keep features for *at least* two minor versions after it was considered deprecated.
Future Releases
~~~~~~~~~~~~~~~
3.4
^^^
**Old style classes**
Issue: `#2147 <https://github.com/pytest-dev/pytest/issues/2147>`_.
Deprecated in ``3.2``.
4.0
^^^
**Yield tests**
Deprecated in ``3.0``.
**pytest-namespace hook**
deprecated in ``3.2``.
**Marks in parameter sets**
Deprecated in ``3.2``.
**--result-log**
Deprecated in ``3.0``.
See `#830 <https://github.com/pytest-dev/pytest/issues/830>`_ for more information. Suggested alternative: `pytest-tap <https://pypi.python.org/pypi/pytest-tap>`_.
**metafunc.addcall**
Issue: `#2876 <https://github.com/pytest-dev/pytest/issues/2876>`_.
Deprecated in ``3.3``.
**pytest_plugins in non-toplevel conftests**
There is a deep conceptual confusion as ``conftest.py`` files themselves are activated/deactivated based on path, but the plugins they depend on aren't.
Issue: `#2639 <https://github.com/pytest-dev/pytest/issues/2639>`_.
Not yet officially deprecated.
**passing a single string to pytest.main()**
Pass a list of strings to ``pytest.main()`` instead.
Deprecated in ``3.1``.
**[pytest] section in setup.cfg**
Use ``[tool:pytest]`` instead for compatibility with other tools.
Deprecated in ``3.0``.
Past Releases
~~~~~~~~~~~~~
3.0
^^^
* The following deprecated commandline options were removed:
* ``--genscript``: no longer supported;
* ``--no-assert``: use ``--assert=plain`` instead;
* ``--nomagic``: use ``--assert=plain`` instead;
* ``--report``: use ``-r`` instead;
* Removed all ``py.test-X*`` entry points. The versioned, suffixed entry points
were never documented and a leftover from a pre-virtualenv era. These entry
points also created broken entry points in wheels, so removing them also
removes a source of confusion for users.
3.3
^^^
* Dropped support for EOL Python 2.6 and 3.3.
Following our deprecation policy, after starting issuing deprecation warnings we keep features for *at least* two minor versions before considering removal.

View File

@@ -116,6 +116,10 @@ You can ask for available builtin or project-custom
Add extra xml properties to the tag for the calling test.
The fixture is callable with ``(name, value)``, with value being automatically
xml-encoded.
record_xml_attribute
Add extra xml attributes to the tag for the calling test.
The fixture is callable with ``(name, value)``, with value being automatically
xml-encoded
caplog
Access and control log capturing.

View File

@@ -225,7 +225,7 @@ You can always peek at the content of the cache using the
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y
rootdir: $REGENDOC_TMPDIR, inifile:
cachedir: $REGENDOC_TMPDIR/.cache
cachedir: $REGENDOC_TMPDIR/.pytest_cache
------------------------------- cache values -------------------------------
cache/lastfailed contains:
{'test_caching.py::test_function': True}

View File

@@ -92,14 +92,14 @@ an example test function that performs some output related checks:
.. code-block:: python
def test_myoutput(capsys): # or use "capfd" for fd-level
print ("hello")
print("hello")
sys.stderr.write("world\n")
out, err = capsys.readouterr()
assert out == "hello\n"
assert err == "world\n"
print ("next")
out, err = capsys.readouterr()
assert out == "next\n"
captured = capsys.readouterr()
assert captured.out == "hello\n"
assert captured.err == "world\n"
print("next")
captured = capsys.readouterr()
assert captured.out == "next\n"
The ``readouterr()`` call snapshots the output so far -
and capturing will be continued. After the test
@@ -117,6 +117,10 @@ system level output streams (FD1 and FD2).
.. versionadded:: 3.3
The return value from ``readouterr`` changed to a ``namedtuple`` with two attributes, ``out`` and ``err``.
.. versionadded:: 3.3
If the code under test writes non-textual data, you can capture this using
the ``capsysbinary`` fixture which instead returns ``bytes`` from
the ``readouterr`` method. The ``capfsysbinary`` fixture is currently only

View File

@@ -152,11 +152,25 @@ above will show verbose output because ``-v`` overwrites ``-q``.
Builtin configuration file options
----------------------------------------------
Here is a list of builtin configuration options that may be written in a ``pytest.ini``, ``tox.ini`` or ``setup.cfg``
file, usually located at the root of your repository. All options must be under a ``[pytest]`` section
(``[tool:pytest]`` for ``setup.cfg`` files).
Configuration file options may be overwritten in the command-line by using ``-o/--override``, which can also be
passed multiple times. The expected format is ``name=value``. For example::
pytest -o console_output_style=classic -o cache_dir=/tmp/mycache
.. confval:: minversion
Specifies a minimal pytest version required for running tests.
minversion = 2.1 # will fail if we run with pytest-2.0
.. code-block:: ini
# content of pytest.ini
[pytest]
minversion = 3.0 # will fail if we run with pytest-2.8
.. confval:: addopts
@@ -165,6 +179,7 @@ Builtin configuration file options
.. code-block:: ini
# content of pytest.ini
[pytest]
addopts = --maxfail=2 -rf # exit after 2 failures, report fail info
@@ -331,3 +346,28 @@ Builtin configuration file options
# content of pytest.ini
[pytest]
console_output_style = classic
.. confval:: empty_parameter_set_mark
.. versionadded:: 3.4
Allows to pick the action for empty parametersets in parameterization
* ``skip`` skips tests with a empty parameterset (default)
* ``xfail`` marks tests with a empty parameterset as xfail(run=False)
.. code-block:: ini
# content of pytest.ini
[pytest]
empty_parameter_set_mark = xfail
.. note::
The default value of this option is planned to change to ``xfail`` in future releases
as this is considered less error prone, see `#3155`_ for more details.
.. _`#3155`: https://github.com/pytest-dev/pytest/issues/3155

View File

@@ -81,9 +81,9 @@ Also, :ref:`usefixtures` and :ref:`autouse` fixtures are supported
when executing text doctest files.
The standard ``doctest`` module provides some setting flags to configure the
strictness of doctest tests. In pytest You can enable those flags those flags
using the configuration file. To make pytest ignore trailing whitespaces and
ignore lengthy exception stack traces you can just write:
strictness of doctest tests. In pytest, you can enable those flags using the
configuration file. To make pytest ignore trailing whitespaces and ignore
lengthy exception stack traces you can just write:
.. code-block:: ini

View File

@@ -157,12 +157,14 @@ class TestRaises(object):
# thanks to Matthew Scott for this test
def test_dynamic_compile_shows_nicely():
import imp
import sys
src = 'def foo():\n assert 1 == 0\n'
name = 'abc-123'
module = py.std.imp.new_module(name)
module = imp.new_module(name)
code = _pytest._code.compile(src, name, 'exec')
py.builtin.exec_(code, module.__dict__)
py.std.sys.modules[name] = module
sys.modules[name] = module
module.foo()

View File

@@ -32,7 +32,7 @@ You can then restrict a test run to only run tests marked with ``webtest``::
$ pytest -v -m webtest
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
cachedir: .cache
cachedir: .pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 4 items
@@ -46,7 +46,7 @@ Or the inverse, running all tests except the webtest ones::
$ pytest -v -m "not webtest"
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
cachedir: .cache
cachedir: .pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 4 items
@@ -67,7 +67,7 @@ tests based on their module, class, method, or function name::
$ pytest -v test_server.py::TestClass::test_method
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
cachedir: .cache
cachedir: .pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 1 item
@@ -80,7 +80,7 @@ You can also select on the class::
$ pytest -v test_server.py::TestClass
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
cachedir: .cache
cachedir: .pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 1 item
@@ -93,7 +93,7 @@ Or select multiple nodes::
$ pytest -v test_server.py::TestClass test_server.py::test_send_http
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
cachedir: .cache
cachedir: .pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 2 items
@@ -131,7 +131,7 @@ select tests based on their names::
$ pytest -v -k http # running with the above defined example module
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
cachedir: .cache
cachedir: .pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 4 items
@@ -145,7 +145,7 @@ And you can also run all tests except the ones that match the keyword::
$ pytest -k "not send_http" -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
cachedir: .cache
cachedir: .pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 4 items
@@ -161,7 +161,7 @@ Or to select "http" and "quick" tests::
$ pytest -k "http or quick" -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
cachedir: .cache
cachedir: .pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 4 items
@@ -432,7 +432,7 @@ The output is as follows::
$ pytest -q -s
Marker info name=my_marker args=(<function hello_world at 0xdeadbeef>,) kwars={}
. [100%]
.
1 passed in 0.12 seconds
We can see that the custom marker has its argument set extended with the function ``hello_world``. This is the key difference between creating a custom marker as a callable, which invokes ``__call__`` behind the scenes, and using ``with_args``.
@@ -477,7 +477,7 @@ Let's run this without capturing output and see what we get::
glob args=('function',) kwargs={'x': 3}
glob args=('class',) kwargs={'x': 2}
glob args=('module',) kwargs={'x': 1}
. [100%]
.
1 passed in 0.12 seconds
marking platform specific tests with pytest

View File

@@ -60,7 +60,7 @@ consulted when reporting in ``verbose`` mode::
nonpython $ pytest -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
cachedir: .cache
cachedir: .pytest_cache
rootdir: $REGENDOC_TMPDIR/nonpython, inifile:
collecting ... collected 2 items

View File

@@ -411,22 +411,24 @@ get on the terminal - we are working on that)::
____________________ test_dynamic_compile_shows_nicely _____________________
def test_dynamic_compile_shows_nicely():
import imp
import sys
src = 'def foo():\n assert 1 == 0\n'
name = 'abc-123'
module = py.std.imp.new_module(name)
module = imp.new_module(name)
code = _pytest._code.compile(src, name, 'exec')
py.builtin.exec_(code, module.__dict__)
py.std.sys.modules[name] = module
sys.modules[name] = module
> module.foo()
failure_demo.py:166:
failure_demo.py:168:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
def foo():
> assert 1 == 0
E AssertionError
<2-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:163>:2: AssertionError
<2-codegen 'abc-123' $REGENDOC_TMPDIR/assertion/failure_demo.py:165>:2: AssertionError
____________________ TestMoreErrors.test_complex_error _____________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@@ -438,7 +440,7 @@ get on the terminal - we are working on that)::
return 43
> somefunc(f(), g())
failure_demo.py:176:
failure_demo.py:178:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
failure_demo.py:9: in somefunc
otherfunc(x,y)
@@ -460,7 +462,7 @@ get on the terminal - we are working on that)::
> a,b = l
E ValueError: not enough values to unpack (expected 2, got 0)
failure_demo.py:180: ValueError
failure_demo.py:182: ValueError
____________________ TestMoreErrors.test_z2_type_error _____________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@@ -470,7 +472,7 @@ get on the terminal - we are working on that)::
> a,b = l
E TypeError: 'int' object is not iterable
failure_demo.py:184: TypeError
failure_demo.py:186: TypeError
______________________ TestMoreErrors.test_startswith ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@@ -483,7 +485,7 @@ get on the terminal - we are working on that)::
E + where False = <built-in method startswith of str object at 0xdeadbeef>('456')
E + where <built-in method startswith of str object at 0xdeadbeef> = '123'.startswith
failure_demo.py:189: AssertionError
failure_demo.py:191: AssertionError
__________________ TestMoreErrors.test_startswith_nested ___________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@@ -500,7 +502,7 @@ get on the terminal - we are working on that)::
E + where '123' = <function TestMoreErrors.test_startswith_nested.<locals>.f at 0xdeadbeef>()
E + and '456' = <function TestMoreErrors.test_startswith_nested.<locals>.g at 0xdeadbeef>()
failure_demo.py:196: AssertionError
failure_demo.py:198: AssertionError
_____________________ TestMoreErrors.test_global_func ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@@ -511,7 +513,7 @@ get on the terminal - we are working on that)::
E + where False = isinstance(43, float)
E + where 43 = globf(42)
failure_demo.py:199: AssertionError
failure_demo.py:201: AssertionError
_______________________ TestMoreErrors.test_instance _______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@@ -522,7 +524,7 @@ get on the terminal - we are working on that)::
E assert 42 != 42
E + where 42 = <failure_demo.TestMoreErrors object at 0xdeadbeef>.x
failure_demo.py:203: AssertionError
failure_demo.py:205: AssertionError
_______________________ TestMoreErrors.test_compare ________________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@@ -532,7 +534,7 @@ get on the terminal - we are working on that)::
E assert 11 < 5
E + where 11 = globf(10)
failure_demo.py:206: AssertionError
failure_demo.py:208: AssertionError
_____________________ TestMoreErrors.test_try_finally ______________________
self = <failure_demo.TestMoreErrors object at 0xdeadbeef>
@@ -543,7 +545,7 @@ get on the terminal - we are working on that)::
> assert x == 0
E assert 1 == 0
failure_demo.py:211: AssertionError
failure_demo.py:213: AssertionError
___________________ TestCustomAssertMsg.test_single_line ___________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
@@ -557,7 +559,7 @@ get on the terminal - we are working on that)::
E assert 1 == 2
E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_single_line.<locals>.A'>.a
failure_demo.py:222: AssertionError
failure_demo.py:224: AssertionError
____________________ TestCustomAssertMsg.test_multiline ____________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
@@ -574,7 +576,7 @@ get on the terminal - we are working on that)::
E assert 1 == 2
E + where 1 = <class 'failure_demo.TestCustomAssertMsg.test_multiline.<locals>.A'>.a
failure_demo.py:228: AssertionError
failure_demo.py:230: AssertionError
___________________ TestCustomAssertMsg.test_custom_repr ___________________
self = <failure_demo.TestCustomAssertMsg object at 0xdeadbeef>
@@ -594,7 +596,7 @@ get on the terminal - we are working on that)::
E assert 1 == 2
E + where 1 = This is JSON\n{\n 'foo': 'bar'\n}.a
failure_demo.py:238: AssertionError
failure_demo.py:240: AssertionError
============================= warnings summary =============================
None
Metafunc.addcall is deprecated and scheduled to be removed in pytest 4.0.

View File

@@ -332,7 +332,7 @@ which will add info only when run with "--v"::
$ pytest -v
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
cachedir: .cache
cachedir: .pytest_cache
info1: did you know that ...
did you?
rootdir: $REGENDOC_TMPDIR, inifile:
@@ -385,8 +385,8 @@ Now we can profile which test functions execute the slowest::
test_some_are_slow.py ... [100%]
========================= slowest 3 test durations =========================
0.30s call test_some_are_slow.py::test_funcslow2
0.20s call test_some_are_slow.py::test_funcslow1
0.58s call test_some_are_slow.py::test_funcslow2
0.41s call test_some_are_slow.py::test_funcslow1
0.10s call test_some_are_slow.py::test_funcfast
========================= 3 passed in 0.12 seconds =========================
@@ -537,7 +537,7 @@ We can run this::
file $REGENDOC_TMPDIR/b/test_error.py, line 1
def test_root(db): # no db here, will error out
E fixture 'db' not found
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_xml_property, recwarn, tmpdir, tmpdir_factory
> available fixtures: cache, capfd, capfdbinary, caplog, capsys, capsysbinary, doctest_namespace, monkeypatch, pytestconfig, record_xml_attribute, record_xml_property, recwarn, tmpdir, tmpdir_factory
> use 'pytest --fixtures [testpath]' for help on them.
$REGENDOC_TMPDIR/b/test_error.py:1
@@ -731,7 +731,7 @@ and run it::
test_module.py Esetting up a test failed! test_module.py::test_setup_fails
Fexecuting test failed test_module.py::test_call_fails
F [100%]
F
================================== ERRORS ==================================
____________________ ERROR at setup of test_setup_fails ____________________
@@ -826,15 +826,20 @@ Instead of freezing the pytest runner as a separate executable, you can make
your frozen program work as the pytest runner by some clever
argument handling during program startup. This allows you to
have a single executable, which is usually more convenient.
Please note that the mechanism for plugin discovery used by pytest
(setupttools entry points) doesn't work with frozen executables so pytest
can't find any third party plugins automatically. To include third party plugins
like ``pytest-timeout`` they must be imported explicitly and passed on to pytest.main.
.. code-block:: python
# contents of app_main.py
import sys
import pytest_timeout # Third party plugin
if len(sys.argv) > 1 and sys.argv[1] == '--pytest':
import pytest
sys.exit(pytest.main(sys.argv[2:]))
sys.exit(pytest.main(sys.argv[2:], plugins=[pytest_timeout]))
else:
# normal application execution: at this point argv can be parsed
# by your argument-parsing library of choice as usual
@@ -845,3 +850,4 @@ This allows you to execute tests using the frozen
application with standard ``pytest`` command-line options::
./app_main --pytest --verbose --tb=long --junitxml=results.xml test-suite/

View File

@@ -68,5 +68,5 @@ If you run this without output capturing::
.test_method1 called
.test other
.test_unit1 method called
. [100%]
.
4 passed in 0.12 seconds

View File

@@ -286,7 +286,7 @@ tests.
Let's execute it::
$ pytest -s -q --tb=no
FF [100%]teardown smtp
FFteardown smtp
2 failed in 0.12 seconds
@@ -391,7 +391,7 @@ We use the ``request.module`` attribute to optionally obtain an
again, nothing much has changed::
$ pytest -s -q --tb=no
FF [100%]finalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com)
FFfinalizing <smtplib.SMTP object at 0xdeadbeef> (smtp.gmail.com)
2 failed in 0.12 seconds
@@ -612,7 +612,7 @@ Here we declare an ``app`` fixture which receives the previously defined
$ pytest -v test_appsetup.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
cachedir: .cache
cachedir: .pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 2 items
@@ -681,40 +681,40 @@ Let's run the tests in verbose mode and with looking at the print-output::
$ pytest -v -s test_module.py
=========================== test session starts ============================
platform linux -- Python 3.x.y, pytest-3.x.y, py-1.x.y, pluggy-0.x.y -- $PYTHON_PREFIX/bin/python3.5
cachedir: .cache
cachedir: .pytest_cache
rootdir: $REGENDOC_TMPDIR, inifile:
collecting ... collected 8 items
test_module.py::test_0[1] SETUP otherarg 1
RUN test0 with otherarg 1
PASSED [ 12%] TEARDOWN otherarg 1
PASSED TEARDOWN otherarg 1
test_module.py::test_0[2] SETUP otherarg 2
RUN test0 with otherarg 2
PASSED [ 25%] TEARDOWN otherarg 2
PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod1] SETUP modarg mod1
RUN test1 with modarg mod1
PASSED [ 37%]
PASSED
test_module.py::test_2[1-mod1] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod1
PASSED [ 50%] TEARDOWN otherarg 1
PASSED TEARDOWN otherarg 1
test_module.py::test_2[2-mod1] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod1
PASSED [ 62%] TEARDOWN otherarg 2
PASSED TEARDOWN otherarg 2
test_module.py::test_1[mod2] TEARDOWN modarg mod1
SETUP modarg mod2
RUN test1 with modarg mod2
PASSED [ 75%]
PASSED
test_module.py::test_2[1-mod2] SETUP otherarg 1
RUN test2 with otherarg 1 and modarg mod2
PASSED [ 87%] TEARDOWN otherarg 1
PASSED TEARDOWN otherarg 1
test_module.py::test_2[2-mod2] SETUP otherarg 2
RUN test2 with otherarg 2 and modarg mod2
PASSED [100%] TEARDOWN otherarg 2
PASSED TEARDOWN otherarg 2
TEARDOWN modarg mod2

View File

@@ -7,32 +7,34 @@ Installation and Getting Started
**PyPI package name**: `pytest <http://pypi.python.org/pypi/pytest>`_
**dependencies**: `py <http://pypi.python.org/pypi/py>`_,
**Dependencies**: `py <http://pypi.python.org/pypi/py>`_,
`colorama (Windows) <http://pypi.python.org/pypi/colorama>`_,
**documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_
**Documentation as PDF**: `download latest <https://media.readthedocs.org/pdf/pytest/latest/pytest.pdf>`_
``pytest`` is a framework that makes building simple and scalable tests easy. Tests are expressive and readable—no boilerplate code required. Get started in minutes with a small unit test or complex functional test for your application or library.
.. _`getstarted`:
.. _installation:
.. _`installation`:
Installation
Install ``pytest``
----------------------------------------
Installation::
1. Run the following command in your command line::
pip install -U pytest
To check your installation has installed the correct version::
2. Check that you installed the correct version::
$ pytest --version
This is pytest version 3.x.y, imported from $PYTHON_PREFIX/lib/python3.5/site-packages/pytest.py
.. _`simpletest`:
Our first test run
Create your first test
----------------------------------------------------------
Let's create a first test file with a simple test function::
Create a simple test function with just four lines of code::
# content of test_sample.py
def func(x):
@@ -41,7 +43,7 @@ Let's create a first test file with a simple test function::
def test_answer():
assert func(3) == 5
That's it. You can execute the test function now::
Thats it. You can now execute the test function::
$ pytest
=========================== test session starts ============================
@@ -62,30 +64,22 @@ That's it. You can execute the test function now::
test_sample.py:5: AssertionError
========================= 1 failed in 0.12 seconds =========================
We got a failure report because our little ``func(3)`` call did not return ``5``.
This test returns a failure report because ``func(3)`` does not return ``5``.
.. note::
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`_.
You can use the ``assert`` statement to verify test expectations. pytests `Advanced assertion introspection <http://docs.python.org/reference/simple_stmts.html#the-assert-statement>`_ will intelligently report intermediate values of the assert expression so you can avoid the many names `of 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
Running multiple tests
Run multiple tests
----------------------------------------------------------
``pytest`` will run all files in the current directory and its subdirectories of the form test_*.py or \*_test.py. More generally, it follows :ref:`standard test discovery rules <test discovery>`.
``pytest`` will run all files of the form test_*.py or \*_test.py in the current directory and its subdirectories. More generally, it follows :ref:`standard test discovery rules <test discovery>`.
Asserting that a certain exception is raised
Assert that a certain exception is raised
--------------------------------------------------------------
If you want to assert that some code raises an exception you can
use the ``raises`` helper::
Use the ``raises`` helper to assert that some code raises an exception::
# content of test_sysexit.py
import pytest
@@ -96,18 +90,16 @@ use the ``raises`` helper::
with pytest.raises(SystemExit):
f()
Running it with, this time in "quiet" reporting mode::
Execute the test function with “quiet reporting mode::
$ pytest -q test_sysexit.py
. [100%]
1 passed in 0.12 seconds
Grouping multiple tests in a class
Group multiple tests in a class
--------------------------------------------------------------
Once you start to have more than a few tests it often makes sense
to group tests logically, in classes and modules. Let's write a class
containing two tests::
Once you develop multiple tests, you may want to group them into a class. pytest makes it easy to create a class containing more than one test::
# content of test_class.py
class TestClass(object):
@@ -119,9 +111,7 @@ containing two tests::
x = "hello"
assert hasattr(x, 'check')
The two tests are found because of the standard :ref:`test discovery`.
There is no need to subclass anything. We can simply
run the module by passing its filename::
``pytest`` discovers all tests following its :ref:`Conventions for Python test discovery <test discovery>`, so it finds both ``test_`` prefixed functions. There is no need to subclass anything. We can simply run the module by passing its filename::
$ pytest -q test_class.py
.F [100%]
@@ -139,26 +129,19 @@ run the module by passing its filename::
test_class.py:8: AssertionError
1 failed, 1 passed in 0.12 seconds
The first test passed, the second failed. Again we can easily see
the intermediate values used in the assertion, helping us to
understand the reason for the failure.
The first test passed and the second failed. You can easily see the intermediate values in the assertion to help you understand the reason for the failure.
Going functional: requesting a unique temporary directory
Request a unique temporary directory for functional tests
--------------------------------------------------------------
For functional tests one often needs to create some files
and pass them to application objects. pytest provides
:ref:`builtinfixtures` which allow to request arbitrary
resources, for example a unique temporary directory::
``pytest`` provides `Builtin fixtures/function arguments <https://docs.pytest.org/en/latest/builtin.html#builtinfixtures>`_ to request arbitrary resources, like a unique temporary directory::
# content of test_tmpdir.py
def test_needsfiles(tmpdir):
print (tmpdir)
assert 0
We list the name ``tmpdir`` in the test function signature and
``pytest`` will lookup and call a fixture factory to create the resource
before performing the test function call. Let's just run it::
List the name ``tmpdir`` in the test function signature and ``pytest`` will lookup and call a fixture factory to create the resource before performing the test function call. Before the test runs, ``pytest`` creates a unique-per-test-invocation temporary directory::
$ pytest -q test_tmpdir.py
F [100%]
@@ -177,22 +160,21 @@ before performing the test function call. Let's just run it::
PYTEST_TMPDIR/test_needsfiles0
1 failed in 0.12 seconds
Before the test runs, a unique-per-test-invocation temporary directory
was created. More info at :ref:`tmpdir handling`.
More info on tmpdir handling is available at `Temporary directories and files <tmpdir handling>`_.
You can find out what kind of builtin :ref:`fixtures` exist by typing::
Find out what kind of builtin ```pytest`` fixtures <fixtures>`_ exist with the command::
pytest --fixtures # shows builtin and custom fixtures
Where to go next
Continue reading
-------------------------------------
Here are a few suggestions where to go next:
Check out additional pytest resources to help you customize tests for your unique workflow:
* :ref:`cmdline` for command line invocation examples
* :ref:`good practices <goodpractices>` for virtualenv, test layout
* :ref:`existingtestsuite` for working with pre-existing tests
* :ref:`fixtures` for providing a functional baseline to your tests
* :ref:`plugins` managing and writing plugins
* ":ref:`cmdline`" for command line invocation examples
* ":ref:`goodpractices`" for virtualenv and test layouts
* ":ref:`existingtestsuite`" for working with pre-existing tests
* ":ref:`fixtures`" for providing a functional baseline to your tests
* ":ref:`plugins`" for managing and writing plugins
.. include:: links.inc

View File

@@ -3,24 +3,11 @@
Logging
-------
.. versionadded 3.3.0
.. versionadded:: 3.3
.. versionchanged:: 3.4
.. note::
This feature is a drop-in replacement for the `pytest-catchlog
<https://pypi.org/project/pytest-catchlog/>`_ plugin and they will conflict
with each other. The backward compatibility API with ``pytest-capturelog``
has been dropped when this feature was introduced, so if for that reason you
still need ``pytest-catchlog`` you can disable the internal feature by
adding to your ``pytest.ini``:
.. code-block:: ini
[pytest]
addopts=-p no:logging
Log messages are captured by default and for each failed test will be shown in
the same manner as captured stdout and stderr.
pytest captures log messages of level ``WARNING`` or above automatically and displays them in their own section
for each failed test in the same manner as captured stdout and stderr.
Running without options::
@@ -29,7 +16,7 @@ Running without options::
Shows failed tests like so::
----------------------- Captured stdlog call ----------------------
test_reporting.py 26 INFO text going to logger
test_reporting.py 26 WARNING text going to logger
----------------------- Captured stdout call ----------------------
text going to stdout
----------------------- Captured stderr call ----------------------
@@ -37,11 +24,10 @@ Shows failed tests like so::
==================== 2 failed in 0.02 seconds =====================
By default each captured log message shows the module, line number, log level
and message. Showing the exact module and line number is useful for testing and
debugging. If desired the log format and date format can be specified to
anything that the logging module supports.
and message.
Running pytest specifying formatting options::
If desired the log and date format can be specified to
anything that the logging module supports by passing specific formatting options::
pytest --log-format="%(asctime)s %(levelname)s %(message)s" \
--log-date-format="%Y-%m-%d %H:%M:%S"
@@ -49,14 +35,14 @@ Running pytest specifying formatting options::
Shows failed tests like so::
----------------------- Captured stdlog call ----------------------
2010-04-10 14:48:44 INFO text going to logger
2010-04-10 14:48:44 WARNING text going to logger
----------------------- Captured stdout call ----------------------
text going to stdout
----------------------- Captured stderr call ----------------------
text going to stderr
==================== 2 failed in 0.02 seconds =====================
These options can also be customized through a configuration file:
These options can also be customized through ``pytest.ini`` file:
.. code-block:: ini
@@ -69,7 +55,7 @@ with::
pytest --no-print-logs
Or in you ``pytest.ini``:
Or in the ``pytest.ini`` file:
.. code-block:: ini
@@ -85,6 +71,10 @@ Shows failed tests in the normal manner as no logs were captured::
text going to stderr
==================== 2 failed in 0.02 seconds =====================
caplog fixture
^^^^^^^^^^^^^^
Inside tests it is possible to change the log level for the captured log
messages. This is supported by the ``caplog`` fixture::
@@ -92,7 +82,7 @@ messages. This is supported by the ``caplog`` fixture::
caplog.set_level(logging.INFO)
pass
By default the level is set on the handler used to catch the log messages,
By default the level is set on the root logger,
however as a convenience it is also possible to set the log level of any
logger::
@@ -100,14 +90,16 @@ logger::
caplog.set_level(logging.CRITICAL, logger='root.baz')
pass
The log levels set are restored automatically at the end of the test.
It is also possible to use a context manager to temporarily change the log
level::
level inside a ``with`` block::
def test_bar(caplog):
with caplog.at_level(logging.INFO):
pass
Again, by default the level of the handler is affected but the level of any
Again, by default the level of the root logger is affected but the level of any
logger can be changed instead with::
def test_bar(caplog):
@@ -115,7 +107,7 @@ logger can be changed instead with::
pass
Lastly all the logs sent to the logger during the test run are made available on
the fixture in the form of both the LogRecord instances and the final log text.
the fixture in the form of both the ``logging.LogRecord`` instances and the final log text.
This is useful for when you want to assert on the contents of a message::
def test_baz(caplog):
@@ -146,12 +138,41 @@ You can call ``caplog.clear()`` to reset the captured log records in a test::
your_test_method()
assert ['Foo'] == [rec.message for rec in caplog.records]
The ``caplop.records`` attribute contains records from the current stage only, so
inside the ``setup`` phase it contains only setup logs, same with the ``call`` and
``teardown`` phases.
To access logs from other stages, use the ``caplog.get_records(when)`` method. As an example,
if you want to make sure that tests which use a certain fixture never log any warnings, you can inspect
the records for the ``setup`` and ``call`` stages during teardown like so:
.. code-block:: python
@pytest.fixture
def window(caplog):
window = create_window()
yield window
for when in ('setup', 'call'):
messages = [x.message for x in caplog.get_records(when) if x.level == logging.WARNING]
if messages:
pytest.fail('warning messages encountered during testing: {}'.format(messages))
caplog fixture API
~~~~~~~~~~~~~~~~~~
.. autoclass:: _pytest.logging.LogCaptureFixture
:members:
.. _live_logs:
Live Logs
^^^^^^^^^
By default, pytest will output any logging records with a level higher or
equal to WARNING. In order to actually see these logs in the console you have to
disable pytest output capture by passing ``-s``.
By setting the :confval:`log_cli` configuration option to ``true``, pytest will output
logging records as they are emitted directly into the console.
You can specify the logging level for which log records with equal or higher
level are printed to the console by passing ``--log-cli-level``. This setting
@@ -190,3 +211,49 @@ option names are:
* ``log_file_level``
* ``log_file_format``
* ``log_file_date_format``
.. _log_release_notes:
Release notes
^^^^^^^^^^^^^
This feature was introduced as a drop-in replacement for the `pytest-catchlog
<https://pypi.org/project/pytest-catchlog/>`_ plugin and they conflict
with each other. The backward compatibility API with ``pytest-capturelog``
has been dropped when this feature was introduced, so if for that reason you
still need ``pytest-catchlog`` you can disable the internal feature by
adding to your ``pytest.ini``:
.. code-block:: ini
[pytest]
addopts=-p no:logging
.. _log_changes_3_4:
Incompatible changes in pytest 3.4
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
This feature was introduced in ``3.3`` and some **incompatible changes** have been
made in ``3.4`` after community feedback:
* Log levels are no longer changed unless explicitly requested by the :confval:`log_level` configuration
or ``--log-level`` command-line options. This allows users to configure logger objects themselves.
* :ref:`Live Logs <live_logs>` is now disabled by default and can be enabled setting the
:confval:`log_cli` configuration option to ``true``. When enabled, the verbosity is increased so logging for each
test is visible.
* :ref:`Live Logs <live_logs>` are now sent to ``sys.stdout`` and no longer require the ``-s`` command-line option
to work.
If you want to partially restore the logging behavior of version ``3.3``, you can add this options to your ``ini``
file:
.. code-block:: ini
[pytest]
log_cli=true
log_level=NOTSET
More details about the discussion that lead to this changes can be read in
issue `#3013 <https://github.com/pytest-dev/pytest/issues/3013>`_.

View File

@@ -123,8 +123,8 @@ To get all combinations of multiple parametrized arguments you can stack
def test_foo(x, y):
pass
This will run the test with the arguments set to ``x=0/y=2``, ``x=0/y=3``, ``x=1/y=2`` and
``x=1/y=3``.
This will run the test with the arguments set to ``x=0/y=2``, ``x=1/y=2``,
``x=0/y=3``, and ``x=1/y=3`` exhausting parameters in the order of the decorators.
.. _`pytest_generate_tests`:

View File

@@ -37,7 +37,7 @@ Here are some examples of projects using ``pytest`` (please send notes via :ref:
* `mwlib <http://pypi.python.org/pypi/mwlib>`_ mediawiki parser and utility library
* `The Translate Toolkit <http://translate.sourceforge.net/wiki/toolkit/index>`_ for localization and conversion
* `execnet <http://codespeak.net/execnet>`_ rapid multi-Python deployment
* `pylib <http://py.rtfd.org>`_ cross-platform path, IO, dynamic code library
* `pylib <https://py.readthedocs.io>`_ cross-platform path, IO, dynamic code library
* `Pacha <http://pacha.cafepais.com/>`_ configuration management in five minutes
* `bbfreeze <http://pypi.python.org/pypi/bbfreeze>`_ create standalone executables from Python scripts
* `pdb++ <http://bitbucket.org/antocuni/pdb>`_ a fancier version of PDB

View File

@@ -106,6 +106,4 @@ When distributing tests on the local machine, ``pytest`` takes care to
configure a basetemp directory for the sub processes such that all temporary
data lands below a single per-test run basetemp directory.
.. _`py.path.local`: http://py.rtfd.org/en/latest/path.html
.. _`py.path.local`: https://py.readthedocs.io/en/latest/path.html

View File

@@ -256,6 +256,66 @@ This will add an extra property ``example_key="1"`` to the generated
Also please note that using this feature will break any schema verification.
This might be a problem when used with some CI servers.
record_xml_attribute
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
.. versionadded:: 3.4
To add an additional xml attribute to a testcase element, you can use
``record_xml_attribute`` fixture. This can also be used to override existing values:
.. code-block:: python
def test_function(record_xml_attribute):
record_xml_attribute("assertions", "REQ-1234")
record_xml_attribute("classname", "custom_classname")
print('hello world')
assert True
Unlike ``record_xml_property``, this will not add a new child element.
Instead, this will add an attribute ``assertions="REQ-1234"`` inside the generated
``testcase`` tag and override the default ``classname`` with ``"classname=custom_classname"``:
.. code-block:: xml
<testcase classname="custom_classname" file="test_function.py" line="0" name="test_function" time="0.003" assertions="REQ-1234">
<system-out>
hello world
</system-out>
</testcase>
.. warning::
``record_xml_attribute`` is an experimental feature, and its interface might be replaced
by something more powerful and general in future versions. The
functionality per-se will be kept, however.
Using this over ``record_xml_property`` can help when using ci tools to parse the xml report.
However, some parsers are quite strict about the elements and attributes that are allowed.
Many tools use an xsd schema (like the example below) to validate incoming xml.
Make sure you are using attribute names that are allowed by your parser.
Below is the Scheme used by Jenkins to validate the XML report:
.. code-block:: xml
<xs:element name="testcase">
<xs:complexType>
<xs:sequence>
<xs:element ref="skipped" minOccurs="0" maxOccurs="1"/>
<xs:element ref="error" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="failure" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="system-out" minOccurs="0" maxOccurs="unbounded"/>
<xs:element ref="system-err" minOccurs="0" maxOccurs="unbounded"/>
</xs:sequence>
<xs:attribute name="name" type="xs:string" use="required"/>
<xs:attribute name="assertions" type="xs:string" use="optional"/>
<xs:attribute name="time" type="xs:string" use="optional"/>
<xs:attribute name="classname" type="xs:string" use="optional"/>
<xs:attribute name="status" type="xs:string" use="optional"/>
</xs:complexType>
</xs:element>
LogXML: add_global_property
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
@@ -389,4 +449,14 @@ hook was invoked::
*** test run reporting finishing
.. note::
Calling ``pytest.main()`` will result in importing your tests and any modules
that they import. Due to the caching mechanism of python's import system,
making subsequent calls to ``pytest.main()`` from the same process will not
reflect changes to those files between the calls. For this reason, making
multiple calls to ``pytest.main()`` from the same process (in order to re-run
tests, for example) is not recommended.
.. include:: links.inc

View File

@@ -112,6 +112,12 @@ decorator or to all tests in a module by setting the ``pytestmark`` variable:
pytestmark = pytest.mark.filterwarnings('error')
.. note::
Except for these features, pytest does not change the python warning filter; it only captures
and displays the warnings which are issued with respect to the currently configured filter,
including changes to the filter made by test functions or by the system under test.
.. note::
``DeprecationWarning`` and ``PendingDeprecationWarning`` are hidden by the standard library

View File

@@ -583,11 +583,22 @@ pytest hook reference
Initialization, command line and configuration hooks
----------------------------------------------------
Bootstrapping hooks
~~~~~~~~~~~~~~~~~~~
Bootstrapping hooks called for plugins registered early enough (internal and setuptools plugins).
.. autofunction:: pytest_load_initial_conftests
.. autofunction:: pytest_cmdline_preparse
.. autofunction:: pytest_cmdline_parse
.. autofunction:: pytest_addoption
.. autofunction:: pytest_cmdline_main
Initialization hooks
~~~~~~~~~~~~~~~~~~~~
Initialization hooks called for plugins and ``conftest.py`` files.
.. autofunction:: pytest_addoption
.. autofunction:: pytest_configure
.. autofunction:: pytest_unconfigure
@@ -596,7 +607,10 @@ Generic "runtest" hooks
All runtest related hooks receive a :py:class:`pytest.Item <_pytest.main.Item>` object.
.. autofunction:: pytest_runtestloop
.. autofunction:: pytest_runtest_protocol
.. autofunction:: pytest_runtest_logstart
.. autofunction:: pytest_runtest_logfinish
.. autofunction:: pytest_runtest_setup
.. autofunction:: pytest_runtest_call
.. autofunction:: pytest_runtest_teardown
@@ -616,6 +630,7 @@ Collection hooks
``pytest`` calls the following hooks for collecting files and directories:
.. autofunction:: pytest_collection
.. autofunction:: pytest_ignore_collect
.. autofunction:: pytest_collect_directory
.. autofunction:: pytest_collect_file
@@ -680,14 +695,22 @@ Reference of objects involved in hooks
.. autoclass:: _pytest.config.Parser()
:members:
.. autoclass:: _pytest.main.Node()
.. autoclass:: _pytest.nodes.Node()
:members:
.. autoclass:: _pytest.main.Collector()
.. autoclass:: _pytest.nodes.Collector()
:members:
:show-inheritance:
.. autoclass:: _pytest.main.Item()
.. autoclass:: _pytest.nodes.FSCollector()
:members:
:show-inheritance:
.. autoclass:: _pytest.main.Session()
:members:
:show-inheritance:
.. autoclass:: _pytest.nodes.Item()
:members:
:show-inheritance:

View File

@@ -1,6 +1,5 @@
import json
import py
import textwrap
issues_url = "https://api.github.com/repos/pytest-dev/pytest/issues"

View File

@@ -18,7 +18,8 @@ from _pytest.debugging import pytestPDB as __pytestPDB
from _pytest.recwarn import warns, deprecated_call
from _pytest.outcomes import fail, skip, importorskip, exit, xfail
from _pytest.mark import MARK_GEN as mark, param
from _pytest.main import Item, Collector, File, Session
from _pytest.main import Session
from _pytest.nodes import Item, Collector, File
from _pytest.fixtures import fillfixtures as _fillfuncargs
from _pytest.python import (
Module, Class, Instance, Function, Generator,

View File

@@ -23,23 +23,34 @@ with open('README.rst') as fd:
long_description = fd.read()
def has_environment_marker_support():
def get_environment_marker_support_level():
"""
Tests that setuptools has support for PEP-426 environment marker support.
Tests how well setuptools supports PEP-426 environment marker.
The first known release to support it is 0.7 (and the earliest on PyPI seems to be 0.7.2
so we're using that), see: http://pythonhosted.org/setuptools/history.html#id142
so we're using that), see: https://setuptools.readthedocs.io/en/latest/history.html#id350
The support is later enhanced to allow direct conditional inclusions inside install_requires,
which is now recommended by setuptools. It first appeared in 36.2.0, went broken with 36.2.1, and
again worked since 36.2.2, so we're using that. See:
https://setuptools.readthedocs.io/en/latest/history.html#v36-2-2
https://github.com/pypa/setuptools/issues/1099
References:
* https://wheel.readthedocs.io/en/latest/index.html#defining-conditional-dependencies
* https://www.python.org/dev/peps/pep-0426/#environment-markers
* https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-platform-specific-dependencies
"""
try:
return pkg_resources.parse_version(setuptools.__version__) >= pkg_resources.parse_version('0.7.2')
version = pkg_resources.parse_version(setuptools.__version__)
if version >= pkg_resources.parse_version('36.2.2'):
return 2
if version >= pkg_resources.parse_version('0.7.2'):
return 1
except Exception as exc:
sys.stderr.write("Could not test setuptool's version: %s\n" % exc)
return False
return 0
def main():
@@ -54,7 +65,11 @@ def main():
# used by tox.ini to test with pluggy master
if '_PYTEST_SETUP_SKIP_PLUGGY_DEP' not in os.environ:
install_requires.append('pluggy>=0.5,<0.7')
if has_environment_marker_support():
environment_marker_support_level = get_environment_marker_support_level()
if environment_marker_support_level >= 2:
install_requires.append('funcsigs;python_version<"3.0"')
install_requires.append('colorama;sys_platform=="win32"')
elif environment_marker_support_level == 1:
extras_require[':python_version<"3.0"'] = ['funcsigs']
extras_require[':sys_platform=="win32"'] = ['colorama']
else:

View File

@@ -151,7 +151,7 @@ def publish_release(ctx, version, user, pypi_name):
@invoke.task(help={
'version': 'version being released',
'write_out': 'write changes to the actial changelog'
'write_out': 'write changes to the actual changelog'
})
def changelog(ctx, version, write_out=False):
if write_out:

View File

@@ -2,6 +2,9 @@
from __future__ import absolute_import, division, print_function
import os
import sys
import types
import six
import _pytest._code
import py
@@ -215,8 +218,8 @@ class TestGeneralUsage(object):
assert not result.ret
def test_issue109_sibling_conftests_not_loaded(self, testdir):
sub1 = testdir.tmpdir.mkdir("sub1")
sub2 = testdir.tmpdir.mkdir("sub2")
sub1 = testdir.mkdir("sub1")
sub2 = testdir.mkdir("sub2")
sub1.join("conftest.py").write("assert 0")
result = testdir.runpytest(sub2)
assert result.ret == EXIT_NOTESTSCOLLECTED
@@ -396,7 +399,7 @@ class TestGeneralUsage(object):
p = tmpdir.join('test_test_plugins_given_as_strings.py')
p.write('def test_foo(): pass')
mod = py.std.types.ModuleType("myplugin")
mod = types.ModuleType("myplugin")
monkeypatch.setitem(sys.modules, 'myplugin', mod)
assert pytest.main(args=[str(tmpdir)], plugins=['myplugin']) == 0
@@ -490,17 +493,17 @@ class TestInvocationVariants(object):
def test_python_minus_m_invocation_ok(self, testdir):
p1 = testdir.makepyfile("def test_hello(): pass")
res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1))
res = testdir.run(sys.executable, "-m", "pytest", str(p1))
assert res.ret == 0
def test_python_minus_m_invocation_fail(self, testdir):
p1 = testdir.makepyfile("def test_fail(): 0/0")
res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1))
res = testdir.run(sys.executable, "-m", "pytest", str(p1))
assert res.ret == 1
def test_python_pytest_package(self, testdir):
p1 = testdir.makepyfile("def test_pass(): pass")
res = testdir.run(py.std.sys.executable, "-m", "pytest", str(p1))
res = testdir.run(sys.executable, "-m", "pytest", str(p1))
assert res.ret == 0
res.stdout.fnmatch_lines(["*1 passed*"])
@@ -533,7 +536,7 @@ class TestInvocationVariants(object):
path = testdir.mkpydir("tpkg")
path.join("test_hello.py").write('raise ImportError')
result = testdir.runpytest_subprocess("--pyargs", "tpkg.test_hello")
result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
assert result.ret != 0
result.stdout.fnmatch_lines([
@@ -551,14 +554,14 @@ class TestInvocationVariants(object):
result.stdout.fnmatch_lines([
"*2 passed*"
])
result = testdir.runpytest("--pyargs", "tpkg.test_hello")
result = testdir.runpytest("--pyargs", "tpkg.test_hello", syspathinsert=True)
assert result.ret == 0
result.stdout.fnmatch_lines([
"*1 passed*"
])
def join_pythonpath(what):
cur = py.std.os.environ.get('PYTHONPATH')
cur = os.environ.get('PYTHONPATH')
if cur:
return str(what) + os.pathsep + cur
return what
@@ -575,7 +578,7 @@ class TestInvocationVariants(object):
])
monkeypatch.setenv('PYTHONPATH', join_pythonpath(testdir))
result = testdir.runpytest("--pyargs", "tpkg.test_missing")
result = testdir.runpytest("--pyargs", "tpkg.test_missing", syspathinsert=True)
assert result.ret != 0
result.stderr.fnmatch_lines([
"*not*found*test_missing*",
@@ -603,11 +606,11 @@ class TestInvocationVariants(object):
# The structure of the test directory is now:
# .
# ├── hello
# │   └── ns_pkg
# │   ├── __init__.py
# │   └── hello
# │   ├── __init__.py
# │   └── test_hello.py
# │ └── ns_pkg
# │ ├── __init__.py
# │ └── hello
# │ ├── __init__.py
# │ └── test_hello.py
# └── world
# └── ns_pkg
# ├── __init__.py
@@ -616,7 +619,7 @@ class TestInvocationVariants(object):
# └── test_world.py
def join_pythonpath(*dirs):
cur = py.std.os.environ.get('PYTHONPATH')
cur = os.environ.get('PYTHONPATH')
if cur:
dirs += (cur,)
return os.pathsep.join(str(p) for p in dirs)
@@ -624,10 +627,9 @@ class TestInvocationVariants(object):
for p in search_path:
monkeypatch.syspath_prepend(p)
os.chdir('world')
# mixed module and filenames:
os.chdir('world')
result = testdir.runpytest("--pyargs", "-v", "ns_pkg.hello", "ns_pkg/world")
testdir.chdir()
assert result.ret == 0
result.stdout.fnmatch_lines([
"*test_hello.py::test_hello*PASSED*",
@@ -638,6 +640,7 @@ class TestInvocationVariants(object):
])
# specify tests within a module
testdir.chdir()
result = testdir.runpytest("--pyargs", "-v", "ns_pkg.world.test_world::test_other")
assert result.ret == 0
result.stdout.fnmatch_lines([
@@ -645,6 +648,76 @@ class TestInvocationVariants(object):
"*1 passed*"
])
@pytest.mark.skipif(not hasattr(os, "symlink"), reason="requires symlinks")
def test_cmdline_python_package_symlink(self, testdir, monkeypatch):
"""
test --pyargs option with packages with path containing symlink can
have conftest.py in their package (#2985)
"""
# dummy check that we can actually create symlinks: on Windows `os.symlink` is available,
# but normal users require special admin privileges to create symlinks.
if sys.platform == 'win32':
try:
os.symlink(str(testdir.tmpdir.ensure('tmpfile')), str(testdir.tmpdir.join('tmpfile2')))
except OSError as e:
pytest.skip(six.text_type(e.args[0]))
monkeypatch.delenv('PYTHONDONTWRITEBYTECODE', raising=False)
search_path = ["lib", os.path.join("local", "lib")]
dirname = "lib"
d = testdir.mkdir(dirname)
foo = d.mkdir("foo")
foo.ensure("__init__.py")
lib = foo.mkdir('bar')
lib.ensure("__init__.py")
lib.join("test_bar.py"). \
write("def test_bar(): pass\n"
"def test_other(a_fixture):pass")
lib.join("conftest.py"). \
write("import pytest\n"
"@pytest.fixture\n"
"def a_fixture():pass")
d_local = testdir.mkdir("local")
symlink_location = os.path.join(str(d_local), "lib")
if six.PY2:
os.symlink(str(d), symlink_location)
else:
os.symlink(str(d), symlink_location, target_is_directory=True)
# The structure of the test directory is now:
# .
# ├── local
# │ └── lib -> ../lib
# └── lib
# └── foo
# ├── __init__.py
# └── bar
# ├── __init__.py
# ├── conftest.py
# └── test_bar.py
def join_pythonpath(*dirs):
cur = os.getenv('PYTHONPATH')
if cur:
dirs += (cur,)
return os.pathsep.join(str(p) for p in dirs)
monkeypatch.setenv('PYTHONPATH', join_pythonpath(*search_path))
for p in search_path:
monkeypatch.syspath_prepend(p)
# module picked up in symlink-ed directory:
result = testdir.runpytest("--pyargs", "-v", "foo.bar")
testdir.chdir()
assert result.ret == 0
result.stdout.fnmatch_lines([
"*lib/foo/bar/test_bar.py::test_bar*PASSED*",
"*lib/foo/bar/test_bar.py::test_other*PASSED*",
"*2 passed*"
])
def test_cmdline_python_package_not_exists(self, testdir):
result = testdir.runpytest("--pyargs", "tpkgwhatv")
assert result.ret
@@ -829,7 +902,7 @@ def test_deferred_hook_checking(testdir):
testdir.syspathinsert()
testdir.makepyfile(**{
'plugin.py': """
class Hooks:
class Hooks(object):
def pytest_my_hook(self, config):
pass
@@ -848,3 +921,46 @@ def test_deferred_hook_checking(testdir):
})
result = testdir.runpytest()
result.stdout.fnmatch_lines(['* 1 passed *'])
def test_fixture_values_leak(testdir):
"""Ensure that fixture objects are properly destroyed by the garbage collector at the end of their expected
life-times (#2981).
"""
testdir.makepyfile("""
import attr
import gc
import pytest
import weakref
@attr.s
class SomeObj(object):
name = attr.ib()
fix_of_test1_ref = None
session_ref = None
@pytest.fixture(scope='session')
def session_fix():
global session_ref
obj = SomeObj(name='session-fixture')
session_ref = weakref.ref(obj)
return obj
@pytest.fixture
def fix(session_fix):
global fix_of_test1_ref
obj = SomeObj(name='local-fixture')
fix_of_test1_ref = weakref.ref(obj)
return obj
def test1(fix):
assert fix_of_test1_ref() is fix
def test2():
gc.collect()
# fixture "fix" created during test1 must have been destroyed by now
assert fix_of_test1_ref() is None
""")
result = testdir.runpytest()
result.stdout.fnmatch_lines(['* 2 passed *'])

View File

@@ -2,6 +2,8 @@
from __future__ import absolute_import, division, print_function
import operator
import os
import sys
import _pytest
import py
import pytest
@@ -472,7 +474,7 @@ class TestFormattedExcinfo(object):
excinfo = _pytest._code.ExceptionInfo()
repr = pr.repr_excinfo(excinfo)
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
if py.std.sys.version_info[0] >= 3:
if sys.version_info[0] >= 3:
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
def test_repr_many_line_source_not_existing(self):
@@ -487,7 +489,7 @@ raise ValueError()
excinfo = _pytest._code.ExceptionInfo()
repr = pr.repr_excinfo(excinfo)
assert repr.reprtraceback.reprentries[1].lines[0] == "> ???"
if py.std.sys.version_info[0] >= 3:
if sys.version_info[0] >= 3:
assert repr.chain[0][0].reprentries[1].lines[0] == "> ???"
def test_repr_source_failing_fullsource(self):
@@ -542,16 +544,16 @@ raise ValueError()
tb = FakeRawTB()
excinfo.traceback = Traceback(tb)
fail = IOError() # noqa
fail = IOError()
repr = pr.repr_excinfo(excinfo)
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
if py.std.sys.version_info[0] >= 3:
if sys.version_info[0] >= 3:
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
fail = py.error.ENOENT # noqa
repr = pr.repr_excinfo(excinfo)
assert repr.reprtraceback.reprentries[0].lines[0] == "> ???"
if py.std.sys.version_info[0] >= 3:
if sys.version_info[0] >= 3:
assert repr.chain[0][0].reprentries[0].lines[0] == "> ???"
def test_repr_local(self):
@@ -738,7 +740,7 @@ raise ValueError()
repr = p.repr_excinfo(excinfo)
assert repr.reprtraceback
assert len(repr.reprtraceback.reprentries) == len(reprtb.reprentries)
if py.std.sys.version_info[0] >= 3:
if sys.version_info[0] >= 3:
assert repr.chain[0][0]
assert len(repr.chain[0][0].reprentries) == len(reprtb.reprentries)
assert repr.reprcrash.path.endswith("mod.py")
@@ -758,7 +760,7 @@ raise ValueError()
def raiseos():
raise OSError(2)
monkeypatch.setattr(py.std.os, 'getcwd', raiseos)
monkeypatch.setattr(os, 'getcwd', raiseos)
assert p._makepath(__file__) == __file__
p.repr_traceback(excinfo)
@@ -816,10 +818,10 @@ raise ValueError()
for style in ("short", "long", "no"):
for showlocals in (True, False):
repr = excinfo.getrepr(style=style, showlocals=showlocals)
if py.std.sys.version_info[0] < 3:
if sys.version_info[0] < 3:
assert isinstance(repr, ReprExceptionInfo)
assert repr.reprtraceback.style == style
if py.std.sys.version_info[0] >= 3:
if sys.version_info[0] >= 3:
assert isinstance(repr, ExceptionChainRepr)
for repr in repr.chain:
assert repr[0].style == style

View File

@@ -2,19 +2,17 @@
# disable flake check on this file because some constructs are strange
# or redundant on purpose and can't be disable on a line-by-line basis
from __future__ import absolute_import, division, print_function
import inspect
import sys
import _pytest._code
import py
import pytest
from _pytest._code import Source
from _pytest._code.source import _ast
from _pytest._code.source import ast
if _ast is not None:
astonly = pytest.mark.nothing
else:
astonly = pytest.mark.xfail("True", reason="only works with AST-compile")
astonly = pytest.mark.nothing
failsonjython = pytest.mark.xfail("sys.platform.startswith('java')")
@@ -190,9 +188,9 @@ class TestSourceParsingAndCompiling(object):
def f():
raise ValueError()
""")
source1 = py.std.inspect.getsource(co1)
source1 = inspect.getsource(co1)
assert 'KeyError' in source1
source2 = py.std.inspect.getsource(co2)
source2 = inspect.getsource(co2)
assert 'ValueError' in source2
def test_getstatement(self):
@@ -376,7 +374,6 @@ def test_deindent():
c = '''while True:
pass
'''
import inspect
lines = deindent(inspect.getsource(f).splitlines())
assert lines == ["def f():", " c = '''while True:", " pass", "'''"]
@@ -464,7 +461,7 @@ def test_getfslineno():
fspath, lineno = getfslineno(A)
_, A_lineno = py.std.inspect.findsource(A)
_, A_lineno = inspect.findsource(A)
assert fspath.basename == "test_source.py"
assert lineno == A_lineno

View File

@@ -58,7 +58,6 @@ def test_str_args_deprecated(tmpdir, testdir):
warnings.append(message)
ret = pytest.main("%s -x" % tmpdir, plugins=[Collect()])
testdir.delete_loaded_modules()
msg = ('passing a string to pytest.main() is deprecated, '
'pass a list of arguments instead.')
assert msg in warnings

View File

@@ -1,6 +1,7 @@
# -*- coding: utf-8 -*-
import logging
import pytest
logger = logging.getLogger(__name__)
sublogger = logging.getLogger(__name__ + '.baz')
@@ -26,6 +27,30 @@ def test_change_level(caplog):
assert 'CRITICAL' in caplog.text
def test_change_level_undo(testdir):
"""Ensure that 'set_level' is undone after the end of the test"""
testdir.makepyfile('''
import logging
def test1(caplog):
caplog.set_level(logging.INFO)
# using + operator here so fnmatch_lines doesn't match the code in the traceback
logging.info('log from ' + 'test1')
assert 0
def test2(caplog):
# using + operator here so fnmatch_lines doesn't match the code in the traceback
logging.info('log from ' + 'test2')
assert 0
''')
result = testdir.runpytest_subprocess()
result.stdout.fnmatch_lines([
'*log from test1*',
'*2 failed in *',
])
assert 'log from test2' not in result.stdout.str()
def test_with_statement(caplog):
with caplog.at_level(logging.INFO):
logger.debug('handler DEBUG level')
@@ -42,6 +67,7 @@ def test_with_statement(caplog):
def test_log_access(caplog):
caplog.set_level(logging.INFO)
logger.info('boo %s', 'arg')
assert caplog.records[0].levelname == 'INFO'
assert caplog.records[0].msg == 'boo %s'
@@ -49,6 +75,7 @@ def test_log_access(caplog):
def test_record_tuples(caplog):
caplog.set_level(logging.INFO)
logger.info('boo %s', 'arg')
assert caplog.record_tuples == [
@@ -57,6 +84,7 @@ def test_record_tuples(caplog):
def test_unicode(caplog):
caplog.set_level(logging.INFO)
logger.info(u'')
assert caplog.records[0].levelname == 'INFO'
assert caplog.records[0].msg == u''
@@ -64,7 +92,29 @@ def test_unicode(caplog):
def test_clear(caplog):
caplog.set_level(logging.INFO)
logger.info(u'')
assert len(caplog.records)
caplog.clear()
assert not len(caplog.records)
@pytest.fixture
def logging_during_setup_and_teardown(caplog):
caplog.set_level('INFO')
logger.info('a_setup_log')
yield
logger.info('a_teardown_log')
assert [x.message for x in caplog.get_records('teardown')] == ['a_teardown_log']
def test_caplog_captures_for_all_stages(caplog, logging_during_setup_and_teardown):
assert not caplog.records
assert not caplog.get_records('call')
logger.info('a_call_log')
assert [x.message for x in caplog.get_records('call')] == ['a_call_log']
assert [x.message for x in caplog.get_records('setup')] == ['a_setup_log']
# This reachers into private API, don't use this type of thing in real tests!
assert set(caplog._item.catch_log_handlers.keys()) == {'setup', 'call'}

View File

@@ -0,0 +1,29 @@
import logging
import py.io
from _pytest.logging import ColoredLevelFormatter
def test_coloredlogformatter():
logfmt = '%(filename)-25s %(lineno)4d %(levelname)-8s %(message)s'
record = logging.LogRecord(
name='dummy', level=logging.INFO, pathname='dummypath', lineno=10,
msg='Test Message', args=(), exc_info=False)
class ColorConfig(object):
class option(object):
pass
tw = py.io.TerminalWriter()
tw.hasmarkup = True
formatter = ColoredLevelFormatter(tw, logfmt)
output = formatter.format(record)
assert output == ('dummypath 10 '
'\x1b[32mINFO \x1b[0m Test Message')
tw.hasmarkup = False
formatter = ColoredLevelFormatter(tw, logfmt)
output = formatter.format(record)
assert output == ('dummypath 10 '
'INFO Test Message')

View File

@@ -1,5 +1,8 @@
# -*- coding: utf-8 -*-
import os
import six
import pytest
@@ -35,7 +38,7 @@ def test_messages_logged(testdir):
logger.info('text going to logger')
assert False
''')
result = testdir.runpytest()
result = testdir.runpytest('--log-level=INFO')
assert result.ret == 1
result.stdout.fnmatch_lines(['*- Captured *log call -*',
'*text going to logger*'])
@@ -58,7 +61,7 @@ def test_setup_logging(testdir):
logger.info('text going to logger from call')
assert False
''')
result = testdir.runpytest()
result = testdir.runpytest('--log-level=INFO')
assert result.ret == 1
result.stdout.fnmatch_lines(['*- Captured *log setup -*',
'*text going to logger from setup*',
@@ -79,7 +82,7 @@ def test_teardown_logging(testdir):
logger.info('text going to logger from teardown')
assert False
''')
result = testdir.runpytest()
result = testdir.runpytest('--log-level=INFO')
assert result.ret == 1
result.stdout.fnmatch_lines(['*- Captured *log call -*',
'*text going to logger from call*',
@@ -141,6 +144,30 @@ def test_disable_log_capturing_ini(testdir):
result.stdout.fnmatch_lines(['*- Captured *log call -*'])
@pytest.mark.parametrize('enabled', [True, False])
def test_log_cli_enabled_disabled(testdir, enabled):
msg = 'critical message logged by test'
testdir.makepyfile('''
import logging
def test_log_cli():
logging.critical("{}")
'''.format(msg))
if enabled:
testdir.makeini('''
[pytest]
log_cli=true
''')
result = testdir.runpytest()
if enabled:
result.stdout.fnmatch_lines([
'test_log_cli_enabled_disabled.py::test_log_cli ',
'test_log_cli_enabled_disabled.py* CRITICAL critical message logged by test',
'PASSED*',
])
else:
assert msg not in result.stdout.str()
def test_log_cli_default_level(testdir):
# Default log file level
testdir.makepyfile('''
@@ -148,32 +175,103 @@ def test_log_cli_default_level(testdir):
import logging
def test_log_cli(request):
plugin = request.config.pluginmanager.getplugin('logging-plugin')
assert plugin.log_cli_handler.level == logging.WARNING
logging.getLogger('catchlog').info("This log message won't be shown")
logging.getLogger('catchlog').warning("This log message will be shown")
print('PASSED')
assert plugin.log_cli_handler.level == logging.NOTSET
logging.getLogger('catchlog').info("INFO message won't be shown")
logging.getLogger('catchlog').warning("WARNING message will be shown")
''')
testdir.makeini('''
[pytest]
log_cli=true
''')
result = testdir.runpytest('-s')
result = testdir.runpytest()
# fnmatch_lines does an assertion internally
result.stdout.fnmatch_lines([
'test_log_cli_default_level.py PASSED',
'test_log_cli_default_level.py::test_log_cli ',
'test_log_cli_default_level.py*WARNING message will be shown*',
])
result.stderr.fnmatch_lines([
"* This log message will be shown"
])
for line in result.errlines:
try:
assert "This log message won't be shown" in line
pytest.fail("A log message was shown and it shouldn't have been")
except AssertionError:
continue
assert "INFO message won't be shown" not in result.stdout.str()
# make sure that that we get a '0' exit code for the testsuite
assert result.ret == 0
def test_log_cli_default_level_multiple_tests(testdir, request):
"""Ensure we reset the first newline added by the live logger between tests"""
filename = request.node.name + '.py'
testdir.makepyfile('''
import logging
def test_log_1():
logging.warning("log message from test_log_1")
def test_log_2():
logging.warning("log message from test_log_2")
''')
testdir.makeini('''
[pytest]
log_cli=true
''')
result = testdir.runpytest()
result.stdout.fnmatch_lines([
'{}::test_log_1 '.format(filename),
'*WARNING*log message from test_log_1*',
'PASSED *50%*',
'{}::test_log_2 '.format(filename),
'*WARNING*log message from test_log_2*',
'PASSED *100%*',
'=* 2 passed in *=',
])
def test_log_cli_default_level_sections(testdir, request):
"""Check that with live logging enable we are printing the correct headers during setup/call/teardown."""
filename = request.node.name + '.py'
testdir.makepyfile('''
import pytest
import logging
@pytest.fixture
def fix(request):
logging.warning("log message from setup of {}".format(request.node.name))
yield
logging.warning("log message from teardown of {}".format(request.node.name))
def test_log_1(fix):
logging.warning("log message from test_log_1")
def test_log_2(fix):
logging.warning("log message from test_log_2")
''')
testdir.makeini('''
[pytest]
log_cli=true
''')
result = testdir.runpytest()
result.stdout.fnmatch_lines([
'{}::test_log_1 '.format(filename),
'*-- live log setup --*',
'*WARNING*log message from setup of test_log_1*',
'*-- live log call --*',
'*WARNING*log message from test_log_1*',
'PASSED *50%*',
'*-- live log teardown --*',
'*WARNING*log message from teardown of test_log_1*',
'{}::test_log_2 '.format(filename),
'*-- live log setup --*',
'*WARNING*log message from setup of test_log_2*',
'*-- live log call --*',
'*WARNING*log message from test_log_2*',
'PASSED *100%*',
'*-- live log teardown --*',
'*WARNING*log message from teardown of test_log_2*',
'=* 2 passed in *=',
])
def test_log_cli_level(testdir):
# Default log file level
testdir.makepyfile('''
@@ -186,22 +284,19 @@ def test_log_cli_level(testdir):
logging.getLogger('catchlog').info("This log message will be shown")
print('PASSED')
''')
testdir.makeini('''
[pytest]
log_cli=true
''')
result = testdir.runpytest('-s', '--log-cli-level=INFO')
# fnmatch_lines does an assertion internally
result.stdout.fnmatch_lines([
'test_log_cli_level.py PASSED',
'test_log_cli_level.py*This log message will be shown',
'PASSED', # 'PASSED' on its own line because the log message prints a new line
])
result.stderr.fnmatch_lines([
"* This log message will be shown"
])
for line in result.errlines:
try:
assert "This log message won't be shown" in line
pytest.fail("A log message was shown and it shouldn't have been")
except AssertionError:
continue
assert "This log message won't be shown" not in result.stdout.str()
# make sure that that we get a '0' exit code for the testsuite
assert result.ret == 0
@@ -210,17 +305,10 @@ def test_log_cli_level(testdir):
# fnmatch_lines does an assertion internally
result.stdout.fnmatch_lines([
'test_log_cli_level.py PASSED',
'test_log_cli_level.py* This log message will be shown',
'PASSED', # 'PASSED' on its own line because the log message prints a new line
])
result.stderr.fnmatch_lines([
"* This log message will be shown"
])
for line in result.errlines:
try:
assert "This log message won't be shown" in line
pytest.fail("A log message was shown and it shouldn't have been")
except AssertionError:
continue
assert "This log message won't be shown" not in result.stdout.str()
# make sure that that we get a '0' exit code for the testsuite
assert result.ret == 0
@@ -230,6 +318,7 @@ def test_log_cli_ini_level(testdir):
testdir.makeini(
"""
[pytest]
log_cli=true
log_cli_level = INFO
""")
testdir.makepyfile('''
@@ -247,17 +336,10 @@ def test_log_cli_ini_level(testdir):
# fnmatch_lines does an assertion internally
result.stdout.fnmatch_lines([
'test_log_cli_ini_level.py PASSED',
'test_log_cli_ini_level.py* This log message will be shown',
'PASSED', # 'PASSED' on its own line because the log message prints a new line
])
result.stderr.fnmatch_lines([
"* This log message will be shown"
])
for line in result.errlines:
try:
assert "This log message won't be shown" in line
pytest.fail("A log message was shown and it shouldn't have been")
except AssertionError:
continue
assert "This log message won't be shown" not in result.stdout.str()
# make sure that that we get a '0' exit code for the testsuite
assert result.ret == 0
@@ -278,7 +360,7 @@ def test_log_file_cli(testdir):
log_file = testdir.tmpdir.join('pytest.log').strpath
result = testdir.runpytest('-s', '--log-file={0}'.format(log_file))
result = testdir.runpytest('-s', '--log-file={0}'.format(log_file), '--log-file-level=WARNING')
# fnmatch_lines does an assertion internally
result.stdout.fnmatch_lines([
@@ -327,6 +409,16 @@ def test_log_file_cli_level(testdir):
assert "This log message won't be shown" not in contents
def test_log_level_not_changed_by_default(testdir):
testdir.makepyfile('''
import logging
def test_log_file():
assert logging.getLogger().level == logging.WARNING
''')
result = testdir.runpytest('-s')
result.stdout.fnmatch_lines('* 1 passed in *')
def test_log_file_ini(testdir):
log_file = testdir.tmpdir.join('pytest.log').strpath
@@ -334,6 +426,7 @@ def test_log_file_ini(testdir):
"""
[pytest]
log_file={0}
log_file_level=WARNING
""".format(log_file))
testdir.makepyfile('''
import pytest
@@ -396,3 +489,53 @@ def test_log_file_ini_level(testdir):
contents = rfh.read()
assert "This log message will be shown" in contents
assert "This log message won't be shown" not in contents
@pytest.mark.parametrize('has_capture_manager', [True, False])
def test_live_logging_suspends_capture(has_capture_manager, request):
"""Test that capture manager is suspended when we emitting messages for live logging.
This tests the implementation calls instead of behavior because it is difficult/impossible to do it using
``testdir`` facilities because they do their own capturing.
We parametrize the test to also make sure _LiveLoggingStreamHandler works correctly if no capture manager plugin
is installed.
"""
import logging
from functools import partial
from _pytest.capture import CaptureManager
from _pytest.logging import _LiveLoggingStreamHandler
class MockCaptureManager:
calls = []
def suspend_global_capture(self):
self.calls.append('suspend_global_capture')
def resume_global_capture(self):
self.calls.append('resume_global_capture')
# sanity check
assert CaptureManager.suspend_capture_item
assert CaptureManager.resume_global_capture
class DummyTerminal(six.StringIO):
def section(self, *args, **kwargs):
pass
out_file = DummyTerminal()
capture_manager = MockCaptureManager() if has_capture_manager else None
handler = _LiveLoggingStreamHandler(out_file, capture_manager)
handler.set_when('call')
logger = logging.getLogger(__name__ + '.test_live_logging_suspends_capture')
logger.addHandler(handler)
request.addfinalizer(partial(logger.removeHandler, handler))
logger.critical('some message')
if has_capture_manager:
assert MockCaptureManager.calls == ['suspend_global_capture', 'resume_global_capture']
else:
assert MockCaptureManager.calls == []
assert out_file.getvalue() == '\nsome message\n'

View File

@@ -4,13 +4,9 @@ import sys
from textwrap import dedent
import _pytest._code
import py
import pytest
from _pytest.main import (
Collector,
EXIT_NOTESTSCOLLECTED
)
from _pytest.main import EXIT_NOTESTSCOLLECTED
from _pytest.nodes import Collector
ignore_parametrized_marks = pytest.mark.filterwarnings('ignore:Applying marks directly to parameters')
@@ -25,7 +21,7 @@ class TestModule(object):
b = testdir.mkdir("b")
p = a.ensure("test_whatever.py")
p.pyimport()
del py.std.sys.modules['test_whatever']
del sys.modules['test_whatever']
b.ensure("test_whatever.py")
result = testdir.runpytest()
result.stdout.fnmatch_lines([
@@ -754,7 +750,7 @@ class TestSorting(object):
assert fn1 == fn2
assert fn1 != modcol
if py.std.sys.version_info < (3, 0):
if sys.version_info < (3, 0):
assert cmp(fn1, fn2) == 0
assert hash(fn1) == hash(fn2)
@@ -883,10 +879,10 @@ class TestConftestCustomization(object):
import sys, os, imp
from _pytest.python import Module
class Loader:
class Loader(object):
def load_module(self, name):
return imp.load_source(name, name + ".narf")
class Finder:
class Finder(object):
def find_module(self, name, path=None):
if os.path.exists(name + ".narf"):
return Loader()

View File

@@ -2828,7 +2828,7 @@ class TestShowFixtures(object):
def test_show_fixtures_indented_in_class(self, testdir):
p = testdir.makepyfile(dedent('''
import pytest
class TestClass:
class TestClass(object):
@pytest.fixture
def fixture1(self):
"""line1

View File

@@ -14,7 +14,7 @@ PY3 = sys.version_info >= (3, 0)
class TestMetafunc(object):
def Metafunc(self, func):
def Metafunc(self, func, config=None):
# the unit tests of this class check if things work correctly
# on the funcarg level, so we don't need a full blown
# initiliazation
@@ -26,7 +26,7 @@ class TestMetafunc(object):
names = fixtures.getfuncargnames(func)
fixtureinfo = FixtureInfo(names)
return python.Metafunc(func, fixtureinfo, None)
return python.Metafunc(func, fixtureinfo, config)
def test_no_funcargs(self, testdir):
def function():
@@ -156,7 +156,19 @@ class TestMetafunc(object):
def test_parametrize_empty_list(self):
def func(y):
pass
metafunc = self.Metafunc(func)
class MockConfig(object):
def getini(self, name):
return ''
@property
def hook(self):
return self
def pytest_make_parametrize_id(self, **kw):
pass
metafunc = self.Metafunc(func, MockConfig())
metafunc.parametrize("y", [])
assert 'skip' == metafunc._calls[0].marks[0].name
@@ -235,6 +247,25 @@ class TestMetafunc(object):
for val, expected in values:
assert _idval(val, 'a', 6, None) == expected
def test_class_or_function_idval(self):
"""unittest for the expected behavior to obtain ids for parametrized
values that are classes or functions: their __name__.
"""
from _pytest.python import _idval
class TestClass(object):
pass
def test_function():
pass
values = [
(TestClass, "TestClass"),
(test_function, "test_function"),
]
for val, expected in values:
assert _idval(val, 'a', 6, None) == expected
@pytest.mark.issue250
def test_idmaker_autoname(self):
from _pytest.python import idmaker
@@ -730,7 +761,7 @@ class TestMetafuncFunctional(object):
def test_attributes(self, testdir):
p = testdir.makepyfile("""
# assumes that generate/provide runs in the same process
import py, pytest
import sys, pytest
def pytest_generate_tests(metafunc):
metafunc.addcall(param=metafunc)
@@ -749,7 +780,7 @@ class TestMetafuncFunctional(object):
def test_method(self, metafunc, pytestconfig):
assert metafunc.config == pytestconfig
assert metafunc.module.__name__ == __name__
if py.std.sys.version_info > (3, 0):
if sys.version_info > (3, 0):
unbound = TestClass.test_method
else:
unbound = TestClass.test_method.im_func

View File

@@ -1,5 +1,6 @@
from __future__ import absolute_import, division, print_function
import py
import subprocess
import sys
import pytest
# test for _argcomplete but not specific for any application
@@ -23,21 +24,21 @@ def equal_with_bash(prefix, ffc, fc, out=None):
def _wrapcall(*args, **kargs):
try:
if py.std.sys.version_info > (2, 7):
return py.std.subprocess.check_output(*args, **kargs).decode().splitlines()
if sys.version_info > (2, 7):
return subprocess.check_output(*args, **kargs).decode().splitlines()
if 'stdout' in kargs:
raise ValueError('stdout argument not allowed, it will be overridden.')
process = py.std.subprocess.Popen(
stdout=py.std.subprocess.PIPE, *args, **kargs)
process = subprocess.Popen(
stdout=subprocess.PIPE, *args, **kargs)
output, unused_err = process.communicate()
retcode = process.poll()
if retcode:
cmd = kargs.get("args")
if cmd is None:
cmd = args[0]
raise py.std.subprocess.CalledProcessError(retcode, cmd)
raise subprocess.CalledProcessError(retcode, cmd)
return output.decode().splitlines()
except py.std.subprocess.CalledProcessError:
except subprocess.CalledProcessError:
return []
@@ -83,7 +84,7 @@ class TestArgComplete(object):
ffc = FastFilesCompleter()
fc = FilesCompleter()
for x in ['/', '/d', '/data', 'qqq', '']:
assert equal_with_bash(x, ffc, fc, out=py.std.sys.stdout)
assert equal_with_bash(x, ffc, fc, out=sys.stdout)
@pytest.mark.skipif("sys.platform in ('win32', 'darwin')")
def test_remove_dir_prefix(self):
@@ -94,4 +95,4 @@ class TestArgComplete(object):
ffc = FastFilesCompleter()
fc = FilesCompleter()
for x in '/usr/'.split():
assert not equal_with_bash(x, ffc, fc, out=py.std.sys.stdout)
assert not equal_with_bash(x, ffc, fc, out=sys.stdout)

View File

@@ -5,6 +5,7 @@ import os
import py_compile
import stat
import sys
import textwrap
import zipfile
import py
import pytest
@@ -128,6 +129,16 @@ class TestAssertionRewrite(object):
assert len(m.body) == 1
assert m.body[0].msg is None
def test_dont_rewrite_plugin(self, testdir):
contents = {
"conftest.py": "pytest_plugins = 'plugin'; import plugin",
"plugin.py": "'PYTEST_DONT_REWRITE'",
"test_foo.py": "def test_foo(): pass",
}
testdir.makepyfile(**contents)
result = testdir.runpytest_subprocess()
assert "warnings" not in "".join(result.outlines)
def test_name(self):
def f():
assert False
@@ -901,7 +912,7 @@ class TestAssertionRewriteHookDetails(object):
def test_reload_is_same(self, testdir):
# A file that will be picked up during collecting.
testdir.tmpdir.join("file.py").ensure()
testdir.tmpdir.join("pytest.ini").write(py.std.textwrap.dedent("""
testdir.tmpdir.join("pytest.ini").write(textwrap.dedent("""
[pytest]
python_files = *.py
"""))
@@ -987,7 +998,7 @@ class TestIssue2121():
def test_simple_failure():
assert 1 + 1 == 3
""")
testdir.tmpdir.join("pytest.ini").write(py.std.textwrap.dedent("""
testdir.tmpdir.join("pytest.ini").write(textwrap.dedent("""
[pytest]
python_files = tests/**.py
"""))

30
testing/test_cache.py → testing/test_cacheprovider.py Executable file → Normal file
View File

@@ -31,7 +31,7 @@ class TestNewAPI(object):
def test_cache_writefail_cachfile_silent(self, testdir):
testdir.makeini("[pytest]")
testdir.tmpdir.join('.cache').write('gone wrong')
testdir.tmpdir.join('.pytest_cache').write('gone wrong')
config = testdir.parseconfigure()
cache = config.cache
cache.set('test/broken', [])
@@ -39,14 +39,14 @@ class TestNewAPI(object):
@pytest.mark.skipif(sys.platform.startswith('win'), reason='no chmod on windows')
def test_cache_writefail_permissions(self, testdir):
testdir.makeini("[pytest]")
testdir.tmpdir.ensure_dir('.cache').chmod(0)
testdir.tmpdir.ensure_dir('.pytest_cache').chmod(0)
config = testdir.parseconfigure()
cache = config.cache
cache.set('test/broken', [])
@pytest.mark.skipif(sys.platform.startswith('win'), reason='no chmod on windows')
def test_cache_failure_warns(self, testdir):
testdir.tmpdir.ensure_dir('.cache').chmod(0)
testdir.tmpdir.ensure_dir('.pytest_cache').chmod(0)
testdir.makepyfile("""
def test_error():
raise Exception
@@ -127,7 +127,7 @@ def test_cache_reportheader(testdir):
""")
result = testdir.runpytest("-v")
result.stdout.fnmatch_lines([
"cachedir: .cache"
"cachedir: .pytest_cache"
])
@@ -201,8 +201,8 @@ class TestLastFailed(object):
])
# Run this again to make sure clear-cache is robust
if os.path.isdir('.cache'):
shutil.rmtree('.cache')
if os.path.isdir('.pytest_cache'):
shutil.rmtree('.pytest_cache')
result = testdir.runpytest("--lf", "--cache-clear")
result.stdout.fnmatch_lines([
"*1 failed*2 passed*",
@@ -410,8 +410,8 @@ class TestLastFailed(object):
def test_lastfailed_collectfailure(self, testdir, monkeypatch):
testdir.makepyfile(test_maybe="""
import py
env = py.std.os.environ
import os
env = os.environ
if '1' == env['FAILIMPORT']:
raise ImportError('fail')
def test_hello():
@@ -439,8 +439,8 @@ class TestLastFailed(object):
def test_lastfailed_failure_subset(self, testdir, monkeypatch):
testdir.makepyfile(test_maybe="""
import py
env = py.std.os.environ
import os
env = os.environ
if '1' == env['FAILIMPORT']:
raise ImportError('fail')
def test_hello():
@@ -448,8 +448,8 @@ class TestLastFailed(object):
""")
testdir.makepyfile(test_maybe2="""
import py
env = py.std.os.environ
import os
env = os.environ
if '1' == env['FAILIMPORT']:
raise ImportError('fail')
def test_hello():
@@ -495,15 +495,15 @@ class TestLastFailed(object):
# Issue #1342
testdir.makepyfile(test_empty='')
testdir.runpytest('-q', '--lf')
assert not os.path.exists('.cache')
assert not os.path.exists('.pytest_cache')
testdir.makepyfile(test_successful='def test_success():\n assert True')
testdir.runpytest('-q', '--lf')
assert not os.path.exists('.cache')
assert not os.path.exists('.pytest_cache')
testdir.makepyfile(test_errored='def test_error():\n assert False')
testdir.runpytest('-q', '--lf')
assert os.path.exists('.cache')
assert os.path.exists('.pytest_cache')
def test_xfail_not_considered_failure(self, testdir):
testdir.makepyfile('''

View File

@@ -1245,7 +1245,7 @@ def test_py36_windowsconsoleio_workaround_non_standard_streams():
"""
from _pytest.capture import _py36_windowsconsoleio_workaround
class DummyStream:
class DummyStream(object):
def write(self, s):
pass

View File

@@ -1,6 +1,7 @@
from __future__ import absolute_import, division, print_function
import pprint
import sys
import pytest
import py
import _pytest._code
from _pytest.main import Session, EXIT_NOTESTSCOLLECTED, _in_venv
@@ -36,7 +37,7 @@ class TestCollector(object):
assert fn1 == fn2
assert fn1 != modcol
if py.std.sys.version_info < (3, 0):
if sys.version_info < (3, 0):
assert cmp(fn1, fn2) == 0
assert hash(fn1) == hash(fn2)
@@ -128,7 +129,7 @@ class TestCollectFS(object):
("activate", "activate.csh", "activate.fish",
"Activate", "Activate.bat", "Activate.ps1"))
def test_ignored_virtualenvs(self, testdir, fname):
bindir = "Scripts" if py.std.sys.platform.startswith("win") else "bin"
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
testdir.tmpdir.ensure("virtual", bindir, fname)
testfile = testdir.tmpdir.ensure("virtual", "test_invenv.py")
testfile.write("def test_hello(): pass")
@@ -147,7 +148,7 @@ class TestCollectFS(object):
("activate", "activate.csh", "activate.fish",
"Activate", "Activate.bat", "Activate.ps1"))
def test_ignored_virtualenvs_norecursedirs_precedence(self, testdir, fname):
bindir = "Scripts" if py.std.sys.platform.startswith("win") else "bin"
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
# norecursedirs takes priority
testdir.tmpdir.ensure(".virtual", bindir, fname)
testfile = testdir.tmpdir.ensure(".virtual", "test_invenv.py")
@@ -163,7 +164,7 @@ class TestCollectFS(object):
"Activate", "Activate.bat", "Activate.ps1"))
def test__in_venv(self, testdir, fname):
"""Directly test the virtual env detection function"""
bindir = "Scripts" if py.std.sys.platform.startswith("win") else "bin"
bindir = "Scripts" if sys.platform.startswith("win") else "bin"
# no bin/activate, not a virtualenv
base_path = testdir.tmpdir.mkdir('venv')
assert _in_venv(base_path) is False
@@ -436,7 +437,7 @@ class TestSession(object):
assert item.name == "test_func"
newid = item.nodeid
assert newid == id
py.std.pprint.pprint(hookrec.calls)
pprint.pprint(hookrec.calls)
topdir = testdir.tmpdir # noqa
hookrec.assert_contains([
("pytest_collectstart", "collector.fspath == topdir"),
@@ -486,7 +487,7 @@ class TestSession(object):
id = p.basename
items, hookrec = testdir.inline_genitems(id)
py.std.pprint.pprint(hookrec.calls)
pprint.pprint(hookrec.calls)
assert len(items) == 2
hookrec.assert_contains([
("pytest_collectstart",
@@ -508,7 +509,7 @@ class TestSession(object):
items, hookrec = testdir.inline_genitems()
assert len(items) == 1
py.std.pprint.pprint(hookrec.calls)
pprint.pprint(hookrec.calls)
hookrec.assert_contains([
("pytest_collectstart", "collector.fspath == test_aaa"),
("pytest_pycollect_makeitem", "name == 'test_func'"),
@@ -529,7 +530,7 @@ class TestSession(object):
items, hookrec = testdir.inline_genitems(id)
assert len(items) == 2
py.std.pprint.pprint(hookrec.calls)
pprint.pprint(hookrec.calls)
hookrec.assert_contains([
("pytest_collectstart", "collector.fspath == test_aaa"),
("pytest_pycollect_makeitem", "name == 'test_func'"),

View File

@@ -1,6 +1,6 @@
from __future__ import absolute_import, division, print_function
import sys
import py
import textwrap
import pytest
import _pytest._code
@@ -57,7 +57,7 @@ class TestParseIni(object):
('pytest', 'pytest.ini')],
)
def test_ini_names(self, testdir, name, section):
testdir.tmpdir.join(name).write(py.std.textwrap.dedent("""
testdir.tmpdir.join(name).write(textwrap.dedent("""
[{section}]
minversion = 1.0
""".format(section=section)))
@@ -66,11 +66,11 @@ class TestParseIni(object):
def test_toxini_before_lower_pytestini(self, testdir):
sub = testdir.tmpdir.mkdir("sub")
sub.join("tox.ini").write(py.std.textwrap.dedent("""
sub.join("tox.ini").write(textwrap.dedent("""
[pytest]
minversion = 2.0
"""))
testdir.tmpdir.join("pytest.ini").write(py.std.textwrap.dedent("""
testdir.tmpdir.join("pytest.ini").write(textwrap.dedent("""
[pytest]
minversion = 1.5
"""))
@@ -731,7 +731,7 @@ class TestRootdir(object):
class TestOverrideIniArgs(object):
@pytest.mark.parametrize("name", "setup.cfg tox.ini pytest.ini".split())
def test_override_ini_names(self, testdir, name):
testdir.tmpdir.join(name).write(py.std.textwrap.dedent("""
testdir.tmpdir.join(name).write(textwrap.dedent("""
[pytest]
custom = 1.0"""))
testdir.makeconftest("""
@@ -781,16 +781,18 @@ class TestOverrideIniArgs(object):
testdir.makeini("""
[pytest]
custom_option_1=custom_option_1
custom_option_2=custom_option_2""")
custom_option_2=custom_option_2
""")
testdir.makepyfile("""
def test_multiple_options(pytestconfig):
prefix = "custom_option"
for x in range(1, 5):
ini_value=pytestconfig.getini("%s_%d" % (prefix, x))
print('\\nini%d:%s' % (x, ini_value))""")
print('\\nini%d:%s' % (x, ini_value))
""")
result = testdir.runpytest(
"--override-ini", 'custom_option_1=fulldir=/tmp/user1',
'custom_option_2=url=/tmp/user2?a=b&d=e',
'-o', 'custom_option_2=url=/tmp/user2?a=b&d=e',
"-o", 'custom_option_3=True',
"-o", 'custom_option_4=no', "-s")
result.stdout.fnmatch_lines(["ini1:fulldir=/tmp/user1",
@@ -853,10 +855,42 @@ class TestOverrideIniArgs(object):
assert rootdir == tmpdir
assert inifile is None
def test_addopts_before_initini(self, testdir, tmpdir, monkeypatch):
def test_addopts_before_initini(self, monkeypatch):
cache_dir = '.custom_cache'
monkeypatch.setenv('PYTEST_ADDOPTS', '-o cache_dir=%s' % cache_dir)
from _pytest.config import get_config
config = get_config()
config._preparse([], addopts=True)
assert config._override_ini == [['cache_dir=%s' % cache_dir]]
assert config._override_ini == ['cache_dir=%s' % cache_dir]
def test_override_ini_does_not_contain_paths(self):
"""Check that -o no longer swallows all options after it (#3103)"""
from _pytest.config import get_config
config = get_config()
config._preparse(['-o', 'cache_dir=/cache', '/some/test/path'])
assert config._override_ini == ['cache_dir=/cache']
def test_multiple_override_ini_options(self, testdir, request):
"""Ensure a file path following a '-o' option does not generate an error (#3103)"""
testdir.makepyfile(**{
"conftest.py": """
def pytest_addoption(parser):
parser.addini('foo', default=None, help='some option')
parser.addini('bar', default=None, help='some option')
""",
"test_foo.py": """
def test(pytestconfig):
assert pytestconfig.getini('foo') == '1'
assert pytestconfig.getini('bar') == '0'
""",
"test_bar.py": """
def test():
assert False
""",
})
result = testdir.runpytest('-o', 'foo=1', '-o', 'bar=0', 'test_foo.py')
assert 'ERROR:' not in result.stderr.str()
result.stdout.fnmatch_lines([
'collected 1 item',
'*= 1 passed in *=',
])

View File

@@ -84,7 +84,7 @@ def test_conftest_in_nonpkg_with_init(tmpdir):
def test_doubledash_considered(testdir):
conf = testdir.mkdir("--option")
conf.join("conftest.py").ensure()
conf.ensure("conftest.py")
conftest = PytestPluginManager()
conftest_setinitial(conftest, [conf.basename, conf.basename])
values = conftest._getconftestmodules(conf)
@@ -232,7 +232,7 @@ def test_fixture_dependency(testdir, monkeypatch):
ct1.write("")
sub = testdir.mkdir("sub")
sub.join("__init__.py").write("")
sub.join("conftest.py").write(py.std.textwrap.dedent("""
sub.join("conftest.py").write(dedent("""
import pytest
@pytest.fixture
@@ -249,7 +249,7 @@ def test_fixture_dependency(testdir, monkeypatch):
"""))
subsub = sub.mkdir("subsub")
subsub.join("__init__.py").write("")
subsub.join("test_bar.py").write(py.std.textwrap.dedent("""
subsub.join("test_bar.py").write(dedent("""
import pytest
@pytest.fixture
@@ -265,16 +265,12 @@ def test_fixture_dependency(testdir, monkeypatch):
def test_conftest_found_with_double_dash(testdir):
sub = testdir.mkdir("sub")
sub.join("conftest.py").write(py.std.textwrap.dedent("""
sub.join("conftest.py").write(dedent("""
def pytest_addoption(parser):
parser.addoption("--hello-world", action="store_true")
"""))
p = sub.join("test_hello.py")
p.write(py.std.textwrap.dedent("""
import pytest
def test_hello(found):
assert found == 1
"""))
p.write("def test_hello(): pass")
result = testdir.runpytest(str(p) + "::test_hello", "-h")
result.stdout.fnmatch_lines("""
*--hello-world*

View File

@@ -879,6 +879,27 @@ def test_record_property_same_name(testdir):
pnodes[1].assert_attr(name="foo", value="baz")
def test_record_attribute(testdir):
testdir.makepyfile("""
import pytest
@pytest.fixture
def other(record_xml_attribute):
record_xml_attribute("bar", 1)
def test_record(record_xml_attribute, other):
record_xml_attribute("foo", "<1");
""")
result, dom = runandparse(testdir, '-rw')
node = dom.find_first_by_tag("testsuite")
tnode = node.find_first_by_tag("testcase")
tnode.assert_attr(bar="1")
tnode.assert_attr(foo="<1")
result.stdout.fnmatch_lines([
'test_record_attribute.py::test_record',
'*record_xml_attribute*experimental*',
])
def test_random_report_log_xdist(testdir):
"""xdist calls pytest_runtest_logreport as they are executed by the slaves,
with nodes from several nodes overlapping, so junitxml must cope with that

View File

@@ -3,7 +3,10 @@ import os
import sys
import pytest
from _pytest.mark import MarkGenerator as Mark, ParameterSet, transfer_markers
from _pytest.mark import (
MarkGenerator as Mark, ParameterSet, transfer_markers,
EMPTY_PARAMETERSET_OPTION,
)
class TestMark(object):
@@ -344,6 +347,21 @@ def test_keyword_option_parametrize(spec, testdir):
assert list(passed) == list(passed_result)
@pytest.mark.parametrize("spec", [
("foo or import", "ERROR: Python keyword 'import' not accepted in expressions passed to '-k'"),
("foo or", "ERROR: Wrong expression passed to '-k': foo or")
])
def test_keyword_option_wrong_arguments(spec, testdir, capsys):
testdir.makepyfile("""
def test_func(arg):
pass
""")
opt, expected_result = spec
testdir.inline_run("-k", opt)
out = capsys.readouterr().err
assert expected_result in out
def test_parametrized_collected_from_command_line(testdir):
"""Parametrized test not collected if test named specified
in command line issue#649.
@@ -876,3 +894,27 @@ class TestMarkDecorator(object):
])
def test__eq__(self, lhs, rhs, expected):
assert (lhs == rhs) == expected
@pytest.mark.parametrize('mark', [None, '', 'skip', 'xfail'])
def test_parameterset_for_parametrize_marks(testdir, mark):
if mark is not None:
testdir.makeini(
"[pytest]\n{}={}".format(EMPTY_PARAMETERSET_OPTION, mark))
config = testdir.parseconfig()
from _pytest.mark import pytest_configure, get_empty_parameterset_mark
pytest_configure(config)
result_mark = get_empty_parameterset_mark(config, ['a'], all)
if mark in (None, ''):
# normalize to the requested name
mark = 'skip'
assert result_mark.name == mark
assert result_mark.kwargs['reason'].startswith("got empty parameter set ")
if mark == 'xfail':
assert result_mark.kwargs.get('run') is False
def test_parameterset_for_parametrize_bad_markname(testdir):
with pytest.raises(pytest.UsageError):
test_parameterset_for_parametrize_marks(testdir, 'bad')

View File

@@ -1,4 +1,5 @@
from __future__ import absolute_import, division, print_function
import argparse
import sys
import os
import py
@@ -189,7 +190,7 @@ class TestParser(object):
assert option.no is False
def test_drop_short_helper(self):
parser = py.std.argparse.ArgumentParser(formatter_class=parseopt.DropShorterLongHelpFormatter)
parser = argparse.ArgumentParser(formatter_class=parseopt.DropShorterLongHelpFormatter)
parser.add_argument('-t', '--twoword', '--duo', '--two-word', '--two',
help='foo').map_long_option = {'two': 'two-word'}
# throws error on --deux only!

View File

@@ -338,7 +338,7 @@ class TestPDB(object):
self.flush(child)
def test_pdb_collection_failure_is_shown(self, testdir):
p1 = testdir.makepyfile("""xxx """)
p1 = testdir.makepyfile("xxx")
result = testdir.runpytest_subprocess("--pdb", p1)
result.stdout.fnmatch_lines([
"*NameError*xxx*",
@@ -402,5 +402,4 @@ class TestPDB(object):
child = testdir.spawn_pytest("--pdbcls=custom_pdb:CustomPdb %s" % str(p1))
child.expect('custom set_trace>')
if child.isalive():
child.wait()
self.flush(child)

View File

@@ -1,8 +1,10 @@
# encoding: UTF-8
from __future__ import absolute_import, division, print_function
import pytest
import py
import os
import re
import sys
import types
from _pytest.config import get_config, PytestPluginManager
from _pytest.main import EXIT_NOTESTSCOLLECTED, Session
@@ -208,14 +210,14 @@ def test_importplugin_error_message(testdir, pytestpm):
expected_message = '.*Error importing plugin "qwe": Not possible to import: .'
expected_traceback = ".*in test_traceback"
assert py.std.re.match(expected_message, str(excinfo.value))
assert py.std.re.match(expected_traceback, str(excinfo.traceback[-1]))
assert re.match(expected_message, str(excinfo.value))
assert re.match(expected_traceback, str(excinfo.traceback[-1]))
class TestPytestPluginManager(object):
def test_register_imported_modules(self):
pm = PytestPluginManager()
mod = py.std.types.ModuleType("x.y.pytest_hello")
mod = types.ModuleType("x.y.pytest_hello")
pm.register(mod)
assert pm.is_registered(mod)
values = pm.get_plugins()
@@ -226,8 +228,8 @@ class TestPytestPluginManager(object):
assert pm.get_plugins() == values
def test_canonical_import(self, monkeypatch):
mod = py.std.types.ModuleType("pytest_xyz")
monkeypatch.setitem(py.std.sys.modules, 'pytest_xyz', mod)
mod = types.ModuleType("pytest_xyz")
monkeypatch.setitem(sys.modules, 'pytest_xyz', mod)
pm = PytestPluginManager()
pm.import_plugin('pytest_xyz')
assert pm.get_plugin('pytest_xyz') == mod
@@ -237,7 +239,7 @@ class TestPytestPluginManager(object):
testdir.syspathinsert()
testdir.makepyfile(pytest_p1="#")
testdir.makepyfile(pytest_p2="#")
mod = py.std.types.ModuleType("temp")
mod = types.ModuleType("temp")
mod.pytest_plugins = ["pytest_p1", "pytest_p2"]
pytestpm.consider_module(mod)
assert pytestpm.get_plugin("pytest_p1").__name__ == "pytest_p1"
@@ -245,12 +247,12 @@ class TestPytestPluginManager(object):
def test_consider_module_import_module(self, testdir):
pytestpm = get_config().pluginmanager
mod = py.std.types.ModuleType("x")
mod = types.ModuleType("x")
mod.pytest_plugins = "pytest_a"
aplugin = testdir.makepyfile(pytest_a="#")
reprec = testdir.make_hook_recorder(pytestpm)
# syspath.prepend(aplugin.dirpath())
py.std.sys.path.insert(0, str(aplugin.dirpath()))
sys.path.insert(0, str(aplugin.dirpath()))
pytestpm.consider_module(mod)
call = reprec.getcall(pytestpm.hook.pytest_plugin_registered.name)
assert call.plugin.__name__ == "pytest_a"

View File

@@ -1,8 +1,12 @@
# -*- coding: utf-8 -*-
from __future__ import absolute_import, division, print_function
import pytest
import os
import py.path
import pytest
import sys
import _pytest.pytester as pytester
from _pytest.pytester import HookRecorder
from _pytest.pytester import CwdSnapshot, SysModulesSnapshot, SysPathsSnapshot
from _pytest.config import PytestPluginManager
from _pytest.main import EXIT_OK, EXIT_TESTSFAILED
@@ -131,19 +135,244 @@ def test_makepyfile_utf8(testdir):
assert u"mixed_encoding = u'São Paulo'".encode('utf-8') in p.read('rb')
def test_inline_run_clean_modules(testdir):
test_mod = testdir.makepyfile("def test_foo(): assert True")
result = testdir.inline_run(str(test_mod))
assert result.ret == EXIT_OK
# rewrite module, now test should fail if module was re-imported
test_mod.write("def test_foo(): assert False")
result2 = testdir.inline_run(str(test_mod))
assert result2.ret == EXIT_TESTSFAILED
class TestInlineRunModulesCleanup(object):
def test_inline_run_test_module_not_cleaned_up(self, testdir):
test_mod = testdir.makepyfile("def test_foo(): assert True")
result = testdir.inline_run(str(test_mod))
assert result.ret == EXIT_OK
# rewrite module, now test should fail if module was re-imported
test_mod.write("def test_foo(): assert False")
result2 = testdir.inline_run(str(test_mod))
assert result2.ret == EXIT_TESTSFAILED
def spy_factory(self):
class SysModulesSnapshotSpy(object):
instances = []
def __init__(self, preserve=None):
SysModulesSnapshotSpy.instances.append(self)
self._spy_restore_count = 0
self._spy_preserve = preserve
self.__snapshot = SysModulesSnapshot(preserve=preserve)
def restore(self):
self._spy_restore_count += 1
return self.__snapshot.restore()
return SysModulesSnapshotSpy
def test_inline_run_taking_and_restoring_a_sys_modules_snapshot(
self, testdir, monkeypatch):
spy_factory = self.spy_factory()
monkeypatch.setattr(pytester, "SysModulesSnapshot", spy_factory)
original = dict(sys.modules)
testdir.syspathinsert()
testdir.makepyfile(import1="# you son of a silly person")
testdir.makepyfile(import2="# my hovercraft is full of eels")
test_mod = testdir.makepyfile("""
import import1
def test_foo(): import import2""")
testdir.inline_run(str(test_mod))
assert len(spy_factory.instances) == 1
spy = spy_factory.instances[0]
assert spy._spy_restore_count == 1
assert sys.modules == original
assert all(sys.modules[x] is original[x] for x in sys.modules)
def test_inline_run_sys_modules_snapshot_restore_preserving_modules(
self, testdir, monkeypatch):
spy_factory = self.spy_factory()
monkeypatch.setattr(pytester, "SysModulesSnapshot", spy_factory)
test_mod = testdir.makepyfile("def test_foo(): pass")
testdir.inline_run(str(test_mod))
spy = spy_factory.instances[0]
assert not spy._spy_preserve("black_knight")
assert spy._spy_preserve("zope")
assert spy._spy_preserve("zope.interface")
assert spy._spy_preserve("zopelicious")
def test_external_test_module_imports_not_cleaned_up(self, testdir):
testdir.syspathinsert()
testdir.makepyfile(imported="data = 'you son of a silly person'")
import imported
test_mod = testdir.makepyfile("""
def test_foo():
import imported
imported.data = 42""")
testdir.inline_run(str(test_mod))
assert imported.data == 42
def test_assert_outcomes_after_pytest_erro(testdir):
def test_inline_run_clean_sys_paths(testdir):
def test_sys_path_change_cleanup(self, testdir):
test_path1 = testdir.tmpdir.join("boink1").strpath
test_path2 = testdir.tmpdir.join("boink2").strpath
test_path3 = testdir.tmpdir.join("boink3").strpath
sys.path.append(test_path1)
sys.meta_path.append(test_path1)
original_path = list(sys.path)
original_meta_path = list(sys.meta_path)
test_mod = testdir.makepyfile("""
import sys
sys.path.append({:test_path2})
sys.meta_path.append({:test_path2})
def test_foo():
sys.path.append({:test_path3})
sys.meta_path.append({:test_path3})""".format(locals()))
testdir.inline_run(str(test_mod))
assert sys.path == original_path
assert sys.meta_path == original_meta_path
def spy_factory(self):
class SysPathsSnapshotSpy(object):
instances = []
def __init__(self):
SysPathsSnapshotSpy.instances.append(self)
self._spy_restore_count = 0
self.__snapshot = SysPathsSnapshot()
def restore(self):
self._spy_restore_count += 1
return self.__snapshot.restore()
return SysPathsSnapshotSpy
def test_inline_run_taking_and_restoring_a_sys_paths_snapshot(
self, testdir, monkeypatch):
spy_factory = self.spy_factory()
monkeypatch.setattr(pytester, "SysPathsSnapshot", spy_factory)
test_mod = testdir.makepyfile("def test_foo(): pass")
testdir.inline_run(str(test_mod))
assert len(spy_factory.instances) == 1
spy = spy_factory.instances[0]
assert spy._spy_restore_count == 1
def test_assert_outcomes_after_pytest_error(testdir):
testdir.makepyfile("def test_foo(): assert True")
result = testdir.runpytest('--unexpected-argument')
with pytest.raises(ValueError, message="Pytest terminal report not found"):
result.assert_outcomes(passed=0)
def test_cwd_snapshot(tmpdir):
foo = tmpdir.ensure('foo', dir=1)
bar = tmpdir.ensure('bar', dir=1)
foo.chdir()
snapshot = CwdSnapshot()
bar.chdir()
assert py.path.local() == bar
snapshot.restore()
assert py.path.local() == foo
class TestSysModulesSnapshot(object):
key = 'my-test-module'
def test_remove_added(self):
original = dict(sys.modules)
assert self.key not in sys.modules
snapshot = SysModulesSnapshot()
sys.modules[self.key] = 'something'
assert self.key in sys.modules
snapshot.restore()
assert sys.modules == original
def test_add_removed(self, monkeypatch):
assert self.key not in sys.modules
monkeypatch.setitem(sys.modules, self.key, 'something')
assert self.key in sys.modules
original = dict(sys.modules)
snapshot = SysModulesSnapshot()
del sys.modules[self.key]
assert self.key not in sys.modules
snapshot.restore()
assert sys.modules == original
def test_restore_reloaded(self, monkeypatch):
assert self.key not in sys.modules
monkeypatch.setitem(sys.modules, self.key, 'something')
assert self.key in sys.modules
original = dict(sys.modules)
snapshot = SysModulesSnapshot()
sys.modules[self.key] = 'something else'
snapshot.restore()
assert sys.modules == original
def test_preserve_modules(self, monkeypatch):
key = [self.key + str(i) for i in range(3)]
assert not any(k in sys.modules for k in key)
for i, k in enumerate(key):
monkeypatch.setitem(sys.modules, k, 'something' + str(i))
original = dict(sys.modules)
def preserve(name):
return name in (key[0], key[1], 'some-other-key')
snapshot = SysModulesSnapshot(preserve=preserve)
sys.modules[key[0]] = original[key[0]] = 'something else0'
sys.modules[key[1]] = original[key[1]] = 'something else1'
sys.modules[key[2]] = 'something else2'
snapshot.restore()
assert sys.modules == original
def test_preserve_container(self, monkeypatch):
original = dict(sys.modules)
assert self.key not in original
replacement = dict(sys.modules)
replacement[self.key] = 'life of brian'
snapshot = SysModulesSnapshot()
monkeypatch.setattr(sys, 'modules', replacement)
snapshot.restore()
assert sys.modules is replacement
assert sys.modules == original
@pytest.mark.parametrize('path_type', ('path', 'meta_path'))
class TestSysPathsSnapshot(object):
other_path = {
'path': 'meta_path',
'meta_path': 'path'}
@staticmethod
def path(n):
return 'my-dirty-little-secret-' + str(n)
def test_restore(self, monkeypatch, path_type):
other_path_type = self.other_path[path_type]
for i in range(10):
assert self.path(i) not in getattr(sys, path_type)
sys_path = [self.path(i) for i in range(6)]
monkeypatch.setattr(sys, path_type, sys_path)
original = list(sys_path)
original_other = list(getattr(sys, other_path_type))
snapshot = SysPathsSnapshot()
transformation = {
'source': (0, 1, 2, 3, 4, 5),
'target': ( 6, 2, 9, 7, 5, 8)} # noqa: E201
assert sys_path == [self.path(x) for x in transformation['source']]
sys_path[1] = self.path(6)
sys_path[3] = self.path(7)
sys_path.append(self.path(8))
del sys_path[4]
sys_path[3:3] = [self.path(9)]
del sys_path[0]
assert sys_path == [self.path(x) for x in transformation['target']]
snapshot.restore()
assert getattr(sys, path_type) is sys_path
assert getattr(sys, path_type) == original
assert getattr(sys, other_path_type) == original_other
def test_preserve_container(self, monkeypatch, path_type):
other_path_type = self.other_path[path_type]
original_data = list(getattr(sys, path_type))
original_other = getattr(sys, other_path_type)
original_other_data = list(original_other)
new = []
snapshot = SysPathsSnapshot()
monkeypatch.setattr(sys, path_type, new)
snapshot.restore()
assert getattr(sys, path_type) is new
assert getattr(sys, path_type) == original_data
assert getattr(sys, other_path_type) is original_other
assert getattr(sys, other_path_type) == original_other_data

View File

@@ -1,7 +1,6 @@
from __future__ import absolute_import, division, print_function
import warnings
import re
import py
import pytest
from _pytest.recwarn import WarningsRecorder
@@ -24,9 +23,9 @@ class TestWarningsRecorderChecker(object):
rec = WarningsRecorder()
with rec:
assert not rec.list
py.std.warnings.warn_explicit("hello", UserWarning, "xyz", 13)
warnings.warn_explicit("hello", UserWarning, "xyz", 13)
assert len(rec.list) == 1
py.std.warnings.warn(DeprecationWarning("hello"))
warnings.warn(DeprecationWarning("hello"))
assert len(rec.list) == 2
warn = rec.pop()
assert str(warn.message) == "hello"
@@ -64,14 +63,14 @@ class TestDeprecatedCall(object):
def dep(self, i, j=None):
if i == 0:
py.std.warnings.warn("is deprecated", DeprecationWarning,
stacklevel=1)
warnings.warn("is deprecated", DeprecationWarning,
stacklevel=1)
return 42
def dep_explicit(self, i):
if i == 0:
py.std.warnings.warn_explicit("dep_explicit", category=DeprecationWarning,
filename="hello", lineno=3)
warnings.warn_explicit("dep_explicit", category=DeprecationWarning,
filename="hello", lineno=3)
def test_deprecated_call_raises(self):
with pytest.raises(AssertionError) as excinfo:
@@ -86,16 +85,16 @@ class TestDeprecatedCall(object):
assert ret == 42
def test_deprecated_call_preserves(self):
onceregistry = py.std.warnings.onceregistry.copy()
filters = py.std.warnings.filters[:]
warn = py.std.warnings.warn
warn_explicit = py.std.warnings.warn_explicit
onceregistry = warnings.onceregistry.copy()
filters = warnings.filters[:]
warn = warnings.warn
warn_explicit = warnings.warn_explicit
self.test_deprecated_call_raises()
self.test_deprecated_call()
assert onceregistry == py.std.warnings.onceregistry
assert filters == py.std.warnings.filters
assert warn is py.std.warnings.warn
assert warn_explicit is py.std.warnings.warn_explicit
assert onceregistry == warnings.onceregistry
assert filters == warnings.filters
assert warn is warnings.warn
assert warn_explicit is warnings.warn_explicit
def test_deprecated_explicit_call_raises(self):
with pytest.raises(AssertionError):

View File

@@ -4,7 +4,7 @@ import os
import _pytest._code
import py
import pytest
from _pytest.main import Node, Item, FSCollector
from _pytest.nodes import Node, Item, FSCollector
from _pytest.resultlog import generic_path, ResultLog, \
pytest_configure, pytest_unconfigure

View File

@@ -2,10 +2,12 @@
from __future__ import absolute_import, division, print_function
import _pytest._code
import inspect
import os
import py
import pytest
import sys
import types
from _pytest import runner, main, outcomes
@@ -32,7 +34,7 @@ class TestSetupState(object):
def setup_module(mod):
raise ValueError(42)
def test_func(): pass
""") # noqa
""")
ss = runner.SetupState()
pytest.raises(ValueError, lambda: ss.prepare(item))
pytest.raises(ValueError, lambda: ss.prepare(item))
@@ -202,6 +204,18 @@ class BaseFunctionalTests(object):
""")
assert rec.ret == 1
def test_logstart_logfinish_hooks(self, testdir):
rec = testdir.inline_runsource("""
import pytest
def test_func():
pass
""")
reps = rec.getcalls("pytest_runtest_logstart pytest_runtest_logfinish")
assert [x._name for x in reps] == ['pytest_runtest_logstart', 'pytest_runtest_logfinish']
for rep in reps:
assert rep.nodeid == 'test_logstart_logfinish_hooks.py::test_func'
assert rep.location == ('test_logstart_logfinish_hooks.py', 1, 'test_func')
def test_exact_teardown_issue90(self, testdir):
rec = testdir.inline_runsource("""
import pytest
@@ -392,10 +406,10 @@ reporttypes = [
@pytest.mark.parametrize('reporttype', reporttypes, ids=[x.__name__ for x in reporttypes])
def test_report_extra_parameters(reporttype):
if hasattr(py.std.inspect, 'signature'):
args = list(py.std.inspect.signature(reporttype.__init__).parameters.keys())[1:]
if hasattr(inspect, 'signature'):
args = list(inspect.signature(reporttype.__init__).parameters.keys())[1:]
else:
args = py.std.inspect.getargspec(reporttype.__init__)[0][1:]
args = inspect.getargspec(reporttype.__init__)[0][1:]
basekw = dict.fromkeys(args, [])
report = reporttype(newthing=1, **basekw)
assert report.newthing == 1
@@ -564,10 +578,10 @@ def test_importorskip(monkeypatch):
importorskip("asdlkj")
try:
sys = importorskip("sys") # noqa
assert sys == py.std.sys
sysmod = importorskip("sys")
assert sysmod is sys
# path = pytest.importorskip("os.path")
# assert path == py.std.os.path
# assert path == os.path
excinfo = pytest.raises(pytest.skip.Exception, f)
path = py.path.local(excinfo.getrepr().reprcrash.path)
# check that importorskip reports the actual call
@@ -575,7 +589,7 @@ def test_importorskip(monkeypatch):
assert path.purebasename == "test_runner"
pytest.raises(SyntaxError, "pytest.importorskip('x y z')")
pytest.raises(SyntaxError, "pytest.importorskip('x=y')")
mod = py.std.types.ModuleType("hello123")
mod = types.ModuleType("hello123")
mod.__version__ = "1.3"
monkeypatch.setitem(sys.modules, "hello123", mod)
pytest.raises(pytest.skip.Exception, """
@@ -595,7 +609,7 @@ def test_importorskip_imports_last_module_part():
def test_importorskip_dev_module(monkeypatch):
try:
mod = py.std.types.ModuleType("mockmodule")
mod = types.ModuleType("mockmodule")
mod.__version__ = '0.13.0.dev-43290'
monkeypatch.setitem(sys.modules, 'mockmodule', mod)
mod2 = pytest.importorskip('mockmodule', minversion='0.12.0')

View File

@@ -589,7 +589,7 @@ class TestSkipif(object):
@pytest.mark.skipif("hasattr(os, 'sep')")
def test_func():
pass
""") # noqa
""")
x = pytest.raises(pytest.skip.Exception, lambda:
pytest_runtest_setup(item))
assert x.value.msg == "condition: hasattr(os, 'sep')"

View File

@@ -326,7 +326,7 @@ def test_repr_python_version(monkeypatch):
try:
monkeypatch.setattr(sys, 'version_info', (2, 5, 1, 'final', 0))
assert repr_pythonversion() == "2.5.1-final-0"
py.std.sys.version_info = x = (2, 3)
sys.version_info = x = (2, 3)
assert repr_pythonversion() == str(x)
finally:
monkeypatch.undo() # do this early as pytest can get confused
@@ -475,11 +475,11 @@ class TestTerminalFunctional(object):
pass
""")
result = testdir.runpytest()
verinfo = ".".join(map(str, py.std.sys.version_info[:3]))
verinfo = ".".join(map(str, sys.version_info[:3]))
result.stdout.fnmatch_lines([
"*===== test session starts ====*",
"platform %s -- Python %s*pytest-%s*py-%s*pluggy-%s" % (
py.std.sys.platform, verinfo,
sys.platform, verinfo,
pytest.__version__, py.__version__, pluggy.__version__),
"*test_header_trailer_info.py .*",
"=* 1 passed*in *.[0-9][0-9] seconds *=",
@@ -966,10 +966,10 @@ def test_no_trailing_whitespace_after_inifile_word(testdir):
assert 'inifile: tox.ini\n' in result.stdout.str()
class TestProgress:
class TestProgress(object):
@pytest.fixture
def many_tests_file(self, testdir):
def many_tests_files(self, testdir):
testdir.makepyfile(
test_bar="""
import pytest
@@ -1006,7 +1006,7 @@ class TestProgress:
'=* 2 passed in *=',
])
def test_normal(self, many_tests_file, testdir):
def test_normal(self, many_tests_files, testdir):
output = testdir.runpytest()
output.stdout.re_match_lines([
r'test_bar.py \.{10} \s+ \[ 50%\]',
@@ -1014,7 +1014,7 @@ class TestProgress:
r'test_foobar.py \.{5} \s+ \[100%\]',
])
def test_verbose(self, many_tests_file, testdir):
def test_verbose(self, many_tests_files, testdir):
output = testdir.runpytest('-v')
output.stdout.re_match_lines([
r'test_bar.py::test_bar\[0\] PASSED \s+ \[ 5%\]',
@@ -1022,14 +1022,14 @@ class TestProgress:
r'test_foobar.py::test_foobar\[4\] PASSED \s+ \[100%\]',
])
def test_xdist_normal(self, many_tests_file, testdir):
def test_xdist_normal(self, many_tests_files, testdir):
pytest.importorskip('xdist')
output = testdir.runpytest('-n2')
output.stdout.re_match_lines([
r'\.{20} \s+ \[100%\]',
])
def test_xdist_verbose(self, many_tests_file, testdir):
def test_xdist_verbose(self, many_tests_files, testdir):
pytest.importorskip('xdist')
output = testdir.runpytest('-n2', '-v')
output.stdout.re_match_lines_random([
@@ -1037,3 +1037,86 @@ class TestProgress:
r'\[gw\d\] \[\s*\d+%\] PASSED test_foo.py::test_foo\[1\]',
r'\[gw\d\] \[\s*\d+%\] PASSED test_foobar.py::test_foobar\[1\]',
])
def test_capture_no(self, many_tests_files, testdir):
output = testdir.runpytest('-s')
output.stdout.re_match_lines([
r'test_bar.py \.{10}',
r'test_foo.py \.{5}',
r'test_foobar.py \.{5}',
])
class TestProgressWithTeardown(object):
"""Ensure we show the correct percentages for tests that fail during teardown (#3088)"""
@pytest.fixture
def contest_with_teardown_fixture(self, testdir):
testdir.makeconftest('''
import pytest
@pytest.fixture
def fail_teardown():
yield
assert False
''')
@pytest.fixture
def many_files(self, testdir, contest_with_teardown_fixture):
testdir.makepyfile(
test_bar='''
import pytest
@pytest.mark.parametrize('i', range(5))
def test_bar(fail_teardown, i):
pass
''',
test_foo='''
import pytest
@pytest.mark.parametrize('i', range(15))
def test_foo(fail_teardown, i):
pass
''',
)
def test_teardown_simple(self, testdir, contest_with_teardown_fixture):
testdir.makepyfile('''
def test_foo(fail_teardown):
pass
''')
output = testdir.runpytest()
output.stdout.re_match_lines([
r'test_teardown_simple.py \.E\s+\[100%\]',
])
def test_teardown_with_test_also_failing(self, testdir, contest_with_teardown_fixture):
testdir.makepyfile('''
def test_foo(fail_teardown):
assert False
''')
output = testdir.runpytest()
output.stdout.re_match_lines([
r'test_teardown_with_test_also_failing.py FE\s+\[100%\]',
])
def test_teardown_many(self, testdir, many_files):
output = testdir.runpytest()
output.stdout.re_match_lines([
r'test_bar.py (\.E){5}\s+\[ 25%\]',
r'test_foo.py (\.E){15}\s+\[100%\]',
])
def test_teardown_many_verbose(self, testdir, many_files):
output = testdir.runpytest('-v')
output.stdout.re_match_lines([
r'test_bar.py::test_bar\[0\] PASSED\s+\[ 5%\]',
r'test_bar.py::test_bar\[0\] ERROR\s+\[ 5%\]',
r'test_bar.py::test_bar\[4\] PASSED\s+\[ 25%\]',
r'test_bar.py::test_bar\[4\] ERROR\s+\[ 25%\]',
])
def test_xdist_normal(self, many_files, testdir):
pytest.importorskip('xdist')
output = testdir.runpytest('-n2')
output.stdout.re_match_lines([
r'[\.E]{40} \s+ \[100%\]',
])

View File

@@ -129,6 +129,7 @@ basepython = python
changedir = doc/en
deps =
sphinx
attrs
PyYAML
commands =
@@ -138,7 +139,7 @@ commands =
basepython = python
usedevelop = True
skipsdist = True
# ensure the given pyargs cant mean anytrhing else
# ensure the given pyargs can't mean anything else
changedir = doc/
deps =
PyYAML
@@ -215,6 +216,9 @@ filterwarnings =
ignore:.*type argument to addoption.*:DeprecationWarning
# produced by python >=3.5 on execnet (pytest-xdist)
ignore:.*inspect.getargspec.*deprecated, use inspect.signature.*:DeprecationWarning
# ignore warning about package resolution using __spec__ or __package__
# should be a temporary solution, see #3061 for discussion
ignore:.*can't resolve package from __spec__ or __package__.*:ImportWarning
[flake8]
max-line-length = 120