use std::path::{Path, PathBuf};
use crate::hir::Expr;
use crate::{CompileFailureClass, CompileStatus, Compiler};
use workshop_rs::catalog::Locale;
use workshop_rs::{Action, Value};
fn find_call<'a>(value: &'a Value, name: &str) -> Option<&'a Value> {
if let Value::Call {
name: value_name,
args,
} = value
{
if value_name == name {
return Some(value);
}
return args.iter().find_map(|arg| find_call(arg, name));
}
None
}
fn action_values(action: &Action) -> Vec<&Value> {
match action {
Action::SetGlobalVariable { value, .. }
| Action::ModifyGlobalVariable { value, .. }
| Action::AssignMember { value, .. }
| Action::If { condition: value }
| Action::ElseIf { condition: value }
| Action::While { condition: value } => vec![value],
Action::SetPlayerVariable { player, value, .. }
| Action::ModifyPlayerVariable { player, value, .. } => vec![player, value],
Action::ForGlobalVariable {
start, stop, step, ..
} => vec![start, stop, step],
Action::ForPlayerVariable {
player,
start,
stop,
step,
..
} => {
vec![player, start, stop, step]
}
Action::Call { args, .. } => args.iter().collect(),
Action::CallSubroutine { .. } | Action::Else | Action::End => Vec::new(),
Action::Disabled { action } => action_values(action),
}
}
fn fixture_dir() -> PathBuf {
Path::new(env!("CARGO_MANIFEST_DIR"))
.join("tests/fixtures/corpus/real-world/overpy-broken-weapons")
}
#[test]
fn minimized_numeric_range_reaches_canonical_setting_value() {
let dir = fixture_dir();
let source = std::fs::read_to_string(dir.join("regressions/numeric-range-setting.opy"))
.expect("minimized regression must be readable");
let compiler = Compiler::new().expect("released Workshop contract must load");
let artifact = compiler
.compile_source_with_locale(
&source,
"regressions/numeric-range-setting.opy",
&dir,
&Locale::new("en-US"),
)
.expect("numeric range setting must lower");
let program = super::canonical_program(&artifact);
let setting = program
.rules
.iter()
.flat_map(|rule| rule.actions.iter())
.flat_map(action_values)
.find_map(|value| find_call(value, "createWorkshopSettingFloat"))
.expect("global initializer must contain the canonical setting value");
let Value::Call { name, args } = setting else {
unreachable!("the search above only returns call values");
};
assert_eq!(name, "createWorkshopSettingFloat");
assert_eq!(args.len(), 6);
assert!(matches!(
&args[0],
Value::String(value) if value == "\u{3000}"
));
assert!(matches!(
&args[3],
Value::Number(value) if *value == 0.5
));
assert!(matches!(
&args[4],
Value::Number(value) if *value == 10.0
));
}
#[test]
fn numeric_range_preserves_hir_type_span_and_round_trip() {
let dir = fixture_dir();
let source = std::fs::read_to_string(dir.join("regressions/numeric-range-setting.opy"))
.expect("minimized regression must be readable");
let hir = crate::compile(&source, "numeric-range-setting.opy", &dir)
.expect("numeric range source must resolve");
hir.validate().expect("numeric range HIR must validate");
let round_trip = crate::hir::parse_value(
serde_json::to_value(&hir).expect("numeric range HIR must serialize"),
)
.expect("numeric range HIR must round-trip");
round_trip
.validate()
.expect("round-tripped HIR must validate");
assert_eq!(hir.dump(), round_trip.dump());
let Expr::Call { args, .. } = find_initializer(&hir) else {
panic!("expected createWorkshopSetting initializer");
};
let Expr::Type { name, args, span } = &args[0] else {
panic!("expected a type literal as the first setting argument");
};
assert_eq!(name, "float");
assert_eq!(args.len(), 2);
assert_eq!(span.unwrap().start.line, 1);
assert_eq!(span.unwrap().start.col, 46);
assert_eq!(span.unwrap().end.col, 59);
}
#[test]
fn malformed_numeric_range_keeps_frontend_diagnostic_boundary() {
let source = "globalvar value = createWorkshopSetting(float[0.5:10, \"\", \"name\", 1, 0)\n";
let report = Compiler::new()
.expect("released Workshop contract must load")
.compile_source_report_with_locale(
source,
"numeric-range-invalid.opy",
Path::new("."),
&Locale::new("en-US"),
);
assert_eq!(report.compile.status, CompileStatus::Failure);
assert_eq!(
report.compile.failure_class,
Some(CompileFailureClass::Frontend)
);
assert_eq!(report.compile.diagnostics[0].code, "parse-error");
}
#[test]
fn motivating_project_compiles_after_numeric_range_and_return_support() {
let dir = fixture_dir();
let source = std::fs::read_to_string(dir.join("broken_weapons.opy"))
.expect("motivating project must be readable");
let report = Compiler::new()
.expect("released Workshop contract must load")
.compile_source_report_with_locale(
&source,
"broken_weapons.opy",
&dir,
&Locale::new("en-US"),
);
assert_eq!(report.compile.status, CompileStatus::Success);
assert!(report.compile.diagnostics.is_empty());
}
fn find_initializer(program: &crate::hir::Program) -> &Expr {
program
.declarations
.iter()
.find_map(|declaration| match declaration {
crate::hir::Declaration::GlobalVariable {
initializer: Some(initializer),
..
} => Some(initializer.as_ref()),
_ => None,
})
.expect("global initializer must exist")
}
#[test]
fn omitted_setting_sort_order_lowers_to_zero() {
let compiler = Compiler::new().expect("released Workshop contract must load");
for (call, function) in [
(
"createWorkshopSettingBool(\"C\", \"N\", true)",
"workshopSettingToggle",
),
(
"createWorkshopSettingInt(\"C\", \"N\", 1, 0, 10)",
"workshopSettingInteger",
),
(
"createWorkshopSettingEnum(\"C\", \"N\", 0, [\"a\", \"b\"])",
"workshopSettingCombo",
),
(
"createWorkshopSettingHero(\"C\", \"N\", Hero.ANA)",
"createWorkshopSettingHero",
),
(
"createWorkshopSetting(int[0:10], \"C\", \"N\", 1)",
"workshopSettingInteger",
),
] {
let source = format!("globalvar g\n\nrule \"p\":\n @Event global\n g = {call}\n");
let artifact = compiler
.compile_source_with_locale(&source, "sort.opy", Path::new("."), &Locale::new("en-US"))
.unwrap_or_else(|error| panic!("{call} must compile: {error}"));
let setting = artifact
.wir
.rules
.iter()
.flat_map(|rule| rule.actions.iter())
.flat_map(action_values)
.find_map(|value| find_call(value, function))
.unwrap_or_else(|| panic!("{call} must lower to its Workshop setting call"));
let Value::Call { name, args } = setting else {
unreachable!("the search above only returns call values");
};
assert_eq!(name, function);
assert!(matches!(args.last(), Some(Value::Number(sort_order)) if *sort_order == 0.0));
}
}