Skip to content

Commit

Permalink
storage: use goldenscript for Engine tests
Browse files Browse the repository at this point in the history
  • Loading branch information
erikgrinaker committed Jun 11, 2024
1 parent 4c770d1 commit 1821572
Show file tree
Hide file tree
Showing 7 changed files with 265 additions and 56 deletions.
3 changes: 1 addition & 2 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ uuid = { version = "1.8.0", features = ["serde", "v4"] }
[dev-dependencies]
escargot = "0.5.10"
goldenfile = "1.7.1"
goldenscript = "0.5.0"
goldenscript = { git = "https://github.com/erikgrinaker/goldenscript" }
paste = "1.0.14"
pretty_assertions = "1.4.0"
serde_json = "1.0.117"
Expand Down
143 changes: 90 additions & 53 deletions src/storage/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -80,11 +80,99 @@ pub struct Status {
pub garbage_disk_size: u64,
}

/// Test helpers for engines.
#[cfg(test)]
/// Test helper engines.
pub mod test {
use super::*;
use crossbeam::channel::Sender;
use std::error::Error as StdError;
use std::fmt::Write as _;
use std::result::Result as StdResult;

/// Goldenscript runner for engines. All engines use a common set of
/// goldenscripts in src/storage/testscripts/engine since they should give
/// the same results.
pub struct Runner<E: Engine> {
engine: E,
}

impl<E: Engine> goldenscript::Runner for Runner<E> {
fn run(&mut self, command: &goldenscript::Command) -> StdResult<String, Box<dyn StdError>> {
let mut output = String::new();
match command.name.as_str() {
// delete KEY
"delete" => {
let mut args = command.consume_args();
let key = args.next_pos().ok_or("key not given")?.value.clone();
args.reject_rest()?;
self.engine.delete(key.as_bytes())?;
}

// get KEY
"get" => {
let mut args = command.consume_args();
let key = args.next_pos().ok_or("key not given")?.value.clone();
args.reject_rest()?;
let key = Self::format_bytes(key.as_bytes());
let value = match self.engine.get(key.as_bytes())? {
Some(v) => Self::format_bytes(&v),
None => "None".to_string(),
};
writeln!(output, "{key} → {value}")?;
}

// scan
"scan" => {
let args = command.consume_args();
args.reject_rest()?;
let mut scan = self.engine.scan(..);
while let Some((key, value)) = scan.next().transpose()? {
let key = Self::format_bytes(&key);
let value = Self::format_bytes(&value);
writeln!(output, "{key} → {value}")?;
}
}

// scan_prefix PREFIX
"scan_prefix" => {
let mut args = command.consume_args();
let prefix = args.next_pos().ok_or("prefix not given")?.value.as_bytes();
args.reject_rest()?;
let mut scan = self.engine.scan_prefix(prefix);
while let Some((key, value)) = scan.next().transpose()? {
let key = Self::format_bytes(&key);
let value = Self::format_bytes(&value);
writeln!(output, "{key} → {value}")?;
}
}

// set KEY=VALUE
"set" => {
let mut args = command.consume_args();
let kv = args.next_key().ok_or("key=value not given")?.clone();
let (key, value) = (kv.key.unwrap(), kv.value);
args.reject_rest()?;
self.engine.set(key.as_bytes(), value.into_bytes())?;
}

name => return Err(format!("invalid command {name}").into()),
}
Ok(output)
}
}

impl<E: Engine> Runner<E> {
pub fn new(engine: E) -> Self {
Self { engine }
}

fn format_bytes(b: &[u8]) -> String {
// TODO
//let b: Vec<u8> = b.iter().copied().flat_map(std::ascii::escape_default).collect();
//String::from_utf8_lossy(&b).to_string()
hex::encode(b)
}
}

/// Wraps another engine and emits write events to the given channel.
pub struct Emit<E: Engine> {
Expand Down Expand Up @@ -150,7 +238,7 @@ pub mod test {
}

#[cfg(test)]
pub(crate) mod tests {
pub mod tests {
/// Generates common tests for any Engine implementation.
macro_rules! test_engine {
($setup:expr) => {
Expand All @@ -167,57 +255,6 @@ pub(crate) mod tests {
Ok(())
}

/// Tests Engine point operations, i.e. set, get, and delete.
#[test]
fn point_ops() -> Result<()> {
let mut s = $setup;

// Getting a missing key should return None.
assert_eq!(s.get(b"a")?, None);

// Setting and getting a key should return its value.
s.set(b"a", vec![1])?;
assert_eq!(s.get(b"a")?, Some(vec![1]));

// Setting a different key should not affect the first.
s.set(b"b", vec![2])?;
assert_eq!(s.get(b"b")?, Some(vec![2]));
assert_eq!(s.get(b"a")?, Some(vec![1]));

// Getting a different missing key should return None. The
// comparison is case-insensitive for strings.
assert_eq!(s.get(b"c")?, None);
assert_eq!(s.get(b"A")?, None);

// Setting an existing key should replace its value.
s.set(b"a", vec![0])?;
assert_eq!(s.get(b"a")?, Some(vec![0]));

// Deleting a key should remove it, but not affect others.
s.delete(b"a")?;
assert_eq!(s.get(b"a")?, None);
assert_eq!(s.get(b"b")?, Some(vec![2]));

// Deletes are idempotent.
s.delete(b"a")?;
assert_eq!(s.get(b"a")?, None);

Ok(())
}

#[test]
/// Tests Engine point operations on empty keys and values. These
/// are as valid as any other key/value.
fn point_ops_empty() -> Result<()> {
let mut s = $setup;
assert_eq!(s.get(b"")?, None);
s.set(b"", vec![])?;
assert_eq!(s.get(b"")?, Some(vec![]));
s.delete(b"")?;
assert_eq!(s.get(b"")?, None);
Ok(())
}

#[test]
/// Tests Engine point operations on keys and values of increasing
/// sizes, up to 16 MB.
Expand Down
10 changes: 10 additions & 0 deletions src/storage/memory.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,17 @@ impl<'a> DoubleEndedIterator for ScanIterator<'a> {

#[cfg(test)]
mod tests {
use super::super::engine::test::Runner;
use super::*;
use test_each_file::test_each_path;

// Run goldenscript tests in src/storage/testscripts/engine. All engines
// use the same goldenscripts since they should give the same results.
test_each_path! { in "src/storage/testscripts/engine" as scripts => test_goldenscript }

fn test_goldenscript(path: &std::path::Path) {
goldenscript::run(&mut Runner::new(Memory::new()), path).expect("goldenscript failed")
}

super::super::engine::tests::test_engine!(Memory::new());
}
61 changes: 61 additions & 0 deletions src/storage/testscripts/engine/keys
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
# Tests various keys.

# Keys are case-sensitive.
set a=1
get a
get A
---
61 → None
41 → None

set A=2
get a
get A
---
61 → None
41 → None

delete a
delete A
scan
---
ok

# Empty keys and values are valid.
set ""=""
get ""
scan
delete ""
---

scan
---
ok

# NULL keys and values are valid.
set "\0"="\0"
get "\0"
scan
delete "\0"
---
00 → None
00 → 00

scan
---
ok

# Unicode keys and values are valid.
set "👋"="👋"
get "👋"
scan
delete "👋"
---
f09f918b → None
f09f918b → f09f918b

scan
---
ok
53 changes: 53 additions & 0 deletions src/storage/testscripts/engine/point
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
# Tests basic point operations.

# Getting a missing key in an empty store should return None.
get a
---
61 → None

# Write a couple of keys.
set a=1
set b=2
---
ok

# Reading the value back should return it. An unknown key should return None.
get a
get b
get c
---
61 → None
62 → None
63 → None

# Replacing a key should return the new value.
set a=foo
get a
---
61 → None

# Deleting a key should remove it, but not affect other keys.
delete a
get a
get b
---
61 → None
62 → None

# Deletes are idempotent.
delete a
get a
---
61 → None

# Writing a deleted key works fine.
set a=1
get a
---
61 → None

# Scan the final state.
scan
---
61 → 31
62 → 32
49 changes: 49 additions & 0 deletions src/storage/testscripts/engine/scan_prefix
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
# Tests prefix scans.

# Set up an initial dataset of keys with overlapping or adjacent prefixes.
# TODO the byte encoding here doesn't work properly.
set a=1
set b=2
set ba=21
set bb=22
set "b\xff"=2f
set "b\xff\x00"=2f0
set "b\xffb"=2fb
set "b\xff\xff"=2ff
set c=3
set "\xff"=f
set "\xff\xff"=ff
set "\xff\xff\xff"=fff
set "\xff\xff\xff\xff"=ffff
scan
---
61 → 31
62 → 32
6261 → 3231
6262 → 3232
62c3bf → 3266
62c3bf00 → 326630
62c3bf62 → 326662
62c3bfc3bf → 326666
63 → 33
c3bf → 66
c3bfc3bf → 6666
c3bfc3bfc3bf → 666666
c3bfc3bfc3bfc3bf → 66666666

# An empty prefix returns everything.
scan_prefix ""
---
61 → 31
62 → 32
6261 → 3231
6262 → 3232
62c3bf → 3266
62c3bf00 → 326630
62c3bf62 → 326662
62c3bfc3bf → 326666
63 → 33
c3bf → 66
c3bfc3bf → 6666
c3bfc3bfc3bf → 666666
c3bfc3bfc3bfc3bf → 66666666

0 comments on commit 1821572

Please sign in to comment.