use bytes::Bytes;
use hclient_core::unversioned::{Timer, Transport};
use hclient_core::{Capabilities, Error, ErrorKind, RequestBody, Timeouts, UnsupportedCapability};
use std::error::Error as StdError;
use std::fmt::Display;
use std::sync::Arc;
use std::sync::atomic::Ordering;
fn assert_send_sync<T: Send + Sync>() {}
fn assert_send<T: Send>() {}
#[test]
fn capability_types_are_send_and_sync() {
assert_send_sync::<Capabilities>();
assert_send_sync::<Timeouts>();
assert_send_sync::<UnsupportedCapability>();
}
#[test]
fn error_is_send_sync_and_constructs_a_real_error_not_just_compiles() {
assert_send_sync::<Error>();
let e = Error::new(ErrorKind::Other, Never);
assert_eq!(e.kind(), &ErrorKind::Other);
}
#[test]
fn request_body_and_its_request_are_send() {
assert_send::<RequestBody>();
assert_send::<http::Request<RequestBody>>();
}
struct Echo {
caps: Capabilities,
}
#[derive(Debug)]
struct Never;
impl Display for Never {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "never")
}
}
impl StdError for Never {}
impl Transport for Echo {
type Body = http_body_util::Full<Bytes>;
type Error = Error;
async fn execute(
&self,
_req: http::Request<RequestBody>,
) -> Result<http::Response<Self::Body>, Self::Error> {
Ok(http::Response::new(http_body_util::Full::new(
Bytes::from_static(b"ok"),
)))
}
fn to_error(&self, e: Self::Error) -> Error {
e
}
fn capabilities(&self) -> &Capabilities {
&self.caps
}
}
struct Bare {
caps: Capabilities,
}
#[derive(Debug, PartialEq)]
struct Custom;
impl Display for Custom {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "backend said no")
}
}
impl StdError for Custom {}
impl Transport for Bare {
type Body = http_body_util::Full<Bytes>;
type Error = Custom;
async fn execute(
&self,
_req: http::Request<RequestBody>,
) -> Result<http::Response<Self::Body>, Self::Error> {
Err(Custom)
}
fn capabilities(&self) -> &Capabilities {
&self.caps
}
}
struct Forgetful {
caps: Capabilities,
}
impl Transport for Forgetful {
type Body = http_body_util::Full<Bytes>;
type Error = Error;
async fn execute(
&self,
_req: http::Request<RequestBody>,
) -> Result<http::Response<Self::Body>, Self::Error> {
Err(Error::new(ErrorKind::Resolve, Never))
}
fn capabilities(&self) -> &Capabilities {
&self.caps
}
}
#[test]
fn the_default_passes_our_own_error_through_even_when_a_backend_forgets_to_override() {
let t = Forgetful {
caps: Capabilities::default(),
};
let e = t.to_error(Error::new(ErrorKind::Tls, Never));
assert_eq!(
e.kind(),
&ErrorKind::Tls,
"the default must recognize its own `Error` and pass it through unchanged"
);
assert_eq!(
e.to_string(),
"Tls: never",
"and not nest a second category in front of the real one"
);
}
#[test]
fn to_error_defaults_to_other_and_keeps_the_source_intact() {
let t = Bare {
caps: Capabilities::default(),
};
let e = t.to_error(Custom);
assert_eq!(e.kind(), &ErrorKind::Other);
let src = StdError::source(&e).expect("Error::new always sets a source");
assert_eq!(
src.downcast_ref::<Custom>(),
Some(&Custom),
"the source must remain itself, not become a string"
);
}
#[test]
fn a_backend_whose_error_is_already_ours_can_pass_it_through_unchanged() {
let t = Echo {
caps: Capabilities::default(),
};
let e = t.to_error(Error::new(ErrorKind::Tls, Never));
assert_eq!(
e.kind(),
&ErrorKind::Tls,
"the identity must preserve the category, not rebuild the error"
);
assert_eq!(
e.to_string(),
"Tls: never",
"and not nest a second category in front of the real one"
);
}
#[test]
fn send_propagates_without_being_declared() {
fn assert_send<T: Send>(_: T) {}
let t = Echo {
caps: Capabilities::default(),
};
let fut = t.execute(http::Request::new(RequestBody::Empty));
assert_send(fut);
}
#[test]
fn non_send_transport_still_satisfies_the_trait() {
struct Local {
caps: Capabilities,
_rc: std::rc::Rc<()>,
}
impl Transport for Local {
type Body = http_body_util::Full<Bytes>;
type Error = Error;
async fn execute(
&self,
_req: http::Request<RequestBody>,
) -> Result<http::Response<Self::Body>, Self::Error> {
Err(Error::new(ErrorKind::Other, Never))
}
fn capabilities(&self) -> &Capabilities {
&self.caps
}
}
let _ = Local {
caps: Capabilities::default(),
_rc: std::rc::Rc::new(()),
};
}
#[test]
fn a_transport_whose_error_is_not_send_still_implements_the_trait() {
#[derive(Debug)]
struct NotSend(std::marker::PhantomData<std::rc::Rc<()>>);
impl Display for NotSend {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "not send")
}
}
impl StdError for NotSend {}
struct LocalErr {
caps: Capabilities,
}
impl Transport for LocalErr {
type Body = http_body_util::Full<Bytes>;
type Error = NotSend;
async fn execute(
&self,
_req: http::Request<RequestBody>,
) -> Result<http::Response<Self::Body>, Self::Error> {
Err(NotSend(std::marker::PhantomData))
}
fn capabilities(&self) -> &Capabilities {
&self.caps
}
}
let t = LocalErr {
caps: Capabilities::default(),
};
assert!(!t.capabilities().streaming_request_body);
}
struct Fake(std::cell::Cell<u64>);
impl Timer for Fake {
type Instant = u64;
type Sleep = std::future::Ready<()>;
fn sleep(&self, _d: core::time::Duration) -> Self::Sleep {
std::future::ready(())
}
fn now(&self) -> Self::Instant {
let v = self.0.get();
self.0.set(v + 1);
v
}
fn elapsed_since(&self, earlier: Self::Instant) -> core::time::Duration {
core::time::Duration::from_secs(self.now().saturating_sub(earlier))
}
}
fn are_ordered<T: Timer>(a: T::Instant, b: T::Instant) -> bool {
a < b
}
#[test]
fn captured_instants_are_orderable_without_a_third_now_call() {
let t = Fake(std::cell::Cell::new(0));
let a = t.now();
let b = t.now();
assert!(
are_ordered::<Fake>(a, b),
"second capture must order after the first"
);
}
#[test]
fn a_non_send_backend_still_satisfies_the_websocket_seam() {
use futures_core::Stream;
use futures_sink::Sink;
use hclient_core::unversioned::{Message, WebSocket, WebSocketConnect};
use std::pin::Pin;
use std::task::{Context, Poll};
struct LocalSocket {
_rc: std::rc::Rc<()>,
}
impl Stream for LocalSocket {
type Item = Result<Message, Error>;
fn poll_next(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
Poll::Ready(None)
}
}
impl Sink<Message> for LocalSocket {
type Error = Error;
fn poll_ready(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
Poll::Ready(Ok(()))
}
fn start_send(self: Pin<&mut Self>, _item: Message) -> Result<(), Error> {
Ok(())
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
Poll::Ready(Ok(()))
}
fn poll_close(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<Result<(), Error>> {
Poll::Ready(Ok(()))
}
}
impl WebSocket for LocalSocket {}
struct LocalBackend(std::rc::Rc<()>);
impl WebSocketConnect for LocalBackend {
type WebSocket = LocalSocket;
async fn websocket(&self, _req: http::Request<()>) -> Result<LocalSocket, Error> {
Ok(LocalSocket {
_rc: self.0.clone(),
})
}
}
let _ = LocalBackend(std::rc::Rc::new(()));
}
#[test]
fn a_non_send_hook_reaches_a_bodys_poll_frame_and_the_transport_still_implements_transport() {
use hclient_core::unversioned::{CloseReason, Closed, ConnectionId, Event, Hooks};
use http_body::{Body as HttpBody, Frame};
use std::pin::Pin;
use std::task::{Context, Poll, Waker};
#[derive(Clone)]
struct LocalHook(std::rc::Rc<std::cell::Cell<usize>>);
impl Hooks for LocalHook {
fn on(&self, _event: Event<'_>) {
self.0.set(self.0.get() + 1);
}
}
struct HookedBody<H> {
hooks: H,
told: bool,
}
impl<H: Hooks + Unpin> HttpBody for HookedBody<H> {
type Data = Bytes;
type Error = Error;
fn poll_frame(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
) -> Poll<Option<Result<Frame<Bytes>, Error>>> {
let this = self.get_mut();
if !this.told {
this.told = true;
this.hooks.on(Event::Closed(Closed {
id: ConnectionId::UNWATCHED,
reason: CloseReason::Ended,
}));
}
Poll::Ready(None)
}
}
struct Watched<H> {
caps: Capabilities,
hooks: H,
}
impl<H: Hooks + Clone + Unpin> Transport for Watched<H> {
type Body = HookedBody<H>;
type Error = Error;
async fn execute(
&self,
_req: http::Request<RequestBody>,
) -> Result<http::Response<Self::Body>, Self::Error> {
Ok(http::Response::new(HookedBody {
hooks: self.hooks.clone(),
told: false,
}))
}
fn capabilities(&self) -> &Capabilities {
&self.caps
}
}
let seen = std::rc::Rc::new(std::cell::Cell::new(0));
let t = Watched {
caps: Capabilities::default(),
hooks: LocalHook(std::rc::Rc::clone(&seen)),
};
let mut cx = Context::from_waker(Waker::noop());
let mut fut = std::pin::pin!(t.execute(http::Request::new(RequestBody::Empty)));
let Poll::Ready(Ok(resp)) = fut.as_mut().poll(&mut cx) else {
panic!("this transport answers on the first poll");
};
let mut body = std::pin::pin!(resp.into_body());
assert!(
matches!(body.as_mut().poll_frame(&mut cx), Poll::Ready(None)),
"the body ends on its first poll"
);
assert_eq!(
seen.get(),
1,
"the hook must have been called from poll_frame — the whole of P13"
);
}
#[test]
fn a_send_hook_leaves_the_transport_and_its_body_send() {
use hclient_core::unversioned::{Event, Hooks, NoHooks};
struct Counting(std::sync::atomic::AtomicUsize);
impl Hooks for Counting {
fn on(&self, _event: Event<'_>) {
self.0.fetch_add(1, Ordering::Relaxed);
}
}
assert_send::<NoHooks>();
assert_send_sync::<NoHooks>();
assert_send::<Counting>();
assert_send::<Arc<Counting>>();
assert_eq!(
std::mem::size_of::<NoHooks>(),
0,
"the no-op hook must be zero-sized, or every transport that stores \
one pays for a caller who asked for nothing"
);
const { assert!(!NoHooks::WATCHING) };
}
#[test]
fn the_id_that_names_no_connection_is_one_the_counter_never_hands_out() {
use hclient_core::unversioned::ConnectionId;
let mut seen = std::collections::HashSet::new();
for _ in 0..64 {
let id = ConnectionId::next();
assert_ne!(
id,
ConnectionId::UNWATCHED,
"the counter handed out the one id that is supposed to mean \
*no connection* — every hook matching a `Head` against the \
connections it was told about would now find one"
);
assert_ne!(
id.get(),
ConnectionId::UNWATCHED.get(),
"and the same through `get()`, which is what a log line prints \
and what `hclient-wasi`'s guest transcript compares"
);
assert!(
seen.insert(id),
"ids must be distinct, or a `Closed` cannot be matched to the \
`Connected` that opened the same socket — which is the only \
reason this type exists"
);
}
}