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..3da043d37 100644 --- a/internal/compatibility/generated/node-v24-internal.json +++ b/internal/compatibility/generated/node-v24-internal.json @@ -54788,6 +54788,12 @@ "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-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", "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 +56342,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -56419,7 +56431,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -59448,7 +59466,13 @@ "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-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" ] }, "anchorSource": "chapter" @@ -61706,7 +61730,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -62014,7 +62044,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -63842,7 +63878,13 @@ "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-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" ] }, "anchorSource": "chapter" @@ -63938,7 +63980,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -64034,7 +64082,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72029,6 +72083,12 @@ "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-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", "tests/fixtures/node-types/net-blocklist.ts" ] }, @@ -72637,7 +72697,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72659,7 +72725,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72717,7 +72789,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72739,7 +72817,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72761,7 +72845,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72783,7 +72873,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -72805,7 +72901,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73148,7 +73250,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73188,7 +73296,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73210,7 +73324,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73271,7 +73391,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73293,7 +73419,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73387,7 +73519,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -73466,7 +73604,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74110,7 +74254,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74132,7 +74282,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74154,7 +74310,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74176,7 +74338,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74198,7 +74366,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74220,7 +74394,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74242,7 +74422,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74264,7 +74450,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74286,7 +74478,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74344,7 +74542,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -74366,7 +74570,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -75823,6 +76033,12 @@ "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-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", "tests/fixtures/node-types/event-loop-metrics.ts" ] }, @@ -76126,7 +76342,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -96067,6 +96289,12 @@ "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-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", "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 +96699,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -96498,7 +96732,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" @@ -96548,7 +96788,13 @@ "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", + "tests/corpus/net-socket-encoding-pipe.cjs" ] }, "anchorSource": "exact" diff --git a/internal/compatibility/static-support.json b/internal/compatibility/static-support.json index 3918a8941..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"] }, + { "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-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-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/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 d2ccb1c39..e95ba05ae 100644 --- a/packages/runtime/src/scr_dyn.h +++ b/packages/runtime/src/scr_dyn.h @@ -2308,6 +2308,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 352a3abb2..0a1616dd9 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..92fd7c9f1 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,16 +1535,66 @@ 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); +} + +/* 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. */ 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); 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; @@ -1531,22 +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); - 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); - } - scr_dyn_chunk_enc(false); - scr_dyn_this_pop(); - free(snap); + scr_net_sock_fire_data(s, chunk, NULL); scr_bytes_release(chunk); if (scr_exc_pending()) return false; } @@ -2577,6 +2674,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 +3725,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 +5134,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 +5401,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 +5499,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 +5790,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 +6145,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 +6471,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 +6613,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 +6728,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-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/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-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/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..22c44f39d 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,56 @@ 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); +}); + +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 c8ac12134..664ee41cc 100644 --- a/tests/node-parallel/README.md +++ b/tests/node-parallel/README.md @@ -12,12 +12,12 @@ 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`). - `--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/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..02f967034 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) { @@ -86,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], { @@ -103,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); @@ -113,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}` : ""}`); }