Skip to content

Commit 6b2a603

Browse files
author
Release Manager
committed
sagemathgh-39142: Fix alarm tests As discussed in https://gist.github.com/user202729/52b0c7134ea34f78a4416 cd19e28e578#checking-the-code-is-indeed-interruptable , the current doctest of SageMath that tests `sage: alarm(0.5); f()` only checks whether `f` can be interrupted within 10 minutes or whatever the doctest time limit is — which is not particularly useful. With this change, if `f` doesn't get interrupted within 0.2 second of `alarm()` fired, a test failure will be reported. ### 📝 Checklist <!-- Put an `x` in all the boxes that apply. --> - [x] The title is concise and informative. - [x] The description explains in detail what this PR is about. - [x] I have linked a relevant issue or discussion. - [x] I have created tests covering the changes. - [x] I have updated the documentation and checked the documentation preview. ### ⌛ Dependencies <!-- List all open PRs that this PR logically depends on. For example, --> <!-- - sagemath#12345: short description why this is a dependency --> <!-- - sagemath#34567: ... --> URL: sagemath#39142 Reported by: user202729 Reviewer(s): Tobias Diez
2 parents a247fc7 + c917dc4 commit 6b2a603

19 files changed

+211
-102
lines changed

src/doc/en/developer/coding_basics.rst

+8
Original file line numberDiff line numberDiff line change
@@ -979,6 +979,14 @@ written.
979979
checked, as they are the most likely to be broken, now or in the future. This
980980
probably belongs to the TESTS block (see :ref:`section-docstring-function`).
981981

982+
- **Interruption:** if the function might take a very long time, use
983+
:func:`~sage.doctest.util.ensure_interruptible_after` to check that the user
984+
can interrupt it. For example, the following tests ``sleep(3)`` can be
985+
interrupted after 1 second::
986+
987+
sage: from sage.doctest.util import ensure_interruptible_after
988+
sage: with ensure_interruptible_after(1) as data: sleep(3)
989+
982990
- **Systematic tests** of all small-sized inputs, or tests of **random**
983991
instances if possible.
984992

src/sage/coding/linear_code.py

+2-4
Original file line numberDiff line numberDiff line change
@@ -787,10 +787,8 @@ def canonical_representative(self, equivalence='semilinear'):
787787
(see :issue:`21651`)::
788788
789789
sage: C = LinearCode(random_matrix(GF(47), 25, 35))
790-
sage: alarm(0.5); C.canonical_representative() # needs sage.libs.gap
791-
Traceback (most recent call last):
792-
...
793-
AlarmInterrupt
790+
sage: from sage.doctest.util import ensure_interruptible_after
791+
sage: with ensure_interruptible_after(0.5): C.canonical_representative() # needs sage.libs.gap
794792
"""
795793
aut_group_can_label = self._canonize(equivalence)
796794
return aut_group_can_label.get_canonical_form(), \

src/sage/data_structures/bounded_integer_sequences.pyx

+2-4
Original file line numberDiff line numberDiff line change
@@ -1372,10 +1372,8 @@ def _biseq_stresstest():
13721372
TESTS::
13731373
13741374
sage: from sage.data_structures.bounded_integer_sequences import _biseq_stresstest
1375-
sage: alarm(1); _biseq_stresstest() # long time
1376-
Traceback (most recent call last):
1377-
...
1378-
AlarmInterrupt
1375+
sage: from sage.doctest.util import ensure_interruptible_after
1376+
sage: with ensure_interruptible_after(1): _biseq_stresstest() # long time
13791377
"""
13801378
cdef int branch
13811379
cdef Py_ssize_t x, y, z

src/sage/doctest/util.py

+151
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,8 @@
2525

2626
from time import time as walltime
2727
from os import sysconf, times
28+
from contextlib import contextmanager
29+
from cysignals.alarm import alarm, cancel_alarm, AlarmInterrupt
2830

2931

3032
def count_noun(number, noun, plural=None, pad_number=False, pad_noun=False):
@@ -749,3 +751,152 @@ def __ne__(self, other):
749751
True
750752
"""
751753
return not (self == other)
754+
755+
756+
@contextmanager
757+
def ensure_interruptible_after(seconds: float, max_wait_after_interrupt: float = 0.2, inaccuracy_tolerance: float = 0.1):
758+
"""
759+
Helper function for doctesting to ensure that the code is interruptible after a certain amount of time.
760+
This should only be used for internal doctesting purposes.
761+
762+
EXAMPLES::
763+
764+
sage: from sage.doctest.util import ensure_interruptible_after
765+
sage: with ensure_interruptible_after(1) as data: sleep(3)
766+
767+
``as data`` is optional, but if it is used, it will contain a few useful values::
768+
769+
sage: data # abs tol 1
770+
{'alarm_raised': True, 'elapsed': 1.0}
771+
772+
``max_wait_after_interrupt`` can be passed if the function may take longer than usual to be interrupted::
773+
774+
sage: # needs sage.misc.cython
775+
sage: cython(r'''
776+
....: from posix.time cimport clock_gettime, CLOCK_REALTIME, timespec, time_t
777+
....: from cysignals.signals cimport sig_check
778+
....:
779+
....: cpdef void uninterruptible_sleep(double seconds):
780+
....: cdef timespec start_time, target_time
781+
....: clock_gettime(CLOCK_REALTIME, &start_time)
782+
....:
783+
....: cdef time_t floor_seconds = <time_t>seconds
784+
....: target_time.tv_sec = start_time.tv_sec + floor_seconds
785+
....: target_time.tv_nsec = start_time.tv_nsec + <long>((seconds - floor_seconds) * 1e9)
786+
....: if target_time.tv_nsec >= 1000000000:
787+
....: target_time.tv_nsec -= 1000000000
788+
....: target_time.tv_sec += 1
789+
....:
790+
....: while True:
791+
....: clock_gettime(CLOCK_REALTIME, &start_time)
792+
....: 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):
793+
....: break
794+
....:
795+
....: cpdef void check_interrupt_only_occasionally():
796+
....: for i in range(10):
797+
....: uninterruptible_sleep(0.8)
798+
....: sig_check()
799+
....: ''')
800+
sage: with ensure_interruptible_after(1): # not passing max_wait_after_interrupt will raise an error
801+
....: check_interrupt_only_occasionally()
802+
Traceback (most recent call last):
803+
...
804+
RuntimeError: Function is not interruptible within 1.0000 seconds, only after 1.60... seconds
805+
sage: with ensure_interruptible_after(1, max_wait_after_interrupt=0.9):
806+
....: check_interrupt_only_occasionally()
807+
808+
TESTS::
809+
810+
sage: with ensure_interruptible_after(2) as data: sleep(1)
811+
Traceback (most recent call last):
812+
...
813+
RuntimeError: Function terminates early after 1... < 2.0000 seconds
814+
sage: data # abs tol 1
815+
{'alarm_raised': False, 'elapsed': 1.0}
816+
817+
The test above requires a large tolerance, because both ``time.sleep`` and
818+
``from posix.unistd cimport usleep`` may have slowdown on the order of 0.1s on Mac,
819+
likely because the system is idle and GitHub CI switches the program out,
820+
and context switch back takes time. Besides, there is an issue with ``Integer``
821+
destructor, see `<https://github.com/sagemath/cysignals/issues/215>`_
822+
So we use busy wait and Python integers::
823+
824+
sage: # needs sage.misc.cython
825+
sage: cython(r'''
826+
....: from posix.time cimport clock_gettime, CLOCK_REALTIME, timespec, time_t
827+
....: from cysignals.signals cimport sig_check
828+
....:
829+
....: cpdef void interruptible_sleep(double seconds):
830+
....: cdef timespec start_time, target_time
831+
....: clock_gettime(CLOCK_REALTIME, &start_time)
832+
....:
833+
....: cdef time_t floor_seconds = <time_t>seconds
834+
....: target_time.tv_sec = start_time.tv_sec + floor_seconds
835+
....: target_time.tv_nsec = start_time.tv_nsec + <long>((seconds - floor_seconds) * 1e9)
836+
....: if target_time.tv_nsec >= 1000000000:
837+
....: target_time.tv_nsec -= 1000000000
838+
....: target_time.tv_sec += 1
839+
....:
840+
....: while True:
841+
....: sig_check()
842+
....: clock_gettime(CLOCK_REALTIME, &start_time)
843+
....: 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):
844+
....: break
845+
....: ''')
846+
sage: with ensure_interruptible_after(2) as data: interruptible_sleep(1r)
847+
Traceback (most recent call last):
848+
...
849+
RuntimeError: Function terminates early after 1.00... < 2.0000 seconds
850+
sage: with ensure_interruptible_after(1) as data: uninterruptible_sleep(2r)
851+
Traceback (most recent call last):
852+
...
853+
RuntimeError: Function is not interruptible within 1.0000 seconds, only after 2.00... seconds
854+
sage: data # abs tol 0.01
855+
{'alarm_raised': True, 'elapsed': 2.0}
856+
sage: with ensure_interruptible_after(1): uninterruptible_sleep(2r); raise RuntimeError
857+
Traceback (most recent call last):
858+
...
859+
RuntimeError: Function is not interruptible within 1.0000 seconds, only after 2.00... seconds
860+
sage: data # abs tol 0.01
861+
{'alarm_raised': True, 'elapsed': 2.0}
862+
863+
::
864+
865+
sage: with ensure_interruptible_after(1) as data: raise ValueError
866+
Traceback (most recent call last):
867+
...
868+
ValueError
869+
sage: data # abs tol 0.01
870+
{'alarm_raised': False, 'elapsed': 0.0}
871+
"""
872+
seconds = float(seconds)
873+
max_wait_after_interrupt = float(max_wait_after_interrupt)
874+
inaccuracy_tolerance = float(inaccuracy_tolerance)
875+
# use Python float to avoid slowdown with Sage Integer (see https://github.com/sagemath/cysignals/issues/215)
876+
data = {}
877+
start_time = walltime()
878+
alarm(seconds)
879+
alarm_raised = False
880+
881+
try:
882+
yield data
883+
except AlarmInterrupt as e:
884+
e.__traceback__ = None # workaround for https://github.com/python/cpython/pull/129276
885+
alarm_raised = True
886+
finally:
887+
before_cancel_alarm_elapsed = walltime() - start_time
888+
cancel_alarm()
889+
elapsed = walltime() - start_time
890+
data["elapsed"] = elapsed
891+
data["alarm_raised"] = alarm_raised
892+
893+
if elapsed > seconds + max_wait_after_interrupt:
894+
raise RuntimeError(
895+
f"Function is not interruptible within {seconds:.4f} seconds, only after {elapsed:.4f} seconds"
896+
+ ("" if alarm_raised else " (__exit__ called before interrupt check)"))
897+
898+
if alarm_raised:
899+
if elapsed < seconds - inaccuracy_tolerance:
900+
raise RuntimeError(f"Interrupted too early: {elapsed:.4f} < {seconds:.4f}, this should not happen")
901+
else:
902+
raise RuntimeError(f"Function terminates early after {elapsed:.4f} < {seconds:.4f} seconds")

src/sage/geometry/integral_points.pxi

+2-4
Original file line numberDiff line numberDiff line change
@@ -531,10 +531,8 @@ cpdef rectangular_box_points(list box_min, list box_max,
531531
....: (0, 0, 0, 0, 0, -1, 2, -1, 0),
532532
....: (0, 0, 0, 0, 0, 0, -1, 2, -1)]
533533
sage: P = Polyhedron(ieqs=ieqs)
534-
sage: alarm(0.5); P.integral_points()
535-
Traceback (most recent call last):
536-
...
537-
AlarmInterrupt
534+
sage: from sage.doctest.util import ensure_interruptible_after
535+
sage: with ensure_interruptible_after(0.5): P.integral_points()
538536
"""
539537
assert len(box_min) == len(box_max)
540538
assert not (count_only and return_saturated)

src/sage/libs/flint/nmod_poly_linkage.pxi

+2-4
Original file line numberDiff line numberDiff line change
@@ -536,10 +536,8 @@ cdef inline int celement_pow(nmod_poly_t res, nmod_poly_t x, long e, nmod_poly_t
536536
Make sure that exponentiation can be interrupted, see :issue:`17470`::
537537
538538
sage: n = 2^23
539-
sage: alarm(0.2); x^n; cancel_alarm()
540-
Traceback (most recent call last):
541-
...
542-
AlarmInterrupt
539+
sage: from sage.doctest.util import ensure_interruptible_after
540+
sage: with ensure_interruptible_after(0.2): (x^n).degree()
543541
"""
544542
if modulus != NULL:
545543
sig_on()

src/sage/libs/gap/element.pyx

+2-4
Original file line numberDiff line numberDiff line change
@@ -1135,10 +1135,8 @@ cdef class GapElement(RingElement):
11351135
Check that this can be interrupted gracefully::
11361136
11371137
sage: a, b = libgap.GL(1000, 3).GeneratorsOfGroup(); g = a * b
1138-
sage: alarm(0.5); g ^ (2 ^ 10000)
1139-
Traceback (most recent call last):
1140-
...
1141-
AlarmInterrupt
1138+
sage: from sage.doctest.util import ensure_interruptible_after
1139+
sage: with ensure_interruptible_after(0.5): g ^ (2 ^ 10000)
11421140
11431141
sage: libgap.CyclicGroup(2) ^ 2
11441142
Traceback (most recent call last):

src/sage/libs/libecm.pyx

+2-4
Original file line numberDiff line numberDiff line change
@@ -143,10 +143,8 @@ def ecmfactor(number, double B1, verbose=False, sigma=0):
143143
Check that ``ecmfactor`` can be interrupted (factoring a large
144144
prime number)::
145145
146-
sage: alarm(0.5); ecmfactor(2^521-1, 1e7)
147-
Traceback (most recent call last):
148-
...
149-
AlarmInterrupt
146+
sage: from sage.doctest.util import ensure_interruptible_after
147+
sage: with ensure_interruptible_after(0.5): ecmfactor(2^521-1, 1e7)
150148
151149
Some special cases::
152150

src/sage/matrix/matrix_integer_dense.pyx

+4-16
Original file line numberDiff line numberDiff line change
@@ -4385,14 +4385,8 @@ cdef class Matrix_integer_dense(Matrix_dense):
43854385
43864386
sage: A = random_matrix(ZZ, 2000, 2000)
43874387
sage: B = random_matrix(ZZ, 2000, 2000)
4388-
sage: t0 = walltime()
4389-
sage: alarm(2); A._solve_iml(B) # long time
4390-
Traceback (most recent call last):
4391-
...
4392-
AlarmInterrupt
4393-
sage: t = walltime(t0)
4394-
sage: t < 10 or t
4395-
True
4388+
sage: from sage.doctest.util import ensure_interruptible_after
4389+
sage: with ensure_interruptible_after(2, max_wait_after_interrupt=8): A._solve_iml(B)
43964390
43974391
ALGORITHM: Uses IML.
43984392
@@ -4549,14 +4543,8 @@ cdef class Matrix_integer_dense(Matrix_dense):
45494543
45504544
sage: A = random_matrix(ZZ, 2000, 2000)
45514545
sage: B = random_matrix(ZZ, 2000, 2000)
4552-
sage: t0 = walltime()
4553-
sage: alarm(2); A._solve_flint(B) # long time
4554-
Traceback (most recent call last):
4555-
...
4556-
AlarmInterrupt
4557-
sage: t = walltime(t0)
4558-
sage: t < 10 or t
4559-
True
4546+
sage: from sage.doctest.util import ensure_interruptible_after
4547+
sage: with ensure_interruptible_after(2, max_wait_after_interrupt=8): A._solve_flint(B)
45604548
45614549
AUTHORS:
45624550

src/sage/matrix/matrix_mod2_dense.pyx

+2-4
Original file line numberDiff line numberDiff line change
@@ -2005,10 +2005,8 @@ cdef class Matrix_mod2_dense(matrix_dense.Matrix_dense): # dense or sparse
20052005
sage: A = random_matrix(GF(2), n, m)
20062006
sage: x = random_vector(GF(2), m)
20072007
sage: B = A*x
2008-
sage: alarm(0.5); sol = A.solve_right(B)
2009-
Traceback (most recent call last):
2010-
...
2011-
AlarmInterrupt
2008+
sage: from sage.doctest.util import ensure_interruptible_after
2009+
sage: with ensure_interruptible_after(0.5): sol = A.solve_right(B)
20122010
"""
20132011
cdef mzd_t *B_entries = (<Matrix_mod2_dense>B)._entries
20142012

src/sage/rings/complex_arb.pyx

+4-7
Original file line numberDiff line numberDiff line change
@@ -1184,13 +1184,10 @@ class ComplexBallField(UniqueRepresentation, sage.rings.abc.ComplexBallField):
11841184
sage: ComplexBallField(100).integral(lambda x, _: sin(x), RBF(0), RBF(1))
11851185
[0.4596976941318602825990633926 +/- ...e-29]
11861186
1187-
sage: from cysignals.alarm import alarm
1188-
sage: alarm(0.1r)
1189-
sage: C = ComplexBallField(1000000)
1190-
sage: C.integral(lambda x, _: x.cos() * x.sin(), 0, 1)
1191-
Traceback (most recent call last):
1192-
...
1193-
AlarmInterrupt
1187+
sage: from sage.doctest.util import ensure_interruptible_after
1188+
sage: with ensure_interruptible_after(0.1):
1189+
....: C = ComplexBallField(1000000)
1190+
....: C.integral(lambda x, _: x.cos() * x.sin(), 0, 1)
11941191
"""
11951192
cdef IntegrationContext ctx = IntegrationContext()
11961193
cdef acb_calc_integrate_opt_t arb_opts

src/sage/rings/factorint_pari.pyx

+4-3
Original file line numberDiff line numberDiff line change
@@ -50,10 +50,11 @@ def factor_using_pari(n, int_=False, debug_level=0, proof=None):
5050
5151
Check that PARI's debug level is properly reset (:issue:`18792`)::
5252
53-
sage: alarm(0.5); factor(2^1000 - 1, verbose=5)
54-
Traceback (most recent call last):
53+
sage: from sage.doctest.util import ensure_interruptible_after
54+
sage: with ensure_interruptible_after(0.5): factor(2^1000 - 1, verbose=5)
5555
...
56-
AlarmInterrupt
56+
doctest:warning...
57+
RuntimeWarning: cypari2 leaked ... bytes on the PARI stack
5758
sage: pari.get_debug_level()
5859
0
5960
"""

src/sage/rings/integer.pyx

+3-8
Original file line numberDiff line numberDiff line change
@@ -7108,21 +7108,16 @@ cdef class Integer(sage.structure.element.EuclideanDomainElement):
71087108
71097109
Check that it can be interrupted (:issue:`17852`)::
71107110
7111-
sage: alarm(0.5); (2^100).binomial(2^22, algorithm='mpir')
7112-
Traceback (most recent call last):
7113-
...
7114-
AlarmInterrupt
7111+
sage: from sage.doctest.util import ensure_interruptible_after
7112+
sage: with ensure_interruptible_after(0.5): (2^100).binomial(2^22, algorithm='mpir')
71157113
71167114
For PARI, we try 10 interrupts with increasing intervals to
71177115
check for reliable interrupting, see :issue:`18919`::
71187116
71197117
sage: from cysignals import AlarmInterrupt
71207118
sage: for i in [1..10]: # long time (5s) # needs sage.libs.pari
7121-
....: try:
7122-
....: alarm(i/11)
7119+
....: with ensure_interruptible_after(i/11):
71237120
....: (2^100).binomial(2^22, algorithm='pari')
7124-
....: except AlarmInterrupt:
7125-
....: pass
71267121
doctest:...: RuntimeWarning: cypari2 leaked ... bytes on the PARI stack...
71277122
"""
71287123
cdef Integer x

src/sage/rings/polynomial/polynomial_element.pyx

+2-4
Original file line numberDiff line numberDiff line change
@@ -2533,10 +2533,8 @@ cdef class Polynomial(CommutativePolynomial):
25332533
sage: K.<a> = GF(2^8)
25342534
sage: x = polygen(K)
25352535
sage: pol = x^1000000 + x + a
2536-
sage: alarm(0.5); pol.any_root()
2537-
Traceback (most recent call last):
2538-
...
2539-
AlarmInterrupt
2536+
sage: from sage.doctest.util import ensure_interruptible_after
2537+
sage: with ensure_interruptible_after(0.5): pol.any_root()
25402538
25412539
Check root computation over large finite fields::
25422540

src/sage/rings/polynomial/polynomial_zmod_flint.pyx

+2-4
Original file line numberDiff line numberDiff line change
@@ -811,10 +811,8 @@ cdef class Polynomial_zmod_flint(Polynomial_template):
811811
812812
sage: R.<x> = PolynomialRing(GF(65537), implementation="FLINT")
813813
sage: f = R.random_element(9973) * R.random_element(10007)
814-
sage: alarm(0.5); f.factor()
815-
Traceback (most recent call last):
816-
...
817-
AlarmInterrupt
814+
sage: from sage.doctest.util import ensure_interruptible_after
815+
sage: with ensure_interruptible_after(0.5): f.factor()
818816
819817
Test zero polynomial::
820818

0 commit comments

Comments
 (0)