use assert_cmd::Command;
use predicates::prelude::*;
use std::fs;
use tempfile::TempDir;
#[test]
fn prints_help() {
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains("Scan software projects"));
}
#[test]
fn scan_prints_markdown_to_stdout_by_default() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.assert()
.success()
.stdout(predicate::str::contains("# Project Report"));
}
#[test]
fn scan_prints_json_to_stdout() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("json")
.assert()
.success()
.stdout(predicate::str::contains("\"identity\""));
}
#[test]
fn scan_prints_terminal_dashboard_when_requested() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.assert()
.success()
.stdout(
predicate::str::contains("Projd Scan Report")
.and(predicate::str::contains("Health"))
.and(predicate::str::contains("Health Score"))
.and(predicate::str::contains("Signal"))
.and(predicate::str::contains("Evidence"))
.and(predicate::str::contains("Source Control"))
.and(predicate::str::contains("License"))
.and(predicate::str::contains("CI Providers"))
.and(predicate::str::contains("Languages"))
.and(predicate::str::contains("Code Statistics"))
.and(predicate::str::contains("Rust"))
.and(predicate::str::contains("files scanned"))
.and(predicate::str::contains("â–ˆ"))
.and(predicate::str::contains("Risks")),
);
}
#[test]
fn scan_terminal_dashboard_prints_git_license_and_ci_summary() {
let fixture = ProjectFixture::new();
fixture.write(".workflow/rust-ci.yml", "version: '1.0'\n");
fixture.write(".gitlab-ci.yml", "stages: []\n");
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.arg("--color")
.arg("never")
.assert()
.success()
.stdout(predicate::str::contains("License"))
.stdout(predicate::str::contains("MIT"))
.stdout(predicate::str::contains("CI Providers"))
.stdout(predicate::str::contains("Gitee Go"))
.stdout(predicate::str::contains("GitLab CI"))
.stdout(predicate::str::contains("Source Control"))
.stdout(predicate::str::contains("Kind"));
}
#[test]
fn scan_terminal_dashboard_supports_ascii_output() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.arg("--style")
.arg("plain")
.assert()
.success()
.stdout(predicate::str::contains("Projd Scan Report"))
.stdout(predicate::str::contains("Grade"))
.stdout(predicate::str::contains("#"))
.stdout(predicate::str::contains("OK"))
.stdout(predicate::str::contains("â–ˆ").not());
}
#[test]
fn scan_terminal_dashboard_aggregates_repeated_sections() {
let fixture = ProjectFixture::workspace();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.arg("--style")
.arg("plain")
.assert()
.success()
.stdout(predicate::str::contains("Cargo"))
.stdout(predicate::str::contains("Rust"))
.stdout(predicate::str::contains("Code Statistics"))
.stdout(predicate::str::contains("3 manifest(s)"))
.stdout(predicate::str::contains("cargo test"))
.stdout(predicate::str::contains("3 source(s)"))
.stdout(predicate::str::contains("crate_a/Cargo.toml").not())
.stdout(predicate::str::contains("crate_b/Cargo.toml").not());
}
#[test]
fn scan_terminal_dashboard_aggregates_risks_by_default() {
let fixture = ProjectFixture::workspace();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.arg("--style")
.arg("plain")
.arg("--color")
.arg("never")
.assert()
.success()
.stdout(predicate::str::contains("manifest-without-lockfile"))
.stdout(predicate::str::contains("2 finding(s)"))
.stdout(predicate::str::contains("Dependency manifest has entries").not());
}
#[test]
fn scan_terminal_dashboard_details_expand_paths() {
let fixture = ProjectFixture::workspace();
fixture.write(".workflow/rust-ci.yml", "version: '1.0'\n");
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--details")
.arg("--no-unicode")
.arg("--style")
.arg("plain")
.arg("--color")
.arg("never")
.assert()
.success()
.stdout(predicate::str::contains("Details"))
.stdout(predicate::str::contains("VCS root"))
.stdout(predicate::str::contains("License path"))
.stdout(predicate::str::contains("CI path"))
.stdout(predicate::str::contains("Dependency manifest"))
.stdout(predicate::str::contains("Test source"))
.stdout(predicate::str::contains("Risk path"))
.stdout(predicate::str::contains("crate_a/Cargo.toml"))
.stdout(predicate::str::contains("rust-ci.yml"));
}
#[test]
fn scan_terminal_dashboard_respects_width_option() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.arg("--style")
.arg("plain")
.arg("--width")
.arg("44")
.assert()
.success()
.stdout(predicate::str::contains("########## 100%"));
}
#[test]
fn scan_terminal_dashboard_supports_color_control() {
let fixture = ProjectFixture::new();
let mut colored = Command::cargo_bin("projd").expect("binary exists");
colored
.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--color")
.arg("always")
.assert()
.success()
.stdout(predicate::str::contains("\u{1b}["));
let mut plain = Command::cargo_bin("projd").expect("binary exists");
plain
.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--color")
.arg("never")
.assert()
.success()
.stdout(predicate::str::contains("\u{1b}[").not());
}
#[test]
fn scan_terminal_dashboard_uses_semantic_colors() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--color")
.arg("always")
.assert()
.success()
.stdout(predicate::str::contains("\u{1b}[1;36m"))
.stdout(predicate::str::contains("\u{1b}[32mOK\u{1b}[0m"))
.stdout(predicate::str::contains("\u{1b}[34m"))
.stdout(predicate::str::contains("\u{1b}[33mWarn\u{1b}[0m"));
}
#[test]
fn scan_terminal_dashboard_supports_table_style() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--style")
.arg("table")
.arg("--color")
.arg("never")
.assert()
.success()
.stdout(predicate::str::contains("Signal"))
.stdout(predicate::str::contains("Status"))
.stdout(predicate::str::contains("Evidence"))
.stdout(predicate::str::contains("Impact"))
.stdout(predicate::str::contains("Code Statistics"))
.stdout(predicate::str::contains("Comments"))
.stdout(predicate::str::contains("Severity"))
.stdout(predicate::str::contains("Code"))
.stdout(predicate::str::contains("Action"))
.stdout(predicate::str::contains("┌"));
}
#[test]
fn scan_terminal_dashboard_supports_compact_style() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--style")
.arg("compact")
.arg("--color")
.arg("never")
.assert()
.success()
.stdout(predicate::str::contains("Signal"))
.stdout(predicate::str::contains("Status"))
.stdout(predicate::str::contains("Evidence"))
.stdout(predicate::str::contains("┌").not());
}
#[test]
fn scan_terminal_dashboard_supports_plain_style() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--style")
.arg("plain")
.arg("--no-unicode")
.arg("--color")
.arg("never")
.assert()
.success()
.stdout(predicate::str::contains("Health"))
.stdout(predicate::str::contains("Signal").not())
.stdout(predicate::str::contains("┌").not());
}
#[test]
fn scan_terminal_dashboard_supports_cinematic_style() {
let fixture = ProjectFixture::workspace();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--style")
.arg("cinematic")
.arg("--color")
.arg("never")
.assert()
.success()
.stdout(predicate::str::contains("PROJD // PROJECT DIAGNOSTIC"))
.stdout(predicate::str::contains("SIGNAL MATRIX"))
.stdout(predicate::str::contains("LANGUAGE SPECTRUM"))
.stdout(predicate::str::contains("BUILD + DEPENDENCY GRID"))
.stdout(predicate::str::contains("TEST + CI TELEMETRY"))
.stdout(predicate::str::contains("ALERT FEED"))
.stdout(predicate::str::contains("HEALTH ::"))
.stdout(predicate::str::contains("RISK ::"));
}
#[test]
fn tui_help_describes_live_hud() {
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("tui")
.arg("--help")
.assert()
.success()
.stdout(predicate::str::contains(
"Render an interactive live terminal HUD",
))
.stdout(predicate::str::contains("--snapshot"));
}
#[test]
fn tui_snapshot_renders_live_hud_frame() {
let fixture = ProjectFixture::workspace();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("tui")
.arg(fixture.path())
.arg("--snapshot")
.assert()
.success()
.stdout(predicate::str::contains("PROJD // LIVE PROJECT DIAGNOSTIC"))
.stdout(predicate::str::contains("HEALTH ::"))
.stdout(predicate::str::contains("SIGNAL MATRIX"))
.stdout(predicate::str::contains("LANGUAGE SPECTRUM"))
.stdout(predicate::str::contains("BUILD + DEPENDENCY GRID"))
.stdout(predicate::str::contains("TEST + CI TELEMETRY"))
.stdout(predicate::str::contains("ALERT FEED"))
.stdout(predicate::str::contains("q quit r rescan tab focus"));
}
#[test]
fn completion_prints_zsh_script() {
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("completion")
.arg("zsh")
.arg("--print")
.assert()
.success()
.stdout(predicate::str::contains("#compdef projd"))
.stdout(predicate::str::contains("scan"))
.stdout(predicate::str::contains("tui"))
.stdout(predicate::str::contains("--style"))
.stdout(predicate::str::contains("--snapshot"));
}
#[test]
fn completion_prints_bash_script() {
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("completion")
.arg("bash")
.arg("--print")
.assert()
.success()
.stdout(predicate::str::contains("_projd()"))
.stdout(predicate::str::contains("scan"))
.stdout(predicate::str::contains("tui"))
.stdout(predicate::str::contains("--format"))
.stdout(predicate::str::contains("--snapshot"));
}
#[test]
fn completion_installs_zsh_script_under_home() {
let fixture = ProjectFixture::empty();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.env("HOME", fixture.path())
.arg("completion")
.arg("zsh")
.assert()
.success()
.stdout(predicate::str::contains("Installed zsh completions"));
let content =
fs::read_to_string(fixture.path().join(".zfunc/_projd")).expect("read zsh completion");
assert!(content.contains("#compdef projd"));
assert!(content.contains("--snapshot"));
}
#[test]
fn completion_installs_bash_script_under_home() {
let fixture = ProjectFixture::empty();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.env("HOME", fixture.path())
.arg("completion")
.arg("bash")
.assert()
.success()
.stdout(predicate::str::contains("Installed bash completions"));
let content = fs::read_to_string(
fixture
.path()
.join(".local/share/bash-completion/completions/projd"),
)
.expect("read bash completion");
assert!(content.contains("_projd()"));
assert!(content.contains("--format"));
}
#[test]
fn scan_terminal_dashboard_cinematic_respects_ascii_output() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--style")
.arg("cinematic")
.arg("--no-unicode")
.arg("--color")
.arg("never")
.assert()
.success()
.stdout(predicate::str::contains("+"))
.stdout(predicate::str::contains("> SIGNAL MATRIX"))
.stdout(predicate::str::contains("â•”").not())
.stdout(predicate::str::contains("â–¸").not());
}
#[test]
fn scan_writes_markdown_output_file() {
let fixture = ProjectFixture::new();
let output = fixture.path().join("report.md");
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--output")
.arg(&output)
.assert()
.success()
.stdout(predicate::str::is_empty());
let content = fs::read_to_string(output).expect("read report");
assert!(content.contains("# Project Report"));
}
#[test]
fn scan_infers_json_output_from_extension() {
let fixture = ProjectFixture::new();
let output = fixture.path().join("scan.json");
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--output")
.arg(&output)
.assert()
.success()
.stdout(predicate::str::is_empty());
let content = fs::read_to_string(output).expect("read json");
assert!(content.contains("\"identity\""));
}
#[test]
fn scan_refuses_to_overwrite_existing_output_file() {
let fixture = ProjectFixture::new();
let output = fixture.path().join("report.md");
fs::write(&output, "existing").expect("write existing output");
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--output")
.arg(&output)
.assert()
.failure()
.stderr(predicate::str::contains("refusing to overwrite"));
assert_eq!(fs::read_to_string(output).expect("read output"), "existing");
}
#[test]
fn scan_overwrites_existing_output_file_when_requested() {
let fixture = ProjectFixture::new();
let output = fixture.path().join("report.md");
fs::write(&output, "existing").expect("write existing output");
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--output")
.arg(&output)
.arg("--overwrite")
.assert()
.success();
let content = fs::read_to_string(output).expect("read output");
assert!(content.contains("# Project Report"));
assert_ne!(content, "existing");
}
#[test]
fn explicit_format_overrides_output_extension() {
let fixture = ProjectFixture::new();
let output = fixture.path().join("report.txt");
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("json")
.arg("--output")
.arg(&output)
.assert()
.success();
let content = fs::read_to_string(output).expect("read output");
assert!(content.contains("\"identity\""));
}
#[test]
fn discover_finds_single_cargo_root() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("discover")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.assert()
.success()
.stdout(
predicate::str::contains("Projd Discover Report")
.and(predicate::str::contains("Found 1 project root"))
.and(predicate::str::contains("cargo_package")),
);
}
#[test]
fn discover_separates_side_by_side_repos() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
for repo in ["alpha", "beta"] {
let dir = temp_dir.path().join(repo);
fs::create_dir_all(&dir).expect("create repo dir");
fs::create_dir(dir.join(".git")).expect("create .git");
fs::write(
dir.join("Cargo.toml"),
"[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
)
.expect("write manifest");
}
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("discover")
.arg(temp_dir.path())
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.assert()
.success()
.stdout(
predicate::str::contains("Found 2 project root")
.and(predicate::str::contains("alpha"))
.and(predicate::str::contains("beta")),
);
}
#[test]
fn discover_workspace_default_does_not_expand() {
let fixture = ProjectFixture::workspace();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("discover")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.assert()
.success()
.stdout(
predicate::str::contains("Found 1 project root")
.and(predicate::str::contains("cargo_workspace")),
);
}
#[test]
fn discover_workspace_expand_lists_members() {
let fixture = ProjectFixture::workspace();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("discover")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.arg("--expand-workspaces")
.assert()
.success()
.stdout(
predicate::str::contains("Found 3 project root")
.and(predicate::str::contains("crate_a"))
.and(predicate::str::contains("crate_b")),
);
}
#[test]
fn discover_json_output_contains_summary() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("discover")
.arg(fixture.path())
.arg("--format")
.arg("json")
.assert()
.success()
.stdout(
predicate::str::contains("\"total\": 1")
.and(predicate::str::contains("\"cargo_package\""))
.and(predicate::str::contains("\"relative_path\"")),
);
}
#[test]
fn discover_rejects_unknown_kind_token() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("discover")
.arg(fixture.path())
.arg("--include-kind")
.arg("not-a-real-kind")
.assert()
.failure()
.stderr(predicate::str::contains("unknown root kind"));
}
#[test]
fn scan_recursive_terminal_shows_summary_and_per_root_rows() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
for repo in ["alpha", "beta"] {
let dir = temp_dir.path().join(repo);
fs::create_dir_all(&dir).expect("create dir");
fs::write(
dir.join("Cargo.toml"),
"[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
)
.expect("write manifest");
fs::create_dir(dir.join("src")).expect("src");
fs::write(dir.join("src/lib.rs"), "pub fn h() {}\n").expect("src/lib.rs");
fs::write(dir.join("README.md"), "# x\n").expect("readme");
fs::write(dir.join("LICENSE"), "MIT\n").expect("license");
}
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(temp_dir.path())
.arg("--recursive")
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.arg("--color")
.arg("never")
.assert()
.success()
.stdout(
predicate::str::contains("Projd Recursive Scan Report")
.and(predicate::str::contains("Found 2 project root"))
.and(predicate::str::contains("alpha"))
.and(predicate::str::contains("beta"))
.and(predicate::str::contains("Top Risk")),
);
}
#[test]
fn scan_recursive_json_emits_roots_array_and_summary() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
for repo in ["alpha", "beta"] {
let dir = temp_dir.path().join(repo);
fs::create_dir_all(&dir).expect("create dir");
fs::write(
dir.join("Cargo.toml"),
"[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
)
.expect("write manifest");
}
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(temp_dir.path())
.arg("--recursive")
.arg("--format")
.arg("json")
.assert()
.success()
.stdout(
predicate::str::contains("\"total\": 2")
.and(predicate::str::contains("\"roots\":"))
.and(predicate::str::contains("\"by_kind\"")),
);
}
#[test]
fn scan_recursive_rejects_cinematic_style() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--recursive")
.arg("--style")
.arg("cinematic")
.assert()
.failure()
.stderr(predicate::str::contains(
"--recursive is not supported with --style cinematic",
));
}
#[test]
fn scan_recursive_requires_recursive_for_extra_flags() {
let fixture = ProjectFixture::new();
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--max-depth")
.arg("4")
.assert()
.failure()
.stderr(predicate::str::contains("--recursive"));
}
fn git_available() -> bool {
std::process::Command::new("git")
.arg("--version")
.output()
.is_ok_and(|output| output.status.success())
}
fn init_test_git_repo(dir: &std::path::Path) {
use std::process::Command;
let envs = [
("GIT_AUTHOR_NAME", "Test User"),
("GIT_AUTHOR_EMAIL", "test@example.com"),
("GIT_COMMITTER_NAME", "Test User"),
("GIT_COMMITTER_EMAIL", "test@example.com"),
];
for args in [
&["init", "-q", "--initial-branch=main"][..],
&["config", "user.email", "test@example.com"][..],
&["config", "user.name", "Test User"][..],
&["add", "."][..],
&["commit", "-q", "-m", "initial commit"][..],
] {
let output = Command::new("git")
.args(args)
.current_dir(dir)
.envs(envs.iter().copied())
.output()
.expect("run git");
assert!(
output.status.success(),
"git {:?} failed: {}",
args,
String::from_utf8_lossy(&output.stderr)
);
}
}
#[test]
fn scan_terminal_shows_activity_row_when_git_repo() {
if !git_available() {
return;
}
let fixture = ProjectFixture::new();
init_test_git_repo(fixture.path());
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.arg("--color")
.arg("never")
.assert()
.success()
.stdout(
predicate::str::contains("Activity")
.and(predicate::str::contains("day(s) ago"))
.and(predicate::str::contains("Contributors")),
);
}
#[test]
fn scan_recursive_terminal_has_last_commit_column() {
let temp_dir = tempfile::tempdir().expect("create temp dir");
for repo in ["alpha", "beta"] {
let dir = temp_dir.path().join(repo);
fs::create_dir_all(&dir).expect("create dir");
fs::write(
dir.join("Cargo.toml"),
"[package]\nname=\"x\"\nversion=\"0.1.0\"\n",
)
.expect("write manifest");
}
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(temp_dir.path())
.arg("--recursive")
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.arg("--color")
.arg("never")
.assert()
.success()
.stdout(predicate::str::contains("Last Commit"));
}
#[test]
fn scan_detailed_contributors_lists_author() {
if !git_available() {
return;
}
let fixture = ProjectFixture::new();
init_test_git_repo(fixture.path());
let mut cmd = Command::cargo_bin("projd").expect("binary exists");
cmd.arg("scan")
.arg(fixture.path())
.arg("--format")
.arg("terminal")
.arg("--no-unicode")
.arg("--color")
.arg("never")
.arg("--details")
.arg("--style")
.arg("plain")
.arg("--detailed-contributors")
.assert()
.success()
.stdout(predicate::str::contains("Test User"));
}
struct ProjectFixture {
temp_dir: TempDir,
}
impl ProjectFixture {
fn empty() -> Self {
let temp_dir = tempfile::tempdir().expect("create temp dir");
Self { temp_dir }
}
fn new() -> Self {
let temp_dir = tempfile::tempdir().expect("create temp dir");
fs::write(
temp_dir.path().join("Cargo.toml"),
"[package]\nname = \"fixture\"\nversion = \"0.1.0\"\n",
)
.expect("write manifest");
fs::create_dir(temp_dir.path().join("src")).expect("create src dir");
fs::write(temp_dir.path().join("src/lib.rs"), "pub fn fixture() {}\n")
.expect("write rust source");
fs::write(temp_dir.path().join("README.md"), "# Fixture\n").expect("write readme");
fs::write(temp_dir.path().join("LICENSE"), "MIT\n").expect("write license");
Self { temp_dir }
}
fn workspace() -> Self {
let temp_dir = tempfile::tempdir().expect("create temp dir");
fs::write(
temp_dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"crate_a\", \"crate_b\"]\n",
)
.expect("write workspace manifest");
fs::write(temp_dir.path().join("README.md"), "# Workspace\n").expect("write readme");
fs::write(temp_dir.path().join("LICENSE"), "MIT\n").expect("write license");
for crate_name in ["crate_a", "crate_b"] {
let crate_dir = temp_dir.path().join(crate_name);
fs::create_dir(&crate_dir).expect("create crate dir");
fs::create_dir(crate_dir.join("src")).expect("create src dir");
fs::write(
crate_dir.join("Cargo.toml"),
format!(
"[package]\nname = \"{crate_name}\"\nversion = \"0.1.0\"\n[dependencies]\nanyhow = \"1\"\n"
),
)
.expect("write crate manifest");
fs::write(crate_dir.join("src/lib.rs"), "pub fn fixture() {}\n")
.expect("write rust source");
}
Self { temp_dir }
}
fn path(&self) -> &std::path::Path {
self.temp_dir.path()
}
fn write(&self, relative: &str, content: &str) {
let path = self.path().join(relative);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create fixture parent");
}
fs::write(path, content).expect("write fixture file");
}
}