TestMain.vala 63 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362
  1. using Invercargill;
  2. using Invercargill.DataStructures;
  3. using InvercargillJson;
  4. namespace Usm.Tests {
  5. int failures = 0;
  6. int passes = 0;
  7. void check(bool condition, string label) {
  8. if (condition) {
  9. passes++;
  10. print("PASS %s\n", label);
  11. } else {
  12. failures++;
  13. print("FAIL %s\n", label);
  14. }
  15. }
  16. /**
  17. * Materialises a manifest from a JSON string the same way the CLI does,
  18. * so validation runs against exactly what `usm manifest` would load.
  19. */
  20. Usm.Manifest manifest_from_json(string json) throws Error {
  21. var element = new JsonElement.from_string(json);
  22. return Usm.Manifest.get_mapper().materialise(element.as<Invercargill.Properties>());
  23. }
  24. /** Minimal valid data-package manifest, with execs injected per test. */
  25. const string DATA_MANIFEST = """
  26. {
  27. "name": "fonts-example",
  28. "version": "1.0.0",
  29. "summary": "Example fonts",
  30. "licences": [],
  31. "flags": ["dataPackage"],
  32. "provides": { "res:fonts/Example.ttf": "source:fonts/Example.ttf" },
  33. "depends": { "runtime": [], "build": [], "manage": ["bin:bash"] },
  34. %s
  35. }
  36. """;
  37. void test_default_ignore() {
  38. var ignore = new Usm.UsmIgnore();
  39. check(ignore.matches(".git", true), "default ignores the .git directory");
  40. check(ignore.matches(".git/config", false), "default ignores everything beneath .git");
  41. check(!ignore.matches(".git", false), "default does not ignore a file named .git");
  42. check(!ignore.matches("src/main.vala", false), "default keeps ordinary files");
  43. check(!ignore.matches(".usmignore", false), ".usmignore can never be ignored");
  44. check(!ignore.matches("MANIFEST.usm", false), "MANIFEST.usm can never be ignored");
  45. }
  46. void test_ignore_patterns() {
  47. var ignore = new Usm.UsmIgnore();
  48. ignore.add_pattern("# a comment");
  49. ignore.add_pattern(" ");
  50. ignore.add_pattern("builddir/");
  51. ignore.add_pattern("*.sqlite");
  52. ignore.add_pattern("docs/generated/");
  53. ignore.add_pattern("src/secret.*");
  54. ignore.add_pattern("notes/?draft.md");
  55. ignore.add_pattern("!keep.sqlite");
  56. check(!ignore.matches("# a comment", false), "comments are skipped");
  57. check(ignore.matches("db.sqlite", false), "suffix pattern matches the basename");
  58. check(ignore.matches("data/deep/db.sqlite", false), "suffix pattern matches at any depth");
  59. check(!ignore.matches("db.sqlite3", false), "suffix pattern anchors the basename end");
  60. check(ignore.matches("builddir", true), "directory-only pattern matches the directory itself");
  61. check(!ignore.matches("builddir", false), "directory-only pattern ignores no plain file");
  62. check(ignore.matches("builddir/meson", false), "directory-only pattern matches everything beneath");
  63. check(ignore.matches("src/builddir", true), "unanchored directory-only pattern matches at any depth");
  64. check(ignore.matches("src/builddir/x", false), "unanchored directory-only pattern covers beneath at any depth");
  65. check(!ignore.matches("builddirs", true), "directory-only pattern does not over-match names");
  66. check(ignore.matches("docs/generated", true), "anchored directory-only pattern matches its path");
  67. check(ignore.matches("docs/generated/a.md", false), "anchored directory-only pattern covers beneath");
  68. check(!ignore.matches("other/docs/generated", true), "anchored directory-only pattern does not match deeper roots");
  69. check(ignore.matches("src/secret.key", false), "anchored pattern matches the full relative path");
  70. check(!ignore.matches("src/sub/secret.key", false), "star never crosses a path separator");
  71. check(!ignore.matches("include/secret.key", false), "anchored pattern does not match other roots");
  72. check(ignore.matches("notes/1draft.md", false), "question mark matches one character");
  73. check(!ignore.matches("notes/12draft.md", false), "question mark matches exactly one character");
  74. check(ignore.matches("keep.sqlite", false), "exclamation mark has no negation power");
  75. check(ignore.matches("!keep.sqlite", false), "exclamation mark is a literal pattern character");
  76. check(ignore.matches(".git", true), ".git stays ignored alongside explicit patterns");
  77. }
  78. void test_ignore_always_included() {
  79. var ignore = new Usm.UsmIgnore();
  80. ignore.add_pattern("*");
  81. ignore.add_pattern(".usmignore");
  82. ignore.add_pattern("MANIFEST.usm");
  83. check(ignore.matches("everything", false), "catch-all pattern matches ordinary files");
  84. check(!ignore.matches(".usmignore", false), "catch-all cannot ignore .usmignore");
  85. check(!ignore.matches("MANIFEST.usm", false), "catch-all cannot ignore MANIFEST.usm");
  86. check(ignore.matches("sub/MANIFEST.usm", false), "nested manifests follow normal rules");
  87. }
  88. void test_ignore_from_root() throws Error {
  89. var root = File.new_build_filename("/tmp", Uuid.string_random());
  90. root.make_directory();
  91. var without = new Usm.UsmIgnore.from_root(root.get_path());
  92. check(without.matches(".git", true), "absent .usmignore falls back to the .git default");
  93. check(!without.matches("db.sqlite", false), "absent .usmignore ignores nothing else");
  94. FileUtils.set_contents(Path.build_filename(root.get_path(), ".usmignore"), "*.sqlite\n");
  95. var with = new Usm.UsmIgnore.from_root(root.get_path());
  96. check(with.matches("db.sqlite", false), ".usmignore is loaded from the package root");
  97. check(!with.matches("MANIFEST.usm", false), "loaded .usmignore keeps MANIFEST.usm included");
  98. Usm.Util.delete_tree(root.get_path());
  99. }
  100. void test_data_package_flag() {
  101. check(Usm.ManifestFlag.DATA_PACKAGE.to_string() == "dataPackage", "dataPackage flag serialises");
  102. check(Usm.ManifestFlag.from_string("dataPackage") == Usm.ManifestFlag.DATA_PACKAGE, "dataPackage flag parses");
  103. }
  104. void test_rebuild_dependants_flag() throws Error {
  105. check(Usm.ManifestFlag.REBUILD_DEPENDANTS.to_string() == "rebuildDependants", "rebuildDependants flag serialises");
  106. check(Usm.ManifestFlag.from_string("rebuildDependants") == Usm.ManifestFlag.REBUILD_DEPENDANTS, "rebuildDependants flag parses");
  107. var flagged = manifest_from_json(APP_MANIFEST.printf("invercargill", "invercargill", depends_with("runtime", "[]"))
  108. .replace("\"flags\": []", "\"flags\": [\"rebuildDependants\"]"));
  109. check(flagged.flags.contains(Usm.ManifestFlag.REBUILD_DEPENDANTS), "rebuildDependants parses within a manifest flags array");
  110. var serialised = ((!)new JsonElement.from_properties(Usm.Manifest.get_mapper().map_from(flagged))).stringify_pretty();
  111. check(manifest_from_json(serialised).flags.contains(Usm.ManifestFlag.REBUILD_DEPENDANTS),
  112. "rebuildDependants round-trips through serialisation");
  113. }
  114. void test_data_package_validation() throws Error {
  115. var clean = manifest_from_json(DATA_MANIFEST.printf("\"execs\": { \"acquire\": \"fetch.sh\" }"));
  116. clean.validate();
  117. check(clean.is_data_package, "dataPackage flag marks a data package");
  118. check(clean.executables.acquire != null, "acquire executable is honoured on data packages");
  119. check(clean.executables.build == null, "data package without build parses");
  120. var no_execs = manifest_from_json(DATA_MANIFEST.printf("\"execs\": { }"));
  121. no_execs.validate();
  122. check(no_execs.executables.build == null, "empty execs parse without a build executable");
  123. var omitted = manifest_from_json(DATA_MANIFEST.printf("\"extras\": { }"));
  124. omitted.validate();
  125. check(omitted.executables != null, "omitting execs entirely parses");
  126. var rejects = 0;
  127. string[] offending = { "build", "install", "rebuild", "test", "remove", "postInstall" };
  128. foreach(var executable in offending) {
  129. try {
  130. var manifest = manifest_from_json(DATA_MANIFEST.printf("\"execs\": { \"%s\": \"run.sh\" }".printf(executable)));
  131. manifest.validate();
  132. print("FAIL dataPackage + %s should not validate\n", executable);
  133. failures++;
  134. }
  135. catch(Usm.ManifestError e) {
  136. rejects++;
  137. }
  138. }
  139. check(rejects == offending.length, "dataPackage + any lifecycle executable is rejected");
  140. try {
  141. var manifest = manifest_from_json(DATA_MANIFEST.printf("\"execs\": { \"install\": \"run.sh\" }"));
  142. manifest.validate();
  143. check(false, "rejection message mentions the offending executable");
  144. }
  145. catch(Usm.ManifestError e) {
  146. check(e.message.contains("dataPackage") && e.message.contains("install"), "rejection message mentions the offending executable");
  147. }
  148. }
  149. void test_data_package_from_file() throws Error {
  150. var root = File.new_build_filename("/tmp", Uuid.string_random());
  151. root.make_directory();
  152. var manifest_path = Path.build_filename(root.get_path(), "MANIFEST.usm");
  153. FileUtils.set_contents(manifest_path, DATA_MANIFEST.printf("\"execs\": { \"install\": \"install.sh\" }"));
  154. try {
  155. new Usm.Manifest.from_file(manifest_path);
  156. check(false, "Manifest.from_file enforces the data-package rule");
  157. }
  158. catch(Usm.ManifestError e) {
  159. check(true, "Manifest.from_file enforces the data-package rule");
  160. }
  161. Usm.Util.delete_tree(root.get_path());
  162. }
  163. void test_configuration_system_package_manager() throws Error {
  164. var config_dir = File.new_build_filename("/tmp", Uuid.string_random());
  165. config_dir.make_directory();
  166. var paths = new Usm.Paths.defaults();
  167. paths.usm_config_dir = config_dir.get_path();
  168. FileUtils.set_contents(Path.build_filename(config_dir.get_path(), "usm.config"),
  169. "{\"is_managed\": false, \"system_package_manager\": { \"query\": [\"/bin/usm-spm-dnf\", \"query\"], \"install\": [\"/bin/usm-spm-dnf\", \"install\"] }}");
  170. var complete = new Usm.Configuration.from_paths(paths);
  171. check(complete.system_package_manager != null && complete.system_package_manager.query != null,
  172. "usm.config system_package_manager section parses");
  173. FileUtils.set_contents(Path.build_filename(config_dir.get_path(), "usm.config"),
  174. "{\"is_managed\": false, \"system_package_manager\": { \"query\": [\"/bin/usm-spm-dnf\", \"query\"] }}");
  175. try {
  176. new Usm.Configuration.from_paths(paths);
  177. check(false, "query without install is rejected");
  178. }
  179. catch(Usm.ConfigurationError e) {
  180. check(true, "query without install is rejected");
  181. }
  182. Usm.Util.delete_tree(config_dir.get_path());
  183. }
  184. // ---- GIO module resource type ------------------------------------------------
  185. void test_gio_module_ref() throws Error {
  186. var resource_ref = new Usm.ResourceRef("gio:libgiognutls.so");
  187. check(resource_ref.resource_type == Usm.ResourceType.GIO_MODULE, "gio: parses as a GIO module ref");
  188. check(resource_ref.resource == "libgiognutls.so", "the gio: name is the module filename");
  189. check(Usm.ResourceType.GIO_MODULE.to_string() == "gio", "gio round-trips through to_string");
  190. check(resource_ref.to_string() == "gio:libgiognutls.so", "the GIO module ref round-trips");
  191. var located = new Usm.ResourceFinder().locate_resource(resource_ref);
  192. check(located != null && located.has_suffix("/gio/modules/libgiognutls.so"),
  193. "the ResourceFinder locates libgiognutls.so in a gio/modules directory");
  194. }
  195. // ---- Dependency-phase model -------------------------------------------------
  196. /** Minimal manifest with the given "depends" section verbatim. */
  197. const string PHASE_MANIFEST = """
  198. {
  199. "name": "phased-app",
  200. "version": "1.0.0",
  201. "summary": "phases",
  202. "licences": [],
  203. "flags": [],
  204. "provides": { "bin:phased-app": "as-expected" },
  205. "depends": %s,
  206. "execs": {}
  207. }
  208. """;
  209. /** Manifest named <name> providing bin:<name> with the given "depends" section. */
  210. const string APP_MANIFEST = """
  211. {
  212. "name": "%s",
  213. "version": "1.0.0",
  214. "summary": "app",
  215. "licences": [],
  216. "flags": [],
  217. "provides": { "bin:%s": "as-expected" },
  218. "depends": %s,
  219. "execs": {}
  220. }
  221. """;
  222. /** A "depends" section with {@link value} in {@link phase} and empty flat arrays elsewhere. */
  223. string depends_with(string phase, string value) {
  224. var parts = new StringBuilder();
  225. foreach(var other in new string[] { "runtime", "build", "manage", "acquire" }) {
  226. if(parts.len > 0) {
  227. parts.append(", ");
  228. }
  229. parts.append_printf("\"%s\": %s", other, other == phase ? value : "[]");
  230. }
  231. return "{ " + parts.str + " }";
  232. }
  233. void test_dependencies_flat_back_compat() throws Error {
  234. var manifest = manifest_from_json(PHASE_MANIFEST.printf(
  235. "{ \"runtime\": [\"bin:bash\", \"bin:coreutils\"], \"build\": [], \"manage\": [], \"acquire\": [\"bin:wget\"] }"));
  236. check(!manifest.dependencies.runtime.is_grouped, "flat runtime phase stays flat");
  237. check(manifest.dependencies.runtime.required.count() == 2, "flat runtime phase holds every ref");
  238. check(manifest.dependencies.runtime.ordered_required().first().to_string() == "bin:bash", "flat phase refs are ordered by string");
  239. check(!manifest.dependencies.build.is_grouped && manifest.dependencies.build.required.count() == 0, "empty flat phase parses");
  240. check(!manifest.dependencies.manage.is_grouped, "manage phase stays flat");
  241. check(manifest.dependencies.acquire != null && manifest.dependencies.acquire.required.count() == 1, "flat acquire phase parses");
  242. check(manifest.dependencies.acquire.ordered_required().first().to_string() == "bin:wget", "acquire refs land in the acquire phase, not manage");
  243. var omitted = manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": [], \"build\": [], \"manage\": [] }"));
  244. check(omitted.dependencies.acquire == null, "omitted acquire phase stays null");
  245. var usm_shape = manifest_from_json(PHASE_MANIFEST.printf(
  246. "{ \"runtime\": [\"lib:libc.so.6\"], \"build\": [\"bin:valac\"], \"manage\": [\"bin:bash\"] }"));
  247. check(usm_shape.dependencies.build.required.any(r => r.to_string() == "bin:valac"), "usm's own flat manifest shape still parses");
  248. }
  249. void test_dependencies_nested() throws Error {
  250. var manifest = manifest_from_json(PHASE_MANIFEST.printf(
  251. "{ \"runtime\": [[\"bin:python3\", \"pc:python3.pc\"], [\"bin:python\", \"pc:python.pc\"]], \"build\": [], \"manage\": [] }"));
  252. check(manifest.dependencies.runtime.is_grouped, "nested runtime phase is grouped");
  253. check(!manifest.dependencies.build.is_grouped, "other phases stay flat");
  254. var groups = manifest.dependencies.runtime.ordered_groups();
  255. check(groups.length == 2, "both candidate groups parse");
  256. check(groups[0].to_string(r => r.to_string(), ",") == "bin:python3,pc:python3.pc", "group order follows manifest order");
  257. check(groups[1].length == 2, "second group members parse");
  258. check(manifest.dependencies.runtime.ordered_all_refs().length == 4, "all_refs unions across groups");
  259. check(manifest.dependencies.runtime.required.count() == 0, "grouped phase keeps the flat required set empty");
  260. }
  261. void test_dependencies_mixed_rejected() throws Error {
  262. string[] phases = { "runtime", "build", "manage", "acquire" };
  263. var rejected = 0;
  264. foreach(var phase in phases) {
  265. try {
  266. manifest_from_json(PHASE_MANIFEST.printf(depends_with(phase, "[\"bin:bash\", [\"bin:other\"]]")));
  267. print("FAIL mixed %s phase should not parse\n", phase);
  268. failures++;
  269. }
  270. catch(Usm.ManifestError e) {
  271. check(e.message.contains(phase), @"mixed-form rejection names the phase ($phase)");
  272. rejected++;
  273. }
  274. }
  275. check(rejected == phases.length, "every phase rejects mixed flat and nested forms");
  276. }
  277. void test_dependencies_empty_group() throws Error {
  278. try {
  279. manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": [[]], \"build\": [], \"manage\": [] }"));
  280. check(false, "empty candidate group is rejected");
  281. }
  282. catch(Usm.ManifestError e) {
  283. check(e.message.contains("runtime"), "empty-group rejection names the phase");
  284. }
  285. try {
  286. manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": [[\"bin:bash\"], []], \"build\": [], \"manage\": [] }"));
  287. check(false, "empty candidate group among groups is rejected");
  288. }
  289. catch(Usm.ManifestError e) {
  290. check(e.message.contains("empty"), "empty-group rejection explains itself");
  291. }
  292. try {
  293. manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": [[[\"bin:bash\"]]], \"build\": [], \"manage\": [] }"));
  294. check(false, "nesting deeper than groups is rejected");
  295. }
  296. catch(Usm.ManifestError e) {
  297. check(true, "nesting deeper than groups is rejected");
  298. }
  299. try {
  300. manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": \"bin:bash\", \"build\": [], \"manage\": [] }"));
  301. check(false, "non-array phase value is rejected");
  302. }
  303. catch(Usm.ManifestError e) {
  304. check(e.message.contains("runtime"), "non-array phase rejection names the phase");
  305. }
  306. var empty_outer = manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": [], \"build\": [], \"manage\": [] }"));
  307. check(!empty_outer.dependencies.runtime.is_grouped && empty_outer.dependencies.runtime.required.count() == 0,
  308. "empty array parses as a flat phase with no requirements");
  309. }
  310. void test_dependencies_roundtrip() throws Error {
  311. var nested = manifest_from_json(PHASE_MANIFEST.printf(
  312. "{ \"runtime\": [[\"bin:python3\", \"pc:python3.pc\"], [\"bin:python\"]], \"build\": [\"bin:make\"], \"manage\": [] }"));
  313. var properties = Usm.Manifest.get_mapper().map_from(nested);
  314. var serialised = ((!)new JsonElement.from_properties(properties)).stringify_pretty();
  315. var reparsed = manifest_from_json(serialised);
  316. check(reparsed.dependencies.runtime.is_grouped, "grouped phase survives serialisation");
  317. check(reparsed.dependencies.runtime.ordered_groups().length == 2, "group count survives serialisation");
  318. check(reparsed.dependencies.runtime.ordered_groups()[0].to_string(r => r.to_string(), ",") == "bin:python3,pc:python3.pc",
  319. "group members survive serialisation");
  320. check(reparsed.dependencies.build.ordered_required().first().to_string() == "bin:make", "flat phase survives serialisation");
  321. check(!reparsed.dependencies.runtime.is_grouped == false, "grouped marker consistent after roundtrip");
  322. var flat = manifest_from_json(PHASE_MANIFEST.printf("{ \"runtime\": [\"bin:bash\"], \"build\": [], \"manage\": [] }"));
  323. var flat_serialised = ((!)new JsonElement.from_properties(Usm.Manifest.get_mapper().map_from(flat))).stringify_pretty();
  324. var flat_reparsed = manifest_from_json(flat_serialised);
  325. check(!flat_reparsed.dependencies.runtime.is_grouped && flat_reparsed.dependencies.runtime.required.count() == 1,
  326. "flat phase roundtrips as flat");
  327. }
  328. // ---- Topological ordering ----------------------------------------------------
  329. /** A package manifest named pkg-<name> providing bin:pkg-<name> with the given runtime refs. */
  330. Usm.AbstractPackage topo_package(string name, string[] runtime_refs) throws Error {
  331. var refs = new StringBuilder();
  332. foreach(var resource_ref in runtime_refs) {
  333. if(refs.len > 0) {
  334. refs.append(",");
  335. }
  336. refs.append_printf("\"%s\"", resource_ref);
  337. }
  338. var json = """
  339. {
  340. "name": "pkg-%s",
  341. "version": "1.0.0",
  342. "summary": "topo",
  343. "licences": [],
  344. "flags": [],
  345. "provides": { "bin:pkg-%s": "as-expected" },
  346. "depends": { "runtime": [%s], "build": [], "manage": [] },
  347. "execs": {}
  348. }
  349. """.printf(name, name, refs.str);
  350. return new Usm.AbstractPackage.from_manifest(manifest_from_json(json));
  351. }
  352. void test_topological_order_linear() throws Error {
  353. var a = topo_package("a", {});
  354. var b = topo_package("b", { "bin:pkg-a" });
  355. var c = topo_package("c", { "bin:pkg-b" });
  356. var order = Usm.Resolver.topological_order(Iterate.these<Usm.AbstractPackage>(a, b, c));
  357. check(order.to_string(p => p.manifest.name, ",") == "pkg-a,pkg-b,pkg-c", "linear chain orders deps first");
  358. }
  359. void test_topological_order_diamond() throws Error {
  360. var a = topo_package("a", {});
  361. var b = topo_package("b", { "bin:pkg-a" });
  362. var c = topo_package("c", { "bin:pkg-a" });
  363. var d = topo_package("d", { "bin:pkg-b", "bin:pkg-c" });
  364. var order = Usm.Resolver.topological_order(Iterate.these<Usm.AbstractPackage>(d, c, b, a));
  365. 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");
  366. }
  367. void test_topological_order_name_tiebreak() throws Error {
  368. var z = topo_package("z", { "bin:pkg-c", "bin:pkg-b" });
  369. var c = topo_package("c", {});
  370. var b = topo_package("b", {});
  371. var order = Usm.Resolver.topological_order(Iterate.these<Usm.AbstractPackage>(z, c, b));
  372. check(order.to_string(p => p.manifest.name, ",") == "pkg-b,pkg-c,pkg-z", "independent packages tie-break by name");
  373. }
  374. void test_topological_order_cycle() throws Error {
  375. var x = topo_package("x", { "bin:pkg-y" });
  376. var y = topo_package("y", { "bin:pkg-x" });
  377. try {
  378. Usm.Resolver.topological_order(Iterate.these<Usm.AbstractPackage>(x, y));
  379. check(false, "cycle is a hard error");
  380. }
  381. catch(Usm.ResolverError e) {
  382. check(e.message.contains("pkg-x") && e.message.contains("pkg-y"), "cycle error names the packages in the cycle");
  383. check(e.message.contains("->"), "cycle error shows the cycle path");
  384. }
  385. }
  386. // ---- Resolver: USM-only resolution, SPM precedence, group selection ----------
  387. /** A tiny fake system package manager helper honouring the query/install contracts. */
  388. const string SPM_STUB_SCRIPT = """#!/bin/bash
  389. mode="$1"; shift
  390. echo "$mode $*" >> "$USM_SPM_LOG"
  391. case "$mode" in
  392. query)
  393. python3 - "$USM_SPM_FIXTURE" "$@" <<'PY'
  394. import json, sys
  395. with open(sys.argv[1]) as handle:
  396. fixture = json.load(handle)
  397. refs = sys.argv[2:]
  398. packages = fixture.get("packages", [])
  399. known = [p for p in packages if any(r in p["resources"] for r in refs)]
  400. provided = set()
  401. for package in known:
  402. provided.update(package["resources"])
  403. print(json.dumps({"not-found": [r for r in refs if r not in provided], "packages": known}))
  404. PY
  405. ;;
  406. install)
  407. total=$#
  408. echo "{\"type\":\"begin\",\"total\":$total}"
  409. n=0
  410. for name in "$@"; do
  411. n=$((n+1))
  412. echo "$name" >> "$USM_SPM_INSTALL_LOG"
  413. echo "{\"type\":\"package\",\"name\":\"$name\",\"current\":$n,\"total\":$total,\"progress\":0.5}"
  414. echo "{\"type\":\"package-complete\",\"name\":\"$name\"}"
  415. done
  416. echo "{\"type\":\"complete\",\"status\":\"ok\",\"installed\":$total}"
  417. ;;
  418. *)
  419. exit 2
  420. ;;
  421. esac
  422. """;
  423. string make_scratch() throws Error {
  424. var dir = File.new_build_filename("/tmp", Uuid.string_random());
  425. dir.make_directory();
  426. return dir.get_path();
  427. }
  428. /** One candidate entry in the fake fixture's package table. */
  429. string spm_candidate(string name, string resource, int deps, int installed) {
  430. return "{\"name\": \"%s\", \"resources\": [\"%s\"], \"dependency-count\": %d, \"installed-dependency-count\": %d}".printf(name, resource, deps, installed);
  431. }
  432. Usm.SystemPackageManager stub_manager(string scratch, string candidates_json) throws Error {
  433. var stub_path = Path.build_filename(scratch, "spm-stub.sh");
  434. FileUtils.set_contents(stub_path, SPM_STUB_SCRIPT);
  435. FileUtils.chmod(stub_path, 0755);
  436. var fixture_path = Path.build_filename(scratch, "fixture.json");
  437. FileUtils.set_contents(fixture_path, @"{ \"packages\": [$candidates_json] }");
  438. Environment.set_variable("USM_SPM_FIXTURE", fixture_path, true);
  439. Environment.set_variable("USM_SPM_LOG", Path.build_filename(scratch, "query.log"), true);
  440. Environment.set_variable("USM_SPM_INSTALL_LOG", Path.build_filename(scratch, "install.log"), true);
  441. var config_dir = Path.build_filename(scratch, "config");
  442. DirUtils.create(config_dir, 0700);
  443. FileUtils.set_contents(Path.build_filename(config_dir, "usm.config"),
  444. "{\"is_managed\": false, \"system_package_manager\": { \"query\": [\"%s\", \"query\"], \"install\": [\"%s\", \"install\"] }}".printf(stub_path, stub_path));
  445. var paths = new Usm.Paths.defaults();
  446. paths.usm_config_dir = config_dir;
  447. return new Usm.SystemPackageManager(new Usm.Configuration.from_paths(paths));
  448. }
  449. /** Builds a .usmc archive for pkg-<name> providing bin:pkg-<name> with the given runtime refs. */
  450. string make_package_archive(string scratch, string name, string[] runtime_refs) throws Error {
  451. var source = Path.build_filename(scratch, @"src-$name");
  452. DirUtils.create(source, 0755);
  453. var refs = new StringBuilder();
  454. foreach(var resource_ref in runtime_refs) {
  455. if(refs.len > 0) {
  456. refs.append(",");
  457. }
  458. refs.append_printf("\"%s\"", resource_ref);
  459. }
  460. FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"),
  461. APP_MANIFEST.printf(@"pkg-$name", @"pkg-$name", depends_with("runtime", "[" + refs.str + "]")));
  462. var archive = Path.build_filename(scratch, @"pkg-$name.usmc");
  463. Usm.Util.archive(source, archive);
  464. return archive;
  465. }
  466. /**
  467. * Builds a .usmc archive for pkg-<name> providing provides_key with the
  468. * given BUILD-phase refs (used for lot-admission fixtures).
  469. */
  470. string make_build_package_archive(string scratch, string name, string provides_key, string[] build_refs) throws Error {
  471. var source = Path.build_filename(scratch, @"src-$name");
  472. DirUtils.create(source, 0755);
  473. var refs = new StringBuilder();
  474. foreach(var resource_ref in build_refs) {
  475. if(refs.len > 0) {
  476. refs.append(",");
  477. }
  478. refs.append_printf("\"%s\"", resource_ref);
  479. }
  480. FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"), """
  481. {
  482. "name": "pkg-%s",
  483. "version": "1.0.0",
  484. "summary": "lots",
  485. "licences": [],
  486. "flags": [],
  487. "provides": { "%s": "as-expected" },
  488. "depends": { "runtime": [], "build": [%s], "manage": [] },
  489. "execs": {}
  490. }
  491. """.printf(name, provides_key, refs.str));
  492. var archive = Path.build_filename(scratch, @"pkg-$name.usmc");
  493. Usm.Util.archive(source, archive);
  494. return archive;
  495. }
  496. /** Caches pkg-<name>'s archive as a CachedPackage and returns it. */
  497. Usm.CachedPackage cache_package(string scratch, string name) throws Error {
  498. var cache_path = Path.build_filename(scratch, @"cache-pkg-$name-1.0.0");
  499. DirUtils.create(cache_path, 0755);
  500. File.new_for_path(Path.build_filename(scratch, @"pkg-$name.usmc"))
  501. .copy(File.new_for_path(Path.build_filename(cache_path, "package.usmc")), FileCopyFlags.OVERWRITE);
  502. return new Usm.CachedPackage(cache_path);
  503. }
  504. Usm.ResolutionResult resolve_manifest(Usm.SystemPackageManager? spm, string manifest_json) throws Error {
  505. var resolver = new Usm.Resolver(new Usm.ResourceFinder());
  506. var roots = new Vector<Usm.AbstractPackage>();
  507. roots.add(new Usm.AbstractPackage.from_manifest(manifest_from_json(manifest_json)));
  508. return resolver.resolve(roots, spm);
  509. }
  510. void test_resolve_diamond_usm_only() throws Error {
  511. var scratch = make_scratch();
  512. var resolver = new Usm.Resolver(new Usm.ResourceFinder());
  513. resolver.supply_package(make_package_archive(scratch, "a", {}));
  514. resolver.supply_package(make_package_archive(scratch, "b", { "bin:pkg-a" }));
  515. resolver.supply_package(make_package_archive(scratch, "c", { "bin:pkg-a" }));
  516. resolver.supply_package(make_package_archive(scratch, "d", { "bin:pkg-b", "bin:pkg-c" }));
  517. var roots = new Vector<Usm.AbstractPackage>();
  518. roots.add(((!)resolver.find_package("pkg-d")));
  519. var result = resolver.resolve(roots, null);
  520. check(result.packages.count() == 4, "USM-only resolution closes the diamond");
  521. check(result.install_order.to_string(p => p.manifest.name, ",") == "pkg-a,pkg-b,pkg-c,pkg-d", "install order is dependencies-first");
  522. 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");
  523. check(result.system_packages.length == 0, "no system packages are chosen without an SPM");
  524. Usm.Util.delete_tree(scratch);
  525. }
  526. void test_resolve_spm_before_usm() throws Error {
  527. var scratch = make_scratch();
  528. var resolver = new Usm.Resolver(new Usm.ResourceFinder());
  529. resolver.supply_package(make_package_archive(scratch, "x", {}));
  530. var roots = new Vector<Usm.AbstractPackage>();
  531. roots.add(new Usm.AbstractPackage.from_manifest(manifest_from_json(
  532. APP_MANIFEST.printf("spm-app", "spm-app", depends_with("runtime", "[\"bin:pkg-x\"]")))));
  533. var spm = stub_manager(scratch, spm_candidate("spm-owns-x", "bin:pkg-x", 3, 1));
  534. var result = resolver.resolve(roots, spm);
  535. check(result.system_packages.length == 1 && result.system_packages[0].name == "spm-owns-x", "a system package wins over an equally-good USM provider");
  536. check(result.packages.count() == 1, "the USM provider is not pulled when the SPM satisfies the ref");
  537. Usm.Util.delete_tree(scratch);
  538. }
  539. void test_group_selection_cheaper_wins() throws Error {
  540. var scratch = make_scratch();
  541. var spm = stub_manager(scratch,
  542. spm_candidate("expensive", "bin:usmt-a1", 5, 0) + "," + spm_candidate("cheap", "bin:usmt-b1", 5, 3));
  543. var result = resolve_manifest(spm, APP_MANIFEST.printf("grouped-app", "grouped-app",
  544. depends_with("runtime", "[[\"bin:usmt-a1\"], [\"bin:usmt-b1\"]]")));
  545. check(result.system_packages.length == 1 && result.system_packages[0].name == "cheap",
  546. "the group whose candidate costs fewer NEW installs wins (dependency-count minus installed-dependency-count)");
  547. Usm.Util.delete_tree(scratch);
  548. }
  549. void test_group_selection_tie_manifest_order() throws Error {
  550. var scratch = make_scratch();
  551. var spm = stub_manager(scratch,
  552. spm_candidate("first-pkg", "bin:usmt-t1", 2, 0) + "," + spm_candidate("second-pkg", "bin:usmt-t2", 2, 0));
  553. var result = resolve_manifest(spm, APP_MANIFEST.printf("grouped-app", "grouped-app",
  554. depends_with("runtime", "[[\"bin:usmt-t1\"], [\"bin:usmt-t2\"]]")));
  555. check(result.system_packages.length == 1 && result.system_packages[0].name == "first-pkg",
  556. "equal-cost groups tie-break to manifest order");
  557. Usm.Util.delete_tree(scratch);
  558. }
  559. void test_group_selection_none_viable_itemised() throws Error {
  560. var scratch = make_scratch();
  561. var spm = stub_manager(scratch, spm_candidate("irrelevant", "bin:usmt-other", 1, 0));
  562. try {
  563. resolve_manifest(spm, APP_MANIFEST.printf("grouped-app", "grouped-app",
  564. depends_with("runtime", "[[\"bin:usmt-a1\", \"bin:usmt-a2\"], [\"bin:usmt-b1\"]]")));
  565. check(false, "no viable group is a hard error");
  566. }
  567. catch(Usm.ResolverError e) {
  568. check(e.message.contains("Group 1") && e.message.contains("Group 2"), "itemised failure lists every group");
  569. check(e.message.contains("bin:usmt-a1") && e.message.contains("bin:usmt-a2") && e.message.contains("bin:usmt-b1"),
  570. "itemised failure lists every missing ref");
  571. check(e.message.contains("no system package provides it"), "itemised failure explains why each ref failed");
  572. }
  573. Usm.Util.delete_tree(scratch);
  574. }
  575. void test_group_selection_single_batched_query() throws Error {
  576. var scratch = make_scratch();
  577. var spm = stub_manager(scratch,
  578. spm_candidate("first-pkg", "bin:usmt-q1", 1, 0) + "," +
  579. spm_candidate("second-pkg", "bin:usmt-q2", 2, 0) + "," +
  580. spm_candidate("builddep-pkg", "bin:usmt-build1", 1, 0));
  581. resolve_manifest(spm, APP_MANIFEST.printf("grouped-app", "grouped-app",
  582. "{ \"runtime\": [[\"bin:usmt-q1\"], [\"bin:usmt-q2\", \"bin:usmt-q3\"]], \"build\": [\"bin:usmt-build1\"], \"manage\": [] }"));
  583. string log;
  584. FileUtils.get_contents(Path.build_filename(scratch, "query.log"), out log);
  585. var queries = log.split("\n");
  586. var query_lines = 0;
  587. var query_line = "";
  588. foreach(var line in queries) {
  589. if(line.has_prefix("query")) {
  590. query_lines++;
  591. query_line = line;
  592. }
  593. }
  594. check(query_lines == 1, "resolution issues exactly one batched SPM query");
  595. 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"),
  596. "the batched query covers refs from every group and phase");
  597. Usm.Util.delete_tree(scratch);
  598. }
  599. void test_system_package_manager_install_stub() throws Error {
  600. var scratch = make_scratch();
  601. var spm = stub_manager(scratch, "");
  602. var names = new Vector<string>();
  603. names.add("one");
  604. names.add("two");
  605. var begin_events = 0;
  606. var complete_events = 0;
  607. var installed_count = 0;
  608. var install_ok = false;
  609. var loop = new MainLoop();
  610. spm.install.begin(names, event => {
  611. if(event.event_type == Usm.SystemInstallEventType.BEGIN) {
  612. begin_events++;
  613. }
  614. if(event.event_type == Usm.SystemInstallEventType.COMPLETE) {
  615. complete_events++;
  616. installed_count = event.installed;
  617. }
  618. }, (obj, res) => {
  619. try {
  620. install_ok = spm.install.end(res);
  621. }
  622. catch(Error e) {
  623. install_ok = false;
  624. }
  625. loop.quit();
  626. });
  627. loop.run();
  628. check(install_ok, "stub install transaction reports success");
  629. check(begin_events == 1 && complete_events == 1, "begin and complete events stream once each");
  630. check(installed_count == 2, "complete event carries the installed count");
  631. string install_log;
  632. FileUtils.get_contents(Path.build_filename(scratch, "install.log"), out install_log);
  633. check(install_log.split("\n").length == 3 && install_log.has_prefix("one\ntwo"), "install helper received the package names in order");
  634. Usm.Util.delete_tree(scratch);
  635. }
  636. void test_transaction_accepts_orders() throws Error {
  637. var scratch = make_scratch();
  638. make_package_archive(scratch, "a", {});
  639. make_package_archive(scratch, "b", { "bin:pkg-a" });
  640. make_package_archive(scratch, "c", { "bin:pkg-a" });
  641. var to_install = new HashSet<Usm.CachedPackage>();
  642. var to_remove = new HashSet<Usm.CachedPackage>();
  643. foreach(var name in new string[] { "a", "b", "c" }) {
  644. var cache_path = Path.build_filename(scratch, @"cache-pkg-$name-1.0.0");
  645. DirUtils.create(cache_path, 0755);
  646. File.new_for_path(Path.build_filename(scratch, @"pkg-$name.usmc"))
  647. .copy(File.new_for_path(Path.build_filename(cache_path, "package.usmc")), FileCopyFlags.OVERWRITE);
  648. var cached = new Usm.CachedPackage(cache_path);
  649. to_install.add(cached);
  650. to_remove.add(cached);
  651. }
  652. var install_order = new Vector<string>();
  653. install_order.add("pkg-c");
  654. install_order.add("pkg-a");
  655. install_order.add("pkg-b");
  656. var remove_order = new Vector<string>();
  657. remove_order.add("pkg-b");
  658. remove_order.add("pkg-c");
  659. remove_order.add("pkg-a");
  660. var transaction = new Usm.Transaction() {
  661. paths = new Usm.Paths(),
  662. resource_finder = new Usm.ResourceFinder(),
  663. to_install = to_install,
  664. to_remove = to_remove,
  665. install_order = install_order,
  666. remove_order = remove_order
  667. };
  668. transaction.strategise();
  669. check(transaction.install_lots.length == 1, "packages without installtime deps form one lot");
  670. 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");
  671. 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");
  672. var short_order = new Vector<string>();
  673. short_order.add("pkg-a");
  674. var partial = new Usm.Transaction() {
  675. paths = new Usm.Paths(),
  676. resource_finder = new Usm.ResourceFinder(),
  677. to_install = to_install,
  678. to_remove = new HashSet<Usm.CachedPackage>(),
  679. install_order = short_order
  680. };
  681. try {
  682. partial.strategise();
  683. check(false, "an order missing packages is rejected");
  684. }
  685. catch(Usm.TransactionError e) {
  686. check(e.message.contains("pkg-b") || e.message.contains("pkg-c"), "the rejection names the uncovered packages");
  687. }
  688. Usm.Util.delete_tree(scratch);
  689. }
  690. void test_transaction_lots_split_at_build_boundaries() throws Error {
  691. var scratch = make_scratch();
  692. make_build_package_archive(scratch, "a", "pc:lib-lot-a.pc", {});
  693. make_build_package_archive(scratch, "b", "pc:lib-lot-b.pc", { "pc:lib-lot-a.pc" });
  694. make_build_package_archive(scratch, "c", "pc:lib-lot-c.pc", { "pc:lib-lot-a.pc" });
  695. make_build_package_archive(scratch, "d", "pc:lib-lot-d.pc", { "pc:lib-lot-b.pc", "pc:lib-lot-c.pc" });
  696. var to_install = new HashSet<Usm.CachedPackage>();
  697. foreach(var name in new string[] { "a", "b", "c", "d" }) {
  698. to_install.add(cache_package(scratch, name));
  699. }
  700. var transaction = new Usm.Transaction() {
  701. paths = new Usm.Paths(),
  702. resource_finder = new Usm.ResourceFinder(),
  703. to_install = to_install,
  704. to_remove = new HashSet<Usm.CachedPackage>()
  705. };
  706. transaction.strategise();
  707. check(transaction.install_lots.length == 3, "a build-dependency diamond splits into one lot per build level");
  708. check(transaction.install_lots[0].to_string(p => p.package_name, ",") == "cache-pkg-a-1.0.0",
  709. "the provider of the shared pc: file builds and installs alone in lot 1");
  710. check(transaction.install_lots[1].to_string(p => p.package_name, ",") == "cache-pkg-b-1.0.0,cache-pkg-c-1.0.0",
  711. "packages whose builds need lot 1's install share lot 2");
  712. check(transaction.install_lots[2].to_string(p => p.package_name, ",") == "cache-pkg-d-1.0.0",
  713. "the diamond tip waits for both lot 2 installs");
  714. Usm.Util.delete_tree(scratch);
  715. }
  716. void test_transaction_manage_deps_admit_same_lot() throws Error {
  717. var scratch = make_scratch();
  718. make_package_archive(scratch, "m1", {});
  719. // m2's manage phase needs m1's bin:, but its build phase needs
  720. // nothing from the transaction — both belong in one lot because the
  721. // manage executable runs at install time, after m1 installs
  722. var source = Path.build_filename(scratch, "src-m2");
  723. DirUtils.create(source, 0755);
  724. FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"),
  725. APP_MANIFEST.printf("pkg-m2", "pkg-m2", depends_with("manage", "[\"bin:pkg-m1\"]")));
  726. var to_install = new HashSet<Usm.CachedPackage>();
  727. to_install.add(cache_package(scratch, "m1"));
  728. var m2_archive = Path.build_filename(scratch, "pkg-m2.usmc");
  729. Usm.Util.archive(source, m2_archive);
  730. to_install.add(cache_package(scratch, "m2"));
  731. var transaction = new Usm.Transaction() {
  732. paths = new Usm.Paths(),
  733. resource_finder = new Usm.ResourceFinder(),
  734. to_install = to_install,
  735. to_remove = new HashSet<Usm.CachedPackage>()
  736. };
  737. transaction.strategise();
  738. check(transaction.install_lots.length == 1, "manage-phase refs satisfiable within the lot keep one lot");
  739. check(transaction.install_lots[0].to_string(p => p.package_name, ",") == "cache-pkg-m1-1.0.0,cache-pkg-m2-1.0.0",
  740. "the manage dependency orders its provider first inside the lot");
  741. Usm.Util.delete_tree(scratch);
  742. }
  743. // ---- rebuildDependants: lookup, planning, dedup ------------------------------
  744. /** Manifest for a package named <name> at <version> providing one key with the given flags and depends. */
  745. const string REBUILD_MANIFEST = """
  746. {
  747. "name": "%s",
  748. "version": "%s",
  749. "summary": "rebuild fixtures",
  750. "licences": [],
  751. "flags": [%s],
  752. "provides": { "%s": "as-expected" },
  753. "depends": %s,
  754. "execs": {}
  755. }
  756. """;
  757. /**
  758. * Caches a package under <state_dir>/packages/<name>-<version> built
  759. * from {@link REBUILD_MANIFEST}, optionally marks it installed, and
  760. * returns its {@link Usm.CachedPackage}.
  761. */
  762. Usm.CachedPackage scratch_package(string state_dir, string name, string version, string provides, string flags, string depends, bool installed) throws Error {
  763. var source = Path.build_filename(state_dir, @"src-$name-$version");
  764. DirUtils.create(source, 0755);
  765. FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"),
  766. REBUILD_MANIFEST.printf(name, version, flags, provides, depends));
  767. var cache_path = Path.build_filename(state_dir, "packages", @"$name-$version");
  768. DirUtils.create_with_parents(cache_path, 0755);
  769. Usm.Util.archive(source, Path.build_filename(cache_path, "package.usmc"));
  770. Usm.Util.delete_tree(source);
  771. if(installed) {
  772. File.new_build_filename(state_dir, "installed", @"$name-$version").make_symbolic_link(cache_path);
  773. }
  774. return new Usm.CachedPackage(cache_path);
  775. }
  776. /** A managed {@link Usm.SystemState} over a scratch tree: usm.config plus empty state directories. */
  777. Usm.SystemState make_state(string scratch) throws Error {
  778. var config_dir = Path.build_filename(scratch, "config");
  779. var state_dir = Path.build_filename(scratch, "state");
  780. DirUtils.create_with_parents(config_dir, 0700);
  781. DirUtils.create_with_parents(state_dir, 0700);
  782. foreach(var part in new string[] { "packages", "installed", "lists" }) {
  783. DirUtils.create(Path.build_filename(state_dir, part), 0700);
  784. }
  785. FileUtils.set_contents(Path.build_filename(config_dir, "usm.config"),
  786. "{\"is_managed\": true, \"managed\": {\"state_path\": \"%s\"}}".printf(state_dir));
  787. var paths = new Usm.Paths();
  788. paths.usm_config_dir = config_dir;
  789. return new Usm.SystemState(paths);
  790. }
  791. void test_find_dependant_names() throws Error {
  792. var scratch = make_scratch();
  793. var state = make_state(scratch);
  794. var state_dir = Path.build_filename(scratch, "state");
  795. var updated = scratch_package(state_dir, "dep-a", "1.0.0", "pc:dep-a.pc", "", depends_with("runtime", "[]"), false);
  796. // an older installed dep-a depending on its own provides is not its own dependant
  797. scratch_package(state_dir, "dep-a", "0.9.0", "pc:dep-a.pc", "", depends_with("runtime", "[\"pc:dep-a.pc\"]"), true);
  798. scratch_package(state_dir, "dep-b", "0.1", "bin:dep-b", "", depends_with("build", "[\"pc:dep-a.pc\"]"), true);
  799. scratch_package(state_dir, "dep-c", "2.0", "bin:dep-c", "",
  800. "{ \"runtime\": [[\"pc:dep-a.pc\", \"opt:other\"], [\"pc:elsewhere.pc\"]], \"build\": [], \"manage\": [] }", true);
  801. scratch_package(state_dir, "unrelated", "1.0", "bin:unrelated", "", depends_with("runtime", "[\"bin:bash\"]"), true);
  802. var dependants = state.find_dependant_names(updated);
  803. check(dependants.length == 2, "exactly the two dependants are found");
  804. check(dependants.any(n => n == "dep-b-0.1"), "a build-phase dependant is found");
  805. check(dependants.any(n => n == "dep-c-2.0"), "a grouped-phase dependant is found via any candidate group");
  806. check(dependants.no(n => n == "unrelated-1.0"), "a package with no dependency on the update is not found");
  807. check(dependants.no(n => n == "dep-a-0.9.0"), "an older installed version of the update is not its own dependant");
  808. // resource-level matching: a canonlib: provide satisfies a lib: dependency
  809. var canonical = scratch_package(state_dir, "dep-libc", "1.0", "canonlib:dep-lib.so", "", depends_with("runtime", "[]"), false);
  810. scratch_package(state_dir, "dep-lib", "3.0", "lib:dep-lib.so", "", depends_with("runtime", "[\"lib:dep-lib.so\"]"), true);
  811. check(state.find_dependant_names(canonical).any(n => n == "dep-lib-3.0"), "a canonlib provide satisfies a lib dependency");
  812. Usm.Util.delete_tree(scratch);
  813. }
  814. void test_rebuild_planning() throws Error {
  815. var scratch = make_scratch();
  816. var state = make_state(scratch);
  817. var state_dir = Path.build_filename(scratch, "state");
  818. // two flagged providers, each with an incoming update
  819. scratch_package(state_dir, "liba", "1.0.0", "pc:liba.pc", "\"rebuildDependants\"", depends_with("runtime", "[]"), true);
  820. var liba_update = scratch_package(state_dir, "liba", "2.0.0", "pc:liba.pc", "\"rebuildDependants\"", depends_with("runtime", "[]"), false);
  821. scratch_package(state_dir, "libx", "1.0.0", "pc:libx.pc", "\"rebuildDependants\"", depends_with("runtime", "[]"), true);
  822. var libx_update = scratch_package(state_dir, "libx", "2.0.0", "pc:libx.pc", "\"rebuildDependants\"", depends_with("runtime", "[]"), false);
  823. // statum depends on liba in its build phase and libx at runtime
  824. scratch_package(state_dir, "statum", "0.1", "bin:statum", "",
  825. "{ \"runtime\": [\"pc:libx.pc\"], \"build\": [\"pc:liba.pc\"], \"manage\": [] }", true);
  826. // client 1.0.0 depends on liba, but client 2.0.0 joins the transaction explicitly
  827. scratch_package(state_dir, "client", "1.0.0", "bin:client", "", depends_with("build", "[\"pc:liba.pc\"]"), true);
  828. var client_update = scratch_package(state_dir, "client", "2.0.0", "bin:client", "", depends_with("build", "[\"pc:liba.pc\"]"), false);
  829. // libz updates without the flag, so its dependant must NOT rebuild
  830. var libz_update = scratch_package(state_dir, "libz", "2.0.0", "pc:libz.pc", "", depends_with("runtime", "[]"), false);
  831. scratch_package(state_dir, "user", "1.0.0", "bin:user", "", depends_with("build", "[\"pc:libz.pc\"]"), true);
  832. var dependants = state.find_dependant_names(liba_update);
  833. check(dependants.length == 2 && dependants.any(n => n == "statum-0.1") && dependants.any(n => n == "client-1.0.0"),
  834. "the lookup sees both of liba's installed dependants before filtering");
  835. var to_install = new HashSet<Usm.CachedPackage>();
  836. to_install.add(liba_update);
  837. to_install.add(libx_update);
  838. to_install.add(client_update);
  839. to_install.add(libz_update);
  840. var transaction = new Usm.Transaction() {
  841. paths = new Usm.Paths(),
  842. resource_finder = new Usm.ResourceFinder(),
  843. to_install = to_install,
  844. to_remove = new HashSet<Usm.CachedPackage>(),
  845. state = state
  846. };
  847. transaction.strategise();
  848. check(transaction.rebuilds.length == 1, "one rebuild is planned across two flagged providers");
  849. check(transaction.rebuilds[0].package.package_name == "statum-0.1", "the shared dependant statum rebuilds exactly once");
  850. check(transaction.rebuilds[0].trigger.package_name == "liba-2.0.0" || transaction.rebuilds[0].trigger.package_name == "libx-2.0.0",
  851. "the rebuild records the flagged trigger");
  852. check(transaction.rebuilds[0].package.state_path == Path.build_filename(state_dir, "packages", "statum-0.1"),
  853. "the rebuild reuses the installed package's own state path");
  854. var rebuilt = new HashSet<string>();
  855. foreach(var entry in transaction.rebuilds) {
  856. rebuilt.add(entry.package.package_name);
  857. }
  858. check(!rebuilt.has("client-1.0.0"), "a dependant already in to_install is not added as a rebuild");
  859. check(!rebuilt.has("user-1.0.0"), "dependants of unflagged providers are not rebuilt");
  860. var lot_names = new StringBuilder();
  861. foreach(var lot in transaction.install_lots) {
  862. lot_names.append(lot.to_string(p => p.package_name, ","));
  863. }
  864. check(!lot_names.str.contains("statum"), "a rebuild target never also builds through the install lots");
  865. Usm.Util.delete_tree(scratch);
  866. }
  867. // ---- explicit-install tracking ------------------------------------------------
  868. void test_origin_information_explicit_install() throws Error {
  869. var scratch = make_scratch();
  870. var cache_path = Path.build_filename(scratch, "cache-explicit-pkg-1.0.0");
  871. DirUtils.create(cache_path, 0755);
  872. var cached = new Usm.CachedPackage(cache_path);
  873. var explicit_record = new Usm.OriginInformation() {
  874. repository = "repo",
  875. listfile = "2020-03-20T14:34:42.382748.usml",
  876. original_path = "explicit-pkg-1.0.0.usmc",
  877. signature_verified = true,
  878. explicitly_installed = true
  879. };
  880. cached.update_origin_information(explicit_record);
  881. var read_explicit = cached.get_origin_information();
  882. check(read_explicit.explicitly_installed, "explicitly_installed true survives an origin-info round trip");
  883. check(read_explicit.repository == "repo" && read_explicit.signature_verified, "provenance fields survive the round trip");
  884. var implicit_record = new Usm.OriginInformation();
  885. implicit_record.explicitly_installed = false;
  886. cached.update_origin_information(implicit_record);
  887. var read_implicit = cached.get_origin_information();
  888. check(!read_implicit.explicitly_installed, "explicitly_installed false survives an origin-info round trip");
  889. check(read_implicit.repository == null, "a transaction-written record without provenance round-trips null fields");
  890. FileUtils.set_contents(Path.build_filename(cache_path, "origin-info"),
  891. "{\"repository\": \"repo\", \"listfile\": \"2020-03-20T14:34:42.382748.usml\", \"original_path\": \"explicit-pkg-1.0.0.usmc\", \"signature_verified\": true}");
  892. check(!cached.get_origin_information().explicitly_installed, "an origin-info file without the field defaults to false");
  893. Usm.Util.delete_tree(scratch);
  894. }
  895. /** No-op build script: the install pipeline only needs it to succeed. */
  896. const string NOOP_BUILD_SCRIPT = """#!/bin/bash
  897. exit 0
  898. """;
  899. /**
  900. * Caches a build-only package named <name> at 1.0.0 with a
  901. * succeeding build script under <scratch>'s managed state and
  902. * returns its cache path.
  903. */
  904. string explicit_flag_package(string scratch, string name) throws Error {
  905. var cache_path = Path.build_filename(scratch, "state", "packages", @"$name-1.0.0");
  906. DirUtils.create_with_parents(cache_path, 0755);
  907. var source = Path.build_filename(scratch, @"src-$name");
  908. DirUtils.create(source, 0755);
  909. FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"), BUILD_MANIFEST.printf(name, "1.0.0"));
  910. FileUtils.set_contents(Path.build_filename(source, "build.sh"), NOOP_BUILD_SCRIPT);
  911. FileUtils.chmod(Path.build_filename(source, "build.sh"), 0755);
  912. Usm.Util.archive(source, Path.build_filename(cache_path, "package.usmc"));
  913. Usm.Util.delete_tree(source);
  914. return cache_path;
  915. }
  916. void test_transaction_records_explicit_flags() throws Error {
  917. var original_dir = Environment.get_current_dir();
  918. var scratch = make_scratch();
  919. var state = make_state(scratch);
  920. var named_path = explicit_flag_package(scratch, "flag-named");
  921. var resolved_path = explicit_flag_package(scratch, "flag-resolved");
  922. var to_install = new HashSet<Usm.CachedPackage>();
  923. to_install.add(new Usm.CachedPackage(named_path));
  924. to_install.add(new Usm.CachedPackage(resolved_path));
  925. var explicit_packages = new Vector<string>();
  926. explicit_packages.add("flag-named");
  927. var transaction = new Usm.Transaction() {
  928. paths = scratch_paths(scratch),
  929. resource_finder = new Usm.ResourceFinder(),
  930. to_install = to_install,
  931. to_remove = new HashSet<Usm.CachedPackage>(),
  932. state = state,
  933. explicit_packages = explicit_packages
  934. };
  935. transaction.run();
  936. check(new Usm.CachedPackage(named_path).get_origin_information().explicitly_installed,
  937. "a package named in explicit_packages installs with the explicit mark");
  938. check(!new Usm.CachedPackage(resolved_path).get_origin_information().explicitly_installed,
  939. "a package only the resolution pulled in installs without the explicit mark");
  940. var rebuild = new Usm.Transaction() {
  941. paths = scratch_paths(scratch),
  942. resource_finder = new Usm.ResourceFinder(),
  943. to_install = new HashSet<Usm.CachedPackage>(),
  944. to_remove = new HashSet<Usm.CachedPackage>(),
  945. state = state
  946. };
  947. rebuild.rebuild_package(new Usm.CachedPackage(named_path));
  948. check(new Usm.CachedPackage(named_path).get_origin_information().explicitly_installed,
  949. "a rebuild of the same cache directory keeps the explicit mark");
  950. Environment.set_current_dir(original_dir);
  951. Usm.Util.delete_tree(scratch);
  952. }
  953. // ---- build archive restoration + clean retry ---------------------------------
  954. /** Manifest for a build-only package: an executable build script, no provides. */
  955. const string BUILD_MANIFEST = """
  956. {
  957. "name": "%s",
  958. "version": "%s",
  959. "summary": "build cache fixture",
  960. "licences": [],
  961. "flags": [],
  962. "provides": {},
  963. "depends": { "runtime": [], "build": [], "manage": [] },
  964. "execs": { "build": "build.sh" }
  965. }
  966. """;
  967. /** Always fails; logs whether the build directory carried the archived marker. */
  968. const string FAILING_BUILD_SCRIPT = """#!/bin/bash
  969. if [ -f "$1/from-archive" ]; then
  970. echo restored >> "$USM_TEST_BUILD_LOG"
  971. else
  972. echo fresh >> "$USM_TEST_BUILD_LOG"
  973. fi
  974. exit 1
  975. """;
  976. /** Succeeds only against the restored archive, so a retry (fresh) fails. */
  977. const string INCREMENTAL_BUILD_SCRIPT = """#!/bin/bash
  978. if [ -f "$1/from-archive" ]; then
  979. echo restored >> "$USM_TEST_BUILD_LOG"
  980. exit 0
  981. fi
  982. echo fresh >> "$USM_TEST_BUILD_LOG"
  983. exit 1
  984. """;
  985. /** Paths with the destination rooted in <scratch> so nothing touches the real filesystem. */
  986. Usm.Paths scratch_paths(string scratch) {
  987. var paths = new Usm.Paths();
  988. paths.destination = Path.build_filename(scratch, "dest");
  989. return paths;
  990. }
  991. /**
  992. * Caches a build-only package named <name> running <script>, optionally
  993. * seeding its cache with a build.tar.xz holding a "from-archive" marker,
  994. * and returns a transaction over it.
  995. */
  996. Usm.Transaction build_transaction(string scratch, string name, string script, bool with_archive) throws Error {
  997. var state = make_state(scratch);
  998. var cache_path = Path.build_filename(scratch, "state", "packages", @"$name-1.0.0");
  999. DirUtils.create_with_parents(cache_path, 0755);
  1000. var source = Path.build_filename(scratch, @"src-$name");
  1001. DirUtils.create(source, 0755);
  1002. FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"), BUILD_MANIFEST.printf(name, "1.0.0"));
  1003. FileUtils.set_contents(Path.build_filename(source, "build.sh"), script);
  1004. FileUtils.chmod(Path.build_filename(source, "build.sh"), 0755);
  1005. Usm.Util.archive(source, Path.build_filename(cache_path, "package.usmc"));
  1006. Usm.Util.delete_tree(source);
  1007. if(with_archive) {
  1008. var stale_build = Path.build_filename(scratch, "stale-build");
  1009. DirUtils.create(stale_build, 0755);
  1010. FileUtils.set_contents(Path.build_filename(stale_build, "from-archive"), "stale");
  1011. Usm.Util.archive(stale_build, Path.build_filename(cache_path, "build.tar.xz"));
  1012. }
  1013. var to_install = new HashSet<Usm.CachedPackage>();
  1014. to_install.add(new Usm.CachedPackage(cache_path));
  1015. return new Usm.Transaction() {
  1016. paths = scratch_paths(scratch),
  1017. resource_finder = new Usm.ResourceFinder(),
  1018. to_install = to_install,
  1019. to_remove = new HashSet<Usm.CachedPackage>(),
  1020. state = state
  1021. };
  1022. }
  1023. void test_build_archive_restore_and_clean_retry() throws Error {
  1024. var original_dir = Environment.get_current_dir();
  1025. var scratch = make_scratch();
  1026. var log_path = Path.build_filename(scratch, "build.log");
  1027. Environment.set_variable("USM_TEST_BUILD_LOG", log_path, true);
  1028. var transaction = build_transaction(scratch, "fail-pkg", FAILING_BUILD_SCRIPT, true);
  1029. var threw = false;
  1030. try {
  1031. transaction.run();
  1032. }
  1033. catch(Usm.TransactionError e) {
  1034. threw = true;
  1035. check(e.message.contains("fail-pkg"), "the clean-retry failure names the package");
  1036. }
  1037. check(threw, "a failing restored build followed by a failing clean retry fails the transaction");
  1038. string log = "";
  1039. FileUtils.get_contents(log_path, out log);
  1040. check(log == "restored\nfresh\n", "the clean retry fires once after the restored build fails");
  1041. Environment.set_current_dir(original_dir);
  1042. Usm.Util.delete_tree(scratch);
  1043. }
  1044. void test_build_archive_restored_incrementally() throws Error {
  1045. var original_dir = Environment.get_current_dir();
  1046. var scratch = make_scratch();
  1047. var log_path = Path.build_filename(scratch, "build.log");
  1048. Environment.set_variable("USM_TEST_BUILD_LOG", log_path, true);
  1049. var cache_path = Path.build_filename(scratch, "state", "packages", "inc-pkg-1.0.0");
  1050. var transaction = build_transaction(scratch, "inc-pkg", INCREMENTAL_BUILD_SCRIPT, true);
  1051. transaction.run();
  1052. string log = "";
  1053. FileUtils.get_contents(log_path, out log);
  1054. check(log == "restored\n", "a successful build runs against the restored archive without a retry");
  1055. check(File.new_for_path(Path.build_filename(cache_path, "build.tar.xz")).query_exists(),
  1056. "cleanup re-archives the build directory for the next transaction");
  1057. Environment.set_current_dir(original_dir);
  1058. Usm.Util.delete_tree(scratch);
  1059. }
  1060. void test_build_failure_without_cache_propagates() throws Error {
  1061. var original_dir = Environment.get_current_dir();
  1062. var scratch = make_scratch();
  1063. var log_path = Path.build_filename(scratch, "build.log");
  1064. Environment.set_variable("USM_TEST_BUILD_LOG", log_path, true);
  1065. var transaction = build_transaction(scratch, "clean-pkg", FAILING_BUILD_SCRIPT, false);
  1066. var threw = false;
  1067. try {
  1068. transaction.run();
  1069. }
  1070. catch(Usm.TransactionError e) {
  1071. threw = true;
  1072. }
  1073. check(threw, "a failing already-clean build propagates without a retry");
  1074. string log = "";
  1075. FileUtils.get_contents(log_path, out log);
  1076. check(log == "fresh\n", "a build without any cache is attempted exactly once");
  1077. Environment.set_current_dir(original_dir);
  1078. Usm.Util.delete_tree(scratch);
  1079. }
  1080. int main() {
  1081. test_default_ignore();
  1082. try {
  1083. test_ignore_patterns();
  1084. test_ignore_always_included();
  1085. test_ignore_from_root();
  1086. }
  1087. catch(Error e) {
  1088. failures++;
  1089. print("FAIL ignore matcher test threw: %s\n", e.message);
  1090. }
  1091. try {
  1092. test_data_package_flag();
  1093. test_rebuild_dependants_flag();
  1094. test_data_package_validation();
  1095. test_data_package_from_file();
  1096. test_configuration_system_package_manager();
  1097. test_gio_module_ref();
  1098. }
  1099. catch(Error e) {
  1100. failures++;
  1101. print("FAIL data package test threw: %s\n", e.message);
  1102. }
  1103. try {
  1104. test_dependencies_flat_back_compat();
  1105. test_dependencies_nested();
  1106. test_dependencies_mixed_rejected();
  1107. test_dependencies_empty_group();
  1108. test_dependencies_roundtrip();
  1109. }
  1110. catch(Error e) {
  1111. failures++;
  1112. print("FAIL dependencies mapper test threw: %s\n", e.message);
  1113. }
  1114. try {
  1115. test_topological_order_linear();
  1116. test_topological_order_diamond();
  1117. test_topological_order_name_tiebreak();
  1118. test_topological_order_cycle();
  1119. }
  1120. catch(Error e) {
  1121. failures++;
  1122. print("FAIL topological order test threw: %s\n", e.message);
  1123. }
  1124. try {
  1125. test_resolve_diamond_usm_only();
  1126. test_resolve_spm_before_usm();
  1127. test_group_selection_cheaper_wins();
  1128. test_group_selection_tie_manifest_order();
  1129. test_group_selection_none_viable_itemised();
  1130. test_group_selection_single_batched_query();
  1131. test_system_package_manager_install_stub();
  1132. test_transaction_accepts_orders();
  1133. test_transaction_lots_split_at_build_boundaries();
  1134. test_transaction_manage_deps_admit_same_lot();
  1135. }
  1136. catch(Error e) {
  1137. failures++;
  1138. print("FAIL resolver test threw: %s\n", e.message);
  1139. }
  1140. try {
  1141. test_find_dependant_names();
  1142. test_rebuild_planning();
  1143. }
  1144. catch(Error e) {
  1145. failures++;
  1146. print("FAIL rebuild planning test threw: %s\n", e.message);
  1147. }
  1148. try {
  1149. test_origin_information_explicit_install();
  1150. test_transaction_records_explicit_flags();
  1151. }
  1152. catch(Error e) {
  1153. failures++;
  1154. print("FAIL explicit install tracking test threw: %s\n", e.message);
  1155. }
  1156. try {
  1157. test_build_archive_restore_and_clean_retry();
  1158. test_build_archive_restored_incrementally();
  1159. test_build_failure_without_cache_propagates();
  1160. }
  1161. catch(Error e) {
  1162. failures++;
  1163. print("FAIL build cache retry test threw: %s\n", e.message);
  1164. }
  1165. print("%d passed, %d failed\n", passes, failures);
  1166. return failures == 0 ? 0 : 1;
  1167. }
  1168. }