use alux_ext::ext;
use alux_http::{HttpBind, HttpServerAlg, HttpServerSetup};
use core::error::Error;
use core::fmt::{self, Display, Formatter};
use core::str;
use core::time::Duration;
use std::io;
use std::net::{SocketAddr, TcpListener};
use std::time::Instant;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::TcpStream;
use tokio::task::LocalSet;
pub type LifecycleError = Box<dyn Error + Send + Sync>;
pub const LIMIT: Duration = Duration::from_secs(20);
pub const SETTLE: Duration = Duration::from_millis(250);
#[ext(name = ExpectLifecycleExt)]
pub impl<This> This
where
This: HttpServerAlg,
This::Error: Into<LifecycleError>,
{
async fn expect_reopening<Compile>(mut self, mut compile: Compile) -> Result<(), LifecycleError>
where
Compile: FnMut() -> Self::Program,
{
LocalSet::new()
.run_until(async move {
let bind = free_address()?;
let mut open = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
answered(bind).await?;
self.expect_closing(&mut open).await?;
let mut reopened = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
answered(bind).await?;
self.expect_closing(&mut reopened).await
})
.await
}
async fn expect_answering_what_is_in_flight<Compile>(mut self, mut compile: Compile) -> Result<(), LifecycleError>
where
Compile: FnMut() -> Self::Program,
{
LocalSet::new()
.run_until(async move {
let bind = free_address()?;
let mut open = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
let mut held = hold(bind, "/pause").await?;
let (closed, delivered) = tokio::join!(self.expect_closing(&mut open), read_answer(&mut held));
closed?;
delivered?;
drop(held);
let mut reopened = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
answered(bind).await?;
self.expect_closing(&mut reopened).await
})
.await
}
async fn expect_ending_what_outlives_the_drain<Compile>(
mut self,
mut compile: Compile,
) -> Result<(), LifecycleError>
where
Compile: FnMut() -> Self::Program,
{
LocalSet::new()
.run_until(async move {
let bind = free_address()?;
let mut open = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
let mut held = hold(bind, "/slow").await?;
let (over, ended) = tokio::join!(self.expect_ending(&mut open), read_ending(&mut held));
over?;
ended?;
drop(held);
let mut reopened = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
answered(bind).await?;
self.expect_closing(&mut reopened).await
})
.await
}
async fn measure_closing<Compile>(
mut self,
mut compile: Compile,
asking_for: &'static str,
) -> Result<Measured, LifecycleError>
where
Compile: FnMut() -> Self::Program,
{
LocalSet::new()
.run_until(async move {
let bind = free_address()?;
let mut open = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
let mut held = hold(bind, asking_for).await?;
let started = Instant::now();
let (times, delivered) = tokio::join!(
async {
let closed = self.close(&mut open).await;
let at_close = started.elapsed();
let ended = self.end(&mut open).await;
(at_close, started.elapsed(), closed.and(ended))
},
read_outcome(&mut held, started)
);
times.2.map_err(Into::into)?;
drop(held);
let reusable = self.open(HttpServerSetup::new(bind, compile())).await;
let measured = Measured {
asked: asking_for,
closed: times.0,
ended: times.1,
in_flight: delivered.0,
at: delivered.1,
reusable: reusable.is_ok(),
};
if let Ok(mut reusable) = reusable {
self.close(&mut reusable).await.map_err(Into::into)?;
}
Ok(measured)
})
.await
}
async fn expect_closing(&mut self, open: &mut Self::Open) -> Result<(), LifecycleError> {
let closing = Instant::now();
let closed = tokio::time::timeout(LIMIT, self.close(open))
.await
.map_err(|_| io::Error::other(format!("closing did not return inside {LIMIT:?}")))?;
closed.map_err(Into::into)?;
bounded_by_the_server(closing, "closing")
}
async fn expect_ending(&mut self, open: &mut Self::Open) -> Result<(), LifecycleError> {
let ending = Instant::now();
let ended = tokio::time::timeout(LIMIT, self.end(open))
.await
.map_err(|_| io::Error::other(format!("ending did not return inside {LIMIT:?}")))?;
ended.map_err(Into::into)?;
bounded_by_the_server(ending, "ending")
}
async fn expect_reopening_while_a_connection_is_held<Compile>(
mut self,
mut compile: Compile,
) -> Result<(), LifecycleError>
where
Compile: FnMut() -> Self::Program,
{
LocalSet::new()
.run_until(async move {
let bind = free_address()?;
let mut open = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
let held = hold(bind, "/slow").await?;
self.expect_closing(&mut open).await?;
let mut reopened = self.open(HttpServerSetup::new(bind, compile())).await.map_err(Into::into)?;
answered(bind).await?;
drop(held);
self.expect_closing(&mut reopened).await
})
.await
}
}
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
pub enum InFlight {
Answered(u16),
CutOff(u16),
Ended,
Unfinished,
}
impl InFlight {
pub const fn is_answered(self) -> bool {
matches!(self, Self::Answered(_))
}
}
impl Display for InFlight {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
match self {
Self::Answered(status) => write!(f, "answered {status}"),
Self::CutOff(status) => write!(f, "{status} cut off mid-answer"),
Self::Ended => f.write_str("ended unanswered"),
Self::Unfinished => f.write_str("still waiting"),
}
}
}
async fn read_outcome(connection: &mut TcpStream, started: Instant) -> (InFlight, Duration) {
let mut answer = Vec::new();
let mut first = None;
let read = tokio::time::timeout(LIMIT, async {
let mut arriving = [0; 1024];
while let Ok(read) = connection.read(&mut arriving).await {
if read == 0 {
break;
}
first.get_or_insert_with(|| started.elapsed());
answer.extend_from_slice(&arriving[..read]);
if states_a_whole_answer(&answer) {
break;
}
}
})
.await;
if read.is_err() {
return (InFlight::Unfinished, started.elapsed());
}
(read_as_answer(&answer), first.unwrap_or_else(|| started.elapsed()))
}
fn read_as_answer(answer: &[u8]) -> InFlight {
let Some(status) = stated_status(answer) else {
return InFlight::Ended;
};
if states_a_whole_answer(answer) {
return InFlight::Answered(status);
}
match answer.windows(4).position(|four| four == b"\r\n\r\n") {
Some(head) => {
let head = String::from_utf8_lossy(&answer[..head]).to_lowercase();
if stated_length(&head).is_none() && !head.contains("transfer-encoding: chunked") {
InFlight::Answered(status)
} else {
InFlight::CutOff(status)
}
}
None => InFlight::CutOff(status),
}
}
fn states_a_whole_answer(answer: &[u8]) -> bool {
let Some(head) = answer.windows(4).position(|four| four == b"\r\n\r\n") else {
return false;
};
let (head, body) = answer.split_at(head + 4);
let head = String::from_utf8_lossy(head).to_lowercase();
match stated_length(&head) {
Some(stated) => body.len() >= stated,
None => head.contains("transfer-encoding: chunked") && body.ends_with(b"0\r\n\r\n"),
}
}
fn stated_status(answer: &[u8]) -> Option<u16> {
let line = answer.split(|byte| *byte == b'\r').next()?;
str::from_utf8(line).ok()?.split_whitespace().nth(1)?.parse().ok()
}
fn stated_length(head: &str) -> Option<usize> {
head.lines().find_map(|line| line.strip_prefix("content-length:")?.trim().parse().ok())
}
#[derive(Debug, Clone, Copy)]
pub struct Measured {
pub asked: &'static str,
pub closed: Duration,
pub ended: Duration,
pub in_flight: InFlight,
pub at: Duration,
pub reusable: bool,
}
impl Display for Measured {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let Self { asked, closed, ended, in_flight, at, reusable } = self;
let (closed, ended, at) = (closed.as_secs_f64(), ended.as_secs_f64(), at.as_secs_f64());
write!(f, "{asked} | {in_flight} at {at:.4}s")?;
write!(f, " | close {closed:.4}s | end {ended:.4}s | reusable {reusable}")
}
}
fn meaned(durations: impl ExactSizeIterator<Item = Duration>) -> Duration {
let Ok(rounds) = u32::try_from(durations.len()) else {
return Duration::ZERO;
};
durations.sum::<Duration>().checked_div(rounds).unwrap_or_default()
}
#[derive(Debug, Clone, Copy)]
pub struct Averaged {
pub asked: &'static str,
pub rounds: usize,
pub closed: Duration,
pub ended: Duration,
pub range: Duration,
pub in_flight: InFlight,
pub at: Duration,
pub reusable: bool,
}
impl Averaged {
pub fn over(rounds: &[Measured]) -> Result<Self, LifecycleError> {
let [first, rest @ ..] = rounds else {
return Err(io::Error::other("averaging nothing").into());
};
if let Some(differs) =
rest.iter().find(|round| round.in_flight != first.in_flight || round.reusable != first.reusable)
{
let (asked, first, differs) = (first.asked, first.in_flight, differs.in_flight);
return Err(io::Error::other(format!("{asked} was {first} in one round and {differs} in another")).into());
}
let closed = rounds.iter().map(|round| round.closed);
let slowest = closed.clone().max().unwrap_or_default();
let quickest = closed.clone().min().unwrap_or_default();
Ok(Self {
asked: first.asked,
rounds: rounds.len(),
closed: meaned(closed),
ended: meaned(rounds.iter().map(|round| round.ended)),
range: slowest.saturating_sub(quickest),
in_flight: first.in_flight,
at: meaned(rounds.iter().map(|round| round.at)),
reusable: first.reusable,
})
}
}
impl Display for Averaged {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
let Self { asked, rounds, closed, ended, range, in_flight, at, reusable } = self;
let (closed, ended) = (closed.as_secs_f64(), ended.as_secs_f64());
let (range, at) = (range.as_secs_f64(), at.as_secs_f64());
write!(f, "{asked} | {in_flight} at {at:.4}s | close {closed:.4}s | end {ended:.4}s")?;
write!(f, " | reusable {reusable} | range {range:.4}s over {rounds}")
}
}
fn bounded_by_the_server(since: Instant, what: &str) -> Result<(), LifecycleError> {
if since.elapsed() >= LIMIT {
return Err(io::Error::other(format!("{what} was bounded by the scenario rather than by the server")).into());
}
Ok(())
}
pub(crate) fn free_address() -> Result<HttpBind, LifecycleError> {
let probe = TcpListener::bind("127.0.0.1:0")?;
Ok(HttpBind::new(probe.local_addr()?))
}
pub(crate) async fn answered(bind: HttpBind) -> Result<(), LifecycleError> {
let address = bind.address();
let mut connection = TcpStream::connect(address).await?;
connection.write_all(asking(address, "/items").as_bytes()).await?;
match read_outcome(&mut connection, Instant::now()).await.0 {
InFlight::Answered(200) => Ok(()),
read => Err(io::Error::other(format!("the server at {address} {read}")).into()),
}
}
pub(crate) async fn hold(bind: HttpBind, asking_for: &str) -> Result<TcpStream, LifecycleError> {
let address = bind.address();
let mut connection = TcpStream::connect(address).await?;
connection.write_all(asking(address, "/items").as_bytes()).await?;
read_answer(&mut connection).await?;
connection.write_all(asking(address, asking_for).as_bytes()).await?;
tokio::time::sleep(SETTLE).await;
Ok(connection)
}
async fn read_answer(connection: &mut TcpStream) -> Result<(), LifecycleError> {
match read_outcome(connection, Instant::now()).await.0 {
InFlight::Answered(200) => Ok(()),
read => Err(io::Error::other(format!("the closing server {read}")).into()),
}
}
async fn read_ending(connection: &mut TcpStream) -> Result<(), LifecycleError> {
match read_outcome(connection, Instant::now()).await.0 {
InFlight::Ended | InFlight::CutOff(_) => Ok(()),
read => Err(io::Error::other(format!("the closed server {read} anyway")).into()),
}
}
fn asking(address: SocketAddr, path: &str) -> String {
format!("GET {path} HTTP/1.1\r\nHost: {address}\r\n\r\n")
}