|
@@ -713,6 +713,20 @@ namespace Usm.Tests {
|
|
|
check(rejected.count() == 0, @"malformed constraints are rejected ($(rejected.to_string(s => s, ", ")))");
|
|
check(rejected.count() == 0, @"malformed constraints are rejected ($(rejected.to_string(s => s, ", ")))");
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ void test_version_ordering() throws Error {
|
|
|
|
|
+ check(version("0.1.1").greater_than(version("0.1")), "extra non-zero component is newer (0.1.1 > 0.1)");
|
|
|
|
|
+ check(version("0.1").less_than(version("0.1.1")), "fewer components is older (0.1 < 0.1.1)");
|
|
|
|
|
+ check(version("0.1.0").compare(version("0.1")) == 0, "trailing zero components stay equal (0.1 == 0.1.0)");
|
|
|
|
|
+ check(version("0.1").compare(version("0.1.0")) == 0, "trailing zero components stay equal both ways (0.1.0 == 0.1)");
|
|
|
|
|
+ check(!version("0.1").greater_than(version("0.1.0")), "trailing zero components do not order (0.1 !> 0.1.0)");
|
|
|
|
|
+ check(version("1.0.0.1").greater_than(version("1.0")), "extra component wins at any depth");
|
|
|
|
|
+ check(version("0.2").greater_than(version("0.1.1")), "shared-prefix difference dominates component count");
|
|
|
|
|
+ check(version("0.1+1").greater_than(version("0.1")), "release suffix still beats release-less");
|
|
|
|
|
+ check(version("0.1.1+1").greater_than(version("0.1.1")), "release breaks numeric ties");
|
|
|
|
|
+ check(version("0.1.1").greater_than(version("0.1+5")), "non-zero component beats any release of the shorter version");
|
|
|
|
|
+ check(version("0.1").compare(version("0.1")) == 0, "identical versions compare equal");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
void test_resource_ref_operator_parsing() throws Error {
|
|
void test_resource_ref_operator_parsing() throws Error {
|
|
|
var constrained = new Usm.ResourceRef("lib:libcmark.so.0>=0.30.0");
|
|
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.base_name == "libcmark.so.0", "base name drops the constraint");
|
|
@@ -926,6 +940,27 @@ print(json.dumps({"not-found": [r for r in refs if r not in provided], "packages
|
|
|
PY
|
|
PY
|
|
|
;;
|
|
;;
|
|
|
plan)
|
|
plan)
|
|
|
|
|
+ # Optional exclusivity pair (USM_SPM_PLAN_CONFLICT_PAIR="a b"):
|
|
|
|
|
+ # when BOTH names appear in the requested set, fail the way the apk
|
|
|
|
|
+ # solver fails a dropbear+openssh mix — exit 3 (resolve) with an
|
|
|
|
|
+ # "unable to select packages" diagnostic. Provider-selection
|
|
|
|
|
+ # fixtures use this to model conflicts the query metadata never
|
|
|
|
|
+ # reported.
|
|
|
|
|
+ if [ -n "$USM_SPM_PLAN_CONFLICT_PAIR" ]; then
|
|
|
|
|
+ pair_a="${USM_SPM_PLAN_CONFLICT_PAIR%% *}"
|
|
|
|
|
+ pair_b="${USM_SPM_PLAN_CONFLICT_PAIR##* }"
|
|
|
|
|
+ have_a=0
|
|
|
|
|
+ have_b=0
|
|
|
|
|
+ for name in "$@"; do
|
|
|
|
|
+ if [ "$name" = "$pair_a" ]; then have_a=1; fi
|
|
|
|
|
+ if [ "$name" = "$pair_b" ]; then have_b=1; fi
|
|
|
|
|
+ done
|
|
|
|
|
+ if [ $have_a -eq 1 ] && [ $have_b -eq 1 ]; then
|
|
|
|
|
+ echo "ERROR: unable to select packages:" >&2
|
|
|
|
|
+ echo " $pair_a: conflicts: $pair_b" >&2
|
|
|
|
|
+ exit 3
|
|
|
|
|
+ fi
|
|
|
|
|
+ fi
|
|
|
# Every material package: dependencies first, chosen names after
|
|
# Every material package: dependencies first, chosen names after
|
|
|
printf '{"packages":['
|
|
printf '{"packages":['
|
|
|
first=1
|
|
first=1
|
|
@@ -1022,6 +1057,31 @@ esac
|
|
|
return archive;
|
|
return archive;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Builds a .usmc archive for pkg-<name> at an explicit version
|
|
|
|
|
+ * providing bin:pkg-<name> with the given runtime refs (used for
|
|
|
|
|
+ * provider-selection fixtures).
|
|
|
|
|
+ */
|
|
|
|
|
+ string make_versioned_package_archive(string scratch, string name, string version, string[] runtime_refs) throws Error {
|
|
|
|
|
+ var source = Path.build_filename(scratch, @"src-$name-$version");
|
|
|
|
|
+ DirUtils.create(source, 0755);
|
|
|
|
|
+
|
|
|
|
|
+ var refs = new StringBuilder();
|
|
|
|
|
+ foreach(var resource_ref in runtime_refs) {
|
|
|
|
|
+ if(refs.len > 0) {
|
|
|
|
|
+ refs.append(",");
|
|
|
|
|
+ }
|
|
|
|
|
+ refs.append_printf("\"%s\"", resource_ref);
|
|
|
|
|
+ }
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"),
|
|
|
|
|
+ APP_MANIFEST.printf(@"pkg-$name", @"pkg-$name", depends_with("runtime", "[" + refs.str + "]"))
|
|
|
|
|
+ .replace("\"version\": \"1.0.0\"", @"\"version\": \"$version\""));
|
|
|
|
|
+
|
|
|
|
|
+ var archive = Path.build_filename(scratch, @"pkg-$name-$version.usmc");
|
|
|
|
|
+ Usm.Util.archive(source, archive);
|
|
|
|
|
+ return archive;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* Builds a .usmc archive for pkg-<name> providing provides_key with the
|
|
* Builds a .usmc archive for pkg-<name> providing provides_key with the
|
|
|
* given BUILD-phase refs (used for lot-admission fixtures).
|
|
* given BUILD-phase refs (used for lot-admission fixtures).
|
|
@@ -1091,9 +1151,34 @@ esac
|
|
|
Usm.Util.delete_tree(scratch);
|
|
Usm.Util.delete_tree(scratch);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- void test_resolve_spm_before_usm() throws Error {
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * A dependency ref must resolve to the HIGHEST version among its
|
|
|
|
|
+ * providers: the catalog is name/version-ascending and the cache
|
|
|
|
|
+ * sorts ahead of repository listings, so a first-match scan pulled
|
|
|
|
|
+ * the oldest cached copy — fresh installs of high-level packages
|
|
|
|
|
+ * shipped stale dependencies until `usm update` ran.
|
|
|
|
|
+ */
|
|
|
|
|
+ void test_resolve_prefers_newest_provider() throws Error {
|
|
|
var scratch = make_scratch();
|
|
var scratch = make_scratch();
|
|
|
var resolver = new Usm.Resolver(new Usm.ResourceFinder());
|
|
var resolver = new Usm.Resolver(new Usm.ResourceFinder());
|
|
|
|
|
+ resolver.supply_package(make_versioned_package_archive(scratch, "x", "1.0.0", {}));
|
|
|
|
|
+ resolver.supply_package(make_versioned_package_archive(scratch, "x", "1.1.0", {}));
|
|
|
|
|
+ resolver.supply_package(make_versioned_package_archive(scratch, "x", "0.9.0", {}));
|
|
|
|
|
+ resolver.supply_package(make_package_archive(scratch, "app", { "bin:pkg-x" }));
|
|
|
|
|
+
|
|
|
|
|
+ var roots = new Vector<Usm.AbstractPackage>();
|
|
|
|
|
+ roots.add(((!)resolver.find_package("pkg-app")));
|
|
|
|
|
+ var result = resolver.resolve(roots, null);
|
|
|
|
|
+
|
|
|
|
|
+ var chosen = result.packages.where(p => p.manifest.name == "pkg-x").to_vector();
|
|
|
|
|
+ check(chosen.length == 1, "exactly one provider version is resolved for the ref");
|
|
|
|
|
+ check(chosen[0].manifest.version.to_string() == "1.1.0", "a dependency ref resolves to the highest provider version, not the oldest cached copy");
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_resolve_spm_before_usm() throws Error {
|
|
|
|
|
+ var scratch = make_scratch(); var resolver = new Usm.Resolver(new Usm.ResourceFinder());
|
|
|
resolver.supply_package(make_package_archive(scratch, "x", {}));
|
|
resolver.supply_package(make_package_archive(scratch, "x", {}));
|
|
|
|
|
|
|
|
var roots = new Vector<Usm.AbstractPackage>();
|
|
var roots = new Vector<Usm.AbstractPackage>();
|
|
@@ -1457,6 +1542,85 @@ esac
|
|
|
Usm.Util.delete_tree(scratch);
|
|
Usm.Util.delete_tree(scratch);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * An update pair chain — every package both removed (its old
|
|
|
|
|
+ * version) and installed (its new one) — must split at build
|
|
|
|
|
+ * boundaries exactly like a fresh chain: the removals run before
|
|
|
|
|
+ * any build, so a dependent's build-phase ref on a removed
|
|
|
|
|
+ * package's provide is satisfied only once the replacement
|
|
|
|
|
+ * installs (an earlier lot). Regression: statum/astralis/inversion
|
|
|
|
|
+ * updated in one transaction used to plan into one lot against
|
|
|
|
|
+ * the stale pre-removal disk state.
|
|
|
|
|
+ */
|
|
|
|
|
+ void test_transaction_update_pairs_split_at_build_boundaries() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ make_build_package_archive(scratch, "x", "pc:lib-upd-x.pc", {});
|
|
|
|
|
+ make_build_package_archive(scratch, "y", "pc:lib-upd-y.pc", { "pc:lib-upd-x.pc" });
|
|
|
|
|
+ make_build_package_archive(scratch, "z", "pc:lib-upd-z.pc", { "pc:lib-upd-y.pc" });
|
|
|
|
|
+
|
|
|
|
|
+ var to_install = new HashSet<Usm.CachedPackage>();
|
|
|
|
|
+ var to_remove = new HashSet<Usm.CachedPackage>();
|
|
|
|
|
+ foreach(var name in new string[] { "x", "y", "z" }) {
|
|
|
|
|
+ var cached = cache_package(scratch, name);
|
|
|
|
|
+ to_install.add(cached);
|
|
|
|
|
+ to_remove.add(cached);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var transaction = new Usm.Transaction() {
|
|
|
|
|
+ paths = new Usm.Paths(),
|
|
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
|
|
+ to_install = to_install,
|
|
|
|
|
+ to_remove = to_remove
|
|
|
|
|
+ };
|
|
|
|
|
+ transaction.strategise();
|
|
|
|
|
+
|
|
|
|
|
+ check(transaction.install_lots.length == 3, "an update-pair chain splits into one lot per build level");
|
|
|
|
|
+ check(transaction.install_lots[0].to_string(p => p.package_name, ",") == "cache-pkg-x-1.0.0",
|
|
|
|
|
+ "the base of the update chain installs before its dependents build");
|
|
|
|
|
+ check(transaction.install_lots[1].to_string(p => p.package_name, ",") == "cache-pkg-y-1.0.0",
|
|
|
|
|
+ "a dependent builds against the freshly installed replacement, not the removed old version");
|
|
|
|
|
+ check(transaction.install_lots[2].to_string(p => p.package_name, ",") == "cache-pkg-z-1.0.0",
|
|
|
|
|
+ "the chain tip waits for the middle update pair");
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * A build-phase ref whose only provider is being REMOVED without a
|
|
|
|
|
+ * replacement in the transaction deadlocks the strategy — and the
|
|
|
|
|
+ * thrown message must say so: the explainer evaluates the same
|
|
|
|
|
+ * poison-aware satisfier the scheduler used, never the stale
|
|
|
|
|
+ * on-disk state (which showed the ref as present and produced a
|
|
|
|
|
+ * dead, reasonless diagnosis).
|
|
|
|
|
+ */
|
|
|
|
|
+ void test_transaction_removed_provider_reason() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ make_build_package_archive(scratch, "gone", "pc:lib-removed.pc", {});
|
|
|
|
|
+ make_build_package_archive(scratch, "needy", "pc:lib-needy.pc", { "pc:lib-removed.pc" });
|
|
|
|
|
+
|
|
|
|
|
+ var to_install = new HashSet<Usm.CachedPackage>();
|
|
|
|
|
+ var to_remove = new HashSet<Usm.CachedPackage>();
|
|
|
|
|
+ to_install.add(cache_package(scratch, "needy"));
|
|
|
|
|
+ to_remove.add(cache_package(scratch, "gone"));
|
|
|
|
|
+
|
|
|
|
|
+ var transaction = new Usm.Transaction() {
|
|
|
|
|
+ paths = new Usm.Paths(),
|
|
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
|
|
+ to_install = to_install,
|
|
|
|
|
+ to_remove = to_remove
|
|
|
|
|
+ };
|
|
|
|
|
+ try {
|
|
|
|
|
+ transaction.strategise();
|
|
|
|
|
+ check(false, "a removed provider without replacement deadlocks the strategy");
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Usm.TransactionError e) {
|
|
|
|
|
+ check("going away" in e.message, "the deadlock reason names the removed provider");
|
|
|
|
|
+ check("pc:lib-removed.pc" in e.message, "the deadlock reason names the poisoned resource");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
void test_transaction_manage_deps_admit_same_lot() throws Error {
|
|
void test_transaction_manage_deps_admit_same_lot() throws Error {
|
|
|
var scratch = make_scratch();
|
|
var scratch = make_scratch();
|
|
|
make_package_archive(scratch, "m1", {});
|
|
make_package_archive(scratch, "m1", {});
|
|
@@ -2759,8 +2923,496 @@ echo "Installing libmeson-pkg.so to /usr/lib"
|
|
|
Usm.Util.delete_tree(scratch);
|
|
Usm.Util.delete_tree(scratch);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The SIGINT flag and its install/restore cycle: the flag is what the
|
|
|
|
|
+ * transaction checkpoints poll, and restore clears it with the
|
|
|
|
|
+ * default disposition.
|
|
|
|
|
+ */
|
|
|
|
|
+ void test_interrupt_flag_cycle() {
|
|
|
|
|
+ Usm.TransactionInterrupt.requested = true;
|
|
|
|
|
+ check(Usm.TransactionInterrupt.requested, "the interrupt flag is settable");
|
|
|
|
|
+ Usm.TransactionInterrupt.restore();
|
|
|
|
|
+ check(!Usm.TransactionInterrupt.requested, "restore clears the flag");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- provider selection ----------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ /** One SPM query candidate for the selection fixtures. */
|
|
|
|
|
+ private Usm.SystemPackageCandidate selection_candidate(string name, string[] resources,
|
|
|
|
|
+ int dependency_count, int installed_count, string[] conflicts = {}) {
|
|
|
|
|
+ var candidate = new Usm.SystemPackageCandidate();
|
|
|
|
|
+ candidate.name = name;
|
|
|
|
|
+ candidate.resources = new Vector<Usm.ResourceRef>();
|
|
|
|
|
+ foreach(var resource in resources) {
|
|
|
|
|
+ candidate.resources.add(new Usm.ResourceRef(resource));
|
|
|
|
|
+ }
|
|
|
|
|
+ candidate.dependency_count = dependency_count;
|
|
|
|
|
+ candidate.installed_dependency_count = installed_count;
|
|
|
|
|
+ candidate.conflicts = new Vector<string>();
|
|
|
|
|
+ foreach(var conflict in conflicts) {
|
|
|
|
|
+ candidate.conflicts.add(conflict);
|
|
|
|
|
+ }
|
|
|
|
|
+ return candidate;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Usm.SystemQueryResult selection_query(Usm.SystemPackageCandidate[] candidates) {
|
|
|
|
|
+ var result = new Usm.SystemQueryResult();
|
|
|
|
|
+ result.not_found = new Vector<string>();
|
|
|
|
|
+ result.packages = new Vector<Usm.SystemPackageCandidate>();
|
|
|
|
|
+ foreach(var candidate in candidates) {
|
|
|
|
|
+ result.packages.add(candidate);
|
|
|
|
|
+ }
|
|
|
|
|
+ return result;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private Vector<Usm.ResourceRef> selection_refs(string[] refs) {
|
|
|
|
|
+ var missing = new Vector<Usm.ResourceRef>();
|
|
|
|
|
+ foreach(var reference in refs) {
|
|
|
|
|
+ missing.add(new Usm.ResourceRef(reference));
|
|
|
|
|
+ }
|
|
|
|
|
+ return missing;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Whether a provider set mixes the dropbear and openssh families —
|
|
|
|
|
+ * the conflicting-union shape that kills apk transactions.
|
|
|
|
|
+ */
|
|
|
|
|
+ private static bool selection_mixes_families(Vector<string> chosen) {
|
|
|
|
|
+ var has_dropbear = false;
|
|
|
|
|
+ var has_openssh = false;
|
|
|
|
|
+ foreach(var name in chosen) {
|
|
|
|
|
+ has_dropbear = has_dropbear || name.has_prefix("dropbear");
|
|
|
|
|
+ has_openssh = has_openssh || name.has_prefix("openssh");
|
|
|
|
|
+ }
|
|
|
|
|
+ return has_dropbear && has_openssh;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * The Alpine dropbear/openssh fixture: bin:ssh has three providers,
|
|
|
|
|
+ * bin:scp two, bin:rsync one, and the two families cannot coexist.
|
|
|
|
|
+ * The apk world declares the conflict asymmetrically (dropbear
|
|
|
|
|
+ * carries `!openssh-client`) — the fixtures mirror both shapes.
|
|
|
|
|
+ */
|
|
|
|
|
+ private Usm.SystemQueryResult dropbear_openssh_query(bool with_metadata) {
|
|
|
|
|
+ var openssh = new string[] { "openssh-client-common", "openssh-client-default", "openssh-client-krb5" };
|
|
|
|
|
+ var dropbear_conflicts = with_metadata ? openssh : new string[] {};
|
|
|
|
|
+ return selection_query({
|
|
|
|
|
+ selection_candidate("dropbear-ssh", { "bin:ssh" }, 6, 4, dropbear_conflicts),
|
|
|
|
|
+ selection_candidate("dropbear-scp", { "bin:scp" }, 6, 4, dropbear_conflicts),
|
|
|
|
|
+ selection_candidate("openssh-client-common", { "bin:scp" }, 7, 6),
|
|
|
|
|
+ selection_candidate("openssh-client-default", { "bin:ssh" }, 9, 6),
|
|
|
|
|
+ selection_candidate("openssh-client-krb5", { "bin:ssh" }, 15, 7),
|
|
|
|
|
+ selection_candidate("rsync", { "bin:rsync" }, 8, 5)
|
|
|
|
|
+ });
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * A fake SPM planner with the dropbear/openssh exclusion the apk
|
|
|
|
|
+ * solver enforces: any set mixing the families fails (null); any
|
|
|
|
|
+ * consistent set plans to itself plus one shared dependency.
|
|
|
|
|
+ */
|
|
|
|
|
+ private Vector<string>? family_aware_planner(Vector<string> names) {
|
|
|
|
|
+ if(selection_mixes_families(names)) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ var planned = new Vector<string>();
|
|
|
|
|
+ planned.add("shared-dep");
|
|
|
|
|
+ planned.add_all(names);
|
|
|
|
|
+ return planned;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_provider_selection_conflict_mapping() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ // Walk-time metadata filter: alpha covers bin:x; beta is the
|
|
|
|
|
+ // cheapest bin:y provider but DECLARES a conflict on alpha, so
|
|
|
|
|
+ // the walk must take the costlier gamma instead — a set mixing
|
|
|
|
|
+ // alpha and beta would not install.
|
|
|
|
|
+ var candidates = string.joinv(",", {
|
|
|
|
|
+ spm_candidate("alpha", "bin:x", 3, 1),
|
|
|
|
|
+ spm_candidate("beta", "bin:y", 2, 0).replace("}", ", \"conflicts\": [\"alpha\"]}"),
|
|
|
|
|
+ spm_candidate("gamma", "bin:y", 9, 0)});
|
|
|
|
|
+ var spm = stub_manager(scratch, candidates);
|
|
|
|
|
+ var result = resolve_manifest(spm, APP_MANIFEST.printf("conflicted", "conflicted",
|
|
|
|
|
+ depends_with("runtime", "[\"bin:x\", \"bin:y\"]")));
|
|
|
|
|
+ check(result.system_packages.length == 2, "both refs still resolve through the SPM");
|
|
|
|
|
+ check(result.system_packages.any(c => c.name == "alpha") && result.system_packages.any(c => c.name == "gamma"),
|
|
|
|
|
+ "a declaring-conflict candidate is skipped for a coinstallable one");
|
|
|
|
|
+ check(!result.system_packages.any(c => c.name == "beta"),
|
|
|
|
|
+ "the candidate declaring the conflict never enters the set");
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_provider_selection_repairs_unplannable_mix() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ // Metadata gap: NO conflicts reported, but the SPM's own solver
|
|
|
|
|
+ // (the stub's plan mode with USM_SPM_PLAN_CONFLICT_PAIR)
|
|
|
|
|
+ // rejects any set mixing beta and alpha. The walk greedily
|
|
|
|
|
+ // picks alpha (bin:x) and beta (cheapest bin:y); the post-walk
|
|
|
|
|
+ // plan verification catches the mix and the shared selector
|
|
|
|
|
+ // repairs it to alpha + gamma without failing the resolve.
|
|
|
|
|
+ var candidates = string.joinv(",", {
|
|
|
|
|
+ spm_candidate("alpha", "bin:x", 3, 1),
|
|
|
|
|
+ spm_candidate("beta", "bin:y", 2, 0),
|
|
|
|
|
+ spm_candidate("gamma", "bin:y", 9, 0)});
|
|
|
|
|
+ var spm = stub_manager(scratch, candidates);
|
|
|
|
|
+ Environment.set_variable("USM_SPM_PLAN_CONFLICT_PAIR", "alpha beta", true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ var result = resolve_manifest(spm, APP_MANIFEST.printf("gap", "gap",
|
|
|
|
|
+ depends_with("runtime", "[\"bin:x\", \"bin:y\"]")));
|
|
|
|
|
+ check(result.system_packages.length == 2
|
|
|
|
|
+ && result.system_packages.any(c => c.name == "alpha")
|
|
|
|
|
+ && result.system_packages.any(c => c.name == "gamma"),
|
|
|
|
|
+ "the unplannable greedy mix is repaired to a coinstallable set");
|
|
|
|
|
+ check(!result.system_packages.any(c => c.name == "beta"),
|
|
|
|
|
+ "the solver-rejected candidate is dropped by the repair");
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ Environment.unset_variable("USM_SPM_PLAN_CONFLICT_PAIR");
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ }
|
|
|
|
|
+ Environment.unset_variable("USM_SPM_PLAN_CONFLICT_PAIR");
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_provider_selection_avoids_conflicting_families() throws Error {
|
|
|
|
|
+ // With full asymmetric metadata AND a planner: exactly one
|
|
|
|
|
+ // provider per ref, never a family mix.
|
|
|
|
|
+ var selection = Usm.ProviderSelection.select(
|
|
|
|
|
+ selection_refs({ "bin:scp", "bin:ssh", "bin:rsync" }),
|
|
|
|
|
+ dropbear_openssh_query(true), family_aware_planner);
|
|
|
|
|
+ check(selection.chosen.length == 3, "one provider per ref is chosen (3 refs, %u chosen)".printf(selection.chosen.length));
|
|
|
|
|
+ check(selection.unresolved.length == 0, "every ref resolves");
|
|
|
|
|
+ check(!selection_mixes_families(selection.chosen), "the chosen set never mixes dropbear and openssh");
|
|
|
|
|
+ check(selection.chosen.any(name => name == "rsync"), "the sole provider of a ref is always chosen");
|
|
|
|
|
+
|
|
|
|
|
+ // Metadata gap — NO conflicts reported, planner only: the
|
|
|
|
|
+ // greedy per-ref cost heuristic alone WOULD mix families
|
|
|
|
|
+ // (openssh-client-common for scp, dropbear-ssh for ssh); the
|
|
|
|
|
+ // planner rejects the mix and a consistent set results.
|
|
|
|
|
+ selection = Usm.ProviderSelection.select(
|
|
|
|
|
+ selection_refs({ "bin:scp", "bin:ssh", "bin:rsync" }),
|
|
|
|
|
+ dropbear_openssh_query(false), family_aware_planner);
|
|
|
|
|
+ check(selection.chosen.length == 3 && selection.unresolved.length == 0,
|
|
|
|
|
+ "metadata-free selection with a planner still covers every ref");
|
|
|
|
|
+ check(!selection_mixes_families(selection.chosen),
|
|
|
|
|
+ "the planner catches the family mix the metadata never reported");
|
|
|
|
|
+
|
|
|
|
|
+ // No planner at all: metadata alone still prevents the mix —
|
|
|
|
|
+ // the asymmetric dropbear declaration filters the openssh
|
|
|
|
|
+ // candidates once any dropbear package is chosen, and vice
|
|
|
|
|
+ // versa the walk keeps whichever family it entered first.
|
|
|
|
|
+ selection = Usm.ProviderSelection.select(
|
|
|
|
|
+ selection_refs({ "bin:scp", "bin:ssh", "bin:rsync" }),
|
|
|
|
|
+ dropbear_openssh_query(true), null);
|
|
|
|
|
+ check(selection.chosen.length == 3 && selection.unresolved.length == 0,
|
|
|
|
|
+ "planner-free selection covers every ref via metadata");
|
|
|
|
|
+ check(!selection_mixes_families(selection.chosen),
|
|
|
|
|
+ "the conflict metadata alone keeps the families separate");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_provider_selection_unresolved_and_reuse() throws Error {
|
|
|
|
|
+ // A ref whose every candidate conflicts with the chosen set
|
|
|
|
|
+ // lands in unresolved instead of aborting the selection.
|
|
|
|
|
+ var query = selection_query({
|
|
|
|
|
+ selection_candidate("alpha", { "bin:a" }, 2, 0),
|
|
|
|
|
+ selection_candidate("beta", { "bin:b" }, 2, 0, { "alpha" }),
|
|
|
|
|
+ selection_candidate("gamma", { "bin:b" }, 9, 0, { "alpha" })
|
|
|
|
|
+ });
|
|
|
|
|
+ var selection = Usm.ProviderSelection.select(selection_refs({ "bin:a", "bin:b" }), query, null);
|
|
|
|
|
+ check(selection.chosen.length == 1 && selection.chosen[0] == "alpha",
|
|
|
|
|
+ "the viable ref still resolves");
|
|
|
|
|
+ check(selection.unresolved.length == 1 && selection.unresolved[0].to_string() == "bin:b",
|
|
|
|
|
+ "a ref with only conflicting candidates is reported unresolved");
|
|
|
|
|
+
|
|
|
|
|
+ // A candidate chosen for one ref is reused for another it also
|
|
|
|
|
+ // covers (no second package for the same provider).
|
|
|
|
|
+ query = selection_query({
|
|
|
|
|
+ selection_candidate("multi", { "bin:a", "bin:b" }, 3, 0),
|
|
|
|
|
+ selection_candidate("single", { "bin:b" }, 1, 0)
|
|
|
|
|
+ });
|
|
|
|
|
+ selection = Usm.ProviderSelection.select(selection_refs({ "bin:a", "bin:b" }), query, null);
|
|
|
|
|
+ check(selection.chosen.length == 1 && selection.chosen[0] == "multi",
|
|
|
|
|
+ "a multi-ref provider satisfies later refs for free");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_provider_selection_plans_cumulative_sets() throws Error {
|
|
|
|
|
+ // The planner sees the CHOSEN SET plus the candidate each
|
|
|
|
|
+ // round — never a bare candidate — so its verdicts are exactly
|
|
|
|
|
+ // the co-installability questions the SPM will face.
|
|
|
|
|
+ var seen_by_planner = new Vector<string>();
|
|
|
|
|
+ Usm.SystemProviderPlan recorder = names => {
|
|
|
|
|
+ seen_by_planner.add(string.joinv("+", names.to_array()));
|
|
|
|
|
+ return family_aware_planner(names);
|
|
|
|
|
+ };
|
|
|
|
|
+ Usm.ProviderSelection.select(
|
|
|
|
|
+ selection_refs({ "bin:rsync", "bin:ssh" }),
|
|
|
|
|
+ dropbear_openssh_query(true), recorder);
|
|
|
|
|
+ check(seen_by_planner.length >= 2
|
|
|
|
|
+ && seen_by_planner[0].contains("rsync")
|
|
|
|
|
+ && !seen_by_planner[0].contains("+"),
|
|
|
|
|
+ "the first round plans the sole candidate alone");
|
|
|
|
|
+ check(seen_by_planner.any(seen => seen.contains("rsync+") || seen.contains("+rsync")),
|
|
|
|
|
+ "later rounds plan the cumulative chosen set");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_system_candidate_conflicts_mapping() throws Error {
|
|
|
|
|
+ // The query contract's conflicts member is optional: helpers
|
|
|
|
|
+ // predating it (or SPMs that cannot tell) omit it, and the
|
|
|
|
|
+ // mapper must yield an empty vector rather than fail.
|
|
|
|
|
+ var absent = Usm.SystemPackageCandidate.get_mapper().materialise(
|
|
|
|
|
+ new JsonElement.from_string("""{"name":"pkg","resources":["bin:x"],"dependency-count":1,"installed-dependency-count":0}""")
|
|
|
|
|
+ .as<Invercargill.Properties>());
|
|
|
|
|
+ check(absent.conflicts != null && absent.conflicts.length == 0,
|
|
|
|
|
+ "an absent conflicts member maps to an empty vector");
|
|
|
|
|
+
|
|
|
|
|
+ var present = Usm.SystemPackageCandidate.get_mapper().materialise(
|
|
|
|
|
+ new JsonElement.from_string("""{"name":"pkg","resources":["bin:x"],"dependency-count":1,"installed-dependency-count":0,"conflicts":["other-pkg"]}""")
|
|
|
|
|
+ .as<Invercargill.Properties>());
|
|
|
|
|
+ check(present.conflicts.length == 1 && present.conflicts[0] == "other-pkg",
|
|
|
|
|
+ "a present conflicts member maps to its names");
|
|
|
|
|
+
|
|
|
|
|
+ // The real patched apk helper's answer for bin:scp + bin:ssh +
|
|
|
|
|
+ // bin:rsync on alpine:latest (2026-10): the dropbear `!openssh-
|
|
|
|
|
+ // client` blocker resolves to the openssh-client providers, the
|
|
|
|
|
+ // openssh split packages carry their own pairwise conflicts,
|
|
|
|
|
+ // and rsync none. Pinned so the contract cannot drift from the
|
|
|
|
|
+ // helper — and note the real gap this fixture documents:
|
|
|
|
|
+ // openssh-client-common declares nothing against dropbear, the
|
|
|
|
|
+ // pair only the SPM's own solver rejects.
|
|
|
|
|
+ var apk_query = Usm.SystemQueryResult.get_mapper().materialise(
|
|
|
|
|
+ new JsonElement.from_string("""{"not-found":[],"packages":[
|
|
|
|
|
+ {"name":"dropbear-scp","resources":["bin:scp"],"dependency-count":6,"installed-dependency-count":4,"conflicts":["openssh-client-default","openssh-client-krb5"]},
|
|
|
|
|
+ {"name":"dropbear-ssh","resources":["bin:ssh"],"dependency-count":6,"installed-dependency-count":4,"conflicts":["openssh-client-default","openssh-client-krb5"]},
|
|
|
|
|
+ {"name":"openssh-client-common","resources":["bin:scp"],"dependency-count":7,"installed-dependency-count":6,"conflicts":[]},
|
|
|
|
|
+ {"name":"openssh-client-default","resources":["bin:ssh"],"dependency-count":9,"installed-dependency-count":6,"conflicts":["openssh-client-krb5"]},
|
|
|
|
|
+ {"name":"openssh-client-krb5","resources":["bin:ssh"],"dependency-count":15,"installed-dependency-count":7,"conflicts":["openssh-client-default"]},
|
|
|
|
|
+ {"name":"rsync","resources":["bin:rsync"],"dependency-count":8,"installed-dependency-count":5,"conflicts":[]}
|
|
|
|
|
+ ]}""")
|
|
|
|
|
+ .as<Invercargill.Properties>());
|
|
|
|
|
+ check(apk_query.packages.length == 6, "the pinned apk query parses");
|
|
|
|
|
+ check(apk_query.packages.any(c => c.name == "dropbear-ssh" && c.conflicts.length == 2),
|
|
|
|
|
+ "the dropbear blocker maps to its two openssh conflict targets");
|
|
|
|
|
+
|
|
|
|
|
+ // Selection over the REAL apk metadata, metadata-only (no
|
|
|
|
|
+ // planner): every ref still resolves — but the set MIXES
|
|
|
|
|
+ // families (openssh-client-common wins bin:scp on cost, then
|
|
|
|
|
+ // dropbear-ssh wins bin:ssh; common declares nothing against
|
|
|
|
|
+ // dropbear). This gap is why planner verification is not
|
|
|
|
|
+ // optional: with the family-aware planner the mix is
|
|
|
|
|
+ // impossible.
|
|
|
|
|
+ var selection = Usm.ProviderSelection.select(
|
|
|
|
|
+ selection_refs({ "bin:scp", "bin:ssh", "bin:rsync" }), apk_query, null);
|
|
|
|
|
+ check(selection.chosen.length == 3 && selection.unresolved.length == 0,
|
|
|
|
|
+ "the real apk metadata selects one provider per ref");
|
|
|
|
|
+ check(selection_mixes_families(selection.chosen),
|
|
|
|
|
+ "the real apk metadata alone leaves the common/dropbear gap (the planner layer exists for exactly this)");
|
|
|
|
|
+
|
|
|
|
|
+ selection = Usm.ProviderSelection.select(
|
|
|
|
|
+ selection_refs({ "bin:scp", "bin:ssh", "bin:rsync" }), apk_query, family_aware_planner);
|
|
|
|
|
+ check(selection.chosen.length == 3 && selection.unresolved.length == 0
|
|
|
|
|
+ && !selection_mixes_families(selection.chosen),
|
|
|
|
|
+ "the planner over the real apk metadata selects a coinstallable family set");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- usmrad: configuration parsing ---------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ const string[] USMRAD_VARIABLES = {
|
|
|
|
|
+ "USMRAD_REPO", "USMRAD_BRANCH", "USMRAD_POLL", "USMRAD_APPDIR", "USMRAD_USM_REPOS", "USMRAD_TERM_GRACE"
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ string?[] saved_usmrad_values;
|
|
|
|
|
+
|
|
|
|
|
+ /** Unsets every USMRAD_* variable, stashing the prior values for {@link restore_usmrad_env}. */
|
|
|
|
|
+ void clear_usmrad_env() {
|
|
|
|
|
+ saved_usmrad_values = new string?[USMRAD_VARIABLES.length];
|
|
|
|
|
+ for(int index = 0; index < USMRAD_VARIABLES.length; index++) {
|
|
|
|
|
+ saved_usmrad_values[index] = Environment.get_variable(USMRAD_VARIABLES[index]);
|
|
|
|
|
+ Environment.unset_variable(USMRAD_VARIABLES[index]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Restores the values {@link clear_usmrad_env} stashed; other tests share this process. */
|
|
|
|
|
+ void restore_usmrad_env() {
|
|
|
|
|
+ for(int index = 0; index < USMRAD_VARIABLES.length; index++) {
|
|
|
|
|
+ var value = saved_usmrad_values[index];
|
|
|
|
|
+ if(value != null) {
|
|
|
|
|
+ Environment.set_variable(USMRAD_VARIABLES[index], value, true);
|
|
|
|
|
+ }
|
|
|
|
|
+ else {
|
|
|
|
|
+ Environment.unset_variable(USMRAD_VARIABLES[index]);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_rad_config_defaults() throws Error {
|
|
|
|
|
+ try {
|
|
|
|
|
+ clear_usmrad_env();
|
|
|
|
|
+ Environment.set_variable("USMRAD_REPO", "https://git.example/app.git", true);
|
|
|
|
|
+
|
|
|
|
|
+ var config = Usm.Rad.Config.from_environment();
|
|
|
|
|
+
|
|
|
|
|
+ check(config.repo == "https://git.example/app.git", "USMRAD_REPO is carried through");
|
|
|
|
|
+ check(config.branch == "master", "branch defaults to master");
|
|
|
|
|
+ check(config.poll_minutes == 0, "polling defaults to disabled");
|
|
|
|
|
+ check(config.poll_interval_seconds == 0, "a disabled poll yields no interval");
|
|
|
|
|
+ check(config.appdir == "/app", "appdir defaults to /app");
|
|
|
|
|
+ check(config.usm_repos.length == 0, "usm_repos defaults to empty");
|
|
|
|
|
+ check(config.term_grace_seconds == 10, "term grace defaults to 10 seconds");
|
|
|
|
|
+ }
|
|
|
|
|
+ finally {
|
|
|
|
|
+ restore_usmrad_env();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_rad_config_errors() throws Error {
|
|
|
|
|
+ try {
|
|
|
|
|
+ clear_usmrad_env();
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ Usm.Rad.Config.from_environment();
|
|
|
|
|
+ check(false, "missing USMRAD_REPO is a configuration error");
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Usm.Rad.RadError e) {
|
|
|
|
|
+ check(e is Usm.Rad.RadError.CONFIGURATION, "missing USMRAD_REPO is a configuration error");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Environment.set_variable("USMRAD_REPO", "https://git.example/app.git", true);
|
|
|
|
|
+
|
|
|
|
|
+ Environment.set_variable("USMRAD_POLL", "abc", true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ Usm.Rad.Config.from_environment();
|
|
|
|
|
+ check(false, "non-numeric USMRAD_POLL is rejected");
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Usm.Rad.RadError e) {
|
|
|
|
|
+ check(e is Usm.Rad.RadError.CONFIGURATION, "non-numeric USMRAD_POLL is rejected");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Environment.set_variable("USMRAD_POLL", "-1", true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ Usm.Rad.Config.from_environment();
|
|
|
|
|
+ check(false, "negative USMRAD_POLL is rejected");
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Usm.Rad.RadError e) {
|
|
|
|
|
+ check(e is Usm.Rad.RadError.CONFIGURATION, "negative USMRAD_POLL is rejected");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Environment.unset_variable("USMRAD_POLL");
|
|
|
|
|
+ Environment.set_variable("USMRAD_TERM_GRACE", "x", true);
|
|
|
|
|
+ try {
|
|
|
|
|
+ Usm.Rad.Config.from_environment();
|
|
|
|
|
+ check(false, "non-numeric USMRAD_TERM_GRACE is rejected");
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Usm.Rad.RadError e) {
|
|
|
|
|
+ check(e is Usm.Rad.RadError.CONFIGURATION, "non-numeric USMRAD_TERM_GRACE is rejected");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Environment.set_variable("USMRAD_TERM_GRACE", "10", true);
|
|
|
|
|
+ Environment.set_variable("USMRAD_POLL", "0.25", true);
|
|
|
|
|
+ var config = Usm.Rad.Config.from_environment();
|
|
|
|
|
+ check(config.poll_minutes == 0.25, "fractional USMRAD_POLL parses");
|
|
|
|
|
+ check(config.poll_interval_seconds == 15, "0.25 minutes converts to a 15 second interval");
|
|
|
|
|
+ }
|
|
|
|
|
+ finally {
|
|
|
|
|
+ restore_usmrad_env();
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- usmrad: slot model ---------------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ void test_rad_slots_fresh_reads_null() throws Error {
|
|
|
|
|
+ var appdir = DirUtils.make_tmp("usmrad-tests-XXXXXX");
|
|
|
|
|
+ var slots = new Usm.Rad.Slots(appdir);
|
|
|
|
|
+ try {
|
|
|
|
|
+ check(slots.read_current_slot() == null, "read_current_slot is null on a fresh appdir");
|
|
|
|
|
+ check(slots.inactive_slot() == null, "inactive_slot is null on a fresh appdir");
|
|
|
|
|
+ Usm.Util.delete_tree(appdir);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ Usm.Util.delete_tree(appdir);
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_rad_slots_layout_flip_and_deployed_commit() throws Error {
|
|
|
|
|
+ var appdir = DirUtils.make_tmp("usmrad-tests-XXXXXX");
|
|
|
|
|
+ var slots = new Usm.Rad.Slots(appdir);
|
|
|
|
|
+ try {
|
|
|
|
|
+ slots.ensure_layout();
|
|
|
|
|
+ foreach(var name in new string[] { "a", "b", "pwd", "repo", "build" }) {
|
|
|
|
|
+ check(FileUtils.test(Path.build_filename(appdir, name), FileTest.IS_DIR), @"ensure_layout creates \"$name\"");
|
|
|
|
|
+ }
|
|
|
|
|
+ slots.ensure_layout();
|
|
|
|
|
+ check(slots.read_deployed_commit() == null, "read_deployed_commit returns null before anything is deployed");
|
|
|
|
|
+
|
|
|
|
|
+ slots.flip_current("a");
|
|
|
|
|
+ check(slots.read_current_slot() == "a", "flip_current makes slot \"a\" current");
|
|
|
|
|
+ check(slots.inactive_slot() == "b", "slot \"b\" is inactive while \"a\" is current");
|
|
|
|
|
+
|
|
|
|
|
+ slots.write_deployed_commit("0123456789abcdef");
|
|
|
|
|
+ check(slots.read_deployed_commit() == "0123456789abcdef", "the deployed commit round-trips");
|
|
|
|
|
+
|
|
|
|
|
+ slots.flip_current("b");
|
|
|
|
|
+ check(slots.read_current_slot() == "b", "flipping the symlink back makes slot \"b\" current");
|
|
|
|
|
+ check(slots.inactive_slot() == "a", "slot \"a\" is inactive while \"b\" is current");
|
|
|
|
|
+ check(slots.read_deployed_commit() == "0123456789abcdef", "the deployed commit survives a slot flip");
|
|
|
|
|
+ Usm.Util.delete_tree(appdir);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ Usm.Util.delete_tree(appdir);
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_rad_slots_resolve_exec_order() throws Error {
|
|
|
|
|
+ var appdir = DirUtils.make_tmp("usmrad-tests-XXXXXX");
|
|
|
|
|
+ var slots = new Usm.Rad.Slots(appdir);
|
|
|
|
|
+ try {
|
|
|
|
|
+ slots.ensure_layout();
|
|
|
|
|
+
|
|
|
|
|
+ var usr_bin_prog = Path.build_filename(slots.slot_path("a"), "usr", "bin", "prog");
|
|
|
|
|
+ DirUtils.create_with_parents(Path.get_dirname(usr_bin_prog), 0755);
|
|
|
|
|
+ FileUtils.set_contents(usr_bin_prog, "#!/bin/sh\nexit 0\n");
|
|
|
|
|
+ Posix.chmod(usr_bin_prog, 0755);
|
|
|
|
|
+
|
|
|
|
|
+ var bin_prog = Path.build_filename(slots.slot_path("a"), "bin", "prog");
|
|
|
|
|
+ DirUtils.create_with_parents(Path.get_dirname(bin_prog), 0755);
|
|
|
|
|
+ FileUtils.set_contents(bin_prog, "#!/bin/sh\nexit 0\n");
|
|
|
|
|
+ Posix.chmod(bin_prog, 0755);
|
|
|
|
|
+
|
|
|
|
|
+ check(slots.resolve_exec("a", "prog") == usr_bin_prog, "usr/bin wins over bin in the exec search order");
|
|
|
|
|
+ check(slots.resolve_exec("a", "absent") == null, "a missing exec name resolves to null");
|
|
|
|
|
+ check(slots.resolve_exec("a", "sub/prog") == null, "an exec name containing \"/\" is rejected");
|
|
|
|
|
+ Usm.Util.delete_tree(appdir);
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ Usm.Util.delete_tree(appdir);
|
|
|
|
|
+ throw e;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
int main() {
|
|
int main() {
|
|
|
test_default_ignore();
|
|
test_default_ignore();
|
|
|
|
|
+ test_interrupt_flag_cycle();
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_provider_selection_avoids_conflicting_families();
|
|
|
|
|
+ test_provider_selection_conflict_mapping();
|
|
|
|
|
+ test_provider_selection_repairs_unplannable_mix();
|
|
|
|
|
+ test_provider_selection_unresolved_and_reuse();
|
|
|
|
|
+ test_provider_selection_plans_cumulative_sets();
|
|
|
|
|
+ test_system_candidate_conflicts_mapping();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL provider selection test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_version_ordering();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL version ordering test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
try {
|
|
try {
|
|
|
test_ignore_patterns();
|
|
test_ignore_patterns();
|
|
|
test_ignore_always_included();
|
|
test_ignore_always_included();
|
|
@@ -2823,6 +3475,7 @@ echo "Installing libmeson-pkg.so to /usr/lib"
|
|
|
test_satisfied_by_constraints();
|
|
test_satisfied_by_constraints();
|
|
|
test_manifest_satisfies_pc_fallback();
|
|
test_manifest_satisfies_pc_fallback();
|
|
|
test_resolve_diamond_usm_only();
|
|
test_resolve_diamond_usm_only();
|
|
|
|
|
+ test_resolve_prefers_newest_provider();
|
|
|
test_resolve_spm_before_usm();
|
|
test_resolve_spm_before_usm();
|
|
|
test_resolver_constrained_lib_dep();
|
|
test_resolver_constrained_lib_dep();
|
|
|
test_resolver_skips_spm_for_constrained_refs();
|
|
test_resolver_skips_spm_for_constrained_refs();
|
|
@@ -2835,6 +3488,8 @@ echo "Installing libmeson-pkg.so to /usr/lib"
|
|
|
test_system_package_manager_install_stub();
|
|
test_system_package_manager_install_stub();
|
|
|
test_transaction_accepts_orders();
|
|
test_transaction_accepts_orders();
|
|
|
test_transaction_lots_split_at_build_boundaries();
|
|
test_transaction_lots_split_at_build_boundaries();
|
|
|
|
|
+ test_transaction_update_pairs_split_at_build_boundaries();
|
|
|
|
|
+ test_transaction_removed_provider_reason();
|
|
|
test_transaction_manage_deps_admit_same_lot();
|
|
test_transaction_manage_deps_admit_same_lot();
|
|
|
test_transaction_preinstalled_resources_satisfy_build_deps();
|
|
test_transaction_preinstalled_resources_satisfy_build_deps();
|
|
|
test_transaction_deadlock_message_details_causes();
|
|
test_transaction_deadlock_message_details_causes();
|
|
@@ -2929,6 +3584,46 @@ echo "Installing libmeson-pkg.so to /usr/lib"
|
|
|
print("FAIL progress reporting test threw: %s\n", e.message);
|
|
print("FAIL progress reporting test threw: %s\n", e.message);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_rad_config_defaults();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL rad config defaults test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_rad_config_errors();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL rad config errors test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_rad_slots_fresh_reads_null();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL rad fresh slot test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_rad_slots_layout_flip_and_deployed_commit();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL rad slot layout test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_rad_slots_resolve_exec_order();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL rad exec resolution test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
print("%d passed, %d failed\n", passes, failures);
|
|
print("%d passed, %d failed\n", passes, failures);
|
|
|
return failures == 0 ? 0 : 1;
|
|
return failures == 0 ? 0 : 1;
|
|
|
}
|
|
}
|