Files
Questie-X/Tests/QuestieLearner_performance_spec.lua
T

679 lines
25 KiB
Lua

describe("QuestieLearner kill-path batching", function()
local queuedTimers
local broadcasts
local QuestieLearner
local simulatedTime
local function read(path)
local f = assert(io.open(path, "r"))
local content = f:read("*a")
f:close()
return content
end
local function has(text, needle)
return text and text:find(needle, 1, true) ~= nil
end
local function drainQueuedTimers()
while next(queuedTimers) do
local currentQueue = queuedTimers
queuedTimers = {}
for i = 1, table.getn(currentQueue) do
local fn = currentQueue[i]
if fn then
simulatedTime = simulatedTime + 1
fn()
end
end
end
end
before_each(function()
dofile("Tests/wow_api_mock.lua")
queuedTimers = {}
broadcasts = {}
simulatedTime = 1000
_G.GetTime = function()
return simulatedTime
end
QuestieCompat.C_Timer.After = function(_, fn)
queuedTimers[table.getn(queuedTimers) + 1] = fn
end
local originalImportModule = QuestieLoader.ImportModule
QuestieLoader.ImportModule = function(self, name)
if name == "QuestieLearnerComms" then
return {
BroadcastLearnedData = function(_, op, typ, id, data)
broadcasts[table.getn(broadcasts) + 1] = {
op = op,
typ = typ,
id = id,
data = data,
}
end,
}
end
return originalImportModule(self, name)
end
Questie.dbLearner.global.settings.enabled = true
Questie.dbLearner.global.settings.learnNpcs = true
Questie.dbLearner.global.settings.learnQuests = true
Questie.dbLearner.global.settings.learnItems = true
Questie.dbLearner.global.settings.learnObjects = true
Questie.dbLearner.global.settings.minConfidencePins = 1
Questie.dbLearner.global.npcs = {}
Questie.dbLearner.global.quests = {}
Questie.dbLearner.global.items = {}
Questie.dbLearner.global.objects = {}
QuestieDB.npcDataOverrides = {}
QuestieDB.questDataOverrides = {}
QuestieDB.itemDataOverrides = {}
QuestieDB.objectDataOverrides = {}
QuestieDB.private = {
npcCache = {
[1001] = { name = "Cached Boar" },
},
}
QuestiePlayer.currentQuestlog = {}
QuestieLearner = dofile("Modules/QuestieLearner.lua")
end)
it("coalesces repeated NPC live updates instead of invalidating DB cache per kill", function()
QuestieLearner:LearnNPC(1001, "Laggy Boar", nil, nil, nil, nil, 41.25, 52.5, 44)
QuestieLearner:LearnNPC(1001, "Laggy Boar", nil, nil, nil, nil, 41.35, 52.6, 44)
assert.is_nil(QuestieDB.npcDataOverrides[1001])
assert.is_table(QuestieDB.private.npcCache[1001])
assert.equals(2, table.getn(queuedTimers))
drainQueuedTimers()
assert.is_table(QuestieDB.npcDataOverrides[1001])
assert.is_nil(QuestieDB.private.npcCache[1001])
assert.is_table(QuestieDB.npcDataOverrides[1001][7])
local flushedSpawnCount = table.getn(QuestieDB.npcDataOverrides[1001][7][44])
QuestieLearner:LearnNPC(1001, "Laggy Boar", nil, nil, nil, nil, 80.0, 80.0, 44)
assert.equals(flushedSpawnCount, table.getn(QuestieDB.npcDataOverrides[1001][7][44]))
assert.is_true(table.getn(queuedTimers) >= 2)
end)
it("force-flushes active quest pins within the NPC live-update flush (no second debounce)", function()
local updateCount = 0
local originalUpdateQuest = QuestieQuest.UpdateQuest
QuestieQuest.UpdateQuest = function(_, questId)
updateCount = updateCount + 1
end
QuestiePlayer.currentQuestlog = { [5500] = true }
local originalGetQuest = QuestieDB.GetQuest
QuestieDB.GetQuest = function(questId)
if questId == 5500 then
return { Objectives = { [1] = { Id = 7500, spawnList = { [7500] = {} } } } }
end
return nil
end
QuestieLearner:LearnNPC(7500, "Quest Boar", nil, nil, nil, nil, 41.0, 52.0, 44)
-- Drain exactly ONE timer round (the NPC live-update flush). The pin refresh
-- must happen inside that same flush, not in a later pinRefreshDelay cycle.
local firstRound = queuedTimers
queuedTimers = {}
for i = 1, table.getn(firstRound) do
simulatedTime = simulatedTime + 1
firstRound[i]()
end
assert.is_true(updateCount >= 1)
-- And draining the rest must terminate (no infinite self-re-arming timer).
drainQueuedTimers()
QuestieDB.GetQuest = originalGetQuest
QuestieQuest.UpdateQuest = originalUpdateQuest
end)
it("coalesces repeated quest-pin refreshes into one flush", function()
local updateCount = 0
local originalUpdateQuest = QuestieQuest.UpdateQuest
QuestieQuest.UpdateQuest = function(_, questId)
updateCount = updateCount + 1
end
QuestiePlayer.currentQuestlog = { [5001] = true }
Questie.dbLearner.global.quests = {}
QuestieDB.questDataOverrides = {}
QuestieLearner:LearnQuestObjectiveNPC(5001, 7001, "Pin Boar slain", 1)
QuestieLearner:LearnQuestObjectiveNPC(5001, 7001, "Pin Boar slain", 1)
assert.is_true(table.getn(queuedTimers) >= 1)
for i = 1, table.getn(queuedTimers) do
queuedTimers[i]()
end
assert.is_table(Questie.dbLearner.global.quests[5001])
assert.is_table(QuestieDB.questDataOverrides[5001])
QuestieQuest.UpdateQuest = originalUpdateQuest
end)
it("suppresses an already queued pin refresh after switching to manual mode", function()
local updateCount = 0
local originalUpdateQuest = QuestieQuest.UpdateQuest
QuestieQuest.UpdateQuest = function(_, questId)
updateCount = updateCount + 1
end
QuestiePlayer.currentQuestlog = { [5003] = true }
Questie.dbLearner.global.quests = {}
QuestieDB.questDataOverrides = {}
QuestieLearner:LearnQuestObjectiveNPC(5003, 7003, "Manual Boar slain", 1)
assert.is_true(table.getn(queuedTimers) >= 1)
Questie.dbLearner.global.settings.pinRefreshMode = "manual"
drainQueuedTimers()
assert.equals(0, updateCount)
QuestieQuest.UpdateQuest = originalUpdateQuest
end)
it("keeps bystander UNIT_DIED eligible for learning in the source path", function()
local learner = read("Modules/QuestieLearner.lua")
assert.is_true(has(learner, "Unconditionally map the spawn position for Ascension DB building"))
assert.is_true(has(learner, "self:LearnNPC(npcId, name, nil, nil, nil, nil, px, py, zoneId)"))
assert.is_false(has(learner, 'if eventType ~= "PARTY_KILL" and not credited then'))
end)
it("still learns and batches local PARTY_KILL combat-log events", function()
QuestieLearner:OnCombatLogEvent(
1234,
"PARTY_KILL",
UnitGUID("player"),
UnitName("player"),
nil,
"Creature-0-0-0-0-7003-0000000001",
"Tagged Boar",
nil
)
assert.is_table(Questie.dbLearner.global.npcs[7003])
assert.equals("Tagged Boar", Questie.dbLearner.global.npcs[7003][1])
assert.equals(2, table.getn(queuedTimers))
end)
it("still learns credited UNIT_DIED combat-log events when PARTY_KILL is absent", function()
QuestieLearner:OnCombatLogEvent(
1234,
"UNIT_DIED",
UnitGUID("player"),
UnitName("player"),
nil,
"Creature-0-0-0-0-7004-0000000001",
"Credited Boar",
nil
)
assert.is_table(Questie.dbLearner.global.npcs[7004])
assert.equals("Credited Boar", Questie.dbLearner.global.npcs[7004][1])
assert.equals(2, table.getn(queuedTimers))
end)
it("cross-links quest giver NPCs and objects learned after the quest without duplicate pin flushes", function()
local updateCount = 0
local originalUpdateQuest = QuestieQuest.UpdateQuest
QuestieQuest.UpdateQuest = function(_, questId)
updateCount = updateCount + 1
end
Questie.db.profile.learnerBroadcast = false
QuestiePlayer.currentQuestlog = { [6001] = true }
QuestieLearner:LearnQuest(6001, {
[1] = "Cross Link Givers",
[2] = { [1] = { 7101 }, [2] = { 8101 } },
[3] = { [1] = { 7102 }, [2] = { 8102 } },
})
queuedTimers = {}
QuestieLearner:LearnNPC(7101, "Starter Guard", nil, nil, nil, nil, 10, 20, 44)
QuestieLearner:LearnNPC(7102, "Finisher Guard", nil, nil, nil, nil, 30, 40, 44)
QuestieLearner:LearnObject(8101, "Starter Chest")
QuestieLearner:LearnObject(8102, "Finisher Chest")
assert.same({ 6001 }, Questie.dbLearner.global.npcs[7101][10])
assert.same({ 6001 }, Questie.dbLearner.global.npcs[7102][11])
assert.same({ 6001 }, Questie.dbLearner.global.objects[8101][2])
assert.same({ 6001 }, Questie.dbLearner.global.objects[8102][3])
assert.same({ 6001 }, QuestieDB.npcDataOverrides[7101][10])
assert.same({ 6001 }, QuestieDB.npcDataOverrides[7102][11])
assert.same({ 6001 }, QuestieDB.objectDataOverrides[8101][2])
assert.same({ 6001 }, QuestieDB.objectDataOverrides[8102][3])
drainQueuedTimers()
assert.is_table(Questie.dbLearner.global.quests[6001])
assert.is_table(QuestieDB.questDataOverrides[6001])
QuestieQuest.UpdateQuest = originalUpdateQuest
end)
it("cross-links item objective drop NPCs into quest creature objectives", function()
Questie.db.profile.learnerBroadcast = false
QuestiePlayer.currentQuestlog = { [6002] = true }
QuestieLearner:LearnQuest(6002, {
[1] = "Cross Link Drops",
[10] = {
[3] = {
{ 2201, 0, 1, "Collect one tusk" },
},
},
})
QuestieLearner:LearnItem(2201, "Quest Tusk", 1, 1, 12, 0)
QuestieLearner:LearnItemDrop(2201, 7201)
assert.equals(7201, Questie.dbLearner.global.quests[6002][10][1][1][1])
assert.equals(7201, QuestieDB.questDataOverrides[6002][10][1][1][1])
QuestieLearner:LearnItemDrop(2201, 7201)
assert.equals(1, table.getn(Questie.dbLearner.global.quests[6002][10][1]))
assert.equals(1, table.getn(QuestieDB.questDataOverrides[6002][10][1]))
end)
it("defers learner objective tooltip registration to protected AscensionDB quest data", function()
Questie.db.profile.learnerBroadcast = false
QuestiePlayer.currentQuestlog = {}
QuestieDB.ascensionOverrideKeys = {
QUEST = {
[8334] = {
[10] = true,
},
},
}
_G._lastRegisteredTooltip = nil
QuestieLearner:LearnQuestObjectiveNPC(8334, 15271, "Tender slain", 1)
assert.is_table(Questie.dbLearner.global.quests[8334])
assert.is_nil(_G._lastRegisteredTooltip)
end)
it("coalesces repeated learner broadcasts into one latest update", function()
QuestieLearner:LearnItem(2001, "Quest Tusk", 1, 1, 12, 0)
QuestieLearner:LearnItem(2001, "Quest Tusk", 1, 1, 12, 0)
assert.equals(0, table.getn(broadcasts))
assert.equals(1, table.getn(queuedTimers))
drainQueuedTimers()
assert.equals(1, table.getn(broadcasts))
assert.equals("NEW", broadcasts[1].op)
assert.equals("ITEM", broadcasts[1].typ)
assert.equals(2001, broadcasts[1].id)
end)
it("coalesces repeated inbound network merges into one inject pass", function()
local injectCount = 0
local originalInject = QuestieLearner.InjectLearnedData
QuestieLearner.InjectLearnedData = function(self)
injectCount = injectCount + 1
QuestieLearner.data = Questie.dbLearner.global
end
QuestieLearner:HandleNetworkData("NPC", 3001, { [1] = "Net Boar", [7] = { [44] = { { 11, 22 } } } }, "NEW")
QuestieLearner:HandleNetworkData("NPC", 3001, { [1] = "Net Boar", [7] = { [44] = { { 11, 22 }, { 33, 44 } } } }, "UPDATE")
assert.equals(0, injectCount)
assert.equals(1, table.getn(queuedTimers))
drainQueuedTimers()
assert.equals(1, injectCount)
assert.equals("Net Boar", Questie.dbLearner.global.npcs[3001][1])
assert.equals(2, table.getn(Questie.dbLearner.global.npcs[3001][7][44]))
QuestieLearner.InjectLearnedData = originalInject
end)
it("forwards OnEvent payload arguments to learner handlers", function()
local originalCreateFrame = _G.CreateFrame
local originalStrsplit = _G.strsplit
local createdFrame = nil
local captured = {
questTurnedIn = nil,
questAccepted = nil,
combatLog = nil,
itemInfoReceived = nil,
questTrackingCleared = nil,
}
_G.CreateFrame = function()
createdFrame = {
RegisterEvent = function() end,
SetScript = function(self, scriptName, fn)
if scriptName == "OnEvent" then
self._onEvent = fn
end
end,
}
return createdFrame
end
_G.strsplit = function(sep, str)
local parts = {}
local pattern = string.format("([^%s]+)", sep)
for part in string.gmatch(str, pattern) do
parts[#parts + 1] = part
end
return unpack(parts)
end
QuestieLearner = dofile("Modules/QuestieLearner.lua")
local originalOnQuestTurnedIn = QuestieLearner.OnQuestTurnedIn
local originalOnQuestAccepted = QuestieLearner.OnQuestAccepted
local originalOnCombatLogEvent = QuestieLearner.OnCombatLogEvent
local originalOnGetItemInfoReceived = QuestieLearner.OnGetItemInfoReceived
local originalClearQuestObjectiveTracking = QuestieLearner.ClearQuestObjectiveTracking
QuestieLearner.OnQuestTurnedIn = function(self, questId, npcId, questFlags)
captured.questTurnedIn = { questId, npcId, questFlags }
end
QuestieLearner.OnQuestAccepted = function(self, questId, questGiver)
captured.questAccepted = { questId, questGiver }
end
QuestieLearner.OnCombatLogEvent = function(self, timestamp, eventType, srcGUID, srcName, srcFlags, dstGUID, dstName, dstFlags, spellId, spellName)
captured.combatLog = { timestamp, eventType, srcGUID, srcName, srcFlags, dstGUID, dstName, dstFlags, spellId, spellName }
end
QuestieLearner.OnGetItemInfoReceived = function(self, itemId)
captured.itemInfoReceived = { itemId }
end
QuestieLearner.ClearQuestObjectiveTracking = function(self, questId)
captured.questTrackingCleared = { questId }
end
QuestieLearner:RegisterEvents()
local frame = createdFrame
frame._onEvent(frame, "QUEST_TURNED_IN", 101, 202, 303)
frame._onEvent(frame, "QUEST_ACCEPTED", 404, 505)
frame._onEvent(frame, "COMBAT_LOG_EVENT_UNFILTERED", 1, "SPELL_DAMAGE", "src-guid", "Src", 2, "dst-guid", "Dst", 4, 777, "Fireball")
frame._onEvent(frame, "GET_ITEM_INFO_RECEIVED", 888)
frame._onEvent(frame, "QUEST_REMOVED", 999)
assert.same({ 101, 202, 303 }, captured.questTurnedIn)
assert.same({ 404, 505 }, captured.questAccepted)
assert.same({ 1, "SPELL_DAMAGE", "src-guid", "Src", 2, "dst-guid", "Dst", 4, 777, "Fireball" }, captured.combatLog)
assert.same({ 888 }, captured.itemInfoReceived)
assert.same({ 999 }, captured.questTrackingCleared)
QuestieLearner.OnQuestTurnedIn = originalOnQuestTurnedIn
QuestieLearner.OnQuestAccepted = originalOnQuestAccepted
QuestieLearner.OnCombatLogEvent = originalOnCombatLogEvent
QuestieLearner.OnGetItemInfoReceived = originalOnGetItemInfoReceived
QuestieLearner.ClearQuestObjectiveTracking = originalClearQuestObjectiveTracking
_G.CreateFrame = originalCreateFrame
_G.strsplit = originalStrsplit
end)
end)
describe("QuestieLearnerComms queue draining", function()
local sentMessages
local processedMessages
local QuestieLearnerComms
local currentTime
before_each(function()
dofile("Tests/wow_api_mock.lua")
sentMessages = {}
processedMessages = {}
currentTime = 1000
local originalImportModule = QuestieLoader.ImportModule
QuestieLoader.ImportModule = function(self, name)
if name == "QuestieLearner" then
return _G.QuestieLearner
end
return originalImportModule(self, name)
end
QuestieLoader.CreateModule = function(self, name)
_G[name] = { private = {} }
return _G[name]
end
_G.LibStub = function(name, silent)
if name == "AceComm-3.0" then
return { RegisterComm = function() end }
elseif name == "LibDeflate" then
return {
CompressDeflate = function(_, s) return s end,
EncodeForPrint = function(_, s) return s end,
DecodeForPrint = function(_, s) return s end,
DecompressDeflate = function(_, s) return s end,
}
elseif name == "AceSerializer-3.0" then
return {
Serialize = function(_, payload)
return payload.op .. ":" .. tostring(payload.id)
end,
Deserialize = function(_, serialized)
local op, id = string.match(serialized, "([^:]+):(.+)")
return true, {
_ver = 2,
op = op,
typ = "NPC",
id = tonumber(id) or id,
d = { [1] = "net" },
}
end,
}
elseif name == "XXH_Lua_Lib" then
return nil
elseif name == "HereBeDragonsQuestie-2.0" then
return {}
end
return {}
end
_G.QuestieLearner = {
HandleNetworkData = function(_, typ, id, d, op)
processedMessages[table.getn(processedMessages) + 1] = {
typ = typ,
id = id,
op = op,
}
end,
}
_G.GetChannelName = function() return 1 end
_G.JoinPermanentChannel = function() end
_G.ChatFrame_RemoveChannel = function() end
_G.DEFAULT_CHAT_FRAME = { GetID = function() return 1 end }
_G.SendChatMessage = function(msg, mode, nilarg, channel)
sentMessages[table.getn(sentMessages) + 1] = msg
end
_G.InCombatLockdown = function() return false end
_G.random = function() return 0 end
_G.GetTime = function()
return currentTime
end
QuestieCompat.C_Timer.NewTicker = function(delay, fn)
return { Cancel = function() end }
end
dofile("Modules/Network/QuestieLearnerComms.lua")
QuestieLearnerComms = _G.QuestieLearnerComms
QuestieLearnerComms:Initialize()
end)
it("drains outgoing and incoming queues in FIFO order without front-removal", function()
QuestieLearnerComms:BroadcastLearnedData("NEW", "NPC", 101, { foo = "a" })
QuestieLearnerComms:BroadcastLearnedData("UPDATE", "NPC", 102, { foo = "b" })
QuestieLearnerComms:OnCommReceived("QuestieLearner", "NEW:201", "CHANNEL", "Alice")
QuestieLearnerComms:OnCommReceived("QuestieLearner", "UPDATE:202", "CHANNEL", "Bob")
QuestieLearnerComms.private:ProcessQueues()
currentTime = currentTime + 4
QuestieLearnerComms.private:ProcessQueues()
currentTime = currentTime + 4
QuestieLearnerComms.private:ProcessQueues()
assert.equals(2, table.getn(sentMessages))
assert.equals("NEW:101", sentMessages[1])
assert.equals("UPDATE:102", sentMessages[2])
assert.equals(2, table.getn(processedMessages))
assert.equals(201, processedMessages[1].id)
assert.equals(202, processedMessages[2].id)
end)
end)
describe("QuestieComms packet sizing", function()
local serializeCount
local sendCount
local QuestieComms
before_each(function()
dofile("Tests/wow_api_mock.lua")
serializeCount = 0
sendCount = 0
local originalCreateModule = QuestieLoader.CreateModule
local originalImportModule = QuestieLoader.ImportModule
QuestieLoader.CreateModule = function(self, name)
_G[name] = { private = {} }
return _G[name]
end
QuestieLoader.ImportModule = function(self, name)
if name == "QuestieSerializer" then
return {
Serialize = function(_, payload)
serializeCount = serializeCount + 1
return "packet:" .. tostring(payload.id or payload[1] or "x")
end,
Deserialize = function() return true, {} end,
}
end
return originalImportModule(self, name)
end
_G.LibStub = function(name, silent)
if name == "AceComm-3.0" then
return { RegisterComm = function() end }
elseif name == "LibDeflate" then
return {
CompressDeflate = function(_, s) return s end,
EncodeForPrint = function(_, s) return s end,
DecodeForPrint = function(_, s) return s end,
DecompressDeflate = function(_, s) return s end,
}
elseif name == "AceSerializer-3.0" then
return {
Serialize = function(_, payload) return "packet:" .. tostring(payload.id or payload[1] or "x") end,
Deserialize = function() return true, {} end,
}
elseif name == "XXH_Lua_Lib" then
return nil
elseif name == "HereBeDragonsQuestie-2.0" then
return {}
end
return {}
end
_G.QuestiePlayer.GetGroupType = function() return "party" end
_G.QuestiePlayer.numberOfGroupMembers = 5
_G.QuestieDB.QuestPointers = {
[101] = true,
[102] = true,
[103] = true,
}
_G.QuestieDB.QueryQuestSingle = function()
return 0
end
_G.QuestieDB.GetQuest = function(questId)
return {
Objectives = {
[1] = { Id = questId * 10 },
},
}
end
_G.QuestLogCache.questLog_DO_NOT_MODIFY = {
[101] = { questTag = "Normal" },
[102] = { questTag = "Normal" },
[103] = { questTag = "Normal" },
}
_G.QuestLogCache.GetQuestObjectives = function()
return {
{ type = "monster", numFulfilled = 0, numRequired = 1 },
}
end
_G.ZoneDB.GetUiMapIdByAreaId = function()
return 1
end
_G.HBD = {
GetZoneDistance = function() return 1 end,
GetPlayerZone = function() return 1 end,
}
_G.GetChannelName = function() return 1 end
_G.SendChatMessage = function() end
_G.Questie.SendCommMessage = function()
sendCount = sendCount + 1
end
_G.UnitInBattleground = function() return false end
_G.random = function() return 0 end
_G.GetTime = function() return 0 end
_G.tinsert = table.insert
QuestieCompat.C_Timer.After = function(_, fn) end
QuestieCompat.C_Timer.NewTicker = function(_, fn)
return { Cancel = function() end }
end
QuestieComms = dofile("Modules/Network/QuestieComms.lua")
QuestieComms.private.CreatePacket = function()
return {
data = {},
write = function() end,
}
end
serializeCount = 0
end)
it("serializes each quest once while packing broadcast blocks", function()
QuestieComms.private:BroadcastQuestLog("QC_ID_BROADCAST_FULL_QUESTLIST", "WHISPER", "Tester")
assert.equals(3, serializeCount)
serializeCount = 0
QuestieComms.private:BroadcastQuestLogV2("QC_ID_BROADCAST_FULL_QUESTLIST", "WHISPER", "Tester")
assert.equals(3, serializeCount)
end)
it("suppresses outgoing QuestieComms immediately when disabled", function()
Questie.db.profile.questieCommsEnabled = false
local packet = QuestieComms.private:CreatePacket(QuestieComms.private.QC_ID_BROADCAST_QUEST_REMOVE)
packet.data.writeMode = QuestieComms.private.QC_WRITE_ALLGROUP
packet.data.priority = "NORMAL"
packet.data.id = 101
packet:write()
assert.equals(0, sendCount)
end)
end)