Skip to content

Commit

Permalink
runtime/storage/mkvs: Test against Go test vectors
Browse files Browse the repository at this point in the history
  • Loading branch information
kostko committed Mar 24, 2020
1 parent 21a5216 commit 57cfbf2
Show file tree
Hide file tree
Showing 4 changed files with 221 additions and 2 deletions.
3 changes: 2 additions & 1 deletion go/storage/mkvs/urkel/tests/fixture.go
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
// Package tests contains helpers for testing MKVS trees.
package tests

const (
Expand All @@ -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"`
Expand Down
2 changes: 2 additions & 0 deletions runtime/src/storage/mkvs/urkel/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
67 changes: 67 additions & 0 deletions runtime/src/storage/mkvs/urkel/tests/mod.rs
Original file line number Diff line number Diff line change
@@ -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<Vec<u8>>,
/// Value that is inserted or that is expected for the given key during lookup.
#[serde(default, deserialize_with = "deserialize_base64")]
pub value: Option<Vec<u8>>,
/// Key that is expected for the given operation (e.g., iterator seek).
#[serde(default, deserialize_with = "deserialize_base64")]
pub expected_key: Option<Vec<u8>>,
}

/// A MKVS tree test vector (a series of tree operations).
pub type TestVector = Vec<Op>;

fn deserialize_base64<'de, D>(deserializer: D) -> Result<Option<Vec<u8>>, D::Error>
where
D: serde::de::Deserializer<'de>,
{
struct Base64Visitor;

impl<'de> serde::de::Visitor<'de> for Base64Visitor {
type Value = Option<Vec<u8>>;

fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
write!(formatter, "base64 ASCII text")
}

fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
base64::decode(v)
.map_err(serde::de::Error::custom)
.map(Some)
}

fn visit_none<E>(self) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(None)
}
}

deserializer.deserialize_str(Base64Visitor)
}
151 changes: 150 additions & 1 deletion runtime/src/storage/mkvs/urkel/tree/tree_test.rs
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -8,6 +9,7 @@ use crate::{
cache::*,
interop::{Driver, ProtocolServer},
sync::*,
tests,
tree::*,
},
LogEntry, LogEntryKind, WriteLog,
Expand Down Expand Up @@ -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<UrkelTree> = None;
let mut root = Hash::empty_hash();

let mut commit_remote = |tree: &mut UrkelTree, remote_tree: &mut Option<UrkelTree>| {
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")
}

0 comments on commit 57cfbf2

Please sign in to comment.