diff options
Diffstat (limited to 'tools/wpt.ts')
| -rwxr-xr-x | tools/wpt.ts | 822 |
1 files changed, 0 insertions, 822 deletions
diff --git a/tools/wpt.ts b/tools/wpt.ts deleted file mode 100755 index 93b96b201..000000000 --- a/tools/wpt.ts +++ /dev/null @@ -1,822 +0,0 @@ -#!/usr/bin/env -S deno run --unstable --allow-write --allow-read --allow-net --allow-env --allow-run -// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license. - -// This script is used to run WPT tests for Deno. - -import { - runSingleTest, - runWithTestUtil, - TestCaseResult, - TestResult, -} from "./wpt/runner.ts"; -import { - assert, - autoConfig, - cargoBuild, - checkPy3Available, - escapeLoneSurrogates, - Expectation, - generateRunInfo, - getExpectation, - getExpectFailForCase, - getManifest, - inspectBrk, - json, - ManifestFolder, - ManifestTestOptions, - ManifestTestVariation, - noIgnore, - quiet, - rest, - runPy, - updateManifest, - wptreport, -} from "./wpt/utils.ts"; -import { pooledMap } from "../tests/util/std/async/pool.ts"; -import { - blue, - bold, - green, - red, - yellow, -} from "../tests/util/std/fmt/colors.ts"; -import { writeAll, writeAllSync } from "../tests/util/std/streams/write_all.ts"; -import { saveExpectation } from "./wpt/utils.ts"; - -class TestFilter { - filter?: string[]; - constructor(filter?: string[]) { - this.filter = filter; - } - - matches(path: string): boolean { - if (this.filter === undefined || this.filter.length == 0) { - return true; - } - for (const filter of this.filter) { - if (filter.startsWith("/")) { - if (path.startsWith(filter)) { - return true; - } - } else { - if (path.substring(1).startsWith(filter)) { - return true; - } - } - } - return false; - } -} - -const command = Deno.args[0]; - -switch (command) { - case "setup": - await checkPy3Available(); - await updateManifest(); - await setup(); - break; - - case "run": - await cargoBuild(); - await run(); - break; - - case "update": - await cargoBuild(); - await update(); - break; - - default: - console.log(`Possible commands: - - setup - Validate that your environment is configured correctly, or help you configure it. - - run - Run all tests like specified in \`expectation.json\`. - - update - Update the \`expectation.json\` to match the current reality. - -More details at https://deno.land/manual@main/contributing/web_platform_tests - - `); - break; -} - -async function setup() { - const hostsPath = Deno.build.os == "windows" - ? `${Deno.env.get("SystemRoot")}\\System32\\drivers\\etc\\hosts` - : "/etc/hosts"; - // TODO(lucacsonato): use this when 1.7.1 is released. - // const records = await Deno.resolveDns("web-platform.test", "A"); - // const etcHostsConfigured = records[0] == "127.0.0.1"; - const hostsFile = await Deno.readTextFile(hostsPath); - const etcHostsConfigured = hostsFile.includes("web-platform.test"); - - if (etcHostsConfigured) { - console.log(hostsPath + " is already configured."); - } else { - const autoConfigure = autoConfig || - confirm( - `The WPT require certain entries to be present in your ${hostsPath} file. Should these be configured automatically?`, - ); - if (autoConfigure) { - const { success, stdout } = await runPy(["wpt", "make-hosts-file"], { - stdout: "piped", - }).output(); - assert(success, "wpt make-hosts-file should not fail"); - const entries = new TextDecoder().decode(stdout); - const file = await Deno.open(hostsPath, { append: true }).catch((err) => { - if (err instanceof Deno.errors.PermissionDenied) { - throw new Error( - `Failed to open ${hostsPath} (permission error). Please run this command again with sudo, or configure the entries manually.`, - ); - } else { - throw err; - } - }); - await writeAll( - file, - new TextEncoder().encode( - "\n\n# Configured for Web Platform Tests (Deno)\n" + entries, - ), - ); - console.log(`Updated ${hostsPath}`); - } else { - console.log(`Please configure the ${hostsPath} entries manually.`); - if (Deno.build.os == "windows") { - console.log("To do this run the following command in PowerShell:"); - console.log(""); - console.log(" cd tests/wpt/suite/"); - console.log( - " python.exe wpt make-hosts-file | Out-File $env:SystemRoot\\System32\\drivers\\etc\\hosts -Encoding ascii -Append", - ); - console.log(""); - } else { - console.log("To do this run the following command in your shell:"); - console.log(""); - console.log(" cd tests/wpt/suite/"); - console.log( - " python3 ./wpt make-hosts-file | sudo tee -a /etc/hosts", - ); - console.log(""); - } - } - } - - console.log(green("Setup complete!")); -} - -interface TestToRun { - path: string; - url: URL; - options: ManifestTestOptions; - expectation: boolean | string[]; -} - -function getTestTimeout(test: TestToRun) { - if (Deno.env.get("CI")) { - // Don't give expected failures the full time - if (test.expectation === false) { - return { long: 60_000, default: 10_000 }; - } - return { long: 4 * 60_000, default: 4 * 60_000 }; - } - - return { long: 60_000, default: 10_000 }; -} - -async function run() { - const startTime = new Date().getTime(); - assert(Array.isArray(rest), "filter must be array"); - const expectation = getExpectation(); - const filter = new TestFilter(rest); - const tests = discoverTestsToRun( - filter, - expectation, - ); - assertAllExpectationsHaveTests(expectation, tests, filter); - const cores = navigator.hardwareConcurrency; - console.log(`Going to run ${tests.length} test files on ${cores} cores.`); - - const results = await runWithTestUtil(false, async () => { - const results: { test: TestToRun; result: TestResult }[] = []; - const inParallel = !(cores === 1 || tests.length === 1); - // ideally we would parallelize all tests, but we ran into some flakiness - // on the CI, so here we're partitioning based on the start of the test path - const partitionedTests = partitionTests(tests); - - const iter = pooledMap(cores, partitionedTests, async (tests) => { - for (const test of tests) { - if (!inParallel) { - console.log(`${blue("-".repeat(40))}\n${bold(test.path)}\n`); - } - const result = await runSingleTest( - test.url, - test.options, - inParallel ? () => {} : createReportTestCase(test.expectation), - inspectBrk, - getTestTimeout(test), - ); - results.push({ test, result }); - if (inParallel) { - console.log(`${blue("-".repeat(40))}\n${bold(test.path)}\n`); - } - reportVariation(result, test.expectation); - } - }); - - for await (const _ of iter) { - // ignore - } - - return results; - }); - const endTime = new Date().getTime(); - - if (json) { - const minifiedResults = []; - for (const result of results) { - const minified = { - file: result.test.path, - name: - Object.fromEntries(result.test.options.script_metadata ?? []).title ?? - null, - cases: result.result.cases.map((case_) => ({ - name: case_.name, - passed: case_.passed, - })), - }; - minifiedResults.push(minified); - } - await Deno.writeTextFile(json, JSON.stringify(minifiedResults)); - } - - if (wptreport) { - const report = await generateWptReport(results, startTime, endTime); - await Deno.writeTextFile(wptreport, JSON.stringify(report)); - } - - const code = reportFinal(results, endTime - startTime); - Deno.exit(code); -} - -async function generateWptReport( - results: { test: TestToRun; result: TestResult }[], - startTime: number, - endTime: number, -) { - const runInfo = await generateRunInfo(); - const reportResults = []; - for (const { test, result } of results) { - const status = result.status !== 0 - ? "CRASH" - : result.harnessStatus?.status === 0 - ? "OK" - : "ERROR"; - let message; - if (result.harnessStatus === null && result.status === 0) { - // If the only error is the event loop running out of tasks, using stderr - // as the message won't help. - message = "Event loop run out of tasks."; - } else { - message = result.harnessStatus?.message ?? (result.stderr.trim() || null); - } - const reportResult = { - test: test.url.pathname + test.url.search + test.url.hash, - subtests: result.cases.map((case_) => { - let expected = undefined; - if (!case_.passed) { - if (typeof test.expectation === "boolean") { - expected = test.expectation ? "PASS" : "FAIL"; - } else if (Array.isArray(test.expectation)) { - expected = test.expectation.includes(case_.name) ? "FAIL" : "PASS"; - } else { - expected = "PASS"; - } - } - - return { - name: escapeLoneSurrogates(case_.name), - status: case_.passed ? "PASS" : "FAIL", - message: escapeLoneSurrogates(case_.message), - expected, - known_intermittent: [], - }; - }), - status, - message: escapeLoneSurrogates(message), - duration: result.duration, - expected: status === "OK" ? undefined : "OK", - "known_intermittent": [], - }; - reportResults.push(reportResult); - } - return { - "run_info": runInfo, - "time_start": startTime, - "time_end": endTime, - "results": reportResults, - }; -} - -// Check that all expectations in the expectations file have a test that will be -// run. -function assertAllExpectationsHaveTests( - expectation: Expectation, - testsToRun: TestToRun[], - filter: TestFilter, -): void { - const tests = new Set(testsToRun.map((t) => t.path)); - const missingTests: string[] = []; - function walk(parentExpectation: Expectation, parent: string) { - for (const [key, expectation] of Object.entries(parentExpectation)) { - const path = `${parent}/${key}`; - if (!filter.matches(path)) continue; - if ( - (typeof expectation == "boolean" || Array.isArray(expectation)) && - key !== "ignore" - ) { - if (!tests.has(path)) { - missingTests.push(path); - } - } else { - walk(expectation, path); - } - } - } - - walk(expectation, ""); - - if (missingTests.length > 0) { - console.log( - red( - "Following tests are missing in manifest, but are present in expectations:", - ), - ); - console.log(""); - console.log(missingTests.join("\n")); - Deno.exit(1); - } -} - -async function update() { - assert(Array.isArray(rest), "filter must be array"); - const startTime = new Date().getTime(); - const filter = new TestFilter(rest); - const tests = discoverTestsToRun(filter, true); - console.log(`Going to run ${tests.length} test files.`); - - const results = await runWithTestUtil(false, async () => { - const results = []; - - for (const test of tests) { - console.log(`${blue("-".repeat(40))}\n${bold(test.path)}\n`); - const result = await runSingleTest( - test.url, - test.options, - json ? () => {} : createReportTestCase(test.expectation), - inspectBrk, - { long: 60_000, default: 10_000 }, - ); - results.push({ test, result }); - reportVariation(result, test.expectation); - } - - return results; - }); - const endTime = new Date().getTime(); - - if (json) { - await Deno.writeTextFile(json, JSON.stringify(results)); - } - - const resultTests: Record< - string, - { passed: string[]; failed: string[]; testSucceeded: boolean } - > = {}; - for (const { test, result } of results) { - if (!resultTests[test.path]) { - resultTests[test.path] = { - passed: [], - failed: [], - testSucceeded: result.status === 0 && result.harnessStatus !== null, - }; - } - for (const case_ of result.cases) { - if (case_.passed) { - resultTests[test.path].passed.push(case_.name); - } else { - resultTests[test.path].failed.push(case_.name); - } - } - } - - const currentExpectation = getExpectation(); - - for (const [path, result] of Object.entries(resultTests)) { - const { passed, failed, testSucceeded } = result; - let finalExpectation: boolean | string[]; - if (failed.length == 0 && testSucceeded) { - finalExpectation = true; - } else if (failed.length > 0 && passed.length > 0 && testSucceeded) { - finalExpectation = failed; - } else { - finalExpectation = false; - } - - insertExpectation( - path.slice(1).split("/"), - currentExpectation, - finalExpectation, - ); - } - - saveExpectation(currentExpectation); - - reportFinal(results, endTime - startTime); - - console.log(blue("Updated expectation.json to match reality.")); - - Deno.exit(0); -} - -function insertExpectation( - segments: string[], - currentExpectation: Expectation, - finalExpectation: boolean | string[], -) { - const segment = segments.shift(); - assert(segment, "segments array must never be empty"); - if (segments.length > 0) { - if ( - !currentExpectation[segment] || - Array.isArray(currentExpectation[segment]) || - typeof currentExpectation[segment] === "boolean" - ) { - currentExpectation[segment] = {}; - } - insertExpectation( - segments, - currentExpectation[segment] as Expectation, - finalExpectation, - ); - } else { - currentExpectation[segment] = finalExpectation; - } -} - -function reportFinal( - results: { test: TestToRun; result: TestResult }[], - duration: number, -): number { - const finalTotalCount = results.length; - let finalFailedCount = 0; - const finalFailed: [string, TestCaseResult][] = []; - let finalExpectedFailedAndFailedCount = 0; - const finalExpectedFailedButPassedTests: [string, TestCaseResult][] = []; - const finalExpectedFailedButPassedFiles: string[] = []; - const finalFailedFiles: string[] = []; - for (const { test, result } of results) { - const { - failed, - failedCount, - expectedFailedButPassed, - expectedFailedAndFailedCount, - } = analyzeTestResult( - result, - test.expectation, - ); - if (result.status !== 0 || result.harnessStatus === null) { - if (test.expectation === false) { - finalExpectedFailedAndFailedCount += 1; - } else { - finalFailedCount += 1; - finalFailedFiles.push(test.path); - } - } else if (failedCount > 0) { - finalFailedCount += 1; - for (const case_ of failed) { - finalFailed.push([test.path, case_]); - } - for (const case_ of expectedFailedButPassed) { - finalExpectedFailedButPassedTests.push([test.path, case_]); - } - } else if ( - test.expectation === false && - expectedFailedAndFailedCount != result.cases.length - ) { - finalExpectedFailedButPassedFiles.push(test.path); - } - } - const finalPassedCount = finalTotalCount - finalFailedCount; - - console.log(bold(blue("=".repeat(40)))); - - if (finalFailed.length > 0) { - console.log(`\nfailures:\n`); - } - for (const result of finalFailed) { - console.log( - ` ${JSON.stringify(`${result[0]} - ${result[1].name}`)}`, - ); - } - if (finalFailedFiles.length > 0) { - console.log(`\nfile failures:\n`); - } - for (const result of finalFailedFiles) { - console.log( - ` ${JSON.stringify(result)}`, - ); - } - if (finalExpectedFailedButPassedTests.length > 0) { - console.log(`\nexpected test failures that passed:\n`); - } - for (const result of finalExpectedFailedButPassedTests) { - console.log( - ` ${JSON.stringify(`${result[0]} - ${result[1].name}`)}`, - ); - } - if (finalExpectedFailedButPassedFiles.length > 0) { - console.log(`\nexpected file failures that passed:\n`); - } - for (const result of finalExpectedFailedButPassedFiles) { - console.log(` ${JSON.stringify(result)}`); - } - - const failed = (finalFailedCount > 0) || - (finalExpectedFailedButPassedFiles.length > 0); - - console.log( - `\nfinal result: ${ - failed ? red("failed") : green("ok") - }. ${finalPassedCount} passed; ${finalFailedCount} failed; ${finalExpectedFailedAndFailedCount} expected failure; total ${finalTotalCount} (${duration}ms)\n`, - ); - - return failed ? 1 : 0; -} - -function analyzeTestResult( - result: TestResult, - expectation: boolean | string[], -): { - failed: TestCaseResult[]; - failedCount: number; - passedCount: number; - totalCount: number; - expectedFailedButPassed: TestCaseResult[]; - expectedFailedButPassedCount: number; - expectedFailedAndFailedCount: number; -} { - const failed = result.cases.filter( - (t) => !getExpectFailForCase(expectation, t.name) && !t.passed, - ); - const expectedFailedButPassed = result.cases.filter( - (t) => getExpectFailForCase(expectation, t.name) && t.passed, - ); - const expectedFailedButPassedCount = expectedFailedButPassed.length; - const failedCount = failed.length + expectedFailedButPassedCount; - const expectedFailedAndFailedCount = result.cases.filter( - (t) => getExpectFailForCase(expectation, t.name) && !t.passed, - ).length; - const totalCount = result.cases.length; - const passedCount = totalCount - failedCount - expectedFailedAndFailedCount; - - return { - failed, - failedCount, - passedCount, - totalCount, - expectedFailedButPassed, - expectedFailedButPassedCount, - expectedFailedAndFailedCount, - }; -} - -function reportVariation(result: TestResult, expectation: boolean | string[]) { - if (result.status !== 0 || result.harnessStatus === null) { - if (result.stderr) { - console.log(`test stderr:\n${result.stderr}\n`); - } - - const expectFail = expectation === false; - const failReason = result.status !== 0 - ? "runner failed during test" - : "the event loop run out of tasks during the test"; - console.log( - `\nfile result: ${ - expectFail ? yellow("failed (expected)") : red("failed") - }. ${failReason} (${formatDuration(result.duration)})\n`, - ); - return; - } - - const { - failed, - failedCount, - passedCount, - totalCount, - expectedFailedButPassed, - expectedFailedButPassedCount, - expectedFailedAndFailedCount, - } = analyzeTestResult(result, expectation); - - if (failed.length > 0) { - console.log(`\nfailures:`); - } - for (const result of failed) { - console.log(`\n${result.name}\n${result.message}\n${result.stack}`); - } - - if (failedCount > 0) { - console.log(`\nfailures:\n`); - } - for (const result of failed) { - console.log(` ${JSON.stringify(result.name)}`); - } - if (expectedFailedButPassedCount > 0) { - console.log(`\nexpected failures that passed:\n`); - } - for (const result of expectedFailedButPassed) { - console.log(` ${JSON.stringify(result.name)}`); - } - if (result.stderr) { - console.log("\ntest stderr:\n" + result.stderr); - } - console.log( - `\nfile result: ${ - failedCount > 0 ? red("failed") : green("ok") - }. ${passedCount} passed; ${failedCount} failed; ${expectedFailedAndFailedCount} expected failure; total ${totalCount} (${ - formatDuration(result.duration) - })\n`, - ); -} - -function createReportTestCase(expectation: boolean | string[]) { - return function reportTestCase({ name, status }: TestCaseResult) { - const expectFail = getExpectFailForCase(expectation, name); - let simpleMessage = `test ${name} ... `; - switch (status) { - case 0: - if (expectFail) { - simpleMessage += red("ok (expected fail)"); - } else { - simpleMessage += green("ok"); - if (quiet) { - // don't print `ok` tests if --quiet is enabled - return; - } - } - break; - case 1: - if (expectFail) { - simpleMessage += yellow("failed (expected)"); - } else { - simpleMessage += red("failed"); - } - break; - case 2: - if (expectFail) { - simpleMessage += yellow("failed (expected)"); - } else { - simpleMessage += red("failed (timeout)"); - } - break; - case 3: - if (expectFail) { - simpleMessage += yellow("failed (expected)"); - } else { - simpleMessage += red("failed (incomplete)"); - } - break; - } - - writeAllSync(Deno.stdout, new TextEncoder().encode(simpleMessage + "\n")); - }; -} - -function discoverTestsToRun( - filter: TestFilter, - expectation: Expectation | string[] | boolean = getExpectation(), -): TestToRun[] { - const manifestFolder = getManifest().items.testharness; - - const testsToRun: TestToRun[] = []; - - function walk( - parentFolder: ManifestFolder, - parentExpectation: Expectation | string[] | boolean, - prefix: string, - ) { - for (const [key, entry] of Object.entries(parentFolder)) { - if (Array.isArray(entry)) { - for ( - const [path, options] of entry.slice( - 1, - ) as ManifestTestVariation[] - ) { - // Test keys ending with ".html" include their own html boilerplate. - // Test keys ending with ".js" will have the necessary boilerplate generated and - // the manifest path will contain the full path to the generated html test file. - // See: https://web-platform-tests.org/writing-tests/testharness.html - if (!key.endsWith(".html") && !key.endsWith(".js")) continue; - - const testHtmlPath = path ?? `${prefix}/${key}`; - const url = new URL(testHtmlPath, "http://web-platform.test:8000"); - if (!url.pathname.endsWith(".html")) { - continue; - } - // These tests require an HTTP2 compatible server. - if (url.pathname.includes(".h2.")) { - continue; - } - // Streaming fetch requests need a server that supports chunked - // encoding, which the WPT test server does not. Unfortunately this - // also disables some useful fetch tests. - if (url.pathname.includes("request-upload")) { - continue; - } - const finalPath = url.pathname + url.search; - - const split = finalPath.split("/"); - const finalKey = split[split.length - 1]; - - const expectation = Array.isArray(parentExpectation) || - typeof parentExpectation == "boolean" - ? parentExpectation - : parentExpectation[finalKey]; - - if (expectation === undefined) continue; - - if (typeof expectation === "object") { - if (typeof expectation.ignore !== "undefined") { - assert( - typeof expectation.ignore === "boolean", - "test entry's `ignore` key must be a boolean", - ); - if (expectation.ignore === true && !noIgnore) continue; - } - } - - if (!noIgnore) { - assert( - Array.isArray(expectation) || typeof expectation == "boolean", - "test entry must not have a folder expectation", - ); - } - - if (!filter.matches(finalPath)) continue; - - testsToRun.push({ - path: finalPath, - url, - options, - expectation, - }); - } - } else { - const expectation = Array.isArray(parentExpectation) || - typeof parentExpectation == "boolean" - ? parentExpectation - : parentExpectation[key]; - - if (expectation === undefined) continue; - - walk(entry, expectation, `${prefix}/${key}`); - } - } - } - walk(manifestFolder, expectation, ""); - - return testsToRun; -} - -function partitionTests(tests: TestToRun[]): TestToRun[][] { - const testsByKey: { [key: string]: TestToRun[] } = {}; - for (const test of tests) { - // Run all WebCryptoAPI tests in parallel - if (test.path.includes("/WebCryptoAPI")) { - testsByKey[test.path] = [test]; - continue; - } - // Paths looks like: /fetch/corb/img-html-correctly-labeled.sub-ref.html - const key = test.path.split("/")[1]; - if (!(key in testsByKey)) { - testsByKey[key] = []; - } - testsByKey[key].push(test); - } - return Object.values(testsByKey); -} - -function formatDuration(duration: number): string { - if (duration >= 5000) { - return red(`${duration}ms`); - } else if (duration >= 1000) { - return yellow(`${duration}ms`); - } else { - return `${duration}ms`; - } -} |
