From 675a8658965c6c415855aaa8bd4ab7ad7799d803 Mon Sep 17 00:00:00 2001 From: Grisha Kruglov Date: Mon, 16 Jul 2018 13:58:34 -0700 Subject: [PATCH] Extract and improve test macros (#787) r=nalexander * Part 1: Extract low-level test framework into mentat_db::debug for re-use. * Part 2: Improve assert_matches!. This corrects an incorrect pattern: a conversion method taking &self but returning an owned value should be named like `to_FOO(&self) -> FOO`. (A reference-to-reference conversion should be named like `as_FOO(&self) -> &FOO`. A consuming conversion should be named like `into_FOO(self) -> FOO`.) In addition, this pushes the conversion via `to_edn` into the `assert_matches!` macro, which lets consumers get a real data structure (say, `Datoms`) and use it directly before or after `assert_matches!`. (Currently, consumers get back `edn::Value` instances, which aren't nearly as pleasant to use as real data structures.) Co-authored-by: Grisha Kruglov * Part 3: Use mentat_db::debug framework in Tolstoy crate. The advantage of this approach is that compiling Tolstoy (or anything that's not db, really) can be quite a bit faster than compiling db. --- db/src/db.rs | 187 ++------------------------------ db/src/debug.rs | 247 ++++++++++++++++++++++++++++++++++++++---- db/src/lib.rs | 3 +- tolstoy/Cargo.toml | 4 + tolstoy/src/lib.rs | 8 +- tolstoy/src/syncer.rs | 73 +++++++++++++ 6 files changed, 317 insertions(+), 205 deletions(-) diff --git a/db/src/db.rs b/db/src/db.rs index 2bd82766..e52a7384 100644 --- a/db/src/db.rs +++ b/db/src/db.rs @@ -426,7 +426,7 @@ impl TypedSQLValue for TypedValue { /// Read an arbitrary [e a v value_type_tag] materialized view from the given table in the SQL /// store. -fn read_materialized_view(conn: &rusqlite::Connection, table: &str) -> Result> { +pub(crate) fn read_materialized_view(conn: &rusqlite::Connection, table: &str) -> Result> { let mut stmt: rusqlite::Statement = conn.prepare(format!("SELECT e, a, v, value_type_tag FROM {}", table).as_str())?; let m: Result> = stmt.query_and_then(&[], |row| { let e: Entid = row.get_checked(0)?; @@ -449,7 +449,7 @@ fn read_partition_map(conn: &rusqlite::Connection) -> Result { } /// Read the ident map materialized view from the given SQL store. -fn read_ident_map(conn: &rusqlite::Connection) -> Result { +pub(crate) fn read_ident_map(conn: &rusqlite::Connection) -> Result { let v = read_materialized_view(conn, "idents")?; v.into_iter().map(|(e, a, typed_value)| { if a != entids::DB_IDENT { @@ -464,7 +464,7 @@ fn read_ident_map(conn: &rusqlite::Connection) -> Result { } /// Read the schema materialized view from the given SQL store. -fn read_attribute_map(conn: &rusqlite::Connection) -> Result { +pub(crate) fn read_attribute_map(conn: &rusqlite::Connection) -> Result { let entid_triples = read_materialized_view(conn, "schema")?; let mut attribute_map = AttributeMap::default(); metadata::update_attribute_map_from_entid_triples(&mut attribute_map, entid_triples, ::std::iter::empty())?; @@ -473,7 +473,7 @@ fn read_attribute_map(conn: &rusqlite::Connection) -> Result { /// Read the materialized views from the given SQL store and return a Mentat `DB` for querying and /// applying transactions. -pub fn read_db(conn: &rusqlite::Connection) -> Result { +pub(crate) fn read_db(conn: &rusqlite::Connection) -> Result { let partition_map = read_partition_map(conn)?; let ident_map = read_ident_map(conn)?; let attribute_map = read_attribute_map(conn)?; @@ -1115,201 +1115,28 @@ mod tests { extern crate env_logger; use super::*; - use bootstrap; - use debug; - use errors; - use edn; + use debug::{TestConn,tempids}; use edn::{ + self, InternSet, }; use edn::entities::{ OpType, - TempId, }; - use mentat_core::{ HasSchema, Keyword, KnownEntid, - TxReport, attribute, }; use mentat_core::util::Either::*; - use rusqlite; use std::collections::{ BTreeMap, }; + use errors; use internal_types::{ Term, - TermWithTempIds, }; - use tx::{ - transact_terms, - }; - - // Macro to parse a `Borrow` to an `edn::Value` and assert the given `edn::Value` `matches` - // against it. - // - // This is a macro only to give nice line numbers when tests fail. - macro_rules! assert_matches { - ( $input: expr, $expected: expr ) => {{ - // Failure to parse the expected pattern is a coding error, so we unwrap. - let pattern_value = edn::parse::value($expected.borrow()) - .expect(format!("to be able to parse expected {}", $expected).as_str()) - .without_spans(); - assert!($input.matches(&pattern_value), - "Expected value:\n{}\nto match pattern:\n{}\n", - $input.to_pretty(120).unwrap(), - pattern_value.to_pretty(120).unwrap()); - }} - } - - // Transact $input against the given $conn, expecting success or a `Result`. - // - // This unwraps safely and makes asserting errors pleasant. - macro_rules! assert_transact { - ( $conn: expr, $input: expr, $expected: expr ) => {{ - trace!("assert_transact: {}", $input); - let result = $conn.transact($input).map_err(|e| e.to_string()); - assert_eq!(result, $expected.map_err(|e| e.to_string())); - }}; - ( $conn: expr, $input: expr ) => {{ - trace!("assert_transact: {}", $input); - let result = $conn.transact($input); - assert!(result.is_ok(), "Expected Ok(_), got `{}`", result.unwrap_err()); - result.unwrap() - }}; - } - - // A connection that doesn't try to be clever about possibly sharing its `Schema`. Compare to - // `mentat::Conn`. - struct TestConn { - sqlite: rusqlite::Connection, - partition_map: PartitionMap, - schema: Schema, - } - - impl TestConn { - fn assert_materialized_views(&self) { - let materialized_ident_map = read_ident_map(&self.sqlite).expect("ident map"); - let materialized_attribute_map = read_attribute_map(&self.sqlite).expect("schema map"); - - let materialized_schema = Schema::from_ident_map_and_attribute_map(materialized_ident_map, materialized_attribute_map).expect("schema"); - assert_eq!(materialized_schema, self.schema); - } - - fn transact(&mut self, transaction: I) -> Result where I: Borrow { - // Failure to parse the transaction is a coding error, so we unwrap. - let entities = edn::parse::entities(transaction.borrow()).expect(format!("to be able to parse {} into entities", transaction.borrow()).as_str()); - - let details = { - // The block scopes the borrow of self.sqlite. - // We're about to write, so go straight ahead and get an IMMEDIATE transaction. - let tx = self.sqlite.transaction_with_behavior(TransactionBehavior::Immediate)?; - // Applying the transaction can fail, so we don't unwrap. - let details = transact(&tx, self.partition_map.clone(), &self.schema, &self.schema, NullWatcher(), entities)?; - tx.commit()?; - details - }; - - let (report, next_partition_map, next_schema, _watcher) = details; - self.partition_map = next_partition_map; - if let Some(next_schema) = next_schema { - self.schema = next_schema; - } - - // Verify that we've updated the materialized views during transacting. - self.assert_materialized_views(); - - Ok(report) - } - - fn transact_simple_terms(&mut self, terms: I, tempid_set: InternSet) -> Result where I: IntoIterator { - let details = { - // The block scopes the borrow of self.sqlite. - // We're about to write, so go straight ahead and get an IMMEDIATE transaction. - let tx = self.sqlite.transaction_with_behavior(TransactionBehavior::Immediate)?; - // Applying the transaction can fail, so we don't unwrap. - let details = transact_terms(&tx, self.partition_map.clone(), &self.schema, &self.schema, NullWatcher(), terms, tempid_set)?; - tx.commit()?; - details - }; - - let (report, next_partition_map, next_schema, _watcher) = details; - self.partition_map = next_partition_map; - if let Some(next_schema) = next_schema { - self.schema = next_schema; - } - - // Verify that we've updated the materialized views during transacting. - self.assert_materialized_views(); - - Ok(report) - } - - fn last_tx_id(&self) -> Entid { - self.partition_map.get(&":db.part/tx".to_string()).unwrap().index - 1 - } - - fn last_transaction(&self) -> edn::Value { - debug::transactions_after(&self.sqlite, &self.schema, self.last_tx_id() - 1).expect("last_transaction").0[0].into_edn() - } - - fn datoms(&self) -> edn::Value { - debug::datoms_after(&self.sqlite, &self.schema, bootstrap::TX0).expect("datoms").into_edn() - } - - fn fulltext_values(&self) -> edn::Value { - debug::fulltext_values(&self.sqlite).expect("fulltext_values").into_edn() - } - - fn with_sqlite(mut conn: rusqlite::Connection) -> TestConn { - let db = ensure_current_version(&mut conn).unwrap(); - - // Does not include :db/txInstant. - let datoms = debug::datoms_after(&conn, &db.schema, 0).unwrap(); - assert_eq!(datoms.0.len(), 94); - - // Includes :db/txInstant. - let transactions = debug::transactions_after(&conn, &db.schema, 0).unwrap(); - assert_eq!(transactions.0.len(), 1); - assert_eq!(transactions.0[0].0.len(), 95); - - let mut parts = db.partition_map; - - // Add a fake partition to allow tests to do things like - // [:db/add 111 :foo/bar 222] - { - let fake_partition = Partition { start: 100, end: 2000, index: 1000, allow_excision: true }; - parts.insert(":db.part/fake".into(), fake_partition); - } - - let test_conn = TestConn { - sqlite: conn, - partition_map: parts, - schema: db.schema, - }; - - // Verify that we've created the materialized views during bootstrapping. - test_conn.assert_materialized_views(); - - test_conn - } - } - - impl Default for TestConn { - fn default() -> TestConn { - TestConn::with_sqlite(new_connection("").expect("Couldn't open in-memory db")) - } - } - - fn tempids(report: &TxReport) -> edn::Value { - let mut map: BTreeMap = BTreeMap::default(); - for (tempid, &entid) in report.tempids.iter() { - map.insert(edn::Value::Text(tempid.clone()), edn::Value::Integer(entid)); - } - edn::Value::Map(map) - } fn run_test_add(mut conn: TestConn) { // Test inserting :db.cardinality/one elements. diff --git a/db/src/debug.rs b/db/src/debug.rs index 664c2460..4bc6c50d 100644 --- a/db/src/debug.rs +++ b/db/src/debug.rs @@ -9,45 +9,99 @@ // specific language governing permissions and limitations under the License. #![allow(dead_code)] +#![allow(unused_macros)] /// Low-level functions for testing. +// Macro to parse a `Borrow` to an `edn::Value` and assert the given `edn::Value` `matches` +// against it. +// +// This is a macro only to give nice line numbers when tests fail. +#[macro_export] +macro_rules! assert_matches { + ( $input: expr, $expected: expr ) => {{ + // Failure to parse the expected pattern is a coding error, so we unwrap. + let pattern_value = edn::parse::value($expected.borrow()) + .expect(format!("to be able to parse expected {}", $expected).as_str()) + .without_spans(); + let input_value = $input.to_edn(); + assert!(input_value.matches(&pattern_value), + "Expected value:\n{}\nto match pattern:\n{}\n", + input_value.to_pretty(120).unwrap(), + pattern_value.to_pretty(120).unwrap()); + }} +} + +// Transact $input against the given $conn, expecting success or a `Result`. +// +// This unwraps safely and makes asserting errors pleasant. +#[macro_export] +macro_rules! assert_transact { + ( $conn: expr, $input: expr, $expected: expr ) => {{ + trace!("assert_transact: {}", $input); + let result = $conn.transact($input).map_err(|e| e.to_string()); + assert_eq!(result, $expected.map_err(|e| e.to_string())); + }}; + ( $conn: expr, $input: expr ) => {{ + trace!("assert_transact: {}", $input); + let result = $conn.transact($input); + assert!(result.is_ok(), "Expected Ok(_), got `{}`", result.unwrap_err()); + result.unwrap() + }}; +} + use std::borrow::Borrow; +use std::collections::BTreeMap; use std::io::{Write}; use itertools::Itertools; use rusqlite; +use rusqlite::{TransactionBehavior}; use rusqlite::types::{ToSql}; use tabwriter::TabWriter; use bootstrap; -use db::TypedSQLValue; +use db::*; +use db::{read_attribute_map,read_ident_map}; use edn; use entids; use errors::Result; use mentat_core::{ HasSchema, SQLValueType, + TxReport, TypedValue, ValueType, }; +use edn::{ + InternSet, +}; use edn::entities::{ EntidOrIdent, + TempId, +}; +use internal_types::{ + TermWithTempIds, }; use schema::{ SchemaBuilding, }; -use types::Schema; +use types::*; +use tx::{ + transact, + transact_terms, +}; +use watcher::NullWatcher; /// Represents a *datom* (assertion) in the store. #[derive(Clone,Debug,Eq,Hash,Ord,PartialOrd,PartialEq)] -pub(crate) struct Datom { +pub struct Datom { // TODO: generalize this. - e: EntidOrIdent, - a: EntidOrIdent, - v: edn::Value, - tx: i64, - added: Option, + pub e: EntidOrIdent, + pub a: EntidOrIdent, + pub v: edn::Value, + pub tx: i64, + pub added: Option, } /// Represents a set of datoms (assertions) in the store. @@ -55,7 +109,7 @@ pub(crate) struct Datom { /// To make comparision easier, we deterministically order. The ordering is the ascending tuple /// ordering determined by `(e, a, (value_type_tag, v), tx)`, where `value_type_tag` is an internal /// value that is not exposed but is deterministic. -pub(crate) struct Datoms(pub Vec); +pub struct Datoms(pub Vec); /// Represents an ordered sequence of transactions in the store. /// @@ -63,13 +117,13 @@ pub(crate) struct Datoms(pub Vec); /// ordering determined by `(e, a, (value_type_tag, v), tx, added)`, where `value_type_tag` is an /// internal value that is not exposed but is deterministic, and `added` is ordered such that /// retracted assertions appear before added assertions. -pub(crate) struct Transactions(pub Vec); +pub struct Transactions(pub Vec); /// Represents the fulltext values in the store. -pub(crate) struct FulltextValues(pub Vec<(i64, String)>); +pub struct FulltextValues(pub Vec<(i64, String)>); impl Datom { - pub(crate) fn into_edn(&self) -> edn::Value { + pub fn to_edn(&self) -> edn::Value { let f = |entid: &EntidOrIdent| -> edn::Value { match *entid { EntidOrIdent::Entid(ref y) => edn::Value::Integer(y.clone()), @@ -88,19 +142,19 @@ impl Datom { } impl Datoms { - pub(crate) fn into_edn(&self) -> edn::Value { - edn::Value::Vector((&self.0).into_iter().map(|x| x.into_edn()).collect()) + pub fn to_edn(&self) -> edn::Value { + edn::Value::Vector((&self.0).into_iter().map(|x| x.to_edn()).collect()) } } impl Transactions { - pub(crate) fn into_edn(&self) -> edn::Value { - edn::Value::Vector((&self.0).into_iter().map(|x| x.into_edn()).collect()) + pub fn to_edn(&self) -> edn::Value { + edn::Value::Vector((&self.0).into_iter().map(|x| x.to_edn()).collect()) } } impl FulltextValues { - pub(crate) fn into_edn(&self) -> edn::Value { + pub fn to_edn(&self) -> edn::Value { edn::Value::Vector((&self.0).into_iter().map(|&(x, ref y)| edn::Value::Vector(vec![edn::Value::Integer(x), edn::Value::Text(y.clone())])).collect()) } } @@ -121,13 +175,18 @@ impl ToIdent for TypedValue { } /// Convert a numeric entid to an ident `Entid` if possible, otherwise a numeric `Entid`. -fn to_entid(schema: &Schema, entid: i64) -> EntidOrIdent { +pub fn to_entid(schema: &Schema, entid: i64) -> EntidOrIdent { schema.get_ident(entid).map_or(EntidOrIdent::Entid(entid), |ident| EntidOrIdent::Ident(ident.clone())) } +// /// Convert a symbolic ident to an ident `Entid` if possible, otherwise a numeric `Entid`. +// pub fn to_ident(schema: &Schema, entid: i64) -> Entid { +// schema.get_ident(entid).map_or(Entid::Entid(entid), |ident| Entid::Ident(ident.clone())) +// } + /// Return the set of datoms in the store, ordered by (e, a, v, tx), but not including any datoms of /// the form [... :db/txInstant ...]. -pub(crate) fn datoms>(conn: &rusqlite::Connection, schema: &S) -> Result { +pub fn datoms>(conn: &rusqlite::Connection, schema: &S) -> Result { datoms_after(conn, schema, bootstrap::TX0 - 1) } @@ -135,7 +194,7 @@ pub(crate) fn datoms>(conn: &rusqlite::Connection, schema: &S) /// ordered by (e, a, v, tx). /// /// The datom set returned does not include any datoms of the form [... :db/txInstant ...]. -pub(crate) fn datoms_after>(conn: &rusqlite::Connection, schema: &S, tx: i64) -> Result { +pub fn datoms_after>(conn: &rusqlite::Connection, schema: &S, tx: i64) -> Result { let borrowed_schema = schema.borrow(); let mut stmt: rusqlite::Statement = conn.prepare("SELECT e, a, v, value_type_tag, tx FROM datoms WHERE tx > ? ORDER BY e ASC, a ASC, value_type_tag ASC, v ASC, tx ASC")?; @@ -175,7 +234,7 @@ pub(crate) fn datoms_after>(conn: &rusqlite::Connection, schem /// given `tx`, ordered by (tx, e, a, v). /// /// Each transaction returned includes the [(transaction-tx) :db/txInstant ...] datom. -pub(crate) fn transactions_after>(conn: &rusqlite::Connection, schema: &S, tx: i64) -> Result { +pub fn transactions_after>(conn: &rusqlite::Connection, schema: &S, tx: i64) -> Result { let borrowed_schema = schema.borrow(); let mut stmt: rusqlite::Statement = conn.prepare("SELECT e, a, v, value_type_tag, tx, added FROM transactions WHERE tx > ? ORDER BY tx ASC, e ASC, a ASC, value_type_tag ASC, v ASC, added ASC")?; @@ -211,7 +270,7 @@ pub(crate) fn transactions_after>(conn: &rusqlite::Connection, } /// Return the set of fulltext values in the store, ordered by rowid. -pub(crate) fn fulltext_values(conn: &rusqlite::Connection) -> Result { +pub fn fulltext_values(conn: &rusqlite::Connection) -> Result { let mut stmt: rusqlite::Statement = conn.prepare("SELECT rowid, text FROM fulltext_values ORDER BY rowid")?; let r: Result> = stmt.query_and_then(&[], |row| { @@ -228,7 +287,7 @@ pub(crate) fn fulltext_values(conn: &rusqlite::Connection) -> Result Result { +pub fn dump_sql_query(conn: &rusqlite::Connection, sql: &str, params: &[&ToSql]) -> Result { let mut stmt: rusqlite::Statement = conn.prepare(sql)?; let mut tw = TabWriter::new(Vec::new()).padding(2); @@ -252,3 +311,145 @@ pub(crate) fn dump_sql_query(conn: &rusqlite::Connection, sql: &str, params: &[& let dump = String::from_utf8(tw.into_inner().unwrap()).unwrap(); Ok(dump) } + +// A connection that doesn't try to be clever about possibly sharing its `Schema`. Compare to +// `mentat::Conn`. +pub struct TestConn { + pub sqlite: rusqlite::Connection, + pub partition_map: PartitionMap, + pub schema: Schema, +} + +impl TestConn { + fn assert_materialized_views(&self) { + let materialized_ident_map = read_ident_map(&self.sqlite).expect("ident map"); + let materialized_attribute_map = read_attribute_map(&self.sqlite).expect("schema map"); + + let materialized_schema = Schema::from_ident_map_and_attribute_map(materialized_ident_map, materialized_attribute_map).expect("schema"); + assert_eq!(materialized_schema, self.schema); + } + + pub fn transact(&mut self, transaction: I) -> Result where I: Borrow { + // Failure to parse the transaction is a coding error, so we unwrap. + let entities = edn::parse::entities(transaction.borrow()).expect(format!("to be able to parse {} into entities", transaction.borrow()).as_str()); + + let details = { + // The block scopes the borrow of self.sqlite. + // We're about to write, so go straight ahead and get an IMMEDIATE transaction. + let tx = self.sqlite.transaction_with_behavior(TransactionBehavior::Immediate)?; + // Applying the transaction can fail, so we don't unwrap. + let details = transact(&tx, self.partition_map.clone(), &self.schema, &self.schema, NullWatcher(), entities)?; + tx.commit()?; + details + }; + + let (report, next_partition_map, next_schema, _watcher) = details; + self.partition_map = next_partition_map; + if let Some(next_schema) = next_schema { + self.schema = next_schema; + } + + // Verify that we've updated the materialized views during transacting. + self.assert_materialized_views(); + + Ok(report) + } + + pub fn transact_simple_terms(&mut self, terms: I, tempid_set: InternSet) -> Result where I: IntoIterator { + let details = { + // The block scopes the borrow of self.sqlite. + // We're about to write, so go straight ahead and get an IMMEDIATE transaction. + let tx = self.sqlite.transaction_with_behavior(TransactionBehavior::Immediate)?; + // Applying the transaction can fail, so we don't unwrap. + let details = transact_terms(&tx, self.partition_map.clone(), &self.schema, &self.schema, NullWatcher(), terms, tempid_set)?; + tx.commit()?; + details + }; + + let (report, next_partition_map, next_schema, _watcher) = details; + self.partition_map = next_partition_map; + if let Some(next_schema) = next_schema { + self.schema = next_schema; + } + + // Verify that we've updated the materialized views during transacting. + self.assert_materialized_views(); + + Ok(report) + } + + pub fn last_tx_id(&self) -> Entid { + self.partition_map.get(&":db.part/tx".to_string()).unwrap().index - 1 + } + + pub fn last_transaction(&self) -> Datoms { + transactions_after(&self.sqlite, &self.schema, self.last_tx_id() - 1).expect("last_transaction").0.pop().unwrap() + } + + pub fn transactions(&self) -> Transactions { + transactions_after(&self.sqlite, &self.schema, bootstrap::TX0).expect("transactions") + } + + pub fn datoms(&self) -> Datoms { + datoms_after(&self.sqlite, &self.schema, bootstrap::TX0).expect("datoms") + } + + pub fn fulltext_values(&self) -> FulltextValues { + fulltext_values(&self.sqlite).expect("fulltext_values") + } + + pub fn with_sqlite(mut conn: rusqlite::Connection) -> TestConn { + let db = ensure_current_version(&mut conn).unwrap(); + + // Does not include :db/txInstant. + let datoms = datoms_after(&conn, &db.schema, 0).unwrap(); + assert_eq!(datoms.0.len(), 94); + + // Includes :db/txInstant. + let transactions = transactions_after(&conn, &db.schema, 0).unwrap(); + assert_eq!(transactions.0.len(), 1); + assert_eq!(transactions.0[0].0.len(), 95); + + let mut parts = db.partition_map; + + // Add a fake partition to allow tests to do things like + // [:db/add 111 :foo/bar 222] + { + let fake_partition = Partition { start: 100, end: 2000, index: 1000, allow_excision: true }; + parts.insert(":db.part/fake".into(), fake_partition); + } + + let test_conn = TestConn { + sqlite: conn, + partition_map: parts, + schema: db.schema, + }; + + // Verify that we've created the materialized views during bootstrapping. + test_conn.assert_materialized_views(); + + test_conn + } +} + +impl Default for TestConn { + fn default() -> TestConn { + TestConn::with_sqlite(new_connection("").expect("Couldn't open in-memory db")) + } +} + +pub struct TempIds(edn::Value); + +impl TempIds { + pub fn to_edn(&self) -> edn::Value { + self.0.clone() + } +} + +pub fn tempids(report: &TxReport) -> TempIds { + let mut map: BTreeMap = BTreeMap::default(); + for (tempid, &entid) in report.tempids.iter() { + map.insert(edn::Value::Text(tempid.clone()), edn::Value::Integer(entid)); + } + TempIds(edn::Value::Map(map)) +} diff --git a/db/src/lib.rs b/db/src/lib.rs index f88aabf5..27cd0943 100644 --- a/db/src/lib.rs +++ b/db/src/lib.rs @@ -39,11 +39,12 @@ pub use errors::{ }; #[macro_use] pub mod errors; +#[macro_use] pub mod debug; + mod add_retract_alter_set; pub mod cache; pub mod db; mod bootstrap; -pub mod debug; pub mod entids; pub mod internal_types; // pub because we need them for building entities programmatically. mod metadata; diff --git a/tolstoy/Cargo.toml b/tolstoy/Cargo.toml index d864ef7d..cd10b340 100644 --- a/tolstoy/Cargo.toml +++ b/tolstoy/Cargo.toml @@ -9,6 +9,7 @@ failure = "0.1.1" failure_derive = "0.1.1" futures = "0.1" hyper = "0.11" +log = "0.4" tokio-core = "0.1" serde = "1.0" serde_json = "1.0" @@ -17,6 +18,9 @@ serde_derive = "1.0" lazy_static = "0.2" uuid = { version = "0.5", features = ["v4", "serde"] } +[dependencies.edn] +path = "../edn" + [dependencies.mentat_core] path = "../core" diff --git a/tolstoy/src/lib.rs b/tolstoy/src/lib.rs index d4293c64..d41b1455 100644 --- a/tolstoy/src/lib.rs +++ b/tolstoy/src/lib.rs @@ -18,6 +18,8 @@ extern crate lazy_static; #[macro_use] extern crate serde_derive; +extern crate edn; + extern crate hyper; // TODO https://github.com/mozilla/mentat/issues/569 // extern crate hyper_tls; @@ -26,7 +28,11 @@ extern crate futures; extern crate serde; extern crate serde_cbor; extern crate serde_json; -extern crate mentat_db; + +// See https://github.com/rust-lang/rust/issues/44342#issuecomment-376010077. +#[cfg_attr(test, macro_use)] extern crate log; +#[cfg_attr(test, macro_use)] extern crate mentat_db; + extern crate mentat_core; extern crate rusqlite; extern crate uuid; diff --git a/tolstoy/src/syncer.rs b/tolstoy/src/syncer.rs index 7abc296d..9bc90fb1 100644 --- a/tolstoy/src/syncer.rs +++ b/tolstoy/src/syncer.rs @@ -413,8 +413,13 @@ impl RemoteClient { #[cfg(test)] mod tests { use super::*; + use std::borrow::Borrow; use std::str::FromStr; + use edn; + + use mentat_db::debug::{TestConn}; + #[test] fn test_remote_client_bound_uri() { let user_uuid = Uuid::from_str(&"316ea470-ce35-4adf-9c61-e0de6e289c59").expect("uuid"); @@ -422,4 +427,72 @@ mod tests { let remote_client = RemoteClient::new(server_uri, user_uuid); assert_eq!("https://example.com/api/0.1/316ea470-ce35-4adf-9c61-e0de6e289c59", remote_client.bound_base_uri()); } + + #[test] + fn test_add() { + let mut conn = TestConn::default(); + + // Test inserting :db.cardinality/one elements. + assert_transact!(conn, "[[:db/add 100 :db.schema/version 1] + [:db/add 101 :db.schema/version 2]]"); + assert_matches!(conn.last_transaction(), + "[[100 :db.schema/version 1 ?tx true] + [101 :db.schema/version 2 ?tx true] + [?tx :db/txInstant ?ms ?tx true]]"); + assert_matches!(conn.datoms(), + "[[100 :db.schema/version 1] + [101 :db.schema/version 2]]"); + + // Test inserting :db.cardinality/many elements. + assert_transact!(conn, "[[:db/add 200 :db.schema/attribute 100] + [:db/add 200 :db.schema/attribute 101]]"); + assert_matches!(conn.last_transaction(), + "[[200 :db.schema/attribute 100 ?tx true] + [200 :db.schema/attribute 101 ?tx true] + [?tx :db/txInstant ?ms ?tx true]]"); + assert_matches!(conn.datoms(), + "[[100 :db.schema/version 1] + [101 :db.schema/version 2] + [200 :db.schema/attribute 100] + [200 :db.schema/attribute 101]]"); + + // Test replacing existing :db.cardinality/one elements. + assert_transact!(conn, "[[:db/add 100 :db.schema/version 11] + [:db/add 101 :db.schema/version 22]]"); + assert_matches!(conn.last_transaction(), + "[[100 :db.schema/version 1 ?tx false] + [100 :db.schema/version 11 ?tx true] + [101 :db.schema/version 2 ?tx false] + [101 :db.schema/version 22 ?tx true] + [?tx :db/txInstant ?ms ?tx true]]"); + assert_matches!(conn.datoms(), + "[[100 :db.schema/version 11] + [101 :db.schema/version 22] + [200 :db.schema/attribute 100] + [200 :db.schema/attribute 101]]"); + + + // Test that asserting existing :db.cardinality/one elements doesn't change the store. + assert_transact!(conn, "[[:db/add 100 :db.schema/version 11] + [:db/add 101 :db.schema/version 22]]"); + assert_matches!(conn.last_transaction(), + "[[?tx :db/txInstant ?ms ?tx true]]"); + assert_matches!(conn.datoms(), + "[[100 :db.schema/version 11] + [101 :db.schema/version 22] + [200 :db.schema/attribute 100] + [200 :db.schema/attribute 101]]"); + + + // Test that asserting existing :db.cardinality/many elements doesn't change the store. + assert_transact!(conn, "[[:db/add 200 :db.schema/attribute 100] + [:db/add 200 :db.schema/attribute 101]]"); + assert_matches!(conn.last_transaction(), + "[[?tx :db/txInstant ?ms ?tx true]]"); + assert_matches!(conn.datoms(), + "[[100 :db.schema/version 11] + [101 :db.schema/version 22] + [200 :db.schema/attribute 100] + [200 :db.schema/attribute 101]]"); + } }