Files
ShaguDPS/tests/threat.lua
T

302 lines
12 KiB
Lua

-- Run from the repository root with Lua 5.0: lua tests/threat.lua
local now, party, raid, target, combat = 10, 1, 0, true, true
local sent, refreshes, draws = {}, 0, 0
local visible = true
function GetTime() return now end
function GetNumPartyMembers() return party end
function GetNumRaidMembers() return raid end
function UnitExists() return target end
function UnitIsPlayer() return nil end
local dead = false
function UnitIsDead() return dead end
function UnitClassification() return 'elite' end
function UnitAffectingCombat() return combat end
function UnitName(unit) return unit == 'player' and 'Tester' or 'Boss' end
function SendAddonMessage(prefix, text, channel)
table.insert(sent, {prefix, text, channel})
end
function CreateFrame()
return {scripts={}, RegisterEvent=function() end,
SetScript=function(self, name, fn) self.scripts[name] = fn end}
end
ShaguDPS = {
config = {[1]={view=5,bars=8}},
data = {classes={}},
parser = {ScanName=function() end, callbacks={refresh={function() refreshes=refreshes+1 end}}},
window = {[1]={IsVisible=function() return visible end, Refresh=function() draws=draws+1 end}},
}
dofile('threat.lua')
local threat, data = ShaguDPS.threat, ShaguDPS.data
local function update()
this = threat
threat.scripts.OnUpdate()
end
local function emit(kind, text)
this, event, arg1, arg2 = threat, kind, 'SERVER', text
threat.scripts.OnEvent()
end
update()
assert(table.getn(sent) == 1 and sent[1][1] == 'TWT_UDTSv4' and sent[1][2] == 'limit=8' and sent[1][3] == 'PARTY')
emit('CHAT_MSG_ADDON', 'TWTv4=Tank:1:1000.5:100.0:1;Tester:0:500.25:50.0:1;#TMTv1=ignored')
assert(data.threat.Tank and data.threat.Tank._sum == 1000.5, 'decimal threat must be parsed')
assert(data.threat.Tester._perc == 50 and data.threat.Tester._melee)
assert(data.threat['Pull Aggro']._sum == 1100)
now = 12
emit('CHAT_MSG_ADDON', 'TWTv4=Tank:1:1200:100:1;Bad:0:oops:10:1;')
assert(data.threat.Tank._sum == 1000.5, 'malformed snapshots leave the previous data intact')
emit('CHAT_MSG_ADDON', 'TWTv4=Tank:1:1200:100:1;Tester:0:700:58:0;')
assert(data.threat.Tester._tps == 99.875)
assert(not data.threat.Bad)
assert(data.threat['Pull Aggro']._sum == 1560)
update()
assert(table.getn(sent) == 1, 'recent external answers suppress duplicate queries')
now = 13.5
update()
assert(table.getn(sent) == 2, 'queries resume after external answers stop')
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:0:0:0:0;')
assert(not data.threat.Tank and not data.threat['Pull Aggro'])
emit('PLAYER_TARGET_CHANGED')
assert(not next(data.threat))
assert(not threat.last_answer and threat.pending)
assert(table.getn(sent) == 3, 'target changes query immediately')
now, party = 15, 0
update()
assert(table.getn(sent) == 3, 'no queries while solo')
now, party, combat = 16, 1, false
update()
assert(table.getn(sent) == 3, 'no queries out of combat')
now, combat, visible = 17, true, false
update()
assert(table.getn(sent) == 3, 'no queries with hidden threat windows')
now, visible, raid = 18, true, 10
update()
assert(sent[4][3] == 'RAID')
assert(draws > 0 and refreshes == 0, 'threat does not refresh damage/heal callbacks')
now = 22
update()
assert(string.find(threat:GetStatus(), 'No threat reply', 1, true))
party, raid = 0, 0
assert(string.find(threat:GetStatus(), 'Join a party', 1, true))
party, target = 1, false
assert(string.find(threat:GetStatus(), 'Select a living', 1, true))
target, combat = true, false
assert(string.find(threat:GetStatus(), 'enter combat', 1, true))
combat = true
emit('PLAYER_TARGET_CHANGED')
assert(string.find(threat:GetStatus(), 'Waiting for threat data', 1, true))
emit('CHAT_MSG_ADDON', 'TWTv4=')
assert(string.find(threat:GetStatus(), 'Waiting for threat data', 1, true), 'previous target reply discarded')
now = 23
update()
emit('CHAT_MSG_ADDON', 'TWTv4=')
assert(string.find(threat:GetStatus(), 'no threat entries', 1, true))
-- Poll at 2 Hz, without waiting for the normal window refresh timer.
emit('PLAYER_TARGET_CHANGED')
emit('CHAT_MSG_ADDON', 'TWTv4=')
now = 30
update()
emit('CHAT_MSG_ADDON', 'TWTv4=')
local requests, previous_draws = table.getn(sent), draws
now = 30.21
update()
assert(table.getn(sent) == requests, 'polls must be rate limited')
now = 30.51
update()
assert(table.getn(sent) == requests + 1, 'poll again after 0.5 seconds')
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:100:100:1;')
assert(draws > previous_draws, 'visible threat window refreshes immediately')
assert(data.threat.Tester._tps == 0, 'first sample has no elapsed time')
now = 30.71
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:120:100:1;')
assert(math.abs(data.threat.Tester._tps - 100) < 0.001, 'TPS updates on the next sample')
now = 30.91
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:110:100:1;')
assert(data.threat.Tester._tps == 0, 'threat reduction resets TPS baseline')
now = 31.11
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:130:100:1;')
assert(math.abs(data.threat.Tester._tps - 100) < 0.001)
emit('CHAT_MSG_ADDON', 'TWTv4=')
now = 31.3
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:200:100:1;')
assert(data.threat.Tester._tps == 0, 'returning players start a fresh sample')
visible = false
previous_draws = draws
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:210:100:1;')
assert(draws == previous_draws, 'hidden windows are not redrawn')
-- Recent replies from another addon must not suppress a new target query.
visible = true
now = 40
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:300:100:1;')
local before_switch = table.getn(sent)
emit('PLAYER_TARGET_CHANGED')
assert(table.getn(sent) == before_switch + 1)
assert(not next(data.threat), 'clear previous target data immediately')
update()
assert(table.getn(sent) == before_switch + 1, 'no duplicate poll in the same frame')
visible = false
emit('PLAYER_TARGET_CHANGED')
assert(table.getn(sent) == before_switch + 1, 'hidden windows do not query on target change')
visible, target = true, false
emit('PLAYER_TARGET_CHANGED')
assert(table.getn(sent) == before_switch + 1, 'no query when target is lost')
local messages = {}
DEFAULT_CHAT_FRAME = { AddMessage=function(self, text) table.insert(messages, text) end }
now, visible, target = 50, true, true
threat.last_foreign = nil
threat:Measure()
for sample = 0, 19 do
now = 50 + sample * 0.5
update()
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:300:100:1;')
end
assert(table.getn(messages) == 1, 'diagnostic does not spam each packet')
now = 60
update()
assert(table.getn(messages) == 2)
assert(string.find(messages[2], '20 requests (2.00/s)', 1, true))
assert(string.find(messages[2], '20 replies (2.00/s)', 1, true))
assert(string.find(messages[2], '0 changed replies', 1, true))
now = 61
update()
emit('CHAT_MSG_ADDON', 'TWTv4=')
assert(table.getn(messages) == 2, 'measurement stops automatically')
-- A constant 100 TPS stream remains stable across rolling-window boundaries.
now = 70
emit('PLAYER_TARGET_CHANGED')
for i = 0, 16 do
now = 70 + i * 0.5
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:' .. (100 + i * 50) .. ':100:1;')
if i > 0 then assert(math.abs(data.threat.Tester._tps - 100) < 0.001) end
end
-- Repeated identical totals age the old gains out of the five-second window.
for i = 1, 10 do
now = 78 + i * 0.5
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:900:100:1;')
end
assert(data.threat.Tester._tps == 0)
now = 83.5
emit('CHAT_MSG_ADDON', 'TWTv4=Tank:1:1000:100:1;')
assert(data.threat['Pull Aggro']._minimum == 1100)
assert(data.threat['Pull Aggro']._sum == 1300)
assert(data.threat['Pull Aggro']._perc == 100)
now = 84
emit('CHAT_MSG_ADDON', 'TWTv4=Tank:1:1000:100:1;Tester:0:100:10:0;')
assert(not data.threat['Pull Aggro']._minimum and data.threat['Pull Aggro']._perc == 100)
now = 87.1
update()
assert(not next(data.threat), 'expire old bars after three seconds without replies')
assert(string.find(threat:GetStatus(), 'stopped arriving', 1, true))
now = 87.2
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:200:100:1;')
assert(data.threat.Tester._tps == 0 and not threat.stale, 'fresh reply recovers from stale state')
now, dead = 88, true
update()
assert(not next(data.threat), 'clear bars on target death')
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:300:100:1;')
assert(not next(data.threat), 'ignore late reply for a dead target')
dead = false
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:300:100:1;')
now, combat = 89, false
update()
assert(not next(data.threat), 'clear bars on combat end')
combat = true
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:300:100:1;')
now, party, raid = 90, 0, 0
update()
assert(not next(data.threat), 'clear bars on leaving the group')
assert(refreshes == 0, 'damage/heal callbacks remain untouched')
-- Same-name mobs remain distinct when client GUIDs are available.
party, raid, target, combat, dead = 1, 0, true, true, false
local targetGuid = '0xA'
UnitGUID = function() return targetGuid end
now = 100
emit('PLAYER_TARGET_CHANGED')
targetGuid = '0xB'
local before_guard = table.getn(sent)
emit('PLAYER_TARGET_CHANGED')
assert(table.getn(sent) == before_guard and not threat.pending)
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:111:100:1;')
assert(not next(data.threat), 'old same-name target reply must not populate new target')
now = 100.8
update()
assert(threat.pending and table.getn(sent) == before_guard + 1)
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:222:100:1;')
assert(data.threat.Tester._sum == 222)
-- An expired pending request must not permanently block fresh replies.
now = 103
update()
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:333:100:1;')
assert(data.threat.Tester._sum == 333)
-- Captured in-game examples: keep server percentages, never normalize twice.
now = 105
emit('CHAT_MSG_ADDON', 'TWTv4=Francin:1:2034:100:1;Tester:0:516:19.5:0;')
assert(data.threat.Tester._perc == 19.5 and data.threat.Francin._perc == 100)
assert(data.threat['Pull Aggro']._sum == math.floor(2034 * 1.3))
assert(data.threat['Pull Aggro']._perc == 100)
now = 106
emit('CHAT_MSG_ADDON', 'TWTv4=Francin:1:2614:100:1;Tester:0:486:16.9:1;')
assert(data.threat.Tester._perc == 16.9)
assert(data.threat['Pull Aggro']._sum == math.floor(2614 * 1.1))
assert(data.threat['Pull Aggro']._perc == 100)
-- One outstanding poll, with retry delay and late-reply quarantine.
now = 110
emit('PLAYER_TARGET_CHANGED')
local first_poll = table.getn(sent)
now = 110.5
update()
assert(table.getn(sent) == first_poll and threat.pending)
now = 111.5
update()
assert(not threat.pending and table.getn(sent) == first_poll)
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:1:999:100:1;')
assert(not next(data.threat))
now = 112
update()
assert(table.getn(sent) == first_poll, 'retry waits after timeout')
now = 112.5
update()
assert(table.getn(sent) == first_poll + 1)
now = 114
update()
now = 115
update()
assert(table.getn(sent) == first_poll + 1, 'consecutive failures increase the delay')
now = 116
update()
assert(table.getn(sent) == first_poll + 2)
-- Warning only fires on usable local replies, rearms below the threshold,
-- and never warns the player already holding aggro.
local warnings = 0
UIErrorsFrame = {AddMessage=function() warnings = warnings + 1 end}
ShaguDPS.config.threat_warning = 3 -- 90%
local function reply(perc, tank)
emit('CHAT_MSG_ADDON', 'TWTv4=Tester:' .. (tank or 0) .. ':900:' .. perc .. ':1;')
end
reply(90)
assert(warnings == 1)
local function pollReply(perc, tank)
now = now + .5
update()
assert(threat.pending)
reply(perc, tank)
end
pollReply(91)
pollReply(89)
pollReply(92)
assert(warnings == 1, 'no repeat from small fluctuations')
pollReply(84)
pollReply(92)
assert(warnings == 2)
pollReply(100, 1)
assert(warnings == 2, 'no warning for the aggro holder')
reply(95)
assert(warnings == 2, 'unmatched reply does not warn')
now = now + .5
emit('PLAYER_TARGET_CHANGED')
reply(95)
assert(warnings == 3, 'target change resets the warning')
ShaguDPS.config.threat_warning = 0
pollReply(99)
assert(warnings == 3, 'warning can be disabled')
print('Threat regression checks passed (Lua 5.0)')