1
0
Fork 0
mirror of https://github.com/ansible-collections/community.general.git synced 2024-09-14 20:13:21 +02:00
community.general/tests/unit/plugins/modules/packaging/os/test_pacman.py
Felix Fontein 10ca62905f
pacman: improve docs, make sure that packages is always returned, deprecate update_cache behavior (#4330)
* Improve docs, make sure that packages is always returned, deprecate update_cache behavior.

* Add cache_updated return value.

* Revert "Add cache_updated return value."

This reverts commit 367297bb5c.

* Update tests/unit/plugins/modules/packaging/os/test_pacman.py

Co-authored-by: Jean Raby <jean@raby.sh>

Co-authored-by: Jean Raby <jean@raby.sh>
2022-03-11 06:54:05 +01:00

1018 lines
36 KiB
Python

# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import absolute_import, division, print_function
__metaclass__ = type
import sys
from ansible.module_utils import basic
from ansible_collections.community.general.tests.unit.compat import mock, unittest
from ansible_collections.community.general.tests.unit.compat.mock import patch
from ansible_collections.community.general.tests.unit.plugins.modules.utils import (
AnsibleExitJson,
AnsibleFailJson,
set_module_args,
exit_json,
fail_json,
)
from ansible_collections.community.general.plugins.modules.packaging.os import pacman
from ansible_collections.community.general.plugins.modules.packaging.os.pacman import (
Package,
VersionTuple,
)
import pytest
import json
def get_bin_path(self, arg, required=False):
"""Mock AnsibleModule.get_bin_path"""
return arg
# This inventory data is tightly coupled with the inventory test and the mock_valid_inventory fixture
valid_inventory = {
"installed_pkgs": {
"file": "5.41-1",
"filesystem": "2021.11.11-1",
"findutils": "4.8.0-1",
"gawk": "5.1.1-1",
"gettext": "0.21-1",
"grep": "3.7-1",
"gzip": "1.11-1",
"pacman": "6.0.1-2",
"pacman-mirrorlist": "20211114-1",
"sed": "4.8-1",
"sqlite": "3.36.0-1",
},
"installed_groups": {
"base-devel": set(["gawk", "grep", "file", "findutils", "pacman", "sed", "gzip", "gettext"])
},
"available_pkgs": {
"acl": "2.3.1-1",
"amd-ucode": "20211027.1d00989-1",
"archlinux-keyring": "20211028-1",
"argon2": "20190702-3",
"attr": "2.5.1-1",
"audit": "3.0.6-5",
"autoconf": "2.71-1",
"automake": "1.16.5-1",
"b43-fwcutter": "019-3",
"gawk": "5.1.1-1",
"grep": "3.7-1",
"sqlite": "3.37.0-1",
"sudo": "1.9.8.p2-3",
},
"available_groups": {
"base-devel": set(
[
"libtool",
"gawk",
"which",
"texinfo",
"fakeroot",
"grep",
"findutils",
"autoconf",
"gzip",
"pkgconf",
"flex",
"patch",
"groff",
"m4",
"bison",
"gcc",
"gettext",
"make",
"file",
"pacman",
"sed",
"automake",
"sudo",
"binutils",
]
),
"some-group": set(["libtool", "sudo", "binutils"]),
},
"upgradable_pkgs": {
"sqlite": VersionTuple(current="3.36.0-1", latest="3.37.0-1"),
},
}
empty_inventory = {
"installed_pkgs": {},
"available_pkgs": {},
"installed_groups": {},
"available_groups": {},
"upgradable_pkgs": {},
}
class TestPacman:
@pytest.fixture(autouse=True)
def run_command(self, mocker):
self.mock_run_command = mocker.patch.object(basic.AnsibleModule, "run_command", autospec=True)
@pytest.fixture
def mock_package_list(self, mocker):
return mocker.patch.object(pacman.Pacman, "package_list", autospec=True)
@pytest.fixture(autouse=True)
def common(self, mocker):
self.mock_module = mocker.patch.multiple(
basic.AnsibleModule,
exit_json=exit_json,
fail_json=fail_json,
get_bin_path=get_bin_path,
)
@pytest.fixture
def mock_empty_inventory(self, mocker):
inv = empty_inventory
return mocker.patch.object(pacman.Pacman, "_build_inventory", return_value=inv)
@pytest.fixture
def mock_valid_inventory(self, mocker):
return mocker.patch.object(pacman.Pacman, "_build_inventory", return_value=valid_inventory)
def test_fail_without_required_args(self):
with pytest.raises(AnsibleFailJson) as e:
set_module_args({})
pacman.main()
assert e.match(r"one of the following is required")
def test_success(self, mock_empty_inventory):
set_module_args({"update_cache": True}) # Simplest args to let init go through
P = pacman.Pacman(pacman.setup_module())
with pytest.raises(AnsibleExitJson) as e:
P.success()
def test_fail(self, mock_empty_inventory):
set_module_args({"update_cache": True})
P = pacman.Pacman(pacman.setup_module())
args = dict(
msg="msg", stdout="something", stderr="somethingelse", cmd=["command", "with", "args"], rc=1
)
with pytest.raises(AnsibleFailJson) as e:
P.fail(**args)
assert all(item in e.value.args[0] for item in args)
@pytest.mark.parametrize(
"expected, run_command_side_effect, raises",
[
(
# Regular run
valid_inventory,
[
[ # pacman --query
0,
"""file 5.41-1
filesystem 2021.11.11-1
findutils 4.8.0-1
gawk 5.1.1-1
gettext 0.21-1
grep 3.7-1
gzip 1.11-1
pacman 6.0.1-2
pacman-mirrorlist 20211114-1
sed 4.8-1
sqlite 3.36.0-1
""",
"",
],
( # pacman --query --group
0,
"""base-devel file
base-devel findutils
base-devel gawk
base-devel gettext
base-devel grep
base-devel gzip
base-devel pacman
base-devel sed
""",
"",
),
( # pacman --sync --list
0,
"""core acl 2.3.1-1 [installed]
core amd-ucode 20211027.1d00989-1
core archlinux-keyring 20211028-1 [installed]
core argon2 20190702-3 [installed]
core attr 2.5.1-1 [installed]
core audit 3.0.6-5 [installed: 3.0.6-2]
core autoconf 2.71-1
core automake 1.16.5-1
core b43-fwcutter 019-3
core gawk 5.1.1-1 [installed]
core grep 3.7-1 [installed]
core sqlite 3.37.0-1 [installed: 3.36.0-1]
code sudo 1.9.8.p2-3
""",
"",
),
( # pacman --sync --group --group
0,
"""base-devel autoconf
base-devel automake
base-devel binutils
base-devel bison
base-devel fakeroot
base-devel file
base-devel findutils
base-devel flex
base-devel gawk
base-devel gcc
base-devel gettext
base-devel grep
base-devel groff
base-devel gzip
base-devel libtool
base-devel m4
base-devel make
base-devel pacman
base-devel patch
base-devel pkgconf
base-devel sed
base-devel sudo
base-devel texinfo
base-devel which
some-group libtool
some-group sudo
some-group binutils
""",
"",
),
( # pacman --query --upgrades
0,
"""sqlite 3.36.0-1 -> 3.37.0-1
systemd 249.6-3 -> 249.7-2 [ignored]
""",
"",
),
],
None,
),
(
# All good, but call to --query --upgrades return 1. aka nothing to upgrade
# with a pacman warning
empty_inventory,
[
(0, "", ""),
(0, "", ""),
(0, "", ""),
(0, "", ""),
(
1,
"",
"warning: config file /etc/pacman.conf, line 34: directive 'TotalDownload' in section 'options' not recognized.",
),
],
None,
),
(
# failure
empty_inventory,
[
(0, "", ""),
(0, "", ""),
(0, "", ""),
(0, "", ""),
(
1,
"partial\npkg\\nlist",
"some warning",
),
],
AnsibleFailJson,
),
],
)
def test_build_inventory(self, expected, run_command_side_effect, raises):
self.mock_run_command.side_effect = run_command_side_effect
set_module_args({"update_cache": True})
if raises:
with pytest.raises(raises):
P = pacman.Pacman(pacman.setup_module())
P._build_inventory()
else:
P = pacman.Pacman(pacman.setup_module())
assert P._build_inventory() == expected
@pytest.mark.parametrize("check_mode_value", [True, False])
def test_upgrade_check_empty_inventory(self, mock_empty_inventory, check_mode_value):
set_module_args({"upgrade": True, "_ansible_check_mode": check_mode_value})
P = pacman.Pacman(pacman.setup_module())
with pytest.raises(AnsibleExitJson) as e:
P.run()
self.mock_run_command.call_count == 0
out = e.value.args[0]
assert "packages" not in out
assert not out["changed"]
assert "diff" not in out
def test_update_db_check(self, mock_empty_inventory):
set_module_args({"update_cache": True, "_ansible_check_mode": True})
P = pacman.Pacman(pacman.setup_module())
with pytest.raises(AnsibleExitJson) as e:
P.run()
self.mock_run_command.call_count == 0
out = e.value.args[0]
assert "packages" not in out
assert out["changed"]
@pytest.mark.parametrize(
"module_args,expected_call",
[
({}, ["pacman", "--sync", "--refresh"]),
({"force": True}, ["pacman", "--sync", "--refresh", "--refresh"]),
(
{"update_cache_extra_args": "--some-extra args"},
["pacman", "--sync", "--refresh", "--some-extra", "args"], # shlex test
),
(
{"force": True, "update_cache_extra_args": "--some-extra args"},
["pacman", "--sync", "--refresh", "--some-extra", "args", "--refresh"],
),
],
)
def test_update_db(self, mock_empty_inventory, module_args, expected_call):
args = {"update_cache": True}
args.update(module_args)
set_module_args(args)
self.mock_run_command.return_value = [0, "stdout", "stderr"]
with pytest.raises(AnsibleExitJson) as e:
P = pacman.Pacman(pacman.setup_module())
P.run()
self.mock_run_command.assert_called_with(mock.ANY, expected_call, check_rc=False)
out = e.value.args[0]
assert out["changed"]
@pytest.mark.parametrize(
"check_mode_value, run_command_data, upgrade_extra_args",
[
# just check
(True, None, None),
(
# for real
False,
{
"args": ["pacman", "--sync", "--sysupgrade", "--quiet", "--noconfirm"],
"return_value": [0, "stdout", "stderr"],
},
None,
),
(
# with extra args
False,
{
"args": [
"pacman",
"--sync",
"--sysupgrade",
"--quiet",
"--noconfirm",
"--some",
"value",
],
"return_value": [0, "stdout", "stderr"],
},
"--some value",
),
],
)
def test_upgrade(self, mock_valid_inventory, check_mode_value, run_command_data, upgrade_extra_args):
args = {"upgrade": True, "_ansible_check_mode": check_mode_value}
if upgrade_extra_args:
args["upgrade_extra_args"] = upgrade_extra_args
set_module_args(args)
if run_command_data and "return_value" in run_command_data:
self.mock_run_command.return_value = run_command_data["return_value"]
P = pacman.Pacman(pacman.setup_module())
with pytest.raises(AnsibleExitJson) as e:
P.run()
out = e.value.args[0]
if check_mode_value:
self.mock_run_command.call_count == 0
if run_command_data and "args" in run_command_data:
self.mock_run_command.assert_called_with(mock.ANY, run_command_data["args"], check_rc=False)
assert out["stdout"] == "stdout"
assert out["stderr"] == "stderr"
assert len(out["packages"]) == 1 and "sqlite" in out["packages"]
assert out["changed"]
assert out["diff"]["before"] and out["diff"]["after"]
def test_upgrade_fail(self, mock_valid_inventory):
set_module_args({"upgrade": True})
self.mock_run_command.return_value = [1, "stdout", "stderr"]
P = pacman.Pacman(pacman.setup_module())
with pytest.raises(AnsibleFailJson) as e:
P.run()
self.mock_run_command.call_count == 1
out = e.value.args[0]
assert out["failed"]
assert out["stdout"] == "stdout"
assert out["stderr"] == "stderr"
@pytest.mark.parametrize(
"state, pkg_names, expected, run_command_data, raises",
[
# regular packages, no resolving required
(
"present",
["acl", "attr"],
[Package(name="acl", source="acl"), Package(name="attr", source="attr")],
None,
None,
),
(
# group expansion
"present",
["acl", "some-group", "attr"],
[
Package(name="acl", source="acl"),
Package(name="binutils", source="binutils"),
Package(name="libtool", source="libtool"),
Package(name="sudo", source="sudo"),
Package(name="attr", source="attr"),
],
None,
None,
),
(
# <repo>/<pkgname> format -> call to pacman to resolve
"present",
["community/elixir"],
[Package(name="elixir", source="community/elixir")],
{
"calls": [
mock.call(
mock.ANY,
["pacman", "--sync", "--print-format", "%n", "community/elixir"],
check_rc=False,
)
],
"side_effect": [(0, "elixir", "")],
},
None,
),
(
# catch all -> call to pacman to resolve (--sync and --upgrade)
"present",
["somepackage-12.3-x86_64.pkg.tar.zst"],
[
Package(
name="somepackage",
source="somepackage-12.3-x86_64.pkg.tar.zst",
source_is_URL=True,
)
],
{
"calls": [
mock.call(
mock.ANY,
[
"pacman",
"--sync",
"--print-format",
"%n",
"somepackage-12.3-x86_64.pkg.tar.zst",
],
check_rc=False,
),
mock.call(
mock.ANY,
[
"pacman",
"--upgrade",
"--print-format",
"%n",
"somepackage-12.3-x86_64.pkg.tar.zst",
],
check_rc=False,
),
],
"side_effect": [(1, "", "nope"), (0, "somepackage", "")],
},
None,
),
(
# install a package that doesn't exist. call pacman twice and give up
"present",
["unknown-package"],
[],
{
# no call validation, since it will fail
"side_effect": [(1, "", "nope"), (1, "", "stillnope")],
},
AnsibleFailJson,
),
(
# Edge case: resolve a pkg that doesn't exist when trying to remove it (state == absent).
# will fallback to file + url format but not complain since it is already not there
# Can happen if a pkg is removed for the repos (or if a repo is disabled/removed)
"absent",
["unknown-package-to-remove"],
[],
{
"calls": [
mock.call(
mock.ANY,
["pacman", "--sync", "--print-format", "%n", "unknown-package-to-remove"],
check_rc=False,
),
mock.call(
mock.ANY,
["pacman", "--upgrade", "--print-format", "%n", "unknown-package-to-remove"],
check_rc=False,
),
],
"side_effect": [(1, "", "nope"), (1, "", "stillnope")],
},
None, # Doesn't fail
),
],
)
def test_package_list(
self, mock_valid_inventory, state, pkg_names, expected, run_command_data, raises
):
set_module_args({"name": pkg_names, "state": state})
P = pacman.Pacman(pacman.setup_module())
P.inventory = P._build_inventory()
if run_command_data:
self.mock_run_command.side_effect = run_command_data["side_effect"]
if raises:
with pytest.raises(raises):
P.package_list()
else:
assert sorted(P.package_list()) == sorted(expected)
if run_command_data:
assert self.mock_run_command.mock_calls == run_command_data["calls"]
@pytest.mark.parametrize("check_mode_value", [True, False])
@pytest.mark.parametrize(
"name, state, package_list",
[
(["already-absent"], "absent", [Package("already-absent", "already-absent")]),
(["grep"], "present", [Package("grep", "grep")]),
],
)
def test_op_packages_nothing_to_do(
self, mock_valid_inventory, mock_package_list, check_mode_value, name, state, package_list
):
set_module_args({"name": name, "state": state, "_ansible_check_mode": check_mode_value})
mock_package_list.return_value = package_list
P = pacman.Pacman(pacman.setup_module())
with pytest.raises(AnsibleExitJson) as e:
P.run()
out = e.value.args[0]
assert not out["changed"]
assert "packages" in out
assert "diff" not in out
self.mock_run_command.call_count == 0
@pytest.mark.parametrize(
"module_args, expected_packages, package_list_out, run_command_data, raises",
[
(
# remove pkg: Check mode -- call to print format but that's it
{"_ansible_check_mode": True, "name": ["grep"], "state": "absent"},
["grep-version"],
[Package("grep", "grep")],
{
"calls": [
mock.call(
mock.ANY,
[
"pacman",
"--remove",
"--noconfirm",
"--noprogressbar",
"--print-format",
"%n-%v",
"grep",
],
check_rc=False,
),
],
"side_effect": [(0, "grep-version", "")],
},
AnsibleExitJson,
),
(
# remove pkg for real now -- with 2 packages
{"name": ["grep", "gawk"], "state": "absent"},
["grep-version", "gawk-anotherversion"],
[Package("grep", "grep"), Package("gawk", "gawk")],
{
"calls": [
mock.call(
mock.ANY,
[
"pacman",
"--remove",
"--noconfirm",
"--noprogressbar",
"--print-format",
"%n-%v",
"grep",
"gawk",
],
check_rc=False,
),
mock.call(
mock.ANY,
["pacman", "--remove", "--noconfirm", "--noprogressbar", "grep", "gawk"],
check_rc=False,
),
],
"side_effect": [
(0, "grep-version\ngawk-anotherversion", ""),
(0, "stdout", "stderr"),
],
},
AnsibleExitJson,
),
(
# remove pkg force + extra_args
{
"name": ["grep"],
"state": "absent",
"force": True,
"extra_args": "--some --extra arg",
},
["grep-version"],
[Package("grep", "grep")],
{
"calls": [
mock.call(
mock.ANY,
[
"pacman",
"--remove",
"--noconfirm",
"--noprogressbar",
"--some",
"--extra",
"arg",
"--nodeps",
"--nodeps",
"--print-format",
"%n-%v",
"grep",
],
check_rc=False,
),
mock.call(
mock.ANY,
[
"pacman",
"--remove",
"--noconfirm",
"--noprogressbar",
"--some",
"--extra",
"arg",
"--nodeps",
"--nodeps",
"grep",
],
check_rc=False,
),
],
"side_effect": [
(0, "grep-version", ""),
(0, "stdout", "stderr"),
],
},
AnsibleExitJson,
),
(
# remove pkg -- Failure to list
{"name": ["grep"], "state": "absent"},
["grep-3.7-1"],
[Package("grep", "grep")],
{
"calls": [
mock.call(
mock.ANY,
[
"pacman",
"--remove",
"--noconfirm",
"--noprogressbar",
"--print-format",
"%n-%v",
"grep",
],
check_rc=False,
)
],
"side_effect": [
(1, "stdout", "stderr"),
],
},
AnsibleFailJson,
),
(
# remove pkg -- Failure to remove
{"name": ["grep"], "state": "absent"},
["grep-3.7-1"],
[Package("grep", "grep")],
{
"calls": [
mock.call(
mock.ANY,
[
"pacman",
"--remove",
"--noconfirm",
"--noprogressbar",
"--print-format",
"%n-%v",
"grep",
],
check_rc=False,
),
mock.call(
mock.ANY,
["pacman", "--remove", "--noconfirm", "--noprogressbar", "grep"],
check_rc=False,
),
],
"side_effect": [
(0, "grep", ""),
(1, "stdout", "stderr"),
],
},
AnsibleFailJson,
),
(
# install pkg: Check mode
{"_ansible_check_mode": True, "name": ["sudo"], "state": "present"},
["sudo"],
[Package("sudo", "sudo")],
{
"calls": [
mock.call(
mock.ANY,
[
"pacman",
"--noconfirm",
"--noprogressbar",
"--needed",
"--sync",
"--print-format",
"%n %v",
"sudo",
],
check_rc=False,
),
],
"side_effect": [(0, "sudo version", "")],
},
AnsibleExitJson,
),
(
# Install pkgs: one regular, one already installed, one file URL and one https URL
{
"name": [
"sudo",
"grep",
"./somepackage-12.3-x86_64.pkg.tar.zst",
"http://example.com/otherpkg-1.2-x86_64.pkg.tar.zst",
],
"state": "present",
},
["sudo", "somepackage", "otherpkg"],
[
Package("sudo", "sudo"),
Package("grep", "grep"),
Package("somepackage", "./somepackage-12.3-x86_64.pkg.tar.zst", source_is_URL=True),
Package(
"otherpkg",
"http://example.com/otherpkg-1.2-x86_64.pkg.tar.zst",
source_is_URL=True,
),
],
{
"calls": [
mock.call(
mock.ANY,
[
"pacman",
"--noconfirm",
"--noprogressbar",
"--needed",
"--sync",
"--print-format",
"%n %v",
"sudo",
],
check_rc=False,
),
mock.call(
mock.ANY,
[
"pacman",
"--noconfirm",
"--noprogressbar",
"--needed",
"--upgrade",
"--print-format",
"%n %v",
"./somepackage-12.3-x86_64.pkg.tar.zst",
"http://example.com/otherpkg-1.2-x86_64.pkg.tar.zst",
],
check_rc=False,
),
mock.call(
mock.ANY,
[
"pacman",
"--noconfirm",
"--noprogressbar",
"--needed",
"--sync",
"sudo",
],
check_rc=False,
),
mock.call(
mock.ANY,
[
"pacman",
"--noconfirm",
"--noprogressbar",
"--needed",
"--upgrade",
"./somepackage-12.3-x86_64.pkg.tar.zst",
"http://example.com/otherpkg-1.2-x86_64.pkg.tar.zst",
],
check_rc=False,
),
],
"side_effect": [
(0, "sudo version", ""),
(0, "somepackage 12.3\notherpkg 1.2", ""),
(0, "", ""),
(0, "", ""),
],
},
AnsibleExitJson,
),
(
# install pkg, extra_args
{"name": ["sudo"], "state": "present", "extra_args": "--some --thing else"},
["sudo"],
[Package("sudo", "sudo")],
{
"calls": [
mock.call(
mock.ANY,
[
"pacman",
"--noconfirm",
"--noprogressbar",
"--needed",
"--some",
"--thing",
"else",
"--sync",
"--print-format",
"%n %v",
"sudo",
],
check_rc=False,
),
mock.call(
mock.ANY,
[
"pacman",
"--noconfirm",
"--noprogressbar",
"--needed",
"--some",
"--thing",
"else",
"--sync",
"sudo",
],
check_rc=False,
),
],
"side_effect": [(0, "sudo version", ""), (0, "", "")],
},
AnsibleExitJson,
),
(
# latest pkg: Check mode
{"_ansible_check_mode": True, "name": ["sqlite"], "state": "latest"},
["sqlite"],
[Package("sqlite", "sqlite")],
{
"calls": [
mock.call(
mock.ANY,
[
"pacman",
"--noconfirm",
"--noprogressbar",
"--needed",
"--sync",
"--print-format",
"%n %v",
"sqlite",
],
check_rc=False,
),
],
"side_effect": [(0, "sqlite new-version", "")],
},
AnsibleExitJson,
),
(
# latest pkg -- one already latest
{"name": ["sqlite", "grep"], "state": "latest"},
["sqlite"],
[Package("sqlite", "sqlite")],
{
"calls": [
mock.call(
mock.ANY,
[
"pacman",
"--noconfirm",
"--noprogressbar",
"--needed",
"--sync",
"--print-format",
"%n %v",
"sqlite",
],
check_rc=False,
),
mock.call(
mock.ANY,
[
"pacman",
"--noconfirm",
"--noprogressbar",
"--needed",
"--sync",
"sqlite",
],
check_rc=False,
),
],
"side_effect": [(0, "sqlite new-version", ""), (0, "", "")],
},
AnsibleExitJson,
),
],
)
def test_op_packages(
self,
mock_valid_inventory,
mock_package_list,
module_args,
expected_packages,
package_list_out,
run_command_data,
raises,
):
set_module_args(module_args)
self.mock_run_command.side_effect = run_command_data["side_effect"]
mock_package_list.return_value = package_list_out
P = pacman.Pacman(pacman.setup_module())
with pytest.raises(raises) as e:
P.run()
out = e.value.args[0]
assert self.mock_run_command.mock_calls == run_command_data["calls"]
if raises == AnsibleExitJson:
assert out["packages"] == expected_packages
assert out["changed"]
assert "packages" in out
assert "diff" in out
else:
assert out["stdout"] == "stdout"
assert out["stderr"] == "stderr"