Introduce OpenMW integration tests

pull/3227/head
Petr Mikheev 3 years ago
parent 9b0928e8f4
commit 76160076db

@ -0,0 +1,63 @@
local testing = require('testing_util')
local self = require('openmw.self')
local util = require('openmw.util')
local core = require('openmw.core')
local input = require('openmw.input')
local types = require('openmw.types')
input.setControlSwitch(input.CONTROL_SWITCH.Fighting, false)
input.setControlSwitch(input.CONTROL_SWITCH.Jumping, false)
input.setControlSwitch(input.CONTROL_SWITCH.Looking, false)
input.setControlSwitch(input.CONTROL_SWITCH.Magic, false)
input.setControlSwitch(input.CONTROL_SWITCH.VanityMode, false)
input.setControlSwitch(input.CONTROL_SWITCH.ViewMode, false)
testing.registerLocalTest('playerMovement',
function()
local startTime = core.getSimulationTime()
local pos = self.position
while core.getSimulationTime() < startTime + 0.5 do
self.controls.jump = false
self.controls.run = true
self.controls.movement = 0
self.controls.sideMovement = 0
local progress = (core.getSimulationTime() - startTime) / 0.5
self.controls.yawChange = util.normalizeAngle(math.rad(90) * progress - self.rotation.z)
coroutine.yield()
end
testing.expectEqualWithDelta(self.rotation.z, math.rad(90), 0.05, 'Incorrect rotation')
while core.getSimulationTime() < startTime + 1.5 do
self.controls.jump = false
self.controls.run = true
self.controls.movement = 1
self.controls.sideMovement = 0
self.controls.yawChange = 0
coroutine.yield()
end
direction = (self.position - pos) / types.Actor.runSpeed(self)
testing.expectEqualWithDelta(direction.x, 1, 0.1, 'Run forward, X coord')
testing.expectEqualWithDelta(direction.y, 0, 0.1, 'Run forward, Y coord')
pos = self.position
while core.getSimulationTime() < startTime + 2.5 do
self.controls.jump = false
self.controls.run = false
self.controls.movement = -1
self.controls.sideMovement = -1
self.controls.yawChange = 0
coroutine.yield()
end
direction = (self.position - pos) / types.Actor.walkSpeed(self)
testing.expectEqualWithDelta(direction.x, -0.707, 0.1, 'Walk diagonally, X coord')
testing.expectEqualWithDelta(direction.y, 0.707, 0.1, 'Walk diagonally, Y coord')
end)
return {
engineHandlers = {
onUpdate = testing.updateLocal,
},
eventHandlers = testing.eventHandlers
}

@ -0,0 +1,67 @@
local testing = require('testing_util')
local core = require('openmw.core')
local async = require('openmw.async')
local util = require('openmw.util')
local function testTimers()
testing.expectAlmostEqual(core.getGameTimeScale(), 30, 'incorrect getGameTimeScale() result')
testing.expectAlmostEqual(core.getSimulationTimeScale(), 1, 'incorrect getSimulationTimeScale result')
local startGameTime = core.getGameTime()
local startSimulationTime = core.getSimulationTime()
local ts1, ts2, th1, th2
local cb = async:registerTimerCallback("tfunc", function(arg)
if arg == 'g' then
th1 = core.getGameTime() - startGameTime
else
ts1 = core.getSimulationTime() - startSimulationTime
end
end)
async:newGameTimer(36, cb, 'g')
async:newSimulationTimer(0.5, cb, 's')
async:newUnsavableGameTimer(72, function()
th2 = core.getGameTime() - startGameTime
end)
async:newUnsavableSimulationTimer(1, function()
ts2 = core.getSimulationTime() - startSimulationTime
end)
while not (ts1 and ts2 and th1 and th2) do coroutine.yield() end
testing.expectAlmostEqual(th1, 36, 'async:newGameTimer failed')
testing.expectAlmostEqual(ts1, 0.5, 'async:newSimulationTimer failed')
testing.expectAlmostEqual(th2, 72, 'async:newUnsavableGameTimer failed')
testing.expectAlmostEqual(ts2, 1, 'async:newUnsavableSimulationTimer failed')
end
local function testTeleport()
player:teleport('', util.vector3(100, 50, 0), util.vector3(0, 0, math.rad(-90)))
coroutine.yield()
testing.expect(player.cell.isExterior, 'teleport to exterior failed')
testing.expectEqualWithDelta(player.position.x, 100, 1, 'incorrect position after teleporting')
testing.expectEqualWithDelta(player.position.y, 50, 1, 'incorrect position after teleporting')
testing.expectEqualWithDelta(player.rotation.z, math.rad(-90), 0.05, 'incorrect rotation after teleporting')
player:teleport('', util.vector3(50, -100, 0))
coroutine.yield()
testing.expect(player.cell.isExterior, 'teleport to exterior failed')
testing.expectEqualWithDelta(player.position.x, 50, 1, 'incorrect position after teleporting')
testing.expectEqualWithDelta(player.position.y, -100, 1, 'incorrect position after teleporting')
testing.expectEqualWithDelta(player.rotation.z, math.rad(-90), 0.05, 'teleporting changes rotation')
end
tests = {
{'timers', testTimers},
{'playerMovement', function() testing.runLocalTest(player, 'playerMovement') end},
{'teleport', testTeleport},
}
return {
engineHandlers = {
onUpdate = testing.testRunner(tests),
onPlayerAdded = function(p) player = p end,
},
eventHandlers = testing.eventHandlers,
}

@ -0,0 +1,3 @@
GLOBAL: test.lua
PLAYER: player.lua

@ -0,0 +1,93 @@
local core = require('openmw.core')
local M = {}
local currentLocalTest = nil
local currentLocalTestError = nil
function M.testRunner(tests)
local fn = function()
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
local co = coroutine.create(fn)
return function()
if coroutine.status(co) ~= 'dead' then
coroutine.resume(co)
end
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.expect(cond, delta, 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
local localTests = {}
local localTestRunner = nil
function M.registerLocalTest(name, fn)
localTests[name] = fn
end
function M.updateLocal()
if localTestRunner and coroutine.status(localTestRunner) ~= 'dead' then
coroutine.resume(localTestRunner)
else
localTestRunner = nil
end
end
M.eventHandlers = {
runLocalTest = function(name) -- used only in local scripts
fn = localTests[name]
if not fn then
core.sendGlobalEvent('localTestFinished', {name=name, errMsg='Test 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,
localTestFinished = function(data) -- used only in global scripts
if data.name ~= currentLocalTest then
error(string.format('localTestFinished with incorrect name %s, expected %s', data.name, currentLocalTest))
end
currentLocalTest = nil
currentLocalTestError = data.errMsg
end,
}
return M

@ -0,0 +1,87 @@
#!/usr/bin/env python3
import argparse, datetime, os, subprocess, sys, shutil
from pathlib import Path
parser = argparse.ArgumentParser(description="OpenMW integration tests.")
parser.add_argument(
"example_suite",
type=str,
help="path to openmw example suite (use 'git clone https://gitlab.com/OpenMW/example-suite/' to get it)",
)
parser.add_argument("--omw", type=str, default="openmw", help="path to openmw binary")
parser.add_argument(
"--workdir", type=str, default="integration_tests_output", help="directory for temporary files and logs"
)
args = parser.parse_args()
example_suite_dir = Path(args.example_suite).resolve()
example_suite_content = example_suite_dir / "game_template" / "data" / "template.omwgame"
if not example_suite_content.is_file():
sys.exit(
f"{example_suite_content} not found, use 'git clone https://gitlab.com/OpenMW/example-suite/' to get it"
)
openmw_binary = Path(args.omw).resolve()
if not openmw_binary.is_file():
sys.exit(f"{openmw_binary} not found")
work_dir = Path(args.workdir).resolve()
work_dir.mkdir(parents=True, exist_ok=True)
config_dir = work_dir / "config"
userdata_dir = work_dir / "userdata"
tests_dir = Path(__file__).resolve().parent / "data" / "integration_tests"
time_str = datetime.datetime.now().strftime("%Y-%m-%d-%H.%M.%S")
def runTest(name):
print(f"Start {name}")
shutil.rmtree(config_dir, ignore_errors=True)
config_dir.mkdir()
shutil.copyfile(example_suite_dir / "settings.cfg", config_dir / "settings.cfg")
test_dir = tests_dir / name
with open(config_dir / "openmw.cfg", "w", encoding="utf-8") as omw_cfg:
omw_cfg.writelines(
(
f'data="{example_suite_dir}{os.sep}game_template{os.sep}data"\n',
f'data-local="{test_dir}"\n',
f'user-data="{userdata_dir}"\n',
"content=template.omwgame\n",
)
)
if (test_dir / "test.omwscripts").exists():
omw_cfg.write("content=test.omwscripts\n")
with subprocess.Popen(
[f"{openmw_binary}", "--replace=config", f"--config={config_dir}", "--skip-menu", "--no-grab"],
stdout=subprocess.PIPE,
stderr=subprocess.STDOUT,
encoding="utf-8",
) as process:
quit_requested = False
for line in process.stdout:
words = line.split(" ")
if len(words) > 1 and words[1] == "E]":
print(line, end="")
elif "Quit requested by a Lua script" in line:
quit_requested = True
elif "TEST_START" in line:
w = line.split("TEST_START")[1].split("\t")
print(f"TEST {w[2].strip()}\t\t", end="")
elif "TEST_OK" in line:
print(f"OK")
elif "TEST_FAILED" in line:
w = line.split("TEST_FAILED")[1].split("\t")
print(f"FAILED {w[3]}\t\t")
process.wait(5)
if not quit_requested:
print("ERROR: Unexpected termination")
shutil.copyfile(config_dir / "openmw.log", work_dir / f"{name}.{time_str}.log")
print(f"{name} finished")
for entry in tests_dir.glob("test_*"):
if entry.is_dir():
runTest(entry.name)
shutil.rmtree(config_dir, ignore_errors=True)
shutil.rmtree(userdata_dir, ignore_errors=True)
Loading…
Cancel
Save