Initial version of rmdepcheck

Signed-off-by: Adam Williamson <awilliam@redhat.com>
This commit is contained in:
Adam Williamson 2025-06-18 12:38:05 +02:00
commit ec12468a08
69 changed files with 1295 additions and 0 deletions

11
.gitignore vendored Normal file
View file

@ -0,0 +1,11 @@
**/*.pyc
**/*.pyo
__pycache__/
build/
dist/
rmdepcheck.egg-info/
.cache/
.eggs/
.tox/
.coverage
coverage.xml

87
README.md Normal file
View file

@ -0,0 +1,87 @@
# rmdepcheck
rmdepcheck is an RPM dependency check tool based on a repository metadata modification approach.
It works by comparing a checked repository to one or more base repositories. First, checks are run
on the base repositories as-is. Next, modified copies of the base repositories' metadata is
created, with all packages from the same source RPM(s) as the package(s) in the checked
repositories removed. Finally, checks are run again on the modified base repositories, with the
checked repositories available to the dependency solver. The results of the two runs are compared.
New failures should indicate problems introduced by the checked repositories. Also, some relevant
checks are run on the checked repositories with reference to the modified base repositories.
Optionally, additional base repositories can be specified which will not be modified, and
additional new repositories can be specified which will not be checked directly. The former is
intended for testing scenarios like stable Fedora releases, which have a frozen release repository
which is never modified, and an updates repository which is updated. The latter is intended for
multilib scenarios; it may be desirable to use such an additional repository for packages for
the multilib arch(es), if e.g. installability of these should not be tested directly.
## Requirements
rmdepcheck has no run-time Python dependencies outside the standard library. However, it requires
several command-line utilities:
* dnf
* zstd
* curl
It checks for these, and will exit early with an error if any of them is not found. rmdepcheck
is written primarily for Red Hat-family distributions, but should in theory be usable anywhere
these utilities can be installed (and forward slashes act as directory separators).
## Installation
Installation of rmdepcheck is entirely optional, it can be run just as well directly from the
repository. Otherwise, rmdepcheck uses setuptools for installation and is PEP 518-compliant. You
can build and install with e.g. the `build` module and `pip`. rmdepcheck can also be installed
directly from PyPI with pip and other tools.
## Usage
Simple usage looks like this:
```
rmdepcheck https://a.base.repo.example/repo,file:///another/baserepo file:///the/testedrepo
```
The to-be-modified base repositories are specified as a comma-separated list. Repositories are
always specified as URLs. Only file:// , http:// and https:// URLs are accepted.
For more complex usage, see `rmdepcheck --help`.
## License
rmdepcheck is released under the [GPL](https://www.gnu.org/licenses/gpl.txt), version 3 or later.
See `COPYING` and the header of `rmdepcheck.py` itself.
## Contributing
Issues and pull requests can be filed in [Codeberg](https://codeberg.org/AdamWill/rmdepcheck).
Pull requests must be signed off (use the `-s` git argument). By signing off
your pull request you are agreeing to the
[Developer's Certificate of Origin](http://developercertificate.org/):
Developer's Certificate of Origin 1.1
By making a contribution to this project, I certify that:
(a) The contribution was created in whole or in part by me and I
have the right to submit it under the open source license
indicated in the file; or
(b) The contribution is based upon previous work that, to the best
of my knowledge, is covered under an appropriate open source
license and I have the right under that license to submit that
work with modifications, whether created in whole or in part
by me, under the same open source license (unless I am
permitted to submit under a different license), as indicated
in the file; or
(c) The contribution was provided directly to me by some other
person who certified (a), (b) or (c) and I have not modified
it.
(d) I understand and agree that this project and the contribution
are public and that a record of the contribution (including all
personal information I submit with it, including my sign-off) is
maintained indefinitely and may be redistributed consistent with
this project or the open source license(s) involved.

0
install.requires Normal file
View file

56
pyproject.toml Normal file
View file

@ -0,0 +1,56 @@
[project]
name = "rmdepcheck"
dynamic = ["dependencies"]
version = "1.0.0"
description = "RPM installability and repoclosure checks based on repomd.xml modification"
readme = "README.md"
license = { file="COPYING" }
requires-python = ">=3.9"
authors = [
{ name = "Adam Williamson", email = "awilliam@redhat.com" },
]
keywords = ["rpm", "dependency", "test", "check", "reverse"]
classifiers = [
"Development Status :: 5 - Production/Stable",
"Intended Audience :: Developers",
"License :: OSI Approved :: GNU General Public License v3 or later (GPLv3+)",
"Operating System :: POSIX :: Linux",
"Programming Language :: Python",
"Programming Language :: Python :: 3.9",
"Programming Language :: Python :: 3.10",
"Programming Language :: Python :: 3.11",
"Programming Language :: Python :: 3.12",
"Programming Language :: Python :: 3.13",
"Programming Language :: Python :: 3.14",
"Topic :: Internet :: WWW/HTTP",
"Topic :: System :: Archiving :: Packaging",
"Topic :: Software Development :: Testing",
]
[project.scripts]
rmdepcheck = "rmdepcheck:main"
[build-system]
requires = ["setuptools>=40.6.0", "setuptools-git", "wheel"]
build-backend = "setuptools.build_meta"
[tool.coverage.run]
parallel = true
branch = true
source_pkgs = ["rmdepcheck"]
[tool.coverage.paths]
source = [".", ".tox/**/site-packages"]
[tool.coverage.report]
show_missing = true
[tool.black]
# don't @ me, Hynek
line-length = 100
[tool.setuptools.dynamic]
dependencies = { file = ["install.requires"] }
[tool.setuptools.package-data]
"rmdepcheck" = ["py.typed"]

27
release.sh Executable file
View file

@ -0,0 +1,27 @@
#!/bin/bash
baddeps=""
# check deps
python3 -m build.__init__ || baddeps="python3-build"
rpm -q twine || baddeps="${baddeps} twine"
if [ -n "${baddeps}" ]; then
echo "${baddeps} must be installed!"
exit 1
fi
if [ "$#" != "1" ]; then
echo "Must pass release version!"
exit 1
fi
version=$1
name=rmdepcheck
sed -i -e "s,version = \".*\",version = \"${version}\", g" pyproject.toml
sed -i -e "s,__version__ = \".*\",__version__ = \"${version}\", g" ${name}.py
git add pyproject.toml ${name}.py
git commit -s -m "Release ${version}"
git push
git tag -a -m "Release ${version}" ${version}
git push origin ${version}
python3 -m build .
twine upload -r pypi dist/${name}-${version}*

347
rmdepcheck.py Executable file
View file

@ -0,0 +1,347 @@
#!/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 os
import subprocess
import sys
import tempfile
import xml.etree.ElementTree as et
from functools import partial
from typing import Sequence
from urllib.parse import urlparse
CURLARGS = ("curl", "-s", "--retry-delay", "10", "--max-time", "300", "--retry", "5")
# 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=*"]
XMLNS = {
"repo": "http://linux.duke.edu/metadata/repo",
"common": "http://linux.duke.edu/metadata/common",
"rpm": "http://linux.duke.edu/metadata/rpm",
}
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 = {}
def hash_repo(repo: str) -> str:
gothash = hashlib.sha256(repo.encode(encoding="utf-8")).hexdigest()[:8]
REPOHASHES[gothash] = repo
return gothash
def parse_repoclosure(rc: str) -> list[tuple[str, str, str]]:
out = []
pkg = None
for line in rc.splitlines():
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
# repoclosure doesn't handle rich deps with 'if' conditions
if line.strip().startswith("(") and " if " in line:
continue
if pkg:
out.append(pkg + (line.strip(),))
return out
def format_rc_errors(errors: list[tuple[str, str, str]]) -> None:
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_file(src: str, dest: str) -> None:
SUBPCHECK(CURLARGS + ("-o", dest, src))
def get_primary(repomdroot: et.Element) -> et.Element:
"""Given a repomd.xml root element, return the element for the
primary data file.
"""
return repomdroot.find("repo:data[@type='primary']", XMLNS)
def download_primary(primary: et.Element, repourl: str, mrepodir: str) -> str:
"""Given the ET element with information about it, and the URL of
the repo to download from and the local directory to download to,
download and uncompress the primary data file, returning
the filename. Note mrepodir/repodata is assumed to exist.
"""
primloc = primary.find("repo:location", XMLNS).attrib["href"]
encprimfn = f"{mrepodir}/{primloc}"
get_file(f"{repourl}/{primloc}", encprimfn)
SUBPCHECK(("unzstd", "-q", encprimfn))
os.remove(encprimfn)
return encprimfn.replace(".zst", "")
def replace_primary(primfn: str, removes: Sequence[str]) -> tuple[str, int, str, int]:
"""Parse the primary data file, remove any packages whose source
package name matches one in removes, and write out a new file
with the correct name (containing its own sha256sum). Return the
checksums and sizes of the new uncompressed and compressed files,
for writing back into the repomd.
"""
rddir = os.path.dirname(primfn)
et.register_namespace("", "http://linux.duke.edu/metadata/common")
et.register_namespace("rpm", "http://linux.duke.edu/metadata/rpm")
primtree = et.parse(primfn)
primroot = primtree.getroot()
for pkg in primroot.findall("common:package", XMLNS):
srpm = pkg.find("common:format", XMLNS).find("rpm:sourcerpm", XMLNS).text
if srpm.rsplit("-", 2)[0] in removes:
primroot.remove(pkg)
tempfn = f"{rddir}/primtemp.xml"
primtree.write(tempfn)
# use hashlib? meh
opensum = SUBPCAPTCHECK(("sha256sum", tempfn)).stdout.split()[0]
opensize = os.path.getsize(tempfn)
SUBPCHECK(("zstd", "-q", tempfn))
csum = SUBPCAPTCHECK(("sha256sum", f"{tempfn}.zst")).stdout.split()[0]
size = os.path.getsize(f"{tempfn}.zst")
os.rename(f"{tempfn}.zst", f"{rddir}/{csum}-primary.xml.zst")
os.remove(tempfn)
os.remove(primfn)
return (csum, size, opensum, opensize)
def get_base_repoclosure(baserepos: Sequence[str], nmbaserepos: Sequence[str]) -> str:
cmdargs = DNFARGS + ["repoclosure"]
for repo in list(baserepos) + list(nmbaserepos):
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-locals
def get_modified_repoclosure(
mrepos: Sequence[str], nmrepos: Sequence[str], nrepos: Sequence[str], removes: Sequence[str]
) -> str:
args = DNFARGS + ["repoclosure"]
# place to stash the modified repos
with tempfile.TemporaryDirectory() as mreposdir:
for mrepo in mrepos:
mrepodir = f"{mreposdir}/{hash_repo(mrepo)}"
os.makedirs(f"{mrepodir}/repodata")
repomdfn = f"{mrepodir}/repodata/repomd.xml"
get_file(f"{mrepo}/repodata/repomd.xml", repomdfn)
et.register_namespace("", "http://linux.duke.edu/metadata/repo")
repomdtree = et.parse(repomdfn)
repomdroot = repomdtree.getroot()
primary = get_primary(repomdroot)
primfn = download_primary(primary, mrepo, mrepodir)
(csum, size, opensum, opensize) = replace_primary(primfn, removes)
# modify the repomd
primary.find("repo:checksum", XMLNS).text = csum
primary.find("repo:size", XMLNS).text = str(size)
primary.find("repo:open-checksum", XMLNS).text = opensum
primary.find("repo:open-size", XMLNS).text = str(opensize)
primary.find("repo:location", XMLNS).attrib["href"] = f"repodata/{csum}-primary.xml.zst"
# requires Python 3.10:
# notprimary = repomdroot.findall("repo:data[@type]", XMLNS)
alldata = repomdroot.findall("repo:data[@type]", XMLNS)
notprimary = [data for data in alldata if data is not primary]
for item in notprimary:
repomdroot.remove(item)
et.register_namespace("", "http://linux.duke.edu/metadata/repo")
repomdtree.write(repomdfn)
# add the modified repo to the repoclosure command
args.extend(["--repofrompath", f"{hash_repo(mrepo)},{mrepodir}"])
# now add the non-modified base repos
for nmrepo in nmrepos:
args.extend(["--repofrompath", f"{hash_repo(nmrepo)},{nmrepo}"])
# now add the new package repos
for nrepo in nrepos:
args.extend(["--repofrompath", f"{hash_repo(nrepo)},{nrepo}"])
# finally, add the check arg
args.append("--check")
args.append(",".join([hash_repo(mrepo) for mrepo in mrepos]))
ret = SUBPCAPTURE(args).stdout
return ret
def get_new_repoclosure(baserepos: Sequence[str], nrepo: str) -> str:
cmdargs = DNFARGS + ["repoclosure"]
for repo in baserepos:
cmdargs.extend(["--repofrompath", f"{hash_repo(repo)},{repo}"])
cmdargs.extend(["--repofrompath", f"{hash_repo(nrepo)},{nrepo}", "--check", hash_repo(nrepo)])
return SUBPCAPTURE(cmdargs).stdout
def get_source_packages(repos: Sequence[str]) -> set[str]:
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 url_check(arg: str) -> str:
parsed = urlparse(arg)
if parsed.scheme in ("http", "https", "file"):
return arg
raise ValueError(f"Unsupported URL scheme {parsed.scheme} in {arg}")
def comma_url(arg: str) -> list[str]:
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 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(
"--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(
"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",
)
parser.add_argument(
"repo",
type=url_check,
help="The URL of the repo containing the main set of new packages to be tested",
)
return parser.parse_args()
def check_utils() -> None:
"""Check required utilities are installed."""
missing = []
for prog in (("zstd", "-V"), ("dnf", "--version"), ("curl", "-V"), ("sha256sum", "--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))
def main() -> None:
"""Main loop."""
try:
check_utils()
exitcode = 0
args = parse_args()
nrepos = [args.repo] + args.addrepos
baserc = parse_repoclosure(get_base_repoclosure(args.baserepos, args.nmbaserepos))
# find source package(s) of our tested repo(s)
# whether to include addrepos is arguable, but should usually be moot
sources = get_source_packages(nrepos)
# get the modified rpmclosure output
modrc = parse_repoclosure(
get_modified_repoclosure(args.baserepos, args.nmbaserepos, nrepos, sources)
)
# figure out the diffs
newerrors = [dep for dep in modrc if dep not in baserc]
fixederrors = [dep for dep in baserc if dep not in modrc]
if fixederrors:
print("Fixed dependencies:")
format_rc_errors(fixederrors)
print("")
if newerrors:
print("New broken dependencies:")
format_rc_errors(newerrors)
exitcode += 1
# get repoclosure on new repo - this is an installability test
newrc = parse_repoclosure(get_new_repoclosure(args.baserepos + args.nmbaserepos, args.repo))
if newrc:
print("Installability problems:")
format_rc_errors(newrc)
exitcode += 2
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:

220
test_rmdepcheck.py Normal file
View file

@ -0,0 +1,220 @@
# 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>
# Unit tests don't always need docstrings.
# pylint: disable=missing-function-docstring
"""Tests for rmdepcheck."""
import os
import shutil
import sys
import tempfile
import xml.etree.ElementTree as et
from unittest import mock
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"),
]
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
def test_get_file():
with tempfile.TemporaryDirectory() as tempdir:
testfile = f"file://{HERE}/pyproject.toml"
rmdepcheck.get_file(testfile, f"{tempdir}/pyproject.toml")
with open(f"{tempdir}/pyproject.toml", "r", encoding="utf-8") as testfh:
assert "rmdepcheck" in testfh.read()
def test_get_download_primary():
repomdtree = et.parse(f"{REPOS}/base/repodata/repomd.xml")
repomdroot = repomdtree.getroot()
primary = rmdepcheck.get_primary(repomdroot)
assert isinstance(primary, et.Element)
assert primary.attrib == {"type": "primary"}
with tempfile.TemporaryDirectory() as tempdir:
os.makedirs(f"{tempdir}/repodata")
rmdepcheck.download_primary(primary, f"file://{REPOS}/base", tempdir)
# NOTE: this filename changes any time mkrepos.py is run
assert os.path.exists(
# pylint: disable-next=line-too-long
f"{tempdir}/repodata/0e38861f6ebd9d0808917661ecdb6db855222b294611209cf801664f900c9779-primary.xml"
)
def test_replace_primary():
with tempfile.TemporaryDirectory() as tempdir:
shutil.copy2(
# this is an old version of base's primary file
f"{TESTDATA}/test_replace_primary.xml",
f"{tempdir}/test.xml",
)
ret = rmdepcheck.replace_primary(f"{tempdir}/test.xml", "ccc")
print(tempdir)
assert ret == (
"42daebf05f6c3cabe9d029acb3a8056f7fc533cde9820b7388d90acb1f6e7dbe",
1067,
"562981a96ba946cd0f993d180108c4dd107d1a5f24210c5768b19cdd93e8f33a",
7531,
)
assert os.path.exists(
# pylint: disable-next=line-too-long
f"{tempdir}/42daebf05f6c3cabe9d029acb3a8056f7fc533cde9820b7388d90acb1f6e7dbe-primary.xml.zst"
)
def test_get_base_repoclosure():
with open(f"{TESTDATA}/test_get_base_repoclosure.txt", "r", encoding="utf-8") as testfh:
expected = testfh.read()
ret = rmdepcheck.get_base_repoclosure([f"file://{REPOS}/base"], [])
assert ret == expected
def test_get_modified_repoclosure():
with open(f"{TESTDATA}/test_get_modified_repoclosure.txt", "r", encoding="utf-8") as testfh:
expected = testfh.read()
ret = rmdepcheck.get_modified_repoclosure(
[f"file://{REPOS}/base"], [], [f"file://{REPOS}/new"], ["aaa", "ccc", "eee", "fff", "ggg"]
)
assert ret == expected
def test_get_new_repoclosure():
with open(f"{TESTDATA}/test_get_new_repoclosure.txt", "r", encoding="utf-8") as testfh:
expected = testfh.read()
ret = rmdepcheck.get_new_repoclosure([f"file://{REPOS}/base"], f"file://{REPOS}/new")
assert ret == expected
def test_get_source_packages():
sources = rmdepcheck.get_source_packages([f"file://{REPOS}/new"])
assert sources == {"111", "aaa", "ccc", "eee", "fff", "ggg"}
def test_url_check():
rmdepcheck.url_check("file:///foo/bar")
rmdepcheck.url_check("https://www.some.where")
rmdepcheck.url_check("http://some.where.insecure")
with pytest.raises(ValueError):
rmdepcheck.url_check("ftp://1997.called")
with pytest.raises(ValueError):
rmdepcheck.url_check("whatisthis")
def test_comma_url():
rmdepcheck.comma_url("https://www.some.where")
rmdepcheck.comma_url("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")
@mock.patch("subprocess.run", autospec=True)
def test_check_utils(mock_run):
rmdepcheck.check_utils()
mock_run.side_effect = [None, FileNotFoundError, None, FileNotFoundError]
with pytest.raises(SystemExit) as excinfo:
rmdepcheck.check_utils()
assert excinfo.value.code == "Please install missing required utilities: dnf sha256sum"
@mock.patch("rmdepcheck.check_utils", 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 == ""
def test_e2e_devel(capsys):
"""End-to-end test similar to a Rawhide or Branched case, with
a single modified base repo and a single check repo.
"""
sys.argv = ["rmdepcheck.py", f"file://{REPOS}/base", 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_devel.txt", "r", encoding="utf-8") as fh:
exptext = fh.read()
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()
assert captured.out == exptext

BIN
testdata/repos/base/aaa-1.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/base/bbb-1.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/base/ccc-1.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/base/ddd-1.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/base/eee-1.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/base/fff-1.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/base/ggg-1.0-1.i686.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/base/ggg-1.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/base/hhh-1.0-1.i686.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/base/hhh-1.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

28
testdata/repos/base/repodata/repomd.xml vendored Normal file
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>1750233629</revision>
<data type="primary">
<checksum type="sha256">0e38861f6ebd9d0808917661ecdb6db855222b294611209cf801664f900c9779</checksum>
<open-checksum type="sha256">a6a5fbcca35c3e7ff2172e589ac775726ae086c5273b5d256592bd3aa2a76378</open-checksum>
<location href="repodata/0e38861f6ebd9d0808917661ecdb6db855222b294611209cf801664f900c9779-primary.xml.zst"/>
<timestamp>1750233629</timestamp>
<size>1283</size>
<open-size>10594</open-size>
</data>
<data type="filelists">
<checksum type="sha256">3a921e54a99aeb0fa147735c75c8bb6a5da6c9950a6af8e7b7ff295aadd2e5b5</checksum>
<open-checksum type="sha256">28ead1ee812ab2c7ede944b76b03f21a4b0595414cb3f87fca3f4038cca373f2</open-checksum>
<location href="repodata/3a921e54a99aeb0fa147735c75c8bb6a5da6c9950a6af8e7b7ff295aadd2e5b5-filelists.xml.zst"/>
<timestamp>1750233629</timestamp>
<size>622</size>
<open-size>1722</open-size>
</data>
<data type="other">
<checksum type="sha256">356035bc36bc5c932e218a655fa7e0ff7fde5caf0ccf38bd74fe17e02c58cc7a</checksum>
<open-checksum type="sha256">b3df717e78f7972b789c8579c7e91d0a85656d6e40c93d034fe23d6fc82a3d11</open-checksum>
<location href="repodata/356035bc36bc5c932e218a655fa7e0ff7fde5caf0ccf38bd74fe17e02c58cc7a-other.xml.zst"/>
<timestamp>1750233629</timestamp>
<size>708</size>
<open-size>2838</open-size>
</data>
</repomd>

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

Binary file not shown.

BIN
testdata/repos/devel/ggg-1.0-1.i686.rpm vendored Normal file

Binary file not shown.

Binary file not shown.

BIN
testdata/repos/devel/hhh-1.0-1.i686.rpm vendored Normal file

Binary file not shown.

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>1750149328</revision>
<data type="primary">
<checksum type="sha256">67ab12ab35b8b305f8527ba6116facfd39bb8101176950fba6082a1b74778a0f</checksum>
<open-checksum type="sha256">8690a2b2c7d950f4ce564be461441d9117a1da9140b0135b070b864f97895c3a</open-checksum>
<location href="repodata/67ab12ab35b8b305f8527ba6116facfd39bb8101176950fba6082a1b74778a0f-primary.xml.zst"/>
<timestamp>1750149328</timestamp>
<size>1271</size>
<open-size>10594</open-size>
</data>
<data type="filelists">
<checksum type="sha256">96baf42ea837d6c2a89cf7a2c52c3e6a665902a92090a94bb6bc86a219617ea0</checksum>
<open-checksum type="sha256">02b311e71aa480a771e6a158d55fc13a142e5c2f0191c814fb29009fe1030a45</open-checksum>
<location href="repodata/96baf42ea837d6c2a89cf7a2c52c3e6a665902a92090a94bb6bc86a219617ea0-filelists.xml.zst"/>
<timestamp>1750149328</timestamp>
<size>621</size>
<open-size>1722</open-size>
</data>
<data type="other">
<checksum type="sha256">26914f49a900cd21e77aa071ebe224918d47e1cfabf604b80bbd16b351fcea10</checksum>
<open-checksum type="sha256">3e7f6535f0fe733e91847525e1cae8b39e3f5cff733d51d112e21eec6c3db5e1</open-checksum>
<location href="repodata/26914f49a900cd21e77aa071ebe224918d47e1cfabf604b80bbd16b351fcea10-other.xml.zst"/>
<timestamp>1750149328</timestamp>
<size>709</size>
<open-size>2838</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>1750233631</revision>
<data type="primary">
<checksum type="sha256">69a3730a283b85a4b3cff7d04bfde3b2b234f0607ebc17319d7a8d143a8e066e</checksum>
<open-checksum type="sha256">e1e2ffd2fb1ee76f87b70750d00ca5677a252b397ab6c2389137a0c33e7b359f</open-checksum>
<location href="repodata/69a3730a283b85a4b3cff7d04bfde3b2b234f0607ebc17319d7a8d143a8e066e-primary.xml.zst"/>
<timestamp>1750233631</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>1750233631</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>1750233631</timestamp>
<size>117</size>
<open-size>121</open-size>
</data>
</repomd>

113
testdata/repos/mkrepos.py vendored Executable file
View file

@ -0,0 +1,113 @@
#!/usr/bin/env python3
# Copyright Red Hat
#
# This script 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>
"""Script to create test packages and repos for rpdepcheck testing."""
import os
import shutil
import rpmfluff
import rpmfluff.yumrepobuild
# base packages, used in both the 'base' and 'stable' repos
pkgaaa = rpmfluff.SimpleRpmBuild("aaa", "1.0", "1", ["x86_64"])
pkgbbb = rpmfluff.SimpleRpmBuild("bbb", "1.0", "1", ["x86_64"])
pkgccc = rpmfluff.SimpleRpmBuild("ccc", "1.0", "1", ["x86_64"])
pkgddd = rpmfluff.SimpleRpmBuild("ddd", "1.0", "1", ["x86_64"])
pkgeee = rpmfluff.SimpleRpmBuild("eee", "1.0", "1", ["x86_64"])
pkgfff = rpmfluff.SimpleRpmBuild("fff", "1.0", "1", ["x86_64"])
pkgggg = rpmfluff.SimpleRpmBuild("ggg", "1.0", "1", ["x86_64", "i686"])
pkghhx = rpmfluff.SimpleRpmBuild("hhh", "1.0", "1", ["x86_64"])
pkghhi = rpmfluff.SimpleRpmBuild("hhh", "1.0", "1", ["i686"])
allbase = (pkgaaa, pkgbbb, pkgccc, pkgddd, pkgeee, pkgfff, pkgggg, pkghhx, pkghhi)
# currently works
pkgbbb.add_requires("aaa < 3.0")
# currently broken
pkgddd.add_requires("ccc = 3.0")
# currently works, both sides replaced in new repo
pkgfff.add_requires("eee = 1.0")
# multilib
pkghhx.add_requires("ggg(x86-64)")
pkghhi.add_requires("ggg(x86-32)")
# existing stable update packages
updaaa = rpmfluff.SimpleRpmBuild("aaa", "2.0", "1", ["x86_64"])
updbbb = rpmfluff.SimpleRpmBuild("bbb", "2.0", "1", ["x86_64"])
updccc = rpmfluff.SimpleRpmBuild("ccc", "2.0", "1", ["x86_64"])
updddd = rpmfluff.SimpleRpmBuild("ddd", "2.0", "1", ["x86_64"])
allupd = (updaaa, updbbb, updccc, updddd)
# should be broken by the new packages
updbbb.add_requires("aaa == 2.0")
# should be OK
updddd.add_requires("ccc >= 2.0")
# replaces aaa-1.0-1, breaks bbb
newaaa = rpmfluff.SimpleRpmBuild("aaa", "3.0", "1", ["x86_64"])
# replaces ccc-1.0-1, fixes ddd
newccc = rpmfluff.SimpleRpmBuild("ccc", "3.0", "1", ["x86_64"])
# replaces eee, would break fff but we also replace fff below
neweee = rpmfluff.SimpleRpmBuild("eee", "3.0", "1", ["x86_64"])
newfff = rpmfluff.SimpleRpmBuild("fff", "3.0", "1", ["x86_64"])
# replaces ggg-1.0-1, breaks hhh.i686 as there's no i686 here
newggg = rpmfluff.SimpleRpmBuild("ggg", "3.0", "1", ["x86_64"])
# intentionally broken to check installability
new111 = rpmfluff.SimpleRpmBuild("111", "3.0", "1", ["x86_64"])
allnew = (newaaa, newccc, neweee, newfff, newggg, new111)
newfff.add_requires("eee = 3.0")
new111.add_requires("nonexistent")
for pkg in allbase + allupd + allnew:
pkg.addVendor("Fedora Project")
pkg.addPackager("Fedora Project")
base = rpmfluff.yumrepobuild.YumRepoBuild(allbase)
basedir = "base"
new = rpmfluff.yumrepobuild.YumRepoBuild(allnew)
newdir = "new"
updates = rpmfluff.yumrepobuild.YumRepoBuild(allupd)
upddir = "updates"
empty = rpmfluff.yumrepobuild.YumRepoBuild([])
empdir = "empty"
def cleanup(repos=True):
dirs = [pkgaaa.get_base_dir()]
if repos:
dirs.extend((basedir, upddir, newdir, empdir))
for _dir in dirs:
if os.path.isdir(_dir):
shutil.rmtree(_dir)
cleanup()
os.mkdir(basedir)
os.mkdir(newdir)
os.mkdir(upddir)
os.mkdir(empdir)
base.repoDir = basedir
new.repoDir = newdir
updates.repoDir = upddir
empty.repoDir = empdir
base.make("x86_64", "i686")
new.make("x86_64")
updates.make("x86_64")
empty.make()
cleanup(repos=False)

BIN
testdata/repos/new/111-3.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/new/aaa-3.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/new/ccc-3.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/new/eee-3.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/new/fff-3.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

BIN
testdata/repos/new/ggg-3.0-1.x86_64.rpm vendored Normal file

Binary file not shown.

28
testdata/repos/new/repodata/repomd.xml vendored Normal file
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>1750233630</revision>
<data type="primary">
<checksum type="sha256">5ecd0d192609426e7cc61b37bbb51667ddcc125041fa76980002047b732d8652</checksum>
<open-checksum type="sha256">25b8466bc077d2eae67c8fef9d2d00917958bb3e3e358a3fa7c24e0c3b4aa30b</open-checksum>
<location href="repodata/5ecd0d192609426e7cc61b37bbb51667ddcc125041fa76980002047b732d8652-primary.xml.zst"/>
<timestamp>1750233630</timestamp>
<size>996</size>
<open-size>6332</open-size>
</data>
<data type="filelists">
<checksum type="sha256">840d94ef3a14d1f0b5a51f376cac9a5d26a780bb2aeb260d4906269adb9e5262</checksum>
<open-checksum type="sha256">99882a419bc00a1d777393148c848c9e04086ebb881ccd645fc5ce4e45e8871a</open-checksum>
<location href="repodata/840d94ef3a14d1f0b5a51f376cac9a5d26a780bb2aeb260d4906269adb9e5262-filelists.xml.zst"/>
<timestamp>1750233630</timestamp>
<size>458</size>
<open-size>1085</open-size>
</data>
<data type="other">
<checksum type="sha256">68cd0412bbf5516d48e81f2030960c3c6783fb587848cdd77f3f71a7ed8f89f5</checksum>
<open-checksum type="sha256">1c4247850bc335618dc84ab32ce1e3489e1ff512596480a92344d07406f67a09</open-checksum>
<location href="repodata/68cd0412bbf5516d48e81f2030960c3c6783fb587848cdd77f3f71a7ed8f89f5-other.xml.zst"/>
<timestamp>1750233630</timestamp>
<size>537</size>
<open-size>1753</open-size>
</data>
</repomd>

Binary file not shown.

Binary file not shown.

Binary file not shown.

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>1750233631</revision>
<data type="primary">
<checksum type="sha256">13fd527aa9f63b0e1c421d94a01a212e0a6bf8a8843a40bfd0bb53895272025f</checksum>
<open-checksum type="sha256">837008ebe4332da3469b9323f97f48de808ea3ca7c3d651e9547c9ed0e17eeef</open-checksum>
<location href="repodata/13fd527aa9f63b0e1c421d94a01a212e0a6bf8a8843a40bfd0bb53895272025f-primary.xml.zst"/>
<timestamp>1750233631</timestamp>
<size>859</size>
<open-size>4359</open-size>
</data>
<data type="filelists">
<checksum type="sha256">9986ef24ccf5a9814ae624cfeb2acb37fa489f75a1bd16ed3000df222b0998e9</checksum>
<open-checksum type="sha256">4a8729de01249c4062fc841bf30a23c577ef13a29a2cd0c7e34015b52e48cf8c</open-checksum>
<location href="repodata/9986ef24ccf5a9814ae624cfeb2acb37fa489f75a1bd16ed3000df222b0998e9-filelists.xml.zst"/>
<timestamp>1750233631</timestamp>
<size>374</size>
<open-size>765</open-size>
</data>
<data type="other">
<checksum type="sha256">0c7503e45ca77efa17c271e1c4b2177c574f1e9e53ce19a5498e1f3bb566a084</checksum>
<open-checksum type="sha256">4c5fe999a1337dc80cf3433aa70cf6f64f3f0f746873f07b22f7eb84e24d8ea6</open-checksum>
<location href="repodata/0c7503e45ca77efa17c271e1c4b2177c574f1e9e53ce19a5498e1f3bb566a084-other.xml.zst"/>
<timestamp>1750233631</timestamp>
<size>450</size>
<open-size>1209</open-size>
</data>
</repomd>

12
testdata/test_e2e_devel.txt vendored Normal file
View file

@ -0,0 +1,12 @@
Fixed dependencies:
package: ddd-1.0-1.x86_64 from file:///var/home/adamw/local/rmdepcheck/testdata/repos/base
ccc = 3.0
New broken dependencies:
package: bbb-1.0-1.x86_64 from file:///var/home/adamw/local/rmdepcheck/testdata/repos/base
aaa < 3.0
package: hhh-1.0-1.i686 from file:///var/home/adamw/local/rmdepcheck/testdata/repos/base
ggg(x86-32)
Installability problems:
package: 111-3.0-1.x86_64 from file:///var/home/adamw/local/rmdepcheck/testdata/repos/new
nonexistent

6
testdata/test_e2e_updates.txt vendored Normal file
View file

@ -0,0 +1,6 @@
New broken dependencies:
package: bbb-2.0-1.x86_64 from file:///var/home/adamw/local/rmdepcheck/testdata/repos/updates
aaa = 2.0
Installability problems:
package: 111-3.0-1.x86_64 from file:///var/home/adamw/local/rmdepcheck/testdata/repos/new
nonexistent

6
testdata/test_format_rc_errors.txt vendored Normal file
View file

@ -0,0 +1,6 @@
package: python3-wxnatpy-0.4.0-13.fc42.noarch from baserepo0
python(abi) = 3.13
python3.13dist(wxpython) >= 4
python3.13dist(xnat) >= 0.3.3
package: python3-x3dh-1.0.4-3.fc43.noarch from baserepo1
python3.14dist(pydantic) >= 1.7.4

View file

@ -0,0 +1,3 @@
package: ddd-1.0-1.x86_64 from a59f6db1
unresolved deps (1):
ccc = 3.0

View file

@ -0,0 +1,6 @@
package: bbb-1.0-1.x86_64 from a59f6db1
unresolved deps (1):
aaa < 3.0
package: hhh-1.0-1.i686 from a59f6db1
unresolved deps (1):
ggg(x86-32)

3
testdata/test_get_new_repoclosure.txt vendored Normal file
View file

@ -0,0 +1,3 @@
package: 111-3.0-1.x86_64 from deb30c7b
unresolved deps (1):
nonexistent

11
testdata/test_parse_repoclosure.txt vendored Normal file
View file

@ -0,0 +1,11 @@
haha this is a sneaky evil line for testing muahaha
package: python3-wxnatpy-0.4.0-13.fc42.noarch from baserepo0
unresolved deps (3):
python(abi) = 3.13
python3.13dist(wxpython) >= 4
python3.13dist(xnat) >= 0.3.3
(foo if bar)
package: python3-x3dh-1.0.4-3.fc43.noarch from baserepo1
unresolved deps (1):
python3.14dist(pydantic) >= 1.7.4
Error: Repoclosure ended with unresolved dependencies (3148) across 832 packages.

215
testdata/test_replace_primary.xml vendored Normal file
View file

@ -0,0 +1,215 @@
<?xml version="1.0" encoding="UTF-8"?>
<metadata xmlns="http://linux.duke.edu/metadata/common" xmlns:rpm="http://linux.duke.edu/metadata/rpm" packages="8">
<package type="rpm">
<name>aaa</name>
<arch>x86_64</arch>
<version epoch="0" ver="1.0" rel="1"/>
<checksum type="sha256" pkgid="YES">7188c68d3407d04571b70e3a8d3c8e6329c34d2dd7af8a6c7364a0b29c991cec</checksum>
<summary>Dummy summary</summary>
<description>This is a dummy description.</description>
<packager>Fedora Project</packager>
<url></url>
<time file="1749998096" build="1749998095"/>
<size package="6329" installed="0" archive="124"/>
<location href="aaa-1.0-1.x86_64.rpm"/>
<format>
<rpm:license>GPL</rpm:license>
<rpm:vendor>Fedora Project</rpm:vendor>
<rpm:group>Applications/Productivity</rpm:group>
<rpm:buildhost>toolbx</rpm:buildhost>
<rpm:sourcerpm>aaa-1.0-1.src.rpm</rpm:sourcerpm>
<rpm:header-range start="4504" end="6285"/>
<rpm:provides>
<rpm:entry name="aaa" flags="EQ" epoch="0" ver="1.0" rel="1"/>
<rpm:entry name="aaa(x86-64)" flags="EQ" epoch="0" ver="1.0" rel="1"/>
</rpm:provides>
</format>
</package>
<package type="rpm">
<name>bbb</name>
<arch>x86_64</arch>
<version epoch="0" ver="1.0" rel="1"/>
<checksum type="sha256" pkgid="YES">3e5425656c6bbfda7b1d707ea39340db1220e9c0618458c4e583a49d060f789b</checksum>
<summary>Dummy summary</summary>
<description>This is a dummy description.</description>
<packager>Fedora Project</packager>
<url></url>
<time file="1749998096" build="1749998095"/>
<size package="6341" installed="0" archive="124"/>
<location href="bbb-1.0-1.x86_64.rpm"/>
<format>
<rpm:license>GPL</rpm:license>
<rpm:vendor>Fedora Project</rpm:vendor>
<rpm:group>Applications/Productivity</rpm:group>
<rpm:buildhost>toolbx</rpm:buildhost>
<rpm:sourcerpm>bbb-1.0-1.src.rpm</rpm:sourcerpm>
<rpm:header-range start="4504" end="6297"/>
<rpm:provides>
<rpm:entry name="bbb" flags="EQ" epoch="0" ver="1.0" rel="1"/>
<rpm:entry name="bbb(x86-64)" flags="EQ" epoch="0" ver="1.0" rel="1"/>
</rpm:provides>
<rpm:requires>
<rpm:entry name="aaa" flags="EQ" epoch="0" ver="1.0"/>
</rpm:requires>
</format>
</package>
<package type="rpm">
<name>ccc</name>
<arch>x86_64</arch>
<version epoch="0" ver="1.0" rel="1"/>
<checksum type="sha256" pkgid="YES">3c810c039112c6d626a9dd628b38af6cbcb387c710b702d51028f6c215c34ad6</checksum>
<summary>Dummy summary</summary>
<description>This is a dummy description.</description>
<packager>Fedora Project</packager>
<url></url>
<time file="1749998096" build="1749998095"/>
<size package="6329" installed="0" archive="124"/>
<location href="ccc-1.0-1.x86_64.rpm"/>
<format>
<rpm:license>GPL</rpm:license>
<rpm:vendor>Fedora Project</rpm:vendor>
<rpm:group>Applications/Productivity</rpm:group>
<rpm:buildhost>toolbx</rpm:buildhost>
<rpm:sourcerpm>ccc-1.0-1.src.rpm</rpm:sourcerpm>
<rpm:header-range start="4504" end="6285"/>
<rpm:provides>
<rpm:entry name="ccc" flags="EQ" epoch="0" ver="1.0" rel="1"/>
<rpm:entry name="ccc(x86-64)" flags="EQ" epoch="0" ver="1.0" rel="1"/>
</rpm:provides>
</format>
</package>
<package type="rpm">
<name>ddd</name>
<arch>x86_64</arch>
<version epoch="0" ver="1.0" rel="1"/>
<checksum type="sha256" pkgid="YES">7694e747a24fa368cfea4f54b074f9504b3d0cc3fed6b040fd8932d7bb9ac42e</checksum>
<summary>Dummy summary</summary>
<description>This is a dummy description.</description>
<packager>Fedora Project</packager>
<url></url>
<time file="1749998096" build="1749998096"/>
<size package="6341" installed="0" archive="124"/>
<location href="ddd-1.0-1.x86_64.rpm"/>
<format>
<rpm:license>GPL</rpm:license>
<rpm:vendor>Fedora Project</rpm:vendor>
<rpm:group>Applications/Productivity</rpm:group>
<rpm:buildhost>toolbx</rpm:buildhost>
<rpm:sourcerpm>ddd-1.0-1.src.rpm</rpm:sourcerpm>
<rpm:header-range start="4504" end="6297"/>
<rpm:provides>
<rpm:entry name="ddd" flags="EQ" epoch="0" ver="1.0" rel="1"/>
<rpm:entry name="ddd(x86-64)" flags="EQ" epoch="0" ver="1.0" rel="1"/>
</rpm:provides>
<rpm:requires>
<rpm:entry name="ccc" flags="EQ" epoch="0" ver="2.0"/>
</rpm:requires>
</format>
</package>
<package type="rpm">
<name>eee</name>
<arch>i686</arch>
<version epoch="0" ver="1.0" rel="1"/>
<checksum type="sha256" pkgid="YES">2f12f8ac088158a42338af348f15cdd27dc9564822602af3ff9ea6c3bf9b5acf</checksum>
<summary>Dummy summary</summary>
<description>This is a dummy description.</description>
<packager>Fedora Project</packager>
<url></url>
<time file="1749998096" build="1749998096"/>
<size package="6273" installed="0" archive="124"/>
<location href="eee-1.0-1.i686.rpm"/>
<format>
<rpm:license>GPL</rpm:license>
<rpm:vendor>Fedora Project</rpm:vendor>
<rpm:group>Applications/Productivity</rpm:group>
<rpm:buildhost>toolbx</rpm:buildhost>
<rpm:sourcerpm>eee-1.0-1.src.rpm</rpm:sourcerpm>
<rpm:header-range start="4504" end="6229"/>
<rpm:provides>
<rpm:entry name="eee" flags="EQ" epoch="0" ver="1.0" rel="1"/>
<rpm:entry name="eee(x86-32)" flags="EQ" epoch="0" ver="1.0" rel="1"/>
</rpm:provides>
</format>
</package>
<package type="rpm">
<name>eee</name>
<arch>x86_64</arch>
<version epoch="0" ver="1.0" rel="1"/>
<checksum type="sha256" pkgid="YES">ad3f753e2debe9e0837515fdcf1cf80ced733bbf5fb430f4dc4b0496bc3d69c8</checksum>
<summary>Dummy summary</summary>
<description>This is a dummy description.</description>
<packager>Fedora Project</packager>
<url></url>
<time file="1749998096" build="1749998096"/>
<size package="6329" installed="0" archive="124"/>
<location href="eee-1.0-1.x86_64.rpm"/>
<format>
<rpm:license>GPL</rpm:license>
<rpm:vendor>Fedora Project</rpm:vendor>
<rpm:group>Applications/Productivity</rpm:group>
<rpm:buildhost>toolbx</rpm:buildhost>
<rpm:sourcerpm>eee-1.0-1.src.rpm</rpm:sourcerpm>
<rpm:header-range start="4504" end="6285"/>
<rpm:provides>
<rpm:entry name="eee" flags="EQ" epoch="0" ver="1.0" rel="1"/>
<rpm:entry name="eee(x86-64)" flags="EQ" epoch="0" ver="1.0" rel="1"/>
</rpm:provides>
</format>
</package>
<package type="rpm">
<name>fff</name>
<arch>i686</arch>
<version epoch="0" ver="1.0" rel="1"/>
<checksum type="sha256" pkgid="YES">fbd2c3c44712e41447f9119811df6313be5b57286bc0679f069667b9865f8dc8</checksum>
<summary>Dummy summary</summary>
<description>This is a dummy description.</description>
<packager>Fedora Project</packager>
<url></url>
<time file="1749998096" build="1749998096"/>
<size package="6285" installed="0" archive="124"/>
<location href="fff-1.0-1.i686.rpm"/>
<format>
<rpm:license>GPL</rpm:license>
<rpm:vendor>Fedora Project</rpm:vendor>
<rpm:group>Applications/Productivity</rpm:group>
<rpm:buildhost>toolbx</rpm:buildhost>
<rpm:sourcerpm>fff-1.0-1.src.rpm</rpm:sourcerpm>
<rpm:header-range start="4504" end="6241"/>
<rpm:provides>
<rpm:entry name="fff" flags="EQ" epoch="0" ver="1.0" rel="1"/>
<rpm:entry name="fff(x86-32)" flags="EQ" epoch="0" ver="1.0" rel="1"/>
</rpm:provides>
<rpm:requires>
<rpm:entry name="eee" flags="EQ" epoch="0" ver="1.0"/>
</rpm:requires>
</format>
</package>
<package type="rpm">
<name>fff</name>
<arch>x86_64</arch>
<version epoch="0" ver="1.0" rel="1"/>
<checksum type="sha256" pkgid="YES">47e7d437c69d68b5f4b8790b20854ad01847d48ab01019011a72d99341498f86</checksum>
<summary>Dummy summary</summary>
<description>This is a dummy description.</description>
<packager>Fedora Project</packager>
<url></url>
<time file="1749998096" build="1749998096"/>
<size package="6341" installed="0" archive="124"/>
<location href="fff-1.0-1.x86_64.rpm"/>
<format>
<rpm:license>GPL</rpm:license>
<rpm:vendor>Fedora Project</rpm:vendor>
<rpm:group>Applications/Productivity</rpm:group>
<rpm:buildhost>toolbx</rpm:buildhost>
<rpm:sourcerpm>fff-1.0-1.src.rpm</rpm:sourcerpm>
<rpm:header-range start="4504" end="6297"/>
<rpm:provides>
<rpm:entry name="fff" flags="EQ" epoch="0" ver="1.0" rel="1"/>
<rpm:entry name="fff(x86-64)" flags="EQ" epoch="0" ver="1.0" rel="1"/>
</rpm:provides>
<rpm:requires>
<rpm:entry name="eee" flags="EQ" epoch="0" ver="1.0"/>
</rpm:requires>
</format>
</package>
</metadata>

2
tests.requires Normal file
View file

@ -0,0 +1,2 @@
coverage[toml]
pytest

25
tox.ini Normal file
View file

@ -0,0 +1,25 @@
[tox]
envlist = py39,py310,py311,py312,py313,py314,ci
skip_missing_interpreters = true
[testenv]
deps =
-r{toxinidir}/install.requires
-r{toxinidir}/tests.requires
commands =
coverage run -m pytest {posargs}
setenv =
PYTHONPATH = {toxinidir}
[testenv:ci]
sitepackages = false
deps =
-r{toxinidir}/tox.requires
skip_install = true
ignore_errors = true
commands =
black rmdepcheck.py test_rmdepcheck.py testdata/repos/mkrepos.py --check
coverage combine
coverage report
coverage xml
diff-cover coverage.xml --fail-under=100
diff-quality --violations=pylint --fail-under=100

5
tox.requires Normal file
View file

@ -0,0 +1,5 @@
black
coverage
diff-cover
pylint
pytest-cov