Skip to content

Commit 8e8c4e4

Browse files
author
Release Manager
committed
gh-36936: `src/sage/doctest/forker.py`: Show '# [failed in baseline]' earlier <!-- ^^^^^ Please provide a concise, informative and self-explanatory title. Don't put issue numbers in there, do this in the PR body below. For example, instead of "Fixes #1234" use "Introduce new method to calculate 1+1" --> <!-- Describe your changes here in detail --> The note "[failed in baseline]" already appears in the doctest failure summary. Here we add the note also to the `sage -t` line during doctesting: https://github.com/sagemath/sage/actions/runs/7281940815/job/19843412065 ?pr=36936#step:11:2744 ``` sage -t --random-seed=256963700569517996050547927079750684585 src/sage/combinat/combinatorial_map.py [75 tests, 0.05 s] sage -t --random-seed=256963700569517996050547927079750684585 src/sage/combinat/cluster_algebra_quiver/quiver.py # [failed in baseline] [320 tests, 3.73 s] sage -t --random-seed=256963700569517996050547927079750684585 src/sage/combinat/composition_signed.py [20 tests, 0.16 s] ``` https://github.com/sagemath/sage/actions/runs/7281940815/job/19843412065 ?pr=36936#step:11:9192 ``` sage -t --random-seed=256963700569517996050547927079750684585 src/sage_setup/setenv.py [0 tests, 0.00 s] sage -t --random-seed=256963700569517996050547927079750684585 src/sage_setup/clean.py # [failed in baseline] ********************************************************************** File "src/sage_setup/clean.py", line 104, in sage_setup.clean._find_stale_files Failed example: for f in stale_iter: if f.endswith(skip_extensions): continue if '/ext_data/' in f: continue print('Found stale file: ' + f) Expected nothing Got: Found stale file: sage/tests/books/judson-abstract- algebra/homomorph-sage-exercises.py Found stale file: sage/tests/books/judson-abstract-algebra/actions- sage-exercises.py Found stale file: sage/tests/books/judson-abstract- algebra/homomorph-sage.py ``` The changes in the code that implement this are also preparation for: - #36558. <!-- Why is this change required? What problem does it solve? --> <!-- If this PR resolves an open issue, please link to it here. For example "Fixes #12345". --> <!-- If your change requires a documentation PR, please link it appropriately. --> ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> <!-- If your change requires a documentation PR, please link it appropriately --> <!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> <!-- Feel free to remove irrelevant items. --> - [ ] The title is concise, informative, and self-explanatory. - [ ] The description explains in detail what this PR is about. - [ ] I have linked a relevant issue or discussion. - [ ] I have created tests covering the changes. - [ ] I have updated the documentation accordingly. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on - #12345: short description why this is a dependency - #34567: ... --> <!-- If you're unsure about any of these, don't hesitate to ask. We're here to help! --> URL: #36936 Reported by: Matthias Köppe Reviewer(s): Kwankyu Lee, Matthias Köppe
2 parents 6df3377 + cf9024f commit 8e8c4e4

File tree

4 files changed

+85
-32
lines changed

4 files changed

+85
-32
lines changed

src/bin/sage-runtests

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -141,7 +141,8 @@ if __name__ == "__main__":
141141
in_filenames = True
142142
new_arguments.append('--')
143143
new_arguments.append(arg)
144-
afterlog = bool(arg == '--logfile')
144+
afterlog = arg in ['--logfile', '--stats_path', '--stats-path',
145+
'--baseline_stats_path', '--baseline-stats-path']
145146

146147
args = parser.parse_args(new_arguments)
147148

src/sage/doctest/control.py

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1097,6 +1097,35 @@ def sort_key(source):
10971097
return -self.stats.get(basename, default).get('walltime', 0), basename
10981098
self.sources = sorted(self.sources, key=sort_key)
10991099

1100+
def source_baseline(self, source):
1101+
r"""
1102+
Return the ``baseline_stats`` value of ``source``.
1103+
1104+
INPUT:
1105+
1106+
- ``source`` -- a :class:`DocTestSource` instance
1107+
1108+
OUTPUT:
1109+
1110+
A dictionary.
1111+
1112+
EXAMPLES::
1113+
1114+
sage: from sage.doctest.control import DocTestDefaults, DocTestController
1115+
sage: from sage.env import SAGE_SRC
1116+
sage: import os
1117+
sage: filename = os.path.join(SAGE_SRC,'sage','doctest','util.py')
1118+
sage: DD = DocTestDefaults()
1119+
sage: DC = DocTestController(DD, [filename])
1120+
sage: DC.expand_files_into_sources()
1121+
sage: DC.source_baseline(DC.sources[0])
1122+
{}
1123+
"""
1124+
if self.baseline_stats:
1125+
basename = source.basename
1126+
return self.baseline_stats.get(basename, {})
1127+
return {}
1128+
11001129
def run_doctests(self):
11011130
"""
11021131
Actually runs the doctests.
@@ -1142,6 +1171,8 @@ def run_doctests(self):
11421171
iterations = ", ".join(iterations)
11431172
if iterations:
11441173
iterations = " (%s)" % (iterations)
1174+
if self.baseline_stats:
1175+
self.log(f"Using --baseline-stats-path={self.options.baseline_stats_path}")
11451176
self.log("Doctesting %s%s%s." % (filestr, threads, iterations))
11461177
self.reporter = DocTestReporter(self)
11471178
self.dispatcher = DocTestDispatcher(self)

src/sage/doctest/forker.py

Lines changed: 21 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -513,7 +513,7 @@ def __init__(self, *args, **kwds):
513513
514514
- ``stdout`` -- an open file to restore for debugging
515515
516-
- ``checker`` -- None, or an instance of
516+
- ``checker`` -- ``None``, or an instance of
517517
:class:`doctest.OutputChecker`
518518
519519
- ``verbose`` -- boolean, determines whether verbose printing
@@ -522,6 +522,8 @@ def __init__(self, *args, **kwds):
522522
- ``optionflags`` -- Controls the comparison with the expected
523523
output. See :mod:`testmod` for more information.
524524
525+
- ``baseline`` -- dictionary, the ``baseline_stats`` value
526+
525527
EXAMPLES::
526528
527529
sage: from sage.doctest.parsing import SageOutputChecker
@@ -535,6 +537,7 @@ def __init__(self, *args, **kwds):
535537
O = kwds.pop('outtmpfile', None)
536538
self.msgfile = kwds.pop('msgfile', None)
537539
self.options = kwds.pop('sage_options')
540+
self.baseline = kwds.pop('baseline', {})
538541
doctest.DocTestRunner.__init__(self, *args, **kwds)
539542
self._fakeout = SageSpoofInOut(O)
540543
if self.msgfile is None:
@@ -1721,12 +1724,14 @@ def serial_dispatch(self):
17211724
"""
17221725
for source in self.controller.sources:
17231726
heading = self.controller.reporter.report_head(source)
1727+
baseline = self.controller.source_baseline(source)
17241728
if not self.controller.options.only_errors:
17251729
self.controller.log(heading)
17261730

17271731
with tempfile.TemporaryFile() as outtmpfile:
17281732
result = DocTestTask(source)(self.controller.options,
1729-
outtmpfile, self.controller.logger)
1733+
outtmpfile, self.controller.logger,
1734+
baseline=baseline)
17301735
outtmpfile.seek(0)
17311736
output = bytes_to_str(outtmpfile.read())
17321737

@@ -1985,9 +1990,10 @@ def sel_exit():
19851990
# Start a new worker.
19861991
import copy
19871992
worker_options = copy.copy(opt)
1993+
baseline = self.controller.source_baseline(source)
19881994
if target_endtime is not None:
19891995
worker_options.target_walltime = (target_endtime - now) / (max(1, pending_tests / opt.nthreads))
1990-
w = DocTestWorker(source, options=worker_options, funclist=[sel_exit])
1996+
w = DocTestWorker(source, options=worker_options, funclist=[sel_exit], baseline=baseline)
19911997
heading = self.controller.reporter.report_head(w.source)
19921998
if not self.controller.options.only_errors:
19931999
w.messages = heading + "\n"
@@ -2128,6 +2134,8 @@ class should be accessed by the child process.
21282134
- ``funclist`` -- a list of callables to be called at the start of
21292135
the child process.
21302136
2137+
- ``baseline`` -- dictionary, the ``baseline_stats`` value
2138+
21312139
EXAMPLES::
21322140
21332141
sage: from sage.doctest.forker import DocTestWorker, DocTestTask
@@ -2147,7 +2155,7 @@ class should be accessed by the child process.
21472155
sage: reporter.report(FDS, False, W.exitcode, result, "")
21482156
[... tests, ... s]
21492157
"""
2150-
def __init__(self, source, options, funclist=[]):
2158+
def __init__(self, source, options, funclist=[], baseline=None):
21512159
"""
21522160
Initialization.
21532161
@@ -2171,6 +2179,7 @@ def __init__(self, source, options, funclist=[]):
21712179
self.source = source
21722180
self.options = options
21732181
self.funclist = funclist
2182+
self.baseline = baseline
21742183

21752184
# Open pipe for messages. These are raw file descriptors,
21762185
# not Python file objects!
@@ -2242,7 +2251,8 @@ def run(self):
22422251
os.close(self.rmessages)
22432252
msgpipe = os.fdopen(self.wmessages, "w")
22442253
try:
2245-
task(self.options, self.outtmpfile, msgpipe, self.result_queue)
2254+
task(self.options, self.outtmpfile, msgpipe, self.result_queue,
2255+
baseline=self.baseline)
22462256
finally:
22472257
msgpipe.close()
22482258
self.outtmpfile.close()
@@ -2508,7 +2518,8 @@ def __init__(self, source):
25082518
"""
25092519
self.source = source
25102520

2511-
def __call__(self, options, outtmpfile=None, msgfile=None, result_queue=None):
2521+
def __call__(self, options, outtmpfile=None, msgfile=None, result_queue=None, *,
2522+
baseline=None):
25122523
"""
25132524
Calling the task does the actual work of running the doctests.
25142525
@@ -2525,6 +2536,8 @@ def __call__(self, options, outtmpfile=None, msgfile=None, result_queue=None):
25252536
- ``result_queue`` -- an instance of :class:`multiprocessing.Queue`
25262537
to store the doctest result. For testing, this can also be None.
25272538
2539+
- ``baseline`` -- a dictionary, the ``baseline_stats`` value.
2540+
25282541
OUTPUT:
25292542
25302543
- ``(doctests, result_dict)`` where ``doctests`` is the number of
@@ -2560,7 +2573,8 @@ def __call__(self, options, outtmpfile=None, msgfile=None, result_queue=None):
25602573
outtmpfile=outtmpfile,
25612574
msgfile=msgfile,
25622575
sage_options=options,
2563-
optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS)
2576+
optionflags=doctest.NORMALIZE_WHITESPACE | doctest.ELLIPSIS,
2577+
baseline=baseline)
25642578
runner.basename = self.source.basename
25652579
runner.filename = self.source.path
25662580
N = options.file_iterations

src/sage/doctest/reporting.py

Lines changed: 31 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -162,14 +162,16 @@ def were_doctests_with_optional_tag_run(self, tag):
162162
return True
163163
return False
164164

165-
def report_head(self, source):
165+
def report_head(self, source, fail_msg=None):
166166
"""
167-
Return the "sage -t [options] file.py" line as string.
167+
Return the ``sage -t [options] file.py`` line as string.
168168
169169
INPUT:
170170
171171
- ``source`` -- a source from :mod:`sage.doctest.sources`
172172
173+
- ``fail_msg`` -- ``None`` or a string
174+
173175
EXAMPLES::
174176
175177
sage: from sage.doctest.reporting import DocTestReporter
@@ -190,6 +192,8 @@ def report_head(self, source):
190192
sage: DD.long = True
191193
sage: print(DTR.report_head(FDS))
192194
sage -t --long .../sage/doctest/reporting.py
195+
sage: print(DTR.report_head(FDS, "Failed by self-sabotage"))
196+
sage -t --long .../sage/doctest/reporting.py # Failed by self-sabotage
193197
"""
194198
cmd = "sage -t"
195199
if self.controller.options.long:
@@ -206,6 +210,13 @@ def report_head(self, source):
206210
if environment != "sage.repl.ipython_kernel.all_jupyter":
207211
cmd += f" --environment={environment}"
208212
cmd += " " + source.printpath
213+
baseline = self.controller.source_baseline(source)
214+
if fail_msg:
215+
cmd += " # " + fail_msg
216+
if baseline.get('failed', False):
217+
if not fail_msg:
218+
cmd += " #"
219+
cmd += " [failed in baseline]"
209220
return cmd
210221

211222
def report(self, source, timeout, return_code, results, output, pid=None):
@@ -399,10 +410,7 @@ def report(self, source, timeout, return_code, results, output, pid=None):
399410
postscript = self.postscript
400411
stats = self.stats
401412
basename = source.basename
402-
if self.controller.baseline_stats:
403-
the_baseline_stats = self.controller.baseline_stats.get(basename, {})
404-
else:
405-
the_baseline_stats = {}
413+
baseline = self.controller.source_baseline(source)
406414
cmd = self.report_head(source)
407415
try:
408416
ntests, result_dict = results
@@ -423,14 +431,12 @@ def report(self, source, timeout, return_code, results, output, pid=None):
423431
fail_msg += " (and interrupt failed)"
424432
else:
425433
fail_msg += " (with %s after interrupt)" % signal_name(sig)
426-
if the_baseline_stats.get('failed', False):
427-
fail_msg += " [failed in baseline]"
428434
log(" %s\n%s\nTests run before %s timed out:" % (fail_msg, "*"*70, process_name))
429435
log(output)
430436
log("*"*70)
431-
postscript['lines'].append(cmd + " # %s" % fail_msg)
437+
postscript['lines'].append(self.report_head(source, fail_msg))
432438
stats[basename] = {"failed": True, "walltime": 1e6, "ntests": ntests}
433-
if not the_baseline_stats.get('failed', False):
439+
if not baseline.get('failed', False):
434440
self.error_status |= 4
435441
elif return_code:
436442
if return_code > 0:
@@ -439,14 +445,12 @@ def report(self, source, timeout, return_code, results, output, pid=None):
439445
fail_msg = "Killed due to %s" % signal_name(-return_code)
440446
if ntests > 0:
441447
fail_msg += " after testing finished"
442-
if the_baseline_stats.get('failed', False):
443-
fail_msg += " [failed in baseline]"
444448
log(" %s\n%s\nTests run before %s failed:" % (fail_msg,"*"*70, process_name))
445449
log(output)
446450
log("*"*70)
447-
postscript['lines'].append(cmd + " # %s" % fail_msg)
451+
postscript['lines'].append(self.report_head(source, fail_msg))
448452
stats[basename] = {"failed": True, "walltime": 1e6, "ntests": ntests}
449-
if not the_baseline_stats.get('failed', False):
453+
if not baseline.get('failed', False):
450454
self.error_status |= (8 if return_code > 0 else 16)
451455
else:
452456
if hasattr(result_dict, 'walltime') and hasattr(result_dict.walltime, '__len__') and len(result_dict.walltime) > 0:
@@ -461,13 +465,13 @@ def report(self, source, timeout, return_code, results, output, pid=None):
461465
log(" Error in doctesting framework (bad result returned)\n%s\nTests run before error:" % ("*"*70))
462466
log(output)
463467
log("*"*70)
464-
postscript['lines'].append(cmd + " # Testing error: bad result")
468+
postscript['lines'].append(self.report_head(source, "Testing error: bad result"))
465469
self.error_status |= 64
466470
elif result_dict.err == 'noresult':
467471
log(" Error in doctesting framework (no result returned)\n%s\nTests run before error:" % ("*"*70))
468472
log(output)
469473
log("*"*70)
470-
postscript['lines'].append(cmd + " # Testing error: no result")
474+
postscript['lines'].append(self.report_head(source, "Testing error: no result"))
471475
self.error_status |= 64
472476
elif result_dict.err == 'tab':
473477
if len(result_dict.tab_linenos) > 5:
@@ -476,11 +480,11 @@ def report(self, source, timeout, return_code, results, output, pid=None):
476480
if len(result_dict.tab_linenos) > 1:
477481
tabs = "s" + tabs
478482
log(" Error: TAB character found at line%s" % (tabs))
479-
postscript['lines'].append(cmd + " # Tab character found")
483+
postscript['lines'].append(self.report_head(source, "Tab character found"))
480484
self.error_status |= 32
481485
elif result_dict.err == 'line_number':
482486
log(" Error: Source line number found")
483-
postscript['lines'].append(cmd + " # Source line number found")
487+
postscript['lines'].append(self.report_head(source, "Source line number found"))
484488
self.error_status |= 256
485489
elif result_dict.err is not None:
486490
# This case should not occur
@@ -497,22 +501,25 @@ def report(self, source, timeout, return_code, results, output, pid=None):
497501
if output:
498502
log("Tests run before doctest exception:\n" + output)
499503
log("*"*70)
500-
postscript['lines'].append(cmd + " # %s" % fail_msg)
504+
postscript['lines'].append(self.report_head(source, fail_msg))
501505
if hasattr(result_dict, 'tb'):
502506
log(result_dict.tb)
503507
if hasattr(result_dict, 'walltime'):
504508
stats[basename] = {"failed": True, "walltime": wall, "ntests": ntests}
505509
else:
506510
stats[basename] = {"failed": True, "walltime": 1e6, "ntests": ntests}
507-
self.error_status |= 64
511+
# This codepath is triggered by doctests that test some timeout
512+
# ("AlarmInterrupt in doctesting framework") or other signal handling
513+
# behavior. This is why we handle the baseline in this codepath,
514+
# in contrast to other "Error in doctesting framework" codepaths.
515+
if not baseline.get('failed', False):
516+
self.error_status |= 64
508517
if result_dict.err is None or result_dict.err == 'tab':
509518
f = result_dict.failures
510519
if f:
511520
fail_msg = "%s failed" % (count_noun(f, "doctest"))
512-
if the_baseline_stats.get('failed', False):
513-
fail_msg += " [failed in baseline]"
514-
postscript['lines'].append(cmd + " # %s" % fail_msg)
515-
if not the_baseline_stats.get('failed', False):
521+
postscript['lines'].append(self.report_head(source, fail_msg))
522+
if not baseline.get('failed', False):
516523
self.error_status |= 1
517524
if f or result_dict.err == 'tab':
518525
stats[basename] = {"failed": True, "walltime": wall, "ntests": ntests}

0 commit comments

Comments
 (0)