From d967819c0669a7cb677002c41a4a82b26350a0bf Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Sun, 26 Jul 2026 23:19:59 +0200 Subject: [PATCH 1/7] test(dns): expand Node parity coverage --- test-parity/node-suite/dns/EVIDENCE.md | 101 ++++++++++++++++ test-parity/node-suite/dns/README.md | 114 ++++++++++++++++++ .../node-suite/dns/constants/error-aliases.ts | 108 ++++++++++++++--- .../dns/fixtures/local-dns-server.mjs | 98 +++++++++++++++ test-parity/node-suite/dns/imports/aliases.ts | 36 ++++++ .../node-suite/dns/imports/default-export.ts | 41 +++---- .../node-suite/dns/imports/descriptors.ts | 12 ++ .../dns/imports/export-inventory.ts | 10 ++ .../dns/imports/function-metadata.ts | 43 +++++++ .../dns/lookup-service/ipv6-loopback.ts | 19 +++ .../dns/lookup-service/port-coercion.ts | 40 ++++++ .../dns/lookup-service/validation.ts | 41 +++++++ .../dns/lookup/callback-validation.ts | 21 ++++ .../node-suite/dns/lookup/falsy-hostname.ts | 28 +++++ .../node-suite/dns/lookup/family-forms.ts | 29 +++++ .../dns/lookup/ip-literals-callback.ts | 27 +++++ .../dns/lookup/ip-literals-promises.ts | 9 ++ test-parity/node-suite/dns/lookup/loopback.ts | 90 +++++++++++--- .../dns/lookup/options-accessors.ts | 33 +++++ .../dns/lookup/options-validation.ts | 29 +++++ .../node-suite/dns/lookup/promisify.ts | 26 ++++ .../node-suite/dns/resolve/address-records.ts | 49 ++++++++ .../node-suite/dns/resolve/any-records.ts | 30 +++++ test-parity/node-suite/dns/resolve/errors.ts | 28 +++++ test-parity/node-suite/dns/resolve/idna.ts | 25 ++++ .../node-suite/dns/resolve/localhost.ts | 72 ----------- .../node-suite/dns/resolve/name-records.ts | 50 ++++++++ .../dns/resolve/reverse-validation.ts | 37 ++++++ .../node-suite/dns/resolve/rrtype-aliases.ts | 41 +++++++ .../dns/resolve/structured-records.ts | 70 +++++++++++ .../node-suite/dns/resolve/txt-record.ts | 23 ++++ .../node-suite/dns/resolver/cancel-active.ts | 33 +++++ .../dns/resolver/cancel-idempotent.ts | 17 +++ .../dns/resolver/constructor-validation.ts | 34 ++++++ .../dns/resolver/method-metadata.ts | 38 ++++++ .../dns/resolver/options-accessors.ts | 25 ++++ .../node-suite/dns/resolver/prototype.ts | 22 ++++ .../dns/resolver/receiver-validation.ts | 32 +++++ .../dns/resolver/resolve-receiver.ts | 42 +++++++ .../dns/resolver/resolve-validation.ts | 67 ++++++++++ .../dns/resolver/set-local-address.ts | 36 ++++++ .../dns/resolver/set-servers-validation.ts | 39 ++++++ .../settings/default-resolver-rebinding.ts | 32 +++++ .../dns/settings/default-result-order.ts | 42 +++---- .../dns/settings/servers-array-semantics.ts | 28 +++++ .../dns/settings/servers-normalization.ts | 13 ++ .../node-suite/dns/settings/servers.ts | 32 ++++- test-parity/node_suite_baseline.json | 4 +- 48 files changed, 1755 insertions(+), 161 deletions(-) create mode 100644 test-parity/node-suite/dns/EVIDENCE.md create mode 100644 test-parity/node-suite/dns/README.md create mode 100644 test-parity/node-suite/dns/fixtures/local-dns-server.mjs create mode 100644 test-parity/node-suite/dns/imports/aliases.ts create mode 100644 test-parity/node-suite/dns/imports/descriptors.ts create mode 100644 test-parity/node-suite/dns/imports/export-inventory.ts create mode 100644 test-parity/node-suite/dns/imports/function-metadata.ts create mode 100644 test-parity/node-suite/dns/lookup-service/ipv6-loopback.ts create mode 100644 test-parity/node-suite/dns/lookup-service/port-coercion.ts create mode 100644 test-parity/node-suite/dns/lookup-service/validation.ts create mode 100644 test-parity/node-suite/dns/lookup/callback-validation.ts create mode 100644 test-parity/node-suite/dns/lookup/falsy-hostname.ts create mode 100644 test-parity/node-suite/dns/lookup/family-forms.ts create mode 100644 test-parity/node-suite/dns/lookup/ip-literals-callback.ts create mode 100644 test-parity/node-suite/dns/lookup/ip-literals-promises.ts create mode 100644 test-parity/node-suite/dns/lookup/options-accessors.ts create mode 100644 test-parity/node-suite/dns/lookup/options-validation.ts create mode 100644 test-parity/node-suite/dns/lookup/promisify.ts create mode 100644 test-parity/node-suite/dns/resolve/address-records.ts create mode 100644 test-parity/node-suite/dns/resolve/any-records.ts create mode 100644 test-parity/node-suite/dns/resolve/errors.ts create mode 100644 test-parity/node-suite/dns/resolve/idna.ts delete mode 100644 test-parity/node-suite/dns/resolve/localhost.ts create mode 100644 test-parity/node-suite/dns/resolve/name-records.ts create mode 100644 test-parity/node-suite/dns/resolve/reverse-validation.ts create mode 100644 test-parity/node-suite/dns/resolve/rrtype-aliases.ts create mode 100644 test-parity/node-suite/dns/resolve/structured-records.ts create mode 100644 test-parity/node-suite/dns/resolve/txt-record.ts create mode 100644 test-parity/node-suite/dns/resolver/cancel-active.ts create mode 100644 test-parity/node-suite/dns/resolver/cancel-idempotent.ts create mode 100644 test-parity/node-suite/dns/resolver/constructor-validation.ts create mode 100644 test-parity/node-suite/dns/resolver/method-metadata.ts create mode 100644 test-parity/node-suite/dns/resolver/options-accessors.ts create mode 100644 test-parity/node-suite/dns/resolver/prototype.ts create mode 100644 test-parity/node-suite/dns/resolver/receiver-validation.ts create mode 100644 test-parity/node-suite/dns/resolver/resolve-receiver.ts create mode 100644 test-parity/node-suite/dns/resolver/resolve-validation.ts create mode 100644 test-parity/node-suite/dns/resolver/set-local-address.ts create mode 100644 test-parity/node-suite/dns/resolver/set-servers-validation.ts create mode 100644 test-parity/node-suite/dns/settings/default-resolver-rebinding.ts create mode 100644 test-parity/node-suite/dns/settings/servers-array-semantics.ts create mode 100644 test-parity/node-suite/dns/settings/servers-normalization.ts diff --git a/test-parity/node-suite/dns/EVIDENCE.md b/test-parity/node-suite/dns/EVIDENCE.md new file mode 100644 index 0000000000..bdab1c1c2d --- /dev/null +++ b/test-parity/node-suite/dns/EVIDENCE.md @@ -0,0 +1,101 @@ +# `node:dns` parity evidence + +## Environment and result + +- Oracle: Node 26.5.0, commit `bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb`. +- Perry: branch `test/expand-node-dns-parity`, built with + `cargo build --release --bin perry`. +- Alternate execution: Deno 2.9.3 and Bun 1.2.18. +- Alternate source review: Deno `34c46613cbe20450b74c0e8d4f0fd8f6f781d807` and + Bun `44f6469e0d4ae93467aa65c7e3bc9001000c7b31`. + +Three complete Node rounds ran all 43 fixtures with zero errors, crashes, or +timeouts and byte-identical aggregate SHA-256 +`c08378de2b06928db9a288ddfc6515bdbc401d814dba853a130d09adfda5a412`. Three +complete focused Perry runs produced the same **18 pass / 25 diff / 0 compile +failure / 0 crash / 0 timeout** result. The baseline records `18/43`. + +One complete alternate-runtime pass produced: + +- Deno: 21 exact matches and 22 diffs; no error, crash, or timeout. +- Bun: 13 exact matches and 30 diffs; no error, crash, or timeout. + +## Stable Perry differences + +- Module surface: the `promises` property is a data property rather than Node's + lazy getter; public functions use different names and arities. +- Lookup: callback requests return `undefined`; callback checks, falsy-host + errors, string families, option getter order, validation, and promisification + differ. +- Lookup service: numeric-string promise coercion and argument checks differ. +- Resolution: TTL objects, typed record fields, callback request objects, and + enumerable DNS error fields differ. A/AAAA values, ANY values after key + canonicalization, IDNA, name records, and TXT records match. +- Resolver: active `cancel()` reports `ETIMEOUT` instead of `ECANCELLED`; + constructor checks, prototype layout, method metadata, resolve validation, and + local-address validation differ. +- Settings: default-resolver method rebinding, sparse/accessor server arrays, + and one bracketed IPv6 normalization case differ. + +## Per-fixture classification + +`pass` and `match` mean exact stdout and exit-code parity with Node 26.5.0. +`diff` means the fixture completed but exposed a stable contract difference. + +| Fixture | Perry | Deno | Bun | +| ---------------------------------------- | ----- | ----- | ----- | +| `constants/error-aliases.ts` | pass | diff | match | +| `imports/aliases.ts` | pass | diff | diff | +| `imports/default-export.ts` | pass | match | match | +| `imports/descriptors.ts` | diff | diff | diff | +| `imports/export-inventory.ts` | pass | diff | diff | +| `imports/function-metadata.ts` | diff | diff | diff | +| `lookup-service/ipv6-loopback.ts` | pass | match | diff | +| `lookup-service/port-coercion.ts` | diff | diff | diff | +| `lookup-service/validation.ts` | diff | match | match | +| `lookup/callback-validation.ts` | diff | match | match | +| `lookup/falsy-hostname.ts` | diff | diff | diff | +| `lookup/family-forms.ts` | diff | diff | diff | +| `lookup/ip-literals-callback.ts` | diff | match | diff | +| `lookup/ip-literals-promises.ts` | pass | match | match | +| `lookup/loopback.ts` | pass | match | match | +| `lookup/options-accessors.ts` | diff | diff | diff | +| `lookup/options-validation.ts` | diff | match | match | +| `lookup/promisify.ts` | diff | diff | diff | +| `resolve/address-records.ts` | diff | match | diff | +| `resolve/any-records.ts` | pass | match | match | +| `resolve/errors.ts` | diff | diff | diff | +| `resolve/idna.ts` | pass | match | diff | +| `resolve/name-records.ts` | pass | diff | match | +| `resolve/reverse-validation.ts` | diff | diff | diff | +| `resolve/rrtype-aliases.ts` | diff | match | diff | +| `resolve/structured-records.ts` | diff | diff | diff | +| `resolve/txt-record.ts` | pass | match | diff | +| `resolver/cancel-active.ts` | diff | match | diff | +| `resolver/cancel-idempotent.ts` | pass | match | match | +| `resolver/constructor-validation.ts` | diff | match | diff | +| `resolver/method-metadata.ts` | diff | diff | diff | +| `resolver/options-accessors.ts` | pass | diff | diff | +| `resolver/prototype.ts` | diff | diff | diff | +| `resolver/receiver-validation.ts` | pass | match | diff | +| `resolver/resolve-receiver.ts` | pass | match | diff | +| `resolver/resolve-validation.ts` | diff | match | diff | +| `resolver/set-local-address.ts` | diff | diff | diff | +| `resolver/set-servers-validation.ts` | pass | match | match | +| `settings/default-resolver-rebinding.ts` | diff | diff | diff | +| `settings/default-result-order.ts` | pass | diff | diff | +| `settings/servers-array-semantics.ts` | diff | match | match | +| `settings/servers-normalization.ts` | diff | diff | match | +| `settings/servers.ts` | pass | diff | diff | + +## Commands + +```sh +cargo build --release --bin perry +NODE_BIN="$HOME/.nvm/versions/node/v26.5.0/bin/node" \ + python3 scripts/node_suite_run.py target/release/perry "$PWD" dns +python3 -m json.tool test-parity/node_suite_baseline.json >/dev/null +``` + +Local authoritative-server cases need permission to bind ephemeral loopback UDP +ports and spawn the helper Node process. diff --git a/test-parity/node-suite/dns/README.md b/test-parity/node-suite/dns/README.md new file mode 100644 index 0000000000..1bb99961c5 --- /dev/null +++ b/test-parity/node-suite/dns/README.md @@ -0,0 +1,114 @@ +# `node:dns` granular parity suite + +This directory compares deterministic public `node:dns` and `node:dns/promises` +behavior with Node 26.5.0. Each TypeScript file has one contract or one small +record family. The differential runner executes this module sequentially. + +## Audited starting point + +The six starting fixtures were reviewed before expansion: + +- `constants/error-aliases.ts` already covered the full public error-code table. +- `imports/default-export.ts` mixed import identity with a live `resolve4()` + request. The request was removed because it queried the host nameserver and + changed between `ECONNREFUSED`, `ENOTFOUND`, and `EBADRESP`. +- `lookup/loopback.ts` uses only the system hosts path and loopback addresses. + It remains as the broad callback/promise smoke case. +- `resolve/localhost.ts` queried the configured nameserver rather than the hosts + file. It was removed and replaced with local authoritative-server fixtures. +- `settings/default-result-order.ts` used host-dependent localhost ordering. It + now tests only shared state, valid values, and invalid-value preservation. +- `settings/servers.ts` only parses and stores server addresses. It remains and + now reports missing alternate-runtime methods without aborting. + +The audit also traced Perry's DNS manifest, native dispatch table, +`crates/perry-runtime/src/dns.rs`, and +`crates/perry-runtime/src/dns_resolver.rs`. Perry implements real wire queries, +but several `Resolver` object, validation, callback-request, TTL, cancellation, +and descriptor contracts still differ from Node. + +## Fixed upstream sources + +The selection was reviewed on 2026-07-26 against these primary snapshots: + +- Node 26.5.0 commit + [`bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb`](https://github.com/nodejs/node/tree/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb), + especially + [`lib/dns.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/lib/dns.js), + [`internal/dns/utils.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/lib/internal/dns/utils.js), + [`callback_resolver.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/lib/internal/dns/callback_resolver.js), + [`promises.js`](https://github.com/nodejs/node/blob/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/lib/internal/dns/promises.js), + and the + [`test-dns*` parallel tests](https://github.com/nodejs/node/tree/bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb/test/parallel). +- Deno main commit + [`34c46613cbe20450b74c0e8d4f0fd8f6f781d807`](https://github.com/denoland/deno/tree/34c46613cbe20450b74c0e8d4f0fd8f6f781d807), + especially + [`dns_test.ts`](https://github.com/denoland/deno/blob/34c46613cbe20450b74c0e8d4f0fd8f6f781d807/tests/unit_node/dns_test.ts) + and its + [`node:dns` polyfill](https://github.com/denoland/deno/blob/34c46613cbe20450b74c0e8d4f0fd8f6f781d807/ext/node/polyfills/dns.ts). +- Bun main commit + [`44f6469e0d4ae93467aa65c7e3bc9001000c7b31`](https://github.com/oven-sh/bun/tree/44f6469e0d4ae93467aa65c7e3bc9001000c7b31), + especially + [`node-dns.test.js`](https://github.com/oven-sh/bun/blob/44f6469e0d4ae93467aa65c7e3bc9001000c7b31/test/js/node/dns/node-dns.test.js), + its selected + [Node DNS tests](https://github.com/oven-sh/bun/tree/44f6469e0d4ae93467aa65c7e3bc9001000c7b31/test/js/node/test/parallel), + and + [`dns.ts`](https://github.com/oven-sh/bun/blob/44f6469e0d4ae93467aa65c7e3bc9001000c7b31/src/js/node/dns.ts). + +Node 26.5.0 is the oracle. Deno and Bun results show whether another runtime +made the same choice; they do not weaken the Node contract. + +## Covered contracts + +- export inventory, default/namespace identity, callback/promise aliases, + descriptors, function names, and arity; +- literal IPv4/IPv6 lookup, localhost loopback, callback request objects, family + forms, option accessor order, option validation, falsy hostnames, and + `util.promisify()` behavior; +- IPv4/IPv6 loopback `lookupService`, port coercion, and argument validation; +- shared default result order, module resolver rebinding, server parsing, + sparse/accessor arrays, invalid-update preservation, and resolver-local server + state; +- `Resolver` prototype layout, constructor option access and validation, + receiver checks, `setLocalAddress`, `setServers`, method validation, active + cancellation, and idempotent cancellation; +- callback and promise A/AAAA with TTL, ANY, CAA, CNAME, MX, NAPTR, NS, PTR, + SOA, SRV, TXT, IDNA, rrtype aliases, reverse validation, and DNS error shape. + +Record fixtures use `fixtures/local-dns-server.mjs`. It starts a child Node +process, binds an ephemeral UDP loopback port, emits an explicit ready barrier, +returns fixed TEST-NET/documentation records, and closes in `finally`. No +fixture sends a query to an internet nameserver. + +## Stopping boundary + +The suite stops at 43 fixtures. A fresh review of the fixed Node, Deno, and Bun +trees found no other public contract that was both deterministic, portable, +non-redundant, and reachable through this print-and-diff harness. + +Excluded on purpose: + +- Node's `test/internet/test-dns-*` files and Bun's public-domain fixtures: + answers, TTLs, delegation, and availability can change. +- Successful `reverse()` was prototyped against the local server. Perry did not + settle within 70 seconds, so retaining it would leave cleanup to the harness + timeout and would not isolate a useful result. Input validation remains. +- Node's two-channel query test was prototyped with two local sockets. Perry did + not pass the auxiliary-server ready barrier within 30 seconds. Server-state + independence remains covered without keeping a timeout that mixes child + process behavior into the DNS result. +- Resolver timeout/retry timing, set-servers-during-query, worker termination, + perf hooks, snapshots, memory faults, malformed packet counts, TCP fallback, + and stress cases depend on timers, scheduler order, internals, workers, or + crash-only harnesses. +- `resolveTlsa()` remains covered by export and method metadata only. It was not + in the requested record-method set or the selected Deno/Bun suites; adding a + value-shape case would not supply cross-runtime evidence. +- Exact host `getServers()` defaults, localhost address order, reverse + hostnames, service names, and non-loopback `lookup()` results depend on OS + configuration. +- DNS-over-TLS/HTTPS, DNSSEC, cache policy, and transport internals are not + public `node:dns` contracts in the selected Node suite. + +See [EVIDENCE.md](EVIDENCE.md) for the repeated measurements and per-fixture +cross-runtime classification. diff --git a/test-parity/node-suite/dns/constants/error-aliases.ts b/test-parity/node-suite/dns/constants/error-aliases.ts index 19568697bf..6c6510bcb5 100644 --- a/test-parity/node-suite/dns/constants/error-aliases.ts +++ b/test-parity/node-suite/dns/constants/error-aliases.ts @@ -9,7 +9,11 @@ console.log("dns.ALL:", typeof dns.ALL, dns.ALL); console.log("dnsPromises.ADDRCONFIG:", typeof dnsPromises.ADDRCONFIG); console.log("dnsPromises.V4MAPPED:", typeof dnsPromises.V4MAPPED); console.log("dnsPromises.ALL:", typeof dnsPromises.ALL); -console.log("dns.promises.NODATA:", typeof dns.promises.NODATA, dns.promises.NODATA); +console.log( + "dns.promises.NODATA:", + typeof dns.promises.NODATA, + dns.promises.NODATA, +); console.log("dns.NODATA:", typeof dns.NODATA, dns.NODATA); console.log("dns.FORMERR:", typeof dns.FORMERR, dns.FORMERR); @@ -31,7 +35,11 @@ console.log("dns.BADSTR:", typeof dns.BADSTR, dns.BADSTR); console.log("dns.BADFLAGS:", typeof dns.BADFLAGS, dns.BADFLAGS); console.log("dns.NONAME:", typeof dns.NONAME, dns.NONAME); console.log("dns.BADHINTS:", typeof dns.BADHINTS, dns.BADHINTS); -console.log("dns.NOTINITIALIZED:", typeof dns.NOTINITIALIZED, dns.NOTINITIALIZED); +console.log( + "dns.NOTINITIALIZED:", + typeof dns.NOTINITIALIZED, + dns.NOTINITIALIZED, +); console.log("dns.LOADIPHLPAPI:", typeof dns.LOADIPHLPAPI, dns.LOADIPHLPAPI); console.log( "dns.ADDRGETNETWORKPARAMS:", @@ -40,22 +48,66 @@ console.log( ); console.log("dns.CANCELLED:", typeof dns.CANCELLED, dns.CANCELLED); -console.log("dnsPromises.NODATA:", typeof dnsPromises.NODATA, dnsPromises.NODATA); -console.log("dnsPromises.FORMERR:", typeof dnsPromises.FORMERR, dnsPromises.FORMERR); -console.log("dnsPromises.SERVFAIL:", typeof dnsPromises.SERVFAIL, dnsPromises.SERVFAIL); -console.log("dnsPromises.NOTFOUND:", typeof dnsPromises.NOTFOUND, dnsPromises.NOTFOUND); -console.log("dnsPromises.NOTIMP:", typeof dnsPromises.NOTIMP, dnsPromises.NOTIMP); -console.log("dnsPromises.REFUSED:", typeof dnsPromises.REFUSED, dnsPromises.REFUSED); -console.log("dnsPromises.BADQUERY:", typeof dnsPromises.BADQUERY, dnsPromises.BADQUERY); -console.log("dnsPromises.BADNAME:", typeof dnsPromises.BADNAME, dnsPromises.BADNAME); -console.log("dnsPromises.BADFAMILY:", typeof dnsPromises.BADFAMILY, dnsPromises.BADFAMILY); -console.log("dnsPromises.BADRESP:", typeof dnsPromises.BADRESP, dnsPromises.BADRESP); +console.log( + "dnsPromises.NODATA:", + typeof dnsPromises.NODATA, + dnsPromises.NODATA, +); +console.log( + "dnsPromises.FORMERR:", + typeof dnsPromises.FORMERR, + dnsPromises.FORMERR, +); +console.log( + "dnsPromises.SERVFAIL:", + typeof dnsPromises.SERVFAIL, + dnsPromises.SERVFAIL, +); +console.log( + "dnsPromises.NOTFOUND:", + typeof dnsPromises.NOTFOUND, + dnsPromises.NOTFOUND, +); +console.log( + "dnsPromises.NOTIMP:", + typeof dnsPromises.NOTIMP, + dnsPromises.NOTIMP, +); +console.log( + "dnsPromises.REFUSED:", + typeof dnsPromises.REFUSED, + dnsPromises.REFUSED, +); +console.log( + "dnsPromises.BADQUERY:", + typeof dnsPromises.BADQUERY, + dnsPromises.BADQUERY, +); +console.log( + "dnsPromises.BADNAME:", + typeof dnsPromises.BADNAME, + dnsPromises.BADNAME, +); +console.log( + "dnsPromises.BADFAMILY:", + typeof dnsPromises.BADFAMILY, + dnsPromises.BADFAMILY, +); +console.log( + "dnsPromises.BADRESP:", + typeof dnsPromises.BADRESP, + dnsPromises.BADRESP, +); console.log( "dnsPromises.CONNREFUSED:", typeof dnsPromises.CONNREFUSED, dnsPromises.CONNREFUSED, ); -console.log("dnsPromises.TIMEOUT:", typeof dnsPromises.TIMEOUT, dnsPromises.TIMEOUT); +console.log( + "dnsPromises.TIMEOUT:", + typeof dnsPromises.TIMEOUT, + dnsPromises.TIMEOUT, +); console.log("dnsPromises.EOF:", typeof dnsPromises.EOF, dnsPromises.EOF); console.log("dnsPromises.FILE:", typeof dnsPromises.FILE, dnsPromises.FILE); console.log("dnsPromises.NOMEM:", typeof dnsPromises.NOMEM, dnsPromises.NOMEM); @@ -64,10 +116,26 @@ console.log( typeof dnsPromises.DESTRUCTION, dnsPromises.DESTRUCTION, ); -console.log("dnsPromises.BADSTR:", typeof dnsPromises.BADSTR, dnsPromises.BADSTR); -console.log("dnsPromises.BADFLAGS:", typeof dnsPromises.BADFLAGS, dnsPromises.BADFLAGS); -console.log("dnsPromises.NONAME:", typeof dnsPromises.NONAME, dnsPromises.NONAME); -console.log("dnsPromises.BADHINTS:", typeof dnsPromises.BADHINTS, dnsPromises.BADHINTS); +console.log( + "dnsPromises.BADSTR:", + typeof dnsPromises.BADSTR, + dnsPromises.BADSTR, +); +console.log( + "dnsPromises.BADFLAGS:", + typeof dnsPromises.BADFLAGS, + dnsPromises.BADFLAGS, +); +console.log( + "dnsPromises.NONAME:", + typeof dnsPromises.NONAME, + dnsPromises.NONAME, +); +console.log( + "dnsPromises.BADHINTS:", + typeof dnsPromises.BADHINTS, + dnsPromises.BADHINTS, +); console.log( "dnsPromises.NOTINITIALIZED:", typeof dnsPromises.NOTINITIALIZED, @@ -83,6 +151,10 @@ console.log( typeof dnsPromises.ADDRGETNETWORKPARAMS, dnsPromises.ADDRGETNETWORKPARAMS, ); -console.log("dnsPromises.CANCELLED:", typeof dnsPromises.CANCELLED, dnsPromises.CANCELLED); +console.log( + "dnsPromises.CANCELLED:", + typeof dnsPromises.CANCELLED, + dnsPromises.CANCELLED, +); console.log("named aliases:", namedNodata, namedPromisesCancelled); diff --git a/test-parity/node-suite/dns/fixtures/local-dns-server.mjs b/test-parity/node-suite/dns/fixtures/local-dns-server.mjs new file mode 100644 index 0000000000..415386a6e9 --- /dev/null +++ b/test-parity/node-suite/dns/fixtures/local-dns-server.mjs @@ -0,0 +1,98 @@ +import { spawn } from "node:child_process"; + +const serverSource = String.raw` +const dgram = require("node:dgram"); +const mode = process.argv[1]; +const u16 = (value) => { const out = Buffer.alloc(2); out.writeUInt16BE(value); return out; }; +const u32 = (value) => { const out = Buffer.alloc(4); out.writeUInt32BE(value); return out; }; +const name = (value) => Buffer.concat(value.split(".").map((part) => Buffer.concat([Buffer.from([Buffer.byteLength(part)]), Buffer.from(part)])).concat(Buffer.from([0]))); +const text = (...parts) => Buffer.concat(parts.map((part) => Buffer.concat([Buffer.from([Buffer.byteLength(part)]), Buffer.from(part)]))); +const record = (type, data, ttl = 60) => Buffer.concat([Buffer.from([0xc0, 0x0c]), u16(type), u16(1), u32(ttl), u16(data.length), data]); +const records = (type) => { + const map = { + 1: [record(1, Buffer.from([203, 0, 113, 7]), 120)], + 2: [record(2, name("ns.example.test"))], + 5: [record(5, name("alias.example.test"))], + 6: [record(6, Buffer.concat([name("ns.example.test"), name("hostmaster.example.test"), u32(1), u32(2), u32(3), u32(4), u32(5)]))], + 12: [record(12, name("ptr.example.test"))], + 15: [record(15, Buffer.concat([u16(10), name("mail.example.test")]))], + 16: [record(16, text("alpha", "beta"))], + 28: [record(28, Buffer.from("20010db8000000000000000000000007", "hex"), 240)], + 33: [record(33, Buffer.concat([u16(10), u16(20), u16(443), name("service.example.test")]))], + 35: [record(35, Buffer.concat([u16(1), u16(2), text("S", "SIP+D2U", "!^.*$!"), name("replacement.example.test")]))], + 52: [record(52, Buffer.concat([Buffer.from([3, 1, 1]), Buffer.from("abcd", "hex")]))], + 257: [record(257, Buffer.concat([Buffer.from([128, 5]), Buffer.from("issueca.example")]))], + }; + return type === 255 ? [map[1][0], map[28][0], map[15][0], map[2][0], map[16][0], map[12][0], map[6][0], map[257][0]] : (map[type] || []); +}; +function start() { + const socket = dgram.createSocket("udp4"); + socket.on("message", (request, remote) => { + let end = 12; + while (request[end] !== 0) end += request[end] + 1; + end += 1; + const type = request.readUInt16BE(end); + process.stdout.write("QUERY\n"); + if (mode === "silent") return; + const question = request.subarray(12, end + 4); + const answers = ["nxdomain", "nodata", "refused"].includes(mode) ? [] : records(type); + const rcode = mode === "nxdomain" ? 3 : mode === "refused" ? 5 : 0; + const header = Buffer.concat([request.subarray(0, 2), Buffer.from([0x81, 0x80 | rcode]), u16(1), u16(answers.length), u16(0), u16(0)]); + socket.send(Buffer.concat([header, question, ...answers]), remote.port, remote.address); + }); + socket.bind(0, "127.0.0.1", () => { + process.stdout.write("READY:" + socket.address().port + "\n"); + }); + return socket; +} +const socket = start(); +process.on("SIGTERM", () => socket.close(() => process.exit(0))); +`; + +export async function startDnsServer(mode = "answer") { + const child = spawn("node", ["-e", serverSource, mode], { + stdio: ["ignore", "pipe", "inherit"], + }); + const lines = []; + const waiters = []; + let buffered = ""; + let readyResolve; + let readyReject; + const ready = new Promise((resolve, reject) => { + readyResolve = resolve; + readyReject = reject; + }); + + child.stdout.setEncoding("utf8"); + child.stdout.on("data", (chunk) => { + buffered += chunk; + while (buffered.includes("\n")) { + const index = buffered.indexOf("\n"); + const line = buffered.slice(0, index); + buffered = buffered.slice(index + 1); + if (line.startsWith("READY:")) readyResolve(Number(line.slice(6))); + else if (waiters.length) waiters.shift()(line); + else lines.push(line); + } + }); + child.once("error", readyReject); + child.once( + "exit", + (code) => readyReject(new Error(`DNS server exited before ready: ${code}`)), + ); + + const port = await ready; + return { + port, + nextQuery() { + if (lines.length) return Promise.resolve(lines.shift()); + return new Promise((resolve) => waiters.push(resolve)); + }, + async close() { + if (child.exitCode !== null) return; + const exited = new Promise((resolve) => child.once("exit", resolve)); + child.kill(); + await exited; + }, + }; +} diff --git a/test-parity/node-suite/dns/imports/aliases.ts b/test-parity/node-suite/dns/imports/aliases.ts new file mode 100644 index 0000000000..a151373909 --- /dev/null +++ b/test-parity/node-suite/dns/imports/aliases.ts @@ -0,0 +1,36 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +const methods = [ + "lookup", + "lookupService", + "resolve", + "resolve4", + "resolve6", + "resolveAny", + "resolveCaa", + "resolveCname", + "resolveMx", + "resolveNaptr", + "resolveNs", + "resolvePtr", + "resolveSoa", + "resolveSrv", + "resolveTlsa", + "resolveTxt", + "reverse", +]; + +console.log("promises identity:", dns.promises === dnsPromises); +console.log("resolver identity:", dns.Resolver === dnsPromises.Resolver); +console.log( + "method identity:", + methods.map((name) => (dns as any)[name] === (dnsPromises as any)[name]).join( + "|", + ), +); +console.log( + "error identity:", + dns.NODATA === dnsPromises.NODATA, + dns.CANCELLED === dnsPromises.CANCELLED, +); diff --git a/test-parity/node-suite/dns/imports/default-export.ts b/test-parity/node-suite/dns/imports/default-export.ts index baf5fe9c54..47af52039b 100644 --- a/test-parity/node-suite/dns/imports/default-export.ts +++ b/test-parity/node-suite/dns/imports/default-export.ts @@ -7,16 +7,31 @@ import process from "node:process"; const dnsBuiltin = process.getBuiltinModule("dns"); const dnsPromisesBuiltin = process.getBuiltinModule("dns/promises"); -console.log("dns namespace has default:", Object.keys(dnsNs).includes("default")); +console.log( + "dns namespace has default:", + Object.keys(dnsNs).includes("default"), +); console.log("dns default identity:", dnsNs.default === dnsDefault); console.log("dns builtin identity:", dnsBuiltin === dnsDefault); -console.log("dns default lacks default key:", !Object.keys(dnsDefault).includes("default")); +console.log( + "dns default lacks default key:", + !Object.keys(dnsDefault).includes("default"), +); console.log("dns lookup types:", typeof dnsDefault.lookup, typeof dnsNs.lookup); console.log("dns lookup identity:", dnsDefault.lookup === dnsNs.lookup); -console.log("promises namespace has default:", Object.keys(dnsPromisesNs).includes("default")); -console.log("promises default identity:", dnsPromisesNs.default === dnsPromisesDefault); -console.log("promises builtin identity:", dnsPromisesBuiltin === dnsPromisesDefault); +console.log( + "promises namespace has default:", + Object.keys(dnsPromisesNs).includes("default"), +); +console.log( + "promises default identity:", + dnsPromisesNs.default === dnsPromisesDefault, +); +console.log( + "promises builtin identity:", + dnsPromisesBuiltin === dnsPromisesDefault, +); console.log( "promises default lacks default key:", !Object.keys(dnsPromisesDefault).includes("default"), @@ -26,19 +41,3 @@ console.log( typeof dnsPromisesDefault.Resolver, typeof dnsPromisesNs.Resolver, ); - -const resolver = new dnsPromisesDefault.Resolver(); -// resolve4 queries the configured nameserver directly (it does not read -// /etc/hosts), so "localhost" yields an A record on some resolvers and -// ESERVFAIL/ENOTFOUND on others — node itself rejects (and aborts on the -// unhandled rejection) when the resolver has no answer for it. Print -// array-or-error-code so node and Perry agree regardless of the machine's -// resolver, the same record-or-error approach the dns/resolve suite uses. -let resolver4Summary: string; -try { - const resolver4 = await resolver.resolve4("localhost"); - resolver4Summary = Array.isArray(resolver4) ? "array" : typeof resolver4; -} catch (e: any) { - resolver4Summary = "err:" + e.code; -} -console.log("promises default resolve4:", resolver4Summary); diff --git a/test-parity/node-suite/dns/imports/descriptors.ts b/test-parity/node-suite/dns/imports/descriptors.ts new file mode 100644 index 0000000000..9c0f6b7872 --- /dev/null +++ b/test-parity/node-suite/dns/imports/descriptors.ts @@ -0,0 +1,12 @@ +import dns from "node:dns"; + +for (const key of ["lookup", "Resolver", "ADDRCONFIG", "NODATA", "promises"]) { + const descriptor = Object.getOwnPropertyDescriptor(dns, key)!; + console.log( + key + ":", + descriptor.enumerable, + descriptor.configurable, + "writable" in descriptor ? descriptor.writable : "accessor", + typeof descriptor.get, + ); +} diff --git a/test-parity/node-suite/dns/imports/export-inventory.ts b/test-parity/node-suite/dns/imports/export-inventory.ts new file mode 100644 index 0000000000..496f1682b9 --- /dev/null +++ b/test-parity/node-suite/dns/imports/export-inventory.ts @@ -0,0 +1,10 @@ +import * as dns from "node:dns"; +import * as dnsPromises from "node:dns/promises"; + +console.log("dns keys:", Object.keys(dns).sort().join("|")); +console.log("promise keys:", Object.keys(dnsPromises).sort().join("|")); +console.log("default keys:", Object.keys(dns.default).sort().join("|")); +console.log( + "promise default keys:", + Object.keys(dnsPromises.default).sort().join("|"), +); diff --git a/test-parity/node-suite/dns/imports/function-metadata.ts b/test-parity/node-suite/dns/imports/function-metadata.ts new file mode 100644 index 0000000000..54e0388d28 --- /dev/null +++ b/test-parity/node-suite/dns/imports/function-metadata.ts @@ -0,0 +1,43 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +const names = [ + "lookup", + "lookupService", + "resolve", + "resolve4", + "resolve6", + "resolveAny", + "resolveCaa", + "resolveCname", + "resolveMx", + "resolveNaptr", + "resolveNs", + "resolvePtr", + "resolveSoa", + "resolveSrv", + "resolveTlsa", + "resolveTxt", + "reverse", + "getServers", + "setServers", + "getDefaultResultOrder", + "setDefaultResultOrder", +]; + +function metadata(value: any): string { + return typeof value === "function" + ? `${value.name}/${value.length}` + : typeof value; +} + +console.log( + "callback:", + names.map((name) => `${name}=${metadata((dns as any)[name])}`).join("|"), +); +console.log( + "promises:", + names.map((name) => `${name}=${metadata((dnsPromises as any)[name])}`).join( + "|", + ), +); diff --git a/test-parity/node-suite/dns/lookup-service/ipv6-loopback.ts b/test-parity/node-suite/dns/lookup-service/ipv6-loopback.ts new file mode 100644 index 0000000000..95f6128cf7 --- /dev/null +++ b/test-parity/node-suite/dns/lookup-service/ipv6-loopback.ts @@ -0,0 +1,19 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +const callback = await new Promise((resolve) => { + dns.lookupService( + "::1", + 0, + (error, hostname, service) => resolve({ error, hostname, service }), + ); +}); +console.log( + "callback:", + callback.error === null, + typeof callback.hostname, + typeof callback.service, +); + +const promise = await dnsPromises.lookupService("::1", 0); +console.log("promise:", typeof promise.hostname, typeof promise.service); diff --git a/test-parity/node-suite/dns/lookup-service/port-coercion.ts b/test-parity/node-suite/dns/lookup-service/port-coercion.ts new file mode 100644 index 0000000000..128c36f55b --- /dev/null +++ b/test-parity/node-suite/dns/lookup-service/port-coercion.ts @@ -0,0 +1,40 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function callback(port: any): Promise { + return new Promise((resolve) => { + dns.lookupService("127.0.0.1", port, (error, hostname, service) => { + resolve(error ? { error: error.code } : { hostname, service }); + }); + }).catch((error: any) => ({ error: error.code })); +} + +function summary(value: any): string { + return value.error + ? `error:${value.error}` + : `${typeof value.hostname}/${typeof value.service}`; +} + +const callbackNumber = await callback(22); +const callbackString = await callback("22"); +console.log( + "callback equal:", + JSON.stringify(callbackNumber) === JSON.stringify(callbackString), + summary(callbackString), +); + +async function promise(port: any): Promise { + try { + return await (dnsPromises.lookupService as any)("127.0.0.1", port); + } catch (error: any) { + return { error: error.code }; + } +} + +const promiseNumber = await promise(22); +const promiseString = await promise("22"); +console.log( + "promise equal:", + JSON.stringify(promiseNumber) === JSON.stringify(promiseString), + summary(promiseString), +); diff --git a/test-parity/node-suite/dns/lookup-service/validation.ts b/test-parity/node-suite/dns/lookup-service/validation.ts new file mode 100644 index 0000000000..db2a4a4ab6 --- /dev/null +++ b/test-parity/node-suite/dns/lookup-service/validation.ts @@ -0,0 +1,41 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function shape(fn: () => unknown): string { + try { + fn(); + return "no throw"; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +const callbackCases: Array<[string, () => unknown]> = [ + ["missing", () => (dns.lookupService as any)("127.0.0.1")], + ["address", () => (dns.lookupService as any)("localhost", 80, () => {})], + ["port low", () => (dns.lookupService as any)("127.0.0.1", -1, () => {})], + ["port high", () => (dns.lookupService as any)("127.0.0.1", 65536, () => {})], + [ + "port text", + () => (dns.lookupService as any)("127.0.0.1", "nope", () => {}), + ], + ["callback", () => (dns.lookupService as any)("127.0.0.1", 80, null)], +]; + +for (const [label, fn] of callbackCases) { + console.log("callback " + label + ":", shape(fn)); +} +for ( + const [label, address, port] of [ + ["missing", "127.0.0.1", undefined], + ["address", "localhost", 80], + ["port low", "127.0.0.1", -1], + ["port high", "127.0.0.1", 65536], + ["port text", "127.0.0.1", "nope"], + ] as const +) { + console.log( + "promise " + label + ":", + shape(() => (dnsPromises.lookupService as any)(address, port)), + ); +} diff --git a/test-parity/node-suite/dns/lookup/callback-validation.ts b/test-parity/node-suite/dns/lookup/callback-validation.ts new file mode 100644 index 0000000000..e6887b32b5 --- /dev/null +++ b/test-parity/node-suite/dns/lookup/callback-validation.ts @@ -0,0 +1,21 @@ +import dns from "node:dns"; + +function shape(fn: () => unknown): string { + try { + fn(); + return "no throw"; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +console.log("missing:", shape(() => (dns.lookup as any)("127.0.0.1"))); +console.log( + "family overload:", + shape(() => (dns.lookup as any)("127.0.0.1", 4)), +); +console.log("null:", shape(() => (dns.lookup as any)("127.0.0.1", null))); +console.log( + "options null callback:", + shape(() => (dns.lookup as any)("127.0.0.1", {}, null)), +); diff --git a/test-parity/node-suite/dns/lookup/falsy-hostname.ts b/test-parity/node-suite/dns/lookup/falsy-hostname.ts new file mode 100644 index 0000000000..7d19351438 --- /dev/null +++ b/test-parity/node-suite/dns/lookup/falsy-hostname.ts @@ -0,0 +1,28 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function callbackShape(hostname: any): string { + try { + dns.lookup(hostname, () => {}); + return "no throw"; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +async function promiseShape(hostname: any): Promise { + try { + await dnsPromises.lookup(hostname); + return "resolved"; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +for (const value of [undefined, null, "", 0, false, NaN]) { + console.log( + String(value) + ":", + callbackShape(value), + await promiseShape(value), + ); +} diff --git a/test-parity/node-suite/dns/lookup/family-forms.ts b/test-parity/node-suite/dns/lookup/family-forms.ts new file mode 100644 index 0000000000..8df687c8c3 --- /dev/null +++ b/test-parity/node-suite/dns/lookup/family-forms.ts @@ -0,0 +1,29 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function callbackFamily(family: any): Promise { + return new Promise((resolve) => { + dns.lookup("127.0.0.1", { family }, (error, address, resultFamily) => { + resolve( + error ? `${error.name}/${error.code}` : `${address}/${resultFamily}`, + ); + }); + }); +} + +function syncShape(fn: () => unknown): string { + try { + const value = fn(); + return value instanceof Promise ? "promise" : typeof value; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +console.log("callback IPv4:", await callbackFamily("IPv4")); +console.log("callback IPv6 mismatch:", await callbackFamily("IPv6")); +console.log( + "promise IPv4:", + syncShape(() => dnsPromises.lookup("127.0.0.1", { family: "IPv4" as any })), +); +console.log("negative zero:", await callbackFamily(-0)); diff --git a/test-parity/node-suite/dns/lookup/ip-literals-callback.ts b/test-parity/node-suite/dns/lookup/ip-literals-callback.ts new file mode 100644 index 0000000000..680892dba8 --- /dev/null +++ b/test-parity/node-suite/dns/lookup/ip-literals-callback.ts @@ -0,0 +1,27 @@ +import dns from "node:dns"; + +function lookup(hostname: string, options?: any): Promise { + return new Promise((resolve) => { + const callback = (...args: any[]) => resolve(args); + const request = options === undefined + ? dns.lookup(hostname, callback) + : dns.lookup(hostname, options, callback); + console.log("request object:", typeof request, request !== null); + }); +} + +for ( + const [hostname, options] of [ + ["127.0.0.1", undefined], + ["::1", undefined], + ["127.0.0.1", { all: true }], + ] as const +) { + const [error, value, family] = await lookup(hostname, options); + console.log( + hostname + ":", + error === null, + JSON.stringify(value), + String(family), + ); +} diff --git a/test-parity/node-suite/dns/lookup/ip-literals-promises.ts b/test-parity/node-suite/dns/lookup/ip-literals-promises.ts new file mode 100644 index 0000000000..5499dbbb3e --- /dev/null +++ b/test-parity/node-suite/dns/lookup/ip-literals-promises.ts @@ -0,0 +1,9 @@ +import dnsPromises from "node:dns/promises"; + +console.log("ipv4:", JSON.stringify(await dnsPromises.lookup("127.0.0.1"))); +console.log("ipv6:", JSON.stringify(await dnsPromises.lookup("::1"))); +console.log( + "all:", + JSON.stringify(await dnsPromises.lookup("127.0.0.1", { all: true })), +); +console.log("promise:", dnsPromises.lookup("127.0.0.1") instanceof Promise); diff --git a/test-parity/node-suite/dns/lookup/loopback.ts b/test-parity/node-suite/dns/lookup/loopback.ts index 595bd7c3a3..b1cb0698e1 100644 --- a/test-parity/node-suite/dns/lookup/loopback.ts +++ b/test-parity/node-suite/dns/lookup/loopback.ts @@ -7,7 +7,8 @@ function isLoopback(address: unknown): boolean { function lookupCb(hostname: string, options?: unknown): Promise { return new Promise((resolve) => { - const cb = (err: any, value: any, family?: any) => resolve({ err, value, family }); + const cb = (err: any, value: any, family?: any) => + resolve({ err, value, family }); if (options === undefined) dns.lookup(hostname, cb); else dns.lookup(hostname, options as any, cb); }); @@ -15,9 +16,13 @@ function lookupCb(hostname: string, options?: unknown): Promise { function lookupServiceCb(address: string, port: number): Promise { return new Promise((resolve) => { - dns.lookupService(address, port, (err: any, hostname: any, service: any) => { - resolve({ err, hostname, service }); - }); + dns.lookupService( + address, + port, + (err: any, hostname: any, service: any) => { + resolve({ err, hostname, service }); + }, + ); }); } @@ -31,31 +36,82 @@ function thrownShape(label: string, fn: () => void): void { } const one = await lookupCb("localhost"); -console.log("callback lookup loopback:", one.err === null, isLoopback(one.value), one.family === 4 || one.family === 6); +console.log( + "callback lookup loopback:", + one.err === null, + isLoopback(one.value), + one.family === 4 || one.family === 6, +); const all = await lookupCb("localhost", { all: true }); -console.log("callback lookup all:", all.err === null, Array.isArray(all.value), all.value.every((entry: any) => isLoopback(entry.address))); +console.log( + "callback lookup all:", + all.err === null, + Array.isArray(all.value), + all.value.every((entry: any) => isLoopback(entry.address)), +); const family4 = await lookupCb("localhost", { family: 4 }); -console.log("callback lookup family4:", family4.err === null, family4.value === "127.0.0.1", family4.family); +console.log( + "callback lookup family4:", + family4.err === null, + family4.value === "127.0.0.1", + family4.family, +); const service = await lookupServiceCb("127.0.0.1", 80); -console.log("callback lookupService:", service.err === null, typeof service.hostname, service.service); +console.log( + "callback lookupService:", + service.err === null, + typeof service.hostname, + service.service, +); const promiseOne = await dnsPromises.lookup("localhost"); -console.log("promise lookup loopback:", isLoopback(promiseOne.address), promiseOne.family === 4 || promiseOne.family === 6); +console.log( + "promise lookup loopback:", + isLoopback(promiseOne.address), + promiseOne.family === 4 || promiseOne.family === 6, +); const promiseAll = await dnsPromises.lookup("localhost", { all: true }); -console.log("promise lookup all:", Array.isArray(promiseAll), promiseAll.every((entry: any) => isLoopback(entry.address))); +console.log( + "promise lookup all:", + Array.isArray(promiseAll), + promiseAll.every((entry: any) => isLoopback(entry.address)), +); const promiseFamily4 = await dnsPromises.lookup("localhost", { family: 4 }); -console.log("promise lookup family4:", promiseFamily4.address === "127.0.0.1", promiseFamily4.family); +console.log( + "promise lookup family4:", + promiseFamily4.address === "127.0.0.1", + promiseFamily4.family, +); const promiseService = await dnsPromises.lookupService("127.0.0.1", 80); -console.log("promise lookupService:", typeof promiseService.hostname, promiseService.service); +console.log( + "promise lookupService:", + typeof promiseService.hostname, + promiseService.service, +); -thrownShape("callback lookup missing callback", () => dns.lookup("localhost" as any)); -thrownShape("callback lookup invalid family", () => dns.lookup("localhost", { family: 5 } as any, () => {})); -thrownShape("callback lookupService bad port", () => dns.lookupService("127.0.0.1", -1, () => {})); -thrownShape("promise lookup invalid family", () => dnsPromises.lookup("localhost", { family: 5 } as any)); -thrownShape("promise lookupService bad port", () => dnsPromises.lookupService("127.0.0.1", -1)); +thrownShape( + "callback lookup missing callback", + () => dns.lookup("localhost" as any), +); +thrownShape( + "callback lookup invalid family", + () => dns.lookup("localhost", { family: 5 } as any, () => {}), +); +thrownShape( + "callback lookupService bad port", + () => dns.lookupService("127.0.0.1", -1, () => {}), +); +thrownShape( + "promise lookup invalid family", + () => dnsPromises.lookup("localhost", { family: 5 } as any), +); +thrownShape( + "promise lookupService bad port", + () => dnsPromises.lookupService("127.0.0.1", -1), +); diff --git a/test-parity/node-suite/dns/lookup/options-accessors.ts b/test-parity/node-suite/dns/lookup/options-accessors.ts new file mode 100644 index 0000000000..bcdcd25606 --- /dev/null +++ b/test-parity/node-suite/dns/lookup/options-accessors.ts @@ -0,0 +1,33 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function options(log: string[]): any { + const value: any = {}; + for ( + const [key, result] of [ + ["hints", 0], + ["family", 4], + ["all", false], + ["verbatim", true], + ["order", "ipv4first"], + ] as const + ) { + Object.defineProperty(value, key, { + get() { + log.push(key); + return result; + }, + }); + } + return value; +} + +const callbackLog: string[] = []; +await new Promise((resolve) => { + dns.lookup("127.0.0.1", options(callbackLog), () => resolve()); +}); +console.log("callback access:", callbackLog.join("|")); + +const promiseLog: string[] = []; +await dnsPromises.lookup("127.0.0.1", options(promiseLog)); +console.log("promise access:", promiseLog.join("|")); diff --git a/test-parity/node-suite/dns/lookup/options-validation.ts b/test-parity/node-suite/dns/lookup/options-validation.ts new file mode 100644 index 0000000000..9bc4b54330 --- /dev/null +++ b/test-parity/node-suite/dns/lookup/options-validation.ts @@ -0,0 +1,29 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function shape(fn: () => unknown): string { + try { + fn(); + return "no throw"; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +const cases: Array<[string, any]> = [ + ["options", "6"], + ["family", { family: "6" }], + ["hints type", { hints: "0" }], + ["hints value", { hints: -1 }], + ["all", { all: 1 }], + ["verbatim", { verbatim: "true" }], + ["order", { order: "true" }], +]; + +for (const [label, options] of cases) { + console.log( + label + ":", + shape(() => dns.lookup("127.0.0.1", options, () => {})), + shape(() => dnsPromises.lookup("127.0.0.1", options)), + ); +} diff --git a/test-parity/node-suite/dns/lookup/promisify.ts b/test-parity/node-suite/dns/lookup/promisify.ts new file mode 100644 index 0000000000..588c367f5d --- /dev/null +++ b/test-parity/node-suite/dns/lookup/promisify.ts @@ -0,0 +1,26 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; +import { promisify } from "node:util"; + +const lookup = promisify(dns.lookup); +const lookupService = promisify(dns.lookupService); + +console.log("lookup result:", JSON.stringify(await lookup("127.0.0.1"))); +const service: any = await lookupService("127.0.0.1", 0); +console.log( + "service keys:", + Object.keys(service).sort().join("|"), + typeof service.hostname, + typeof service.service, +); +console.log( + "custom identity:", + (dns.lookup as any)[promisify.custom] === dnsPromises.lookup, +); +console.log( + "custom args enumerable:", + Object.getOwnPropertyDescriptor( + dns.lookup, + Symbol.for("nodejs.util.promisify.customArgs"), + )?.enumerable, +); diff --git a/test-parity/node-suite/dns/resolve/address-records.ts b/test-parity/node-suite/dns/resolve/address-records.ts new file mode 100644 index 0000000000..aee84fd14a --- /dev/null +++ b/test-parity/node-suite/dns/resolve/address-records.ts @@ -0,0 +1,49 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; +import { startDnsServer } from "../fixtures/local-dns-server.mjs"; + +function callback4(options?: any): Promise { + return new Promise((resolve, reject) => { + const done = (error: any, value: any) => + error ? reject(error) : resolve(value); + options === undefined + ? dns.resolve4("example.test", done) + : dns.resolve4("example.test", options, done); + }); +} + +function callback6(): Promise { + return new Promise((resolve, reject) => { + dns.resolve6( + "example.test", + (error, value) => error ? reject(error) : resolve(value), + ); + }); +} + +const server = await startDnsServer(); +try { + const address = `127.0.0.1:${server.port}`; + dns.setServers([address]); + dnsPromises.setServers([address]); + console.log("callback A:", JSON.stringify(await callback4())); + console.log( + "callback A ttl:", + JSON.stringify(await callback4({ ttl: true })), + ); + console.log("callback AAAA:", JSON.stringify(await callback6())); + console.log( + "promise A:", + JSON.stringify(await dnsPromises.resolve4("example.test")), + ); + console.log( + "promise A ttl:", + JSON.stringify(await dnsPromises.resolve4("example.test", { ttl: true })), + ); + console.log( + "promise AAAA:", + JSON.stringify(await dnsPromises.resolve6("example.test")), + ); +} finally { + await server.close(); +} diff --git a/test-parity/node-suite/dns/resolve/any-records.ts b/test-parity/node-suite/dns/resolve/any-records.ts new file mode 100644 index 0000000000..11c1a552ea --- /dev/null +++ b/test-parity/node-suite/dns/resolve/any-records.ts @@ -0,0 +1,30 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; +import { startDnsServer } from "../fixtures/local-dns-server.mjs"; + +function stable(value: any[]): string { + return JSON.stringify(value.map((record) => { + const copy = { ...record }; + if (copy.type !== "A" && copy.type !== "AAAA") delete copy.ttl; + return Object.fromEntries( + Object.keys(copy).sort().map((key) => [key, copy[key]]), + ); + })); +} + +const server = await startDnsServer(); +try { + const address = `127.0.0.1:${server.port}`; + dns.setServers([address]); + dnsPromises.setServers([address]); + const callback: any[] = await new Promise((resolve, reject) => { + dns.resolveAny( + "example.test", + (error, value) => error ? reject(error) : resolve(value), + ); + }); + console.log("callback:", stable(callback)); + console.log("promise:", stable(await dnsPromises.resolveAny("example.test"))); +} finally { + await server.close(); +} diff --git a/test-parity/node-suite/dns/resolve/errors.ts b/test-parity/node-suite/dns/resolve/errors.ts new file mode 100644 index 0000000000..c211e97514 --- /dev/null +++ b/test-parity/node-suite/dns/resolve/errors.ts @@ -0,0 +1,28 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; +import { startDnsServer } from "../fixtures/local-dns-server.mjs"; + +function summary(error: any): string { + return `${error.name}/${error.code}/${error.syscall}/${error.hostname}/${ + Object.keys(error).sort().join("|") + }`; +} + +for (const mode of ["nxdomain", "nodata", "refused"] as const) { + const server = await startDnsServer(mode); + try { + const address = `127.0.0.1:${server.port}`; + dns.setServers([address]); + dnsPromises.setServers([address]); + const callback = await new Promise((resolve) => { + dns.resolve4(`${mode}.example.test`, (error) => resolve(summary(error))); + }); + const promise = await dnsPromises.resolve4(`${mode}.example.test`).then( + () => "resolved", + (error) => summary(error), + ); + console.log(mode + ":", callback, promise); + } finally { + await server.close(); + } +} diff --git a/test-parity/node-suite/dns/resolve/idna.ts b/test-parity/node-suite/dns/resolve/idna.ts new file mode 100644 index 0000000000..018472f0ce --- /dev/null +++ b/test-parity/node-suite/dns/resolve/idna.ts @@ -0,0 +1,25 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; +import { startDnsServer } from "../fixtures/local-dns-server.mjs"; + +const server = await startDnsServer(); +try { + const address = `127.0.0.1:${server.port}`; + dns.setServers([address]); + dnsPromises.setServers([address]); + const callback = await new Promise((resolve) => { + dns.resolve4( + "mañana.example", + (error, value) => + resolve(error ? `error:${error.code}` : JSON.stringify(value)), + ); + }); + console.log("callback:", callback); + const promise = await dnsPromises.resolve4("mañana.example").then( + (value) => JSON.stringify(value), + (error) => `error:${error.code}`, + ); + console.log("promise:", promise); +} finally { + await server.close(); +} diff --git a/test-parity/node-suite/dns/resolve/localhost.ts b/test-parity/node-suite/dns/resolve/localhost.ts deleted file mode 100644 index d495aaef5a..0000000000 --- a/test-parity/node-suite/dns/resolve/localhost.ts +++ /dev/null @@ -1,72 +0,0 @@ -import * as dns from "node:dns"; -import * as dnsPromises from "node:dns/promises"; - -// resolve* answers depend on the machine's configured nameserver (some -// resolvers answer A/AAAA for "localhost", some return ENODATA/NXDOMAIN), so -// every check prints either the matched record or the error code instead of -// assuming success — node and Perry must agree either way. - -function callbackCall(fn: (cb: (err: any, value: any) => void) => void): Promise { - return new Promise((resolve) => { - fn((err, value) => { - resolve({ err, value }); - }); - }); -} - -function summarize(err: any, value: unknown, expected: string): string { - if (err) return "err:" + err.code; - if (!Array.isArray(value)) return "value:" + typeof value; - return value.includes(expected) ? "has " + expected : JSON.stringify(value); -} - -function reverseSummary(err: any, value: unknown): string { - if (err) return "err:" + err.code; - return JSON.stringify(value); -} - -function thrownShape(label: string, fn: () => void): void { - try { - fn(); - console.log(label + ":", "no throw"); - } catch (e: any) { - console.log(label + ":", e.name, e.code); - } -} - -const callback4 = await callbackCall((cb) => dns.resolve4("localhost", cb)); -const callback6 = await callbackCall((cb) => dns.resolve6("localhost", cb)); -const callbackA = await callbackCall((cb) => dns.resolve("localhost", "A", cb)); -const callbackReverse = await callbackCall((cb) => dns.reverse("127.0.0.1", cb)); -console.log("callback resolve4:", summarize(callback4.err, callback4.value, "127.0.0.1")); -console.log("callback resolve6:", summarize(callback6.err, callback6.value, "::1")); -console.log("callback resolve A:", summarize(callbackA.err, callbackA.value, "127.0.0.1")); -console.log("callback reverse:", reverseSummary(callbackReverse.err, callbackReverse.value)); - -let promise4: string; -try { - promise4 = summarize(null, await dnsPromises.resolve4("localhost"), "127.0.0.1"); -} catch (e: any) { - promise4 = "err:" + e.code; -} -console.log("promise resolve4:", promise4); - -let promiseReverse: string; -try { - promiseReverse = reverseSummary(null, await dnsPromises.reverse("127.0.0.1")); -} catch (e: any) { - promiseReverse = "err:" + e.code; -} -console.log("promise reverse:", promiseReverse); - -const promiseResolver = new dnsPromises.Resolver(); -let resolver4: string; -try { - resolver4 = summarize(null, await promiseResolver.resolve4("localhost"), "127.0.0.1"); -} catch (e: any) { - resolver4 = "err:" + e.code; -} -console.log("promise resolver resolve4:", resolver4); - -thrownShape("callback bad rrtype", () => dns.resolve("localhost", "BAD", () => {})); -thrownShape("promise bad rrtype", () => dnsPromises.resolve("localhost", "BAD" as any)); diff --git a/test-parity/node-suite/dns/resolve/name-records.ts b/test-parity/node-suite/dns/resolve/name-records.ts new file mode 100644 index 0000000000..d6f11d0652 --- /dev/null +++ b/test-parity/node-suite/dns/resolve/name-records.ts @@ -0,0 +1,50 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; +import { startDnsServer } from "../fixtures/local-dns-server.mjs"; + +function callback( + call: (done: (error: any, value: any) => void) => void, +): Promise { + return new Promise((resolve, reject) => + call((error, value) => error ? reject(error) : resolve(value)) + ); +} + +const server = await startDnsServer(); +try { + const address = `127.0.0.1:${server.port}`; + dns.setServers([address]); + dnsPromises.setServers([address]); + console.log( + "callback resolveCname:", + JSON.stringify( + await callback((done) => dns.resolveCname("example.test", done)), + ), + ); + console.log( + "promise resolveCname:", + JSON.stringify(await dnsPromises.resolveCname("example.test")), + ); + console.log( + "callback resolveNs:", + JSON.stringify( + await callback((done) => dns.resolveNs("example.test", done)), + ), + ); + console.log( + "promise resolveNs:", + JSON.stringify(await dnsPromises.resolveNs("example.test")), + ); + console.log( + "callback resolvePtr:", + JSON.stringify( + await callback((done) => dns.resolvePtr("example.test", done)), + ), + ); + console.log( + "promise resolvePtr:", + JSON.stringify(await dnsPromises.resolvePtr("example.test")), + ); +} finally { + await server.close(); +} diff --git a/test-parity/node-suite/dns/resolve/reverse-validation.ts b/test-parity/node-suite/dns/resolve/reverse-validation.ts new file mode 100644 index 0000000000..76ae720d2f --- /dev/null +++ b/test-parity/node-suite/dns/resolve/reverse-validation.ts @@ -0,0 +1,37 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function shape(fn: () => unknown): string { + try { + const value = fn(); + return value instanceof Promise ? "promise" : typeof value; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +async function promiseShape(value: Promise): Promise { + try { + await value; + return "resolved"; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +console.log( + "callback address:", + shape(() => dns.reverse("not-an-ip", () => {})), +); +console.log( + "callback missing:", + shape(() => (dns.reverse as any)("127.0.0.1")), +); +console.log( + "callback callback:", + shape(() => (dns.reverse as any)("127.0.0.1", null)), +); +console.log( + "promise address:", + await promiseShape(dnsPromises.reverse("not-an-ip")), +); diff --git a/test-parity/node-suite/dns/resolve/rrtype-aliases.ts b/test-parity/node-suite/dns/resolve/rrtype-aliases.ts new file mode 100644 index 0000000000..aa4f459116 --- /dev/null +++ b/test-parity/node-suite/dns/resolve/rrtype-aliases.ts @@ -0,0 +1,41 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; +import { startDnsServer } from "../fixtures/local-dns-server.mjs"; + +function callback( + call: (done: (error: any, value: any) => void) => unknown, +): Promise { + return new Promise((resolve, reject) => { + const returned = call((error, value) => + error ? reject(error) : resolve(value) + ); + console.log("callback return:", typeof returned, returned !== null); + }); +} + +const server = await startDnsServer(); +try { + const address = `127.0.0.1:${server.port}`; + dns.setServers([address]); + dnsPromises.setServers([address]); + console.log( + "default callback:", + JSON.stringify(await callback((done) => dns.resolve("example.test", done))), + ); + console.log( + "A callback:", + JSON.stringify( + await callback((done) => dns.resolve("example.test", "A", done)), + ), + ); + console.log( + "default promise:", + JSON.stringify(await dnsPromises.resolve("example.test")), + ); + console.log( + "A promise:", + JSON.stringify(await dnsPromises.resolve("example.test", "A")), + ); +} finally { + await server.close(); +} diff --git a/test-parity/node-suite/dns/resolve/structured-records.ts b/test-parity/node-suite/dns/resolve/structured-records.ts new file mode 100644 index 0000000000..3094da4ca6 --- /dev/null +++ b/test-parity/node-suite/dns/resolve/structured-records.ts @@ -0,0 +1,70 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; +import { startDnsServer } from "../fixtures/local-dns-server.mjs"; + +function callback( + call: (done: (error: any, value: any) => void) => void, +): Promise { + return new Promise((resolve, reject) => + call((error, value) => error ? reject(error) : resolve(value)) + ); +} + +const server = await startDnsServer(); +try { + const address = `127.0.0.1:${server.port}`; + dns.setServers([address]); + dnsPromises.setServers([address]); + console.log( + "callback resolveCaa:", + JSON.stringify( + await callback((done) => dns.resolveCaa("example.test", done)), + ), + ); + console.log( + "promise resolveCaa:", + JSON.stringify(await dnsPromises.resolveCaa("example.test")), + ); + console.log( + "callback resolveMx:", + JSON.stringify( + await callback((done) => dns.resolveMx("example.test", done)), + ), + ); + console.log( + "promise resolveMx:", + JSON.stringify(await dnsPromises.resolveMx("example.test")), + ); + console.log( + "callback resolveNaptr:", + JSON.stringify( + await callback((done) => dns.resolveNaptr("example.test", done)), + ), + ); + console.log( + "promise resolveNaptr:", + JSON.stringify(await dnsPromises.resolveNaptr("example.test")), + ); + console.log( + "callback resolveSoa:", + JSON.stringify( + await callback((done) => dns.resolveSoa("example.test", done)), + ), + ); + console.log( + "promise resolveSoa:", + JSON.stringify(await dnsPromises.resolveSoa("example.test")), + ); + console.log( + "callback resolveSrv:", + JSON.stringify( + await callback((done) => dns.resolveSrv("example.test", done)), + ), + ); + console.log( + "promise resolveSrv:", + JSON.stringify(await dnsPromises.resolveSrv("example.test")), + ); +} finally { + await server.close(); +} diff --git a/test-parity/node-suite/dns/resolve/txt-record.ts b/test-parity/node-suite/dns/resolve/txt-record.ts new file mode 100644 index 0000000000..ab641dbd63 --- /dev/null +++ b/test-parity/node-suite/dns/resolve/txt-record.ts @@ -0,0 +1,23 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; +import { startDnsServer } from "../fixtures/local-dns-server.mjs"; + +const server = await startDnsServer(); +try { + const address = `127.0.0.1:${server.port}`; + dns.setServers([address]); + dnsPromises.setServers([address]); + const callback = await new Promise((resolve, reject) => { + dns.resolveTxt( + "example.test", + (error, value) => error ? reject(error) : resolve(value), + ); + }); + console.log("callback:", JSON.stringify(callback)); + console.log( + "promise:", + JSON.stringify(await dnsPromises.resolveTxt("example.test")), + ); +} finally { + await server.close(); +} diff --git a/test-parity/node-suite/dns/resolver/cancel-active.ts b/test-parity/node-suite/dns/resolver/cancel-active.ts new file mode 100644 index 0000000000..960f6b9cd4 --- /dev/null +++ b/test-parity/node-suite/dns/resolver/cancel-active.ts @@ -0,0 +1,33 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; +import { startDnsServer } from "../fixtures/local-dns-server.mjs"; + +const server = await startDnsServer("silent"); +try { + const address = `127.0.0.1:${server.port}`; + dns.setServers([address]); + dnsPromises.setServers([address]); + + const callbackResolver = new dns.Resolver(); + callbackResolver.setServers([address]); + const callbackResult = new Promise((resolve) => { + callbackResolver.resolve4( + "callback.example.test", + (error) => resolve(`${error?.name}/${error?.code}/${error?.syscall}`), + ); + }); + await server.nextQuery(); + callbackResolver.cancel(); + console.log("callback:", await callbackResult); + + const promiseResolver = new dnsPromises.Resolver(); + promiseResolver.setServers([address]); + const promiseResult = promiseResolver.resolve4("promise.example.test").catch(( + error, + ) => `${error.name}/${error.code}/${error.syscall}`); + await server.nextQuery(); + promiseResolver.cancel(); + console.log("promise:", await promiseResult); +} finally { + await server.close(); +} diff --git a/test-parity/node-suite/dns/resolver/cancel-idempotent.ts b/test-parity/node-suite/dns/resolver/cancel-idempotent.ts new file mode 100644 index 0000000000..98282072b0 --- /dev/null +++ b/test-parity/node-suite/dns/resolver/cancel-idempotent.ts @@ -0,0 +1,17 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +for ( + const [label, resolver] of [ + ["callback", new dns.Resolver()], + ["promises", new dnsPromises.Resolver()], + ] as const +) { + resolver.setServers(["127.0.0.1:5300"]); + console.log( + label + ":", + resolver.cancel(), + resolver.cancel(), + resolver.getServers().length > 0, + ); +} diff --git a/test-parity/node-suite/dns/resolver/constructor-validation.ts b/test-parity/node-suite/dns/resolver/constructor-validation.ts new file mode 100644 index 0000000000..7c1a242382 --- /dev/null +++ b/test-parity/node-suite/dns/resolver/constructor-validation.ts @@ -0,0 +1,34 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function shape(Resolver: any, options: any): string { + try { + new Resolver(options); + return "ok"; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +const cases: Array<[string, any]> = [ + ["undefined", undefined], + ["null", null], + ["timeout -1", { timeout: -1 }], + ["timeout zero", { timeout: -0 }], + ["timeout float", { timeout: 1.5 }], + ["timeout type", { timeout: "1" }], + ["tries one", { tries: 1 }], + ["tries zero", { tries: 0 }], + ["tries type", { tries: "1" }], + ["max zero", { maxTimeout: -0 }], + ["max negative", { maxTimeout: -1 }], + ["max float", { maxTimeout: 1.5 }], +]; + +for (const [label, options] of cases) { + console.log( + label + ":", + shape(dns.Resolver, options), + shape(dnsPromises.Resolver, options), + ); +} diff --git a/test-parity/node-suite/dns/resolver/method-metadata.ts b/test-parity/node-suite/dns/resolver/method-metadata.ts new file mode 100644 index 0000000000..446e9f0680 --- /dev/null +++ b/test-parity/node-suite/dns/resolver/method-metadata.ts @@ -0,0 +1,38 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +const methods = [ + "resolve", + "resolve4", + "resolve6", + "resolveAny", + "resolveCaa", + "resolveCname", + "resolveMx", + "resolveNaptr", + "resolveNs", + "resolvePtr", + "resolveSoa", + "resolveSrv", + "resolveTlsa", + "resolveTxt", + "reverse", + "cancel", + "getServers", + "setServers", + "setLocalAddress", +]; + +function metadata(prototype: any): string { + return methods.map((name) => { + const value = prototype[name]; + return `${name}=${ + typeof value === "function" + ? `${value.name}/${value.length}` + : typeof value + }`; + }).join("|"); +} + +console.log("callback:", metadata(dns.Resolver.prototype)); +console.log("promises:", metadata(dnsPromises.Resolver.prototype)); diff --git a/test-parity/node-suite/dns/resolver/options-accessors.ts b/test-parity/node-suite/dns/resolver/options-accessors.ts new file mode 100644 index 0000000000..34e90e46ba --- /dev/null +++ b/test-parity/node-suite/dns/resolver/options-accessors.ts @@ -0,0 +1,25 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function run(Resolver: any): string { + const log: string[] = []; + const prototype = { + get timeout() { + log.push("timeout"); + return -1; + }, + get tries() { + log.push("tries"); + return 4; + }, + get maxTimeout() { + log.push("maxTimeout"); + return 0; + }, + }; + new Resolver(Object.create(prototype)); + return log.join("|"); +} + +console.log("callback:", run(dns.Resolver)); +console.log("promises:", run(dnsPromises.Resolver)); diff --git a/test-parity/node-suite/dns/resolver/prototype.ts b/test-parity/node-suite/dns/resolver/prototype.ts new file mode 100644 index 0000000000..7686bec832 --- /dev/null +++ b/test-parity/node-suite/dns/resolver/prototype.ts @@ -0,0 +1,22 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function ownNames(value: object): string { + return Object.getOwnPropertyNames(value).sort().join("|"); +} + +console.log("callback prototype:", ownNames(dns.Resolver.prototype)); +console.log("promise prototype:", ownNames(dnsPromises.Resolver.prototype)); +console.log( + "callback base:", + ownNames(Object.getPrototypeOf(dns.Resolver.prototype)), +); +console.log( + "promise base:", + ownNames(Object.getPrototypeOf(dnsPromises.Resolver.prototype)), +); +console.log( + "constructors:", + new dns.Resolver() instanceof dns.Resolver, + new dnsPromises.Resolver() instanceof dnsPromises.Resolver, +); diff --git a/test-parity/node-suite/dns/resolver/receiver-validation.ts b/test-parity/node-suite/dns/resolver/receiver-validation.ts new file mode 100644 index 0000000000..e3201bd57b --- /dev/null +++ b/test-parity/node-suite/dns/resolver/receiver-validation.ts @@ -0,0 +1,32 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function shape(fn: () => unknown): string { + try { + fn(); + return "no throw"; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +for ( + const [label, prototype] of [ + ["callback", dns.Resolver.prototype], + ["promises", dnsPromises.Resolver.prototype], + ] as const +) { + console.log(label + " cancel:", shape(() => prototype.cancel.call({}))); + console.log( + label + " getServers:", + shape(() => prototype.getServers.call({})), + ); + console.log( + label + " setServers:", + shape(() => prototype.setServers.call({}, ["127.0.0.1"])), + ); + console.log( + label + " setLocalAddress:", + shape(() => prototype.setLocalAddress.call({}, "127.0.0.1")), + ); +} diff --git a/test-parity/node-suite/dns/resolver/resolve-receiver.ts b/test-parity/node-suite/dns/resolver/resolve-receiver.ts new file mode 100644 index 0000000000..7271e5d20e --- /dev/null +++ b/test-parity/node-suite/dns/resolver/resolve-receiver.ts @@ -0,0 +1,42 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; +import { startDnsServer } from "../fixtures/local-dns-server.mjs"; + +function shape(fn: () => unknown): string { + try { + const value = fn(); + return value instanceof Promise ? "promise" : typeof value; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +async function asyncShape(fn: () => Promise): Promise { + try { + await fn(); + return "resolved"; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +const server = await startDnsServer(); +try { + const address = `127.0.0.1:${server.port}`; + dns.setServers([address]); + dnsPromises.setServers([address]); + console.log( + "callback:", + shape(() => + dns.Resolver.prototype.resolve4.call({}, "example.test", () => {}) + ), + ); + console.log( + "promise:", + await asyncShape(() => + dnsPromises.Resolver.prototype.resolve4.call({}, "example.test") + ), + ); +} finally { + await server.close(); +} diff --git a/test-parity/node-suite/dns/resolver/resolve-validation.ts b/test-parity/node-suite/dns/resolver/resolve-validation.ts new file mode 100644 index 0000000000..5b429a5e8a --- /dev/null +++ b/test-parity/node-suite/dns/resolver/resolve-validation.ts @@ -0,0 +1,67 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function shape(fn: () => unknown): string { + try { + const result = fn(); + return result instanceof Promise ? "promise" : typeof result; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +const callback = new dns.Resolver(); +const promises = new dnsPromises.Resolver(); + +for ( + const [label, fn] of [ + [ + "callback missing rrtype", + () => (callback.resolve as any)("example.test"), + ], + [ + "callback bad rrtype", + () => (callback.resolve as any)("example.test", "BAD", () => {}), + ], + [ + "callback rrtype type", + () => (callback.resolve as any)("example.test", [], () => {}), + ], + [ + "callback missing name", + () => (callback.resolve4 as any)(undefined, () => {}), + ], + [ + "callback missing callback", + () => (callback.resolve4 as any)("example.test"), + ], + [ + "callback bad callback", + () => (callback.resolve4 as any)("example.test", null), + ], + ] as const +) { + console.log(label + ":", shape(fn)); +} + +async function promiseShape(fn: () => Promise): Promise { + try { + await fn(); + return "resolved"; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +console.log( + "promise bad rrtype:", + await promiseShape(() => (promises.resolve as any)("example.test", "BAD")), +); +console.log( + "promise rrtype type:", + await promiseShape(() => (promises.resolve as any)("example.test", [])), +); +console.log( + "promise missing name:", + await promiseShape(() => (promises.resolve4 as any)()), +); diff --git a/test-parity/node-suite/dns/resolver/set-local-address.ts b/test-parity/node-suite/dns/resolver/set-local-address.ts new file mode 100644 index 0000000000..afd2051571 --- /dev/null +++ b/test-parity/node-suite/dns/resolver/set-local-address.ts @@ -0,0 +1,36 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function shape(fn: () => unknown): string { + try { + return "ok/" + String(fn()); + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +for ( + const [label, resolver] of [ + ["callback", new dns.Resolver()], + ["promises", new dnsPromises.Resolver()], + ] as const +) { + console.log( + label + " ipv4:", + shape(() => resolver.setLocalAddress("127.0.0.1")), + ); + console.log(label + " ipv6:", shape(() => resolver.setLocalAddress("::1"))); + console.log( + label + " pair:", + shape(() => resolver.setLocalAddress("127.0.0.1", "::1")), + ); + console.log( + label + " wrong pair:", + shape(() => resolver.setLocalAddress("127.0.0.1", "127.0.0.1")), + ); + console.log(label + " bad:", shape(() => resolver.setLocalAddress("bad"))); + console.log( + label + " missing:", + shape(() => (resolver.setLocalAddress as any)()), + ); +} diff --git a/test-parity/node-suite/dns/resolver/set-servers-validation.ts b/test-parity/node-suite/dns/resolver/set-servers-validation.ts new file mode 100644 index 0000000000..3d55008327 --- /dev/null +++ b/test-parity/node-suite/dns/resolver/set-servers-validation.ts @@ -0,0 +1,39 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +function shape(fn: () => unknown): string { + try { + fn(); + return "ok"; + } catch (error: any) { + return `${error.name}/${error.code}`; + } +} + +for ( + const [label, resolver] of [ + ["callback", new dns.Resolver()], + ["promises", new dnsPromises.Resolver()], + ] as const +) { + resolver.setServers(["127.0.0.1"]); + const before = resolver.getServers().join("|"); + console.log( + label + " not array:", + shape(() => resolver.setServers("127.0.0.1" as any)), + ); + console.log( + label + " bad element:", + shape(() => resolver.setServers([123] as any)), + ); + console.log(label + " bad ip:", shape(() => resolver.setServers(["bad"]))); + console.log( + label + " preserved:", + before === resolver.getServers().join("|"), + ); + console.log( + label + " empty:", + shape(() => resolver.setServers([])), + resolver.getServers().length, + ); +} diff --git a/test-parity/node-suite/dns/settings/default-resolver-rebinding.ts b/test-parity/node-suite/dns/settings/default-resolver-rebinding.ts new file mode 100644 index 0000000000..6fdf0d4f82 --- /dev/null +++ b/test-parity/node-suite/dns/settings/default-resolver-rebinding.ts @@ -0,0 +1,32 @@ +import dns from "node:dns"; +import dnsPromises from "node:dns/promises"; + +const callbackBefore = dns.resolve4; +const promiseBefore = dnsPromises.resolve4; + +function promiseServers(): string { + return typeof dnsPromises.getServers === "function" + ? dnsPromises.getServers().join("|") + : "absent"; +} + +dns.setServers(["127.0.0.1:5301"]); +console.log("callback rebound:", callbackBefore !== dns.resolve4); +console.log( + "promise rebound by callback:", + promiseBefore !== dnsPromises.resolve4, +); +console.log( + "shared callback set:", + dns.getServers().join("|"), + promiseServers(), +); + +const callbackAfter = dns.resolve4; +const promiseAfter = dnsPromises.resolve4; +if (typeof dnsPromises.setServers === "function") { + dnsPromises.setServers(["127.0.0.1:5302"]); +} +console.log("callback rebound by promise:", callbackAfter !== dns.resolve4); +console.log("promise rebound:", promiseAfter !== dnsPromises.resolve4); +console.log("promise-only set:", dns.getServers().join("|"), promiseServers()); diff --git a/test-parity/node-suite/dns/settings/default-result-order.ts b/test-parity/node-suite/dns/settings/default-result-order.ts index e04ea2eb70..5c5fbd114c 100644 --- a/test-parity/node-suite/dns/settings/default-result-order.ts +++ b/test-parity/node-suite/dns/settings/default-result-order.ts @@ -1,18 +1,6 @@ import * as dns from "node:dns"; import * as dnsPromises from "node:dns/promises"; -function lookupCb(hostname: string): Promise { - return new Promise((resolve) => { - dns.lookup(hostname, (err, address, family) => { - resolve({ err, address, family }); - }); - }); -} - -function orderSummary(values: Array<{ address: string; family: number }>): string { - return values.map((value) => `${value.family}:${value.address}`).join("|"); -} - function thrownShape(label: string, fn: () => void): void { try { fn(); @@ -22,22 +10,24 @@ function thrownShape(label: string, fn: () => void): void { } } +function getPromiseOrder(): string { + return typeof dnsPromises.getDefaultResultOrder === "function" + ? dnsPromises.getDefaultResultOrder() + : "absent"; +} + +console.log("initial:", dns.getDefaultResultOrder(), getPromiseOrder()); + dns.setDefaultResultOrder("ipv4first"); -const callback4 = await lookupCb("localhost"); -const promise4 = await dnsPromises.lookup("localhost"); -const all4 = await dnsPromises.lookup("localhost", { all: true }); -console.log("ipv4 callback:", callback4.err === null, callback4.address, callback4.family); -console.log("ipv4 promise:", promise4.address, promise4.family); -console.log("ipv4 all:", orderSummary(all4)); +console.log("callback set:", dns.getDefaultResultOrder(), getPromiseOrder()); + +if (typeof dnsPromises.setDefaultResultOrder === "function") { + dnsPromises.setDefaultResultOrder("ipv6first"); +} +console.log("promise set:", dns.getDefaultResultOrder(), getPromiseOrder()); -dnsPromises.setDefaultResultOrder("ipv6first"); -const callback6 = await lookupCb("localhost"); -const promise6 = await dnsPromises.lookup("localhost"); -const all6 = await dnsPromises.lookup("localhost", { all: true }); -console.log("ipv6 callback:", callback6.err === null, callback6.address, callback6.family); -console.log("ipv6 promise:", promise6.address, promise6.family); -console.log("ipv6 all:", orderSummary(all6)); -console.log("shared order:", dns.getDefaultResultOrder(), dnsPromises.getDefaultResultOrder()); +dns.setDefaultResultOrder("verbatim"); +console.log("verbatim:", dns.getDefaultResultOrder(), getPromiseOrder()); thrownShape("invalid order", () => dns.setDefaultResultOrder("bad" as any)); console.log("order preserved:", dns.getDefaultResultOrder()); diff --git a/test-parity/node-suite/dns/settings/servers-array-semantics.ts b/test-parity/node-suite/dns/settings/servers-array-semantics.ts new file mode 100644 index 0000000000..9fcff17329 --- /dev/null +++ b/test-parity/node-suite/dns/settings/servers-array-semantics.ts @@ -0,0 +1,28 @@ +import dns from "node:dns"; + +const sparse: string[] = []; +sparse[0] = "127.0.0.1"; +sparse[2] = "0.0.0.0"; +dns.setServers(sparse); +console.log("sparse:", dns.getServers().join("|")); + +const accessed: number[] = []; +const shrinking = ["127.0.0.1", "192.168.1.1", "unused", "127.1.0.1"]; +Object.defineProperty(shrinking, 2, { + enumerable: true, + get() { + accessed.push(2); + shrinking.length = 3; + return "0.0.0.0"; + }, +}); +dns.setServers(shrinking); +console.log("shrinking:", dns.getServers().join("|"), accessed.join("|")); + +const before = dns.getServers().join("|"); +try { + dns.setServers(["invalid"]); +} catch (error: any) { + console.log("invalid:", error.name, error.code); +} +console.log("preserved:", before === dns.getServers().join("|")); diff --git a/test-parity/node-suite/dns/settings/servers-normalization.ts b/test-parity/node-suite/dns/settings/servers-normalization.ts new file mode 100644 index 0000000000..300d60dcfb --- /dev/null +++ b/test-parity/node-suite/dns/settings/servers-normalization.ts @@ -0,0 +1,13 @@ +import dns from "node:dns"; + +for ( + const servers of [ + ["4.4.4.4:53", "[2001:4860:4860::8888]:53"], + ["103.238.225.181:666", "[fe80::483a:5aff:fee6:1f04]:666"], + ["fe80::483a:5aff:fee6:1f04", "[fe80::483a:5aff:fee6:1f04]"], + [], + ] +) { + dns.setServers(servers); + console.log(JSON.stringify(servers) + ":", dns.getServers().join("|")); +} diff --git a/test-parity/node-suite/dns/settings/servers.ts b/test-parity/node-suite/dns/settings/servers.ts index c11e856b82..3977b976a7 100644 --- a/test-parity/node-suite/dns/settings/servers.ts +++ b/test-parity/node-suite/dns/settings/servers.ts @@ -10,21 +10,43 @@ function thrownShape(label: string, fn: () => void): void { } } -dns.setServers(["8.8.8.8", "[2001:4860:4860::8888]:53", "1.1.1.1:5353", "[2001:4860:4860::8844]:5353"]); -dnsPromises.setServers(["9.9.9.9"]); +dns.setServers([ + "8.8.8.8", + "[2001:4860:4860::8888]:53", + "1.1.1.1:5353", + "[2001:4860:4860::8844]:5353", +]); +if (typeof dnsPromises.setServers === "function") { + dnsPromises.setServers(["9.9.9.9"]); +} console.log("callback servers:", dns.getServers().join("|")); -console.log("promise servers:", dnsPromises.getServers().join("|")); +console.log( + "promise servers:", + typeof dnsPromises.getServers === "function" + ? dnsPromises.getServers().join("|") + : "absent", +); const resolverA = new dns.Resolver(); const resolverB = new dns.Resolver(); resolverA.setServers(["4.4.4.4"]); resolverB.setServers(["[2001:db8::1]:5353"]); -console.log("resolver servers:", resolverA.getServers().join("|"), resolverB.getServers().join("|")); +console.log( + "resolver servers:", + resolverA.getServers().join("|"), + resolverB.getServers().join("|"), +); console.log("module servers unchanged:", dns.getServers().join("|")); const promiseResolver = new dnsPromises.Resolver(); promiseResolver.setServers(["5.5.5.5"]); -console.log("promise resolver servers:", promiseResolver.getServers().join("|"), dnsPromises.getServers().join("|")); +console.log( + "promise resolver servers:", + promiseResolver.getServers().join("|"), + typeof dnsPromises.getServers === "function" + ? dnsPromises.getServers().join("|") + : "absent", +); console.log("cancel returns:", resolverA.cancel(), promiseResolver.cancel()); thrownShape("invalid not array", () => dns.setServers("8.8.8.8" as any)); diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index b2287fc0bc..c7dfe4064e 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -50,8 +50,8 @@ "total": 69 }, "dns": { - "pass": 6, - "total": 6 + "pass": 18, + "total": 43 }, "domain": { "pass": 3, From 89565bdf47d54886f5de903ca0c8a440d7281f53 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Sun, 26 Jul 2026 23:44:25 +0200 Subject: [PATCH 2/7] fix(dns): address parity review feedback --- test-parity/node-suite/dns/EVIDENCE.md | 101 ---------------- test-parity/node-suite/dns/README.md | 113 ++++++++++++++++-- .../dns/fixtures/local-dns-server.mjs | 19 ++- .../node-suite/dns/imports/descriptors.ts | 6 +- test-parity/node-suite/dns/resolve/idna.ts | 2 +- .../node-suite/dns/resolver/cancel-active.ts | 5 +- .../dns/resolver/resolve-validation.ts | 14 +++ 7 files changed, 143 insertions(+), 117 deletions(-) delete mode 100644 test-parity/node-suite/dns/EVIDENCE.md diff --git a/test-parity/node-suite/dns/EVIDENCE.md b/test-parity/node-suite/dns/EVIDENCE.md deleted file mode 100644 index bdab1c1c2d..0000000000 --- a/test-parity/node-suite/dns/EVIDENCE.md +++ /dev/null @@ -1,101 +0,0 @@ -# `node:dns` parity evidence - -## Environment and result - -- Oracle: Node 26.5.0, commit `bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb`. -- Perry: branch `test/expand-node-dns-parity`, built with - `cargo build --release --bin perry`. -- Alternate execution: Deno 2.9.3 and Bun 1.2.18. -- Alternate source review: Deno `34c46613cbe20450b74c0e8d4f0fd8f6f781d807` and - Bun `44f6469e0d4ae93467aa65c7e3bc9001000c7b31`. - -Three complete Node rounds ran all 43 fixtures with zero errors, crashes, or -timeouts and byte-identical aggregate SHA-256 -`c08378de2b06928db9a288ddfc6515bdbc401d814dba853a130d09adfda5a412`. Three -complete focused Perry runs produced the same **18 pass / 25 diff / 0 compile -failure / 0 crash / 0 timeout** result. The baseline records `18/43`. - -One complete alternate-runtime pass produced: - -- Deno: 21 exact matches and 22 diffs; no error, crash, or timeout. -- Bun: 13 exact matches and 30 diffs; no error, crash, or timeout. - -## Stable Perry differences - -- Module surface: the `promises` property is a data property rather than Node's - lazy getter; public functions use different names and arities. -- Lookup: callback requests return `undefined`; callback checks, falsy-host - errors, string families, option getter order, validation, and promisification - differ. -- Lookup service: numeric-string promise coercion and argument checks differ. -- Resolution: TTL objects, typed record fields, callback request objects, and - enumerable DNS error fields differ. A/AAAA values, ANY values after key - canonicalization, IDNA, name records, and TXT records match. -- Resolver: active `cancel()` reports `ETIMEOUT` instead of `ECANCELLED`; - constructor checks, prototype layout, method metadata, resolve validation, and - local-address validation differ. -- Settings: default-resolver method rebinding, sparse/accessor server arrays, - and one bracketed IPv6 normalization case differ. - -## Per-fixture classification - -`pass` and `match` mean exact stdout and exit-code parity with Node 26.5.0. -`diff` means the fixture completed but exposed a stable contract difference. - -| Fixture | Perry | Deno | Bun | -| ---------------------------------------- | ----- | ----- | ----- | -| `constants/error-aliases.ts` | pass | diff | match | -| `imports/aliases.ts` | pass | diff | diff | -| `imports/default-export.ts` | pass | match | match | -| `imports/descriptors.ts` | diff | diff | diff | -| `imports/export-inventory.ts` | pass | diff | diff | -| `imports/function-metadata.ts` | diff | diff | diff | -| `lookup-service/ipv6-loopback.ts` | pass | match | diff | -| `lookup-service/port-coercion.ts` | diff | diff | diff | -| `lookup-service/validation.ts` | diff | match | match | -| `lookup/callback-validation.ts` | diff | match | match | -| `lookup/falsy-hostname.ts` | diff | diff | diff | -| `lookup/family-forms.ts` | diff | diff | diff | -| `lookup/ip-literals-callback.ts` | diff | match | diff | -| `lookup/ip-literals-promises.ts` | pass | match | match | -| `lookup/loopback.ts` | pass | match | match | -| `lookup/options-accessors.ts` | diff | diff | diff | -| `lookup/options-validation.ts` | diff | match | match | -| `lookup/promisify.ts` | diff | diff | diff | -| `resolve/address-records.ts` | diff | match | diff | -| `resolve/any-records.ts` | pass | match | match | -| `resolve/errors.ts` | diff | diff | diff | -| `resolve/idna.ts` | pass | match | diff | -| `resolve/name-records.ts` | pass | diff | match | -| `resolve/reverse-validation.ts` | diff | diff | diff | -| `resolve/rrtype-aliases.ts` | diff | match | diff | -| `resolve/structured-records.ts` | diff | diff | diff | -| `resolve/txt-record.ts` | pass | match | diff | -| `resolver/cancel-active.ts` | diff | match | diff | -| `resolver/cancel-idempotent.ts` | pass | match | match | -| `resolver/constructor-validation.ts` | diff | match | diff | -| `resolver/method-metadata.ts` | diff | diff | diff | -| `resolver/options-accessors.ts` | pass | diff | diff | -| `resolver/prototype.ts` | diff | diff | diff | -| `resolver/receiver-validation.ts` | pass | match | diff | -| `resolver/resolve-receiver.ts` | pass | match | diff | -| `resolver/resolve-validation.ts` | diff | match | diff | -| `resolver/set-local-address.ts` | diff | diff | diff | -| `resolver/set-servers-validation.ts` | pass | match | match | -| `settings/default-resolver-rebinding.ts` | diff | diff | diff | -| `settings/default-result-order.ts` | pass | diff | diff | -| `settings/servers-array-semantics.ts` | diff | match | match | -| `settings/servers-normalization.ts` | diff | diff | match | -| `settings/servers.ts` | pass | diff | diff | - -## Commands - -```sh -cargo build --release --bin perry -NODE_BIN="$HOME/.nvm/versions/node/v26.5.0/bin/node" \ - python3 scripts/node_suite_run.py target/release/perry "$PWD" dns -python3 -m json.tool test-parity/node_suite_baseline.json >/dev/null -``` - -Local authoritative-server cases need permission to bind ephemeral loopback UDP -ports and spawn the helper Node process. diff --git a/test-parity/node-suite/dns/README.md b/test-parity/node-suite/dns/README.md index 1bb99961c5..c63ff3b8e4 100644 --- a/test-parity/node-suite/dns/README.md +++ b/test-parity/node-suite/dns/README.md @@ -75,10 +75,112 @@ made the same choice; they do not weaken the Node contract. - callback and promise A/AAAA with TTL, ANY, CAA, CNAME, MX, NAPTR, NS, PTR, SOA, SRV, TXT, IDNA, rrtype aliases, reverse validation, and DNS error shape. -Record fixtures use `fixtures/local-dns-server.mjs`. It starts a child Node -process, binds an ephemeral UDP loopback port, emits an explicit ready barrier, -returns fixed TEST-NET/documentation records, and closes in `finally`. No -fixture sends a query to an internet nameserver. +Record fixtures use `fixtures/local-dns-server.mjs`. The `.mjs` extension keeps +the helper out of the runner's recursive `*.ts` fixture discovery and makes its +ES module mode explicit. It starts the same child Node server for every runtime, +binds an ephemeral UDP loopback port, returns fixed TEST-NET/documentation +records, and closes in `finally`. No fixture sends a query to an internet +nameserver. + +## Environment and result + +- Oracle: Node 26.5.0, commit `bebd1b8d92bf4cc917844d6335ed1ecf9c2a75fb`. +- Perry runtime: commit `563c35951b347aabac3e093efd9c8b2af8ecd5d9`, built with + `rustc 1.95.0 (59807616e 2026-04-14)` and `cargo build --release --bin perry`. +- Alternate execution: Deno 2.9.3 and Bun 1.2.18. +- Alternate source review: Deno `34c46613cbe20450b74c0e8d4f0fd8f6f781d807` and + Bun `44f6469e0d4ae93467aa65c7e3bc9001000c7b31`. + +Three complete Node rounds ran all 43 fixtures with zero errors, crashes, or +timeouts and byte-identical aggregate SHA-256 +`014f34411fbadf4ba8a7c0e78fb741053458437133212524553900ae5657c252`. Three +complete focused Perry runs produced the same **18 pass / 25 diff / 0 compile +failure / 0 crash / 0 timeout** result. The baseline records `18/43`. + +One complete alternate-runtime pass produced: + +- Deno: 21 exact matches and 22 diffs; no error, crash, or timeout. +- Bun: 13 exact matches and 30 diffs; no error, crash, or timeout. + +## Stable Perry differences + +- Module surface: the `promises` property is a data property rather than Node's + lazy getter; public functions use different names and arities. +- Lookup: callback requests return `undefined`; callback checks, falsy-host + errors, string families, option getter order, validation, and promisification + differ. +- Lookup service: numeric-string promise coercion and argument checks differ. +- Resolution: TTL objects, typed record fields, callback request objects, and + enumerable DNS error fields differ. A/AAAA values, ANY values after key + canonicalization, IDNA, name records, and TXT records match. +- Resolver: active `cancel()` reports `ETIMEOUT` instead of `ECANCELLED`; + constructor checks, prototype layout, method metadata, resolve validation, and + local-address validation differ. +- Settings: default-resolver method rebinding, sparse/accessor server arrays, + and one bracketed IPv6 normalization case differ. + +## Per-fixture classification + +`pass` and `match` mean exact stdout and exit-code parity with Node 26.5.0. +`diff` means the fixture completed but exposed a stable contract difference. + +| Fixture | Perry | Deno | Bun | +| ---------------------------------------- | ----- | ----- | ----- | +| `constants/error-aliases.ts` | pass | diff | match | +| `imports/aliases.ts` | pass | diff | diff | +| `imports/default-export.ts` | pass | match | match | +| `imports/descriptors.ts` | diff | diff | diff | +| `imports/export-inventory.ts` | pass | diff | diff | +| `imports/function-metadata.ts` | diff | diff | diff | +| `lookup-service/ipv6-loopback.ts` | pass | match | diff | +| `lookup-service/port-coercion.ts` | diff | diff | diff | +| `lookup-service/validation.ts` | diff | match | match | +| `lookup/callback-validation.ts` | diff | match | match | +| `lookup/falsy-hostname.ts` | diff | diff | diff | +| `lookup/family-forms.ts` | diff | diff | diff | +| `lookup/ip-literals-callback.ts` | diff | match | diff | +| `lookup/ip-literals-promises.ts` | pass | match | match | +| `lookup/loopback.ts` | pass | match | match | +| `lookup/options-accessors.ts` | diff | diff | diff | +| `lookup/options-validation.ts` | diff | match | match | +| `lookup/promisify.ts` | diff | diff | diff | +| `resolve/address-records.ts` | diff | match | diff | +| `resolve/any-records.ts` | pass | match | match | +| `resolve/errors.ts` | diff | diff | diff | +| `resolve/idna.ts` | pass | match | diff | +| `resolve/name-records.ts` | pass | diff | match | +| `resolve/reverse-validation.ts` | diff | diff | diff | +| `resolve/rrtype-aliases.ts` | diff | match | diff | +| `resolve/structured-records.ts` | diff | diff | diff | +| `resolve/txt-record.ts` | pass | match | diff | +| `resolver/cancel-active.ts` | diff | match | diff | +| `resolver/cancel-idempotent.ts` | pass | match | match | +| `resolver/constructor-validation.ts` | diff | match | diff | +| `resolver/method-metadata.ts` | diff | diff | diff | +| `resolver/options-accessors.ts` | pass | diff | diff | +| `resolver/prototype.ts` | diff | diff | diff | +| `resolver/receiver-validation.ts` | pass | match | diff | +| `resolver/resolve-receiver.ts` | pass | match | diff | +| `resolver/resolve-validation.ts` | diff | match | diff | +| `resolver/set-local-address.ts` | diff | diff | diff | +| `resolver/set-servers-validation.ts` | pass | match | match | +| `settings/default-resolver-rebinding.ts` | diff | diff | diff | +| `settings/default-result-order.ts` | pass | diff | diff | +| `settings/servers-array-semantics.ts` | diff | match | match | +| `settings/servers-normalization.ts` | diff | diff | match | +| `settings/servers.ts` | pass | diff | diff | + +## Commands + +```sh +cargo build --release --bin perry +NODE_BIN="$HOME/.nvm/versions/node/v26.5.0/bin/node" \ + python3 scripts/node_suite_run.py target/release/perry "$PWD" dns +python3 -m json.tool test-parity/node_suite_baseline.json >/dev/null +``` + +Local authoritative-server cases need permission to bind ephemeral loopback UDP +ports and spawn the helper Node process. ## Stopping boundary @@ -109,6 +211,3 @@ Excluded on purpose: configuration. - DNS-over-TLS/HTTPS, DNSSEC, cache policy, and transport internals are not public `node:dns` contracts in the selected Node suite. - -See [EVIDENCE.md](EVIDENCE.md) for the repeated measurements and per-fixture -cross-runtime classification. diff --git a/test-parity/node-suite/dns/fixtures/local-dns-server.mjs b/test-parity/node-suite/dns/fixtures/local-dns-server.mjs index 415386a6e9..eb8e8a77bd 100644 --- a/test-parity/node-suite/dns/fixtures/local-dns-server.mjs +++ b/test-parity/node-suite/dns/fixtures/local-dns-server.mjs @@ -28,14 +28,23 @@ const records = (type) => { function start() { const socket = dgram.createSocket("udp4"); socket.on("message", (request, remote) => { - let end = 12; - while (request[end] !== 0) end += request[end] + 1; - end += 1; + const labels = []; + let cursor = 12; + while (request[cursor] !== 0) { + const length = request[cursor++]; + labels.push(request.subarray(cursor, cursor + length).toString("ascii")); + cursor += length; + } + const end = cursor + 1; const type = request.readUInt16BE(end); - process.stdout.write("QUERY\n"); + process.stdout.write("QUERY:" + labels.join(".") + "\n"); if (mode === "silent") return; const question = request.subarray(12, end + 4); - const answers = ["nxdomain", "nodata", "refused"].includes(mode) ? [] : records(type); + const answers = ["nxdomain", "nodata", "refused"].includes(mode) + ? [] + : mode === "idna" && labels.join(".") !== "xn--maana-pta.example" + ? [record(1, Buffer.from([203, 0, 113, 8]), 120)] + : records(type); const rcode = mode === "nxdomain" ? 3 : mode === "refused" ? 5 : 0; const header = Buffer.concat([request.subarray(0, 2), Buffer.from([0x81, 0x80 | rcode]), u16(1), u16(answers.length), u16(0), u16(0)]); socket.send(Buffer.concat([header, question, ...answers]), remote.port, remote.address); diff --git a/test-parity/node-suite/dns/imports/descriptors.ts b/test-parity/node-suite/dns/imports/descriptors.ts index 9c0f6b7872..e9b4a5a08b 100644 --- a/test-parity/node-suite/dns/imports/descriptors.ts +++ b/test-parity/node-suite/dns/imports/descriptors.ts @@ -1,7 +1,11 @@ import dns from "node:dns"; for (const key of ["lookup", "Resolver", "ADDRCONFIG", "NODATA", "promises"]) { - const descriptor = Object.getOwnPropertyDescriptor(dns, key)!; + const descriptor = Object.getOwnPropertyDescriptor(dns, key); + if (!descriptor) { + console.log(key + ": missing"); + continue; + } console.log( key + ":", descriptor.enumerable, diff --git a/test-parity/node-suite/dns/resolve/idna.ts b/test-parity/node-suite/dns/resolve/idna.ts index 018472f0ce..72f8786852 100644 --- a/test-parity/node-suite/dns/resolve/idna.ts +++ b/test-parity/node-suite/dns/resolve/idna.ts @@ -2,7 +2,7 @@ import dns from "node:dns"; import dnsPromises from "node:dns/promises"; import { startDnsServer } from "../fixtures/local-dns-server.mjs"; -const server = await startDnsServer(); +const server = await startDnsServer("idna"); try { const address = `127.0.0.1:${server.port}`; dns.setServers([address]); diff --git a/test-parity/node-suite/dns/resolver/cancel-active.ts b/test-parity/node-suite/dns/resolver/cancel-active.ts index 960f6b9cd4..8a5241f392 100644 --- a/test-parity/node-suite/dns/resolver/cancel-active.ts +++ b/test-parity/node-suite/dns/resolver/cancel-active.ts @@ -6,8 +6,9 @@ const server = await startDnsServer("silent"); try { const address = `127.0.0.1:${server.port}`; dns.setServers([address]); - dnsPromises.setServers([address]); - + if (typeof dnsPromises.setServers === "function") { + dnsPromises.setServers([address]); + } const callbackResolver = new dns.Resolver(); callbackResolver.setServers([address]); const callbackResult = new Promise((resolve) => { diff --git a/test-parity/node-suite/dns/resolver/resolve-validation.ts b/test-parity/node-suite/dns/resolver/resolve-validation.ts index 5b429a5e8a..40d9f40ffc 100644 --- a/test-parity/node-suite/dns/resolver/resolve-validation.ts +++ b/test-parity/node-suite/dns/resolver/resolve-validation.ts @@ -1,5 +1,6 @@ import dns from "node:dns"; import dnsPromises from "node:dns/promises"; +import { startDnsServer } from "../fixtures/local-dns-server.mjs"; function shape(fn: () => unknown): string { try { @@ -65,3 +66,16 @@ console.log( "promise missing name:", await promiseShape(() => (promises.resolve4 as any)()), ); + +const server = await startDnsServer(); +try { + const address = `127.0.0.1:${server.port}`; + callback.setServers([address]); + console.log( + "callback default rrtype:", + typeof (callback.resolve as any)("example.test", () => {}), + ); + callback.cancel(); +} finally { + await server.close(); +} From d7561e063b028e046fdfdc6073e086a5fd888d73 Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Sun, 26 Jul 2026 23:58:55 +0200 Subject: [PATCH 3/7] fix(dns): avoid unfinished resolver callback --- test-parity/node-suite/dns/README.md | 6 +++++- .../node-suite/dns/resolver/resolve-validation.ts | 14 -------------- 2 files changed, 5 insertions(+), 15 deletions(-) diff --git a/test-parity/node-suite/dns/README.md b/test-parity/node-suite/dns/README.md index c63ff3b8e4..12f03aaf60 100644 --- a/test-parity/node-suite/dns/README.md +++ b/test-parity/node-suite/dns/README.md @@ -93,7 +93,7 @@ nameserver. Three complete Node rounds ran all 43 fixtures with zero errors, crashes, or timeouts and byte-identical aggregate SHA-256 -`014f34411fbadf4ba8a7c0e78fb741053458437133212524553900ae5657c252`. Three +`e18b3a7e82b9309c1f8db862d25a5ee1ec2210a61a4f0ad09582ebff948a2f2d`. Three complete focused Perry runs produced the same **18 pass / 25 diff / 0 compile failure / 0 crash / 0 timeout** result. The baseline records `18/43`. @@ -195,6 +195,10 @@ Excluded on purpose: - Successful `reverse()` was prototyped against the local server. Perry did not settle within 70 seconds, so retaining it would leave cleanup to the harness timeout and would not isolate a useful result. Input validation remains. +- The valid callback `Resolver.resolve(hostname, callback)` default-rrtype + overload was prototyped against the local server. Node and Deno settled, but + Perry did not invoke the callback. Retaining it would require an arbitrary + timeout or fire-and-forget cleanup; argument validation remains covered. - Node's two-channel query test was prototyped with two local sockets. Perry did not pass the auxiliary-server ready barrier within 30 seconds. Server-state independence remains covered without keeping a timeout that mixes child diff --git a/test-parity/node-suite/dns/resolver/resolve-validation.ts b/test-parity/node-suite/dns/resolver/resolve-validation.ts index 40d9f40ffc..5b429a5e8a 100644 --- a/test-parity/node-suite/dns/resolver/resolve-validation.ts +++ b/test-parity/node-suite/dns/resolver/resolve-validation.ts @@ -1,6 +1,5 @@ import dns from "node:dns"; import dnsPromises from "node:dns/promises"; -import { startDnsServer } from "../fixtures/local-dns-server.mjs"; function shape(fn: () => unknown): string { try { @@ -66,16 +65,3 @@ console.log( "promise missing name:", await promiseShape(() => (promises.resolve4 as any)()), ); - -const server = await startDnsServer(); -try { - const address = `127.0.0.1:${server.port}`; - callback.setServers([address]); - console.log( - "callback default rrtype:", - typeof (callback.resolve as any)("example.test", () => {}), - ); - callback.cancel(); -} finally { - await server.close(); -} From 83c65c8bf9a7a48cece85c2e686b99e7aff0e89a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Wed, 29 Jul 2026 22:33:43 +0200 Subject: [PATCH 4/7] test(dns): expose IDNA wire queries --- test-parity/node-suite/dns/resolve/idna.ts | 2 ++ 1 file changed, 2 insertions(+) diff --git a/test-parity/node-suite/dns/resolve/idna.ts b/test-parity/node-suite/dns/resolve/idna.ts index 72f8786852..07c0614bed 100644 --- a/test-parity/node-suite/dns/resolve/idna.ts +++ b/test-parity/node-suite/dns/resolve/idna.ts @@ -15,11 +15,13 @@ try { ); }); console.log("callback:", callback); + console.log("callback query:", await server.nextQuery()); const promise = await dnsPromises.resolve4("mañana.example").then( (value) => JSON.stringify(value), (error) => `error:${error.code}`, ); console.log("promise:", promise); + console.log("promise query:", await server.nextQuery()); } finally { await server.close(); } From 09462111b9888de4de1ca7a2785a63790bb4634b Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Wed, 29 Jul 2026 23:38:28 +0200 Subject: [PATCH 5/7] test(dns): exercise active cancellation idempotence --- scripts/node_suite_regression_check.py | 11 ++++- .../dns/resolver/cancel-idempotent.ts | 42 ++++++++++++++----- 2 files changed, 40 insertions(+), 13 deletions(-) diff --git a/scripts/node_suite_regression_check.py b/scripts/node_suite_regression_check.py index 028108013e..3571e640c6 100755 --- a/scripts/node_suite_regression_check.py +++ b/scripts/node_suite_regression_check.py @@ -71,8 +71,15 @@ def main(): if cur["pass"] < floor["pass"]: regressions.append( f"{mod}: {cur['pass']}/{cur['total']} < floor {floor['pass']}/{floor['total']} (-{floor['pass'] - cur['pass']})") - elif cur["pass"] > floor["pass"]: - improvements.append(f"{mod}: {cur['pass']}/{cur['total']} (+{cur['pass'] - floor['pass']})") + if cur["total"] < floor["total"]: + regressions.append( + f"{mod}: {cur['total']} fixtures < floor {floor['total']} (-{floor['total'] - cur['total']})") + pass_delta = cur["pass"] - floor["pass"] + fixture_delta = cur["total"] - floor["total"] + if pass_delta > 0 or fixture_delta > 0: + improvements.append( + f"{mod}: {cur['pass']}/{cur['total']} " + f"({pass_delta:+d} passes, {fixture_delta:+d} fixtures)") # Overall is derived, not stored (avoids cross-PR merge conflicts on a # shared aggregate). Compute it from the per-module floors at report time. diff --git a/test-parity/node-suite/dns/resolver/cancel-idempotent.ts b/test-parity/node-suite/dns/resolver/cancel-idempotent.ts index 98282072b0..3b94922779 100644 --- a/test-parity/node-suite/dns/resolver/cancel-idempotent.ts +++ b/test-parity/node-suite/dns/resolver/cancel-idempotent.ts @@ -1,17 +1,37 @@ import dns from "node:dns"; import dnsPromises from "node:dns/promises"; +import { startDnsServer } from "../fixtures/local-dns-server.mjs"; -for ( - const [label, resolver] of [ - ["callback", new dns.Resolver()], - ["promises", new dnsPromises.Resolver()], - ] as const -) { - resolver.setServers(["127.0.0.1:5300"]); +const server = await startDnsServer("silent"); +try { + const address = `127.0.0.1:${server.port}`; + const callbackResolver = new dns.Resolver(); + callbackResolver.setServers([address]); + const callbackResult = new Promise((resolve) => { + callbackResolver.resolve4("callback.example.test", (error) => { + resolve(`${error?.name}/${error?.code}/${error?.syscall}`); + }); + }); + await server.nextQuery(); console.log( - label + ":", - resolver.cancel(), - resolver.cancel(), - resolver.getServers().length > 0, + "callback cancel:", + callbackResolver.cancel(), + callbackResolver.cancel(), ); + console.log("callback result:", await callbackResult); + + const promiseResolver = new dnsPromises.Resolver(); + promiseResolver.setServers([address]); + const promiseResult = promiseResolver.resolve4("promise.example.test").catch(( + error, + ) => `${error.name}/${error.code}/${error.syscall}`); + await server.nextQuery(); + console.log( + "promises cancel:", + promiseResolver.cancel(), + promiseResolver.cancel(), + ); + console.log("promises result:", await promiseResult); +} finally { + await server.close(); } From f22acb4ff93f82314c26842a13eeb944cdba0e9a Mon Sep 17 00:00:00 2001 From: TheHypnoo Date: Thu, 30 Jul 2026 00:00:56 +0200 Subject: [PATCH 6/7] style(codegen): format loop purity match arm --- crates/perry-codegen/src/loop_purity.rs | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/crates/perry-codegen/src/loop_purity.rs b/crates/perry-codegen/src/loop_purity.rs index aeace93d48..364d34cb3b 100644 --- a/crates/perry-codegen/src/loop_purity.rs +++ b/crates/perry-codegen/src/loop_purity.rs @@ -121,9 +121,7 @@ fn expr_alloc_free(e: &Expr) -> bool { // Element READS never allocate — they return an existing element / a // number. Recurse so the object and index are themselves alloc-free. Expr::IndexGet { object, index } => expr_alloc_free(object) && expr_alloc_free(index), - Expr::BufferIndexGet { buffer, index } => { - expr_alloc_free(buffer) && expr_alloc_free(index) - } + Expr::BufferIndexGet { buffer, index } => expr_alloc_free(buffer) && expr_alloc_free(index), Expr::Uint8ArrayGet { array, index } => expr_alloc_free(array) && expr_alloc_free(index), // `arr[i]++` / `--`: read-modify-write of an existing numeric slot, no // growth, no allocation. From 72964ae664b1eb992bac933b719acf2bbe87b3fb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Thu, 30 Jul 2026 05:14:31 +0200 Subject: [PATCH 7/7] test(dns): make expanded parity floor reliable --- changelog.d/6862-dns-parity-expansion.md | 1 + scripts/node_suite_regression_check.py | 19 ++++++++++++++++--- scripts/node_suite_run.py | 16 ++++++++++------ test-parity/node-suite/dns/README.md | 5 +++-- .../dns/resolver/cancel-idempotent.ts | 4 ++++ test-parity/node_suite_baseline.json | 10 ++++++++-- 6 files changed, 42 insertions(+), 13 deletions(-) create mode 100644 changelog.d/6862-dns-parity-expansion.md diff --git a/changelog.d/6862-dns-parity-expansion.md b/changelog.d/6862-dns-parity-expansion.md new file mode 100644 index 0000000000..f4c8501b82 --- /dev/null +++ b/changelog.d/6862-dns-parity-expansion.md @@ -0,0 +1 @@ +Expanded deterministic `node:dns` parity coverage from 6 to 43 fixtures and strengthened the node-suite regression floor to protect both pass counts and fixture counts. diff --git a/scripts/node_suite_regression_check.py b/scripts/node_suite_regression_check.py index 3571e640c6..02fcd5d326 100755 --- a/scripts/node_suite_regression_check.py +++ b/scripts/node_suite_regression_check.py @@ -52,15 +52,23 @@ def main(): print(f"ERROR: runner exited {proc.returncode}", file=sys.stderr) return 2 - # Parse "module pass total %" rows from the runner table. + # Parse "module pass total % outcome=count ..." rows from the runner table. # The header row ("module pass total %") can't match because pass/total # are not digits, so no name-based exclusion is needed — and excluding the # name "module" would wrongly drop the real node:module module. current = {} for line in proc.stdout.splitlines(): - m = re.match(r"^(\S+)\s+(\d+)\s+(\d+)\s+[\d.]+", line) + m = re.match(r"^(\S+)\s+(\d+)\s+(\d+)\s+[\d.]+(?:\s+(.*))?$", line) if m: - current[m.group(1)] = {"pass": int(m.group(2)), "total": int(m.group(3))} + outcomes = { + name: int(count) + for name, count in re.findall(r"(\w+)=(\d+)", m.group(4) or "") + } + current[m.group(1)] = { + "pass": int(m.group(2)), + "total": int(m.group(3)), + "outcomes": outcomes, + } regressions, improvements = [], [] for mod, floor in baseline.items(): @@ -74,6 +82,11 @@ def main(): if cur["total"] < floor["total"]: regressions.append( f"{mod}: {cur['total']} fixtures < floor {floor['total']} (-{floor['total'] - cur['total']})") + for outcome, ceiling in floor.get("outcomes", {}).items(): + count = cur["outcomes"].get(outcome, 0) + if count > ceiling: + regressions.append( + f"{mod}: {outcome}={count} > ceiling {ceiling} (+{count - ceiling})") pass_delta = cur["pass"] - floor["pass"] fixture_delta = cur["total"] - floor["total"] if pass_delta > 0 or fixture_delta > 0: diff --git a/scripts/node_suite_run.py b/scripts/node_suite_run.py index 83f8555ffc..24edea37d3 100644 --- a/scripts/node_suite_run.py +++ b/scripts/node_suite_run.py @@ -103,7 +103,7 @@ def run_one(args): try: n = subprocess.run([NODE, path], capture_output=True, text=True, timeout=30) except Exception: - return (mod, "node_err") + return (mod, "node_err", path) # A non-zero node exit can be intentional (the test exercises an error path), # so we don't bucket it as node_err; we require Perry to match BOTH stdout and # the exit code below, which keeps genuine error-path parity counted as pass. @@ -112,15 +112,15 @@ def run_one(args): try: c = subprocess.run([PERRY, path, "-o", out], capture_output=True, text=True, timeout=120) if c.returncode != 0: - return (mod, "compile_fail") + return (mod, "compile_fail", path) p = subprocess.run([out], capture_output=True, text=True, timeout=30) except Exception: - return (mod, "perry_err") + return (mod, "perry_err", path) # Match stdout byte-for-byte (ignore only trailing-newline noise, not leading # whitespace) AND exit code — so a Perry crash that happened to print matching # output before dying is a diff, not a false pass. ok = (normalize(n.stdout.rstrip("\n")) == normalize(p.stdout.rstrip("\n"))) and (n.returncode == p.returncode) - return (mod, "pass" if ok else "diff") + return (mod, "pass" if ok else "diff", path) # --- pre-warm one test per module serially --- @@ -144,11 +144,15 @@ def run_one(args): sys.stderr.write(f"fast lane: {len(fast)} tests @6, slow lane: {len(slow)} tests @1\n") sys.stderr.flush() with ThreadPoolExecutor(max_workers=6) as ex: - for mod, outcome in ex.map(run_one, fast): + for mod, outcome, path in ex.map(run_one, fast): res[mod][outcome] += 1 + if outcome not in {"pass", "diff"}: + sys.stderr.write(f"{outcome}: {os.path.relpath(path, ROOT)}\n") for t in slow: - mod, outcome = run_one(t) + mod, outcome, path = run_one(t) res[mod][outcome] += 1 + if outcome not in {"pass", "diff"}: + sys.stderr.write(f"{outcome}: {os.path.relpath(path, ROOT)}\n") # --- report --- tot_p = tot = 0 diff --git a/test-parity/node-suite/dns/README.md b/test-parity/node-suite/dns/README.md index 12f03aaf60..597e2d4c21 100644 --- a/test-parity/node-suite/dns/README.md +++ b/test-parity/node-suite/dns/README.md @@ -94,8 +94,9 @@ nameserver. Three complete Node rounds ran all 43 fixtures with zero errors, crashes, or timeouts and byte-identical aggregate SHA-256 `e18b3a7e82b9309c1f8db862d25a5ee1ec2210a61a4f0ad09582ebff948a2f2d`. Three -complete focused Perry runs produced the same **18 pass / 25 diff / 0 compile -failure / 0 crash / 0 timeout** result. The baseline records `18/43`. +complete focused Perry runs produced the same **17 pass / 26 diff / 0 compile +failure / 0 crash / 0 timeout** result after active cancellation idempotence was +added. The baseline records `17/43`. One complete alternate-runtime pass produced: diff --git a/test-parity/node-suite/dns/resolver/cancel-idempotent.ts b/test-parity/node-suite/dns/resolver/cancel-idempotent.ts index 3b94922779..58d32ae811 100644 --- a/test-parity/node-suite/dns/resolver/cancel-idempotent.ts +++ b/test-parity/node-suite/dns/resolver/cancel-idempotent.ts @@ -5,6 +5,10 @@ import { startDnsServer } from "../fixtures/local-dns-server.mjs"; const server = await startDnsServer("silent"); try { const address = `127.0.0.1:${server.port}`; + dns.setServers([address]); + if (typeof dnsPromises.setServers === "function") { + dnsPromises.setServers([address]); + } const callbackResolver = new dns.Resolver(); callbackResolver.setServers([address]); const callbackResult = new Promise((resolve) => { diff --git a/test-parity/node_suite_baseline.json b/test-parity/node_suite_baseline.json index a5cb9c3241..3f51ef30d0 100644 --- a/test-parity/node_suite_baseline.json +++ b/test-parity/node_suite_baseline.json @@ -50,8 +50,14 @@ "total": 69 }, "dns": { - "pass": 18, - "total": 43 + "pass": 17, + "total": 43, + "outcomes": { + "diff": 26, + "node_err": 0, + "compile_fail": 0, + "perry_err": 0 + } }, "domain": { "pass": 18,