| ############################################################################ | |
| # Copyright (C) SchedMD LLC. | |
| ############################################################################ | |
| import logging | |
| import re | |
| import pytest | |
| import atf | |
| ERROR_TYPE = "error" | |
| OUTPUT_TYPE = "output" | |
| node_count = 4 | |
| # Setup | |
| @pytest.fixture(scope="module", autouse=True) | |
| def setup(): | |
| atf.require_nodes(node_count) | |
| atf.require_slurm_running() | |
| # creates either an error or an output file with file name formatting | |
| def create_file_path(fnp, file_type=OUTPUT_TYPE): | |
| if file_type == ERROR_TYPE: | |
| return f"file_err.%{fnp}.error" | |
| return f"file_out.%{fnp}.output" | |
| # creates either an error or an output file with file name formatting | |
| def create_file(val, file_type=OUTPUT_TYPE): | |
| if file_type == ERROR_TYPE: | |
| return f"file_err.{val}.error" | |
| return f"file_out.{val}.output" | |
| def remove_file(file_path): | |
| atf.run_command(f"rm {file_path}") | |
| # returns the directory listing, waiting until all expected files appear. | |
| # If expected is None, waits for at least one file. | |
| def wait_and_assert_files(expected=None): | |
| for t in atf.timer(quiet=True): | |
| files = atf.run_command_output("ls").splitlines() | |
| if expected is None: | |
| if files: | |
| return files | |
| elif all(f in files for f in expected): | |
| return files | |
| logging.debug( | |
| f"Found files: {files}. Expected files: {expected}. Remaining time: {t}" | |
| ) | |
| assert False, f"Expected files not found: expected={expected}, got={files}" | |
| def test_output_error_formatting(): | |
| """Verify srun stdout/err file name formatting (--output and --error options).""" | |
| # Test %t puts the task identifier in the file names | |
| task_count = 5 | |
| file_out = create_file_path("t") | |
| atf.run_job(f"--output={file_out} -N1 -n{task_count} -O id") | |
| tmp_dir_list = wait_and_assert_files( | |
| [create_file(task) for task in range(task_count)] | |
| ) | |
| for task in range(task_count): | |
| task_file = create_file(task) | |
| assert task_file in tmp_dir_list, f"%t: task file ({task_file}) was not created" | |
| remove_file(task_file) | |
| file_err = create_file_path("t", ERROR_TYPE) | |
| atf.run_job(f"--error={file_err} -N1 -n{task_count} -O uid") | |
| tmp_dir_list = wait_and_assert_files( | |
| [create_file(task, ERROR_TYPE) for task in range(task_count)] | |
| ) | |
| for task in range(task_count): | |
| task_file = create_file(task, ERROR_TYPE) | |
| assert task_file in tmp_dir_list, f"%t: task file ({task_file}) was not created" | |
| remove_file(task_file) | |
| # Test %j puts the job id in the file names | |
| file_out = create_file_path("j") | |
| job_id = atf.submit_job_srun(f"--output={file_out} -N1 -O id") | |
| file_out = wait_and_assert_files()[0] | |
| assert ( | |
| re.search(str(job_id), file_out) is not None | |
| ), f"%j: Job id ({job_id}) was not in file name ({file_out})" | |
| remove_file(file_out) | |
| file_err = create_file_path("j", ERROR_TYPE) | |
| job_id = atf.submit_job_srun(f"--error={file_err} -N1 -O uid") | |
| file_err = wait_and_assert_files()[0] | |
| assert ( | |
| re.search(str(job_id), file_err) is not None | |
| ), f"%j: Job id ({job_id}) was not in file name ({file_err})" | |
| remove_file(file_err) | |
| # Test %% creates a file with one % in the file name | |
| output_file = create_file("%") | |
| file_out = create_file_path("%") | |
| atf.run_job(f"--output={file_out} -N1 -O id") | |
| wait_and_assert_files([output_file]) | |
| remove_file(output_file) | |
| error_file = create_file("%", ERROR_TYPE) | |
| file_err = create_file_path("%", ERROR_TYPE) | |
| atf.run_job(f"--error={file_err} -N1 -O uid") | |
| wait_and_assert_files([error_file]) | |
| remove_file(error_file) | |
| # Test %J puts the job.step id in the file name | |
| file_out = create_file_path("J") | |
| error_out = atf.run_job_error(f"--output={file_out} -v -N1 -O id") | |
| match = re.search(r"(?<=StepId=)\d+\.\d+", error_out) | |
| step_id = match.group(0) | |
| file_out = wait_and_assert_files()[0] | |
| assert ( | |
| re.search(step_id, file_out) is not None | |
| ), f"%J: Step id ({step_id}) was not in file name ({file_out})" | |
| remove_file(file_out) | |
| file_err = create_file_path("J", ERROR_TYPE) | |
| error_out = atf.run_job_error(f"--error={file_err} -v -N1 -O uid") | |
| match = re.search(r"(?<=StepId=)\d+\.\d+", error_out) | |
| step_id = match.group(0) | |
| file_err = wait_and_assert_files()[0] | |
| assert ( | |
| re.search(step_id, file_err) is not None | |
| ), f"%J: Step id ({step_id}) was not in file name ({file_err})" | |
| remove_file(file_err) | |
| # Test %u puts the user name in the file name | |
| user_name = atf.properties["test-user"] | |
| file_out = create_file_path("u") | |
| atf.run_job(f"--output={file_out} -N1 -O id") | |
| file_out = wait_and_assert_files()[0] | |
| assert re.search( | |
| user_name, file_out | |
| ), f"%u: User name ({user_name}) was not in file name ({file_out})" | |
| remove_file(file_out) | |
| file_err = create_file_path("u", ERROR_TYPE) | |
| atf.run_job(f"--error={file_err} -N1 -O uid") | |
| file_err = wait_and_assert_files()[0] | |
| assert re.search( | |
| user_name, file_err | |
| ), f"%u: User name ({user_name}) was not in file name ({file_err})" | |
| remove_file(file_err) | |
| # Test %n puts the node identifier relative to current job in the file name | |
| node_id = 0 | |
| file_out = create_file_path("n") | |
| result_out = create_file(node_id) | |
| atf.run_job(f"--output={file_out} -N1 -O id") | |
| file_out = wait_and_assert_files()[0] | |
| assert ( | |
| result_out in file_out | |
| ), f"%n: Node id ({node_id}) was not in file name ({file_out})" | |
| remove_file(file_out) | |
| file_err = create_file_path("n", ERROR_TYPE) | |
| result_err = create_file(node_id, ERROR_TYPE) | |
| atf.run_job(f"--error={file_err} -N1 -O id") | |
| file_err = wait_and_assert_files()[0] | |
| assert ( | |
| result_err in file_err | |
| ), f"%n: Node id ({node_id}) was not in file name ({file_err})" | |
| remove_file(file_err) | |
| # Test %s puts the step identifier in the file name | |
| step_count = 4 | |
| node_count = "1" | |
| file_out = create_file_path("s") | |
| file_in = "file_in.s.input" | |
| atf.make_bash_script( | |
| file_in, | |
| f"""for i in {{1..{step_count}}} | |
| do | |
| srun -O --output={file_out} true | |
| done""", | |
| ) | |
| job_id = atf.submit_job_sbatch(f"-N{node_count} --output /dev/null {str(file_in)}") | |
| atf.wait_for_job_state(job_id, "DONE") | |
| tmp_dir_list = wait_and_assert_files( | |
| [create_file(step) for step in range(step_count)] | |
| ) | |
| for step in range(0, step_count): | |
| step_file = create_file(step) | |
| assert step_file in tmp_dir_list, f"%s: Step file ({step_file}) was not created" | |
| remove_file(step_file) | |
| file_err = create_file_path("s", ERROR_TYPE) | |
| atf.make_bash_script( | |
| file_in, | |
| f"""for i in {{1..{step_count}}} | |
| do | |
| srun -O --error={file_err} true | |
| done""", | |
| ) | |
| job_id = atf.submit_job_sbatch(f"-N{node_count} --output /dev/null {str(file_in)}") | |
| atf.wait_for_job_state(job_id, "DONE") | |
| tmp_dir_list = wait_and_assert_files( | |
| [create_file(step, ERROR_TYPE) for step in range(step_count)] | |
| ) | |
| for step in range(0, step_count): | |
| step_file = create_file(step, ERROR_TYPE) | |
| assert step_file in tmp_dir_list, f"%s: Step file ({step_file}) was not created" | |
| remove_file(step_file) | |
| # Test %x puts the Job name in the file name | |
| job_command = "uid" | |
| file_out = create_file_path("x") | |
| file_err = create_file_path("x", ERROR_TYPE) | |
| job_id = atf.submit_job_srun(f"--output={file_out} {job_command}") | |
| job_name = atf.get_job_parameter(job_id, "JobName") | |
| assert ( | |
| job_command == job_name | |
| ), f"%x: Job command ({job_command}) is not the same as the JobName ({job_name})" | |
| result_out = create_file(job_command) | |
| wait_and_assert_files([result_out]) | |
| remove_file(result_out) | |
| job_id = atf.submit_job_srun(f"--error={file_err} {job_command}") | |
| job_name = atf.get_job_parameter(job_id, "JobName") | |
| assert ( | |
| job_command == job_name | |
| ), f"%x: Job command ({job_command}) is not the same as the JobName ({job_name})" | |
| result_err = create_file(job_command, ERROR_TYPE) | |
| wait_and_assert_files([result_err]) | |
| remove_file(result_err) | |
| # Test %N puts the short hostname in the file name | |
| file_out = create_file_path("N") | |
| file_err = create_file_path("N", ERROR_TYPE) | |
| job_id = atf.submit_job_srun(f"--output={file_out} -N1 -O id") | |
| node_name = atf.get_job_parameter(job_id, "NodeList") | |
| if atf.get_version("sbin/slurmd") >= (25, 11): | |
| # Ticket 23357: Use the node_name instead of hostname with %N | |
| expected_name = node_name | |
| else: | |
| expected_name = atf.get_node_parameter(node_name, "hostname") | |
| node_addr = atf.get_node_parameter(node_name, "address") | |
| if node_addr != expected_name and not atf.is_integer(node_addr[0]): | |
| expected_name = node_addr | |
| result_out = create_file(expected_name) | |
| wait_and_assert_files([result_out]) | |
| remove_file(result_out) | |
| atf.submit_job_srun(f"--error={file_err} -N1 -O -w {node_name} true") | |
| result_err = create_file(expected_name, ERROR_TYPE) | |
| wait_and_assert_files([result_err]) | |
| remove_file(result_err) | |
| # Test %A puts the Job array's master job allocation number in the file name | |
| array_size = 2 | |
| file_out = create_file_path("A") | |
| file_err = create_file_path("A", ERROR_TYPE) | |
| file_in = "file_in.A.input" | |
| atf.make_bash_script(file_in, f"""srun -O --output={file_out} hostname""") | |
| job_id = atf.submit_job_sbatch( | |
| f"-N1 --output=/dev/null --array=1-{array_size} {file_in}" | |
| ) | |
| atf.wait_for_job_state(job_id, "DONE") | |
| remove_file(file_in) | |
| result_out = create_file(str(job_id)) | |
| wait_and_assert_files([result_out]) | |
| remove_file(result_out) | |
| atf.make_bash_script(file_in, f"""srun -O --error={file_err} uid""") | |
| job_id = atf.submit_job_sbatch( | |
| f"-N1 --output=/dev/null --array=1-{array_size} {file_in}" | |
| ) | |
| atf.wait_for_job_state(job_id, "DONE") | |
| remove_file(file_in) | |
| result_err = create_file(str(job_id), ERROR_TYPE) | |
| wait_and_assert_files([result_err]) | |
| remove_file(result_err) | |
| # Test %a puts the Job array ID in the file name | |
| array_size = 2 | |
| file_out = create_file_path("A.%a") | |
| file_err = create_file_path("A.%a", ERROR_TYPE) | |
| file_in = "file_in.A.a.input" | |
| atf.make_bash_script(file_in, f"""srun -O --output={file_out} hostname""") | |
| job_id = atf.submit_job_sbatch( | |
| f"-N1 --output=/dev/null --array=1-{array_size} {file_in}" | |
| ) | |
| atf.wait_for_job_state(job_id, "DONE") | |
| tmp_dir_list = wait_and_assert_files( | |
| [create_file(f"{job_id}.{array_id}") for array_id in range(1, array_size + 1)] | |
| ) | |
| for array_id in range(1, array_size + 1): | |
| id_file = create_file(str(job_id) + "." + str(array_id)) | |
| assert ( | |
| id_file in tmp_dir_list | |
| ), f"%a: Job array file ({id_file}) was not created" | |
| remove_file(id_file) | |
| atf.make_bash_script(file_in, f"""srun -O --error={file_err} uid""") | |
| job_id = atf.submit_job_sbatch( | |
| f"-N1 --output=/dev/null --array=1-{array_size} {file_in}" | |
| ) | |
| atf.wait_for_job_state(job_id, "DONE") | |
| tmp_dir_list = wait_and_assert_files( | |
| [ | |
| create_file(f"{job_id}.{array_id}", ERROR_TYPE) | |
| for array_id in range(1, array_size + 1) | |
| ] | |
| ) | |
| for array_id in range(1, array_size + 1): | |
| id_file = create_file(str(job_id) + "." + str(array_id), ERROR_TYPE) | |
| assert ( | |
| id_file in tmp_dir_list | |
| ), f"%a: Job array file ({id_file}) was not created" | |
| remove_file(id_file) |