diff --git a/crates/git-cli/tests/common.rs b/crates/git-cli/tests/common.rs index dd59bc31..4fe3ef59 100644 --- a/crates/git-cli/tests/common.rs +++ b/crates/git-cli/tests/common.rs @@ -6,7 +6,7 @@ use nils_test_support::StubBinDir; use nils_test_support::bin::resolve; use nils_test_support::cmd::{CmdOptions, CmdOutput, run_with}; pub use nils_test_support::git::git; -use nils_test_support::git::{InitRepoOptions, init_repo_with}; +use nils_test_support::git::init_repo_main_with_initial_commit; use tempfile::TempDir; pub struct GitCliHarness { @@ -22,17 +22,7 @@ impl GitCliHarness { std::fs::create_dir_all(&xdg_config_home).expect("create XDG_CONFIG_HOME"); let stub_bin_dir = StubBinDir::new(); - let pbcopy = nils_test_support::stubs::pbcopy_stub_script(); - stub_bin_dir.write_exe("pbcopy", pbcopy.as_str()); - let wl_copy = nils_test_support::stubs::wl_copy_stub_script(); - stub_bin_dir.write_exe("wl-copy", wl_copy.as_str()); - let xclip = nils_test_support::stubs::xclip_stub_script(); - stub_bin_dir.write_exe("xclip", xclip.as_str()); - let xsel = nils_test_support::stubs::xsel_stub_script(); - stub_bin_dir.write_exe("xsel", xsel.as_str()); - stub_bin_dir.write_exe("file", nils_test_support::stubs::file_stub_script()); - let git_scope = nils_test_support::stubs::git_scope_stub_script(); - stub_bin_dir.write_exe("git-scope", git_scope.as_str()); + nils_test_support::stubs::install_git_cli_runtime_stubs(&stub_bin_dir); Self { home_dir, @@ -75,11 +65,7 @@ impl Default for GitCliHarness { } pub fn init_repo() -> tempfile::TempDir { - init_repo_with( - InitRepoOptions::new() - .with_branch("main") - .with_initial_commit(), - ) + init_repo_main_with_initial_commit() } pub fn init_bare_remote() -> tempfile::TempDir { diff --git a/crates/git-lock/tests/common.rs b/crates/git-lock/tests/common.rs index 8c64cffe..cb9a3e33 100644 --- a/crates/git-lock/tests/common.rs +++ b/crates/git-lock/tests/common.rs @@ -1,42 +1,34 @@ -#![allow(dead_code)] - use std::path::Path; use std::process::Output; use nils_test_support::bin::resolve; -use nils_test_support::cmd::{options_in_dir_with_envs, run_resolved}; -use nils_test_support::git::{InitRepoOptions, init_repo_with}; +use nils_test_support::cmd::run_resolved_in_dir_with_stdin_str; +use nils_test_support::git::init_repo_main_with_initial_commit; #[allow(unused_imports)] pub use nils_test_support::git::{commit_file, git, repo_id}; +#[allow(dead_code)] pub fn init_repo() -> tempfile::TempDir { - init_repo_with( - InitRepoOptions::new() - .with_branch("main") - .with_initial_commit(), - ) + init_repo_main_with_initial_commit() } +#[allow(dead_code)] pub fn git_lock_bin() -> std::path::PathBuf { resolve("git-lock") } +#[allow(dead_code)] pub fn run_git_lock_output( dir: &Path, args: &[&str], envs: &[(&str, &str)], input: Option<&str>, ) -> Output { - let mut options = options_in_dir_with_envs(dir, envs); - options = match input { - Some(data) => options.with_stdin_str(data), - None => options.with_stdin_bytes(&[]), - }; - - let output = run_resolved("git-lock", args, &options); + let output = run_resolved_in_dir_with_stdin_str("git-lock", dir, args, envs, input); output.into_output() } +#[allow(dead_code)] pub fn run_git_lock( dir: &Path, args: &[&str], diff --git a/crates/image-processing/src/cli.rs b/crates/image-processing/src/cli.rs index 49d6a5dc..853d5637 100644 --- a/crates/image-processing/src/cli.rs +++ b/crates/image-processing/src/cli.rs @@ -16,37 +16,6 @@ impl Operation { } } -#[allow(dead_code)] -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct FromSvgValidationRule { - pub when: &'static str, - pub expect: &'static str, -} - -#[allow(dead_code)] -pub const FROM_SVG_VALIDATION_MATRIX: [FromSvgValidationRule; 5] = [ - FromSvgValidationRule { - when: "subcommand=convert", - expect: "requires --from-svg + --to png|webp|svg + --out", - }, - FromSvgValidationRule { - when: "subcommand=convert", - expect: "forbids --in", - }, - FromSvgValidationRule { - when: "subcommand=convert", - expect: "accepts optional --width/--height for png|webp outputs", - }, - FromSvgValidationRule { - when: "subcommand=svg-validate", - expect: "requires exactly one --in and explicit --out", - }, - FromSvgValidationRule { - when: "subcommand=svg-validate", - expect: "forbids --from-svg/--to/--width/--height", - }, -]; - #[derive(Debug, Parser)] #[command( name = "image-processing", diff --git a/crates/memo-cli/tests/support/mod.rs b/crates/memo-cli/tests/support/mod.rs index bb4cb4f9..3714173a 100644 --- a/crates/memo-cli/tests/support/mod.rs +++ b/crates/memo-cli/tests/support/mod.rs @@ -1,18 +1,19 @@ -#![allow(dead_code)] - use nils_test_support::cmd::{CmdOptions, CmdOutput, run_resolved}; use std::fs; use std::path::{Path, PathBuf}; +#[allow(dead_code)] pub fn test_db_path(name: &str) -> PathBuf { let dir = tempfile::tempdir().expect("tempdir should be created"); dir.keep().join(format!("{name}.db")) } +#[allow(dead_code)] pub fn parse_json_stdout(output: &CmdOutput) -> serde_json::Value { serde_json::from_slice(&output.stdout).expect("stdout should be valid JSON") } +#[allow(dead_code)] pub fn fixture_json(name: &str) -> serde_json::Value { let path = PathBuf::from(env!("CARGO_MANIFEST_DIR")) .join("tests") @@ -26,10 +27,12 @@ pub fn fixture_json(name: &str) -> serde_json::Value { }) } +#[allow(dead_code)] pub fn run_memo_cli(db_path: &Path, args: &[&str], stdin: Option<&str>) -> CmdOutput { run_memo_cli_with_env(db_path, args, stdin, &[]) } +#[allow(dead_code)] pub fn run_memo_cli_with_env( db_path: &Path, args: &[&str], diff --git a/crates/nils-test-support/src/cmd.rs b/crates/nils-test-support/src/cmd.rs index 7278072e..775b2624 100644 --- a/crates/nils-test-support/src/cmd.rs +++ b/crates/nils-test-support/src/cmd.rs @@ -192,6 +192,26 @@ pub fn run_resolved_in_dir( run_resolved(bin_name, args, &options) } +/// Resolve and run a workspace binary in a specific directory with optional +/// UTF-8 stdin. +/// +/// When `stdin` is `None`, this helper sends empty stdin bytes to keep test +/// command execution non-interactive. +pub fn run_resolved_in_dir_with_stdin_str( + bin_name: &str, + dir: &Path, + args: &[&str], + envs: &[(&str, &str)], + stdin: Option<&str>, +) -> CmdOutput { + let mut options = options_in_dir_with_envs(dir, envs); + options = match stdin { + Some(input) => options.with_stdin_str(input), + None => options.with_stdin_bytes(&[]), + }; + run_resolved(bin_name, args, &options) +} + pub fn run_with(bin: &Path, args: &[&str], options: &CmdOptions) -> CmdOutput { run_impl(bin, args, options, None) } diff --git a/crates/nils-test-support/src/fs.rs b/crates/nils-test-support/src/fs.rs index 8b5bf97c..febfd6ce 100644 --- a/crates/nils-test-support/src/fs.rs +++ b/crates/nils-test-support/src/fs.rs @@ -14,6 +14,10 @@ pub fn write_text(path: &Path, contents: &str) -> PathBuf { path.to_path_buf() } +pub fn write_text_in_dir(dir: &Path, rel: &str, contents: &str) -> PathBuf { + write_text(&dir.join(rel), contents) +} + pub fn write_bytes(path: &Path, contents: &[u8]) -> PathBuf { ensure_parent_dir(path); std::fs::write(path, contents).expect("write bytes"); @@ -41,3 +45,7 @@ pub fn write_executable(path: &Path, contents: &str) -> PathBuf { path.to_path_buf() } + +pub fn write_executable_in_dir(dir: &Path, rel: &str, contents: &str) -> PathBuf { + write_executable(&dir.join(rel), contents) +} diff --git a/crates/nils-test-support/src/git.rs b/crates/nils-test-support/src/git.rs index 67cd4c01..8214fd91 100644 --- a/crates/nils-test-support/src/git.rs +++ b/crates/nils-test-support/src/git.rs @@ -116,6 +116,18 @@ pub fn init_repo_with(options: InitRepoOptions) -> TempDir { dir } +pub fn init_repo_main() -> TempDir { + init_repo_with(InitRepoOptions::new().with_branch("main")) +} + +pub fn init_repo_main_with_initial_commit() -> TempDir { + init_repo_with( + InitRepoOptions::new() + .with_branch("main") + .with_initial_commit(), + ) +} + pub fn worktree_add_branch(repo: &Path, worktree_path: &Path, branch: &str) { let worktree_path = worktree_path.to_string_lossy().to_string(); git(repo, &["worktree", "add", &worktree_path, "-b", branch]); diff --git a/crates/nils-test-support/src/stubs.rs b/crates/nils-test-support/src/stubs.rs index a7943ac8..e7a83c18 100644 --- a/crates/nils-test-support/src/stubs.rs +++ b/crates/nils-test-support/src/stubs.rs @@ -291,6 +291,27 @@ pub fn git_scope_stub_script() -> String { script } +/// Install the default external command stubs used by git-cli integration +/// harnesses. +pub fn install_git_cli_runtime_stubs(dir: &crate::StubBinDir) { + let pbcopy = pbcopy_stub_script(); + dir.write_exe("pbcopy", &pbcopy); + + let wl_copy = wl_copy_stub_script(); + dir.write_exe("wl-copy", &wl_copy); + + let xclip = xclip_stub_script(); + dir.write_exe("xclip", &xclip); + + let xsel = xsel_stub_script(); + dir.write_exe("xsel", &xsel); + + dir.write_exe("file", file_stub_script()); + + let git_scope = git_scope_stub_script(); + dir.write_exe("git-scope", &git_scope); +} + #[cfg(test)] mod tests { use super::*; @@ -448,4 +469,15 @@ mod tests { let git_scope = git_scope_stub_script(); assert_all_contains(&git_scope, &["echo \"git-scope $*\"", "exit 0"]); } + + #[test] + fn install_git_cli_runtime_stubs_writes_expected_executables() { + let dir = crate::StubBinDir::new(); + install_git_cli_runtime_stubs(&dir); + + for name in ["pbcopy", "wl-copy", "xclip", "xsel", "file", "git-scope"] { + let path = dir.path().join(name); + assert!(path.exists(), "missing stub executable: {}", path.display()); + } + } } diff --git a/crates/nils-test-support/tests/bin_cmd.rs b/crates/nils-test-support/tests/bin_cmd.rs index c55bffaf..31e9d00a 100644 --- a/crates/nils-test-support/tests/bin_cmd.rs +++ b/crates/nils-test-support/tests/bin_cmd.rs @@ -117,6 +117,44 @@ printf "%s|%s" "${NTS_REMOVE_ME-unset}" "${NTS_KEEP_ME-unset}" assert_eq!(output.stdout_text(), "unset|present"); } +#[cfg(unix)] +#[test] +fn run_resolved_in_dir_with_stdin_str_supports_optional_text_stdin() { + let lock = GlobalStateLock::new(); + let temp = TempDir::new().expect("tempdir"); + let script = r#"#!/bin/sh +printf "%s|" "${NTS_VALUE-unset}" +cat - +"#; + write_exe(temp.path(), "resolved-stdin-test", script); + let bin = temp.path().join("resolved-stdin-test"); + let _guard = EnvGuard::set( + &lock, + "CARGO_BIN_EXE_resolved-stdin-test", + bin.to_str().expect("path"), + ); + + let with_text = cmd::run_resolved_in_dir_with_stdin_str( + "resolved-stdin-test", + temp.path(), + &[], + &[("NTS_VALUE", "ok")], + Some("payload"), + ); + assert_eq!(with_text.code, 0); + assert_eq!(with_text.stdout_text(), "ok|payload"); + + let without_text = cmd::run_resolved_in_dir_with_stdin_str( + "resolved-stdin-test", + temp.path(), + &[], + &[("NTS_VALUE", "ok")], + None, + ); + assert_eq!(without_text.code, 0); + assert_eq!(without_text.stdout_text(), "ok|"); +} + #[cfg(unix)] #[test] fn run_with_env_set_wins_after_env_remove() { diff --git a/crates/nils-test-support/tests/fs.rs b/crates/nils-test-support/tests/fs.rs index a0e45f1e..d40d8ffe 100644 --- a/crates/nils-test-support/tests/fs.rs +++ b/crates/nils-test-support/tests/fs.rs @@ -11,6 +11,13 @@ fn write_text_creates_parents_and_writes_contents() { assert_eq!(std_fs::read_to_string(written).expect("read"), "hello\n"); } +#[test] +fn write_text_in_dir_joins_relative_path_and_writes_contents() { + let temp = tempfile::TempDir::new().expect("tempdir"); + let written = fs::write_text_in_dir(temp.path(), "nested/dir/file.txt", "hello\n"); + assert_eq!(std_fs::read_to_string(written).expect("read"), "hello\n"); +} + #[test] fn write_bytes_preserves_raw_bytes() { let temp = tempfile::TempDir::new().expect("tempdir"); @@ -42,6 +49,16 @@ fn write_executable_writes_contents() { ); } +#[test] +fn write_executable_in_dir_joins_relative_path() { + let temp = tempfile::TempDir::new().expect("tempdir"); + let written = fs::write_executable_in_dir(temp.path(), "bin/run.sh", "#!/bin/sh\necho ok\n"); + assert_eq!( + std_fs::read_to_string(written).expect("read"), + "#!/bin/sh\necho ok\n" + ); +} + #[cfg(unix)] #[test] fn write_executable_sets_unix_mode() { diff --git a/crates/nils-test-support/tests/git.rs b/crates/nils-test-support/tests/git.rs index 47233260..8ecde211 100644 --- a/crates/nils-test-support/tests/git.rs +++ b/crates/nils-test-support/tests/git.rs @@ -12,6 +12,23 @@ fn init_repo_with_default_branch_and_config() { assert_eq!(email.trim_end(), "test@example.com"); } +#[test] +fn init_repo_main_sets_main_branch() { + let repo = git::init_repo_main(); + let branch = git::git(repo.path(), &["symbolic-ref", "--short", "HEAD"]); + assert_eq!(branch.trim_end(), "main"); +} + +#[test] +fn init_repo_main_with_initial_commit_sets_main_and_commits() { + let repo = git::init_repo_main_with_initial_commit(); + let branch = git::git(repo.path(), &["symbolic-ref", "--short", "HEAD"]); + assert_eq!(branch.trim_end(), "main"); + + let head = git::git(repo.path(), &["rev-parse", "HEAD"]); + assert_eq!(head.trim().len(), 40); +} + #[test] fn init_repo_with_initial_commit_creates_commit() { let repo = git::init_repo_with(git::InitRepoOptions::new().with_initial_commit()); diff --git a/crates/plan-issue-cli/tests/common.rs b/crates/plan-issue-cli/tests/common.rs index e569aa55..9e7446e3 100644 --- a/crates/plan-issue-cli/tests/common.rs +++ b/crates/plan-issue-cli/tests/common.rs @@ -25,17 +25,9 @@ fn run_bin_with_options(bin_name: &str, args: &[&str], options: CmdOptions) -> C } } -fn run_bin_with_env(bin_name: &str, args: &[&str], env: &[(&str, &str)]) -> CmdOut { - run_bin_with_options(bin_name, args, plan_issue_cmd_options().with_envs(env)) -} - -fn run_bin(bin_name: &str, args: &[&str]) -> CmdOut { - run_bin_with_options(bin_name, args, plan_issue_cmd_options()) -} - #[allow(dead_code)] pub fn run_plan_issue(args: &[&str]) -> CmdOut { - run_bin("plan-issue", args) + run_bin_with_options("plan-issue", args, plan_issue_cmd_options()) } #[allow(dead_code)] @@ -45,10 +37,14 @@ pub fn run_plan_issue_with_options(args: &[&str], options: CmdOptions) -> CmdOut #[allow(dead_code)] pub fn run_plan_issue_local(args: &[&str]) -> CmdOut { - run_bin("plan-issue-local", args) + run_bin_with_options("plan-issue-local", args, plan_issue_cmd_options()) } #[allow(dead_code)] pub fn run_plan_issue_local_with_env(args: &[&str], env: &[(&str, &str)]) -> CmdOut { - run_bin_with_env("plan-issue-local", args, env) + run_bin_with_options( + "plan-issue-local", + args, + plan_issue_cmd_options().with_envs(env), + ) } diff --git a/crates/semantic-commit/tests/common.rs b/crates/semantic-commit/tests/common.rs index eb975177..f1d3db81 100644 --- a/crates/semantic-commit/tests/common.rs +++ b/crates/semantic-commit/tests/common.rs @@ -1,39 +1,34 @@ -#![allow(dead_code)] - use std::path::Path; use std::process::Output; -use nils_test_support::cmd::{options_in_dir_with_envs, run_resolved}; -use nils_test_support::fs::{write_executable as write_executable_file, write_text}; -use nils_test_support::git::{InitRepoOptions, init_repo_with}; +use nils_test_support::cmd::run_resolved_in_dir_with_stdin_str; +use nils_test_support::fs::{write_executable_in_dir, write_text_in_dir}; +use nils_test_support::git::init_repo_main; #[allow(unused_imports)] pub use nils_test_support::git::{git, git_output}; +#[allow(dead_code)] pub fn init_repo() -> tempfile::TempDir { - init_repo_with(InitRepoOptions::new().with_branch("main")) + init_repo_main() } +#[allow(dead_code)] pub fn write_file(dir: &Path, name: &str, contents: &str) { - let path = dir.join(name); - write_text(&path, contents); + write_text_in_dir(dir, name, contents); } +#[allow(dead_code)] pub fn run_semantic_commit_output( dir: &Path, args: &[&str], envs: &[(&str, &str)], input: Option<&str>, ) -> Output { - let mut options = options_in_dir_with_envs(dir, envs); - options = match input { - Some(data) => options.with_stdin_str(data), - None => options.with_stdin_bytes(&[]), - }; - let output = run_resolved("semantic-commit", args, &options); + let output = run_resolved_in_dir_with_stdin_str("semantic-commit", dir, args, envs, input); output.into_output() } +#[allow(dead_code)] pub fn write_executable(dir: &Path, rel: &str, contents: &str) { - let path = dir.join(rel); - write_executable_file(&path, contents); + write_executable_in_dir(dir, rel, contents); }