Commit Graph

272 Commits

Author SHA1 Message Date
ItsAllAboutTheCode
80e6c46ee8 Fixed Lutris UnitTest when run on github
The primary causes of the issue is the manipulation of how the gog_cloud.py and gog_cloud_hooks.py was getting imported in UnitTest via the importlib mechanism. That seemed to have broken the resolution of paths to `lutris.services.*`.
As the GTK issues that it was trying to side step has been fixed at the `tests` root level, the workaround is no longer needed.

Additional changes have been added to help harden any potentially UnitTest environment and  circular import issues.
* Updated the github unit test run to use a virtual env to install the package dependencies and run the test
* Moved the imports of `lutris.services` subpackage to the method calls inside of the `__init__.py` script which will defer any potential circular imports until after the `lutris/services/__init__.py` script is fully imported.
2026-04-11 09:47:37 -05:00
ItsAllAboutTheCode
0a38190c4d Fix Lutris GUI reporting Vita installed titles as missing
The Vita3K emulator launches Vita titles using their Title ID and not a file path.
Therefore the Vita3K runner is setup to accept the title ID field.
However that title ID field was set as the 'main_file' option in the config yaml that gets generated when a Vita entry is added.
The missing installed title check in Lutris invokes the game.py [get_path_from_config](4620e1ba2f/lutris/game.py (L643)) which treats the `main_file` key as a file path.

As the Vita title ID is not a valid file path, the Missing Games check would flag all Vita installed titles as missing.

The fix here is to change the key from 'main_file' to 'title_id' so that the field is not treated as a file path.
2026-03-31 01:06:15 -05:00
Sebastien Duthil
c14de1ed31 mypy: add typing annotations on gui/application.py 2026-03-28 23:50:24 -04:00
ItsAllAboutTheCode
900904c4db Added unittest run to the static analysis workflow
Also updated the pre-commit hook to invoke the test as well.

Added a nose2 plugin that skip the Dialog test when running on CI
2026-03-24 10:42:47 -05:00
ItsAllAboutTheCode
b67e5e067a Adding explicit require_verions to GdkPixbuff inside of startup.py
This fixes an error about importing `GdkPixbuf` without specify a version.

Also added a `__init__.py` to the `tests` directory, so that it is seen as a package which allows the python `unittest` module to discover test within it.
2026-03-24 08:58:34 -05:00
Daniel Johnson
4d0641e50b Rename the world! All unit tests will start with '_test_' to avoid ambiguity. This means stuff like 'test_config.py' will no longer be loaded as test module.
This means that very module must be explicitly loaded by various tests that need the 'gi.requires_versions' part- they no longer sometimes get this by accident (sometimes).

In turn, _test_cloud_save_progress will no longer stub stuff in sys.modules, which is awful- it breaks later tests. That in turns makes it much chattier since it now actually logs stuff. But it seems to pass for all that.

Yikes!

Resolves #6570

Much research and drudgery done by Claude Code 🤖 before it clocked out.
2026-03-24 09:20:25 -04:00
Daniel Johnson
c6950f3a4a Remove redundant str() on game IDs now converted at DB boundary
With game IDs converted to str inside games.py query functions,
these manual str() wrappers at callsites are no longer needed.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 17:11:26 -04:00
Daniel Johnson
bb1fb966e3 Return None from get_game_by_field when no game is found
Previously returned an empty dict, which masked missing-game errors and
was inconsistent with the Optional[DbGameDict] return type of
get_game_for_service. Callers that use the result as a fallback dict
(Game.__init__, Runner.__init__, web/pico8 runners) now use 'or {}'.
Callers that need a real game (install_updates, install_dlc,
on_game_duplicate, generate_script, uninstall sync) now guard against
None and bail out instead of crashing with a KeyError.

Also fixes redundant str() in add_or_update and wrong param type on
get_matching_game.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-23 16:48:47 -04:00
Daniel Johnson
75287851cb Hack to make the test pass- we're converting IDs to str earlier and it's causing problems. 2026-03-22 18:18:01 -04:00
Daniel Johnson
82428a1954 Fix cloud sync test: set skip_cloud_sync on mock game
MagicMock returns a truthy object for unset attributes, so
game.skip_cloud_sync was truthy, causing _sync_location to
auto-resolve conflicts as uploads instead of showing the dialog.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-16 16:51:02 -04:00
Mathieu Comandon
e98326ec2d Add GOG depot-based downloads via gogdl integration
Integrate heroic-gogdl as a runtime component to download GOG games
directly from GOG's depot/CDN system (the same backend GOG Galaxy uses),
offering faster downloads and incremental updates compared to offline
installers.

Changes:
- New `gogdl_setup` installer command for depot downloads
- Auth bridge between Lutris GOG tokens and gogdl's format
- Depot installer offered as primary, offline installer as fallback
- Post-install game detection from goggame-*.info (DOSBox, ScummVM,
  Wine, native)
- Progress bar in the installer window during depot downloads
- Incremental depot updates for depot-installed games
- Handle expired GOG tokens gracefully (auto re-login via
  AuthTokenExpiredError)
- Wine prefix set to $GAMEDIR/pfx, separate from game files

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-03-15 23:33:19 -07:00
Daniel Johnson
62f721956e The GOG cloud sync progress popup dialog is super annoying as it usually just briefly flashes up. Instead, let's use the sidebar download queue
Replace CloudSyncProgressDialog with CloudSyncProgressAdapter that bridges
the sync worker thread's progress_callback to ProgressBox polling in the
existing sidebar DownloadQueue. The stop button provides skip-sync capability.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:47:02 -04:00
Daniel Johnson
a18cfd2f6a Fix test failures in PR #6525 cloud sync tests
- Replace object.__new__() with CloudSyncProgressDialog.__new__() to
  fix Python 3.13+ compatibility (object.__new__ rejects classes whose
  __init__ takes extra parameters)
- Gzip-compress mock response data in download_file tests to match the
  actual gzip.decompress() call in the implementation

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-10 07:13:39 -04:00
Daniel Johnson
e9dcd9922d Add type annotations to DownloadCollectionProgressBox and test helpers to satisfy mypy.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-03-09 18:34:12 -04:00
e88z4
6eb1f994d5 feat: Optimize GOG download speeds with parallel connections and resume support
Phase 1 - Chunk size & cache protection:
- Increase download chunk size from 8KB to 512KB (64x improvement)
- Add connection pooling support to Downloader (session + chunk_size params)
- Add lutris/util/download_cache.py: prevents deletion of in-progress files
  before installation completes; lifecycle states DOWNLOADING -> DOWNLOADED
  -> INSTALLING -> INSTALLED/FAILED; preserves failed installs for 7 days
- Integrate cache protection into interpreter.py cleanup, download progress
  boxes, and installation commands

Phase 2 - Multi-connection parallel downloader:
- New GOGDownloader (lutris/util/gog_downloader.py): parallel HTTP Range
  requests (4 workers by default) for large GOG installer files
- HTTP Range support detection with automatic single-stream fallback
- Connection pooling via requests.Session + HTTPAdapter
- Per-worker retry with exponential backoff (3 attempts)
- Thread-safe progress tracking for UI integration
- InstallerFile.downloader_class property for service-specific injection
- GOG service injects GOGDownloader via _format_links()

Phase 3 - Download resume with persistent byte-range tracking:
- New lutris/util/download_progress.py: atomic JSON sidecar (.progress)
  tracks completed byte ranges across crashes, hibernate, network errors
- GOGDownloader resumes from remaining ranges on restart; skips entirely
  if all ranges complete; starts fresh on file_size mismatch
- Atomic write (os.replace) for crash-safe progress file updates

Phase 4 - Concurrent file downloads, stall detection, pipelined I/O:
- DownloadCollectionProgressBox: concurrent downloads (MAX_CONCURRENT_FILES=2,
  prefetch-one), _ActiveDownload per-file state, aggregate progress/ETA,
  per-download independent retry
- Downloader: DownloadStallError, stall detection (LOW_SPEED_LIMIT=200 B/s,
  LOW_SPEED_TIME=30s), retry loop, _do_download()/_prepare_retry()
- GOGDownloader: pipelined writer thread (_writer_loop) decouples disk I/O
  from download workers; eliminates disk-write latency

Quality gates:
- ruff check: all checks passed
- ruff format: all files formatted
- mypy: success, no issues
- pytest: all tests passed
2026-03-09 06:23:03 +00:00
Mathieu Comandon
0cfbbec530 feat: replace pulsing loader with progress bar in cloud sync dialog
Thread a progress callback from the sync dialog through the GOG cloud
hooks into the sync engine so per-file progress is reported. The dialog
now shows a real progress bar (e.g. "3 / 10") with the current filename
(middle-ellipsized to prevent dialog resizing).
2026-03-08 01:02:32 -08:00
e88z4
c6d8ff6d65 feat: add cloud sync progress dialog to prevent UI freeze
Replace blocking cloud sync calls during game launch and quit with a
non-blocking progress dialog (CloudSyncProgressDialog). The sync work
runs on a background thread while a pulsing progress bar keeps the GTK
main loop alive, preventing the window manager from flagging Lutris as
'not responding'.

Changes:
- New: lutris/gui/dialogs/cloud_sync_progress.py
  Modal-style dialog with pulsing progress bar, status labels, and
  a 'Skip Sync' button for users who want to launch immediately.
- Modified: lutris/game.py
  Pre-launch sync (configure_game) and post-quit sync (on_game_quit)
  now use the progress dialog for GOG games instead of blocking calls.
- New: tests/test_cloud_sync_progress.py
  14 unit tests covering dialog construction, callbacks, cancel/skip
  behaviour, pulse lifecycle, and sync completion handling.

Passes ruff and mypy static analysis.
2026-03-07 22:59:38 -08:00
e88z4
68d561b68e feat: Add GOG cloud save synchronization support
Implement RFC-compliant OAuth2 with PKCE-based cloud save sync for GOG.com.
Enables automatic upload/download of game saves before launch and after exit.

Key features:
- OAuth2 PKCE authentication flow (RFC 6749, RFC 7636)
- Automatic sync on game launch (download) and exit (upload)
- Conflict detection with local-wins protection
- Gzip compression/decompression for cloud storage
- Platform-specific save location resolution (Windows/Linux/Mac)
- Progress tracking with detailed logging

Implementation details:
- Game-scoped authentication using builds API credentials
- Platform mapping: lowercase for builds API, capitalized for remote-config
- MD5 checksum verification for download integrity
- Timestamp-based conflict detection
- GTK dialog for manual conflict resolution

Technical fixes:
- Added gzip decompression for downloaded saves (was writing compressed)
- Fixed platform parameter capitalization mismatch between APIs
- Implemented dual platform mapping for different GOG endpoints
- Added None checks for mypy compliance in importlib usage

Testing:
- 111 unit tests with 98-99% code coverage
- Verified upload/download integrity with MD5 checksums
- Tested conflict detection and data protection
- Validated OAuth2 token refresh flow
- All mypy and ruff checks passing

Files:
- lutris/services/gog_cloud.py: Core cloud storage protocol (~958 lines)
- lutris/services/gog_cloud_hooks.py: Game lifecycle hooks (~287 lines)
- lutris/gui/dialogs/cloud_sync.py: Conflict resolution dialog
- lutris/game.py: Integration with game launch/exit
- tests/test_gog_cloud.py: Comprehensive test suite
- tests/test_gog_cloud_hooks.py: Hook integration tests
2026-03-07 22:59:38 -08:00
Sebastien Duthil
c13a6af1e1 util: rewrite cache_single as class decorator
Why:

* remove type-checking issues
* seems more appropriate than using nonlocal and monkey-patching
  cache_clear()
2026-02-22 12:01:23 -08:00
ItsAllAboutTheCode
390624661b Added Open Folder Location for game art
Refactored the game_common class to split out the Game Info panel UI to it's own file.
The Game Info panel has been updated to support the "Advanced Options" switch.
When the Advanced Options switch is selected, several text entries become available with the path to the art images on the filesystem, as well as a Open Folder location button to jump to the location of the art image.

This is the part of implementing the browse UI option for issue 4213 to navigate to the UI image location.
2026-02-14 14:46:53 -08:00
Mathieu Comandon
11ee730ca8 Finish removing runner updater 2025-06-29 01:24:37 -07:00
Dan Johnson
949ed354f2 MyPy fixes
My MyPy complains about the use of a Python 3.9 feature (why?) and then does not detect any type errors! I fix the test so MyPy will detect my actual type annotation error.

And then I fix that.
2025-06-17 09:30:03 -04:00
Eikeno
1d40df3cf2 modify refs from ~/.config/lutris to ~/.local/share/lutris
Also make utils/clean_config use CONFIG_DIR from settings.py instead of
hardcoded ref.

1 typo fix
2025-03-22 18:30:29 +01:00
wrightgabriel0220
df6a1c9f3d Fixes formatting in BIOS config changes 2024-12-28 11:43:44 -08:00
Mathieu Comandon
2f0775bfb3 Revert test back to wine-ge-8-26 2024-04-12 00:25:43 -07:00
Mathieu Comandon
782e3330dd Add test for incorrect version 2024-03-28 14:28:57 -07:00
Mathieu Comandon
960c078e94 Add mock for runner version 2024-03-28 13:24:45 -07:00
Mathieu Comandon
916e8aa0db Start breaking up version code and add tests 2024-03-28 12:51:00 -07:00
Mathieu Comandon
c490ae2a8d Move some runner tests in runners folder 2024-03-28 10:58:33 -07:00
Daniel Johnson
44c2419ac1 Replace game filter with more efficient version.
This will just leave the list alone if 'Installed games only' is off and the filter text is empty.

Also, it no longer strips the filter text over and over again, but just once at the start. It still has to strip each game name, but it's half the strippings now.

Also, fix some ruff issues.
2024-03-21 18:36:19 -04:00
ItsAllAboutTheCode
ed0f86e6f6 Add Vita3K runner support
A Unit Test script has also been added to validate the runner options.

resolves #5357
2024-03-18 17:47:11 -07:00
Mathieu Comandon
24f8fee432 Re-enable imports sorting 2024-02-24 21:14:32 -08:00
Mathieu Comandon
38fbe9f05e Ruff reformat 2024-02-24 21:02:06 -08:00
Mathieu Comandon
de70ab6cb0 Rename PGA_DB to DB_PATH 2024-02-17 20:12:46 -08:00
Mathieu Comandon
bdfb77771f Reorder display options and simplify driver version logging 2024-02-01 03:55:25 -08:00
Mathieu Comandon
a663f8006e Accept more formats in the playtime edit widget, add tests 2024-01-10 18:26:41 -08:00
Mathieu Comandon
85444a0b77 Rename common.py to game_common.py 2023-12-28 15:29:42 -08:00
Daniel Johnson
0ce76eed22 And the big one - replace the FILE_NOT_FOUND error code with an exception, MissingGameExecutableError, that provides the custom message. 2023-12-27 18:41:08 -05:00
Daniel Johnson
4b0592c8cd Update unit tests to represent the new rounding behavior
Truncating the minutes part of a playtime was unstable; format(parse(x) didn't result in 'x'; rounding fixes that. But the unit tests were impacted, so let's update them.

Resolves #5191
2023-12-17 17:21:03 -05:00
Daniel Johnson
32841b9ab6 Fix up the PCX2 test, so it will work even though PCSX2 is not there.
This isn't as fancy as MagicMock, but I need to stub out a couple of functions in the runner being testing, so MagicMock doesn't work.
2023-12-16 11:27:42 -05:00
Daniel Johnson
382d5681d5 Fix "scope" option and restore "Default installation folder"
This was the only option to use the 'scope' feature, and in 0.5.14 it was available in system and runner options, but not games.

This fix restores that behavior.

The trick is to pass the config_level through to the various option boxes, not just the config_section. The first is which dialog you are in (roughly) and the second is which tab.
2023-12-12 18:47:46 -05:00
Daniel Johnson
e52f3add18 Make all my new error messages for localization. 2023-11-29 19:16:01 -05:00
Daniel Johnson
22e8305314 Rework the mocks so the PCSX2 tests will still run even when it's not installed. 2023-11-29 19:16:01 -05:00
Daniel Johnson
2eec736fd9 Replace add_url_tags() with gtk_safe_urls(), which does both conversion and escaping.
add_url_tags() isn't correct- it does not handle escaping and can't be safely combined with gtk_safe(), as is done in two places.

But URLs can explicitly contain &, and that must be escaped. Text not inside URLs needs to be escaped, but escaping before or after the anchors breaks the URLs.

This function splits the text up into URLs and not-URLs and handles each case.
2023-11-04 07:36:28 -04:00
K900
1f1d554df3 scummvm: handle runner not being available in get_command, add/fix test 2023-10-29 15:51:53 -07:00
Mathieu Comandon
97600fdca7 Remove 'and' between hours and minutes 2023-08-25 17:00:39 -07:00
Mathieu Comandon
41c300fefd Group delegate classes in the delegates module 2023-06-14 00:49:10 -07:00
Daniel Johnson
a245387b03 Merge pull request #4866 from lengau/nvidia
Make Nvidia info not depend on /proc access
2023-05-31 15:10:42 -04:00
Daniel Johnson
14ae02536e First cut at version compatibility restrictions
This code looks for a '.lutris_compat.json' file at the root of a
component's version's directory.

This contains, in JSON, the minimum Lutris version required. If Lutris
finds this to be too late, it will try earlier versions.

The code is messy and this could download a lot of versions, since
it tries them one at a time. But it's a start.
2023-05-29 11:37:44 -07:00
Alex Lowe
5415015f6f Typing additions for drivers file 2023-05-20 22:27:16 -04:00