Repository navigation
Wpt: stand a server up, and find out what "needs a wpt server" was really hiding - #3287
Merged
Merged
Conversation
…ally hiding Thirteen rows of `_notVendored` read "needs a wpt server" — roughly a quarter of everything parked, and understated twice over, because a file needing one usually cannot produce a test report at all and so lands in that table rather than in the exclusion table. `WptServer` is that server: an in-process HTTP/1.1 origin on the loopback interface, a raw `TcpListener` on port 0, started once per test run. Not `wptserve` — that would mean a Python dependency in a .NET test suite's CI and would give up the vendored-and-byte-verified model `Vendor/README.md` depends on. It serves the *vendored* corpus, plus a C# port of six `.py` handlers, and `WptServerTests` holds each of those to the upstream source at the pin so a reimplementation cannot quietly answer something else. The census the thirteen rows deserved turned out to matter more than the server: only about a third of what they parked was waiting on HTTP. `cors/` (21 files) needs a CORS request mode `RequestConstructor` documents not having, and a second origin; `credentials/` needs a cookie jar `SocketsHttpHandler` deliberately does not carry; and `policies/` holds no `.any.js` file at all, so that row was parking nothing runnable. Every row is now stated file by file, in terms of the engine feature it needs rather than in terms of a server. Twenty files come back — nine of `basic/`, two of `body/`, four of `redirect/`, two of `headers/` and three of `response/`, in three new theories. Two of them (`body/formdata.any.js`, `body/mime-type.any.js`) never wanted a server at all: they build their own Request and Response, and the old directory glob was simply wrong about them. 326 assertions, 232 of them passing. Three things about the lane are load-bearing, and each is documented where it lives. It is a *list*, not a rule — giving the whole corpus a real `fetch` would turn thirty green rows into network-dependent ones for nothing, and the lane is the only place in the driver that grants `WebApiFeatures.Fetch`, so `TheServerLaneHoldsExactlyTheFilesItNames` holds it in both directions; the engine's `UrlFilter` is the server's own port, re-checked on every redirect hop, so nothing can reach the network even inside it. The shim supplies the API base URL the engine deliberately has not got, exactly as `RequestConstructor` tells a host to (`new URL(relative, base).href`), and pointedly does not wrap `Request`. And the drive loop learned to wait for work with no due time: a request in flight is a thread-pool completion `TimeUntilNextScheduledWork` cannot report, so the server lane polls with an idle timer reset by a test settling. The 94 rows that do not pass are named in the exclusion table under six categories, four of them new. Five are genuine defects the corpus found the moment it could make a real request, and they are deliberately left unfixed here so that the change which first runs a suite is not also the change that moves the engine: no default `Accept: */*` (sebastienros#3279), a body stream on a HEAD response (sebastienros#3280), a mutable header list on a fetched response (sebastienros#3281), three content headers dropped from a bodiless request (sebastienros#3282), and `clone()` teeing without structured-cloning the chunks (sebastienros#3283). `NeedsTriage` is no longer empty, which is that mechanism working. `MatchesPattern` gained one thing: `\*` for a literal asterisk. Without it there is no pattern at all that names `accept-header.any.js`'s "…with value '*/*'" row and not its passing sibling "…with value 'custom/*'". Verified: solution builds Release on every target framework; `Jint.Tests` green on net10.0 (10,567) and net472 (7,256), and again with `JINT_HOST_CONTRACT_VERIFICATION=1` alongside `Jint.Tests.PublicInterface` (2,507 / 1,913). The wpt suite ran five times over and reported the same 458 cases every time; with three runs of `upstream/main` for comparison the medians are 68 s before and 73 s after, so the twenty new files cost about 5 s. The inventory table is sebastienros#3276's derived one, and `JINT_WPT_CENSUS=1` agrees with the figures written here — which is the table checking the prose rather than the other way round. Every vendored file was byte-verified against its upstream blob id at the pin. Closes sebastienros#3260 Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01FHnbgQGW5QgfatxL6DgGEd
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #3260.
Thirteen rows of
WptTestRunner._notVendoredread "needs a wpt server" — roughly a quarter of everything parked, and understated twice over, because a file needing one usually cannot produce a test report at all and so lands in the not-vendored table rather than in the exclusion table, whereNeedsWptServerwas a single row.This stands the server up. It also does the census those thirteen rows deserved, and that turned out to matter more than the server: only about a third of what they parked was waiting on HTTP.
Phase A — the inventory, before anything was built
The thirteen rows are seven directory globs and six named files. Behind them: 82
.any.jsfiles, read at the pinned commit6c7127bd..any.jsbehind itfetch/api/abort/*general.any.jsincludes/common/get-host-info.sub.js(wptserve substitution) and../request/request-error.jsat file scope; the other two build aRequestfrom a relative urlfetch/api/basic/*Referer, forbidden-header filtering), a second origin, an HTTP cache, HTTP/2,blob:/data:,garbageCollect(),.sub.substitution, or an/xhr/resources/handlerfetch/api/body/*Request/Response. The third wantsResponse.prototype.textStream, an unmerged Fetch pullfetch/api/cors/*Access-Control-*, an opaque filtered response, and a second origin.RequestConstructordocuments neither reading nor validatingmode— there is no origin model to assert againstfetch/api/credentials/*SocketsHttpHandler.UseCookiesis deliberately off), HTTP authentication, thecredentialsinit memberfetch/api/policies/*.any.jsfile at all —.htmldocuments,.headerssidecars and the.jsthey load. This row was parking nothing runnablefetch/api/redirect/*Referer,keepalive+ a window, HTTP/2,blob:/data:, or are.tentativeheaders/header-values.any.jsheaders/header-values-normalize.any.jsheaders/headers-no-cors.any.js"no-cors"request moderesponse/response-cancel-stream.any.jsresponse/response-clone.any.jsresponse/response-headers-guard.any.jsThe endpoint inventory behind those 21 is small and repetitive: seven handlers (
inspect-headers.py,status.py,method.py,redirect.py,redirect-empty-location.py,clean-stash.py,trickle.py), one static file (top.txt), one helper (/common/utils.js) and a rootGET /. No.headersfile, no charset generation, no cross-origin alias, one trickled body.Payoff, in tests rather than files. 20 files vendored (the 21st,
body/textstream.any.js, wants an unimplemented member), registering 326 assertions, of which 232 pass. Two of the 20 needed nothing but a corrected reason.Two findings that changed the shape of the work, both of which arrived only when the files actually ran:
fetch("../resources/status.py?…")is aTypeErrorin Jint by design —RequestConstructordocuments declining to invent a document URL and tells a host to "resolve it yourself withnew URL(relative, base).href". Without that, a server unlocks nothing. The shim now does exactly what the doc tells a host to do, against alocation.hrefthat is where the server really serves the file. It pointedly does not wrapRequest: rows that construct one from a relative url stay excluded underNeedsApiBaseUrl.Advanced.WaitForScheduledWorkdocuments as findable only by polling. The server lane polls, with an idle timer reset by progress (a test settling) rather than by the wall clock.Phase B — what was built
WptServer: rawTcpListener, port 0, in-process, per runNot
wptserve(Deno's model): a Python dependency in a .NET test suite's CI, and it gives up the vendored-and-byte-verified modelVendor/README.mddepends on.WptServerserves the vendored corpus — the sameWptCorpusbytes every other suite reads.Not
HttpListenerand not Kestrel, and each reason is a property the suites need:HttpListenertakes a URL prefix and has no ephemeral form, so it would need a port picked in advance and a race to go with it. A collision in CI is a flake, and this cannot have one.HttpListenerneeds an ACL for anything butlocalhoston Windows; Kestrel needsMicrosoft.AspNetCore.App.trickle.pydribble a body out and what makes the query's percent-decoding byte-exact (below).The divergences from upstream's handlers, stated
The
.pyfiles cannot run, so six handlers are reimplemented in C#. This is the one part of the corpus that is not vendored bytes, andWptServerTestsis what stops a reimplementation quietly answering something else — 13 tests, each written from the upstream source at the pin and named after it.inspect-headers.pyx-request-<name>?corsbranch — no CORS model, so every file passing it is parked for that insteadstatus.pymethod.pyNO) and the body?corsbranchredirect.pyredirect_status,location,simple, the query-preserving location rewrite with its&count=hop counter, and thetoken/max_countstashAccess-Control-*, theOPTIONSpreflight branch,redirect_referrerpolicy,delay(no vendored file passes one)redirect-empty-location.pyLocationclean-stash.pytrickle.pycountlines ofTEST_TRICKLE, one everymsms, same delay before the headersHow each was verified. Every row above has a test asserting the behaviour the vendored file depends on:
InspectHeadersEchoesTheHeadersItWasAskedFor,StatusAnswersWithWhatTheQueryAsksFor,MethodEchoesTheRequestBackToTheCaller,RedirectAnswersTheStatusAndRewritesTheLocation,RedirectLeavesASimpleLocationAlone,RedirectCountsHopsInTheStashAndReportsThemAtMaxCount,RedirectEmptyLocationSendsAnEmptyLocationHeader,TrickleWritesTheLinesItWasAskedFor,TrickleSendsNoContentTypeWhenAskedNotTo,StaticFilesComeOutOfTheVendoredCorpus,APathTheCorpusDoesNotHoldIsA404.One divergence was found by a suite before a test caught it, and is worth stating on its own: the whole server holds wire bytes as Latin-1. wptserve works in bytes —
request.GET.first(b"content")is a percent-decoded byte string its handlers write straight back.basic/text-utf8.any.jsasks forcontent=%fe%ff%4e%09, a UTF-16BE body served astext/plain;charset=UTF-8, precisely to see whatResponse.text()makes of bytes that are not valid UTF-8. A first pass usedUri.UnescapeDataString, which replaced them with U+FFFD before the engine ever saw them, and the file asserted against the server's mistake.TheQueryIsDecodedToBytesRatherThanToTextpins it.The lane is a list, not a rule
Giving the whole
fetch/api/corpus a realfetchwould turn thirty already-green rows into network-dependent ones for nothing — they build their ownResponseand never ask for a URL.WptHarness._serverBackedFilesnames 17 files, andTheServerLaneHoldsExactlyTheFilesItNamesholds it in both directions, because the lane is the only place in the driver that grantsWebApiFeatures.Fetchand is therefore the boundary of the promise that no suite can open a socket. Inside it,Options.WebApi.Fetch.UrlFilterisWptServer.Owns— re-checked on every redirect hop — so nothing can reach the network there either.Tables brought back in line
fetch/api/basic,body,redirect), 20 minimum-test entries, and the driver's own inventory checks extended.MatchesPatterngained\*for a literal asterisk. Without it there is no pattern that namesaccept-header.any.js's failing…with value '*/*'row and not its passing sibling…with value 'custom/*'— every literal segment of the one is a segment of the other, so the two-sided rule could never be satisfied.The 94 rows that do not pass
Six categories, four of them new.
Five are genuine engine defects the corpus found the moment it could make a real request. Filed separately and deliberately not fixed here, so that the change which first runs a suite is not also the change that moves the engine.
NeedsTriageis no longer empty, which is that mechanism working:Accept: */*HEADresponse carries a body streamHeadersare mutableclone()tees without structured-cloning the chunksEach is localised by a passing sibling — the row that sets
Acceptexplicitly passes, the 204/205/304 rows pass,redirect-method's POST rows pass,response-clone's other ten rows pass — which is what says these are the defects rather than the harness.The other 69 are decisions or environment, and add up exactly: 35 + 21 + 10 + 2 + 1.
NeedsXmlHttpRequest(new). Bothheader-values*files run their whole table twice, once throughXMLHttpRequest.setRequestHeader. The driver's XHR is a corpus reader that refuses anything but a GET, by name.NeedsOpaqueRedirect(new).redirect: "manual"hands the script the redirect response rather than a browser's opaque filtered one —FetchTransportdocuments this as Node's reading of HTTP fetch step 6. Thefollowanderrorrows of both redirect files pass.NeedsApiBaseUrl.text-utf8.any.jsbuilds aRequestfrom the empty string; its other ten rows go throughfetchand pass.NeedsPermissiveHeaderTransport(new). See below.NeedsBrowserRequestHeaders(new).Accept-Languageis a browser reporting a user's language preferences.One divergence that is neither the engine nor the server
AHeaderValueAboveAsciiDoesNotSurviveTheHttpStackprobes every byte from0x01to0xFFthrough a real request and a real response, with no engine in the picture, and measures the line exactly: every byte up to0x7Fround-trips — control characters included, which are the ones that look most likely to be refused — and every byte from0x80up does not. The Fetch Standard's header value is any byte but NUL, LF and CR, soheader-values.any.jsandrequest-headers-nonascii.any.jsare asserting something a browser carries and the .NET request/response header encodings (ASCII by default) do not. That is what puts those two rows in their own category rather than inNeedsTriage.Flake evidence
A network-dependent suite is a new flake surface and this repository has spent real effort removing those (#3213, #3221, #3234). What makes this one deterministic:
TcpListener(IPAddress.Loopback, 0)and the kernel picks; there is nothing to collide with.TrickleWritesTheLinesItWasAskedForasserts the bytes, not the timing. The drive loop's idle timer is reset by a test settling, not by elapsed time.fetchtimeout (10 s) is deliberately inside the drive loop's grace period (20 s).Measured: five consecutive runs of
--filter Jint.Tests.Wpt, 458 cases every time, 71/73/73/81/83 s. Three runs ofupstream/mainfor comparison: 424 cases, 64/68/73 s. Median 68 s → 73 s, so the twenty new files cost about 5 s.Verification
dotnet build -c Releasefor the solution, every target framework: 0 errors. (One pre-existing MSB3277 inJint.Tests.CommonScripts' net472 leg, untouched by this change.)Jint.Tests: 10,567 passed / 5 skipped on net10.0, 7,256 / 4 on net472.JINT_HOST_CONTRACT_VERIFICATION=1: same counts, plusJint.Tests.PublicInterface2,507 (net10.0) and 1,913 (net472).JINT_WPT_CENSUS=1: the inventory table Wpt: derive the corpus inventory table instead of maintaining it by hand #3276 derives agrees with every figure written here — the table checking the prose rather than the other way round.upstream/mainafter Wpt: derive the corpus inventory table instead of maintaining it by hand #3276 landed and re-verified in full.Against the verdict
"A quarter of everything parked" was not a quarter of anything. The honest reading of the census is that the thirteen rows were badly over-stated:
policies/*parked no runnable file at all, andcors/*pluscredentials/*— 24 of the 82 — are blocked by engine features a server does nothing about. If this PR had only corrected the reasons and vendored the twobody/files that needed nothing, it would have delivered a real part of the value at none of the risk. Someone who thinks the flake surface is not worth 232 passing assertions has a coherent position, and the reason-corrections could be split out of this change cleanly.The shim now resolves relative URLs, and that is the harness doing something the engine will not. It is defensible —
RequestConstructorprescribes exactly this to a host, and the shim already plays the browsing environment by supplyinglocationandGLOBAL— but it is one more place where a green row depends on the harness. The mitigation is thatRequestis deliberately not wrapped, so the constructor's own divergence is still asserted by 10 excluded rows and by the whole offetch/api/request/staying unvendored.Six handlers in C# is six things that can drift from upstream.
WptServerTestsis the answer, but it is a test of the reimplementation, not the byte-verification the rest of the corpus gets. A corpus bump that changesredirect.pywill not fail anything until someone reads it.The 20-second grace period is a number. It is not derived from anything: it has to exceed the lane's 10 s fetch timeout and be well inside the 5-minute harness deadline, and 20 was picked in that window. A genuinely hung server-lane file costs 20 s of a run before it reports, where every other lane reports at once.
NeedsTriagegoing from 0 to 25 rows is a real cost.AGENTS.mdsays the emptiness is the signal, and this change spends it. Five issues are filed, but until they are paid the category no longer means "the corpus has found something new".🤖 Generated with Claude Code