Compare commits

...

11 Commits

Author SHA1 Message Date
Cursor Agent
56861526ce Add error handling for listening on file descriptors in Bun's net module 2025-05-30 04:55:03 +00:00
Jarred Sumner
7765b61038 Update environment.json 2025-05-29 21:46:24 -07:00
Jarred Sumner
8a06ddb1fb Update environment.json 2025-05-29 21:11:53 -07:00
Jarred Sumner
2e76e69939 Update environment.json 2025-05-29 21:08:55 -07:00
Jarred Sumner
aa404b14c4 Fix http Expect header handling (#20026)
Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com>
Co-authored-by: Ciro Spaciari <ciro.spaciari@gmail.com>
2025-05-29 20:03:32 -07:00
Jarred Sumner
a4819b41e9 Fix setSourceMapsEnabled node test (#20039)
Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com>
2025-05-29 18:04:31 -07:00
Jarred Sumner
f5bfda9699 Fix http socket encoding check (#20031)
Co-authored-by: Jarred-Sumner <709451+Jarred-Sumner@users.noreply.github.com>
2025-05-29 17:05:51 -07:00
Jarred Sumner
9f5adfefe3 Add action that ensures the node test is downloaded from node's repo 2025-05-29 17:02:11 -07:00
Jarred Sumner
316c8d6c48 Add node:test:cp package.json script 2025-05-29 16:48:37 -07:00
Ashcon Partovi
da87890532 ci: Fix build image step with zig (#20023) 2025-05-29 16:07:35 -07:00
Meghan Denny
576f66c149 fix test-net-server-drop-connections.js (#19995) 2025-05-29 13:55:25 -07:00
16 changed files with 439 additions and 35 deletions

View File

@@ -309,6 +309,19 @@ function getCppAgent(platform, options) {
});
}
/**
* @returns {Platform}
*/
function getZigPlatform() {
return {
os: "linux",
arch: "aarch64",
abi: "musl",
distro: "alpine",
release: "3.21",
};
}
/**
* @param {Platform} platform
* @param {PipelineOptions} options
@@ -322,19 +335,9 @@ function getZigAgent(platform, options) {
// queue: "build-zig",
// };
return getEc2Agent(
{
os: "linux",
arch: "aarch64",
abi: "musl",
distro: "alpine",
release: "3.21",
},
options,
{
instanceType: "r8g.large",
},
);
return getEc2Agent(getZigPlatform(), options, {
instanceType: "r8g.large",
});
}
/**
@@ -1105,6 +1108,11 @@ async function getPipeline(options = {}) {
steps.push(
...relevantBuildPlatforms.map(target => {
const imageKey = getImageKey(target);
const zigImageKey = getImageKey(getZigPlatform());
const dependsOn = imagePlatforms.has(zigImageKey) ? [`${zigImageKey}-build-image`] : [];
if (imagePlatforms.has(imageKey)) {
dependsOn.push(`${imageKey}-build-image`);
}
return getStepWithDependsOn(
{
@@ -1114,7 +1122,7 @@ async function getPipeline(options = {}) {
? [getBuildBunStep(target, options)]
: [getBuildCppStep(target, options), getBuildZigStep(target, options), getLinkBunStep(target, options)],
},
imagePlatforms.has(imageKey) ? `${imageKey}-build-image` : undefined,
...dependsOn,
);
}),
);

45
.github/workflows/codex-test-sync.yml vendored Normal file
View File

@@ -0,0 +1,45 @@
name: Codex Test Sync
on:
pull_request:
types: [labeled, opened]
env:
BUN_VERSION: "canary"
jobs:
sync-node-tests:
runs-on: ubuntu-latest
if: |
(github.event.action == 'labeled' && github.event.label.name == 'codex') ||
(github.event.action == 'opened' && contains(github.event.pull_request.labels.*.name, 'codex'))
permissions:
contents: write
pull-requests: write
steps:
- name: Checkout
uses: actions/checkout@v4
with:
token: ${{ secrets.GITHUB_TOKEN }}
fetch-depth: 0
- name: Setup Bun
uses: ./.github/actions/setup-bun
with:
bun-version: ${{ env.BUN_VERSION }}
- name: Get changed files and sync tests
run: |
# Get the list of changed files from the PR
git diff --name-only origin/main...HEAD | while read -r file; do
if [[ "$file" =~ ^test/js/node/test/(parallel|sequential)/(.+)\.js$ ]]; then
test_name="${BASH_REMATCH[2]}"
echo "Syncing test: $test_name"
bun node:test:cp "$test_name"
fi
done
- name: Commit changes
uses: stefanzweifel/git-auto-commit-action@v5
with:
commit_message: "Sync Node.js tests with upstream"

View File

@@ -76,6 +76,7 @@
"zig-format:check": "bun run analysis:no-llvm --target zig-format-check",
"prettier": "bunx prettier@latest --plugin=prettier-plugin-organize-imports --config .prettierrc --write scripts packages src docs 'test/**/*.{test,spec}.{ts,tsx,js,jsx,mts,mjs,cjs,cts}' '!test/**/*fixture*.*'",
"node:test": "node ./scripts/runner.node.mjs --quiet --exec-path=$npm_execpath --node-tests ",
"node:test:cp": "bun ./scripts/fetch-node-test.ts ",
"clean:zig": "rm -rf build/debug/cache/zig build/debug/CMakeCache.txt 'build/debug/*.o' .zig-cache zig-out || true",
"sync-webkit-source": "bun ./scripts/sync-webkit-source.ts"
}

View File

@@ -1,4 +1,4 @@
# Version: 7
# Version: 8
# A script that installs the dependencies needed to build and test Bun.
# This should work on Windows 10 or newer with PowerShell.

View File

@@ -1,5 +1,5 @@
#!/bin/sh
# Version: 10
# Version: 11
# A script that installs the dependencies needed to build and test Bun.
# This should work on macOS and Linux with a POSIX shell.

112
scripts/fetch-node-test.ts Normal file
View File

@@ -0,0 +1,112 @@
import { mkdirSync, writeFileSync } from "fs";
import path, { dirname, join } from "path";
const options: RequestInit = {};
if (process.env.GITHUB_TOKEN) {
options.headers = {
Authorization: `Bearer ${process.env.GITHUB_TOKEN}`,
};
}
async function fetchNodeTest(testName: string) {
const nodeRepoUrl = "https://raw.githubusercontent.com/nodejs/node/main";
const extensions = ["js", "mjs", "ts"];
const testDirs = ["test/parallel", "test/sequential"];
// Try different combinations of test name patterns
const testNameVariations = [
testName,
testName.startsWith("test-") ? testName : `test-${testName}`,
testName.replace(/^test-/, ""),
];
for (const testDir of testDirs) {
for (const nameVariation of testNameVariations) {
// Try with extensions
for (const ext of extensions) {
const testPath = `${testDir}/${nameVariation}.${ext}`;
const url = `${nodeRepoUrl}/${testPath}`;
try {
console.log(`Trying: ${url}`);
const response = await fetch(url, options);
if (response.ok) {
const content = await response.text();
const localPath = join("test/js/node", testPath);
// Create directory if it doesn't exist
mkdirSync(dirname(localPath), { recursive: true });
// Write the file
writeFileSync(localPath, content);
console.log(
`✅ Successfully fetched and saved: ${localPath} (${new Intl.NumberFormat("en-US", {
notation: "compact",
unit: "kilobyte",
}).format(Buffer.byteLength(content, "utf-8"))})`,
);
return localPath;
}
} catch (error) {
// Continue to next variation
}
}
// Try without extension
const testPath = `${testDir}/${nameVariation}`;
const url = `${nodeRepoUrl}/${testPath}`;
try {
console.log(`Trying: ${url}`);
const response = await fetch(url, options);
if (response.ok) {
const content = await response.text();
const localPath = join("test/js/node", testPath);
// Create directory if it doesn't exist
mkdirSync(dirname(localPath), { recursive: true });
// Write the file
writeFileSync(localPath, content);
console.log(
`✅ Successfully fetched and saved: ${localPath} (${new Intl.NumberFormat("en-US", {
notation: "compact",
unit: "kilobyte",
}).format(Buffer.byteLength(content, "utf-8"))})`,
);
return localPath;
}
} catch (error) {
// Continue to next variation
}
}
}
throw new Error(`❌ Could not find test: ${testName}`);
}
// Get test name from command line arguments
let testName = process.argv[2];
if (testName.startsWith(path.join(import.meta.dirname, ".."))) {
testName = testName.slice(path.join(import.meta.dirname, "..").length);
}
if (testName.startsWith("test/parallel/")) {
testName = testName.replace("test/parallel/", "");
} else if (testName.startsWith("test/sequential/")) {
testName = testName.replace("test/sequential/", "");
}
if (!testName) {
console.error("Usage: bun scripts/fetch-node-test.ts <test-name>");
process.exit(1);
}
try {
await fetchNodeTest(testName);
} catch (error) {
console.error(error.message);
process.exit(1);
}

View File

@@ -290,7 +290,7 @@ export async function spawn(command, options = {}) {
if (exitCode !== 0 && isWindows) {
const exitReason = getWindowsExitReason(exitCode);
if (exitReason) {
exitCode = exitReason;
signalCode = exitReason;
}
}
@@ -386,7 +386,7 @@ export function spawnSync(command, options = {}) {
if (exitCode !== 0 && isWindows) {
const exitReason = getWindowsExitReason(exitCode);
if (exitReason) {
exitCode = exitReason;
signalCode = exitReason;
}
}
@@ -442,9 +442,37 @@ export function spawnSyncSafe(command, options = {}) {
* @returns {string | undefined}
*/
export function getWindowsExitReason(exitCode) {
const ntStatusPath = "C:\\Program Files (x86)\\Windows Kits\\10\\Include\\10.0.22621.0\\shared\\ntstatus.h";
const nthStatus = readFile(ntStatusPath, { cache: true });
const windowsKitPath = "C:\\Program Files (x86)\\Windows Kits";
if (!existsSync(windowsKitPath)) {
return;
}
const windowsKitPaths = readdirSync(windowsKitPath)
.filter(filename => isFinite(parseInt(filename)))
.sort((a, b) => parseInt(b) - parseInt(a));
let ntStatusPath;
for (const windowsKitPath of windowsKitPaths) {
const includePath = `${windowsKitPath}\\Include`;
if (!existsSync(includePath)) {
continue;
}
const windowsSdkPaths = readdirSync(includePath).sort();
for (const windowsSdkPath of windowsSdkPaths) {
const statusPath = `${includePath}\\${windowsSdkPath}\\shared\\ntstatus.h`;
if (existsSync(statusPath)) {
ntStatusPath = statusPath;
break;
}
}
}
if (!ntStatusPath) {
return;
}
const nthStatus = readFile(ntStatusPath, { cache: true });
const match = nthStatus.match(new RegExp(`(STATUS_\\w+).*0x${exitCode?.toString(16)}`, "i"));
if (match) {
const [, exitReason] = match;

View File

@@ -3139,6 +3139,21 @@ JSC_DEFINE_HOST_FUNCTION(Process_stubEmptyFunction, (JSGlobalObject * globalObje
return JSValue::encode(jsUndefined());
}
JSC_DEFINE_HOST_FUNCTION(Process_setSourceMapsEnabled, (JSC::JSGlobalObject * lexicalGlobalObject, JSC::CallFrame* callFrame))
{
Zig::GlobalObject* globalObject = defaultGlobalObject(lexicalGlobalObject);
auto& vm = JSC::getVM(globalObject);
auto scope = DECLARE_THROW_SCOPE(vm);
JSValue arg0 = callFrame->argument(0);
if (!arg0.isBoolean()) {
return Bun::ERR::INVALID_ARG_TYPE(scope, globalObject, "enabled"_s, "boolean"_s, arg0);
}
globalObject->processObject()->m_sourceMapsEnabled = arg0.toBoolean(globalObject);
return JSValue::encode(jsUndefined());
}
JSC_DEFINE_HOST_FUNCTION(Process_stubFunctionReturningArray, (JSGlobalObject * globalObject, CallFrame* callFrame))
{
return JSValue::encode(JSC::constructEmptyArray(globalObject, nullptr));
@@ -3645,7 +3660,7 @@ extern "C" void Process__emitErrorEvent(Zig::GlobalObject* global, EncodedJSValu
resourceUsage Process_functionResourceUsage Function 0
revision constructRevision PropertyCallback
send constructProcessSend PropertyCallback
setSourceMapsEnabled Process_stubEmptyFunction Function 1
setSourceMapsEnabled Process_setSourceMapsEnabled Function 1
setUncaughtExceptionCaptureCallback Process_setUncaughtExceptionCaptureCallback Function 1
stderr constructStderr PropertyCallback
stdin constructStdin PropertyCallback

View File

@@ -50,6 +50,7 @@ public:
~Process();
bool m_isExitCodeObservable = false;
bool m_sourceMapsEnabled = false;
static constexpr unsigned StructureFlags = Base::StructureFlags | HasStaticPropertyTable;

View File

@@ -1071,12 +1071,19 @@ const ServerPrototype = {
http_res.assignSocket(socket);
}
}
} else if (http_req.headers.expect === "100-continue") {
if (server.listenerCount("checkContinue") > 0) {
server.emit("checkContinue", http_req, http_res);
} else if (http_req.headers.expect !== undefined) {
if (http_req.headers.expect === "100-continue") {
if (server.listenerCount("checkContinue") > 0) {
server.emit("checkContinue", http_req, http_res);
} else {
http_res.writeContinue();
server.emit("request", http_req, http_res);
}
} else if (server.listenerCount("checkExpectation") > 0) {
server.emit("checkExpectation", http_req, http_res);
} else {
http_res.writeContinue();
server.emit("request", http_req, http_res);
http_res.writeHead(417);
http_res.end();
}
} else {
server.emit("request", http_req, http_res);
@@ -1408,6 +1415,12 @@ const NodeHTTPServerSocket = class Socket extends Duplex {
return this;
}
setEncoding(_encoding) {
const err = new Error("Changing the socket encoding is not allowed per RFC7230 Section 3.");
err.code = "ERR_HTTP_SOCKET_ENCODING";
throw err;
}
unref() {
return this;
}

View File

@@ -88,7 +88,6 @@ function isIP(s): 0 | 4 | 6 {
}
const bunTlsSymbol = Symbol.for("::buntls::");
const bunSocketServerConnections = Symbol.for("::bunnetserverconnections::");
const bunSocketServerOptions = Symbol.for("::bunnetserveroptions::");
const owner_symbol = Symbol("owner_symbol");
@@ -339,7 +338,7 @@ const ServerHandlers: SocketHandler = {
const data = this.data;
if (!data) return;
data.server[bunSocketServerConnections]--;
data.server._connections--;
{
if (!data[kclosed]) {
data[kclosed] = true;
@@ -385,7 +384,7 @@ const ServerHandlers: SocketHandler = {
return;
}
}
if (self.maxConnections && self[bunSocketServerConnections] >= self.maxConnections) {
if (self.maxConnections != null && self._connections >= self.maxConnections) {
const data = {
localAddress: _socket.localAddress,
localPort: _socket.localPort || this.localPort,
@@ -404,7 +403,7 @@ const ServerHandlers: SocketHandler = {
const bunTLS = _socket[bunTlsSymbol];
const isTLS = typeof bunTLS === "function";
self[bunSocketServerConnections]++;
self._connections++;
if (pauseOnConnect) {
_socket.pause();
@@ -2075,7 +2074,6 @@ function Server(options?, connectionListener?) {
// https://nodejs.org/api/net.html#netcreateserveroptions-connectionlistener
const {
maxConnections, //
allowHalfOpen = false,
keepAlive = false,
keepAliveInitialDelay = 0,
@@ -2092,7 +2090,6 @@ function Server(options?, connectionListener?) {
this._unref = false;
this.listeningId = 1;
this[bunSocketServerConnections] = 0;
this[bunSocketServerOptions] = undefined;
this.allowHalfOpen = allowHalfOpen;
this.keepAlive = keepAlive;
@@ -2100,7 +2097,6 @@ function Server(options?, connectionListener?) {
this.highWaterMark = highWaterMark;
this.pauseOnConnect = Boolean(pauseOnConnect);
this.noDelay = noDelay;
this.maxConnections = Number.isSafeInteger(maxConnections) && maxConnections > 0 ? maxConnections : 0;
options.connectionListener = connectionListener;
this[bunSocketServerOptions] = options;
@@ -2163,7 +2159,7 @@ Server.prototype[Symbol.asyncDispose] = function () {
};
Server.prototype._emitCloseIfDrained = function _emitCloseIfDrained() {
if (this._handle || this[bunSocketServerConnections] > 0) {
if (this._handle || this._connections > 0) {
return;
}
process.nextTick(() => {
@@ -2192,7 +2188,7 @@ Server.prototype.getConnections = function getConnections(callback) {
//in Bun case we will never error on getConnections
//node only errors if in the middle of the couting the server got disconnected, what never happens in Bun
//if disconnected will only pass null as well and 0 connected
callback(null, this._handle ? this[bunSocketServerConnections] : 0);
callback(null, this._handle ? this._connections : 0);
}
return this;
};
@@ -2372,6 +2368,18 @@ Server.prototype[kRealListen] = function (
_onListen,
fd,
) {
// Check if we're trying to listen on a file descriptor
if (fd != null) {
// Bun doesn't support listening on file descriptors, so emit an async error like Node.js does
const error = new Error("listen EINVAL: invalid argument");
error.code = "EINVAL";
error.errno = -22;
error.syscall = "listen";
setTimeout(emitErrorNextTick, 1, this, error);
return;
}
if (path) {
this._handle = Bun.listen({
unix: path,
@@ -2383,6 +2391,8 @@ Server.prototype[kRealListen] = function (
socket: ServerHandlers,
});
} else if (fd != null) {
// NOTE: This block is unreachable because fd != null cases are handled earlier
// with an async error emission. This code is kept for clarity but will never execute.
this._handle = Bun.listen({
fd,
hostname,

View File

@@ -0,0 +1,26 @@
const common = require('../common');
const assert = require('assert');
const http = require('http');
const server = http.createServer().listen(0, connectToServer);
server.on('connection', common.mustCall((socket) => {
assert.throws(
() => {
socket.setEncoding('');
},
{
code: 'ERR_HTTP_SOCKET_ENCODING',
name: 'Error',
message: 'Changing the socket encoding is not allowed per RFC7230 Section 3.'
}
);
socket.end();
}));
function connectToServer() {
const client = new http.Agent().createConnection(this.address().port, () => {
client.end();
}).on('end', () => server.close());
}

View File

@@ -0,0 +1,55 @@
// Spec documentation http://httpwg.github.io/specs/rfc7231.html#header.expect
'use strict';
const common = require('../common');
const assert = require('assert');
const http = require('http');
const tests = [417, 417];
let testsComplete = 0;
let testIdx = 0;
const s = http.createServer((req, res) => {
throw new Error('this should never be executed');
});
s.listen(0, nextTest);
function nextTest() {
const options = {
port: s.address().port,
headers: { 'Expect': 'meoww' }
};
if (testIdx === tests.length) {
return s.close();
}
const test = tests[testIdx];
if (testIdx > 0) {
s.on('checkExpectation', common.mustCall((req, res) => {
res.statusCode = 417;
res.end();
}));
}
http.get(options, (response) => {
console.log(`client: expected status: ${test}`);
console.log(`client: statusCode: ${response.statusCode}`);
assert.strictEqual(response.statusCode, test);
assert.strictEqual(response.statusMessage, 'Expectation Failed');
response.on('end', () => {
testsComplete++;
testIdx++;
nextTest();
});
response.resume();
});
}
process.on('exit', () => {
assert.strictEqual(testsComplete, 2);
});

View File

@@ -0,0 +1,33 @@
// Copyright Joyent, Inc. and other Node contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a
// copy of this software and associated documentation files (the
// "Software"), to deal in the Software without restriction, including
// without limitation the rights to use, copy, modify, merge, publish,
// distribute, sublicense, and/or sell copies of the Software, and to permit
// persons to whom the Software is furnished to do so, subject to the
// following conditions:
//
// The above copyright notice and this permission notice shall be included
// in all copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
// OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
// MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN
// NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM,
// DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE
// USE OR OTHER DEALINGS IN THE SOFTWARE.
'use strict';
const common = require('../common');
const assert = require('assert');
const net = require('net');
// This should fail with an async EINVAL error, not throw an exception
net.createServer(common.mustNotCall())
.listen({ fd: 0 })
.on('error', common.mustCall(function(e) {
assert(e instanceof Error);
assert(['EINVAL', 'ENOTSOCK'].includes(e.code));
}));

View File

@@ -0,0 +1,41 @@
'use strict';
const common = require('../common');
const assert = require('assert');
const net = require('net');
let firstSocket;
const dormantServer = net.createServer(common.mustNotCall());
const server = net.createServer(common.mustCall((socket) => {
firstSocket = socket;
}));
dormantServer.maxConnections = 0;
server.maxConnections = 1;
dormantServer.on('drop', common.mustCall((data) => {
assert.strictEqual(!!data.localAddress, true);
assert.strictEqual(!!data.localPort, true);
assert.strictEqual(!!data.remoteAddress, true);
assert.strictEqual(!!data.remotePort, true);
assert.strictEqual(!!data.remoteFamily, true);
dormantServer.close();
}));
server.on('drop', common.mustCall((data) => {
assert.strictEqual(!!data.localAddress, true);
assert.strictEqual(!!data.localPort, true);
assert.strictEqual(!!data.remoteAddress, true);
assert.strictEqual(!!data.remotePort, true);
assert.strictEqual(!!data.remoteFamily, true);
firstSocket.destroy();
server.close();
}));
dormantServer.listen(0, () => {
net.createConnection(dormantServer.address().port);
});
server.listen(0, () => {
net.createConnection(server.address().port);
net.createConnection(server.address().port);
});

View File

@@ -0,0 +1,16 @@
'use strict';
require('../common');
const assert = require('assert');
const unexpectedValues = [
undefined,
null,
1,
{},
() => {},
];
for (const it of unexpectedValues) {
assert.throws(() => {
process.setSourceMapsEnabled(it);
}, /ERR_INVALID_ARG_TYPE/);
}