diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 40b30cd13..6255219e6 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -67,19 +67,25 @@ jobs: compiler: msvc plugins: "default" config: "empty" - # TODO: uncomment once we have a linux build we can download from bay12 - # - os: ubuntu - # compiler: gcc-10 - # plugins: "default" - # config: "default" - # - os: ubuntu - # compiler: gcc-12 - # plugins: "all" - # config: "default" + - os: ubuntu + compiler: gcc-10 + plugins: "default" + config: "default" + - os: ubuntu + compiler: gcc-12 + plugins: "all" + config: "default" steps: - name: Set env shell: bash run: echo "DF_FOLDER=DF" >> $GITHUB_ENV + - name: Install dependencies + if: matrix.os == 'ubuntu' + run: | + sudo apt-get update + sudo apt-get install \ + libsdl2-2.0-0 \ + libsdl2-image-2.0-0 - name: Clone DFHack uses: actions/checkout@v3 with: @@ -117,8 +123,14 @@ jobs: - name: Install DFHack shell: bash run: tar xjf test-${{ matrix.compiler }}.tar.bz2 -C ${{ env.DF_FOLDER }} + - name: Start X server + if: matrix.os == 'ubuntu' + run: Xvfb :0 -screen 0 1600x1200x24 & - name: Run lua tests timeout-minutes: 10 + env: + DISPLAY: :0 + TERM: xterm-256color run: python ci/run-tests.py --keep-status "${{ env.DF_FOLDER }}" - name: Check RPC interface run: python ci/check-rpc.py "${{ env.DF_FOLDER }}/dfhack-rpc.txt" diff --git a/ci/download-df.sh b/ci/download-df.sh index 12e9a41e3..399b75714 100755 --- a/ci/download-df.sh +++ b/ci/download-df.sh @@ -18,7 +18,7 @@ elif test "$OS_TARGET" = "ubuntu"; then WGET=wget df_url="${df_url}_linux.tar.bz2" df_archive_name="df.tar.bz2" - df_extract_cmd="tar -x -j --strip-components=1 -f" + df_extract_cmd="tar -x -j -C ${DF_FOLDER} -f" else echo "Unhandled OS target: ${OS_TARGET}" exit 1 @@ -29,22 +29,25 @@ if ! $WGET -v "$df_url" -O "$df_archive_name"; then exit 1 fi +md5sum "$df_archive_name" + save_url="https://dffd.bay12games.com/download.php?id=15434&f=dreamfort.7z" save_archive_name="test_save.7z" -save_extract_cmd="7z x -oDF/save" +save_extract_cmd="7z x -o${DF_FOLDER}/save" if ! $WGET -v "$save_url" -O "$save_archive_name"; then echo "Failed to download test save from $save_url" exit 1 fi +md5sum "$save_archive_name" + echo Extracting +mkdir -p ${DF_FOLDER} $df_extract_cmd "$df_archive_name" $save_extract_cmd "$save_archive_name" -mv DF/save/* DF/save/region1 +mv ${DF_FOLDER}/save/* ${DF_FOLDER}/save/region1 echo Done ls -l - -md5sum "$df_archive_name" "$save_archive_name" diff --git a/ci/run-tests.py b/ci/run-tests.py index 3d646a2f7..13eeb099c 100755 --- a/ci/run-tests.py +++ b/ci/run-tests.py @@ -65,14 +65,12 @@ if not os.path.exists(init_txt_path): shutil.copyfile(init_txt_path, init_txt_path + '.orig') with open(init_txt_path) as f: init_contents = f.read() -init_contents = change_setting(init_contents, 'INTRO', 'NO') init_contents = change_setting(init_contents, 'SOUND', 'NO') init_contents = change_setting(init_contents, 'WINDOWED', 'YES') -init_contents = change_setting(init_contents, 'WINDOWEDX', '80') -init_contents = change_setting(init_contents, 'WINDOWEDY', '25') -init_contents = change_setting(init_contents, 'FPS', 'YES') -if args.headless: - init_contents = change_setting(init_contents, 'PRINT_MODE', 'TEXT') +init_contents = change_setting(init_contents, 'WINDOWEDX', '1200') +init_contents = change_setting(init_contents, 'WINDOWEDY', '800') +#if args.headless: +# init_contents = change_setting(init_contents, 'PRINT_MODE', 'TEXT') init_path = 'dfhack-config/init' if not os.path.isdir('hack/init'): diff --git a/ci/test.lua b/ci/test.lua index ac0e5718d..372d8f262 100644 --- a/ci/test.lua +++ b/ci/test.lua @@ -2,6 +2,7 @@ --@ module = true local expect = require('test_util.expect') +local gui = require('gui') local helpdb = require('helpdb') local json = require('json') local mock = require('test_util.mock') @@ -151,33 +152,37 @@ end test_envvars.require = clean_require test_envvars.reqscript = clean_reqscript -local function is_title_screen(scr) - scr = scr or dfhack.gui.getCurViewscreen() - return df.viewscreen_titlest:is_instance(scr) +local function is_title_screen() + return dfhack.gui.matchFocusString('title/Default') end --- This only handles pre-fortress-load screens. It will time out if the player --- has already loaded a fortress or is in any screen that can't get to the title --- screen by sending ESC keys. -local function ensure_title_screen() +local function wait_for(ms, desc, predicate) local start_ms = dfhack.getTickCount() local prev_ms = start_ms - while df.viewscreen_initial_prepst:is_instance(dfhack.gui.getCurViewscreen()) do + while not predicate() do delay(10) - -- wait up to 1 minute for the game to load and show the title screen local now_ms = dfhack.getTickCount() - if now_ms - start_ms > 60000 then - qerror(('Could not find title screen (timed out at %s)'):format( - dfhack.gui.getCurFocus(true)[1])) + if now_ms - start_ms > ms then + qerror(('%s took too long (timed out at %s)'):format( + desc, dfhack.gui.getCurFocus(true)[1])) end if now_ms - prev_ms > 1000 then - print('Waiting for game to load and show title screen...') + print(('Waiting for %s...'):format(desc)) prev_ms = now_ms end end +end + +-- This only handles pre-fortress-load screens. It will time out if the player +-- has already loaded a fortress or is in any screen that can't get to the title +-- screen by sending ESC keys. +local function ensure_title_screen() + if df.viewscreen_dwarfmodest:is_instance(dfhack.gui.getDFViewscreen(true)) then + qerror('Cannot reach title screen from loaded fort') + end for i = 1, 100 do local scr = dfhack.gui.getCurViewscreen() - if is_title_screen(scr) then + if is_title_screen() then print('Found title screen') return end @@ -189,54 +194,94 @@ local function ensure_title_screen() dfhack.gui.getCurFocus(true)[1])) end -local function is_fortress(focus_string) - focus_string = focus_string or dfhack.gui.getCurFocus(true) - return focus_string == 'dwarfmode/Default' +local function is_fortress() + return dfhack.gui.matchFocusString('dwarfmode/Default') +end + +-- error out if we're not running in a CI environment +-- the tests may corrupt saves, and we don't want to unexpectedly ruin a real player save +-- this heuristic is not perfect, but it should be able to detect most cases +local function ensure_ci_save(scr) + if #scr.savegame_header ~= 1 + or #scr.savegame_header_world ~= 1 + or not string.find(scr.savegame_header[0].fort_name, 'Dream') + then + qerror('Unexpected test save in slot 0; please manually load a fort for ' .. + 'running fortress mode tests. note that tests may alter or corrupt the ' .. + 'fort! Do not save after running tests.') + end +end + +local function click_top_title_button(scr) + local sw, sh = dfhack.screen.getWindowSize() + df.global.gps.mouse_x = sw // 2 + df.global.gps.precise_mouse_x = df.global.gps.mouse_x * df.global.gps.tile_pixel_x + if sh < 60 then + df.global.gps.mouse_y = 25 + else + df.global.gps.mouse_y = (sh // 2) + 3 + end + df.global.gps.precise_mouse_y = df.global.gps.mouse_y * df.global.gps.tile_pixel_y + df.global.enabler.tracking_on = 1 + df.global.enabler.mouse_lbut = 1 + df.global.enabler.mouse_lbut_down = 1 + gui.simulateInput(scr, '_MOUSE_L') +end + +local function load_first_save(scr) + if #scr.savegame_header == 0 then + qerror('no savegames available to load') + end + + click_top_title_button(scr) + wait_for(1000, 'world list', function() + return scr.mode == 2 + end) + click_top_title_button(scr) + wait_for(1000, 'savegame list', function() + return scr.mode == 3 + end) + click_top_title_button(scr) + wait_for(1000, 'loadgame progress bar', function() + return dfhack.gui.matchFocusString('loadgame') + end) end -- Requires that a fortress game is already loaded or is ready to be loaded via --- the "Continue Playing" option in the title screen. Otherwise the function +-- the "Continue active game" option in the title screen. Otherwise the function -- will time out and/or exit with error. local function ensure_fortress(config) - local focus_string = dfhack.gui.getCurFocus(true) for screen_timeout = 1,10 do - if is_fortress(focus_string) then - print('Loaded fortress map') + if is_fortress() then + print('Fortress map is loaded') -- pause the game (if it's not already paused) dfhack.gui.resetDwarfmodeView(true) return end - local scr = dfhack.gui.getCurViewscreen(true) - if focus_string == 'title' or - focus_string == 'dfhack/lua/load_screen' then + local scr = dfhack.gui.getCurViewscreen() + if dfhack.gui.matchFocusString('title/Default', scr) then + print('Attempting to load the test fortress') + -- TODO: reinstate loading of a specified save dir; for now + -- just load the first possible save, which will at least let us + -- run fortress tests in CI -- qerror()'s on falure - dfhack.run_script('load-save', config.save_dir) - elseif focus_string ~= 'loadgame' then + -- dfhack.run_script('load-save', config.save_dir) + ensure_ci_save(scr) + load_first_save(scr) + elseif not dfhack.gui.matchFocusString('loadgame', scr) then -- if we're not actively loading a game, hope we're in -- a screen where hitting ESC will get us to the game map -- or the title screen scr:feed_key(df.interface_key.LEAVESCREEN) end -- wait for current screen to change - local prev_focus_string = focus_string - for frame_timeout = 1,100 do - delay(10) - focus_string = dfhack.gui.getCurFocus(true) - if focus_string ~= prev_focus_string then - goto next_screen - end - if frame_timeout % 10 == 0 then - print(string.format( - 'Loading fortress (currently at screen: %s)', - focus_string)) - end - end - print('Timed out waiting for screen to change') - break - ::next_screen:: + local prev_focus_string = dfhack.gui.getCurFocus()[1] + wait_for(60000, 'screen change', function() + return dfhack.gui.getCurFocus()[1] ~= prev_focus_string + end) end qerror(string.format('Could not load fortress (timed out at %s)', - focus_string)) + table.concat(dfhack.gui.getCurFocus(), ' '))) end local MODES = { @@ -578,6 +623,10 @@ local function filter_tests(tests, config) end local function run_tests(tests, status, counts, config) + wait_for(60000, 'game load', function() + local scr = dfhack.gui.getDFViewscreen() + return not df.viewscreen_initial_prepst:is_instance(scr) + end) print(('Running %d tests'):format(#tests)) local start_ms = dfhack.getTickCount() local num_skipped = 0 @@ -596,12 +645,13 @@ local function run_tests(tests, status, counts, config) goto skip end end + -- pre-emptively mark the test as failed in case we induce a crash + status[test.full_name] = TestStatus.FAILED + save_test_status(status) if run_test(test, status, counts) then status[test.full_name] = TestStatus.PASSED - else - status[test.full_name] = TestStatus.FAILED + save_test_status(status) end - save_test_status(status) ::skip:: end local elapsed_ms = dfhack.getTickCount() - start_ms diff --git a/test/plugins/orders.lua b/test/plugins/orders.lua index c5fae8eb2..d851adb02 100644 --- a/test/plugins/orders.lua +++ b/test/plugins/orders.lua @@ -1,5 +1,5 @@ config.mode = 'fortress' ---config.target = 'orders' +config.target = 'orders' local FILE_PATH_PATTERN = 'dfhack-config/orders/%s.json'