Skip to content

Commit

Permalink
Merge pull request #2 from kingledion/remove-couchwrapper-trait
Browse files Browse the repository at this point in the history
Remove couchwrapper trait
  • Loading branch information
kingledion authored Dec 27, 2022
2 parents 00ef65f + eafdcb7 commit 029b389
Show file tree
Hide file tree
Showing 2 changed files with 73 additions and 63 deletions.
7 changes: 6 additions & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
[package]
name = "couch_rs_test"
version = "0.1.0"
version = "0.2.0"
edition = "2021"
license = "MIT"
description = "Tools for testing a CouchDB repository implemented in Rust with couch_rs."
repository = "https://github.com/kingledion/couch_rs_test"
readme = "README.md"
keywords = ["couchdb", "testing"]

[dependencies]
http = "0.2"
Expand Down
129 changes: 67 additions & 62 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -4,45 +4,55 @@
//! CouchDB instance. For a given database name, the created databases append a random string to guarantee
//! uniqueness of multiple tests in parallel in the same CouchDB instance.
//!
//! In a 'data layer' application model, there is a discrete structure that is used to represent the data
//! layer of an application. When using this package, that data layer is a CouchDB instance. Advanced
//! functionality can be built into the data layer by implementing methods on the data layer struct. A
//! example use is defining a commonly used but complex query as a method.
//! ```rust
//! use serde_json::json;
//! use couch_rs_test::{TestRepo, TestRepoConfig};
//!
//! If the data layer application model is used, a helper trait [CouchDBWrapper] can be applied to the data
//! layer struct; this struct would wraps a [couch_rs::Client](https://docs.rs/couch_rs/latest/couch_rs/struct.Client.html)
//! and the name of a database to use. For testing,
//! implementing the [CouchDBWrapper] trait allows it to be wrapped in the [TestRepo] class
//! to provide automatic creationg and destruction of tables during integration tests.
//! async fn new_database(
//! config_uri: &str,
//! config_username: &str,
//! config_password: &str,
//! config_dbname: &str,
//! ) -> TestRepo {
//!
//! The struct against which [CouchDBWrapper] is implemented should only refer to a single CouchDB database.
//! Even though the primary interface is the [couch_rs::Client](https://docs.rs/couch_rs/latest/couch_rs/struct.Client.html),
//! and not the couch_rs database, the automatic
//! creation and destruction implies that only one underlying database is managed by one struct.
//! let repo: TestRepo = match TestRepo::new(
//! TestRepoConfig::new(
//! config_uri,
//! config_username,
//! config_password,
//! config_dbname,
//! )
//! ).await {
//! Ok(r) => r,
//! // if db creation fails, test will fail, so just panic
//! Err(e) => panic!("Failed to create test database: {}", e),
//! };
//!
//! // write test data into the newly created database
//! let data = &mut vec!{
//! json!{{"some": "data"}},
//! json!{{"some": "other-data"}}
//! };
//!
//! match repo.with_data(data).await {
//! Ok(cnt) => log::info!("Added {} entries to test database {}", cnt, repo.db.name()),
//! Err(e) => panic!("Failed to set up database: {}", e),
//! };
//! repo
//! }
//! ```

#![warn(missing_docs)]

use couch_rs::{document::TypedCouchDocument, error::CouchError, Client};
use std::error::Error;
use couch_rs::{database::Database, document::TypedCouchDocument, error::CouchError, Client};
use rand::{distributions::Alphanumeric, Rng};
use tokio_util::sync::CancellationToken;

/// A wrapper for a struct that manages on CouchDB database
/// Configuration for [TestRepo].
///
/// This trait should be implemented on a struct that manages a CouchDB database, thus enabling that
/// struct to be used in [TestRepo].
pub trait CouchDBWrapper {
/// Create a new instance of the underlying struct, by passing the sepcifications for the couch_rs
/// client and the name of the database.
fn wrap(uri: &str, username: &str, password: &str, db_name: &str) -> Self;
/// Return the underlying client
fn client(&self) -> &Client;
/// Return the underlying database name
fn dbname(&self) -> &str;
}

/// Configuration for [TestRepo]. This configuration is also passed to the underlying struct on which
/// [CouchDBWrapper] is implemented, in order to create a new [couch_rs::Client](https://docs.rs/couch_rs/latest/couch_rs/struct.Client.html)
/// and identify the associated database.
/// This configuration is to create a new [couch_rs::Client](https://docs.rs/couch_rs/latest/couch_rs/struct.Client.html)
/// and name the associated [couch_rs::database::Database](https://docs.rs/couch_rs/latest/couch_rs/database/struct.Database.html).
#[derive(Clone)]
pub struct TestRepoConfig {
uri: String,
Expand All @@ -52,8 +62,8 @@ pub struct TestRepoConfig {
}

impl TestRepoConfig {
/// Create a new configuration; identifying the uri, username and password for the CouchDB instance as
/// well as the database name.
/// Create a new configuration; identifying the uri, username and password for the CouchDB client
/// instance as well as the database name for the underlying database.
pub fn new(uri: &str, uname: &str, pwd: &str, dbname: &str) -> TestRepoConfig {
TestRepoConfig {
uri: uri.to_string(),
Expand All @@ -73,36 +83,32 @@ impl TestRepoConfig {

/// A wrapper for a struct that encapsulates functionality of an application's data layer.
///
/// The type parameter is the type of the data layer struct to be wrapped. The data layer struct must
/// implement the [CouchDBWrapper] trait.
///
/// Creation of a new instance of this struct will create a unique, associated CouchDB database. Internally,
/// this struct implements a drop token and watcher to determine when this struct is de-allocated, thus
/// triggering destruction of the associated CouchDB database.
pub struct TestRepo<T: CouchDBWrapper> {
/// A struct that encapsulates the functionality of an application's data layer. Test using
/// this wrapper should call for this struct in order to perform test actions against this
/// Creation of a new instance of this struct will create a unique [couch_rs::database::Database](https://docs.rs/couch_rs/latest/couch_rs/database/struct.Database.html).
/// Internally, this struct implements a drop token and watcher to determine when this struct is de-allocated,
/// thus triggering destruction of the associated CouchDB database.
pub struct TestRepo {
/// A [couch_rs::database::Database](https://docs.rs/couch_rs/latest/couch_rs/database/struct.Database.html).
/// Tests using this wrapper should access this struct in order to perform test actions against this
/// TestRepo's ephemeral CouchDB database.
pub repo: T,
pub db: Database,

drop_token: CancellationToken,
dropped_token: CancellationToken,
}

impl<T: CouchDBWrapper> TestRepo<T> {
/// Creates a new instance of TestRepo wrapping a new instance of the type paramter (an implementation
/// of [CouchDBWrapper]). This function will create a new [couch_rs::Client](https://docs.rs/couch_rs/latest/couch_rs/struct.Client.html)
/// from the parameters passed
/// as part of the [TestRepoConfig] argument. It will then create a new database in CouchDB using the
/// client connection and a database name consisting of the name defined in config plus a random suffix.
/// This randomization of database names helps prevent collisions during parallel test runs against
/// the same CouchDB instance.
impl TestRepo {
/// Creates a new instance of TestRepo wrapping a new instance of [couch_rs::database::Database](https://docs.rs/couch_rs/latest/couch_rs/database/struct.Database.html).
/// This function will create a new [couch_rs::Client](https://docs.rs/couch_rs/latest/couch_rs/struct.Client.html)
/// from the parameters passed as part of the [TestRepoConfig] argument and then create a new database
/// in CouchDB using the client connection and a database name consisting of the name defined in config
/// plus a random suffix.This randomization of database names helps prevent collisions during parallel
/// test excutions against the same CouchDB instance.
///
/// This function also creates a drop token and watcher to determine when this instance is de-allocated
/// The watcher spawn an asynchronous thread that will observe the drop token every 100 milliseconds.
/// when this instance is deallocated, the drop token is destroyed and the watcher will trigger the
/// destruction of the database instance created by this method.
pub async fn new(arg_cfg: TestRepoConfig) -> TestRepo<T> {
pub async fn new(arg_cfg: TestRepoConfig) -> Result<TestRepo, Box<dyn Error>> {
// create random identifier for database and append to db name
let test_identifier = rand::thread_rng()
.sample_iter(&Alphanumeric)
Expand All @@ -114,17 +120,17 @@ impl<T: CouchDBWrapper> TestRepo<T> {
let db_unique_name = format!("{}-{}", arg_cfg.db_name, test_identifier);
let cfg = arg_cfg.with_name(db_unique_name);

let wrapped = T::wrap(&cfg.uri, &cfg.username, &cfg.password, &cfg.db_name);
let client = Client::new(&cfg.uri, &cfg.username, &cfg.password)?;


let drop_token = CancellationToken::new();
let dropped_token = TestRepo::<T>::start_drop_watcher(&drop_token, cfg.clone()).await;
let dropped_token = TestRepo::start_drop_watcher(&drop_token, cfg.clone()).await;

// connect to database and return wrapping repository
log::info!("Creating database {} for testing", cfg.db_name);

// create test database - panic on fail
let c = wrapped.client();
match c.make_db(&cfg.db_name).await {
match client.make_db(&cfg.db_name).await {
Ok(_) => {}
Err(e) => {
match e.status() {
Expand All @@ -146,11 +152,11 @@ impl<T: CouchDBWrapper> TestRepo<T> {
}
};

TestRepo {
repo: wrapped,
Ok(TestRepo {
db: client.db(&cfg.db_name).await?,
drop_token: drop_token,
dropped_token: dropped_token,
}
})
}

/// Pushes data to the unique database associated with this instance. Data is pushed via the
Expand All @@ -160,8 +166,7 @@ impl<T: CouchDBWrapper> TestRepo<T> {
&self,
data: &mut [S],
) -> Result<usize, CouchError> {
let db = self.repo.client().db(&self.repo.dbname()).await?;
let result = db.bulk_docs(data).await?;
let result = self.db.bulk_docs(data).await?;
return Ok(result.len());
}

Expand All @@ -179,7 +184,7 @@ impl<T: CouchDBWrapper> TestRepo<T> {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
}

TestRepo::<T>::drop(cfg).await;
TestRepo::drop(cfg).await;

dropped_token.cancel();
});
Expand All @@ -203,7 +208,7 @@ impl<T: CouchDBWrapper> TestRepo<T> {

}

impl<T: CouchDBWrapper> Drop for TestRepo<T> {
impl Drop for TestRepo {
fn drop(&mut self) {
self.drop_token.cancel();

Expand Down

0 comments on commit 029b389

Please sign in to comment.