Files
benilla-octo/crates/benilla-ui/examples/uiparent_probe.rs
T

157 lines
7.7 KiB
Rust

//! `uiparent_probe`: run the reported spot, not a paraphrase of it.
//!
//! ```text
//! cargo run -q -p benilla-ui --example uiparent_probe -- /path/to/UIParent.lua
//! cargo run -q -p benilla-ui --example uiparent_probe -- /path/to/FriendsFrame.lua
//! ```
//!
//! Loads a real TurtleWoW `Interface\FrameXML` file through benilla's VM and calls the function its
//! 1.12-era error was reported in, so the raise is reproduced at the exact line and then cleared:
//!
//! - `UIParent.lua` — `UIFrameFade_CreateInfo`, whose `wipe(frame.fadeInfo)` (line 1420) raised
//! `attempt to call global 'wipe' (a nil value)`.
//! - `FriendsFrame.lua` — the guild-roster filter's `trim(strlower(...))` (line 490).
//!
//! These files load far past the reported line in a real session, so a clean `OK` means the chunk
//! parsed, its globals landed, and the call itself behaved. Exits 1 on any raise, naming the chunk.
use std::path::Path;
use benilla_ui::script::UiScript;
fn main() {
let mut args = std::env::args().skip(1);
let path = args
.next()
.unwrap_or_else(|| panic!("usage: uiparent_probe <UIParent.lua|FriendsFrame.lua>..."));
let bytes = std::fs::read(Path::new(&path)).expect("read the FrameXML file");
let name = format!("Interface\\FrameXML\\{}", file_name(&path));
let script = UiScript::new().expect("new UiScript");
script
.run_chunk_named(&bytes, &name)
.unwrap_or_else(|e| fail(&format!("loading {name}"), e));
// Each file is probed at the function a 1.12-era error was reported in, chosen by its name.
let call = match file_name(&path).as_str() {
"UIParent.lua" => {
r#"
local f = CreateFrame("Frame")
local first = UIFrameFade_CreateInfo(f)
first.mode = "IN"
first.timeToFade = 0.5
local second = UIFrameFade_CreateInfo(f)
-- the wipe branch never reassigns `frame.fadeInfo`, so it returns the SAME table,
-- emptied: which is what makes `wipe` a same-identity clear, not a replacement
return first == second and first.mode == nil and first.timeToFade == nil
and getn(first) == 0
"#
}
"FriendsFrame.lua" => {
r#"
-- The reported line 490 sits inside `GuildStatus_Update`, so the probe calls THAT, not a
-- transcription of the line — the traceback then names FriendsFrame.lua:490 the way the
-- session did. The handful of globals it touches first are stubbed here; each is a
-- no-argument answer the real binding supplies, and an empty roster means the filter
-- loop never runs, which leaves the trim/wipe pair as the reachable work.
local function stub(n) return function() return n end end
-- Every widget this file reaches for is created by `FriendsFrame.xml` at load time, not
-- by its Lua, so a Lua-only chunk starts with none of them. Naming each one would be a
-- list as long as the file; a `_G` metatable answers them all the way the XML would, and
-- it is keyed on the name so the API the probe IS testing still raises.
local FRAME_NAMES = {
FriendsFrame = true, GuildFrame = true, SearchResult = true,
}
local function frame_stub()
return { Enable = stub(nil), Disable = stub(nil), Show = stub(nil),
Hide = stub(nil), SetText = stub(nil), SetTextColor = stub(nil),
SetPoint = stub(nil), ClearAllPoints = stub(nil), SetParent = stub(nil),
SetHeight = stub(nil), SetWidth = stub(nil), SetScale = stub(nil),
ShowChildren = stub(nil), GetText = stub(" Guild Name "),
GetName = stub("ProbeFrame"), GetObjectType = stub("Frame"),
GetParent = stub(nil), IsShown = stub(nil), GetNumChildren = stub(0),
GetChildren = stub({}), SetAllPoints = stub(nil), SetFocus = stub(nil) }
end
local under_test = { wipe = true, trim = true, strsplit = true, strjoin = true }
-- Answered as frames, but really bare functions in the 1.12 UI: a frame stub would
-- raise `attempt to call global 'X' (a table value)` on the first one the file reaches.
-- 1.12's FauxScrollFrame API is bare functions (`FauxScrollFrame_Update`, `..._SetOffset`),
-- and the family is long enough that listing it is how the probe goes stale. Every one
-- is matched by prefix; the `Get*` shapes answer 0, which is an empty scrollbar.
local function is_bare(name)
return strfind(name, "FauxScrollFrame_") ~= nil
end
setmetatable(_G, {
__index = function(_, name)
-- The four names the probe is here to test stay nil, so a missing one still
-- raises `attempt to call global '<name>'` at the real line.
if under_test[name] then return nil end
if is_bare(name) then
local gets = strfind(name, "_Get") ~= nil
local fn = stub(gets and 0 or nil)
rawset(_G, name, fn)
return fn
end
if not (strfind(name, "Frame") or strfind(name, "Button")
or strfind(name, "Text") or FRAME_NAMES[name]) then
return nil
end
local f = frame_stub()
rawset(_G, name, f)
return f
end,
})
-- Bare-API globals the metatable above would otherwise answer with a frame stub, because
-- their names read like widgets: `GetText("GUILD_TOTAL", ...)` is 1.12's localisation
-- lookup, not a widget method.
GetText = stub("%d")
GUILD_TOTAL = "Guild Total"
GUILD_TOTALONLINE = "Guild Total Online"
-- The same trap for the FauxScrollFrame API: `FauxScrollFrame_*` are bare functions in
-- the 1.12 UI, but the name matches no widget suffix, so they are listed as one.
PanelTemplates_SetTab = stub(nil)
ShowUIPanel = stub(nil)
HideUIPanel = stub(nil)
GetNumGuildMembers = stub(0)
GetGuildRosterInfo = stub("")
GetGuildRosterSelection = stub(0)
GetGuildRosterMOTD = stub("")
FRIENDS_LEVEL_TEMPLATE = "%d %s"
IsGuildLeader = stub(nil)
CanGuildInvite = stub(nil)
CanEditMOTD = stub(nil)
GetGuildInfo = stub(nil)
GetGuildLeaderInfo = stub(nil)
UnitName = stub("probe")
GetClassInfo = stub("Warrior")
GUILD_MOTD_EDITLABEL = "motd"
GuildStatus_Update()
-- the wipe ran on the global roster table, and the trim fed it a clean search string
return getn(SearchResult) == 0
"#
}
other => panic!("no probe written for {other} — add the function its error named"),
};
let ok: bool = script
.eval(call)
.unwrap_or_else(|e| fail(&format!("the reported call in {name}"), e));
if !ok {
eprintln!(
"FAILED: the reported call in {name} ran but did not behave as 1.12-era code expects"
);
std::process::exit(1);
}
println!("OK: {name} loaded and the reported call ran clean");
}
fn file_name(path: &str) -> String {
path.rsplit(['/', '\\']).next().unwrap_or(path).to_string()
}
fn fail(stage: &str, e: mlua::Error) -> ! {
eprintln!("FAILED while {stage}: {e}");
std::process::exit(1);
}