From 476de6fac728df58e0ae4446247996884a69f415 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:13:57 +0000 Subject: [PATCH 1/3] test: skip test-tonic when the rustup shim has no toolchain, fail fast on cargo errors The darwin-aarch64-15.1 agent was reconfigured on Jul 9 to run as a user with /opt/rust/bin in PATH but no RUSTUP_HOME, so Bun.which('cargo') finds the rustup proxy while 'cargo run' in the throwaway tmpDir (which has no rust-toolchain.toml and thus needs a rustup default) exits immediately with: error: rustup could not choose a version of cargo to run, because one wasn't specified explicitly, and no default is configured. startServer() then broke out of its stdout read loop on the 'done' branch and awaited a never-settled promise until the 150s hook timeout, after which afterAll threw on 'server.kill' with server still undefined. Probe 'cargo --version' with the same stripped env and an outside-the- repo cwd and feed that into describe.skipIf, rewrite the read loop to accumulate stdout and throw with the captured stderr when it closes without a 'Listening on' line, drain stderr concurrently so a chatty compile cannot wedge the pipe, and null-guard the afterAll kill. --- .../js/third_party/grpc-js/test-tonic.test.ts | 61 ++++++++++++------- 1 file changed, 40 insertions(+), 21 deletions(-) diff --git a/test/js/third_party/grpc-js/test-tonic.test.ts b/test/js/third_party/grpc-js/test-tonic.test.ts index bc8f75c97509..122eacf66804 100644 --- a/test/js/third_party/grpc-js/test-tonic.test.ts +++ b/test/js/third_party/grpc-js/test-tonic.test.ts @@ -35,7 +35,27 @@ const packageDefinition = protoLoader.loadSync(join(import.meta.dir, "fixtures/t type Server = { address: string; kill: () => Promise }; -const cargoBin = Bun.which("cargo") as string; +const cargoBin = Bun.which("cargo"); +// `Bun.which` finds the rustup shim whenever /opt/rust/bin (or ~/.cargo/bin) is +// in PATH, but the shim still fails when the agent user has no default +// toolchain (macOS CI runs the agent with PATH set and RUSTUP_HOME unset on +// some boxes). Probe with the env and outside-the-repo cwd that `cargo run` +// below will see so we skip instead of timing out for 150s. +const cargoEnv = { + PATH: process.env.PATH, + CARGO_HOME: process.env.CARGO_HOME, + RUSTUP_HOME: process.env.RUSTUP_HOME, + RUSTUP_TOOLCHAIN: process.env.RUSTUP_TOOLCHAIN, +}; +const cargoWorks = + !!cargoBin && + Bun.spawnSync({ + cmd: [cargoBin, "--version"], + env: cargoEnv, + cwd: tmpdir(), + stdout: "ignore", + stderr: "ignore", + }).exitCode === 0; // Stable per-machine cache so persistent CI agents don't re-download protoc and // re-compile the entire tonic/tokio dependency tree (~50s) on every run. @@ -67,24 +87,24 @@ async function startServer(): Promise { const protocExec = join(protocPath, binPath); await chmod(protocExec, 0o755); - const server = Bun.spawn([cargoBin, "run", "--quiet", path.join(tmpDir, "server")], { + const server = Bun.spawn([cargoBin!, "run", "--quiet", path.join(tmpDir, "server")], { cwd: tmpDir, env: { + ...cargoEnv, PROTOC: protocExec, - PATH: process.env.PATH, - CARGO_HOME: process.env.CARGO_HOME, - RUSTUP_HOME: process.env.RUSTUP_HOME, // Keep cargo's target dir outside the throwaway tmpDir so registry deps // (tonic, tokio, prost, ...) compile once per machine instead of once per run. CARGO_TARGET_DIR: join(cacheDir, "target"), }, stdout: "pipe", stdin: "ignore", - stderr: "inherit", + stderr: "pipe", }); { - const { promise, reject, resolve } = Promise.withResolvers(); + // Drain stderr immediately so a chatty compile can't fill the pipe buffer + // and wedge the child before it gets to print "Listening on". + const stderrPromise = server.stderr.text(); const reader = server.stdout.getReader(); const decoder = new TextDecoder(); async function killServer() { @@ -94,30 +114,29 @@ async function startServer(): Promise { rmSync(tmpDir, { recursive: true, force: true }); } catch {} } + let text = ""; while (true) { const { done, value } = await reader.read(); - if (done) { - break; - } - const text = decoder.decode(value); + if (value) text += decoder.decode(value, { stream: true }); if (text.includes("Listening on")) { const [_, address] = text.split("Listening on "); - resolve({ + return { address: address?.trim(), kill: killServer, - }); - break; - } else { - await killServer(); - reject(new Error("Server not started")); - break; + }; } + if (done) break; } - return await promise; + // stdout closed without a "Listening on" line: cargo/rustup failed or the + // build errored. Surface stderr so the failure is diagnosable instead of + // awaiting a never-settled promise until the hook times out. + const [stderr, exitCode] = await Promise.all([stderrPromise, server.exited]); + await killServer(); + throw new Error(`tonic server exited (${exitCode}) before reporting an address:\n${stderr || text}`); } } -describe.skipIf(!cargoBin || !releases[release])("test tonic server", () => { +describe.skipIf(!cargoWorks || !releases[release])("test tonic server", () => { let server: Server; beforeAll(async () => { @@ -125,7 +144,7 @@ describe.skipIf(!cargoBin || !releases[release])("test tonic server", () => { }); afterAll(() => { - server.kill(); + server?.kill(); }); test("flow control should work in both directions", async () => { From 8e28edc496d200be9487240bef614a16057e3195 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Sat, 11 Jul 2026 09:19:42 +0000 Subject: [PATCH 2/3] review: wait for the full readiness line, await teardown, trim comment Parse the address only after the terminating newline so a chunk boundary inside 'Listening on ' cannot yield a partial address, make the afterAll hook await killServer(), and condense the cargo probe comment to three lines. --- .../js/third_party/grpc-js/test-tonic.test.ts | 20 +++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/test/js/third_party/grpc-js/test-tonic.test.ts b/test/js/third_party/grpc-js/test-tonic.test.ts index 122eacf66804..639726c3bec0 100644 --- a/test/js/third_party/grpc-js/test-tonic.test.ts +++ b/test/js/third_party/grpc-js/test-tonic.test.ts @@ -36,11 +36,9 @@ const packageDefinition = protoLoader.loadSync(join(import.meta.dir, "fixtures/t type Server = { address: string; kill: () => Promise }; const cargoBin = Bun.which("cargo"); -// `Bun.which` finds the rustup shim whenever /opt/rust/bin (or ~/.cargo/bin) is -// in PATH, but the shim still fails when the agent user has no default -// toolchain (macOS CI runs the agent with PATH set and RUSTUP_HOME unset on -// some boxes). Probe with the env and outside-the-repo cwd that `cargo run` -// below will see so we skip instead of timing out for 150s. +// `Bun.which` can find a rustup shim that has no usable default toolchain. +// Probe with the env and outside-the-repo cwd `cargo run` will see below so +// this suite skips instead of timing out for 150s on such agents. const cargoEnv = { PATH: process.env.PATH, CARGO_HOME: process.env.CARGO_HOME, @@ -114,14 +112,16 @@ async function startServer(): Promise { rmSync(tmpDir, { recursive: true, force: true }); } catch {} } + const marker = "Listening on "; let text = ""; while (true) { const { done, value } = await reader.read(); if (value) text += decoder.decode(value, { stream: true }); - if (text.includes("Listening on")) { - const [_, address] = text.split("Listening on "); + const markerIndex = text.indexOf(marker); + const lineEnd = markerIndex < 0 ? -1 : text.indexOf("\n", markerIndex + marker.length); + if (lineEnd >= 0) { return { - address: address?.trim(), + address: text.slice(markerIndex + marker.length, lineEnd).trim(), kill: killServer, }; } @@ -143,8 +143,8 @@ describe.skipIf(!cargoWorks || !releases[release])("test tonic server", () => { server = await startServer(); }); - afterAll(() => { - server?.kill(); + afterAll(async () => { + await server?.kill(); }); test("flow control should work in both directions", async () => { From 9b00b0637c8cd463b9ab8a3491425d72e2753574 Mon Sep 17 00:00:00 2001 From: robobun <117481402+robobun@users.noreply.github.com> Date: Wed, 15 Jul 2026 17:52:30 +0000 Subject: [PATCH 3/3] drop the cargoWorks probe; keep the original skipIf(!cargoBin) A cargo shim with no usable toolchain should fail loudly (now with the captured rustup stderr and exit code in seconds) rather than skip quietly, so a misconfigured agent is visible in CI. The .profile on darwin-test-arm64-1 has been fixed out of band so cargo works there again. --- .../js/third_party/grpc-js/test-tonic.test.ts | 29 +++++-------------- 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/test/js/third_party/grpc-js/test-tonic.test.ts b/test/js/third_party/grpc-js/test-tonic.test.ts index 639726c3bec0..b7c71bf8faa7 100644 --- a/test/js/third_party/grpc-js/test-tonic.test.ts +++ b/test/js/third_party/grpc-js/test-tonic.test.ts @@ -35,25 +35,7 @@ const packageDefinition = protoLoader.loadSync(join(import.meta.dir, "fixtures/t type Server = { address: string; kill: () => Promise }; -const cargoBin = Bun.which("cargo"); -// `Bun.which` can find a rustup shim that has no usable default toolchain. -// Probe with the env and outside-the-repo cwd `cargo run` will see below so -// this suite skips instead of timing out for 150s on such agents. -const cargoEnv = { - PATH: process.env.PATH, - CARGO_HOME: process.env.CARGO_HOME, - RUSTUP_HOME: process.env.RUSTUP_HOME, - RUSTUP_TOOLCHAIN: process.env.RUSTUP_TOOLCHAIN, -}; -const cargoWorks = - !!cargoBin && - Bun.spawnSync({ - cmd: [cargoBin, "--version"], - env: cargoEnv, - cwd: tmpdir(), - stdout: "ignore", - stderr: "ignore", - }).exitCode === 0; +const cargoBin = Bun.which("cargo") as string; // Stable per-machine cache so persistent CI agents don't re-download protoc and // re-compile the entire tonic/tokio dependency tree (~50s) on every run. @@ -85,11 +67,14 @@ async function startServer(): Promise { const protocExec = join(protocPath, binPath); await chmod(protocExec, 0o755); - const server = Bun.spawn([cargoBin!, "run", "--quiet", path.join(tmpDir, "server")], { + const server = Bun.spawn([cargoBin, "run", "--quiet", path.join(tmpDir, "server")], { cwd: tmpDir, env: { - ...cargoEnv, PROTOC: protocExec, + PATH: process.env.PATH, + CARGO_HOME: process.env.CARGO_HOME, + RUSTUP_HOME: process.env.RUSTUP_HOME, + RUSTUP_TOOLCHAIN: process.env.RUSTUP_TOOLCHAIN, // Keep cargo's target dir outside the throwaway tmpDir so registry deps // (tonic, tokio, prost, ...) compile once per machine instead of once per run. CARGO_TARGET_DIR: join(cacheDir, "target"), @@ -136,7 +121,7 @@ async function startServer(): Promise { } } -describe.skipIf(!cargoWorks || !releases[release])("test tonic server", () => { +describe.skipIf(!cargoBin || !releases[release])("test tonic server", () => { let server: Server; beforeAll(async () => {