use std::collections::BTreeSet;
use workshop_rs::catalog::{Catalog, Locale};
use workshop_rs::emitter;
use workshop_rs::parser;
use super::common;
fn round_trip(text: &str, locale: &str) -> String {
let catalog = Catalog::builtin().unwrap();
let locale = Locale::new(locale);
let program = parser::parse_wir_with_context(text, &catalog, &locale, &catalog)
.unwrap_or_else(|error| panic!("parse failed: {error:?}\n{text}"));
emitter::emit_wir(&program, &catalog, &locale)
.unwrap_or_else(|error| panic!("emit failed: {error:?}"))
}
fn assert_lines_preserved(actions: &[&str], locale: &str, event: &str, section: &str) {
let body: String = actions
.iter()
.map(|line| [" ", line, "\n"].concat())
.collect();
let text = format!(
"{section} (\"t\") {{\n {event} {{\n {}\n }}\n actions {{\n{body} }}\n}}\n",
if locale == "zh-CN" {
"持续 - 全局;"
} else {
"Ongoing - Global;"
}
);
let emitted = round_trip(&text, locale);
for line in actions {
assert!(
emitted.contains(&format!(" {line}\n")),
"{locale}: `{line}` was not preserved:\n{emitted}"
);
}
}
#[test]
fn overpy_constant_forms_round_trip_in_en_us() {
assert_lines_preserved(
&[
"Set Global Variable(A, All Players(Team 1));",
"Set Global Variable(A, Players On Hero(Hero(Mercy), Team 1));",
"Set Global Variable(A, Array(Hero(D.Va), Hero(Reinhardt), Hero(Winston)));",
"Set Global Variable(A, Array(Team 1, Team 2));",
"Set Global Variable(A, Array(Color(Red), Color(Blue)));",
"Set Global Variable(A, Array(Map(Hanamura)));",
"Set Global Variable(A, Array(Button(Reload), Button(Jump)));",
"Set Global Variable(A, Compare(Global.A, ==, Team 1));",
"Small Message(Event Player, Custom String(\"x{0}\", Button(Reload)));",
],
"en-US",
"event",
"rule",
);
}
#[test]
fn overpy_constant_forms_round_trip_in_zh_cn() {
assert_lines_preserved(
&[
"设置全局变量(A, 所有玩家(队伍1));",
"设置全局变量(A, 选择英雄的玩家(英雄(天使), 队伍1));",
"设置全局变量(A, 数组(英雄(D.Va), 英雄(莱因哈特), 英雄(温斯顿)));",
"设置全局变量(A, 数组(队伍1, 队伍2));",
"设置全局变量(A, 数组(颜色(红色), 颜色(蓝色)));",
"设置全局变量(A, 数组(地图(花村)));",
"设置全局变量(A, 数组(按钮(装填), 按钮(跳跃)));",
"设置全局变量(A, 比较(全局.A, ==, 队伍1));",
"小字体信息(事件玩家, 自定义字符串(\"x{0}\", 按钮(装填)));",
],
"zh-CN",
"事件",
"规则",
);
}
#[test]
fn a_team_wrapper_in_the_input_is_written_bare_in_every_locale() {
let text = "rule (\"t\") {\n event {\n Ongoing - Global;\n }\n actions {\n Set Global Variable(A, All Players(Team(Team 1)));\n }\n}\n";
let catalog = Catalog::builtin().unwrap();
let program =
parser::parse_wir_with_context(text, &catalog, &Locale::new("en-US"), &catalog).unwrap();
for (locale, expected) in [
("en-US", "All Players(Team 1)"),
("zh-CN", "所有玩家(队伍1)"),
] {
let emitted = emitter::emit_wir(&program, &catalog, &Locale::new(locale)).unwrap();
assert!(emitted.contains(expected), "{locale}:\n{emitted}");
}
}
#[test]
fn team_constants_in_color_slots_keep_their_authored_form() {
for (locale, section, event, actions) in [
(
"en-US",
"rule",
"event",
&[
"Play Effect(All Players(All Teams), Echo Sticky Bomb Explosion Effect, Team 1, Event Player, 200);",
"Play Effect(All Players(All Teams), Echo Sticky Bomb Explosion Effect, Team 2, Event Player, 200);",
"Play Effect(All Players(All Teams), Echo Sticky Bomb Explosion Effect, Color(Team 2), Event Player, 200);",
][..],
),
(
"zh-CN",
"规则",
"事件",
&[
"播放效果(所有玩家(所有队伍), “回声”黏性炸弹爆炸效果, 队伍1, 事件玩家, 200);",
"播放效果(所有玩家(所有队伍), “回声”黏性炸弹爆炸效果, 队伍2, 事件玩家, 200);",
"播放效果(所有玩家(所有队伍), “回声”黏性炸弹爆炸效果, 颜色(队伍2), 事件玩家, 200);",
][..],
),
] {
assert_lines_preserved(actions, locale, event, section);
}
}
#[test]
fn yes_no_settings_are_written_with_the_client_word() {
for (locale, text) in [
(
"en-US",
"settings {\n lobby {\n Allow Players Who Are In Queue: No\n Swap Teams After Match: Yes\n }\n}\n",
),
(
"zh-CN",
"设置 {\n 大厅 {\n 队列中的玩家可以加入: 否\n 比赛结束后转换队伍: 是\n }\n}\n",
),
] {
assert_eq!(round_trip(text, locale).trim_end(), text.trim_end());
}
}
fn wrapper_counts(text: &str) -> Vec<usize> {
[
"Team", "队伍", "Hero", "英雄", "Button", "按钮", "Color", "颜色",
]
.iter()
.map(|name| {
let pattern = format!(r"(^|[^\p{{L}}\p{{N}}_]){name}\(");
regex::Regex::new(&pattern).unwrap().find_iter(text).count()
})
.collect()
}
fn tokens(text: &str) -> BTreeSet<String> {
regex::Regex::new(r"[\p{L}\p{N}_.]+")
.unwrap()
.find_iter(text)
.map(|token| token.as_str().to_string())
.collect()
}
#[test]
fn overpy_generated_projects_keep_wrapper_counts_and_add_no_tokens() {
let cases: Vec<_> = common::cases()
.iter()
.filter(|case| matches!(case.id, "ai-pve" | "bastion"))
.collect();
assert_eq!(cases.len(), 2, "both OverPy-generated fixtures must run");
for case in cases {
let (source, locale) = common::source(case);
let emitted = round_trip(&source, locale.as_str());
assert_eq!(
wrapper_counts(&source),
wrapper_counts(&emitted),
"{} wrapper call counts changed",
case.id
);
let added: Vec<_> = tokens(&emitted)
.difference(&tokens(&source))
.cloned()
.collect();
assert!(
added.is_empty(),
"{} emission added tokens: {added:?}",
case.id
);
}
}
#[test]
fn zh_cn_action_spellings_follow_the_pinned_overpy_emission() {
let spellings = [
"消除所有图标;",
"消除所有地图文本;",
"消除所有效果;",
"消除所有HUD文本;",
"关闭游戏预设通告模式;",
"关闭游戏预设完成条件;",
"关闭游戏预设音乐模式;",
"关闭游戏预设计分模式;",
"开启游戏预设音乐模式;",
"比赛时间继续;",
];
let body: String = spellings
.iter()
.map(|line| [" ", line, "\n"].concat())
.collect();
let text = format!(
"规则 (\"t\") {{\n 事件 {{\n 持续 - 全局;\n }}\n 动作 {{\n{body} }}\n}}\n"
);
let emitted = round_trip(&text, "zh-CN");
for line in spellings {
assert!(
emitted.contains(&format!(" {line}\n")),
"`{line}` was not preserved:\n{emitted}"
);
}
}