From cf96408e76c157c2bf531a947fd794edd0df7558 Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Mon, 16 Dec 2024 13:31:45 +0700 Subject: [PATCH] Fix alarm tests --- src/sage/coding/linear_code.py | 6 +- src/sage/doctest/util.py | 118 ++++++++++++++++++ src/sage/geometry/integral_points.pxi | 6 +- src/sage/libs/libecm.pyx | 6 +- src/sage/matrix/matrix_integer_dense.pyx | 20 +-- src/sage/matrix/matrix_mod2_dense.pyx | 6 +- src/sage/misc/cython.py | 11 +- src/sage/rings/complex_arb.pyx | 11 +- src/sage/rings/factorint_pari.pyx | 7 +- src/sage/rings/integer.pyx | 11 +- .../rings/polynomial/polynomial_element.pyx | 6 +- src/sage/rings/qqbar.py | 15 ++- .../elliptic_curves/descent_two_isogeny.pyx | 6 +- src/sage/sets/recursively_enumerated_set.pyx | 21 +--- src/sage/structure/coerce_actions.pyx | 6 +- 15 files changed, 168 insertions(+), 88 deletions(-) diff --git a/src/sage/coding/linear_code.py b/src/sage/coding/linear_code.py index 3e9d388c434..5d61dbfba37 100644 --- a/src/sage/coding/linear_code.py +++ b/src/sage/coding/linear_code.py @@ -789,10 +789,8 @@ def canonical_representative(self, equivalence='semilinear'): (see :issue:`21651`):: sage: C = LinearCode(random_matrix(GF(47), 25, 35)) - sage: alarm(0.5); C.canonical_representative() # needs sage.libs.gap - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.5): C.canonical_representative() # needs sage.libs.gap """ aut_group_can_label = self._canonize(equivalence) return aut_group_can_label.get_canonical_form(), \ diff --git a/src/sage/doctest/util.py b/src/sage/doctest/util.py index e17df277c1f..57d005c28f0 100644 --- a/src/sage/doctest/util.py +++ b/src/sage/doctest/util.py @@ -25,6 +25,8 @@ from time import time as walltime from os import sysconf, times +from contextlib import contextmanager +from cysignals.alarm import alarm, cancel_alarm, AlarmInterrupt def count_noun(number, noun, plural=None, pad_number=False, pad_noun=False): @@ -749,3 +751,119 @@ def __ne__(self, other): True """ return not (self == other) + + +@contextmanager +def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = 0.2, inaccuracy_tolerance: float = 0.1): + """ + Helper function for doctesting to ensure that the code is interruptible after a certain amount of time. + This should only be used for internal doctesting purposes. + + EXAMPLES:: + + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(1) as data: sleep(3) + + ``as data`` is optional, but if it is used, it will contain a few useful values:: + + sage: data # abs tol 0.2 + {'alarm_raised': True, 'elapsed': 1.0} + + ``max_wait_after_interrupt`` can be passed if the function may take longer than usual to be interrupted:: + + sage: cython(''' + ....: from libc.time cimport clock_t, clock, CLOCKS_PER_SEC + ....: from cysignals.signals cimport sig_check + ....: cpdef void uninterruptible_sleep(double seconds): + ....: cdef clock_t target = clock() + (CLOCKS_PER_SEC * seconds) + ....: while clock() < target: + ....: pass + ....: cpdef void check_interrupt_only_occasionally(): + ....: for i in range(10): + ....: uninterruptible_sleep(0.8) + ....: sig_check() + ....: ''', compiler_directives={'preliminary_late_includes_cy28': True}) + sage: with ensure_interruptible_after(1) as data: # not passing max_wait_after_interrupt will raise an error + ....: check_interrupt_only_occasionally() + Traceback (most recent call last): + ... + RuntimeError: Function is not interruptible within 1.0000 seconds, only after 1... seconds + sage: with ensure_interruptible_after(1, max_wait_after_interrupt=0.7): + ....: check_interrupt_only_occasionally() + + TESTS:: + + sage: data['elapsed'] # abs tol 0.2 # 1.6 = 0.8 * 2 + 1.6 + + This test ensures the ``# cython: ...`` header comment doesn't work + and ``cysignals`` requires ``preliminary_late_includes_cy28=True`` + (when it is finally fixed then this test and the one above can be modified + to remove the flag):: + + sage: # needs sage.misc.cython + sage: cython(''' + ....: # cython: preliminary_late_includes_cy28=True + ....: # ^ required by cysignals + ....: from cysignals.signals cimport sig_check + ....: ''') + Traceback (most recent call last): + ... + RuntimeError: ... + + :: + + sage: with ensure_interruptible_after(2) as data: sleep(1) + Traceback (most recent call last): + ... + RuntimeError: Function terminates early after 1... < 2.0000 seconds + sage: data # abs tol 0.2 + {'alarm_raised': False, 'elapsed': 1.0} + sage: with ensure_interruptible_after(1) as data: raise ValueError + Traceback (most recent call last): + ... + ValueError + sage: data # abs tol 0.2 + {'alarm_raised': False, 'elapsed': 0.0} + + :: + + sage: # needs sage.misc.cython + sage: with ensure_interruptible_after(1) as data: uninterruptible_sleep(2) + Traceback (most recent call last): + ... + RuntimeError: Function is not interruptible within 1.0000 seconds, only after 2... seconds + sage: data # abs tol 0.2 + {'alarm_raised': True, 'elapsed': 2.0} + sage: with ensure_interruptible_after(1): uninterruptible_sleep(2); raise RuntimeError + Traceback (most recent call last): + ... + RuntimeError: Function is not interruptible within 1.0000 seconds, only after 2... seconds + sage: data # abs tol 0.2 + {'alarm_raised': True, 'elapsed': 2.0} + """ + data = {} + start_time = walltime() + alarm(seconds) + alarm_raised = False + + try: + yield data + except AlarmInterrupt: + alarm_raised = True + finally: + cancel_alarm() + elapsed = walltime() - start_time + data["elapsed"] = elapsed + data["alarm_raised"] = alarm_raised + + if elapsed > seconds + max_wait_after_interrupt: + raise RuntimeError( + f"Function is not interruptible within {seconds:.4f} seconds, only after {elapsed:.4f} seconds" + + ("" if alarm_raised else " (__exit__ called before interrupt check)")) + + if alarm_raised: + if elapsed < seconds - inaccuracy_tolerance: + raise RuntimeError(f"Interrupted too early: {elapsed:.4f} < {seconds:.4f}, this should not happen") + else: + raise RuntimeError(f"Function terminates early after {elapsed:.4f} < {seconds:.4f} seconds") diff --git a/src/sage/geometry/integral_points.pxi b/src/sage/geometry/integral_points.pxi index a67535b450b..f351e63f5aa 100644 --- a/src/sage/geometry/integral_points.pxi +++ b/src/sage/geometry/integral_points.pxi @@ -531,10 +531,8 @@ cpdef rectangular_box_points(list box_min, list box_max, ....: (0, 0, 0, 0, 0, -1, 2, -1, 0), ....: (0, 0, 0, 0, 0, 0, -1, 2, -1)] sage: P = Polyhedron(ieqs=ieqs) - sage: alarm(0.5); P.integral_points() - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.5): P.integral_points() """ assert len(box_min) == len(box_max) assert not (count_only and return_saturated) diff --git a/src/sage/libs/libecm.pyx b/src/sage/libs/libecm.pyx index 6e0fc8668a5..86492a5e232 100644 --- a/src/sage/libs/libecm.pyx +++ b/src/sage/libs/libecm.pyx @@ -143,10 +143,8 @@ def ecmfactor(number, double B1, verbose=False, sigma=0): Check that ``ecmfactor`` can be interrupted (factoring a large prime number):: - sage: alarm(0.5); ecmfactor(2^521-1, 1e7) - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.5): ecmfactor(2^521-1, 1e7) Some special cases:: diff --git a/src/sage/matrix/matrix_integer_dense.pyx b/src/sage/matrix/matrix_integer_dense.pyx index 9ea2335b297..5e1c83dac50 100644 --- a/src/sage/matrix/matrix_integer_dense.pyx +++ b/src/sage/matrix/matrix_integer_dense.pyx @@ -4385,14 +4385,8 @@ cdef class Matrix_integer_dense(Matrix_dense): sage: A = random_matrix(ZZ, 2000, 2000) sage: B = random_matrix(ZZ, 2000, 2000) - sage: t0 = walltime() - sage: alarm(2); A._solve_iml(B) # long time - Traceback (most recent call last): - ... - AlarmInterrupt - sage: t = walltime(t0) - sage: t < 10 or t - True + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(2, max_wait_after_interrupt=8): A._solve_iml(B) ALGORITHM: Uses IML. @@ -4549,14 +4543,8 @@ cdef class Matrix_integer_dense(Matrix_dense): sage: A = random_matrix(ZZ, 2000, 2000) sage: B = random_matrix(ZZ, 2000, 2000) - sage: t0 = walltime() - sage: alarm(2); A._solve_flint(B) # long time - Traceback (most recent call last): - ... - AlarmInterrupt - sage: t = walltime(t0) - sage: t < 10 or t - True + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(2, max_wait_after_interrupt=8): A._solve_flint(B) AUTHORS: diff --git a/src/sage/matrix/matrix_mod2_dense.pyx b/src/sage/matrix/matrix_mod2_dense.pyx index 55f39acf67f..3ee7e0f10f9 100644 --- a/src/sage/matrix/matrix_mod2_dense.pyx +++ b/src/sage/matrix/matrix_mod2_dense.pyx @@ -1975,10 +1975,8 @@ cdef class Matrix_mod2_dense(matrix_dense.Matrix_dense): # dense or sparse sage: A = random_matrix(GF(2), n, m) sage: x = random_vector(GF(2), m) sage: B = A*x - sage: alarm(0.5); sol = A.solve_right(B) - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.5): sol = A.solve_right(B) """ cdef mzd_t *B_entries = (B)._entries diff --git a/src/sage/misc/cython.py b/src/sage/misc/cython.py index c542e0d1919..2c3ed6d9080 100644 --- a/src/sage/misc/cython.py +++ b/src/sage/misc/cython.py @@ -80,7 +80,7 @@ def _standard_libs_libdirs_incdirs_aliases(): def cython(filename, verbose=0, compile_message=False, use_cache=False, create_local_c_file=False, annotate=True, sage_namespace=True, - create_local_so_file=False): + compiler_directives={}, create_local_so_file=False): r""" Compile a Cython file. This converts a Cython file to a C (or C++ file), and then compiles that. The .c file and the .so file are @@ -113,6 +113,10 @@ def cython(filename, verbose=0, compile_message=False, - ``sage_namespace`` -- boolean (default: ``True``); if ``True``, import ``sage.all`` + - ``compiler_directives`` -- dictionary (default: ``{}``); extra compiler + directives to pass to Cython. Usually this can be provided by ``# cython: ...`` + comments in the Cython file, but there are a few exceptions + - ``create_local_so_file`` -- boolean (default: ``False``); if ``True``, save a copy of the compiled .so file in the current directory @@ -348,7 +352,8 @@ def cython(filename, verbose=0, compile_message=False, libraries=standard_libs, library_dirs=standard_libdirs) - directives = {'language_level': 3, 'cdivision': True} + compiler_directives = {'language_level': 3, 'cdivision': True, **compiler_directives} + # let user-provided compiler directives override the defaults try: # Change directories to target_dir so that Cython produces the correct @@ -360,7 +365,7 @@ def cython(filename, verbose=0, compile_message=False, ext, = cythonize([ext], aliases=aliases, include_path=includes, - compiler_directives=directives, + compiler_directives=compiler_directives, quiet=(verbose <= 0), errors_to_stderr=False, use_listing_file=True) diff --git a/src/sage/rings/complex_arb.pyx b/src/sage/rings/complex_arb.pyx index fb9d821a413..400bd4dc387 100644 --- a/src/sage/rings/complex_arb.pyx +++ b/src/sage/rings/complex_arb.pyx @@ -1184,13 +1184,10 @@ class ComplexBallField(UniqueRepresentation, sage.rings.abc.ComplexBallField): sage: ComplexBallField(100).integral(lambda x, _: sin(x), RBF(0), RBF(1)) [0.4596976941318602825990633926 +/- ...e-29] - sage: from cysignals.alarm import alarm - sage: alarm(0.1r) - sage: C = ComplexBallField(1000000) - sage: C.integral(lambda x, _: x.cos() * x.sin(), 0, 1) - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.1): + ....: C = ComplexBallField(1000000) + ....: C.integral(lambda x, _: x.cos() * x.sin(), 0, 1) """ cdef IntegrationContext ctx = IntegrationContext() cdef acb_calc_integrate_opt_t arb_opts diff --git a/src/sage/rings/factorint_pari.pyx b/src/sage/rings/factorint_pari.pyx index a8d748c5845..620453a65d4 100644 --- a/src/sage/rings/factorint_pari.pyx +++ b/src/sage/rings/factorint_pari.pyx @@ -50,10 +50,11 @@ def factor_using_pari(n, int_=False, debug_level=0, proof=None): Check that PARI's debug level is properly reset (:issue:`18792`):: - sage: alarm(0.5); factor(2^1000 - 1, verbose=5) - Traceback (most recent call last): + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.5): factor(2^1000 - 1, verbose=5) ... - AlarmInterrupt + doctest:warning... + RuntimeWarning: cypari2 leaked ... bytes on the PARI stack sage: pari.get_debug_level() 0 """ diff --git a/src/sage/rings/integer.pyx b/src/sage/rings/integer.pyx index c9d1ff65bc6..d5d72f2ade9 100644 --- a/src/sage/rings/integer.pyx +++ b/src/sage/rings/integer.pyx @@ -7108,21 +7108,16 @@ cdef class Integer(sage.structure.element.EuclideanDomainElement): Check that it can be interrupted (:issue:`17852`):: - sage: alarm(0.5); (2^100).binomial(2^22, algorithm='mpir') - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.5): (2^100).binomial(2^22, algorithm='mpir') For PARI, we try 10 interrupts with increasing intervals to check for reliable interrupting, see :issue:`18919`:: sage: from cysignals import AlarmInterrupt sage: for i in [1..10]: # long time (5s) # needs sage.libs.pari - ....: try: - ....: alarm(i/11) + ....: with ensure_interruptible_after(i/11): ....: (2^100).binomial(2^22, algorithm='pari') - ....: except AlarmInterrupt: - ....: pass doctest:...: RuntimeWarning: cypari2 leaked ... bytes on the PARI stack... """ cdef Integer x diff --git a/src/sage/rings/polynomial/polynomial_element.pyx b/src/sage/rings/polynomial/polynomial_element.pyx index 389e538f83e..624bcf3ee6e 100644 --- a/src/sage/rings/polynomial/polynomial_element.pyx +++ b/src/sage/rings/polynomial/polynomial_element.pyx @@ -2533,10 +2533,8 @@ cdef class Polynomial(CommutativePolynomial): sage: K. = GF(2^8) sage: x = polygen(K) sage: pol = x^1000000 + x + a - sage: alarm(0.5); pol.any_root() - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.5): pol.any_root() Check root computation over large finite fields:: diff --git a/src/sage/rings/qqbar.py b/src/sage/rings/qqbar.py index 3806663eaf0..a52694cf82b 100644 --- a/src/sage/rings/qqbar.py +++ b/src/sage/rings/qqbar.py @@ -7106,14 +7106,13 @@ def exactify(self): sage: x = polygen(AA) sage: p = AA(2)^(1/100) * x + AA(3)^(1/100) sage: cp = AA.common_polynomial(p) - sage: alarm(0.5); cp.generator() - Traceback (most recent call last): - ... - AlarmInterrupt - sage: alarm(0.5); cp.generator() - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.5): cp.generator() + doctest:warning... + RuntimeWarning: cypari2 leaked ... bytes on the PARI stack + sage: with ensure_interruptible_after(0.5): cp.generator() + doctest:warning... + RuntimeWarning: cypari2 leaked ... bytes on the PARI stack """ if self._exact: return diff --git a/src/sage/schemes/elliptic_curves/descent_two_isogeny.pyx b/src/sage/schemes/elliptic_curves/descent_two_isogeny.pyx index 16bad60ba56..f83568c2af2 100755 --- a/src/sage/schemes/elliptic_curves/descent_two_isogeny.pyx +++ b/src/sage/schemes/elliptic_curves/descent_two_isogeny.pyx @@ -1208,10 +1208,8 @@ def two_descent_by_two_isogeny(E, Elliptic Curve defined by y^2 = x^3 - x^2 - 900*x - 10098 over Rational Field sage: E.sha().an() 4 - sage: alarm(0.5); two_descent_by_two_isogeny(E, global_limit_large=10^8) - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.5): two_descent_by_two_isogeny(E, global_limit_large=10^8) """ cdef Integer a1, a2, a3, a4, a6, s2, s4, s6 cdef Integer c, d, x0 diff --git a/src/sage/sets/recursively_enumerated_set.pyx b/src/sage/sets/recursively_enumerated_set.pyx index b20fa8cbe8a..098074b1649 100644 --- a/src/sage/sets/recursively_enumerated_set.pyx +++ b/src/sage/sets/recursively_enumerated_set.pyx @@ -1122,11 +1122,8 @@ cdef class RecursivelyEnumeratedSet_symmetric(RecursivelyEnumeratedSet_generic): {0} sage: next(it) {-1, 1} - sage: from cysignals.alarm import alarm - sage: alarm(0.02); next(it) - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.02): next(it) sage: next(it) Traceback (most recent call last): ... @@ -1175,11 +1172,8 @@ cdef class RecursivelyEnumeratedSet_symmetric(RecursivelyEnumeratedSet_generic): ....: sleep(0.1r) ....: return [a - 1, a + 1] sage: C = RecursivelyEnumeratedSet([0], f, structure='symmetric') - sage: from cysignals.alarm import alarm - sage: alarm(0.45); C.graded_component(10) - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.45): C.graded_component(10) sage: C.graded_component(1) {-1, 1} sage: C.graded_component(2) @@ -1394,11 +1388,8 @@ cdef class RecursivelyEnumeratedSet_graded(RecursivelyEnumeratedSet_generic): ....: sleep(0.1r) ....: return [a + 1, a + I] sage: C = RecursivelyEnumeratedSet([0], f, structure='graded') - sage: from cysignals.alarm import alarm - sage: alarm(0.45); C.graded_component(10) - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.45): C.graded_component(10) sage: C.graded_component(2) {2*I, I + 1, 2} sage: C.graded_component(3) diff --git a/src/sage/structure/coerce_actions.pyx b/src/sage/structure/coerce_actions.pyx index 17bbd397c98..f146a332368 100644 --- a/src/sage/structure/coerce_actions.pyx +++ b/src/sage/structure/coerce_actions.pyx @@ -801,10 +801,8 @@ cdef class IntegerMulAction(IntegerAction): sage: # needs sage.schemes sage: P = E([2,1,1]) - sage: alarm(0.001); 2^(10^8) * P - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.001): 2^(10^8) * P Verify that cysignals correctly detects that the above exception has been handled::