Skip to content

Commit ff4152b

Browse files
authored
Merge pull request #3289 from Zac-HD/fix-junit-xml
Fix `junit-xml` support
2 parents ea10729 + 2e57d1e commit ff4152b

File tree

9 files changed

+223
-154
lines changed

9 files changed

+223
-154
lines changed

.github/workflows/main.yml

+2
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,8 @@ jobs:
4343
- lint-conjecture-rust
4444
- check-nose
4545
- check-pytest46
46+
- check-pytest54
47+
- check-pytest62
4648
- check-django40
4749
- check-django32
4850
- check-django22

AUTHORS.rst

+1
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ their individual contributions.
2323
* `Benjamin Palmer <https://github.com/benjpalmer>`_
2424
* `Bex Dunn <https://github.com/BexDunn>`_ ([email protected])
2525
* `Bill Tucker <https://github.com/imbilltucker>`_ ([email protected])
26+
* `Brandon Chinn <https://github.com/brandonchinn178>`_
2627
* `Bryant Eisenbach <https://github.com/fubuloubu>`_
2728
* `Buck Evan, copyright Google LLC <https://github.com/bukzor>`_
2829
* `Cameron McGill <https://www.github.com/Cameron-JM>`_

CONTRIBUTING.rst

+51-1
Original file line numberDiff line numberDiff line change
@@ -165,7 +165,7 @@ Some notable commands:
165165
``./build.sh check-coverage`` will verify 100% code coverage by running a
166166
curated subset of the test suite.
167167

168-
``./build.sh check-py36`` (etc.) will run most of the test suite against a
168+
``./build.sh check-py37`` (etc.) will run most of the test suite against a
169169
particular python version.
170170

171171
``./build.sh format`` will reformat your code according to the Hypothesis coding style. You should use this before each
@@ -180,3 +180,53 @@ Run ``./build.sh tasks`` for a list of all supported build task names.
180180
Note: The build requires a lot of different versions of python, so rather than have you install them yourself,
181181
the build system will install them itself in a local directory. This means that the first time you run a task you
182182
may have to wait a while as the build downloads and installs the right version of python for you.
183+
184+
~~~~~~~~~~~~~
185+
Running Tests
186+
~~~~~~~~~~~~~
187+
188+
The tasks described above will run all of the tests (e.g. ``check-py37``). But
189+
the ``tox`` task will give finer-grained control over the test runner. At a
190+
high level, the task takes the form:
191+
192+
.. code-block::
193+
194+
./build.sh tox py37-custom 3.7.13 [tox args] -- [pytest args]
195+
196+
Namely, first provide the tox environment (see ``tox.ini``), then the python
197+
version to test with, then any ``tox`` or ``pytest`` args as needed. For
198+
example, to run all of the tests in the file
199+
``tests/nocover/test_conjecture_engine.py`` with python 3.8:
200+
201+
.. code-block::
202+
203+
./build.sh tox py38-custom 3.8.13 -- tests/nocover/test_conjecture_engine.py
204+
205+
See the ``tox`` docs and ``pytest`` docs for more information:
206+
* https://docs.pytest.org/en/latest/how-to/usage.html
207+
* https://tox.wiki/en/latest/config.html#cli
208+
209+
^^^^^^^^^^^
210+
Test Layout
211+
^^^^^^^^^^^
212+
213+
See ``hypothesis-python/tests/README.rst``
214+
215+
^^^^^^^^^^^^^^^^
216+
Useful Arguments
217+
^^^^^^^^^^^^^^^^
218+
219+
Some useful arguments to pytest include:
220+
221+
* You can pass ``-n 0`` to turn off ``pytest-xdist``'s parallel test execution.
222+
Sometimes for running just a small number of tests its startup time is longer
223+
than the time it saves (this will vary from system to system), so this can
224+
be helpful if you find yourself waiting on test runners to start a lot.
225+
* You can use ``-k`` to select a subset of tests to run. This matches on substrings
226+
of the test names. For example ``-kfoo`` will only run tests that have "foo" as
227+
a substring of their name. You can also use composite expressions here.
228+
e.g. ``-k'foo and not bar'`` will run anything containing foo that doesn't
229+
also contain bar. `More information on how to select tests to run can be found
230+
in the pytest documentation <https://docs.pytest.org/en/latest/usage.html#specifying-tests-selecting-tests>`__.
231+
232+

guides/testing-hypothesis.rst

+2-116
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ a starting point, not the final goal.
3737
because when it was found and fixed, someone wrote a test to make sure it
3838
couldn't come back!
3939

40-
The ``tests/`` directory has some notes in the README file on where various
40+
The ``hypothesis-python/tests/`` directory has some notes in the README file on where various
4141
kinds of tests can be found or added. Go there for the practical stuff, or
4242
just ask one of the maintainers for help on a pull request!
4343

@@ -48,122 +48,8 @@ Further reading: How `SQLite is tested <https://sqlite.org/testing.html>`_,
4848
Dan Luu writes about `fuzz testing <https://danluu.com/testing/>`_ and
4949
`broken processes <https://danluu.com/wat/>`_, among other things.
5050

51-
---------------------------------------
52-
Setting up a virtualenv to run tests in
53-
---------------------------------------
54-
55-
If you want to run individual tests rather than relying on the make tasks
56-
(which you probably will), it's easiest to do this in a virtualenv.
57-
58-
The following will give you a working virtualenv for running tests in:
59-
60-
.. code-block:: bash
61-
62-
pip install virtualenv
63-
python -m virtualenv testing-venv
64-
65-
# On Windows: testing-venv\Scripts\activate
66-
source testing-venv/bin/activate
67-
68-
# Can also use pip install -e .[all] to get
69-
# all optional dependencies
70-
pip install -e .
71-
72-
# Test specific dependencies.
73-
pip install -r requirements/test.in
74-
75-
Now whenever you want to run tests you can just activate the virtualenv
76-
using ``source testing-venv/bin/activate`` or ``testing-venv\Scripts\activate``
77-
and all of the dependencies will be available to you and your local copy
78-
of Hypothesis will be on the path (so any edits will be picked up automatically
79-
and you don't need to reinstall it in the local virtualenv).
80-
8151
-------------
8252
Running Tests
8353
-------------
8454

85-
In order to run tests outside of the make/tox/etc set up, you'll need an
86-
environment where Hypothesis is on the path and all of the testing dependencies
87-
are installed.
88-
We recommend doing this inside a virtualenv as described in the previous section.
89-
90-
All testing is done using `pytest <https://docs.pytest.org/en/latest/>`_,
91-
with a couple of plugins installed. For advanced usage we recommend reading the
92-
pytest documentation, but this section will give you a primer in enough of the
93-
common commands and arguments to get started.
94-
95-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
96-
Selecting Which Files to Run
97-
~~~~~~~~~~~~~~~~~~~~~~~~~~~~
98-
99-
The following invocation runs all of the tests in the file
100-
`tests/cover/test_conjecture_engine.py`:
101-
102-
.. code-block::
103-
104-
python -m pytest tests/cover/test_conjecture_engine.py
105-
106-
If you want to run multiple files you can pass them all as arguments, and if
107-
you pass a directory then it will run all files in that directory.
108-
For example the following runs all the files in `test_conjecture_engine.py`
109-
and `test_slippage.py`
110-
111-
.. code-block::
112-
113-
python -m pytest tests/cover/test_conjecture_engine.py tests/cover/test_slippage.py
114-
115-
If you were running this in bash (if you're not sure: if you're not on Windows
116-
you probably are) you could also use the syntax:
117-
118-
.. code-block::
119-
120-
python -m pytest tests/cover/test_{conjecture_engine,slippage}.py
121-
122-
And the following would run all tests under `tests/cover`:
123-
124-
.. code-block::
125-
126-
python -m pytest tests/cover
127-
128-
129-
~~~~~~~~~~~
130-
Test Layout
131-
~~~~~~~~~~~
132-
133-
The top level structure of the tests in Hypothesis looks as follows:
134-
135-
* ``cover`` contains tests that we measure coverage for. This is intended to
136-
be a fairly minimal and fast set of tests that still gives pretty good
137-
confidence in the behaviour of the test suite. It is currently failing at
138-
both "minimal" and "fast", but we're trying to move it back in that
139-
direction. Try not to add tests to this unless they're actually to cover
140-
some specific target.
141-
* ``nocover`` is a general dumping ground for slower tests that aren't needed
142-
to achieve coverage.
143-
* ``quality`` is for expensive tests about the distribution or shrinking of
144-
examples. These will only be run on one Python version.
145-
* The remaining test directories are for testing specific extras modules and
146-
should have the same name.
147-
148-
As a rule of thumb when writing new tests, they should go in nocover unless
149-
they are for a specific extras module or to deliberately target a particular
150-
line for coverage. In the latter case, prefer fast unit tests over larger and
151-
slower integration tests (we are not currently very good at this).
152-
153-
154-
~~~~~~~~~~~~~~~~
155-
Useful Arguments
156-
~~~~~~~~~~~~~~~~
157-
158-
Some useful arguments to pytest include:
159-
160-
* You can pass ``-n 0`` to turn off ``pytest-xdist``'s parallel test execution.
161-
Sometimes for running just a small number of tests its startup time is longer
162-
than the time it saves (this will vary from system to system), so this can
163-
be helpful if you find yourself waiting on test runners to start a lot.
164-
* You can use ``-k`` to select a subset of tests to run. This matches on substrings
165-
of the test names. For example ``-kfoo`` will only run tests that have "foo" as
166-
a substring of their name. You can also use composite expressions here.
167-
e.g. ``-k'foo and not bar'`` will run anything containing foo that doesn't
168-
also contain bar. `More information on how to select tests to run can be found
169-
in the pytest documentation <https://docs.pytest.org/en/latest/usage.html#specifying-tests-selecting-tests>`__.
55+
Tests are run via ``build.sh``. See ``CONTRIBUTING.rst`` for more details.

hypothesis-python/RELEASE.rst

+16
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
RELEASE_TYPE: patch
2+
3+
The Hypothesis pytest plugin was not outputting valid xunit2 nodes when
4+
``--junit-xml`` was specified. This has been broken since Pytest 5.4, which
5+
changed the internal API for adding nodes to the junit report.
6+
7+
This also fixes the issue when using hypothesis with ``--junit-xml`` and
8+
``pytest-xdist`` where the junit xml report would not be xunit2 compatible.
9+
Now, when using with ``pytest-xdist``, the junit report will just omit the
10+
``<properties>`` node.
11+
12+
For more details, see `this pytest issue <https://github.com/pytest-dev/pytest/issues/1126#issuecomment-484581283>`__,
13+
`this pytest issue <https://github.com/pytest-dev/pytest/issues/7767#issuecomment-1082436256>`__,
14+
and :issue:`1935`
15+
16+
Thanks to Brandon Chinn for this bug fix!

hypothesis-python/src/_hypothesis_pytestplugin.py

+45-34
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,11 @@
2525

2626
import pytest
2727

28+
try:
29+
from _pytest.junitxml import xml_key
30+
except ImportError:
31+
xml_key = "_xml" # type: ignore
32+
2833
LOAD_PROFILE_OPTION = "--hypothesis-profile"
2934
VERBOSITY_OPTION = "--hypothesis-verbosity"
3035
PRINT_STATISTICS_OPTION = "--hypothesis-show-statistics"
@@ -53,6 +58,8 @@
5358
check to assure Hypothesis that you understand what you are doing.
5459
"""
5560

61+
STATS_KEY = "_hypothesis_stats"
62+
5663

5764
class StoringReporter:
5865
def __init__(self, config):
@@ -255,9 +262,7 @@ def pytest_runtest_call(item):
255262

256263
def note_statistics(stats):
257264
stats["nodeid"] = item.nodeid
258-
item.hypothesis_statistics = base64.b64encode(
259-
describe_statistics(stats).encode()
260-
).decode()
265+
item.hypothesis_statistics = describe_statistics(stats)
261266

262267
with collector.with_value(note_statistics):
263268
with with_reporter(store):
@@ -266,6 +271,16 @@ def note_statistics(stats):
266271
if store.results:
267272
item.hypothesis_report_information = list(store.results)
268273

274+
def _stash_get(config, key, default):
275+
if hasattr(config, "stash"):
276+
# pytest 7
277+
return config.stash.get(key, default)
278+
elif hasattr(config, "_store"):
279+
# pytest 5.4
280+
return config._store.get(key, default)
281+
else:
282+
return getattr(config, key, default)
283+
269284
@pytest.hookimpl(hookwrapper=True)
270285
def pytest_runtest_makereport(item, call):
271286
report = (yield).get_result()
@@ -274,46 +289,42 @@ def pytest_runtest_makereport(item, call):
274289
("Hypothesis", "\n".join(item.hypothesis_report_information))
275290
)
276291
if hasattr(item, "hypothesis_statistics") and report.when == "teardown":
292+
stats = item.hypothesis_statistics
293+
stats_base64 = base64.b64encode(stats.encode()).decode()
294+
277295
name = "hypothesis-statistics-" + item.nodeid
278-
try:
279-
item.config._xml.add_global_property(name, item.hypothesis_statistics)
280-
except AttributeError:
281-
# --junitxml not passed, or Pytest 4.5 (before add_global_property)
282-
# We'll fail xunit2 xml schema checks, upgrade pytest if you care.
283-
report.user_properties.append((name, item.hypothesis_statistics))
296+
297+
# Include hypothesis information to the junit XML report.
298+
#
299+
# Note that when `pytest-xdist` is enabled, `xml_key` is not present in the
300+
# stash, so we don't add anything to the junit XML report in that scenario.
301+
# https://github.com/pytest-dev/pytest/issues/7767#issuecomment-1082436256
302+
xml = _stash_get(item.config, xml_key, None)
303+
if xml:
304+
xml.add_global_property(name, stats_base64)
305+
306+
# If there's a terminal report, include our summary stats for each test
307+
terminalreporter = item.config.pluginmanager.getplugin("terminalreporter")
308+
if terminalreporter is not None:
309+
# ideally, we would store this on terminalreporter.config.stash, but
310+
# pytest-xdist doesn't copy that back to the controller
311+
report.__dict__[STATS_KEY] = stats
312+
284313
# If there's an HTML report, include our summary stats for each test
285-
stats = base64.b64decode(item.hypothesis_statistics.encode()).decode()
286314
pytest_html = item.config.pluginmanager.getplugin("html")
287315
if pytest_html is not None: # pragma: no cover
288316
report.extra = getattr(report, "extra", []) + [
289317
pytest_html.extras.text(stats, name="Hypothesis stats")
290318
]
291319

292320
def pytest_terminal_summary(terminalreporter):
293-
if not terminalreporter.config.getoption(PRINT_STATISTICS_OPTION):
294-
return
295-
terminalreporter.section("Hypothesis Statistics")
296-
297-
def report(properties):
298-
for name, value in properties:
299-
if name.startswith("hypothesis-statistics-"):
300-
if hasattr(value, "uniobj"):
301-
# Under old versions of pytest, `value` was a `py.xml.raw`
302-
# rather than a string, so we get the (unicode) string off it.
303-
value = value.uniobj
304-
line = base64.b64decode(value.encode()).decode() + "\n\n"
305-
terminalreporter.write_line(line)
306-
307-
try:
308-
global_properties = terminalreporter.config._xml.global_properties
309-
except AttributeError:
310-
# terminalreporter.stats is a dict, where the empty string appears to
311-
# always be the key for a list of _pytest.reports.TestReport objects
312-
for test_report in terminalreporter.stats.get("", []):
313-
if test_report.when == "teardown":
314-
report(test_report.user_properties)
315-
else:
316-
report(global_properties)
321+
if terminalreporter.config.getoption(PRINT_STATISTICS_OPTION):
322+
terminalreporter.section("Hypothesis Statistics")
323+
for reports in terminalreporter.stats.values():
324+
for report in reports:
325+
stats = report.__dict__.get(STATS_KEY)
326+
if stats:
327+
terminalreporter.write_line(stats + "\n\n")
317328

318329
def pytest_collection_modifyitems(items):
319330
if "hypothesis" not in sys.modules:

0 commit comments

Comments
 (0)