|
25 | 25 |
|
26 | 26 | from time import time as walltime
|
27 | 27 | from os import sysconf, times
|
| 28 | +from contextlib import contextmanager |
| 29 | +from cysignals.alarm import alarm, cancel_alarm, AlarmInterrupt |
28 | 30 |
|
29 | 31 |
|
30 | 32 | def count_noun(number, noun, plural=None, pad_number=False, pad_noun=False):
|
@@ -749,3 +751,152 @@ def __ne__(self, other):
|
749 | 751 | True
|
750 | 752 | """
|
751 | 753 | 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") |
0 commit comments