Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
fa7db2e
Implement # sage.doctest: flaky marker
user202729 Feb 17, 2025
359bd0d
Mark a few files as flaky
user202729 Feb 17, 2025
8375fa5
Increase die_timeout
user202729 Feb 17, 2025
299a76b
Some missing documentation update
user202729 Feb 18, 2025
bd49fd2
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 Feb 22, 2025
6cf82bd
Merge branch 'develop' into flaky-file
user202729 Mar 3, 2025
d5202db
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 Mar 10, 2025
cc22c5a
Also handle flaky segmentation fault etc.
user202729 Mar 18, 2025
48f79d6
Some more markers
user202729 Mar 18, 2025
f8580bb
Fix some bugs
user202729 Mar 20, 2025
e0fc148
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 Mar 27, 2025
7450eab
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 Apr 19, 2025
6657bf8
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 Apr 29, 2025
11b7094
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 May 18, 2025
fc287b9
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 May 19, 2025
4c8c5d4
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 Jun 1, 2025
14acfc6
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 Jun 27, 2025
a478463
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 Jul 7, 2025
d682d70
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 Jul 27, 2025
fe70ed1
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 Aug 2, 2025
3db3e9c
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 Aug 28, 2025
e9b1b35
Merge remote-tracking branch 'upstream/develop' into flaky-file
user202729 Sep 11, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions src/sage/algebras/fusion_rings/fusion_ring.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# sage.doctest: flaky
"""
Fusion Rings
"""
Expand Down
134 changes: 117 additions & 17 deletions src/sage/doctest/forker.py
Original file line number Diff line number Diff line change
Expand Up @@ -1724,6 +1724,21 @@
"""
Create parallel :class:`DocTestWorker` processes and dispatches
doctesting tasks.

.. NOTE::

If this is run directly in the normal Sage command-line,
it calls :func:`init_sage` which in turn calls
:meth:`~sage.repl.rich_output.display_manager.DisplayManager.switch_backend`
to the doctest backend, which is incompatible with the IPython-based
command-line. As such, if an error such as
``TypeError: cannot unpack non-iterable NoneType object`` is seen,
a workaround is to run the following::

sage: # not tested
sage: from sage.repl.rich_output.backend_ipython import BackendIPythonCommandline
sage: backend = BackendIPythonCommandline()
sage: get_ipython().display_formatter.dm.switch_backend(backend, shell=get_ipython())
"""
def __init__(self, controller: DocTestController):
"""
Expand Down Expand Up @@ -1851,6 +1866,67 @@
1 of 1 in ...
[1 test, 1 failure, ...s wall]
Killing test ...

TESTS:

Test flaky files. This test should fail the first time and success the second time::

sage: with NTF(suffix='.py', mode='w+t') as f1:
....: t = walltime()
....: _ = f1.write(f"# sage.doctest: flaky\n'''\nsage: sleep(10 if walltime() < {t+1} else 0)\n'''")
....: f1.flush()
....: DC = DocTestController(DocTestDefaults(timeout=2),
....: [f1.name])
....: DC.expand_files_into_sources()
....: DD = DocTestDispatcher(DC)
....: DR = DocTestReporter(DC)
....: DC.reporter = DR
....: DC.dispatcher = DD
....: DC.timer = Timer().start()
....: DD.parallel_dispatch()
sage -t ...
sage -t ...
[1 test, ...s wall]

This test always fail, so even flaky can't help it::

sage: with NTF(suffix='.py', mode='w+t') as f1:
....: _ = f1.write(f"# sage.doctest: flaky\n'''\nsage: sleep(10)\n'''")
....: f1.flush()
....: DC = DocTestController(DocTestDefaults(timeout=2),
....: [f1.name])
....: DC.expand_files_into_sources()
....: DD = DocTestDispatcher(DC)
....: DR = DocTestReporter(DC)
....: DC.reporter = DR
....: DC.dispatcher = DD
....: DC.timer = Timer().start()
....: DD.parallel_dispatch()
sage -t ...
sage -t ...
Timed out
**********************************************************************
...

Of course without flaky, the test should fail (since it timeouts the first time)::

sage: with NTF(suffix='.py', mode='w+t') as f1:
....: t = walltime()
....: _ = f1.write(f"'''\nsage: sleep(10 if walltime() < {t+1} else 0)\n'''")
....: f1.flush()
....: DC = DocTestController(DocTestDefaults(timeout=2),
....: [f1.name])
....: DC.expand_files_into_sources()
....: DD = DocTestDispatcher(DC)
....: DR = DocTestReporter(DC)
....: DC.reporter = DR
....: DC.dispatcher = DD
....: DC.timer = Timer().start()
....: DD.parallel_dispatch()
sage -t ...
Timed out
**********************************************************************
...
"""
opt = self.controller.options

Expand Down Expand Up @@ -1937,6 +2013,28 @@
# precision.
now = time.time()

def start_new_worker(source: DocTestSource, num_retries_left: typing.Optional[int] = None) -> DocTestWorker:
nonlocal opt, target_endtime, now, pending_tests, sel_exit
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))

Check warning on line 2022 in src/sage/doctest/forker.py

View check run for this annotation

Codecov / codecov/patch

src/sage/doctest/forker.py#L2022

Added line #L2022 was not covered by tests
w = DocTestWorker(source, options=worker_options, funclist=[sel_exit], baseline=baseline)
if num_retries_left is not None:
w.num_retries_left = num_retries_left
elif 'flaky' in source.file_optional_tags:
w.num_retries_left = 1
heading = self.controller.reporter.report_head(w.source)
if not self.controller.options.only_errors:
w.messages = heading + "\n"
# Store length of heading to detect if the
# worker has something interesting to report.
w.heading_len = len(w.messages)
w.start() # This might take some time
w.deadline = time.time() + opt.timeout
return w

# If there were any substantial changes in the state
# (new worker started or finished worker reported),
# restart this while loop instead of calling pselect().
Expand Down Expand Up @@ -1994,6 +2092,14 @@
# Similarly, process finished workers.
new_finished = []
for w in finished:
if w.killed and w.num_retries_left > 0:
# in this case, the messages from w should be suppressed
# (better handling could be implemented later)
if follow is w:
follow = None
workers.append(start_new_worker(w.source, w.num_retries_left - 1))
continue

if opt.exitfirst and w.result[1].failures:
abort_now = True
elif follow is not None and follow is not w:
Expand Down Expand Up @@ -2026,28 +2132,13 @@
while (source_iter is not None and len(workers) < opt.nthreads
and (not job_client or job_client.acquire())):
try:
source = next(source_iter)
source: DocTestSource = next(source_iter)
except StopIteration:
source_iter = None
if job_client:
job_client.release()
else:
# 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], baseline=baseline)
heading = self.controller.reporter.report_head(w.source)
if not self.controller.options.only_errors:
w.messages = heading + "\n"
# Store length of heading to detect if the
# worker has something interesting to report.
w.heading_len = len(w.messages)
w.start() # This might take some time
w.deadline = time.time() + opt.timeout
workers.append(w)
workers.append(start_new_worker(source))
restart = True

# Recompute state if needed
Expand Down Expand Up @@ -2181,6 +2272,7 @@

EXAMPLES::

sage: # long time
sage: from sage.doctest.forker import DocTestWorker, DocTestTask
sage: from sage.doctest.sources import FileDocTestSource
sage: from sage.doctest.reporting import DocTestReporter
Expand Down Expand Up @@ -2223,6 +2315,10 @@
self.funclist = funclist
self.baseline = baseline

# This is not used by this class in any way, but DocTestDispatcher
# uses this to keep track of reruns for flaky tests.
self.num_retries_left = 0

# Open pipe for messages. These are raw file descriptors,
# not Python file objects!
self.rmessages, self.wmessages = os.pipe()
Expand Down Expand Up @@ -2305,6 +2401,7 @@

TESTS::

sage: # long time
sage: from sage.doctest.forker import DocTestWorker, DocTestTask
sage: from sage.doctest.sources import FileDocTestSource
sage: from sage.doctest.reporting import DocTestReporter
Expand Down Expand Up @@ -2344,6 +2441,7 @@

EXAMPLES::

sage: # long time
sage: from sage.doctest.forker import DocTestWorker, DocTestTask
sage: from sage.doctest.sources import FileDocTestSource
sage: from sage.doctest.reporting import DocTestReporter
Expand Down Expand Up @@ -2378,6 +2476,7 @@

EXAMPLES::

sage: # long time
sage: from sage.doctest.forker import DocTestWorker, DocTestTask
sage: from sage.doctest.sources import FileDocTestSource
sage: from sage.doctest.reporting import DocTestReporter
Expand Down Expand Up @@ -2508,6 +2607,7 @@

EXAMPLES::

sage: # long time
sage: from sage.doctest.forker import DocTestTask
sage: from sage.doctest.sources import FileDocTestSource
sage: from sage.doctest.control import DocTestDefaults, DocTestController
Expand Down
34 changes: 33 additions & 1 deletion src/sage/doctest/parsing.py
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@
ansi_escape_sequence = re.compile(r"(\x1b[@-Z\\-~]|\x1b\[.*?[@-~]|\x9b.*?[@-~])")

special_optional_regex = (
"py2|long time|not implemented|not tested|optional|needs|known bug"
"py2|long time|not implemented|not tested|optional|needs|known bug|flaky"
)
tag_with_explanation_regex = r"((?:!?\w|[.])*)\s*(?:\((?P<cmd_explanation>.*?)\))?"
optional_regex = re.compile(
Expand Down Expand Up @@ -97,6 +97,7 @@ def parse_optional_tags(
- ``'not tested'``
- ``'known bug'`` (possible values are ``None``, ``linux`` and ``macos``)
- ``'py2'``
- ``'flaky'``
- ``'optional -- FEATURE...'`` or ``'needs FEATURE...'`` --
the dictionary will just have the key ``'FEATURE'``

Expand Down Expand Up @@ -166,6 +167,17 @@ def parse_optional_tags(
sage: parse_optional_tags("sage: #this is not #needs scipy\n....: import scipy",
....: return_string_sans_tags=True)
({'scipy': None}, 'sage: #this is not \n....: import scipy', False)

TESTS::

sage: parse_optional_tags("# flaky")
{'flaky': None}

Remember to update the documentation above whenever the following changes::

sage: from sage.doctest.parsing import special_optional_regex
sage: special_optional_regex.pattern
'py2|long time|not implemented|not tested|optional|needs|known bug|flaky'
"""
safe, literals, state = strip_string_literals(string)
split = safe.split('\n', 1)
Expand Down Expand Up @@ -260,6 +272,11 @@ def parse_file_optional_tags(lines):
sage: with open(filename, "r") as f:
....: parse_file_optional_tags(enumerate(f))
{'xyz': None}
sage: with open(filename, "w") as f:
....: _ = f.write("# sage.doctest: flaky")
sage: with open(filename, "r") as f:
....: parse_file_optional_tags(enumerate(f))
{'flaky': None}
"""
tags = {}
for line_count, line in lines:
Expand Down Expand Up @@ -990,6 +1007,14 @@ def parse(self, string, *args):
'C.minimum_distance(algorithm="guava") # optional - guava\n'
sage: dte.want
'...\n24\n'

Test putting flaky tag in a single test::

sage: example5 = 'sage: 1 # flaky\n1'
sage: parsed5 = DTP.parse(example5)
Traceback (most recent call last):
...
NotImplementedError: 'flaky' tag is only implemented for whole file, not for single test
"""
# Regular expressions
find_sage_prompt = re.compile(r"^(\s*)sage: ", re.M)
Expand Down Expand Up @@ -1063,6 +1088,8 @@ def check_and_clear_tag_counts():
for item in res:
if isinstance(item, doctest.Example):
optional_tags_with_values, _, is_persistent = parse_optional_tags(item.source, return_string_sans_tags=True)
if "flaky" in optional_tags_with_values:
raise NotImplementedError("'flaky' tag is only implemented for whole file, not for single test")
optional_tags = set(optional_tags_with_values)
if is_persistent:
check_and_clear_tag_counts()
Expand All @@ -1087,6 +1114,11 @@ def check_and_clear_tag_counts():
('not tested' in optional_tags)):
continue

if 'flaky' in optional_tags:
# since a single test cannot have the 'flaky' tag,
# this must come from file_optional_tags
optional_tags.remove('flaky')

if 'long time' in optional_tags:
if self.long:
optional_tags.remove('long time')
Expand Down
1 change: 1 addition & 0 deletions src/sage/rings/polynomial/multi_polynomial_libsingular.pyx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# sage.doctest: flaky
r"""
Multivariate Polynomials via libSINGULAR

Expand Down
1 change: 1 addition & 0 deletions src/sage/rings/polynomial/polynomial_element.pyx
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
# sage.doctest: flaky
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

From the log it looks like this was correctly retried once. But it still timeout…? (what's the probability this fails twice?)

"""
Univariate polynomial base class

Expand Down Expand Up @@ -2585,7 +2586,7 @@
sage: x = polygen(K)
sage: pol = x^1000000 + x + a
sage: from sage.doctest.util import ensure_interruptible_after
sage: with ensure_interruptible_after(0.5): pol.any_root()

Check failure on line 2589 in src/sage/rings/polynomial/polynomial_element.pyx

View workflow job for this annotation

GitHub Actions / test-long (src/sage/[p-z]*)

Timed out

/sage/local/var/lib/sage/venv-python3.10/lib/python3.10/site-packages/cysignals/signals.cpython-310-x86_64-linux-gnu.so(+0x93d4)[0x7f9d34c373d4] /sage/local/var/lib/sage/venv-python3.10/lib/python3.10/site-packages/cysignals/signals.cpython-310-x86_64-linux-gnu.so(+0x9496)[0x7f9d34c37496] /sage/local/var/lib/sage/venv-python3.10/lib/python3.10/site-packages/cysignals/signals.cpython-310-x86_64-linux-gnu.so(+0xc881)[0x7f9d34c3a881] /lib/x86_64-linux-gnu/libc.so.6(+0x42520)[0x7f9d361b6520] /lib/x86_64-linux-gnu/libc.so.6(+0x91117)[0x7f9d36205117] /lib/x86_64-linux-gnu/libc.so.6(+0x9cc78)[0x7f9d36210c78] python3(PyThread_acquire_lock_timed+0xa0)[0x55904308bdd0] python3(+0x1a0a64)[0x5590430dfa64] python3(+0x18bde7)[0x5590430cade7] python3(_PyEval_EvalFrameDefault+0x8af)[0x5590430b497f] python3(+0x198731)[0x5590430d7731] python3(+0x18a7c5)[0x5590430c97c5] python3(PyObject_CallFunctionObjArgs+0xa3)[0x5590430c9683] /sage/local/var/lib/sage/venv-python3.10/lib/python3.10/site-packages/coverage/tracer.cpython-310-x86_64-linux-gnu.so(+0x337e)[0x7f9d35e9f37e] python3(+0x232bf7)[0x559043171bf7] python3(_PyEval_EvalFrameDefault+0x665e)[0x5590430ba72e] python3(+0x1a73d0)[0x5590430e63d0] python3(+0x277f5f)[0x5590431b6f5f] python3(+0x2c3d41)[0x559043202d41] python3(+0x2c3b6f)[0x559043202b6f] python3(+0x18ab58)[0x5590430c9b58] python3(_PyObject_MakeTpCall+0x25b)[0x5590430c039b] python3(+0x22cbd5)[0x55904316bbd5] python3(_PyEval_EvalFrameDefault+0x670c)[0x5590430ba7dc] python3(+0x1984d1)[0x5590430d74d1] python3(+0x166629)[0x5590430a5629] python3(_PyEval_EvalFrameDefault+0x4937)[0x5590430b8a07] python3(+0x25aa16)[0x559043199a16] python3(PyEval_EvalCode+0x86)[0x5590431998e6] python3(+0x25ffed)[0x55904319efed] python3(+0x18b5e9)[0x5590430ca5e9] python3(+0x22cbb4)[0x55904316bbb4] python3(_PyEval_EvalFrameDefault+0x6574)[0x5590430ba644] python3(_PyFunction_Vectorcall+0x7c)[0x5590430ca38c] python3(+0x22cb42)[0x55904316bb42] python3(_PyEval_EvalFrameDefault+0x670c)[0x5590430ba7dc] python3(_PyFunction_Vectorcall+0x7c)[0x5590430ca38c] python3(+0x22cb42)[0x55904316bb42] python3(_PyEval_EvalFrameDefault+0x670c)[0x5590430ba7dc] python3(_PyFunction_Vectorcall+0x7c)[0x5590430ca38c] python3(+0x22cb42)[0x55904316bb42] python3(_PyEval_EvalFrameDefault+0x670c)[0x5590430ba7dc] python3(_PyFunction_Vectorcall+0x7c)[0x5590430ca38c] python3(+0x22cb42)[0x55904316bb42] python3(_PyEval_EvalFrameDefault+0x670c)[0x5590430ba7dc] python3(_PyFunction_Vectorcall+0x7c)[0x5590430ca38c] python3(_PyObject_FastCallDictTstate+0x16d)[0x5590430bf61d] python3(_PyObject_Call_Prepend+0x5c)[0x5590430d462c] python3(+0x29d464)[0x5590431dc464] python3(_PyObject_MakeTpCall+0x25b)[0x5590430c039b] python3(_PyEval_EvalFrameDefault+0x6fa3)[0x5590430bb073] python3(_PyFunction_Vectorcall+0x7c)[0x5590430ca38c] python3(+0x22cb42)[0x55904316bb42] python3(_PyEval_EvalFrameDefault+0x670c)[0x5590430ba7dc] python3(_PyFunction_Vectorcall+0x7c)[0x5590430ca38c] python3(PyObject_Call+0x122)[0x5590430d8172] python3(_PyEval_EvalFrameDefault+0x2b60)[0x5590430b6c30] python3(+0x1984d1)[0x5590430d74d1] python3(+0x22cb42)[0x55904316bb42] python3(_PyEval_EvalFrameDefault+0x6fa3)[0x5590430bb073] python3(_PyFunction_Vectorcall+0x7c)[0x5590430ca38c] python3(+0x22cb42)[0x55904316bb42] python3(_PyEval_EvalFrameDefault+0x670c)[0x5590430ba7dc] python3(_PyObject_FastCallDictTstate+0xc4)[0x5590430bf574] python3(+0x194844)[0x5590430d3844] python3(_PyObject_MakeTpCall+0x1fc)[0x5590430c033c] python3(_PyEval_EvalFrameDefault+0x6574)[0x5590430ba644] python3(_PyFunction_Vectorcall+0x7c)[0x5590430ca38c] python3(+0x22cb42)[0x55904316bb42] python3(_PyEval_EvalFrameDefault+0x6a30)[0x5590430bab00] python3(_PyFunction_Vectorcall+0x7c)[0x5590430ca38c] python3(+0x22cb42)[0x55904316bb42] python3(_PyEval_EvalFrameDefault+0x6a30)[0x5590430bab00] python3(+0x1984d1)[0x5590430d74d1] python3(+0x22cb42)[0x55904316bb42] python3(_PyEval_EvalFrameDefault+0x6a30)[0x5590430bab00] python3(_PyFunction_Vectorcall+0x7c)[0x5590430ca38c] python3(+0x22cb42)[0x55904316bb42] python3(_PyEval_EvalFrameDefault+0x670c)[0x5590430ba7dc] python3(_PyFunction_Vec

Check root computation over large finite fields::

Expand Down
Loading