169 lines
5.5 KiB
Rust
169 lines
5.5 KiB
Rust
use dever_core::source::SourceMap;
|
|
use dever_core::syntax::{Declaration, InputKind, Package, TypeShape};
|
|
|
|
fn parsed(source: &str) -> Package {
|
|
let mut sources = SourceMap::default();
|
|
let id = sources.add("main.dever", source);
|
|
dever_core::parse(sources.get(id)).unwrap_or_else(|errors| {
|
|
panic!(
|
|
"{}",
|
|
errors
|
|
.iter()
|
|
.map(|error| error.render(&sources))
|
|
.collect::<String>()
|
|
)
|
|
})
|
|
}
|
|
|
|
fn formatted(source: &str) -> String {
|
|
let mut sources = SourceMap::default();
|
|
let id = sources.add("main.dever", source);
|
|
dever_core::format::format(sources.get(id)).expect("valid contract syntax")
|
|
}
|
|
|
|
#[test]
|
|
fn parses_package_type_and_function_contracts() {
|
|
let package = parsed(
|
|
r#"public type Port {
|
|
private value: Int >= 1 and <= 65535
|
|
scale: Decimal > -1.5
|
|
}
|
|
public type Result {
|
|
Ready(port: Port)
|
|
error Failed(message: Text)
|
|
}
|
|
public read(step: handler(value: Int >= 1) (result: Int <= 65535)) (result: Int >= 1) recover("configured fallback") pure {
|
|
result = 8080
|
|
}"#,
|
|
);
|
|
let Declaration::Type(port) = &package.declarations[0] else {
|
|
panic!("record")
|
|
};
|
|
let TypeShape::Record(fields) = &port.shape else {
|
|
panic!("record")
|
|
};
|
|
assert!(fields[0].private);
|
|
assert_eq!(fields[0].bounds.len(), 2);
|
|
assert!(!fields[1].private);
|
|
assert_eq!(fields[1].bounds[0].number, "-1.5");
|
|
let Declaration::Type(result) = &package.declarations[1] else {
|
|
panic!("choice")
|
|
};
|
|
let TypeShape::Choice(variants) = &result.shape else {
|
|
panic!("choice")
|
|
};
|
|
assert!(!variants[0].error);
|
|
assert!(variants[1].error);
|
|
assert_eq!(variants[1].name.text, "Failed");
|
|
let Declaration::Function(function) = &package.declarations[2] else {
|
|
panic!("function")
|
|
};
|
|
assert!(function.pure);
|
|
assert_eq!(function.recovery.as_deref(), Some("configured fallback"));
|
|
assert_eq!(function.outputs[0].bounds.len(), 1);
|
|
let InputKind::Handler(handler) = &function.inputs[0].kind else {
|
|
panic!("handler")
|
|
};
|
|
assert_eq!(handler.inputs[0].bounds.len(), 1);
|
|
assert_eq!(handler.outputs[0].bounds.len(), 1);
|
|
}
|
|
|
|
#[test]
|
|
fn preserves_contextual_identifiers() {
|
|
let package = parsed(
|
|
"type Record { private: Int }\ntype Choice { error(value: Int) }\ndepends() () {}\npure() () {}\nrecover() () {}",
|
|
);
|
|
let Declaration::Type(record) = &package.declarations[0] else {
|
|
panic!("record")
|
|
};
|
|
let TypeShape::Record(fields) = &record.shape else {
|
|
panic!("record")
|
|
};
|
|
assert!(!fields[0].private);
|
|
assert_eq!(fields[0].name.text, "private");
|
|
let Declaration::Type(choice) = &package.declarations[1] else {
|
|
panic!("choice")
|
|
};
|
|
let TypeShape::Choice(variants) = &choice.shape else {
|
|
panic!("choice")
|
|
};
|
|
assert!(!variants[0].error);
|
|
assert_eq!(variants[0].name.text, "error");
|
|
}
|
|
|
|
#[test]
|
|
fn depends_remains_an_ordinary_function_name() {
|
|
let source =
|
|
"depends() (value: Int) { value = 1 }\npublic main() (value: Int) { value = depends() }";
|
|
let output = formatted(source);
|
|
assert_eq!(formatted(&output), output);
|
|
|
|
let mut sources = SourceMap::default();
|
|
let id = sources.add("main.dever", output);
|
|
let package = dever_core::parse(sources.get(id)).unwrap();
|
|
assert!(dever_core::check(&sources).is_ok());
|
|
assert!(matches!(package.declarations[0], Declaration::Function(_)));
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_removed_package_dependency_clauses() {
|
|
let mut sources = SourceMap::default();
|
|
let id = sources.add("main.dever", "package app exposes () depends ()");
|
|
assert!(dever_core::parse(sources.get(id)).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn rejects_ambiguous_or_invalid_contract_markers() {
|
|
for declaration in [
|
|
"f() () pure pure {}",
|
|
"f() () recover(\"one\") recover(\"two\") {}",
|
|
"f() () recover(\"\") {}",
|
|
"f() () recover(\" \") {}",
|
|
"f() () recover(1) {}",
|
|
"f() (private result: Int) {}",
|
|
"f(step: handler(private value: Int) ()) () {}",
|
|
"type Result { Ready(private value: Int) }",
|
|
"type Result { private Ready }",
|
|
"type Port { value: Int >= 1 and }",
|
|
] {
|
|
let mut sources = SourceMap::default();
|
|
let id = sources.add("main.dever", declaration.to_string());
|
|
assert!(
|
|
dever_core::parse(sources.get(id)).is_err(),
|
|
"accepted: {declaration}"
|
|
);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn formatting_preserves_contracts_comments_and_canonical_marker_order() {
|
|
let source = r#"public type Port {
|
|
private value:Int >= 1 and <=65535 # valid range
|
|
}
|
|
public type Result {
|
|
Ready(value:Int >= 1)
|
|
error Failed(message:Text) # failure
|
|
}
|
|
public main() (result:Int >=1) recover("use \"configured\" port") # recovery
|
|
pure # effect
|
|
{result=8080}
|
|
"#;
|
|
let output = formatted(source);
|
|
assert_eq!(formatted(&output), output);
|
|
assert!(output.contains("private value: Int >= 1 and <= 65535"));
|
|
assert!(output.contains("error Failed("));
|
|
for comment in ["# valid range", "# failure", "# recovery", "# effect"] {
|
|
assert_eq!(output.matches(comment).count(), 1);
|
|
}
|
|
assert!(output.find("pure # effect").unwrap() < output.find("recover(").unwrap());
|
|
let package = parsed(&output);
|
|
let Declaration::Function(function) = &package.declarations[2] else {
|
|
panic!("function")
|
|
};
|
|
assert!(function.pure);
|
|
assert_eq!(
|
|
function.recovery.as_deref(),
|
|
Some("use \"configured\" port")
|
|
);
|
|
}
|