Skip to content

Commit 2881319

Browse files
committed
fix(runtime): fix timer IDs, enable WPT timers, and resolve clippy issues
Signed-off-by: iammdzaidalam <161572905+iammdzaidalam@users.noreply.github.com>
1 parent ea51866 commit 2881319

7 files changed

Lines changed: 65 additions & 109 deletions

File tree

core/engine/src/builtins/iterable/tests.rs

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -447,6 +447,7 @@ fn iterator_concat_return_result_shape() {
447447
}
448448

449449
#[test]
450+
#[cfg(feature = "experimental")]
450451
fn iterator_includes_basic() {
451452
run_test_actions([
452453
TestAction::run("const gen = () => Iterator.from([1, 3]);"),
@@ -463,6 +464,7 @@ fn iterator_includes_basic() {
463464
}
464465

465466
#[test]
467+
#[cfg(feature = "experimental")]
466468
fn iterator_includes_generator() {
467469
run_test_actions([
468470
TestAction::run("function* gen() { yield 1; yield 3; }"),
@@ -479,6 +481,7 @@ fn iterator_includes_generator() {
479481
}
480482

481483
#[test]
484+
#[cfg(feature = "experimental")]
482485
fn iterator_includes_errors() {
483486
run_test_actions([
484487
TestAction::run("const gen = () => Iterator.from([1, 3]);"),

core/engine/src/context/mod.rs

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -500,6 +500,12 @@ impl Context {
500500
self.job_executor().run_jobs(self)
501501
}
502502

503+
/// Clears all queued jobs from the job executor.
504+
#[inline]
505+
pub fn clear_jobs(&mut self) {
506+
self.job_executor().clear_jobs();
507+
}
508+
503509
/// Abstract operation [`ClearKeptObjects`][clear].
504510
///
505511
/// Clears all objects maintained alive by calls to the [`AddToKeptObjects`][add] abstract

core/engine/src/job.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -815,6 +815,9 @@ pub trait JobExecutor: Any {
815815
{
816816
self.run_jobs(&mut context.borrow_mut())
817817
}
818+
819+
/// Clears all queued jobs.
820+
fn clear_jobs(&self) {}
818821
}
819822

820823
/// A job executor that does nothing.
@@ -946,6 +949,11 @@ impl JobExecutor for SimpleJobExecutor {
946949
future::block_on(self.run_jobs_async(&RefCell::new(context)))
947950
}
948951

952+
fn clear_jobs(&self) {
953+
self.clear();
954+
self.stop.store(true, Ordering::Release);
955+
}
956+
949957
async fn run_jobs_async(self: Rc<Self>, context: &RefCell<&mut Context>) -> JsResult<()>
950958
where
951959
Self: Sized,

core/runtime/src/interval.rs

Lines changed: 24 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ use boa_engine::interop::JsRest;
55
use boa_engine::job::{CancellationToken, IntervalJob, NativeJobFn};
66
use boa_engine::job::{NativeJob, TimeoutJob};
77
use boa_engine::object::builtins::JsFunction;
8-
use boa_engine::value::{IntegerOrInfinity, Nullable};
8+
99
use boa_engine::{Context, IntoJsFunctionCopied, JsResult, JsValue, js_error, js_string};
1010
use std::collections::HashMap;
1111
use std::num::NonZeroU32;
@@ -54,8 +54,9 @@ impl IntervalInnerState {
5454
}
5555

5656
/// Delete an interval ID from the active map.
57-
fn clear_interval(&mut self, id: NonZeroU32) -> Option<CancellationToken> {
57+
fn clear_interval(&mut self, id: u32) -> Option<CancellationToken> {
5858
self.active_map.retain(|_, v| !v.revoked());
59+
let id = NonZeroU32::new(id)?;
5960
self.active_map.remove(&id)
6061
}
6162
}
@@ -78,13 +79,10 @@ pub fn set_timeout(
7879
return Ok(0);
7980
};
8081

81-
// Spec says if delay is not a number, it should be equal to 0.
82-
let delay = delay_in_msec
83-
.unwrap_or_default()
84-
.to_integer_or_infinity(context)
85-
.unwrap_or(IntegerOrInfinity::Integer(0));
86-
// The spec converts the delay to a 32-bit signed integer.
87-
let delay = u64::from(delay.clamp_finite(0, u32::MAX));
82+
// The spec converts the delay to a WebIDL `long`, which maps to `i32`.
83+
// Negative values are clamped to 0.
84+
let delay_i32 = delay_in_msec.unwrap_or_default().to_i32(context)?;
85+
let delay = u64::from(u32::try_from(delay_i32).unwrap_or(0));
8886

8987
let state = IntervalInnerState::from_context(context);
9088
let id = state.next_id()?;
@@ -133,12 +131,10 @@ pub fn set_interval(
133131
return Ok(0);
134132
};
135133

136-
// Spec says if delay is not a number, it should be equal to 0.
137-
let delay = delay_in_msec
138-
.unwrap_or_default()
139-
.to_integer_or_infinity(context)
140-
.unwrap_or(IntegerOrInfinity::Integer(0));
141-
let delay = u64::from(delay.clamp_finite(0, u32::MAX));
134+
// The spec converts the delay to a WebIDL `long`, which maps to `i32`.
135+
// Negative values are clamped to 0.
136+
let delay_i32 = delay_in_msec.unwrap_or_default().to_i32(context)?;
137+
let delay = u64::from(u32::try_from(delay_i32).unwrap_or(0));
142138

143139
let state = IntervalInnerState::from_context(context);
144140
let id = state.next_id()?;
@@ -169,18 +165,20 @@ pub fn set_interval(
169165
/// See [MDN](https://developer.mozilla.org/en-US/docs/Web/API/Window/clearTimeout).
170166
///
171167
/// Please note that this is the same exact method as `clearInterval`, as both can be
172-
/// used interchangeably.
173-
pub fn clear_timeout(id: Nullable<Option<u32>>, context: &mut Context) {
174-
let Some(id) = id.flatten() else {
175-
return;
176-
};
177-
let Some(id) = NonZeroU32::new(id) else {
178-
return;
179-
};
180-
let handler_map = IntervalInnerState::from_context(context);
181-
if let Some(token) = handler_map.clear_interval(id) {
182-
token.cancel(context);
168+
/// used interchangeably. Invalid, zero, or negative IDs are silently ignored,
169+
/// matching browser behavior.
170+
///
171+
/// # Errors
172+
/// Returns an error if the `id` argument cannot be converted to an `i32`.
173+
pub fn clear_timeout(id: Option<JsValue>, context: &mut Context) -> JsResult<JsValue> {
174+
let id = id.unwrap_or_default().to_i32(context)?;
175+
if id > 0 {
176+
let handler_map = IntervalInnerState::from_context(context);
177+
if let Some(token) = handler_map.clear_interval(id.cast_unsigned()) {
178+
token.cancel(context);
179+
}
183180
}
181+
Ok(JsValue::undefined())
184182
}
185183

186184
/// Register the interval module into the given context.

core/runtime/src/interval/tests.rs

Lines changed: 0 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -261,64 +261,3 @@ fn set_interval_delay() {
261261
context,
262262
);
263263
}
264-
265-
#[test]
266-
fn timer_ids_are_positive_and_unique() {
267-
let clock = Rc::new(FixedClock::default());
268-
let context = &mut create_context(clock);
269-
270-
run_test_actions_with(
271-
[
272-
TestAction::run(indoc! {r#"
273-
id1 = setTimeout(() => {}, 0);
274-
id2 = setInterval(() => {}, 100);
275-
id3 = setTimeout(() => {}, 0);
276-
"#}),
277-
TestAction::inspect_context(|ctx| {
278-
let id1 = ctx.global_object().get(js_str!("id1"), ctx).unwrap();
279-
let id2 = ctx.global_object().get(js_str!("id2"), ctx).unwrap();
280-
let id3 = ctx.global_object().get(js_str!("id3"), ctx).unwrap();
281-
282-
let id1 = id1.as_i32().expect("id1 should be an integer");
283-
let id2 = id2.as_i32().expect("id2 should be an integer");
284-
let id3 = id3.as_i32().expect("id3 should be an integer");
285-
286-
assert!(id1 > 0, "setTimeout must return ID > 0, got {id1}");
287-
assert!(id2 > 0, "setInterval must return ID > 0, got {id2}");
288-
assert!(id3 > 0, "subsequent timer ID must be > 0, got {id3}");
289-
290-
assert_ne!(id1, id2, "timer IDs must be unique");
291-
assert_ne!(id2, id3, "timer IDs must be unique");
292-
assert_ne!(id1, id3, "timer IDs must be unique");
293-
}),
294-
],
295-
context,
296-
);
297-
}
298-
299-
#[test]
300-
fn no_callback_returns_zero_sentinel() {
301-
let clock = Rc::new(FixedClock::default());
302-
let context = &mut create_context(clock);
303-
304-
run_test_actions_with(
305-
[
306-
TestAction::run(indoc! {r#"
307-
id_valid = setTimeout(() => {}, 0);
308-
id_no_cb = setTimeout();
309-
"#}),
310-
TestAction::inspect_context(|ctx| {
311-
let id_valid = ctx.global_object().get(js_str!("id_valid"), ctx).unwrap();
312-
let id_no_cb = ctx.global_object().get(js_str!("id_no_cb"), ctx).unwrap();
313-
314-
assert_eq!(id_no_cb.as_i32(), Some(0), "no-callback sentinel must be 0");
315-
assert_ne!(
316-
id_valid.as_i32(),
317-
Some(0),
318-
"valid timer ID must not collide with sentinel"
319-
);
320-
}),
321-
],
322-
context,
323-
);
324-
}

tests/wpt/Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ boa_engine = { path = "../../core/engine" }
99
boa_gc = { path = "../../core/gc" }
1010
boa_runtime = { path = "../../core/runtime", features = ["all"] }
1111
rstest = "0.25.0"
12+
rustls = { version = "0.23", default-features = false, features = ["ring"] }
1213
url = { version = "2.5.4", features = [] }
1314

1415
[build-dependencies]

tests/wpt/src/lib.rs

Lines changed: 23 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -118,31 +118,19 @@ impl TestSuiteSource {
118118

119119
fn scripts(&self) -> Result<Vec<String>, Box<dyn std::error::Error>> {
120120
let mut scripts: Vec<String> = Vec::new();
121-
let dir = self
122-
.path
123-
.parent()
124-
.expect("Could not get the parent directory");
125121

126122
'outer: for script in self.meta()?.get("script").unwrap_or(&Vec::new()) {
127123
let script = script
128124
.split_once('?')
129125
.map_or(script.to_string(), |(s, _)| s.to_string());
130126

131-
// Resolve the source path relative to the script path, but under the wpt_path.
132-
let script_path = Path::new(&script);
133-
let path = if script_path.is_relative() {
134-
dir.join(script_path)
135-
} else {
136-
script_path.to_path_buf()
137-
};
138-
139127
for (from, to) in REWRITE_RULES {
140-
if path.to_string_lossy().as_ref() == *from {
128+
if script == *from {
141129
scripts.push((*to).to_string());
142130
continue 'outer;
143131
}
144132
}
145-
scripts.push(path.to_string_lossy().to_string());
133+
scripts.push(script);
146134
}
147135
Ok(scripts)
148136
}
@@ -259,8 +247,9 @@ fn result_callback__(
259247
}
260248

261249
#[track_caller]
262-
fn complete_callback__(ContextData(test_done): ContextData<TestCompletion>) {
250+
fn complete_callback__(ContextData(test_done): ContextData<TestCompletion>, context: &mut Context) {
263251
test_done.done();
252+
context.clear_jobs();
264253
}
265254

266255
#[derive(Debug, Clone, Trace, Finalize, JsData)]
@@ -285,6 +274,7 @@ impl TestCompletion {
285274
// in clippy.
286275
#[allow(unused)]
287276
fn execute_test_file(path: &Path) {
277+
rustls::crypto::ring::default_provider().install_default().ok();
288278
let dir = path.parent().unwrap();
289279
let wpt_path = PathBuf::from(
290280
std::env::var("WPT_ROOT").expect("Could not find the WPT_ROOT environment variable"),
@@ -333,13 +323,10 @@ fn execute_test_file(path: &Path) {
333323
let source = TestSuiteSource::new(path);
334324
for script in source.scripts().expect("Could not get scripts") {
335325
// Resolve the source path relative to the script path, but under the wpt_path.
336-
let script_path = Path::new(&script);
337-
let path = if script_path.is_relative() {
338-
dir.join(script_path)
339-
} else if script_path.starts_with(&wpt_path) {
340-
script_path.to_path_buf()
326+
let path = if script.starts_with('/') {
327+
wpt_path.join(script.strip_prefix('/').unwrap())
341328
} else {
342-
wpt_path.join(script_path.strip_prefix("/").unwrap())
329+
dir.join(&script)
343330
};
344331

345332
let path = path.canonicalize().expect("Could not canonicalize path");
@@ -398,8 +385,8 @@ fn console(
398385
fn encoding(
399386
#[base_dir = "${WPT_ROOT}"]
400387
#[files("encoding/api-*.any.js")]
401-
#[files("encoding/textencoder-constructor-non-utf.any.js")]
402388
// TODO: re-enable those when better encoding and options are supported.
389+
// #[files("encoding/textencoder-constructor-non-utf.any.js")]
403390
// #[files("encoding/textdecoder-*.any.js")]
404391
// #[files("encoding/textencoder-*.any.js")]
405392
#[exclude("idlharness")]
@@ -440,3 +427,17 @@ fn fetch(
440427
) {
441428
execute_test_file(&path);
442429
}
430+
431+
/// Test the timers with the WPT test suite.
432+
#[cfg(not(clippy))]
433+
#[rstest::rstest]
434+
fn timers(
435+
#[base_dir = "${WPT_ROOT}"]
436+
#[files("html/webappapis/timers/*.any.js")]
437+
#[exclude("idlharness")]
438+
// String-eval form of setTimeout is not implemented in boa_runtime.
439+
#[exclude("evil-spec-example")]
440+
path: PathBuf,
441+
) {
442+
execute_test_file(&path);
443+
}

0 commit comments

Comments
 (0)