Skip to content

Commit 1d36b0b

Browse files
committed
Add storage adapter tests from automerge-repo
1 parent 60b808f commit 1d36b0b

File tree

5 files changed

+264
-162
lines changed

5 files changed

+264
-162
lines changed

samod/src/storage.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,8 @@ mod filesystem;
88
mod in_memory;
99
pub use in_memory::InMemoryStorage;
1010

11+
pub mod testing;
12+
1113
#[cfg(feature = "tokio")]
1214
pub use filesystem::tokio::FilesystemStorage as TokioFilesystemStorage;
1315

samod/src/storage/testing.rs

Lines changed: 191 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,191 @@
1+
//! Storage adapter testing utilities
2+
//!
3+
//! rewritten from:
4+
//! automerge-repo/packages/automerge-repo/src/helpers/tests/storage-adapter-tests.ts
5+
//!
6+
//! Provides a test suite for any implementation of the `Storage` trait.
7+
//! Based on the TypeScript `runStorageAdapterTests` from automerge-repo.
8+
9+
#![allow(dead_code)]
10+
11+
use rand::Rng;
12+
use std::future::Future;
13+
use std::pin::Pin;
14+
use std::sync::LazyLock;
15+
16+
use super::{Storage, StorageKey};
17+
18+
pub fn payload_a() -> Vec<u8> {
19+
vec![0, 1, 127, 99, 154, 235]
20+
}
21+
22+
pub fn payload_b() -> Vec<u8> {
23+
vec![1, 76, 160, 53, 57, 10, 230]
24+
}
25+
26+
pub fn payload_c() -> Vec<u8> {
27+
vec![2, 111, 74, 131, 236, 96, 142, 193]
28+
}
29+
30+
static LARGE_PAYLOAD: LazyLock<Vec<u8>> = LazyLock::new(|| {
31+
let mut vec = vec![0u8; 100000];
32+
rand::rng().fill(&mut vec[..]);
33+
vec
34+
});
35+
36+
pub fn large_payload() -> Vec<u8> {
37+
LARGE_PAYLOAD.clone()
38+
}
39+
40+
/// Trait for storage test fixtures
41+
pub trait StorageTestFixture: Sized + Send {
42+
/// The storage type being tested
43+
type Storage: Storage + Send + Sync + 'static;
44+
45+
/// Setup the test fixture
46+
fn setup() -> impl std::future::Future<Output = Self> + Send;
47+
48+
/// Get reference to the storage adapter
49+
fn storage(&self) -> &Self::Storage;
50+
51+
/// Optional cleanup
52+
fn teardown(self) -> impl std::future::Future<Output = ()> + Send {
53+
async {}
54+
}
55+
}
56+
57+
/// Helper to run a single test with setup and teardown
58+
async fn run_test<F, TestFn>(test_fn: TestFn)
59+
where
60+
F: StorageTestFixture,
61+
TestFn: for<'a> FnOnce(&'a F::Storage) -> Pin<Box<dyn Future<Output = ()> + Send + 'a>> + Send,
62+
{
63+
let fixture = F::setup().await;
64+
test_fn(fixture.storage()).await;
65+
fixture.teardown().await;
66+
}
67+
68+
/// Run all storage adapter acceptance tests
69+
pub async fn run_storage_adapter_tests<F: StorageTestFixture>() {
70+
run_test::<F, _>(|a| Box::pin(test_load_should_return_none_if_no_data(a))).await;
71+
run_test::<F, _>(|a| Box::pin(test_save_and_load_should_return_data_that_was_saved(a))).await;
72+
run_test::<F, _>(|a| Box::pin(test_save_and_load_should_work_with_composite_keys(a))).await;
73+
run_test::<F, _>(|a| Box::pin(test_save_and_load_should_work_with_large_payload(a))).await;
74+
run_test::<F, _>(|a| Box::pin(test_load_range_should_return_empty_if_no_data(a))).await;
75+
run_test::<F, _>(|a| Box::pin(test_save_and_load_range_should_return_all_matching_data(a)))
76+
.await;
77+
run_test::<F, _>(|a| Box::pin(test_save_and_load_range_should_only_load_matching_values(a)))
78+
.await;
79+
run_test::<F, _>(|a| Box::pin(test_save_and_remove_should_be_empty_after_removing(a))).await;
80+
run_test::<F, _>(|a| Box::pin(test_save_and_save_should_overwrite(a))).await;
81+
}
82+
83+
// describe("load")
84+
pub async fn test_load_should_return_none_if_no_data<S: Storage>(adapter: &S) {
85+
let actual = adapter
86+
.load(StorageKey::from_parts(["AAAAA", "sync-state", "xxxxx"]).unwrap())
87+
.await;
88+
89+
assert_eq!(actual, None);
90+
}
91+
92+
// describe("save and load")
93+
pub async fn test_save_and_load_should_return_data_that_was_saved<S: Storage>(adapter: &S) {
94+
let key = StorageKey::from_parts(["storage-adapter-id"]).unwrap();
95+
adapter.put(key.clone(), payload_a()).await;
96+
97+
let actual = adapter.load(key).await;
98+
99+
assert_eq!(actual, Some(payload_a()));
100+
}
101+
102+
pub async fn test_save_and_load_should_work_with_composite_keys<S: Storage>(adapter: &S) {
103+
let key = StorageKey::from_parts(["AAAAA", "sync-state", "xxxxx"]).unwrap();
104+
adapter.put(key.clone(), payload_a()).await;
105+
106+
let actual = adapter.load(key).await;
107+
108+
assert_eq!(actual, Some(payload_a()));
109+
}
110+
111+
pub async fn test_save_and_load_should_work_with_large_payload<S: Storage>(adapter: &S) {
112+
let key = StorageKey::from_parts(["AAAAA", "sync-state", "xxxxx"]).unwrap();
113+
adapter.put(key.clone(), large_payload()).await;
114+
115+
let actual = adapter.load(key).await;
116+
117+
assert_eq!(actual, Some(large_payload()));
118+
}
119+
120+
// describe("loadRange")
121+
pub async fn test_load_range_should_return_empty_if_no_data<S: Storage>(adapter: &S) {
122+
let result = adapter.load_range(StorageKey::from_parts(["AAAAA"]).unwrap()).await;
123+
124+
assert_eq!(result.len(), 0);
125+
}
126+
127+
// describe("save and loadRange")
128+
pub async fn test_save_and_load_range_should_return_all_matching_data<S: Storage>(adapter: &S) {
129+
let key_a = StorageKey::from_parts(["AAAAA", "sync-state", "xxxxx"]).unwrap();
130+
let key_b = StorageKey::from_parts(["AAAAA", "snapshot", "yyyyy"]).unwrap();
131+
let key_c = StorageKey::from_parts(["AAAAA", "sync-state", "zzzzz"]).unwrap();
132+
133+
adapter.put(key_a.clone(), payload_a()).await;
134+
adapter.put(key_b.clone(), payload_b()).await;
135+
adapter.put(key_c.clone(), payload_c()).await;
136+
137+
let result = adapter.load_range(StorageKey::from_parts(["AAAAA"]).unwrap()).await;
138+
139+
assert_eq!(result.len(), 3);
140+
assert_eq!(result.get(&key_a), Some(&payload_a()));
141+
assert_eq!(result.get(&key_b), Some(&payload_b()));
142+
assert_eq!(result.get(&key_c), Some(&payload_c()));
143+
144+
let sync_result = adapter
145+
.load_range(StorageKey::from_parts(["AAAAA", "sync-state"]).unwrap())
146+
.await;
147+
148+
assert_eq!(sync_result.len(), 2);
149+
assert_eq!(sync_result.get(&key_a), Some(&payload_a()));
150+
assert_eq!(sync_result.get(&key_c), Some(&payload_c()));
151+
}
152+
153+
pub async fn test_save_and_load_range_should_only_load_matching_values<S: Storage>(adapter: &S) {
154+
let key_a = StorageKey::from_parts(["AAAAA", "sync-state", "xxxxx"]).unwrap();
155+
let key_c = StorageKey::from_parts(["BBBBB", "sync-state", "zzzzz"]).unwrap();
156+
157+
adapter.put(key_a.clone(), payload_a()).await;
158+
adapter.put(key_c.clone(), payload_c()).await;
159+
160+
let actual = adapter.load_range(StorageKey::from_parts(["AAAAA"]).unwrap()).await;
161+
162+
assert_eq!(actual.len(), 1);
163+
assert_eq!(actual.get(&key_a), Some(&payload_a()));
164+
}
165+
166+
// describe("save and remove")
167+
pub async fn test_save_and_remove_should_be_empty_after_removing<S: Storage>(adapter: &S) {
168+
let key = StorageKey::from_parts(["AAAAA", "snapshot", "xxxxx"]).unwrap();
169+
adapter.put(key.clone(), payload_a()).await;
170+
adapter.delete(key.clone()).await;
171+
172+
let range_result = adapter.load_range(StorageKey::from_parts(["AAAAA"]).unwrap()).await;
173+
assert_eq!(range_result.len(), 0);
174+
175+
let load_result = adapter.load(key).await;
176+
assert_eq!(load_result, None);
177+
}
178+
179+
// describe("save and save")
180+
pub async fn test_save_and_save_should_overwrite<S: Storage>(adapter: &S) {
181+
let key = StorageKey::from_parts(["AAAAA", "sync-state", "xxxxx"]).unwrap();
182+
adapter.put(key.clone(), payload_a()).await;
183+
adapter.put(key.clone(), payload_b()).await;
184+
185+
let result = adapter
186+
.load_range(StorageKey::from_parts(["AAAAA", "sync-state"]).unwrap())
187+
.await;
188+
189+
assert_eq!(result.len(), 1);
190+
assert_eq!(result.get(&key), Some(&payload_b()));
191+
}

samod/tests/gio_storage_test.rs

Lines changed: 19 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,8 @@
11
#[cfg(feature = "gio")]
22
mod gio_tests {
3-
use std::collections::HashMap;
43
use tempfile::TempDir;
54

6-
use samod::storage::{GioFilesystemStorage, Storage};
5+
use samod::storage::{testing::StorageTestFixture, GioFilesystemStorage, Storage};
76
use samod_core::StorageKey;
87

98
fn init_logging() {
@@ -12,93 +11,36 @@ mod gio_tests {
1211
.try_init();
1312
}
1413

15-
#[test]
16-
fn test_gio_storage_basic_operations() {
17-
init_logging();
18-
19-
let main_context = glib::MainContext::new();
20-
main_context.block_on(async {
21-
let temp_dir = TempDir::new().unwrap();
22-
let storage = GioFilesystemStorage::new(temp_dir.path());
23-
24-
let key = StorageKey::from_parts(vec!["test"]).unwrap();
25-
let data = b"hello world".to_vec();
26-
27-
// Test put and load
28-
storage.put(key.clone(), data.clone()).await;
29-
let loaded = storage.load(key.clone()).await;
30-
assert_eq!(loaded, Some(data));
31-
32-
// Test delete
33-
storage.delete(key.clone()).await;
34-
let loaded_after_delete = storage.load(key).await;
35-
assert_eq!(loaded_after_delete, None);
36-
});
37-
}
38-
39-
#[test]
40-
fn test_gio_storage_load_range() {
41-
init_logging();
42-
43-
let main_context = glib::MainContext::new();
44-
main_context.block_on(async {
45-
let temp_dir = TempDir::new().unwrap();
46-
let storage = GioFilesystemStorage::new(temp_dir.path());
47-
48-
let base_key = StorageKey::from_parts(vec!["test_prefix"]).unwrap();
49-
50-
// Put multiple files with the same prefix
51-
let key1 = StorageKey::from_parts(vec!["test_prefix", "file1"]).unwrap();
52-
let key2 = StorageKey::from_parts(vec!["test_prefix", "file2"]).unwrap();
53-
let key3 = StorageKey::from_parts(vec!["test_prefix", "subdir", "file3"]).unwrap();
54-
55-
let data1 = b"data1".to_vec();
56-
let data2 = b"data2".to_vec();
57-
let data3 = b"data3".to_vec();
58-
59-
storage.put(key1.clone(), data1.clone()).await;
60-
storage.put(key2.clone(), data2.clone()).await;
61-
storage.put(key3.clone(), data3.clone()).await;
62-
63-
// Load range
64-
let loaded_range = storage.load_range(base_key).await;
65-
66-
let mut expected = HashMap::new();
67-
expected.insert(key1, data1);
68-
expected.insert(key2, data2);
69-
expected.insert(key3, data3);
70-
71-
assert_eq!(loaded_range, expected);
72-
});
14+
struct GioFilesystemStorageFixture {
15+
storage: GioFilesystemStorage,
16+
_temp_dir: TempDir,
7317
}
7418

75-
#[test]
76-
fn test_gio_storage_nonexistent_file() {
77-
init_logging();
19+
impl StorageTestFixture for GioFilesystemStorageFixture {
20+
type Storage = GioFilesystemStorage;
7821

79-
let main_context = glib::MainContext::new();
80-
main_context.block_on(async {
22+
async fn setup() -> Self {
8123
let temp_dir = TempDir::new().unwrap();
8224
let storage = GioFilesystemStorage::new(temp_dir.path());
83-
84-
let key = StorageKey::from_parts(vec!["nonexistent"]).unwrap();
85-
let loaded = storage.load(key).await;
86-
assert_eq!(loaded, None);
87-
});
25+
Self {
26+
storage,
27+
_temp_dir: temp_dir,
28+
}
29+
}
30+
31+
fn storage(&self) -> &Self::Storage {
32+
&self.storage
33+
}
8834
}
8935

9036
#[test]
91-
fn test_gio_storage_empty_range() {
37+
fn gio_filesystem_storage_standard_tests() {
9238
init_logging();
9339

9440
let main_context = glib::MainContext::new();
9541
main_context.block_on(async {
96-
let temp_dir = TempDir::new().unwrap();
97-
let storage = GioFilesystemStorage::new(temp_dir.path());
98-
99-
let prefix = StorageKey::from_parts(vec!["empty_prefix"]).unwrap();
100-
let loaded_range = storage.load_range(prefix).await;
101-
assert!(loaded_range.is_empty());
42+
samod::storage::testing::run_storage_adapter_tests::<GioFilesystemStorageFixture>()
43+
.await;
10244
});
10345
}
10446

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
mod in_memory_tests {
2+
use samod::storage::{testing::StorageTestFixture, InMemoryStorage};
3+
4+
fn init_logging() {
5+
let _ = tracing_subscriber::fmt()
6+
.with_env_filter(tracing_subscriber::EnvFilter::from_default_env())
7+
.try_init();
8+
}
9+
10+
struct InMemoryStorageFixture {
11+
storage: InMemoryStorage,
12+
}
13+
14+
impl StorageTestFixture for InMemoryStorageFixture {
15+
type Storage = InMemoryStorage;
16+
17+
async fn setup() -> Self {
18+
Self {
19+
storage: InMemoryStorage::new(),
20+
}
21+
}
22+
23+
fn storage(&self) -> &Self::Storage {
24+
&self.storage
25+
}
26+
}
27+
28+
#[tokio::test]
29+
async fn in_memory_storage_standard_tests() {
30+
init_logging();
31+
samod::storage::testing::run_storage_adapter_tests::<InMemoryStorageFixture>().await;
32+
}
33+
}

0 commit comments

Comments
 (0)