Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

async satisfaction #117

Merged
merged 3 commits into from
Jun 7, 2023
Merged
Show file tree
Hide file tree
Changes from 1 commit
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ regex = "1"
futures-timer = "3.0.2"
futures = "0.3.5"
hyper = { version = "0.14", features = ["full"] }
tokio = { version = "1.5.0", features = ["rt", "io-util", "time"] }
tokio = { version = "1.5.0", features = ["rt"] }
deadpool = "0.9.2"
async-trait = "0.1"
once_cell = "1"
Expand Down
36 changes: 35 additions & 1 deletion src/mock_server/bare_server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,9 @@ use crate::mock_set::MockId;
use crate::mock_set::MountedMockSet;
use crate::{mock::Mock, verification::VerificationOutcome, Request};
use std::net::{SocketAddr, TcpListener, TcpStream};
use std::sync::atomic::AtomicBool;
use std::sync::Arc;
use tokio::sync::Notify;
use tokio::sync::RwLock;
use tokio::task::LocalSet;

Expand Down Expand Up @@ -101,8 +103,9 @@ impl BareMockServer {
/// When the returned `MockGuard` is dropped, `MockServer` will verify that the expectations set on the scoped `Mock` were
/// verified - if not, it will panic.
pub async fn register_as_scoped(&self, mock: Mock) -> MockGuard {
let mock_id = self.state.write().await.mock_set.register(mock);
let (notify, mock_id) = self.state.write().await.mock_set.register(mock);
MockGuard {
notify,
mock_id,
server_state: self.state.clone(),
}
Expand Down Expand Up @@ -182,6 +185,7 @@ Check `wiremock`'s documentation on scoped mocks for more details."]
pub struct MockGuard {
mock_id: MockId,
server_state: Arc<RwLock<MockServerState>>,
notify: Arc<(Notify, AtomicBool)>,
}

impl MockGuard {
Expand All @@ -190,6 +194,35 @@ impl MockGuard {
let (mounted_mock, _) = &state.mock_set[self.mock_id];
mounted_mock.received_requests()
}

pub async fn satisfied(&self) {
conradludgate marked this conversation as resolved.
Show resolved Hide resolved
let MockGuard {
mock_id,
server_state,
notify,
} = self;
let notification = notify.0.notified();
tokio::pin!(notification); // std::pin::pin was added in 1.68.0
conradludgate marked this conversation as resolved.
Show resolved Hide resolved
if notification.as_mut().enable() {
// reraise a signal just in case
notify.0.notify_waiters();
return;
}

if self.notify.1.load(std::sync::atomic::Ordering::Acquire) {
return;
}

let state = server_state.read().await;
let report = state.mock_set.verify(*mock_id);
if report.is_satisfied() {
// reraise a signal just in case another waiter joined the queue
notify.0.notify_waiters();
return;
}
conradludgate marked this conversation as resolved.
Show resolved Hide resolved

notification.await
}
}

impl Drop for MockGuard {
Expand All @@ -198,6 +231,7 @@ impl Drop for MockGuard {
let MockGuard {
mock_id,
server_state,
..
} = self;
let mut state = server_state.write().await;
let report = state.mock_set.verify(*mock_id);
Expand Down
27 changes: 17 additions & 10 deletions src/mock_set.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,11 @@ use crate::{Mock, Request, ResponseTemplate};
use futures_timer::Delay;
use http_types::{Response, StatusCode};
use log::debug;
use std::ops::{Index, IndexMut};
use std::{
ops::{Index, IndexMut},
sync::{atomic::AtomicBool, Arc},
};
use tokio::sync::Notify;

/// The collection of mocks used by a `MockServer` instance to match against
/// incoming requests.
Expand Down Expand Up @@ -67,15 +71,18 @@ impl MountedMockSet {
}
}

pub(crate) fn register(&mut self, mock: Mock) -> MockId {
pub(crate) fn register(&mut self, mock: Mock) -> (Arc<(Notify, AtomicBool)>, MockId) {
let n_registered_mocks = self.mocks.len();
let active_mock = MountedMock::new(mock, n_registered_mocks);
let notify = active_mock.notify();
self.mocks.push((active_mock, MountedMockState::InScope));

MockId {
index: self.mocks.len() - 1,
generation: self.generation,
}
(
notify,
MockId {
index: self.mocks.len() - 1,
generation: self.generation,
},
)
}

pub(crate) fn reset(&mut self) {
Expand Down Expand Up @@ -179,7 +186,7 @@ mod tests {
// Assert
let mut set = MountedMockSet::new();
let mock = Mock::given(path("/")).respond_with(ResponseTemplate::new(200));
let mock_id = set.register(mock);
let (_, mock_id) = set.register(mock);

// Act
set.reset();
Expand All @@ -194,8 +201,8 @@ mod tests {
let mut set = MountedMockSet::new();
let first_mock = Mock::given(path("/")).respond_with(ResponseTemplate::new(200));
let second_mock = Mock::given(path("/hello")).respond_with(ResponseTemplate::new(500));
let first_mock_id = set.register(first_mock);
let second_mock_id = set.register(second_mock);
let (_, first_mock_id) = set.register(first_mock);
let (_, second_mock_id) = set.register(second_mock);

// Act
set.deactivate(first_mock_id);
Expand Down
25 changes: 24 additions & 1 deletion src/mounted_mock.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,7 @@
use std::sync::{atomic::AtomicBool, Arc};

use tokio::sync::Notify;

use crate::{verification::VerificationReport, Match, Mock, Request, ResponseTemplate};

/// Given the behaviour specification as a [`Mock`](crate::Mock), keep track of runtime information
Expand All @@ -14,6 +18,8 @@ pub(crate) struct MountedMock {

// matched requests:
matched_requests: Vec<crate::Request>,

notify: Arc<(Notify, AtomicBool)>,
}

impl MountedMock {
Expand All @@ -23,6 +29,7 @@ impl MountedMock {
n_matched_requests: 0,
position_in_set,
matched_requests: Vec::new(),
notify: Arc::new((Notify::new(), AtomicBool::new(false))),
}
}

Expand All @@ -46,7 +53,19 @@ impl MountedMock {
// Increase match count
self.n_matched_requests += 1;
// Keep track of request
self.matched_requests.push(request.clone())
self.matched_requests.push(request.clone());

// notification of satisfaction
if self
.specification
.expectation_range
.contains(self.n_matched_requests)
conradludgate marked this conversation as resolved.
Show resolved Hide resolved
{
self.notify
.1
.store(true, std::sync::atomic::Ordering::Release);
self.notify.0.notify_waiters();
}
}

matched
Expand All @@ -71,4 +90,8 @@ impl MountedMock {
pub(crate) fn received_requests(&self) -> Vec<crate::Request> {
self.matched_requests.clone()
}

pub(crate) fn notify(&self) -> Arc<(Notify, AtomicBool)> {
self.notify.clone()
}
}
66 changes: 66 additions & 0 deletions tests/mocks.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
use futures::FutureExt;
use http_types::StatusCode;
use serde::Serialize;
use serde_json::json;
use std::net::TcpStream;
use std::time::Duration;
use wiremock::matchers::{body_json, body_partial_json, method, path, PathExactMatcher};
use wiremock::{Mock, MockServer, ResponseTemplate};

Expand Down Expand Up @@ -273,3 +275,67 @@ async fn use_mock_guard_to_verify_requests_from_mock() {

assert_eq!(value, json!({"attempt": 99}));
}

#[async_std::test]
async fn use_mock_guard_to_await_satisfaction_readiness() {
// Arrange
let mock_server = MockServer::start().await;

let satisfy = mock_server
.register_as_scoped(
Mock::given(method("POST"))
.and(PathExactMatcher::new("satisfy"))
.respond_with(ResponseTemplate::new(200))
.expect(1),
)
.await;

let eventually_satisfy = mock_server
.register_as_scoped(
Mock::given(method("POST"))
.and(PathExactMatcher::new("eventually_satisfy"))
.respond_with(ResponseTemplate::new(200))
.expect(1),
)
.await;

// Act one
let uri = mock_server.uri();
let response = surf::post(format!("{uri}/satisfy")).await.unwrap();
assert_eq!(response.status(), StatusCode::Ok);

// Assert
satisfy
.satisfied()
.now_or_never()
.expect("should be satisfied immediately");

eventually_satisfy
.satisfied()
.now_or_never()
.ok_or(())
.expect_err("should not be satisfied yet");

// Act two
async_std::task::spawn(async move {
async_std::task::sleep(Duration::from_millis(50)).await;
let response = surf::post(format!("{uri}/eventually_satisfy"))
.await
.unwrap();
assert_eq!(response.status(), StatusCode::Ok);
});

// Assert
eventually_satisfy
.satisfied()
.now_or_never()
.ok_or(())
.expect_err("should not be satisfied yet");

async_std::io::timeout(
Duration::from_millis(100),
eventually_satisfy.satisfied().map(Ok),
)
.await
.expect("should be satisfied");
}