use proptest to check invariants

This commit is contained in:
Dustin J. Mitchell
2019-12-28 14:31:37 -05:00
parent 8799636c1a
commit 0a2293a9c5
6 changed files with 448 additions and 46 deletions

View File

@@ -4,3 +4,6 @@
mod errors;
mod operation;
mod taskdb;
pub use operation::Operation;
pub use taskdb::DB;

View File

@@ -72,6 +72,7 @@ impl Operation {
(None, Some(operation2))
} else if timestamp1 > timestamp2 {
// prefer the later modification
//(Some(operation1), None)
(Some(operation1), None)
} else {
// arbitrarily resolve in favor of the first operation
@@ -89,6 +90,10 @@ impl Operation {
mod test {
use super::*;
use crate::taskdb::DB;
use chrono::{Duration, Utc};
// note that `tests/operation_transform_invariant.rs` tests the transform function quite
// thoroughly, so this testing is light.
fn test_transform(
o1: Operation,
@@ -102,14 +107,14 @@ mod test {
// check that the two operation sequences have the same effect, enforcing the invariant of
// the transform function.
let mut db1 = DB::new();
db1.apply(o1).unwrap();
db1.apply(o1);
if let Some(o) = o2p {
db1.apply(o).unwrap();
db1.apply(o);
}
let mut db2 = DB::new();
db2.apply(o2).unwrap();
db2.apply(o2);
if let Some(o) = o1p {
db2.apply(o).unwrap();
db2.apply(o);
}
assert_eq!(db1.tasks(), db2.tasks());
}
@@ -120,10 +125,100 @@ mod test {
let uuid2 = Uuid::new_v4();
test_transform(
Operation::Create { uuid: uuid1 },
Operation::Create { uuid: uuid2 },
Some(Operation::Create { uuid: uuid1 }),
Some(Operation::Create { uuid: uuid2 }),
Create { uuid: uuid1 },
Create { uuid: uuid2 },
Some(Create { uuid: uuid1 }),
Some(Create { uuid: uuid2 }),
);
}
#[test]
fn test_related_updates_different_props() {
let uuid = Uuid::new_v4();
let timestamp = Utc::now();
test_transform(
Update {
uuid,
property: "abc".into(),
value: true.into(),
timestamp,
},
Update {
uuid,
property: "def".into(),
value: false.into(),
timestamp,
},
Some(Update {
uuid,
property: "abc".into(),
value: true.into(),
timestamp,
}),
Some(Update {
uuid,
property: "def".into(),
value: false.into(),
timestamp,
}),
);
}
#[test]
fn test_related_updates_same_prop() {
let uuid = Uuid::new_v4();
let timestamp1 = Utc::now();
let timestamp2 = timestamp1 + Duration::seconds(10);
test_transform(
Update {
uuid,
property: "abc".into(),
value: true.into(),
timestamp: timestamp1,
},
Update {
uuid,
property: "abc".into(),
value: false.into(),
timestamp: timestamp2,
},
None,
Some(Update {
uuid,
property: "abc".into(),
value: false.into(),
timestamp: timestamp2,
}),
);
}
#[test]
fn test_related_updates_same_prop_same_time() {
let uuid = Uuid::new_v4();
let timestamp = Utc::now();
test_transform(
Update {
uuid,
property: "abc".into(),
value: true.into(),
timestamp,
},
Update {
uuid,
property: "abc".into(),
value: false.into(),
timestamp,
},
Some(Update {
uuid,
property: "abc".into(),
value: true.into(),
timestamp,
}),
None,
);
}
}

View File

@@ -1,11 +1,10 @@
use crate::errors::Error;
use crate::operation::Operation;
use serde_json::Value;
use std::collections::hash_map::Entry;
use std::collections::HashMap;
use uuid::Uuid;
#[derive(PartialEq, Debug)]
#[derive(PartialEq, Debug, Clone)]
pub struct DB {
// The current state, with all pending operations applied
tasks: HashMap<Uuid, HashMap<String, Value>>,
@@ -30,18 +29,15 @@ impl DB {
}
/// Apply an operation to the DB. Aside from synchronization operations, this
/// is the only way to modify the DB.
pub fn apply(&mut self, op: Operation) -> Result<(), Error> {
/// is the only way to modify the DB. In cases where an operation does not
/// make sense, this function will ignore the operation.
pub fn apply(&mut self, op: Operation) {
match op {
Operation::Create { uuid } => {
match self.tasks.entry(uuid) {
Entry::Occupied(_) => {
return Err(Error::DBError(format!("Task {} already exists", uuid)));
}
ent @ Entry::Vacant(_) => {
ent.or_insert(HashMap::new());
}
};
// insert if the task does not already exist
if let ent @ Entry::Vacant(_) = self.tasks.entry(uuid) {
ent.or_insert(HashMap::new());
}
}
Operation::Update {
ref uuid,
@@ -49,18 +45,13 @@ impl DB {
ref value,
timestamp: _,
} => {
match self.tasks.get_mut(uuid) {
Some(task) => {
task.insert(property.clone(), value.clone());
}
None => {
return Err(Error::DBError(format!("Task {} does not exist", uuid)));
}
};
// update if this task exists, otherwise ignore
if let Some(task) = self.tasks.get_mut(uuid) {
task.insert(property.clone(), value.clone());
}
}
};
self.operations.push(op);
Ok(())
}
/// Get a read-only reference to the underlying set of tasks.
@@ -82,7 +73,7 @@ mod tests {
let mut db = DB::new();
let uuid = Uuid::new_v4();
let op = Operation::Create { uuid };
db.apply(op.clone()).unwrap();
db.apply(op.clone());
let mut exp = HashMap::new();
exp.insert(uuid, HashMap::new());
@@ -94,11 +85,14 @@ mod tests {
fn test_apply_create_exists() {
let mut db = DB::new();
let uuid = Uuid::new_v4();
db.apply(Operation::Create { uuid }).unwrap();
assert_eq!(
db.apply(Operation::Create { uuid }),
Err(Error::DBError(format!("Task {} already exists", uuid)))
);
let op = Operation::Create { uuid };
db.apply(op.clone());
db.apply(op.clone());
let mut exp = HashMap::new();
exp.insert(uuid, HashMap::new());
assert_eq!(db.tasks(), &exp);
assert_eq!(db.operations, vec![op.clone(), op]);
}
#[test]
@@ -106,14 +100,14 @@ mod tests {
let mut db = DB::new();
let uuid = Uuid::new_v4();
let op1 = Operation::Create { uuid };
db.apply(op1.clone()).unwrap();
db.apply(op1.clone());
let op2 = Operation::Update {
uuid,
property: String::from("title"),
value: Value::from("\"my task\""),
timestamp: Utc::now(),
};
db.apply(op2.clone()).unwrap();
db.apply(op2.clone());
let mut exp = HashMap::new();
let mut task = HashMap::new();
@@ -127,14 +121,15 @@ mod tests {
fn test_apply_update_does_not_exist() {
let mut db = DB::new();
let uuid = Uuid::new_v4();
assert_eq!(
db.apply(Operation::Update {
uuid,
property: String::from("title"),
value: Value::from("\"my task\""),
timestamp: Utc::now(),
}),
Err(Error::DBError(format!("Task {} does not exist", uuid)))
);
let op = Operation::Update {
uuid,
property: String::from("title"),
value: Value::from("\"my task\""),
timestamp: Utc::now(),
};
db.apply(op.clone());
assert_eq!(db.tasks(), &HashMap::new());
assert_eq!(db.operations, vec![op]);
}
}