mirror of
https://github.com/freedombox/FreedomBox.git
synced 2026-02-04 08:13:38 +00:00
Fixes: #1358 - Refresh the apt cache if required packages for an app are not found and if the cache is more than 1 hour old (or non-existent). - If required packages are found, don't refresh the package cache even if the cache is outdated. This is because the check operation could lead to many minutes of waiting before app can be installed. Tests: - Remove /var/lib/apt/lists/* and /var/cache/apt/pkgcache.bin. Visit an app setup page. apt cache is updated and it take a while to check that the app is available. App is shown as available. If page is refreshed, this time, the cache is not updated. - Set the modification of /var/cache/apt/pkgcache.bin file to more than 2 hours ago with 'touch -d "2 hours ago" /var/cache/apt/pkgcache.bin'. Then refreshing the page will not refresh the cache. - Repeat test with an app that is not available such as Janus. Again apt cache is refreshed. App is shown as not available. On refresh, the cache is not updated. - Set the modification of /var/cache/apt/pkgcache.bin file to more than 2 hours ago with 'touch -d "2 hours ago" /var/cache/apt/pkgcache.bin'. Then refreshing the page will not refresh the cache. Signed-off-by: Sunil Mohan Adapa <sunil@medhas.org> Reviewed-by: Joseph Nuthalapati <njoseph@riseup.net> - Remove redundant if condition in setup.html template - Use JavaScript fetch() API instead of XMLHTTPRequest class - Update a comment in test_package.py Signed-off-by: Joseph Nuthalapati <njoseph@riseup.net>
394 lines
14 KiB
Python
394 lines
14 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
|
|
|
|
|
|
def test_packages_get_actual_packages():
|
|
"""Test resolving of package expressions to actual packages."""
|
|
component = Packages('test-component', ['python3'])
|
|
assert component.get_actual_packages() == ['python3']
|
|
|
|
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() == []
|
|
|
|
|
|
@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."""
|
|
path = Mock()
|
|
path_class.return_value = path
|
|
|
|
# Packages found in cache
|
|
component = Packages('test-component', ['package1', 'package2'])
|
|
cache.return_value = ['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 = ['package1']
|
|
path.exists.return_value = True
|
|
path.stat.return_value.st_mtime = time.time()
|
|
assert not component.is_available()
|
|
refresh_package_lists.assert_not_called()
|
|
|
|
# Packages not found, cache does not exist
|
|
cache.return_value = ['package1']
|
|
path.exists.return_value = False
|
|
assert not component.is_available()
|
|
refresh_package_lists.assert_called_once()
|
|
|
|
# Packages not found, cache is stale
|
|
cache.return_value = ['package1']
|
|
refresh_package_lists.reset_mock()
|
|
path.exists.return_value = True
|
|
path.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, but packages found after refresh
|
|
cache.side_effect = [['package1'], ['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
|