From fa56e3d8fb3b3beceb6087dc4af26af0342f449a Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 23 Dec 2023 20:51:20 -0800 Subject: [PATCH 01/98] feat(test_framework): add an in-game lua testing framework This is currently just a demonstration, and is not ready for production use yet. Tests can be executed with `/runtests ... `, where any test file (excluding file extension) that matches any pattern is included. `/runtests` by itself will run all tests. Features: * setup/cleanup functions can be defined in each test file. cleanup will always run, even if the test code fails. * nicely formatted test results, including errors with line information * two ways to call synced code: * `x = SyncedProxy..()`, which is equivalent to `x = Y.Z()` executed in a synced context. * `x = SyncedRun(function() ... end)`, which allows execution of arbitrary code in a synced context. This is faster for things involving loops or many synced calls. * wait API to allow in-game actions to happen during a test. For example, `Test.waitFrames(5)` will wait 5 frames before resuming test execution. * `spy(parent, target)`, which allows tracking all calls to a spied function. * isolated environments for each test, while still having access to most globals accessible to widgets. Included are several sample widget tests, of a few different kinds: * balance tests, that set up fights between units * an example demonstrating different the wait API * a test for a user widget (gui_battle_resource_tracker) * several tests for a built-in widget (gui_selfd_icons) * some of these have sections testing bugs that are not fixed yet, and are thus commented out. The serpent library is included for straightforward serialization. --- common/luaUtilities/serpent.lua | 152 ++++ luarules/gadgets/dbg_test_framework_proxy.lua | 112 +++ luarules/testing/util.lua | 360 +++++++++ .../balance/test_arm_vs_cor_fighters.lua | 91 +++ .../Tests/balance/test_grunts_vs_pawns.lua | 79 ++ luaui/Widgets/Tests/example/test_wait.lua | 32 + .../test_gui_battle_resource_tracker.lua | 52 ++ .../test_gui_selfd_icons_armasp.lua | 46 ++ .../test_gui_selfd_icons_armpw.lua | 52 ++ .../test_gui_selfd_icons_armvp.lua | 46 ++ luaui/Widgets/dbg_test_framework.lua | 750 ++++++++++++++++++ luaui/Widgets/gui_selfd_icons.lua | 4 +- 12 files changed, 1774 insertions(+), 2 deletions(-) create mode 100644 common/luaUtilities/serpent.lua create mode 100644 luarules/gadgets/dbg_test_framework_proxy.lua create mode 100644 luarules/testing/util.lua create mode 100644 luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua create mode 100644 luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua create mode 100644 luaui/Widgets/Tests/example/test_wait.lua create mode 100644 luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua create mode 100644 luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armasp.lua create mode 100644 luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armpw.lua create mode 100644 luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua create mode 100644 luaui/Widgets/dbg_test_framework.lua diff --git a/common/luaUtilities/serpent.lua b/common/luaUtilities/serpent.lua new file mode 100644 index 00000000000..d3461829ddf --- /dev/null +++ b/common/luaUtilities/serpent.lua @@ -0,0 +1,152 @@ +local n, v = "serpent", "0.303" -- (C) 2012-18 Paul Kulchenko; MIT License +local c, d = "Paul Kulchenko", "Lua serializer and pretty printer" +local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'} +local badtype = {thread = true, userdata = true, cdata = true} +local getmetatable = debug and debug.getmetatable or getmetatable +local pairs = function(t) return next, t end -- avoid using __pairs in Lua 5.2+ +local keyword, globals, G = {}, {}, (_G or _ENV or widget or gadget) +for _,k in ipairs({'and', 'break', 'do', 'else', 'elseif', 'end', 'false', + 'for', 'function', 'goto', 'if', 'in', 'local', 'nil', 'not', 'or', 'repeat', + 'return', 'then', 'true', 'until', 'while'}) do keyword[k] = true end +for k,v in pairs(G) do globals[v] = k end -- build func to name mapping +for _,g in ipairs({'coroutine', 'debug', 'io', 'math', 'string', 'table', 'os'}) do + for k,v in pairs(type(G[g]) == 'table' and G[g] or {}) do globals[v] = g..'.'..k end end + +local function s(t, opts) + local name, indent, fatal, maxnum = opts.name, opts.indent, opts.fatal, opts.maxnum + local sparse, custom, huge = opts.sparse, opts.custom, not opts.nohuge + local space, maxl = (opts.compact and '' or ' '), (opts.maxlevel or math.huge) + local maxlen, metatostring = tonumber(opts.maxlength), opts.metatostring + local iname, comm = '_'..(name or ''), opts.comment and (tonumber(opts.comment) or math.huge) + local numformat = opts.numformat or "%.17g" + local seen, sref, syms, symn = {}, {'local '..iname..'={}'}, {}, 0 + local function gensym(val) return '_'..(tostring(tostring(val)):gsub("[^%w]",""):gsub("(%d%w+)", + -- tostring(val) is needed because __tostring may return a non-string value + function(s) if not syms[s] then symn = symn+1; syms[s] = symn end return tostring(syms[s]) end)) end + local function safestr(s) return type(s) == "number" and (huge and snum[tostring(s)] or numformat:format(s)) + or type(s) ~= "string" and tostring(s) -- escape NEWLINE/010 and EOF/026 + or ("%q"):format(s):gsub("\010","n"):gsub("\026","\\026") end + -- handle radix changes in some locales + if opts.fixradix and (".1f"):format(1.2) ~= "1.2" then + local origsafestr = safestr + safestr = function(s) return type(s) == "number" + and (nohuge and snum[tostring(s)] or numformat:format(s):gsub(",",".")) or origsafestr(s) + end + end + local function comment(s,l) return comm and (l or 0) < comm and ' --[['..select(2, pcall(tostring, s))..']]' or '' end + local function globerr(s,l) return globals[s] and globals[s]..comment(s,l) or not fatal + and safestr(select(2, pcall(tostring, s))) or error("Can't serialize "..tostring(s)) end + local function safename(path, name) -- generates foo.bar, foo[3], or foo['b a r'] + local n = name == nil and '' or name + local plain = type(n) == "string" and n:match("^[%l%u_][%w_]*$") and not keyword[n] + local safe = plain and n or '['..safestr(n)..']' + return (path or '')..(plain and path and '.' or '')..safe, safe end + local alphanumsort = type(opts.sortkeys) == 'function' and opts.sortkeys or function(k, o, n) -- k=keys, o=originaltable, n=padding + local maxn, to = tonumber(n) or 12, {number = 'a', string = 'b'} + local function padnum(d) return ("%0"..tostring(maxn).."d"):format(tonumber(d)) end + table.sort(k, function(a,b) + -- sort numeric keys first: k[key] is not nil for numerical keys + return (k[a] ~= nil and 0 or to[type(a)] or 'z')..(tostring(a):gsub("%d+",padnum)) + < (k[b] ~= nil and 0 or to[type(b)] or 'z')..(tostring(b):gsub("%d+",padnum)) end) end + local function val2str(t, name, indent, insref, path, plainindex, level) + local ttype, level, mt = type(t), (level or 0), getmetatable(t) + local spath, sname = safename(path, name) + local tag = plainindex and + ((type(name) == "number") and '' or name..space..'='..space) or + (name ~= nil and sname..space..'='..space or '') + if seen[t] then -- already seen this element + sref[#sref+1] = spath..space..'='..space..seen[t] + return tag..'nil'..comment('ref', level) + end + -- protect from those cases where __tostring may fail + if type(mt) == 'table' and metatostring ~= false then + local to, tr = pcall(function() return mt.__tostring(t) end) + local so, sr = pcall(function() return mt.__serialize(t) end) + if (to or so) then -- knows how to serialize itself + seen[t] = insref or spath + t = so and sr or tr + ttype = type(t) + end -- new value falls through to be serialized + end + if ttype == "table" then + if level >= maxl then return tag..'{}'..comment('maxlvl', level) end + seen[t] = insref or spath + if next(t) == nil then return tag..'{}'..comment(t, level) end -- table empty + if maxlen and maxlen < 0 then return tag..'{}'..comment('maxlen', level) end + local maxn, o, out = math.min(#t, maxnum or #t), {}, {} + for key = 1, maxn do o[key] = key end + if not maxnum or #o < maxnum then + local n = #o -- n = n + 1; o[n] is much faster than o[#o+1] on large tables + for key in pairs(t) do + if o[key] ~= key then n = n + 1; o[n] = key end + end + end + if maxnum and #o > maxnum then o[maxnum+1] = nil end + if opts.sortkeys and #o > maxn then alphanumsort(o, t, opts.sortkeys) end + local sparse = sparse and #o > maxn -- disable sparsness if only numeric keys (shorter output) + for n, key in ipairs(o) do + local value, ktype, plainindex = t[key], type(key), n <= maxn and not sparse + if opts.valignore and opts.valignore[value] -- skip ignored values; do nothing + or opts.keyallow and not opts.keyallow[key] + or opts.keyignore and opts.keyignore[key] + or opts.valtypeignore and opts.valtypeignore[type(value)] -- skipping ignored value types + or sparse and value == nil then -- skipping nils; do nothing + elseif ktype == 'table' or ktype == 'function' or badtype[ktype] then + if not seen[key] and not globals[key] then + sref[#sref+1] = 'placeholder' + local sname = safename(iname, gensym(key)) -- iname is table for local variables + sref[#sref] = val2str(key,sname,indent,sname,iname,true) + end + sref[#sref+1] = 'placeholder' + local path = seen[t]..'['..tostring(seen[key] or globals[key] or gensym(key))..']' + sref[#sref] = path..space..'='..space..tostring(seen[value] or val2str(value,nil,indent,path)) + else + out[#out+1] = val2str(value,key,indent,nil,seen[t],plainindex,level+1) + if maxlen then + maxlen = maxlen - #out[#out] + if maxlen < 0 then break end + end + end + end + local prefix = string.rep(indent or '', level) + local head = indent and '{\n'..prefix..indent or '{' + local body = table.concat(out, ','..(indent and '\n'..prefix..indent or space)) + local tail = indent and "\n"..prefix..'}' or '}' + return (custom and custom(tag,head,body,tail,level) or tag..head..body..tail)..comment(t, level) + elseif badtype[ttype] then + seen[t] = insref or spath + return tag..globerr(t, level) + elseif ttype == 'function' then + seen[t] = insref or spath + if opts.nocode then return tag.."function() --[[..skipped..]] end"..comment(t, level) end + local ok, res = pcall(string.dump, t) + local func = ok and "((loadstring or load)("..safestr(res)..",'@serialized'))"..comment(t, level) + return tag..(func or globerr(t, level)) + else return tag..safestr(t) end -- handle all other types + end + local sepr = indent and "\n" or ";"..space + local body = val2str(t, name, indent) -- this call also populates sref + local tail = #sref>1 and table.concat(sref, sepr)..sepr or '' + local warn = opts.comment and #sref>1 and space.."--[[incomplete output with shared/self-references skipped]]" or '' + return not name and body..warn or "do local "..body..sepr..tail.."return "..name..sepr.."end" +end + +local function deserialize(data, opts) + local env = (opts and opts.safe == false) and G + or setmetatable({}, { + __index = function(t,k) return t end, + __call = function(t,...) error("cannot call functions") end + }) + local f, res = (loadstring or load)('return '..data, nil, nil, env) + if not f then f, res = (loadstring or load)(data, nil, nil, env) end + if not f then return f, res end + if setfenv then setfenv(f, env) end + return pcall(f) +end + +local function merge(a, b) if b then for k,v in pairs(b) do a[k] = v end end; return a; end +return { _NAME = n, _COPYRIGHT = c, _DESCRIPTION = d, _VERSION = v, serialize = s, + load = deserialize, + dump = function(a, opts) return s(a, merge({name = '_', compact = true, sparse = true}, opts)) end, + line = function(a, opts) return s(a, merge({sortkeys = true, comment = true}, opts)) end, + block = function(a, opts) return s(a, merge({indent = ' ', sortkeys = true, comment = true}, opts)) end } diff --git a/luarules/gadgets/dbg_test_framework_proxy.lua b/luarules/gadgets/dbg_test_framework_proxy.lua new file mode 100644 index 00000000000..61f43417b41 --- /dev/null +++ b/luarules/gadgets/dbg_test_framework_proxy.lua @@ -0,0 +1,112 @@ +function gadget:GetInfo() + return { + name = "Test Framework Proxy", + desc = "Proxy for Synced commands", + author = "citrine", + date = "2023", + license = "GNU GPL, v2 or later", + version = 0, + layer = 9999, + enabled = true, + } +end + +if not gadgetHandler:IsSyncedCode() then + return +end + +if not Spring.Utilities.IsDevMode() then + return +end + +local LOG_LEVEL = LOG.INFO + +VFS.Include('luarules/testing/util.lua') + +local function log(level, str, ...) + if level < LOG_LEVEL then + return + end + + Spring.Log( + gadget:GetInfo().name, + LOG.NOTICE, + str + ) +end + +local Extra = { + clearMap = function() + for _, unitID in ipairs(Spring.GetAllUnits()) do + Spring.DestroyUnit(unitID, false, true, nil, true) + end + for _, featureID in ipairs(Spring.GetAllFeatures()) do + Spring.DestroyFeature(featureID) + end + end, +} + +local function processCall(msg, env) + log(LOG.DEBUG, "[processCall] " .. msg) + local data, returnId = deserializeFunctionCall(msg) + + local fn = evaluateStringPath(data.path, env) + + if not fn then + error("[processCall] could not find function: " .. data.path) + end + + local pcallOk, pcallResult = splitFirstElement(pack(pcall(fn, unpack(data.args)))) + + log(LOG.DEBUG, "[processCall.pcall] " .. table.toString({ + pcall = { pcallOk, pcallResult }, + returnId = returnId, + })) + + local serializedReturn = serializeFunctionReturn(pcallOk, pcallResult, returnId) + + log(LOG.DEBUG, "[processCall.SendLuaUIMsg] " .. PROXY_RETURN_PREFIX .. serializedReturn) + Spring.SendLuaUIMsg(PROXY_RETURN_PREFIX .. serializedReturn) + +end + +local function processCode(msg) + log(LOG.DEBUG, "[processCode] " .. table.toString({ + msg = msg, + })) + + local fn, returnId = deserializeFunctionRun(msg) + + local pcallOk, pcallResult = fn() + + log(LOG.DEBUG, "[processCode.pcall] " .. table.toString({ + pcall = { pcallOk, pcallResult }, + returnId = returnId, + })) + + local serializedReturn = serializeFunctionReturn(pcallOk, pcallResult, returnId) + + log(LOG.DEBUG, "[processCode.SendLuaUIMsg] " .. PROXY_RETURN_PREFIX .. serializedReturn) + Spring.SendLuaUIMsg(PROXY_RETURN_PREFIX .. serializedReturn) +end + +local RECEIVE_MODES = { + [PROXY_PREFIX] = function(msg) + processCall(msg, _G) + end, + [PROXY_EXTRA_PREFIX] = function(msg) + processCall(msg, Extra) + end, + [PROXY_RUN_PREFIX] = function(msg) + processCode(msg) + end, +} + +function gadget:RecvLuaMsg(msg, playerID) + for prefix, fn in pairs(RECEIVE_MODES) do + if msg:sub(1, #prefix) == prefix then + fn(msg:sub(#prefix + 1)) + return + end + end +end diff --git a/luarules/testing/util.lua b/luarules/testing/util.lua new file mode 100644 index 00000000000..b07ece57bcd --- /dev/null +++ b/luarules/testing/util.lua @@ -0,0 +1,360 @@ +local serpent = serpent or VFS.Include('common/luaUtilities/serpent.lua') + +TEST_RESULT = { + PASS = 1, + FAIL = 2, + SKIPPED = 3, + ERROR = 4, +} + +TEST_RESULT_ID_TO_STRING = {} +for k, v in pairs(TEST_RESULT) do + TEST_RESULT_ID_TO_STRING[v] = k +end + +function pack(...) + return { ... } +end + +function splitFirstElement(tbl) + if type(tbl) ~= "table" then + error("Input must be a table") + end + + if #tbl < 1 then + return nil, {} + end + + local firstElement = table.remove(tbl, 1) + return firstElement, tbl +end + +function clamp(min, max, num) + if (num < min) then + return min + elseif (num > max) then + return max + end + return num +end + +function rgbToColorCode(r, g, b) + local rs = clamp(1, 255, math.round(255 * r)) + local gs = clamp(1, 255, math.round(255 * g)) + local bs = clamp(1, 255, math.round(255 * b)) + return "\255" .. string.char(rs) .. string.char(gs) .. string.char(bs) +end + +function rgbReset() + return rgbToColorCode(0.85, 0.85, 0.85) +end + +function formatTestResult(testResult) + local resultColor + if testResult.result == TEST_RESULT.PASS then + resultColor = rgbToColorCode(0, 1, 0) + elseif testResult.result == TEST_RESULT.FAIL then + resultColor = rgbToColorCode(1, 0, 0) + elseif testResult.result == TEST_RESULT.SKIP then + resultColor = rgbToColorCode(0.8, 0.8, 0) + elseif testResult.result == TEST_RESULT.ERROR then + resultColor = rgbToColorCode(0.8, 0, 0.8) + end + local resultStr = resultColor .. TEST_RESULT_ID_TO_STRING[testResult.result] .. rgbReset() + local s = string.format("%s: %s", resultStr, testResult.label) + if testResult.frames ~= nil then + s = s .. " [" .. testResult.frames .. " frames]" + end + if testResult.error ~= nil then + s = s .. " | " .. testResult.error + end + return s +end + +function evaluateStringPath(str, initialEnv) + local parts = {} + for part in str:gmatch("[^.]+") do + table.insert(parts, part) + end + + local obj = initialEnv or _G + + for _, part in ipairs(parts) do + obj = obj[part] + if obj == nil then + return nil + end + end + + return obj +end + +PROXY_SEPARATOR = "||||" + +PROXY_PREFIX = "testframeworkproxy" .. PROXY_SEPARATOR +PROXY_EXTRA_PREFIX = "testframeworkproxyextra" .. PROXY_SEPARATOR +PROXY_RUN_PREFIX = "testframeworkproxyrun" .. PROXY_SEPARATOR + +PROXY_RETURN_PREFIX = "testframeworkproxyreturn" .. PROXY_SEPARATOR + +function registerCallins(target, callback, callins) + local callinNames = { + 'UnitCreated', + 'UnitFinished', + 'UnitFromFactory', + 'UnitReverseBuilt', + 'UnitDestroyed', + 'RenderUnitDestroyed', + 'UnitTaken', + 'UnitGiven', + 'UnitIdle', + 'UnitCommand', + 'UnitCmdDone', + 'UnitDamaged', + --'UnitStunned', + 'UnitEnteredRadar', + 'UnitEnteredLos', + 'UnitLeftRadar', + 'UnitLeftLos', + 'UnitEnteredUnderwater', + 'UnitEnteredWater', + 'UnitEnteredAir', + 'UnitLeftUnderwater', + 'UnitLeftWater', + 'UnitLeftAir', + 'UnitSeismicPing', + 'UnitLoaded', + 'UnitUnloaded', + 'UnitCloaked', + 'UnitDecloaked', + 'UnitMoveFailed', + 'UnitHarvestStorageFull', + } + + for _, callinName in ipairs(callinNames) do + if callins == nil or table.contains(callins, callinName) then + target[callinName] = function(...) + local args = { ... } + table.remove(args, 1) + callback(callinName, args) + end + end + end +end + +local function getLocals(i) + local result = {} + local j = 1 + while true do + local n, v = debug.getlocal(i, j) + if n == nil then + break + end + result[n] = v + j = j + 1 + end + return result +end + +local function getUpvalues(fn) + local result = {} + local j = 1 + while true do + local n, v = debug.getupvalue(fn, j) + if n == nil then + break + end + result[n] = v + j = j + 1 + end + return result +end + +local currentReturnId = 0 + +function serializeFunctionCall(path, args) + currentReturnId = currentReturnId + 1 + + local data = { + path = path, + args = args, + returnId = currentReturnId, + } + + return serpent.dump(data), currentReturnId +end + +function deserializeFunctionCall(serializedCall) + local dataOk, data = serpent.load(serializedCall, { safe = false }) + + if not dataOk then + -- error parsing data + error(data) + end + + return data, data.returnId +end + +function serializeFunctionRun(fn, stackDistance) + local fnLocals = getLocals(stackDistance + 1) + --local fnUpvalues = getUpvalues(fn) + + currentReturnId = currentReturnId + 1 + + local data = { + fn = fn, + locals = fnLocals, + --upvalues = fnUpvalues, + returnId = currentReturnId, + } + + return serpent.dump(data), currentReturnId +end + +function deserializeFunctionRun(serializedFn) + local dataOk, data = serpent.load(serializedFn, { safe = false }) + + if not dataOk then + -- error parsing data + error(data) + end + + local callableFunction = function() + local pcallOk, pcallResult = splitFirstElement(pack(pcall(data.fn, data.locals))) + return pcallOk, pcallResult + end + + return callableFunction, data.returnId +end + +function serializeFunctionReturn(pcallOk, pcallResult, returnId) + local data = nil + if pcallOk then + data = { + success = true, + result = pcallResult, + returnId = returnId, + } + else + data = { + success = false, + error = pcallResult, + returnId = returnId, + } + end + + return serpent.dump(data) +end + +function deserializeFunctionReturn(serializedReturn) + local dataOk, data = serpent.load(serializedReturn) + + if not dataOk then + -- error parsing data + error(data) + end + + if data.success then + return data.success, data.result, data.returnId + else + return data.success, data.error, data.returnId + end +end + +function spy(parent, target) + local original = parent[target] + local calls = {} + local wrapper = function(...) + local args = { ... } + calls[#calls + 1] = table.copy(args) + return original(unpack(args)) + end + parent[target] = wrapper + return { + calls = calls, + remove = function() + parent[target] = original + end + } +end + +function matchesPatterns(str, patterns) + for _, p in ipairs(patterns) do + if string.match(str, p) then + return true + end + end + return false +end + +function splitPhrases(input) + local result = {} + local currentPhrase = "" + + local function appendPhrase(phrase) + table.insert(result, phrase:match("^%s*(.-)%s*$")) -- Trim whitespace + currentPhrase = "" + end + + local i = 1 + local len = string.len(input) + + while i <= len do + local char = string.sub(input, i, i) + + if char == " " and currentPhrase ~= "" then + appendPhrase(currentPhrase) + elseif char == "\"" then + local quoteStart = i + repeat + i = i + 1 + char = string.sub(input, i, i) + if char == "\\" then + i = i + 1 -- Skip escaped character + end + until char == "\"" or i > len + + local quoteEnd = i + appendPhrase(string.sub(input, quoteStart + 1, quoteEnd - 1)) + else + currentPhrase = currentPhrase .. char + end + + i = i + 1 + end + + if currentPhrase ~= "" then + appendPhrase(currentPhrase) + end + + return result +end + +function removeFileExtension(filename) + local lastDotIndex = filename:match(".+()%.%w+$") + if lastDotIndex then + return filename:sub(1, lastDotIndex - 1) + else + return filename + end +end + +function yieldable_pcall(func, ...) + -- this works just like pcall, but while pcall fails on yield, this handles yield transparently + local function helper(co, ok, ...) + if ok then + if coroutine.status(co) == "dead" then + return true, (...) + end + return helper(co, coroutine.resume(co, coroutine.yield(...))) + else + return false, (...) + end + end + + local co = coroutine.create(function(...) + return func(...) + end) + + return helper(co, coroutine.resume(co, ...)) +end diff --git a/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua b/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua new file mode 100644 index 00000000000..a1bf05a3e70 --- /dev/null +++ b/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua @@ -0,0 +1,91 @@ +function setup() + Test.clearMap() +end + +function cleanup() + Test.clearMap() +end + +function test() + local units = { + [0] = "armfig", + [1] = "corveng" + } + local n = 200 + + local midX, midZ = Game.mapSizeX / 2, Game.mapSizeZ / 2 + local xOffset = 1000 + local zStep = 10 + local startZ = midZ - zStep * n / 2 + + -- make two lines of units facing each other + SyncedRun(function(locals) + do + local x = locals.midX - locals.xOffset + for i = 1, locals.n do + local z = locals.startZ + locals.zStep * i + local y = Spring.GetGroundHeight(x, z) + local unitID = Spring.CreateUnit(locals.units[0], x, y, z, "east", 0) + end + end + + do + local x = locals.midX + locals.xOffset + for i = 1, locals.n do + local z = locals.startZ + locals.zStep * i + local y = Spring.GetGroundHeight(x, z) + local unitID = Spring.CreateUnit(locals.units[1], x, y, z, "west", 1) + end + + end + end) + + Test.waitFrames(1) + + if false then + Spring.GiveOrderToUnitArray(Spring.GetTeamUnits(0), CMD.FIGHT, { midX, 0, midZ }, 0) + Spring.GiveOrderToUnitArray(Spring.GetTeamUnits(1), CMD.FIGHT, { midX, 0, midZ }, 0) + else + for _, unitID in ipairs(Spring.GetAllUnits()) do + local ux, uy, uz = Spring.GetUnitPosition(unitID) + + Spring.GiveOrderToUnit(unitID, CMD.FIGHT, { 2 * midX - ux, 0, uz }, 0) + Spring.GiveOrderToUnit(unitID, CMD.FIGHT, { midX, 0, midZ }, { "shift" }) + end + end + + Spring.SendCommands("setspeed " .. 5) + + -- wait until one team has no units left + Test.waitUntil(function() + return #(Spring.GetTeamUnits(0)) == 0 or #(Spring.GetTeamUnits(1)) == 0 + end, 60 * 30) + + Spring.SendCommands("setspeed " .. 1) + + if #(Spring.GetTeamUnits(0)) > #(Spring.GetTeamUnits(1)) then + winner = 0 + elseif #(Spring.GetTeamUnits(1)) > #(Spring.GetTeamUnits(0)) then + winner = 1 + end + + resultStr = "RESULT: " + if winner ~= nil then + unitName = units[winner] + if UnitDefNames and units[winner] and UnitDefNames[units[winner]] then + unitName = UnitDefNames[units[winner]].translatedHumanName or units[winner] + end + unitsLeft = #(Spring.GetAllUnits()) + resultStr = resultStr .. "team " .. winner .. " wins" + resultStr = resultStr .. " with " .. unitsLeft + resultStr = resultStr .. " (" .. string.format("%.f%%", 100 * unitsLeft / n) .. ")" + resultStr = resultStr .. " " .. unitName .. " left" + else + resultStr = resultStr .. "tie" + end + + Spring.Echo(resultStr) + + -- cor fighters should win + assert(winner == 1) +end diff --git a/luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua b/luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua new file mode 100644 index 00000000000..835d26c4ffe --- /dev/null +++ b/luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua @@ -0,0 +1,79 @@ +function setup() + Test.clearMap() +end + +function cleanup() + Test.clearMap() +end + +function test() + local units = { + [0] = "armpw", + [1] = "corak" + } + local n = 20 + + local midX, midZ = Game.mapSizeX / 2, Game.mapSizeZ / 2 + local xOffset = 200 + local zStep = 30 + local startZ = midZ - zStep * n / 2 + + -- make two lines of units facing each other + SyncedRun(function(locals) + do + local x = locals.midX - locals.xOffset + for i = 1, locals.n do + local z = locals.startZ + local y = Spring.GetGroundHeight(x, z) + Spring.CreateUnit(locals.units[0], x, y, z + locals.zStep * i, "east", 0) + end + end + + do + local x = locals.midX + locals.xOffset + for i = 1, locals.n do + local z = locals.startZ + local y = Spring.GetGroundHeight(x, z) + Spring.CreateUnit(locals.units[1], x, y, z + locals.zStep * i, "west", 1) + end + + end + end) + + Test.waitFrames(1) + + Spring.GiveOrderToUnitArray(Spring.GetTeamUnits(0), CMD.FIGHT, { midX, 0, midZ }, 0) + Spring.GiveOrderToUnitArray(Spring.GetTeamUnits(1), CMD.FIGHT, { midX, 0, midZ }, 0) + + Spring.SendCommands("setspeed " .. 20) + + -- wait until one team has no units left + Test.waitUntil(function() + return #(Spring.GetTeamUnits(0)) == 0 or #(Spring.GetTeamUnits(1)) == 0 + end, 30 * 30) + + Spring.SendCommands("setspeed " .. 1) + + if #(Spring.GetTeamUnits(0)) > #(Spring.GetTeamUnits(1)) then + winner = 0 + elseif #(Spring.GetTeamUnits(1)) > #(Spring.GetTeamUnits(0)) then + winner = 1 + end + + resultStr = "RESULT: " + if winner ~= nil then + unitName = units[winner] + if UnitDefNames and units[winner] and UnitDefNames[units[winner]] then + unitName = UnitDefNames[units[winner]].translatedHumanName or units[winner] + end + resultStr = resultStr .. "team " .. winner .. " wins" + resultStr = resultStr .. " with " .. #(Spring.GetAllUnits()) .. " " .. unitName .. " left" + else + resultStr = resultStr .. "tie" + end + + Spring.Echo(resultStr) + + -- pawns should win + assert(winner == 0) +end diff --git a/luaui/Widgets/Tests/example/test_wait.lua b/luaui/Widgets/Tests/example/test_wait.lua new file mode 100644 index 00000000000..fbd6879c2c6 --- /dev/null +++ b/luaui/Widgets/Tests/example/test_wait.lua @@ -0,0 +1,32 @@ +function setup() + Test.clearMap() +end + +function cleanup() + Test.clearMap() +end + +function test() + Spring.Echo("[test_wait] waiting 5 frames") + Test.waitFrames(5) + + local x, z = Game.mapSizeX / 2, Game.mapSizeZ / 2 + local y = Spring.GetGroundHeight(x, z) + + createdUnitID = SyncedRun(function(locals) + return Spring.CreateUnit("armpw", locals.x, locals.y, locals.z, 0, 0) + end) + + Spring.Echo("[test_wait] waiting for UnitCreated on unitID=" .. createdUnitID) + Test.waitUntilCallin("UnitCreated", function(unitID, unitDefID, unitTeam, builderID) + Spring.Echo("Saw UnitCreated for unitID=" .. unitID) + return unitID == createdUnitID + end, 10) + + startFrame = SyncedProxy.Spring.GetGameFrame() + + Spring.Echo("[test_wait] waiting 3 frames, but the hard way") + Test.waitUntil(function() + return (Spring.GetGameFrame() - startFrame > 3) + end) +end diff --git a/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua b/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua new file mode 100644 index 00000000000..e6779347b1f --- /dev/null +++ b/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua @@ -0,0 +1,52 @@ +function setup() + Test.clearMap() +end + +function cleanup() + Test.clearMap() +end + +function test() + widget = widgetHandler:FindWidget("Battle Resource Tracker") + assert(widget) + + widget.spatialHash:clear() + + combineEventsSpy = Test.spy(widget, "combineEvents") + + local x, z = Game.mapSizeX / 2, Game.mapSizeZ / 2 + local y = Spring.GetGroundHeight(x, z) + local n = 5 + + local unit = "armpw" + + unitM = UnitDefNames[unit].metalCost + unitE = UnitDefNames[unit].energyCost + + SyncedRun(function(locals) + for i = 1, locals.n do + Spring.CreateUnit(locals.unit, locals.x, locals.y, locals.z + 10 * i, 0, 0) + end + end) + + Test.clearMap() + + assert(#(combineEventsSpy.calls) == n - 1, #(combineEventsSpy.calls)) + + events = widget.spatialHash:allEvents() + assert(#events == 1) + + assert(events[1].n == n, events[1].n) + + totalM = 0 + for _, v in pairs(events[1].metal) do + totalM = totalM + v + end + assert(totalM == n * unitM) + + totalE = 0 + for _, v in pairs(events[1].energy) do + totalE = totalE + v + end + assert(totalE == n * unitE) +end diff --git a/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armasp.lua b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armasp.lua new file mode 100644 index 00000000000..12f87be6619 --- /dev/null +++ b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armasp.lua @@ -0,0 +1,46 @@ +function setup() + Test.clearMap() +end + +function cleanup() + Test.clearMap() +end + +function test() + widget = widgetHandler:FindWidget("Self-Destruct Icons") + assert(widget) + + local x, z = Game.mapSizeX / 2, Game.mapSizeZ / 2 + local y = Spring.GetGroundHeight(x, z) + + unitID = SyncedRun(function(locals) + return Spring.CreateUnit( + "armasp", + locals.x, locals.y, locals.z, + 0, 0 + ) + end) + + assert(table.count(widget.activeSelfD) == 0) + assert(table.count(widget.queuedSelfD) == 0) + + -- standard selfd command + Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, 0) + Test.waitFrames(1) + assert(table.count(widget.activeSelfD) == 1) + assert(table.count(widget.queuedSelfD) == 0) + + -- cancel selfd order + Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, 0) + Test.waitFrames(1) + assert(table.count(widget.activeSelfD) == 0) + assert(table.count(widget.queuedSelfD) == 0) + + -- currently fails + ---- queued selfd order (repair pad should not be able to queue selfd) + --Spring.GiveOrderToUnit(unitID, CMD.MOVE, { 1, 1, 1 }, 0) + --Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, { "shift" }) + --Test.waitFrames(1) + --assert(table.count(widget.activeSelfD) == 0) + --assert(table.count(widget.queuedSelfD) == 0) +end diff --git a/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armpw.lua b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armpw.lua new file mode 100644 index 00000000000..ee47e09a76d --- /dev/null +++ b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armpw.lua @@ -0,0 +1,52 @@ +function setup() + Test.clearMap() +end + +function cleanup() + Test.clearMap() +end + +function test() + widget = widgetHandler:FindWidget("Self-Destruct Icons") + assert(widget) + + local x, z = Game.mapSizeX / 2, Game.mapSizeZ / 2 + local y = Spring.GetGroundHeight(x, z) + + unitID = SyncedRun(function(locals) + return Spring.CreateUnit( + "armpw", + locals.x, locals.y, locals.z, + 0, 0 + ) + end) + + assert(table.count(widget.activeSelfD) == 0) + assert(table.count(widget.queuedSelfD) == 0) + + -- standard selfd command + Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, 0) + Test.waitFrames(1) + assert(table.count(widget.activeSelfD) == 1) + assert(table.count(widget.queuedSelfD) == 0) + + -- cancel selfd order + Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, 0) + Test.waitFrames(1) + assert(table.count(widget.activeSelfD) == 0) + assert(table.count(widget.queuedSelfD) == 0) + + -- queued selfd order + Spring.GiveOrderToUnit(unitID, CMD.MOVE, { 1, 1, 1 }, 0) + Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, { "shift" }) + Test.waitFrames(1) + assert(table.count(widget.activeSelfD) == 0) + assert(table.count(widget.queuedSelfD) == 1) + + -- currently fails + ---- remove move order + --Spring.GiveOrderToUnit(unitID, CMD.REMOVE, { CMD.MOVE }, { "alt" }) + --Test.waitFrames(1) + --assert(table.count(widget.activeSelfD) == 1) + --assert(table.count(widget.queuedSelfD) == 0) +end diff --git a/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua new file mode 100644 index 00000000000..c7c07c71b23 --- /dev/null +++ b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua @@ -0,0 +1,46 @@ +function setup() + Test.clearMap() +end + +function cleanup() + Test.clearMap() +end + +function test() + widget = widgetHandler:FindWidget("Self-Destruct Icons") + assert(widget) + + local x, z = Game.mapSizeX / 2, Game.mapSizeZ / 2 + local y = Spring.GetGroundHeight(x, z) + + unitID = SyncedRun(function(locals) + return Spring.CreateUnit( + "armvp", + locals.x, locals.y, locals.z, + 0, 0 + ) + end) + + assert(table.count(widget.activeSelfD) == 0) + assert(table.count(widget.queuedSelfD) == 0) + + -- standard selfd command + Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, 0) + Test.waitFrames(1) + assert(table.count(widget.activeSelfD) == 1) + assert(table.count(widget.queuedSelfD) == 0) + + -- cancel selfd order + Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, 0) + Test.waitFrames(1) + assert(table.count(widget.activeSelfD) == 0) + assert(table.count(widget.queuedSelfD) == 0) + + -- currently fails + ---- queued selfd order (factory can queue selfd, but it applies to units it builds, not itself) + --Spring.GiveOrderToUnit(unitID, CMD.MOVE, { 1, 1, 1 }, 0) + --Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, { "shift" }) + --Test.waitFrames(1) + --assert(table.count(widget.activeSelfD) == 0) + --assert(table.count(widget.queuedSelfD) == 0) +end diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua new file mode 100644 index 00000000000..40261b16ac4 --- /dev/null +++ b/luaui/Widgets/dbg_test_framework.lua @@ -0,0 +1,750 @@ +function widget:GetInfo() + return { + name = "Test Framework Widget", + desc = "Test framework helper widget", + date = "2023", + license = "GNU GPL, v2 or later", + version = 0, + layer = 0, + enabled = true, + handler = true, + } +end + +VFS.Include('luarules/testing/util.lua') + +local LOG_LEVEL = LOG.INFO + +local RETURN_TIMEOUT = 30 +local DEFAULT_WAIT_TIMEOUT = 5 * 30 + +local SHOW_ALL_RESULTS = false + +-- utils +-- ===== +local function log(level, str, ...) + if level < LOG_LEVEL then + return + end + Spring.Log( + widget:GetInfo().name, + LOG.NOTICE, + str + ) +end + +-- main code +-- ========= + +local testRoots = { + "LuaUI/Widgets/tests", + "LuaUI/Tests", + "luarules/tests/", +} + +local function findTestFiles(directory, patterns, rootDirectory, result) + if rootDirectory == nil then + if directory:sub(-1) ~= '/' then + directory = directory .. '/' + end + rootDirectory = directory + end + + if result == nil then + result = {} + end + + for _, filename in ipairs(VFS.DirList(directory, "*", VFS.RAW_FIRST)) do + local relativePath = string.sub(filename, string.len(rootDirectory) + 1) + local withoutExtension = removeFileExtension(filename) + if patterns == nil or #patterns == 0 or matchesPatterns(withoutExtension, patterns) then + log(LOG.INFO, "Found test file: " .. relativePath) + result[#result + 1] = { + filename = filename, + label = relativePath, + } + end + end + + for _, subDir in ipairs(VFS.SubDirs(directory, "*", VFS.RAW_FIRST)) do + findTestFiles(subDir, patterns, rootDirectory, result) + end + + return result +end + +local function findAllTestFiles(patterns) + log(LOG.DEBUG, "[findAllTestFiles] " .. table.toString({ + patterns = patterns, + })) + local result = {} + for _, path in ipairs(testRoots) do + for _, testFileInfo in ipairs(findTestFiles(path, patterns)) do + result[#result + 1] = testFileInfo + end + end + return result +end + +local function displayTestResults(results) + log(LOG.INFO, "=====TEST RESULTS=====") + for _, testResult in ipairs(results) do + log(LOG.INFO, formatTestResult(testResult)) + end +end + +local testRunState +local activeTestState +local resumeState +local returnState +local callinState +local spyControls + +local function resetTestRunState() + log(LOG.DEBUG, "[resetTestRunState]") + testRunState = { + runningTests = false, + files = {}, + filesIndex = nil, + results = {}, + } +end + +local function resetActiveTestState() + log(LOG.DEBUG, "[resetActiveTestState]") + activeTestState = { + coroutine = nil, + environment = nil, + startFrame = nil, + file = nil, + label = nil, + } +end + +local function resetResumeState() + log(LOG.DEBUG, "[resetResumeState]") + resumeState = { + predicate = nil, + timeoutLeft = nil, + } +end + +local function resetReturnState() + log(LOG.DEBUG, "[resetReturnState]") + returnState = { + waitingForReturnId = nil, + success = nil, + pendingValueOrError = nil, + timeoutLeft = nil, + } +end + +local function resetCallinState() + log(LOG.DEBUG, "[resetCallinState]") + callinState = { + buffer = {}, + current = {}, + } +end + +local function resetSpyCtrls() + log(LOG.DEBUG, "[resetSpyCtrls]") + spyControls = {} +end + +local function resetState() + log(LOG.DEBUG, "[resetState]") + resetTestRunState() + resetActiveTestState() + resetResumeState() + resetReturnState() + resetCallinState() + resetSpyCtrls() +end + +resetState() + +registerCallins(widget, function(name, args) + if not testRunState.runningTests then + return + end + + if callinState.buffer[name] == nil then + callinState.buffer[name] = {} + end + callinState.buffer[name][#(callinState.buffer) + 1] = args +end) + +local function startTests(patterns) + log(LOG.DEBUG, "[startTests] " .. table.toString({ + patterns = patterns, + })) + + resetState() + + log(LOG.NOTICE, "=====FINDING TESTS=====") + + if type(patterns) == "string" then + patterns = { patterns } + end + + testRunState.files = findAllTestFiles(patterns) + + if testRunState.files == nil or #(testRunState.files) == 0 then + log(LOG.INFO, "no test files found") + return + end + + testRunState.runningTests = true + testRunState.index = 1 + + log(LOG.NOTICE, "=====RUNNING TESTS=====") +end + +local function finishTest(result) + for _, control in ipairs(spyControls) do + control.remove() + end + + result.label = result.label or activeTestState.label + if activeTestState and activeTestState.startFrame and result.frames == nil then + result.frames = Spring.GetGameFrame() - activeTestState.startFrame + end + + log(LOG.NOTICE, formatTestResult(result)) + + testRunState.results[#(testRunState.results) + 1] = result + + resetActiveTestState() + resetResumeState() + resetReturnState() + resetCallinState() + + if testRunState.index < #(testRunState.files) then + testRunState.index = testRunState.index + 1 + else + -- done + testRunState.index = nil + testRunState.runningTests = false + if SHOW_ALL_RESULTS then + displayTestResults(testRunState.results) + end + end +end + +local function createNestedProxy(prefix, path) + return setmetatable({}, { + __index = function(_, key) + local newPath = path and (path .. "." .. key) or key + return createNestedProxy(prefix, newPath) + end, + __call = function(_, ...) + local args = { ... } + local serializedFn, returnId = serializeFunctionCall(path, args) + + returnState = { + waitingForReturnId = returnId, + success = nil, + pendingValueOrError = nil, + timeoutLeft = RETURN_TIMEOUT, + } + + log(LOG.DEBUG, "[createNestedProxy." .. prefix .. ".send] " .. table.toString({ + serializedFn = serializedFn, + })) + Spring.SendLuaRulesMsg(prefix .. serializedFn) + + local resumeOk, resumeResult = coroutine.yield() + + log(LOG.DEBUG, "[createNestedProxy." .. prefix .. ".return] " .. table.toString({ + resumeOk = resumeOk, + resumeResult = resumeResult, + })) + + if not resumeOk then + error(resumeResult[1], 2) + end + + return unpack(resumeResult) + end, + }) +end + +SyncedProxy = createNestedProxy(PROXY_PREFIX) +SyncedExtra = createNestedProxy(PROXY_EXTRA_PREFIX) + +SyncedRun = function(fn) + local serializedFn, returnId = serializeFunctionRun(fn, 3) + + returnState = { + waitingForReturnId = returnId, + success = nil, + pendingValueOrError = nil, + timeoutLeft = RETURN_TIMEOUT, + } + + log(LOG.DEBUG, "[SyncedRun.send] " .. table.toString({ + serializedFn = serializedFn, + })) + Spring.SendLuaRulesMsg(PROXY_RUN_PREFIX .. serializedFn) + + local resumeOk, resumeResult = coroutine.yield() + + log(LOG.DEBUG, "[SyncedRun.return] " .. table.toString({ + resumeOk = resumeOk, + resumeResult = resumeResult, + })) + + if not resumeOk then + error(resumeResult[1], 3) + end + + return unpack(resumeResult) +end + +Test = { + waitUntil = function(f, timeout, errorOffset) + if timeout == nil then + timeout = DEFAULT_WAIT_TIMEOUT + end + + resumeState = { + predicate = f, + timeoutLeft = timeout, + } + + local resumeOk, resumeResult = coroutine.yield() + + log(LOG.DEBUG, "[waitUntil.return] " .. table.toString({ + resumeOk = resumeOk, + resumeResult = resumeResult, + })) + + resetResumeState() + + if not resumeOk then + error(resumeResult, 3 + (errorOffset or 0)) + end + end, + waitFrames = function(frames) + local startFrame = Spring.GetGameFrame() + Test.waitUntil( + function() + return Spring.GetGameFrame() >= (startFrame + frames) + end, + frames + 5, + 1 + ) + end, + waitUntilCallin = function(name, predicate, timeout) + Test.waitUntil( + function() + for _, args in ipairs(callinState.current[name]) do + if predicate == nil or predicate(unpack(args)) then + return true + end + end + return false + end, + timeout, + 1 + ) + end, + spy = function(...) + local spyCtrl = spy(...) + spyControls[#spyControls + 1] = spyCtrl + return spyCtrl + end, + clearMap = SyncedExtra.clearMap, +} + +function widget:RecvLuaMsg(msg) + if not returnState.waitingForReturnId then + return + end + + if msg:sub(1, #PROXY_RETURN_PREFIX) == PROXY_RETURN_PREFIX then + local serializedReturn = msg:sub(#PROXY_RETURN_PREFIX + 1) + local returnOk, returnValueOrError, returnId = deserializeFunctionReturn(serializedReturn) + + log(LOG.DEBUG, "[RecvLuaMsg] " .. table.toString({ + serializedReturn = serializedReturn, + returnOk = returnOk, + returnValue = returnValueOrError, + returnId = returnId, + })) + + if returnId == returnState.waitingForReturnId then + -- this is the return we were waiting for (otherwise we ignore it) + returnState = { + waitingForReturnId = nil, + success = returnOk, + pendingValueOrError = returnValueOrError, + timeoutLeft = nil, + } + end + end +end + +local function runTestInternal() + log(LOG.DEBUG, "[runTestInternal]") + + local setupOk, setupResult + if setup ~= nil then + setupOk, setupResult = yieldable_pcall(setup) + else + setupOk = true + end + + local testOk, testResult + if setupOk then + testOk, testResult = yieldable_pcall(test) + end + + -- always try to run cleanup + local cleanupOk, cleanupResult + if cleanup ~= nil then + cleanupOk, cleanupResult = yieldable_pcall(cleanup) + else + cleanupOk = true + end + + if not cleanupOk then + error(cleanupResult, 2) + end + + if not setupOk then + error(setupResult, 2) + end + + if not testOk then + error(testResult, 2) + end +end + +local function initializeTestEnvironment() + local env = { + -- test framework + Test = Test, + SyncedProxy = SyncedProxy, + SyncedRun = SyncedRun, + __runTestInternal = runTestInternal, + yieldable_pcall = yieldable_pcall, + + -- widgets + widgetHandler = widgetHandler, + + -- game + VFS = VFS, + Script = Script, + Spring = Spring, + Engine = Engine, + Platform = Platform, + Game = Game, + gl = gl, + GL = GL, + CMD = CMD, + CMDTYPE = CMDTYPE, + LOG = LOG, + + UnitDefs = UnitDefs, + UnitDefNames = UnitDefNames, + FeatureDefs = FeatureDefs, + FeatureDefNames = FeatureDefNames, + WeaponDefs = WeaponDefs, + WeaponDefNames = WeaponDefNames, + + pack = pack, + pcall = pcall, + io = io, + os = os, + math = math, + debug = debug, + tracy = tracy, + table = table, + string = string, + package = package, + --coroutine = coroutine, + assert = assert, + error = error, + print = print, + next = next, + pairs = pairs, + ipairs = ipairs, + tonumber = tonumber, + tostring = tostring, + type = type, + unpack = unpack, + select = select, + + Json = Json, + } + + return env +end + +local function loadTestFromFile(filename) + if not VFS.FileExists(filename) then + return false, "missing file: " .. filename + end + + local text = VFS.LoadFile(filename, VFS.RAW_FIRST) + + if text == nil or text == "" then + return false, "missing file content: " .. filename + end + + local chunk, err = loadstring(text, filename) + if chunk == nil then + return false, err + end + + local testEnvironment = initializeTestEnvironment() + + setfenv(chunk, testEnvironment) + + local success, err = pcall(chunk) + if not success then + return false, err + end + + if testEnvironment.test == nil then + return false, "no test() function" + end + + setfenv(testEnvironment.__runTestInternal, testEnvironment) + + return true, testEnvironment +end + +local function handleReturn() + log(LOG.DEBUG, "[handleReturn]") + if returnState.success == nil and returnState.waitingForReturnId == nil then + -- no return to handle, so just continue + log(LOG.DEBUG, "[handleReturn] nil success -> continue") + return { + status = "continue", + } + end + + if returnState.timeoutLeft ~= nil then + returnState.timeoutLeft = returnState.timeoutLeft - 1 + log(LOG.DEBUG, "decrementing return timeout: " .. returnState.timeoutLeft) + + if returnState.timeoutLeft == 0 then + -- resume took too long, result is error + log(LOG.DEBUG, "[handleReturn] timeout -> error") + return { + status = "error", + error = "waiting for synced return timed out" + } + end + end + + if returnState.waitingForReturnId then + log(LOG.DEBUG, "[handleReturn] waiting for return -> wait") + return { + status = "wait" + } + end + + local tempReturnValue = returnState.pendingValueOrError + returnState.pendingValueOrError = nil + + if returnState.success then + -- we're ok to resume + log(LOG.DEBUG, "[handleReturn] success -> continue") + resetReturnState() + return { + status = "continue", + returnValue = tempReturnValue + } + else + -- remote function errored + log(LOG.DEBUG, "[handleReturn] remote error -> error") + return { + status = "error", + error = tempReturnValue, + } + end +end + +local function handleWait() + log(LOG.DEBUG, "[handleWait]") + if resumeState.predicate == nil then + log(LOG.DEBUG, "[handleWait] nil predicate -> continue") + return { + status = "continue", + } + end + + resumeState.timeoutLeft = resumeState.timeoutLeft - 1 + + if resumeState.timeoutLeft == 0 then + -- resume took too long, result is error + log(LOG.DEBUG, "[handleWait] timeout -> error") + return { + status = "error", + error = "resume predicate timed out", + } + end + + local success, returnOrError = pcall(resumeState.predicate) + if success then + -- predicate ran successfully + log(LOG.DEBUG, "[handleWait] predicate success") + if returnOrError then + -- succeeded, we can resume + log(LOG.DEBUG, "[handleWait] predicate success + true -> continue") + resetResumeState() + return { + status = "continue" + } + else + -- failed, wait and try again next frame + log(LOG.DEBUG, "[handleWait] predicate success + false -> wait") + return { + status = "wait" + } + end + else + -- error during predicate + log(LOG.DEBUG, "[handleWait] predicate error -> error") + return { + status = "error", + error = returnOrError + } + end + + log(LOG.DEBUG, "[handleWait] fallthrough -> continue") + return { + status = "continue", + } +end + +function widget:GameFrame(frame) + if not testRunState.runningTests then + return + end + + log(LOG.DEBUG, "FRAME " .. frame .. " | " .. table.toString({ + resumeState = { + predicate = resumeState.predicate ~= nil and "present" or "nil", + timeoutLeft = resumeState.timeoutLeft, + }, + returnState = returnState, + coroutineStatus = activeTestState.coroutine and coroutine.status(activeTestState.coroutine) or "nil", + callinState = callinState, + })) + + --*result = { + -- status = + -- | "continue" (ok to continue to other checks and resuming test) + -- | "error" (an error occurred, skip directly to resuming test and pass the error for it to be handled) + -- | "wait" (waiting on something, return immediately) + -- error = (status="error" only) + -- returnValue = (status="continue" only, optional) + --} + local returnResult = handleReturn() + log(LOG.DEBUG, "[returnResult] " .. table.toString({ + returnResult = returnResult, + })) + + if returnResult.status == "wait" then + return + end + + local waitResult = handleWait() + log(LOG.DEBUG, "[waitResult] " .. table.toString({ + waitResult = waitResult, + })) + + if waitResult.status == "wait" then + return + end + + -- is there a test set up? if not, create one + if activeTestState.coroutine == nil then + activeTestState.label = testRunState.files[testRunState.index].label + activeTestState.file = testRunState.files[testRunState.index] + + local success, envOrError = loadTestFromFile(activeTestState.file.filename) + + if success then + log(LOG.DEBUG, "Initializing test: " .. activeTestState.label) + activeTestState.environment = envOrError + activeTestState.coroutine = coroutine.create(activeTestState.environment.__runTestInternal) + activeTestState.startFrame = frame + else + finishTest({ + result = TEST_RESULT.ERROR, + error = envOrError + }) + return + end + end + + -- in between resumes, shift the callin buffer (so we get all calls that happened since we last resumed; this + -- includes callins that happened during a wait or synced call) + callinState.current = callinState.buffer + callinState.buffer = {} + + -- resume the test + if coroutine.status(activeTestState.coroutine) == "suspended" then + local coroutineOk, coroutineArgs + if returnResult.returnValue ~= nil then + coroutineOk = true + coroutineArgs = returnResult.returnValue + elseif returnResult.status == "error" then + coroutineOk = false + coroutineArgs = returnResult.error + elseif waitResult.status == "error" then + coroutineOk = false + coroutineArgs = waitResult.error + else + coroutineOk = true + coroutineArgs = nil + end + + log( + LOG.DEBUG, + "Resuming test: " .. activeTestState.label .. " with value: " .. table.toString({ + coroutineOk = coroutineOk, + coroutineArgs = coroutineArgs, + }) + ) + + local result, error = coroutine.resume(activeTestState.coroutine, coroutineOk, coroutineArgs) + if not result then + -- test fail + finishTest({ + result = TEST_RESULT.FAIL, + error = error, + }) + return + end + end + + if coroutine.status(activeTestState.coroutine) == "dead" then + -- test pass + finishTest({ + result = TEST_RESULT.PASS, + }) + end +end + +function widget:Initialize() + widgetHandler.actionHandler:AddAction( + self, + "runtests", + function(cmd, optLine, optWords, data, isRepeat, release, actions) + startTests(splitPhrases(optLine)) + end, + nil, + "t" + ) +end + +function widget:Shutdown() + widgetHandler.actionHandler:RemoveAction("runtests", "t") +end diff --git a/luaui/Widgets/gui_selfd_icons.lua b/luaui/Widgets/gui_selfd_icons.lua index 5d1bd474970..c4db755b01f 100644 --- a/luaui/Widgets/gui_selfd_icons.lua +++ b/luaui/Widgets/gui_selfd_icons.lua @@ -35,11 +35,11 @@ local font = gl.LoadFont(fontfile, fontfileSize*fontfileScale, fontfileOutlineSi -- {unitID -> unitDefID, ... } -- presence of a unitID key indicates that the unit has an active (counting down) SELFD command -local activeSelfD = {} +activeSelfD = {} -- {unitID -> unitDefID, ... } -- presence of a unitID key indicates that the unit has a queued SELFD command -local queuedSelfD = {} +queuedSelfD = {} local drawLists = {} From 712484f15c665fe89ee3ddad10942a799bcfa6eb Mon Sep 17 00:00:00 2001 From: Citrine Date: Thu, 11 Jan 2024 11:32:17 -0800 Subject: [PATCH 02/98] style(test_framework): remove author field --- luarules/gadgets/dbg_test_framework_proxy.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/luarules/gadgets/dbg_test_framework_proxy.lua b/luarules/gadgets/dbg_test_framework_proxy.lua index 61f43417b41..da236f51ff1 100644 --- a/luarules/gadgets/dbg_test_framework_proxy.lua +++ b/luarules/gadgets/dbg_test_framework_proxy.lua @@ -2,7 +2,6 @@ function gadget:GetInfo() return { name = "Test Framework Proxy", desc = "Proxy for Synced commands", - author = "citrine", date = "2023", license = "GNU GPL, v2 or later", version = 0, From 162c953b84d01d42892639c7939a68c07d62bb08 Mon Sep 17 00:00:00 2001 From: Citrine Date: Thu, 11 Jan 2024 11:16:27 -0800 Subject: [PATCH 03/98] feat(test_framework): run test code during Update() and GameFrame() This allows tests to run pre-game. --- luaui/Widgets/dbg_test_framework.lua | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 40261b16ac4..c25bfeafe7a 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -622,7 +622,7 @@ local function handleWait() } end -function widget:GameFrame(frame) +local function step() if not testRunState.runningTests then return end @@ -674,7 +674,7 @@ function widget:GameFrame(frame) log(LOG.DEBUG, "Initializing test: " .. activeTestState.label) activeTestState.environment = envOrError activeTestState.coroutine = coroutine.create(activeTestState.environment.__runTestInternal) - activeTestState.startFrame = frame + activeTestState.startFrame = Spring.GetGameFrame() else finishTest({ result = TEST_RESULT.ERROR, @@ -733,6 +733,14 @@ function widget:GameFrame(frame) end end +function widget:GameFrame(frame) + step() +end + +function widget:Update(dt) + step() +end + function widget:Initialize() widgetHandler.actionHandler:AddAction( self, From e35d0100be88b57d69f111f2e694656da048ef5b Mon Sep 17 00:00:00 2001 From: Citrine Date: Thu, 11 Jan 2024 11:19:05 -0800 Subject: [PATCH 04/98] feat(test_framework): add timers and waitTime --- luarules/testing/util.lua | 3 +++ luaui/Widgets/dbg_test_framework.lua | 36 +++++++++++++++++++++++++++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/luarules/testing/util.lua b/luarules/testing/util.lua index b07ece57bcd..ef7cab4ed3b 100644 --- a/luarules/testing/util.lua +++ b/luarules/testing/util.lua @@ -65,6 +65,9 @@ function formatTestResult(testResult) if testResult.frames ~= nil then s = s .. " [" .. testResult.frames .. " frames]" end + if testResult.milliseconds ~= nil then + s = s .. " [" .. testResult.milliseconds .. " ms]" + end if testResult.error ~= nil then s = s .. " | " .. testResult.error end diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index c25bfeafe7a..02a947b0c0e 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -93,6 +93,10 @@ local function displayTestResults(results) end end +local gameTimer +local runTestsTimer +local testTimer + local testRunState local activeTestState local resumeState @@ -199,6 +203,8 @@ local function startTests(patterns) testRunState.index = 1 log(LOG.NOTICE, "=====RUNNING TESTS=====") + + runTestsTimer = Spring.GetTimer() end local function finishTest(result) @@ -211,6 +217,8 @@ local function finishTest(result) result.frames = Spring.GetGameFrame() - activeTestState.startFrame end + result.milliseconds = Spring.DiffTimers(Spring.GetTimer(), testTimer, true) + log(LOG.NOTICE, formatTestResult(result)) testRunState.results[#(testRunState.results) + 1] = result @@ -336,6 +344,16 @@ Test = { 1 ) end, + waitTime = function(milliseconds, timeout) + local startTimer = Spring.GetTimer() + Test.waitUntil( + function() + return Spring.DiffTimers(Spring.GetTimer(), startTimer, true) >= milliseconds + end, + timeout or (milliseconds / 10 + 1), -- assume 100fps for timeout + 1 + ) + end, waitUntilCallin = function(name, predicate, timeout) Test.waitUntil( function() @@ -622,12 +640,24 @@ local function handleWait() } end +local function getGameTime() + return Spring.DiffTimers(Spring.GetTimer(), gameTimer, true) +end + +local function getRunTestsTime() + return Spring.DiffTimers(Spring.GetTimer(), runTestsTimer, true) +end + +local function getTestTime() + return Spring.DiffTimers(Spring.GetTimer(), testTimer, true) +end + local function step() if not testRunState.runningTests then return end - log(LOG.DEBUG, "FRAME " .. frame .. " | " .. table.toString({ + log(LOG.DEBUG, "FRAME " .. Spring.GetGameFrame() .. " | " .. getRunTestsTime() .. "ms | " .. table.toString({ resumeState = { predicate = resumeState.predicate ~= nil and "present" or "nil", timeoutLeft = resumeState.timeoutLeft, @@ -675,6 +705,8 @@ local function step() activeTestState.environment = envOrError activeTestState.coroutine = coroutine.create(activeTestState.environment.__runTestInternal) activeTestState.startFrame = Spring.GetGameFrame() + + testTimer = Spring.GetTimer() else finishTest({ result = TEST_RESULT.ERROR, @@ -751,6 +783,8 @@ function widget:Initialize() nil, "t" ) + + gameTimer = Spring.GetTimer() end function widget:Shutdown() From 8dec91bdf2fe54917644ff094d26c0ed9319b4ea Mon Sep 17 00:00:00 2001 From: Citrine Date: Thu, 11 Jan 2024 11:19:18 -0800 Subject: [PATCH 05/98] feat(test_framework): expose WG to tests --- luaui/Widgets/dbg_test_framework.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 02a947b0c0e..7856c810b19 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -451,6 +451,7 @@ local function initializeTestEnvironment() -- widgets widgetHandler = widgetHandler, + WG = WG, -- game VFS = VFS, From 830691b645ee8015cc3e1dd2a0013e20209839ce Mon Sep 17 00:00:00 2001 From: Citrine Date: Thu, 11 Jan 2024 11:10:02 -0800 Subject: [PATCH 06/98] test(cmd_stop_selfd): add test for basic functionality --- .../cmd_stop_selfd/test_cmd_stop_selfd.lua | 41 +++++++++++++++++++ 1 file changed, 41 insertions(+) create mode 100644 luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua diff --git a/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua b/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua new file mode 100644 index 00000000000..00574d1d749 --- /dev/null +++ b/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua @@ -0,0 +1,41 @@ +function setup() + Test.clearMap() +end + +function cleanup() + Test.clearMap() +end + +function test() + widget = widgetHandler:FindWidget("Stop means Stop") + assert(widget) + + local myTeamID = Spring.GetMyTeamID() + + unitID = SyncedRun(function(locals) + local x, z = Game.mapSizeX / 2, Game.mapSizeZ / 2 + local y = Spring.GetGroundHeight(x, z) + return Spring.CreateUnit("armpw", x, y, z, 0, locals.myTeamID) + end) + + -- issue selfd and then issue stop + Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, 0) + Test.waitFrames(1) + assert(Spring.GetUnitSelfDTime(unitID) > 0) + + Spring.GiveOrderToUnit(unitID, CMD.STOP, {}, 0) + Test.waitFrames(1) + assert(Spring.GetUnitSelfDTime(unitID) == 0) + assert(#(Spring.GetCommandQueue(unitID, 1)) == 0) + + -- issue {move, selfd}, then issue stop + Spring.GiveOrderToUnit(unitID, CMD.MOVE, { 1, 1, 1 }, 0) + Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, { "shift" }) + Test.waitFrames(1) + assert(Spring.GetUnitSelfDTime(unitID) == 0) + + Spring.GiveOrderToUnit(unitID, CMD.STOP, {}, 0) + Test.waitFrames(1) + assert(Spring.GetUnitSelfDTime(unitID) == 0) + assert(#(Spring.GetCommandQueue(unitID, 1)) == 0) +end From edb148d49404a0d56fefdd1aa7426aba0c17a0f9 Mon Sep 17 00:00:00 2001 From: Citrine Date: Thu, 11 Jan 2024 12:04:57 -0800 Subject: [PATCH 07/98] fix(test_framework): fix waitTime timeout math --- luaui/Widgets/dbg_test_framework.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 7856c810b19..b2f1878afcc 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -350,7 +350,7 @@ Test = { function() return Spring.DiffTimers(Spring.GetTimer(), startTimer, true) >= milliseconds end, - timeout or (milliseconds / 10 + 1), -- assume 100fps for timeout + timeout or (milliseconds * 30 / 1000 + 5), 1 ) end, From 72acf657a59a0fb5634c6c304cc41cb876e4d892 Mon Sep 17 00:00:00 2001 From: Citrine Date: Thu, 11 Jan 2024 12:06:11 -0800 Subject: [PATCH 08/98] fix(test_framework): temporarily disable Update step right now it breaks wait timeouts --- luaui/Widgets/dbg_test_framework.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index b2f1878afcc..54e185546d4 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -771,7 +771,7 @@ function widget:GameFrame(frame) end function widget:Update(dt) - step() + --step() end function widget:Initialize() From 9d5d99fdbfefb54096ab34f12310dabf7ca7a6b1 Mon Sep 17 00:00:00 2001 From: Citrine Date: Thu, 11 Jan 2024 12:32:56 -0800 Subject: [PATCH 09/98] fix(test_framework): restore step during update, and update wait test --- luaui/Widgets/Tests/example/test_wait.lua | 3 +++ luaui/Widgets/dbg_test_framework.lua | 27 +++++++++-------------- 2 files changed, 14 insertions(+), 16 deletions(-) diff --git a/luaui/Widgets/Tests/example/test_wait.lua b/luaui/Widgets/Tests/example/test_wait.lua index fbd6879c2c6..c01fb82b6b6 100644 --- a/luaui/Widgets/Tests/example/test_wait.lua +++ b/luaui/Widgets/Tests/example/test_wait.lua @@ -29,4 +29,7 @@ function test() Test.waitUntil(function() return (Spring.GetGameFrame() - startFrame > 3) end) + + Spring.Echo("[test_wait] waiting 1000 ms") + Test.waitTime(1000) end diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 54e185546d4..95439623054 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -129,7 +129,7 @@ local function resetResumeState() log(LOG.DEBUG, "[resetResumeState]") resumeState = { predicate = nil, - timeoutLeft = nil, + timeoutExpireFrame = nil, } end @@ -139,7 +139,7 @@ local function resetReturnState() waitingForReturnId = nil, success = nil, pendingValueOrError = nil, - timeoutLeft = nil, + timeoutExpireFrame = nil, } end @@ -254,7 +254,7 @@ local function createNestedProxy(prefix, path) waitingForReturnId = returnId, success = nil, pendingValueOrError = nil, - timeoutLeft = RETURN_TIMEOUT, + timeoutExpireFrame = Spring.GetGameFrame() + RETURN_TIMEOUT, } log(LOG.DEBUG, "[createNestedProxy." .. prefix .. ".send] " .. table.toString({ @@ -288,7 +288,7 @@ SyncedRun = function(fn) waitingForReturnId = returnId, success = nil, pendingValueOrError = nil, - timeoutLeft = RETURN_TIMEOUT, + timeoutExpireFrame = Spring.GetGameFrame() + RETURN_TIMEOUT, } log(LOG.DEBUG, "[SyncedRun.send] " .. table.toString({ @@ -318,7 +318,7 @@ Test = { resumeState = { predicate = f, - timeoutLeft = timeout, + timeoutExpireFrame = Spring.GetGameFrame() + timeout, } local resumeOk, resumeResult = coroutine.yield() @@ -398,7 +398,7 @@ function widget:RecvLuaMsg(msg) waitingForReturnId = nil, success = returnOk, pendingValueOrError = returnValueOrError, - timeoutLeft = nil, + timeoutExpireFrame = nil, } end end @@ -546,11 +546,8 @@ local function handleReturn() } end - if returnState.timeoutLeft ~= nil then - returnState.timeoutLeft = returnState.timeoutLeft - 1 - log(LOG.DEBUG, "decrementing return timeout: " .. returnState.timeoutLeft) - - if returnState.timeoutLeft == 0 then + if returnState.timeoutExpireFrame ~= nil then + if Spring.GetGameFrame() >= returnState.timeoutExpireFrame then -- resume took too long, result is error log(LOG.DEBUG, "[handleReturn] timeout -> error") return { @@ -597,9 +594,7 @@ local function handleWait() } end - resumeState.timeoutLeft = resumeState.timeoutLeft - 1 - - if resumeState.timeoutLeft == 0 then + if Spring.GetGameFrame() >= resumeState.timeoutExpireFrame then -- resume took too long, result is error log(LOG.DEBUG, "[handleWait] timeout -> error") return { @@ -661,7 +656,7 @@ local function step() log(LOG.DEBUG, "FRAME " .. Spring.GetGameFrame() .. " | " .. getRunTestsTime() .. "ms | " .. table.toString({ resumeState = { predicate = resumeState.predicate ~= nil and "present" or "nil", - timeoutLeft = resumeState.timeoutLeft, + timeoutExpireFrame = resumeState.timeoutExpireFrame, }, returnState = returnState, coroutineStatus = activeTestState.coroutine and coroutine.status(activeTestState.coroutine) or "nil", @@ -771,7 +766,7 @@ function widget:GameFrame(frame) end function widget:Update(dt) - --step() + step() end function widget:Initialize() From fc3ee0a30d0ffbc6dda4129f290f9cedf74050af Mon Sep 17 00:00:00 2001 From: Citrine Date: Thu, 11 Jan 2024 18:20:41 -0800 Subject: [PATCH 10/98] Add summary of serpent.lua Co-authored-by: sprunk --- common/luaUtilities/serpent.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/common/luaUtilities/serpent.lua b/common/luaUtilities/serpent.lua index d3461829ddf..27a2bf03368 100644 --- a/common/luaUtilities/serpent.lua +++ b/common/luaUtilities/serpent.lua @@ -1,3 +1,4 @@ +-- provides a consistent interface for de/serialisation local n, v = "serpent", "0.303" -- (C) 2012-18 Paul Kulchenko; MIT License local c, d = "Paul Kulchenko", "Lua serializer and pretty printer" local snum = {[tostring(1/0)]='1/0 --[[math.huge]]',[tostring(-1/0)]='-1/0 --[[-math.huge]]',[tostring(0/0)]='0/0'} From fa2abf4e1c1beedfc1c76fa0e3926e0144becd62 Mon Sep 17 00:00:00 2001 From: Citrine Date: Fri, 12 Jan 2024 11:32:04 -0800 Subject: [PATCH 11/98] style(barwidgets): remove extra whitespace --- luaui/barwidgets.lua | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/luaui/barwidgets.lua b/luaui/barwidgets.lua index cdfb4c8fa44..7815f8b8458 100644 --- a/luaui/barwidgets.lua +++ b/luaui/barwidgets.lua @@ -438,19 +438,19 @@ end function widgetHandler:AddSpadsMessage(contents) -- The canonical, agreed format is the following: -- This must be called from an unsynced context, cause it needs playername and playerid and stuff - + -- The game sends a lua message, which should be base64'd to prevent wierd character bullshit: - -- Lua Message Format: + -- Lua Message Format: -- leetspeek luaspads:base64message -- lu@$p@d$:ABCEDFGS== - -- Must contain, with triangle bracket literals [space][space] + -- Must contain, with triangle bracket literals [space][space] -- will get parsed by barmanager, and forwarded to autohostmonitor as: - -- match-event <35> + -- match-event <35> local myPlayerID = Spring.GetMyPlayerID() local myPlayerName = Spring.GetPlayerInfo(myPlayerID,false) local gameSeconds = math.max(0,math.round(Spring.GetGameFrame() / 30)) - if type(contents) == 'table' then - contents = Json.encode(contents) + if type(contents) == 'table' then + contents = Json.encode(contents) end local rawmessage = string.format("<%s> <%s> <%d>", myPlayerName, contents, gameSeconds) local b64message = 'lu@$p@d$:' .. string.base64Encode(rawmessage) @@ -487,7 +487,7 @@ function widgetHandler:LoadWidget(filename, fromZip) -- user widgets may not access widgetHandler -- fixme: remove the or true part if widget.GetInfo and widget:GetInfo().handler then - if fromZip or true then + if fromZip or true then widget.widgetHandler = self else Spring.Echo('Failed to load: ' .. basename .. ' (user widgets may not access widgetHandler)', fromZip, filename, allowuserwidgets) @@ -554,9 +554,9 @@ function widgetHandler:LoadWidget(filename, fromZip) self.knownWidgets[name].active = false return nil end - if not fromZip then + if not fromZip then local md5 = VFS.CalculateHash(text,0) - if widgetHandler.widgetHashes[md5] == nil then + if widgetHandler.widgetHashes[md5] == nil then widgetHandler.widgetHashes[md5] = filename -- Embed LuaRules message that we enabled a new user widget --local success, err = pcall(widgetHandler.AddSpadsMessage, widgetHandler, tostring(filename) .. ":" .. tostring(md5)) @@ -565,7 +565,7 @@ function widgetHandler:LoadWidget(filename, fromZip) --end end end - + -- load the config data local config = self.configData[name] if widget.SetConfigData and config then @@ -1581,7 +1581,7 @@ function widgetHandler:KeyPress(key, mods, isRepeat, label, unicode, scanCode, a return false end -function widgetHandler:KeyRelease(key, mods, label, unicode, scanCode, actions) +function widgetHandler:KeyRelease(key, mods, label, unicode, scanCode, actions) tracy.ZoneBeginN("W:KeyRelease") local textOwner = self.textOwner @@ -1716,7 +1716,7 @@ function widgetHandler:ControllerAdded(deviceIndex) return true end end - + tracy.ZoneEnd() return false end @@ -2196,7 +2196,7 @@ function widgetHandler:UnitIdle(unitID, unitDefID, unitTeam) end function widgetHandler:UnitCommand(unitID, unitDefID, unitTeam, cmdId, cmdParams, cmdOpts, cmdTag, playerID, fromSynced, fromLua) - + tracy.ZoneBeginN("W:UnitCommand") for _, w in ipairs(self.UnitCommandList) do w:UnitCommand(unitID, unitDefID, unitTeam, From eb133c096434bdb8a478854e7e4c3266ce7db2b4 Mon Sep 17 00:00:00 2001 From: Citrine Date: Fri, 12 Jan 2024 12:23:00 -0800 Subject: [PATCH 12/98] feat(barwidgets): add option to expose locals when loading widgets Calling `widgetHandler:EnableWidget(widgetName, true)` instead of `widgetHandler:EnableWidget(widgetName)` will now allow access to local variables as if they were globals, with `widget.localVariable`. Read and write are both supported. --- luarules/testing/localsAccess.lua | 77 +++++++++++++++++++++++++++++++ luaui/barwidgets.lua | 44 ++++++++++++++++-- 2 files changed, 117 insertions(+), 4 deletions(-) create mode 100644 luarules/testing/localsAccess.lua diff --git a/luarules/testing/localsAccess.lua b/luarules/testing/localsAccess.lua new file mode 100644 index 00000000000..7857afff8af --- /dev/null +++ b/luarules/testing/localsAccess.lua @@ -0,0 +1,77 @@ +local localsDetectorString = [[ + +local __locals = {} +local __i = 1 +while true do + local name, _ = debug.getlocal(1, __i) + if not name then break end + + if name ~= "__i" and name ~= "__locals" then + table.insert(__locals, name) + end + + __i = __i + 1 +end +return __locals +]] + +local function generateLocalsAccessStr(localsNames) + local content = "\n__localsAccess = {\n" + content = content .. " getters = {\n" + for _, name in ipairs(localsNames) do + content = content .. " " .. name .. " = function() return " .. name .. " end,\n" + end + content = content .. " },\n" + content = content .. " setters = {\n" + for _, name in ipairs(localsNames) do + content = content .. " " .. name .. " = function(__value) " .. name .. " = __value end,\n" + end + content = content .. " },\n" + content = content .. "}\n" + return content +end + +local function generateLocalsAccessMetatable(baseMetatable) + return { + __index = function(t, k) + if t.__localsAccess and t.__localsAccess.getters[k] ~= nil then + return t.__localsAccess.getters[k]() + elseif baseMetatable and baseMetatable.__index ~= nil then + if type(baseMetatable.__index) == "table" then + return baseMetatable.__index[k] + else + return baseMetatable.__index(t, k) + end + else + rawget(t, k) + end + end, + __newindex = function(t, k, v) + if t.__localsAccess and t.__localsAccess.setters[k] ~= nil then + t.__localsAccess.setters[k](v) + elseif baseMetatable and baseMetatable.__newindex ~= nil then + return baseMetatable.__newindex(t, k, v) + else + return rawset(t, k, v) + end + end, + } +end + +--[[ +example usage: + +local function loadFileWithLocals(filename) + local _, localsNames = loadFile(filename, localsDetectorString) + local env, _ = loadFile(filename, generateLocalsAccessStr(localsNames)) + setmetatable(env, generateLocalsAccessMetatable(getmetatable(env))) + + return env +end +]]-- + +return { + localsDetectorString = localsDetectorString, + generateLocalsAccessStr = generateLocalsAccessStr, + generateLocalsAccessMetatable = generateLocalsAccessMetatable, +} diff --git a/luaui/barwidgets.lua b/luaui/barwidgets.lua index 7815f8b8458..d7883c2a178 100644 --- a/luaui/barwidgets.lua +++ b/luaui/barwidgets.lua @@ -460,13 +460,45 @@ end -function widgetHandler:LoadWidget(filename, fromZip) +function widgetHandler:LoadWidget(filename, fromZip, enableLocalsAccess) local basename = Basename(filename) local text = VFS.LoadFile(filename, not (self.allowUserWidgets and allowuserwidgets) and VFS.ZIP or VFS.RAW_FIRST) if text == nil then Spring.Echo('Failed to load: ' .. basename .. ' (missing file: ' .. filename .. ')') return nil end + + if enableLocalsAccess then + -- enableLocalsAccess makes it so local variables within the widget can be accessed as if they were globals (as + -- opposed to not being able to access them at all from outside the widget). This is accomplished by loading the + -- widget with an additional code snippet to list all of the local variables, getting that result, and then + -- loading again with a code snippet that sets up external access to those variables. + localsAccess = localsAccess or VFS.Include('luarules/testing/localsAccess.lua') + + local textWithLocalsDetector = text .. localsAccess.localsDetectorString + + local chunk, err = loadstring(textWithLocalsDetector, filename) + if chunk == nil then + Spring.Echo('Failed to load: ' .. basename .. ' (' .. err .. ')') + return nil + end + + local widget = widgetHandler:NewWidget() + setfenv(chunk, widget) + local success, valOrErr = pcall(chunk) + if not success then + Spring.Echo('Failed to load: ' .. basename .. ' (' .. valOrErr .. ')') + return nil + end + if err == false then + return nil -- widget asked for a silent death + end + + local localsNames = valOrErr + + text = text .. localsAccess.generateLocalsAccessStr(localsNames) + end + local chunk, err = loadstring(text, filename) if chunk == nil then Spring.Echo('Failed to load: ' .. basename .. ' (' .. err .. ')') @@ -484,6 +516,10 @@ function widgetHandler:LoadWidget(filename, fromZip) return nil -- widget asked for a silent death end + if enableLocalsAccess then + setmetatable(widget, localsAccess.generateLocalsAccessMetatable(getmetatable(widget))) + end + -- user widgets may not access widgetHandler -- fixme: remove the or true part if widget.GetInfo and widget:GetInfo().handler then @@ -939,19 +975,19 @@ function widgetHandler:IsWidgetKnown(name) return self.knownWidgets[name] and true or false end -function widgetHandler:EnableWidget(name) +function widgetHandler:EnableWidget(name, enableLocalsAccess) local ki = self.knownWidgets[name] if not ki then Spring.Echo("EnableWidget(), could not find widget: " .. tostring(name)) return false end if not ki.active then - Spring.Echo('Loading: ' .. ki.filename) + Spring.Echo('Loading: ' .. ki.filename .. (enableLocalsAccess and " (with locals)" or "")) local order = widgetHandler.orderList[name] if not order or order <= 0 then self.orderList[name] = 1 end - local w = self:LoadWidget(ki.filename, ki.fromZip) + local w = self:LoadWidget(ki.filename, ki.fromZip, enableLocalsAccess) if not w then return false end From 7f725892dfdf3c545e1a52c6b1423b15b3043fa7 Mon Sep 17 00:00:00 2001 From: Citrine Date: Fri, 12 Jan 2024 12:23:50 -0800 Subject: [PATCH 13/98] test(gui_selfd_icons): load widget with locals This allows us to leave the widget file untouched and still test it. --- .../test_gui_selfd_icons_armasp.lua | 16 ++++++++++++++-- .../test_gui_selfd_icons_armpw.lua | 15 ++++++++++++++- .../test_gui_selfd_icons_armvp.lua | 15 ++++++++++++++- luaui/Widgets/gui_selfd_icons.lua | 4 ++-- 4 files changed, 44 insertions(+), 6 deletions(-) diff --git a/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armasp.lua b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armasp.lua index 12f87be6619..41fbc4b371d 100644 --- a/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armasp.lua +++ b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armasp.lua @@ -1,15 +1,27 @@ +local widgetName = "Self-Destruct Icons" + function setup() Test.clearMap() + + initialWidgetActive = widgetHandler.knownWidgets[widgetName].active + if initialWidgetActive then + widgetHandler:DisableWidget(widgetName) + end + widgetHandler:EnableWidget(widgetName, true) end function cleanup() Test.clearMap() + + widgetHandler:DisableWidget(widgetName) + if initialWidgetActive then + widgetHandler:EnableWidget(widgetName, false) + end end function test() - widget = widgetHandler:FindWidget("Self-Destruct Icons") + widget = widgetHandler:FindWidget(widgetName) assert(widget) - local x, z = Game.mapSizeX / 2, Game.mapSizeZ / 2 local y = Spring.GetGroundHeight(x, z) diff --git a/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armpw.lua b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armpw.lua index ee47e09a76d..99b5ab7b037 100644 --- a/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armpw.lua +++ b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armpw.lua @@ -1,13 +1,26 @@ +local widgetName = "Self-Destruct Icons" + function setup() Test.clearMap() + + initialWidgetActive = widgetHandler.knownWidgets[widgetName].active + if initialWidgetActive then + widgetHandler:DisableWidget(widgetName) + end + widgetHandler:EnableWidget(widgetName, true) end function cleanup() Test.clearMap() + + widgetHandler:DisableWidget(widgetName) + if initialWidgetActive then + widgetHandler:EnableWidget(widgetName, false) + end end function test() - widget = widgetHandler:FindWidget("Self-Destruct Icons") + widget = widgetHandler:FindWidget(widgetName) assert(widget) local x, z = Game.mapSizeX / 2, Game.mapSizeZ / 2 diff --git a/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua index c7c07c71b23..420c40e1c73 100644 --- a/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua +++ b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua @@ -1,13 +1,26 @@ +local widgetName = "Self-Destruct Icons" + function setup() Test.clearMap() + + initialWidgetActive = widgetHandler.knownWidgets[widgetName].active + if initialWidgetActive then + widgetHandler:DisableWidget(widgetName) + end + widgetHandler:EnableWidget(widgetName, true) end function cleanup() Test.clearMap() + + widgetHandler:DisableWidget(widgetName) + if initialWidgetActive then + widgetHandler:EnableWidget(widgetName, false) + end end function test() - widget = widgetHandler:FindWidget("Self-Destruct Icons") + widget = widgetHandler:FindWidget(widgetName) assert(widget) local x, z = Game.mapSizeX / 2, Game.mapSizeZ / 2 diff --git a/luaui/Widgets/gui_selfd_icons.lua b/luaui/Widgets/gui_selfd_icons.lua index c4db755b01f..5d1bd474970 100644 --- a/luaui/Widgets/gui_selfd_icons.lua +++ b/luaui/Widgets/gui_selfd_icons.lua @@ -35,11 +35,11 @@ local font = gl.LoadFont(fontfile, fontfileSize*fontfileScale, fontfileOutlineSi -- {unitID -> unitDefID, ... } -- presence of a unitID key indicates that the unit has an active (counting down) SELFD command -activeSelfD = {} +local activeSelfD = {} -- {unitID -> unitDefID, ... } -- presence of a unitID key indicates that the unit has a queued SELFD command -queuedSelfD = {} +local queuedSelfD = {} local drawLists = {} From af7b54402fb874859e52116345ad80417c6bc92b Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 13 Jan 2024 19:24:18 -0800 Subject: [PATCH 14/98] test(gui_battle_resource_tracker): load widget with locals --- .../test_gui_battle_resource_tracker.lua | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua b/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua index e6779347b1f..34ce7a73fcc 100644 --- a/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua +++ b/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua @@ -1,13 +1,26 @@ +local widgetName = "Battle Resource Tracker" + function setup() Test.clearMap() + + initialWidgetActive = widgetHandler.knownWidgets[widgetName].active + if initialWidgetActive then + widgetHandler:DisableWidget(widgetName) + end + widgetHandler:EnableWidget(widgetName, true) end function cleanup() Test.clearMap() + + widgetHandler:DisableWidget(widgetName) + if initialWidgetActive then + widgetHandler:EnableWidget(widgetName, false) + end end function test() - widget = widgetHandler:FindWidget("Battle Resource Tracker") + widget = widgetHandler:FindWidget(widgetName) assert(widget) widget.spatialHash:clear() From 76d45fc7c2dfa65790c0d57397b097c7a1387229 Mon Sep 17 00:00:00 2001 From: Citrine Date: Fri, 12 Jan 2024 12:25:28 -0800 Subject: [PATCH 15/98] test(balance): restore game speed to 1 in cleanup --- luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua | 2 ++ luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua | 2 ++ 2 files changed, 4 insertions(+) diff --git a/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua b/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua index a1bf05a3e70..2e8017434a2 100644 --- a/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua +++ b/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua @@ -4,6 +4,8 @@ end function cleanup() Test.clearMap() + + Spring.SendCommands("setspeed " .. 1) end function test() diff --git a/luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua b/luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua index 835d26c4ffe..c2297fad8ab 100644 --- a/luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua +++ b/luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua @@ -4,6 +4,8 @@ end function cleanup() Test.clearMap() + + Spring.SendCommands("setspeed " .. 1) end function test() From daa4d765eac176b303efcd39df36dd4f558028e3 Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 13 Jan 2024 15:58:38 -0800 Subject: [PATCH 16/98] refactor(test_framework): store locals as an ordered list This more closely matches how they would be restored with the debug library (when that becomes an option). --- luarules/testing/util.lua | 17 ++++++++++++++--- 1 file changed, 14 insertions(+), 3 deletions(-) diff --git a/luarules/testing/util.lua b/luarules/testing/util.lua index ef7cab4ed3b..b16a2bc4a9e 100644 --- a/luarules/testing/util.lua +++ b/luarules/testing/util.lua @@ -153,7 +153,7 @@ local function getLocals(i) if n == nil then break end - result[n] = v + result[#result + 1] = { n, v } j = j + 1 end return result @@ -167,7 +167,7 @@ local function getUpvalues(fn) if n == nil then break end - result[n] = v + result[#result + 1] = { n, v } j = j + 1 end return result @@ -222,8 +222,19 @@ function deserializeFunctionRun(serializedFn) error(data) end + local localsDictionary = {} + + for i = 1, #data.locals do + local key = data.locals[i][1] + local value = data.locals[i][2] + + if key ~= nil and value ~= nil then + localsDictionary[key] = value + end + end + local callableFunction = function() - local pcallOk, pcallResult = splitFirstElement(pack(pcall(data.fn, data.locals))) + local pcallOk, pcallResult = splitFirstElement(pack(pcall(data.fn, localsDictionary))) return pcallOk, pcallResult end From f85ce0bb9d55c65df49ff4cb74d1d076044b2e2f Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 13 Jan 2024 19:05:07 -0800 Subject: [PATCH 17/98] fix(test_framework): access correct field in callin buffer --- luaui/Widgets/dbg_test_framework.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 95439623054..c805f039750 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -176,7 +176,7 @@ registerCallins(widget, function(name, args) if callinState.buffer[name] == nil then callinState.buffer[name] = {} end - callinState.buffer[name][#(callinState.buffer) + 1] = args + callinState.buffer[name][#(callinState.buffer[name]) + 1] = args end) local function startTests(patterns) From a48fe73d25e4897582242da7854c4f7b8c7ab2ce Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 13 Jan 2024 19:05:26 -0800 Subject: [PATCH 18/98] feat(test_framework): don't start tests if tests are already running --- luaui/Widgets/dbg_test_framework.lua | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index c805f039750..b3f7fdda879 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -184,6 +184,11 @@ local function startTests(patterns) patterns = patterns, })) + if testRunState.runningTests then + log(LOG.WARNING, "Tests are already running!") + return + end + resetState() log(LOG.NOTICE, "=====FINDING TESTS=====") From 74d1ff3d09364c2489c6f3515ae61499bee085ee Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 13 Jan 2024 19:07:20 -0800 Subject: [PATCH 19/98] fix(test_framework): clear callin buffer between tests The previous behavior was between coroutine resumes, which led to callins occasionally being lost. --- luaui/Widgets/dbg_test_framework.lua | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index b3f7fdda879..343152553c2 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -147,7 +147,6 @@ local function resetCallinState() log(LOG.DEBUG, "[resetCallinState]") callinState = { buffer = {}, - current = {}, } end @@ -362,7 +361,7 @@ Test = { waitUntilCallin = function(name, predicate, timeout) Test.waitUntil( function() - for _, args in ipairs(callinState.current[name]) do + for _, args in ipairs(callinState.buffer[name] or {}) do if predicate == nil or predicate(unpack(args)) then return true end @@ -717,11 +716,6 @@ local function step() end end - -- in between resumes, shift the callin buffer (so we get all calls that happened since we last resumed; this - -- includes callins that happened during a wait or synced call) - callinState.current = callinState.buffer - callinState.buffer = {} - -- resume the test if coroutine.status(activeTestState.coroutine) == "suspended" then local coroutineOk, coroutineArgs From 85d5ec89c83cf7055859e084aafe355d0ab8ef4c Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 13 Jan 2024 19:07:33 -0800 Subject: [PATCH 20/98] fix(test_framework): adjust error stack distance to match usage Previously, line information was lost because the distance was set too high. --- luaui/Widgets/dbg_test_framework.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 343152553c2..623e2dc30ea 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -335,7 +335,7 @@ Test = { resetResumeState() if not resumeOk then - error(resumeResult, 3 + (errorOffset or 0)) + error(resumeResult, 2 + (errorOffset or 0)) end end, waitFrames = function(frames) From ab667b181afcdf4716f80f26ac23fbb756552556 Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 13 Jan 2024 19:08:22 -0800 Subject: [PATCH 21/98] test(cmd_stop_selfd): wait a bit longer in between checks Previously, sometimes the widget wouldn't have enough time to act before the test checked results. --- .../Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua b/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua index 00574d1d749..d71b39d8eec 100644 --- a/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua +++ b/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua @@ -20,22 +20,22 @@ function test() -- issue selfd and then issue stop Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, 0) - Test.waitFrames(1) + Test.waitFrames(3) assert(Spring.GetUnitSelfDTime(unitID) > 0) Spring.GiveOrderToUnit(unitID, CMD.STOP, {}, 0) - Test.waitFrames(1) + Test.waitFrames(3) assert(Spring.GetUnitSelfDTime(unitID) == 0) assert(#(Spring.GetCommandQueue(unitID, 1)) == 0) -- issue {move, selfd}, then issue stop Spring.GiveOrderToUnit(unitID, CMD.MOVE, { 1, 1, 1 }, 0) Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, { "shift" }) - Test.waitFrames(1) + Test.waitFrames(3) assert(Spring.GetUnitSelfDTime(unitID) == 0) Spring.GiveOrderToUnit(unitID, CMD.STOP, {}, 0) - Test.waitFrames(1) + Test.waitFrames(3) assert(Spring.GetUnitSelfDTime(unitID) == 0) assert(#(Spring.GetCommandQueue(unitID, 1)) == 0) end From 448caf13ff66c26ea022107ca10fdb38f4a37445 Mon Sep 17 00:00:00 2001 From: Citrine Date: Sun, 14 Jan 2024 00:00:22 -0800 Subject: [PATCH 22/98] test(test_framework): remove clutter log messages --- luaui/Widgets/dbg_test_framework.lua | 28 ---------------------------- 1 file changed, 28 deletions(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 623e2dc30ea..1c200516447 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -541,10 +541,8 @@ local function loadTestFromFile(filename) end local function handleReturn() - log(LOG.DEBUG, "[handleReturn]") if returnState.success == nil and returnState.waitingForReturnId == nil then -- no return to handle, so just continue - log(LOG.DEBUG, "[handleReturn] nil success -> continue") return { status = "continue", } @@ -562,7 +560,6 @@ local function handleReturn() end if returnState.waitingForReturnId then - log(LOG.DEBUG, "[handleReturn] waiting for return -> wait") return { status = "wait" } @@ -590,9 +587,7 @@ local function handleReturn() end local function handleWait() - log(LOG.DEBUG, "[handleWait]") if resumeState.predicate == nil then - log(LOG.DEBUG, "[handleWait] nil predicate -> continue") return { status = "continue", } @@ -610,7 +605,6 @@ local function handleWait() local success, returnOrError = pcall(resumeState.predicate) if success then -- predicate ran successfully - log(LOG.DEBUG, "[handleWait] predicate success") if returnOrError then -- succeeded, we can resume log(LOG.DEBUG, "[handleWait] predicate success + true -> continue") @@ -620,7 +614,6 @@ local function handleWait() } else -- failed, wait and try again next frame - log(LOG.DEBUG, "[handleWait] predicate success + false -> wait") return { status = "wait" } @@ -633,11 +626,6 @@ local function handleWait() error = returnOrError } end - - log(LOG.DEBUG, "[handleWait] fallthrough -> continue") - return { - status = "continue", - } end local function getGameTime() @@ -657,16 +645,6 @@ local function step() return end - log(LOG.DEBUG, "FRAME " .. Spring.GetGameFrame() .. " | " .. getRunTestsTime() .. "ms | " .. table.toString({ - resumeState = { - predicate = resumeState.predicate ~= nil and "present" or "nil", - timeoutExpireFrame = resumeState.timeoutExpireFrame, - }, - returnState = returnState, - coroutineStatus = activeTestState.coroutine and coroutine.status(activeTestState.coroutine) or "nil", - callinState = callinState, - })) - --*result = { -- status = -- | "continue" (ok to continue to other checks and resuming test) @@ -676,18 +654,12 @@ local function step() -- returnValue = (status="continue" only, optional) --} local returnResult = handleReturn() - log(LOG.DEBUG, "[returnResult] " .. table.toString({ - returnResult = returnResult, - })) if returnResult.status == "wait" then return end local waitResult = handleWait() - log(LOG.DEBUG, "[waitResult] " .. table.toString({ - waitResult = waitResult, - })) if waitResult.status == "wait" then return From 46e5a3308f391d87d1bc9a5f7106190f90d9a68e Mon Sep 17 00:00:00 2001 From: Citrine Date: Sun, 14 Jan 2024 09:27:39 -0800 Subject: [PATCH 23/98] test(test_framework): add some more debug log messages --- luaui/Widgets/dbg_test_framework.lua | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 1c200516447..22d5cdfbf4e 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -339,6 +339,7 @@ Test = { end end, waitFrames = function(frames) + log(LOG.DEBUG, "[waitFrames] " .. frames) local startFrame = Spring.GetGameFrame() Test.waitUntil( function() @@ -347,8 +348,10 @@ Test = { frames + 5, 1 ) + log(LOG.DEBUG, "[waitFrames.done]") end, waitTime = function(milliseconds, timeout) + log(LOG.DEBUG, "[waitTime] " .. milliseconds) local startTimer = Spring.GetTimer() Test.waitUntil( function() @@ -357,8 +360,10 @@ Test = { timeout or (milliseconds * 30 / 1000 + 5), 1 ) + log(LOG.DEBUG, "[waitTime.done]") end, waitUntilCallin = function(name, predicate, timeout) + log(LOG.DEBUG, "[waitUntilCallin] " .. name) Test.waitUntil( function() for _, args in ipairs(callinState.buffer[name] or {}) do @@ -371,6 +376,7 @@ Test = { timeout, 1 ) + log(LOG.DEBUG, "[waitUntilCallin.done]") end, spy = function(...) local spyCtrl = spy(...) @@ -413,33 +419,50 @@ local function runTestInternal() local setupOk, setupResult if setup ~= nil then + log(LOG.DEBUG, "[runTestInternal.setup]") setupOk, setupResult = yieldable_pcall(setup) + log(LOG.DEBUG, "[runTestInternal.setup.done] " .. table.toString({ + setupOk, setupResult + })) else + log(LOG.DEBUG, "[runTestInternal.setup.skipped]") setupOk = true end local testOk, testResult if setupOk then + log(LOG.DEBUG, "[runTestInternal.test]") testOk, testResult = yieldable_pcall(test) + log(LOG.DEBUG, "[runTestInternal.test.done] " .. table.toString({ + testOk, testResult + })) end -- always try to run cleanup local cleanupOk, cleanupResult if cleanup ~= nil then + log(LOG.DEBUG, "[runTestInternal.cleanup]") cleanupOk, cleanupResult = yieldable_pcall(cleanup) + log(LOG.DEBUG, "[runTestInternal.cleanup.done] " .. table.toString({ + cleanupOk, cleanupResult + })) else + log(LOG.DEBUG, "[runTestInternal.cleanup.skipped]") cleanupOk = true end if not cleanupOk then + log(LOG.DEBUG, "[runTestInternal.cleanup.error]") error(cleanupResult, 2) end if not setupOk then + log(LOG.DEBUG, "[runTestInternal.setup.error]") error(setupResult, 2) end if not testOk then + log(LOG.DEBUG, "[runTestInternal.test.error]") error(testResult, 2) end end @@ -714,6 +737,10 @@ local function step() ) local result, error = coroutine.resume(activeTestState.coroutine, coroutineOk, coroutineArgs) + log(LOG.DEBUG, "Unresuming test: " .. table.toString({ + result = result, + error = error, + })) if not result then -- test fail finishTest({ From 3accbcf8134579c81e72a99c57d299190753c9a4 Mon Sep 17 00:00:00 2001 From: Citrine Date: Sun, 14 Jan 2024 09:52:53 -0800 Subject: [PATCH 24/98] feat(test_framework): always show all results --- luaui/Widgets/dbg_test_framework.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 22d5cdfbf4e..a47399ea13d 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -18,7 +18,7 @@ local LOG_LEVEL = LOG.INFO local RETURN_TIMEOUT = 30 local DEFAULT_WAIT_TIMEOUT = 5 * 30 -local SHOW_ALL_RESULTS = false +local SHOW_ALL_RESULTS = true -- utils -- ===== From 9b28df1d207ececde5f446e133c5d2e8dc62f51b Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 09:20:13 -0800 Subject: [PATCH 25/98] test(gui_battle_resource_tracker): fail earlier if widget is not present --- .../test_gui_battle_resource_tracker.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua b/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua index 34ce7a73fcc..1b23be6c807 100644 --- a/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua +++ b/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua @@ -1,6 +1,8 @@ local widgetName = "Battle Resource Tracker" function setup() + assert(widgetHandler.knownWidgets[widgetName] ~= nil) + Test.clearMap() initialWidgetActive = widgetHandler.knownWidgets[widgetName].active From 3755348f49e7609b8fb050dbaad5d2e15ba8ab67 Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 09:11:56 -0800 Subject: [PATCH 26/98] docs(test_framework): use better widget/gadget names/descriptions --- luarules/gadgets/dbg_test_framework_proxy.lua | 4 ++-- luaui/Widgets/dbg_test_framework.lua | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/luarules/gadgets/dbg_test_framework_proxy.lua b/luarules/gadgets/dbg_test_framework_proxy.lua index da236f51ff1..0673ff9e5b7 100644 --- a/luarules/gadgets/dbg_test_framework_proxy.lua +++ b/luarules/gadgets/dbg_test_framework_proxy.lua @@ -1,7 +1,7 @@ function gadget:GetInfo() return { - name = "Test Framework Proxy", - desc = "Proxy for Synced commands", + name = "Test Framework Synced Proxy", + desc = "Proxy for synced commands and code", date = "2023", license = "GNU GPL, v2 or later", version = 0, diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index a47399ea13d..69c0cbab41c 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -1,7 +1,7 @@ function widget:GetInfo() return { - name = "Test Framework Widget", - desc = "Test framework helper widget", + name = "Test Framework Runner", + desc = "Run tests with: /runtests ...", date = "2023", license = "GNU GPL, v2 or later", version = 0, From 629195411260d12282463c71873cd92cfcd81a9e Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 09:12:56 -0800 Subject: [PATCH 27/98] feat(test_framework): only step during update if we are pre-game Otherwise we do a lot of steps very quickly. In headless mode this significantly impacts how the game runs. --- luaui/Widgets/dbg_test_framework.lua | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 69c0cbab41c..fcde2804b67 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -764,7 +764,9 @@ function widget:GameFrame(frame) end function widget:Update(dt) - step() + if Spring.GetGameFrame() <= 0 then + step() + end end function widget:Initialize() From 1643bbd940c2199214c2cd1112614927418836d3 Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 09:17:28 -0800 Subject: [PATCH 28/98] fix(test_framework): clear callin buffer after each waitUntilCallin This is a tradeoff - this makes it so we can wait in cases where the same event happens several times in a test, but it also makes it so we can't wait for several of the same type of event sequentially (such as waiting for several UnitCreated or UnitCommand events after causing several). --- luaui/Widgets/dbg_test_framework.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index fcde2804b67..9616cac62cb 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -376,6 +376,7 @@ Test = { timeout, 1 ) + callinState.buffer[name] = {} log(LOG.DEBUG, "[waitUntilCallin.done]") end, spy = function(...) From 419cef4c7719945eef7c16a0b1bc4aeb716804b8 Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 09:18:15 -0800 Subject: [PATCH 29/98] test(test_framework): shorten some very long debug log messages --- luarules/gadgets/dbg_test_framework_proxy.lua | 4 +--- luaui/Widgets/dbg_test_framework.lua | 8 ++------ 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/luarules/gadgets/dbg_test_framework_proxy.lua b/luarules/gadgets/dbg_test_framework_proxy.lua index 0673ff9e5b7..73e8b7c8112 100644 --- a/luarules/gadgets/dbg_test_framework_proxy.lua +++ b/luarules/gadgets/dbg_test_framework_proxy.lua @@ -70,9 +70,7 @@ local function processCall(msg, env) end local function processCode(msg) - log(LOG.DEBUG, "[processCode] " .. table.toString({ - msg = msg, - })) + log(LOG.DEBUG, "[processCode]") local fn, returnId = deserializeFunctionRun(msg) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 9616cac62cb..7a0e55b82a6 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -261,9 +261,7 @@ local function createNestedProxy(prefix, path) timeoutExpireFrame = Spring.GetGameFrame() + RETURN_TIMEOUT, } - log(LOG.DEBUG, "[createNestedProxy." .. prefix .. ".send] " .. table.toString({ - serializedFn = serializedFn, - })) + log(LOG.DEBUG, "[createNestedProxy." .. prefix .. ".send]") Spring.SendLuaRulesMsg(prefix .. serializedFn) local resumeOk, resumeResult = coroutine.yield() @@ -295,9 +293,7 @@ SyncedRun = function(fn) timeoutExpireFrame = Spring.GetGameFrame() + RETURN_TIMEOUT, } - log(LOG.DEBUG, "[SyncedRun.send] " .. table.toString({ - serializedFn = serializedFn, - })) + log(LOG.DEBUG, "[SyncedRun.send]") Spring.SendLuaRulesMsg(PROXY_RUN_PREFIX .. serializedFn) local resumeOk, resumeResult = coroutine.yield() From 635110304903204b0f0152f9f47c793f5be1d35c Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 17:16:42 -0800 Subject: [PATCH 30/98] feat(test_framework): remove widget if not in dev mode --- luaui/Widgets/dbg_test_framework.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 7a0e55b82a6..d93ab9f4f54 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -767,6 +767,10 @@ function widget:Update(dt) end function widget:Initialize() + if not Spring.Utilities.IsDevMode() then + widgetHandler:RemoveWidget(self) + end + widgetHandler.actionHandler:AddAction( self, "runtests", From 0c0aa7ef827b0ec264fd15d154fd33d3356a7981 Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 17:23:06 -0800 Subject: [PATCH 31/98] refactor(test_framework): rearrange and use timer functions --- luaui/Widgets/dbg_test_framework.lua | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index d93ab9f4f54..e4482c9d38b 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -97,6 +97,18 @@ local gameTimer local runTestsTimer local testTimer +local function getGameTime() + return Spring.DiffTimers(Spring.GetTimer(), gameTimer, true) +end + +local function getRunTestsTime() + return Spring.DiffTimers(Spring.GetTimer(), runTestsTimer, true) +end + +local function getTestTime() + return Spring.DiffTimers(Spring.GetTimer(), testTimer, true) +end + local testRunState local activeTestState local resumeState @@ -221,7 +233,7 @@ local function finishTest(result) result.frames = Spring.GetGameFrame() - activeTestState.startFrame end - result.milliseconds = Spring.DiffTimers(Spring.GetTimer(), testTimer, true) + result.milliseconds = getTestTime() log(LOG.NOTICE, formatTestResult(result)) @@ -648,18 +660,6 @@ local function handleWait() end end -local function getGameTime() - return Spring.DiffTimers(Spring.GetTimer(), gameTimer, true) -end - -local function getRunTestsTime() - return Spring.DiffTimers(Spring.GetTimer(), runTestsTimer, true) -end - -local function getTestTime() - return Spring.DiffTimers(Spring.GetTimer(), testTimer, true) -end - local function step() if not testRunState.runningTests then return From c01279f66aa9b51448f628015a699173736ef979 Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 17:35:39 -0800 Subject: [PATCH 32/98] fix(test_framework): add missing RemoveAction --- luaui/Widgets/dbg_test_framework.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index e4482c9d38b..ae629911c4f 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -786,4 +786,5 @@ end function widget:Shutdown() widgetHandler.actionHandler:RemoveAction("runtests", "t") + widgetHandler.actionHandler:RemoveAction("runtestsheadless", "t") end From bfc4b30fb6ce2946566235bcfbff04fb0ff2da0d Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 17:35:56 -0800 Subject: [PATCH 33/98] fix(test_framework): add nil checks on timer functions --- luaui/Widgets/dbg_test_framework.lua | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index ae629911c4f..2783f180e32 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -98,15 +98,21 @@ local runTestsTimer local testTimer local function getGameTime() - return Spring.DiffTimers(Spring.GetTimer(), gameTimer, true) + if gameTimer ~= nil then + return Spring.DiffTimers(Spring.GetTimer(), gameTimer, true) + end end local function getRunTestsTime() - return Spring.DiffTimers(Spring.GetTimer(), runTestsTimer, true) + if runTestsTimer ~= nil then + return Spring.DiffTimers(Spring.GetTimer(), runTestsTimer, true) + end end local function getTestTime() - return Spring.DiffTimers(Spring.GetTimer(), testTimer, true) + if testTimer ~= nil then + return Spring.DiffTimers(Spring.GetTimer(), testTimer, true) + end end local testRunState From 50364b8b313e81d217d1a8e2409697fd6a31a69b Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 17:52:35 -0800 Subject: [PATCH 34/98] feat(test_framework): add Test.clearCallinBuffer() This should let tests be more specific about the events they wait for with Test.waitUntilCallin(). --- luaui/Widgets/dbg_test_framework.lua | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 2783f180e32..0eaa8b572d3 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -399,6 +399,13 @@ Test = { return spyCtrl end, clearMap = SyncedExtra.clearMap, + clearCallinBuffer = function(name) + if name ~= nil then + callinState.buffer[name] = {} + else + callinState.buffer = {} + end + end, } function widget:RecvLuaMsg(msg) From d2682ace5434327aa7cf3b39536521049500e615 Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 17:53:15 -0800 Subject: [PATCH 35/98] feat(test_framework): add Test.waitUntilCallinArgs() This is a more concise version of Test.waitUntilCallin() --- .../Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua | 8 ++++---- luaui/Widgets/dbg_test_framework.lua | 11 +++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua b/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua index d71b39d8eec..c0f93b77486 100644 --- a/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua +++ b/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua @@ -20,22 +20,22 @@ function test() -- issue selfd and then issue stop Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, 0) - Test.waitFrames(3) + Test.waitUntilCallinArgs("UnitCommand", {nil, nil, nil, CMD.SELFD, nil, nil, nil}) assert(Spring.GetUnitSelfDTime(unitID) > 0) Spring.GiveOrderToUnit(unitID, CMD.STOP, {}, 0) - Test.waitFrames(3) + Test.waitUntilCallinArgs("UnitCommand", {nil, nil, nil, CMD.SELFD, nil, nil, nil}) assert(Spring.GetUnitSelfDTime(unitID) == 0) assert(#(Spring.GetCommandQueue(unitID, 1)) == 0) -- issue {move, selfd}, then issue stop Spring.GiveOrderToUnit(unitID, CMD.MOVE, { 1, 1, 1 }, 0) Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, { "shift" }) - Test.waitFrames(3) + Test.waitUntilCallinArgs("UnitCommand", {nil, nil, nil, CMD.SELFD, nil, nil, nil}) assert(Spring.GetUnitSelfDTime(unitID) == 0) Spring.GiveOrderToUnit(unitID, CMD.STOP, {}, 0) - Test.waitFrames(3) + Test.waitUntilCallinArgs("UnitCommand", {nil, nil, nil, CMD.STOP, nil, nil, nil}) assert(Spring.GetUnitSelfDTime(unitID) == 0) assert(#(Spring.GetCommandQueue(unitID, 1)) == 0) end diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 0eaa8b572d3..2b0e82cd4be 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -393,6 +393,17 @@ Test = { callinState.buffer[name] = {} log(LOG.DEBUG, "[waitUntilCallin.done]") end, + waitUntilCallinArgs = function(name, expectedArgs) + Test.waitUntilCallin(name, function(...) + local currentArgs = { ... } + for k, v in pairs(expectedArgs) do + if currentArgs[k] == nil or currentArgs[k] ~= v then + return false + end + return true + end + end) + end, spy = function(...) local spyCtrl = spy(...) spyControls[#spyControls + 1] = spyCtrl From 71f96987c1153811872dc7fd6ffb01dc3e748a74 Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 09:19:44 -0800 Subject: [PATCH 36/98] feat(test_framework): add support for running in headless mode This can be used with `/runtestsheadless`. It removes color output, schedules tests to run at game start, logs results to a file, and quits the game when all tests have finished running. --- luarules/testing/util.lua | 28 ++++++++++++------- luaui/Widgets/dbg_test_framework.lua | 42 ++++++++++++++++++++++++++-- 2 files changed, 58 insertions(+), 12 deletions(-) diff --git a/luarules/testing/util.lua b/luarules/testing/util.lua index b16a2bc4a9e..0d9e73d804d 100644 --- a/luarules/testing/util.lua +++ b/luarules/testing/util.lua @@ -49,18 +49,26 @@ function rgbReset() return rgbToColorCode(0.85, 0.85, 0.85) end -function formatTestResult(testResult) +function formatTestResult(testResult, noColor) local resultColor - if testResult.result == TEST_RESULT.PASS then - resultColor = rgbToColorCode(0, 1, 0) - elseif testResult.result == TEST_RESULT.FAIL then - resultColor = rgbToColorCode(1, 0, 0) - elseif testResult.result == TEST_RESULT.SKIP then - resultColor = rgbToColorCode(0.8, 0.8, 0) - elseif testResult.result == TEST_RESULT.ERROR then - resultColor = rgbToColorCode(0.8, 0, 0.8) + local resetColor + if noColor then + resultColor = "" + resetColor = "" + else + if testResult.result == TEST_RESULT.PASS then + resultColor = rgbToColorCode(0, 1, 0) + elseif testResult.result == TEST_RESULT.FAIL then + resultColor = rgbToColorCode(1, 0, 0) + elseif testResult.result == TEST_RESULT.SKIP then + resultColor = rgbToColorCode(0.8, 0.8, 0) + elseif testResult.result == TEST_RESULT.ERROR then + resultColor = rgbToColorCode(0.8, 0, 0.8) + end + resetColor = rgbReset() end - local resultStr = resultColor .. TEST_RESULT_ID_TO_STRING[testResult.result] .. rgbReset() + + local resultStr = resultColor .. TEST_RESULT_ID_TO_STRING[testResult.result] .. resetColor local s = string.format("%s: %s", resultStr, testResult.label) if testResult.frames ~= nil then s = s .. " [" .. testResult.frames .. " frames]" diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 2b0e82cd4be..9200064e2ed 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -20,6 +20,12 @@ local DEFAULT_WAIT_TIMEOUT = 5 * 30 local SHOW_ALL_RESULTS = true +local noColorOutput = false +local quitWhenDone = false +local gameStartTestPatterns = nil +local logFilePath = nil +local logFile = nil + -- utils -- ===== local function log(level, str, ...) @@ -31,6 +37,9 @@ local function log(level, str, ...) LOG.NOTICE, str ) + if logFile ~= nil then + logFile:write(str .. "\n") + end end -- main code @@ -89,7 +98,7 @@ end local function displayTestResults(results) log(LOG.INFO, "=====TEST RESULTS=====") for _, testResult in ipairs(results) do - log(LOG.INFO, formatTestResult(testResult)) + log(LOG.INFO, formatTestResult(testResult, noColorOutput)) end end @@ -206,6 +215,10 @@ local function startTests(patterns) return end + if logFilePath ~= nil then + logFile = io.open(logFilePath, "w") + end + resetState() log(LOG.NOTICE, "=====FINDING TESTS=====") @@ -241,7 +254,7 @@ local function finishTest(result) result.milliseconds = getTestTime() - log(LOG.NOTICE, formatTestResult(result)) + log(LOG.NOTICE, formatTestResult(result, noColorOutput)) testRunState.results[#(testRunState.results) + 1] = result @@ -259,6 +272,14 @@ local function finishTest(result) if SHOW_ALL_RESULTS then displayTestResults(testRunState.results) end + + if logFile ~= nil and io.type(logFile) == "file" then + io.close(logFile) + end + + if quitWhenDone then + Spring.SendCommands("quitforce") + end end end @@ -781,6 +802,11 @@ local function step() end function widget:GameFrame(frame) + if gameStartTestPatterns ~= nil and frame >= 0 then + startTests(gameStartTestPatterns) + gameStartTestPatterns = nil + end + step() end @@ -804,6 +830,18 @@ function widget:Initialize() nil, "t" ) + widgetHandler.actionHandler:AddAction( + self, + "runtestsheadless", + function(cmd, optLine, optWords, data, isRepeat, release, actions) + noColorOutput = true + quitWhenDone = true + gameStartTestPatterns = splitPhrases(optLine) + logFilePath = "testlog.txt" + end, + nil, + "t" + ) gameTimer = Spring.GetTimer() end From 8aa4a8c725b848a2a48f2434f541fc257776cad2 Mon Sep 17 00:00:00 2001 From: Citrine Date: Tue, 16 Jan 2024 17:47:32 -0800 Subject: [PATCH 37/98] feat(test_framework): add watchdog to handle runner crash in headless --- luaui/Widgets/dbg_test_framework.lua | 2 ++ luaui/Widgets/dbg_test_framework_watchdog.lua | 26 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 luaui/Widgets/dbg_test_framework_watchdog.lua diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 9200064e2ed..644f07ffa64 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -821,6 +821,8 @@ function widget:Initialize() widgetHandler:RemoveWidget(self) end + widgetHandler:EnableWidget("Test Framework Watchdog") + widgetHandler.actionHandler:AddAction( self, "runtests", diff --git a/luaui/Widgets/dbg_test_framework_watchdog.lua b/luaui/Widgets/dbg_test_framework_watchdog.lua new file mode 100644 index 00000000000..b1be52af81d --- /dev/null +++ b/luaui/Widgets/dbg_test_framework_watchdog.lua @@ -0,0 +1,26 @@ +function widget:GetInfo() + return { + name = "Test Framework Watchdog", + desc = "Quits game if test runner exits", + date = "2024", + license = "GNU GPL, v2 or later", + version = 0, + layer = 9999, + enabled = false, + handler = true, + } +end + +local CHECK_PERIOD = 1 +local t = 0 +function widget:Update(dt) + t = t + dt + if t > CHECK_PERIOD then + t = 0 + if widgetHandler:FindWidget("Test Framework Runner") == nil then + Spring.Log(widget:GetInfo().name, LOG.WARNING, "Test runner crashed, exiting game") + widgetHandler:DisableWidget(widget.GetInfo().name) + Spring.SendCommands("quitforce") + end + end +end From c6fa4b8b49a26f437a82cfec38ef7d613d5aab36 Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 20 Jan 2024 18:33:22 -0800 Subject: [PATCH 38/98] fix(test_framework): use relativePath, not just filename for matching --- luaui/Widgets/dbg_test_framework.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 644f07ffa64..8defbac2936 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -65,7 +65,7 @@ local function findTestFiles(directory, patterns, rootDirectory, result) for _, filename in ipairs(VFS.DirList(directory, "*", VFS.RAW_FIRST)) do local relativePath = string.sub(filename, string.len(rootDirectory) + 1) - local withoutExtension = removeFileExtension(filename) + local withoutExtension = removeFileExtension(relativePath) if patterns == nil or #patterns == 0 or matchesPatterns(withoutExtension, patterns) then log(LOG.INFO, "Found test file: " .. relativePath) result[#result + 1] = { From 793913d2c5a73df6928598578c2ee69c93465ae3 Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 20 Jan 2024 18:34:23 -0800 Subject: [PATCH 39/98] refactor(test_framework): file -> filename --- luaui/Widgets/dbg_test_framework.lua | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 8defbac2936..68b7311569e 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -147,7 +147,7 @@ local function resetActiveTestState() coroutine = nil, environment = nil, startFrame = nil, - file = nil, + filename = nil, label = nil, } end @@ -733,9 +733,9 @@ local function step() -- is there a test set up? if not, create one if activeTestState.coroutine == nil then activeTestState.label = testRunState.files[testRunState.index].label - activeTestState.file = testRunState.files[testRunState.index] + activeTestState.filename = testRunState.files[testRunState.index].filename - local success, envOrError = loadTestFromFile(activeTestState.file.filename) + local success, envOrError = loadTestFromFile(activeTestState.filename) if success then log(LOG.DEBUG, "Initializing test: " .. activeTestState.label) From 742201322412048e2da5a8c4ffefb1aeabfe9706 Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 20 Jan 2024 18:34:53 -0800 Subject: [PATCH 40/98] feat(test_framework): add index and filename to test result data --- luaui/Widgets/dbg_test_framework.lua | 2 ++ 1 file changed, 2 insertions(+) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 68b7311569e..7b053be3944 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -247,7 +247,9 @@ local function finishTest(result) control.remove() end + result.index = result.index or testRunState.index result.label = result.label or activeTestState.label + result.filename = result.filename or activeTestState.filename if activeTestState and activeTestState.startFrame and result.frames == nil then result.frames = Spring.GetGameFrame() - activeTestState.startFrame end From 092676ab03fc13b36909c589da1706f6fb6e54c0 Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 20 Jan 2024 18:35:58 -0800 Subject: [PATCH 41/98] feat(test_framework): replace log file with real test results file It writes in mocha JSON format, for compatibility with other tools. --- luarules/testing/mochaJsonReporter.lua | 81 ++++++++++++++++++++++++++ luaui/Widgets/dbg_test_framework.lua | 51 ++++++++++++---- 2 files changed, 120 insertions(+), 12 deletions(-) create mode 100644 luarules/testing/mochaJsonReporter.lua diff --git a/luarules/testing/mochaJsonReporter.lua b/luarules/testing/mochaJsonReporter.lua new file mode 100644 index 00000000000..56b4570fd4e --- /dev/null +++ b/luarules/testing/mochaJsonReporter.lua @@ -0,0 +1,81 @@ +local function formatTimestamp(ts) + return os.date("%Y-%m-%dT%H:%M:%S", ts) +end + +local MochaJSONReporter = {} + +function MochaJSONReporter:new() + local obj = { + totalTests = 0, + totalPasses = 0, + totalFailures = 0, + startTime = nil, + endTime = nil, + duration = nil, + tests = {} + } + setmetatable(obj, self) + self.__index = self + return obj +end + +function MochaJSONReporter:startTests() + self.startTime = os.time() +end + +function MochaJSONReporter:endTests(duration) + self.endTime = os.time() + self.duration = duration +end + +function MochaJSONReporter:testResult(label, filePath, success, duration, errorMessage) + local result = { + title = label, + fullTitle = label, + file = filePath, + duration = duration, + } + if success then + self.totalPasses = self.totalPasses + 1 + result.err = {} + else + self.totalFailures = self.totalFailures + 1 + if errorMessage ~= nil then + result.err = { + message = errorMessage, + stack = errorMessage + } + else + result.err = { + message = "", + } + end + end + + self.totalTests = self.totalTests + 1 + self.tests[#(self.tests) + 1] = result +end + +function MochaJSONReporter:report(filePath) + local output = { + ["stats"] = { + ["suites"] = 1, + ["tests"] = self.totalTests, + ["passes"] = self.totalPasses, + ["pending"] = 0, + ["failures"] = self.totalFailures, + ["start"] = formatTimestamp(self.startTime), + ["end"] = formatTimestamp(self.endTime), + ["duration"] = self.duration + }, + ["tests"] = self.tests + } + + local encoded = Json.encode(output) + + local file = io.open(filePath, "w") + file:write(encoded) + file:close() +end + +return MochaJSONReporter diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 7b053be3944..8790d5797ef 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -12,6 +12,7 @@ function widget:GetInfo() end VFS.Include('luarules/testing/util.lua') +local MochaJSONReporter = VFS.Include('luarules/testing/mochaJsonReporter.lua') local LOG_LEVEL = LOG.INFO @@ -23,8 +24,8 @@ local SHOW_ALL_RESULTS = true local noColorOutput = false local quitWhenDone = false local gameStartTestPatterns = nil -local logFilePath = nil -local logFile = nil +local testResultsFilePath = nil +local testReporter = nil -- utils -- ===== @@ -37,9 +38,38 @@ local function log(level, str, ...) LOG.NOTICE, str ) - if logFile ~= nil then - logFile:write(str .. "\n") +end + +local function logStartTests() + if testResultsFilePath == nil then + return + end + + testReporter = MochaJSONReporter:new() + testReporter:startTests() +end + +local function logEndTests(duration) + if testResultsFilePath == nil then + return end + testReporter:endTests(duration) + + testReporter:report(testResultsFilePath) +end + +local function logTestResult(testResult) + if testResultsFilePath == nil then + return + end + + testReporter:testResult( + testResult.label, + testResult.filename, + (testResult.result == TEST_RESULT.PASS), + testResult.milliseconds, + testResult.error + ) end -- main code @@ -215,9 +245,7 @@ local function startTests(patterns) return end - if logFilePath ~= nil then - logFile = io.open(logFilePath, "w") - end + logStartTests() resetState() @@ -253,11 +281,12 @@ local function finishTest(result) if activeTestState and activeTestState.startFrame and result.frames == nil then result.frames = Spring.GetGameFrame() - activeTestState.startFrame end - result.milliseconds = getTestTime() log(LOG.NOTICE, formatTestResult(result, noColorOutput)) + logTestResult(result) + testRunState.results[#(testRunState.results) + 1] = result resetActiveTestState() @@ -275,9 +304,7 @@ local function finishTest(result) displayTestResults(testRunState.results) end - if logFile ~= nil and io.type(logFile) == "file" then - io.close(logFile) - end + logEndTests(getRunTestsTime()) if quitWhenDone then Spring.SendCommands("quitforce") @@ -841,7 +868,7 @@ function widget:Initialize() noColorOutput = true quitWhenDone = true gameStartTestPatterns = splitPhrases(optLine) - logFilePath = "testlog.txt" + testResultsFilePath = "testlog/results.json" end, nil, "t" From f787fa9f896453b3674f74d55b29336c59ec1805 Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 20 Jan 2024 18:36:40 -0800 Subject: [PATCH 42/98] feat(test_framework): add docker setup for headless testing --- tools/headless_testing/.gitattributes | 1 + tools/headless_testing/.gitignore | 1 + tools/headless_testing/Dockerfile | 18 +++++++++++++++ tools/headless_testing/docker-compose.yml | 15 ++++++++++++ tools/headless_testing/download-engine.sh | 11 +++++++++ tools/headless_testing/start.sh | 4 ++++ tools/headless_testing/startscript.txt | 28 +++++++++++++++++++++++ 7 files changed, 78 insertions(+) create mode 100644 tools/headless_testing/.gitattributes create mode 100644 tools/headless_testing/.gitignore create mode 100644 tools/headless_testing/Dockerfile create mode 100644 tools/headless_testing/docker-compose.yml create mode 100644 tools/headless_testing/download-engine.sh create mode 100644 tools/headless_testing/start.sh create mode 100644 tools/headless_testing/startscript.txt diff --git a/tools/headless_testing/.gitattributes b/tools/headless_testing/.gitattributes new file mode 100644 index 00000000000..de99ed83175 --- /dev/null +++ b/tools/headless_testing/.gitattributes @@ -0,0 +1 @@ +* text eol=lf \ No newline at end of file diff --git a/tools/headless_testing/.gitignore b/tools/headless_testing/.gitignore new file mode 100644 index 00000000000..89a4f780e3d --- /dev/null +++ b/tools/headless_testing/.gitignore @@ -0,0 +1 @@ +testlog \ No newline at end of file diff --git a/tools/headless_testing/Dockerfile b/tools/headless_testing/Dockerfile new file mode 100644 index 00000000000..7b7cf2b668d --- /dev/null +++ b/tools/headless_testing/Dockerfile @@ -0,0 +1,18 @@ +FROM ubuntu:devel + +WORKDIR /bar + +RUN apt-get update && \ + apt-get install -y curl jq 7zip + +RUN mkdir -p maps games engine + +COPY --chmod=755 ./download-engine.sh /bar/ +RUN /bar/download-engine.sh + +RUN /bar/engine/*/pr-downloader --filesystem-writepath "/bar" download-map "Full Metal Plate 1.5" + +COPY ./startscript.txt /bar/ +COPY --chmod=755 ./start.sh /bar/ + +ENTRYPOINT /bar/start.sh /bar /bar/startscript.txt diff --git a/tools/headless_testing/docker-compose.yml b/tools/headless_testing/docker-compose.yml new file mode 100644 index 00000000000..908237bf2f3 --- /dev/null +++ b/tools/headless_testing/docker-compose.yml @@ -0,0 +1,15 @@ +version: '3' + +services: + bar: + build: + context: . + dockerfile: Dockerfile + volumes: + - ../..:/bar/games/BAR.sdd:ro + - ../../../../LuaUI/Widgets:/bar/LuaUI/Widgets:ro + - ./testlog:/bar/testlog:rw + - cache:/bar/cache:rw + +volumes: + cache: diff --git a/tools/headless_testing/download-engine.sh b/tools/headless_testing/download-engine.sh new file mode 100644 index 00000000000..324592240f6 --- /dev/null +++ b/tools/headless_testing/download-engine.sh @@ -0,0 +1,11 @@ +#!/bin/bash + +json_url="https://launcher-config.beyondallreason.dev/config.json" +json=$(curl $json_url) + +url=$(echo $json | jq -r '.setups[] | select(.package.id == "manual-linux") | .downloads.resources[] | select(.destination | contains("engine")) | .url') +destination=$(echo $json | jq -r '.setups[] | select(.package.id == "manual-linux") | .downloads.resources[] | select(.destination | contains("engine")) | .destination') + +mkdir -p "$destination" + +curl -L $url -o temp.7z && 7z x temp.7z -o"$destination" && rm -f temp.7z diff --git a/tools/headless_testing/start.sh b/tools/headless_testing/start.sh new file mode 100644 index 00000000000..745e403c211 --- /dev/null +++ b/tools/headless_testing/start.sh @@ -0,0 +1,4 @@ +#!/bin/bash + +rm -rf ./LuaUI/Config +$1/engine/*/spring-headless --isolation --write-dir "$1" "$2" diff --git a/tools/headless_testing/startscript.txt b/tools/headless_testing/startscript.txt new file mode 100644 index 00000000000..88cbfe216a5 --- /dev/null +++ b/tools/headless_testing/startscript.txt @@ -0,0 +1,28 @@ +[GAME] +{ + MapName=Full Metal Plate 1.5; + GameType=Beyond All Reason $VERSION; + GameStartDelay=0; + StartPosType=0; + RecordDemo=0; + MyPlayerName=TestRunner; + IsHost=1; + FixedRNGSeed = 1; + [MODOPTIONS] + { + debugcommands=1:cheat|2:godmode|30:runtestsheadless; + deathmode=neverend; + } + [ALLYTEAM0] + { + } + [TEAM0] + { + allyteam=0; + teamleader=0; + } + [PLAYER0] + { + team=0; + } +} From 9db911b8c51f1044ffaaf6398a23da27795761b5 Mon Sep 17 00:00:00 2001 From: Citrine Date: Sat, 20 Jan 2024 20:09:16 -0800 Subject: [PATCH 43/98] feat(test_framework): set env vars for pr-downloader and engine config --- tools/headless_testing/Dockerfile | 8 +++++--- tools/headless_testing/download-engine.sh | 3 +++ tools/headless_testing/download-maps.sh | 3 +++ tools/headless_testing/springsettings.cfg | 1 + 4 files changed, 12 insertions(+), 3 deletions(-) create mode 100644 tools/headless_testing/download-maps.sh create mode 100644 tools/headless_testing/springsettings.cfg diff --git a/tools/headless_testing/Dockerfile b/tools/headless_testing/Dockerfile index 7b7cf2b668d..e837ade17ee 100644 --- a/tools/headless_testing/Dockerfile +++ b/tools/headless_testing/Dockerfile @@ -8,11 +8,13 @@ RUN apt-get update && \ RUN mkdir -p maps games engine COPY --chmod=755 ./download-engine.sh /bar/ -RUN /bar/download-engine.sh +RUN ./download-engine.sh -RUN /bar/engine/*/pr-downloader --filesystem-writepath "/bar" download-map "Full Metal Plate 1.5" +COPY --chmod=755 ./download-maps.sh /bar/ +RUN ./download-maps.sh COPY ./startscript.txt /bar/ +COPY ./springsettings.cfg /bar/ COPY --chmod=755 ./start.sh /bar/ -ENTRYPOINT /bar/start.sh /bar /bar/startscript.txt +ENTRYPOINT ./start.sh /bar /bar/startscript.txt diff --git a/tools/headless_testing/download-engine.sh b/tools/headless_testing/download-engine.sh index 324592240f6..0df873b932c 100644 --- a/tools/headless_testing/download-engine.sh +++ b/tools/headless_testing/download-engine.sh @@ -5,6 +5,9 @@ json=$(curl $json_url) url=$(echo $json | jq -r '.setups[] | select(.package.id == "manual-linux") | .downloads.resources[] | select(.destination | contains("engine")) | .url') destination=$(echo $json | jq -r '.setups[] | select(.package.id == "manual-linux") | .downloads.resources[] | select(.destination | contains("engine")) | .destination') +env_variables=$(echo $json | jq -r '.setups[] | select(.package.id == "manual-linux") | .env_variables | to_entries[] | "\(.key)=\(.value)"') + +echo "$env_variables" > "config.env" mkdir -p "$destination" diff --git a/tools/headless_testing/download-maps.sh b/tools/headless_testing/download-maps.sh new file mode 100644 index 00000000000..76245c62e28 --- /dev/null +++ b/tools/headless_testing/download-maps.sh @@ -0,0 +1,3 @@ +#!/bin/bash + +env $(cat ./config.env) engine/*/pr-downloader --filesystem-writepath "/bar" --download-map "Full Metal Plate 1.5" diff --git a/tools/headless_testing/springsettings.cfg b/tools/headless_testing/springsettings.cfg new file mode 100644 index 00000000000..4cfae692517 --- /dev/null +++ b/tools/headless_testing/springsettings.cfg @@ -0,0 +1 @@ +RapidTagResolutionOrder = repos-cdn.beyondallreason.dev;repos.beyondallreason.dev From dbdeb0e9b632713e8fd5c8885475220413740987 Mon Sep 17 00:00:00 2001 From: Citrine Date: Sun, 21 Jan 2024 12:03:49 -0800 Subject: [PATCH 44/98] refactor(test_framework): refine docker scripts --- tools/headless_testing/Dockerfile | 23 ++++++++++++++++------- tools/headless_testing/download-engine.sh | 13 +++---------- tools/headless_testing/download-maps.sh | 2 +- tools/headless_testing/parse-config.sh | 9 +++++++++ tools/headless_testing/start.sh | 2 +- 5 files changed, 30 insertions(+), 19 deletions(-) create mode 100644 tools/headless_testing/parse-config.sh diff --git a/tools/headless_testing/Dockerfile b/tools/headless_testing/Dockerfile index e837ade17ee..15281b60506 100644 --- a/tools/headless_testing/Dockerfile +++ b/tools/headless_testing/Dockerfile @@ -2,19 +2,28 @@ FROM ubuntu:devel WORKDIR /bar +ENV BAR_ROOT=/bar +ENV BAR_CONFIG_JSON=./config.json +ENV BAR_CONFIG_ENV=./config.env + RUN apt-get update && \ apt-get install -y curl jq 7zip RUN mkdir -p maps games engine -COPY --chmod=755 ./download-engine.sh /bar/ -RUN ./download-engine.sh +ADD https://launcher-config.beyondallreason.dev/config.json $BAR_CONFIG_JSON + +COPY --chmod=755 ./parse-config.sh . +RUN ./parse-config.sh + +COPY --chmod=755 ./download-engine.sh . +RUN export $(cat $BAR_CONFIG_ENV | xargs) && ./download-engine.sh -COPY --chmod=755 ./download-maps.sh /bar/ -RUN ./download-maps.sh +COPY --chmod=755 ./download-maps.sh . +RUN export $(cat $BAR_CONFIG_ENV | xargs) && ./download-maps.sh -COPY ./startscript.txt /bar/ -COPY ./springsettings.cfg /bar/ -COPY --chmod=755 ./start.sh /bar/ +COPY ./startscript.txt . +COPY ./springsettings.cfg . +COPY --chmod=755 ./start.sh . ENTRYPOINT ./start.sh /bar /bar/startscript.txt diff --git a/tools/headless_testing/download-engine.sh b/tools/headless_testing/download-engine.sh index 0df873b932c..f478cbbf88e 100644 --- a/tools/headless_testing/download-engine.sh +++ b/tools/headless_testing/download-engine.sh @@ -1,14 +1,7 @@ #!/bin/bash -json_url="https://launcher-config.beyondallreason.dev/config.json" -json=$(curl $json_url) +mkdir -p "$ENGINE_DESTINATION" -url=$(echo $json | jq -r '.setups[] | select(.package.id == "manual-linux") | .downloads.resources[] | select(.destination | contains("engine")) | .url') -destination=$(echo $json | jq -r '.setups[] | select(.package.id == "manual-linux") | .downloads.resources[] | select(.destination | contains("engine")) | .destination') -env_variables=$(echo $json | jq -r '.setups[] | select(.package.id == "manual-linux") | .env_variables | to_entries[] | "\(.key)=\(.value)"') +TEMP_FILE=$(mktemp --suffix=.7z) -echo "$env_variables" > "config.env" - -mkdir -p "$destination" - -curl -L $url -o temp.7z && 7z x temp.7z -o"$destination" && rm -f temp.7z +curl -L "$ENGINE_URL" -o "$TEMP_FILE" && 7z x "$TEMP_FILE" -o"$ENGINE_DESTINATION" && rm -f temp.7z diff --git a/tools/headless_testing/download-maps.sh b/tools/headless_testing/download-maps.sh index 76245c62e28..e92c1e4dca1 100644 --- a/tools/headless_testing/download-maps.sh +++ b/tools/headless_testing/download-maps.sh @@ -1,3 +1,3 @@ #!/bin/bash -env $(cat ./config.env) engine/*/pr-downloader --filesystem-writepath "/bar" --download-map "Full Metal Plate 1.5" +engine/*/pr-downloader --filesystem-writepath "$BAR_ROOT" --download-map "Full Metal Plate 1.5" diff --git a/tools/headless_testing/parse-config.sh b/tools/headless_testing/parse-config.sh new file mode 100644 index 00000000000..7fff0756d0e --- /dev/null +++ b/tools/headless_testing/parse-config.sh @@ -0,0 +1,9 @@ +#!/bin/bash + +url=$(cat "$BAR_CONFIG_JSON" | jq -r '.setups[] | select(.package.id == "manual-linux") | .downloads.resources[] | select(.destination | contains("engine")) | .url') +destination=$(cat "$BAR_CONFIG_JSON" | jq -r '.setups[] | select(.package.id == "manual-linux") | .downloads.resources[] | select(.destination | contains("engine")) | .destination') +env_variables=$(cat "$BAR_CONFIG_JSON" | jq -r '.setups[] | select(.package.id == "manual-linux") | .env_variables | to_entries[] | "\(.key)=\(.value)"') + +echo "$env_variables" > "$BAR_CONFIG_ENV" +echo "ENGINE_URL=\"$url\"" >> "$BAR_CONFIG_ENV" +echo "ENGINE_DESTINATION=\"$destination\"" >> "$BAR_CONFIG_ENV" diff --git a/tools/headless_testing/start.sh b/tools/headless_testing/start.sh index 745e403c211..ffcde7b8d39 100644 --- a/tools/headless_testing/start.sh +++ b/tools/headless_testing/start.sh @@ -1,4 +1,4 @@ #!/bin/bash -rm -rf ./LuaUI/Config +rm -rf $1/LuaUI/Config $1/engine/*/spring-headless --isolation --write-dir "$1" "$2" From 761438a14a448a9c5b942c22b061a7c67c14efba Mon Sep 17 00:00:00 2001 From: Citrine Date: Sun, 21 Jan 2024 12:43:25 -0800 Subject: [PATCH 45/98] feat(test_framework): Add workflow scripts to show test results in PRs The trigger for running tests is disabled until we want to enable them. In order to run, these scripts (like other workflow scripts) must be in the main branch (master). --- .github/workflows/process_test_results.yml | 41 ++++++++++++++++++++++ .github/workflows/run_tests.yml | 34 ++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 .github/workflows/process_test_results.yml create mode 100644 .github/workflows/run_tests.yml diff --git a/.github/workflows/process_test_results.yml b/.github/workflows/process_test_results.yml new file mode 100644 index 00000000000..55a7604eff5 --- /dev/null +++ b/.github/workflows/process_test_results.yml @@ -0,0 +1,41 @@ +name: Process Test Results + +on: + workflow_run: + workflows: ["Run Tests"] + types: + - completed + +permissions: {} + +jobs: + process-test-results: + name: Process Test Results + runs-on: ubuntu-latest + if: github.event.workflow_run.conclusion != 'skipped' + + permissions: + checks: write + + # needed unless run with comment_mode: off + pull-requests: write + + # required by download step to access artifacts API + actions: read + + steps: + - name: Download Artifacts + uses: actions/download-artifact@v4 + with: + path: artifacts + github-token: ${{ github.token }} + run-id: ${{ github.event.workflow_run.id }} + + - name: Publish Test Results + uses: EnricoMi/publish-unit-test-result-action@v2 + with: + time_unit: milliseconds + commit: ${{ github.event.workflow_run.head_sha }} + event_file: artifacts/Event File/event.json + event_name: ${{ github.event.workflow_run.event }} + files: "artifacts/Test Results/*.json" diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml new file mode 100644 index 00000000000..19a7d0a603b --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,34 @@ +name: Run Tests + +on: +# pull_request + workflow_dispatch: + +jobs: + upload-event_file: + name: Upload Event File + runs-on: ubuntu-latest + steps: + - name: Upload Event File + uses: actions/upload-artifact@v4 + with: + name: Event File + path: ${{ github.event_path }} + + run-tests: + name: Run Tests + runs-on: ubuntu-latest + steps: + - name: Checkout Repository + uses: actions/checkout@v4 + + - name: Run Tests + run: docker-compose -f tools/headless_testing/docker-compose.yml up + + - name: Upload Test Results + if: always() + uses: actions/upload-artifact@v4 + with: + name: Test Results + path: | + tools/headless_testing/testlog/results.json From 81a8b20d4a3a11621d9038e058e94494f1bdc6ea Mon Sep 17 00:00:00 2001 From: Citrine Date: Sun, 21 Jan 2024 20:32:21 -0800 Subject: [PATCH 46/98] fix(test_framework): Only enable watchdog if running in headless mode --- luaui/Widgets/dbg_test_framework.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 8790d5797ef..3f4f80340d5 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -850,8 +850,6 @@ function widget:Initialize() widgetHandler:RemoveWidget(self) end - widgetHandler:EnableWidget("Test Framework Watchdog") - widgetHandler.actionHandler:AddAction( self, "runtests", @@ -869,6 +867,8 @@ function widget:Initialize() quitWhenDone = true gameStartTestPatterns = splitPhrases(optLine) testResultsFilePath = "testlog/results.json" + + widgetHandler:EnableWidget("Test Framework Watchdog") end, nil, "t" From 2f50a47093c33fdfb1c55bc705af69aebe8a9d70 Mon Sep 17 00:00:00 2001 From: Citrine Date: Mon, 5 Feb 2024 23:03:08 -0800 Subject: [PATCH 47/98] feat(test_framework): add support for skip() function in tests --- luarules/testing/util.lua | 2 +- .../Tests/balance/test_grunts_vs_pawns.lua | 4 +++ .../cmd_stop_selfd/test_cmd_stop_selfd.lua | 12 ++++--- luaui/Widgets/dbg_test_framework.lua | 36 +++++++++++++++---- 4 files changed, 42 insertions(+), 12 deletions(-) diff --git a/luarules/testing/util.lua b/luarules/testing/util.lua index 0d9e73d804d..9736c3aec5c 100644 --- a/luarules/testing/util.lua +++ b/luarules/testing/util.lua @@ -3,7 +3,7 @@ local serpent = serpent or VFS.Include('common/luaUtilities/serpent.lua') TEST_RESULT = { PASS = 1, FAIL = 2, - SKIPPED = 3, + SKIP = 3, ERROR = 4, } diff --git a/luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua b/luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua index c2297fad8ab..336667142ba 100644 --- a/luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua +++ b/luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua @@ -1,3 +1,7 @@ +function skip() + return Game.mapName ~= "Full Metal Plate 1.5" +end + function setup() Test.clearMap() end diff --git a/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua b/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua index c0f93b77486..57a1edec80d 100644 --- a/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua +++ b/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua @@ -1,3 +1,7 @@ +function skip() + return Game.mapName ~= "Full Metal Plate 1.5" +end + function setup() Test.clearMap() end @@ -20,22 +24,22 @@ function test() -- issue selfd and then issue stop Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, 0) - Test.waitUntilCallinArgs("UnitCommand", {nil, nil, nil, CMD.SELFD, nil, nil, nil}) + Test.waitUntilCallinArgs("UnitCommand", { nil, nil, nil, CMD.SELFD, nil, nil, nil }) assert(Spring.GetUnitSelfDTime(unitID) > 0) Spring.GiveOrderToUnit(unitID, CMD.STOP, {}, 0) - Test.waitUntilCallinArgs("UnitCommand", {nil, nil, nil, CMD.SELFD, nil, nil, nil}) + Test.waitUntilCallinArgs("UnitCommand", { nil, nil, nil, CMD.SELFD, nil, nil, nil }) assert(Spring.GetUnitSelfDTime(unitID) == 0) assert(#(Spring.GetCommandQueue(unitID, 1)) == 0) -- issue {move, selfd}, then issue stop Spring.GiveOrderToUnit(unitID, CMD.MOVE, { 1, 1, 1 }, 0) Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, { "shift" }) - Test.waitUntilCallinArgs("UnitCommand", {nil, nil, nil, CMD.SELFD, nil, nil, nil}) + Test.waitUntilCallinArgs("UnitCommand", { nil, nil, nil, CMD.SELFD, nil, nil, nil }) assert(Spring.GetUnitSelfDTime(unitID) == 0) Spring.GiveOrderToUnit(unitID, CMD.STOP, {}, 0) - Test.waitUntilCallinArgs("UnitCommand", {nil, nil, nil, CMD.STOP, nil, nil, nil}) + Test.waitUntilCallinArgs("UnitCommand", { nil, nil, nil, CMD.STOP, nil, nil, nil }) assert(Spring.GetUnitSelfDTime(unitID) == 0) assert(#(Spring.GetCommandQueue(unitID, 1)) == 0) end diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 3f4f80340d5..e9b185da535 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -500,6 +500,24 @@ end local function runTestInternal() log(LOG.DEBUG, "[runTestInternal]") + local skipOk, skipResult + if skip ~= nil then + log(LOG.DEBUG, "[runTestInternal.skip]") + skipOk, skipResult = yieldable_pcall(skip) + log(LOG.DEBUG, "[runTestInternal.skip.done] " .. table.toString({ + skipOk, skipResult + })) + + if not skipOk then + log(LOG.DEBUG, "[runTestInternal.skip.error]") + error(skipResult, 2) + end + + if skipResult then + return TEST_RESULT.SKIP + end + end + local setupOk, setupResult if setup ~= nil then log(LOG.DEBUG, "[runTestInternal.setup]") @@ -548,6 +566,8 @@ local function runTestInternal() log(LOG.DEBUG, "[runTestInternal.test.error]") error(testResult, 2) end + + return TEST_RESULT.PASS end local function initializeTestEnvironment() @@ -558,6 +578,7 @@ local function initializeTestEnvironment() SyncedRun = SyncedRun, __runTestInternal = runTestInternal, yieldable_pcall = yieldable_pcall, + TEST_RESULT = TEST_RESULT, -- widgets widgetHandler = widgetHandler, @@ -783,6 +804,7 @@ local function step() end -- resume the test + local resumeOk, resumeResult if coroutine.status(activeTestState.coroutine) == "suspended" then local coroutineOk, coroutineArgs if returnResult.returnValue ~= nil then @@ -807,25 +829,25 @@ local function step() }) ) - local result, error = coroutine.resume(activeTestState.coroutine, coroutineOk, coroutineArgs) + resumeOk, resumeResult = coroutine.resume(activeTestState.coroutine, coroutineOk, coroutineArgs) log(LOG.DEBUG, "Unresuming test: " .. table.toString({ - result = result, - error = error, + result = resumeOk, + error = resumeResult, })) - if not result then + if not resumeOk then -- test fail finishTest({ result = TEST_RESULT.FAIL, - error = error, + error = resumeResult, }) return end end if coroutine.status(activeTestState.coroutine) == "dead" then - -- test pass + -- test did not fail or error, so may have been pass or skip finishTest({ - result = TEST_RESULT.PASS, + result = resumeResult or TEST_RESULT.PASS, }) end end From 23f207f1490129a118d757b9fe26156b8c8c37ae Mon Sep 17 00:00:00 2001 From: Citrine Date: Mon, 5 Feb 2024 23:04:16 -0800 Subject: [PATCH 48/98] fix(test_framework): disable watchdog on load Just in case it was already on, this will disable it. It will then be re-enabled later if necessary. --- luaui/Widgets/dbg_test_framework.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index e9b185da535..02f4916a453 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -868,6 +868,7 @@ function widget:Update(dt) end function widget:Initialize() + widgetHandler:DisableWidget("Test Framework Watchdog") if not Spring.Utilities.IsDevMode() then widgetHandler:RemoveWidget(self) end From 653a6fdde12b564aefa1f19a074cbe8bbd230c2f Mon Sep 17 00:00:00 2001 From: Citrine Date: Mon, 5 Feb 2024 23:04:37 -0800 Subject: [PATCH 49/98] fix(test_framework): fix return values in localsAccess metatable --- luarules/testing/localsAccess.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/luarules/testing/localsAccess.lua b/luarules/testing/localsAccess.lua index 7857afff8af..c32a9068c7a 100644 --- a/luarules/testing/localsAccess.lua +++ b/luarules/testing/localsAccess.lua @@ -43,12 +43,12 @@ local function generateLocalsAccessMetatable(baseMetatable) return baseMetatable.__index(t, k) end else - rawget(t, k) + return rawget(t, k) end end, __newindex = function(t, k, v) if t.__localsAccess and t.__localsAccess.setters[k] ~= nil then - t.__localsAccess.setters[k](v) + return t.__localsAccess.setters[k](v) elseif baseMetatable and baseMetatable.__newindex ~= nil then return baseMetatable.__newindex(t, k, v) else From d19d526d6015343896a0cdbdc733fc5c55695529 Mon Sep 17 00:00:00 2001 From: Citrine Date: Mon, 5 Feb 2024 23:11:40 -0800 Subject: [PATCH 50/98] fix(test_framework): use proper method call syntax in watchdog widget --- luaui/Widgets/dbg_test_framework_watchdog.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaui/Widgets/dbg_test_framework_watchdog.lua b/luaui/Widgets/dbg_test_framework_watchdog.lua index b1be52af81d..9564b80c142 100644 --- a/luaui/Widgets/dbg_test_framework_watchdog.lua +++ b/luaui/Widgets/dbg_test_framework_watchdog.lua @@ -19,7 +19,7 @@ function widget:Update(dt) t = 0 if widgetHandler:FindWidget("Test Framework Runner") == nil then Spring.Log(widget:GetInfo().name, LOG.WARNING, "Test runner crashed, exiting game") - widgetHandler:DisableWidget(widget.GetInfo().name) + widgetHandler:DisableWidget(widget:GetInfo().name) Spring.SendCommands("quitforce") end end From df096317d386d77345d2729beb9c1b13dca70bc6 Mon Sep 17 00:00:00 2001 From: Citrine Date: Thu, 11 Jan 2024 13:15:12 -0800 Subject: [PATCH 51/98] feat(test_framework): add assertTablesEqual --- luarules/testing/assertions.lua | 51 ++++++++++++++++++++++++++++ luaui/Widgets/dbg_test_framework.lua | 6 ++++ 2 files changed, 57 insertions(+) create mode 100644 luarules/testing/assertions.lua diff --git a/luarules/testing/assertions.lua b/luarules/testing/assertions.lua new file mode 100644 index 00000000000..981fa79a5c2 --- /dev/null +++ b/luarules/testing/assertions.lua @@ -0,0 +1,51 @@ +function assertTablesEqual(table1, table2, margin, visited, path) + visited = visited or {} + path = path or {} + margin = margin or 0 + + local function buildPathString() + local pathString = "" + for _, key in ipairs(path) do + pathString = pathString .. "[" .. tostring(key) .. "]" + end + return pathString + end + + if type(table1) ~= "table" or type(table2) ~= "table" then + if type(table1) == "number" and type(table2) == "number" then + assert(math.abs(table1 - table2) <= margin, "Numbers are not close enough at path: " .. buildPathString()) + else + assert(table1 == table2, "Tables are not equal at path: " .. buildPathString()) + end + return + end + + if visited[table1] or visited[table2] then + -- Prevent infinite recursion on circular references + assert(table1 == table2, "Tables are not equal (circular reference) at path: " .. buildPathString()) + return + end + + visited[table1] = true + visited[table2] = true + + for key, value1 in pairs(table1) do + local value2 = table2[key] + table.insert(path, key) + assertTablesEqual(value1, value2, margin, visited, path) + table.remove(path) + end + + for key, value2 in pairs(table2) do + local value1 = table1[key] + if value1 == nil then + assert(false, "Tables are not equal, extra key '" .. tostring(key) .. "' in second table at path: " .. buildPathString()) + end + end +end + + + +return { + assertTablesEqual = assertTablesEqual, +} diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 02f4916a453..5c2c28718a1 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -13,6 +13,7 @@ end VFS.Include('luarules/testing/util.lua') local MochaJSONReporter = VFS.Include('luarules/testing/mochaJsonReporter.lua') +local assertions = VFS.Include('luarules/testing/assertions.lua') local LOG_LEVEL = LOG.INFO @@ -630,6 +631,11 @@ local function initializeTestEnvironment() Json = Json, } + for k, v in pairs(assertions) do + Spring.Echo(k) + env[k] = v + end + return env end From b0174618f0c6404b65ac8a8fba8bcfc3ca427a6a Mon Sep 17 00:00:00 2001 From: Citrine Date: Mon, 5 Feb 2024 23:18:25 -0800 Subject: [PATCH 52/98] test(mex-building-overhaul): add a test for some pregame actions --- .../Tests/mex-building-overhaul/pregame1.lua | 84 +++++++++++++++++++ 1 file changed, 84 insertions(+) create mode 100644 luaui/Widgets/Tests/mex-building-overhaul/pregame1.lua diff --git a/luaui/Widgets/Tests/mex-building-overhaul/pregame1.lua b/luaui/Widgets/Tests/mex-building-overhaul/pregame1.lua new file mode 100644 index 00000000000..dfc2c04e38b --- /dev/null +++ b/luaui/Widgets/Tests/mex-building-overhaul/pregame1.lua @@ -0,0 +1,84 @@ +function setup() + Test.clearMap() + + widget_cmd_extractor_snap = widgetHandler:FindWidget("Extractor Snap (mex/geo)") + assert(widget_cmd_extractor_snap) + + widget_gui_pregame_build = widgetHandler:FindWidget("Pregame Queue") + assert(widget_gui_pregame_build) + + WG['pregame-build'].setBuildQueue({}) + WG["pregame-build"].setPreGamestartDefID(nil) + + initialCameraState = Spring.GetCameraState() + + Spring.SetCameraState({ + mode = 5, + }) + + -- wait for camera to move + Test.waitTime(10) +end + +function cleanup() + Test.clearMap() + + WG['pregame-build'].setBuildQueue({}) + WG["pregame-build"].setPreGamestartDefID(nil) + + Spring.SetCameraState(initialCameraState) +end + +function test() + mexUnitDefId = UnitDefNames["armmex"].id + metalSpots = WG['resource_spot_finder'].metalSpotsList + + midX, midZ = Game.mapSizeX / 2, Game.mapSizeZ / 2 + targetMex = nil + targetMexDistance = 1e20 + for i = 1, #metalSpots do + local distance2 = math.distance2dSquared(midX, midZ, metalSpots[i].x, metalSpots[i].z) + if distance2 < targetMexDistance then + targetMexDistance = distance2 + targetMex = metalSpots[i] + end + end + + -- Place a mex off of a mex spot - expect mex snap to position it on the spot, as close as possible to cursor position + WG["pregame-build"].setPreGamestartDefID(mexUnitDefId) + sx, sy, sz = Spring.WorldToScreenCoords(targetMex.x - 200, targetMex.y, targetMex.z - 200) + Spring.WarpMouse(sx, sy) + + -- wait for widgets to respond + Test.waitTime(10) + + -- did it snap? + assert(WG.ExtractorSnap.position ~= nil) + + -- did it snap to the closest mex? + assert(math.distance2d( + WG.ExtractorSnap.position.x, + WG.ExtractorSnap.position.z, + targetMex.x, + targetMex.z + ) < 100) + + snappedPosition = table.copy(WG.ExtractorSnap.position) + + -- queue the mex build + Script.LuaUI.MousePress(sx, sy, 1) + + -- wait for widgets to respond + Test.waitTime(10) + + -- did the mex get placed in the right spot? + buildQueue = WG['pregame-build'].getBuildQueue() + assert(#buildQueue == 1) + assertTablesEqual(buildQueue[1], { + mexUnitDefId, + snappedPosition.x, + snappedPosition.y, + snappedPosition.z, + 0 + }, 0.1) +end From a518063aae20ffc95a8faaef709418f35c94226f Mon Sep 17 00:00:00 2001 From: Citrine Date: Fri, 9 Feb 2024 21:57:47 -0800 Subject: [PATCH 53/98] feat(test_framework): add Test.mock() Test.mock() works very similarly to Test.spy(). It takes a parent table, a target function name, and an optional replacement function. Whenever the target function is called, the call is recorded, and then if a replacement function was specified, that is called. The original is never called. --- luarules/testing/util.lua | 21 +++++++++++++++++++++ luaui/Widgets/dbg_test_framework.lua | 5 +++++ 2 files changed, 26 insertions(+) diff --git a/luarules/testing/util.lua b/luarules/testing/util.lua index 9736c3aec5c..640f3b5b465 100644 --- a/luarules/testing/util.lua +++ b/luarules/testing/util.lua @@ -293,6 +293,27 @@ function spy(parent, target) end parent[target] = wrapper return { + original = original, + calls = calls, + remove = function() + parent[target] = original + end + } +end + +function mock(parent, target, fn) + local original = parent[target] + local calls = {} + local wrapper = function(...) + local args = { ... } + calls[#calls + 1] = table.copy(args) + if fn ~= nil then + return fn(unpack(args)) + end + end + parent[target] = wrapper + return { + original = original, calls = calls, remove = function() parent[target] = original diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 5c2c28718a1..389ba78d6cd 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -460,6 +460,11 @@ Test = { spyControls[#spyControls + 1] = spyCtrl return spyCtrl end, + mock = function(...) + local mockCtrl = mock(...) + spyControls[#spyControls + 1] = mockCtrl + return mockCtrl + end, clearMap = SyncedExtra.clearMap, clearCallinBuffer = function(name) if name ~= nil then From be50aebb91a5fe8ffd46a2e51b9ae1e59eb9bf0e Mon Sep 17 00:00:00 2001 From: Citrine Date: Fri, 9 Feb 2024 21:58:16 -0800 Subject: [PATCH 54/98] fix(test_framework): misplaced return statement --- luaui/Widgets/dbg_test_framework.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index 389ba78d6cd..c9db2f32205 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -451,8 +451,8 @@ Test = { if currentArgs[k] == nil or currentArgs[k] ~= v then return false end - return true end + return true end) end, spy = function(...) From 911ddcb06dec4a71f97ad89ff87077e4614e538a Mon Sep 17 00:00:00 2001 From: Citrine Date: Fri, 9 Feb 2024 22:03:59 -0800 Subject: [PATCH 55/98] refactor(test_framework): make SKIP color a little clearer --- luarules/testing/util.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luarules/testing/util.lua b/luarules/testing/util.lua index 640f3b5b465..80872e4f9a4 100644 --- a/luarules/testing/util.lua +++ b/luarules/testing/util.lua @@ -61,7 +61,7 @@ function formatTestResult(testResult, noColor) elseif testResult.result == TEST_RESULT.FAIL then resultColor = rgbToColorCode(1, 0, 0) elseif testResult.result == TEST_RESULT.SKIP then - resultColor = rgbToColorCode(0.8, 0.8, 0) + resultColor = rgbToColorCode(1, 0.8, 0) elseif testResult.result == TEST_RESULT.ERROR then resultColor = rgbToColorCode(0.8, 0, 0.8) end From 9ec24fa0bff996f82d1e0c21b509eced117910d3 Mon Sep 17 00:00:00 2001 From: Citrine Date: Fri, 9 Feb 2024 22:04:30 -0800 Subject: [PATCH 56/98] test(mex-building-overhaul): skip pregame test if not pregame --- luaui/Widgets/Tests/mex-building-overhaul/pregame1.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/luaui/Widgets/Tests/mex-building-overhaul/pregame1.lua b/luaui/Widgets/Tests/mex-building-overhaul/pregame1.lua index dfc2c04e38b..d2d573cb12b 100644 --- a/luaui/Widgets/Tests/mex-building-overhaul/pregame1.lua +++ b/luaui/Widgets/Tests/mex-building-overhaul/pregame1.lua @@ -1,3 +1,7 @@ +function skip() + return Spring.GetGameFrame() > 0 +end + function setup() Test.clearMap() From c3c9d5a8c8bef16dd79e5ab9108fb2b6a921bb19 Mon Sep 17 00:00:00 2001 From: Citrine Date: Fri, 9 Feb 2024 22:13:33 -0800 Subject: [PATCH 57/98] fix(test_framework): remove extra Spring.Echo --- luaui/Widgets/dbg_test_framework.lua | 1 - 1 file changed, 1 deletion(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index c9db2f32205..cd1363a5b13 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -637,7 +637,6 @@ local function initializeTestEnvironment() } for k, v in pairs(assertions) do - Spring.Echo(k) env[k] = v end From c7996091deec3701b4ea56abb56e1d8e5c056add Mon Sep 17 00:00:00 2001 From: Citrine Date: Fri, 9 Feb 2024 22:15:34 -0800 Subject: [PATCH 58/98] test(test_framework): add an example test for mocks --- luaui/Widgets/Tests/example/test_mock.lua | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 luaui/Widgets/Tests/example/test_mock.lua diff --git a/luaui/Widgets/Tests/example/test_mock.lua b/luaui/Widgets/Tests/example/test_mock.lua new file mode 100644 index 00000000000..5bf0a34757c --- /dev/null +++ b/luaui/Widgets/Tests/example/test_mock.lua @@ -0,0 +1,9 @@ +function test() + mock_SpringGetModKeyState = Test.mock(Spring, "GetModKeyState", function() + return true, false, true, false + end) + + assertTablesEqual(pack(Spring.GetModKeyState()), {true, false, true, false}) + + assert(#(mock_SpringGetModKeyState.calls) == 1) +end From 3781c368ac40b2635ed458afcf432d24ab96a892 Mon Sep 17 00:00:00 2001 From: thehobojoe Date: Sat, 10 Feb 2024 00:58:43 -0600 Subject: [PATCH 59/98] Test that building queue can be edited with a mex selected (not overriden by mex snap) --- .../Tests/mex-building/pregame_mex_queue.lua | 101 ++++++++++++++++++ .../pregame_mex_snap.lua} | 0 2 files changed, 101 insertions(+) create mode 100644 luaui/Widgets/Tests/mex-building/pregame_mex_queue.lua rename luaui/Widgets/Tests/{mex-building-overhaul/pregame1.lua => mex-building/pregame_mex_snap.lua} (100%) diff --git a/luaui/Widgets/Tests/mex-building/pregame_mex_queue.lua b/luaui/Widgets/Tests/mex-building/pregame_mex_queue.lua new file mode 100644 index 00000000000..a14c1569910 --- /dev/null +++ b/luaui/Widgets/Tests/mex-building/pregame_mex_queue.lua @@ -0,0 +1,101 @@ +-- Test whether mexes are able to clear queued buildings by shift-clicking +function setup() + Test.clearMap() + + widget_cmd_extractor_snap = widgetHandler:FindWidget("Extractor Snap (mex/geo)") + assert(widget_cmd_extractor_snap) + + widget_gui_pregame_build = widgetHandler:FindWidget("Pregame Queue") + assert(widget_gui_pregame_build) + + WG['pregame-build'].setBuildQueue({}) + WG["pregame-build"].setPreGamestartDefID(nil) + + initialCameraState = Spring.GetCameraState() + + Spring.SetCameraState({ + mode = 5, + }) + + -- wait for camera to move + Test.waitTime(10) +end + +function cleanup() + Test.clearMap() + + WG['pregame-build'].setBuildQueue({}) + WG["pregame-build"].setPreGamestartDefID(nil) + + Spring.SetCameraState(initialCameraState) +end + +function test() + mexUnitDefId = UnitDefNames["armmex"].id + metalSpots = WG['resource_spot_finder'].metalSpotsList + + midX, midZ = Game.mapSizeX / 2, Game.mapSizeZ / 2 + targetMex = nil + targetMexDistance = 1e20 + for i = 1, #metalSpots do + local distance2 = math.distance2dSquared(midX, midZ, metalSpots[i].x, metalSpots[i].z) + if distance2 < targetMexDistance then + targetMexDistance = distance2 + targetMex = metalSpots[i] + end + end + + -- Place a mex off of a mex spot - expect mex snap to position it on the spot, as close as possible to cursor position + WG["pregame-build"].setPreGamestartDefID(mexUnitDefId) + sx, sy, sz = Spring.WorldToScreenCoords(targetMex.x - 200, targetMex.y, targetMex.z - 200) + Spring.WarpMouse(sx, sy) + + -- wait for widgets to respond + Test.waitTime(10) + + -- did it snap? + assert(WG.ExtractorSnap.position ~= nil) + + -- did it snap to the closest mex? + assert(math.distance2d( + WG.ExtractorSnap.position.x, + WG.ExtractorSnap.position.z, + targetMex.x, + targetMex.z + ) < 100) + + snappedPosition = table.copy(WG.ExtractorSnap.position) + + -- queue the mex build + Script.LuaUI.MousePress(sx, sy, 1) + + -- wait for widgets to respond + Test.waitTime(10) + + -- move mouse to snapped position + sx, sy, sz = Spring.WorldToScreenCoords(snappedPosition.x, snappedPosition.y, snappedPosition.z) + Spring.WarpMouse(sx, sy) + + -- select mex again + WG["pregame-build"].setPreGamestartDefID(mexUnitDefId) + + -- wait for widgets to respond + Test.waitTime(10) + + -- mock shift, and mouse press on top of the snapped position + -- this should clear the queued mex, and not try to snap it to another position + Test.mock(Spring, "GetModKeyState", function() + return false, false, false, true + end) + Script.LuaUI.MousePress(sx, sy, 1) + + -- wait for widgets to respond + Test.waitTime(10) + + -- clear blueprint + WG["pregame-build"].setPreGamestartDefID(nil) + + -- Did the mex get de-queued? + buildQueue = WG['pregame-build'].getBuildQueue() + assert(#buildQueue == 0, "Build queue should be empty") +end diff --git a/luaui/Widgets/Tests/mex-building-overhaul/pregame1.lua b/luaui/Widgets/Tests/mex-building/pregame_mex_snap.lua similarity index 100% rename from luaui/Widgets/Tests/mex-building-overhaul/pregame1.lua rename to luaui/Widgets/Tests/mex-building/pregame_mex_snap.lua From fd076be937a3bbbafc1b93c1b7dd116d2ed8be94 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 01:36:00 +0100 Subject: [PATCH 60/98] Try change the docker compose command. --- .github/workflows/run_tests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 19a7d0a603b..0d9056d95bd 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -23,7 +23,7 @@ jobs: uses: actions/checkout@v4 - name: Run Tests - run: docker-compose -f tools/headless_testing/docker-compose.yml up + run: docker compose -f tools/headless_testing/docker-compose.yml up - name: Upload Test Results if: always() From 632e79633e81775fc579f7adea7d450997fffb43 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 01:51:23 +0100 Subject: [PATCH 61/98] Fix conflict. --- .../Tests/mex-building/pregame_mex_queue.lua | 34 ------------------- 1 file changed, 34 deletions(-) diff --git a/luaui/Widgets/Tests/mex-building/pregame_mex_queue.lua b/luaui/Widgets/Tests/mex-building/pregame_mex_queue.lua index 7bfcfd0c58f..1adda50e002 100644 --- a/luaui/Widgets/Tests/mex-building/pregame_mex_queue.lua +++ b/luaui/Widgets/Tests/mex-building/pregame_mex_queue.lua @@ -1,25 +1,15 @@ -<<<<<<< HEAD -======= function skip() return Spring.GetGameFrame() > 0 end ->>>>>>> master -- Test whether mexes are able to clear queued buildings by shift-clicking function setup() Test.clearMap() -<<<<<<< HEAD widget_cmd_extractor_snap = widgetHandler:FindWidget("Extractor Snap (mex/geo)") assert(widget_cmd_extractor_snap) widget_gui_pregame_build = widgetHandler:FindWidget("Pregame Queue") -======= - local widget_cmd_extractor_snap = widgetHandler:FindWidget("Extractor Snap (mex/geo)") - assert(widget_cmd_extractor_snap) - - local widget_gui_pregame_build = widgetHandler:FindWidget("Pregame Queue") ->>>>>>> master assert(widget_gui_pregame_build) WG['pregame-build'].setBuildQueue({}) @@ -44,15 +34,6 @@ function cleanup() Spring.SetCameraState(initialCameraState) end -<<<<<<< HEAD -function test() - mexUnitDefId = UnitDefNames["armmex"].id - metalSpots = WG['resource_spot_finder'].metalSpotsList - - midX, midZ = Game.mapSizeX / 2, Game.mapSizeZ / 2 - targetMex = nil - targetMexDistance = 1e20 -======= -- tests both pregame mex snap behavior, as well as basic queue and blueprint handling function test() local mexUnitDefId = UnitDefNames["armmex"].id @@ -61,7 +42,6 @@ function test() local midX, midZ = Game.mapSizeX / 2, Game.mapSizeZ / 2 local targetMex = nil local targetMexDistance = 1e20 ->>>>>>> master for i = 1, #metalSpots do local distance2 = math.distance2dSquared(midX, midZ, metalSpots[i].x, metalSpots[i].z) if distance2 < targetMexDistance then @@ -72,13 +52,9 @@ function test() -- Place a mex off of a mex spot - expect mex snap to position it on the spot, as close as possible to cursor position WG["pregame-build"].setPreGamestartDefID(mexUnitDefId) -<<<<<<< HEAD - sx, sy, sz = Spring.WorldToScreenCoords(targetMex.x - 200, targetMex.y, targetMex.z - 200) -======= local activeBlueprint = WG["pregame-build"].getPreGameDefID() assert(activeBlueprint == mexUnitDefId, "Active blueprint should be armmex") local sx, sy, sz = Spring.WorldToScreenCoords(targetMex.x - 200, targetMex.y, targetMex.z - 200) ->>>>>>> master Spring.WarpMouse(sx, sy) -- wait for widgets to respond @@ -95,11 +71,7 @@ function test() targetMex.z ) < 100) -<<<<<<< HEAD - snappedPosition = table.copy(WG.ExtractorSnap.position) -======= local snappedPosition = table.copy(WG.ExtractorSnap.position) ->>>>>>> master -- queue the mex build Script.LuaUI.MousePress(sx, sy, 1) @@ -129,16 +101,10 @@ function test() -- clear blueprint WG["pregame-build"].setPreGamestartDefID(nil) -<<<<<<< HEAD - - -- Did the mex get de-queued? - buildQueue = WG['pregame-build'].getBuildQueue() -======= activeBlueprint = WG["pregame-build"].getPreGameDefID() assert(activeBlueprint == nil, "Active blueprint should be nil") -- Did the mex get de-queued? local buildQueue = WG['pregame-build'].getBuildQueue() ->>>>>>> master assert(#buildQueue == 0, "Build queue should be empty") end From 1db677fa138f4bcf6083e04e21899a9a1f6beae1 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 01:53:13 +0100 Subject: [PATCH 62/98] Disable cus_gl4. --- luarules/gadgets/cus_gl4.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luarules/gadgets/cus_gl4.lua b/luarules/gadgets/cus_gl4.lua index 19d64c91c8b..e5628f45405 100644 --- a/luarules/gadgets/cus_gl4.lua +++ b/luarules/gadgets/cus_gl4.lua @@ -7,7 +7,7 @@ function gadget:GetInfo() date = "20220310", license = "GNU GPL, v2 or later", layer = 0, - enabled = true, + enabled = false, } end From 5a3ebce7370289b05e9f0689e1cc947572331d66 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 08:56:18 +0100 Subject: [PATCH 63/98] Add missing RemoveWidget at gfx_unit_stencil_gl4:goodbye. --- luaui/Widgets/gfx_unit_stencil_gl4.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/luaui/Widgets/gfx_unit_stencil_gl4.lua b/luaui/Widgets/gfx_unit_stencil_gl4.lua index 7005bcf96ca..65aac668696 100644 --- a/luaui/Widgets/gfx_unit_stencil_gl4.lua +++ b/luaui/Widgets/gfx_unit_stencil_gl4.lua @@ -187,6 +187,7 @@ void main(void) local function goodbye(reason) Spring.Echo("Unit Stencil GL4 widget exiting with reason: "..reason) + widgetHandler:RemoveWidget() end local resolution = 4 local vsx, vsy From 7e47229e1f5d734d1b75e81be0229b3568a7e81e Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 09:09:53 +0100 Subject: [PATCH 64/98] Return from Initialize when unitStencilShader could not be created. --- luaui/Widgets/gfx_unit_stencil_gl4.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/luaui/Widgets/gfx_unit_stencil_gl4.lua b/luaui/Widgets/gfx_unit_stencil_gl4.lua index 65aac668696..cd809992c07 100644 --- a/luaui/Widgets/gfx_unit_stencil_gl4.lua +++ b/luaui/Widgets/gfx_unit_stencil_gl4.lua @@ -393,6 +393,9 @@ end function widget:Initialize() unitStencilShader = InitDrawPrimitiveAtUnit(shaderConfig, "unitStencils") + if not unitStencilShader then + return + end widget:ViewResize() WG['unitstencilapi'] = {} From a10e5f32506c3d6883d246dc87568e3eb5026d30 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 09:20:47 +0100 Subject: [PATCH 65/98] Add debug print. --- luaui/Widgets/dbg_test_runner.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/luaui/Widgets/dbg_test_runner.lua b/luaui/Widgets/dbg_test_runner.lua index 2c4bada6f2d..80aec7d764d 100644 --- a/luaui/Widgets/dbg_test_runner.lua +++ b/luaui/Widgets/dbg_test_runner.lua @@ -921,6 +921,7 @@ local function runTestInternal() end local function initializeTestEnvironment() + Spring.Echo("[Test Framework Runner] Initialize test environment") local env = { -- test framework Test = Test, From c823ea174e2acfeb4063d3fbc5ae073a7f6157fc Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 09:48:16 +0100 Subject: [PATCH 66/98] Remove dbg_test_framework widget. --- luaui/Widgets/dbg_test_framework.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua index cd1363a5b13..ee78a7c64a2 100644 --- a/luaui/Widgets/dbg_test_framework.lua +++ b/luaui/Widgets/dbg_test_framework.lua @@ -6,7 +6,7 @@ function widget:GetInfo() license = "GNU GPL, v2 or later", version = 0, layer = 0, - enabled = true, + enabled = false, handler = true, } end From 5bd737ebeb4cc8227db6e4babd0b456bf2a9d391 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 10:11:17 +0100 Subject: [PATCH 67/98] Remove if gl.CreateShader doesn't exist. --- luaui/Widgets/api_unit_tracker_gl4.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/luaui/Widgets/api_unit_tracker_gl4.lua b/luaui/Widgets/api_unit_tracker_gl4.lua index f0f3f3db52c..ac592d3d92a 100644 --- a/luaui/Widgets/api_unit_tracker_gl4.lua +++ b/luaui/Widgets/api_unit_tracker_gl4.lua @@ -738,6 +738,10 @@ function widget:GameStart() end function widget:Initialize() + if not gl.CreateShader then -- no shader support, so just remove the widget itself, especially for headless + widgetHandler:RemoveWidget() + return + end gameFrame = Spring.GetGameFrame() spec, fullview = Spring.GetSpectatingState() myTeamID = Spring.GetMyTeamID() From 2a8a02e094733430cdbe34e905ec2fac416be28e Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 10:12:14 +0100 Subject: [PATCH 68/98] Remove if no gl.CreateShader. --- luaui/Widgets/gfx_deferred_rendering_GL4.lua | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/luaui/Widgets/gfx_deferred_rendering_GL4.lua b/luaui/Widgets/gfx_deferred_rendering_GL4.lua index a5bc8660b1a..c2c45d998d4 100644 --- a/luaui/Widgets/gfx_deferred_rendering_GL4.lua +++ b/luaui/Widgets/gfx_deferred_rendering_GL4.lua @@ -1612,7 +1612,10 @@ function widget:TextCommand(command) end function widget:Initialize() - + if not gl.CreateShader then -- no shader support, so just remove the widget itself, especially for headless + widgetHandler:RemoveWidget() + return + end Spring.Debug.TraceEcho("Initialize DLGL4") if Spring.GetConfigString("AllowDeferredMapRendering") == '0' or Spring.GetConfigString("AllowDeferredModelRendering") == '0' then Spring.Echo('Deferred Rendering (gfx_deferred_rendering.lua) requires AllowDeferredMapRendering and AllowDeferredModelRendering to be enabled in springsettings.cfg!') From 3fd9debadd8e1095188c4098438b8cec4b9d47f2 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 10:38:00 +0100 Subject: [PATCH 69/98] Protect a few more widgets from no gl.CreateShader. --- luaui/Widgets/gfx_deferred_rendering_GL4.lua | 3 +++ luaui/Widgets/gfx_ssao.lua | 8 +++++++- luaui/Widgets/gui_ground_ao_plates_gl4.lua | 4 ++++ 3 files changed, 14 insertions(+), 1 deletion(-) diff --git a/luaui/Widgets/gfx_deferred_rendering_GL4.lua b/luaui/Widgets/gfx_deferred_rendering_GL4.lua index c2c45d998d4..149a9cdc6b0 100644 --- a/luaui/Widgets/gfx_deferred_rendering_GL4.lua +++ b/luaui/Widgets/gfx_deferred_rendering_GL4.lua @@ -1070,6 +1070,9 @@ function widget:VisibleUnitRemoved(unitID) -- remove all the lights for this uni end function widget:Shutdown() + if not gl.CreateShader then + return + end -- TODO: delete the VBOs and shaders like a good boy WG['lightsgl4'] = nil widgetHandler:DeregisterGlobal('AddPointLight') diff --git a/luaui/Widgets/gfx_ssao.lua b/luaui/Widgets/gfx_ssao.lua index ef85e275c9d..b6b93015deb 100644 --- a/luaui/Widgets/gfx_ssao.lua +++ b/luaui/Widgets/gfx_ssao.lua @@ -572,6 +572,10 @@ function widget:ViewResize() end function widget:Initialize() + if not gl.CreateShader then -- no shader support, so just remove the widget itself, especially for headless + widgetHandler:RemoveWidget() + return + end WG['ssao'] = {} WG['ssao'].getPreset = function() return preset @@ -630,7 +634,9 @@ function widget:Update(dt) end function widget:Shutdown() - + if not gl.CreateShader then + return + end -- restore unit lighting settings if presets[preset].tonemapA then Spring.SetConfigFloat("tonemapA", initialTonemapA) diff --git a/luaui/Widgets/gui_ground_ao_plates_gl4.lua b/luaui/Widgets/gui_ground_ao_plates_gl4.lua index 2efc5cd82de..3237edc4462 100644 --- a/luaui/Widgets/gui_ground_ao_plates_gl4.lua +++ b/luaui/Widgets/gui_ground_ao_plates_gl4.lua @@ -87,6 +87,10 @@ function widget:DrawWorldPreUnit() end function widget:Initialize() + if not gl.CreateShader then -- no shader support, so just remove the widget itself, especially for headless + widgetHandler:RemoveWidget() + return + end -- Init texture atlas --makeAtlas() From 248acada61bc12b68ab996d4c31ffaaa01ca2bb5 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 10:45:01 +0100 Subject: [PATCH 70/98] Protect cmd_extractor_snap Shutdown from no gl4. --- luaui/Widgets/cmd_extractor_snap.lua | 3 +++ 1 file changed, 3 insertions(+) diff --git a/luaui/Widgets/cmd_extractor_snap.lua b/luaui/Widgets/cmd_extractor_snap.lua index 7e982dd9df4..852c0064d5f 100644 --- a/luaui/Widgets/cmd_extractor_snap.lua +++ b/luaui/Widgets/cmd_extractor_snap.lua @@ -323,5 +323,8 @@ end function widget:Shutdown() + if not WG.DrawUnitShapeGL4 then + return + end clear() end From e3168bcc9548f911aaeff9616e2c54205bd5c1e3 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 10:49:37 +0100 Subject: [PATCH 71/98] Allow api_blueprint to work even with no gl4. --- luaui/Widgets/api_blueprint.lua | 16 ++++++++++++++-- 1 file changed, 14 insertions(+), 2 deletions(-) diff --git a/luaui/Widgets/api_blueprint.lua b/luaui/Widgets/api_blueprint.lua index 0b6dd3969ba..b855d5909ee 100644 --- a/luaui/Widgets/api_blueprint.lua +++ b/luaui/Widgets/api_blueprint.lua @@ -227,6 +227,9 @@ local function makeInstanceVBO(layout, vertexVBO, numVertices) end local function initGL4() + if not gl.CreateShader then + return + end local outlineVBO, outlineVertices = makeOutlineVBO() outlineInstanceVBO = makeInstanceVBO(outlineInstanceVBOLayout, outlineVBO, outlineVertices) @@ -611,6 +614,9 @@ end ---@param buildPositions StartPoints ---@param teamID number local function updateInstances(blueprint, buildPositions, teamID) + if not gl.CreateShader then + return + end if not blueprint or not buildPositions then clearInstances() return @@ -668,6 +674,9 @@ local function drawOutlines() end function widget:DrawWorldPreUnit() + if not gl.CreateShader then + return + end if not activeBlueprint then return end @@ -727,13 +736,13 @@ end function widget:Initialize() if not gl.CreateShader then -- no shader support, so just remove the widget itself, especially for headless - widgetHandler:RemoveWidget() + -- widgetHandler:RemoveWidget() return end if not initGL4() then -- shader compile failed - widgetHandler:RemoveWidget() + -- widgetHandler:RemoveWidget() return end @@ -757,6 +766,9 @@ end function widget:Shutdown() WG["api_blueprint"] = nil + if not gl.CreateShader then + return + end clearInstances() if outlineInstanceVBO and outlineInstanceVBO.VAO then From fb894ad62122f190670c256d79e95fa86ce276a1 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 10:57:18 +0100 Subject: [PATCH 72/98] Add expectCallin to test_wait. --- luaui/Widgets/TestsExamples/test_utilities/test_wait.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/luaui/Widgets/TestsExamples/test_utilities/test_wait.lua b/luaui/Widgets/TestsExamples/test_utilities/test_wait.lua index c01fb82b6b6..78140bea0c3 100644 --- a/luaui/Widgets/TestsExamples/test_utilities/test_wait.lua +++ b/luaui/Widgets/TestsExamples/test_utilities/test_wait.lua @@ -1,5 +1,6 @@ function setup() Test.clearMap() + Test.expectCallin("UnitCreated") end function cleanup() From 067ac2368ad64596ca806279a3f94337427deaff Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 11:02:07 +0100 Subject: [PATCH 73/98] Give orders with synced run at test_arm_vs_cor_fighters. --- .../balance/test_arm_vs_cor_fighters.lua | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/luaui/Widgets/TestsExamples/balance/test_arm_vs_cor_fighters.lua b/luaui/Widgets/TestsExamples/balance/test_arm_vs_cor_fighters.lua index 7aac181496d..2e6a0e44d28 100644 --- a/luaui/Widgets/TestsExamples/balance/test_arm_vs_cor_fighters.lua +++ b/luaui/Widgets/TestsExamples/balance/test_arm_vs_cor_fighters.lua @@ -52,12 +52,16 @@ function test() Spring.GiveOrderToUnitArray(Spring.GetTeamUnits(0), CMD.FIGHT, { midX, 0, midZ }, 0) Spring.GiveOrderToUnitArray(Spring.GetTeamUnits(1), CMD.FIGHT, { midX, 0, midZ }, 0) else - for _, unitID in ipairs(Spring.GetAllUnits()) do - local ux, uy, uz = Spring.GetUnitPosition(unitID) - - Spring.GiveOrderToUnit(unitID, CMD.FIGHT, { 2 * midX - ux, 0, uz }, 0) - Spring.GiveOrderToUnit(unitID, CMD.FIGHT, { midX, 0, midZ }, { "shift" }) - end + SyncedRun(function() + local midX = locals.midX + local midZ = locals.midZ + for _, unitID in ipairs(Spring.GetAllUnits()) do + local ux, uy, uz = Spring.GetUnitPosition(unitID) + + Spring.GiveOrderToUnit(unitID, CMD.FIGHT, { 2 * midX - ux, 0, uz }, 0) + Spring.GiveOrderToUnit(unitID, CMD.FIGHT, { midX, 0, midZ }, { "shift" }) + end + end) end Spring.SendCommands("setspeed " .. 5) From 360bf54ebcc02cfb0c7a28b2fc1347fe10eacad2 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 16:06:52 +0100 Subject: [PATCH 74/98] Allow declaring capability dependencies at ghInfo and whInfo. --- common/platformFunctions.lua | 50 +++++++++++++++++++++++++ init.lua | 2 + luarules/gadgets.lua | 5 +++ luarules/gadgets/cus_gl4.lua | 1 + luaui/Widgets/gfx_DrawUnitShape_GL4.lua | 5 +-- luaui/Widgets/gfx_decals_gl4.lua | 5 +-- luaui/Widgets/gui_attackrange_gl4.lua | 5 +-- luaui/barwidgets.lua | 7 ++++ 8 files changed, 68 insertions(+), 12 deletions(-) create mode 100644 common/platformFunctions.lua diff --git a/common/platformFunctions.lua b/common/platformFunctions.lua new file mode 100644 index 00000000000..d64baf8a3e3 --- /dev/null +++ b/common/platformFunctions.lua @@ -0,0 +1,50 @@ + +local hasGL4 = false +local hasGL = false +local hasShaders = false +local hasFBO = false +local isSyncedCode = (SendToUnsynced ~= nil) + +local function determineCapabilities() + if not gl.GetString() == "" then + hasGL = true + end + if gl.CreateShader and Platform.glHaveGLSL then + hasShaders = true + end + if gl.CreateFBO then + hasFBO = true + end + + if hasFBO and hasShaders and Platform.glHaveGL4 then + hasGL4 = true + end +end + +local function checkRequires(allRequires) + if not allRequires or isSyncedCode then + return true + end + for _, req in pairs(allRequires) do + if req == 'gl' and not hasGL then + return false + elseif req == 'gl4' and not hasGL4 then + return false + elseif req == 'shaders' and not hasShaders then + return false + elseif req == 'fbo' and not hasVBO then + return false + end + end + return true +end + +local function extendPlatform() + Platform.gl = Platform.gl or hasGL + Platform.gl4 = Platform.gl4 or hasGL4 + Platform.glHaveFBO = Platform.glHaveFBO or hasFBO + Platform.check = checkRequires +end + +determineCapabilities() +extendPlatform() diff --git a/init.lua b/init.lua index 552bdbe696a..1df82255ddc 100644 --- a/init.lua +++ b/init.lua @@ -32,6 +32,8 @@ if commonFunctions.spring[environment] then local springFunctions = VFS.Include('common/springFunctions.lua') Spring.Utilities = Spring.Utilities or springFunctions.Utilities Spring.Debug = Spring.Debug or springFunctions.Debug + -- extend platform + VFS.Include('common/platformFunctions.lua') end if commonFunctions.i18n[environment] then diff --git a/luarules/gadgets.lua b/luarules/gadgets.lua index fdbf7bb2b2b..049b28ae491 100644 --- a/luarules/gadgets.lua +++ b/luarules/gadgets.lua @@ -415,6 +415,11 @@ function gadgetHandler:LoadGadget(filename, overridevfsmode) return nil -- gadget asked for a quiet death end + if gadget.GetInfo and not Platform.check(gadget.GetInfo().depends) then + Spring.Echo('Disabling ' .. gadget:GetInfo().name .. ' for missing capabilities') + return nil + end + -- raw access to gadgetHandler if gadget.GetInfo and gadget:GetInfo().handler then gadget.gadgetHandler = self diff --git a/luarules/gadgets/cus_gl4.lua b/luarules/gadgets/cus_gl4.lua index 19d64c91c8b..195c6d48678 100644 --- a/luarules/gadgets/cus_gl4.lua +++ b/luarules/gadgets/cus_gl4.lua @@ -8,6 +8,7 @@ function gadget:GetInfo() license = "GNU GPL, v2 or later", layer = 0, enabled = true, + depends = {'gl4'}, } end diff --git a/luaui/Widgets/gfx_DrawUnitShape_GL4.lua b/luaui/Widgets/gfx_DrawUnitShape_GL4.lua index 3a85817135f..80392812ba0 100644 --- a/luaui/Widgets/gfx_DrawUnitShape_GL4.lua +++ b/luaui/Widgets/gfx_DrawUnitShape_GL4.lua @@ -8,6 +8,7 @@ function widget:GetInfo() license = "GNU GPL, v2 or later", layer = -9999, enabled = true, + depends = {'gl4'}, } end @@ -438,10 +439,6 @@ if TESTMODE then end function widget:Initialize() - if not gl.CreateShader then -- no shader support, so just remove the widget itself, especially for headless - widgetHandler:RemoveWidget() - return - end for unitDefID, unitDef in pairs(UnitDefs) do if unitDef.model and unitDef.model.textures and unitDef.model.textures.tex1 then unitDefIDtoTex1[unitDefID] = unitDef.model.textures.tex1:lower() diff --git a/luaui/Widgets/gfx_decals_gl4.lua b/luaui/Widgets/gfx_decals_gl4.lua index cf946a6e073..8b982ba559b 100644 --- a/luaui/Widgets/gfx_decals_gl4.lua +++ b/luaui/Widgets/gfx_decals_gl4.lua @@ -7,6 +7,7 @@ function widget:GetInfo() license = "Lua code: GNU GPL, v2 or later, Shader GLSL code: (c) Beherith (mysterme@gmail.com)", layer = 999, enabled = true, + depends = {'gl4'}, } end @@ -1925,10 +1926,6 @@ local function UnitScriptDecal(unitID, unitDefID, whichDecal, posx, posz, headin end function widget:Initialize() - if not gl.CreateShader then -- no shader support, so just remove the widget itself, especially for headless - widgetHandler:RemoveWidget() - return - end local t0 = Spring.GetTimer() --if makeAtlases() == false then -- goodbye("Failed to init texture atlas for DecalsGL4") diff --git a/luaui/Widgets/gui_attackrange_gl4.lua b/luaui/Widgets/gui_attackrange_gl4.lua index 9c7b93b009d..16600f2cbd4 100644 --- a/luaui/Widgets/gui_attackrange_gl4.lua +++ b/luaui/Widgets/gui_attackrange_gl4.lua @@ -12,6 +12,7 @@ function widget:GetInfo() license = "Lua: GPLv2, GLSL: (c) Beherith (mysterme@gmail.com)", layer = -99, enabled = true, + depends = {'gl4'}, } end @@ -670,10 +671,6 @@ function widget:PlayerChanged(playerID) end function widget:Initialize() - if not gl.CreateShader then -- no shader support, so just remove the widget itself, especially for headless - widgetHandler:RemoveWidget(self) - return - end initUnitList() if initGL4() == false then diff --git a/luaui/barwidgets.lua b/luaui/barwidgets.lua index 1ed8e70e471..04788d13a8b 100644 --- a/luaui/barwidgets.lua +++ b/luaui/barwidgets.lua @@ -480,6 +480,13 @@ function widgetHandler:LoadWidget(filename, fromZip, enableLocalsAccess) return nil -- widget asked for a silent death end + if widget.GetInfo then + if not Platform.check(widget:GetInfo().depend) then + Spring.Echo('Disabling ' .. widget:GetInfo().name .. ' for missing capabilities') + return nil + end + end + if enableLocalsAccess then setmetatable(widget, localsAccess.generateLocalsAccessMetatable(getmetatable(widget))) end From c4b70c9de82ea4875e722d21b2a792078f7a5309 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 16:20:56 +0100 Subject: [PATCH 75/98] Print some platform info. --- common/platformFunctions.lua | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/common/platformFunctions.lua b/common/platformFunctions.lua index d64baf8a3e3..dced65af7cd 100644 --- a/common/platformFunctions.lua +++ b/common/platformFunctions.lua @@ -48,3 +48,7 @@ end determineCapabilities() extendPlatform() + +Spring.Echo("PLATFORM", isSyncedCode) +Spring.Echo("PLATFORM GL", hasGL, hasGL4, hasShaders, hasFBO) +Spring.Echo("PLATFORM ATTRS", Platform.glHaveGLSL, Platform.glHaveGL4, gl.GetString(), Platform.glVendor, Platform.glVersion, Platform.glRenderer) From 70d784614b91763cdac8f946a9540768c97e56f4 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 16:39:56 +0100 Subject: [PATCH 76/98] Fixes. --- common/platformFunctions.lua | 7 +++++-- luarules/gadgets.lua | 2 +- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/common/platformFunctions.lua b/common/platformFunctions.lua index dced65af7cd..1d8b3ab9956 100644 --- a/common/platformFunctions.lua +++ b/common/platformFunctions.lua @@ -1,4 +1,4 @@ - +if not Platform then return end local hasGL4 = false local hasGL = false local hasShaders = false @@ -6,7 +6,10 @@ local hasFBO = false local isSyncedCode = (SendToUnsynced ~= nil) local function determineCapabilities() - if not gl.GetString() == "" then + if not gl then + return + end + if gl.GetString(0x1F00) ~= "" then hasGL = true end if gl.CreateShader and Platform.glHaveGLSL then diff --git a/luarules/gadgets.lua b/luarules/gadgets.lua index 049b28ae491..2233011794e 100644 --- a/luarules/gadgets.lua +++ b/luarules/gadgets.lua @@ -415,7 +415,7 @@ function gadgetHandler:LoadGadget(filename, overridevfsmode) return nil -- gadget asked for a quiet death end - if gadget.GetInfo and not Platform.check(gadget.GetInfo().depends) then + if gadget.GetInfo and (Platform and not Platform.check(gadget.GetInfo().depends)) then Spring.Echo('Disabling ' .. gadget:GetInfo().name .. ' for missing capabilities') return nil end From 4f5393cea6b9d7426d54f82e04a350a7da67cf2e Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 16:44:03 +0100 Subject: [PATCH 77/98] Allow declaring capability dependencies at ghInfo and whInfo. --- common/platformFunctions.lua | 53 ++++++++++++++++++++++++++++++++++++ init.lua | 2 ++ luarules/gadgets.lua | 5 ++++ luaui/barwidgets.lua | 7 +++++ 4 files changed, 67 insertions(+) create mode 100644 common/platformFunctions.lua diff --git a/common/platformFunctions.lua b/common/platformFunctions.lua new file mode 100644 index 00000000000..900f4237dc4 --- /dev/null +++ b/common/platformFunctions.lua @@ -0,0 +1,53 @@ +if not Platform then return end +local hasGL4 = false +local hasGL = false +local hasShaders = false +local hasFBO = false +local isSyncedCode = (SendToUnsynced ~= nil) + +local function determineCapabilities() + if not gl then + return + end + if gl.GetString(0x1F00) ~= "" then + hasGL = true + end + if gl.CreateShader and Platform.glHaveGLSL then + hasShaders = true + end + if gl.CreateFBO then + hasFBO = true + end + + if hasFBO and hasShaders and Platform.glHaveGL4 then + hasGL4 = true + end +end + +local function checkRequires(allRequires) + if not allRequires or isSyncedCode then + return true + end + for _, req in pairs(allRequires) do + if req == 'gl' and not hasGL then + return false + elseif req == 'gl4' and not hasGL4 then + return false + elseif req == 'shaders' and not hasShaders then + return false + elseif req == 'fbo' and not hasVBO then + return false + end + end + return true +end + +local function extendPlatform() + Platform.gl = Platform.gl or hasGL + Platform.gl4 = Platform.gl4 or hasGL4 + Platform.glHaveFBO = Platform.glHaveFBO or hasFBO + Platform.check = checkRequires +end + +determineCapabilities() +extendPlatform() diff --git a/init.lua b/init.lua index 552bdbe696a..1df82255ddc 100644 --- a/init.lua +++ b/init.lua @@ -32,6 +32,8 @@ if commonFunctions.spring[environment] then local springFunctions = VFS.Include('common/springFunctions.lua') Spring.Utilities = Spring.Utilities or springFunctions.Utilities Spring.Debug = Spring.Debug or springFunctions.Debug + -- extend platform + VFS.Include('common/platformFunctions.lua') end if commonFunctions.i18n[environment] then diff --git a/luarules/gadgets.lua b/luarules/gadgets.lua index fdbf7bb2b2b..2233011794e 100644 --- a/luarules/gadgets.lua +++ b/luarules/gadgets.lua @@ -415,6 +415,11 @@ function gadgetHandler:LoadGadget(filename, overridevfsmode) return nil -- gadget asked for a quiet death end + if gadget.GetInfo and (Platform and not Platform.check(gadget.GetInfo().depends)) then + Spring.Echo('Disabling ' .. gadget:GetInfo().name .. ' for missing capabilities') + return nil + end + -- raw access to gadgetHandler if gadget.GetInfo and gadget:GetInfo().handler then gadget.gadgetHandler = self diff --git a/luaui/barwidgets.lua b/luaui/barwidgets.lua index 1ed8e70e471..04788d13a8b 100644 --- a/luaui/barwidgets.lua +++ b/luaui/barwidgets.lua @@ -480,6 +480,13 @@ function widgetHandler:LoadWidget(filename, fromZip, enableLocalsAccess) return nil -- widget asked for a silent death end + if widget.GetInfo then + if not Platform.check(widget:GetInfo().depend) then + Spring.Echo('Disabling ' .. widget:GetInfo().name .. ' for missing capabilities') + return nil + end + end + if enableLocalsAccess then setmetatable(widget, localsAccess.generateLocalsAccessMetatable(getmetatable(widget))) end From 2a876c7f4a57577979473ec1a3cfd6cc877a3b3c Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 16:44:43 +0100 Subject: [PATCH 78/98] Add depends stance to a few widgets and gadgets. --- luarules/gadgets/cus_gl4.lua | 1 + luaui/Widgets/gfx_DrawUnitShape_GL4.lua | 5 +---- luaui/Widgets/gfx_decals_gl4.lua | 5 +---- luaui/Widgets/gui_attackrange_gl4.lua | 5 +---- 4 files changed, 4 insertions(+), 12 deletions(-) diff --git a/luarules/gadgets/cus_gl4.lua b/luarules/gadgets/cus_gl4.lua index 19d64c91c8b..195c6d48678 100644 --- a/luarules/gadgets/cus_gl4.lua +++ b/luarules/gadgets/cus_gl4.lua @@ -8,6 +8,7 @@ function gadget:GetInfo() license = "GNU GPL, v2 or later", layer = 0, enabled = true, + depends = {'gl4'}, } end diff --git a/luaui/Widgets/gfx_DrawUnitShape_GL4.lua b/luaui/Widgets/gfx_DrawUnitShape_GL4.lua index 3a85817135f..80392812ba0 100644 --- a/luaui/Widgets/gfx_DrawUnitShape_GL4.lua +++ b/luaui/Widgets/gfx_DrawUnitShape_GL4.lua @@ -8,6 +8,7 @@ function widget:GetInfo() license = "GNU GPL, v2 or later", layer = -9999, enabled = true, + depends = {'gl4'}, } end @@ -438,10 +439,6 @@ if TESTMODE then end function widget:Initialize() - if not gl.CreateShader then -- no shader support, so just remove the widget itself, especially for headless - widgetHandler:RemoveWidget() - return - end for unitDefID, unitDef in pairs(UnitDefs) do if unitDef.model and unitDef.model.textures and unitDef.model.textures.tex1 then unitDefIDtoTex1[unitDefID] = unitDef.model.textures.tex1:lower() diff --git a/luaui/Widgets/gfx_decals_gl4.lua b/luaui/Widgets/gfx_decals_gl4.lua index cf946a6e073..8b982ba559b 100644 --- a/luaui/Widgets/gfx_decals_gl4.lua +++ b/luaui/Widgets/gfx_decals_gl4.lua @@ -7,6 +7,7 @@ function widget:GetInfo() license = "Lua code: GNU GPL, v2 or later, Shader GLSL code: (c) Beherith (mysterme@gmail.com)", layer = 999, enabled = true, + depends = {'gl4'}, } end @@ -1925,10 +1926,6 @@ local function UnitScriptDecal(unitID, unitDefID, whichDecal, posx, posz, headin end function widget:Initialize() - if not gl.CreateShader then -- no shader support, so just remove the widget itself, especially for headless - widgetHandler:RemoveWidget() - return - end local t0 = Spring.GetTimer() --if makeAtlases() == false then -- goodbye("Failed to init texture atlas for DecalsGL4") diff --git a/luaui/Widgets/gui_attackrange_gl4.lua b/luaui/Widgets/gui_attackrange_gl4.lua index 9c7b93b009d..16600f2cbd4 100644 --- a/luaui/Widgets/gui_attackrange_gl4.lua +++ b/luaui/Widgets/gui_attackrange_gl4.lua @@ -12,6 +12,7 @@ function widget:GetInfo() license = "Lua: GPLv2, GLSL: (c) Beherith (mysterme@gmail.com)", layer = -99, enabled = true, + depends = {'gl4'}, } end @@ -670,10 +671,6 @@ function widget:PlayerChanged(playerID) end function widget:Initialize() - if not gl.CreateShader then -- no shader support, so just remove the widget itself, especially for headless - widgetHandler:RemoveWidget(self) - return - end initUnitList() if initGL4() == false then From 7795b32c794843d6eb61d7053a3fb8efe44fba9f Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 16:50:57 +0100 Subject: [PATCH 79/98] Add parameter to gl.GetString --- common/platformFunctions.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/platformFunctions.lua b/common/platformFunctions.lua index 1d8b3ab9956..0ecb022cdc1 100644 --- a/common/platformFunctions.lua +++ b/common/platformFunctions.lua @@ -54,4 +54,4 @@ extendPlatform() Spring.Echo("PLATFORM", isSyncedCode) Spring.Echo("PLATFORM GL", hasGL, hasGL4, hasShaders, hasFBO) -Spring.Echo("PLATFORM ATTRS", Platform.glHaveGLSL, Platform.glHaveGL4, gl.GetString(), Platform.glVendor, Platform.glVersion, Platform.glRenderer) +Spring.Echo("PLATFORM ATTRS", Platform.glHaveGLSL, Platform.glHaveGL4, gl and gl.GetString(0x1F00), Platform.glVendor, Platform.glVersion, Platform.glRenderer) From 11e999102d14250b93b77ff71c818c51f476cbec Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 17:03:39 +0100 Subject: [PATCH 80/98] Move widget capabilities check to InsertWidgetRaw and RemoveWidgetRaw. --- luaui/barwidgets.lua | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/luaui/barwidgets.lua b/luaui/barwidgets.lua index 04788d13a8b..500028352f7 100644 --- a/luaui/barwidgets.lua +++ b/luaui/barwidgets.lua @@ -480,13 +480,6 @@ function widgetHandler:LoadWidget(filename, fromZip, enableLocalsAccess) return nil -- widget asked for a silent death end - if widget.GetInfo then - if not Platform.check(widget:GetInfo().depend) then - Spring.Echo('Disabling ' .. widget:GetInfo().name .. ' for missing capabilities') - return nil - end - end - if enableLocalsAccess then setmetatable(widget, localsAccess.generateLocalsAccessMetatable(getmetatable(widget))) end @@ -918,6 +911,10 @@ function widgetHandler:InsertWidgetRaw(widget) if widget == nil then return end + if widget.GetInfo and not Platform.check(widget:GetInfo().depends) then + Spring.Echo('Disabling ' .. widget:GetInfo().name .. ' for missing capabilities') + return + end SafeWrapWidget(widget) @@ -939,6 +936,9 @@ function widgetHandler:RemoveWidgetRaw(widget) if widget == nil or widget.whInfo == nil then return end + if not Platform.check(widget.whInfo.depends) then + return + end if self.textOwner == widget then self.textOwner = nil From 78f65ce360f38f546994cbf920594716b9b64777 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 17:26:21 +0100 Subject: [PATCH 81/98] Protect api_unit_tracker_gl4 from error in shutdown. --- luaui/Widgets/api_unit_tracker_gl4.lua | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/luaui/Widgets/api_unit_tracker_gl4.lua b/luaui/Widgets/api_unit_tracker_gl4.lua index f0f3f3db52c..b10471db86e 100644 --- a/luaui/Widgets/api_unit_tracker_gl4.lua +++ b/luaui/Widgets/api_unit_tracker_gl4.lua @@ -820,10 +820,12 @@ function widget:Shutdown() numVisibleUnits = 0 - WG['unittrackerapi'].visibleUnits = visibleUnits - WG['unittrackerapi'].visibleUnitsTeam = visibleUnitsTeam - WG['unittrackerapi'].alliedUnits = alliedUnits - WG['unittrackerapi'].alliedUnitsTeam = alliedUnitsTeam + if WG['unittrackerapi'] then + WG['unittrackerapi'].visibleUnits = visibleUnits + WG['unittrackerapi'].visibleUnitsTeam = visibleUnitsTeam + WG['unittrackerapi'].alliedUnits = alliedUnits + WG['unittrackerapi'].alliedUnitsTeam = alliedUnitsTeam + end visibleUnitsChanged() alliedUnitsChanged() From 03dc98c459733bff260b922793f3e1728d5cb081 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 19:12:55 +0100 Subject: [PATCH 82/98] Add something random. --- .../Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua index 0dc98c1ab07..a486914b7a4 100644 --- a/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua +++ b/luaui/Widgets/Tests/gui_selfd_icons/test_gui_selfd_icons_armvp.lua @@ -33,13 +33,13 @@ function test() assert(table.count(widget.queuedSelfD) == 0) -- standard selfd command - Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, 0) + Spring.GiveOrderToUnit(unitID, CMD.SELFD, 0, 0) Test.waitFrames(1) assert(table.count(widget.activeSelfD) == 1) assert(table.count(widget.queuedSelfD) == 0) -- cancel selfd order - Spring.GiveOrderToUnit(unitID, CMD.SELFD, {}, 0) + Spring.GiveOrderToUnit(unitID, CMD.SELFD, 0, 0) Test.waitFrames(1) assert(table.count(widget.activeSelfD) == 0) assert(table.count(widget.queuedSelfD) == 0) From f78c7932beb99d24884c797323748450fe0e41ff Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 19:15:06 +0100 Subject: [PATCH 83/98] Add stance for pull request. --- .github/workflows/run_tests.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 0d9056d95bd..156c8807b1a 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -3,6 +3,8 @@ name: Run Tests on: # pull_request workflow_dispatch: + pull_request: + types: [opened, reopened] jobs: upload-event_file: From 28f6b92580f38461cf56f97d96524ead4f8cd5c4 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 19:56:52 +0100 Subject: [PATCH 84/98] Different empty message. --- common/testing/mocha_json_reporter.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/common/testing/mocha_json_reporter.lua b/common/testing/mocha_json_reporter.lua index 56b4570fd4e..8bc84b236f6 100644 --- a/common/testing/mocha_json_reporter.lua +++ b/common/testing/mocha_json_reporter.lua @@ -47,7 +47,7 @@ function MochaJSONReporter:testResult(label, filePath, success, duration, errorM } else result.err = { - message = "", + message = "", } end end From 4c8281ab20a1076c4fa4822e4fd020c0c78c7523 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 20:19:13 +0100 Subject: [PATCH 85/98] Fix test_wait.lua. --- luaui/Widgets/Tests/example/test_wait.lua | 1 + 1 file changed, 1 insertion(+) diff --git a/luaui/Widgets/Tests/example/test_wait.lua b/luaui/Widgets/Tests/example/test_wait.lua index c01fb82b6b6..06ce4c95cb4 100644 --- a/luaui/Widgets/Tests/example/test_wait.lua +++ b/luaui/Widgets/Tests/example/test_wait.lua @@ -1,5 +1,6 @@ function setup() Test.clearMap() + Text.expectCallin("UnitCreated") end function cleanup() From 8968b7c3bf839df5f90469db38e006ab80ff645f Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 20:23:39 +0100 Subject: [PATCH 86/98] Count skip as pass at least for now. --- luaui/Widgets/dbg_test_runner.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaui/Widgets/dbg_test_runner.lua b/luaui/Widgets/dbg_test_runner.lua index 80aec7d764d..b784d868713 100644 --- a/luaui/Widgets/dbg_test_runner.lua +++ b/luaui/Widgets/dbg_test_runner.lua @@ -81,7 +81,7 @@ local function logTestResult(testResult) testReporter:testResult( testResult.label, testResult.filename, - (testResult.result == TestResults.TEST_RESULT.PASS), + (testResult.result == TestResults.TEST_RESULT.PASS or testResult.result == TestResults.TEST_RESULT.SKIP), testResult.milliseconds, testResult.error ) From afea9eaa8a0e90fa2c6baad6000897512fcccf7c Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 20:37:57 +0100 Subject: [PATCH 87/98] Count skipped independently. --- common/testing/mocha_json_reporter.lua | 9 +++++++-- luaui/Widgets/dbg_test_runner.lua | 3 ++- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/common/testing/mocha_json_reporter.lua b/common/testing/mocha_json_reporter.lua index 8bc84b236f6..3cd16fe8134 100644 --- a/common/testing/mocha_json_reporter.lua +++ b/common/testing/mocha_json_reporter.lua @@ -8,6 +8,7 @@ function MochaJSONReporter:new() local obj = { totalTests = 0, totalPasses = 0, + totalSkipped = 0, totalFailures = 0, startTime = nil, endTime = nil, @@ -28,14 +29,17 @@ function MochaJSONReporter:endTests(duration) self.duration = duration end -function MochaJSONReporter:testResult(label, filePath, success, duration, errorMessage) +function MochaJSONReporter:testResult(label, filePath, success, skipped, duration, errorMessage) local result = { title = label, fullTitle = label, file = filePath, duration = duration, } - if success then + if skipped then + self.totalSkipped = self.totalSkipped + 1 + result.err = {} + elseif success then self.totalPasses = self.totalPasses + 1 result.err = {} else @@ -62,6 +66,7 @@ function MochaJSONReporter:report(filePath) ["suites"] = 1, ["tests"] = self.totalTests, ["passes"] = self.totalPasses, + ["skipped"] = self.totalSkipped, ["pending"] = 0, ["failures"] = self.totalFailures, ["start"] = formatTimestamp(self.startTime), diff --git a/luaui/Widgets/dbg_test_runner.lua b/luaui/Widgets/dbg_test_runner.lua index b784d868713..b5a32924cd1 100644 --- a/luaui/Widgets/dbg_test_runner.lua +++ b/luaui/Widgets/dbg_test_runner.lua @@ -81,7 +81,8 @@ local function logTestResult(testResult) testReporter:testResult( testResult.label, testResult.filename, - (testResult.result == TestResults.TEST_RESULT.PASS or testResult.result == TestResults.TEST_RESULT.SKIP), + (testResult.result == TestResults.TEST_RESULT.PASS), + (testResult.result == TestResults.TEST_RESULT.SKIP), testResult.milliseconds, testResult.error ) From 35016e35be506d17a3587abe5501af635b60d5a8 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 21:01:13 +0100 Subject: [PATCH 88/98] Fix typo. --- luaui/Widgets/Tests/example/test_wait.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaui/Widgets/Tests/example/test_wait.lua b/luaui/Widgets/Tests/example/test_wait.lua index 06ce4c95cb4..78140bea0c3 100644 --- a/luaui/Widgets/Tests/example/test_wait.lua +++ b/luaui/Widgets/Tests/example/test_wait.lua @@ -1,6 +1,6 @@ function setup() Test.clearMap() - Text.expectCallin("UnitCreated") + Test.expectCallin("UnitCreated") end function cleanup() From a3779b8cfb2049199a2ebbba252d2bcc3f3c8a03 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 21:11:27 +0100 Subject: [PATCH 89/98] Remove specific types since defaults are fine. --- .github/workflows/run_tests.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 156c8807b1a..81133d63fd0 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -4,7 +4,6 @@ on: # pull_request workflow_dispatch: pull_request: - types: [opened, reopened] jobs: upload-event_file: From 40c25f5d76e505a727b36e43add8bdc2fc1438f3 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 21:17:37 +0100 Subject: [PATCH 90/98] Don't remove if no gl. --- luaui/Widgets/api_blueprint.lua | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/luaui/Widgets/api_blueprint.lua b/luaui/Widgets/api_blueprint.lua index b855d5909ee..303b31b6e5c 100644 --- a/luaui/Widgets/api_blueprint.lua +++ b/luaui/Widgets/api_blueprint.lua @@ -530,6 +530,9 @@ local BUILD_MODES_HANDLERS = { local instanceIDs = {} local function clearInstances() + if not gl.CreateShader then + return + end clearInstanceTable(outlineInstanceVBO) if WG.StopDrawUnitShapeGL4 then @@ -734,17 +737,7 @@ local function setActiveBuilders(unitIDs) end function widget:Initialize() - if not gl.CreateShader then - -- no shader support, so just remove the widget itself, especially for headless - -- widgetHandler:RemoveWidget() - return - end - - if not initGL4() then - -- shader compile failed - -- widgetHandler:RemoveWidget() - return - end + initGL4() WG["api_blueprint"] = { setActiveBlueprint = setActiveBlueprint, From fa6e9f27399988ccc6cdf7be0dd24235e88f433a Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 21:44:11 +0100 Subject: [PATCH 91/98] SyncedRun for the test_arm_vs_cor_fighters orders. --- .../Tests/balance/test_arm_vs_cor_fighters.lua | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua b/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua index 2e8017434a2..a005c98e781 100644 --- a/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua +++ b/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua @@ -48,12 +48,16 @@ function test() Spring.GiveOrderToUnitArray(Spring.GetTeamUnits(0), CMD.FIGHT, { midX, 0, midZ }, 0) Spring.GiveOrderToUnitArray(Spring.GetTeamUnits(1), CMD.FIGHT, { midX, 0, midZ }, 0) else - for _, unitID in ipairs(Spring.GetAllUnits()) do - local ux, uy, uz = Spring.GetUnitPosition(unitID) - - Spring.GiveOrderToUnit(unitID, CMD.FIGHT, { 2 * midX - ux, 0, uz }, 0) - Spring.GiveOrderToUnit(unitID, CMD.FIGHT, { midX, 0, midZ }, { "shift" }) - end + SyncedRun(function() + local midX = locals.midX + local midZ = locals.midZ + for _, unitID in ipairs(Spring.GetAllUnits()) do + local ux, uy, uz = Spring.GetUnitPosition(unitID) + + Spring.GiveOrderToUnit(unitID, CMD.FIGHT, { 2 * midX - ux, 0, uz }, 0) + Spring.GiveOrderToUnit(unitID, CMD.FIGHT, { midX, 0, midZ }, { "shift" }) + end + end) end Spring.SendCommands("setspeed " .. 5) From f99feab07b5287ccb6787bb591ec30141ea46c0a Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 21:45:24 +0100 Subject: [PATCH 92/98] Remove battle resource tracker test. --- .../test_gui_battle_resource_tracker.lua | 67 ------------------- 1 file changed, 67 deletions(-) delete mode 100644 luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua diff --git a/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua b/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua deleted file mode 100644 index 1b23be6c807..00000000000 --- a/luaui/Widgets/Tests/gui_battle_resource_tracker/test_gui_battle_resource_tracker.lua +++ /dev/null @@ -1,67 +0,0 @@ -local widgetName = "Battle Resource Tracker" - -function setup() - assert(widgetHandler.knownWidgets[widgetName] ~= nil) - - Test.clearMap() - - initialWidgetActive = widgetHandler.knownWidgets[widgetName].active - if initialWidgetActive then - widgetHandler:DisableWidget(widgetName) - end - widgetHandler:EnableWidget(widgetName, true) -end - -function cleanup() - Test.clearMap() - - widgetHandler:DisableWidget(widgetName) - if initialWidgetActive then - widgetHandler:EnableWidget(widgetName, false) - end -end - -function test() - widget = widgetHandler:FindWidget(widgetName) - assert(widget) - - widget.spatialHash:clear() - - combineEventsSpy = Test.spy(widget, "combineEvents") - - local x, z = Game.mapSizeX / 2, Game.mapSizeZ / 2 - local y = Spring.GetGroundHeight(x, z) - local n = 5 - - local unit = "armpw" - - unitM = UnitDefNames[unit].metalCost - unitE = UnitDefNames[unit].energyCost - - SyncedRun(function(locals) - for i = 1, locals.n do - Spring.CreateUnit(locals.unit, locals.x, locals.y, locals.z + 10 * i, 0, 0) - end - end) - - Test.clearMap() - - assert(#(combineEventsSpy.calls) == n - 1, #(combineEventsSpy.calls)) - - events = widget.spatialHash:allEvents() - assert(#events == 1) - - assert(events[1].n == n, events[1].n) - - totalM = 0 - for _, v in pairs(events[1].metal) do - totalM = totalM + v - end - assert(totalM == n * unitM) - - totalE = 0 - for _, v in pairs(events[1].energy) do - totalE = totalE + v - end - assert(totalE == n * unitE) -end From e06d2264bf4f8a45b57295636f6c22c7a23c2961 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 21:46:53 +0100 Subject: [PATCH 93/98] Also run on push to master. --- .github/workflows/run_tests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 81133d63fd0..7514908c476 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -4,6 +4,9 @@ on: # pull_request workflow_dispatch: pull_request: + push: + branches: + - 'master' jobs: upload-event_file: From 2531761db6d9b9b4498fd7313fde03285e2a42f4 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 21:51:52 +0100 Subject: [PATCH 94/98] Add missing locals. --- luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua b/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua index a005c98e781..782878ceb4b 100644 --- a/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua +++ b/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua @@ -48,7 +48,7 @@ function test() Spring.GiveOrderToUnitArray(Spring.GetTeamUnits(0), CMD.FIGHT, { midX, 0, midZ }, 0) Spring.GiveOrderToUnitArray(Spring.GetTeamUnits(1), CMD.FIGHT, { midX, 0, midZ }, 0) else - SyncedRun(function() + SyncedRun(function(locals) local midX = locals.midX local midZ = locals.midZ for _, unitID in ipairs(Spring.GetAllUnits()) do From dd38ac7e4e20e6baf428e49ddda708ac1e98711d Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 21:57:39 +0100 Subject: [PATCH 95/98] Update to use Supreme Isthmus v1.8. --- tools/headless_testing/download-maps.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/headless_testing/download-maps.sh b/tools/headless_testing/download-maps.sh index 8bfa7d8b972..22b139819cf 100644 --- a/tools/headless_testing/download-maps.sh +++ b/tools/headless_testing/download-maps.sh @@ -1,4 +1,5 @@ #!/bin/bash -engine/*/pr-downloader --filesystem-writepath "$BAR_ROOT" --download-map "Full Metal Plate 1.5" -engine/*/pr-downloader --filesystem-writepath "$BAR_ROOT" --download-map "Supreme Isthmus v1.6.4" +#engine/*/pr-downloader --filesystem-writepath "$BAR_ROOT" --download-map "Full Metal Plate 1.5" +engine/*/pr-downloader --filesystem-writepath "$BAR_ROOT" --download-map "Supreme Isthmus v1.8" +#engine/*/pr-downloader --filesystem-writepath "$BAR_ROOT" --download-map "All That Glitters v2.2" From 08dc08940d9b13f37da2d03664102222385344b6 Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 21:57:39 +0100 Subject: [PATCH 96/98] Update to use Supreme Isthmus v1.8. --- tools/headless_testing/download-maps.sh | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/headless_testing/download-maps.sh b/tools/headless_testing/download-maps.sh index 8bfa7d8b972..22b139819cf 100644 --- a/tools/headless_testing/download-maps.sh +++ b/tools/headless_testing/download-maps.sh @@ -1,4 +1,5 @@ #!/bin/bash -engine/*/pr-downloader --filesystem-writepath "$BAR_ROOT" --download-map "Full Metal Plate 1.5" -engine/*/pr-downloader --filesystem-writepath "$BAR_ROOT" --download-map "Supreme Isthmus v1.6.4" +#engine/*/pr-downloader --filesystem-writepath "$BAR_ROOT" --download-map "Full Metal Plate 1.5" +engine/*/pr-downloader --filesystem-writepath "$BAR_ROOT" --download-map "Supreme Isthmus v1.8" +#engine/*/pr-downloader --filesystem-writepath "$BAR_ROOT" --download-map "All That Glitters v2.2" From d206969fd45ade1df692320feb679e1ebd01f0ae Mon Sep 17 00:00:00 2001 From: Saurtron Date: Wed, 18 Dec 2024 22:00:03 +0100 Subject: [PATCH 97/98] Also set Supreme 1.8 at startscript.txt. --- tools/headless_testing/startscript.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/headless_testing/startscript.txt b/tools/headless_testing/startscript.txt index f3b768a22bb..de123d076d9 100644 --- a/tools/headless_testing/startscript.txt +++ b/tools/headless_testing/startscript.txt @@ -1,6 +1,6 @@ [GAME] { - MapName=Supreme Isthmus v1.6.4; + MapName=Supreme Isthmus v1.8; GameType=Beyond All Reason $VERSION; GameStartDelay=0; StartPosType=0; From baa7b50bd2bf8eed57b3d7b2802facebff17daae Mon Sep 17 00:00:00 2001 From: Saurtron Date: Thu, 19 Dec 2024 00:37:10 +0100 Subject: [PATCH 98/98] Add skipped as pending. --- common/testing/mocha_json_reporter.lua | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/common/testing/mocha_json_reporter.lua b/common/testing/mocha_json_reporter.lua index 3cd16fe8134..ce99cdb9207 100644 --- a/common/testing/mocha_json_reporter.lua +++ b/common/testing/mocha_json_reporter.lua @@ -66,8 +66,7 @@ function MochaJSONReporter:report(filePath) ["suites"] = 1, ["tests"] = self.totalTests, ["passes"] = self.totalPasses, - ["skipped"] = self.totalSkipped, - ["pending"] = 0, + ["pending"] = self.totalSkipped, ["failures"] = self.totalFailures, ["start"] = formatTimestamp(self.startTime), ["end"] = formatTimestamp(self.endTime),