Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
173 changes: 173 additions & 0 deletions tests/test_cleanup.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,173 @@
"""Tests for scripts/cleanup.py"""

import os
import sys
import tempfile
from pathlib import Path


def _import_cleanup():
"""Import cleanup module from scripts directory."""
import importlib.util
spec = importlib.util.spec_from_file_location(
"cleanup",
os.path.join(os.path.dirname(__file__), "..", "scripts", "cleanup.py"),
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod


class TestParseNEVR:
"""Test RPM filename parsing (name-epoch:version-release)."""

def test_rpm_with_epoch(self):
"""RPM with epoch: name-epoch:version-release.arch.rpm."""
mod = _import_cleanup()
result = mod.parse_nevr("glibc-2:2.38-11.fc40.x86_64.rpm")
assert result == ("glibc", "2", "2.38", "11.fc40.x86_64"), (
f"Unexpected result: {result}"
)

def test_rpm_with_epoch_kernel(self):
"""Kernel RPM with epoch."""
mod = _import_cleanup()
result = mod.parse_nevr("kernel-6:6.8.5-301.fc40.x86_64.rpm")
assert result == ("kernel", "6", "6.8.5", "301.fc40.x86_64"), (
f"Unexpected result: {result}"
)

def test_rpm_with_multi_digit_epoch(self):
"""RPM with multi-digit epoch."""
mod = _import_cleanup()
result = mod.parse_nevr("python3-10:3.12.2-1.fc40.x86_64.rpm")
assert result == ("python3", "10", "3.12.2", "1.fc40.x86_64"), (
f"Unexpected result: {result}"
)

def test_rpm_without_epoch(self):
"""RPM without epoch falls back to raw filename, epoch='0'."""
mod = _import_cleanup()
result = mod.parse_nevr("bash-5.2.15-2.fc40.x86_64.rpm")
assert result == ("bash-5.2.15-2.fc40.x86_64.rpm", "0", "", ""), (
f"Unexpected result: {result}"
)

def test_non_rpm_filename(self):
"""Non-RPM filename falls back to raw name, epoch='0'."""
mod = _import_cleanup()
result = mod.parse_nevr("source.tar.gz")
assert result == ("source.tar.gz", "0", "", ""), (
f"Unexpected result: {result}"
)

def test_rpm_name_with_dots(self):
"""RPM name containing dots still parses correctly."""
mod = _import_cleanup()
result = mod.parse_nevr("lib.python3-1:3.0-1.fc40.x86_64.rpm")
# Greedy (.+) in name + version, but epoch digit group anchors
assert isinstance(result, tuple) and len(result) == 4
assert result[1] == "1" # epoch
assert result[3] != "" # release present


class TestGetRpmVersionSortKey:
"""Test RPM version sort key generation."""

def test_higher_version_sorts_after_lower(self):
"""Higher version (2.39 > 2.38) should produce a larger sort key."""
mod = _import_cleanup()
newer = mod.get_rpm_version_sort_key("glibc-2:2.39-1.fc40.x86_64.rpm")
older = mod.get_rpm_version_sort_key("glibc-2:2.38-1.fc40.x86_64.rpm")
assert newer > older, (
f"Expected newer version to sort after older: {newer} <= {older}"
)

def test_same_filename_produces_same_key(self):
"""Identical filenames must produce identical sort keys."""
mod = _import_cleanup()
k1 = mod.get_rpm_version_sort_key("glibc-2:2.38-1.fc40.x86_64.rpm")
k2 = mod.get_rpm_version_sort_key("glibc-2:2.38-1.fc40.x86_64.rpm")
assert k1 == k2, (
f"Expected identical keys for same filename: {k1} != {k2}"
)

def test_higher_release_sorts_after_lower(self):
"""Higher release (11 > 10) produces a larger sort key."""
mod = _import_cleanup()
higher = mod.get_rpm_version_sort_key("glibc-2:2.38-11.fc40.x86_64.rpm")
lower = mod.get_rpm_version_sort_key("glibc-2:2.38-10.fc40.x86_64.rpm")
assert higher > lower, (
f"Expected higher release to sort after lower: {higher} <= {lower}"
)

def test_fallback_no_epoch_produces_tuple(self):
"""RPM without epoch produces a valid sortable tuple."""
mod = _import_cleanup()
key = mod.get_rpm_version_sort_key("bash-5.2.15-2.fc40.x86_64.rpm")
assert isinstance(key, tuple) and len(key) >= 3

def test_different_packages_different_keys(self):
"""Different package names produce different sort keys."""
mod = _import_cleanup()
k1 = mod.get_rpm_version_sort_key("glibc-1:2.38-1.fc40.x86_64.rpm")
k2 = mod.get_rpm_version_sort_key("bash-1:5.2-1.fc40.x86_64.rpm")
assert k1 != k2

def test_different_epoch_sorts_correctly(self):
"""Higher epoch sorts after lower, even with same version."""
mod = _import_cleanup()
higher_epoch = mod.get_rpm_version_sort_key("glibc-2:2.38-1.fc40.x86_64.rpm")
lower_epoch = mod.get_rpm_version_sort_key("glibc-1:2.38-1.fc40.x86_64.rpm")
assert higher_epoch > lower_epoch, (
f"Expected higher epoch to sort after lower: {higher_epoch} <= {lower_epoch}"
)


class TestCleanupLocal:
"""Test local directory cleanup logic."""

def _create_rpm(self, directory: Path, name: str) -> Path:
"""Create a dummy RPM file with the given name."""
rpm_path = directory / name
rpm_path.write_text("dummy rpm content")
return rpm_path

def test_empty_directory_no_crash(self):
"""Empty directory must not cause errors."""
mod = _import_cleanup()
with tempfile.TemporaryDirectory() as tmpdir:
# Should not raise
mod.cleanup_local(tmpdir, max_versions=3, dry_run=False)

def test_dry_run_preserves_files(self):
"""Dry run must not delete any files."""
mod = _import_cleanup()
with tempfile.TemporaryDirectory() as tmpdir:
d = Path(tmpdir)
self._create_rpm(d, "glibc-2:2.38-10.fc40.x86_64.rpm")
self._create_rpm(d, "glibc-2:2.38-11.fc40.x86_64.rpm")

mod.cleanup_local(str(d), max_versions=1, dry_run=True)

remaining = list(d.glob("*.rpm"))
assert len(remaining) == 2, (
f"Expected all files preserved in dry run, got {len(remaining)}"
)

def test_live_mode_no_crash(self):
"""Live mode must not crash and should leave files intact when each has unique filename."""
mod = _import_cleanup()
with tempfile.TemporaryDirectory() as tmpdir:
d = Path(tmpdir)
self._create_rpm(d, "glibc-2:2.38-10.fc40.x86_64.rpm")
self._create_rpm(d, "glibc-2:2.38-11.fc40.x86_64.rpm")

# Each unique filename is its own group of 1 version,
# so max_versions=1 keeps everything
mod.cleanup_local(str(d), max_versions=1, dry_run=False)

remaining = list(d.glob("*.rpm"))
assert len(remaining) == 2, (
f"Expected 2 files (each unique filename = own group), got {len(remaining)}"
)
184 changes: 184 additions & 0 deletions tests/test_copr_build_chain.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,184 @@
"""Tests for scripts/copr-build-chain.py

Tests the core pure-logic functions (resolve_pkg, get_rpm_name) using
temporary spec files and directories. Avoids testing functions that
call COPR CLI or external build infrastructure.
"""

import os
import sys
import tempfile
from pathlib import Path


def _import_module():
"""Import copr-build-chain module from scripts directory."""
import importlib.util
spec = importlib.util.spec_from_file_location(
"copr_build_chain",
os.path.join(os.path.dirname(__file__), "..", "scripts", "copr-build-chain.py"),
)
mod = importlib.util.module_from_spec(spec)
spec.loader.exec_module(mod)
return mod


def _create_spec(tmpdir: Path, filename: str, name: str = None) -> Path:
"""Create a minimal .spec file returning the path."""
if name is None:
name = Path(filename).stem
spec_path = tmpdir / filename
spec_path.write_text(
f"Name: {name}\n"
f"Version: 1.0\n"
f"Release: 1%{{?dist}}\n"
f"License: MIT\n"
f"Summary: Test package\n"
f"%description\nTest description.\n"
)
return spec_path


class TestGetRpmName:
"""Test extraction of RPM Name from spec files."""

def test_basic_spec(self):
"""Standard spec file with Name field."""
mod = _import_module()
with tempfile.TemporaryDirectory() as tmpdir:
spec = _create_spec(Path(tmpdir), "glib2.spec", name="glib2")
result = mod.get_rpm_name(str(spec))
assert result == "glib2", f"Expected 'glib2', got {result!r}"

def test_spec_with_dashes(self):
"""Spec with name containing dashes."""
mod = _import_module()
with tempfile.TemporaryDirectory() as tmpdir:
spec = _create_spec(Path(tmpdir), "gnome-shell.spec", name="gnome-shell")
result = mod.get_rpm_name(str(spec))
assert result == "gnome-shell", f"Expected 'gnome-shell', got {result!r}"

def test_spec_with_plus_in_name(self):
"""Spec with name containing plus sign."""
mod = _import_module()
with tempfile.TemporaryDirectory() as tmpdir:
spec = _create_spec(Path(tmpdir), "gtk4.spec", name="gtk4+extra")
result = mod.get_rpm_name(str(spec))
assert result == "gtk4+extra", f"Expected 'gtk4+extra', got {result!r}"

def test_missing_spec_file(self):
"""Missing spec file raises FileNotFoundError (no graceful handling yet)."""
mod = _import_module()
import pytest
with pytest.raises(FileNotFoundError):
mod.get_rpm_name("/nonexistent/path/pkg.spec")

def test_spec_with_extra_whitespace(self):
"""Spec Name field with extra whitespace is stripped."""
mod = _import_module()
with tempfile.TemporaryDirectory() as tmpdir:
spec = Path(tmpdir) / "mutter.spec"
spec.write_text("Name:\tmutter\nVersion: 42.0\n")
result = mod.get_rpm_name(str(spec))
assert result == "mutter", f"Expected 'mutter', got {result!r}"


class TestResolvePkg:
"""Test package resolution from path/spec_override/copr_name."""

def test_copr_name_override(self):
"""copr_name_override bypasses spec lookup entirely."""
mod = _import_module()
result = mod.resolve_pkg(
pkg_path="/some/path",
spec_override=None,
copr_name_override="gnome-shell",
)
assert result == ("gnome-shell", None, "gnome-shell"), (
f"Unexpected result: {result}"
)

def test_spec_override_bootstrap(self):
"""spec_override derives COPR name from spec filename stem."""
mod = _import_module()
with tempfile.TemporaryDirectory() as tmpdir:
d = Path(tmpdir)
_create_spec(d, "glib2-bootstrap.spec", name="glib2")
result = mod.resolve_pkg(str(d), spec_override="glib2-bootstrap.spec")
# copr_pkg_name = stem of spec_override = "glib2-bootstrap"
assert result is not None, "Expected a result, got None"
copr_name, spec_file, rpm_name = result
assert copr_name == "glib2-bootstrap", (
f"Expected COPR name 'glib2-bootstrap', got {copr_name!r}"
)
assert spec_file is not None and spec_file.endswith("glib2-bootstrap.spec")
assert rpm_name == "glib2", (
f"Expected RPM name 'glib2', got {rpm_name!r}"
)

def test_no_override_picks_first_spec(self):
"""No spec_override picks the first non-bootstrap, non-rawhide spec."""
mod = _import_module()
with tempfile.TemporaryDirectory() as tmpdir:
d = Path(tmpdir)
# Create multiple specs — should pick the non-bootstrap one
_create_spec(d, "glib2-bootstrap.spec", name="glib2")
_create_spec(d, "glib2.spec", name="glib2")
result = mod.resolve_pkg(str(d), spec_override=None)
assert result is not None, "Expected a result, got None"
copr_name, spec_file, rpm_name = result
assert copr_name == "glib2", (
f"Expected COPR name 'glib2', got {copr_name!r}"
)
assert "bootstrap" not in spec_file, (
f"Should not pick bootstrap spec, got {spec_file}"
)

def test_only_bootstrap_spec_available(self):
"""When only bootstrap specs exist, picks the first one."""
mod = _import_module()
with tempfile.TemporaryDirectory() as tmpdir:
d = Path(tmpdir)
_create_spec(d, "glib2-bootstrap.spec", name="glib2")
result = mod.resolve_pkg(str(d), spec_override=None)
assert result is not None, "Expected a result, got None"
copr_name, _, rpm_name = result
assert copr_name == "glib2", f"Expected 'glib2', got {copr_name!r}"
assert rpm_name == "glib2", f"Expected 'glib2', got {rpm_name!r}"

def test_no_specs_at_all(self):
"""Directory with no .spec files returns (None, None, None)."""
mod = _import_module()
with tempfile.TemporaryDirectory() as tmpdir:
d = Path(tmpdir)
result = mod.resolve_pkg(str(d), spec_override=None)
assert result == (None, None, None), (
f"Expected (None, None, None), got {result}"
)

def test_spec_override_file_not_found(self):
"""Spec override referencing a missing file returns (None, None, None)."""
mod = _import_module()
with tempfile.TemporaryDirectory() as tmpdir:
d = Path(tmpdir)
result = mod.resolve_pkg(str(d), spec_override="missing.spec")
assert result == (None, None, None), (
f"Expected (None, None, None) for missing spec, got {result}"
)

def test_different_rpm_name_from_spec(self):
"""When spec filename and RPM Name differ, COPR name follows the rule."""
mod = _import_module()
with tempfile.TemporaryDirectory() as tmpdir:
d = Path(tmpdir)
_create_spec(d, "weird-filename.spec", name="actual-rpm-name")
result = mod.resolve_pkg(str(d), spec_override=None)
assert result is not None, "Expected a result, got None"
copr_name, _, rpm_name = result
assert rpm_name == "actual-rpm-name", (
f"Expected rpm_name 'actual-rpm-name', got {rpm_name!r}"
)
# Without spec_override, copr_name = rpm_name
assert copr_name == "actual-rpm-name", (
f"Expected copr_name 'actual-rpm-name', got {copr_name!r}"
)
Loading