mirror of
https://github.com/ghostty-org/ghostty.git
synced 2025-07-22 19:56:08 +03:00
Redo: Handle execveZ failures in Command.zig
tests (#3176)
https://github.com/ghostty-org/ghostty/pull/3130 do over. Github seems to have lost track of the fork status for the previous work. (perhaps permissions?). Rebased the PR to main as I can't see why it could possibly fail. <details> <summary>Previous description</summary> Bit of a rabbit hole came up while trying to package ghostty for nixos. zig build test would just hang when run as part of checkPhase in a nix build. (rather than the nix develop ... command run in CI). Here's what I understand so far: /usr/bin/env does not exist in a nix build sandbox. This only works as a test binary when running in nix develop as it shares its environment with the user where this compatibility crutch exists. When executing Command.zig tests that reference /usr/bin/env the eventual call to fork+execevZ will duplicate the running test process as execevZ returns rather than dissapearing into the new exec'd process. Duplicating a test process via fork does unexepected things. zig build test will hang for <reasons?>. A test binary created via -Demit-test-exe will run two copies of the test suite producing two different failure outputs for the same test. (or ~4 copies of the test framework, one extra for each test that fails this way) /bin/sh can be used and an alternative test target. It isn't amazing as it's relying on stdenv creating /bin/sh and it just existing on user systems. Other alternatives I can think of would require build flags or some sort of contract with packaging around what binary will exist for the Command.zig tests. </details>
This commit is contained in:
@ -587,8 +587,8 @@ test "createNullDelimitedEnvMap" {
|
|||||||
test "Command: pre exec" {
|
test "Command: pre exec" {
|
||||||
if (builtin.os.tag == .windows) return error.SkipZigTest;
|
if (builtin.os.tag == .windows) return error.SkipZigTest;
|
||||||
var cmd: Command = .{
|
var cmd: Command = .{
|
||||||
.path = "/usr/bin/env",
|
.path = "/bin/sh",
|
||||||
.args = &.{ "/usr/bin/env", "-v" },
|
.args = &.{ "/bin/sh", "-v" },
|
||||||
.pre_exec = (struct {
|
.pre_exec = (struct {
|
||||||
fn do(_: *Command) void {
|
fn do(_: *Command) void {
|
||||||
// This runs in the child, so we can exit and it won't
|
// This runs in the child, so we can exit and it won't
|
||||||
@ -598,7 +598,7 @@ test "Command: pre exec" {
|
|||||||
}).do,
|
}).do,
|
||||||
};
|
};
|
||||||
|
|
||||||
try cmd.start(testing.allocator);
|
try cmd.testingStart();
|
||||||
try testing.expect(cmd.pid != null);
|
try testing.expect(cmd.pid != null);
|
||||||
const exit = try cmd.wait(true);
|
const exit = try cmd.wait(true);
|
||||||
try testing.expect(exit == .Exited);
|
try testing.expect(exit == .Exited);
|
||||||
@ -629,12 +629,12 @@ test "Command: redirect stdout to file" {
|
|||||||
.args = &.{"C:\\Windows\\System32\\whoami.exe"},
|
.args = &.{"C:\\Windows\\System32\\whoami.exe"},
|
||||||
.stdout = stdout,
|
.stdout = stdout,
|
||||||
} else .{
|
} else .{
|
||||||
.path = "/usr/bin/env",
|
.path = "/bin/sh",
|
||||||
.args = &.{ "/usr/bin/env", "-v" },
|
.args = &.{ "/bin/sh", "-c", "echo hello" },
|
||||||
.stdout = stdout,
|
.stdout = stdout,
|
||||||
};
|
};
|
||||||
|
|
||||||
try cmd.start(testing.allocator);
|
try cmd.testingStart();
|
||||||
try testing.expect(cmd.pid != null);
|
try testing.expect(cmd.pid != null);
|
||||||
const exit = try cmd.wait(true);
|
const exit = try cmd.wait(true);
|
||||||
try testing.expect(exit == .Exited);
|
try testing.expect(exit == .Exited);
|
||||||
@ -663,13 +663,13 @@ test "Command: custom env vars" {
|
|||||||
.stdout = stdout,
|
.stdout = stdout,
|
||||||
.env = &env,
|
.env = &env,
|
||||||
} else .{
|
} else .{
|
||||||
.path = "/usr/bin/env",
|
.path = "/bin/sh",
|
||||||
.args = &.{ "/usr/bin/env", "sh", "-c", "echo $VALUE" },
|
.args = &.{ "/bin/sh", "-c", "echo $VALUE" },
|
||||||
.stdout = stdout,
|
.stdout = stdout,
|
||||||
.env = &env,
|
.env = &env,
|
||||||
};
|
};
|
||||||
|
|
||||||
try cmd.start(testing.allocator);
|
try cmd.testingStart();
|
||||||
try testing.expect(cmd.pid != null);
|
try testing.expect(cmd.pid != null);
|
||||||
const exit = try cmd.wait(true);
|
const exit = try cmd.wait(true);
|
||||||
try testing.expect(exit == .Exited);
|
try testing.expect(exit == .Exited);
|
||||||
@ -699,13 +699,13 @@ test "Command: custom working directory" {
|
|||||||
.stdout = stdout,
|
.stdout = stdout,
|
||||||
.cwd = "C:\\Windows\\System32",
|
.cwd = "C:\\Windows\\System32",
|
||||||
} else .{
|
} else .{
|
||||||
.path = "/usr/bin/env",
|
.path = "/bin/sh",
|
||||||
.args = &.{ "/usr/bin/env", "sh", "-c", "pwd" },
|
.args = &.{ "/bin/sh", "-c", "pwd" },
|
||||||
.stdout = stdout,
|
.stdout = stdout,
|
||||||
.cwd = "/usr/bin",
|
.cwd = "/tmp",
|
||||||
};
|
};
|
||||||
|
|
||||||
try cmd.start(testing.allocator);
|
try cmd.testingStart();
|
||||||
try testing.expect(cmd.pid != null);
|
try testing.expect(cmd.pid != null);
|
||||||
const exit = try cmd.wait(true);
|
const exit = try cmd.wait(true);
|
||||||
try testing.expect(exit == .Exited);
|
try testing.expect(exit == .Exited);
|
||||||
@ -718,7 +718,51 @@ test "Command: custom working directory" {
|
|||||||
|
|
||||||
if (builtin.os.tag == .windows) {
|
if (builtin.os.tag == .windows) {
|
||||||
try testing.expectEqualStrings("C:\\Windows\\System32\r\n", contents);
|
try testing.expectEqualStrings("C:\\Windows\\System32\r\n", contents);
|
||||||
|
} else if (builtin.os.tag == .macos) {
|
||||||
|
try testing.expectEqualStrings("/private/tmp\n", contents);
|
||||||
} else {
|
} else {
|
||||||
try testing.expectEqualStrings("/usr/bin\n", contents);
|
try testing.expectEqualStrings("/tmp\n", contents);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test validate an execveZ failure correctly terminates when error.ExecFailedInChild is correctly handled
|
||||||
|
//
|
||||||
|
// Incorrectly handling an error.ExecFailedInChild results in a second copy of the test process running.
|
||||||
|
// Duplicating the test process leads to weird behavior
|
||||||
|
// zig build test will hang
|
||||||
|
// test binary created via -Demit-test-exe will run 2 copies of the test suite
|
||||||
|
test "Command: posix fork handles execveZ failure" {
|
||||||
|
if (builtin.os.tag == .windows) {
|
||||||
|
return error.SkipZigTest;
|
||||||
|
}
|
||||||
|
var td = try TempDir.init();
|
||||||
|
defer td.deinit();
|
||||||
|
var stdout = try createTestStdout(td.dir);
|
||||||
|
defer stdout.close();
|
||||||
|
|
||||||
|
var cmd: Command = .{
|
||||||
|
.path = "/not/a/binary",
|
||||||
|
.args = &.{ "/not/a/binary", "" },
|
||||||
|
.stdout = stdout,
|
||||||
|
.cwd = "/bin",
|
||||||
|
};
|
||||||
|
|
||||||
|
try cmd.testingStart();
|
||||||
|
try testing.expect(cmd.pid != null);
|
||||||
|
const exit = try cmd.wait(true);
|
||||||
|
try testing.expect(exit == .Exited);
|
||||||
|
try testing.expect(exit.Exited == 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
// If cmd.start fails with error.ExecFailedInChild it's the _child_ process that is running. If it does not
|
||||||
|
// terminate in response to that error both the parent and child will continue as if they _are_ the test suite
|
||||||
|
// process.
|
||||||
|
fn testingStart(self: *Command) !void {
|
||||||
|
self.start(testing.allocator) catch |err| {
|
||||||
|
if (err == error.ExecFailedInChild) {
|
||||||
|
// I am a child process, I must not get confused and continue running the rest of the test suite.
|
||||||
|
posix.exit(1);
|
||||||
|
}
|
||||||
|
return err;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
Reference in New Issue
Block a user