Files
core/test/dever-tests/tests/error_effects.rs
T

177 lines
6.3 KiB
Rust

mod support;
use support::{checked, rejected, sources};
const ERRORS: &str = r#"public type ComputeResult {
Done(value: Int)
error Invalid(value: Int)
}
compute(value: 0) (answer: Int) {
fail(ComputeResult.Invalid(value))
}
compute(value: other) (answer: Int) {
answer = value + 1
}
render(result: ComputeResult.Done(value)) (text: Text) recover("render handled compute errors") {
text = int.to_text(value)
}
render(result: ComputeResult.Invalid(value)) (text: Text) recover("render handled compute errors") {
text = "invalid " + int.to_text(value)
}
"#;
#[test]
fn fail_and_result_have_stable_syntax_and_formatting() {
let source = format!("{ERRORS}main() (text: Text) {{ text = render(result(compute(0))) }}");
let mut map = sources(&source);
let formatted = dever_core::format::format(map.get(map.files()[0].id())).unwrap();
map = sources(&formatted);
let reformatted = dever_core::format::format(map.get(map.files()[0].id())).unwrap();
assert_eq!(formatted, reformatted);
assert!(formatted.contains("fail(ComputeResult.Invalid(value))"));
assert!(formatted.contains("result(compute(0))"));
}
#[test]
fn error_sets_propagate_and_are_visible_in_the_public_api() {
let source = format!(
"{ERRORS}forward(value: Int) (answer: Int) {{ answer = compute(value) }}\npublic main() (answer: Int) {{ answer = forward(1) }}"
);
let program = checked(&sources(&source));
let snapshot = program.api_snapshot();
assert!(
snapshot.contains("function main.main() (answer: Int [])")
&& snapshot.contains("errors=[main.ComputeResult.Invalid]"),
"{snapshot}"
);
}
#[test]
fn result_capture_requires_matching_success_and_error_variants() {
checked(&sources(&format!(
"{ERRORS}main() (text: Text) {{ text = render(result(compute(0))) }}"
)));
rejected(
&format!(
"{ERRORS}type Wrong {{ Done(value: Int)\nerror Other(value: Int) }}\nwrong(result: Wrong.Done(value)) () recover(\"handled\") {{}}\nwrong(result: Wrong.Other(value)) () recover(\"handled\") {{}}\nmain() () {{ wrong(result(compute(0))) }}"
),
"result capture errors do not match the call",
);
rejected(
&format!(
"{ERRORS}type Wrong {{ Done\nerror Invalid(value: Int) }}\nwrong(result: Wrong) () recover(\"handled\") {{}}\nmain() () {{ wrong(result(compute(0))) }}"
),
"success payload must match",
);
}
#[test]
fn result_capture_does_not_introduce_a_concurrency_effect() {
checked(&sources(&format!(
"{ERRORS}main() (result: ComputeResult) pure {{ result = result(compute(0)) }}"
)));
}
#[test]
fn fail_must_be_an_error_variant_and_end_its_clause() {
rejected(
"type Outcome { Done\nerror Failed }\npublic main() () { fail(Outcome.Done) }",
"fail requires an error choice variant",
);
rejected(
"type Outcome { error Failed }\npublic main() () { fail(Outcome.Failed)\ndever.io.println(\"unreachable\") }",
"fail must be the final statement",
);
}
#[test]
fn native_default_propagation_reaches_the_cli_boundary_once() {
let output = support::run(
"type Outcome { error Failed(value: Int) }\nstep() () { fail(Outcome.Failed(7)) }\npublic main() () { step() }",
);
assert!(!output.status.success());
assert!(output.stdout.is_empty());
let stderr = String::from_utf8(output.stderr).unwrap();
assert_eq!(stderr.lines().count(), 1, "{stderr}");
assert!(stderr.contains("main.Outcome.Failed(7)"), "{stderr}");
assert!(stderr.contains("main.dever:3:"), "{stderr}");
}
#[test]
fn native_result_capture_converts_success_and_business_failure_to_choice_values() {
for (value, expected) in [("0", "invalid 0"), ("4", "5")] {
let output = support::stdout(&format!(
"{ERRORS}main() (text: Text) {{ text = render(result(compute({value}))) }}"
));
assert_eq!(output, format!("text = {expected}\n"));
}
}
#[test]
fn task_wait_preserves_business_error_identity() {
let output = support::run(
"type Outcome { error Failed(value: Int) }\nstep() () { fail(Outcome.Failed(9)) }\npublic main() () { task = run(step())\nwait(task) }",
);
assert!(!output.status.success());
let stderr = String::from_utf8(output.stderr).unwrap();
assert!(stderr.contains("main.Outcome.Failed(9)"), "{stderr}");
assert!(stderr.contains("main.dever:4:"), "{stderr}");
}
#[test]
fn capture_wraps_a_shared_error_choice_without_losing_its_identity() {
let source = r#"public type Failure {
error Missing(message: Text)
error Conflict(message: Text)
}
public type Outcome {
Done(value: Int)
error Failed(error: Failure)
}
load(conflict: true) (value: Int) { fail(Failure.Conflict("duplicate")) }
load(conflict: false) (value: Int) { fail(Failure.Missing("absent")) }
public main() (outcome: Outcome) { outcome = result(load(false)) }
"#;
assert_eq!(
support::stdout(source),
"outcome = main.Outcome.Failed(main.Failure.Missing(absent))\n"
);
#[cfg(feature = "reference")]
{
let sources = sources(source);
let result = dever_core::reference::evaluate(&checked(&sources), "main.main").unwrap();
assert!(result.fault.is_none(), "{:?}", result.fault);
assert_eq!(
result.stdout,
"outcome = main.Outcome.Failed(main.Failure.Missing(absent))\n"
);
}
rejected(
&source.replace(
"error Failed(error: Failure)",
"error Failed(error: Failure)\nerror Again(error: Failure)",
),
"overlapping error wrappers",
);
rejected(
&source.replace(
"outcome = result(load(false))",
"outcome = result(load(false))\noutcome = Outcome.Done(0)",
),
"failure is not handled",
);
let wrapped = source.replace(
"public main() (outcome: Outcome) { outcome = result(load(false)) }",
r#"
rewrap(captured: Outcome.Done(number)) (answer: Int) { answer = number }
rewrap(captured: Outcome.Failed(error)) (answer: Int) { fail(Outcome.Failed(error)) }
load_again() (answer: Int) { answer = rewrap(result(load(false))) }
public main() (outcome: Outcome) { outcome = result(load_again()) }
"#,
);
assert_eq!(
support::stdout(&wrapped),
"outcome = main.Outcome.Failed(main.Failure.Missing(absent))\n"
);
}