Initial Dever 0.0.1
This commit is contained in:
commit
81233d098c
1479 files changed
+163782
No files matched your search
@@ -0,0 +1,127 @@
|
||||
use super::{ADAPTER, APP, PORT, packages};
|
||||
|
||||
#[test]
|
||||
fn roles_cannot_call_around_the_port_boundary() {
|
||||
for call in [
|
||||
"app.read(value)",
|
||||
"domain.read(value)",
|
||||
"port.read(value)",
|
||||
"foreign.library.read(value)",
|
||||
"shop.account.adapter.other.helper(value)",
|
||||
] {
|
||||
let adapter = format!("port.read(value: Int) (answer: Int) {{ answer = {call} }}");
|
||||
let error = packages(&[
|
||||
("shop.account.app", APP),
|
||||
(
|
||||
"shop.account.domain",
|
||||
"read(value: Int) (answer: Int) { answer = value }",
|
||||
),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter.local", &adapter),
|
||||
(
|
||||
"foreign.library",
|
||||
"public read(value: Int) (answer: Int) { answer = value }",
|
||||
),
|
||||
])
|
||||
.unwrap_err();
|
||||
assert!(
|
||||
matches!(error.code, "C004" | "C005" | "C006"),
|
||||
"{call}: {error:?}"
|
||||
);
|
||||
}
|
||||
for caller in ["shop.account.domain", "shop.other.app", "foreign"] {
|
||||
let error = packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", ADAPTER),
|
||||
(
|
||||
caller,
|
||||
"entry(value: Int) (answer: Int) { answer = shop.account.port.read(value) }",
|
||||
),
|
||||
])
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code, "C006", "{caller}: {error:?}");
|
||||
}
|
||||
for call in [
|
||||
"adapter.helper(value)",
|
||||
"adapter.port.read(value)",
|
||||
"shop.account.adapter.port.read(value)",
|
||||
] {
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", &APP.replace("port.read(value)", call)),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", ADAPTER)
|
||||
])
|
||||
.is_err(),
|
||||
"{call}"
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn only_app_types_in_the_complete_contract_graph_are_visible() {
|
||||
let app = "type Problem { error Missing(message: Text) }\ntype Person { name: Text }\ntype Unrelated { number: Int }\nread(value: Person) (answer: Text) { answer = port.read(port.Row { person = value }) }";
|
||||
let port = "type Row { person: app.Person }\nread(value: Row) (answer: Text) fails app.Problem";
|
||||
let adapter = "port.read(value: port.Row) (answer: Text) { answer = label(value.person) }\nlabel(value: app.Person) (answer: Text) { answer = value.name }";
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.adapter", adapter),
|
||||
("shop.account.port", port),
|
||||
("shop.account.app", app)
|
||||
])
|
||||
.is_ok()
|
||||
);
|
||||
for bad in [
|
||||
format!("{adapter}\nunused(value: app.Unrelated) () {{}}"),
|
||||
adapter.replace(
|
||||
"answer = label(value.person)",
|
||||
"hidden = app.Unrelated { number = 1 }\nanswer = label(value.person)",
|
||||
),
|
||||
format!("{adapter}\ntype Hidden {{ value: List<app.Unrelated> }}"),
|
||||
] {
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.adapter", &bad),
|
||||
("shop.account.port", port),
|
||||
("shop.account.app", app)
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C006"
|
||||
);
|
||||
}
|
||||
let bad = format!("{port}\ntype Unused {{ value: app.Unrelated }}");
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", app),
|
||||
("shop.account.port", &bad),
|
||||
("shop.account.adapter", adapter)
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C006"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn adapter_cannot_read_another_ports_private_dto() {
|
||||
let error = packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
(
|
||||
"shop.account.port.other",
|
||||
"type Hidden { value: Int }\nwrite(value: Int) () fails app.Problem",
|
||||
),
|
||||
(
|
||||
"shop.account.adapter.first",
|
||||
&format!("{ADAPTER}\nunused(value: port.other.Hidden) () {{}}"),
|
||||
),
|
||||
(
|
||||
"shop.account.adapter.second",
|
||||
"port.other.write(value: Int) () {}",
|
||||
),
|
||||
])
|
||||
.unwrap_err();
|
||||
assert_eq!(error.code, "C006");
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
use super::signature;
|
||||
use dever::compiler::{check, model::Program};
|
||||
use dever::syntax::{diagnostic::Diagnostic, source::Source};
|
||||
|
||||
fn packages(files: &[(&str, &str)]) -> Result<Program, Diagnostic> {
|
||||
let mut modules = Vec::new();
|
||||
let mut identities = Vec::new();
|
||||
for (index, (identity, text)) in files.iter().enumerate() {
|
||||
let path = if *identity == "dever.io" {
|
||||
"lib/dever/io.dever"
|
||||
} else {
|
||||
"case.dever"
|
||||
};
|
||||
modules.push(dever::syntax::parse(&Source::new(
|
||||
index,
|
||||
path,
|
||||
(*text).into(),
|
||||
))?);
|
||||
identities.push((*identity).into());
|
||||
}
|
||||
check::check(modules, &identities)
|
||||
}
|
||||
|
||||
const IO: &str = include_str!("../../../../lib/dever/io.dever");
|
||||
const PORT: &str = "open(path: Text) (file: dever.system.File) fails dever.io.OpenResult";
|
||||
const ADAPTER: &str = "port.open(path: Text) (file: dever.system.File) { file = open(path) }\nopen(path: Text) (file: dever.system.File) { file = dever.io.open(path) }";
|
||||
|
||||
#[test]
|
||||
fn boundary_retains_real_effects_without_permitting_unmediated_io() {
|
||||
let app = "open(path: Text) (file: dever.system.File) { file = port.open(path) }";
|
||||
let program = packages(&[
|
||||
("shop.files.app", app),
|
||||
("shop.files.port", PORT),
|
||||
("shop.files.adapter", ADAPTER),
|
||||
("dever.io", IO),
|
||||
])
|
||||
.unwrap();
|
||||
let effects = signature(&program, "open").effects;
|
||||
assert!(effects.file && effects.blocking && !effects.unmediated_io);
|
||||
for body in [
|
||||
"file = dever.io.open(path)",
|
||||
"old = port.open(path)\nfile = dever.io.open(path)",
|
||||
"files = each(dever.io.open, [path])\nfile = port.open(path)",
|
||||
"old = result(dever.io.open(path))\nfile = port.open(path)",
|
||||
] {
|
||||
let app = format!("open(path: Text) (file: dever.system.File) {{ {body} }}");
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.files.app", &app),
|
||||
("shop.files.port", PORT),
|
||||
("shop.files.adapter", ADAPTER),
|
||||
("dever.io", IO)
|
||||
])
|
||||
.is_err(),
|
||||
"{body}"
|
||||
);
|
||||
}
|
||||
let pure = app.replace(" {", " pure {");
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.files.app", &pure),
|
||||
("shop.files.port", PORT),
|
||||
("shop.files.adapter", ADAPTER),
|
||||
("dever.io", IO)
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C013"
|
||||
);
|
||||
let scheduled = app.replace("port.open(path)", "blocking(port.open(path))");
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.files.app", &scheduled),
|
||||
("shop.files.port", PORT),
|
||||
("shop.files.adapter", ADAPTER),
|
||||
("dever.io", IO)
|
||||
])
|
||||
.is_ok()
|
||||
);
|
||||
let asynchronous = app.replace(
|
||||
"file = port.open(path)",
|
||||
"other = blocking(port.open(path))\nfile = port.open(path)",
|
||||
);
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.files.app", &asynchronous),
|
||||
("shop.files.port", PORT),
|
||||
("shop.files.adapter", ADAPTER),
|
||||
("dever.io", IO)
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C013"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_cannot_hide_blocking_candidate_effects() {
|
||||
let app = "open(path: Text) (file: dever.system.File) { file = port.open(path) }";
|
||||
let worker =
|
||||
"port.open(path: Text) (file: dever.system.File) { file = blocking(dever.io.open(path)) }";
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.files.app", app),
|
||||
("shop.files.port", PORT),
|
||||
("shop.files.adapter.inline", ADAPTER),
|
||||
("shop.files.adapter.worker", worker),
|
||||
("dever.io", IO),
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C013"
|
||||
);
|
||||
let program = packages(&[
|
||||
("shop.files.app", app),
|
||||
("shop.files.port", PORT),
|
||||
("shop.files.adapter.first", worker),
|
||||
("shop.files.adapter.second", worker),
|
||||
("dever.io", IO),
|
||||
])
|
||||
.unwrap();
|
||||
let effects = signature(&program, "open").effects;
|
||||
assert!(effects.file && effects.suspend && effects.configuration);
|
||||
assert!(!effects.blocking && !effects.unmediated_io);
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
use super::package::packages;
|
||||
use dever::compiler::model::{Node, Program, Signature, Statement};
|
||||
|
||||
mod boundary;
|
||||
mod effects;
|
||||
mod settings;
|
||||
|
||||
const APP: &str = "type Problem { error Missing(message: Text)\n error Denied(code: Int) }\nread(value: Int) (answer: Int) { answer = port.read(value) }";
|
||||
const PORT: &str = "read(value: Int) (answer: Int) fails app.Problem";
|
||||
const ADAPTER: &str = "port.read(value: Int) (answer: Int) { answer = helper(value) }\nhelper(value: Int) (answer: Int) { answer = value + 1 }";
|
||||
|
||||
fn signature<'program>(program: &'program Program, name: &str) -> &'program Signature {
|
||||
&program
|
||||
.functions
|
||||
.iter()
|
||||
.find(|function| function.signature.name == name)
|
||||
.unwrap()
|
||||
.signature
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn port_alias_binds_directly_and_preserves_the_declared_error_set() {
|
||||
let program = packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", ADAPTER),
|
||||
("shop.account.api", "cmd read = app.read"),
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(program.functions.len(), 3);
|
||||
let app = &program.functions[program.commands[0].function];
|
||||
let Statement::Assign { value, .. } = &app.clauses[0].body[0] else {
|
||||
panic!()
|
||||
};
|
||||
let Node::Call(index, _) = value.kind else {
|
||||
panic!()
|
||||
};
|
||||
assert_eq!(program.functions[index].signature.name, "port.read");
|
||||
assert_eq!(app.signature.failures.len(), 2);
|
||||
assert_eq!(
|
||||
app.signature.failures,
|
||||
program.functions[index].signature.failures
|
||||
);
|
||||
assert!(!app.signature.effects.unmediated_io);
|
||||
assert!(program.api_snapshot().contains("Problem.Denied"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn topics_dtos_static_handlers_and_adapter_clauses_share_existing_type_checks() {
|
||||
let program = packages(&[
|
||||
(
|
||||
"shop.account.app",
|
||||
"type Problem { error Missing(message: Text) }\nread(value: Int) (answer: Int) { row = port.mail.Row { value = value }\nanswer = apply(port.mail.read, row) }\napply(action: handler(row: port.mail.Row) (answer: Int), row: port.mail.Row) (answer: Int) { answer = action(row) }",
|
||||
),
|
||||
(
|
||||
"shop.account.port.mail",
|
||||
"type Row { value: Int }\nread(row: Row) (answer: Int) fails app.Problem",
|
||||
),
|
||||
(
|
||||
"shop.account.adapter.local",
|
||||
"port.mail.read(row: port.mail.Row) (answer: Int) { answer = row.value }",
|
||||
),
|
||||
]);
|
||||
// App 的公开 handler 参数不能泄漏 Port DTO;在领域内经普通 App 函数也不能绕过。
|
||||
assert_eq!(program.unwrap_err().code, "C006");
|
||||
let program = packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", "port.read(value: Int < 0) (answer: Int) { fail(app.Problem.Missing(\"negative\")) }\nport.read(value: other) (answer: Int) { answer = value }"),
|
||||
]).unwrap();
|
||||
assert_eq!(signature(&program, "read").failures.len(), 2);
|
||||
assert!(packages(&[
|
||||
("shop.account.app", "type Problem { error Missing(message: Text) }\nread(value: Int) (answer: Int) { row = port.mail.Row { value = value }\nanswer = port.mail.read(row) }"),
|
||||
("shop.account.port.mail", "type Row { value: Int }\nread(row: Row) (answer: Int) fails app.Problem"),
|
||||
("shop.account.adapter.local", "port.mail.read(row: port.mail.Row) (answer: Int) { answer = row.value }"),
|
||||
]).is_ok());
|
||||
assert!(
|
||||
packages(&[
|
||||
(
|
||||
"shop.account.app",
|
||||
&APP.replace("port.read(value)", "sum(each(port.read, [value]))")
|
||||
),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", ADAPTER),
|
||||
])
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn ports_reject_missing_mismatched_and_partial_implementations() {
|
||||
for adapter in [
|
||||
"port.read(wrong: Int) (answer: Int) { answer = wrong }",
|
||||
"port.read(value: Text) (answer: Int) { answer = 1 }",
|
||||
"port.read(value: Int) (wrong: Int) { wrong = value }",
|
||||
"port.read(value: Int, action: handler() ()) (answer: Int) { answer = value }",
|
||||
"port.unknown(value: Int) (answer: Int) { answer = value }",
|
||||
"helper(value: Int) (answer: Int) { answer = value }",
|
||||
] {
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", adapter)
|
||||
])
|
||||
.is_err(),
|
||||
"{adapter}"
|
||||
);
|
||||
}
|
||||
assert!(packages(&[("shop.account.app", APP), ("shop.account.port", PORT)]).is_err());
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", APP),
|
||||
(
|
||||
"shop.account.port",
|
||||
&format!("{PORT}\nwrite(value: Int) () fails app.Problem")
|
||||
),
|
||||
("shop.account.adapter.first", ADAPTER),
|
||||
(
|
||||
"shop.account.adapter.second",
|
||||
"port.write(value: Int) () {}"
|
||||
),
|
||||
])
|
||||
.unwrap_err()
|
||||
.message
|
||||
.contains("complete Port")
|
||||
);
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter.first", ADAPTER),
|
||||
("shop.account.adapter.second", ADAPTER),
|
||||
])
|
||||
.is_ok()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn port_declarations_require_plain_types_and_public_explicit_errors() {
|
||||
for port in [
|
||||
"read(value: Int) (answer: Int) { answer = value }",
|
||||
"read(value: Int < 0) (answer: Int) fails app.Problem",
|
||||
"read(value: other) (answer: Int) fails app.Problem",
|
||||
"read(value: handler() ()) (answer: Int) fails app.Problem",
|
||||
"read(value: Int) (answer: Int > 0) fails app.Problem",
|
||||
"read(value: Int) (answer: Int) pure fails app.Problem",
|
||||
"read(value: Int) (answer: Int) recover(\"ignore\") fails app.Problem",
|
||||
"read(value: Int) (answer: Int) fails Int",
|
||||
"read(value: Int) (answer: Int) fails app.Problem?",
|
||||
"read(value: Int) (answer: Int) fails app.Record",
|
||||
"read(value: Int) (answer: Int) fails app.State",
|
||||
"type Local { error Failed }\nread(value: Int) (answer: Int) fails Local",
|
||||
"public read(value: Int) (answer: Int) fails app.Problem",
|
||||
] {
|
||||
let app = format!("{APP}\ntype Record {{ value: Int }}\ntype State {{ Ready }}");
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", &app),
|
||||
("shop.account.port", port),
|
||||
("shop.account.adapter", ADAPTER)
|
||||
])
|
||||
.is_err(),
|
||||
"{port}"
|
||||
);
|
||||
}
|
||||
assert!(packages(&[("main", "read(value: Int) (answer: Int) fails Problem")]).is_err());
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", &format!("{PORT}\n{PORT}")),
|
||||
("shop.account.adapter", ADAPTER)
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C002"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn implementations_cannot_propagate_undeclared_errors() {
|
||||
let adapter = "type Private { error Other }\nport.read(value: Int) (answer: Int) { fail(Private.Other()) }";
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", adapter)
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C012"
|
||||
);
|
||||
let app = "type Problem { error Missing(message: Text)\nerror Denied(code: Int) }\ntype ReadResult { Ready(value: Int)\nerror Failed(problem: Problem) }\nread(value: Int) (answer: ReadResult) { answer = result(port.read(value)) }";
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", app),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", ADAPTER)
|
||||
])
|
||||
.is_ok()
|
||||
);
|
||||
let app = app.replace(
|
||||
"answer = result(port.read(value))",
|
||||
"lost = result(port.read(value))\nanswer = ReadResult.Ready(value)",
|
||||
);
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", &app),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", ADAPTER)
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C012"
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,194 @@
|
||||
use super::{APP, PORT, packages, signature};
|
||||
|
||||
#[test]
|
||||
fn external_worker_secrets_belong_only_in_settings() {
|
||||
let adapter = "setting { token: Secret }\nexternal exec \"worker\" {}";
|
||||
for kind in [
|
||||
"Text",
|
||||
"Secret",
|
||||
"Secret?",
|
||||
"List<Secret>",
|
||||
"app.Credentials",
|
||||
] {
|
||||
let app = format!(
|
||||
"type Problem {{ error Failed(message: Text) }}\ntype Credentials {{ tokens: List<Secret?> }}\nread(value: {kind}) (answer: Int) {{ answer = port.read(value) }}"
|
||||
);
|
||||
let port = format!("read(value: {kind}) (answer: Int) fails app.Problem");
|
||||
let result = packages(&[
|
||||
("shop.account.app", &app),
|
||||
("shop.account.port", &port),
|
||||
("shop.account.adapter", adapter),
|
||||
]);
|
||||
if kind == "Text" {
|
||||
assert!(result.is_ok(), "{result:?}");
|
||||
} else {
|
||||
let error = result.unwrap_err();
|
||||
assert!(
|
||||
error.message.contains("Secret belongs only in setting"),
|
||||
"{kind}: {error:?}"
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_are_private_readonly_typed_values() {
|
||||
let adapter = "setting { offset: Int\ntoken: Secret }\nport.read(value: Int) (answer: Int) { answer = helper(value) }\nhelper(value: Int) (answer: Int) pure { answer = value + setting.offset }";
|
||||
let program = packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", adapter),
|
||||
])
|
||||
.unwrap();
|
||||
assert!(signature(&program, "read").effects.configuration);
|
||||
assert!(signature(&program, "helper").pure);
|
||||
assert_eq!(signature(&program, "read").failures.len(), 2);
|
||||
for body in [
|
||||
"setting.offset = value\nanswer = value",
|
||||
"setting = value\nanswer = value",
|
||||
"answer = setting.missing",
|
||||
"answer = setting.offset()",
|
||||
"answer = setting.token",
|
||||
] {
|
||||
let bad = format!(
|
||||
"setting {{ offset: Int\ntoken: Secret }}\nport.read(value: Int) (answer: Int) {{ {body} }}"
|
||||
);
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", &bad)
|
||||
])
|
||||
.is_err(),
|
||||
"{body}"
|
||||
);
|
||||
}
|
||||
let empty =
|
||||
"setting {}\nport.read(value: Int) (answer: Int) { setting = value\nanswer = value }";
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", empty)
|
||||
])
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn settings_reject_wrong_roles_invalid_wire_fields_and_shadowing() {
|
||||
for owner in [
|
||||
"main",
|
||||
"shop.account.app",
|
||||
"shop.account.port",
|
||||
"shop.account.domain",
|
||||
] {
|
||||
assert!(
|
||||
packages(&[(owner, "setting { offset: Int }")]).is_err(),
|
||||
"{owner}"
|
||||
);
|
||||
}
|
||||
for setting in [
|
||||
"setting { Ready }",
|
||||
"setting { private offset: Int }",
|
||||
"setting { offset: Int > 0 }",
|
||||
"setting { file: dever.system.File }",
|
||||
"setting { values: Map<Text, Secret> }",
|
||||
"setting { offset: Int }\nsetting { token: Secret }",
|
||||
"setting { missing: Unknown }",
|
||||
] {
|
||||
let adapter =
|
||||
format!("{setting}\nport.read(value: Int) (answer: Int) {{ answer = value }}");
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", &adapter)
|
||||
])
|
||||
.is_err(),
|
||||
"{setting}"
|
||||
);
|
||||
}
|
||||
for helper in [
|
||||
"helper(setting: Int) () {}",
|
||||
"helper() (setting: Int) { setting = 1 }",
|
||||
] {
|
||||
let adapter = format!(
|
||||
"setting {{ offset: Int }}\nport.read(value: Int) (answer: Int) {{ answer = value }}\n{helper}"
|
||||
);
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter", &adapter)
|
||||
])
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unused_adapter_candidates_still_enforce_failures_and_complete_contracts() {
|
||||
let valid = "port.read(value: Int) (answer: Int) { answer = value }";
|
||||
let invalid = "type Private { error Failed }\nport.read(value: Int) (answer: Int) { fail(Private.Failed()) }";
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter.local", valid),
|
||||
("shop.account.adapter.remote", invalid)
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C012"
|
||||
);
|
||||
let extra_port = format!("{PORT}\nwrite(value: Int) () fails app.Problem");
|
||||
let complete = format!("{valid}\nport.write(value: Int) () {{}}");
|
||||
assert!(
|
||||
packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", &extra_port),
|
||||
("shop.account.adapter.local", &complete),
|
||||
("shop.account.adapter.remote", valid)
|
||||
])
|
||||
.is_err()
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn selection_keeps_all_candidate_effects_and_failure_identity() {
|
||||
let quiet = "setting { offset: Int }\nport.read(value: Int) (answer: Int) { answer = value + setting.offset }";
|
||||
let scheduled = "port.read(value: Int) (answer: Int) { answer = wait(run(helper(value))) }\nhelper(value: Int) (answer: Int) { answer = value }";
|
||||
let program = packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter.local", quiet),
|
||||
("shop.account.adapter.remote", scheduled),
|
||||
])
|
||||
.unwrap();
|
||||
let effects = signature(&program, "read").effects;
|
||||
assert!(effects.configuration && effects.suspend && effects.schedule);
|
||||
let pure = APP.replace("(answer: Int) {", "(answer: Int) pure {");
|
||||
assert_eq!(
|
||||
packages(&[
|
||||
("shop.account.app", &pure),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter.local", quiet),
|
||||
("shop.account.adapter.remote", scheduled),
|
||||
])
|
||||
.unwrap_err()
|
||||
.code,
|
||||
"C013"
|
||||
);
|
||||
let error = "port.read(value: Int) (answer: Int) { fail(app.Problem.Missing(\"missing\")) }";
|
||||
let program = packages(&[
|
||||
("shop.account.app", APP),
|
||||
("shop.account.port", PORT),
|
||||
("shop.account.adapter.local", quiet),
|
||||
("shop.account.adapter.remote", error),
|
||||
])
|
||||
.unwrap();
|
||||
assert_eq!(signature(&program, "read").failures.len(), 2);
|
||||
assert!(signature(&program, "read").effects.configuration);
|
||||
assert!(!program.api_snapshot().contains("setting.offset"));
|
||||
}
|
||||
Reference in new issue
Block a user