feat: works

This commit is contained in:
2022-07-17 10:11:20 +08:00
parent 4ba63b4c2e
commit 74a202f1ed
458 changed files with 125067 additions and 8 deletions

View File

@@ -0,0 +1,107 @@
use boa_engine::{
builtins::JsArgs,
object::{JsObject, ObjectInitializer},
property::Attribute,
Context, JsResult, JsValue,
};
/// Initializes the object in the context.
pub(super) fn init(context: &mut Context) -> JsObject {
let global_obj = context.global_object().clone();
let obj = ObjectInitializer::new(context)
.function(create_realm, "createRealm", 0)
.function(detach_array_buffer, "detachArrayBuffer", 2)
.function(eval_script, "evalScript", 1)
.function(gc, "gc", 0)
.property("global", global_obj, Attribute::default())
// .property("agent", agent, Attribute::default())
.build();
context.register_global_property("$262", obj.clone(), Attribute::empty());
obj
}
/// The `$262.createRealm()` function.
///
/// Creates a new ECMAScript Realm, defines this API on the new realm's global object, and
/// returns the `$262` property of the new realm's global object.
#[allow(clippy::unnecessary_wraps)]
fn create_realm(_this: &JsValue, _: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
let mut context = Context::default();
// add the $262 object.
let js_262 = init(&mut context);
Ok(JsValue::new(js_262))
}
/// The `$262.detachArrayBuffer()` function.
///
/// Implements the `DetachArrayBuffer` abstract operation.
fn detach_array_buffer(
_this: &JsValue,
args: &[JsValue],
context: &mut Context,
) -> JsResult<JsValue> {
#[inline]
fn type_err(context: &mut Context) -> JsValue {
context.construct_type_error("The provided object was not an ArrayBuffer")
}
let array_buffer = args
.get(0)
.and_then(JsValue::as_object)
.ok_or_else(|| type_err(context))?;
let mut array_buffer = array_buffer.borrow_mut();
let array_buffer = array_buffer
.as_array_buffer_mut()
.ok_or_else(|| type_err(context))?;
// 1. Assert: IsSharedArrayBuffer(arrayBuffer) is false. TODO
// 2. If key is not present, set key to undefined.
let key = args.get_or_undefined(1);
// 3. If SameValue(arrayBuffer.[[ArrayBufferDetachKey]], key) is false, throw a TypeError exception.
if !JsValue::same_value(&array_buffer.array_buffer_detach_key, key) {
return context.throw_type_error("Cannot detach array buffer with different key");
}
// 4. Set arrayBuffer.[[ArrayBufferData]] to null.
array_buffer.array_buffer_data = None;
// 5. Set arrayBuffer.[[ArrayBufferByteLength]] to 0.
array_buffer.array_buffer_byte_length = 0;
// 6. Return NormalCompletion(null).
Ok(JsValue::null())
}
/// The `$262.evalScript()` function.
///
/// Accepts a string value as its first argument and executes it as an ECMAScript script.
fn eval_script(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
if let Some(source_text) = args.get(0).and_then(JsValue::as_string) {
match context.parse(source_text.as_str()) {
// TODO: check strict
Err(e) => context.throw_type_error(format!("Uncaught Syntax Error: {e}")),
// Calling eval here parses the code a second time.
// TODO: We can fix this after we have have defined the public api for the vm executer.
Ok(_) => context.eval(source_text.as_str()),
}
} else {
Ok(JsValue::undefined())
}
}
/// The `$262.gc()` function.
///
/// Wraps the host's garbage collection invocation mechanism, if such a capability exists.
/// Must throw an exception if no capability exists. This is necessary for testing the
/// semantics of any feature that relies on garbage collection, e.g. the `WeakRef` API.
#[allow(clippy::unnecessary_wraps)]
fn gc(_this: &JsValue, _: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
boa_gc::force_collect();
Ok(JsValue::undefined())
}

View File

@@ -0,0 +1,402 @@
//! Execution module for the test runner.
mod js262;
use super::{
Harness, Outcome, Phase, SuiteResult, Test, TestFlags, TestOutcomeResult, TestResult,
TestSuite, IGNORED,
};
use boa_engine::{
builtins::JsArgs, object::FunctionBuilder, property::Attribute, syntax::Parser, Context,
JsResult, JsValue,
};
use boa_gc::{Cell, Finalize, Gc, Trace};
use colored::Colorize;
use rayon::prelude::*;
use std::panic;
impl TestSuite {
/// Runs the test suite.
pub(crate) fn run(&self, harness: &Harness, verbose: u8, parallel: bool) -> SuiteResult {
if verbose != 0 {
println!("Suite {}:", self.name);
}
let suites: Vec<_> = if parallel {
self.suites
.par_iter()
.map(|suite| suite.run(harness, verbose, parallel))
.collect()
} else {
self.suites
.iter()
.map(|suite| suite.run(harness, verbose, parallel))
.collect()
};
let tests: Vec<_> = if parallel {
self.tests
.par_iter()
.flat_map(|test| test.run(harness, verbose))
.collect()
} else {
self.tests
.iter()
.flat_map(|test| test.run(harness, verbose))
.collect()
};
let mut features = Vec::new();
for test_iter in self.tests.iter() {
for feature_iter in test_iter.features.iter() {
features.push(feature_iter.to_string());
}
}
if verbose != 0 {
println!();
}
// Count passed tests
let mut passed = 0;
let mut ignored = 0;
let mut panic = 0;
for test in &tests {
match test.result {
TestOutcomeResult::Passed => passed += 1,
TestOutcomeResult::Ignored => ignored += 1,
TestOutcomeResult::Panic => panic += 1,
TestOutcomeResult::Failed => {}
}
}
// Count total tests
let mut total = tests.len();
for suite in &suites {
total += suite.total;
passed += suite.passed;
ignored += suite.ignored;
panic += suite.panic;
features.append(&mut suite.features.clone());
}
if verbose != 0 {
println!(
"Suite {} results: total: {total}, passed: {}, ignored: {}, failed: {} (panics: \
{}{}), conformance: {:.2}%",
self.name,
passed.to_string().green(),
ignored.to_string().yellow(),
(total - passed - ignored).to_string().red(),
if panic == 0 {
"0".normal()
} else {
panic.to_string().red()
},
if panic == 0 { "" } else { "" }.red(),
(passed as f64 / total as f64) * 100.0
);
}
SuiteResult {
name: self.name.clone(),
total,
passed,
ignored,
panic,
suites,
tests,
features,
}
}
}
impl Test {
/// Runs the test.
pub(crate) fn run(&self, harness: &Harness, verbose: u8) -> Vec<TestResult> {
let mut results = Vec::new();
if self.flags.contains(TestFlags::STRICT) && !self.flags.contains(TestFlags::RAW) {
results.push(self.run_once(harness, true, verbose));
}
if self.flags.contains(TestFlags::NO_STRICT) || self.flags.contains(TestFlags::RAW) {
results.push(self.run_once(harness, false, verbose));
}
results
}
/// Runs the test once, in strict or non-strict mode
fn run_once(&self, harness: &Harness, strict: bool, verbose: u8) -> TestResult {
if verbose > 1 {
println!(
"`{}`{}: starting",
self.name,
if strict { " (strict mode)" } else { "" }
);
}
let test_content = if strict {
format!("\"use strict\";\n{}", self.content)
} else {
self.content.to_string()
};
let (result, result_text) = if !IGNORED.contains_any_flag(self.flags)
&& !IGNORED.contains_test(&self.name)
&& !IGNORED.contains_any_feature(&self.features)
&& (matches!(self.expected_outcome, Outcome::Positive)
|| matches!(
self.expected_outcome,
Outcome::Negative {
phase: Phase::Parse,
error_type: _,
}
)
|| matches!(
self.expected_outcome,
Outcome::Negative {
phase: Phase::Early,
error_type: _,
}
)
|| matches!(
self.expected_outcome,
Outcome::Negative {
phase: Phase::Runtime,
error_type: _,
}
)) {
let res = panic::catch_unwind(|| match self.expected_outcome {
Outcome::Positive => {
let mut context = Context::default();
let callback_obj = CallbackObject::default();
// TODO: timeout
match self.set_up_env(harness, &mut context, callback_obj.clone()) {
Ok(_) => {
let res = context.eval(&test_content);
let passed = res.is_ok()
&& matches!(*callback_obj.result.borrow(), Some(true) | None);
let text = match res {
Ok(val) => val.display().to_string(),
Err(e) => format!("Uncaught {}", e.display()),
};
(passed, text)
}
Err(e) => (false, e),
}
}
Outcome::Negative {
phase: Phase::Parse | Phase::Early,
ref error_type,
} => {
assert_eq!(
error_type.as_ref(),
"SyntaxError",
"non-SyntaxError parsing/early error found in {}",
self.name
);
let mut context = Context::default();
match context.parse(&test_content) {
Ok(statement_list) => match context.compile(&statement_list) {
Ok(_) => (false, "StatementList compilation should fail".to_owned()),
Err(e) => (true, format!("Uncaught {e:?}")),
},
Err(e) => (true, format!("Uncaught {e}")),
}
}
Outcome::Negative {
phase: Phase::Resolution,
error_type: _,
} => todo!("check module resolution errors"),
Outcome::Negative {
phase: Phase::Runtime,
ref error_type,
} => {
let mut context = Context::default();
if let Err(e) = Parser::new(test_content.as_bytes()).parse_all(&mut context) {
(false, format!("Uncaught {e}"))
} else {
// TODO: timeout
match self.set_up_env(harness, &mut context, CallbackObject::default()) {
Ok(_) => match context.eval(&test_content) {
Ok(res) => (false, res.display().to_string()),
Err(e) => {
let passed = e
.display()
.internals(true)
.to_string()
.contains(error_type.as_ref());
(passed, format!("Uncaught {}", e.display()))
}
},
Err(e) => (false, e),
}
}
}
});
let result = res.map_or_else(
|_| {
eprintln!("last panic was on test \"{}\"", self.name);
(TestOutcomeResult::Panic, String::new())
},
|(res, text)| {
if res {
(TestOutcomeResult::Passed, text)
} else {
(TestOutcomeResult::Failed, text)
}
},
);
if verbose > 1 {
println!(
"`{}`{}: {}",
self.name,
if strict { " (strict mode)" } else { "" },
if matches!(result, (TestOutcomeResult::Passed, _)) {
"Passed".green()
} else if matches!(result, (TestOutcomeResult::Failed, _)) {
"Failed".red()
} else {
"⚠ Panic ⚠".red()
}
);
} else {
print!(
"{}",
if matches!(result, (TestOutcomeResult::Passed, _)) {
".".green()
} else {
".".red()
}
);
}
result
} else {
if verbose > 1 {
println!(
"`{}`{}: {}",
self.name,
if strict { " (strict mode)" } else { "" },
"Ignored".yellow()
);
} else {
print!("{}", ".".yellow());
}
(TestOutcomeResult::Ignored, String::new())
};
if verbose > 2 {
println!(
"`{}`{}: result text",
self.name,
if strict { " (strict mode)" } else { "" },
);
println!("{result_text}");
println!();
}
TestResult {
name: self.name.clone(),
strict,
result,
result_text: result_text.into_boxed_str(),
}
}
/// Sets the environment up to run the test.
fn set_up_env(
&self,
harness: &Harness,
context: &mut Context,
callback_obj: CallbackObject,
) -> Result<(), String> {
// Register the print() function.
Self::register_print_fn(context, callback_obj);
// add the $262 object.
let _js262 = js262::init(context);
if self.flags.contains(TestFlags::RAW) {
return Ok(());
}
context
.eval(harness.assert.as_ref())
.map_err(|e| format!("could not run assert.js:\n{}", e.display()))?;
context
.eval(harness.sta.as_ref())
.map_err(|e| format!("could not run sta.js:\n{}", e.display()))?;
if self.flags.contains(TestFlags::ASYNC) {
context
.eval(harness.doneprint_handle.as_ref())
.map_err(|e| format!("could not run doneprintHandle.js:\n{}", e.display()))?;
}
for include in self.includes.iter() {
context
.eval(
&harness
.includes
.get(include)
.ok_or_else(|| format!("could not find the {include} include file."))?
.as_ref(),
)
.map_err(|e| {
format!(
"could not run the {include} include file:\nUncaught {}",
e.display()
)
})?;
}
Ok(())
}
/// Registers the print function in the context.
fn register_print_fn(context: &mut Context, callback_object: CallbackObject) {
// We use `FunctionBuilder` to define a closure with additional captures.
let js_function =
FunctionBuilder::closure_with_captures(context, test262_print, callback_object)
.name("print")
.length(1)
.build();
context.register_global_property(
"print",
js_function,
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
);
}
}
/// Object which includes the result of the async operation.
#[derive(Debug, Clone, Default, Trace, Finalize)]
struct CallbackObject {
result: Gc<Cell<Option<bool>>>,
}
/// `print()` function required by the test262 suite.
#[allow(clippy::unnecessary_wraps)]
fn test262_print(
_this: &JsValue,
args: &[JsValue],
captures: &mut CallbackObject,
_context: &mut Context,
) -> JsResult<JsValue> {
if let Some(message) = args.get_or_undefined(0).as_string() {
*captures.result.borrow_mut() = Some(message.as_str() == "Test262:AsyncTestComplete");
} else {
*captures.result.borrow_mut() = Some(false);
}
Ok(JsValue::undefined())
}

View File

@@ -0,0 +1,552 @@
//! Test262 test runner
//!
//! This crate will run the full ECMAScript test suite (Test262) and report compliance of the
//! `boa` context.
#![doc(
html_logo_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg",
html_favicon_url = "https://raw.githubusercontent.com/boa-dev/boa/main/assets/logo.svg"
)]
#![cfg_attr(not(test), deny(clippy::unwrap_used))]
#![warn(
clippy::perf,
clippy::single_match_else,
clippy::dbg_macro,
clippy::doc_markdown,
clippy::wildcard_imports,
clippy::struct_excessive_bools,
clippy::doc_markdown,
clippy::semicolon_if_nothing_returned,
clippy::pedantic
)]
#![deny(
clippy::all,
clippy::cast_lossless,
clippy::redundant_closure_for_method_calls,
clippy::unnested_or_patterns,
clippy::trivially_copy_pass_by_ref,
clippy::needless_pass_by_value,
clippy::match_wildcard_for_single_variants,
clippy::map_unwrap_or,
unused_qualifications,
unused_import_braces,
unused_lifetimes,
unreachable_pub,
trivial_numeric_casts,
// rustdoc,
missing_debug_implementations,
missing_copy_implementations,
deprecated_in_future,
meta_variable_misuse,
non_ascii_idents,
rust_2018_compatibility,
rust_2018_idioms,
future_incompatible,
nonstandard_style,
)]
#![allow(
clippy::use_self, // TODO: deny once false positives are fixed
clippy::module_name_repetitions,
clippy::cast_possible_truncation,
clippy::cast_sign_loss,
clippy::cast_precision_loss,
clippy::cast_possible_wrap,
clippy::cast_ptr_alignment,
clippy::missing_panics_doc,
clippy::too_many_lines,
clippy::unreadable_literal,
clippy::missing_inline_in_public_items,
clippy::cognitive_complexity,
clippy::must_use_candidate,
clippy::missing_errors_doc,
clippy::as_conversions,
clippy::let_unit_value,
rustdoc::missing_doc_code_examples
)]
mod exec;
mod read;
mod results;
use self::{
read::{read_harness, read_suite, read_test, MetaData, Negative, TestFlag},
results::{compare_results, write_json},
};
use anyhow::{bail, Context};
use bitflags::bitflags;
use colored::Colorize;
use fxhash::{FxHashMap, FxHashSet};
use once_cell::sync::Lazy;
use serde::{Deserialize, Serialize};
use std::{
fs,
path::{Path, PathBuf},
};
use structopt::StructOpt;
/// Structure to allow defining ignored tests, features and files that should
/// be ignored even when reading.
#[derive(Debug)]
struct Ignored {
tests: FxHashSet<Box<str>>,
features: FxHashSet<Box<str>>,
files: FxHashSet<Box<str>>,
flags: TestFlags,
}
impl Ignored {
/// Checks if the ignore list contains the given test name in the list of
/// tests to ignore.
pub(crate) fn contains_test(&self, test: &str) -> bool {
self.tests.contains(test)
}
/// Checks if the ignore list contains the given feature name in the list
/// of features to ignore.
pub(crate) fn contains_any_feature(&self, features: &[Box<str>]) -> bool {
features
.iter()
.any(|feature| self.features.contains(feature))
}
/// Checks if the ignore list contains the given file name in the list to
/// ignore from reading.
pub(crate) fn contains_file(&self, file: &str) -> bool {
self.files.contains(file)
}
pub(crate) fn contains_any_flag(&self, flags: TestFlags) -> bool {
flags.intersects(self.flags)
}
}
impl Default for Ignored {
fn default() -> Self {
Self {
tests: FxHashSet::default(),
features: FxHashSet::default(),
files: FxHashSet::default(),
flags: TestFlags::empty(),
}
}
}
/// List of ignored tests.
static IGNORED: Lazy<Ignored> = Lazy::new(|| {
let path = Path::new("test_ignore.txt");
if path.exists() {
let filtered = fs::read_to_string(path).expect("could not read test filters");
filtered
.lines()
.filter(|line| !line.is_empty() && !line.starts_with("//"))
.fold(Ignored::default(), |mut ign, line| {
// let mut line = line.to_owned();
if line.starts_with("file:") {
let file = line
.strip_prefix("file:")
.expect("prefix disappeared")
.trim()
.to_owned()
.into_boxed_str();
let test = if file.ends_with(".js") {
file.strip_suffix(".js")
.expect("suffix disappeared")
.to_owned()
.into_boxed_str()
} else {
file.clone()
};
ign.files.insert(file);
ign.tests.insert(test);
} else if line.starts_with("feature:") {
ign.features.insert(
line.strip_prefix("feature:")
.expect("prefix disappeared")
.trim()
.to_owned()
.into_boxed_str(),
);
} else if line.starts_with("flag:") {
let flag = line
.strip_prefix("flag:")
.expect("prefix disappeared")
.trim()
.parse::<TestFlag>()
.expect("invalid flag found");
ign.flags.insert(flag.into());
} else {
let mut test = line.trim();
if test
.rsplit('.')
.next()
.map(|ext| ext.eq_ignore_ascii_case("js"))
== Some(true)
{
test = test.strip_suffix(".js").expect("suffix disappeared");
}
ign.tests.insert(test.to_owned().into_boxed_str());
}
ign
})
} else {
Ignored::default()
}
});
/// Boa test262 tester
#[derive(StructOpt, Debug)]
#[structopt(name = "Boa test262 tester")]
enum Cli {
/// Run the test suite.
Run {
/// Whether to show verbose output.
#[structopt(short, long, parse(from_occurrences))]
verbose: u8,
/// Path to the Test262 suite.
#[structopt(long, parse(from_os_str), default_value = "./test262")]
test262_path: PathBuf,
/// Which specific test or test suite to run. Should be a path relative to the Test262 directory: e.g. "test/language/types/number"
#[structopt(short, long, parse(from_os_str), default_value = "test")]
suite: PathBuf,
/// Optional output folder for the full results information.
#[structopt(short, long, parse(from_os_str))]
output: Option<PathBuf>,
/// Execute tests serially
#[structopt(short, long)]
disable_parallelism: bool,
},
Compare {
/// Base results of the suite.
#[structopt(parse(from_os_str))]
base: PathBuf,
/// New results to compare.
#[structopt(parse(from_os_str))]
new: PathBuf,
/// Whether to use markdown output
#[structopt(short, long)]
markdown: bool,
},
}
/// Program entry point.
fn main() {
match Cli::from_args() {
Cli::Run {
verbose,
test262_path,
suite,
output,
disable_parallelism,
} => {
if let Err(e) = run_test_suite(
verbose,
!disable_parallelism,
test262_path.as_path(),
suite.as_path(),
output.as_deref(),
) {
eprintln!("Error: {e}");
let mut src = e.source();
while let Some(e) = src {
eprintln!(" caused by: {e}");
src = e.source();
}
std::process::exit(1);
}
}
Cli::Compare {
base,
new,
markdown,
} => compare_results(base.as_path(), new.as_path(), markdown),
}
}
/// Runs the full test suite.
fn run_test_suite(
verbose: u8,
parallel: bool,
test262_path: &Path,
suite: &Path,
output: Option<&Path>,
) -> anyhow::Result<()> {
if let Some(path) = output {
if path.exists() {
if !path.is_dir() {
bail!("the output path must be a directory.");
}
} else {
fs::create_dir_all(path).context("could not create the output directory")?;
}
}
if verbose != 0 {
println!("Loading the test suite...");
}
let harness = read_harness(test262_path).context("could not read harness")?;
if suite.to_string_lossy().ends_with(".js") {
let test = read_test(&test262_path.join(suite)).with_context(|| {
let suite = suite.display();
format!("could not read the test {suite}")
})?;
if verbose != 0 {
println!("Test loaded, starting...");
}
test.run(&harness, verbose);
println!();
} else {
let suite = read_suite(&test262_path.join(suite)).with_context(|| {
let suite = suite.display();
format!("could not read the suite {suite}")
})?;
if verbose != 0 {
println!("Test suite loaded, starting tests...");
}
let results = suite.run(&harness, verbose, parallel);
println!();
println!("Results:");
println!("Total tests: {}", results.total);
println!("Passed tests: {}", results.passed.to_string().green());
println!("Ignored tests: {}", results.ignored.to_string().yellow());
println!(
"Failed tests: {} (panics: {})",
(results.total - results.passed - results.ignored)
.to_string()
.red(),
results.panic.to_string().red()
);
println!(
"Conformance: {:.2}%",
(results.passed as f64 / results.total as f64) * 100.0
);
write_json(results, output, verbose)
.context("could not write the results to the output JSON file")?;
}
Ok(())
}
/// All the harness include files.
#[derive(Debug, Clone)]
struct Harness {
assert: Box<str>,
sta: Box<str>,
doneprint_handle: Box<str>,
includes: FxHashMap<Box<str>, Box<str>>,
}
/// Represents a test suite.
#[derive(Debug, Clone)]
struct TestSuite {
name: Box<str>,
suites: Box<[TestSuite]>,
tests: Box<[Test]>,
}
/// Outcome of a test suite.
#[derive(Debug, Clone, Serialize, Deserialize)]
struct SuiteResult {
#[serde(rename = "n")]
name: Box<str>,
#[serde(rename = "c")]
total: usize,
#[serde(rename = "o")]
passed: usize,
#[serde(rename = "i")]
ignored: usize,
#[serde(rename = "p")]
panic: usize,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
#[serde(rename = "s")]
suites: Vec<SuiteResult>,
#[serde(rename = "t")]
#[serde(skip_serializing_if = "Vec::is_empty", default)]
tests: Vec<TestResult>,
#[serde(rename = "f")]
#[serde(skip_serializing_if = "Vec::is_empty", default)]
features: Vec<String>,
}
/// Outcome of a test.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[allow(dead_code)]
struct TestResult {
#[serde(rename = "n")]
name: Box<str>,
#[serde(rename = "s", default)]
strict: bool,
#[serde(skip)]
result_text: Box<str>,
#[serde(rename = "r")]
result: TestOutcomeResult,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
enum TestOutcomeResult {
#[serde(rename = "O")]
Passed,
#[serde(rename = "I")]
Ignored,
#[serde(rename = "F")]
Failed,
#[serde(rename = "P")]
Panic,
}
/// Represents a test.
#[derive(Debug, Clone, Default)]
#[allow(dead_code)]
struct Test {
name: Box<str>,
description: Box<str>,
esid: Option<Box<str>>,
flags: TestFlags,
information: Box<str>,
features: Box<[Box<str>]>,
expected_outcome: Outcome,
includes: Box<[Box<str>]>,
locale: Locale,
content: Box<str>,
}
impl Test {
/// Creates a new test.
#[inline]
fn new<N, C>(name: N, content: C, metadata: MetaData) -> Self
where
N: Into<Box<str>>,
C: Into<Box<str>>,
{
Self {
name: name.into(),
description: metadata.description,
esid: metadata.esid,
flags: metadata.flags.into(),
information: metadata.info,
features: metadata.features,
expected_outcome: Outcome::from(metadata.negative),
includes: metadata.includes,
locale: metadata.locale,
content: content.into(),
}
}
/// Sets the name of the test.
fn set_name<N>(&mut self, name: N)
where
N: Into<Box<str>>,
{
self.name = name.into();
}
}
/// An outcome for a test.
#[derive(Debug, Clone)]
enum Outcome {
Positive,
Negative { phase: Phase, error_type: Box<str> },
}
impl Default for Outcome {
fn default() -> Self {
Self::Positive
}
}
impl From<Option<Negative>> for Outcome {
fn from(neg: Option<Negative>) -> Self {
neg.map(|neg| Self::Negative {
phase: neg.phase,
error_type: neg.error_type,
})
.unwrap_or_default()
}
}
bitflags! {
struct TestFlags: u16 {
const STRICT = 0b000000001;
const NO_STRICT = 0b000000010;
const MODULE = 0b000000100;
const RAW = 0b000001000;
const ASYNC = 0b000010000;
const GENERATED = 0b000100000;
const CAN_BLOCK_IS_FALSE = 0b001000000;
const CAN_BLOCK_IS_TRUE = 0b010000000;
const NON_DETERMINISTIC = 0b100000000;
}
}
impl Default for TestFlags {
fn default() -> Self {
Self::STRICT | Self::NO_STRICT
}
}
impl From<TestFlag> for TestFlags {
fn from(flag: TestFlag) -> Self {
match flag {
TestFlag::OnlyStrict => Self::STRICT,
TestFlag::NoStrict => Self::NO_STRICT,
TestFlag::Module => Self::MODULE,
TestFlag::Raw => Self::RAW,
TestFlag::Async => Self::ASYNC,
TestFlag::Generated => Self::GENERATED,
TestFlag::CanBlockIsFalse => Self::CAN_BLOCK_IS_FALSE,
TestFlag::CanBlockIsTrue => Self::CAN_BLOCK_IS_TRUE,
TestFlag::NonDeterministic => Self::NON_DETERMINISTIC,
}
}
}
impl<T> From<T> for TestFlags
where
T: AsRef<[TestFlag]>,
{
fn from(flags: T) -> Self {
let flags = flags.as_ref();
if flags.is_empty() {
Self::default()
} else {
let mut result = Self::empty();
for flag in flags {
result |= Self::from(*flag);
}
if !result.intersects(Self::default()) {
result |= Self::default();
}
result
}
}
}
/// Phase for an error.
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "lowercase")]
enum Phase {
Parse,
Early,
Resolution,
Runtime,
}
/// Locale information structure.
#[derive(Debug, Default, Clone, Deserialize)]
#[serde(transparent)]
#[allow(dead_code)]
struct Locale {
locale: Box<[Box<str>]>,
}

View File

@@ -0,0 +1,214 @@
//! Module to read the list of test suites from disk.
use super::{Harness, Locale, Phase, Test, TestSuite, IGNORED};
use anyhow::Context;
use fxhash::FxHashMap;
use serde::Deserialize;
use std::{fs, io, path::Path, str::FromStr};
/// Representation of the YAML metadata in Test262 tests.
#[derive(Debug, Clone, Deserialize)]
#[allow(dead_code)]
pub(super) struct MetaData {
pub(super) description: Box<str>,
pub(super) esid: Option<Box<str>>,
pub(super) es5id: Option<Box<str>>,
pub(super) es6id: Option<Box<str>>,
#[serde(default)]
pub(super) info: Box<str>,
#[serde(default)]
pub(super) features: Box<[Box<str>]>,
#[serde(default)]
pub(super) includes: Box<[Box<str>]>,
#[serde(default)]
pub(super) flags: Box<[TestFlag]>,
#[serde(default)]
pub(super) negative: Option<Negative>,
#[serde(default)]
pub(super) locale: Locale,
}
/// Negative test information structure.
#[derive(Debug, Clone, Deserialize)]
pub(super) struct Negative {
pub(super) phase: Phase,
#[serde(rename = "type")]
pub(super) error_type: Box<str>,
}
/// Individual test flag.
#[derive(Debug, Clone, Copy, Deserialize)]
#[serde(rename_all = "camelCase")]
pub(super) enum TestFlag {
OnlyStrict,
NoStrict,
Module,
Raw,
Async,
Generated,
#[serde(rename = "CanBlockIsFalse")]
CanBlockIsFalse,
#[serde(rename = "CanBlockIsTrue")]
CanBlockIsTrue,
#[serde(rename = "non-deterministic")]
NonDeterministic,
}
impl FromStr for TestFlag {
type Err = String; // TODO: improve error type.
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"onlyStrict" => Ok(Self::OnlyStrict),
"noStrict" => Ok(Self::NoStrict),
"module" => Ok(Self::Module),
"raw" => Ok(Self::Raw),
"async" => Ok(Self::Async),
"generated" => Ok(Self::Generated),
"CanBlockIsFalse" => Ok(Self::CanBlockIsFalse),
"CanBlockIsTrue" => Ok(Self::CanBlockIsTrue),
"non-deterministic" => Ok(Self::NonDeterministic),
_ => Err(format!("unknown test flag: {s}")),
}
}
}
/// Reads the Test262 defined bindings.
pub(super) fn read_harness(test262_path: &Path) -> anyhow::Result<Harness> {
let mut includes = FxHashMap::default();
for entry in
fs::read_dir(test262_path.join("harness")).context("error reading the harness directory")?
{
let entry = entry?;
let file_name = entry.file_name();
let file_name = file_name.to_string_lossy();
if file_name == "assert.js" || file_name == "sta.js" || file_name == "doneprintHandle.js" {
continue;
}
let content = fs::read_to_string(entry.path())
.with_context(|| format!("error reading the harnes/{file_name}"))?;
includes.insert(
file_name.into_owned().into_boxed_str(),
content.into_boxed_str(),
);
}
let assert = fs::read_to_string(test262_path.join("harness/assert.js"))
.context("error reading harnes/assert.js")?
.into_boxed_str();
let sta = fs::read_to_string(test262_path.join("harness/sta.js"))
.context("error reading harnes/sta.js")?
.into_boxed_str();
let doneprint_handle = fs::read_to_string(test262_path.join("harness/doneprintHandle.js"))
.context("error reading harnes/doneprintHandle.js")?
.into_boxed_str();
Ok(Harness {
assert,
sta,
doneprint_handle,
includes,
})
}
/// Reads a test suite in the given path.
pub(super) fn read_suite(path: &Path) -> anyhow::Result<TestSuite> {
let name = path
.file_name()
.with_context(|| format!("test suite with no name found: {}", path.display()))?
.to_str()
.with_context(|| format!("non-UTF-8 suite name found: {}", path.display()))?;
let mut suites = Vec::new();
let mut tests = Vec::new();
// TODO: iterate in parallel
for entry in path.read_dir().context("retrieving entry")? {
let entry = entry?;
if entry.file_type().context("retrieving file type")?.is_dir() {
suites.push(read_suite(entry.path().as_path()).with_context(|| {
let path = entry.path();
let suite = path.display();
format!("error reading sub-suite {suite}")
})?);
} else if entry.file_name().to_string_lossy().contains("_FIXTURE") {
continue;
} else if IGNORED.contains_file(&entry.file_name().to_string_lossy()) {
let mut test = Test::default();
test.set_name(entry.file_name().to_string_lossy());
tests.push(test);
} else {
tests.push(read_test(entry.path().as_path()).with_context(|| {
let path = entry.path();
let suite = path.display();
format!("error reading test {suite}")
})?);
}
}
Ok(TestSuite {
name: name.into(),
suites: suites.into_boxed_slice(),
tests: tests.into_boxed_slice(),
})
}
/// Reads information about a given test case.
pub(super) fn read_test(path: &Path) -> io::Result<Test> {
let name = path
.file_stem()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("test with no file name found: {}", path.display()),
)
})?
.to_str()
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidInput,
format!("non-UTF-8 file name found: {}", path.display()),
)
})?;
let content = fs::read_to_string(path)?;
let metadata = read_metadata(&content, path)?;
Ok(Test::new(name, content, metadata))
}
/// Reads the metadata from the input test code.
fn read_metadata(code: &str, test: &Path) -> io::Result<MetaData> {
use once_cell::sync::Lazy;
use regex::Regex;
/// Regular expression to retrieve the metadata of a test.
static META_REGEX: Lazy<Regex> = Lazy::new(|| {
Regex::new(r#"/\*\-{3}((?:.|\n)*)\-{3}\*/"#)
.expect("could not compile metadata regular expression")
});
let yaml = META_REGEX
.captures(code)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("no metadata found for test {}", test.display()),
)
})?
.get(1)
.ok_or_else(|| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("no metadata found for test {}", test.display()),
)
})?
.as_str()
.replace('\r', "\n");
serde_yaml::from_str(&yaml).map_err(|e| io::Error::new(io::ErrorKind::InvalidData, e))
}

View File

@@ -0,0 +1,500 @@
use super::SuiteResult;
use serde::{Deserialize, Serialize};
use std::{
env, fs,
io::{self, BufReader, BufWriter},
path::Path,
};
/// Structure to store full result information.
#[derive(Debug, Clone, Deserialize, Serialize)]
struct ResultInfo {
#[serde(rename = "c")]
commit: Box<str>,
#[serde(rename = "u")]
test262_commit: Box<str>,
#[serde(rename = "r")]
results: SuiteResult,
}
/// Structure to store full result information.
#[derive(Debug, Clone, Deserialize, Serialize)]
struct ReducedResultInfo {
#[serde(rename = "c")]
commit: Box<str>,
#[serde(rename = "u")]
test262_commit: Box<str>,
#[serde(rename = "t")]
total: usize,
#[serde(rename = "o")]
passed: usize,
#[serde(rename = "i")]
ignored: usize,
#[serde(rename = "p")]
panic: usize,
}
impl From<ResultInfo> for ReducedResultInfo {
/// Creates a new reduced suite result from a full suite result.
fn from(info: ResultInfo) -> Self {
Self {
commit: info.commit,
test262_commit: info.test262_commit,
total: info.results.total,
passed: info.results.passed,
ignored: info.results.ignored,
panic: info.results.panic,
}
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
struct FeaturesInfo {
#[serde(rename = "c")]
commit: Box<str>,
#[serde(rename = "u")]
test262_commit: Box<str>,
#[serde(rename = "n")]
suite_name: Box<str>,
#[serde(rename = "f")]
features: Vec<String>,
}
fn remove_duplicates(features_vec: &[String]) -> Vec<String> {
let mut result = features_vec.to_vec();
result.sort();
result.dedup();
result
}
impl From<ResultInfo> for FeaturesInfo {
fn from(info: ResultInfo) -> Self {
Self {
commit: info.commit,
test262_commit: info.test262_commit,
suite_name: info.results.name,
features: remove_duplicates(&info.results.features),
}
}
}
/// File name of the "latest results" JSON file.
const LATEST_FILE_NAME: &str = "latest.json";
/// File name of the "all results" JSON file.
const RESULTS_FILE_NAME: &str = "results.json";
/// File name of the "features" JSON file.
const FEATURES_FILE_NAME: &str = "features.json";
/// Writes the results of running the test suite to the given JSON output file.
///
/// It will append the results to the ones already present, in an array.
pub(crate) fn write_json(
results: SuiteResult,
output: Option<&Path>,
verbose: u8,
) -> io::Result<()> {
if let Some(path) = output {
let mut branch = env::var("GITHUB_REF").unwrap_or_default();
if branch.starts_with("refs/pull") {
branch = "pull".to_owned();
}
let path = if branch.is_empty() {
path.to_path_buf()
} else {
let folder = path.join(branch);
fs::create_dir_all(&folder)?;
folder
};
// We make sure we are using the latest commit information in GitHub pages:
update_gh_pages_repo(path.as_path(), verbose);
if verbose != 0 {
println!("Writing the results to {}...", path.display());
}
// Write the latest results.
let latest_path = path.join(LATEST_FILE_NAME);
let new_results = ResultInfo {
commit: env::var("GITHUB_SHA").unwrap_or_default().into_boxed_str(),
test262_commit: get_test262_commit(),
results,
};
let latest_output = BufWriter::new(fs::File::create(latest_path)?);
serde_json::to_writer(latest_output, &new_results)?;
// Write the full list of results, retrieving the existing ones first.
let all_path = path.join(RESULTS_FILE_NAME);
let mut all_results: Vec<ReducedResultInfo> = if all_path.exists() {
serde_json::from_reader(BufReader::new(fs::File::open(&all_path)?))?
} else {
Vec::new()
};
all_results.push(new_results.clone().into());
let output = BufWriter::new(fs::File::create(&all_path)?);
serde_json::to_writer(output, &all_results)?;
if verbose != 0 {
println!("Results written correctly");
}
// Write the full list of features, existing features go first.
let features_path = path.join(FEATURES_FILE_NAME);
let mut all_features: Vec<FeaturesInfo> = if features_path.exists() {
serde_json::from_reader(BufReader::new(fs::File::open(&features_path)?))?
} else {
Vec::new()
};
all_features.push(new_results.into());
let features_output = BufWriter::new(fs::File::create(&features_path)?);
serde_json::to_writer(features_output, &all_features)?;
if verbose != 0 {
println!("Features written correctly");
}
}
Ok(())
}
/// Gets the commit OID of the test262 submodule.
fn get_test262_commit() -> Box<str> {
let mut commit_id = fs::read_to_string(".git/modules/test262/HEAD")
.expect("did not find git submodule ref at '.git/modules/test262/HEAD'");
// Remove newline.
commit_id.pop();
commit_id.into_boxed_str()
}
/// Updates the GitHub pages repository by pulling latest changes before writing the new things.
fn update_gh_pages_repo(path: &Path, verbose: u8) {
if env::var("GITHUB_REF").is_ok() {
use std::process::Command;
// We run the command to pull the gh-pages branch: git -C ../gh-pages/ pull origin
Command::new("git")
.args(&["-C", "../gh-pages", "pull", "--ff-only"])
.output()
.expect("could not update GitHub Pages");
// Copy the full results file
let from = Path::new("../gh-pages/test262/refs/heads/main/").join(RESULTS_FILE_NAME);
let to = path.join(RESULTS_FILE_NAME);
if verbose != 0 {
println!(
"Copying the {} file to {} in order to add the results",
from.display(),
to.display()
);
}
fs::copy(from, to).expect("could not copy the main results file");
}
}
/// Compares the results of two test suite runs.
pub(crate) fn compare_results(base: &Path, new: &Path, markdown: bool) {
let base_results: ResultInfo = serde_json::from_reader(BufReader::new(
fs::File::open(base).expect("could not open the base results file"),
))
.expect("could not read the base results");
let new_results: ResultInfo = serde_json::from_reader(BufReader::new(
fs::File::open(new).expect("could not open the new results file"),
))
.expect("could not read the new results");
let base_total = base_results.results.total as isize;
let new_total = new_results.results.total as isize;
let total_diff = new_total - base_total;
let base_passed = base_results.results.passed as isize;
let new_passed = new_results.results.passed as isize;
let passed_diff = new_passed - base_passed;
let base_ignored = base_results.results.ignored as isize;
let new_ignored = new_results.results.ignored as isize;
let ignored_diff = new_ignored - base_ignored;
let base_failed = base_total - base_passed - base_ignored;
let new_failed = new_total - new_passed - new_ignored;
let failed_diff = new_failed - base_failed;
let base_panics = base_results.results.panic as isize;
let new_panics = new_results.results.panic as isize;
let panic_diff = new_panics - base_panics;
let base_conformance = (base_passed as f64 / base_total as f64) * 100_f64;
let new_conformance = (new_passed as f64 / new_total as f64) * 100_f64;
let conformance_diff = new_conformance - base_conformance;
let test_diff = compute_result_diff(Path::new(""), &base_results.results, &new_results.results);
if markdown {
use num_format::{Locale, ToFormattedString};
/// Generates a proper diff format, with some bold text if things change.
fn diff_format(diff: isize) -> String {
format!(
"{}{}{}{}",
if diff == 0 { "" } else { "**" },
if diff > 0 { "+" } else { "" },
diff.to_formatted_string(&Locale::en),
if diff == 0 { "" } else { "**" }
)
}
println!("#### VM implementation");
println!("| Test result | main count | PR count | difference |");
println!("| :---------: | :----------: | :------: | :--------: |");
println!(
"| Total | {} | {} | {} |",
base_total.to_formatted_string(&Locale::en),
new_total.to_formatted_string(&Locale::en),
diff_format(total_diff),
);
println!(
"| Passed | {} | {} | {} |",
base_passed.to_formatted_string(&Locale::en),
new_passed.to_formatted_string(&Locale::en),
diff_format(passed_diff),
);
println!(
"| Ignored | {} | {} | {} |",
base_ignored.to_formatted_string(&Locale::en),
new_ignored.to_formatted_string(&Locale::en),
diff_format(ignored_diff),
);
println!(
"| Failed | {} | {} | {} |",
base_failed.to_formatted_string(&Locale::en),
new_failed.to_formatted_string(&Locale::en),
diff_format(failed_diff),
);
println!(
"| Panics | {} | {} | {} |",
base_panics.to_formatted_string(&Locale::en),
new_panics.to_formatted_string(&Locale::en),
diff_format(panic_diff),
);
println!(
"| Conformance | {:.2}% | {:.2}% | {}{}{:.2}%{} |",
base_conformance,
new_conformance,
if conformance_diff.abs() > f64::EPSILON {
"**"
} else {
""
},
if conformance_diff > 0_f64 { "+" } else { "" },
conformance_diff,
if conformance_diff.abs() > f64::EPSILON {
"**"
} else {
""
},
);
if !test_diff.fixed.is_empty() {
println!();
println!(
"<details><summary><b>Fixed tests ({}):</b></summary>",
test_diff.fixed.len()
);
println!("\n```");
for test in test_diff.fixed {
println!("{test}");
}
println!("```");
println!("</details>");
}
if !test_diff.broken.is_empty() {
println!();
println!(
"<details><summary><b>Broken tests ({}):</b></summary>",
test_diff.broken.len()
);
println!("\n```");
for test in test_diff.broken {
println!("{test}");
}
println!("```");
println!("</details>");
}
if !test_diff.new_panics.is_empty() {
println!();
println!(
"<details><summary><b>New panics ({}):</b></summary>",
test_diff.new_panics.len()
);
println!("\n```");
for test in test_diff.new_panics {
println!("{test}");
}
println!("```");
println!("</details>");
}
if !test_diff.panic_fixes.is_empty() {
println!();
println!(
"<details><summary><b>Fixed panics ({}):</b></summary>",
test_diff.panic_fixes.len()
);
println!("\n```");
for test in test_diff.panic_fixes {
println!("{test}");
}
println!("```");
println!("</details>");
}
} else {
println!("Test262 conformance changes:");
println!("| Test result | main | PR | difference |");
println!(
"| Passed | {base_passed:^6} | {new_passed:^5} | {:^10} |",
base_passed - new_passed
);
println!(
"| Ignored | {base_ignored:^6} | {new_ignored:^5} | {:^10} |",
base_ignored - new_ignored
);
println!(
"| Failed | {base_failed:^6} | {new_failed:^5} | {:^10} |",
base_failed - new_failed,
);
println!(
"| Panics | {base_panics:^6} | {new_panics:^5} | {:^10} |",
base_panics - new_panics
);
if !test_diff.fixed.is_empty() {
println!();
println!("Fixed tests ({}):", test_diff.fixed.len());
for test in test_diff.fixed {
println!("{test}");
}
}
if !test_diff.broken.is_empty() {
println!();
println!("Broken tests ({}):", test_diff.broken.len());
for test in test_diff.broken {
println!("{test}");
}
}
if !test_diff.new_panics.is_empty() {
println!();
println!("New panics ({}):", test_diff.new_panics.len());
for test in test_diff.new_panics {
println!("{test}");
}
}
if !test_diff.panic_fixes.is_empty() {
println!();
println!("Fixed panics ({}):", test_diff.panic_fixes.len());
for test in test_diff.panic_fixes {
println!("{test}");
}
}
}
}
/// Test differences.
#[derive(Debug, Clone, Default)]
struct ResultDiff {
fixed: Vec<Box<str>>,
broken: Vec<Box<str>>,
new_panics: Vec<Box<str>>,
panic_fixes: Vec<Box<str>>,
}
impl ResultDiff {
/// Extends the diff with new results.
fn extend(&mut self, new: Self) {
self.fixed.extend(new.fixed);
self.broken.extend(new.broken);
self.new_panics.extend(new.new_panics);
self.panic_fixes.extend(new.panic_fixes);
}
}
/// Compares a base and a new result and returns the list of differences.
fn compute_result_diff(
base: &Path,
base_result: &SuiteResult,
new_result: &SuiteResult,
) -> ResultDiff {
use super::TestOutcomeResult;
let mut final_diff = ResultDiff::default();
for base_test in &base_result.tests {
if let Some(new_test) = new_result
.tests
.iter()
.find(|new_test| new_test.strict == base_test.strict && new_test.name == base_test.name)
{
let test_name = format!(
"test/{}/{}.js {}(previously {:?})",
base.display(),
new_test.name,
if base_test.strict {
"[strict mode] "
} else {
""
},
base_test.result
)
.into_boxed_str();
#[allow(clippy::match_same_arms)]
match (base_test.result, new_test.result) {
(a, b) if a == b => {}
(TestOutcomeResult::Ignored, TestOutcomeResult::Failed) => {}
(_, TestOutcomeResult::Passed) => final_diff.fixed.push(test_name),
(TestOutcomeResult::Panic, _) => final_diff.panic_fixes.push(test_name),
(_, TestOutcomeResult::Failed) => final_diff.broken.push(test_name),
(_, TestOutcomeResult::Panic) => final_diff.new_panics.push(test_name),
_ => {}
}
}
}
for base_suite in &base_result.suites {
if let Some(new_suite) = new_result
.suites
.iter()
.find(|new_suite| new_suite.name == base_suite.name)
{
let new_base = base.join(new_suite.name.as_ref());
let diff = compute_result_diff(new_base.as_path(), base_suite, new_suite);
final_diff.extend(diff);
}
}
final_diff
}