agent_first_data/cli.rs
1use crate::output::{OutputFormat, OutputTo};
2use crate::protocol::{
3 BuildError, Event, LogLevel, ProtocolViolation, json_error, json_log, json_progress,
4 json_result, validate_protocol_event,
5};
6use crate::redaction::OutputOptions;
7use serde_json::Value;
8
9// ═══════════════════════════════════════════
10// Public API: CLI Helpers
11// ═══════════════════════════════════════════
12
13/// Parsed and normalized log filters (trimmed, lowercased, deduplicated).
14///
15/// Semantics (a stable contract):
16/// - An **empty** set emits no logs (filtering is opt-in, not opt-out).
17/// - The single wildcard word `"all"` emits every log. (`"*"` is not special —
18/// there is one wildcard spelling, not two.)
19/// - Otherwise a log is emitted iff its lowercased event name **starts with**
20/// any filter string (prefix match).
21///
22/// Consequence to know: a mistyped filter simply matches nothing, so it
23/// silently emits no output — that is the documented behavior, not a bug.
24#[derive(Clone, Debug, Default, PartialEq, Eq)]
25pub struct LogFilters(Vec<String>);
26
27impl LogFilters {
28 /// Create a new LogFilters from filter strings. Entries are trimmed,
29 /// lowercased, and de-duplicated; empty entries are dropped.
30 pub fn new<I, S>(filters: I) -> Self
31 where
32 I: IntoIterator<Item = S>,
33 S: AsRef<str>,
34 {
35 let mut out: Vec<String> = Vec::new();
36 for entry in filters {
37 let s = entry.as_ref().trim().to_ascii_lowercase();
38 if !s.is_empty() && !out.contains(&s) {
39 out.push(s);
40 }
41 }
42 Self(out)
43 }
44
45 /// Check if an event should be logged based on these filters.
46 ///
47 /// Returns `false` if empty (no logs). Returns `true` if the set contains
48 /// the wildcard word `"all"`. Otherwise returns `true` iff the lowercased
49 /// event name starts with any filter (prefix match).
50 pub fn enabled(&self, event: &str) -> bool {
51 if self.0.is_empty() {
52 return false;
53 }
54 let event_lower = event.to_ascii_lowercase();
55 if self.0.contains(&"all".to_string()) {
56 return true;
57 }
58 self.0.iter().any(|filter| event_lower.starts_with(filter))
59 }
60
61 /// Check if this filter set is empty (no filters configured).
62 pub fn is_empty(&self) -> bool {
63 self.0.is_empty()
64 }
65
66 /// Access the underlying filter strings as a slice.
67 pub fn as_slice(&self) -> &[String] {
68 &self.0
69 }
70}
71
72/// Parse `--output` flag value into [`OutputFormat`].
73///
74/// Returns `Err` with a value-safe message suitable for passing to
75/// [`crate::build_cli_error`] on unknown values.
76///
77/// ```
78/// use agent_first_data::{cli_parse_output, OutputFormat};
79/// assert!(matches!(cli_parse_output("json"), Ok(OutputFormat::Json)));
80/// assert!(cli_parse_output("xml").is_err());
81/// ```
82pub fn cli_parse_output(s: &str) -> Result<OutputFormat, String> {
83 s.parse()
84 .map_err(|_| "invalid --output format: expected json, yaml, or plain".to_string())
85}
86
87/// Normalize `--log` flag entries: trim, lowercase, deduplicate, remove empty.
88///
89/// Accepts pre-split entries produced by a caller's comma-list parser.
90///
91/// ```
92/// use agent_first_data::{cli_parse_log_filters, LogFilters};
93/// let f = cli_parse_log_filters(&["Query", " error ", "query"]);
94/// assert_eq!(f, LogFilters::new(["query", "error"]));
95/// ```
96pub fn cli_parse_log_filters<S: AsRef<str>>(entries: &[S]) -> LogFilters {
97 LogFilters::new(entries.iter().map(AsRef::as_ref))
98}
99
100/// Error returned by [`CliEmitter`].
101#[derive(Debug)]
102pub enum CliEmitterError {
103 /// A protocol-validation failure.
104 Validation(ProtocolViolation),
105 /// An event builder rejected its inputs (empty code/message, reserved field).
106 Build(BuildError),
107 /// An emitter lifecycle rule was violated (terminal ordering).
108 Lifecycle(String),
109 /// Writing the event to the underlying writer failed.
110 Write(std::io::Error),
111}
112
113impl std::fmt::Display for CliEmitterError {
114 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
115 match self {
116 Self::Validation(v) => write!(f, "{v}"),
117 Self::Build(e) => write!(f, "{e}"),
118 Self::Lifecycle(err) => f.write_str(err),
119 Self::Write(err) => write!(f, "failed to write CLI event: {err}"),
120 }
121 }
122}
123
124impl CliEmitterError {
125 /// Return the underlying writer error, when event emission failed during I/O.
126 pub const fn io_error(&self) -> Option<&std::io::Error> {
127 match self {
128 Self::Write(err) => Some(err),
129 Self::Validation(_) | Self::Build(_) | Self::Lifecycle(_) => None,
130 }
131 }
132
133 /// Return the underlying writer error kind, when available.
134 pub fn io_error_kind(&self) -> Option<std::io::ErrorKind> {
135 self.io_error().map(std::io::Error::kind)
136 }
137}
138
139impl std::error::Error for CliEmitterError {
140 fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
141 self.io_error()
142 .map(|err| err as &(dyn std::error::Error + 'static))
143 }
144}
145
146impl From<std::io::Error> for CliEmitterError {
147 fn from(err: std::io::Error) -> Self {
148 Self::Write(err)
149 }
150}
151
152impl From<BuildError> for CliEmitterError {
153 fn from(err: BuildError) -> Self {
154 Self::Build(err)
155 }
156}
157
158/// Where a [`CliEmitter`] sends its events, selected by `--output-to`.
159///
160/// The stream an event lands on follows the program's *consumption mode*, not
161/// the event's shape (see the spec's CLI Event Framing):
162///
163/// - [`OutputTo::Split`] (the default) is finite one-shot mode: `result` goes
164/// to `stdout`, while `error`/`progress`/`log` go to `stderr`. `stdout`
165/// therefore carries only successful payloads, so a shell capture or pipe
166/// never mistakes a failure for data.
167/// - [`OutputTo::Stdout`] / [`OutputTo::Stderr`] are event-stream mode: every
168/// event, including `error`, is collapsed onto that one stream so a consumer
169/// reading it in order (`kind`-branching) sees preserved ordering.
170///
171/// A command is an event stream when it produces more than one caller-needed
172/// output over time — a chunked payload, or an address the caller must act on
173/// before the command can report its outcome. Such a command defaults to
174/// [`OutputTo::Stdout`] and rejects an explicit `split`, because splitting it
175/// would strand the caller's data on the diagnostic stream. If a
176/// `kind:"progress"` event carries a payload the caller must read, the command
177/// is an event stream that has not declared itself.
178/// Stateful emitter for structured CLI executions.
179///
180/// The output format, redaction policy, and stream routing are fixed when the
181/// emitter is created. Emitting after a terminal event, emitting a repeated
182/// terminal event, and writer failures all return explicit errors.
183///
184/// Routing follows the consumption mode ([`OutputTo`]):
185///
186/// - [`CliEmitter::finite`] / [`CliEmitter::finite_with`] — finite one-shot:
187/// `result` → the primary writer (stdout), `error`/`progress`/`log` → the
188/// diagnostic writer (stderr). This is the recommended default for a
189/// one-shot CLI, so shell capture and pipelines never treat a failure as data.
190/// - [`CliEmitter::stream`] — event stream: every event, including `error`,
191/// goes to the single writer, preserving interleaved ordering.
192/// - [`CliEmitter::from_output_to`] builds either shape from a parsed
193/// [`OutputTo`] selector.
194pub struct CliEmitter<W: std::io::Write> {
195 writer: W,
196 diagnostic: Option<Box<dyn std::io::Write>>,
197 format: OutputFormat,
198 output_options: OutputOptions,
199 strict_protocol: bool,
200 terminal_emitted: bool,
201}
202
203impl<W: std::io::Write> CliEmitter<W> {
204 /// Create an event-stream emitter: every event goes to `writer`.
205 ///
206 /// Alias for [`CliEmitter::stream`]. Use [`CliEmitter::finite`] for a
207 /// one-shot command that should split `result`/`error` across stdout/stderr.
208 pub fn new(writer: W, format: OutputFormat) -> Self {
209 Self::stream(writer, format)
210 }
211
212 /// Create an event-stream emitter with custom output options.
213 pub fn with_options(writer: W, format: OutputFormat, output_options: OutputOptions) -> Self {
214 Self {
215 writer,
216 diagnostic: None,
217 format,
218 output_options,
219 strict_protocol: false,
220 terminal_emitted: false,
221 }
222 }
223
224 /// Create an event-stream emitter: every event, including `error`, goes to
225 /// the single `writer`, preserving interleaved ordering. Pick this when the
226 /// consumer reads one ordered stream and branches on `kind`.
227 pub fn stream(writer: W, format: OutputFormat) -> Self {
228 Self::with_options(writer, format, OutputOptions::default())
229 }
230
231 /// Create a finite one-shot emitter with explicit sinks: `result` goes to
232 /// `result_writer`, while `error`/`progress`/`log` go to `diagnostic`.
233 pub fn finite_with(
234 result_writer: W,
235 diagnostic: impl std::io::Write + 'static,
236 format: OutputFormat,
237 ) -> Self {
238 Self::finite_with_options(result_writer, diagnostic, format, OutputOptions::default())
239 }
240
241 /// Create a finite one-shot emitter with explicit sinks and output options.
242 pub fn finite_with_options(
243 result_writer: W,
244 diagnostic: impl std::io::Write + 'static,
245 format: OutputFormat,
246 output_options: OutputOptions,
247 ) -> Self {
248 Self {
249 writer: result_writer,
250 diagnostic: Some(Box::new(diagnostic)),
251 format,
252 output_options,
253 strict_protocol: false,
254 terminal_emitted: false,
255 }
256 }
257
258 /// Require the AFDATA recommended strict profile for every emitted event.
259 pub fn with_strict_protocol(mut self) -> Self {
260 self.strict_protocol = true;
261 self
262 }
263
264 /// Emit a typed Event (unified entry for all event kinds).
265 ///
266 /// Accepts only SDK-constructed Event; for dynamic JSON, use emit_validated_value.
267 pub fn emit(&mut self, event: Event) -> Result<(), CliEmitterError> {
268 let value = event.into_value();
269 self.write_event(value)
270 }
271
272 /// Emit and validate dynamic JSON, then apply redaction/formatting/write.
273 ///
274 /// Runs strict validation first, ensuring the dynamic JSON is safe.
275 pub fn emit_validated_value(&mut self, value: Value) -> Result<(), CliEmitterError> {
276 validate_protocol_event(&value, true).map_err(CliEmitterError::Validation)?;
277 self.write_event(value)
278 }
279
280 /// Convenience: build and emit a result event.
281 pub fn emit_result(&mut self, payload: Value) -> Result<(), CliEmitterError> {
282 self.emit(json_result(payload).build())
283 }
284
285 /// Convenience: build and emit an error event.
286 pub fn emit_error(&mut self, code: &str, message: &str) -> Result<(), CliEmitterError> {
287 self.emit(json_error(code, message).build()?)
288 }
289
290 /// Convenience: build and emit a progress event.
291 pub fn emit_progress(&mut self, message: &str) -> Result<(), CliEmitterError> {
292 self.emit(json_progress(serde_json::json!({ "message": message })).build())
293 }
294
295 /// Convenience: build and emit a log event.
296 pub fn emit_log(&mut self, level: LogLevel, message: &str) -> Result<(), CliEmitterError> {
297 self.emit(
298 json_log(serde_json::json!({
299 "level": level.as_str(),
300 "message": message,
301 }))
302 .build(),
303 )
304 }
305
306 /// Emit `event` as the terminal event and resolve the outcome to a process
307 /// exit code, so a one-shot CLI need not hand-roll the emit-then-exit dance.
308 ///
309 /// A successful write returns `success_code`; a broken pipe (the reader hung
310 /// up) returns `0` and says nothing. Any other write failure returns `4`
311 /// after reporting `output_write_failed` as an `error` event: on the
312 /// diagnostic sink in finite mode, on the process's stderr in event-stream
313 /// mode, whose only writer is the one that just failed. That report is best
314 /// effort — a stderr that cannot be written either leaves only the code. An
315 /// event the emitter refuses (a protocol violation or a second terminal
316 /// event) is the command's own failure, not an
317 /// output one, and returns `1`.
318 /// A library never calls `process::exit` itself — return this code from
319 /// `main` (`std::process::ExitCode::from(code)`).
320 pub fn finish(&mut self, event: Event, success_code: u8) -> u8 {
321 match self.emit(event) {
322 Ok(()) => success_code,
323 Err(err) if err.io_error_kind() == Some(std::io::ErrorKind::BrokenPipe) => 0,
324 Err(CliEmitterError::Write(_)) => {
325 self.report_output_write_failed();
326 4
327 }
328 Err(
329 CliEmitterError::Validation(_)
330 | CliEmitterError::Build(_)
331 | CliEmitterError::Lifecycle(_),
332 ) => 1,
333 }
334 }
335
336 // Event-stream mode has no diagnostic sink, so the report goes to the
337 // process's stderr: the spec's Channel policy sanctions it here exactly as
338 // it does for the wired constructors below.
339 #[allow(clippy::disallowed_methods)]
340 fn report_output_write_failed(&mut self) {
341 let event = crate::cli_afdata::cli_output_write_failed_event();
342 if self.diagnostic.is_some() {
343 let _unwritable = self.emit(event);
344 } else {
345 let mut stderr =
346 CliEmitter::with_options(std::io::stderr(), self.format, OutputOptions::default())
347 .with_strict_protocol();
348 let _unwritable = stderr.emit(event);
349 }
350 }
351
352 /// Convenience over [`CliEmitter::finish`]: emit a `result` payload and
353 /// return `0` on success.
354 ///
355 /// For an error, build it with [`json_error`] (`.hint(…)`, `.retryable(…)`,
356 /// `.field(…)` as needed) and pass the event to [`CliEmitter::finish`] with
357 /// the desired exit code — the builder is the error type, so no separate
358 /// error-emitting convenience is needed.
359 pub fn finish_result(&mut self, payload: Value) -> u8 {
360 self.finish(json_result(payload).build(), 0)
361 }
362
363 /// Access the underlying writer.
364 pub fn into_inner(self) -> W {
365 self.writer
366 }
367
368 fn write_event(&mut self, event: Value) -> Result<(), CliEmitterError> {
369 validate_protocol_event(&event, self.strict_protocol)
370 .map_err(CliEmitterError::Validation)?;
371 let kind = event.get("kind").and_then(Value::as_str).ok_or_else(|| {
372 CliEmitterError::Validation(ProtocolViolation {
373 rule: "kind_invalid",
374 pointer: "/kind".to_string(),
375 message: "event.kind is required".to_string(),
376 })
377 })?;
378 match kind {
379 "log" | "progress" => {
380 if self.terminal_emitted {
381 return Err(CliEmitterError::Lifecycle(
382 "cannot emit non-terminal event after terminal event".to_string(),
383 ));
384 }
385 }
386 "result" | "error" => {
387 if self.terminal_emitted {
388 return Err(CliEmitterError::Lifecycle(
389 "cannot emit duplicate terminal event".to_string(),
390 ));
391 }
392 }
393 _ => {
394 return Err(CliEmitterError::Validation(ProtocolViolation {
395 rule: "kind_unsupported",
396 pointer: "/kind".to_string(),
397 message: format!("unsupported event kind {kind:?}"),
398 }));
399 }
400 }
401 let rendered = crate::formatting::render(&event, self.format, &self.output_options);
402 // Finite mode (a diagnostic sink is present) splits by kind: `result`
403 // stays on the primary writer (stdout), while `error`/`progress`/`log`
404 // are diagnostics routed to the diagnostic writer (stderr). Event-stream
405 // mode (no diagnostic sink) keeps every event on the single writer.
406 match &mut self.diagnostic {
407 Some(diagnostic) if kind != "result" => {
408 write_event_line(diagnostic.as_mut(), &rendered)
409 }
410 _ => write_event_line(&mut self.writer, &rendered),
411 }?;
412 if matches!(kind, "result" | "error") {
413 self.terminal_emitted = true;
414 }
415 Ok(())
416 }
417}
418
419/// Write one rendered event line (payload plus trailing newline) and flush.
420fn write_event_line(writer: &mut dyn std::io::Write, rendered: &str) -> std::io::Result<()> {
421 writer.write_all(rendered.as_bytes())?;
422 writer.write_all(b"\n")?;
423 writer.flush()
424}
425
426// The emitter's own diagnostic sink is the spec's sanctioned exception to the
427// "no ad-hoc stderr" rule (Channel policy): a finite one-shot emitter routes
428// `error`/`progress`/`log` to `std::io::stderr` on purpose, so these wired
429// constructors are allowed to name it directly.
430#[allow(clippy::disallowed_methods)]
431impl CliEmitter<std::io::Stdout> {
432 /// Create a finite one-shot emitter wired to the process streams: `result`
433 /// → `stdout`, `error`/`progress`/`log` → `stderr`. The recommended default
434 /// for a one-shot CLI.
435 pub fn finite(format: OutputFormat) -> Self {
436 Self::finite_with(std::io::stdout(), std::io::stderr(), format)
437 }
438
439 /// Create a finite one-shot emitter wired to the process streams, with
440 /// custom output options.
441 pub fn finite_options(format: OutputFormat, output_options: OutputOptions) -> Self {
442 Self::finite_with_options(std::io::stdout(), std::io::stderr(), format, output_options)
443 }
444}
445
446// Same sanctioned exception as above: `from_output_to` wires the process
447// streams (`std::io::stderr` included) as the emitter's own sinks.
448#[allow(clippy::disallowed_methods)]
449impl CliEmitter<Box<dyn std::io::Write>> {
450 /// Use the registry's typed format and routing. Raw plans use JSON diagnostics.
451 pub fn from_plan(plan: &crate::cli_spec::OutputPlan) -> Self {
452 Self::from_output_to(
453 plan.output_to().unwrap_or(OutputTo::Split),
454 plan.output_format().unwrap_or(OutputFormat::Json),
455 )
456 .with_strict_protocol()
457 }
458
459 /// Build an emitter from a parsed [`OutputTo`] selector, wired to the
460 /// process streams: `Split` is finite mode (`result` → stdout, everything
461 /// else → stderr); `Stdout`/`Stderr` are event-stream mode onto that stream.
462 pub fn from_output_to(selector: OutputTo, format: OutputFormat) -> Self {
463 Self::from_output_to_with(selector, format, OutputOptions::default())
464 }
465
466 /// As [`CliEmitter::from_output_to`], with custom output options.
467 pub fn from_output_to_with(
468 selector: OutputTo,
469 format: OutputFormat,
470 output_options: OutputOptions,
471 ) -> Self {
472 match selector {
473 OutputTo::Split => Self::finite_with_options(
474 Box::new(std::io::stdout()),
475 std::io::stderr(),
476 format,
477 output_options,
478 ),
479 OutputTo::Stdout => {
480 Self::with_options(Box::new(std::io::stdout()), format, output_options)
481 }
482 OutputTo::Stderr => {
483 Self::with_options(Box::new(std::io::stderr()), format, output_options)
484 }
485 }
486 }
487}
488
489/// Write a raw document to the process stream `selector` names.
490///
491/// Not everything a CLI emits is an event. `--docs` renders a Markdown
492/// reference, `--output plain` help renders a human catalog, and `shell bash`
493/// renders a sourceable script: the consumer is `>` or `source`, so wrapping
494/// them in a protocol envelope would mean `tool --docs > cli.md` wrote JSON.
495/// Every CLI compiled from a registry inherits those outcomes, so something
496/// must write bytes to a process stream — and this is that one place. Without
497/// it each CLI hand-rolls the same dispatch, and they drift: this crate's own
498/// binary treats a broken pipe as success while other tools returned a failure
499/// exit code for it.
500///
501/// A broken pipe is success. `tool --docs | head` closes the reader early, and
502/// reading the first page of a document is not a failure to report.
503///
504/// `Split` writes to stdout: a document is the result, not a diagnostic.
505// The spec's Channel policy sanctions this sink, exactly as it does the
506// emitter's constructors above; `clippy.toml`'s `disallowed-methods` keeps
507// stray writes elsewhere from bypassing this routing.
508#[allow(clippy::disallowed_methods)]
509pub fn write_raw(text: &str, selector: OutputTo) -> std::io::Result<()> {
510 let written = match selector {
511 OutputTo::Stderr => write_raw_to(&mut std::io::stderr().lock(), text),
512 OutputTo::Split | OutputTo::Stdout => write_raw_to(&mut std::io::stdout().lock(), text),
513 };
514 forgive_broken_pipe(written)
515}
516
517// Flush even a final fragment without a newline, so a buffered write failure
518// belongs to this call rather than an ignored process-shutdown flush.
519fn write_raw_to(writer: &mut impl std::io::Write, text: &str) -> std::io::Result<()> {
520 writer.write_all(text.as_bytes())?;
521 writer.flush()
522}
523
524/// Write registry-owned raw output with the standard process exit policy.
525pub fn write_raw_exit(text: &str, plan: &crate::cli_spec::OutputPlan) -> std::process::ExitCode {
526 match write_raw(text, plan.output_to().unwrap_or(OutputTo::Stdout)) {
527 Ok(()) => std::process::ExitCode::SUCCESS,
528 Err(_) => {
529 let mut emitter = CliEmitter::from_output_to(OutputTo::Stderr, OutputFormat::Json)
530 .with_strict_protocol();
531 let _unwritable = emitter.emit(crate::cli_afdata::cli_output_write_failed_event());
532 std::process::ExitCode::from(4)
533 }
534 }
535}
536
537/// A closed reader is not a failure to report.
538///
539/// Split out so the policy is reachable from a test: piping a document into
540/// `head` is a normal way to read one, and the exit code must not say the tool
541/// failed. Every hand-rolled copy of this write got to decide that separately,
542/// and they did not agree.
543fn forgive_broken_pipe(result: std::io::Result<()>) -> std::io::Result<()> {
544 match result {
545 Err(error) if error.kind() == std::io::ErrorKind::BrokenPipe => Ok(()),
546 other => other,
547 }
548}
549
550/// Build a standard CLI version event: a `kind:"result"` event whose payload is
551/// `{ "code": "version", "name": <name>, "version": <version> }`, plus
552/// `"display_name"`/`"build"` when given. `name` is the short/bin identity
553/// (e.g. `"afdata"`); `display_name` is an optional human-facing product name
554/// (e.g. `"Agent-First Data"`); `build` is an opaque caller-supplied identifier (a git
555/// commit SHA, for example) — its meaning is entirely up to the caller. Both
556/// are `None` when unavailable, and simply absent from the payload.
557pub fn build_cli_version(
558 name: &str,
559 display_name: Option<&str>,
560 version: &str,
561 build: Option<&str>,
562) -> Event {
563 let mut payload = serde_json::json!({
564 "code": "version",
565 "name": name,
566 "version": version,
567 });
568 if let Some(display_name) = display_name {
569 payload["display_name"] = Value::String(display_name.to_string());
570 }
571 if let Some(build) = build {
572 payload["build"] = Value::String(build.to_string());
573 }
574 json_result(payload).build()
575}
576
577/// Render a CLI version response as a protocol-v1 event in `format`.
578pub fn cli_render_version(
579 name: &str,
580 display_name: Option<&str>,
581 version: &str,
582 build: Option<&str>,
583 format: OutputFormat,
584) -> String {
585 let mut rendered = crate::formatting::render(
586 build_cli_version(name, display_name, version, build).as_value(),
587 format,
588 &OutputOptions::default(),
589 );
590 while rendered.ends_with('\n') {
591 rendered.pop();
592 }
593 rendered.push('\n');
594 rendered
595}
596
597#[cfg(test)]
598mod raw_write_tests {
599 use super::forgive_broken_pipe;
600 use std::io::{Error, ErrorKind};
601
602 #[test]
603 fn raw_fragments_report_a_flush_failure() {
604 struct FailsOnFlush;
605 impl std::io::Write for FailsOnFlush {
606 fn write(&mut self, bytes: &[u8]) -> std::io::Result<usize> {
607 Ok(bytes.len())
608 }
609 fn flush(&mut self) -> std::io::Result<()> {
610 Err(std::io::ErrorKind::PermissionDenied.into())
611 }
612 }
613 assert_eq!(
614 super::write_raw_to(&mut FailsOnFlush, "fragment").map_err(|error| error.kind()),
615 Err(std::io::ErrorKind::PermissionDenied)
616 );
617 }
618
619 #[test]
620 fn a_closed_reader_is_success_and_every_other_failure_is_not() {
621 assert!(forgive_broken_pipe(Ok(())).is_ok());
622 assert!(forgive_broken_pipe(Err(Error::from(ErrorKind::BrokenPipe))).is_ok());
623 let denied = forgive_broken_pipe(Err(Error::from(ErrorKind::PermissionDenied)));
624 assert_eq!(
625 denied.map_err(|error| error.kind()),
626 Err(ErrorKind::PermissionDenied)
627 );
628 }
629}