using Invercargill; using Invercargill.DataStructures; using InvercargillJson; namespace Usm.Tests { int failures = 0; int passes = 0; void check(bool condition, string label) { if (condition) { passes++; print("PASS %s\n", label); } else { failures++; print("FAIL %s\n", label); } } /** * Materialises a manifest from a JSON string the same way the CLI does, * so validation runs against exactly what `usm manifest` would load. */ Usm.Manifest manifest_from_json(string json) throws Error { var element = new JsonElement.from_string(json); return Usm.Manifest.get_mapper().materialise(element.as()); } /** Minimal valid data-package manifest, with execs injected per test. */ const string DATA_MANIFEST = """ { "name": "fonts-example", "version": "1.0.0", "summary": "Example fonts", "licences": [], "flags": ["dataPackage"], "provides": { "res:fonts/Example.ttf": "source:fonts/Example.ttf" }, "depends": { "runtime": [], "build": [], "manage": ["bin:bash"] }, %s } """; void test_default_ignore() { var ignore = new Usm.UsmIgnore(); check(ignore.matches(".git", true), "default ignores the .git directory"); check(ignore.matches(".git/config", false), "default ignores everything beneath .git"); check(!ignore.matches(".git", false), "default does not ignore a file named .git"); check(!ignore.matches("src/main.vala", false), "default keeps ordinary files"); check(!ignore.matches(".usmignore", false), ".usmignore can never be ignored"); check(!ignore.matches("MANIFEST.usm", false), "MANIFEST.usm can never be ignored"); } void test_ignore_patterns() { var ignore = new Usm.UsmIgnore(); ignore.add_pattern("# a comment"); ignore.add_pattern(" "); ignore.add_pattern("builddir/"); ignore.add_pattern("*.sqlite"); ignore.add_pattern("docs/generated/"); ignore.add_pattern("src/secret.*"); ignore.add_pattern("notes/?draft.md"); ignore.add_pattern("!keep.sqlite"); check(!ignore.matches("# a comment", false), "comments are skipped"); check(ignore.matches("db.sqlite", false), "suffix pattern matches the basename"); check(ignore.matches("data/deep/db.sqlite", false), "suffix pattern matches at any depth"); check(!ignore.matches("db.sqlite3", false), "suffix pattern anchors the basename end"); check(ignore.matches("builddir", true), "directory-only pattern matches the directory itself"); check(!ignore.matches("builddir", false), "directory-only pattern ignores no plain file"); check(ignore.matches("builddir/meson", false), "directory-only pattern matches everything beneath"); check(ignore.matches("src/builddir", true), "unanchored directory-only pattern matches at any depth"); check(ignore.matches("src/builddir/x", false), "unanchored directory-only pattern covers beneath at any depth"); check(!ignore.matches("builddirs", true), "directory-only pattern does not over-match names"); check(ignore.matches("docs/generated", true), "anchored directory-only pattern matches its path"); check(ignore.matches("docs/generated/a.md", false), "anchored directory-only pattern covers beneath"); check(!ignore.matches("other/docs/generated", true), "anchored directory-only pattern does not match deeper roots"); check(ignore.matches("src/secret.key", false), "anchored pattern matches the full relative path"); check(!ignore.matches("src/sub/secret.key", false), "star never crosses a path separator"); check(!ignore.matches("include/secret.key", false), "anchored pattern does not match other roots"); check(ignore.matches("notes/1draft.md", false), "question mark matches one character"); check(!ignore.matches("notes/12draft.md", false), "question mark matches exactly one character"); check(ignore.matches("keep.sqlite", false), "exclamation mark has no negation power"); check(ignore.matches("!keep.sqlite", false), "exclamation mark is a literal pattern character"); check(ignore.matches(".git", true), ".git stays ignored alongside explicit patterns"); } void test_ignore_always_included() { var ignore = new Usm.UsmIgnore(); ignore.add_pattern("*"); ignore.add_pattern(".usmignore"); ignore.add_pattern("MANIFEST.usm"); check(ignore.matches("everything", false), "catch-all pattern matches ordinary files"); check(!ignore.matches(".usmignore", false), "catch-all cannot ignore .usmignore"); check(!ignore.matches("MANIFEST.usm", false), "catch-all cannot ignore MANIFEST.usm"); check(ignore.matches("sub/MANIFEST.usm", false), "nested manifests follow normal rules"); } void test_ignore_from_root() throws Error { var root = File.new_build_filename("/tmp", Uuid.string_random()); root.make_directory(); var without = new Usm.UsmIgnore.from_root(root.get_path()); check(without.matches(".git", true), "absent .usmignore falls back to the .git default"); check(!without.matches("db.sqlite", false), "absent .usmignore ignores nothing else"); FileUtils.set_contents(Path.build_filename(root.get_path(), ".usmignore"), "*.sqlite\n"); var with = new Usm.UsmIgnore.from_root(root.get_path()); check(with.matches("db.sqlite", false), ".usmignore is loaded from the package root"); check(!with.matches("MANIFEST.usm", false), "loaded .usmignore keeps MANIFEST.usm included"); Usm.Util.delete_tree(root.get_path()); } void test_data_package_flag() { check(Usm.ManifestFlag.DATA_PACKAGE.to_string() == "dataPackage", "dataPackage flag serialises"); check(Usm.ManifestFlag.from_string("dataPackage") == Usm.ManifestFlag.DATA_PACKAGE, "dataPackage flag parses"); } void test_rebuild_dependants_flag() throws Error { check(Usm.ManifestFlag.REBUILD_DEPENDANTS.to_string() == "rebuildDependants", "rebuildDependants flag serialises"); check(Usm.ManifestFlag.from_string("rebuildDependants") == Usm.ManifestFlag.REBUILD_DEPENDANTS, "rebuildDependants flag parses"); var flagged = manifest_from_json(APP_MANIFEST.printf("invercargill", "invercargill", depends_with("runtime", "[]")) .replace("\"flags\": []", "\"flags\": [\"rebuildDependants\"]")); check(flagged.flags.contains(Usm.ManifestFlag.REBUILD_DEPENDANTS), "rebuildDependants parses within a manifest flags array"); var serialised = ((!)new JsonElement.from_properties(Usm.Manifest.get_mapper().map_from(flagged))).stringify_pretty(); check(manifest_from_json(serialised).flags.contains(Usm.ManifestFlag.REBUILD_DEPENDANTS), "rebuildDependants round-trips through serialisation"); } void test_data_package_validation() throws Error { var clean = manifest_from_json(DATA_MANIFEST.printf("\"execs\": { \"acquire\": \"fetch.sh\" }")); clean.validate(); check(clean.is_data_package, "dataPackage flag marks a data package"); check(clean.executables.acquire != null, "acquire executable is honoured on data packages"); check(clean.executables.build == null, "data package without build parses"); var no_execs = manifest_from_json(DATA_MANIFEST.printf("\"execs\": { }")); no_execs.validate(); check(no_execs.executables.build == null, "empty execs parse without a build executable"); var omitted = manifest_from_json(DATA_MANIFEST.printf("\"extras\": { }")); omitted.validate(); check(omitted.executables != null, "omitting execs entirely parses"); var rejects = 0; string[] offending = { "build", "install", "rebuild", "test", "remove", "postInstall" }; foreach(var executable in offending) { try { var manifest = manifest_from_json(DATA_MANIFEST.printf("\"execs\": { \"%s\": \"run.sh\" }".printf(executable))); manifest.validate(); print("FAIL dataPackage + %s should not validate\n", executable); failures++; } catch(Usm.ManifestError e) { rejects++; } } check(rejects == offending.length, "dataPackage + any lifecycle executable is rejected"); try { var manifest = manifest_from_json(DATA_MANIFEST.printf("\"execs\": { \"install\": \"run.sh\" }")); manifest.validate(); check(false, "rejection message mentions the offending executable"); } catch(Usm.ManifestError e) { check(e.message.contains("dataPackage") && e.message.contains("install"), "rejection message mentions the offending executable"); } } void test_data_package_from_file() throws Error { var root = File.new_build_filename("/tmp", Uuid.string_random()); root.make_directory(); var manifest_path = Path.build_filename(root.get_path(), "MANIFEST.usm"); FileUtils.set_contents(manifest_path, DATA_MANIFEST.printf("\"execs\": { \"install\": \"install.sh\" }")); try { new Usm.Manifest.from_file(manifest_path); check(false, "Manifest.from_file enforces the data-package rule"); } catch(Usm.ManifestError e) { check(true, "Manifest.from_file enforces the data-package rule"); } Usm.Util.delete_tree(root.get_path()); } void test_configuration_system_package_manager() throws Error { var config_dir = File.new_build_filename("/tmp", Uuid.string_random()); config_dir.make_directory(); var paths = new Usm.Paths.defaults(); paths.usm_config_dir = config_dir.get_path(); FileUtils.set_contents(Path.build_filename(config_dir.get_path(), "usm.config"), "{\"is_managed\": false, \"system_package_manager\": { \"query\": [\"/bin/usm-spm-dnf\", \"query\"], \"install\": [\"/bin/usm-spm-dnf\", \"install\"] }}"); var complete = new Usm.Configuration.from_paths(paths); check(complete.system_package_manager != null && complete.system_package_manager.query != null, "usm.config system_package_manager section parses"); FileUtils.set_contents(Path.build_filename(config_dir.get_path(), "usm.config"), "{\"is_managed\": false, \"system_package_manager\": { \"query\": [\"/bin/usm-spm-dnf\", \"query\"] }}"); try { new Usm.Configuration.from_paths(paths); check(false, "query without install is rejected"); } catch(Usm.ConfigurationError e) { check(true, "query without install is rejected"); } Usm.Util.delete_tree(config_dir.get_path()); } // ---- GIO module resource type ------------------------------------------------ void test_gio_module_ref() throws Error { var resource_ref = new Usm.ResourceRef("gio:libgiognutls.so"); check(resource_ref.resource_type == Usm.ResourceType.GIO_MODULE, "gio: parses as a GIO module ref"); check(resource_ref.resource == "libgiognutls.so", "the gio: name is the module filename"); check(Usm.ResourceType.GIO_MODULE.to_string() == "gio", "gio round-trips through to_string"); check(resource_ref.to_string() == "gio:libgiognutls.so", "the GIO module ref round-trips"); var located = new Usm.ResourceFinder().locate_resource(resource_ref); check(located != null && located.has_suffix("/gio/modules/libgiognutls.so"), "the ResourceFinder locates libgiognutls.so in a gio/modules directory"); } // ---- Dependency-phase model ------------------------------------------------- /** Minimal manifest with the given "depends" section verbatim. */ const string PHASE_MANIFEST = """ { "name": "phased-app", "version": "1.0.0", "summary": "phases", "licences": [], "flags": [], "provides": { "bin:phased-app": "as-expected" }, "depends": %s, "execs": {} } """; /** Manifest named providing bin: with the given "depends" section. */ const string APP_MANIFEST = """ { "name": "%s", "version": "1.0.0", "summary": "app", "licences": [], "flags": [], "provides": { "bin:%s": "as-expected" }, "depends": %s, "execs": {} } """; /** A "depends" section with {@link value} in {@link phase} and empty flat arrays elsewhere. */ string depends_with(string phase, string value) { var parts = new StringBuilder(); foreach(var other in new string[] { "runtime", "build", "manage", "acquire" }) { if(parts.len > 0) { parts.append(", "); } parts.append_printf("\"%s\": %s", other, other == phase ? value : "[]"); } return "{ " + parts.str + " }"; } void test_dependencies_flat_back_compat() throws Error { var manifest = manifest_from_json(PHASE_MANIFEST.printf( "{ \"runtime\": [\"bin:bash\", \"bin:coreutils\"], \"build\": [], \"manage\": [], \"acquire\": [\"bin:wget\"] }")); check(!manifest.dependencies.runtime.is_grouped, "flat runtime phase stays flat"); check(manifest.dependencies.runtime.required.count() == 2, "flat runtime phase holds every ref"); check(manifest.dependencies.runtime.ordered_required().first().to_string() == "bin:bash", "flat phase refs are ordered by string"); check(!manifest.dependencies.build.is_grouped && manifest.dependencies.build.required.count() == 0, "empty flat phase parses"); check(!manifest.dependencies.manage.is_grouped, "manage phase stays flat"); check(manifest.dependencies.acquire != null && manifest.dependencies.acquire.required.count() == 1, "flat acquire phase parses"); check(manifest.dependencies.acquire.ordered_required().first().to_string() == "bin:wget", "acquire refs land in the acquire phase, not manage"); var omitted = manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": [], \"build\": [], \"manage\": [] }")); check(omitted.dependencies.acquire == null, "omitted acquire phase stays null"); var usm_shape = manifest_from_json(PHASE_MANIFEST.printf( "{ \"runtime\": [\"lib:libc.so.6\"], \"build\": [\"bin:valac\"], \"manage\": [\"bin:bash\"] }")); check(usm_shape.dependencies.build.required.any(r => r.to_string() == "bin:valac"), "usm's own flat manifest shape still parses"); } void test_dependencies_nested() throws Error { var manifest = manifest_from_json(PHASE_MANIFEST.printf( "{ \"runtime\": [[\"bin:python3\", \"pc:python3.pc\"], [\"bin:python\", \"pc:python.pc\"]], \"build\": [], \"manage\": [] }")); check(manifest.dependencies.runtime.is_grouped, "nested runtime phase is grouped"); check(!manifest.dependencies.build.is_grouped, "other phases stay flat"); var groups = manifest.dependencies.runtime.ordered_groups(); check(groups.length == 2, "both candidate groups parse"); check(groups[0].to_string(r => r.to_string(), ",") == "bin:python3,pc:python3.pc", "group order follows manifest order"); check(groups[1].length == 2, "second group members parse"); check(manifest.dependencies.runtime.ordered_all_refs().length == 4, "all_refs unions across groups"); check(manifest.dependencies.runtime.required.count() == 0, "grouped phase keeps the flat required set empty"); } void test_dependencies_mixed_rejected() throws Error { string[] phases = { "runtime", "build", "manage", "acquire" }; var rejected = 0; foreach(var phase in phases) { try { manifest_from_json(PHASE_MANIFEST.printf(depends_with(phase, "[\"bin:bash\", [\"bin:other\"]]"))); print("FAIL mixed %s phase should not parse\n", phase); failures++; } catch(Usm.ManifestError e) { check(e.message.contains(phase), @"mixed-form rejection names the phase ($phase)"); rejected++; } } check(rejected == phases.length, "every phase rejects mixed flat and nested forms"); } void test_dependencies_empty_group() throws Error { try { manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": [[]], \"build\": [], \"manage\": [] }")); check(false, "empty candidate group is rejected"); } catch(Usm.ManifestError e) { check(e.message.contains("runtime"), "empty-group rejection names the phase"); } try { manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": [[\"bin:bash\"], []], \"build\": [], \"manage\": [] }")); check(false, "empty candidate group among groups is rejected"); } catch(Usm.ManifestError e) { check(e.message.contains("empty"), "empty-group rejection explains itself"); } try { manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": [[[\"bin:bash\"]]], \"build\": [], \"manage\": [] }")); check(false, "nesting deeper than groups is rejected"); } catch(Usm.ManifestError e) { check(true, "nesting deeper than groups is rejected"); } try { manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": \"bin:bash\", \"build\": [], \"manage\": [] }")); check(false, "non-array phase value is rejected"); } catch(Usm.ManifestError e) { check(e.message.contains("runtime"), "non-array phase rejection names the phase"); } var empty_outer = manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": [], \"build\": [], \"manage\": [] }")); check(!empty_outer.dependencies.runtime.is_grouped && empty_outer.dependencies.runtime.required.count() == 0, "empty array parses as a flat phase with no requirements"); } void test_dependencies_roundtrip() throws Error { var nested = manifest_from_json(PHASE_MANIFEST.printf( "{ \"runtime\": [[\"bin:python3\", \"pc:python3.pc\"], [\"bin:python\"]], \"build\": [\"bin:make\"], \"manage\": [] }")); var properties = Usm.Manifest.get_mapper().map_from(nested); var serialised = ((!)new JsonElement.from_properties(properties)).stringify_pretty(); var reparsed = manifest_from_json(serialised); check(reparsed.dependencies.runtime.is_grouped, "grouped phase survives serialisation"); check(reparsed.dependencies.runtime.ordered_groups().length == 2, "group count survives serialisation"); check(reparsed.dependencies.runtime.ordered_groups()[0].to_string(r => r.to_string(), ",") == "bin:python3,pc:python3.pc", "group members survive serialisation"); check(reparsed.dependencies.build.ordered_required().first().to_string() == "bin:make", "flat phase survives serialisation"); check(!reparsed.dependencies.runtime.is_grouped == false, "grouped marker consistent after roundtrip"); var flat = manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": [\"bin:bash\"], \"build\": [], \"manage\": [] }")); var flat_serialised = ((!)new JsonElement.from_properties(Usm.Manifest.get_mapper().map_from(flat))).stringify_pretty(); var flat_reparsed = manifest_from_json(flat_serialised); check(!flat_reparsed.dependencies.runtime.is_grouped && flat_reparsed.dependencies.runtime.required.count() == 1, "flat phase roundtrips as flat"); } // ---- Topological ordering ---------------------------------------------------- /** A package manifest named pkg- providing bin:pkg- with the given runtime refs. */ Usm.AbstractPackage topo_package(string name, string[] runtime_refs) throws Error { var refs = new StringBuilder(); foreach(var resource_ref in runtime_refs) { if(refs.len > 0) { refs.append(","); } refs.append_printf("\"%s\"", resource_ref); } var json = """ { "name": "pkg-%s", "version": "1.0.0", "summary": "topo", "licences": [], "flags": [], "provides": { "bin:pkg-%s": "as-expected" }, "depends": { "runtime": [%s], "build": [], "manage": [] }, "execs": {} } """.printf(name, name, refs.str); return new Usm.AbstractPackage.from_manifest(manifest_from_json(json)); } void test_topological_order_linear() throws Error { var a = topo_package("a", {}); var b = topo_package("b", { "bin:pkg-a" }); var c = topo_package("c", { "bin:pkg-b" }); var order = Usm.Resolver.topological_order(Iterate.these(a, b, c)); check(order.to_string(p => p.manifest.name, ",") == "pkg-a,pkg-b,pkg-c", "linear chain orders deps first"); } void test_topological_order_diamond() throws Error { var a = topo_package("a", {}); var b = topo_package("b", { "bin:pkg-a" }); var c = topo_package("c", { "bin:pkg-a" }); var d = topo_package("d", { "bin:pkg-b", "bin:pkg-c" }); var order = Usm.Resolver.topological_order(Iterate.these(d, c, b, a)); check(order.to_string(p => p.manifest.name, ",") == "pkg-a,pkg-b,pkg-c,pkg-d", "diamond orders the shared dep first and the dependent last"); } void test_topological_order_name_tiebreak() throws Error { var z = topo_package("z", { "bin:pkg-c", "bin:pkg-b" }); var c = topo_package("c", {}); var b = topo_package("b", {}); var order = Usm.Resolver.topological_order(Iterate.these(z, c, b)); check(order.to_string(p => p.manifest.name, ",") == "pkg-b,pkg-c,pkg-z", "independent packages tie-break by name"); } void test_topological_order_cycle() throws Error { var x = topo_package("x", { "bin:pkg-y" }); var y = topo_package("y", { "bin:pkg-x" }); try { Usm.Resolver.topological_order(Iterate.these(x, y)); check(false, "cycle is a hard error"); } catch(Usm.ResolverError e) { check(e.message.contains("pkg-x") && e.message.contains("pkg-y"), "cycle error names the packages in the cycle"); check(e.message.contains("->"), "cycle error shows the cycle path"); } } // ---- Resolver: USM-only resolution, SPM precedence, group selection ---------- /** A tiny fake system package manager helper honouring the query/install contracts. */ const string SPM_STUB_SCRIPT = """#!/bin/bash mode="$1"; shift echo "$mode $*" >> "$USM_SPM_LOG" case "$mode" in query) python3 - "$USM_SPM_FIXTURE" "$@" <<'PY' import json, sys with open(sys.argv[1]) as handle: fixture = json.load(handle) refs = sys.argv[2:] packages = fixture.get("packages", []) known = [p for p in packages if any(r in p["resources"] for r in refs)] provided = set() for package in known: provided.update(package["resources"]) print(json.dumps({"not-found": [r for r in refs if r not in provided], "packages": known})) PY ;; install) total=$# echo "{\"type\":\"begin\",\"total\":$total}" n=0 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-complete\",\"name\":\"$name\"}" done echo "{\"type\":\"complete\",\"status\":\"ok\",\"installed\":$total}" ;; *) exit 2 ;; esac """; string make_scratch() throws Error { var dir = File.new_build_filename("/tmp", Uuid.string_random()); dir.make_directory(); return dir.get_path(); } /** One candidate entry in the fake fixture's package table. */ string spm_candidate(string name, string resource, int deps, int installed) { return "{\"name\": \"%s\", \"resources\": [\"%s\"], \"dependency-count\": %d, \"installed-dependency-count\": %d}".printf(name, resource, deps, installed); } Usm.SystemPackageManager stub_manager(string scratch, string candidates_json) throws Error { var stub_path = Path.build_filename(scratch, "spm-stub.sh"); FileUtils.set_contents(stub_path, SPM_STUB_SCRIPT); FileUtils.chmod(stub_path, 0755); var fixture_path = Path.build_filename(scratch, "fixture.json"); FileUtils.set_contents(fixture_path, @"{ \"packages\": [$candidates_json] }"); Environment.set_variable("USM_SPM_FIXTURE", fixture_path, true); Environment.set_variable("USM_SPM_LOG", Path.build_filename(scratch, "query.log"), true); Environment.set_variable("USM_SPM_INSTALL_LOG", Path.build_filename(scratch, "install.log"), true); var config_dir = Path.build_filename(scratch, "config"); DirUtils.create(config_dir, 0700); FileUtils.set_contents(Path.build_filename(config_dir, "usm.config"), "{\"is_managed\": false, \"system_package_manager\": { \"query\": [\"%s\", \"query\"], \"install\": [\"%s\", \"install\"] }}".printf(stub_path, stub_path)); var paths = new Usm.Paths.defaults(); paths.usm_config_dir = config_dir; return new Usm.SystemPackageManager(new Usm.Configuration.from_paths(paths)); } /** Builds a .usmc archive for pkg- providing bin:pkg- with the given runtime refs. */ string make_package_archive(string scratch, string name, string[] runtime_refs) throws Error { var source = Path.build_filename(scratch, @"src-$name"); 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 + "]"))); var archive = Path.build_filename(scratch, @"pkg-$name.usmc"); Usm.Util.archive(source, archive); return archive; } /** * Builds a .usmc archive for pkg- providing provides_key with the * given BUILD-phase refs (used for lot-admission fixtures). */ string make_build_package_archive(string scratch, string name, string provides_key, string[] build_refs) throws Error { var source = Path.build_filename(scratch, @"src-$name"); DirUtils.create(source, 0755); var refs = new StringBuilder(); foreach(var resource_ref in build_refs) { if(refs.len > 0) { refs.append(","); } refs.append_printf("\"%s\"", resource_ref); } FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"), """ { "name": "pkg-%s", "version": "1.0.0", "summary": "lots", "licences": [], "flags": [], "provides": { "%s": "as-expected" }, "depends": { "runtime": [], "build": [%s], "manage": [] }, "execs": {} } """.printf(name, provides_key, refs.str)); var archive = Path.build_filename(scratch, @"pkg-$name.usmc"); Usm.Util.archive(source, archive); return archive; } /** Caches pkg-'s archive as a CachedPackage and returns it. */ Usm.CachedPackage cache_package(string scratch, string name) throws Error { var cache_path = Path.build_filename(scratch, @"cache-pkg-$name-1.0.0"); DirUtils.create(cache_path, 0755); File.new_for_path(Path.build_filename(scratch, @"pkg-$name.usmc")) .copy(File.new_for_path(Path.build_filename(cache_path, "package.usmc")), FileCopyFlags.OVERWRITE); return new Usm.CachedPackage(cache_path); } Usm.ResolutionResult resolve_manifest(Usm.SystemPackageManager? spm, string manifest_json) throws Error { var resolver = new Usm.Resolver(new Usm.ResourceFinder()); var roots = new Vector(); roots.add(new Usm.AbstractPackage.from_manifest(manifest_from_json(manifest_json))); return resolver.resolve(roots, spm); } void test_resolve_diamond_usm_only() throws Error { var scratch = make_scratch(); var resolver = new Usm.Resolver(new Usm.ResourceFinder()); resolver.supply_package(make_package_archive(scratch, "a", {})); resolver.supply_package(make_package_archive(scratch, "b", { "bin:pkg-a" })); resolver.supply_package(make_package_archive(scratch, "c", { "bin:pkg-a" })); resolver.supply_package(make_package_archive(scratch, "d", { "bin:pkg-b", "bin:pkg-c" })); var roots = new Vector(); roots.add(((!)resolver.find_package("pkg-d"))); var result = resolver.resolve(roots, null); check(result.packages.count() == 4, "USM-only resolution closes the diamond"); check(result.install_order.to_string(p => p.manifest.name, ",") == "pkg-a,pkg-b,pkg-c,pkg-d", "install order is dependencies-first"); check(result.removal_order.to_string(p => p.manifest.name, ",") == "pkg-d,pkg-c,pkg-b,pkg-a", "removal order is the reverse of install order"); check(result.system_packages.length == 0, "no system packages are chosen without an SPM"); 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", {})); var roots = new Vector(); roots.add(new Usm.AbstractPackage.from_manifest(manifest_from_json( APP_MANIFEST.printf("spm-app", "spm-app", depends_with("runtime", "[\"bin:pkg-x\"]"))))); var spm = stub_manager(scratch, spm_candidate("spm-owns-x", "bin:pkg-x", 3, 1)); var result = resolver.resolve(roots, spm); check(result.system_packages.length == 1 && result.system_packages[0].name == "spm-owns-x", "a system package wins over an equally-good USM provider"); check(result.packages.count() == 1, "the USM provider is not pulled when the SPM satisfies the ref"); Usm.Util.delete_tree(scratch); } void test_group_selection_cheaper_wins() throws Error { var scratch = make_scratch(); var spm = stub_manager(scratch, spm_candidate("expensive", "bin:usmt-a1", 5, 0) + "," + spm_candidate("cheap", "bin:usmt-b1", 5, 3)); var result = resolve_manifest(spm, APP_MANIFEST.printf("grouped-app", "grouped-app", depends_with("runtime", "[[\"bin:usmt-a1\"], [\"bin:usmt-b1\"]]"))); check(result.system_packages.length == 1 && result.system_packages[0].name == "cheap", "the group whose candidate costs fewer NEW installs wins (dependency-count minus installed-dependency-count)"); Usm.Util.delete_tree(scratch); } void test_group_selection_tie_manifest_order() throws Error { var scratch = make_scratch(); var spm = stub_manager(scratch, spm_candidate("first-pkg", "bin:usmt-t1", 2, 0) + "," + spm_candidate("second-pkg", "bin:usmt-t2", 2, 0)); var result = resolve_manifest(spm, APP_MANIFEST.printf("grouped-app", "grouped-app", depends_with("runtime", "[[\"bin:usmt-t1\"], [\"bin:usmt-t2\"]]"))); check(result.system_packages.length == 1 && result.system_packages[0].name == "first-pkg", "equal-cost groups tie-break to manifest order"); Usm.Util.delete_tree(scratch); } void test_group_selection_none_viable_itemised() throws Error { var scratch = make_scratch(); var spm = stub_manager(scratch, spm_candidate("irrelevant", "bin:usmt-other", 1, 0)); try { resolve_manifest(spm, APP_MANIFEST.printf("grouped-app", "grouped-app", depends_with("runtime", "[[\"bin:usmt-a1\", \"bin:usmt-a2\"], [\"bin:usmt-b1\"]]"))); check(false, "no viable group is a hard error"); } catch(Usm.ResolverError e) { check(e.message.contains("Group 1") && e.message.contains("Group 2"), "itemised failure lists every group"); check(e.message.contains("bin:usmt-a1") && e.message.contains("bin:usmt-a2") && e.message.contains("bin:usmt-b1"), "itemised failure lists every missing ref"); check(e.message.contains("no system package provides it"), "itemised failure explains why each ref failed"); } Usm.Util.delete_tree(scratch); } void test_group_selection_single_batched_query() throws Error { var scratch = make_scratch(); var spm = stub_manager(scratch, spm_candidate("first-pkg", "bin:usmt-q1", 1, 0) + "," + spm_candidate("second-pkg", "bin:usmt-q2", 2, 0) + "," + spm_candidate("builddep-pkg", "bin:usmt-build1", 1, 0)); resolve_manifest(spm, APP_MANIFEST.printf("grouped-app", "grouped-app", "{ \"runtime\": [[\"bin:usmt-q1\"], [\"bin:usmt-q2\", \"bin:usmt-q3\"]], \"build\": [\"bin:usmt-build1\"], \"manage\": [] }")); string log; FileUtils.get_contents(Path.build_filename(scratch, "query.log"), out log); var queries = log.split("\n"); var query_lines = 0; var query_line = ""; foreach(var line in queries) { if(line.has_prefix("query")) { query_lines++; query_line = line; } } check(query_lines == 1, "resolution issues exactly one batched SPM query"); check(query_line.contains("bin:usmt-q1") && query_line.contains("bin:usmt-q2") && query_line.contains("bin:usmt-q3") && query_line.contains("bin:usmt-build1"), "the batched query covers refs from every group and phase"); Usm.Util.delete_tree(scratch); } void test_system_package_manager_install_stub() throws Error { var scratch = make_scratch(); var spm = stub_manager(scratch, ""); var names = new Vector(); names.add("one"); names.add("two"); var begin_events = 0; var complete_events = 0; var installed_count = 0; var install_ok = false; var loop = new MainLoop(); spm.install.begin(names, event => { if(event.event_type == Usm.SystemInstallEventType.BEGIN) { begin_events++; } if(event.event_type == Usm.SystemInstallEventType.COMPLETE) { complete_events++; installed_count = event.installed; } }, (obj, res) => { try { install_ok = spm.install.end(res); } catch(Error e) { install_ok = false; } loop.quit(); }); loop.run(); check(install_ok, "stub install transaction reports success"); check(begin_events == 1 && complete_events == 1, "begin and complete events stream once each"); check(installed_count == 2, "complete event carries the installed count"); string 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"); Usm.Util.delete_tree(scratch); } void test_transaction_accepts_orders() throws Error { var scratch = make_scratch(); make_package_archive(scratch, "a", {}); make_package_archive(scratch, "b", { "bin:pkg-a" }); make_package_archive(scratch, "c", { "bin:pkg-a" }); var to_install = new HashSet(); var to_remove = new HashSet(); foreach(var name in new string[] { "a", "b", "c" }) { var cache_path = Path.build_filename(scratch, @"cache-pkg-$name-1.0.0"); DirUtils.create(cache_path, 0755); File.new_for_path(Path.build_filename(scratch, @"pkg-$name.usmc")) .copy(File.new_for_path(Path.build_filename(cache_path, "package.usmc")), FileCopyFlags.OVERWRITE); var cached = new Usm.CachedPackage(cache_path); to_install.add(cached); to_remove.add(cached); } var install_order = new Vector(); install_order.add("pkg-c"); install_order.add("pkg-a"); install_order.add("pkg-b"); var remove_order = new Vector(); remove_order.add("pkg-b"); remove_order.add("pkg-c"); remove_order.add("pkg-a"); var transaction = new Usm.Transaction() { paths = new Usm.Paths(), resource_finder = new Usm.ResourceFinder(), to_install = to_install, to_remove = to_remove, install_order = install_order, remove_order = remove_order }; transaction.strategise(); check(transaction.install_lots.length == 1, "packages without installtime deps form one lot"); check(transaction.install_lots[0].to_string(p => p.package_name, ",") == "cache-pkg-c-1.0.0,cache-pkg-a-1.0.0,cache-pkg-b-1.0.0", "the transaction installs in the supplied resolution order"); check(transaction.removal_order.to_string(p => p.package_name, ",") == "cache-pkg-b-1.0.0,cache-pkg-c-1.0.0,cache-pkg-a-1.0.0", "the transaction removes in the supplied reverse order"); var short_order = new Vector(); short_order.add("pkg-a"); var partial = new Usm.Transaction() { paths = new Usm.Paths(), resource_finder = new Usm.ResourceFinder(), to_install = to_install, to_remove = new HashSet(), install_order = short_order }; try { partial.strategise(); check(false, "an order missing packages is rejected"); } catch(Usm.TransactionError e) { check(e.message.contains("pkg-b") || e.message.contains("pkg-c"), "the rejection names the uncovered packages"); } Usm.Util.delete_tree(scratch); } void test_transaction_lots_split_at_build_boundaries() throws Error { var scratch = make_scratch(); make_build_package_archive(scratch, "a", "pc:lib-lot-a.pc", {}); make_build_package_archive(scratch, "b", "pc:lib-lot-b.pc", { "pc:lib-lot-a.pc" }); make_build_package_archive(scratch, "c", "pc:lib-lot-c.pc", { "pc:lib-lot-a.pc" }); make_build_package_archive(scratch, "d", "pc:lib-lot-d.pc", { "pc:lib-lot-b.pc", "pc:lib-lot-c.pc" }); var to_install = new HashSet(); foreach(var name in new string[] { "a", "b", "c", "d" }) { 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() }; transaction.strategise(); check(transaction.install_lots.length == 3, "a build-dependency diamond splits into one lot per build level"); check(transaction.install_lots[0].to_string(p => p.package_name, ",") == "cache-pkg-a-1.0.0", "the provider of the shared pc: file builds and installs alone in lot 1"); check(transaction.install_lots[1].to_string(p => p.package_name, ",") == "cache-pkg-b-1.0.0,cache-pkg-c-1.0.0", "packages whose builds need lot 1's install share lot 2"); check(transaction.install_lots[2].to_string(p => p.package_name, ",") == "cache-pkg-d-1.0.0", "the diamond tip waits for both lot 2 installs"); Usm.Util.delete_tree(scratch); } void test_transaction_manage_deps_admit_same_lot() throws Error { var scratch = make_scratch(); make_package_archive(scratch, "m1", {}); // m2's manage phase needs m1's bin:, but its build phase needs // nothing from the transaction — both belong in one lot because the // manage executable runs at install time, after m1 installs var source = Path.build_filename(scratch, "src-m2"); DirUtils.create(source, 0755); FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"), APP_MANIFEST.printf("pkg-m2", "pkg-m2", depends_with("manage", "[\"bin:pkg-m1\"]"))); var to_install = new HashSet(); to_install.add(cache_package(scratch, "m1")); var m2_archive = Path.build_filename(scratch, "pkg-m2.usmc"); Usm.Util.archive(source, m2_archive); to_install.add(cache_package(scratch, "m2")); var transaction = new Usm.Transaction() { paths = new Usm.Paths(), resource_finder = new Usm.ResourceFinder(), to_install = to_install, to_remove = new HashSet() }; transaction.strategise(); check(transaction.install_lots.length == 1, "manage-phase refs satisfiable within the lot keep one lot"); check(transaction.install_lots[0].to_string(p => p.package_name, ",") == "cache-pkg-m1-1.0.0,cache-pkg-m2-1.0.0", "the manage dependency orders its provider first inside the lot"); Usm.Util.delete_tree(scratch); } // ---- rebuildDependants: lookup, planning, dedup ------------------------------ /** Manifest for a package named at providing one key with the given flags and depends. */ const string REBUILD_MANIFEST = """ { "name": "%s", "version": "%s", "summary": "rebuild fixtures", "licences": [], "flags": [%s], "provides": { "%s": "as-expected" }, "depends": %s, "execs": {} } """; /** * Caches a package under /packages/- built * from {@link REBUILD_MANIFEST}, optionally marks it installed, and * returns its {@link Usm.CachedPackage}. */ Usm.CachedPackage scratch_package(string state_dir, string name, string version, string provides, string flags, string depends, bool installed) throws Error { var source = Path.build_filename(state_dir, @"src-$name-$version"); DirUtils.create(source, 0755); FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"), REBUILD_MANIFEST.printf(name, version, flags, provides, depends)); var cache_path = Path.build_filename(state_dir, "packages", @"$name-$version"); DirUtils.create_with_parents(cache_path, 0755); Usm.Util.archive(source, Path.build_filename(cache_path, "package.usmc")); Usm.Util.delete_tree(source); if(installed) { File.new_build_filename(state_dir, "installed", @"$name-$version").make_symbolic_link(cache_path); } return new Usm.CachedPackage(cache_path); } /** A managed {@link Usm.SystemState} over a scratch tree: usm.config plus empty state directories. */ Usm.SystemState make_state(string scratch) throws Error { var config_dir = Path.build_filename(scratch, "config"); var state_dir = Path.build_filename(scratch, "state"); DirUtils.create_with_parents(config_dir, 0700); DirUtils.create_with_parents(state_dir, 0700); foreach(var part in new string[] { "packages", "installed", "lists" }) { DirUtils.create(Path.build_filename(state_dir, part), 0700); } FileUtils.set_contents(Path.build_filename(config_dir, "usm.config"), "{\"is_managed\": true, \"managed\": {\"state_path\": \"%s\"}}".printf(state_dir)); var paths = new Usm.Paths(); paths.usm_config_dir = config_dir; return new Usm.SystemState(paths); } void test_find_dependant_names() throws Error { var scratch = make_scratch(); var state = make_state(scratch); var state_dir = Path.build_filename(scratch, "state"); var updated = scratch_package(state_dir, "dep-a", "1.0.0", "pc:dep-a.pc", "", depends_with("runtime", "[]"), false); // an older installed dep-a depending on its own provides is not its own dependant scratch_package(state_dir, "dep-a", "0.9.0", "pc:dep-a.pc", "", depends_with("runtime", "[\"pc:dep-a.pc\"]"), true); scratch_package(state_dir, "dep-b", "0.1", "bin:dep-b", "", depends_with("build", "[\"pc:dep-a.pc\"]"), true); scratch_package(state_dir, "dep-c", "2.0", "bin:dep-c", "", "{ \"runtime\": [[\"pc:dep-a.pc\", \"opt:other\"], [\"pc:elsewhere.pc\"]], \"build\": [], \"manage\": [] }", true); scratch_package(state_dir, "unrelated", "1.0", "bin:unrelated", "", depends_with("runtime", "[\"bin:bash\"]"), true); var dependants = state.find_dependant_names(updated); check(dependants.length == 2, "exactly the two dependants are found"); check(dependants.any(n => n == "dep-b-0.1"), "a build-phase dependant is found"); check(dependants.any(n => n == "dep-c-2.0"), "a grouped-phase dependant is found via any candidate group"); check(dependants.no(n => n == "unrelated-1.0"), "a package with no dependency on the update is not found"); check(dependants.no(n => n == "dep-a-0.9.0"), "an older installed version of the update is not its own dependant"); // resource-level matching: a canonlib: provide satisfies a lib: dependency var canonical = scratch_package(state_dir, "dep-libc", "1.0", "canonlib:dep-lib.so", "", depends_with("runtime", "[]"), false); scratch_package(state_dir, "dep-lib", "3.0", "lib:dep-lib.so", "", depends_with("runtime", "[\"lib:dep-lib.so\"]"), true); check(state.find_dependant_names(canonical).any(n => n == "dep-lib-3.0"), "a canonlib provide satisfies a lib dependency"); Usm.Util.delete_tree(scratch); } void test_rebuild_planning() throws Error { var scratch = make_scratch(); var state = make_state(scratch); var state_dir = Path.build_filename(scratch, "state"); // two flagged providers, each with an incoming update scratch_package(state_dir, "liba", "1.0.0", "pc:liba.pc", "\"rebuildDependants\"", depends_with("runtime", "[]"), true); var liba_update = scratch_package(state_dir, "liba", "2.0.0", "pc:liba.pc", "\"rebuildDependants\"", depends_with("runtime", "[]"), false); scratch_package(state_dir, "libx", "1.0.0", "pc:libx.pc", "\"rebuildDependants\"", depends_with("runtime", "[]"), true); var libx_update = scratch_package(state_dir, "libx", "2.0.0", "pc:libx.pc", "\"rebuildDependants\"", depends_with("runtime", "[]"), false); // statum depends on liba in its build phase and libx at runtime scratch_package(state_dir, "statum", "0.1", "bin:statum", "", "{ \"runtime\": [\"pc:libx.pc\"], \"build\": [\"pc:liba.pc\"], \"manage\": [] }", true); // client 1.0.0 depends on liba, but client 2.0.0 joins the transaction explicitly scratch_package(state_dir, "client", "1.0.0", "bin:client", "", depends_with("build", "[\"pc:liba.pc\"]"), true); var client_update = scratch_package(state_dir, "client", "2.0.0", "bin:client", "", depends_with("build", "[\"pc:liba.pc\"]"), false); // libz updates without the flag, so its dependant must NOT rebuild var libz_update = scratch_package(state_dir, "libz", "2.0.0", "pc:libz.pc", "", depends_with("runtime", "[]"), false); scratch_package(state_dir, "user", "1.0.0", "bin:user", "", depends_with("build", "[\"pc:libz.pc\"]"), true); var dependants = state.find_dependant_names(liba_update); check(dependants.length == 2 && dependants.any(n => n == "statum-0.1") && dependants.any(n => n == "client-1.0.0"), "the lookup sees both of liba's installed dependants before filtering"); var to_install = new HashSet(); to_install.add(liba_update); to_install.add(libx_update); to_install.add(client_update); to_install.add(libz_update); var transaction = new Usm.Transaction() { paths = new Usm.Paths(), resource_finder = new Usm.ResourceFinder(), to_install = to_install, to_remove = new HashSet(), state = state }; transaction.strategise(); check(transaction.rebuilds.length == 1, "one rebuild is planned across two flagged providers"); check(transaction.rebuilds[0].package.package_name == "statum-0.1", "the shared dependant statum rebuilds exactly once"); check(transaction.rebuilds[0].trigger.package_name == "liba-2.0.0" || transaction.rebuilds[0].trigger.package_name == "libx-2.0.0", "the rebuild records the flagged trigger"); check(transaction.rebuilds[0].package.state_path == Path.build_filename(state_dir, "packages", "statum-0.1"), "the rebuild reuses the installed package's own state path"); var rebuilt = new HashSet(); foreach(var entry in transaction.rebuilds) { rebuilt.add(entry.package.package_name); } check(!rebuilt.has("client-1.0.0"), "a dependant already in to_install is not added as a rebuild"); check(!rebuilt.has("user-1.0.0"), "dependants of unflagged providers are not rebuilt"); var lot_names = new StringBuilder(); foreach(var lot in transaction.install_lots) { lot_names.append(lot.to_string(p => p.package_name, ",")); } check(!lot_names.str.contains("statum"), "a rebuild target never also builds through the install lots"); Usm.Util.delete_tree(scratch); } // ---- explicit-install tracking ------------------------------------------------ void test_origin_information_explicit_install() throws Error { var scratch = make_scratch(); var cache_path = Path.build_filename(scratch, "cache-explicit-pkg-1.0.0"); DirUtils.create(cache_path, 0755); var cached = new Usm.CachedPackage(cache_path); var explicit_record = new Usm.OriginInformation() { repository = "repo", listfile = "2020-03-20T14:34:42.382748.usml", original_path = "explicit-pkg-1.0.0.usmc", signature_verified = true, explicitly_installed = true }; cached.update_origin_information(explicit_record); var read_explicit = cached.get_origin_information(); check(read_explicit.explicitly_installed, "explicitly_installed true survives an origin-info round trip"); check(read_explicit.repository == "repo" && read_explicit.signature_verified, "provenance fields survive the round trip"); var implicit_record = new Usm.OriginInformation(); implicit_record.explicitly_installed = false; cached.update_origin_information(implicit_record); var read_implicit = cached.get_origin_information(); check(!read_implicit.explicitly_installed, "explicitly_installed false survives an origin-info round trip"); check(read_implicit.repository == null, "a transaction-written record without provenance round-trips null fields"); FileUtils.set_contents(Path.build_filename(cache_path, "origin-info"), "{\"repository\": \"repo\", \"listfile\": \"2020-03-20T14:34:42.382748.usml\", \"original_path\": \"explicit-pkg-1.0.0.usmc\", \"signature_verified\": true}"); check(!cached.get_origin_information().explicitly_installed, "an origin-info file without the field defaults to false"); Usm.Util.delete_tree(scratch); } /** No-op build script: the install pipeline only needs it to succeed. */ const string NOOP_BUILD_SCRIPT = """#!/bin/bash exit 0 """; /** * Caches a build-only package named at 1.0.0 with a * succeeding build script under 's managed state and * returns its cache path. */ string explicit_flag_package(string scratch, string name) throws Error { var cache_path = Path.build_filename(scratch, "state", "packages", @"$name-1.0.0"); DirUtils.create_with_parents(cache_path, 0755); var source = Path.build_filename(scratch, @"src-$name"); DirUtils.create(source, 0755); FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"), BUILD_MANIFEST.printf(name, "1.0.0")); FileUtils.set_contents(Path.build_filename(source, "build.sh"), NOOP_BUILD_SCRIPT); FileUtils.chmod(Path.build_filename(source, "build.sh"), 0755); Usm.Util.archive(source, Path.build_filename(cache_path, "package.usmc")); Usm.Util.delete_tree(source); return cache_path; } void test_transaction_records_explicit_flags() throws Error { var original_dir = Environment.get_current_dir(); var scratch = make_scratch(); var state = make_state(scratch); var named_path = explicit_flag_package(scratch, "flag-named"); var resolved_path = explicit_flag_package(scratch, "flag-resolved"); var to_install = new HashSet(); to_install.add(new Usm.CachedPackage(named_path)); to_install.add(new Usm.CachedPackage(resolved_path)); var explicit_packages = new Vector(); explicit_packages.add("flag-named"); var transaction = new Usm.Transaction() { paths = scratch_paths(scratch), resource_finder = new Usm.ResourceFinder(), to_install = to_install, to_remove = new HashSet(), state = state, explicit_packages = explicit_packages }; transaction.run(); check(new Usm.CachedPackage(named_path).get_origin_information().explicitly_installed, "a package named in explicit_packages installs with the explicit mark"); check(!new Usm.CachedPackage(resolved_path).get_origin_information().explicitly_installed, "a package only the resolution pulled in installs without the explicit mark"); var rebuild = new Usm.Transaction() { paths = scratch_paths(scratch), resource_finder = new Usm.ResourceFinder(), to_install = new HashSet(), to_remove = new HashSet(), state = state }; rebuild.rebuild_package(new Usm.CachedPackage(named_path)); check(new Usm.CachedPackage(named_path).get_origin_information().explicitly_installed, "a rebuild of the same cache directory keeps the explicit mark"); Environment.set_current_dir(original_dir); Usm.Util.delete_tree(scratch); } // ---- build archive restoration + clean retry --------------------------------- /** Manifest for a build-only package: an executable build script, no provides. */ const string BUILD_MANIFEST = """ { "name": "%s", "version": "%s", "summary": "build cache fixture", "licences": [], "flags": [], "provides": {}, "depends": { "runtime": [], "build": [], "manage": [] }, "execs": { "build": "build.sh" } } """; /** Always fails; logs whether the build directory carried the archived marker. */ const string FAILING_BUILD_SCRIPT = """#!/bin/bash if [ -f "$1/from-archive" ]; then echo restored >> "$USM_TEST_BUILD_LOG" else echo fresh >> "$USM_TEST_BUILD_LOG" fi exit 1 """; /** Succeeds only against the restored archive, so a retry (fresh) fails. */ const string INCREMENTAL_BUILD_SCRIPT = """#!/bin/bash if [ -f "$1/from-archive" ]; then echo restored >> "$USM_TEST_BUILD_LOG" exit 0 fi echo fresh >> "$USM_TEST_BUILD_LOG" exit 1 """; /** Paths with the destination rooted in so nothing touches the real filesystem. */ Usm.Paths scratch_paths(string scratch) { var paths = new Usm.Paths(); paths.destination = Path.build_filename(scratch, "dest"); return paths; } /** * Caches a build-only package named running