authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2025-12-26 17:23:14-08:00
committergravatar for andrew@ziglang.orgAndrew Kelley <andrew@ziglang.org> 2025-12-26 19:58:56-08:00
loge3f36d0d81b85c213d03ffcd090ce19bfcbb3fbc
tree5459b5986e3ed68c0a3810c857f9bae4c2d2c6b7
parent4f8aa8213d6314cba7ea21c5b13a5b9acbac3ad7

Fix a few standalone tests on Windows


5 files changed, 208 insertions(+), 91 deletions(-)

test/standalone/env_vars/main.zig+2
...@@ -3,6 +3,8 @@ const builtin = @import("builtin");...@@ -3,6 +3,8 @@ const builtin = @import("builtin");
33
4// Note: the environment variables under test are set by the build.zig4// Note: the environment variables under test are set by the build.zig
5pub fn main() !void {5pub fn main() !void {
6 @setEvalBranchQuota(10000);
7
6 var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;8 var gpa: std.heap.GeneralPurposeAllocator(.{}) = .init;
7 defer _ = gpa.deinit();9 defer _ = gpa.deinit();
8 const allocator = gpa.allocator();10 const allocator = gpa.allocator();
test/standalone/windows_argv/build.zig+1-1
...@@ -67,7 +67,7 @@ pub fn build(b: *std.Build) !void {...@@ -67,7 +67,7 @@ pub fn build(b: *std.Build) !void {
6767
68 // Only target the MSVC ABI if MSVC/Windows SDK is available68 // Only target the MSVC ABI if MSVC/Windows SDK is available
69 const has_msvc = has_msvc: {69 const has_msvc = has_msvc: {
70 const sdk = std.zig.WindowsSdk.find(b.allocator, builtin.cpu.arch) catch |err| switch (err) {70 const sdk = std.zig.WindowsSdk.find(b.allocator, b.graph.io, builtin.cpu.arch) catch |err| switch (err) {
71 error.OutOfMemory => @panic("oom"),71 error.OutOfMemory => @panic("oom"),
72 else => break :has_msvc false,72 else => break :has_msvc false,
73 };73 };
test/standalone/windows_bat_args/fuzz.zig+41-2
...@@ -10,6 +10,7 @@ pub fn main() anyerror!void {...@@ -10,6 +10,7 @@ pub fn main() anyerror!void {
10 const gpa = debug_alloc_inst.allocator();10 const gpa = debug_alloc_inst.allocator();
1111
12 var threaded: Io.Threaded = .init(gpa, .{});12 var threaded: Io.Threaded = .init(gpa, .{});
13 defer threaded.deinit();
13 const io = threaded.io();14 const io = threaded.io();
1415
15 var it = try std.process.argsWithAllocator(gpa);16 var it = try std.process.argsWithAllocator(gpa);
...@@ -41,8 +42,8 @@ pub fn main() anyerror!void {...@@ -41,8 +42,8 @@ pub fn main() anyerror!void {
41 std.debug.print("rand seed: {}\n", .{seed});42 std.debug.print("rand seed: {}\n", .{seed});
42 }43 }
4344
44 var tmp = std.testing.tmpDir(.{});45 var tmp = tmpDir(io, .{});
45 defer tmp.cleanup();46 defer tmp.cleanup(io);
4647
47 try std.process.setCurrentDir(io, tmp.dir);48 try std.process.setCurrentDir(io, tmp.dir);
48 defer std.process.setCurrentDir(io, tmp.parent_dir) catch {};49 defer std.process.setCurrentDir(io, tmp.parent_dir) catch {};
...@@ -167,3 +168,41 @@ fn randomArg(gpa: Allocator, rand: std.Random) ![]const u8 {...@@ -167,3 +168,41 @@ fn randomArg(gpa: Allocator, rand: std.Random) ![]const u8 {
167168
168 return buf.toOwnedSlice(gpa);169 return buf.toOwnedSlice(gpa);
169}170}
171
172pub fn tmpDir(io: Io, opts: Io.Dir.OpenOptions) TmpDir {
173 var random_bytes: [TmpDir.random_bytes_count]u8 = undefined;
174 std.crypto.random.bytes(&random_bytes);
175 var sub_path: [TmpDir.sub_path_len]u8 = undefined;
176 _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes);
177
178 const cwd = Io.Dir.cwd();
179 var cache_dir = cwd.createDirPathOpen(io, ".zig-cache", .{}) catch
180 @panic("unable to make tmp dir for testing: unable to make and open .zig-cache dir");
181 defer cache_dir.close(io);
182 const parent_dir = cache_dir.createDirPathOpen(io, "tmp", .{}) catch
183 @panic("unable to make tmp dir for testing: unable to make and open .zig-cache/tmp dir");
184 const dir = parent_dir.createDirPathOpen(io, &sub_path, .{ .open_options = opts }) catch
185 @panic("unable to make tmp dir for testing: unable to make and open the tmp dir");
186
187 return .{
188 .dir = dir,
189 .parent_dir = parent_dir,
190 .sub_path = sub_path,
191 };
192}
193
194pub const TmpDir = struct {
195 dir: Io.Dir,
196 parent_dir: Io.Dir,
197 sub_path: [sub_path_len]u8,
198
199 const random_bytes_count = 12;
200 const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count);
201
202 pub fn cleanup(self: *TmpDir, io: Io) void {
203 self.dir.close(io);
204 self.parent_dir.deleteTree(io, &self.sub_path) catch {};
205 self.parent_dir.close(io);
206 self.* = undefined;
207 }
208};
test/standalone/windows_bat_args/test.zig+86-48
...@@ -15,8 +15,8 @@ pub fn main() anyerror!void {...@@ -15,8 +15,8 @@ pub fn main() anyerror!void {
15 _ = it.next() orelse unreachable; // skip binary name15 _ = it.next() orelse unreachable; // skip binary name
16 const child_exe_path_orig = it.next() orelse unreachable;16 const child_exe_path_orig = it.next() orelse unreachable;
1717
18 var tmp = std.testing.tmpDir(.{});18 var tmp = tmpDir(io, .{});
19 defer tmp.cleanup();19 defer tmp.cleanup(io);
2020
21 try std.process.setCurrentDir(io, tmp.dir);21 try std.process.setCurrentDir(io, tmp.dir);
22 defer std.process.setCurrentDir(io, tmp.parent_dir) catch {};22 defer std.process.setCurrentDir(io, tmp.parent_dir) catch {};
...@@ -47,41 +47,41 @@ pub fn main() anyerror!void {...@@ -47,41 +47,41 @@ pub fn main() anyerror!void {
47 buf.shrinkRetainingCapacity(preamble_len);47 buf.shrinkRetainingCapacity(preamble_len);
4848
49 // Test cases are from https://github.com/rust-lang/rust/blob/master/tests/ui/std/windows-bat-args.rs49 // Test cases are from https://github.com/rust-lang/rust/blob/master/tests/ui/std/windows-bat-args.rs
50 try testExecError(error.InvalidBatchScriptArg, gpa, &.{"\x00"});50 try testExecError(error.InvalidBatchScriptArg, gpa, io, &.{"\x00"});
51 try testExecError(error.InvalidBatchScriptArg, gpa, &.{"\n"});51 try testExecError(error.InvalidBatchScriptArg, gpa, io, &.{"\n"});
52 try testExecError(error.InvalidBatchScriptArg, gpa, &.{"\r"});52 try testExecError(error.InvalidBatchScriptArg, gpa, io, &.{"\r"});
53 try testExec(gpa, &.{ "a", "b" }, null);53 try testExec(gpa, io, &.{ "a", "b" }, null);
54 try testExec(gpa, &.{ "c is for cat", "d is for dog" }, null);54 try testExec(gpa, io, &.{ "c is for cat", "d is for dog" }, null);
55 try testExec(gpa, &.{ "\"", " \"" }, null);55 try testExec(gpa, io, &.{ "\"", " \"" }, null);
56 try testExec(gpa, &.{ "\\", "\\" }, null);56 try testExec(gpa, io, &.{ "\\", "\\" }, null);
57 try testExec(gpa, &.{">file.txt"}, null);57 try testExec(gpa, io, &.{">file.txt"}, null);
58 try testExec(gpa, &.{"whoami.exe"}, null);58 try testExec(gpa, io, &.{"whoami.exe"}, null);
59 try testExec(gpa, &.{"&a.exe"}, null);59 try testExec(gpa, io, &.{"&a.exe"}, null);
60 try testExec(gpa, &.{"&echo hello "}, null);60 try testExec(gpa, io, &.{"&echo hello "}, null);
61 try testExec(gpa, &.{ "&echo hello", "&whoami", ">file.txt" }, null);61 try testExec(gpa, io, &.{ "&echo hello", "&whoami", ">file.txt" }, null);
62 try testExec(gpa, &.{"!TMP!"}, null);62 try testExec(gpa, io, &.{"!TMP!"}, null);
63 try testExec(gpa, &.{"key=value"}, null);63 try testExec(gpa, io, &.{"key=value"}, null);
64 try testExec(gpa, &.{"\"key=value\""}, null);64 try testExec(gpa, io, &.{"\"key=value\""}, null);
65 try testExec(gpa, &.{"key = value"}, null);65 try testExec(gpa, io, &.{"key = value"}, null);
66 try testExec(gpa, &.{"key=[\"value\"]"}, null);66 try testExec(gpa, io, &.{"key=[\"value\"]"}, null);
67 try testExec(gpa, &.{ "", "a=b" }, null);67 try testExec(gpa, io, &.{ "", "a=b" }, null);
68 try testExec(gpa, &.{"key=\"foo bar\""}, null);68 try testExec(gpa, io, &.{"key=\"foo bar\""}, null);
69 try testExec(gpa, &.{"key=[\"my_value]"}, null);69 try testExec(gpa, io, &.{"key=[\"my_value]"}, null);
70 try testExec(gpa, &.{"key=[\"my_value\",\"other-value\"]"}, null);70 try testExec(gpa, io, &.{"key=[\"my_value\",\"other-value\"]"}, null);
71 try testExec(gpa, &.{"key\\=value"}, null);71 try testExec(gpa, io, &.{"key\\=value"}, null);
72 try testExec(gpa, &.{"key=\"&whoami\""}, null);72 try testExec(gpa, io, &.{"key=\"&whoami\""}, null);
73 try testExec(gpa, &.{"key=\"value\"=5"}, null);73 try testExec(gpa, io, &.{"key=\"value\"=5"}, null);
74 try testExec(gpa, &.{"key=[\">file.txt\"]"}, null);74 try testExec(gpa, io, &.{"key=[\">file.txt\"]"}, null);
75 try testExec(gpa, &.{"%hello"}, null);75 try testExec(gpa, io, &.{"%hello"}, null);
76 try testExec(gpa, &.{"%PATH%"}, null);76 try testExec(gpa, io, &.{"%PATH%"}, null);
77 try testExec(gpa, &.{"%%cd:~,%"}, null);77 try testExec(gpa, io, &.{"%%cd:~,%"}, null);
78 try testExec(gpa, &.{"%PATH%PATH%"}, null);78 try testExec(gpa, io, &.{"%PATH%PATH%"}, null);
79 try testExec(gpa, &.{"\">file.txt"}, null);79 try testExec(gpa, io, &.{"\">file.txt"}, null);
80 try testExec(gpa, &.{"abc\"&echo hello"}, null);80 try testExec(gpa, io, &.{"abc\"&echo hello"}, null);
81 try testExec(gpa, &.{"123\">file.txt"}, null);81 try testExec(gpa, io, &.{"123\">file.txt"}, null);
82 try testExec(gpa, &.{"\"&echo hello&whoami.exe"}, null);82 try testExec(gpa, io, &.{"\"&echo hello&whoami.exe"}, null);
83 try testExec(gpa, &.{ "\"hello^\"world\"", "hello &echo oh no >file.txt" }, null);83 try testExec(gpa, io, &.{ "\"hello^\"world\"", "hello &echo oh no >file.txt" }, null);
84 try testExec(gpa, &.{"&whoami.exe"}, null);84 try testExec(gpa, io, &.{"&whoami.exe"}, null);
8585
86 // Ensure that trailing space and . characters can't lead to unexpected bat/cmd script execution.86 // Ensure that trailing space and . characters can't lead to unexpected bat/cmd script execution.
87 // In many Windows APIs (including CreateProcess), trailing space and . characters are stripped87 // In many Windows APIs (including CreateProcess), trailing space and . characters are stripped
...@@ -99,14 +99,14 @@ pub fn main() anyerror!void {...@@ -99,14 +99,14 @@ pub fn main() anyerror!void {
99 // > "args1.bat .. "99 // > "args1.bat .. "
100 // '"args1.bat .. "' is not recognized as an internal or external command,100 // '"args1.bat .. "' is not recognized as an internal or external command,
101 // operable program or batch file.101 // operable program or batch file.
102 try std.testing.expectError(error.FileNotFound, testExecBat(gpa, "args1.bat .. ", &.{"abc"}, null));102 try std.testing.expectError(error.FileNotFound, testExecBat(gpa, io, "args1.bat .. ", &.{"abc"}, null));
103 const absolute_with_trailing = blk: {103 const absolute_with_trailing = blk: {
104 const absolute_path = try Io.Dir.realPathFileAbsoluteAlloc(io, "args1.bat", gpa);104 const absolute_path = try Io.Dir.cwd().realPathFileAlloc(io, "args1.bat", gpa);
105 defer gpa.free(absolute_path);105 defer gpa.free(absolute_path);
106 break :blk try std.mem.concat(gpa, u8, &.{ absolute_path, " .. " });106 break :blk try std.mem.concat(gpa, u8, &.{ absolute_path, " .. " });
107 };107 };
108 defer gpa.free(absolute_with_trailing);108 defer gpa.free(absolute_with_trailing);
109 try std.testing.expectError(error.FileNotFound, testExecBat(gpa, absolute_with_trailing, &.{"abc"}, null));109 try std.testing.expectError(error.FileNotFound, testExecBat(gpa, io, absolute_with_trailing, &.{"abc"}, null));
110110
111 var env = env: {111 var env = env: {
112 var env = try std.process.getEnvMap(gpa);112 var env = try std.process.getEnvMap(gpa);
...@@ -120,20 +120,20 @@ pub fn main() anyerror!void {...@@ -120,20 +120,20 @@ pub fn main() anyerror!void {
120 break :env env;120 break :env env;
121 };121 };
122 defer env.deinit();122 defer env.deinit();
123 try testExec(gpa, &.{"%FOO%"}, &env);123 try testExec(gpa, io, &.{"%FOO%"}, &env);
124124
125 // Ensure that none of the `>file.txt`s have caused file.txt to be created125 // Ensure that none of the `>file.txt`s have caused file.txt to be created
126 try std.testing.expectError(error.FileNotFound, tmp.dir.access("file.txt", .{}));126 try std.testing.expectError(error.FileNotFound, tmp.dir.access(io, "file.txt", .{}));
127}127}
128128
129fn testExecError(err: anyerror, gpa: Allocator, args: []const []const u8) !void {129fn testExecError(err: anyerror, gpa: Allocator, io: Io, args: []const []const u8) !void {
130 return std.testing.expectError(err, testExec(gpa, args, null));130 return std.testing.expectError(err, testExec(gpa, io, args, null));
131}131}
132132
133fn testExec(gpa: Allocator, args: []const []const u8, env: ?*std.process.EnvMap) !void {133fn testExec(gpa: Allocator, io: Io, args: []const []const u8, env: ?*std.process.EnvMap) !void {
134 try testExecBat(gpa, "args1.bat", args, env);134 try testExecBat(gpa, io, "args1.bat", args, env);
135 try testExecBat(gpa, "args2.bat", args, env);135 try testExecBat(gpa, io, "args2.bat", args, env);
136 try testExecBat(gpa, "args3.bat", args, env);136 try testExecBat(gpa, io, "args3.bat", args, env);
137}137}
138138
139fn testExecBat(gpa: Allocator, io: Io, bat: []const u8, args: []const []const u8, env: ?*std.process.EnvMap) !void {139fn testExecBat(gpa: Allocator, io: Io, bat: []const u8, args: []const []const u8, env: ?*std.process.EnvMap) !void {
...@@ -164,3 +164,41 @@ fn testExecBat(gpa: Allocator, io: Io, bat: []const u8, args: []const []const u8...@@ -164,3 +164,41 @@ fn testExecBat(gpa: Allocator, io: Io, bat: []const u8, args: []const []const u8
164 i += 1;164 i += 1;
165 }165 }
166}166}
167
168pub fn tmpDir(io: Io, opts: Io.Dir.OpenOptions) TmpDir {
169 var random_bytes: [TmpDir.random_bytes_count]u8 = undefined;
170 std.crypto.random.bytes(&random_bytes);
171 var sub_path: [TmpDir.sub_path_len]u8 = undefined;
172 _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes);
173
174 const cwd = Io.Dir.cwd();
175 var cache_dir = cwd.createDirPathOpen(io, ".zig-cache", .{}) catch
176 @panic("unable to make tmp dir for testing: unable to make and open .zig-cache dir");
177 defer cache_dir.close(io);
178 const parent_dir = cache_dir.createDirPathOpen(io, "tmp", .{}) catch
179 @panic("unable to make tmp dir for testing: unable to make and open .zig-cache/tmp dir");
180 const dir = parent_dir.createDirPathOpen(io, &sub_path, .{ .open_options = opts }) catch
181 @panic("unable to make tmp dir for testing: unable to make and open the tmp dir");
182
183 return .{
184 .dir = dir,
185 .parent_dir = parent_dir,
186 .sub_path = sub_path,
187 };
188}
189
190pub const TmpDir = struct {
191 dir: Io.Dir,
192 parent_dir: Io.Dir,
193 sub_path: [sub_path_len]u8,
194
195 const random_bytes_count = 12;
196 const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count);
197
198 pub fn cleanup(self: *TmpDir, io: Io) void {
199 self.dir.close(io);
200 self.parent_dir.deleteTree(io, &self.sub_path) catch {};
201 self.parent_dir.close(io);
202 self.* = undefined;
203 }
204};
test/standalone/windows_spawn/main.zig+78-40
...@@ -19,8 +19,8 @@ pub fn main() anyerror!void {...@@ -19,8 +19,8 @@ pub fn main() anyerror!void {
19 _ = it.next() orelse unreachable; // skip binary name19 _ = it.next() orelse unreachable; // skip binary name
20 const hello_exe_cache_path = it.next() orelse unreachable;20 const hello_exe_cache_path = it.next() orelse unreachable;
2121
22 var tmp = std.testing.tmpDir(.{});22 var tmp = tmpDir(io, .{});
23 defer tmp.cleanup();23 defer tmp.cleanup(io);
2424
25 const tmp_absolute_path = try tmp.dir.realPathFileAlloc(io, ".", gpa);25 const tmp_absolute_path = try tmp.dir.realPathFileAlloc(io, ".", gpa);
26 defer gpa.free(tmp_absolute_path);26 defer gpa.free(tmp_absolute_path);
...@@ -44,10 +44,10 @@ pub fn main() anyerror!void {...@@ -44,10 +44,10 @@ pub fn main() anyerror!void {
44 ) == windows.TRUE);44 ) == windows.TRUE);
4545
46 // No PATH, so it should fail to find anything not in the cwd46 // No PATH, so it should fail to find anything not in the cwd
47 try testExecError(error.FileNotFound, gpa, "something_missing");47 try testExecError(error.FileNotFound, gpa, io, "something_missing");
4848
49 // make sure we don't get error.BadPath traversing out of cwd with a relative path49 // make sure we don't get error.BadPath traversing out of cwd with a relative path
50 try testExecError(error.FileNotFound, gpa, "..\\.\\.\\.\\\\..\\more_missing");50 try testExecError(error.FileNotFound, gpa, io, "..\\.\\.\\.\\\\..\\more_missing");
5151
52 std.debug.assert(windows.kernel32.SetEnvironmentVariableW(52 std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
53 utf16Literal("PATH"),53 utf16Literal("PATH"),
...@@ -55,12 +55,12 @@ pub fn main() anyerror!void {...@@ -55,12 +55,12 @@ pub fn main() anyerror!void {
55 ) == windows.TRUE);55 ) == windows.TRUE);
5656
57 // Move hello.exe into the tmp dir which is now added to the path57 // Move hello.exe into the tmp dir which is now added to the path
58 try Io.Dir.cwd().copyFile(hello_exe_cache_path, tmp.dir, "hello.exe", .{});58 try Io.Dir.cwd().copyFile(hello_exe_cache_path, tmp.dir, "hello.exe", io, .{});
5959
60 // with extension should find the .exe (case insensitive)60 // with extension should find the .exe (case insensitive)
61 try testExec(gpa, "HeLLo.exe", "hello from exe\n");61 try testExec(gpa, io, "HeLLo.exe", "hello from exe\n");
62 // without extension should find the .exe (case insensitive)62 // without extension should find the .exe (case insensitive)
63 try testExec(gpa, "heLLo", "hello from exe\n");63 try testExec(gpa, io, "heLLo", "hello from exe\n");
64 // with invalid cwd64 // with invalid cwd
65 try std.testing.expectError(error.FileNotFound, testExecWithCwd(gpa, io, "hello.exe", "missing_dir", ""));65 try std.testing.expectError(error.FileNotFound, testExecWithCwd(gpa, io, "hello.exe", "missing_dir", ""));
6666
...@@ -70,33 +70,33 @@ pub fn main() anyerror!void {...@@ -70,33 +70,33 @@ pub fn main() anyerror!void {
70 try tmp.dir.writeFile(io, .{ .sub_path = "hello.cmd", .data = "@echo hello from cmd" });70 try tmp.dir.writeFile(io, .{ .sub_path = "hello.cmd", .data = "@echo hello from cmd" });
7171
72 // with extension should find the .bat (case insensitive)72 // with extension should find the .bat (case insensitive)
73 try testExec(gpa, "heLLo.bat", "hello from bat\r\n");73 try testExec(gpa, io, "heLLo.bat", "hello from bat\r\n");
74 // with extension should find the .cmd (case insensitive)74 // with extension should find the .cmd (case insensitive)
75 try testExec(gpa, "heLLo.cmd", "hello from cmd\r\n");75 try testExec(gpa, io, "heLLo.cmd", "hello from cmd\r\n");
76 // without extension should find the .exe (since its first in PATHEXT)76 // without extension should find the .exe (since its first in PATHEXT)
77 try testExec(gpa, "heLLo", "hello from exe\n");77 try testExec(gpa, io, "heLLo", "hello from exe\n");
7878
79 // now rename the exe to not have an extension79 // now rename the exe to not have an extension
80 try renameExe(tmp.dir, "hello.exe", "hello");80 try renameExe(tmp.dir, io, "hello.exe", "hello");
8181
82 // with extension should now fail82 // with extension should now fail
83 try testExecError(error.FileNotFound, gpa, "hello.exe");83 try testExecError(error.FileNotFound, gpa, io, "hello.exe");
84 // without extension should succeed (case insensitive)84 // without extension should succeed (case insensitive)
85 try testExec(gpa, "heLLo", "hello from exe\n");85 try testExec(gpa, io, "heLLo", "hello from exe\n");
8686
87 try tmp.dir.createDir(io, "something", .default_dir);87 try tmp.dir.createDir(io, "something", .default_dir);
88 try renameExe(tmp.dir, "hello", "something/hello.exe");88 try renameExe(tmp.dir, io, "hello", "something/hello.exe");
8989
90 const relative_path_no_ext = try std.fs.path.join(gpa, &.{ tmp_relative_path, "something/hello" });90 const relative_path_no_ext = try std.fs.path.join(gpa, &.{ tmp_relative_path, "something/hello" });
91 defer gpa.free(relative_path_no_ext);91 defer gpa.free(relative_path_no_ext);
9292
93 // Giving a full relative path to something/hello should work93 // Giving a full relative path to something/hello should work
94 try testExec(gpa, relative_path_no_ext, "hello from exe\n");94 try testExec(gpa, io, relative_path_no_ext, "hello from exe\n");
95 // But commands with path separators get excluded from PATH searching, so this will fail95 // But commands with path separators get excluded from PATH searching, so this will fail
96 try testExecError(error.FileNotFound, gpa, "something/hello");96 try testExecError(error.FileNotFound, gpa, io, "something/hello");
9797
98 // Now that .BAT is the first PATHEXT that should be found, this should succeed98 // Now that .BAT is the first PATHEXT that should be found, this should succeed
99 try testExec(gpa, "heLLo", "hello from bat\r\n");99 try testExec(gpa, io, "heLLo", "hello from bat\r\n");
100100
101 // Add a hello.exe that is not a valid executable101 // Add a hello.exe that is not a valid executable
102 try tmp.dir.writeFile(io, .{ .sub_path = "hello.exe", .data = "invalid" });102 try tmp.dir.writeFile(io, .{ .sub_path = "hello.exe", .data = "invalid" });
...@@ -105,26 +105,26 @@ pub fn main() anyerror!void {...@@ -105,26 +105,26 @@ pub fn main() anyerror!void {
105 // case for .EXE extensions, where if they ever try to get executed but they are105 // case for .EXE extensions, where if they ever try to get executed but they are
106 // invalid, that gets treated as a fatal error wherever they are found and InvalidExe106 // invalid, that gets treated as a fatal error wherever they are found and InvalidExe
107 // is returned immediately.107 // is returned immediately.
108 try testExecError(error.InvalidExe, gpa, "hello.exe");108 try testExecError(error.InvalidExe, gpa, io, "hello.exe");
109 // Same thing applies to the command with no extension--even though there is a109 // Same thing applies to the command with no extension--even though there is a
110 // hello.bat that could be executed, it should stop after it tries executing110 // hello.bat that could be executed, it should stop after it tries executing
111 // hello.exe and getting InvalidExe.111 // hello.exe and getting InvalidExe.
112 try testExecError(error.InvalidExe, gpa, "hello");112 try testExecError(error.InvalidExe, gpa, io, "hello");
113113
114 // If we now rename hello.exe to have no extension, it will behave differently114 // If we now rename hello.exe to have no extension, it will behave differently
115 try renameExe(tmp.dir, "hello.exe", "hello");115 try renameExe(tmp.dir, io, "hello.exe", "hello");
116116
117 // Now, trying to execute it without an extension should treat InvalidExe as recoverable117 // Now, trying to execute it without an extension should treat InvalidExe as recoverable
118 // and skip over it and find hello.bat and execute that118 // and skip over it and find hello.bat and execute that
119 try testExec(gpa, "hello", "hello from bat\r\n");119 try testExec(gpa, io, "hello", "hello from bat\r\n");
120120
121 // If we rename the invalid exe to something else121 // If we rename the invalid exe to something else
122 try renameExe(tmp.dir, "hello", "goodbye");122 try renameExe(tmp.dir, io, "hello", "goodbye");
123 // Then we should now get FileNotFound when trying to execute 'goodbye',123 // Then we should now get FileNotFound when trying to execute 'goodbye',
124 // since that is what the original error will be after searching for 'goodbye'124 // since that is what the original error will be after searching for 'goodbye'
125 // in the cwd. It will try to execute 'goodbye' from the PATH but the InvalidExe error125 // in the cwd. It will try to execute 'goodbye' from the PATH but the InvalidExe error
126 // should be ignored in this case.126 // should be ignored in this case.
127 try testExecError(error.FileNotFound, gpa, "goodbye");127 try testExecError(error.FileNotFound, gpa, io, "goodbye");
128128
129 // Now let's set the tmp dir as the cwd and set the path only include the "something" sub dir129 // Now let's set the tmp dir as the cwd and set the path only include the "something" sub dir
130 try std.process.setCurrentDir(io, tmp.dir);130 try std.process.setCurrentDir(io, tmp.dir);
...@@ -139,26 +139,26 @@ pub fn main() anyerror!void {...@@ -139,26 +139,26 @@ pub fn main() anyerror!void {
139139
140 // Now trying to execute goodbye should give error.InvalidExe since it's the original140 // Now trying to execute goodbye should give error.InvalidExe since it's the original
141 // error that we got when trying within the cwd141 // error that we got when trying within the cwd
142 try testExecError(error.InvalidExe, gpa, "goodbye");142 try testExecError(error.InvalidExe, gpa, io, "goodbye");
143143
144 // hello should still find the .bat144 // hello should still find the .bat
145 try testExec(gpa, "hello", "hello from bat\r\n");145 try testExec(gpa, io, "hello", "hello from bat\r\n");
146146
147 // If we rename something/hello.exe to something/goodbye.exe147 // If we rename something/hello.exe to something/goodbye.exe
148 try renameExe(tmp.dir, "something/hello.exe", "something/goodbye.exe");148 try renameExe(tmp.dir, io, "something/hello.exe", "something/goodbye.exe");
149 // And try to execute goodbye, then the one in something should be found149 // And try to execute goodbye, then the one in something should be found
150 // since the one in cwd is an invalid executable150 // since the one in cwd is an invalid executable
151 try testExec(gpa, "goodbye", "hello from exe\n");151 try testExec(gpa, io, "goodbye", "hello from exe\n");
152152
153 // If we use an absolute path to execute the invalid goodbye153 // If we use an absolute path to execute the invalid goodbye
154 const goodbye_abs_path = try std.mem.join(gpa, "\\", &.{ tmp_absolute_path, "goodbye" });154 const goodbye_abs_path = try std.mem.join(gpa, "\\", &.{ tmp_absolute_path, "goodbye" });
155 defer gpa.free(goodbye_abs_path);155 defer gpa.free(goodbye_abs_path);
156 // then the PATH should not be searched and we should get InvalidExe156 // then the PATH should not be searched and we should get InvalidExe
157 try testExecError(error.InvalidExe, gpa, goodbye_abs_path);157 try testExecError(error.InvalidExe, gpa, io, goodbye_abs_path);
158158
159 // If we try to exec but provide a cwd that is an absolute path, the PATH159 // If we try to exec but provide a cwd that is an absolute path, the PATH
160 // should still be searched and the goodbye.exe in something should be found.160 // should still be searched and the goodbye.exe in something should be found.
161 try testExecWithCwd(gpa, "goodbye", tmp_absolute_path, "hello from exe\n");161 try testExecWithCwd(gpa, io, "goodbye", tmp_absolute_path, "hello from exe\n");
162162
163 // introduce some extra path separators into the path which is dealt with inside the spawn call.163 // introduce some extra path separators into the path which is dealt with inside the spawn call.
164 const denormed_something_subdir_size = std.mem.replacementSize(u16, something_subdir_abs_path, utf16Literal("\\"), utf16Literal("\\\\\\\\"));164 const denormed_something_subdir_size = std.mem.replacementSize(u16, something_subdir_abs_path, utf16Literal("\\"), utf16Literal("\\\\\\\\"));
...@@ -177,20 +177,20 @@ pub fn main() anyerror!void {...@@ -177,20 +177,20 @@ pub fn main() anyerror!void {
177 null,177 null,
178 ) == windows.TRUE);178 ) == windows.TRUE);
179179
180 try testExecWithCwd(gpa, "goodbye", denormed_something_subdir_wtf8, "hello from exe\n");180 try testExecWithCwd(gpa, io, "goodbye", denormed_something_subdir_wtf8, "hello from exe\n");
181181
182 // normalization should also work if the non-normalized path is found in the PATH var.182 // normalization should also work if the non-normalized path is found in the PATH var.
183 std.debug.assert(windows.kernel32.SetEnvironmentVariableW(183 std.debug.assert(windows.kernel32.SetEnvironmentVariableW(
184 utf16Literal("PATH"),184 utf16Literal("PATH"),
185 denormed_something_subdir_abs_path,185 denormed_something_subdir_abs_path,
186 ) == windows.TRUE);186 ) == windows.TRUE);
187 try testExec(gpa, "goodbye", "hello from exe\n");187 try testExec(gpa, io, "goodbye", "hello from exe\n");
188188
189 // now make sure we can launch executables "outside" of the cwd189 // now make sure we can launch executables "outside" of the cwd
190 var subdir_cwd = try tmp.dir.openDir(denormed_something_subdir_wtf8, .{});190 var subdir_cwd = try tmp.dir.openDir(io, denormed_something_subdir_wtf8, .{});
191 defer subdir_cwd.close(io);191 defer subdir_cwd.close(io);
192192
193 try renameExe(tmp.dir, "something/goodbye.exe", "hello.exe");193 try renameExe(tmp.dir, io, "something/goodbye.exe", "hello.exe");
194 try std.process.setCurrentDir(io, subdir_cwd);194 try std.process.setCurrentDir(io, subdir_cwd);
195195
196 // clear the PATH again196 // clear the PATH again
...@@ -200,15 +200,15 @@ pub fn main() anyerror!void {...@@ -200,15 +200,15 @@ pub fn main() anyerror!void {
200 ) == windows.TRUE);200 ) == windows.TRUE);
201201
202 // while we're at it make sure non-windows separators work fine202 // while we're at it make sure non-windows separators work fine
203 try testExec(gpa, "../hello", "hello from exe\n");203 try testExec(gpa, io, "../hello", "hello from exe\n");
204}204}
205205
206fn testExecError(err: anyerror, gpa: Allocator, command: []const u8) !void {206fn testExecError(err: anyerror, gpa: Allocator, io: Io, command: []const u8) !void {
207 return std.testing.expectError(err, testExec(gpa, command, ""));207 return std.testing.expectError(err, testExec(gpa, io, command, ""));
208}208}
209209
210fn testExec(gpa: Allocator, command: []const u8, expected_stdout: []const u8) !void {210fn testExec(gpa: Allocator, io: Io, command: []const u8, expected_stdout: []const u8) !void {
211 return testExecWithCwd(gpa, command, null, expected_stdout);211 return testExecWithCwd(gpa, io, command, null, expected_stdout);
212}212}
213213
214fn testExecWithCwd(gpa: Allocator, io: Io, command: []const u8, cwd: ?[]const u8, expected_stdout: []const u8) !void {214fn testExecWithCwd(gpa: Allocator, io: Io, command: []const u8, cwd: ?[]const u8, expected_stdout: []const u8) !void {
...@@ -223,9 +223,9 @@ fn testExecWithCwd(gpa: Allocator, io: Io, command: []const u8, cwd: ?[]const u8...@@ -223,9 +223,9 @@ fn testExecWithCwd(gpa: Allocator, io: Io, command: []const u8, cwd: ?[]const u8
223 try std.testing.expectEqualStrings(expected_stdout, result.stdout);223 try std.testing.expectEqualStrings(expected_stdout, result.stdout);
224}224}
225225
226fn renameExe(dir: Io.Dir, old_sub_path: []const u8, new_sub_path: []const u8) !void {226fn renameExe(dir: Io.Dir, io: Io, old_sub_path: []const u8, new_sub_path: []const u8) !void {
227 var attempt: u5 = 0;227 var attempt: u5 = 0;
228 while (true) break dir.rename(old_sub_path, new_sub_path) catch |err| switch (err) {228 while (true) break dir.rename(old_sub_path, dir, new_sub_path, io) catch |err| switch (err) {
229 error.AccessDenied => {229 error.AccessDenied => {
230 if (attempt == 13) return error.AccessDenied;230 if (attempt == 13) return error.AccessDenied;
231 // give the kernel a chance to finish closing the executable handle231 // give the kernel a chance to finish closing the executable handle
...@@ -236,3 +236,41 @@ fn renameExe(dir: Io.Dir, old_sub_path: []const u8, new_sub_path: []const u8) !v...@@ -236,3 +236,41 @@ fn renameExe(dir: Io.Dir, old_sub_path: []const u8, new_sub_path: []const u8) !v
236 else => |e| return e,236 else => |e| return e,
237 };237 };
238}238}
239
240pub fn tmpDir(io: Io, opts: Io.Dir.OpenOptions) TmpDir {
241 var random_bytes: [TmpDir.random_bytes_count]u8 = undefined;
242 std.crypto.random.bytes(&random_bytes);
243 var sub_path: [TmpDir.sub_path_len]u8 = undefined;
244 _ = std.fs.base64_encoder.encode(&sub_path, &random_bytes);
245
246 const cwd = Io.Dir.cwd();
247 var cache_dir = cwd.createDirPathOpen(io, ".zig-cache", .{}) catch
248 @panic("unable to make tmp dir for testing: unable to make and open .zig-cache dir");
249 defer cache_dir.close(io);
250 const parent_dir = cache_dir.createDirPathOpen(io, "tmp", .{}) catch
251 @panic("unable to make tmp dir for testing: unable to make and open .zig-cache/tmp dir");
252 const dir = parent_dir.createDirPathOpen(io, &sub_path, .{ .open_options = opts }) catch
253 @panic("unable to make tmp dir for testing: unable to make and open the tmp dir");
254
255 return .{
256 .dir = dir,
257 .parent_dir = parent_dir,
258 .sub_path = sub_path,
259 };
260}
261
262pub const TmpDir = struct {
263 dir: Io.Dir,
264 parent_dir: Io.Dir,
265 sub_path: [sub_path_len]u8,
266
267 const random_bytes_count = 12;
268 const sub_path_len = std.fs.base64_encoder.calcSize(random_bytes_count);
269
270 pub fn cleanup(self: *TmpDir, io: Io) void {
271 self.dir.close(io);
272 self.parent_dir.deleteTree(io, &self.sub_path) catch {};
273 self.parent_dir.close(io);
274 self.* = undefined;
275 }
276};