Commit 373d48212f

Igor Anić <igor.anic@gmail.com>
2024-03-30 23:41:13
fetch: add pathological packages test
Using test cases from: https://github.com/ianprime0509/pathological-packages repository. Depends on existence of the FAT32 file system. Folder is in FAT32 file system because it is case insensitive and and does not support symlinks. It is complicated test case requires internet connection, depends on existence of FAT32 in the specific location. But it is so valuable for development. Running `zig test Package.zig` is so much faster than building zig binary and running `zig fetch URL`. Committing it here although it should probably be removed.
1 parent 5a38924
Changed files (1)
src
Package
src/Package/Fetch.zig
@@ -1890,16 +1890,19 @@ const UnpackResult = struct {
     }
 };
 
-test "fetch tarball: fail with unable to create file" {
+test "tarball with duplicate file names" {
+    const gpa = std.testing.allocator;
     var tmp = std.testing.tmpDir(.{});
     defer tmp.cleanup();
 
     const tarball_name = "package.tar";
     try createTestTarball(tmp.dir, tarball_name, false);
+    const tarball_path = try std.fmt.allocPrint(gpa, "zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name });
+    defer gpa.free(tarball_path);
 
     // Run tarball fetch, expect to fail
     var fb: TestFetchBuilder = undefined;
-    var fetch = try fb.build(std.testing.allocator, tmp, tarball_name);
+    var fetch = try fb.build(gpa, tmp.dir, tarball_path);
     defer fb.deinit();
     try std.testing.expectError(error.FetchFailed, fetch.run());
 
@@ -1911,16 +1914,19 @@ test "fetch tarball: fail with unable to create file" {
     );
 }
 
-test "fetch tarball: error path are excluded" {
+test "tarball with error paths excluded" {
+    const gpa = std.testing.allocator;
     var tmp = std.testing.tmpDir(.{});
     defer tmp.cleanup();
 
     const tarball_name = "package.tar";
     try createTestTarball(tmp.dir, tarball_name, true);
+    const tarball_path = try std.fmt.allocPrint(gpa, "zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name });
+    defer gpa.free(tarball_path);
 
     // Run tarball fetch, should succeed
     var fb: TestFetchBuilder = undefined;
-    var fetch = try fb.build(std.testing.allocator, tmp, tarball_name);
+    var fetch = try fb.build(std.testing.allocator, tmp.dir, tarball_path);
     defer fb.deinit();
     try fetch.run();
 
@@ -1944,9 +1950,8 @@ const TestFetchBuilder = struct {
     job_queue: Fetch.JobQueue,
     fetch: Fetch,
 
-    fn build(self: *TestFetchBuilder, allocator: std.mem.Allocator, tmp: std.testing.TmpDir, tarball_name: []const u8) !*Fetch {
-        const cache_dir = try tmp.dir.makeOpenPath("zig-global-cache", .{});
-        const path_or_url = try std.fmt.allocPrint(allocator, "zig-cache/tmp/{s}/{s}", .{ tmp.sub_path, tarball_name });
+    fn build(self: *TestFetchBuilder, allocator: std.mem.Allocator, cache_parent_dir: std.fs.Dir, path_or_url: []const u8) !*Fetch {
+        const cache_dir = try cache_parent_dir.makeOpenPath("zig-global-cache", .{});
 
         try self.thread_pool.init(.{ .allocator = allocator });
         self.http_client = .{ .allocator = allocator };
@@ -1993,7 +1998,6 @@ const TestFetchBuilder = struct {
     }
 
     fn deinit(self: *TestFetchBuilder) void {
-        self.fetch.arena.child_allocator.free(self.fetch.location.path_or_url);
         self.fetch.deinit();
         self.job_queue.deinit();
         self.root_prog_node.end();
@@ -2043,8 +2047,9 @@ const TestFetchBuilder = struct {
 
         const em = errors.getErrorMessage(errors.getMessages()[0]);
         try std.testing.expectEqual(1, em.count);
-        try std.testing.expectEqual(notes_len, em.notes_len);
-
+        if (notes_len > 0) {
+            try std.testing.expectEqual(notes_len, em.notes_len);
+        }
         var al = std.ArrayList(u8).init(std.testing.allocator);
         defer al.deinit();
         try errors.renderToWriter(.{ .ttyconf = .no_color }, al.writer());
@@ -2098,3 +2103,119 @@ fn createTestTarball(dir: fs.Dir, tarball_name: []const u8, with_manifest: bool)
         try file.writeAll(&[_]u8{0} ** (512 - build_zig_zon.len));
     }
 }
+
+// Using test cases from: https://github.com/ianprime0509/pathological-packages
+// repository. Depends on existence of the FAT32 file system at /tmp/fat32.mnt
+// (look at the fat32TmpDir function below how to create it). If that folder is
+// not found test will be skipped. Folder is in FAT32 file system because it is
+// case insensitive and and does not support symlinks.
+test "pathological packages" {
+    const gpa = std.testing.allocator;
+    var buf: [128]u8 = undefined;
+
+    const urls: []const []const u8 = &.{
+        "https://github.com/ianprime0509/pathological-packages/archive/{s}.tar.gz",
+        "git+https://github.com/ianprime0509/pathological-packages#{s}",
+    };
+    const branches: []const []const u8 = &.{
+        "excluded-case-collisions",
+        "excluded-symlinks",
+        "included-case-collisions",
+        "included-symlinks",
+    };
+
+    // Expected fetched package files or error message for each combination of url/branch.
+    const expected = [_]struct {
+        files: []const []const u8 = &.{},
+        err_msg: []const u8 = "",
+    }{
+        // tar
+        .{ .files = &.{ "build.zig", "build.zig.zon" } },
+        .{ .files = &.{ "build.zig", "build.zig.zon", "main" } },
+        .{ .err_msg =
+        \\error: unable to unpack tarball
+        \\    note: unable to create file 'main': PathAlreadyExists
+        \\    note: unable to create file 'subdir/main': PathAlreadyExists
+        \\
+        },
+        .{ .err_msg =
+        \\error: unable to unpack tarball
+        \\    note: unable to create symlink from 'link' to 'main': AccessDenied
+        \\    note: unable to create symlink from 'subdir/link' to 'main': AccessDenied
+        \\
+        },
+        // git
+        .{ .files = &.{ "build.zig", "build.zig.zon" } },
+        .{ .files = &.{ "build.zig", "build.zig.zon", "main" } },
+        .{ .err_msg =
+        \\error: unable to unpack packfile
+        \\    note: unable to create file 'main': PathAlreadyExists
+        \\    note: unable to create file 'subdir/main': PathAlreadyExists
+        \\
+        },
+        .{ .err_msg =
+        \\error: unable to unpack packfile
+        \\    note: unable to create symlink from 'link' to 'main': AccessDenied
+        \\    note: unable to create symlink from 'subdir/link' to 'main': AccessDenied
+        \\
+        },
+    };
+
+    var expected_no: usize = 0;
+    inline for (urls) |url_fmt| {
+        var tmp = try fat32TmpDir();
+        defer tmp.cleanup();
+
+        for (branches) |branch| {
+            defer expected_no += 1;
+            const url = try std.fmt.bufPrint(&buf, url_fmt, .{branch});
+            // std.debug.print("fetching url: {s}\n", .{url});
+
+            var fb: TestFetchBuilder = undefined;
+            var fetch = try fb.build(gpa, tmp.dir, url);
+            defer fb.deinit();
+
+            const ex = expected[expected_no];
+            if (ex.err_msg.len > 0) {
+                try std.testing.expectError(error.FetchFailed, fetch.run());
+                try fb.expectFetchErrors(0, ex.err_msg);
+            } else {
+                try fetch.run();
+                try fb.expectPackageFiles(ex.files);
+            }
+        }
+    }
+}
+
+// Using logic from std.testing.tmpDir() to make temporary directory at specific
+// location.
+//
+// This assumes FAT32 file system in /tmp/fat32.mnt folder,
+// created with something like this:
+// $ cd /tmp && fallocate -l 1M fat32.fs && mkfs.fat -F32 fat32.fs &&  mkdir fat32.mnt && sudo mount -o rw,umask=0000 fat32.fs fat32.mnt
+//
+// To remove that folder:
+// $ cd /tmp && sudo umount fat32.mnt && rm -rf fat32.mnt fat32.fs
+//
+pub fn fat32TmpDir() !std.testing.TmpDir {
+    const fat32fs_path = "/tmp/fat32.mnt/";
+
+    const random_bytes_count = 12;
+    var random_bytes: [random_bytes_count]u8 = undefined;
+    std.crypto.random.bytes(&random_bytes);
+    var sub_path: [std.fs.base64_encoder.calcSize(random_bytes_count)]u8 = undefined;
+    _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes);
+
+    const parent_dir = std.fs.openDirAbsolute(fat32fs_path, .{}) catch |err| switch (err) {
+        error.FileNotFound => return error.SkipZigTest,
+        else => return err,
+    };
+    const dir = parent_dir.makeOpenPath(&sub_path, .{}) catch
+        @panic("unable to make tmp dir for testing: unable to make and open the tmp dir");
+
+    return .{
+        .dir = dir,
+        .parent_dir = parent_dir,
+        .sub_path = sub_path,
+    };
+}