From f80c7b2355deffe4c1db891626d4ee2ac95372d9 Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 22:31:38 +0100 Subject: [PATCH 1/7] Expect openmw.cfg to exist --- scripts/integration_tests.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/scripts/integration_tests.py b/scripts/integration_tests.py index 17f89fbe2f..d742ac8bf9 100755 --- a/scripts/integration_tests.py +++ b/scripts/integration_tests.py @@ -58,10 +58,8 @@ def runTest(name): ) for path in content_paths: omw_cfg.write(f'content={path.name}\n') - if (test_dir / "openmw.cfg").exists(): - omw_cfg.write(open(test_dir / "openmw.cfg").read()) - elif (test_dir / "test.omwscripts").exists(): - omw_cfg.write("content=test.omwscripts\n") + with open(test_dir / "openmw.cfg") as stream: + omw_cfg.write(stream.read()) with open(config_dir / "settings.cfg", "a", encoding="utf-8") as settings_cfg: settings_cfg.write( "[Video]\n" From c298210844d9c5e60a90886178d80af549539a4f Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 22:35:51 +0100 Subject: [PATCH 2/7] Make integration_tests.py output more verbose * Make it look more like googletest. * Print total and failed number of tests. * Print failed tests names. * Print duration of each test and total. * Hide all logs by default. --- scripts/integration_tests.py | 79 +++++++++++++++++++++++------------- 1 file changed, 51 insertions(+), 28 deletions(-) diff --git a/scripts/integration_tests.py b/scripts/integration_tests.py index d742ac8bf9..850ac30b71 100755 --- a/scripts/integration_tests.py +++ b/scripts/integration_tests.py @@ -1,6 +1,13 @@ #!/usr/bin/env python3 -import argparse, datetime, os, subprocess, sys, shutil +import argparse +import datetime +import os +import shutil +import subprocess +import sys +import time + from pathlib import Path parser = argparse.ArgumentParser(description="OpenMW integration tests.") @@ -40,12 +47,13 @@ testing_util_dir = tests_dir / "testing_util" time_str = datetime.datetime.now().strftime("%Y-%m-%d-%H.%M.%S") -def runTest(name): - print(f"Start {name}") +def run_test(test_name): + start = time.time() + print(f'[----------] Running tests from {test_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 + test_dir = tests_dir / test_name with open(config_dir / "openmw.cfg", "w", encoding="utf-8") as omw_cfg: for path in content_paths: omw_cfg.write(f'data="{path.parent}"\n') @@ -72,61 +80,76 @@ def runTest(name): f"memory limit = {1024 * 1024 * 256}\n" ) stdout_lines = list() - exit_ok = True test_success = True + fatal_errors = list() with subprocess.Popen( [openmw_binary, "--replace=config", "--config", config_dir, "--skip-menu", "--no-grab", "--no-sound"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8", env={ - "OPENMW_OSG_STATS_FILE": str(work_dir / f"{name}.{time_str}.osg_stats.log"), + "OPENMW_OSG_STATS_FILE": str(work_dir / f"{test_name}.{time_str}.osg_stats.log"), "OPENMW_OSG_STATS_LIST": "times", **os.environ, }, ) as process: quit_requested = False + running_test_number = None + running_test_name = None + count = 0 + failed_tests = list() + test_start = None for line in process.stdout: if args.verbose: sys.stdout.write(line) else: stdout_lines.append(line) - words = line.split(" ") - if len(words) > 1 and words[1] == "E]": - print(line, end="") - elif "Quit requested by a Lua script" in line: + if "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="") + test_start = time.time() + number, name = line.split("TEST_START")[1].strip().split("\t", maxsplit=1) + running_test_number = int(number) + running_test_name = name + count += 1 + print(f"[ RUN ] {running_test_name}") elif "TEST_OK" in line: - print(f"OK") + duration = (time.time() - test_start) * 1000 + number, name = line.split("TEST_OK")[1].strip().split("\t", maxsplit=1) + assert running_test_number == int(number) + print(f"[ OK ] {running_test_name} ({duration:.3f} ms)") elif "TEST_FAILED" in line: - w = line.split("TEST_FAILED")[1].split("\t") - print(f"FAILED {w[3]}\t\t") - test_success = False + duration = (time.time() - test_start) * 1000 + number, name, error = line.split("TEST_FAILED")[1].strip().split("\t", maxsplit=2) + assert running_test_number == int(number) + print(error) + print(f"[ FAILED ] {running_test_name} ({duration:.3f} ms)") + failed_tests.append(running_test_name) process.wait(5) if not quit_requested: - print("ERROR: Unexpected termination") - exit_ok = False + fatal_errors.append("unexpected termination") if process.returncode != 0: - print(f"ERROR: openmw exited with code {process.returncode}") - exit_ok = False + fatal_errors.append(f"openmw exited with code {process.returncode}") if os.path.exists(config_dir / "openmw.log"): - shutil.copyfile(config_dir / "openmw.log", work_dir / f"{name}.{time_str}.log") - if not exit_ok and not args.verbose: + shutil.copyfile(config_dir / "openmw.log", work_dir / f"{test_name}.{time_str}.log") + if fatal_errors and not args.verbose: sys.stdout.writelines(stdout_lines) - if test_success and exit_ok: - print(f"{name} succeeded") - else: - print(f"{name} failed") - return test_success and exit_ok + total_duration = (time.time() - start) * 1000 + print(f'\n[----------] {count} tests from {test_name} ({total_duration:.3f} ms total)') + print(f"[ PASSED ] {count - len(failed_tests)} tests.") + if fatal_errors: + print(f"[ FAILED ] fatal error: {'; '.join(fatal_errors)}") + if failed_tests: + print(f"[ FAILED ] {len(failed_tests)} tests, listed below:") + for failed_test in failed_tests: + print(f"[ FAILED ] {failed_test}") + return len(failed_tests) == 0 and not fatal_errors status = 0 for entry in tests_dir.glob("test_*"): if entry.is_dir(): - if not runTest(entry.name): + if not run_test(entry.name): status = -1 if status == 0: shutil.rmtree(config_dir, ignore_errors=True) From 8b62f025231565a4beb9a07d93d6164caee8108f Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 22:55:35 +0100 Subject: [PATCH 3/7] Use world.players to initialize player in global tests --- .../integration_tests/test_lua_api/test.lua | 34 +++++++++---------- 1 file changed, 17 insertions(+), 17 deletions(-) diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/test.lua index ff2cd9bb33..5686065698 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/test.lua @@ -42,6 +42,7 @@ local function testTimers() end local function testTeleport() + local player = world.players[1] player:teleport('', util.vector3(100, 50, 500), util.transform.rotateZ(math.rad(90))) coroutine.yield() testing.expect(player.cell.isExterior, 'teleport to exterior failed') @@ -221,8 +222,10 @@ local function testMemoryLimit() end local function initPlayer() + local player = world.players[1] player:teleport('', util.vector3(4096, 4096, 1745), util.transform.identity) coroutine.yield() + return player end local function testVFS() @@ -272,8 +275,7 @@ local function testVFS() end local function testCommitCrime() - initPlayer() - local player = world.players[1] + local player = initPlayer() testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `testCommitCrime`') testing.expectEqual(I.Crimes == nil, false, 'Crimes interface should be available in global contexts') @@ -295,51 +297,50 @@ local function testCommitCrime() end local function testRecordModelProperty() - initPlayer() - local player = world.players[1] + local player = initPlayer() testing.expectEqual(types.NPC.record(player).model, 'meshes/basicplayer.dae') end tests = { {'timers', testTimers}, {'rotating player with controls.yawChange should change rotation', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerYawRotation') end}, {'rotating player with controls.pitchChange should change rotation', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerPitchRotation') end}, {'rotating player with controls.pitchChange and controls.yawChange should change rotation', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerPitchAndYawRotation') end}, {'rotating player should not lead to nan rotation', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerRotation') end}, {'playerForwardRunning', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerForwardRunning') end}, {'playerDiagonalWalking', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerDiagonalWalking') end}, {'findPath', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'findPath') end}, {'findRandomPointAroundCircle', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'findRandomPointAroundCircle') end}, {'castNavigationRay', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'castNavigationRay') end}, {'findNearestNavMeshPosition', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'findNearestNavMeshPosition') end}, {'teleport', testTeleport}, @@ -351,11 +352,11 @@ tests = { {'mwscript', testMWScript}, {'testMemoryLimit', testMemoryLimit}, {'playerMemoryLimit', function() - initPlayer() + local player = initPlayer() testing.runLocalTest(player, 'playerMemoryLimit') end}, {'player with equipped weapon on attack should damage health of other actors', function() - initPlayer() + local player = initPlayer() world.createObject('basic_dagger1h', 1):moveInto(player) testing.runLocalTest(player, 'playerWeaponAttack') end}, @@ -367,7 +368,6 @@ tests = { return { engineHandlers = { onUpdate = testing.testRunner(tests), - onPlayerAdded = function(p) player = p end, }, eventHandlers = testing.eventHandlers, } From 981ca957c1755fd59bfbdd3ef95d74ea790e15a5 Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 23:12:17 +0100 Subject: [PATCH 4/7] Register global tests to run them --- .../integration_tests/test_lua_api/test.lua | 135 +++++++----------- .../testing_util/testing_util.lua | 51 ++++--- scripts/data/morrowind_tests/global.lua | 23 +-- .../data/morrowind_tests/global_dialogues.lua | 66 ++++----- .../data/morrowind_tests/global_issues.lua | 66 ++++----- .../data/morrowind_tests/global_mwscript.lua | 64 ++++----- 6 files changed, 181 insertions(+), 224 deletions(-) diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/test.lua index 5686065698..f55c44df63 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/test.lua @@ -7,7 +7,7 @@ local vfs = require('openmw.vfs') local world = require('openmw.world') local I = require('openmw.interfaces') -local function testTimers() +testing.registerGlobalTest('testTimers', function() testing.expectAlmostEqual(core.getGameTimeScale(), 30, 'incorrect getGameTimeScale() result') testing.expectAlmostEqual(core.getSimulationTimeScale(), 1, 'incorrect getSimulationTimeScale result') @@ -39,9 +39,9 @@ local function testTimers() testing.expectGreaterOrEqual(ts1, 0.5, 'async:newSimulationTimer failed') testing.expectGreaterOrEqual(th2, 72, 'async:newUnsavableGameTimer failed') testing.expectGreaterOrEqual(ts2, 1, 'async:newUnsavableSimulationTimer failed') -end +end) -local function testTeleport() +testing.registerGlobalTest('testTeleport', function() local player = world.players[1] player:teleport('', util.vector3(100, 50, 500), util.transform.rotateZ(math.rad(90))) coroutine.yield() @@ -72,16 +72,16 @@ local function testTeleport() 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:getYaw(), math.rad(-90), 0.05, 'teleporting changes rotation') -end +end) -local function testGetGMST() +testing.registerGlobalTest('testGetGMST', function() testing.expectEqual(core.getGMST('non-existed gmst'), nil) testing.expectEqual(core.getGMST('Water_RippleFrameCount'), 4) testing.expectEqual(core.getGMST('Inventory_DirectionalDiffuseR'), 0.5) testing.expectEqual(core.getGMST('Level_Up_Level2'), 'something') -end +end) -local function testMWScript() +testing.registerGlobalTest('testMWScript', function() local variableStoreCount = 18 local variableStore = world.mwscript.getGlobalVariables(player) testing.expectEqual(variableStoreCount, #variableStore) @@ -101,7 +101,7 @@ local function testMWScript() indexCheck = indexCheck + 1 end testing.expectEqual(variableStoreCount, indexCheck) -end +end) local function testRecordStore(store, storeName, skipPairs) testing.expect(store.records) @@ -122,7 +122,7 @@ local function testRecordStore(store, storeName, skipPairs) testing.expectEqual(status, true, storeName) end -local function testRecordStores() +testing.registerGlobalTest('testRecordStores', function() for key, type in pairs(types) do if type.records then testRecordStore(type, key) @@ -141,9 +141,9 @@ local function testRecordStores() testRecordStore(types.NPC.classes, "classes") testRecordStore(types.NPC.races, "races") testRecordStore(types.Player.birthSigns, "birthSigns") -end +end) -local function testRecordCreation() +testing.registerGlobalTest('testRecordCreation', function() local newLight = { isCarriable = true, isDynamic = true, @@ -166,9 +166,9 @@ local function testRecordCreation() for key, value in pairs(newLight) do testing.expectEqual(record[key], value) end -end +end) -local function testUTF8Chars() +testing.registerGlobalTest('testUTF8Chars', function() testing.expectEqual(utf8.codepoint("😀"), 0x1F600) local chars = {} @@ -193,9 +193,9 @@ local function testUTF8Chars() testing.expectEqual(utf8.codepoint(char), codepoint) testing.expectEqual(utf8.len(char), 1) end -end +end) -local function testUTF8Strings() +testing.registerGlobalTest('testUTF8Strings', function() local utf8str = "Hello, 你好, 🌎!" local str = "" @@ -206,9 +206,9 @@ local function testUTF8Strings() testing.expectEqual(utf8.len(utf8str), 13) testing.expectEqual(utf8.offset(utf8str, 9), 11) -end +end) -local function testMemoryLimit() +testing.registerGlobalTest('testMemoryLimit', function() local ok, err = pcall(function() local t = {} local n = 1 @@ -219,7 +219,7 @@ local function testMemoryLimit() end) testing.expectEqual(ok, false, 'Script reaching memory limit should fail') testing.expectEqual(err, 'not enough memory') -end +end) local function initPlayer() local player = world.players[1] @@ -228,7 +228,7 @@ local function initPlayer() return player end -local function testVFS() +testing.registerGlobalTest('testVFS', function() local file = 'test_vfs_dir/lines.txt' local nosuchfile = 'test_vfs_dir/nosuchfile' testing.expectEqual(vfs.fileExists(file), true, 'lines.txt should exist') @@ -272,9 +272,9 @@ local function testVFS() for _,v in pairs(expectedLines) do testing.expectEqual(getLine(), v) end -end +end) -local function testCommitCrime() +testing.registerGlobalTest('testCommitCrime', function() local player = initPlayer() testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `testCommitCrime`') testing.expectEqual(I.Crimes == nil, false, 'Crimes interface should be available in global contexts') @@ -294,80 +294,41 @@ local function testCommitCrime() types.Player.setCrimeLevel(player, 0) testing.expectEqual(I.Crimes.commitCrime(player, { victim = victim, type = types.Player.OFFENSE_TYPE.Theft, arg = 50 }).wasCrimeSeen, true, "Running a crime with a valid victim should notify them when the player is not sneaking, even if it's not explicitly passed in") testing.expectEqual(types.Player.getCrimeLevel(player), 0, "Crime level should not change if the victim's alarm value is low and there's no other witnesses") -end +end) -local function testRecordModelProperty() +testing.registerGlobalTest('testRecordModelProperty', function() local player = initPlayer() testing.expectEqual(types.NPC.record(player).model, 'meshes/basicplayer.dae') +end) + +local function registerPlayerTest(name) + testing.registerGlobalTest(name, function() + local player = initPlayer() + testing.runLocalTest(player, name) + end) end -tests = { - {'timers', testTimers}, - {'rotating player with controls.yawChange should change rotation', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerYawRotation') - end}, - {'rotating player with controls.pitchChange should change rotation', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerPitchRotation') - end}, - {'rotating player with controls.pitchChange and controls.yawChange should change rotation', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerPitchAndYawRotation') - end}, - {'rotating player should not lead to nan rotation', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerRotation') - end}, - {'playerForwardRunning', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerForwardRunning') - end}, - {'playerDiagonalWalking', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerDiagonalWalking') - end}, - {'findPath', function() - local player = initPlayer() - testing.runLocalTest(player, 'findPath') - end}, - {'findRandomPointAroundCircle', function() - local player = initPlayer() - testing.runLocalTest(player, 'findRandomPointAroundCircle') - end}, - {'castNavigationRay', function() - local player = initPlayer() - testing.runLocalTest(player, 'castNavigationRay') - end}, - {'findNearestNavMeshPosition', function() - local player = initPlayer() - testing.runLocalTest(player, 'findNearestNavMeshPosition') - end}, - {'teleport', testTeleport}, - {'getGMST', testGetGMST}, - {'recordStores', testRecordStores}, - {'recordCreation', testRecordCreation}, - {'utf8Chars', testUTF8Chars}, - {'utf8Strings', testUTF8Strings}, - {'mwscript', testMWScript}, - {'testMemoryLimit', testMemoryLimit}, - {'playerMemoryLimit', function() - local player = initPlayer() - testing.runLocalTest(player, 'playerMemoryLimit') - end}, - {'player with equipped weapon on attack should damage health of other actors', function() - local player = initPlayer() - world.createObject('basic_dagger1h', 1):moveInto(player) - testing.runLocalTest(player, 'playerWeaponAttack') - end}, - {'vfs', testVFS}, - {'testCommitCrime', testCommitCrime}, - {'recordModelProperty', testRecordModelProperty}, -} +registerPlayerTest('playerYawRotation') +registerPlayerTest('playerPitchRotation') +registerPlayerTest('playerPitchAndYawRotation') +registerPlayerTest('playerRotation') +registerPlayerTest('playerForwardRunning') +registerPlayerTest('playerDiagonalWalking') +registerPlayerTest('findPath') +registerPlayerTest('findRandomPointAroundCircle') +registerPlayerTest('castNavigationRay') +registerPlayerTest('findNearestNavMeshPosition') +registerPlayerTest('playerMemoryLimit') + +testing.registerGlobalTest('playerWeaponAttack', function() + local player = initPlayer() + world.createObject('basic_dagger1h', 1):moveInto(player) + testing.runLocalTest(player, 'playerWeaponAttack') +end) return { engineHandlers = { - onUpdate = testing.testRunner(tests), + onUpdate = testing.makeUpdateGlobal(), }, eventHandlers = testing.eventHandlers, } diff --git a/scripts/data/integration_tests/testing_util/testing_util.lua b/scripts/data/integration_tests/testing_util/testing_util.lua index 7b886636ed..2d67c6ca8a 100644 --- a/scripts/data/integration_tests/testing_util/testing_util.lua +++ b/scripts/data/integration_tests/testing_util/testing_util.lua @@ -2,12 +2,19 @@ local core = require('openmw.core') local util = require('openmw.util') local M = {} + +local globalTestsOrder = {} +local globalTests = {} +local globalTestRunner = nil + +local localTests = {} +local localTestRunner = nil local currentLocalTest = nil local currentLocalTestError = nil -function M.testRunner(tests) +function M.makeUpdateGlobal() local fn = function() - for i, test in ipairs(tests) do + for i, test in ipairs(globalTestsOrder) do local name, fn = unpack(test) print('TEST_START', i, name) local status, err = pcall(fn) @@ -27,6 +34,11 @@ function M.testRunner(tests) end end +function M.registerGlobalTest(name, fn) + globalTests[name] = fn + table.insert(globalTestsOrder, {name, fn}) +end + function M.runLocalTest(obj, name) currentLocalTest = name currentLocalTestError = nil @@ -39,7 +51,21 @@ function M.runLocalTest(obj, name) end end -function M.expect(cond, delta, msg) +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 @@ -182,28 +208,11 @@ function M.formatActualExpected(actual, expected) return string.format('actual: %s, expected: %s', actual, expected) 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 - if not core.isWorldPaused() then - coroutine.resume(localTestRunner) - end - 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'}) + core.sendGlobalEvent('localTestFinished', {name=name, errMsg='Local test is not found'}) return end localTestRunner = coroutine.create(function() diff --git a/scripts/data/morrowind_tests/global.lua b/scripts/data/morrowind_tests/global.lua index fb7113d1b1..a78d90e408 100644 --- a/scripts/data/morrowind_tests/global.lua +++ b/scripts/data/morrowind_tests/global.lua @@ -8,28 +8,13 @@ if not core.contentFiles.has('Morrowind.esm') then error('This test requires Morrowind.esm') end -function makeTests(modules) - local tests = {} - - for _, moduleName in ipairs(modules) do - local module = require(moduleName) - for _, v in ipairs(module) do - table.insert(tests, {string.format('[%s] %s', moduleName, v[1]), v[2]}) - end - end - - return tests -end - -local testModules = { - 'global_issues', - 'global_dialogues', - 'global_mwscript', -} +require('global_issues') +require('global_dialogues') +require('global_mwscript') return { engineHandlers = { - onUpdate = testing.testRunner(makeTests(testModules)), + onUpdate = testing.makeUpdateGlobal(), }, eventHandlers = testing.eventHandlers, } diff --git a/scripts/data/morrowind_tests/global_dialogues.lua b/scripts/data/morrowind_tests/global_dialogues.lua index 397eb8461c..68f4f4a747 100644 --- a/scripts/data/morrowind_tests/global_dialogues.lua +++ b/scripts/data/morrowind_tests/global_dialogues.lua @@ -13,35 +13,37 @@ function iterateOverRecords(records) return firstRecordId, lastRecordId, count end -return { - {'Should support iteration over journal dialogues', function() - local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.journal.records) - testing.expectEqual(firstRecordId, '11111 test journal') - testing.expectEqual(lastRecordId, 'va_vamprich') - testing.expectEqual(count, 632) - end}, - {'Should support iteration over topic dialogues', function() - local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.topic.records) - testing.expectEqual(firstRecordId, '1000-drake pledge') - testing.expectEqual(lastRecordId, 'zenithar') - testing.expectEqual(count, 1698) - end}, - {'Should support iteration over greeting dialogues', function() - local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.greeting.records) - testing.expectEqual(firstRecordId, 'greeting 0') - testing.expectEqual(lastRecordId, 'greeting 9') - testing.expectEqual(count, 10) - end}, - {'Should support iteration over persuasion dialogues', function() - local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.persuasion.records) - testing.expectEqual(firstRecordId, 'admire fail') - testing.expectEqual(lastRecordId, 'taunt success') - testing.expectEqual(count, 10) - end}, - {'Should support iteration over voice dialogues', function() - local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.voice.records) - testing.expectEqual(firstRecordId, 'alarm') - testing.expectEqual(lastRecordId, 'thief') - testing.expectEqual(count, 8) - end}, -} +testing.registerGlobalTest('[dialogues] Should support iteration over journal dialogues', function() + local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.journal.records) + testing.expectEqual(firstRecordId, '11111 test journal') + testing.expectEqual(lastRecordId, 'va_vamprich') + testing.expectEqual(count, 632) +end) + +testing.registerGlobalTest('[dialogues] Should support iteration over topic dialogues', function() + local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.topic.records) + testing.expectEqual(firstRecordId, '1000-drake pledge') + testing.expectEqual(lastRecordId, 'zenithar') + testing.expectEqual(count, 1698) +end) + +testing.registerGlobalTest('[dialogues] Should support iteration over greeting dialogues', function() + local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.greeting.records) + testing.expectEqual(firstRecordId, 'greeting 0') + testing.expectEqual(lastRecordId, 'greeting 9') + testing.expectEqual(count, 10) +end) + +testing.registerGlobalTest('[dialogues] Should support iteration over persuasion dialogues', function() + local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.persuasion.records) + testing.expectEqual(firstRecordId, 'admire fail') + testing.expectEqual(lastRecordId, 'taunt success') + testing.expectEqual(count, 10) +end) + +testing.registerGlobalTest('[dialogues] Should support iteration over voice dialogues', function() + local firstRecordId, lastRecordId, count = iterateOverRecords(core.dialogue.voice.records) + testing.expectEqual(firstRecordId, 'alarm') + testing.expectEqual(lastRecordId, 'thief') + testing.expectEqual(count, 8) +end) diff --git a/scripts/data/morrowind_tests/global_issues.lua b/scripts/data/morrowind_tests/global_issues.lua index 2afad085b0..a3400da87c 100644 --- a/scripts/data/morrowind_tests/global_issues.lua +++ b/scripts/data/morrowind_tests/global_issues.lua @@ -4,37 +4,37 @@ local world = require('openmw.world') local core = require('openmw.core') local types = require('openmw.types') -return { - {'Player should be able to walk up stairs in Ebonheart docks (#4247)', function() - world.players[1]:teleport('', util.vector3(19867, -102180, -79), util.transform.rotateZ(math.rad(91))) - coroutine.yield() - testing.runLocalTest(world.players[1], 'Player should be able to walk up stairs in Ebonheart docks (#4247)') - end}, - {'Guard in Imperial Prison Ship should find path (#7241)', function() - world.players[1]:teleport('Imperial Prison Ship', util.vector3(61, -135, -105), util.transform.rotateZ(math.rad(-20))) - coroutine.yield() - testing.runLocalTest(world.players[1], 'Guard in Imperial Prison Ship should find path (#7241)') - end}, - {'Should keep reference to an object moved into container (#7663)', function() - world.players[1]:teleport('ToddTest', util.vector3(2176, 3648, -191), util.transform.rotateZ(math.rad(0))) - coroutine.yield() - local barrel = world.createObject('barrel_01', 1) - local fargothRing = world.createObject('ring_keley', 1) - coroutine.yield() - testing.expectEqual(types.Container.inventory(barrel):find('ring_keley'), nil) - fargothRing:moveInto(types.Container.inventory(barrel)) - coroutine.yield() - testing.expectEqual(fargothRing.recordId, 'ring_keley') - local isFargothRing = function(actual) - if actual == nil then - return 'ring_keley is not found' - end - if actual.id ~= fargothRing.id then - return 'found ring_keley id does not match expected: actual=' .. tostring(actual.id) - .. ', expected=' .. tostring(fargothRing.id) - end - return '' +testing.registerGlobalTest('[issues] Player should be able to walk up stairs in Ebonheart docks (#4247)', function() + world.players[1]:teleport('', util.vector3(19867, -102180, -79), util.transform.rotateZ(math.rad(91))) + coroutine.yield() + testing.runLocalTest(world.players[1], 'Player should be able to walk up stairs in Ebonheart docks (#4247)') +end) + +testing.registerGlobalTest('[issues] Guard in Imperial Prison Ship should find path (#7241)', function() + world.players[1]:teleport('Imperial Prison Ship', util.vector3(61, -135, -105), util.transform.rotateZ(math.rad(-20))) + coroutine.yield() + testing.runLocalTest(world.players[1], 'Guard in Imperial Prison Ship should find path (#7241)') +end) + +testing.registerGlobalTest('[issues] Should keep reference to an object moved into container (#7663)', function() + world.players[1]:teleport('ToddTest', util.vector3(2176, 3648, -191), util.transform.rotateZ(math.rad(0))) + coroutine.yield() + local barrel = world.createObject('barrel_01', 1) + local fargothRing = world.createObject('ring_keley', 1) + coroutine.yield() + testing.expectEqual(types.Container.inventory(barrel):find('ring_keley'), nil) + fargothRing:moveInto(types.Container.inventory(barrel)) + coroutine.yield() + testing.expectEqual(fargothRing.recordId, 'ring_keley') + local isFargothRing = function(actual) + if actual == nil then + return 'ring_keley is not found' end - testing.expectThat(types.Container.inventory(barrel):find('ring_keley'), isFargothRing) - end}, -} + if actual.id ~= fargothRing.id then + return 'found ring_keley id does not match expected: actual=' .. tostring(actual.id) + .. ', expected=' .. tostring(fargothRing.id) + end + return '' + end + testing.expectThat(types.Container.inventory(barrel):find('ring_keley'), isFargothRing) +end) diff --git a/scripts/data/morrowind_tests/global_mwscript.lua b/scripts/data/morrowind_tests/global_mwscript.lua index a4347ea66e..fec9fcdba5 100644 --- a/scripts/data/morrowind_tests/global_mwscript.lua +++ b/scripts/data/morrowind_tests/global_mwscript.lua @@ -14,38 +14,38 @@ function iterateOverVariables(variables) return first, last, count end -return { - {'Should support iteration over an empty set of script variables', function() - local mainVars = world.mwscript.getGlobalScript('main').variables - local first, last, count = iterateOverVariables(mainVars) - testing.expectEqual(first, nil) - testing.expectEqual(last, nil) - testing.expectEqual(count, 0) - testing.expectEqual(count, #mainVars) - end}, - {'Should support iteration of script variables', function() - local jiub = world.getObjectByFormId(core.getFormId('Morrowind.esm', 172867)) - local jiubVars = world.mwscript.getLocalScript(jiub).variables - local first, last, count = iterateOverVariables(jiubVars) +testing.registerGlobalTest('[mwscript] Should support iteration over an empty set of script variables', function() + local mainVars = world.mwscript.getGlobalScript('main').variables + local first, last, count = iterateOverVariables(mainVars) + testing.expectEqual(first, nil) + testing.expectEqual(last, nil) + testing.expectEqual(count, 0) + testing.expectEqual(count, #mainVars) +end) - testing.expectEqual(first, 'state') - testing.expectEqual(last, 'timer') - testing.expectEqual(count, 3) - testing.expectEqual(count, #jiubVars) - end}, - {'Should support numeric and string indices for getting and setting', function() - local jiub = world.getObjectByFormId(core.getFormId('Morrowind.esm', 172867)) - local jiubVars = world.mwscript.getLocalScript(jiub).variables +testing.registerGlobalTest('[mwscript] Should support iteration of script variables', function() + local jiub = world.getObjectByFormId(core.getFormId('Morrowind.esm', 172867)) + local jiubVars = world.mwscript.getLocalScript(jiub).variables + local first, last, count = iterateOverVariables(jiubVars) - testing.expectEqual(jiubVars[1], jiubVars.state) - testing.expectEqual(jiubVars[2], jiubVars.wandering) - testing.expectEqual(jiubVars[3], jiubVars.timer) + testing.expectEqual(first, 'state') + testing.expectEqual(last, 'timer') + testing.expectEqual(count, 3) + testing.expectEqual(count, #jiubVars) +end) - jiubVars[1] = 123; - testing.expectEqual(jiubVars.state, 123) - jiubVars.wandering = 42; - testing.expectEqual(jiubVars[2], 42) - jiubVars[3] = 1.25; - testing.expectEqual(jiubVars.timer, 1.25) - end}, -} +testing.registerGlobalTest('[mwscript] Should support numeric and string indices for getting and setting', function() + local jiub = world.getObjectByFormId(core.getFormId('Morrowind.esm', 172867)) + local jiubVars = world.mwscript.getLocalScript(jiub).variables + + testing.expectEqual(jiubVars[1], jiubVars.state) + testing.expectEqual(jiubVars[2], jiubVars.wandering) + testing.expectEqual(jiubVars[3], jiubVars.timer) + + jiubVars[1] = 123; + testing.expectEqual(jiubVars.state, 123) + jiubVars.wandering = 42; + testing.expectEqual(jiubVars[2], 42) + jiubVars[3] = 1.25; + testing.expectEqual(jiubVars.timer, 1.25) +end) From 7a9c2d5e88e7e8ab9e4355776dd0b8263851de04 Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 23:18:07 +0100 Subject: [PATCH 5/7] Split local and global event handlers --- .../integration_tests/test_lua_api/player.lua | 2 +- .../integration_tests/test_lua_api/test.lua | 2 +- .../testing_util/testing_util.lua | 23 +++++++++++-------- scripts/data/morrowind_tests/global.lua | 2 +- scripts/data/morrowind_tests/player.lua | 2 +- 5 files changed, 18 insertions(+), 13 deletions(-) diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 16f0f2eea3..9620866e6e 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -346,5 +346,5 @@ return { engineHandlers = { onFrame = testing.updateLocal, }, - eventHandlers = testing.eventHandlers + eventHandlers = testing.localEventHandlers, } diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/test.lua index f55c44df63..2f336de8d9 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/test.lua @@ -330,5 +330,5 @@ return { engineHandlers = { onUpdate = testing.makeUpdateGlobal(), }, - eventHandlers = testing.eventHandlers, + eventHandlers = testing.globalEventHandlers, } diff --git a/scripts/data/integration_tests/testing_util/testing_util.lua b/scripts/data/integration_tests/testing_util/testing_util.lua index 2d67c6ca8a..f5de16fb2d 100644 --- a/scripts/data/integration_tests/testing_util/testing_util.lua +++ b/scripts/data/integration_tests/testing_util/testing_util.lua @@ -208,8 +208,20 @@ function M.formatActualExpected(actual, expected) return string.format('actual: %s, expected: %s', actual, expected) end -M.eventHandlers = { - runLocalTest = function(name) -- used only in local scripts +-- used only in global scripts +M.globalEventHandlers = { + localTestFinished = function(data) + 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, +} + +-- 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'}) @@ -223,13 +235,6 @@ M.eventHandlers = { 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 diff --git a/scripts/data/morrowind_tests/global.lua b/scripts/data/morrowind_tests/global.lua index a78d90e408..f17e72cda9 100644 --- a/scripts/data/morrowind_tests/global.lua +++ b/scripts/data/morrowind_tests/global.lua @@ -16,5 +16,5 @@ return { engineHandlers = { onUpdate = testing.makeUpdateGlobal(), }, - eventHandlers = testing.eventHandlers, + eventHandlers = testing.globalEventHandlers, } diff --git a/scripts/data/morrowind_tests/player.lua b/scripts/data/morrowind_tests/player.lua index 7435b49553..226d9754f0 100644 --- a/scripts/data/morrowind_tests/player.lua +++ b/scripts/data/morrowind_tests/player.lua @@ -80,5 +80,5 @@ return { engineHandlers = { onFrame = testing.updateLocal, }, - eventHandlers = testing.eventHandlers + eventHandlers = testing.localEventHandlers, } From 0e19b1dd7510133bf024d4f79dd3e74ef4bf6b1c Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 00:55:22 +0100 Subject: [PATCH 6/7] 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. --- .../test_lua_api/{test.lua => global.lua} | 48 ++++----- .../integration_tests/test_lua_api/menu.lua | 43 ++++++++ .../integration_tests/test_lua_api/openmw.cfg | 2 +- .../integration_tests/test_lua_api/player.lua | 16 +-- .../test_lua_api/test.omwscripts | 2 - .../test_lua_api/test_lua_api.omwscripts | 3 + .../testing_util/testing_util.lua | 102 +++++++++++++++--- scripts/integration_tests.py | 2 +- 8 files changed, 167 insertions(+), 51 deletions(-) rename scripts/data/integration_tests/test_lua_api/{test.lua => global.lua} (91%) create mode 100644 scripts/data/integration_tests/test_lua_api/menu.lua delete mode 100644 scripts/data/integration_tests/test_lua_api/test.omwscripts create mode 100644 scripts/data/integration_tests/test_lua_api/test_lua_api.omwscripts diff --git a/scripts/data/integration_tests/test_lua_api/test.lua b/scripts/data/integration_tests/test_lua_api/global.lua similarity index 91% rename from scripts/data/integration_tests/test_lua_api/test.lua rename to scripts/data/integration_tests/test_lua_api/global.lua index 2f336de8d9..cc8240554a 100644 --- a/scripts/data/integration_tests/test_lua_api/test.lua +++ b/scripts/data/integration_tests/test_lua_api/global.lua @@ -7,7 +7,7 @@ local vfs = require('openmw.vfs') local world = require('openmw.world') local I = require('openmw.interfaces') -testing.registerGlobalTest('testTimers', function() +testing.registerGlobalTest('timers', function() testing.expectAlmostEqual(core.getGameTimeScale(), 30, 'incorrect getGameTimeScale() result') testing.expectAlmostEqual(core.getSimulationTimeScale(), 1, 'incorrect getSimulationTimeScale result') @@ -41,7 +41,7 @@ testing.registerGlobalTest('testTimers', function() testing.expectGreaterOrEqual(ts2, 1, 'async:newUnsavableSimulationTimer failed') end) -testing.registerGlobalTest('testTeleport', function() +testing.registerGlobalTest('teleport', function() local player = world.players[1] player:teleport('', util.vector3(100, 50, 500), util.transform.rotateZ(math.rad(90))) coroutine.yield() @@ -74,14 +74,14 @@ testing.registerGlobalTest('testTeleport', function() testing.expectEqualWithDelta(player.rotation:getYaw(), math.rad(-90), 0.05, 'teleporting changes rotation') end) -testing.registerGlobalTest('testGetGMST', function() +testing.registerGlobalTest('getGMST', function() testing.expectEqual(core.getGMST('non-existed gmst'), nil) testing.expectEqual(core.getGMST('Water_RippleFrameCount'), 4) testing.expectEqual(core.getGMST('Inventory_DirectionalDiffuseR'), 0.5) testing.expectEqual(core.getGMST('Level_Up_Level2'), 'something') end) -testing.registerGlobalTest('testMWScript', function() +testing.registerGlobalTest('MWScript', function() local variableStoreCount = 18 local variableStore = world.mwscript.getGlobalVariables(player) testing.expectEqual(variableStoreCount, #variableStore) @@ -122,7 +122,7 @@ local function testRecordStore(store, storeName, skipPairs) testing.expectEqual(status, true, storeName) end -testing.registerGlobalTest('testRecordStores', function() +testing.registerGlobalTest('record stores', function() for key, type in pairs(types) do if type.records then testRecordStore(type, key) @@ -143,7 +143,7 @@ testing.registerGlobalTest('testRecordStores', function() testRecordStore(types.Player.birthSigns, "birthSigns") end) -testing.registerGlobalTest('testRecordCreation', function() +testing.registerGlobalTest('record creation', function() local newLight = { isCarriable = true, isDynamic = true, @@ -168,7 +168,7 @@ testing.registerGlobalTest('testRecordCreation', function() end end) -testing.registerGlobalTest('testUTF8Chars', function() +testing.registerGlobalTest('UTF-8 characters', function() testing.expectEqual(utf8.codepoint("😀"), 0x1F600) local chars = {} @@ -195,7 +195,7 @@ testing.registerGlobalTest('testUTF8Chars', function() end end) -testing.registerGlobalTest('testUTF8Strings', function() +testing.registerGlobalTest('UTF-8 strings', function() local utf8str = "Hello, 你好, 🌎!" local str = "" @@ -208,7 +208,7 @@ testing.registerGlobalTest('testUTF8Strings', function() testing.expectEqual(utf8.offset(utf8str, 9), 11) end) -testing.registerGlobalTest('testMemoryLimit', function() +testing.registerGlobalTest('memory limit', function() local ok, err = pcall(function() local t = {} local n = 1 @@ -228,7 +228,7 @@ local function initPlayer() return player end -testing.registerGlobalTest('testVFS', function() +testing.registerGlobalTest('vfs', function() local file = 'test_vfs_dir/lines.txt' local nosuchfile = 'test_vfs_dir/nosuchfile' testing.expectEqual(vfs.fileExists(file), true, 'lines.txt should exist') @@ -274,9 +274,9 @@ testing.registerGlobalTest('testVFS', function() end end) -testing.registerGlobalTest('testCommitCrime', function() +testing.registerGlobalTest('commit crime', function() local player = initPlayer() - testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `testCommitCrime`') + testing.expectEqual(player == nil, false, 'A viable player reference should exist to run `commit crime`') testing.expectEqual(I.Crimes == nil, false, 'Crimes interface should be available in global contexts') -- Reset crime level to have a clean slate @@ -296,8 +296,8 @@ testing.registerGlobalTest('testCommitCrime', function() testing.expectEqual(types.Player.getCrimeLevel(player), 0, "Crime level should not change if the victim's alarm value is low and there's no other witnesses") end) -testing.registerGlobalTest('testRecordModelProperty', function() - local player = initPlayer() +testing.registerGlobalTest('record model property', function() + local player = world.players[1] testing.expectEqual(types.NPC.record(player).model, 'meshes/basicplayer.dae') end) @@ -308,27 +308,27 @@ local function registerPlayerTest(name) end) end -registerPlayerTest('playerYawRotation') -registerPlayerTest('playerPitchRotation') -registerPlayerTest('playerPitchAndYawRotation') -registerPlayerTest('playerRotation') -registerPlayerTest('playerForwardRunning') -registerPlayerTest('playerDiagonalWalking') +registerPlayerTest('player yaw rotation') +registerPlayerTest('player pitch rotation') +registerPlayerTest('player pitch and yaw rotation') +registerPlayerTest('player rotation') +registerPlayerTest('player forward running') +registerPlayerTest('player diagonal walking') registerPlayerTest('findPath') registerPlayerTest('findRandomPointAroundCircle') registerPlayerTest('castNavigationRay') registerPlayerTest('findNearestNavMeshPosition') -registerPlayerTest('playerMemoryLimit') +registerPlayerTest('player memory limit') -testing.registerGlobalTest('playerWeaponAttack', function() +testing.registerGlobalTest('player weapon attack', function() local player = initPlayer() world.createObject('basic_dagger1h', 1):moveInto(player) - testing.runLocalTest(player, 'playerWeaponAttack') + testing.runLocalTest(player, 'player weapon attack') end) return { engineHandlers = { - onUpdate = testing.makeUpdateGlobal(), + onUpdate = testing.updateGlobal, }, eventHandlers = testing.globalEventHandlers, } diff --git a/scripts/data/integration_tests/test_lua_api/menu.lua b/scripts/data/integration_tests/test_lua_api/menu.lua new file mode 100644 index 0000000000..37f7cb826e --- /dev/null +++ b/scripts/data/integration_tests/test_lua_api/menu.lua @@ -0,0 +1,43 @@ +local testing = require('testing_util') +local menu = require('openmw.menu') + +local function registerGlobalTest(name, description) + testing.registerMenuTest(description or name, function() + menu.newGame() + coroutine.yield() + testing.runGlobalTest(name) + end) +end + +registerGlobalTest('timers') +registerGlobalTest('teleport') +registerGlobalTest('getGMST') +registerGlobalTest('MWScript') +registerGlobalTest('record stores') +registerGlobalTest('record creation') +registerGlobalTest('UTF-8 characters') +registerGlobalTest('UTF-8 strings') +registerGlobalTest('memory limit') +registerGlobalTest('vfs') +registerGlobalTest('commit crime') +registerGlobalTest('record model property') + +registerGlobalTest('player yaw rotation', 'rotating player with controls.yawChange should change rotation') +registerGlobalTest('player pitch rotation', 'rotating player with controls.pitchChange should change rotation') +registerGlobalTest('player pitch and yaw rotation', 'rotating player with controls.pitchChange and controls.yawChange should change rotation') +registerGlobalTest('player rotation', 'rotating player should not lead to nan rotation') +registerGlobalTest('player forward running') +registerGlobalTest('player diagonal walking') +registerGlobalTest('findPath') +registerGlobalTest('findRandomPointAroundCircle') +registerGlobalTest('castNavigationRay') +registerGlobalTest('findNearestNavMeshPosition') +registerGlobalTest('player memory limit') +registerGlobalTest('player weapon attack', 'player with equipped weapon on attack should damage health of other actors') + +return { + engineHandlers = { + onFrame = testing.makeUpdateMenu(), + }, + eventHandlers = testing.menuEventHandlers, +} diff --git a/scripts/data/integration_tests/test_lua_api/openmw.cfg b/scripts/data/integration_tests/test_lua_api/openmw.cfg index 68e50644a0..efd1cf331c 100644 --- a/scripts/data/integration_tests/test_lua_api/openmw.cfg +++ b/scripts/data/integration_tests/test_lua_api/openmw.cfg @@ -1,4 +1,4 @@ -content=test.omwscripts +content=test_lua_api.omwscripts # Needed to test `core.getGMST` fallback=Water_RippleFrameCount,4 diff --git a/scripts/data/integration_tests/test_lua_api/player.lua b/scripts/data/integration_tests/test_lua_api/player.lua index 9620866e6e..74769da8e0 100644 --- a/scripts/data/integration_tests/test_lua_api/player.lua +++ b/scripts/data/integration_tests/test_lua_api/player.lua @@ -40,7 +40,7 @@ local function rotateByPitch(object, target) rotate(object, target, nil) end -testing.registerLocalTest('playerYawRotation', +testing.registerLocalTest('player yaw rotation', function() local initialAlphaXZ, initialGammaXZ = self.rotation:getAnglesXZ() local initialAlphaZYX, initialBetaZYX, initialGammaZYX = self.rotation:getAnglesZYX() @@ -60,7 +60,7 @@ testing.registerLocalTest('playerYawRotation', testing.expectEqualWithDelta(gamma2, initialGammaZYX, 0.05, 'Gamma rotation in ZYX convention should not change') end) -testing.registerLocalTest('playerPitchRotation', +testing.registerLocalTest('player pitch rotation', function() local initialAlphaXZ, initialGammaXZ = self.rotation:getAnglesXZ() local initialAlphaZYX, initialBetaZYX, initialGammaZYX = self.rotation:getAnglesZYX() @@ -80,7 +80,7 @@ testing.registerLocalTest('playerPitchRotation', testing.expectEqualWithDelta(gamma2, targetPitch, 0.05, 'Incorrect gamma rotation in ZYX convention') end) -testing.registerLocalTest('playerPitchAndYawRotation', +testing.registerLocalTest('player pitch and yaw rotation', function() local targetPitch = math.rad(-30) local targetYaw = math.rad(-60) @@ -99,7 +99,7 @@ testing.registerLocalTest('playerPitchAndYawRotation', testing.expectEqualWithDelta(gamma2, math.rad(-16), 0.05, 'Incorrect gamma rotation in ZYX convention') end) -testing.registerLocalTest('playerRotation', +testing.registerLocalTest('player rotation', function() local rotation = math.sqrt(2) local endTime = core.getSimulationTime() + 3 @@ -123,7 +123,7 @@ testing.registerLocalTest('playerRotation', end end) -testing.registerLocalTest('playerForwardRunning', +testing.registerLocalTest('player forward running', function() local startPos = self.position local endTime = core.getSimulationTime() + 1 @@ -141,7 +141,7 @@ testing.registerLocalTest('playerForwardRunning', testing.expectEqualWithDelta(direction.y, 1, 0.1, 'Run forward, Y coord') end) -testing.registerLocalTest('playerDiagonalWalking', +testing.registerLocalTest('player diagonal walking', function() local startPos = self.position local endTime = core.getSimulationTime() + 1 @@ -220,7 +220,7 @@ testing.registerLocalTest('findNearestNavMeshPosition', 'Navigation mesh position ' .. testing.formatActualExpected(result, expected)) end) -testing.registerLocalTest('playerMemoryLimit', +testing.registerLocalTest('player memory limit', function() local ok, err = pcall(function() local str = 'a' @@ -232,7 +232,7 @@ testing.registerLocalTest('playerMemoryLimit', testing.expectEqual(err, 'not enough memory') end) -testing.registerLocalTest('playerWeaponAttack', +testing.registerLocalTest('player weapon attack', function() camera.setMode(camera.MODE.ThirdPerson) diff --git a/scripts/data/integration_tests/test_lua_api/test.omwscripts b/scripts/data/integration_tests/test_lua_api/test.omwscripts deleted file mode 100644 index 80507392f7..0000000000 --- a/scripts/data/integration_tests/test_lua_api/test.omwscripts +++ /dev/null @@ -1,2 +0,0 @@ -GLOBAL: test.lua -PLAYER: player.lua diff --git a/scripts/data/integration_tests/test_lua_api/test_lua_api.omwscripts b/scripts/data/integration_tests/test_lua_api/test_lua_api.omwscripts new file mode 100644 index 0000000000..4ce925e61d --- /dev/null +++ b/scripts/data/integration_tests/test_lua_api/test_lua_api.omwscripts @@ -0,0 +1,3 @@ +MENU: menu.lua +GLOBAL: global.lua +PLAYER: player.lua diff --git a/scripts/data/integration_tests/testing_util/testing_util.lua b/scripts/data/integration_tests/testing_util/testing_util.lua index f5de16fb2d..5a69cb89ed 100644 --- a/scripts/data/integration_tests/testing_util/testing_util.lua +++ b/scripts/data/integration_tests/testing_util/testing_util.lua @@ -3,29 +3,21 @@ 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 -function M.makeUpdateGlobal() - local fn = function() - for i, test in ipairs(globalTestsOrder) 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 function makeTestCoroutine(fn) local co = coroutine.create(fn) return function() if coroutine.status(co) ~= 'dead' then @@ -34,11 +26,64 @@ function M.makeUpdateGlobal() 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 @@ -208,11 +253,38 @@ 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)) + error(string.format('localTestFinished with incorrect name %s, expected %s', data.name, currentLocalTest), 2) end currentLocalTest = nil currentLocalTestError = data.errMsg diff --git a/scripts/integration_tests.py b/scripts/integration_tests.py index 850ac30b71..80c97f8b73 100755 --- a/scripts/integration_tests.py +++ b/scripts/integration_tests.py @@ -83,7 +83,7 @@ def run_test(test_name): test_success = True fatal_errors = list() with subprocess.Popen( - [openmw_binary, "--replace=config", "--config", config_dir, "--skip-menu", "--no-grab", "--no-sound"], + [openmw_binary, "--replace=config", "--config", config_dir, "--no-grab"], stdout=subprocess.PIPE, stderr=subprocess.STDOUT, encoding="utf-8", From 2892e19c4346d30a2b5f86ef0c04e7b03d3d8126 Mon Sep 17 00:00:00 2001 From: elsid Date: Wed, 26 Feb 2025 23:42:16 +0100 Subject: [PATCH 7/7] Run integration tests with verbose output --- CI/run_integration_tests.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CI/run_integration_tests.sh b/CI/run_integration_tests.sh index e79408926a..6cc3bd55bf 100755 --- a/CI/run_integration_tests.sh +++ b/CI/run_integration_tests.sh @@ -9,7 +9,7 @@ git checkout FETCH_HEAD cd .. xvfb-run --auto-servernum --server-args='-screen 0 640x480x24x60' \ - scripts/integration_tests.py --omw build/install/bin/openmw --workdir integration_tests_output example-suite/ + scripts/integration_tests.py --verbose --omw build/install/bin/openmw --workdir integration_tests_output example-suite/ ls integration_tests_output/*.osg_stats.log | while read v; do scripts/osg_stats.py --stats '.*' --regexp_match < "${v}"