use core::pin::Pin;
use core::task::{Context, Poll};
use tokio::io::{AsyncWrite, AsyncWriteExt};
use tokio::sync::{Mutex, MutexGuard};
use super::StdioError;
#[derive(Debug)]
pub struct Diagnostics<W> {
writer: Mutex<W>,
}
impl Diagnostics<tokio::io::Stderr> {
#[must_use]
pub fn stderr() -> Self {
Self::new(tokio::io::stderr())
}
}
impl<W> Diagnostics<W> {
#[must_use]
pub fn new(writer: W) -> Self {
Self {
writer: Mutex::new(writer),
}
}
}
impl<W> Diagnostics<W>
where
W: AsyncWrite + Unpin,
{
pub async fn write_all(&self, bytes: &[u8]) -> Result<(), StdioError> {
let mut writer = self.writer.lock().await;
writer
.write_all(bytes)
.await
.map_err(|_| StdioError::Transport)?;
drop(writer);
Ok(())
}
pub async fn writer(&self) -> DiagnosticsWriter<'_, W> {
DiagnosticsWriter {
guard: self.writer.lock().await,
}
}
}
#[derive(Debug)]
pub struct DiagnosticsWriter<'a, W> {
guard: MutexGuard<'a, W>,
}
impl<W> AsyncWrite for DiagnosticsWriter<'_, W>
where
W: AsyncWrite + Unpin,
{
fn poll_write(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
buf: &[u8],
) -> Poll<Result<usize, std::io::Error>> {
Pin::new(&mut *self.get_mut().guard).poll_write(cx, buf)
}
fn poll_flush(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut *self.get_mut().guard).poll_flush(cx)
}
fn poll_shutdown(
self: Pin<&mut Self>,
cx: &mut Context<'_>,
) -> Poll<Result<(), std::io::Error>> {
Pin::new(&mut *self.get_mut().guard).poll_shutdown(cx)
}
}
#[cfg(test)]
mod tests {
use std::io;
use assert_matches::assert_matches;
use tokio::io::AsyncWriteExt;
use super::super::StdioError;
use super::super::testing::{Broken, Capture};
use super::Diagnostics;
#[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_diagnostic_lands_in_the_injected_buffer() -> Result<(), Failure> {
let capture = Capture::shared();
let diagnostics = Diagnostics::new(capture.clone());
diagnostics
.write_all(b"peer announced version 2025-06-18\n")
.await?;
assert_eq!(capture.contents(), "peer announced version 2025-06-18\n");
Ok(())
}
#[tokio::test]
async fn the_real_channel_is_constructible_and_writable() -> Result<(), Failure> {
let diagnostics = Diagnostics::stderr();
diagnostics
.write_all(b"diagnostics: the stderr channel is live\n")
.await?;
Ok(())
}
#[tokio::test]
async fn successive_diagnostics_accumulate() -> Result<(), Failure> {
let capture = Capture::shared();
let diagnostics = Diagnostics::new(capture.clone());
diagnostics.write_all(b"first\n").await?;
diagnostics.write_all(b"second\n").await?;
assert_eq!(capture.contents(), "first\nsecond\n");
Ok(())
}
#[tokio::test]
async fn the_channel_also_works_as_a_plain_async_writer() -> Result<(), Failure> {
let capture = Capture::shared();
let diagnostics = Diagnostics::new(capture.clone());
let mut borrowed = diagnostics.writer().await;
borrowed.write_all(b"through the trait\n").await?;
borrowed.flush().await?;
drop(borrowed);
assert_eq!(capture.contents(), "through the trait\n");
Ok(())
}
#[tokio::test]
async fn a_short_write_is_reported_as_a_count_not_an_error() -> Result<(), Failure> {
let capture = Capture::shared();
let diagnostics = Diagnostics::new(capture.clone());
let mut borrowed = diagnostics.writer().await;
borrowed
.write_all(b"a longer line than the limit allows")
.await?;
assert_eq!(capture.contents(), "a longer line than the limit allows");
Ok(())
}
#[tokio::test]
async fn a_refused_diagnostic_is_a_typed_error() {
let diagnostics = Diagnostics::new(Broken);
let result = diagnostics.write_all(b"never lands\n").await;
assert_matches!(result, Err(StdioError::Transport));
}
}