authorgravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2022-12-18 02:31:00-08:00
committergravatar for squeek502@hotmail.comRyan Liptak <squeek502@hotmail.com> 2022-12-18 03:55:28-08:00
log0cbc59f2274663b9d956170fff3bf7f477b2b3cf
treec8a63d9bfd0eeecaef1b53831df50e97eddcfead
parente9c48e663145910d4934c3d1d4249da20682ba90

standalone tests: Add windows spawn test

Tests a decent amount of edge cases dealing with how PATH and PATHEXT searching is handled.

5 files changed, 189 insertions(+), 0 deletions(-)

lib/std/os/windows/kernel32.zig+2
......@@ -177,6 +177,8 @@ pub extern "kernel32" fn GetEnvironmentStringsW() callconv(WINAPI) ?[*:0]u16;
177177
178178pub extern "kernel32" fn GetEnvironmentVariableW(lpName: LPWSTR, lpBuffer: [*]u16, nSize: DWORD) callconv(WINAPI) DWORD;
179179
180pub extern "kernel32" fn SetEnvironmentVariableW(lpName: LPCWSTR, lpValue: ?LPCWSTR) callconv(WINAPI) BOOL;
181
180182pub extern "kernel32" fn GetExitCodeProcess(hProcess: HANDLE, lpExitCode: *DWORD) callconv(WINAPI) BOOL;
181183
182184pub extern "kernel32" fn GetFileSizeEx(hFile: HANDLE, lpFileSize: *LARGE_INTEGER) callconv(WINAPI) BOOL;
test/standalone.zig+4
......@@ -63,6 +63,10 @@ pub fn addCases(cases: *tests.StandaloneContext) void {
6363 cases.addBuildFile("test/standalone/load_dynamic_library/build.zig", .{});
6464 }
6565
66 if (builtin.os.tag == .windows) {
67 cases.addBuildFile("test/standalone/windows_spawn/build.zig", .{});
68 }
69
6670 cases.addBuildFile("test/standalone/c_compiler/build.zig", .{
6771 .build_modes = true,
6872 .cross_targets = true,
test/standalone/windows_spawn/build.zig created+16
......@@ -0,0 +1,16 @@
1const Builder = @import("std").build.Builder;
2
3pub fn build(b: *Builder) void {
4 const mode = b.standardReleaseOptions();
5
6 const hello = b.addExecutable("hello", "hello.zig");
7 hello.setBuildMode(mode);
8
9 const main = b.addExecutable("main", "main.zig");
10 main.setBuildMode(mode);
11 const run = main.run();
12 run.addArtifactArg(hello);
13
14 const test_step = b.step("test", "Test it");
15 test_step.dependOn(&run.step);
16}
test/standalone/windows_spawn/hello.zig created+6
......@@ -0,0 +1,6 @@
1const std = @import("std");
2
3pub fn main() !void {
4 const stdout = std.io.getStdOut().writer();
5 try stdout.writeAll("hello from exe\n");
6}
test/standalone/windows_spawn/main.zig created+161
......@@ -0,0 +1,161 @@
1const std = @import("std");
2const windows = std.os.windows;
3const utf16Literal = std.unicode.utf8ToUtf16LeStringLiteral;
4
5pub fn main() anyerror!void {
6 var gpa = std.heap.GeneralPurposeAllocator(.{}){};
7 defer if (gpa.deinit()) @panic("found memory leaks");
8 const allocator = gpa.allocator();
9
10 var it = try std.process.argsWithAllocator(allocator);
11 defer it.deinit();
12 _ = it.next() orelse unreachable; // skip binary name
13 const hello_exe_cache_path = it.next() orelse unreachable;
14
15 var tmp = std.testing.tmpDir(.{});
16 defer tmp.cleanup();
17
18 const tmp_absolute_path = try tmp.dir.realpathAlloc(allocator, ".");
19 defer allocator.free(tmp_absolute_path);
20 const tmp_absolute_path_w = try std.unicode.utf8ToUtf16LeWithNull(allocator, tmp_absolute_path);
21 defer allocator.free(tmp_absolute_path_w);
22 const cwd_absolute_path = try std.fs.cwd().realpathAlloc(allocator, ".");
23 defer allocator.free(cwd_absolute_path);
24 const tmp_relative_path = try std.fs.path.relative(allocator, cwd_absolute_path, tmp_absolute_path);
25 defer allocator.free(tmp_relative_path);
26
27 // Clear PATH
28 std.debug.assert(std.os.windows.kernel32.SetEnvironmentVariableW(
29 utf16Literal("PATH"),
30 null,
31 ) == windows.TRUE);
32
33 // Set PATHEXT to something predictable
34 std.debug.assert(std.os.windows.kernel32.SetEnvironmentVariableW(
35 utf16Literal("PATHEXT"),
36 utf16Literal(".COM;.EXE;.BAT;.CMD;.JS"),
37 ) == windows.TRUE);
38
39 // No PATH, so it should fail to find anything not in the cwd
40 try testExecError(error.FileNotFound, allocator, "something_missing");
41
42 std.debug.assert(std.os.windows.kernel32.SetEnvironmentVariableW(
43 utf16Literal("PATH"),
44 tmp_absolute_path_w,
45 ) == windows.TRUE);
46
47 // Move hello.exe into the tmp dir which is now added to the path
48 try std.fs.cwd().copyFile(hello_exe_cache_path, tmp.dir, "hello.exe", .{});
49
50 // with extension should find the .exe (case insensitive)
51 try testExec(allocator, "HeLLo.exe", "hello from exe\n");
52 // without extension should find the .exe (case insensitive)
53 try testExec(allocator, "heLLo", "hello from exe\n");
54
55 // now add a .bat
56 try tmp.dir.writeFile("hello.bat", "@echo hello from bat");
57 // and a .cmd
58 try tmp.dir.writeFile("hello.cmd", "@echo hello from cmd");
59
60 // with extension should find the .bat (case insensitive)
61 try testExec(allocator, "heLLo.bat", "hello from bat\r\n");
62 // with extension should find the .cmd (case insensitive)
63 try testExec(allocator, "heLLo.cmd", "hello from cmd\r\n");
64 // without extension should find the .exe (since its first in PATHEXT)
65 try testExec(allocator, "heLLo", "hello from exe\n");
66
67 // now rename the exe to not have an extension
68 try tmp.dir.rename("hello.exe", "hello");
69
70 // with extension should now fail
71 try testExecError(error.FileNotFound, allocator, "hello.exe");
72 // without extension should succeed (case insensitive)
73 try testExec(allocator, "heLLo", "hello from exe\n");
74
75 try tmp.dir.makeDir("something");
76 try tmp.dir.rename("hello", "something/hello.exe");
77
78 const relative_path_no_ext = try std.fs.path.join(allocator, &.{ tmp_relative_path, "something/hello" });
79 defer allocator.free(relative_path_no_ext);
80
81 // Giving a full relative path to something/hello should work
82 try testExec(allocator, relative_path_no_ext, "hello from exe\n");
83 // But commands with path separators get excluded from PATH searching, so this will fail
84 try testExecError(error.FileNotFound, allocator, "something/hello");
85
86 // Now that .BAT is the first PATHEXT that should be found, this should succeed
87 try testExec(allocator, "heLLo", "hello from bat\r\n");
88
89 // Add a hello.exe that is not a valid executable
90 try tmp.dir.writeFile("hello.exe", "invalid");
91
92 // Trying to execute it with extension will give InvalidExe. This is a special
93 // case for .EXE extensions, where if they ever try to get executed but they are
94 // invalid, that gets treated as a fatal error wherever they are found and InvalidExe
95 // is returned immediately.
96 try testExecError(error.InvalidExe, allocator, "hello.exe");
97 // Same thing applies to the command with no extension--even though there is a
98 // hello.bat that could be executed, it should stop after it tries executing
99 // hello.exe and getting InvalidExe.
100 try testExecError(error.InvalidExe, allocator, "hello");
101
102 // If we now rename hello.exe to have no extension, it will behave differently
103 try tmp.dir.rename("hello.exe", "hello");
104
105 // Now, trying to execute it without an extension should treat InvalidExe as recoverable
106 // and skip over it and find hello.bat and execute that
107 try testExec(allocator, "hello", "hello from bat\r\n");
108
109 // If we rename the invalid exe to something else
110 try tmp.dir.rename("hello", "goodbye");
111 // Then we should now get FileNotFound when trying to execute 'goodbye',
112 // since that is what the original error will be after searching for 'goodbye'
113 // in the cwd. It will try to execute 'goodbye' from the PATH but the InvalidExe error
114 // should be ignored in this case.
115 try testExecError(error.FileNotFound, allocator, "goodbye");
116
117 // Now let's set the tmp dir as the cwd and set the path only include the "something" sub dir
118 try tmp.dir.setAsCwd();
119 const something_subdir_abs_path = try std.mem.concatWithSentinel(allocator, u16, &.{ tmp_absolute_path_w, utf16Literal("\\something") }, 0);
120 defer allocator.free(something_subdir_abs_path);
121
122 std.debug.assert(std.os.windows.kernel32.SetEnvironmentVariableW(
123 utf16Literal("PATH"),
124 something_subdir_abs_path,
125 ) == windows.TRUE);
126
127 // Now trying to execute goodbye should give error.InvalidExe since it's the original
128 // error that we got when trying within the cwd
129 try testExecError(error.InvalidExe, allocator, "goodbye");
130
131 // hello should still find the .bat
132 try testExec(allocator, "hello", "hello from bat\r\n");
133
134 // If we rename something/hello.exe to something/goodbye.exe
135 try tmp.dir.rename("something/hello.exe", "something/goodbye.exe");
136 // And try to execute goodbye, then the one in something should be found
137 // since the one in cwd is an invalid executable
138 try testExec(allocator, "goodbye", "hello from exe\n");
139
140 // If we use an absolute path to execute the invalid goodbye
141 const goodbye_abs_path = try std.mem.join(allocator, "\\", &.{ tmp_absolute_path, "goodbye" });
142 defer allocator.free(goodbye_abs_path);
143 // then the PATH should not be searched and we should get InvalidExe
144 try testExecError(error.InvalidExe, allocator, goodbye_abs_path);
145}
146
147fn testExecError(err: anyerror, allocator: std.mem.Allocator, command: []const u8) !void {
148 return std.testing.expectError(err, testExec(allocator, command, ""));
149}
150
151fn testExec(allocator: std.mem.Allocator, command: []const u8, expected_stdout: []const u8) !void {
152 var result = try std.ChildProcess.exec(.{
153 .allocator = allocator,
154 .argv = &[_][]const u8{command},
155 });
156 defer allocator.free(result.stdout);
157 defer allocator.free(result.stderr);
158
159 try std.testing.expectEqualStrings("", result.stderr);
160 try std.testing.expectEqualStrings(expected_stdout, result.stdout);
161}