diff options
| -rw-r--r-- | .travis.yml | 4 | ||||
| -rw-r--r-- | Cargo.toml | 12 | ||||
| -rw-r--r-- | src/database.rs | 24 | ||||
| -rw-r--r-- | src/query.rs | 7 | ||||
| -rw-r--r-- | src/tags.rs | 12 | ||||
| -rw-r--r-- | tests/fixtures.rs | 216 | ||||
| -rw-r--r-- | tests/lib.rs | 11 | ||||
| -rw-r--r-- | tests/main.rs | 91 | ||||
| -rw-r--r-- | tests/test_database.rs | 337 |
9 files changed, 669 insertions, 45 deletions
diff --git a/.travis.yml b/.travis.yml index ab21b0a..b4c0414 100644 --- a/.travis.yml +++ b/.travis.yml @@ -9,7 +9,11 @@ addons: apt: packages: - libnotmuch-dev + - notmuch + - git script: - cargo build --no-default-features --verbose --all + # clone notmuch to have mail corpora + - git clone git://git.notmuchmail.org/git/notmuch /tmp/notmuch - cargo test --no-default-features --verbose --all @@ -7,6 +7,7 @@ repository = "https://github.com/vhdirk/notmuch-rs" description = "Rust interface and bindings for notmuch" license = "GPL-3.0+" readme = "README.md" +keywords = ["email", "notmuch"] [badges] travis-ci = { repository = "vhdirk/notmuch-rs" } @@ -18,13 +19,18 @@ supercow = "0.1.0" [dev-dependencies] dirs = "1.0" +tempfile = "3" +gethostname = "0.2.0" +maildir = "0.3.2" +lettre = "0.9.2" +lettre_email = "0.9.2" [features] v0_21 = [] v0_26 = ["v0_21"] default = ["v0_26"] - [[test]] -name = "main" -harness = false +name = "tests" +path = "tests/lib.rs" +harness = true
\ No newline at end of file diff --git a/src/database.rs b/src/database.rs index 53ba76b..44ad040 100644 --- a/src/database.rs +++ b/src/database.rs @@ -6,6 +6,7 @@ use std::ptr; use supercow::Supercow; use libc; +use std::cmp::{PartialEq, PartialOrd, Ordering}; use error::{Error, Result}; use ffi; @@ -25,8 +26,6 @@ use utils::ScopedSupercow; // Re-exported under database module for pretty namespacin'. pub use ffi::DatabaseMode; -#[derive(Copy, Clone, Debug)] -pub struct Version(libc::c_uint); #[derive(Clone, Debug)] pub struct Revision { @@ -34,6 +33,21 @@ pub struct Revision { pub uuid: String, } +impl PartialEq for Revision { + fn eq(&self, other: &Revision) -> bool{ + self.uuid == other.uuid && self.revision == other.revision + } +} + +impl PartialOrd for Revision { + fn partial_cmp(&self, other: &Revision) -> Option<Ordering>{ + if self.uuid != other.uuid { + return None; + } + self.revision.partial_cmp(&other.revision) + } +} + #[derive(Debug)] pub struct Database { @@ -79,7 +93,7 @@ impl Database { }) } - pub fn close(&mut self) -> Result<()> { + pub fn close(&self) -> Result<()> { unsafe { ffi::notmuch_database_close(self.ptr) }.as_result()?; Ok(()) @@ -143,8 +157,8 @@ impl Database { ) } - pub fn version(&self) -> Version { - Version(unsafe { ffi::notmuch_database_get_version(self.ptr) }) + pub fn version(&self) -> u32 { + unsafe { ffi::notmuch_database_get_version(self.ptr) } } #[cfg(feature = "v0_21")] diff --git a/src/query.rs b/src/query.rs index 0ea5268..50b56e5 100644 --- a/src/query.rs +++ b/src/query.rs @@ -46,6 +46,13 @@ impl<'d> Query<'d> { <Database as DatabaseExt>::create_query(db, query_string) } + pub fn query_string(self: &Self) -> String { + let qstring = unsafe { + CStr::from_ptr(ffi::notmuch_query_get_query_string(self.ptr)) + }; + qstring.to_str().unwrap().to_string() + } + /// Specify the sorting desired for this query. pub fn set_sort(self: &Self, sort: Sort) { unsafe { ffi::notmuch_query_set_sort(self.ptr, sort.into()) } diff --git a/src/tags.rs b/src/tags.rs index 3fb1613..40a45c8 100644 --- a/src/tags.rs +++ b/src/tags.rs @@ -1,3 +1,4 @@ +use std::cmp::PartialEq; use std::ffi::CStr; use std::iter::Iterator; use std::ops::Drop; @@ -11,7 +12,7 @@ pub trait TagsOwner {} pub struct Tags<'o, O> where O: TagsOwner + 'o, { - ptr: *mut ffi::notmuch_tags_t, + pub(crate) ptr: *mut ffi::notmuch_tags_t, marker: ScopedPhantomcow<'o, O>, } @@ -24,6 +25,15 @@ where } } +impl<'o, O> PartialEq for Tags<'o, O> +where + O: TagsOwner + 'o, +{ + fn eq(&self, other: &Self) -> bool { + self.ptr == other.ptr + } +} + impl<'o, O> Tags<'o, O> where O: TagsOwner + 'o, diff --git a/tests/fixtures.rs b/tests/fixtures.rs new file mode 100644 index 0000000..8887da8 --- /dev/null +++ b/tests/fixtures.rs @@ -0,0 +1,216 @@ +extern crate dirs; +extern crate tempfile; +extern crate notmuch; +extern crate gethostname; +extern crate maildir; +extern crate lettre; +extern crate lettre_email; + +use std::ffi::OsStr; +use std::io::{self, Result, Write}; +use std::fs::{self, File}; +use std::rc::Rc; +use std::path::{Path, PathBuf}; +use tempfile::{tempdir, tempdir_in, Builder, TempDir}; +use std::net::ToSocketAddrs; +use std::process::Command; +use std::time::{SystemTime, UNIX_EPOCH}; +use maildir::Maildir; +use lettre_email::{EmailBuilder, Header}; +use lettre::SendableEmail; + + +pub fn timestamp_ms() -> u128 { + let start = SystemTime::now(); + let time_since_epoch = start.duration_since(UNIX_EPOCH).unwrap(); + time_since_epoch.as_millis() +} + +// A basic test interface to a valid maildir directory. +// +// This creates a valid maildir and provides a simple mechanism to +// deliver test emails to it. It also writes a notmuch-config file +// in the top of the maildir. +pub struct MailBox { + root_dir: TempDir, + idcount: u32, + maildir: Maildir +} + +impl MailBox { + + // Creates a new maildir fixture. Since this is only used for tests, + // may just panic of something is wrong + pub fn new() -> Self { + + let root_dir = tempdir().unwrap(); + let root_path = root_dir.path().to_path_buf(); + + let tmp_path = root_path.join("tmp"); + fs::create_dir(&tmp_path).unwrap(); + + let cfg_fname = root_path.join("notmuch-config"); + let mut cfg_file = File::create(cfg_fname).unwrap(); + write!(cfg_file, r#" + [database] + path={tmppath} + [user] + name=Some Hacker + primary_email=dst@example.com + [new] + tags=unread;inbox; + ignore= + [search] + exclude_tags=deleted;spam; + [maildir] + synchronize_flags=true + [crypto] + gpg_path=gpg + "#, tmppath=root_path.to_string_lossy()).unwrap(); + + let maildir = Maildir::from(root_path.to_path_buf()); + maildir.create_dirs().unwrap(); + + Self { + root_dir, + idcount: 0, + maildir + } + } + + /// Return a new unique message ID + // fn next_msgid(&mut self) -> String{ + // let hostname = gethostname::gethostname(); + // let msgid = format!("{}@{}", self.idcount, hostname.to_string_lossy()); + // self.idcount += 1; + // msgid + // } + + pub fn path(&self) -> PathBuf + { + self.root_dir.path().into() + } + + pub fn hostname(&self) -> String { + let hname = gethostname::gethostname(); + hname.to_string_lossy().into() + } + + /// Deliver a new mail message in the mbox. + /// This does only adds the message to maildir, does not insert it + /// into the notmuch database. + /// returns a tuple of (msgid, pathname). + pub fn deliver(&self, + subject: Option<String>, + body: Option<String>, + to: Option<String>, + from: Option<String>, + headers: Vec<(String, String)>, + is_new: bool, // Move to new dir or cur dir? + keywords: Option<Vec<String>>, // List of keywords or labels + seen: bool, // Seen flag (cur dir only) + replied: bool, // Replied flag (cur dir only) + flagged: bool) // Flagged flag (cur dir only) + -> Result<(String, PathBuf)> + { + + let mut builder = EmailBuilder::new(); + + if let Some(val) = subject { + builder = builder.subject(val); + } + if let Some(val) = body { + builder = builder.text(val); + } + builder = match to { + Some(val) => builder.to(val), + None => builder.to(format!("to@{}.localhost", self.hostname())) + }; + builder = match from { + Some(val) => builder.from(val), + None => builder.from(format!("from@{}.localhost", self.hostname())) + }; + + for h in headers.into_iter(){ + let hdr: Header = h.into(); + builder = builder.header(hdr); + } + + let msg:SendableEmail = builder.build().unwrap().into(); + + // not sure why lettre doesn't add the host suffix itself + let msg_id = msg.message_id().to_string() + ".lettre@localhost"; + let id = if is_new { + self.maildir.store_new(&msg.message_to_string().unwrap().as_bytes()).unwrap() + }else{ + let mut flags = String::from(""); + if flagged { + flags += "F"; + } + if replied { + flags += "R"; + } + if seen { + flags += "S"; + } + self.maildir.store_cur_with_flags(&msg.message_to_string().unwrap().as_bytes(), flags.as_str()).unwrap() + }; + + let mut msgpath = self.path(); + msgpath = if is_new { + msgpath.join("new") + } else { + msgpath.join("cur") + }; + + msgpath = msgpath.join(&id); + + Ok((msg_id, msgpath)) + } +} + +impl Drop for MailBox { + fn drop(&mut self) { + } +} + + +#[derive(Clone, Debug)] +pub struct NotmuchCommand { + maildir_path: PathBuf +} + +impl NotmuchCommand { + + /// Return a function which runs notmuch commands on our test maildir. + /// + /// This uses the notmuch-config file created by the ``maildir`` + /// fixture. + pub fn new(maildir_path: &PathBuf) -> Self { + Self { + maildir_path: maildir_path.clone() + } + } + + /// Run a notmuch comand. + /// + /// This function runs with a timeout error as many notmuch + /// commands may block if multiple processes are trying to open + /// the database in write-mode. It is all too easy to + /// accidentally do this in the unittests. + pub fn run<I, S>(&self, args: I) -> Result<()> + where + I: IntoIterator<Item=S>, + S: AsRef<OsStr> + { + let cfg_fname = self.maildir_path.join("notmuch-config"); + + Command::new("notmuch").env("NOTMUCH_CONFIG", &cfg_fname) + .args(args) + .status()?; + Ok(()) + } + +} + + diff --git a/tests/lib.rs b/tests/lib.rs new file mode 100644 index 0000000..c5095f4 --- /dev/null +++ b/tests/lib.rs @@ -0,0 +1,11 @@ +extern crate dirs; +extern crate tempfile; +extern crate notmuch; +extern crate gethostname; +extern crate maildir; +extern crate lettre; +extern crate lettre_email; + +mod fixtures; +mod test_database; + diff --git a/tests/main.rs b/tests/main.rs index 9ad0a36..17db2bc 100644 --- a/tests/main.rs +++ b/tests/main.rs @@ -1,43 +1,62 @@ extern crate dirs; +extern crate tempfile; extern crate notmuch; +extern crate gethostname; +extern crate maildir; +extern crate lettre; +extern crate lettre_email; use std::sync::Arc; use notmuch::{Query, QueryExt}; -fn main() { - let mut mail_path = dirs::home_dir().unwrap(); - mail_path.push(".mail"); - - match notmuch::Database::open( - &mail_path.to_str().unwrap().to_string(), - notmuch::DatabaseMode::ReadOnly, - ) { - Ok(db) => { - #[cfg(feature = "v0_21")] - { - let rev = db.revision(); - println!("db revision: {:?}", rev); - } - let query = { - let dbr = Arc::new(db); - - notmuch::Query::create(dbr.clone(), &"".to_string()).unwrap() - }; - - // let mut threads = query.search_threads().unwrap(); - - // let mut threads = db.create_query(&"".to_string()).unwrap().search_threads().unwrap(); - - let mut threads = Arc::new(<Query as QueryExt>::search_threads(query).unwrap()); - - for thread in Arc::get_mut(&mut threads).unwrap() - { - println!("thread {:?} {:?}", thread.subject(), thread.authors()); - } - } - Err(err) => { - println!("Got error while trying to open db: {:?}", err); - } - } -} +mod fixtures; +use fixtures::{MailBox, NotmuchCommand}; + + + + +// fn main() { +// let mut mail_path = dirs::home_dir().unwrap(); +// mail_path.push(".mail"); + +// let md = MailBox::new(); +// let nmcmd = NotMuchCommand::new(md.path()); + +// match notmuch::Database::open( +// &mail_path.to_str().unwrap().to_string(), +// notmuch::DatabaseMode::ReadOnly, +// ) { +// Ok(db) => { +// #[cfg(feature = "v0_21")] +// { +// let rev = db.revision(); +// println!("db revision: {:?}", rev); +// } +// let query = { +// let dbr = Arc::new(db); + +// notmuch::Query::create(dbr.clone(), &"".to_string()).unwrap() +// }; + +// // let mut threads = query.search_threads().unwrap(); + +// // let mut threads = db.create_query(&"".to_string()).unwrap().search_threads().unwrap(); + +// let mut threads = Arc::new(<Query as QueryExt>::search_threads(query).unwrap()); + +// for thread in Arc::get_mut(&mut threads).unwrap() +// { +// println!("thread {:?} {:?}", thread.subject(), thread.authors()); +// } +// } +// Err(err) => { +// println!("Got error while trying to open db: {:?}", err); +// } +// } +// } + + + + + diff --git a/tests/test_database.rs b/tests/test_database.rs new file mode 100644 index 0000000..96d9bcc --- /dev/null +++ b/tests/test_database.rs @@ -0,0 +1,337 @@ +use fixtures::{NotmuchCommand, MailBox}; + +// #[test] +// // fn test_config_pathname_default(){ + +// // monkeypatch.delenv('NOTMUCH_CONFIG', raising=False) +// // user = pathlib.Path('~/.notmuch-config').expanduser() +// // assert dbmod._config_pathname() == user + +// // } + +mod database { + + use super::*; + + #[test] + fn test_create(){ + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()); + assert!(db.is_ok()); + + assert!(mailbox.path().join(".notmuch/xapian").exists()); + } + + #[test] + fn test_create_already_open(){ + let mailbox = MailBox::new(); + let db1 = notmuch::Database::create(&mailbox.path()); + assert!(db1.is_ok()); + + let db2 = notmuch::Database::create(&mailbox.path()); + assert!(db2.is_err()); + } + + + #[test] + fn test_create_existing(){ + let mailbox = MailBox::new(); + notmuch::Database::create(&mailbox.path()).unwrap(); + + let db2 = notmuch::Database::create(&mailbox.path()); + assert!(db2.is_err()); + } + + + #[test] + fn test_close(){ + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + + assert!(db.close().is_ok()); + } + + #[test] + fn test_drop_noclose(){ + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + + drop(db); + } + + #[test] + fn test_close_drop(){ + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + db.close().unwrap(); + drop(db); + } + + #[test] + fn test_path(){ + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + assert!(db.path() == mailbox.path()); + } + + #[test] + fn test_version(){ + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + assert!(db.version() > 0); + } + +} + + +mod atomic { + use super::*; + + // TODO: how do I test this?? + +} + + +mod revision { + use super::*; + + #[test] + fn test_single_rev(){ + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + + let rev0 = db.revision(); + let rev1 = db.revision(); + + assert!(rev0 == rev1); + assert!(rev0 <= rev1); + assert!(rev0 >= rev1); + assert!(!(rev0 < rev1)); + assert!(!(rev0 > rev1)); + } + + #[test] + fn test_diff_db(){ + let mailbox0 = MailBox::new(); + let db0 = notmuch::Database::create(&mailbox0.path()).unwrap(); + let rev0 = db0.revision(); + + + let mailbox1 = MailBox::new(); + let db1 = notmuch::Database::create(&mailbox1.path()).unwrap(); + let rev1 = db1.revision(); + + assert!(rev0 != rev1); + assert!(rev0.uuid != rev1.uuid); + } + + #[test] + fn test_cmp(){ + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + + let rev0 = db.revision(); + + let (_, filename) = mailbox.deliver(None, None, None, None, vec![], true, None, false, false, false).unwrap(); + + db.index_file(&filename, None).unwrap(); + + let rev1 = db.revision(); + + assert!(rev0 < rev1); + assert!(rev0 <= rev1); + assert!(!(rev0 > rev1)); + assert!(!(rev0 >= rev1)); + assert!(!(rev0 == rev1)); + assert!(rev0 != rev1); + + + } + + // TODO: add tests for revisions comparisons + +} + + +mod messages { + use super::*; + + #[test] + fn test_add_message() { + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + + let (msgid, filename) = mailbox.deliver(None, None, None, None, vec![], true, None, false, false, false).unwrap(); + let msg = db.index_file(&filename, None).unwrap(); + + assert!(msg.filename() == filename); + assert!(msg.id() == msgid); + + } + + #[test] + fn test_remove_message() { + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + + let (msgid, filename) = mailbox.deliver(None, None, None, None, vec![], true, None, false, false, false).unwrap(); + let msg = db.index_file(&filename, None).unwrap(); + assert!(db.find_message(&msgid).unwrap().is_some()); + + db.remove_message(&filename).unwrap(); + assert!(db.find_message(&msgid).unwrap().is_none()); + } + + #[test] + fn test_find_message() { + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + + let (msgid, filename) = mailbox.deliver(None, None, None, None, vec![], true, None, false, false, false).unwrap(); + let msg0 = db.index_file(&filename, None).unwrap(); + + let msg1 = db.find_message(&msgid).unwrap().unwrap(); + assert!(msg0.id() == msgid); + assert!(msg0.id() == msg1.id()); + + assert!(msg0.filename() == filename); + assert!(msg0.filename() == msg1.filename()); + } + + #[test] + fn test_find_message_notfound() { + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + + assert!(db.find_message(&"foo").unwrap().is_none()); + } + +} + +mod tags { + use super::*; + + #[test] + fn test_none() { + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + + let tags = db.all_tags().unwrap(); + + assert!(tags.count() == 0); + } + + #[test] + fn test_some() { + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + let (_, filename) = mailbox.deliver(None, None, None, None, vec![], true, None, false, false, false).unwrap(); + let msg = db.index_file(&filename, None).unwrap(); + + msg.add_tag(&"hello").unwrap(); + let tags: Vec<String> = db.all_tags().unwrap().collect(); + + assert!(tags.len() == 1); + assert!(tags.iter().any(|x| x == "hello")); + } + + #[test] + fn test_iters() { + let mailbox = MailBox::new(); + let db = notmuch::Database::create(&mailbox.path()).unwrap(); + + let t1: Vec<String> = db.all_tags().unwrap().collect(); + let t2: Vec<String> = db.all_tags().unwrap().collect(); + assert!(t1 == t2); + } + +} + +struct PopulatedDatabase { + // Return a read-write Database. + // The database will have 3 messages, 2 threads. + + pub mailbox: MailBox, + pub database: notmuch::Database, +} + +impl PopulatedDatabase { + pub fn new() -> Self{ + let mailbox = MailBox::new(); + + let (msgid, _) = mailbox.deliver(None, Some("foo".to_string()), None, None, vec![], true, None, false, false, false).unwrap(); + mailbox.deliver(None, Some("bar".to_string()), None, None, vec![], true, None, false, false, false).unwrap(); + mailbox.deliver(None, Some("baz".to_string()), None, None, vec![("In-Reply-To".to_string(), format!("<{}>", msgid))], true, None, false, false, false).unwrap(); + + let cmd = NotmuchCommand::new(&mailbox.path()); + cmd.run(vec!["new"]).unwrap(); + + let database = notmuch::Database::open(&mailbox.path(), notmuch::DatabaseMode::ReadWrite).unwrap(); + + Self { + mailbox, + database + } + } +} + +mod query { + use super::*; + + #[test] + fn test_count_messages() { + let db = PopulatedDatabase::new(); + + let query = db.database.create_query("*").unwrap(); + assert!(query.count_messages().unwrap() == 3); + } + + #[test] + fn test_message_no_results() { + let db = PopulatedDatabase::new(); + + let query = db.database.create_query("not_a_matching_query").unwrap(); + let mut messages = query.search_messages().unwrap(); + let msg = messages.next(); + assert!(msg.is_none()); + } + + #[test] + fn test_message_match() { + let db = PopulatedDatabase::new(); + + let query = db.database.create_query("*").unwrap(); + let mut messages = query.search_messages().unwrap(); + let msg = messages.next(); + assert!(msg.is_some()); + } + + #[test] + fn test_count_threads() { + let db = PopulatedDatabase::new(); + + let query = db.database.create_query("*").unwrap(); + assert!(query.count_threads().unwrap() == 2); + } + + #[test] + fn test_threads_no_results() { + let db = PopulatedDatabase::new(); + + let query = db.database.create_query("not_a_matching_query").unwrap(); + let mut threads = query.search_threads().unwrap(); + let thrd = threads.next(); + assert!(thrd.is_none()); + } + + #[test] + fn test_threads_match() { + let db = PopulatedDatabase::new(); + + let query = db.database.create_query("*").unwrap(); + let mut threads = query.search_threads().unwrap(); + let thrd = threads.next(); + assert!(thrd.is_some()); + } +} + |
