Repository: HypothesisWorks/hypothesis

Issue:
Error with Django parallel test runner: hypothesis.errors.DeadlineExceeded is unpickleable
```
<snip>
  File "/usr/lib/python3.8/unittest/suite.py", line 122, in run
    test(result)
  File "/root/apps/netdb_demo/lib/python3.8/site-packages/hypothesis/extra/django/_impl.py", line 47, in __call__
    return unittest.TestCase.__call__(self, result)
  File "/usr/lib/python3.8/unittest/case.py", line 736, in __call__
    return self.run(*args, **kwds)
  File "/usr/lib/python3.8/unittest/case.py", line 684, in run
    self._feedErrorsToResult(result, outcome.errors)
  File "/usr/lib/python3.8/unittest/case.py", line 602, in _feedErrorsToResult
    result.addError(test, exc_info)
  File "/root/apps/netdb_demo/lib/python3.8/site-packages/django/test/runner.py", line 197, in addError
    self.check_picklable(test, err)
  File "/root/apps/netdb_demo/lib/python3.8/site-packages/django/test/runner.py", line 130, in check_picklable
    self._confirm_picklable(err)
  File "/root/apps/netdb_demo/lib/python3.8/site-packages/django/test/runner.py", line 104, in _confirm_picklable
    pickle.loads(pickle.dumps(obj))
  File "/root/apps/netdb_demo/lib/python3.8/site-packages/tblib/pickling_support.py", line 26, in unpickle_exception
    inst = func(*args)
TypeError: __init__() missing 1 required positional argument: 'deadline'
```
This happens because Exception by default reduces to arguments passed to its `__init__`, and DeadlineExceeded (and FailedHealthCheck, afaict) changes the signature when calling `super().__init__`.

One solution is to call `super().__init__()` with the same arguments, and override `__str__`; another would be to override `__reduce__`.


Discussion:

Resolving patch:
diff --git a/hypothesis-python/src/hypothesis/errors.py b/hypothesis-python/src/hypothesis/errors.py
--- a/hypothesis-python/src/hypothesis/errors.py
+++ b/hypothesis-python/src/hypothesis/errors.py
@@ -93,12 +93,7 @@ class HypothesisWarning(HypothesisException, Warning):
 
 
 class FailedHealthCheck(_Trimmable):
-    """Raised when a test fails a preliminary healthcheck that occurs before
-    execution."""
-
-    def __init__(self, message, check):
-        super().__init__(message)
-        self.health_check = check
+    """Raised when a test fails a healthcheck."""
 
 
 class NonInteractiveExampleWarning(HypothesisWarning):
@@ -149,6 +144,9 @@ def __init__(self, runtime, deadline):
         self.runtime = runtime
         self.deadline = deadline
 
+    def __reduce__(self):
+        return (type(self), (self.runtime, self.deadline))
+
 
 class StopTest(BaseException):
     """Raised when a test should stop running and return control to
diff --git a/hypothesis-python/src/hypothesis/internal/healthcheck.py b/hypothesis-python/src/hypothesis/internal/healthcheck.py
--- a/hypothesis-python/src/hypothesis/internal/healthcheck.py
+++ b/hypothesis-python/src/hypothesis/internal/healthcheck.py
@@ -24,4 +24,4 @@ def fail_health_check(settings, message, label):
         f"If you want to disable just this health check, add {label} "
         "to the suppress_health_check settings for this test."
     )
-    raise FailedHealthCheck(message, label)
+    raise FailedHealthCheck(message)


Test patch:
diff --git a/hypothesis-python/tests/conjecture/test_engine.py b/hypothesis-python/tests/conjecture/test_engine.py
--- a/hypothesis-python/tests/conjecture/test_engine.py
+++ b/hypothesis-python/tests/conjecture/test_engine.py
@@ -404,7 +404,7 @@ def accept(f):
 
         with pytest.raises(FailedHealthCheck) as e:
             runner.run()
-        assert e.value.health_check == label
+        assert str(label) in str(e.value)
         assert not runner.interesting_examples
 
     return accept
diff --git a/hypothesis-python/tests/cover/test_health_checks.py b/hypothesis-python/tests/cover/test_health_checks.py
--- a/hypothesis-python/tests/cover/test_health_checks.py
+++ b/hypothesis-python/tests/cover/test_health_checks.py
@@ -176,7 +176,7 @@ def test(b):
     with pytest.raises(FailedHealthCheck) as exc:
         test()
 
-    assert exc.value.health_check == HealthCheck.large_base_example
+    assert str(HealthCheck.large_base_example) in str(exc.value)
 
 
 def test_example_that_shrinks_to_overrun_fails_health_check():
@@ -187,7 +187,7 @@ def test(b):
     with pytest.raises(FailedHealthCheck) as exc:
         test()
 
-    assert exc.value.health_check == HealthCheck.large_base_example
+    assert str(HealthCheck.large_base_example) in str(exc.value)
 
 
 def test_it_is_an_error_to_suppress_non_iterables():
diff --git a/hypothesis-python/tests/cover/test_regressions.py b/hypothesis-python/tests/cover/test_regressions.py
--- a/hypothesis-python/tests/cover/test_regressions.py
+++ b/hypothesis-python/tests/cover/test_regressions.py
@@ -8,12 +8,22 @@
 # v. 2.0. If a copy of the MPL was not distributed with this file, You can
 # obtain one at https://mozilla.org/MPL/2.0/.
 
+import pickle
 import random
+from datetime import timedelta
 from unittest.mock import Mock
 
 import pytest
 
-from hypothesis import Verbosity, assume, given, seed, settings, strategies as st
+from hypothesis import (
+    Verbosity,
+    assume,
+    errors,
+    given,
+    seed,
+    settings,
+    strategies as st,
+)
 
 
 def strat():
@@ -111,3 +121,27 @@ def test_prng_state_unpolluted_by_given_issue_1266():
         assert second == third
     else:
         assert second != third
+
+
+exc_instances = [
+    errors.NoSuchExample("foobar", extra="baz"),
+    errors.DeadlineExceeded(
+        runtime=timedelta(seconds=1.5), deadline=timedelta(seconds=1.0)
+    ),
+]
+
+
+@pytest.mark.parametrize("exc", exc_instances, ids=repr)
+def test_exceptions_are_picklable(exc):
+    # See https://github.com/HypothesisWorks/hypothesis/issues/3426
+    pickle.loads(pickle.dumps(exc))
+
+
+def test_no_missed_custom_init_exceptions():
+    untested_errors_with_custom_init = {
+        et
+        for et in vars(errors).values()
+        if isinstance(et, type) and issubclass(et, Exception) and "__init__" in vars(et)
+    } - {type(exc) for exc in exc_instances}
+    print(untested_errors_with_custom_init)
+    assert not untested_errors_with_custom_init