use std::fs;
use std::path::{Path, PathBuf};
use std::process::Command;
use tempfile::TempDir;
const PROJECT: &str = "demo_app";
const DEPRECATED_KRAB_CORE_ALIASES: &[&str] = &["grpc", "db"];
struct Run {
args: String,
code: Option<i32>,
success: bool,
stdout: String,
stderr: String,
}
impl Run {
fn report(&self) -> String {
format!(
"`krab {}` exited with {:?}\n--- stdout ---\n{}\n--- stderr ---\n{}",
self.args, self.code, self.stdout, self.stderr
)
}
}
fn krab_command(cwd: &Path) -> Command {
let mut command = Command::new(env!("CARGO_BIN_EXE_krab"));
command
.current_dir(cwd)
.env("KRAB_AUTH_MODE", "static")
.env("KRAB_ENVIRONMENT", "dev")
.env_remove("KRAB_OIDC_ISSUER")
.env_remove("KRAB_OIDC_AUDIENCE")
.env_remove("KRAB_RUNTIME_TOPOLOGY")
.env_remove("KRAB_RUNTIME_ENDPOINTS_JSON");
command
}
fn run(cwd: &Path, args: &[&str]) -> Run {
let output = krab_command(cwd)
.args(args)
.output()
.unwrap_or_else(|err| panic!("failed to spawn `krab {}`: {err}", args.join(" ")));
Run {
args: args.join(" "),
code: output.status.code(),
success: output.status.success(),
stdout: String::from_utf8_lossy(&output.stdout).into_owned(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
}
}
fn run_ok(cwd: &Path, args: &[&str]) -> Run {
let run = run(cwd, args);
assert!(run.success, "{}", run.report());
run
}
fn scaffold(template: &str) -> (TempDir, PathBuf) {
let temp = TempDir::new().expect("tempdir");
let created = run_ok(temp.path(), &["new", PROJECT, "--template", template]);
assert!(
created.stdout.contains("created successfully"),
"{}",
created.report()
);
let root = temp.path().join(PROJECT);
for relative in ["Cargo.toml", "krab.toml", "src/main.rs"] {
assert!(
root.join(relative).is_file(),
"`krab new` did not write {relative}:\n{}",
created.report()
);
}
(temp, root)
}
fn read(root: &Path, relative: &str) -> String {
fs::read_to_string(root.join(relative))
.unwrap_or_else(|err| panic!("{relative} unreadable: {err}"))
}
fn declaration_lines(source: &str, decl: &str) -> usize {
source.lines().filter(|line| line.trim() == decl).count()
}
#[derive(Debug)]
struct DoctorCheck {
name: String,
level: String,
details: Vec<String>,
}
impl DoctorCheck {
fn has_detail_containing(&self, needle: &str) -> bool {
self.details.iter().any(|detail| detail.contains(needle))
}
}
fn parse_doctor_report(stdout: &str) -> Vec<DoctorCheck> {
let mut checks: Vec<DoctorCheck> = Vec::new();
for line in stdout.lines() {
let trimmed = line.trim_end();
if let Some(rest) = trimmed.strip_prefix('[') {
if let Some((level, name)) = rest.split_once("] ") {
checks.push(DoctorCheck {
name: name.trim().to_string(),
level: level.to_string(),
details: Vec::new(),
});
continue;
}
}
if let Some(detail) = trimmed.trim_start().strip_prefix("- ") {
if let Some(current) = checks.last_mut() {
current.details.push(detail.to_string());
}
}
}
checks
}
fn doctor_check<'a>(checks: &'a [DoctorCheck], name: &str) -> &'a DoctorCheck {
checks
.iter()
.find(|check| check.name == name)
.unwrap_or_else(|| panic!("no `{name}` check in the doctor report: {checks:#?}"))
}
fn krab_core_features(manifest: &str) -> Vec<String> {
let line = manifest
.lines()
.find(|line| line.trim_start().starts_with("krab_core ="))
.unwrap_or_else(|| panic!("generated manifest declares no krab_core:\n{manifest}"));
let opened = line
.find("features = [")
.map(|at| at + "features = [".len())
.unwrap_or_else(|| panic!("krab_core dependency carries no feature list: {line}"));
let rest = &line[opened..];
let closed = rest
.find(']')
.unwrap_or_else(|| panic!("unterminated krab_core feature list: {line}"));
rest[..closed]
.split(',')
.map(|feature| feature.trim().trim_matches('"').to_string())
.filter(|feature| !feature.is_empty())
.collect()
}
fn assert_no_deprecated_aliases(features: &[String], context: &str) {
for feature in features {
assert!(
!DEPRECATED_KRAB_CORE_ALIASES.contains(&feature.as_str()),
"{context} requests the deprecated krab_core alias {feature:?}; scaffold against \
the canonical feature so removing the alias cannot break generated projects. \
Emitted: {features:?}"
);
}
}
#[test]
fn doctor_strict_passes_on_an_untouched_scaffold() {
let (_temp, root) = scaffold("default");
let strict = run(&root, &["doctor", "--diagnostics", "--strict"]);
assert!(
strict.success,
"a freshly generated project must pass its own strict gate: {}",
strict.report()
);
}
#[test]
fn doctor_in_a_generated_project_passes_and_marks_framework_only_checks_skipped() {
let (_temp, root) = scaffold("default");
let doctor = run_ok(&root, &["doctor", "--diagnostics"]);
let checks = parse_doctor_report(&doctor.stdout);
let names: Vec<&str> = checks.iter().map(|check| check.name.as_str()).collect();
assert_eq!(
names,
vec![
"project-model",
"service-config",
"environment-policy",
"topology-boundaries",
],
"{}",
doctor.report()
);
let topology = doctor_check(&checks, "topology-boundaries");
assert_eq!(topology.level, "SKIP", "{}", doctor.report());
assert!(
topology.has_detail_containing("contract-payload-derives")
&& topology.has_detail_containing("only exists in a Krab framework checkout"),
"the skip must name the check and why it does not apply: {}",
doctor.report()
);
assert!(
topology.has_detail_containing("service-source-scan"),
"a generated project has no services/ directory, so that scan is skipped too: {}",
doctor.report()
);
assert!(
!doctor.stdout.contains("Failed reading") && !doctor.stderr.contains("Failed reading"),
"{}",
doctor.report()
);
assert!(
!doctor.stdout.contains("check could not run"),
"{}",
doctor.report()
);
assert!(!doctor.stdout.contains("[FAIL]"), "{}", doctor.report());
assert!(
doctor.stdout.contains("check(s) skipped as not applicable"),
"the summary line hides that part of the suite never ran: {}",
doctor.report()
);
let project_model = doctor_check(&checks, "project-model");
assert_eq!(project_model.level, "OK", "{}", doctor.report());
assert!(
project_model.has_detail_containing(&format!("frontend_bin={PROJECT}")),
"project-model must read the generated krab.toml, not the workspace default: {}",
doctor.report()
);
let service_config = doctor_check(&checks, "service-config");
assert_eq!(service_config.level, "SKIP", "{}", doctor.report());
assert!(
service_config.has_detail_containing("single-service project"),
"a skipped service-config must explain itself: {}",
doctor.report()
);
assert_eq!(
doctor_check(&checks, "environment-policy").level,
"OK",
"{}",
doctor.report()
);
}
#[test]
fn doctor_in_the_framework_checkout_still_runs_the_contract_check() {
let repo_root = Path::new(env!("CARGO_MANIFEST_DIR")).join("../../..");
let contract = repo_root.join("crates/framework/krab_core/src/service_contract.rs");
if !contract.is_file() {
eprintln!(
"{} is absent; skipping the framework-checkout contrast",
contract.display()
);
return;
}
let doctor = run(&repo_root, &["doctor", "--diagnostics"]);
let checks = parse_doctor_report(&doctor.stdout);
let topology = doctor_check(&checks, "topology-boundaries");
assert!(
topology.has_detail_containing("contract_path=")
&& topology.has_detail_containing("service_contract.rs"),
"the contract payload check must actually run here: {}",
doctor.report()
);
assert!(
!topology.has_detail_containing("skipped contract-payload-derives"),
"the framework checkout has the contract file; skipping it would hide real violations: {}",
doctor.report()
);
assert!(
topology.has_detail_containing("checked_rust_files="),
"services/ exists here, so the source scan must run: {}",
doctor.report()
);
}
#[test]
fn topology_doctor_in_a_generated_project_passes_and_names_the_skipped_checks() {
let (_temp, root) = scaffold("default");
let topology = run_ok(&root, &["topology", "doctor", "--diagnostics"]);
assert!(
topology
.stdout
.contains("skipped (not applicable to this project):"),
"{}",
topology.report()
);
for check in ["service-source-scan", "contract-payload-derives"] {
assert!(
topology.stdout.contains(check),
"`{check}` is not reported as skipped: {}",
topology.report()
);
}
assert!(
topology
.stdout
.contains("topology doctor passed (2 check(s) skipped as not applicable)"),
"a pass must be qualified by how much of the suite never ran: {}",
topology.report()
);
assert!(
topology
.stdout
.contains("checked orchestrator service health/restart policy in krab.toml"),
"{}",
topology.report()
);
assert!(
!topology.stdout.contains("orchestrator-service-config:"),
"krab.toml is present, so that check must not be listed as skipped: {}",
topology.report()
);
}
fn assert_second_run_keeps_hand_written_content(args: &[&str], relative: &str) {
let (_temp, root) = scaffold("default");
run_ok(&root, args);
let generated = root.join(relative);
assert!(
generated.is_file(),
"{relative} was not generated by `krab {}`",
args.join(" ")
);
let sentinel = format!("// hand written, must survive `krab {}`\n", args.join(" "));
fs::write(&generated, &sentinel).expect("simulate a user edit");
let main_before = read(&root, "src/main.rs");
let second = run_ok(&root, args);
assert_eq!(
fs::read_to_string(&generated).expect("file still readable"),
sentinel,
"the second `krab {}` overwrote hand-written content in {relative}:\n{}",
args.join(" "),
second.report()
);
assert!(
second
.stdout
.contains("kept as is, nothing was overwritten"),
"a re-run must say it kept the file, not claim it created one: {}",
second.report()
);
assert!(
!second.stdout.contains("created at"),
"the second run reports a creation that did not happen: {}",
second.report()
);
assert_eq!(
read(&root, "src/main.rs"),
main_before,
"a re-run must not touch src/main.rs again: {}",
second.report()
);
}
#[test]
fn gen_component_run_twice_keeps_the_users_file() {
assert_second_run_keeps_hand_written_content(
&["gen", "component", "Counter"],
"src/components/counter.rs",
);
}
#[test]
fn gen_route_run_twice_keeps_the_users_file() {
assert_second_run_keeps_hand_written_content(&["gen", "route", "About"], "src/routes/about.rs");
}
#[test]
fn gen_server_function_run_twice_keeps_the_users_file() {
assert_second_run_keeps_hand_written_content(
&["gen", "server-function", "load_user"],
"src/server_functions/load_user.rs",
);
}
#[test]
fn a_second_generated_route_extends_the_existing_index() {
let (_temp, root) = scaffold("default");
run_ok(&root, &["gen", "route", "About"]);
run_ok(&root, &["gen", "route", "Contact"]);
let mod_rs = read(&root, "src/routes/mod.rs");
for decl in ["pub mod about;", "pub mod contact;"] {
assert_eq!(
declaration_lines(&mod_rs, decl),
1,
"expected exactly one `{decl}`:\n{mod_rs}"
);
}
assert!(
mod_rs.contains("router = router.route(\"/contact\", get(contact::handler));"),
"{mod_rs}"
);
let main_rs = read(&root, "src/main.rs");
assert_eq!(
main_rs.matches("routes::router()").count(),
1,
"the router is merged once regardless of route count:\n{main_rs}"
);
}
#[test]
fn generated_modules_are_declared_in_main_and_the_route_is_registered() {
let (_temp, root) = scaffold("default");
run_ok(&root, &["gen", "component", "Counter"]);
run_ok(&root, &["gen", "route", "About"]);
run_ok(&root, &["gen", "server-function", "load_user"]);
let main_rs = read(&root, "src/main.rs");
for decl in ["mod components;", "mod routes;", "mod server_functions;"] {
assert_eq!(
declaration_lines(&main_rs, decl),
1,
"src/main.rs must declare `{decl}` exactly once, or the generated file is never \
compiled:\n{main_rs}"
);
}
let merge_at = main_rs
.find("app.merge(routes::router())")
.unwrap_or_else(|| panic!("the routes router is never merged into `app`:\n{main_rs}"));
let router_built_at = main_rs
.find("Router::new()")
.unwrap_or_else(|| panic!("scaffold no longer builds a Router:\n{main_rs}"));
let served_at = main_rs
.find("axum::serve(listener, app)")
.unwrap_or_else(|| panic!("scaffold never serves `app`:\n{main_rs}"));
assert!(
router_built_at < merge_at && merge_at < served_at,
"the merge is outside the window where it type-checks:\n{main_rs}"
);
let routes_mod = read(&root, "src/routes/mod.rs");
assert!(routes_mod.contains("pub mod about;"), "{routes_mod}");
assert!(
routes_mod.contains("router = router.route(\"/about\", get(about::handler));"),
"{routes_mod}"
);
for (relative, decl) in [
("src/components/mod.rs", "pub mod counter;"),
("src/server_functions/mod.rs", "pub mod load_user;"),
] {
let index = read(&root, relative);
assert!(index.contains(decl), "{relative}:\n{index}");
}
let route_rs = read(&root, "src/routes/about.rs");
assert!(
route_rs.contains("pub async fn handler()"),
"the index registers `about::handler`, which the route module must export:\n{route_rs}"
);
}
#[test]
fn gen_service_grpc_emits_the_canonical_feature_not_the_deprecated_alias() {
let temp = TempDir::new().expect("tempdir");
let generated = run_ok(
temp.path(),
&["gen", "service", "payments_api", "--type", "grpc"],
);
assert!(
generated.stdout.contains("created successfully"),
"{}",
generated.report()
);
let manifest = read(temp.path(), "payments_api/Cargo.toml");
let features = krab_core_features(&manifest);
assert_eq!(features, ["grpc-semantics"], "{manifest}");
assert_no_deprecated_aliases(&features, "`krab gen service --type grpc`");
}
#[test]
fn gen_service_emits_only_features_krab_core_declares() {
for (protocol, expected) in [("rest", "rest"), ("graphql", "graphql"), ("rpc", "rest")] {
let temp = TempDir::new().expect("tempdir");
let name = format!("svc_{protocol}");
run_ok(
temp.path(),
&["gen", "service", name.as_str(), "--type", protocol],
);
let manifest = read(temp.path(), &format!("{name}/Cargo.toml"));
let features = krab_core_features(&manifest);
assert_eq!(features, [expected], "--type {protocol}:\n{manifest}");
assert_no_deprecated_aliases(&features, &format!("`krab gen service --type {protocol}`"));
}
}
#[test]
fn the_saas_template_requests_db_postgres_not_the_deprecated_db_alias() {
let (_temp, root) = scaffold("saas");
let manifest = read(&root, "Cargo.toml");
let mut features = krab_core_features(&manifest);
features.sort();
assert_eq!(features, ["db-postgres", "rest"], "{manifest}");
assert_no_deprecated_aliases(&features, "`krab new --template saas`");
}
#[test]
fn version_flag_reports_the_binary_name_and_crate_version() {
let temp = TempDir::new().expect("tempdir");
let version = run_ok(temp.path(), &["--version"]);
assert_eq!(
version.stdout.trim(),
format!("krab {}", env!("CARGO_PKG_VERSION")),
"{}",
version.report()
);
}