Skip to content

Commit ffaf0fb

Browse files
authored
PHD: use clap for more cargo xtask phd args (#645)
Currently, `cargo xtask phd` does a bunch of "bash-style" arg parsing, looping over any unrecognized arguments. This was intended as a way of passing through arguments to `phd-runner`. However, because `clap` will stick any unrecognized arguments in `trailing_var_args`, we can just use normal `clap` arg parsing to handle the arguments that we default before passing through to `phd-runner`. Now, the `cargo xtask phd` command has nicer help text for its overridden arguments, and it can use `clap`'s built-in validation of conflicting arguments. This seems nicer.
1 parent 206cee8 commit ffaf0fb

File tree

2 files changed

+222
-94
lines changed

2 files changed

+222
-94
lines changed

xtask/src/main.rs

+1
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ struct Args {
2020
}
2121

2222
#[derive(Subcommand)]
23+
#[allow(clippy::large_enum_variant)]
2324
enum Cmds {
2425
/// Run suite of clippy checks
2526
Clippy {

xtask/src/task_phd.rs

+221-94
Original file line numberDiff line numberDiff line change
@@ -41,6 +41,7 @@ macro_rules! cargo_warn {
4141
}
4242

4343
#[derive(clap::Subcommand, Debug, Clone)]
44+
#[allow(clippy::large_enum_variant)]
4445
pub(crate) enum Cmd {
4546
/// Run the PHD test suite.
4647
Run {
@@ -68,22 +69,11 @@ pub(crate) enum Cmd {
6869

6970
#[derive(clap::Parser, Debug, Clone)]
7071
pub(crate) struct RunArgs {
71-
/// Build `propolis-server` in release mode.
72-
#[clap(long = "release", short = 'r')]
73-
propolis_release_mode: bool,
74-
7572
/// If set, temporary directories older than one day will not be
7673
/// deleted.
7774
#[clap(long)]
7875
no_tidy: bool,
7976

80-
/// If set, skip migration-from-base tests.
81-
///
82-
/// If this flag is present, `cargo xtask phd` will not pass
83-
/// `--base-propolis-branch master` to the `phd-runner` command.
84-
#[clap(long)]
85-
no_base_propolis: bool,
86-
8777
/// Arguments to pass to `phd-runner run`.
8878
///
8979
/// If the `--propolis-server-cmd`, `--crucible-downstairs-commit`,
@@ -95,6 +85,163 @@ pub(crate) struct RunArgs {
9585
/// `phd-runner`.
9686
#[clap(trailing_var_arg = true, allow_hyphen_values = true)]
9787
phd_args: Vec<String>,
88+
89+
#[clap(flatten)]
90+
propolis_args: PropolisArgs,
91+
92+
#[clap(flatten)]
93+
artifact_args: ArtifactStoreArgs,
94+
95+
#[clap(flatten)]
96+
base_propolis_args: BasePropolisArgs,
97+
98+
#[clap(flatten)]
99+
crucible_args: CrucibleArgs,
100+
}
101+
102+
#[derive(Debug, Clone, clap::Parser)]
103+
#[group(id = "propolis", required = false, multiple = false)]
104+
#[command(next_help_heading = "Propolis Selection")]
105+
struct PropolisArgs {
106+
/// The command to use to launch the Propolis server.
107+
///
108+
/// If this is not present, a Propolis server binary will be built automatically.
109+
#[clap(long = "propolis-server-cmd", value_parser, value_hint = clap::ValueHint::FilePath)]
110+
server_cmd: Option<Utf8PathBuf>,
111+
112+
/// If set, build `propolis-server` in release mode.
113+
#[clap(long, short = 'r')]
114+
release: bool,
115+
}
116+
117+
#[derive(Debug, Clone, clap::Parser)]
118+
#[group(id = "base-propolis", required = false, multiple = false)]
119+
#[command(next_help_heading = "Migration Base Propolis Selection")]
120+
struct BasePropolisArgs {
121+
/// Git branch name to use for the "migration base" Propolis server artifact
122+
/// for migration-from-base tests.
123+
///
124+
/// If this argument is provided, PHD will download the latest Propolis
125+
/// server artifact from Buildomat for the provided branch name, and use it
126+
/// to test migration from that Propolis version to the Propolis revision
127+
/// under test.
128+
///
129+
/// This argument conflicts with the `--base-propolis-commit`,
130+
/// `--base-propolis-cmd`, and `--no-base-propolis` arguments. If none of
131+
/// these arguments are provided, `cargo xtask phd` will automatically pass
132+
/// `--base-propolis-branch master` to `phd-runner`.
133+
#[clap(long, value_parser)]
134+
base_propolis_branch: Option<String>,
135+
136+
/// Git commit hash to use for the "migration base" Propolis server artifact for
137+
/// migration from base tests.
138+
///
139+
/// If this argument is provided, PHD will download the Propolis server
140+
/// artifact from Buildomat for the provided commit hash, and use it
141+
/// to test migration from that Propolis version to the Propolis revision
142+
/// under test.
143+
///
144+
/// This argument conflicts with the `--base-propolis-branch`,
145+
/// `--base-propolis-cmd`, and `--no-base-propolis` arguments. If none of
146+
/// these arguments are provided, `cargo xtask phd` will automatically pass
147+
/// `--base-propolis-branch master` to `phd-runner`.
148+
#[clap(long, value_parser)]
149+
base_propolis_commit: Option<String>,
150+
151+
/// The path of a local command to use as the "migration base" Propolis
152+
/// server for migration-from-base tests.
153+
///
154+
/// If this argument is provided, PHD will use the provided command to run
155+
/// to test migration from that Propolis binary to the Propolis revision
156+
/// under test.
157+
///
158+
/// This argument conflicts with the `--base-propolis-branch`,
159+
/// `--base-propolis-commit`, and `--no-base-propolis` arguments. If none of
160+
/// these arguments are provided, `cargo xtask phd` will automatically pass
161+
/// `--base-propolis-branch master` to `phd-runner`.
162+
#[clap(
163+
long,
164+
value_hint = clap::ValueHint::FilePath,
165+
value_parser
166+
)]
167+
base_propolis_cmd: Option<Utf8PathBuf>,
168+
169+
/// If set, skip migration-from-base tests.
170+
///
171+
/// If this flag is present, `cargo xtask phd` will not pass
172+
/// `--base-propolis-branch master` to the `phd-runner` command.
173+
///
174+
/// This flag conflicts with the `--base-propolis-branch`,
175+
/// `--base-propolis-commit`, and `--base-propolis-cmd` arguments. If none
176+
/// of these arguments are provided, `cargo xtask phd` will automatically
177+
/// pass `--base-propolis-branch master` to `phd-runner`.
178+
#[clap(long)]
179+
no_base_propolis: bool,
180+
}
181+
182+
#[derive(Debug, Clone, clap::Parser)]
183+
#[group(id = "crucible", required = false, multiple = false)]
184+
#[command(next_help_heading = "Crucible Downstairs Selection")]
185+
struct CrucibleArgs {
186+
/// The path of a local command to use to launch Crucible downstairs
187+
/// servers.
188+
///
189+
/// This argument conflicts with the `--crucible-downstairs-commit` and
190+
/// `--no-crucible` arguments. If none of the `--crucible-downstairs-cmd`,
191+
/// `--crucible-downstairs-commit`, and `--no-crucible` arguments are
192+
/// provided, then `cargo xtask phd` will pass `--crucible-downstairs-commit
193+
/// auto` to `phd-runner`.
194+
#[clap(long, value_parser, value_hint = clap::ValueHint::FilePath)]
195+
crucible_downstairs_cmd: Option<Utf8PathBuf>,
196+
197+
/// Git revision to use to download Crucible downstairs artifacts from
198+
/// Buildomat.
199+
///
200+
/// This may either be the string 'auto' or a 40-character Git commit
201+
/// hash. If this is 'auto', then the Git revision of Crucible is determined
202+
/// automatically based on the Propolis workspace's Cargo git dependency on
203+
/// the `crucible` crate (determined when `phd-runner` is built). If an
204+
/// explicit commit hash is provided, that commit is downloaded from
205+
/// Buildomat, regardless of which version of the `crucible` crate Propolis
206+
/// depends on.
207+
///
208+
/// This argument conflicts with the `--crucible-downstairs-cmd` and
209+
/// `--no-crucible` arguments. If none of the `--crucible-downstairs-cmd`,
210+
/// `--crucible-downstairs-commit`, and `--no-crucible` arguments are
211+
/// provided, then `cargo xtask phd` will pass `--crucible-downstairs-commit
212+
/// auto` to `phd-runner`.
213+
#[clap(long, value_parser)]
214+
crucible_downstairs_commit: Option<String>,
215+
216+
/// If set, skip Crucible tests.
217+
///
218+
/// If this flag is present, `cargo xtask phd` will not pass
219+
/// `--crucible-downstairs-commit auto` to the `phd-runner` command.
220+
///
221+
/// This flag conflicts with the `--crucible-downstairs-cmd` and
222+
/// `--crucible-downstairs-commit` arguments. If none of the
223+
/// `--crucible-downstairs-cmd`, `--crucible-downstairs-commit`, and
224+
/// `--no-crucible` arguments are provided, then `cargo xtask phd` will pass
225+
/// `--crucible-downstairs-commit auto` to `phd-runner`.
226+
#[clap(long)]
227+
no_crucible: bool,
228+
}
229+
230+
#[derive(Debug, Clone, clap::Parser)]
231+
#[command(next_help_heading = "Artifact Store Options")]
232+
struct ArtifactStoreArgs {
233+
/// The path to a TOML file describing the artifact store to use for this
234+
/// run.
235+
#[clap(long, value_parser, value_hint = clap::ValueHint::FilePath)]
236+
artifact_toml_path: Option<Utf8PathBuf>,
237+
238+
/// The directory in which artifacts (guest OS images, bootroms, etc.)
239+
/// are to be stored.
240+
///
241+
/// If this argument is not provided, the default artifact store directory
242+
/// will be created in `target/phd/artifacts`.
243+
#[clap(long, value_parser)]
244+
artifact_directory: Option<Utf8PathBuf>,
98245
}
99246

100247
impl Cmd {
@@ -109,10 +256,12 @@ impl Cmd {
109256
let now = time::SystemTime::now();
110257

111258
let RunArgs {
112-
propolis_release_mode,
113259
no_tidy,
260+
propolis_args,
114261
phd_args,
115-
no_base_propolis,
262+
artifact_args,
263+
base_propolis_args,
264+
crucible_args,
116265
} = match self {
117266
Self::Run { args } => args,
118267
Self::Tidy => {
@@ -137,66 +286,13 @@ impl Cmd {
137286
}
138287
};
139288

140-
// Bash-script-style arg parsing, rather than using `clap`, because we
141-
// want to filter out the args we default regardless of their position in
142-
// the input. A `clap` parser can only accept unrecognized args if they're
143-
// trailing after all recognized args, which isn't the behavior we want, as
144-
// we don't know what order the `phd-runner` command line will come in.
145-
let mut arg_iter = phd_args.iter().map(String::as_str);
146-
let mut bonus_args = Vec::new();
147-
let mut propolis_base_branch = None;
148-
// If set, the `base-propolis-cmd` or `base-propolis-commit` CLI args
149-
// were passed, so we should skip adding `--base-propolis-branch`.
150-
let mut overridden_base_propolis = no_base_propolis;
151-
let mut propolis_local_path = None;
152-
let mut crucible_commit = None;
153-
let mut artifacts_toml = None;
154-
let mut artifact_dir = None;
155-
while let Some(arg) = arg_iter.next() {
156-
macro_rules! take_next_arg {
157-
($var:ident) => {{
158-
let val = arg_iter.next().ok_or_else(|| {
159-
anyhow::anyhow!("Missing value for argument `{}`", arg)
160-
})?;
161-
cargo_log!("Overridden", "{} {val:?}", arg);
162-
$var = Some(val);
163-
}};
164-
}
165-
match arg {
166-
args::PROPOLIS_BASE => take_next_arg!(propolis_base_branch),
167-
args::PROPOLIS_CMD => take_next_arg!(propolis_local_path),
168-
args::CRUCIBLE_COMMIT => take_next_arg!(crucible_commit),
169-
args::ARTIFACTS_TOML => take_next_arg!(artifacts_toml),
170-
args::ARTIFACTS_DIR => take_next_arg!(artifact_dir),
171-
args::PROPOLIS_BASE_COMMIT | args::PROPOLIS_BASE_CMD => {
172-
overridden_base_propolis = true;
173-
bonus_args.push(arg);
174-
175-
let val = arg_iter.next().ok_or_else(|| {
176-
anyhow::anyhow!("Missing value for argument `{arg}`")
177-
})?;
178-
bonus_args.push(val);
179-
cargo_log!("Overridden", "{} {val:?}", arg);
180-
}
181-
182-
_ => bonus_args.push(arg),
183-
}
184-
}
185-
186-
let propolis_local_path = match propolis_local_path {
187-
Some(path) => {
188-
if propolis_release_mode {
189-
cargo_warn!(
190-
"setting `--release` to build propolis-server in release mode \
191-
does nothing if an existing propolis binary path is \
192-
provided using `{}`",
193-
args::PROPOLIS_CMD,
194-
);
195-
}
196-
path.into()
289+
let propolis_local_path = match propolis_args.server_cmd {
290+
Some(cmd) => {
291+
cargo_log!("Using", "local Propolis server command {cmd}");
292+
cmd
197293
}
198294
None => {
199-
let bin = build_bin("propolis-server", propolis_release_mode)?;
295+
let bin = build_bin("propolis-server", propolis_args.release)?;
200296
let path = bin
201297
.path()
202298
.try_into()
@@ -205,8 +301,10 @@ impl Cmd {
205301
}
206302
};
207303

208-
let artifact_dir =
209-
artifact_dir.map(Utf8PathBuf::from).unwrap_or_else(|| {
304+
let artifact_dir = artifact_args
305+
.artifact_directory
306+
.map(Utf8PathBuf::from)
307+
.unwrap_or_else(|| {
210308
// if there's no explicitly overridden `artifact_dir` path, use
211309
// `target/phd/artifacts`.
212310
phd_dir.join("artifacts")
@@ -234,8 +332,10 @@ impl Cmd {
234332

235333
mkdir(&tmp_dir, "temp directory")?;
236334

237-
let artifacts_toml =
238-
artifacts_toml.map(Utf8PathBuf::from).unwrap_or_else(|| {
335+
let artifacts_toml = artifact_args
336+
.artifact_toml_path
337+
.map(Utf8PathBuf::from)
338+
.unwrap_or_else(|| {
239339
// if there's no explicitly overridden `artifacts.toml` path,
240340
// determine the default one from the workspace path.
241341
relativize(&meta.workspace_root)
@@ -252,36 +352,63 @@ impl Cmd {
252352
let phd_runner = build_bin("phd-runner", false)?;
253353
let mut cmd = phd_runner.command();
254354
cmd.arg("run")
255-
.arg(args::PROPOLIS_CMD)
355+
.arg("--propolis-server-cmd")
256356
.arg(&propolis_local_path)
257-
.arg(args::CRUCIBLE_COMMIT)
258-
.arg(crucible_commit.unwrap_or("auto"))
259-
.arg(args::ARTIFACTS_TOML)
357+
.arg("--artifact-toml-path")
260358
.arg(&artifacts_toml)
261-
.arg(args::ARTIFACTS_DIR)
359+
.arg("--artifact-directory")
262360
.arg(&artifact_dir)
263361
.arg("--tmp-directory")
264-
.arg(&tmp_dir)
265-
.args(bonus_args);
362+
.arg(&tmp_dir);
363+
crucible_args.configure_command(&mut cmd);
364+
base_propolis_args.configure_command(&mut cmd);
365+
cmd.args(phd_args);
266366

267-
if !overridden_base_propolis {
268-
cmd.arg(args::PROPOLIS_BASE)
269-
.arg(propolis_base_branch.unwrap_or("master"));
270-
}
271367
let status = run_exit_code(&mut cmd)?;
272368

273369
std::process::exit(status);
274370
}
275371
}
276372

277-
mod args {
278-
pub(super) const PROPOLIS_CMD: &str = "--propolis-server-cmd";
279-
pub(super) const PROPOLIS_BASE: &str = "--base-propolis-branch";
280-
pub(super) const PROPOLIS_BASE_COMMIT: &str = "--base-propolis-commit";
281-
pub(super) const PROPOLIS_BASE_CMD: &str = "--base-propolis-cmd";
282-
pub(super) const CRUCIBLE_COMMIT: &str = "--crucible-downstairs-commit";
283-
pub(super) const ARTIFACTS_TOML: &str = "--artifact-toml-path";
284-
pub(super) const ARTIFACTS_DIR: &str = "--artifact-directory";
373+
impl CrucibleArgs {
374+
fn configure_command(&self, cmd: &mut Command) {
375+
if let Some(ref path) = self.crucible_downstairs_cmd {
376+
cargo_log!("Using", "local Crucible downstairs: {path}");
377+
cmd.arg("--crucible-downstairs-cmd").arg(path);
378+
} else if let Some(ref commit) = self.crucible_downstairs_commit {
379+
cargo_log!("Using", "Crucible downstairs from commit: {commit}");
380+
cmd.arg("--crucible-downstairs-commit").arg(commit);
381+
} else if self.no_crucible {
382+
cargo_log!("Skipping", "Crucible tests");
383+
} else {
384+
cmd.arg("--crucible-downstairs-commit").arg("auto");
385+
}
386+
}
387+
}
388+
389+
impl BasePropolisArgs {
390+
fn configure_command(&self, cmd: &mut Command) {
391+
if let Some(ref path) = self.base_propolis_cmd {
392+
cargo_log!("Using", "local migration-base Propolis: {path}");
393+
cmd.arg("--base-propolis-cmd").arg(path);
394+
} else if let Some(ref commit) = self.base_propolis_commit {
395+
cargo_log!(
396+
"Using",
397+
"migration-base Propolis from commit: {commit}"
398+
);
399+
cmd.arg("--base-propolis-commit").arg(commit);
400+
} else if let Some(ref branch) = self.base_propolis_branch {
401+
cargo_log!(
402+
"Using",
403+
"migration-base Propolis from branch: {branch}"
404+
);
405+
cmd.arg("--base-propolis-branch").arg(branch);
406+
} else if self.no_base_propolis {
407+
cargo_log!("Skipping", "migration-from-base tests");
408+
} else {
409+
cmd.arg("--base-propolis-branch").arg("master");
410+
}
411+
}
285412
}
286413

287414
fn build_bin(

0 commit comments

Comments
 (0)