Skip to content

Commit 95fde9c

Browse files
authored
Merge pull request #3935 from Zac-HD/faster-coverage
Parallel coverage tests
2 parents 58a84a4 + 17296f0 commit 95fde9c

File tree

23 files changed

+125
-73
lines changed

23 files changed

+125
-73
lines changed

.github/workflows/main.yml

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,9 @@ jobs:
7373
- check-pandas13
7474
- check-pandas12
7575
- check-pandas11
76+
# - check-crosshair-cover
77+
# - check-crosshair-nocover
78+
# - check-crosshair-niche
7679
- check-py38-oldestnumpy
7780
fail-fast: false
7881
steps:
@@ -117,41 +120,43 @@ jobs:
117120
path: |
118121
hypothesis-python/.coverage*
119122
!hypothesis-python/.coveragerc
120-
hypothesis-python/branch-check
123+
hypothesis-python/branch-check*
121124
122125
test-win:
123126
runs-on: windows-latest
124127
strategy:
125128
matrix:
126-
include:
127-
- python-version: "3.9"
128-
- python-version: "3.10"
129-
- python-version: "3.11"
130-
- python-version: "3.11"
131-
python-architecture: "x86"
129+
python:
130+
- version: "3.9"
131+
- version: "3.11"
132+
- version: "3.11"
133+
architecture: "x86"
134+
whichtests:
135+
- nocover
136+
- cover+rest
132137
fail-fast: false
133138
steps:
134139
- uses: actions/checkout@v3
135140
with:
136141
fetch-depth: 0
137-
- name: Set up Python ${{ matrix.python-version }} ${{ matrix.python-architecture }}
142+
- name: Set up Python ${{ matrix.python.version }} ${{ matrix.python.architecture }}
138143
uses: actions/setup-python@v4
139144
with:
140-
python-version: ${{ matrix.python-version }}
141-
architecture: ${{ matrix.python-architecture }}
145+
python-version: ${{ matrix.python.version }}
146+
architecture: ${{ matrix.python.architecture }}
142147
- name: Restore cache
143148
uses: actions/cache@v3
144149
with:
145150
path: |
146151
~\appdata\local\pip\cache
147152
vendor\bundle
148153
.tox
149-
key: deps-${{ runner.os }}-${{ matrix.python-architecture }}-${{ hashFiles('requirements/*.txt') }}-${{ matrix.python-version }}
154+
key: deps-${{ runner.os }}-${{ matrix.python.architecture }}-${{ hashFiles('requirements/*.txt') }}-${{ matrix.python.version }}
150155
restore-keys: |
151-
deps-${{ runner.os }}-${{ matrix.python-architecture }}-${{ hashFiles('requirements/*.txt') }}
152-
deps-${{ runner.os }}-${{ matrix.python-architecture }}
156+
deps-${{ runner.os }}-${{ matrix.python.architecture }}-${{ hashFiles('requirements/*.txt') }}
157+
deps-${{ runner.os }}-${{ matrix.python.architecture }}
153158
- name: Use old pandas on win32
154-
if: matrix.python-architecture
159+
if: matrix.python.architecture
155160
# See https://github.com/pandas-dev/pandas/issues/54979
156161
run: |
157162
(Get-Content .\requirements\coverage.txt) -replace 'pandas==[0-9.]+', 'pandas==2.0.3' | Out-File .\requirements\coverage.txt
@@ -162,7 +167,7 @@ jobs:
162167
pip install -r requirements/coverage.txt
163168
pip install hypothesis-python/[all]
164169
- name: Run tests
165-
run: python -m pytest --numprocesses auto hypothesis-python/tests/ --ignore=hypothesis-python/tests/quality/ --ignore=hypothesis-python/tests/ghostwriter/ --ignore=hypothesis-python/tests/patching/
170+
run: python -m pytest --numprocesses auto ${{ matrix.whichtests == 'nocover' && 'hypothesis-python/tests/nocover' || 'hypothesis-python/tests/ --ignore=hypothesis-python/tests/nocover/ --ignore=hypothesis-python/tests/quality/ --ignore=hypothesis-python/tests/ghostwriter/ --ignore=hypothesis-python/tests/patching/' }}
166171

167172
test-osx:
168173
runs-on: macos-latest

.gitignore

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@
1111
# generic build components
1212

1313
.runtimes
14-
/hypothesis-python/branch-check
14+
/hypothesis-python/branch-check*
1515
/pythonpython3.*
1616
/pythonpypy3.*
1717
.pyodide-xbuildenv

build.sh

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,8 @@ if [ -n "${GITHUB_ACTIONS-}" ] || [ -n "${CODESPACES-}" ] ; then
2525
else
2626
# Otherwise, we install it from scratch
2727
# NOTE: tooling keeps this version in sync with ci_version in tooling
28-
"$SCRIPTS/ensure-python.sh" 3.10.13
29-
PYTHON=$(pythonloc 3.10.13)/bin/python
28+
"$SCRIPTS/ensure-python.sh" 3.10.14
29+
PYTHON=$(pythonloc 3.10.14)/bin/python
3030
fi
3131

3232
TOOL_REQUIREMENTS="$ROOT/requirements/tools.txt"

hypothesis-python/RELEASE.rst

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
RELEASE_TYPE: patch
2+
3+
This patch includes the :obj:`~hypothesis.settings.backend` setting in the
4+
``how_generated`` field of our :doc:`observability output <observability>`.

hypothesis-python/scripts/validate_branch_check.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,12 @@
1111
import json
1212
import sys
1313
from collections import defaultdict
14+
from pathlib import Path
1415

1516
if __name__ == "__main__":
16-
with open("branch-check", encoding="utf-8") as i:
17-
data = [json.loads(l) for l in i]
17+
data = []
18+
for p in Path.cwd().glob("branch-check*"):
19+
data.extend(json.loads(l) for l in p.read_text("utf-8").splitlines())
1820

1921
checks = defaultdict(set)
2022

hypothesis-python/setup.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def local_file(name):
6060
"pytest": ["pytest>=4.6"],
6161
"dpcontracts": ["dpcontracts>=0.4"],
6262
"redis": ["redis>=3.0.0"],
63-
"crosshair": ["hypothesis-crosshair>=0.0.2", "crosshair-tool>=0.0.51"],
63+
"crosshair": ["hypothesis-crosshair>=0.0.2", "crosshair-tool>=0.0.53"],
6464
# zoneinfo is an odd one: every dependency is conditional, because they're
6565
# only necessary on old versions of Python or Windows systems or emscripten.
6666
"zoneinfo": [

hypothesis-python/src/hypothesis/core.py

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -786,7 +786,6 @@ def __init__(self, stuff, test, settings, random, wrapped_test):
786786
self.explain_traces = defaultdict(set)
787787
self._start_timestamp = time.time()
788788
self._string_repr = ""
789-
self._jsonable_arguments = {}
790789
self._timing_features = {}
791790

792791
@property
@@ -913,7 +912,7 @@ def run(data):
913912
),
914913
)
915914
self._string_repr = printer.getvalue()
916-
self._jsonable_arguments = {
915+
data._observability_arguments = {
917916
**dict(enumerate(map(to_jsonable, args))),
918917
**{k: to_jsonable(v) for k, v in kwargs.items()},
919918
}
@@ -1085,19 +1084,23 @@ def _execute_once_for_engine(self, data: ConjectureData) -> None:
10851084
# Conditional here so we can save some time constructing the payload; in
10861085
# other cases (without coverage) it's cheap enough to do that regardless.
10871086
if TESTCASE_CALLBACKS:
1088-
if self.failed_normally or self.failed_due_to_deadline:
1089-
phase = "shrink"
1090-
elif runner := getattr(self, "_runner", None):
1087+
if runner := getattr(self, "_runner", None):
10911088
phase = runner._current_phase
1089+
elif self.failed_normally or self.failed_due_to_deadline:
1090+
phase = "shrink"
10921091
else: # pragma: no cover # in case of messing with internals
10931092
phase = "unknown"
1093+
backend_desc = f", using backend={self.settings.backend!r}" * (
1094+
self.settings.backend != "hypothesis"
1095+
and not getattr(runner, "_switch_to_hypothesis_provider", False)
1096+
)
10941097
tc = make_testcase(
10951098
start_timestamp=self._start_timestamp,
10961099
test_name_or_nodeid=self.test_identifier,
10971100
data=data,
1098-
how_generated=f"generated during {phase} phase",
1101+
how_generated=f"during {phase} phase{backend_desc}",
10991102
string_repr=self._string_repr,
1100-
arguments={**self._jsonable_arguments, **data._observability_args},
1103+
arguments=data._observability_args,
11011104
timing=self._timing_features,
11021105
coverage=tractable_coverage_report(trace) or None,
11031106
phase=phase,
@@ -1217,7 +1220,7 @@ def run_engine(self):
12171220
"status": "passed" if sys.exc_info()[0] else "failed",
12181221
"status_reason": str(origin or "unexpected/flaky pass"),
12191222
"representation": self._string_repr,
1220-
"arguments": self._jsonable_arguments,
1223+
"arguments": ran_example._observability_args,
12211224
"how_generated": "minimal failing example",
12221225
"features": {
12231226
**{

hypothesis-python/src/hypothesis/extra/_patching.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -121,7 +121,7 @@ def __call_node_to_example_dec(self, node, via):
121121
cst.Module([]).code_for_node(via),
122122
mode=black.FileMode(line_length=self.line_length),
123123
)
124-
except ImportError:
124+
except (ImportError, AttributeError):
125125
return None # See https://github.com/psf/black/pull/4224
126126
via = cst.parse_expression(pretty.strip())
127127
return cst.Decorator(via)

hypothesis-python/src/hypothesis/extra/array_api.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -424,12 +424,12 @@ def do_draw(self, data):
424424
while elements.more():
425425
i = data.draw_integer(0, self.array_size - 1)
426426
if i in assigned:
427-
elements.reject()
427+
elements.reject("chose an array index we've already used")
428428
continue
429429
val = data.draw(self.elements_strategy)
430430
if self.unique:
431431
if val in seen:
432-
elements.reject()
432+
elements.reject("chose an element we've already used")
433433
continue
434434
else:
435435
seen.add(val)

hypothesis-python/src/hypothesis/internal/conjecture/data.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2273,13 +2273,13 @@ def _pop_ir_tree_node(self, ir_type: IRTypeName, kwargs: IRKWargsType) -> IRNode
22732273
# (in fact, it is possible that giving up early here results in more time
22742274
# for useful shrinks to run).
22752275
if node.ir_type != ir_type:
2276-
self.mark_invalid()
2276+
self.mark_invalid(f"(internal) want a {ir_type} but have a {node.ir_type}")
22772277

22782278
# if a node has different kwargs (and so is misaligned), but has a value
22792279
# that is allowed by the expected kwargs, then we can coerce this node
22802280
# into an aligned one by using its value. It's unclear how useful this is.
22812281
if not ir_value_permitted(node.value, node.ir_type, kwargs):
2282-
self.mark_invalid()
2282+
self.mark_invalid(f"(internal) got a {ir_type} but outside the valid range")
22832283

22842284
return node
22852285

@@ -2348,7 +2348,7 @@ def draw(
23482348
strategy.validate()
23492349

23502350
if strategy.is_empty:
2351-
self.mark_invalid("strategy is empty")
2351+
self.mark_invalid(f"empty strategy {self!r}")
23522352

23532353
if self.depth >= MAX_DEPTH:
23542354
self.mark_invalid("max depth exceeded")

hypothesis-python/src/hypothesis/internal/conjecture/engine.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -809,6 +809,15 @@ def generate_new_examples(self):
809809

810810
self.test_function(data)
811811

812+
if (
813+
data.status == Status.OVERRUN
814+
and max_length < BUFFER_SIZE
815+
and "invalid because" not in data.events
816+
):
817+
data.events["invalid because"] = (
818+
"reduced max size for early examples (avoids flaky health checks)"
819+
)
820+
812821
self.generate_mutations_from(data)
813822

814823
# Although the optimisations are logically a distinct phase, we

hypothesis-python/src/hypothesis/internal/coverage.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def record_branch(name, value):
6161
if key in written:
6262
return
6363
written.add(key)
64-
with open("branch-check", mode="a", encoding="utf-8") as log:
64+
with open(f"branch-check-{os.getpid()}", mode="a", encoding="utf-8") as log:
6565
log.write(json.dumps({"name": name, "value": value}) + "\n")
6666

6767
description_stack = []

hypothesis-python/src/hypothesis/internal/observability.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,7 @@ def make_testcase(
3636
start_timestamp: float,
3737
test_name_or_nodeid: str,
3838
data: ConjectureData,
39-
how_generated: str = "unknown",
39+
how_generated: str,
4040
string_repr: str = "<unknown>",
4141
arguments: Optional[dict] = None,
4242
timing: Dict[str, float],

hypothesis-python/src/hypothesis/stateful.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -478,7 +478,7 @@ def do_draw(self, data):
478478
machine = data.draw(self_strategy)
479479
bundle = machine.bundle(self.name)
480480
if not bundle:
481-
data.mark_invalid()
481+
data.mark_invalid(f"Cannot draw from empty bundle {self.name!r}")
482482
# Shrink towards the right rather than the left. This makes it easier
483483
# to delete data generated earlier, as when the error is towards the
484484
# end there can be a lot of hard to remove padding.

hypothesis-python/src/hypothesis/strategies/_internal/datetime.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ def do_draw(self, data):
155155

156156
# If we happened to end up with a disallowed imaginary time, reject it.
157157
if (not self.allow_imaginary) and datetime_does_not_exist(result):
158-
data.mark_invalid("nonexistent datetime")
158+
data.mark_invalid(f"{result} does not exist (usually a DST transition)")
159159
return result
160160

161161
def draw_naive_datetime_and_combine(self, data, tz):

hypothesis-python/tests/common/setup.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ def run():
6161

6262
settings.register_profile("debug", settings(verbosity=Verbosity.debug))
6363

64-
settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default"))
65-
6664
for backend in set(AVAILABLE_PROVIDERS) - {"hypothesis"}:
6765
settings.register_profile(backend, backend=backend) # e.g. "crosshair"
66+
67+
settings.load_profile(os.getenv("HYPOTHESIS_PROFILE", "default"))

hypothesis-python/tox.ini

Lines changed: 21 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -103,6 +103,21 @@ commands =
103103
python -bb -X dev -m pytest tests/pandas -n auto
104104
# Adding a new pandas? See comment above!
105105

106+
[testenv:crosshair-{cover,nocover,niche,custom}]
107+
deps =
108+
-r../requirements/test.txt
109+
-e .[crosshair]
110+
allowlist_externals =
111+
bash
112+
setenv=
113+
HYPOTHESIS_PROFILE=crosshair
114+
commands =
115+
# invoke with `./build.sh check-crosshair-cover -- -x -Wignore`
116+
cover: python -bb -X dev -m pytest -n auto tests/cover/ tests/pytest/ {posargs}
117+
nocover: python -bb -X dev -m pytest -n auto tests/nocover/ {posargs}
118+
niche: bash scripts/other-tests.sh
119+
custom: python -bb -X dev -m pytest {posargs}
120+
106121
[testenv:django32]
107122
commands =
108123
pip install .[pytz]
@@ -166,20 +181,16 @@ setenv=
166181
PYTHONWARNDEFAULTENCODING=1
167182
HYPOTHESIS_INTERNAL_COVERAGE=true
168183
commands_pre =
169-
rm -f branch-check
184+
rm -f branch-check*
170185
pip install .[zoneinfo]
171-
python -m coverage --version
172-
python -m coverage debug sys
173-
# Explicitly erase any old .coverage file so the report never sees it.
174-
python -m coverage erase
175186
# Produce a coverage report even if the test suite fails.
176187
# (The tox task will still count as failed.)
177188
ignore_errors = true
178189
commands =
179-
python -bb -X dev -m coverage run --rcfile=.coveragerc --source=hypothesis -m pytest -n0 --ff {posargs} \
190+
python -bb -X dev -m pytest -n auto --ff {posargs} \
191+
--cov=hypothesis.internal.conjecture --cov-config=.coveragerc \
180192
tests/cover tests/conjecture tests/datetime tests/numpy tests/pandas tests/lark \
181193
tests/redis tests/dpcontracts tests/codemods tests/typing_extensions tests/patching tests/test_annotated_types.py
182-
python -m coverage report
183194
python scripts/validate_branch_check.py
184195

185196

@@ -189,12 +200,10 @@ deps =
189200
setenv=
190201
PYTHONWARNDEFAULTENCODING=1
191202
HYPOTHESIS_INTERNAL_COVERAGE=true
192-
commands_pre =
193-
python -m coverage erase
194-
ignore_errors = true
195203
commands =
196-
python -bb -X dev -m coverage run --rcfile=.coveragerc --source=hypothesis.internal.conjecture -m pytest -n0 --strict-markers tests/conjecture
197-
python -m coverage report
204+
python -bb -X dev \
205+
-m pytest -n auto tests/conjecture/ \
206+
--cov=hypothesis.internal.conjecture --cov-config=.coveragerc
198207

199208

200209
[testenv:examples3]

pytest.ini

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,3 +22,5 @@ filterwarnings =
2222
default:`np\.bool` is a deprecated alias for the builtin `bool`:DeprecationWarning
2323
default:`np\.complex` is a deprecated alias for the builtin `complex`:DeprecationWarning
2424
default:`np\.object` is a deprecated alias for the builtin `object`:DeprecationWarning
25+
# pytest-cov can't see into subprocesses; we'll see <100% covered if this is an issue
26+
ignore:Module hypothesis.* was previously imported, but not measured

requirements/coverage.in

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
annotated-types
22
black
33
click
4-
coverage
54
dpcontracts
65
fakeredis
76
lark
@@ -13,3 +12,5 @@ python-dateutil
1312
pytz
1413
typing-extensions
1514
-r test.in
15+
# Need the unreleased compatibility fix for pytest-xdist rsyncdirs deprecation
16+
git+https://github.com/pytest-dev/pytest-cov.git@9757222e2e044361e70125ebdd96e5eb87395983

0 commit comments

Comments
 (0)