Compare commits
5 commits
Author | SHA1 | Date | |
---|---|---|---|
a91dcb3a4a | |||
|
2f646c4131 | ||
3f4e9fdc92 | |||
8007f01d94 | |||
9b7a2870ff |
8 changed files with 179 additions and 53 deletions
14
CHANGELOG.md
14
CHANGELOG.md
|
@ -7,6 +7,20 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
|
|||
|
||||
## [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
|
||||
|
||||
### Fixed
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
[package]
|
||||
name = "kxio"
|
||||
version = "4.0.1"
|
||||
version = "5.0.0"
|
||||
edition = "2021"
|
||||
authors = ["Paul Campbell <pcampbell@kemitix.net>"]
|
||||
description = "Provides injectable Filesystem and Network resources to make code more testable"
|
||||
|
|
30
README.md
30
README.md
|
@ -1,14 +1,15 @@
|
|||
# kxio
|
||||
|
||||
`kxio` is a Rust library that provides injectable `FileSystem` and `Network`
|
||||
resources to enhance the testability of your code. By abstracting system-level
|
||||
interactions, `kxio` enables easier mocking and testing of code that relies on
|
||||
file system and network operations.
|
||||
`kxio` is a Rust library that provides injectable `FileSystem`, `Network` and
|
||||
`Print` resources to enhance the testability of your code. By abstracting
|
||||
system-level interactions, `kxio` enables easier mocking and testing of code
|
||||
that relies on file system and network operations.
|
||||
|
||||
## Features
|
||||
|
||||
- **Filesystem Abstraction**
|
||||
- **Network Abstraction**
|
||||
- **Print Abstraction**
|
||||
- **Enhanced Testability**
|
||||
|
||||
## Filesystem
|
||||
|
@ -30,6 +31,27 @@ The Network module offers a testable interface over the `reqwest` crate. For
|
|||
comprehensive documentation and usage examples, please refer to
|
||||
<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
|
||||
|
||||
Add `kxio` to your `Cargo.toml`:
|
||||
|
|
|
@ -151,6 +151,8 @@ mod tests {
|
|||
|
||||
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
|
||||
// unconsumed request matches.
|
||||
// let mock_net = kxio::net::MockNet::try_from(net).expect("recover mock");
|
||||
|
|
|
@ -32,15 +32,17 @@ struct Plan {
|
|||
response: reqwest::Response,
|
||||
}
|
||||
impl Plan {
|
||||
#[tracing::instrument]
|
||||
#[tracing::instrument(skip(self))]
|
||||
fn matches(&self, request: &Request) -> bool {
|
||||
let url = request.url();
|
||||
let is_match =
|
||||
self.match_request.iter().all(|criteria| match criteria {
|
||||
MatchRequest::Body(body) => {
|
||||
tracing::trace!(body = ?request.body(), "BODY");
|
||||
request.body().and_then(reqwest::Body::as_bytes) == Some(body)
|
||||
}
|
||||
MatchRequest::Header { name, value } => {
|
||||
tracing::trace!(%name, %value, "HEADER");
|
||||
request
|
||||
.headers()
|
||||
.iter()
|
||||
|
@ -51,13 +53,29 @@ impl Plan {
|
|||
request_header_name.as_str() == name && request_header_value == value
|
||||
})
|
||||
}
|
||||
MatchRequest::Method(method) => request.method() == http::Method::from(method),
|
||||
MatchRequest::Scheme(scheme) => url.scheme() == scheme,
|
||||
MatchRequest::Host(host) => url.host_str() == Some(host),
|
||||
MatchRequest::Path(path) => url.path() == path,
|
||||
MatchRequest::Fragment(fragment) => url.fragment() == Some(fragment),
|
||||
MatchRequest::Method(method) => {
|
||||
tracing::trace!(%method, "METHOD");
|
||||
request.method() == http::Method::from(method)
|
||||
}
|
||||
MatchRequest::Scheme(scheme) => {
|
||||
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(
|
||||
|(request_query_name, request_query_value)| {
|
||||
tracing::trace!(%name, %value, "QUERY");
|
||||
request_query_name.as_ref() == name && request_query_value.as_ref() == value
|
||||
},
|
||||
),
|
||||
|
@ -384,7 +402,7 @@ impl<'net> ReqBuilder<'net> {
|
|||
let client = self.net.client();
|
||||
// URL
|
||||
let mut url = self.url;
|
||||
tracing::trace!(?url);
|
||||
tracing::trace!(%url);
|
||||
// Query Parameters
|
||||
if !self.query.is_empty() {
|
||||
url.push('?');
|
||||
|
@ -535,6 +553,7 @@ fn basic_auth_header_value(
|
|||
pub struct MockNet {
|
||||
plans: Rc<RefCell<Plans>>,
|
||||
}
|
||||
|
||||
impl MockNet {
|
||||
/// Helper to create a default [Client].
|
||||
///
|
||||
|
@ -599,6 +618,11 @@ impl MockNet {
|
|||
tracing::debug!("reset plans");
|
||||
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 {
|
||||
fn from(mock_net: MockNet) -> Self {
|
||||
|
@ -610,47 +634,6 @@ 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)]
|
||||
fn log_unused_plans(unused: &[Plan]) {
|
||||
if !unused.is_empty() {
|
||||
|
|
|
@ -1,3 +1,5 @@
|
|||
/// Macro to print the expression to the `Printer` with a trailing newline.
|
||||
///
|
||||
#[macro_export]
|
||||
macro_rules! kxprintln {
|
||||
($printer:expr, $($arg:tt)*) => {{
|
||||
|
|
|
@ -11,6 +11,7 @@
|
|||
//!
|
||||
//! ```
|
||||
//! use kxio::print::{Print, StandardPrint};
|
||||
//! use kxio::{kxeprintln, kxprintln};
|
||||
//!
|
||||
//! fn print_hello(printer: &impl Print) {
|
||||
//! printer.println("Hello, World!");
|
||||
|
@ -18,6 +19,8 @@
|
|||
//!
|
||||
//! let printer = StandardPrint;
|
||||
//! print_hello(&printer);
|
||||
//! kxprintln!(&printer, "Hello, world!");
|
||||
//! kxeprintln!(&printer, "Terminating!");
|
||||
//! ```
|
||||
|
||||
mod macros;
|
||||
|
|
100
tests/net.rs
100
tests/net.rs
|
@ -53,6 +53,52 @@ async fn test_get_url() {
|
|||
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]
|
||||
async fn test_post_url() {
|
||||
//given
|
||||
|
@ -642,3 +688,57 @@ async fn test_get_with_user_agent() {
|
|||
assert_eq!(response.status(), StatusCode::FORBIDDEN);
|
||||
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