From b95c9c071270d6466fcf874d3f2bd06e1ac841a2 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 16 Jul 2026 00:20:17 +0200 Subject: [PATCH 1/4] test(dgram): expand granular Node parity coverage --- test-parity/node-suite/dgram/STATUS.md | 82 +++++++++++++++++++ .../dgram/connection/state-transitions.ts | 47 +++++++++++ .../dgram/lifecycle/abort-signal.ts | 25 ++++++ .../dgram/lifecycle/bind-overloads.ts | 28 +++++++ .../node-suite/dgram/metrics/queue-and-ref.ts | 12 +++ .../dgram/send/callback-ordering.ts | 33 ++++++++ .../dgram/send/connected-overloads.ts | 26 ++++++ .../node-suite/dgram/send/default-host.ts | 23 ++++++ .../dgram/send/empty-and-multiple.ts | 39 +++++++++ .../node-suite/dgram/send/overloads.ts | 32 ++++++++ .../dgram/validation/create-socket-options.ts | 36 ++++++++ .../dgram/validation/send-arguments.ts | 25 ++++++ test-parity/node_suite_baseline.json | 4 +- 13 files changed, 410 insertions(+), 2 deletions(-) create mode 100644 test-parity/node-suite/dgram/STATUS.md create mode 100644 test-parity/node-suite/dgram/connection/state-transitions.ts create mode 100644 test-parity/node-suite/dgram/lifecycle/abort-signal.ts create mode 100644 test-parity/node-suite/dgram/lifecycle/bind-overloads.ts create mode 100644 test-parity/node-suite/dgram/metrics/queue-and-ref.ts create mode 100644 test-parity/node-suite/dgram/send/callback-ordering.ts create mode 100644 test-parity/node-suite/dgram/send/connected-overloads.ts create mode 100644 test-parity/node-suite/dgram/send/default-host.ts create mode 100644 test-parity/node-suite/dgram/send/empty-and-multiple.ts create mode 100644 test-parity/node-suite/dgram/send/overloads.ts create mode 100644 test-parity/node-suite/dgram/validation/create-socket-options.ts create mode 100644 test-parity/node-suite/dgram/validation/send-arguments.ts diff --git a/test-parity/node-suite/dgram/STATUS.md b/test-parity/node-suite/dgram/STATUS.md new file mode 100644 index 0000000000..0cdb8add72 --- /dev/null +++ b/test-parity/node-suite/dgram/STATUS.md @@ -0,0 +1,82 @@ +# `node:dgram` granular parity status + +## Upstream evidence + +This expansion was assessed against primary repositories captured on 2026-07-15: + +- Node.js [`34c28d5a`](https://github.com/nodejs/node/tree/34c28d5a69f4f00cd599adcbe57834435d3a683b/test): 82 `test-dgram*` files in `test/parallel` and `test/sequential`. +- Deno [`34f9f47f`](https://github.com/denoland/deno/blob/34f9f47f42d2a316efb8245aadc343f8d7cdf5c4/tests/node_compat/config.jsonc): 69 selected `parallel/test-dgram*` or `sequential/test-dgram*` entries. +- Bun [`da08a6b8`](https://github.com/oven-sh/bun/tree/da08a6b8da3fdde3da8aa7e1453584aa681e9c04/test/js/node): 68 copied upstream `test-dgram*` files plus Bun's focused `node-dgram.test.js`. + +The Perry fixtures are diagnostic adaptations, not verbatim copies. They use ephemeral ports, loopback addresses, sequential round trips, and deterministic summaries rather than upstream harness helpers or fixed ports. + +| Perry area | Representative Node files | Deno selection | Bun copy | +| --- | --- | --- | --- | +| socket and send validation | `test-dgram-createSocket-type.js`, `test-dgram-send-bad-arguments.js`, `test-dgram-send-invalid-msg-type.js` | all 3 | send cases (2/3) | +| bind and close lifecycle | `test-dgram-bind.js`, `test-dgram-bind-default-address.js` | both | both | +| AbortSignal | `test-dgram-close-signal.js`, `test-dgram-abort-closed.js` | both | both | +| connection state | `test-dgram-connect.js` | selected | copied | +| send overloads and byte counts | `test-dgram-send-callback-buffer.js`, `test-dgram-bytes-length.js`, `test-dgram-connect-send-callback-buffer.js` | all 3 | all 3 | +| omitted host | `test-dgram-send-default-host.js`, `test-dgram-connect-send-default-host.js` | both | both | +| empty and multiple sends | `test-dgram-send-empty-buffer.js`, `test-dgram-implicit-bind.js` | both (empty buffer is Darwin-disabled/flaky) | both | +| callback timing | `test-dgram-send-callback-recursive.js` | selected | copied | +| queue and reference state | `test-dgram-send-queue-info.js`, `test-dgram-ref.js`, `test-dgram-unref.js` | ref/unref (2/3) | ref/unref (2/3) | + +## Current coverage + +The directory contains 15 fixtures: the original 4 broad cases and 11 granular cases added in this expansion. + +- `validation/`: socket-type matrices, message/list validation, and port errors. +- `lifecycle/`: default/port/options bind overloads with deterministic close, plus AbortSignal validation and post-close abort behavior. +- `connection/`: invalid ports, pending/connected state guards, disconnect errors, and reconnect state. +- `send/`: unconnected and connected string/typed-array overloads, omitted-host behavior, empty datagrams, multiple implicit-bind sends, callback byte counts, and callback asynchrony. +- `metrics/`: queue metrics and `ref()`/`unref()` identity before bind, after bind, and after close. +- Existing broad cases retain unicast loopback, import/API shape, socket controls, and multicast membership coverage. + +The measured Node 26.5.0 focused run is 12/15 parity passes, with no compile failures, crashes, or skipped fixtures. The three stable mismatches intentionally diagnose Perry behavior: + +1. A second `connect()` while pending or connected is accepted instead of throwing `ERR_SOCKET_DGRAM_IS_CONNECTED`. +2. A non-AbortSignal `signal` option is accepted instead of throwing `ERR_INVALID_ARG_TYPE`. +3. A successful `send()` callback runs synchronously instead of asynchronously. + +The work was exercised in coherent batches. Validation/lifecycle probes first exposed unsupported buffer-size validation, repeated bind/close semantics, and active/pre-aborted signal closure. Connection/send probes then separated supported string, typed-array, empty-buffer, default-host, and multiple-send behavior from unsupported offset/length and scatter-gather overloads. The final batch added stable callback-ordering and queue/ref diagnostics before the complete focused rerun. + +## Stopping judgment and exclusions + +Further upstream ports were stopped where they would duplicate the cases above or cross into a separate runtime/platform feature: + +- **Separate runtime work:** Node's new `bindSync()`/`connectSync()`, `Symbol.asyncDispose`, block lists, custom DNS lookup, descriptor/handle binding, send buffer offset/length bounds, scatter-gather arrays, active/pre-aborted signal closure, and exact repeated bind/close return/error semantics. +- **Platform/slow assessment:** `reusePort`, shared ports, cluster/child-process handle transfer, interface-specific IPv6 and link-local addresses, multicast interface/loopback variants, and source-specific multicast beyond the existing smoke fixture. +- **Kernel-sensitive errors:** message-size, out-of-band buffer, receive errors, implicit-bind failure, and address-specific OS error text. +- **Scheduler-sensitive races:** close during bind/lookup/listening, recursive send callbacks, error-quelching races, burst close behavior, ping-pong stress, and unref/cluster process-liveness tests. +- **Redundant upstream variants:** the many connected/unconnected callback, empty-packet, default-host, buffer, typed-array, and multiple-send files are represented by smaller grouped fixtures here. + +These exclusions keep the default granular lane deterministic on loopback while preserving the three actionable Perry mismatches as focused regression targets. + +## Verification + +```text +NODE_BIN=/tmp/node-v26.5.0/bin/node \ +PERRY_NO_AUTO_OPTIMIZE=1 \ +CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 \ +./run_parity_tests.sh --suite node-suite --module dgram + +Parity Pass: 12 +Parity Fail: 3 +Compile Fail: 0 +Crashed: 0 +Skipped: 0 +Parity Rate: 80.0% + +NODE_BIN=/tmp/node-v26.5.0/bin/node \ +python3 scripts/node_suite_run.py "$PWD/target/release/perry" "$PWD" dgram + +dgram 12 15 80.0 diff=3 + +cargo fmt --all -- --check +./scripts/check_file_size.sh +python3 -m json.tool test-parity/node_suite_baseline.json +git diff --check +``` + +No wider module run is required for this baseline ratchet: the executable changes are confined to `node-suite/dgram`, and the baseline runner was measured directly against all 15 dgram fixtures. diff --git a/test-parity/node-suite/dgram/connection/state-transitions.ts b/test-parity/node-suite/dgram/connection/state-transitions.ts new file mode 100644 index 0000000000..54d22ec2e2 --- /dev/null +++ b/test-parity/node-suite/dgram/connection/state-transitions.ts @@ -0,0 +1,47 @@ +import * as dgram from "node:dgram"; + +function codeOf(fn: () => unknown): string { + try { + fn(); + return "none"; + } catch (error: unknown) { + return (error as { code?: string }).code ?? "Error"; + } +} + +const socket = dgram.createSocket("udp4"); +const firstPeer = dgram.createSocket("udp4"); +const secondPeer = dgram.createSocket("udp4"); +await Promise.all([ + new Promise((resolve) => firstPeer.bind(0, "127.0.0.1", () => resolve())), + new Promise((resolve) => secondPeer.bind(0, "127.0.0.1", () => resolve())), +]); +const firstPort = firstPeer.address().port; +const secondPort = secondPeer.address().port; + +console.log("disconnect before connect:", codeOf(() => socket.disconnect())); +console.log("bad ports:", [0, -1, 65536].map((port) => codeOf(() => socket.connect(port))).join(",")); + +const firstConnect = new Promise((resolve) => { + socket.connect(firstPort, "127.0.0.1", () => resolve()); +}); +console.log("connect while pending:", codeOf(() => socket.connect(firstPort))); +await firstConnect; + +let remote = socket.remoteAddress(); +console.log("first remote:", remote.address, remote.family, remote.port === firstPort); +console.log("connect while connected:", codeOf(() => socket.connect(secondPort))); +console.log("disconnect result:", socket.disconnect()); +console.log("remote after disconnect:", codeOf(() => socket.remoteAddress())); +console.log("repeat disconnect:", codeOf(() => socket.disconnect())); + +await new Promise((resolve) => { + socket.connect(secondPort, "127.0.0.1", () => resolve()); +}); +remote = socket.remoteAddress(); +console.log("reconnected remote:", remote.address, remote.family, remote.port === secondPort); +await Promise.all([ + new Promise((resolve) => socket.close(() => resolve())), + new Promise((resolve) => firstPeer.close(() => resolve())), + new Promise((resolve) => secondPeer.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/lifecycle/abort-signal.ts b/test-parity/node-suite/dgram/lifecycle/abort-signal.ts new file mode 100644 index 0000000000..71e0b5ab25 --- /dev/null +++ b/test-parity/node-suite/dgram/lifecycle/abort-signal.ts @@ -0,0 +1,25 @@ +import * as dgram from "node:dgram"; + +let invalidSignalSocket: dgram.Socket | undefined; +function invalidSignalResult(): string { + try { + invalidSignalSocket = dgram.createSocket({ type: "udp4", signal: {} as AbortSignal }); + return "accepted"; + } catch (error: unknown) { + return (error as { code?: string }).code ?? "Error"; + } +} + +console.log("invalid signal:", invalidSignalResult()); +if (invalidSignalSocket) { + await new Promise((resolve) => invalidSignalSocket!.close(() => resolve())); +} + +const controller = new AbortController(); +const socket = dgram.createSocket({ type: "udp4", signal: controller.signal }); +let closes = 0; +socket.on("close", () => closes++); +await new Promise((resolve) => socket.close(() => resolve())); +controller.abort(); +await new Promise((resolve) => queueMicrotask(resolve)); +console.log("abort after close:", closes); diff --git a/test-parity/node-suite/dgram/lifecycle/bind-overloads.ts b/test-parity/node-suite/dgram/lifecycle/bind-overloads.ts new file mode 100644 index 0000000000..cc70e9891d --- /dev/null +++ b/test-parity/node-suite/dgram/lifecycle/bind-overloads.ts @@ -0,0 +1,28 @@ +import * as dgram from "node:dgram"; + +for (const mode of ["default", "port", "options"] as const) { + const socket = dgram.createSocket("udp4"); + + await new Promise((resolve) => { + function onListening(this: dgram.Socket) { + const address = socket.address(); + console.log( + mode, + address.address, + address.family, + typeof address.port, + address.port > 0, + this === socket, + ); + socket.close(() => resolve()); + } + + if (mode === "default") { + socket.bind(onListening); + } else if (mode === "port") { + socket.bind(0, onListening); + } else { + socket.bind({ port: 0, address: "127.0.0.1" }, onListening); + } + }); +} diff --git a/test-parity/node-suite/dgram/metrics/queue-and-ref.ts b/test-parity/node-suite/dgram/metrics/queue-and-ref.ts new file mode 100644 index 0000000000..cf9dd79a65 --- /dev/null +++ b/test-parity/node-suite/dgram/metrics/queue-and-ref.ts @@ -0,0 +1,12 @@ +import * as dgram from "node:dgram"; + +const socket = dgram.createSocket("udp4"); +console.log("initial queue:", socket.getSendQueueSize(), socket.getSendQueueCount()); +console.log("unbound identities:", socket.unref() === socket, socket.ref() === socket); + +await new Promise((resolve) => socket.bind(0, "127.0.0.1", () => resolve())); +console.log("bound identities:", socket.unref() === socket, socket.ref() === socket); +console.log("bound queue:", socket.getSendQueueSize(), socket.getSendQueueCount()); + +await new Promise((resolve) => socket.close(() => resolve())); +console.log("closed identities:", socket.unref() === socket, socket.ref() === socket); diff --git a/test-parity/node-suite/dgram/send/callback-ordering.ts b/test-parity/node-suite/dgram/send/callback-ordering.ts new file mode 100644 index 0000000000..b32b2c2b3b --- /dev/null +++ b/test-parity/node-suite/dgram/send/callback-ordering.ts @@ -0,0 +1,33 @@ +import * as dgram from "node:dgram"; + +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", () => resolve())); +const sender = dgram.createSocket("udp4"); + +let synchronous = true; +let callbackWasAsync = false; +let callbackBytes = -1; +let callbackError = "unset"; + +const received = new Promise((resolve) => { + receiver.once("message", (message) => resolve(message.toString())); +}); +const sent = new Promise((resolve) => { + sender.send("callback", receiver.address().port, "127.0.0.1", (error, bytes) => { + callbackWasAsync = !synchronous; + callbackBytes = bytes; + callbackError = error === null ? "null" : error.code; + resolve(); + }); +}); +synchronous = false; + +await Promise.all([received, sent]); +console.log("callback async:", callbackWasAsync); +console.log("callback result:", callbackError, callbackBytes); +console.log("message:", await received); + +await Promise.all([ + new Promise((resolve) => sender.close(() => resolve())), + new Promise((resolve) => receiver.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/send/connected-overloads.ts b/test-parity/node-suite/dgram/send/connected-overloads.ts new file mode 100644 index 0000000000..4063aae071 --- /dev/null +++ b/test-parity/node-suite/dgram/send/connected-overloads.ts @@ -0,0 +1,26 @@ +import * as dgram from "node:dgram"; + +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", () => resolve())); +const sender = dgram.createSocket("udp4"); +await new Promise((resolve) => { + sender.connect(receiver.address().port, "127.0.0.1", () => resolve()); +}); + +async function roundTrip(label: string, message: string | Uint8Array) { + const received = new Promise((resolve) => { + receiver.once("message", (value) => resolve(value.toString())); + }); + const callback = new Promise((resolve) => { + sender.send(message, (error, bytes) => resolve(`${error === null}:${bytes}`)); + }); + console.log(label, await received, await callback); +} + +await roundTrip("string", "connected string"); +await roundTrip("typed", new Uint8Array([116, 121, 112, 101, 100])); + +await Promise.all([ + new Promise((resolve) => sender.close(() => resolve())), + new Promise((resolve) => receiver.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/send/default-host.ts b/test-parity/node-suite/dgram/send/default-host.ts new file mode 100644 index 0000000000..725ebceee8 --- /dev/null +++ b/test-parity/node-suite/dgram/send/default-host.ts @@ -0,0 +1,23 @@ +import * as dgram from "node:dgram"; + +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", () => resolve())); +const sender = dgram.createSocket("udp4"); + +const first = new Promise((resolve) => { + receiver.once("message", (message) => resolve(message.toString())); +}); +await new Promise((resolve) => sender.send("implicit host", receiver.address().port, () => resolve())); +console.log("unconnected default host:", await first); + +await new Promise((resolve) => sender.connect(receiver.address().port, () => resolve())); +const second = new Promise((resolve) => { + receiver.once("message", (message) => resolve(message.toString())); +}); +await new Promise((resolve) => sender.send("connected host", () => resolve())); +console.log("connected default host:", await second); + +await Promise.all([ + new Promise((resolve) => sender.close(() => resolve())), + new Promise((resolve) => receiver.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/send/empty-and-multiple.ts b/test-parity/node-suite/dgram/send/empty-and-multiple.ts new file mode 100644 index 0000000000..3717453c1d --- /dev/null +++ b/test-parity/node-suite/dgram/send/empty-and-multiple.ts @@ -0,0 +1,39 @@ +import * as dgram from "node:dgram"; + +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", () => resolve())); +const port = receiver.address().port; +const sender = dgram.createSocket("udp4"); + +async function roundTrip( + label: string, + send: (callback: (error: Error | null, bytes: number) => void) => void, +) { + const message = new Promise((resolve) => receiver.once("message", resolve)); + const sent = new Promise((resolve) => { + send((error, bytes) => resolve(`${error === null}:${bytes}`)); + }); + const received = await message; + console.log(label, received.length, await sent); +} + +await roundTrip("empty buffer", (callback) => { + sender.send(Buffer.alloc(0), port, "127.0.0.1", callback); +}); +const messages: string[] = []; +const multipleReceived = new Promise((resolve) => { + receiver.on("message", (message) => { + messages.push(message.toString()); + if (messages.length === 2) resolve(messages.sort().join(",")); + }); +}); +await Promise.all([ + new Promise((resolve) => sender.send("first", port, "127.0.0.1", () => resolve())), + new Promise((resolve) => sender.send("second", port, "127.0.0.1", () => resolve())), +]); +console.log("multiple sends:", await multipleReceived); + +await Promise.all([ + new Promise((resolve) => sender.close(() => resolve())), + new Promise((resolve) => receiver.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/send/overloads.ts b/test-parity/node-suite/dgram/send/overloads.ts new file mode 100644 index 0000000000..5b3376b748 --- /dev/null +++ b/test-parity/node-suite/dgram/send/overloads.ts @@ -0,0 +1,32 @@ +import * as dgram from "node:dgram"; + +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", () => resolve())); +const port = receiver.address().port; +const sender = dgram.createSocket("udp4"); + +async function sendAndReceive( + expected: string, + send: (callback: (error: Error | null, bytes: number) => void) => void, +) { + const received = new Promise((resolve) => { + receiver.once("message", (message) => resolve(message.toString())); + }); + const callback = new Promise((resolve) => { + send((error, bytes) => resolve(`${error === null}:${bytes}`)); + }); + console.log(expected, await received, await callback); +} + +await sendAndReceive("string", (callback) => { + sender.send("string", port, "127.0.0.1", callback); +}); + +await sendAndReceive("typed", (callback) => { + sender.send(new Uint8Array([116, 121, 112, 101, 100]), port, "127.0.0.1", callback); +}); + +await Promise.all([ + new Promise((resolve) => sender.close(() => resolve())), + new Promise((resolve) => receiver.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/validation/create-socket-options.ts b/test-parity/node-suite/dgram/validation/create-socket-options.ts new file mode 100644 index 0000000000..5be4976491 --- /dev/null +++ b/test-parity/node-suite/dgram/validation/create-socket-options.ts @@ -0,0 +1,36 @@ +import * as dgram from "node:dgram"; + +function errorOf(fn: () => unknown): string { + try { + fn(); + return "none"; + } catch (error: unknown) { + const value = error as { code?: string; name?: string }; + return `${value.name}:${value.code}`; + } +} + +const invalidTypes: unknown[] = [ + "udp5", + ["udp4"], + new String("udp4"), + 1, + {}, + true, + false, + null, + undefined, +]; + +console.log( + "invalid types:", + invalidTypes.map((type) => errorOf(() => dgram.createSocket(type as never))).join(","), +); + +const udp4 = dgram.createSocket({ type: "udp4" }); +const udp6 = dgram.createSocket({ type: "udp6" }); +console.log("valid option types:", typeof udp4.send, typeof udp6.send); +await Promise.all([ + new Promise((resolve) => udp4.close(() => resolve())), + new Promise((resolve) => udp6.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/validation/send-arguments.ts b/test-parity/node-suite/dgram/validation/send-arguments.ts new file mode 100644 index 0000000000..d360a1a2ee --- /dev/null +++ b/test-parity/node-suite/dgram/validation/send-arguments.ts @@ -0,0 +1,25 @@ +import * as dgram from "node:dgram"; + +function codeOf(fn: () => unknown): string { + try { + fn(); + return "none"; + } catch (error: unknown) { + return (error as { code?: string; name?: string }).code ?? + (error as { name?: string }).name ?? "Error"; + } +} + +const socket = dgram.createSocket("udp4"); +const buffer = Buffer.from("hello"); + +console.log("missing message:", codeOf(() => socket.send())); +console.log("number message:", codeOf(() => socket.send(23, 12345, "127.0.0.1"))); +console.log( + "bad list member:", + codeOf(() => socket.send([buffer, 23] as never, 12345, "127.0.0.1")), +); +console.log("port zero:", codeOf(() => socket.send(buffer, 0, "127.0.0.1"))); +console.log("port high:", codeOf(() => socket.send(buffer, 65536, "127.0.0.1"))); + +await new Promise((resolve) => socket.close(() => resolve())); diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index 15e974f4c1..f858a8ece1 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -47,8 +47,8 @@ "total": 242 }, "dgram": { - "pass": 4, - "total": 4 + "pass": 12, + "total": 15 }, "diagnostics_channel": { "pass": 66, From c39a79d3766116310aa4137d312e91c3db770e4f Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 16 Jul 2026 07:59:24 +0200 Subject: [PATCH 2/4] test(dgram): broaden upstream parity coverage --- test-parity/node-suite/dgram/STATUS.md | 60 +++++++++---------- .../dgram/control/ttl-validation.ts | 26 ++++++++ .../dgram/lifecycle/async-dispose-shape.ts | 8 +++ .../dgram/lifecycle/bind-conflict.ts | 16 +++++ .../dgram/lifecycle/close-arguments.ts | 14 +++++ .../dgram/lifecycle/custom-lookup.ts | 14 +++++ .../node-suite/dgram/metrics/buffer-sizes.ts | 30 ++++++++++ .../node-suite/dgram/send/blocklist.ts | 23 +++++++ .../node-suite/dgram/send/buffer-range.ts | 22 +++++++ .../dgram/send/create-socket-listener.ts | 18 ++++++ .../dgram/send/default-address-values.ts | 22 +++++++ .../node-suite/dgram/send/error-routing.ts | 19 ++++++ .../dgram/validation/address-arguments.ts | 30 ++++++++++ .../validation/connected-send-arguments.ts | 32 ++++++++++ .../create-socket-advanced-options.ts | 32 ++++++++++ .../dgram/validation/message-views.ts | 46 ++++++++++++++ .../node-suite/dgram/validation/sendto.ts | 31 ++++++++++ test-parity/node_suite_baseline.json | 8 +-- 18 files changed, 416 insertions(+), 35 deletions(-) create mode 100644 test-parity/node-suite/dgram/control/ttl-validation.ts create mode 100644 test-parity/node-suite/dgram/lifecycle/async-dispose-shape.ts create mode 100644 test-parity/node-suite/dgram/lifecycle/bind-conflict.ts create mode 100644 test-parity/node-suite/dgram/lifecycle/close-arguments.ts create mode 100644 test-parity/node-suite/dgram/lifecycle/custom-lookup.ts create mode 100644 test-parity/node-suite/dgram/metrics/buffer-sizes.ts create mode 100644 test-parity/node-suite/dgram/send/blocklist.ts create mode 100644 test-parity/node-suite/dgram/send/buffer-range.ts create mode 100644 test-parity/node-suite/dgram/send/create-socket-listener.ts create mode 100644 test-parity/node-suite/dgram/send/default-address-values.ts create mode 100644 test-parity/node-suite/dgram/send/error-routing.ts create mode 100644 test-parity/node-suite/dgram/validation/address-arguments.ts create mode 100644 test-parity/node-suite/dgram/validation/connected-send-arguments.ts create mode 100644 test-parity/node-suite/dgram/validation/create-socket-advanced-options.ts create mode 100644 test-parity/node-suite/dgram/validation/message-views.ts create mode 100644 test-parity/node-suite/dgram/validation/sendto.ts diff --git a/test-parity/node-suite/dgram/STATUS.md b/test-parity/node-suite/dgram/STATUS.md index 0cdb8add72..b0c0389117 100644 --- a/test-parity/node-suite/dgram/STATUS.md +++ b/test-parity/node-suite/dgram/STATUS.md @@ -2,11 +2,11 @@ ## Upstream evidence -This expansion was assessed against primary repositories captured on 2026-07-15: +This expansion was reassessed against primary repositories on 2026-07-16: -- Node.js [`34c28d5a`](https://github.com/nodejs/node/tree/34c28d5a69f4f00cd599adcbe57834435d3a683b/test): 82 `test-dgram*` files in `test/parallel` and `test/sequential`. -- Deno [`34f9f47f`](https://github.com/denoland/deno/blob/34f9f47f42d2a316efb8245aadc343f8d7cdf5c4/tests/node_compat/config.jsonc): 69 selected `parallel/test-dgram*` or `sequential/test-dgram*` entries. -- Bun [`da08a6b8`](https://github.com/oven-sh/bun/tree/da08a6b8da3fdde3da8aa7e1453584aa681e9c04/test/js/node): 68 copied upstream `test-dgram*` files plus Bun's focused `node-dgram.test.js`. +- Node.js [`608112af`](https://github.com/nodejs/node/tree/608112affae2cf44d2f8a0a6bfe7967193b459c8/test): 82 `test-dgram*` files in `test/parallel` and `test/sequential`. +- Deno [`f8a17c81`](https://github.com/denoland/deno/blob/f8a17c8171569fa2870d740030aaa59c91fdf9ee/tests/node_compat/config.jsonc): 69 selected `parallel/test-dgram*` or `sequential/test-dgram*` entries. +- Bun [`c4fad462`](https://github.com/oven-sh/bun/tree/c4fad462e7dc20e5e9780f848db42e1e2f52186d/test/js/node): 68 copied upstream `test-dgram*` files plus Bun's focused `node-dgram.test.js`. The Perry fixtures are diagnostic adaptations, not verbatim copies. They use ephemeral ports, loopback addresses, sequential round trips, and deterministic summaries rather than upstream harness helpers or fixed ports. @@ -21,57 +21,55 @@ The Perry fixtures are diagnostic adaptations, not verbatim copies. They use eph | empty and multiple sends | `test-dgram-send-empty-buffer.js`, `test-dgram-implicit-bind.js` | both (empty buffer is Darwin-disabled/flaky) | both | | callback timing | `test-dgram-send-callback-recursive.js` | selected | copied | | queue and reference state | `test-dgram-send-queue-info.js`, `test-dgram-ref.js`, `test-dgram-unref.js` | ref/unref (2/3) | ref/unref (2/3) | +| address and connected-send validation | `test-dgram-send-address-types.js`, `test-dgram-send-bad-arguments.js` | both | both | +| legacy and advanced send forms | `test-dgram-sendto.js`, `test-dgram-send-callback-buffer-length.js`, `test-dgram-send-callback-multi-buffer.js` | all 3 | all 3 | +| lookup, errors, and blocking | `test-dgram-custom-lookup.js`, `test-dgram-send-error.js`, `test-dgram-send-cb-quelches-error.js`, `test-dgram-blocklist.js` | lookup/error cases (3/4) | lookup/error cases (3/4) | +| close, bind conflict, and disposal | `test-dgram-close-is-not-callback.js`, `test-dgram-bind-error-repeat.js`, `test-dgram-async-dispose.mjs` | all 3 | all 3 | +| controls and buffer metrics | `test-dgram-setTTL.js`, `test-dgram-multicast-setTTL.js`, `test-dgram-socket-buffer-size.js` | TTL cases (2/3) | TTL cases (2/3) | +| constructor message listener | `test-dgram-udp4.js` | selected | copied | ## Current coverage -The directory contains 15 fixtures: the original 4 broad cases and 11 granular cases added in this expansion. +The directory contains 31 fixtures: the original 4 broad cases and 27 granular cases added in this expansion. -- `validation/`: socket-type matrices, message/list validation, and port errors. -- `lifecycle/`: default/port/options bind overloads with deterministic close, plus AbortSignal validation and post-close abort behavior. +- `validation/`: socket-type/options matrices, message/list/address validation, connected-send guards, DataView/scatter-gather acceptance, legacy `sendto()`, and port errors. +- `lifecycle/`: default/port/options bind overloads, bind conflicts, custom lookup, close arguments, async-dispose shape, AbortSignal validation, and post-close abort behavior. - `connection/`: invalid ports, pending/connected state guards, disconnect errors, and reconnect state. -- `send/`: unconnected and connected string/typed-array overloads, omitted-host behavior, empty datagrams, multiple implicit-bind sends, callback byte counts, and callback asynchrony. -- `metrics/`: queue metrics and `ref()`/`unref()` identity before bind, after bind, and after close. +- `send/`: unconnected and connected overloads, constructor listeners, default address/host behavior, empty and multiple sends, buffer ranges, block lists, DNS error routing, callback byte counts, and callback asynchrony. +- `metrics/`: buffer-size validation, queue metrics, and `ref()`/`unref()` identity before bind, after bind, and after close. +- `control/`: deterministic TTL boundary validation. - Existing broad cases retain unicast loopback, import/API shape, socket controls, and multicast membership coverage. -The measured Node 26.5.0 focused run is 12/15 parity passes, with no compile failures, crashes, or skipped fixtures. The three stable mismatches intentionally diagnose Perry behavior: +The clean Node 26.5.0 focused baseline run is 16/31 parity passes with 15 stable output differences and no compile failures, runtime errors, or skipped fixtures. The differences diagnose: -1. A second `connect()` while pending or connected is accepted instead of throwing `ERR_SOCKET_DGRAM_IS_CONNECTED`. -2. A non-AbortSignal `signal` option is accepted instead of throwing `ERR_INVALID_ARG_TYPE`. -3. A successful `send()` callback runs synchronously instead of asynchronously. +1. Repeated `connect()` calls and connected range/destination overloads miss `ERR_SOCKET_DGRAM_IS_CONNECTED` guards. +2. Invalid `signal`, `lookup`, buffer-size, and address options/arguments are accepted. +3. Successful `send()` callbacks run synchronously. +4. `sendto()` lacks Node-coded argument errors, scatter-gather arrays are rejected, and buffer offset/length ranges are ignored. +5. `Symbol.asyncDispose`, custom lookup dispatch, and send block lists are not implemented. +6. `close()` does not return the socket, an empty address is treated as a DNS name, and multicast TTL handles `Infinity` differently. -The work was exercised in coherent batches. Validation/lifecycle probes first exposed unsupported buffer-size validation, repeated bind/close semantics, and active/pre-aborted signal closure. Connection/send probes then separated supported string, typed-array, empty-buffer, default-host, and multiple-send behavior from unsupported offset/length and scatter-gather overloads. The final batch added stable callback-ordering and queue/ref diagnostics before the complete focused rerun. +The work was exercised in coherent batches. The first pass covered core validation, bind/connect lifecycle, basic overloads, callbacks, queue/ref metrics, and AbortSignal. The follow-up added 16 cases selected from the larger Node/Deno/Bun corpora: address and advanced-option validation, legacy and connected send forms, custom lookup, close/dispose behavior, bind conflicts, block lists, DNS error routing, buffer metrics, TTL boundaries, constructor listeners, and message-view/scatter-gather behavior. ## Stopping judgment and exclusions Further upstream ports were stopped where they would duplicate the cases above or cross into a separate runtime/platform feature: -- **Separate runtime work:** Node's new `bindSync()`/`connectSync()`, `Symbol.asyncDispose`, block lists, custom DNS lookup, descriptor/handle binding, send buffer offset/length bounds, scatter-gather arrays, active/pre-aborted signal closure, and exact repeated bind/close return/error semantics. -- **Platform/slow assessment:** `reusePort`, shared ports, cluster/child-process handle transfer, interface-specific IPv6 and link-local addresses, multicast interface/loopback variants, and source-specific multicast beyond the existing smoke fixture. +- **Separate runtime work:** Node's new `bindSync()`/`connectSync()`, descriptor/handle binding, active/pre-aborted signal closure, and exact repeated bind/close error semantics. Surface diagnostics for async disposal, block lists, custom lookup, ranges, and scatter-gather now remain in the suite. +- **Platform/slow assessment:** `reusePort`, shared ports, cluster/child-process handle transfer, IPv6 loopback/link-local/interface-specific cases without a capability guard, multicast interface/loopback variants, and source-specific multicast beyond the existing smoke fixture. - **Kernel-sensitive errors:** message-size, out-of-band buffer, receive errors, implicit-bind failure, and address-specific OS error text. - **Scheduler-sensitive races:** close during bind/lookup/listening, recursive send callbacks, error-quelching races, burst close behavior, ping-pong stress, and unref/cluster process-liveness tests. - **Redundant upstream variants:** the many connected/unconnected callback, empty-packet, default-host, buffer, typed-array, and multiple-send files are represented by smaller grouped fixtures here. -These exclusions keep the default granular lane deterministic on loopback while preserving the three actionable Perry mismatches as focused regression targets. +These exclusions keep the default granular lane deterministic on loopback while preserving the 15 actionable Perry differences as focused regression targets. ## Verification ```text -NODE_BIN=/tmp/node-v26.5.0/bin/node \ -PERRY_NO_AUTO_OPTIMIZE=1 \ -CARGO_PROFILE_RELEASE_CODEGEN_UNITS=16 \ -./run_parity_tests.sh --suite node-suite --module dgram - -Parity Pass: 12 -Parity Fail: 3 -Compile Fail: 0 -Crashed: 0 -Skipped: 0 -Parity Rate: 80.0% - NODE_BIN=/tmp/node-v26.5.0/bin/node \ python3 scripts/node_suite_run.py "$PWD/target/release/perry" "$PWD" dgram -dgram 12 15 80.0 diff=3 +dgram 16 31 51.6 diff=15 cargo fmt --all -- --check ./scripts/check_file_size.sh @@ -79,4 +77,4 @@ python3 -m json.tool test-parity/node_suite_baseline.json git diff --check ``` -No wider module run is required for this baseline ratchet: the executable changes are confined to `node-suite/dgram`, and the baseline runner was measured directly against all 15 dgram fixtures. +No wider module run is required for this module floor: the executable changes are confined to `node-suite/dgram`, and the baseline runner was measured directly against all 31 dgram fixtures. Aggregate metadata remains the last clean full-suite snapshot. diff --git a/test-parity/node-suite/dgram/control/ttl-validation.ts b/test-parity/node-suite/dgram/control/ttl-validation.ts new file mode 100644 index 0000000000..532d6137fd --- /dev/null +++ b/test-parity/node-suite/dgram/control/ttl-validation.ts @@ -0,0 +1,26 @@ +import * as dgram from "node:dgram"; + +function codeOf(fn: () => unknown): string { + try { + fn(); + return "none"; + } catch (error: unknown) { + return (error as { code?: string }).code ?? "Error"; + } +} + +const socket = dgram.createSocket("udp4"); +await new Promise((resolve) => socket.bind(0, "127.0.0.1", () => resolve())); + +console.log( + "ttl invalid:", + [0, 256, Infinity, "64"].map((value) => codeOf(() => socket.setTTL(value as never))).join(","), +); +console.log( + "multicast ttl invalid:", + [-1, 256, Infinity, "64"] + .map((value) => codeOf(() => socket.setMulticastTTL(value as never))) + .join(","), +); +console.log("ttl valid:", socket.setTTL(64), socket.setMulticastTTL(0)); +await new Promise((resolve) => socket.close(() => resolve())); diff --git a/test-parity/node-suite/dgram/lifecycle/async-dispose-shape.ts b/test-parity/node-suite/dgram/lifecycle/async-dispose-shape.ts new file mode 100644 index 0000000000..27e59ff130 --- /dev/null +++ b/test-parity/node-suite/dgram/lifecycle/async-dispose-shape.ts @@ -0,0 +1,8 @@ +import * as dgram from "node:dgram"; + +const socket = dgram.createSocket("udp4"); +console.log( + "async dispose:", + typeof (socket as unknown as Record)[Symbol.asyncDispose], +); +await new Promise((resolve) => socket.close(() => resolve())); diff --git a/test-parity/node-suite/dgram/lifecycle/bind-conflict.ts b/test-parity/node-suite/dgram/lifecycle/bind-conflict.ts new file mode 100644 index 0000000000..92e6eed24b --- /dev/null +++ b/test-parity/node-suite/dgram/lifecycle/bind-conflict.ts @@ -0,0 +1,16 @@ +import * as dgram from "node:dgram"; + +const first = dgram.createSocket("udp4"); +await new Promise((resolve) => first.bind(0, "127.0.0.1", () => resolve())); + +const second = dgram.createSocket("udp4"); +const error = new Promise((resolve) => { + second.once("error", (value) => resolve(`${value.code}:${value.syscall}`)); +}); +second.bind(first.address().port, "127.0.0.1"); +console.log("bind conflict:", await error); + +await Promise.all([ + new Promise((resolve) => first.close(() => resolve())), + new Promise((resolve) => second.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/lifecycle/close-arguments.ts b/test-parity/node-suite/dgram/lifecycle/close-arguments.ts new file mode 100644 index 0000000000..9f0212fc64 --- /dev/null +++ b/test-parity/node-suite/dgram/lifecycle/close-arguments.ts @@ -0,0 +1,14 @@ +import * as dgram from "node:dgram"; + +const socket = dgram.createSocket("udp4"); +let closeEvents = 0; +socket.on("close", () => closeEvents++); +const closed = new Promise((resolve) => { + socket.once("close", () => queueMicrotask(resolve)); +}); + +const result = (socket.close as (callback?: unknown) => dgram.Socket)("not a callback"); +await closed; + +console.log("close result self:", result === socket); +console.log("close events:", closeEvents); diff --git a/test-parity/node-suite/dgram/lifecycle/custom-lookup.ts b/test-parity/node-suite/dgram/lifecycle/custom-lookup.ts new file mode 100644 index 0000000000..0a21d5ed69 --- /dev/null +++ b/test-parity/node-suite/dgram/lifecycle/custom-lookup.ts @@ -0,0 +1,14 @@ +import * as dgram from "node:dgram"; + +const calls: string[] = []; +const socket = dgram.createSocket({ + type: "udp4", + lookup(hostname, family, callback) { + calls.push(`${hostname}:${family}`); + callback(null, "0.0.0.0", 4); + }, +}); + +await new Promise((resolve) => socket.bind(0, () => resolve())); +console.log("lookup calls:", calls.length, calls[0]?.startsWith("0.0.0.0:") ?? false); +await new Promise((resolve) => socket.close(() => resolve())); diff --git a/test-parity/node-suite/dgram/metrics/buffer-sizes.ts b/test-parity/node-suite/dgram/metrics/buffer-sizes.ts new file mode 100644 index 0000000000..4c71217433 --- /dev/null +++ b/test-parity/node-suite/dgram/metrics/buffer-sizes.ts @@ -0,0 +1,30 @@ +import * as dgram from "node:dgram"; + +function codeOf(fn: () => unknown): string { + try { + fn(); + return "none"; + } catch (error: unknown) { + return (error as { code?: string }).code ?? "Error"; + } +} + +const unbound = dgram.createSocket("udp4"); +console.log("unbound send get:", codeOf(() => unbound.getSendBufferSize())); +console.log("unbound recv set:", codeOf(() => unbound.setRecvBufferSize(8192))); +await new Promise((resolve) => unbound.close(() => resolve())); + +const socket = dgram.createSocket("udp4"); +await new Promise((resolve) => socket.bind(0, "127.0.0.1", () => resolve())); +console.log( + "invalid recv:", + [-1, Infinity, "bad"].map((value) => codeOf(() => socket.setRecvBufferSize(value as never))).join(","), +); +console.log( + "invalid send:", + [-1, Infinity, "bad"].map((value) => codeOf(() => socket.setSendBufferSize(value as never))).join(","), +); +socket.setRecvBufferSize(10000); +socket.setSendBufferSize(10000); +console.log("positive sizes:", socket.getRecvBufferSize() > 0, socket.getSendBufferSize() > 0); +await new Promise((resolve) => socket.close(() => resolve())); diff --git a/test-parity/node-suite/dgram/send/blocklist.ts b/test-parity/node-suite/dgram/send/blocklist.ts new file mode 100644 index 0000000000..12cbda98bd --- /dev/null +++ b/test-parity/node-suite/dgram/send/blocklist.ts @@ -0,0 +1,23 @@ +import * as dgram from "node:dgram"; +import { BlockList } from "node:net"; + +const blockList = new BlockList(); +blockList.addAddress("127.0.0.1"); + +const sendSocket = dgram.createSocket({ type: "udp4", sendBlockList: blockList }); +const sendCode = await new Promise((resolve) => { + sendSocket.send("blocked", 12345, "127.0.0.1", (error) => { + resolve(error?.code ?? "none"); + }); +}); +console.log("blocked send:", sendCode); +await new Promise((resolve) => sendSocket.close(() => resolve())); + +const connectSocket = dgram.createSocket({ type: "udp4", sendBlockList: blockList }); +const connectCode = await new Promise((resolve) => { + connectSocket.connect(12345, "127.0.0.1", (error) => { + resolve(error?.code ?? "none"); + }); +}); +console.log("blocked connect:", connectCode); +await new Promise((resolve) => connectSocket.close(() => resolve())); diff --git a/test-parity/node-suite/dgram/send/buffer-range.ts b/test-parity/node-suite/dgram/send/buffer-range.ts new file mode 100644 index 0000000000..e4d722367b --- /dev/null +++ b/test-parity/node-suite/dgram/send/buffer-range.ts @@ -0,0 +1,22 @@ +import * as dgram from "node:dgram"; + +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", () => resolve())); +const sender = dgram.createSocket("udp4"); +const message = Buffer.from("--slice--"); + +const received = new Promise((resolve) => { + receiver.once("message", (value) => resolve(value.toString())); +}); +const callback = new Promise((resolve) => { + sender.send(message, 2, 5, receiver.address().port, "127.0.0.1", (error, bytes) => { + resolve(`${error === null}:${bytes}`); + }); +}); + +console.log("range message:", await received); +console.log("range callback:", await callback); +await Promise.all([ + new Promise((resolve) => sender.close(() => resolve())), + new Promise((resolve) => receiver.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/send/create-socket-listener.ts b/test-parity/node-suite/dgram/send/create-socket-listener.ts new file mode 100644 index 0000000000..e4c5d07f25 --- /dev/null +++ b/test-parity/node-suite/dgram/send/create-socket-listener.ts @@ -0,0 +1,18 @@ +import * as dgram from "node:dgram"; + +const receiver = dgram.createSocket("udp4", (message, rinfo) => { + console.log("constructor listener:", message.toString(), rinfo.family, rinfo.size); +}); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", () => resolve())); +const sender = dgram.createSocket("udp4"); + +const received = new Promise((resolve) => receiver.once("message", () => resolve())); +await new Promise((resolve) => { + sender.send("listener", receiver.address().port, "127.0.0.1", () => resolve()); +}); +await received; + +await Promise.all([ + new Promise((resolve) => sender.close(() => resolve())), + new Promise((resolve) => receiver.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/send/default-address-values.ts b/test-parity/node-suite/dgram/send/default-address-values.ts new file mode 100644 index 0000000000..1ef20c6a3c --- /dev/null +++ b/test-parity/node-suite/dgram/send/default-address-values.ts @@ -0,0 +1,22 @@ +import * as dgram from "node:dgram"; + +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", () => resolve())); +const sender = dgram.createSocket("udp4"); + +async function send(address: string | null | undefined) { + return await new Promise((resolve) => { + sender.send("default", receiver.address().port, address as never, (error, bytes) => { + resolve(`${error?.code ?? "null"}:${bytes ?? "none"}`); + }); + }); +} + +console.log("empty:", await send("")); +console.log("null:", await send(null)); +console.log("undefined:", await send(undefined)); + +await Promise.all([ + new Promise((resolve) => sender.close(() => resolve())), + new Promise((resolve) => receiver.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/send/error-routing.ts b/test-parity/node-suite/dgram/send/error-routing.ts new file mode 100644 index 0000000000..fa1a863fd6 --- /dev/null +++ b/test-parity/node-suite/dgram/send/error-routing.ts @@ -0,0 +1,19 @@ +import * as dgram from "node:dgram"; + +const withCallback = dgram.createSocket("udp4"); +let callbackErrorEvents = 0; +withCallback.on("error", () => callbackErrorEvents++); +const callbackCode = await new Promise((resolve) => { + withCallback.send("x", 12345, "missing.invalid", (error) => resolve(error?.code ?? "none")); +}); +await new Promise((resolve) => queueMicrotask(resolve)); +console.log("callback route:", callbackCode, callbackErrorEvents); +await new Promise((resolve) => withCallback.close(() => resolve())); + +const withEvent = dgram.createSocket("udp4"); +const eventCode = new Promise((resolve) => { + withEvent.once("error", (error) => resolve(error.code)); +}); +withEvent.send("x", 12345, "missing.invalid"); +console.log("event route:", await eventCode); +await new Promise((resolve) => withEvent.close(() => resolve())); diff --git a/test-parity/node-suite/dgram/validation/address-arguments.ts b/test-parity/node-suite/dgram/validation/address-arguments.ts new file mode 100644 index 0000000000..d33474ddf2 --- /dev/null +++ b/test-parity/node-suite/dgram/validation/address-arguments.ts @@ -0,0 +1,30 @@ +import * as dgram from "node:dgram"; + +function codeOf(fn: () => unknown): string { + try { + fn(); + return "none"; + } catch (error: unknown) { + return (error as { code?: string; name?: string }).code ?? + (error as { name?: string }).name ?? "Error"; + } +} + +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", () => resolve())); +const sender = dgram.createSocket("udp4"); +const invalidAddresses: unknown[] = [[], 0, 1, true, false, 0n, 1n, {}, Symbol("address")]; + +console.log( + "invalid addresses:", + invalidAddresses + .map((address) => + codeOf(() => sender.send("x", receiver.address().port, address as never)) + ) + .join(","), +); + +await Promise.all([ + new Promise((resolve) => sender.close(() => resolve())), + new Promise((resolve) => receiver.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/validation/connected-send-arguments.ts b/test-parity/node-suite/dgram/validation/connected-send-arguments.ts new file mode 100644 index 0000000000..a91e2bd9ec --- /dev/null +++ b/test-parity/node-suite/dgram/validation/connected-send-arguments.ts @@ -0,0 +1,32 @@ +import * as dgram from "node:dgram"; + +function codeOf(fn: () => unknown): string { + try { + fn(); + return "none"; + } catch (error: unknown) { + return (error as { code?: string; name?: string }).code ?? + (error as { name?: string }).name ?? "Error"; + } +} + +const peer = dgram.createSocket("udp4"); +await new Promise((resolve) => peer.bind(0, "127.0.0.1", () => resolve())); +const socket = dgram.createSocket("udp4"); +await new Promise((resolve) => { + socket.connect(peer.address().port, "127.0.0.1", () => resolve()); +}); + +console.log( + "destination while connected:", + codeOf(() => socket.send("x", peer.address().port, "127.0.0.1")), +); +console.log( + "range destination while connected:", + codeOf(() => socket.send(Buffer.from("x"), 0, 1, peer.address().port, "127.0.0.1")), +); + +await Promise.all([ + new Promise((resolve) => socket.close(() => resolve())), + new Promise((resolve) => peer.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/validation/create-socket-advanced-options.ts b/test-parity/node-suite/dgram/validation/create-socket-advanced-options.ts new file mode 100644 index 0000000000..3a2505f165 --- /dev/null +++ b/test-parity/node-suite/dgram/validation/create-socket-advanced-options.ts @@ -0,0 +1,32 @@ +import * as dgram from "node:dgram"; + +const acceptedSockets: dgram.Socket[] = []; +function codeOf(fn: () => unknown): string { + try { + const socket = fn() as dgram.Socket | undefined; + if (socket) acceptedSockets.push(socket); + return "none"; + } catch (error: unknown) { + return (error as { code?: string; name?: string }).code ?? + (error as { name?: string }).name ?? "Error"; + } +} + +console.log( + "invalid lookup:", + [null, true, 0, "lookup", {}] + .map((lookup) => codeOf(() => dgram.createSocket({ type: "udp4", lookup: lookup as never }))) + .join(","), +); +console.log( + "invalid recv size:", + codeOf(() => dgram.createSocket({ type: "udp4", recvBufferSize: "bad" as never })), +); +console.log( + "invalid send size:", + codeOf(() => dgram.createSocket({ type: "udp4", sendBufferSize: "bad" as never })), +); + +await Promise.all( + acceptedSockets.map((socket) => new Promise((resolve) => socket.close(() => resolve()))), +); diff --git a/test-parity/node-suite/dgram/validation/message-views.ts b/test-parity/node-suite/dgram/validation/message-views.ts new file mode 100644 index 0000000000..76bff6b73a --- /dev/null +++ b/test-parity/node-suite/dgram/validation/message-views.ts @@ -0,0 +1,46 @@ +import * as dgram from "node:dgram"; + +function codeOf(fn: () => unknown): string { + try { + fn(); + return "none"; + } catch (error: unknown) { + return (error as { code?: string; name?: string }).code ?? + (error as { name?: string }).name ?? "Error"; + } +} + +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", () => resolve())); +const sender = dgram.createSocket("udp4"); +const data = new ArrayBuffer(4); +new Uint8Array(data).set([118, 105, 101, 119]); + +const viewReceived = new Promise((resolve) => { + receiver.once("message", (message) => resolve(message.toString())); +}); +const viewCallback = new Promise((resolve) => { + sender.send(new DataView(data), receiver.address().port, "127.0.0.1", (error, bytes) => { + resolve(`${error === null}:${bytes}`); + }); +}); +console.log("data view:", await viewReceived, await viewCallback); + +let finishScatter: (value: string) => void = () => {}; +const scatterCallback = new Promise((resolve) => { + finishScatter = resolve; +}); +const scatterResult = codeOf(() => { + sender.send([Buffer.from("a"), "b"], receiver.address().port, "127.0.0.1", (error, bytes) => { + finishScatter(`${error === null}:${bytes}`); + }); +}); +console.log("scatter accepted:", scatterResult); +if (scatterResult === "none") { + console.log("scatter callback:", await scatterCallback); +} + +await Promise.all([ + new Promise((resolve) => sender.close(() => resolve())), + new Promise((resolve) => receiver.close(() => resolve())), +]); diff --git a/test-parity/node-suite/dgram/validation/sendto.ts b/test-parity/node-suite/dgram/validation/sendto.ts new file mode 100644 index 0000000000..2fdb437f1f --- /dev/null +++ b/test-parity/node-suite/dgram/validation/sendto.ts @@ -0,0 +1,31 @@ +import * as dgram from "node:dgram"; + +function codeOf(fn: () => unknown): string { + try { + fn(); + return "none"; + } catch (error: unknown) { + return (error as { code?: string; name?: string }).code ?? + (error as { name?: string }).name ?? "Error"; + } +} + +const socket = dgram.createSocket("udp4"); +console.log("missing args:", codeOf(() => socket.sendto())); +console.log( + "bad offset:", + codeOf(() => socket.sendto("buffer", "offset" as never, 1, 12345, "127.0.0.1")), +); +console.log( + "bad length:", + codeOf(() => socket.sendto("buffer", 1, "length" as never, 12345, "127.0.0.1")), +); +console.log( + "bad port:", + codeOf(() => socket.sendto("buffer", 1, 1, false as never, "127.0.0.1")), +); +console.log( + "bad address:", + codeOf(() => socket.sendto("buffer", 1, 1, 12345, false as never)), +); +await new Promise((resolve) => socket.close(() => resolve())); diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index f858a8ece1..bcfa755880 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -1,8 +1,8 @@ { "_schema": { "description": "Floor baseline for scripts/node_suite_regression_check.py. Each module's run must produce pass >= floor.pass; dropping below is a regression (exit 1). Improvements are always accepted and reported as ratchet candidates. Captured in the node-26 environment with scripts/node_suite_run.py (pre-warm + fast/slow lanes).", - "oracle": "node v26.3.0 on Linux (the box)", - "note": "Deterministic modules are floored at full pass. Timing/racy modules (http2, net, stream, diagnostics_channel, fs-promises) carry a small margin below observed pass so ordinary flake does not false-alarm; the guard still catches real regressions, which are large (e.g. dns 6->0, http 19->9). node_suite_run.normalize() scrubs environment-variant tokens (console.time hrtime durations, stack-trace frame lines) symmetrically before the stdout compare, so console is floored at full pass (119) on its deterministic content. http is verified 19/19 in isolation but the full-suite harness flakes to 17 under port contention, so it is floored at 17 (flake margin, not a regression); a real http break is a much larger drop. Floors refreshed from a clean node-26 run at 2810/2863 (98.1%)." + "oracle": "aggregate: node v26.3.0 on Linux; dgram focused floor: node v26.5.0 (pinned by .node-version)", + "note": "Deterministic modules are floored at full pass. Timing/racy modules (http2, net, stream, diagnostics_channel, fs-promises) carry a small margin below observed pass so ordinary flake does not false-alarm; the guard still catches real regressions, which are large (e.g. dns 6->0, http 19->9). node_suite_run.normalize() scrubs environment-variant tokens (console.time hrtime durations, stack-trace frame lines) symmetrically before the stdout compare, so console is floored at full pass (119) on its deterministic content. http is verified 19/19 in isolation but the full-suite harness flakes to 17 under port contention, so it is floored at 17 (flake margin, not a regression); a real http break is a much larger drop. Overall remains the last clean full-suite snapshot at 2810/2863 (98.1%); dgram is ratcheted independently from a clean focused baseline run." }, "overall": { "pass": 2810, @@ -47,8 +47,8 @@ "total": 242 }, "dgram": { - "pass": 12, - "total": 15 + "pass": 16, + "total": 31 }, "diagnostics_channel": { "pass": 66, From 42cf2b357ce7cc991d103ac972a6bdd8b75770f5 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 16 Jul 2026 08:32:22 +0200 Subject: [PATCH 3/4] test: extend deterministic dgram parity coverage --- test-parity/node-suite/dgram/STATUS.md | 35 +++++++++-------- .../dgram/connection/connect-ordering.ts | 24 ++++++++++++ .../dgram/lifecycle/bind-error-retry.ts | 32 +++++++++++++++ .../dgram/lifecycle/close-ordering.ts | 17 ++++++++ .../metrics/create-socket-buffer-options.ts | 15 +++++++ .../node-suite/dgram/send/empty-array.ts | 35 +++++++++++++++++ .../dgram/send/implicit-bind-state.ts | 39 +++++++++++++++++++ .../dgram/validation/buffer-bounds.ts | 37 ++++++++++++++++++ .../dgram/validation/message-views.ts | 5 ++- test-parity/node_suite_baseline.json | 4 +- 10 files changed, 223 insertions(+), 20 deletions(-) create mode 100644 test-parity/node-suite/dgram/connection/connect-ordering.ts create mode 100644 test-parity/node-suite/dgram/lifecycle/bind-error-retry.ts create mode 100644 test-parity/node-suite/dgram/lifecycle/close-ordering.ts create mode 100644 test-parity/node-suite/dgram/metrics/create-socket-buffer-options.ts create mode 100644 test-parity/node-suite/dgram/send/empty-array.ts create mode 100644 test-parity/node-suite/dgram/send/implicit-bind-state.ts create mode 100644 test-parity/node-suite/dgram/validation/buffer-bounds.ts diff --git a/test-parity/node-suite/dgram/STATUS.md b/test-parity/node-suite/dgram/STATUS.md index b0c0389117..345543126d 100644 --- a/test-parity/node-suite/dgram/STATUS.md +++ b/test-parity/node-suite/dgram/STATUS.md @@ -13,55 +13,56 @@ The Perry fixtures are diagnostic adaptations, not verbatim copies. They use eph | Perry area | Representative Node files | Deno selection | Bun copy | | --- | --- | --- | --- | | socket and send validation | `test-dgram-createSocket-type.js`, `test-dgram-send-bad-arguments.js`, `test-dgram-send-invalid-msg-type.js` | all 3 | send cases (2/3) | -| bind and close lifecycle | `test-dgram-bind.js`, `test-dgram-bind-default-address.js` | both | both | +| bind and close lifecycle | `test-dgram-bind.js`, `test-dgram-bind-error-repeat.js`, `test-dgram-close.js` | all 3 | all 3 | | AbortSignal | `test-dgram-close-signal.js`, `test-dgram-abort-closed.js` | both | both | | connection state | `test-dgram-connect.js` | selected | copied | | send overloads and byte counts | `test-dgram-send-callback-buffer.js`, `test-dgram-bytes-length.js`, `test-dgram-connect-send-callback-buffer.js` | all 3 | all 3 | | omitted host | `test-dgram-send-default-host.js`, `test-dgram-connect-send-default-host.js` | both | both | -| empty and multiple sends | `test-dgram-send-empty-buffer.js`, `test-dgram-implicit-bind.js` | both (empty buffer is Darwin-disabled/flaky) | both | +| empty and multiple sends | `test-dgram-send-empty-buffer.js`, `test-dgram-send-empty-array.js`, `test-dgram-implicit-bind.js` | all 3 (empty buffer is Darwin-disabled/flaky) | all 3 | | callback timing | `test-dgram-send-callback-recursive.js` | selected | copied | | queue and reference state | `test-dgram-send-queue-info.js`, `test-dgram-ref.js`, `test-dgram-unref.js` | ref/unref (2/3) | ref/unref (2/3) | | address and connected-send validation | `test-dgram-send-address-types.js`, `test-dgram-send-bad-arguments.js` | both | both | | legacy and advanced send forms | `test-dgram-sendto.js`, `test-dgram-send-callback-buffer-length.js`, `test-dgram-send-callback-multi-buffer.js` | all 3 | all 3 | | lookup, errors, and blocking | `test-dgram-custom-lookup.js`, `test-dgram-send-error.js`, `test-dgram-send-cb-quelches-error.js`, `test-dgram-blocklist.js` | lookup/error cases (3/4) | lookup/error cases (3/4) | | close, bind conflict, and disposal | `test-dgram-close-is-not-callback.js`, `test-dgram-bind-error-repeat.js`, `test-dgram-async-dispose.mjs` | all 3 | all 3 | -| controls and buffer metrics | `test-dgram-setTTL.js`, `test-dgram-multicast-setTTL.js`, `test-dgram-socket-buffer-size.js` | TTL cases (2/3) | TTL cases (2/3) | +| controls and buffer metrics | `test-dgram-setTTL.js`, `test-dgram-multicast-setTTL.js`, `test-dgram-socket-buffer-size.js`, `test-dgram-createSocket-type.js` | TTL/create options (3/4) | TTL cases (2/4) | | constructor message listener | `test-dgram-udp4.js` | selected | copied | ## Current coverage -The directory contains 31 fixtures: the original 4 broad cases and 27 granular cases added in this expansion. +The directory contains 38 fixtures: the original 4 broad cases and 34 granular cases added in this expansion. -- `validation/`: socket-type/options matrices, message/list/address validation, connected-send guards, DataView/scatter-gather acceptance, legacy `sendto()`, and port errors. -- `lifecycle/`: default/port/options bind overloads, bind conflicts, custom lookup, close arguments, async-dispose shape, AbortSignal validation, and post-close abort behavior. -- `connection/`: invalid ports, pending/connected state guards, disconnect errors, and reconnect state. -- `send/`: unconnected and connected overloads, constructor listeners, default address/host behavior, empty and multiple sends, buffer ranges, block lists, DNS error routing, callback byte counts, and callback asynchrony. -- `metrics/`: buffer-size validation, queue metrics, and `ref()`/`unref()` identity before bind, after bind, and after close. +- `validation/`: socket-type/options matrices, message/list/address validation, connected-send guards, DataView/scatter-gather delivery, legacy `sendto()`, port errors, and offset/length bounds. +- `lifecycle/`: default/port/options bind overloads, bind conflicts and retry state, custom lookup, close arguments and ordering, async-dispose shape, AbortSignal validation, and post-close abort behavior. +- `connection/`: invalid ports, pending/connected state guards, disconnect errors, reconnect state, and connect event/callback ordering. +- `send/`: unconnected and connected overloads, implicit binding, constructor listeners, default address/host behavior, empty buffers/arrays and multiple sends, buffer ranges, block lists, DNS error routing, callback byte counts, and callback asynchrony. +- `metrics/`: constructor and setter buffer sizes, queue metrics, and `ref()`/`unref()` identity before bind, after bind, and after close. - `control/`: deterministic TTL boundary validation. - Existing broad cases retain unicast loopback, import/API shape, socket controls, and multicast membership coverage. -The clean Node 26.5.0 focused baseline run is 16/31 parity passes with 15 stable output differences and no compile failures, runtime errors, or skipped fixtures. The differences diagnose: +The clean Node 26.5.0 focused baseline run is 18/38 parity passes with 20 stable output differences and no compile failures, runtime errors, or skipped fixtures. The differences diagnose: 1. Repeated `connect()` calls and connected range/destination overloads miss `ERR_SOCKET_DGRAM_IS_CONNECTED` guards. 2. Invalid `signal`, `lookup`, buffer-size, and address options/arguments are accepted. 3. Successful `send()` callbacks run synchronously. -4. `sendto()` lacks Node-coded argument errors, scatter-gather arrays are rejected, and buffer offset/length ranges are ignored. +4. `sendto()` lacks Node-coded argument errors, scatter-gather and empty arrays are rejected, and buffer offset/length bounds are ignored. 5. `Symbol.asyncDispose`, custom lookup dispatch, and send block lists are not implemented. -6. `close()` does not return the socket, an empty address is treated as a DNS name, and multicast TTL handles `Infinity` differently. +6. Constructor buffer sizes are ignored, failed binds leave `address()` reporting `EBADF`, and `close` callback/event ordering differs. +7. `close()` does not return the socket, an empty address is treated as a DNS name, and multicast TTL handles `Infinity` differently. -The work was exercised in coherent batches. The first pass covered core validation, bind/connect lifecycle, basic overloads, callbacks, queue/ref metrics, and AbortSignal. The follow-up added 16 cases selected from the larger Node/Deno/Bun corpora: address and advanced-option validation, legacy and connected send forms, custom lookup, close/dispose behavior, bind conflicts, block lists, DNS error routing, buffer metrics, TTL boundaries, constructor listeners, and message-view/scatter-gather behavior. +The work was exercised in coherent batches. The first pass covered core validation, bind/connect lifecycle, basic overloads, callbacks, queue/ref metrics, and AbortSignal. Later batches added 23 cases selected from the larger Node/Deno/Bun corpora: address and advanced-option validation, legacy and connected send forms, custom lookup, close/dispose behavior, bind conflicts and retries, block lists, DNS error routing, buffer metrics/options, TTL boundaries, constructor listeners, message-view/scatter-gather behavior, bounds validation, empty arrays, and implicit-bind state. ## Stopping judgment and exclusions Further upstream ports were stopped where they would duplicate the cases above or cross into a separate runtime/platform feature: -- **Separate runtime work:** Node's new `bindSync()`/`connectSync()`, descriptor/handle binding, active/pre-aborted signal closure, and exact repeated bind/close error semantics. Surface diagnostics for async disposal, block lists, custom lookup, ranges, and scatter-gather now remain in the suite. +- **Separate runtime work:** Node's new `bindSync()`/`connectSync()`, descriptor/handle binding, active/pre-aborted signal closure, and repeated bind/close states beyond the deterministic conflict/retry and ordering cases. Surface diagnostics for async disposal, block lists, custom lookup, ranges, and scatter-gather now remain in the suite. - **Platform/slow assessment:** `reusePort`, shared ports, cluster/child-process handle transfer, IPv6 loopback/link-local/interface-specific cases without a capability guard, multicast interface/loopback variants, and source-specific multicast beyond the existing smoke fixture. - **Kernel-sensitive errors:** message-size, out-of-band buffer, receive errors, implicit-bind failure, and address-specific OS error text. - **Scheduler-sensitive races:** close during bind/lookup/listening, recursive send callbacks, error-quelching races, burst close behavior, ping-pong stress, and unref/cluster process-liveness tests. - **Redundant upstream variants:** the many connected/unconnected callback, empty-packet, default-host, buffer, typed-array, and multiple-send files are represented by smaller grouped fixtures here. -These exclusions keep the default granular lane deterministic on loopback while preserving the 15 actionable Perry differences as focused regression targets. +These exclusions keep the default granular lane deterministic on loopback while preserving the 20 actionable Perry differences as focused regression targets. ## Verification @@ -69,7 +70,7 @@ These exclusions keep the default granular lane deterministic on loopback while NODE_BIN=/tmp/node-v26.5.0/bin/node \ python3 scripts/node_suite_run.py "$PWD/target/release/perry" "$PWD" dgram -dgram 16 31 51.6 diff=15 +dgram 18 38 47.4 diff=20 cargo fmt --all -- --check ./scripts/check_file_size.sh @@ -77,4 +78,4 @@ python3 -m json.tool test-parity/node_suite_baseline.json git diff --check ``` -No wider module run is required for this module floor: the executable changes are confined to `node-suite/dgram`, and the baseline runner was measured directly against all 31 dgram fixtures. Aggregate metadata remains the last clean full-suite snapshot. +No wider module run is required for this module floor: the executable changes are confined to `node-suite/dgram`, and the baseline runner was measured directly against all 38 dgram fixtures. Aggregate metadata remains the last clean full-suite snapshot. diff --git a/test-parity/node-suite/dgram/connection/connect-ordering.ts b/test-parity/node-suite/dgram/connection/connect-ordering.ts new file mode 100644 index 0000000000..42073b265a --- /dev/null +++ b/test-parity/node-suite/dgram/connection/connect-ordering.ts @@ -0,0 +1,24 @@ +import * as dgram from "node:dgram"; + +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", resolve)); + +const sender = dgram.createSocket("udp4"); +const order: string[] = []; +sender.once("connect", () => order.push("event")); + +await new Promise((resolve) => { + sender.connect(receiver.address().port, "127.0.0.1", () => { + order.push("callback"); + resolve(); + }); +}); + +const remote = sender.remoteAddress(); +console.log("connect order:", order.join(",")); +console.log("remote:", remote.address, remote.family, remote.port === receiver.address().port); + +await Promise.all([ + new Promise((resolve) => sender.close(resolve)), + new Promise((resolve) => receiver.close(resolve)), +]); diff --git a/test-parity/node-suite/dgram/lifecycle/bind-error-retry.ts b/test-parity/node-suite/dgram/lifecycle/bind-error-retry.ts new file mode 100644 index 0000000000..591c4b28c7 --- /dev/null +++ b/test-parity/node-suite/dgram/lifecycle/bind-error-retry.ts @@ -0,0 +1,32 @@ +import * as dgram from "node:dgram"; + +const owner = dgram.createSocket("udp4"); +await new Promise((resolve) => owner.bind(0, "127.0.0.1", resolve)); + +const candidate = dgram.createSocket("udp4"); +const results: string[] = []; + +for (let attempt = 0; attempt < 2; attempt++) { + const result = await new Promise((resolve) => { + candidate.once("error", (value) => { + resolve(`${value.code}:${value.syscall}`); + }); + candidate.bind(owner.address().port, "127.0.0.1", () => resolve("listening")); + }); + results.push(result); + if (result === "listening") break; +} + +console.log("retry results:", results.join(",")); +let addressState: string; +try { + addressState = `port-${candidate.address().port}`; +} catch (error: unknown) { + addressState = (error as { code?: string }).code ?? "Error"; +} +console.log("address after errors:", addressState); + +await Promise.all([ + new Promise((resolve) => owner.close(resolve)), + new Promise((resolve) => candidate.close(resolve)), +]); diff --git a/test-parity/node-suite/dgram/lifecycle/close-ordering.ts b/test-parity/node-suite/dgram/lifecycle/close-ordering.ts new file mode 100644 index 0000000000..0c1ebb3080 --- /dev/null +++ b/test-parity/node-suite/dgram/lifecycle/close-ordering.ts @@ -0,0 +1,17 @@ +import * as dgram from "node:dgram"; + +const socket = dgram.createSocket("udp4"); +await new Promise((resolve) => socket.bind(0, "127.0.0.1", resolve)); + +const order: string[] = []; +const closed = new Promise((resolve) => { + socket.once("close", () => { + order.push("event"); + resolve(); + }); +}); + +socket.close(() => order.push("callback")); +await closed; +await Promise.resolve(); +console.log("close order:", order.join(",")); diff --git a/test-parity/node-suite/dgram/metrics/create-socket-buffer-options.ts b/test-parity/node-suite/dgram/metrics/create-socket-buffer-options.ts new file mode 100644 index 0000000000..dbffa0ed0c --- /dev/null +++ b/test-parity/node-suite/dgram/metrics/create-socket-buffer-options.ts @@ -0,0 +1,15 @@ +import * as dgram from "node:dgram"; + +const socket = dgram.createSocket({ + type: "udp4", + recvBufferSize: 10_000, + sendBufferSize: 15_000, +}); +await new Promise((resolve) => socket.bind(0, "127.0.0.1", resolve)); + +const recvSize = socket.getRecvBufferSize(); +const sendSize = socket.getSendBufferSize(); +console.log("receive option applied:", recvSize === 10_000 || recvSize === 20_000); +console.log("send option applied:", sendSize === 15_000 || sendSize === 30_000); + +await new Promise((resolve) => socket.close(resolve)); diff --git a/test-parity/node-suite/dgram/send/empty-array.ts b/test-parity/node-suite/dgram/send/empty-array.ts new file mode 100644 index 0000000000..92d547af0e --- /dev/null +++ b/test-parity/node-suite/dgram/send/empty-array.ts @@ -0,0 +1,35 @@ +import * as dgram from "node:dgram"; + +function codeOf(fn: () => unknown): string { + try { + fn(); + return "none"; + } catch (error: unknown) { + return (error as { code?: string; name?: string }).code ?? + (error as { name?: string }).name ?? "Error"; + } +} + +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", resolve)); +const sender = dgram.createSocket("udp4"); + +let finishCallback: (value: string) => void = () => {}; +const callback = new Promise((resolve) => { + finishCallback = resolve; +}); +const result = codeOf(() => { + sender.send([], receiver.address().port, "127.0.0.1", (error, bytes) => { + finishCallback(`${error === null}:${bytes}`); + }); +}); + +console.log("empty array accepted:", result); +if (result === "none") { + console.log("empty array callback:", await callback); +} + +await Promise.all([ + new Promise((resolve) => sender.close(resolve)), + new Promise((resolve) => receiver.close(resolve)), +]); diff --git a/test-parity/node-suite/dgram/send/implicit-bind-state.ts b/test-parity/node-suite/dgram/send/implicit-bind-state.ts new file mode 100644 index 0000000000..71468fcd94 --- /dev/null +++ b/test-parity/node-suite/dgram/send/implicit-bind-state.ts @@ -0,0 +1,39 @@ +import * as dgram from "node:dgram"; + +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", resolve)); + +const first = dgram.createSocket("udp4"); +const second = dgram.createSocket("udp4"); +const received: string[] = []; +const messages = new Promise((resolve) => { + receiver.on("message", (message) => { + received.push(message.toString()); + if (received.length === 2) resolve(); + }); +}); + +await Promise.all([ + new Promise((resolve, reject) => { + first.send("first", receiver.address().port, "127.0.0.1", (error) => + error ? reject(error) : resolve() + ); + }), + new Promise((resolve, reject) => { + second.send("second", receiver.address().port, "127.0.0.1", (error) => + error ? reject(error) : resolve() + ); + }), + messages, +]); + +const firstAddress = first.address(); +const secondAddress = second.address(); +console.log("messages:", received.sort().join(",")); +console.log("families:", firstAddress.family, secondAddress.family); +console.log("ports assigned:", firstAddress.port > 0, secondAddress.port > 0); +console.log("ports distinct:", firstAddress.port !== secondAddress.port); + +await new Promise((resolve) => first.close(resolve)); +await new Promise((resolve) => second.close(resolve)); +await new Promise((resolve) => receiver.close(resolve)); diff --git a/test-parity/node-suite/dgram/validation/buffer-bounds.ts b/test-parity/node-suite/dgram/validation/buffer-bounds.ts new file mode 100644 index 0000000000..6ec67a8ba2 --- /dev/null +++ b/test-parity/node-suite/dgram/validation/buffer-bounds.ts @@ -0,0 +1,37 @@ +import * as dgram from "node:dgram"; + +function codeOf(fn: () => unknown): string { + try { + fn(); + return "none"; + } catch (error: unknown) { + return (error as { code?: string; name?: string }).code ?? + (error as { name?: string }).name ?? "Error"; + } +} + +const socket = dgram.createSocket("udp4"); +await new Promise((resolve) => socket.bind(0, "127.0.0.1", resolve)); +const receiver = dgram.createSocket("udp4"); +await new Promise((resolve) => receiver.bind(0, "127.0.0.1", resolve)); +const message = Buffer.from("hello"); +const callback = () => {}; +const port = receiver.address().port; + +console.log( + "offset:", + codeOf(() => socket.send(message, 6, 0, port, "127.0.0.1", callback)), +); +console.log( + "length:", + codeOf(() => socket.send(message, 0, 6, port, "127.0.0.1", callback)), +); +console.log( + "combined:", + codeOf(() => socket.send(message, 3, 4, port, "127.0.0.1", callback)), +); + +await Promise.all([ + new Promise((resolve) => socket.close(resolve)), + new Promise((resolve) => receiver.close(resolve)), +]); diff --git a/test-parity/node-suite/dgram/validation/message-views.ts b/test-parity/node-suite/dgram/validation/message-views.ts index 76bff6b73a..75aa061f47 100644 --- a/test-parity/node-suite/dgram/validation/message-views.ts +++ b/test-parity/node-suite/dgram/validation/message-views.ts @@ -30,6 +30,9 @@ let finishScatter: (value: string) => void = () => {}; const scatterCallback = new Promise((resolve) => { finishScatter = resolve; }); +const scatterReceived = new Promise((resolve) => { + receiver.once("message", (message) => resolve(message.toString())); +}); const scatterResult = codeOf(() => { sender.send([Buffer.from("a"), "b"], receiver.address().port, "127.0.0.1", (error, bytes) => { finishScatter(`${error === null}:${bytes}`); @@ -37,7 +40,7 @@ const scatterResult = codeOf(() => { }); console.log("scatter accepted:", scatterResult); if (scatterResult === "none") { - console.log("scatter callback:", await scatterCallback); + console.log("scatter delivery:", await scatterReceived, await scatterCallback); } await Promise.all([ diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index bcfa755880..fc3b9c5a3f 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -47,8 +47,8 @@ "total": 242 }, "dgram": { - "pass": 16, - "total": 31 + "pass": 18, + "total": 38 }, "diagnostics_channel": { "pass": 66, From d26023d843679cd00561e21e0ee3187572b113af Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 16 Jul 2026 08:53:53 +0200 Subject: [PATCH 4/4] test: type dgram bind errors --- test-parity/node-suite/dgram/lifecycle/bind-error-retry.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/test-parity/node-suite/dgram/lifecycle/bind-error-retry.ts b/test-parity/node-suite/dgram/lifecycle/bind-error-retry.ts index 591c4b28c7..06977b3d0a 100644 --- a/test-parity/node-suite/dgram/lifecycle/bind-error-retry.ts +++ b/test-parity/node-suite/dgram/lifecycle/bind-error-retry.ts @@ -9,7 +9,8 @@ const results: string[] = []; for (let attempt = 0; attempt < 2; attempt++) { const result = await new Promise((resolve) => { candidate.once("error", (value) => { - resolve(`${value.code}:${value.syscall}`); + const error = value as Error & { code?: string; syscall?: string }; + resolve(`${error.code}:${error.syscall}`); }); candidate.bind(owner.address().port, "127.0.0.1", () => resolve("listening")); });