import sys import json import subprocess from pathlib import Path import pytest WINDOWS_GUI = pytest.mark.skipif(sys.platform != 'win32', reason='Windows Tk packaging smoke test') def _disabled(widget): return str(widget.cget('state')) == 'disabled' def _drain_events(app): """Functional assertions wait for all queued events, even when Tk yields.""" while not app.events.empty(): app._drain() def _display_text(widget): """Read visible captions, including StringVar-backed result labels.""" values = [] for option in ('text', 'textvariable'): if option in widget.keys(): value = widget.cget(option) if option == 'textvariable' and value: value = ('' if 'show' in widget.keys() and widget.cget('show') else widget.getvar(value)) values.append(str(value)) for child in widget.winfo_children(): values.extend(_display_text(child)) return values @pytest.fixture(scope='module') def app_window(tmp_path_factory): from halo_frame_installer import gui # Tk applications use one root/interpreter; reset its state between scenarios. instance = gui.App(tmp_path_factory.mktemp('gui') / 'state') instance.withdraw() instance.update_idletasks() try: yield instance finally: for callback in instance.tk.call('after', 'info'): instance.after_cancel(callback) instance.destroy() @pytest.fixture def app(app_window, monkeypatch): from halo_frame_installer import gui dialogs = [] for name in ('showinfo', 'showerror'): monkeypatch.setattr(gui.messagebox, name, lambda title, message, **kwargs: dialogs.append((title, message))) monkeypatch.setattr(gui.messagebox, 'askyesno', lambda *args, **kwargs: True) def forbidden_network(*args, **kwargs): raise AssertionError('The GUI verification must not open an SSH connection.') monkeypatch.setattr(gui.SSHConnection, 'connect', forbidden_network) instance = app_window for callback in instance.tk.call('after', 'info'): instance.after_cancel(callback) instance.events.queue.clear() instance.last_result = None instance.source.set('') instance.host.set('frame') instance.port.set('22') instance.transport.set('network') instance.adb_path.set('') instance.usb_serial.set('') instance.fingerprints = {} instance.password.set('') instance.password_to_redact = '' instance.authorized.set(False) instance.steam_closed.set(False) instance.keep_saves.set(True) instance.mode.set('install') instance.max_page = 0 instance.installing = False instance.uninstall_committing = False instance.operation = None instance.cancel.clear() if instance.activity_open: instance._toggle_activity() if instance.advanced_open: instance._toggle_advanced() instance._set_busy(False) instance._show_page(0) instance.progress_state.reset() instance._display_progress() instance.log_lines.clear() instance.log.configure(state='normal') instance.log.delete('1.0', 'end') instance.log.configure(state='disabled') instance.withdraw() instance.update_idletasks() instance._test_dialogs = dialogs try: yield instance finally: for callback in instance.tk.call('after', 'info'): instance.after_cancel(callback) instance.password.set('') instance.password_to_redact = '' class _ImmediateThread: """Run an injected offline installer in the calling Tk thread.""" def __init__(self, target, **kwargs): self.target = target def start(self): self.target() def test_control_guide_matches_expected_layout(): from halo_frame_installer.gui import CONTROLS for action in ('Jump', 'Melee', 'Reload / use', 'Change weapon', 'LB Change grenade', 'RB Flashlight', 'Both grips Recenter'): assert action in CONTROLS @WINDOWS_GUI def test_community_mod_title_and_content_disclosure_are_visible_and_accurate(app): import tkinter as tk from halo_frame_installer.gui import APP_TITLE, CONTENT_DISCLOSURE, ARTWORK_CREDIT assert app.title() == APP_TITLE == 'Halo • Community Steam Frame VR Mod Installer' visible = '\n'.join(_display_text(app)) assert 'STEAM FRAME VR MOD' in visible and 'COMMUNITY MOD INSTALLER' in visible assert CONTENT_DISCLOSURE in app.data_disclosure.cget('text') assert 'my original Xbox game data with this mod' in app.data_authorization.cget('text') app._show_help() dialogs = [widget for widget in app.winfo_children() if isinstance(widget, tk.Toplevel)] try: assert len(dialogs) == 1 text = '\n'.join(_display_text(dialogs[0])) assert CONTENT_DISCLOSURE in text and ARTWORK_CREDIT in text assert 'public source using your authorized Xbox data' in text for claim in ('copyright-free', 'copyright free', 'unstolen', 'vimm', 'archive.org'): assert claim not in text.lower() finally: for dialog in dialogs: dialog.destroy() @WINDOWS_GUI def test_long_build_failure_has_readable_dialog_and_complete_redacted_log(app): app.password_to_redact = 'offline-secret' details = 'Native build failed. The existing game was kept.\n' + ('compiler diagnostic offline-secret\n' * 200) app.events.put(('error', details)) _drain_events(app) _, message = app._test_dialogs[-1] assert message.startswith('Native build failed. The existing game was kept.') assert len(message) < 600 and 'Show activity' in message and 'Save log' in message assert 'offline-secret' not in message assert '\n'.join(app.log_lines).count('compiler diagnostic [redacted]') == 200 @WINDOWS_GUI def test_gui_offline_smoke(): resources = Path(__file__).resolve().parents[1] / 'resources' code = ('import json, sys; from pathlib import Path; ' 'resources = Path(sys.argv[1]); ' 'sys.path.insert(0, str(resources.parent / "src")); ' 'from halo_frame_installer.selftest import smoke_test; ' 'print(json.dumps(smoke_test(resources)))') report = subprocess.run([sys.executable, '-c', code, str(resources)], capture_output=True, text=True, check=True) result = json.loads(report.stdout) assert result['ok'] and result['guiInitialized'] from halo_frame_installer import __version__ assert result['version'] == __version__ assert result['guiPagesInitialized'] == ['gameData', 'connection', 'install'] assert result['steamInfoAutomaticOnInstall'] and result['steamDescriptionUsesNotes'] assert result['networkConnections'] == 0 @WINDOWS_GUI def test_pages_preserve_inputs_and_mode(app): app.source.set('X:/my-original-xbox-image.iso') app.host.set('192.0.2.24') app.port.set('2222') app.password.set('offline-secret') app.authorized.set(True) app.steam_closed.set(True) app.mode.set('repair') for index in (1, 2, 0, 2, 1, 0): app._show_page(index) app.update_idletasks() assert app.current_page == index assert (app.source.get(), app.host.get(), app.port.get(), app.password.get(), app.authorized.get(), app.steam_closed.get(), app.mode.get()) == ( 'X:/my-original-xbox-image.iso', '192.0.2.24', '2222', 'offline-secret', True, True, 'repair') @WINDOWS_GUI def test_usb_and_network_choices_preserve_inputs_across_pages(app): app.host.set('192.0.2.24') app.port.set('2222') app.adb_path.set('C:/Platform Tools/adb.exe') app.usb_serial.set('FRAME-SERIAL') app.password.set('offline-secret') app.transport.set('usb') app._show_page(1) app.update_idletasks() assert app.usb_panel.winfo_manager() == 'pack' assert not app.network_panel.winfo_manager() assert app.usb_advanced.winfo_manager() == 'pack' assert not app.network_advanced.winfo_manager() assert app.summary_frame.get() == 'USB-C cable · FRAME-SERIAL' for index in (2, 0, 1): app._show_page(index) assert app.transport.get() == 'usb' assert app.adb_path.get() == 'C:/Platform Tools/adb.exe' assert app.usb_serial.get() == 'FRAME-SERIAL' assert app.password.get() == 'offline-secret' app.transport.set('network') assert app.network_panel.winfo_manager() == 'pack' assert not app.usb_panel.winfo_manager() assert app.network_advanced.winfo_manager() == 'pack' assert not app.usb_advanced.winfo_manager() assert (app.host.get(), app.port.get()) == ('192.0.2.24', '2222') assert app.summary_frame.get() == '192.0.2.24' assert 'Wi-Fi and Ethernet' in app.connection_prepare_note.cget('text') @WINDOWS_GUI def test_usb_settings_ignore_hidden_network_inputs_and_use_device_fingerprint(app): app.host.set('invalid hidden address') app.port.set('invalid-port') app.password.set('offline-secret') app.transport.set('usb') app.usb_serial.set(' FRAME-SERIAL ') app.adb_path.set(' C:/Platform Tools/adb.exe ') app.fingerprints = {'usb:FRAME-SERIAL': 'SHA256:usb', '127.0.0.1:2222': 'SHA256:wrong'} settings = app._settings() assert settings.transport == 'usb' assert settings.port == 22 assert settings.usb_serial == 'FRAME-SERIAL' assert settings.adb_path == 'C:/Platform Tools/adb.exe' assert settings.host_identity == 'usb:FRAME-SERIAL' assert settings.known_host_fingerprint == 'SHA256:usb' assert settings.known_host_fingerprints == app.fingerprints assert settings.known_host_fingerprints is not app.fingerprints assert settings.close_steam_for_shortcut is False @WINDOWS_GUI def test_automatic_usb_selection_supplies_saved_device_fingerprints(app): app.password.set('offline-secret') app.transport.set('usb') app.fingerprints = {'usb:FRAME-SERIAL': 'SHA256:usb'} settings = app._settings() assert settings.usb_serial is None assert settings.adb_path is None assert settings.known_host_fingerprint is None assert settings.known_host_fingerprints == {'usb:FRAME-SERIAL': 'SHA256:usb'} assert app.summary_frame.get() == 'USB-C cable · Detect connected Frame' @WINDOWS_GUI def test_trusted_usb_fingerprint_persists_by_serial_without_forwarded_port(app): import threading ready, answer = threading.Event(), [] app.events.put(('fingerprint', 'usb:FRAME-SERIAL', 'ssh-ed25519', 'SHA256:usb', 22, ready, answer)) _drain_events(app) assert ready.is_set() and answer == [True] assert app.fingerprints == {'usb:FRAME-SERIAL': 'SHA256:usb'} assert json.loads(app.state_file.read_text(encoding='utf-8')) == app.fingerprints app.events.put(('connected', 'usb:FRAME-SERIAL', 'SHA256:usb')) _drain_events(app) assert app.fingerprints == {'usb:FRAME-SERIAL': 'SHA256:usb'} assert app._load_fingerprints() == app.fingerprints assert '127.0.0.1' not in app.state_file.read_text(encoding='utf-8') @WINDOWS_GUI def test_network_settings_keep_address_port_and_existing_trust_identity(app): app.host.set('192.0.2.24') app.port.set('2222') app.password.set('offline-secret') app.fingerprints = {'192.0.2.24:2222': 'SHA256:network'} settings = app._settings() assert settings.transport == 'network' assert settings.host_identity == '192.0.2.24:2222' assert settings.known_host_fingerprint == 'SHA256:network' app.events.put(('connected', settings.host_identity, 'SHA256:network')) _drain_events(app) assert app.fingerprints == {'192.0.2.24:2222': 'SHA256:network'} @WINDOWS_GUI def test_connection_worker_remembers_resolved_usb_identity_without_opening_network(app, monkeypatch): from halo_frame_installer import gui closed = [] class OfflineConnection: host_fingerprint = 'SHA256:usb' def __init__(self, settings): self.settings = settings def connect(self): assert self.settings.transport == 'usb' self.settings.usb_serial = 'FRAME-SERIAL' def run(self, argv, **kwargs): assert argv == ['uname', '-m'] return 'aarch64' def close(self): closed.append(True) monkeypatch.setattr(gui, 'SSHConnection', OfflineConnection) monkeypatch.setattr(gui.threading, 'Thread', _ImmediateThread) app.transport.set('usb') app.password.set('offline-secret') app._test_connection() _drain_events(app) assert closed == [True] assert app.fingerprints == {'usb:FRAME-SERIAL': 'SHA256:usb'} assert not app.busy assert 'Connected to your Frame' in app.connection_status.get() @WINDOWS_GUI @pytest.mark.parametrize('connection_fails', [False, True]) def test_connection_cleanup_failure_restores_controls_clears_password_and_never_reports_success(app, monkeypatch, connection_fails): from halo_frame_installer import gui from halo_frame_installer.ssh import SSHError captured = [] actions = [] class OfflineConnection: host_fingerprint = 'SHA256:usb' def __init__(self, settings): self.settings = settings captured.append(settings) def connect(self): self.settings.usb_serial = 'FRAME-SERIAL' if connection_fails: raise SSHError('Offline connection failed.') def run(self, argv, **kwargs): return 'aarch64' def close(self): actions.append('close') raise SSHError('The USB forward could not be removed: offline-secret') monkeypatch.setattr(gui, 'SSHConnection', OfflineConnection) monkeypatch.setattr(gui.threading, 'Thread', _ImmediateThread) app.transport.set('usb') app.password.set('offline-secret') app._test_connection() assert actions == ['close'] assert captured[0].password == '' queued = list(app.events.queue) assert not any(event[0] == 'connected' for event in queued) assert queued[-1] == ('idle',) _drain_events(app) assert not app.busy assert not app.password.get() assert not _disabled(app.primary_button) assert all(not _disabled(choice) for choice in app.transport_choices) assert app.fingerprints == {} assert 'needs attention' in app.connection_status.get() assert 'SSH connection verified' not in '\n'.join(app.log_lines) log = '\n'.join(app.log_lines) assert 'USB forward could not be removed' in log assert 'offline-secret' not in log assert 'disconnecting' in log assert app._test_dialogs if connection_fails: assert 'Offline connection failed' in log else: assert any('disconnecting' in message for _, message in app._test_dialogs) @WINDOWS_GUI def test_adb_browse_retains_selection_and_is_disabled_during_setup(app, monkeypatch): from halo_frame_installer import gui calls = [] monkeypatch.setattr(gui.filedialog, 'askopenfilename', lambda **kwargs: calls.append(kwargs) or 'C:/Platform Tools/adb.exe') app.transport.set('usb') app._choose_adb() assert app.adb_path.get() == 'C:/Platform Tools/adb.exe' app._set_busy(True) assert all(_disabled(choice) for choice in app.transport_choices) assert all(_disabled(entry) for entry in app.entries) app._choose_adb() assert len(calls) == 1 for choice in app.transport_choices: choice._select() assert app.transport.get() == 'usb' app._set_busy(False) assert all(not _disabled(choice) for choice in app.transport_choices) @WINDOWS_GUI def test_usb_help_describes_cable_password_internet_and_official_tools(app): import tkinter as tk app.transport.set('usb') text = '\n'.join(_display_text(app)) for phrase in ('USB tools are included', 'data-capable USB-C cable', 'USB debugging prompt', 'SSH password', 'internet access'): assert phrase in text app._show_help() dialogs = [widget for widget in app.winfo_children() if isinstance(widget, tk.Toplevel)] try: assert len(dialogs) == 1 text = '\n'.join(_display_text(dialogs[0])) assert 'Developer Mode enabled' in text assert 'Wi-Fi or Ethernet network' in text from halo_frame_installer.gui import PLATFORM_TOOLS_URL assert PLATFORM_TOOLS_URL == 'https://developer.android.com/tools/releases/platform-tools' finally: for dialog in dialogs: dialog.destroy() @WINDOWS_GUI def test_continue_requires_authorized_data_but_allows_repair_without_image(app): app._nav_to(2) assert app.current_page == 0 assert _disabled(app.nav_buttons[2]) app.mode.set('install') app._continue() assert app.current_page == 0 assert app._test_dialogs app.mode.set('repair') app._continue() assert app.current_page == 0 # Existing data still requires authorization. app.authorized.set(True) app._continue() assert app.current_page == 1 assert not app.source.get() app.password.set('offline-secret') app.port.set('invalid-port') app.steam_closed.set(True) app._continue() assert app.current_page == 1 app.port.set('22') app.steam_closed.set(False) app._continue() assert app.current_page == 1 app.steam_closed.set(True) app._continue() assert app.current_page == 2 @WINDOWS_GUI def test_busy_disables_navigation_and_inputs_until_idle(app): app.source.set('X:/my-original-xbox-image.iso') app.authorized.set(True) app.password.set('offline-secret') app.steam_closed.set(True) app._show_page(1) app._set_busy(True) for control in (*app.nav_buttons, app.back_button, app.primary_button, *app.entries): assert _disabled(control) assert not _disabled(app.cancel_button) app.events.put(('idle',)) _drain_events(app) assert not app.busy assert not _disabled(app.primary_button) assert not _disabled(app.back_button) assert all(not _disabled(control) for control in app.nav_buttons[:2]) assert _disabled(app.nav_buttons[2]) # The next page remains gated by Continue. assert _disabled(app.cancel_button) @WINDOWS_GUI def test_repair_uses_existing_maps_and_clears_password(app, monkeypatch): from halo_frame_installer import gui, install calls = [] def fake_run(settings, maps_dir, progress, cancel): calls.append((maps_dir, settings.adopt_existing_native, settings.password)) progress('repair', 'Reinstalling program files', 64) return install.InstallResult('/home/steamos/Games/HaloCENativeVR', True, {'status': 'added', 'appid': 123}, 'SHA256:offline', repaired=True, backup_path='/home/steamos/Games/HaloCENativeVR.backup-offline') monkeypatch.setattr(install, 'run', fake_run) monkeypatch.setattr(gui.threading, 'Thread', _ImmediateThread) monkeypatch.setattr(gui, 'inspect_image', lambda *args, **kwargs: pytest.fail('Repair inspected a local image.')) monkeypatch.setattr(gui, 'inspect_maps', lambda *args, **kwargs: pytest.fail('Repair inspected a local maps folder.')) app.mode.set('repair') app.authorized.set(True) app.steam_closed.set(True) app.password.set('offline-secret') app._show_page(2) app._install(repair=True) _drain_events(app) assert calls == [(None, True, 'offline-secret')] assert app.last_result.repaired assert float(app.progress['value']) == 100 assert 'repaired' in app.status.get().lower() assert not app.password.get() assert not app.busy assert any('backup-offline' in line for line in app.log_lines) @WINDOWS_GUI def test_progress_and_errors_redact_password_on_every_display(app): secret = 'offline-secret' app.password_to_redact = secret app.password.set(secret) app._show_page(2) app._set_busy(True) app.events.put(('progress', 'build ' + secret, 'Compiler diagnostic ' + secret, 38)) _drain_events(app) assert float(app.progress['value']) == 0 # Unknown phases cannot corrupt overall progress. assert secret not in app.status.get() assert secret not in '\n'.join(app.log_lines) assert secret not in '\n'.join(_display_text(app)) app.events.put(('error', 'Connection failed: ' + secret)) app.events.put(('clear_password',)) app.events.put(('idle',)) _drain_events(app) assert not app.busy assert not app.password.get() assert any('[redacted]' in line for line in app.log_lines) assert secret not in '\n'.join(message for title, message in app._test_dialogs) assert secret not in '\n'.join(_display_text(app)) @WINDOWS_GUI def test_manual_steam_completion_keeps_actionable_instructions_redacted(app): from halo_frame_installer.install import InstallResult secret = 'offline-secret' app.password_to_redact = secret app._show_page(2) app._set_busy(True) result = InstallResult('/home/steamos/Games/HaloCENativeVR', False, {'status': 'manual', 'reason': 'Steam is still running. ' + secret, 'instructions': 'Use Steam Add a Non-Steam Game, then click Add to Steam again.'}, None) app.events.put(('complete', result)) app.events.put(('idle',)) _drain_events(app) assert app.last_result.requires_manual_steam_step assert float(app.progress['value']) == 99 assert 'Add to Steam again' in app.status.get() assert not _disabled(app.retry_button) assert secret not in app.status.get() assert secret not in '\n'.join(app.log_lines) assert secret not in '\n'.join(_display_text(app)) assert secret not in '\n'.join(message for title, message in app._test_dialogs) @WINDOWS_GUI def test_minimum_window_keeps_primary_navigation_in_bounds(app): width, height = app.minsize() app.geometry(f'{width}x{height}') app.deiconify() for index in range(3): app._show_page(index) app.update() # Process native window mapping after deiconify on Windows. for control in (*app.nav_buttons, app.primary_button, app.back_button): assert control.winfo_ismapped() left = control.winfo_rootx() - app.winfo_rootx() top = control.winfo_rooty() - app.winfo_rooty() assert left >= 0 and top >= 0 assert left + control.winfo_width() <= app.winfo_width() assert top + control.winfo_height() <= app.winfo_height() @WINDOWS_GUI def test_real_phase_progress_stays_determinate_and_details_do_not_replace_status(app): app.installing = True app.password_to_redact = 'offline-secret' app._show_page(2) app._begin_setup_progress() app.events.put(('progress', 'reuse', 'All retained maps verified.', 100)) app.events.put(('progress', 'build', 'Pulling the build container.', None)) app.events.put(('progress', 'compile', 'Completed 40 of 100 build tasks.', 40)) app.events.put(('progress', 'detail', 'compile: [40/100] Building offline-secret.cpp', None)) _drain_events(app) assert str(app.progress.cget('mode')) == 'determinate' assert float(app.progress['value']) == pytest.approx(79.8) assert app.progress_label.get() == '79%' assert app.phase.get() == 'Compiling Halo and VR support' assert app.status.get() == 'Completed 40 of 100 build tasks.' assert app.log_lines[-1] == 'compile: [40/100] Building [redacted].cpp' assert app.activity_open assert not app.install_summary.winfo_manager() @WINDOWS_GUI def test_error_and_cancel_keep_progress_and_new_attempt_resets_it(app): app._begin_setup_progress() app.events.put(('progress', 'compile', 'Building Halo.', 50)) _drain_events(app) held = float(app.progress['value']) app._cancel_setup() app.events.put(('progress', 'compile', 'Late queued build report.', 100)) app.events.put(('error', 'Setup cancelled. Existing saves were kept.')) app.events.put(('idle',)) _drain_events(app) assert float(app.progress['value']) == held assert str(app.progress.cget('mode')) == 'determinate' app._begin_setup_progress() assert float(app.progress['value']) == 0 assert app.progress_label.get() == '0%' @WINDOWS_GUI def test_box_art_pending_stays_at_99_with_retry_action(app): from halo_frame_installer.install import InstallResult result = InstallResult('/home/steamos/Games/HaloCENativeVR', False, {'status': 'added', 'artwork': {'status': 'manual', 'reason': 'Steam artwork directory is busy.'}}, None) app.events.put(('complete', result)) app.events.put(('idle',)) _drain_events(app) assert float(app.progress['value']) == 99 assert 'Steam info pending' in app.phase.get() assert not _disabled(app.retry_button) @WINDOWS_GUI def test_disconnect_phase_does_not_finish_before_the_installer_result(app): from halo_frame_installer.install import InstallResult app.events.put(('progress', 'disconnect', 'Closing the setup SSH connection.', None)) _drain_events(app) assert float(app.progress['value']) == 99 assert app.phase.get() == 'Closing the setup connection' app.events.put(('progress', 'disconnected', 'Setup has disconnected from your Frame.', None)) _drain_events(app) assert float(app.progress['value']) == 99 assert app.phase.get() == 'Disconnected from your Frame' result = InstallResult('/home/steamos/Games/HaloCENativeVR', False, {'status': 'added'}, None) app.events.put(('complete', result)) _drain_events(app) assert float(app.progress['value']) == 100 @WINDOWS_GUI def test_detail_burst_is_retained_and_drain_yields_to_the_window(app): for index in range(250): app.events.put(('progress', 'detail', f'compile: task {index}', None)) app._drain() assert not app.events.empty() # One turn drains at most 100 events. while not app.events.empty(): app._drain() assert app.log_lines == [f'compile: task {index}' for index in range(250)] assert float(app.progress['value']) == 0 @WINDOWS_GUI def test_installer_worker_does_not_throttle_adjacent_build_details(app, monkeypatch): from halo_frame_installer import gui, install def fake_run(settings, maps_dir, progress, cancel): progress('compile', 'Compiling the native game.', 50) for index in range(250): progress('detail', f'compile: task {index}', None) return install.InstallResult('/home/steamos/Games/HaloCENativeVR', False, {'status': 'added'}, None) monkeypatch.setattr(install, 'run', fake_run) monkeypatch.setattr(gui.threading, 'Thread', _ImmediateThread) app.authorized.set(True) app.steam_closed.set(True) app.password.set('offline-secret') app._install(repair=True) while not app.events.empty(): app._drain() assert [line for line in app.log_lines if line.startswith('compile: task ')] == [ f'compile: task {index}' for index in range(250)] assert float(app.progress['value']) == 100 assert not app.password.get() @WINDOWS_GUI def test_library_retry_resets_the_progress_before_starting_worker(app, monkeypatch): from halo_frame_installer import gui class HoldingThread: def __init__(self, **kwargs): pass def start(self): pass monkeypatch.setattr(gui.threading, 'Thread', HoldingThread) app.progress_state.finish(pending=True) app._display_progress() app.steam_closed.set(True) app.password.set('offline-secret') app._retry_steam() assert float(app.progress['value']) == 0 assert app.progress_label.get() == '0%' assert app.activity_open and app.busy @WINDOWS_GUI def test_minimum_window_keeps_running_progress_and_scrollable_activity_visible(app): width, height = app.minsize() app.geometry(f'{width}x{height}') app.deiconify() app.installing = True app._show_page(2) app._begin_setup_progress() app._set_busy(True) app.update() for control in (app.progress, app.phase_label, app.log, app.activity_scrollbar, app.cancel_button): assert control.winfo_ismapped() left = control.winfo_rootx() - app.winfo_rootx() top = control.winfo_rooty() - app.winfo_rooty() assert left >= 0 and top >= 0 assert left + control.winfo_width() <= app.winfo_width() assert top + control.winfo_height() <= app.winfo_height() @WINDOWS_GUI def test_uninstall_navigation_needs_ssh_but_no_iso_or_game_data_authorization(app): app.mode.set('uninstall') assert app.keep_saves.get() assert not app.source.get() and not app.authorized.get() assert app.uninstall_card.winfo_manager() assert not app.data_panel.winfo_manager() and not app.data_consent.winfo_manager() app._continue() assert app.current_page == 1 app._continue() assert app.current_page == 1 # An SSH password is still required. app.password.set('offline-secret') app._continue() assert app.current_page == 1 # Closing games is still required. app.steam_closed.set(True) app._continue() assert app.current_page == 2 assert app.primary_button.cget('text') == 'Uninstall Halo VR' assert app.summary_source.get() == '~/Games/HaloCENativeVR' assert 'backup' in app.summary_action.get() app.keep_saves.set(False) assert 'Remove saves' in app.summary_action.get() app.mode.set('install') app._show_page(0) app._continue() assert app.current_page == 0 # The install data/authorization gate returns. assert app.data_panel.winfo_manager() and app.data_consent.winfo_manager() assert not app.uninstall_card.winfo_manager() @WINDOWS_GUI @pytest.mark.parametrize('keep_saves', [True, False]) def test_uninstall_worker_uses_no_game_data_and_clears_credentials(app, monkeypatch, keep_saves): from halo_frame_installer import gui, install calls = [] confirmations = [] def fake_uninstall(self, settings, progress, cancel, *, keep_saves): calls.append((settings, settings.password, keep_saves)) progress('uninstall', 'Removing native Halo.', 50) progress('disconnect', 'Closing the setup SSH connection.', None) progress('disconnected', 'Setup has disconnected from your Frame.', None) return install.UninstallResult('/home/steamos/Games/HaloCENativeVR', True, False, '/home/steamos/Games/HaloCENativeVR-saves-offline' if keep_saves else None, {'status': 'removed'}, None) monkeypatch.setattr(install.Installer, 'uninstall', fake_uninstall) monkeypatch.setattr(gui.threading, 'Thread', _ImmediateThread) monkeypatch.setattr(gui.messagebox, 'askyesno', lambda title, message, **kwargs: confirmations.append(message) or True) for name in ('inspect_image', 'inspect_maps', 'extract_image', 'copy_maps'): monkeypatch.setattr(gui, name, lambda *args, **kwargs: pytest.fail('Uninstall accessed game data.')) monkeypatch.setattr(app.music, 'load', lambda *args: pytest.fail('Uninstall loaded game music.')) app.mode.set('uninstall') app.keep_saves.set(keep_saves) app.steam_closed.set(True) app.password.set('offline-secret') app._uninstall() _drain_events(app) settings, password, saved = calls[0] assert password == 'offline-secret' and saved is keep_saves assert settings.close_steam_for_shortcut is False assert not settings.password and not app.password.get() assert len(confirmations) == 1 assert '~/Games/HaloCENativeVR' in confirmations[0] and 'Steam library entry' in confirmations[0] assert ('backed up' if keep_saves else 'will also be removed') in confirmations[0] assert float(app.progress['value']) == 100 assert app.phase.get() == 'Halo VR uninstalled' assert app.last_result is None and not app.retry_button.winfo_manager() assert not app.busy and not app.uninstall_committing assert app._test_dialogs[-1][0] == 'Uninstall complete' assert 'Open Steam and launch' not in app.status.get() assert ('Campaign save backup:' in app.status.get()) is keep_saves @WINDOWS_GUI def test_uninstall_confirmation_can_decline_without_starting_worker(app, monkeypatch): from halo_frame_installer import gui, install monkeypatch.setattr(gui.messagebox, 'askyesno', lambda *args, **kwargs: False) monkeypatch.setattr(install.Installer, 'uninstall', lambda *args, **kwargs: pytest.fail('Uninstall was not confirmed.')) app.mode.set('uninstall') app.steam_closed.set(True) app.password.set('offline-secret') app._uninstall() assert not app.busy and not app.installing assert not any(event[0] in ('progress', 'uninstall_complete', 'error') for event in list(app.events.queue)) @WINDOWS_GUI def test_missing_native_game_has_an_idempotent_uninstall_message(app): from halo_frame_installer.install import UninstallResult result = UninstallResult('/home/steamos/Games/HaloCENativeVR', False, True, None, {'status': 'already-absent'}, None) app.events.put(('uninstall_complete', result)) _drain_events(app) assert float(app.progress['value']) == 100 assert 'already absent' in app.status.get() assert 'launch' not in app.status.get().lower() @WINDOWS_GUI def test_uninstall_can_cancel_before_removal_but_not_during_commit(app, monkeypatch): from halo_frame_installer import gui app.mode.set('uninstall') app.operation = 'uninstall' app.installing = True app._set_busy(True) app._cancel_setup() assert app.cancel.is_set() app.cancel.clear() app.events.put(('progress', 'uninstall', 'Finishing the removal transaction.', None)) _drain_events(app) assert app.uninstall_committing and _disabled(app.cancel_button) monkeypatch.setattr(gui.messagebox, 'askyesno', lambda *args, **kwargs: pytest.fail('Removal cannot be interrupted.')) app._cancel_setup() app._close() assert not app.cancel.is_set() assert 'Finishing removal' in app.status.get() app.events.put(('idle',)) _drain_events(app) assert not app.uninstall_committing and not app.busy @WINDOWS_GUI def test_uninstall_choices_and_active_layout_fit_minimum_window(app): app.geometry('900x620') app.deiconify() app.mode.set('uninstall') app._show_page(0) app.update() uninstall_choice = app.mode_choices[-1] assert uninstall_choice.value == 'uninstall' and uninstall_choice.winfo_ismapped() for control in (uninstall_choice, app.primary_button): left = control.winfo_rootx() - app.winfo_rootx() top = control.winfo_rooty() - app.winfo_rooty() assert left >= 0 and top >= 0 assert left + control.winfo_width() <= app.winfo_width() assert top + control.winfo_height() <= app.winfo_height() @WINDOWS_GUI def test_menu_music_is_enabled_without_a_stop_or_mute_button(app): assert not app.music.muted assert not hasattr(app, 'music_button') and not hasattr(app, '_toggle_music') visible = '\n'.join(_display_text(app)) assert 'Music: OFF' not in visible and 'Stop music' not in visible @WINDOWS_GUI def test_default_window_shows_all_operations_and_game_data_authorization(app): app.geometry('1040x720') app.deiconify() app._show_page(0) app.update() canvas = app.pages[0].canvas top = canvas.winfo_rooty() bottom = top + canvas.winfo_height() for control in (*app.mode_choices, app.data_authorization): assert control.winfo_ismapped() assert top <= control.winfo_rooty() assert control.winfo_rooty() + control.winfo_height() <= bottom assert len({control.winfo_rooty() for control in app.mode_choices}) == 1 assert 'uninstall' in app.page_subtitle.cget('text').lower() @WINDOWS_GUI def test_revision_guidance_names_validated_rev_2_and_checked_cache_builds(app): import tkinter as tk assert 'USA Rev 2 validated' in app.asset_status.get() assert 'checked automatically' in app.asset_status.get() app._show_help() dialogs = [widget for widget in app.winfo_children() if isinstance(widget, tk.Toplevel)] try: assert len(dialogs) == 1 text = '\n'.join(_display_text(dialogs[0])) for build in ('01.10.12.2276', '01.08.15.1749', '01.01.14.2342'): assert build in text assert 'NTSC:' in text and 'PAL:' in text assert 'PC and Xbox 360 data are not supported' in text finally: for dialog in dialogs: dialog.destroy() @WINDOWS_GUI @pytest.mark.parametrize('mode', ['install', 'repair', 'uninstall']) @pytest.mark.parametrize('games_closed', [False, True]) def test_game_closed_acknowledgment_never_authorizes_steam_shutdown(app, mode, games_closed): app.mode.set(mode) app.steam_closed.set(games_closed) app.password.set('offline-secret') assert app._settings().close_steam_for_shortcut is False assert 'Steam Home and SteamVR stay running' in app.connection_steam_note.cget('text') @WINDOWS_GUI @pytest.mark.parametrize('operation', ['continue', 'install', 'retry', 'uninstall']) def test_closed_game_gate_only_requests_closing_games(app, operation): app.password.set('offline-secret') app.authorized.set(True) app.steam_closed.set(False) if operation == 'continue': app._show_page(1) app._continue() elif operation == 'install': app._install(repair=True) elif operation == 'retry': app._retry_steam() else: app.mode.set('uninstall') app._uninstall() message = app._test_dialogs[-1][1] assert 'Save and close running games' in message assert 'Keep Steam Home and SteamVR running' in message assert 'restart Steam' not in message and 'Quit Steam' not in message assert not app.busy @WINDOWS_GUI @pytest.mark.parametrize('kind', ['add', 'artwork', 'remove']) def test_manual_library_fallback_uses_steam_ui_without_teardown_instructions(app, kind): from halo_frame_installer.install import InstallResult, UninstallResult game = '/home/steamos/Games/HaloCENativeVR' if kind == 'remove': result = UninstallResult(game, False, False, None, {'status': 'manual', 'reason': 'Steam is active.'}, None) event = 'uninstall_complete' expected = 'Remove Non-Steam Game' else: steam = ({'status': 'manual', 'reason': 'Steam is active.'} if kind == 'add' else {'status': 'added', 'artwork': {'status': 'manual', 'reason': 'Artwork is pending.'}}) result = InstallResult(game, False, steam, None) event = 'complete' expected = 'Add a Non-Steam Game' if kind == 'add' else 'custom artwork' app.events.put((event, result)) _drain_events(app) assert float(app.progress['value']) == 99 assert expected in app.status.get() assert 'Quit Steam' not in app.status.get() and 'restart Steam' not in app.status.get() @WINDOWS_GUI @pytest.mark.parametrize('mode', ['install', 'repair']) def test_install_and_repair_describe_automatic_steam_info_without_separate_choice(app, mode): import tkinter as tk app.mode.set(mode) assert {choice.value for choice in app.mode_choices} == {'install', 'repair', 'uninstall'} assert not hasattr(app, 'library_card') assert 'Steam artwork and game Notes are added automatically' in app.summary_note.get() assert app.data_panel.winfo_manager() and app.data_consent.winfo_manager() app._show_page(2) assert app.primary_button.cget('text') == ('Install Halo VR' if mode == 'install' else 'Repair Halo VR') app._show_help() dialogs = [widget for widget in app.winfo_children() if isinstance(widget, tk.Toplevel)] try: text = '\n'.join(_display_text(dialogs[0])) assert 'Install and repair automatically add Steam artwork and a game description in Steam Notes' in text assert 'Steam info updates' not in text finally: for dialog in dialogs: dialog.destroy() @WINDOWS_GUI @pytest.mark.parametrize('transport', ['network', 'usb']) def test_pending_steam_retry_does_not_read_game_data_or_build_and_clears_credentials(app, monkeypatch, transport): from halo_frame_installer import gui, install calls = [] def fake_library(self, settings, progress, cancel): calls.append((settings, settings.password)) assert settings.transport == transport assert not settings.close_steam_for_shortcut progress('steam', 'Updating library info.', 100) progress('disconnected', 'Setup has disconnected from your Frame.', None) return install.InstallResult('/home/steamos/Games/HaloCENativeVR', False, {'status': 'added', 'artwork': {'status': 'added', 'icon': {'status': 'added'}}, 'notes': {'status': 'added', 'title': 'About Halo', 'content': 'Campaign and controls.', 'message': 'Added.'}}, None) monkeypatch.setattr(install.Installer, 'add_to_steam', fake_library) monkeypatch.setattr(install, 'run', lambda *args, **kwargs: pytest.fail('Steam info rebuilt the game.')) monkeypatch.setattr(gui.threading, 'Thread', _ImmediateThread) for name in ('inspect_image', 'inspect_maps', 'extract_image', 'copy_maps'): monkeypatch.setattr(gui, name, lambda *args, **kwargs: pytest.fail('Steam info accessed game data.')) monkeypatch.setattr(app.music, 'load', lambda *args: pytest.fail('Steam info read game music.')) app.transport.set(transport) app.steam_closed.set(True) app.password.set('offline-secret') app.last_result = install.InstallResult('/home/steamos/Games/HaloCENativeVR', False, {'status': 'added', 'notes': {'status': 'manual', 'title': 'About Halo', 'content': 'Campaign.'}}, None) app._show_page(2) app._set_busy(False) assert app.retry_button.winfo_manager() and not _disabled(app.retry_button) app.retry_button.invoke() _drain_events(app) assert len(calls) == 1 settings, password = calls[0] assert password == 'offline-secret' assert not settings.password and not app.password.get() assert not app.busy and not app.installing assert float(app.progress['value']) == 100 assert app.phase.get() == 'Steam info updated' assert 'native game and saves were kept' in app.status.get() assert 'installed.' not in app.status.get() assert app._test_dialogs[-1][0] == 'Steam info updated' assert not app.retry_button.winfo_manager() @WINDOWS_GUI @pytest.mark.parametrize('pending', ['icon', 'notes', 'artwork']) def test_steam_info_partial_results_stay_actionable_without_claiming_complete_update(app, pending): from halo_frame_installer.install import InstallResult app.operation = 'library' app.password_to_redact = 'offline-secret' steam = {'status': 'added', 'artwork': {'status': 'added', 'icon': {'status': 'added'}}, 'notes': {'status': 'added', 'title': 'About Halo', 'content': 'Campaign and controls.'}} if pending == 'icon': steam['artwork']['icon'] = {'status': 'manual', 'reason': 'Icon not persisted. offline-secret'} elif pending == 'notes': steam['notes'].update(status='manual', message='Open Steam Notes. offline-secret') else: steam['artwork']['status'] = 'manual' steam['artwork']['reason'] = 'Banner unavailable. offline-secret' app.events.put(('complete', InstallResult('/home/steamos/Games/HaloCENativeVR', False, steam, None))) app.events.put(('idle',)) _drain_events(app) assert float(app.progress['value']) == 99 assert app.phase.get() == 'Ready to play · Steam info pending' assert 'some Steam library info needs one more step' in app.status.get() assert 'has been updated' not in app.status.get() assert not _disabled(app.retry_button) assert 'offline-secret' not in app.status.get() assert 'offline-secret' not in '\n'.join(app.log_lines) assert app._test_dialogs[-1][0] == 'Steam info needs attention' assert bool(app.notes_button.winfo_manager()) is (pending == 'notes') @WINDOWS_GUI def test_manual_steam_description_is_copyable_and_never_overrides_about_game(app): import tkinter as tk from halo_frame_installer.gui import Button from halo_frame_installer.install import InstallResult app.operation = 'library' app.password_to_redact = 'offline-secret' app.events.put(('complete', InstallResult('/home/steamos/Games/HaloCENativeVR', False, {'status': 'added', 'notes': {'status': 'manual', 'title': 'Halo CE VR — About & controls', 'content': 'Explore Halo.\nA: Jump. B: Melee. offline-secret', 'message': 'Steam Notes needs a manual step.'}}, None))) app.events.put(('clear_password',)) app.events.put(('idle',)) _drain_events(app) assert app.notes_button.winfo_manager() and not _disabled(app.notes_button) assert 'description in Steam Notes' in app.status.get() app._show_steam_note() dialogs = [widget for widget in app.winfo_children() if isinstance(widget, tk.Toplevel)] try: assert len(dialogs) == 1 visible = '\n'.join(_display_text(dialogs[0])) assert 'Add this description in Steam Notes' in visible assert 'About this game' not in visible def descendants(widget): for child in widget.winfo_children(): yield child yield from descendants(child) fields = [widget for widget in descendants(dialogs[0]) if isinstance(widget, tk.Text)] assert len(fields) == 1 assert fields[0].get('1.0', 'end-1c') == 'Explore Halo.\nA: Jump. B: Melee. [redacted]' copy = next(widget for widget in descendants(dialogs[0]) if isinstance(widget, Button) and widget.cget('text') == 'Copy description') copy.invoke() assert app.clipboard_get() == 'Explore Halo.\nA: Jump. B: Melee. [redacted]' app.clipboard_clear() finally: for dialog in dialogs: dialog.destroy() @WINDOWS_GUI @pytest.mark.parametrize('icon_status', ['existing', 'custom', 'unchanged', 'preserved']) def test_preserved_steam_info_is_successful_without_manual_pending(app, icon_status): from halo_frame_installer.install import InstallResult app.operation = 'library' app.events.put(('complete', InstallResult('/home/steamos/Games/HaloCENativeVR', False, {'status': 'added', 'artwork': {'status': 'unchanged', 'icon': {'status': icon_status}}, 'notes': {'status': 'custom', 'title': 'Halo CE VR — About & controls', 'content': 'Player notes.'}}, None))) app.events.put(('idle',)) _drain_events(app) assert float(app.progress['value']) == 100 assert app.phase.get() == 'Steam info updated' assert 'existing custom game note was kept' in app.status.get() assert not app.notes_button.winfo_manager() @WINDOWS_GUI def test_all_operation_tile_captions_fit_minimum_width(app): app.geometry('900x620') app.deiconify() app._show_page(0) app.update() assert {choice.value for choice in app.mode_choices} == {'install', 'repair', 'uninstall'} for choice in app.mode_choices: for item in choice.find_all(): if choice.type(item) == 'text': left, top, right, bottom = choice.bbox(item) assert 0 <= left < right <= choice.winfo_width() assert 0 <= top < bottom <= choice.winfo_height() @WINDOWS_GUI def test_iso_inspection_runs_off_tk_thread_and_window_remains_responsive(app, monkeypatch, tmp_path): import threading from types import SimpleNamespace from halo_frame_installer import gui real_thread = threading.Thread started, release = threading.Event(), threading.Event() workers, inspected_threads, music_loads = [], [], [] def inspect(path, *, cancel_event): inspected_threads.append(threading.get_ident()) assert path == tmp_path / 'Halo.iso' assert not cancel_event.is_set() started.set() assert release.wait(5) return SimpleNamespace(map_count=24, estimated_bytes=1_000_000_000, build='01.10.12.2276') def schedule(**kwargs): thread = real_thread(**kwargs) workers.append(thread) return thread monkeypatch.setattr(gui.threading, 'Thread', schedule) monkeypatch.setattr(gui, 'inspect_image', inspect) monkeypatch.setattr(app.music, 'load', music_loads.append) try: app._inspect(tmp_path / 'Halo.iso') assert started.wait(2) assert app.busy and not app.source.get() assert inspected_threads == [workers[0].ident] assert workers[0].ident != threading.get_ident() heartbeat = [] app.after(0, lambda: heartbeat.append(True)) app.update() assert heartbeat == [True] # Tk processed a timer while validation was blocked. assert music_loads == [None] # The old preview stops before reading the new ISO. finally: release.set() for worker in workers: worker.join(3) _drain_events(app) assert not app.busy assert app.source.get() == str(tmp_path / 'Halo.iso') assert 'Validated 24 original Xbox maps' in app.asset_status.get() assert music_loads == [None, tmp_path / 'Halo.iso'] @pytest.fixture def queued_inspection(app, monkeypatch): from types import SimpleNamespace from halo_frame_installer import gui workers, music_loads = [], [] class DeferredThread: def __init__(self, target, **kwargs): self.target = target workers.append(self) def start(self): pass monkeypatch.setattr(gui.threading, 'Thread', DeferredThread) monkeypatch.setattr(app.music, 'load', music_loads.append) return SimpleNamespace(workers=workers, music_loads=music_loads, info=SimpleNamespace(map_count=24, estimated_bytes=1_000_000_000, build='01.10.12.2276')) @WINDOWS_GUI @pytest.mark.parametrize('old_error', [False, True]) @pytest.mark.parametrize('old_finishes_first', [False, True]) def test_late_inspection_results_do_not_replace_new_selection_or_clear_its_busy_state(app, monkeypatch, tmp_path, queued_inspection, old_error, old_finishes_first): from halo_frame_installer import gui old, new = tmp_path / 'old.iso', tmp_path / 'new.iso' cancellations = {} def inspect(path, *, cancel_event): cancellations[path] = cancel_event if path == old and old_error: raise ValueError('The old image was invalid.') return queued_inspection.info monkeypatch.setattr(gui, 'inspect_image', inspect) app._inspect(old) old_cancel = app._inspection_cancel app._inspect(new) assert old_cancel.is_set() if old_finishes_first: queued_inspection.workers[0].target() _drain_events(app) assert app.busy and not app.source.get() assert app.asset_status.get() == 'Checking original Xbox game data…' queued_inspection.workers[1].target() _drain_events(app) if not old_finishes_first: queued_inspection.workers[0].target() _drain_events(app) assert app.source.get() == str(new) assert app.status.get() == 'Game data validated. Connect your Frame next.' assert not app.busy assert cancellations[old].is_set() and not cancellations[new].is_set() assert queued_inspection.music_loads == [None, None, new] assert not app._test_dialogs assert 'old image' not in '\n'.join(app.log_lines) @WINDOWS_GUI def test_clearing_source_cancels_inspection_and_ignores_its_late_success(app, monkeypatch, tmp_path, queued_inspection): from halo_frame_installer import gui monkeypatch.setattr(gui, 'inspect_image', lambda path, **kwargs: queued_inspection.info) app._inspect(tmp_path / 'Halo.iso') cancel = app._inspection_cancel app.source.set('') assert cancel.is_set() and not app.busy queued_inspection.workers[0].target() _drain_events(app) assert not app.source.get() assert app.asset_status.get() == gui.XBOX_REVISION_NOTE assert queued_inspection.music_loads == [None, None] assert not app._test_dialogs @WINDOWS_GUI @pytest.mark.parametrize('folder', [False, True]) def test_cancelled_source_inspection_keeps_previous_selection_and_does_not_start_music(app, monkeypatch, tmp_path, queued_inspection, folder): from halo_frame_installer import gui selected = tmp_path / ('maps' if folder else 'Halo.iso') if folder: selected.mkdir() calls = [] def inspect(path, *, cancel_event): calls.append((path, cancel_event.is_set())) # Even a reader finishing after cancellation cannot accept its result. return queued_inspection.info monkeypatch.setattr(gui, 'inspect_maps' if folder else 'inspect_image', inspect) app.source.set('previous.iso') app._inspect(selected) app._cancel_setup() queued_inspection.workers[0].target() _drain_events(app) assert calls == [(selected, True)] assert app.source.get() == 'previous.iso' assert not app.busy assert app.phase.get() == 'Game data check cancelled' assert 'previous selection was kept' in app.status.get() assert queued_inspection.music_loads == [None] assert not app._test_dialogs @WINDOWS_GUI def test_current_inspection_failure_preserves_selection_and_readable_redacted_error(app, monkeypatch, tmp_path, queued_inspection): from halo_frame_installer import gui def invalid(path, *, cancel_event): raise ValueError('Invalid original Xbox image. offline-secret\nHeader validation failed.') monkeypatch.setattr(gui, 'inspect_image', invalid) app.source.set('previous.iso') app.password_to_redact = 'offline-secret' app._inspect(tmp_path / 'invalid.iso') queued_inspection.workers[0].target() _drain_events(app) assert not app.busy assert app.source.get() == 'previous.iso' assert "Couldn't validate this data" in app.asset_status.get() assert len(app._test_dialogs) == 1 assert 'Header validation failed.' in '\n'.join(app.log_lines) assert 'offline-secret' not in '\n'.join(app.log_lines) assert 'offline-secret' not in app._test_dialogs[0][1] assert queued_inspection.music_loads == [None] @WINDOWS_GUI @pytest.mark.parametrize('folder', [False, True]) def test_cancel_during_install_revalidation_stops_before_extraction_and_clears_credentials(app, monkeypatch, tmp_path, folder): from halo_frame_installer import gui, install selected = tmp_path / ('maps' if folder else 'Halo.iso') if folder: selected.mkdir() name = 'inspect_maps' if folder else 'inspect_image' inspect_source = getattr(gui, name) inspections, settings_used = [], [] get_settings = app._settings def remember_settings(): settings = get_settings() settings_used.append(settings) return settings def cancel_validation(path, *, cancel_event): assert cancel_event is app.cancel inspections.append(path) # Model Cancel arriving as the worker starts its second validation. cancel_event.set() return inspect_source(path, cancel_event=cancel_event) monkeypatch.setattr(app, '_settings', remember_settings) monkeypatch.setattr(gui, name, cancel_validation) monkeypatch.setattr(gui.threading, 'Thread', _ImmediateThread) for action in ('extract_image', 'copy_maps'): monkeypatch.setattr(gui, action, lambda *args, **kwargs: pytest.fail('Cancelled validation extracted maps.')) monkeypatch.setattr(install, 'run', lambda *args, **kwargs: pytest.fail('Cancelled validation began installation.')) app.source.set(str(selected)) app.authorized.set(True) app.steam_closed.set(True) app.password.set('offline-secret') app._install() _drain_events(app) assert inspections == [selected] assert app.cancel.is_set() assert not app.busy and not app.installing assert not app.password.get() and not settings_used[0].password assert not app.password_to_redact assert app.source.get() == str(selected) assert 'cancelled' in '\n'.join(app.log_lines).lower() assert 'offline-secret' not in '\n'.join(app.log_lines) assert float(app.progress['value']) == 0 assert not _disabled(app.primary_button) @WINDOWS_GUI @pytest.mark.parametrize('operation', ['install', 'library']) def test_completed_game_operation_reports_pending_connection_cleanup(app, operation): from halo_frame_installer.install import InstallResult app.operation = operation app.password_to_redact = 'offline-secret' result = InstallResult('/home/steamos/Games/HaloCENativeVR', False, {'status': 'added', 'notes': {'status': 'added'}}, None, cleanup_warning='Unplug the USB cable; cleanup is pending. offline-secret') app.events.put(('complete', result)) app.events.put(('clear_password',)) app.events.put(('idle',)) _drain_events(app) assert float(app.progress['value']) == 99 assert app.phase.get() == 'Connection cleanup needs attention' assert 'game operation finished' in app.status.get() assert 'Unplug the USB cable' in app.status.get() assert not app.retry_button.winfo_manager() assert 'offline-secret' not in app.status.get() assert 'offline-secret' not in '\n'.join(app.log_lines) assert app._test_dialogs[-1][0] == 'Connection cleanup needs attention' assert not app.busy and not app.installing @WINDOWS_GUI @pytest.mark.parametrize('pending', [False, True]) def test_uninstall_recovery_reports_retained_files_and_earlier_save_backups(app, pending): from halo_frame_installer.install import UninstallResult backup = '/home/steamos/Games/HaloCENativeVR-saves-' + 'a'*32 retained = '/home/steamos/Games/.HaloCENativeVR-uninstall-unverified' app.password_to_redact = 'offline-secret' result = UninstallResult('/home/steamos/Games/HaloCENativeVR', not pending, False, None, {'status': 'removed'}, None, removal_pending=pending, removal_warning='Earlier uninstall files ' + ('were retained' if pending else 'were removed') + '. offline-secret', recovered_save_backup_paths=(backup,), retained_quarantine_paths=(retained,) if pending else ()) app.events.put(('uninstall_complete', result)) app.events.put(('clear_password',)) app.events.put(('idle',)) _drain_events(app) assert float(app.progress['value']) == (99 if pending else 100) assert 'Earlier campaign save backup: ' + backup in app.status.get() assert 'already absent' not in app.status.get() assert 'offline-secret' not in app.status.get() assert 'offline-secret' not in '\n'.join(app.log_lines) if pending: assert 'Preserved uninstall folder: ' + retained in app.status.get() assert 'Removal is not complete' in app.status.get() assert 'has been uninstalled' not in app.status.get() assert app._test_dialogs[-1][0] == 'Uninstall needs attention' else: assert 'has been uninstalled' in app.status.get() assert app._test_dialogs[-1][0] == 'Uninstall complete' @WINDOWS_GUI def test_uninstalled_game_reports_pending_connection_cleanup(app): from halo_frame_installer.install import UninstallResult result = UninstallResult('/home/steamos/Games/HaloCENativeVR', True, False, None, {'status': 'removed'}, None, cleanup_warning='Unplug the USB cable before retrying setup.') app.events.put(('uninstall_complete', result)) app.events.put(('idle',)) _drain_events(app) assert float(app.progress['value']) == 99 assert 'has been uninstalled' in app.status.get() assert 'Connection cleanup needs attention' in app.status.get() assert 'Unplug the USB cable' in app.status.get() assert app._test_dialogs[-1][0] == 'Uninstall needs attention'