use std::path::{Path, PathBuf};
use crate::Compiler;
use workshop_rs::catalog::{Catalog, Locale};
use workshop_rs::roundtrip::equivalent;
use workshop_rs::{Action, Value};
fn collect_calls<'a>(value: &'a Value, name: &str, calls: &mut Vec<&'a [Value]>) {
match value {
Value::Call {
name: call_name,
args,
} => {
if call_name == name {
calls.push(args);
}
for argument in args {
collect_calls(argument, name, calls);
}
}
Value::Array(values) => {
for value in values {
collect_calls(value, name, calls);
}
}
Value::Vector { x, y, z } => {
collect_calls(x, name, calls);
collect_calls(y, name, calls);
collect_calls(z, name, calls);
}
Value::PlayerVariable { player, .. } => collect_calls(player, name, calls),
_ => {}
}
}
fn fixture_dir(name: &str) -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/corpus/synthetic")
.join(name)
}
fn compile_fixture(name: &str) -> crate::CompilationArtifact {
let dir = fixture_dir(name);
let source = std::fs::read_to_string(dir.join("source.opy")).expect("source must be readable");
let hir = crate::compile(&source, "source.opy", &dir).expect("fixture must resolve");
Compiler::new()
.expect("released workshop contract must load")
.compile_hir(&hir)
.expect("fixture must lower to canonical WIR")
}
fn compile_real_world(name: &str, source_name: &str) -> crate::CompilationArtifact {
let dir = Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/corpus/real-world")
.join(name);
let source = std::fs::read_to_string(dir.join(source_name)).expect("source must be readable");
let hir = crate::compile(&source, source_name, &dir).expect("real-world source must resolve");
Compiler::new()
.expect("released workshop contract must load")
.compile_hir(&hir)
.expect("real-world source must lower to canonical WIR")
}
fn oracle_workshop(name: &str) -> String {
let value: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(fixture_dir(name).join("oracle.json"))
.expect("oracle must be readable"),
)
.expect("oracle must parse");
value["compile"]["workshop"]
.as_str()
.expect("oracle must contain Workshop output")
.to_string()
}
fn assert_matches_oracle(name: &str) {
let artifact = compile_fixture(name);
let catalog = Catalog::builtin().expect("catalog must load");
let locale = Locale::new("en-US");
let native =
workshop_rs::parser::parse(&artifact.emitted, &catalog, &locale).unwrap_or_else(|error| {
panic!("native output must reparse: {error}\n{}", artifact.emitted)
});
let oracle = workshop_rs::parser::parse(&oracle_workshop(name), &catalog, &locale)
.expect("oracle output must reparse");
assert!(
equivalent(&native, &oracle),
"lowering diverged for {name}\n--- native ---\n{}\n--- oracle ---\n{}",
artifact.emitted,
oracle_workshop(name)
);
}
fn parsed_oracle(name: &str) -> workshop_rs::Program {
workshop_rs::parser::parse(
&oracle_workshop(name),
&Catalog::builtin().expect("catalog must load"),
&Locale::new("en-US"),
)
.expect("oracle output must reparse")
}
#[test]
fn bastion_condition_folding_matches_pinned_reference_contract() {
let name = "bastion-contextual-values";
let artifact = compile_fixture(name);
let oracle = parsed_oracle(name);
let split_artifact = compile_fixture("bastion-contextual-values-split");
let split_oracle = parsed_oracle("bastion-contextual-values-split");
assert!(equivalent(&artifact.wir, &oracle));
assert!(equivalent(&split_artifact.wir, &split_oracle));
assert!(equivalent(&oracle, &split_oracle));
assert_eq!(artifact.wir.rules[0].conditions.len(), 3);
assert_eq!(oracle.rules[0].conditions.len(), 3);
assert_eq!(split_artifact.wir.rules[0].conditions.len(), 3);
assert!(matches!(
oracle.rules[0].conditions[0].value,
workshop_rs::Value::Call { ref name, .. } if name == "=="
));
assert!(matches!(
oracle.rules[0].conditions[1].value,
workshop_rs::Value::Call { ref name, .. } if name == "=="
));
let transition = compile_fixture("bastion-condition-reevaluation");
let transition_oracle = parsed_oracle("bastion-condition-reevaluation");
assert!(equivalent(&transition.wir, &transition_oracle));
assert_eq!(transition.wir.rules[0].conditions.len(), 2);
assert_eq!(transition_oracle.rules[0].conditions.len(), 2);
assert!(
transition
.emitted
.contains("Wait(0.016, Ignore Condition);")
);
assert!(
transition
.emitted
.contains("Modify Global Variable(hits, Add, 1);")
);
assert!(
artifact
.emitted
.contains("Set Global Variable At Index(values, True, Null);")
);
assert!(
artifact
.emitted
.contains("Set Status(Event Player, Null, Invincible, False);")
);
assert!(artifact.emitted.contains(
"Create Effect(Null, Ring, Color(Red), Vector(False, True, False), False, Visible To);"
));
assert!(artifact.emitted.contains("Wait(False, Ignore Condition);"));
assert!(
artifact
.emitted
.contains("For Global Variable(index, False, True, False);")
);
}
#[test]
fn catalog_backed_receiver_calls_match_the_pinned_oracle() {
assert_matches_oracle("receiver-calls");
}
#[test]
fn catalog_backed_contextual_chase_calls_match_the_pinned_oracle() {
let artifact = compile_fixture("chase-condition-agentlab");
assert!(
artifact
.emitted
.contains("Chase Global Variable Over Time(Global.Round_Attack_Time, 0, 30, None);")
);
}
#[test]
fn chase_keyword_forms_match_the_pinned_oracle() {
assert_matches_oracle("chase-keywords");
}
#[test]
fn catalog_enum_members_lower_and_validate() {
let artifact = compile_fixture("chase-enums");
for expected in [
"Set Global Variable(time_reeval, None);",
"Set Global Variable(time_reeval, Destination and Duration);",
"Set Global Variable(rate_reeval, None);",
"Set Global Variable(rate_reeval, Destination and Rate);",
] {
assert!(artifact.emitted.contains(expected), "missing {expected}");
}
}
#[test]
fn pinned_builtin_domains_and_constants_lower_through_the_catalog() {
let source = r#"globalvar value
rule "domains":
@Event eachPlayer
@Condition eventPlayer.isHoldingButton(Button.INTERACT)
value = Team.ALL
value = Hero.BRIGITTE
value = Map.BLACK_FOREST_WINTER
value = getCurrentGamemode() == Gamemode.ASSAULT
value = Color.LIGHT_RED
value = Vector.BACKWARD
value = Math.FUCKTON_OF_SPACES
value = Math.FUCKTON_OF_NEWLINES
value = Math.SPHERE_HORIZONTAL_RADIUS_MULT
value = Math.RING_EXPLOSION_RADIUS_MULT
rule "slot":
@Event playerJoined
@Team 1
@Slot 11
disableInspector()
"#;
let hir = crate::compile(source, "builtin-domains.opy", Path::new("."))
.expect("pinned domain and constant source must resolve");
let artifact = Compiler::new()
.expect("released Workshop contract must load")
.compile_hir(&hir)
.expect("pinned domain and constant source must lower");
assert!(artifact.emitted.contains("Player Joined Match;"));
assert!(artifact.emitted.contains("Slot 11;"));
assert!(artifact.emitted.contains("Custom String("));
assert!(artifact.emitted.contains("0.984724"));
assert!(artifact.emitted.contains("0.48"));
}
#[test]
fn pinned_texture_members_lower_with_texture_tag_setup() {
let source = "globalvar value\nrule \"textures\":\n @Event global\n value = Texture.MOUSE_CURSOR\n value = Texture.ASSAULT\n";
let hir = crate::compile(source, "textures.opy", Path::new(".")).unwrap();
let artifact = Compiler::new().unwrap().compile_hir(&hir).unwrap();
assert!(artifact.emitted.contains("127: __holygrail__"));
assert!(
artifact
.emitted
.contains("Create Dummy Bot(All Heroes, If-Then-Else(")
);
let mut slot_queries = Vec::new();
for action in artifact.wir.rules.iter().flat_map(|rule| &rule.actions) {
if let Action::Call { args, .. } = action {
for value in args {
collect_calls(value, "getNumberOfSlots", &mut slot_queries);
}
}
}
for team in ["TEAM_1", "TEAM_2"] {
assert!(slot_queries.iter().any(|args| matches!(
*args,
[Value::Enum { value_type, value }] if value_type == "Team" && value == team
)));
}
assert!(
artifact
.emitted
.contains("OverPy <\u{AD}tx> / <\u{AD}fg> setup code")
);
assert!(
artifact
.emitted
.contains("String Split(First Of(Last Created Entity),")
);
assert!(artifact.emitted.contains("String Replace("));
assert!(artifact.emitted.contains("String Slice("));
assert!(artifact.emitted.contains("Destroy All Dummy Bots;"));
assert!(
artifact
.emitted
.contains("Custom String(\"{0}txc0000000002dd21>\", Global.__holygrail__)")
);
assert!(
artifact
.emitted
.contains("Custom String(\"{0}txc00000000020af8>\", Global.__holygrail__)")
);
}
#[test]
fn texture_setup_and_formatted_named_entities_match_pinned_features() {
let artifact = compile_fixture("texture-entity-compat");
let oracle = oracle_workshop("texture-entity-compat");
for marker in [
"Create Dummy Bot(All Heroes,",
"Start Forcing Dummy Bot Name(Last Created Entity,",
"String Split(First Of(Last Created Entity),",
"String Replace(",
"String Slice(String Replace(",
"126, True",
"Destroy All Dummy Bots;",
"Custom String(\"{0}txc0000000002dd21>\", Global.__holygrail__)",
"Custom String(\"â– {0}\", Match Time)",
] {
assert!(
oracle.contains(marker),
"pinned oracle is missing required feature marker: {marker}"
);
assert!(
artifact.emitted.contains(marker),
"native output is missing pinned feature marker: {marker}\n{}",
artifact.emitted
);
}
}
#[test]
fn aliased_member_lowers_to_the_canonical_catalog_identity() {
let source = "globalvar value\nrule \"r\":\n @Event eachPlayer\n @Condition eventPlayer.getHero() == None\n value = eventPlayer.getHero()\n";
let hir = crate::compile(source, "source.opy", Path::new(".")).expect("frontend resolves");
let artifact = Compiler::new()
.expect("compiler loads")
.compile_hir(&hir)
.expect("the member alias must lower to Hero Of");
assert!(artifact.emitted.contains("Hero Of(Event Player)"));
}
#[test]
fn append_receiver_uses_the_canonical_modify_operation() {
let source = "globalvar values\nrule \"r\":\n @Event global\n values.append(1)\n";
let hir = crate::compile(source, "source.opy", Path::new(".")).expect("frontend resolves");
let artifact = Compiler::new()
.expect("compiler loads")
.compile_hir(&hir)
.expect("append lowers to canonical WIR");
assert!(
artifact
.emitted
.contains("Modify Global Variable(values, Append To Array, 1);")
);
}
#[test]
fn computed_numbers_and_cardinal_vectors_use_canonical_workshop_spellings() {
let source = r#"globalvar value
rule "canonical values":
@Event global
value = vect(0, 1, 0)
value = 1.5 / sqrt(2) + 0.75
"#;
let hir = crate::compile(source, "canonical-values.opy", Path::new("."))
.expect("source must resolve");
let artifact = Compiler::new()
.expect("released workshop contract must load")
.compile_hir(&hir)
.expect("source must lower");
assert!(artifact.emitted.contains("Set Global Variable(value, Up);"));
assert!(artifact.emitted.contains("1.810660171779821"));
assert!(!artifact.emitted.contains("1.8106601717798212"));
}
#[test]
fn real_world_cake_exercises_catalog_lowering_end_to_end() {
let first = compile_real_world("overpy-cake", "source.opy");
let second = compile_real_world("overpy-cake", "source.opy");
assert_eq!(
first.emitted, second.emitted,
"emission must be deterministic"
);
let program = super::canonical_program(&first);
let mut calls = std::collections::BTreeSet::new();
let mut value_calls = std::collections::BTreeSet::new();
for rule in &program.rules {
for action in &rule.actions {
collect_action_calls(action, &mut calls, &mut value_calls);
}
for condition in &rule.conditions {
collect_value_calls(&condition.value, &mut value_calls);
}
}
for expected in ["createBeamEffect", "playEffect"] {
assert!(
calls.contains(expected),
"real-world cake must lower {expected}"
);
}
for expected in ["randomReal", "randomValueInArray"] {
assert!(
value_calls.contains(expected),
"real-world cake must lower value {expected}"
);
}
}
fn collect_action_calls(
action: &workshop_rs::Action,
calls: &mut std::collections::BTreeSet<String>,
value_calls: &mut std::collections::BTreeSet<String>,
) {
match action {
workshop_rs::Action::Call { name, args } => {
calls.insert(name.clone());
for value in args {
collect_value_calls(value, value_calls);
}
}
workshop_rs::Action::SetGlobalVariable { value, .. }
| workshop_rs::Action::ModifyGlobalVariable { value, .. }
| workshop_rs::Action::AssignMember { value, .. }
| workshop_rs::Action::If { condition: value }
| workshop_rs::Action::ElseIf { condition: value }
| workshop_rs::Action::While { condition: value } => {
collect_value_calls(value, value_calls)
}
workshop_rs::Action::SetPlayerVariable { player, value, .. }
| workshop_rs::Action::ModifyPlayerVariable { player, value, .. } => {
collect_value_calls(player, value_calls);
collect_value_calls(value, value_calls);
}
workshop_rs::Action::ForGlobalVariable {
start, stop, step, ..
} => {
for value in [start, stop, step] {
collect_value_calls(value, value_calls);
}
}
workshop_rs::Action::ForPlayerVariable {
player,
start,
stop,
step,
..
} => {
for value in [player, start, stop, step] {
collect_value_calls(value, value_calls);
}
}
workshop_rs::Action::Disabled { action } => {
collect_action_calls(action, calls, value_calls)
}
workshop_rs::Action::CallSubroutine { .. }
| workshop_rs::Action::Else
| workshop_rs::Action::End => {}
}
}
fn collect_value_calls(value: &workshop_rs::Value, calls: &mut std::collections::BTreeSet<String>) {
match value {
workshop_rs::Value::Call { name, args } => {
calls.insert(name.clone());
for arg in args {
collect_value_calls(arg, calls);
}
}
workshop_rs::Value::Array(values) => {
for value in values {
collect_value_calls(value, calls);
}
}
workshop_rs::Value::Vector { x, y, z } => {
for value in [x, y, z] {
collect_value_calls(value, calls);
}
}
workshop_rs::Value::PlayerVariable { player, .. } => collect_value_calls(player, calls),
_ => {}
}
}