Files
2026-10-30 16:56:31 +00:00

151 lines
7.7 KiB
Lua

-- usage: lua50 test_slt.lua <slt dir> <bonusscanner dir>
local SLT, BS = arg[1], arg[2]
dofile("tests/mockwow.lua")
local clock = 0
function GetTime() return clock end
local fails = 0
local function check(name, cond, extra)
print((cond and "PASS " or "FAIL ") .. name .. (extra and (" " .. extra) or ""))
if not cond then fails = fails + 1 end
end
-- load BonusScanner for parity reference
dofile(BS .. "/Localization.lua"); dofile(BS .. "/BonusScanner.lua")
MOCK.gear = MOCK.SAMPLE_GEAR
MOCK.makeBonusScannerFrames()
MOCK.fire("PLAYER_ENTERING_WORLD"); MOCK.tick(1.1)
local bsD, bsS = BonusScanner:GetBonus("DMG"), BonusScanner:GetBonus("SHADOWDMG")
-- load SLT v4
dofile(SLT .. "/Locale.lua"); dofile(SLT .. "/Scanner.lua"); dofile(SLT .. "/ScaledLifeTap.lua")
MOCK.fire("VARIABLES_LOADED"); MOCK.fire("PLAYER_ENTERING_WORLD")
-- 1. parity with BonusScanner on sample gear
SLT_Config.source = "internal"
local d, s = ScaledLifeTap_Scanner:Get()
check("built-in scanner matches BonusScanner on sample gear", d == bsD and s == bsS,
string.format("built-in D=%d S=%d / BonusScanner D=%d S=%d", d, s, bsD, bsS))
-- 2. parity on assorted edge-case lines (each fed as its own item)
local cases = {
{"x","+40 Shadow Spell Damage"}, {"x","Shadow Damage +20"}, {"x","+13 Spell Damage and Healing/+1 Stamina"},
{"x","Minor Wizard Oil (30 min)"}, {"x","Lesser Wizard Oil"}, {"x","Wizard Oil"},
{"x","Spell Damage +15 and +1% Spell Critical Strike"}, {"x","+20 Fire Spell Damage"},
{"x","Equip: Increases damage done to Undead by magical spells and effects by up to 60."},
{"x","Equip: Increases healing done by spells and effects by up to 44."},
{"x","+10 Shadow Resistance"}, {"x","Use: Increases damage and healing done by magical spells and effects by up to 175 for 15 sec."},
{"x","Set A (2/5)", "Set: Increases damage and healing done by magical spells and effects by up to 23.", "(4) Set: Increases damage and healing done by magical spells and effects by up to 99."},
}
for i, item in ipairs(cases) do
MOCK.gear = { [1] = item }
BonusScanner:ScanEquipment()
ScaledLifeTap_Scanner:MarkDirty()
local d2, s2 = ScaledLifeTap_Scanner:Get()
local ok = d2 == tonumber(BonusScanner:GetBonus("DMG")) and s2 == tonumber(BonusScanner:GetBonus("SHADOWDMG"))
check("parity: " .. item[2], ok, string.format("v4 D=%d S=%d / BS D=%d S=%d", d2, s2, BonusScanner:GetBonus("DMG"), BonusScanner:GetBonus("SHADOWDMG")))
end
-- set bonus counted once even when 3 pieces carry the same "Set:" line
MOCK.gear = {
[1] = {"a","Set X (3/8)","Set: Increases damage and healing done by magical spells and effects by up to 23."},
[3] = {"b","Set X (3/8)","Set: Increases damage and healing done by magical spells and effects by up to 23."},
[5] = {"c","Set X (3/8)","Set: Increases damage and healing done by magical spells and effects by up to 23."},
}
ScaledLifeTap_Scanner:MarkDirty()
check("active set bonus counted once", (ScaledLifeTap_Scanner:Get()) == 23)
-- 3. lazy scanning: no rescans without inventory change; party events ignored
MOCK.gear = MOCK.SAMPLE_GEAR
ScaledLifeTap_Scanner:MarkDirty(); ScaledLifeTap_Scanner:Get()
MOCK.tooltipCalls = 0
for i = 1, 100 do ScaledLifeTap_Scanner:Get() end
check("100 casts with unchanged gear -> 0 rescans", MOCK.tooltipCalls == 0)
MOCK.fire("UNIT_INVENTORY_CHANGED", "party1"); ScaledLifeTap_Scanner:Get()
check("party member gear change ignored", MOCK.tooltipCalls == 0)
MOCK.fire("UNIT_INVENTORY_CHANGED", "player"); ScaledLifeTap_Scanner:Get()
check("player gear change -> exactly one rescan", MOCK.tooltipCalls == 17, MOCK.tooltipCalls .. " slot reads")
local hasOnUpdate = false
for _, fr in ipairs(MOCK.frames) do if fr.name and string.find(fr.name, "ScaledLifeTap") and fr.scripts.OnUpdate then hasOnUpdate = true end end
check("no OnUpdate handler", not hasOnUpdate)
-- 4. no Life Tap in spellbook: v3 would loop forever, v4 must return
MOCK.spellbook = { {"Shadow Bolt","Rank 1"} }
MOCK.fire("SPELLS_CHANGED"); MOCK.casts = {}
ScaledLifeTap_CastLifeTap()
check("no Life Tap learned: returns without casting", table.getn(MOCK.casts) == 0)
-- 5. rank selection
MOCK.spellbook = { {"Shadow Bolt","Rank 9"}, {"Life Tap","Rank 1"}, {"Life Tap","Rank 2"}, {"Life Tap","Rank 3"},
{"Life Tap","Rank 4"}, {"Life Tap","Rank 5"}, {"Life Tap","Rank 6"}, {"Corruption","Rank 7"} }
MOCK.fire("SPELLS_CHANGED")
SLT_Config.bonus = 0
-- bonus = 443+75 = 518; Imp LT 2/2 -> x1.2
-- r6: cost 424+0.8*518=838.4 gain 1006; r5: 724.4/869; r4 634.4/761; r3 554.4/665; r2 427.2/513; r1 226.8/272
local function cast(hp, mana)
MOCK.hp, MOCK.mana = hp, mana; MOCK.casts = {}
ScaledLifeTap_CastLifeTap()
return MOCK.casts[1]
end
check("missing 4000 mana -> rank 6", cast(3500, 1000) == "id:7")
check("missing 900 mana -> rank 5", cast(3500, 4100) == "id:6")
check("missing 600 mana -> rank 2", cast(3500, 4400) == "id:3")
check("missing 100 mana, overtap on -> rank 1", cast(3500, 4900) == "id:2")
SLT_Config.overtap = false
check("missing 100 mana, overtap off -> nothing", cast(3500, 4900) == nil)
SLT_Config.overtap = true
check("full mana -> nothing", cast(3500, 5000) == nil)
check("low health (800) -> highest rank that leaves >0 hp (r5, 724)", cast(800, 1000) == "id:6")
check("health 200 -> nothing (would cost more than you have)", cast(200, 1000) == nil)
SLT_Config.reserve = 50
check("reserve 50% (1750) at 2300 hp -> r2 (r3 would leave 1745)", cast(2300, 1000) == "id:3")
SLT_Config.reserve = 0
MOCK.talents[1][5][2] = 0; MOCK.fire("CHARACTER_POINTS_CHANGED")
check("no Imp LT talent: missing 840 -> r6 fits exactly (838)", cast(3500, 4160) == "id:7")
MOCK.talents[1][5][2] = 2; MOCK.fire("CHARACTER_POINTS_CHANGED")
-- 6. source=auto uses BonusScanner when loaded
MOCK.loaded = { BonusScanner = true }
SLT_Config.source = "auto"
MOCK.gear = { [1] = {"x","+100 Spell Damage"} }
ScaledLifeTap_Scanner:MarkDirty()
BonusScanner:ScanEquipment()
local total, _, _, src = ScaledLifeTap_GetDamageBonus()
check("auto source skips BonusScanner (uses built-in)", src == "built-in scanner", src .. " " .. total)
SLT_Config.source = "bonusscanner"
total, _, _, src = ScaledLifeTap_GetDamageBonus()
check("forced source bonusscanner still works", src == "BonusScanner" and total == 100, src .. " " .. total)
SLT_Config.source = "auto"
MOCK.loaded = nil
-- 7. v3 saved variable migration
SLT_Config = { overtapEnabled = false }
MOCK.fire("VARIABLES_LOADED")
check("v3 overtapEnabled=false migrated", SLT_Config.overtap == false and SLT_Config.source == "auto")
-- 8. slash commands don't error
MOCK.echo = false
for _, c in ipairs({"", "status", "overtap", "debug on", "reserve 20", "bonus 40", "source internal", "rescan", "cast", "garbage"}) do
SlashCmdList["SCALEDLIFETAP"](c)
end
check("slash commands run", SLT_Config.reserve == 20 and SLT_Config.bonus == 40 and SLT_Config.source == "internal")
-- 9. performance: built-in scan vs BonusScanner
MOCK.gear = MOCK.SAMPLE_GEAR
collectgarbage(); collectgarbage(1e9)
local N = 2000
local kb0, t0 = gcinfo(), os.clock()
for i = 1, N do ScaledLifeTap_Scanner:Scan() end
local tnew, knew = (os.clock() - t0) / N * 1e6, (gcinfo() - kb0) / N
collectgarbage(); collectgarbage(1e9)
kb0, t0 = gcinfo(), os.clock()
for i = 1, N do BonusScanner:ScanEquipment() end
local tbs, kbs = (os.clock() - t0) / N * 1e6, (gcinfo() - kb0) / N
print(string.format("PERF full gear scan: v4 built-in %.0f us, %.2f KB garbage | BonusScanner %.0f us, %.2f KB garbage", tnew, knew, tbs, kbs))
collectgarbage(); collectgarbage(1e9)
SLT_Config.source = "internal"; SLT_Config.debug = false
kb0, t0 = gcinfo(), os.clock()
for i = 1, 100000 do MOCK.mana = 1000; ScaledLifeTap_CastLifeTap() end
print(string.format("PERF cast with cached gear: %.2f us/press, %.3f KB garbage/press", (os.clock() - t0) / 100000 * 1e6, (gcinfo() - kb0) / 100000))
print(fails == 0 and "ALL TESTS PASSED" or (fails .. " FAILED"))