Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 23 additions & 0 deletions tests/support.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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``."""

Expand Down
3 changes: 2 additions & 1 deletion tests/test_brokered_process.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")

Expand Down Expand Up @@ -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
Expand Down
23 changes: 21 additions & 2 deletions tests/test_worker_isolation.py
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
Expand All @@ -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(
Expand Down
Loading