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
20 changes: 14 additions & 6 deletions test/conftest.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,22 +129,24 @@ def launch_policy():
--import-mode=importlib, so they reach them through this fixture)."""
import types
return types.SimpleNamespace(scaled_timeout=scaled_timeout, died_by_signal=died_by_signal,
launched_environment=launched_environment, os=os)
launched_environment=launched_environment, retrying_run=retrying_run,
os=os)


@pytest.fixture(autouse=True)
def _retry_an_mpi_launch_that_never_started(monkeypatch):
"""Every ``subprocess.run`` from a test, hardened for a shared runner:
def retrying_run(run):
"""``run`` (a ``subprocess.run``), hardened for a shared runner:

* launched again, up to three times in all, if an mpiexec never started
(MPI_LAUNCH_FAILURES); the pause before a relaunch is PROTEUS_TEST_MPI_RETRY_DELAY
seconds (default 3), doubled before the last attempt;
* launched once more, with a RuntimeWarning, if it died by a signal (died_by_signal);
* its ``timeout`` scaled by PROTEUS_TEST_TIMEOUT_SCALE (scaled_timeout);
* run with PYTHONFAULTHANDLER=1 (launched_environment).

A function of ``run``, rather than closed over ``subprocess.run`` inside the fixture, so
that a test can hand it a stand-in and look at what it was called with.
"""
import warnings
run = subprocess.run

def run_with_retry(cmd, *args, **kwargs):
if 'timeout' in kwargs:
Expand All @@ -171,7 +173,13 @@ def run_with_retry(cmd, *args, **kwargs):
time.sleep(float(os.environ.get('PROTEUS_TEST_MPI_RETRY_DELAY', 3.0)))
continue
return result
monkeypatch.setattr(subprocess, 'run', run_with_retry)
return run_with_retry


@pytest.fixture(autouse=True)
def _retry_an_mpi_launch_that_never_started(monkeypatch):
"""Every ``subprocess.run`` from a test goes through :func:`retrying_run`."""
monkeypatch.setattr(subprocess, 'run', retrying_run(subprocess.run))


@pytest.fixture(autouse=True)
Expand Down
21 changes: 16 additions & 5 deletions test/test_mpi_launch_retry.py
Original file line number Diff line number Diff line change
Expand Up @@ -157,12 +157,23 @@ def test_a_launchers_signal_report_counts_as_a_crash(launch_policy):
assert died_by_signal(subprocess.CompletedProcess(['x'], 0, '', '')) is None


def test_the_timeout_is_scaled_for_a_slow_runner(monkeypatch):
def test_the_timeout_is_scaled_for_a_slow_runner(monkeypatch, launch_policy):
# Not by running something slow against a short timeout: that depends on how long an
# interpreter takes to start, which on a loaded runner is the very thing that varies
# (it failed there with 0.3 s of margin). The wrapper is handed a stand-in for
# subprocess.run and asked what timeout it passed on.
monkeypatch.setenv('PROTEUS_TEST_TIMEOUT_SCALE', '3')
# 1.2 s of sleep against a 0.5 s timeout: only the scale (1.5 s) lets it finish
result = subprocess.run([sys.executable, '-c', 'import time; time.sleep(1.2)'],
timeout=0.5, capture_output=True)
assert result.returncode == 0
seen = {}

def stand_in(cmd, *args, **kwargs):
seen.update(kwargs)
return subprocess.CompletedProcess(cmd, 0, b'', b'')

launch_policy.retrying_run(stand_in)(['x'], timeout=0.5)
assert seen['timeout'] == 1.5
seen.clear()
launch_policy.retrying_run(stand_in)(['x'])
assert 'timeout' not in seen # none given, none invented


def test_the_scale_is_at_least_one_and_at_most_four(monkeypatch, launch_policy):
Expand Down
Loading