mirror of https://github.com/boa-dev/boa.git
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
1023 lines
28 KiB
1023 lines
28 KiB
//! Test262 test runner |
|
//! |
|
//! This crate will run the full ECMAScript test suite (Test262) and report compliance of the |
|
//! `boa` engine. |
|
#![doc = include_str!("../ABOUT.md")] |
|
#![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( |
|
// rustc lint groups https://doc.rust-lang.org/rustc/lints/groups.html |
|
warnings, |
|
future_incompatible, |
|
let_underscore, |
|
nonstandard_style, |
|
rust_2018_compatibility, |
|
rust_2018_idioms, |
|
rust_2021_compatibility, |
|
unused, |
|
|
|
// rustc allowed-by-default lints https://doc.rust-lang.org/rustc/lints/listing/allowed-by-default.html |
|
missing_docs, |
|
macro_use_extern_crate, |
|
meta_variable_misuse, |
|
missing_abi, |
|
missing_copy_implementations, |
|
missing_debug_implementations, |
|
non_ascii_idents, |
|
noop_method_call, |
|
single_use_lifetimes, |
|
trivial_casts, |
|
trivial_numeric_casts, |
|
unreachable_pub, |
|
unsafe_op_in_unsafe_fn, |
|
unused_crate_dependencies, |
|
unused_import_braces, |
|
unused_lifetimes, |
|
unused_qualifications, |
|
unused_tuple_struct_fields, |
|
variant_size_differences, |
|
|
|
// rustdoc lints https://doc.rust-lang.org/rustdoc/lints.html |
|
rustdoc::broken_intra_doc_links, |
|
rustdoc::private_intra_doc_links, |
|
rustdoc::missing_crate_level_docs, |
|
rustdoc::private_doc_tests, |
|
rustdoc::invalid_codeblock_attributes, |
|
rustdoc::invalid_rust_codeblocks, |
|
rustdoc::bare_urls, |
|
|
|
// clippy allowed by default |
|
clippy::dbg_macro, |
|
|
|
// clippy categories https://doc.rust-lang.org/clippy/ |
|
clippy::all, |
|
clippy::correctness, |
|
clippy::suspicious, |
|
clippy::style, |
|
clippy::complexity, |
|
clippy::perf, |
|
clippy::pedantic, |
|
)] |
|
#![allow( |
|
clippy::too_many_lines, |
|
clippy::redundant_pub_crate, |
|
clippy::cast_precision_loss |
|
)] |
|
|
|
mod edition; |
|
mod exec; |
|
mod read; |
|
mod results; |
|
|
|
use self::{ |
|
read::{read_harness, read_suite, read_test, MetaData, Negative, TestFlag}, |
|
results::{compare_results, write_json}, |
|
}; |
|
use bitflags::bitflags; |
|
use boa_engine::optimizer::OptimizerOptions; |
|
use clap::{ArgAction, Parser, ValueHint}; |
|
use color_eyre::{ |
|
eyre::{bail, eyre, WrapErr}, |
|
Result, |
|
}; |
|
use colored::Colorize; |
|
use edition::SpecEdition; |
|
use read::ErrorType; |
|
use rustc_hash::{FxHashMap, FxHashSet}; |
|
use serde::{ |
|
de::{Unexpected, Visitor}, |
|
Deserialize, Deserializer, Serialize, |
|
}; |
|
use std::{ |
|
ops::{Add, AddAssign}, |
|
path::{Path, PathBuf}, |
|
process::Command, |
|
}; |
|
|
|
/// Structure that contains the configuration of the tester. |
|
#[derive(Debug, Deserialize)] |
|
struct Config { |
|
#[serde(default)] |
|
commit: String, |
|
#[serde(default)] |
|
ignored: Ignored, |
|
} |
|
|
|
impl Config { |
|
/// Get the `Test262` repository commit. |
|
pub(crate) fn commit(&self) -> &str { |
|
&self.commit |
|
} |
|
|
|
/// Get [`Ignored`] `Test262` tests and features. |
|
pub(crate) const fn ignored(&self) -> &Ignored { |
|
&self.ignored |
|
} |
|
} |
|
|
|
/// Structure to allow defining ignored tests, features and files that should |
|
/// be ignored even when reading. |
|
#[derive(Debug, Deserialize)] |
|
struct Ignored { |
|
#[serde(default)] |
|
tests: FxHashSet<Box<str>>, |
|
#[serde(default)] |
|
features: FxHashSet<Box<str>>, |
|
#[serde(default = "TestFlags::empty")] |
|
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_feature(&self, feature: &str) -> bool { |
|
if self.features.contains(feature) { |
|
return true; |
|
} |
|
// Some features are an accessor instead of a simple feature name e.g. `Intl.DurationFormat`. |
|
// This ensures those are also ignored. |
|
feature |
|
.split('.') |
|
.next() |
|
.map(|feat| self.features.contains(feat)) |
|
.unwrap_or_default() |
|
} |
|
|
|
pub(crate) const 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(), |
|
flags: TestFlags::empty(), |
|
} |
|
} |
|
} |
|
|
|
/// Boa test262 tester |
|
#[derive(Debug, Parser)] |
|
#[command(author, version, about, name = "Boa test262 tester")] |
|
enum Cli { |
|
/// Run the test suite. |
|
Run { |
|
/// Whether to show verbose output. |
|
#[arg(short, long, action = ArgAction::Count)] |
|
verbose: u8, |
|
|
|
/// Path to the Test262 suite. |
|
#[arg( |
|
long, |
|
value_hint = ValueHint::DirPath, |
|
conflicts_with = "test262_commit" |
|
)] |
|
test262_path: Option<PathBuf>, |
|
|
|
/// Override config's Test262 commit. To checkout the latest commit set this to "latest". |
|
#[arg(long)] |
|
test262_commit: Option<String>, |
|
|
|
/// Which specific test or test suite to run. Should be a path relative to the Test262 directory: e.g. "test/language/types/number" |
|
#[arg(short, long, default_value = "test", value_hint = ValueHint::AnyPath)] |
|
suite: PathBuf, |
|
|
|
/// Enable optimizations |
|
#[arg(long, short = 'O')] |
|
optimize: bool, |
|
|
|
/// Optional output folder for the full results information. |
|
#[arg(short, long, value_hint = ValueHint::DirPath)] |
|
output: Option<PathBuf>, |
|
|
|
/// Execute tests serially |
|
#[arg(short, long)] |
|
disable_parallelism: bool, |
|
|
|
/// Path to a TOML file containing tester config. |
|
#[arg(short, long, default_value = "test262_config.toml", value_hint = ValueHint::FilePath)] |
|
config: PathBuf, |
|
|
|
/// Maximum ECMAScript edition to test for. |
|
#[arg(long)] |
|
edition: Option<SpecEdition>, |
|
|
|
/// Displays the conformance results per ECMAScript edition. |
|
#[arg(long)] |
|
versioned: bool, |
|
}, |
|
/// Compare two test suite results. |
|
Compare { |
|
/// Base results of the suite. |
|
#[arg(value_hint = ValueHint::FilePath)] |
|
base: PathBuf, |
|
|
|
/// New results to compare. |
|
#[arg(value_hint = ValueHint::FilePath)] |
|
new: PathBuf, |
|
|
|
/// Whether to use markdown output |
|
#[arg(short, long)] |
|
markdown: bool, |
|
}, |
|
} |
|
|
|
const DEFAULT_TEST262_DIRECTORY: &str = "test262"; |
|
|
|
/// Program entry point. |
|
fn main() -> Result<()> { |
|
color_eyre::install()?; |
|
match Cli::parse() { |
|
Cli::Run { |
|
verbose, |
|
test262_path, |
|
test262_commit, |
|
suite, |
|
output, |
|
optimize, |
|
disable_parallelism, |
|
config: config_path, |
|
edition, |
|
versioned, |
|
} => { |
|
let config: Config = { |
|
let input = std::fs::read_to_string(config_path)?; |
|
toml::from_str(&input).wrap_err("could not decode tester config file")? |
|
}; |
|
|
|
let test262_commit = test262_commit |
|
.as_deref() |
|
.or_else(|| Some(config.commit())) |
|
.filter(|s| !["", "latest"].contains(s)); |
|
|
|
let test262_path = if let Some(path) = test262_path.as_deref() { |
|
path |
|
} else { |
|
clone_test262(test262_commit, verbose)?; |
|
|
|
Path::new(DEFAULT_TEST262_DIRECTORY) |
|
}; |
|
|
|
run_test_suite( |
|
&config, |
|
verbose, |
|
!disable_parallelism, |
|
test262_path, |
|
suite.as_path(), |
|
output.as_deref(), |
|
edition.unwrap_or_default(), |
|
versioned, |
|
if optimize { |
|
OptimizerOptions::OPTIMIZE_ALL |
|
} else { |
|
OptimizerOptions::empty() |
|
}, |
|
) |
|
} |
|
Cli::Compare { |
|
base, |
|
new, |
|
markdown, |
|
} => compare_results(base.as_path(), new.as_path(), markdown), |
|
} |
|
} |
|
|
|
/// Returns the commit hash and commit message of the provided branch name. |
|
fn get_last_branch_commit(branch: &str, verbose: u8) -> Result<(String, String)> { |
|
if verbose > 1 { |
|
println!("Getting last commit on '{branch}' branch"); |
|
} |
|
let result = Command::new("git") |
|
.arg("log") |
|
.args(["-n", "1"]) |
|
.arg("--pretty=format:%H %s") |
|
.arg(branch) |
|
.current_dir(DEFAULT_TEST262_DIRECTORY) |
|
.output()?; |
|
|
|
if !result.status.success() { |
|
bail!( |
|
"test262 getting commit hash and message failed with return code {:?}", |
|
result.status.code() |
|
); |
|
} |
|
|
|
let output = std::str::from_utf8(&result.stdout)?.trim(); |
|
|
|
let (hash, message) = output |
|
.split_once(' ') |
|
.expect("git log output to contain hash and message"); |
|
|
|
Ok((hash.into(), message.into())) |
|
} |
|
|
|
fn reset_test262_commit(commit: &str, verbose: u8) -> Result<()> { |
|
if verbose != 0 { |
|
println!("Reset test262 to commit: {commit}..."); |
|
} |
|
|
|
let result = Command::new("git") |
|
.arg("reset") |
|
.arg("--hard") |
|
.arg(commit) |
|
.current_dir(DEFAULT_TEST262_DIRECTORY) |
|
.status()?; |
|
|
|
if !result.success() { |
|
bail!( |
|
"test262 commit {commit} checkout failed with return code: {:?}", |
|
result.code() |
|
); |
|
} |
|
|
|
Ok(()) |
|
} |
|
|
|
fn clone_test262(commit: Option<&str>, verbose: u8) -> Result<()> { |
|
const TEST262_REPOSITORY: &str = "https://github.com/tc39/test262"; |
|
|
|
let update = commit.is_none(); |
|
|
|
if Path::new(DEFAULT_TEST262_DIRECTORY).is_dir() { |
|
let (current_commit_hash, current_commit_message) = |
|
get_last_branch_commit("HEAD", verbose)?; |
|
|
|
if let Some(commit) = commit { |
|
if current_commit_hash == commit { |
|
return Ok(()); |
|
} |
|
} |
|
|
|
if verbose != 0 { |
|
println!("Fetching latest test262 commits..."); |
|
} |
|
let result = Command::new("git") |
|
.arg("fetch") |
|
.current_dir(DEFAULT_TEST262_DIRECTORY) |
|
.status()?; |
|
|
|
if !result.success() { |
|
bail!( |
|
"Test262 fetching latest failed with return code {:?}", |
|
result.code() |
|
); |
|
} |
|
|
|
if let Some(commit) = commit { |
|
println!("Test262 switching to commit {commit}..."); |
|
reset_test262_commit(commit, verbose)?; |
|
return Ok(()); |
|
} |
|
|
|
if verbose != 0 { |
|
println!("Checking latest Test262 with current HEAD..."); |
|
} |
|
let (latest_commit_hash, latest_commit_message) = |
|
get_last_branch_commit("origin/main", verbose)?; |
|
|
|
if current_commit_hash != latest_commit_hash { |
|
if update { |
|
println!("Updating Test262 repository:"); |
|
} else { |
|
println!("Warning Test262 repository is not in sync, use '--test262-commit latest' to automatically update it:"); |
|
} |
|
|
|
println!(" Current commit: {current_commit_hash} {current_commit_message}"); |
|
println!(" Latest commit: {latest_commit_hash} {latest_commit_message}"); |
|
|
|
if update { |
|
reset_test262_commit(&latest_commit_hash, verbose)?; |
|
} |
|
} |
|
|
|
return Ok(()); |
|
} |
|
|
|
println!("Cloning test262..."); |
|
let result = Command::new("git") |
|
.arg("clone") |
|
.arg(TEST262_REPOSITORY) |
|
.arg(DEFAULT_TEST262_DIRECTORY) |
|
.status()?; |
|
|
|
if !result.success() { |
|
bail!( |
|
"Cloning Test262 repository failed with return code {:?}", |
|
result.code() |
|
); |
|
} |
|
|
|
if let Some(commit) = commit { |
|
if verbose != 0 { |
|
println!("Reset Test262 to commit: {commit}..."); |
|
} |
|
|
|
reset_test262_commit(commit, verbose)?; |
|
} |
|
|
|
Ok(()) |
|
} |
|
|
|
/// Runs the full test suite. |
|
#[allow(clippy::too_many_arguments)] |
|
fn run_test_suite( |
|
config: &Config, |
|
verbose: u8, |
|
parallel: bool, |
|
test262_path: &Path, |
|
suite: &Path, |
|
output: Option<&Path>, |
|
edition: SpecEdition, |
|
versioned: bool, |
|
optimizer_options: OptimizerOptions, |
|
) -> Result<()> { |
|
if let Some(path) = output { |
|
if path.exists() { |
|
if !path.is_dir() { |
|
bail!("the output path must be a directory."); |
|
} |
|
} else { |
|
std::fs::create_dir_all(path).wrap_err("could not create the output directory")?; |
|
} |
|
} |
|
|
|
if verbose != 0 { |
|
println!("Loading the test suite..."); |
|
} |
|
let harness = read_harness(test262_path).wrap_err("could not read harness")?; |
|
|
|
if suite.to_string_lossy().ends_with(".js") { |
|
let test = read_test(&test262_path.join(suite)).wrap_err_with(|| { |
|
let suite = suite.display(); |
|
format!("could not read the test {suite}") |
|
})?; |
|
|
|
if test.edition <= edition { |
|
if verbose != 0 { |
|
println!("Test loaded, starting..."); |
|
} |
|
test.run(&harness, verbose, optimizer_options); |
|
} else { |
|
println!( |
|
"Minimum spec edition of test is bigger than the specified edition. Skipping." |
|
); |
|
} |
|
|
|
println!(); |
|
} else { |
|
let suite = |
|
read_suite(&test262_path.join(suite), config.ignored(), false).wrap_err_with(|| { |
|
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, edition, optimizer_options); |
|
|
|
if versioned { |
|
let mut table = comfy_table::Table::new(); |
|
table.load_preset(comfy_table::presets::UTF8_HORIZONTAL_ONLY); |
|
table.set_header(vec![ |
|
"Edition", "Total", "Passed", "Ignored", "Failed", "Panics", "%", |
|
]); |
|
for column in table.column_iter_mut().skip(1) { |
|
column.set_cell_alignment(comfy_table::CellAlignment::Right); |
|
} |
|
for (v, stats) in SpecEdition::all_editions() |
|
.filter(|v| *v <= edition) |
|
.map(|v| { |
|
let stats = results.versioned_stats.get(v).unwrap_or(results.stats); |
|
(v, stats) |
|
}) |
|
{ |
|
let Statistics { |
|
total, |
|
passed, |
|
ignored, |
|
panic, |
|
} = stats; |
|
let failed = total - passed - ignored; |
|
let conformance = (passed as f64 / total as f64) * 100.0; |
|
let conformance = format!("{conformance:.2}"); |
|
table.add_row(vec![ |
|
v.to_string(), |
|
total.to_string(), |
|
passed.to_string(), |
|
ignored.to_string(), |
|
failed.to_string(), |
|
panic.to_string(), |
|
conformance, |
|
]); |
|
} |
|
println!("\n\nResults\n"); |
|
println!("{table}"); |
|
} else { |
|
let Statistics { |
|
total, |
|
passed, |
|
ignored, |
|
panic, |
|
} = results.stats; |
|
println!("\n\nResults ({edition}):"); |
|
println!("Total tests: {total}"); |
|
println!("Passed tests: {}", passed.to_string().green()); |
|
println!("Ignored tests: {}", ignored.to_string().yellow()); |
|
println!( |
|
"Failed tests: {} ({})", |
|
(total - passed - ignored).to_string().red(), |
|
format!("{panic} panics").red() |
|
); |
|
println!( |
|
"Conformance: {:.2}%", |
|
(passed as f64 / total as f64) * 100.0 |
|
); |
|
} |
|
|
|
if let Some(output) = output { |
|
write_json(results, output, verbose, test262_path) |
|
.wrap_err("could not write the results to the output JSON file")?; |
|
} |
|
} |
|
|
|
Ok(()) |
|
} |
|
|
|
/// All the harness include files. |
|
#[derive(Debug, Clone)] |
|
struct Harness { |
|
assert: HarnessFile, |
|
sta: HarnessFile, |
|
doneprint_handle: HarnessFile, |
|
includes: FxHashMap<Box<str>, HarnessFile>, |
|
} |
|
|
|
#[derive(Debug, Clone)] |
|
struct HarnessFile { |
|
content: Box<str>, |
|
path: Box<Path>, |
|
} |
|
|
|
/// Represents a test suite. |
|
#[derive(Debug, Clone)] |
|
struct TestSuite { |
|
name: Box<str>, |
|
path: Box<Path>, |
|
suites: Box<[TestSuite]>, |
|
tests: Box<[Test]>, |
|
} |
|
|
|
/// Represents a tests statistic |
|
#[derive(Default, Debug, Copy, Clone, Serialize, Deserialize)] |
|
struct Statistics { |
|
#[serde(rename = "t")] |
|
total: usize, |
|
#[serde(rename = "o")] |
|
passed: usize, |
|
#[serde(rename = "i")] |
|
ignored: usize, |
|
#[serde(rename = "p")] |
|
panic: usize, |
|
} |
|
|
|
impl Add for Statistics { |
|
type Output = Self; |
|
|
|
fn add(self, rhs: Self) -> Self::Output { |
|
Self { |
|
total: self.total + rhs.total, |
|
passed: self.passed + rhs.passed, |
|
ignored: self.ignored + rhs.ignored, |
|
panic: self.panic + rhs.panic, |
|
} |
|
} |
|
} |
|
|
|
impl AddAssign for Statistics { |
|
fn add_assign(&mut self, rhs: Self) { |
|
self.total += rhs.total; |
|
self.passed += rhs.passed; |
|
self.ignored += rhs.ignored; |
|
self.panic += rhs.panic; |
|
} |
|
} |
|
|
|
/// Represents tests statistics separated by ECMAScript edition |
|
#[derive(Default, Debug, Copy, Clone, Serialize)] |
|
struct VersionedStats { |
|
es5: Statistics, |
|
es6: Statistics, |
|
es7: Statistics, |
|
es8: Statistics, |
|
es9: Statistics, |
|
es10: Statistics, |
|
es11: Statistics, |
|
es12: Statistics, |
|
es13: Statistics, |
|
es14: Statistics, |
|
} |
|
|
|
impl<'de> Deserialize<'de> for VersionedStats { |
|
fn deserialize<D>(deserializer: D) -> std::result::Result<Self, D::Error> |
|
where |
|
D: Deserializer<'de>, |
|
{ |
|
#[derive(Deserialize)] |
|
struct Inner { |
|
es5: Statistics, |
|
es6: Statistics, |
|
es7: Statistics, |
|
es8: Statistics, |
|
es9: Statistics, |
|
es10: Statistics, |
|
es11: Statistics, |
|
es12: Statistics, |
|
es13: Statistics, |
|
#[serde(default)] |
|
es14: Option<Statistics>, |
|
} |
|
|
|
let inner = Inner::deserialize(deserializer)?; |
|
|
|
let Inner { |
|
es5, |
|
es6, |
|
es7, |
|
es8, |
|
es9, |
|
es10, |
|
es11, |
|
es12, |
|
es13, |
|
es14, |
|
} = inner; |
|
let es14 = es14.unwrap_or(es13); |
|
|
|
Ok(Self { |
|
es5, |
|
es6, |
|
es7, |
|
es8, |
|
es9, |
|
es10, |
|
es11, |
|
es12, |
|
es13, |
|
es14, |
|
}) |
|
} |
|
} |
|
|
|
impl VersionedStats { |
|
/// Applies `f` to all the statistics for which its edition is bigger or equal |
|
/// than `min_edition`. |
|
fn apply(&mut self, min_edition: SpecEdition, f: fn(&mut Statistics)) { |
|
for edition in SpecEdition::all_editions().filter(|&edition| min_edition <= edition) { |
|
if let Some(stats) = self.get_mut(edition) { |
|
f(stats); |
|
} |
|
} |
|
} |
|
|
|
/// Gets the statistics corresponding to `edition`, returning `None` if `edition` |
|
/// is `SpecEdition::ESNext`. |
|
const fn get(&self, edition: SpecEdition) -> Option<Statistics> { |
|
let stats = match edition { |
|
SpecEdition::ES5 => self.es5, |
|
SpecEdition::ES6 => self.es6, |
|
SpecEdition::ES7 => self.es7, |
|
SpecEdition::ES8 => self.es8, |
|
SpecEdition::ES9 => self.es9, |
|
SpecEdition::ES10 => self.es10, |
|
SpecEdition::ES11 => self.es11, |
|
SpecEdition::ES12 => self.es12, |
|
SpecEdition::ES13 => self.es13, |
|
SpecEdition::ES14 => self.es14, |
|
SpecEdition::ESNext => return None, |
|
}; |
|
Some(stats) |
|
} |
|
|
|
/// Gets a mutable reference to the statistics corresponding to `edition`, returning `None` if |
|
/// `edition` is `SpecEdition::ESNext`. |
|
fn get_mut(&mut self, edition: SpecEdition) -> Option<&mut Statistics> { |
|
let stats = match edition { |
|
SpecEdition::ES5 => &mut self.es5, |
|
SpecEdition::ES6 => &mut self.es6, |
|
SpecEdition::ES7 => &mut self.es7, |
|
SpecEdition::ES8 => &mut self.es8, |
|
SpecEdition::ES9 => &mut self.es9, |
|
SpecEdition::ES10 => &mut self.es10, |
|
SpecEdition::ES11 => &mut self.es11, |
|
SpecEdition::ES12 => &mut self.es12, |
|
SpecEdition::ES13 => &mut self.es13, |
|
SpecEdition::ES14 => &mut self.es14, |
|
SpecEdition::ESNext => return None, |
|
}; |
|
Some(stats) |
|
} |
|
} |
|
|
|
impl Add for VersionedStats { |
|
type Output = Self; |
|
|
|
fn add(self, rhs: Self) -> Self::Output { |
|
Self { |
|
es5: self.es5 + rhs.es5, |
|
es6: self.es6 + rhs.es6, |
|
es7: self.es7 + rhs.es7, |
|
es8: self.es8 + rhs.es8, |
|
es9: self.es9 + rhs.es9, |
|
es10: self.es10 + rhs.es10, |
|
es11: self.es11 + rhs.es11, |
|
es12: self.es12 + rhs.es12, |
|
es13: self.es13 + rhs.es13, |
|
es14: self.es14 + rhs.es14, |
|
} |
|
} |
|
} |
|
|
|
impl AddAssign for VersionedStats { |
|
fn add_assign(&mut self, rhs: Self) { |
|
self.es5 += rhs.es5; |
|
self.es6 += rhs.es6; |
|
self.es7 += rhs.es7; |
|
self.es8 += rhs.es8; |
|
self.es9 += rhs.es9; |
|
self.es10 += rhs.es10; |
|
self.es11 += rhs.es11; |
|
self.es12 += rhs.es12; |
|
self.es13 += rhs.es13; |
|
self.es14 += rhs.es14; |
|
} |
|
} |
|
|
|
/// Outcome of a test suite. |
|
#[derive(Debug, Clone, Serialize, Deserialize)] |
|
struct SuiteResult { |
|
#[serde(rename = "n")] |
|
name: Box<str>, |
|
#[serde(rename = "a")] |
|
stats: Statistics, |
|
#[serde(rename = "av", default)] |
|
versioned_stats: VersionedStats, |
|
#[serde(skip_serializing_if = "Vec::is_empty", default)] |
|
#[serde(rename = "s")] |
|
suites: Vec<SuiteResult>, |
|
#[serde(skip_serializing_if = "Vec::is_empty", default)] |
|
#[serde(rename = "t")] |
|
tests: Vec<TestResult>, |
|
#[serde(skip_serializing_if = "FxHashSet::is_empty", default)] |
|
#[serde(rename = "f")] |
|
features: FxHashSet<String>, |
|
} |
|
|
|
/// Outcome of a test. |
|
#[derive(Debug, Clone, Serialize, Deserialize)] |
|
#[allow(dead_code)] |
|
struct TestResult { |
|
#[serde(rename = "n")] |
|
name: Box<str>, |
|
#[serde(rename = "v", default)] |
|
edition: SpecEdition, |
|
#[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)] |
|
#[allow(dead_code)] |
|
struct Test { |
|
name: Box<str>, |
|
path: Box<Path>, |
|
description: Box<str>, |
|
esid: Option<Box<str>>, |
|
edition: SpecEdition, |
|
flags: TestFlags, |
|
information: Box<str>, |
|
expected_outcome: Outcome, |
|
features: FxHashSet<Box<str>>, |
|
includes: FxHashSet<Box<str>>, |
|
locale: Locale, |
|
ignored: bool, |
|
} |
|
|
|
impl Test { |
|
/// Creates a new test. |
|
fn new<N, C>(name: N, path: C, metadata: MetaData) -> Result<Self> |
|
where |
|
N: Into<Box<str>>, |
|
C: Into<Box<Path>>, |
|
{ |
|
let edition = SpecEdition::from_test_metadata(&metadata) |
|
.map_err(|feats| eyre!("test metadata contained unknown features: {feats:?}"))?; |
|
|
|
Ok(Self { |
|
edition, |
|
name: name.into(), |
|
description: metadata.description, |
|
esid: metadata.esid, |
|
flags: metadata.flags.into(), |
|
information: metadata.info, |
|
features: metadata.features.into_vec().into_iter().collect(), |
|
expected_outcome: Outcome::from(metadata.negative), |
|
includes: metadata.includes.into_vec().into_iter().collect(), |
|
locale: metadata.locale, |
|
path: path.into(), |
|
ignored: false, |
|
}) |
|
} |
|
|
|
/// Sets the test as ignored. |
|
#[inline] |
|
fn set_ignored(&mut self) { |
|
self.ignored = true; |
|
} |
|
|
|
/// Checks if this is a module test. |
|
#[inline] |
|
const fn is_module(&self) -> bool { |
|
self.flags.contains(TestFlags::MODULE) |
|
} |
|
} |
|
|
|
/// An outcome for a test. |
|
#[derive(Debug, Clone)] |
|
enum Outcome { |
|
Positive, |
|
Negative { phase: Phase, error_type: ErrorType }, |
|
} |
|
|
|
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! { |
|
#[derive(Debug, Clone, Copy)] |
|
struct TestFlags: u16 { |
|
const STRICT = 0b0_0000_0001; |
|
const NO_STRICT = 0b0_0000_0010; |
|
const MODULE = 0b0_0000_0100; |
|
const RAW = 0b0_0000_1000; |
|
const ASYNC = 0b0_0001_0000; |
|
const GENERATED = 0b0_0010_0000; |
|
const CAN_BLOCK_IS_FALSE = 0b0_0100_0000; |
|
const CAN_BLOCK_IS_TRUE = 0b0_1000_0000; |
|
const NON_DETERMINISTIC = 0b1_0000_0000; |
|
} |
|
} |
|
|
|
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 |
|
} |
|
} |
|
} |
|
|
|
impl<'de> Deserialize<'de> for TestFlags { |
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error> |
|
where |
|
D: Deserializer<'de>, |
|
{ |
|
struct FlagsVisitor; |
|
|
|
impl<'de> Visitor<'de> for FlagsVisitor { |
|
type Value = TestFlags; |
|
|
|
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
|
write!(formatter, "a sequence of flags") |
|
} |
|
|
|
fn visit_seq<A>(self, mut seq: A) -> Result<Self::Value, A::Error> |
|
where |
|
A: serde::de::SeqAccess<'de>, |
|
{ |
|
let mut flags = TestFlags::empty(); |
|
while let Some(elem) = seq.next_element::<TestFlag>()? { |
|
flags |= elem.into(); |
|
} |
|
Ok(flags) |
|
} |
|
} |
|
|
|
struct RawFlagsVisitor; |
|
|
|
impl Visitor<'_> for RawFlagsVisitor { |
|
type Value = TestFlags; |
|
|
|
fn expecting(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { |
|
write!(formatter, "a flags number") |
|
} |
|
|
|
fn visit_u16<E>(self, v: u16) -> Result<Self::Value, E> |
|
where |
|
E: serde::de::Error, |
|
{ |
|
TestFlags::from_bits(v).ok_or_else(|| { |
|
E::invalid_value(Unexpected::Unsigned(v.into()), &"a valid flag number") |
|
}) |
|
} |
|
} |
|
|
|
if deserializer.is_human_readable() { |
|
deserializer.deserialize_seq(FlagsVisitor) |
|
} else { |
|
deserializer.deserialize_u16(RawFlagsVisitor) |
|
} |
|
} |
|
} |
|
|
|
/// Phase for an error. |
|
#[derive(Debug, Clone, Copy, Deserialize)] |
|
#[serde(rename_all = "lowercase")] |
|
enum Phase { |
|
Parse, |
|
Resolution, |
|
Runtime, |
|
} |
|
|
|
/// Locale information structure. |
|
#[derive(Debug, Default, Clone, Deserialize)] |
|
#[serde(transparent)] |
|
#[allow(dead_code)] |
|
struct Locale { |
|
locale: Box<[Box<str>]>, |
|
}
|
|
|