#![allow(
dead_code,
reason = "FakeDotRenderer + DOT_BODY_LIMIT consumed in PHASE-05+"
)]
use std::process::Stdio;
use std::time::Duration;
use async_trait::async_trait;
use tokio::io::AsyncWriteExt;
use crate::map_server::error::MapServerError;
use crate::map_server::state::DotRenderer;
pub(crate) const DOT_BODY_LIMIT: usize = 1_048_576;
const DOT_TIMEOUT: Duration = Duration::from_secs(10);
#[async_trait]
impl DotRenderer for crate::map_server::state::RealDotRenderer {
#[expect(
clippy::expect_used,
reason = "stdin configured as Stdio::piped() so take() always returns Some"
)]
async fn render_svg(&self, dot: &[u8]) -> Result<Vec<u8>, MapServerError> {
let mut child = tokio::process::Command::new("dot")
.arg("-Tsvg")
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped())
.kill_on_drop(true)
.spawn()
.map_err(|e| match e.kind() {
std::io::ErrorKind::NotFound => MapServerError::ToolUnavailable { tool: "dot" },
_ => MapServerError::Other(e.into()),
})?;
let mut stdin = child.stdin.take().expect("stdin piped");
let dot_owned = dot.to_vec();
tokio::time::timeout(DOT_TIMEOUT, stdin.write_all(&dot_owned))
.await
.map_err(|_elapsed| MapServerError::Timeout { command: "dot" })?
.map_err(|e| MapServerError::Other(e.into()))?;
drop(stdin);
let output = tokio::time::timeout(DOT_TIMEOUT, child.wait_with_output())
.await
.map_err(|_elapsed| MapServerError::Timeout { command: "dot" })?
.map_err(|e| MapServerError::Other(e.into()))?;
if !output.status.success() {
return Err(MapServerError::CommandFailed {
command: "dot",
status: output.status.code(),
stderr: String::from_utf8_lossy(&output.stderr).into_owned(),
});
}
Ok(output.stdout)
}
}
pub(crate) struct FakeDotRenderer {
pub(crate) mode: FakeDotMode,
}
pub(crate) enum FakeDotMode {
Success(Vec<u8>),
ToolUnavailable,
CommandFailed { stderr: String },
Timeout,
}
#[async_trait]
impl DotRenderer for FakeDotRenderer {
async fn render_svg(&self, _dot: &[u8]) -> Result<Vec<u8>, MapServerError> {
match &self.mode {
FakeDotMode::Success(svg) => Ok(svg.clone()),
FakeDotMode::ToolUnavailable => Err(MapServerError::ToolUnavailable { tool: "dot" }),
FakeDotMode::CommandFailed { stderr } => Err(MapServerError::CommandFailed {
command: "dot",
status: Some(1),
stderr: stderr.clone(),
}),
FakeDotMode::Timeout => Err(MapServerError::Timeout { command: "dot" }),
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[tokio::test]
async fn fake_success_returns_svg_bytes() {
let renderer = FakeDotRenderer {
mode: FakeDotMode::Success(b"<svg></svg>".to_vec()),
};
let result = renderer.render_svg(b"digraph { a -> b }").await.unwrap();
assert_eq!(result, b"<svg></svg>");
}
#[tokio::test]
async fn fake_tool_unavailable_returns_503_error() {
let renderer = FakeDotRenderer {
mode: FakeDotMode::ToolUnavailable,
};
let err = renderer
.render_svg(b"digraph { a -> b }")
.await
.unwrap_err();
match err {
MapServerError::ToolUnavailable { tool } => assert_eq!(tool, "dot"),
other => panic!("expected ToolUnavailable, got {other:?}"),
}
}
#[tokio::test]
async fn fake_command_failed_returns_422_error_with_stderr() {
let renderer = FakeDotRenderer {
mode: FakeDotMode::CommandFailed {
stderr: "syntax error".to_owned(),
},
};
let err = renderer.render_svg(b"garbage").await.unwrap_err();
match err {
MapServerError::CommandFailed {
command,
status,
stderr,
} => {
assert_eq!(command, "dot");
assert_eq!(status, Some(1));
assert_eq!(stderr, "syntax error");
}
other => panic!("expected CommandFailed, got {other:?}"),
}
}
#[tokio::test]
async fn fake_timeout_returns_504_error() {
let renderer = FakeDotRenderer {
mode: FakeDotMode::Timeout,
};
let err = renderer
.render_svg(b"digraph { a -> b }")
.await
.unwrap_err();
match err {
MapServerError::Timeout { command } => assert_eq!(command, "dot"),
other => panic!("expected Timeout, got {other:?}"),
}
}
#[tokio::test]
async fn real_dot_valid_input_returns_svg() {
if which::which("dot").is_err() {
return; }
let renderer = crate::map_server::state::RealDotRenderer;
let result = renderer.render_svg(b"digraph { a -> b }").await.unwrap();
let svg = String::from_utf8_lossy(&result);
assert!(svg.contains("<svg"), "expected SVG output, got: {svg}");
}
#[tokio::test]
async fn real_dot_garbage_input_returns_command_failed() {
if which::which("dot").is_err() {
return; }
let renderer = crate::map_server::state::RealDotRenderer;
let err = renderer.render_svg(b"not valid dot").await.unwrap_err();
match err {
MapServerError::CommandFailed { stderr, .. } => {
assert!(!stderr.is_empty(), "expected error output from dot");
}
other => panic!("expected CommandFailed, got {other:?}"),
}
}
}