use assert_cmd::Command;
use predicates::prelude::*;
fn grapha() -> Command {
Command::cargo_bin("grapha").unwrap()
}
fn index_temp_project(source: &str) -> tempfile::TempDir {
let dir = tempfile::tempdir().unwrap();
let store_dir = dir.path().join(".grapha");
std::fs::write(dir.path().join("main.rs"), source).unwrap();
grapha()
.args([
"index",
dir.path().to_str().unwrap(),
"--store-dir",
store_dir.to_str().unwrap(),
])
.assert()
.success();
dir
}
#[test]
fn analyze_outputs_dataflow_fields() {
let dir = tempfile::tempdir().unwrap();
std::fs::write(
dir.path().join("main.rs"),
"fn main() { save_data(); }\nfn save_data() {}\n",
)
.unwrap();
grapha()
.args(["analyze", dir.path().to_str().unwrap()])
.assert()
.success()
.stdout(predicate::str::contains("nodes"))
.stdout(predicate::str::contains("edges"));
}
#[test]
fn index_and_entries_works() {
let dir = tempfile::tempdir().unwrap();
let store_dir = dir.path().join(".grapha");
std::fs::write(
dir.path().join("main.rs"),
"fn main() { helper(); }\nfn helper() {}\n",
)
.unwrap();
grapha()
.args([
"index",
dir.path().to_str().unwrap(),
"--store-dir",
store_dir.to_str().unwrap(),
])
.assert()
.success();
grapha()
.args(["flow", "entries", "-p", dir.path().to_str().unwrap()])
.assert()
.success()
.stdout(predicate::str::contains("entries"));
}
#[test]
fn trace_command_works() {
let dir = index_temp_project("fn main() { helper(); }\nfn helper() {}\n");
grapha()
.args(["flow", "trace", "main", "-p", dir.path().to_str().unwrap()])
.assert()
.success()
.stdout(predicate::str::contains("entry"));
}
#[test]
fn reverse_command_works() {
let dir = index_temp_project("fn main() { helper(); }\nfn helper() {}\n");
grapha()
.args([
"flow",
"trace",
"helper",
"--direction",
"reverse",
"-p",
dir.path().to_str().unwrap(),
])
.assert()
.success()
.stdout(predicate::str::contains("symbol"));
}
#[test]
fn reverse_trace_respects_depth_limit() {
let dir = index_temp_project("fn main() { mid(); }\nfn mid() { helper(); }\nfn helper() {}\n");
grapha()
.args([
"flow",
"trace",
"helper",
"--direction",
"reverse",
"--depth",
"1",
"-p",
dir.path().to_str().unwrap(),
])
.assert()
.success()
.stdout(predicate::str::contains("\"total_entries\": 0"));
grapha()
.args([
"flow",
"trace",
"helper",
"--direction",
"reverse",
"--depth",
"2",
"-p",
dir.path().to_str().unwrap(),
])
.assert()
.success()
.stdout(predicate::str::contains("\"total_entries\": 1"));
}
#[test]
fn impact_command_defaults_to_json() {
let dir = index_temp_project("fn main() { helper(); }\nfn helper() {}\n");
grapha()
.args([
"symbol",
"impact",
"helper",
"-p",
dir.path().to_str().unwrap(),
])
.assert()
.success()
.stdout(predicate::str::contains("\"source\""))
.stdout(predicate::str::contains("\"total_affected\": 1"));
}
#[test]
fn context_tree_format_works() {
let dir = index_temp_project("fn main() { helper(); }\nfn helper() {}\n");
grapha()
.args([
"symbol",
"context",
"helper",
"-p",
dir.path().to_str().unwrap(),
"--format",
"tree",
])
.assert()
.success()
.stdout(predicate::str::contains("helper [function] (main.rs)"))
.stdout(predicate::str::contains("callers (1)"))
.stdout(predicate::str::contains("main [function] (main.rs)"))
.stdout(predicate::str::contains("└──"));
}
#[test]
fn entries_tree_format_works() {
let dir = index_temp_project("fn main() { helper(); }\nfn helper() {}\n");
grapha()
.args([
"flow",
"entries",
"-p",
dir.path().to_str().unwrap(),
"--format",
"tree",
])
.assert()
.success()
.stdout(predicate::str::contains("entry points (1)"))
.stdout(predicate::str::contains("main [function] (main.rs)"))
.stdout(predicate::str::contains("└──"));
}
#[test]
fn trace_tree_format_works() {
let dir = index_temp_project("fn main() { helper(); }\nfn helper() {}\n");
grapha()
.args([
"flow",
"trace",
"main",
"-p",
dir.path().to_str().unwrap(),
"--format",
"tree",
])
.assert()
.success()
.stdout(predicate::str::contains("main [function] (main.rs)"))
.stdout(predicate::str::contains(
"summary: flows=0, reads=0, writes=0, async_crossings=0",
))
.stdout(predicate::str::contains("flows (0)"));
}
#[test]
fn reverse_tree_format_works() {
let dir = index_temp_project("fn main() { helper(); }\nfn helper() {}\n");
grapha()
.args([
"flow",
"trace",
"helper",
"--direction",
"reverse",
"-p",
dir.path().to_str().unwrap(),
"--format",
"tree",
])
.assert()
.success()
.stdout(predicate::str::contains("helper [function] (main.rs)"))
.stdout(predicate::str::contains("affected entries (1)"))
.stdout(predicate::str::contains(
"main [entry] [function] (main.rs)",
))
.stdout(predicate::str::contains("└──"))
.stdout(predicate::str::contains("\"symbol\"").not());
}
#[test]
fn impact_tree_format_works() {
let dir = index_temp_project("fn main() { helper(); }\nfn helper() {}\n");
grapha()
.args([
"symbol",
"impact",
"helper",
"-p",
dir.path().to_str().unwrap(),
"--format",
"tree",
])
.assert()
.success()
.stdout(predicate::str::contains("helper [function] (main.rs)"))
.stdout(predicate::str::contains(
"summary: depth_1=1, depth_2=0, depth_3_plus=0, total=1",
))
.stdout(predicate::str::contains("dependents (1)"))
.stdout(predicate::str::contains("main [function] (main.rs)"))
.stdout(predicate::str::contains("└──"))
.stdout(predicate::str::contains("\"source\"").not());
}
#[test]
fn context_tree_for_swiftui_body_shows_structure() {
let dir = tempfile::tempdir().unwrap();
let store_dir = dir.path().join(".grapha");
std::fs::write(
dir.path().join("ContentView.swift"),
r#"
import SwiftUI
struct ContentView: View {
var body: some View {
VStack {
Text("Hello")
}
}
}
"#,
)
.unwrap();
grapha()
.args([
"index",
dir.path().to_str().unwrap(),
"--store-dir",
store_dir.to_str().unwrap(),
])
.assert()
.success();
grapha()
.args([
"symbol",
"context",
"body",
"-p",
dir.path().to_str().unwrap(),
"--format",
"tree",
])
.assert()
.success()
.stdout(predicate::str::contains(
"body [property] (ContentView.swift)",
))
.stdout(predicate::str::contains("contains (1)"))
.stdout(predicate::str::contains(
"VStack [view] (ContentView.swift)",
))
.stdout(predicate::str::contains("contained_by (1)"))
.stdout(predicate::str::contains(
"ContentView [struct] (ContentView.swift)",
));
}
#[test]
fn help_output_lists_new_command_tree() {
grapha()
.args(["--help"])
.assert()
.success()
.stdout(predicate::str::contains("symbol"))
.stdout(predicate::str::contains("flow"))
.stdout(predicate::str::contains("l10n"))
.stdout(predicate::str::contains("repo"))
.stdout(predicate::str::contains("reverse").not());
grapha()
.args(["symbol", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("search"))
.stdout(predicate::str::contains("context"))
.stdout(predicate::str::contains("impact"));
grapha()
.args(["flow", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("trace"))
.stdout(predicate::str::contains("graph"))
.stdout(predicate::str::contains("entries"));
grapha()
.args(["l10n", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("symbol"))
.stdout(predicate::str::contains("usages"));
grapha()
.args(["repo", "--help"])
.assert()
.success()
.stdout(predicate::str::contains("changes"));
}
#[test]
fn removed_top_level_commands_fail() {
grapha()
.args(["reverse"])
.assert()
.failure()
.stderr(predicate::str::contains("unrecognized subcommand"));
grapha()
.args(["localize"])
.assert()
.failure()
.stderr(predicate::str::contains("unrecognized subcommand"));
grapha()
.args(["dataflow"])
.assert()
.failure()
.stderr(predicate::str::contains("unrecognized subcommand"));
}