@@ -358,10 +358,17 @@ def finish(self) -> None:
358
358
def s (v : int ) -> str :
359
359
return 's' if v != 1 else ''
360
360
361
+ header = 'Doctest summary'
362
+ if self .total_failures or self .setup_failures or self .cleanup_failures :
363
+ self .app .statuscode = 1
364
+ if self .config .doctest_fail_fast :
365
+ header = f'{ header } (exiting after first failed test)'
366
+ underline = '=' * len (header )
367
+
361
368
self ._out (
362
369
f"""
363
- Doctest summary
364
- ===============
370
+ { header }
371
+ { underline }
365
372
{ self .total_tries :5} test{ s (self .total_tries )}
366
373
{ self .total_failures :5} failure{ s (self .total_failures )} in tests
367
374
{ self .setup_failures :5} failure{ s (self .setup_failures )} in setup code
@@ -370,15 +377,14 @@ def s(v: int) -> str:
370
377
)
371
378
self .outfile .close ()
372
379
373
- if self .total_failures or self .setup_failures or self .cleanup_failures :
374
- self .app .statuscode = 1
375
-
376
380
def write_documents (self , docnames : Set [str ]) -> None :
377
381
logger .info (bold ('running tests...' ))
378
382
for docname in sorted (docnames ):
379
383
# no need to resolve the doctree
380
384
doctree = self .env .get_doctree (docname )
381
- self .test_doc (docname , doctree )
385
+ success = self .test_doc (docname , doctree )
386
+ if not success and self .config .doctest_fail_fast :
387
+ break
382
388
383
389
def get_filename_for_node (self , node : Node , docname : str ) -> str :
384
390
"""Try to get the file which actually contains the doctest, not the
@@ -419,7 +425,7 @@ def skipped(self, node: Element) -> bool:
419
425
exec (self .config .doctest_global_cleanup , context ) # NoQA: S102
420
426
return should_skip
421
427
422
- def test_doc (self , docname : str , doctree : Node ) -> None :
428
+ def test_doc (self , docname : str , doctree : Node ) -> bool :
423
429
groups : dict [str , TestGroup ] = {}
424
430
add_to_all_groups = []
425
431
self .setup_runner = SphinxDocTestRunner (verbose = False , optionflags = self .opt )
@@ -496,13 +502,17 @@ def condition(node: Node) -> bool:
496
502
for group in groups .values ():
497
503
group .add_code (code )
498
504
if not groups :
499
- return
505
+ return True
500
506
501
507
show_successes = self .config .doctest_show_successes
502
508
if show_successes :
503
509
self ._out (f'\n Document: { docname } \n ----------{ "-" * len (docname )} \n ' )
510
+ success = True
504
511
for group in groups .values ():
505
- self .test_group (group )
512
+ if not self .test_group (group ):
513
+ success = False
514
+ if self .config .doctest_fail_fast :
515
+ break
506
516
# Separately count results from setup code
507
517
res_f , res_t = self .setup_runner .summarize (self ._out , verbose = False )
508
518
self .setup_failures += res_f
@@ -517,13 +527,14 @@ def condition(node: Node) -> bool:
517
527
)
518
528
self .cleanup_failures += res_f
519
529
self .cleanup_tries += res_t
530
+ return success
520
531
521
532
def compile (
522
533
self , code : str , name : str , type : str , flags : Any , dont_inherit : bool
523
534
) -> Any :
524
535
return compile (code , name , self .type , flags , dont_inherit )
525
536
526
- def test_group (self , group : TestGroup ) -> None :
537
+ def test_group (self , group : TestGroup ) -> bool :
527
538
ns : dict [str , Any ] = {}
528
539
529
540
def run_setup_cleanup (
@@ -553,9 +564,10 @@ def run_setup_cleanup(
553
564
# run the setup code
554
565
if not run_setup_cleanup (self .setup_runner , group .setup , 'setup' ):
555
566
# if setup failed, don't run the group
556
- return
567
+ return False
557
568
558
569
# run the tests
570
+ success = True
559
571
for code in group .tests :
560
572
if len (code ) == 1 :
561
573
# ordinary doctests (code/output interleaved)
@@ -608,11 +620,19 @@ def run_setup_cleanup(
608
620
self .type = 'exec' # multiple statements again
609
621
# DocTest.__init__ copies the globs namespace, which we don't want
610
622
test .globs = ns
623
+ old_f = self .test_runner .failures
611
624
# also don't clear the globs namespace after running the doctest
612
625
self .test_runner .run (test , out = self ._warn_out , clear_globs = False )
626
+ if self .test_runner .failures > old_f :
627
+ success = False
628
+ if self .config .doctest_fail_fast :
629
+ break
613
630
614
631
# run the cleanup
615
- run_setup_cleanup (self .cleanup_runner , group .cleanup , 'cleanup' )
632
+ if not run_setup_cleanup (self .cleanup_runner , group .cleanup , 'cleanup' ):
633
+ return False
634
+
635
+ return success
616
636
617
637
618
638
def setup (app : Sphinx ) -> ExtensionMetadata :
@@ -638,6 +658,7 @@ def setup(app: Sphinx) -> ExtensionMetadata:
638
658
'' ,
639
659
types = frozenset ({int }),
640
660
)
661
+ app .add_config_value ('doctest_fail_fast' , False , '' , types = frozenset ({bool }))
641
662
return {
642
663
'version' : sphinx .__display_version__ ,
643
664
'parallel_read_safe' : True ,
0 commit comments