package: Don't uninstall packages that are in use by other apps

Closes: #2262.

Tests:

- Unit tests work

- Uninstall of email app succeeds without uninstalling openssl package.

Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org>
Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
This commit is contained in:
Sunil Mohan Adapa 2023-01-02 15:36:19 -08:00 committed by James Valleroy
parent bc09cd42bb
commit 16cb8ee021
No known key found for this signature in database
GPG Key ID: 77C0C75E7B650808
2 changed files with 83 additions and 3 deletions

View File

@ -12,7 +12,7 @@ from django.utils.translation import gettext as _
from django.utils.translation import gettext_lazy from django.utils.translation import gettext_lazy
import plinth.privileged.packages as privileged import plinth.privileged.packages as privileged
from plinth import app from plinth import app as app_module
from plinth.errors import MissingPackageError from plinth.errors import MissingPackageError
from plinth.utils import format_lazy from plinth.utils import format_lazy
@ -94,7 +94,7 @@ class PackageOr(PackageExpression):
return self.package2.actual() return self.package2.actual()
class Packages(app.FollowerComponent): class Packages(app_module.FollowerComponent):
"""Component to manage the packages of an app. """Component to manage the packages of an app.
This component is responsible for installation, upgrades and uninstallation This component is responsible for installation, upgrades and uninstallation
@ -182,7 +182,23 @@ class Packages(app.FollowerComponent):
def uninstall(self): def uninstall(self):
"""Uninstall and purge the packages.""" """Uninstall and purge the packages."""
uninstall(self.get_actual_packages()) packages = self.get_actual_packages()
packages_set = set(packages)
for app in app_module.App.list():
# uninstall() will be called on Packages of this app separately
# for uninstalling this app.
if app == self.app:
continue
if app.get_setup_state() == app_module.App.SetupState.NEEDS_SETUP:
continue
# Remove packages used by other installed apps
for component in app.get_components_of_type(Packages):
packages_set -= set(component.get_actual_packages())
# Preserve order of packages for ease of testing
uninstall([package for package in packages if package in packages_set])
def diagnose(self): def diagnose(self):
"""Run diagnostics and return results.""" """Run diagnostics and return results."""

View File

@ -13,6 +13,12 @@ from plinth.errors import MissingPackageError
from plinth.package import Package, Packages, packages_installed from plinth.package import Package, Packages, packages_installed
@pytest.fixture(autouse=True)
def fixture_clean_apps():
"""Fixture to ensure clean set of global apps."""
App._all_apps = {}
class TestPackageExpressions(unittest.TestCase): class TestPackageExpressions(unittest.TestCase):
def test_package(self): def test_package(self):
@ -159,6 +165,64 @@ def test_packages_uninstall(uninstall):
uninstall.assert_has_calls([call(['python3', 'bash'])]) uninstall.assert_has_calls([call(['python3', 'bash'])])
@patch('plinth.package.uninstall')
@patch('apt.Cache')
def test_packages_uninstall_exclusion(cache, uninstall):
"""Test excluding packages from other installed apps when uninstalling."""
cache.return_value = {
'package11': Mock(candidate=Mock(version='2.0', is_installed=True)),
'package12': Mock(candidate=Mock(version='3.0', is_installed=False)),
'package2': Mock(candidate=Mock(version='4.0', is_installed=True)),
'package3': Mock(candidate=Mock(version='5.0', is_installed=True)),
}
class TestApp1(App):
"""Test app."""
app_id = 'test-app1'
def __init__(self):
super().__init__()
component = Packages('test-component11',
['package11', 'package2', 'package3'])
self.add(component)
component = Packages('test-component12',
['package12', 'package2', 'package3'])
self.add(component)
class TestApp2(App):
"""Test app."""
app_id = 'test-app2'
def __init__(self):
super().__init__()
component = Packages('test-component2', ['package2'])
self.add(component)
def get_setup_state(self):
return App.SetupState.UP_TO_DATE
class TestApp3(App):
"""Test app."""
app_id = 'test-app3'
def __init__(self):
super().__init__()
component = Packages('test-component3', ['package3'])
self.add(component)
def get_setup_state(self):
return App.SetupState.NEEDS_SETUP
app1 = TestApp1()
TestApp2()
TestApp3()
app1.uninstall()
uninstall.assert_has_calls(
[call(['package11', 'package3']),
call(['package12', 'package3'])])
@patch('apt.Cache') @patch('apt.Cache')
def test_diagnose(cache): def test_diagnose(cache):
"""Test checking for latest version of the package.""" """Test checking for latest version of the package."""