From 005776989b0dc83656a0b3a0a459fb7ede2c561e Mon Sep 17 00:00:00 2001 From: mcarans Date: Sun, 5 Apr 2026 20:37:57 +1200 Subject: [PATCH 01/25] Add JS quit command and related Obj-C plumbing Make AddOns folder in oolite.app not build Add Python test that launches Oolite, takes a snapshot and quits --- .../Debug.oxp/Scripts/oolite-debug-console.js | 11 ++ Makefile | 7 +- ShellScripts/Linux/install_freedesktop_fn.sh | 16 +- ShellScripts/common/post_build.sh | 3 +- installers/appimage/create_appimage.sh | 5 - installers/win32/OOlite.nsi | 4 +- src/Core/Scripting/OOJSGlobal.m | 18 ++ src/Core/Universe.h | 1 + src/Core/Universe.m | 7 + tests/oolite_tester.py | 168 ++++++++++++++++++ 10 files changed, 223 insertions(+), 17 deletions(-) create mode 100644 tests/oolite_tester.py diff --git a/DebugOXP/Debug.oxp/Scripts/oolite-debug-console.js b/DebugOXP/Debug.oxp/Scripts/oolite-debug-console.js index 6860594ba..9e987d836 100644 --- a/DebugOXP/Debug.oxp/Scripts/oolite-debug-console.js +++ b/DebugOXP/Debug.oxp/Scripts/oolite-debug-console.js @@ -858,6 +858,17 @@ this.evaluate = function evaluate(command, PARAM) } } +this.quit = function() { + log("debugConsole.automation", "Triggering global quitGame bridge..."); + + // This calls the native function you added to OOJSGlobal.m, + // which in turn calls [Universe quitGame] + if (typeof quitGame === "function") { + quitGame(); + } else { + log("debugConsole.automation", "Error: quitGame() global function not found. Did you recompile?"); + } +}; // Identify the location of the eval() command above for the debug location formatter. this.markConsoleEntryPoint = special.markConsoleEntryPoint; diff --git a/Makefile b/Makefile index 4a00c2cd4..fbc2c8254 100755 --- a/Makefile +++ b/Makefile @@ -33,7 +33,7 @@ help: .PHONY: release release: $(MAKE) -f GNUmakefile debug=no strip=yes lto=yes - mkdir -p build/AddOns && rm -rf build/AddOns/Basic-debug.oxp && cp -rf DebugOXP/Debug.oxp build/AddOns/Basic-debug.oxp + mkdir -p oolite.app/AddOns && rm -rf oolite.app/AddOns/Basic-debug.oxp && cp -rf DebugOXP/Debug.oxp oolite.app/AddOns/Basic-debug.oxp .PHONY: release-deployment release-deployment: @@ -42,18 +42,17 @@ release-deployment: .PHONY: release-snapshot release-snapshot: $(MAKE) -f GNUmakefile SNAPSHOT_BUILD=yes debug=no - mkdir -p build/AddOns && rm -rf build/AddOns/Basic-debug.oxp && cp -rf DebugOXP/Debug.oxp build/AddOns/Basic-debug.oxp + mkdir -p oolite.app/AddOns && rm -rf oolite.app/AddOns/Basic-debug.oxp && cp -rf DebugOXP/Debug.oxp oolite.app/AddOns/Basic-debug.oxp .PHONY: debug debug: $(MAKE) -f GNUmakefile debug=yes strip=no - mkdir -p build/AddOns && rm -rf build/AddOns/Basic-debug.oxp && cp -rf DebugOXP/Debug.oxp build/AddOns/Basic-debug.oxp + mkdir -p oolite.app/AddOns && rm -rf oolite.app/AddOns/Basic-debug.oxp && cp -rf DebugOXP/Debug.oxp oolite.app/AddOns/Basic-debug.oxp .PHONY: clean clean: $(MAKE) -f GNUmakefile clean $(RM) -rf oolite.app - $(RM) -rf build/AddOns .PHONY: all all: release release-deployment release-snapshot debug diff --git a/ShellScripts/Linux/install_freedesktop_fn.sh b/ShellScripts/Linux/install_freedesktop_fn.sh index 26ec37f00..54f42146d 100755 --- a/ShellScripts/Linux/install_freedesktop_fn.sh +++ b/ShellScripts/Linux/install_freedesktop_fn.sh @@ -5,7 +5,7 @@ install_freedesktop() { # $1: app folder (destination) # $2: appdata or metainfo - local err_msg="❌ Error: Failed to install " + local err_msg="❌ Error: Failed to" SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) pushd "$SCRIPT_DIR" @@ -19,13 +19,19 @@ install_freedesktop() { APPSHR="$1/share" # Install binaries and scripts - install -D "$PROGDIR/oolite" "$APPBIN/oolite" || { echo "$err_msg oolite binary" >&2; return 1; } - install -D "$PROGDIR/run_oolite.sh" "$APPBIN/run_oolite.sh" || { echo "$err_msg run_oolite.sh" >&2; return 1; } - install -D "$PROGDIR/splash-launcher" "$APPBIN/splash-launcher" || { echo "$err_msg splash-launcher" >&2; return 1; } + install -D "$PROGDIR/oolite" "$APPBIN/oolite" || { echo "$err_msg install oolite binary" >&2; return 1; } + install -D "$PROGDIR/run_oolite.sh" "$APPBIN/run_oolite.sh" || { echo "$err_msg install run_oolite.sh" >&2; return 1; } + install -D "$PROGDIR/splash-launcher" "$APPBIN/splash-launcher" || { echo "$err_msg install splash-launcher" >&2; return 1; } # Resources copy mkdir -p "$APPBIN/Resources" - cp -rf "$PROGDIR/Resources/." "$APPBIN/Resources/" || { echo "$err_msg Copying Resources folder" >&2; return 1; } + cp -rf "$PROGDIR/Resources/." "$APPBIN/Resources/" || { echo "$err_msg copy Resources folder" >&2; return 1; } + + # AddOns copy if folder exists in oolite.app + if [ -d "$PROGDIR/AddOns" ]; then + mkdir -p "$APPBIN/AddOns" + cp -rf "$PROGDIR/AddOns/." "$APPBIN/AddOns/" || { echo "$err_msg copy AddOns folder" >&2; return 1; } + fi install -D "GNUstep.conf.template" "$APPBIN/Resources/GNUstep.conf.template" || { echo "$err_msg GNUstep template" >&2; return 1; } diff --git a/ShellScripts/common/post_build.sh b/ShellScripts/common/post_build.sh index eca068dff..cf94fcaa5 100755 --- a/ShellScripts/common/post_build.sh +++ b/ShellScripts/common/post_build.sh @@ -44,7 +44,8 @@ run_script() { for resdir in "${RESOURCE_DIRS[@]}"; do cp -rfu "$resdir" "$PROGDIR/Resources" done - + + cp -fu tests/oolite_tester.py "$PROGDIR" cp -fu Resources/README.TXT "$PROGDIR/Resources" cp -fu Resources/InfoPlist.strings "$PROGDIR/Resources" cp -fu src/Cocoa/Info-Oolite.plist "$PROGDIR/Resources/Info-gnustep.plist" diff --git a/installers/appimage/create_appimage.sh b/installers/appimage/create_appimage.sh index 3bae45325..5c1f9a594 100755 --- a/installers/appimage/create_appimage.sh +++ b/installers/appimage/create_appimage.sh @@ -22,11 +22,6 @@ run_script() { return 1 fi - if (( $# == 1 )); then - echo "Including Basic-debug.oxp" - cp -rf AddOns "$APPBIN" - fi - LINUXDEPLOY_BIN="./linuxdeploy" if [ ! -x "$LINUXDEPLOY_BIN" ]; then echo "📥 linuxdeploy not found or not executable. Downloading..." diff --git a/installers/win32/OOlite.nsi b/installers/win32/OOlite.nsi index 8bd1e6394..dfb6a4764 100644 --- a/installers/win32/OOlite.nsi +++ b/installers/win32/OOlite.nsi @@ -133,7 +133,7 @@ SectionEnd Section "Basic-debug.OXP" ooDebugOXP ; Do not use any of the Debug OXP files when we are building Deployment SetOutPath $INSTDIR -File /r "..\..\build\AddOns" +File /r "${DST}\AddOns" SectionEnd ; Below are the descriptions of the two component sections @@ -251,7 +251,7 @@ File "Privacy.pdf" ${If} ${ADDCHANGELOG} == "1" File "..\..\Doc\CHANGELOG.TXT" ${EndIf} -File /r /x .git /x .svn /x *~ "${DST}" +File /r /x AddOns .git /x .svn /x *~ "${DST}" ; Generate version info FileOpen $9 release.txt w ;Opens a Empty File and fills it diff --git a/src/Core/Scripting/OOJSGlobal.m b/src/Core/Scripting/OOJSGlobal.m index 6f9ceece8..0fc36b9f2 100644 --- a/src/Core/Scripting/OOJSGlobal.m +++ b/src/Core/Scripting/OOJSGlobal.m @@ -74,6 +74,7 @@ - (void)sendMonitorLogMessage:(NSString *)message static JSBool GlobalSetScreenBackgroundForKey(JSContext *context, uintN argc, jsval *vp); static JSBool GlobalAutoAIForRole(JSContext *context, uintN argc, jsval *vp); static JSBool GlobalPauseGame(JSContext *context, uintN argc, jsval *vp); +static JSBool GlobalQuitGame(JSContext *context, uintN argc, jsval *vp); static JSBool GlobalGetGuiColorSettingForKey(JSContext *context, uintN argc, jsval *vp); static JSBool GlobalSetGuiColorSettingForKey(JSContext *context, uintN argc, jsval *vp); static JSBool GlobalSetExtraGuiScreenKeys(JSContext *context, uintN argc, jsval *vp); @@ -153,6 +154,7 @@ - (void)sendMonitorLogMessage:(NSString *)message { "takeSnapShot", GlobalTakeSnapShot, 1 }, #endif { "pauseGame", GlobalPauseGame, 0 }, + { "quitGame", GlobalQuitGame, 0 }, { 0 } }; @@ -853,3 +855,19 @@ static JSBool GlobalPauseGame(JSContext *context, uintN argc, jsval *vp) OOJS_NATIVE_EXIT } + +// quitGame() : Boolean +static JSBool GlobalQuitGame(JSContext *context, uintN argc, jsval *vp) +{ + OOJS_NATIVE_ENTER(context) + + // Log the event so you see it in Latest.log + OOLog(@"script.debug.quit", @"Quit requested via JavaScript global.quitGame()"); + + // Call the method you just added to Universe + [UNIVERSE quitGame]; + + OOJS_RETURN_BOOL(YES); + + OOJS_NATIVE_EXIT +} \ No newline at end of file diff --git a/src/Core/Universe.h b/src/Core/Universe.h index cb3eca1d0..11d70ad3a 100644 --- a/src/Core/Universe.h +++ b/src/Core/Universe.h @@ -414,6 +414,7 @@ enum #endif - (void) pauseGame; +- (void) quitGame; - (void) carryPlayerOn:(StationEntity*)carrier inWormhole:(WormholeEntity*)wormhole; - (void) setUpUniverseFromStation; diff --git a/src/Core/Universe.m b/src/Core/Universe.m index 8c3afa3e1..eca7ae17f 100644 --- a/src/Core/Universe.m +++ b/src/Core/Universe.m @@ -1050,6 +1050,13 @@ - (void) pauseGame [[self gameController] setGamePaused:YES]; } +- (void) quitGame +{ +OOLog(@"universe.quit", @"Quit command received by Universe."); + + // Use the internal convenience method instead of NSApp + [[self gameController] exitAppWithContext:@"Universe Request"]; +} - (void) carryPlayerOn:(StationEntity*)carrier inWormhole:(WormholeEntity*)wormhole { diff --git a/tests/oolite_tester.py b/tests/oolite_tester.py new file mode 100644 index 000000000..394cefa33 --- /dev/null +++ b/tests/oolite_tester.py @@ -0,0 +1,168 @@ +import socket +import subprocess +import time +import os +import select +import sys +import struct +import plistlib +import tempfile +import shutil + +# --- PROTOCOL CONSTANTS (from _protocol.py) --- +# Using the exact string definitions required by the Oolite Debug Protocol +REQUEST_CONNECTION = "Request Connection" +APPROVE_CONNECTION = "Approve Connection" +PERFORM_COMMAND = "Perform Command" +PACKET_TYPE_KEY = "packet type" +MESSAGE_KEY = "message" +CONSOLE_IDENTITY_KEY = "console identity" +OOLITE_VERSION_KEY = "Oolite version" + +# --- CONFIGURATION --- +OOLITE_BIN = "./oolite" +PORT = 8563 +HOST = "127.0.0.1" + +def send_plist_packet(sock, packet): + """ + Implements the framing protocol from PropertyListPacketProtocol.py: + 1. Encodes the dictionary as an XML Property List. + 2. Calculates a 32-bit big-endian integer for the length. + 3. Prepends this 4-byte header to the data. + """ + try: + # Matches writePlistToString logic for Python 3 + data = plistlib.dumps(packet, fmt=plistlib.FMT_XML) + length = len(data) + + # Header is 4 bytes, network-endian (Big-Endian) + header = struct.pack(">I", length) + + sock.sendall(header + data) + return True + except Exception as e: + print(f"[!] Failed to encode/send packet: {e}") + return False + +def receive_plist_packet(sock): + """ + Implements the receiving state machine from PropertyListPacketProtocol.py: + 1. Reads the 4-byte header to determine expected length. + 2. Reads the specific number of bytes for the XML payload. + """ + try: + header = sock.recv(4) + if len(header) < 4: return None + + length = struct.unpack(">I", header)[0] + data = b"" + while len(data) < length: + chunk = sock.recv(length - len(data)) + if not chunk: break + data += chunk + + return plistlib.loads(data) + except Exception as e: + print(f"[!] Error receiving packet: {e}") + return None + +def run_test(snapshots_dir): + # Setup TCP Server + server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) + server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) + server_sock.bind((HOST, PORT)) + server_sock.listen(1) + server_sock.setblocking(False) + + print(f"[*] Console server listening on {PORT}") + + # Environment configuration for headless snapshotting + env = os.environ.copy() + env["SDL_AUDIODRIVER"] = "dummy" + env["ALSOFT_DRIVERS"] = "null" + env["OO_SNAPSHOTSDIR"] = snapshots_dir + + # Launch Oolite with -p to trigger debug console connection + cmd = ["xwfb-run", "--", OOLITE_BIN, "--no-splash", "-p"] + proc = subprocess.Popen(cmd, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + + conn = None + try: + # 1. Wait for Oolite to initiate connection + timeout = time.time() + 20 + while time.time() < timeout: + readable, _, _ = select.select([server_sock], [], [], 1) + if readable: + conn, addr = server_sock.accept() + print(f"[+] Oolite connected from {addr}") + break + + if not conn: + print("[!] Failure: Oolite failed to connect.") + return False + + # 2. THE HANDSHAKE (Required by OoliteDebugConsoleProtocol.py) + # Wait for 'Request Connection' from Oolite + pkt = receive_plist_packet(conn) + + if pkt and pkt.get(PACKET_TYPE_KEY) == REQUEST_CONNECTION: + print(f"[+] Handshake started. Oolite version: {pkt.get(OOLITE_VERSION_KEY)}") + + # Respond with 'Approve Connection' and identity + approval = { + PACKET_TYPE_KEY: APPROVE_CONNECTION, + CONSOLE_IDENTITY_KEY: "OoliteAutomationTester" + } + send_plist_packet(conn, approval) + print("[*] Connection Approved.") + else: + print("[!] Handshake failed: Expected 'Request Connection'.") + return False + + # Allow time for engine state transition + time.sleep(1) + + # 3. THE COMMAND (Using Perform Command packet type) + # Combines snapshot and quit into one execution string + print("[*] Requesting snapshot and quit...") + cmd_packet = { + PACKET_TYPE_KEY: PERFORM_COMMAND, + MESSAGE_KEY: "takeSnapShot(); quit();" + } + send_plist_packet(conn, cmd_packet) + + # 4. WAIT FOR PROCESS EXIT + print("[*] Waiting for process to exit...") + try: + proc.wait(timeout=15) + + # 5. VERIFY OUTPUT + snaps = [f for f in os.listdir(snapshots_dir) if f.endswith(".png")] + if snaps: + snap_path = os.path.join(snapshots_dir, snaps[0]) + print(f"[+] Success: Captured {snap_path} ({os.path.getsize(snap_path)//1024} KB)") + return True + else: + print("[!] Error: Oolite exited but no snapshot was found.") + return False + + except subprocess.TimeoutExpired: + print("[!] Error: Oolite ignored the shutdown command.") + return False + + finally: + if conn: conn.close() + server_sock.close() + if proc.poll() is None: + proc.kill() + +if __name__ == "__main__": + # Use a temporary directory for snapshot storage + temp_dir = tempfile.mkdtemp(prefix="oolite_test_") + try: + success = run_test(temp_dir) + finally: + shutil.rmtree(temp_dir) + + sys.exit(0 if success else 1) \ No newline at end of file From 19407708d0a96ca4f5466da0219ad23f2ade6b86 Mon Sep 17 00:00:00 2001 From: mcarans Date: Mon, 6 Apr 2026 14:18:24 +1200 Subject: [PATCH 02/25] Add launch snapshot test Add make -f Makefile test Add to test_builds workflow Add Windows test build --- .github/workflows/test_builds.yaml | 43 +++++++- Makefile | 4 + ShellScripts/common/post_build.sh | 1 - .../{oolite_tester.py => launch_snapshot.py} | 104 ++++++++++++++---- tests/run_test.sh | 32 ++++++ 5 files changed, 156 insertions(+), 28 deletions(-) rename tests/{oolite_tester.py => launch_snapshot.py} (62%) create mode 100755 tests/run_test.sh diff --git a/.github/workflows/test_builds.yaml b/.github/workflows/test_builds.yaml index 34d7bedc3..0d8c107c0 100644 --- a/.github/workflows/test_builds.yaml +++ b/.github/workflows/test_builds.yaml @@ -23,7 +23,7 @@ jobs: sudo ShellScripts/Linux/install_deps_root.sh - name: Build Oolite run: | - ShellScripts/common/build_oolite.sh pkg-appimage + ShellScripts/common/build_oolite.sh test-snapshot fedora: runs-on: ubuntu-latest @@ -50,7 +50,7 @@ jobs: ShellScripts/Linux/install_deps_root.sh - name: Build Oolite run: | - ShellScripts/common/build_oolite.sh pkg-appimage + ShellScripts/common/build_oolite.sh test-snapshot arch: runs-on: ubuntu-latest @@ -77,4 +77,41 @@ jobs: ShellScripts/Linux/install_deps_root.sh - name: Build Oolite run: | - ShellScripts/common/build_oolite.sh pkg-appimage + ShellScripts/common/build_oolite.sh test-snapshot + + windows: + runs-on: windows-latest + steps: + - name: Set up MSYS2 + uses: msys2/setup-msys2@v2 + with: + update: true + + - name: Checkout Oolite + uses: actions/checkout@v6 + with: + path: oolite + fetch-depth: 0 + submodules: true + + - name: Download latest release assets + uses: robinraju/release-downloader@v1 + with: + repository: OoliteProject/oolite_windeps_build + latest: true + fileName: "*" + out-file-path: oolite/build/packages + + - name: Install packages + shell: msys2 {0} + env: + msystem: UCRT64 + run: | + oolite/ShellScripts/Windows/install_deps.sh clang + + - name: Build Oolite + shell: msys2 {0} + env: + msystem: UCRT64 + run: | + oolite/ShellScripts/common/build_oolite.sh test-snapshot diff --git a/Makefile b/Makefile index fbc2c8254..e5c02f74f 100755 --- a/Makefile +++ b/Makefile @@ -49,6 +49,10 @@ debug: $(MAKE) -f GNUmakefile debug=yes strip=no mkdir -p oolite.app/AddOns && rm -rf oolite.app/AddOns/Basic-debug.oxp && cp -rf DebugOXP/Debug.oxp oolite.app/AddOns/Basic-debug.oxp +.PHONY: test +test: release-snapshot + tests/run_test.sh + .PHONY: clean clean: $(MAKE) -f GNUmakefile clean diff --git a/ShellScripts/common/post_build.sh b/ShellScripts/common/post_build.sh index cf94fcaa5..ce6c05a64 100755 --- a/ShellScripts/common/post_build.sh +++ b/ShellScripts/common/post_build.sh @@ -45,7 +45,6 @@ run_script() { cp -rfu "$resdir" "$PROGDIR/Resources" done - cp -fu tests/oolite_tester.py "$PROGDIR" cp -fu Resources/README.TXT "$PROGDIR/Resources" cp -fu Resources/InfoPlist.strings "$PROGDIR/Resources" cp -fu src/Cocoa/Info-Oolite.plist "$PROGDIR/Resources/Info-gnustep.plist" diff --git a/tests/oolite_tester.py b/tests/launch_snapshot.py similarity index 62% rename from tests/oolite_tester.py rename to tests/launch_snapshot.py index 394cefa33..30592c814 100644 --- a/tests/oolite_tester.py +++ b/tests/launch_snapshot.py @@ -1,3 +1,4 @@ +import argparse import socket import subprocess import time @@ -11,19 +12,20 @@ # --- PROTOCOL CONSTANTS (from _protocol.py) --- # Using the exact string definitions required by the Oolite Debug Protocol -REQUEST_CONNECTION = "Request Connection" -APPROVE_CONNECTION = "Approve Connection" -PERFORM_COMMAND = "Perform Command" -PACKET_TYPE_KEY = "packet type" -MESSAGE_KEY = "message" -CONSOLE_IDENTITY_KEY = "console identity" -OOLITE_VERSION_KEY = "Oolite version" +REQUEST_CONNECTION = "Request Connection" +APPROVE_CONNECTION = "Approve Connection" +PERFORM_COMMAND = "Perform Command" +PACKET_TYPE_KEY = "packet type" +MESSAGE_KEY = "message" +CONSOLE_IDENTITY_KEY = "console identity" +OOLITE_VERSION_KEY = "Oolite version" # --- CONFIGURATION --- -OOLITE_BIN = "./oolite" +IS_WINDOWS = sys.platform == "win32" or (os.name == "nt") PORT = 8563 HOST = "127.0.0.1" + def send_plist_packet(sock, packet): """ Implements the framing protocol from PropertyListPacketProtocol.py: @@ -45,6 +47,7 @@ def send_plist_packet(sock, packet): print(f"[!] Failed to encode/send packet: {e}") return False + def receive_plist_packet(sock): """ Implements the receiving state machine from PropertyListPacketProtocol.py: @@ -53,13 +56,15 @@ def receive_plist_packet(sock): """ try: header = sock.recv(4) - if len(header) < 4: return None + if len(header) < 4: + return None length = struct.unpack(">I", header)[0] data = b"" while len(data) < length: chunk = sock.recv(length - len(data)) - if not chunk: break + if not chunk: + break data += chunk return plistlib.loads(data) @@ -67,7 +72,8 @@ def receive_plist_packet(sock): print(f"[!] Error receiving packet: {e}") return None -def run_test(snapshots_dir): + +def run_test(bin_name, snapshots_dir): # Setup TCP Server server_sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) server_sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) @@ -83,9 +89,22 @@ def run_test(snapshots_dir): env["ALSOFT_DRIVERS"] = "null" env["OO_SNAPSHOTSDIR"] = snapshots_dir - # Launch Oolite with -p to trigger debug console connection - cmd = ["xwfb-run", "--", OOLITE_BIN, "--no-splash", "-p"] - proc = subprocess.Popen(cmd, env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL) + # Launch Oolite + if IS_WINDOWS: + cmd = [f"./{bin_name}", "--no-splash"] + creation_flags = 0x08000000 + else: + cmd = ["xwfb-run", "--", f"./{bin_name}", "--no-splash"] + creation_flags = 0 + + print(f"[*] Executing: {' '.join(cmd)}") + proc = subprocess.Popen( + cmd, + env=env, + stdout=subprocess.DEVNULL, + stderr=subprocess.DEVNULL, + creationflags=creation_flags, + ) conn = None try: @@ -107,12 +126,14 @@ def run_test(snapshots_dir): pkt = receive_plist_packet(conn) if pkt and pkt.get(PACKET_TYPE_KEY) == REQUEST_CONNECTION: - print(f"[+] Handshake started. Oolite version: {pkt.get(OOLITE_VERSION_KEY)}") + print( + f"[+] Handshake started. Oolite version: {pkt.get(OOLITE_VERSION_KEY)}" + ) # Respond with 'Approve Connection' and identity approval = { PACKET_TYPE_KEY: APPROVE_CONNECTION, - CONSOLE_IDENTITY_KEY: "OoliteAutomationTester" + CONSOLE_IDENTITY_KEY: "OoliteAutomationTester", } send_plist_packet(conn, approval) print("[*] Connection Approved.") @@ -128,7 +149,7 @@ def run_test(snapshots_dir): print("[*] Requesting snapshot and quit...") cmd_packet = { PACKET_TYPE_KEY: PERFORM_COMMAND, - MESSAGE_KEY: "takeSnapShot(); quit();" + MESSAGE_KEY: "takeSnapShot(); quit();", } send_plist_packet(conn, cmd_packet) @@ -141,7 +162,9 @@ def run_test(snapshots_dir): snaps = [f for f in os.listdir(snapshots_dir) if f.endswith(".png")] if snaps: snap_path = os.path.join(snapshots_dir, snaps[0]) - print(f"[+] Success: Captured {snap_path} ({os.path.getsize(snap_path)//1024} KB)") + print( + f"[+] Success: Captured {snap_path} ({os.path.getsize(snap_path) // 1024} KB)" + ) return True else: print("[!] Error: Oolite exited but no snapshot was found.") @@ -152,17 +175,50 @@ def run_test(snapshots_dir): return False finally: - if conn: conn.close() + if conn: + conn.close() server_sock.close() if proc.poll() is None: proc.kill() + if __name__ == "__main__": - # Use a temporary directory for snapshot storage - temp_dir = tempfile.mkdtemp(prefix="oolite_test_") + parser = argparse.ArgumentParser(description="Oolite Automation Tester") + parser.add_argument( + "--path", default="./", help="Path to the Oolite directory (default: ./)" + ) + args = parser.parse_args() + + # Determine binary name and original path + bin_name = "oolite.exe" if IS_WINDOWS else "oolite" + original_cwd = os.getcwd() + target_dir = os.path.abspath(args.path) + + # If the user pointed to a file, get the containing directory + if os.path.isfile(target_dir): + bin_name = os.path.basename(target_dir) + target_dir = os.path.dirname(target_dir) + + # Use a temporary directory for snapshots relative to the current CWD + # before we jump into the Oolite folder. + temp_snap_dir = tempfile.mkdtemp(prefix="oolite_snapshot_") + + success = False try: - success = run_test(temp_dir) + print(f"[*] Moving to {target_dir}") + os.chdir(target_dir) + + # Run the test from within the Oolite directory + success = run_test(bin_name, temp_snap_dir) + + except Exception as e: + print(f"[!] Critical Error: {e}") finally: - shutil.rmtree(temp_dir) + # ALWAYS return to the original path + os.chdir(original_cwd) + print(f"[*] Returned to {original_cwd}") + + # Cleanup snapshots + shutil.rmtree(temp_snap_dir) - sys.exit(0 if success else 1) \ No newline at end of file + sys.exit(0 if success else 1) diff --git a/tests/run_test.sh b/tests/run_test.sh new file mode 100755 index 000000000..ab85dd819 --- /dev/null +++ b/tests/run_test.sh @@ -0,0 +1,32 @@ +#!/bin/bash +# Processes Oolite data files after compilation + +run_script() { + if python3 --version >/dev/null 2>&1; then + local PYTHON_CMD="python3" + elif python --version >/dev/null 2>&1; then + local PYTHON_CMD="python" + else + echo "❌ Python executable not found!" >&2 + return 1 + fi + + SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) + pushd "$SCRIPT_DIR" + + if ! $PYTHON_CMD launch_snapshot.py --path=../oolite.app; then + echo "❌ Oolite test failed!" >&2 + fi + + echo "✅ Oolite test completed successfully" + popd +} + +run_script "$@" +status=$? + +# Exit only if not sourced +if [[ "${BASH_SOURCE[0]}" == "${0}" ]]; then + exit $status +fi + From c2804fa52125fbc76f7d3b97e45949a0d50afa7a Mon Sep 17 00:00:00 2001 From: mcarans Date: Mon, 6 Apr 2026 14:23:12 +1200 Subject: [PATCH 03/25] Fix workflow --- .github/workflows/test_builds.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_builds.yaml b/.github/workflows/test_builds.yaml index 0d8c107c0..727b7f028 100644 --- a/.github/workflows/test_builds.yaml +++ b/.github/workflows/test_builds.yaml @@ -23,7 +23,7 @@ jobs: sudo ShellScripts/Linux/install_deps_root.sh - name: Build Oolite run: | - ShellScripts/common/build_oolite.sh test-snapshot + ShellScripts/common/build_oolite.sh test fedora: runs-on: ubuntu-latest @@ -50,7 +50,7 @@ jobs: ShellScripts/Linux/install_deps_root.sh - name: Build Oolite run: | - ShellScripts/common/build_oolite.sh test-snapshot + ShellScripts/common/build_oolite.sh test arch: runs-on: ubuntu-latest @@ -77,7 +77,7 @@ jobs: ShellScripts/Linux/install_deps_root.sh - name: Build Oolite run: | - ShellScripts/common/build_oolite.sh test-snapshot + ShellScripts/common/build_oolite.sh test windows: runs-on: windows-latest @@ -114,4 +114,4 @@ jobs: env: msystem: UCRT64 run: | - oolite/ShellScripts/common/build_oolite.sh test-snapshot + oolite/ShellScripts/common/build_oolite.sh test From 14c39b45497d9fd692ebff8da5b4d5edf4e5106e Mon Sep 17 00:00:00 2001 From: mcarans Date: Mon, 6 Apr 2026 15:38:53 +1200 Subject: [PATCH 04/25] Try setting permissions first and use official action if possible --- .github/workflows/test_builds.yaml | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/.github/workflows/test_builds.yaml b/.github/workflows/test_builds.yaml index 727b7f028..02fa9ebb8 100644 --- a/.github/workflows/test_builds.yaml +++ b/.github/workflows/test_builds.yaml @@ -38,10 +38,15 @@ jobs: run: | dnf -y update dnf -y install git - - name: Checkout Oolite + - name: Pre-setup ownership run: | + # This must run before the checkout action if the action tries to do git commands + mkdir -p "$GITHUB_WORKSPACE" git config --global --add safe.directory "$GITHUB_WORKSPACE" - git clone --recursive https://github.com/${{ github.repository }} . + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: true - name: Checkout dependencies run: | ShellScripts/Linux/checkout_deps.sh @@ -65,10 +70,15 @@ jobs: run: | pacman -Syu --noconfirm pacman -S --noconfirm --needed git - - name: Checkout Oolite + - name: Pre-setup ownership run: | + # This must run before the checkout action if the action tries to do git commands + mkdir -p "$GITHUB_WORKSPACE" git config --global --add safe.directory "$GITHUB_WORKSPACE" - git clone --recursive https://github.com/${{ github.repository }} . + - uses: actions/checkout@v6 + with: + fetch-depth: 0 + submodules: true - name: Checkout dependencies run: | ShellScripts/Linux/checkout_deps.sh From 922bd24f33c031dadc0e5c6650f614fe4f0403dc Mon Sep 17 00:00:00 2001 From: mcarans Date: Mon, 6 Apr 2026 15:52:31 +1200 Subject: [PATCH 05/25] install missing deps --- ShellScripts/Linux/install_deps_root.sh | 9 +++++++++ ShellScripts/Linux/install_package_fn.sh | 9 +++++++++ 2 files changed, 18 insertions(+) diff --git a/ShellScripts/Linux/install_deps_root.sh b/ShellScripts/Linux/install_deps_root.sh index 5c2d16a03..11d9bd81f 100755 --- a/ShellScripts/Linux/install_deps_root.sh +++ b/ShellScripts/Linux/install_deps_root.sh @@ -30,6 +30,15 @@ run_script() { if ! install_package gnutls-dev; then return 1 fi + # Check Python + if ! python3 --version >/dev/null 2>&1; then + if ! install_package python; then + return 1 + fi + fi + if ! install_package xwfb-run; then + return 1 + fi if ! install_package icu-dev; then return 1 fi diff --git a/ShellScripts/Linux/install_package_fn.sh b/ShellScripts/Linux/install_package_fn.sh index e57b2fb71..765a8dc74 100755 --- a/ShellScripts/Linux/install_package_fn.sh +++ b/ShellScripts/Linux/install_package_fn.sh @@ -37,6 +37,15 @@ install_package() { arch) PKG_NAME="gnutls" ;; esac ;; + "python") + case "$CURRENT_DISTRO" in + debian) PKG_NAME="python3-pip" ;; + redhat) PKG_NAME="python3-pip" ;; + arch) PKG_NAME="python-pip" ;; + esac ;; + + "xwfb-run") PKG_NAME="xwayland-run weston" ;; + "icu-dev") case "$CURRENT_DISTRO" in debian) PKG_NAME="libicu-dev" ;; From 996995e6a995e56f57f273fbae3eeae0fa19442b Mon Sep 17 00:00:00 2001 From: mcarans Date: Mon, 6 Apr 2026 15:58:34 +1200 Subject: [PATCH 06/25] Missing return 1 --- tests/run_test.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/run_test.sh b/tests/run_test.sh index ab85dd819..792d84a40 100755 --- a/tests/run_test.sh +++ b/tests/run_test.sh @@ -16,6 +16,7 @@ run_script() { if ! $PYTHON_CMD launch_snapshot.py --path=../oolite.app; then echo "❌ Oolite test failed!" >&2 + return 1 fi echo "✅ Oolite test completed successfully" From 36b589f67c8f0e91ca7af86ca1c3009f33162a6a Mon Sep 17 00:00:00 2001 From: mcarans Date: Mon, 6 Apr 2026 16:54:26 +1200 Subject: [PATCH 07/25] Install Python --- ShellScripts/Windows/install_deps.sh | 1 + 1 file changed, 1 insertion(+) diff --git a/ShellScripts/Windows/install_deps.sh b/ShellScripts/Windows/install_deps.sh index c4139adf9..a3eb76f85 100755 --- a/ShellScripts/Windows/install_deps.sh +++ b/ShellScripts/Windows/install_deps.sh @@ -39,6 +39,7 @@ run_script() { pacman -S pactoys --noconfirm pacboy -S binutils --noconfirm pacboy -S uutils-coreutils --noconfirm + pacboy -S python-pip source ../common/checkout_submodules_fn.sh checkout_submodules From c87182abcb827c6934bee62cd15f220ad01a881c Mon Sep 17 00:00:00 2001 From: mcarans Date: Mon, 6 Apr 2026 16:54:52 +1200 Subject: [PATCH 08/25] Install Python --- ShellScripts/Windows/install_deps.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ShellScripts/Windows/install_deps.sh b/ShellScripts/Windows/install_deps.sh index a3eb76f85..05dcdcae2 100755 --- a/ShellScripts/Windows/install_deps.sh +++ b/ShellScripts/Windows/install_deps.sh @@ -39,7 +39,7 @@ run_script() { pacman -S pactoys --noconfirm pacboy -S binutils --noconfirm pacboy -S uutils-coreutils --noconfirm - pacboy -S python-pip + pacboy -S python-pip --noconfirm source ../common/checkout_submodules_fn.sh checkout_submodules From e607465722c9a6534e8ecefb2f74ff78dcd23d29 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 6 Apr 2026 17:37:16 +1200 Subject: [PATCH 09/25] Fix test on Windows --- src/Core/ResourceManager.m | 2 -- tests/launch_snapshot.py | 4 +--- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/Core/ResourceManager.m b/src/Core/ResourceManager.m index 0c80ac6b1..3d70528f2 100644 --- a/src/Core/ResourceManager.m +++ b/src/Core/ResourceManager.m @@ -190,9 +190,7 @@ + (NSArray *)userRootPaths stringByDeletingLastPathComponent] stringByAppendingPathComponent:@"AddOns"], #endif -#if !OOLITE_WINDOWS @"AddOns", -#endif [[OOOXZManager sharedManager] extractAddOnsPath], nil]; sUserRootPaths = [[[[OOOXZManager sharedManager] additionalAddOnsPaths] arrayByAddingObjectsFromArray:defaultAddOnsPaths] retain]; diff --git a/tests/launch_snapshot.py b/tests/launch_snapshot.py index 30592c814..1fbfcdee0 100644 --- a/tests/launch_snapshot.py +++ b/tests/launch_snapshot.py @@ -92,10 +92,8 @@ def run_test(bin_name, snapshots_dir): # Launch Oolite if IS_WINDOWS: cmd = [f"./{bin_name}", "--no-splash"] - creation_flags = 0x08000000 else: cmd = ["xwfb-run", "--", f"./{bin_name}", "--no-splash"] - creation_flags = 0 print(f"[*] Executing: {' '.join(cmd)}") proc = subprocess.Popen( @@ -103,7 +101,6 @@ def run_test(bin_name, snapshots_dir): env=env, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL, - creationflags=creation_flags, ) conn = None @@ -114,6 +111,7 @@ def run_test(bin_name, snapshots_dir): readable, _, _ = select.select([server_sock], [], [], 1) if readable: conn, addr = server_sock.accept() + conn.setblocking(True) print(f"[+] Oolite connected from {addr}") break From 66c0180ee8e156bf5b2c24360f76337f9f5ac3e3 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 6 Apr 2026 17:48:59 +1200 Subject: [PATCH 10/25] Update OOlite.nsi Exclude AddOns in oolite.app folder as it is already one level up --- installers/win32/OOlite.nsi | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/installers/win32/OOlite.nsi b/installers/win32/OOlite.nsi index dfb6a4764..1da430bf0 100644 --- a/installers/win32/OOlite.nsi +++ b/installers/win32/OOlite.nsi @@ -251,7 +251,7 @@ File "Privacy.pdf" ${If} ${ADDCHANGELOG} == "1" File "..\..\Doc\CHANGELOG.TXT" ${EndIf} -File /r /x AddOns .git /x .svn /x *~ "${DST}" +File /r /x AddOns /x .git /x .svn /x *~ "${DST}" ; Generate version info FileOpen $9 release.txt w ;Opens a Empty File and fills it From f5819fc5f91e8a9f470d56b30975dd11408138b4 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 6 Apr 2026 17:54:19 +1200 Subject: [PATCH 11/25] Try installing mesa for CI --- .github/workflows/test_builds.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.github/workflows/test_builds.yaml b/.github/workflows/test_builds.yaml index 02fa9ebb8..63255b347 100644 --- a/.github/workflows/test_builds.yaml +++ b/.github/workflows/test_builds.yaml @@ -119,6 +119,11 @@ jobs: run: | oolite/ShellScripts/Windows/install_deps.sh clang + - name: Install Mesa Windows + uses: f3d-app/install-mesa-windows-action@v1 + with: + path: ${{github.workspace}}/oolite.app + - name: Build Oolite shell: msys2 {0} env: From 702bb31a4212abf5e0478318c81e9b2f0786dae8 Mon Sep 17 00:00:00 2001 From: Mike Date: Mon, 6 Apr 2026 18:01:48 +1200 Subject: [PATCH 12/25] Try again --- .github/workflows/test_builds.yaml | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/.github/workflows/test_builds.yaml b/.github/workflows/test_builds.yaml index 63255b347..fc98339a9 100644 --- a/.github/workflows/test_builds.yaml +++ b/.github/workflows/test_builds.yaml @@ -97,10 +97,8 @@ jobs: with: update: true - - name: Checkout Oolite - uses: actions/checkout@v6 + - uses: actions/checkout@v6 with: - path: oolite fetch-depth: 0 submodules: true @@ -110,17 +108,17 @@ jobs: repository: OoliteProject/oolite_windeps_build latest: true fileName: "*" - out-file-path: oolite/build/packages + out-file-path: build/packages - name: Install packages shell: msys2 {0} env: msystem: UCRT64 run: | - oolite/ShellScripts/Windows/install_deps.sh clang + ShellScripts/Windows/install_deps.sh clang - name: Install Mesa Windows - uses: f3d-app/install-mesa-windows-action@v1 + uses: f3d-app/install-mesa-windows-action@v2 with: path: ${{github.workspace}}/oolite.app @@ -129,4 +127,4 @@ jobs: env: msystem: UCRT64 run: | - oolite/ShellScripts/common/build_oolite.sh test + ShellScripts/common/build_oolite.sh test From e2cef2236699d96a875faf619ced161546594f82 Mon Sep 17 00:00:00 2001 From: mcarans Date: Mon, 6 Apr 2026 20:08:14 +1200 Subject: [PATCH 13/25] Try ucrt64 mesa --- .github/workflows/test_builds.yaml | 6 +----- tests/run_test.sh | 10 ++++++++++ 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_builds.yaml b/.github/workflows/test_builds.yaml index fc98339a9..44358edbe 100644 --- a/.github/workflows/test_builds.yaml +++ b/.github/workflows/test_builds.yaml @@ -116,11 +116,7 @@ jobs: msystem: UCRT64 run: | ShellScripts/Windows/install_deps.sh clang - - - name: Install Mesa Windows - uses: f3d-app/install-mesa-windows-action@v2 - with: - path: ${{github.workspace}}/oolite.app + pacboy -S mesa --noconfirm - name: Build Oolite shell: msys2 {0} diff --git a/tests/run_test.sh b/tests/run_test.sh index 792d84a40..be4473add 100755 --- a/tests/run_test.sh +++ b/tests/run_test.sh @@ -14,6 +14,16 @@ run_script() { SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" &> /dev/null && pwd) pushd "$SCRIPT_DIR" + if [[ -n "$MSYSTEM" ]]; then + MESA_DLL="${MSYSTEM_PREFIX}/bin/opengl32.dll" + TARGET_DIR="../oolite.app" + + if [[ -f "$MESA_DLL" ]]; then + echo "📦 Found $MSYSTEM Mesa driver at $MESA_DLL" + cp "$MESA_DLL" "$TARGET_DIR/" + fi + fi + if ! $PYTHON_CMD launch_snapshot.py --path=../oolite.app; then echo "❌ Oolite test failed!" >&2 return 1 From cfce678bc260051c18c892f4b0ab2ea50840fdc1 Mon Sep 17 00:00:00 2001 From: mcarans Date: Tue, 7 Apr 2026 06:52:14 +1200 Subject: [PATCH 14/25] Try interactive session hack --- .github/workflows/test_builds.yaml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/.github/workflows/test_builds.yaml b/.github/workflows/test_builds.yaml index 44358edbe..3f683b084 100644 --- a/.github/workflows/test_builds.yaml +++ b/.github/workflows/test_builds.yaml @@ -118,6 +118,26 @@ jobs: ShellScripts/Windows/install_deps.sh clang pacboy -S mesa --noconfirm + - name: Enable interactive desktop for SDL + shell: powershell + run: | + # Allow the runner service account to access the interactive window station + $code = @" + using System; + using System.Runtime.InteropServices; + public class Desktop { + [DllImport("user32.dll")] public static extern IntPtr OpenWindowStation(string name, bool inherit, uint access); + [DllImport("user32.dll")] public static extern bool SetProcessWindowStation(IntPtr hWinSta); + [DllImport("user32.dll")] public static extern IntPtr OpenDesktop(string name, uint flags, bool inherit, uint access); + [DllImport("user32.dll")] public static extern bool SetThreadDesktop(IntPtr hDesktop); + } + "@ + Add-Type -TypeDefinition $code + $winsta = [Desktop]::OpenWindowStation("WinSta0", $false, 0x37F) + [Desktop]::SetProcessWindowStation($winsta) + $desktop = [Desktop]::OpenDesktop("Default", 0, $false, 0x1FF) + [Desktop]::SetThreadDesktop($desktop) + - name: Build Oolite shell: msys2 {0} env: From 765658487ba0803056f3051f40603e435f0f2488 Mon Sep 17 00:00:00 2001 From: mcarans Date: Tue, 7 Apr 2026 08:53:00 +1200 Subject: [PATCH 15/25] Gallium driver env var --- tests/launch_snapshot.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/launch_snapshot.py b/tests/launch_snapshot.py index 1fbfcdee0..bd98dfd89 100644 --- a/tests/launch_snapshot.py +++ b/tests/launch_snapshot.py @@ -85,6 +85,7 @@ def run_test(bin_name, snapshots_dir): # Environment configuration for headless snapshotting env = os.environ.copy() + env["GALLIUM_DRIVER"] = "llvmpipe" env["SDL_AUDIODRIVER"] = "dummy" env["ALSOFT_DRIVERS"] = "null" env["OO_SNAPSHOTSDIR"] = snapshots_dir From 2ad82c53238dcfd3c05b7f3f535ef0e510ce0b73 Mon Sep 17 00:00:00 2001 From: mcarans Date: Tue, 7 Apr 2026 09:07:10 +1200 Subject: [PATCH 16/25] Remove hack Install mesa in standard install (since by default it won't be used instead of hardware acceleration) --- .github/workflows/test_builds.yaml | 21 --------------------- ShellScripts/Windows/install_deps.sh | 1 + 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/.github/workflows/test_builds.yaml b/.github/workflows/test_builds.yaml index 3f683b084..ac247786b 100644 --- a/.github/workflows/test_builds.yaml +++ b/.github/workflows/test_builds.yaml @@ -116,27 +116,6 @@ jobs: msystem: UCRT64 run: | ShellScripts/Windows/install_deps.sh clang - pacboy -S mesa --noconfirm - - - name: Enable interactive desktop for SDL - shell: powershell - run: | - # Allow the runner service account to access the interactive window station - $code = @" - using System; - using System.Runtime.InteropServices; - public class Desktop { - [DllImport("user32.dll")] public static extern IntPtr OpenWindowStation(string name, bool inherit, uint access); - [DllImport("user32.dll")] public static extern bool SetProcessWindowStation(IntPtr hWinSta); - [DllImport("user32.dll")] public static extern IntPtr OpenDesktop(string name, uint flags, bool inherit, uint access); - [DllImport("user32.dll")] public static extern bool SetThreadDesktop(IntPtr hDesktop); - } - "@ - Add-Type -TypeDefinition $code - $winsta = [Desktop]::OpenWindowStation("WinSta0", $false, 0x37F) - [Desktop]::SetProcessWindowStation($winsta) - $desktop = [Desktop]::OpenDesktop("Default", 0, $false, 0x1FF) - [Desktop]::SetThreadDesktop($desktop) - name: Build Oolite shell: msys2 {0} diff --git a/ShellScripts/Windows/install_deps.sh b/ShellScripts/Windows/install_deps.sh index 05dcdcae2..8c5d09285 100755 --- a/ShellScripts/Windows/install_deps.sh +++ b/ShellScripts/Windows/install_deps.sh @@ -40,6 +40,7 @@ run_script() { pacboy -S binutils --noconfirm pacboy -S uutils-coreutils --noconfirm pacboy -S python-pip --noconfirm + pacboy -S mesa --noconfirm source ../common/checkout_submodules_fn.sh checkout_submodules From 1069c1cc3bcfb4738ba561001b1edf42243b35d8 Mon Sep 17 00:00:00 2001 From: mcarans Date: Tue, 7 Apr 2026 09:47:26 +1200 Subject: [PATCH 17/25] Fix appimage --- .github/workflows/build-all.yaml | 2 +- ShellScripts/Linux/install_deps_root.sh | 16 ++++++++++++++-- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/.github/workflows/build-all.yaml b/.github/workflows/build-all.yaml index 66e860658..932a61119 100644 --- a/.github/workflows/build-all.yaml +++ b/.github/workflows/build-all.yaml @@ -35,7 +35,7 @@ jobs: - name: Install packages run: | - sudo oolite/ShellScripts/Linux/install_deps_root.sh + sudo oolite/ShellScripts/Linux/install_deps_root.sh --skip-wayland - name: Build Oolite run: | diff --git a/ShellScripts/Linux/install_deps_root.sh b/ShellScripts/Linux/install_deps_root.sh index 11d9bd81f..4214267cf 100755 --- a/ShellScripts/Linux/install_deps_root.sh +++ b/ShellScripts/Linux/install_deps_root.sh @@ -4,6 +4,16 @@ run_script() { + local skip_wayland=false + + # Parse arguments + while [[ "$#" -gt 0 ]]; do + case $1 in + -s|--skip-wayland) skip_wayland=true; shift ;; + *) shift ;; + esac + done + # If current user ID is NOT 0 (root) if [[ $EUID -ne 0 ]]; then echo "This script requires root to install dependencies. Rerun and escalate privileges (eg. sudo ...)" @@ -36,8 +46,10 @@ run_script() { return 1 fi fi - if ! install_package xwfb-run; then - return 1 + if [[ $skip_wayland == false ]]; then + if ! install_package xwfb-run; then + return 1 + fi fi if ! install_package icu-dev; then return 1 From 24ac6248ca3012e6a0deef0b6f3b0e2052c4572b Mon Sep 17 00:00:00 2001 From: mcarans Date: Tue, 7 Apr 2026 10:02:58 +1200 Subject: [PATCH 18/25] Better GH Action naming --- .github/workflows/build-all.yaml | 4 +--- .github/workflows/test_builds.yaml | 2 +- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/.github/workflows/build-all.yaml b/.github/workflows/build-all.yaml index 932a61119..7ca0c4d5c 100644 --- a/.github/workflows/build-all.yaml +++ b/.github/workflows/build-all.yaml @@ -1,6 +1,4 @@ ---- -name: build-all - +name: Build Oolite package for various platforms on: workflow_dispatch: push: diff --git a/.github/workflows/test_builds.yaml b/.github/workflows/test_builds.yaml index ac247786b..a0a3de15e 100644 --- a/.github/workflows/test_builds.yaml +++ b/.github/workflows/test_builds.yaml @@ -1,4 +1,4 @@ -name: Build Oolite package on various platforms +name: Test Oolite package on various platforms on: push: pull_request: From 57b4288581ea7e1add9f4e69b3e5eb7290ce5396 Mon Sep 17 00:00:00 2001 From: mcarans Date: Tue, 7 Apr 2026 10:17:50 +1200 Subject: [PATCH 19/25] Update README.md to show how to run test --- README.md | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 498d43db3..1ff5b713e 100644 --- a/README.md +++ b/README.md @@ -101,13 +101,13 @@ sudo ShellScripts/Linux/install_deps_root.sh ### Building Oolite -Next run this in your bash or MSYS2 prompt to build Oolite: +Next run this in your Bash or MSYS2 prompt to build Oolite: ```bash ShellScripts/common/build_oolite.sh release ``` -The completed build (executable and games files) can be found in the Oolite.app directory. +The completed build (executable and games files) can be found in the oolite.app directory. Subsequently, you can clean and build as follows: @@ -116,6 +116,12 @@ make -f Makefile clean make -f Makefile release -j$(nproc) ``` +You can run a test from your Bash or MSYS2 prompt as follows: + +```bash +make -f Makefile test +``` + On Linux, you will need to run this beforehand: `source /usr/local/share/GNUstep/Makefiles/GNUstep.sh` On Windows, this is set up be default in the shell: `source $MINGW_PREFIX/share/GNUstep/Makefiles/GNUstep.sh` From 3676dfa1a577dbb34bbcfdb6954b959672b9bd8e Mon Sep 17 00:00:00 2001 From: mcarans Date: Tue, 7 Apr 2026 10:48:32 +1200 Subject: [PATCH 20/25] Fix formatting Remove extraneous comments and improve others --- DebugOXP/Debug.oxp/Scripts/oolite-debug-console.js | 3 +-- src/Core/Scripting/OOJSGlobal.m | 2 -- src/Core/Universe.m | 4 +--- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/DebugOXP/Debug.oxp/Scripts/oolite-debug-console.js b/DebugOXP/Debug.oxp/Scripts/oolite-debug-console.js index 9e987d836..04653b6c8 100644 --- a/DebugOXP/Debug.oxp/Scripts/oolite-debug-console.js +++ b/DebugOXP/Debug.oxp/Scripts/oolite-debug-console.js @@ -861,8 +861,7 @@ this.evaluate = function evaluate(command, PARAM) this.quit = function() { log("debugConsole.automation", "Triggering global quitGame bridge..."); - // This calls the native function you added to OOJSGlobal.m, - // which in turn calls [Universe quitGame] + // This calls the native function in OOJSGlobal.m, which in turn calls [Universe quitGame] if (typeof quitGame === "function") { quitGame(); } else { diff --git a/src/Core/Scripting/OOJSGlobal.m b/src/Core/Scripting/OOJSGlobal.m index 0fc36b9f2..5fe8b18ad 100644 --- a/src/Core/Scripting/OOJSGlobal.m +++ b/src/Core/Scripting/OOJSGlobal.m @@ -861,10 +861,8 @@ static JSBool GlobalQuitGame(JSContext *context, uintN argc, jsval *vp) { OOJS_NATIVE_ENTER(context) - // Log the event so you see it in Latest.log OOLog(@"script.debug.quit", @"Quit requested via JavaScript global.quitGame()"); - // Call the method you just added to Universe [UNIVERSE quitGame]; OOJS_RETURN_BOOL(YES); diff --git a/src/Core/Universe.m b/src/Core/Universe.m index eca7ae17f..3ac6b10ec 100644 --- a/src/Core/Universe.m +++ b/src/Core/Universe.m @@ -1052,9 +1052,7 @@ - (void) pauseGame - (void) quitGame { -OOLog(@"universe.quit", @"Quit command received by Universe."); - - // Use the internal convenience method instead of NSApp + OOLog(@"universe.quit", @"Quit command received by Universe."); [[self gameController] exitAppWithContext:@"Universe Request"]; } From 1981cdec71cc13a1806d70d622a71fb70fbdd47e Mon Sep 17 00:00:00 2001 From: mcarans Date: Tue, 7 Apr 2026 15:00:34 +1200 Subject: [PATCH 21/25] Add variable and sleep for longer before taking snapshot --- tests/launch_snapshot.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/launch_snapshot.py b/tests/launch_snapshot.py index bd98dfd89..015032a66 100644 --- a/tests/launch_snapshot.py +++ b/tests/launch_snapshot.py @@ -85,6 +85,7 @@ def run_test(bin_name, snapshots_dir): # Environment configuration for headless snapshotting env = os.environ.copy() + env["LIBGL_ALWAYS_SOFTWARE"] = "1" env["GALLIUM_DRIVER"] = "llvmpipe" env["SDL_AUDIODRIVER"] = "dummy" env["ALSOFT_DRIVERS"] = "null" @@ -141,7 +142,7 @@ def run_test(bin_name, snapshots_dir): return False # Allow time for engine state transition - time.sleep(1) + time.sleep(2) # 3. THE COMMAND (Using Perform Command packet type) # Combines snapshot and quit into one execution string From 47e3694d66849487e7821f63d630635e8efc6caa Mon Sep 17 00:00:00 2001 From: mcarans Date: Tue, 7 Apr 2026 15:15:26 +1200 Subject: [PATCH 22/25] Sleep for even longer before taking snapshot --- tests/launch_snapshot.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/launch_snapshot.py b/tests/launch_snapshot.py index 015032a66..9ac68da78 100644 --- a/tests/launch_snapshot.py +++ b/tests/launch_snapshot.py @@ -142,7 +142,7 @@ def run_test(bin_name, snapshots_dir): return False # Allow time for engine state transition - time.sleep(2) + time.sleep(5) # 3. THE COMMAND (Using Perform Command packet type) # Combines snapshot and quit into one execution string From 71f7538b02e9c383bc3f50de444111d41674d467 Mon Sep 17 00:00:00 2001 From: mcarans Date: Tue, 7 Apr 2026 16:49:01 +1200 Subject: [PATCH 23/25] Check png size --- tests/launch_snapshot.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/launch_snapshot.py b/tests/launch_snapshot.py index 9ac68da78..c9c6bd221 100644 --- a/tests/launch_snapshot.py +++ b/tests/launch_snapshot.py @@ -24,6 +24,7 @@ IS_WINDOWS = sys.platform == "win32" or (os.name == "nt") PORT = 8563 HOST = "127.0.0.1" +MIN_FILE_SIZE_KB = 100 # Threshold for a valid render def send_plist_packet(sock, packet): @@ -162,9 +163,16 @@ def run_test(bin_name, snapshots_dir): snaps = [f for f in os.listdir(snapshots_dir) if f.endswith(".png")] if snaps: snap_path = os.path.join(snapshots_dir, snaps[0]) - print( - f"[+] Success: Captured {snap_path} ({os.path.getsize(snap_path) // 1024} KB)" - ) + file_size_kb = os.path.getsize(snap_path) // 1024 + print(f"[*] Captured {snap_path} ({file_size_kb} KB)") + + if file_size_kb < MIN_FILE_SIZE_KB: + print( + f"[!] Failure: Snapshot is too small ({file_size_kb} KB). Likely a black frame." + ) + return False + + print("[+] Success: Snapshot passed quality check.") return True else: print("[!] Error: Oolite exited but no snapshot was found.") From 922fa4a0e6dfee65628d5733b84a5fbd65856582 Mon Sep 17 00:00:00 2001 From: mcarans Date: Wed, 8 Apr 2026 07:43:17 +1200 Subject: [PATCH 24/25] Move quitGame into ifndef NDEBUG --- src/Core/Scripting/OOJSGlobal.m | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Core/Scripting/OOJSGlobal.m b/src/Core/Scripting/OOJSGlobal.m index 5fe8b18ad..875cd19c5 100644 --- a/src/Core/Scripting/OOJSGlobal.m +++ b/src/Core/Scripting/OOJSGlobal.m @@ -152,9 +152,9 @@ - (void)sendMonitorLogMessage:(NSString *)message #ifndef NDEBUG { "takeSnapShot", GlobalTakeSnapShot, 1 }, + { "quitGame", GlobalQuitGame, 0 }, #endif { "pauseGame", GlobalPauseGame, 0 }, - { "quitGame", GlobalQuitGame, 0 }, { 0 } }; From da98a04487f5f3e60748f1cd2b913f4e18a8c0a8 Mon Sep 17 00:00:00 2001 From: mcarans Date: Wed, 8 Apr 2026 07:54:44 +1200 Subject: [PATCH 25/25] Improve comments in Python --- tests/launch_snapshot.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/tests/launch_snapshot.py b/tests/launch_snapshot.py index c9c6bd221..3a23a84ec 100644 --- a/tests/launch_snapshot.py +++ b/tests/launch_snapshot.py @@ -10,7 +10,8 @@ import tempfile import shutil -# --- PROTOCOL CONSTANTS (from _protocol.py) --- +# --- PROTOCOL CONSTANTS --- +# See https://github.com/OoliteProject/oolite-debug-console/blob/master/ooliteConsoleServer/_protocol.py # Using the exact string definitions required by the Oolite Debug Protocol REQUEST_CONNECTION = "Request Connection" APPROVE_CONNECTION = "Approve Connection" @@ -29,7 +30,8 @@ def send_plist_packet(sock, packet): """ - Implements the framing protocol from PropertyListPacketProtocol.py: + Implements the framing protocol from + https://github.com/OoliteProject/oolite-debug-console/blob/master/ooliteConsoleServer/PropertyListPacketProtocol.py: 1. Encodes the dictionary as an XML Property List. 2. Calculates a 32-bit big-endian integer for the length. 3. Prepends this 4-byte header to the data. @@ -51,7 +53,8 @@ def send_plist_packet(sock, packet): def receive_plist_packet(sock): """ - Implements the receiving state machine from PropertyListPacketProtocol.py: + Implements the receiving state machine from: + https://github.com/OoliteProject/oolite-debug-console/blob/master/ooliteConsoleServer/OoliteDebugConsoleProtocol.py 1. Reads the 4-byte header to determine expected length. 2. Reads the specific number of bytes for the XML payload. """ @@ -122,7 +125,8 @@ def run_test(bin_name, snapshots_dir): print("[!] Failure: Oolite failed to connect.") return False - # 2. THE HANDSHAKE (Required by OoliteDebugConsoleProtocol.py) + # 2. THE HANDSHAKE from: + # https://github.com/OoliteProject/oolite-debug-console/blob/master/ooliteConsoleServer/OoliteDebugConsoleProtocol.py # Wait for 'Request Connection' from Oolite pkt = receive_plist_packet(conn)