Skip to content

Commit 4a702a9

Browse files
authored
Merge pull request #4152 from HypothesisWorks/DRMacIver/ci-config
Default to CI-appropriate settings when running on CI
2 parents 8d88e01 + 13c3785 commit 4a702a9

File tree

8 files changed

+116
-33
lines changed

8 files changed

+116
-33
lines changed

hypothesis-python/RELEASE.rst

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
RELEASE_TYPE: minor
2+
3+
Hypothesis now detects if it is running on a CI server and provides better default settings for running on CI in this case.

hypothesis-python/docs/settings.rst

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -250,6 +250,14 @@ by your conftest you can load one with the command line option ``--hypothesis-pr
250250
$ pytest tests --hypothesis-profile <profile-name>
251251
252252
253+
Hypothesis comes with two built-in profiles, ``ci`` and ``default``.
254+
``ci`` is set up to have good defaults for running in a CI environment, so emphasizes determinism, while the
255+
``default`` settings are picked to be more likely to find bugs and to have a good workflow when used for local development.
256+
257+
Hypothesis will automatically detect certain common CI environments and use the CI profile automatically
258+
when running in them.
259+
In particular, if you wish to use the ``ci`` profile, setting the ``CI`` environment variable will do this.
260+
253261
.. _healthchecks:
254262

255263
-------------

hypothesis-python/src/hypothesis/_settings.py

Lines changed: 24 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -63,6 +63,7 @@ def __get__(self, obj, type=None):
6363
from hypothesis.database import ExampleDatabase
6464

6565
result = ExampleDatabase(not_set)
66+
assert result is not not_set
6667
return result
6768
except KeyError:
6869
raise AttributeError(self.name) from None
@@ -407,6 +408,8 @@ def _max_examples_validator(x):
407408
:ref:`separate settings profiles <settings_profiles>` - for example running
408409
quick deterministic tests on every commit, and a longer non-deterministic
409410
nightly testing run.
411+
412+
By default when running on CI, this will be set to True.
410413
""",
411414
)
412415

@@ -682,6 +685,8 @@ def _validate_deadline(x):
682685
variability in test run time).
683686
684687
Set this to ``None`` to disable this behaviour entirely.
688+
689+
By default when running on CI, this will be set to None.
685690
""",
686691
)
687692

@@ -694,13 +699,11 @@ def is_in_ci() -> bool:
694699

695700
settings._define_setting(
696701
"print_blob",
697-
default=is_in_ci(),
698-
show_default=False,
702+
default=False,
699703
options=(True, False),
700704
description="""
701705
If set to ``True``, Hypothesis will print code for failing examples that can be used with
702706
:func:`@reproduce_failure <hypothesis.reproduce_failure>` to reproduce the failing example.
703-
The default is ``True`` if the ``CI`` or ``TF_BUILD`` env vars are set, ``False`` otherwise.
704707
""",
705708
)
706709

@@ -750,6 +753,24 @@ def note_deprecation(
750753

751754
settings.register_profile("default", settings())
752755
settings.load_profile("default")
756+
757+
assert settings.default is not None
758+
759+
CI = settings(
760+
derandomize=True,
761+
deadline=None,
762+
database=None,
763+
print_blob=True,
764+
suppress_health_check=[HealthCheck.too_slow],
765+
)
766+
767+
settings.register_profile("ci", CI)
768+
769+
770+
# This is tested in a subprocess so the branch doesn't show up in coverage.
771+
if is_in_ci(): # pragma: no cover
772+
settings.load_profile("ci")
773+
753774
assert settings.default is not None
754775

755776

hypothesis-python/src/hypothesis/core.py

Lines changed: 16 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -1564,23 +1564,23 @@ def wrapped_test(*arguments, **kwargs):
15641564
"to ensure that each example is run in a separate "
15651565
"database transaction."
15661566
)
1567-
if settings.database is not None:
1568-
nonlocal prev_self
1569-
# Check selfy really is self (not e.g. a mock) before we health-check
1570-
cur_self = (
1571-
stuff.selfy
1572-
if getattr(type(stuff.selfy), test.__name__, None) is wrapped_test
1573-
else None
1567+
1568+
nonlocal prev_self
1569+
# Check selfy really is self (not e.g. a mock) before we health-check
1570+
cur_self = (
1571+
stuff.selfy
1572+
if getattr(type(stuff.selfy), test.__name__, None) is wrapped_test
1573+
else None
1574+
)
1575+
if prev_self is Unset:
1576+
prev_self = cur_self
1577+
elif cur_self is not prev_self:
1578+
msg = (
1579+
f"The method {test.__qualname__} was called from multiple "
1580+
"different executors. This may lead to flaky tests and "
1581+
"nonreproducible errors when replaying from database."
15741582
)
1575-
if prev_self is Unset:
1576-
prev_self = cur_self
1577-
elif cur_self is not prev_self:
1578-
msg = (
1579-
f"The method {test.__qualname__} was called from multiple "
1580-
"different executors. This may lead to flaky tests and "
1581-
"nonreproducible errors when replaying from database."
1582-
)
1583-
fail_health_check(settings, msg, HealthCheck.differing_executors)
1583+
fail_health_check(settings, msg, HealthCheck.differing_executors)
15841584

15851585
state = StateForActualGivenExecution(
15861586
stuff, test, settings, random, wrapped_test
@@ -1675,7 +1675,6 @@ def wrapped_test(*arguments, **kwargs):
16751675
# The exception caught here should either be an actual test
16761676
# failure (or BaseExceptionGroup), or some kind of fatal error
16771677
# that caused the engine to stop.
1678-
16791678
generated_seed = wrapped_test._hypothesis_internal_use_generated_seed
16801679
with local_settings(settings):
16811680
if not (state.failed_normally or generated_seed is None):

hypothesis-python/tests/common/setup.py

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
from warnings import filterwarnings
1313

1414
from hypothesis import HealthCheck, Phase, Verbosity, settings
15-
from hypothesis._settings import not_set
15+
from hypothesis._settings import CI, is_in_ci, not_set
1616
from hypothesis.internal.conjecture.data import AVAILABLE_PROVIDERS
1717
from hypothesis.internal.coverage import IN_COVERAGE_TESTS
1818

@@ -45,13 +45,14 @@ def run():
4545
v = getattr(x, s.name)
4646
# Check if it has a dynamically defined default and if so skip comparison.
4747
if getattr(settings, s.name).show_default:
48-
assert (
49-
v == s.default
48+
assert v == s.default or (
49+
is_in_ci() and v == getattr(CI, s.name)
5050
), f"({v!r} == x.{s.name}) != (s.{s.name} == {s.default!r})"
5151

5252
settings.register_profile(
5353
"default",
5454
settings(
55+
settings.get_profile("default"),
5556
max_examples=20 if IN_COVERAGE_TESTS else not_set,
5657
phases=list(Phase), # Dogfooding the explain phase
5758
),

hypothesis-python/tests/cover/test_settings.py

Lines changed: 55 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
# obtain one at https://mozilla.org/MPL/2.0/.
1010

1111
import datetime
12+
import os
1213
import subprocess
1314
import sys
1415
from unittest import TestCase
@@ -25,7 +26,7 @@
2526
note_deprecation,
2627
settings,
2728
)
28-
from hypothesis.database import ExampleDatabase
29+
from hypothesis.database import ExampleDatabase, InMemoryExampleDatabase
2930
from hypothesis.errors import (
3031
HypothesisDeprecationWarning,
3132
InvalidArgument,
@@ -108,12 +109,13 @@ def test_can_not_set_verbosity_to_non_verbosity():
108109

109110
@pytest.mark.parametrize("db", [None, ExampleDatabase()])
110111
def test_inherits_an_empty_database(db):
111-
assert settings.default.database is not None
112-
s = settings(database=db)
113-
assert s.database is db
114-
with local_settings(s):
115-
t = settings()
116-
assert t.database is db
112+
with local_settings(settings(database=InMemoryExampleDatabase())):
113+
assert settings.default.database is not None
114+
s = settings(database=db)
115+
assert s.database is db
116+
with local_settings(s):
117+
t = settings()
118+
assert t.database is db
117119

118120

119121
@pytest.mark.parametrize("db", [None, ExampleDatabase()])
@@ -273,6 +275,7 @@ def test_settings_as_decorator_must_be_on_callable():
273275
from hypothesis.configuration import set_hypothesis_home_dir
274276
from hypothesis.database import DirectoryBasedExampleDatabase
275277
278+
settings.load_profile("default")
276279
settings.default.database
277280
278281
if __name__ == '__main__':
@@ -476,8 +479,12 @@ def __repr__(self):
476479
assert "parent=(not settings repr)" in str(excinfo.value)
477480

478481

482+
def test_default_settings_do_not_use_ci():
483+
assert settings.get_profile("default").suppress_health_check == ()
484+
485+
479486
def test_show_changed():
480-
s = settings(max_examples=999, database=None)
487+
s = settings(settings.get_profile("default"), max_examples=999, database=None)
481488
assert s.show_changed() == "database=None, max_examples=999"
482489

483490

@@ -511,3 +518,43 @@ def test_deprecated_settings_not_in_settings_all_list():
511518
assert al == ls
512519
assert HealthCheck.return_value not in ls
513520
assert HealthCheck.not_a_test_method not in ls
521+
522+
523+
@skipif_emscripten
524+
def test_check_defaults_to_derandomize_when_running_on_ci():
525+
env = dict(os.environ)
526+
env["CI"] = "true"
527+
528+
assert (
529+
subprocess.check_output(
530+
[
531+
sys.executable,
532+
"-c",
533+
"from hypothesis import settings\nprint(settings().derandomize)",
534+
],
535+
env=env,
536+
text=True,
537+
encoding="utf-8",
538+
).strip()
539+
== "True"
540+
)
541+
542+
543+
@skipif_emscripten
544+
def test_check_defaults_to_randomize_when_not_running_on_ci():
545+
env = dict(os.environ)
546+
env.pop("CI", None)
547+
env.pop("TF_BUILD", None)
548+
assert (
549+
subprocess.check_output(
550+
[
551+
sys.executable,
552+
"-c",
553+
"from hypothesis import settings\nprint(settings().derandomize)",
554+
],
555+
env=env,
556+
text=True,
557+
encoding="utf-8",
558+
).strip()
559+
== "False"
560+
)

hypothesis-python/tests/pytest/test_capture.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,8 @@ def test_healthcheck_traceback_is_hidden(x):
9393
"""
9494

9595

96-
def test_healthcheck_traceback_is_hidden(testdir):
96+
def test_healthcheck_traceback_is_hidden(testdir, monkeypatch):
97+
monkeypatch.delenv("CI", raising=False)
9798
script = testdir.makepyfile(TRACEBACKHIDE_HEALTHCHECK)
9899
result = testdir.runpytest(script, "--verbose")
99100
def_token = "__ test_healthcheck_traceback_is_hidden __"

hypothesis-python/tests/pytest/test_seeding.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -82,7 +82,10 @@ def test_failure(i):
8282
"""
8383

8484

85-
def test_repeats_healthcheck_when_following_seed_instruction(testdir, tmp_path):
85+
def test_repeats_healthcheck_when_following_seed_instruction(
86+
testdir, tmp_path, monkeypatch
87+
):
88+
monkeypatch.delenv("CI", raising=False)
8689
health_check_test = HEALTH_CHECK_FAILURE.replace(
8790
"<file>", repr(str(tmp_path / "seen"))
8891
)

0 commit comments

Comments
 (0)