commit cf6325b779deb9dad1192b12eab9f0fc342cf2bb
parent 6b59a8ec82cd7754605b71a5aefe72ba069bb724
Author: triesap <tyson@radroots.org>
Date: Tue, 22 Sep 2026 22:08:56 +0000
test(hyf): close C002C pipe-rollback and accounting review findings
- Create the stdio runner pipes through a rollback constructor so a later
pipe failure closes the earlier descriptors, with an injected-failure
control proving the partial-pipe path
- Add executed controls for cleanup-failure observability, wrong request and
connection accounting, and a non-timeout completion-probe error treated as
failure rather than success
- Close the descriptor planted by the census control and assert the count
returns to its baseline
Diffstat:
3 files changed, 130 insertions(+), 13 deletions(-)
diff --git a/tests/parent_lifecycle.mojo b/tests/parent_lifecycle.mojo
@@ -68,10 +68,48 @@ def now_ms() -> Int:
@fieldwise_init
-struct PipeFds(Movable):
+struct PipeFds(Copyable, Movable):
var read_fd: Int
var write_fd: Int
+ def __copyinit__(out self, existing: Self):
+ self.read_fd = existing.read_fd
+ self.write_fd = existing.write_fd
+
+
+@fieldwise_init
+struct PipeTriple(Movable):
+ var stdin_pipe: PipeFds
+ var stdout_pipe: PipeFds
+ var stderr_pipe: PipeFds
+
+
+def close_pipe(var pipe: PipeFds):
+ close_fd(pipe.read_fd)
+ close_fd(pipe.write_fd)
+
+
+def make_three_pipes(inject_fail_after: Int = -1) raises -> PipeTriple:
+ """Create three owned pipes, closing earlier ones if a later one fails.
+
+ ``inject_fail_after`` is a test-only control: when >= 0 the constructor
+ raises after that many successful pipes, proving the rollback path.
+ """
+ var fds = InlineArray[Int, 6](fill=-1)
+ for index in range(3):
+ if inject_fail_after >= 0 and index == inject_fail_after:
+ for slot in range(6):
+ close_fd(fds[slot])
+ raise Error("lifecycle: injected pipe creation failure")
+ var pipe = make_pipe()
+ fds[index * 2] = pipe.read_fd
+ fds[index * 2 + 1] = pipe.write_fd
+ return PipeTriple(
+ PipeFds(fds[0], fds[1]),
+ PipeFds(fds[2], fds[3]),
+ PipeFds(fds[4], fds[5]),
+ )
+
def ignore_sigpipe():
"""Ignore SIGPIPE so a peer-close race surfaces as EPIPE, not parent death.
diff --git a/tests/stdio_process_helper.mojo b/tests/stdio_process_helper.mojo
@@ -27,7 +27,7 @@ from parent_lifecycle import (
close_fd,
dup2_fd,
fork_pid,
- make_pipe,
+ make_three_pipes,
now_ms,
poll_three,
read_fd,
@@ -152,9 +152,7 @@ def run_stdio_entrypoint_with_deadline(
arg1: String,
deadline_ms: Int,
) raises -> Value:
- var stdin_pipe = make_pipe()
- var stdout_pipe = make_pipe()
- var stderr_pipe = make_pipe()
+ var pipes = make_three_pipes()
var command = String("mojo")
var include_flag = String("-I")
@@ -183,12 +181,12 @@ def run_stdio_entrypoint_with_deadline(
process_arg1.as_c_string_slice()
)
- var stdin_read_fd = stdin_pipe.read_fd
- var stdin_write_fd = stdin_pipe.write_fd
- var stdout_read_fd = stdout_pipe.read_fd
- var stdout_write_fd = stdout_pipe.write_fd
- var stderr_read_fd = stderr_pipe.read_fd
- var stderr_write_fd = stderr_pipe.write_fd
+ var stdin_read_fd = pipes.stdin_pipe.read_fd
+ var stdin_write_fd = pipes.stdin_pipe.write_fd
+ var stdout_read_fd = pipes.stdout_pipe.read_fd
+ var stdout_write_fd = pipes.stdout_pipe.write_fd
+ var stderr_read_fd = pipes.stderr_pipe.read_fd
+ var stderr_write_fd = pipes.stderr_pipe.write_fd
var command_ptr = command.as_c_string_slice().unsafe_ptr()
var argv_ptr = argv.unsafe_ptr()
diff --git a/tests/test_provider_helpers.mojo b/tests/test_provider_helpers.mojo
@@ -9,6 +9,7 @@ from std.testing import TestSuite, assert_true, assert_equal
from std.ffi import ErrNo, c_int, external_call
from flare.net import SocketAddr
+from flare.net.socket import RawSocket
from flare.tcp import TcpListener, TcpStream
from parent_lifecycle import (
@@ -21,7 +22,9 @@ from parent_lifecycle import (
dup2_fd,
fork_pid,
make_pipe,
+ make_three_pipes,
open_fd_count,
+ open_fd_count_checked,
parse_ready_line,
parse_ready_or_cleanup,
pid_not_waitable,
@@ -33,6 +36,7 @@ from parent_lifecycle import (
write_raw,
)
from strict_fixture import (
+ ConnectionReader,
ExchangeScript,
FramedRequest,
authorization_reason,
@@ -1243,9 +1247,86 @@ def test_descriptor_census_detects_planted_high_fd() raises:
assert_true(planted >= 0)
assert_true(with_pipe > before)
close_fd(planted)
+ close_fd(pipe.read_fd)
close_fd(pipe.write_fd)
- var after = open_fd_count()
- assert_true(after <= with_pipe)
+ assert_equal(open_fd_count(), before)
+
+
+def test_partial_pipe_failure_rolls_back() raises:
+ # LC01: a later pipe failure must close the pipes already created.
+ var before = open_fd_count_checked()
+ var message = ""
+ try:
+ _ = make_three_pipes(1)
+ except e:
+ message = String(e)
+ assert_true(message.find("injected pipe creation failure") >= 0)
+ assert_equal(open_fd_count_checked(), before)
+
+
+def test_cleanup_failure_is_observable() raises:
+ # LC01/D36: cleanup failure must be observable, never silently swallowed.
+ var state = PipedChildState(
+ pid=0,
+ report_fd=-1,
+ pending="",
+ eof=False,
+ closed=False,
+ deadline_ms=100,
+ expected_requests=1,
+ reaped=False,
+ ok=False,
+ phase="pending",
+ case_label="-",
+ reason="not_reaped",
+ requests=0,
+ connections=0,
+ cleanup_error="",
+ status=ProcessStatus("pending", False, -1, 0, 0, ""),
+ observed=ProcessStatus("pending", False, -1, 0, 0, ""),
+ observed_valid=False,
+ )
+ var stub = SpawnedMaxLocalStub(0, 0, state^)
+ stub.cleanup()
+ assert_true(stub.cleanup_error().find("unreaped") >= 0)
+ assert_true(not stub.status().cleanup_proved())
+
+
+def test_result_truth_rejects_wrong_request_count() raises:
+ var stub = _owned_report_child(
+ 0,
+ "result ok phase=complete case=- reason=ok requests=2 connections=1\n",
+ )
+ stub.reap()
+ assert_true(not stub.ok())
+ assert_equal(stub.reason(), "request_count_mismatch")
+
+
+def test_result_truth_rejects_invalid_connection_count() raises:
+ var stub = _owned_report_child(
+ 0,
+ "result ok phase=complete case=- reason=ok requests=1 connections=5\n",
+ )
+ stub.reap()
+ assert_true(not stub.ok())
+ assert_equal(stub.reason(), "connection_count_invalid")
+
+
+def test_completion_probe_error_is_not_success() raises:
+ # LC05: a non-timeout completion-probe I/O/setup error must not be read as
+ # a successful completion.
+ var pipe = make_pipe()
+ var sock = RawSocket(c_int(pipe.read_fd), c_int(2), c_int(1), True)
+ var stream = TcpStream(sock^, SocketAddr.localhost(UInt16(1)))
+ var reader = ConnectionReader(stream^)
+ var raised = False
+ try:
+ _ = reader.probe_completion(20)
+ except e:
+ raised = True
+ _ = String(e)
+ close_fd(pipe.write_fd)
+ assert_true(raised)
def test_descriptor_census_detects_planted_socket() raises: