Source code for tests.test_verify

# Copyright Kevin Deldycke <[email protected]> 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.

"""Tests for `repomatic.release.verify`: native header verification."""

from __future__ import annotations

import sys
from pathlib import Path

import pytest
from click_extra.testing import CliRunner
from extra_platforms import AARCH64, X86_64
from packaging.version import Version

from repomatic.cli.main import repomatic
from repomatic.release.binary import MACHINE_IDS, NUITKA_BUILD_TARGETS, BinaryFormat
from repomatic.release.verify import (
    _elf_info,
    _macho_info,
    _pe_machine,
    verify_binary_arch,
    verify_binary_floor,
)
from tests.test_binary import (
    EM_AARCH64,
    EM_X86_64,
    macho_cpu_type,
    make_elf,
    make_fat,
    make_macho,
    make_pe,
    pe_machine_id,
)


def _machines_for(binary_format: BinaryFormat) -> set[int]:
    """Every machine identifier recorded for one executable format."""
    return {
        machine
        for (candidate, _arch), machine in MACHINE_IDS.items()
        if candidate is binary_format
    }


[docs] @pytest.mark.parametrize( ("target", "payload"), [ ("linux-arm64", make_elf(EM_AARCH64)), ("linux-x64", make_elf(EM_X86_64)), ("macos-arm64", make_macho(macho_cpu_type(AARCH64))), ("macos-x64", make_fat([make_macho(macho_cpu_type(X86_64))])), ("windows-arm64", make_pe(pe_machine_id(AARCH64))), ("windows-x64", make_pe(pe_machine_id(X86_64))), ], ) def test_matching_arch(tmp_path, target, payload): """Binary with matching architecture passes verification.""" binary = tmp_path / "test.bin" binary.write_bytes(payload) # Should not raise. verify_binary_arch(NUITKA_BUILD_TARGETS[target], binary)
[docs] @pytest.mark.parametrize( ("target", "payload"), [ # Wrong architecture, right format. ("linux-arm64", make_elf(EM_X86_64)), ("macos-arm64", make_macho(macho_cpu_type(X86_64))), ("windows-x64", make_pe(pe_machine_id(AARCH64))), # Wrong executable format entirely. ("linux-x64", make_pe(pe_machine_id(X86_64))), ("windows-x64", make_elf(EM_X86_64)), # Not an executable at all. ("linux-x64", b""), ], ) def test_mismatched_arch(tmp_path, target, payload): """Binary with mismatched architecture or format raises ValueError.""" binary = tmp_path / "test.bin" binary.write_bytes(payload) with pytest.raises(ValueError, match="Binary architecture mismatch"): verify_binary_arch(NUITKA_BUILD_TARGETS[target], binary)
[docs] def test_verify_binary_rejects_an_unknown_target(tmp_path): """The command refuses a target outside the build roster, before any read.""" binary = tmp_path / "test.bin" binary.touch() result = CliRunner().invoke( repomatic, ["verify-binary", "--target", "unknown-platform", "--binary", str(binary)], ) assert result.exit_code == 2, result.output assert "unknown-platform" in result.output
[docs] def test_elf_info_without_verneed(tmp_path): """A sectionless ELF reports its machine and no glibc requirement.""" sample = tmp_path / "sample.bin" sample.write_bytes(make_elf(EM_X86_64)) assert _elf_info(sample) == (EM_X86_64, None)
[docs] def test_elf_info_skips_glibc_feature_versions(tmp_path): """A feature entry like `GLIBC_ABI_DT_RELR` sets no glibc floor. Files linked with packed relocations require it, like the Python interpreter Arch Linux ships. """ sample = tmp_path / "sample.bin" versions = ("GLIBC_2.34", "GLIBC_ABI_DT_RELR", "GLIBC_2.38") sample.write_bytes(make_elf(EM_X86_64, versions)) assert _elf_info(sample) == (EM_X86_64, "2.38")
[docs] def test_elf_info_orders_glibc_versions_numerically(tmp_path): """`2.4` is older than `2.38`, which a comparison of strings gets wrong.""" sample = tmp_path / "sample.bin" sample.write_bytes(make_elf(EM_X86_64, ("GLIBC_2.4", "GLIBC_2.38", "GLIBC_2.17"))) assert _elf_info(sample) == (EM_X86_64, "2.38")
[docs] @pytest.mark.parametrize( ("wide", "big_endian"), [(True, True), (False, False), (False, True)], ids=["elf64-be", "elf32-le", "elf32-be"], ) def test_elf_info_reads_both_classes_and_byte_orders(tmp_path, wide, big_endian): """The parser follows the class and byte order the identification declares.""" sample = tmp_path / "sample.bin" sample.write_bytes( make_elf(EM_AARCH64, ("GLIBC_2.17",), wide=wide, big_endian=big_endian) ) assert _elf_info(sample) == (EM_AARCH64, "2.17")
[docs] def test_elf_info_rejects_a_truncated_header(tmp_path): """A file cut inside its ELF header is reported, not read as sectionless.""" sample = tmp_path / "sample.bin" sample.write_bytes(make_elf(EM_X86_64)[:40]) with pytest.raises(ValueError, match="Truncated ELF header"): _elf_info(sample)
[docs] def test_macho_info_fat_slices(tmp_path): """CPU types accumulate and the floor maxes across fat slices.""" sample = tmp_path / "sample.bin" sample.write_bytes( make_fat([ make_macho(macho_cpu_type(AARCH64), "12.4"), make_macho(macho_cpu_type(X86_64), "10.15"), ]) ) cpu_types, floor = _macho_info(sample) assert cpu_types == _machines_for(BinaryFormat.MACHO) assert floor == "12.4"
[docs] def test_pe_machine_parsing(tmp_path): """The COFF machine field is read from PE headers, None elsewhere.""" sample = tmp_path / "sample.exe" sample.write_bytes(make_pe(pe_machine_id(AARCH64))) assert _pe_machine(sample) == pe_machine_id(AARCH64) sample.write_bytes(b"MZ not a real PE") assert _pe_machine(sample) is None
[docs] def test_running_interpreter_parses(): """The parsers agree with the running interpreter's own executable.""" executable = Path(sys.executable) if sys.platform.startswith("linux"): machine, floor = _elf_info(executable) assert machine in _machines_for(BinaryFormat.ELF) assert floor is None or Version(floor) >= Version("2") elif sys.platform == "darwin": cpu_types, floor = _macho_info(executable) assert cpu_types & _machines_for(BinaryFormat.MACHO) assert floor is not None elif sys.platform == "win32": assert _pe_machine(executable) in _machines_for(BinaryFormat.PE)
[docs] def test_floor_within_bounds_macos(tmp_path): """Files at or below the declared macOS floor pass verification.""" binary = tmp_path / "test.bin" binary.write_bytes(make_macho(macho_cpu_type(AARCH64), "11.0")) dist = tmp_path / "app.dist" dist.mkdir() (dist / "lib.dylib").write_bytes(make_macho(macho_cpu_type(AARCH64), "10.9")) # Should not raise. verify_binary_floor(NUITKA_BUILD_TARGETS["macos-arm64"], binary, [dist])
[docs] def test_floor_exceeded_macos(tmp_path): """A dist file above the declared macOS floor fails verification.""" binary = tmp_path / "test.bin" binary.write_bytes(make_macho(macho_cpu_type(AARCH64), "11.0")) dist = tmp_path / "app.dist" dist.mkdir() (dist / "lib.dylib").write_bytes(make_macho(macho_cpu_type(AARCH64), "26.0")) with pytest.raises(ValueError, match="OS floor exceeded") as excinfo: verify_binary_floor(NUITKA_BUILD_TARGETS["macos-arm64"], binary, [dist]) assert "26.0" in str(excinfo.value) assert "lib.dylib" in str(excinfo.value)
[docs] @pytest.mark.parametrize( ("measured", "passes"), [ ("2.17", True), ("2.28", True), ("2.39", False), ], ) def test_floor_linux(tmp_path, measured, passes): """Linux floors compare the measured glibc requirement to the declared floor.""" binary = tmp_path / "test.bin" binary.write_bytes(make_elf(EM_X86_64, (f"GLIBC_{measured}",))) target = NUITKA_BUILD_TARGETS["linux-x64"] if passes: verify_binary_floor(target, binary) else: with pytest.raises(ValueError, match="OS floor exceeded"): verify_binary_floor(target, binary)
[docs] def test_floor_windows_is_documentation_only(tmp_path): """Windows targets have no enforceable floor and always pass.""" binary = tmp_path / "test.exe" binary.write_bytes(make_pe(pe_machine_id(X86_64))) # Should not raise, and must not even look at the headers. verify_binary_floor(NUITKA_BUILD_TARGETS["windows-x64"], binary)