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,51 @@ 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 [
172
+ Type [BaseException ], Tuple [Type [BaseException ], ...]
173
+ ] = BaseException ,
174
+ ) -> "example" :
175
+ """Mark this example as an expected failure, like pytest.mark.xfail().
176
+
177
+ Expected-failing examples allow you to check that your test does fail on
178
+ some examples, and therefore build confidence that *passing* tests are
179
+ because your code is working, not because the test is missing something.
180
+
181
+ .. code-block:: python
182
+
183
+ @example(...).xfail()
184
+ @example(...).xfail(reason="Prices must be non-negative")
185
+ @example(...).xfail(raises=(KeyError, ValueError))
186
+ @example(...).xfail(sys.version_info[:2] >= (3, 9), reason="needs py39+")
187
+ @example(...).xfail(condition=sys.platform != "linux", raises=OSError)
188
+ def test(x):
189
+ pass
190
+ """
191
+ check_type (bool , condition , "condition" )
192
+ check_type (str , reason , "reason" )
193
+ if not (
194
+ isinstance (raises , type ) and issubclass (raises , BaseException )
195
+ ) and not (
196
+ isinstance (raises , tuple )
197
+ and raises # () -> expected to fail with no error, which is impossible
198
+ and all (
199
+ isinstance (r , type ) and issubclass (r , BaseException ) for r in raises
200
+ )
201
+ ):
202
+ raise InvalidArgument (
203
+ f"raises={ raises !r} must be an exception type or tuple of exception types"
204
+ )
205
+ if condition :
206
+ self ._this_example = attr .evolve (
207
+ self ._this_example , raises = raises , reason = reason
208
+ )
209
+ return self
210
+
159
211
def via (self , * whence : str ) -> "example" :
160
212
"""Attach a machine-readable label noting whence this example came.
161
213
@@ -400,9 +452,7 @@ def draw(self, strategy):
400
452
assert self .__draws == 0
401
453
self .__draws += 1
402
454
# The main strategy for given is always a tuples strategy that returns
403
- # first positional arguments then keyword arguments. When building this
404
- # object already converted all positional arguments to keyword arguments,
405
- # so this is the correct format to return.
455
+ # first positional arguments then keyword arguments.
406
456
return self .__args , self .__kwargs
407
457
408
458
@@ -414,6 +464,7 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s
414
464
]
415
465
416
466
for example in reversed (getattr (wrapped_test , "hypothesis_explicit_examples" , ())):
467
+ assert isinstance (example , Example )
417
468
# All of this validation is to check that @example() got "the same" arguments
418
469
# as @given, i.e. corresponding to the same parameters, even though they might
419
470
# be any mixture of positional and keyword arguments.
@@ -455,12 +506,47 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s
455
506
with local_settings (state .settings ):
456
507
fragments_reported = []
457
508
try :
509
+ adata = ArtificialDataForExample (arguments , example_kwargs )
510
+ bits = ", " .join (nicerepr (x ) for x in arguments ) + ", " .join (
511
+ f"{ k } ={ nicerepr (v )} " for k , v in example_kwargs .items ()
512
+ )
458
513
with with_reporter (fragments_reported .append ):
459
- state .execute_once (
460
- ArtificialDataForExample (arguments , example_kwargs ),
461
- is_final = True ,
462
- print_example = True ,
463
- )
514
+ if example .raises is None :
515
+ state .execute_once (adata , is_final = True , print_example = True )
516
+ else :
517
+ # @example(...).xfail(...)
518
+ try :
519
+ state .execute_once (adata , is_final = True , print_example = True )
520
+ except failure_exceptions_to_catch () as err :
521
+ if not isinstance (err , example .raises ):
522
+ raise
523
+ except example .raises as err :
524
+ # We'd usually check this as early as possible, but it's
525
+ # possible for failure_exceptions_to_catch() to grow when
526
+ # e.g. pytest is imported between import- and test-time.
527
+ raise InvalidArgument (
528
+ f"@example({ bits } ) raised an expected { err !r} , "
529
+ "but Hypothesis does not treat this as a test failure"
530
+ ) from err
531
+ else :
532
+ # Unexpectedly passing; always raise an error in this case.
533
+ reason = f" because { example .reason } " * bool (example .reason )
534
+ if example .raises is BaseException :
535
+ name = "exception" # special-case no raises= arg
536
+ elif not isinstance (example .raises , tuple ):
537
+ name = example .raises .__name__
538
+ elif len (example .raises ) == 1 :
539
+ name = example .raises [0 ].__name__
540
+ else :
541
+ name = (
542
+ ", " .join (ex .__name__ for ex in example .raises [:- 1 ])
543
+ + f", or { example .raises [- 1 ].__name__ } "
544
+ )
545
+ vowel = name .upper ()[0 ] in "AEIOU"
546
+ raise AssertionError (
547
+ f"Expected a{ 'n' * vowel } { name } from @example({ bits } )"
548
+ f"{ reason } , but no exception was raised."
549
+ )
464
550
except UnsatisfiedAssumption :
465
551
# Odd though it seems, we deliberately support explicit examples that
466
552
# are then rejected by a call to `assume()`. As well as iterative
@@ -478,7 +564,7 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s
478
564
# One user error - whether misunderstanding or typo - we've seen a few
479
565
# times is to pass strategies to @example() where values are expected.
480
566
# Checking is easy, and false-positives not much of a problem, so:
481
- if any (
567
+ if isinstance ( err , failure_exceptions_to_catch ()) and any (
482
568
isinstance (arg , SearchStrategy )
483
569
for arg in example .args + tuple (example .kwargs .values ())
484
570
):
@@ -494,6 +580,7 @@ def execute_explicit_examples(state, wrapped_test, arguments, kwargs, original_s
494
580
if (
495
581
state .settings .report_multiple_bugs
496
582
and pytest_shows_exceptiongroups
583
+ and isinstance (err , failure_exceptions_to_catch ())
497
584
and not isinstance (err , skip_exceptions_to_reraise ())
498
585
):
499
586
continue
0 commit comments