317 lines
12 KiB
Rust
317 lines
12 KiB
Rust
//! Existing source-7 admission experiment; imports no desktop producer.
|
|
use oa_core::{source_project::*, *};
|
|
use std::{fs, path::Path};
|
|
|
|
struct Case {
|
|
kind: &'static str,
|
|
fields: Vec<(&'static str, &'static str, bool)>,
|
|
expected: OccurrenceBody,
|
|
}
|
|
fn cases() -> Vec<Case> {
|
|
vec![
|
|
Case {
|
|
kind: "Exchange",
|
|
fields: vec![
|
|
("exchange", "none", false),
|
|
("sender", "none", false),
|
|
("receiver", "none", false),
|
|
("message_kind", "Asynchronous", false),
|
|
("reply_to", "none", false),
|
|
("values", "[]", false),
|
|
("operation", "none", false),
|
|
],
|
|
expected: OccurrenceBody::Exchange {
|
|
exchange: None,
|
|
sender: None,
|
|
receiver: None,
|
|
message_kind: MessageKind::Asynchronous,
|
|
reply_to: None,
|
|
values: vec![],
|
|
operation: None,
|
|
},
|
|
},
|
|
Case {
|
|
kind: "Action",
|
|
fields: vec![("action", "none", false), ("lifeline", "none", false)],
|
|
expected: OccurrenceBody::Action {
|
|
action: None,
|
|
lifeline: None,
|
|
},
|
|
},
|
|
Case {
|
|
kind: "State",
|
|
fields: vec![("state", "none", false), ("lifeline", "none", false)],
|
|
expected: OccurrenceBody::State {
|
|
state: None,
|
|
lifeline: None,
|
|
},
|
|
},
|
|
Case {
|
|
kind: "SequenceReference",
|
|
fields: vec![("sequence", "none", false)],
|
|
expected: OccurrenceBody::SequenceReference { sequence: None },
|
|
},
|
|
Case {
|
|
kind: "ScenarioUse",
|
|
fields: vec![("scenario", "none", false), ("bindings", "[]", true)],
|
|
expected: OccurrenceBody::ScenarioUse {
|
|
scenario: None,
|
|
bindings: vec![],
|
|
},
|
|
},
|
|
Case {
|
|
kind: "Control",
|
|
fields: vec![("kind", "Choice", true)],
|
|
expected: OccurrenceBody::Control {
|
|
kind: ControlKind::Choice,
|
|
},
|
|
},
|
|
Case {
|
|
kind: "Fragment",
|
|
fields: vec![("operator", "Alternative", true), ("branches", "[]", true)],
|
|
expected: OccurrenceBody::Fragment {
|
|
operator: FragmentOperator::Alternative,
|
|
branches: vec![],
|
|
},
|
|
},
|
|
Case {
|
|
kind: "Branch",
|
|
fields: vec![("guard", "\"\"", true), ("occurrences", "[]", true)],
|
|
expected: OccurrenceBody::Branch {
|
|
guard: String::new(),
|
|
occurrences: vec![],
|
|
},
|
|
},
|
|
]
|
|
}
|
|
fn id(last: u8) -> Id {
|
|
Id::parse_str(&format!("00000000-0000-4000-8000-{last:012}")).unwrap()
|
|
}
|
|
fn fixture(layer: Layer, kind: &str, fields: &str) -> String {
|
|
let prefix = if layer == Layer::Oa { "oa" } else { "sa" };
|
|
let sequence = if layer == Layer::Oa {
|
|
"oa_process"
|
|
} else {
|
|
"sa_functional_chain"
|
|
};
|
|
let viewpoint = if layer == Layer::Oa {
|
|
"OperationalActivity"
|
|
} else {
|
|
"Functional"
|
|
};
|
|
let root_sequence = matches!(
|
|
kind,
|
|
"Exchange" | "Action" | "Control" | "SequenceReference"
|
|
);
|
|
let owner_kind = if root_sequence {
|
|
sequence.to_owned()
|
|
} else {
|
|
format!("{prefix}_scenario")
|
|
};
|
|
let owner_view = if root_sequence {
|
|
String::new()
|
|
} else {
|
|
format!("viewpoint {viewpoint};")
|
|
};
|
|
let step = if kind == "Branch" {
|
|
"holder"
|
|
} else {
|
|
"subject"
|
|
};
|
|
let parent = if kind == "Branch" { "holder" } else { "owner" };
|
|
let holder = if kind == "Branch" {
|
|
format!(
|
|
"{prefix}_occurrence holder \"Fragment owner\" id \"{}\" {{ parent @owner; body {{ Fragment {{ operator Alternative; branches [@subject]; }}; }}; }}",
|
|
id(4)
|
|
)
|
|
} else {
|
|
String::new()
|
|
};
|
|
format!(
|
|
"arcadia 7;\nproject defaults \"Constructor defaults\" id \"{}\" {{\n {owner_kind} owner \"Owner\" id \"{}\" {{ {owner_view} steps [@{step}]; }}\n {holder}\n // Authored omissions must stay omitted through no-op and a name edit.\n {prefix}_occurrence subject \"Subject\" id \"{}\" {{ parent @{parent}; body {{ {kind} {{ {fields} }}; }}; }}\n}}\n",
|
|
id(1),
|
|
id(2),
|
|
id(3)
|
|
)
|
|
}
|
|
fn bundle(source: &str) -> SourceBundle {
|
|
SourceBundle {
|
|
entry: "project.arcadia".into(),
|
|
files: [("project.arcadia".into(), source.into())]
|
|
.into_iter()
|
|
.collect(),
|
|
}
|
|
}
|
|
fn compile(source: &str) -> Result<CompiledSourceProject, Vec<SourceDiagnostic>> {
|
|
compile_bundle(&bundle(source), ProjectLimits::default())
|
|
}
|
|
fn body(project: &Project) -> &OccurrenceBody {
|
|
let Some(SharedBody::Occurrence { body, .. }) = project.elements[&id(3)].shared() else {
|
|
panic!("Subject is not an occurrence")
|
|
};
|
|
body
|
|
}
|
|
fn accepted(out: &Path, name: &str, source: &str, expected: &Project) -> usize {
|
|
fs::write(out.join(format!("{name}.arcadia")), source).unwrap();
|
|
let compiled = compile(source).unwrap_or_else(|e| panic!("{name}: {e:?}"));
|
|
assert_eq!(
|
|
&compiled.decoded.project, expected,
|
|
"{name}: exact complete Project"
|
|
);
|
|
assert_eq!(
|
|
compiled.bundle,
|
|
bundle(source),
|
|
"{name}: explicit UUIDs need no stabilization edit"
|
|
);
|
|
execute_batch(expected, &[]).unwrap();
|
|
let noop = update_bundle(&compiled, expected, ProjectLimits::default()).unwrap();
|
|
assert_eq!(
|
|
noop.bundle, compiled.bundle,
|
|
"{name}: no-op preserves all authored omissions"
|
|
);
|
|
let renamed = execute_batch(
|
|
expected,
|
|
&[Command::Rename(id(3), "Renamed subject".into())],
|
|
)
|
|
.unwrap();
|
|
let edited = update_bundle(&compiled, &renamed, ProjectLimits::default()).unwrap();
|
|
assert_eq!(
|
|
edited.bundle,
|
|
bundle(&source.replace("\"Subject\"", "\"Renamed subject\"")),
|
|
"{name}: name-only edit preserves constructor spelling"
|
|
);
|
|
let canonical = language::format(expected);
|
|
fs::write(out.join(format!("{name}.canonical.arcadia")), &canonical).unwrap();
|
|
assert_eq!(
|
|
compile(&canonical).unwrap().decoded.project,
|
|
*expected,
|
|
"{name}: canonical roundtrip"
|
|
);
|
|
if matches!(
|
|
body(expected),
|
|
OccurrenceBody::Exchange {
|
|
operation: None,
|
|
..
|
|
}
|
|
) {
|
|
assert!(canonical.contains("message_kind Asynchronous;"));
|
|
assert!(canonical.contains("reply_to none;") && canonical.contains("values [];"));
|
|
assert!(
|
|
!canonical.contains("operation "),
|
|
"None operation is deliberately skipped by canonical serialization"
|
|
);
|
|
}
|
|
validation::validate(expected).len()
|
|
}
|
|
fn main() {
|
|
let out_arg = std::env::args().nth(1).expect("output directory");
|
|
let out = Path::new(&out_arg);
|
|
fs::create_dir_all(out).unwrap();
|
|
let mut rows = String::from("case\tlayer\tresult\tdetail\n");
|
|
let mut passes = 0;
|
|
let mut rejects = 0;
|
|
for layer in [Layer::Oa, Layer::Sa] {
|
|
for case in cases() {
|
|
let explicit_fields = case
|
|
.fields
|
|
.iter()
|
|
.map(|(name, value, _)| format!("{name} {value};"))
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
let explicit = fixture(layer, case.kind, &explicit_fields);
|
|
let baseline = compile(&explicit)
|
|
.unwrap_or_else(|e| panic!("{layer} {} explicit: {e:?}", case.kind));
|
|
assert_eq!(body(&baseline.decoded.project), &case.expected);
|
|
let name = format!("{layer}-{}-explicit", case.kind);
|
|
let issues = accepted(out, &name, &explicit, &baseline.decoded.project);
|
|
rows.push_str(&format!("{name}\t{layer}\taccept\texact Project, raw/no-op/name/canonical; {issues} model issues\n"));
|
|
passes += 1;
|
|
for (omit, _, required) in &case.fields {
|
|
let fields = case
|
|
.fields
|
|
.iter()
|
|
.filter(|(field, _, _)| field != omit)
|
|
.map(|(field, value, _)| format!("{field} {value};"))
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
let source = fixture(layer, case.kind, &fields);
|
|
let name = format!("{layer}-{}-omit-{omit}", case.kind);
|
|
if *required {
|
|
fs::write(out.join(format!("{name}.arcadia")), &source).unwrap();
|
|
let errors = compile(&source).expect_err("Required field omission must reject");
|
|
let detail = format!("{errors:?}");
|
|
assert!(
|
|
detail.contains("missing field") && detail.contains(omit),
|
|
"{name}: must fail on the missing constructor field: {detail}"
|
|
);
|
|
rows.push_str(&format!("{name}\t{layer}\treject\t{detail}\n"));
|
|
rejects += 1;
|
|
} else {
|
|
let issues = accepted(out, &name, &source, &baseline.decoded.project);
|
|
rows.push_str(&format!("{name}\t{layer}\taccept\texact Project, raw/no-op/name/canonical; {issues} model issues\n"));
|
|
passes += 1;
|
|
}
|
|
}
|
|
if case.fields.iter().any(|(_, _, required)| !required) {
|
|
let fields = case
|
|
.fields
|
|
.iter()
|
|
.filter(|(_, _, required)| *required)
|
|
.map(|(field, value, _)| format!("{field} {value};"))
|
|
.collect::<Vec<_>>()
|
|
.join(" ");
|
|
let name = format!("{layer}-{}-omit-all-optional", case.kind);
|
|
let issues = accepted(
|
|
out,
|
|
&name,
|
|
&fixture(layer, case.kind, &fields),
|
|
&baseline.decoded.project,
|
|
);
|
|
rows.push_str(&format!("{name}\t{layer}\taccept\texact Project, raw/no-op/name/canonical; {issues} model issues\n"));
|
|
passes += 1;
|
|
}
|
|
}
|
|
let prefix = if layer == Layer::Oa { "oa" } else { "sa" };
|
|
let source = format!(
|
|
"arcadia 7;\nproject defaults \"Defaults\" id \"{}\" {{ {prefix}_scenario scene \"Scene\" id \"{}\" {{ }} }}\n",
|
|
id(1),
|
|
id(2)
|
|
);
|
|
let compiled = compile(&source).unwrap();
|
|
let explicit = source.replace("{ }", "{ viewpoint MixedLegacy; }");
|
|
assert_eq!(
|
|
compiled.decoded.project,
|
|
compile(&explicit).unwrap().decoded.project
|
|
);
|
|
assert_eq!(compiled.bundle, bundle(&source));
|
|
assert!(matches!(
|
|
compiled.decoded.project.elements[&id(2)].shared(),
|
|
Some(SharedBody::Scenario {
|
|
viewpoint: ScenarioViewpoint::MixedLegacy,
|
|
..
|
|
})
|
|
));
|
|
let canonical = language::format(&compiled.decoded.project);
|
|
assert_eq!(
|
|
compile(&canonical).unwrap().decoded.project,
|
|
compiled.decoded.project
|
|
);
|
|
fs::write(
|
|
out.join(format!("{layer}-scenario-viewpoint-omitted.arcadia")),
|
|
source,
|
|
)
|
|
.unwrap();
|
|
fs::write(
|
|
out.join(format!("{layer}-scenario-viewpoint-explicit.arcadia")),
|
|
explicit,
|
|
)
|
|
.unwrap();
|
|
rows.push_str(&format!("{layer}-scenario-viewpoint\t{layer}\taccept\texact MixedLegacy Project, retained omission, canonical roundtrip\n"));
|
|
passes += 1;
|
|
}
|
|
fs::write(out.join("results.tsv"), rows).unwrap();
|
|
println!(
|
|
"PASS: {passes} accepted explicit/omitted comparisons; {rejects} missing-required-field refusals; both source-7 layers. Full Project, canonical roundtrip, no-op and exact name-only source retention checked."
|
|
);
|
|
}
|