231 lines
12 KiB
Rust
231 lines
12 KiB
Rust
mod support;
|
|
|
|
use support::{checked, sources};
|
|
|
|
fn application(files: &[(&str, &str)]) -> dever_core::hir::Program {
|
|
let root = support::temp::TemporaryDirectory::new();
|
|
for (relative, source) in files {
|
|
let path = root.path().join("module").join(relative);
|
|
std::fs::create_dir_all(path.parent().unwrap()).unwrap();
|
|
std::fs::write(path, source).unwrap();
|
|
}
|
|
checked(&dever_core::source::SourceMap::load(&root.path().join("module")).unwrap())
|
|
}
|
|
|
|
fn warnings(source: &str, code: &str) -> Vec<dever_core::diagnostic::Diagnostic> {
|
|
let sources = sources(source);
|
|
let user_source = sources.files()[0].id();
|
|
checked(&sources)
|
|
.warnings()
|
|
.iter()
|
|
.filter(|warning| warning.code == code && warning.primary.source == user_source)
|
|
.cloned()
|
|
.collect()
|
|
}
|
|
|
|
#[test]
|
|
fn identical_private_implementations_are_reported_with_source_evidence() {
|
|
let source = "first(value: Int) (answer: Int) { answer = value + 1 }\nsecond(renamed: Int) (answer: Int) { answer = renamed + 1 }";
|
|
let duplicates = warnings(source, "W003");
|
|
assert_eq!(duplicates.len(), 1);
|
|
assert_eq!(duplicates[0].related.len(), 1);
|
|
assert!(duplicates[0].message.contains("main.first"));
|
|
assert!(duplicates[0].message.contains("main.second"));
|
|
assert_ne!(duplicates[0].primary, duplicates[0].related[0].span);
|
|
}
|
|
|
|
#[test]
|
|
fn visibility_contract_and_operator_differences_are_not_duplicates() {
|
|
for source in [
|
|
"public first(value: Int) (answer: Int) { answer = value + 1 }\nsecond(value: Int) (answer: Int) { answer = value + 1 }",
|
|
"first(value: Int) (answer: Int) pure { answer = value + 1 }\nsecond(value: Int) (answer: Int) { answer = value + 1 }",
|
|
"first(value: Int) (answer: Int) { answer = value + 1 }\nsecond(value: Int) (answer: Int) { answer = value - 1 }",
|
|
"first(value: Int) (answer: Int) { answer = value }\nsecond(value: Int) (different: Int) { different = value }",
|
|
] {
|
|
assert!(warnings(source, "W003").is_empty(), "{source}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn repeated_pure_calls_require_unchanged_inputs_and_an_available_result() {
|
|
let prefix = "calculate(value: Int) (answer: Int) { answer = value + 1 }\n";
|
|
let repeated = format!(
|
|
"{prefix}main(value: Int) (answer: Int) {{ first = calculate(value)\nsecond = calculate(value)\nanswer = first + second }}"
|
|
);
|
|
let diagnostics = warnings(&repeated, "W004");
|
|
assert_eq!(diagnostics.len(), 1);
|
|
assert_eq!(diagnostics[0].related.len(), 1);
|
|
for body in [
|
|
"first = calculate(value)\nvalue = value + 1\nsecond = calculate(value)\nanswer = first + second",
|
|
"first = calculate(value)\nfirst = 0\nsecond = calculate(value)\nanswer = first + second",
|
|
] {
|
|
assert!(
|
|
warnings(
|
|
&format!("{prefix}main(value: Int) (answer: Int) {{ {body} }}"),
|
|
"W004"
|
|
)
|
|
.is_empty()
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn field_writes_and_effectful_calls_are_not_reuse_candidates() {
|
|
let field = "public type Counter { value: Int }\ncalculate(value: Int) (answer: Int) { answer = value + 1 }\npublic main(counter: Counter) (answer: Int) { first = calculate(counter.value)\ncounter.value = counter.value + 1\nsecond = calculate(counter.value)\nanswer = first + second }";
|
|
assert!(warnings(field, "W004").is_empty());
|
|
let effects = "noisy(value: Int) (answer: Int) { dever.io.println(\"called\")\nanswer = value }\npublic main(value: Int) (answer: Int) { first = noisy(value)\nsecond = noisy(value)\nanswer = first + second }";
|
|
assert!(warnings(effects, "W004").is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn forwarding_warning_excludes_public_adapters_recovery_and_constrained_outputs() {
|
|
let direct = "calculate(value: Int) (answer: Int) { answer = value + 1 }\nwrapper(value: Int) (answer: Int) { answer = calculate(value) }";
|
|
assert_eq!(warnings(direct, "W005").len(), 1);
|
|
for source in [
|
|
"calculate(value: Int) (answer: Int) { answer = value + 1 }\npublic wrapper(value: Int) (answer: Int) { answer = calculate(value) }",
|
|
"calculate(value: Int) (answer: Int) { answer = value + 1 }\nwrapper(value: Int) (answer: Int) recover(\"application policy\") { answer = calculate(value) }",
|
|
"calculate() (answer: Int >= 1) { answer = 1 }\nwrapper() (answer: Int >= 1) { answer = calculate() }",
|
|
"type PrivateValue { private value: Int }\ncreate(value: Int) (answer: PrivateValue) { answer = PrivateValue { value = value } }\nwrapper(value: Int) (answer: PrivateValue) { answer = create(value) }",
|
|
] {
|
|
assert!(warnings(source, "W005").is_empty(), "{source}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn existing_unused_write_warning_is_not_duplicated_as_dead_computation() {
|
|
let program = checked(&sources(
|
|
"calculate(value: Int) (answer: Int) { answer = value + 1 }\npublic main() () { unused = calculate(1) }",
|
|
));
|
|
assert!(
|
|
program
|
|
.warnings()
|
|
.iter()
|
|
.any(|warning| warning.code == "W001")
|
|
);
|
|
assert!(
|
|
!program
|
|
.warnings()
|
|
.iter()
|
|
.any(|warning| warning.code == "W006")
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn application_rejects_reachable_private_copies_with_renamed_inputs() {
|
|
let program = application(&[
|
|
("sample/count/domain.dever", "first(value: Int) (answer: Int) pure { answer = value + 1 }\nsecond(renamed: Int) (answer: Int) pure { answer = renamed + 1 }"),
|
|
("sample/count/app.dever", "total(value: Int) (answer: Int) { first = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }"),
|
|
("sample/count/api.dever", "cmd total = app.total"),
|
|
]);
|
|
let warnings: Vec<_> = program.warnings().iter().filter(|item| item.code == "W003").collect();
|
|
let errors = program.application_errors().unwrap();
|
|
assert_eq!(errors.len(), 1, "{errors:?}");
|
|
assert_eq!(errors[0].code, "C016");
|
|
assert!(errors[0].message.contains("same implementation and contract"));
|
|
assert!(errors[0].message.contains("domain.first"));
|
|
assert!(errors[0].message.contains("domain.second"));
|
|
assert_eq!(errors[0].related.len(), 1);
|
|
assert!(warnings.iter().any(|warning| warning.primary == errors[0].primary));
|
|
assert_ne!(errors[0].primary, errors[0].related[0].span);
|
|
}
|
|
|
|
#[test]
|
|
fn application_rejects_repeated_pure_calls_using_the_warning_proof() {
|
|
let program = application(&[
|
|
("sample/count/domain.dever", "calculate(value: Int) (answer: Int) pure { answer = value + 1 }"),
|
|
("sample/count/app.dever", "total(value: Int) (answer: Int) { first = domain.calculate(value)\nsecond = domain.calculate(value)\nanswer = first + second }"),
|
|
("sample/count/api.dever", "cmd total = app.total"),
|
|
]);
|
|
let errors = program.application_errors().unwrap();
|
|
assert_eq!(errors.len(), 1, "{errors:?}");
|
|
assert_eq!(errors[0].code, "C016");
|
|
assert!(errors[0].message.contains("earlier result is still available"));
|
|
assert_eq!(errors[0].related.len(), 1);
|
|
assert!(program.warnings().iter().any(|warning| {
|
|
warning.code == "W004" && warning.primary == errors[0].primary
|
|
}));
|
|
}
|
|
|
|
#[test]
|
|
fn application_does_not_reject_changed_inputs_results_or_effectful_calls() {
|
|
for (app, domain, api) in [
|
|
(
|
|
"total(value: Int) (answer: Int) { first = domain.calculate(value)\nvalue = value + 1\nsecond = domain.calculate(value)\nanswer = first + second }",
|
|
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
|
|
"cmd total = app.total",
|
|
),
|
|
(
|
|
"total(value: Int) (answer: Int) { first = domain.calculate(value)\nfirst = 0\nsecond = domain.calculate(value)\nanswer = first + second }",
|
|
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
|
|
"cmd total = app.total",
|
|
),
|
|
(
|
|
"type Counter { value: Int }\ntotal(counter: Counter) (answer: Int) { first = domain.calculate(counter.value)\ncounter.value = counter.value + 1\nsecond = domain.calculate(counter.value)\nanswer = first + second }",
|
|
"calculate(value: Int) (answer: Int) pure { answer = value + 1 }",
|
|
"cmd total = app.total",
|
|
),
|
|
(
|
|
"noisy(value: Int) (answer: Int) { dever.io.println(\"called\")\nanswer = value }\ntotal(value: Int) (answer: Int) { first = noisy(value)\nsecond = noisy(value)\nanswer = first + second }",
|
|
"",
|
|
"cmd total = app.total",
|
|
),
|
|
] {
|
|
let program = application(&[
|
|
("sample/count/app.dever", app),
|
|
("sample/count/domain.dever", domain),
|
|
("sample/count/api.dever", api),
|
|
]);
|
|
let errors = program.application_errors().unwrap();
|
|
assert!(errors.is_empty(), "{app}: {errors:?}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn application_preserves_different_contracts_and_owners() {
|
|
for domain in [
|
|
"first(value: Int) (answer: Int) pure { answer = value }\nsecond(value: Int) (different: Int) pure { different = value }",
|
|
"first(value: Int) (answer: Int) pure recover(\"first policy\") { answer = value }\nsecond(value: Int) (answer: Int) pure recover(\"second policy\") { answer = value }",
|
|
"first() (answer: Int >= 1) pure { answer = 1 }\nsecond() (answer: Int >= 0) pure { answer = 1 }",
|
|
] {
|
|
let inputs = if domain.contains("value:") { "1" } else { "" };
|
|
let app = format!("total() (answer: Int) {{ first = domain.first({inputs})\nsecond = domain.second({inputs})\nanswer = first + second }}");
|
|
let program = application(&[
|
|
("sample/count/domain.dever", domain),
|
|
("sample/count/app.dever", &app),
|
|
("sample/count/api.dever", "cmd total = app.total"),
|
|
]);
|
|
assert!(program.application_errors().unwrap().is_empty(), "{domain}");
|
|
}
|
|
let program = application(&[
|
|
("first/count/domain.dever", "calculate(value: Int) (answer: Int) pure { answer = value + 1 }"),
|
|
("first/count/app.dever", "total(value: Int) (answer: Int) { answer = domain.calculate(value) }"),
|
|
("first/count/api.dever", "cmd total = app.total"),
|
|
("second/count/domain.dever", "calculate(value: Int) (answer: Int) pure { answer = value + 1 }"),
|
|
("second/count/app.dever", "total(value: Int) (answer: Int) { answer = domain.calculate(value) }"),
|
|
("second/count/api.dever", "cmd total = app.total"),
|
|
]);
|
|
assert!(program.application_errors().unwrap().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn application_keeps_effectful_private_copies_and_adapter_redundancy_as_warnings() {
|
|
let program = application(&[
|
|
("sample/count/domain.dever", "emit(value: Int) (answer: Int) { dever.io.println(\"called\")\nanswer = value }\nfirst(value: Int) (answer: Int) { answer = emit(value) + 1 }\nsecond(renamed: Int) (answer: Int) { answer = emit(renamed) + 1 }"),
|
|
("sample/count/app.dever", "total(value: Int) (answer: Int) { first = domain.first(value)\nsecond = domain.second(value)\nanswer = first + second }"),
|
|
("sample/count/api.dever", "cmd total = app.total"),
|
|
]);
|
|
assert!(program.warnings().iter().any(|warning| warning.code == "W003"));
|
|
assert!(program.application_errors().unwrap().is_empty());
|
|
|
|
let program = application(&[
|
|
("sample/count/port.dever", "read(value: Int) (answer: Int) fails dever.time.SleepResult"),
|
|
("sample/count/adapter.dever", "first(value: Int) (answer: Int) { answer = value + 1 }\nsecond(value: Int) (answer: Int) { answer = value + 1 }\nport.read(value: Int) (answer: Int) { one = first(value)\ntwo = second(value)\nthree = first(value)\nanswer = one + two + three }"),
|
|
("sample/count/app.dever", "read(value: Int) (answer: Int) { answer = port.read(value) }"),
|
|
("sample/count/api.dever", "cmd read = app.read"),
|
|
]);
|
|
for code in ["W003", "W004"] {
|
|
assert!(program.warnings().iter().any(|warning| warning.code == code));
|
|
}
|
|
assert!(program.application_errors().unwrap().is_empty());
|
|
}
|