_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
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())