Browse Source

Add tests from WPT and fix them in the Console (#3979)

pull/3995/head
Hans Larsen 3 months ago committed by GitHub
parent
commit
dd32789ae4
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 33
      core/engine/src/value/mod.rs
  2. 258
      core/runtime/src/console/mod.rs
  3. 229
      core/runtime/src/console/tests.rs

33
core/engine/src/value/mod.rs

@ -21,7 +21,14 @@ use boa_profiler::Profiler;
#[doc(inline)]
pub use conversions::convert::Convert;
use crate::object::{JsFunction, JsPromise};
pub(crate) use self::conversions::IntoOrUndefined;
#[doc(inline)]
pub use self::{
conversions::try_from_js::TryFromJs, display::ValueDisplay, integer::IntegerOrInfinity,
operations::*, r#type::Type,
};
use crate::builtins::RegExp;
use crate::object::{JsFunction, JsPromise, JsRegExp};
use crate::{
builtins::{
number::{f64_to_int32, f64_to_uint32},
@ -35,13 +42,6 @@ use crate::{
Context, JsBigInt, JsResult, JsString,
};
pub(crate) use self::conversions::IntoOrUndefined;
#[doc(inline)]
pub use self::{
conversions::try_from_js::TryFromJs, display::ValueDisplay, integer::IntegerOrInfinity,
operations::*, r#type::Type,
};
mod conversions;
pub(crate) mod display;
mod equality;
@ -221,6 +221,23 @@ impl JsValue {
.and_then(|o| JsPromise::from_object(o).ok())
}
/// Returns true if the value is a regular expression object.
#[inline]
#[must_use]
pub fn is_regexp(&self) -> bool {
matches!(self, Self::Object(obj) if obj.is::<RegExp>())
}
/// Returns the value as a regular expression if the value is a regexp, otherwise `None`.
#[inline]
#[must_use]
pub fn as_regexp(&self) -> Option<JsRegExp> {
self.as_object()
.filter(|obj| obj.is::<RegExp>())
.cloned()
.and_then(|o| JsRegExp::from_object(o).ok())
}
/// Returns true if the value is a symbol.
#[inline]
#[must_use]

258
core/runtime/src/console/mod.rs

@ -14,12 +14,13 @@
#[cfg(test)]
mod tests;
use boa_engine::property::Attribute;
use boa_engine::{
js_str, js_string,
native_function::NativeFunction,
object::{JsObject, ObjectInitializer},
value::{JsValue, Numeric},
Context, JsArgs, JsData, JsError, JsResult, JsStr, JsString,
Context, JsArgs, JsData, JsError, JsResult, JsStr, JsString, JsSymbol,
};
use boa_gc::{Finalize, Trace};
use rustc_hash::FxHashMap;
@ -86,9 +87,19 @@ impl Logger for DefaultLogger {
/// This represents the `console` formatter.
fn formatter(data: &[JsValue], context: &mut Context) -> JsResult<String> {
fn to_string(value: &JsValue, context: &mut Context) -> JsResult<String> {
// There is a slight difference between the standard [`JsValue::to_string`] and
// the way Console actually logs, w.r.t Symbols.
if let Some(s) = value.as_symbol() {
Ok(s.to_string())
} else {
Ok(value.to_string(context)?.to_std_string_escaped())
}
}
match data {
[] => Ok(String::new()),
[val] => Ok(val.to_string(context)?.to_std_string_escaped()),
[val] => to_string(val, context),
data => {
let mut formatted = String::new();
let mut arg_index = 1;
@ -124,11 +135,27 @@ fn formatter(data: &[JsValue], context: &mut Context) -> JsResult<String> {
}
/* string */
's' => {
let arg = data
.get_or_undefined(arg_index)
.to_string(context)?
.to_std_string_escaped();
formatted.push_str(&arg);
let arg = data.get_or_undefined(arg_index);
// If a JS value implements `toString()`, call it.
let mut written = false;
if let Some(obj) = arg.as_object() {
if let Ok(to_string) = obj.get(js_str!("toString"), context) {
if let Some(to_string_fn) = to_string.as_function() {
let arg = to_string_fn
.call(arg, &[], context)?
.to_string(context)?;
formatted.push_str(&arg.to_std_string_escaped());
written = true;
}
}
}
if !written {
let arg = arg.to_string(context)?.to_std_string_escaped();
formatted.push_str(&arg);
}
arg_index += 1;
}
'%' => formatted.push('%'),
@ -145,10 +172,8 @@ fn formatter(data: &[JsValue], context: &mut Context) -> JsResult<String> {
/* unformatted data */
for rest in data.iter().skip(arg_index) {
formatted.push_str(&format!(
" {}",
rest.to_string(context)?.to_std_string_escaped()
));
formatted.push(' ');
formatted.push_str(&to_string(rest, context)?);
}
Ok(formatted)
@ -175,6 +200,24 @@ impl Console {
/// Name of the built-in `console` property.
pub const NAME: JsStr<'static> = js_str!("console");
/// Modify the context to include the `console` object.
///
/// # Errors
/// This function will return an error if the property cannot be defined on the global object.
pub fn register_with_logger<L>(context: &mut Context, logger: L) -> JsResult<()>
where
L: Logger + 'static,
{
let console = Self::init_with_logger(context, logger);
context.register_global_property(
Self::NAME,
console,
Attribute::WRITABLE | Attribute::CONFIGURABLE,
)?;
Ok(())
}
/// Initializes the `console` with a special logger.
#[allow(clippy::too_many_lines)]
pub fn init_with_logger<L>(context: &mut Context, logger: L) -> JsObject
@ -210,98 +253,107 @@ impl Console {
let state = Rc::new(RefCell::new(Self::default()));
let logger = Rc::new(logger);
ObjectInitializer::with_native_data(Self::default(), context)
.function(
console_method(Self::assert, state.clone(), logger.clone()),
js_string!("assert"),
0,
)
.function(
console_method_mut(Self::clear, state.clone(), logger.clone()),
js_string!("clear"),
0,
)
.function(
console_method(Self::debug, state.clone(), logger.clone()),
js_string!("debug"),
0,
)
.function(
console_method(Self::error, state.clone(), logger.clone()),
js_string!("error"),
0,
)
.function(
console_method(Self::info, state.clone(), logger.clone()),
js_string!("info"),
0,
)
.function(
console_method(Self::log, state.clone(), logger.clone()),
js_string!("log"),
0,
)
.function(
console_method(Self::trace, state.clone(), logger.clone()),
js_string!("trace"),
0,
)
.function(
console_method(Self::warn, state.clone(), logger.clone()),
js_string!("warn"),
0,
)
.function(
console_method_mut(Self::count, state.clone(), logger.clone()),
js_string!("count"),
0,
)
.function(
console_method_mut(Self::count_reset, state.clone(), logger.clone()),
js_string!("countReset"),
0,
)
.function(
console_method_mut(Self::group, state.clone(), logger.clone()),
js_string!("group"),
0,
)
.function(
console_method_mut(Self::group_collapsed, state.clone(), logger.clone()),
js_string!("groupCollapsed"),
0,
)
.function(
console_method_mut(Self::group_end, state.clone(), logger.clone()),
js_string!("groupEnd"),
0,
)
.function(
console_method_mut(Self::time, state.clone(), logger.clone()),
js_string!("time"),
0,
)
.function(
console_method(Self::time_log, state.clone(), logger.clone()),
js_string!("timeLog"),
0,
)
.function(
console_method_mut(Self::time_end, state.clone(), logger.clone()),
js_string!("timeEnd"),
0,
)
.function(
console_method(Self::dir, state.clone(), logger.clone()),
js_string!("dir"),
0,
)
.function(
console_method(Self::dir, state, logger.clone()),
js_string!("dirxml"),
0,
)
.build()
ObjectInitializer::with_native_data_and_proto(
Self::default(),
JsObject::with_object_proto(context.realm().intrinsics()),
context,
)
.property(
JsSymbol::to_string_tag(),
Self::NAME,
Attribute::CONFIGURABLE,
)
.function(
console_method(Self::assert, state.clone(), logger.clone()),
js_string!("assert"),
0,
)
.function(
console_method_mut(Self::clear, state.clone(), logger.clone()),
js_string!("clear"),
0,
)
.function(
console_method(Self::debug, state.clone(), logger.clone()),
js_string!("debug"),
0,
)
.function(
console_method(Self::error, state.clone(), logger.clone()),
js_string!("error"),
0,
)
.function(
console_method(Self::info, state.clone(), logger.clone()),
js_string!("info"),
0,
)
.function(
console_method(Self::log, state.clone(), logger.clone()),
js_string!("log"),
0,
)
.function(
console_method(Self::trace, state.clone(), logger.clone()),
js_string!("trace"),
0,
)
.function(
console_method(Self::warn, state.clone(), logger.clone()),
js_string!("warn"),
0,
)
.function(
console_method_mut(Self::count, state.clone(), logger.clone()),
js_string!("count"),
0,
)
.function(
console_method_mut(Self::count_reset, state.clone(), logger.clone()),
js_string!("countReset"),
0,
)
.function(
console_method_mut(Self::group, state.clone(), logger.clone()),
js_string!("group"),
0,
)
.function(
console_method_mut(Self::group_collapsed, state.clone(), logger.clone()),
js_string!("groupCollapsed"),
0,
)
.function(
console_method_mut(Self::group_end, state.clone(), logger.clone()),
js_string!("groupEnd"),
0,
)
.function(
console_method_mut(Self::time, state.clone(), logger.clone()),
js_string!("time"),
0,
)
.function(
console_method(Self::time_log, state.clone(), logger.clone()),
js_string!("timeLog"),
0,
)
.function(
console_method_mut(Self::time_end, state.clone(), logger.clone()),
js_string!("timeEnd"),
0,
)
.function(
console_method(Self::dir, state.clone(), logger.clone()),
js_string!("dir"),
0,
)
.function(
console_method(Self::dir, state, logger.clone()),
js_string!("dirxml"),
0,
)
.build()
}
/// Initializes the `console` built-in object.

229
core/runtime/src/console/tests.rs

@ -1,6 +1,8 @@
use super::{formatter, Console};
use crate::test::{run_test_actions, run_test_actions_with, TestAction};
use boa_engine::{js_string, property::Attribute, Context, JsValue};
use crate::Logger;
use boa_engine::{js_string, property::Attribute, Context, JsError, JsResult, JsValue};
use boa_gc::{Gc, GcRefCell};
use indoc::indoc;
#[test]
@ -110,3 +112,228 @@ fn console_log_cyclic() {
);
// Should not stack overflow
}
/// A logger that records all log messages.
#[derive(Clone, Debug, Default, boa_engine::Trace, boa_engine::Finalize)]
struct RecordingLogger {
log: Gc<GcRefCell<String>>,
}
impl Logger for RecordingLogger {
fn log(&self, msg: String, state: &Console) -> JsResult<()> {
use std::fmt::Write;
let indent = 2 * state.groups.len();
writeln!(self.log.borrow_mut(), "{msg:>indent$}").map_err(JsError::from_rust)
}
fn info(&self, msg: String, state: &Console) -> JsResult<()> {
self.log(msg, state)
}
fn warn(&self, msg: String, state: &Console) -> JsResult<()> {
self.log(msg, state)
}
fn error(&self, msg: String, state: &Console) -> JsResult<()> {
self.log(msg, state)
}
}
/// Harness methods to be used in JS tests.
const TEST_HARNESS: &str = r#"
function assert_true(condition, message) {
if (!condition) {
throw new Error(`Assertion failed: ${message}`);
}
}
function assert_own_property(obj, prop) {
assert_true(
Object.prototype.hasOwnProperty.call(obj, prop),
`Expected ${prop.toString()} to be an own property`,
);
}
function assert_equals(actual, expected, message) {
assert_true(
actual === expected,
`${message} (actual: ${actual.toString()}, expected: ${expected.toString()})`,
);
}
function assert_throws_js(error, func) {
try {
func();
} catch (e) {
if (e instanceof error) {
return;
}
throw new Error(`Expected ${error.name} to be thrown, but got ${e.name}`);
}
throw new Error(`Expected ${error.name} to be thrown, but no exception was thrown`);
}
// To keep the tests as close to the WPT tests as possible, we define `self` to
// be `globalThis`.
const self = globalThis;
"#;
/// The WPT test `console/console-log-symbol.any.js`.
#[test]
fn wpt_log_symbol_any() {
let mut context = Context::default();
let logger = RecordingLogger::default();
Console::register_with_logger(&mut context, logger.clone()).unwrap();
run_test_actions_with(
[
TestAction::run(TEST_HARNESS),
TestAction::run(indoc! {r#"
console.log(Symbol());
console.log(Symbol("abc"));
console.log(Symbol.for("def"));
console.log(Symbol.isConcatSpreadable);
"#}),
],
&mut context,
);
let logs = logger.log.borrow().clone();
assert_eq!(
logs,
indoc! { r#"
Symbol()
Symbol(abc)
Symbol(def)
Symbol(Symbol.isConcatSpreadable)
"# }
);
}
/// The WPT test `console/console-is-a-namespace.any.js`.
#[test]
fn wpt_console_is_a_namespace() {
let mut context = Context::default();
let logger = RecordingLogger::default();
Console::register_with_logger(&mut context, logger.clone()).unwrap();
run_test_actions_with(
[
TestAction::run(TEST_HARNESS),
// console exists on the global object
TestAction::run(indoc! {r#"
assert_true(globalThis.hasOwnProperty("console"));
"#}),
// console has the right property descriptors
TestAction::run(indoc! {r#"
const propDesc = Object.getOwnPropertyDescriptor(self, "console");
assert_equals(propDesc.writable, true, "must be writable");
assert_equals(propDesc.enumerable, false, "must not be enumerable");
assert_equals(propDesc.configurable, true, "must be configurable");
assert_equals(propDesc.value, console, "must have the right value");
"#}),
// The prototype chain must be correct
TestAction::run(indoc! {r#"
const prototype1 = Object.getPrototypeOf(console);
const prototype2 = Object.getPrototypeOf(prototype1);
assert_equals(Object.getOwnPropertyNames(prototype1).length, 0, "The [[Prototype]] must have no properties");
assert_equals(prototype2, Object.prototype, "The [[Prototype]]'s [[Prototype]] must be %ObjectPrototype%");
"#}),
],
&mut context,
);
}
/// The WPT test `console/console-label-conversion.any.js`.
#[test]
fn wpt_console_label_conversion() {
let mut context = Context::default();
let logger = RecordingLogger::default();
Console::register_with_logger(&mut context, logger.clone()).unwrap();
run_test_actions_with(
[
TestAction::run(TEST_HARNESS),
TestAction::run(indoc! {r#"
const methods = ['count', 'countReset', 'time', 'timeLog', 'timeEnd'];
"#}),
// console.${method}()'s label gets converted to string via label.toString() when label is an object
TestAction::run(indoc! {r#"
for (const method of methods) {
let labelToStringCalled = false;
console[method]({
toString() {
labelToStringCalled = true;
}
});
assert_true(labelToStringCalled, `${method}() must call toString() on label when label is an object`);
}
"#}),
// ${method} must re-throw any exceptions thrown by label.toString() conversion
TestAction::run(indoc! {r#"
for (const method of methods) {
assert_throws_js(Error, () => {
console[method]({
toString() {
throw new Error('conversion error');
}
});
});
}
"#}),
],
&mut context,
);
}
/// The WPT test `console/console-namespace-object-class-string.any.js`.
#[test]
fn console_namespace_object_class_string() {
let mut context = Context::default();
let logger = RecordingLogger::default();
Console::register_with_logger(&mut context, logger.clone()).unwrap();
run_test_actions_with(
[
TestAction::run(TEST_HARNESS),
// @@toStringTag exists on the namespace object with the appropriate descriptor
TestAction::run(indoc! {r#"
assert_own_property(console, Symbol.toStringTag);
const propDesc = Object.getOwnPropertyDescriptor(console, Symbol.toStringTag);
assert_equals(propDesc.value, "console", "value");
assert_equals(propDesc.writable, false, "writable");
assert_equals(propDesc.enumerable, false, "enumerable");
assert_equals(propDesc.configurable, true, "configurable");
"#}),
// Object.prototype.toString applied to the namespace object
TestAction::run(indoc! {r#"
assert_equals(console.toString(), "[object console]");
assert_equals(Object.prototype.toString.call(console), "[object console]");
"#}),
// Object.prototype.toString applied after modifying the namespace object's @@toStringTag
TestAction::run(indoc! {r#"
assert_own_property(console, Symbol.toStringTag, "Precondition: @@toStringTag on the namespace object");
// t.add_cleanup(() => {
// Object.defineProperty(console, Symbol.toStringTag, { value: "console" });
// });
Object.defineProperty(console, Symbol.toStringTag, { value: "Test" });
assert_equals(console.toString(), "[object Test]");
assert_equals(Object.prototype.toString.call(console), "[object Test]");
"#}),
// Object.prototype.toString applied after deleting @@toStringTag
TestAction::run(indoc! {r#"
assert_own_property(console, Symbol.toStringTag, "Precondition: @@toStringTag on the namespace object");
// t.add_cleanup(() => {
// Object.defineProperty(console, Symbol.toStringTag, { value: "console" });
// });
assert_true(delete console[Symbol.toStringTag]);
assert_equals(console.toString(), "[object Object]");
assert_equals(Object.prototype.toString.call(console), "[object Object]");
"#}),
],
&mut context,
);
}

Loading…
Cancel
Save