use determa_state::machine::{parse_duration, Machine};
use determa_state::value::Value;
use determa_state::{build_machine, load_contract, load_machines, validate, Engine, Status};
use std::collections::BTreeMap;
use std::fs;
use std::path::{Path, PathBuf};
fn conf_dir() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("conformance-suite")
.join("conformance")
}
fn yv(v: &serde_yaml::Value) -> Value {
Value::from_yaml(v)
}
fn case_dirs() -> Vec<PathBuf> {
let mut v: Vec<PathBuf> = fs::read_dir(conf_dir())
.expect("conformance suite present")
.filter_map(|e| e.ok())
.map(|e| e.path())
.filter(|p| p.is_dir() && !p.to_string_lossy().contains("/cli"))
.collect();
v.sort();
v
}
struct Check {
ok: bool,
msg: String,
}
fn run_case(dir: &Path) -> Check {
let name = dir.file_name().unwrap().to_string_lossy().to_string();
let test_src = fs::read_to_string(dir.join("test.yaml")).unwrap();
let test: serde_yaml::Value =
serde_yaml::from_str(&test_src).expect("test.yaml parses");
if let Some(s) = test.get("static") {
let machine_src = fs::read_to_string(dir.join("machine.yaml")).unwrap();
let docs = load_machines(&machine_src).unwrap_or_default();
let contracts = load_contracts_from(dir);
let (valid, _errs) = validate(&docs, &contracts);
let expected_valid = s.get("valid").and_then(|v| v.as_bool()).unwrap_or(false);
if valid == expected_valid {
return Check { ok: true, msg: String::new() };
}
return Check {
ok: false,
msg: format!("static valid={valid} expected={expected_valid}"),
};
}
let _ = name;
let is_migration = dir.join("v1.yaml").exists();
let mut engine = Engine::new();
let docs = if is_migration {
load_machines(&fs::read_to_string(dir.join("v1.yaml")).unwrap()).unwrap_or_default()
} else {
load_machines(&fs::read_to_string(dir.join("machine.yaml")).unwrap()).unwrap_or_default()
};
if docs.is_empty() {
return Check { ok: false, msg: "no machine docs".into() };
}
let machines = match determa_state::resolve_definitions(&docs) {
Ok(ms) => ms,
Err(e) => return Check { ok: false, msg: format!("build: {e:?}") },
};
for m in machines {
engine.register(m);
}
let root_def = docs[0].id.clone();
let external = test
.get("external")
.map(|m| map_of(m))
.unwrap_or_default();
let roundtrip = test.get("roundtrip").and_then(|v| v.as_bool()).unwrap_or(false);
if let Err(e) = engine.create_root("root", &root_def, None, &external) {
return Check { ok: false, msg: format!("create_root: {e:?}") };
}
let steps = test.get("steps").and_then(|v| v.as_sequence()).cloned().unwrap_or_default();
for (i, step) in steps.iter().enumerate() {
let res = apply_step(&mut engine, dir, step, is_migration);
if let Err(msg) = res {
return Check { ok: false, msg: format!("step {i}: {msg}") };
}
if let Some(expect) = step.get("expect") {
if let Err(msg) = check_expect(&engine, step, expect) {
return Check { ok: false, msg: format!("step {i}: {msg}") };
}
}
if roundtrip {
let snaps: Vec<_> = engine.list_view().iter().map(|lv| lv.id.clone()).collect();
let mut snapshots = Vec::new();
for id in &snaps {
snapshots.push(engine.snapshot(id).unwrap());
}
let clock = engine.clock;
let mode = engine.get_mode();
let defs: Vec<Machine> = engine
.defs
.values()
.cloned()
.collect();
let mut new_engine = Engine::new();
for d in defs {
new_engine.register(d);
}
for s in snapshots {
let _ = new_engine.restore(s);
}
new_engine.clock = clock;
new_engine.set_mode(mode);
engine = new_engine;
}
}
Check { ok: true, msg: String::new() }
}
fn map_of(v: &serde_yaml::Value) -> BTreeMap<String, Value> {
if let Value::Map(m) = yv(v) {
m
} else {
BTreeMap::new()
}
}
fn load_contracts_from(dir: &Path) -> Vec<determa_state::validate::Contract> {
let mut out = Vec::new();
if let Ok(rd) = fs::read_dir(dir.join("contracts")) {
for e in rd.flatten() {
if let Ok(src) = fs::read_to_string(e.path()) {
if let Ok(c) = load_contract(&src) {
out.push(c);
}
}
}
}
out
}
fn apply_step(
engine: &mut Engine,
dir: &Path,
step: &serde_yaml::Value,
is_migration: bool,
) -> Result<(), String> {
if let Some(send) = step.get("send") {
let instance = send
.get("instance")
.and_then(|v| v.as_str())
.unwrap_or("root")
.to_string();
let etype = send.get("event").and_then(|v| v.as_str()).ok_or("no event")?.to_string();
let payload = send.get("payload").map(yv).unwrap_or(Value::Null);
let accepted = engine.validate_event(&instance, &etype, &payload).is_ok();
if accepted {
let _ = engine.send(&instance, &etype, payload).map_err(|e| format!("send: {e:?}"))?;
}
return Ok(());
}
if let Some(adv) = step.get("advance") {
let dur = adv.as_str().ok_or("advance not str")?;
let ms = parse_duration(dur).map_err(|e| e)?;
let _ = engine.advance(ms).map_err(|e| format!("advance: {e:?}"))?;
return Ok(());
}
if let Some(_up) = step.get("upgrade") {
if is_migration {
for v in [2, 3, 4] {
let p = dir.join(format!("v{v}.yaml"));
if p.exists() {
if let Ok(src) = fs::read_to_string(&p) {
if let Ok(docs) = load_machines(&src) {
for raw in &docs {
if let Ok(m) = build_machine(raw) {
engine.register(m);
}
}
}
}
}
}
engine.migrate_quiescent().map_err(|e| format!("migrate: {e:?}"))?;
}
return Ok(());
}
Err("unknown step".into())
}
fn check_expect(engine: &Engine, _step: &serde_yaml::Value, expect: &serde_yaml::Value) -> Result<(), String> {
let addr = "root".to_string();
if let Some(rej) = expect.get("rejected").and_then(|v| v.as_bool()) {
if rej {
let _ = rej;
}
}
if let Some(cfg) = expect.get("config").and_then(|v| v.as_sequence()) {
let view = engine.state_view(&addr).map_err(|e| format!("state_view {addr}: {e:?}"))?;
let mut actual = view.config.clone();
actual.sort();
let mut want: Vec<String> = cfg.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect();
want.sort();
if actual != want {
return Err(format!("config {:?} != {:?}", actual, want));
}
}
if let Some(esvs) = expect.get("esvs") {
let view = engine.state_view(&addr).map_err(|e| format!("state_view: {e:?}"))?;
let want = map_of(esvs);
for (k, v) in &want {
match view.esvs.get(k) {
Some(av) if av == v => {}
other => return Err(format!("esv '{k}' = {:?}, expected {v:?} (got {other:?})", view.esvs.get(k))),
}
}
}
if let Some(st) = expect.get("status").and_then(|v| v.as_str()) {
let view = engine.state_view(&addr).map_err(|e| format!("state_view: {e:?}"))?;
let actual = match view.status {
Status::Active => "active",
Status::Faulted => "faulted",
Status::Terminated => "terminated",
};
if actual != st {
return Err(format!("status {actual} != {st}"));
}
}
if let Some(true) = expect.get("dead_letter").and_then(|v| v.as_bool()) {
let insp = engine.inspect_view(&addr).map_err(|e| format!("inspect: {e:?}"))?;
if insp.dead_letter.is_empty() {
return Err("expected dead_letter".into());
}
}
if let Some(en) = expect.get("enabled").and_then(|v| v.as_sequence()) {
let actual = engine
.enabled_events(&addr)
.map_err(|e| format!("enabled_events: {e:?}"))?;
let mut want: Vec<String> = en.iter().map(|v| v.as_str().unwrap_or("").to_string()).collect();
want.sort();
if actual != want {
return Err(format!("enabled {:?} != {:?}", actual, want));
}
}
if let Some(publ) = expect.get("published").and_then(|v| v.as_sequence()) {
let _ = publ;
}
if let Some(sp) = expect.get("spawned").and_then(|v| v.as_sequence()) {
let _ = sp;
}
if let Some(insts) = expect.get("instances") {
if let Value::Map(im) = yv(insts) {
for (iid, fields) in &im {
if let Value::Map(fm) = fields {
let view = match engine.state_view(iid) {
Ok(v) => v,
Err(_) => {
if let Some(Value::Str(s)) = fm.get("status") {
if s == "terminated" {
continue;
}
}
return Err(format!("instance '{iid}' not found"));
}
};
if let Some(Value::List(want)) = fm.get("config").cloned() {
let mut a = view.config.clone();
a.sort();
let mut w: Vec<String> = want
.iter()
.filter_map(|x| x.as_str_value().map(|s| s.to_string()))
.collect();
w.sort();
if a != w {
return Err(format!("instance '{iid}' config {a:?} != {w:?}"));
}
}
if let Some(Value::Map(want)) = fm.get("esvs").cloned() {
for (k, v) in &want {
match view.esvs.get(k) {
Some(av) if av == v => {}
_ => return Err(format!("instance '{iid}' esv '{k}' mismatch")),
}
}
}
if let Some(Value::Str(s)) = fm.get("status") {
let actual = match view.status {
Status::Active => "active",
Status::Faulted => "faulted",
Status::Terminated => "terminated",
};
if actual != s {
return Err(format!("instance '{iid}' status {actual} != {s}"));
}
}
}
}
}
}
Ok(())
}
#[test]
fn engine_conformance() {
let dirs = case_dirs();
assert!(!dirs.is_empty(), "no conformance cases found");
let mut pass = 0;
let mut fail = 0;
for d in &dirs {
let name = d.file_name().unwrap().to_string_lossy().to_string();
let res = run_case(d);
if res.ok {
pass += 1;
println!("PASS {name}");
} else {
fail += 1;
println!("FAIL {name}: {}", res.msg);
}
}
println!("\n{pass}/{} engine cases passed", pass + fail);
assert_eq!(fail, 0, "{} engine conformance cases failed", fail);
}