|
@@ -0,0 +1,910 @@
|
|
|
|
|
+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_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());
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 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");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // ---- 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
|
|
|
|
|
+ ;;
|
|
|
|
|
+ install)
|
|
|
|
|
+ total=$#
|
|
|
|
|
+ echo "{\"type\":\"begin\",\"total\":$total}"
|
|
|
|
|
+ n=0
|
|
|
|
|
+ for name in "$@"; do
|
|
|
|
|
+ n=$((n+1))
|
|
|
|
|
+ echo "$name" >> "$USM_SPM_INSTALL_LOG"
|
|
|
|
|
+ echo "{\"type\":\"package\",\"name\":\"$name\",\"current\":$n,\"total\":$total,\"progress\":0.5}"
|
|
|
|
|
+ echo "{\"type\":\"package-complete\",\"name\":\"$name\"}"
|
|
|
|
|
+ done
|
|
|
|
|
+ echo "{\"type\":\"complete\",\"status\":\"ok\",\"installed\":$total}"
|
|
|
|
|
+ ;;
|
|
|
|
|
+ *)
|
|
|
|
|
+ exit 2
|
|
|
|
|
+ ;;
|
|
|
|
|
+esac
|
|
|
|
|
+""";
|
|
|
|
|
+
|
|
|
|
|
+ string make_scratch() throws Error {
|
|
|
|
|
+ var dir = File.new_build_filename("/tmp", Uuid.string_random());
|
|
|
|
|
+ dir.make_directory();
|
|
|
|
|
+ return dir.get_path();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** One candidate entry in the fake fixture's package table. */
|
|
|
|
|
+ string spm_candidate(string name, string resource, int deps, int installed) {
|
|
|
|
|
+ return "{\"name\": \"%s\", \"resources\": [\"%s\"], \"dependency-count\": %d, \"installed-dependency-count\": %d}".printf(name, resource, deps, installed);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ Usm.SystemPackageManager stub_manager(string scratch, string candidates_json) throws Error {
|
|
|
|
|
+ var stub_path = Path.build_filename(scratch, "spm-stub.sh");
|
|
|
|
|
+ FileUtils.set_contents(stub_path, SPM_STUB_SCRIPT);
|
|
|
|
|
+ FileUtils.chmod(stub_path, 0755);
|
|
|
|
|
+
|
|
|
|
|
+ var fixture_path = Path.build_filename(scratch, "fixture.json");
|
|
|
|
|
+ FileUtils.set_contents(fixture_path, @"{ \"packages\": [$candidates_json] }");
|
|
|
|
|
+ Environment.set_variable("USM_SPM_FIXTURE", fixture_path, true);
|
|
|
|
|
+ Environment.set_variable("USM_SPM_LOG", Path.build_filename(scratch, "query.log"), true);
|
|
|
|
|
+ Environment.set_variable("USM_SPM_INSTALL_LOG", Path.build_filename(scratch, "install.log"), true);
|
|
|
|
|
+
|
|
|
|
|
+ var config_dir = Path.build_filename(scratch, "config");
|
|
|
|
|
+ DirUtils.create(config_dir, 0700);
|
|
|
|
|
+ FileUtils.set_contents(Path.build_filename(config_dir, "usm.config"),
|
|
|
|
|
+ "{\"is_managed\": false, \"system_package_manager\": { \"query\": [\"%s\", \"query\"], \"install\": [\"%s\", \"install\"] }}".printf(stub_path, stub_path));
|
|
|
|
|
+
|
|
|
|
|
+ var paths = new Usm.Paths.defaults();
|
|
|
|
|
+ paths.usm_config_dir = config_dir;
|
|
|
|
|
+ return new Usm.SystemPackageManager(new Usm.Configuration.from_paths(paths));
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /** Builds a .usmc archive for pkg-<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);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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.has_prefix("one\ntwo"), "install helper received the package names in order");
|
|
|
|
|
+
|
|
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ void test_transaction_accepts_orders() throws Error {
|
|
|
|
|
+ var scratch = make_scratch();
|
|
|
|
|
+ make_package_archive(scratch, "a", {});
|
|
|
|
|
+ make_package_archive(scratch, "b", { "bin:pkg-a" });
|
|
|
|
|
+ make_package_archive(scratch, "c", { "bin:pkg-a" });
|
|
|
|
|
+
|
|
|
|
|
+ var to_install = new HashSet<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);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ 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_data_package_validation();
|
|
|
|
|
+ test_data_package_from_file();
|
|
|
|
|
+ test_configuration_system_package_manager();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL data package 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_resolve_diamond_usm_only();
|
|
|
|
|
+ test_resolve_spm_before_usm();
|
|
|
|
|
+ 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();
|
|
|
|
|
+ }
|
|
|
|
|
+ catch(Error e) {
|
|
|
|
|
+ failures++;
|
|
|
|
|
+ print("FAIL resolver test threw: %s\n", e.message);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ print("%d passed, %d failed\n", passes, failures);
|
|
|
|
|
+ return failures == 0 ? 0 : 1;
|
|
|
|
|
+ }
|
|
|
|
|
+}
|