use mercs2_bridge::reply::{parse_reply, Reply, Value};
use mercs2_bridge::{Bridge, BridgeError};
use std::time::{Duration, Instant};
const SAVE_PREFIX: &str = "ess_live_probe";
const MENU_ID_GLOBAL: &str = "__ess_live_probe_menu";
const SPAWN_GLOBAL: &str = "__ess_live_probe_spawn";
const SPAWN_MENU_ID: &str = "__ess_live_probe_spawn_menu";
#[derive(Debug, Clone)]
pub struct Options {
pub addr: String,
pub menu: Option<String>,
pub spawn: Option<String>,
pub spawn_dist: f64,
pub remove_deadline: Duration,
pub poll_interval: Duration,
pub swf_deadline: Duration,
}
pub const DEFAULT_SWF_DEADLINE: Duration = Duration::from_secs(6);
pub const DEFAULT_REMOVE_DEADLINE: Duration = Duration::from_secs(5);
pub const DEFAULT_POLL_INTERVAL: Duration = Duration::from_millis(250);
pub const DEFAULT_SPAWN_DIST: f64 = 10.0;
#[derive(Debug, Clone)]
pub enum Expect {
Bool(bool),
AnyBool,
Num(f64),
AnyNum,
Str(&'static str),
AnyStr,
}
fn value_matches(e: &Expect, v: &Value) -> bool {
match (e, v) {
(Expect::Bool(want), Value::Bool(got)) => want == got,
(Expect::AnyBool, Value::Bool(_)) => true,
(Expect::Num(want), Value::Num(got)) => want == got,
(Expect::AnyNum, Value::Num(got)) => !got.is_nan(),
(Expect::Str(want), Value::Str(got)) => want == got,
(Expect::AnyStr, Value::Str(_)) => true,
_ => false,
}
}
pub fn judge(reply: &Reply, expect: &[Expect]) -> Result<Vec<Value>, String> {
let values = match reply {
Reply::Ok(v) => v,
other => return Err(format!("not an [ok] reply: {other:?}")),
};
if values.len() != expect.len() {
return Err(format!("expected {} values {expect:?}, got {}: {values:?}", expect.len(), values.len()));
}
for (i, (e, v)) in expect.iter().zip(values).enumerate() {
if !value_matches(e, v) {
return Err(format!("value {} is {v:?}, expected {e:?} (all: {values:?})", i + 1));
}
}
Ok(values.clone())
}
pub fn lua_str(s: &str) -> Result<String, String> {
if let Some(c) = s.chars().find(|c| !(' '..='~').contains(c)) {
return Err(format!("{s:?} holds {c:?}: only printable ASCII can be passed into a chunk"));
}
Ok(format!("\"{}\"", s.replace('\\', "\\\\").replace('"', "\\\"")))
}
#[derive(Debug, Clone)]
pub struct CheckResult {
pub name: &'static str,
pub pass: bool,
pub evidence: String,
}
impl std::fmt::Display for CheckResult {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{} {}: {}", if self.pass { "PASS" } else { "FAIL" }, self.name, self.evidence)
}
}
#[derive(Debug)]
pub enum Unreachable {
Connect(BridgeError),
NoAnswer(BridgeError),
}
impl std::fmt::Display for Unreachable {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Unreachable::Connect(e) => write!(f, "{e}"),
Unreachable::NoAnswer(e) => write!(f, "connected, but the first chunk got no answer: {e}"),
}
}
}
pub fn exit_code(results: &[CheckResult]) -> i32 {
if !results.is_empty() && results.iter().all(|r| r.pass) {
0
} else {
1
}
}
struct Runner {
bridge: Bridge,
results: Vec<CheckResult>,
}
impl Runner {
fn record(&mut self, name: &'static str, pass: bool, evidence: String) {
self.results.push(CheckResult { name, pass, evidence });
}
fn not_run(&mut self, name: &'static str, because: &str) {
self.record(name, false, format!("not run: {because}"));
}
fn send(&mut self, name: &'static str, body: &str) -> Option<String> {
match self.bridge.eval(&format!("-- ess-live:{name}\n{body}")) {
Ok(text) => Some(text),
Err(e) => {
self.record(name, false, format!("bridge error: {e}"));
None
}
}
}
fn check(&mut self, name: &'static str, body: &str, expect: &[Expect]) -> Option<Vec<Value>> {
let text = self.send(name, body)?;
match judge(&parse_reply(&text), expect) {
Ok(values) => {
self.record(name, true, format!("{text:?}"));
Some(values)
}
Err(why) => {
self.record(name, false, format!("{text:?} — {why}"));
None
}
}
}
}
const ARITY: &str = "local function n(...) return select('#', ...), ... end\n";
pub fn run(opts: &Options) -> Result<Vec<CheckResult>, Unreachable> {
let bridge = Bridge::connect_at(opts.addr.as_str()).map_err(Unreachable::Connect)?;
let mut r = Runner { bridge, results: Vec::new() };
let first = r.bridge.eval("-- ess-live:bridge\nreturn 1+1").map_err(Unreachable::NoAnswer)?;
match judge(&parse_reply(&first), &[Expect::Num(2.0)]) {
Ok(_) => r.record("bridge", true, format!("{first:?}")),
Err(why) => r.record("bridge", false, format!("{first:?} — {why}")),
}
if let Some(v) = r.check(
"ess_loaded",
"local E = rawget(_G, 'Ess')\n\
if type(E) ~= 'table' then return type(E) end\n\
return type(E), type(E.VERSION), tostring(E.VERSION)",
&[Expect::Str("table"), Expect::Str("string"), Expect::AnyStr],
) {
if let (Some(last), Some(Value::Str(ver))) = (r.results.last_mut(), v.get(2)) {
last.evidence = format!("Ess.VERSION = {ver}; {}", last.evidence);
}
}
r.check(
"loader_printf",
"local c = select('#', Loader.Printf('[ess-live] Loader.Printf probe'))\n\
Ess.Log('ess-live: Ess.Log probe')\n\
return c",
&[Expect::Num(0.0)],
);
r.check(
"loader_pop_key_events",
&format!(
"{ARITY}local c, e = n(Loader.PopKeyEvents())\n\
local p = Ess.Input.poll()\n\
return c, type(e), type(p.pressed), type(p.down), type(p.down(0x10))"
),
&[Expect::Num(1.0), Expect::Str("string"), Expect::Str("table"), Expect::Str("function"), Expect::Str("boolean")],
);
r.check(
"loader_get_keyboard_state",
&format!(
"{ARITY}local c, ks = n(Loader.GetKeyboardState())\n\
local len, bad = -1, -1\n\
if type(ks) == 'string' then\n\
\x20 len, bad = #ks, 0\n\
\x20 for i = 1, #ks do local b = string.byte(ks, i); if b ~= 0 and b ~= 128 then bad = bad + 1 end end\n\
end\n\
return c, type(ks), len, bad, type(Ess.Input.held(0x10))"
),
&[Expect::Num(1.0), Expect::Str("string"), Expect::Num(256.0), Expect::Num(0.0), Expect::Str("boolean")],
);
r.check(
"loader_is_key_down",
"local ok, held = pcall(Loader.IsKeyDown, 0x10)\n\
local c = select('#', Loader.IsKeyDown(0x10))\n\
local badOk = pcall(Loader.IsKeyDown, '16')\n\
return ok, type(held), c, badOk",
&[Expect::Bool(true), Expect::Str("boolean"), Expect::Num(1.0), Expect::Bool(false)],
);
r.check(
"loader_clear_key_events",
"local c = select('#', Loader.ClearKeyEvents())\n\
local ok = pcall(Loader.ClearKeyEvents)\n\
local after = Loader.PopKeyEvents()\n\
local ce = select('#', Ess.Input.clear())\n\
return c, ok, #after, ce",
&[Expect::Num(0.0), Expect::Bool(true), Expect::Num(0.0), Expect::Num(0.0)],
);
r.check(
"loader_save_load_var",
&format!(
"{ARITY}local K = '{SAVE_PREFIX}_raw'\n\
local r = {{ pcall(function()\n\
\x20 local cs = select('#', Loader.SaveVar(K, 7))\n\
\x20 local cl, v = n(Loader.LoadVar(K))\n\
\x20 local cd = select('#', Loader.SaveVar(K, nil))\n\
\x20 local cg, g = n(Loader.LoadVar(K))\n\
\x20 local badOk = pcall(Loader.SaveVar, K, {{}})\n\
\x20 return cs, cl, type(v), v, cd, cg, type(g), badOk\n\
end) }}\n\
pcall(Loader.SaveVar, K, nil)\n\
if not r[1] then error(r[2], 0) end\n\
return r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9]"
),
&[
Expect::Num(0.0),
Expect::Num(1.0),
Expect::Str("number"),
Expect::Num(7.0),
Expect::Num(0.0),
Expect::Num(1.0),
Expect::Str("nil"),
Expect::Bool(false),
],
);
r.check(
"loader_get_load_phase",
&format!("{ARITY}local c, i, nm = n(Loader.GetLoadPhase())\nreturn c, type(i), type(nm), i, nm"),
&[Expect::Num(2.0), Expect::Str("number"), Expect::Str("string"), Expect::AnyNum, Expect::AnyStr],
);
r.check(
"ess_savevar_roundtrip",
&format!(
"local sv = Ess.SaveVar.ns('{SAVE_PREFIX}')\n\
local r = {{ pcall(function()\n\
\x20 sv:set('num', 42.5); sv:set('str', 'ess-live'); sv:set('bool', true)\n\
\x20 local a, b, c, f = sv:get('num'), sv:get('str'), sv:get('bool'), sv:flag('bool')\n\
\x20 sv:set('num', nil); sv:set('str', nil); sv:set('bool', nil)\n\
\x20 return a, b, c, f, sv:get('num', 'default'), sv:get('str', 'default'), sv:get('bool', 'default'), sv:flag('bool')\n\
end) }}\n\
for _, k in ipairs({{ 'num', 'str', 'bool' }}) do pcall(Loader.SaveVar, '{SAVE_PREFIX}_' .. k, nil) end\n\
if not r[1] then error(r[2], 0) end\n\
return r[2], r[3], r[4], r[5], r[6], r[7], r[8], r[9]"
),
&[
Expect::Num(42.5),
Expect::Str("ess-live"),
Expect::Bool(true),
Expect::Bool(true),
Expect::Str("default"),
Expect::Str("default"),
Expect::Str("default"),
Expect::Bool(false),
],
);
if let Some(menu) = &opts.menu {
menu_checks(&mut r, menu, opts);
}
if let Some(template) = &opts.spawn {
spawn_checks(&mut r, template, opts);
}
Ok(r.results)
}
const MENU_PRECONDITION: &str = "local S = Ess.UI._S\n\
local id = S.openId\n\
local wasOpen = false\n\
if id ~= nil and type(S.menus) == 'table' and type(S.menus[id]) == 'table' then wasOpen = S.menus[id].open == true end\n\
local closedId = 'none'\n\
local closedOk = true\n\
if wasOpen then\n\
\x20 local rt = S.menus[id]\n\
\x20 if type(rt.menu) ~= 'table' then return 'the open menu keeps no menu object to close', tostring(id) end\n\
\x20 rt.menu:close()\n\
\x20 closedId = tostring(id)\n\
\x20 closedOk = rt.open == false\n\
end\n\
local stillOpen = false\n\
if S.openId ~= nil and type(S.menus[S.openId]) == 'table' then stillOpen = S.menus[S.openId].open == true end\n\
return type(S), wasOpen, closedId, closedOk, stillOpen, (Player.GetLocalPlayer() and true or false), (Player.GetLocalCharacter() and true or false)";
const MENU_PRECONDITION_EXPECT: [Expect; 7] = [
Expect::Str("table"),
Expect::AnyBool,
Expect::AnyStr,
Expect::Bool(true),
Expect::Bool(false),
Expect::Bool(true),
Expect::Bool(true),
];
const SWF_ARM: &str = "local function ty(t, k) if type(t) ~= 'table' then return 'nil' end return type(t[k]) end\n\
local FW = type(MrxGuiBase) == 'table' and MrxGuiBase.FlashWidget or nil\n\
local rtExists = Ess.UI._S.rt ~= nil\n\
local armed = false\n\
if type(FW) == 'table' and type(FW.SetSwfFile) == 'function' and not rtExists then\n\
\x20 local orig = Ess.Gfx.widget\n\
\x20 _G.__ess_live_probe_swf = { orig = orig, built = false, fired = false, readyAtFire = false }\n\
\x20 Ess.Gfx.widget = function(file, x, y, w, h, onLoad)\n\
\x20 local rec = _G.__ess_live_probe_swf\n\
\x20 if rec and not rec.built and file == Ess.UI.FILES.runtime and onLoad ~= nil then\n\
\x20 rec.built = true\n\
\x20 local inner = onLoad\n\
\x20 onLoad = function(...)\n\
\x20 local r2 = _G.__ess_live_probe_swf\n\
\x20 if r2 and not r2.fired then r2.fired = true; r2.readyAtFire = Ess.UI._rtReady() end\n\
\x20 return inner(...)\n\
\x20 end\n\
\x20 end\n\
\x20 return orig(file, x, y, w, h, onLoad)\n\
\x20 end\n\
\x20 armed = true\n\
end\n\
return ty(MrxGuiBase, 'FlashWidget'), ty(FW, 'new'), ty(FW, 'SetLocation'), ty(FW, 'SetSwfFile'), ty(FW, 'SetFlashEventHandler'), ty(FW, 'CallActionScriptCallback'), ty(MrxGuiBase, 'AddWidget'), rtExists, armed";
const SWF_RESTORE: &str = "local rec = _G.__ess_live_probe_swf\n\
if rec == nil then return 'not armed' end\n\
local restored = false\n\
if rec.orig ~= nil then Ess.Gfx.widget = rec.orig; rec.orig = nil; restored = true end\n\
return rec.built, restored, Ess.UI._S.rt ~= nil";
const SWF_POLL: &str = "local rec = _G.__ess_live_probe_swf\n\
if rec == nil then return 'not armed' end\n\
return rec.fired, rec.readyAtFire == true, Ess.UI._rtReady()";
const SWF_CLEANUP: &str = "_G.__ess_live_probe_swf = nil\nreturn true";
const SWF_ARM_EXPECT: [Expect; 9] = [
Expect::Str("table"),
Expect::Str("function"),
Expect::Str("function"),
Expect::Str("function"),
Expect::Str("function"),
Expect::Str("function"),
Expect::Str("function"),
Expect::AnyBool,
Expect::AnyBool,
];
fn swf_eval(r: &mut Runner, part: &str, body: &str) -> Result<(String, Reply), String> {
r.bridge
.eval(&format!("-- ess-live:{part}\n{body}"))
.map(|t| {
let p = parse_reply(&t);
(t, p)
})
.map_err(|e| format!("{part}: bridge error: {e}"))
}
fn swf_arm(r: &mut Runner) -> Result<String, String> {
let (text, reply) = swf_eval(r, "swf_arm", SWF_ARM)?;
let v = judge(&reply, &SWF_ARM_EXPECT).map_err(|why| format!("PRESENT check failed: {text:?} — {why}"))?;
match (&v[7], &v[8]) {
(Value::Bool(false), Value::Bool(true)) => Ok(format!("PRESENT {text:?}")),
(Value::Bool(true), _) => Err(format!(
"PRESENT, but no SWF load to observe: Ess's shared runtime (Ess.UI._S.rt) already exists this world, \
and the movie loads only when rtEnsure builds it (42_ui_engine.lua:329-341; reset on world load, \
42_ui_engine.lua:686-691). Reload the world and run ess-live before any Ess UI is shown. {text:?}"
)),
_ => Err(format!("not armed: {text:?}")),
}
}
fn swf_finish(r: &mut Runner, armed_evidence: String, restore: Result<(String, Reply), String>, opened_at: Instant, opts: &Options) {
let restore = restore.and_then(|(text, reply)| {
judge(&reply, &[Expect::AnyBool, Expect::Bool(true), Expect::AnyBool])
.map(|v| (text.clone(), v))
.map_err(|why| format!("restore of Ess.Gfx.widget failed: {text:?} — {why}"))
});
let (restore_text, v) = match restore {
Ok(x) => x,
Err(e) => {
r.record("swf_callback", false, format!("{e}; {armed_evidence}"));
return;
}
};
if v[0] != Value::Bool(true) {
r.record(
"swf_callback",
false,
format!(
"opening the menu did not build Ess's runtime widget through Ess.Gfx.widget \
(42_ui_engine.lua:333): {restore_text:?}; {armed_evidence}"
),
);
return;
}
let mut polls = 0u32;
loop {
let (text, reply) = match swf_eval(r, "swf_poll", SWF_POLL) {
Ok(x) => x,
Err(e) => {
r.record("swf_callback", false, format!("{e}; {armed_evidence}"));
return;
}
};
polls += 1;
let ms = opened_at.elapsed().as_millis();
let v = match judge(&reply, &[Expect::AnyBool, Expect::AnyBool, Expect::AnyBool]) {
Ok(v) => v,
Err(why) => {
r.record("swf_callback", false, format!("{text:?} — {why}; {armed_evidence}"));
return;
}
};
if v[0] == Value::Bool(true) {
let path = if v[1] == Value::Bool(true) {
"Ess had already gone ready by the 3 s timeout fallback (42_ui_engine.lua:386-388) before the callback fired late"
} else {
"Ess went ready through the load callback (42_ui_engine.lua:333-335), before the timeout fallback"
};
r.record(
"swf_callback",
true,
format!("RUNS: the SetSwfFile load callback fired, observed by poll {polls} at {ms} ms after the menu-open reply; path: {path}. {text:?}; {armed_evidence}"),
);
return;
}
if opened_at.elapsed() >= opts.swf_deadline {
let path = if v[2] == Value::Bool(true) {
"Ess went ready through the 3 s timeout fallback (42_ui_engine.lua:386-388)"
} else {
"Ess is not ready either"
};
r.record(
"swf_callback",
false,
format!("the SetSwfFile load callback did not fire within {ms} ms ({polls} polls); {path}. {text:?}; {armed_evidence}"),
);
return;
}
std::thread::sleep(opts.poll_interval);
}
}
fn menu_checks(r: &mut Runner, menu: &str, opts: &Options) {
let path = match lua_str(menu) {
Ok(p) => p,
Err(e) => {
r.record("menu_precondition", false, format!("--menu: {e}"));
r.not_run("menu_open", "menu_precondition failed");
r.not_run("menu_close", "menu_precondition failed");
r.not_run("swf_callback", "menu_precondition failed");
return;
}
};
let pre = r.check("menu_precondition", MENU_PRECONDITION, &MENU_PRECONDITION_EXPECT);
if let (Some(v), Some(last)) = (&pre, r.results.last_mut()) {
if let (Value::Bool(true), Value::Str(id)) = (&v[1], &v[2]) {
last.evidence = format!(
"an Ess menu was already open: closed {id:?} first via Menu:close (49_ui_menu.lua:211-220); {}",
last.evidence
);
}
}
if pre.is_none() {
r.not_run("menu_open", "menu_precondition failed");
r.not_run("menu_close", "menu_precondition failed");
r.not_run("swf_callback", "menu_precondition failed");
return;
}
let armed = swf_arm(r);
let opened = r.check(
"menu_open",
&format!(
"local ran = Loader.LoadFile({path})\n\
local S = Ess.UI._S\n\
local id = S.openId\n\
local isOpen = false\n\
if id ~= nil and type(S.menus) == 'table' and type(S.menus[id]) == 'table' then isOpen = S.menus[id].open == true end\n\
_G.{MENU_ID_GLOBAL} = id\n\
return ran, type(id), tostring(id), isOpen"
),
&[Expect::Bool(true), Expect::Str("string"), Expect::AnyStr, Expect::Bool(true)],
);
let opened_at = Instant::now();
let swf = armed.map(|ev| (ev, swf_eval(r, "swf_restore", SWF_RESTORE)));
if opened.is_none() {
r.not_run("menu_close", "menu_open failed");
} else {
menu_close(r, &path);
}
match swf {
Ok((ev, restore)) => {
swf_finish(r, ev, restore, opened_at, opts);
if let Err(e) = swf_eval(r, "swf_cleanup", SWF_CLEANUP) {
r.record("swf_cleanup", false, e);
}
}
Err(e) => r.record("swf_callback", false, e),
}
}
fn menu_close(r: &mut Runner, path: &str) {
r.check(
"menu_close",
&format!(
"local id = _G.{MENU_ID_GLOBAL}\n\
_G.{MENU_ID_GLOBAL} = nil\n\
local ran = Loader.LoadFile({path})\n\
local S = Ess.UI._S\n\
local closed = false\n\
if id ~= nil and type(S.menus) == 'table' and type(S.menus[id]) == 'table' then closed = S.menus[id].open == false end\n\
return ran, tostring(id), closed, S.openId == nil"
),
&[Expect::Bool(true), Expect::AnyStr, Expect::Bool(true), Expect::Bool(true)],
);
}
fn spawn_checks(r: &mut Runner, template: &str, opts: &Options) {
let names = ["spawn", "spawn_remove", "spawn_gone", "spawn_cleanup"];
let tmpl = match lua_str(template) {
Ok(t) => t,
Err(e) => {
r.record("spawn", false, format!("--spawn: {e}"));
for &n in &names[1..] {
r.not_run(n, "spawn failed");
}
return;
}
};
if !opts.spawn_dist.is_finite() || opts.spawn_dist < 0.0 {
r.record("spawn", false, format!("--spawn-dist {} is not a finite distance >= 0", opts.spawn_dist));
for &n in &names[1..] {
r.not_run(n, "spawn failed");
}
return;
}
let spawned = r.send(
"spawn",
&format!(
"local m = Ess.UI.Menu{{ title = '{SPAWN_MENU_ID}', id = '{SPAWN_MENU_ID}' }}\n\
local cap\n\
m:entry('probe', function(ctx) cap = ctx end)\n\
m:_choose({{ _node = m._children[1] }})\n\
Ess.UI._S.menus['{SPAWN_MENU_ID}'] = nil\n\
if cap == nil then return 'the menu action never ran: no ctx' end\n\
local u = cap:spawn({tmpl}, {dist})\n\
_G.{SPAWN_GLOBAL} = u\n\
return type(u), Ess.Object.valid(u), Ess.Object.alive(u)",
dist = opts.spawn_dist
),
);
let Some(spawned) = spawned else {
for &n in &names[1..] {
r.not_run(n, "spawn got no reply");
}
return;
};
match judge(&parse_reply(&spawned), &[Expect::Str("userdata"), Expect::Bool(true), Expect::Bool(true)]) {
Ok(_) => r.record("spawn", true, format!("{spawned:?}")),
Err(why) => r.record("spawn", false, format!("{spawned:?} — {why}")),
}
let removed = r.check(
"spawn_remove",
&format!(
"local u = _G.{SPAWN_GLOBAL}\n\
if u == nil then return 'nothing spawned' end\n\
Ess.Object.remove(u)\n\
return true"
),
&[Expect::Bool(true)],
);
if removed.is_some() {
spawn_gone(r, opts);
} else {
r.not_run("spawn_gone", "spawn_remove failed");
}
r.check("spawn_cleanup", &format!("_G.{SPAWN_GLOBAL} = nil\nreturn true"), &[Expect::Bool(true)]);
}
fn spawn_gone(r: &mut Runner, opts: &Options) {
let start = Instant::now();
let mut polls = 0u32;
loop {
let Some(text) = r.send("spawn_gone", &format!("return Ess.Object.alive(_G.{SPAWN_GLOBAL})")) else {
return;
};
polls += 1;
match judge(&parse_reply(&text), &[Expect::AnyBool]) {
Ok(v) if v[0] == Value::Bool(false) => {
r.record(
"spawn_gone",
true,
format!("{text:?} after {polls} polls, {} ms", start.elapsed().as_millis()),
);
return;
}
Ok(_) => {
if start.elapsed() >= opts.remove_deadline {
r.record(
"spawn_gone",
false,
format!("{text:?} — still alive after {polls} polls, {} ms", start.elapsed().as_millis()),
);
return;
}
std::thread::sleep(opts.poll_interval);
}
Err(why) => {
r.record("spawn_gone", false, format!("{text:?} — {why}"));
return;
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use mercs2_bridge::Server;
use std::sync::atomic::{AtomicUsize, Ordering};
use std::sync::Arc;
fn marker(chunk: &str) -> &str {
chunk.lines().next().and_then(|l| l.strip_prefix("-- ess-live:")).unwrap_or("")
}
fn healthy(name: &str) -> String {
match name {
"bridge" => "[ok]\t2",
"ess_loaded" => "[ok]\t\"table\"\t\"string\"\t\"0.7.0\"",
"loader_printf" => "[ok]\t0",
"loader_pop_key_events" => "[ok]\t1\t\"string\"\t\"table\"\t\"function\"\t\"boolean\"",
"loader_get_keyboard_state" => "[ok]\t1\t\"string\"\t256\t0\t\"boolean\"",
"loader_is_key_down" => "[ok]\ttrue\t\"boolean\"\t1\tfalse",
"loader_clear_key_events" => "[ok]\t0\ttrue\t0\t0",
"loader_save_load_var" => "[ok]\t0\t1\t\"number\"\t7\t0\t1\t\"nil\"\tfalse",
"loader_get_load_phase" => "[ok]\t2\t\"number\"\t\"string\"\t20\t\"World fully loaded (GlobalExit)\"",
"ess_savevar_roundtrip" => {
"[ok]\t42.5\t\"ess-live\"\ttrue\ttrue\t\"default\"\t\"default\"\t\"default\"\tfalse"
}
"menu_precondition" => "[ok]\t\"table\"\tfalse\t\"none\"\ttrue\tfalse\ttrue\ttrue",
"menu_open" => "[ok]\ttrue\t\"string\"\t\"Ferd's AiO SpawnMenu v0.85\"\ttrue",
"menu_close" => "[ok]\ttrue\t\"Ferd's AiO SpawnMenu v0.85\"\ttrue\ttrue",
"swf_arm" => {
"[ok]\t\"table\"\t\"function\"\t\"function\"\t\"function\"\t\"function\"\t\"function\"\t\"function\"\tfalse\ttrue"
}
"swf_restore" => "[ok]\ttrue\ttrue\ttrue",
"swf_poll" => "[ok]\ttrue\tfalse\ttrue",
"swf_cleanup" => "[ok]\ttrue",
"spawn" => "[ok]\t\"userdata\"\ttrue\ttrue",
"spawn_remove" => "[ok]\ttrue",
"spawn_gone" => "[ok]\tfalse",
"spawn_cleanup" => "[ok]\ttrue",
other => panic!("a chunk with no healthy reply: {other:?}"),
}
.to_string()
}
fn serve(handler: impl FnMut(&str) -> String + Send + 'static) -> String {
let server = Server::bind("127.0.0.1:0").expect("bind ephemeral");
let addr = server.local_addr().expect("local addr").to_string();
let mut handler = handler;
std::thread::spawn(move || {
let _ = server.serve(|chunk| handler(chunk));
});
addr
}
fn opts(addr: String) -> Options {
Options {
addr,
menu: Some("OnKey/AllInOneSpawnMenu.lua".into()),
spawn: Some("HMMWV (Softtop)".into()),
spawn_dist: DEFAULT_SPAWN_DIST,
remove_deadline: Duration::from_millis(1500),
poll_interval: Duration::from_millis(10),
swf_deadline: Duration::from_millis(1500),
}
}
fn by_name<'a>(results: &'a [CheckResult], name: &str) -> &'a CheckResult {
results.iter().find(|r| r.name == name).unwrap_or_else(|| panic!("no result for {name}: {results:#?}"))
}
const ALL: &[&str] = &[
"bridge",
"ess_loaded",
"loader_printf",
"loader_pop_key_events",
"loader_get_keyboard_state",
"loader_is_key_down",
"loader_clear_key_events",
"loader_save_load_var",
"loader_get_load_phase",
"ess_savevar_roundtrip",
"menu_precondition",
"menu_open",
"menu_close",
"swf_callback",
"spawn",
"spawn_remove",
"spawn_gone",
"spawn_cleanup",
];
#[test]
fn ess_live_all_good_passes_every_check_and_exits_0() {
let gone_polls = Arc::new(AtomicUsize::new(0));
let polls = gone_polls.clone();
let addr = serve(move |chunk| match marker(chunk) {
"spawn_gone" if polls.fetch_add(1, Ordering::SeqCst) < 2 => "[ok]\ttrue".into(),
name => healthy(name),
});
let results = run(&opts(addr)).expect("reachable");
let names: Vec<&str> = results.iter().map(|r| r.name).collect();
assert_eq!(names, ALL);
assert!(results.iter().all(|r| r.pass), "{results:#?}");
assert_eq!(exit_code(&results), 0);
assert_eq!(gone_polls.load(Ordering::SeqCst), 3);
assert!(by_name(&results, "ess_loaded").evidence.starts_with("Ess.VERSION = 0.7.0"));
}
#[test]
fn ess_live_without_menu_or_spawn_runs_only_the_core_checks() {
let addr = serve(|chunk| healthy(marker(chunk)));
let results = run(&Options { menu: None, spawn: None, ..opts(addr) }).expect("reachable");
let names: Vec<&str> = results.iter().map(|r| r.name).collect();
assert_eq!(names, &ALL[..10]);
assert_eq!(exit_code(&results), 0);
}
#[test]
fn ess_live_a_wrong_type_fails_that_check_only() {
let addr = serve(|chunk| match marker(chunk) {
"loader_get_keyboard_state" => "[ok]\t1\t\"string\"\t255\t0\t\"boolean\"".into(),
"loader_is_key_down" => "[ok]\ttrue\t\"number\"\t1\tfalse".into(),
name => healthy(name),
});
let results = run(&opts(addr)).expect("reachable");
let failed: Vec<&str> = results.iter().filter(|r| !r.pass).map(|r| r.name).collect();
assert_eq!(failed, ["loader_get_keyboard_state", "loader_is_key_down"]);
assert!(by_name(&results, "loader_get_keyboard_state").evidence.contains("Num(255.0)"));
assert_eq!(exit_code(&results), 1);
}
#[test]
fn ess_live_a_runtime_error_fails_with_the_message_as_evidence() {
let addr = serve(|chunk| match marker(chunk) {
"ess_savevar_roundtrip" => "[runtime] \"attempt to index field 'SaveVar' (a nil value)\"".into(),
name => healthy(name),
});
let results = run(&opts(addr)).expect("reachable");
let r = by_name(&results, "ess_savevar_roundtrip");
assert!(!r.pass);
assert!(r.evidence.contains("Runtime(") && r.evidence.contains("attempt to index field 'SaveVar'"), "{r:?}");
assert_eq!(exit_code(&results), 1);
}
#[test]
fn ess_live_a_menu_that_does_not_open_fails_and_close_is_not_run() {
let addr = serve(|chunk| match marker(chunk) {
"menu_open" => "[ok]\ttrue\t\"nil\"\t\"nil\"\tfalse".into(),
name => healthy(name),
});
let results = run(&opts(addr)).expect("reachable");
assert!(!by_name(&results, "menu_open").pass);
let close = by_name(&results, "menu_close");
assert!(!close.pass && close.evidence.starts_with("not run"), "{close:?}");
assert_eq!(exit_code(&results), 1);
}
#[test]
fn ess_live_an_open_menu_is_closed_first_then_open_and_close_pass() {
let addr = serve(|chunk| match marker(chunk) {
"menu_precondition" => {
assert!(chunk.contains("rt.menu:close()"), "the close goes through Ess's Menu:close");
"[ok]\t\"table\"\ttrue\t\"Other Menu\"\ttrue\tfalse\ttrue\ttrue".into()
}
name => healthy(name),
});
let results = run(&opts(addr)).expect("reachable");
let pre = by_name(&results, "menu_precondition");
assert!(pre.pass, "{pre:?}");
assert!(pre.evidence.starts_with("an Ess menu was already open: closed \"Other Menu\" first"), "{pre:?}");
assert!(by_name(&results, "menu_open").pass);
assert!(by_name(&results, "menu_close").pass);
assert_eq!(exit_code(&results), 0);
}
#[test]
fn ess_live_an_open_menu_the_close_does_not_shut_fails_the_precondition() {
for reply in [
"[ok]\t\"table\"\ttrue\t\"Other Menu\"\tfalse\tfalse\ttrue\ttrue",
"[ok]\t\"table\"\ttrue\t\"Other Menu\"\ttrue\ttrue\ttrue\ttrue",
"[ok]\t\"the open menu keeps no menu object to close\"\t\"Other Menu\"",
] {
let addr = serve(move |chunk| match marker(chunk) {
"menu_precondition" => reply.into(),
name => healthy(name),
});
let results = run(&opts(addr)).expect("reachable");
assert!(!by_name(&results, "menu_precondition").pass, "{reply}");
for n in ["menu_open", "menu_close", "swf_callback"] {
let c = by_name(&results, n);
assert!(!c.pass && c.evidence.starts_with("not run"), "{c:?}");
}
assert_eq!(exit_code(&results), 1);
}
}
#[test]
fn ess_live_no_local_player_fails_the_precondition() {
let addr = serve(|chunk| match marker(chunk) {
"menu_precondition" => "[ok]\t\"table\"\tfalse\t\"none\"\ttrue\tfalse\tfalse\tfalse".into(),
name => healthy(name),
});
let results = run(&opts(addr)).expect("reachable");
assert!(!by_name(&results, "menu_precondition").pass);
assert_eq!(exit_code(&results), 1);
}
fn swf_order(log: &std::sync::Mutex<Vec<String>>) -> Vec<String> {
log.lock().unwrap().iter().filter(|n| n.starts_with("swf_") || n.starts_with("menu_")).cloned().collect()
}
#[test]
fn ess_live_swf_callback_fired_passes_and_names_the_callback_path() {
let log = Arc::new(std::sync::Mutex::new(Vec::new()));
let seen = log.clone();
let polls = Arc::new(AtomicUsize::new(0));
let p = polls.clone();
let addr = serve(move |chunk| {
let name = marker(chunk).to_string();
seen.lock().unwrap().push(name.clone());
match name.as_str() {
"swf_poll" if p.fetch_add(1, Ordering::SeqCst) < 2 => "[ok]\tfalse\tfalse\tfalse".into(),
n => healthy(n),
}
});
let results = run(&opts(addr)).expect("reachable");
let swf = by_name(&results, "swf_callback");
assert!(swf.pass, "{swf:?}");
assert!(swf.evidence.contains("RUNS") && swf.evidence.contains("through the load callback"), "{swf:?}");
assert!(swf.evidence.contains("PRESENT"), "{swf:?}");
assert_eq!(polls.load(Ordering::SeqCst), 3);
let order = swf_order(&log);
let pos = |n: &str| order.iter().position(|x| x == n).unwrap_or_else(|| panic!("{n} missing: {order:?}"));
assert!(pos("swf_arm") < pos("menu_open") && pos("menu_open") < pos("swf_restore"));
assert!(pos("swf_restore") < pos("menu_close") && pos("menu_close") < pos("swf_poll"));
assert_eq!(order.last().map(String::as_str), Some("swf_cleanup"));
assert_eq!(exit_code(&results), 0);
}
#[test]
fn ess_live_swf_callback_fired_late_passes_and_names_the_timeout_path() {
let addr = serve(|chunk| match marker(chunk) {
"swf_poll" => "[ok]\ttrue\ttrue\ttrue".into(),
n => healthy(n),
});
let results = run(&opts(addr)).expect("reachable");
let swf = by_name(&results, "swf_callback");
assert!(swf.pass && swf.evidence.contains("timeout fallback") && swf.evidence.contains("fired late"), "{swf:?}");
}
#[test]
fn ess_live_swf_callback_not_fired_fails_at_the_deadline_naming_the_timeout_path() {
let cleaned = Arc::new(AtomicUsize::new(0));
let c = cleaned.clone();
let addr = serve(move |chunk| match marker(chunk) {
"swf_poll" => "[ok]\tfalse\tfalse\ttrue".into(),
"swf_cleanup" => {
c.fetch_add(1, Ordering::SeqCst);
healthy("swf_cleanup")
}
n => healthy(n),
});
let mut o = opts(addr);
o.swf_deadline = Duration::from_millis(100);
let results = run(&o).expect("reachable");
let swf = by_name(&results, "swf_callback");
assert!(!swf.pass, "{swf:?}");
assert!(swf.evidence.contains("did not fire") && swf.evidence.contains("3 s timeout fallback"), "{swf:?}");
assert_eq!(cleaned.load(Ordering::SeqCst), 1, "cleanup still runs");
assert_eq!(exit_code(&results), 1);
}
#[test]
fn ess_live_swf_callback_handler_missing_fails_present_and_arms_nothing() {
let log = Arc::new(std::sync::Mutex::new(Vec::new()));
let seen = log.clone();
let addr = serve(move |chunk| {
let name = marker(chunk).to_string();
seen.lock().unwrap().push(name.clone());
match name.as_str() {
"swf_arm" => {
"[ok]\t\"table\"\t\"function\"\t\"function\"\t\"nil\"\t\"function\"\t\"function\"\t\"function\"\tfalse\tfalse".into()
}
n => healthy(n),
}
});
let results = run(&opts(addr)).expect("reachable");
let swf = by_name(&results, "swf_callback");
assert!(!swf.pass && swf.evidence.contains("PRESENT check failed"), "{swf:?}");
let order = swf_order(&log);
assert!(!order.iter().any(|n| n == "swf_restore" || n == "swf_poll" || n == "swf_cleanup"), "{order:?}");
assert!(by_name(&results, "menu_open").pass && by_name(&results, "menu_close").pass);
assert_eq!(exit_code(&results), 1);
}
#[test]
fn ess_live_swf_callback_with_the_runtime_already_built_fails_with_how_to_observe_it() {
let addr = serve(|chunk| match marker(chunk) {
"swf_arm" => {
"[ok]\t\"table\"\t\"function\"\t\"function\"\t\"function\"\t\"function\"\t\"function\"\t\"function\"\ttrue\tfalse".into()
}
n => healthy(n),
});
let results = run(&opts(addr)).expect("reachable");
let swf = by_name(&results, "swf_callback");
assert!(!swf.pass && swf.evidence.contains("already exists"), "{swf:?}");
}
#[test]
fn ess_live_an_object_that_never_goes_away_fails_after_the_deadline() {
let addr = serve(|chunk| match marker(chunk) {
"spawn_gone" => "[ok]\ttrue".into(),
name => healthy(name),
});
let mut o = opts(addr);
o.remove_deadline = Duration::from_millis(100);
let results = run(&o).expect("reachable");
let gone = by_name(&results, "spawn_gone");
assert!(!gone.pass && gone.evidence.contains("still alive"), "{gone:?}");
assert!(by_name(&results, "spawn_cleanup").pass, "cleanup still runs");
assert_eq!(exit_code(&results), 1);
}
#[test]
fn ess_live_a_bridge_that_is_down_is_unreachable() {
let addr = {
let l = std::net::TcpListener::bind("127.0.0.1:0").expect("bind");
l.local_addr().expect("addr").to_string()
};
match run(&opts(addr)) {
Err(Unreachable::Connect(_)) => {}
other => panic!("expected an unreachable bridge, got {other:?}"),
}
}
#[test]
fn ess_live_replies_parse_into_typed_values() {
assert_eq!(parse_reply("[ok]"), Reply::Ok(vec![]));
assert_eq!(
parse_reply("[ok]\tnil\ttrue\t2.5\t\"hi\"\t<table>\t<function>\t1e+006\t-1.#INF\t<tt=7 val=0000ABCD>"),
Reply::Ok(vec![
Value::Nil,
Value::Bool(true),
Value::Num(2.5),
Value::Str("hi".into()),
Value::Table,
Value::Function,
Value::Num(1_000_000.0),
Value::Num(f64::NEG_INFINITY),
Value::Other("<tt=7 val=0000ABCD>".into()),
])
);
assert_eq!(parse_reply("[runtime] \"boom\""), Reply::Runtime("\"boom\"".into()));
assert_eq!(parse_reply("[compile] \"x\""), Reply::Compile("\"x\"".into()));
assert_eq!(parse_reply("[bridge] too big"), Reply::Bridge("too big".into()));
assert_eq!(parse_reply("2"), Reply::Unrecognized("2".into()));
assert!(judge(&parse_reply("[ok]\t\"2\""), &[Expect::Num(2.0)]).is_err());
assert!(judge(&parse_reply("[ok]\t\"a\tb\""), &[Expect::AnyStr]).is_err());
assert!(judge(&parse_reply("[ok]\t2\t3"), &[Expect::Num(2.0)]).is_err());
}
#[test]
fn ess_live_lua_literals_escape_and_refuse_non_ascii() {
assert_eq!(lua_str("OnKey/a b.lua").unwrap(), "\"OnKey/a b.lua\"");
assert_eq!(lua_str("a\"b\\c").unwrap(), "\"a\\\"b\\\\c\"");
assert!(lua_str("M\u{f6}bius").is_err());
assert!(lua_str("a\nb").is_err());
}
}