Skip to content

A heap of tests, and the bugs they found - #354

Merged
kloptops merged 15 commits into
PortsMaster:mainfrom
kloptops:main-tests
Oct 21, 2026
Merged

kloptops merged 15 commits into
PortsMaster:mainfrom
kloptops:main-tests

Conversation

@kloptops

Copy link
Copy Markdown
Member

Adds tests across harbourmaster and pugwash: 882 → 1,601 tests, coverage 61% → 86%. Every bug below has a test that fails without its fix. The suite passes on Python 3.7 and 3.11, and the errors-only lint is clean.

Includes fix-source-update (its own PR), which some tests depend on.

Data loss

  • Uninstall could delete outside the ports folder. A .. in an installed port.json's items (or ../x or an absolute path in its files) was deleted along with the port, and .. is the SD card root on muOS. Install was safe because it builds items from the zip. port_info_load() now drops unsafe item names (info.is_safe_port_item()), and uninstall skips them whatever port.json says.

PortMaster wouldn't start

  • A corrupt config.json, for example after a power cut while saving: HarbourMaster, the update check and the GUI now start from defaults (new harbourmaster.json_load_dict()). Same for themes.json.
  • A corrupt runtimes.json.
  • A selected custom theme with broken JSON, or one that fails to load (for example, a typo in an element's area). Reinstalling didn't help because themes survive a reinstall. It now falls back to the default theme, and broken themes are left out of the theme list.
  • A version.json missing the channel's version, url or md5.

Installs failing because of the frontend

  • muOS/TrimUI catalogue errors failed the port install. The "a broken gamelist never fails an install" guard now covers them.
  • muOS: a catalogue missing its box/preview/text folders. They're created inside an existing catalogue.
  • RetroDECK with neither retrodeck.json nor retrodeck.cfg.

fifo_control

  • A command that raised left the done file on WAIT, so the shell script waited forever. Each command now answers FAIL and the loop carries on.
  • register_clear_info always raised.
  • The data/% format argument of progress went into the total instead.
  • With a single argument, do_fifo_control raised IndexError.

Ports and runtimes

  • A port.json with no name took the first letter of its port's name (ports_info.json's items are mostly strings).
  • An unsigned script renamed and found by its md5 had its names swapped, so its port showed as broken.
  • A signed renamed script was also listed as an unknown port.
  • list_runtimes() saved config.json on every call, and never noticed a runtime copied in after it was listed as Not Installed.
  • When PortMaster-Info files over 1 MB failed validation, they were deleted and then copied: FileNotFoundError.

Smaller fixes

  • harbourmaster ports.json crashed on ports that aren't installed.
  • harbourmaster help device_info crashed because the command had no docstring.
  • --no-log without a log file: the CLI silently didn't run the command, and pugwash removed every log handler.
  • Choosing the default language crashed when the language came from the system.
  • Odd input that crashed:
    • a theme colour that isn't hex (#ggg)
    • runtime names that aren't in the usual form
    • unreadable or timezone-aware timestamps
    • stdout without a real file behind it
  • EventManager.buttons was missing SELECT, SCRN and the stick directions until first pressed.

New test files

  • test_cli_commands, test_hm_info, test_hm_ports, test_hm_runtimes
  • test_platform_hooks: every CFW's hooks, against a fake device root
  • test_update_check, test_util_edges, test_hardware_edges
  • test_gui_scenes and test_gui_screens: the menus driven a button at a time
  • test_main, test_events: real SDL events
  • test_sdl_helpers, test_fifo_commands: through a real named pipe
  • test_theme_engine

The tests that use absolute device paths point them into a temp folder, because this suite also runs on devices as root.

kloptops and others added 15 commits October 21, 2026 15:16
- PortMasterV3.update() emptied the port list before checking whether it had
  already updated this run, so "Update ports" right after the startup update
  showed nothing until PortMaster restarted.
- When ports.json can't be fetched, the cached ports are loaded instead of
  leaving the list empty for the session.
- net.fetch() caught only ConnectionError: a read timeout (a slow first-boot
  network) went up through HarbourMaster's startup. It now catches any
  requests error, and fetch_json() returns None for a body that isn't JSON
  (a captive portal's page).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
…they found

New tests: test_cli_commands.py (every command in-process, ConsoleCallback,
fifo_control, main's flags), test_hm_info.py (load_info against a fake
PortMaster-Info tree) and test_hm_ports.py (load_ports' repairs, renamed
scripts, broken/unknown ports, attributes, requirements, listing, sorting,
images).

Fixed:
- `harbourmaster ports.json` crashed on any port that isn't installed
  (KeyError: 'status').
- `harbourmaster help device_info` crashed: it had no docstring.
- A corrupt runtimes.json crashed every start.
- A PortMaster-Info file over 1MB that failed validation was deleted and then
  copied over the good one: FileNotFoundError.
- A port.json without a name took the first letter of its port's name from
  ports_info.json (whose items are mostly strings), and missed scripts whose
  names aren't lower case.
- An unsigned renamed script found by its md5 got its names swapped, so its
  port showed as broken.
- A signed renamed script of a port without a port.json was also listed as an
  unknown port.
- A port.json made for such a port left out its own 'port.json' entry.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
…ck, and the bugs they found

New tests: test_platform_hooks.py (every CFW's hooks against a fake device
root), test_hm_runtimes.py (list_runtimes, check_runtime, featured ports) and
test_update_check.py (the self-update check, broken config files).

Fixed:
- A broken config.json (cut off by a power off while saving) stopped
  PortMaster starting: HarbourMaster, the update check and the GUI now start
  from the defaults. themes.json likewise, and one missing keys no longer
  takes the theme list down. New harbourmaster.json_load_dict() for these;
  json_safe_load() also survives a file that isn't text.
- A version.json without the channel's version/url/md5 crashed the update check.
- muOS/TrimUI catalogue errors failed the port install: the "a broken gamelist
  never fails an install" guard now covers them too.
- muOS: a catalogue without its box/preview/text folders failed every install
  with a gameinfo.xml; they're made inside an existing catalogue.
- RetroDECK without a retrodeck.json or .cfg failed every such install too.
- list_runtimes() saved config.json on every call, and never noticed a runtime
  copied in after it was listed as Not Installed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
…o, config, hardware

Uninstalling deletes each name in the port's port.json, and a `..` in its items
(or a `../x` or absolute path in its files) deleted outside the ports folder:
`..` took the folder holding it, on muOS the SD card. Install was safe (it takes
the items from the zip), but a hand-edited or broken port.json on the device
wasn't. port_info_load() now drops any item that isn't a plain relative name
(new info.is_safe_port_item()), and uninstall skips anything else whatever
port.json says.

Also fixed:
- runtime_nicename() crashed on runtimes not named the usual way
  ("mono.squashfs" in a hand-written port.json).
- datetime_compare() crashed on a broken or timezone-aware time (a source's
  last_checked): it now counts as long ago, so the check runs.
- cprint() crashed when stdout isn't a real file (in_terminal()).
- nice_size() divided once too often past TB.
- items_opt that isn't a list, and non-string items, crashed port_info_load().

New tests: test_util_edges.py (text, files, net, port_info_load/merge, safe
uninstall in a sandbox), test_hardware_edges.py (the quick CFW/device probe
against a fake root, get_info details) and more detect_paths cases.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
test_gui_scenes.py: the main menu (every entry, exit, the Konami code), ports
lists (moving, selecting, empty lists, no HarbourMaster), filters (genres,
attributes, porters, sorting and reversing, clearing), featured ports
(categories, lists), every option (toggles, release channels, restore,
GCD modes, gamelist, TrimUI's port location, the secret options), the
language list and the on screen keyboard.

Also:
- Options' "Delete PortMaster Config" crashed when the config folder was
  already gone; it and "Delete PortMaster Runtimes" use the HarbourMaster's
  folders (the same ones on a device).
- LanguageScene: A on a blank row can't crash (the d-pad skips them, a
  theme's list might not).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
test_gui_screens.py: the startup warnings for unsupported firmware and low RAM
(against a fake device root), the disclaimer and warning screens (waiting,
accepting, quitting), which first screen TempMenuScene picks, port info
(install, uninstall, next/previous port, the popup and its links), the runtime
manager (details, check, download all, uninstall, ports using a runtime) and
the theme and colour scheme lists.

ThemeSchemeScene: A on a blank row can't crash, as in LanguageScene.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
- `harbourmaster --no-log` with no PortMaster folder at import raised
  NameError (LOG_FILE_HANDLE was never set), which logger.catch swallowed:
  the command never ran.
- pugwash: the same case called logger.remove(None), which removes every
  handler, stderr's too.
- EventManager.buttons was seeded with SDL keycodes and axis names instead of
  button names, so SELECT, SCRN and the stick directions were missing until
  first pressed.

New tests: test_main.py (flags, logging, offline detection, fifo_control,
the update check, the reboot file, the frontend refresh), test_events.py
(keyboard, controller buttons, sticks and triggers, key repeat, hotplug,
the xbox fix, pushed onto SDL's real event queue).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
hex_color_decode() raised ValueError on a colour that isn't hex ("#ggg" in a
custom theme); it is white now, as a bad length already was. It also handed
out the same cached list to every caller, it's a tuple now.

test_sdl_helpers.py: Rect maths (fit, clip, inflate, edges, scaling), theme
areas (NamedRects: fractions, pixels, negative offsets, errors), colours, dict
merging, autoscroll, timers and animations.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
- A fifo command that raised ended fifo_control with the done file on WAIT,
  so the shell script waited for good. Each command now answers FAIL instead
  and the loop carries on.
- register_clear_info unpacked three values from two: it always raised.
- progress put its "data"/"%" format into the total: the format was never
  used, and with a non-numeric total it crashed comparing an int and a str.
- do_fifo_control checked the length of its second path, not of its
  arguments: one argument raised IndexError.

test_fifo_commands.py: registers, messages, progress, message_box and
selection_list option parsing, and the do_fifo_control loop driven through a
real named pipe, as PortMasterDialog.txt drives it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
- The selected theme's theme.json cut off or hand-edited into bad JSON:
  PortMaster crashed on every start, and reinstalling didn't help (themes
  survive it). It now falls back to the default theme, as for a missing one.
- A theme that is valid JSON but fails to load (eg: a typo in an element's
  area) likewise: ThemeEngine.gui_init() uses the default theme instead. A
  forced theme (the theme editor) still raises, so the error shows.
- Any installed theme with a broken theme.json took the theme list down;
  it's left out now.

test_theme_engine.py: requirement tags in theme keys, theme_update/merge,
the installed and downloadable themes list (updates, images, order), the
current theme's fallbacks, forced themes and schemes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
…lt language

The tests came from the beta branch, where these apply at once (reload_theme /
reload_language) and there are port links and a theme editor. Here they ask,
save, and quit to apply at the next start, so the tests check that instead.
The theme fallback is tested with Theme.gui_init stood in for, which needs no
reloading.

Choosing the default language crashed (KeyError) when the language came from
the system rather than the config: del cfg_data['language'] is a pop now.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
Even if it was fetched when PortMaster started: a release may have been made
since. It used to stop at "Up to date already".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
It checked exists() then is_file(): if harbourmaster removed the stale file in
between, the test took the missing file for the pipe, and its open() made a
plain file there, so mkfifo failed with FileExistsError (seen on Python 3.13
in CI). One stat() and S_ISFIFO instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01NxABmR1rUUgi4iicVLZchY
@kloptops
kloptops merged commit c903de7 into PortsMaster:main Oct 21, 2026
4 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant