diff --git a/scripts/runner.node.mjs b/scripts/runner.node.mjs index d54ae66faade..b45bf04e27da 100755 --- a/scripts/runner.node.mjs +++ b/scripts/runner.node.mjs @@ -28,7 +28,7 @@ import { writeFileSync, } from "node:fs"; import { readFile } from "node:fs/promises"; -import { availableParallelism, userInfo } from "node:os"; +import { availableParallelism } from "node:os"; import { basename, dirname, extname, join, relative, sep } from "node:path"; import { createInterface } from "node:readline"; import { setTimeout as setTimeoutPromise } from "node:timers/promises"; @@ -54,6 +54,7 @@ import { getOs, getSecret, getShell, + getUserInfo, getWindowsExitReason, isAndroid, isBuildkite, @@ -844,7 +845,14 @@ async function runTests() { const absoluteTestPath = join(testsPath, testPath); const title = relative(cwd, absoluteTestPath).replaceAll(sep, "/"); if (isNodeTest(testPath)) { - const testContent = readFileSync(absoluteTestPath, "utf-8"); + let testContent = ""; + try { + testContent = readFileSync(absoluteTestPath, "utf-8"); + } catch (error) { + // Gone since discovery (a wiped checkout). The step below fails on + // it with bun's own error instead of this throw ending the whole run. + if (error?.code !== "ENOENT") throw error; + } const flagsMatch = /^\/\/ Flags:[^\S\r\n]+(--[^\r\n]*)$/m.exec(testContent); const testFlags = flagsMatch ? flagsMatch[1].split(/\s+/).filter(flag => resolutionGatingFlags.has(flag.split("=")[0])) @@ -1762,7 +1770,7 @@ function getCombinedPath(execPath) { async function spawnBun(execPath, { args, cwd, timeout, gracefulTimeout, idleTimeout, env, stdout, stderr }) { const path = getCombinedPath(execPath); const tmpdirPath = mkdtempSync(join(tmpdir(), "buntmp-")); - const { username, homedir } = userInfo(); + const { username, homedir } = getUserInfo(); const shellPath = getShell(); const bunEnv = { ...process.env, diff --git a/scripts/utils.mjs b/scripts/utils.mjs index 75da2ff08d2e..b4ce6416a74b 100755 --- a/scripts/utils.mjs +++ b/scripts/utils.mjs @@ -1609,11 +1609,42 @@ export function getHostname() { } /** - * @returns {string} + * @typedef {object} UserInfo + * @property {string | undefined} username + * @property {string | undefined} homedir + */ + +/** @type {UserInfo | undefined} */ +let cachedUserInfo; + +/** + * The user that runs this process, looked up once. `os.userInfo()` is a + * getpwuid_r() lookup that can fail at runtime: a macOS host that is shutting + * down answers ENOENT for its own uid once opendirectoryd is gone. The login + * session exported the same values, so a failed lookup falls back to them. + * @returns {UserInfo} + */ +export function getUserInfo() { + if (!cachedUserInfo) { + try { + const { username, homedir } = userInfo(); + cachedUserInfo = { username, homedir }; + } catch (error) { + cachedUserInfo = { + username: getEnv("USER", false) || getEnv("LOGNAME", false) || getEnv("USERNAME", false), + homedir: getEnv("HOME", false) || getEnv("USERPROFILE", false), + }; + console.warn("os.userInfo() failed, using USER and HOME from the environment instead:", error); + } + } + return cachedUserInfo; +} + +/** + * @returns {string | undefined} */ export function getUsername() { - const { username } = userInfo(); - return username; + return getUserInfo().username; } /** diff --git a/test/internal/runner-user-info.test.ts b/test/internal/runner-user-info.test.ts new file mode 100644 index 000000000000..1ac96d967caa --- /dev/null +++ b/test/internal/runner-user-info.test.ts @@ -0,0 +1,35 @@ +/** + * scripts/runner.node.mjs sets USER and HOME for every `bun test` it spawns + * from getUserInfo() in scripts/utils.mjs. spawnBun used to call os.userInfo() + * per test file instead, and that call throws (`uv_os_get_passwd returned + * ENOENT`) on a macOS agent whose host has begun to shut down, which ended the + * whole shard with exit 1. getUserInfo() looks the user up once and falls back + * to the environment when the lookup fails. + */ +import { expect, mock, test } from "bun:test"; +import * as os from "node:os"; +import { getUserInfo, getUsername } from "../../scripts/utils.mjs"; + +test("getUserInfo() falls back to USER and HOME when os.userInfo() throws, and looks the user up once", () => { + const real = { ...os }; + let calls = 0; + mock.module("node:os", () => ({ + ...real, + userInfo: () => { + calls++; + throw new Error("A system error occurred: uv_os_get_passwd returned ENOENT (no such file or directory)"); + }, + })); + try { + const { env } = process; + const expected = { username: env.USER || env.LOGNAME || env.USERNAME, homedir: env.HOME || env.USERPROFILE }; + + const first = getUserInfo(); + expect(first).toEqual(expected); + expect(getUserInfo()).toBe(first); + expect(getUsername()).toBe(expected.username); + expect(calls).toBe(1); + } finally { + mock.module("node:os", () => real); + } +});