tests: new typescript WPT runner (#9269)

This commit is contained in:
Luca Casonato 2021-01-27 15:06:18 +01:00 committed by GitHub
parent ecfda65eff
commit 2638aa03a5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
16 changed files with 1623 additions and 666 deletions

View file

@ -1,35 +0,0 @@
## Web Platform Tests
The WPT are test suites for Web platform specs, like Fetch, WHATWG Streams, or
console. Deno is able to run most `.any.js` and `.window.js` web platform tests.
This directory contains a `wpt.jsonc` file that is used to configure our WPT
test runner. You can use this json file to set which WPT suites to run, and
which tests we expect to fail (due to bugs or because they are out of scope for
Deno).
To include a new test file to run, add it to the array of test files for the
corresponding suite. For example we want to enable
`streams/readable-streams/general`. The file would then look like this:
```json
{
"streams": ["readable-streams/general"]
}
```
If you need more configurability over which test cases in a test file of a suite
to run, you can use the object representation. In the example below, we
configure `streams/readable-streams/general` to expect
`ReadableStream can't be constructed with an invalid type` to fail.
```json
{
"streams": [
{
"name": "readable-streams/general",
"expectFail": ["ReadableStream can't be constructed with an invalid type"]
}
]
}
```

View file

@ -6,12 +6,9 @@ use deno_core::url;
use deno_runtime::deno_fetch::reqwest;
use deno_runtime::deno_websocket::tokio_tungstenite;
use std::io::{BufRead, Write};
use std::path::Path;
use std::path::PathBuf;
use std::process::Command;
use tempfile::TempDir;
use test_util as util;
use walkdir::WalkDir;
macro_rules! itest(
($name:ident {$( $key:ident: $value:expr,)*}) => {
@ -5193,249 +5190,6 @@ fn denort_direct_use_error() {
assert!(!status.success());
}
fn concat_bundle(
files: Vec<(PathBuf, String)>,
bundle_path: &Path,
init: String,
) -> String {
let bundle_url = url::Url::from_file_path(bundle_path).unwrap().to_string();
let mut bundle = init.clone();
let mut bundle_line_count = init.lines().count() as u32;
let mut source_map = sourcemap::SourceMapBuilder::new(Some(&bundle_url));
// In classic workers, `importScripts()` performs an actual import.
// However, we don't implement that function in Deno as we want to enforce
// the use of ES6 modules.
// To work around this, we:
// 1. Define `importScripts()` as a no-op (code below)
// 2. Capture its parameter from the source code and add it to the list of
// files to concatenate. (see `web_platform_tests()`)
bundle.push_str("function importScripts() {}\n");
bundle_line_count += 1;
for (path, text) in files {
let path = std::fs::canonicalize(path).unwrap();
let url = url::Url::from_file_path(path).unwrap().to_string();
let src_id = source_map.add_source(&url);
source_map.set_source_contents(src_id, Some(&text));
for (line_index, line) in text.lines().enumerate() {
bundle.push_str(line);
bundle.push('\n');
source_map.add_raw(
bundle_line_count,
0,
line_index as u32,
0,
Some(src_id),
None,
);
bundle_line_count += 1;
}
bundle.push('\n');
bundle_line_count += 1;
}
let mut source_map_buf: Vec<u8> = vec![];
source_map
.into_sourcemap()
.to_writer(&mut source_map_buf)
.unwrap();
bundle.push_str("//# sourceMappingURL=data:application/json;base64,");
let encoded_map = base64::encode(source_map_buf);
bundle.push_str(&encoded_map);
bundle
}
// TODO(lucacasonato): DRY with tsc_config.rs
/// Convert a jsonc libraries `JsonValue` to a serde `Value`.
fn jsonc_to_serde(j: jsonc_parser::JsonValue) -> serde_json::Value {
use jsonc_parser::JsonValue;
use serde_json::Value;
use std::str::FromStr;
match j {
JsonValue::Array(arr) => {
let vec = arr.into_iter().map(jsonc_to_serde).collect();
Value::Array(vec)
}
JsonValue::Boolean(bool) => Value::Bool(bool),
JsonValue::Null => Value::Null,
JsonValue::Number(num) => {
let number =
serde_json::Number::from_str(&num).expect("could not parse number");
Value::Number(number)
}
JsonValue::Object(obj) => {
let mut map = serde_json::map::Map::new();
for (key, json_value) in obj.into_iter() {
map.insert(key, jsonc_to_serde(json_value));
}
Value::Object(map)
}
JsonValue::String(str) => Value::String(str),
}
}
#[test]
fn web_platform_tests() {
use deno_core::serde::Deserialize;
#[derive(Deserialize)]
#[serde(untagged)]
enum WptConfig {
Simple(String),
#[serde(rename_all = "camelCase")]
Options {
name: String,
expect_fail: Vec<String>,
},
}
let text =
std::fs::read_to_string(util::tests_path().join("wpt.jsonc")).unwrap();
let jsonc = jsonc_parser::parse_to_value(&text).unwrap().unwrap();
let config: std::collections::HashMap<String, Vec<WptConfig>> =
deno_core::serde_json::from_value(jsonc_to_serde(jsonc)).unwrap();
for (suite_name, includes) in config.into_iter() {
let suite_path = util::wpt_path().join(suite_name);
let dir = WalkDir::new(&suite_path)
.into_iter()
.filter_map(Result::ok)
.filter(|e| e.file_type().is_file())
.filter(|f| {
let filename = f.file_name().to_str().unwrap();
filename.ends_with(".any.js")
|| filename.ends_with(".window.js")
|| filename.ends_with(".worker.js")
})
.filter_map(|f| {
let path = f
.path()
.strip_prefix(&suite_path)
.unwrap()
.to_str()
.unwrap();
for cfg in &includes {
match cfg {
WptConfig::Simple(name) if path.starts_with(name) => {
return Some((f.path().to_owned(), vec![]))
}
WptConfig::Options { name, expect_fail }
if path.starts_with(name) =>
{
return Some((f.path().to_owned(), expect_fail.to_vec()))
}
_ => {}
}
}
None
});
let testharness_path = util::wpt_path().join("resources/testharness.js");
let testharness_text = std::fs::read_to_string(&testharness_path)
.unwrap()
.replace("output:true", "output:false");
let testharnessreporter_path =
util::tests_path().join("wpt_testharnessconsolereporter.js");
let testharnessreporter_text =
std::fs::read_to_string(&testharnessreporter_path).unwrap();
for (test_file_path, expect_fail) in dir {
let test_file_text = std::fs::read_to_string(&test_file_path).unwrap();
let imports: Vec<(PathBuf, String)> = test_file_text
.split('\n')
.into_iter()
.filter_map(|t| {
// Hack: we don't implement `importScripts()`, and instead capture the
// parameter in source code; see `concat_bundle()` for more details.
if let Some(rest_import_scripts) = t.strip_prefix("importScripts(\"")
{
if let Some(import_path) = rest_import_scripts.strip_suffix("\");")
{
// The code in `testharness.js` silences the test outputs.
if import_path != "/resources/testharness.js" {
return Some(import_path);
}
}
}
t.strip_prefix("// META: script=")
})
.map(|s| {
let s = if s == "/resources/WebIDLParser.js" {
"/resources/webidl2/lib/webidl2.js"
} else {
s
};
if s.starts_with('/') {
util::wpt_path().join(format!(".{}", s))
} else {
test_file_path.parent().unwrap().join(s)
}
})
.map(|path| {
let text = std::fs::read_to_string(&path).unwrap();
(path, text)
})
.collect();
let mut variants: Vec<&str> = test_file_text
.split('\n')
.into_iter()
.filter_map(|t| t.strip_prefix("// META: variant="))
.collect();
if variants.is_empty() {
variants.push("");
}
for variant in variants {
let mut files = Vec::with_capacity(3 + imports.len());
files.push((testharness_path.clone(), testharness_text.clone()));
files.push((
testharnessreporter_path.clone(),
testharnessreporter_text.clone(),
));
files.extend(imports.clone());
files.push((test_file_path.clone(), test_file_text.clone()));
let mut file = tempfile::Builder::new()
.prefix("wpt-bundle-")
.suffix(".js")
.rand_bytes(5)
.tempfile()
.unwrap();
let bundle = concat_bundle(files, file.path(), "".to_string());
file.write_all(bundle.as_bytes()).unwrap();
let child = util::deno_cmd()
.current_dir(test_file_path.parent().unwrap())
.arg("run")
.arg("--location")
.arg(&format!("http://web-platform-tests/?{}", variant))
.arg("-A")
.arg(file.path())
.arg(deno_core::serde_json::to_string(&expect_fail).unwrap())
.arg("--quiet")
.stdin(std::process::Stdio::piped())
.spawn()
.unwrap();
let output = child.wait_with_output().unwrap();
if !output.status.success() {
file.keep().unwrap();
}
assert!(output.status.success());
}
}
}
}
#[tokio::test(flavor = "multi_thread", worker_threads = 2)]
async fn test_resolve_dns() {
use std::collections::BTreeMap;

View file

@ -1,250 +0,0 @@
{
"streams": [
// "piping/abort",
// "piping/close-propagation-backward",
// "piping/close-propagation-forward",
// "piping/error-propagation-backward",
// "piping/error-propagation-forward",
"piping/flow-control",
// "piping/general",
"piping/multiple-propagation",
"piping/pipe-through",
"piping/then-interception",
// "piping/throwing-options",
// "piping/transform-streams",
"queuing-strategies.any",
// "readable-byte-streams",
// "readable-streams/async-iterator",
// "readable-streams/bad-strategies",
// "readable-streams/bad-underlying-source",
// "readable-streams/cancel",
// "readable-streams/constructor",
"readable-streams/count-queuing-strategy-integration",
"readable-streams/default-reader",
"readable-streams/floating-point-total-queue-size",
"readable-streams/garbage-collection",
"readable-streams/general",
{
"name": "readable-streams/patched-global",
"expectFail": [
"ReadableStream async iterator should use the original values of getReader() and ReadableStreamDefaultReader methods"
]
},
"readable-streams/reentrant-strategies",
"readable-streams/tee",
// "readable-streams/templated",
"transform-streams/backpressure",
"transform-streams/errors",
"transform-streams/flush",
"transform-streams/general",
"transform-streams/lipfuzz",
// "transform-streams/patched-global",
"transform-streams/properties",
"transform-streams/reentrant-strategies",
"transform-streams/strategies",
// "transform-streams/terminate",
// "writable-streams/aborting",
// "writable-streams/bad-strategies",
"writable-streams/bad-underlying-sinks",
"writable-streams/byte-length-queuing-strategy",
// "writable-streams/close",
// "writable-streams/constructor",
"writable-streams/count-queuing-strategy",
"writable-streams/error",
"writable-streams/floating-point-total-queue-size",
"writable-streams/general",
"writable-streams/properties",
"writable-streams/reentrant-strategy",
"writable-streams/start",
"writable-streams/write"
],
"encoding": [
"api-basics",
"api-invalid-label",
"api-replacement-encodings",
"api-surrogates-utf8",
// TODO(lucacasonato): enable encodeInto. We have a bug in implementaiton.
// {
// "name": "encodeInto",
// "expectFail": [
// "encodeInto() and a detached output buffer"
// ]
// },
// "encodeInto",
// TODO(lucacasonato): enable when we support iso-2022-jp
// "iso-2022-jp-decoder",
// TODO(lucacasonato): uses XMLHttpRequest unnecessarily. should be fixed upstream before enabling
// "replacement-encodings",
"textdecoder-byte-order-marks",
{
"name": "textdecoder-copy",
"expectFail": [
// TODO(lucacasonato): enable when we have stream support
"Modify buffer after passing it in (ArrayBuffer)",
"Modify buffer after passing it in (SharedArrayBuffer)"
]
},
"textdecoder-fatal-single-byte",
"textdecoder-fatal.",
"textdecoder-ignorebom",
{
"name": "textdecoder-labels",
"expectFail": [
"cseucpkdfmtjapanese => EUC-JP",
"euc-jp => EUC-JP",
"x-euc-jp => EUC-JP",
"csiso2022jp => ISO-2022-JP",
"iso-2022-jp => ISO-2022-JP",
"csshiftjis => Shift_JIS",
"ms932 => Shift_JIS",
"ms_kanji => Shift_JIS",
"shift-jis => Shift_JIS",
"shift_jis => Shift_JIS",
"sjis => Shift_JIS",
"windows-31j => Shift_JIS",
"x-sjis => Shift_JIS",
"cseuckr => EUC-KR",
"csksc56011987 => EUC-KR",
"euc-kr => EUC-KR",
"iso-ir-149 => EUC-KR",
"korean => EUC-KR",
"ks_c_5601-1987 => EUC-KR",
"ks_c_5601-1989 => EUC-KR",
"ksc5601 => EUC-KR",
"ksc_5601 => EUC-KR",
"windows-949 => EUC-KR",
"x-user-defined => x-user-defined"
]
},
// TODO(lucacasonato): enable when we have stream support
// "textdecoder-streaming",
"textdecoder-utf16-surrogates",
{
"name": "textencoder-constructor-non-utf",
"expectFail": [
"Encoding argument supported for decode: EUC-JP",
"Encoding argument supported for decode: ISO-2022-JP",
"Encoding argument supported for decode: Shift_JIS",
"Encoding argument supported for decode: EUC-KR",
"Encoding argument supported for decode: x-user-defined"
]
},
"textencoder-utf16-surrogates",
"legacy-mb-schinese"
// TODO(lucacasonato): uses XMLHttpRequest unnecessarily. should be fixed upstream before enabling
// "unsupported-encodings",
],
"dom": [
"abort/event"
],
"hr-time": [
"monotonic-clock"
],
"html": [
"webappapis/microtask-queuing/queue-microtask-exceptions.any",
"webappapis/microtask-queuing/queue-microtask.any",
"webappapis/timers"
],
"user-timing": [
"clear_all_marks",
"clear_all_measures",
"clear_non_existent_mark",
"clear_non_existent_measure",
"clear_one_mark",
"clear_one_measure",
"entry_type",
"mark-entry-constructor",
"mark-errors",
"mark-measure-return-objects",
"mark.any",
"measure_syntax_err",
"measure-l3",
"structured-serialize-detail",
"user_timing_exists"
],
"wasm": [
"jsapi/constructor/compile",
"jsapi/constructor/multi-value",
"jsapi/constructor/toStringTag",
"jsapi/constructor/validate",
"jsapi/global/constructor",
"jsapi/global/toString",
"jsapi/global/value-get-set",
"jsapi/global/valueOf",
"jsapi/instance/toString",
"jsapi/instance/constructor-caching",
"jsapi/memory/toString",
"jsapi/module/constructor",
"jsapi/module/customSections",
"jsapi/module/exports",
"jsapi/module/imports",
"jsapi/module/toString",
"jsapi/table/get-set",
"jsapi/table/toString",
"webapi/body",
"webapi/invalid-args",
"webapi/rejected-arg",
"webapi/status",
"webapi/create_multiple_memory",
"create_multiple_memory"
//FAILING TESTS
// "jsapi/constructor/instantiate-bad-imports",
// "jsapi/constructor/instantiate",
// "jsapi/global/type",
// "jsapi/instance/constructor-bad-imports",
// "jsapi/instance/constructor",
// "jsapi/instance/exports",
// "jsapi/memory/buffer",
// "jsapi/memory/constructor-shared",
// "jsapi/memory/constructor-types",
// "jsapi/memory/constructor",
// "jsapi/memory/grow",
// "jsapi/memory/type",
// "jsapi/table/constructor-types",
// "jsapi/table/constructor",
// "jsapi/table/grow-reftypes",
// "jsapi/table/grow",
// "jsapi/table/length",
// "jsapi/idlharness",
// "jsapi/instance",
// "jsapi/prototypes",
// "serialization/arraybuffer/transfer"
// "serialization/module/nested-worker-success",
// "serialization/module/serialization-via-idb",
// "serialization/module/serialization-via-notifications-api",
// "webapi/abort",
// "webapi/contenttype",
// "webapi/empty-body",
// "webapi/historical",
// "webapi/idlharness",
// "webapi/instantiateStreaming-bad-imports",
// "webapi/instantiateStreaming",
// "webapi/invalid-code",
// "webapi/origin",
],
"console": [
"console-is-a-namespace",
"console-label-conversion",
"console-namespace-object-class-string",
"console-tests-historical"
],
"WebCryptoApi": [
"getRandomValues"
],
"WebIDL": [
"ecmascript-binding/es-exceptions/DOMException-constants",
"ecmascript-binding/es-exceptions/DOMException-constructor-and-prototype",
"ecmascript-binding/es-exceptions/DOMException-constructor-behavior",
{
"name": "ecmascript-binding/es-exceptions/DOMException-custom-bindings",
"expectFail": [
// TODO(kt3k): Enable this test.
// We can pass this test by using Object.setPrototypeOf(...) instead of
// class...extends, but that causes a problem in printing of uncaught
// DOMException. We might need to modify how to print uncaught error in
// `//core/error.rs`.
"does not inherit from Error: class-side"
]
}
]
}

View file

@ -1,129 +0,0 @@
const noColor = globalThis.Deno?.noColor ?? true;
const enabled = !noColor;
function code(open, close) {
return {
open: `\x1b[${open.join(";")}m`,
close: `\x1b[${close}m`,
regexp: new RegExp(`\\x1b\\[${close}m`, "g"),
};
}
function run(str, code) {
return enabled
? `${code.open}${str.replace(code.regexp, code.open)}${code.close}`
: str;
}
function red(str) {
return run(str, code([31], 39));
}
export function green(str) {
return run(str, code([32], 39));
}
export function yellow(str) {
return run(str, code([33], 39));
}
const testResults = [];
const testsExpectFail = JSON.parse(Deno.args[0]);
function shouldExpectFail(name) {
if (testsExpectFail.includes(name)) return true;
for (const expectFail of testsExpectFail) {
if (name.startsWith(expectFail)) return true;
}
return false;
}
window.add_result_callback(({ message, name, stack, status }) => {
const expectFail = shouldExpectFail(name);
testResults.push({
name,
passed: status === 0,
expectFail,
message,
stack,
});
let simpleMessage = `test ${name} ... `;
switch (status) {
case 0:
if (expectFail) {
simpleMessage += red("ok (expected fail)");
} else {
simpleMessage += green("ok");
if (Deno.args[1] == "--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;
}
console.log(simpleMessage);
});
window.add_completion_callback((tests, harnessStatus) => {
const failed = testResults.filter((t) => !t.expectFail && !t.passed);
const expectedFailedButPassed = testResults.filter((t) =>
t.expectFail && t.passed
);
const expectedFailedButPassedCount = expectedFailedButPassed.length;
const failedCount = failed.length + expectedFailedButPassedCount;
const expectedFailedAndFailedCount = testResults.filter((t) =>
t.expectFail && !t.passed
).length;
const totalCount = testResults.length;
const passedCount = totalCount - failedCount - expectedFailedAndFailedCount;
if (failed.length > 0) {
console.log(`\nfailures:`);
}
for (const result of failed) {
console.log(
`\n${result.name}\n${result.message}\n${result.stack}`,
);
}
if (failed.length > 0) {
console.log(`\nfailures:\n`);
}
for (const result of failed) {
console.log(` ${result.name}`);
}
if (expectedFailedButPassedCount > 0) {
console.log(`\nexpected failures that passed:\n`);
}
for (const result of expectedFailedButPassed) {
console.log(` ${result.name}`);
}
console.log(
`\ntest result: ${
failedCount > 0 ? red("failed") : green("ok")
}. ${passedCount} passed; ${failedCount} failed; ${expectedFailedAndFailedCount} expected failure; total ${totalCount}\n`,
);
Deno.exit(failedCount > 0 ? 1 : 0);
});