Files
core/test/rust/native/inspection/dispatch.rs
T
2026-11-03 03:32:07 +00:00

236 lines
8.9 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
use super::Project;
use std::{collections::BTreeSet, path::PathBuf};
fn fixture(name: &str) -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../test/rust/fixtures")
.join(name)
.canonicalize()
.unwrap()
}
fn charts(page: &str) -> impl Iterator<Item = &str> {
page.split("```mermaid\n")
.skip(1)
.map(|chart| chart.split("```").next().unwrap())
}
fn node(chart: &str, text: &str) -> String {
chart
.lines()
.find(|line| line.contains(text) && !line.contains(" -->") && !line.contains("click "))
.unwrap_or_else(|| panic!("missing {text}:\n{chart}"))
.trim()
.split(['[', '{', '('])
.next()
.unwrap()
.into()
}
// 按图的边检查可达性,不能把节点声明顺序误当执行顺序。
fn reaches(chart: &str, from: &str, to: &str) -> bool {
let mut pending = vec![from.to_owned()];
let mut seen = BTreeSet::new();
while let Some(current) = pending.pop() {
if current == to {
return true;
}
if !seen.insert(current.clone()) {
continue;
}
for line in chart.lines() {
if line.trim().starts_with(&format!("{current} -->")) {
pending.push(line.split_whitespace().last().unwrap().to_owned());
}
}
}
false
}
#[test]
fn http_flows_follow_authentication_permission_decode_call_and_transaction_order() {
let root = fixture("auth");
let project = Project::load(&root).unwrap();
let index = project.inspect().unwrap();
let pages = index.links(&root).unwrap();
let page = &pages[&root.join("module/demo/account/link.md")];
let program = project.check().unwrap();
let rust = dever::compiler::emit::application::generate(&program);
for (position, entry) in program.http.iter().enumerate() {
if !entry.path.starts_with("/demo/account/") {
continue;
}
let chart = charts(page)
.find(|chart| chart.contains(&format!("路由 {} {},", entry.method, entry.path)))
.unwrap();
let decode = node(chart, "解码具体参数");
let call = node(
chart,
&format!(
"[[\"demo.account.{}\"",
program.functions[entry.function].signature.name
),
);
assert!(reaches(chart, &decode, &call));
assert!(!reaches(chart, &node(chart, "失败:保留 HTTP"), &call));
assert!(chart.contains("demo.account.verify"));
let emitted = rust
.split(&format!("async fn api{position}("))
.nth(1)
.unwrap()
.split("struct HttpArgs")
.next()
.unwrap();
assert!(
emitted.find("authentication.prepare_at").unwrap()
< emitted.find("let args: HttpArgs").unwrap()
);
match entry.access {
dever::compiler::model::auth::Access::Protected => {
let permission = node(chart, "权限检查:");
assert!(reaches(chart, &permission, &decode));
assert!(!reaches(chart, &decode, &permission));
assert!(
emitted.find("authorization::authorize_entry").unwrap()
< emitted.find("let args: HttpArgs").unwrap()
);
}
dever::compiler::model::auth::Access::Public => {
assert!(chart.contains("public 忽略自动 Cookie"));
assert!(chart.contains("没有显式凭据:匿名"));
assert!(!chart.contains("权限检查:"));
}
dever::compiler::model::auth::Access::Authenticated => {
assert!(!chart.contains("没有显式凭据:匿名"));
assert!(!chart.contains("权限检查:"));
}
}
let write = program.functions[entry.function]
.signature
.effects
.database_write
&& !entry.long;
assert_eq!(chart.contains("进入写入口事务"), write);
if write {
assert!(reaches(chart, &call, &node(chart, "提交入口拥有的事务")));
assert!(reaches(
chart,
&node(chart, "失败:保留 HTTP"),
&node(chart, "回滚入口拥有的事务")
));
}
}
let anchors: Vec<_> = page
.lines()
.filter(|line| line.starts_with("<a id="))
.collect();
assert_eq!(anchors.len(), anchors.iter().collect::<BTreeSet<_>>().len());
}
#[test]
fn rest_flows_link_actual_field_functions_owner_model_and_failure_exits() {
let root = fixture("auth");
let project = Project::load(&root).unwrap();
let index = project.inspect().unwrap();
let pages = index.links(&root).unwrap();
let page = &pages[&root.join("module/demo/account/link.md")];
let chart = charts(page)
.find(|chart| chart.contains("自动 REST /demo/account/admin/profile"))
.unwrap();
for label in [
"405 + Allow",
"Owner",
"COUNT + 分页 SELECT",
"SELECT 单条",
"INSERT RETURNING",
"UPDATE RETURNING",
"DELETE",
"提交入口拥有的事务",
"回滚入口拥有的事务",
] {
assert!(chart.contains(label), "{label}");
}
let create = node(chart, "model.profile.#rest_create_note");
let replace = node(chart, "model.profile.#rest_replace_note");
assert!(!reaches(chart, &create, &replace));
assert!(!reaches(chart, &replace, &create));
assert!(reaches(chart, &create, &node(chart, "INSERT RETURNING")));
assert!(!reaches(chart, &node(chart, "失败:保留 HTTP"), &create));
let normalize = node(chart, "规范化字段 name:去首尾空白");
let validate = chart
.lines()
.filter(|line| line.contains("检查字段 name 的长度 / 有限数合同"))
.map(|line| line.trim().split('[').next().unwrap())
.find(|node| reaches(chart, &normalize, node))
.unwrap();
assert!(reaches(chart, validate, &node(chart, "INSERT RETURNING")));
assert!(!reaches(chart, &normalize, &node(chart, "组装分页查询")));
assert!(chart.contains("model/profile.dever#L"));
assert!(index.calls.iter().any(|call| call.kind == "rest-field"));
assert!(index.calls.iter().any(|call| call.kind == "rest-model"));
for call in index.calls.iter().filter(|call| call.kind == "rest-field") {
let symbol = &index.symbols[call.callee];
assert!(!symbol.declarations.is_empty());
assert!(
index.calls.iter().any(|next| next.caller == symbol.id),
"missing field expression calls: {}",
symbol.name
);
}
let program = project.check().unwrap();
let rust = dever::compiler::emit::application::generate(&program);
let rest = rust
.split("async fn rest0(")
.nth(1)
.unwrap()
.split("struct RestPage1")
.next()
.unwrap();
assert!(
rest.find("authorization::authorize_entry").unwrap()
< rest.find("let executor = database_context.on").unwrap()
);
assert!(rest.contains("database_context.finish(scope,owner,outcome.is_ok())"));
}
#[test]
fn adapter_flows_have_exclusive_real_targets_and_setting_source_locations() {
let root = fixture("adapters");
let index = Project::load(&root).unwrap().inspect().unwrap();
let pages = index.links(&root).unwrap();
let page = &pages[&root.join("module/demo/value/link.md")];
let chart = charts(page)
.find(|chart| {
chart.contains("demo.value.adapter.local.port.calculate") && chart.contains("启动配置")
})
.unwrap();
let local = node(chart, "demo.value.adapter.local.port.calculate");
let remote = node(chart, "demo.value.adapter.remote.port.calculate");
assert!(!reaches(chart, &local, &remote));
assert!(!reaches(chart, &remote, &local));
assert!(index.calls.iter().any(|call| call.kind == "adapter"
&& index.symbols[call.callee].name == "demo.value.adapter.remote.port.calculate"));
assert!(page.contains("配置字段(不显示值)"));
assert!(page.contains("adapter/local.dever#L"));
assert!(!page.contains("未展开"));
}
#[test]
fn external_adapters_end_at_declared_command_or_worker_boundaries() {
let examples = PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../skills/assets/examples")
.canonicalize()
.unwrap();
for (name, label) in [("command", "外部命令边界"), ("external-lib", "Worker 边界")] {
let root = examples.join(name);
let pages = Project::load(&root)
.unwrap()
.inspect()
.unwrap()
.links(&root)
.unwrap();
assert!(pages.values().any(|page| page.contains(label)));
assert!(pages.values().all(|page| !page.contains("未展开")));
}
}