use std::io;
use std::pin::Pin;
use std::sync::{Arc, Mutex, PoisonError};
use std::task::{Context, Poll};
use tokio::io::{AsyncWrite, ReadBuf};
#[derive(Clone, Default)]
pub(super) struct Capture {
written: Arc<Mutex<Vec<u8>>>,
}
impl Capture {
#[must_use]
pub(super) fn shared() -> Self {
Self::default()
}
#[must_use]
pub(super) fn bytes(&self) -> Vec<u8> {
self.written.lock().map_or_else(
|poisoned| poisoned.into_inner().clone(),
|written| written.clone(),
)
}
#[must_use]
pub(super) fn contents(&self) -> String {
String::from_utf8_lossy(&self.bytes()).into_owned()
}
}
impl AsyncWrite for Capture {
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<io::Result<usize>> {
let this = self.get_mut();
let mut written = this.written.lock().unwrap_or_else(PoisonError::into_inner);
written.extend_from_slice(buf);
drop(written);
Poll::Ready(Ok(buf.len()))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
pub(super) struct Broken;
impl AsyncWrite for Broken {
fn poll_write(
self: Pin<&mut Self>,
_cx: &mut Context<'_>,
_buf: &[u8],
) -> Poll<io::Result<usize>> {
Poll::Ready(Err(io::Error::other("injected write failure")))
}
fn poll_flush(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
fn poll_shutdown(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<io::Result<()>> {
Poll::Ready(Ok(()))
}
}
const _: fn(&mut ReadBuf<'_>) = |_| {};
#[cfg(test)]
mod tests {
use std::io;
use tokio::io::AsyncWriteExt;
use super::{Broken, Capture};
use crate::adapters::primary::stdio::StdioError;
#[derive(Debug)]
#[expect(dead_code, reason = "the payloads are read by the derived Debug")]
enum Failure {
Stdio(StdioError),
Io(io::Error),
}
impl From<StdioError> for Failure {
fn from(error: StdioError) -> Self {
Self::Stdio(error)
}
}
impl From<io::Error> for Failure {
fn from(error: io::Error) -> Self {
Self::Io(error)
}
}
#[tokio::test]
async fn a_handle_taken_before_the_write_still_sees_it() -> Result<(), Failure> {
let capture = Capture::shared();
let mut writer = capture.clone();
writer.write_all(b"written").await?;
assert_eq!(capture.contents(), "written");
Ok(())
}
#[tokio::test]
async fn both_accessors_agree_and_neither_consumes() -> Result<(), Failure> {
let capture = Capture::shared();
let mut writer = capture.clone();
writer.write_all(b"bytes").await?;
assert_eq!(capture.bytes(), b"bytes".to_vec());
assert_eq!(capture.bytes(), b"bytes".to_vec());
assert_eq!(capture.contents(), "bytes");
assert_eq!(capture.contents(), "bytes");
Ok(())
}
#[tokio::test]
async fn successive_writes_accumulate() -> Result<(), Failure> {
let capture = Capture::shared();
let mut writer = capture.clone();
writer.write_all(b"one\n").await?;
writer.write_all(b"two\n").await?;
assert_eq!(capture.contents(), "one\ntwo\n");
Ok(())
}
#[tokio::test]
async fn the_broken_writer_refuses() {
let mut writer = Broken;
assert!(writer.write_all(b"anything").await.is_err());
}
}