From f209553299ee7db74f3756f0f89518f1367c6b9b Mon Sep 17 00:00:00 2001 From: Adam Williamson Date: Sat, 11 Apr 2026 17:25:46 -0700 Subject: [PATCH] Also get/modify filelists and modules, improve parser efficiency (#23) The main goal here is to also download and modify filelists and modules metadata to fix some problems I saw in EPEL 9 update tests. We need the filelists metadata to get correct results if a package has a dependency on a file that is not present in the primary metadata; if we don't also download filelists, we'll get an incorrect "new" broken dependency because repoclosure on the modified repository will not be able to find the package that contains the file. Similarly, we need modules metadata (if present) to ensure that dnf knows module packages are module packages. Without the modules metadata it treats all module packages as non-module packages, so repoclosure on the modified repo might incorrectly say a dep of a non-module package is fixed because it can be satisfied by a module package. However, there was a big trap lurking here: the filelists metadata is even larger than the primary metadata, and both are kinda huge. Previously we were using ElementTree.parse(), which loads the entire XML tree into memory; this was already using >6GB of RAM for a typical primary metadata file. If you tried to load both primary and filelists into RAM at once (as my initial attempt did) it uses a huge amount of RAM and probably gets OOM killed. So, we'll parse the XML as a chunked bytestring. As we go along, we split out package elements, one by one. Everything that is not part of a package element gets passed straight through to the output file. When we encounter a package element, we parse it with lxml (which gives us a nice ~2x speedup over ElementTree). We decide whether to drop it. We do this the same way as before for the primary metadata, but record the pkgid (which is usually the checksum). When parsing the filelists metadata, we take the list of pkgids removed from the primary metadata as input, and remove all package elements with the same pkgid. If we decide to drop the package, we move to the end of it in the current input chunk. Otherwise, we write the chunk through to the output file, then move ahead and continue. We use the `open()` methods of various compression libraries to achieve transparent decompression and recompression, and track uncompressed checksums and sizes along the way. This also adds support for various other compression formats; previously we assumed zstd. Supporting at least gzip is important as there are still extant RHEL releases with only gzip-compressed metadata. We switch the other uses of ElementTree to lxml for consistency. Signed-off-by: Adam Williamson --- install.requires | 2 + pyproject.toml | 3 + rmdepcheck.py | 342 ++++++++++++++---- tests/test_rmdepcheck.py | 127 +++++-- tests/testdata/test_replace_fl_binary.xml | 33 ++ tests/testdata/test_replace_fl_binary.xml.gz | Bin 0 -> 663 bytes tests/testdata/test_replace_fl_binary.xml.xz | Bin 0 -> 700 bytes tests/testdata/test_replace_fl_binary.xml.zst | Bin 0 -> 629 bytes .../test_replace_fl_binary_expected.xml | 30 ++ tests/testdata/test_replace_fl_source.xml | 9 + tests/testdata/test_replace_fl_source.xml.gz | Bin 0 -> 300 bytes tests/testdata/test_replace_fl_source.xml.xz | Bin 0 -> 344 bytes tests/testdata/test_replace_fl_source.xml.zst | Bin 0 -> 279 bytes .../test_replace_fl_source_expected.xml | 6 + .../test_replace_fl_source_minified.xml | 1 + ...st_replace_fl_source_minified_expected.xml | 1 + .../testdata/test_replace_primary_binary.xml | 189 ++++++---- .../test_replace_primary_binary.xml.gz | Bin 0 -> 1377 bytes .../test_replace_primary_binary.xml.xz | Bin 0 -> 1316 bytes .../test_replace_primary_binary.xml.zst | Bin 0 -> 1296 bytes .../test_replace_primary_binary_expected.xml | 243 +++++++++++++ .../testdata/test_replace_primary_source.xml | 8 +- .../test_replace_primary_source.xml.gz | Bin 0 -> 677 bytes .../test_replace_primary_source.xml.xz | Bin 0 -> 716 bytes .../test_replace_primary_source.xml.zst | Bin 0 -> 665 bytes .../test_replace_primary_source_expected.xml | 27 ++ .../test_replace_primary_source_minified.xml | 1 + ...place_primary_source_minified_expected.xml | 1 + tox.requires | 2 + 29 files changed, 864 insertions(+), 161 deletions(-) create mode 100644 tests/testdata/test_replace_fl_binary.xml create mode 100644 tests/testdata/test_replace_fl_binary.xml.gz create mode 100644 tests/testdata/test_replace_fl_binary.xml.xz create mode 100644 tests/testdata/test_replace_fl_binary.xml.zst create mode 100644 tests/testdata/test_replace_fl_binary_expected.xml create mode 100644 tests/testdata/test_replace_fl_source.xml create mode 100644 tests/testdata/test_replace_fl_source.xml.gz create mode 100644 tests/testdata/test_replace_fl_source.xml.xz create mode 100644 tests/testdata/test_replace_fl_source.xml.zst create mode 100644 tests/testdata/test_replace_fl_source_expected.xml create mode 100644 tests/testdata/test_replace_fl_source_minified.xml create mode 100644 tests/testdata/test_replace_fl_source_minified_expected.xml create mode 100644 tests/testdata/test_replace_primary_binary.xml.gz create mode 100644 tests/testdata/test_replace_primary_binary.xml.xz create mode 100644 tests/testdata/test_replace_primary_binary.xml.zst create mode 100644 tests/testdata/test_replace_primary_binary_expected.xml create mode 100644 tests/testdata/test_replace_primary_source.xml.gz create mode 100644 tests/testdata/test_replace_primary_source.xml.xz create mode 100644 tests/testdata/test_replace_primary_source.xml.zst create mode 100644 tests/testdata/test_replace_primary_source_expected.xml create mode 100644 tests/testdata/test_replace_primary_source_minified.xml create mode 100644 tests/testdata/test_replace_primary_source_minified_expected.xml diff --git a/install.requires b/install.requires index e69de29..65569d3 100644 --- a/install.requires +++ b/install.requires @@ -0,0 +1,2 @@ +backports.zstd ; python_version<'3.14' +lxml diff --git a/pyproject.toml b/pyproject.toml index d2d3dbd..050f71d 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,5 +56,8 @@ show_missing = true # don't @ me, Hynek line-length = 100 +[tool.mypy] +plugins = ["mypy_plugin_lxml.main"] + [tool.setuptools.dynamic] dependencies = { file = ["install.requires"] } diff --git a/rmdepcheck.py b/rmdepcheck.py index f787fe3..4b31859 100755 --- a/rmdepcheck.py +++ b/rmdepcheck.py @@ -19,6 +19,8 @@ # # Author(s): Adam Williamson +# pylint: disable=c-extension-no-member + """RPM package installability and reverse-dependency checks using a repository modification strategy (hence 'rm'). """ @@ -26,24 +28,34 @@ repository modification strategy (hence 'rm'). # Standard libraries import argparse +import gzip import hashlib import json +import lzma import os import platform import subprocess import sys import tempfile -import xml.etree.ElementTree as et - +from contextlib import contextmanager from functools import partial -from typing import Iterable +from typing import Any, Generator, Iterable from urllib.parse import urlparse +import lxml.etree as et + +if sys.version_info >= (3, 14): + from compression import zstd +else: + from backports import zstd # pragma: no cover + # 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] +# Mainly for tests to override to exercise the parser +CHUNKSIZE = 1 << 20 CURLARGS = ("curl", "-s", "-f", "-L", "--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 @@ -59,6 +71,7 @@ SUBPCAPTURE = partial(subprocess.run, capture_output=True, text=True, check=Fals SUBPCAPTCHECK = partial(subprocess.run, capture_output=True, text=True, check=True) SUBPCHECK = partial(subprocess.run, check=True) REPOHASHES = {} +SAFEPARSER = et.XMLParser(resolve_entities=False) def mfind(element: et.Element, string: str, ns: dict) -> et.Element: @@ -122,62 +135,236 @@ 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 mfind(repomdroot, "repo:data[@type='primary']", XMLNS) - - -def download_primary(primary: et.Element, repourl: str, mrepodir: str) -> str: +def download_element(element: 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. + download the specified data file, returning the filename. Note + mrepodir/repodata is assumed to exist. """ - primloc = mfind(primary, "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", "") + elemloc = mfind(element, "repo:location", XMLNS).attrib["href"] + encelemfn = f"{mrepodir}/{elemloc}" + get_file(f"{repourl}/{elemloc}", encelemfn) + return encelemfn -def replace_primary(primfn: str, removes: Iterable[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. +# we can stop using Any and use io.Reader / io.Writer once we stop +# caring about Python < 3.14 +@contextmanager +def _open_read(path: str) -> Generator[Any, None, None]: + """Open a repo metadata file for reading, decompressing transparently. + Written by Cursor 2.6.11 + Claude 4.6 Opus. """ - 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): - if mfind(pkg, "common:arch", XMLNS).text == "src": - spkg = mfind(pkg, "common:name", XMLNS).text + if path.endswith(".gz"): + with gzip.open(path, "rb") as f: + yield f + elif path.endswith(".xz"): + with lzma.open(path, "rb") as f: + yield f + elif path.endswith((".zst", ".zstd")): + with zstd.open(path, "rb") as f: + yield f + else: + with open(path, "rb") as f: + yield f + + +@contextmanager +def _open_write(path: str) -> Generator[Any, None, None]: + """Open a repo metadata file for writing, compressing transparently. + Written by Cursor 2.6.11 + Claude 4.6 Opus. + """ + if path.endswith(".gz"): + with gzip.open(path, "wb", compresslevel=6) as f: + yield f + elif path.endswith(".xz"): + with lzma.open(path, "wb", preset=6) as f: + yield f + elif path.endswith((".zst", ".zstd")): + with zstd.open(path, "wb", level=3) as f: + yield f + else: + with open(path, "wb") as f: + yield f + + +def _next_chunk(infh: Any, size: int = 0) -> bytes: + """Read an arbitrarily-sized chunk of data from infh, ensuring it + ends with a > for ease of parsing. Defaults to quite a large size. + Note that using size=1 acts as 'read to end of next tag'. + """ + if not size: + size = CHUNKSIZE + chunk = infh.read(size) + while chunk and not chunk.endswith(b">"): + char = infh.read(1) + if char: + chunk += char else: - spkg = mfind(mfind(pkg, "common:format", XMLNS), "rpm:sourcerpm", XMLNS).text - if spkg: - spkg = spkg.rsplit("-", 2)[0] - if spkg and spkg in removes: - primroot.remove(pkg) + break + return chunk - tempfn = f"{rddir}/primtemp.xml" - primtree.write(tempfn) - with open(tempfn, "rb") as tempfh: - opensum = hashlib.sha256(tempfh.read()).hexdigest() - opensize = os.path.getsize(tempfn) - SUBPCHECK(("zstd", "-q", tempfn)) - with open(f"{tempfn}.zst", "rb") as tempfhz: - csum = hashlib.sha256(tempfhz.read()).hexdigest() - 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 _maybe_remove(currpkg: bytes, removes: Iterable[str], flmode: bool) -> str: + """Given a bytestring representing a single XML package element + starting "", parse it to find + the (source) package name and compare against removes to decide + whether it should be removed. Returns the pkgid of the package if + it should be removed, empty string if it should not. + """ + # force in namespace definition, ugh. we have to do this or else + # lxml will refuse to parse the fragment. we know it starts with + # ' tuple[set[str], str, int]: + """Parse primary or filelists XML metadata, remove packages + in removes. Works slightly differently in each mode. + """ + removed: set = set() + # parse infh in chunks, pass through non-package content to outfh. + # parse package content one-by-one, using _maybe_remove to decide + # whether to keep (pass through) or drop each package text blob + currpkg = b"" + chunk = _next_chunk(infh) + sha = hashlib.sha256() + size = 0 + while chunk: + if currpkg: + # look for end of package text + endpos = chunk.find(b"") + if endpos != -1: + # decide whether to remove package + currpkg += chunk[: endpos + 10] + nextpos = endpos + 10 + # if we're at the end of the chunk, read in another + # tag and add it, to simplify the next bit + if nextpos >= len(chunk): + chunk += _next_chunk(infh, size=1) + # if next char is \n, include it in the package block + nextchar = chunk[endpos + 10 : endpos + 11] + if nextchar == b"\n": + currpkg += b"\n" + nextpos = endpos + 11 + pkgid = _maybe_remove(currpkg, removes, flmode) + if pkgid: + # we should remove it + removed.add(pkgid) + else: + # pass it through, update csum and size + sha.update(currpkg) + size += len(currpkg) + outfh.write(currpkg) + # reset current package text buffer + currpkg = b"" + # move to appropriate position in chunk + chunk = chunk[nextpos:] + continue + # package does not end in current chunk, add whole + # chunk to buffer and read next + currpkg += chunk + chunk = _next_chunk(infh) + continue + # we're not in a package block, so find the next package tag + # there is a potential issue with packager tags, but we should + # always encounter a package tag before we encounter a + # packager tag + startpos = chunk.find(b" + chunk = chunk[startpos + 8 :] + continue + # no package tag found in chunk, pass through whole + # chunk, updating csum and size + sha.update(chunk) + size += len(chunk) + outfh.write(chunk) + # read next chunk + chunk = _next_chunk(infh) + continue + + return (removed, sha.hexdigest(), size) + + +# pylint: disable-next=too-many-locals +def replace_packages( + primfn: str, flfn: str, removes: Iterable[str] +) -> tuple[tuple[str, int, str, int], ...]: + """Parse the primary and filelists data files, remove any packages + whose source package name matches one in removes, and write out new + files with the correct names (containing their own sha256sum). + Return the checksums and sizes of the new uncompressed and + compressed files, for writing back into the repomd. We use raw + line by line text parsing to do this, because these files are huge + and parsing them with ElementTree.parse uses a huge amount of RAM. + Using iterparse would be messier and harder than doing this. + """ + ret = [] + primremoved: Iterable[str] = set() + # find the repodata directory + rddir = os.path.dirname(primfn) + # parse both primary and filelists metadata + for fn, typ, flmode in ((primfn, "primary", False), (flfn, "filelists", True)): + if flmode: + # remove the same packages we removed in the prior iteration + toremove = primremoved + else: + # remove packages built from the srpm names in 'removes' + toremove = removes + # get the file extensions + exts = os.path.basename(fn).split(".")[1:] + # construct a temporary filename with the same extensions + tempfn = ".".join([f"{rddir}/{typ}temp"] + exts) + with _open_read(fn) as infh, _open_write(tempfn) as outfh: + # parse from input file to temporary file with inline + # compression, checksumming and size discovery + removed, opensum, opensize = parse_xml(infh, outfh, toremove, flmode=flmode) + if not flmode: + # populate the to-remove set for the next iteration + primremoved = removed + # get compressed checksum and size + with open(tempfn, "rb") as outfh: + csum = hashlib.sha256(outfh.read()).hexdigest() + size = os.path.getsize(tempfn) + # rename output file to expected name with checksum and type + os.rename(tempfn, ".".join([f"{rddir}/{csum}-{typ}"] + exts)) + # return compressed and uncompressed sums and sizes + ret.append((csum, size, opensum, opensize)) + return tuple(ret) def get_base_repoclosure(baserepos: Iterable[str], nmbaserepos: Iterable[str]) -> str: @@ -213,26 +400,45 @@ def get_modified_repoclosure( 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) + repomdtree = et.parse(repomdfn, parser=SAFEPARSER) repomdroot = repomdtree.getroot() - primary = get_primary(repomdroot) - primfn = download_primary(primary, mrepo, mrepodir) - csum, size, opensum, opensize = replace_primary(primfn, removes) - + # we need to also download and modify filelists, for file + # dependencies that aren't included in primary + filelists = mfind(repomdroot, "repo:data[@type='filelists']", XMLNS) + flfn = download_element(filelists, mrepo, mrepodir) + flexts = os.path.basename(flfn).split(".")[1:] + primary = mfind(repomdroot, "repo:data[@type='primary']", XMLNS) + primfn = download_element(primary, mrepo, mrepodir) + primexts = os.path.basename(primfn).split(".")[1:] + # https://github.com/pylint-dev/pylint/issues/5671#issuecomment-4239834783 + primdata, fldata = replace_packages(primfn, flfn, removes) # pylint:disable=W0632 + # we also need the module metadata if present, or else + # module packages will be treated as non-module and cause + # false results + modules = repomdroot.find("repo:data[@type='modules']", XMLNS) + if modules is not None: + # setting up a test repo with module metadata is a huge pain + download_element(modules, mrepo, mrepodir) # pragma: no cover # modify the repomd - mfind(primary, "repo:checksum", XMLNS).text = csum - mfind(primary, "repo:size", XMLNS).text = str(size) - mfind(primary, "repo:open-checksum", XMLNS).text = opensum - mfind(primary, "repo:open-size", XMLNS).text = str(opensize) - mfind(primary, "repo:location", XMLNS).attrib["href"] = f"repodata/{csum}-primary.xml.zst" - # requires Python 3.10: - # notprimary = repomdroot.findall("repo:data[@type]", XMLNS) + for typ, data in ((primary, primdata), (filelists, fldata)): + csum, size, opensum, opensize = data + mfind(typ, "repo:checksum", XMLNS).text = csum + mfind(typ, "repo:size", XMLNS).text = str(size) + mfind(typ, "repo:open-checksum", XMLNS).text = opensum + mfind(typ, "repo:open-size", XMLNS).text = str(opensize) + if typ == primary: + mfind(primary, "repo:location", XMLNS).attrib["href"] = ".".join( + [f"repodata/{csum}-primary"] + primexts + ) + else: + mfind(filelists, "repo:location", XMLNS).attrib["href"] = ".".join( + [f"repodata/{csum}-filelists"] + flexts + ) + alldata = repomdroot.findall("repo:data[@type]", XMLNS) - notprimary = [data for data in alldata if data is not primary] - for item in notprimary: + notused = [data for data in alldata if data not in [primary, filelists, modules]] + for item in notused: 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}"]) @@ -442,7 +648,7 @@ def parse_args() -> argparse.Namespace: def check_utils() -> None: """Check required utilities are installed.""" missing = [] - for prog in (("zstd", "-V"), ("dnf", "--version"), ("curl", "-V")): + for prog in (("dnf", "--version"), ("curl", "-V")): try: subprocess.run(prog, stdout=subprocess.DEVNULL, check=True) except FileNotFoundError: diff --git a/tests/test_rmdepcheck.py b/tests/test_rmdepcheck.py index e70d4f4..35a9bd6 100644 --- a/tests/test_rmdepcheck.py +++ b/tests/test_rmdepcheck.py @@ -22,6 +22,10 @@ """Tests for rmdepcheck.""" +import glob +import gzip +import io +import lzma import os import shutil import sys @@ -29,6 +33,11 @@ import tempfile import xml.etree.ElementTree as et from unittest import mock +if sys.version_info >= (3, 14): + from compression import zstd +else: + from backports import zstd + import pytest import rmdepcheck @@ -79,61 +88,137 @@ def test_get_file(): assert "This file is" in testfh.read() -def test_download_primary(): +def test_download_element(): repomdtree = et.parse(f"{REPOS}/base/repodata/repomd.xml") repomdroot = repomdtree.getroot() - primary = rmdepcheck.get_primary(repomdroot) + primary = repomdroot.find("repo:data[@type='primary']", rmdepcheck.XMLNS) 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) + rmdepcheck.download_element(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/54942fbb3415a66f4ca49463d36439407c816b3eed8e30fc43016bd70ac9d456-primary.xml" + f"{tempdir}/repodata/54942fbb3415a66f4ca49463d36439407c816b3eed8e30fc43016bd70ac9d456-primary.xml.zst" ) +def test_parse_xml_empty(): + """Test parse_xml can handle an empty repo. We mostly test it + implicitly, but easiest to test this explicitly. + """ + empty = b""" + + +""" + infh = io.BytesIO(empty) + outfh = io.BytesIO() + assert rmdepcheck.parse_xml(infh, outfh, ["somepackage"], False) == ( + set(), + "0f9fd176ae380f60833f432e009774b256c2e77fc50740be60eaaf06d49ee004", + 168, + ) + outfh.seek(0) + assert outfh.read() == empty + + +@pytest.mark.parametrize("extension", (".xml", ".xml.zst", ".xml.gz", ".xml.xz")) @pytest.mark.parametrize( "repotup", ( ( "binary", ( - "42daebf05f6c3cabe9d029acb3a8056f7fc533cde9820b7388d90acb1f6e7dbe", - 1067, - "562981a96ba946cd0f993d180108c4dd107d1a5f24210c5768b19cdd93e8f33a", - 7531, + ( + "8cba35dd9233f535f90d2c1a6f7c00a8ae8bfcc9b0854f50ef593ab8f685cd7c", + 9596, + ), + ( + "03254e576554e240d9d2434ac9e6f06e71d50adf5e5699acdecd28d0ceafaa37", + 1562, + ), ), ), ( "source", ( - "3378e32450503892f65c6ff2a77a451f069146c4284a69f21752e113918da64f", - 566, - "7ac52d6a0f3c0314a58f78eff39f4a7b5721127816b1b59d8ef30f4a29e091d9", - 1045, + ( + "c55ba80f7615e041c9d84b8237a6dd4afb9e56c9d7e1776324a164d4e0b921a1", + 1082, + ), + ( + "97c9e867dca2f9f1d0c1f45e77c5ffb9e6b91e5a76b73240bc162aa4d8b8bd9b", + 282, + ), ), ), ), ) -def test_replace_primary(repotup): +def test_replace_packages(extension, repotup): + # this exercises some rarely-encountered edge case parser paths + if extension == ".xml": + rmdepcheck.CHUNKSIZE = 1 repo, expected = repotup with tempfile.TemporaryDirectory() as tempdir: shutil.copy2( # binary.xml is an old version of base's primary file # source.xml is a primary file from a repo with just # ccc.src and ddd.src packages - f"{TESTDATA}/test_replace_primary_{repo}.xml", - f"{tempdir}/test.xml", + f"{TESTDATA}/test_replace_primary_{repo}{extension}", + f"{tempdir}/testprim{extension}", ) - ret = rmdepcheck.replace_primary(f"{tempdir}/test.xml", "ccc") + shutil.copy2( + f"{TESTDATA}/test_replace_fl_{repo}{extension}", + f"{tempdir}/testfl{extension}", + ) + ret = rmdepcheck.replace_packages( + f"{tempdir}/testprim{extension}", f"{tempdir}/testfl{extension}", "ccc" + ) + # skip the compressed sum and size as they may change with + # python version + ret = ((ret[0][2], ret[0][3]), (ret[1][2], ret[1][3])) assert ret == expected - assert os.path.exists( - # pylint: disable-next=line-too-long - f"{tempdir}/{expected[0]}-primary.xml.zst" + gotprim = glob.glob(f"{tempdir}/*primary.xml*")[0] + gotfl = glob.glob(f"{tempdir}/*filelists.xml*")[0] + funcmap = { + ".xml": open, + ".xml.zst": zstd.open, + ".xml.gz": gzip.open, + ".xml.xz": lzma.open, + } + with funcmap[extension](gotprim, "rb") as gotpfh: + with open(f"{TESTDATA}/test_replace_primary_{repo}_expected.xml", "rb") as exppfh: + assert gotpfh.read() == exppfh.read() + with funcmap[extension](gotfl, "rb") as gotffh: + with open(f"{TESTDATA}/test_replace_fl_{repo}_expected.xml", "rb") as expffh: + assert gotffh.read() == expffh.read() + + +def test_replace_packages_minified(): + rmdepcheck.CHUNKSIZE = 1 + with tempfile.TemporaryDirectory() as tempdir: + shutil.copy2( + # same as source but with all newlines removed + f"{TESTDATA}/test_replace_primary_source_minified.xml", + f"{tempdir}/testprim.xml", ) + shutil.copy2( + # same as source but with all newlines removed + f"{TESTDATA}/test_replace_fl_source_minified.xml", + f"{tempdir}/testfl.xml", + ) + rmdepcheck.replace_packages(f"{tempdir}/testprim.xml", f"{tempdir}/testfl.xml", "ccc") + gotprim = glob.glob(f"{tempdir}/*primary.xml*")[0] + gotfl = glob.glob(f"{tempdir}/*filelists.xml*")[0] + with open(gotprim, "rb") as gotpfh: + with open( + f"{TESTDATA}/test_replace_primary_source_minified_expected.xml", "rb" + ) as exppfh: + assert gotpfh.read() == exppfh.read() + with open(gotfl, "rb") as gotffh: + with open(f"{TESTDATA}/test_replace_fl_source_minified_expected.xml", "rb") as expffh: + assert gotffh.read() == expffh.read() def test_get_base_repoclosure(): @@ -256,10 +341,10 @@ def test_check_arch(_): @mock.patch("subprocess.run", autospec=True) def test_check_utils(mock_run): rmdepcheck.check_utils() - mock_run.side_effect = [FileNotFoundError, None, FileNotFoundError] + mock_run.side_effect = [None, FileNotFoundError] with pytest.raises(SystemExit) as excinfo: rmdepcheck.check_utils() - assert excinfo.value.code == "Please install missing required utilities: zstd curl" + assert excinfo.value.code == "Please install missing required utilities: curl" @mock.patch("rmdepcheck.check_utils", side_effect=KeyboardInterrupt) diff --git a/tests/testdata/test_replace_fl_binary.xml b/tests/testdata/test_replace_fl_binary.xml new file mode 100644 index 0000000..0cd47e0 --- /dev/null +++ b/tests/testdata/test_replace_fl_binary.xml @@ -0,0 +1,33 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/testdata/test_replace_fl_binary.xml.gz b/tests/testdata/test_replace_fl_binary.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..0d5d2e4f33084729f2aa97fee091ccc17dcefe78 GIT binary patch literal 663 zcmV;I0%-joiwFpqgWYKW19W9`bYF61aBN{?WnX4&Ut(!)VRCsccx`L|wUxng+c*q{ z?|ll5?i+(72!frsz3mgU=N%9PY)>2~lQ=th{Do)Y_OQLRK1m`e>f?|9hwi_=KOUEV z?dA3GblR@z4%Wp^`ZN!x-FE%{?eD*M{rct8y&sNtJiMM?mso##-LCud`T6r^b3B~h z-tXqygWcJD+dSG?rkrK-WBs~3i$2KC;++I*Z1wTHJU{FXbGu5JK%y`cN=ikuA~-@D zG&^t?8MSy;nTCLpxy}Y=4rNvp^sz1{d9>I^WL@M%_uKWI=^w)Q*0S7x9Br}Zr^{3v zFGfBN`eMhpTQ>o<`JnngAB%$xcBnWQkXjPpREPV>*|7^6ry!Wsi$Oyw!#QQK zE8iI7CVc4HTvVx{49)~#g;l!*fFXIUy-dqZQPksFxl+zb+zV%LzVfN+ZS>2%%M1;5 zP?n%e1HHJUpq)80O93xTkO?z3n3S~A=$@@j8G7ZLbKZonR~aQdHKy}OnrDWvpaJS= zogjt#1P38OfP!+=m$FY9zz9_GmCwv>!e>!Zs&2D#QtEXG>nwzgpbye4LbHz9UBZ!r zWyTpav(-|R+oUU>yWfN_CroxR|0S9o&GU%F&ViO92qQ8~%p_HyN%hnV%tt= zV30v0H)u{pLK~FXx*57dilP=QlS*oOne)VsA`Qc|p0NH%$9})RfqD)iJ+)G!7i-ZW xP&JQ&;W%l<0-~oL>G3`brq)V$PjXd{<}AkW3%cJ^@8^Hz%YRd2*v+~I004ygPBs7l literal 0 HcmV?d00001 diff --git a/tests/testdata/test_replace_fl_binary.xml.xz b/tests/testdata/test_replace_fl_binary.xml.xz new file mode 100644 index 0000000000000000000000000000000000000000..774802ca478378b2cbd546070b7a571ed139e91c GIT binary patch literal 700 zcmV;t0z>`%H+ooF000E$*0e?h!1)Ba4Iu#*000000002y{dnWx2Dt)pT>u^r%ZCxz z&SsGhgC5HLdo$F3>9^V_F?JZEE)^a5ymM%pF!1guNXVqJBmKwRcCx@Qf81FwoS)3{&vUaM4fK|B&_1J3D3aPhq`)UPXd>Wnq5klu9l^PP46*vJK1$%U~cic$8&*cHvX zDYDv0blyKsksMz8F}1R(q$ou^W94lI@gfh77n~=V-3r*!Xf zIWF6UC=vm9lOzbODh($P)6&JtcI=*5K=5gU=?)0McIz8L@XGc zC4^7L$jS2#c&ZQ?zl)Q7*xTBJPIPmF#WpUl?T;F!MfPNt0Ho%0K&-2TIXnQ>F2!^nM78RnsN1jRp2YzD+8rNe7a-lK9!!OmH5>2KCugO#V7dggwGsx zmQ>vj3K(J}#<~ejC3z`T@A^t*3sQHE!4fuI6F=o{qo-^JN^vOlV;llpm=9*xNIDHH~h*D&_X^nLpJy`u6U!kw+ zZSYsl@h6fc&WfaE59h$((kWdurpsfzBpm=B(@PvmJhqfk_E`5!66b6g$RO0c4&op zzK&*^b*>uy^S8#U)NtABTbIR>ne)r?EQxG}C{Z$h*5hQ}XF=%k{P*a>oX4xsa`h52 z>nHx(T~{;td+<`Z=D9Ms_@On)q@OPLX8XU$WT{Dbm3yc@*XuPE3xI|~F$yE%4N5IA zcw&)A05p_cXEBd7l_JGV%v!cwjotVMZ{8Y&nS1au#-|e552HjGdk ziX1e1yrl(;fdbjo5hI&&jweQHIp@U literal 0 HcmV?d00001 diff --git a/tests/testdata/test_replace_fl_binary_expected.xml b/tests/testdata/test_replace_fl_binary_expected.xml new file mode 100644 index 0000000..5b4818d --- /dev/null +++ b/tests/testdata/test_replace_fl_binary_expected.xml @@ -0,0 +1,30 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/tests/testdata/test_replace_fl_source.xml b/tests/testdata/test_replace_fl_source.xml new file mode 100644 index 0000000..301576a --- /dev/null +++ b/tests/testdata/test_replace_fl_source.xml @@ -0,0 +1,9 @@ + + + + + + + + + \ No newline at end of file diff --git a/tests/testdata/test_replace_fl_source.xml.gz b/tests/testdata/test_replace_fl_source.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..67d32b749697be2317f3bdeb910fb9a79558bba9 GIT binary patch literal 300 zcmV+{0n`2;iwFpqgWYKW19W9`bYF61aBN{?WnX4&UvqDDa${vKcx`L|t&mGn!!Qhn z_ng9`J&j$z5~gW)oB$3WOLCh@nr4!e>G3f$0Tygo=`DS~4yVU;P#-cc-EC~a>@+AD z>Bil7ZsGmy^|^-A@#&DeL56Nw7ghSlrG?A7PA^q8bmRT8<9jbV;d^zJ6&Y8oe)>U8 zNIjmV%R2}})P}A1zF3P)x{lc-;^dRI-*C+Q4{*OvJ$?cJAbe}L0RRAT*o*N1 literal 0 HcmV?d00001 diff --git a/tests/testdata/test_replace_fl_source.xml.xz b/tests/testdata/test_replace_fl_source.xml.xz new file mode 100644 index 0000000000000000000000000000000000000000..3397f61e235a370e9983e4ec642af1b8aa5954ec GIT binary patch literal 344 zcmV-e0jK``H+ooF000E$*0e?hz?cHJ10ewx000000001tW|p4d0k#1UT>u^r%ZCxz z&SsGhgC5HLdo$F3>9^V_F?JZEE)^a5ymM%pF!1guNXVqJBmKwRcCx@Qf81FwoS)3{&vm5qiSfD$Rj0No>qI}0yUAg<=x`AJg;V&od#&kEgKpS zNlh-pc}U&&rbd`vUnA_JQh{a4A*zzz8QQR$SHSt9|J!05IT3SHZ}tV>pJaw}8IvNn zA+&;>C=etEn_MKXa}5^+|6*cUv|GoTmz`qM3Jolpi){~lDr698B9EL>~ z1UCRL04)Fr#Sa#WhiQ%@0Gg(QM(Jssg_0(TX=cMLiJ=q}0tgHcXf!;`C;XX8cd#@* z2}X)$fohyZAqfmyJ2UCJxqU}m%eAlNY>l6=wAJ{G0RgGRP?VrinubPrLrRaH`g@wr zq4+tN0f#P)&5U#t&{T<1hN2!x8V??|Qg_!loqm)yd-5SHmpku!vvkiF+3mT9v=++$ zvz090RC; + + + + + \ No newline at end of file diff --git a/tests/testdata/test_replace_fl_source_minified.xml b/tests/testdata/test_replace_fl_source_minified.xml new file mode 100644 index 0000000..de799b1 --- /dev/null +++ b/tests/testdata/test_replace_fl_source_minified.xml @@ -0,0 +1 @@ + diff --git a/tests/testdata/test_replace_fl_source_minified_expected.xml b/tests/testdata/test_replace_fl_source_minified_expected.xml new file mode 100644 index 0000000..d9e30d7 --- /dev/null +++ b/tests/testdata/test_replace_fl_source_minified_expected.xml @@ -0,0 +1 @@ + diff --git a/tests/testdata/test_replace_primary_binary.xml b/tests/testdata/test_replace_primary_binary.xml index ad87cd9..2029628 100644 --- a/tests/testdata/test_replace_primary_binary.xml +++ b/tests/testdata/test_replace_primary_binary.xml @@ -1,15 +1,15 @@ - + aaa x86_64 - 7188c68d3407d04571b70e3a8d3c8e6329c34d2dd7af8a6c7364a0b29c991cec + a3d06ac0d14a113c2c40f407b6d9089aab53f2cade404a285d70eef03d54a4fb Dummy summary This is a dummy description. Fedora Project - @@ -57,12 +57,12 @@ ccc x86_64 - 3c810c039112c6d626a9dd628b38af6cbcb387c710b702d51028f6c215c34ad6 + 4fc7facc674ab82600e38cc9a3bfe46f5593de27c4acfe27585622c6893820f8 Dummy summary This is a dummy description. Fedora Project - \ No newline at end of file diff --git a/tests/testdata/test_replace_primary_binary.xml.gz b/tests/testdata/test_replace_primary_binary.xml.gz new file mode 100644 index 0000000000000000000000000000000000000000..3badbbf07684147e4b4f2d3fa50b35aca29f91ed GIT binary patch literal 1377 zcmV-n1)lmJiwFpqgWYKW19W9`bYF61aBN{?WnXY|X>DP0d0%2_ZeenHE_iKh0Nq+m zZ`(!?z2{dDJhca#`@Ks;Qc$2tFFB-Wdg!T`-JO+KOQK3qhTUJ^p(KZDlfaQB%K!n( zB8RgxoE^TmZ)uO--Y@3a9X9K#UXF8q&GHPFp^nvZI?jLp^#01_Zzpd?3v3}m3mLgD z*W-NFwyT?>m{-fq{dL^j;x)!i@#s~AdaMSuLI%K|Xo5f^Z8{r1+fWly!fe zwGS&C=bg*sO_q(8u)qlbjEe11XM!foCil+#Y1F9Lo^;lIT^X+GFdOGo8(H>MHaMrF zyx2O18HU^SX0gw-o`KMY?&o$|#c}?}yWjE&$jA%?7P$h>We`EJq?q+4mdup^Udto` zL{vhYtzgpqoVuWowd~hvsBHgd~U5$ z&Iu#(%x|iBJTN`Z*t+@$ANw`VjTB{`Rm*h?^Et-tRCisd?z(V7?FY^40NqVyvj)>R zrxspOFX*%D+;!LCooA|>1+?8EosjuvUIkpPaq`Q@52NDH(7W8>lB)fp=7USU@3g5m ztI1ER6@}T}(z>7laTD6=u4*54qjpaHpzU1D>UBG5>w50*cV4}<_gvSTCLkS6`Wn|w z=nC8o>K*z)Go**7xoTiZk37v}(~fhcnd%TpQ)=j$+Re`1f7R4?6}5h6+8c4%HV;{c zi|zd8S(?L?0QK(I{7D#mE65Mjo-3n%JTlm>(!;wP6_0)W{3t$wa_{{`QSKly^e!>2 zykp8(4$N`wQ}RM2Rf0mtE%#bsX*g5T0WzzM^vQ7nIB=9Jemu&lg@Z?VU*qX0FYR$C zr`)HboPwMU<@@jr{=KQ1@1&;>pT5W?Dc65;90oj|NkRx0WfGNwO%Q^y3OqrIF_bQZ z5{N2LnWS|oBMKW7ghb!kX(L21u9Qx&V+8N zW%Yt6ONgWyf;R#PEJQ9!8{SJadZCdO8p$5ogp3)WX3tNnhjT^#bKSok=LjE9!-m(nV2@v=J~mVbqQX zQ)&+`L}`>Ytn?-b028&3WrCEvcMypQf^ev%49-R~aFjnsFHj2ykMh38%j*ToeLBi1 z$i?--chJ)r^}=*I{Xdgb#+eu55y2J5?j>aRhlWyN65eYWy+RJie94?~&VUAHK_^~H z7NfCb6j*8;%-e-;@QASTcpjk!?lIQ&ZHO$=S9m&$kRCgVMJV^_EJ8uHEF#5gXoasJ z{6)2b^(K)nK%-g7L}bw6T&EC~W64_J$vEz?Ok4*9t$<1HJWACG3fdqH8{^N~5%dTc zu2wiF%8%U(-E$YL^$O>9pMz@Z0q>iI20mBQpV?hY@GZH@{zVkVkrOsxO1wv literal 0 HcmV?d00001 diff --git a/tests/testdata/test_replace_primary_binary.xml.xz b/tests/testdata/test_replace_primary_binary.xml.xz new file mode 100644 index 0000000000000000000000000000000000000000..95608bf939a370e8e30b4ac2612e41a0b5090c2e GIT binary patch literal 1316 zcmV+<1>5@lH+ooF000E$*0e?hz~c$xQXv5r000000001ge&E62DPaWLT>u^r%ZCxz z&SsGhgC5HLdo$F3>9^V_F?JZEE)^a5ymM%pF!2BFRpseW9)Q-LPt=oq?6E zi%hf}V|v*8KFEb8V2>;!T2M|wb=CuL_>0yL-;qr|#vTFq)cEr6txeyD+8+(}E_!&8 zpZ%t#&D3@p-dFFW&@iB}v<`=p>C*9G5=u7XaMDmiPzK!5e(fXECGO%gJJr3%ta%%_ zYmk#7me4hhJN>J3`RL8WO76F$r4-g?FFgzz_=2%(>PxhiyktU?B)P8DgnQ?}yMe-Q z%u7VjKW26-kTXu=%9Sm@+sikY!9&AWvP*F*8aG`0rYJb=yyxe9lAz)*3=VVXKYHzO zh;S`n0vzIoT{V1C@iv*n#;U+duN2*qm6=Rt$X*X+~n zXDSTp)_xYF9Hail8WL-62MN5Sw&GhQFFW8lLKYiH;w*2gV6i(V&Bjhc#drT;5H>g# znLqVUvp;8(iX)7G78ugMCjPi&3{@=KoIih#AAOa~E4_M(T1u%}-)bjurR!XEjj>@f;(gYCwCMz5uAxP~Cj z45pVKlXzaqp|?}ljq3>Ll9EZ48JexB?k9AeKQ+I5}yV{NG*$$fci&Ry>eu?zz(q#9n z!wnrbtWTcT!TFdT{?Ib@^Y(p$T2n+m&^N!iAQhSvS@1W{^wnlgOF|2qB9yxKcF`It z?KbIm6ILfUAOR&@8=@f~kr@pE_&M~>4-?rv_XiZ_`mbZ-4cpvlQtPulE*U)%{%T)7 zdi-GKZvsdTz;)O>!8jb{%1LJFyKvd5krr^*H(u9N=-wQ_+_RnGsJtb<1%bed6K~W7 zn)tBT(RGgue$DJwu*OuX<5QM$|LR11d^7hXZbm96sV7o+nIa!NBJTot!a0r%gT>;QIb~;2G(-#N&Y`k=~5RhTV2cYjK1A z!I`<&X{uEUPG&p)dkxg}uo>!fPBUXHd7AoRufYe?YzTDS>x_&BqnpvI>azvTK&gm3 z1PZ7Y0_6kv31nQJ|7`Splx4{x^u4u>fhox(L@d^Cx21|_i0@W8U-pm`e(GsPa6wi* z33JC1l<2?tmf|k8_mnU?g3h1|>H1)xxv&4B)r)+XX1p&lbvuw#n1|!N=fTf~?g>(< z@of!E{Sk!Vbq0mD;cGqf+r5q!-cv|f@p6s$9R>;7?-4D2iNYMr7ytmU!~{M^#0mWX a0sjf&QUCxAZqGTf#Ao{g000001X)`A5tShT literal 0 HcmV?d00001 diff --git a/tests/testdata/test_replace_primary_binary.xml.zst b/tests/testdata/test_replace_primary_binary.xml.zst new file mode 100644 index 0000000000000000000000000000000000000000..2f5249f7d724f98d2fb97fa5fd9e32869f1a1153 GIT binary patch literal 1296 zcmV+r1@HPOwJ-f-Vki|T0Gdn~4JHsxst!k(@Ca>f5it`HAtr@`oXSf>OrN`CyGw~4 zgN&-GjsR5w+yK}Bu>cX&j$nz9grSW!B*&1P#=(++oJrwCEO{e|@$ftg1It6S&$Ez_ zWPaP`n(4bMY!}xyZQjf4nVY*Zs_ZwRw%5jsnNr(SEAP{0yBrM;Ksc0kmI4lwl0zn2Srl~m;DS>z#jGeZ3@+SeX5IEuj z0tXKcg-JLKal(y58PuU9AV-S4!9Y9?1d`2nsrZw<2GuvRrs_#=ZYrpvx0UVJn*Y8fSC*ak~D-IVX|`Xm8;~hX4nI1BW6pi+JNO9-=AXlDE?UMG6 zI8q~=#WarfR*bDds1C&u&Kv$X6Ot^5Xn`aXy96j&U~G$3Z=CagU-oqUb{D@X^}+U7 zbKL3VV-;QdyQgpY|GT@pyZc(~X^T2v|9Tr)WL38M+M8{kIi*j+$xXfS($$OAjNemX zv$;;Xdv|wtck|qazFyAfSiM%*R(nfdWUp*vT&v?;wa>my<*K{hWY_PNa&TgT!!ciTtx3>ACL8vA{v|IJ;(-uAkey@v|dd~-Ff*fb4`+?da(m~M0H?9@w- zD!x|>;cINo?^b)MRQFsT{5RoZ48t%CBNxLk46fx)`id{Y*R4#dcMEmhz8GIrIn`Hr zo6X#u>%B==O|2K7!!XSM|NsB5*Dx|;&g0^Mxu}7S(nLfi5D^iPBuR3xA|V>urVSFH zf*^`9IKvP)2q8j3h_E6c0wN#+#0Z24^8pSi0Ob7=KOU+L8F};k^Lss$I*NbL>j3e$ zmSh>afEEi_A6Gx1$#JY^*-zk|&n~m9F5kA`AF2>h93tu5?+TYsdY%Jl`p0{oE3P)Y zs*a_yJ^yD&1zLJNW`(ly6r=Qt4?XkV<&x1y+}$^^#&m9c$uqk*S-d+y>=YpToG69v{M*~3y@tPs*snW0bjb5K zcX*ZK%s>Hv%Tu1biM9-N4Dtkt_|8OWt&wNus}Sk2__6UnoE>Xwp~i+NB$eNfBoUS(Gxxb(lO=5|NqY!|br68e^o}f} z2cNHk=d(M-WW}Pn#|ep^Q?7{Y)t%DOsKrO}lBj5OQ*_R1zecYB8;p(>Q{(*u6WCF+ zOC$*3*Mc-BI5|p`#Pa%Hv(RhRzBGe3JsxeYZ9%Ru=xWP_+Pr}y&q{qw8c|w1=>pZ6 GFyez7i+u?I literal 0 HcmV?d00001 diff --git a/tests/testdata/test_replace_primary_binary_expected.xml b/tests/testdata/test_replace_primary_binary_expected.xml new file mode 100644 index 0000000..823f016 --- /dev/null +++ b/tests/testdata/test_replace_primary_binary_expected.xml @@ -0,0 +1,243 @@ + + + + aaa + x86_64 + + a3d06ac0d14a113c2c40f407b6d9089aab53f2cade404a285d70eef03d54a4fb + Dummy summary + This is a dummy description. + Fedora Project + + + + bbb + x86_64 + + 8af0eb8f054b804671a0815bffb22f49a4e8171b54e9610438ae07463bf812ae + Dummy summary + This is a dummy description. + Fedora Project + + + + ddd + x86_64 + + 8299adf0ecb62a00e1b4f3fa1ab8d2288a30f38602642470d61c56b9f2e7dabc + Dummy summary + This is a dummy description. + Fedora Project + + + + eee + x86_64 + + 95ab3d70bdf8b1c2d202c4e04cb7960153fa4b6460a300c838a62fd6b0840632 + Dummy summary + This is a dummy description. + Fedora Project + + + + fff + x86_64 + + e26065fed581c695ba8cd3647609b6c2aa6d5bd9faffbb8ad1c2c28e533c87d6 + Dummy summary + This is a dummy description. + Fedora Project + + + + ggg + i686 + + 2141e54e9285d94ef2bb53db4e1ae0b9086886a5a07a5f1930dd678682e95ef8 + Dummy summary + This is a dummy description. + Fedora Project + + + + ggg + x86_64 + + 7b6f760e650902d0a94ef5fcd480f77a1f6818e9f15cea54a0318be34d364c83 + Dummy summary + This is a dummy description. + Fedora Project + + + + hhh + i686 + + 8280ab6b2707f63add64ad79ee4ff01faf343d6317cadd6f18f4aa745abf145c + Dummy summary + This is a dummy description. + Fedora Project + + + + hhh + x86_64 + + f0aa917356d9c3cba05c2b346d5a73e5ab919b3df9d3417731c2a25cf3d5ae80 + Dummy summary + This is a dummy description. + Fedora Project + + + \ No newline at end of file diff --git a/tests/testdata/test_replace_primary_source.xml b/tests/testdata/test_replace_primary_source.xml index de5d101..363ee33 100644 --- a/tests/testdata/test_replace_primary_source.xml +++ b/tests/testdata/test_replace_primary_source.xml @@ -4,12 +4,12 @@ ccc src - 81b6e8246bcf98be9d18b0848c70e1614d522939231f08559c80970a6dd367ae + 04f2d544eba701ba69cd064a6b35ca4854497674abfc76b5acd05cc1b597b73a Dummy summary This is a dummy description. Fedora Project -