Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix alarm tests #39142

Draft
wants to merge 8 commits into
base: develop
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from 7 commits
Commits
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
8 changes: 8 additions & 0 deletions src/doc/en/developer/coding_basics.rst
Original file line number Diff line number Diff line change
Expand Up @@ -979,6 +979,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.

Expand Down
6 changes: 2 additions & 4 deletions src/sage/coding/linear_code.py
Original file line number Diff line number Diff line change
Expand Up @@ -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(), \
Expand Down
6 changes: 2 additions & 4 deletions src/sage/data_structures/bounded_integer_sequences.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
148 changes: 148 additions & 0 deletions src/sage/doctest/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -749,3 +751,149 @@
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 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, time_t
....: from cysignals.signals cimport sig_check
....:
....: cpdef void uninterruptible_sleep(double seconds):
....: cdef timespec start_time, target_time
....: clock_gettime(CLOCK_REALTIME, &start_time)
....:
....: cdef time_t floor_seconds = <time_t>seconds
....: target_time.tv_sec = start_time.tv_sec + floor_seconds
....: target_time.tv_nsec = start_time.tv_nsec + <long>((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): # 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.60... seconds
sage: with ensure_interruptible_after(1, max_wait_after_interrupt=0.9):
....: check_interrupt_only_occasionally()

TESTS::

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 1
{'alarm_raised': False, 'elapsed': 1.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 = <time_t>seconds
....: target_time.tv_sec = start_time.tv_sec + floor_seconds
....: target_time.tv_nsec = start_time.tv_nsec + <long>((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):
...
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.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)
inaccuracy_tolerance = float(inaccuracy_tolerance)
# use Python float to avoid unexplained slowdown with Sage objects
data = {}
start_time = walltime()
alarm(seconds)
alarm_raised = False

try:
yield data
except AlarmInterrupt:
alarm_raised = True
finally:
before_cancel_alarm_elapsed = walltime() - start_time
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")

Check warning on line 897 in src/sage/doctest/util.py

View check run for this annotation

Codecov / codecov/patch

src/sage/doctest/util.py#L897

Added line #L897 was not covered by tests
else:
raise RuntimeError(f"Function terminates early after {elapsed:.4f} < {seconds:.4f} seconds")
6 changes: 2 additions & 4 deletions src/sage/geometry/integral_points.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
6 changes: 2 additions & 4 deletions src/sage/libs/flint/nmod_poly_linkage.pxi
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
6 changes: 2 additions & 4 deletions src/sage/libs/gap/element.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down
6 changes: 2 additions & 4 deletions src/sage/libs/libecm.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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::

Expand Down
20 changes: 4 additions & 16 deletions src/sage/matrix/matrix_integer_dense.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -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:

Expand Down
6 changes: 2 additions & 4 deletions src/sage/matrix/matrix_mod2_dense.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = (<Matrix_mod2_dense>B)._entries

Expand Down
11 changes: 4 additions & 7 deletions src/sage/rings/complex_arb.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
7 changes: 4 additions & 3 deletions src/sage/rings/factorint_pari.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -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
"""
Expand Down
11 changes: 3 additions & 8 deletions src/sage/rings/integer.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -3071,7 +3071,7 @@
so a memory leak will not go unnoticed)::

sage: n = prod(primes_first_n(25)) # needs sage.libs.pari
sage: for i in range(20): # long time # needs sage.libs.pari

Check warning on line 3074 in src/sage/rings/integer.pyx

View workflow job for this annotation

GitHub Actions / test-new

Warning: slow doctest:

slow doctest:

Check warning on line 3074 in src/sage/rings/integer.pyx

View workflow job for this annotation

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

Warning: slow doctest:

slow doctest:
....: try:
....: alarm(RDF.random_element(1e-3, 0.5))
....: _ = n.divisors()
Expand Down Expand Up @@ -7108,21 +7108,16 @@

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
Expand Down
6 changes: 2 additions & 4 deletions src/sage/rings/polynomial/polynomial_element.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -2533,10 +2533,8 @@ cdef class Polynomial(CommutativePolynomial):
sage: K.<a> = 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::

Expand Down
6 changes: 2 additions & 4 deletions src/sage/rings/polynomial/polynomial_zmod_flint.pyx
Original file line number Diff line number Diff line change
Expand Up @@ -811,10 +811,8 @@ cdef class Polynomial_zmod_flint(Polynomial_template):

sage: R.<x> = 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::

Expand Down
Loading
Loading