zig/lib/std/fs/test.zig

1910 lines
70 KiB
Zig
Raw Normal View History

const std = @import("../std.zig");
const builtin = @import("builtin");
const testing = std.testing;
const os = std.os;
const fs = std.fs;
const mem = std.mem;
const wasi = std.os.wasi;
const ArenaAllocator = std.heap.ArenaAllocator;
const Dir = std.fs.Dir;
const File = std.fs.File;
const tmpDir = testing.tmpDir;
const SymLinkFlags = std.fs.Dir.SymLinkFlags;
const PathType = enum {
relative,
absolute,
unc,
pub fn isSupported(self: PathType, target_os: std.Target.Os) bool {
return switch (self) {
.relative => true,
.absolute => std.os.isGetFdPathSupportedOnTarget(target_os),
.unc => target_os.tag == .windows,
};
}
pub const TransformError = std.os.RealPathError || error{OutOfMemory};
pub const TransformFn = fn (allocator: mem.Allocator, dir: Dir, relative_path: []const u8) TransformError![]const u8;
pub fn getTransformFn(comptime path_type: PathType) TransformFn {
switch (path_type) {
.relative => return struct {
fn transform(allocator: mem.Allocator, dir: Dir, relative_path: []const u8) TransformError![]const u8 {
_ = allocator;
_ = dir;
return relative_path;
}
}.transform,
.absolute => return struct {
fn transform(allocator: mem.Allocator, dir: Dir, relative_path: []const u8) TransformError![]const u8 {
// The final path may not actually exist which would cause realpath to fail.
// So instead, we get the path of the dir and join it with the relative path.
var fd_path_buf: [fs.MAX_PATH_BYTES]u8 = undefined;
const dir_path = try os.getFdPath(dir.fd, &fd_path_buf);
return fs.path.join(allocator, &.{ dir_path, relative_path });
}
}.transform,
.unc => return struct {
fn transform(allocator: mem.Allocator, dir: Dir, relative_path: []const u8) TransformError![]const u8 {
// Any drive absolute path (C:\foo) can be converted into a UNC path by
// using '127.0.0.1' as the server name and '<drive letter>$' as the share name.
var fd_path_buf: [fs.MAX_PATH_BYTES]u8 = undefined;
const dir_path = try os.getFdPath(dir.fd, &fd_path_buf);
const windows_path_type = std.os.windows.getUnprefixedPathType(u8, dir_path);
switch (windows_path_type) {
.unc_absolute => return fs.path.join(allocator, &.{ dir_path, relative_path }),
.drive_absolute => {
// `C:\<...>` -> `\\127.0.0.1\C$\<...>`
const prepended = "\\\\127.0.0.1\\";
var path = try fs.path.join(allocator, &.{ prepended, dir_path, relative_path });
path[prepended.len + 1] = '$';
return path;
},
else => unreachable,
}
}
}.transform,
}
}
};
const TestContext = struct {
path_type: PathType,
arena: ArenaAllocator,
2023-11-23 04:12:22 +08:00
tmp: testing.TmpDir,
dir: std.fs.Dir,
transform_fn: *const PathType.TransformFn,
pub fn init(path_type: PathType, allocator: mem.Allocator, transform_fn: *const PathType.TransformFn) TestContext {
const tmp = tmpDir(.{ .iterate = true });
return .{
.path_type = path_type,
.arena = ArenaAllocator.init(allocator),
.tmp = tmp,
2023-11-23 04:12:22 +08:00
.dir = tmp.dir,
.transform_fn = transform_fn,
};
}
pub fn deinit(self: *TestContext) void {
self.arena.deinit();
self.tmp.cleanup();
}
/// Returns the `relative_path` transformed into the TestContext's `path_type`.
/// The result is allocated by the TestContext's arena and will be free'd during
/// `TestContext.deinit`.
pub fn transformPath(self: *TestContext, relative_path: []const u8) ![]const u8 {
return self.transform_fn(self.arena.allocator(), self.dir, relative_path);
}
};
/// `test_func` must be a function that takes a `*TestContext` as a parameter and returns `!void`.
/// `test_func` will be called once for each PathType that the current target supports,
/// and will be passed a TestContext that can transform a relative path into the path type under test.
/// The TestContext will also create a tmp directory for you (and will clean it up for you too).
fn testWithAllSupportedPathTypes(test_func: anytype) !void {
try testWithPathTypeIfSupported(.relative, test_func);
try testWithPathTypeIfSupported(.absolute, test_func);
try testWithPathTypeIfSupported(.unc, test_func);
}
fn testWithPathTypeIfSupported(comptime path_type: PathType, test_func: anytype) !void {
if (!(comptime path_type.isSupported(builtin.os))) return;
var ctx = TestContext.init(path_type, testing.allocator, path_type.getTransformFn());
defer ctx.deinit();
try test_func(&ctx);
}
// For use in test setup. If the symlink creation fails on Windows with
// AccessDenied, then make the test failure silent (it is not a Zig failure).
fn setupSymlink(dir: Dir, target: []const u8, link: []const u8, flags: SymLinkFlags) !void {
return dir.symLink(target, link, flags) catch |err| switch (err) {
// Symlink requires admin privileges on windows, so this test can legitimately fail.
error.AccessDenied => if (builtin.os.tag == .windows) return error.SkipZigTest else return err,
else => return err,
};
}
// For use in test setup. If the symlink creation fails on Windows with
// AccessDenied, then make the test failure silent (it is not a Zig failure).
fn setupSymlinkAbsolute(target: []const u8, link: []const u8, flags: SymLinkFlags) !void {
return fs.symLinkAbsolute(target, link, flags) catch |err| switch (err) {
error.AccessDenied => if (builtin.os.tag == .windows) return error.SkipZigTest else return err,
else => return err,
};
}
test "Dir.readLink" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
// Create some targets
const file_target_path = try ctx.transformPath("file.txt");
try ctx.dir.writeFile(file_target_path, "nonsense");
const dir_target_path = try ctx.transformPath("subdir");
try ctx.dir.makeDir(dir_target_path);
// test 1: symlink to a file
try setupSymlink(ctx.dir, file_target_path, "symlink1", .{});
try testReadLink(ctx.dir, file_target_path, "symlink1");
// test 2: symlink to a directory (can be different on Windows)
try setupSymlink(ctx.dir, dir_target_path, "symlink2", .{ .is_directory = true });
try testReadLink(ctx.dir, dir_target_path, "symlink2");
// test 3: relative path symlink
const parent_file = ".." ++ fs.path.sep_str ++ "target.txt";
var subdir = try ctx.dir.makeOpenPath("subdir", .{});
defer subdir.close();
try setupSymlink(subdir, parent_file, "relative-link.txt", .{});
try testReadLink(subdir, parent_file, "relative-link.txt");
}
}.impl);
}
fn testReadLink(dir: Dir, target_path: []const u8, symlink_path: []const u8) !void {
var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;
const actual = try dir.readLink(symlink_path, buffer[0..]);
try testing.expectEqualStrings(target_path, actual);
}
fn testReadLinkAbsolute(target_path: []const u8, symlink_path: []const u8) !void {
var buffer: [fs.MAX_PATH_BYTES]u8 = undefined;
const given = try fs.readLinkAbsolute(symlink_path, buffer[0..]);
try testing.expectEqualStrings(target_path, given);
}
test "File.stat on a File that is a symlink returns Kind.sym_link" {
// This test requires getting a file descriptor of a symlink which
// is not possible on all targets
switch (builtin.target.os.tag) {
.windows, .linux => {},
else => return error.SkipZigTest,
}
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const dir_target_path = try ctx.transformPath("subdir");
try ctx.dir.makeDir(dir_target_path);
try setupSymlink(ctx.dir, dir_target_path, "symlink", .{ .is_directory = true });
var symlink = switch (builtin.target.os.tag) {
.windows => windows_symlink: {
const w = std.os.windows;
const sub_path_w = try std.os.windows.cStrToPrefixedFileW(ctx.dir.fd, "symlink");
var result = Dir{
.fd = undefined,
};
const path_len_bytes = @as(u16, @intCast(sub_path_w.span().len * 2));
var nt_name = w.UNICODE_STRING{
.Length = path_len_bytes,
.MaximumLength = path_len_bytes,
.Buffer = @constCast(&sub_path_w.data),
};
var attr = w.OBJECT_ATTRIBUTES{
.Length = @sizeOf(w.OBJECT_ATTRIBUTES),
.RootDirectory = if (fs.path.isAbsoluteWindowsW(sub_path_w.span())) null else ctx.dir.fd,
.Attributes = 0,
.ObjectName = &nt_name,
.SecurityDescriptor = null,
.SecurityQualityOfService = null,
};
var io: w.IO_STATUS_BLOCK = undefined;
const rc = w.ntdll.NtCreateFile(
&result.fd,
w.STANDARD_RIGHTS_READ | w.FILE_READ_ATTRIBUTES | w.FILE_READ_EA | w.SYNCHRONIZE | w.FILE_TRAVERSE,
&attr,
&io,
null,
w.FILE_ATTRIBUTE_NORMAL,
w.FILE_SHARE_READ | w.FILE_SHARE_WRITE,
w.FILE_OPEN,
// FILE_OPEN_REPARSE_POINT is the important thing here
w.FILE_OPEN_REPARSE_POINT | w.FILE_DIRECTORY_FILE | w.FILE_SYNCHRONOUS_IO_NONALERT | w.FILE_OPEN_FOR_BACKUP_INTENT,
null,
0,
);
switch (rc) {
.SUCCESS => break :windows_symlink result,
else => return w.unexpectedStatus(rc),
}
},
.linux => linux_symlink: {
const sub_path_c = try os.toPosixPath("symlink");
// the O_NOFOLLOW | O_PATH combination can obtain a fd to a symlink
// note that if O_DIRECTORY is set, then this will error with ENOTDIR
const flags = os.O.NOFOLLOW | os.O.PATH | os.O.RDONLY | os.O.CLOEXEC;
const fd = try os.openatZ(ctx.dir.fd, &sub_path_c, flags, 0);
break :linux_symlink Dir{ .fd = fd };
},
else => unreachable,
};
defer symlink.close();
const stat = try symlink.stat();
try testing.expectEqual(File.Kind.sym_link, stat.kind);
}
}.impl);
}
test "openDir" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const allocator = ctx.arena.allocator();
const subdir_path = try ctx.transformPath("subdir");
try ctx.dir.makeDir(subdir_path);
for ([_][]const u8{ "", ".", ".." }) |sub_path| {
const dir_path = try fs.path.join(allocator, &.{ subdir_path, sub_path });
var dir = try ctx.dir.openDir(dir_path, .{});
defer dir.close();
}
}
}.impl);
}
test "accessAbsolute" {
if (builtin.os.tag == .wasi) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
const base_path = blk: {
const relative_path = try fs.path.join(allocator, &.{ "zig-cache", "tmp", tmp.sub_path[0..] });
break :blk try fs.realpathAlloc(allocator, relative_path);
};
try fs.accessAbsolute(base_path, .{});
}
test "openDirAbsolute" {
if (builtin.os.tag == .wasi) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.makeDir("subdir");
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
const base_path = blk: {
const relative_path = try fs.path.join(allocator, &.{ "zig-cache", "tmp", tmp.sub_path[0..], "subdir" });
break :blk try fs.realpathAlloc(allocator, relative_path);
};
{
var dir = try fs.openDirAbsolute(base_path, .{});
defer dir.close();
}
for ([_][]const u8{ ".", ".." }) |sub_path| {
const dir_path = try fs.path.join(allocator, &.{ base_path, sub_path });
var dir = try fs.openDirAbsolute(dir_path, .{});
defer dir.close();
}
}
test "openDir cwd parent '..'" {
2021-04-03 03:17:06 +08:00
if (builtin.os.tag == .wasi) return error.SkipZigTest;
var dir = try fs.cwd().openDir("..", .{});
defer dir.close();
2021-04-03 03:17:06 +08:00
}
test "openDir non-cwd parent '..'" {
switch (builtin.os.tag) {
.wasi, .netbsd, .openbsd => return error.SkipZigTest,
else => {},
}
var tmp = tmpDir(.{});
defer tmp.cleanup();
var subdir = try tmp.dir.makeOpenPath("subdir", .{});
defer subdir.close();
var dir = try subdir.openDir("..", .{});
defer dir.close();
const expected_path = try tmp.dir.realpathAlloc(testing.allocator, ".");
defer testing.allocator.free(expected_path);
const actual_path = try dir.realpathAlloc(testing.allocator, ".");
defer testing.allocator.free(actual_path);
try testing.expectEqualStrings(expected_path, actual_path);
}
test "readLinkAbsolute" {
if (builtin.os.tag == .wasi) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
// Create some targets
try tmp.dir.writeFile("file.txt", "nonsense");
try tmp.dir.makeDir("subdir");
// Get base abs path
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
const base_path = blk: {
const relative_path = try fs.path.join(allocator, &.{ "zig-cache", "tmp", tmp.sub_path[0..] });
break :blk try fs.realpathAlloc(allocator, relative_path);
};
{
const target_path = try fs.path.join(allocator, &.{ base_path, "file.txt" });
const symlink_path = try fs.path.join(allocator, &.{ base_path, "symlink1" });
// Create symbolic link by path
try setupSymlinkAbsolute(target_path, symlink_path, .{});
try testReadLinkAbsolute(target_path, symlink_path);
}
{
const target_path = try fs.path.join(allocator, &.{ base_path, "subdir" });
const symlink_path = try fs.path.join(allocator, &.{ base_path, "symlink2" });
// Create symbolic link to a directory by path
try setupSymlinkAbsolute(target_path, symlink_path, .{ .is_directory = true });
try testReadLinkAbsolute(target_path, symlink_path);
}
}
test "Dir.Iterator" {
var tmp_dir = tmpDir(.{ .iterate = true });
defer tmp_dir.cleanup();
// First, create a couple of entries to iterate over.
const file = try tmp_dir.dir.createFile("some_file", .{});
file.close();
try tmp_dir.dir.makeDir("some_dir");
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var entries = std.ArrayList(Dir.Entry).init(allocator);
// Create iterator.
var iter = tmp_dir.dir.iterate();
while (try iter.next()) |entry| {
// We cannot just store `entry` as on Windows, we're re-using the name buffer
// which means we'll actually share the `name` pointer between entries!
const name = try allocator.dupe(u8, entry.name);
try entries.append(Dir.Entry{ .name = name, .kind = entry.kind });
}
try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
}
test "Dir.Iterator many entries" {
var tmp_dir = tmpDir(.{ .iterate = true });
defer tmp_dir.cleanup();
const num = 1024;
var i: usize = 0;
var buf: [4]u8 = undefined; // Enough to store "1024".
while (i < num) : (i += 1) {
const name = try std.fmt.bufPrint(&buf, "{}", .{i});
2023-11-23 04:12:22 +08:00
const file = try tmp_dir.dir.createFile(name, .{});
file.close();
}
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var entries = std.ArrayList(Dir.Entry).init(allocator);
// Create iterator.
2023-11-23 04:12:22 +08:00
var iter = tmp_dir.dir.iterate();
while (try iter.next()) |entry| {
// We cannot just store `entry` as on Windows, we're re-using the name buffer
// which means we'll actually share the `name` pointer between entries!
const name = try allocator.dupe(u8, entry.name);
try entries.append(.{ .name = name, .kind = entry.kind });
}
i = 0;
while (i < num) : (i += 1) {
const name = try std.fmt.bufPrint(&buf, "{}", .{i});
try testing.expect(contains(&entries, .{ .name = name, .kind = .file }));
}
}
test "Dir.Iterator twice" {
var tmp_dir = tmpDir(.{ .iterate = true });
defer tmp_dir.cleanup();
// First, create a couple of entries to iterate over.
const file = try tmp_dir.dir.createFile("some_file", .{});
file.close();
try tmp_dir.dir.makeDir("some_dir");
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
var i: u8 = 0;
while (i < 2) : (i += 1) {
var entries = std.ArrayList(Dir.Entry).init(allocator);
// Create iterator.
var iter = tmp_dir.dir.iterate();
while (try iter.next()) |entry| {
// We cannot just store `entry` as on Windows, we're re-using the name buffer
// which means we'll actually share the `name` pointer between entries!
const name = try allocator.dupe(u8, entry.name);
try entries.append(Dir.Entry{ .name = name, .kind = entry.kind });
}
try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
}
}
2022-10-05 18:13:21 +08:00
test "Dir.Iterator reset" {
var tmp_dir = tmpDir(.{ .iterate = true });
2022-10-05 18:13:21 +08:00
defer tmp_dir.cleanup();
// First, create a couple of entries to iterate over.
2023-11-23 04:12:22 +08:00
const file = try tmp_dir.dir.createFile("some_file", .{});
2022-10-05 18:13:21 +08:00
file.close();
2023-11-23 04:12:22 +08:00
try tmp_dir.dir.makeDir("some_dir");
2022-10-05 18:13:21 +08:00
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
// Create iterator.
2023-11-23 04:12:22 +08:00
var iter = tmp_dir.dir.iterate();
2022-10-05 18:13:21 +08:00
var i: u8 = 0;
while (i < 2) : (i += 1) {
var entries = std.ArrayList(Dir.Entry).init(allocator);
2022-10-05 18:13:21 +08:00
while (try iter.next()) |entry| {
// We cannot just store `entry` as on Windows, we're re-using the name buffer
// which means we'll actually share the `name` pointer between entries!
const name = try allocator.dupe(u8, entry.name);
try entries.append(.{ .name = name, .kind = entry.kind });
}
try testing.expectEqual(@as(usize, 2), entries.items.len); // note that the Iterator skips '.' and '..'
try testing.expect(contains(&entries, .{ .name = "some_file", .kind = .file }));
try testing.expect(contains(&entries, .{ .name = "some_dir", .kind = .directory }));
2022-10-05 18:13:21 +08:00
iter.reset();
}
}
test "Dir.Iterator but dir is deleted during iteration" {
var tmp = std.testing.tmpDir(.{});
defer tmp.cleanup();
// Create directory and setup an iterator for it
2023-11-23 04:12:22 +08:00
var subdir = try tmp.dir.makeOpenPath("subdir", .{ .iterate = true });
defer subdir.close();
2023-11-23 04:12:22 +08:00
var iterator = subdir.iterate();
// Create something to iterate over within the subdir
try tmp.dir.makePath("subdir" ++ fs.path.sep_str ++ "b");
// Then, before iterating, delete the directory that we're iterating.
// This is a contrived reproduction, but this could happen outside of the program, in another thread, etc.
// If we get an error while trying to delete, we can skip this test (this will happen on platforms
// like Windows which will give FileBusy if the directory is currently open for iteration).
tmp.dir.deleteTree("subdir") catch return error.SkipZigTest;
// Now, when we try to iterate, the next call should return null immediately.
const entry = try iterator.next();
try std.testing.expect(entry == null);
// On Linux, we can opt-in to receiving a more specific error by calling `nextLinux`
if (builtin.os.tag == .linux) {
try std.testing.expectError(error.DirNotFound, iterator.nextLinux());
}
}
fn entryEql(lhs: Dir.Entry, rhs: Dir.Entry) bool {
return mem.eql(u8, lhs.name, rhs.name) and lhs.kind == rhs.kind;
}
fn contains(entries: *const std.ArrayList(Dir.Entry), el: Dir.Entry) bool {
for (entries.items) |entry| {
if (entryEql(entry, el)) return true;
}
return false;
}
test "Dir.realpath smoke test" {
if (!comptime std.os.isGetFdPathSupportedOnTarget(builtin.os)) return error.SkipZigTest;
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const allocator = ctx.arena.allocator();
const test_file_path = try ctx.transformPath("test_file");
const test_dir_path = try ctx.transformPath("test_dir");
var buf: [fs.MAX_PATH_BYTES]u8 = undefined;
// FileNotFound if the path doesn't exist
try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(allocator, test_file_path));
try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_file_path, &buf));
try testing.expectError(error.FileNotFound, ctx.dir.realpathAlloc(allocator, test_dir_path));
try testing.expectError(error.FileNotFound, ctx.dir.realpath(test_dir_path, &buf));
// Now create the file and dir
try ctx.dir.writeFile(test_file_path, "");
try ctx.dir.makeDir(test_dir_path);
const base_path = try ctx.transformPath(".");
const base_realpath = try ctx.dir.realpathAlloc(allocator, base_path);
const expected_file_path = try fs.path.join(
allocator,
&.{ base_realpath, "test_file" },
);
const expected_dir_path = try fs.path.join(
allocator,
&.{ base_realpath, "test_dir" },
);
// First, test non-alloc version
{
const file_path = try ctx.dir.realpath(test_file_path, &buf);
try testing.expectEqualStrings(expected_file_path, file_path);
const dir_path = try ctx.dir.realpath(test_dir_path, &buf);
try testing.expectEqualStrings(expected_dir_path, dir_path);
}
// Next, test alloc version
{
const file_path = try ctx.dir.realpathAlloc(allocator, test_file_path);
try testing.expectEqualStrings(expected_file_path, file_path);
const dir_path = try ctx.dir.realpathAlloc(allocator, test_dir_path);
try testing.expectEqualStrings(expected_dir_path, dir_path);
}
}
}.impl);
}
test "readAllAlloc" {
var tmp_dir = tmpDir(.{});
defer tmp_dir.cleanup();
var file = try tmp_dir.dir.createFile("test_file", .{ .read = true });
defer file.close();
const buf1 = try file.readToEndAlloc(testing.allocator, 1024);
defer testing.allocator.free(buf1);
try testing.expectEqual(@as(usize, 0), buf1.len);
const write_buf: []const u8 = "this is a test.\nthis is a test.\nthis is a test.\nthis is a test.\n";
try file.writeAll(write_buf);
try file.seekTo(0);
// max_bytes > file_size
const buf2 = try file.readToEndAlloc(testing.allocator, 1024);
defer testing.allocator.free(buf2);
2021-05-05 01:47:26 +08:00
try testing.expectEqual(write_buf.len, buf2.len);
try testing.expectEqualStrings(write_buf, buf2);
try file.seekTo(0);
// max_bytes == file_size
const buf3 = try file.readToEndAlloc(testing.allocator, write_buf.len);
defer testing.allocator.free(buf3);
2021-05-05 01:47:26 +08:00
try testing.expectEqual(write_buf.len, buf3.len);
try testing.expectEqualStrings(write_buf, buf3);
try file.seekTo(0);
// max_bytes < file_size
2021-05-05 01:47:26 +08:00
try testing.expectError(error.FileTooBig, file.readToEndAlloc(testing.allocator, write_buf.len - 1));
}
test "Dir.statFile" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const test_file_name = try ctx.transformPath("test_file");
try testing.expectError(error.FileNotFound, ctx.dir.statFile(test_file_name));
try ctx.dir.writeFile(test_file_name, "");
const stat = try ctx.dir.statFile(test_file_name);
try testing.expectEqual(File.Kind.file, stat.kind);
}
}.impl);
}
test "statFile on dangling symlink" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const symlink_name = try ctx.transformPath("dangling-symlink");
const symlink_target = "." ++ fs.path.sep_str ++ "doesnotexist";
try setupSymlink(ctx.dir, symlink_target, symlink_name, .{});
try std.testing.expectError(error.FileNotFound, ctx.dir.statFile(symlink_name));
}
}.impl);
}
test "directory operations on files" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const test_file_name = try ctx.transformPath("test_file");
var file = try ctx.dir.createFile(test_file_name, .{ .read = true });
file.close();
try testing.expectError(error.PathAlreadyExists, ctx.dir.makeDir(test_file_name));
try testing.expectError(error.NotDir, ctx.dir.openDir(test_file_name, .{}));
try testing.expectError(error.NotDir, ctx.dir.deleteDir(test_file_name));
if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
try testing.expectError(error.PathAlreadyExists, fs.makeDirAbsolute(test_file_name));
try testing.expectError(error.NotDir, fs.deleteDirAbsolute(test_file_name));
}
// ensure the file still exists and is a file as a sanity check
file = try ctx.dir.openFile(test_file_name, .{});
const stat = try file.stat();
try testing.expectEqual(File.Kind.file, stat.kind);
file.close();
}
}.impl);
}
test "file operations on directories" {
// TODO: fix this test on FreeBSD. https://github.com/ziglang/zig/issues/1759
if (builtin.os.tag == .freebsd) return error.SkipZigTest;
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const test_dir_name = try ctx.transformPath("test_dir");
try ctx.dir.makeDir(test_dir_name);
try testing.expectError(error.IsDir, ctx.dir.createFile(test_dir_name, .{}));
try testing.expectError(error.IsDir, ctx.dir.deleteFile(test_dir_name));
switch (builtin.os.tag) {
// no error when reading a directory.
.dragonfly, .netbsd => {},
// Currently, WASI will return error.Unexpected (via ENOTCAPABLE) when attempting fd_read on a directory handle.
// TODO: Re-enable on WASI once https://github.com/bytecodealliance/wasmtime/issues/1935 is resolved.
.wasi => {},
else => {
try testing.expectError(error.IsDir, ctx.dir.readFileAlloc(testing.allocator, test_dir_name, std.math.maxInt(usize)));
},
}
// Note: The `.mode = .read_write` is necessary to ensure the error occurs on all platforms.
// TODO: Add a read-only test as well, see https://github.com/ziglang/zig/issues/5732
try testing.expectError(error.IsDir, ctx.dir.openFile(test_dir_name, .{ .mode = .read_write }));
if (ctx.path_type == .absolute and comptime PathType.absolute.isSupported(builtin.os)) {
try testing.expectError(error.IsDir, fs.createFileAbsolute(test_dir_name, .{}));
try testing.expectError(error.IsDir, fs.deleteFileAbsolute(test_dir_name));
}
// ensure the directory still exists as a sanity check
var dir = try ctx.dir.openDir(test_dir_name, .{});
dir.close();
}
}.impl);
}
test "makeOpenPath parent dirs do not exist" {
var tmp_dir = tmpDir(.{});
defer tmp_dir.cleanup();
var dir = try tmp_dir.dir.makeOpenPath("root_dir/parent_dir/some_dir", .{});
dir.close();
// double check that the full directory structure was created
var dir_verification = try tmp_dir.dir.openDir("root_dir/parent_dir/some_dir", .{});
dir_verification.close();
}
test "deleteDir" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const test_dir_path = try ctx.transformPath("test_dir");
const test_file_path = try ctx.transformPath("test_dir" ++ fs.path.sep_str ++ "test_file");
// deleting a non-existent directory
try testing.expectError(error.FileNotFound, ctx.dir.deleteDir(test_dir_path));
// deleting a non-empty directory
try ctx.dir.makeDir(test_dir_path);
try ctx.dir.writeFile(test_file_path, "");
try testing.expectError(error.DirNotEmpty, ctx.dir.deleteDir(test_dir_path));
// deleting an empty directory
try ctx.dir.deleteFile(test_file_path);
try ctx.dir.deleteDir(test_dir_path);
}
}.impl);
}
test "Dir.rename files" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
// Rename on Windows can hit intermittent AccessDenied errors
// when certain conditions are true about the host system.
// For now, skip this test when the path type is UNC to avoid them.
// See https://github.com/ziglang/zig/issues/17134
if (ctx.path_type == .unc) return;
const missing_file_path = try ctx.transformPath("missing_file_name");
const something_else_path = try ctx.transformPath("something_else");
try testing.expectError(error.FileNotFound, ctx.dir.rename(missing_file_path, something_else_path));
// Renaming files
const test_file_name = try ctx.transformPath("test_file");
const renamed_test_file_name = try ctx.transformPath("test_file_renamed");
var file = try ctx.dir.createFile(test_file_name, .{ .read = true });
file.close();
try ctx.dir.rename(test_file_name, renamed_test_file_name);
// Ensure the file was renamed
try testing.expectError(error.FileNotFound, ctx.dir.openFile(test_file_name, .{}));
file = try ctx.dir.openFile(renamed_test_file_name, .{});
file.close();
// Rename to self succeeds
try ctx.dir.rename(renamed_test_file_name, renamed_test_file_name);
// Rename to existing file succeeds
const existing_file_path = try ctx.transformPath("existing_file");
var existing_file = try ctx.dir.createFile(existing_file_path, .{ .read = true });
existing_file.close();
try ctx.dir.rename(renamed_test_file_name, existing_file_path);
try testing.expectError(error.FileNotFound, ctx.dir.openFile(renamed_test_file_name, .{}));
file = try ctx.dir.openFile(existing_file_path, .{});
file.close();
}
}.impl);
}
test "Dir.rename directories" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
// Rename on Windows can hit intermittent AccessDenied errors
// when certain conditions are true about the host system.
// For now, skip this test when the path type is UNC to avoid them.
// See https://github.com/ziglang/zig/issues/17134
if (ctx.path_type == .unc) return;
const test_dir_path = try ctx.transformPath("test_dir");
const test_dir_renamed_path = try ctx.transformPath("test_dir_renamed");
// Renaming directories
try ctx.dir.makeDir(test_dir_path);
try ctx.dir.rename(test_dir_path, test_dir_renamed_path);
// Ensure the directory was renamed
try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_path, .{}));
var dir = try ctx.dir.openDir(test_dir_renamed_path, .{});
// Put a file in the directory
var file = try dir.createFile("test_file", .{ .read = true });
file.close();
dir.close();
const test_dir_renamed_again_path = try ctx.transformPath("test_dir_renamed_again");
try ctx.dir.rename(test_dir_renamed_path, test_dir_renamed_again_path);
// Ensure the directory was renamed and the file still exists in it
try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_renamed_path, .{}));
dir = try ctx.dir.openDir(test_dir_renamed_again_path, .{});
file = try dir.openFile("test_file", .{});
file.close();
dir.close();
}
}.impl);
}
test "Dir.rename directory onto empty dir" {
// TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
if (builtin.os.tag == .windows) return error.SkipZigTest;
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const test_dir_path = try ctx.transformPath("test_dir");
const target_dir_path = try ctx.transformPath("target_dir_path");
try ctx.dir.makeDir(test_dir_path);
try ctx.dir.makeDir(target_dir_path);
try ctx.dir.rename(test_dir_path, target_dir_path);
// Ensure the directory was renamed
try testing.expectError(error.FileNotFound, ctx.dir.openDir(test_dir_path, .{}));
var dir = try ctx.dir.openDir(target_dir_path, .{});
dir.close();
}
}.impl);
}
test "Dir.rename directory onto non-empty dir" {
// TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
if (builtin.os.tag == .windows) return error.SkipZigTest;
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const test_dir_path = try ctx.transformPath("test_dir");
const target_dir_path = try ctx.transformPath("target_dir_path");
try ctx.dir.makeDir(test_dir_path);
var target_dir = try ctx.dir.makeOpenPath(target_dir_path, .{});
var file = try target_dir.createFile("test_file", .{ .read = true });
file.close();
target_dir.close();
// Rename should fail with PathAlreadyExists if target_dir is non-empty
try testing.expectError(error.PathAlreadyExists, ctx.dir.rename(test_dir_path, target_dir_path));
// Ensure the directory was not renamed
var dir = try ctx.dir.openDir(test_dir_path, .{});
dir.close();
}
}.impl);
}
test "Dir.rename file <-> dir" {
// TODO: Fix on Windows, see https://github.com/ziglang/zig/issues/6364
if (builtin.os.tag == .windows) return error.SkipZigTest;
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const test_file_path = try ctx.transformPath("test_file");
const test_dir_path = try ctx.transformPath("test_dir");
var file = try ctx.dir.createFile(test_file_path, .{ .read = true });
file.close();
try ctx.dir.makeDir(test_dir_path);
try testing.expectError(error.IsDir, ctx.dir.rename(test_file_path, test_dir_path));
try testing.expectError(error.NotDir, ctx.dir.rename(test_dir_path, test_file_path));
}
}.impl);
}
test "rename" {
var tmp_dir1 = tmpDir(.{});
defer tmp_dir1.cleanup();
var tmp_dir2 = tmpDir(.{});
defer tmp_dir2.cleanup();
// Renaming files
const test_file_name = "test_file";
const renamed_test_file_name = "test_file_renamed";
var file = try tmp_dir1.dir.createFile(test_file_name, .{ .read = true });
file.close();
try fs.rename(tmp_dir1.dir, test_file_name, tmp_dir2.dir, renamed_test_file_name);
// ensure the file was renamed
2021-05-05 01:47:26 +08:00
try testing.expectError(error.FileNotFound, tmp_dir1.dir.openFile(test_file_name, .{}));
file = try tmp_dir2.dir.openFile(renamed_test_file_name, .{});
file.close();
}
test "renameAbsolute" {
if (builtin.os.tag == .wasi) return error.SkipZigTest;
var tmp_dir = tmpDir(.{});
defer tmp_dir.cleanup();
// Get base abs path
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
const base_path = blk: {
const relative_path = try fs.path.join(allocator, &.{ "zig-cache", "tmp", tmp_dir.sub_path[0..] });
break :blk try fs.realpathAlloc(allocator, relative_path);
};
2021-05-05 01:47:26 +08:00
try testing.expectError(error.FileNotFound, fs.renameAbsolute(
try fs.path.join(allocator, &.{ base_path, "missing_file_name" }),
try fs.path.join(allocator, &.{ base_path, "something_else" }),
));
// Renaming files
const test_file_name = "test_file";
const renamed_test_file_name = "test_file_renamed";
var file = try tmp_dir.dir.createFile(test_file_name, .{ .read = true });
file.close();
try fs.renameAbsolute(
try fs.path.join(allocator, &.{ base_path, test_file_name }),
try fs.path.join(allocator, &.{ base_path, renamed_test_file_name }),
);
// ensure the file was renamed
2021-05-05 01:47:26 +08:00
try testing.expectError(error.FileNotFound, tmp_dir.dir.openFile(test_file_name, .{}));
file = try tmp_dir.dir.openFile(renamed_test_file_name, .{});
const stat = try file.stat();
try testing.expectEqual(File.Kind.file, stat.kind);
file.close();
// Renaming directories
const test_dir_name = "test_dir";
const renamed_test_dir_name = "test_dir_renamed";
try tmp_dir.dir.makeDir(test_dir_name);
try fs.renameAbsolute(
try fs.path.join(allocator, &.{ base_path, test_dir_name }),
try fs.path.join(allocator, &.{ base_path, renamed_test_dir_name }),
);
// ensure the directory was renamed
2021-05-05 01:47:26 +08:00
try testing.expectError(error.FileNotFound, tmp_dir.dir.openDir(test_dir_name, .{}));
var dir = try tmp_dir.dir.openDir(renamed_test_dir_name, .{});
dir.close();
}
test "openSelfExe" {
Add/fix missing WASI functionality to pass libstd tests This rather large commit adds/fixes missing WASI functionality in `libstd` needed to pass the `libstd` tests. As such, now by default tests targeting `wasm32-wasi` target are enabled in `test/tests.zig` module. However, they can be disabled by passing the `-Dskip-wasi=true` flag when invoking the `zig build test` command. When the flag is set to `false`, i.e., when WASI tests are included, `wasmtime` with `--dir=.` is used as the default testing command. Since the majority of `libstd` tests were relying on `fs.cwd()` call to get current working directory handle wrapped in `Dir` struct, in order to make the tests WASI-friendly, `fs.cwd()` call was replaced with `testing.getTestDir()` function which resolved to either `fs.cwd()` for non-WASI targets, or tries to fetch the preopen list from the WASI runtime and extract a preopen for '.' path. The summary of changes introduced by this commit: * implement `Dir.makeDir` and `Dir.openDir` targeting WASI * implement `Dir.deleteFile` and `Dir.deleteDir` targeting WASI * fix `os.close` and map errors in `unlinkat` * move WASI-specific `mkdirat` and `unlinkat` from `std.fs.wasi` to `std.os` module * implement `lseek_{SET, CUR, END}` targeting WASI * implement `futimens` targeting WASI * implement `ftruncate` targeting WASI * implement `readv`, `writev`, `pread{v}`, `pwrite{v}` targeting WASI * make sure ANSI escape codes are _not_ used in stderr or stdout in WASI, as WASI always sanitizes stderr, and sanitizes stdout if fd is a TTY * fix specifying WASI rights when opening/creating files/dirs * tweak `AtomicFile` to be WASI-compatible * implement `os.renameatWasi` for WASI-compliant `os.renameat` function * implement sleep() targeting WASI * fix `process.getEnvMap` targeting WASI
2020-05-05 23:23:49 +08:00
if (builtin.os.tag == .wasi) return error.SkipZigTest;
const self_exe_file = try std.fs.openSelfExe(.{});
self_exe_file.close();
}
test "deleteTree does not follow symlinks" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.makePath("b");
{
var a = try tmp.dir.makeOpenPath("a", .{});
defer a.close();
try setupSymlink(a, "../b", "b", .{ .is_directory = true });
}
try tmp.dir.deleteTree("a");
try testing.expectError(error.FileNotFound, tmp.dir.access("a", .{}));
try tmp.dir.access("b", .{});
}
test "deleteTree on a symlink" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
// Symlink to a file
try tmp.dir.writeFile("file", "");
try setupSymlink(tmp.dir, "file", "filelink", .{});
try tmp.dir.deleteTree("filelink");
try testing.expectError(error.FileNotFound, tmp.dir.access("filelink", .{}));
try tmp.dir.access("file", .{});
// Symlink to a directory
try tmp.dir.makePath("dir");
try setupSymlink(tmp.dir, "dir", "dirlink", .{ .is_directory = true });
try tmp.dir.deleteTree("dirlink");
try testing.expectError(error.FileNotFound, tmp.dir.access("dirlink", .{}));
try tmp.dir.access("dir", .{});
}
test "makePath, put some files in it, deleteTree" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const allocator = ctx.arena.allocator();
const dir_path = try ctx.transformPath("os_test_tmp");
try ctx.dir.makePath(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c" }));
try ctx.dir.writeFile(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c", "file.txt" }), "nonsense");
try ctx.dir.writeFile(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "file2.txt" }), "blah");
try ctx.dir.deleteTree(dir_path);
try testing.expectError(error.FileNotFound, ctx.dir.openDir(dir_path, .{}));
}
}.impl);
}
test "makePath, put some files in it, deleteTreeMinStackSize" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const allocator = ctx.arena.allocator();
const dir_path = try ctx.transformPath("os_test_tmp");
try ctx.dir.makePath(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c" }));
try ctx.dir.writeFile(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "c", "file.txt" }), "nonsense");
try ctx.dir.writeFile(try fs.path.join(allocator, &.{ "os_test_tmp", "b", "file2.txt" }), "blah");
try ctx.dir.deleteTreeMinStackSize(dir_path);
try testing.expectError(error.FileNotFound, ctx.dir.openDir(dir_path, .{}));
}
}.impl);
}
test "makePath in a directory that no longer exists" {
if (builtin.os.tag == .windows) return error.SkipZigTest; // Windows returns FileBusy if attempting to remove an open dir
var tmp = tmpDir(.{});
defer tmp.cleanup();
try tmp.parent_dir.deleteTree(&tmp.sub_path);
try testing.expectError(error.FileNotFound, tmp.dir.makePath("sub-path"));
}
test "makePath but sub_path contains pre-existing file" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.makeDir("foo");
try tmp.dir.writeFile("foo/bar", "");
try testing.expectError(error.NotDir, tmp.dir.makePath("foo/bar/baz"));
}
fn expectDir(dir: Dir, path: []const u8) !void {
var d = try dir.openDir(path, .{});
d.close();
}
test "makepath existing directories" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.makeDir("A");
const tmpA = try tmp.dir.openDir("A", .{});
try tmpA.makeDir("B");
const testPath = "A" ++ fs.path.sep_str ++ "B" ++ fs.path.sep_str ++ "C";
try tmp.dir.makePath(testPath);
try expectDir(tmp.dir, testPath);
}
test "makepath through existing valid symlink" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.makeDir("realfolder");
try setupSymlink(tmp.dir, "." ++ fs.path.sep_str ++ "realfolder", "working-symlink", .{});
try tmp.dir.makePath("working-symlink" ++ fs.path.sep_str ++ "in-realfolder");
try expectDir(tmp.dir, "realfolder" ++ fs.path.sep_str ++ "in-realfolder");
}
test "makepath relative walks" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
const relPath = try fs.path.join(testing.allocator, &.{
"first", "..", "second", "..", "third", "..", "first", "A", "..", "B", "..", "C",
});
defer testing.allocator.free(relPath);
try tmp.dir.makePath(relPath);
// How .. is handled is different on Windows than non-Windows
switch (builtin.os.tag) {
.windows => {
// On Windows, .. is resolved before passing the path to NtCreateFile,
// meaning everything except `first/C` drops out.
try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "C");
try testing.expectError(error.FileNotFound, tmp.dir.access("second", .{}));
try testing.expectError(error.FileNotFound, tmp.dir.access("third", .{}));
},
else => {
try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "A");
try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "B");
try expectDir(tmp.dir, "first" ++ fs.path.sep_str ++ "C");
try expectDir(tmp.dir, "second");
try expectDir(tmp.dir, "third");
},
}
}
test "makepath ignores '.'" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
// Path to create, with "." elements:
const dotPath = try fs.path.join(testing.allocator, &.{
"first", ".", "second", ".", "third",
});
defer testing.allocator.free(dotPath);
// Path to expect to find:
const expectedPath = try fs.path.join(testing.allocator, &.{
"first", "second", "third",
});
defer testing.allocator.free(expectedPath);
try tmp.dir.makePath(dotPath);
try expectDir(tmp.dir, expectedPath);
}
fn testFilenameLimits(iterable_dir: Dir, maxed_filename: []const u8) !void {
// setup, create a dir and a nested file both with maxed filenames, and walk the dir
{
2023-11-23 04:12:22 +08:00
var maxed_dir = try iterable_dir.makeOpenPath(maxed_filename, .{});
defer maxed_dir.close();
try maxed_dir.writeFile(maxed_filename, "");
var walker = try iterable_dir.walk(testing.allocator);
defer walker.deinit();
var count: usize = 0;
while (try walker.next()) |entry| {
try testing.expectEqualStrings(maxed_filename, entry.basename);
count += 1;
}
try testing.expectEqual(@as(usize, 2), count);
}
// ensure that we can delete the tree
2023-11-23 04:12:22 +08:00
try iterable_dir.deleteTree(maxed_filename);
}
test "max file name component lengths" {
var tmp = tmpDir(.{ .iterate = true });
defer tmp.cleanup();
if (builtin.os.tag == .windows) {
// U+FFFF is the character with the largest code point that is encoded as a single
// UTF-16 code unit, so Windows allows for NAME_MAX of them.
const maxed_windows_filename = ("\u{FFFF}".*) ** std.os.windows.NAME_MAX;
2023-11-23 04:12:22 +08:00
try testFilenameLimits(tmp.dir, &maxed_windows_filename);
} else if (builtin.os.tag == .wasi) {
// On WASI, the maxed filename depends on the host OS, so in order for this test to
// work on any host, we need to use a length that will work for all platforms
// (i.e. the minimum MAX_NAME_BYTES of all supported platforms).
const maxed_wasi_filename = [_]u8{'1'} ** 255;
2023-11-23 04:12:22 +08:00
try testFilenameLimits(tmp.dir, &maxed_wasi_filename);
} else {
const maxed_ascii_filename = [_]u8{'1'} ** std.fs.MAX_NAME_BYTES;
2023-11-23 04:12:22 +08:00
try testFilenameLimits(tmp.dir, &maxed_ascii_filename);
}
}
test "writev, readv" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
const line1 = "line1\n";
const line2 = "line2\n";
var buf1: [line1.len]u8 = undefined;
var buf2: [line2.len]u8 = undefined;
var write_vecs = [_]std.os.iovec_const{
.{
.iov_base = line1,
.iov_len = line1.len,
},
.{
.iov_base = line2,
.iov_len = line2.len,
},
};
var read_vecs = [_]std.os.iovec{
.{
.iov_base = &buf2,
.iov_len = buf2.len,
},
.{
.iov_base = &buf1,
.iov_len = buf1.len,
},
};
var src_file = try tmp.dir.createFile("test.txt", .{ .read = true });
defer src_file.close();
try src_file.writevAll(&write_vecs);
try testing.expectEqual(@as(u64, line1.len + line2.len), try src_file.getEndPos());
try src_file.seekTo(0);
const read = try src_file.readvAll(&read_vecs);
try testing.expectEqual(@as(usize, line1.len + line2.len), read);
try testing.expectEqualStrings(&buf1, "line2\n");
try testing.expectEqualStrings(&buf2, "line1\n");
}
test "pwritev, preadv" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
const line1 = "line1\n";
const line2 = "line2\n";
var buf1: [line1.len]u8 = undefined;
var buf2: [line2.len]u8 = undefined;
var write_vecs = [_]std.os.iovec_const{
.{
.iov_base = line1,
.iov_len = line1.len,
},
.{
.iov_base = line2,
.iov_len = line2.len,
},
};
var read_vecs = [_]std.os.iovec{
.{
.iov_base = &buf2,
.iov_len = buf2.len,
},
.{
.iov_base = &buf1,
.iov_len = buf1.len,
},
};
var src_file = try tmp.dir.createFile("test.txt", .{ .read = true });
defer src_file.close();
try src_file.pwritevAll(&write_vecs, 16);
try testing.expectEqual(@as(u64, 16 + line1.len + line2.len), try src_file.getEndPos());
const read = try src_file.preadvAll(&read_vecs, 16);
try testing.expectEqual(@as(usize, line1.len + line2.len), read);
try testing.expectEqualStrings(&buf1, "line2\n");
try testing.expectEqualStrings(&buf2, "line1\n");
}
test "access file" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const dir_path = try ctx.transformPath("os_test_tmp");
const file_path = try ctx.transformPath("os_test_tmp" ++ fs.path.sep_str ++ "file.txt");
try ctx.dir.makePath(dir_path);
try testing.expectError(error.FileNotFound, ctx.dir.access(file_path, .{}));
try ctx.dir.writeFile(file_path, "");
try ctx.dir.access(file_path, .{});
try ctx.dir.deleteTree(dir_path);
}
}.impl);
}
test "sendfile" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.makePath("os_test_tmp");
defer tmp.dir.deleteTree("os_test_tmp") catch {};
var dir = try tmp.dir.openDir("os_test_tmp", .{});
defer dir.close();
const line1 = "line1\n";
const line2 = "second line\n";
var vecs = [_]std.os.iovec_const{
.{
.iov_base = line1,
.iov_len = line1.len,
},
.{
.iov_base = line2,
.iov_len = line2.len,
},
};
var src_file = try dir.createFile("sendfile1.txt", .{ .read = true });
defer src_file.close();
try src_file.writevAll(&vecs);
var dest_file = try dir.createFile("sendfile2.txt", .{ .read = true });
defer dest_file.close();
const header1 = "header1\n";
const header2 = "second header\n";
const trailer1 = "trailer1\n";
const trailer2 = "second trailer\n";
var hdtr = [_]std.os.iovec_const{
.{
.iov_base = header1,
.iov_len = header1.len,
},
.{
.iov_base = header2,
.iov_len = header2.len,
},
.{
.iov_base = trailer1,
.iov_len = trailer1.len,
},
.{
.iov_base = trailer2,
.iov_len = trailer2.len,
},
};
var written_buf: [100]u8 = undefined;
try dest_file.writeFileAll(src_file, .{
.in_offset = 1,
.in_len = 10,
.headers_and_trailers = &hdtr,
.header_count = 2,
});
const amt = try dest_file.preadAll(&written_buf, 0);
try testing.expectEqualStrings("header1\nsecond header\nine1\nsecontrailer1\nsecond trailer\n", written_buf[0..amt]);
}
2020-08-12 03:49:43 +08:00
test "copyRangeAll" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
try tmp.dir.makePath("os_test_tmp");
defer tmp.dir.deleteTree("os_test_tmp") catch {};
var dir = try tmp.dir.openDir("os_test_tmp", .{});
defer dir.close();
var src_file = try dir.createFile("file1.txt", .{ .read = true });
defer src_file.close();
const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP";
try src_file.writeAll(data);
var dest_file = try dir.createFile("file2.txt", .{ .read = true });
defer dest_file.close();
var written_buf: [100]u8 = undefined;
_ = try src_file.copyRangeAll(0, dest_file, 0, data.len);
const amt = try dest_file.preadAll(&written_buf, 0);
try testing.expectEqualStrings(data, written_buf[0..amt]);
2020-08-12 03:49:43 +08:00
}
test "copyFile" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const data = "u6wj+JmdF3qHsFPE BUlH2g4gJCmEz0PP";
const src_file = try ctx.transformPath("tmp_test_copy_file.txt");
const dest_file = try ctx.transformPath("tmp_test_copy_file2.txt");
const dest_file2 = try ctx.transformPath("tmp_test_copy_file3.txt");
try ctx.dir.writeFile(src_file, data);
defer ctx.dir.deleteFile(src_file) catch {};
try ctx.dir.copyFile(src_file, ctx.dir, dest_file, .{});
defer ctx.dir.deleteFile(dest_file) catch {};
try ctx.dir.copyFile(src_file, ctx.dir, dest_file2, .{ .override_mode = File.default_mode });
defer ctx.dir.deleteFile(dest_file2) catch {};
try expectFileContents(ctx.dir, dest_file, data);
try expectFileContents(ctx.dir, dest_file2, data);
}
}.impl);
}
fn expectFileContents(dir: Dir, file_path: []const u8, data: []const u8) !void {
const contents = try dir.readFileAlloc(testing.allocator, file_path, 1000);
defer testing.allocator.free(contents);
2021-05-05 01:47:26 +08:00
try testing.expectEqualSlices(u8, data, contents);
}
test "AtomicFile" {
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const allocator = ctx.arena.allocator();
const test_out_file = try ctx.transformPath("tmp_atomic_file_test_dest.txt");
const test_content =
\\ hello!
\\ this is a test file
;
{
var af = try ctx.dir.atomicFile(test_out_file, .{});
defer af.deinit();
try af.file.writeAll(test_content);
try af.finish();
}
const content = try ctx.dir.readFileAlloc(allocator, test_out_file, 9999);
try testing.expectEqualStrings(test_content, content);
try ctx.dir.deleteFile(test_out_file);
}
}.impl);
}
test "open file with exclusive nonblocking lock twice" {
Add/fix missing WASI functionality to pass libstd tests This rather large commit adds/fixes missing WASI functionality in `libstd` needed to pass the `libstd` tests. As such, now by default tests targeting `wasm32-wasi` target are enabled in `test/tests.zig` module. However, they can be disabled by passing the `-Dskip-wasi=true` flag when invoking the `zig build test` command. When the flag is set to `false`, i.e., when WASI tests are included, `wasmtime` with `--dir=.` is used as the default testing command. Since the majority of `libstd` tests were relying on `fs.cwd()` call to get current working directory handle wrapped in `Dir` struct, in order to make the tests WASI-friendly, `fs.cwd()` call was replaced with `testing.getTestDir()` function which resolved to either `fs.cwd()` for non-WASI targets, or tries to fetch the preopen list from the WASI runtime and extract a preopen for '.' path. The summary of changes introduced by this commit: * implement `Dir.makeDir` and `Dir.openDir` targeting WASI * implement `Dir.deleteFile` and `Dir.deleteDir` targeting WASI * fix `os.close` and map errors in `unlinkat` * move WASI-specific `mkdirat` and `unlinkat` from `std.fs.wasi` to `std.os` module * implement `lseek_{SET, CUR, END}` targeting WASI * implement `futimens` targeting WASI * implement `ftruncate` targeting WASI * implement `readv`, `writev`, `pread{v}`, `pwrite{v}` targeting WASI * make sure ANSI escape codes are _not_ used in stderr or stdout in WASI, as WASI always sanitizes stderr, and sanitizes stdout if fd is a TTY * fix specifying WASI rights when opening/creating files/dirs * tweak `AtomicFile` to be WASI-compatible * implement `os.renameatWasi` for WASI-compliant `os.renameat` function * implement sleep() targeting WASI * fix `process.getEnvMap` targeting WASI
2020-05-05 23:23:49 +08:00
if (builtin.os.tag == .wasi) return error.SkipZigTest;
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
const file1 = try ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
defer file1.close();
const file2 = ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
try testing.expectError(error.WouldBlock, file2);
}
}.impl);
}
test "open file with shared and exclusive nonblocking lock" {
if (builtin.os.tag == .wasi) return error.SkipZigTest;
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
const file1 = try ctx.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });
defer file1.close();
const file2 = ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
try testing.expectError(error.WouldBlock, file2);
}
}.impl);
}
test "open file with exclusive and shared nonblocking lock" {
if (builtin.os.tag == .wasi) return error.SkipZigTest;
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const filename = try ctx.transformPath("file_nonblocking_lock_test.txt");
const file1 = try ctx.dir.createFile(filename, .{ .lock = .exclusive, .lock_nonblocking = true });
defer file1.close();
const file2 = ctx.dir.createFile(filename, .{ .lock = .shared, .lock_nonblocking = true });
try testing.expectError(error.WouldBlock, file2);
}
}.impl);
}
test "open file with exclusive lock twice, make sure second lock waits" {
if (builtin.single_threaded) return error.SkipZigTest;
if (std.io.is_async) {
// This test starts its own threads and is not compatible with async I/O.
return error.SkipZigTest;
}
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const filename = try ctx.transformPath("file_lock_test.txt");
const file = try ctx.dir.createFile(filename, .{ .lock = .exclusive });
errdefer file.close();
const S = struct {
fn checkFn(dir: *fs.Dir, path: []const u8, started: *std.Thread.ResetEvent, locked: *std.Thread.ResetEvent) !void {
started.set();
const file1 = try dir.createFile(path, .{ .lock = .exclusive });
locked.set();
file1.close();
}
};
var started = std.Thread.ResetEvent{};
var locked = std.Thread.ResetEvent{};
const t = try std.Thread.spawn(.{}, S.checkFn, .{
&ctx.dir,
filename,
&started,
&locked,
});
defer t.join();
// Wait for the spawned thread to start trying to acquire the exclusive file lock.
// Then wait a bit to make sure that can't acquire it since we currently hold the file lock.
started.wait();
try testing.expectError(error.Timeout, locked.timedWait(10 * std.time.ns_per_ms));
// Release the file lock which should unlock the thread to lock it and set the locked event.
file.close();
locked.wait();
}
}.impl);
}
test "open file with exclusive nonblocking lock twice (absolute paths)" {
Add/fix missing WASI functionality to pass libstd tests This rather large commit adds/fixes missing WASI functionality in `libstd` needed to pass the `libstd` tests. As such, now by default tests targeting `wasm32-wasi` target are enabled in `test/tests.zig` module. However, they can be disabled by passing the `-Dskip-wasi=true` flag when invoking the `zig build test` command. When the flag is set to `false`, i.e., when WASI tests are included, `wasmtime` with `--dir=.` is used as the default testing command. Since the majority of `libstd` tests were relying on `fs.cwd()` call to get current working directory handle wrapped in `Dir` struct, in order to make the tests WASI-friendly, `fs.cwd()` call was replaced with `testing.getTestDir()` function which resolved to either `fs.cwd()` for non-WASI targets, or tries to fetch the preopen list from the WASI runtime and extract a preopen for '.' path. The summary of changes introduced by this commit: * implement `Dir.makeDir` and `Dir.openDir` targeting WASI * implement `Dir.deleteFile` and `Dir.deleteDir` targeting WASI * fix `os.close` and map errors in `unlinkat` * move WASI-specific `mkdirat` and `unlinkat` from `std.fs.wasi` to `std.os` module * implement `lseek_{SET, CUR, END}` targeting WASI * implement `futimens` targeting WASI * implement `ftruncate` targeting WASI * implement `readv`, `writev`, `pread{v}`, `pwrite{v}` targeting WASI * make sure ANSI escape codes are _not_ used in stderr or stdout in WASI, as WASI always sanitizes stderr, and sanitizes stdout if fd is a TTY * fix specifying WASI rights when opening/creating files/dirs * tweak `AtomicFile` to be WASI-compatible * implement `os.renameatWasi` for WASI-compliant `os.renameat` function * implement sleep() targeting WASI * fix `process.getEnvMap` targeting WASI
2020-05-05 23:23:49 +08:00
if (builtin.os.tag == .wasi) return error.SkipZigTest;
2023-03-15 11:22:31 +08:00
var random_bytes: [12]u8 = undefined;
std.crypto.random.bytes(&random_bytes);
2023-03-15 11:22:31 +08:00
var random_b64: [fs.base64_encoder.calcSize(random_bytes.len)]u8 = undefined;
_ = fs.base64_encoder.encode(&random_b64, &random_bytes);
2023-03-15 11:22:31 +08:00
const sub_path = random_b64 ++ "-zig-test-absolute-paths.txt";
2023-03-15 11:22:31 +08:00
const gpa = testing.allocator;
const cwd = try std.process.getCwdAlloc(gpa);
defer gpa.free(cwd);
const filename = try fs.path.resolve(gpa, &.{ cwd, sub_path });
2023-03-15 11:22:31 +08:00
defer gpa.free(filename);
defer fs.deleteFileAbsolute(filename) catch {}; // createFileAbsolute can leave files on failures
2023-03-15 11:22:31 +08:00
const file1 = try fs.createFileAbsolute(filename, .{
.lock = .exclusive,
2023-03-15 11:22:31 +08:00
.lock_nonblocking = true,
});
const file2 = fs.createFileAbsolute(filename, .{
.lock = .exclusive,
2023-03-15 11:22:31 +08:00
.lock_nonblocking = true,
});
file1.close();
2021-05-05 01:47:26 +08:00
try testing.expectError(error.WouldBlock, file2);
}
test "walker" {
if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
var tmp = tmpDir(.{ .iterate = true });
defer tmp.cleanup();
// iteration order of walker is undefined, so need lookup maps to check against
const expected_paths = std.ComptimeStringMap(void, .{
.{"dir1"},
.{"dir2"},
.{"dir3"},
.{"dir4"},
.{"dir3" ++ fs.path.sep_str ++ "sub1"},
.{"dir3" ++ fs.path.sep_str ++ "sub2"},
.{"dir3" ++ fs.path.sep_str ++ "sub2" ++ fs.path.sep_str ++ "subsub1"},
});
const expected_basenames = std.ComptimeStringMap(void, .{
.{"dir1"},
.{"dir2"},
.{"dir3"},
.{"dir4"},
.{"sub1"},
.{"sub2"},
.{"subsub1"},
});
for (expected_paths.kvs) |kv| {
try tmp.dir.makePath(kv.key);
}
var walker = try tmp.dir.walk(testing.allocator);
defer walker.deinit();
var num_walked: usize = 0;
while (try walker.next()) |entry| {
testing.expect(expected_basenames.has(entry.basename)) catch |err| {
std.debug.print("found unexpected basename: {s}\n", .{std.fmt.fmtSliceEscapeLower(entry.basename)});
return err;
};
testing.expect(expected_paths.has(entry.path)) catch |err| {
std.debug.print("found unexpected path: {s}\n", .{std.fmt.fmtSliceEscapeLower(entry.path)});
return err;
};
// make sure that the entry.dir is the containing dir
var entry_dir = try entry.dir.openDir(entry.basename, .{});
defer entry_dir.close();
num_walked += 1;
}
try testing.expectEqual(expected_paths.kvs.len, num_walked);
}
test "walker without fully iterating" {
if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
var tmp = tmpDir(.{ .iterate = true });
defer tmp.cleanup();
2023-11-23 04:12:22 +08:00
var walker = try tmp.dir.walk(testing.allocator);
defer walker.deinit();
// Create 2 directories inside the tmp directory, but then only iterate once before breaking.
// This ensures that walker doesn't try to close the initial directory when not fully iterating.
2023-11-23 04:12:22 +08:00
try tmp.dir.makePath("a");
try tmp.dir.makePath("b");
var num_walked: usize = 0;
while (try walker.next()) |_| {
num_walked += 1;
break;
}
try testing.expectEqual(@as(usize, 1), num_walked);
}
test "'.' and '..' in fs.Dir functions" {
if (builtin.os.tag == .wasi and builtin.link_libc) return error.SkipZigTest;
if (builtin.os.tag == .windows and builtin.cpu.arch == .aarch64) {
// https://github.com/ziglang/zig/issues/17134
return error.SkipZigTest;
}
try testWithAllSupportedPathTypes(struct {
fn impl(ctx: *TestContext) !void {
const subdir_path = try ctx.transformPath("./subdir");
const file_path = try ctx.transformPath("./subdir/../file");
const copy_path = try ctx.transformPath("./subdir/../copy");
const rename_path = try ctx.transformPath("./subdir/../rename");
const update_path = try ctx.transformPath("./subdir/../update");
try ctx.dir.makeDir(subdir_path);
try ctx.dir.access(subdir_path, .{});
var created_subdir = try ctx.dir.openDir(subdir_path, .{});
created_subdir.close();
const created_file = try ctx.dir.createFile(file_path, .{});
created_file.close();
try ctx.dir.access(file_path, .{});
try ctx.dir.copyFile(file_path, ctx.dir, copy_path, .{});
try ctx.dir.rename(copy_path, rename_path);
const renamed_file = try ctx.dir.openFile(rename_path, .{});
renamed_file.close();
try ctx.dir.deleteFile(rename_path);
try ctx.dir.writeFile(update_path, "something");
const prev_status = try ctx.dir.updateFile(file_path, ctx.dir, update_path, .{});
2023-11-23 04:50:31 +08:00
try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status);
try ctx.dir.deleteDir(subdir_path);
}
}.impl);
}
test "'.' and '..' in absolute functions" {
if (builtin.os.tag == .wasi) return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
var arena = ArenaAllocator.init(testing.allocator);
defer arena.deinit();
const allocator = arena.allocator();
const base_path = blk: {
const relative_path = try fs.path.join(allocator, &.{ "zig-cache", "tmp", tmp.sub_path[0..] });
break :blk try fs.realpathAlloc(allocator, relative_path);
};
const subdir_path = try fs.path.join(allocator, &.{ base_path, "./subdir" });
try fs.makeDirAbsolute(subdir_path);
try fs.accessAbsolute(subdir_path, .{});
var created_subdir = try fs.openDirAbsolute(subdir_path, .{});
created_subdir.close();
const created_file_path = try fs.path.join(allocator, &.{ subdir_path, "../file" });
const created_file = try fs.createFileAbsolute(created_file_path, .{});
created_file.close();
try fs.accessAbsolute(created_file_path, .{});
const copied_file_path = try fs.path.join(allocator, &.{ subdir_path, "../copy" });
try fs.copyFileAbsolute(created_file_path, copied_file_path, .{});
const renamed_file_path = try fs.path.join(allocator, &.{ subdir_path, "../rename" });
try fs.renameAbsolute(copied_file_path, renamed_file_path);
const renamed_file = try fs.openFileAbsolute(renamed_file_path, .{});
renamed_file.close();
try fs.deleteFileAbsolute(renamed_file_path);
const update_file_path = try fs.path.join(allocator, &.{ subdir_path, "../update" });
2021-05-24 07:35:31 +08:00
const update_file = try fs.createFileAbsolute(update_file_path, .{});
try update_file.writeAll("something");
update_file.close();
const prev_status = try fs.updateFileAbsolute(created_file_path, update_file_path, .{});
2023-11-23 04:50:31 +08:00
try testing.expectEqual(fs.Dir.PrevStatus.stale, prev_status);
2021-05-24 07:35:31 +08:00
try fs.deleteDirAbsolute(subdir_path);
}
2021-11-05 23:19:49 +08:00
test "chmod" {
if (builtin.os.tag == .windows or builtin.os.tag == .wasi)
return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
const file = try tmp.dir.createFile("test_file", .{ .mode = 0o600 });
defer file.close();
try testing.expectEqual(@as(File.Mode, 0o600), (try file.stat()).mode & 0o7777);
2021-11-05 23:19:49 +08:00
try file.chmod(0o644);
try testing.expectEqual(@as(File.Mode, 0o644), (try file.stat()).mode & 0o7777);
2021-11-05 23:19:49 +08:00
try tmp.dir.makeDir("test_dir");
var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true });
defer dir.close();
2021-11-05 23:19:49 +08:00
try dir.chmod(0o700);
try testing.expectEqual(@as(File.Mode, 0o700), (try dir.stat()).mode & 0o7777);
2021-11-05 23:19:49 +08:00
}
test "chown" {
if (builtin.os.tag == .windows or builtin.os.tag == .wasi)
return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
const file = try tmp.dir.createFile("test_file", .{});
defer file.close();
try file.chown(null, null);
try tmp.dir.makeDir("test_dir");
var dir = try tmp.dir.openDir("test_dir", .{ .iterate = true });
defer dir.close();
try dir.chown(null, null);
2021-11-05 23:19:49 +08:00
}
std.fs: Implement cross-platform metadata API Implements a cross-platform metadata API, aiming to reduce unnecessary Unix-dependence of the `std.fs` api. Presently, all OSes beside Windows are treated as Unix; this is likely the best way to treat things by default, instead of explicitly listing each Unix-like OS. Platform-specific operations are not provided by `File.Metadata`, and instead are to be accessed from `File.Metadata.inner`. Adds: - File.setPermissions() : Sets permission of a file according to a `Permissions` struct (not available on WASI) - File.Permissions : A cross-platform representation of file permissions - Permissions.readOnly() : Returns whether the file is read-only - Permissions.setReadOnly() : Sets whether the file is read-only - Permissions.unixSet() : Sets permissions for a class (UNIX-only) - Permissions.unixGet() : Checks a permission for a class (UNIX-only) - Permissions.unixNew() : Returns a new Permissions struct to represent the passed mode (UNIX-only) - File.Metadata : A cross-platform representation of file metadata - Metadata.size() : Returns the size of a file - Metadata.permissions() : Returns a `Permissions` struct, representing permissions on the file - Metadata.kind() : Returns the `Kind` of the file - Metadata.accessed() : Returns the time the file was last accessed - Metadata.modified() : Returns the time the file was last modified - Metadata.created() : Returns the time the file was created (this is an optional, as the underlying filesystem, or OS may not support this) Methods of `File.Metadata` are also available for the below, so I won't repeat myself The below may be used for platform-specific functionality - File.MetadataUnix : The internal implementation of `File.Metadata` on Unices - File.MetadataLinux : The internal implementation of `File.Metadata` on Linux - File.MetadataWindows : The implementation of `File.Metadata` on Windows
2022-01-09 05:22:23 +08:00
test "File.Metadata" {
var tmp = tmpDir(.{});
defer tmp.cleanup();
const file = try tmp.dir.createFile("test_file", .{ .read = true });
defer file.close();
const metadata = try file.metadata();
try testing.expectEqual(File.Kind.file, metadata.kind());
try testing.expectEqual(@as(u64, 0), metadata.size());
std.fs: Implement cross-platform metadata API Implements a cross-platform metadata API, aiming to reduce unnecessary Unix-dependence of the `std.fs` api. Presently, all OSes beside Windows are treated as Unix; this is likely the best way to treat things by default, instead of explicitly listing each Unix-like OS. Platform-specific operations are not provided by `File.Metadata`, and instead are to be accessed from `File.Metadata.inner`. Adds: - File.setPermissions() : Sets permission of a file according to a `Permissions` struct (not available on WASI) - File.Permissions : A cross-platform representation of file permissions - Permissions.readOnly() : Returns whether the file is read-only - Permissions.setReadOnly() : Sets whether the file is read-only - Permissions.unixSet() : Sets permissions for a class (UNIX-only) - Permissions.unixGet() : Checks a permission for a class (UNIX-only) - Permissions.unixNew() : Returns a new Permissions struct to represent the passed mode (UNIX-only) - File.Metadata : A cross-platform representation of file metadata - Metadata.size() : Returns the size of a file - Metadata.permissions() : Returns a `Permissions` struct, representing permissions on the file - Metadata.kind() : Returns the `Kind` of the file - Metadata.accessed() : Returns the time the file was last accessed - Metadata.modified() : Returns the time the file was last modified - Metadata.created() : Returns the time the file was created (this is an optional, as the underlying filesystem, or OS may not support this) Methods of `File.Metadata` are also available for the below, so I won't repeat myself The below may be used for platform-specific functionality - File.MetadataUnix : The internal implementation of `File.Metadata` on Unices - File.MetadataLinux : The internal implementation of `File.Metadata` on Linux - File.MetadataWindows : The implementation of `File.Metadata` on Windows
2022-01-09 05:22:23 +08:00
_ = metadata.accessed();
_ = metadata.modified();
_ = metadata.created();
}
test "File.Permissions" {
if (builtin.os.tag == .wasi)
return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
const file = try tmp.dir.createFile("test_file", .{ .read = true });
defer file.close();
const metadata = try file.metadata();
var permissions = metadata.permissions();
try testing.expect(!permissions.readOnly());
permissions.setReadOnly(true);
try testing.expect(permissions.readOnly());
try file.setPermissions(permissions);
const new_permissions = (try file.metadata()).permissions();
try testing.expect(new_permissions.readOnly());
// Must be set to non-read-only to delete
permissions.setReadOnly(false);
try file.setPermissions(permissions);
}
test "File.PermissionsUnix" {
if (builtin.os.tag == .windows or builtin.os.tag == .wasi)
return error.SkipZigTest;
var tmp = tmpDir(.{});
defer tmp.cleanup();
const file = try tmp.dir.createFile("test_file", .{ .mode = 0o666, .read = true });
defer file.close();
const metadata = try file.metadata();
var permissions = metadata.permissions();
permissions.setReadOnly(true);
try testing.expect(permissions.readOnly());
try testing.expect(!permissions.inner.unixHas(.user, .write));
permissions.inner.unixSet(.user, .{ .write = true });
try testing.expect(!permissions.readOnly());
try testing.expect(permissions.inner.unixHas(.user, .write));
try testing.expect(permissions.inner.mode & 0o400 != 0);
permissions.setReadOnly(true);
try file.setPermissions(permissions);
permissions = (try file.metadata()).permissions();
try testing.expect(permissions.readOnly());
// Must be set to non-read-only to delete
permissions.setReadOnly(false);
try file.setPermissions(permissions);
const permissions_unix = File.PermissionsUnix.unixNew(0o754);
try testing.expect(permissions_unix.unixHas(.user, .execute));
try testing.expect(!permissions_unix.unixHas(.other, .execute));
}
test "delete a read-only file on windows" {
if (builtin.os.tag != .windows)
2023-05-02 00:22:28 +08:00
return error.SkipZigTest;
var tmp = testing.tmpDir(.{});
2023-05-02 00:22:28 +08:00
defer tmp.cleanup();
const file = try tmp.dir.createFile("test_file", .{ .read = true });
2023-05-02 00:22:28 +08:00
defer file.close();
// Create a file and make it read-only
const metadata = try file.metadata();
var permissions = metadata.permissions();
permissions.setReadOnly(true);
try file.setPermissions(permissions);
// If the OS and filesystem support it, POSIX_SEMANTICS and IGNORE_READONLY_ATTRIBUTE
// is used meaning that the deletion of a read-only file will succeed.
// Otherwise, this delete will fail and the read-only flag must be unset before it's
// able to be deleted.
const delete_result = tmp.dir.deleteFile("test_file");
if (delete_result) {
try testing.expectError(error.FileNotFound, tmp.dir.deleteFile("test_file"));
} else |err| {
try testing.expectEqual(@as(anyerror, error.AccessDenied), err);
// Now make the file not read-only
permissions.setReadOnly(false);
try file.setPermissions(permissions);
try tmp.dir.deleteFile("test_file");
}
}
test "delete a setAsCwd directory on Windows" {
if (builtin.os.tag != .windows) return error.SkipZigTest;
var tmp = tmpDir(.{});
// Set tmp dir as current working directory.
try tmp.dir.setAsCwd();
tmp.dir.close();
try testing.expectError(error.FileBusy, tmp.parent_dir.deleteTree(&tmp.sub_path));
// Now set the parent dir as the current working dir for clean up.
try tmp.parent_dir.setAsCwd();
try tmp.parent_dir.deleteTree(&tmp.sub_path);
// Close the parent "tmp" so we don't leak the HANDLE.
tmp.parent_dir.close();
}