fabro/lib/crates/fabro-cli/tests/it/cmd/render_graph.rs
Bryan Helmkamp 95b101a26f
lint(clippy): disallow blocking std::io and std::net on Tokio paths
Extends the workspace clippy.toml — which already bans std:🧵:sleep,
std:🧵:spawn, and std::process::Command::new on Tokio paths — with:

- disallowed-types: std::io::{Read, Write, BufRead, BufReader, BufWriter}
  and std::net::{TcpStream, TcpListener, UdpSocket}
- disallowed-methods: std::io::{stdin, stdout, stderr}

Non-blocking std::io items (Error, ErrorKind, Result, IsTerminal, Cursor)
remain allowed. std::fs is intentionally deferred.

Annotates ~24 pre-existing sync call sites with #[expect(..., reason = "...")]
matching the established pattern. All annotations describe why blocking I/O
is intentional in that context (sync CLI command, test helper, pre-fork
flush, etc.), so a future conversion to async will surface as an unfulfilled
lint expectation instead of silently drifting.

Fixes one real Tokio-path issue surfaced by the new lint:
fabro-cli's server-start daemon-health poller (try_connect) was a sync fn
called from async execute_daemon; std::net::TcpStream::connect_timeout
blocked a Tokio worker for up to 100ms per poll iteration. Converted to
tokio::net::{TcpStream, UnixStream} with tokio::time::timeout.

One follow-up flagged in-code: fabro-agent/src/cli.rs's JSON event writer
uses std::io::stdout() inside tokio::spawn. Annotated with a FOLLOW-UP
reason pointing at tokio::io::stdout; left unchanged since volume is low
and scope exceeded this pass.

Verified: clippy clean, cargo +nightly fmt --check clean, full nextest
workspace run (4131 passed, 182 skipped).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-19 16:06:02 -04:00

111 lines
3.4 KiB
Rust

#![expect(
clippy::disallowed_methods,
reason = "These CLI integration tests intentionally spawn the real fabro binary and stream DOT over stdio to verify the internal render subprocess contract."
)]
#![expect(
clippy::disallowed_types,
reason = "integration tests write DOT to the spawned child's stdin via std::io::Write"
)]
use std::io::Write;
use std::process::{Command, Stdio};
use fabro_test::{fabro_snapshot, test_context};
fn render_graph_command(context: &fabro_test::TestContext) -> Command {
let mut cmd = Command::new(env!("CARGO_BIN_EXE_fabro"));
fabro_test::apply_test_isolation(&mut cmd, &context.home_dir);
cmd.current_dir(&context.temp_dir);
cmd
}
#[test]
fn help() {
let context = test_context!();
let mut cmd = context.command();
cmd.args(["__render-graph", "--help"]);
fabro_snapshot!(context.filters(), cmd, @"
success: true
exit_code: 0
----- stdout -----
Render a DOT graph to SVG (internal)
Usage: fabro __render-graph [OPTIONS]
Options:
--json Output as JSON [env: FABRO_JSON=]
--debug Enable DEBUG-level logging (default is INFO) [env: FABRO_DEBUG=]
--no-upgrade-check Disable automatic upgrade check [env: FABRO_NO_UPGRADE_CHECK=true]
--quiet Suppress non-essential output [env: FABRO_QUIET=]
--verbose Enable verbose output [env: FABRO_VERBOSE=]
-h, --help Print help
----- stderr -----
");
}
#[test]
fn render_graph_outputs_svg() {
let context = test_context!();
let mut cmd = render_graph_command(&context);
cmd.args(["__render-graph"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("render-graph subprocess should spawn");
child
.stdin
.as_mut()
.expect("stdin should be piped")
.write_all(b"digraph { a -> b }")
.expect("stdin write should succeed");
let output = child
.wait_with_output()
.expect("render-graph subprocess should exit");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).expect("stdout should be valid UTF-8");
assert!(
stdout.contains("<svg"),
"expected SVG output, got: {}",
&stdout[..stdout.len().min(200)]
);
}
#[test]
fn render_graph_bad_input_uses_render_error_protocol() {
let context = test_context!();
let mut cmd = render_graph_command(&context);
cmd.args(["__render-graph"])
.stdin(Stdio::piped())
.stdout(Stdio::piped())
.stderr(Stdio::piped());
let mut child = cmd.spawn().expect("render-graph subprocess should spawn");
child
.stdin
.as_mut()
.expect("stdin should be piped")
.write_all(b"not valid dot")
.expect("stdin write should succeed");
let output = child
.wait_with_output()
.expect("render-graph subprocess should exit");
assert!(
output.status.success(),
"stderr: {}",
String::from_utf8_lossy(&output.stderr)
);
let stdout = String::from_utf8(output.stdout).expect("stdout should be valid UTF-8");
assert!(
stdout.starts_with("RENDER_ERROR:"),
"expected render error protocol, got: {stdout}"
);
}