|
|
@@ -232,6 +232,30 @@ namespace Usm.Tests {
|
|
|
"the ResourceFinder locates libgiognutls.so in a gio/modules directory");
|
|
|
}
|
|
|
|
|
|
+ // ---- Repository listing size field -------------------------------------------
|
|
|
+
|
|
|
+ void test_repository_listing_entry_size() throws Error {
|
|
|
+ var mapper = Usm.RepositoryListingEntry.get_mapper();
|
|
|
+ var entry = new Usm.RepositoryListingEntry() {
|
|
|
+ path = "sized-pkg-1.0.0.usmc",
|
|
|
+ manifest = manifest_from_json(APP_MANIFEST.printf("sized-pkg", "sized-pkg", depends_with("runtime", "[]"))),
|
|
|
+ sha512sum = Wrap.byte_array({ 1, 2, 3 }),
|
|
|
+ size = 4096
|
|
|
+ };
|
|
|
+
|
|
|
+ var serialised = ((!)new JsonElement.from_properties(mapper.map_from(entry))).stringify_pretty();
|
|
|
+ check(serialised.contains("\"size\"") && serialised.contains("4096"), "size serialises into the listing entry");
|
|
|
+
|
|
|
+ var reparsed = mapper.materialise(new JsonElement.from_string(serialised).as<Invercargill.Properties>());
|
|
|
+ check(reparsed.path == "sized-pkg-1.0.0.usmc" && reparsed.size == 4096, "size round-trips through serialisation");
|
|
|
+
|
|
|
+ JsonElement removed;
|
|
|
+ var legacy = new JsonElement.from_string(serialised).as<JsonObject>();
|
|
|
+ legacy.remove("size", out removed);
|
|
|
+ var pre_upgrade = mapper.materialise(legacy);
|
|
|
+ check(pre_upgrade.size == 0, "a pre-upgrade entry without size materialises as 0");
|
|
|
+ }
|
|
|
+
|
|
|
// ---- Dependency-phase model -------------------------------------------------
|
|
|
|
|
|
/** Minimal manifest with the given "depends" section verbatim. */
|
|
|
@@ -1263,6 +1287,256 @@ exit 1
|
|
|
Usm.Util.delete_tree(scratch);
|
|
|
}
|
|
|
|
|
|
+ // ---- Transaction rollback journal --------------------------------------------
|
|
|
+
|
|
|
+ /**
|
|
|
+ * Caches a package whose build script produces the provided `bin:`
|
|
|
+ * payload from the build directory (or fails, or produces nothing,
|
|
|
+ * per {@link script}), optionally marking it installed.
|
|
|
+ */
|
|
|
+ Usm.CachedPackage rollback_fixture_package(string state_dir, string name, string version, string script, string[] build_refs, bool installed) throws Error {
|
|
|
+ var source = Path.build_filename(state_dir, @"src-$name-$version");
|
|
|
+ DirUtils.create_with_parents(source, 0755);
|
|
|
+
|
|
|
+ var refs = new StringBuilder();
|
|
|
+ foreach(var resource_ref in build_refs) {
|
|
|
+ if(refs.len > 0) {
|
|
|
+ refs.append(",");
|
|
|
+ }
|
|
|
+ refs.append_printf("\"%s\"", resource_ref);
|
|
|
+ }
|
|
|
+ FileUtils.set_contents(Path.build_filename(source, "MANIFEST.usm"), """
|
|
|
+ {
|
|
|
+ "name": "%s",
|
|
|
+ "version": "%s",
|
|
|
+ "summary": "rollback fixture",
|
|
|
+ "licences": [],
|
|
|
+ "flags": [],
|
|
|
+ "provides": { "bin:%s": "build:out" },
|
|
|
+ "depends": { "runtime": [], "build": [%s], "manage": [] },
|
|
|
+ "execs": { "build": "build.sh" }
|
|
|
+ }
|
|
|
+ """.printf(name, version, name, refs.str));
|
|
|
+ FileUtils.set_contents(Path.build_filename(source, "build.sh"), script);
|
|
|
+ FileUtils.chmod(Path.build_filename(source, "build.sh"), 0755);
|
|
|
+
|
|
|
+ var cache_path = Path.build_filename(state_dir, "packages", @"$name-$version");
|
|
|
+ DirUtils.create_with_parents(cache_path, 0755);
|
|
|
+ Usm.Util.archive(source, Path.build_filename(cache_path, "package.usmc"));
|
|
|
+ Usm.Util.delete_tree(source);
|
|
|
+
|
|
|
+ if(installed) {
|
|
|
+ File.new_build_filename(state_dir, "installed", @"$name-$version").make_symbolic_link(cache_path);
|
|
|
+ }
|
|
|
+ return new Usm.CachedPackage(cache_path);
|
|
|
+ }
|
|
|
+
|
|
|
+ const string ROLLBACK_FAILING_BUILD = """#!/bin/bash
|
|
|
+exit 1
|
|
|
+""";
|
|
|
+
|
|
|
+ /** Builds cleanly but produces nothing, so the install step fails. */
|
|
|
+ const string ROLLBACK_EMPTY_BUILD = """#!/bin/bash
|
|
|
+exit 0
|
|
|
+""";
|
|
|
+
|
|
|
+ string rollback_payload_build(string payload) {
|
|
|
+ return "#!/bin/bash\nprintf '%s' > \"$1/out\"\n".printf(payload);
|
|
|
+ }
|
|
|
+
|
|
|
+ void test_journal_entry_complete_rollback_roundtrip() throws Error {
|
|
|
+ var scratch = make_scratch();
|
|
|
+
|
|
|
+ var package_root = Path.build_filename(scratch, "cache", "journalled-1.0.0");
|
|
|
+ var upgrade_root = Path.build_filename(scratch, "cache", "journalled-2.0.0");
|
|
|
+ DirUtils.create_with_parents(package_root, 0700);
|
|
|
+ DirUtils.create_with_parents(upgrade_root, 0700);
|
|
|
+ var pkg = new Usm.CachedPackage(package_root);
|
|
|
+ var upgrade = new Usm.CachedPackage(upgrade_root);
|
|
|
+
|
|
|
+ var lib_path = Path.build_filename(scratch, "dest", "usr", "lib", "libjournalled.so");
|
|
|
+ DirUtils.create_with_parents(Path.get_dirname(lib_path), 0700);
|
|
|
+ FileUtils.set_contents(lib_path, "old contents");
|
|
|
+ var mark_path = Path.build_filename(scratch, "state", "installed", "journalled-1.0.0");
|
|
|
+ DirUtils.create_with_parents(Path.get_dirname(mark_path), 0700);
|
|
|
+ File.new_for_path(mark_path).make_symbolic_link(package_root);
|
|
|
+
|
|
|
+ var journal = new Usm.Journal(Path.build_filename(scratch, "backup"));
|
|
|
+ journal.begin();
|
|
|
+ check(File.new_for_path(Path.build_filename(scratch, "backup", journal.transaction_id, "journal.json")).query_exists(),
|
|
|
+ "begin creates the journal file");
|
|
|
+
|
|
|
+ // a completed removal: the file is backed up and the mark target recorded
|
|
|
+ journal.entry_remove(pkg, { lib_path }, mark_path, package_root);
|
|
|
+ journal.backup_file(lib_path, pkg.package_name);
|
|
|
+ File.new_for_path(lib_path).delete();
|
|
|
+ File.new_for_path(mark_path).delete();
|
|
|
+ journal.complete(pkg.package_name);
|
|
|
+
|
|
|
+ // a half-applied install on top: one fresh file, the old path
|
|
|
+ // overwritten, the new mark already linked — never completed
|
|
|
+ var fresh_path = Path.build_filename(scratch, "dest", "usr", "bin", "journalled");
|
|
|
+ DirUtils.create_with_parents(Path.get_dirname(fresh_path), 0700);
|
|
|
+ var upgrade_mark_path = Path.build_filename(scratch, "state", "installed", "journalled-2.0.0");
|
|
|
+ FileUtils.set_contents(lib_path, "new contents");
|
|
|
+ journal.entry_install(upgrade, { fresh_path }, { lib_path }, upgrade_mark_path, null);
|
|
|
+ journal.backup_file(lib_path, upgrade.package_name);
|
|
|
+ FileUtils.set_contents(fresh_path, "binary");
|
|
|
+ File.new_for_path(upgrade_mark_path).make_symbolic_link(upgrade_root);
|
|
|
+
|
|
|
+ journal.rollback();
|
|
|
+
|
|
|
+ check(!File.new_for_path(fresh_path).query_exists(), "rollback removes the failed install's created file");
|
|
|
+ string restored;
|
|
|
+ FileUtils.get_contents(lib_path, out restored);
|
|
|
+ check(restored == "old contents", "rollback restores overwritten files from the backup");
|
|
|
+ check(!File.new_for_path(upgrade_mark_path).query_exists(), "rollback removes a mark the failed install created");
|
|
|
+ check(File.new_for_path(mark_path).query_exists(), "rollback re-creates the removal's installed mark");
|
|
|
+
|
|
|
+ var description = journal.describe_rollback();
|
|
|
+ check(description.contains("uninstalled \"journalled-2.0.0\"") && description.contains("reinstalled \"journalled-1.0.0\""),
|
|
|
+ "the rollback transcript names every undone operation");
|
|
|
+
|
|
|
+ string recorded;
|
|
|
+ FileUtils.get_contents(Path.build_filename(scratch, "backup", journal.transaction_id, "journal.json"), out recorded);
|
|
|
+ check(recorded.contains("\"status\":\"completed\"") && recorded.contains("\"files_removed\":[\"" + lib_path + "\"]"),
|
|
|
+ "journal.json records the operation, its status and its files");
|
|
|
+
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
+ }
|
|
|
+
|
|
|
+ void test_journal_finish_removes_backup_directory() throws Error {
|
|
|
+ var scratch = make_scratch();
|
|
|
+ var backup_root = Path.build_filename(scratch, "backup");
|
|
|
+
|
|
|
+ var journal = new Usm.Journal(backup_root);
|
|
|
+ journal.begin();
|
|
|
+ var transaction_directory = Path.build_filename(backup_root, journal.transaction_id);
|
|
|
+ check(File.new_for_path(transaction_directory).query_exists(), "begin creates the transaction backup directory");
|
|
|
+
|
|
|
+ journal.finish();
|
|
|
+ check(!File.new_for_path(transaction_directory).query_exists(), "finish removes the transaction backup directory");
|
|
|
+ check(!File.new_for_path(backup_root).query_exists(), "finish removes the emptied backup root");
|
|
|
+
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
+ }
|
|
|
+
|
|
|
+ void test_transaction_rollback_on_build_failure() throws Error {
|
|
|
+ var original_dir = Environment.get_current_dir();
|
|
|
+ var scratch = make_scratch();
|
|
|
+ var state = make_state(scratch);
|
|
|
+ var state_dir = Path.build_filename(scratch, "state");
|
|
|
+ var paths = scratch_paths(scratch);
|
|
|
+
|
|
|
+ // rb-b build-depends on rb-a, so rb-a and rb-c install in lot 1
|
|
|
+ // and rb-b builds in lot 2 — after both lot-1 installs completed
|
|
|
+ rollback_fixture_package(state_dir, "rb-a", "1.0.0", rollback_payload_build("payload-a"), {}, false);
|
|
|
+ rollback_fixture_package(state_dir, "rb-b", "1.0.0", ROLLBACK_FAILING_BUILD, { "bin:rb-a" }, false);
|
|
|
+ rollback_fixture_package(state_dir, "rb-c", "1.0.0", rollback_payload_build("payload-c"), {}, false);
|
|
|
+
|
|
|
+ var to_install = new HashSet<Usm.CachedPackage>();
|
|
|
+ foreach(var name in new string[] { "rb-a", "rb-b", "rb-c" }) {
|
|
|
+ to_install.add(new Usm.CachedPackage(Path.build_filename(state_dir, "packages", @"$name-1.0.0")));
|
|
|
+ }
|
|
|
+
|
|
|
+ var transaction = new Usm.Transaction() {
|
|
|
+ paths = paths,
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
+ to_install = to_install,
|
|
|
+ to_remove = new HashSet<Usm.CachedPackage>(),
|
|
|
+ state = state
|
|
|
+ };
|
|
|
+
|
|
|
+ var threw = false;
|
|
|
+ try {
|
|
|
+ transaction.run();
|
|
|
+ }
|
|
|
+ catch(Usm.TransactionError e) {
|
|
|
+ threw = true;
|
|
|
+ }
|
|
|
+ check(threw, "a lot-2 build failure fails the transaction");
|
|
|
+
|
|
|
+ check(!File.new_for_path(paths.get_suggested_path_for_resource(new Usm.ResourceRef("bin:rb-a"))).query_exists(),
|
|
|
+ "the rollback removes the first package's installed file");
|
|
|
+ check(!File.new_for_path(paths.get_suggested_path_for_resource(new Usm.ResourceRef("bin:rb-c"))).query_exists(),
|
|
|
+ "the rollback removes the third package's installed file");
|
|
|
+ check(!File.new_for_path(paths.get_suggested_path_for_resource(new Usm.ResourceRef("bin:rb-b"))).query_exists(),
|
|
|
+ "the failing package's file never appeared");
|
|
|
+ check(!File.new_for_path(Path.build_filename(state_dir, "installed", "rb-a-1.0.0")).query_exists(),
|
|
|
+ "the rollback removes the first package's installed mark");
|
|
|
+ check(!File.new_for_path(Path.build_filename(state_dir, "installed", "rb-c-1.0.0")).query_exists(),
|
|
|
+ "the rollback removes the third package's installed mark");
|
|
|
+
|
|
|
+ var found_journal = false;
|
|
|
+ var backup_root = Path.build_filename(scratch, "dest", "var", "usm", "backup");
|
|
|
+ foreach(var transaction_id in Iterate.directory(backup_root)) {
|
|
|
+ var journal_path = Path.build_filename(backup_root, transaction_id, "journal.json");
|
|
|
+ if(!File.new_for_path(journal_path).query_exists()) {
|
|
|
+ continue;
|
|
|
+ }
|
|
|
+ found_journal = true;
|
|
|
+ string recorded;
|
|
|
+ FileUtils.get_contents(journal_path, out recorded);
|
|
|
+ check(recorded.contains("\"package\":\"rb-a-1.0.0\"") && recorded.contains("\"package\":\"rb-c-1.0.0\""),
|
|
|
+ "the kept journal records both rolled-back installs");
|
|
|
+ }
|
|
|
+ check(found_journal, "a rolled-back transaction keeps its journal for inspection");
|
|
|
+
|
|
|
+ Environment.set_current_dir(original_dir);
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
+ }
|
|
|
+
|
|
|
+ void test_transaction_rollback_restores_previous_version() throws Error {
|
|
|
+ var original_dir = Environment.get_current_dir();
|
|
|
+ var scratch = make_scratch();
|
|
|
+ var state = make_state(scratch);
|
|
|
+ var state_dir = Path.build_filename(scratch, "state");
|
|
|
+ var paths = scratch_paths(scratch);
|
|
|
+
|
|
|
+ rollback_fixture_package(state_dir, "rb-u", "1.0.0", rollback_payload_build("payload-old"), {}, false);
|
|
|
+ var first = new Usm.Transaction() {
|
|
|
+ paths = paths,
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
+ to_install = Iterate.these<Usm.CachedPackage>(new Usm.CachedPackage(Path.build_filename(state_dir, "packages", "rb-u-1.0.0"))).to_hash_set(),
|
|
|
+ to_remove = new HashSet<Usm.CachedPackage>(),
|
|
|
+ state = state
|
|
|
+ };
|
|
|
+ first.run();
|
|
|
+
|
|
|
+ var bin_path = paths.get_suggested_path_for_resource(new Usm.ResourceRef("bin:rb-u"));
|
|
|
+ check(File.new_for_path(bin_path).query_exists(), "the old version installs before the update");
|
|
|
+
|
|
|
+ rollback_fixture_package(state_dir, "rb-u", "2.0.0", ROLLBACK_EMPTY_BUILD, {}, false);
|
|
|
+ var second = new Usm.Transaction() {
|
|
|
+ paths = paths,
|
|
|
+ resource_finder = new Usm.ResourceFinder(),
|
|
|
+ to_install = Iterate.these<Usm.CachedPackage>(new Usm.CachedPackage(Path.build_filename(state_dir, "packages", "rb-u-2.0.0"))).to_hash_set(),
|
|
|
+ to_remove = Iterate.these<Usm.CachedPackage>(new Usm.CachedPackage(Path.build_filename(state_dir, "packages", "rb-u-1.0.0"))).to_hash_set(),
|
|
|
+ state = state
|
|
|
+ };
|
|
|
+
|
|
|
+ var threw = false;
|
|
|
+ try {
|
|
|
+ second.run();
|
|
|
+ }
|
|
|
+ catch(Usm.TransactionError e) {
|
|
|
+ threw = true;
|
|
|
+ }
|
|
|
+ check(threw, "the new version's install failure fails the update");
|
|
|
+
|
|
|
+ string restored;
|
|
|
+ FileUtils.get_contents(bin_path, out restored);
|
|
|
+ check(restored == "payload-old", "the old version's file is restored from the backup");
|
|
|
+ check(state.read_installed_target(new Usm.CachedPackage(Path.build_filename(state_dir, "packages", "rb-u-1.0.0")))
|
|
|
+ == Path.build_filename(state_dir, "packages", "rb-u-1.0.0"),
|
|
|
+ "the old version's installed mark is re-created pointing at its cache directory");
|
|
|
+ check(!File.new_for_path(Path.build_filename(state_dir, "installed", "rb-u-2.0.0")).query_exists(),
|
|
|
+ "the failed version's installed mark never lingers");
|
|
|
+
|
|
|
+ Environment.set_current_dir(original_dir);
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
+ }
|
|
|
+
|
|
|
int main() {
|
|
|
test_default_ignore();
|
|
|
try {
|
|
|
@@ -1288,6 +1562,14 @@ exit 1
|
|
|
print("FAIL data package test threw: %s\n", e.message);
|
|
|
}
|
|
|
|
|
|
+ try {
|
|
|
+ test_repository_listing_entry_size();
|
|
|
+ }
|
|
|
+ catch(Error e) {
|
|
|
+ failures++;
|
|
|
+ print("FAIL repository listing size test threw: %s\n", e.message);
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
test_dependencies_flat_back_compat();
|
|
|
test_dependencies_nested();
|
|
|
@@ -1356,6 +1638,24 @@ exit 1
|
|
|
print("FAIL build cache retry test threw: %s\n", e.message);
|
|
|
}
|
|
|
|
|
|
+ try {
|
|
|
+ test_journal_entry_complete_rollback_roundtrip();
|
|
|
+ test_journal_finish_removes_backup_directory();
|
|
|
+ }
|
|
|
+ catch(Error e) {
|
|
|
+ failures++;
|
|
|
+ print("FAIL journal test threw: %s\n", e.message);
|
|
|
+ }
|
|
|
+
|
|
|
+ try {
|
|
|
+ test_transaction_rollback_on_build_failure();
|
|
|
+ test_transaction_rollback_restores_previous_version();
|
|
|
+ }
|
|
|
+ catch(Error e) {
|
|
|
+ failures++;
|
|
|
+ print("FAIL transaction rollback test threw: %s\n", e.message);
|
|
|
+ }
|
|
|
+
|
|
|
print("%d passed, %d failed\n", passes, failures);
|
|
|
return failures == 0 ? 0 : 1;
|
|
|
}
|