You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
354 lines
11 KiB
354 lines
11 KiB
--@non-alpha@
|
|
do return end
|
|
--@end-non-alpha@
|
|
|
|
---@class DBMGUI
|
|
local DBM_GUI = DBM_GUI
|
|
|
|
DBM_GUI.Cat_Development = DBM_GUI:CreateNewPanel("Development & Testing", "option")
|
|
|
|
local infoArea = DBM_GUI.Cat_Development:CreateArea("Development and Testing UI")
|
|
infoArea:CreateText("You are seeing this UI tab because you have an alpha or development build of DBM installed.", nil, true)
|
|
|
|
local testPanel = DBM_GUI.Cat_Development:CreateNewPanel("Tests", "option")
|
|
|
|
---@class TimeWarpSlider: DBMPanelSlider
|
|
local timeWarpSlider = testPanel:CreateSlider("", 1, 500, 1, 400)
|
|
timeWarpSlider:SetPoint("TOPLEFT", testPanel.frame, "TOPLEFT", 20, -20)
|
|
timeWarpSlider:SetScript("OnValueChanged", function(self, value)
|
|
local sliderMax = select(2, self:GetMinMaxValues())
|
|
if value >= sliderMax then -- slider at max == dynamic fastest speed
|
|
DBM_Test_DefaultTimeWarp = 0
|
|
timeWarpSlider.textFrame:SetText("Time warp: dynamic (fastest)")
|
|
if DBM.Test.timeWarper then
|
|
DBM.Test.timeWarper:SetSpeed(0)
|
|
end
|
|
return
|
|
end
|
|
value = self:TransformInput(value)
|
|
DBM_Test_DefaultTimeWarp = value
|
|
timeWarpSlider.textFrame:SetFormattedText("Time warp: %dx", value)
|
|
if DBM.Test.timeWarper then
|
|
DBM.Test.timeWarper:SetSpeed(value)
|
|
end
|
|
end)
|
|
|
|
-- exponential slider that isn't too steep and feels good
|
|
timeWarpSlider.steepness = 3.3
|
|
function timeWarpSlider:TransformInput(value)
|
|
local sliderMin, sliderMax = self:GetMinMaxValues()
|
|
value = (value - sliderMin) / (sliderMax - sliderMin)
|
|
return (math.exp(value * self.steepness) - 1) / (math.exp(self.steepness) - 1) * (sliderMax - sliderMin) + sliderMin
|
|
end
|
|
|
|
function timeWarpSlider:TransformInputInverse(value)
|
|
local sliderMin, sliderMax = self:GetMinMaxValues()
|
|
value = (value - sliderMin) / (sliderMax - sliderMin)
|
|
return math.log((math.exp(self.steepness) - 1) * (value - 1 / (1 - math.exp(self.steepness)))) / self.steepness * (sliderMax - sliderMin) + sliderMin
|
|
end
|
|
|
|
|
|
local runButtons = {}
|
|
|
|
---@param tests TestDefinition[]
|
|
local function getTestDurationString(tests)
|
|
local duration = 0
|
|
for _, test in ipairs(tests) do
|
|
duration = duration + test.log[#test.log][1] + 3.1 -- Tests wait 3.1 second for post-combat handlers (full event deregistration)
|
|
end
|
|
duration = math.floor(duration)
|
|
local sec = duration % 60
|
|
local min = math.floor(duration / 60)
|
|
return ("%d:%02d"):format(min, sec)
|
|
end
|
|
|
|
---@param uiInfo TestUiInfo
|
|
---@param test TestDefinition
|
|
---@param result? TestResultEnum
|
|
local function setCombinedTestResults(uiInfo, test, result)
|
|
if uiInfo.numTests == 1 and result == "Failure" then
|
|
-- TODO: add flakiness detection here: did this succeed or fail in prior runs?
|
|
test.uiInfo.statusText:SetText("Failed")
|
|
test.uiInfo.statusText:SetTextColor(RED_FONT_COLOR:GetRGB())
|
|
return
|
|
end
|
|
if result then
|
|
uiInfo.childTestState[test] = result
|
|
end
|
|
local successCount = 0
|
|
local failCount = 0
|
|
for _, v in pairs(uiInfo.childTestState) do
|
|
if v == "Success" then
|
|
successCount = successCount + 1
|
|
elseif v == "Failure" then
|
|
failCount = failCount + 1
|
|
end
|
|
end
|
|
uiInfo.statusText:SetFormattedText("%d/%d", successCount, uiInfo.numTests)
|
|
if failCount > 0 then
|
|
uiInfo.statusText:SetTextColor(RED_FONT_COLOR:GetRGB())
|
|
elseif successCount == uiInfo.numTests then
|
|
uiInfo.statusText:SetTextColor(GREEN_FONT_COLOR:GetRGB())
|
|
else
|
|
uiInfo.statusText:SetTextColor(ORANGE_FONT_COLOR:GetRGB())
|
|
end
|
|
end
|
|
|
|
---@type TestDefinition[]
|
|
local queuedTests = {}
|
|
local runAllOrStopButton
|
|
|
|
local function stopAll()
|
|
DBM.Test:StopTests()
|
|
for _, test in ipairs(queuedTests) do
|
|
setCombinedTestResults(test.uiInfo, test)
|
|
end
|
|
table.wipe(queuedTests)
|
|
for _, button in ipairs(runButtons) do
|
|
button:Enable()
|
|
end
|
|
runAllOrStopButton:SetText("Run all tests")
|
|
end
|
|
|
|
runAllOrStopButton = testPanel:CreateButton("Run all tests", 100, 35, function()
|
|
if #queuedTests > 0 then
|
|
stopAll()
|
|
elseif runButtons[1] then
|
|
runButtons[1]:GetScript("OnClick")(runButtons[1])
|
|
else
|
|
error("no tests installed")
|
|
end
|
|
end)
|
|
runAllOrStopButton:SetPoint("TOPRIGHT", testPanel.frame, "TOPRIGHT", -10, -5)
|
|
|
|
---@param test TestDefinition
|
|
local function onTestStart(test)
|
|
test.uiInfo.statusText:SetText("Running")
|
|
test.uiInfo.statusText:SetTextColor(LIGHTBLUE_FONT_COLOR:GetRGB())
|
|
end
|
|
|
|
---@param results TestReporter
|
|
local function onTestFinish(test, results, testCount, numTests)
|
|
if queuedTests[#queuedTests] == test then
|
|
queuedTests[#queuedTests] = nil
|
|
end
|
|
local result = results:GetResult()
|
|
for _, parent in ipairs(test.uiInfo.parents) do
|
|
setCombinedTestResults(parent.uiInfo, test, result)
|
|
end
|
|
setCombinedTestResults(test.uiInfo, test, result)
|
|
test.uiInfo.lastResults = results
|
|
if results:HasDiff() then
|
|
test.uiInfo.showDiffButton:Show()
|
|
end
|
|
if results:HasErrors() then
|
|
test.uiInfo.showErrorsButton:Show()
|
|
end
|
|
if testCount == numTests then
|
|
for _, button in ipairs(runButtons) do
|
|
button:Enable()
|
|
end
|
|
runAllOrStopButton:SetText("Run all tests")
|
|
end
|
|
end
|
|
|
|
---@param event TestCallbackEvent
|
|
---@param test TestDefinition
|
|
local function testStatusCallback(event, test, ...)
|
|
if event == "TestStart" then
|
|
return onTestStart(test)
|
|
else
|
|
return onTestFinish(test, ...)
|
|
end
|
|
end
|
|
|
|
local function onRunTestClicked(tests)
|
|
return function()
|
|
for _, button in ipairs(runButtons) do
|
|
button:Disable()
|
|
end
|
|
runAllOrStopButton:SetText("Stop tests")
|
|
for i = #tests, 1, -1 do
|
|
local test = tests[i]
|
|
queuedTests[#queuedTests + 1] = test
|
|
test.uiInfo.showDiffButton:Hide()
|
|
test.uiInfo.showErrorsButton:Hide()
|
|
test.uiInfo.statusText:SetText("Queued")
|
|
test.uiInfo.statusText:SetTextColor(BLUE_FONT_COLOR:GetRGB())
|
|
end
|
|
onTestStart(tests[1])
|
|
DBM.Test:RunTests(tests, nil, testStatusCallback)
|
|
end
|
|
end
|
|
|
|
local testYIndex = 1
|
|
---@param tests TestDefinition[]
|
|
---@return TestUiInfo
|
|
local function createTestEntry(testName, tests, parents, indentation)
|
|
local yDistance = 22
|
|
local yPos = -yDistance * testYIndex - 35
|
|
testYIndex = testYIndex + 1
|
|
local xOffset = indentation * 10
|
|
---@class TestUiInfo
|
|
local uiInfo = {
|
|
parents = parents,
|
|
numTests = #tests,
|
|
---@type table<TestDefinition, TestResultEnum>
|
|
childTestState = {}
|
|
}
|
|
|
|
local statusText = testPanel:CreateText("", 55, false)
|
|
uiInfo.statusText = statusText
|
|
if #tests > 0 then
|
|
statusText:SetText("0/" .. #tests)
|
|
end
|
|
statusText:SetPoint("TOPLEFT", testPanel.frame, "TOPLEFT", 7, yPos)
|
|
statusText:SetMaxLines(1)
|
|
local nameText = testPanel:CreateText(testName, 0, false)
|
|
nameText:SetWidth(400) -- set width last like this instead of via parameter to avoid some odd placement for truncated text
|
|
nameText:SetMaxLines(1)
|
|
nameText:SetPoint("TOPLEFT", testPanel.frame, "TOPLEFT", 60 + xOffset, yPos)
|
|
local runButton = testPanel:CreateButton("Run", 40, 22, onRunTestClicked(tests))
|
|
runButton.myheight = yDistance
|
|
runButtons[#runButtons + 1] = runButton
|
|
runButton:SetPoint("TOPRIGHT", testPanel.frame, "TOPRIGHT", -10, yPos)
|
|
local durationText = testPanel:CreateText(getTestDurationString(tests), 50, false, nil, "RIGHT")
|
|
durationText:SetPoint("RIGHT", runButton, "LEFT", -5, 0)
|
|
if #tests == 1 then
|
|
---@class TestDefinition
|
|
local test = tests[1]
|
|
local showDiffButton = testPanel:CreateButton("Show diff", 0, 22, function(self)
|
|
if test.uiInfo.lastResults then
|
|
test.uiInfo.lastResults:ReportDiff(true)
|
|
end
|
|
end)
|
|
uiInfo.showDiffButton = showDiffButton
|
|
showDiffButton:SetPoint("RIGHT", runButton, "LEFT", -50, 0)
|
|
showDiffButton:Hide()
|
|
local showErrorsButton = testPanel:CreateButton("Show errors", 0, 22, function(self)
|
|
if test.uiInfo.lastResults then
|
|
test.uiInfo.lastResults:ReportErrors()
|
|
end
|
|
end)
|
|
uiInfo.showErrorsButton = showErrorsButton
|
|
showErrorsButton:SetPoint("RIGHT", showDiffButton, "LEFT", -5, 0)
|
|
showErrorsButton:Hide()
|
|
uiInfo.lastResults = nil
|
|
test.uiInfo = uiInfo
|
|
end
|
|
return uiInfo
|
|
end
|
|
|
|
---@param node TestTreeNode
|
|
local function gatherChildTests(node, result)
|
|
result = result or {}
|
|
for _, v in ipairs(node.children) do
|
|
if v.test then
|
|
result[#result + 1] = v.test
|
|
end
|
|
gatherChildTests(v, result)
|
|
end
|
|
return result
|
|
end
|
|
|
|
local function getParents(node)
|
|
local result = {}
|
|
while node.parent do
|
|
result[#result + 1] = node.parent
|
|
node = node.parent
|
|
end
|
|
return result
|
|
end
|
|
|
|
---@class node TestTreeNode
|
|
local function createTestTreeEntry(node)
|
|
-- Note: this not ideal if we have nodes that have a test and children (e.g., tests named a/b and a/b/c), it happens to work by coincidence
|
|
-- as the only code that relies on having uiInfo in the node is the code that iterates over parents and both entries have the same parents.
|
|
-- Also note that tests a/b and a/b/c shouldn't exist at the same time if you follow the naming pattern.
|
|
if node.test then
|
|
node.uiInfo = createTestEntry(node.test.name, {node.test}, getParents(node), node.depth)
|
|
end
|
|
if node.count > 1 then
|
|
local name = node.path == "" and "All tests" or (node.path .. "/*")
|
|
node.uiInfo = createTestEntry(name, gatherChildTests(node), getParents(node), node.depth)
|
|
end
|
|
for _, v in ipairs(node.children) do
|
|
createTestTreeEntry(v)
|
|
end
|
|
end
|
|
|
|
---@class TestTreeNode
|
|
local root = {
|
|
---@type TestTreeNode?
|
|
parent = nil,
|
|
---@type TestTreeNode[]
|
|
children = {},
|
|
---@type TestDefinition
|
|
test = nil, -- Node contains a test, if we follow the intended naming then this is mutually exclusive with #children > 0.
|
|
path = "",
|
|
depth = 0,
|
|
count = 0,
|
|
---@type TestUiInfo
|
|
uiInfo = nil,
|
|
---@type table<string, TestTreeNode>
|
|
entries = {} -- Only used during tree construction, do not use afterwards.
|
|
}
|
|
|
|
local function insertElement(node, depth, name, pathElements)
|
|
node.count = (node.count or 0) + 1
|
|
local path = pathElements[depth]
|
|
if not node.entries[path] then
|
|
local entry = {parent = node, entries = {}, path = table.concat(pathElements, "/", 1, depth), children = {}, depth = depth, count = 0}
|
|
node.entries[path] = entry
|
|
table.insert(node.children, entry)
|
|
end
|
|
if depth < #pathElements then
|
|
insertElement(node.entries[path], depth + 1, name, pathElements)
|
|
else
|
|
node.entries[path].test = DBM.Test.Registry.tests[name]
|
|
node.entries[path].count = (node.entries[path].count or 0) + 1
|
|
end
|
|
end
|
|
|
|
local function decrementDepth(node)
|
|
node.depth = node.depth - 1
|
|
for _, v in ipairs(node.children) do
|
|
decrementDepth(v)
|
|
end
|
|
end
|
|
|
|
local function pruneTree(node)
|
|
local i = 1
|
|
while i <= #node.children do
|
|
local v = node.children[i]
|
|
pruneTree(v)
|
|
if not v.test and #v.children == 1 then
|
|
v.children[1].parent = v.children[1].parent.parent
|
|
node.children[i] = v.children[1]
|
|
decrementDepth(v.children[1])
|
|
else
|
|
i = i + 1
|
|
end
|
|
end
|
|
end
|
|
|
|
local initialized
|
|
testPanel.frame:HookScript("OnShow", function(self)
|
|
if initialized then return end
|
|
initialized = true
|
|
DBM.Test:LoadAllTests()
|
|
if not DBM.Test:TestsLoaded() then
|
|
local area = testPanel:CreateArea("No tests available")
|
|
area:CreateText("Could not find any test definitions, check if DBM-Test-* mods are installed and enabled.", nil, true)
|
|
return
|
|
end
|
|
local savedTimeWarpSliderVal = DBM_Test_DefaultTimeWarp or 0
|
|
savedTimeWarpSliderVal = savedTimeWarpSliderVal > 0 and savedTimeWarpSliderVal or 500
|
|
timeWarpSlider:SetValue(timeWarpSlider:TransformInputInverse(savedTimeWarpSliderVal))
|
|
for _, testName in ipairs(DBM.Test.Registry.sortedTests) do
|
|
local pathElements = {string.split("/", testName)}
|
|
insertElement(root, 1, testName, pathElements)
|
|
end
|
|
pruneTree(root)
|
|
createTestTreeEntry(root)
|
|
end)
|
|
|