Skip to main content

clawless_cli/presenter/
terminal.rs

1//! Terminal output adapter
2//!
3//! This module defines [`TerminalPresenter`], the concrete [`Presenter`] adapter for terminal
4//! output. `TerminalPresenter` is a stateless presenter: it renders each event as it arrives,
5//! writing to stdout or stderr based on its [`OutputMode`] and filtering by [`Verbosity`].
6//!
7//! `TerminalPresenter` is constructed once via its builder, consumed by a single call to
8//! [`present`], and dropped when the command completes. The builder requires an [`EventReceiver`]
9//! and defaults [`Verbosity`] to [`Verbosity::Default`] and [`OutputMode`] to
10//! [`OutputMode::Text`].
11//!
12//! [`EventReceiver`]: clawless_core::event::EventReceiver
13//! [`OutputMode`]: crate::output::OutputMode
14//! [`Presenter`]: super::Presenter
15//! [`Verbosity`]: crate::output::Verbosity
16//! [`present`]: super::Presenter::present
17
18use std::future::Future;
19use std::io::Write;
20use std::pin::Pin;
21
22use async_trait::async_trait;
23use bon::Builder;
24use clawless_core::context::Interactivity;
25use clawless_core::event::process::ProcessEvent;
26use clawless_core::event::prompt::PromptRequest;
27use clawless_core::event::{Event, EventReceiver};
28use clawless_core::process::Stream;
29
30use super::Presenter;
31use super::line_question;
32use super::line_reader::LineReader;
33use crate::error::CommandResult;
34use crate::output::OutputMode;
35use crate::output::Verbosity;
36
37/// Terminal presenter adapter
38///
39/// Renders command output to the terminal. In text mode, all output goes to stdout. In JSON
40/// mode, messages go to stderr and artifacts are serialized as JSON to stdout. This follows the
41/// convention used by `gh`, `kubectl`, and `jq`.
42///
43/// The presenter also asks the user the questions of the command. It writes a question to
44/// stderr in both modes, and the user answers with one line on stdin. Verbosity and output mode
45/// do not suppress a question. A presenter that is not built as interactive drops every
46/// question.
47///
48/// `TerminalPresenter` is constructed once via its [builder], consumed by a single call to
49/// [`present`], and dropped when the command completes. The presenter holds the [`EventReceiver`]
50/// alive during command execution so that [`EventSender`]s do not receive errors when sending.
51/// After the command completes, the presenter and its receiver are dropped.
52///
53/// # Examples
54///
55/// ```
56/// use clawless_core::event::event_channel;
57/// use clawless_cli::presenter::TerminalPresenter;
58///
59/// let (_sender, receiver) = event_channel();
60/// let presenter = TerminalPresenter::builder().receiver(receiver).build();
61/// ```
62///
63/// [`EventReceiver`]: clawless_core::event::EventReceiver
64/// [`EventSender`]: clawless_core::event::EventSender
65/// [`present`]: super::Presenter::present
66/// [builder]: TerminalPresenter::builder
67#[derive(Debug, Builder)]
68pub struct TerminalPresenter {
69    /// How much detail to render. The presenter drops events below this level
70    #[builder(default)]
71    verbosity: Verbosity,
72
73    /// Whether to render events as text or as JSON
74    #[builder(default)]
75    mode: OutputMode,
76
77    /// Whether a user is present who can answer a prompt
78    #[builder(default)]
79    interactivity: Interactivity,
80
81    /// Stream of events that the command produces
82    receiver: EventReceiver,
83}
84
85/// Reports whether an event of a run belongs on the error stream
86///
87/// A program separates its result from its diagnostics, and the presenter keeps that separation:
88/// what a program wrote to its standard error is written to the standard error of the
89/// application. A reader that redirects one of the two streams therefore sees the same split that
90/// running the program by hand would give.
91///
92/// The start and the end of a run are not output of the program. They belong with the result,
93/// which is where a transcript reads in the order that a person expects.
94// r[impl process.render.streams]
95fn is_diagnostic(event: &ProcessEvent) -> bool {
96    match event {
97        ProcessEvent::Started { .. } => false,
98        ProcessEvent::Finished { .. } => false,
99        ProcessEvent::Line { line, .. } => match line.stream() {
100            Stream::StandardError => true,
101            Stream::StandardOutput => false,
102        },
103    }
104}
105
106/// Renders one event for the given verbosity and output mode
107///
108/// `stdout` and `stderr` are the standard output and the standard error of the application. A
109/// test passes buffers.
110///
111/// In text mode, messages and details go to `stdout`. They therefore interleave with the
112/// artifacts, in the order that the command produced them.
113///
114/// In JSON mode, messages and details go to `stderr` instead. `stdout` then carries only JSON
115/// artifacts, which a caller can pipe into another tool.
116///
117/// The output of an external program is supplementary, so it follows the same rule as a detail
118/// and appears only when the user asks for verbose output. A command that wants a program to be
119/// visible at the default verbosity says so itself with a message.
120///
121/// The presenter handles a prompt before it renders an event. This function drops a prompt that
122/// reaches it.
123///
124/// # Panics
125///
126/// Panics if the process cannot write to the output stream. A reader that closes the pipe
127/// causes this panic.
128// Writing to the process output stream fails only when the stream itself is gone, such as a
129// pipe the reader has closed. A presenter whose output stream has vanished has nowhere left
130// to report the failure, so it fails loudly rather than dropping output silently.
131// r[impl process.render.verbosity]
132#[allow(clippy::expect_used)]
133fn render_event(
134    event: Event,
135    verbosity: Verbosity,
136    mode: OutputMode,
137    stdout: &mut impl Write,
138    stderr: &mut impl Write,
139) {
140    match event {
141        Event::Message(msg) => match verbosity {
142            Verbosity::Quiet => {}
143            Verbosity::Default | Verbosity::Verbose => match mode {
144                OutputMode::Text => writeln!(stdout, "{msg}").expect("should write message"),
145                OutputMode::Json => writeln!(stderr, "{msg}").expect("should write message"),
146            },
147        },
148        Event::Detail(msg) => match verbosity {
149            Verbosity::Quiet | Verbosity::Default => {}
150            Verbosity::Verbose => match mode {
151                OutputMode::Text => writeln!(stdout, "{msg}").expect("should write detail"),
152                OutputMode::Json => writeln!(stderr, "{msg}").expect("should write detail"),
153            },
154        },
155        Event::Artifact(artifact) => {
156            let line = match mode {
157                OutputMode::Text => artifact.to_string(),
158                OutputMode::Json => {
159                    serde_json::to_string(&artifact).expect("should serialize artifact to JSON")
160                }
161            };
162            writeln!(stdout, "{line}").expect("should write artifact");
163        }
164        Event::Process(event) => match verbosity {
165            Verbosity::Quiet | Verbosity::Default => {}
166            Verbosity::Verbose => {
167                let to_stderr = match mode {
168                    OutputMode::Json => true,
169                    OutputMode::Text => is_diagnostic(&event),
170                };
171
172                if to_stderr {
173                    writeln!(stderr, "{event}").expect("should write process event");
174                } else {
175                    writeln!(stdout, "{event}").expect("should write process event");
176                }
177            }
178        },
179        Event::Prompt(request) => drop(request),
180    }
181}
182
183/// Asks the user the question of a request, on the display and from the input
184///
185/// The display is the standard error in every output mode, so the standard output carries only
186/// the result of the command.
187///
188/// The presenter drops a selection without options, because no line is an answer to it.
189async fn ask_user(request: PromptRequest, input: &mut LineReader, display: &mut impl Write) {
190    match request {
191        PromptRequest::Confirm { question, reply } => {
192            line_question::ask(&question, reply, input, display).await;
193        }
194        PromptRequest::Select { question, reply } => {
195            if question.options().is_empty() {
196                drop(reply);
197            } else {
198                line_question::ask(&question, reply, input, display).await;
199            }
200        }
201        PromptRequest::Text { question, reply } => {
202            line_question::ask(&question, reply, input, display).await;
203        }
204    }
205}
206
207impl TerminalPresenter {
208    /// Presents the output of a command on the given streams
209    ///
210    /// [`Presenter::present`] passes the standard output and the standard error of the
211    /// application, and the standard input if a user is present. A test passes buffers and
212    /// scripted input.
213    ///
214    /// Each write locks its stream for one line only, so the presenter does not block other
215    /// writers for the whole presentation.
216    ///
217    /// The presenter handles one event at a time. A question therefore appears after the
218    /// output that the command sent before it, and later output appears after the answer.
219    /// Verbosity does not apply to a question. Without an input, the presenter drops every
220    /// prompt.
221    ///
222    /// # Errors
223    ///
224    /// Returns the error of the command if the command fails.
225    ///
226    /// # Panics
227    ///
228    /// Panics if the command panicked, and if a stream cannot be written.
229    async fn present_on(
230        self,
231        command: Pin<Box<dyn Future<Output = CommandResult> + Send>>,
232        stdout: &mut impl Write,
233        stderr: &mut impl Write,
234        mut input: Option<LineReader>,
235    ) -> CommandResult {
236        let Self {
237            verbosity,
238            mode,
239            interactivity: _,
240            mut receiver,
241        } = self;
242
243        let command_handle = tokio::spawn(command);
244
245        while let Some(event) = receiver.recv().await {
246            match event {
247                Event::Prompt(request) => match &mut input {
248                    Some(input) => ask_user(*request, input, stderr).await,
249                    None => drop(request),
250                },
251                Event::Message(_) | Event::Detail(_) | Event::Artifact(_) | Event::Process(_) => {
252                    render_event(event, verbosity, mode, stdout, stderr);
253                }
254            }
255        }
256
257        // The join fails only if the command task panicked or was aborted. Resuming the
258        // panic on this thread preserves the original panic message for the user.
259        #[allow(clippy::expect_used)]
260        command_handle.await.expect("command task panicked")
261    }
262}
263
264#[async_trait(?Send)]
265impl Presenter for TerminalPresenter {
266    async fn present(
267        self,
268        command: Pin<Box<dyn Future<Output = CommandResult> + Send>>,
269    ) -> CommandResult {
270        let input = match self.interactivity {
271            Interactivity::Interactive => Some(LineReader::stdin()),
272            Interactivity::NonInteractive => None,
273        };
274
275        self.present_on(
276            command,
277            &mut std::io::stdout(),
278            &mut std::io::stderr(),
279            input,
280        )
281        .await
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    // An assertion in a test panics by design. A `# Panics` section on every test
288    // would repeat that and give the reader no information.
289    #![allow(clippy::missing_panics_doc)]
290
291    use std::collections::VecDeque;
292    use std::sync::{Arc, Mutex};
293    use std::time::Duration;
294
295    use clawless_core::context::Interactivity;
296    use clawless_core::event::process::{Outcome, RunId};
297    use clawless_core::event::prompt::PromptRequest;
298    use clawless_core::event::{EventSender, event_channel};
299    use clawless_core::output::Output;
300    use clawless_core::process::Invocation;
301    use clawless_core::process::Line;
302    use clawless_core::prompt::{Confirm, Confirmation, Prompt, Select};
303
304    use super::*;
305
306    /// Collects what a presenter writes to both of its streams, in the order of the writes
307    #[derive(Clone, Debug, Default)]
308    struct Transcript {
309        /// The bytes of every write so far
310        bytes: Arc<Mutex<Vec<u8>>>,
311    }
312
313    impl Transcript {
314        /// Returns everything that was written so far
315        fn text(&self) -> String {
316            String::from_utf8_lossy(&self.bytes.lock().expect("should lock")).into_owned()
317        }
318    }
319
320    impl Write for Transcript {
321        fn write(&mut self, buffer: &[u8]) -> std::io::Result<usize> {
322            self.bytes
323                .lock()
324                .expect("should lock")
325                .extend_from_slice(buffer);
326
327            Ok(buffer.len())
328        }
329
330        fn flush(&mut self) -> std::io::Result<()> {
331            Ok(())
332        }
333    }
334
335    /// Returns a command that says what it is about to do, asks, and reports the answer
336    fn confirming(sender: EventSender) -> Pin<Box<dyn Future<Output = CommandResult> + Send>> {
337        Box::pin(async move {
338            let output = Output::new(sender);
339            let prompt = Prompt::builder()
340                .output(output.clone())
341                .interactivity(Interactivity::Interactive)
342                .build();
343
344            output.message("About to release 1.4.0.").await?;
345            let answer = prompt
346                .confirm(Confirm::new("Release?").with_default(Confirmation::No))
347                .await?;
348            output.message(format!("The user said {answer:?}.")).await?;
349
350            Ok(())
351        })
352    }
353
354    /// Returns an input over the lines that a user types
355    fn typed(lines: &[&str]) -> LineReader {
356        let mut lines = lines
357            .iter()
358            .map(|line| (*line).to_owned())
359            .collect::<VecDeque<_>>();
360
361        LineReader::new(move |buffer| {
362            let line = lines.pop_front().unwrap_or_default();
363            buffer.push_str(&line);
364            Ok(line.len())
365        })
366    }
367
368    #[test]
369    fn builder_with_defaults_uses_default_verbosity_and_mode() {
370        let (_sender, receiver) = event_channel();
371
372        let presenter = TerminalPresenter::builder().receiver(receiver).build();
373
374        assert_eq!(presenter.verbosity, Verbosity::Default);
375        assert_eq!(presenter.mode, OutputMode::Text);
376    }
377
378    #[test]
379    fn builder_with_mode_uses_provided_mode() {
380        let (_sender, receiver) = event_channel();
381
382        let presenter = TerminalPresenter::builder()
383            .receiver(receiver)
384            .mode(OutputMode::Json)
385            .build();
386
387        assert_eq!(presenter.mode, OutputMode::Json);
388    }
389
390    #[test]
391    fn builder_with_verbosity_uses_provided_verbosity() {
392        let (_sender, receiver) = event_channel();
393
394        let presenter = TerminalPresenter::builder()
395            .receiver(receiver)
396            .verbosity(Verbosity::Verbose)
397            .build();
398
399        assert_eq!(presenter.verbosity, Verbosity::Verbose);
400    }
401
402    #[test]
403    fn is_diagnostic_with_a_finished_run_returns_false() {
404        let event = ProcessEvent::Finished {
405            id: RunId::next(),
406            invocation: Invocation::new("git"),
407            outcome: Outcome::Incomplete,
408            duration: Duration::ZERO,
409        };
410
411        let diagnostic = is_diagnostic(&event);
412
413        assert!(!diagnostic);
414    }
415
416    // r[verify process.render.streams]
417    #[test]
418    fn is_diagnostic_with_a_standard_error_line_returns_true() {
419        let event = ProcessEvent::Line {
420            id: RunId::next(),
421            line: Line::new(Stream::StandardError, "no such file"),
422        };
423
424        let diagnostic = is_diagnostic(&event);
425
426        assert!(diagnostic);
427    }
428
429    #[test]
430    fn is_diagnostic_with_a_standard_output_line_returns_false() {
431        let event = ProcessEvent::Line {
432            id: RunId::next(),
433            line: Line::new(Stream::StandardOutput, "hello"),
434        };
435
436        let diagnostic = is_diagnostic(&event);
437
438        assert!(!diagnostic);
439    }
440
441    #[test]
442    fn is_diagnostic_with_a_started_run_returns_false() {
443        let event = ProcessEvent::Started {
444            id: RunId::next(),
445            invocation: Invocation::new("git"),
446            process_id: None,
447        };
448
449        let diagnostic = is_diagnostic(&event);
450
451        assert!(!diagnostic);
452    }
453
454    #[tokio::test]
455    async fn present_consumes_events_from_channel() {
456        let (sender, receiver) = event_channel();
457        let presenter = TerminalPresenter::builder().receiver(receiver).build();
458
459        presenter
460            .present(Box::pin(async move {
461                sender
462                    .send(Event::Message("consumed".to_string()))
463                    .await
464                    .expect("should send");
465                Ok(())
466            }))
467            .await
468            .expect("should succeed");
469    }
470
471    #[tokio::test]
472    async fn present_on_with_a_prompt_asks_after_the_earlier_output() {
473        let (sender, receiver) = event_channel();
474        let presenter = TerminalPresenter::builder().receiver(receiver).build();
475        let transcript = Transcript::default();
476
477        presenter
478            .present_on(
479                confirming(sender),
480                &mut transcript.clone(),
481                &mut transcript.clone(),
482                Some(typed(&["y\n"])),
483            )
484            .await
485            .expect("should succeed");
486
487        assert_eq!(
488            transcript.text(),
489            "About to release 1.4.0.\nRelease? [y/N] The user said Yes.\n"
490        );
491    }
492
493    #[tokio::test]
494    async fn present_on_with_a_prompt_at_quiet_verbosity_still_asks() {
495        let (sender, receiver) = event_channel();
496        let presenter = TerminalPresenter::builder()
497            .receiver(receiver)
498            .verbosity(Verbosity::Quiet)
499            .build();
500        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
501
502        presenter
503            .present_on(
504                confirming(sender),
505                &mut stdout,
506                &mut stderr,
507                Some(typed(&["y\n"])),
508            )
509            .await
510            .expect("should succeed");
511
512        assert_eq!(String::from_utf8_lossy(&stderr), "Release? [y/N] ");
513    }
514
515    #[tokio::test]
516    async fn present_on_with_a_prompt_in_json_mode_keeps_the_question_off_stdout() {
517        let (sender, receiver) = event_channel();
518        let presenter = TerminalPresenter::builder()
519            .receiver(receiver)
520            .mode(OutputMode::Json)
521            .build();
522        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
523
524        presenter
525            .present_on(
526                confirming(sender),
527                &mut stdout,
528                &mut stderr,
529                Some(typed(&["y\n"])),
530            )
531            .await
532            .expect("should succeed");
533
534        assert_eq!(String::from_utf8_lossy(&stdout), "");
535    }
536
537    #[tokio::test]
538    async fn present_on_with_a_prompt_in_text_mode_asks_on_stderr() {
539        let (sender, receiver) = event_channel();
540        let presenter = TerminalPresenter::builder().receiver(receiver).build();
541        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
542
543        presenter
544            .present_on(
545                confirming(sender),
546                &mut stdout,
547                &mut stderr,
548                Some(typed(&["y\n"])),
549            )
550            .await
551            .expect("should succeed");
552
553        assert_eq!(String::from_utf8_lossy(&stderr), "Release? [y/N] ");
554    }
555
556    #[tokio::test]
557    async fn present_on_with_a_selection_sends_the_position_to_the_command() {
558        let (sender, receiver) = event_channel();
559        let presenter = TerminalPresenter::builder().receiver(receiver).build();
560        let transcript = Transcript::default();
561
562        presenter
563            .present_on(
564                Box::pin(async move {
565                    let output = Output::new(sender);
566                    let prompt = Prompt::builder()
567                        .output(output.clone())
568                        .interactivity(Interactivity::Interactive)
569                        .build();
570
571                    let kind = prompt.select("Kind", ["Added", "Fixed"]).await?;
572                    output.message(format!("The kind is {kind}.")).await?;
573
574                    Ok(())
575                }),
576                &mut transcript.clone(),
577                &mut transcript.clone(),
578                Some(typed(&["2\n"])),
579            )
580            .await
581            .expect("should succeed");
582
583        assert_eq!(
584            transcript.text(),
585            "Kind:\n  1) Added\n  2) Fixed\nEnter a number [1-2]: The kind is Fixed.\n"
586        );
587    }
588
589    #[tokio::test]
590    async fn present_on_with_a_selection_without_options_drops_the_prompt() {
591        let (sender, receiver) = event_channel();
592        let presenter = TerminalPresenter::builder().receiver(receiver).build();
593        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
594
595        let error = presenter
596            .present_on(
597                Box::pin(async move {
598                    let options: [&str; 0] = [];
599                    let (request, pending) = PromptRequest::select(Select::new("Kind", options));
600                    Output::new(sender).prompt(request).await?;
601                    pending.wait().await?;
602
603                    Ok(())
604                }),
605                &mut stdout,
606                &mut stderr,
607                Some(typed(&["1\n"])),
608            )
609            .await
610            .expect_err("should fail");
611
612        assert_eq!(
613            error.to_string(),
614            "the prompt was dropped without an answer"
615        );
616    }
617
618    #[tokio::test]
619    async fn present_on_with_a_text_prompt_sends_the_line_to_the_command() {
620        let (sender, receiver) = event_channel();
621        let presenter = TerminalPresenter::builder().receiver(receiver).build();
622        let transcript = Transcript::default();
623
624        presenter
625            .present_on(
626                Box::pin(async move {
627                    let output = Output::new(sender);
628                    let prompt = Prompt::builder()
629                        .output(output.clone())
630                        .interactivity(Interactivity::Interactive)
631                        .build();
632
633                    let title = prompt.text("Title").await?;
634                    output.message(format!("The title is {title}.")).await?;
635
636                    Ok(())
637                }),
638                &mut transcript.clone(),
639                &mut transcript.clone(),
640                Some(typed(&["Fix the race\n"])),
641            )
642            .await
643            .expect("should succeed");
644
645        assert_eq!(transcript.text(), "Title: The title is Fix the race.\n");
646    }
647
648    #[tokio::test]
649    async fn present_on_without_an_input_drops_the_prompt() {
650        let (sender, receiver) = event_channel();
651        let presenter = TerminalPresenter::builder().receiver(receiver).build();
652        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
653
654        let error = presenter
655            .present_on(confirming(sender), &mut stdout, &mut stderr, None)
656            .await
657            .expect_err("should fail");
658
659        assert_eq!(
660            error.root_cause().to_string(),
661            "the prompt was dropped without an answer"
662        );
663    }
664
665    #[tokio::test]
666    async fn present_on_writes_the_events_of_the_command_to_the_streams() {
667        let (sender, receiver) = event_channel();
668        let presenter = TerminalPresenter::builder().receiver(receiver).build();
669        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
670
671        presenter
672            .present_on(
673                Box::pin(async move {
674                    sender
675                        .send(Event::Message("hello".to_owned()))
676                        .await
677                        .expect("should send");
678                    Ok(())
679                }),
680                &mut stdout,
681                &mut stderr,
682                None,
683            )
684            .await
685            .expect("should succeed");
686
687        assert_eq!((stdout, stderr), (b"hello\n".to_vec(), Vec::new()));
688    }
689
690    #[tokio::test]
691    async fn present_with_a_prompt_drops_the_request() {
692        let (sender, receiver) = event_channel();
693        let presenter = TerminalPresenter::builder().receiver(receiver).build();
694
695        let error = presenter
696            .present(Box::pin(async move {
697                let (request, pending) = PromptRequest::confirm(Confirm::new("Release?"));
698                sender
699                    .send(Event::Prompt(Box::new(request)))
700                    .await
701                    .expect("should send");
702                pending.wait().await?;
703                Ok(())
704            }))
705            .await
706            .expect_err("should fail");
707
708        assert_eq!(
709            error.to_string(),
710            "the prompt was dropped without an answer"
711        );
712    }
713
714    #[tokio::test]
715    async fn present_with_error_propagates_error() {
716        let (sender, receiver) = event_channel();
717        let presenter = TerminalPresenter::builder().receiver(receiver).build();
718
719        let error = presenter
720            .present(Box::pin(async move {
721                drop(sender);
722                Err(anyhow::anyhow!("command failed"))
723            }))
724            .await
725            .expect_err("should fail");
726
727        assert_eq!(error.to_string(), "command failed");
728    }
729
730    #[tokio::test]
731    async fn present_with_ok_returns_ok() {
732        let (sender, receiver) = event_channel();
733        let presenter = TerminalPresenter::builder().receiver(receiver).build();
734
735        presenter
736            .present(Box::pin(async move {
737                drop(sender);
738                Ok(())
739            }))
740            .await
741            .expect("should succeed");
742    }
743
744    #[tokio::test]
745    async fn present_with_receiver_keeps_channel_open_during_execution() {
746        let (sender, receiver) = event_channel();
747        let presenter = TerminalPresenter::builder().receiver(receiver).build();
748
749        presenter
750            .present(Box::pin(async move {
751                sender
752                    .send(clawless_core::event::Event::Message("hello".to_string()))
753                    .await
754                    .expect("should send while presenter holds receiver");
755                Ok(())
756            }))
757            .await
758            .expect("should succeed");
759    }
760
761    #[test]
762    fn render_event_with_a_detail_at_default_verbosity_writes_nothing() {
763        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
764
765        render_event(
766            Event::Detail("reading".to_owned()),
767            Verbosity::Default,
768            OutputMode::Text,
769            &mut stdout,
770            &mut stderr,
771        );
772
773        assert_eq!((stdout, stderr), (Vec::new(), Vec::new()));
774    }
775
776    #[test]
777    fn render_event_with_a_detail_at_verbose_verbosity_writes_to_stdout() {
778        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
779
780        render_event(
781            Event::Detail("reading".to_owned()),
782            Verbosity::Verbose,
783            OutputMode::Text,
784            &mut stdout,
785            &mut stderr,
786        );
787
788        assert_eq!((stdout, stderr), (b"reading\n".to_vec(), Vec::new()));
789    }
790
791    #[test]
792    fn render_event_with_a_message_at_quiet_verbosity_writes_nothing() {
793        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
794
795        render_event(
796            Event::Message("hello".to_owned()),
797            Verbosity::Quiet,
798            OutputMode::Text,
799            &mut stdout,
800            &mut stderr,
801        );
802
803        assert_eq!((stdout, stderr), (Vec::new(), Vec::new()));
804    }
805
806    #[test]
807    fn render_event_with_a_message_in_json_mode_writes_to_stderr() {
808        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
809
810        render_event(
811            Event::Message("hello".to_owned()),
812            Verbosity::Default,
813            OutputMode::Json,
814            &mut stdout,
815            &mut stderr,
816        );
817
818        assert_eq!((stdout, stderr), (Vec::new(), b"hello\n".to_vec()));
819    }
820
821    #[test]
822    fn render_event_with_a_message_in_text_mode_writes_to_stdout() {
823        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
824
825        render_event(
826            Event::Message("hello".to_owned()),
827            Verbosity::Default,
828            OutputMode::Text,
829            &mut stdout,
830            &mut stderr,
831        );
832
833        assert_eq!((stdout, stderr), (b"hello\n".to_vec(), Vec::new()));
834    }
835
836    #[test]
837    fn render_event_with_a_standard_error_line_in_text_mode_writes_to_stderr() {
838        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
839
840        render_event(
841            Event::Process(Box::new(ProcessEvent::Line {
842                id: RunId::next(),
843                line: Line::new(Stream::StandardError, "no such file"),
844            })),
845            Verbosity::Verbose,
846            OutputMode::Text,
847            &mut stdout,
848            &mut stderr,
849        );
850
851        assert_eq!((stdout, stderr), (Vec::new(), b"no such file\n".to_vec()));
852    }
853
854    #[test]
855    fn render_event_with_an_artifact_in_json_mode_writes_json_to_stdout() {
856        let (mut stdout, mut stderr) = (Vec::new(), Vec::new());
857
858        render_event(
859            Event::Artifact(Box::new(42)),
860            Verbosity::Quiet,
861            OutputMode::Json,
862            &mut stdout,
863            &mut stderr,
864        );
865
866        assert_eq!((stdout, stderr), (b"42\n".to_vec(), Vec::new()));
867    }
868
869    #[test]
870    fn trait_send() {
871        fn assert_send<T: Send>() {}
872        assert_send::<TerminalPresenter>();
873    }
874
875    #[test]
876    fn trait_unpin() {
877        fn assert_unpin<T: Unpin>() {}
878        assert_unpin::<TerminalPresenter>();
879    }
880}