diff --git a/.github/workflows/autils_migration_announcement.yml b/.github/workflows/autils_migration_announcement.yml index b0433e5dab..c95e6a8cf4 100644 --- a/.github/workflows/autils_migration_announcement.yml +++ b/.github/workflows/autils_migration_announcement.yml @@ -9,6 +9,7 @@ on: - '**/astring.py' - '**/crypto.py' - '**/data_structures.py' + - '**/distro.py' - '**/external/gdbmi_parser.py' - '**/external/spark.py' - '**/gdb.py' diff --git a/.pylintrc_utils b/.pylintrc_utils index 82c5f7c718..731ac4afe5 100644 --- a/.pylintrc_utils +++ b/.pylintrc_utils @@ -7,7 +7,7 @@ extension-pkg-whitelist=netifaces # Add files or directories to the blacklist. They should be base names, not # paths. -ignore=CVS,archive.py,asset.py,astring.py,aurl.py,build.py,cloudinit.py,cpu.py,data_factory.py,datadrainer.py,debug.py,diff_validator.py,disk.py,distro.py,dmesg.py,download.py,exit_codes.py,file_utils.py,filelock.py,git.py,iso9660.py,kernel.py,linux.py,linux_modules.py,lv_utils.py,memory.py,multipath.py,nvme.py,partition.py,pci.py,pmem.py,podman.py,service.py,softwareraid.py,ssh.py,stacktrace.py,sysinfo.py,vmimage.py,wait.py,gdbmi_parser.py,spark.py,distro_packages.py,inspector.py,main.py,manager.py,apt.py,base.py,dnf.py,dpkg.py,rpm.py,yum.py,zypper.py,deprecation.py +ignore=CVS,archive.py,asset.py,astring.py,aurl.py,build.py,cloudinit.py,cpu.py,data_factory.py,datadrainer.py,debug.py,diff_validator.py,disk.py,dmesg.py,download.py,exit_codes.py,file_utils.py,filelock.py,git.py,iso9660.py,kernel.py,linux.py,linux_modules.py,lv_utils.py,memory.py,multipath.py,nvme.py,partition.py,pci.py,pmem.py,podman.py,service.py,softwareraid.py,ssh.py,stacktrace.py,sysinfo.py,vmimage.py,wait.py,gdbmi_parser.py,spark.py,distro_packages.py,inspector.py,main.py,manager.py,apt.py,base.py,dnf.py,dpkg.py,rpm.py,yum.py,zypper.py,deprecation.py # regex matches against base names, not paths. ignore-patterns=.git diff --git a/avocado/utils/distro.py b/avocado/utils/distro.py index 4b2e4d904d..ca1b5ca93a 100644 --- a/avocado/utils/distro.py +++ b/avocado/utils/distro.py @@ -12,10 +12,7 @@ # Copyright: Red Hat Inc. 2013-2014 # Author: Lucas Meneghel Rodrigues -""" -This module provides the client facilities to detect the Linux Distribution -it's running under. -""" +"""Detect the Linux distribution running on a local or remote machine.""" import logging import os @@ -38,13 +35,10 @@ # pylint: disable=R0903 class LinuxDistro: - """ - Simple collection of information for a Linux Distribution - """ + """Simple collection of information for a Linux Distribution.""" def __init__(self, name, version, release, arch): - """ - Initializes a new Linux Distro + """Initialize a new Linux Distro. :param name: a short name that precisely distinguishes this Linux Distribution among all others. @@ -74,6 +68,11 @@ def __init__(self, name, version, release, arch): self.arch = arch def __repr__(self): + """Return a string representation of the Linux distro. + + :return: formatted string with name, version, release and arch. + :rtype: str + """ return ( f"" @@ -96,10 +95,10 @@ def __repr__(self): class Probe: - """ - Probes the machine and does it best to confirm it's the right distro. - If given an avocado.utils.ssh.Session object representing another machine, Probe - will attempt to detect another machine's distro via an ssh connection. + """Probe a machine to confirm which Linux distribution it runs. + + If given an avocado.utils.ssh.Session object representing another machine, + Probe will attempt to detect that machine's distro via an ssh connection. """ #: Points to a file that can determine if this machine is running a given @@ -121,16 +120,20 @@ class Probe: CHECK_VERSION_REGEX = None def __init__(self, session=None): + """Initialize a Probe instance. + + :param session: optional ssh session to probe a remote machine. + :type session: avocado.utils.ssh.Session or None + """ self.score = 0 self.session = session def check_for_remote_file(self, file_name): - """ - Checks if provided file exists in remote machine + """Check if a file exists on a remote machine. - :param file_name: name of file + :param file_name: path to the file on the remote machine. :type file_name: str - :returns: whether the file exists in remote machine or not + :return: whether the file exists on the remote machine. :rtype: bool """ if self.session and not self.session.cmd(f"test -f {file_name}").exit_status: @@ -138,12 +141,14 @@ def check_for_remote_file(self, file_name): return False def check_name_for_file(self): - """ - Checks if this class will look for a file and return a distro + """Check if this class can look for a file and return a distro name. The conditions that must be true include the file that identifies the distro file being set (:attr:`CHECK_FILE`) and the name of the - distro to be returned (:attr:`CHECK_FILE_DISTRO_NAME`) + distro to be returned (:attr:`CHECK_FILE_DISTRO_NAME`). + + :return: whether both CHECK_FILE and CHECK_FILE_DISTRO_NAME are set. + :rtype: bool """ if self.CHECK_FILE is None: return False @@ -154,8 +159,10 @@ def check_name_for_file(self): return True def name_for_file(self): - """ - Get the distro name if the :attr:`CHECK_FILE` is set and exists + """Get the distro name if :attr:`CHECK_FILE` is set and exists. + + :return: the distro name or None if the file does not exist. + :rtype: str or None """ if self.check_name_for_file(): if self.check_for_remote_file(self.CHECK_FILE): @@ -165,13 +172,16 @@ def name_for_file(self): return None def check_name_for_file_contains(self): - """ - Checks if this class will look for text on a file and return a distro + """Check if this class can search file content and return a distro. The conditions that must be true include the file that identifies the distro file being set (:attr:`CHECK_FILE`), the text to look for inside the distro file (:attr:`CHECK_FILE_CONTAINS`) and the name - of the distro to be returned (:attr:`CHECK_FILE_DISTRO_NAME`) + of the distro to be returned (:attr:`CHECK_FILE_DISTRO_NAME`). + + :return: whether CHECK_FILE, CHECK_FILE_CONTAINS and + CHECK_FILE_DISTRO_NAME are all set. + :rtype: bool """ if self.CHECK_FILE is None: return False @@ -185,8 +195,10 @@ def check_name_for_file_contains(self): return True def name_for_file_contains(self): - """ - Get the distro if the :attr:`CHECK_FILE` is set and has content + """Get the distro if :attr:`CHECK_FILE` contains expected text. + + :return: the distro name or None if the text is not found. + :rtype: str or None """ if self.check_name_for_file_contains(): check_file = None @@ -212,8 +224,10 @@ def name_for_file_contains(self): return None def check_version(self): - """ - Checks if this class will look for a regex in file and return a distro + """Check if this class can extract a version via regex. + + :return: whether both CHECK_FILE and CHECK_VERSION_REGEX are set. + :rtype: bool """ if self.CHECK_FILE is None: return False @@ -224,8 +238,11 @@ def check_version(self): return True def _get_version_match(self): - """ - Returns the match result for the version regex on the file content + """Return the regex match result for the version file content. + + :return: the regex match object, or None if no match or file + is unavailable. + :rtype: re.Match or None """ if self.check_version(): if self.session: @@ -252,8 +269,10 @@ def _get_version_match(self): return None def version(self): - """ - Returns the version of the distro + """Return the version of the distro. + + :return: the version string, or UNKNOWN_DISTRO_VERSION. + :rtype: str or int """ version = UNKNOWN_DISTRO_VERSION match = self._get_version_match() @@ -263,14 +282,18 @@ def version(self): return version def check_release(self): - """ - Checks if this has the conditions met to look for the release number + """Check if the regex has enough groups to extract a release number. + + :return: whether a release can be extracted from the version regex. + :rtype: bool """ return self.check_version() and self.CHECK_VERSION_REGEX.groups > 1 def release(self): - """ - Returns the release of the distro + """Return the release of the distro. + + :return: the release string, or UNKNOWN_DISTRO_RELEASE. + :rtype: str or int """ release = UNKNOWN_DISTRO_RELEASE match = self._get_version_match() @@ -281,10 +304,10 @@ def release(self): return release def get_distro(self): - """ - :param session: ssh connection between another machine + """Run all configured checks and return the detected distro. - Returns the :class:`LinuxDistro` this probe detected + :return: the detected :class:`LinuxDistro` or :data:`UNKNOWN_DISTRO`. + :rtype: :class:`LinuxDistro` """ name = None version = UNKNOWN_DISTRO_VERSION @@ -324,9 +347,7 @@ def get_distro(self): class RedHatProbe(Probe): - """ - Probe with version checks for Red Hat Enterprise Linux systems - """ + """Probe with version checks for Red Hat Enterprise Linux systems.""" CHECK_FILE = "/etc/redhat-release" CHECK_FILE_CONTAINS = "Red Hat Enterprise Linux" @@ -337,9 +358,7 @@ class RedHatProbe(Probe): class CentosProbe(RedHatProbe): - """ - Probe with version checks for CentOS systems - """ + """Probe with version checks for CentOS systems.""" CHECK_FILE = "/etc/redhat-release" CHECK_FILE_CONTAINS = "CentOS Linux" @@ -348,9 +367,7 @@ class CentosProbe(RedHatProbe): class CentosStreamProbe(RedHatProbe): - """ - Probe with version checks for CentOS Stream systems - """ + """Probe with version checks for CentOS Stream systems.""" CHECK_FILE = "/etc/redhat-release" CHECK_FILE_CONTAINS = "CentOS Stream" @@ -359,9 +376,7 @@ class CentosStreamProbe(RedHatProbe): class FedoraProbe(RedHatProbe): - """ - Probe with version checks for Fedora systems - """ + """Probe with version checks for Fedora systems.""" CHECK_FILE = "/etc/fedora-release" CHECK_FILE_CONTAINS = "Fedora" @@ -370,9 +385,7 @@ class FedoraProbe(RedHatProbe): class AmazonLinuxProbe(Probe): - """ - Probe for Amazon Linux systems - """ + """Probe for Amazon Linux systems.""" CHECK_FILE = "/etc/os-release" CHECK_FILE_CONTAINS = "Amazon Linux" @@ -383,9 +396,7 @@ class AmazonLinuxProbe(Probe): class DebianProbe(Probe): - """ - Simple probe with file checks for Debian systems - """ + """Probe with file checks for Debian systems.""" CHECK_FILE = "/etc/debian_version" CHECK_FILE_DISTRO_NAME = "debian" @@ -393,9 +404,7 @@ class DebianProbe(Probe): class UbuntuProbe(Probe): - """ - Simple probe for Ubuntu systems in general - """ + """Probe for Ubuntu systems.""" CHECK_FILE = "/etc/os-release" CHECK_FILE_CONTAINS = "ubuntu" @@ -406,9 +415,7 @@ class UbuntuProbe(Probe): class SUSEProbe(Probe): - """ - Simple probe for SUSE systems in general - """ + """Probe for SUSE systems with custom VERSION_ID parsing.""" CHECK_FILE = "/etc/os-release" CHECK_FILE_CONTAINS = "SUSE" @@ -417,6 +424,11 @@ class SUSEProbe(Probe): CHECK_FILE_DISTRO_NAME = "SuSE" def get_distro(self): + """Detect SUSE distro and parse VERSION_ID for version and release. + + :return: the detected :class:`LinuxDistro` with parsed version. + :rtype: :class:`LinuxDistro` + """ distro = super().get_distro() # if the default methods find SUSE, detect version @@ -448,9 +460,7 @@ def get_distro(self): class OpenEulerProbe(Probe): - """ - Simple probe for openEuler systems in general - """ + """Probe for openEuler systems.""" CHECK_FILE = "/etc/openEuler-release" CHECK_FILE_CONTAINS = "openEuler release" @@ -459,9 +469,7 @@ class OpenEulerProbe(Probe): class UnionTechProbe(Probe): - """ - Simple probe for UnionTech systems in general - """ + """Probe for UnionTech OS systems.""" CHECK_FILE = "/etc/os-version" CHECK_FILE_CONTAINS = "UnionTech OS" @@ -474,8 +482,10 @@ class UnionTechProbe(Probe): def register_probe(probe_class): - """ - Register a probe to be run during autodetection + """Register a probe to be run during autodetection. + + :param probe_class: the probe class to register. + :type probe_class: type """ if probe_class not in REGISTERED_PROBES: REGISTERED_PROBES.append(probe_class) @@ -494,15 +504,14 @@ def register_probe(probe_class): def detect(session=None): - """ - Attempts to detect the Linux Distribution running on this machine. + """Attempt to detect the Linux distribution running on this machine. - If given an avocado.utils.ssh.Session object, it will attempt to detect the - distro of another machine via an ssh connection. + If given an avocado.utils.ssh.Session object, it will attempt to detect + the distro of another machine via an ssh connection. - :param session: ssh connection between another machine - :type session: avocado.utils.ssh.Session - :returns: the detected :class:`LinuxDistro` or :data:`UNKNOWN_DISTRO` + :param session: ssh connection to another machine. + :type session: avocado.utils.ssh.Session or None + :return: the detected :class:`LinuxDistro` or :data:`UNKNOWN_DISTRO`. :rtype: :class:`LinuxDistro` """ results = [] @@ -523,12 +532,27 @@ def detect(session=None): class Spec: - """ - Describes a distro, usually for setting minimum distro requirements - """ + """Describe a distro, usually for setting minimum distro requirements.""" def __init__(self, name, min_version=None, min_release=None, arch=None): + """Initialize a distro specification. + + :param name: the distribution name to match. + :type name: str + :param min_version: minimum acceptable version. + :type min_version: int or None + :param min_release: minimum acceptable release. + :type min_release: int or None + :param arch: required architecture. + :type arch: str or None + """ self.name = name self.min_version = min_version self.min_release = min_release self.arch = arch + + +# pylint: disable=wrong-import-position +from avocado.utils.deprecation import log_deprecation + +log_deprecation.warning("distro") diff --git a/selftests/check.py b/selftests/check.py index 4fd6dc98d3..bf999f15ba 100755 --- a/selftests/check.py +++ b/selftests/check.py @@ -27,9 +27,9 @@ "job-api-check-tmp-directory-exists": 1, "nrunner-interface": 90, "nrunner-requirement": 28, - "unit": 976, + "unit": 1000, "jobs": 11, - "functional-parallel": 368, + "functional-parallel": 369, "functional-serial": 7, "optional-plugins": 0, "optional-plugins-golang": 2, diff --git a/selftests/functional/utils/distro.py b/selftests/functional/utils/distro.py index b7f34eafb5..1d69ec85a2 100644 --- a/selftests/functional/utils/distro.py +++ b/selftests/functional/utils/distro.py @@ -1,10 +1,12 @@ import os +import platform from avocado import Test from avocado.core.exit_codes import AVOCADO_ALL_OK from avocado.core.job import Job from avocado.core.nrunner.runnable import Runnable from avocado.core.suite import TestSuite +from avocado.utils import distro class Distro(Test): @@ -64,3 +66,28 @@ def test_debian_12_7(self): + os.uname().machine.encode() + b") version 12 release 7\n", ) + + +class DistroDetectLocal(Test): + """Tests distro detection on the local system without containers.""" + + def test_detect_current_system(self): + """Verify detect() returns a valid result for the running system.""" + result = distro.detect() + self.assertIsInstance(result, distro.LinuxDistro) + has_release_file = any( + os.path.exists(p) + for p in [ + "/etc/os-release", + "/etc/redhat-release", + "/etc/fedora-release", + "/etc/debian_version", + ] + ) + if has_release_file: + self.assertNotEqual( + result.name, + distro.UNKNOWN_DISTRO_NAME, + "detect() should identify a known distro on this system", + ) + self.assertEqual(result.arch, platform.machine()) diff --git a/selftests/unit/utils/distro.py b/selftests/unit/utils/distro.py index 669d702d12..66bd7b078c 100644 --- a/selftests/unit/utils/distro.py +++ b/selftests/unit/utils/distro.py @@ -1,9 +1,28 @@ +import os import re +import tempfile +import unittest import unittest.mock from avocado.utils import distro +class LinuxDistroTest(unittest.TestCase): + def test_init(self): + dist = distro.LinuxDistro("fedora", "38", "0", "x86_64") + self.assertEqual(dist.name, "fedora") + self.assertEqual(dist.version, "38") + self.assertEqual(dist.release, "0") + self.assertEqual(dist.arch, "x86_64") + + def test_repr(self): + dist = distro.LinuxDistro("rhel", "9", "1", "x86_64") + self.assertEqual( + repr(dist), + "", + ) + + class ProbeTest(unittest.TestCase): def test_check_name_for_file_fail(self): class MyProbe(distro.Probe): @@ -12,14 +31,6 @@ class MyProbe(distro.Probe): my_probe = MyProbe() self.assertFalse(my_probe.check_name_for_file()) - def test_check_name_for_file(self): - class MyProbe(distro.Probe): - CHECK_FILE = "/etc/issue" - CHECK_FILE_DISTRO_NAME = "superdistro" - - my_probe = MyProbe() - self.assertTrue(my_probe.check_name_for_file()) - def test_check_name_for_file_contains_fail(self): class MyProbe(distro.Probe): CHECK_FILE = "/etc/issue" @@ -28,15 +39,6 @@ class MyProbe(distro.Probe): my_probe = MyProbe() self.assertFalse(my_probe.check_name_for_file_contains()) - def test_check_name_for_file_contains(self): - class MyProbe(distro.Probe): - CHECK_FILE = "/etc/issue" - CHECK_FILE_CONTAINS = "text" - CHECK_FILE_DISTRO_NAME = "superdistro" - - my_probe = MyProbe() - self.assertTrue(my_probe.check_name_for_file_contains()) - def test_check_version_fail(self): class MyProbe(distro.Probe): CHECK_VERSION_REGEX = re.compile(r"distro version (\d+)") @@ -44,14 +46,6 @@ class MyProbe(distro.Probe): my_probe = MyProbe() self.assertFalse(my_probe.check_version()) - def test_version_returnable(self): - class MyProbe(distro.Probe): - CHECK_FILE = "/etc/distro-release" - CHECK_VERSION_REGEX = re.compile(r"distro version (\d+)") - - my_probe = MyProbe() - self.assertTrue(my_probe.check_version()) - def test_name_for_file(self): distro_file = "/etc/superdistro-issue" distro_name = "superdistro" @@ -68,6 +62,281 @@ class MyProbe(distro.Probe): mocked.assert_called_once_with(distro_file) self.assertEqual(distro_name, probed_distro_name) + def test_name_for_file_contains_match(self): + fd, tmpfile = tempfile.mkstemp(suffix="-release") + with os.fdopen(fd, "w") as tmp: + tmp.write("Welcome to MyDistro 5.0\n") + self.addCleanup(os.unlink, tmpfile) + + class MyProbe(distro.Probe): + CHECK_FILE = tmpfile + CHECK_FILE_CONTAINS = "MyDistro" + CHECK_FILE_DISTRO_NAME = "mydistro" + + self.assertEqual(MyProbe().name_for_file_contains(), "mydistro") + + def test_name_for_file_contains_no_match(self): + fd, tmpfile = tempfile.mkstemp(suffix="-release") + with os.fdopen(fd, "w") as tmp: + tmp.write("Some other content\n") + self.addCleanup(os.unlink, tmpfile) + + class MyProbe(distro.Probe): + CHECK_FILE = tmpfile + CHECK_FILE_CONTAINS = "MyDistro" + CHECK_FILE_DISTRO_NAME = "mydistro" + + self.assertIsNone(MyProbe().name_for_file_contains()) + + def test_name_for_file_contains_ioerror(self): + class MyProbe(distro.Probe): + CHECK_FILE = "/etc/fake-release" + CHECK_FILE_CONTAINS = "MyDistro" + CHECK_FILE_DISTRO_NAME = "mydistro" + + with unittest.mock.patch( + "avocado.utils.distro.os.path.exists", return_value=True + ): + with unittest.mock.patch( + "builtins.open", side_effect=IOError("Permission denied") + ): + self.assertIsNone(MyProbe().name_for_file_contains()) + + def test_version_from_file(self): + class MyProbe(distro.Probe): + CHECK_FILE = "/etc/fake-release" + CHECK_VERSION_REGEX = re.compile(r"Release (\d+)\.(\d+)") + + with unittest.mock.patch( + "avocado.utils.distro.os.path.exists", return_value=True + ): + with unittest.mock.patch( + "builtins.open", + unittest.mock.mock_open(read_data="Release 9.3\n"), + ): + self.assertEqual(MyProbe().version(), "9") + + def test_version_no_match(self): + class MyProbe(distro.Probe): + CHECK_FILE = "/etc/fake-release" + CHECK_VERSION_REGEX = re.compile(r"Release (\d+)\.(\d+)") + + with unittest.mock.patch( + "avocado.utils.distro.os.path.exists", return_value=True + ): + with unittest.mock.patch( + "builtins.open", + unittest.mock.mock_open(read_data="No version here\n"), + ): + self.assertEqual(MyProbe().version(), distro.UNKNOWN_DISTRO_VERSION) + + def test_release_from_file(self): + class MyProbe(distro.Probe): + CHECK_FILE = "/etc/fake-release" + CHECK_VERSION_REGEX = re.compile(r"Release (\d+)\.(\d+)") + + with unittest.mock.patch( + "avocado.utils.distro.os.path.exists", return_value=True + ): + with unittest.mock.patch( + "builtins.open", + unittest.mock.mock_open(read_data="Release 9.3\n"), + ): + self.assertEqual(MyProbe().release(), "3") + + def test_release_single_group(self): + class MyProbe(distro.Probe): + CHECK_FILE = "/etc/fake-release" + CHECK_VERSION_REGEX = re.compile(r"Release (\d+)") + + with unittest.mock.patch( + "avocado.utils.distro.os.path.exists", return_value=True + ): + with unittest.mock.patch( + "builtins.open", + unittest.mock.mock_open(read_data="Release 9\n"), + ): + self.assertEqual(MyProbe().release(), distro.UNKNOWN_DISTRO_RELEASE) + + def test_check_release_single_group(self): + class MyProbe(distro.Probe): + CHECK_FILE = "/etc/fake-release" + CHECK_VERSION_REGEX = re.compile(r"Release (\d+)") + + self.assertFalse(MyProbe().check_release()) + + def test_get_distro_detected(self): + fd, tmpfile = tempfile.mkstemp(suffix="-release") + with os.fdopen(fd, "w") as tmp: + tmp.write("TestDistro release 5.2 (Final)\n") + self.addCleanup(os.unlink, tmpfile) + + class MyProbe(distro.Probe): + CHECK_FILE = tmpfile + CHECK_FILE_CONTAINS = "TestDistro" + CHECK_FILE_DISTRO_NAME = "testdistro" + CHECK_VERSION_REGEX = re.compile(r"TestDistro release (\d+)\.(\d+).*") + + result = MyProbe().get_distro() + self.assertEqual(result.name, "testdistro") + self.assertEqual(result.version, "5") + self.assertEqual(result.release, "2") + + def test_get_distro_unknown(self): + result = distro.Probe().get_distro() + self.assertIs(result, distro.UNKNOWN_DISTRO) + + def test_check_for_remote_file_found(self): + mock_session = unittest.mock.MagicMock() + mock_session.cmd.return_value.exit_status = 0 + probe = distro.Probe(session=mock_session) + self.assertTrue(probe.check_for_remote_file("/etc/some-file")) + mock_session.cmd.assert_called_once_with("test -f /etc/some-file") + + def test_check_for_remote_file_not_found(self): + mock_session = unittest.mock.MagicMock() + mock_session.cmd.return_value.exit_status = 1 + probe = distro.Probe(session=mock_session) + self.assertFalse(probe.check_for_remote_file("/etc/nonexistent")) + + +class ProbeRegexTest(unittest.TestCase): + """Tests version regex patterns against real release file strings.""" + + def test_redhat_regex(self): + match = distro.RedHatProbe.CHECK_VERSION_REGEX.match( + "Red Hat Enterprise Linux Server release 9.3 (Plow)" + ) + self.assertIsNotNone(match) + self.assertEqual(match.group(1), "9") + self.assertEqual(match.group(2), "3") + + def test_centos_stream_regex(self): + match = distro.CentosStreamProbe.CHECK_VERSION_REGEX.match( + "CentOS Stream release 9" + ) + self.assertIsNotNone(match) + self.assertEqual(match.group(1), "9") + + def test_debian_numeric_version(self): + match = distro.DebianProbe.CHECK_VERSION_REGEX.match("12.7") + self.assertIsNotNone(match) + self.assertEqual(match.group(2), "12") + + def test_debian_codename_version(self): + match = distro.DebianProbe.CHECK_VERSION_REGEX.match("trixie/sid") + self.assertIsNotNone(match) + self.assertEqual(match.group(1), "sid") + + def test_ubuntu_regex(self): + content = 'NAME="Ubuntu"\nVERSION_ID="22.04"\nID=ubuntu\n' + match = distro.UbuntuProbe.CHECK_VERSION_REGEX.match(content) + self.assertIsNotNone(match) + self.assertEqual(match.group(1), "22.04") + + def test_amazon_regex(self): + content = 'NAME="Amazon Linux"\nVERSION="2"\nVERSION_ID="2"\n' + match = distro.AmazonLinuxProbe.CHECK_VERSION_REGEX.match(content) + self.assertIsNotNone(match) + self.assertEqual(match.group(1), "2") + + +class SUSEProbeTest(unittest.TestCase): + def test_suse_version_parsing(self): + os_release = ( + 'NAME="SUSE Linux Enterprise Server"\n' + 'VERSION="12-SP2"\n' + 'VERSION_ID="12.2"\n' + 'ID="sles"\n' + ) + fd, tmpfile = tempfile.mkstemp(suffix="-os-release") + with os.fdopen(fd, "w") as tmp: + tmp.write(os_release) + self.addCleanup(os.unlink, tmpfile) + + with unittest.mock.patch.object(distro.SUSEProbe, "CHECK_FILE", tmpfile): + result = distro.SUSEProbe().get_distro() + self.assertEqual(result.name, "SuSE") + self.assertEqual(result.version, 12) + self.assertEqual(result.release, 2) + + +class RegisterProbeTest(unittest.TestCase): + def setUp(self): + self._original_probes = distro.REGISTERED_PROBES[:] + + def tearDown(self): + distro.REGISTERED_PROBES[:] = self._original_probes + + def test_register_new_probe(self): + class CustomProbe(distro.Probe): + CHECK_FILE = "/etc/custom-release" + + initial_count = len(distro.REGISTERED_PROBES) + distro.register_probe(CustomProbe) + self.assertEqual(len(distro.REGISTERED_PROBES), initial_count + 1) + self.assertIn(CustomProbe, distro.REGISTERED_PROBES) + + def test_register_duplicate_ignored(self): + class CustomProbe(distro.Probe): + CHECK_FILE = "/etc/custom-release" + + distro.register_probe(CustomProbe) + count_after_first = len(distro.REGISTERED_PROBES) + distro.register_probe(CustomProbe) + self.assertEqual(len(distro.REGISTERED_PROBES), count_after_first) + + +class DetectTest(unittest.TestCase): + def setUp(self): + self._original_probes = distro.REGISTERED_PROBES[:] + + def tearDown(self): + distro.REGISTERED_PROBES[:] = self._original_probes + + def test_detect_returns_best_scoring(self): + fd, tmpfile = tempfile.mkstemp(suffix="-release") + with os.fdopen(fd, "w") as tmp: + tmp.write("HighScore release 3.1\n") + self.addCleanup(os.unlink, tmpfile) + + class LowProbe(distro.Probe): + CHECK_FILE = tmpfile + CHECK_FILE_DISTRO_NAME = "lowscore" + + class HighProbe(distro.Probe): + CHECK_FILE = tmpfile + CHECK_FILE_CONTAINS = "HighScore" + CHECK_FILE_DISTRO_NAME = "highscore" + CHECK_VERSION_REGEX = re.compile(r"HighScore release (\d+)\.(\d+).*") + + distro.REGISTERED_PROBES[:] = [LowProbe, HighProbe] + result = distro.detect() + self.assertEqual(result.name, "highscore") + self.assertEqual(result.version, "3") + self.assertEqual(result.release, "1") + + def test_detect_no_probes_returns_unknown(self): + distro.REGISTERED_PROBES[:] = [] + result = distro.detect() + self.assertIs(result, distro.UNKNOWN_DISTRO) + + +class SpecTest(unittest.TestCase): + def test_init_with_all_args(self): + spec = distro.Spec("fedora", min_version=38, min_release=0, arch="x86_64") + self.assertEqual(spec.name, "fedora") + self.assertEqual(spec.min_version, 38) + self.assertEqual(spec.min_release, 0) + self.assertEqual(spec.arch, "x86_64") + + def test_init_defaults(self): + spec = distro.Spec("rhel") + self.assertEqual(spec.name, "rhel") + self.assertIsNone(spec.min_version) + self.assertIsNone(spec.min_release) + self.assertIsNone(spec.arch) + if __name__ == "__main__": unittest.main()