|
@@ -0,0 +1,2333 @@
|
|
|
|
|
+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<Invercargill.Properties>());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 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");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- Repository listing size field -------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ void test_repository_listing_entry_size() throws Error {
|
|
|
|
|
+ var mapper = Usm.RepositoryListingEntry.get_mapper();
|
|
|
|
|
+ var entry = new Usm.RepositoryListingEntry() {
|
|
|
|
|
+ path = "sized-pkg-1.0.0.usmc",
|
|
|
|
|
+ manifest = manifest_from_json(APP_MANIFEST.printf("sized-pkg", "sized-pkg", depends_with("runtime", "[]"))),
|
|
|
|
|
+ sha512sum = Wrap.byte_array({ 1, 2, 3 }),
|
|
|
|
|
+ size = 4096
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ var serialised = ((!)new JsonElement.from_properties(mapper.map_from(entry))).stringify_pretty();
|
|
|
|
|
+ check(serialised.contains("\"size\"") && serialised.contains("4096"), "size serialises into the listing entry");
|
|
|
|
|
+
|
|
|
|
|
+ var reparsed = mapper.materialise(new JsonElement.from_string(serialised).as<Invercargill.Properties>());
|
|
|
|
|
+ check(reparsed.path == "sized-pkg-1.0.0.usmc" && reparsed.size == 4096, "size round-trips through serialisation");
|
|
|
|
|
+
|
|
|
|
|
+ JsonElement removed;
|
|
|
|
|
+ var legacy = new JsonElement.from_string(serialised).as<JsonObject>();
|
|
|
|
|
+ legacy.remove("size", out removed);
|
|
|
|
|
+ var pre_upgrade = mapper.materialise(legacy);
|
|
|
|
|
+ check(pre_upgrade.size == 0, "a pre-upgrade entry without size materialises as 0");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 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 <name> providing bin:<name> 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");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 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 ----------------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ /** A package manifest named pkg-<name> providing bin:pkg-<name> 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<Usm.AbstractPackage>(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<Usm.AbstractPackage>(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<Usm.AbstractPackage>(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<Usm.AbstractPackage>(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
|
|
|
|
|
+ ;;
|
|
|
|
|
+ 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)
|
|
|
|
|
+ total=$#
|
|
|
|
|
+ echo "{\"type\":\"begin\",\"total\":$total}"
|
|
|
|
|
+ # 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
|
|
|
|
|
+ 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 "$name" >> "$USM_SPM_INSTALL_LOG"
|
|
|
|
|
+ n=$((n+1))
|
|
|
|
|
+ done
|
|
|
|
|
+ # A duplicate completion, as dnf emits for repeated members
|
|
|
|
|
+ echo "{\"type\":\"package-complete\",\"name\":\"$1\"}"
|
|
|
|
|
+ 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-<name> providing bin:pkg-<name> 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-<name> 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-<name>'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<Usm.AbstractPackage>();
|
|
|
|
|
+ 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<Usm.AbstractPackage>();
|
|
|
|
|
+ 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<Usm.AbstractPackage>();
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** 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 {
|
|
|
|
|
+ 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<string>();
|
|
|
|
|
+ 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.contains("one") && install_log.contains("two"), "install helper received both package names");
|
|
|
|
|
+
|
|
|
|
|
+ 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<Usm.CachedPackage>();
|
|
|
|
|
+ var to_remove = new HashSet<Usm.CachedPackage>();
|
|
|
|
|
+ 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<string>();
|
|
|
|
|
+ install_order.add("pkg-c");
|
|
|
|
|
+ install_order.add("pkg-a");
|
|
|
|
|
+ install_order.add("pkg-b");
|
|
|
|
|
+ var remove_order = new Vector<string>();
|
|
|
|
|
+ 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<string>();
|
|
|
|
|
+ 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<Usm.CachedPackage>(),
|
|
|
|
|
+ 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<Usm.CachedPackage>();
|
|
|
|
|
+ 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<Usm.CachedPackage>()
|
|
|
|
|
+ };
|
|
|
|
|
+ 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<Usm.CachedPackage>();
|
|
|
|
|
+ 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<Usm.CachedPackage>()
|
|
|
|
|
+ };
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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 ------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ /** Manifest for a package named <name> at <version> 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 <state_dir>/packages/<name>-<version> 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<Usm.CachedPackage>();
|
|
|
|
|
+ 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<Usm.CachedPackage>(),
|
|
|
|
|
+ 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<string>();
|
|
|
|
|
+ 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 <name> at 1.0.0 with a
|
|
|
|
|
+ * succeeding build script under <scratch>'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<Usm.CachedPackage>();
|
|
|
|
|
+ to_install.add(new Usm.CachedPackage(named_path));
|
|
|
|
|
+ to_install.add(new Usm.CachedPackage(resolved_path));
|
|
|
|
|
+
|
|
|
|
|
+ var explicit_packages = new Vector<string>();
|
|
|
|
|
+ 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<Usm.CachedPackage>(),
|
|
|
|
|
+ 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<Usm.CachedPackage>(),
|
|
|
|
|
+ to_remove = new HashSet<Usm.CachedPackage>(),
|
|
|
|
|
+ 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 <scratch> 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 <name> running <script>, optionally
|
|
|
|
|
+ * seeding its cache with a build.tar.xz holding a "from-archive" marker,
|
|
|
|
|
+ * and returns a transaction over it.
|
|
|
|
|
+ */
|
|
|
|
|
+ Usm.Transaction build_transaction(string scratch, string name, string script, bool with_archive) throws Error {
|
|
|
|
|
+ var state = make_state(scratch);
|
|
|
|
|
+ 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"), 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);
|
|
|
|
|
+
|
|
|
|
|
+ if(with_archive) {
|
|
|
|
|
+ var stale_build = Path.build_filename(scratch, "stale-build");
|
|
|
|
|
+ DirUtils.create(stale_build, 0755);
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(stale_build, "from-archive"), "stale");
|
|
|
|
|
+ Usm.Util.archive(stale_build, Path.build_filename(cache_path, "build.tar.xz"));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var to_install = new HashSet<Usm.CachedPackage>();
|
|
|
|
|
+ to_install.add(new Usm.CachedPackage(cache_path));
|
|
|
|
|
+ return new Usm.Transaction() {
|
|
|
|
|
+ paths = scratch_paths(scratch),
|
|
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
|
|
+ to_install = to_install,
|
|
|
|
|
+ to_remove = new HashSet<Usm.CachedPackage>(),
|
|
|
|
|
+ state = state
|
|
|
|
|
+ };
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_build_archive_restore_and_clean_retry() throws Error {
|
|
|
|
|
+ var original_dir = Environment.get_current_dir();
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var log_path = Path.build_filename(scratch, "build.log");
|
|
|
|
|
+ Environment.set_variable("USM_TEST_BUILD_LOG", log_path, true);
|
|
|
|
|
+
|
|
|
|
|
+ var transaction = build_transaction(scratch, "fail-pkg", FAILING_BUILD_SCRIPT, true);
|
|
|
|
|
+ var threw = false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ transaction.run();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Usm.TransactionError e) {
|
|
|
|
|
+ threw = true;
|
|
|
|
|
+ check(e.message.contains("fail-pkg"), "the clean-retry failure names the package");
|
|
|
|
|
+ }
|
|
|
|
|
+ check(threw, "a failing restored build followed by a failing clean retry fails the transaction");
|
|
|
|
|
+
|
|
|
|
|
+ string log = "";
|
|
|
|
|
+ FileUtils.get_contents(log_path, out log);
|
|
|
|
|
+ check(log == "restored\nfresh\n", "the clean retry fires once after the restored build fails");
|
|
|
|
|
+
|
|
|
|
|
+ Environment.set_current_dir(original_dir);
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_build_archive_restored_incrementally() throws Error {
|
|
|
|
|
+ var original_dir = Environment.get_current_dir();
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var log_path = Path.build_filename(scratch, "build.log");
|
|
|
|
|
+ Environment.set_variable("USM_TEST_BUILD_LOG", log_path, true);
|
|
|
|
|
+
|
|
|
|
|
+ var cache_path = Path.build_filename(scratch, "state", "packages", "inc-pkg-1.0.0");
|
|
|
|
|
+ var transaction = build_transaction(scratch, "inc-pkg", INCREMENTAL_BUILD_SCRIPT, true);
|
|
|
|
|
+ transaction.run();
|
|
|
|
|
+
|
|
|
|
|
+ string log = "";
|
|
|
|
|
+ FileUtils.get_contents(log_path, out log);
|
|
|
|
|
+ check(log == "restored\n", "a successful build runs against the restored archive without a retry");
|
|
|
|
|
+ check(File.new_for_path(Path.build_filename(cache_path, "build.tar.xz")).query_exists(),
|
|
|
|
|
+ "cleanup re-archives the build directory for the next transaction");
|
|
|
|
|
+
|
|
|
|
|
+ Environment.set_current_dir(original_dir);
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_build_failure_without_cache_propagates() throws Error {
|
|
|
|
|
+ var original_dir = Environment.get_current_dir();
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var log_path = Path.build_filename(scratch, "build.log");
|
|
|
|
|
+ Environment.set_variable("USM_TEST_BUILD_LOG", log_path, true);
|
|
|
|
|
+
|
|
|
|
|
+ var transaction = build_transaction(scratch, "clean-pkg", FAILING_BUILD_SCRIPT, false);
|
|
|
|
|
+ var threw = false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ transaction.run();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Usm.TransactionError e) {
|
|
|
|
|
+ threw = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ check(threw, "a failing already-clean build propagates without a retry");
|
|
|
|
|
+
|
|
|
|
|
+ string log = "";
|
|
|
|
|
+ FileUtils.get_contents(log_path, out log);
|
|
|
|
|
+ check(log == "fresh\n", "a build without any cache is attempted exactly once");
|
|
|
|
|
+
|
|
|
|
|
+ Environment.set_current_dir(original_dir);
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- Transaction rollback journal --------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * Caches a package whose build script produces the provided `bin:`
|
|
|
|
|
+ * payload from the build directory (or fails, or produces nothing,
|
|
|
|
|
+ * per {@link script}), optionally marking it installed.
|
|
|
|
|
+ */
|
|
|
|
|
+ Usm.CachedPackage rollback_fixture_package(string state_dir, string name, string version, string script, string[] build_refs, bool installed) throws Error {
|
|
|
|
|
+ var source = Path.build_filename(state_dir, @"src-$name-$version");
|
|
|
|
|
+ DirUtils.create_with_parents(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": "%s",
|
|
|
|
|
+ "version": "%s",
|
|
|
|
|
+ "summary": "rollback fixture",
|
|
|
|
|
+ "licences": [],
|
|
|
|
|
+ "flags": [],
|
|
|
|
|
+ "provides": { "bin:%s": "build:out" },
|
|
|
|
|
+ "depends": { "runtime": [], "build": [%s], "manage": [] },
|
|
|
|
|
+ "execs": { "build": "build.sh" }
|
|
|
|
|
+ }
|
|
|
|
|
+ """.printf(name, version, name, refs.str));
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(source, "build.sh"), script);
|
|
|
|
|
+ FileUtils.chmod(Path.build_filename(source, "build.sh"), 0755);
|
|
|
|
|
+
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const string ROLLBACK_FAILING_BUILD = """#!/bin/bash
|
|
|
|
|
+exit 1
|
|
|
|
|
+""";
|
|
|
|
|
+
|
|
|
|
|
+ /** Builds cleanly but produces nothing, so the install step fails. */
|
|
|
|
|
+ const string ROLLBACK_EMPTY_BUILD = """#!/bin/bash
|
|
|
|
|
+exit 0
|
|
|
|
|
+""";
|
|
|
|
|
+
|
|
|
|
|
+ string rollback_payload_build(string payload) {
|
|
|
|
|
+ return "#!/bin/bash\nprintf '%s' > \"$1/out\"\n".printf(payload);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_journal_entry_complete_rollback_roundtrip() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+
|
|
|
|
|
+ var package_root = Path.build_filename(scratch, "cache", "journalled-1.0.0");
|
|
|
|
|
+ var upgrade_root = Path.build_filename(scratch, "cache", "journalled-2.0.0");
|
|
|
|
|
+ DirUtils.create_with_parents(package_root, 0700);
|
|
|
|
|
+ DirUtils.create_with_parents(upgrade_root, 0700);
|
|
|
|
|
+ var pkg = new Usm.CachedPackage(package_root);
|
|
|
|
|
+ var upgrade = new Usm.CachedPackage(upgrade_root);
|
|
|
|
|
+
|
|
|
|
|
+ var lib_path = Path.build_filename(scratch, "dest", "usr", "lib", "libjournalled.so");
|
|
|
|
|
+ DirUtils.create_with_parents(Path.get_dirname(lib_path), 0700);
|
|
|
|
|
+ FileUtils.set_contents(lib_path, "old contents");
|
|
|
|
|
+ var mark_path = Path.build_filename(scratch, "state", "installed", "journalled-1.0.0");
|
|
|
|
|
+ DirUtils.create_with_parents(Path.get_dirname(mark_path), 0700);
|
|
|
|
|
+ File.new_for_path(mark_path).make_symbolic_link(package_root);
|
|
|
|
|
+
|
|
|
|
|
+ var journal = new Usm.Journal(Path.build_filename(scratch, "backup"));
|
|
|
|
|
+ journal.begin();
|
|
|
|
|
+ check(File.new_for_path(Path.build_filename(scratch, "backup", journal.transaction_id, "journal.json")).query_exists(),
|
|
|
|
|
+ "begin creates the journal file");
|
|
|
|
|
+
|
|
|
|
|
+ // a completed removal: the file is backed up and the mark target recorded
|
|
|
|
|
+ journal.entry_remove(pkg, { lib_path }, mark_path, package_root);
|
|
|
|
|
+ journal.backup_file(lib_path, pkg.package_name);
|
|
|
|
|
+ File.new_for_path(lib_path).delete();
|
|
|
|
|
+ File.new_for_path(mark_path).delete();
|
|
|
|
|
+ journal.complete(pkg.package_name);
|
|
|
|
|
+
|
|
|
|
|
+ // a half-applied install on top: one fresh file, the old path
|
|
|
|
|
+ // overwritten, the new mark already linked — never completed
|
|
|
|
|
+ var fresh_path = Path.build_filename(scratch, "dest", "usr", "bin", "journalled");
|
|
|
|
|
+ DirUtils.create_with_parents(Path.get_dirname(fresh_path), 0700);
|
|
|
|
|
+ var upgrade_mark_path = Path.build_filename(scratch, "state", "installed", "journalled-2.0.0");
|
|
|
|
|
+ FileUtils.set_contents(lib_path, "new contents");
|
|
|
|
|
+ journal.entry_install(upgrade, { fresh_path }, { lib_path }, upgrade_mark_path, null);
|
|
|
|
|
+ journal.backup_file(lib_path, upgrade.package_name);
|
|
|
|
|
+ FileUtils.set_contents(fresh_path, "binary");
|
|
|
|
|
+ File.new_for_path(upgrade_mark_path).make_symbolic_link(upgrade_root);
|
|
|
|
|
+
|
|
|
|
|
+ journal.rollback();
|
|
|
|
|
+
|
|
|
|
|
+ check(!File.new_for_path(fresh_path).query_exists(), "rollback removes the failed install's created file");
|
|
|
|
|
+ string restored;
|
|
|
|
|
+ FileUtils.get_contents(lib_path, out restored);
|
|
|
|
|
+ check(restored == "old contents", "rollback restores overwritten files from the backup");
|
|
|
|
|
+ check(!File.new_for_path(upgrade_mark_path).query_exists(), "rollback removes a mark the failed install created");
|
|
|
|
|
+ check(File.new_for_path(mark_path).query_exists(), "rollback re-creates the removal's installed mark");
|
|
|
|
|
+
|
|
|
|
|
+ var description = journal.describe_rollback();
|
|
|
|
|
+ check(description.contains("uninstalled \"journalled-2.0.0\"") && description.contains("reinstalled \"journalled-1.0.0\""),
|
|
|
|
|
+ "the rollback transcript names every undone operation");
|
|
|
|
|
+
|
|
|
|
|
+ string recorded;
|
|
|
|
|
+ FileUtils.get_contents(Path.build_filename(scratch, "backup", journal.transaction_id, "journal.json"), out recorded);
|
|
|
|
|
+ check(recorded.contains("\"status\":\"completed\"") && recorded.contains("\"files_removed\":[\"" + lib_path + "\"]"),
|
|
|
|
|
+ "journal.json records the operation, its status and its files");
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_journal_finish_removes_backup_directory() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var backup_root = Path.build_filename(scratch, "backup");
|
|
|
|
|
+
|
|
|
|
|
+ var journal = new Usm.Journal(backup_root);
|
|
|
|
|
+ journal.begin();
|
|
|
|
|
+ var transaction_directory = Path.build_filename(backup_root, journal.transaction_id);
|
|
|
|
|
+ check(File.new_for_path(transaction_directory).query_exists(), "begin creates the transaction backup directory");
|
|
|
|
|
+
|
|
|
|
|
+ journal.finish();
|
|
|
|
|
+ check(!File.new_for_path(transaction_directory).query_exists(), "finish removes the transaction backup directory");
|
|
|
|
|
+ check(!File.new_for_path(backup_root).query_exists(), "finish removes the emptied backup root");
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_transaction_rollback_on_build_failure() throws Error {
|
|
|
|
|
+ var original_dir = Environment.get_current_dir();
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var state = make_state(scratch);
|
|
|
|
|
+ var state_dir = Path.build_filename(scratch, "state");
|
|
|
|
|
+ var paths = scratch_paths(scratch);
|
|
|
|
|
+
|
|
|
|
|
+ // rb-b build-depends on rb-a, so rb-a and rb-c install in lot 1
|
|
|
|
|
+ // and rb-b builds in lot 2 — after both lot-1 installs completed
|
|
|
|
|
+ rollback_fixture_package(state_dir, "rb-a", "1.0.0", rollback_payload_build("payload-a"), {}, false);
|
|
|
|
|
+ rollback_fixture_package(state_dir, "rb-b", "1.0.0", ROLLBACK_FAILING_BUILD, { "bin:rb-a" }, false);
|
|
|
|
|
+ rollback_fixture_package(state_dir, "rb-c", "1.0.0", rollback_payload_build("payload-c"), {}, false);
|
|
|
|
|
+
|
|
|
|
|
+ var to_install = new HashSet<Usm.CachedPackage>();
|
|
|
|
|
+ foreach(var name in new string[] { "rb-a", "rb-b", "rb-c" }) {
|
|
|
|
|
+ to_install.add(new Usm.CachedPackage(Path.build_filename(state_dir, "packages", @"$name-1.0.0")));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ var transaction = new Usm.Transaction() {
|
|
|
|
|
+ paths = paths,
|
|
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
|
|
+ to_install = to_install,
|
|
|
|
|
+ to_remove = new HashSet<Usm.CachedPackage>(),
|
|
|
|
|
+ state = state
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ var threw = false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ transaction.run();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Usm.TransactionError e) {
|
|
|
|
|
+ threw = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ check(threw, "a lot-2 build failure fails the transaction");
|
|
|
|
|
+
|
|
|
|
|
+ check(!File.new_for_path(paths.get_suggested_path_for_resource(new Usm.ResourceRef("bin:rb-a"))).query_exists(),
|
|
|
|
|
+ "the rollback removes the first package's installed file");
|
|
|
|
|
+ check(!File.new_for_path(paths.get_suggested_path_for_resource(new Usm.ResourceRef("bin:rb-c"))).query_exists(),
|
|
|
|
|
+ "the rollback removes the third package's installed file");
|
|
|
|
|
+ check(!File.new_for_path(paths.get_suggested_path_for_resource(new Usm.ResourceRef("bin:rb-b"))).query_exists(),
|
|
|
|
|
+ "the failing package's file never appeared");
|
|
|
|
|
+ check(!File.new_for_path(Path.build_filename(state_dir, "installed", "rb-a-1.0.0")).query_exists(),
|
|
|
|
|
+ "the rollback removes the first package's installed mark");
|
|
|
|
|
+ check(!File.new_for_path(Path.build_filename(state_dir, "installed", "rb-c-1.0.0")).query_exists(),
|
|
|
|
|
+ "the rollback removes the third package's installed mark");
|
|
|
|
|
+
|
|
|
|
|
+ var found_journal = false;
|
|
|
|
|
+ var backup_root = Path.build_filename(scratch, "dest", "var", "usm", "backup");
|
|
|
|
|
+ foreach(var transaction_id in Iterate.directory(backup_root)) {
|
|
|
|
|
+ var journal_path = Path.build_filename(backup_root, transaction_id, "journal.json");
|
|
|
|
|
+ if(!File.new_for_path(journal_path).query_exists()) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ }
|
|
|
|
|
+ found_journal = true;
|
|
|
|
|
+ string recorded;
|
|
|
|
|
+ FileUtils.get_contents(journal_path, out recorded);
|
|
|
|
|
+ check(recorded.contains("\"package\":\"rb-a-1.0.0\"") && recorded.contains("\"package\":\"rb-c-1.0.0\""),
|
|
|
|
|
+ "the kept journal records both rolled-back installs");
|
|
|
|
|
+ }
|
|
|
|
|
+ check(found_journal, "a rolled-back transaction keeps its journal for inspection");
|
|
|
|
|
+
|
|
|
|
|
+ Environment.set_current_dir(original_dir);
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_transaction_rollback_restores_previous_version() throws Error {
|
|
|
|
|
+ var original_dir = Environment.get_current_dir();
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var state = make_state(scratch);
|
|
|
|
|
+ var state_dir = Path.build_filename(scratch, "state");
|
|
|
|
|
+ var paths = scratch_paths(scratch);
|
|
|
|
|
+
|
|
|
|
|
+ rollback_fixture_package(state_dir, "rb-u", "1.0.0", rollback_payload_build("payload-old"), {}, false);
|
|
|
|
|
+ var first = new Usm.Transaction() {
|
|
|
|
|
+ paths = paths,
|
|
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
|
|
+ to_install = Iterate.these<Usm.CachedPackage>(new Usm.CachedPackage(Path.build_filename(state_dir, "packages", "rb-u-1.0.0"))).to_hash_set(),
|
|
|
|
|
+ to_remove = new HashSet<Usm.CachedPackage>(),
|
|
|
|
|
+ state = state
|
|
|
|
|
+ };
|
|
|
|
|
+ first.run();
|
|
|
|
|
+
|
|
|
|
|
+ var bin_path = paths.get_suggested_path_for_resource(new Usm.ResourceRef("bin:rb-u"));
|
|
|
|
|
+ check(File.new_for_path(bin_path).query_exists(), "the old version installs before the update");
|
|
|
|
|
+
|
|
|
|
|
+ rollback_fixture_package(state_dir, "rb-u", "2.0.0", ROLLBACK_EMPTY_BUILD, {}, false);
|
|
|
|
|
+ var second = new Usm.Transaction() {
|
|
|
|
|
+ paths = paths,
|
|
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
|
|
+ to_install = Iterate.these<Usm.CachedPackage>(new Usm.CachedPackage(Path.build_filename(state_dir, "packages", "rb-u-2.0.0"))).to_hash_set(),
|
|
|
|
|
+ to_remove = Iterate.these<Usm.CachedPackage>(new Usm.CachedPackage(Path.build_filename(state_dir, "packages", "rb-u-1.0.0"))).to_hash_set(),
|
|
|
|
|
+ state = state
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ var threw = false;
|
|
|
|
|
+ try {
|
|
|
|
|
+ second.run();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Usm.TransactionError e) {
|
|
|
|
|
+ threw = true;
|
|
|
|
|
+ }
|
|
|
|
|
+ check(threw, "the new version's install failure fails the update");
|
|
|
|
|
+
|
|
|
|
|
+ string restored;
|
|
|
|
|
+ FileUtils.get_contents(bin_path, out restored);
|
|
|
|
|
+ check(restored == "payload-old", "the old version's file is restored from the backup");
|
|
|
|
|
+ check(state.read_installed_target(new Usm.CachedPackage(Path.build_filename(state_dir, "packages", "rb-u-1.0.0")))
|
|
|
|
|
+ == Path.build_filename(state_dir, "packages", "rb-u-1.0.0"),
|
|
|
|
|
+ "the old version's installed mark is re-created pointing at its cache directory");
|
|
|
|
|
+ check(!File.new_for_path(Path.build_filename(state_dir, "installed", "rb-u-2.0.0")).query_exists(),
|
|
|
|
|
+ "the failed version's installed mark never lingers");
|
|
|
|
|
+
|
|
|
|
|
+ Environment.set_current_dir(original_dir);
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- Granular progress reporting ----------------------------------------------
|
|
|
|
|
+
|
|
|
|
|
+ /** Whether the reports climb monotonically from start to end. */
|
|
|
|
|
+ bool monotonic(float[] fractions) {
|
|
|
|
|
+ for(int i = 1; i < fractions.length; i++) {
|
|
|
|
|
+ if(fractions[i] < fractions[i - 1]) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_unarchive_with_progress() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var source = Path.build_filename(scratch, "src");
|
|
|
|
|
+ DirUtils.create(source, 0755);
|
|
|
|
|
+ for(int i = 0; i < 5; i++) {
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(source, @"file$i.txt"), @"content $i");
|
|
|
|
|
+ }
|
|
|
|
|
+ var archive = Path.build_filename(scratch, "pkg.usmc");
|
|
|
|
|
+ Usm.Util.archive(source, archive);
|
|
|
|
|
+
|
|
|
|
|
+ float[] fractions = {};
|
|
|
|
|
+ var destination = Path.build_filename(scratch, "out");
|
|
|
|
|
+ Usm.Util.unarchive_with_progress(archive, destination, f => { fractions += f; });
|
|
|
|
|
+ check(fractions.length == 6, "one progress report fires per extracted archive member (including the root)");
|
|
|
|
|
+ check(fractions[0] > 0.0f, "the first member already reports progress");
|
|
|
|
|
+ check(fractions[fractions.length - 1] == 1.0f, "the last member reports full progress");
|
|
|
|
|
+ check(monotonic(fractions), "extraction progress never regresses");
|
|
|
|
|
+ check(File.new_for_path(Path.build_filename(destination, "file3.txt")).query_exists(), "the archive extracts fully");
|
|
|
|
|
+
|
|
|
|
|
+ var bare = Path.build_filename(scratch, "out-bare");
|
|
|
|
|
+ Usm.Util.unarchive_with_progress(archive, bare, null);
|
|
|
|
|
+ check(File.new_for_path(Path.build_filename(bare, "file0.txt")).query_exists(), "a null delegate extracts without progress reporting");
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_resolver_resolution_progress() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var resolver = new Usm.Resolver(new Usm.ResourceFinder());
|
|
|
|
|
+ resolver.supply_package(make_package_archive(scratch, "pa", {}));
|
|
|
|
|
+ resolver.supply_package(make_package_archive(scratch, "pb", { "bin:pkg-pa" }));
|
|
|
|
|
+
|
|
|
|
|
+ var roots = new Vector<Usm.AbstractPackage>();
|
|
|
|
|
+ roots.add(((!)resolver.find_package("pkg-pb")));
|
|
|
|
|
+
|
|
|
|
|
+ float[] fractions = {};
|
|
|
|
|
+ resolver.resolution_progress = f => { fractions += f; };
|
|
|
|
|
+ var result = resolver.resolve(roots, null);
|
|
|
|
|
+
|
|
|
|
|
+ check(result.packages.count() == 2, "resolution still closes the closure with reporting enabled");
|
|
|
|
|
+ check(fractions.length == 3, "one report fires per resolved package plus a completion clamp");
|
|
|
|
|
+ check(fractions[0] == 0.5f, "the first package reports against the roots-x-2 estimate");
|
|
|
|
|
+ check(fractions[fractions.length - 1] == 1.0f, "completion clamps to full progress");
|
|
|
|
|
+ check(monotonic(fractions), "resolution progress never regresses");
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_install_script_progress_parsing() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var original_dir = Environment.get_current_dir();
|
|
|
|
|
+ var source = Path.build_filename(scratch, "src");
|
|
|
|
|
+ DirUtils.create(source, 0755);
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"), """
|
|
|
|
|
+ {
|
|
|
|
|
+ "name": "meson-pkg",
|
|
|
|
|
+ "version": "1.0.0",
|
|
|
|
|
+ "summary": "install progress fixture",
|
|
|
|
|
+ "licences": [],
|
|
|
|
|
+ "flags": ["ninjaStyleProgress"],
|
|
|
|
|
+ "provides": { "bin:meson-pkg": "as-expected" },
|
|
|
|
|
+ "depends": { "runtime": [], "build": [], "manage": [] },
|
|
|
|
|
+ "execs": { "install": "install.sh" }
|
|
|
|
|
+ }
|
|
|
|
|
+ """);
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(source, "install.sh"), """#!/bin/bash
|
|
|
|
|
+echo "Installing meson-pkg to /usr/bin"
|
|
|
|
|
+echo "noise that is not an install line"
|
|
|
|
|
+echo "Installing libmeson-pkg.so to /usr/lib"
|
|
|
|
|
+""");
|
|
|
|
|
+ FileUtils.chmod(Path.build_filename(source, "install.sh"), 0755);
|
|
|
|
|
+
|
|
|
|
|
+ Environment.set_current_dir(source);
|
|
|
|
|
+ var manifest = new Usm.Manifest.from_file("MANIFEST.usm");
|
|
|
|
|
+ float[] fractions = {};
|
|
|
|
|
+ var install_dir = Path.build_filename(scratch, "install");
|
|
|
|
|
+ DirUtils.create(install_dir, 0755);
|
|
|
|
|
+ var proc = manifest.run_install(Path.build_filename(scratch, "build"), install_dir, new Usm.Paths(),
|
|
|
|
|
+ Usm.InstallType.FRESH, SubprocessFlags.STDOUT_SILENCE, f => { fractions += f; });
|
|
|
|
|
+ check(proc != null, "a ninjaStyleProgress install script spawns through the parsing path");
|
|
|
|
|
+ ((!)proc).wait_check();
|
|
|
|
|
+
|
|
|
|
|
+ check(fractions.length == 2, "each Installing line reports exactly once and noise is skipped");
|
|
|
|
|
+ check(fractions[0] == 0.5f, "reports run against the provides-count-x-2 estimate");
|
|
|
|
|
+ check(fractions[1] == 1.0f, "the last Installing line reaches full progress");
|
|
|
|
|
+
|
|
|
|
|
+ Environment.set_current_dir(original_dir);
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_transaction_unpack_progress() throws Error {
|
|
|
|
|
+ var original_dir = Environment.get_current_dir();
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ var state = make_state(scratch);
|
|
|
|
|
+
|
|
|
|
|
+ var cache_path = Path.build_filename(scratch, "state", "packages", "unpack-pkg-1.0.0");
|
|
|
|
|
+ DirUtils.create_with_parents(cache_path, 0755);
|
|
|
|
|
+ var source = Path.build_filename(scratch, "src-unpack-pkg");
|
|
|
|
|
+ DirUtils.create(source, 0755);
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"), BUILD_MANIFEST.printf("unpack-pkg", "1.0.0"));
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(source, "build.sh"), NOOP_BUILD_SCRIPT);
|
|
|
|
|
+ FileUtils.chmod(Path.build_filename(source, "build.sh"), 0755);
|
|
|
|
|
+ for(int i = 0; i < 4; i++) {
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(source, @"payload$i.txt"), "data");
|
|
|
|
|
+ }
|
|
|
|
|
+ Usm.Util.archive(source, Path.build_filename(cache_path, "package.usmc"));
|
|
|
|
|
+ Usm.Util.delete_tree(source);
|
|
|
|
|
+
|
|
|
|
|
+ var to_install = new HashSet<Usm.CachedPackage>();
|
|
|
|
|
+ to_install.add(new Usm.CachedPackage(cache_path));
|
|
|
|
|
+
|
|
|
|
|
+ var transaction = new Usm.Transaction() {
|
|
|
|
|
+ paths = scratch_paths(scratch),
|
|
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
|
|
+ to_install = to_install,
|
|
|
|
|
+ to_remove = new HashSet<Usm.CachedPackage>(),
|
|
|
|
|
+ state = state
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ float[] unpack_fractions = {};
|
|
|
|
|
+ transaction.progress_updated.connect((task_type, subject, current_task, total_tasks, task_progress) => {
|
|
|
|
|
+ if(task_type == Usm.TransactionTask.UNPACKING) {
|
|
|
|
|
+ unpack_fractions += task_progress;
|
|
|
|
|
+ }
|
|
|
|
|
+ });
|
|
|
|
|
+ transaction.run();
|
|
|
|
|
+
|
|
|
|
|
+ check(unpack_fractions.length >= 6, "unpacking reports the clean, every extracted member and completion");
|
|
|
|
|
+ check(unpack_fractions[0] == 0.0f, "the task opens at zero before the source clean");
|
|
|
|
|
+ check(unpack_fractions[1] == 0.5f, "the source clean lands at the half-way report");
|
|
|
|
|
+ check(unpack_fractions[unpack_fractions.length - 1] == 1.0f, "unpacking completes at full progress");
|
|
|
|
|
+ check(monotonic(unpack_fractions), "unpack progress never regresses");
|
|
|
|
|
+
|
|
|
|
|
+ Environment.set_current_dir(original_dir);
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ int main() {
|
|
|
|
|
+ test_default_ignore();
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_ignore_patterns();
|
|
|
|
|
+ test_ignore_always_included();
|
|
|
|
|
+ test_ignore_from_root();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL ignore matcher test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_data_package_flag();
|
|
|
|
|
+ test_rebuild_dependants_flag();
|
|
|
|
|
+ test_data_package_validation();
|
|
|
|
|
+ test_data_package_from_file();
|
|
|
|
|
+ test_configuration_system_package_manager();
|
|
|
|
|
+ test_gio_module_ref();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL data package test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_repository_listing_entry_size();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL repository listing size test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_dependencies_flat_back_compat();
|
|
|
|
|
+ test_dependencies_nested();
|
|
|
|
|
+ test_dependencies_mixed_rejected();
|
|
|
|
|
+ test_dependencies_empty_group();
|
|
|
|
|
+ test_dependencies_roundtrip();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL dependencies mapper test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_topological_order_linear();
|
|
|
|
|
+ test_topological_order_diamond();
|
|
|
|
|
+ test_topological_order_name_tiebreak();
|
|
|
|
|
+ test_topological_order_cycle();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL topological order test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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_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_tie_manifest_order();
|
|
|
|
|
+ test_group_selection_none_viable_itemised();
|
|
|
|
|
+ test_group_selection_single_batched_query();
|
|
|
|
|
+ test_system_package_manager_install_stub();
|
|
|
|
|
+ test_transaction_accepts_orders();
|
|
|
|
|
+ test_transaction_lots_split_at_build_boundaries();
|
|
|
|
|
+ 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) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL resolver test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_find_dependant_names();
|
|
|
|
|
+ test_rebuild_planning();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL rebuild planning test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_origin_information_explicit_install();
|
|
|
|
|
+ test_transaction_records_explicit_flags();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL explicit install tracking test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_build_archive_restore_and_clean_retry();
|
|
|
|
|
+ test_build_archive_restored_incrementally();
|
|
|
|
|
+ test_build_failure_without_cache_propagates();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL build cache retry test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_journal_entry_complete_rollback_roundtrip();
|
|
|
|
|
+ test_journal_finish_removes_backup_directory();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL journal test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_transaction_rollback_on_build_failure();
|
|
|
|
|
+ test_transaction_rollback_restores_previous_version();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL transaction rollback test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ try {
|
|
|
|
|
+ test_unarchive_with_progress();
|
|
|
|
|
+ test_resolver_resolution_progress();
|
|
|
|
|
+ test_install_script_progress_parsing();
|
|
|
|
|
+ test_transaction_unpack_progress();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL progress reporting test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ print("%d passed, %d failed\n", passes, failures);
|
|
|
|
|
+ return failures == 0 ? 0 : 1;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|