function CreateTestPrints(output, tag, timestamp) tag = tag or "" local err_tag = "GT_ERROR " .. tag GameTestsPrint = CreatePrint { tag, output = output } GameTestsPrintf = CreatePrint { tag, output = output, format = string.format } GameTestsError = CreatePrint { err_tag, output = output, timestamp = timestamp } GameTestsErrorf = CreatePrint { err_tag, output = output, timestamp = timestamp, format = string.format } end if FirstLoad then GameTestsRunning = false GameTestsPrint = false GameTestsPrintf = false GameTestsError = false GameTestsErrorf = false GameTestsErrorsFilename = "svnAssets/Logs/GameTestsErrors.log" GameTestsFlushErrors = empty_func -- if you want to see the prints from asserts inside the prints some sub-section of your test, call this when the sub-section ends CreateTestPrints() end function RunGameTests(time_start_up, game_tests_name, ...) time_start_up = os.time() - (time_start_up or os.time()) game_tests_name = game_tests_name or "GameTests" CreateRealTimeThread( function(...) AsyncFileDelete(GameTestsErrorsFilename) local game_tests_errors_file, error_msg = io.open(GameTestsErrorsFilename, "w+") if not game_tests_errors_file then print("Failed to open GameTestsErrors.log:", error_msg) end local function GameTestOutput(s) ConsolePrintNoLog(s) if game_tests_errors_file then game_tests_errors_file:write(s, "\n") end end CreateTestPrints(GameTestOutput) GameTestsPrintf("Lua rev: %d, Assets rev: %d", LuaRevision, AssetsRevision) LoadBinAssets("") -- saving presets that include ColorizationPropSet requires knowledge about the number of colorization channels for entities GameTestsRunning = true Msg("GameTestsBegin", true) SetAllDevDlcs(true) table.change(config, "GameTests", { Backtrace = false, SilentVMEStack = true, }) UpdateThreadDebugHook() local game_tests_table = _G[game_tests_name] local tests_to_run = {...} if #tests_to_run == 0 then tests_to_run = table.keys2(game_tests_table, "sorted") end local log_lines_processed = 0 local any_failed local lua_error_prefix = "[LUA ERROR] " GameTestsFlushErrors = function() FlushLogFile() local err, log_file = AsyncFileToString(GetLogFile(), false, false, "lines") if not err then for i = log_lines_processed+1, #log_file do local line = log_file[i] if line:starts_with(lua_error_prefix) then GameTestsErrorf("%s", string.sub(line, #lua_error_prefix+1)) any_failed = true elseif line:match("%)%: ASSERT.*failed") then GameTestsErrorf("once", "%s", line) any_failed = true elseif line:match(".*%.lua%(%d*%): ") then GameTestsErrorf("%s", line) any_failed = true elseif line:match("COMPILE!.*fx") then GameTestsPrint("once", line) end end log_lines_processed = #log_file else GameTestsPrint("Failed to load log file from game " .. GetLogFile() .. " : " .. err) end if game_tests_errors_file then game_tests_errors_file:flush() end end GameTestsFlushErrors() local all_tests_start_time = GetPreciseTicks() for _, test in ipairs(tests_to_run) do if game_tests_table[test] then CreateTestPrints(GameTestOutput, test, "gametime") GameTestsPrint("Start...") local time = GetPreciseTicks() Msg("GameTestBegin", test) local success = sprocall(game_tests_table[test], time_start_up, game_tests_name) if not success then any_failed = true end Msg("GameTestEnd", test) GameTestsFlushErrors() GameTestsPrint(string.format("...end. Duration %i ms. Since start %i sec.", GetPreciseTicks() - time , (GetPreciseTicks() - all_tests_start_time) / 1000)) else GameTestsError("GameTest not found:", test) end end if any_failed then FlushLogFile() local err, log_file = AsyncFileToString(GetLogFile(), false, false, "lines") if not err then CreateTestPrints(GameTestOutput, "GT_LOG") GameTestsPrint("Complete log file from run follows:") GameTestsPrint(string.rep("-", 80)) for _, line in ipairs(log_file) do GameTestsPrint(line) end end end if game_tests_errors_file then game_tests_errors_file:close() end GameTestsRunning = false Msg("GameTestsEnd", true) table.restore(config, "GameTests", true) UpdateThreadDebugHook() CreateTestPrints() quit() end, ...) end function DbgRunGameTests(game_tests_table, names) if not IsRealTimeThread() then return CreateRealTimeThread(DbgRunGameTests, game_tests_table, names) end GameTestsRunning = true Msg("GameTestsBegin") local old = LocalStorage.DisableDLC SetAllDevDlcs(true) game_tests_table = game_tests_table or GameTests names = names or table.keys(game_tests_table, true) local times = {} local st = GetPreciseTicks() for _, name in ipairs(names) do local func = game_tests_table[name] if not func then printf("No such test", name) else CreateTestPrints(print, name) Msg("GameTestBegin", name) print("Testing", name) CloseMenuDialogs() local time = GetPreciseTicks() sprocall(func) time = GetPreciseTicks() - time Msg("GameTestEnd", name) printf("Done testing %s in %d ms", name, time) times[name] = time end end if #names > 1 then printf("Done testing all in %d ms", GetPreciseTicks() - st) for _, name in ipairs(names) do printf("\t%s: %d ms", name, times[name]) end print() end LocalStorage.DisableDLC = old SaveLocalStorage() CreateTestPrints() GameTestsRunning = false Msg("GameTestsEnd") end function DbgRunGameTest(name, game_tests_table) return DbgRunGameTests(game_tests_table, {name}) end GameTests = {} GameTestsNightly = {} -- these are defined per project g_UIAutoTestButtonsMap = false g_UIGameChangeMap = ChangeMap g_UIGetContentTop = function() return GetInGameInterface() end g_UIGetBuildingsList = false g_UISpecialToggleButton = {match = false} -- match is function checking button properties to recognize special ones g_UIBlacklistButton = {match = false} -- match is function checking button properties to recognize black listed g_UIPrepareTest = false -- funtion to call on UI test start, e.g. cheat for research all local function IsSpecialToggleButton(button, id) if g_UISpecialToggleButton[id] then return true end local match = g_UISpecialToggleButton.match return match and match(button) end local function IsBlacklistedButton(button, id) local id = rawget(button, "Id") if id and g_UIBlacklistButton[id] then return true end local match = g_UIBlacklistButton.match return match and match(button) end local function GetContentSnapshot(content) content = content or g_UIGetContentTop() local snapshot, used = {}, {} for idx, window in ipairs(content) do if not used[window] then used[window] = true snapshot[idx] = GetContentSnapshot(window) end end return snapshot, used end local function DetectNewWindows(snapshot, used) local new_snapshot, new_used = GetContentSnapshot() local windows = setmetatable({}, weak_keys_meta) for window in pairs(new_used) do if not used[window] then table.insert(windows, window) end end return windows end local function GetButtons(windows, buttons) buttons = buttons or {} for _, control in ipairs(windows) do if control:IsKindOf("XButton") then if not IsBlacklistedButton(control) then table.insert(buttons, control) end else GetButtons(control, buttons) end end return buttons end local function FilterWindowsWithButtons(windows) local windows_with_buttons = {} for _, window in ipairs(windows) do local buttons = GetButtons(window) if #buttons > 0 then table.insert(windows_with_buttons, {window = window, buttons = buttons}) end end return windows_with_buttons end local function GetSelectObjContainer(obj) local snapshot, used = GetContentSnapshot() SelectObj(obj) WaitMsg("SelectionChange", 1000) local windows = DetectNewWindows(snapshot, used) local windows_with_buttons = FilterWindowsWithButtons(windows) assert(#windows_with_buttons <= 1) return #windows_with_buttons == 1 and windows_with_buttons[1] end local function GetButtonPressContainer(button) local snapshot, used = GetContentSnapshot() button:Press() local windows = DetectNewWindows(snapshot, used) local windows_with_buttons = FilterWindowsWithButtons(windows) assert(#windows_with_buttons <= 1) return #windows_with_buttons == 1 and windows_with_buttons[1] end local function GetButtonId(button, idx) return button.Id or string.format("idChild_%d", idx) end function FindButton(container, id) for _, control in ipairs(container) do if control:IsKindOf("XButton") then if GetButtonId(control) == id then return control end else local button = FindButton(control, id) if button then return button end end end end local function ExpandGraph(node, buttons) node.children = node.children or {} for idx, button in ipairs(buttons) do local id = GetButtonId(button, idx) table.insert(node.children, {processed = {}, children = {}, parent = node, id = id, expanded = false}) end node.expanded = true end local function MarkNodeProcessed(node) node.parent.processed[node.id] = true end local function GenNodePath(node, nodes) for idx, child in ipairs(node.children) do if not node.processed[child.id] then table.insert(nodes, child) if child.expanded then local old_len = #buttons GenButtonSequence(child, nodes) if #nodes > old_len then return end else return end table.remove(nodes) node.processed[child.id] = true end end end local function FindButtonSequence(root) local nodes = {} GenNodePath(root, nodes) if #nodes > 0 then local node = nodes[#nodes] local buttons = {} for i = 1, #nodes - 1 do buttons[i] = nodes[i].id end return buttons, node end end function GetSingleBuildingClassList(list) local buildings, class_taken = {}, {} for _, bld in ipairs(list) do if not class_taken[bld.class] then table.insert(buildings, bld) class_taken[bld.class] = true end end return buildings end function GameTests.BuildingButtons() if not g_UIAutoTestButtonsMap then return end local time_started = GetPreciseTicks() if GetMapName() ~= g_UIAutoTestButtonsMap then g_UIGameChangeMap(g_UIAutoTestButtonsMap) end local list, content while not (list and content) do list = g_UIGetBuildingsList() content = g_UIGetContentTop() Sleep(50) end if g_UIPrepareTest then g_UIPrepareTest() end --print(string.format("Testing UI buttons for %d buildings", #list)) local clicks = 0 SelectObj(false) for bld_idx, bld in ipairs(list) do local container = IsValid(bld) and GetSelectObjContainer(bld) if container then local root = {processed = {}, children = {}, expanded = false} ExpandGraph(root, container.buttons) local buttons, node = FindButtonSequence(root) while container and buttons do for _, button in ipairs(buttons) do -- TODO: keep changing container here button:Press() clicks = clicks + 1 end local button = FindButton(container.window, node.id) if button and button:GetVisible() and button:GetEnabled() and not IsBlacklistedButton(button) then --print(string.format("Pressing %s:%s", bld.class, node.id)) local new_container = GetButtonPressContainer(button) if new_container then ExpandGraph(node, new_container.buttons) end if IsSpecialToggleButton(button, node.id) then --print(string.format("Toggling off %s:%s", bld.class, node.id)) button:Press() -- toggle it clicks = clicks + 1 end end MarkNodeProcessed(node) SelectObj(false) container = GetSelectObjContainer(bld) -- TODO: detect graph cycles buttons, node = FindButtonSequence(root) end end SelectObj(false) end GameTestsPrintf("Testing %d building for %d UI buttons clicks finished: %ds.", #list, clicks, (GetPreciseTicks() - time_started) / 1000) end function GameTestAddReferenceValue(type, name, value, comment, tolerance_mul, tolerance_div) if not type then return end local results_file = "AppData/Benchmarks/GameTestReferenceValues.lua" local _, str_result = AsyncFileToString(results_file) local _, referenceValues = LuaCodeToTuple(str_result) referenceValues = referenceValues or {} local avg_previous, avg_items = 0, 0 local maxResults = 5 -- how many results should be stored per test referenceValues[type] = referenceValues[type] or {} local benchmark_results = table.copy(referenceValues[type]) benchmark_results[name] = benchmark_results[name] or {} for oldInd, oldCamera in pairs(benchmark_results[name]) do if oldCamera.comment == comment then avg_previous = avg_previous + oldCamera.value avg_items = avg_items + 1 else table.remove(benchmark_results[name], oldInd) GameTestsPrintf("Old %s not matching, deleting results for %s data!", name, type) end end table.insert(benchmark_results[name], {comment = comment, value = value}) referenceValues[type] = benchmark_results while true do if #benchmark_results[name] > maxResults then table.remove(benchmark_results[name], 1) else break end end if avg_items == 0 then GameTestsPrintf("No previous results to compare to for %s: %s. New results saved.",type, name) else avg_previous = avg_previous/avg_items end if avg_previous ~= 0 then if abs( 100.0 - ( ( (value*1.0)* 100.0) / (avg_previous*1.0) ) ) <= (tolerance_mul*1.0)/(tolerance_div*1.0) * 100.0 then GameTestsPrintf("Reference value %s: %s is %s, avg of previous is %s", type, name, value, avg_previous) else GameTestsErrorf("Reference value %s: %s is %s, avg of previous is %s", type, name, value, avg_previous) GameTestsPrintf("Camera properties: "..tostring(comment)) end end AsyncCreatePath("AppData/Benchmarks") local err = AsyncStringToFile(results_file, ValueToLuaCode(referenceValues)) if err then GameTestsError("Failed to create file with reference values", results_file, err) end end function GameTestsNightly.ReferenceImages() -- change map and video mode for consistency in tests if not config.RenderingTestsMap then GameTestsPrint("config.RenderingTestsMap map not specified, skipping the test.") return end if not MapData[config.RenderingTestsMap] then GameTestsError(config.RenderingTestsMap, "map not found, could not complete test.") return end ChangeMap(config.RenderingTestsMap) SetMouseDeltaMode(true) ChangeVideoMode(512, 512, 0, false, false) SetLightmodel(0, LightmodelPresets.ArtPreview, 0) WaitNextFrame(10) local allowedDifference = 80 -- the lower the value, the more different the images are allowed to be -- max is (inf), if images are identical, (0) means images have absolutely nothing in common -- usually, when two images are quite simmilar, results vary from 80 to 100+ local cameras = Presets.Camera["reference"] if not cameras or #cameras == 0 then GameTestsPrint("No recorded 'reference' Cameras, could not complete test.") return end local ostime = os.time() local results = {} for i, cam in ipairs(cameras) do local logs_gt_src = "svnAssets/Logs/"..cam.id..".png" local logs_ref_src = "svnAssets/Logs/"..cam.id.."_"..ostime.."_reference.png" local logs_diff_src = "svnAssets/Logs/"..cam.id.."_"..ostime.."_diffResult.png" cam:ApplyProperties() cam:beginFunc() camera.Lock() Sleep(3500) AsyncCreatePath("svnAssets/Logs") local ref_img_path = "svnAssets/Tests/ReferenceImages/" local name = ref_img_path .. cam.id .. ".png" local err = AsyncCopyFile(name, logs_gt_src, "raw") if err then err = AsyncExec(string.format("svn update %s --set-depth infinity", ConvertToOSPath(ref_img_path)), true, true) if err then GameTestsErrorf("Reference images folder '%s' could not be updated. Reason: %s!", ConvertToOSPath(ref_img_path), err) return end err = AsyncExec(string.format("svn update %s --depth infinity", ConvertToOSPath(ref_img_path)), true, true) if err then GameTestsErrorf("Reference images folder '%s' could not be updated. Reason: %s!", ConvertToOSPath(ref_img_path), err) return end err = AsyncCopyFile(name, logs_gt_src, "raw") if err then GameTestsErrorf("Reference images could not be copied from Tests folder for '%s' --> '%s'. Reason: %s. Try increasing SVN update depth manually!", ConvertToOSPath(name), ConvertToOSPath(logs_gt_src), err) return end end AsyncFileDelete(logs_ref_src) WriteScreenshot(logs_ref_src, 512, 512) Sleep(300) local err, img_err = CompareImages( logs_gt_src, logs_ref_src, logs_diff_src, 4) if img_err then if img_err < allowedDifference then GameTestsErrorf("Image taken from "..cam.id.." is too different from reference image!") end end cam:endFunc() WaitNextFrame(1) table.insert(results, {id = cam.id, img_err = img_err}) end local newHTMLTable = {"", "
", "Camera ID | ", "Image error metric | ", "Ground Truth | ", "Difference | ", "New Image |
---|---|---|---|---|
",img.id," | ", img.img_err, " | ", " |