blob: e94646de342c3ba454fcf69eb52e764cb14cddcd [file] [edit]
############################################################################
# Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
############################################################################
"""Test sacct -c reading the jobcomp/mysql records of finished jobs."""
import random
import pytest
import atf
# Job names unique to this run, as the database can keep rows of old runs
token = random.randrange(0, 999999999)
@pytest.fixture(scope="module", autouse=True)
def setup():
atf.require_build_config("HAVE_MYSQL")
atf.require_accounting(modify=True)
atf.require_config_parameter("JobCompType", "jobcomp/mysql")
# The database user may only have access to the accounting database.
# set_config_parameter() keeps the case of the values.
slurmdbd_conf = atf.get_config(live=False, source="slurmdbd")
for name, default in (
("Loc", "slurm_acct_db"),
("User", atf.properties["slurm-user"]),
("Host", None),
("Port", None),
("Pass", None),
):
atf.set_config_parameter(
f"JobComp{name}", slurmdbd_conf.get(f"Storage{name}", default)
)
atf.require_slurm_running()
def run_job(name, command="true"):
"""Run a batch job until it is done and return its id."""
job_id = atf.submit_job_sbatch(
f"-J {name} -o /dev/null --wrap '{command}'", fatal=True
)
atf.wait_for_job_state(job_id, "DONE", fatal=True)
return job_id
def get_records(job_ids):
"""Return the sacct -c records of job_ids as {(id, name): [state, user, nodes]}."""
output = atf.run_command_output(
f"sacct -c -j {','.join(map(str, job_ids))} -n -P "
"-o jobid,jobname,state,user,nodelist"
)
rows = [line.split("|") for line in output.splitlines()]
return {(int(row[0]), row[1]): row[2:] for row in rows}
def test_sacct_completion():
"""sacct -c lists finished jobs from the jobcomp database, not pending ones."""
held_name = f"jobcomp_held_{token}"
ok_name = f"jobcomp_ok_{token}"
failed_name = f"jobcomp_failed_{token}"
held_id = atf.submit_job_sbatch(
f"-H -J {held_name} -o /dev/null --wrap true", fatal=True
)
# Accounting lists the pending job, so sacct -c must not read accounting
for _ in atf.timer(fatal=True):
if atf.run_command_output(f"sacct -j {held_id} -n -X -o jobid").strip():
break
# Old sacct versions can change the table, so read it after the jobs end
ok_id = run_job(ok_name)
failed_id = run_job(failed_name, "exit 3")
user = atf.properties["test-user"]
ok_nodes = atf.get_job_parameter(ok_id, "NodeList", fatal=True)
failed_nodes = atf.get_job_parameter(failed_id, "NodeList", fatal=True)
for _ in atf.timer():
records = get_records([ok_id, failed_id, held_id])
if {(ok_id, ok_name), (failed_id, failed_name)} <= records.keys():
break
assert records.get((ok_id, ok_name)) == [
"COMPLETED",
user,
ok_nodes,
], f"sacct -c should list job {ok_id} as COMPLETED by {user} on {ok_nodes}"
assert records.get((failed_id, failed_name)) == [
"FAILED",
user,
failed_nodes,
], f"sacct -c should list job {failed_id} as FAILED by {user} on {failed_nodes}"
assert (
held_id,
held_name,
) not in records, f"sacct -c should not list pending job {held_id}"