Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,147 @@
|
||||
use super::{Directory, Project};
|
||||
use std::fs;
|
||||
|
||||
fn page(source: &str) -> String {
|
||||
let directory = Directory::new();
|
||||
let root = directory.0.join("flow project");
|
||||
dever::compiler::project::create(&root).unwrap();
|
||||
fs::write(root.join("module/hello/greeting/app.dever"), source).unwrap();
|
||||
let pages = Project::load(&root)
|
||||
.unwrap()
|
||||
.inspect()
|
||||
.unwrap()
|
||||
.links(&root)
|
||||
.unwrap();
|
||||
pages[&root.join("module/hello/greeting/link.md")].clone()
|
||||
}
|
||||
|
||||
fn chart<'a>(page: &'a str, name: &str) -> &'a str {
|
||||
page.split(&format!("### hello.greeting.{name}\n"))
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.split("```mermaid\n")
|
||||
.nth(1)
|
||||
.unwrap()
|
||||
.split("```")
|
||||
.next()
|
||||
.unwrap()
|
||||
}
|
||||
|
||||
fn node(chart: &str, label: &str) -> String {
|
||||
chart
|
||||
.lines()
|
||||
.find(|line| line.contains(&format!("\"{label}\"")))
|
||||
.unwrap_or_else(|| panic!("missing node {label}:\n{chart}"))
|
||||
.trim()
|
||||
.split(['[', '{', '('])
|
||||
.next()
|
||||
.unwrap()
|
||||
.to_owned()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flows_evaluate_nested_arguments_before_calls_and_keep_clause_branches_separate() {
|
||||
let page = page(
|
||||
"greet(name: Text) (message: Text) { message = choose(present(name), decorate(clean(name))) }\npresent(name: Text) (value: Bool) { value = name != \"\" }\nclean(name: Text) (value: Text) { value = name + \"!\" }\ndecorate(name: Text) (value: Text) { value = \"[\" + name + \"]\" }\nchoose(value: true, name: Text) (message: Text) { message = name }\nchoose(value: other, name: Text) (message: Text) { message = \"empty\" }\n",
|
||||
);
|
||||
let main = chart(&page, "greet");
|
||||
let calls: Vec<_> = ["present", "clean", "decorate", "choose"]
|
||||
.iter()
|
||||
.map(|name| node(main, &format!("hello.greeting.{name}")))
|
||||
.collect();
|
||||
for pair in calls.windows(2) {
|
||||
assert!(main.contains(&format!("{} --> {}", pair[0], pair[1])));
|
||||
}
|
||||
let branch = chart(&page, "choose");
|
||||
assert!(branch.contains("按实参匹配分句(互斥)"));
|
||||
let positive = node(branch, "value: true");
|
||||
let negative = node(branch, "value: other");
|
||||
assert!(!branch.contains(&format!("{positive} --> {negative}")));
|
||||
assert!(!branch.contains(&format!("{negative} --> {positive}")));
|
||||
assert!(page.contains("[hello.greeting.choose](link.md#flow-"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flows_short_circuit_and_task_launch_do_not_turn_into_unconditional_calls() {
|
||||
let page = page(
|
||||
"greet(name: Text) (message: Text) {\n enabled = present(name) and allowed(name)\n task = run(label(enabled))\n message = wait(task)\n}\npresent(name: Text) (value: Bool) { value = name != \"\" }\nallowed(name: Text) (value: Bool) { value = name != \"blocked\" }\nlabel(value: true) (message: Text) { message = \"yes\" }\nlabel(value: other) (message: Text) { message = \"no\" }\n",
|
||||
);
|
||||
let main = chart(&page, "greet");
|
||||
let condition = node(main, "present(name)");
|
||||
let evaluate = node(main, "求右侧值");
|
||||
let allowed = node(main, "hello.greeting.allowed");
|
||||
assert!(main.contains(&format!("{condition} -->|\"true\"| {evaluate}")));
|
||||
assert!(main.contains(&format!("{evaluate} --> {allowed}")));
|
||||
assert!(main.contains("false:跳过右侧"));
|
||||
let launch = node(main, "启动任务");
|
||||
let child = node(main, "hello.greeting.label");
|
||||
assert!(main.contains(&format!("{launch} -.->|\"子任务独立执行\"| {child}")));
|
||||
assert!(main.contains("返回 Task,父流程继续"));
|
||||
assert!(main.contains("等待任务结果:task"));
|
||||
assert!(!main.contains(&format!("{launch} --> {child}")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flows_collection_has_empty_path_and_repeated_callback_instead_of_one_call() {
|
||||
let page = page(
|
||||
"greet(name: Text) (message: Text) {\n values = each(label, [true, false])\n message = name + \"!\"\n}\nlabel(value: true) (text: Text) { text = \"yes\" }\nlabel(value: other) (text: Text) { text = \"no\" }\n",
|
||||
);
|
||||
let main = chart(&page, "greet");
|
||||
let condition = node(main, "还有元素?");
|
||||
let callback = node(main, "hello.greeting.label");
|
||||
assert!(main.contains("否(包括空集合)"));
|
||||
assert!(main.contains(&format!("{callback} -->|\"下一元素\"| {condition}")));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flows_timeout_only_calls_fallback_after_cancellation_and_explicit_stop_waits() {
|
||||
let page = page(
|
||||
"greet(name: Text) (message: Text) {\n abandoned = run(pause())\n stop(abandoned)\n message = timeout(run(work(name)), 1, fallback)\n}\npause() () { dever.task.sleep(1) }\nwork(name: Text) (message: Text) {\n dever.task.sleep(1)\n message = name\n}\nfallback() (message: Text) { message = \"expired\" }\n",
|
||||
);
|
||||
let main = chart(&page, "greet");
|
||||
let wait = node(main, "限时等待任务");
|
||||
let cancel = node(main, "取消并等待任务退出");
|
||||
let fallback = node(main, "hello.greeting.fallback");
|
||||
assert!(main.contains(&format!("{wait} -->|\"超时\"| {cancel}")));
|
||||
assert!(main.contains(&format!("{cancel} --> {fallback}")));
|
||||
assert!(!main.contains(&format!("{wait} --> {fallback}")));
|
||||
assert!(main.contains("请求取消并等待退出:abandoned"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flows_capture_distinguishes_business_failure_from_fault_and_fail_has_no_success_exit() {
|
||||
let page = page(
|
||||
"type Outcome { Done(value: Text)\n error Failed(message: Text) }\ngreet(name: Text) (message: Bool) recover(\"observe captured failure\") {\n outcome = capture(name)\n message = outcome == Outcome.Failed(name)\n}\ncapture(name: Text) (message: Outcome) { message = result(bad(name)) }\nbad(name: Text) (value: Text) { fail(Outcome.Failed(name)) }\n",
|
||||
);
|
||||
let main = chart(&page, "capture");
|
||||
assert!(main.contains("声明的业务失败"));
|
||||
assert!(main.contains("Fault(不转换为业务失败)"));
|
||||
let failed = chart(&page, "bad");
|
||||
assert!(failed.contains("传播错误"));
|
||||
assert!(!failed.contains("\"返回\""));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn flows_and_navigation_keep_direct_typed_decode_capture() {
|
||||
let source = "type Outcome { Done(value: Text)\n error Failed(cause: dever.json.JsonResult) }\ngreet(name: Text) (message: Text) { payload: Json = dever.json.encode(name)\n value: Outcome = result(dever.json.decode(payload))\n message = render(value) }\nrender(value: Outcome.Done(text)) (message: Text) recover(\"显示无效输入\") { message = text }\nrender(value: Outcome.Failed(cause)) (message: Text) recover(\"显示无效输入\") { message = \"invalid\" }\n";
|
||||
let page = page(source);
|
||||
let flow = chart(&page, "greet");
|
||||
assert!(flow.contains("类型化 JSON 解码"));
|
||||
assert!(flow.contains("声明的业务失败"));
|
||||
let directory = Directory::new();
|
||||
let file = directory.0.join("main.dever");
|
||||
fs::write(&file, source).unwrap();
|
||||
let index = Project::load(&file).unwrap().inspect().unwrap();
|
||||
let decode = index
|
||||
.symbols
|
||||
.iter()
|
||||
.find(|symbol| symbol.name == "dever.json.decode")
|
||||
.unwrap();
|
||||
let calls = index
|
||||
.calls
|
||||
.iter()
|
||||
.filter(|call| call.callee == decode.id)
|
||||
.collect::<Vec<_>>();
|
||||
assert_eq!(calls.len(), 1);
|
||||
assert_eq!(calls[0].kind, "operation");
|
||||
}
|
||||
Reference in new issue
Block a user