git mirror - github.com/owenewans/holy - branch master
clone: https://src.holypkg.eu/holy/

file tests/index.py

#!/usr/bin/env python3
"""a read-only file index over the installed set: which artifact owns a path and
which artifacts provide a name, with several candidates named rather than chosen."""
import hashlib
import json
import pathlib
import subprocess
import sys
import tempfile


binary = str(pathlib.Path(sys.argv[1]).resolve())


def call(*args, status=0):
    result = subprocess.run([binary, *map(str, args)], capture_output=True, text=True)
    assert result.returncode == status, (args, result.returncode, result.stdout, result.stderr)
    return result.stdout


def build(root, tree, name, *, files, provides="", deps="", arch="noarch", libc="nolibc"):
    import shutil
    shutil.rmtree(tree, ignore_errors=True)
    (tree / "HOLY").mkdir(parents=True)
    (tree / "DATA").mkdir(parents=True)
    (tree / "HOLY/meta").write_text(
        "format holy-package-1\nname %s\nversion 1\nrelease 1\nos linux\narch %s\nlibc %s\n"
        % (name, arch, libc))
    for field, body in (("deps", deps), ("provides", provides),
                        ("hooks", ""), ("origin", ""), ("transform", "")):
        (tree / "HOLY" / field).write_text(body)
    for relative, body in files.items():
        path = tree / "DATA" / relative
        path.parent.mkdir(parents=True, exist_ok=True)
        if isinstance(body, bytes):
            path.write_bytes(body)
        else:
            path.write_text(body)
    call("manifest", "generate", tree, "--output", root / (name + "-files"))
    (tree / "HOLY/files").write_text((root / (name + "-files")).read_text())
    artifact = root / (name + ".holy")
    call("pack", tree, "--output", artifact)
    call("cache", "stage", "local:" + str(artifact), "--root", root)
    return hashlib.sha256(artifact.read_bytes()).hexdigest()


def install(root, *digests):
    out = call("db", "plan-set", *digests, "--root", root)
    plan = [line.split()[10] for line in out.splitlines() if line.startswith("plan-set ")][0]
    call("db", "apply-set", plan, *digests, "--root", root)
    return out


def records(text):
    return [json.loads(line) for line in text.splitlines()]


def main():
    with tempfile.TemporaryDirectory() as scratch:
        base = pathlib.Path(scratch)
        root = base / "root"
        root.mkdir()
        tree = base / "tree"
        call("db", "init", "--root", root)

        # an empty root has an index with nothing in it
        report = records(call("index", "--root", root, "--json"))
        summary = report[-1]
        assert summary["type"] == "summary" and summary["artifacts"] == 0, summary
        assert summary["paths"] == 0 and summary["capabilities"] == 0, summary

        # a library, a second provider of its SONAME and an application that needs
        # one of them
        # the capability the index looks up is a declared one, so the fixtures are
        # text payloads: a requirement that names a package closes the set, while the
        # soname both providers declare is the ambiguity the index reports
        lib = build(root, tree, "indexlib", files={"usr/lib/libindex.so.1": "the library\n"},
                    provides="provide package indexlib noarch nolibc - metadata\n"
                             "provide soname libindex.so.1 noarch nolibc - metadata\n")
        # the rival keeps the soname but places its file elsewhere, so a set can hold
        # both providers: a path is owned by one artifact and a name by several
        rival = build(root, base / "tree2", "indexrival",
                      files={"usr/lib/rival/libindex.so.1": "the rival\n"},
                      provides="provide package indexrival noarch nolibc - metadata\n"
                               "provide soname libindex.so.1 noarch nolibc - metadata\n")
        app = build(root, base / "tree3", "indexapp", files={"usr/bin/indexapp": "the app\n"},
                    deps="require dep-1 indexapp package indexlib any any any - indexlib "
                         "metadata\n")
        out = install(root, app, lib)
        assert "requirement dep-1 consumer %s provider %s package indexlib" % (
            app, lib) in out, out
        # only the library the set chose is installed, so the index has one owner of
        # the path and one candidate for the soname
        text = call("index", "--root", root)
        assert "artifacts 2" in text, text
        assert "capability %s soname \"libindex.so.1\"" % lib in text, text
        assert "capability %s soname" % rival not in text, text

        # a path the index knows has one owner, named with its artifact
        found = call("index", "--root", root, "--path", "/usr/lib/libindex.so.1")
        assert 'owner %s "usr/lib/libindex.so.1"' % lib in found, found
        assert "candidates 1" in found, found
        # a leading slash is optional, since a manifest records the relative path
        assert call("index", "--root", root, "--path", "usr/lib/libindex.so.1") == found
        assert 'owner %s "usr/bin/indexapp"' % app in call(
            "index", "--root", root, "--path", "/usr/bin/indexapp")
        # a directory is shared, so it owns nothing
        result = subprocess.run([binary, "index", "--root", str(root), "--path", "/usr/lib"],
                                capture_output=True, text=True)
        assert result.returncode == 6, result
        assert "candidates 0" in result.stdout, result.stdout
        # a path no artifact owns is reported as unavailable
        result = subprocess.run([binary, "index", "--root", str(root), "--path", "/absent"],
                                capture_output=True, text=True)
        assert result.returncode == 6, result

        # a capability with one provider is a single candidate
        text = call("index", "--root", root, "--capability", "soname", "--name", "libindex.so.1")
        assert "candidate %s soname \"libindex.so.1\" arch \"noarch\" libc \"nolibc\"" % lib in text, text
        assert "candidates 1" in text, text
        # a capability no artifact provides is unavailable
        result = subprocess.run([binary, "index", "--root", str(root), "--capability", "soname",
                                 "--name", "absent.so.1"], capture_output=True, text=True)
        assert result.returncode == 6, result

        # a second provider of one SONAME names both candidates and chooses neither
        install(root, rival)
        text = call("index", "--root", root, "--capability", "soname", "--name", "libindex.so.1",
                    status=1)
        assert "candidate %s soname" % lib in text, text
        assert "candidate %s soname" % rival in text, text
        assert "candidates 2" in text, text
        # a path is owned by one artifact, so the rival's own path has one owner while
        # its soname has two candidates
        found = call("index", "--root", root, "--path", "/usr/lib/rival/libindex.so.1")
        assert "candidates 1" in found, found
        assert 'owner %s "usr/lib/rival/libindex.so.1"' % rival in found, found
        assert lib not in found, found

        # the conflict report and the index agree on what two artifacts both offer
        call("conflict", "--root", root, status=1)

        # the json form carries the same records
        events = records(call("index", "--root", root, "--capability", "soname", "--name",
                              "libindex.so.1", "--json", status=1))
        candidates = [event for event in events if event["type"] == "candidate"]
        assert len(candidates) == 2, events
        assert sorted(event["artifact"] for event in candidates) == sorted([lib, rival]), events
        for event in candidates:
            assert event["kind"] == "soname" and event["name"] == "libindex.so.1", event
            assert event["arch"] == "noarch" and event["libc"] == "nolibc", event
        assert events[-1]["type"] == "summary" and events[-1]["candidates"] == 2, events[-1]
        everything = records(call("index", "--root", root, "--json"))
        artifacts = [event for event in everything if event["type"] == "artifact"]
        assert len(artifacts) == 3, everything
        names = sorted(event["name"] for event in artifacts)
        assert names == ["indexapp", "indexlib", "indexrival"], names

        # an argument that names no subject, both subjects, or a relative path
        for args, wanted in ((["--path"], "usage: holypkg index"),
                             (["--path", "/a", "--capability", "soname", "--name", "b"],
                              "usage: holypkg index"),
                             (["--capability", "soname"], "usage: holypkg index"),
                             (["--name", "b"], "usage: holypkg index"),
                             (["--path", "/usr/../etc"], "usage: holypkg index"),
                             (["--path", "/"], "usage: holypkg index"),
                             (["--nonsense"], "usage: holypkg index")):
            result = subprocess.run([binary, "index", "--root", str(root), *args],
                                    capture_output=True, text=True)
            assert result.returncode == 2, (args, result)
            assert wanted in result.stderr, (args, result.stderr)
        assert (base / "empty").mkdir() or True
        result = subprocess.run([binary, "index", "--root", str(base / "empty")],
                                capture_output=True, text=True)
        assert result.returncode == 1, result
        assert "invalid-state" in result.stderr, result.stderr

    print("file index fixtures passed")
    return 0


if __name__ == "__main__":
    sys.exit(main())