From dccc6c6b0755ef9c3d7fad84546d24cfc7922e62 Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Mon, 14 Jul 2025 21:53:36 +0700 Subject: [PATCH] Show long time warnings as GitHub annotations --- src/sage/doctest/forker.py | 27 +++++++++++++++++------ src/sage/doctest/parsing.py | 6 +++--- src/sage/doctest/sources.py | 20 +++++++++-------- src/sage/doctest/test.py | 36 +++++++++++++++++++++++++++++++ src/sage/doctest/tests/sleep2.rst | 4 ++++ 5 files changed, 74 insertions(+), 19 deletions(-) create mode 100644 src/sage/doctest/tests/sleep2.rst diff --git a/src/sage/doctest/forker.py b/src/sage/doctest/forker.py index d1f5c4ca85a..84db9ddc53a 100644 --- a/src/sage/doctest/forker.py +++ b/src/sage/doctest/forker.py @@ -583,6 +583,8 @@ def _run(self, test, compileflags, out): Since it needs to be able to read stdout, it should be called while spoofing using :class:`SageSpoofInOut`. + INPUT: see :meth:`run`. + EXAMPLES:: sage: from sage.doctest.parsing import SageOutputChecker @@ -628,6 +630,7 @@ def _run(self, test, compileflags, out): check = self._checker.check_output # Process each example. + example: doctest.Example for examplenum, example in enumerate(test.examples): if failures: # If exitfirst is set, abort immediately after a @@ -1185,7 +1188,7 @@ def compile_and_execute(self, example, compiler, globs): example.total_state = self.running_global_digest.hexdigest() example.doctest_state = self.running_doctest_digest.hexdigest() - def _failure_header(self, test, example, message='Failed example:'): + def _failure_header(self, test, example, message='Failed example:', extra=None): """ We strip out ``sage:`` prompts, so we override :meth:`doctest.DocTestRunner._failure_header` for better @@ -1197,6 +1200,14 @@ def _failure_header(self, test, example, message='Failed example:'): - ``example`` -- a :class:`doctest.Example` instance in ``test`` + - ``message`` -- a message to be shown. Must not have a newline + + - ``extra`` -- an extra message to be shown in GitHub annotation + + Note that ``message`` and ``extra`` are not accepted by + :meth:`doctest.DocTestRunner._failure_header`, as such by Liskov + substitution principle this method must be callable without passing those. + OUTPUT: string used for reporting that the given example failed EXAMPLES:: @@ -1260,6 +1271,8 @@ def _failure_header(self, test, example, message='Failed example:'): message += ' [failed in baseline]' else: command = f'::error title={message}' + if extra: + message += f': {extra}' if extra := getattr(example, 'extra', None): message += f': {extra}' if test.filename: @@ -1561,12 +1574,12 @@ def report_overtime(self, out, test, example, got, *, check_timer=None): Test ran for 1.23s cpu, 2.50s wall Check ran for 2.34s cpu, 3.12s wall """ - out(self._failure_header(test, example, 'Warning: slow doctest:') + - ('Test ran for %.2fs cpu, %.2fs wall\nCheck ran for %.2fs cpu, %.2fs wall\n' - % (example.cputime, - example.walltime, - check_timer.cputime, - check_timer.walltime))) + time_info = ('Test ran for %.2fs cpu, %.2fs wall\nCheck ran for %.2fs cpu, %.2fs wall\n' + % (example.cputime, + example.walltime, + check_timer.cputime, + check_timer.walltime)) + out(self._failure_header(test, example, 'Warning: slow doctest:', time_info) + time_info) def report_unexpected_exception(self, out, test, example, exc_info): r""" diff --git a/src/sage/doctest/parsing.py b/src/sage/doctest/parsing.py index 242457e8783..3214816bd69 100644 --- a/src/sage/doctest/parsing.py +++ b/src/sage/doctest/parsing.py @@ -829,7 +829,7 @@ def __ne__(self, other): """ return not (self == other) - def parse(self, string, *args): + def parse(self, string, *args) -> list[doctest.Example | str]: r""" A Sage specialization of :class:`doctest.DocTestParser`. @@ -1015,8 +1015,8 @@ def parse(self, string, *args): string = find_python_continuation.sub(r"\1" + ellipsis_tag + r"\2", string) string = find_sage_prompt.sub(r"\1>>> sage: ", string) string = find_sage_continuation.sub(r"\1...", string) - res = doctest.DocTestParser.parse(self, string, *args) - filtered = [] + res: list[doctest.Example | str] = doctest.DocTestParser.parse(self, string, *args) + filtered: list[doctest.Example | str] = [] persistent_optional_tags = self.file_optional_tags persistent_optional_tag_setter = None persistent_optional_tag_setter_index = None diff --git a/src/sage/doctest/sources.py b/src/sage/doctest/sources.py index ba6c05137b3..5395136f18a 100644 --- a/src/sage/doctest/sources.py +++ b/src/sage/doctest/sources.py @@ -193,7 +193,7 @@ def __ne__(self, other): """ return not (self == other) - def _process_doc(self, doctests, doc, namespace, start): + def _process_doc(self, doctests: list[doctest.DocTest], doc, namespace, start): """ Appends doctests defined in ``doc`` to the list ``doctests``. @@ -266,7 +266,7 @@ def file_optional_tags(self): """ return set() - def _create_doctests(self, namespace, tab_okay=None): + def _create_doctests(self, namespace, tab_okay=None) -> tuple[list[doctest.DocTest], dict]: """ Create a list of doctests defined in this source. @@ -314,7 +314,7 @@ def _create_doctests(self, namespace, tab_okay=None): probed_tags=self.options.probe, file_optional_tags=self.file_optional_tags) self.linking = False - doctests = [] + doctests: list[doctest.DocTest] = [] in_docstring = False unparsed_doc = False doc = [] @@ -480,7 +480,7 @@ def __iter__(self): for lineno, line in enumerate(self.source.split('\n')): yield lineno + self.lineno_shift, line + '\n' - def create_doctests(self, namespace): + def create_doctests(self, namespace) -> tuple[list[doctest.DocTest], dict]: r""" Create doctests from this string. @@ -492,8 +492,8 @@ def create_doctests(self, namespace): - ``doctests`` -- list of doctests defined by this string - - ``tab_locations`` -- either ``False`` or a list of linenumbers - on which tabs appear + - ``extras`` -- dictionary with ``extras['tab']`` either + ``False`` or a list of linenumbers on which tabs appear EXAMPLES:: @@ -503,10 +503,12 @@ def create_doctests(self, namespace): sage: s = "'''\n sage: 2 + 2\n 4\n'''" sage: PythonStringSource = dynamic_class('PythonStringSource',(StringDocTestSource, PythonSource)) sage: PSS = PythonStringSource('', s, DocTestDefaults(), 'runtime') - sage: dt, tabs = PSS.create_doctests({}) + sage: dt, extras = PSS.create_doctests({}) sage: for t in dt: ....: print("{} {}".format(t.name, t.examples[0].sage_source)) 2 + 2 + sage: extras + {...'tab': []...} """ return self._create_doctests(namespace) @@ -736,7 +738,7 @@ def file_optional_tags(self): from .parsing import parse_file_optional_tags return parse_file_optional_tags(self) - def create_doctests(self, namespace): + def create_doctests(self, namespace) -> tuple[list[doctest.DocTest], dict]: r""" Return a list of doctests for this file. @@ -910,7 +912,7 @@ class SourceLanguage: Currently supported languages include Python, ReST and LaTeX. """ - def parse_docstring(self, docstring, namespace, start): + def parse_docstring(self, docstring, namespace, start) -> list[doctest.DocTest]: """ Return a list of doctest defined in this docstring. diff --git a/src/sage/doctest/test.py b/src/sage/doctest/test.py index c6aa9ac6147..2602f439762 100644 --- a/src/sage/doctest/test.py +++ b/src/sage/doctest/test.py @@ -48,6 +48,42 @@ ... 0 +Check slow doctest warnings are correctly raised:: + + sage: subprocess.call(["sage", "-t", "--warn-long", # long time + ....: "--random-seed=0", "--optional=sage", "sleep2.rst"], **kwds) + Running doctests... + Doctesting 1 file. + sage -t --warn-long --random-seed=0 sleep2.rst + ********************************************************************** + File "sleep2.rst", line 4, in sage.doctest.tests.sleep2 + Warning: slow doctest: + while walltime(t) < 2: pass + Test ran for ...s cpu, ...s wall + Check ran for ...s cpu, ...s wall + [2 tests, ...s wall] + ---------------------------------------------------------------------- + All tests passed! + ---------------------------------------------------------------------- + ... + 0 + sage: subprocess.call(["sage", "-t", "--format=github", "--warn-long", # long time + ....: "--random-seed=0", "--optional=sage", "sleep2.rst"], **kwds) + Running doctests... + Doctesting 1 file. + sage -t --warn-long --random-seed=0 sleep2.rst + ********************************************************************** + ::warning title=Warning: slow doctest:,file=sleep2.rst,line=4::slow doctest:: Test ran for ...s cpu, ...s wall%0ACheck ran for ...s cpu, ...s wall%0A + while walltime(t) < 2: pass + Test ran for ...s cpu, ...s wall + Check ran for ...s cpu, ...s wall + [2 tests, ...s wall] + ---------------------------------------------------------------------- + All tests passed! + ---------------------------------------------------------------------- + ... + 0 + Check handling of tolerances:: sage: subprocess.call(["python3", "-m", "sage.doctest", "--warn-long", "0", # long time diff --git a/src/sage/doctest/tests/sleep2.rst b/src/sage/doctest/tests/sleep2.rst new file mode 100644 index 00000000000..3e9d8c4ed78 --- /dev/null +++ b/src/sage/doctest/tests/sleep2.rst @@ -0,0 +1,4 @@ +Test raising slow doctest warning (cputime instead of walltime is checked so we need busy loop):: + + sage: t = walltime() + sage: while walltime(t) < 2: pass