Compare commits

...

5 commits
v4.0.1 ... main

Author SHA1 Message Date
a91dcb3a4a tests(net): add explicit tests for assert_no_unused_plans
All checks were successful
Test / build (map[name:stable]) (push) Successful in 8m14s
Test / build (map[name:nightly]) (push) Successful in 8m23s
Release Please / Release-plz (push) Successful in 1m23s
2024-12-29 22:07:41 +00:00
ForgeJo Action. See: https://git.kemitix.net/kemitix/rust
2f646c4131 chore: release v5.0.0
All checks were successful
Test / build (map[name:nightly]) (pull_request) Successful in 7m47s
Test / build (map[name:stable]) (pull_request) Successful in 8m7s
Test / build (map[name:nightly]) (push) Successful in 7m42s
Test / build (map[name:stable]) (push) Successful in 8m43s
Release Please / Release-plz (push) Successful in 6m17s
Signed-off-by: ForgeJo Action. See: https://git.kemitix.net/kemitix/rust <action@git.kemitix.net>
2024-12-29 20:08:55 +00:00
3f4e9fdc92 docs(print): add details to readme and an example
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 8m48s
Test / build (map[name:stable]) (push) Successful in 8m53s
Release Please / Release-plz (push) Successful in 1m39s
2024-12-29 19:57:20 +00:00
8007f01d94 fix(net)!: remove Drop assertions for any unused plans
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 8m36s
Test / build (map[name:stable]) (push) Successful in 10m10s
Release Please / Release-plz (push) Successful in 1m50s
Tests should use the existing `assert_no_unused_plans` method available
on both `Net` and `MockNet`.

This removes the problem of assetions being applied early when there are
multiple clones of the `Net` or `MockNet` and one of them is dropped.

feat(net): add `MockNet:;plans_left`

tests(net): add tests for MockNet::try_from
2024-12-29 19:33:27 +00:00
9b7a2870ff feat(net): add tracing to matching each criteria for mock request
All checks were successful
Test / build (map[name:nightly]) (push) Successful in 9m28s
Test / build (map[name:stable]) (push) Successful in 10m53s
Release Please / Release-plz (push) Successful in 1m56s
2024-12-28 17:54:53 +00:00
8 changed files with 179 additions and 53 deletions

View file

@ -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

View file

@ -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"

View file

@ -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`:

View file

@ -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");

View file

@ -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() {

View file

@ -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)*) => {{

View file

@ -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;

View file

@ -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());
}