From e944a54809c4e5e18bf019f4e3c34ca6054e63bb Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Wed, 20 Dec 2023 13:38:10 -0800 Subject: [PATCH 1/8] src/sage/doctest/forker.py: Show '# [failed in baseline]' earlier --- src/sage/doctest/forker.py | 41 +++++++++++++++++++++++++++++++------- 1 file changed, 34 insertions(+), 7 deletions(-) diff --git a/src/sage/doctest/forker.py b/src/sage/doctest/forker.py index efd28d10abe..32889e88c29 100644 --- a/src/sage/doctest/forker.py +++ b/src/sage/doctest/forker.py @@ -513,7 +513,7 @@ def __init__(self, *args, **kwds): - ``stdout`` -- an open file to restore for debugging - - ``checker`` -- None, or an instance of + - ``checker`` -- ``None``, or an instance of :class:`doctest.OutputChecker` - ``verbose`` -- boolean, determines whether verbose printing @@ -522,6 +522,9 @@ def __init__(self, *args, **kwds): - ``optionflags`` -- Controls the comparison with the expected output. See :mod:`testmod` for more information. + - ``baseline`` -- ``None`` or a dictionary, the ``baseline_stats`` + value + EXAMPLES:: sage: from sage.doctest.parsing import SageOutputChecker @@ -535,6 +538,9 @@ def __init__(self, *args, **kwds): O = kwds.pop('outtmpfile', None) self.msgfile = kwds.pop('msgfile', None) self.options = kwds.pop('sage_options') + self.baseline = kwds.pop('baseline', None) + if self.baseline is None: + self.baseline = {} doctest.DocTestRunner.__init__(self, *args, **kwds) self._fakeout = SageSpoofInOut(O) if self.msgfile is None: @@ -1721,12 +1727,20 @@ def serial_dispatch(self): """ for source in self.controller.sources: heading = self.controller.reporter.report_head(source) + basename = source.basename + if self.controller.baseline_stats: + the_baseline_stats = self.controller.baseline_stats.get(basename, {}) + else: + the_baseline_stats = {} + if the_baseline_stats.get('failed', False): + heading += " # [failed in baseline]" if not self.controller.options.only_errors: self.controller.log(heading) with tempfile.TemporaryFile() as outtmpfile: result = DocTestTask(source)(self.controller.options, - outtmpfile, self.controller.logger) + outtmpfile, self.controller.logger, + baseline=the_baseline_stats) outtmpfile.seek(0) output = bytes_to_str(outtmpfile.read()) @@ -1985,10 +1999,17 @@ def sel_exit(): # Start a new worker. import copy worker_options = copy.copy(opt) + basename = source.basename + if self.controller.baseline_stats: + the_baseline_stats = self.controller.baseline_stats.get(basename, {}) + else: + the_baseline_stats = {} if target_endtime is not None: worker_options.target_walltime = (target_endtime - now) / (max(1, pending_tests / opt.nthreads)) - w = DocTestWorker(source, options=worker_options, funclist=[sel_exit]) + w = DocTestWorker(source, options=worker_options, baseline=the_baseline_stats, funclist=[sel_exit]) heading = self.controller.reporter.report_head(w.source) + if the_baseline_stats.get('failed', False): + heading += " # [failed in baseline]" if not self.controller.options.only_errors: w.messages = heading + "\n" # Store length of heading to detect if the @@ -2147,7 +2168,7 @@ class should be accessed by the child process. sage: reporter.report(FDS, False, W.exitcode, result, "") [... tests, ... s] """ - def __init__(self, source, options, funclist=[]): + def __init__(self, source, options, funclist=[], baseline=None): """ Initialization. @@ -2171,6 +2192,7 @@ def __init__(self, source, options, funclist=[]): self.source = source self.options = options self.funclist = funclist + self.baseline = baseline # Open pipe for messages. These are raw file descriptors, # not Python file objects! @@ -2242,7 +2264,7 @@ def run(self): os.close(self.rmessages) msgpipe = os.fdopen(self.wmessages, "w") try: - task(self.options, self.outtmpfile, msgpipe, self.result_queue) + task(self.options, self.outtmpfile, msgpipe, self.result_queue, baseline=self.baseline) finally: msgpipe.close() self.outtmpfile.close() @@ -2508,7 +2530,8 @@ def __init__(self, source): """ self.source = source - def __call__(self, options, outtmpfile=None, msgfile=None, result_queue=None): + def __call__(self, options, outtmpfile=None, msgfile=None, result_queue=None, *, + baseline=None): """ Calling the task does the actual work of running the doctests. @@ -2525,6 +2548,9 @@ def __call__(self, options, outtmpfile=None, msgfile=None, result_queue=None): - ``result_queue`` -- an instance of :class:`multiprocessing.Queue` to store the doctest result. For testing, this can also be None. + - ``baseline`` -- ``None`` or a dictionary, the ``baseline_stats`` + value. + OUTPUT: - ``(doctests, result_dict)`` where ``doctests`` is the number of @@ -2560,7 +2586,8 @@ def __call__(self, options, outtmpfile=None, msgfile=None, result_queue=None): outtmpfile=outtmpfile, msgfile=msgfile, sage_options=options, - optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS) + optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS, + baseline=baseline) runner.basename = self.source.basename runner.filename = self.source.path N = options.file_iterations From 91449f7d84a031401798f1c33fec364004093779 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Wed, 20 Dec 2023 15:30:09 -0800 Subject: [PATCH 2/8] Refactor through new function DocTestController.source_baseline --- src/sage/doctest/control.py | 28 ++++++++++++++++++++++++++++ src/sage/doctest/forker.py | 35 +++++++++++++---------------------- src/sage/doctest/reporting.py | 17 +++++++---------- 3 files changed, 48 insertions(+), 32 deletions(-) diff --git a/src/sage/doctest/control.py b/src/sage/doctest/control.py index fd92dc386bf..37562e1717c 100644 --- a/src/sage/doctest/control.py +++ b/src/sage/doctest/control.py @@ -1097,6 +1097,34 @@ def sort_key(source): return -self.stats.get(basename, default).get('walltime', 0), basename self.sources = sorted(self.sources, key=sort_key) + def source_baseline(self, source): + r""" + Return the ``baseline_stats`` value of ``source``. + + INPUT: + + - ``source`` -- a :class:`DocTestSource` instance + + OUTPUT: + + A dictionary. + + EXAMPLES:: + + sage: from sage.doctest.control import DocTestDefaults, DocTestController + sage: from sage.env import SAGE_SRC + sage: import os + sage: filename = os.path.join(SAGE_SRC,'sage','doctest','util.py') + sage: DD = DocTestDefaults() + sage: DC = DocTestController(DD, [filename]) + sage: DC.source_baseline(DC.sources[0]) + {} + """ + if self.baseline_stats: + basename = source.basename + return self.baseline_stats.get(basename, {}) + return {} + def run_doctests(self): """ Actually runs the doctests. diff --git a/src/sage/doctest/forker.py b/src/sage/doctest/forker.py index 32889e88c29..a8b3a7be5aa 100644 --- a/src/sage/doctest/forker.py +++ b/src/sage/doctest/forker.py @@ -522,8 +522,7 @@ def __init__(self, *args, **kwds): - ``optionflags`` -- Controls the comparison with the expected output. See :mod:`testmod` for more information. - - ``baseline`` -- ``None`` or a dictionary, the ``baseline_stats`` - value + - ``baseline`` -- dictionary, the ``baseline_stats`` value EXAMPLES:: @@ -538,9 +537,7 @@ def __init__(self, *args, **kwds): O = kwds.pop('outtmpfile', None) self.msgfile = kwds.pop('msgfile', None) self.options = kwds.pop('sage_options') - self.baseline = kwds.pop('baseline', None) - if self.baseline is None: - self.baseline = {} + self.baseline = kwds.pop('baseline', {}) doctest.DocTestRunner.__init__(self, *args, **kwds) self._fakeout = SageSpoofInOut(O) if self.msgfile is None: @@ -1727,12 +1724,8 @@ def serial_dispatch(self): """ for source in self.controller.sources: heading = self.controller.reporter.report_head(source) - basename = source.basename - if self.controller.baseline_stats: - the_baseline_stats = self.controller.baseline_stats.get(basename, {}) - else: - the_baseline_stats = {} - if the_baseline_stats.get('failed', False): + baseline = self.controller.source_baseline(source) + if baseline.get('failed', False): heading += " # [failed in baseline]" if not self.controller.options.only_errors: self.controller.log(heading) @@ -1740,7 +1733,7 @@ def serial_dispatch(self): with tempfile.TemporaryFile() as outtmpfile: result = DocTestTask(source)(self.controller.options, outtmpfile, self.controller.logger, - baseline=the_baseline_stats) + baseline=baseline) outtmpfile.seek(0) output = bytes_to_str(outtmpfile.read()) @@ -1999,16 +1992,12 @@ def sel_exit(): # Start a new worker. import copy worker_options = copy.copy(opt) - basename = source.basename - if self.controller.baseline_stats: - the_baseline_stats = self.controller.baseline_stats.get(basename, {}) - else: - the_baseline_stats = {} + baseline = self.controller.source_baseline(source) if target_endtime is not None: worker_options.target_walltime = (target_endtime - now) / (max(1, pending_tests / opt.nthreads)) - w = DocTestWorker(source, options=worker_options, baseline=the_baseline_stats, funclist=[sel_exit]) + w = DocTestWorker(source, options=worker_options, funclist=[sel_exit], baseline=baseline) heading = self.controller.reporter.report_head(w.source) - if the_baseline_stats.get('failed', False): + if baseline.get('failed', False): heading += " # [failed in baseline]" if not self.controller.options.only_errors: w.messages = heading + "\n" @@ -2149,6 +2138,8 @@ class should be accessed by the child process. - ``funclist`` -- a list of callables to be called at the start of the child process. + - ``baseline`` -- dictionary, the ``baseline_stats`` value + EXAMPLES:: sage: from sage.doctest.forker import DocTestWorker, DocTestTask @@ -2264,7 +2255,8 @@ def run(self): os.close(self.rmessages) msgpipe = os.fdopen(self.wmessages, "w") try: - task(self.options, self.outtmpfile, msgpipe, self.result_queue, baseline=self.baseline) + task(self.options, self.outtmpfile, msgpipe, self.result_queue, + baseline=self.baseline) finally: msgpipe.close() self.outtmpfile.close() @@ -2548,8 +2540,7 @@ def __call__(self, options, outtmpfile=None, msgfile=None, result_queue=None, *, - ``result_queue`` -- an instance of :class:`multiprocessing.Queue` to store the doctest result. For testing, this can also be None. - - ``baseline`` -- ``None`` or a dictionary, the ``baseline_stats`` - value. + - ``baseline`` -- a dictionary, the ``baseline_stats`` value. OUTPUT: diff --git a/src/sage/doctest/reporting.py b/src/sage/doctest/reporting.py index a86153ce326..dcc0dee792c 100644 --- a/src/sage/doctest/reporting.py +++ b/src/sage/doctest/reporting.py @@ -399,10 +399,7 @@ def report(self, source, timeout, return_code, results, output, pid=None): postscript = self.postscript stats = self.stats basename = source.basename - if self.controller.baseline_stats: - the_baseline_stats = self.controller.baseline_stats.get(basename, {}) - else: - the_baseline_stats = {} + baseline = self.controller.source_baseline(source) cmd = self.report_head(source) try: ntests, result_dict = results @@ -423,14 +420,14 @@ def report(self, source, timeout, return_code, results, output, pid=None): fail_msg += " (and interrupt failed)" else: fail_msg += " (with %s after interrupt)" % signal_name(sig) - if the_baseline_stats.get('failed', False): + if baseline.get('failed', False): fail_msg += " [failed in baseline]" log(" %s\n%s\nTests run before %s timed out:" % (fail_msg, "*"*70, process_name)) log(output) log("*"*70) postscript['lines'].append(cmd + " # %s" % fail_msg) stats[basename] = {"failed": True, "walltime": 1e6, "ntests": ntests} - if not the_baseline_stats.get('failed', False): + if not baseline.get('failed', False): self.error_status |= 4 elif return_code: if return_code > 0: @@ -439,14 +436,14 @@ def report(self, source, timeout, return_code, results, output, pid=None): fail_msg = "Killed due to %s" % signal_name(-return_code) if ntests > 0: fail_msg += " after testing finished" - if the_baseline_stats.get('failed', False): + if baseline.get('failed', False): fail_msg += " [failed in baseline]" log(" %s\n%s\nTests run before %s failed:" % (fail_msg,"*"*70, process_name)) log(output) log("*"*70) postscript['lines'].append(cmd + " # %s" % fail_msg) stats[basename] = {"failed": True, "walltime": 1e6, "ntests": ntests} - if not the_baseline_stats.get('failed', False): + if not baseline.get('failed', False): self.error_status |= (8 if return_code > 0 else 16) else: if hasattr(result_dict, 'walltime') and hasattr(result_dict.walltime, '__len__') and len(result_dict.walltime) > 0: @@ -509,10 +506,10 @@ def report(self, source, timeout, return_code, results, output, pid=None): f = result_dict.failures if f: fail_msg = "%s failed" % (count_noun(f, "doctest")) - if the_baseline_stats.get('failed', False): + if baseline.get('failed', False): fail_msg += " [failed in baseline]" postscript['lines'].append(cmd + " # %s" % fail_msg) - if not the_baseline_stats.get('failed', False): + if not baseline.get('failed', False): self.error_status |= 1 if f or result_dict.err == 'tab': stats[basename] = {"failed": True, "walltime": wall, "ntests": ntests} From 60f056c2dd7567a7d30d083afa04c5e5ba9fad80 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Wed, 20 Dec 2023 20:45:27 -0800 Subject: [PATCH 3/8] src/sage/doctest/control.py: Fix doctest --- src/sage/doctest/control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/sage/doctest/control.py b/src/sage/doctest/control.py index 37562e1717c..96a73f600b2 100644 --- a/src/sage/doctest/control.py +++ b/src/sage/doctest/control.py @@ -1117,6 +1117,7 @@ def source_baseline(self, source): sage: filename = os.path.join(SAGE_SRC,'sage','doctest','util.py') sage: DD = DocTestDefaults() sage: DC = DocTestController(DD, [filename]) + sage: DC.expand_files_into_sources() sage: DC.source_baseline(DC.sources[0]) {} """ From 3b9eaa433a0e743c66f417c10b80f0b3af289d9e Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Wed, 20 Dec 2023 21:13:05 -0800 Subject: [PATCH 4/8] src/sage/doctest/control.py: Log 'Using --baseline-stats-path=...' --- src/sage/doctest/control.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/sage/doctest/control.py b/src/sage/doctest/control.py index 96a73f600b2..1a128983187 100644 --- a/src/sage/doctest/control.py +++ b/src/sage/doctest/control.py @@ -1171,6 +1171,8 @@ def run_doctests(self): iterations = ", ".join(iterations) if iterations: iterations = " (%s)" % (iterations) + if self.baseline_stats: + self.log(f"Using --baseline-stats-path={self.options.baseline_stats_path}") self.log("Doctesting %s%s%s." % (filestr, threads, iterations)) self.reporter = DocTestReporter(self) self.dispatcher = DocTestDispatcher(self) From 235824bae52611c4e41e7d94c87f2146672b51ba Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Thu, 21 Dec 2023 10:48:40 -0800 Subject: [PATCH 5/8] src/bin/sage-runtests: Generalize argparse hack --- src/bin/sage-runtests | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/bin/sage-runtests b/src/bin/sage-runtests index e25cebb48a8..4a8b05cfab6 100755 --- a/src/bin/sage-runtests +++ b/src/bin/sage-runtests @@ -141,7 +141,8 @@ if __name__ == "__main__": in_filenames = True new_arguments.append('--') new_arguments.append(arg) - afterlog = bool(arg == '--logfile') + afterlog = arg in ['--logfile', '--stats_path', '--stats-path', + '--baseline_stats_path', '--baseline-stats-path'] args = parser.parse_args(new_arguments) From 719f8d0d2281608c3794afd1fec0b6a24a146766 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Thu, 21 Dec 2023 11:23:44 -0800 Subject: [PATCH 6/8] DocTestReporter.report_head: Move all printing of 'failed in baseline' here --- src/sage/doctest/forker.py | 4 ---- src/sage/doctest/reporting.py | 37 ++++++++++++++++++++--------------- 2 files changed, 21 insertions(+), 20 deletions(-) diff --git a/src/sage/doctest/forker.py b/src/sage/doctest/forker.py index a8b3a7be5aa..ea0d49e0e4e 100644 --- a/src/sage/doctest/forker.py +++ b/src/sage/doctest/forker.py @@ -1725,8 +1725,6 @@ def serial_dispatch(self): for source in self.controller.sources: heading = self.controller.reporter.report_head(source) baseline = self.controller.source_baseline(source) - if baseline.get('failed', False): - heading += " # [failed in baseline]" if not self.controller.options.only_errors: self.controller.log(heading) @@ -1997,8 +1995,6 @@ def sel_exit(): worker_options.target_walltime = (target_endtime - now) / (max(1, pending_tests / opt.nthreads)) w = DocTestWorker(source, options=worker_options, funclist=[sel_exit], baseline=baseline) heading = self.controller.reporter.report_head(w.source) - if baseline.get('failed', False): - heading += " # [failed in baseline]" if not self.controller.options.only_errors: w.messages = heading + "\n" # Store length of heading to detect if the diff --git a/src/sage/doctest/reporting.py b/src/sage/doctest/reporting.py index dcc0dee792c..42cc3b6d85e 100644 --- a/src/sage/doctest/reporting.py +++ b/src/sage/doctest/reporting.py @@ -162,14 +162,16 @@ def were_doctests_with_optional_tag_run(self, tag): return True return False - def report_head(self, source): + def report_head(self, source, fail_msg=None): """ - Return the "sage -t [options] file.py" line as string. + Return the ``sage -t [options] file.py`` line as string. INPUT: - ``source`` -- a source from :mod:`sage.doctest.sources` + - ``fail_msg`` -- ``None`` or a string + EXAMPLES:: sage: from sage.doctest.reporting import DocTestReporter @@ -190,6 +192,8 @@ def report_head(self, source): sage: DD.long = True sage: print(DTR.report_head(FDS)) sage -t --long .../sage/doctest/reporting.py + sage: print(DTR.report_head(FDS, "Failed by self-sabotage")) + sage -t --long .../sage/doctest/reporting.py # Failed by self-sabotage """ cmd = "sage -t" if self.controller.options.long: @@ -206,6 +210,13 @@ def report_head(self, source): if environment != "sage.repl.ipython_kernel.all_jupyter": cmd += f" --environment={environment}" cmd += " " + source.printpath + baseline = self.controller.source_baseline(source) + if fail_msg: + cmd += " # " + fail_msg + if baseline.get('failed', False): + if not fail_msg: + cmd += " #" + cmd += " [failed in baseline]" return cmd def report(self, source, timeout, return_code, results, output, pid=None): @@ -420,12 +431,10 @@ def report(self, source, timeout, return_code, results, output, pid=None): fail_msg += " (and interrupt failed)" else: fail_msg += " (with %s after interrupt)" % signal_name(sig) - if baseline.get('failed', False): - fail_msg += " [failed in baseline]" log(" %s\n%s\nTests run before %s timed out:" % (fail_msg, "*"*70, process_name)) log(output) log("*"*70) - postscript['lines'].append(cmd + " # %s" % fail_msg) + postscript['lines'].append(self.report_head(source, fail_msg)) stats[basename] = {"failed": True, "walltime": 1e6, "ntests": ntests} if not baseline.get('failed', False): self.error_status |= 4 @@ -436,12 +445,10 @@ def report(self, source, timeout, return_code, results, output, pid=None): fail_msg = "Killed due to %s" % signal_name(-return_code) if ntests > 0: fail_msg += " after testing finished" - if baseline.get('failed', False): - fail_msg += " [failed in baseline]" log(" %s\n%s\nTests run before %s failed:" % (fail_msg,"*"*70, process_name)) log(output) log("*"*70) - postscript['lines'].append(cmd + " # %s" % fail_msg) + postscript['lines'].append(self.report_head(source, fail_msg)) stats[basename] = {"failed": True, "walltime": 1e6, "ntests": ntests} if not baseline.get('failed', False): self.error_status |= (8 if return_code > 0 else 16) @@ -458,13 +465,13 @@ def report(self, source, timeout, return_code, results, output, pid=None): log(" Error in doctesting framework (bad result returned)\n%s\nTests run before error:" % ("*"*70)) log(output) log("*"*70) - postscript['lines'].append(cmd + " # Testing error: bad result") + postscript['lines'].append(self.report_head(source, "Testing error: bad result")) self.error_status |= 64 elif result_dict.err == 'noresult': log(" Error in doctesting framework (no result returned)\n%s\nTests run before error:" % ("*"*70)) log(output) log("*"*70) - postscript['lines'].append(cmd + " # Testing error: no result") + postscript['lines'].append(self.report_head(source, "Testing error: no result")) self.error_status |= 64 elif result_dict.err == 'tab': if len(result_dict.tab_linenos) > 5: @@ -473,11 +480,11 @@ def report(self, source, timeout, return_code, results, output, pid=None): if len(result_dict.tab_linenos) > 1: tabs = "s" + tabs log(" Error: TAB character found at line%s" % (tabs)) - postscript['lines'].append(cmd + " # Tab character found") + postscript['lines'].append(self.report_head(source, "Tab character found")) self.error_status |= 32 elif result_dict.err == 'line_number': log(" Error: Source line number found") - postscript['lines'].append(cmd + " # Source line number found") + postscript['lines'].append(self.report_head(source, "Source line number found")) self.error_status |= 256 elif result_dict.err is not None: # This case should not occur @@ -494,7 +501,7 @@ def report(self, source, timeout, return_code, results, output, pid=None): if output: log("Tests run before doctest exception:\n" + output) log("*"*70) - postscript['lines'].append(cmd + " # %s" % fail_msg) + postscript['lines'].append(self.report_head(source, fail_msg)) if hasattr(result_dict, 'tb'): log(result_dict.tb) if hasattr(result_dict, 'walltime'): @@ -506,9 +513,7 @@ def report(self, source, timeout, return_code, results, output, pid=None): f = result_dict.failures if f: fail_msg = "%s failed" % (count_noun(f, "doctest")) - if baseline.get('failed', False): - fail_msg += " [failed in baseline]" - postscript['lines'].append(cmd + " # %s" % fail_msg) + postscript['lines'].append(self.report_head(source, fail_msg)) if not baseline.get('failed', False): self.error_status |= 1 if f or result_dict.err == 'tab': From 19f64d4c8ba768a3c59cb2912f2b981910216fe6 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Fri, 22 Dec 2023 21:31:22 -0800 Subject: [PATCH 7/8] src/sage/doctest/reporting.py: Make 'AlarmInterrupt in doctesting framework' baseline-aware --- src/sage/doctest/reporting.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sage/doctest/reporting.py b/src/sage/doctest/reporting.py index 42cc3b6d85e..cda0e8a3f20 100644 --- a/src/sage/doctest/reporting.py +++ b/src/sage/doctest/reporting.py @@ -508,7 +508,8 @@ def report(self, source, timeout, return_code, results, output, pid=None): stats[basename] = {"failed": True, "walltime": wall, "ntests": ntests} else: stats[basename] = {"failed": True, "walltime": 1e6, "ntests": ntests} - self.error_status |= 64 + if not baseline.get('failed', False): # e.g. AlarmInterrupt in doctesting framework + self.error_status |= 64 if result_dict.err is None or result_dict.err == 'tab': f = result_dict.failures if f: From cf9024f816ce3857ca6b064a8e117fd3306a57b5 Mon Sep 17 00:00:00 2001 From: Matthias Koeppe Date: Tue, 26 Dec 2023 18:14:42 -0800 Subject: [PATCH 8/8] src/sage/doctest/reporting.py: Improve comment --- src/sage/doctest/reporting.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/sage/doctest/reporting.py b/src/sage/doctest/reporting.py index cda0e8a3f20..993ebeaa7a1 100644 --- a/src/sage/doctest/reporting.py +++ b/src/sage/doctest/reporting.py @@ -508,7 +508,11 @@ def report(self, source, timeout, return_code, results, output, pid=None): stats[basename] = {"failed": True, "walltime": wall, "ntests": ntests} else: stats[basename] = {"failed": True, "walltime": 1e6, "ntests": ntests} - if not baseline.get('failed', False): # e.g. AlarmInterrupt in doctesting framework + # This codepath is triggered by doctests that test some timeout + # ("AlarmInterrupt in doctesting framework") or other signal handling + # behavior. This is why we handle the baseline in this codepath, + # in contrast to other "Error in doctesting framework" codepaths. + if not baseline.get('failed', False): self.error_status |= 64 if result_dict.err is None or result_dict.err == 'tab': f = result_dict.failures