use std::fs;
use tempfile::TempDir;
mod config_tests {
use super::*;
use plzplz::config::{self, FailHook};
fn write_config(dir: &TempDir, content: &str) -> std::path::PathBuf {
let path = dir.path().join("plz.toml");
fs::write(&path, content).unwrap();
path
}
#[test]
fn parse_minimal_config() {
let dir = TempDir::new().unwrap();
let path = write_config(
&dir,
r#"
[tasks.hello]
run = "echo hello"
"#,
);
let cfg = config::load(&path).unwrap();
assert!(cfg.tasks.contains_key("hello"));
}
#[test]
fn parse_full_config() {
let dir = TempDir::new().unwrap();
let path = write_config(
&dir,
r#"
[tasks.build]
run = "echo build"
description = "Build the project"
[tasks.test]
run_serial = ["echo one", "echo two"]
[tasks.lint]
run_parallel = ["echo a", "echo b"]
"#,
);
let cfg = config::load(&path).unwrap();
assert_eq!(cfg.tasks.len(), 3);
}
#[test]
fn parse_task_with_run() {
let dir = TempDir::new().unwrap();
let path = write_config(
&dir,
r#"
[tasks.hello]
run = "echo hello"
"#,
);
let cfg = config::load(&path).unwrap();
assert_eq!(cfg.tasks["hello"].run.as_deref(), Some("echo hello"));
}
#[test]
fn parse_task_with_run_serial() {
let dir = TempDir::new().unwrap();
let path = write_config(
&dir,
r#"
[tasks.multi]
run_serial = ["echo one", "echo two"]
"#,
);
let cfg = config::load(&path).unwrap();
let serial = cfg.tasks["multi"].run_serial.as_ref().unwrap();
assert_eq!(serial, &["echo one", "echo two"]);
}
#[test]
fn parse_task_with_run_parallel() {
let dir = TempDir::new().unwrap();
let path = write_config(
&dir,
r#"
[tasks.multi]
run_parallel = ["echo a", "echo b"]
"#,
);
let cfg = config::load(&path).unwrap();
let parallel = cfg.tasks["multi"].run_parallel.as_ref().unwrap();
assert_eq!(parallel, &["echo a", "echo b"]);
}
#[test]
fn parse_task_with_tool_env() {
let dir = TempDir::new().unwrap();
let path = write_config(
&dir,
r#"
[tasks.a]
run = "test"
env = "uv"
[tasks.b]
run = "test"
env = "pnpm"
"#,
);
let cfg = config::load(&path).unwrap();
assert_eq!(cfg.tasks["a"].tool_env.as_deref(), Some("uv"));
assert_eq!(cfg.tasks["b"].tool_env.as_deref(), Some("pnpm"));
}
#[test]
fn parse_task_with_dir() {
let dir = TempDir::new().unwrap();
let path = write_config(
&dir,
r#"
[tasks.sub]
run = "echo hi"
dir = "subdir"
"#,
);
let cfg = config::load(&path).unwrap();
assert_eq!(cfg.tasks["sub"].dir.as_deref(), Some("subdir"));
}
#[test]
fn parse_fail_hook_command() {
let dir = TempDir::new().unwrap();
let path = write_config(
&dir,
r#"
[tasks.lint]
run = "cargo clippy"
fail_hook = "cargo fmt"
"#,
);
let cfg = config::load(&path).unwrap();
match &cfg.tasks["lint"].fail_hook {
Some(FailHook::Command(cmd)) => assert_eq!(cmd, "cargo fmt"),
other => panic!("Expected Command, got {:?}", other),
}
}
#[test]
fn parse_fail_hook_suggest() {
let dir = TempDir::new().unwrap();
let path = write_config(
&dir,
r#"
[tasks.lint]
run = "cargo clippy"
fail_hook = { suggest_command = "plz fix" }
"#,
);
let cfg = config::load(&path).unwrap();
match &cfg.tasks["lint"].fail_hook {
Some(FailHook::Suggest { suggest_command }) => {
assert_eq!(suggest_command, "plz fix")
}
other => panic!("Expected Suggest, got {:?}", other),
}
}
#[test]
fn parse_fail_hook_message() {
let dir = TempDir::new().unwrap();
let path = write_config(
&dir,
r#"
[tasks.lint]
run = "cargo clippy"
fail_hook = { message = "Run plz fix first" }
"#,
);
let cfg = config::load(&path).unwrap();
match &cfg.tasks["lint"].fail_hook {
Some(FailHook::Message(msg)) => assert_eq!(msg, "Run plz fix first"),
other => panic!("Expected Message, got {:?}", other),
}
}
#[test]
fn parse_explicit_description() {
let dir = TempDir::new().unwrap();
let path = write_config(
&dir,
r#"
[tasks.build]
run = "cargo build"
description = "Build the project"
"#,
);
let cfg = config::load(&path).unwrap();
assert_eq!(
cfg.tasks["build"].description.as_deref(),
Some("Build the project")
);
}
#[test]
fn parse_comment_description() {
let dir = TempDir::new().unwrap();
let path = write_config(
&dir,
r#"
# Build the project
[tasks.build]
run = "cargo build"
"#,
);
let cfg = config::load(&path).unwrap();
assert_eq!(
cfg.tasks["build"].description.as_deref(),
Some("Build the project")
);
}
#[test]
fn extract_comment_single_line() {
assert_eq!(
config::extract_comment("# Hello world\n"),
Some("Hello world".to_string())
);
}
#[test]
fn extract_comment_multi_line() {
assert_eq!(
config::extract_comment("# Line one\n# Line two\n"),
Some("Line one Line two".to_string())
);
}
#[test]
fn extract_comment_empty() {
assert_eq!(config::extract_comment(""), None);
assert_eq!(config::extract_comment("\n\n"), None);
}
#[test]
fn parse_invalid_toml_errors() {
let dir = TempDir::new().unwrap();
let path = write_config(&dir, "this is not valid toml [[[");
assert!(config::load(&path).is_err());
}
#[test]
fn parse_missing_file_errors() {
let dir = TempDir::new().unwrap();
let path = dir.path().join("nonexistent.toml");
assert!(config::load(&path).is_err());
}
}
mod runner_tests {
use super::*;
use plzplz::config;
use plzplz::runner;
fn load_config(dir: &TempDir, content: &str) -> config::PlzConfig {
let path = dir.path().join("plz.toml");
fs::write(&path, content).unwrap();
config::load(&path).unwrap()
}
#[test]
fn run_simple_command() {
let dir = TempDir::new().unwrap();
let cfg = load_config(
&dir,
r#"
[tasks.hello]
run = "echo hello"
"#,
);
runner::run_task(&cfg, "hello", dir.path(), false).unwrap();
}
#[test]
fn run_failing_command() {
let dir = TempDir::new().unwrap();
let cfg = load_config(
&dir,
r#"
[tasks.fail]
run = "false"
"#,
);
assert!(runner::run_task(&cfg, "fail", dir.path(), false).is_err());
}
#[test]
fn run_serial_commands() {
let dir = TempDir::new().unwrap();
let marker = dir.path().join("marker.txt");
let cfg = load_config(
&dir,
&format!(
r#"
[tasks.serial]
run_serial = ["echo first", "touch {}"]
"#,
marker.display()
),
);
runner::run_task(&cfg, "serial", dir.path(), false).unwrap();
assert!(marker.exists());
}
#[test]
fn run_serial_stops_on_failure() {
let dir = TempDir::new().unwrap();
let marker = dir.path().join("should_not_exist.txt");
let cfg = load_config(
&dir,
&format!(
r#"
[tasks.serial]
run_serial = ["false", "touch {}"]
"#,
marker.display()
),
);
assert!(runner::run_task(&cfg, "serial", dir.path(), false).is_err());
assert!(!marker.exists());
}
#[test]
fn run_parallel_commands() {
let dir = TempDir::new().unwrap();
let m1 = dir.path().join("p1.txt");
let m2 = dir.path().join("p2.txt");
let cfg = load_config(
&dir,
&format!(
r#"
[tasks.par]
run_parallel = ["touch {}", "touch {}"]
"#,
m1.display(),
m2.display()
),
);
runner::run_task(&cfg, "par", dir.path(), false).unwrap();
assert!(m1.exists());
assert!(m2.exists());
}
#[test]
fn run_parallel_fails_on_error() {
let dir = TempDir::new().unwrap();
let cfg = load_config(
&dir,
r#"
[tasks.par]
run_parallel = ["true", "false"]
"#,
);
assert!(runner::run_task(&cfg, "par", dir.path(), false).is_err());
}
#[test]
fn run_with_working_dir() {
let dir = TempDir::new().unwrap();
let subdir = dir.path().join("mysubdir");
fs::create_dir(&subdir).unwrap();
let out = dir.path().join("pwd_out.txt");
let cfg = load_config(
&dir,
&format!(
r#"
[tasks.wd]
run = "pwd > {}"
dir = "mysubdir"
"#,
out.display()
),
);
runner::run_task(&cfg, "wd", dir.path(), false).unwrap();
let content = fs::read_to_string(&out).unwrap();
assert!(content.trim().ends_with("mysubdir"));
}
#[test]
fn run_with_tool_env_uv() {
let dir = TempDir::new().unwrap();
let cfg = load_config(
&dir,
r#"
[tasks.uv_test]
run = "echo hello"
env = "uv"
"#,
);
assert_eq!(cfg.tasks["uv_test"].tool_env.as_deref(), Some("uv"));
let _ = runner::run_task(&cfg, "uv_test", dir.path(), false);
}
#[test]
fn run_with_tool_env_pnpm() {
let dir = TempDir::new().unwrap();
let cfg = load_config(
&dir,
r#"
[tasks.pnpm_test]
run = "echo hello"
env = "pnpm"
"#,
);
assert_eq!(cfg.tasks["pnpm_test"].tool_env.as_deref(), Some("pnpm"));
let _ = runner::run_task(&cfg, "pnpm_test", dir.path(), false);
}
#[test]
fn run_task_reference() {
let dir = TempDir::new().unwrap();
let marker = dir.path().join("ref_marker.txt");
let cfg = load_config(
&dir,
&format!(
r#"
[tasks.caller]
run = "plz:target"
[tasks.target]
run = "touch {}"
"#,
marker.display()
),
);
runner::run_task(&cfg, "caller", dir.path(), false).unwrap();
assert!(marker.exists());
}
#[test]
fn run_unknown_task_errors() {
let dir = TempDir::new().unwrap();
let cfg = load_config(
&dir,
r#"
[tasks.hello]
run = "echo hello"
"#,
);
let err = runner::run_task(&cfg, "nonexistent", dir.path(), false);
assert!(err.is_err());
assert!(err.unwrap_err().to_string().contains("Unknown task"));
}
#[test]
fn run_serial_with_task_ref() {
let dir = TempDir::new().unwrap();
let m1 = dir.path().join("s1.txt");
let m2 = dir.path().join("s2.txt");
let cfg = load_config(
&dir,
&format!(
r#"
[tasks.main]
run_serial = ["touch {}", "plz:helper"]
[tasks.helper]
run = "touch {}"
"#,
m1.display(),
m2.display()
),
);
runner::run_task(&cfg, "main", dir.path(), false).unwrap();
assert!(m1.exists());
assert!(m2.exists());
}
#[test]
fn run_parallel_with_task_ref() {
let dir = TempDir::new().unwrap();
let m1 = dir.path().join("p1.txt");
let m2 = dir.path().join("p2.txt");
let cfg = load_config(
&dir,
&format!(
r#"
[tasks.main]
run_parallel = ["touch {}", "plz:helper"]
[tasks.helper]
run = "touch {}"
"#,
m1.display(),
m2.display()
),
);
runner::run_task(&cfg, "main", dir.path(), false).unwrap();
assert!(m1.exists());
assert!(m2.exists());
}
}
mod init_tests {
use super::*;
use plzplz::init;
#[test]
fn detect_project_type_rust() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
unsafe {
std::env::set_var("PLZ_CONFIG_DIR", dir.path().join("no_config"));
}
let types = init::detect_project_types(dir.path());
assert!(types.iter().any(|t| t.name == "rust"));
}
#[test]
fn detect_project_type_pnpm() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("pnpm-lock.yaml"), "").unwrap();
unsafe {
std::env::set_var("PLZ_CONFIG_DIR", dir.path().join("no_config"));
}
let types = init::detect_project_types(dir.path());
assert!(types.iter().any(|t| t.name == "pnpm"));
}
#[test]
fn detect_project_type_uv() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("uv.lock"), "").unwrap();
unsafe {
std::env::set_var("PLZ_CONFIG_DIR", dir.path().join("no_config"));
}
let types = init::detect_project_types(dir.path());
assert!(types.iter().any(|t| t.name == "uv"));
}
#[test]
fn detect_project_type_multiple() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
fs::write(dir.path().join("pnpm-lock.yaml"), "").unwrap();
unsafe {
std::env::set_var("PLZ_CONFIG_DIR", dir.path().join("no_config"));
}
let types = init::detect_project_types(dir.path());
assert!(types.len() >= 2);
assert!(types.iter().any(|t| t.name == "rust"));
assert!(types.iter().any(|t| t.name == "pnpm"));
}
#[test]
fn add_suffix_renames_tasks() {
let toml = r#"
[tasks.dev]
run = "cargo run"
[tasks.build]
run = "cargo build"
"#;
let (_, tasks) = init::parse_default(toml).unwrap();
let result = init::add_suffix_to_toml(toml, "rust", &tasks);
assert!(result.contains("[tasks.dev-rust]"));
assert!(result.contains("[tasks.build-rust]"));
assert!(!result.contains("[tasks.dev]"));
assert!(!result.contains("[tasks.build]"));
}
#[test]
fn add_suffix_updates_plz_references() {
let toml = r#"
[tasks.lint]
run_serial = ["cargo clippy", "cargo fmt --check"]
fail_hook = { suggest_command = "plz fix" }
[tasks.fix]
run_serial = ["cargo fmt", "cargo clippy --fix --allow-dirty"]
"#;
let (_, tasks) = init::parse_default(toml).unwrap();
let result = init::add_suffix_to_toml(toml, "rust", &tasks);
assert!(result.contains("plz fix-rust"), "got:\n{result}");
}
#[test]
fn add_suffix_not_applied_for_single_type() {
let toml = r#"
[tasks.dev]
run = "cargo run"
"#;
let (_, tasks) = init::parse_default(toml).unwrap();
assert!(tasks.iter().any(|(n, _)| n == "dev"));
}
#[test]
fn detect_project_type_none() {
let dir = TempDir::new().unwrap();
unsafe {
std::env::set_var("PLZ_CONFIG_DIR", dir.path().join("no_config"));
}
let types = init::detect_project_types(dir.path());
assert!(types.is_empty());
}
#[test]
fn parse_default_rust() {
let rust_toml = init::DEFAULTS
.iter()
.find(|(name, _, _)| *name == "rust")
.unwrap()
.2;
let (_, tasks) = init::parse_default(rust_toml).unwrap();
let names: Vec<&str> = tasks.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"build"));
assert!(names.contains(&"test"));
assert!(names.contains(&"lint"));
}
#[test]
fn parse_default_pnpm() {
let pnpm_toml = init::DEFAULTS
.iter()
.find(|(name, _, _)| *name == "pnpm")
.unwrap()
.2;
let (_, tasks) = init::parse_default(pnpm_toml).unwrap();
let names: Vec<&str> = tasks.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"dev"));
assert!(names.contains(&"build"));
assert!(names.contains(&"test"));
}
#[test]
fn parse_default_uv() {
let uv_toml = init::DEFAULTS
.iter()
.find(|(name, _, _)| *name == "uv")
.unwrap()
.2;
let (_, tasks) = init::parse_default(uv_toml).unwrap();
let names: Vec<&str> = tasks.iter().map(|(n, _)| n.as_str()).collect();
assert!(names.contains(&"test"));
assert!(names.contains(&"build"));
assert!(names.contains(&"lint"));
}
#[test]
fn merge_defaults_override_task() {
let embedded = r#"
[tasks.build]
run = "cargo build"
[tasks.test]
run = "cargo test"
"#;
let user = r#"
[tasks.build]
run = "cargo build --release"
"#;
let (doc, _) = init::merge_defaults(embedded, user).unwrap();
let tasks = doc.get("tasks").unwrap().as_table().unwrap();
let build = tasks.get("build").unwrap().as_table().unwrap();
assert_eq!(
build.get("run").unwrap().as_str().unwrap(),
"cargo build --release"
);
}
#[test]
fn merge_defaults_blank_removes_task() {
let embedded = r#"
[tasks.build]
run = "cargo build"
[tasks.test]
run = "cargo test"
"#;
let user = r#"
[tasks.test]
"#;
let (doc, tasks) = init::merge_defaults(embedded, user).unwrap();
let table = doc.get("tasks").unwrap().as_table().unwrap();
assert!(table.get("build").is_some());
assert!(table.get("test").is_none());
assert!(!tasks.iter().any(|(n, _)| n == "test"));
}
#[test]
fn merge_defaults_preserves_unmodified() {
let embedded = r#"
[tasks.build]
run = "cargo build"
[tasks.test]
run = "cargo test"
[tasks.lint]
run = "cargo clippy"
"#;
let user = r#"
[tasks.build]
run = "cargo build --release"
"#;
let (doc, _) = init::merge_defaults(embedded, user).unwrap();
let table = doc.get("tasks").unwrap().as_table().unwrap();
assert!(table.get("test").is_some());
assert!(table.get("lint").is_some());
let test = table.get("test").unwrap().as_table().unwrap();
assert_eq!(test.get("run").unwrap().as_str().unwrap(), "cargo test");
}
#[test]
fn detect_project_type_rust_with_commented_out_defaults() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("Cargo.toml"), "[package]").unwrap();
let defaults_dir = dir.path().join("config/defaults");
fs::create_dir_all(&defaults_dir).unwrap();
fs::write(
defaults_dir.join("rust.plz.toml"),
r#"# These defaults extend the built-ins.
# Uncomment to override. Leave blank to omit from the list.
# [tasks.build]
# run = "cargo build"
# [tasks.test]
# run = "cargo test"
"#,
)
.unwrap();
unsafe {
std::env::set_var("PLZ_CONFIG_DIR", dir.path().join("config"));
}
let types = init::detect_project_types(dir.path());
assert!(
types.iter().any(|t| t.name == "rust"),
"Should detect Cargo.toml even when user defaults are all commented out"
);
}
#[test]
fn generate_scaffold_comments_out_content() {
let input = r#"[tasks.build]
run = "cargo build"
# Run tests
[tasks.test]
run = "cargo test"
"#;
let result = init::generate_scaffold(input);
assert!(result.contains("# [tasks.build]"));
assert!(result.contains("# run = \"cargo build\""));
assert!(result.contains("# Run tests"));
assert!(!result.contains("# # Run tests"));
}
}
mod cli_tests {
use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;
#[allow(deprecated)]
fn plz() -> Command {
Command::cargo_bin("plz").unwrap()
}
#[test]
fn cli_no_args_no_config_shows_help() {
let dir = TempDir::new().unwrap();
plz()
.current_dir(dir.path())
.assert()
.success()
.stdout(predicate::str::contains("Usage:"));
}
#[test]
fn cli_run_task() {
let dir = TempDir::new().unwrap();
let marker = dir.path().join("ran.txt");
fs::write(
dir.path().join("plz.toml"),
format!(
r#"
[tasks.hello]
run = "touch {}"
"#,
marker.display()
),
)
.unwrap();
plz()
.arg("hello")
.current_dir(dir.path())
.assert()
.success();
assert!(marker.exists());
}
#[test]
fn cli_run_unknown_task_errors() {
let dir = TempDir::new().unwrap();
fs::write(
dir.path().join("plz.toml"),
r#"
[tasks.hello]
run = "echo hello"
"#,
)
.unwrap();
plz()
.arg("nonexistent")
.current_dir(dir.path())
.assert()
.failure()
.stderr(predicate::str::contains("Unknown task"));
}
#[test]
fn cli_init_already_exists() {
let dir = TempDir::new().unwrap();
fs::write(dir.path().join("plz.toml"), "[tasks]").unwrap();
plz()
.arg("init")
.current_dir(dir.path())
.assert()
.failure()
.stderr(predicate::str::contains("already exists"));
}
}