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