|
@@ -408,6 +408,191 @@ namespace Usm.Tests {
|
|
|
"flat phase roundtrips as flat");
|
|
"flat phase roundtrips as flat");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // ---- Version constraints -----------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Version version(string text) throws Error {
|
|
|
|
|
+ return new Usm.Version.from_string(text);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_version_constraint_semantics() throws Error {
|
|
|
|
|
+ string parsed_base = null;
|
|
|
|
|
+ var constraint = Usm.VersionConstraint.find_in("libcmark.so>=0.30.0", out parsed_base);
|
|
|
|
|
+ check(constraint != null && parsed_base == "libcmark.so", "operator splits base from constraint");
|
|
|
|
|
+ check(constraint.to_string() == ">=0.30.0", "constraint round-trips");
|
|
|
|
|
+
|
|
|
|
|
+ var at_least = ((!)constraint);
|
|
|
|
|
+ check(at_least.satisfied_by(version("0.30.0")), ">= accepts the boundary");
|
|
|
|
|
+ check(at_least.satisfied_by(version("0.31.1")), ">= accepts newer");
|
|
|
|
|
+ check(at_least.satisfied_by(version("1.2.0")), ">= accepts major bumps");
|
|
|
|
|
+ check(!at_least.satisfied_by(version("0.29.9")), ">= rejects older");
|
|
|
|
|
+
|
|
|
|
|
+ string compatible_base = null;
|
|
|
|
|
+ var compatible = (!)Usm.VersionConstraint.find_in("libcmark.so~=0.31.0", out compatible_base);
|
|
|
|
|
+ check(compatible.satisfied_by(version("0.31.0")), "~= accepts the boundary");
|
|
|
|
|
+ check(compatible.satisfied_by(version("0.31.1")), "~= accepts patch bumps within the family");
|
|
|
|
|
+ check(!compatible.satisfied_by(version("0.32.0")), "~= rejects minor bumps");
|
|
|
|
|
+ check(!compatible.satisfied_by(version("1.0.0")), "~= rejects major bumps");
|
|
|
|
|
+
|
|
|
|
|
+ string wide_base = null;
|
|
|
|
|
+ var wide = (!)Usm.VersionConstraint.find_in("libcmark.so~=0.31", out wide_base);
|
|
|
|
|
+ check(wide.satisfied_by(version("0.33.0")), "two-component ~= pins only the major");
|
|
|
|
|
+ check(!wide.satisfied_by(version("1.0.0")), "two-component ~= still rejects major bumps");
|
|
|
|
|
+
|
|
|
|
|
+ string equals_base = null;
|
|
|
|
|
+ var equals = (!)Usm.VersionConstraint.find_in("libcmark.so==0.31", out equals_base);
|
|
|
|
|
+ check(equals.satisfied_by(version("0.31")), "== accepts the exact version");
|
|
|
|
|
+ check(equals.satisfied_by(version("0.31.0")), "== tolerates omitted trailing components");
|
|
|
|
|
+ check(equals.satisfied_by(version("0.31.5")), "== treats missing trailing components as unspecified");
|
|
|
|
|
+ check(!equals.satisfied_by(version("0.32.0")), "== rejects a differing minor");
|
|
|
|
|
+
|
|
|
|
|
+ string at_most_base = null;
|
|
|
|
|
+ var at_most = (!)Usm.VersionConstraint.find_in("libcmark.so<=0.31.1", out at_most_base);
|
|
|
|
|
+ check(at_most.satisfied_by(version("0.31.0")), "<= accepts older");
|
|
|
|
|
+ check(at_most.satisfied_by(version("0.31.1")), "<= accepts the boundary");
|
|
|
|
|
+ check(!at_most.satisfied_by(version("0.32.0")), "<= rejects newer");
|
|
|
|
|
+
|
|
|
|
|
+ check(Usm.VersionConstraint.find_in("libcmark.so", out parsed_base) == null && parsed_base == "libcmark.so",
|
|
|
|
|
+ "operator-free text carries no constraint");
|
|
|
|
|
+
|
|
|
|
|
+ var rejected = new Vector<string>();
|
|
|
|
|
+ foreach(var malformed in new string[] { "libcmark.so>=", ">=0.31.0", "libcmark.so>=0.31rc1", "libcmark.so~=0", "libcmark.so>=1<=2" }) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ Usm.VersionConstraint.find_in(malformed, out parsed_base);
|
|
|
|
|
+ rejected.add(malformed);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ check(rejected.count() == 0, @"malformed constraints are rejected ($(rejected.to_string(s => s, ", ")))");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_resource_ref_operator_parsing() throws Error {
|
|
|
|
|
+ var constrained = new Usm.ResourceRef("lib:libcmark.so.0>=0.30.0");
|
|
|
|
|
+ check(constrained.base_name == "libcmark.so.0", "base name drops the constraint");
|
|
|
|
|
+ check(constrained.resource == "libcmark.so.0>=0.30.0", "full resource keeps the constraint");
|
|
|
|
|
+ check(constrained.to_string() == "lib:libcmark.so.0>=0.30.0", "ref round-trips");
|
|
|
|
|
+
|
|
|
|
|
+ var plain = new Usm.ResourceRef("lib:libcmark.so.0");
|
|
|
|
|
+ check(plain.base_name == "libcmark.so.0" && plain.constraint == null, "plain ref carries no constraint");
|
|
|
|
|
+
|
|
|
|
|
+ var malformed = new Vector<string>();
|
|
|
|
|
+ foreach(var broken in new string[] { "lib", "lib:", "", ":" }) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ new Usm.ResourceRef(broken);
|
|
|
|
|
+ malformed.add(broken);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ check(malformed.count() == 0, @"structurally invalid refs are rejected ($(malformed.to_string(s => s, ", ")))");
|
|
|
|
|
+
|
|
|
|
|
+ var tail = Usm.ResourceRef.version_tail("libcmark.so", "libcmark.so.0.31.1");
|
|
|
|
|
+ check(tail != null && ((!)tail).to_string() == "0.31.1", "soname tail parses as a version");
|
|
|
|
|
+ check(Usm.ResourceRef.version_tail("libcmark.so", "libcmark.so") == null, "unversioned soname has no tail");
|
|
|
|
|
+ check(Usm.ResourceRef.version_tail("libcmark.so", "libcmark.so.0.31rc1") == null, "non-numeric tail is no version");
|
|
|
|
|
+ check(Usm.ResourceRef.version_tail("libcmark.so", "libcmark.so.0.31.1.2") != null, "four-component tails parse");
|
|
|
|
|
+ check(Usm.ResourceRef.version_tail("libcmark.so", "libothercmark.so.0.31.1") == null, "tails must extend the exact base");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_depends_operator_type_rules() throws Error {
|
|
|
|
|
+ var accepted = manifest_from_json(PHASE_MANIFEST.printf(
|
|
|
|
|
+ "{ \"runtime\": [\"lib:libcmark.so>=0.30.0\"], \"build\": [\"pc:libcmark.pc==0.31.1\"], \"manage\": [] }"));
|
|
|
|
|
+ check(accepted.dependencies.runtime.ordered_required().first().to_string() == "lib:libcmark.so>=0.30.0",
|
|
|
|
|
+ "lib dependencies accept operators");
|
|
|
|
|
+ check(accepted.dependencies.build.ordered_required().first().to_string() == "pc:libcmark.pc==0.31.1",
|
|
|
|
|
+ "pc dependencies accept operators");
|
|
|
|
|
+
|
|
|
|
|
+ var violations = new Vector<string>();
|
|
|
|
|
+ foreach(var rule in new string[] { "bin:python3>=3.8", "tag:family.feature>=1.0", "inc:glib-2.0>=2.0", "vapi:gtk-4.0.vapi~=4.0" }) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ manifest_from_json(PHASE_MANIFEST.printf(@"{ \"runtime\": [\"$rule\"], \"build\": [], \"manage\": [] }"));
|
|
|
|
|
+ violations.add(rule);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ check(violations.count() == 0, @"operators on non-lib/pc dependency types are rejected ($(violations.to_string(s => s, ", ")))");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_provides_declaration_rules() throws Error {
|
|
|
|
|
+ var declared = manifest_from_json("""
|
|
|
|
|
+ {
|
|
|
|
|
+ "name": "cmark-lib",
|
|
|
|
|
+ "version": "0.31.1",
|
|
|
|
|
+ "summary": "cmark",
|
|
|
|
|
+ "licences": [],
|
|
|
|
|
+ "flags": [],
|
|
|
|
|
+ "provides": { "pc:cmark.pc==0.31.1": "as-expected", "lib:libcmark.so.0.31.1": "as-expected" },
|
|
|
|
|
+ "depends": { "runtime": [], "build": [], "manage": [] },
|
|
|
|
|
+ "execs": {}
|
|
|
|
|
+ }
|
|
|
|
|
+ """);
|
|
|
|
|
+ check(declared.provides.count() == 2, "pc == declarations and concrete lib provides parse");
|
|
|
|
|
+
|
|
|
|
|
+ var violations = new Vector<string>();
|
|
|
|
|
+ foreach(var rule in new string[] {
|
|
|
|
|
+ "\"lib:libcmark.so.0.31.1==0.31.1\"",
|
|
|
|
|
+ "\"pc:cmark.pc>=0.31.1\"",
|
|
|
|
|
+ "\"bin:cmark==1.0\""
|
|
|
|
|
+ }) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ manifest_from_json(@"{ \"name\": \"x\", \"version\": \"1.0.0\", \"summary\": \"x\", \"licences\": [], \"flags\": [], \"provides\": { $rule: \"as-expected\" }, \"depends\": { \"runtime\": [], \"build\": [], \"manage\": [] }, \"execs\": {} }");
|
|
|
|
|
+ violations.add(rule);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ check(violations.count() == 0, @"only pc provides may carry == declarations ($(violations.to_string(s => s, ", ")))");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_satisfied_by_constraints() throws Error {
|
|
|
|
|
+ var floor = new Usm.ResourceRef("lib:libcmark.so>=0.30.0");
|
|
|
|
|
+ check(floor.satisfied_by(new Usm.ResourceRef("lib:libcmark.so.0.31.1")), "floor matches a newer soname tail");
|
|
|
|
|
+ check(floor.satisfied_by(new Usm.ResourceRef("lib:libcmark.so.0.30.0")), "floor matches the boundary tail");
|
|
|
|
|
+ check(!floor.satisfied_by(new Usm.ResourceRef("lib:libcmark.so.0.29.9")), "floor rejects an older tail");
|
|
|
|
|
+ check(!floor.satisfied_by(new Usm.ResourceRef("lib:libcmark.so")), "floor rejects an unversioned name");
|
|
|
|
|
+ check(!floor.satisfied_by(new Usm.ResourceRef("lib:libother.so.0.31.1")), "floor requires the same base name");
|
|
|
|
|
+ check(floor.satisfied_by(new Usm.ResourceRef("canonlib:libcmark.so.0.31.1")), "floor accepts a canonlib provider");
|
|
|
|
|
+ check(!floor.satisfied_by(new Usm.ResourceRef("pc:libcmark.pc")), "floor does not cross resource types");
|
|
|
|
|
+
|
|
|
|
|
+ var family = new Usm.ResourceRef("lib:libcmark.so~=0.31.0");
|
|
|
|
|
+ check(family.satisfied_by(new Usm.ResourceRef("lib:libcmark.so.0.31.1")), "family matches within the minor");
|
|
|
|
|
+ check(!family.satisfied_by(new Usm.ResourceRef("lib:libcmark.so.0.32.0")), "family rejects the next minor");
|
|
|
|
|
+
|
|
|
|
|
+ var plain = new Usm.ResourceRef("lib:libcmark.so.0.31.1");
|
|
|
|
|
+ check(plain.satisfied_by(new Usm.ResourceRef("lib:libcmark.so.0.31.1")), "plain dep matches the exact provide");
|
|
|
|
|
+ check(!plain.satisfied_by(new Usm.ResourceRef("lib:libcmark.so.0.30.0")), "plain dep still requires exactness");
|
|
|
|
|
+ check(plain.satisfied_by(new Usm.ResourceRef("canonlib:libcmark.so.0.31.1")), "plain dep accepts canonlib providers");
|
|
|
|
|
+
|
|
|
|
|
+ var plain_pc = new Usm.ResourceRef("pc:cmark.pc");
|
|
|
|
|
+ check(plain_pc.satisfied_by(new Usm.ResourceRef("pc:cmark.pc==0.31.1")), "plain pc dep matches a declared provide");
|
|
|
|
|
+ var declared_pc = new Usm.ResourceRef("pc:cmark.pc>=0.30.0");
|
|
|
|
|
+ check(declared_pc.satisfied_by(new Usm.ResourceRef("pc:cmark.pc==0.31.1")), "pc floor matches a satisfying declaration");
|
|
|
|
|
+ check(!declared_pc.satisfied_by(new Usm.ResourceRef("pc:cmark.pc==0.29.0")), "pc floor rejects a low declaration");
|
|
|
|
|
+ check(!declared_pc.satisfied_by(new Usm.ResourceRef("pc:cmark.pc")), "pc floor cannot be proven by an undeclared provide");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_manifest_satisfies_pc_fallback() throws Error {
|
|
|
|
|
+ var provider = manifest_from_json("""
|
|
|
|
|
+ {
|
|
|
|
|
+ "name": "cmark-lib",
|
|
|
|
|
+ "version": "0.31.1",
|
|
|
|
|
+ "summary": "cmark",
|
|
|
|
|
+ "licences": [],
|
|
|
|
|
+ "flags": [],
|
|
|
|
|
+ "provides": { "pc:cmark.pc": "as-expected", "pc:other.pc==0.5.0": "as-expected" },
|
|
|
|
|
+ "depends": { "runtime": [], "build": [], "manage": [] },
|
|
|
|
|
+ "execs": {}
|
|
|
|
|
+ }
|
|
|
|
|
+ """);
|
|
|
|
|
+ check(provider.satisfies(new Usm.ResourceRef("pc:cmark.pc>=0.30.0")), "undeclared pc provide falls back to the package version");
|
|
|
|
|
+ check(provider.satisfies(new Usm.ResourceRef("pc:cmark.pc==0.31.1")), "fallback version is the package version");
|
|
|
|
|
+ check(!provider.satisfies(new Usm.ResourceRef("pc:cmark.pc>=0.32.0")), "fallback respects the floor");
|
|
|
|
|
+ check(provider.satisfies(new Usm.ResourceRef("pc:other.pc>=0.5.0")), "explicit declaration satisfies a floor");
|
|
|
|
|
+ check(!provider.satisfies(new Usm.ResourceRef("pc:other.pc>=0.6.0")), "explicit declaration wins over the package version");
|
|
|
|
|
+ check(!provider.satisfies(new Usm.ResourceRef("pc:absent.pc>=0.1")), "unrelated names never match");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ---- Topological ordering ----------------------------------------------------
|
|
// ---- Topological ordering ----------------------------------------------------
|
|
|
|
|
|
|
|
/** A package manifest named pkg-<name> providing bin:pkg-<name> with the given runtime refs. */
|
|
/** A package manifest named pkg-<name> providing bin:pkg-<name> with the given runtime refs. */
|
|
@@ -493,16 +678,43 @@ for package in known:
|
|
|
print(json.dumps({"not-found": [r for r in refs if r not in provided], "packages": known}))
|
|
print(json.dumps({"not-found": [r for r in refs if r not in provided], "packages": known}))
|
|
|
PY
|
|
PY
|
|
|
;;
|
|
;;
|
|
|
|
|
+ plan)
|
|
|
|
|
+ # Every material package: dependencies first, chosen names after
|
|
|
|
|
+ printf '{"packages":['
|
|
|
|
|
+ first=1
|
|
|
|
|
+ for name in "$@"; do
|
|
|
|
|
+ if [ $first -eq 0 ]; then printf ','; fi
|
|
|
|
|
+ first=0
|
|
|
|
|
+ printf '"dep-of-%s"' "$name"
|
|
|
|
|
+ done
|
|
|
|
|
+ for name in "$@"; do
|
|
|
|
|
+ printf ',"%s"' "$name"
|
|
|
|
|
+ done
|
|
|
|
|
+ printf ']}\n'
|
|
|
|
|
+ ;;
|
|
|
install)
|
|
install)
|
|
|
total=$#
|
|
total=$#
|
|
|
echo "{\"type\":\"begin\",\"total\":$total}"
|
|
echo "{\"type\":\"begin\",\"total\":$total}"
|
|
|
- n=0
|
|
|
|
|
|
|
+ # Like dnf: a download phase streaming package events in its own
|
|
|
|
|
+ # order, then an rpm transaction completing in a DIFFERENT order
|
|
|
|
|
+ # than the plan's — the display must not depend on either matching
|
|
|
for name in "$@"; do
|
|
for name in "$@"; do
|
|
|
- n=$((n+1))
|
|
|
|
|
- echo "$name" >> "$USM_SPM_INSTALL_LOG"
|
|
|
|
|
- echo "{\"type\":\"package\",\"name\":\"$name\",\"current\":$n,\"total\":$total,\"progress\":0.5}"
|
|
|
|
|
|
|
+ echo "{\"type\":\"package\",\"name\":\"$name\",\"current\":$RANDOM,\"total\":999,\"progress\":0.4}"
|
|
|
|
|
+ done
|
|
|
|
|
+ for name in $(printf '%s\n' "$@" | tac); do
|
|
|
|
|
+ echo "{\"type\":\"package\",\"name\":\"dep-of-$name\",\"current\":$RANDOM,\"total\":999,\"progress\":0.5}"
|
|
|
|
|
+ done
|
|
|
|
|
+ n=0
|
|
|
|
|
+ for name in $(printf '%s\n' "$@" | tac); do
|
|
|
|
|
+ echo "{\"type\":\"package\",\"name\":\"dep-of-$name\",\"current\":$n,\"total\":999,\"progress\":0.9}"
|
|
|
|
|
+ echo "{\"type\":\"package-complete\",\"name\":\"dep-of-$name\"}"
|
|
|
|
|
+ echo "{\"type\":\"package\",\"name\":\"$name\",\"current\":$n,\"total\":999,\"progress\":0.95}"
|
|
|
echo "{\"type\":\"package-complete\",\"name\":\"$name\"}"
|
|
echo "{\"type\":\"package-complete\",\"name\":\"$name\"}"
|
|
|
|
|
+ echo "$name" >> "$USM_SPM_INSTALL_LOG"
|
|
|
|
|
+ n=$((n+1))
|
|
|
done
|
|
done
|
|
|
|
|
+ # A duplicate completion, as dnf emits for repeated members
|
|
|
|
|
+ echo "{\"type\":\"package-complete\",\"name\":\"$1\"}"
|
|
|
echo "{\"type\":\"complete\",\"status\":\"ok\",\"installed\":$total}"
|
|
echo "{\"type\":\"complete\",\"status\":\"ok\",\"installed\":$total}"
|
|
|
;;
|
|
;;
|
|
|
*)
|
|
*)
|
|
@@ -650,6 +862,144 @@ esac
|
|
|
Usm.Util.delete_tree(scratch);
|
|
Usm.Util.delete_tree(scratch);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /** Builds a .usmc archive from a complete manifest JSON string. */
|
|
|
|
|
+ string make_archive_from_manifest(string scratch, string manifest_json) throws Error {
|
|
|
|
|
+ var source = Path.build_filename(scratch, @"src-$(Uuid.string_random())");
|
|
|
|
|
+ DirUtils.create(source, 0755);
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"), manifest_json);
|
|
|
|
|
+ var archive = Path.build_filename(scratch, @"pkg-$(Uuid.string_random()).usmc");
|
|
|
|
|
+ Usm.Util.archive(source, archive);
|
|
|
|
|
+ return archive;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_resolver_constrained_lib_dep() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var resolver = new Usm.Resolver(new Usm.ResourceFinder());
|
|
|
|
|
+ resolver.supply_package(make_archive_from_manifest(scratch, """
|
|
|
|
|
+ {
|
|
|
|
|
+ "name": "usmt-cmark",
|
|
|
|
|
+ "version": "0.31.1",
|
|
|
|
|
+ "summary": "constrained provider",
|
|
|
|
|
+ "licences": [],
|
|
|
|
|
+ "flags": [],
|
|
|
|
|
+ "provides": { "lib:libusmt-cmark.so.0.31.1": "as-expected" },
|
|
|
|
|
+ "depends": { "runtime": [], "build": [], "manage": [] },
|
|
|
|
|
+ "execs": {}
|
|
|
|
|
+ }
|
|
|
|
|
+ """));
|
|
|
|
|
+
|
|
|
|
|
+ var roots = new Vector<Usm.AbstractPackage>();
|
|
|
|
|
+ roots.add(new Usm.AbstractPackage.from_manifest(manifest_from_json(
|
|
|
|
|
+ APP_MANIFEST.printf("cmark-app", "cmark-app", depends_with("runtime", "[\"lib:libusmt-cmark.so>=0.30.0\"]")))));
|
|
|
|
|
+ var result = resolver.resolve(roots, null);
|
|
|
|
|
+
|
|
|
|
|
+ check(result.packages.count() == 2, "a constrained lib dep pulls its USM provider");
|
|
|
|
|
+ check(result.install_order.to_string(p => p.manifest.name, ",") == "usmt-cmark,cmark-app",
|
|
|
|
|
+ "the provider installs before the dependent");
|
|
|
|
|
+ check(result.system_packages.length == 0, "no system packages without an SPM");
|
|
|
|
|
+
|
|
|
|
|
+ // The same provider does not satisfy a family constraint it breaks
|
|
|
|
|
+ var roots_mismatch = new Vector<Usm.AbstractPackage>();
|
|
|
|
|
+ roots_mismatch.add(new Usm.AbstractPackage.from_manifest(manifest_from_json(
|
|
|
|
|
+ APP_MANIFEST.printf("cmark-app2", "cmark-app2", depends_with("runtime", "[\"lib:libusmt-cmark.so~=0.30.0\"]")))));
|
|
|
|
|
+ try {
|
|
|
|
|
+ resolver.resolve(roots_mismatch, null);
|
|
|
|
|
+ check(false, "an unsatisfiable family constraint fails resolution");
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ check(e is Usm.ResolverError, "an unsatisfiable family constraint fails resolution");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_resolver_skips_spm_for_constrained_refs() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var resolver = new Usm.Resolver(new Usm.ResourceFinder());
|
|
|
|
|
+ resolver.supply_package(make_archive_from_manifest(scratch, """
|
|
|
|
|
+ {
|
|
|
|
|
+ "name": "usmt-skip",
|
|
|
|
|
+ "version": "1.0.0",
|
|
|
|
|
+ "summary": "skip provider",
|
|
|
|
|
+ "licences": [],
|
|
|
|
|
+ "flags": [],
|
|
|
|
|
+ "provides": { "lib:libusmt-skip.so.2.1.0": "as-expected" },
|
|
|
|
|
+ "depends": { "runtime": [], "build": [], "manage": [] },
|
|
|
|
|
+ "execs": {}
|
|
|
|
|
+ }
|
|
|
|
|
+ """));
|
|
|
|
|
+
|
|
|
|
|
+ var roots = new Vector<Usm.AbstractPackage>();
|
|
|
|
|
+ roots.add(new Usm.AbstractPackage.from_manifest(manifest_from_json(
|
|
|
|
|
+ APP_MANIFEST.printf("skip-app", "skip-app",
|
|
|
|
|
+ depends_with("runtime", "[\"lib:libusmt-skip.so>=2.0.0\", \"bin:usmt-plain\"]")))));
|
|
|
|
|
+
|
|
|
|
|
+ var spm = stub_manager(scratch, spm_candidate("spm-owns-plain", "bin:usmt-plain", 2, 1));
|
|
|
|
|
+ var result = resolver.resolve(roots, spm);
|
|
|
|
|
+
|
|
|
|
|
+ check(result.packages.count() == 2, "the constrained dep resolves from the USM provider");
|
|
|
|
|
+ check(result.system_packages.length == 1 && result.system_packages[0].name == "spm-owns-plain",
|
|
|
|
|
+ "the plain dep still resolves from the SPM");
|
|
|
|
|
+
|
|
|
|
|
+ var log = "";
|
|
|
|
|
+ FileUtils.get_contents(Path.build_filename(scratch, "query.log"), out log);
|
|
|
|
|
+ check(log.contains("usmt-skip.so>=") == false, "the SPM is never queried for the constrained ref");
|
|
|
|
|
+ check(log.contains("bin:usmt-plain"), "the SPM is queried for plain refs in the same batch");
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_pc_finder_version_constraint() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var pcdir = Path.build_filename(scratch, "pkgconfig");
|
|
|
|
|
+ DirUtils.create(pcdir, 0755);
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(pcdir, "usmt.pc"),
|
|
|
|
|
+ "prefix=/usr\n\nName: usmt\nDescription: test\nVersion: 1.2.3\nCflags: -I/usr/include\n");
|
|
|
|
|
+
|
|
|
|
|
+ var previous = Environment.get_variable("PKG_CONFIG_PATH");
|
|
|
|
|
+ Environment.set_variable("PKG_CONFIG_PATH", pcdir, true);
|
|
|
|
|
+ var finder = new Usm.ResourceFinder();
|
|
|
|
|
+ try {
|
|
|
|
|
+ check(finder.has_resource(new Usm.ResourceRef("pc:usmt.pc")), "plain pc ref found by existence");
|
|
|
|
|
+ check(finder.has_resource(new Usm.ResourceRef("pc:usmt.pc>=1.0.0")), "floor below the declared version satisfies");
|
|
|
|
|
+ check(finder.has_resource(new Usm.ResourceRef("pc:usmt.pc==1.2.3")), "== the declared version satisfies");
|
|
|
|
|
+ check(finder.has_resource(new Usm.ResourceRef("pc:usmt.pc~=1.2.0")), "~= within the declared minor satisfies");
|
|
|
|
|
+ check(!finder.has_resource(new Usm.ResourceRef("pc:usmt.pc>=2.0")), "floor above the declared version fails");
|
|
|
|
|
+ check(!finder.has_resource(new Usm.ResourceRef("pc:usmt.pc~=1.3.0")), "~= outside the declared minor fails");
|
|
|
|
|
+ check(!finder.has_resource(new Usm.ResourceRef("pc:usmt-absent.pc>=1.0")), "absent pc files fail");
|
|
|
|
|
+ }
|
|
|
|
|
+ finally {
|
|
|
|
|
+ if(previous != null) {
|
|
|
|
|
+ Environment.set_variable("PKG_CONFIG_PATH", previous, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ Environment.unset_variable("PKG_CONFIG_PATH");
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_pc_file_read_version() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var path = Path.build_filename(scratch, "a.pc");
|
|
|
|
|
+
|
|
|
|
|
+ FileUtils.set_contents(path, "# comment\nName: a\n Version: 2.1.0 \nCflags: -I${prefix}/include\n");
|
|
|
|
|
+ var parsed = Usm.PcFile.read_version(path);
|
|
|
|
|
+ check(parsed != null && ((!)parsed).to_string() == "2.1.0", "version keyword parsed with surrounding whitespace");
|
|
|
|
|
+
|
|
|
|
|
+ FileUtils.set_contents(path, "prefix=/usr\nName: a\nVersion: ${pcfiledir}/ver\n");
|
|
|
|
|
+ check(Usm.PcFile.read_version(path) == null, "variable-expanding version yields null");
|
|
|
|
|
+
|
|
|
|
|
+ FileUtils.set_contents(path, "Name: a\nDescription: no version here\n");
|
|
|
|
|
+ check(Usm.PcFile.read_version(path) == null, "missing version keyword yields null");
|
|
|
|
|
+
|
|
|
|
|
+ FileUtils.set_contents(path, "Name: a\nVersion: 1.0rc1\n");
|
|
|
|
|
+ check(Usm.PcFile.read_version(path) == null, "non-numeric version yields null");
|
|
|
|
|
+
|
|
|
|
|
+ check(Usm.PcFile.read_version(Path.build_filename(scratch, "absent.pc")) == null, "unreadable file yields null");
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
void test_group_selection_cheaper_wins() throws Error {
|
|
void test_group_selection_cheaper_wins() throws Error {
|
|
|
var scratch = make_scratch();
|
|
var scratch = make_scratch();
|
|
|
var spm = stub_manager(scratch,
|
|
var spm = stub_manager(scratch,
|
|
@@ -763,7 +1113,7 @@ esac
|
|
|
|
|
|
|
|
string install_log;
|
|
string install_log;
|
|
|
FileUtils.get_contents(Path.build_filename(scratch, "install.log"), out install_log);
|
|
FileUtils.get_contents(Path.build_filename(scratch, "install.log"), out install_log);
|
|
|
- check(install_log.split("\n").length == 3 && install_log.has_prefix("one\ntwo"), "install helper received the package names in order");
|
|
|
|
|
|
|
+ check(install_log.split("\n").length == 3 && install_log.contains("one") && install_log.contains("two"), "install helper received both package names");
|
|
|
|
|
|
|
|
Usm.Util.delete_tree(scratch);
|
|
Usm.Util.delete_tree(scratch);
|
|
|
}
|
|
}
|
|
@@ -892,6 +1242,154 @@ esac
|
|
|
Usm.Util.delete_tree(scratch);
|
|
Usm.Util.delete_tree(scratch);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ void test_transaction_preinstalled_resources_satisfy_build_deps() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ // A package whose build phase needs something nothing on this
|
|
|
|
|
+ // machine and nothing in the transaction provides — the exact
|
|
|
|
|
+ // shape of a system-package-provided toolchain dependency in a
|
|
|
|
|
+ // freshly provisioned container
|
|
|
|
|
+ make_build_package_archive(scratch, "s", "pc:lib-lot-s.pc", { "pc:no-such-file-anywhere-7f3.pc" });
|
|
|
|
|
+ var to_install = new HashSet<Usm.CachedPackage>();
|
|
|
|
|
+ to_install.add(cache_package(scratch, "s"));
|
|
|
|
|
+
|
|
|
|
|
+ var blocked = new Usm.Transaction() {
|
|
|
|
|
+ paths = new Usm.Paths(),
|
|
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
|
|
+ to_install = to_install,
|
|
|
|
|
+ to_remove = new HashSet<Usm.CachedPackage>()
|
|
|
|
|
+ };
|
|
|
|
|
+ try {
|
|
|
|
|
+ blocked.strategise();
|
|
|
|
|
+ check(false, "a build ref nothing provides still blocks the transaction");
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Usm.TransactionError e) {
|
|
|
|
|
+ check(e.message.contains("pkg-s") && e.message.contains("pc:no-such-file-anywhere-7f3.pc"),
|
|
|
|
|
+ "the failure names the package and the unmet ref");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var preinstalled = new HashSet<Usm.ResourceRef>();
|
|
|
|
|
+ preinstalled.add(new Usm.ResourceRef("pc:no-such-file-anywhere-7f3.pc"));
|
|
|
|
|
+ var provisioned = new Usm.Transaction() {
|
|
|
|
|
+ paths = new Usm.Paths(),
|
|
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
|
|
+ preinstalled_resources = preinstalled,
|
|
|
|
|
+ to_install = to_install,
|
|
|
|
|
+ to_remove = new HashSet<Usm.CachedPackage>()
|
|
|
|
|
+ };
|
|
|
|
|
+ provisioned.strategise();
|
|
|
|
|
+
|
|
|
|
|
+ check(provisioned.install_lots.length == 1, "preinstalled (system-provided) resources satisfy build-phase refs");
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_transaction_deadlock_message_details_causes() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ // Two packages whose build phases need each other's provides —
|
|
|
|
|
+ // unschedulable under the earlier-lot rule for build-phase refs
|
|
|
|
|
+ make_build_package_archive(scratch, "x", "pc:lib-cyc-x.pc", { "pc:lib-cyc-y.pc" });
|
|
|
|
|
+ make_build_package_archive(scratch, "y", "pc:lib-cyc-y.pc", { "pc:lib-cyc-x.pc" });
|
|
|
|
|
+ make_build_package_archive(scratch, "z", "pc:lib-cyc-z.pc", { "pc:also-nowhere-at-all-2c9.pc" });
|
|
|
|
|
+
|
|
|
|
|
+ var to_install = new HashSet<Usm.CachedPackage>();
|
|
|
|
|
+ foreach(var name in new string[] { "x", "y", "z" }) {
|
|
|
|
|
+ to_install.add(cache_package(scratch, name));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var transaction = new Usm.Transaction() {
|
|
|
|
|
+ paths = new Usm.Paths(),
|
|
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
|
|
+ to_install = to_install,
|
|
|
|
|
+ to_remove = new HashSet<Usm.CachedPackage>()
|
|
|
|
|
+ };
|
|
|
|
|
+ try {
|
|
|
|
|
+ transaction.strategise();
|
|
|
|
|
+ check(false, "the mutual build dependency cannot be scheduled");
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Usm.TransactionError e) {
|
|
|
|
|
+ var message = e.message;
|
|
|
|
|
+ check(message.contains("3 package(s) could not be scheduled"), "the headline counts the stuck packages");
|
|
|
|
|
+ check(message.contains("pkg-x") && message.contains("pkg-y") && message.contains("pkg-z"), "every stuck package is named");
|
|
|
|
|
+ check(message.contains("pc:lib-cyc-y.pc (build phase) is part of a dependency cycle") && message.contains("pkg-y"),
|
|
|
|
|
+ "the cyclic ref names the ref, its phase and its stuck provider");
|
|
|
|
|
+ check(message.contains("pc:also-nowhere-at-all-2c9.pc (build phase) is unmet"), "the unmet ref names the ref and its phase");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_transaction_installs_system_packages() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var spm = stub_manager(scratch, "");
|
|
|
|
|
+
|
|
|
|
|
+ var candidates = new Vector<Usm.SystemPackageCandidate>();
|
|
|
|
|
+ foreach(var name in new string[] { "one", "two" }) {
|
|
|
|
|
+ var candidate = new Usm.SystemPackageCandidate();
|
|
|
|
|
+ candidate.name = name;
|
|
|
|
|
+ candidate.resources = new Vector<Usm.ResourceRef>();
|
|
|
|
|
+ candidates.add(candidate);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // The material set the stub's plan mode reports: dependencies
|
|
|
|
|
+ // first, chosen names after
|
|
|
|
|
+ var chosen_names = new Vector<string>();
|
|
|
|
|
+ chosen_names.add("one");
|
|
|
|
|
+ chosen_names.add("two");
|
|
|
|
|
+ var plan = spm.plan_install(chosen_names);
|
|
|
|
|
+ check(plan.length == 4 && plan[0] == "dep-of-one" && plan[3] == "two", "plan_install returns the material set in helper order");
|
|
|
|
|
+
|
|
|
|
|
+ var paths = new Usm.Paths.defaults();
|
|
|
|
|
+ paths.destination = scratch;
|
|
|
|
|
+
|
|
|
|
|
+ var transaction = new Usm.Transaction() {
|
|
|
|
|
+ paths = paths,
|
|
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
|
|
+ system_package_manager = spm,
|
|
|
|
|
+ system_packages = candidates,
|
|
|
|
|
+ system_install_plan = plan,
|
|
|
|
|
+ to_install = new HashSet<Usm.CachedPackage>(),
|
|
|
|
|
+ to_remove = new HashSet<Usm.CachedPackage>()
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ var events = new Vector<string>();
|
|
|
|
|
+ uint last_total = 0;
|
|
|
|
|
+ transaction.progress_updated.connect((task, subject, index, total, progress) => {
|
|
|
|
|
+ var kind = task == TransactionTask.INSTALLING_SYSTEM ? "sys" : task == TransactionTask.STRATEGISING ? "plan" : "other";
|
|
|
|
|
+ events.add(@"$kind|$(subject ?? "")|$index|$total");
|
|
|
|
|
+ last_total = total;
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ transaction.strategise();
|
|
|
|
|
+ check(last_total == 5, "the task count covers every material system package");
|
|
|
|
|
+
|
|
|
|
|
+ transaction.run();
|
|
|
|
|
+
|
|
|
|
|
+ // Completions claim slots IN ARRIVAL ORDER: the stub completes
|
|
|
|
|
+ // the plan set in a different order than the plan lists it, and
|
|
|
|
|
+ // its download events stream in yet another order with nonsense
|
|
|
|
|
+ // positions — the actions must follow the real install sequence
|
|
|
|
|
+ check(events.any(e => e.has_prefix("sys|dep-of-two|1|5")), "the first completion claims the first slot");
|
|
|
|
|
+ check(events.any(e => e.has_prefix("sys|two|2|5")), "the second completion claims the second slot");
|
|
|
|
|
+ check(events.any(e => e.has_prefix("sys|dep-of-one|3|5")), "the third completion claims the third slot");
|
|
|
|
|
+ check(events.any(e => e.has_prefix("sys|one|4|5")), "the fourth completion claims the fourth slot");
|
|
|
|
|
+ check(!events.any(e => e.has_prefix("other|")), "no USM action runs when the transaction holds only system packages");
|
|
|
|
|
+
|
|
|
|
|
+ string install_log;
|
|
|
|
|
+ FileUtils.get_contents(Path.build_filename(scratch, "install.log"), out install_log);
|
|
|
|
|
+ check(install_log.split("\n").length == 3 && install_log.contains("one") && install_log.contains("two"), "the native helper received both chosen names, dependencies excluded");
|
|
|
|
|
+
|
|
|
|
|
+ var summary = new Usm.TransactionSummary() {
|
|
|
|
|
+ system_installs = candidates,
|
|
|
|
|
+ system_dependencies = plan.where(n => n != "one" && n != "two").to_vector()
|
|
|
|
|
+ };
|
|
|
|
|
+ var plan_preview = summary.describe(false);
|
|
|
|
|
+ check(plan_preview.contains("install one (system package)") && plan_preview.contains("install two (system package)"), "the plan preview lists the chosen system package installs");
|
|
|
|
|
+ check(plan_preview.contains("install dep-of-one (system dependency)") && plan_preview.contains("install dep-of-two (system dependency)"), "the plan preview lists the material dependencies");
|
|
|
|
|
+ check(!summary.is_empty(), "system installs alone are a non-empty plan");
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
// ---- rebuildDependants: lookup, planning, dedup ------------------------------
|
|
// ---- rebuildDependants: lookup, planning, dedup ------------------------------
|
|
|
|
|
|
|
|
/** Manifest for a package named <name> at <version> providing one key with the given flags and depends. */
|
|
/** Manifest for a package named <name> at <version> providing one key with the given flags and depends. */
|
|
@@ -1743,8 +2241,18 @@ echo "Installing libmeson-pkg.so to /usr/lib"
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
try {
|
|
try {
|
|
|
|
|
+ test_version_constraint_semantics();
|
|
|
|
|
+ test_resource_ref_operator_parsing();
|
|
|
|
|
+ test_depends_operator_type_rules();
|
|
|
|
|
+ test_provides_declaration_rules();
|
|
|
|
|
+ test_satisfied_by_constraints();
|
|
|
|
|
+ test_manifest_satisfies_pc_fallback();
|
|
|
test_resolve_diamond_usm_only();
|
|
test_resolve_diamond_usm_only();
|
|
|
test_resolve_spm_before_usm();
|
|
test_resolve_spm_before_usm();
|
|
|
|
|
+ test_resolver_constrained_lib_dep();
|
|
|
|
|
+ test_resolver_skips_spm_for_constrained_refs();
|
|
|
|
|
+ test_pc_finder_version_constraint();
|
|
|
|
|
+ test_pc_file_read_version();
|
|
|
test_group_selection_cheaper_wins();
|
|
test_group_selection_cheaper_wins();
|
|
|
test_group_selection_tie_manifest_order();
|
|
test_group_selection_tie_manifest_order();
|
|
|
test_group_selection_none_viable_itemised();
|
|
test_group_selection_none_viable_itemised();
|
|
@@ -1753,6 +2261,9 @@ echo "Installing libmeson-pkg.so to /usr/lib"
|
|
|
test_transaction_accepts_orders();
|
|
test_transaction_accepts_orders();
|
|
|
test_transaction_lots_split_at_build_boundaries();
|
|
test_transaction_lots_split_at_build_boundaries();
|
|
|
test_transaction_manage_deps_admit_same_lot();
|
|
test_transaction_manage_deps_admit_same_lot();
|
|
|
|
|
+ test_transaction_preinstalled_resources_satisfy_build_deps();
|
|
|
|
|
+ test_transaction_deadlock_message_details_causes();
|
|
|
|
|
+ test_transaction_installs_system_packages();
|
|
|
}
|
|
}
|
|
|
catch(Error e) {
|
|
catch(Error e) {
|
|
|
failures++;
|
|
failures++;
|