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..7514908c476 --- /dev/null +++ b/.github/workflows/run_tests.yml @@ -0,0 +1,38 @@ +name: Run Tests + +on: +# pull_request + workflow_dispatch: + pull_request: + push: + branches: + - 'master' + +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 diff --git a/common/platformFunctions.lua b/common/platformFunctions.lua new file mode 100644 index 00000000000..0ecb022cdc1 --- /dev/null +++ b/common/platformFunctions.lua @@ -0,0 +1,57 @@ +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() + +Spring.Echo("PLATFORM", isSyncedCode) +Spring.Echo("PLATFORM GL", hasGL, hasGL4, hasShaders, hasFBO) +Spring.Echo("PLATFORM ATTRS", Platform.glHaveGLSL, Platform.glHaveGL4, gl and gl.GetString(0x1F00), Platform.glVendor, Platform.glVersion, Platform.glRenderer) diff --git a/common/testing/mocha_json_reporter.lua b/common/testing/mocha_json_reporter.lua index 56b4570fd4e..ce99cdb9207 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 @@ -47,7 +51,7 @@ function MochaJSONReporter:testResult(label, filePath, success, duration, errorM } else result.err = { - message = "", + message = "", } end end @@ -62,7 +66,7 @@ function MochaJSONReporter:report(filePath) ["suites"] = 1, ["tests"] = self.totalTests, ["passes"] = self.totalPasses, - ["pending"] = 0, + ["pending"] = self.totalSkipped, ["failures"] = self.totalFailures, ["start"] = formatTimestamp(self.startTime), ["end"] = formatTimestamp(self.endTime), 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/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/luarules/gadgets/dbg_test_framework_proxy.lua b/luarules/gadgets/dbg_test_framework_proxy.lua new file mode 100644 index 00000000000..73e8b7c8112 --- /dev/null +++ b/luarules/gadgets/dbg_test_framework_proxy.lua @@ -0,0 +1,109 @@ +function gadget:GetInfo() + return { + name = "Test Framework Synced Proxy", + desc = "Proxy for synced commands and code", + 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]") + + 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/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/luarules/testing/localsAccess.lua b/luarules/testing/localsAccess.lua new file mode 100644 index 00000000000..c32a9068c7a --- /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 + return rawget(t, k) + end + end, + __newindex = function(t, k, v) + if t.__localsAccess and t.__localsAccess.setters[k] ~= nil then + return 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/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/luarules/testing/util.lua b/luarules/testing/util.lua new file mode 100644 index 00000000000..80872e4f9a4 --- /dev/null +++ b/luarules/testing/util.lua @@ -0,0 +1,403 @@ +local serpent = serpent or VFS.Include('common/luaUtilities/serpent.lua') + +TEST_RESULT = { + PASS = 1, + FAIL = 2, + SKIP = 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, noColor) + local resultColor + 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(1, 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] .. resetColor + local s = string.format("%s: %s", resultStr, testResult.label) + 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 + 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[#result + 1] = { 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[#result + 1] = { 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 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, localsDictionary))) + 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 { + 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 + 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..782878ceb4b --- /dev/null +++ b/luaui/Widgets/Tests/balance/test_arm_vs_cor_fighters.lua @@ -0,0 +1,97 @@ +function setup() + Test.clearMap() +end + +function cleanup() + Test.clearMap() + + Spring.SendCommands("setspeed " .. 1) +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 + SyncedRun(function(locals) + 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) + + -- 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..336667142ba --- /dev/null +++ b/luaui/Widgets/Tests/balance/test_grunts_vs_pawns.lua @@ -0,0 +1,85 @@ +function skip() + return Game.mapName ~= "Full Metal Plate 1.5" +end + +function setup() + Test.clearMap() +end + +function cleanup() + Test.clearMap() + + Spring.SendCommands("setspeed " .. 1) +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/cmd_stop_selfd/test_cmd_stop_selfd.lua b/luaui/Widgets/Tests/cmd_stop_selfd/test_cmd_stop_selfd.lua index cc27ae58f53..f9d94ac809e 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,5 +1,5 @@ function skip() - return Spring.GetGameFrame() <= 0 + return Game.mapName ~= "Full Metal Plate 1.5" or Spring.GetGameFrame() <= 0 end function setup() 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 diff --git a/luaui/Widgets/Tests/example/test_wait.lua b/luaui/Widgets/Tests/example/test_wait.lua new file mode 100644 index 00000000000..78140bea0c3 --- /dev/null +++ b/luaui/Widgets/Tests/example/test_wait.lua @@ -0,0 +1,36 @@ +function setup() + Test.clearMap() + Test.expectCallin("UnitCreated") +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) + + Spring.Echo("[test_wait] waiting 1000 ms") + Test.waitTime(1000) +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 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) diff --git a/luaui/Widgets/Tests/mex-building/pregame_mex_queue.lua b/luaui/Widgets/Tests/mex-building/pregame_mex_queue.lua index 30668834fa8..1adda50e002 100644 --- a/luaui/Widgets/Tests/mex-building/pregame_mex_queue.lua +++ b/luaui/Widgets/Tests/mex-building/pregame_mex_queue.lua @@ -6,10 +6,10 @@ end function setup() Test.clearMap() - local widget_cmd_extractor_snap = widgetHandler:FindWidget("Extractor Snap (mex/geo)") + widget_cmd_extractor_snap = widgetHandler:FindWidget("Extractor Snap (mex/geo)") assert(widget_cmd_extractor_snap) - local widget_gui_pregame_build = widgetHandler:FindWidget("Pregame Queue") + widget_gui_pregame_build = widgetHandler:FindWidget("Pregame Queue") assert(widget_gui_pregame_build) WG['pregame-build'].setBuildQueue({}) 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) 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() diff --git a/luaui/Widgets/api_blueprint.lua b/luaui/Widgets/api_blueprint.lua index 0b6dd3969ba..303b31b6e5c 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) @@ -527,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 @@ -611,6 +617,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 +677,9 @@ local function drawOutlines() end function widget:DrawWorldPreUnit() + if not gl.CreateShader then + return + end if not activeBlueprint then return end @@ -725,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, @@ -757,6 +759,9 @@ end function widget:Shutdown() WG["api_blueprint"] = nil + if not gl.CreateShader then + return + end clearInstances() if outlineInstanceVBO and outlineInstanceVBO.VAO then diff --git a/luaui/Widgets/api_unit_tracker_gl4.lua b/luaui/Widgets/api_unit_tracker_gl4.lua index f0f3f3db52c..48522bb833c 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() @@ -820,10 +824,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() 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 diff --git a/luaui/Widgets/dbg_test_framework.lua b/luaui/Widgets/dbg_test_framework.lua new file mode 100644 index 00000000000..ee78a7c64a2 --- /dev/null +++ b/luaui/Widgets/dbg_test_framework.lua @@ -0,0 +1,916 @@ +function widget:GetInfo() + return { + name = "Test Framework Runner", + desc = "Run tests with: /runtests ...", + date = "2023", + license = "GNU GPL, v2 or later", + version = 0, + layer = 0, + enabled = false, + handler = true, + } +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 + +local RETURN_TIMEOUT = 30 +local DEFAULT_WAIT_TIMEOUT = 5 * 30 + +local SHOW_ALL_RESULTS = true + +local noColorOutput = false +local quitWhenDone = false +local gameStartTestPatterns = nil +local testResultsFilePath = nil +local testReporter = nil + +-- utils +-- ===== +local function log(level, str, ...) + if level < LOG_LEVEL then + return + end + Spring.Log( + widget:GetInfo().name, + LOG.NOTICE, + str + ) +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 +-- ========= + +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(relativePath) + 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, noColorOutput)) + end +end + +local gameTimer +local runTestsTimer +local testTimer + +local function getGameTime() + if gameTimer ~= nil then + return Spring.DiffTimers(Spring.GetTimer(), gameTimer, true) + end +end + +local function getRunTestsTime() + if runTestsTimer ~= nil then + return Spring.DiffTimers(Spring.GetTimer(), runTestsTimer, true) + end +end + +local function getTestTime() + if testTimer ~= nil then + return Spring.DiffTimers(Spring.GetTimer(), testTimer, true) + 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, + filename = nil, + label = nil, + } +end + +local function resetResumeState() + log(LOG.DEBUG, "[resetResumeState]") + resumeState = { + predicate = nil, + timeoutExpireFrame = nil, + } +end + +local function resetReturnState() + log(LOG.DEBUG, "[resetReturnState]") + returnState = { + waitingForReturnId = nil, + success = nil, + pendingValueOrError = nil, + timeoutExpireFrame = nil, + } +end + +local function resetCallinState() + log(LOG.DEBUG, "[resetCallinState]") + callinState = { + buffer = {}, + } +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[name]) + 1] = args +end) + +local function startTests(patterns) + log(LOG.DEBUG, "[startTests] " .. table.toString({ + patterns = patterns, + })) + + if testRunState.runningTests then + log(LOG.WARNING, "Tests are already running!") + return + end + + logStartTests() + + 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=====") + + runTestsTimer = Spring.GetTimer() +end + +local function finishTest(result) + for _, control in ipairs(spyControls) do + 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 + result.milliseconds = getTestTime() + + log(LOG.NOTICE, formatTestResult(result, noColorOutput)) + + logTestResult(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 + + logEndTests(getRunTestsTime()) + + if quitWhenDone then + Spring.SendCommands("quitforce") + 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, + timeoutExpireFrame = Spring.GetGameFrame() + RETURN_TIMEOUT, + } + + log(LOG.DEBUG, "[createNestedProxy." .. prefix .. ".send]") + 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, + timeoutExpireFrame = Spring.GetGameFrame() + RETURN_TIMEOUT, + } + + log(LOG.DEBUG, "[SyncedRun.send]") + 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, + timeoutExpireFrame = Spring.GetGameFrame() + timeout, + } + + local resumeOk, resumeResult = coroutine.yield() + + log(LOG.DEBUG, "[waitUntil.return] " .. table.toString({ + resumeOk = resumeOk, + resumeResult = resumeResult, + })) + + resetResumeState() + + if not resumeOk then + error(resumeResult, 2 + (errorOffset or 0)) + end + end, + waitFrames = function(frames) + log(LOG.DEBUG, "[waitFrames] " .. frames) + local startFrame = Spring.GetGameFrame() + Test.waitUntil( + function() + return Spring.GetGameFrame() >= (startFrame + frames) + end, + 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() + return Spring.DiffTimers(Spring.GetTimer(), startTimer, true) >= milliseconds + end, + 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 + if predicate == nil or predicate(unpack(args)) then + return true + end + end + return false + end, + timeout, + 1 + ) + 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 + end + return true + end) + end, + spy = function(...) + local spyCtrl = spy(...) + 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 + callinState.buffer[name] = {} + else + callinState.buffer = {} + end + end, +} + +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, + timeoutExpireFrame = nil, + } + end + end +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]") + 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 + + return TEST_RESULT.PASS +end + +local function initializeTestEnvironment() + local env = { + -- test framework + Test = Test, + SyncedProxy = SyncedProxy, + SyncedRun = SyncedRun, + __runTestInternal = runTestInternal, + yieldable_pcall = yieldable_pcall, + TEST_RESULT = TEST_RESULT, + + -- widgets + widgetHandler = widgetHandler, + WG = WG, + + -- 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, + } + + for k, v in pairs(assertions) do + env[k] = v + end + + 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() + if returnState.success == nil and returnState.waitingForReturnId == nil then + -- no return to handle, so just continue + return { + status = "continue", + } + end + + 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 { + status = "error", + error = "waiting for synced return timed out" + } + end + end + + if returnState.waitingForReturnId then + 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() + if resumeState.predicate == nil then + return { + status = "continue", + } + end + + if Spring.GetGameFrame() >= resumeState.timeoutExpireFrame 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 + 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 + return { + status = "wait" + } + end + else + -- error during predicate + log(LOG.DEBUG, "[handleWait] predicate error -> error") + return { + status = "error", + error = returnOrError + } + end +end + +local function step() + if not testRunState.runningTests then + return + end + + --*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() + + if returnResult.status == "wait" then + return + end + + local waitResult = handleWait() + + 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.filename = testRunState.files[testRunState.index].filename + + local success, envOrError = loadTestFromFile(activeTestState.filename) + + if success then + log(LOG.DEBUG, "Initializing test: " .. activeTestState.label) + activeTestState.environment = envOrError + activeTestState.coroutine = coroutine.create(activeTestState.environment.__runTestInternal) + activeTestState.startFrame = Spring.GetGameFrame() + + testTimer = Spring.GetTimer() + else + finishTest({ + result = TEST_RESULT.ERROR, + error = envOrError + }) + return + end + end + + -- resume the test + local resumeOk, resumeResult + 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, + }) + ) + + resumeOk, resumeResult = coroutine.resume(activeTestState.coroutine, coroutineOk, coroutineArgs) + log(LOG.DEBUG, "Unresuming test: " .. table.toString({ + result = resumeOk, + error = resumeResult, + })) + if not resumeOk then + -- test fail + finishTest({ + result = TEST_RESULT.FAIL, + error = resumeResult, + }) + return + end + end + + if coroutine.status(activeTestState.coroutine) == "dead" then + -- test did not fail or error, so may have been pass or skip + finishTest({ + result = resumeResult or TEST_RESULT.PASS, + }) + end +end + +function widget:GameFrame(frame) + if gameStartTestPatterns ~= nil and frame >= 0 then + startTests(gameStartTestPatterns) + gameStartTestPatterns = nil + end + + step() +end + +function widget:Update(dt) + if Spring.GetGameFrame() <= 0 then + step() + end +end + +function widget:Initialize() + widgetHandler:DisableWidget("Test Framework Watchdog") + if not Spring.Utilities.IsDevMode() then + widgetHandler:RemoveWidget(self) + end + + widgetHandler.actionHandler:AddAction( + self, + "runtests", + function(cmd, optLine, optWords, data, isRepeat, release, actions) + startTests(splitPhrases(optLine)) + end, + nil, + "t" + ) + widgetHandler.actionHandler:AddAction( + self, + "runtestsheadless", + function(cmd, optLine, optWords, data, isRepeat, release, actions) + noColorOutput = true + quitWhenDone = true + gameStartTestPatterns = splitPhrases(optLine) + testResultsFilePath = "testlog/results.json" + + widgetHandler:EnableWidget("Test Framework Watchdog") + end, + nil, + "t" + ) + + gameTimer = Spring.GetTimer() +end + +function widget:Shutdown() + widgetHandler.actionHandler:RemoveAction("runtests", "t") + widgetHandler.actionHandler:RemoveAction("runtestsheadless", "t") +end diff --git a/luaui/Widgets/dbg_test_framework_watchdog.lua b/luaui/Widgets/dbg_test_framework_watchdog.lua new file mode 100644 index 00000000000..9564b80c142 --- /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 diff --git a/luaui/Widgets/dbg_test_runner.lua b/luaui/Widgets/dbg_test_runner.lua index 2c4bada6f2d..b5a32924cd1 100644 --- a/luaui/Widgets/dbg_test_runner.lua +++ b/luaui/Widgets/dbg_test_runner.lua @@ -82,6 +82,7 @@ local function logTestResult(testResult) testResult.label, testResult.filename, (testResult.result == TestResults.TEST_RESULT.PASS), + (testResult.result == TestResults.TEST_RESULT.SKIP), testResult.milliseconds, testResult.error ) @@ -921,6 +922,7 @@ local function runTestInternal() end local function initializeTestEnvironment() + Spring.Echo("[Test Framework Runner] Initialize test environment") local env = { -- test framework Test = Test, 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/gfx_deferred_rendering_GL4.lua b/luaui/Widgets/gfx_deferred_rendering_GL4.lua index a5bc8660b1a..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') @@ -1612,7 +1615,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!') 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/gfx_unit_stencil_gl4.lua b/luaui/Widgets/gfx_unit_stencil_gl4.lua index 7005bcf96ca..cd809992c07 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 @@ -392,6 +393,9 @@ end function widget:Initialize() unitStencilShader = InitDrawPrimitiveAtUnit(shaderConfig, "unitStencils") + if not unitStencilShader then + return + end widget:ViewResize() WG['unitstencilapi'] = {} 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/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() diff --git a/luaui/barwidgets.lua b/luaui/barwidgets.lua index 1ed8e70e471..2b391461b33 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 @@ -911,6 +918,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) @@ -932,6 +943,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 diff --git a/tools/headless_testing/download-maps.sh b/tools/headless_testing/download-maps.sh index 8901c70e6f9..22b139819cf 100644 --- a/tools/headless_testing/download-maps.sh +++ b/tools/headless_testing/download-maps.sh @@ -1,3 +1,5 @@ #!/bin/bash -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" diff --git a/tools/headless_testing/startscript.txt b/tools/headless_testing/startscript.txt index e41b9cf9f84..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; @@ -10,7 +10,7 @@ FixedRNGSeed = 1; [MODOPTIONS] { - debugcommands=1:cheat|2:godmode|3:globallos|30:runtestsheadless; + debugcommands=1:cheat|2:godmode|30:runtestsheadless; deathmode=neverend; } [ALLYTEAM0]