diff --git a/go/storage/mkvs/urkel/tests/fixture.go b/go/storage/mkvs/urkel/tests/fixture.go index 9360ad58091..ee40dfb0c05 100644 --- a/go/storage/mkvs/urkel/tests/fixture.go +++ b/go/storage/mkvs/urkel/tests/fixture.go @@ -1,3 +1,4 @@ +// Package tests contains helpers for testing MKVS trees. package tests const ( @@ -11,7 +12,7 @@ const ( OpIteratorSeek = "IteratorSeek" ) -// Op is a tree operation. +// Op is a tree operation used in test vectors. type Op struct { // Op is the operation name. Op string `json:"op"` diff --git a/runtime/src/storage/mkvs/urkel/mod.rs b/runtime/src/storage/mkvs/urkel/mod.rs index 3fab4127d22..0a1ea62d72f 100644 --- a/runtime/src/storage/mkvs/urkel/mod.rs +++ b/runtime/src/storage/mkvs/urkel/mod.rs @@ -5,5 +5,7 @@ mod cache; mod interop; pub mod marshal; pub mod sync; +#[cfg(test)] +mod tests; pub use tree::{Depth, Key, Root, UrkelTree}; diff --git a/runtime/src/storage/mkvs/urkel/tests/mod.rs b/runtime/src/storage/mkvs/urkel/tests/mod.rs new file mode 100644 index 00000000000..f197e30d1d7 --- /dev/null +++ b/runtime/src/storage/mkvs/urkel/tests/mod.rs @@ -0,0 +1,67 @@ +//! Helpers for testing MKVS trees. +use std::fmt; + +use base64; +use serde; +use serde_derive::Deserialize; + +/// Tree operation kind. +#[derive(Clone, Debug, Deserialize)] +pub enum OpKind { + Insert, + Remove, + Get, + IteratorSeek, +} + +/// Tree operation used in test vectors. +#[derive(Clone, Debug, Deserialize)] +pub struct Op { + /// Operation kind. + pub op: OpKind, + /// Key that is inserted, removed or looked up. + #[serde(default, deserialize_with = "deserialize_base64")] + pub key: Option>, + /// Value that is inserted or that is expected for the given key during lookup. + #[serde(default, deserialize_with = "deserialize_base64")] + pub value: Option>, + /// Key that is expected for the given operation (e.g., iterator seek). + #[serde(default, deserialize_with = "deserialize_base64")] + pub expected_key: Option>, +} + +/// A MKVS tree test vector (a series of tree operations). +pub type TestVector = Vec; + +fn deserialize_base64<'de, D>(deserializer: D) -> Result>, D::Error> +where + D: serde::de::Deserializer<'de>, +{ + struct Base64Visitor; + + impl<'de> serde::de::Visitor<'de> for Base64Visitor { + type Value = Option>; + + fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result { + write!(formatter, "base64 ASCII text") + } + + fn visit_str(self, v: &str) -> Result + where + E: serde::de::Error, + { + base64::decode(v) + .map_err(serde::de::Error::custom) + .map(Some) + } + + fn visit_none(self) -> Result + where + E: serde::de::Error, + { + Ok(None) + } + } + + deserializer.deserialize_str(Base64Visitor) +} diff --git a/runtime/src/storage/mkvs/urkel/tree/tree_test.rs b/runtime/src/storage/mkvs/urkel/tree/tree_test.rs index 31010f55da3..6de5ec6c367 100644 --- a/runtime/src/storage/mkvs/urkel/tree/tree_test.rs +++ b/runtime/src/storage/mkvs/urkel/tree/tree_test.rs @@ -1,5 +1,6 @@ use io_context::Context; -use std::{collections::HashSet, iter::FromIterator}; +use serde_json; +use std::{collections::HashSet, fs::File, io::BufReader, iter::FromIterator, path::Path}; use crate::{ common::crypto::hash::Hash, @@ -8,6 +9,7 @@ use crate::{ cache::*, interop::{Driver, ProtocolServer}, sync::*, + tests, tree::*, }, LogEntry, LogEntryKind, WriteLog, @@ -874,3 +876,150 @@ fn test_node_eviction() { "cache.leaf_value_size" ); } + +/// Location of the test vectors directory (from Go). +const TEST_VECTORS_DIR: &'static str = "../go/storage/mkvs/urkel/testdata"; + +fn test_special_case_from_json(fixture: &'static str) { + let server = ProtocolServer::new(); + + let file = + File::open(Path::new(TEST_VECTORS_DIR).join(fixture)).expect("failed to open fixture"); + let reader = BufReader::new(file); + + let ops: tests::TestVector = serde_json::from_reader(reader).expect("failed to parse fixture"); + + let mut tree = UrkelTree::make() + .with_capacity(0, 0) + .new(Box::new(NoopReadSyncer {})); + let mut remote_tree: Option = None; + let mut root = Hash::empty_hash(); + + let mut commit_remote = |tree: &mut UrkelTree, remote_tree: &mut Option| { + let (write_log, hash) = + UrkelTree::commit(tree, Context::background(), Default::default(), 0).expect("commit"); + server.apply_existing(&write_log, root, hash, Default::default(), 0); + + remote_tree.replace( + UrkelTree::make() + .with_capacity(0, 0) + .with_root(Root { + hash, + ..Default::default() + }) + .new(server.read_sync()), + ); + root = hash; + }; + + for op in ops { + match op.op { + tests::OpKind::Insert => { + let key = op.key.unwrap(); + let value = op.value.unwrap_or_default(); + + if let Some(ref mut remote_tree) = remote_tree { + remote_tree + .insert(Context::background(), &key, &value) + .expect("insert"); + } + + tree.insert(Context::background(), &key, &value) + .expect("insert"); + + commit_remote(&mut tree, &mut remote_tree); + } + tests::OpKind::Remove => { + let key = op.key.unwrap(); + + if let Some(ref mut remote_tree) = remote_tree { + remote_tree + .remove(Context::background(), &key) + .expect("remove"); + let value = remote_tree + .get(Context::background(), &key) + .expect("get (after remove)"); + assert!(value.is_none(), "get (after remove) should return None"); + } + + tree.remove(Context::background(), &key).expect("remove"); + let value = tree + .get(Context::background(), &key) + .expect("get (after remove)"); + assert!(value.is_none(), "get (after remove) should return None"); + + commit_remote(&mut tree, &mut remote_tree); + } + tests::OpKind::Get => { + let value = tree + .get(Context::background(), &op.key.unwrap()) + .expect("get"); + assert_eq!(value, op.value, "get should return the correct value"); + } + tests::OpKind::IteratorSeek => { + let key = op.key.unwrap(); + let expected_key = op.expected_key.as_ref(); + let value = op.value.as_ref(); + + if let Some(ref mut remote_tree) = remote_tree { + let mut it = remote_tree.iter(Context::background()); + it.seek(&key); + assert!(it.error().is_none(), "seek"); + + let item = Iterator::next(&mut it); + assert_eq!( + expected_key, + item.as_ref().map(|p| &p.0), + "iterator should be at correct key" + ); + assert_eq!( + value, + item.as_ref().map(|p| &p.1), + "iterator should be at correct value" + ); + } + + let mut it = tree.iter(Context::background()); + it.seek(&key); + assert!(it.error().is_none(), "seek"); + + let item = Iterator::next(&mut it); + assert_eq!( + expected_key, + item.as_ref().map(|p| &p.0), + "iterator should be at correct key" + ); + assert_eq!( + value, + item.as_ref().map(|p| &p.1), + "iterator should be at correct value" + ); + } + } + } +} + +#[test] +fn test_special_case_1() { + test_special_case_from_json("case-1.json") +} + +#[test] +fn test_special_case_2() { + test_special_case_from_json("case-2.json") +} + +#[test] +fn test_special_case_3() { + test_special_case_from_json("case-3.json") +} + +#[test] +fn test_special_case_4() { + test_special_case_from_json("case-4.json") +} + +#[test] +fn test_special_case_5() { + test_special_case_from_json("case-5.json") +}