Refactor into a module with two CLI scripts, add tests, fix bug
All checks were successful
CI via Tox / tox (pull_request) Successful in 1m50s
AI Code Review / ai-review (pull_request_target) Successful in 29s

Packaging two Python scripts, one of which is a wrapper for the
other, is pretty awkward. But more importantly, we can make the
EL "wrapper" work better with a refactor, too.

This makes the CLI scripts independent of each other but sharing
a lot of bits via `shared.py`. We put handy run-it-locally
wrappers for each CLI script at the top level; you *can* install
the package properly and you'll get `rmdepcheck` and `rdcelwrap`
executables but we'll likely rarely do that.

The wrapper will now run faster because it will reuse the same
metadata between variants (previously it got re-downloaded with
every execution of `rmdepcheck.py`) and it can now nicely collect
up all the repoclosure strings before using the same parse-and-
exit code as rmdepcheck, which solves a lot of awkward issues
around output.

We also add tests covering the EL wrapper, as part of which it
made sense to factor out the Pungi config URL discovery (which I
meant to do anyway). We fix a bug that was exposed by writing the
tests: we have to check variants that depend on variants we have
packages for, even if we don't have packages for those variants.

This isn't great git hygiene, sorry. Should have made the refactor,
the addition of tests and the bug fix into separate commits. But
it's awkward and time-consuming to unpick now and I don't think
it's a good use of time.

Signed-off-by: Adam Williamson <awilliam@redhat.com>
This commit is contained in:
Adam Williamson 2026-05-08 17:32:11 -07:00
commit 57f086fe8e
116 changed files with 1603 additions and 993 deletions

View file

@ -10,7 +10,7 @@ jobs:
image: quay.io/fedora/fedora:latest
steps:
- name: Install required packages
run: dnf -y install nodejs tox git
run: dnf -y install nodejs tox git createrepo_c
- uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8
with:
fetch-depth: 0

View file

@ -1 +1,2 @@
requests
urllib3

View file

@ -35,7 +35,8 @@ Issues = "https://codeberg.org/AdamWill/rmdepcheck/issues"
Changelog = "https://codeberg.org/AdamWill/rmdepcheck/src/branch/main/CHANGELOG.md"
[project.scripts]
rmdepcheck = "rmdepcheck:main"
rmdepcheck = "rmdepcheck.rmdepcheck:main"
rdcelwrap = "rmdepcheck.rdcelwrap:main"
[build-system]
requires = ["setuptools>=40.6.0", "setuptools-git", "wheel"]
@ -47,7 +48,7 @@ branch = true
source_pkgs = ["rmdepcheck"]
[tool.coverage.paths]
source = [".", ".tox/**/site-packages"]
source = ["src", ".tox/**/site-packages"]
[tool.coverage.report]
show_missing = true

14
rdclocal.py Executable file
View file

@ -0,0 +1,14 @@
#!/usr/bin/python3
"""Convenience wrapper for executing rdcelwrap from a git checkout."""
import os
import sys
# add src subdirectory directory to module import path
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "src"))
from rmdepcheck.rmdepcheck import main # pylint: disable=wrong-import-position
if __name__ == "__main__":
main()

14
rewlocal.py Executable file
View file

@ -0,0 +1,14 @@
#!/usr/bin/python3
"""Convenience wrapper for executing rdcelwrap from a git checkout."""
import os
import sys
# add src subdirectory directory to module import path
sys.path.insert(0, os.path.join(os.path.dirname(os.path.realpath(__file__)), "src"))
from rmdepcheck.rdcelwrap import main # pylint: disable=wrong-import-position
if __name__ == "__main__":
main()

View file

@ -1,445 +0,0 @@
#!/usr/bin/python3
# Copyright Red Hat
#
# This file is part of rmdepcheck.
#
# rmdepcheck 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 3 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, see <http://www.gnu.org/licenses/>.
#
# Author(s): Adam Williamson <awilliam@redhat.com>
"""RPM package installability and reverse-dependency checks using a
repository modification strategy (hence 'rm').
"""
# Standard libraries
import argparse
import hashlib
import json
import os
import platform
import subprocess
import sys
import tempfile
from functools import partial
from typing import Iterable
from urllib.parse import urlparse
# type alias for the tuples produced by parse_repoclosure
# can't properly declare this because type statement was only added in
# 3.12, and TypeAlias is deprecated since 3.12 and wasn't in 3.9
DepTuple = tuple[str, str, str]
# use a fresh temporary cache for each run to avoid collisions between
# runs and polluting the 'real' cache
# pylint: disable-next=consider-using-with
DNFTEMP = tempfile.TemporaryDirectory(prefix="rmdepcheck", dir="/var/tmp")
DNFARGS = ["dnf", "--setopt", f"cachedir={DNFTEMP.name}", "-q", "--disablerepo=*"]
SUBPCAPTURE = partial(subprocess.run, capture_output=True, text=True, check=False)
SUBPCAPTCHECK = partial(subprocess.run, capture_output=True, text=True, check=True)
SUBPCHECK = partial(subprocess.run, check=True)
REPOHASHES: dict[str, str] = {}
def hash_repo(repo: str) -> str:
"""Generate a hash for the repo name, stash it in a dict so we can
map back out later, and return it. This is so we can show the repo
URLs in our final output, as opposed to non-useful made-up repo
names. Not security sensitive.
"""
gothash = hashlib.sha256(repo.encode(encoding="utf-8")).hexdigest()[:8]
REPOHASHES[gothash] = repo
return gothash
def parse_repoclosure(rc: str) -> list[DepTuple]:
"""Given some `dnf repoclosure` output, parse it into a list of
3-tuples each containing a package name, a repo URL (or generated
repo name if we can't look up the hash, should only happen in
tests) and an unresolved dependency for that package.
"""
out = []
pkg = None
for line in rc.splitlines():
if not line.strip():
continue
if line.strip().startswith("package:"):
# package, repo
elems = line.split()
pkg = (elems[1], REPOHASHES.get(elems[3], elems[3]))
continue
if line.strip().startswith("unresolved deps (") or line.strip().startswith("Error:"):
continue
# anything else is an unresolved dep
if pkg:
out.append(pkg + (line.strip(),))
return out
def format_rc_errors(errors: list[DepTuple]) -> None:
"""Format and print parse_repoclosure-style tuples for humans to
read. Used for final output after we do some diffing on the lists
of tuples.
"""
pkg = ("", "")
for error in errors:
if error[:2] != pkg:
pkg = error[:2]
print(f"package: {error[0]} from {error[1]}")
print(f" {error[2]}")
def get_base_repoclosure(
baserepos: Iterable[str], ncbaserepos: Iterable[str], nmbaserepos: Iterable[str]
) -> str:
"""Gets the reference repoclosure text. Base repos, non-checked
base repos and non-modified base repos are available to the
solver, but only the base repos are checked.
"""
cmdargs = DNFARGS + ["repoclosure"]
for repo in list(baserepos) + list(nmbaserepos) + list(ncbaserepos):
cmdargs.extend(["--repofrompath", f"{hash_repo(repo)},{repo}"])
cmdargs.append("--check")
# only check the repos that will be modified
cmdargs.append(",".join([hash_repo(baserepo) for baserepo in baserepos]))
return SUBPCAPTURE(cmdargs).stdout
def get_modified_and_new_repoclosure(
baserepos: list[str],
ncbaserepos: list[str],
nmbaserepos: list[str],
newrepos: list[str],
removes: Iterable[str],
) -> tuple[str, str]:
"""Runs repoclosure with new repo included and excludepkgs used
for modified base repos, and returns the modified repoclosure
text. Non-modified base repos, modified base repos and non-checked
base repos after modification, and new repos are available to
the solver; only the base repos are checked in the "modified"
check and only the first new repo is checked in the "new" check.
"""
queryargs = DNFARGS + ["repoquery", "--queryformat", "%{source_name},%{full_nevra}\n"]
rcargs = DNFARGS + ["repoclosure"]
for mrepo in baserepos + ncbaserepos:
# figure out what to exclude
excludes = []
args = queryargs + ["--repofrompath", f"{hash_repo(mrepo)},{mrepo}"]
out = SUBPCAPTURE(args).stdout
for line in out.splitlines():
# sname, nevr
elems = line.split(",")
if len(elems) != 2:
continue
if elems[0] in removes:
excludes.append(elems[1])
# add each modified base repo with excludepkgs set to the
# list we discovered above, in the repoclosure args
rcargs.extend(["--repofrompath", f"{hash_repo(mrepo)},{mrepo}"])
# there is a theoretical risk we might exceed MAX_ARG_STRLEN
# here, which is usually 131072:
# https://stackoverflow.com/a/29802900/4460661
# still, 131072 is enough for 2621 50-character NEVRs...
rcargs.extend(["--setopt", f"{hash_repo(mrepo)}.excludepkgs={','.join(excludes)}"])
# now add the non-modified base repos and new package repos
for repo in nmbaserepos + newrepos:
rcargs.extend(["--repofrompath", f"{hash_repo(repo)},{repo}"])
# finally, add the check arg
rcargs.append("--check")
rcargs.append(",".join([hash_repo(baserepo) for baserepo in baserepos]))
# get the modified repoclosure
mod = SUBPCAPTURE(rcargs).stdout
new = ""
if newrepos:
# the first newrepo is the checked repo, only check that
rcargs[-1] = hash_repo(newrepos[0])
# get the new repoclosure
new = SUBPCAPTURE(rcargs).stdout
return (mod, new)
def get_source_packages(repos: Iterable[str]) -> set[str]:
"""Finds and returns the source package names for all packages in
the repositories specified, as a set.
"""
args = list(DNFARGS)
for repo in repos:
args.extend(["--repofrompath", f"{hash_repo(repo)},{repo}"])
args.extend(["repoquery", "--qf", "%{sourcerpm} "])
srpms = SUBPCAPTCHECK(args).stdout.split()
return {srpm.rsplit("-", 2)[0] for srpm in srpms}
def handle_preexisting(fixederrors: list[DepTuple], newrc: list[DepTuple]) -> list[DepTuple]:
"""Catch cases where an error 'moved' from baserc to newrc - a
package in the set under test previously had a dependency issue,
and the new build does not fix it. We should not report this as a
fixed issue in the old repo and a new issue in the repo under
test. Note this modifies the passed lists in-place. See:
https://forge.fedoraproject.org/quality/rmdepcheck/issues/17
"""
preexisting = []
if fixederrors and newrc:
# iterate over copies so we can modify originals on the fly
fecopy = fixederrors.copy()
nrcopy = newrc.copy()
for fixeddep in fecopy:
# see if we can find a broken dep in newrc - the set of
# errors in the repository under test - that exactly
# matches this one, and is for the same package name
newmatch = [
newdep
for newdep in nrcopy
if newdep[0].rsplit("-", 2)[0] == fixeddep[0].rsplit("-", 2)[0]
and newdep[2] == fixeddep[2]
]
if len(newmatch) == 1:
# if so, remove the dep from both lists and add it
# to the new return list so we report it correctly
fixederrors.remove(fixeddep)
newrc.remove(newmatch[0])
preexisting.append(newmatch[0])
return preexisting
def url_check(arg: str) -> str:
"""Check arg is a file, http or https URL. Automatically converts
local paths to file:// URIs."""
# If it's an existing local path, convert to file:// URI
if os.path.exists(arg):
return f"file://{os.path.abspath(arg)}"
parsed = urlparse(arg)
if parsed.scheme in ("http", "https", "file"):
return arg
if parsed.scheme:
raise ValueError(f"Unsupported URL scheme {parsed.scheme} in {arg}")
raise ValueError(f"No URL scheme in {arg}")
def comma_url(arg: str) -> list[str]:
"""Check arg is a comma-separated list of URLs and return them
all as a list. If arg is the empty string, return empty list.
"""
if arg == "":
return []
split = arg.split(",")
for item in split:
try:
url_check(item)
except ValueError as err:
newerr = str(err) + f"from {arg}"
raise ValueError(newerr) from err
return split
def comma_list(arg: str) -> list[str]:
"""Handle a comma-separated list, return as a list."""
if arg == "":
return []
return arg.split(",")
def check_arch(arg: str) -> str:
"""Use host arch if no value passed."""
if arg:
return arg
return platform.machine()
def parse_args() -> argparse.Namespace:
"""Parse arguments with argparse."""
parser = argparse.ArgumentParser(
description=("Reverse dependency check implemented as a repoclosure diff.")
)
parser.add_argument(
"--addrepos",
type=comma_url,
default="",
help="The URL(s) of additional repositories containing new packages to be tested "
"(comma-separated). This is mainly intended for multilib cases: i.e. for testing "
"x86_64 package sets it should contain the matching i686 packages. It will be "
"available to the repoclosure check, but will be ignored by the installability check",
)
parser.add_argument(
"--ncbaserepos",
type=comma_url,
default="",
help="The URL(s) of non-checked base repositories to compare against (comma-separated). "
"These repositories *will* be modified as part of testing, but they will not be checked "
"for repoclosure. They should be repositories whose packages would be replaced with those "
"from the new package repo(s), and which we want to be available to the dependency solver "
"when checking the repoclosure of other repositories, but which we are for some reason not "
"interested in checking the repoclosure of - e.g. the buildroot for Fedora ELN",
)
parser.add_argument(
"--nmbaserepos",
type=comma_url,
default="",
help="The URL(s) of non-modified base repositories to compare against (comma-separated). "
"These repositories *will not* be modified as part of testing. They should be repositories "
"whose packages would *not* be replaced with those from the new package repo(s) - e.g. "
"the frozen release repository for a stable release, which is never changed",
)
parser.add_argument(
"--json",
action="store_true",
help="Output results in JSON rather than human-readable format",
)
parser.add_argument(
"--onlyerrors",
action="store_true",
help="Only print messages about problems caused, not about things that would be fixed",
)
parser.add_argument(
"--removes",
action="store_true",
help="Alternative mode: test removal (only) of all binary packages in the baserepos built "
"from the source package(s) specified as the final argument (a comma-separated list)",
)
parser.add_argument(
"--arch",
type=check_arch,
default="",
help="Arch to operate on. Must match arch of passed repositories. Defaults to host arch",
)
parser.add_argument(
"baserepos",
type=comma_url,
help="The URL(s) of base repositories to compare against (comma-separated). "
"These repositories *will* be modified as part of testing. They should be "
"repositories whose packages would be replaced with those from the new package "
"repo(s) - e.g. the main repository for a development release, or the updates repository "
"for a stable release",
)
# I wanted to do this with parse_known_args, but it messes up --help. aw
if "--removes" in sys.argv:
parser.add_argument(
"removes",
type=comma_list,
help="A comma-separated list of source packages to test the removal of",
)
else:
parser.add_argument(
"repo",
metavar="repo_or_removes",
type=url_check,
help="The URL of the repo containing the main set of new packages to be tested, "
"or a comma-separated list of source packages to test the removal of (if --removes "
"is passed)",
)
args = parser.parse_args()
if args.removes:
args.repo = ""
else:
args.removes = ""
return args
def check_dnf() -> None:
"""Check DNF is installed."""
try:
subprocess.run(("dnf", "--version"), stdout=subprocess.DEVNULL, check=True)
except FileNotFoundError:
sys.exit("Please install missing required utilities: dnf")
def main() -> None:
"""Main loop."""
try:
check_dnf()
exitcode = 0
args = parse_args()
if f"--forcearch={args.arch}" not in DNFARGS:
DNFARGS.append(f"--forcearch={args.arch}")
if args.removes:
newrepos = []
sources = args.removes
iut = "the specified source package removals"
else:
newrepos = [args.repo] + args.addrepos
# find source package(s) of our tested repo(s)
# whether to include addrepos is arguable, but should usually be moot
sources = get_source_packages(newrepos)
iut = "the tested packages"
baseraw = get_base_repoclosure(args.baserepos, args.ncbaserepos, args.nmbaserepos)
baserc = parse_repoclosure(baseraw)
# get the modified rpmclosure output
modraw, newraw = get_modified_and_new_repoclosure(
args.baserepos, args.ncbaserepos, args.nmbaserepos, newrepos, sources
)
modrc = parse_repoclosure(modraw)
newrc = []
if newraw:
newrc = parse_repoclosure(newraw)
newerrors = [dep for dep in modrc if dep not in baserc]
fixederrors = [dep for dep in baserc if dep not in modrc]
preexisting = handle_preexisting(fixederrors, newrc)
# output
if args.json:
jsonout = {}
if newerrors:
if args.json:
jsonout["newerrors"] = [list(err) for err in newerrors]
else:
print(f"Dependencies of other packages that would be BROKEN by {iut}:")
format_rc_errors(newerrors)
exitcode += 1
if newrc:
if args.json:
jsonout["installability"] = [list(err) for err in newrc]
else:
print("")
print("New dependency problems in the tested packages themselves:")
format_rc_errors(newrc)
exitcode += 2
if preexisting:
if args.json:
jsonout["installability_preexisting"] = [list(err) for err in preexisting]
else:
print("")
print("Pre-existing dependency problems in the tested packages themselves:")
format_rc_errors(preexisting)
exitcode += 4
if fixederrors:
if args.json:
jsonout["fixederrors"] = [list(err) for err in fixederrors]
elif not args.onlyerrors:
print("")
print(f"Dependencies of other packages that would be FIXED by {iut}:")
format_rc_errors(fixederrors)
if args.json:
json.dump(jsonout, sys.stdout, indent=4)
sys.stdout.write("\n")
sys.exit(exitcode)
except KeyboardInterrupt:
sys.stderr.write("Interrupted, exiting...\n")
sys.exit(1)
if __name__ == "__main__": # pragma: no cover
main()
# vim: set textwidth=100 ts=8 et sw=4:

View file

@ -0,0 +1,29 @@
#!/usr/bin/python3
# Copyright Red Hat
#
# This file is part of rmdepcheck.
#
# rmdepcheck 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 3 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, see <http://www.gnu.org/licenses/>.
#
# Author(s): Adam Williamson <awilliam@redhat.com>
"""RPM package installability and reverse-dependency checks using a
repository modification strategy (hence 'rm').
"""
__version__ = "1.0.0"
# vim: set textwidth=100 ts=8 et sw=4:

203
rdc-el-wrapper.py → src/rmdepcheck/rdcelwrap.py Executable file → Normal file
View file

@ -1,5 +1,3 @@
#!/usr/bin/python3
# Copyright Red Hat
#
# This file is part of rmdepcheck.
@ -20,7 +18,7 @@
# Author(s): Adam Williamson <awilliam@redhat.com>
"""Opinionated rmdepcheck wrapper that handles the awkward Enterprise
"""Opinionated rmdepcheck frontend that handles the awkward Enterprise
Linux case, where we have to test against multiple shipped variants
with complex lookaside relationships, all filtered from a shared build
root.
@ -32,29 +30,37 @@ import argparse
import ast
import os
import pathlib
import platform
import re
import shutil
import subprocess
import sys
import tempfile
from functools import partial
from urllib.parse import urlparse
from requests import Session
from requests.adapters import HTTPAdapter
from requests.packages.urllib3.util.retry import Retry
from urllib3.util.retry import Retry
from rmdepcheck.shared import (
SUBPCAPTURE,
get_dnf_args,
check_arch,
url_check,
check_utils,
get_source_packages,
get_base_repoclosure,
get_modified_and_new_repoclosure,
parse_display_exit,
)
VrDict = dict[str, str]
DNFTEMP = tempfile.TemporaryDirectory(prefix="rdcelndnf", dir="/var/tmp")
DNFARGS = ["dnf", "--setopt", f"cachedir={DNFTEMP.name}", "-q", "--disablerepo=*"]
HERE = os.path.abspath(os.path.dirname(__file__))
SESSION = Session()
SUBPCAPTURE = partial(subprocess.run, capture_output=True, text=True, check=False)
def split_put(put: pathlib.Path, repotemp: pathlib.Path, vrs: VrDict) -> set[str]:
def split_put(
put: pathlib.Path, repotemp: pathlib.Path, vrs: VrDict, dnfargs: list[str]
) -> set[str]:
"""Splits a directory containing the packages to be tested into
multiple repositories per variant. put ('packages under test')
is the filesystem path containing packages under test (it cannot
@ -73,8 +79,12 @@ def split_put(put: pathlib.Path, repotemp: pathlib.Path, vrs: VrDict) -> set[str
# generate per-variant package name lists
pkgs = {}
for variant, repo in vrs.items():
args = DNFARGS + [
"--repofrompath", f"{variant},{repo}", "repoquery", "--queryformat", "%{name}\n"
args = dnfargs + [
"--repofrompath",
f"{variant},{repo}",
"repoquery",
"--queryformat",
"%{name}\n",
]
res = SUBPCAPTURE(args)
res.check_returncode()
@ -84,7 +94,7 @@ def split_put(put: pathlib.Path, repotemp: pathlib.Path, vrs: VrDict) -> set[str
for _file in os.listdir(put):
if _file.endswith(".rpm"):
pkgname = _file.rsplit("-", 2)[0]
for (variant, pkgnames) in pkgs.items():
for variant, pkgnames in pkgs.items():
if pkgname in pkgnames:
populated.add(variant)
os.makedirs(repotemp / variant, exist_ok=True)
@ -99,70 +109,57 @@ def split_put(put: pathlib.Path, repotemp: pathlib.Path, vrs: VrDict) -> set[str
return populated
def get_variant_repos(compose: str, arch: str) -> VrDict:
def get_variant_repos(compose: str, ci: dict, arch: str) -> VrDict:
"""Discover variants with repositories in a compose, return a
dict with variant IDs as keys and repo URLs as values.
"""
resp = SESSION.get(f"{compose}/metadata/composeinfo.json")
resp.raise_for_status()
ci = resp.json()
variants = ci["payload"]["variants"]
variants = ci.get("payload", {}).get("variants", {})
ret = {}
for variant in variants:
if variants[variant]["id"].lower() == "buildroot":
vid = variants[variant].get("id", variant)
# as of 2026-05 yselkowitz says he does not want to know about
# buildroot-only dep issues
if vid.lower() == "buildroot":
continue
repopath = variants[variant]["paths"].get("repository", {}).get(arch, "")
repopath = variants[variant].get("paths", {}).get("repository", {}).get(arch, "")
if not repopath:
continue
ret[variants[variant]["id"]] = f"{compose}/{repopath}"
ret[vid] = f"{compose}/{repopath}"
return ret
def get_val():
def pungi_config_url(ci: dict) -> str:
"""Return the URL with the appropriate Pungi config for the
release. Here be magic knowledge.
"""
version = ci.get("payload", {}).get("release", {}).get("version", "")
short = ci.get("payload", {}).get("release", {}).get("short", "")
if version.lower() == "eln" and short.lower() == "fedora":
# pylint: disable-next=line-too-long
return "https://forge.fedoraproject.org/releng/pungi-fedora/raw/branch/eln/fedora/override.conf"
raise ValueError(f"Don't know Pungi config URL for {short} {version}")
def get_val(ci: dict) -> dict:
"""Get the variants_as_lookaside list which tells us which
variants pull in which other variants. FIXME: this is currently
ELN specific.
"""
resp = SESSION.get(
"https://forge.fedoraproject.org/releng/pungi-fedora/raw/branch/eln/fedora/override.conf"
)
url = pungi_config_url(ci)
resp = SESSION.get(url)
resp.raise_for_status()
override = resp.text
# next three lines suggested by AI
match = re.search(r"variant_as_lookaside\s*=\s*(\[.*?\])", override, re.DOTALL)
if not match:
raise ValueError("Could not find variant_as_lookaside in configuration")
raise ValueError(f"Could not find variant_as_lookaside in configuration at {url}")
# eek
return ast.literal_eval(match.group(1))
def url_check(arg: str) -> str:
def url_check_http(arg: str) -> str:
"""Check arg is an http or https URL."""
parsed = urlparse(arg)
if parsed.scheme in ("http", "https"):
return arg
if parsed.scheme:
raise ValueError(f"Unsupported URL scheme {parsed.scheme} in {arg}")
raise ValueError(f"No URL scheme in {arg}")
def check_arch(arg: str) -> str:
"""Use host arch if no value passed."""
if arg:
return arg
return platform.machine()
def check_utils() -> None:
"""Check required utilities are installed."""
missing = []
for prog in (("dnf", "--version"), ("createrepo", "--version")):
try:
subprocess.run(prog, stdout=subprocess.DEVNULL, check=True)
except FileNotFoundError:
missing.append(prog[0])
if missing:
sys.exit("Please install missing required utilities: " + " ".join(missing))
return url_check(arg, fileok=False)
def parse_args() -> argparse.Namespace:
@ -172,7 +169,7 @@ def parse_args() -> argparse.Namespace:
)
parser.add_argument(
"compose",
type=url_check,
type=url_check_http,
help="The URL(s) of a compose to discover variant repos from and check against",
)
parser.add_argument(
@ -186,10 +183,15 @@ def parse_args() -> argparse.Namespace:
default="",
help="Arch to operate on. Must match arch of passed repository",
)
parser.add_argument(
"--json",
action="store_true",
help="Output results in JSON rather than human-readable format",
)
return parser.parse_args()
def main() -> None:
def main() -> None: # pylint: disable=too-many-locals
"""Main loop."""
try:
# set up requests session with retries
@ -197,60 +199,83 @@ def main() -> None:
total=5,
backoff_factor=0.5,
status_forcelist=[502, 503, 504],
allowed_methods={'GET'},
allowed_methods={"GET"},
)
SESSION.mount('https://', HTTPAdapter(max_retries=retries))
SESSION.mount("https://", HTTPAdapter(max_retries=retries))
# check DNF is installed
check_utils()
# check DNF and createrepo are installed
check_utils((("dnf", "--version"), ("createrepo", "--version")))
# set up args
args = parse_args()
if f"--forcearch={args.arch}" not in DNFARGS:
DNFARGS.append(f"--forcearch={args.arch}")
# repoclosure output strings
basercs = ""
modrcs = ""
newrcs = ""
# get composeinfo
resp = SESSION.get(f"{args.compose}/metadata/composeinfo.json")
resp.raise_for_status()
ci = resp.json()
# find variants, do the repo split
vrs = get_variant_repos(args.compose, args.arch)
val = get_val()
with tempfile.TemporaryDirectory(prefix="rdcelnrepo", dir="/var/tmp") as repotd:
vrs = get_variant_repos(args.compose, ci, args.arch)
val = get_val(ci)
with (
tempfile.TemporaryDirectory(prefix="rdcelnrepo", dir="/var/tmp") as repotd,
tempfile.TemporaryDirectory(prefix="rdcelwrap", dir="/var/tmp") as dnftemp,
):
dnfargs = get_dnf_args(dnftemp, args.arch)
repotemp = pathlib.Path(repotd)
variants = split_put(args.repo, repotemp, vrs)
# this is "all the variants with packages in the new repo"
variants = split_put(args.repo, repotemp, vrs, dnfargs)
# we have to check all variants we have new packages in
# *and* all variants that depend on them
tocheck = set(variants)
for pair in val:
if pair[0] in variants:
tocheck.add(pair[1])
rets = set()
for variant in variants:
for variant in tocheck:
baserepo = vrs[variant]
# figure out what other variants to pull in per val
depvars = set()
for pair in val:
if pair[0] == variant:
depvars.add(pair[1])
# put the compose repos for all variants in basearg, even
# ones we didn't 'populate' locally
basearg = vrs[variant]
ncbasearg = ",".join(vrs[var] for var in depvars)
newarg = str(repotemp / variant)
# put depended-on variant-split PUT repos that actually
# exist in addrepos
# put the compose repos for all depended-on variants in
# ncbaserepos, even ones we didn't 'populate' locally
ncbaserepos = [vrs[var] for var in depvars]
newrepo = ""
newrepos = []
if variant in variants:
# first newrepo is the PUT repo for this variant,
# *if* it exists
newrepo = str(repotemp / variant)
newrepos.append(newrepo)
# additional repos are depended-on variant-split PUT
# repos that actually exist
# variants contains only variants we 'populated'
addarg = ",".join(str(repotemp / var) for var in depvars if var in variants)
# run rmdepcheck, track return code
# FIXME: what to do about JSON mode? capture output, combine?
rargs = [f"{HERE}/rmdepcheck.py", "--arch", args.arch]
if addarg:
rargs.extend(("--addrepos", addarg))
if ncbasearg:
rargs.extend(("--ncbaserepos", ncbasearg))
rargs.extend((basearg, newarg))
rets.add(subprocess.run(rargs, check=False).returncode)
addrepos = [str(repotemp / var) for var in depvars if var in variants]
newrepos.extend(addrepos)
# we exit the sum of all *unique* return codes
sys.exit(sum(rets))
baserc = get_base_repoclosure([baserepo], ncbaserepos, [], dnfargs)
basercs = f"{basercs}\n{baserc}"
# get the modified rpmclosure output
sources = get_source_packages(newrepos, dnfargs)
modrc, newrc = get_modified_and_new_repoclosure(
[baserepo], ncbaserepos, [], newrepo, addrepos, sources, dnfargs
)
modrcs = f"{modrcs}\n{modrc}"
if newrc:
newrcs = f"{newrcs}\n{newrc}"
# output
parse_display_exit(basercs, modrcs, newrcs, "the tested packages", args.json, False, True)
except KeyboardInterrupt:
sys.stderr.write("Interrupted, exiting...\n")
sys.exit(1)
if __name__ == "__main__": # pragma: no cover
main()
# vim: set textwidth=100 ts=8 et sw=4:

View file

@ -0,0 +1,203 @@
# Copyright Red Hat
#
# This file is part of rmdepcheck.
#
# rmdepcheck 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 3 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, see <http://www.gnu.org/licenses/>.
#
# Author(s): Adam Williamson <awilliam@redhat.com>
"""RPM package installability and reverse-dependency checks using a
repository modification strategy (hence 'rm').
"""
# Standard libraries
import argparse
import sys
import tempfile
from rmdepcheck.shared import (
get_dnf_args,
check_arch,
url_check,
check_utils,
get_source_packages,
get_base_repoclosure,
get_modified_and_new_repoclosure,
parse_display_exit,
)
def url_check_file(arg: str) -> str:
"""Check arg is a file, http or https URL. Automatically converts
local paths to file:// URIs."""
return url_check(arg, fileok=True)
def comma_url(arg: str) -> list[str]:
"""Check arg is a comma-separated list of URLs and return them
all as a list. If arg is the empty string, return empty list.
"""
if arg == "":
return []
split = arg.split(",")
for item in split:
try:
url_check_file(item)
except ValueError as err:
newerr = str(err) + f"from {arg}"
raise ValueError(newerr) from err
return split
def comma_list(arg: str) -> list[str]:
"""Handle a comma-separated list, return as a list."""
if arg == "":
return []
return arg.split(",")
def parse_args() -> argparse.Namespace:
"""Parse arguments with argparse."""
parser = argparse.ArgumentParser(
description=("Reverse dependency check implemented as a repoclosure diff.")
)
parser.add_argument(
"--addrepos",
type=comma_url,
default="",
help="The URL(s) of additional repositories containing new packages to be tested "
"(comma-separated). This is mainly intended for multilib cases: i.e. for testing "
"x86_64 package sets it should contain the matching i686 packages. It will be "
"available to the repoclosure check, but will be ignored by the installability check",
)
parser.add_argument(
"--ncbaserepos",
type=comma_url,
default="",
help="The URL(s) of non-checked base repositories to compare against (comma-separated). "
"These repositories *will* be modified as part of testing, but they will not be checked "
"for repoclosure. They should be repositories whose packages would be replaced with those "
"from the new package repo(s), and which we want to be available to the dependency solver "
"when checking the repoclosure of other repositories, but which we are for some reason not "
"interested in checking the repoclosure of - e.g. the buildroot for Fedora ELN",
)
parser.add_argument(
"--nmbaserepos",
type=comma_url,
default="",
help="The URL(s) of non-modified base repositories to compare against (comma-separated). "
"These repositories *will not* be modified as part of testing. They should be repositories "
"whose packages would *not* be replaced with those from the new package repo(s) - e.g. "
"the frozen release repository for a stable release, which is never changed",
)
parser.add_argument(
"--json",
action="store_true",
help="Output results in JSON rather than human-readable format",
)
parser.add_argument(
"--onlyerrors",
action="store_true",
help="Only print messages about problems caused, not about things that would be fixed",
)
parser.add_argument(
"--removes",
action="store_true",
help="Alternative mode: test removal (only) of all binary packages in the baserepos built "
"from the source package(s) specified as the final argument (a comma-separated list)",
)
parser.add_argument(
"--arch",
type=check_arch,
default="",
help="Arch to operate on. Must match arch of passed repositories. Defaults to host arch",
)
parser.add_argument(
"baserepos",
type=comma_url,
help="The URL(s) of base repositories to compare against (comma-separated). "
"These repositories *will* be modified as part of testing. They should be "
"repositories whose packages would be replaced with those from the new package "
"repo(s) - e.g. the main repository for a development release, or the updates repository "
"for a stable release",
)
# I wanted to do this with parse_known_args, but it messes up --help. aw
if "--removes" in sys.argv:
parser.add_argument(
"removes",
type=comma_list,
help="A comma-separated list of source packages to test the removal of",
)
else:
parser.add_argument(
"repo",
metavar="repo_or_removes",
type=url_check_file,
help="The URL of the repo containing the main set of new packages to be tested, "
"or a comma-separated list of source packages to test the removal of (if --removes "
"is passed)",
)
args = parser.parse_args()
if args.removes:
args.repo = ""
else:
args.removes = ""
return args
def main() -> None:
"""Main loop."""
try:
check_utils((("dnf", "--version"),))
args = parse_args()
with tempfile.TemporaryDirectory(prefix="rdcelwrap", dir="/var/tmp") as dnftemp:
dnfargs = get_dnf_args(dnftemp, args.arch)
if args.removes:
newrepo = ""
sources = args.removes
iut = "the specified source package removals"
else:
newrepo = args.repo
newrepos = [newrepo] + args.addrepos
# find source package(s) of our tested repo(s)
# whether to include addrepos is arguable, but should usually be moot
sources = get_source_packages(newrepos, dnfargs)
iut = "the tested packages"
baseraw = get_base_repoclosure(
args.baserepos, args.ncbaserepos, args.nmbaserepos, dnfargs
)
# get the modified rpmclosure output
modraw, newraw = get_modified_and_new_repoclosure(
args.baserepos,
args.ncbaserepos,
args.nmbaserepos,
newrepo,
args.addrepos,
sources,
dnfargs,
)
# output
parse_display_exit(baseraw, modraw, newraw, iut, args.json, args.onlyerrors, False)
except KeyboardInterrupt:
sys.stderr.write("Interrupted, exiting...\n")
sys.exit(1)
# vim: set textwidth=100 ts=8 et sw=4:

341
src/rmdepcheck/shared.py Normal file
View file

@ -0,0 +1,341 @@
# Copyright Red Hat
#
# This file is part of rmdepcheck.
#
# rmdepcheck 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 3 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, see <http://www.gnu.org/licenses/>.
#
# Author(s): Adam Williamson <awilliam@redhat.com>
"""RPM package installability and reverse-dependency checks using a
repository modification strategy (hence 'rm') This file contains things
shared between the two main scripts.
"""
import hashlib
import json
import os
import platform
import subprocess
import sys
from functools import partial
from typing import Iterable
from urllib.parse import urlparse
# type alias for the tuples produced by parse_repoclosure
# can't properly declare this because type statement was only added in
# 3.12, and TypeAlias is deprecated since 3.12 and wasn't in 3.9
DepTuple = tuple[str, str, str]
# use a fresh temporary cache for each run to avoid collisions between
# runs and polluting the 'real' cache
# pylint: disable-next=consider-using-with
SUBPCAPTURE = partial(subprocess.run, capture_output=True, text=True, check=False)
SUBPCAPTCHECK = partial(subprocess.run, capture_output=True, text=True, check=True)
SUBPCHECK = partial(subprocess.run, check=True) # pylint: disable=invalid-name
REPOHASHES: dict[str, str] = {}
def get_dnf_args(cachedir: str, arch: str) -> list[str]:
"""Construct our basic dnf args."""
return [
"dnf",
"--setopt",
f"cachedir={cachedir}",
"-q",
"--disablerepo=*",
f"--forcearch={arch}",
]
def check_arch(arg: str) -> str:
"""Use host arch if no value passed."""
if arg:
return arg
return platform.machine()
def url_check(arg: str, fileok: bool) -> str:
"""Check arg is a file, http or https URL. Automatically converts
local paths to file:// URIs."""
# If it's an existing local path, convert to file:// URI
if os.path.exists(arg):
if fileok:
return f"file://{os.path.abspath(arg)}"
raise ValueError("Value must be an HTTP or HTTPS URL")
parsed = urlparse(arg)
schemes = ["http", "https"]
if fileok:
schemes.append("file")
if parsed.scheme in schemes:
return arg
if parsed.scheme:
raise ValueError(f"Unsupported URL scheme {parsed.scheme} in {arg}")
raise ValueError(f"No URL scheme in {arg}")
def check_utils(utils: tuple[tuple[str, ...], ...]) -> None:
"""Check required utilities are installed."""
missing = []
for prog in utils:
try:
subprocess.run(prog, stdout=subprocess.DEVNULL, check=True)
except FileNotFoundError:
missing.append(prog[0])
if missing:
sys.exit("Please install missing required utilities: " + " ".join(missing))
def get_source_packages(repos: Iterable[str], dnfargs: list[str]) -> set[str]:
"""Finds and returns the source package names for all packages in
the repositories specified, as a set.
"""
gspargs = list(dnfargs)
for repo in repos:
gspargs.extend(["--repofrompath", f"{hash_repo(repo)},{repo}"])
gspargs.extend(["repoquery", "--qf", "%{sourcerpm} "])
srpms = SUBPCAPTCHECK(gspargs).stdout.split()
return {srpm.rsplit("-", 2)[0] for srpm in srpms}
def hash_repo(repo: str) -> str:
"""Generate a hash for the repo name, stash it in a dict so we can
map back out later, and return it. This is so we can show the repo
URLs in our final output, as opposed to non-useful made-up repo
names. Not security sensitive.
"""
gothash = hashlib.sha256(repo.encode(encoding="utf-8")).hexdigest()[:8]
REPOHASHES[gothash] = repo
return gothash
def parse_repoclosure(rc: str) -> list[DepTuple]:
"""Given some `dnf repoclosure` output, parse it into a list of
3-tuples each containing a package name, a repo URL (or generated
repo name if we can't look up the hash, should only happen in
tests) and an unresolved dependency for that package.
"""
out = []
pkg = None
for line in rc.splitlines():
if not line.strip():
continue
if line.strip().startswith("package:"):
# package, repo
elems = line.split()
pkg = (elems[1], REPOHASHES.get(elems[3], elems[3]))
continue
if line.strip().startswith("unresolved deps (") or line.strip().startswith("Error:"):
continue
# anything else is an unresolved dep
if pkg:
out.append(pkg + (line.strip(),))
return out
def format_rc_errors(errors: list[DepTuple], form: str = "pass") -> None:
"""Format and print parse_repoclosure-style tuples for humans to
read. Used for final output after we do some diffing on the lists
of tuples.
"""
pkg = ("", "")
for error in errors:
if error[:2] != pkg:
pkg = error[:2]
if form == "split":
print(f"package: {error[0]} from {error[1].split('/')[-1]}")
elif form == "strip":
print(f"package: {error[0]}")
else:
print(f"package: {error[0]} from {error[1]}")
print(f" {error[2]}")
def get_base_repoclosure(
baserepos: Iterable[str],
ncbaserepos: Iterable[str],
nmbaserepos: Iterable[str],
dnfargs: list[str],
) -> str:
"""Gets the reference repoclosure text. Base repos, non-checked
base repos and non-modified base repos are available to the
solver, but only the base repos are checked.
"""
cmdargs = dnfargs + ["repoclosure"]
for repo in list(baserepos) + list(nmbaserepos) + list(ncbaserepos):
cmdargs.extend(["--repofrompath", f"{hash_repo(repo)},{repo}"])
cmdargs.append("--check")
# only check the repos that will be modified
cmdargs.append(",".join([hash_repo(baserepo) for baserepo in baserepos]))
return SUBPCAPTURE(cmdargs).stdout
# pylint: disable-next=too-many-arguments,too-many-locals,too-many-positional-arguments
def get_modified_and_new_repoclosure(
baserepos: list[str],
ncbaserepos: list[str],
nmbaserepos: list[str],
newrepo: str,
addrepos: list[str],
removes: Iterable[str],
dnfargs: list[str],
) -> tuple[str, str]:
"""Runs repoclosure with new repo included and excludepkgs used
for modified base repos, and returns the modified repoclosure
text. Non-modified base repos, modified base repos and non-checked
base repos after modification, and new repos are available to
the solver; only the base repos are checked in the "modified"
check and only the first new repo is checked in the "new" check.
"""
queryargs = dnfargs + ["repoquery", "--queryformat", "%{source_name},%{full_nevra}\n"]
rcargs = dnfargs + ["repoclosure"]
for mrepo in baserepos + ncbaserepos:
# figure out what to exclude
excludes = []
args = queryargs + ["--repofrompath", f"{hash_repo(mrepo)},{mrepo}"]
for line in SUBPCAPTURE(args).stdout.splitlines():
# sname, nevr
elems = line.split(",")
if len(elems) != 2:
continue
if elems[0] in removes:
excludes.append(elems[1])
# add each modified base repo with excludepkgs set to the
# list we discovered above, in the repoclosure args
rcargs.extend(["--repofrompath", f"{hash_repo(mrepo)},{mrepo}"])
# there is a theoretical risk we might exceed MAX_ARG_STRLEN
# here, which is usually 131072:
# https://stackoverflow.com/a/29802900/4460661
# still, 131072 is enough for 2621 50-character NEVRs...
rcargs.extend(["--setopt", f"{hash_repo(mrepo)}.excludepkgs={','.join(excludes)}"])
# now add the non-modified base repos and new package repos
for repo in nmbaserepos + [nr for nr in [newrepo] if nr] + addrepos:
rcargs.extend(["--repofrompath", f"{hash_repo(repo)},{repo}"])
# finally, add the check arg
rcargs.append("--check")
rcargs.append(",".join([hash_repo(baserepo) for baserepo in baserepos]))
# get the modified repoclosure
mod = SUBPCAPTURE(rcargs).stdout
new = ""
if newrepo:
rcargs[-1] = hash_repo(newrepo)
# get the new repoclosure
new = SUBPCAPTURE(rcargs).stdout
return (mod, new)
def handle_preexisting(fixederrors: list[DepTuple], newrc: list[DepTuple]) -> list[DepTuple]:
"""Catch cases where an error 'moved' from baserc to newrc - a
package in the set under test previously had a dependency issue,
and the new build does not fix it. We should not report this as a
fixed issue in the old repo and a new issue in the repo under
test. Note this modifies the passed lists in-place. See:
https://forge.fedoraproject.org/quality/rmdepcheck/issues/17
"""
preexisting = []
if fixederrors and newrc:
# iterate over copies so we can modify originals on the fly
fecopy = fixederrors.copy()
nrcopy = newrc.copy()
for fixeddep in fecopy:
# see if we can find a broken dep in newrc - the set of
# errors in the repository under test - that exactly
# matches this one, and is for the same package name
newmatch = [
newdep
for newdep in nrcopy
if newdep[0].rsplit("-", 2)[0] == fixeddep[0].rsplit("-", 2)[0]
and newdep[2] == fixeddep[2]
]
if len(newmatch) == 1:
# if so, remove the dep from both lists and add it
# to the new return list so we report it correctly
fixederrors.remove(fixeddep)
newrc.remove(newmatch[0])
preexisting.append(newmatch[0])
return preexisting
# pylint: disable-next=too-many-branches,too-many-arguments,too-many-positional-arguments
def parse_display_exit(
baseraw: str,
modraw: str,
newraw: str,
iut: str,
dojson: bool,
onlyerrors: bool,
splitrepo: bool,
) -> None:
"""Take the raw rmdepcheck output strings, parse them, produce
human-readable or JSON output, and exit with an appropriate code.
"""
exitcode = 0
baserc = parse_repoclosure(baseraw)
modrc = parse_repoclosure(modraw)
newrc = []
if newraw:
newrc = parse_repoclosure(newraw)
newerrors = [dep for dep in modrc if dep not in baserc]
fixederrors = [dep for dep in baserc if dep not in modrc]
preexisting = handle_preexisting(fixederrors, newrc)
# output
if dojson:
jsonout = {}
if newerrors:
if dojson:
jsonout["newerrors"] = [list(err) for err in newerrors]
else:
print(f"Dependencies of other packages that would be BROKEN by {iut}:")
format_rc_errors(newerrors)
exitcode += 1
if newrc:
if dojson:
jsonout["installability"] = [list(err) for err in newrc]
else:
print("")
print("New dependency problems in the tested packages themselves:")
if splitrepo:
format_rc_errors(newrc, form="split")
else:
format_rc_errors(newrc, form="strip")
exitcode += 2
if preexisting:
if dojson:
jsonout["installability_preexisting"] = [list(err) for err in preexisting]
else:
print("")
print("Pre-existing dependency problems in the tested packages themselves:")
format_rc_errors(preexisting)
exitcode += 4
if fixederrors:
if dojson:
jsonout["fixederrors"] = [list(err) for err in fixederrors]
elif not onlyerrors:
print("")
print(f"Dependencies of other packages that would be FIXED by {iut}:")
format_rc_errors(fixederrors)
if dojson:
json.dump(jsonout, sys.stdout, indent=4)
sys.stdout.write("\n")
sys.exit(exitcode)
# vim: set textwidth=100 ts=8 et sw=4:

52
tests/conftest.py Normal file
View file

@ -0,0 +1,52 @@
# Copyright Red Hat
#
# This file is part of rmdepcheck.
#
# rmdepcheck 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 3 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, see <http://www.gnu.org/licenses/>.
#
# Author: Adam Williamson <awilliam@redhat.com>
"""Test configuration and fixtures."""
import os
import subprocess
import time
from urllib.error import URLError, HTTPError
from urllib.request import urlopen
import pytest
@pytest.yield_fixture(scope="session")
def http(request): # pylint: disable=unused-argument
"""Run a SimpleHTTPServer that sits in front of our mock ELN
compose. We just do this with subprocess as we need it to run
parallel to the tests and this is really the easiest way. Note
we also stash override.conf in the compose dir, it's just
convenient that way.
"""
root = os.path.join(os.path.dirname(os.path.realpath(__file__)), "testdata", "repos", "eln")
args = ("python3", "-m", "http.server", "5001")
proc = subprocess.Popen(args, cwd=root) # pylint: disable=consider-using-with
# block until the server is actually running
resp = None
while not resp:
try:
resp = urlopen("http://localhost:5001/BaseOS") # pylint: disable=consider-using-with
except (ValueError, URLError, HTTPError):
time.sleep(0.1)
yield
# teardown
proc.kill()

View file

@ -23,296 +23,414 @@
"""Tests for rmdepcheck."""
import os
import pathlib
import sys
import tempfile
from unittest import mock
from rmdepcheck.shared import (
get_dnf_args,
check_arch,
url_check,
check_utils,
get_source_packages,
hash_repo,
parse_repoclosure,
format_rc_errors,
get_base_repoclosure,
get_modified_and_new_repoclosure,
handle_preexisting,
)
from rmdepcheck import rmdepcheck, rdcelwrap
import pytest
import rmdepcheck
HERE = os.path.abspath(os.path.dirname(__file__))
TESTDATA = f"{HERE}/testdata"
REPOS = f"{TESTDATA}/repos"
def test_parse_repoclosure():
with open(f"{TESTDATA}/test_parse_repoclosure.txt", "r", encoding="utf-8") as fh:
rctext = fh.read()
assert rmdepcheck.parse_repoclosure(rctext) == [
("python3-wxnatpy-0.4.0-13.fc42.noarch", "baserepo0", "python(abi) = 3.13"),
("python3-wxnatpy-0.4.0-13.fc42.noarch", "baserepo0", "python3.13dist(wxpython) >= 4"),
("python3-wxnatpy-0.4.0-13.fc42.noarch", "baserepo0", "python3.13dist(xnat) >= 0.3.3"),
("python3-x3dh-1.0.4-3.fc43.noarch", "baserepo1", "python3.14dist(pydantic) >= 1.7.4"),
]
class TestShared:
"""Unit tests for things from rmdepcheck.shared."""
def test_parse_repoclosure(self):
with open(f"{TESTDATA}/test_parse_repoclosure.txt", "r", encoding="utf-8") as fh:
rctext = fh.read()
assert parse_repoclosure(rctext) == [
("python3-wxnatpy-0.4.0-13.fc42.noarch", "baserepo0", "python(abi) = 3.13"),
("python3-wxnatpy-0.4.0-13.fc42.noarch", "baserepo0", "python3.13dist(wxpython) >= 4"),
("python3-wxnatpy-0.4.0-13.fc42.noarch", "baserepo0", "python3.13dist(xnat) >= 0.3.3"),
("python3-x3dh-1.0.4-3.fc43.noarch", "baserepo1", "python3.14dist(pydantic) >= 1.7.4"),
]
def test_format_rc_errors(self, capsys):
errs = [
("python3-wxnatpy-0.4.0-13.fc42.noarch", "baserepo0", "python(abi) = 3.13"),
("python3-wxnatpy-0.4.0-13.fc42.noarch", "baserepo0", "python3.13dist(wxpython) >= 4"),
("python3-wxnatpy-0.4.0-13.fc42.noarch", "baserepo0", "python3.13dist(xnat) >= 0.3.3"),
("python3-x3dh-1.0.4-3.fc43.noarch", "baserepo1", "python3.14dist(pydantic) >= 1.7.4"),
]
format_rc_errors(errs)
captured = capsys.readouterr()
with open(f"{TESTDATA}/test_format_rc_errors.txt", "r", encoding="utf-8") as fh:
exptext = fh.read()
assert captured.out == exptext
def test_get_base_repoclosure(self):
repo = f"file://{REPOS}/base"
with open(f"{TESTDATA}/test_get_base_repoclosure.txt", "r", encoding="utf-8") as testfh:
expected = testfh.read()
expected = expected.replace("{HASH}", hash_repo(repo))
with tempfile.TemporaryDirectory(prefix="testrdc", dir="/var/tmp") as dnftemp:
dnfargs = get_dnf_args(dnftemp, "x86_64")
ret = get_base_repoclosure([repo], [], [], dnfargs)
assert ret == expected
def test_get_modified_and_new_repoclosure(self):
brepo = f"file://{REPOS}/base"
nrepo = f"file://{REPOS}/new"
with open(f"{TESTDATA}/test_get_modified_repoclosure.txt", "r", encoding="utf-8") as testfh:
expectedmod = testfh.read()
expectedmod = expectedmod.replace("{HASH}", hash_repo(brepo))
with open(f"{TESTDATA}/test_get_new_repoclosure.txt", "r", encoding="utf-8") as testfh:
expectednew = testfh.read()
expectednew = expectednew.replace("{HASH}", hash_repo(nrepo))
with tempfile.TemporaryDirectory(prefix="testrdc", dir="/var/tmp") as dnftemp:
dnfargs = get_dnf_args(dnftemp, "x86_64")
ret = get_modified_and_new_repoclosure(
[brepo], [], [], nrepo, [], ["aaa", "ccc", "eee", "fff", "ggg"], dnfargs
)
assert ret == (expectedmod, expectednew)
def test_get_modified_and_new_repoclosure_commasafe(self):
"""Check we survive repoquery output lines that are, precisely,
a string from 'removes'. This is a pretty unlikely scenario, but
hey. Any other line with no commas is actually safe because it
will fail the `if elems[0] in removes` check.
"""
qamock = mock.Mock()
qamock.stdout = "ccc\nbbb,bbb-0:1.0-1.x86_64\nccc,ccc-0:1.0-1.x86_64"
rcmock = mock.Mock()
rcmock.stdout = ""
with mock.patch("rmdepcheck.shared.SUBPCAPTURE", side_effect=[qamock, rcmock]):
with tempfile.TemporaryDirectory(prefix="testrdc", dir="/var/tmp") as dnftemp:
dnfargs = get_dnf_args(dnftemp, "x86_64")
# this shouldn't raise an exception
get_modified_and_new_repoclosure(
["file:///foo/bar"], [], [], "", [], ["ccc"], dnfargs
)
def test_get_source_packages(self):
with tempfile.TemporaryDirectory(prefix="testrdc", dir="/var/tmp") as dnftemp:
dnfargs = get_dnf_args(dnftemp, "x86_64")
sources = get_source_packages([f"file://{REPOS}/new"], dnfargs)
assert sources == {"111", "222", "aaa", "ccc", "eee", "fff", "ggg"}
def test_handle_preexisting(self):
fixederrors = [
("libreoffice-langpack-lv-1:26.2.2.1-0.1.eln155.x86_64", "ogrepo", "mythes-lv"),
("libreoffice-langpack-nb-1:26.2.2.1-0.1.eln155.x86_64", "ogrepo", "mythes-nb"),
("libreoffice-langpack-nn-1:26.2.2.1-0.1.eln155.x86_64", "ogrepo", "mythes-nn"),
("someotherpackage-1.0-1.eln155.x86_64", "newrepo", "someotherdep"),
]
newrc = [
("libreoffice-langpack-lv-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "mythes-lv"),
("libreoffice-langpack-nb-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "mythes-nb"),
("libreoffice-langpack-nn-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "mythes-nn"),
("libreoffice-langpack-nn-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "foo"),
("someotherpackage-1.0-1.eln155.x86_64", "newrepo", "mythes-lv"),
]
# this should null out the 'matching' errors (the first three in
# each list), but leave the 'non-matching' ones (the others)
ret = handle_preexisting(fixederrors, newrc)
assert fixederrors == [
("someotherpackage-1.0-1.eln155.x86_64", "newrepo", "someotherdep"),
]
assert newrc == [
("libreoffice-langpack-nn-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "foo"),
("someotherpackage-1.0-1.eln155.x86_64", "newrepo", "mythes-lv"),
]
# we should return the errors dropped from 'newrc' (so these can
# be reported separately)
assert ret == [
("libreoffice-langpack-lv-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "mythes-lv"),
("libreoffice-langpack-nb-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "mythes-nb"),
("libreoffice-langpack-nn-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "mythes-nn"),
]
def test_url_check(self):
url_check("file:///foo/bar", fileok=True)
with pytest.raises(ValueError):
url_check("file:///foo/bar", fileok=False)
url_check("https://www.some.where", fileok=True)
url_check("http://some.where.insecure", fileok=True)
# Test automatic conversion of existing local paths to file:// URIs
result = url_check(REPOS, fileok=True)
assert result == f"file://{REPOS}"
with pytest.raises(ValueError):
url_check(REPOS, fileok=False)
# Test with relative path that exists
result = url_check("tests", fileok=True)
assert result.startswith("file://")
assert result.endswith("/tests")
with pytest.raises(ValueError):
url_check("ftp://1997.called", fileok=True)
# Non-existent paths without scheme should still raise ValueError
with pytest.raises(ValueError):
url_check("whatisthis", fileok=True)
@mock.patch("platform.machine", return_value="foobar")
def test_check_arch(self, _):
assert check_arch("x86_64") == "x86_64"
assert check_arch("") == "foobar"
@mock.patch("subprocess.run", autospec=True)
def test_check_utils(self, mock_run):
check_utils((("dnf", "--version"),))
mock_run.side_effect = FileNotFoundError
with pytest.raises(SystemExit) as excinfo:
check_utils((("dnf", "--version"),))
assert excinfo.value.code == "Please install missing required utilities: dnf"
def test_format_rc_errors(capsys):
errs = [
("python3-wxnatpy-0.4.0-13.fc42.noarch", "baserepo0", "python(abi) = 3.13"),
("python3-wxnatpy-0.4.0-13.fc42.noarch", "baserepo0", "python3.13dist(wxpython) >= 4"),
("python3-wxnatpy-0.4.0-13.fc42.noarch", "baserepo0", "python3.13dist(xnat) >= 0.3.3"),
("python3-x3dh-1.0.4-3.fc43.noarch", "baserepo1", "python3.14dist(pydantic) >= 1.7.4"),
]
rmdepcheck.format_rc_errors(errs)
captured = capsys.readouterr()
with open(f"{TESTDATA}/test_format_rc_errors.txt", "r", encoding="utf-8") as fh:
exptext = fh.read()
assert captured.out == exptext
class TestRmdepcheck:
"""Unit tests for things in rmdepcheck.rmdepcheck."""
def test_comma_url(self):
assert rmdepcheck.comma_url("https://www.some.where") == ["https://www.some.where"]
assert rmdepcheck.comma_url("https://www.some.where,file:///foo/bar") == [
"https://www.some.where",
"file:///foo/bar",
]
with pytest.raises(ValueError):
rmdepcheck.comma_url("https://www.some.where,ftp://1997.called")
with pytest.raises(ValueError):
rmdepcheck.comma_url("ftp://1997.called")
with pytest.raises(ValueError):
rmdepcheck.comma_url("https://www.some.where,whatisthis")
def test_comma_list(self):
assert rmdepcheck.comma_list("foo,bar") == ["foo", "bar"]
assert rmdepcheck.comma_list("") == []
@mock.patch("rmdepcheck.rmdepcheck.check_utils", side_effect=KeyboardInterrupt)
def test_ctrl_c(self, _, capsys):
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 1
captured = capsys.readouterr()
assert captured.err == "Interrupted, exiting...\n"
def test_get_base_repoclosure():
repo = f"file://{REPOS}/base"
with open(f"{TESTDATA}/test_get_base_repoclosure.txt", "r", encoding="utf-8") as testfh:
expected = testfh.read()
expected = expected.replace("{HASH}", rmdepcheck.hash_repo(repo))
ret = rmdepcheck.get_base_repoclosure([repo], [], [])
assert ret == expected
class TestRdcelwrap:
"Unit tests for things in rmdepcheck.rdcelwrap." ""
def test_split_put_error(self):
with pytest.raises(ValueError) as err:
rdcelwrap.split_put(
pathlib.Path("/non/existent/place"),
pathlib.Path("/whatever"),
{"foo": "bar"},
"x86_64",
)
assert str(err.value) == "split_put: invalid put value /non/existent/place"
def test_get_variant_repos_arch(self):
fakeci = {
"payload": {
"variants": {
"BaseOS": {
"id": "BaseOS",
"paths": {
"repository": {
"someotherarch": "somepath",
}
},
}
}
}
}
# should not crash
assert not rdcelwrap.get_variant_repos("somecompose", fakeci, "somearch")
def test_pungi_config_url(self):
fakeci = {
"payload": {
"release": {
"version": "eln",
"short": "Fedora",
}
}
}
ret = rdcelwrap.pungi_config_url(fakeci)
assert ret == (
"https://forge.fedoraproject.org"
"/releng/pungi-fedora/raw/branch/eln/fedora/override.conf"
)
fakeci["payload"]["release"]["short"] = "CentOS"
fakeci["payload"]["release"]["version"] = "10.1"
with pytest.raises(ValueError) as err:
rdcelwrap.pungi_config_url(fakeci)
assert str(err.value) == "Don't know Pungi config URL for CentOS 10.1"
@mock.patch("rmdepcheck.rdcelwrap.SESSION.get", autospec=True)
def test_get_val_not_found(self, fakeget):
fakeci = {
"payload": {
"release": {
"version": "eln",
"short": "Fedora",
}
}
}
fakeget.return_value.text = "nope, not in here"
with pytest.raises(ValueError) as err:
rdcelwrap.get_val(fakeci)
assert str(err.value) == (
"Could not find variant_as_lookaside in configuration at https://"
"forge.fedoraproject.org/releng/pungi-fedora/raw/branch/eln/fedora/override.conf"
)
@mock.patch("rmdepcheck.rdcelwrap.check_utils", side_effect=KeyboardInterrupt)
def test_ctrl_c(self, _, capsys):
with pytest.raises(SystemExit) as excinfo:
rdcelwrap.main()
assert excinfo.value.code == 1
captured = capsys.readouterr()
assert captured.err == "Interrupted, exiting...\n"
def test_get_modified_and_new_repoclosure():
brepo = f"file://{REPOS}/base"
nrepo = f"file://{REPOS}/new"
with open(f"{TESTDATA}/test_get_modified_repoclosure.txt", "r", encoding="utf-8") as testfh:
expectedmod = testfh.read()
expectedmod = expectedmod.replace("{HASH}", rmdepcheck.hash_repo(brepo))
with open(f"{TESTDATA}/test_get_new_repoclosure.txt", "r", encoding="utf-8") as testfh:
expectednew = testfh.read()
expectednew = expectednew.replace("{HASH}", rmdepcheck.hash_repo(nrepo))
ret = rmdepcheck.get_modified_and_new_repoclosure(
[brepo], [], [], [nrepo], ["aaa", "ccc", "eee", "fff", "ggg"]
class TestE2E:
"""End-to-end functional tests for rmdepcheck and rdcelwrap."""
def test_e2e_null(self, capsys):
"""End-to-end test using an empty repository, to test what happens
when no changes are found.
"""
sys.argv = ["rmdepcheck.py", f"file://{REPOS}/base", f"file://{REPOS}/empty"]
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 0
captured = capsys.readouterr()
assert captured.out == ""
@pytest.mark.parametrize("output", ("human", "json", "onlyerrors"))
def test_e2e_devel(self, output, capsys):
"""End-to-end test similar to a Rawhide or Branched case, with
a single modified base repo and a single check repo. Also tests
JSON output.
"""
sys.argv = ["rmdepcheck.py", f"file://{REPOS}/base", f"file://{REPOS}/new"]
if output == "json":
sys.argv.insert(1, "--json")
if output == "onlyerrors":
sys.argv.insert(1, "--onlyerrors")
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 3
captured = capsys.readouterr()
if output == "human":
fname = "test_e2e_devel.txt"
elif output == "onlyerrors":
fname = "test_e2e_devel_onlyerrors.txt"
else:
fname = "test_e2e_devel_json.txt"
with open(f"{TESTDATA}/{fname}", "r", encoding="utf-8") as fh:
exptext = fh.read()
exptext = exptext.replace("{REPOS}", REPOS)
assert captured.out == exptext
def test_e2e_updates(self, capsys):
"""End-to-end test similar to a stable Fedora release, with one
non-modified 'frozen' base repo and one modified updates repo.
"""
sys.argv = [
"rmdepcheck.py",
"--nmbaserepos",
f"file://{REPOS}/base",
f"file://{REPOS}/updates",
f"file://{REPOS}/new",
]
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 3
captured = capsys.readouterr()
with open(f"{TESTDATA}/test_e2e_updates.txt", "r", encoding="utf-8") as fh:
exptext = fh.read()
exptext = exptext.replace("{REPOS}", REPOS)
assert captured.out == exptext
@mock.patch(
"rmdepcheck.rdcelwrap.pungi_config_url",
autospec=True,
return_value="http://localhost:5001/override.conf",
)
assert ret == (expectedmod, expectednew)
def test_e2e_eln(self, _, capsys, http): # pylint: disable=unused-argument
"""End-to-end test of the EL mode. This also exercises the non-
checked repository feature. The setup mimics "real" EL, more or
less. There are BaseOS, CRB, Extras and Buildroot repos. BaseOS
contains epa and epb. Extras contains epc. CRB contains epd and
epe. Buildroot contains epf. epe and epf both require epa = 1.0.
The "update" contains epa 2.0, an epb that requires epc, and an
epd that requires epa.
This sets us several expectations. The update breaks both epe and
epf, but we should only see a report about epe, since epf is in
the buildroot and so should be ignored. epb requiring epc is
wrong because BaseOS cannot require packages in Extras; this
should be reported. It should only be reported *once*, though,
when we're checking BaseOS, even though the BaseOS repos will be
in scope for each run (testing non-checked base repos). epd
requiring epa is fine; no error should be reported.
"""
sys.argv = [
"rdcelwrap.py",
"--arch",
"x86_64",
"http://localhost:5001",
f"{REPOS}/elnnew",
]
with pytest.raises(SystemExit) as excinfo:
rdcelwrap.main()
assert excinfo.value.code == 3
captured = capsys.readouterr()
with open(f"{TESTDATA}/test_e2e_eln.txt", "r", encoding="utf-8") as fh:
exptext = fh.read()
exptext = exptext.replace("{REPOS}", REPOS)
assert captured.out == exptext
def test_e2e_removes(self, capsys):
"""End-to-end test of the alternate --removes mode."""
sys.argv = ["rmdepcheck.py", "--removes", f"file://{REPOS}/base", "aaa,eee"]
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 1
captured = capsys.readouterr()
with open(f"{TESTDATA}/test_e2e_removes.txt", "r", encoding="utf-8") as fh:
exptext = fh.read()
exptext = exptext.replace("{REPOS}", REPOS)
assert captured.out == exptext
@pytest.mark.parametrize("output", ("human", "json"))
def test_e2e_unfixed(self, output, capsys):
"""End-to-end test where the new repository contains a package
with an unfixed pre-existing dependency issue.
"""
sys.argv = ["rmdepcheck.py", f"file://{REPOS}/base", f"file://{REPOS}/unfixed"]
if output == "json":
sys.argv.insert(1, "--json")
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 4
captured = capsys.readouterr()
if output == "human":
fname = "test_e2e_unfixed.txt"
else:
fname = "test_e2e_unfixed_json.txt"
with open(f"{TESTDATA}/{fname}", "r", encoding="utf-8") as fh:
exptext = fh.read()
exptext = exptext.replace("{REPOS}", REPOS)
assert captured.out == exptext
def test_get_modified_and_new_repoclosure_commasafe():
"""Check we survive repoquery output lines that are, precisely,
a string from 'removes'. This is a pretty unlikely scenario, but
hey. Any other line with no commas is actually safe because it
will fail the `if elems[0] in removes` check.
"""
qamock = mock.Mock()
qamock.stdout = "ccc\nbbb,bbb-0:1.0-1.x86_64\nccc,ccc-0:1.0-1.x86_64"
rcmock = mock.Mock()
rcmock.stdout = ""
with mock.patch("rmdepcheck.SUBPCAPTURE", side_effect=[qamock, rcmock]):
# this shouldn't raise an exception
rmdepcheck.get_modified_and_new_repoclosure(["file:///foo/bar"], [], [], [], ["ccc"])
def test_get_source_packages():
sources = rmdepcheck.get_source_packages([f"file://{REPOS}/new"])
assert sources == {"111", "222", "aaa", "ccc", "eee", "fff", "ggg"}
def test_handle_preexisting():
fixederrors = [
("libreoffice-langpack-lv-1:26.2.2.1-0.1.eln155.x86_64", "ogrepo", "mythes-lv"),
("libreoffice-langpack-nb-1:26.2.2.1-0.1.eln155.x86_64", "ogrepo", "mythes-nb"),
("libreoffice-langpack-nn-1:26.2.2.1-0.1.eln155.x86_64", "ogrepo", "mythes-nn"),
("someotherpackage-1.0-1.eln155.x86_64", "newrepo", "someotherdep"),
]
newrc = [
("libreoffice-langpack-lv-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "mythes-lv"),
("libreoffice-langpack-nb-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "mythes-nb"),
("libreoffice-langpack-nn-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "mythes-nn"),
("libreoffice-langpack-nn-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "foo"),
("someotherpackage-1.0-1.eln155.x86_64", "newrepo", "mythes-lv"),
]
# this should null out the 'matching' errors (the first three in
# each list), but leave the 'non-matching' ones (the others)
ret = rmdepcheck.handle_preexisting(fixederrors, newrc)
assert fixederrors == [
("someotherpackage-1.0-1.eln155.x86_64", "newrepo", "someotherdep"),
]
assert newrc == [
("libreoffice-langpack-nn-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "foo"),
("someotherpackage-1.0-1.eln155.x86_64", "newrepo", "mythes-lv"),
]
# we should return the errors dropped from 'newrc' (so these can
# be reported separately)
assert ret == [
("libreoffice-langpack-lv-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "mythes-lv"),
("libreoffice-langpack-nb-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "mythes-nb"),
("libreoffice-langpack-nn-1:26.2.2.2-0.1.eln155.x86_64", "newrepo", "mythes-nn"),
]
def test_url_check():
rmdepcheck.url_check("file:///foo/bar")
rmdepcheck.url_check("https://www.some.where")
rmdepcheck.url_check("http://some.where.insecure")
# Test automatic conversion of existing local paths to file:// URIs
result = rmdepcheck.url_check(REPOS)
assert result == f"file://{REPOS}"
# Test with relative path that exists
result = rmdepcheck.url_check("tests")
assert result.startswith("file://")
assert result.endswith("/tests")
with pytest.raises(ValueError):
rmdepcheck.url_check("ftp://1997.called")
# Non-existent paths without scheme should still raise ValueError
with pytest.raises(ValueError):
rmdepcheck.url_check("whatisthis")
def test_comma_url():
assert rmdepcheck.comma_url("https://www.some.where") == ["https://www.some.where"]
assert rmdepcheck.comma_url("https://www.some.where,file:///foo/bar") == [
"https://www.some.where",
"file:///foo/bar",
]
with pytest.raises(ValueError):
rmdepcheck.comma_url("https://www.some.where,ftp://1997.called")
with pytest.raises(ValueError):
rmdepcheck.comma_url("ftp://1997.called")
with pytest.raises(ValueError):
rmdepcheck.comma_url("https://www.some.where,whatisthis")
def test_comma_list():
assert rmdepcheck.comma_list("foo,bar") == ["foo", "bar"]
assert rmdepcheck.comma_list("") == []
@mock.patch("platform.machine", return_value="foobar")
def test_check_arch(_):
assert rmdepcheck.check_arch("x86_64") == "x86_64"
assert rmdepcheck.check_arch("") == "foobar"
@mock.patch("subprocess.run", autospec=True)
def test_check_dnf(mock_run):
rmdepcheck.check_dnf()
mock_run.side_effect = FileNotFoundError
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.check_dnf()
assert excinfo.value.code == "Please install missing required utilities: dnf"
@mock.patch("rmdepcheck.check_dnf", side_effect=KeyboardInterrupt)
def test_ctrl_c(_, capsys):
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 1
captured = capsys.readouterr()
assert captured.err == "Interrupted, exiting...\n"
def test_e2e_null(capsys):
"""End-to-end test using an empty repository, to test what happens
when no changes are found.
"""
sys.argv = ["rmdepcheck.py", f"file://{REPOS}/base", f"file://{REPOS}/empty"]
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 0
captured = capsys.readouterr()
assert captured.out == ""
@pytest.mark.parametrize("output", ("human", "json", "onlyerrors"))
def test_e2e_devel(output, capsys):
"""End-to-end test similar to a Rawhide or Branched case, with
a single modified base repo and a single check repo. Also tests
JSON output.
"""
sys.argv = ["rmdepcheck.py", f"file://{REPOS}/base", f"file://{REPOS}/new"]
if output == "json":
sys.argv.insert(1, "--json")
if output == "onlyerrors":
sys.argv.insert(1, "--onlyerrors")
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 3
captured = capsys.readouterr()
if output == "human":
fname = "test_e2e_devel.txt"
elif output == "onlyerrors":
fname = "test_e2e_devel_onlyerrors.txt"
else:
fname = "test_e2e_devel_json.txt"
with open(f"{TESTDATA}/{fname}", "r", encoding="utf-8") as fh:
exptext = fh.read()
exptext = exptext.replace("{REPOS}", REPOS)
assert captured.out == exptext
def test_e2e_updates(capsys):
"""End-to-end test similar to a stable Fedora release, with one
non-modified 'frozen' base repo and one modified updates repo.
"""
sys.argv = [
"rmdepcheck.py",
"--nmbaserepos",
f"file://{REPOS}/base",
f"file://{REPOS}/updates",
f"file://{REPOS}/new",
]
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 3
captured = capsys.readouterr()
with open(f"{TESTDATA}/test_e2e_updates.txt", "r", encoding="utf-8") as fh:
exptext = fh.read()
exptext = exptext.replace("{REPOS}", REPOS)
assert captured.out == exptext
def test_e2e_unchecked(capsys):
"""End-to-end test with a smaller repo as the base repo and a
larger buildroot repo as a non-checked base repo. The new repo
contains a package that breaks the deps of one package in the base
repo and one package in the non-checked base repo, and itself
requires a package from the non-checked base repo. We expect to
see a failure for the broken base repo package, but no failure for
the broken non-checked base repo package or the new package itself.
"""
sys.argv = [
"rmdepcheck.py",
"--ncbaserepos",
f"file://{REPOS}/elnroot",
f"file://{REPOS}/elnbase",
f"file://{REPOS}/elnnew",
]
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 1
captured = capsys.readouterr()
with open(f"{TESTDATA}/test_e2e_eln.txt", "r", encoding="utf-8") as fh:
exptext = fh.read()
exptext = exptext.replace("{REPOS}", REPOS)
assert captured.out == exptext
def test_e2e_removes(capsys):
"""End-to-end test of the alternate --removes mode."""
sys.argv = ["rmdepcheck.py", "--removes", f"file://{REPOS}/base", "aaa,eee"]
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 1
captured = capsys.readouterr()
with open(f"{TESTDATA}/test_e2e_removes.txt", "r", encoding="utf-8") as fh:
exptext = fh.read()
exptext = exptext.replace("{REPOS}", REPOS)
assert captured.out == exptext
@pytest.mark.parametrize("output", ("human", "json"))
def test_e2e_unfixed(output, capsys):
"""End-to-end test where the new repository contains a package
with an unfixed pre-existing dependency issue.
"""
sys.argv = ["rmdepcheck.py", f"file://{REPOS}/base", f"file://{REPOS}/unfixed"]
if output == "json":
sys.argv.insert(1, "--json")
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.main()
assert excinfo.value.code == 4
captured = capsys.readouterr()
if output == "human":
fname = "test_e2e_unfixed.txt"
else:
fname = "test_e2e_unfixed_json.txt"
with open(f"{TESTDATA}/{fname}", "r", encoding="utf-8") as fh:
exptext = fh.read()
exptext = exptext.replace("{REPOS}", REPOS)
assert captured.out == exptext
# vim: set textwidth=100 ts=8 et sw=4:

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,27 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
<revision>1777498820</revision>
<revision>1778717667</revision>
<data type="primary">
<checksum type="sha256">6b696476761c56d8bceb3e8997d31ca6977aaf63901130fe94c708e7d0cf429d</checksum>
<open-checksum type="sha256">54c3435b6735114519dd995763d0a654a564872f34118ce620e7148d7488738e</open-checksum>
<location href="repodata/6b696476761c56d8bceb3e8997d31ca6977aaf63901130fe94c708e7d0cf429d-primary.xml.zst"/>
<timestamp>1777498820</timestamp>
<size>1281</size>
<checksum type="sha256">725d5a8541dc6ac107257c1ccd264b53cfdfb601e10f1dffe1db40462fcab118</checksum>
<open-checksum type="sha256">577f2cb77ecdf002fbcfc3d3d3d5972e9574d262a0a32068f5b795556d6a1bcf</open-checksum>
<location href="repodata/725d5a8541dc6ac107257c1ccd264b53cfdfb601e10f1dffe1db40462fcab118-primary.xml.zst"/>
<timestamp>1778717667</timestamp>
<size>1278</size>
<open-size>10594</open-size>
</data>
<data type="filelists">
<checksum type="sha256">1acaeb6689285db3bfe681d374748eed2bc7cd9c1af87b32a27819f776ecacb6</checksum>
<open-checksum type="sha256">05695974b3212a3e0a658e431c4c6eb1cf20b4346365b9a062c4469e7a85c6a1</open-checksum>
<location href="repodata/1acaeb6689285db3bfe681d374748eed2bc7cd9c1af87b32a27819f776ecacb6-filelists.xml.zst"/>
<timestamp>1777498820</timestamp>
<size>621</size>
<checksum type="sha256">934bc8a9464d6232eb2e22aea3275da638dd1d733d9af49f402ac5d0072b505c</checksum>
<open-checksum type="sha256">863daf657e56f2234c416ab3878b88305619ab38614dee37253281a6f38cd296</open-checksum>
<location href="repodata/934bc8a9464d6232eb2e22aea3275da638dd1d733d9af49f402ac5d0072b505c-filelists.xml.zst"/>
<timestamp>1778717667</timestamp>
<size>624</size>
<open-size>1722</open-size>
</data>
<data type="other">
<checksum type="sha256">6868a9258bc5b257d087b3f926cbdbc014866cac98e1c7047bb0de5c8f4e8b63</checksum>
<open-checksum type="sha256">29e4f16b3e7312bdeecc87f41baa5f8200df3793aaab472e131dcc6e010130b8</open-checksum>
<location href="repodata/6868a9258bc5b257d087b3f926cbdbc014866cac98e1c7047bb0de5c8f4e8b63-other.xml.zst"/>
<timestamp>1777498820</timestamp>
<checksum type="sha256">9a69e354abcfd9f7de70b62321bab6263e340615e628cbf96518ffe4ab1bc376</checksum>
<open-checksum type="sha256">b62ee7d9a3bfa4aed63880cf15f0022c25930473ec0a5004388db138c165b285</open-checksum>
<location href="repodata/9a69e354abcfd9f7de70b62321bab6263e340615e628cbf96518ffe4ab1bc376-other.xml.zst"/>
<timestamp>1778717667</timestamp>
<size>710</size>
<open-size>2838</open-size>
</data>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
<revision>1778717670</revision>
<data type="primary">
<checksum type="sha256">69a3730a283b85a4b3cff7d04bfde3b2b234f0607ebc17319d7a8d143a8e066e</checksum>
<open-checksum type="sha256">e1e2ffd2fb1ee76f87b70750d00ca5677a252b397ab6c2389137a0c33e7b359f</open-checksum>
<location href="repodata/69a3730a283b85a4b3cff7d04bfde3b2b234f0607ebc17319d7a8d143a8e066e-primary.xml.zst"/>
<timestamp>1778717670</timestamp>
<size>123</size>
<open-size>167</open-size>
</data>
<data type="filelists">
<checksum type="sha256">9b07d97dc6ececed89aac0650b67bfb292647fe9fbaca48f629465be5f53f82b</checksum>
<open-checksum type="sha256">bf9808b81cb2dbc54b4b8e35adc584ddcaa73bd81f7088d73bf7dbbada961310</open-checksum>
<location href="repodata/9b07d97dc6ececed89aac0650b67bfb292647fe9fbaca48f629465be5f53f82b-filelists.xml.zst"/>
<timestamp>1778717670</timestamp>
<size>118</size>
<open-size>125</open-size>
</data>
<data type="other">
<checksum type="sha256">6b37cc67608a24beaa81e1191d218f2ffd6b1191dceb5c100bac2e66249d518d</checksum>
<open-checksum type="sha256">e0ed5e0054194df036cf09c1a911e15bf2a4e7f26f2a788b6f47d53e80717ccc</open-checksum>
<location href="repodata/6b37cc67608a24beaa81e1191d218f2ffd6b1191dceb5c100bac2e66249d518d-other.xml.zst"/>
<timestamp>1778717670</timestamp>
<size>117</size>
<open-size>121</open-size>
</data>
</repomd>

Binary file not shown.

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
<revision>1778717670</revision>
<data type="primary">
<checksum type="sha256">9daa049560d116a0c3e824db9385975140f68e97a03ae87214197284cbbbfad8</checksum>
<open-checksum type="sha256">3da88c933c481526801b2d09467c8e54e4edb8f54bf98422684c243650fdee96</open-checksum>
<location href="repodata/9daa049560d116a0c3e824db9385975140f68e97a03ae87214197284cbbbfad8-primary.xml.zst"/>
<timestamp>1778717670</timestamp>
<size>676</size>
<open-size>2163</open-size>
</data>
<data type="filelists">
<checksum type="sha256">e9b667b6a1059447627647ab73574fdee0a9c934cc7bfcf739f32edd1afae3d2</checksum>
<open-checksum type="sha256">de153bb5dc84de1eabec3de3762909234945d17228ca42b53cee3aa4b3da63f7</open-checksum>
<location href="repodata/e9b667b6a1059447627647ab73574fdee0a9c934cc7bfcf739f32edd1afae3d2-filelists.xml.zst"/>
<timestamp>1778717670</timestamp>
<size>279</size>
<open-size>445</open-size>
</data>
<data type="other">
<checksum type="sha256">73c134c24423339ee3c1b12e215c925e853b58ade283876a72ca7b92fbf246c4</checksum>
<open-checksum type="sha256">a1a6a080f1db568019eee61e3b62d74f3dafcb40867b0f2faf218585b7059123</open-checksum>
<location href="repodata/73c134c24423339ee3c1b12e215c925e853b58ade283876a72ca7b92fbf246c4-other.xml.zst"/>
<timestamp>1778717670</timestamp>
<size>352</size>
<open-size>665</open-size>
</data>
</repomd>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
<revision>1778717671</revision>
<data type="primary">
<checksum type="sha256">8d4fa97f0d4d6bdeca349744217b0e9d694aea2cef332765e97c2252c4df6431</checksum>
<open-checksum type="sha256">803c8426b401a9880c4f3e606fd7648a4d619ad1b9f64f029ba2edf0d922fcff</open-checksum>
<location href="repodata/8d4fa97f0d4d6bdeca349744217b0e9d694aea2cef332765e97c2252c4df6431-primary.xml.zst"/>
<timestamp>1778717671</timestamp>
<size>626</size>
<open-size>1265</open-size>
</data>
<data type="filelists">
<checksum type="sha256">4c41bd6e67d5b8fc2da462e55f106fcdc729daf0e722ace44fbf1f0688f4cf68</checksum>
<open-checksum type="sha256">69b6ea42561116fa83b8c836736c307693c9d425c6b5d8c9793c86bfa15069bd</open-checksum>
<location href="repodata/4c41bd6e67d5b8fc2da462e55f106fcdc729daf0e722ace44fbf1f0688f4cf68-filelists.xml.zst"/>
<timestamp>1778717671</timestamp>
<size>223</size>
<open-size>285</open-size>
</data>
<data type="other">
<checksum type="sha256">f4ecf39d19149d8918b7485eea237aa87723615393e5f15408b577e2114818d7</checksum>
<open-checksum type="sha256">6afc2f6d415b482a37ee91bb209367ff45b7f6f3434775ee219e73abf819ae72</open-checksum>
<location href="repodata/f4ecf39d19149d8918b7485eea237aa87723615393e5f15408b577e2114818d7-other.xml.zst"/>
<timestamp>1778717671</timestamp>
<size>304</size>
<open-size>393</open-size>
</data>
</repomd>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
<revision>1778717670</revision>
<data type="primary">
<checksum type="sha256">adce22870e107115797acfb5b60eb3ae16af020f137c2dd783447099c23683cb</checksum>
<open-checksum type="sha256">d0ab817c34b063b3ff19b611f8e4ce132fa18fc1b5f41c424e5f8faa0bc52aa5</open-checksum>
<location href="repodata/adce22870e107115797acfb5b60eb3ae16af020f137c2dd783447099c23683cb-primary.xml.zst"/>
<timestamp>1778717670</timestamp>
<size>704</size>
<open-size>2235</open-size>
</data>
<data type="filelists">
<checksum type="sha256">a939af998dafa1b5c771494f948ca1a12e7c8e0ba4b324f784caeca60580b944</checksum>
<open-checksum type="sha256">d4cbedeac0b70877906e01ff83afedde852c88d56f5bf4477e8f4ec80d02e9e5</open-checksum>
<location href="repodata/a939af998dafa1b5c771494f948ca1a12e7c8e0ba4b324f784caeca60580b944-filelists.xml.zst"/>
<timestamp>1778717670</timestamp>
<size>279</size>
<open-size>445</open-size>
</data>
<data type="other">
<checksum type="sha256">63510d69370f860af7ae99ffd909ecee7358765b80abb684b7a38f70e2bd977a</checksum>
<open-checksum type="sha256">df320cc37c560b6aa899b3344d87a701d307398f662dc4e7437a22adddc0c2b0</open-checksum>
<location href="repodata/63510d69370f860af7ae99ffd909ecee7358765b80abb684b7a38f70e2bd977a-other.xml.zst"/>
<timestamp>1778717670</timestamp>
<size>353</size>
<open-size>665</open-size>
</data>
</repomd>

View file

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
<revision>1778717670</revision>
<data type="primary">
<checksum type="sha256">d852b0588e7a6b861bf02df7aff77fd692bc597986c9fa22323a36fdb2d73a06</checksum>
<open-checksum type="sha256">323a9591c1a344cf10d2c9c5f541f118eb922e3e1e8a2274072459699647eb50</open-checksum>
<location href="repodata/d852b0588e7a6b861bf02df7aff77fd692bc597986c9fa22323a36fdb2d73a06-primary.xml.zst"/>
<timestamp>1778717670</timestamp>
<size>606</size>
<open-size>1165</open-size>
</data>
<data type="filelists">
<checksum type="sha256">718eae20ba82ada746eb049df2e88a420766a22c0a835e3ac0d666d1e5acfbaa</checksum>
<open-checksum type="sha256">dae4f03d98107c89b4af8380c4a6a7036b4809f7e68d60a3d2fa2a3a3429f010</open-checksum>
<location href="repodata/718eae20ba82ada746eb049df2e88a420766a22c0a835e3ac0d666d1e5acfbaa-filelists.xml.zst"/>
<timestamp>1778717670</timestamp>
<size>224</size>
<open-size>285</open-size>
</data>
<data type="other">
<checksum type="sha256">ef66a3ad019261b3603fddafbb1a8aafdc9ebb314370553c518be659d83b0ada</checksum>
<open-checksum type="sha256">d890073321ddf2010162771341ea12e3de65d0a4ef40a4cafa696d4cea29fe23</open-checksum>
<location href="repodata/ef66a3ad019261b3603fddafbb1a8aafdc9ebb314370553c518be659d83b0ada-other.xml.zst"/>
<timestamp>1778717670</timestamp>
<size>306</size>
<open-size>393</open-size>
</data>
</repomd>

View file

@ -0,0 +1,53 @@
{
"payload": {
"release": {
"internal": false,
"name": "Fedora",
"short": "Fedora",
"type": "ga",
"version": "eln"
},
"variants": {
"AppStream": {
"id": "AppStream",
"paths": {
"repository": {
"x86_64": "AppStream"
}
}
},
"BaseOS": {
"id": "BaseOS",
"paths": {
"repository": {
"x86_64": "BaseOS"
}
}
},
"Buildroot": {
"id": "Buildroot",
"paths": {
"repository": {
"x86_64": "Buildroot"
}
}
},
"CRB": {
"id": "CRB",
"paths": {
"repository": {
"x86_64": "CRB"
}
}
},
"Extras": {
"id": "Extras",
"paths": {
"repository": {
"x86_64": "Extras"
}
}
}
}
}
}

91
tests/testdata/repos/eln/override.conf vendored Normal file
View file

@ -0,0 +1,91 @@
# This files overrides default variables defined in the configs in
# the ../shared directory.
from images import *
# Fedora signing keys.
sigkeys = ['f577861e', '6d9f90a6']
# Architectures supported by Fedora ELN.
tree_arches = ['aarch64', 'ppc64le', 's390x', 'x86_64']
# For Fedora-ELN, we do not inherit builds from parent tags.
pkgset_koji_inherit = False
# No jigdo needed in Fedora.
create_jigdo = False
# We only build repositories, installer and images in Fedora so far.
skip_phases = [
"createiso",
"live_media",
"live_images",
"ostree",
"osbs",
]
# Enables macboot on x86_64 for all variants and disables upgrade image building
# everywhere.
# Use 4GB image size for all arches.
lorax_options = [
('^.*$', {
'x86_64': {
'nomacboot': True
},
'*': {
'rootfs_size': 4
}
})
]
# Drop the variants we do not care about from the variant_as_lookaside.
variant_as_lookaside = [
("AppStream", "BaseOS"),
("Extras", "BaseOS"),
("Extras", "AppStream"),
("Extras", "CRB"),
("HighAvailability", "BaseOS"),
("HighAvailability", "AppStream"),
("RT", "BaseOS"),
("RT", "AppStream"),
("NFV", "BaseOS"),
("NFV", "AppStream"),
("CRB", "BaseOS"),
("CRB", "AppStream"),
("SAP", "BaseOS"),
("SAP", "AppStream"),
("SAP", "HighAvailability"),
("SAPHANA", "BaseOS"),
("SAPHANA", "AppStream"),
("SAPHANA", "HighAvailability"),
("Buildroot", "BaseOS"),
("Buildroot", "AppStream"),
("Buildroot", "CRB"),
("Buildroot", "Extras"),
("Buildroot", "HighAvailability"),
("Buildroot", "NFV"),
("Buildroot", "RT"),
("Buildroot", "SAP"),
("Buildroot", "SAPHANA"),
]
# No product_id for Fedora.
product_id_allow_missing = False
# These will be inherited by live_media, live_images and image_build
global_release = '!RELEASE_FROM_LABEL_DATE_TYPE_RESPIN'
global_version = 'ELN'
# live_images ignores this in favor of live_target
global_target = 'eln'
# kiwi images need another target that uses old mock chroot
kiwibuild_target = 'eln-kiwi'
# kiwi image global configuration
kiwibuild_description_scm = 'git+https://pagure.io/fedora-kiwi-descriptions.git?#HEAD'
kiwibuild_description_path = 'Fedora-ELN.kiwi'
kiwibuild_version = '11'
kiwibuild_repo_releasever = 'eln'
# <name>-<version>-<ID>.<arch>
kiwibuild_bundle_name_format = '%N-%v-%I.%A'

View file

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
<revision>1777498823</revision>
<data type="primary">
<checksum type="sha256">f4ed4ad1632ef201cb823cb62f2408918097df27d66aaee819fc22ba37ab262c</checksum>
<open-checksum type="sha256">12c7b2df4b3e597c8a412e6b187f0379f9024b0c19db0a6a22615dd1b5b09962</open-checksum>
<location href="repodata/f4ed4ad1632ef201cb823cb62f2408918097df27d66aaee819fc22ba37ab262c-primary.xml.zst"/>
<timestamp>1777498823</timestamp>
<size>699</size>
<open-size>2263</open-size>
</data>
<data type="filelists">
<checksum type="sha256">e66e2d334117944d094138fdc62fda05db2e252cd29aeffa765a71451a8856dc</checksum>
<open-checksum type="sha256">48d1e74e7b0cb3bf5286c6bf23c19000cb94afdf982c113b926688e51b100a36</open-checksum>
<location href="repodata/e66e2d334117944d094138fdc62fda05db2e252cd29aeffa765a71451a8856dc-filelists.xml.zst"/>
<timestamp>1777498823</timestamp>
<size>279</size>
<open-size>445</open-size>
</data>
<data type="other">
<checksum type="sha256">6212147882b929cefe09ce147e26e1020b738656c9517eac9a52a058d7793a31</checksum>
<open-checksum type="sha256">c8b2793ecacedcdba8b304a308130a84ac2613447053514f1e0f040cf5220132</open-checksum>
<location href="repodata/6212147882b929cefe09ce147e26e1020b738656c9517eac9a52a058d7793a31-other.xml.zst"/>
<timestamp>1777498823</timestamp>
<size>351</size>
<open-size>665</open-size>
</data>
</repomd>

Binary file not shown.

View file

@ -1,28 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
<revision>1777498823</revision>
<revision>1778717671</revision>
<data type="primary">
<checksum type="sha256">e1fb8cbc0efa75785de59ffca8a3de5dd3928ab2075891beabbdedf2a74608b0</checksum>
<open-checksum type="sha256">edfd3c129d9db24508a6dcd38765281f02cdf98609587ce9ccc3885cd226237f</open-checksum>
<location href="repodata/e1fb8cbc0efa75785de59ffca8a3de5dd3928ab2075891beabbdedf2a74608b0-primary.xml.zst"/>
<timestamp>1777498823</timestamp>
<size>629</size>
<open-size>1265</open-size>
<checksum type="sha256">ec7341345e04def216d16e0db04b625570797b0142fe7150924d08f34acc1356</checksum>
<open-checksum type="sha256">c09d2fb0e387f9b1d76bddbda53a17ff410e7376c32765e8c1883e8aa5125e10</open-checksum>
<location href="repodata/ec7341345e04def216d16e0db04b625570797b0142fe7150924d08f34acc1356-primary.xml.zst"/>
<timestamp>1778717671</timestamp>
<size>764</size>
<open-size>3299</open-size>
</data>
<data type="filelists">
<checksum type="sha256">029e08c67f723be4b217594371bb7f9f223cb866b934d857d9bddb4234e9c942</checksum>
<open-checksum type="sha256">8519cd36f5dcc309402aa70eb2463eebd439a134f410ee2b052afe51dddb6265</open-checksum>
<location href="repodata/029e08c67f723be4b217594371bb7f9f223cb866b934d857d9bddb4234e9c942-filelists.xml.zst"/>
<timestamp>1777498823</timestamp>
<size>226</size>
<open-size>285</open-size>
<checksum type="sha256">6f2f32cc8b75d194c83ef1075936ed263a468f298ce34bf64a112a510aed46f8</checksum>
<open-checksum type="sha256">d1edbf9e8bb2c6455b8b5ad6506b19b3d89bea5e0e4cc07eb3f675ab536a090a</open-checksum>
<location href="repodata/6f2f32cc8b75d194c83ef1075936ed263a468f298ce34bf64a112a510aed46f8-filelists.xml.zst"/>
<timestamp>1778717671</timestamp>
<size>323</size>
<open-size>605</open-size>
</data>
<data type="other">
<checksum type="sha256">c083429e71ea2c4bea418159abf7debbf79dd70f21731b1b4acc5195658e7890</checksum>
<open-checksum type="sha256">bd83ff93ecd4e37aa7dc35a1dfc61a5c8d560c14eea47fe443c86ca184853289</open-checksum>
<location href="repodata/c083429e71ea2c4bea418159abf7debbf79dd70f21731b1b4acc5195658e7890-other.xml.zst"/>
<timestamp>1777498823</timestamp>
<size>305</size>
<open-size>393</open-size>
<checksum type="sha256">b5d3790249f642dafa96a7b5ba61938b50e19cc4c57fe63d7e4206a869c10925</checksum>
<open-checksum type="sha256">7246bec78e312d7765aa885c6e196cf347adf944323c9d7593d57ef92340f763</open-checksum>
<location href="repodata/b5d3790249f642dafa96a7b5ba61938b50e19cc4c57fe63d7e4206a869c10925-other.xml.zst"/>
<timestamp>1778717671</timestamp>
<size>398</size>
<open-size>937</open-size>
</data>
</repomd>

View file

@ -1,28 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
<revision>1777498823</revision>
<data type="primary">
<checksum type="sha256">5548c16c17c9999a288149e4c028529fea3de98d310a6f6b9aaf5f9913c7ac7b</checksum>
<open-checksum type="sha256">a1a05ef98f83ff3a3d3f9916306cd31ca7a948599849ec779a95d6f7e8a40214</open-checksum>
<location href="repodata/5548c16c17c9999a288149e4c028529fea3de98d310a6f6b9aaf5f9913c7ac7b-primary.xml.zst"/>
<timestamp>1777498823</timestamp>
<size>830</size>
<open-size>4359</open-size>
</data>
<data type="filelists">
<checksum type="sha256">081516c8122bf9e116d0314902b635ef595c121ba2e7a9d8d448de7e636c948b</checksum>
<open-checksum type="sha256">d25e63e1db6cb9a61a2a98c0ad43abda59e7826aa7551738079e02b16d75a912</open-checksum>
<location href="repodata/081516c8122bf9e116d0314902b635ef595c121ba2e7a9d8d448de7e636c948b-filelists.xml.zst"/>
<timestamp>1777498823</timestamp>
<size>366</size>
<open-size>765</open-size>
</data>
<data type="other">
<checksum type="sha256">117a29ab84c4728bf8da3b0b942c823be18f4d190e710ed797182925cfad70ba</checksum>
<open-checksum type="sha256">a68bfd0ad8980ca8b2f26e4498cba5d9eb2c30885ca8fd1cac84f655687fe63f</open-checksum>
<location href="repodata/117a29ab84c4728bf8da3b0b942c823be18f4d190e710ed797182925cfad70ba-other.xml.zst"/>
<timestamp>1777498823</timestamp>
<size>445</size>
<open-size>1209</open-size>
</data>
</repomd>

View file

@ -1,11 +1,11 @@
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
<revision>1777498823</revision>
<revision>1778717671</revision>
<data type="primary">
<checksum type="sha256">69a3730a283b85a4b3cff7d04bfde3b2b234f0607ebc17319d7a8d143a8e066e</checksum>
<open-checksum type="sha256">e1e2ffd2fb1ee76f87b70750d00ca5677a252b397ab6c2389137a0c33e7b359f</open-checksum>
<location href="repodata/69a3730a283b85a4b3cff7d04bfde3b2b234f0607ebc17319d7a8d143a8e066e-primary.xml.zst"/>
<timestamp>1777498823</timestamp>
<timestamp>1778717671</timestamp>
<size>123</size>
<open-size>167</open-size>
</data>
@ -13,7 +13,7 @@
<checksum type="sha256">9b07d97dc6ececed89aac0650b67bfb292647fe9fbaca48f629465be5f53f82b</checksum>
<open-checksum type="sha256">bf9808b81cb2dbc54b4b8e35adc584ddcaa73bd81f7088d73bf7dbbada961310</open-checksum>
<location href="repodata/9b07d97dc6ececed89aac0650b67bfb292647fe9fbaca48f629465be5f53f82b-filelists.xml.zst"/>
<timestamp>1777498823</timestamp>
<timestamp>1778717671</timestamp>
<size>118</size>
<open-size>125</open-size>
</data>
@ -21,7 +21,7 @@
<checksum type="sha256">6b37cc67608a24beaa81e1191d218f2ffd6b1191dceb5c100bac2e66249d518d</checksum>
<open-checksum type="sha256">e0ed5e0054194df036cf09c1a911e15bf2a4e7f26f2a788b6f47d53e80717ccc</open-checksum>
<location href="repodata/6b37cc67608a24beaa81e1191d218f2ffd6b1191dceb5c100bac2e66249d518d-other.xml.zst"/>
<timestamp>1777498823</timestamp>
<timestamp>1778717671</timestamp>
<size>117</size>
<open-size>121</open-size>
</data>

View file

@ -86,24 +86,34 @@ newddd = rpmfluff.SimpleRpmBuild("ddd", "3.0", "1", ["x86_64"])
newddd.add_requires("ccc = 3.0")
unfixed = (newddd,)
# ELN scenario packages. The "shipped repo" will contain only
# epa and epc. The "buildroot repo" will contain all four
# ELN scenario packages
epa = rpmfluff.SimpleRpmBuild("epa", "1.0", "1", ["x86_64"])
epb = rpmfluff.SimpleRpmBuild("epb", "1.0", "1", ["x86_64"])
epc = rpmfluff.SimpleRpmBuild("epc", "1.0", "1", ["x86_64"])
epd = rpmfluff.SimpleRpmBuild("epd", "1.0", "1", ["x86_64"])
alleln = (epa, epb, epc, epd)
shipeln = (epa, epc)
newepc = rpmfluff.SimpleRpmBuild("epc", "2.0", "1", ["x86_64"])
# these will both be broken by the new epc, but only epa is in the
# "shipped repo", epb is not
epa.add_requires("epc = 1.0")
epb.add_requires("epc = 1.0")
# this requirement can only be satisfied in the buildroot repo, we
# should not report that as an error
newepc.add_requires("epd = 1.0")
epe = rpmfluff.SimpleRpmBuild("epe", "1.0", "1", ["x86_64"])
epf = rpmfluff.SimpleRpmBuild("epe", "1.0", "1", ["x86_64"])
alleln = (epa, epb, epc, epd, epf)
baseeln = (epa, epb)
exteln = (epc,)
crbeln = (epd, epe)
rooteln = (epf,)
for pkg in allbase + allupd + allnew + unfixed + alleln + (newepc,):
newepa = rpmfluff.SimpleRpmBuild("epa", "2.0", "1", ["x86_64"])
newepb = rpmfluff.SimpleRpmBuild("epb", "2.0", "1", ["x86_64"])
newepd = rpmfluff.SimpleRpmBuild("epd", "2.0", "1", ["x86_64"])
neweln = (newepa, newepb, newepd)
# these will both be broken by the new epa, but epf should be ignored
# as it's buildroot-only
epe.add_requires("epa = 1.0")
epf.add_requires("epa = 1.0")
# this is invalid as baseos cannot depend on extras, should be caught
newepb.add_requires("epc")
# this is fine
newepd.add_requires("epa")
for pkg in allbase + allupd + allnew + unfixed + alleln + neweln:
pkg.addVendor("Fedora Project")
pkg.addPackager("Fedora Project")
@ -115,16 +125,25 @@ unfixed = rpmfluff.yumrepobuild.YumRepoBuild(unfixed)
unfixed.repoDir = "unfixed"
updates = rpmfluff.yumrepobuild.YumRepoBuild(allupd)
updates.repoDir = "updates"
elnbase = rpmfluff.yumrepobuild.YumRepoBuild(shipeln)
elnbase.repoDir = "elnbase"
elnroot = rpmfluff.yumrepobuild.YumRepoBuild(alleln)
elnroot.repoDir = "elnroot"
elnnew = rpmfluff.yumrepobuild.YumRepoBuild((newepc,))
elnbase = rpmfluff.yumrepobuild.YumRepoBuild(baseeln)
elnbase.repoDir = "eln/BaseOS"
elnext = rpmfluff.yumrepobuild.YumRepoBuild(exteln)
elnext.repoDir = "eln/Extras"
elncrb = rpmfluff.yumrepobuild.YumRepoBuild(crbeln)
elncrb.repoDir = "eln/CRB"
elnapp = rpmfluff.yumrepobuild.YumRepoBuild([])
elnapp.repoDir = "eln/AppStream"
elnroot = rpmfluff.yumrepobuild.YumRepoBuild(rooteln)
elnroot.repoDir = "eln/Buildroot"
elnnew = rpmfluff.yumrepobuild.YumRepoBuild(neweln)
elnnew.repoDir = "elnnew"
empty = rpmfluff.yumrepobuild.YumRepoBuild([])
empty.repoDir = "empty"
alldirs = [r.repoDir for r in (base, new, unfixed, updates, elnbase, elnroot, elnnew, empty)]
alldirs = [
r.repoDir
for r in (base, new, unfixed, updates, elnbase, elnext, elncrb, elnapp, elnroot, elnnew, empty)
]
def cleanup(repos=True):
@ -140,14 +159,18 @@ def cleanup(repos=True):
cleanup()
for _dir in alldirs:
os.mkdir(_dir)
os.makedirs(_dir)
base.make("x86_64", "i686")
new.make("x86_64")
unfixed.make("x86_64")
updates.make("x86_64")
elnbase.make("x86_64")
elnext.make("x86_64")
elncrb.make("x86_64")
elnapp.make("x86_64")
elnroot.make("x86_64")
elnnew.make("x86_64")
empty.make()
cleanup(repos=False)

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

View file

@ -1,28 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
<revision>1777498821</revision>
<revision>1778717669</revision>
<data type="primary">
<checksum type="sha256">182e2add8c3d3069057852ed9944419578aa72b31eb9233b18b6ce68fa86ba05</checksum>
<open-checksum type="sha256">4ffb8600b26cbe3f260b10f2f7c35e17c92839fe66e5f1f13e37ec170f4d0d16</open-checksum>
<location href="repodata/182e2add8c3d3069057852ed9944419578aa72b31eb9233b18b6ce68fa86ba05-primary.xml.zst"/>
<timestamp>1777498821</timestamp>
<size>1069</size>
<checksum type="sha256">0a7860c188784cab59dcc483eea7c38d8e9da287f45492897b95bd1a425da7ce</checksum>
<open-checksum type="sha256">2ba4a2dbf3603d8fde7bd5d1595d18058f834b485a9611f49e18acca29b3cc61</open-checksum>
<location href="repodata/0a7860c188784cab59dcc483eea7c38d8e9da287f45492897b95bd1a425da7ce-primary.xml.zst"/>
<timestamp>1778717669</timestamp>
<size>1067</size>
<open-size>7430</open-size>
</data>
<data type="filelists">
<checksum type="sha256">8e6cd282fee67920e9fd5728c68c86ad8d8ca56f9e8db935bd9358e22f4b86a8</checksum>
<open-checksum type="sha256">601f4c051e7fb1541e74b63516ad2f5fceecafae40b02a15bef878f1e941b159</open-checksum>
<location href="repodata/8e6cd282fee67920e9fd5728c68c86ad8d8ca56f9e8db935bd9358e22f4b86a8-filelists.xml.zst"/>
<timestamp>1777498821</timestamp>
<checksum type="sha256">cf45583b0c95946ca4ca3d567a3da5db1183158763f9b2dc006bc980a91f7393</checksum>
<open-checksum type="sha256">69839c5fe82af86f9d68cf88757f8886943d9d0ca5387636315eb27330670e7c</open-checksum>
<location href="repodata/cf45583b0c95946ca4ca3d567a3da5db1183158763f9b2dc006bc980a91f7393-filelists.xml.zst"/>
<timestamp>1778717669</timestamp>
<size>499</size>
<open-size>1245</open-size>
</data>
<data type="other">
<checksum type="sha256">d6dae028f2d94a20a5ada403a89d418b7d31c646d2350605999882725a5b0862</checksum>
<open-checksum type="sha256">bfbc10574abc73bf3586a122246953f8073cde56afb8b4470d856ffe0d7823b0</open-checksum>
<location href="repodata/d6dae028f2d94a20a5ada403a89d418b7d31c646d2350605999882725a5b0862-other.xml.zst"/>
<timestamp>1777498821</timestamp>
<size>581</size>
<checksum type="sha256">4f693bcdc44f61c53c77e39ee300085d9e54b731fb75a9b664a8e0b9f6516c09</checksum>
<open-checksum type="sha256">c5f2112ab0f5b051aa8dc39e900a07ac7ba66294f08f144708bba996cb5c7881</open-checksum>
<location href="repodata/4f693bcdc44f61c53c77e39ee300085d9e54b731fb75a9b664a8e0b9f6516c09-other.xml.zst"/>
<timestamp>1778717669</timestamp>
<size>580</size>
<open-size>2025</open-size>
</data>
</repomd>

Binary file not shown.

View file

@ -1,27 +1,27 @@
<?xml version="1.0" encoding="UTF-8"?>
<repomd xmlns="http://linux.duke.edu/metadata/repo" xmlns:rpm="http://linux.duke.edu/metadata/rpm">
<revision>1777498822</revision>
<revision>1778717669</revision>
<data type="primary">
<checksum type="sha256">daa25dfd3c818ca6b5104612d827dac24e70a2050986ed49457124116013f4ef</checksum>
<open-checksum type="sha256">ee07278548ed0fee3569e6b1aa00a4488ef82c0216d5fb835899edca81d00ebe</open-checksum>
<location href="repodata/daa25dfd3c818ca6b5104612d827dac24e70a2050986ed49457124116013f4ef-primary.xml.zst"/>
<timestamp>1777498822</timestamp>
<size>626</size>
<checksum type="sha256">dd80d9c03852999e37b04db876c82b0483a6e906c5884b4561d83a97d7c341c7</checksum>
<open-checksum type="sha256">291264d5bc3e5102ea3553e1f3a8017e7bed89c574fde750f0247c85c2003448</open-checksum>
<location href="repodata/dd80d9c03852999e37b04db876c82b0483a6e906c5884b4561d83a97d7c341c7-primary.xml.zst"/>
<timestamp>1778717669</timestamp>
<size>628</size>
<open-size>1265</open-size>
</data>
<data type="filelists">
<checksum type="sha256">77dfe7c593c3e37af3f924516b5aa25cc512a5b8070f75fbeb4b1e844973835b</checksum>
<open-checksum type="sha256">7e2dfb6357481ec47ff798fc40501850c014472d258cccc9ac49ec9c1d176acb</open-checksum>
<location href="repodata/77dfe7c593c3e37af3f924516b5aa25cc512a5b8070f75fbeb4b1e844973835b-filelists.xml.zst"/>
<timestamp>1777498822</timestamp>
<checksum type="sha256">9d54f4c3d3ab7c943b7e15193e2544bcb82bf7b2e8408ee3167134b0a453255d</checksum>
<open-checksum type="sha256">207660f59451194f04a8c198b5244310cf182ecdbaaae5e819f18ad3851acf1d</open-checksum>
<location href="repodata/9d54f4c3d3ab7c943b7e15193e2544bcb82bf7b2e8408ee3167134b0a453255d-filelists.xml.zst"/>
<timestamp>1778717669</timestamp>
<size>225</size>
<open-size>285</open-size>
</data>
<data type="other">
<checksum type="sha256">a583ea09de5b85171eb9110dacfd240e527828cff25324ce894d3dd9c80cf961</checksum>
<open-checksum type="sha256">747fadb7b8374294f06c6ed1c48f21a25d7ac797625c574d13adf81377341014</open-checksum>
<location href="repodata/a583ea09de5b85171eb9110dacfd240e527828cff25324ce894d3dd9c80cf961-other.xml.zst"/>
<timestamp>1777498822</timestamp>
<checksum type="sha256">ef5302dc1a621cb60e1087af579f4e2bb66e668033b107b8706a39f2c37f8026</checksum>
<open-checksum type="sha256">0e8c0475f1c246f3d2dbb831a437572872bb464a55e356b01808ad4009eec9ba</open-checksum>
<location href="repodata/ef5302dc1a621cb60e1087af579f4e2bb66e668033b107b8706a39f2c37f8026-other.xml.zst"/>
<timestamp>1778717669</timestamp>
<size>305</size>
<open-size>393</open-size>
</data>

Binary file not shown.

Some files were not shown because too many files have changed in this diff Show more