From d80b34d92dc735392a46afa35545a8a19cfe622d Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 22:18:16 -0500 Subject: [PATCH 1/2] Match Node's socket encodings, rejected writes and event loop liveness --- .../limitations/socket-data-encoding.mdx | 2 +- .../generated/node-v24-internal.json | 279 +++++++++++++++--- internal/compatibility/static-support.json | 2 +- .../corpus/net-paused-socket-exit.cjs.json | 6 + .../net-server-socket-peer-close.cjs.json | 6 + .../corpus/net-socket-method-values.cjs.json | 6 + .../net-socket-set-encoding-names.ts.json | 6 + .../corpus/net-socket-write-rejected.cjs.json | 6 + packages/runtime/src/scr_dyn.h | 1 + packages/runtime/src/scr_json.c | 5 + packages/runtime/src/scr_net.c | 253 +++++++++++++--- tests/corpus/net-paused-socket-exit.cjs | 24 ++ tests/corpus/net-server-socket-peer-close.cjs | 29 ++ tests/corpus/net-socket-method-values.cjs | 41 +++ tests/corpus/net-socket-set-encoding-names.ts | 46 +++ tests/corpus/net-socket-write-rejected.cjs | 76 +++++ .../net-paused-socket-exit.cjs.json | 1 + .../net-server-socket-peer-close.cjs.json | 1 + .../net-socket-method-values.cjs.json | 1 + .../net-socket-set-encoding-names.ts.json | 1 + .../net-socket-write-rejected.cjs.json | 1 + tests/harness/node-parallel-runner.test.ts | 13 +- tests/node-parallel/README.md | 2 +- tests/node-parallel/common/index.cjs | 50 ++++ tests/node-parallel/common/tmpdir.cjs | 36 +++ tests/node-parallel/run.mjs | 7 +- 26 files changed, 816 insertions(+), 85 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-paused-socket-exit.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-server-socket-peer-close.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-method-values.cjs.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-set-encoding-names.ts.json create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-write-rejected.cjs.json create mode 100644 tests/corpus/net-paused-socket-exit.cjs create mode 100644 tests/corpus/net-server-socket-peer-close.cjs create mode 100644 tests/corpus/net-socket-method-values.cjs create mode 100644 tests/corpus/net-socket-set-encoding-names.ts create mode 100644 tests/corpus/net-socket-write-rejected.cjs create mode 100644 tests/harness/corpus-costs/net-paused-socket-exit.cjs.json create mode 100644 tests/harness/corpus-costs/net-server-socket-peer-close.cjs.json create mode 100644 tests/harness/corpus-costs/net-socket-method-values.cjs.json create mode 100644 tests/harness/corpus-costs/net-socket-set-encoding-names.ts.json create mode 100644 tests/harness/corpus-costs/net-socket-write-rejected.cjs.json create mode 100644 tests/node-parallel/common/tmpdir.cjs diff --git a/docs/content/limitations/socket-data-encoding.mdx b/docs/content/limitations/socket-data-encoding.mdx index 4384dd97f..955857607 100644 --- a/docs/content/limitations/socket-data-encoding.mdx +++ b/docs/content/limitations/socket-data-encoding.mdx @@ -1,3 +1,3 @@ -After `socket.setEncoding("utf8")`, an inline `'data'` listener receives strings, as in Node, whatever type its chunk parameter declares. +After `socket.setEncoding()`, an inline `'data'` listener receives strings, as in Node, whatever type its chunk parameter declares. A listener passed by reference, such as a named function whose parameter is typed `Buffer`, keeps its `Buffer` parameter, so a string chunk throws `TypeError: expected Uint8Array at $[0], got string`. Declare that parameter as `string` or `string | Buffer`, or pass an inline function. diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 5e5cc8c4c..961b40bd4 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -54788,6 +54788,11 @@ "tests/corpus/2986-http-static-client-socket-finish.ts", "tests/corpus/2987-http-static-timeout-errors.ts", "tests/corpus/2988-http-static-client-abort.ts", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", "tests/fixtures/server/cases/http-trailers/main.ts", "tests/fixtures/server/cases/http2-connect/main.ts", "tests/fixtures/server/cases/upgrade-both/main.ts" @@ -56336,7 +56341,12 @@ "evidence": "compiler-dedicated:server.close", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -56419,7 +56429,12 @@ "evidence": "compiler-dedicated:server.listen", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -59448,7 +59463,12 @@ "tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs", "tests/corpus/2672-http-request-response-callback.ts", - "tests/corpus/2689-http-server-timeout-properties.ts" + "tests/corpus/2689-http-server-timeout-properties.ts", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs" ] }, "anchorSource": "chapter" @@ -61706,7 +61726,12 @@ "evidence": "compiler-dedicated:server.close", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -62014,7 +62039,12 @@ "evidence": "compiler-dedicated:server.close", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -63842,7 +63872,12 @@ "tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs", "tests/corpus/2672-http-request-response-callback.ts", - "tests/corpus/2689-http-server-timeout-properties.ts" + "tests/corpus/2689-http-server-timeout-properties.ts", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs" ] }, "anchorSource": "chapter" @@ -63938,7 +63973,12 @@ "evidence": "compiler-dedicated:server.close", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -64034,7 +64074,12 @@ "evidence": "compiler-dedicated:server.listen", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -72029,6 +72074,11 @@ "tests/corpus/2672-http-request-response-callback.ts", "tests/corpus/2689-http-server-timeout-properties.ts", "tests/corpus/5659-net-blocklist-socketaddress.ts", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", "tests/fixtures/node-types/net-blocklist.ts" ] }, @@ -72637,7 +72687,12 @@ "evidence": "compiler-dedicated:server.address", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -72659,7 +72714,12 @@ "evidence": "compiler-dedicated:server.close", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -72717,7 +72777,12 @@ "evidence": "compiler-dedicated:server.listen", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -72739,7 +72804,12 @@ "evidence": "compiler-dedicated:server.listen", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -72761,7 +72831,12 @@ "evidence": "compiler-dedicated:server.listen", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -72783,7 +72858,12 @@ "evidence": "compiler-dedicated:server.listen", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -72805,7 +72885,12 @@ "evidence": "compiler-dedicated:server.listen", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -73148,7 +73233,12 @@ "evidence": "compiler-dedicated:socket.destroy", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -73188,7 +73278,12 @@ "evidence": "compiler-dedicated:socket.end", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -73210,7 +73305,12 @@ "evidence": "compiler-dedicated:socket.pause", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -73271,7 +73371,12 @@ "evidence": "compiler-dedicated:socket.resume", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -73293,7 +73398,12 @@ "evidence": "compiler-dedicated:socket.setEncoding", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -73387,7 +73497,12 @@ "evidence": "compiler-dedicated:socket.setTimeout", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -73466,7 +73581,12 @@ "evidence": "compiler-dedicated:socket.write", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -74110,7 +74230,12 @@ "evidence": "compiler-dedicated:net.connect", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -74132,7 +74257,12 @@ "evidence": "compiler-dedicated:net.connect", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -74154,7 +74284,12 @@ "evidence": "compiler-dedicated:net.connect", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -74176,7 +74311,12 @@ "evidence": "compiler-dedicated:net.connect", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -74198,7 +74338,12 @@ "evidence": "compiler-dedicated:net.createConnection", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -74220,7 +74365,12 @@ "evidence": "compiler-dedicated:net.createConnection", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -74242,7 +74392,12 @@ "evidence": "compiler-dedicated:net.createConnection", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -74264,7 +74419,12 @@ "evidence": "compiler-dedicated:net.createConnection", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -74286,7 +74446,12 @@ "evidence": "compiler-dedicated:net.createServer", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -74344,7 +74509,12 @@ "evidence": "compiler-dedicated:net.getDefaultAutoSelectFamilyAttemptTimeout", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -74366,7 +74536,12 @@ "evidence": "compiler-dedicated:net.setDefaultAutoSelectFamilyAttemptTimeout", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -75823,6 +75998,11 @@ "tests/corpus/5214-perf-entry-inspect.ts", "tests/corpus/5215-perf-timing-validation.js", "tests/corpus/6397-event-loop-metrics.ts", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", "tests/fixtures/node-types/event-loop-metrics.ts" ] }, @@ -76126,7 +76306,12 @@ "evidence": "compiler-dedicated:net.connect", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -96067,6 +96252,11 @@ "tests/corpus/7440-tls-server-add-context.ts", "tests/corpus/7441-tls-client-error.ts", "tests/corpus/7442-tls-server-contexts.mjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", "tests/fixtures/node-types/tls-client-certificates.ts", "tests/fixtures/node-types/tls-peer-certificates.ts", "tests/fixtures/node-types/tls-server-contexts.ts", @@ -96471,7 +96661,12 @@ "evidence": "compiler-dedicated:server.address", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -96498,7 +96693,12 @@ "evidence": "compiler-dedicated:server.close", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" @@ -96548,7 +96748,12 @@ "evidence": "compiler-dedicated:server.listen", "tests": [ "tests/corpus/2500-net-autosel-timeout.ts", - "tests/corpus/2596-net-arg-ladders.cjs" + "tests/corpus/2596-net-arg-ladders.cjs", + "tests/corpus/net-socket-set-encoding-names.ts", + "tests/corpus/net-socket-write-rejected.cjs", + "tests/corpus/net-socket-method-values.cjs", + "tests/corpus/net-paused-socket-exit.cjs", + "tests/corpus/net-server-socket-peer-close.cjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 3918a8941..2d74d4de5 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -479,7 +479,7 @@ { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, { "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/5302-stream-promises-options.ts", "tests/corpus/5303-stream-finished-callback-opts.cjs", "tests/corpus/5425-pipeline-generator-stage.ts", "tests/corpus/5428-pipeline-generator-stages.cjs", "tests/corpus/5617-pipeline-function-ends.ts", "tests/corpus/5619-pipeline-function-ends.cjs", "tests/corpus/writable-error-callbacks.ts"] }, - { "symbols": ["net.createServer", "net.connect", "net.createConnection", "net.getDefaultAutoSelectFamilyAttemptTimeout", "net.setDefaultAutoSelectFamilyAttemptTimeout", "server.address", "server.close", "server.listen", "socket.destroy", "socket.end", "socket.pause", "socket.resume", "socket.setEncoding", "socket.setTimeout", "socket.write"], "status": "partial", "evidence": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs"] }, + { "symbols": ["net.createServer", "net.connect", "net.createConnection", "net.getDefaultAutoSelectFamilyAttemptTimeout", "net.setDefaultAutoSelectFamilyAttemptTimeout", "server.address", "server.close", "server.listen", "socket.destroy", "socket.end", "socket.pause", "socket.resume", "socket.setEncoding", "socket.setTimeout", "socket.write"], "status": "partial", "evidence": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs", "tests/corpus/net-socket-set-encoding-names.ts", "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", "tests/corpus/net-server-socket-peer-close.cjs"] }, { "symbols": ["dgram.createSocket", "socket.address", "socket.bind", "socket.close", "socket.connect", "socket.ref", "socket.remoteAddress", "socket.send", "socket.unref"], "status": "partial", "evidence": ["tests/corpus/2597-dgram-send-ladders.cjs"] }, { "symbols": ["net.SocketAddress", "net.BlockList"], "status": "partial", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] }, { "symbols": ["SocketAddress.parse", "BlockList.isBlockList", "blockList.addAddress", "blockList.addAddresses", "blockList.addCIDR", "blockList.addCIDRs", "blockList.addRange", "blockList.addSubnet", "blockList.check", "blockList.clear", "blockList.fromJSON", "blockList.removeAddress", "blockList.removeCIDR", "blockList.removeRange", "blockList.removeSubnet", "blockList.toJSON"], "status": "supported", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-paused-socket-exit.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-paused-socket-exit.cjs.json new file mode 100644 index 000000000..62eabb179 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-paused-socket-exit.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/net-paused-socket-exit.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-server-socket-peer-close.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-server-socket-peer-close.cjs.json new file mode 100644 index 000000000..27d486e24 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-server-socket-peer-close.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/net-server-socket-peer-close.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-method-values.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-method-values.cjs.json new file mode 100644 index 000000000..6b758ed60 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-method-values.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/net-socket-method-values.cjs" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-set-encoding-names.ts.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-set-encoding-names.ts.json new file mode 100644 index 000000000..1add896ba --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-set-encoding-names.ts.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/net-socket-set-encoding-names.ts" + ], + "diags": [] +} diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-write-rejected.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-write-rejected.cjs.json new file mode 100644 index 000000000..3605e422c --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-write-rejected.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/net-socket-write-rejected.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_dyn.h b/packages/runtime/src/scr_dyn.h index 2bc1d9a31..b1689bd69 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -2304,6 +2304,7 @@ ScrDyn *scr_gen_receiver(void); * firing site opens/closes it around a 'data' pass; scr_dyn_new_chunk * answers a Buffer-flavored bytes box, or a string inside the window. */ void scr_dyn_chunk_enc(bool utf8); +void scr_dyn_chunk_text(ScrStr *text); /* a decoded string instead; borrowed, NULL closes */ ScrDyn *scr_dyn_new_chunk(const ScrBytes *b); /* The JSON text of a dyn value: the JSON.stringify text (+1), diff --git a/packages/runtime/src/scr_json.c b/packages/runtime/src/scr_json.c index 090bfbfc8..4c42d054b 100644 --- a/packages/runtime/src/scr_json.c +++ b/packages/runtime/src/scr_json.c @@ -6430,10 +6430,15 @@ ScrDyn *scr_dyn_bytes_key_get(const ScrDyn *value, const ScrStr *key) { * bodies — the overwhelming test shape — are exact. */ static SCR_TL bool scr_dyn_chunk_utf8; void scr_dyn_chunk_enc(bool utf8) { scr_dyn_chunk_utf8 = utf8; } +/* A firing site that decodes with a StringDecoder (net sockets) names + * the decoded string instead (borrowed; NULL closes the window). */ +static SCR_TL ScrStr *scr_dyn_chunk_str; +void scr_dyn_chunk_text(ScrStr *text) { scr_dyn_chunk_str = text; } /* One 'data' payload as the dyn value the current window dictates: * a Buffer-flavored bytes box, or a string inside a setEncoding window. */ ScrDyn *scr_dyn_new_chunk(const ScrBytes *b) { + if (scr_dyn_chunk_str != NULL) return scr_dyn_new_str(scr_dyn_chunk_str); if (scr_dyn_chunk_utf8) { ScrStr *s = scr_bytes_string(b, 0, b->len); ScrDyn *d = scr_dyn_new_str(s); diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 9347bdc9b..2c9793d0b 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -506,7 +506,12 @@ struct ScrNetSocket { bool unrefd; /* socket.unref(): does not keep the loop alive */ bool connecting; bool rd_eof; /* peer FIN seen (or read half abandoned) */ - bool enc_utf8; /* setEncoding('utf8'): 'data' delivers strings */ + bool fin_seen; /* peer FIN seen: 'end' fired (Node's writeAfterFIN state) */ + /* setEncoding(enc): the canonical encoding (+1, NULL without one) and the + * StringDecoder's packed partial sequence (scr_strdec_*). 'data' then + * delivers the decoded strings. */ + ScrStr *encoding; + double dec_pending; bool wr_ending; /* end() called: FIN when the buffer drains */ bool wr_done; /* FIN sent (or write half dead) */ bool had_error; @@ -604,6 +609,10 @@ struct ScrNetSocket { * finish_ls waits for the FIN (wr_done); wcb_ls waits for the write * buffer to drain (this surface's flush moment). */ ScrNetLs finish_ls, wcb_ls; + /* The error of the last user write when the socket's state rejected it + * (static text, NULL after an accepted write): that write's callback + * receives it on the next tick. */ + const char *write_err_code, *write_err_msg; bool finish_pending; /* wr_done seen: fire finish_ls at the next sweep */ ScrNetLs ready_ls; /* 'ready': right after the client's 'connect' listeners */ size_t bytes_read; /* socket.bytesRead: every byte delivered from the peer */ @@ -777,6 +786,7 @@ void scr_net_sock_release(ScrNetSocket *s) { scr_als_ctx_release(s->als); scr_als_ctx_release(s->end_ctx); scr_str_release(s->pending_err); + scr_str_release(s->encoding); scr_str_release(s->remote_cache); for (size_t i = 0; i < s->dial_n; i++) scr_str_release(s->dial_ips[i]); free(s->dial_ips); @@ -1325,11 +1335,45 @@ static bool scr_net_sock_write_open(ScrNetSocket *w) { * is set. A write the kernel takes at once answers true whatever its * size. A write to an ended or closed socket answers false, and so does * one whose send fails and closes the socket. */ +/* A user write the socket's state rejects, as Node's Socket.write and + * Writable.write answer it: after the peer's FIN (Node's writeAfterFIN, + * allowHalfOpen false) EPIPE "This socket has been ended by the other + * party"; after end() ERR_STREAM_WRITE_AFTER_END; after destroy() + * ERR_STREAM_DESTROYED. The write's callback receives the error on the + * next tick. The first two destroy a socket that is not destroyed yet with + * the error, so 'error' and 'close' follow. */ +static void scr_net_sock_write_rejected(ScrNetSocket *w) { + bool destroy = true; + if (w->fin_seen) { + w->write_err_code = "EPIPE"; + w->write_err_msg = "This socket has been ended by the other party"; + } else if (w->wr_ending) { + w->write_err_code = "ERR_STREAM_WRITE_AFTER_END"; + w->write_err_msg = "write after end"; + } else { + w->write_err_code = "ERR_STREAM_DESTROYED"; + w->write_err_msg = "Cannot call write after a stream was destroyed"; + destroy = false; + } + if (!destroy || w->fd < 0 || w->close_emitted) return; + if (!w->pending_err) { + w->pending_err = scr_str_new(w->write_err_msg, strlen(w->write_err_msg)); + w->pending_code = w->write_err_code; + } + w->had_error = true; + scr_net_sock_close_fd(w); +} + static bool scr_net_sock_user_write(ScrNetSocket *s, const char *data, size_t len) { ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s; + w->write_err_code = w->write_err_msg = NULL; bool open = scr_net_sock_write_open(w); + if (!open) { + scr_net_sock_write_rejected(w); + return false; + } scr_net_sock_write_raw(s, data, len); - if (!open || !scr_net_sock_write_open(w)) return false; + if (!scr_net_sock_write_open(w)) return false; bool ok = w->wpending < SCR_NET_WRITE_HWM; if (!ok) w->need_drain = true; return ok; @@ -1491,11 +1535,39 @@ static void scr_net_sock_flush(ScrNetSocket *s) { } } +/* The decoded string of the 'data' emit in progress (borrowed, NULL for a + * Buffer emit): the string-typed listener adapter answers it. */ +static SCR_TL ScrStr *scr_net_chunk_text = NULL; + +/* One 'data' emit to the listeners: the chunk as a Buffer, or `text` (the + * decoded string after setEncoding). Exactly one of chunk and text is + * non-NULL. */ +static void scr_net_sock_fire_data(ScrNetSocket *s, ScrBytes *chunk, ScrStr *text) { + ScrBytes *empty = NULL; + if (chunk == NULL) chunk = empty = scr_bytes_new(SCR_BYTES_U8, 0); + ScrNetL *snap; + size_t nl = scr_net_ls_snapshot(&s->data_ls, &snap); + scr_dyn_this_push(s, SCR_DYNH_NET_SOCKET); + ScrStr *outer = scr_net_chunk_text; + scr_net_chunk_text = text; + scr_dyn_chunk_text(text); + for (size_t i = 0; i < nl; i++) { + if (!scr_exc_pending()) ((ScrNetDataFn)snap[i].fn)(snap[i].cb, chunk); + scr_closure_release(snap[i].cb); + } + scr_dyn_chunk_text(NULL); + scr_net_chunk_text = outer; + scr_dyn_this_pop(); + free(snap); + scr_bytes_release(empty); +} + /* EOF: 'end' fires now (directly — Node's poll-phase emit), then the * write half ends (allowHalfOpen false), and the fd closes once the FIN * is out. 'close' waits for the sweep. */ static void scr_net_sock_eof(ScrNetSocket *s) { s->rd_eof = true; + s->fin_seen = true; scr_net_sock_update_read(s); if (s->native_eof) { s->native_eof(s->native_ctx); @@ -1509,6 +1581,14 @@ static void scr_net_sock_eof(ScrNetSocket *s) { scr_net_sock_maybe_finish_write(dst); } } + if (s->encoding != NULL && s->data_ls.n > 0) { + /* the decoder's end(): a held partial sequence is the last 'data' */ + ScrStr *tail = scr_strdec_end(s->encoding, s->dec_pending); + s->dec_pending = 0; + if (tail->len > 0) scr_net_sock_fire_data(s, NULL, tail); + scr_str_release(tail); + if (scr_exc_pending()) return; + } scr_net_fire0_this(&s->end_ls, s, SCR_DYNH_NET_SOCKET); if (scr_exc_pending()) return; if (!s->wr_ending) { @@ -1536,17 +1616,16 @@ static bool scr_net_sock_deliver(ScrNetSocket *s, const char *buf, size_t n) { ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n); chunk->is_buffer = true; /* Node delivers a Buffer */ memcpy(chunk->data, buf, n); - ScrNetL *snap; - size_t nl = scr_net_ls_snapshot(&s->data_ls, &snap); - scr_dyn_this_push(s, SCR_DYNH_NET_SOCKET); - scr_dyn_chunk_enc(s->enc_utf8); - for (size_t i = 0; i < nl; i++) { - if (!scr_exc_pending()) ((ScrNetDataFn)snap[i].fn)(snap[i].cb, chunk); - scr_closure_release(snap[i].cb); + if (s->encoding != NULL) { + /* the decoder answers the complete characters; a partial sequence + * waits for the next chunk, and an empty answer emits nothing */ + ScrStr *text = scr_strdec_write(s->encoding, s->dec_pending, chunk); + s->dec_pending = scr_strdec_next(s->encoding, s->dec_pending, chunk); + if (text->len > 0) scr_net_sock_fire_data(s, NULL, text); + scr_str_release(text); + } else { + scr_net_sock_fire_data(s, chunk, NULL); } - scr_dyn_chunk_enc(false); - scr_dyn_this_pop(); - free(snap); scr_bytes_release(chunk); if (scr_exc_pending()) return false; } @@ -2577,6 +2656,9 @@ static void scr_net_server_accept(ScrNetServer *srv) { } scr_dyn_this_pop(); free(snap); + /* With no consumer yet the socket still reads ahead, as Node's + * stream does from the start, so a peer's FIN or reset is seen. */ + if (!srv->native_conn && !scr_exc_pending()) scr_net_sock_update_read(sock); } scr_net_sock_release(sock); /* registry + server-count refs remain */ if (scr_exc_pending()) return; @@ -3625,6 +3707,11 @@ static void scr_net_sock_add_write_cb(ScrNetSocket *s, ScrClosure *cb, void *fn) /* write(chunk, callback): fires when the write buffer drains (this * surface's flush moment), from the sweep. */ void scr_net_sock_on_write_flush(ScrNetSocket *s, ScrClosure *cb /*moves*/) { + ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s; + if (w->write_err_code != NULL) { + scr_next_tick(cb); /* the rejected write's callback */ + return; + } if (s->close_emitted) { scr_closure_release(cb); return; @@ -5029,7 +5116,33 @@ void scr_net_server_close_dyn(ScrNetServer *s, const ScrDyn *listener /*borrowed scr_net_server_close(s, cb); } +/* The rejected write's callback: callback(error) on the next tick. */ +static void scr_net_write_err_fire(ScrClosure *self) { + ScrDyn *fn = (ScrDyn *)scr_box_get_ref(self->caps[0]); + ScrDyn *err = (ScrDyn *)scr_box_get_ref(self->caps[1]); + ScrDyn *args[1] = { err }; + scr_dyn_release(scr_dyn_call(fn, args, 1, "callback")); + scr_dyn_release(err); + scr_dyn_release(fn); +} + void scr_net_sock_on_write_flush_dyn(ScrNetSocket *s, const ScrDyn *listener /*borrowed*/) { + ScrNetSocket *w = s->adopted_by != NULL ? s->adopted_by : s; + if (w->write_err_code != NULL) { + ScrStr *msg = scr_str_new(w->write_err_msg, strlen(w->write_err_msg)); + ScrError *e = scr_error_new(SCR_ERR_ERROR, msg); + scr_str_release(msg); + scr_error_set_code(e, w->write_err_code); + ScrClosure *clo = scr_dyn_listener_closure_fn(listener, (void *)&scr_net_write_err_fire); + ScrClosure *job = scr_closure_new((void *)&scr_net_write_err_fire, 2); + job->caps[0] = scr_box_retain(clo->caps[0]); + job->caps[1] = scr_box_new_obj(scr_dyn_retain_v, scr_dyn_release_v, scr_dyn_trace_v); + scr_box_set_ref(job->caps[1], scr_dyn_from_error(e)); + scr_error_release(e); + scr_closure_release(clo); + scr_next_tick(job); + return; + } if (s->close_emitted) return; scr_net_sock_add_write_cb(s, scr_dyn_listener_closure_fn(listener, (void *)&scr_net_dyn_fire_arg), (void *)&scr_net_dyn_fire_arg); @@ -5270,31 +5383,17 @@ void scr_net_server_emit_connection(ScrNetServer *srv, ScrNetSocket *sock) { * firing server-level events ('request': Node's this === server). */ ScrNetServer *scr_net_sock_server(ScrNetSocket *s) { return s->server; } -/* socket.setEncoding(enc) — the req twin (scr_http_req_set_encoding's - * contract: utf8 flips 'data' to strings, real-but-unsupported encodings - * throw the 'not supported yet' Error, unknown names throw - * ERR_UNKNOWN_ENCODING). */ +/* socket.setEncoding(enc): Node's Readable.setEncoding with a new + * StringDecoder — any Buffer encoding name in any ASCII case; an unknown + * name throws ERR_UNKNOWN_ENCODING. 'data' then delivers the decoded + * strings, and a partial sequence held at EOF is the last 'data'. */ void scr_net_sock_set_encoding(ScrNetSocket *s, ScrStr *enc /*borrowed*/) { - if ((enc->len == 4 && memcmp(enc->data, "utf8", 4) == 0) || - (enc->len == 5 && memcmp(enc->data, "utf-8", 5) == 0)) { - s->enc_utf8 = true; - return; - } - static const char *const known[] = { "ascii", "latin1", "binary", "base64", - "base64url", "hex", "ucs2", "ucs-2", "utf16le", "utf-16le", NULL }; - for (size_t i = 0; known[i]; i++) { - if (enc->len == strlen(known[i]) && memcmp(enc->data, known[i], enc->len) == 0) { - char msg[128]; - int n = snprintf(msg, sizeof msg, "setEncoding('%s') is not supported yet (only 'utf8' here)", - known[i]); - scr_throw_error_msg(SCR_ERR_ERROR, msg, (size_t)n); - return; - } - } - char msg[128]; - int n = snprintf(msg, sizeof msg, "Unknown encoding: %.*s", - (int)(enc->len < 64 ? enc->len : 64), enc->data); - scr_throw_error_msg_code(SCR_ERR_TYPE, msg, (size_t)n, "ERR_UNKNOWN_ENCODING"); + /* Node's normalizeEncoding answers utf8 for the empty name */ + ScrStr *canonical = enc->len == 0 ? scr_str_new("utf8", 4) : scr_bytes_normalize_encoding(enc); + if (canonical == NULL) return; /* ERR_UNKNOWN_ENCODING pending */ + scr_str_release(s->encoding); + s->encoding = canonical; + s->dec_pending = 0; } /* pipe(dst): every arrived chunk forwards to dst; src EOF end()s dst @@ -5382,6 +5481,10 @@ void scr_net_data_thunk_dyn(ScrClosure *cb, ScrBytes *chunk) { * a string-typed listener there mistypes its own input, and the decode * is the honest reading of the annotation. */ void scr_net_data_thunk_str(ScrClosure *cb, ScrBytes *chunk) { + if (scr_net_chunk_text != NULL) { + ((void (*)(ScrClosure *, ScrStr *))cb->fn)(cb, scr_str_retain(scr_net_chunk_text)); + return; + } ScrStr *enc = scr_str_new("utf8", 4); ScrStr *text = scr_bytes_to_str(chunk, enc); scr_str_release(enc); @@ -5669,6 +5772,7 @@ void scr_net_sock_transport_report(ScrNetSocket *s, const char *msg, const char void scr_net_sock_transport_ended(ScrNetSocket *s) { s->rd_eof = true; + s->fin_seen = true; scr_net_fire0_this(&s->end_ls, s, SCR_DYNH_NET_SOCKET); } @@ -6023,12 +6127,24 @@ static void scr_net_sweep(void) { /* ── the loop hooks (scr_async.c) ────────────────────────────────────── */ -/* Loop liveness: a ref'd server or socket, or pending protocol work. */ +/* Whether a socket keeps the loop alive, as libuv's active TCP handle + * does: a ref'd socket that is connecting, reading or writing, or that has + * events still to deliver. A connected socket whose reads are off (pause(), + * or a full read-ahead buffer) and that has nothing to write does not, so a + * program whose only open handle is such a socket exits, as under Node. */ +static bool scr_net_sock_keeps_alive(ScrNetSocket *s) { + if (s->unrefd) return false; + if (s->fd < 0 || s->connecting || s->lookup_wait || scr_net_sock_has_work(s, true)) return true; + return s->read_armed || s->write_armed || s->wlen > s->whead || (s->wr_ending && !s->wr_done); +} + +/* Loop liveness: a ref'd server, an active socket, or pending protocol + * work. */ static bool scr_net_pending(void) { for (ScrNetServer *srv = scr_net_servers; srv; srv = srv->next) if (!srv->unrefd) return true; for (ScrNetSocket *sock = scr_net_socks; sock; sock = sock->next) - if (!sock->unrefd) return true; + if (scr_net_sock_keeps_alive(sock)) return true; return scr_net_proto_pending(); } @@ -6337,8 +6453,9 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho bool ok = chunk->kind == SCR_DYN_STR ? scr_net_sock_write_str(s, chunk->v.str) : scr_net_sock_write_bytes(s, chunk->v.bytes); if (argc > 1 && args[1]->kind == SCR_DYN_FUNC) { - /* write(chunk, cb): fires when the buffer drains (sweep-deferred) */ - scr_net_sock_on_write_flush(s, scr_dyn_listener_closure0(args[1])); + /* write(chunk, cb): cb(null) when the buffer drains (sweep-deferred), + * or cb(error) for a rejected write */ + scr_net_sock_on_write_flush_dyn(s, args[1]); } return scr_dyn_new_bool(ok); } @@ -6478,6 +6595,60 @@ static ScrDyn *scr_net_dynh_sock_invoke(void *h, ScrDyn *self, const char *metho return NULL; } +/* The Socket members a property read answers as functions (`typeof + * socket.setKeepAlive`, `const write = socket.write`): the names + * scr_net_dynh_sock_invoke dispatches or reports as not supported yet. + * Each name is one function value, as Node's prototype methods are, and + * calls the member on the Socket it is called on. */ +static const char *const scr_net_dynh_sock_methods[] = { + "on", "once", "addListener", "write", "end", "pause", "resume", "setNoDelay", + "setKeepAlive", "destroySoon", "destroy", "setTimeout", "setEncoding", "pipe", "toString", + "ref", "unref", "address", "resetAndDestroy", "connect", "cork", "uncork", "read", "unpipe", + "off", "removeListener", "removeAllListeners", "emit", "prependListener", + "prependOnceListener", "listenerCount", "listeners", +}; +#define SCR_NET_DYNH_SOCK_METHODS (sizeof scr_net_dynh_sock_methods / sizeof *scr_net_dynh_sock_methods) +static SCR_TL ScrDyn *scr_net_dynh_sock_method_fns[SCR_NET_DYNH_SOCK_METHODS]; + +static void scr_net_dynh_sock_methods_release(void) { + for (size_t i = 0; i < SCR_NET_DYNH_SOCK_METHODS; i++) { + scr_dyn_release(scr_net_dynh_sock_method_fns[i]); + scr_net_dynh_sock_method_fns[i] = NULL; + } +} + +static ScrDyn *scr_net_dynh_sock_method_call(ScrClosure *clo, ScrDyn *const *args, size_t argc) { + const char *method = scr_net_dynh_sock_methods[(size_t)scr_box_get_f64(clo->caps[0])]; + ScrDyn *self = scr_dyn_this_get(); + ScrDyn *result = NULL; + if (self->kind == SCR_DYN_HANDLE && self->v.handle.tag == SCR_DYNH_NET_SOCKET) + result = scr_net_dynh_sock_invoke(self->v.handle.ptr, self, method, args, argc, method); + else + scr_net_dynh_unsupported(method, "called on a value other than a Socket"); + scr_dyn_release(self); + return result; +} + +static ScrDyn *scr_net_dynh_sock_method_value(const char *key) { + for (size_t i = 0; i < SCR_NET_DYNH_SOCK_METHODS; i++) { + if (strcmp(key, scr_net_dynh_sock_methods[i]) != 0) continue; + if (scr_net_dynh_sock_method_fns[i] == NULL) { + bool first = true; + for (size_t j = 0; j < SCR_NET_DYNH_SOCK_METHODS; j++) + if (scr_net_dynh_sock_method_fns[j] != NULL) first = false; + if (first) scr_atexit(scr_net_dynh_sock_methods_release); + ScrClosure *clo = scr_closure_new(NULL, 1); + clo->caps[0] = scr_box_new(SCR_BOX_F64); + scr_box_set_f64(clo->caps[0], (double)i); + scr_net_dynh_sock_method_fns[i] = + scr_dyn_new_func(clo, &scr_net_dynh_sock_method_call, 0, "(...args)", + scr_net_dynh_sock_methods[i]); + } + return scr_dyn_retain(scr_net_dynh_sock_method_fns[i]); + } + return NULL; +} + static ScrDyn *scr_net_dynh_sock_get(void *h, const char *key, size_t key_len) { ScrNetSocket *s = (ScrNetSocket *)h; (void)key_len; @@ -6539,7 +6710,7 @@ static ScrDyn *scr_net_dynh_sock_get(void *h, const char *key, size_t key_len) { } } } - return NULL; + return scr_net_dynh_sock_method_value(key); } static bool scr_net_dynh_sock_set(void *h, const char *key, size_t key_len, const ScrDyn *value) { diff --git a/tests/corpus/net-paused-socket-exit.cjs b/tests/corpus/net-paused-socket-exit.cjs new file mode 100644 index 000000000..6c598f379 --- /dev/null +++ b/tests/corpus/net-paused-socket-exit.cjs @@ -0,0 +1,24 @@ +// A connected socket that is paused and has nothing to write does not +// keep the process alive: with the server and its connection unref'd, +// the process exits before the unref'd timer would fire. +"use strict"; +const net = require("net"); + +net + .createServer((connection) => { + connection.unref(); + }) + .listen(0, "127.0.0.1", function () { + const client = net.connect(this.address().port, "127.0.0.1"); + client.on("connect", () => { + console.log("connected"); + client.pause(); + }); + setTimeout(() => { + console.log("timer fired"); + process.exit(1); + }, 2000).unref(); + }) + .unref(); + +process.on("exit", (code) => console.log("exit", code)); diff --git a/tests/corpus/net-server-socket-peer-close.cjs b/tests/corpus/net-server-socket-peer-close.cjs new file mode 100644 index 000000000..18ccd6ac1 --- /dev/null +++ b/tests/corpus/net-server-socket-peer-close.cjs @@ -0,0 +1,29 @@ +// An accepted socket reads ahead before it has a consumer, as Node's +// stream does, so it sees the peer's FIN or reset: with only 'end' and +// 'close' listeners it emits 'end' and 'close' when the client ends, and +// 'error' (ECONNRESET) and 'close' when the client calls +// resetAndDestroy(). +"use strict"; +const net = require("net"); + +function run(label, finish, done) { + const server = net.createServer((socket) => { + socket.on("end", () => console.log(label, "server socket end")); + socket.on("error", (err) => console.log(label, "server socket error", err.code, err.message)); + socket.on("close", (hadError) => { + console.log(label, "server socket close", hadError); + server.close(done); + }); + }); + server.listen(0, "127.0.0.1", () => { + const client = net.connect(server.address().port, "127.0.0.1", () => { + setTimeout(() => finish(client), 20); + }); + }); +} + +run("end:", (client) => client.end(), () => + run("destroy:", (client) => client.destroy(), () => + run("reset:", (client) => client.resetAndDestroy(), () => console.log("done")), + ), +); diff --git a/tests/corpus/net-socket-method-values.cjs b/tests/corpus/net-socket-method-values.cjs new file mode 100644 index 000000000..6e64b4f86 --- /dev/null +++ b/tests/corpus/net-socket-method-values.cjs @@ -0,0 +1,41 @@ +// A socket read through an untyped binding answers its methods as +// functions: typeof reports "function", every socket answers the same +// function for a name, as Node's prototype methods are, and calling the +// function with a socket as `this` runs the method on that socket. +"use strict"; +const net = require("net"); + +const names = ["write", "end", "setKeepAlive", "setNoDelay", "setTimeout", "setEncoding", "pause", "resume", "destroy", "on", "once", "address", "ref", "unref"]; +let first; +const server = net.createServer((socket) => { + console.log(names.map((name) => `${name}:${typeof socket[name]}`).join(" ")); + console.log("missing:", typeof socket.notAMethod); + if (first === undefined) { + first = socket; + return; + } + console.log("same function:", names.every((name) => socket[name] === first[name])); + const write = socket.write; + write.call(socket, "via call\n"); + socket.end.call(socket); + first.end(); +}); + +server.listen(0, "127.0.0.1", () => { + const { port } = server.address(); + let open = 2; + const texts = []; + for (let i = 0; i < 2; i++) { + const client = net.connect(port, "127.0.0.1"); + let text = ""; + client.setEncoding("utf8"); + client.on("data", (chunk) => (text += chunk)); + client.on("end", () => { + texts.push(JSON.stringify(text)); + if (--open === 0) { + console.log("clients:", texts.sort().join(" ")); + server.close(); + } + }); + } +}); diff --git a/tests/corpus/net-socket-set-encoding-names.ts b/tests/corpus/net-socket-set-encoding-names.ts new file mode 100644 index 000000000..7e2ec40b6 --- /dev/null +++ b/tests/corpus/net-socket-set-encoding-names.ts @@ -0,0 +1,46 @@ +// socket.setEncoding takes any Buffer encoding name in any ASCII case and +// decodes 'data' with a StringDecoder: a character split across two +// chunks arrives whole, a chunk that completes no character emits no +// 'data', and a partial character held at the end is the last 'data'. +// An unknown name throws ERR_UNKNOWN_ENCODING. +import * as net from "node:net"; + +const server = net.createServer((socket) => { + socket.on("error", () => {}); + // "a", then the two bytes of "é" split across writes, then "b" and the + // first two bytes of "€", which never complete. + socket.write(Buffer.from([0x61, 0xc3])); + setTimeout(() => socket.end(Buffer.from([0xa9, 0x62, 0xe2, 0x82])), 50); +}); + +server.listen(0, "127.0.0.1", () => { + const { port } = server.address() as net.AddressInfo; + const encodings = ["UTF8", "Utf-8", "latin1", "BINARY", "hex", "base64", "ascii", "ucs2", "UTF16LE"]; + let left = encodings.length; + const results = new Map(); + for (const encoding of encodings) { + const chunks: string[] = []; + const client = net.connect(port, "127.0.0.1"); + client.setEncoding(encoding as BufferEncoding); + client.on("data", (chunk: string) => chunks.push(`${typeof chunk}:${JSON.stringify(chunk)}`)); + client.on("end", () => { + results.set(encoding, chunks.join(" | ")); + if (--left === 0) { + for (const name of encodings) console.log(name, results.get(name)); + server.close(); + } + }); + } + const probe = net.connect(port, "127.0.0.1"); + probe.on("error", () => {}); + for (const name of ["nope", "utf-16", ""]) { + try { + probe.setEncoding(name as BufferEncoding); + console.log(JSON.stringify(name), "accepted"); + } catch (error) { + const e = error as NodeJS.ErrnoException; + console.log(JSON.stringify(name), e.name, e.code, e.message); + } + } + probe.destroy(); +}); diff --git a/tests/corpus/net-socket-write-rejected.cjs b/tests/corpus/net-socket-write-rejected.cjs new file mode 100644 index 000000000..949681819 --- /dev/null +++ b/tests/corpus/net-socket-write-rejected.cjs @@ -0,0 +1,76 @@ +// A write the socket's state rejects calls its callback with Node's error +// on the next tick: EPIPE after the peer's FIN, ERR_STREAM_WRITE_AFTER_END +// after end(), and ERR_STREAM_DESTROYED after destroy(). The first two +// destroy a socket that is still open, so 'error' and 'close' follow; a +// destroyed socket emits no 'error'. +"use strict"; +const net = require("net"); + +const describe = (err) => (err ? `${err.constructor.name} ${err.code} ${err.message}` : String(err)); +const steps = []; +const log = (line) => steps.push(line); + +function afterFin(done) { + let serverSocket; + const server = net.createServer((socket) => { + serverSocket = socket; + socket.resume(); + socket.on("error", (err) => log(`fin: error ${describe(err)}`)); + }); + server.listen(0, "127.0.0.1", () => { + const client = net.connect(server.address().port, "127.0.0.1", () => { + client.on("end", () => { + const ok = serverSocket.write("late", (err) => { + log(`fin: callback ${describe(err)}`); + server.close(done); + }); + log(`fin: write answered ${ok}`); + }); + client.end(); + }); + }); +} + +function afterEnd(done) { + const server = net.createServer((socket) => { + socket.on("error", (err) => log(`end: error ${describe(err)}`)); + socket.on("close", (hadError) => { + log(`end: close ${hadError}`); + server.close(done); + }); + socket.end("bye"); + const ok = socket.write("late", (err) => log(`end: callback ${describe(err)}`)); + log(`end: write answered ${ok}`); + }); + server.listen(0, "127.0.0.1", () => { + const client = net.connect(server.address().port, "127.0.0.1"); + client.on("error", () => {}); + client.resume(); + }); +} + +function afterDestroy(done) { + const server = net.createServer((socket) => { + socket.on("error", (err) => log(`destroy: error ${describe(err)}`)); + socket.on("close", (hadError) => { + log(`destroy: close ${hadError}`); + server.close(done); + }); + socket.destroy(); + const ok = socket.write("late", (err) => log(`destroy: callback ${describe(err)}`)); + log(`destroy: write answered ${ok}`); + }); + server.listen(0, "127.0.0.1", () => { + const client = net.connect(server.address().port, "127.0.0.1"); + client.on("error", () => {}); + client.resume(); + }); +} + +afterFin(() => + afterEnd(() => + afterDestroy(() => { + for (const step of steps) console.log(step); + }), + ), +); diff --git a/tests/harness/corpus-costs/net-paused-socket-exit.cjs.json b/tests/harness/corpus-costs/net-paused-socket-exit.cjs.json new file mode 100644 index 000000000..030fd6454 --- /dev/null +++ b/tests/harness/corpus-costs/net-paused-socket-exit.cjs.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 3200 } diff --git a/tests/harness/corpus-costs/net-server-socket-peer-close.cjs.json b/tests/harness/corpus-costs/net-server-socket-peer-close.cjs.json new file mode 100644 index 000000000..030fd6454 --- /dev/null +++ b/tests/harness/corpus-costs/net-server-socket-peer-close.cjs.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 3200 } diff --git a/tests/harness/corpus-costs/net-socket-method-values.cjs.json b/tests/harness/corpus-costs/net-socket-method-values.cjs.json new file mode 100644 index 000000000..030fd6454 --- /dev/null +++ b/tests/harness/corpus-costs/net-socket-method-values.cjs.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 3200 } diff --git a/tests/harness/corpus-costs/net-socket-set-encoding-names.ts.json b/tests/harness/corpus-costs/net-socket-set-encoding-names.ts.json new file mode 100644 index 000000000..030fd6454 --- /dev/null +++ b/tests/harness/corpus-costs/net-socket-set-encoding-names.ts.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 3200 } diff --git a/tests/harness/corpus-costs/net-socket-write-rejected.cjs.json b/tests/harness/corpus-costs/net-socket-write-rejected.cjs.json new file mode 100644 index 000000000..030fd6454 --- /dev/null +++ b/tests/harness/corpus-costs/net-socket-write-rejected.cjs.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 3200 } diff --git a/tests/harness/node-parallel-runner.test.ts b/tests/harness/node-parallel-runner.test.ts index a829c4d78..9caebd258 100644 --- a/tests/harness/node-parallel-runner.test.ts +++ b/tests/harness/node-parallel-runner.test.ts @@ -24,13 +24,17 @@ test("runs that end by different signals, or by a signal and an exit, differ", ( expect(termination({ code: 0, signal: null })).toBe(0); }); -test("a relative --node path links the checkout's fixtures into the minimal tree", () => { +test("a relative --node path links the checkout's fixtures and common helpers into the minimal tree", () => { const checkout = join(scratch, "node"); mkdirSync(join(checkout, "test", "parallel"), { recursive: true }); mkdirSync(join(checkout, "test", "fixtures"), { recursive: true }); mkdirSync(join(checkout, "test", "common"), { recursive: true }); writeFileSync(join(checkout, "test", "fixtures", "marker.txt"), "fixture\n"); - writeFileSync(join(checkout, "test", "common", "fixtures.js"), "module.exports = {};\n"); + for (const helper of ["fixtures.js", "dns.js", "internet.js"]) + writeFileSync( + join(checkout, "test", "common", helper), + `module.exports = ${JSON.stringify(helper)};\n`, + ); const out = join(scratch, "out"); const result = spawnSync( process.execPath, @@ -49,4 +53,9 @@ test("a relative --node path links the checkout's fixtures into the minimal tree const linked = join(out, "tree", "test", "fixtures", "marker.txt"); expect(existsSync(linked)).toBe(true); expect(readFileSync(linked, "utf8")).toBe("fixture\n"); + for (const helper of ["dns.js", "internet.js"]) + expect(readFileSync(join(out, "tree", "test", "common", helper), "utf8")).toBe( + `module.exports = ${JSON.stringify(helper)};\n`, + ); + expect(existsSync(join(out, "tree", "test", "common", "tmpdir.js"))).toBe(true); }); diff --git a/tests/node-parallel/README.md b/tests/node-parallel/README.md index c8ac12134..f24f6b29e 100644 --- a/tests/node-parallel/README.md +++ b/tests/node-parallel/README.md @@ -12,7 +12,7 @@ Options: - `--node`: a Node.js checkout, or its `test` directory. Use the version in `.node-version`. - `--pattern`: a regular expression over file names in `test/parallel`. - `--common`: `minimal` (the default) or `node`. - - `minimal` copies the selected files into `/tree/test/parallel` and uses `common/index.cjs` and `common/wpt.cjs` from this directory as `test/common/index.js` and `test/common/wpt.js`, with the checkout's `test/fixtures` and `test/common/fixtures.js`. The minimal common has the helpers these files use (`mustCall`, `mustNotCall`, `skip`, `expectWarning`, `invalidArgTypeHelper`, `spawnPromisified`, `hasIntl`, `isWindows`, ...) with Node's semantics, so a verdict measures the module under test rather than Node's full `test/common`. + - `minimal` copies the selected files into `/tree/test/parallel` and uses `common/index.cjs`, `common/wpt.cjs` and `common/tmpdir.cjs` from this directory as `test/common/index.js`, `test/common/wpt.js` and `test/common/tmpdir.js`, with the checkout's `test/fixtures`, `test/common/fixtures.js`, `test/common/dns.js` and `test/common/internet.js`. The minimal common has the helpers these files use (`mustCall`, `mustNotCall`, `mustSucceed`, `skip`, `expectWarning`, `expectsError`, `invalidArgTypeHelper`, `spawnPromisified`, `hasIntl`, `isWindows`, `PIPE`, ...) with Node's semantics, so a verdict measures the module under test rather than Node's full `test/common`. - `node` builds the files in place against Node's own `test/common`. - `--out`: where executables and results go (default `node_modules/.cache/scriptc-node-parallel`). - `--cli`: the scriptc CLI entry (default `packages/cli/dist/main.js`). diff --git a/tests/node-parallel/common/index.cjs b/tests/node-parallel/common/index.cjs index 098351161..4d14e5639 100644 --- a/tests/node-parallel/common/index.cjs +++ b/tests/node-parallel/common/index.cjs @@ -13,6 +13,7 @@ const isWindows = process.platform === "win32"; const isAIX = os.type() === "AIX"; const isIBMi = os.type() === "OS400"; const isSunOS = process.platform === "sunos"; +const hasCrypto = Boolean(process.versions.openssl); const mustCallChecks = []; function runCallChecks(exitCode) { @@ -60,6 +61,13 @@ function mustCallAtLeast(fn, minimum) { return mustCallInner(fn, minimum, "minimum"); } +function mustSucceed(fn, exact) { + return mustCall(function (err, ...args) { + assert.ifError(err); + if (typeof fn === "function") return fn.apply(this, args); + }, exact); +} + function mustNotCall(msg) { return function mustNotCall() { assert.fail(msg || "function should not have been called"); @@ -89,6 +97,23 @@ function expectWarning(name, expected, code) { }); } +function expectsError(validator, exact) { + return mustCall((...args) => { + if (args.length !== 1) assert.fail(`Expected one argument, got ${inspect(args)}`); + const error = args.pop(); + assert.strictEqual(Object.prototype.propertyIsEnumerable.call(error, "message"), false); + assert.throws(() => { + throw error; + }, validator); + return true; + }, exact); +} + +function platformTimeout(ms) { + if (isAIX || isIBMi) return (typeof ms === "bigint" ? 2n : 2) * ms; + return ms; +} + function invalidArgTypeHelper(input) { if (input == null) return ` Received ${input}`; if (typeof input === "function") return ` Received function ${input.name}`; @@ -122,6 +147,7 @@ function spawnPromisified(...args) { } module.exports = { + hasCrypto, hasIntl, isAIX, isDebug, @@ -131,8 +157,32 @@ module.exports = { mustCall, mustCallAtLeast, mustNotCall, + mustSucceed, skip, expectWarning, + expectsError, invalidArgTypeHelper, + platformTimeout, spawnPromisified, + get hasIPv6() { + const re = isWindows ? /Loopback Pseudo-Interface/ : isIBMi ? /\*LOOPBACK/ : /lo/; + const interfaces = os.networkInterfaces(); + return Object.keys(interfaces).some( + (name) => re.test(name) && interfaces[name].some(({ family }) => family === "IPv6"), + ); + }, + inFreeBSDJail: false, + localhostIPv4: "127.0.0.1", + // The helpers below load their modules on first use, so that files of other + // lanes do not load node:net or the tmpdir helper. + get defaultAutoSelectFamilyAttemptTimeout() { + return require("net").getDefaultAutoSelectFamilyAttemptTimeout(); + }, + get PIPE() { + const path = require("path"); + const pipePrefix = isWindows + ? "\\\\.\\pipe\\" + : path.relative(process.cwd(), `${require("./tmpdir").path}/`); + return path.join(pipePrefix, `node-test.${process.pid}.sock`); + }, }; diff --git a/tests/node-parallel/common/tmpdir.cjs b/tests/node-parallel/common/tmpdir.cjs new file mode 100644 index 000000000..85d19c911 --- /dev/null +++ b/tests/node-parallel/common/tmpdir.cjs @@ -0,0 +1,36 @@ +"use strict"; +// test/common/tmpdir.js for the minimal test/common: a per-run directory +// beside test/parallel, with the semantics of Node's helper. +const fs = require("fs"); +const path = require("path"); + +const testRoot = path.resolve(__dirname, ".."); +let tmpPath = path.join(testRoot, `.tmp.${process.env.TEST_SERIAL_ID || process.env.TEST_THREAD_ID || "0"}`); + +let firstRefresh = true; +function refresh() { + fs.rmSync(tmpPath, { maxRetries: 3, recursive: true, force: true }); + fs.mkdirSync(tmpPath); + if (firstRefresh) { + firstRefresh = false; + process.on("exit", () => { + process.chdir(testRoot); + fs.rmSync(tmpPath, { maxRetries: 3, recursive: true, force: true }); + }); + } +} + +function resolve(...paths) { + return path.resolve(tmpPath, ...paths); +} + +module.exports = { + refresh, + resolve, + get path() { + return tmpPath; + }, + set path(newPath) { + tmpPath = path.resolve(newPath); + }, +}; diff --git a/tests/node-parallel/run.mjs b/tests/node-parallel/run.mjs index e7a1e3cec..dee37c10f 100644 --- a/tests/node-parallel/run.mjs +++ b/tests/node-parallel/run.mjs @@ -42,8 +42,8 @@ const out = resolve(values.out); mkdirSync(out, { recursive: true }); // The minimal lane copies the selected files into a tree whose test/common is -// this directory's index.cjs and wpt.cjs, next to the checkout's fixtures and -// common/fixtures.js. +// this directory's index.cjs, wpt.cjs and tmpdir.cjs, next to the checkout's +// fixtures, common/fixtures.js, common/dns.js and common/internet.js. let testRoot = nodeTest; const files = readdirSync(join(nodeTest, "parallel")) .filter((name) => name.endsWith(".js") && pattern.test(name)) @@ -59,6 +59,9 @@ if (values.common === "minimal") { cpSync(join(nodeTest, "common", "fixtures.js"), join(testRoot, "common", "fixtures.js")); cpSync(join(here, "common", "index.cjs"), join(testRoot, "common", "index.js")); cpSync(join(here, "common", "wpt.cjs"), join(testRoot, "common", "wpt.js")); + cpSync(join(here, "common", "tmpdir.cjs"), join(testRoot, "common", "tmpdir.js")); + for (const helper of ["dns.js", "internet.js"]) + cpSync(join(nodeTest, "common", helper), join(testRoot, "common", helper)); } function run(cmd, args, opts) { From f67678b4227bfe415d684f406c797b397ff6f776 Mon Sep 17 00:00:00 2001 From: Chris Tate Date: Thu, 29 Oct 2026 23:16:13 -0500 Subject: [PATCH 2/2] Pipe decoded socket strings and give lane jobs their own tmpdirs After socket.setEncoding(enc), socket.pipe(destination) wrote the raw bytes it read to the destination. It now writes the decoded strings, as Node's pipe writes each string chunk: the destination receives their UTF-8 bytes, and a partial character the decoder holds at EOF is written before the destination ends. tests/node-parallel/run.mjs runs each job's files with its own TEST_THREAD_ID, as Node's test runner does, so test/common/tmpdir gives concurrent files different directories instead of one shared .tmp.0. --- .../generated/node-v24-internal.json | 111 ++++++++++++------ internal/compatibility/static-support.json | 2 +- .../corpus/net-socket-encoding-pipe.cjs.json | 6 + packages/runtime/src/scr_net.c | 54 ++++++--- tests/corpus/net-socket-encoding-pipe.cjs | 44 +++++++ .../net-socket-encoding-pipe.cjs.json | 1 + tests/harness/node-parallel-runner.test.ts | 47 ++++++++ tests/node-parallel/README.md | 2 +- tests/node-parallel/run.mjs | 13 +- 9 files changed, 220 insertions(+), 60 deletions(-) create mode 100644 packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-encoding-pipe.cjs.json create mode 100644 tests/corpus/net-socket-encoding-pipe.cjs create mode 100644 tests/harness/corpus-costs/net-socket-encoding-pipe.cjs.json diff --git a/internal/compatibility/generated/node-v24-internal.json b/internal/compatibility/generated/node-v24-internal.json index 961b40bd4..3da043d37 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -54790,6 +54790,7 @@ "tests/corpus/2988-http-static-client-abort.ts", "tests/corpus/net-paused-socket-exit.cjs", "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-socket-set-encoding-names.ts", "tests/corpus/net-socket-write-rejected.cjs", @@ -56346,7 +56347,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -56434,7 +56436,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -59466,6 +59469,7 @@ "tests/corpus/2689-http-server-timeout-properties.ts", "tests/corpus/net-paused-socket-exit.cjs", "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-socket-set-encoding-names.ts", "tests/corpus/net-socket-write-rejected.cjs" @@ -61731,7 +61735,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -62044,7 +62049,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -63875,6 +63881,7 @@ "tests/corpus/2689-http-server-timeout-properties.ts", "tests/corpus/net-paused-socket-exit.cjs", "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-socket-set-encoding-names.ts", "tests/corpus/net-socket-write-rejected.cjs" @@ -63978,7 +63985,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -64079,7 +64087,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72076,6 +72085,7 @@ "tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/corpus/net-paused-socket-exit.cjs", "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-socket-set-encoding-names.ts", "tests/corpus/net-socket-write-rejected.cjs", @@ -72692,7 +72702,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72719,7 +72730,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72782,7 +72794,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72809,7 +72822,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72836,7 +72850,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72863,7 +72878,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72890,7 +72906,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73238,7 +73255,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73283,7 +73301,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73310,7 +73329,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73376,7 +73396,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73403,7 +73424,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73502,7 +73524,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73586,7 +73609,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74235,7 +74259,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74262,7 +74287,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74289,7 +74315,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74316,7 +74343,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74343,7 +74371,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74370,7 +74399,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74397,7 +74427,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74424,7 +74455,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74451,7 +74483,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74514,7 +74547,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74541,7 +74575,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -76000,6 +76035,7 @@ "tests/corpus/6397-event-loop-metrics.ts", "tests/corpus/net-paused-socket-exit.cjs", "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-socket-set-encoding-names.ts", "tests/corpus/net-socket-write-rejected.cjs", @@ -76311,7 +76347,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -96254,6 +96291,7 @@ "tests/corpus/7442-tls-server-contexts.mjs", "tests/corpus/net-paused-socket-exit.cjs", "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-socket-set-encoding-names.ts", "tests/corpus/net-socket-write-rejected.cjs", @@ -96666,7 +96704,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -96698,7 +96737,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -96753,7 +96793,8 @@ "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", - "tests/corpus/net-server-socket-peer-close.cjs" + "tests/corpus/net-server-socket-peer-close.cjs", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 2d74d4de5..50d41994e 100644 --- a/internal/compatibility/static-support.json +++ b/internal/compatibility/static-support.json @@ -479,7 +479,7 @@ { "symbols": ["setImmediate", "setInterval", "setTimeout", "clearImmediate", "clearInterval", "clearTimeout", "timersPromises.setTimeout", "timersPromises.setImmediate", "timersPromises.setInterval", "immediate.hasRef", "immediate.ref", "immediate.unref", "timeout.hasRef", "timeout.ref", "timeout.unref", "timeout.refresh"], "status": "partial", "evidence": ["tests/corpus/1804-timers-module.ts", "tests/corpus/1805-timer-callback-args.ts", "tests/corpus/2093-timers-promises.ts", "tests/corpus/2852-timers-promises-interval.ts", "tests/corpus/5830-timers-promises-signal.ts"] }, { "symbols": ["timersPromises.scheduler.wait", "timersPromises.scheduler.yield"], "status": "partial", "evidence": ["tests/corpus/5839-timers-promises-scheduler.ts", "tests/corpus/5889-timers-scheduler-signal.ts"] }, { "symbols": ["stream.finished", "stream.pipeline", "stream.getDefaultHighWaterMark", "readable.push", "readable.unshift", "readable.read", "readable.pause", "readable.resume", "readable.isPaused", "readable.setEncoding", "readable.pipe", "readable.unpipe", "readable.destroy", "writable.write", "writable.end", "writable.cork", "writable.uncork", "writable.setDefaultEncoding"], "status": "partial", "evidence": ["tests/corpus/1685-stream-readable-basics.ts", "tests/corpus/1688-stream-writable-basics.ts", "tests/corpus/1814-stream-pipeline.cjs", "tests/corpus/5302-stream-promises-options.ts", "tests/corpus/5303-stream-finished-callback-opts.cjs", "tests/corpus/5425-pipeline-generator-stage.ts", "tests/corpus/5428-pipeline-generator-stages.cjs", "tests/corpus/5617-pipeline-function-ends.ts", "tests/corpus/5619-pipeline-function-ends.cjs", "tests/corpus/writable-error-callbacks.ts"] }, - { "symbols": ["net.createServer", "net.connect", "net.createConnection", "net.getDefaultAutoSelectFamilyAttemptTimeout", "net.setDefaultAutoSelectFamilyAttemptTimeout", "server.address", "server.close", "server.listen", "socket.destroy", "socket.end", "socket.pause", "socket.resume", "socket.setEncoding", "socket.setTimeout", "socket.write"], "status": "partial", "evidence": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs", "tests/corpus/net-socket-set-encoding-names.ts", "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", "tests/corpus/net-server-socket-peer-close.cjs"] }, + { "symbols": ["net.createServer", "net.connect", "net.createConnection", "net.getDefaultAutoSelectFamilyAttemptTimeout", "net.setDefaultAutoSelectFamilyAttemptTimeout", "server.address", "server.close", "server.listen", "socket.destroy", "socket.end", "socket.pause", "socket.resume", "socket.setEncoding", "socket.setTimeout", "socket.write"], "status": "partial", "evidence": ["tests/corpus/2500-net-autosel-timeout.ts", "tests/corpus/2596-net-arg-ladders.cjs", "tests/corpus/net-socket-set-encoding-names.ts", "tests/corpus/net-socket-write-rejected.cjs", "tests/corpus/net-socket-method-values.cjs", "tests/corpus/net-paused-socket-exit.cjs", "tests/corpus/net-server-socket-peer-close.cjs", "tests/corpus/net-socket-encoding-pipe.cjs"] }, { "symbols": ["dgram.createSocket", "socket.address", "socket.bind", "socket.close", "socket.connect", "socket.ref", "socket.remoteAddress", "socket.send", "socket.unref"], "status": "partial", "evidence": ["tests/corpus/2597-dgram-send-ladders.cjs"] }, { "symbols": ["net.SocketAddress", "net.BlockList"], "status": "partial", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] }, { "symbols": ["SocketAddress.parse", "BlockList.isBlockList", "blockList.addAddress", "blockList.addAddresses", "blockList.addCIDR", "blockList.addCIDRs", "blockList.addRange", "blockList.addSubnet", "blockList.check", "blockList.clear", "blockList.fromJSON", "blockList.removeAddress", "blockList.removeCIDR", "blockList.removeRange", "blockList.removeSubnet", "blockList.toJSON"], "status": "supported", "evidence": ["tests/corpus/5659-net-blocklist-socketaddress.ts", "tests/fixtures/node-types/net-blocklist.ts"] }, diff --git a/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-encoding-pipe.cjs.json b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-encoding-pipe.cjs.json new file mode 100644 index 000000000..59d8f4e80 --- /dev/null +++ b/packages/compiler/test/ts7/baselines/order-parity/tests/corpus/net-socket-encoding-pipe.cjs.json @@ -0,0 +1,6 @@ +{ + "order": [ + "/tests/corpus/net-socket-encoding-pipe.cjs" + ], + "diags": [] +} diff --git a/packages/runtime/src/scr_net.c b/packages/runtime/src/scr_net.c index 2c9793d0b..92fd7c9f1 100644 --- a/packages/runtime/src/scr_net.c +++ b/packages/runtime/src/scr_net.c @@ -1562,6 +1562,19 @@ static void scr_net_sock_fire_data(ScrNetSocket *s, ScrBytes *chunk, ScrStr *tex scr_bytes_release(empty); } +/* One decoded 'data' string after setEncoding: the pipe destination + * writes it, as Node's pipe writes the string chunk (UTF-8), and the + * 'data' listeners receive it. */ +static void scr_net_sock_emit_text(ScrNetSocket *s, ScrStr *text) { + if (s->pipe_dst) { + char *owned; + const char *bytes = scr_str_utf8(text, &owned); + scr_net_sock_write_raw(s->pipe_dst, bytes, text->len); + scr_str_utf8_end(owned); + } + if (s->data_ls.n > 0) scr_net_sock_fire_data(s, NULL, text); +} + /* EOF: 'end' fires now (directly — Node's poll-phase emit), then the * write half ends (allowHalfOpen false), and the fd closes once the FIN * is out. 'close' waits for the sweep. */ @@ -1573,6 +1586,15 @@ static void scr_net_sock_eof(ScrNetSocket *s) { s->native_eof(s->native_ctx); if (scr_exc_pending()) return; } + if (s->encoding != NULL && (s->data_ls.n > 0 || s->pipe_dst != NULL)) { + /* the decoder's end(): a held partial sequence is the last 'data', + * and the pipe destination receives it before its end() */ + ScrStr *tail = scr_strdec_end(s->encoding, s->dec_pending); + s->dec_pending = 0; + if (tail->len > 0) scr_net_sock_emit_text(s, tail); + scr_str_release(tail); + if (scr_exc_pending()) return; + } if (s->pipe_dst) { /* pipe's end-the-destination default */ ScrNetSocket *dst = s->pipe_dst; @@ -1581,14 +1603,6 @@ static void scr_net_sock_eof(ScrNetSocket *s) { scr_net_sock_maybe_finish_write(dst); } } - if (s->encoding != NULL && s->data_ls.n > 0) { - /* the decoder's end(): a held partial sequence is the last 'data' */ - ScrStr *tail = scr_strdec_end(s->encoding, s->dec_pending); - s->dec_pending = 0; - if (tail->len > 0) scr_net_sock_fire_data(s, NULL, tail); - scr_str_release(tail); - if (scr_exc_pending()) return; - } scr_net_fire0_this(&s->end_ls, s, SCR_DYNH_NET_SOCKET); if (scr_exc_pending()) return; if (!s->wr_ending) { @@ -1611,21 +1625,25 @@ static bool scr_net_sock_deliver(ScrNetSocket *s, const char *buf, size_t n) { * next chunk takes the listener path */ return true; } + if (s->encoding != NULL && (s->pipe_dst != NULL || s->data_ls.n > 0)) { + /* the decoder answers the complete characters; a partial sequence + * waits for the next chunk, and an empty answer emits nothing */ + ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n); + chunk->is_buffer = true; + memcpy(chunk->data, buf, n); + ScrStr *text = scr_strdec_write(s->encoding, s->dec_pending, chunk); + s->dec_pending = scr_strdec_next(s->encoding, s->dec_pending, chunk); + scr_bytes_release(chunk); + if (text->len > 0) scr_net_sock_emit_text(s, text); + scr_str_release(text); + return !scr_exc_pending(); + } if (s->pipe_dst) scr_net_sock_write_raw(s->pipe_dst, buf, n); if (s->data_ls.n > 0) { ScrBytes *chunk = scr_bytes_new(SCR_BYTES_U8, (double)n); chunk->is_buffer = true; /* Node delivers a Buffer */ memcpy(chunk->data, buf, n); - if (s->encoding != NULL) { - /* the decoder answers the complete characters; a partial sequence - * waits for the next chunk, and an empty answer emits nothing */ - ScrStr *text = scr_strdec_write(s->encoding, s->dec_pending, chunk); - s->dec_pending = scr_strdec_next(s->encoding, s->dec_pending, chunk); - if (text->len > 0) scr_net_sock_fire_data(s, NULL, text); - scr_str_release(text); - } else { - scr_net_sock_fire_data(s, chunk, NULL); - } + scr_net_sock_fire_data(s, chunk, NULL); scr_bytes_release(chunk); if (scr_exc_pending()) return false; } diff --git a/tests/corpus/net-socket-encoding-pipe.cjs b/tests/corpus/net-socket-encoding-pipe.cjs new file mode 100644 index 000000000..ff81151d9 --- /dev/null +++ b/tests/corpus/net-socket-encoding-pipe.cjs @@ -0,0 +1,44 @@ +// socket.pipe(destination) after socket.setEncoding(enc) writes the +// decoded strings to the destination, as Node's pipe writes each string +// chunk: the destination receives their UTF-8 bytes, a split character +// arrives whole, and a partial character held at EOF is written before the +// destination ends. +"use strict"; +const net = require("net"); + +const source = net.createServer((socket) => { + socket.on("error", () => {}); + socket.write(Buffer.from([0x61, 0xc3])); + setTimeout(() => socket.end(Buffer.from([0xa9, 0x62, 0xe2, 0x82])), 50); +}); + +const received = new Map(); +const collector = net.createServer((socket) => { + const chunks = []; + let label = null; + socket.on("data", (chunk) => chunks.push(chunk)); + socket.on("end", () => { + const all = Buffer.concat(chunks); + const newline = all.indexOf(0x0a); + label = all.subarray(0, newline).toString(); + received.set(label, all.subarray(newline + 1).toString("hex")); + if (received.size === encodings.length) { + for (const name of encodings) console.log(name, received.get(name)); + source.close(); + collector.close(); + } + }); +}); + +const encodings = ["utf8", "hex", "base64", "latin1", "ucs2"]; +source.listen(0, "127.0.0.1", () => { + collector.listen(0, "127.0.0.1", () => { + for (const encoding of encodings) { + const destination = net.connect(collector.address().port, "127.0.0.1"); + destination.write(encoding + "\n"); + const client = net.connect(source.address().port, "127.0.0.1"); + client.setEncoding(encoding); + client.pipe(destination); + } + }); +}); diff --git a/tests/harness/corpus-costs/net-socket-encoding-pipe.cjs.json b/tests/harness/corpus-costs/net-socket-encoding-pipe.cjs.json new file mode 100644 index 000000000..030fd6454 --- /dev/null +++ b/tests/harness/corpus-costs/net-socket-encoding-pipe.cjs.json @@ -0,0 +1 @@ +{ "plain": 2100, "san": 3200 } diff --git a/tests/harness/node-parallel-runner.test.ts b/tests/harness/node-parallel-runner.test.ts index 9caebd258..22c44f39d 100644 --- a/tests/harness/node-parallel-runner.test.ts +++ b/tests/harness/node-parallel-runner.test.ts @@ -59,3 +59,50 @@ test("a relative --node path links the checkout's fixtures and common helpers in ); expect(existsSync(join(out, "tree", "test", "common", "tmpdir.js"))).toBe(true); }); + +test("concurrent files get their own test/common tmpdir", () => { + const checkout = join(scratch, "node-tmpdir"); + mkdirSync(join(checkout, "test", "parallel"), { recursive: true }); + mkdirSync(join(checkout, "test", "fixtures"), { recursive: true }); + mkdirSync(join(checkout, "test", "common"), { recursive: true }); + for (const helper of ["fixtures.js", "dns.js", "internet.js"]) + writeFileSync(join(checkout, "test", "common", helper), "module.exports = {};\n"); + // Each file refreshes the tmpdir, writes a marker and reads it back after + // the other file has refreshed its own: with a shared directory the + // marker is gone and the file exits with 1. + const body = `const fs = require("fs"); +const tmpdir = require("../common/tmpdir"); +tmpdir.refresh(); +fs.writeFileSync(tmpdir.resolve("marker"), __filename); +setTimeout(() => { + process.exit(fs.existsSync(tmpdir.resolve("marker")) ? 0 : 1); +}, 500); +`; + for (const name of ["test-tmp-a.js", "test-tmp-b.js"]) + writeFileSync(join(checkout, "test", "parallel", name), body); + const failingCli = join(scratch, "failing-cli.mjs"); + writeFileSync(failingCli, "process.exit(1);\n"); + const out = join(scratch, "out-tmpdir"); + const result = spawnSync( + process.execPath, + [ + join(repo, "tests/node-parallel/run.mjs"), + "--node", + checkout, + "--pattern", + "^test-tmp-", + "--out", + out, + "--cli", + failingCli, + "--jobs", + "2", + ], + { cwd: scratch, encoding: "utf8" }, + ); + expect(result.status, result.stderr).toBe(0); + const results = JSON.parse(readFileSync(join(out, "results.json"), "utf8")) as { + nodeExit: number; + }[]; + expect(results.map((r) => r.nodeExit)).toEqual([0, 0]); +}); diff --git a/tests/node-parallel/README.md b/tests/node-parallel/README.md index f24f6b29e..664ee41cc 100644 --- a/tests/node-parallel/README.md +++ b/tests/node-parallel/README.md @@ -18,6 +18,6 @@ Options: - `--cli`: the scriptc CLI entry (default `packages/cli/dist/main.js`). - `--jobs`: concurrent files (default 4). -Node runs each file with its `// Flags:` line. Results are written to `/results.json` and `/results.md`. +Node runs each file with its `// Flags:` line. Each job runs its files with its own `TEST_THREAD_ID`, as Node's test runner does, so `test/common/tmpdir` gives concurrent files different directories. Results are written to `/results.json` and `/results.md`. Before a lane relies on the minimal common, run its files under Node in `/tree/test/parallel`: they should all pass there. diff --git a/tests/node-parallel/run.mjs b/tests/node-parallel/run.mjs index dee37c10f..02f967034 100644 --- a/tests/node-parallel/run.mjs +++ b/tests/node-parallel/run.mjs @@ -89,11 +89,14 @@ const firstCause = (text) => .trim() .slice(0, 300); -async function one(file) { +// Each job runs its files with its own TEST_THREAD_ID, as Node's test runner +// does, so test/common/tmpdir gives concurrent files different directories. +async function one(file, job) { + const env = { ...process.env, TEST_THREAD_ID: String(job) }; const dir = join(testRoot, "parallel"); const source = readFileSync(join(dir, file), "utf8"); const flags = (/^\/\/ Flags: (.*)$/m.exec(source)?.[1] ?? "").split(/\s+/).filter(Boolean); - const node = await run(process.execPath, [...flags, file], { cwd: dir, timeout: 120_000 }); + const node = await run(process.execPath, [...flags, file], { cwd: dir, env, timeout: 120_000 }); const exe = join(out, "bin", `${file}.exe`); mkdirSync(join(out, "bin"), { recursive: true }); const build = await run(process.execPath, [values.cli, "build", file, "-o", exe], { @@ -106,7 +109,7 @@ async function one(file) { result.cause = firstCause(build.stdout + build.stderr); return result; } - const native = await run(exe, [], { cwd: dir, timeout: 120_000 }); + const native = await run(exe, [], { cwd: dir, env, timeout: 120_000 }); result.exit = termination(native); result.verdict = sameTermination(node, native) ? "pass" : "fail"; if (result.verdict !== "pass") result.cause = firstCause(native.stderr + native.stdout); @@ -116,9 +119,9 @@ async function one(file) { const results = []; const queue = [...files]; await Promise.all( - Array.from({ length: Number(values.jobs) }, async () => { + Array.from({ length: Number(values.jobs) }, async (_, job) => { for (let file = queue.shift(); file !== undefined; file = queue.shift()) { - const result = await one(file); + const result = await one(file, job); results.push(result); console.log(`${result.verdict.padEnd(8)} ${file}${result.cause ? ` ${result.cause}` : ""}`); }