//! `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 ...")); 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 ''` 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); }