Skip to content

Commit 5f54e67

Browse files
committed
feat(left-right-tlcache): add simple ests
Add a test provider and several tests. Tests are serialized because they run in the same thread and access the same cache. That could be avoided using distinct caches,though. Signed-off-by: Fredi Raspall <[email protected]>
1 parent 199f1c1 commit 5f54e67

File tree

3 files changed

+369
-0
lines changed

3 files changed

+369
-0
lines changed

Cargo.lock

Lines changed: 1 addition & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

left-right-tlcache/Cargo.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,6 @@ license = "Apache-2.0"
99
left-right = { workspace = true }
1010
ahash = { workspace = true }
1111
thiserror = { workspace = true }
12+
13+
[dev-dependencies]
14+
serial_test = { workspace = true }

left-right-tlcache/src/lib.rs

Lines changed: 365 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,3 +209,368 @@ macro_rules! make_thread_local_readhandle_cache {
209209
}
210210
};
211211
}
212+
213+
#[cfg(test)]
214+
mod tests {
215+
#![allow(clippy::collapsible_if)]
216+
217+
use super::*;
218+
use left_right::{Absorb, ReadHandleFactory, WriteHandle};
219+
use serial_test::serial;
220+
use std::sync::Mutex;
221+
// Our left-right protected struct
222+
#[derive(Debug, Clone)]
223+
struct TestStruct {
224+
id: u64,
225+
data: String,
226+
}
227+
impl TestStruct {
228+
fn new(id: u64, data: &str) -> Self {
229+
Self {
230+
id,
231+
data: data.to_string(),
232+
}
233+
}
234+
}
235+
// Implement identity for TestStruct
236+
impl Identity<u64> for TestStruct {
237+
fn identity(&self) -> u64 {
238+
self.id
239+
}
240+
}
241+
242+
// Dummy implementation of Absorb for TestStruct
243+
#[derive(Debug)]
244+
enum TestStructChange {
245+
Update(String),
246+
}
247+
impl Absorb<TestStructChange> for TestStruct {
248+
fn absorb_first(&mut self, op: &mut TestStructChange, _other: &Self) {
249+
match op {
250+
TestStructChange::Update(data) => {
251+
self.data = data.clone();
252+
}
253+
}
254+
}
255+
fn sync_with(&mut self, first: &Self) {
256+
*self = first.clone();
257+
}
258+
}
259+
260+
// create local cache
261+
make_thread_local_readhandle_cache!(TEST_CACHE, u64, TestStruct);
262+
263+
// ReadHandle "owner" implementing ReadHandleProvider
264+
#[derive(Debug)]
265+
struct TestProviderEntry<TestStruct: Absorb<TestStructChange>, TestStructChange> {
266+
id: u64,
267+
factory: ReadHandleFactory<TestStruct>,
268+
// writer owning the TestStruct. We use option to easily drop it
269+
// and Mutex to make the provider Sync.
270+
writer: Option<Mutex<WriteHandle<TestStruct, TestStructChange>>>,
271+
}
272+
impl TestProviderEntry<TestStruct, TestStructChange> {
273+
fn new(
274+
id: u64,
275+
factory: ReadHandleFactory<TestStruct>,
276+
writer: Option<Mutex<WriteHandle<TestStruct, TestStructChange>>>,
277+
) -> Self {
278+
Self {
279+
id,
280+
factory,
281+
writer,
282+
}
283+
}
284+
}
285+
#[derive(Debug)]
286+
struct TestProvider {
287+
data: HashMap<u64, TestProviderEntry<TestStruct, TestStructChange>>,
288+
version: u64,
289+
}
290+
impl TestProvider {
291+
fn new() -> Self {
292+
Self {
293+
data: HashMap::new(),
294+
version: 0,
295+
}
296+
}
297+
fn add_object(&mut self, key: u64, identity: u64) {
298+
if key != identity {
299+
let entry = self.data.get(&identity).unwrap();
300+
let new = TestProviderEntry::new(identity, entry.factory.clone(), None);
301+
self.data.insert(key, new);
302+
} else {
303+
let object = TestStruct::new(identity, "unset");
304+
let (w, r) = left_right::new_from_empty(object);
305+
let entry = TestProviderEntry::new(identity, r.factory(), Some(Mutex::new(w)));
306+
self.data.insert(key, entry);
307+
let stored = self.data.get(&key).unwrap();
308+
assert_eq!(stored.id, identity);
309+
}
310+
self.version = self.version.wrapping_add(1);
311+
}
312+
fn mod_object(&mut self, key: u64, data: &str) {
313+
if let Some(object) = self.data.get_mut(&key) {
314+
if let Some(writer_lock) = &mut object.writer {
315+
#[allow(clippy::mut_mutex_lock)] // lock exists just to make provider Sync
316+
let mut writer = writer_lock.lock().unwrap();
317+
writer.append(TestStructChange::Update(data.to_owned()));
318+
writer.publish();
319+
}
320+
}
321+
}
322+
fn drop_writer(&mut self, key: u64) {
323+
if let Some(object) = self.data.get_mut(&key) {
324+
let x = object.writer.take();
325+
drop(x);
326+
self.version = self.version.wrapping_add(1);
327+
}
328+
}
329+
}
330+
331+
// Implement trait ReadHandleProvider
332+
impl ReadHandleProvider for TestProvider {
333+
type Data = TestStruct;
334+
type Key = u64;
335+
fn get_version(&self) -> u64 {
336+
self.version
337+
}
338+
fn get_factory(
339+
&self,
340+
key: &Self::Key,
341+
) -> Option<(&ReadHandleFactory<Self::Data>, Self::Key, u64)> {
342+
self.data
343+
.get(key)
344+
.map(|entry| (&entry.factory, entry.id, self.version))
345+
}
346+
fn get_identity(&self, key: &Self::Key) -> Option<Self::Key> {
347+
self.data.get(key).map(|entry| entry.id)
348+
}
349+
}
350+
351+
#[serial]
352+
#[test]
353+
fn test_readhandle_cache_basic() {
354+
// start fresh
355+
ReadHandleCache::purge(&TEST_CACHE);
356+
357+
// build provider
358+
let mut provider = TestProvider::new();
359+
provider.add_object(1, 1);
360+
provider.add_object(2, 2);
361+
provider.mod_object(1, "object-1");
362+
provider.mod_object(2, "object-2");
363+
364+
// add alias for 1
365+
provider.add_object(6000, 1);
366+
367+
{
368+
let key = 1;
369+
println!("Test: Access to object with key {key}");
370+
let h = ReadHandleCache::get_reader(&TEST_CACHE, key, &provider).unwrap();
371+
let x = h.enter().unwrap();
372+
let obj = x.as_ref();
373+
assert_eq!(obj.id, 1);
374+
assert_eq!(obj.data, "object-1");
375+
}
376+
377+
{
378+
let key = 2;
379+
println!("Test: Access to object with key {key}");
380+
let h = ReadHandleCache::get_reader(&TEST_CACHE, key, &provider).unwrap();
381+
let x = h.enter().unwrap();
382+
let obj = x.as_ref();
383+
assert_eq!(obj.id, 2);
384+
assert_eq!(obj.data, "object-2");
385+
}
386+
387+
{
388+
// 6000 is alias for 1: should access object with id 1
389+
let key = 6000;
390+
println!("Test: Access to object with key {key}");
391+
let h = ReadHandleCache::get_reader(&TEST_CACHE, key, &provider).unwrap();
392+
let x = h.enter().unwrap();
393+
let obj = x.as_ref();
394+
assert_eq!(obj.id, 1);
395+
assert_eq!(obj.data, "object-1");
396+
}
397+
398+
TEST_CACHE.with(|x| {
399+
let x = x.handles.borrow();
400+
assert!(x.contains_key(&6000));
401+
assert!(x.contains_key(&1));
402+
assert!(x.contains_key(&2));
403+
assert_eq!(x.len(), 3);
404+
println!("Test: cache contains entries");
405+
});
406+
407+
println!("Change: Let 6000 be a key for 2 instead of 1");
408+
provider.add_object(6000, 2);
409+
provider.mod_object(2, "object-2-modified");
410+
{
411+
let key = 6000;
412+
let h = ReadHandleCache::get_reader(&TEST_CACHE, key, &provider).unwrap();
413+
let x = h.enter().unwrap();
414+
let obj = x.as_ref();
415+
assert_eq!(obj.id, 2);
416+
assert_eq!(obj.data, "object-2-modified");
417+
}
418+
{
419+
let key = 6000;
420+
println!("Test: Access to object with key {key}");
421+
let h = ReadHandleCache::get_reader(&TEST_CACHE, key, &provider).unwrap();
422+
let x = h.enter().unwrap();
423+
let obj = x.as_ref();
424+
assert_eq!(obj.id, 2);
425+
assert_eq!(obj.data, "object-2-modified");
426+
}
427+
428+
println!("Change: drop data for object 1. It should not be accessible");
429+
provider.drop_writer(1);
430+
{
431+
let key = 1;
432+
println!("Test: Access to object with key {key}");
433+
let h = ReadHandleCache::get_reader(&TEST_CACHE, key, &provider);
434+
assert!(h.is_err_and(|e| e == ReadHandleCacheError::NotAccessible(key)));
435+
}
436+
437+
println!("Change: drop data for object 2: should not be accessible by keys 2 and 6000");
438+
provider.drop_writer(2);
439+
{
440+
let key = 2;
441+
println!("Test: Access to object with key {key}");
442+
let h = ReadHandleCache::get_reader(&TEST_CACHE, key, &provider);
443+
assert!(h.is_err_and(|e| e == ReadHandleCacheError::NotAccessible(key)));
444+
}
445+
{
446+
let key = 6000;
447+
println!("Test: Access to object with key {key}");
448+
let h = ReadHandleCache::get_reader(&TEST_CACHE, key, &provider);
449+
assert!(h.is_err_and(|e| e == ReadHandleCacheError::NotAccessible(key)));
450+
}
451+
452+
// ensure cache is clean
453+
TEST_CACHE.with(|x| {
454+
let x = x.handles.borrow();
455+
assert!(!x.contains_key(&6000));
456+
assert!(!x.contains_key(&1));
457+
assert!(!x.contains_key(&2));
458+
assert!(x.is_empty());
459+
println!("Test: cache is empty");
460+
});
461+
}
462+
463+
#[serial]
464+
#[test]
465+
fn test_readhandle_cache_multi_invalidation() {
466+
// start fresh
467+
ReadHandleCache::purge(&TEST_CACHE);
468+
469+
const NUM_ALIASES: u64 = 10;
470+
471+
// build provider
472+
let mut provider = TestProvider::new();
473+
provider.add_object(1, 1);
474+
provider.mod_object(1, "object-1");
475+
476+
// add aliases
477+
for k in 1..=NUM_ALIASES {
478+
let alias = 100 + k;
479+
provider.add_object(alias, 1);
480+
}
481+
482+
// query for identity and all aliases
483+
ReadHandleCache::get_reader(&TEST_CACHE, 1, &provider).unwrap();
484+
for k in 1..=NUM_ALIASES {
485+
let alias = 100 + k;
486+
ReadHandleCache::get_reader(&TEST_CACHE, alias, &provider).unwrap();
487+
}
488+
489+
// cache should contain NUM_ALIASES + 1 entries
490+
TEST_CACHE
491+
.with(|cache| assert_eq!(cache.handles.borrow().len() as u64, (NUM_ALIASES + 1u64)));
492+
493+
provider.drop_writer(1);
494+
495+
// do single query for identity
496+
let h = ReadHandleCache::get_reader(&TEST_CACHE, 1, &provider);
497+
assert!(h.is_err_and(|e| e == ReadHandleCacheError::NotAccessible(1)));
498+
499+
// all entries should have been invalidated
500+
TEST_CACHE.with(|cache| assert!(cache.handles.borrow().is_empty()));
501+
502+
// querying again with key = identity should fail
503+
let h = ReadHandleCache::get_reader(&TEST_CACHE, 1, &provider);
504+
assert!(h.is_err_and(|e| e == ReadHandleCacheError::NotAccessible(1)));
505+
506+
// querying again with aliases should fail too. Aliases are 100 + k k=1..=NUM_ALIASES
507+
let alias = 100 + 1;
508+
let h = ReadHandleCache::get_reader(&TEST_CACHE, alias, &provider);
509+
assert!(h.is_err_and(|e| e == ReadHandleCacheError::NotAccessible(alias)));
510+
}
511+
512+
#[serial]
513+
#[test]
514+
fn test_readhandle_cache() {
515+
// start fresh
516+
ReadHandleCache::purge(&TEST_CACHE);
517+
518+
// build provider and populate it
519+
const NUM_HANDLES: u64 = 1000;
520+
let mut provider = TestProvider::new();
521+
for id in 0..=NUM_HANDLES {
522+
provider.add_object(id, id);
523+
provider.mod_object(id, format!("object-id-{id}").as_ref());
524+
provider.add_object(id + NUM_HANDLES + 1, id);
525+
}
526+
527+
// access all objects by id
528+
for id in 0..=NUM_HANDLES {
529+
let h = ReadHandleCache::get_reader(&TEST_CACHE, id, &provider).unwrap();
530+
let x = h.enter().unwrap();
531+
let obj = x.as_ref();
532+
assert_eq!(obj.id, id);
533+
}
534+
535+
// access all objects by alias
536+
for id in 0..=NUM_HANDLES {
537+
let alias = id + NUM_HANDLES + 1;
538+
let h = ReadHandleCache::get_reader(&TEST_CACHE, alias, &provider).unwrap();
539+
let x = h.enter().unwrap();
540+
let obj = x.as_ref();
541+
assert_eq!(obj.id, id);
542+
}
543+
544+
// modify all objects and replace all aliases
545+
for id in 0..=NUM_HANDLES {
546+
let alias = 2 * NUM_HANDLES + 1 - id;
547+
provider.mod_object(id, format!("modified-id-{id}").as_ref());
548+
provider.add_object(alias, id);
549+
}
550+
551+
// access objects with re-assigned aliases
552+
for id in 0..=NUM_HANDLES {
553+
let alias = 2 * NUM_HANDLES + 1 - id;
554+
let h = ReadHandleCache::get_reader(&TEST_CACHE, alias, &provider).unwrap();
555+
let x = h.enter().unwrap();
556+
let obj = x.as_ref();
557+
assert_eq!(obj.id, id);
558+
assert_eq!(obj.data, format!("modified-id-{id}"));
559+
}
560+
561+
// invalidate all writers
562+
for id in 0..=NUM_HANDLES {
563+
provider.drop_writer(id);
564+
}
565+
566+
// access objects from alias: none should be accessible
567+
for id in 0..=NUM_HANDLES {
568+
let alias = 2 * NUM_HANDLES + 1 - id;
569+
let h = ReadHandleCache::get_reader(&TEST_CACHE, alias, &provider);
570+
assert!(h.is_err_and(|e| e == ReadHandleCacheError::NotAccessible(alias)));
571+
}
572+
573+
// all handles from cache should have been removed as we looked up them all
574+
TEST_CACHE.with(|cache| assert!(cache.handles.borrow().is_empty()));
575+
}
576+
}

0 commit comments

Comments
 (0)