use std::fs;
use std::path::Path;
use std::process::{Command, Output};
use tempfile::TempDir;
use crate::common::run_autumn;
const fn autumn_bin() -> &'static str {
env!("CARGO_BIN_EXE_autumn")
}
fn project(files: &[(&str, &str)]) -> TempDir {
let dir = tempfile::tempdir().expect("create temp project dir");
fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\n\n[package]\nname = \"edgeapp\"\nversion = \"0.1.0\"\nedition = \"2021\"\n",
)
.expect("write Cargo.toml");
for (rel, contents) in files {
let path = dir.path().join(rel);
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).expect("create source dir");
}
fs::write(path, contents).expect("write source");
}
dir
}
fn project_with_manifest(manifest: &str, files: &[(&str, &str)]) -> TempDir {
let dir = project(files);
fs::write(dir.path().join("Cargo.toml"), manifest).expect("overwrite Cargo.toml");
dir
}
fn doctor_json(root: &Path) -> (serde_json::Value, Option<i32>) {
let empty_path = tempfile::tempdir().expect("create empty PATH dir");
let out: Output = Command::new(autumn_bin())
.args(["doctor", "--json"])
.current_dir(root)
.env("PATH", empty_path.path())
.output()
.expect("failed to run autumn doctor");
let stdout = String::from_utf8_lossy(&out.stdout).into_owned();
let json_str = stdout
.find('{')
.map_or(stdout.as_str(), |idx| &stdout[idx..]);
let report = serde_json::from_str(json_str).unwrap_or_else(|e| {
panic!(
"`autumn doctor --json` must emit parseable JSON: {e}\nstdout:\n{stdout}\nstderr:\n{}",
String::from_utf8_lossy(&out.stderr)
)
});
(report, out.status.code())
}
fn check<'a>(report: &'a serde_json::Value, name: &str) -> &'a serde_json::Value {
report["checks"]
.as_array()
.expect("report carries a checks array")
.iter()
.find(|c| c["name"] == name)
.unwrap_or_else(|| panic!("doctor must emit a `{name}` check; got {report}"))
}
const GUARDED_EDGE_APP: &str = r#"
use autumn_web::prelude::*;
#[get("/dashboard")]
#[edge]
#[secured]
pub async fn dashboard() -> &'static str {
"private"
}
pub fn edge_route_list() -> Vec<EdgeRoute> {
edge_routes![dashboard]
}
"#;
const UNREGISTERED_EDGE_APP: &str = r#"
use autumn_web::prelude::*;
#[get("/greet")]
#[edge]
pub async fn greet() -> &'static str {
"hello"
}
"#;
const PLAIN_APP: &str = r#"
use autumn_web::prelude::*;
#[get("/")]
pub async fn home() -> &'static str {
"hello"
}
"#;
const REGISTERED_EDGE_APP: &str = r#"
use autumn_web::prelude::*;
#[get("/greet")]
#[edge]
pub async fn greet() -> &'static str {
"hello"
}
pub fn edge_route_list() -> Vec<EdgeRoute> {
edge_routes![greet]
}
"#;
const DB_EDGE_APP: &str = r#"
use autumn_web::prelude::*;
#[get("/dashboard")]
#[edge(needs(db))]
pub async fn dashboard(db: Db) -> &'static str {
"rows"
}
pub fn edge_route_list() -> Vec<EdgeRoute> {
edge_routes![dashboard]
}
"#;
#[test]
fn doctor_warns_on_edge_route_with_unsupported_capability() {
let dir = project(&[
("src/main.rs", DB_EDGE_APP),
(
"src/bin/edge-capsule.rs",
"fn main() { autumn_edge::serve(edgeapp::edge_route_list()); }\n",
),
]);
let (report, _) = doctor_json(dir.path());
let caps = check(&report, "edge_capabilities");
assert_eq!(caps["status"], "warn", "{caps}");
let detail = caps["detail"].as_str().unwrap_or_default();
assert!(detail.contains("dashboard @ src/main.rs:"), "{detail}");
assert!(detail.contains("needs(db)"), "{detail}");
assert!(detail.contains("`Db`"), "{detail}");
assert!(
caps["hint"]
.as_str()
.unwrap_or_default()
.contains("Remove #[edge]"),
"{caps}"
);
}
#[test]
fn doctor_only_warns_for_an_unsupported_route_in_an_orphan_file() {
let dir = project(&[
("src/main.rs", "fn main() {}\n"),
("src/old_routes.rs", DB_EDGE_APP),
(
"src/bin/edge-capsule.rs",
"fn main() { autumn_edge::serve(edgeapp::edge_route_list()); }\n",
),
]);
let (report, _) = doctor_json(dir.path());
let caps = check(&report, "edge_capabilities");
assert_eq!(caps["status"], "warn", "{caps}");
assert!(
caps["detail"]
.as_str()
.unwrap_or_default()
.contains("dashboard @ src/old_routes.rs:"),
"{caps}"
);
}
#[test]
fn build_names_an_unsupported_edge_capability_before_compiling() {
let dir = project(&[
("src/main.rs", DB_EDGE_APP),
(
"src/bin/edge-capsule.rs",
"fn main() { autumn_edge::serve(edgeapp::edge_route_list()); }\n",
),
]);
let (stdout, stderr, code) = run_autumn(dir.path(), &["build"], &[]);
let combined = format!("{stdout}{stderr}");
assert_ne!(code, Some(0), "{combined}");
let named = combined
.find("dashboard @ src/main.rs:")
.unwrap_or_else(|| panic!("the route must be named: {combined}"));
assert!(combined.contains("needs(db)"), "{combined}");
assert!(combined.contains("Remove #[edge]"), "{combined}");
if let Some(compiling) = combined.find("Compiling") {
assert!(
named < compiling,
"the name must come before the compile: {combined}"
);
}
}
#[test]
fn doctor_fails_on_edge_route_with_auth_guard() {
let dir = project(&[
("src/main.rs", GUARDED_EDGE_APP),
(
"src/bin/edge-capsule.rs",
"fn main() { autumn_edge::serve(edgeapp::edge_route_list()); }\n",
),
]);
let (report, code) = doctor_json(dir.path());
assert_ne!(
code,
Some(0),
"an #[edge] route behind #[secured] must fail doctor: {report}"
);
let edge_routes = check(&report, "edge_routes");
assert_eq!(edge_routes["status"], "fail", "{edge_routes}");
let detail = edge_routes["detail"].as_str().unwrap_or_default();
assert!(
detail.contains("dashboard @ src/main.rs:"),
"detail must name the handler and its location: {detail}"
);
assert!(detail.contains("#[secured]"), "{detail}");
assert!(
edge_routes["hint"]
.as_str()
.unwrap_or_default()
.contains("unauthenticated read-path"),
"the hint must say why the pair is impossible: {edge_routes}"
);
}
#[test]
fn doctor_warns_on_unregistered_edge_route() {
let dir = project(&[("src/main.rs", UNREGISTERED_EDGE_APP)]);
let (report, _) = doctor_json(dir.path());
let edge_routes = check(&report, "edge_routes");
assert_eq!(edge_routes["status"], "warn", "{edge_routes}");
assert!(
edge_routes["detail"]
.as_str()
.unwrap_or_default()
.contains("greet @ src/main.rs:"),
"{edge_routes}"
);
assert!(
edge_routes["hint"]
.as_str()
.unwrap_or_default()
.contains("edge_routes![]"),
"{edge_routes}"
);
}
#[test]
fn doctor_edge_checks_pass_without_edge_routes() {
let dir = project(&[("src/main.rs", PLAIN_APP)]);
let (report, _) = doctor_json(dir.path());
for name in ["edge_target", "edge_routes", "edge_capabilities"] {
let result = check(&report, name);
assert_eq!(result["status"], "pass", "{result}");
assert_eq!(result["detail"], "no #[edge] routes", "{result}");
}
}
#[test]
fn doctor_warns_from_a_virtual_workspace_root() {
let dir = tempfile::tempdir().expect("create temp project dir");
fs::write(
dir.path().join("Cargo.toml"),
"[workspace]\nmembers = [\"app\"]\n",
)
.expect("write Cargo.toml");
let (report, _) = doctor_json(dir.path());
for name in ["edge_target", "edge_routes", "edge_capabilities"] {
let result = check(&report, name);
assert_eq!(result["status"], "warn", "{result}");
assert_eq!(
result["detail"], "Cargo.toml is a workspace root with no [package]",
"{result}"
);
assert!(
result["hint"]
.as_str()
.unwrap_or_default()
.contains("member crate directory"),
"{result}"
);
}
}
#[test]
fn doctor_warns_capsule_missing_when_autobins_disabled_and_undeclared() {
let dir = project_with_manifest(
"[workspace]\n\n[package]\nname = \"edgeapp\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\
autobins = false\n",
&[
("src/main.rs", REGISTERED_EDGE_APP),
(
"src/bin/edge-capsule.rs",
"fn main() { autumn_edge::serve(edgeapp::edge_route_list()); }\n",
),
],
);
let (report, _) = doctor_json(dir.path());
let edge_routes = check(&report, "edge_routes");
assert_eq!(edge_routes["status"], "warn", "{edge_routes}");
assert!(
edge_routes["detail"]
.as_str()
.unwrap_or_default()
.contains("edge-capsule.rs is missing"),
"{edge_routes}"
);
}
#[test]
fn doctor_honors_a_custom_capsule_bin_path() {
let dir = project_with_manifest(
"[workspace]\n\n[package]\nname = \"edgeapp\"\nversion = \"0.1.0\"\nedition = \"2021\"\n\n\
[[bin]]\nname = \"edge-capsule\"\npath = \"cmd/edge.rs\"\n",
&[
("src/main.rs", UNREGISTERED_EDGE_APP),
(
"cmd/edge.rs",
"fn main() { autumn_edge::serve(edge_routes![greet]); }\n",
),
],
);
let (report, _) = doctor_json(dir.path());
let edge_routes = check(&report, "edge_routes");
assert_eq!(
edge_routes["status"], "pass",
"a registration written only in a custom [[bin]] path must be seen, \
not reported as missing: {edge_routes}"
);
}
#[test]
fn build_edge_flag_without_edge_routes_fails_before_compiling() {
let dir = project(&[("src/main.rs", PLAIN_APP)]);
let (stdout, stderr, code) = run_autumn(dir.path(), &["build", "--edge"], &[]);
let combined = format!("{stdout}{stderr}");
assert_ne!(code, Some(0), "{combined}");
assert!(
combined.contains("no #[edge] routes found"),
"the error must say what is missing: {combined}"
);
assert!(
combined.contains("edge_routes![]"),
"the error must say how to fix it: {combined}"
);
assert!(
!combined.contains("Compiling"),
"the flag check must run before the native build: {combined}"
);
}
#[test]
fn build_with_edge_routes_checks_the_wasi_target_before_compiling() {
let dir = project(&[
("src/main.rs", UNREGISTERED_EDGE_APP),
(
"src/bin/edge-capsule.rs",
"fn main() { autumn_edge::serve(edge_routes![greet]); }\n",
),
]);
for args in [&["build"][..], &["build", "--embed"][..]] {
let (stdout, stderr, code) = run_autumn(dir.path(), args, &[]);
let combined = format!("{stdout}{stderr}");
assert_ne!(code, Some(0), "{args:?}: {combined}");
assert!(
combined.contains("`wasm32-wasip1` target is not installed"),
"{args:?}: the error must name the missing target: {combined}"
);
assert!(
combined.contains("rustup target add wasm32-wasip1"),
"{args:?}: the error must say how to fix it: {combined}"
);
assert!(
!combined.contains("Compiling"),
"{args:?}: the check must run before the native build: {combined}"
);
}
}