From abb8d018a38822e126357ee343a7d692692f94c2 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 01/11] Fix alarm tests --- src/sage/coding/linear_code.py | 6 +- .../bounded_integer_sequences.pyx | 6 +- src/sage/doctest/util.py | 103 ++++++++++++++++++ src/sage/geometry/integral_points.pxi | 6 +- src/sage/libs/flint/nmod_poly_linkage.pxi | 6 +- src/sage/libs/gap/element.pyx | 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/rings/complex_arb.pyx | 11 +- src/sage/rings/factorint_pari.pyx | 7 +- src/sage/rings/integer.pyx | 11 +- .../rings/polynomial/polynomial_element.pyx | 6 +- .../polynomial/polynomial_zmod_flint.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 +- 18 files changed, 153 insertions(+), 101 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/data_structures/bounded_integer_sequences.pyx b/src/sage/data_structures/bounded_integer_sequences.pyx index 96dd83b4f8e..a96a55e5b50 100644 --- a/src/sage/data_structures/bounded_integer_sequences.pyx +++ b/src/sage/data_structures/bounded_integer_sequences.pyx @@ -1372,10 +1372,8 @@ def _biseq_stresstest(): TESTS:: sage: from sage.data_structures.bounded_integer_sequences import _biseq_stresstest - sage: alarm(1); _biseq_stresstest() # long time - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(1): _biseq_stresstest() # long time """ cdef int branch cdef Py_ssize_t x, y, z diff --git a/src/sage/doctest/util.py b/src/sage/doctest/util.py index e17df277c1f..34c99a4827e 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,104 @@ 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() + ....: ''') + 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.9): + ....: check_interrupt_only_occasionally() + + TESTS:: + + sage: data['elapsed'] # abs tol 0.3 # 1.6 = 0.8 * 2 + 1.6 + + :: + + 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/flint/nmod_poly_linkage.pxi b/src/sage/libs/flint/nmod_poly_linkage.pxi index b1be0216a2e..2bd2ae1205f 100644 --- a/src/sage/libs/flint/nmod_poly_linkage.pxi +++ b/src/sage/libs/flint/nmod_poly_linkage.pxi @@ -536,10 +536,8 @@ cdef inline int celement_pow(nmod_poly_t res, nmod_poly_t x, long e, nmod_poly_t Make sure that exponentiation can be interrupted, see :issue:`17470`:: sage: n = 2^23 - sage: alarm(0.2); x^n; cancel_alarm() - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.2): (x^n).degree() """ if modulus != NULL: sig_on() diff --git a/src/sage/libs/gap/element.pyx b/src/sage/libs/gap/element.pyx index b35626b29e1..3faa39c5329 100644 --- a/src/sage/libs/gap/element.pyx +++ b/src/sage/libs/gap/element.pyx @@ -1135,10 +1135,8 @@ cdef class GapElement(RingElement): Check that this can be interrupted gracefully:: sage: a, b = libgap.GL(1000, 3).GeneratorsOfGroup(); g = a * b - sage: alarm(0.5); g ^ (2 ^ 10000) - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.5): g ^ (2 ^ 10000) sage: libgap.CyclicGroup(2) ^ 2 Traceback (most recent call last): 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/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/polynomial/polynomial_zmod_flint.pyx b/src/sage/rings/polynomial/polynomial_zmod_flint.pyx index 5b3539d6b70..2bde0865f72 100644 --- a/src/sage/rings/polynomial/polynomial_zmod_flint.pyx +++ b/src/sage/rings/polynomial/polynomial_zmod_flint.pyx @@ -811,10 +811,8 @@ cdef class Polynomial_zmod_flint(Polynomial_template): sage: R. = PolynomialRing(GF(65537), implementation="FLINT") sage: f = R.random_element(9973) * R.random_element(10007) - sage: alarm(0.5); f.factor() - Traceback (most recent call last): - ... - AlarmInterrupt + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(0.5): f.factor() Test zero polynomial:: 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:: From 40dfc5f36afa8dd5f6a9e19977328cd7cb12e0f1 Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Thu, 19 Dec 2024 09:21:53 +0700 Subject: [PATCH 02/11] Fix test fail by suppress warning --- src/sage/rings/qqbar.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/sage/rings/qqbar.py b/src/sage/rings/qqbar.py index a52694cf82b..eedd9bd5dc5 100644 --- a/src/sage/rings/qqbar.py +++ b/src/sage/rings/qqbar.py @@ -7083,7 +7083,7 @@ def complex_roots(self, prec, multiplicity): return roots def exactify(self): - """ + r""" Compute a common field that holds all of the algebraic coefficients of this polynomial, then factor the polynomial over that field. Store the factors for later use (ignoring multiplicity). @@ -7107,12 +7107,13 @@ def exactify(self): sage: p = AA(2)^(1/100) * x + AA(3)^(1/100) sage: cp = AA.common_polynomial(p) 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 + sage: from warnings import catch_warnings, filterwarnings + sage: with ensure_interruptible_after(0.5), catch_warnings(): + ....: filterwarnings("ignore", r"cypari2 leaked \d+ bytes on the PARI stack") + ....: cp.generator() + sage: with ensure_interruptible_after(0.5), catch_warnings(): + ....: filterwarnings("ignore", r"cypari2 leaked \d+ bytes on the PARI stack") + ....: cp.generator() """ if self._exact: return From 63d482a32feed9d7886ca35bb7a7fa49932bed5a Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Thu, 19 Dec 2024 12:15:18 +0700 Subject: [PATCH 03/11] Wait by realtime, avoid unexplained slowdown --- src/sage/doctest/util.py | 55 +++++++++++++++++++++++++--------------- 1 file changed, 35 insertions(+), 20 deletions(-) diff --git a/src/sage/doctest/util.py b/src/sage/doctest/util.py index 34c99a4827e..6bb2967ae73 100644 --- a/src/sage/doctest/util.py +++ b/src/sage/doctest/util.py @@ -766,49 +766,59 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = ``as data`` is optional, but if it is used, it will contain a few useful values:: - sage: data # abs tol 0.2 + sage: data # abs tol 0.01 {'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 + sage: cython(r''' + ....: from posix.time cimport clock_gettime, CLOCK_REALTIME, timespec ....: from cysignals.signals cimport sig_check + ....: from time import time as walltime + ....: ....: cpdef void uninterruptible_sleep(double seconds): - ....: cdef clock_t target = clock() + (CLOCKS_PER_SEC * seconds) - ....: while clock() < target: - ....: pass + ....: cdef timespec start_time, target_time + ....: start_walltime = walltime() + ....: clock_gettime(CLOCK_REALTIME, &start_time) + ....: + ....: cdef int floor_seconds = seconds + ....: target_time.tv_sec = start_time.tv_sec + floor_seconds + ....: target_time.tv_nsec = start_time.tv_nsec + ((seconds - floor_seconds) * 1e9) + ....: if target_time.tv_nsec >= 1000000000: + ....: target_time.tv_nsec -= 1000000000 + ....: target_time.tv_sec += 1 + ....: + ....: while True: + ....: clock_gettime(CLOCK_REALTIME, &start_time) + ....: if start_time.tv_sec > target_time.tv_sec or (start_time.tv_sec == target_time.tv_sec and start_time.tv_nsec >= target_time.tv_nsec): + ....: break + ....: ....: cpdef void check_interrupt_only_occasionally(): ....: for i in range(10): ....: uninterruptible_sleep(0.8) ....: sig_check() ....: ''') - sage: with ensure_interruptible_after(1) as data: # not passing max_wait_after_interrupt will raise an error + sage: with ensure_interruptible_after(1): # 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 + RuntimeError: Function is not interruptible within 1.0000 seconds, only after 1.60... seconds sage: with ensure_interruptible_after(1, max_wait_after_interrupt=0.9): ....: check_interrupt_only_occasionally() TESTS:: - sage: data['elapsed'] # abs tol 0.3 # 1.6 = 0.8 * 2 - 1.6 - - :: - 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 + RuntimeError: Function terminates early after 1.00... < 2.0000 seconds + sage: data # abs tol 0.01 {'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 + sage: data # abs tol 0.01 {'alarm_raised': False, 'elapsed': 0.0} :: @@ -817,16 +827,20 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = 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 + RuntimeError: Function is not interruptible within 1.0000 seconds, only after 2.00... seconds + sage: data # abs tol 0.01 {'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 + RuntimeError: Function is not interruptible within 1.0000 seconds, only after 2.00... seconds + sage: data # abs tol 0.01 {'alarm_raised': True, 'elapsed': 2.0} """ + seconds = float(seconds) + max_wait_after_interrupt = float(max_wait_after_interrupt) + inaccuracy_tolerance = float(inaccuracy_tolerance) + # use Python float to avoid unexplained slowdown with Sage objects data = {} start_time = walltime() alarm(seconds) @@ -837,6 +851,7 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = except AlarmInterrupt: alarm_raised = True finally: + before_cancel_alarm_elapsed = walltime() - start_time cancel_alarm() elapsed = walltime() - start_time data["elapsed"] = elapsed From a3233d87a6646974b132cefd35c5c826bdc86d1b Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Thu, 19 Dec 2024 14:48:25 +0700 Subject: [PATCH 04/11] Change some sleep() call to use Python float --- src/sage/doctest/util.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/sage/doctest/util.py b/src/sage/doctest/util.py index 6bb2967ae73..4e36a541d3f 100644 --- a/src/sage/doctest/util.py +++ b/src/sage/doctest/util.py @@ -762,7 +762,7 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = EXAMPLES:: sage: from sage.doctest.util import ensure_interruptible_after - sage: with ensure_interruptible_after(1) as data: sleep(3) + sage: with ensure_interruptible_after(1) as data: sleep(3r) ``as data`` is optional, but if it is used, it will contain a few useful values:: @@ -808,7 +808,8 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = TESTS:: - sage: with ensure_interruptible_after(2) as data: sleep(1) + sage: # we use 1r instead of 1 to avoid unexplained slowdown + sage: with ensure_interruptible_after(2) as data: sleep(1r) Traceback (most recent call last): ... RuntimeError: Function terminates early after 1.00... < 2.0000 seconds From cdba9cdd60d51c56bd0d2f8d1cabcb1235a2dd08 Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Fri, 20 Dec 2024 09:28:52 +0700 Subject: [PATCH 05/11] Fix tests on Mac --- src/sage/doctest/util.py | 65 +++++++++++++++++++++++++++++----------- 1 file changed, 47 insertions(+), 18 deletions(-) diff --git a/src/sage/doctest/util.py b/src/sage/doctest/util.py index 4e36a541d3f..47ed4e767ee 100644 --- a/src/sage/doctest/util.py +++ b/src/sage/doctest/util.py @@ -762,28 +762,27 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = EXAMPLES:: sage: from sage.doctest.util import ensure_interruptible_after - sage: with ensure_interruptible_after(1) as data: sleep(3r) + 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.01 + sage: data # abs tol 1 {'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: # needs sage.misc.cython sage: cython(r''' - ....: from posix.time cimport clock_gettime, CLOCK_REALTIME, timespec + ....: from posix.time cimport clock_gettime, CLOCK_REALTIME, timespec, time_t ....: from cysignals.signals cimport sig_check - ....: from time import time as walltime ....: ....: cpdef void uninterruptible_sleep(double seconds): ....: cdef timespec start_time, target_time - ....: start_walltime = walltime() ....: clock_gettime(CLOCK_REALTIME, &start_time) ....: - ....: cdef int floor_seconds = seconds + ....: cdef time_t floor_seconds = seconds ....: target_time.tv_sec = start_time.tv_sec + floor_seconds - ....: target_time.tv_nsec = start_time.tv_nsec + ((seconds - floor_seconds) * 1e9) + ....: target_time.tv_nsec = start_time.tv_nsec + ((seconds - floor_seconds) * 1e9) ....: if target_time.tv_nsec >= 1000000000: ....: target_time.tv_nsec -= 1000000000 ....: target_time.tv_sec += 1 @@ -808,23 +807,44 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = TESTS:: - sage: # we use 1r instead of 1 to avoid unexplained slowdown - sage: with ensure_interruptible_after(2) as data: sleep(1r) + sage: with ensure_interruptible_after(2) as data: sleep(1) Traceback (most recent call last): ... - RuntimeError: Function terminates early after 1.00... < 2.0000 seconds - sage: data # abs tol 0.01 + RuntimeError: Function terminates early after 1... < 2.0000 seconds + sage: data # abs tol 1 {'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.01 - {'alarm_raised': False, 'elapsed': 0.0} - :: + The test above requires a large tolerance, because both ``time.sleep`` and + ``from posix.unistd cimport usleep`` may have slowdown on the order of 0.1s on Mac, + likely because the system is idle and GitHub CI switches the program out, + and context switch back takes time. So we use busy wait instead:: sage: # needs sage.misc.cython + sage: cython(r''' + ....: from posix.time cimport clock_gettime, CLOCK_REALTIME, timespec, time_t + ....: from cysignals.signals cimport sig_check + ....: + ....: cpdef void interruptible_sleep(double seconds): + ....: cdef timespec start_time, target_time + ....: clock_gettime(CLOCK_REALTIME, &start_time) + ....: + ....: cdef time_t floor_seconds = seconds + ....: target_time.tv_sec = start_time.tv_sec + floor_seconds + ....: target_time.tv_nsec = start_time.tv_nsec + ((seconds - floor_seconds) * 1e9) + ....: if target_time.tv_nsec >= 1000000000: + ....: target_time.tv_nsec -= 1000000000 + ....: target_time.tv_sec += 1 + ....: + ....: while True: + ....: sig_check() + ....: clock_gettime(CLOCK_REALTIME, &start_time) + ....: if start_time.tv_sec > target_time.tv_sec or (start_time.tv_sec == target_time.tv_sec and start_time.tv_nsec >= target_time.tv_nsec): + ....: break + ....: ''') + sage: with ensure_interruptible_after(2) as data: interruptible_sleep(1) + Traceback (most recent call last): + ... + RuntimeError: Function terminates early after 1.00... < 2.0000 seconds sage: with ensure_interruptible_after(1) as data: uninterruptible_sleep(2) Traceback (most recent call last): ... @@ -837,6 +857,15 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = RuntimeError: Function is not interruptible within 1.0000 seconds, only after 2.00... seconds sage: data # abs tol 0.01 {'alarm_raised': True, 'elapsed': 2.0} + + :: + + sage: with ensure_interruptible_after(1) as data: raise ValueError + Traceback (most recent call last): + ... + ValueError + sage: data # abs tol 0.01 + {'alarm_raised': False, 'elapsed': 0.0} """ seconds = float(seconds) max_wait_after_interrupt = float(max_wait_after_interrupt) From e431b2f9ec9fe55dcda4cea6c8bd300cdf4c69ef Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Mon, 23 Dec 2024 12:59:21 +0700 Subject: [PATCH 06/11] Add remark in developer coding convention --- src/doc/en/developer/coding_basics.rst | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/doc/en/developer/coding_basics.rst b/src/doc/en/developer/coding_basics.rst index 9b56de46ad0..b80ac4b958f 100644 --- a/src/doc/en/developer/coding_basics.rst +++ b/src/doc/en/developer/coding_basics.rst @@ -966,6 +966,14 @@ written. checked, as they are the most likely to be broken, now or in the future. This probably belongs to the TESTS block (see :ref:`section-docstring-function`). +- **Interruption:** if the function might take a very long time, use + :func:`~sage.doctest.util.ensure_interruptible_after` to check that the user + can interrupt it. For example, the following tests ``sleep(3)`` can be + interrupted after 1 second:: + + sage: from sage.doctest.util import ensure_interruptible_after + sage: with ensure_interruptible_after(1) as data: sleep(3) + - **Systematic tests** of all small-sized inputs, or tests of **random** instances if possible. From 44ad9b4e3645be7ff48df728e18b5c582bee4729 Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Fri, 24 Jan 2025 19:10:45 +0700 Subject: [PATCH 07/11] Try to use Python integers --- src/sage/doctest/util.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/sage/doctest/util.py b/src/sage/doctest/util.py index 47ed4e767ee..9fec38f486d 100644 --- a/src/sage/doctest/util.py +++ b/src/sage/doctest/util.py @@ -817,7 +817,9 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = The test above requires a large tolerance, because both ``time.sleep`` and ``from posix.unistd cimport usleep`` may have slowdown on the order of 0.1s on Mac, likely because the system is idle and GitHub CI switches the program out, - and context switch back takes time. So we use busy wait instead:: + and context switch back takes time. Besides, there is an issue with ``Integer`` + destructor, see ``_ + So we use busy wait and Python integers:: sage: # needs sage.misc.cython sage: cython(r''' @@ -841,17 +843,17 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = ....: if start_time.tv_sec > target_time.tv_sec or (start_time.tv_sec == target_time.tv_sec and start_time.tv_nsec >= target_time.tv_nsec): ....: break ....: ''') - sage: with ensure_interruptible_after(2) as data: interruptible_sleep(1) + sage: with ensure_interruptible_after(2) as data: interruptible_sleep(1r) Traceback (most recent call last): ... RuntimeError: Function terminates early after 1.00... < 2.0000 seconds - sage: with ensure_interruptible_after(1) as data: uninterruptible_sleep(2) + sage: with ensure_interruptible_after(1) as data: uninterruptible_sleep(2r) Traceback (most recent call last): ... RuntimeError: Function is not interruptible within 1.0000 seconds, only after 2.00... seconds sage: data # abs tol 0.01 {'alarm_raised': True, 'elapsed': 2.0} - sage: with ensure_interruptible_after(1): uninterruptible_sleep(2); raise RuntimeError + sage: with ensure_interruptible_after(1): uninterruptible_sleep(2r); raise RuntimeError Traceback (most recent call last): ... RuntimeError: Function is not interruptible within 1.0000 seconds, only after 2.00... seconds @@ -870,7 +872,7 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = seconds = float(seconds) max_wait_after_interrupt = float(max_wait_after_interrupt) inaccuracy_tolerance = float(inaccuracy_tolerance) - # use Python float to avoid unexplained slowdown with Sage objects + # use Python float to avoid slowdown with Sage Integer (see https://github.com/sagemath/cysignals/issues/215) data = {} start_time = walltime() alarm(seconds) From 59756a1b17ea9b9fc7e3f1efb1feb24383e632bd Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Sun, 5 Jan 2025 00:19:12 +0700 Subject: [PATCH 08/11] Specify use_sources=True in cython compilation --- src/sage/misc/cython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/misc/cython.py b/src/sage/misc/cython.py index 29c013eab19..c85be325c3d 100644 --- a/src/sage/misc/cython.py +++ b/src/sage/misc/cython.py @@ -60,7 +60,7 @@ def _standard_libs_libdirs_incdirs_aliases(): if SAGE_LOCAL: standard_libdirs.append(os.path.join(SAGE_LOCAL, "lib")) standard_libdirs.extend(aliases["CBLAS_LIBDIR"] + aliases["NTL_LIBDIR"]) - standard_incdirs = sage_include_directories() + aliases["CBLAS_INCDIR"] + aliases["NTL_INCDIR"] + standard_incdirs = sage_include_directories(use_sources=True) + aliases["CBLAS_INCDIR"] + aliases["NTL_INCDIR"] return standard_libs, standard_libdirs, standard_incdirs, aliases ################################################################ From 7eca8049f238cb3f367ed6c773513d6eed3645f7 Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Sat, 25 Jan 2025 10:16:37 +0700 Subject: [PATCH 09/11] Add workaround to make tests work on Python 3.12 --- src/sage/doctest/util.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/sage/doctest/util.py b/src/sage/doctest/util.py index 9fec38f486d..a62d48cfa19 100644 --- a/src/sage/doctest/util.py +++ b/src/sage/doctest/util.py @@ -880,7 +880,8 @@ def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = try: yield data - except AlarmInterrupt: + except AlarmInterrupt as e: + e.__traceback__ = None # workaround for https://github.com/python/cpython/pull/129276 alarm_raised = True finally: before_cancel_alarm_elapsed = walltime() - start_time From 9259991bb6945664163f99b2043d3ce11933e554 Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Sat, 25 Jan 2025 12:11:23 +0700 Subject: [PATCH 10/11] Retrigger CI From 5f3eafa8d256f31a762c6ac75141e90c4b969440 Mon Sep 17 00:00:00 2001 From: user202729 <25191436+user202729@users.noreply.github.com> Date: Sat, 25 Jan 2025 15:04:20 +0700 Subject: [PATCH 11/11] Revert "Specify use_sources=True in cython compilation" This reverts commit 59756a1b17ea9b9fc7e3f1efb1feb24383e632bd. --- src/sage/misc/cython.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/sage/misc/cython.py b/src/sage/misc/cython.py index c85be325c3d..29c013eab19 100644 --- a/src/sage/misc/cython.py +++ b/src/sage/misc/cython.py @@ -60,7 +60,7 @@ def _standard_libs_libdirs_incdirs_aliases(): if SAGE_LOCAL: standard_libdirs.append(os.path.join(SAGE_LOCAL, "lib")) standard_libdirs.extend(aliases["CBLAS_LIBDIR"] + aliases["NTL_LIBDIR"]) - standard_incdirs = sage_include_directories(use_sources=True) + aliases["CBLAS_INCDIR"] + aliases["NTL_INCDIR"] + standard_incdirs = sage_include_directories() + aliases["CBLAS_INCDIR"] + aliases["NTL_INCDIR"] return standard_libs, standard_libdirs, standard_incdirs, aliases ################################################################