docker_wrapper/
command.rs

1//! Command trait architecture for extensible Docker command implementations.
2//!
3//! This module provides a base trait that all Docker commands implement,
4//! allowing for both structured high-level APIs and escape hatches for
5//! any unimplemented options via raw arguments.
6
7use crate::error::{Error, Result};
8use crate::platform::PlatformInfo;
9use async_trait::async_trait;
10use std::collections::HashMap;
11use std::ffi::OsStr;
12use std::path::PathBuf;
13use std::process::Stdio;
14use tokio::process::Command as TokioCommand;
15
16// Re-export all command modules
17pub mod attach;
18pub mod bake;
19pub mod build;
20pub mod builder;
21pub mod commit;
22pub mod compose_attach;
23pub mod compose_build;
24pub mod compose_create;
25pub mod compose_down;
26pub mod compose_exec;
27pub mod compose_kill;
28pub mod compose_logs;
29pub mod compose_ls;
30pub mod compose_pause;
31pub mod compose_ps;
32pub mod compose_restart;
33pub mod compose_rm;
34pub mod compose_run;
35pub mod compose_start;
36pub mod compose_stop;
37pub mod compose_unpause;
38pub mod compose_up;
39pub mod container_prune;
40pub mod context;
41pub mod cp;
42pub mod create;
43pub mod diff;
44pub mod events;
45pub mod exec;
46pub mod export;
47pub mod history;
48pub mod image_prune;
49pub mod images;
50pub mod import;
51pub mod info;
52pub mod init;
53pub mod inspect;
54pub mod kill;
55pub mod load;
56pub mod login;
57pub mod logout;
58pub mod logs;
59pub mod network;
60pub mod pause;
61pub mod port;
62pub mod ps;
63pub mod pull;
64pub mod push;
65pub mod rename;
66pub mod restart;
67pub mod rm;
68pub mod rmi;
69pub mod run;
70pub mod save;
71pub mod search;
72pub mod start;
73pub mod stats;
74pub mod stop;
75pub mod system;
76pub mod tag;
77pub mod top;
78pub mod unpause;
79pub mod update;
80pub mod version;
81pub mod volume;
82pub mod wait;
83
84/// Unified trait for all Docker commands (both regular and compose)
85#[async_trait]
86pub trait DockerCommand {
87    /// The output type this command produces
88    type Output;
89
90    /// Get the command executor for extensibility
91    fn get_executor(&self) -> &CommandExecutor;
92
93    /// Get mutable command executor for extensibility
94    fn get_executor_mut(&mut self) -> &mut CommandExecutor;
95
96    /// Build the complete command arguments including subcommands
97    fn build_command_args(&self) -> Vec<String>;
98
99    /// Execute the command and return the typed output
100    async fn execute(&self) -> Result<Self::Output>;
101
102    /// Helper method to execute the command with proper error handling
103    async fn execute_command(&self, command_args: Vec<String>) -> Result<CommandOutput> {
104        let executor = self.get_executor();
105
106        // For compose commands, we need to handle "docker compose <subcommand>"
107        // For regular commands, we handle "docker <command>"
108        if command_args.first() == Some(&"compose".to_string()) {
109            // This is a compose command - args are already formatted correctly
110            executor.execute_command("docker", command_args).await
111        } else {
112            // Regular docker command - first arg is the command name
113            let command_name = command_args
114                .first()
115                .unwrap_or(&"docker".to_string())
116                .clone();
117            let remaining_args = command_args.iter().skip(1).cloned().collect();
118            executor
119                .execute_command(&command_name, remaining_args)
120                .await
121        }
122    }
123
124    /// Add a raw argument to the command (escape hatch)
125    fn arg<S: AsRef<OsStr>>(&mut self, arg: S) -> &mut Self {
126        self.get_executor_mut().add_arg(arg);
127        self
128    }
129
130    /// Add multiple raw arguments to the command (escape hatch)
131    fn args<I, S>(&mut self, args: I) -> &mut Self
132    where
133        I: IntoIterator<Item = S>,
134        S: AsRef<OsStr>,
135    {
136        self.get_executor_mut().add_args(args);
137        self
138    }
139
140    /// Add a flag option (e.g., --detach, --rm)
141    fn flag(&mut self, flag: &str) -> &mut Self {
142        self.get_executor_mut().add_flag(flag);
143        self
144    }
145
146    /// Add a key-value option (e.g., --name value, --env key=value)
147    fn option(&mut self, key: &str, value: &str) -> &mut Self {
148        self.get_executor_mut().add_option(key, value);
149        self
150    }
151}
152
153/// Base configuration for all compose commands
154#[derive(Debug, Clone, Default)]
155pub struct ComposeConfig {
156    /// Compose file paths (-f, --file)
157    pub files: Vec<PathBuf>,
158    /// Project name (-p, --project-name)
159    pub project_name: Option<String>,
160    /// Project directory (--project-directory)
161    pub project_directory: Option<PathBuf>,
162    /// Profiles to enable (--profile)
163    pub profiles: Vec<String>,
164    /// Environment file (--env-file)
165    pub env_file: Option<PathBuf>,
166    /// Run in compatibility mode
167    pub compatibility: bool,
168    /// Execute in dry run mode
169    pub dry_run: bool,
170    /// Progress output type
171    pub progress: Option<ProgressType>,
172    /// ANSI control characters
173    pub ansi: Option<AnsiMode>,
174    /// Max parallelism (-1 for unlimited)
175    pub parallel: Option<i32>,
176}
177
178/// Progress output type for compose commands
179#[derive(Debug, Clone, Copy)]
180pub enum ProgressType {
181    /// Auto-detect
182    Auto,
183    /// TTY output
184    Tty,
185    /// Plain text output
186    Plain,
187    /// JSON output
188    Json,
189    /// Quiet mode
190    Quiet,
191}
192
193impl std::fmt::Display for ProgressType {
194    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
195        match self {
196            Self::Auto => write!(f, "auto"),
197            Self::Tty => write!(f, "tty"),
198            Self::Plain => write!(f, "plain"),
199            Self::Json => write!(f, "json"),
200            Self::Quiet => write!(f, "quiet"),
201        }
202    }
203}
204
205/// ANSI control character mode
206#[derive(Debug, Clone, Copy)]
207pub enum AnsiMode {
208    /// Never print ANSI
209    Never,
210    /// Always print ANSI
211    Always,
212    /// Auto-detect
213    Auto,
214}
215
216impl std::fmt::Display for AnsiMode {
217    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
218        match self {
219            Self::Never => write!(f, "never"),
220            Self::Always => write!(f, "always"),
221            Self::Auto => write!(f, "auto"),
222        }
223    }
224}
225
226impl ComposeConfig {
227    /// Create a new compose configuration
228    #[must_use]
229    pub fn new() -> Self {
230        Self::default()
231    }
232
233    /// Add a compose file
234    #[must_use]
235    pub fn file(mut self, path: impl Into<PathBuf>) -> Self {
236        self.files.push(path.into());
237        self
238    }
239
240    /// Set project name
241    #[must_use]
242    pub fn project_name(mut self, name: impl Into<String>) -> Self {
243        self.project_name = Some(name.into());
244        self
245    }
246
247    /// Set project directory
248    #[must_use]
249    pub fn project_directory(mut self, dir: impl Into<PathBuf>) -> Self {
250        self.project_directory = Some(dir.into());
251        self
252    }
253
254    /// Add a profile
255    #[must_use]
256    pub fn profile(mut self, profile: impl Into<String>) -> Self {
257        self.profiles.push(profile.into());
258        self
259    }
260
261    /// Set environment file
262    #[must_use]
263    pub fn env_file(mut self, path: impl Into<PathBuf>) -> Self {
264        self.env_file = Some(path.into());
265        self
266    }
267
268    /// Enable compatibility mode
269    #[must_use]
270    pub fn compatibility(mut self) -> Self {
271        self.compatibility = true;
272        self
273    }
274
275    /// Enable dry run mode
276    #[must_use]
277    pub fn dry_run(mut self) -> Self {
278        self.dry_run = true;
279        self
280    }
281
282    /// Set progress output type
283    #[must_use]
284    pub fn progress(mut self, progress: ProgressType) -> Self {
285        self.progress = Some(progress);
286        self
287    }
288
289    /// Set ANSI mode
290    #[must_use]
291    pub fn ansi(mut self, ansi: AnsiMode) -> Self {
292        self.ansi = Some(ansi);
293        self
294    }
295
296    /// Set max parallelism
297    #[must_use]
298    pub fn parallel(mut self, parallel: i32) -> Self {
299        self.parallel = Some(parallel);
300        self
301    }
302
303    /// Build global compose arguments
304    #[must_use]
305    pub fn build_global_args(&self) -> Vec<String> {
306        let mut args = Vec::new();
307
308        // Add compose files
309        for file in &self.files {
310            args.push("--file".to_string());
311            args.push(file.to_string_lossy().to_string());
312        }
313
314        // Add project name
315        if let Some(ref name) = self.project_name {
316            args.push("--project-name".to_string());
317            args.push(name.clone());
318        }
319
320        // Add project directory
321        if let Some(ref dir) = self.project_directory {
322            args.push("--project-directory".to_string());
323            args.push(dir.to_string_lossy().to_string());
324        }
325
326        // Add profiles
327        for profile in &self.profiles {
328            args.push("--profile".to_string());
329            args.push(profile.clone());
330        }
331
332        // Add environment file
333        if let Some(ref env_file) = self.env_file {
334            args.push("--env-file".to_string());
335            args.push(env_file.to_string_lossy().to_string());
336        }
337
338        // Add flags
339        if self.compatibility {
340            args.push("--compatibility".to_string());
341        }
342
343        if self.dry_run {
344            args.push("--dry-run".to_string());
345        }
346
347        // Add progress type
348        if let Some(progress) = self.progress {
349            args.push("--progress".to_string());
350            args.push(progress.to_string());
351        }
352
353        // Add ANSI mode
354        if let Some(ansi) = self.ansi {
355            args.push("--ansi".to_string());
356            args.push(ansi.to_string());
357        }
358
359        // Add parallel limit
360        if let Some(parallel) = self.parallel {
361            args.push("--parallel".to_string());
362            args.push(parallel.to_string());
363        }
364
365        args
366    }
367}
368
369/// Extended trait for Docker Compose commands
370pub trait ComposeCommand: DockerCommand {
371    /// Get the compose configuration
372    fn get_config(&self) -> &ComposeConfig;
373
374    /// Get mutable compose configuration for builder pattern
375    fn get_config_mut(&mut self) -> &mut ComposeConfig;
376
377    /// Get the compose subcommand name (e.g., "up", "down", "ps")
378    fn subcommand(&self) -> &'static str;
379
380    /// Build command-specific arguments (without global compose args)
381    fn build_subcommand_args(&self) -> Vec<String>;
382
383    /// Build complete command arguments including "compose" and global args\
384    /// (This provides the implementation for `DockerCommandV2::build_command_args`)
385    fn build_command_args(&self) -> Vec<String> {
386        let mut args = vec!["compose".to_string()];
387
388        // Add global compose arguments
389        args.extend(self.get_config().build_global_args());
390
391        // Add the subcommand
392        args.push(self.subcommand().to_string());
393
394        // Add command-specific arguments
395        args.extend(self.build_subcommand_args());
396
397        // Add raw arguments from executor
398        args.extend(self.get_executor().raw_args.clone());
399
400        args
401    }
402
403    /// Helper builder methods for common compose config options
404    #[must_use]
405    fn file<P: Into<PathBuf>>(mut self, file: P) -> Self
406    where
407        Self: Sized,
408    {
409        self.get_config_mut().files.push(file.into());
410        self
411    }
412
413    /// Set project name for compose command
414    #[must_use]
415    fn project_name(mut self, name: impl Into<String>) -> Self
416    where
417        Self: Sized,
418    {
419        self.get_config_mut().project_name = Some(name.into());
420        self
421    }
422}
423
424/// Common functionality for executing Docker commands
425#[derive(Debug, Clone)]
426pub struct CommandExecutor {
427    /// Additional raw arguments added via escape hatch
428    pub raw_args: Vec<String>,
429    /// Platform information for runtime abstraction
430    pub platform_info: Option<PlatformInfo>,
431}
432
433impl CommandExecutor {
434    /// Create a new command executor
435    #[must_use]
436    pub fn new() -> Self {
437        Self {
438            raw_args: Vec::new(),
439            platform_info: None,
440        }
441    }
442
443    /// Create a new command executor with platform detection
444    ///
445    /// # Errors
446    ///
447    /// Returns an error if platform detection fails
448    pub fn with_platform() -> Result<Self> {
449        let platform_info = PlatformInfo::detect()?;
450        Ok(Self {
451            raw_args: Vec::new(),
452            platform_info: Some(platform_info),
453        })
454    }
455
456    /// Set the platform information
457    #[must_use]
458    pub fn platform(mut self, platform_info: PlatformInfo) -> Self {
459        self.platform_info = Some(platform_info);
460        self
461    }
462
463    /// Get the runtime command to use
464    fn get_runtime_command(&self) -> String {
465        if let Some(ref platform_info) = self.platform_info {
466            platform_info.runtime.command().to_string()
467        } else {
468            "docker".to_string()
469        }
470    }
471
472    /// Execute a Docker command with the given arguments
473    ///
474    /// # Errors
475    /// Returns an error if the Docker command fails to execute or returns a non-zero exit code
476    pub async fn execute_command(
477        &self,
478        command_name: &str,
479        args: Vec<String>,
480    ) -> Result<CommandOutput> {
481        // Prepend raw args (they should come before command-specific args)
482        let mut all_args = self.raw_args.clone();
483        all_args.extend(args);
484
485        // Insert the command name at the beginning
486        all_args.insert(0, command_name.to_string());
487
488        let runtime_command = self.get_runtime_command();
489        let mut command = TokioCommand::new(&runtime_command);
490
491        // Set environment variables from platform info
492        if let Some(ref platform_info) = self.platform_info {
493            for (key, value) in platform_info.environment_vars() {
494                command.env(key, value);
495            }
496        }
497
498        let output = command
499            .args(&all_args)
500            .stdout(Stdio::piped())
501            .stderr(Stdio::piped())
502            .output()
503            .await
504            .map_err(|e| {
505                Error::custom(format!(
506                    "Failed to execute {runtime_command} {command_name}: {e}"
507                ))
508            })?;
509
510        let stdout = String::from_utf8_lossy(&output.stdout).to_string();
511        let stderr = String::from_utf8_lossy(&output.stderr).to_string();
512        let success = output.status.success();
513        let exit_code = output.status.code().unwrap_or(-1);
514
515        if !success {
516            return Err(Error::command_failed(
517                format!("{} {}", runtime_command, all_args.join(" ")),
518                exit_code,
519                stdout,
520                stderr,
521            ));
522        }
523
524        Ok(CommandOutput {
525            stdout,
526            stderr,
527            exit_code,
528            success,
529        })
530    }
531
532    /// Add a raw argument
533    pub fn add_arg<S: AsRef<OsStr>>(&mut self, arg: S) {
534        self.raw_args
535            .push(arg.as_ref().to_string_lossy().to_string());
536    }
537
538    /// Add multiple raw arguments
539    pub fn add_args<I, S>(&mut self, args: I)
540    where
541        I: IntoIterator<Item = S>,
542        S: AsRef<OsStr>,
543    {
544        for arg in args {
545            self.add_arg(arg);
546        }
547    }
548
549    /// Add a flag option
550    pub fn add_flag(&mut self, flag: &str) {
551        let flag_arg = if flag.starts_with('-') {
552            flag.to_string()
553        } else if flag.len() == 1 {
554            format!("-{flag}")
555        } else {
556            format!("--{flag}")
557        };
558        self.raw_args.push(flag_arg);
559    }
560
561    /// Add a key-value option
562    pub fn add_option(&mut self, key: &str, value: &str) {
563        let key_arg = if key.starts_with('-') {
564            key.to_string()
565        } else if key.len() == 1 {
566            format!("-{key}")
567        } else {
568            format!("--{key}")
569        };
570        self.raw_args.push(key_arg);
571        self.raw_args.push(value.to_string());
572    }
573}
574
575impl Default for CommandExecutor {
576    fn default() -> Self {
577        Self::new()
578    }
579}
580
581/// Output from executing a Docker command
582#[derive(Debug, Clone)]
583pub struct CommandOutput {
584    /// Standard output from the command
585    pub stdout: String,
586    /// Standard error from the command
587    pub stderr: String,
588    /// Exit code
589    pub exit_code: i32,
590    /// Whether the command was successful
591    pub success: bool,
592}
593
594impl CommandOutput {
595    /// Get stdout lines as a vector
596    #[must_use]
597    pub fn stdout_lines(&self) -> Vec<&str> {
598        self.stdout.lines().collect()
599    }
600
601    /// Get stderr lines as a vector
602    #[must_use]
603    pub fn stderr_lines(&self) -> Vec<&str> {
604        self.stderr.lines().collect()
605    }
606
607    /// Check if stdout is empty
608    #[must_use]
609    pub fn stdout_is_empty(&self) -> bool {
610        self.stdout.trim().is_empty()
611    }
612
613    /// Check if stderr is empty
614    #[must_use]
615    pub fn stderr_is_empty(&self) -> bool {
616        self.stderr.trim().is_empty()
617    }
618}
619
620/// Helper for building environment variables
621#[derive(Debug, Clone, Default)]
622pub struct EnvironmentBuilder {
623    vars: HashMap<String, String>,
624}
625
626impl EnvironmentBuilder {
627    /// Create a new environment builder
628    #[must_use]
629    pub fn new() -> Self {
630        Self::default()
631    }
632
633    /// Add an environment variable
634    #[must_use]
635    pub fn var(mut self, key: impl Into<String>, value: impl Into<String>) -> Self {
636        self.vars.insert(key.into(), value.into());
637        self
638    }
639
640    /// Add multiple environment variables from a `HashMap`
641    #[must_use]
642    pub fn vars(mut self, vars: HashMap<String, String>) -> Self {
643        self.vars.extend(vars);
644        self
645    }
646
647    /// Build the environment arguments for Docker
648    #[must_use]
649    pub fn build_args(&self) -> Vec<String> {
650        let mut args = Vec::new();
651        for (key, value) in &self.vars {
652            args.push("--env".to_string());
653            args.push(format!("{key}={value}"));
654        }
655        args
656    }
657
658    /// Get the environment variables as a `HashMap`
659    #[must_use]
660    pub fn as_map(&self) -> &HashMap<String, String> {
661        &self.vars
662    }
663}
664
665/// Helper for building port mappings
666#[derive(Debug, Clone, Default)]
667pub struct PortBuilder {
668    mappings: Vec<PortMapping>,
669}
670
671impl PortBuilder {
672    /// Create a new port builder
673    #[must_use]
674    pub fn new() -> Self {
675        Self::default()
676    }
677
678    /// Add a port mapping
679    #[must_use]
680    pub fn port(mut self, host_port: u16, container_port: u16) -> Self {
681        self.mappings.push(PortMapping {
682            host_port: Some(host_port),
683            container_port,
684            protocol: Protocol::Tcp,
685            host_ip: None,
686        });
687        self
688    }
689
690    /// Add a port mapping with protocol
691    #[must_use]
692    pub fn port_with_protocol(
693        mut self,
694        host_port: u16,
695        container_port: u16,
696        protocol: Protocol,
697    ) -> Self {
698        self.mappings.push(PortMapping {
699            host_port: Some(host_port),
700            container_port,
701            protocol,
702            host_ip: None,
703        });
704        self
705    }
706
707    /// Add a dynamic port mapping (Docker assigns host port)
708    #[must_use]
709    pub fn dynamic_port(mut self, container_port: u16) -> Self {
710        self.mappings.push(PortMapping {
711            host_port: None,
712            container_port,
713            protocol: Protocol::Tcp,
714            host_ip: None,
715        });
716        self
717    }
718
719    /// Build the port arguments for Docker
720    #[must_use]
721    pub fn build_args(&self) -> Vec<String> {
722        let mut args = Vec::new();
723        for mapping in &self.mappings {
724            args.push("--publish".to_string());
725            args.push(mapping.to_string());
726        }
727        args
728    }
729
730    /// Get the port mappings
731    #[must_use]
732    pub fn mappings(&self) -> &[PortMapping] {
733        &self.mappings
734    }
735}
736
737/// Port mapping configuration
738#[derive(Debug, Clone)]
739pub struct PortMapping {
740    /// Host port (None for dynamic allocation)
741    pub host_port: Option<u16>,
742    /// Container port
743    pub container_port: u16,
744    /// Protocol (TCP or UDP)
745    pub protocol: Protocol,
746    /// Host IP to bind to (None for all interfaces)
747    pub host_ip: Option<std::net::IpAddr>,
748}
749
750impl std::fmt::Display for PortMapping {
751    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
752        let protocol_suffix = match self.protocol {
753            Protocol::Tcp => "",
754            Protocol::Udp => "/udp",
755        };
756
757        if let Some(host_port) = self.host_port {
758            if let Some(host_ip) = self.host_ip {
759                write!(
760                    f,
761                    "{}:{}:{}{}",
762                    host_ip, host_port, self.container_port, protocol_suffix
763                )
764            } else {
765                write!(
766                    f,
767                    "{}:{}{}",
768                    host_port, self.container_port, protocol_suffix
769                )
770            }
771        } else {
772            write!(f, "{}{}", self.container_port, protocol_suffix)
773        }
774    }
775}
776
777/// Network protocol for port mappings
778#[derive(Debug, Clone, Copy, PartialEq, Eq)]
779pub enum Protocol {
780    /// TCP protocol
781    Tcp,
782    /// UDP protocol
783    Udp,
784}
785
786#[cfg(test)]
787mod tests {
788    use super::*;
789
790    #[test]
791    fn test_command_executor_args() {
792        let mut executor = CommandExecutor::new();
793        executor.add_arg("test");
794        executor.add_args(vec!["arg1", "arg2"]);
795        executor.add_flag("detach");
796        executor.add_flag("d");
797        executor.add_option("name", "test-container");
798
799        assert_eq!(
800            executor.raw_args,
801            vec![
802                "test",
803                "arg1",
804                "arg2",
805                "--detach",
806                "-d",
807                "--name",
808                "test-container"
809            ]
810        );
811    }
812
813    #[test]
814    fn test_environment_builder() {
815        let env = EnvironmentBuilder::new()
816            .var("KEY1", "value1")
817            .var("KEY2", "value2");
818
819        let args = env.build_args();
820        assert!(args.contains(&"--env".to_string()));
821        assert!(args.contains(&"KEY1=value1".to_string()));
822        assert!(args.contains(&"KEY2=value2".to_string()));
823    }
824
825    #[test]
826    fn test_port_builder() {
827        let ports = PortBuilder::new()
828            .port(8080, 80)
829            .dynamic_port(443)
830            .port_with_protocol(8081, 81, Protocol::Udp);
831
832        let args = ports.build_args();
833        assert!(args.contains(&"--publish".to_string()));
834        assert!(args.contains(&"8080:80".to_string()));
835        assert!(args.contains(&"443".to_string()));
836        assert!(args.contains(&"8081:81/udp".to_string()));
837    }
838
839    #[test]
840    fn test_port_mapping_display() {
841        let tcp_mapping = PortMapping {
842            host_port: Some(8080),
843            container_port: 80,
844            protocol: Protocol::Tcp,
845            host_ip: None,
846        };
847        assert_eq!(tcp_mapping.to_string(), "8080:80");
848
849        let udp_mapping = PortMapping {
850            host_port: Some(8081),
851            container_port: 81,
852            protocol: Protocol::Udp,
853            host_ip: None,
854        };
855        assert_eq!(udp_mapping.to_string(), "8081:81/udp");
856
857        let dynamic_mapping = PortMapping {
858            host_port: None,
859            container_port: 443,
860            protocol: Protocol::Tcp,
861            host_ip: None,
862        };
863        assert_eq!(dynamic_mapping.to_string(), "443");
864    }
865
866    #[test]
867    fn test_command_output_helpers() {
868        let output = CommandOutput {
869            stdout: "line1\nline2".to_string(),
870            stderr: "error1\nerror2".to_string(),
871            exit_code: 0,
872            success: true,
873        };
874
875        assert_eq!(output.stdout_lines(), vec!["line1", "line2"]);
876        assert_eq!(output.stderr_lines(), vec!["error1", "error2"]);
877        assert!(!output.stdout_is_empty());
878        assert!(!output.stderr_is_empty());
879
880        let empty_output = CommandOutput {
881            stdout: "   ".to_string(),
882            stderr: String::new(),
883            exit_code: 0,
884            success: true,
885        };
886
887        assert!(empty_output.stdout_is_empty());
888        assert!(empty_output.stderr_is_empty());
889    }
890}