Skip to content

Commit 4651999

Browse files
committed
Implement specified timeout for slow doctests
1 parent 4cdd703 commit 4651999

File tree

1 file changed

+51
-1
lines changed

1 file changed

+51
-1
lines changed

src/sage/doctest/forker.py

Lines changed: 51 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -518,6 +518,54 @@ def getvalue(self):
518518
TestResults = 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+
521569
class 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

Comments
 (0)