1
0
mirror of https://gitlab.com/OpenMW/openmw.git synced 2025-03-14 19:21:50 +00:00
elsid 0e19b1dd75
Run Lua integration tests starting with menu script
This allows writing tests for menu scripts.

Keep global script as entry point to morrowind tests.

Fix menu.newGame and menu.loadGame to hide main menu.
2025-03-08 13:14:20 +01:00

313 lines
8.6 KiB
Lua

local core = require('openmw.core')
local util = require('openmw.util')
local M = {}
local menuTestsOrder = {}
local menuTests = {}
local globalTestsOrder = {}
local globalTests = {}
local globalTestRunner = nil
local currentGlobalTest = nil
local currentGlobalTestError = nil
local localTests = {}
local localTestRunner = nil
local currentLocalTest = nil
local currentLocalTestError = nil
local function makeTestCoroutine(fn)
local co = coroutine.create(fn)
return function()
if coroutine.status(co) ~= 'dead' then
coroutine.resume(co)
end
end
end
local function runTests(tests)
for i, test in ipairs(tests) do
local name, fn = unpack(test)
print('TEST_START', i, name)
local status, err = pcall(fn)
if status then
print('TEST_OK', i, name)
else
print('TEST_FAILED', i, name, err)
end
end
core.quit()
end
function M.makeUpdateMenu()
return makeTestCoroutine(function()
print('Running menu tests...')
runTests(menuTestsOrder)
end)
end
function M.makeUpdateGlobal()
return makeTestCoroutine(function()
print('Running global tests...')
runTests(globalTestsOrder)
end)
end
function M.registerMenuTest(name, fn)
menuTests[name] = fn
table.insert(menuTestsOrder, {name, fn})
end
function M.runGlobalTest(name)
currentGlobalTest = name
currentGlobalTestError = nil
core.sendGlobalEvent('runGlobalTest', name)
while currentGlobalTest do
coroutine.yield()
end
if currentGlobalTestError then
error(currentGlobalTestError, 2)
end
end
function M.registerGlobalTest(name, fn)
globalTests[name] = fn
table.insert(globalTestsOrder, {name, fn})
end
function M.updateGlobal()
if globalTestRunner and coroutine.status(globalTestRunner) ~= 'dead' then
coroutine.resume(globalTestRunner)
else
globalTestRunner = nil
end
end
function M.runLocalTest(obj, name)
currentLocalTest = name
currentLocalTestError = nil
obj:sendEvent('runLocalTest', name)
while currentLocalTest do
coroutine.yield()
end
if currentLocalTestError then
error(currentLocalTestError, 2)
end
end
function M.registerLocalTest(name, fn)
localTests[name] = fn
end
function M.updateLocal()
if localTestRunner and coroutine.status(localTestRunner) ~= 'dead' then
if not core.isWorldPaused() then
coroutine.resume(localTestRunner)
end
else
localTestRunner = nil
end
end
function M.expect(cond, msg)
if not cond then
error(msg or '"true" expected', 2)
end
end
function M.expectEqualWithDelta(v1, v2, delta, msg)
if math.abs(v1 - v2) > delta then
error(string.format('%s: %f ~= %f', msg or '', v1, v2), 2)
end
end
function M.expectAlmostEqual(v1, v2, msg)
if math.abs(v1 - v2) / (math.abs(v1) + math.abs(v2)) > 0.05 then
error(string.format('%s: %f ~= %f', msg or '', v1, v2), 2)
end
end
function M.expectGreaterOrEqual(v1, v2, msg)
if not (v1 >= v2) then
error(string.format('%s: %f >= %f', msg or '', v1, v2), 2)
end
end
function M.expectGreaterThan(v1, v2, msg)
if not (v1 > v2) then
error(string.format('%s: %s > %s', msg or '', v1, v2), 2)
end
end
function M.expectLessOrEqual(v1, v2, msg)
if not (v1 <= v2) then
error(string.format('%s: %s <= %s', msg or '', v1, v2), 2)
end
end
function M.expectEqual(v1, v2, msg)
if not (v1 == v2) then
error(string.format('%s: %s ~= %s', msg or '', v1, v2), 2)
end
end
function M.expectNotEqual(v1, v2, msg)
if v1 == v2 then
error(string.format('%s: %s == %s', msg or '', v1, v2), 2)
end
end
function M.closeToVector(expected, maxDistance)
return function(actual)
local distance = (expected - actual):length()
if distance <= maxDistance then
return ''
end
return string.format('%s is too far from expected %s: %s > %s', actual, expected, distance, maxDistance)
end
end
---
-- Matcher verifying that given value is an array each element of which matches elements of expected.
-- @function elementsAreArray
-- @param expected#array of values or matcher functions.
-- @usage
-- local t = {42, 13}
-- local matcher = function(actual)
-- if actual ~= 42 then
-- return string.format('%s is not 42', actual)
-- end
-- return ''
-- end
-- expectThat({42, 13}, elementsAreArray({matcher, 13}))
function M.elementsAreArray(expected)
local expected_matchers = {}
for i, v in ipairs(expected) do
if type(v) == 'function' then
expected_matchers[i] = v
else
expected_matchers[i] = function (other)
if expected[i].__eq(expected[i], other) then
return ''
end
return string.format('%s element %s does no match expected: %s', i, other, expected[i])
end
end
end
return function(actual)
if #actual < #expected_matchers then
return string.format('number of elements is less than expected: %s < %s', #actual, #expected_matchers)
end
local message = ''
for i, v in ipairs(actual) do
if i > #expected_matchers then
message = string.format('%s\n%s element is out of expected range: %s', message, i, #expected_matchers)
break
end
local match_message = expected_matchers[i](v)
if match_message ~= '' then
message = string.format('%s\n%s', message, match_message)
end
end
return message
end
end
---
-- Matcher verifying that given number is not a nan.
-- @function isNotNan
-- @usage
-- expectThat(value, isNotNan())
function M.isNotNan(expected)
return function(actual)
if actual ~= actual then
return 'actual value is nan, expected to be not nan'
end
return ''
end
end
---
-- Verifies that given value matches provided matcher.
-- @function expectThat
-- @param value#any any value to match.
-- @param matcher#function a function returing empty string in the case of success or a message explaining the mismatch.
-- @param msg#string a message to prefix failure reason.
-- @usage
-- local matcher = function(actual)
-- if actual == 42 then
-- return ''
-- end
-- return string.format('%s is not 42', actual)
-- end
-- expectThat(42, matcher)
function M.expectThat(value, matcher, msg)
local message = matcher(value)
if message ~= '' then
error(string.format('%s: actual does not match expected: %s', msg or 'Failure', message), 2)
end
end
function M.formatActualExpected(actual, expected)
return string.format('actual: %s, expected: %s', actual, expected)
end
-- used only in menu scripts
M.menuEventHandlers = {
globalTestFinished = function(data)
if data.name ~= currentGlobalTest then
error(string.format('globalTestFinished with incorrect name %s, expected %s', data.name, currentGlobalTest), 2)
end
currentGlobalTest = nil
currentGlobalTestError = data.errMsg
end,
}
-- used only in global scripts
M.globalEventHandlers = {
runGlobalTest = function(name)
fn = globalTests[name]
local types = require('openmw.types')
local world = require('openmw.world')
if not fn then
types.Player.sendMenuEvent(world.players[1], 'globalTestFinished', {name=name, errMsg='Global test is not found'})
return
end
globalTestRunner = coroutine.create(function()
local status, err = pcall(fn)
if status then
err = nil
end
types.Player.sendMenuEvent(world.players[1], 'globalTestFinished', {name=name, errMsg=err})
end)
end,
localTestFinished = function(data)
if data.name ~= currentLocalTest then
error(string.format('localTestFinished with incorrect name %s, expected %s', data.name, currentLocalTest), 2)
end
currentLocalTest = nil
currentLocalTestError = data.errMsg
end,
}
-- used only in local scripts
M.localEventHandlers = {
runLocalTest = function(name)
fn = localTests[name]
if not fn then
core.sendGlobalEvent('localTestFinished', {name=name, errMsg='Local test is not found'})
return
end
localTestRunner = coroutine.create(function()
local status, err = pcall(fn)
if status then
err = nil
end
core.sendGlobalEvent('localTestFinished', {name=name, errMsg=err})
end)
end,
}
return M