Compare commits
No commits in common. "main" and "v4.0.1" have entirely different histories.
8 changed files with 53 additions and 179 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -7,20 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
||||||
|
|
||||||
## [Unreleased]
|
## [Unreleased]
|
||||||
|
|
||||||
## [5.0.0](https://git.kemitix.net/kemitix/kxio/compare/v4.0.1...v5.0.0) - 2024-12-29
|
|
||||||
|
|
||||||
### Added
|
|
||||||
|
|
||||||
- *(net)* add tracing to matching each criteria for mock request
|
|
||||||
|
|
||||||
### Fixed
|
|
||||||
|
|
||||||
- *(net)* [**breaking**] remove Drop assertions for any unused plans
|
|
||||||
|
|
||||||
### Other
|
|
||||||
|
|
||||||
- *(print)* add details to readme and an example
|
|
||||||
|
|
||||||
## [4.0.1](https://git.kemitix.net/kemitix/kxio/compare/v4.0.0...v4.0.1) - 2024-12-25
|
## [4.0.1](https://git.kemitix.net/kemitix/kxio/compare/v4.0.0...v4.0.1) - 2024-12-25
|
||||||
|
|
||||||
### Fixed
|
### Fixed
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
[package]
|
[package]
|
||||||
name = "kxio"
|
name = "kxio"
|
||||||
version = "5.0.0"
|
version = "4.0.1"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
authors = ["Paul Campbell <pcampbell@kemitix.net>"]
|
authors = ["Paul Campbell <pcampbell@kemitix.net>"]
|
||||||
description = "Provides injectable Filesystem and Network resources to make code more testable"
|
description = "Provides injectable Filesystem and Network resources to make code more testable"
|
||||||
|
|
30
README.md
30
README.md
|
@ -1,15 +1,14 @@
|
||||||
# kxio
|
# kxio
|
||||||
|
|
||||||
`kxio` is a Rust library that provides injectable `FileSystem`, `Network` and
|
`kxio` is a Rust library that provides injectable `FileSystem` and `Network`
|
||||||
`Print` resources to enhance the testability of your code. By abstracting
|
resources to enhance the testability of your code. By abstracting system-level
|
||||||
system-level interactions, `kxio` enables easier mocking and testing of code
|
interactions, `kxio` enables easier mocking and testing of code that relies on
|
||||||
that relies on file system and network operations.
|
file system and network operations.
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- **Filesystem Abstraction**
|
- **Filesystem Abstraction**
|
||||||
- **Network Abstraction**
|
- **Network Abstraction**
|
||||||
- **Print Abstraction**
|
|
||||||
- **Enhanced Testability**
|
- **Enhanced Testability**
|
||||||
|
|
||||||
## Filesystem
|
## Filesystem
|
||||||
|
@ -31,27 +30,6 @@ The Network module offers a testable interface over the `reqwest` crate. For
|
||||||
comprehensive documentation and usage examples, please refer to
|
comprehensive documentation and usage examples, please refer to
|
||||||
<https://docs.rs/kxio/latest/kxio/net/>
|
<https://docs.rs/kxio/latest/kxio/net/>
|
||||||
|
|
||||||
## Print
|
|
||||||
|
|
||||||
The Print module provides three implementations of the `Printer` trait:
|
|
||||||
|
|
||||||
- `StandardPrint` - behaves as normal, printing to `STDOUT` and `STDERR`
|
|
||||||
- `NullPrint` - swallows all prints, outputting nothing
|
|
||||||
- `TestPrint` - captures all print output and makes it available for assertions in tests
|
|
||||||
|
|
||||||
It also provides macros to use with each:
|
|
||||||
|
|
||||||
- `kxprintln`
|
|
||||||
- `kxprint`
|
|
||||||
- `kxeprintln`
|
|
||||||
- `kxeprint`
|
|
||||||
|
|
||||||
They are analogous to the `std` macros: `println`, `print`, `eprintln` and `eprint` respectively.
|
|
||||||
|
|
||||||
Each of the `kx{e}print{ln}` macros takes a reference to an instance of the `Printer` trait as the first parameter.
|
|
||||||
|
|
||||||
For comprehensive documentation and usage examples, please refer to <https://docs.rs/kxio/latest/kxio/print/>
|
|
||||||
|
|
||||||
## Getting Started
|
## Getting Started
|
||||||
|
|
||||||
Add `kxio` to your `Cargo.toml`:
|
Add `kxio` to your `Cargo.toml`:
|
||||||
|
|
|
@ -151,8 +151,6 @@ mod tests {
|
||||||
|
|
||||||
assert_eq!(contents, "contents");
|
assert_eq!(contents, "contents");
|
||||||
|
|
||||||
net.assert_no_unused_plans();
|
|
||||||
|
|
||||||
// not needed for this test, but should it be needed, we can avoid checking for any
|
// not needed for this test, but should it be needed, we can avoid checking for any
|
||||||
// unconsumed request matches.
|
// unconsumed request matches.
|
||||||
// let mock_net = kxio::net::MockNet::try_from(net).expect("recover mock");
|
// let mock_net = kxio::net::MockNet::try_from(net).expect("recover mock");
|
||||||
|
|
|
@ -32,17 +32,15 @@ struct Plan {
|
||||||
response: reqwest::Response,
|
response: reqwest::Response,
|
||||||
}
|
}
|
||||||
impl Plan {
|
impl Plan {
|
||||||
#[tracing::instrument(skip(self))]
|
#[tracing::instrument]
|
||||||
fn matches(&self, request: &Request) -> bool {
|
fn matches(&self, request: &Request) -> bool {
|
||||||
let url = request.url();
|
let url = request.url();
|
||||||
let is_match =
|
let is_match =
|
||||||
self.match_request.iter().all(|criteria| match criteria {
|
self.match_request.iter().all(|criteria| match criteria {
|
||||||
MatchRequest::Body(body) => {
|
MatchRequest::Body(body) => {
|
||||||
tracing::trace!(body = ?request.body(), "BODY");
|
|
||||||
request.body().and_then(reqwest::Body::as_bytes) == Some(body)
|
request.body().and_then(reqwest::Body::as_bytes) == Some(body)
|
||||||
}
|
}
|
||||||
MatchRequest::Header { name, value } => {
|
MatchRequest::Header { name, value } => {
|
||||||
tracing::trace!(%name, %value, "HEADER");
|
|
||||||
request
|
request
|
||||||
.headers()
|
.headers()
|
||||||
.iter()
|
.iter()
|
||||||
|
@ -53,29 +51,13 @@ impl Plan {
|
||||||
request_header_name.as_str() == name && request_header_value == value
|
request_header_name.as_str() == name && request_header_value == value
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
MatchRequest::Method(method) => {
|
MatchRequest::Method(method) => request.method() == http::Method::from(method),
|
||||||
tracing::trace!(%method, "METHOD");
|
MatchRequest::Scheme(scheme) => url.scheme() == scheme,
|
||||||
request.method() == http::Method::from(method)
|
MatchRequest::Host(host) => url.host_str() == Some(host),
|
||||||
}
|
MatchRequest::Path(path) => url.path() == path,
|
||||||
MatchRequest::Scheme(scheme) => {
|
MatchRequest::Fragment(fragment) => url.fragment() == Some(fragment),
|
||||||
tracing::trace!(%scheme, "SCHEME");
|
|
||||||
url.scheme() == scheme
|
|
||||||
}
|
|
||||||
MatchRequest::Host(host) => {
|
|
||||||
tracing::trace!(%host,"HOST");
|
|
||||||
url.host_str() == Some(host)
|
|
||||||
}
|
|
||||||
MatchRequest::Path(path) => {
|
|
||||||
tracing::trace!(%path,"PATH");
|
|
||||||
url.path() == path
|
|
||||||
}
|
|
||||||
MatchRequest::Fragment(fragment) => {
|
|
||||||
tracing::trace!(%fragment,"FRAGMENT");
|
|
||||||
url.fragment() == Some(fragment)
|
|
||||||
}
|
|
||||||
MatchRequest::Query { name, value } => url.query_pairs().into_iter().any(
|
MatchRequest::Query { name, value } => url.query_pairs().into_iter().any(
|
||||||
|(request_query_name, request_query_value)| {
|
|(request_query_name, request_query_value)| {
|
||||||
tracing::trace!(%name, %value, "QUERY");
|
|
||||||
request_query_name.as_ref() == name && request_query_value.as_ref() == value
|
request_query_name.as_ref() == name && request_query_value.as_ref() == value
|
||||||
},
|
},
|
||||||
),
|
),
|
||||||
|
@ -402,7 +384,7 @@ impl<'net> ReqBuilder<'net> {
|
||||||
let client = self.net.client();
|
let client = self.net.client();
|
||||||
// URL
|
// URL
|
||||||
let mut url = self.url;
|
let mut url = self.url;
|
||||||
tracing::trace!(%url);
|
tracing::trace!(?url);
|
||||||
// Query Parameters
|
// Query Parameters
|
||||||
if !self.query.is_empty() {
|
if !self.query.is_empty() {
|
||||||
url.push('?');
|
url.push('?');
|
||||||
|
@ -553,7 +535,6 @@ fn basic_auth_header_value(
|
||||||
pub struct MockNet {
|
pub struct MockNet {
|
||||||
plans: Rc<RefCell<Plans>>,
|
plans: Rc<RefCell<Plans>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl MockNet {
|
impl MockNet {
|
||||||
/// Helper to create a default [Client].
|
/// Helper to create a default [Client].
|
||||||
///
|
///
|
||||||
|
@ -618,11 +599,6 @@ impl MockNet {
|
||||||
tracing::debug!("reset plans");
|
tracing::debug!("reset plans");
|
||||||
self.plans.take();
|
self.plans.take();
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns the number of plans added and not yet matched against.
|
|
||||||
pub fn plans_left(&self) -> usize {
|
|
||||||
self.plans.borrow().len()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
impl From<MockNet> for Net {
|
impl From<MockNet> for Net {
|
||||||
fn from(mock_net: MockNet) -> Self {
|
fn from(mock_net: MockNet) -> Self {
|
||||||
|
@ -634,6 +610,47 @@ impl From<MockNet> for Net {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl Drop for MockNet {
|
||||||
|
#[cfg_attr(test, mutants::skip)]
|
||||||
|
#[tracing::instrument]
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Don't assert during panic to avoid double panic
|
||||||
|
if std::thread::panicking() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
let unused = self.plans.take();
|
||||||
|
if !unused.is_empty() {
|
||||||
|
log_unused_plans(&unused);
|
||||||
|
assert!(
|
||||||
|
unused.is_empty(),
|
||||||
|
"{} expected requests were not made",
|
||||||
|
unused.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
impl Drop for Net {
|
||||||
|
#[cfg_attr(test, mutants::skip)]
|
||||||
|
#[tracing::instrument]
|
||||||
|
fn drop(&mut self) {
|
||||||
|
// Don't assert during panic to avoid double panic
|
||||||
|
if std::thread::panicking() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if let Some(plans) = &self.plans {
|
||||||
|
let unused = plans.try_lock().expect("lock plans").take();
|
||||||
|
if !unused.is_empty() {
|
||||||
|
log_unused_plans(&unused);
|
||||||
|
assert!(
|
||||||
|
unused.is_empty(),
|
||||||
|
"{} expected requests were not made",
|
||||||
|
unused.len()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg_attr(test, mutants::skip)]
|
#[cfg_attr(test, mutants::skip)]
|
||||||
fn log_unused_plans(unused: &[Plan]) {
|
fn log_unused_plans(unused: &[Plan]) {
|
||||||
if !unused.is_empty() {
|
if !unused.is_empty() {
|
||||||
|
|
|
@ -1,5 +1,3 @@
|
||||||
/// Macro to print the expression to the `Printer` with a trailing newline.
|
|
||||||
///
|
|
||||||
#[macro_export]
|
#[macro_export]
|
||||||
macro_rules! kxprintln {
|
macro_rules! kxprintln {
|
||||||
($printer:expr, $($arg:tt)*) => {{
|
($printer:expr, $($arg:tt)*) => {{
|
||||||
|
|
|
@ -11,7 +11,6 @@
|
||||||
//!
|
//!
|
||||||
//! ```
|
//! ```
|
||||||
//! use kxio::print::{Print, StandardPrint};
|
//! use kxio::print::{Print, StandardPrint};
|
||||||
//! use kxio::{kxeprintln, kxprintln};
|
|
||||||
//!
|
//!
|
||||||
//! fn print_hello(printer: &impl Print) {
|
//! fn print_hello(printer: &impl Print) {
|
||||||
//! printer.println("Hello, World!");
|
//! printer.println("Hello, World!");
|
||||||
|
@ -19,8 +18,6 @@
|
||||||
//!
|
//!
|
||||||
//! let printer = StandardPrint;
|
//! let printer = StandardPrint;
|
||||||
//! print_hello(&printer);
|
//! print_hello(&printer);
|
||||||
//! kxprintln!(&printer, "Hello, world!");
|
|
||||||
//! kxeprintln!(&printer, "Terminating!");
|
|
||||||
//! ```
|
//! ```
|
||||||
|
|
||||||
mod macros;
|
mod macros;
|
||||||
|
|
100
tests/net.rs
100
tests/net.rs
|
@ -53,52 +53,6 @@ async fn test_get_url() {
|
||||||
assert_eq!(response.bytes().await.expect("response body"), "Get OK");
|
assert_eq!(response.bytes().await.expect("response body"), "Get OK");
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn test_when_all_plans_match_assert_is_ok() {
|
|
||||||
//given
|
|
||||||
let mock_net = kxio::net::mock();
|
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
|
||||||
|
|
||||||
mock_net
|
|
||||||
.on()
|
|
||||||
.get(url)
|
|
||||||
.respond(StatusCode::OK)
|
|
||||||
.body("Get OK")
|
|
||||||
.expect("mock");
|
|
||||||
let net = Net::from(mock_net);
|
|
||||||
|
|
||||||
//when
|
|
||||||
let _response = net.get(url).send().await.expect("response");
|
|
||||||
|
|
||||||
//then
|
|
||||||
net.assert_no_unused_plans();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
#[should_panic]
|
|
||||||
async fn test_when_not_all_plans_match_assert_fails() {
|
|
||||||
//given
|
|
||||||
let mock_net = kxio::net::mock();
|
|
||||||
|
|
||||||
let url = "https://www.example.com";
|
|
||||||
|
|
||||||
mock_net
|
|
||||||
.on()
|
|
||||||
.get(url)
|
|
||||||
.respond(StatusCode::OK)
|
|
||||||
.body("Get OK")
|
|
||||||
.expect("mock");
|
|
||||||
let net = Net::from(mock_net);
|
|
||||||
|
|
||||||
//when
|
|
||||||
// request is not made
|
|
||||||
// let _response = net.get(url).send().await.expect("response");
|
|
||||||
|
|
||||||
//then
|
|
||||||
net.assert_no_unused_plans();
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn test_post_url() {
|
async fn test_post_url() {
|
||||||
//given
|
//given
|
||||||
|
@ -688,57 +642,3 @@ async fn test_get_with_user_agent() {
|
||||||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||||
assert_eq!(valid.status(), StatusCode::OK);
|
assert_eq!(valid.status(), StatusCode::OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
|
||||||
fn test_reset_removes_all_plans() {
|
|
||||||
//given
|
|
||||||
let mock_net = kxio::net::mock();
|
|
||||||
let url = "https://www.example.com/path";
|
|
||||||
|
|
||||||
mock_net
|
|
||||||
.on()
|
|
||||||
.get(url)
|
|
||||||
.user_agent("007")
|
|
||||||
.respond(StatusCode::OK)
|
|
||||||
.mock()
|
|
||||||
.expect("mock");
|
|
||||||
mock_net
|
|
||||||
.on()
|
|
||||||
.get(url)
|
|
||||||
.user_agent("orange")
|
|
||||||
.respond(StatusCode::FORBIDDEN)
|
|
||||||
.mock()
|
|
||||||
.expect("mock");
|
|
||||||
assert_eq!(mock_net.plans_left(), 2);
|
|
||||||
|
|
||||||
//when
|
|
||||||
mock_net.reset();
|
|
||||||
|
|
||||||
//then
|
|
||||||
assert_eq!(mock_net.plans_left(), 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn try_from_with_net_from_a_mock_net() {
|
|
||||||
//given
|
|
||||||
let mock_net = kxio::net::mock();
|
|
||||||
let net = Net::from(mock_net);
|
|
||||||
|
|
||||||
//when
|
|
||||||
let result = MockNet::try_from(net).await;
|
|
||||||
|
|
||||||
//then
|
|
||||||
assert!(result.is_ok());
|
|
||||||
}
|
|
||||||
|
|
||||||
#[tokio::test]
|
|
||||||
async fn try_from_with_net_not_from_a_mock_net() {
|
|
||||||
//given
|
|
||||||
let net = kxio::net::new();
|
|
||||||
|
|
||||||
//when
|
|
||||||
let result = MockNet::try_from(net).await;
|
|
||||||
|
|
||||||
//then
|
|
||||||
assert!(result.is_err());
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue