blob: 2ceb4aeee5a60621b2fdd75ee94e393538da958b [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test that #SCRON --export=NIL stops a cron job loading the login environment.
scrontab used to set SLURM_GET_USER_ENV=1 for every entry, so slurmd loaded the
user's login environment even when the entry asked for --export=NIL. sbatch
documents NIL as the mode that skips that load. Requires Slurm 26.11+
(ticket 25969).
slurmd merges the login environment over the submitted one, so the job still
sees SLURM_GET_USER_ENV=1 whenever the load was requested. That variable is
the observable here.
The jobs have to run to be observed: the environment is not visible while a
job is pending, and AccountingStoreFlags=job_env only records it at job start.
Every entry runs each minute, so the test waits for the next minute boundary.
"""
import pytest
import atf
# Fires every minute. Unlike test_169_1, these jobs must actually run.
schedule = "* * * * *"
# NIL leaves the job without a PATH, so the command must be an absolute path.
command = "/usr/bin/env"
# One crontab holds every case so the module waits for one cron minute only.
cases = {
"cron_169_2_nil": ("--export=NIL", False),
"cron_169_2_none": ("--export=NONE", True),
"cron_169_2_default": (None, True),
}
pytestmark = pytest.mark.slow
# Setup
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_version(
(26, 11),
"bin/sbatch",
reason="Ticket 25969: scrontab honors #SCRON --export=NIL in 26.11",
)
atf.require_config_parameter_includes("ScronParameters", "enable")
atf.require_slurm_running()
@pytest.fixture(autouse=True)
def clean_crontab():
"""Guarantees every test both starts and ends with no crontab.
Removing the crontab cancels the entries' pending jobs, so no entry fires
again after the module.
"""
atf.run_command("scrontab -r", quiet=True)
yield
atf.run_command("scrontab -r", quiet=True)
def output_path(name):
"""Returns the absolute output file of the cron entry named name."""
return atf.module_tmp_path / f"{name}.out"
def install_crontab(*lines):
"""Installs a crontab built from lines and fatal if the command fails"""
crontab = "".join(f"{line}\n" for line in lines)
atf.run_command("scrontab -", input=crontab, fatal=True)
def read_job_env(name):
"""Returns the environment the cron job named name printed, once it ran.
The output file exists from the moment the job starts, so wait for a
variable slurmstepd always sets rather than for the file alone.
"""
text = ""
# Up to one cron minute, plus scheduling latency, before the job runs.
for _ in atf.timer(timeout=150):
text = atf.run_command_output(f"cat {output_path(name)}", quiet=True)
if "SLURM_JOB_ID=" in text:
break
else:
pytest.fail(f"Cron job {name} did not run and write its environment")
return dict(line.split("=", 1) for line in text.splitlines() if "=" in line)
def test_export_nil_skips_the_login_environment():
"""Verify only --export=NIL submits a cron job without SLURM_GET_USER_ENV.
NONE and an entry with no --export are the controls: both must still ask
slurmd for the login environment. Without them, a scrontab that never set
the flag at all would pass the NIL check too.
"""
lines = []
for name, (export, _) in cases.items():
lines.append(f"#SCRON --job-name={name}")
lines.append(f"#SCRON --output={output_path(name)}")
if export:
lines.append(f"#SCRON {export}")
lines.append(f"{schedule} {command}")
install_crontab(*lines)
for name, (export, wants_login_env) in cases.items():
env = read_job_env(name)
requested = env.get("SLURM_GET_USER_ENV") == "1"
if wants_login_env:
assert (
requested
), f"{export or 'No --export'} should load the login environment"
else:
assert not requested, f"{export} should not load the login environment"