From 8ad40d960915e48b62f70f90df0f3da6a5b78d61 Mon Sep 17 00:00:00 2001 From: Mark Gregory Date: Fri, 23 Oct 2026 11:03:06 +0100 Subject: [PATCH] updates to launcer --- backups/tests-20261023-110150/scripts/test.sh | 41 ++++ backups/tests/test_oriontoolbar.py | 90 +++++++++ docs/MANUAL_TEST_PLAN.md | 38 ++++ patches/install_oriontoolbar_tests.py | 177 ++++++++++++++++++ scripts/test.sh | 53 ++---- 5 files changed, 362 insertions(+), 37 deletions(-) create mode 100755 backups/tests-20261023-110150/scripts/test.sh create mode 100644 backups/tests/test_oriontoolbar.py create mode 100644 docs/MANUAL_TEST_PLAN.md create mode 100644 patches/install_oriontoolbar_tests.py diff --git a/backups/tests-20261023-110150/scripts/test.sh b/backups/tests-20261023-110150/scripts/test.sh new file mode 100755 index 0000000..56a758b --- /dev/null +++ b/backups/tests-20261023-110150/scripts/test.sh @@ -0,0 +1,41 @@ +#!/bin/zsh +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +mkdir -p "$ROOT/.build/build-logs" +LOG="$ROOT/.build/build-logs/test-$(date +%Y%m%d-%H%M%S).log" +exec > >(tee "$LOG") 2>&1 +print '=== OrionToolbar tests ===' +# A successful compile is the minimum regression check. +"$ROOT/scripts/build.sh" +APP="$ROOT/.build/Build/Products/Debug/OrionToolbar.app" +plutil -lint "$APP/Contents/Info.plist" +# Verify that the Parallels executable and JSON discovery work independently of SwiftUI. +PRLCTL='' +for candidate in /usr/local/bin/prlctl '/Applications/Parallels Desktop.app/Contents/MacOS/prlctl'; do + if [[ -x "$candidate" ]]; then PRLCTL="$candidate"; break; fi +done +if [[ -n "$PRLCTL" ]]; then + "$PRLCTL" list -a -j | python3 -c ' +import json,sys +v=json.load(sys.stdin) +assert isinstance(v,list), "Expected Parallels JSON array" +for item in v: + assert isinstance(item,dict), "Invalid VM record" + assert item.get("name"), "VM without name" + assert item.get("uuid"), "VM without UUID" +print("Parallels discovery OK: {} VM(s)".format(len(v))) +' +else + echo 'SKIP: Parallels CLI not installed' +fi +if [[ -d "$ROOT/tests" ]] && find "$ROOT/tests" -name 'test_*.py' -print -quit | grep -q .; then + python3 -m unittest discover -s "$ROOT/tests" -v +fi +# Run Xcode tests only if a test plan is configured for this scheme. +if [[ "${RUN_XCODE_TESTS:-0}" == 1 ]]; then + xcodebuild -project "$ROOT/OrionToolbar.xcodeproj" -scheme OrionToolbar -configuration Debug -destination 'platform=macOS' -derivedDataPath "$ROOT/.build" CODE_SIGNING_ALLOWED=NO test +else + echo 'XCTest not run (set RUN_XCODE_TESTS=1 if the scheme has a test target).' +fi +print "Tests complete. Log: $LOG" diff --git a/backups/tests/test_oriontoolbar.py b/backups/tests/test_oriontoolbar.py new file mode 100644 index 0000000..11072cd --- /dev/null +++ b/backups/tests/test_oriontoolbar.py @@ -0,0 +1,90 @@ +#!/usr/bin/env python3 +"""Source-contract and macOS runtime smoke tests. No VM state is changed.""" +import os +import re +import shutil +import subprocess +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +CONTROLLER = (ROOT/'OrionToolbar/OrionVMController.swift').read_text() +VIEW = (ROOT/'OrionToolbar/OrionMenuView.swift').read_text() +APP = (ROOT/'OrionToolbar/OrionToolbarApp.swift').read_text() + +class SourceContracts(unittest.TestCase): + def test_menu_bar_app(self): + self.assertIn('MenuBarExtra', APP) + self.assertIn('.menuBarExtraStyle(.window)', APP) + def test_vm_discovery(self): + self.assertIn('func discoverVMs()', CONTROLLER) + self.assertIn('runPrlctl(["list", "-a", "-j"])', CONTROLLER) + def test_refresh_probes_on_startup_and_timer(self): + self.assertGreaterEqual(CONTROLLER.count('await refreshServiceAvailability()'), 2) + self.assertIn('func refreshServiceAvailability()', CONTROLLER) + def test_ssh_only_if_reachable(self): + self.assertEqual(VIEW.count('vm.reachableSSH.contains(server.id)'), 2) + self.assertIn('server.sshUser.isEmpty', VIEW) + def test_mailpit_only_if_reachable(self): + self.assertEqual(VIEW.count('vm.reachableMailpit.contains(server.id)'), 2) + self.assertIn('server.mailpitPort != nil', VIEW) + def test_running_filter_for_probes(self): + block = CONTROLLER.split('func refreshServiceAvailability()', 1)[1].split('private nonisolated static func tcpPortOpen',1)[0] + self.assertIn('"running"', block) + self.assertIn('reachableSSH = Set(', block) + self.assertIn('reachableMailpit = Set(', block) + def test_probe_timeouts(self): + self.assertIn('"-G", "2", "-z"', CONTROLLER) + self.assertRegex(CONTROLLER, r'timeoutInterval\s*=\s*3') + def test_actions(self): + for name in ('openSSH','openMailpit','operate','hideVM','restoreVM','reorderVM','reorderServer','integrate','removeDefinition'): + with self.subTest(name=name): + self.assertRegex(CONTROLLER, r'func\s+' + name + r'\s*\(') + def test_vm_operations(self): + for operation in ('"start"','"stop"','"restart"','"force-stop"'): + with self.subTest(operation=operation): + self.assertIn(operation, VIEW + CONTROLLER) + def test_persisted_order_and_visibility(self): + self.assertIn('orionVMOrder.v1', CONTROLLER) + self.assertIn('orionHiddenVMIDs.v1', CONTROLLER) + self.assertIn('UserDefaults.standard', CONTROLLER) + def test_drag_and_drop(self): + self.assertGreaterEqual(VIEW.count('.onDrag'), 2) + self.assertGreaterEqual(VIEW.count('.onDrop'), 2) + def test_icons(self): + self.assertIn('VM.app', CONTROLLER) + self.assertIn('func icon(for machine:', CONTROLLER) + self.assertIn('func icon(for server:', CONTROLLER) + def test_settings(self): + self.assertIn('OrionSettingsWindowPresenter', VIEW) + self.assertIn('SettingsView()', VIEW) + def test_confirmation_before_force_stop(self): + self.assertIn('pendingForceStop', VIEW) + self.assertIn('.confirmationDialog("Force stop virtual machine?', VIEW) + def test_button_spacing(self): + self.assertIn('.padding(.leading, 12)', VIEW) + def test_foreground_activation_contract(self): + # Source-level contract only; actual window z-order requires manual UI test. + self.assertIn('NSWorkspace.shared', CONTROLLER) + self.assertIn('func openSSH', CONTROLLER) + self.assertIn('func openMailpit', CONTROLLER) + +@unittest.skipUnless(sys.platform == 'darwin' and os.getenv('ORION_LIVE') == '1', 'Set ORION_LIVE=1 on macOS') +class LiveReadOnly(unittest.TestCase): + def test_parallels_list_json(self): + import json + executable = shutil.which('prlctl') or '/Applications/Parallels Desktop.app/Contents/MacOS/prlctl' + if not Path(executable).exists(): self.skipTest('Parallels CLI not installed') + p = subprocess.run([executable,'list','-a','-j'], capture_output=True, text=True, timeout=20) + self.assertEqual(p.returncode,0,p.stderr) + data = json.loads(p.stdout) + self.assertIsInstance(data,list) + for vm in data: + self.assertIn('uuid',vm) + self.assertIn('name',vm) + self.assertIn('status',vm) + def test_xcode_project(self): + self.assertTrue((ROOT/'OrionToolbar.xcodeproj/project.pbxproj').is_file()) + +if __name__ == '__main__': unittest.main() diff --git a/docs/MANUAL_TEST_PLAN.md b/docs/MANUAL_TEST_PLAN.md new file mode 100644 index 0000000..47e8baf --- /dev/null +++ b/docs/MANUAL_TEST_PLAN.md @@ -0,0 +1,38 @@ +# OrionToolbar manual regression checklist + +Run `./scripts/test.sh --all` first. These checks require a person using macOS; automated source checks **cannot** prove UI focus or VM lifecycle behaviour. + +## Dashboard +- [ ] Menu bar opens; Running, All, Integrated sections expand/collapse. +- [ ] VM status and IP match Parallels; refresh updates them. +- [ ] VM icons show the expected OS; custom integrated icon persists. +- [ ] Reorder All VMs, wait 20 seconds, reopen; order persists. +- [ ] Reorder Integrated Servers, wait 20 seconds, reopen; order persists. +- [ ] Remove a VM from toolbar, confirm it is still in Parallels; re-add it. +- [ ] Settings opens in front; edits survive app relaunch. +- [ ] Button spacing remains readable; no unwanted chevrons. + +## SSH and Mailpit +- [ ] VM stopped: neither service button visible. +- [ ] VM running with SSH port closed: SSH button hidden after refresh. +- [ ] VM running with SSH port open: SSH button visible after refresh. +- [ ] SSH click: Terminal appears **above** OrionToolbar and SSH starts. +- [ ] VM running without Mailpit: Mailpit button hidden after refresh. +- [ ] VM running with Mailpit HTTP responding: button visible after refresh. +- [ ] Mailpit click: browser appears **above** OrionToolbar and correct page loads. +- [ ] Stop service without stopping VM: corresponding button disappears after refresh. +- [ ] Restart service: corresponding button reappears after refresh. +- [ ] Open and close Settings after launching Terminal/browser; foreground behaviour still correct. + +## VM operations — changes VM state; use disposable VM only +- [ ] Start a stopped test VM; status becomes Running. +- [ ] Restart test VM; busy indicator appears then clears. +- [ ] Stop test VM; status becomes Stopped; service buttons disappear. +- [ ] Resume a paused/suspended test VM, if supported. +- [ ] Force Stop shows confirmation; Cancel does not stop VM. +- [ ] Force Stop confirmed only on a disposable VM; state updates. +- [ ] Failed Parallels command displays an error and clears busy indicator. + +## Final +- [ ] Quit and relaunch; preferences, hidden VMs and ordering persist. +- [ ] Review `.build/test-logs/` and `../.build/build-logs`. diff --git a/patches/install_oriontoolbar_tests.py b/patches/install_oriontoolbar_tests.py new file mode 100644 index 0000000..480c132 --- /dev/null +++ b/patches/install_oriontoolbar_tests.py @@ -0,0 +1,177 @@ +#!/usr/bin/env python3 +"""Install non-destructive OrionToolbar regression tests into the current project.""" +from pathlib import Path +from datetime import datetime +import shutil +import sys + +root = Path.cwd() +if not (root/'OrionToolbar/OrionVMController.swift').is_file() or not (root/'OrionToolbar/OrionMenuView.swift').is_file(): + sys.exit('Run from the OrionToolbar repository root. No changes made.') +files = { + 'tests/test_oriontoolbar.py': r'''#!/usr/bin/env python3 +"""Source-contract and macOS runtime smoke tests. No VM state is changed.""" +import os +import re +import shutil +import subprocess +import sys +import unittest +from pathlib import Path + +ROOT = Path(__file__).resolve().parents[1] +CONTROLLER = (ROOT/'OrionToolbar/OrionVMController.swift').read_text() +VIEW = (ROOT/'OrionToolbar/OrionMenuView.swift').read_text() +APP = (ROOT/'OrionToolbar/OrionToolbarApp.swift').read_text() + +class SourceContracts(unittest.TestCase): + def test_menu_bar_app(self): + self.assertIn('MenuBarExtra', APP) + self.assertIn('.menuBarExtraStyle(.window)', APP) + def test_vm_discovery(self): + self.assertIn('func discoverVMs()', CONTROLLER) + self.assertIn('runPrlctl(["list", "-a", "-j"])', CONTROLLER) + def test_refresh_probes_on_startup_and_timer(self): + self.assertGreaterEqual(CONTROLLER.count('await refreshServiceAvailability()'), 2) + self.assertIn('func refreshServiceAvailability()', CONTROLLER) + def test_ssh_only_if_reachable(self): + self.assertEqual(VIEW.count('vm.reachableSSH.contains(server.id)'), 2) + self.assertIn('server.sshUser.isEmpty', VIEW) + def test_mailpit_only_if_reachable(self): + self.assertEqual(VIEW.count('vm.reachableMailpit.contains(server.id)'), 2) + self.assertIn('server.mailpitPort != nil', VIEW) + def test_running_filter_for_probes(self): + block = CONTROLLER.split('func refreshServiceAvailability()', 1)[1].split('private nonisolated static func tcpPortOpen',1)[0] + self.assertIn('"running"', block) + self.assertIn('reachableSSH = Set(', block) + self.assertIn('reachableMailpit = Set(', block) + def test_probe_timeouts(self): + self.assertIn('"-G", "2", "-z"', CONTROLLER) + self.assertRegex(CONTROLLER, r'timeoutInterval\s*=\s*3') + def test_actions(self): + for name in ('openSSH','openMailpit','operate','hideVM','restoreVM','reorderVM','reorderServer','integrate','removeDefinition'): + with self.subTest(name=name): + self.assertRegex(CONTROLLER, r'func\s+' + name + r'\s*\(') + def test_vm_operations(self): + for operation in ('"start"','"stop"','"restart"','"force-stop"'): + with self.subTest(operation=operation): + self.assertIn(operation, VIEW + CONTROLLER) + def test_persisted_order_and_visibility(self): + self.assertIn('orionVMOrder.v1', CONTROLLER) + self.assertIn('orionHiddenVMIDs.v1', CONTROLLER) + self.assertIn('UserDefaults.standard', CONTROLLER) + def test_drag_and_drop(self): + self.assertGreaterEqual(VIEW.count('.onDrag'), 2) + self.assertGreaterEqual(VIEW.count('.onDrop'), 2) + def test_icons(self): + self.assertIn('VM.app', CONTROLLER) + self.assertIn('func icon(for machine:', CONTROLLER) + self.assertIn('func icon(for server:', CONTROLLER) + def test_settings(self): + self.assertIn('OrionSettingsWindowPresenter', VIEW) + self.assertIn('SettingsView()', VIEW) + def test_confirmation_before_force_stop(self): + self.assertIn('pendingForceStop', VIEW) + self.assertIn('.confirmationDialog("Force stop virtual machine?', VIEW) + def test_button_spacing(self): + self.assertIn('.padding(.leading, 12)', VIEW) + def test_foreground_activation_contract(self): + # Source-level contract only; actual window z-order requires manual UI test. + self.assertIn('NSWorkspace.shared', CONTROLLER) + self.assertIn('func openSSH', CONTROLLER) + self.assertIn('func openMailpit', CONTROLLER) + +@unittest.skipUnless(sys.platform == 'darwin' and os.getenv('ORION_LIVE') == '1', 'Set ORION_LIVE=1 on macOS') +class LiveReadOnly(unittest.TestCase): + def test_parallels_list_json(self): + import json + executable = shutil.which('prlctl') or '/Applications/Parallels Desktop.app/Contents/MacOS/prlctl' + if not Path(executable).exists(): self.skipTest('Parallels CLI not installed') + p = subprocess.run([executable,'list','-a','-j'], capture_output=True, text=True, timeout=20) + self.assertEqual(p.returncode,0,p.stderr) + data = json.loads(p.stdout) + self.assertIsInstance(data,list) + for vm in data: + self.assertIn('uuid',vm) + self.assertIn('name',vm) + self.assertIn('status',vm) + def test_xcode_project(self): + self.assertTrue((ROOT/'OrionToolbar.xcodeproj/project.pbxproj').is_file()) + +if __name__ == '__main__': unittest.main() +''', + 'scripts/test.sh': r'''#!/bin/zsh +set -euo pipefail +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +cd "$ROOT" +mkdir -p .build/test-logs +LOG=".build/test-logs/test-$(date +%Y%m%d-%H%M%S).log" +{ + echo '=== ORIONTOOLBAR REGRESSION TESTS ===' + echo 'Source contracts (non-destructive)' + /usr/bin/env python3 -m unittest discover -s tests -p 'test_*.py' -v + if [[ "${1:-}" == '--live' || "${1:-}" == '--all' ]]; then + echo 'Read-only Parallels integration checks' + ORION_LIVE=1 /usr/bin/env python3 -m unittest discover -s tests -p 'test_*.py' -v + fi + if [[ "${1:-}" == '--build' || "${1:-}" == '--all' ]]; then + echo 'Xcode build (does not launch app)' + /bin/zsh scripts/build.sh + fi +} 2>&1 | tee "$LOG" +echo "Test log: $LOG" +''', + 'tests/MANUAL_TEST_PLAN.md': '''# OrionToolbar manual regression checklist + +Run `./scripts/test.sh --all` first. These checks require a person using macOS; automated source checks **cannot** prove UI focus or VM lifecycle behaviour. + +## Dashboard +- [ ] Menu bar opens; Running, All, Integrated sections expand/collapse. +- [ ] VM status and IP match Parallels; refresh updates them. +- [ ] VM icons show the expected OS; custom integrated icon persists. +- [ ] Reorder All VMs, wait 20 seconds, reopen; order persists. +- [ ] Reorder Integrated Servers, wait 20 seconds, reopen; order persists. +- [ ] Remove a VM from toolbar, confirm it is still in Parallels; re-add it. +- [ ] Settings opens in front; edits survive app relaunch. +- [ ] Button spacing remains readable; no unwanted chevrons. + +## SSH and Mailpit +- [ ] VM stopped: neither service button visible. +- [ ] VM running with SSH port closed: SSH button hidden after refresh. +- [ ] VM running with SSH port open: SSH button visible after refresh. +- [ ] SSH click: Terminal appears **above** OrionToolbar and SSH starts. +- [ ] VM running without Mailpit: Mailpit button hidden after refresh. +- [ ] VM running with Mailpit HTTP responding: button visible after refresh. +- [ ] Mailpit click: browser appears **above** OrionToolbar and correct page loads. +- [ ] Stop service without stopping VM: corresponding button disappears after refresh. +- [ ] Restart service: corresponding button reappears after refresh. +- [ ] Open and close Settings after launching Terminal/browser; foreground behaviour still correct. + +## VM operations — changes VM state; use disposable VM only +- [ ] Start a stopped test VM; status becomes Running. +- [ ] Restart test VM; busy indicator appears then clears. +- [ ] Stop test VM; status becomes Stopped; service buttons disappear. +- [ ] Resume a paused/suspended test VM, if supported. +- [ ] Force Stop shows confirmation; Cancel does not stop VM. +- [ ] Force Stop confirmed only on a disposable VM; state updates. +- [ ] Failed Parallels command displays an error and clears busy indicator. + +## Final +- [ ] Quit and relaunch; preferences, hidden VMs and ordering persist. +- [ ] Review `.build/test-logs/` and `.build/build-logs/`. +''' +} +for rel in files: + target=root/rel + if target.exists(): + backup=root/'backups'/('tests-'+datetime.now().strftime('%Y%m%d-%H%M%S'))/rel + backup.parent.mkdir(parents=True,exist_ok=True) + shutil.copy2(target,backup) +for rel, content in files.items(): + target=root/rel + target.parent.mkdir(parents=True,exist_ok=True) + target.write_text(content) +(root/'scripts/test.sh').chmod(0o755) +print('Installed:', ', '.join(files)) +print('Run: ./scripts/test.sh') +print('Run build + live checks: ./scripts/test.sh --all') diff --git a/scripts/test.sh b/scripts/test.sh index 56a758b..59589db 100755 --- a/scripts/test.sh +++ b/scripts/test.sh @@ -2,40 +2,19 @@ set -euo pipefail ROOT="$(cd "$(dirname "$0")/.." && pwd)" cd "$ROOT" -mkdir -p "$ROOT/.build/build-logs" -LOG="$ROOT/.build/build-logs/test-$(date +%Y%m%d-%H%M%S).log" -exec > >(tee "$LOG") 2>&1 -print '=== OrionToolbar tests ===' -# A successful compile is the minimum regression check. -"$ROOT/scripts/build.sh" -APP="$ROOT/.build/Build/Products/Debug/OrionToolbar.app" -plutil -lint "$APP/Contents/Info.plist" -# Verify that the Parallels executable and JSON discovery work independently of SwiftUI. -PRLCTL='' -for candidate in /usr/local/bin/prlctl '/Applications/Parallels Desktop.app/Contents/MacOS/prlctl'; do - if [[ -x "$candidate" ]]; then PRLCTL="$candidate"; break; fi -done -if [[ -n "$PRLCTL" ]]; then - "$PRLCTL" list -a -j | python3 -c ' -import json,sys -v=json.load(sys.stdin) -assert isinstance(v,list), "Expected Parallels JSON array" -for item in v: - assert isinstance(item,dict), "Invalid VM record" - assert item.get("name"), "VM without name" - assert item.get("uuid"), "VM without UUID" -print("Parallels discovery OK: {} VM(s)".format(len(v))) -' -else - echo 'SKIP: Parallels CLI not installed' -fi -if [[ -d "$ROOT/tests" ]] && find "$ROOT/tests" -name 'test_*.py' -print -quit | grep -q .; then - python3 -m unittest discover -s "$ROOT/tests" -v -fi -# Run Xcode tests only if a test plan is configured for this scheme. -if [[ "${RUN_XCODE_TESTS:-0}" == 1 ]]; then - xcodebuild -project "$ROOT/OrionToolbar.xcodeproj" -scheme OrionToolbar -configuration Debug -destination 'platform=macOS' -derivedDataPath "$ROOT/.build" CODE_SIGNING_ALLOWED=NO test -else - echo 'XCTest not run (set RUN_XCODE_TESTS=1 if the scheme has a test target).' -fi -print "Tests complete. Log: $LOG" +mkdir -p .build/test-logs +LOG=".build/test-logs/test-$(date +%Y%m%d-%H%M%S).log" +{ + echo '=== ORIONTOOLBAR REGRESSION TESTS ===' + echo 'Source contracts (non-destructive)' + /usr/bin/env python3 -m unittest discover -s tests -p 'test_*.py' -v + if [[ "${1:-}" == '--live' || "${1:-}" == '--all' ]]; then + echo 'Read-only Parallels integration checks' + ORION_LIVE=1 /usr/bin/env python3 -m unittest discover -s tests -p 'test_*.py' -v + fi + if [[ "${1:-}" == '--build' || "${1:-}" == '--all' ]]; then + echo 'Xcode build (does not launch app)' + /bin/zsh scripts/build.sh + fi +} 2>&1 | tee "$LOG" +echo "Test log: $LOG"