|
|
@@ -1761,6 +1761,234 @@ exit 1
|
|
|
Usm.Util.delete_tree(scratch);
|
|
|
}
|
|
|
|
|
|
+ // ---- Conditional build-artifact re-archiving --------------------------------
|
|
|
+
|
|
|
+ /**
|
|
|
+ * A CachedPackage over a scratch cache directory holding a build
|
|
|
+ * directory with a fixed tree (a file, a sub-directory with a file
|
|
|
+ * and a symbolic link) — the seed for the re-archive tests.
|
|
|
+ */
|
|
|
+ Usm.CachedPackage make_built_package(string scratch, string name) throws Error {
|
|
|
+ var cache_path = Path.build_filename(scratch, "state", "packages", @"$name-1.0.0");
|
|
|
+ DirUtils.create_with_parents(Path.build_filename(cache_path, "build", "sub"), 0755);
|
|
|
+ FileUtils.set_contents(Path.build_filename(cache_path, "build", "one.txt"), "first\n");
|
|
|
+ FileUtils.set_contents(Path.build_filename(cache_path, "build", "sub", "two.txt"), "second\n");
|
|
|
+ File.new_for_path(Path.build_filename(cache_path, "build", "link"))
|
|
|
+ .make_symbolic_link("one.txt");
|
|
|
+ return new Usm.CachedPackage(cache_path);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Archives the package's build directory and records its digest sidecar. */
|
|
|
+ void archive_built_package(Usm.CachedPackage package) throws Error {
|
|
|
+ var build_dir = Path.build_filename(package.state_path, "build");
|
|
|
+ var archive_path = Path.build_filename(package.state_path, "build.tar.xz");
|
|
|
+ Usm.Util.archive(build_dir, archive_path);
|
|
|
+ FileUtils.set_contents(Path.build_filename(package.state_path, "build.tar.xz.digest"),
|
|
|
+ Usm.Util.tree_digest(build_dir) + "\n");
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Extracts `archive` into a fresh scratch directory and returns the contents of `member`. */
|
|
|
+ string archived_member(string scratch, string archive, string member) throws Error {
|
|
|
+ var extracted = Path.build_filename(scratch, @"extract-$(Uuid.string_random())");
|
|
|
+ DirUtils.create(extracted, 0755);
|
|
|
+ Usm.Util.unarchive(archive, extracted);
|
|
|
+ string contents = null;
|
|
|
+ FileUtils.get_contents(Path.build_filename(extracted, member), out contents);
|
|
|
+ return contents;
|
|
|
+ }
|
|
|
+
|
|
|
+ /** Modification time of `path` in seconds since the epoch. */
|
|
|
+ int64 file_mtime(string path) {
|
|
|
+ return Stat(path).st_mtime;
|
|
|
+ }
|
|
|
+
|
|
|
+ void test_archive_build_skips_unchanged_tree() throws Error {
|
|
|
+ var scratch = make_scratch();
|
|
|
+ var package = make_built_package(scratch, "skip-pkg");
|
|
|
+ archive_built_package(package);
|
|
|
+
|
|
|
+ var archive_path = Path.build_filename(package.state_path, "build.tar.xz");
|
|
|
+ var digest_path = Path.build_filename(package.state_path, "build.tar.xz.digest");
|
|
|
+ string before = null;
|
|
|
+ FileUtils.get_contents(archive_path, out before);
|
|
|
+ var before_mtime = file_mtime(archive_path);
|
|
|
+
|
|
|
+ var last_fraction = -1.0f;
|
|
|
+ package.archive_build((fraction) => { last_fraction = fraction; });
|
|
|
+
|
|
|
+ string after = null;
|
|
|
+ FileUtils.get_contents(archive_path, out after);
|
|
|
+ check(after == before, "an unchanged build tree keeps the archive bytes");
|
|
|
+ check(file_mtime(archive_path) == before_mtime, "an unchanged build tree keeps the archive mtime");
|
|
|
+ check(!File.new_for_path(Path.build_filename(package.state_path, "build")).query_exists(),
|
|
|
+ "skipping the re-archive still cleans the build directory");
|
|
|
+ check(File.new_for_path(digest_path).query_exists(), "skipping the re-archive keeps the digest sidecar");
|
|
|
+ check(last_fraction == 1.0f, "skipping the re-archive still completes progress");
|
|
|
+
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
+ }
|
|
|
+
|
|
|
+ /** One mutation of a build tree before it is re-archived. */
|
|
|
+ delegate void BuildTreeMutation(string build_dir) throws Error;
|
|
|
+
|
|
|
+ /**
|
|
|
+ * One re-archive-on-change case: seeds a package named <name> with a
|
|
|
+ * build tree, archives it, applies <mutate> (which may throw — a
|
|
|
+ * failed mutation must fail the test, not silently re-test the
|
|
|
+ * unchanged tree), re-archives and asserts the archive was rewritten.
|
|
|
+ */
|
|
|
+ void rearchived_after_change(string scratch, string name, owned BuildTreeMutation mutate) throws Error {
|
|
|
+ var package = make_built_package(scratch, name);
|
|
|
+ archive_built_package(package);
|
|
|
+ var digest_path = Path.build_filename(package.state_path, "build.tar.xz.digest");
|
|
|
+ string digest_before = null;
|
|
|
+ FileUtils.get_contents(digest_path, out digest_before);
|
|
|
+
|
|
|
+ mutate(Path.build_filename(package.state_path, "build"));
|
|
|
+ // Computed before archive_build cleans the directory: the digest
|
|
|
+ // the fresh sidecar has to describe
|
|
|
+ var changed_tree_digest = Usm.Util.tree_digest(Path.build_filename(package.state_path, "build"));
|
|
|
+ package.archive_build(null);
|
|
|
+
|
|
|
+ string digest_after = null;
|
|
|
+ FileUtils.get_contents(digest_path, out digest_after);
|
|
|
+ check(digest_after.strip() != digest_before.strip(),
|
|
|
+ @"a changed build tree ($name) re-archives into a new digest sidecar");
|
|
|
+ check(digest_after.strip() == changed_tree_digest,
|
|
|
+ @"a changed build tree ($name) records its own tree in the fresh sidecar");
|
|
|
+ check(!File.new_for_path(Path.build_filename(package.state_path, "build")).query_exists(),
|
|
|
+ @"a changed build tree ($name) still cleans the build directory");
|
|
|
+
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
+ }
|
|
|
+
|
|
|
+ void test_archive_build_rearchives_changes() throws Error {
|
|
|
+ var scratch = make_scratch();
|
|
|
+
|
|
|
+ rearchived_after_change(scratch, "mod-pkg", (build) => {
|
|
|
+ FileUtils.set_contents(Path.build_filename(build, "one.txt"), "changed\n");
|
|
|
+ });
|
|
|
+ check(true, "a modified file re-archives");
|
|
|
+
|
|
|
+ rearchived_after_change(scratch, "add-pkg", (build) => {
|
|
|
+ FileUtils.set_contents(Path.build_filename(build, "new.txt"), "new\n");
|
|
|
+ });
|
|
|
+ check(true, "an added file re-archives");
|
|
|
+
|
|
|
+ rearchived_after_change(scratch, "rm-pkg", (build) => {
|
|
|
+ File.new_for_path(Path.build_filename(build, "sub", "two.txt")).delete();
|
|
|
+ });
|
|
|
+ check(true, "a removed file re-archives");
|
|
|
+
|
|
|
+ rearchived_after_change(scratch, "mode-pkg", (build) => {
|
|
|
+ FileUtils.chmod(Path.build_filename(build, "one.txt"), 0700);
|
|
|
+ });
|
|
|
+ check(true, "a changed file mode re-archives");
|
|
|
+
|
|
|
+ rearchived_after_change(scratch, "link-pkg", (build) => {
|
|
|
+ File.new_for_path(Path.build_filename(build, "link")).delete();
|
|
|
+ File.new_for_path(Path.build_filename(build, "link")).make_symbolic_link("sub/two.txt");
|
|
|
+ });
|
|
|
+ check(true, "a changed symlink target re-archives");
|
|
|
+ }
|
|
|
+
|
|
|
+ void test_archive_build_rearchives_modified_content() throws Error {
|
|
|
+ var scratch = make_scratch();
|
|
|
+ var package = make_built_package(scratch, "content-pkg");
|
|
|
+ archive_built_package(package);
|
|
|
+
|
|
|
+ var archive_path = Path.build_filename(package.state_path, "build.tar.xz");
|
|
|
+ FileUtils.set_contents(Path.build_filename(package.state_path, "build", "one.txt"), "changed\n");
|
|
|
+ package.archive_build(null);
|
|
|
+
|
|
|
+ check(archived_member(scratch, archive_path, "one.txt") == "changed\n",
|
|
|
+ "a modified file's new content lands in the re-archived archive");
|
|
|
+
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
+ }
|
|
|
+
|
|
|
+ void test_archive_build_rearchives_without_sidecar() throws Error {
|
|
|
+ var scratch = make_scratch();
|
|
|
+ var package = make_built_package(scratch, "legacy-pkg");
|
|
|
+ var build_dir = Path.build_filename(package.state_path, "build");
|
|
|
+ var archive_path = Path.build_filename(package.state_path, "build.tar.xz");
|
|
|
+ Usm.Util.archive(build_dir, archive_path); // legacy: no digest sidecar
|
|
|
+ var tree_digest_before = Usm.Util.tree_digest(build_dir);
|
|
|
+
|
|
|
+ package.archive_build(null);
|
|
|
+
|
|
|
+ string digest = null;
|
|
|
+ FileUtils.get_contents(Path.build_filename(package.state_path, "build.tar.xz.digest"), out digest);
|
|
|
+ check(digest != null && digest.strip() == tree_digest_before,
|
|
|
+ "an archive without a digest sidecar re-archives and records the digest sidecar");
|
|
|
+ check(archived_member(scratch, archive_path, "one.txt") == "first\n",
|
|
|
+ "the re-archived legacy archive holds the build tree's contents");
|
|
|
+
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
+ }
|
|
|
+
|
|
|
+ void test_build_digest_sidecar_lifecycle() throws Error {
|
|
|
+ var scratch = make_scratch();
|
|
|
+ var package = make_built_package(scratch, "cycle-pkg");
|
|
|
+ var cache_path = package.state_path;
|
|
|
+ var archive_path = Path.build_filename(cache_path, "build.tar.xz");
|
|
|
+ var digest_path = Path.build_filename(cache_path, "build.tar.xz.digest");
|
|
|
+
|
|
|
+ // get_build_archive archives a bare build directory
|
|
|
+ var returned = package.get_build_archive();
|
|
|
+ check(returned == archive_path, "get_build_archive creates the archive from the build directory");
|
|
|
+ check(File.new_for_path(digest_path).query_exists(),
|
|
|
+ "get_build_archive records the digest sidecar alongside the archive");
|
|
|
+
|
|
|
+ // An archive-only cache keeps its sidecar; unarchiving leaves it alone
|
|
|
+ FileUtils.set_contents(digest_path, "describes-the-archive-not-the-tree\n");
|
|
|
+ package.unarchive_build();
|
|
|
+ check(File.new_for_path(digest_path).query_exists(),
|
|
|
+ "unarchiving a build artifact leaves the digest sidecar alone");
|
|
|
+
|
|
|
+ // A second archive_build over the restored tree matches the digest
|
|
|
+ var digest = Usm.Util.tree_digest(Path.build_filename(cache_path, "build"));
|
|
|
+ FileUtils.set_contents(digest_path, digest + "\n");
|
|
|
+ var before_mtime = file_mtime(archive_path);
|
|
|
+ package.archive_build(null);
|
|
|
+ check(file_mtime(archive_path) == before_mtime,
|
|
|
+ "an archive restored and re-archived unchanged keeps its bytes");
|
|
|
+
|
|
|
+ // clean_build_cache removes the archive together with the sidecar
|
|
|
+ package.clean_build_cache();
|
|
|
+ check(!File.new_for_path(archive_path).query_exists() && !File.new_for_path(digest_path).query_exists(),
|
|
|
+ "clean_build_cache removes the archive and the digest sidecar together");
|
|
|
+
|
|
|
+ Usm.Util.delete_tree(scratch);
|
|
|
+ }
|
|
|
+
|
|
|
+ void test_tree_digest_determinism() throws Error {
|
|
|
+ var scratch = make_scratch();
|
|
|
+
|
|
|
+ var tree = Path.build_filename(scratch, "tree");
|
|
|
+ DirUtils.create_with_parents(Path.build_filename(tree, "d"), 0755);
|
|
|
+ FileUtils.set_contents(Path.build_filename(tree, "a.txt"), "alpha\n");
|
|
|
+ FileUtils.set_contents(Path.build_filename(tree, "d", "b.txt"), "beta\n");
|
|
|
+ File.new_for_path(Path.build_filename(tree, "d", "link")).make_symbolic_link("../a.txt");
|
|
|
+
|
|
|
+ var first = Usm.Util.tree_digest(tree);
|
|
|
+ check(first == Usm.Util.tree_digest(tree), "hashing the same tree twice yields the same digest");
|
|
|
+
|
|
|
+ // Same content at a different path is a different tree
|
|
|
+ var moved = Path.build_filename(scratch, "moved");
|
|
|
+ DirUtils.create_with_parents(Path.build_filename(moved, "d"), 0755);
|
|
|
+ FileUtils.set_contents(Path.build_filename(moved, "z.txt"), "alpha\n");
|
|
|
+ FileUtils.set_contents(Path.build_filename(moved, "d", "b.txt"), "beta\n");
|
|
|
+ File.new_for_path(Path.build_filename(moved, "d", "link")).make_symbolic_link("../a.txt");
|
|
|
+ check(Usm.Util.tree_digest(moved) != first, "a same-content file at a different path digests differently");
|
|
|
+
|
|
|
+ // A dot-prefixed temp file inside the tree is part of the tree
|
|
|
+ FileUtils.set_contents(Path.build_filename(tree, ".usm-tmp"), "scratch\n");
|
|
|
+ check(Usm.Util.tree_digest(tree) != first, "a temporary file inside the tree changes the digest");
|
|
|
+
|
|
|
+ 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();
|
|
|
@@ -2292,6 +2520,12 @@ echo "Installing libmeson-pkg.so to /usr/lib"
|
|
|
test_build_archive_restore_and_clean_retry();
|
|
|
test_build_archive_restored_incrementally();
|
|
|
test_build_failure_without_cache_propagates();
|
|
|
+ test_archive_build_skips_unchanged_tree();
|
|
|
+ test_archive_build_rearchives_changes();
|
|
|
+ test_archive_build_rearchives_modified_content();
|
|
|
+ test_archive_build_rearchives_without_sidecar();
|
|
|
+ test_build_digest_sidecar_lifecycle();
|
|
|
+ test_tree_digest_determinism();
|
|
|
}
|
|
|
catch(Error e) {
|
|
|
failures++;
|