#!/usr/bin/env python3
# coding: utf-8
#
# srop-false-positive-1: False positive for Syd's SROP detection
# Copyright (c) 2025 Ali Polatel <alip@chesswob.org>
# Based in part upon python's test_signal.py.
# Released under the same license as Python.

import contextlib
import signal
import sys
import threading
import unittest


@contextlib.contextmanager
def catch_unraisable_exception():
    class CM:
        def __init__(self):
            self.unraisable = None

    cm = CM()

    def hook(obj):
        cm.unraisable = obj

    old_hook = sys.unraisablehook
    sys.unraisablehook = hook
    try:
        yield cm
    finally:
        sys.unraisablehook = old_hook


class StressTest(unittest.TestCase):
    """
    Stress signal delivery, especially when a signal arrives in
    the middle of recomputing the signal state or executing
    previously tripped signal handlers.
    """

    @unittest.skipUnless(hasattr(signal, "SIGUSR1"), "test needs SIGUSR1")
    def test_stress_modifying_handlers(self):
        # bpo-43406: race condition between trip_signal() and signal.signal
        signum = signal.SIGUSR1
        num_sent_signals = 0
        num_received_signals = 0
        do_stop = False

        def custom_handler(signum, frame):
            nonlocal num_received_signals
            num_received_signals += 1

        def set_interrupts():
            nonlocal num_sent_signals
            while not do_stop:
                signal.raise_signal(signum)
                num_sent_signals += 1

        def cycle_handlers():
            while num_sent_signals < 100 or num_received_signals < 1:
                for i in range(20000):
                    # Cycle between a Python-defined and a non-Python handler
                    for handler in [custom_handler, signal.SIG_IGN]:
                        signal.signal(signum, handler)

        old_handler = signal.signal(signum, custom_handler)
        self.addCleanup(signal.signal, signum, old_handler)

        t = threading.Thread(target=set_interrupts)
        try:
            ignored = False
            with catch_unraisable_exception() as cm:
                t.start()
                cycle_handlers()
                do_stop = True
                t.join()

                if cm.unraisable is not None:
                    # An unraisable exception may be printed out when
                    # a signal is ignored due to the aforementioned
                    # race condition, check it.
                    self.assertIsInstance(cm.unraisable.exc_value, OSError)
                    self.assertIn(
                        f"Signal {signum:d} ignored due to race condition",
                        str(cm.unraisable.exc_value),
                    )
                    ignored = True

            # bpo-43406: Even if it is unlikely, it's technically possible that
            # all signals were ignored because of race conditions.
            if not ignored:
                # Sanity check that some signals were received, but not all
                self.assertGreater(num_received_signals, 0)
            self.assertLessEqual(num_received_signals, num_sent_signals)
        finally:
            do_stop = True
            t.join()


if __name__ == "__main__":
    unittest.main()
