Files
NLnetLabs-krill/src/commons/storage/test.rs
T

394 lines
12 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! Tests for the storage module.
//!
//! This module contains tests that are run for each storage backend which
//! requires a wee bit of macro magic.
#![cfg(test)]
use std::sync::{Mutex, MutexGuard};
use lazy_static::lazy_static;
use tempfile::{TempDir, tempdir};
use url::Url;
use super::{Ident, KeyValueStore, StorageSystem};
//------------ Macro to Construct Tests --------------------------------------
/// A macro to construct a function testing each backend.
///
/// For details, see the macro invocation below. This is just up here because
/// Rust wants it that way.
macro_rules! testfns {
(
$(
fn $name:ident($harness:ident: impl Harness) $body:block
)*
) => {
mod testfns {
use super::*;
$(
pub fn $name($harness: impl Harness) $body
)*
}
mod memory {
use super::*;
$(
#[test]
fn $name() {
super::testfns::$name(MemoryHarness::new());
}
)*
}
mod disk {
use super::*;
$(
#[test]
fn $name() {
super::testfns::$name(DiskHarness::new());
}
)*
}
}
}
//------------ Test Data -----------------------------------------------------
const NAMESPACE: &Ident = Ident::make("namespace");
const SCOPE: &Ident = Ident::make("scope");
const SCOPE_2: &Ident= Ident::make("other_scope");
const KEY: &Ident = Ident::make("key");
const KEY_2: &Ident = Ident::make("other_key");
const CONTENT: u32 = 42;
const CONTENT_2: u32 = 43;
const CONTENT_3: u32 = 44;
const CONTENT_4: u32 = 45;
//------------ Test Functions ------------------------------------------------
// All the test functions.
//
// They all need to have the same signature taking one argument as an
// `impl Harness` and return unit. Each function will be transformed into a
// test function for each of the backends (currently memory and disk). The
// harness will give it access to a temporary test store atop that given
// backend. You can create a store for a specific namespace via the `store`
// method and access the store’s URL via `url`. This will allow you to
// determine the backend via the URL’s scheme if needed.
testfns! {
fn store_get(harness: impl Harness) {
let store = harness.store(NAMESPACE);
store.store(None, KEY, &CONTENT).unwrap();
assert!(store.has(None, KEY).unwrap());
assert_eq!(store.get(None, KEY).unwrap(), Some(CONTENT));
}
fn store_new(harness: impl Harness) {
let store = harness.store(NAMESPACE);
assert!(store.store_new(None, KEY, &CONTENT).is_ok());
assert!(store.store_new(None, KEY, &CONTENT).is_err());
}
fn store_scoped(harness: impl Harness) {
let store = harness.store(NAMESPACE);
store.store(Some(SCOPE), KEY, &CONTENT).unwrap();
assert!(store.has(Some(SCOPE), KEY).unwrap());
assert_eq!(store.get(Some(SCOPE), KEY).unwrap(), Some(CONTENT));
assert!(store.has_scope(SCOPE).unwrap());
store.store(None, KEY, &CONTENT_2).unwrap();
assert!(store.has(None, KEY).unwrap());
assert_eq!(store.get(None, KEY).unwrap(), Some(CONTENT_2));
assert!(store.has(Some(SCOPE), KEY).unwrap());
assert_eq!(store.get(Some(SCOPE), KEY).unwrap(), Some(CONTENT));
}
fn get(harness: impl Harness) {
let store = harness.store(NAMESPACE);
assert!(store.get::<String>(None, KEY).unwrap().is_none());
store.store(None, KEY, &CONTENT).unwrap();
assert_eq!(store.get(None, KEY).unwrap(), Some(CONTENT));
}
fn has(harness: impl Harness) {
let store = harness.store(NAMESPACE);
assert!(!store.has(None, KEY).unwrap());
store.store(None, KEY, &CONTENT).unwrap();
assert!(store.has(None, KEY).unwrap());
}
fn drop_global_key(harness: impl Harness) {
let store = harness.store(NAMESPACE);
assert!(store.drop_key(None, KEY).is_err());
store.store(None, KEY, &CONTENT).unwrap();
assert!(store.has(None, KEY).unwrap());
store.drop_key(None, KEY).unwrap();
assert!(!store.has(None, KEY).unwrap());
}
fn drop_scoped_key(harness: impl Harness) {
let store = harness.store(NAMESPACE);
assert!(store.drop_key(Some(SCOPE), KEY).is_err());
store.store(Some(SCOPE), KEY, &CONTENT).unwrap();
assert!(store.has(Some(SCOPE), KEY).unwrap());
store.drop_key(Some(SCOPE), KEY).unwrap();
assert!(!store.has(Some(SCOPE), KEY).unwrap());
assert!(!store.has_scope(SCOPE).unwrap());
}
fn drop_scope(harness: impl Harness) {
let store = harness.store(NAMESPACE);
store.store(Some(SCOPE), KEY, &CONTENT).unwrap();
store.store(Some(SCOPE_2), KEY_2, &CONTENT_2).unwrap();
assert!(store.has_scope(SCOPE).unwrap());
assert!(store.has_scope(SCOPE_2).unwrap());
assert!(store.has(Some(SCOPE), KEY).unwrap());
assert!(store.has(Some(SCOPE_2), KEY_2).unwrap());
store.drop_scope(SCOPE).unwrap();
assert!(!store.has_scope(SCOPE).unwrap());
assert!(store.has_scope(SCOPE_2).unwrap());
assert!(!store.has(Some(SCOPE), KEY).unwrap());
assert!(store.has(Some(SCOPE_2), KEY_2).unwrap());
}
fn wipe(harness: impl Harness) {
let store = harness.store(NAMESPACE);
store.store(Some(SCOPE), KEY, &CONTENT).unwrap();
assert!(store.has_scope(SCOPE).unwrap());
assert!(store.has(Some(SCOPE), KEY).unwrap());
store.wipe().unwrap();
assert!(!store.has_scope(SCOPE).unwrap());
assert!(!store.has(Some(SCOPE), KEY).unwrap());
assert!(store.keys(None, "").unwrap().is_empty());
assert!(store.keys(Some(SCOPE), "").unwrap().is_empty());
}
fn scopes(harness: impl Harness) {
let store = harness.store(NAMESPACE);
assert!(store.scopes().unwrap().is_empty());
store.store(Some(SCOPE), KEY, &CONTENT).unwrap();
assert_eq!(store.scopes().unwrap(), [SCOPE.into()]);
store.store(Some(SCOPE_2), KEY_2, &CONTENT_2).unwrap();
let mut scopes = store.scopes().unwrap();
scopes.sort();
let mut expected = vec![SCOPE.into(), SCOPE_2.into()];
expected.sort();
assert_eq!(scopes, expected);
store.drop_scope(SCOPE_2).unwrap();
assert_eq!(store.scopes().unwrap(), [SCOPE.into()]);
store.drop_scope(SCOPE).unwrap();
assert!(store.scopes().unwrap().is_empty());
}
fn has_scope(harness: impl Harness) {
let store = harness.store(NAMESPACE);
assert!(!store.has_scope(SCOPE).unwrap());
store.store(Some(SCOPE), KEY, &CONTENT).unwrap();
assert!(store.has_scope(SCOPE).unwrap());
store.drop_key(Some(SCOPE), KEY).unwrap();
assert!(!store.has_scope(SCOPE).unwrap());
}
fn keys(harness: impl Harness) {
let store = harness.store(NAMESPACE);
const KEY_ID: &Ident = Ident::make("command--id");
const KEY_LS: &Ident = Ident::make("command--ls");
const KEY_OTHER: &Ident = Ident::make("other");
// key: Some(SCOPE), KEY_ID
// key2: Some(SCOPE), KEY_LS
// key3: None, KEY_OTHER
// key4: None, KEY_ID
store.store(Some(SCOPE), KEY_ID, &CONTENT).unwrap();
store.store(Some(SCOPE), KEY_LS, &CONTENT_2).unwrap();
store.store(None, KEY_OTHER, &CONTENT_3).unwrap();
store.store(None, KEY_ID, &CONTENT_4).unwrap();
let mut keys = store.keys(Some(SCOPE), "command--").unwrap();
keys.sort();
let mut expected = vec![KEY_ID.into(), KEY_LS.into()];
expected.sort();
assert_eq!(keys, expected);
assert_eq!(
store.keys(Some(SCOPE), KEY_LS.as_str()).unwrap(),
[KEY_LS.into()]
);
assert_eq!(store.keys(Some(SCOPE), KEY_OTHER.as_str()).unwrap(), []);
assert_eq!(
store.keys(None, KEY_OTHER.as_str()).unwrap(),
[KEY_OTHER.into()]
);
let mut keys = store.keys(Some(SCOPE), "").unwrap();
keys.sort();
let mut expected = vec![KEY_ID.into(), KEY_LS.into()];
expected.sort();
assert_eq!(keys, expected);
}
fn move_value(harness: impl Harness) {
let store = harness.store(NAMESPACE);
store.store(Some(SCOPE), KEY, &CONTENT).unwrap();
store.store(Some(SCOPE), KEY_2, &CONTENT_2).unwrap();
assert!(store.has_scope(SCOPE).unwrap());
assert!(!store.has_scope(SCOPE_2).unwrap());
assert_eq!(store.get(Some(SCOPE), KEY).unwrap(), Some(CONTENT));
assert!(!store.has(Some(SCOPE_2), KEY).unwrap());
assert_eq!(store.get(Some(SCOPE), KEY_2).unwrap(), Some(CONTENT_2));
assert!(!store.has(Some(SCOPE_2), KEY_2).unwrap());
store.execute(None, |store| {
store.move_value(Some(SCOPE), KEY, Some(SCOPE_2), KEY)
}).unwrap();
assert!(store.has_scope(SCOPE).unwrap());
assert!(store.has_scope(SCOPE_2).unwrap());
assert!(!store.has(Some(SCOPE), KEY).unwrap());
assert_eq!(store.get(Some(SCOPE_2), KEY).unwrap(), Some(CONTENT));
assert_eq!(store.get(Some(SCOPE), KEY_2).unwrap(), Some(CONTENT_2));
assert!(!store.has(Some(SCOPE_2), KEY_2).unwrap());
store.execute(None, |store| {
store.move_value(Some(SCOPE), KEY_2, Some(SCOPE_2), KEY_2)
}).unwrap();
assert!(!store.has_scope(SCOPE).unwrap());
assert!(store.has_scope(SCOPE_2).unwrap());
assert!(!store.has(Some(SCOPE), KEY).unwrap());
assert_eq!(store.get(Some(SCOPE_2), KEY).unwrap(), Some(CONTENT));
assert!(!store.has(Some(SCOPE), KEY_2).unwrap());
assert_eq!(store.get(Some(SCOPE_2), KEY_2).unwrap(), Some(CONTENT_2));
}
}
//------------ Harness -------------------------------------------------------
/// A test harness for a specific storage backend.
trait Harness {
/// Returns the URL of the backend.
#[allow(dead_code)]
fn url(&self) -> Url;
/// Creates a new store for the given namespace.
fn store(&self, namespace: &Ident) -> KeyValueStore;
}
//------------ MemoryHarness -------------------------------------------------
/// The test harness for the memory backend.
///
/// Because there is only a single shared memory store for the whole process,
/// we can only run a single test using it at the same time and need to wipe
/// it clean before the test. This is why there are a lock and a guard here.
struct MemoryHarness<'a> {
_guard: MutexGuard<'a, ()>,
storage: StorageSystem,
}
lazy_static! {
static ref MEMORY_LOCK: Mutex<()> = Mutex::new(());
}
impl<'a> MemoryHarness<'a> {
fn new() -> Self {
loop {
let _guard = match MEMORY_LOCK.lock() {
Ok(guard) => guard,
Err(_) => {
// If a thread panicked, the lock gets poisoned. But we
// know that a panicked thread (and its test) has ended,
// so we can clear the poison and try to acquire the
// lock again.
MEMORY_LOCK.clear_poison();
continue;
}
};
super::backends::memory::Store::wipe_all();
return Self {
_guard,
storage: StorageSystem::new(
Url::parse("memory:").unwrap()
).unwrap(),
}
}
}
}
impl<'a> Harness for MemoryHarness<'a> {
fn url(&self) -> Url {
self.storage.default_uri().clone()
}
fn store(&self, namespace: &Ident) -> KeyValueStore {
self.storage.open(namespace).unwrap()
}
}
//------------ DiskHarness ---------------------------------------------------
/// The test harness for the dist backend.
///
/// Creates a temporary directory using the `tempfile` crate which will be
/// removed automatically when the harness is dropped.
struct DiskHarness {
_dir: TempDir,
storage: StorageSystem,
}
impl DiskHarness {
fn new() -> Self {
let _dir = tempdir().unwrap();
let url = format!("local://{}", _dir.path().display());
let storage = StorageSystem::new(Url::parse(&url).unwrap()).unwrap();
Self { _dir, storage }
}
}
impl Harness for DiskHarness {
fn url(&self) -> Url {
self.storage.default_uri().clone()
}
fn store(&self, namespace: &Ident) -> KeyValueStore {
self.storage.open(namespace).unwrap()
}
}