Merge branch 'lua_save_load_test' into 'master'
Some checks failed
Build and test / MacOS (push) Has been cancelled
Build and test / Read .env file and expose it as output (push) Has been cancelled
Build and test / Ubuntu (push) Has been cancelled
Build and test / Windows (2019) (push) Has been cancelled
Build and test / Windows (2022) (push) Has been cancelled

Add Lua integration tests for loading and saving

See merge request OpenMW/openmw!4604
This commit is contained in:
psi29a 2025-03-27 11:05:12 +00:00
commit 72aefbf191
6 changed files with 324 additions and 105 deletions

View file

@ -326,6 +326,24 @@ testing.registerGlobalTest('player weapon attack', function()
testing.runLocalTest(player, 'player weapon attack')
end)
testing.registerGlobalTest('load while teleporting - init player', function()
local player = world.players[1]
player:teleport('Museum of Wonders', util.vector3(0, -1500, 111), util.transform.rotateZ(math.rad(180)))
end)
testing.registerGlobalTest('load while teleporting - teleport', function()
local player = world.players[1]
local landracer = world.createObject('landracer')
landracer:teleport(player.cell, player.position + util.vector3(0, 500, 0))
coroutine.yield()
local door = world.getObjectByFormId(core.getFormId('the_hub.omwaddon', 26))
door:activateBy(player)
coroutine.yield()
landracer:teleport(player.cell, player.position)
end)
return {
engineHandlers = {
onUpdate = testing.updateGlobal,

View file

@ -1,12 +1,63 @@
local testing = require('testing_util')
local matchers = require('matchers')
local menu = require('openmw.menu')
testing.registerMenuTest('save and load', function()
menu.newGame()
coroutine.yield()
menu.saveGame('save and load')
coroutine.yield()
local directorySaves = {}
directorySaves['save_and_load.omwsave'] = {
playerName = '',
playerLevel = 1,
timePlayed = 0,
description = 'save and load',
contentFiles = {
'builtin.omwscripts',
'template.omwgame',
'landracer.omwaddon',
'the_hub.omwaddon',
'test_lua_api.omwscripts',
},
creationTime = matchers.isAny(),
}
local expectedAllSaves = {}
expectedAllSaves[' - 1'] = directorySaves
testing.expectThat(menu.getAllSaves(), matchers.equalTo(expectedAllSaves))
menu.loadGame(' - 1', 'save_and_load.omwsave')
coroutine.yield()
menu.deleteGame(' - 1', 'save_and_load.omwsave')
testing.expectThat(menu.getAllSaves(), matchers.equalTo({}))
end)
testing.registerMenuTest('load while teleporting', function()
menu.newGame()
coroutine.yield()
testing.runGlobalTest('load while teleporting - init player')
menu.saveGame('load while teleporting')
coroutine.yield()
testing.runGlobalTest('load while teleporting - teleport')
menu.loadGame(' - 1', 'load_while_teleporting.omwsave')
coroutine.yield()
menu.deleteGame(' - 1', 'load_while_teleporting.omwsave')
end)
local function registerGlobalTest(name, description)
testing.registerMenuTest(description or name, function()
menu.newGame()
coroutine.yield()
testing.runGlobalTest(name)
end)
testing.registerMenuTest(description or name, function()
menu.newGame()
coroutine.yield()
testing.runGlobalTest(name)
end)
end
registerGlobalTest('timers')

View file

@ -6,6 +6,7 @@ local input = require('openmw.input')
local types = require('openmw.types')
local nearby = require('openmw.nearby')
local camera = require('openmw.camera')
local matchers = require('matchers')
types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Controls, false)
types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Fighting, false)
@ -113,13 +114,13 @@ testing.registerLocalTest('player rotation',
coroutine.yield()
local alpha1, gamma1 = self.rotation:getAnglesXZ()
testing.expectThat(alpha1, testing.isNotNan(), 'Alpha rotation in XZ convention is nan')
testing.expectThat(gamma1, testing.isNotNan(), 'Gamma rotation in XZ convention is nan')
testing.expectThat(alpha1, matchers.isNotNan(), 'Alpha rotation in XZ convention is nan')
testing.expectThat(gamma1, matchers.isNotNan(), 'Gamma rotation in XZ convention is nan')
local alpha2, beta2, gamma2 = self.rotation:getAnglesZYX()
testing.expectThat(alpha2, testing.isNotNan(), 'Alpha rotation in ZYX convention is nan')
testing.expectThat(beta2, testing.isNotNan(), 'Beta rotation in ZYX convention is nan')
testing.expectThat(gamma2, testing.isNotNan(), 'Gamma rotation in ZYX convention is nan')
testing.expectThat(alpha2, matchers.isNotNan(), 'Alpha rotation in ZYX convention is nan')
testing.expectThat(beta2, matchers.isNotNan(), 'Beta rotation in ZYX convention is nan')
testing.expectThat(gamma2, matchers.isNotNan(), 'Gamma rotation in ZYX convention is nan')
end
end)

View file

@ -0,0 +1,218 @@
local module = {}
---
-- Matcher verifying that distance between given value and expected is not greater than maxDistance.
-- @function elementsAreArray
-- @param expected#vector.
-- @usage
-- expectThat(util.vector2(0, 0), closeToVector(util.vector2(0, 1), 1))
function module.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 module.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 module.isNotNan()
return function(actual)
if actual ~= actual then
return 'actual value is nan, expected to be not nan'
end
return ''
end
end
---
-- Matcher accepting any value.
-- @function isAny
-- @usage
-- expectThat(value, isAny())
function module.isAny()
return function(actual)
return ''
end
end
local function serializeArray(a)
local result = nil
for _, v in ipairs(a) do
if result == nil then
result = string.format('{%s', serialize(v))
else
result = string.format('%s, %s', result, serialize(v))
end
end
if result == nil then
return '{}'
end
return string.format('%s}', result)
end
local function serializeTable(t)
local result = nil
for k, v in pairs(t) do
if result == nil then
result = string.format('{%q = %s', k, serialize(v))
else
result = string.format('%s, %q = %s', result, k, serialize(v))
end
end
if result == nil then
return '{}'
end
return string.format('%s}', result)
end
local function isArray(t)
local i = 1
for _ in pairs(t) do
if t[i] == nil then
return false
end
i = i + 1
end
return true
end
function serialize(v)
local t = type(v)
if t == 'string' then
return string.format('%q', v)
elseif t == 'table' then
if isArray(v) then
return serializeArray(v)
end
return serializeTable(v)
end
return string.format('%s', v)
end
local function compareScalars(v1, v2)
if v1 == v2 then
return ''
end
if type(v1) == 'string' then
return string.format('%q ~= %q', v1, v2)
end
return string.format('%s ~= %s', v1, v2)
end
local function collectKeys(t)
local result = {}
for key in pairs(t) do
table.insert(result, key)
end
table.sort(result)
return result
end
local function compareTables(t1, t2)
local keys1 = collectKeys(t1)
local keys2 = collectKeys(t2)
if #keys1 ~= #keys2 then
return string.format('table size mismatch: %d ~= %d', #keys1, #keys2)
end
for i = 1, #keys1 do
local key1 = keys1[i]
local key2 = keys2[i]
if key1 ~= key2 then
return string.format('table keys mismatch: %q ~= %q', key1, key2)
end
local d = compare(t1[key1], t2[key2])
if d ~= '' then
return string.format('table values mismatch at key %s: %s', serialize(key1), d)
end
end
return ''
end
function compare(v1, v2)
local type1 = type(v1)
local type2 = type(v2)
if type2 == 'function' then
return v2(v1)
end
if type1 ~= type2 then
return string.format('types mismatch: %s ~= %s', type1, type2)
end
if type1 == 'nil' then
return ''
elseif type1 == 'table' then
return compareTables(v1, v2)
elseif type1 == 'nil' or type1 == 'boolean' or type1 == 'number' or type1 == 'string' then
return compareScalars(v1, v2)
end
error('unsupported type: %s', type1)
end
---
-- Matcher verifying that given value is equal to expected. Accepts nil, boolean, number, string and table or matcher
-- function.
-- @function equalTo
-- @usage
-- expectThat({a = {42, 'foo', {b = true}}}, equalTo({a = {42, 'foo', {b = true}}}))
function module.equalTo(expected)
return function(actual)
local diff = compare(actual, expected)
if diff == '' then
return ''
end
return string.format('%s; actual: %s; expected: %s', diff, serialize(actual, ''), serialize(expected, ''))
end
end
return module

View file

@ -158,76 +158,6 @@ function M.expectNotEqual(v1, v2, msg)
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

View file

@ -5,6 +5,7 @@ local testing = require('testing_util')
local util = require('openmw.util')
local types = require('openmw.types')
local nearby = require('openmw.nearby')
local matchers = require('matchers')
types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Fighting, false)
types.Player.setControlSwitch(self, types.Player.CONTROL_SWITCH.Jumping, false)
@ -45,33 +46,33 @@ testing.registerLocalTest('Guard in Imperial Prison Ship should find path (#7241
testing.expectLessOrEqual((util.vector2(path[#path].x, path[#path].y) - util.vector2(dst.x, dst.y)):length(), 1, 'Last path point x, y')
testing.expectLessOrEqual(path[#path].z - dst.z, 20, 'Last path point z')
if agentBounds.shapeType == nearby.COLLISION_SHAPE_TYPE.Aabb then
testing.expectThat(path, testing.elementsAreArray({
testing.closeToVector(util.vector3(34.29737091064453125, 806.3817138671875, 112.76610565185546875), 1e-1),
testing.closeToVector(util.vector3(15, 1102, 112.2945709228515625), 1e-1),
testing.closeToVector(util.vector3(-112, 1110, 112.2945709228515625), 1e-1),
testing.closeToVector(util.vector3(-118, 1393, 112.2945709228515625), 1e-1),
testing.closeToVector(util.vector3(-67.99993896484375, 1421.2000732421875, 112.2945709228515625), 1e-1),
testing.closeToVector(util.vector3(-33.999935150146484375, 1414.4000244140625, 112.2945709228515625), 1e-1),
testing.closeToVector(util.vector3(-6.79993534088134765625, 1380.4000244140625, 85.094573974609375), 1e-1),
testing.closeToVector(util.vector3(79, 724, -104.83390045166015625), 1e-1),
testing.closeToVector(util.vector3(84, 290.000030517578125, -104.83390045166015625), 1e-1),
testing.closeToVector(util.vector3(83.552001953125, 42.26399993896484375, -104.58989715576171875), 1e-1),
testing.closeToVector(util.vector3(89, -105, -98.72841644287109375), 1e-1),
testing.closeToVector(util.vector3(90, -90, -99.7056884765625), 1e-1),
testing.expectThat(path, matchers.elementsAreArray({
matchers.closeToVector(util.vector3(34.29737091064453125, 806.3817138671875, 112.76610565185546875), 1e-1),
matchers.closeToVector(util.vector3(15, 1102, 112.2945709228515625), 1e-1),
matchers.closeToVector(util.vector3(-112, 1110, 112.2945709228515625), 1e-1),
matchers.closeToVector(util.vector3(-118, 1393, 112.2945709228515625), 1e-1),
matchers.closeToVector(util.vector3(-67.99993896484375, 1421.2000732421875, 112.2945709228515625), 1e-1),
matchers.closeToVector(util.vector3(-33.999935150146484375, 1414.4000244140625, 112.2945709228515625), 1e-1),
matchers.closeToVector(util.vector3(-6.79993534088134765625, 1380.4000244140625, 85.094573974609375), 1e-1),
matchers.closeToVector(util.vector3(79, 724, -104.83390045166015625), 1e-1),
matchers.closeToVector(util.vector3(84, 290.000030517578125, -104.83390045166015625), 1e-1),
matchers.closeToVector(util.vector3(83.552001953125, 42.26399993896484375, -104.58989715576171875), 1e-1),
matchers.closeToVector(util.vector3(89, -105, -98.72841644287109375), 1e-1),
matchers.closeToVector(util.vector3(90, -90, -99.7056884765625), 1e-1),
}))
elseif agentBounds.shapeType == nearby.COLLISION_SHAPE_TYPE.Cylinder then
testing.expectThat(path, testing.elementsAreArray({
testing.closeToVector(util.vector3(34.29737091064453125, 806.3817138671875, 112.76610565185546875), 1e-1),
testing.closeToVector(util.vector3(-13.5999355316162109375, 1060.800048828125, 112.2945709228515625), 1e-1),
testing.closeToVector(util.vector3(-27.1999359130859375, 1081.2000732421875, 112.2945709228515625), 1e-1),
testing.closeToVector(util.vector3(-81.59993743896484375, 1128.800048828125, 112.2945709228515625), 1e-1),
testing.closeToVector(util.vector3(-101.99993896484375, 1156.0001220703125, 112.2945709228515625), 1e-1),
testing.closeToVector(util.vector3(-118, 1393, 112.2945709228515625), 1e-1),
testing.closeToVector(util.vector3(7, 1470, 114.73973846435546875), 1e-1),
testing.closeToVector(util.vector3(79, 724, -104.83390045166015625), 1e-1),
testing.closeToVector(util.vector3(84, 290.000030517578125, -104.83390045166015625), 1e-1),
testing.closeToVector(util.vector3(95, 27, -104.83390045166015625), 1e-1),
testing.closeToVector(util.vector3(90, -90, -104.83390045166015625), 1e-1),
testing.expectThat(path, matchers.elementsAreArray({
matchers.closeToVector(util.vector3(34.29737091064453125, 806.3817138671875, 112.76610565185546875), 1e-1),
matchers.closeToVector(util.vector3(-13.5999355316162109375, 1060.800048828125, 112.2945709228515625), 1e-1),
matchers.closeToVector(util.vector3(-27.1999359130859375, 1081.2000732421875, 112.2945709228515625), 1e-1),
matchers.closeToVector(util.vector3(-81.59993743896484375, 1128.800048828125, 112.2945709228515625), 1e-1),
matchers.closeToVector(util.vector3(-101.99993896484375, 1156.0001220703125, 112.2945709228515625), 1e-1),
matchers.closeToVector(util.vector3(-118, 1393, 112.2945709228515625), 1e-1),
matchers.closeToVector(util.vector3(7, 1470, 114.73973846435546875), 1e-1),
matchers.closeToVector(util.vector3(79, 724, -104.83390045166015625), 1e-1),
matchers.closeToVector(util.vector3(84, 290.000030517578125, -104.83390045166015625), 1e-1),
matchers.closeToVector(util.vector3(95, 27, -104.83390045166015625), 1e-1),
matchers.closeToVector(util.vector3(90, -90, -104.83390045166015625), 1e-1),
}))
end
end)