mirror of https://github.com/OpenMW/openmw.git
Introduce OpenMW integration tests
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…
Reference in New Issue