| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362 |
- 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");
- }
- // ---- 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);
- }
- // ---- 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);
- }
- 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_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);
- }
- 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);
- }
- print("%d passed, %d failed\n", passes, failures);
- return failures == 0 ? 0 : 1;
- }
- }
|