diff --git a/Cargo.lock b/Cargo.lock index cca798e..ffa716e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -424,6 +424,37 @@ dependencies = [ "serde", ] +[[package]] +name = "camino" +version = "1.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8b96ec4966b5813e2c0507c1f86115c8c5abaadc3980879c3424042a02fd1ad3" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo-platform" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "24b1f0365a6c6bb4020cd05806fd0d33c44d38046b8bd7f0e40814b9763cabfc" +dependencies = [ + "serde", +] + +[[package]] +name = "cargo_metadata" +version = "0.14.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4acbb09d9ee8e23699b9634375c72795d095bf268439da88562cf9b501f181fa" +dependencies = [ + "camino", + "cargo-platform", + "semver", + "serde", + "serde_json", +] + [[package]] name = "cc" version = "1.1.36" @@ -2023,6 +2054,7 @@ dependencies = [ "iroh-blake3", "iroh-blobs", "iroh-gossip", + "iroh-io", "iroh-metrics", "iroh-net", "iroh-router", @@ -2047,6 +2079,7 @@ dependencies = [ "strum", "tempfile", "test-strategy", + "testdir", "testresult", "thiserror", "tokio", @@ -2678,6 +2711,15 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "38bf9645c8b145698bb0b18a4637dcacbc421ea49bef2317e4fd8065a387cf21" +[[package]] +name = "ntapi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e8a3895c6391c39d7fe7ebc444a87eb2991b2a0bc718fdabd071eec617fc68e4" +dependencies = [ + "winapi", +] + [[package]] name = "nu-ansi-term" version = "0.46.0" @@ -3988,6 +4030,9 @@ name = "semver" version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "61697e0a1c7e512e84a621326239844a24d8207b4669b41bc18b32ea5cbf988b" +dependencies = [ + "serde", +] [[package]] name = "serde" @@ -4460,6 +4505,20 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "sysinfo" +version = "0.26.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5c18a6156d1f27a9592ee18c1a846ca8dd5c258b7179fc193ae87c74ebb666f5" +dependencies = [ + "cfg-if", + "core-foundation-sys", + "libc", + "ntapi", + "once_cell", + "winapi", +] + [[package]] name = "system-configuration" version = "0.6.1" @@ -4506,6 +4565,20 @@ dependencies = [ "syn 2.0.87", ] +[[package]] +name = "testdir" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ee79e927b64d193f5abb60d20a0eb56be0ee5a242fdeb8ce3bf054177006de52" +dependencies = [ + "anyhow", + "backtrace", + "cargo_metadata", + "once_cell", + "sysinfo", + "whoami", +] + [[package]] name = "testresult" version = "0.4.1" @@ -5066,6 +5139,12 @@ version = "0.11.0+wasi-snapshot-preview1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c8d87e72b64a3b4db28d11ce29237c246188f4f51057d65a7eab63b7987e423" +[[package]] +name = "wasite" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b8dad83b4f25e74f184f64c43b150b91efe7647395b42289f38e50566d82855b" + [[package]] name = "wasm-bindgen" version = "0.2.95" @@ -5164,6 +5243,17 @@ dependencies = [ "rustls-pki-types", ] +[[package]] +name = "whoami" +version = "1.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "372d5b87f58ec45c384ba03563b03544dc5fadc3983e434b286913f5b4a9bb6d" +dependencies = [ + "redox_syscall", + "wasite", + "web-sys", +] + [[package]] name = "widestring" version = "1.1.0" diff --git a/Cargo.toml b/Cargo.toml index 41fb628..12edbf0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -76,6 +76,8 @@ tracing-subscriber = { version = "0.3.18", features = ["env-filter"] } parking_lot = "0.12.3" testresult = "0.4.1" nested_enum_utils = "0.1.0" +iroh-io = "0.6.1" +testdir = "0.9.1" [features] default = ["net", "metrics", "engine", "rpc", "test-utils"] diff --git a/src/rpc/client.rs b/src/rpc/client.rs index 50d3746..92a2e43 100644 --- a/src/rpc/client.rs +++ b/src/rpc/client.rs @@ -791,9 +791,9 @@ impl ExportFileProgress { #[derive(Debug, Clone, PartialEq, Eq)] pub struct ExportFileOutcome { /// The size of the entry - size: u64, + pub size: u64, /// The path to which the entry was saved - path: PathBuf, + pub path: PathBuf, } impl Stream for ExportFileProgress { @@ -936,275 +936,4 @@ mod tests { Ok(()) } - - // /// Test that closing a doc does not close other instances. - // #[tokio::test] - // async fn test_doc_close() -> Result<()> { - // let _guard = iroh_test::logging::setup(); - - // let node = iroh::node::Node::memory().enable_docs().spawn().await?; - // let author = node.authors().default().await?; - // // open doc two times - // let doc1 = node.docs().create().await?; - // let doc2 = node.docs().open(doc1.id()).await?.expect("doc to exist"); - // // close doc1 instance - // doc1.close().await?; - // // operations on doc1 now fail. - // assert!(doc1.set_bytes(author, "foo", "bar").await.is_err()); - // // dropping doc1 will close the doc if not already closed - // // wait a bit because the close-on-drop spawns a task for which we cannot track completion. - // drop(doc1); - // tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; - - // // operations on doc2 still succeed - // doc2.set_bytes(author, "foo", "bar").await?; - // Ok(()) - // } - - // #[tokio::test] - // async fn test_doc_import_export() -> Result<()> { - // let _guard = iroh_test::logging::setup(); - - // let node = iroh::node::Node::memory().enable_docs().spawn().await?; - - // // create temp file - // let temp_dir = tempfile::tempdir().context("tempdir")?; - - // let in_root = temp_dir.path().join("in"); - // tokio::fs::create_dir_all(in_root.clone()) - // .await - // .context("create dir all")?; - // let out_root = temp_dir.path().join("out"); - - // let path = in_root.join("test"); - - // let size = 100; - // let mut buf = vec![0u8; size]; - // rand::thread_rng().fill_bytes(&mut buf); - // let mut file = tokio::fs::File::create(path.clone()) - // .await - // .context("create file")?; - // file.write_all(&buf.clone()).await.context("write_all")?; - // file.flush().await.context("flush")?; - - // // create doc & author - // let client = node.rpc_client().clone(); - // let docs_client = Client::from_service(client); - // let doc = docs_client.create().await.context("doc create")?; - // let author = client.authors().create().await.context("author create")?; - - // // import file - // let import_outcome = doc - // .import_file( - // author, - // iroh::util::fs::path_to_key(path.clone(), None, Some(in_root))?, - // path, - // true, - // ) - // .await - // .context("import file")? - // .finish() - // .await - // .context("import finish")?; - - // // export file - // let entry = doc - // .get_one(Query::author(author).key_exact(import_outcome.key)) - // .await - // .context("get one")? - // .unwrap(); - // let key = entry.key().to_vec(); - // let export_outcome = doc - // .export_file( - // entry, - // iroh::util::fs::key_to_path(key, None, Some(out_root))?, - // ExportMode::Copy, - // ) - // .await - // .context("export file")? - // .finish() - // .await - // .context("export finish")?; - - // let got_bytes = tokio::fs::read(export_outcome.path) - // .await - // .context("tokio read")?; - // assert_eq!(buf, got_bytes); - - // Ok(()) - // } - - // #[tokio::test] - // async fn test_authors() -> Result<()> { - // let node = Node::memory().enable_docs().spawn().await?; - - // // default author always exists - // let authors: Vec<_> = node.authors().list().await?.try_collect().await?; - // assert_eq!(authors.len(), 1); - // let default_author = node.authors().default().await?; - // assert_eq!(authors, vec![default_author]); - - // let author_id = node.authors().create().await?; - - // let authors: Vec<_> = node.authors().list().await?.try_collect().await?; - // assert_eq!(authors.len(), 2); - - // let author = node - // .authors() - // .export(author_id) - // .await? - // .expect("should have author"); - // node.authors().delete(author_id).await?; - // let authors: Vec<_> = node.authors().list().await?.try_collect().await?; - // assert_eq!(authors.len(), 1); - - // node.authors().import(author).await?; - - // let authors: Vec<_> = node.authors().list().await?.try_collect().await?; - // assert_eq!(authors.len(), 2); - - // assert!(node.authors().default().await? != author_id); - // node.authors().set_default(author_id).await?; - // assert_eq!(node.authors().default().await?, author_id); - - // Ok(()) - // } - - // #[tokio::test] - // async fn test_default_author_memory() -> Result<()> { - // let iroh = Node::memory().enable_docs().spawn().await?; - // let author = iroh.authors().default().await?; - // assert!(iroh.authors().export(author).await?.is_some()); - // assert!(iroh.authors().delete(author).await.is_err()); - // Ok(()) - // } - - // #[cfg(feature = "fs-store")] - // #[tokio::test] - // async fn test_default_author_persist() -> Result<()> { - // use crate::util::path::IrohPaths; - - // let _guard = iroh_test::logging::setup(); - - // let iroh_root_dir = tempfile::TempDir::new().unwrap(); - // let iroh_root = iroh_root_dir.path(); - - // // check that the default author exists and cannot be deleted. - // let default_author = { - // let iroh = Node::persistent(iroh_root) - // .await - // .unwrap() - // .enable_docs() - // .spawn() - // .await - // .unwrap(); - // let author = iroh.authors().default().await.unwrap(); - // assert!(iroh.authors().export(author).await.unwrap().is_some()); - // assert!(iroh.authors().delete(author).await.is_err()); - // iroh.shutdown().await.unwrap(); - // author - // }; - - // // check that the default author is persisted across restarts. - // { - // let iroh = Node::persistent(iroh_root) - // .await - // .unwrap() - // .enable_docs() - // .spawn() - // .await - // .unwrap(); - // let author = iroh.authors().default().await.unwrap(); - // assert_eq!(author, default_author); - // assert!(iroh.authors().export(author).await.unwrap().is_some()); - // assert!(iroh.authors().delete(author).await.is_err()); - // iroh.shutdown().await.unwrap(); - // }; - - // // check that a new default author is created if the default author file is deleted - // // manually. - // let default_author = { - // tokio::fs::remove_file(IrohPaths::DefaultAuthor.with_root(iroh_root)) - // .await - // .unwrap(); - // let iroh = Node::persistent(iroh_root) - // .await - // .unwrap() - // .enable_docs() - // .spawn() - // .await - // .unwrap(); - // let author = iroh.authors().default().await.unwrap(); - // assert!(author != default_author); - // assert!(iroh.authors().export(author).await.unwrap().is_some()); - // assert!(iroh.authors().delete(author).await.is_err()); - // iroh.shutdown().await.unwrap(); - // author - // }; - - // // check that the node fails to start if the default author is missing from the docs store. - // { - // let mut docs_store = iroh_docs::store::fs::Store::persistent( - // IrohPaths::DocsDatabase.with_root(iroh_root), - // ) - // .unwrap(); - // docs_store.delete_author(default_author).unwrap(); - // docs_store.flush().unwrap(); - // drop(docs_store); - // let iroh = Node::persistent(iroh_root) - // .await - // .unwrap() - // .enable_docs() - // .spawn() - // .await; - // assert!(iroh.is_err()); - - // // somehow the blob store is not shutdown correctly (yet?) on macos. - // // so we give it some time until we find a proper fix. - // #[cfg(target_os = "macos")] - // tokio::time::sleep(Duration::from_secs(1)).await; - - // tokio::fs::remove_file(IrohPaths::DefaultAuthor.with_root(iroh_root)) - // .await - // .unwrap(); - // drop(iroh); - // let iroh = Node::persistent(iroh_root) - // .await - // .unwrap() - // .enable_docs() - // .spawn() - // .await; - // assert!(iroh.is_ok()); - // iroh.unwrap().shutdown().await.unwrap(); - // } - - // // check that the default author can be set manually and is persisted. - // let default_author = { - // let iroh = Node::persistent(iroh_root) - // .await - // .unwrap() - // .enable_docs() - // .spawn() - // .await - // .unwrap(); - // let author = iroh.authors().create().await.unwrap(); - // iroh.authors().set_default(author).await.unwrap(); - // assert_eq!(iroh.authors().default().await.unwrap(), author); - // iroh.shutdown().await.unwrap(); - // author - // }; - // { - // let iroh = Node::persistent(iroh_root) - // .await - // .unwrap() - // .enable_docs() - // .spawn() - // .await - // .unwrap(); - // assert_eq!(iroh.authors().default().await.unwrap(), default_author); - // iroh.shutdown().await.unwrap(); - // } - - // Ok(()) - // } } diff --git a/tests/client.rs b/tests/client.rs new file mode 100644 index 0000000..aaae906 --- /dev/null +++ b/tests/client.rs @@ -0,0 +1,282 @@ +use anyhow::{Context, Result}; +use futures_util::TryStreamExt; +use iroh_blobs::{ + store::ExportMode, + util::fs::{key_to_path, path_to_key}, +}; +use iroh_docs::store::Query; +use rand::RngCore; +use tokio::io::AsyncWriteExt; +use util::Node; + +mod util; + +/// Test that closing a doc does not close other instances. +#[tokio::test] +async fn test_doc_close() -> Result<()> { + let _guard = iroh_test::logging::setup(); + + let node = Node::memory().spawn().await?; + let author = node.docs().author_default().await?; + // open doc two times + let doc1 = node.docs().create().await?; + let doc2 = node.docs().open(doc1.id()).await?.expect("doc to exist"); + // close doc1 instance + doc1.close().await?; + // operations on doc1 now fail. + assert!(doc1.set_bytes(author, "foo", "bar").await.is_err()); + // dropping doc1 will close the doc if not already closed + // wait a bit because the close-on-drop spawns a task for which we cannot track completion. + drop(doc1); + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + // operations on doc2 still succeed + doc2.set_bytes(author, "foo", "bar").await?; + Ok(()) +} + +#[tokio::test] +async fn test_doc_import_export() -> Result<()> { + let _guard = iroh_test::logging::setup(); + + let node = Node::memory().spawn().await?; + + // create temp file + let temp_dir = tempfile::tempdir().context("tempdir")?; + + let in_root = temp_dir.path().join("in"); + tokio::fs::create_dir_all(in_root.clone()) + .await + .context("create dir all")?; + let out_root = temp_dir.path().join("out"); + + let path = in_root.join("test"); + + let size = 100; + let mut buf = vec![0u8; size]; + rand::thread_rng().fill_bytes(&mut buf); + let mut file = tokio::fs::File::create(path.clone()) + .await + .context("create file")?; + file.write_all(&buf.clone()).await.context("write_all")?; + file.flush().await.context("flush")?; + + // create doc & author + let client = node.client(); + let docs_client = client.docs(); + let doc = docs_client.create().await.context("doc create")?; + let author = docs_client.author_create().await.context("author create")?; + + // import file + let import_outcome = doc + .import_file( + author, + path_to_key(path.clone(), None, Some(in_root))?, + path, + true, + ) + .await + .context("import file")? + .finish() + .await + .context("import finish")?; + + // export file + let entry = doc + .get_one(Query::author(author).key_exact(import_outcome.key)) + .await + .context("get one")? + .unwrap(); + let key = entry.key().to_vec(); + let export_outcome = doc + .export_file( + entry, + key_to_path(key, None, Some(out_root))?, + ExportMode::Copy, + ) + .await + .context("export file")? + .finish() + .await + .context("export finish")?; + + let got_bytes = tokio::fs::read(export_outcome.path) + .await + .context("tokio read")?; + assert_eq!(buf, got_bytes); + + Ok(()) +} + +#[tokio::test] +async fn test_authors() -> Result<()> { + let node = Node::memory().spawn().await?; + + // default author always exists + let authors: Vec<_> = node.docs().author_list().await?.try_collect().await?; + assert_eq!(authors.len(), 1); + let default_author = node.docs().author_default().await?; + assert_eq!(authors, vec![default_author]); + + let author_id = node.docs().author_create().await?; + + let authors: Vec<_> = node.docs().author_list().await?.try_collect().await?; + assert_eq!(authors.len(), 2); + + let author = node + .docs() + .author_export(author_id) + .await? + .expect("should have author"); + node.docs().author_delete(author_id).await?; + let authors: Vec<_> = node.docs().author_list().await?.try_collect().await?; + assert_eq!(authors.len(), 1); + + node.docs().author_import(author).await?; + + let authors: Vec<_> = node.docs().author_list().await?.try_collect().await?; + assert_eq!(authors.len(), 2); + + assert!(node.docs().author_default().await? != author_id); + node.docs().author_set_default(author_id).await?; + assert_eq!(node.docs().author_default().await?, author_id); + + Ok(()) +} + +#[tokio::test] +async fn test_default_author_memory() -> Result<()> { + let iroh = Node::memory().spawn().await?; + let author = iroh.docs().author_default().await?; + assert!(iroh.docs().author_export(author).await?.is_some()); + assert!(iroh.docs().author_delete(author).await.is_err()); + Ok(()) +} + +#[cfg(feature = "fs-store")] +#[tokio::test] +async fn test_default_author_persist() -> Result<()> { + use crate::util::path::IrohPaths; + + let _guard = iroh_test::logging::setup(); + + let iroh_root_dir = tempfile::TempDir::new().unwrap(); + let iroh_root = iroh_root_dir.path(); + + // check that the default author exists and cannot be deleted. + let default_author = { + let iroh = Node::persistent(iroh_root) + .await + .unwrap() + .enable_docs() + .spawn() + .await + .unwrap(); + let author = iroh.authors().default().await.unwrap(); + assert!(iroh.authors().export(author).await.unwrap().is_some()); + assert!(iroh.authors().delete(author).await.is_err()); + iroh.shutdown().await.unwrap(); + author + }; + + // check that the default author is persisted across restarts. + { + let iroh = Node::persistent(iroh_root) + .await + .unwrap() + .enable_docs() + .spawn() + .await + .unwrap(); + let author = iroh.authors().default().await.unwrap(); + assert_eq!(author, default_author); + assert!(iroh.authors().export(author).await.unwrap().is_some()); + assert!(iroh.authors().delete(author).await.is_err()); + iroh.shutdown().await.unwrap(); + }; + + // check that a new default author is created if the default author file is deleted + // manually. + let default_author = { + tokio::fs::remove_file(IrohPaths::DefaultAuthor.with_root(iroh_root)) + .await + .unwrap(); + let iroh = Node::persistent(iroh_root) + .await + .unwrap() + .enable_docs() + .spawn() + .await + .unwrap(); + let author = iroh.authors().default().await.unwrap(); + assert!(author != default_author); + assert!(iroh.authors().export(author).await.unwrap().is_some()); + assert!(iroh.authors().delete(author).await.is_err()); + iroh.shutdown().await.unwrap(); + author + }; + + // check that the node fails to start if the default author is missing from the docs store. + { + let mut docs_store = + iroh_docs::store::fs::Store::persistent(IrohPaths::DocsDatabase.with_root(iroh_root)) + .unwrap(); + docs_store.delete_author(default_author).unwrap(); + docs_store.flush().unwrap(); + drop(docs_store); + let iroh = Node::persistent(iroh_root) + .await + .unwrap() + .enable_docs() + .spawn() + .await; + assert!(iroh.is_err()); + + // somehow the blob store is not shutdown correctly (yet?) on macos. + // so we give it some time until we find a proper fix. + #[cfg(target_os = "macos")] + tokio::time::sleep(Duration::from_secs(1)).await; + + tokio::fs::remove_file(IrohPaths::DefaultAuthor.with_root(iroh_root)) + .await + .unwrap(); + drop(iroh); + let iroh = Node::persistent(iroh_root) + .await + .unwrap() + .enable_docs() + .spawn() + .await; + assert!(iroh.is_ok()); + iroh.unwrap().shutdown().await.unwrap(); + } + + // check that the default author can be set manually and is persisted. + let default_author = { + let iroh = Node::persistent(iroh_root) + .await + .unwrap() + .enable_docs() + .spawn() + .await + .unwrap(); + let author = iroh.authors().create().await.unwrap(); + iroh.authors().set_default(author).await.unwrap(); + assert_eq!(iroh.authors().default().await.unwrap(), author); + iroh.shutdown().await.unwrap(); + author + }; + { + let iroh = Node::persistent(iroh_root) + .await + .unwrap() + .enable_docs() + .spawn() + .await + .unwrap(); + assert_eq!(iroh.authors().default().await.unwrap(), default_author); + iroh.shutdown().await.unwrap(); + } + + Ok(()) +} diff --git a/tests/gc.rs b/tests/gc.rs index 02e5f91..76908b6 100644 --- a/tests/gc.rs +++ b/tests/gc.rs @@ -176,7 +176,6 @@ async fn gc_hashseq_impl() -> Result<()> { Ok(()) } -#[cfg(feature = "fs-store")] mod file { use std::{io, path::PathBuf}; @@ -235,7 +234,7 @@ mod file { let (node, _) = wrap_in_node(bao_store.clone(), Duration::from_secs(10)).await; let client = node.client(); let doc = client.docs().create().await?; - let author = client.authors().create().await?; + let author = client.docs().author_create().await?; let temp_path = dir.join("temp"); tokio::fs::create_dir_all(&temp_path).await?; let mut to_import = Vec::new();