mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-01-21 07:55:00 +00:00
- Ensure that packages that are not installable to negative priority are not shown as available. Tests: - Set priority of an available package to less than 0. This package will be shown as not-available in the app install page. - Normal apps are shown as available and can be installed as usual. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: James Valleroy <jvalleroy@mailbox.org>
438 lines
16 KiB
Python
438 lines
16 KiB
Python
# SPDX-License-Identifier: AGPL-3.0-or-later
|
|
"""
|
|
Test module for package module.
|
|
"""
|
|
|
|
import time
|
|
import unittest
|
|
from unittest.mock import Mock, call, patch
|
|
|
|
import pytest
|
|
|
|
from plinth.app import App
|
|
from plinth.diagnostic_check import DiagnosticCheck, Result
|
|
from plinth.errors import MissingPackageError
|
|
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):
|
|
|
|
def test_package(self):
|
|
"""Test resolving package names."""
|
|
package = Package('python3')
|
|
assert package.possible() == ['python3']
|
|
assert package.actual() == 'python3'
|
|
|
|
package = Package('unknown-package')
|
|
assert package.possible() == ['unknown-package']
|
|
self.assertRaises(MissingPackageError, package.actual)
|
|
|
|
def test_package_or_expression(self):
|
|
"""Test resolving package OR expressions."""
|
|
expression = Package('python3') | Package('unknown-package')
|
|
assert expression.possible() == ['python3', 'unknown-package']
|
|
assert expression.actual() == 'python3'
|
|
|
|
expression = Package('unknown-package') | Package('python3')
|
|
assert expression.possible() == ['unknown-package', 'python3']
|
|
assert expression.actual() == 'python3'
|
|
|
|
# When both packages are available, prefer the first.
|
|
expression = Package('bash') | Package('dash')
|
|
assert expression.possible() == ['bash', 'dash']
|
|
assert expression.actual() == 'bash'
|
|
|
|
expression = Package('unknown-package') | Package(
|
|
'another-unknown-package')
|
|
assert expression.possible() == [
|
|
'unknown-package', 'another-unknown-package'
|
|
]
|
|
self.assertRaises(MissingPackageError, expression.actual)
|
|
|
|
|
|
def test_packages_init():
|
|
"""Test initialization of packages component."""
|
|
component = Packages('test-component', ['foo', 'bar'])
|
|
assert component.possible_packages == ['foo', 'bar']
|
|
assert component.component_id == 'test-component'
|
|
assert not component.skip_recommends
|
|
assert component.conflicts == []
|
|
assert component.conflicts_action is None
|
|
assert not component.rerun_setup_on_upgrade
|
|
|
|
with pytest.raises(ValueError):
|
|
Packages(None, [])
|
|
|
|
component = Packages('test-component', [], skip_recommends=True,
|
|
conflicts=['conflict1', 'conflict2'],
|
|
conflicts_action=Packages.ConflictsAction.IGNORE,
|
|
rerun_setup_on_upgrade=True)
|
|
assert component.possible_packages == []
|
|
assert component.skip_recommends
|
|
assert component.conflicts == ['conflict1', 'conflict2']
|
|
assert component.conflicts_action == Packages.ConflictsAction.IGNORE
|
|
assert component.rerun_setup_on_upgrade
|
|
|
|
|
|
@patch('apt.Cache')
|
|
def test_packages_get_actual_packages(cache):
|
|
"""Test resolving of package expressions to actual packages."""
|
|
cache.return_value = {
|
|
'python3': Mock(name='python3', candidate='1.0'),
|
|
'janus': Mock(name='janus', candidate=None)
|
|
}
|
|
|
|
component = Packages('test-component', ['python3'])
|
|
assert component.get_actual_packages() == ['python3']
|
|
|
|
component = Packages('test-component', ['unknown-package'])
|
|
with pytest.raises(MissingPackageError):
|
|
component.get_actual_packages()
|
|
|
|
component = Packages('test-component',
|
|
[Package('unknown-package') | Package('python3')])
|
|
assert component.get_actual_packages() == ['python3']
|
|
|
|
component = Packages('test-component', [], skip_recommends=True,
|
|
conflicts=['conflict1', 'conflict2'],
|
|
conflicts_action=Packages.ConflictsAction.IGNORE)
|
|
assert component.get_actual_packages() == []
|
|
|
|
component = Packages('test-component', ['janus'])
|
|
with pytest.raises(MissingPackageError):
|
|
component.get_actual_packages()
|
|
|
|
|
|
@patch('plinth.package.install')
|
|
def test_packages_setup(install):
|
|
"""Test setting up packages component."""
|
|
|
|
class TestApp(App):
|
|
"""Test app"""
|
|
app_id = 'test-app'
|
|
|
|
component = Packages('test-component', ['python3', 'bash'])
|
|
app = TestApp()
|
|
app.add(component)
|
|
app.setup(old_version=3)
|
|
install.assert_has_calls(
|
|
[call(['python3', 'bash'], skip_recommends=False)])
|
|
|
|
component = Packages('test-component', ['bash', 'perl'],
|
|
skip_recommends=True)
|
|
app = TestApp()
|
|
app.add(component)
|
|
app.setup(old_version=3)
|
|
install.assert_has_calls([call(['bash', 'perl'], skip_recommends=True)])
|
|
|
|
component = Packages('test-component',
|
|
[Package('python3') | Package('unknown-package')])
|
|
app = TestApp()
|
|
app.add(component)
|
|
app.setup(old_version=3)
|
|
install.assert_has_calls([call(['python3'], skip_recommends=False)])
|
|
|
|
|
|
@patch('plinth.package.packages_installed')
|
|
@patch('plinth.package.uninstall')
|
|
@patch('plinth.package.install')
|
|
def test_packages_setup_with_conflicts(install, uninstall, packages_installed):
|
|
"""Test setting up packages with conflicts."""
|
|
packages_installed.return_value = ['exim4-base']
|
|
|
|
component = Packages('test-component', ['bash'], conflicts=['exim4-base'],
|
|
conflicts_action=Packages.ConflictsAction.REMOVE)
|
|
component.setup(old_version=0)
|
|
uninstall.assert_has_calls([call(['exim4-base'], purge=False)])
|
|
install.assert_has_calls([call(['bash'], skip_recommends=False)])
|
|
|
|
uninstall.reset_mock()
|
|
install.reset_mock()
|
|
component = Packages('test-component', ['bash'], conflicts=['exim4-base'])
|
|
component.setup(old_version=0)
|
|
uninstall.assert_not_called()
|
|
install.assert_has_calls([call(['bash'], skip_recommends=False)])
|
|
|
|
uninstall.reset_mock()
|
|
install.reset_mock()
|
|
component = Packages('test-component', ['bash'],
|
|
conflicts=['exim4-base', 'not-installed-package'],
|
|
conflicts_action=Packages.ConflictsAction.IGNORE)
|
|
component.setup(old_version=0)
|
|
uninstall.assert_not_called()
|
|
install.assert_has_calls([call(['bash'], skip_recommends=False)])
|
|
|
|
|
|
@patch('plinth.package.refresh_package_lists')
|
|
@patch('plinth.package.uninstall')
|
|
def test_packages_uninstall(uninstall, _refresh_package_lists):
|
|
"""Test uninstalling packages component."""
|
|
|
|
class TestApp(App):
|
|
"""Test app"""
|
|
app_id = 'test-app'
|
|
|
|
component = Packages('test-component', ['python3', 'bash'])
|
|
app = TestApp()
|
|
app.add(component)
|
|
app.uninstall()
|
|
uninstall.assert_has_calls([call(['python3', 'bash'], purge=True)])
|
|
|
|
|
|
@patch('plinth.package.refresh_package_lists')
|
|
@patch('plinth.package.uninstall')
|
|
@patch('apt.Cache')
|
|
def test_packages_uninstall_exclusion(cache, uninstall,
|
|
_refresh_package_lists):
|
|
"""Test excluding packages from other installed apps when uninstalling."""
|
|
|
|
def _get_mock_package(installed_version='1.0', dependencies=None,
|
|
recommends=None):
|
|
mock_dependencies = []
|
|
for or_dependencies in (dependencies or []):
|
|
mock_or_dependency = Mock(or_dependencies=[])
|
|
mock_dependencies.append(mock_or_dependency)
|
|
for dependency in or_dependencies:
|
|
mock = Mock()
|
|
mock.name = dependency
|
|
mock_or_dependency.or_dependencies.append(mock)
|
|
|
|
mock_recommends = []
|
|
for or_dependencies in (recommends or []):
|
|
mock_or_dependency = Mock(or_dependencies=[])
|
|
mock_recommends.append(mock_or_dependency)
|
|
for dependency in or_dependencies:
|
|
mock = Mock()
|
|
mock.name = dependency
|
|
mock_or_dependency.or_dependencies.append(mock)
|
|
|
|
mock = Mock(
|
|
version=installed_version or '1.0',
|
|
is_installed=bool(installed_version),
|
|
installed=Mock(dependencies=mock_dependencies,
|
|
recommends=mock_recommends))
|
|
return mock
|
|
|
|
package2 = _get_mock_package('4.0', [['dep1', 'dep2'], ['dep3'], ['dep4']],
|
|
[['dep5']])
|
|
cache.return_value = {
|
|
'package11': _get_mock_package('2.0'),
|
|
'package12': _get_mock_package(None),
|
|
'package2': package2,
|
|
'package3': _get_mock_package('5.0', ['unknown-dep1']),
|
|
'dep1': _get_mock_package('6.0'),
|
|
'dep2': _get_mock_package('6.1'),
|
|
'dep3': _get_mock_package('6.2'),
|
|
'dep4': _get_mock_package(None),
|
|
'dep5': _get_mock_package('6.4'),
|
|
'dep6': _get_mock_package('6.5'),
|
|
}
|
|
|
|
class TestApp1(App):
|
|
"""Test app."""
|
|
app_id = 'test-app1'
|
|
|
|
def __init__(self):
|
|
super().__init__()
|
|
component = Packages('test-component11', [
|
|
'package11', 'package2', 'package3', 'dep1', 'dep2', 'dep3',
|
|
'dep4', 'dep6'
|
|
])
|
|
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(['package12', 'package3'], purge=True),
|
|
call(['package11', 'package3', 'dep6'], purge=True)
|
|
])
|
|
|
|
|
|
@patch('apt.Cache')
|
|
def test_diagnose(cache):
|
|
"""Test checking for latest version of the package."""
|
|
cache.return_value = {
|
|
'package2': Mock(candidate=Mock(version='2.0', is_installed=True)),
|
|
'package3': Mock(candidate=Mock(version='3.0', is_installed=False)),
|
|
'package7': Mock(candidate=Mock(version='4.0', is_installed=True)),
|
|
}
|
|
component = Packages('test-component', [
|
|
'package1', 'package2', 'package3',
|
|
Package('package4') | Package('package5'),
|
|
Package('package6') | Package('package7')
|
|
])
|
|
results = component.diagnose()
|
|
assert results == [
|
|
DiagnosticCheck(
|
|
'package-available-package1',
|
|
'Package {package_expression} is not available for install',
|
|
Result.FAILED, {'package_expression': 'package1'},
|
|
'test-component'),
|
|
DiagnosticCheck(
|
|
'package-latest-package2',
|
|
'Package {package_name} is the latest version ({latest_version})',
|
|
Result.PASSED, {
|
|
'package_name': 'package2',
|
|
'latest_version': '2.0'
|
|
}, 'test-component'),
|
|
DiagnosticCheck(
|
|
'package-latest-package3',
|
|
'Package {package_name} is the latest version ({latest_version})',
|
|
Result.WARNING, {
|
|
'package_name': 'package3',
|
|
'latest_version': '3.0'
|
|
}, 'test-component'),
|
|
DiagnosticCheck(
|
|
'package-available-package4 | package5',
|
|
'Package {package_expression} is not available for install',
|
|
Result.FAILED, {'package_expression': 'package4 | package5'},
|
|
'test-component'),
|
|
DiagnosticCheck(
|
|
'package-latest-package7',
|
|
'Package {package_name} is the latest version ({latest_version})',
|
|
Result.PASSED, {
|
|
'package_name': 'package7',
|
|
'latest_version': '4.0'
|
|
}, 'test-component'),
|
|
]
|
|
|
|
|
|
@patch('plinth.package.packages_installed')
|
|
def test_packages_find_conflicts(packages_installed_):
|
|
"""Test finding conflicts."""
|
|
packages_installed_.return_value = []
|
|
component = Packages('test-component', ['package3', 'package4'])
|
|
assert component.find_conflicts() is None
|
|
|
|
packages_installed_.return_value = []
|
|
component = Packages('test-component', ['package3', 'package4'],
|
|
conflicts=['package5', 'package6'],
|
|
conflicts_action=Packages.ConflictsAction.IGNORE)
|
|
assert component.find_conflicts() == []
|
|
|
|
packages_installed_.return_value = ['package1', 'package2']
|
|
component = Packages('test-component', ['package3', 'package4'],
|
|
conflicts=['package1', 'package2'],
|
|
conflicts_action=Packages.ConflictsAction.IGNORE)
|
|
assert component.find_conflicts() == ['package1', 'package2']
|
|
|
|
|
|
@patch('plinth.package.refresh_package_lists')
|
|
@patch('apt.Cache')
|
|
@patch('pathlib.Path')
|
|
def test_packages_is_available(path_class, cache, refresh_package_lists):
|
|
"""Test checking for available packages."""
|
|
package_cache = Mock()
|
|
sources_dir = Mock()
|
|
sources_dir.glob.return_value = []
|
|
sources_list = Mock()
|
|
|
|
def path_side_effect(path):
|
|
return {
|
|
'/var/cache/apt/pkgcache.bin': package_cache,
|
|
'/etc/apt/sources.list.d': sources_dir,
|
|
'/etc/apt/sources.list': sources_list
|
|
}[path]
|
|
|
|
def get_cache_packages(packages):
|
|
return {
|
|
package: Mock(name=package, candidate='1.0')
|
|
for package in packages
|
|
}
|
|
|
|
path_class.side_effect = path_side_effect
|
|
|
|
# Packages found in cache
|
|
component = Packages('test-component', ['package1', 'package2'])
|
|
cache.return_value = get_cache_packages(['package1', 'package2'])
|
|
assert component.is_available()
|
|
path_class.assert_not_called()
|
|
refresh_package_lists.assert_not_called()
|
|
|
|
# Packages not found, cache exists and is fresh
|
|
cache.return_value = get_cache_packages(['package1'])
|
|
package_cache.exists.return_value = True
|
|
package_cache.stat.return_value.st_mtime = time.time()
|
|
sources_list.stat.return_value.st_mtime = time.time() - 100
|
|
assert not component.is_available()
|
|
refresh_package_lists.assert_not_called()
|
|
|
|
# Packages not found, cache does not exist
|
|
cache.return_value = get_cache_packages(['package1'])
|
|
package_cache.exists.return_value = False
|
|
assert not component.is_available()
|
|
refresh_package_lists.assert_called_once()
|
|
|
|
# Packages not found, cache is stale because it older than 1 hour
|
|
cache.return_value = get_cache_packages(['package1'])
|
|
refresh_package_lists.reset_mock()
|
|
package_cache.exists.return_value = True
|
|
package_cache.stat.return_value.st_mtime = time.time() - 7200
|
|
assert not component.is_available()
|
|
refresh_package_lists.assert_called_once()
|
|
|
|
# Packages not found, cache is stale because it older than sources.list
|
|
cache.return_value = get_cache_packages(['package1'])
|
|
refresh_package_lists.reset_mock()
|
|
package_cache.exists.return_value = True
|
|
package_cache.stat.return_value.st_mtime = time.time() - 100
|
|
sources_list.stat.return_value.st_mtime = time.time()
|
|
assert not component.is_available()
|
|
refresh_package_lists.assert_called_once()
|
|
|
|
# Packages not found, cache is stale, but packages found after refresh
|
|
cache.side_effect = [
|
|
get_cache_packages(['package1']),
|
|
get_cache_packages(['package1', 'package2'])
|
|
]
|
|
refresh_package_lists.reset_mock()
|
|
assert component.is_available()
|
|
|
|
|
|
def test_packages_installed():
|
|
"""Test packages_installed()."""
|
|
# list as input
|
|
assert len(packages_installed([])) == 0
|
|
assert len(packages_installed(['unknown-package'])) == 0
|
|
assert len(packages_installed(['python3'])) == 1
|
|
# tuples as input
|
|
assert len(packages_installed(())) == 0
|
|
assert len(packages_installed(('unknown-package', ))) == 0
|
|
assert len(packages_installed(('python3', ))) == 1
|