157 lines
7.7 KiB
Rust
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);
|
|
}
|