use crate::support::{sandbox_with_plugin, stderr_of, stdout_of, Sandbox};
#[test]
fn with_no_plugins_it_exits_three_and_explains_itself() {
let sb = Sandbox::new();
sb.file("Cargo.toml");
let out = sb.run(&[]);
assert_eq!(
out.status.code(),
Some(3),
"no project type detected is exit code 3"
);
let err = stderr_of(&out);
assert!(err.contains("no project type detected"), "{err}");
assert!(err.contains("stopped because"), "{err}");
assert!(err.contains("No plugins are installed"), "{err}");
assert!(
!err.contains("pmpx plugin add cargo"),
"it must not recommend a specific plugin: {err}"
);
assert!(err.contains(".pmpx.toml"), "{err}");
}
#[test]
fn exec_still_works_with_zero_plugins() {
let sb = Sandbox::new();
sb.file("Cargo.toml");
let out = sb.run(&["exec", "cargo", "--version"]);
assert!(
out.status.success(),
"it should succeed after degrading to passing through verbatim: {}",
stderr_of(&out)
);
assert!(stdout_of(&out).contains("cargo"), "{}", stdout_of(&out));
}
#[test]
fn detects_the_project_and_runs_the_backend_command() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
let out = sb.ok(&["build"]);
assert!(
out.contains("pmpx-probe"),
"the backend command should really run: {out}"
);
}
#[test]
fn the_plugin_receives_root_matched_verb_and_args_across_the_boundary() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
sb.file("fakepm.json");
let out = sb.ok(&["build"]);
let line = out
.lines()
.find(|l| l.contains("pmpx-probe"))
.unwrap_or_else(|| panic!("no echo came back: {out}"));
let got_root = line
.split_once("root=")
.and_then(|(_, rest)| rest.split_once(" matched="))
.map(|(root, _)| root)
.unwrap_or_else(|| panic!("the echo has no root=: {line}"));
let got = std::fs::canonicalize(got_root).unwrap_or_else(|e| {
panic!("the root the plugin received cannot be resolved ({got_root}): {e}")
});
let want = std::fs::canonicalize(&sb.project).expect("the sandbox directory should exist");
assert_eq!(got, want, "project_root was passed wrongly: {line}");
assert!(
line.contains("matched=fakepm.lock|fakepm.json"),
"matched is wrong (strong evidence should come first): {line}"
);
assert!(line.contains("verb=build"), "{line}");
assert!(line.contains("args="), "{line}");
let got_start = line
.split_once("start=")
.and_then(|(_, rest)| rest.split_once(" reason="))
.map(|(start, _)| start)
.unwrap_or_else(|| panic!("the echo has no start=: {line}"));
let got = std::fs::canonicalize(got_start)
.unwrap_or_else(|e| panic!("start_dir cannot be resolved ({got_start}): {e}"));
assert_eq!(got, want, "start_dir was passed wrongly: {line}");
assert!(line.contains("reason=scored"), "{line}");
assert!(line.contains("score=110"), "{line}");
}
#[test]
fn a_file_declared_in_the_manifest_reaches_the_plugin() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
std::fs::write(sb.project.join("fakepm.json"), "pinned: true").unwrap();
let out = sb.ok(&["build"]);
let line = out.lines().find(|l| l.contains("pmpx-probe")).unwrap();
assert!(
line.contains("declared=pinned: true"),
"the declared file's contents should reach the plugin: {line}"
);
}
#[test]
fn a_declared_file_that_is_missing_is_absent() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
let out = sb.ok(&["build"]);
let line = out.lines().find(|l| l.contains("pmpx-probe")).unwrap();
assert!(line.contains("declared=none"), "{line}");
}
#[test]
fn a_manifest_that_lies_about_its_abi_is_reported_but_still_used() {
let lib = crate::support::build_fake_plugin();
let sb = Sandbox::new();
sb.install_plugin(
"pmpx-plugin-fakepm",
&crate::support::FAKEPM_MANIFEST.replace("abi = 3", "abi = 2"),
Some(&lib),
);
sb.file("fakepm.lock");
let out = sb.run(&["build"]);
assert!(out.status.success(), "{}", stderr_of(&out));
assert!(
stdout_of(&out).contains("pmpx-probe"),
"the plugin still has to run: {}",
stdout_of(&out)
);
let err = stderr_of(&out);
assert!(
err.contains("declares ABI 2") && err.contains("reports 3"),
"the disagreement has to be said out loud: {err}"
);
}
#[test]
fn a_pinned_plugin_is_told_it_was_pinned() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
sb.file(".pmpx.toml");
std::fs::write(
sb.project.join(".pmpx.toml"),
"[plugin]\nfaketest = \"fakepm\"\n\n[scripts]\nbuild = \"fakepm run build\"\n",
)
.unwrap();
let out = sb.run(&["--debug", "build"]);
assert!(out.status.success(), "{}", stderr_of(&out));
let stdout = stdout_of(&out);
let line = stdout.lines().find(|l| l.contains("pmpx-probe")).unwrap();
assert!(line.contains("reason=pinned"), "{line}");
assert!(line.contains("score=100"), "{line}");
let err = stderr_of(&out);
let context_line = err
.lines()
.find(|l| l.contains("[fakepm] context:"))
.unwrap_or_else(|| panic!("the plugin logged no context: {err}"));
assert!(context_line.contains("reason=pinned"), "{context_line}");
assert!(context_line.contains("faketest=fakepm"), "{context_line}");
assert!(!context_line.contains("scripts="), "{context_line}");
assert!(context_line.contains(".pmpx.toml"), "{context_line}");
}
#[test]
fn the_passthrough_separator_reaches_the_plugin() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
let out = sb.run(&["run", "some-target", "--", "--release"]);
assert!(out.status.success(), "{}", stderr_of(&out));
let stdout = stdout_of(&out);
assert!(
stdout.contains("args=some-target,--,--release"),
"the separator has to survive into the argument list: {stdout}"
);
}
#[test]
fn a_plugin_named_with_dash_p_is_told_it_was_explicit() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
let out = sb.ok(&["-p", "fakepm", "build"]);
let line = out.lines().find(|l| l.contains("pmpx-probe")).unwrap();
assert!(line.contains("reason=explicit"), "{line}");
assert!(line.contains("score=0"), "{line}");
}
#[test]
fn args_after_the_double_dash_reach_the_plugin() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
let out = sb.ok(&["test", "--", "--nocapture", "some-filter"]);
let line = out.lines().find(|l| l.contains("pmpx-probe")).unwrap();
assert!(line.contains("verb=test"), "{line}");
assert!(
line.contains("args=--nocapture,some-filter"),
"the arguments after `--` must go through verbatim: {line}"
);
}
#[test]
fn a_bare_pmpx_reports_the_selection() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
let out = sb.ok(&[]);
assert!(out.contains("Project root"), "{out}");
assert!(
out.contains("fakepm"),
"it should report the selected plugin: {out}"
);
}
#[test]
fn info_lists_every_candidate_and_score() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
let out = sb.ok(&["info"]);
assert!(out.contains("Candidates and scores"), "{out}");
assert!(out.contains("fakepm"), "{out}");
assert!(
out.contains("100"),
"strong evidence should be 100 points: {out}"
);
assert!(out.contains("compiled with"), "{out}");
assert!(out.contains("target"), "{out}");
}
#[test]
fn debug_traces_every_phase_on_stderr() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
let out = sb.run(&["--debug", "build"]);
assert!(out.status.success(), "{}", stderr_of(&out));
let err = stderr_of(&out);
for phase in [
"pmpx debug:",
"cli.parse",
"session.root",
"detect.score",
"detect.decide",
"backend.load",
"spawn.resolve",
"backend.run",
"total",
] {
assert!(err.contains(phase), "the trace is missing {phase}:\n{err}");
}
let stdout = stdout_of(&out);
assert!(!stdout.contains("pmpx debug:"), "{stdout}");
}
#[test]
fn quiet_does_not_turn_the_trace_off() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
let out = sb.run(&["--quiet", "--debug", "build"]);
assert!(out.status.success(), "{}", stderr_of(&out));
let err = stderr_of(&out);
assert!(err.contains("pmpx debug:"), "{err}");
assert!(
!err.contains("pmpx ->"),
"`--quiet` should still have suppressed the resolved command: {err}"
);
}
#[test]
fn a_plugins_own_log_line_carries_its_id() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
let traced = sb.run(&["--debug", "build"]);
assert!(traced.status.success(), "{}", stderr_of(&traced));
let err = stderr_of(&traced);
assert!(
err.contains("[fakepm] mapping build"),
"the plugin's line should carry its id: {err}"
);
let plain = sb.run(&["build"]);
assert!(plain.status.success(), "{}", stderr_of(&plain));
let err = stderr_of(&plain);
assert!(
!err.contains("mapping build"),
"a plugin's notes need --debug: {err}"
);
}
#[test]
fn exec_falls_back_when_the_plugin_says_unsupported() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
let out = sb.run(&["exec", "cargo", "--version"]);
assert!(
out.status.success(),
"it should degrade to passing through verbatim when the plugin says unsupported: {}",
stderr_of(&out)
);
assert!(
stdout_of(&out).contains("cargo"),
"it should really have run cargo --version: {}",
stdout_of(&out)
);
}
#[test]
fn other_verbs_do_not_fall_back() {
let (sb, _lib) = sandbox_with_plugin();
sb.file("fakepm.lock");
let out = sb.run(&["remove", "something"]);
assert_eq!(
out.status.code(),
Some(1),
"a plugin blowing up is exit code 1\nstderr: {}",
stderr_of(&out)
);
assert!(
stderr_of(&out).contains("fakepm"),
"it should say which plugin had the problem: {}",
stderr_of(&out)
);
}