Source code for tests.test_platform_info

# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
"""Test os-release parsing and platform information gathering."""

from __future__ import annotations

import io
import os
import shutil
import subprocess

import pytest

from extra_platforms.platform_info import (
    _hostnamectl_os_release,
    _parse_cpe_name,
    _parse_hostnamectl_content,
    _parse_os_release,
    _parse_os_release_content,
    invalidate_os_release_cache,
    linux_info,
    os_release_id,
)

HOSTNAMECTL_CLOUDLINUX = """\
   Static hostname: web.illinois.edu
         Icon name: computer-vm
           Chassis: vm
           Boot ID: 5b5b8c522ccd4974808f192aea001491
    Virtualization: kvm
  Operating System: CloudLinux 7.6 (Vladimir Lyakhov)
       CPE OS Name: cpe:/o:cloudlinux:cloudlinux:7.6:GA:server
            Kernel: Linux 3.10.0-962.3.2.lve1.5.24.9.el7.x86_64
      Architecture: x86-64
"""
"""Status output of the CloudLinux VM reported in
[`python-distro/distro#240`](https://github.com/python-distro/distro/issues/240),
where no os-release file is readable.
"""


[docs] @pytest.fixture def fresh_os_release_caches(): """Clear derived os-release caches around a test patching ``_parse_os_release``. Only the caches of ``os_release_id()`` and ``linux_info()`` are cleared: ``_parse_os_release()`` itself is replaced by ``monkeypatch``, whose teardown runs after this fixture's, so the patched stand-in (a plain lambda) has no cache to clear. """ os_release_id.cache_clear() linux_info.cache_clear() yield os_release_id.cache_clear() linux_info.cache_clear()
[docs] @pytest.fixture def fresh_hostnamectl_caches(): """Clear the caches a `hostnamectl`-driving test fills. ``_parse_os_release()`` memoizes whatever the fallback answered, so it is cleared next to ``_hostnamectl_os_release()``. Both are cleared on entry and exit, which ``fresh_os_release_caches`` cannot do for ``_parse_os_release()``: no test using this fixture replaces either function, so the real ``cache_clear()`` stays reachable on both sides. """ _hostnamectl_os_release.cache_clear() _parse_os_release.cache_clear() yield _hostnamectl_os_release.cache_clear() _parse_os_release.cache_clear()
[docs] @pytest.mark.parametrize( ("content", "expected"), ( pytest.param("", {}, id="empty_input"), pytest.param( "# This is a comment\n\nID=fedora\n", {"id": "fedora"}, id="comments_and_blank_lines", ), pytest.param( 'PRETTY_NAME="Ubuntu 22.04.3 LTS"\n' 'NAME="Ubuntu"\n' 'VERSION_ID="22.04"\n' 'VERSION="22.04.3 LTS (Jammy Jellyfish)"\n' "VERSION_CODENAME=jammy\n" "ID=ubuntu\n" "ID_LIKE=debian\n", { "pretty_name": "Ubuntu 22.04.3 LTS", "name": "Ubuntu", "version_id": "22.04", "version": "22.04.3 LTS (Jammy Jellyfish)", "version_codename": "jammy", "id": "ubuntu", "id_like": "debian", }, id="typical_ubuntu", ), pytest.param( 'NAME="Fedora Linux"\n' 'VERSION="39 (Workstation Edition)"\n' "ID=fedora\n" "VERSION_ID=39\n" 'PRETTY_NAME="Fedora Linux 39 (Workstation Edition)"\n', { "name": "Fedora Linux", "version": "39 (Workstation Edition)", "id": "fedora", "version_id": "39", "pretty_name": "Fedora Linux 39 (Workstation Edition)", # Codename extracted from the VERSION parenthetical. "version_codename": "Workstation Edition", }, id="typical_fedora", ), pytest.param( 'ID="centos"\nNAME="CentOS Stream"\n', {"id": "centos", "name": "CentOS Stream"}, id="quoted_values", ), pytest.param( 'VERSION="10, Buster"\nID=debian\n', { "version": "10, Buster", "id": "debian", # Codename extracted from the VERSION comma separator. "version_codename": "Buster", }, id="codename_from_version_comma", ), pytest.param( 'VERSION="22.04.3 LTS (Jammy Jellyfish)"\n' "VERSION_CODENAME=jammy\n" "ID=ubuntu\n", { "version": "22.04.3 LTS (Jammy Jellyfish)", # VERSION_CODENAME takes precedence over VERSION extraction. "version_codename": "jammy", "id": "ubuntu", }, id="version_codename_precedence", ), pytest.param( "ID=linuxmint\nUBUNTU_CODENAME=focal\n", { "id": "linuxmint", "ubuntu_codename": "focal", # UBUNTU_CODENAME is a fallback for VERSION_CODENAME. "version_codename": "focal", }, id="ubuntu_codename_fallback", ), pytest.param( "ID=test\nPRETTY_NAME=Test\n", {"id": "test", "pretty_name": "Test"}, id="keys_lowercased", ), ), ) def test_parse_os_release_content(content, expected): """Parsing os-release content produces the exact expected mapping.""" assert _parse_os_release_content(io.StringIO(content)) == expected
[docs] @pytest.mark.parametrize( ("raw_id", "expected"), ( ("ol", "ol"), ("opensuse-leap", "opensuse-leap"), ("opensuse-slowroll", "opensuse-slowroll"), ("opensuse-tumbleweed", "opensuse-tumbleweed"), ("Ubuntu", "ubuntu"), ("fedora", "fedora"), ), ) def test_os_release_id_sanitization( raw_id, expected, monkeypatch, fresh_os_release_caches ): """IDs are lowercased but otherwise preserved, sub-variants included.""" monkeypatch.setattr( "extra_platforms.platform_info._parse_os_release", lambda: {"id": raw_id}, ) assert os_release_id() == expected
[docs] def test_os_release_id_empty(monkeypatch, fresh_os_release_caches): """Return empty string when no ID field is present.""" monkeypatch.setattr( "extra_platforms.platform_info._parse_os_release", dict, ) assert os_release_id() == ""
[docs] def test_linux_info_structure(monkeypatch, fresh_os_release_caches): """Return dict has expected keys and version_parts structure.""" monkeypatch.setattr( "extra_platforms.platform_info._parse_os_release", lambda: {"id": "ubuntu", "version_id": "22.04.3", "id_like": "debian"}, ) info = linux_info() assert info["id"] == "ubuntu" assert info["version"] == "22.04.3" assert info["version_parts"] == { "major": "22", "minor": "04", "build_number": "3", } assert info["like"] == "debian"
[docs] def test_linux_info_empty_version(monkeypatch, fresh_os_release_caches): """Version parts are None when version_id is absent.""" monkeypatch.setattr( "extra_platforms.platform_info._parse_os_release", lambda: {"id": "arch"}, ) info = linux_info() assert info["version"] is None assert info["version_parts"] == { "major": None, "minor": None, "build_number": None, } assert info["like"] is None assert info["codename"] is None
[docs] @pytest.mark.parametrize( ("cpe_name", "distro_id"), ( ("cpe:/o:almalinux:almalinux:10::baseos", "almalinux"), ("cpe:/o:alt:server:10.1", "altlinux"), ("cpe:/o:amazon:linux:2016.03:ga", "amzn"), ("cpe:2.3:o:amazon:amazon_linux:2023", "amzn"), ("cpe:/o:centos:centos:7", "centos"), ("cpe:/o:cloudlinux:cloudlinux:7.3:GA:server", "cloudlinux"), ("cpe:/o:fedoraproject:fedora:19", "fedora"), ("cpe:/o:ibm:kvmibm:1.1.1", "kvmibm"), ("cpe:/o:opensuse:leap:15.2", "opensuse-leap"), ("cpe:/o:opensuse:opensuse:42.1", "opensuse"), ("cpe:/o:oracle:linux:7:5:server", "ol"), ("cpe:/o:redhat:enterprise_linux:9::baseos", "rhel"), ("cpe:/o:rocky:rocky:8.4:GA", "rocky"), ("cpe:/o:scientificlinux:scientificlinux:7.2:GA", "rhel"), ("cpe:/o:slackware:slackware_linux:14.1", "slackware"), ("cpe:/o:suse:sles:12:sp1", "sles"), ), ) def test_parse_cpe_name_matches_os_release_id(cpe_name, distro_id): """A CPE name derives the `ID` the same system declares in its os-release. Both fields of each case are read from one real os-release file, as collected in [python-distro's test resources](https://github.com/python-distro/distro/tree/master/tests/resources/distros). That covers every ``vendor:product`` pair those fixtures carry, so a new entry in ``CPE_ID_OVERRIDES`` that contradicts a released distribution fails here. """ assert _parse_cpe_name(cpe_name)["id"] == distro_id
[docs] @pytest.mark.parametrize( ("cpe_name", "expected"), ( pytest.param( "cpe:/o:cloudlinux:cloudlinux:7.6:GA:server", { "cpe_name": "cpe:/o:cloudlinux:cloudlinux:7.6:GA:server", "id": "cloudlinux", "version_id": "7.6", }, id="uri_binding_drops_trailing_components", ), pytest.param( "cpe:2.3:o:amazon:amazon_linux:2023", { "cpe_name": "cpe:2.3:o:amazon:amazon_linux:2023", "id": "amzn", "version_id": "2023", }, id="formatted_string_binding", ), pytest.param( "cpe:/o:vendor:product", {"cpe_name": "cpe:/o:vendor:product", "id": "product"}, id="no_version", ), pytest.param( "cpe:/o:vendor:product:*", {"cpe_name": "cpe:/o:vendor:product:*", "id": "product"}, id="any_version_placeholder", ), pytest.param( "cpe:/o:vendor:-:1.0", {"cpe_name": "cpe:/o:vendor:-:1.0", "version_id": "1.0"}, id="not_applicable_product_placeholder", ), pytest.param("cpe:/a:vendor:product:1.0", {}, id="application_not_os"), pytest.param("cpe:/h:vendor:product:1.0", {}, id="hardware_not_os"), pytest.param("", {}, id="empty_input"), pytest.param("CloudLinux 7.6", {}, id="not_a_cpe_name"), ), ) def test_parse_cpe_name(cpe_name, expected): """Parsing a CPE name produces the exact expected mapping.""" assert _parse_cpe_name(cpe_name) == expected
[docs] @pytest.mark.parametrize( ("content", "expected"), ( pytest.param("", {}, id="empty_input"), pytest.param( HOSTNAMECTL_CLOUDLINUX, { "cpe_name": "cpe:/o:cloudlinux:cloudlinux:7.6:GA:server", "id": "cloudlinux", "version_id": "7.6", "pretty_name": "CloudLinux 7.6 (Vladimir Lyakhov)", # Codename extracted from the pretty name parenthetical. "version_codename": "Vladimir Lyakhov", }, id="cloudlinux_vm", ), pytest.param( " Operating System: Ubuntu 22.04.3 LTS\n" " Kernel: Linux 6.5.0-14-generic\n", {"pretty_name": "Ubuntu 22.04.3 LTS"}, id="no_cpe_line", ), pytest.param( " CPE OS Name: cpe:/o:fedoraproject:fedora:39\n", { "cpe_name": "cpe:/o:fedoraproject:fedora:39", "id": "fedora", "version_id": "39", }, id="no_pretty_name_line", ), pytest.param( " Static hostname: fridge\n" " Chassis: container\n" " Kernel: Linux 6.5.0-14-generic\n", {}, id="host_lines_only", ), pytest.param( "Failed to query system properties: Connection refused\n", {}, id="bus_error_message", ), ), ) def test_parse_hostnamectl_content(content, expected): """Parsing hostnamectl output produces the exact expected mapping.""" assert _parse_hostnamectl_content(content.splitlines()) == expected
def _fake_run(stdout): """Build a `subprocess.run` stand-in returning ``stdout``.""" def run(*args, **kwargs): return subprocess.CompletedProcess(args, 0, stdout=stdout, stderr="") return run
[docs] def test_hostnamectl_os_release(monkeypatch, fresh_hostnamectl_caches): """A reachable systemd bus rebuilds the os-release fields.""" monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/hostnamectl") monkeypatch.setattr(subprocess, "run", _fake_run(HOSTNAMECTL_CLOUDLINUX)) assert _hostnamectl_os_release() == { "cpe_name": "cpe:/o:cloudlinux:cloudlinux:7.6:GA:server", "id": "cloudlinux", "version_id": "7.6", "pretty_name": "CloudLinux 7.6 (Vladimir Lyakhov)", "version_codename": "Vladimir Lyakhov", }
[docs] def test_hostnamectl_os_release_skips_missing_binary( monkeypatch, fresh_hostnamectl_caches ): """No subprocess is spawned on a system shipping no `hostnamectl`.""" def forbidden_run(*args, **kwargs): raise AssertionError("hostnamectl must not be spawned when absent") monkeypatch.setattr(shutil, "which", lambda name: None) monkeypatch.setattr(subprocess, "run", forbidden_run) assert _hostnamectl_os_release() == {}
[docs] @pytest.mark.parametrize( "error", ( pytest.param(FileNotFoundError("hostnamectl"), id="binary_missing"), pytest.param(PermissionError("hostnamectl"), id="not_executable"), pytest.param( subprocess.CalledProcessError(1, "hostnamectl"), id="non_zero_exit" ), pytest.param( subprocess.TimeoutExpired("hostnamectl", 2), id="unresponsive_bus" ), ), ) def test_hostnamectl_os_release_degrades(error, monkeypatch, fresh_hostnamectl_caches): """Every failure to reach the bus degrades to an empty result.""" def failing_run(*args, **kwargs): raise error monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/hostnamectl") monkeypatch.setattr(subprocess, "run", failing_run) assert _hostnamectl_os_release() == {}
[docs] def test_parse_os_release_falls_back_to_hostnamectl( monkeypatch, fresh_hostnamectl_caches, fresh_os_release_caches ): """A system hiding both os-release files is identified through the bus.""" monkeypatch.setattr(os.path, "isfile", lambda path: False) monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/hostnamectl") monkeypatch.setattr(subprocess, "run", _fake_run(HOSTNAMECTL_CLOUDLINUX)) assert _parse_os_release()["id"] == "cloudlinux" assert os_release_id() == "cloudlinux" assert linux_info() == { "id": "cloudlinux", "version": "7.6", "version_parts": {"major": "7", "minor": "6", "build_number": None}, "like": None, "codename": "Vladimir Lyakhov", }
[docs] def test_parse_os_release_prefers_files_over_hostnamectl( monkeypatch, fresh_hostnamectl_caches, fresh_os_release_caches ): """A readable os-release file is never second-guessed through the bus.""" def forbidden_run(*args, **kwargs): raise AssertionError("hostnamectl must not be spawned when a file is readable") monkeypatch.setattr(os.path, "isfile", lambda path: path == "/etc/os-release") monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/hostnamectl") monkeypatch.setattr(subprocess, "run", forbidden_run) monkeypatch.setattr( "extra_platforms.platform_info.open", lambda *args, **kwargs: io.StringIO("ID=ubuntu\n"), raising=False, ) assert _parse_os_release() == {"id": "ubuntu"}
[docs] def test_invalidate_os_release_cache_clears_hostnamectl( monkeypatch, fresh_hostnamectl_caches ): """The hostnamectl result is re-read after a cache invalidation.""" monkeypatch.setattr(shutil, "which", lambda name: "/usr/bin/hostnamectl") monkeypatch.setattr(subprocess, "run", _fake_run(HOSTNAMECTL_CLOUDLINUX)) assert _hostnamectl_os_release()["id"] == "cloudlinux" monkeypatch.setattr( subprocess, "run", _fake_run(" Operating System: Fedora Linux 39\n"), ) # The cached CloudLinux answer is still served before invalidation. assert _hostnamectl_os_release()["id"] == "cloudlinux" invalidate_os_release_cache() assert _hostnamectl_os_release() == {"pretty_name": "Fedora Linux 39"}