diff --git a/tests/conftest.py b/tests/conftest.py new file mode 100644 index 0000000..c7e58fa --- /dev/null +++ b/tests/conftest.py @@ -0,0 +1,17 @@ +"""Make the ``distro`` package importable without an install. + +Tries, in order: this workdir's ``distro-src/src`` checkout and the +repo root's ``src`` layout (the layout the PR targets). +""" + +import os +import sys + +_HERE = os.path.dirname(os.path.abspath(__file__)) +_CANDIDATES = ( + os.path.join(_HERE, "..", "distro-src", "src"), + os.path.join(_HERE, "..", "..", "src"), +) +for _candidate in _CANDIDATES: + if os.path.isdir(os.path.join(_candidate, "distro")): + sys.path.insert(0, os.path.normpath(_candidate)) diff --git a/tests/test_distro_release_lsb_extended.py b/tests/test_distro_release_lsb_extended.py new file mode 100644 index 0000000..ece10cb --- /dev/null +++ b/tests/test_distro_release_lsb_extended.py @@ -0,0 +1,168 @@ +"""Extended distro-release / lsb-release / uname coverage for distro. + +Probed behaviours pinned here: + +* A distro-release file's ``id`` comes from its *file name* + (``centos-release`` -> ``centos``, ``redhat-release`` -> ``redhat`` + then normalised to ``rhel``); a file with a random name yields no + ``id`` even though name/version/codename still parse. +* ``lsb_release`` output is only used when ``include_lsb`` is true, + and its ``Distributor ID`` goes through the same normalisation + table (``RedHatEnterpriseServer`` -> ``rhel``). +* ``_parse_uname_content`` deliberately returns ``{}`` for Linux + (so the kernel version never becomes the distro version) and + parses other systems (``FreeBSD 13.2-RELEASE`` -> freebsd/13.2). +""" + +import os +import stat + +import pytest + +from distro import LinuxDistribution + +MISSING = "/nonexistent/os-release" + + +def distro_only(tmp_path, filename, content): + path = tmp_path / filename + path.write_text(content) + return LinuxDistribution( + os_release_file=MISSING, + distro_release_file=str(path), + include_lsb=False, + include_uname=False, + ) + + +def test_centos_release_file(tmp_path): + d = distro_only(tmp_path, "centos-release", "CentOS Linux release 7.9.2009 (Core)\n") + assert d.id() == "centos" + assert d.name() == "CentOS Linux" + assert d.version() == "7.9.2009" + assert d.version_parts() == ("7", "9", "2009") + assert d.codename() == "Core" + assert d.distro_release_info() == { + "name": "CentOS Linux", + "version_id": "7.9.2009", + "codename": "Core", + "id": "centos", + } + + +def test_redhat_release_file_id_is_normalized_to_rhel(tmp_path): + d = distro_only(tmp_path, "redhat-release", "Red Hat Enterprise Linux release 7.9 (Maipo)\n") + assert d.id() == "rhel" + assert d.name() == "Red Hat Enterprise Linux" + assert d.version() == "7.9" + assert d.codename() == "Maipo" + + +def test_fedora_release_file(tmp_path): + d = distro_only(tmp_path, "fedora-release", "Fedora release 30 (Thirty)\n") + assert d.id() == "fedora" + assert d.version() == "30" + assert d.codename() == "Thirty" + + +def test_randomly_named_release_file_has_no_id(tmp_path): + d = distro_only(tmp_path, "myfile.txt", "CentOS Linux release 7.9.2009 (Core)\n") + assert d.id() == "" + assert d.name() == "CentOS Linux" + assert d.version() == "7.9.2009" + + +def test_parse_distro_release_content_directly(tmp_path): + d = distro_only(tmp_path, "fedora-release", "Fedora release 30 (Thirty)\n") + assert d._parse_distro_release_content("Fedora release 30 (Thirty)\n") == { + "name": "Fedora", + "version_id": "30", + "codename": "Thirty", + } + assert d._parse_distro_release_content("Some Unparsed Line\n") == { + "name": "Some Unparsed Line" + } + assert d._parse_distro_release_content("") == {} + + +def test_parse_lsb_release_content_directly(tmp_path): + d = distro_only(tmp_path, "fedora-release", "Fedora release 30 (Thirty)\n") + parsed = d._parse_lsb_release_content( + [ + "Distributor ID:\tDebian\n", + "Description:\tDebian GNU/Linux 12 (bookworm)\n", + "Release:\t12\n", + "Codename:\tbookworm\n", + "No colon here\n", + ] + ) + assert parsed == { + "distributor_id": "Debian", + "description": "Debian GNU/Linux 12 (bookworm)", + "release": "12", + "codename": "bookworm", + } + + +def test_lsb_n_a_becomes_empty(tmp_path): + d = distro_only(tmp_path, "fedora-release", "Fedora release 30 (Thirty)\n") + parsed = d._parse_lsb_release_content(["Codename:\tn/a\n"]) + assert parsed == {"codename": ""} + + +def test_fake_lsb_release_command_via_path(tmp_path, monkeypatch): + bindir = tmp_path / "bin" + bindir.mkdir() + script = bindir / "lsb_release" + script.write_text( + "#!/bin/sh\n" + "printf 'Distributor ID:\\tRedHatEnterpriseServer\\n" + "Description:\\tRed Hat Enterprise Linux Server release 7.9 (Maipo)\\n" + "Release:\\t7.9\\nCodename:\\tMaipo\\n'\n" + ) + script.chmod(script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + monkeypatch.setenv("PATH", str(bindir) + os.pathsep + os.environ["PATH"]) + d = LinuxDistribution( + os_release_file=MISSING, + distro_release_file="/nonexistent/nofile", + include_lsb=True, + include_uname=False, + ) + assert d.id() == "rhel" + assert d.name() == "RedHatEnterpriseServer" + assert d.version() == "7.9" + assert d.codename() == "Maipo" + assert d.lsb_release_info()["release"] == "7.9" + + +def test_lsb_disabled_ignores_command(tmp_path, monkeypatch): + bindir = tmp_path / "bin" + bindir.mkdir() + script = bindir / "lsb_release" + script.write_text("#!/bin/sh\nprintf 'Distributor ID:\\tDebian\\nRelease:\\t12\\n'\n") + script.chmod(script.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + monkeypatch.setenv("PATH", str(bindir) + os.pathsep + os.environ["PATH"]) + d = LinuxDistribution( + os_release_file=MISSING, + distro_release_file="/nonexistent/nofile", + include_lsb=False, + include_uname=False, + ) + assert d.lsb_release_info() == {} + assert d.id() == "" + + +def test_parse_uname_content(tmp_path): + d = distro_only(tmp_path, "fedora-release", "Fedora release 30 (Thirty)\n") + assert d._parse_uname_content(["Linux 6.8.0"]) == {} + assert d._parse_uname_content([]) == {} + assert d._parse_uname_content(["FreeBSD 13.2-RELEASE"]) == { + "id": "freebsd", + "name": "FreeBSD", + "release": "13.2", + } + + +def test_to_str_decodes_bytes(tmp_path): + d = distro_only(tmp_path, "fedora-release", "Fedora release 30 (Thirty)\n") + assert d._to_str(b"hello") == "hello" diff --git a/tests/test_info_version_extended.py b/tests/test_info_version_extended.py new file mode 100644 index 0000000..f653202 --- /dev/null +++ b/tests/test_info_version_extended.py @@ -0,0 +1,125 @@ +"""Extended info / version / best-version coverage for distro. + +Probed behaviours pinned here: + +* ``version(best=True)`` picks the candidate with the most dots + (``1.2.3`` from a pretty-name parse beats ``1.2`` from + ``VERSION_ID``); without ``best`` the first non-empty candidate + wins. +* ``version(pretty=True)`` appends ``" ()"`` only when a + codename exists. +* ``info()`` returns the same values as the individual accessors, + with ``version_parts`` as a dict (not the tuple returned by + ``version_parts()``). +* ``LinuxDistribution.__repr__`` starts with the class name and the + file paths used. +""" + +import pytest + +from distro import LinuxDistribution + +MISSING = "/nonexistent/os-release" +NO_RELEASE = "/nonexistent/nofile" + + +def make(tmp_path, content): + path = tmp_path / "os-release" + path.write_text(content) + return LinuxDistribution( + os_release_file=str(path), + distro_release_file=NO_RELEASE, + include_lsb=False, + include_uname=False, + ) + + +def test_info_matches_accessors(tmp_path): + path = tmp_path / "os-release" + path.write_text( + 'PRETTY_NAME="Ubuntu 24.04.5 LTS"\nNAME="Ubuntu"\nVERSION_ID="24.04"\n' + 'VERSION="24.04.5 LTS (Noble Numbat)"\nVERSION_CODENAME=noble\nID=ubuntu\nID_LIKE=debian\n' + ) + d = LinuxDistribution( + os_release_file=str(path), + distro_release_file=NO_RELEASE, + include_lsb=False, + include_uname=False, + ) + assert d.info() == { + "id": "ubuntu", + "version": "24.04", + "version_parts": {"major": "24", "minor": "04", "build_number": ""}, + "like": "debian", + "codename": "noble", + } + assert d.info(pretty=True)["version"] == "24.04 (noble)" + + +def test_version_best_picks_most_dots(tmp_path): + d = make(tmp_path, 'ID=x\nVERSION_ID="1.2"\nPRETTY_NAME="X 1.2.3"\n') + assert d.version() == "1.2" + assert d.version(best=True) == "1.2.3" + assert d.version_parts(best=True) == ("1", "2", "3") + assert d.version_parts() == ("1", "2", "") + + +def test_version_pretty_without_codename_has_no_suffix(tmp_path): + d = make(tmp_path, 'ID=x\nVERSION_ID="8.10"\nNAME="Oracle Linux"\n') + assert d.version(pretty=True) == "8.10" + + +def test_version_parts_for_single_number_and_empty(tmp_path): + d = make(tmp_path, 'ID=x\nVERSION_ID="7"\n') + assert d.version_parts() == ("7", "", "") + assert d.major_version() == "7" + assert d.minor_version() == "" + assert d.build_number() == "" + + +def test_version_parts_best_flag(tmp_path): + d = make(tmp_path, 'ID=x\nVERSION_ID="1.2"\nPRETTY_NAME="X 1.2.3"\n') + assert d.major_version(best=True) == "1" + assert d.minor_version(best=True) == "2" + assert d.build_number(best=True) == "3" + + +def test_like_returns_id_like_verbatim(tmp_path): + d = make(tmp_path, 'ID=x\nID_LIKE="rhel fedora"\n') + assert d.like() == "rhel fedora" + + +def test_name_pretty_falls_back_to_name_plus_version(tmp_path): + # The pretty fallback does *not* use the os-release NAME: it uses + # the distro-release name (empty here) plus the pretty version, + # hence the leading space -- the same quirk as the Oracle case. + d = make(tmp_path, 'ID=x\nNAME="X"\nVERSION_ID="2.0"\nVERSION_CODENAME=two\n') + assert d.name(pretty=True) == " 2.0 (two)" + + +def test_repr_contains_class_and_paths(tmp_path): + d = make(tmp_path, 'ID=x\n') + text = repr(d) + assert text.startswith("LinuxDistribution(") + assert "os_release_file=" in text + assert "distro_release_file=" in text + + +def test_package_version_is_pinned(tmp_path): + import distro + + assert distro.__version__ == "1.9.0" + + +def test_linux_distribution_tuple_shape(tmp_path): + d = make(tmp_path, 'ID=x\nNAME="X"\nVERSION_ID="1.0"\n') + result = d.linux_distribution() + assert isinstance(result, tuple) and len(result) == 3 + + +def test_deprecated_linux_distribution_module_function_shape(tmp_path): + # The module-level linux_distribution() is deprecated but still + # returns a 3-tuple for the instance; check the instance method + # here, since the module function reflects the host, not fixtures. + d = make(tmp_path, 'ID=x\nNAME="X"\nVERSION_ID="1.0"\n') + assert d.linux_distribution(full_distribution_name=False)[0] == "x" diff --git a/tests/test_os_release_extended.py b/tests/test_os_release_extended.py new file mode 100644 index 0000000..8242a78 --- /dev/null +++ b/tests/test_os_release_extended.py @@ -0,0 +1,191 @@ +"""Extended os-release parsing coverage for distro. + +Every instance here is built from a temporary os-release file with +lsb/uname disabled and a nonexistent distro-release file, so no test +depends on the host distribution. Probed behaviours pinned here: + +* Single- and double-quoted values are unquoted; comments and + blank lines are ignored; a bare ``EMPTY=`` line is kept as ``""``. +* ``VERSION="... (Codename)"`` yields both ``codename`` (from + ``VERSION_CODENAME`` when present, which wins) and + ``release_codename`` (from the parenthesised part of ``VERSION``). +* ``name(pretty=True)`` without a ``PRETTY_NAME`` builds + ``" "`` -- with an empty name that is just + ``" "`` (a leading space), as probed for Oracle Linux. +* ``singular``/``version`` fallbacks: a missing os-release file + yields empty strings everywhere, not exceptions. +""" + +import pytest + +from distro import LinuxDistribution + +UBUNTU = """PRETTY_NAME="Ubuntu 24.04.5 LTS" +NAME="Ubuntu" +VERSION_ID="24.04" +VERSION="24.04.5 LTS (Noble Numbat)" +VERSION_CODENAME=noble +ID=ubuntu +ID_LIKE=debian +""" + +ORACLE = """NAME="Oracle Linux" +VERSION="8.10" +ID="ol" +VERSION_ID="8.10" +ID_LIKE="rhel fedora" +""" + +QUOTED = """NAME='Single Quoted' +ID="double" +VERSION_ID='1.2.3' +PRETTY_NAME="Pretty 'Quoted' Name" +""" + +COMMENTS = """# a comment + +ID=commented +#VERSION_ID="9" +EMPTY= +""" + + +def make(tmp_path, content): + path = tmp_path / "os-release" + path.write_text(content) + return LinuxDistribution( + os_release_file=str(path), + distro_release_file="/nonexistent/nofile", + include_lsb=False, + include_uname=False, + ) + + +def test_ubuntu_full_profile(tmp_path): + d = make(tmp_path, UBUNTU) + assert d.id() == "ubuntu" + assert d.name() == "Ubuntu" + assert d.name(pretty=True) == "Ubuntu 24.04.5 LTS" + assert d.version() == "24.04" + assert d.version(pretty=True) == "24.04 (noble)" + assert d.version_parts() == ("24", "04", "") + assert (d.major_version(), d.minor_version(), d.build_number()) == ("24", "04", "") + assert d.like() == "debian" + assert d.codename() == "noble" + assert d.linux_distribution() == ("Ubuntu", "24.04", "Noble Numbat") + assert d.linux_distribution(full_distribution_name=False) == ("ubuntu", "24.04", "Noble Numbat") + + +def test_ubuntu_os_release_info_dict(tmp_path): + d = make(tmp_path, UBUNTU) + info = d.os_release_info() + assert info["pretty_name"] == "Ubuntu 24.04.5 LTS" + assert info["version_id"] == "24.04" + assert info["codename"] == "noble" + assert info["release_codename"] == "Noble Numbat" + assert info["id_like"] == "debian" + + +def test_oracle_id_is_normalized(tmp_path): + d = make(tmp_path, ORACLE) + assert d.os_release_attr("id") == "ol" + assert d.id() == "oracle" + assert d.name() == "Oracle Linux" + assert d.version() == "8.10" + assert d.like() == "rhel fedora" + assert d.codename() == "" + + +def test_oracle_pretty_name_without_pretty_name_has_leading_space(tmp_path): + d = make(tmp_path, ORACLE) + assert d.name(pretty=True) == " 8.10" + + +def test_quoted_values_are_unquoted(tmp_path): + d = make(tmp_path, QUOTED) + assert d.name() == "Single Quoted" + assert d.id() == "double" + assert d.version() == "1.2.3" + assert d.version_parts() == ("1", "2", "3") + assert d.name(pretty=True) == "Pretty 'Quoted' Name" + + +def test_comments_blank_lines_and_empty_values(tmp_path): + d = make(tmp_path, COMMENTS) + assert d.id() == "commented" + assert d.version() == "" + assert d.os_release_info() == {"id": "commented", "empty": ""} + + +def test_minimal_file_and_missing_file(tmp_path): + d = make(tmp_path, "ID=testos\n") + assert d.id() == "testos" + assert d.name() == "" + assert d.version() == "" + assert d.info() == { + "id": "testos", + "version": "", + "version_parts": {"major": "", "minor": "", "build_number": ""}, + "like": "", + "codename": "", + } + missing = LinuxDistribution( + os_release_file="/nonexistent/os-release", + distro_release_file="/nonexistent/nofile", + include_lsb=False, + include_uname=False, + ) + assert missing.id() == "" + assert missing.info()["id"] == "" + assert missing.version_parts() == ("", "", "") + + +def test_os_release_attr_missing_key_is_empty(tmp_path): + d = make(tmp_path, UBUNTU) + assert d.os_release_attr("nope") == "" + + +def test_parse_os_release_content_directly(tmp_path): + import io + + d = make(tmp_path, UBUNTU) + parsed = d._parse_os_release_content( + io.StringIO('ID=foo\nNAME="Bar Baz"\n# c\nBADLINE\nVERSION_ID=1.0\n') + ) + assert parsed == {"id": "foo", "name": "Bar Baz", "version_id": "1.0"} + + +def test_version_codename_beats_version_parentheses(tmp_path): + content = ( + 'NAME="X"\nID=x\nVERSION_ID="1.0"\n' + 'VERSION="1.0 (FromVersion)"\nVERSION_CODENAME=fromfield\n' + ) + d = make(tmp_path, content) + assert d.codename() == "fromfield" + assert d.os_release_info()["release_codename"] == "FromVersion" + + +def test_empty_version_codename_means_no_codename(tmp_path): + content = 'NAME="X"\nID=x\nVERSION_ID="1.0"\nVERSION_CODENAME=""\n' + d = make(tmp_path, content) + assert d.codename() == "" + + +def test_root_dir_layout(tmp_path): + root = tmp_path / "root" + (root / "etc").mkdir(parents=True) + (root / "etc" / "os-release").write_text(UBUNTU) + d = LinuxDistribution(root_dir=str(root)) + assert d.id() == "ubuntu" + assert d.version() == "24.04" + assert d.os_release_file == str(root / "etc" / "os-release") + + +def test_debian_version_file_via_root_dir(tmp_path): + root = tmp_path / "root" + (root / "etc").mkdir(parents=True) + (root / "etc" / "os-release").write_text('ID=debian\nNAME="Debian"\n') + (root / "etc" / "debian_version").write_text("12.4\n") + d = LinuxDistribution(root_dir=str(root)) + assert d.id() == "debian" + assert d.version() == "12.4"