From 0dfb6b1ffff106c9b37db1c3ae50d30ac01ca57b Mon Sep 17 00:00:00 2001 From: Matthew Robert Wesney <157447210+dovvnloading@users.noreply.github.com> Date: Sat, 17 Oct 2026 08:29:59 -0400 Subject: [PATCH] test(execution): start the job-object tests on the real interpreter Inside a Windows virtual environment sys.executable is the venv redirector, which starts the base interpreter as its own child in a job object of its own. Four tests put sys.executable in a job and read the job back from inside the child, so in a venv they read the redirector's job (limit flags 0x3000) and a one-process job refused the interpreter as a second process. They pass on a plain interpreter, which is why CI never saw it; CONTRIBUTING.md tells contributors to work in a venv. tests/support.py gains real_interpreter() (sys._base_executable on Windows, else sys.executable), used by _run_in_job and the brokered job-probe test, plus a test that the process started is the process that runs. The workers are unaffected: multiprocessing already starts the base interpreter directly in a venv. Co-Authored-By: Claude Opus 5.5 --- tests/support.py | 23 +++++++++++++++++++++++ tests/test_brokered_process.py | 3 ++- tests/test_worker_isolation.py | 23 +++++++++++++++++++++-- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/tests/support.py b/tests/support.py index c3d7244b..ea0e3298 100644 --- a/tests/support.py +++ b/tests/support.py @@ -9,6 +9,8 @@ from collections.abc import Callable from datetime import datetime, timedelta, timezone import json +import os +import sys import threading import time from typing import Any @@ -60,6 +62,27 @@ def wait_until( time.sleep(0.005) +def real_interpreter() -> str: + """The Python interpreter itself, for a child a test places in a job object. + + Inside a Windows virtual environment ``sys.executable`` is a small + redirector that starts the base interpreter as its own child, in a job + object of its own. A test that puts ``sys.executable`` in a job and asks the + child which job it is in then reads the redirector's job, and a job limited + to one process refuses the interpreter as the second. The workers are not + affected: ``multiprocessing`` starts the base interpreter directly for the + same reason. The base interpreter does not see the virtual environment's + packages, so run only standard-library code with it. Outside a virtual + environment this is ``sys.executable``. + """ + + if os.name == "nt": + base = getattr(sys, "_base_executable", None) + if base and os.path.isfile(base): + return str(base) + return sys.executable + + def live_cortex_threads(baseline: set[threading.Thread]) -> list[threading.Thread]: """Return the ``cortex-*`` threads alive now that were not in ``baseline``.""" diff --git a/tests/test_brokered_process.py b/tests/test_brokered_process.py index 0efd6325..9ceef16c 100644 --- a/tests/test_brokered_process.py +++ b/tests/test_brokered_process.py @@ -28,6 +28,7 @@ CodeExecutionError, ) import job_probe +from support import real_interpreter windows_only = pytest.mark.skipif(os.name != "nt", reason="uses Windows job objects") @@ -187,7 +188,7 @@ def test_a_brokered_process_reads_back_the_job_it_was_placed_in(): """Run through the real broker: the child's first act is to ask which job it is in.""" result = code_execution._run_brokered_process( - [sys.executable, str(Path(job_probe.__file__))], workspace=Path.cwd(), timeout=60 + [real_interpreter(), str(Path(job_probe.__file__))], workspace=Path.cwd(), timeout=60 ) assert result["returncode"] == 0 diff --git a/tests/test_worker_isolation.py b/tests/test_worker_isolation.py index 80a48d2b..36b77c33 100644 --- a/tests/test_worker_isolation.py +++ b/tests/test_worker_isolation.py @@ -55,7 +55,7 @@ from cortex_backend.execution.local_scratch_attempt import SCRATCH_WORKER_JOB_LIMITS, LocalScratchAttempt from cortex_backend.execution.recipe_coordinator import RecipeExecutionError from cortex_backend.execution.recipes import parse_image_transform -from support import wait_until +from support import real_interpreter, wait_until import job_probe import worker_probes @@ -94,10 +94,12 @@ def _run_in_job(limits: JobLimits, *arguments: str) -> str: The child waits for one line on stdin before it does anything, so the job is attached first; that is the whole point of holding a worker at a checkpoint. + The child is the interpreter itself, not a virtual environment's redirector + (see ``real_interpreter``), so the job is attached to the process that runs. """ child = subprocess.Popen( - [sys.executable, *arguments], + [real_interpreter(), *arguments], stdin=subprocess.PIPE, stdout=subprocess.PIPE, text=True, @@ -114,6 +116,23 @@ def _run_in_job(limits: JobLimits, *arguments: str) -> str: child.wait(timeout=30) +def test_the_interpreter_the_job_tests_start_is_the_process_that_runs(): + """The process handed to the job is the one that executes the test's code. + + Under a Windows virtual environment ``sys.executable`` fails this: the + process started is a redirector and the code runs in its child. + """ + + child = subprocess.Popen( + [real_interpreter(), "-c", "import os; print(os.getpid())"], + stdout=subprocess.PIPE, + text=True, + ) + output, _ = child.communicate(timeout=60) + + assert int(output) == child.pid + + @windows_only def test_a_real_job_carries_exactly_the_limits_it_was_given(): limits = JobLimits(