libservo: Allow running more than one Servo test in a run (#36532)

A `Servo` instance can only be constructed once per program execution
and cannot be passed between threads. This change adds a special thread
to run `Servo` unit tests. This will allow creating suites of `WebView`
unit tests.

Testing: This change includes a new test.

Signed-off-by: Martin Robinson <mrobinson@igalia.com>
This commit is contained in:
Martin Robinson 2025-04-16 09:54:54 +02:00 committed by GitHub
parent 9aa09d73b5
commit 6bad65a5a1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 92 additions and 14 deletions

1
Cargo.lock generated
View file

@ -4306,6 +4306,7 @@ dependencies = [
name = "libservo" name = "libservo"
version = "0.0.1" version = "0.0.1"
dependencies = [ dependencies = [
"anyhow",
"arboard", "arboard",
"background_hang_monitor", "background_hang_monitor",
"base", "base",

View file

@ -124,6 +124,7 @@ gaol = "0.2.1"
webxr = { path = "../webxr", features = ["ipc", "glwindow", "headless", "openxr-api"] } webxr = { path = "../webxr", features = ["ipc", "glwindow", "headless", "openxr-api"] }
[dev-dependencies] [dev-dependencies]
anyhow = "1.0.97"
http = { workspace = true } http = { workspace = true }
libservo = { path = ".", features = ["tracing"] } libservo = { path = ".", features = ["tracing"] }
rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs"] } rustls = { version = "0.23", default-features = false, features = ["aws-lc-rs"] }

View file

@ -3,23 +3,35 @@
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
use std::rc::Rc; use std::rc::Rc;
use std::sync::Arc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, OnceLock};
use std::time::Duration; use std::time::Duration;
use anyhow::Error;
use compositing::windowing::EmbedderMethods; use compositing::windowing::EmbedderMethods;
use compositing_traits::rendering_context::{RenderingContext, SoftwareRenderingContext}; use compositing_traits::rendering_context::{RenderingContext, SoftwareRenderingContext};
use crossbeam_channel::{Receiver, Sender, unbounded};
use dpi::PhysicalSize; use dpi::PhysicalSize;
use embedder_traits::EventLoopWaker; use embedder_traits::EventLoopWaker;
use euclid::Scale; use parking_lot::Mutex;
use servo::Servo; use servo::Servo;
pub struct ServoTest { pub struct ServoTest {
servo: Servo, servo: Servo,
} }
impl Drop for ServoTest {
fn drop(&mut self) {
self.servo.start_shutting_down();
while self.servo.spin_event_loop() {
std::thread::sleep(Duration::from_millis(1));
}
self.servo.deinit();
}
}
impl ServoTest { impl ServoTest {
pub fn new() -> Self { fn new() -> Self {
let rendering_context = Rc::new( let rendering_context = Rc::new(
SoftwareRenderingContext::new(PhysicalSize { SoftwareRenderingContext::new(PhysicalSize {
width: 500, width: 500,
@ -63,14 +75,59 @@ impl ServoTest {
pub fn servo(&self) -> &Servo { pub fn servo(&self) -> &Servo {
&self.servo &self.servo
} }
}
impl Drop for ServoTest { /// Run a Servo test. All tests are run in a `ServoTestThread` and serially. Currently
fn drop(&mut self) { /// Servo does not support launching concurrent instances, in order to ensure
self.servo.start_shutting_down(); /// isolation and allow for more than a single test per instance.
while self.servo.spin_event_loop() { pub fn run(
std::thread::sleep(Duration::from_millis(1)); test_function: impl FnOnce(&ServoTest) -> Result<(), anyhow::Error> + Send + Sync + 'static,
} ) {
self.servo.deinit(); static SERVO_TEST_THREAD: Mutex<OnceLock<ServoTestThread>> = Mutex::new(OnceLock::new());
let test_thread = SERVO_TEST_THREAD.lock();
test_thread
.get_or_init(ServoTestThread::new)
.run_test(Box::new(test_function));
}
}
type TestFunction =
Box<dyn FnOnce(&ServoTest) -> Result<(), anyhow::Error> + Send + Sync + 'static>;
struct ServoTestThread {
test_function_sender: Sender<TestFunction>,
result_receiver: Receiver<Result<(), Error>>,
}
impl ServoTestThread {
fn new() -> Self {
let (result_sender, result_receiver) = unbounded();
let (test_function_sender, test_function_receiver) = unbounded();
// Defined here rather than at the end of this method in order to take advantage
// of Rust type inference.
let thread = Self {
test_function_sender,
result_receiver,
};
let _ = std::thread::spawn(move || {
let servo_test = ServoTest::new();
while let Ok(incoming_test_function) = test_function_receiver.recv() {
let _ = result_sender.send(incoming_test_function(&servo_test));
}
});
thread
}
fn run_test(&self, test_function: TestFunction) {
let _ = self.test_function_sender.send(Box::new(test_function));
let result = self
.result_receiver
.recv()
.expect("Servo test thread should always return a result.");
if let Err(result) = result {
unreachable!("{result}");
}
} }
} }

View file

@ -2,12 +2,31 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this * License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at https://mozilla.org/MPL/2.0/. */ * file, You can obtain one at https://mozilla.org/MPL/2.0/. */
//! Servo API unit tests.
//!
//! Since all Servo tests must rust serially on the same thread, it is important
//! that tests never panic. In order to ensure this, use `anyhow::ensure!` instead
//! of `assert!` for test assertions. `ensure!` will produce a `Result::Err` in
//! place of panicking.
mod common; mod common;
use anyhow::ensure;
use common::*; use common::*;
use servo::WebViewBuilder;
#[test] #[test]
fn test_simple_servo_start_and_stop() { fn test_simple_servo_is_not_animating_by_default() {
let shared_test = ServoTest::new(); ServoTest::run(|servo_test| {
assert!(!shared_test.servo().animating()); ensure!(!servo_test.servo().animating());
Ok(())
});
}
#[test]
fn test_simple_servo_construct_webview() {
ServoTest::run(|servo_test| {
WebViewBuilder::new(servo_test.servo()).build();
Ok(())
});
} }