From a758ba73efc07b4cffda08246e9ed3ed34c9de68 Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 11 Oct 2025 15:43:48 +0200 Subject: [PATCH 1/2] Introduce a backend interface and use it for package operations --- src/Core/Backend.vala | 18 ++++++++++++++++++ src/Core/FlatpakBackend.vala | 6 +++--- src/Core/Package.vala | 27 ++++++++++++--------------- src/meson.build | 1 + 4 files changed, 34 insertions(+), 18 deletions(-) create mode 100644 src/Core/Backend.vala diff --git a/src/Core/Backend.vala b/src/Core/Backend.vala new file mode 100644 index 000000000..1ad9d3dbc --- /dev/null +++ b/src/Core/Backend.vala @@ -0,0 +1,18 @@ +/* + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public interface AppCenterCore.Backend : Object { + public signal void operation_finished (Package package, Package.State operation, Error? error); + + public abstract void notify_package_changed (Package package); + + public abstract bool is_package_installed (Package package) throws GLib.Error; + + public abstract async bool install_package (Package package, ChangeInformation? change_info, Cancellable? cancellable) throws GLib.Error; + public abstract async bool remove_package (Package package, ChangeInformation? change_info, Cancellable? cancellable) throws GLib.Error; + public abstract async bool update_package (Package package, ChangeInformation? change_info, Cancellable? cancellable) throws GLib.Error; +} diff --git a/src/Core/FlatpakBackend.vala b/src/Core/FlatpakBackend.vala index c82a4776f..482b3c632 100644 --- a/src/Core/FlatpakBackend.vala +++ b/src/Core/FlatpakBackend.vala @@ -23,6 +23,7 @@ public class AppCenterCore.FlatpakPackage : Package { public FlatpakPackage (string uid, Flatpak.Installation installation, AppStream.Component component) { Object ( uid: uid, + backend: FlatpakBackend.get_default (), installation: installation, component: component ); @@ -49,8 +50,7 @@ public class AppCenterCore.FlatpakPackage : Package { } } -public class AppCenterCore.FlatpakBackend : Object { - public signal void operation_finished (Package package, Package.State operation, Error? error); +public class AppCenterCore.FlatpakBackend : Object, Backend { public signal void cache_flush_needed (); public signal void on_metadata_remote_preprocessed (string remote_title); public signal void package_list_changed (); @@ -234,7 +234,7 @@ public class AppCenterCore.FlatpakBackend : Object { runtime_updates_component.summary = _("Updates to app runtimes"); runtime_updates_component.add_icon (runtime_icon); - runtime_updates = new AppCenterCore.Package ("runtime-updates", runtime_updates_component); + runtime_updates = new AppCenterCore.Package ("runtime-updates", this, runtime_updates_component); additional_updates = new GLib.ListStore (typeof (Package)); additional_updates.append (runtime_updates); diff --git a/src/Core/Package.vala b/src/Core/Package.vala index 05cfbd84e..e7d8fcb77 100644 --- a/src/Core/Package.vala +++ b/src/Core/Package.vala @@ -104,6 +104,7 @@ public class AppCenterCore.Package : Object { public const string DEFAULT_PRICE_DOLLARS = "1"; public string uid { get; construct; } + public weak Backend backend { get; construct; } public AppStream.Component component { get; protected set; } public ChangeInformation change_information { public get; private set; } @@ -174,9 +175,9 @@ public class AppCenterCore.Package : Object { return false; } - if (component.get_id () in AppCenter.App.settings.get_strv ("paid-apps")) { - return false; - } + // if (component.get_id () in AppCenter.App.settings.get_strv ("paid-apps")) { + // return false; + // } var newest_release = get_newest_release (); if (newest_release != null && newest_release.get_urgency () == AppStream.UrgencyKind.CRITICAL) { @@ -429,8 +430,8 @@ public class AppCenterCore.Package : Object { action_cancellable = new GLib.Cancellable (); } - public Package (string uid, AppStream.Component component) { - Object (uid: uid, component: component); + public Package (string uid, Backend backend, AppStream.Component component) { + Object (uid: uid, backend: backend, component: component); } public void replace_component (AppStream.Component component) { @@ -466,7 +467,7 @@ public class AppCenterCore.Package : Object { // Only trigger a notify if the state has changed, quite a lot of things listen to this if (state != new_state) { state = new_state; - FlatpakBackend.get_default ().notify_package_changed (this); + backend.notify_package_changed (this); } } @@ -486,24 +487,22 @@ public class AppCenterCore.Package : Object { return false; } - unowned var flatpak_backend = AppCenterCore.FlatpakBackend.get_default (); - try { bool success = yield perform_operation (State.INSTALLING, State.INSTALLED, State.NOT_INSTALLED); if (success) { - flatpak_backend.operation_finished (this, State.INSTALLING, null); + backend.operation_finished (this, State.INSTALLING, null); } return success; } catch (Error e) { - flatpak_backend.operation_finished (this, State.INSTALLING, e); + backend.operation_finished (this, State.INSTALLING, e); return false; } } public async bool uninstall () throws Error { // We possibly don't know if this package is installed or not yet, so trigger that check first - _installed = AppCenterCore.FlatpakBackend.get_default ().is_package_installed (this); + _installed = backend.is_package_installed (this); update_state (); @@ -552,12 +551,10 @@ public class AppCenterCore.Package : Object { change_information.start (); state = initial_state; - FlatpakBackend.get_default ().notify_package_changed (this); + backend.notify_package_changed (this); } private async bool perform_package_operation () throws GLib.Error { - unowned var backend = AppCenterCore.FlatpakBackend.get_default (); - switch (state) { case State.UPDATING: var success = yield backend.update_package (this, change_information, action_cancellable); @@ -593,7 +590,7 @@ public class AppCenterCore.Package : Object { change_information.cancel (); } - FlatpakBackend.get_default ().notify_package_changed (this); + backend.notify_package_changed (this); } public uint cached_search_score = 0; diff --git a/src/meson.build b/src/meson.build index 1008c3939..d19b6b9bc 100644 --- a/src/meson.build +++ b/src/meson.build @@ -5,6 +5,7 @@ appcenter_files = files( 'SuspendControl.vala', 'Utils.vala', 'Core' / 'AddonFilter.vala', + 'Core' / 'Backend.vala', 'Core/CardUtils.vala', 'Core' / 'CategoryManager.vala', 'Core/ChangeInformation.vala', From e12a44b78446a81c74338c5e828a2016fdb4cb0f Mon Sep 17 00:00:00 2001 From: Leonhard Kargl Date: Sat, 11 Oct 2025 15:44:56 +0200 Subject: [PATCH 2/2] Add a simple package operations test --- meson.build | 11 ++-- test/Core/PackageOperations.vala | 105 +++++++++++++++++++++++++++++++ test/CoreTest.vala | 1 + test/MockBackend.vala | 69 ++++++++++++++++++++ test/meson.build | 13 +++- 5 files changed, 194 insertions(+), 5 deletions(-) create mode 100644 test/Core/PackageOperations.vala create mode 100644 test/MockBackend.vala diff --git a/meson.build b/meson.build index c8893ad70..08424a900 100644 --- a/meson.build +++ b/meson.build @@ -20,6 +20,7 @@ add_project_arguments(['--vapidir', vapi_dir], language: 'vala') glib = dependency ('glib-2.0') gobject = dependency ('gobject-2.0') gio = dependency ('gio-2.0') +gio_unix = dependency ('gio-unix-2.0') gee = dependency ('gee-0.8') gtk = dependency ('gtk4', version: '>=4.10') granite = dependency ('granite-7', version: '>=7.7.0') @@ -38,20 +39,22 @@ posix = meson.get_compiler('vala').find_library('posix') dbus = dependency ('dbus-1') core_deps = [ + appstream, + flatpak, + gee, glib, gobject, gio, + gio_unix, + gtk, json, libsoup, + xml, ] dependencies = core_deps + [ - gtk, granite, adwaita, - appstream, - flatpak, - xml, math_dep, polkit, portal, diff --git a/test/Core/PackageOperations.vala b/test/Core/PackageOperations.vala new file mode 100644 index 000000000..31e9bd9f4 --- /dev/null +++ b/test/Core/PackageOperations.vala @@ -0,0 +1,105 @@ +/* + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +void add_package_operations_tests () { + Test.add_func ("/package/operations/simple", () => { + var loop = new MainLoop (); + test_package_operations_simple.begin (() => { + loop.quit (); + }); + loop.run (); + }); +} + +public async void test_package_operations_simple () { + var backend = new MockBackend (); + var component = new AppStream.Component (); + var package = new AppCenterCore.Package ("test-package", backend, component); + + try { + yield test_run_op (package, backend, INSTALLING, NOT_INSTALLED, INSTALLED); + + package.change_information.updatable_packages.add ("test-package"); + package.update_state (); + + yield test_run_op (package, backend, UPDATING, UPDATE_AVAILABLE, INSTALLED); + + yield test_run_op (package, backend, REMOVING, INSTALLED, NOT_INSTALLED); + } catch (Error e) { + assert_not_reached (); + } +} + +public async void test_run_op ( + AppCenterCore.Package package, + MockBackend backend, + AppCenterCore.Package.State op, + AppCenterCore.Package.State state_before, + AppCenterCore.Package.State state_after +) throws Error { + assert_cmpint (package.state, EQ, state_before); + + Error? error = null; + + AsyncReadyCallback callback = (obj, res) => { + try { + switch (op) { + case INSTALLING: + package.install.end (res); + break; + + case UPDATING: + package.update.end (res); + break; + + case REMOVING: + package.uninstall.end (res); + break; + + default: + assert_not_reached (); + } + } catch (Error e) { + error = e; + } + + Idle.add (() => { + test_run_op.callback (); + return Source.REMOVE; + }); + }; + + switch (op) { + case INSTALLING: + package.install.begin (callback); + break; + + case UPDATING: + package.update.begin (callback); + break; + + case REMOVING: + package.uninstall.begin (callback); + break; + + default: + assert_not_reached (); + } + + assert_cmpint (package.state, EQ, op); + + backend.finish_operation (); + + yield; + + if (error != null) { + assert_cmpint (package.state, EQ, state_before); + throw error; + } else { + assert_cmpint (package.state, EQ, state_after); + } +} diff --git a/test/CoreTest.vala b/test/CoreTest.vala index 3c1c1216b..f6a876e84 100644 --- a/test/CoreTest.vala +++ b/test/CoreTest.vala @@ -3,5 +3,6 @@ void main (string[] args) { add_card_tests (); add_houston_tests (); add_stripe_tests (); + add_package_operations_tests (); Test.run (); } diff --git a/test/MockBackend.vala b/test/MockBackend.vala new file mode 100644 index 000000000..d584065ed --- /dev/null +++ b/test/MockBackend.vala @@ -0,0 +1,69 @@ +/* + * SPDX-FileCopyrightText: 2025 elementary, Inc. (https://elementary.io) + * SPDX-License-Identifier: GPL-3.0-or-later + * + * Authored by: Leonhard Kargl + */ + +public class MockBackend : Object, AppCenterCore.Backend { + public signal void finish_operation (); + + public int package_changed_notified { get; private set; default = 0; } + + public Error? next_operation_error { get; set; default = null; } + + public Gee.List installed_packages { get; construct; } + + construct { + installed_packages = new Gee.ArrayList (); + } + + public void notify_package_changed (AppCenterCore.Package package) { + package_changed_notified++; + } + + public bool is_package_installed (AppCenterCore.Package package) throws GLib.Error { + return package in installed_packages; + } + + public async bool install_package (AppCenterCore.Package package, AppCenterCore.ChangeInformation? change_info, Cancellable? cancellable) throws GLib.Error { + if (yield run_operation ()) { + installed_packages.add (package); + return true; + } + return false; + } + + public async bool remove_package (AppCenterCore.Package package, AppCenterCore.ChangeInformation? change_info, Cancellable? cancellable) throws GLib.Error { + if (yield run_operation ()) { + installed_packages.remove (package); + return true; + } + return false; + } + + public async bool update_package (AppCenterCore.Package package, AppCenterCore.ChangeInformation? change_info, Cancellable? cancellable) throws GLib.Error { + return yield run_operation (); + } + + private async bool run_operation () throws GLib.Error { + ulong handler = 0; + handler = finish_operation.connect (() => { + disconnect (handler); + + Idle.add (() => { + run_operation.callback (); + return Source.REMOVE; + }); + }); + + yield; + + if (next_operation_error != null) { + var error = next_operation_error; + next_operation_error = null; + throw error; + } + return true; + } +} diff --git a/test/meson.build b/test/meson.build index 6705eb265..d8608ef44 100644 --- a/test/meson.build +++ b/test/meson.build @@ -2,13 +2,24 @@ core_tests = executable( meson.project_name() + '-core-tests', 'Core/CardUtils.vala', 'Core/Houston.vala', + 'Core/PackageOperations.vala', 'Core/Stripe.vala', 'CoreTest.vala', + 'MockBackend.vala', 'MockHttpClient.vala', + meson.project_source_root() + '/src/Core/AddonFilter.vala', + meson.project_source_root() + '/src/Core/Backend.vala', meson.project_source_root() + '/src/Core/CardUtils.vala', + meson.project_source_root() + '/src/Core/ChangeInformation.vala', + meson.project_source_root() + '/src/Core/FlatpakBackend.vala', meson.project_source_root() + '/src/Core/Houston.vala', meson.project_source_root() + '/src/Core/HttpClient.vala', + meson.project_source_root() + '/src/Core/Job.vala', + meson.project_source_root() + '/src/Core/Package.vala', + meson.project_source_root() + '/src/Core/SearchEngine.vala', meson.project_source_root() + '/src/Core/Stripe.vala', + meson.project_source_root() + '/src/Utils.vala', + config_file, dependencies: core_deps ) @@ -24,4 +35,4 @@ integration_tests = executable( test('AppCenter core tests', core_tests) if get_option('integration_tests') test('AppCenter integration tests', integration_tests) -endif \ No newline at end of file +endif