Thanks to visit codestin.com
Credit goes to github.com

Skip to content

Commit d7d79af

Browse files
committed
Warn about unknown placeholders directly after submitting a job
1 parent c7b034e commit d7d79af

6 files changed

Lines changed: 200 additions & 119 deletions

File tree

crates/hyperqueue/src/client/commands/submit.rs

Lines changed: 39 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,9 +18,10 @@ use crate::client::status::StatusList;
1818
use crate::common::arraydef::IntArray;
1919
use crate::common::fsutils::get_current_dir;
2020
use crate::common::placeholders::{
21-
parse_resolvable_string, StringPart, CWD_PLACEHOLDER, JOB_ID_PLACEHOLDER,
22-
SUBMIT_DIR_PLACEHOLDER, TASK_ID_PLACEHOLDER,
21+
get_unknown_placeholders, parse_resolvable_string, StringPart, CWD_PLACEHOLDER,
22+
JOB_ID_PLACEHOLDER, SUBMIT_DIR_PLACEHOLDER, TASK_ID_PLACEHOLDER,
2323
};
24+
use crate::common::strutils::pluralize;
2425
use crate::common::timeutils::ArgDuration;
2526
use crate::transfer::connection::ClientConnection;
2627
use crate::transfer::messages::{
@@ -414,6 +415,41 @@ fn warn_missing_task_id(opts: &JobSubmitOpts, task_count: u32) {
414415
}
415416
}
416417

418+
/// Warns about unknown placeholders in various paths.
419+
fn warn_unknown_placeholders(opts: &JobSubmitOpts) {
420+
let check = |path: Option<&Path>, context: &str| {
421+
if let Some(path) = path {
422+
let unknown = get_unknown_placeholders(path.to_str().unwrap());
423+
if !unknown.is_empty() {
424+
let placeholder_str = pluralize("placeholder", unknown.len());
425+
log::warn!(
426+
"Found unknown {} `{}` in {}",
427+
placeholder_str,
428+
unknown.join(", "),
429+
context
430+
);
431+
}
432+
}
433+
};
434+
435+
check(
436+
opts.stdout.as_ref().and_then(|arg| match &arg.0 {
437+
StdioDef::File(path) => Some(path.as_path()),
438+
_ => None,
439+
}),
440+
"stdout path",
441+
);
442+
check(
443+
opts.stderr.as_ref().and_then(|arg| match &arg.0 {
444+
StdioDef::File(path) => Some(path.as_path()),
445+
_ => None,
446+
}),
447+
"stderr path",
448+
);
449+
check(opts.log.as_deref(), "log path");
450+
check(Some(opts.cwd.as_path()), "working directory path");
451+
}
452+
417453
/// Returns an error if working directory contains the CWD placeholder.
418454
fn check_valid_cwd(opts: &JobSubmitOpts) -> anyhow::Result<()> {
419455
let placeholders = parse_resolvable_string(opts.cwd.to_str().unwrap());
@@ -431,6 +467,7 @@ fn check_valid_cwd(opts: &JobSubmitOpts) -> anyhow::Result<()> {
431467
fn check_suspicious_options(opts: &JobSubmitOpts, task_count: u32) -> anyhow::Result<()> {
432468
warn_array_task_count(opts, task_count);
433469
warn_missing_task_id(opts, task_count);
470+
warn_unknown_placeholders(opts);
434471
check_valid_cwd(opts)?;
435472
Ok(())
436473
}

crates/hyperqueue/src/common/placeholders.rs

Lines changed: 23 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@ use crate::common::env::{HQ_INSTANCE_ID, HQ_JOB_ID, HQ_SUBMIT_DIR, HQ_TASK_ID};
1414
use crate::common::parser::NomResult;
1515
use crate::{JobId, Map};
1616

17+
// If a new placeholder is added, also change `get_unknown_placeholders`
1718
pub const TASK_ID_PLACEHOLDER: &str = "TASK_ID";
1819
pub const JOB_ID_PLACEHOLDER: &str = "JOB_ID";
1920
pub const INSTANCE_ID_PLACEHOLDER: &str = "INSTANCE_ID";
@@ -63,6 +64,27 @@ pub fn fill_placeholders_log(value: &mut PathBuf, job_id: JobId, submit_dir: &Pa
6364
*value = resolve(&placeholders, value.to_str().unwrap()).into();
6465
}
6566

67+
/// Find placeholders in the input that are not supported by HyperQueue.
68+
pub fn get_unknown_placeholders(input: &str) -> Vec<&str> {
69+
let known_placeholders = [
70+
TASK_ID_PLACEHOLDER,
71+
JOB_ID_PLACEHOLDER,
72+
INSTANCE_ID_PLACEHOLDER,
73+
CWD_PLACEHOLDER,
74+
SUBMIT_DIR_PLACEHOLDER,
75+
];
76+
77+
let mut unknown = Vec::new();
78+
for placeholder in parse_resolvable_string(input) {
79+
if let StringPart::Placeholder(placeholder) = placeholder {
80+
if !known_placeholders.contains(&placeholder) {
81+
unknown.push(placeholder);
82+
}
83+
}
84+
}
85+
unknown
86+
}
87+
6688
fn insert_submit_data<'a>(map: &mut PlaceholderMap<'a>, job_id: JobId, submit_dir: &'a Path) {
6789
map.insert(JOB_ID_PLACEHOLDER, job_id.to_string().into());
6890
map.insert(SUBMIT_DIR_PLACEHOLDER, submit_dir.to_str().unwrap().into());
@@ -101,14 +123,7 @@ fn resolve(map: &PlaceholderMap, input: &str) -> String {
101123
StringPart::Verbatim(data) => buffer.write_str(data),
102124
StringPart::Placeholder(placeholder) => match map.get(placeholder) {
103125
Some(value) => buffer.write_str(value.deref()),
104-
None => {
105-
log::warn!(
106-
"Encountered an unknown placeholder `{}` in `{}`",
107-
placeholder,
108-
input
109-
);
110-
buffer.write_fmt(format_args!("%{{{}}}", placeholder))
111-
}
126+
None => buffer.write_fmt(format_args!("%{{{}}}", placeholder)),
112127
},
113128
}
114129
.unwrap();

tests/test_array.py

Lines changed: 0 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
import os
44
import time
55

6-
import pytest
7-
86
from .conftest import HqEnv
97
from .utils import JOB_TABLE_ROWS, wait_for_job_state
108
from .utils.job import default_task_output
@@ -204,20 +202,6 @@ def test_array_mix_with_simple_jobs(hq_env: HqEnv):
204202
assert table.get_column_value("Tasks")[i] == "4" if i % 2 == 0 else "1"
205203

206204

207-
@pytest.mark.parametrize("channel", ("stdout", "stderr"))
208-
def test_warning_missing_placeholder_in_output(hq_env: HqEnv, channel: str):
209-
hq_env.start_server()
210-
output = hq_env.command(
211-
["submit", "--array=1-4", f"--{channel}=foo", "/bin/hostname"]
212-
)
213-
assert (
214-
f"You have submitted an array job, but the `{channel}` "
215-
+ "path does not contain the task ID placeholder."
216-
in output
217-
)
218-
assert f"Consider adding `%{{TASK_ID}}` to the `--{channel}` value."
219-
220-
221205
def test_array_times(hq_env: HqEnv):
222206
hq_env.start_server()
223207
hq_env.start_worker()

tests/test_job.py

Lines changed: 1 addition & 80 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import os
22
import time
33
from datetime import datetime
4-
from os.path import abspath, isdir, isfile
4+
from os.path import isdir, isfile
55
from pathlib import Path
66

77
import pytest
@@ -166,15 +166,6 @@ def test_job_output_default(hq_env: HqEnv, tmp_path):
166166
assert f.read() == ""
167167

168168

169-
def test_cwd_recursive_placeholder(hq_env: HqEnv):
170-
hq_env.start_server()
171-
hq_env.command(
172-
["submit", "--cwd", "%{CWD}/foo", "--", "bash", "-c", "echo 'hello'"],
173-
expect_fail="Working directory path cannot contain the working "
174-
"directory placeholder `%{CWD}`.",
175-
)
176-
177-
178169
def test_create_output_folders(hq_env: HqEnv):
179170
hq_env.start_server()
180171
hq_env.start_worker()
@@ -233,18 +224,6 @@ def test_job_output_absolute_path(hq_env: HqEnv, tmp_path):
233224
assert f.read() == ""
234225

235226

236-
def test_job_paths_prefilled_placeholders(hq_env: HqEnv):
237-
"""
238-
Checks that job paths are partially resolved after submit.
239-
"""
240-
hq_env.start_server()
241-
hq_env.command(["submit", "hostname"])
242-
wait_for_job_state(hq_env, 1, "WAITING")
243-
244-
table = hq_env.command(["job", "info", "1"], as_table=True)
245-
assert table.get_row_value("Stdout") == default_task_output(task_id="%{TASK_ID}")
246-
247-
248227
def test_job_output_none(hq_env: HqEnv, tmp_path):
249228
hq_env.start_server()
250229
hq_env.start_worker(cpus=1)
@@ -710,64 +689,6 @@ def test_job_tasks_table(hq_env: HqEnv):
710689
assert worker == "" or worker == "worker1"
711690

712691

713-
def test_task_resolve_submit_placeholders(hq_env: HqEnv):
714-
hq_env.start_server()
715-
716-
hq_env.command(["submit", "echo", "test"])
717-
table = hq_env.command(["job", "info", "1", "--tasks"], as_table=True)[
718-
JOB_TABLE_ROWS:
719-
].as_horizontal()
720-
wait_for_job_state(hq_env, 1, "WAITING")
721-
table.check_column_value("Working directory", 0, "")
722-
table.check_column_value("Stdout", 0, "")
723-
table.check_column_value("Stderr", 0, "")
724-
725-
hq_env.start_worker()
726-
727-
wait_for_job_state(hq_env, 1, "FINISHED")
728-
table = hq_env.command(["job", "info", "1", "--tasks"], as_table=True)[
729-
JOB_TABLE_ROWS:
730-
].as_horizontal()
731-
table.check_column_value("Working directory", 0, os.getcwd())
732-
table.check_column_value("Stdout", 0, default_task_output())
733-
table.check_column_value("Stderr", 0, default_task_output(type="stderr"))
734-
735-
736-
def test_task_resolve_worker_placeholders(hq_env: HqEnv):
737-
hq_env.start_server()
738-
739-
hq_env.command(
740-
[
741-
"submit",
742-
"--stdout",
743-
"%{INSTANCE_ID}.out",
744-
"--stderr",
745-
"%{INSTANCE_ID}.err",
746-
"--cwd",
747-
"%{INSTANCE_ID}-dir",
748-
"echo",
749-
"test",
750-
]
751-
)
752-
table = hq_env.command(["job", "info", "1", "--tasks"], as_table=True)[
753-
JOB_TABLE_ROWS:
754-
].as_horizontal()
755-
wait_for_job_state(hq_env, 1, "WAITING")
756-
table.check_column_value("Working directory", 0, "")
757-
table.check_column_value("Stdout", 0, "")
758-
table.check_column_value("Stderr", 0, "")
759-
760-
hq_env.start_worker()
761-
762-
wait_for_job_state(hq_env, 1, "FINISHED")
763-
table = hq_env.command(["job", "info", "1", "--tasks"], as_table=True)[
764-
JOB_TABLE_ROWS:
765-
].as_horizontal()
766-
table.check_column_value("Working directory", 0, abspath("0-dir"))
767-
table.check_column_value("Stdout", 0, abspath("0.out"))
768-
table.check_column_value("Stderr", 0, abspath("0.err"))
769-
770-
771692
def test_job_wait(hq_env: HqEnv):
772693
hq_env.start_server()
773694
hq_env.start_worker()

tests/test_placeholders.py

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
import os
2+
from os.path import abspath
3+
4+
import pytest
5+
6+
from .conftest import HqEnv
7+
from .utils import JOB_TABLE_ROWS, wait_for_job_state
8+
from .utils.job import default_task_output
9+
10+
11+
def test_cwd_recursive_placeholder(hq_env: HqEnv):
12+
hq_env.start_server()
13+
hq_env.command(
14+
["submit", "--cwd", "%{CWD}/foo", "--", "bash", "-c", "echo 'hello'"],
15+
expect_fail="Working directory path cannot contain the working "
16+
"directory placeholder `%{CWD}`.",
17+
)
18+
19+
20+
def test_job_paths_prefilled_placeholders(hq_env: HqEnv):
21+
"""
22+
Checks that job paths are partially resolved after submit.
23+
"""
24+
hq_env.start_server()
25+
hq_env.command(["submit", "hostname"])
26+
wait_for_job_state(hq_env, 1, "WAITING")
27+
28+
table = hq_env.command(["job", "info", "1"], as_table=True)
29+
assert table.get_row_value("Stdout") == default_task_output(task_id="%{TASK_ID}")
30+
31+
32+
def test_task_resolve_submit_placeholders(hq_env: HqEnv):
33+
hq_env.start_server()
34+
35+
hq_env.command(["submit", "echo", "test"])
36+
table = hq_env.command(["job", "info", "1", "--tasks"], as_table=True)[
37+
JOB_TABLE_ROWS:
38+
].as_horizontal()
39+
wait_for_job_state(hq_env, 1, "WAITING")
40+
table.check_column_value("Working directory", 0, "")
41+
table.check_column_value("Stdout", 0, "")
42+
table.check_column_value("Stderr", 0, "")
43+
44+
hq_env.start_worker()
45+
46+
wait_for_job_state(hq_env, 1, "FINISHED")
47+
table = hq_env.command(["job", "info", "1", "--tasks"], as_table=True)[
48+
JOB_TABLE_ROWS:
49+
].as_horizontal()
50+
table.check_column_value("Working directory", 0, os.getcwd())
51+
table.check_column_value("Stdout", 0, default_task_output())
52+
table.check_column_value("Stderr", 0, default_task_output(type="stderr"))
53+
54+
55+
def test_task_resolve_worker_placeholders(hq_env: HqEnv):
56+
hq_env.start_server()
57+
58+
hq_env.command(
59+
[
60+
"submit",
61+
"--stdout",
62+
"%{INSTANCE_ID}.out",
63+
"--stderr",
64+
"%{INSTANCE_ID}.err",
65+
"--cwd",
66+
"%{INSTANCE_ID}-dir",
67+
"echo",
68+
"test",
69+
]
70+
)
71+
table = hq_env.command(["job", "info", "1", "--tasks"], as_table=True)[
72+
JOB_TABLE_ROWS:
73+
].as_horizontal()
74+
wait_for_job_state(hq_env, 1, "WAITING")
75+
table.check_column_value("Working directory", 0, "")
76+
table.check_column_value("Stdout", 0, "")
77+
table.check_column_value("Stderr", 0, "")
78+
79+
hq_env.start_worker()
80+
81+
wait_for_job_state(hq_env, 1, "FINISHED")
82+
table = hq_env.command(["job", "info", "1", "--tasks"], as_table=True)[
83+
JOB_TABLE_ROWS:
84+
].as_horizontal()
85+
table.check_column_value("Working directory", 0, abspath("0-dir"))
86+
table.check_column_value("Stdout", 0, abspath("0.out"))
87+
table.check_column_value("Stderr", 0, abspath("0.err"))
88+
89+
90+
def test_stream_submit_placeholder(hq_env: HqEnv):
91+
hq_env.start_server()
92+
hq_env.command(
93+
["submit", "--log", "log-%{JOB_ID}", "--", "bash", "-c", "echo Hello"]
94+
)
95+
hq_env.start_workers(1)
96+
wait_for_job_state(hq_env, 1, "FINISHED")
97+
98+
lines = set(hq_env.command(["log", "log-1", "show"], as_lines=True))
99+
assert "0:0> Hello" in lines
100+
assert "0: > stream closed" in lines
101+
102+
103+
@pytest.mark.parametrize("channel", ("stdout", "stderr"))
104+
def test_warning_missing_placeholder_in_output(hq_env: HqEnv, channel: str):
105+
hq_env.start_server()
106+
output = hq_env.command(
107+
["submit", "--array=1-4", f"--{channel}=foo", "/bin/hostname"]
108+
)
109+
assert (
110+
f"You have submitted an array job, but the `{channel}` "
111+
+ "path does not contain the task ID placeholder."
112+
in output
113+
)
114+
assert f"Consider adding `%{{TASK_ID}}` to the `--{channel}` value."
115+
116+
117+
def test_unknown_placeholder(hq_env: HqEnv):
118+
hq_env.start_server()
119+
output = hq_env.command(
120+
[
121+
"submit",
122+
"--log",
123+
"log-%{FOO}",
124+
"--stdout",
125+
"dir/%{BAR}/%{BAZ}",
126+
"--stderr",
127+
"dir/%{TAS_ID}",
128+
"--cwd",
129+
"%{BAR}",
130+
"--",
131+
"hostname",
132+
]
133+
)
134+
assert "Found unknown placeholder `FOO` in log path" in output
135+
assert "Found unknown placeholders `BAR, BAZ` in stdout path" in output
136+
assert "Found unknown placeholder `TAS_ID` in stderr path" in output
137+
assert "Found unknown placeholder `BAR` in working directory path" in output

0 commit comments

Comments
 (0)