use super::{Command, Directory, Project}; use std::fs; use std::path::Path; fn inspect(root: &Path, check: bool) -> std::process::Output { let mut command = Command::new(env!("CARGO_BIN_EXE_dever")); command.arg("inspect").arg(root).arg("--links"); if check { command.arg("--check"); } command.output().unwrap() } fn success(output: std::process::Output) -> String { assert!( output.status.success(), "{}", String::from_utf8_lossy(&output.stderr) ); String::from_utf8(output.stdout).unwrap() } #[test] fn domain_links_check_is_read_only_and_generation_is_stable() { { let directory = Directory::new(); let root = directory.0.join("project with spaces"); dever::compiler::project::create(&root).unwrap(); let link = root.join("module/hello/greeting/link.md"); fs::remove_file(&link).unwrap(); let missing = inspect(&root, true); assert_eq!(missing.status.code(), Some(1)); assert!(!link.exists()); assert!(success(inspect(&root, false)).contains("1 changed")); let first = fs::read_to_string(&link).unwrap(); assert!(first.contains("CMD [hello.greeting.api.greet]")); assert!(!first.contains(root.to_str().unwrap())); assert!(first.contains("app.dever#L")); let modified = fs::metadata(&link).unwrap().modified().unwrap(); assert!(success(inspect(&root, false)).contains("0 changed")); assert_eq!(fs::metadata(&link).unwrap().modified().unwrap(), modified); success(inspect(&root, true)); let source = root.join("module/hello/greeting/app.dever"); let text = fs::read_to_string(&source).unwrap(); fs::write(&source, text.replace("Hello, ", "Welcome, ")).unwrap(); assert_eq!(inspect(&root, true).status.code(), Some(1)); assert_eq!(fs::read_to_string(&link).unwrap(), first); success(inspect(&root, false)); assert_ne!(fs::read_to_string(&link).unwrap(), first); success(inspect(&root, true)); } } #[test] fn domain_links_keep_every_cross_domain_call_site_and_refresh_both_ends() { let directory = Directory::new(); let root = directory.0.join("project"); dever::compiler::project::create(&root).unwrap(); for domain in ["checkout", "account"] { let owner = root.join("module/shop").join(domain); fs::create_dir_all(&owner).unwrap(); fs::write(owner.join("app.dever"), "welcome(name: Text) (message: Text) {\n message = hello.greeting.greet(name) + hello.greeting.greet(\"other\")\n}\n").unwrap(); fs::write(owner.join("api.dever"), "cmd welcome = app.welcome\n").unwrap(); } let unrelated = root.join("module/empty/types"); fs::create_dir_all(&unrelated).unwrap(); fs::write( unrelated.join("domain.dever"), "type Label { value: Text }\n", ) .unwrap(); success(inspect(&root, false)); let incoming = root.join("module/hello/greeting/link.md"); let first = fs::read_to_string(&incoming).unwrap(); let incoming_calls = |text: &str| { text.split("## 外部调用方\n") .nth(1) .unwrap() .split("## 能力与内部调用\n") .next() .unwrap() .matches("| [hello.greeting.greet]") .count() }; assert_eq!(incoming_calls(&first), 4); assert!(first.contains("shop.checkout.welcome")); assert!(first.contains("shop.account.welcome")); assert!(first.contains("../../shop/checkout/app.dever#L2")); let outgoing = fs::read_to_string(root.join("module/shop/checkout/link.md")).unwrap(); assert!(outgoing.contains("跨领域 [hello.greeting](../../hello/greeting/link.md)")); let untouched = unrelated.join("link.md"); let modified = fs::metadata(&untouched).unwrap().modified().unwrap(); fs::write( root.join("module/shop/checkout/app.dever"), "welcome(name: Text) (message: Text) {\n message = hello.greeting.greet(name) + \"!\"\n}\n", ) .unwrap(); let stale = inspect(&root, true); assert_eq!(stale.status.code(), Some(1)); let report = String::from_utf8(stale.stdout).unwrap(); assert!(report.contains("stale module/hello/greeting/link.md")); assert!(report.contains("stale module/shop/checkout/link.md")); assert!(!report.contains("stale module/empty/types/link.md")); success(inspect(&root, false)); assert_eq!(incoming_calls(&fs::read_to_string(&incoming).unwrap()), 3); assert_eq!( fs::metadata(&untouched).unwrap().modified().unwrap(), modified ); fs::remove_file(root.join("module/shop/account/app.dever")).unwrap(); fs::remove_file(root.join("module/shop/account/api.dever")).unwrap(); let removed = root.join("module/shop/account/link.md"); assert_eq!(inspect(&root, true).status.code(), Some(1)); assert!(removed.exists()); assert!(success(inspect(&root, false)).contains("removed module/shop/account/link.md")); assert!(!removed.exists()); assert!( !fs::read_to_string(&incoming) .unwrap() .contains("shop.account.welcome") ); } #[test] fn domain_links_preserve_handwritten_files_and_reject_invalid_source_before_writing() { let directory = Directory::new(); let root = directory.0.join("project"); dever::compiler::project::create(&root).unwrap(); let link = root.join("module/hello/greeting/link.md"); fs::write(&link, "# 手工说明\n").unwrap(); let refused = inspect(&root, false); assert!(!refused.status.success()); assert!(String::from_utf8_lossy(&refused.stderr).contains("handwritten")); assert_eq!(fs::read_to_string(&link).unwrap(), "# 手工说明\n"); fs::remove_file(&link).unwrap(); success(inspect(&root, false)); let first = fs::read_to_string(&link).unwrap(); let source = root.join("module/hello/greeting/app.dever"); let text = fs::read_to_string(&source).unwrap(); fs::write(&source, text.replace("\"Hello, \" + name", "unknown(name)")).unwrap(); assert!(!inspect(&root, false).status.success()); assert_eq!(fs::read_to_string(&link).unwrap(), first); } #[test] fn domain_links_mark_api_job_and_dispatch_without_inventing_execution_order() { let fixtures = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../test/rust/fixtures") .canonicalize() .unwrap(); let job = fixtures.join("job"); let pages = Project::load(&job) .unwrap() .inspect() .unwrap() .links(&job) .unwrap(); let text = &pages[&job.join("module/demo/queue/link.md")]; assert!(text.contains("Job [demo.queue.job.publish]")); assert!(text.contains("| 入队 |")); assert!(text.contains("按求值顺序")); assert!(text.contains("不是某次运行的日志")); assert!(text.contains("flowchart TD")); assert!(text.contains("由 Worker 后续调度,不等待执行")); let http = Path::new(env!("CARGO_MANIFEST_DIR")) .join("../skills/assets/examples/http-rest") .canonicalize() .unwrap(); let pages = Project::load(&http) .unwrap() .inspect() .unwrap() .links(&http) .unwrap(); assert!(pages.values().any(|text| text.contains("- API ["))); assert!(pages.values().all(|text| text.contains("自动 REST"))); } #[test] fn domain_links_reject_conflicting_options_and_non_project_sources() { let directory = Directory::new(); let source = directory.0.join("main.dever"); fs::write(&source, "main() (value: Int) { value = 1 }\n").unwrap(); let invalid = inspect(&source, false); assert!(!invalid.status.success()); assert!(String::from_utf8_lossy(&invalid.stderr).contains("project root")); for flags in [ vec!["--check"], vec!["--links", "--json"], vec!["--links", "--symbol", "main.main"], vec!["--links", "--links"], ] { let output = Command::new(env!("CARGO_BIN_EXE_dever")) .arg("inspect") .arg(&source) .args(flags) .output() .unwrap(); assert!(!output.status.success()); assert!(output.stdout.is_empty()); } }