@@ -518,6 +518,54 @@ def getvalue(self):
518518TestResults = namedtuple ('TestResults' , 'failed attempted' )
519519
520520
521+ def _parse_example_timeout (source : str , default_timeout : float ) -> float :
522+ """
523+ Parse the timeout value from a doctest example's source.
524+
525+ INPUT:
526+
527+ - ``source`` -- the source code of a ``doctest.Example``
528+ - ``default_timeout`` -- the default timeout value to use
529+
530+ OUTPUT:
531+
532+ - a float, the timeout value to use for the example
533+
534+ TESTS::
535+
536+ sage: from sage.doctest.forker import _parse_example_timeout
537+ sage: _parse_example_timeout("sleep(10) # long time (limit 10s)", 5.0r)
538+ 10.0
539+ sage: _parse_example_timeout("sleep(10) # long time (limit 10s, possible regression)", 5.0r)
540+ 10.0
541+ sage: _parse_example_timeout("sleep(10) # long time (20s)", 5.0r)
542+ 5.0
543+ sage: _parse_example_timeout("sleep(10) # long time (limit 1a2s)", 5.0r)
544+ Traceback (most recent call last):
545+ ...
546+ ValueError: malformed optional tag '# long time (limit 1a2s)', should be '# long time (limit <number>s)'
547+ sage: _parse_example_timeout("sleep(10) # long time (:issue:`12345`)", 5.0r)
548+ 5.0
549+ """
550+ # TODO this double-parsing is inefficient, should make :meth:`SageDocTestParser.parse`
551+ # return subclass of doctest.Example that already include the timeout value
552+ from sage .doctest .parsing import parse_optional_tags
553+ value = parse_optional_tags (source ).get ("long time" , None )
554+ if value is None :
555+ # either has the "long time" tag without any value in parentheses,
556+ # or tag not present
557+ return default_timeout
558+ assert isinstance (value , str )
559+ match = re .fullmatch (r'\s*limit\s+(\S+)s(\s*,.*)?' , value .strip ())
560+ if match :
561+ try :
562+ return float (match [1 ])
563+ except ValueError :
564+ raise ValueError (f"malformed optional tag '# long time ({ value } )', should be '# long time (limit <number>s)'" )
565+ else :
566+ return default_timeout
567+
568+
521569class SageDocTestRunner (doctest .DocTestRunner ):
522570 def __init__ (self , * args , ** kwds ):
523571 """
@@ -820,8 +868,10 @@ def compiler(example):
820868 if example .warnings :
821869 for warning in example .warnings :
822870 out (self ._failure_header (test , example , f'Warning: { warning } ' ))
871+
823872 if outcome is SUCCESS :
824- if self .options .warn_long > 0 and example .cputime + check_timer .cputime > self .options .warn_long :
873+ if self .options .warn_long > 0 and example .cputime + check_timer .cputime > _parse_example_timeout (
874+ example .source , self .options .warn_long ):
825875 self .report_overtime (out , test , example , got ,
826876 check_timer = check_timer )
827877 elif example .warnings :
0 commit comments