use std::fs;
use std::os::unix::fs::PermissionsExt;
use std::path::{Path, PathBuf};
use std::process::{Command, Output};
struct Fixture {
root: tempfile::TempDir,
}
impl Fixture {
fn new() -> Self {
let root = tempfile::tempdir().expect("temp dir");
fs::create_dir_all(root.path().join("bin")).expect("mkdir bin");
fs::create_dir_all(root.path().join("log")).expect("mkdir log");
fs::create_dir_all(root.path().join("project")).expect("mkdir project");
Self { root }
}
fn project(&self) -> PathBuf {
self.root.path().join("project")
}
fn file(&self, path: &str, contents: &str) -> &Self {
let full = self.project().join(path);
fs::create_dir_all(full.parent().expect("parent")).expect("mkdir");
fs::write(full, contents).expect("write");
self
}
fn tool(&self, path: &str, body: &str) -> &Self {
let name = Path::new(path).file_name().expect("name").to_string_lossy();
let full = self.project().join(path);
fs::create_dir_all(full.parent().expect("parent")).expect("mkdir");
write_script(&full, &name, body);
self
}
fn shim(&self, name: &str, body: &str) -> &Self {
write_script(&self.root.path().join("bin").join(name), name, body);
self
}
fn calls(&self, name: &str) -> Option<Vec<String>> {
let text = fs::read_to_string(self.root.path().join("log").join(name)).ok()?;
Some(text.lines().map(str::to_owned).collect())
}
fn opi(&self, dir: &str, args: &[&str]) -> Output {
let dir = self.project().join(dir);
fs::create_dir_all(&dir).expect("mkdir cwd");
Command::new(env!("CARGO_BIN_EXE_opi"))
.args(args)
.current_dir(dir)
.env_clear()
.env("PATH", self.root.path().join("bin"))
.env("HOME", self.root.path())
.env("OPI_TEST_LOG", self.root.path().join("log"))
.output()
.expect("opi runs")
}
}
fn write_script(path: &Path, name: &str, body: &str) {
let script = format!(
"#!/bin/sh\nprintf '%s\\n' \"$(pwd -P)\" \"$@\" > \"$OPI_TEST_LOG/{name}\"\n{body}\n"
);
fs::write(path, script).expect("write script");
fs::set_permissions(path, fs::Permissions::from_mode(0o755)).expect("chmod");
}
fn stdout(output: &Output) -> String {
String::from_utf8_lossy(&output.stdout).into_owned()
}
fn stderr(output: &Output) -> String {
String::from_utf8_lossy(&output.stderr).into_owned()
}
fn canonical(path: PathBuf) -> String {
path.canonicalize()
.expect("canonicalize")
.to_string_lossy()
.into_owned()
}
#[test]
fn a_script_runs_through_the_detected_manager_with_its_arguments() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"scripts":{"build":"x"}}"#)
.file("package-lock.json", "{}")
.shim("npm", "exit 0");
let output = fixture.opi(".", &["build", "--verbose"]);
assert!(output.status.success(), "{}", stderr(&output));
let calls = fixture.calls("npm").expect("npm ran");
assert_eq!(
calls[1..],
["run", "build", "--", "--verbose"],
"npm needs `--` before the script's own flags"
);
}
#[test]
fn a_member_script_is_addressed_the_way_the_manager_expects() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"name":"root","workspaces":["apps/*"]}"#)
.file("pnpm-lock.yaml", "")
.file(
"apps/blog/package.json",
r#"{"name":"blog","scripts":{"dev":"x"}}"#,
)
.shim("pnpm", "exit 0");
let output = fixture.opi(".", &["blog/dev"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
fixture.calls("pnpm").expect("pnpm ran")[1..],
["--filter", "blog", "run", "dev"]
);
}
#[test]
fn the_scripts_exit_code_and_output_are_its_own() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"scripts":{"test":"x"}}"#)
.file("pnpm-lock.yaml", "")
.shim("pnpm", "echo 'from the script'\nexit 7");
let output = fixture.opi(".", &["test"]);
assert_eq!(output.status.code(), Some(7));
assert_eq!(stdout(&output), "from the script\n");
}
#[test]
fn a_script_starts_where_opi_was_started() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"scripts":{"dev":"x"}}"#)
.file("package-lock.json", "{}")
.shim("npm", "exit 0");
let output = fixture.opi("src/pages", &["dev"]);
assert!(output.status.success(), "{}", stderr(&output));
assert_eq!(
fixture.calls("npm").expect("npm ran")[0],
canonical(fixture.project().join("src/pages"))
);
}
#[test]
fn a_missing_package_manager_is_named_rather_than_crashed_on() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"scripts":{"build":"x"}}"#)
.file("pnpm-lock.yaml", "");
let output = fixture.opi(".", &["build"]);
assert_eq!(output.status.code(), Some(1));
let said = stderr(&output);
assert!(said.contains("Cannot run pnpm"), "{said}");
assert!(said.contains("installed and on your PATH"), "{said}");
}
#[test]
fn a_cargo_task_covers_the_whole_workspace_from_inside_a_member() {
let fixture = Fixture::new();
fixture
.file(
"Cargo.toml",
"[workspace]\nmembers = [\"crates/*\"]\n\n[package]\nname = \"repo\"\n",
)
.file("crates/app/Cargo.toml", "[package]\nname = \"app\"\n")
.shim("cargo", "exit 0");
let output = fixture.opi("crates/app", &["cargo test"]);
assert!(output.status.success(), "{}", stderr(&output));
let calls = fixture.calls("cargo").expect("cargo ran");
assert_eq!(calls[1..], ["test", "--all-features", "--workspace"]);
}
#[test]
fn a_piped_list_is_plain_text() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"scripts":{"dev":"x","build":"y"}}"#)
.file("package-lock.json", "{}");
let output = fixture.opi(".", &[]);
assert!(output.status.success(), "{}", stderr(&output));
let printed = stdout(&output);
assert!(
printed.contains("dev") && printed.contains("build"),
"{printed}"
);
assert!(!printed.contains('\u{1b}'), "no escape codes into a pipe");
}
#[test]
fn a_members_tool_runs_in_the_member_even_without_scripts() {
let fixture = Fixture::new();
fixture
.file(
"package.json",
r#"{"name":"root","workspaces":["packages/*"]}"#,
)
.file("package-lock.json", "{}")
.file(
"packages/lib/package.json",
r#"{"name":"lib","devDependencies":{"eslint":"9.0.0"}}"#,
)
.tool("node_modules/.bin/eslint", "echo '1 problem'\nexit 1");
let output = fixture.opi(".", &["--health"]);
assert_eq!(output.status.code(), Some(1));
let printed = stdout(&output);
assert!(printed.contains("Lint (lib)"), "{printed}");
assert!(printed.contains("1 problem"), "{printed}");
assert_eq!(
fixture.calls("eslint").expect("eslint ran")[0],
canonical(fixture.project().join("packages/lib"))
);
}
#[test]
fn a_yarn_pnp_project_is_checked_through_yarn() {
let fixture = Fixture::new();
fixture
.file(
"package.json",
r#"{"name":"pnp","devDependencies":{"eslint":"9.0.0"}}"#,
)
.file("yarn.lock", "")
.shim("yarn", "exit 0");
let output = fixture.opi(".", &["--health"]);
assert!(output.status.success(), "{}", stdout(&output));
assert_eq!(
fixture.calls("yarn").expect("yarn ran")[1..],
["run", "eslint", "."]
);
}
#[test]
fn a_lockfile_out_of_step_with_its_manifest_fails_health() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"name":"app","dependencies":{"ms":"2.1.3"}}"#)
.file("pnpm-lock.yaml", "")
.shim(
"pnpm",
"echo ' ERR_PNPM_OUTDATED_LOCKFILE Cannot install with \"frozen-lockfile\" because pnpm-lock.yaml is not up to date with package.json' >&2\nexit 1",
);
let output = fixture.opi(".", &["--health"]);
let printed = stdout(&output);
assert_eq!(output.status.code(), Some(1), "{printed}");
assert!(printed.contains("Lockfile"), "{printed}");
assert!(printed.contains("ERR_PNPM_OUTDATED_LOCKFILE"), "{printed}");
assert_eq!(
fixture.calls("pnpm").expect("pnpm ran")[1..],
[
"install",
"--frozen-lockfile",
"--lockfile-only",
"--ignore-scripts",
"--offline"
]
);
}
#[test]
fn the_commit_workflow_asks_about_the_lockfile() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"name":"app"}"#)
.file("package-lock.json", "{}")
.shim("npm", "exit 0");
let output = fixture.opi(".", &["--check", "commit"]);
let printed = stdout(&output);
assert!(output.status.success(), "{printed}");
assert!(printed.contains("Lockfile"), "{printed}");
assert_eq!(
fixture.calls("npm").expect("npm ran")[1..],
["ci", "--dry-run", "--ignore-scripts"]
);
}
#[test]
fn a_saved_push_selection_skips_unselected_checks_and_builds() {
let fixture = Fixture::new();
fixture
.file(
"package.json",
r#"{"name":"app","scripts":{"build":"build"},"devDependencies":{"eslint":"9"}}"#,
)
.file(
".opi/checks.json",
r#"{"workflows":{"push":{"checks":["Secrets"],"build":false}}}"#,
)
.tool("node_modules/.bin/eslint", "exit 1")
.shim("npm", "exit 1");
let output = fixture.opi(".", &["--check", "push"]);
assert!(output.status.success(), "{}", stdout(&output));
assert!(stdout(&output).contains("Secrets"));
assert!(fixture.calls("eslint").is_none());
assert!(fixture.calls("npm").is_none());
}
#[test]
fn an_invalid_or_unavailable_selection_fails_before_running_checks() {
let fixture = Fixture::new();
fixture
.file(
"package.json",
r#"{"name":"app","devDependencies":{"eslint":"9"}}"#,
)
.tool("node_modules/.bin/eslint", "exit 0");
for contents in [
r#"{"workflows":{"push":{"checks":["Typo"],"build":false}}}"#,
r#"{"workflows":{"push":{"checks":["TypeScript"],"build":false}}}"#,
] {
fixture.file(".opi/checks.json", contents);
let output = fixture.opi(".", &["--check", "push"]);
assert_eq!(output.status.code(), Some(1), "{}", stdout(&output));
assert!(fixture.calls("eslint").is_none());
}
}
#[test]
fn a_selected_build_without_a_build_script_is_reported_as_unavailable() {
let fixture = Fixture::new();
fixture
.file(
"package.json",
r#"{"name":"app","devDependencies":{"eslint":"9"}}"#,
)
.file(
".opi/checks.json",
r#"{"workflows":{"deploy":{"checks":["Lint", "TypeScript"],"build":true}}}"#,
)
.tool("node_modules/.bin/eslint", "exit 0");
let output = fixture.opi(".", &["--check", "deploy"]);
assert_eq!(output.status.code(), Some(1), "{}", stdout(&output));
assert!(
stderr(&output).contains("TypeScript, Build"),
"{}",
stderr(&output)
);
assert!(fixture.calls("eslint").is_none());
}
#[test]
fn deploy_checks_build_but_never_run_the_deploy_script() {
let fixture = Fixture::new();
fixture
.file(
"package.json",
r#"{"name":"app","scripts":{"build":"build","deploy":"publish"}}"#,
)
.shim("npm", "exit 0");
let output = fixture.opi(".", &["--check", "deploy"]);
assert!(output.status.success(), "{}", stdout(&output));
assert_eq!(fixture.calls("npm").unwrap()[1..], ["run", "build"]);
assert!(stdout(&output).contains("Ready to deploy"));
}
#[test]
fn choosing_checks_without_a_terminal_never_writes_configuration() {
let fixture = Fixture::new();
fixture.file("package.json", r#"{"name":"app"}"#);
let output = fixture.opi(".", &["--checks", "push"]);
assert_eq!(output.status.code(), Some(1));
assert!(stderr(&output).contains("needs a terminal"));
assert!(!fixture.project().join(".opi").exists());
}
#[test]
fn bundled_secrets_need_no_installed_scanner_or_git() {
let fixture = Fixture::new();
fixture.file("package.json", r#"{"name":"app"}"#).file(
"key.pem",
&["-----BEGIN RSA ", "PRIVATE KEY-----\nexample\n"].concat(),
);
let output = fixture.opi(".", &["--health"]);
let printed = stdout(&output);
assert_eq!(output.status.code(), Some(1), "{printed}");
assert!(printed.contains("nosecrets"), "{printed}");
assert!(printed.contains("key.pem:1:"), "{printed}");
assert!(!printed.contains("-----BEGIN"), "{printed}");
}
#[test]
fn commit_wiring_checks_the_index_instead_of_an_unstaged_fix() {
let fixture = Fixture::new();
fixture.file("package.json", r#"{"name":"app"}"#).file(
"key.pem",
&["-----BEGIN RSA ", "PRIVATE KEY-----\nexample\n"].concat(),
);
let git = std::env::split_paths(&std::env::var_os("PATH").unwrap())
.map(|dir| dir.join("git"))
.find(|path| path.is_file())
.unwrap();
std::os::unix::fs::symlink(git, fixture.root.path().join("bin/git")).unwrap();
for args in [vec!["init", "-q"], vec!["add", "."]] {
assert!(
Command::new("git")
.current_dir(fixture.project())
.args(args)
.output()
.unwrap()
.status
.success()
);
}
fixture.file("key.pem", "clean\n");
let output = fixture.opi(".", &["--check", "commit"]);
let printed = stdout(&output);
assert_eq!(output.status.code(), Some(1), "{printed}");
assert!(printed.contains("key.pem:1:"), "{printed}");
assert!(printed.contains("Staged files"), "{printed}");
fixture.file(
".opi/checks.json",
r#"{"workflows":{"commit":{"checks":["Secrets"],"build":false}}}"#,
);
let output = fixture.opi(".", &["--check", "commit"]);
let printed = stdout(&output);
assert!(printed.contains("key.pem:1:"), "{printed}");
assert!(!printed.contains("Staged files"), "{printed}");
}
#[test]
fn a_failed_audit_is_not_reported_as_clean() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"name":"app"}"#)
.file("package-lock.json", "{}")
.shim(
"npm",
"echo 'npm error code ENOTFOUND registry.npmjs.org' >&2\nexit 1",
);
let output = fixture.opi(".", &["--security"]);
let printed = stdout(&output);
assert_ne!(output.status.code(), Some(0), "{printed}");
assert!(printed.contains("ENOTFOUND"), "{printed}");
assert!(!printed.contains("no known vulnerabilities"), "{printed}");
assert!(!printed.contains("Nothing to act on"), "{printed}");
}
#[test]
fn a_clean_yarn_audit_prints_nothing_and_is_clean() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"name":"app"}"#)
.file("yarn.lock", "")
.shim("yarn", "exit 0");
let output = fixture.opi(".", &["--security"]);
let printed = stdout(&output);
assert!(output.status.success(), "{printed}");
assert!(printed.contains("no known vulnerabilities"), "{printed}");
assert_eq!(
fixture.calls("yarn").expect("yarn ran")[1..],
["npm", "audit", "--all", "--recursive", "--json"]
);
}
#[test]
fn a_yarn_advisory_is_reported_and_fails_the_run() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"name":"app"}"#)
.file("yarn.lock", "")
.shim(
"yarn",
r#"printf '%s\n' '{"value":"lodash","children":{"ID":1106913,"Severity":"high","Vulnerable Versions":"<4.17.21","Dependents":["app@workspace:."]}}'
exit 1"#,
);
let output = fixture.opi(".", &["--security"]);
let printed = stdout(&output);
assert_eq!(output.status.code(), Some(1), "{printed}");
assert!(printed.contains("lodash"), "{printed}");
}
#[test]
fn a_failed_outdated_is_not_everything_is_current() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"name":"app"}"#)
.file("package-lock.json", "{}")
.shim("npm", "echo 'npm error network request failed' >&2\nexit 1");
let output = fixture.opi(".", &["--updates"]);
let printed = stdout(&output);
assert_eq!(output.status.code(), Some(1), "{printed}");
assert!(printed.contains("network request failed"), "{printed}");
assert!(!printed.contains("everything is current"), "{printed}");
}
#[test]
fn a_silent_outdated_is_everything_is_current() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"name":"app"}"#)
.file("package-lock.json", "{}")
.shim("npm", "exit 0");
let output = fixture.opi(".", &["--updates"]);
let printed = stdout(&output);
assert!(output.status.success(), "{printed}");
assert!(printed.contains("everything is current"), "{printed}");
}
#[test]
fn bun_is_never_asked_to_update_without_a_terminal() {
let fixture = Fixture::new();
fixture
.file("package.json", r#"{"name":"app"}"#)
.file("bun.lock", "")
.shim("bun", "exit 0");
let output = fixture.opi(".", &["--updates"]);
let printed = stdout(&output);
assert!(output.status.success(), "{printed}");
assert!(printed.contains("in a terminal"), "{printed}");
assert!(
fixture.calls("bun").is_none(),
"bun update ran without asking"
);
}