diff --git a/lib/ansible/module_utils/facts/system/local.py b/lib/ansible/module_utils/facts/system/local.py
index 3d656f5a345..ac887b3599f 100644
--- a/lib/ansible/module_utils/facts/system/local.py
+++ b/lib/ansible/module_utils/facts/system/local.py
@@ -1,17 +1,5 @@
-# This file is part of Ansible
-#
-# Ansible is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Ansible is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Ansible. If not, see .
+# Copyright: Contributors to the Ansible project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
diff --git a/lib/ansible/module_utils/facts/system/platform.py b/lib/ansible/module_utils/facts/system/platform.py
index 94819861b4b..c3c131b1c90 100644
--- a/lib/ansible/module_utils/facts/system/platform.py
+++ b/lib/ansible/module_utils/facts/system/platform.py
@@ -1,17 +1,5 @@
-# This file is part of Ansible
-#
-# Ansible is free software: you can redistribute it and/or modify
-# it under the terms of the GNU General Public License as published by
-# the Free Software Foundation, either version 3 of the License, or
-# (at your option) any later version.
-#
-# Ansible is distributed in the hope that it will be useful,
-# but WITHOUT ANY WARRANTY; without even the implied warranty of
-# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
-# GNU General Public License for more details.
-#
-# You should have received a copy of the GNU General Public License
-# along with Ansible. If not, see .
+# Copyright: Contributors to the Ansible project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
from __future__ import annotations
@@ -84,9 +72,10 @@ class PlatformFactCollector(BaseFactCollector):
platform_facts['architecture'] = data[0]
else:
bootinfo_bin = module.get_bin_path('bootinfo')
- rc, out, err = module.run_command([bootinfo_bin, '-p'])
- data = out.splitlines()
- platform_facts['architecture'] = data[0]
+ if bootinfo_bin is not None:
+ rc, out, err = module.run_command([bootinfo_bin, '-p'])
+ data = out.splitlines()
+ platform_facts['architecture'] = data[0]
elif platform_facts['system'] == 'OpenBSD':
platform_facts['architecture'] = platform.uname()[5]
diff --git a/test/units/module_utils/facts/system/test_chroot.py b/test/units/module_utils/facts/system/test_chroot.py
index 0baa78fd249..37d9a06ce5f 100644
--- a/test/units/module_utils/facts/system/test_chroot.py
+++ b/test/units/module_utils/facts/system/test_chroot.py
@@ -3,16 +3,12 @@
from __future__ import annotations
-import stat
-from collections import namedtuple
+from os import stat_result
import pytest
-
from ansible.module_utils.facts.system.chroot import ChrootFactCollector
-MOCKSTAT = namedtuple("mock_stat", ["st_dev", "st_ino", "st_size", "st_mtime"])
-
class TestChrootFacts:
def test_debian_chroot(self, monkeypatch):
@@ -24,22 +20,19 @@ class TestChrootFacts:
mocker.patch(
"os.stat",
side_effect=[
- MOCKSTAT(st_dev=stat.S_IFDIR, st_ino=2, st_size=0, st_mtime=0),
- MOCKSTAT(st_dev=stat.S_IFDIR, st_ino=3, st_size=1, st_mtime=0),
+ stat_result([0, 2, 0, 0, 0, 0, 0, 0, 0, 0]),
+ stat_result([0, 3, 0, 0, 0, 0, 0, 0, 0, 0]),
],
)
chroot_facts = ChrootFactCollector().collect()
assert chroot_facts["is_chroot"] is True
- def _mock_os_stat_exception(self):
- raise Exception("fake os.stat exception")
-
def test_detect_chroot_no_btrfs_no_xfs(self, mocker):
mocker.patch(
"os.stat",
side_effect=[
- MOCKSTAT(st_dev=stat.S_IFDIR, st_ino=2, st_size=0, st_mtime=0),
- self._mock_os_stat_exception,
+ stat_result([0, 2, 0, 0, 0, 0, 0, 0, 0, 0]),
+ Exception("fake os.stat exception"),
],
)
chroot_facts = ChrootFactCollector().collect()
@@ -49,8 +42,8 @@ class TestChrootFacts:
mocker.patch(
"os.stat",
side_effect=[
- MOCKSTAT(st_dev=stat.S_IFDIR, st_ino=2, st_size=0, st_mtime=0),
- self._mock_os_stat_exception,
+ stat_result([0, 2, 0, 0, 0, 0, 0, 0, 0, 0]),
+ Exception("fake os.stat exception"),
],
)
@@ -75,8 +68,8 @@ class TestChrootFacts:
mocker.patch(
"os.stat",
side_effect=[
- MOCKSTAT(st_dev=stat.S_IFDIR, st_ino=2, st_size=0, st_mtime=0),
- self._mock_os_stat_exception,
+ stat_result([0, 2, 0, 0, 0, 0, 0, 0, 0, 0]),
+ Exception("fake os.stat exception"),
],
)
mocker.patch.object(module, "get_bin_path", return_value="/usr/bin/stat")
diff --git a/test/units/module_utils/facts/system/test_local.py b/test/units/module_utils/facts/system/test_local.py
new file mode 100644
index 00000000000..7732fe0a531
--- /dev/null
+++ b/test/units/module_utils/facts/system/test_local.py
@@ -0,0 +1,77 @@
+# Copyright: Contributors to the Ansible project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import annotations
+
+from os import stat_result
+
+from ansible.module_utils.facts.system.local import LocalFactCollector
+
+
+class TestLocalFacts:
+ def test_local_no_module(self):
+ local_facts = LocalFactCollector().collect()
+ assert local_facts == {"local": {}}
+
+ def test_local_no_fact_path_exists(self, mocker):
+ module = mocker.Mock()
+ mocker.patch("os.path.exists", return_value=False)
+ local_facts = LocalFactCollector().collect(module=module)
+ assert local_facts == {"local": {}}
+
+ def test_local_facts(self, mocker):
+ module = mocker.MagicMock()
+ module.params = {"fact_path": "/usr/local/facts"}
+ mocker.patch("os.path.exists", return_value=True)
+ mocker.patch("glob.glob", return_value=["/usr/local/facts/sample.fact"])
+ local_facts = LocalFactCollector().collect(module=module)
+ assert "Could not stat fact" in local_facts["local"]["sample"]
+
+ mock_stat = stat_result([mocker.MagicMock()] * 10)
+ mocker.patch("os.stat", return_value=mock_stat)
+ mocker.patch.object(module, "run_command", return_value=(1, "", "failed"))
+ local_facts = LocalFactCollector().collect(module=module)
+ assert "Failure executing" in local_facts["local"]["sample"]
+
+ mock_output = """{"defaults": {"foo": "bar"}}"""
+ mocker.patch.object(module, "run_command", return_value=(0, mock_output, ""))
+ local_facts = LocalFactCollector().collect(module=module)
+ assert local_facts["local"]["sample"]["defaults"]["foo"] == "bar"
+
+ mock_config_output = "foo=bar\n"
+ mocker.patch.object(
+ module, "run_command", return_value=(0, mock_config_output, "")
+ )
+ local_facts = LocalFactCollector().collect(module=module)
+ assert "error loading facts as JSON or ini" in local_facts["local"]["sample"]
+
+ mock_config_output = "[defaults]\nfoo=bar\n"
+ mocker.patch.object(
+ module, "run_command", return_value=(0, mock_config_output, "")
+ )
+ local_facts = LocalFactCollector().collect(module=module)
+ assert local_facts["local"]["sample"]["defaults"]["foo"] == "bar"
+
+ mock_config_output = "[defaults]\n"
+ mocker.patch.object(
+ module, "run_command", return_value=(0, mock_config_output, "")
+ )
+ local_facts = LocalFactCollector().collect(module=module)
+ assert local_facts["local"]["sample"]["defaults"] == {}
+
+ mock_config_output = "[defaults]\n"
+ mocker.patch.object(
+ module, "run_command", return_value=(0, mock_config_output, "")
+ )
+ local_facts = LocalFactCollector().collect(module=module)
+ assert local_facts["local"]["sample"]["defaults"] == {}
+
+ mocker.patch.object(module, "run_command", return_value=(0, "", ""))
+ mocker.patch(
+ "json.loads", side_effect=Exception("fake _mock_json_load exception")
+ )
+ local_facts = LocalFactCollector().collect(module=module)
+ assert (
+ "Failed to convert (/usr/local/facts/sample.fact)"
+ in local_facts["local"]["sample"]
+ )
diff --git a/test/units/module_utils/facts/system/test_platform.py b/test/units/module_utils/facts/system/test_platform.py
new file mode 100644
index 00000000000..5f8df6dfb25
--- /dev/null
+++ b/test/units/module_utils/facts/system/test_platform.py
@@ -0,0 +1,122 @@
+# Copyright: Contributors to the Ansible project
+# GNU General Public License v3.0+ (see COPYING or https://www.gnu.org/licenses/gpl-3.0.txt)
+
+from __future__ import annotations
+
+import re
+
+import pytest
+
+from ansible.module_utils.facts.system.platform import PlatformFactCollector
+
+SOLARIS_I86_RE_PATTERN = re.compile(r"i([3456]86|86pc)")
+
+
+class TestPlatformFacts:
+ def test_platform_system(self, mocker):
+ mocker.patch("platform.system", return_value="Darwin")
+ mocker.patch("platform.release", return_value="23.5.0")
+ mocker.patch("platform.python_version", return_value="3.11.4")
+ mocker.patch("socket.getfqdn", return_value="localhost.localdomain")
+ mocker.patch("platform.node", return_value="localhost.localdomain")
+ mac_kernel_ver = "Darwin Kernel Version 23.5.0: Wed May 1 20:12:58 PDT 2024; root:xnu-10063.121.3~5/RELEASE_ARM64_T6000"
+ mocker.patch("platform.version", return_value=mac_kernel_ver)
+ mocker.patch("platform.machine", return_value="arm64")
+ mock_machine_id = "fe31eab802474047fd1e9f8ca050234b"
+ mocker.patch(
+ "ansible.module_utils.facts.system.platform.get_file_content",
+ return_value=mock_machine_id,
+ )
+ platform_facts = PlatformFactCollector().collect()
+
+ assert "system" in platform_facts
+ assert "kernel" in platform_facts
+ assert "kernel_version" in platform_facts
+ assert "python_version" in platform_facts
+ assert "fqdn" in platform_facts
+ assert "hostname" in platform_facts
+ assert "nodename" in platform_facts
+ assert "domain" in platform_facts
+ assert "machine" in platform_facts
+ assert "architecture" in platform_facts
+
+ assert platform_facts["system"] == "Darwin"
+ assert platform_facts["kernel"] == "23.5.0"
+ assert "Darwin" in platform_facts["kernel_version"]
+ assert platform_facts["python_version"] == "3.11.4"
+ assert platform_facts["fqdn"] == "localhost.localdomain"
+ assert platform_facts["hostname"] == "localhost"
+ assert platform_facts["nodename"] == "localhost.localdomain"
+ assert platform_facts["domain"] == "localdomain"
+ assert platform_facts["machine"] == "arm64"
+ assert platform_facts["architecture"] == "arm64"
+ assert platform_facts["machine_id"] == mock_machine_id
+
+ @pytest.mark.parametrize(
+ "platform_machine",
+ [
+ pytest.param("AMD64", id="amd64"),
+ pytest.param("aarch64", id="arm64"),
+ pytest.param("aarch64", id="armhf"),
+ pytest.param("armv7l", id="armhf"),
+ pytest.param("ppc", id="powerpc"),
+ pytest.param("ppc64le", id="ppc64el"),
+ pytest.param("x86_64", id="amd64"),
+ pytest.param("x86_64", id="i386"),
+ pytest.param("s390x", id="s390x"),
+ pytest.param("riscv64", id="riscv64"),
+ pytest.param("unknownarch", id="unknown-arch"),
+ pytest.param("i386", id="solaris-i386"),
+ pytest.param("i386", id="solaris-i386-64"),
+ ],
+ )
+ def test_platform_machine(self, mocker, platform_machine):
+ platform_facts = PlatformFactCollector().collect()
+ mocker.patch("platform.machine", return_value=platform_machine)
+ assert "machine" in platform_facts
+ assert "userspace_bits" in platform_facts
+ assert "architecture" in platform_facts
+
+ if platform_facts["machine"] == "x86_64":
+ assert platform_facts["architecture"] == platform_facts["machine"]
+ assert "userspace_architecture" in platform_facts
+ if platform_facts["userspace_bits"] == "64":
+ assert platform_facts["userspace_architecture"] == "x86_64"
+ elif platform_facts["userspace_bits"] == "32":
+ assert platform_facts["userspace_architecture"] == "i386"
+ elif SOLARIS_I86_RE_PATTERN.search(platform_facts["machine"]):
+ assert platform_facts["architecture"] == "i386"
+ if platform_facts["userspace_bits"] == "64":
+ assert platform_facts["userspace_architecture"] == "x86_64"
+ elif platform_facts["userspace_bits"] == "32":
+ assert platform_facts["userspace_architecture"] == "i386"
+ else:
+ assert platform_facts["architecture"] == platform_facts["machine"]
+
+ def test_platform_aix(self, mocker):
+ module = mocker.MagicMock()
+ mocker.patch("platform.system", return_value="AIX")
+ mocker.patch.object(module, "get_bin_path", return_value="/usr/bin/getconf")
+ mocker.patch.object(module, "run_command", return_value=(0, "chrp\n", ""))
+ platform_facts = PlatformFactCollector().collect(module=module)
+ assert platform_facts["architecture"] == "chrp"
+
+ mocker.patch.object(module, "get_bin_path", side_effect=[None, "fake/bootinfo"])
+ platform_facts = PlatformFactCollector().collect(module=module)
+ assert platform_facts["architecture"] == "chrp"
+
+ def test_platform_openbsd(self, mocker):
+ module = mocker.MagicMock()
+ mocker.patch("platform.system", return_value="OpenBSD")
+ mocker.patch("platform.release", return_value="7.4")
+ mocker.patch("platform.version", return_value="7.4")
+ mocker.patch("socket.getfqdn", return_value="localhost.localdomain")
+ mocker.patch("platform.node", return_value="localhost.localdomain")
+ mocker.patch("platform.architecture", return_value=("64bit", "ELF"))
+ mocker.patch("platform.machine", return_value="amd64")
+ mocker.patch(
+ "platform.uname",
+ return_value=["OpenBSD", "openbsd", "7.4", "7.4", "amd64", "amd64"],
+ )
+ platform_facts = PlatformFactCollector().collect(module=module)
+ assert platform_facts["architecture"] == "amd64"