diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index a207f13..c14eb08 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -48,7 +48,7 @@ jobs: strategy: fail-fast: false matrix: - python_version: ["3.8", "3.9", "3.10", "3.11", "3.12"] + python_version: ["3.8", "3.9", "3.10", "3.11", "3.12", "3.13", "3.14"] steps: - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 diff --git a/.github/workflows/validate.yaml b/.github/workflows/validate.yaml index acbf87e..adda169 100644 --- a/.github/workflows/validate.yaml +++ b/.github/workflows/validate.yaml @@ -19,7 +19,7 @@ jobs: - name: Set up Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0 with: - python-version: '3.12-dev' + python-version: "3.14" - name: Install Python dependencies run: | python3 -m pip install -r requirements-dev.txt diff --git a/pyproject.toml b/pyproject.toml index d42508a..8d7b5b8 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -25,6 +25,8 @@ classifiers = [ "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", + "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Framework :: Pytest", ] diff --git a/src/pytest_pystack/_debug_detect.py b/src/pytest_pystack/_debug_detect.py index 5e2784f..d8eafa1 100644 --- a/src/pytest_pystack/_debug_detect.py +++ b/src/pytest_pystack/_debug_detect.py @@ -1,8 +1,9 @@ import inspect # used for introspection of module name -import multiprocessing import sys -debug_detected = multiprocessing.Event() +from ._multiprocessing_context import MP_CTX + +debug_detected = MP_CTX.Event() # bdb covers pdb, ipdb, and possibly others # pydevd covers PyCharm, VSCode, and possibly others KNOWN_DEBUGGING_MODULES = {"pydevd", "bdb", "pydevd_frame_evaluator"} diff --git a/src/pytest_pystack/_monitor.py b/src/pytest_pystack/_monitor.py new file mode 100644 index 0000000..e6a1c8a --- /dev/null +++ b/src/pytest_pystack/_monitor.py @@ -0,0 +1,53 @@ +import shlex +import subprocess +import sys +from queue import Empty + +from ._config import PystackConfig + + +def monitor(config: PystackConfig, pid, queue, debug_detected): + pystack_cmd = [ + config.pystack_path, + "remote", + ] + if config.pystack_args: + pystack_cmd += shlex.split(config.pystack_args) + handled_test_cases = set() + while True: + testcase = queue.get() + if testcase is None: + break + + if testcase in handled_test_cases: + continue + handled_test_cases.add(testcase) + + try: + new_testcase = queue.get(timeout=config.threshold) + if new_testcase != testcase: + print( + f"new test {new_testcase} should not start before previous {testcase} test finished", + file=sys.__stderr__, + ) + raise Exception( + "new test should not start before previous test finished" + ) + except Empty: + output = "" + output += f"\n\n**** PYSTACK -- {testcase} ***\n" + output += f"Timed out waiting for process {pid} to finish {testcase}:" + proc = subprocess.run( + [*pystack_cmd, str(pid)], + stdout=subprocess.PIPE, + text=True, + ) + output += proc.stdout + output += "**** PYSTACK ***\n" + is_debug = debug_detected.is_set() + debug_detected.clear() + if config.print_stderr and not is_debug: + print(output, file=sys.__stderr__) + if config.output_file: + with open(config.output_file, "a") as f: + print(output, file=f) diff --git a/src/pytest_pystack/_monitor_process.py b/src/pytest_pystack/_monitor_process.py index b24fa0a..5ac6dcb 100644 --- a/src/pytest_pystack/_monitor_process.py +++ b/src/pytest_pystack/_monitor_process.py @@ -1,25 +1,22 @@ -import multiprocessing import os -import shlex -import subprocess -import sys -from queue import Empty -from ._config import PystackConfig from ._debug_detect import debug_detected +from ._monitor import monitor +from ._multiprocessing_context import MP_CTX -_queue = multiprocessing.Queue() +_queue = MP_CTX.Queue() _process = None def start(config): global _process - _process = multiprocessing.Process( - target=_run_monitor, + _process = MP_CTX.Process( + target=monitor, args=( config, os.getpid(), _queue, + debug_detected, ), name="pystack_monitor", ) @@ -33,50 +30,3 @@ def stop(): _process.join(timeout=5) if _process.is_alive(): _process.kill() - - -def _run_monitor(config: PystackConfig, pid, queue): - pystack_cmd = [ - config.pystack_path, - "remote", - ] - if config.pystack_args: - pystack_cmd += shlex.split(config.pystack_args) - handled_test_cases = set() - while True: - testcase = queue.get() - if testcase is None: - break - - if testcase in handled_test_cases: - continue - handled_test_cases.add(testcase) - - try: - new_testcase = queue.get(timeout=config.threshold) - if new_testcase != testcase: - print( - f"new test {new_testcase} should not start before previous {testcase} test finished", - file=sys.__stderr__, - ) - raise Exception( - "new test should not start before previous test finished" - ) - except Empty: - output = "" - output += f"\n\n**** PYSTACK -- {testcase} ***\n" - output += f"Timed out waiting for process {pid} to finish {testcase}:" - proc = subprocess.run( - [*pystack_cmd, str(pid)], - stdout=subprocess.PIPE, - text=True, - ) - output += proc.stdout - output += "**** PYSTACK ***\n" - is_debug = debug_detected.is_set() - debug_detected.clear() - if config.print_stderr and not is_debug: - print(output, file=sys.__stderr__) - if config.output_file: - with open(config.output_file, "a") as f: - print(output, file=f) diff --git a/src/pytest_pystack/_multiprocessing_context.py b/src/pytest_pystack/_multiprocessing_context.py new file mode 100644 index 0000000..ddc21c7 --- /dev/null +++ b/src/pytest_pystack/_multiprocessing_context.py @@ -0,0 +1,7 @@ +import multiprocessing + +# We have known incompatibilities with the "forkserver" start method. +# The "fork" method is expected to work, but can potentially lead to +# strange failure modes because of things being inherited by our monitor +# process that shouldn't have been. Use "spawn" unconditionally instead. +MP_CTX = multiprocessing.get_context("spawn")