_ _
| |_ ___| |_ _
| | . | | | |
|_|_|___|_|_ |
|___|
git mirror - github.com/owenewans/holy - branch master
file tests/split.py
#!/usr/bin/env python3
"""proposes the split outputs of a prepared package tree, and proves that a path no
rule settles is reported as a decision rather than assigned by a file extension."""
import pathlib
import shutil
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(sources, name, arch="x86_64"):
"""one prepared tree, as manifest generate and pack expect it"""
tree = sources / f"tree-{name}"
(tree / "HOLY").mkdir(parents=True)
(tree / "DATA").mkdir()
(tree / "HOLY/meta").write_text(
f"format holy-package-1\nname {name}\nversion 1\nrelease 1\nos linux\n"
f"arch {arch}\nlibc nolibc\n")
for field in ("deps", "provides", "hooks", "origin", "transform", "files"):
(tree / "HOLY" / field).write_text("")
return tree
def put(tree, relative, body, mode=0o644):
target = tree / "DATA" / relative
target.parent.mkdir(parents=True, exist_ok=True)
target.write_text(body)
target.chmod(mode)
def compiler():
for name in ("cc", "gcc", "clang"):
if shutil.which(name):
return name
return None
def compile_into(cc, arguments, output):
output.parent.mkdir(parents=True, exist_ok=True)
result = subprocess.run([cc, *map(str, arguments)], capture_output=True, text=True)
assert result.returncode == 0, (arguments, result.stdout, result.stderr)
return output
def read(path):
records = {}
for line in path.read_text().splitlines():
fields = line.split(" ", 2)
records.setdefault(fields[0], []).append(line)
return path.read_text()
def main():
cc = compiler()
if not cc:
print("a host compiler required for the split fixture", file=sys.stderr)
return 6
with tempfile.TemporaryDirectory() as scratch:
root = pathlib.Path(scratch)
sources = root / "sources"
sources.mkdir()
# the payload a real project installs: a program, a versioned object, an
# unversioned plugin, a static archive, headers, pkg-config metadata,
# documentation, a license and a payload link
tree = build(sources, "fixture")
main_source = sources / "main.c"
main_source.write_text("int main(void) { return 0; }\n")
library_source = sources / "lib.c"
library_source.write_text("int answer(void) { return 42; }\n")
program = compile_into(cc, ["-static", str(main_source), "-o",
str(tree / "DATA/usr/bin/fixture")],
tree / "DATA/usr/bin/fixture")
program.chmod(0o755)
library = compile_into(cc, ["-shared", "-fPIC", "-Wl,-soname,libfixture.so.1",
str(library_source), "-o",
str(tree / "DATA/usr/lib/libfixture.so.1")],
tree / "DATA/usr/lib/libfixture.so.1")
library.chmod(0o755)
plugin = compile_into(cc, ["-shared", "-fPIC", str(library_source), "-o",
str(tree / "DATA/usr/lib/pluginfixture.so")],
tree / "DATA/usr/lib/pluginfixture.so")
plugin.chmod(0o755)
(tree / "DATA/usr/lib/libfixture.so").symlink_to("libfixture.so.1")
object_file = compile_into(cc, ["-c", str(library_source), "-o",
str(sources / "lib.o")], sources / "lib.o")
archive = tree / "DATA/usr/lib/libfixture.a"
archive.parent.mkdir(parents=True, exist_ok=True)
result = subprocess.run(["ar", "rcs", str(archive), str(object_file)],
capture_output=True, text=True)
assert result.returncode == 0, result.stderr
put(tree, "usr/include/fixture.h", "int answer(void);\n")
put(tree, "usr/lib/pkgconfig/fixture.pc", "Name: fixture\nVersion: 1\n")
put(tree, "usr/share/man/man1/fixture.1", ".TH FIXTURE 1\n")
put(tree, "usr/share/doc/fixture/README", "the manual page fixture\n")
put(tree, "usr/share/licenses/fixture", "MIT\n")
# a license and a runtime data file are not documentation, however a general
# pattern would match them
proposal = root / "proposal"
report = call("split", tree, "--output", proposal, status=3)
assert "paths 10 runtime 4 devel 2 docs 2 debug 0 decisions 2" in report, report
body = proposal.read_text()
assert body.startswith('format holy-split-1\nname "fixture"\n'), body
# the runtime output keeps the program, the versioned object, the payload link
# and the license
for path, output, reason in (
("usr/bin/fixture", "fixture", "executable"),
("usr/lib/libfixture.so.1", "fixture", "versioned shared object"),
("usr/lib/libfixture.so", "fixture", "payload link"),
("usr/share/licenses/fixture", "fixture", "runtime payload")):
assert f'path "{path}" "{output}" "{reason}"' in body, (path, body)
# headers and pkg-config metadata are proposed for the development output
assert 'path "usr/include/fixture.h" "fixture-devel" "header"' in body, body
assert 'path "usr/lib/pkgconfig/fixture.pc" "fixture-devel" "pkg-config metadata"' \
in body, body
# man pages and reference documents are proposed for the documentation output
assert 'path "usr/share/man/man1/fixture.1" "fixture-doc" "documentation"' in body, body
assert 'path "usr/share/doc/fixture/README" "fixture-doc" "documentation"' in body, body
# an unversioned object and a static archive are decisions, not assignments:
# a file extension is not a sufficient criterion and a tree states no
# metadata or dlopen probe that would settle either
assert 'decision "usr/lib/pluginfixture.so" "unversioned-object" suggested "fixture"' \
in body, body
assert 'decision "usr/lib/libfixture.a" "static-archive" suggested "fixture-devel"' \
in body, body
assert "unversioned-object\n" in report and "static-archive\n" in report, report
# the outputs the proposal declares are the ones it assigns
assert 'output "fixture" runtime\n' in body, body
assert 'output "fixture-devel" devel\n' in body, body
assert 'output "fixture-doc" docs\n' in body, body
# an explicit rule outranks the heuristic, so the same tree proposes no
# decision once the operator states where the object and the archive belong
ruled = root / "ruled"
report = call("split", tree, "--output", ruled, "--split", "fixture",
"usr/lib/pluginfixture.so", "--split", "fixture-devel",
"usr/lib/libfixture.a")
assert "decisions 0" in report, report
body = ruled.read_text()
assert 'path "usr/lib/pluginfixture.so" "fixture" "explicit rule"' in body, body
assert 'path "usr/lib/libfixture.a" "fixture-devel" "explicit rule"' in body, body
assert "decision " not in body, body
# two rules naming different outputs for one path is a contradiction the
# operator has to settle, so the path is a decision rather than a guess
repeated = root / "repeated"
report = call("split", tree, "--output", repeated, "--split", "fixture",
"usr/lib/*", "--split", "fixture-devel", "usr/lib/*", status=3)
assert "decisions 5" in report, report
body = repeated.read_text()
assert 'decision "usr/lib/pluginfixture.so" "repeated"' in body, body
assert 'decision "usr/lib/libfixture.a" "repeated"' in body, body
# a link that leaves the tree it installs into is a decision, and so is an
# absolute one, because a payload carries neither
for target, name in (("../../../outside", "escape"), ("/usr/bin/escape", "absolute")):
link = tree / f"DATA/usr/bin/{name}"
link.symlink_to(target)
escaped = root / f"escaped-{name}"
report = call("split", tree, "--output", escaped, status=3)
assert f'decision "usr/bin/{name}" "escaping-link"' in escaped.read_text(), target
assert f"decision usr/bin/{name} escaping-link\n" in report, report
link.unlink()
# a rule may only name an output this proposal declares
unknown = root / "unknown"
result = subprocess.run([binary, "split", tree, "--output", str(unknown), "--split",
"elsewhere", "usr/lib/*"], capture_output=True, text=True)
assert result.returncode == 2, result
assert "undeclared output elsewhere" in result.stderr, result.stderr
# a tree without HOLY/meta, and a tree that is not there
bare = build(sources, "bare")
for field in ("deps", "provides", "hooks", "origin", "transform", "files"):
(bare / "HOLY" / field).write_text("")
(bare / "HOLY/meta").unlink()
result = subprocess.run([binary, "split", bare, "--output", str(root / "bare.txt")],
capture_output=True, text=True)
assert result.returncode == 2, result
assert "HOLY/meta" in result.stderr, result.stderr
result = subprocess.run([binary, "split", root / "absent", "--output",
str(root / "absent.txt")], capture_output=True, text=True)
assert result.returncode == 6, result
assert "prepared tree unavailable" in result.stderr, result.stderr
# a data-only tree proposes one runtime output and no development or
# documentation output, because a declared output that receives nothing is
# what a build rejects
data = build(sources, "dataonly")
put(data, "usr/share/fixture/data", "runtime data\n")
plain = root / "plain"
report = call("split", data, "--output", plain)
assert "paths 1 runtime 1 devel 0 docs 0 debug 0 decisions 0" in report, report
body = plain.read_text()
assert 'output "dataonly" runtime\n' in body, body
assert 'output "dataonly-devel"' not in body, body
assert 'output "dataonly-doc"' not in body, body
assert 'output "dataonly-debug"' not in body, body
# a debug output is cut from the runtime files by build-id, so a payload
# built with a note names the debug file and keeps the build-id the stripped
# artifact is matched against, and one without a note is a decision
program_source = sources / "program.c"
program_source.write_text("int main(void) { return 0; }\n")
debugged = build(sources, "debugged")
noted = compile_into(cc, ["-g", "-O0", "-Wl,--build-id", str(program_source), "-o",
str(debugged / "DATA/usr/bin/debugged")],
debugged / "DATA/usr/bin/debugged")
noted.chmod(0o755)
put(debugged, "usr/include/debugged.h", "int answer(void);\n")
proposal = root / "debugged"
report = call("split", debugged, "--output", proposal, "--debug")
assert "debug 1 decisions 0" in report, report
body = proposal.read_text()
assert 'output "debugged-debug" debug\n' in body, body
assert 'debug "usr/bin/debugged" "' in body, body
identity = call("elf", noted).splitlines()
build_id = [line.split()[1] for line in identity if line.startswith("build-id ")][0]
assert f'debug "usr/bin/debugged" "{build_id}"\n' in body, body
# the note is what a stripped artifact keeps, so the tool that cuts the pair
# is named rather than invented per file
assert "objcopy --only-keep-debug" in body, body
assert "--add-gnu-debuglink" in body, body
# the header is still development material, and the program is a runtime file
# the debug output is cut from
assert 'path "usr/include/debugged.h" "debugged-devel" "header"' in body, body
assert 'path "usr/bin/debugged" "debugged" "runtime artifact for a debug file"' \
in body, body
assert "no-build-id" not in body, body
# a payload without a build-id note cannot be tied to a debug file, so the
# path is a decision rather than an untraceable debug entry
unnoted = build(sources, "unnoted")
plain_program = compile_into(cc, ["-g", "-O0", str(program_source), "-o",
str(unnoted / "DATA/usr/bin/unnoted")],
unnoted / "DATA/usr/bin/unnoted")
plain_program.chmod(0o755)
proposal = root / "unnoted"
report = call("split", unnoted, "--output", proposal, "--debug", status=3)
assert "debug 0" in report, report
body = proposal.read_text()
assert 'decision "usr/bin/unnoted" "no-build-id" suggested "unnoted"' in body, body
assert "no build-id note ties a debug file" in body, body
assert "\ndebug " not in body, body
print("split proposal fixtures passed")
return 0
if __name__ == "__main__":
sys.exit(main())