34
34
Hashable ,
35
35
List ,
36
36
Optional ,
37
+ Tuple ,
38
+ Type ,
37
39
TypeVar ,
38
40
Union ,
39
41
overload ,
92
94
get_signature ,
93
95
impersonate ,
94
96
is_mock ,
97
+ nicerepr ,
95
98
proxies ,
96
99
repr_call ,
97
100
)
98
101
from hypothesis .internal .scrutineer import Tracer , explanatory_lines
102
+ from hypothesis .internal .validation import check_type
99
103
from hypothesis .reporting import (
100
104
current_verbosity ,
101
105
report ,
134
138
class Example :
135
139
args = attr .ib ()
136
140
kwargs = attr .ib ()
141
+ # Plus two optional arguments for .xfail()
142
+ raises = attr .ib (default = None )
143
+ reason = attr .ib (default = None )
137
144
138
145
139
146
class example :
@@ -156,6 +163,49 @@ def __call__(self, test: TestFunc) -> TestFunc:
156
163
test .hypothesis_explicit_examples .append (self ._this_example ) # type: ignore
157
164
return test
158
165
166
+ def xfail (
167
+ self ,
168
+ condition : bool = True ,
169
+ * ,
170
+ reason : str = "" ,
171
+ raises : Union [Type [BaseException ], Tuple [BaseException , ...]] = BaseException ,
172
+ ) -> "example" :
173
+ """Mark this example as an expected failure, like pytest.mark.xfail().
174
+
175
+ Expected-failing examples allow you to check that your test does fail on
176
+ some examples, and therefore build confidence that *passing* tests are
177
+ because your code is working, not because the test is missing something.
178
+
179
+ .. code-block:: python
180
+
181
+ @example(...).xfail()
182
+ @example(...).xfail(reason="Prices must be non-negative")
183
+ @example(...).xfail(raises=(KeyError, ValueError))
184
+ @example(...).xfail(sys.version_info[:2] >= (3, 9), reason="needs py39+")
185
+ @example(...).xfail(condition=sys.platform != "linux", raises=OSError)
186
+ def test(x):
187
+ pass
188
+ """
189
+ check_type (bool , condition , "condition" )
190
+ check_type (str , reason , "reason" )
191
+ if not (
192
+ isinstance (raises , type ) and issubclass (raises , BaseException )
193
+ ) and not (
194
+ isinstance (raises , tuple )
195
+ and raises # () -> expected to fail with no error, which is impossible
196
+ and all (
197
+ isinstance (r , type ) and issubclass (r , BaseException ) for r in raises
198
+ )
199
+ ):
200
+ raise InvalidArgument (
201
+ f"raises={ raises !r} must be an exception type or tuple of exception types"
202
+ )
203
+ if condition :
204
+ self ._this_example = attr .evolve (
205
+ self ._this_example , raises = raises , reason = reason
206
+ )
207
+ return self
208
+
159
209
def via (self , * whence : str ) -> "example" :
160
210
"""Attach a machine-readable label noting whence this example came.
161
211
@@ -454,12 +504,47 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s
454
504
with local_settings (state .settings ):
455
505
fragments_reported = []
456
506
try :
507
+ adata = ArtificialDataForExample (arguments , example_kwargs )
508
+ bits = ", " .join (nicerepr (x ) for x in arguments ) + ", " .join (
509
+ f"{ k } ={ nicerepr (v )} " for k , v in example_kwargs .items ()
510
+ )
457
511
with with_reporter (fragments_reported .append ):
458
- state .execute_once (
459
- ArtificialDataForExample (arguments , example_kwargs ),
460
- is_final = True ,
461
- print_example = True ,
462
- )
512
+ if example .raises is None :
513
+ state .execute_once (adata , is_final = True , print_example = True )
514
+ else :
515
+ # @example(...).xfail(...)
516
+ try :
517
+ state .execute_once (adata , is_final = True , print_example = True )
518
+ except failure_exceptions_to_catch () as err :
519
+ if not isinstance (err , example .raises ):
520
+ raise
521
+ except example .raises as err :
522
+ # We'd usually check this as early as possible, but it's
523
+ # possible for failure_exceptions_to_catch() to grow when
524
+ # e.g. pytest is imported between import- and test-time.
525
+ raise InvalidArgument (
526
+ f"@example({ bits } ) raised an expected { err !r} , "
527
+ "but Hypothesis does not treat this as a test failure"
528
+ ) from err
529
+ else :
530
+ # Unexpectedly passing; always raise an error in this case.
531
+ reason = f" because { example .reason } " * bool (example .reason )
532
+ if example .raises is BaseException :
533
+ name = "exception" # special-case no raises= arg
534
+ elif not isinstance (example .raises , tuple ):
535
+ name = example .raises .__name__
536
+ elif len (example .raises ) == 1 :
537
+ name = example .raises [0 ].__name__
538
+ else :
539
+ name = (
540
+ ", " .join (ex .__name__ for ex in example .raises [:- 1 ])
541
+ + f", or { example .raises [- 1 ].__name__ } "
542
+ )
543
+ vowel = name .upper ()[0 ] in "AEIOU"
544
+ raise AssertionError (
545
+ f"Expected a{ 'n' * vowel } { name } from @example({ bits } )"
546
+ f"{ reason } , but no exception was raised."
547
+ )
463
548
except UnsatisfiedAssumption :
464
549
# Odd though it seems, we deliberately support explicit examples that
465
550
# are then rejected by a call to `assume()`. As well as iterative
0 commit comments