Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,235 @@
|
||||
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("未展开")));
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user