Skip to content

Commit

Permalink
sagemathgh-36936: src/sage/doctest/forker.py: Show '# [failed in ba…
Browse files Browse the repository at this point in the history
…seline]' 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 sagemath#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:
- sagemath#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 sagemath#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
- sagemath#12345: short description why this is a dependency
- sagemath#34567: ...
-->

<!-- If you're unsure about any of these, don't hesitate to ask. We're
here to help! -->
    
URL: sagemath#36936
Reported by: Matthias Köppe
Reviewer(s): Kwankyu Lee, Matthias Köppe
  • Loading branch information
Release Manager committed Dec 27, 2023
2 parents 2c46ca9 + cf9024f commit 39cc971
Show file tree
Hide file tree
Showing 4 changed files with 85 additions and 32 deletions.
3 changes: 2 additions & 1 deletion src/bin/sage-runtests
Original file line number Diff line number Diff line change
Expand Up @@ -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)

Expand Down
31 changes: 31 additions & 0 deletions src/sage/doctest/control.py
Original file line number Diff line number Diff line change
Expand Up @@ -1097,6 +1097,35 @@ 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.expand_files_into_sources()
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.
Expand Down Expand Up @@ -1142,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)
Expand Down
28 changes: 21 additions & 7 deletions src/sage/doctest/forker.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -522,6 +522,8 @@ def __init__(self, *args, **kwds):
- ``optionflags`` -- Controls the comparison with the expected
output. See :mod:`testmod` for more information.
- ``baseline`` -- dictionary, the ``baseline_stats`` value
EXAMPLES::
sage: from sage.doctest.parsing import SageOutputChecker
Expand All @@ -535,6 +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', {})
doctest.DocTestRunner.__init__(self, *args, **kwds)
self._fakeout = SageSpoofInOut(O)
if self.msgfile is None:
Expand Down Expand Up @@ -1721,12 +1724,14 @@ def serial_dispatch(self):
"""
for source in self.controller.sources:
heading = self.controller.reporter.report_head(source)
baseline = self.controller.source_baseline(source)
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=baseline)
outtmpfile.seek(0)
output = bytes_to_str(outtmpfile.read())

Expand Down Expand Up @@ -1985,9 +1990,10 @@ def sel_exit():
# Start a new worker.
import copy
worker_options = copy.copy(opt)
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, funclist=[sel_exit])
w = DocTestWorker(source, options=worker_options, funclist=[sel_exit], baseline=baseline)
heading = self.controller.reporter.report_head(w.source)
if not self.controller.options.only_errors:
w.messages = heading + "\n"
Expand Down Expand Up @@ -2128,6 +2134,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
Expand All @@ -2147,7 +2155,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.
Expand All @@ -2171,6 +2179,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!
Expand Down Expand Up @@ -2242,7 +2251,8 @@ 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()
Expand Down Expand Up @@ -2508,7 +2518,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.
Expand All @@ -2525,6 +2536,8 @@ 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`` -- a dictionary, the ``baseline_stats`` value.
OUTPUT:
- ``(doctests, result_dict)`` where ``doctests`` is the number of
Expand Down Expand Up @@ -2560,7 +2573,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
Expand Down
55 changes: 31 additions & 24 deletions src/sage/doctest/reporting.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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:
Expand All @@ -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):
Expand Down Expand Up @@ -399,10 +410,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
Expand All @@ -423,14 +431,12 @@ 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):
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 the_baseline_stats.get('failed', False):
if not baseline.get('failed', False):
self.error_status |= 4
elif return_code:
if return_code > 0:
Expand All @@ -439,14 +445,12 @@ 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):
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 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:
Expand All @@ -461,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:
Expand All @@ -476,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
Expand All @@ -497,22 +501,25 @@ 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'):
stats[basename] = {"failed": True, "walltime": wall, "ntests": ntests}
else:
stats[basename] = {"failed": True, "walltime": 1e6, "ntests": ntests}
self.error_status |= 64
# 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
if f:
fail_msg = "%s failed" % (count_noun(f, "doctest"))
if the_baseline_stats.get('failed', False):
fail_msg += " [failed in baseline]"
postscript['lines'].append(cmd + " # %s" % fail_msg)
if not the_baseline_stats.get('failed', False):
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':
stats[basename] = {"failed": True, "walltime": wall, "ntests": ntests}
Expand Down

0 comments on commit 39cc971

Please sign in to comment.