|  | 
| 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,119 @@ 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 0.2 | 
|  | 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: cython(''' | 
|  | 775 | +        ....: from libc.time cimport clock_t, clock, CLOCKS_PER_SEC | 
|  | 776 | +        ....: from cysignals.signals cimport sig_check | 
|  | 777 | +        ....: cpdef void uninterruptible_sleep(double seconds): | 
|  | 778 | +        ....:     cdef clock_t target = clock() + <clock_t>(CLOCKS_PER_SEC * seconds) | 
|  | 779 | +        ....:     while clock() < target: | 
|  | 780 | +        ....:         pass | 
|  | 781 | +        ....: cpdef void check_interrupt_only_occasionally(): | 
|  | 782 | +        ....:     for i in range(10): | 
|  | 783 | +        ....:         uninterruptible_sleep(0.8) | 
|  | 784 | +        ....:         sig_check() | 
|  | 785 | +        ....: ''', compiler_directives={'preliminary_late_includes_cy28': True}) | 
|  | 786 | +        sage: with ensure_interruptible_after(1) as data:  # not passing max_wait_after_interrupt will raise an error | 
|  | 787 | +        ....:     check_interrupt_only_occasionally() | 
|  | 788 | +        Traceback (most recent call last): | 
|  | 789 | +        ... | 
|  | 790 | +        RuntimeError: Function is not interruptible within 1.0000 seconds, only after 1... seconds | 
|  | 791 | +        sage: with ensure_interruptible_after(1, max_wait_after_interrupt=0.7): | 
|  | 792 | +        ....:     check_interrupt_only_occasionally() | 
|  | 793 | +
 | 
|  | 794 | +    TESTS:: | 
|  | 795 | +
 | 
|  | 796 | +        sage: data['elapsed']  # abs tol 0.2  # 1.6 = 0.8 * 2 | 
|  | 797 | +        1.6 | 
|  | 798 | +
 | 
|  | 799 | +    This test ensures the ``# cython: ...`` header comment doesn't work | 
|  | 800 | +    and ``cysignals`` requires ``preliminary_late_includes_cy28=True`` | 
|  | 801 | +    (when it is finally fixed then this test and the one above can be modified | 
|  | 802 | +    to remove the flag):: | 
|  | 803 | +
 | 
|  | 804 | +        sage: # needs sage.misc.cython | 
|  | 805 | +        sage: cython(''' | 
|  | 806 | +        ....: # cython: preliminary_late_includes_cy28=True | 
|  | 807 | +        ....: # ^ required by cysignals | 
|  | 808 | +        ....: from cysignals.signals cimport sig_check | 
|  | 809 | +        ....: ''') | 
|  | 810 | +        Traceback (most recent call last): | 
|  | 811 | +        ... | 
|  | 812 | +        RuntimeError: ... | 
|  | 813 | +
 | 
|  | 814 | +    :: | 
|  | 815 | +
 | 
|  | 816 | +        sage: with ensure_interruptible_after(2) as data: sleep(1) | 
|  | 817 | +        Traceback (most recent call last): | 
|  | 818 | +        ... | 
|  | 819 | +        RuntimeError: Function terminates early after 1... < 2.0000 seconds | 
|  | 820 | +        sage: data  # abs tol 0.2 | 
|  | 821 | +        {'alarm_raised': False, 'elapsed': 1.0} | 
|  | 822 | +        sage: with ensure_interruptible_after(1) as data: raise ValueError | 
|  | 823 | +        Traceback (most recent call last): | 
|  | 824 | +        ... | 
|  | 825 | +        ValueError | 
|  | 826 | +        sage: data  # abs tol 0.2 | 
|  | 827 | +        {'alarm_raised': False, 'elapsed': 0.0} | 
|  | 828 | +
 | 
|  | 829 | +    :: | 
|  | 830 | +
 | 
|  | 831 | +        sage: # needs sage.misc.cython | 
|  | 832 | +        sage: with ensure_interruptible_after(1) as data: uninterruptible_sleep(2) | 
|  | 833 | +        Traceback (most recent call last): | 
|  | 834 | +        ... | 
|  | 835 | +        RuntimeError: Function is not interruptible within 1.0000 seconds, only after 2... seconds | 
|  | 836 | +        sage: data  # abs tol 0.2 | 
|  | 837 | +        {'alarm_raised': True, 'elapsed': 2.0} | 
|  | 838 | +        sage: with ensure_interruptible_after(1): uninterruptible_sleep(2); raise RuntimeError | 
|  | 839 | +        Traceback (most recent call last): | 
|  | 840 | +        ... | 
|  | 841 | +        RuntimeError: Function is not interruptible within 1.0000 seconds, only after 2... seconds | 
|  | 842 | +        sage: data  # abs tol 0.2 | 
|  | 843 | +        {'alarm_raised': True, 'elapsed': 2.0} | 
|  | 844 | +    """ | 
|  | 845 | +    data = {} | 
|  | 846 | +    start_time = walltime() | 
|  | 847 | +    alarm(seconds) | 
|  | 848 | +    alarm_raised = False | 
|  | 849 | + | 
|  | 850 | +    try: | 
|  | 851 | +        yield data | 
|  | 852 | +    except AlarmInterrupt: | 
|  | 853 | +        alarm_raised = True | 
|  | 854 | +    finally: | 
|  | 855 | +        cancel_alarm() | 
|  | 856 | +        elapsed = walltime() - start_time | 
|  | 857 | +        data["elapsed"] = elapsed | 
|  | 858 | +        data["alarm_raised"] = alarm_raised | 
|  | 859 | + | 
|  | 860 | +    if elapsed > seconds + max_wait_after_interrupt: | 
|  | 861 | +        raise RuntimeError( | 
|  | 862 | +                f"Function is not interruptible within {seconds:.4f} seconds, only after {elapsed:.4f} seconds" | 
|  | 863 | +                + ("" if alarm_raised else " (__exit__ called before interrupt check)")) | 
|  | 864 | + | 
|  | 865 | +    if alarm_raised: | 
|  | 866 | +        if elapsed < seconds - inaccuracy_tolerance: | 
|  | 867 | +            raise RuntimeError(f"Interrupted too early: {elapsed:.4f} < {seconds:.4f}, this should not happen") | 
|  | 868 | +    else: | 
|  | 869 | +        raise RuntimeError(f"Function terminates early after {elapsed:.4f} < {seconds:.4f} seconds") | 
0 commit comments