Files
servo/components/storage/tests/webstorage.rs
Jonathan Schwender f4877c190e Embed default resources in Servo applications using a servo-default-resources crate (#43182)
This PR considers the following constraints:

- Resources must be available when building servo via a published
crates.io package (i.e. no `../../../resources/<file>` file references).
- Minimal setup when writing tests (`nextest` spawns each test in its
own process, so we don't want to explicitly initialize the resource
handler for every `#[test]` fn)
- Use local resources when developing locally
- Support loading the resources from a proper resource directory if the
embedder wishes so, including via a custom mechanism, not necessarily as
files

(File) Resources that are only accessed from servoshell are out of scope
of this PR, since it mainly focusses on unblocking publishing `libservo`
to crates.io.

Baking the resources into the binary by default simplifies the setup a
lot. We already supported that before, but only for testing purposes and
explicitly not for production builds.

Using [`inventory`](https://crates.io/crates/inventory) adds a simple
way for the embedder to replace the default baked in resources, while
also keeping the test usage of baked in resources simple.

rippy.png is also referenced from image_cache - We simply duplicate it,
since the image is small, to avoid adding unnecessarily complex
solutions like adding a dedicated crate.


Testing: Covered by existing tests. [mach try
full](https://github.com/jschwe/servo/actions/runs/23811669469)
Fixes: Part of #43145

---------

Signed-off-by: Jonathan Schwender <schwenderjonathan@gmail.com>
2026-04-01 07:16:28 +00:00

432 lines
13 KiB
Rust

/* This Source Code Form is subject to the terms of the Mozilla Public
* 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/. */
use profile::mem as profile_mem;
use servo_base::generic_channel as base_channel;
use servo_base::generic_channel::GenericSend;
use servo_base::id::TEST_WEBVIEW_ID;
use servo_default_resources as _;
use servo_url::ServoUrl;
use storage_traits::StorageThreads;
use storage_traits::webstorage_thread::{WebStorageThreadMsg, WebStorageType};
use tempfile::TempDir;
pub(crate) struct WebStorageTest {
tmp_dir: Option<TempDir>,
threads: StorageThreads,
}
impl WebStorageTest {
pub(crate) fn new() -> Self {
let tmp_dir = tempfile::tempdir().unwrap();
let config_dir = tmp_dir.path().to_path_buf();
let mem_profiler_chan = profile_mem::Profiler::create();
let threads = storage::new_storage_threads(mem_profiler_chan, Some(config_dir));
Self {
tmp_dir: Some(tmp_dir),
threads: threads.0,
}
}
pub(crate) fn new_in_memory() -> Self {
let mem_profiler_chan = profile_mem::Profiler::create();
let threads = storage::new_storage_threads(mem_profiler_chan, None);
Self {
tmp_dir: None,
threads: threads.0,
}
}
pub(crate) fn restart(mut self) -> Self {
let tmp_dir = self.tmp_dir.take();
let config_dir = tmp_dir.as_ref().map(|d| d.path().to_path_buf());
let mem_profiler_chan = profile_mem::Profiler::create();
let threads = storage::new_storage_threads(mem_profiler_chan, config_dir);
Self {
tmp_dir: tmp_dir,
threads: threads.0,
}
}
pub(crate) fn threads(&self) -> StorageThreads {
self.threads.clone()
}
pub(crate) fn length(&self, storage_type: WebStorageType, url: &ServoUrl) -> usize {
let (sender, receiver) = base_channel::channel().unwrap();
self.threads
.send(WebStorageThreadMsg::Length(
sender,
storage_type,
TEST_WEBVIEW_ID,
url.clone(),
))
.unwrap();
receiver.recv().unwrap()
}
pub(crate) fn key(
&self,
storage_type: WebStorageType,
url: &ServoUrl,
index: u32,
) -> Option<String> {
let (sender, receiver) = base_channel::channel().unwrap();
self.threads
.send(WebStorageThreadMsg::Key(
sender,
storage_type,
TEST_WEBVIEW_ID,
url.clone(),
index,
))
.unwrap();
receiver.recv().unwrap()
}
pub(crate) fn keys(&self, storage_type: WebStorageType, url: &ServoUrl) -> Vec<String> {
let (sender, receiver) = base_channel::channel().unwrap();
self.threads
.send(WebStorageThreadMsg::Keys(
sender,
storage_type,
TEST_WEBVIEW_ID,
url.clone(),
))
.unwrap();
receiver.recv().unwrap()
}
pub(crate) fn get_item(
&self,
storage_type: WebStorageType,
url: &ServoUrl,
key: &str,
) -> Option<String> {
let (sender, receiver) = base_channel::channel().unwrap();
self.threads
.send(WebStorageThreadMsg::GetItem(
sender,
storage_type,
TEST_WEBVIEW_ID,
url.clone(),
key.into(),
))
.unwrap();
receiver.recv().unwrap()
}
pub(crate) fn set_item(
&self,
storage_type: WebStorageType,
url: &ServoUrl,
key: &str,
value: &str,
) -> Result<(bool, Option<String>), ()> {
let (sender, receiver) = base_channel::channel().unwrap();
self.threads
.send(WebStorageThreadMsg::SetItem(
sender,
storage_type,
TEST_WEBVIEW_ID,
url.clone(),
key.into(),
value.into(),
))
.unwrap();
receiver.recv().unwrap()
}
pub(crate) fn remove_item(
&self,
storage_type: WebStorageType,
url: &ServoUrl,
key: &str,
) -> Option<String> {
let (sender, receiver) = base_channel::channel().unwrap();
self.threads
.send(WebStorageThreadMsg::RemoveItem(
sender,
storage_type,
TEST_WEBVIEW_ID,
url.clone(),
key.into(),
))
.unwrap();
receiver.recv().unwrap()
}
pub(crate) fn clear(&self, storage_type: WebStorageType, url: &ServoUrl) -> bool {
let (sender, receiver) = base_channel::channel().unwrap();
self.threads
.send(WebStorageThreadMsg::Clear(
sender,
storage_type,
TEST_WEBVIEW_ID,
url.clone(),
))
.unwrap();
receiver.recv().unwrap()
}
/// Gracefully shut down the webstorage thread to avoid dangling threads in tests.
fn shutdown(&self) {
let (sender, receiver) = base_channel::channel().unwrap();
self.threads
.send(WebStorageThreadMsg::Exit(sender))
.expect("failed to send Exit");
// Wait for acknowledgement so the thread terminates before the test ends.
let _ = receiver.recv();
}
}
impl Drop for WebStorageTest {
fn drop(&mut self) {
self.shutdown();
}
}
#[test]
fn set_and_get_item() {
let test = WebStorageTest::new();
let url = ServoUrl::parse("https://example.com").unwrap();
// Set a value.
let result = test.set_item(WebStorageType::Local, &url, "foo", "bar");
assert_eq!(result, Ok((true, None)));
// Retrieve the value.
let result = test.get_item(WebStorageType::Local, &url, "foo");
assert_eq!(result, Some("bar".into()));
}
#[test]
fn set_and_get_item_in_memory() {
let test = WebStorageTest::new_in_memory();
let url = ServoUrl::parse("https://example.com").unwrap();
// Set a value.
let result = test.set_item(WebStorageType::Local, &url, "foo", "bar");
assert_eq!(result, Ok((true, None)));
// Retrieve the value.
let result = test.get_item(WebStorageType::Local, &url, "foo");
assert_eq!(result, Some("bar".into()));
}
#[test]
fn length_key_and_keys() {
let test = WebStorageTest::new();
let url = ServoUrl::parse("https://example.com").unwrap();
// Insert two items.
for (k, v) in [("foo", "v1"), ("bar", "v2")] {
let _ = test.set_item(WebStorageType::Local, &url, k, v);
}
// Verify length.
let result = test.length(WebStorageType::Local, &url);
assert_eq!(result, 2);
// Verify key(0) returns one of the inserted keys.
let result = test.key(WebStorageType::Local, &url, 0);
let key0 = result.unwrap();
assert!(key0 == "foo" || key0 == "bar");
// Verify keys vector contains both keys.
let result = test.keys(WebStorageType::Local, &url);
assert_eq!(result.len(), 2);
assert!(result.contains(&"foo".to_string()));
assert!(result.contains(&"bar".to_string()));
}
#[test]
fn remove_item_and_clear() {
let test = WebStorageTest::new();
let url = ServoUrl::parse("https://example.com").unwrap();
// Insert items.
for (k, v) in [("foo", "v1"), ("bar", "v2")] {
let _ = test.set_item(WebStorageType::Local, &url, k, v);
}
// Remove one item and verify old value is returned.
let result = test.remove_item(WebStorageType::Local, &url, "foo");
assert_eq!(result, Some("v1".into()));
// Removing again should return None.
let result = test.remove_item(WebStorageType::Local, &url, "foo");
assert_eq!(result, None);
// Clear storage and verify it reported change.
let result = test.clear(WebStorageType::Local, &url);
assert!(result);
// Length should now be zero.
let result = test.length(WebStorageType::Local, &url);
assert_eq!(result, 0);
}
fn test_origin_descriptors(
test: WebStorageTest,
storage_type: WebStorageType,
survives_restart: bool,
) {
let threads = test.threads();
let url = ServoUrl::parse("https://example.com").unwrap();
// Set a value.
let _ = test.set_item(storage_type, &url, "foo", "bar");
// Verify descriptors.
let descriptors = threads.webstorage_origins(storage_type);
assert_eq!(descriptors.len(), 1);
assert_eq!(descriptors[0].name, "https://example.com");
// Restart storage threads.
let test = test.restart();
let threads = test.threads();
// There should still be descriptors.
let descriptors = threads.webstorage_origins(storage_type);
if survives_restart {
assert_eq!(descriptors.len(), 1);
assert_eq!(descriptors[0].name, "https://example.com");
} else {
assert!(descriptors.is_empty());
}
}
#[test]
fn origin_descriptors_session() {
let test = WebStorageTest::new();
test_origin_descriptors(
test,
WebStorageType::Session,
/* survives_restart */ false,
);
}
#[test]
fn origin_descriptors_local() {
let test = WebStorageTest::new();
test_origin_descriptors(
test,
WebStorageType::Local,
/* survives_restart */ true,
);
}
fn test_clear_data_for_sites(test: WebStorageTest, storage_type: WebStorageType) {
let threads = test.threads();
let url = ServoUrl::parse("https://example.com").unwrap();
// Set a value.
let _ = test.set_item(storage_type, &url, "foo", "bar");
// Verify length.
let result = test.length(storage_type, &url);
assert_eq!(result, 1);
// Verify descriptors.
let descriptors = threads.webstorage_origins(storage_type);
assert_eq!(descriptors.len(), 1);
// Clear site.
threads.clear_webstorage_for_sites(storage_type, &["example.com"]);
// Length should now be zero.
let result = test.length(storage_type, &url);
assert_eq!(result, 0);
// There should now be no descriptors.
let descriptors = threads.webstorage_origins(storage_type);
match storage_type {
WebStorageType::Session => assert_eq!(descriptors.len(), 0),
WebStorageType::Local =>
// TODO: Fix localStorage to not create origin descriptors for
// read only operations (the length check above).
{
assert_eq!(descriptors.len(), 1)
},
}
// Restart storage threads.
let test = test.restart();
let threads = test.threads();
// Length should still be zero.
let result = test.length(storage_type, &url);
assert_eq!(result, 0);
// There should still be no descriptors.
let descriptors = threads.webstorage_origins(storage_type);
match storage_type {
WebStorageType::Session => assert_eq!(descriptors.len(), 0),
WebStorageType::Local =>
// TODO: Fix localStorage to not create origin descriptors for
// read only operations (the length check above).
{
assert_eq!(descriptors.len(), 1)
},
}
// Set a different value.
let _ = test.set_item(storage_type, &url, "foo2", "bar2");
// Verify the original value doesn't exist.
let result = test.get_item(storage_type, &url, "foo");
assert_eq!(result, None);
}
#[test]
fn clear_data_for_sites_session() {
let test = WebStorageTest::new();
test_clear_data_for_sites(test, WebStorageType::Session);
}
#[test]
fn clear_data_for_sites_local() {
let test = WebStorageTest::new();
test_clear_data_for_sites(test, WebStorageType::Local);
}
#[test]
fn clear_data_for_sites_local_in_memory() {
let test = WebStorageTest::new_in_memory();
test_clear_data_for_sites(test, WebStorageType::Local);
}
#[test]
fn no_storage_type_conflict() {
// Ensures that editing session storage does not affect local storage and vice versa.
let mut test = WebStorageTest::new();
let url = ServoUrl::parse("https://example.com").unwrap();
test.set_item(
WebStorageType::Local,
&url,
"key".into(),
"local_value".into(),
)
.unwrap();
// Set session storage item.
test.set_item(
WebStorageType::Session,
&url,
"key".into(),
"session_value".into(),
)
.unwrap();
// Shutdown threads to ensure data is cleared from session storage and local storage is loaded from disk
test = test.restart();
let result = test.get_item(WebStorageType::Local, &url, "key".into());
assert_eq!(result, Some("local_value".into()));
// Get session storage item.
let result = test.get_item(WebStorageType::Session, &url, "key".into());
assert_eq!(result, None);
}