Source code for tests.test_detection

# Copyright Kevin Deldycke <kevin@deldycke.com> and contributors.
#
# This program 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 2
# of the License, or (at your option) any later version.
#
# This program 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 this program; if not, write to the Free Software
# Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.

from __future__ import annotations

import ast
import functools
import inspect
import re
from itertools import chain
from pathlib import Path

import pytest

import extra_platforms
from extra_platforms import (
    ALL_GROUPS,
    ALL_TRAITS,
    UNKNOWN,
    Group,
    Trait,
    invalidate_caches,
    is_aarch64,
    is_arm,
    is_github_ci,
    is_gitlab_ci,
    is_windows,
    is_x86_64,
)
from extra_platforms import detection as detection_module


[docs] @pytest.mark.parametrize( "obj", list(chain(ALL_TRAITS, ALL_GROUPS)), ids=lambda obj: obj.id ) def test_detection_trait_functions(obj: Trait | Group): # All traits must implement a real function in the detection module. if isinstance(obj, Trait): check_func = getattr(detection_module, obj.detection_func_id) assert hasattr(extra_platforms, obj.detection_func_id) # current property is aligned with detection function. assert check_func() == obj.current # All groups' detection functions are dynamically generated, but still must exist. else: assert not hasattr(detection_module, obj.detection_func_id) check_func = getattr(extra_platforms, obj.detection_func_id) # Groups do not have a "current" property. assert not hasattr(obj, "current") assert isinstance(check_func, functools._lru_cache_wrapper) assert isinstance(check_func(), bool) # Ensure the detection function name is lowercase. assert obj.detection_func_id.islower() # Verify the docstring contains an rST link to the symbol. # Format: either old style `SYMBOL_ID <...#extra_platforms.SYMBOL_ID>`_ # or new style Sphinx role :data:`~extra_platforms.SYMBOL_ID` # or short-path style :data:`~SYMBOL_ID` assert check_func.__doc__ is not None and re.search( rf":data:`~(?:extra_platforms\.)?{re.escape(obj.symbol_id)}`", check_func.__doc__, )
[docs] def test_detection_heuristics_sorting(): """Detection heuristics must be sorted within each section.""" detection_path = Path(inspect.getfile(detection_module)) tree = ast.parse(detection_path.read_bytes()) source_lines = detection_path.read_text(encoding="utf-8").splitlines() # Find section boundaries by looking for comment markers. arch_section_start = None platform_section_start = None ci_section_start = None for i, line in enumerate(source_lines, start=1): if "Architecture detection heuristics" in line: arch_section_start = i elif "Platform detection heuristics" in line: platform_section_start = i elif "CI/CD detection heuristics" in line: ci_section_start = i assert arch_section_start is not None, "Architecture section not found" assert platform_section_start is not None, "Platform section not found" assert ci_section_start is not None, "CI/CD section not found" assert arch_section_start < platform_section_start assert platform_section_start < ci_section_start # Collect heuristic functions by section. all_heuristic_ids = [] arch_heuristics = [] platform_heuristics = [] ci_heuristics = [] for node in tree.body: if isinstance(node, ast.FunctionDef) and node.name.startswith("is_"): func_id = node.name assert func_id.islower() all_heuristic_ids.append(func_id) line_no = node.lineno if line_no >= arch_section_start and line_no < platform_section_start: arch_heuristics.append(func_id) elif line_no >= platform_section_start and line_no < ci_section_start: platform_heuristics.append(func_id) elif line_no >= ci_section_start: ci_heuristics.append(func_id) # Check there is no extra "is_" function. # All traits, including UNKNOWN traits, must have detection functions. assert {f"is_{p.id}" for p in ALL_TRAITS} == set(all_heuristic_ids) # We only allow one generic "is_unknown*()" detection heuristics per category. for heuristics in [arch_heuristics, platform_heuristics, ci_heuristics]: non_generic_func_ids = [ func_id for func_id in heuristics if func_id.startswith("is_unknown") ] assert len(non_generic_func_ids) <= 1, ( f"More than 1 is_unknown*() detection heuristics defined in {heuristics!r}" ) if len(non_generic_func_ids): assert non_generic_func_ids[-1].startswith("is_unknown") # Verify each category is sorted alphabetically within itself. assert non_generic_func_ids == sorted(non_generic_func_ids), ( f"Heuristics are not sorted: {non_generic_func_ids!r}" )
[docs] def test_is_arm_depends_on_arm_variants(): """Test that is_arm() correctly calls ARM variant detection functions.""" # Clear caches to ensure fresh evaluation. invalidate_caches() # Call is_arm() to ensure it internally calls the ARM variant functions. result = is_arm() # We can't easily test the internal calls without mocking, # but we can verify the function returns a boolean. assert isinstance(result, bool) invalidate_caches()
[docs] def test_detection_functions_cached(): """Test that detection functions are cached with @cache decorator.""" # Clear caches first. invalidate_caches() # Call each function twice. _ = is_aarch64() _ = is_aarch64() _ = is_windows() _ = is_windows() _ = is_x86_64() _ = is_x86_64() # Check that cache_info shows hits. assert is_aarch64.cache_info().hits >= 1 assert is_windows.cache_info().hits >= 1 assert is_x86_64.cache_info().hits >= 1 invalidate_caches()
[docs] def test_environment_variable_ci_detection(monkeypatch): """Test CI detection based on environment variables.""" invalidate_caches() # Mock GitHub CI environment variable. monkeypatch.setenv("GITHUB_ACTIONS", "true") invalidate_caches() assert is_github_ci() is True # Remove GitHub CI and add GitLab CI. monkeypatch.delenv("GITHUB_ACTIONS", raising=False) monkeypatch.setenv("GITLAB_CI", "true") invalidate_caches() assert is_gitlab_ci() is True # Clean up. monkeypatch.delenv("GITLAB_CI", raising=False) invalidate_caches()
[docs] def test_detection_no_circular_dependencies(): """Test that detection functions can all be called without circular dependency issues.""" invalidate_caches() # Call all trait detection functions. for trait in ALL_TRAITS: # Access the current property, which calls the detection function. _ = trait.current # If no exception was raised, there are no circular dependencies. invalidate_caches()