docker_wrapper/command/
info.rs

1//! Docker info command implementation
2//!
3//! This module provides functionality to retrieve Docker system information,
4//! including daemon configuration, storage details, and runtime information.
5
6use super::{CommandExecutor, CommandOutput, DockerCommand};
7use crate::error::{Error, Result};
8use async_trait::async_trait;
9use std::fmt;
10
11/// Command for retrieving Docker system information
12///
13/// The `InfoCommand` provides a builder pattern for constructing Docker info commands
14/// with various output format options.
15///
16/// # Examples
17///
18/// ```rust
19/// use docker_wrapper::InfoCommand;
20///
21/// // Basic system info
22/// let info = InfoCommand::new();
23///
24/// // JSON format output
25/// let info = InfoCommand::new().format_json();
26///
27/// // Custom format
28/// let info = InfoCommand::new()
29///     .format("{{.ServerVersion}}");
30/// ```
31#[derive(Debug, Clone)]
32pub struct InfoCommand {
33    /// Output format
34    format: Option<String>,
35    /// Command executor for running the command
36    pub executor: CommandExecutor,
37}
38
39/// Docker system information
40#[derive(Debug, Clone, PartialEq)]
41pub struct SystemInfo {
42    /// Docker server version
43    pub server_version: String,
44    /// Storage driver in use
45    pub storage_driver: String,
46    /// Logging driver
47    pub logging_driver: String,
48    /// Cgroup driver
49    pub cgroup_driver: String,
50    /// Cgroup version
51    pub cgroup_version: String,
52    /// Number of containers
53    pub containers: u32,
54    /// Number of running containers
55    pub containers_running: u32,
56    /// Number of paused containers
57    pub containers_paused: u32,
58    /// Number of stopped containers
59    pub containers_stopped: u32,
60    /// Number of images
61    pub images: u32,
62    /// Docker root directory
63    pub docker_root_dir: String,
64    /// Debug mode enabled
65    pub debug: bool,
66    /// Experimental features enabled
67    pub experimental: bool,
68    /// Total memory
69    pub mem_total: u64,
70    /// Number of CPUs
71    pub ncpu: u32,
72    /// Operating system
73    pub operating_system: String,
74    /// OS type
75    pub os_type: String,
76    /// Architecture
77    pub architecture: String,
78    /// Kernel version
79    pub kernel_version: String,
80    /// Name (hostname)
81    pub name: String,
82    /// Docker daemon ID
83    pub id: String,
84}
85
86/// Docker registry configuration
87#[derive(Debug, Clone, PartialEq)]
88pub struct RegistryConfig {
89    /// Insecure registries
90    pub insecure_registries: Vec<String>,
91    /// Index configs
92    pub index_configs: Vec<String>,
93    /// Mirrors
94    pub mirrors: Vec<String>,
95}
96
97/// Docker runtime information
98#[derive(Debug, Clone, PartialEq)]
99pub struct RuntimeInfo {
100    /// Default runtime
101    pub default_runtime: String,
102    /// Available runtimes
103    pub runtimes: Vec<String>,
104}
105
106/// Complete Docker system information
107#[derive(Debug, Clone, PartialEq)]
108pub struct DockerInfo {
109    /// Basic system information
110    pub system: SystemInfo,
111    /// Registry configuration
112    pub registry: Option<RegistryConfig>,
113    /// Runtime information
114    pub runtime: Option<RuntimeInfo>,
115    /// Warnings from the Docker daemon
116    pub warnings: Vec<String>,
117}
118
119/// Output from an info command execution
120///
121/// Contains the raw output from the Docker info command and provides
122/// convenience methods for parsing system information.
123#[derive(Debug, Clone)]
124pub struct InfoOutput {
125    /// Raw output from the Docker command
126    pub output: CommandOutput,
127    /// Parsed Docker information
128    pub docker_info: Option<DockerInfo>,
129}
130
131impl InfoCommand {
132    /// Creates a new info command
133    ///
134    /// # Examples
135    ///
136    /// ```rust
137    /// use docker_wrapper::InfoCommand;
138    ///
139    /// let info = InfoCommand::new();
140    /// ```
141    #[must_use]
142    pub fn new() -> Self {
143        Self {
144            format: None,
145            executor: CommandExecutor::default(),
146        }
147    }
148
149    /// Sets the output format
150    ///
151    /// # Arguments
152    ///
153    /// * `format` - Output format string or template
154    ///
155    /// # Examples
156    ///
157    /// ```rust
158    /// use docker_wrapper::InfoCommand;
159    ///
160    /// let info = InfoCommand::new()
161    ///     .format("{{.ServerVersion}}");
162    /// ```
163    #[must_use]
164    pub fn format(mut self, format: impl Into<String>) -> Self {
165        self.format = Some(format.into());
166        self
167    }
168
169    /// Sets output format to JSON
170    ///
171    /// # Examples
172    ///
173    /// ```rust
174    /// use docker_wrapper::InfoCommand;
175    ///
176    /// let info = InfoCommand::new().format_json();
177    /// ```
178    #[must_use]
179    pub fn format_json(self) -> Self {
180        self.format("json")
181    }
182
183    /// Sets output format to table (default)
184    #[must_use]
185    pub fn format_table(self) -> Self {
186        Self {
187            format: None,
188            executor: self.executor,
189        }
190    }
191
192    /// Gets the command executor
193    #[must_use]
194    pub fn get_executor(&self) -> &CommandExecutor {
195        &self.executor
196    }
197
198    /// Gets the command executor mutably
199    pub fn get_executor_mut(&mut self) -> &mut CommandExecutor {
200        &mut self.executor
201    }
202
203    /// Builds the command arguments for Docker info
204    #[must_use]
205    pub fn build_command_args(&self) -> Vec<String> {
206        let mut args = vec!["info".to_string()];
207
208        // Add format option
209        if let Some(ref format) = self.format {
210            args.push("--format".to_string());
211            args.push(format.clone());
212        }
213
214        // Add any additional raw arguments
215        args.extend(self.executor.raw_args.clone());
216
217        args
218    }
219
220    /// Parses the info output
221    fn parse_output(&self, output: &CommandOutput) -> Result<Option<DockerInfo>> {
222        if let Some(ref format) = self.format {
223            if format == "json" {
224                return Self::parse_json_output(output);
225            }
226        }
227
228        Ok(Self::parse_table_output(output))
229    }
230
231    /// Parses JSON formatted info output
232    fn parse_json_output(output: &CommandOutput) -> Result<Option<DockerInfo>> {
233        let parsed: serde_json::Value = serde_json::from_str(&output.stdout)
234            .map_err(|e| Error::parse_error(format!("Failed to parse info JSON output: {e}")))?;
235
236        let system = SystemInfo {
237            server_version: parsed["ServerVersion"].as_str().unwrap_or("").to_string(),
238            storage_driver: parsed["Driver"].as_str().unwrap_or("").to_string(),
239            logging_driver: parsed["LoggingDriver"].as_str().unwrap_or("").to_string(),
240            cgroup_driver: parsed["CgroupDriver"].as_str().unwrap_or("").to_string(),
241            cgroup_version: parsed["CgroupVersion"].as_str().unwrap_or("").to_string(),
242            containers: u32::try_from(parsed["Containers"].as_u64().unwrap_or(0)).unwrap_or(0),
243            containers_running: u32::try_from(parsed["ContainersRunning"].as_u64().unwrap_or(0))
244                .unwrap_or(0),
245            containers_paused: u32::try_from(parsed["ContainersPaused"].as_u64().unwrap_or(0))
246                .unwrap_or(0),
247            containers_stopped: u32::try_from(parsed["ContainersStopped"].as_u64().unwrap_or(0))
248                .unwrap_or(0),
249            images: u32::try_from(parsed["Images"].as_u64().unwrap_or(0)).unwrap_or(0),
250            docker_root_dir: parsed["DockerRootDir"].as_str().unwrap_or("").to_string(),
251            debug: parsed["Debug"].as_bool().unwrap_or(false),
252            experimental: parsed["ExperimentalBuild"].as_bool().unwrap_or(false),
253            mem_total: parsed["MemTotal"].as_u64().unwrap_or(0),
254            ncpu: u32::try_from(parsed["NCPU"].as_u64().unwrap_or(0)).unwrap_or(0),
255            operating_system: parsed["OperatingSystem"].as_str().unwrap_or("").to_string(),
256            os_type: parsed["OSType"].as_str().unwrap_or("").to_string(),
257            architecture: parsed["Architecture"].as_str().unwrap_or("").to_string(),
258            kernel_version: parsed["KernelVersion"].as_str().unwrap_or("").to_string(),
259            name: parsed["Name"].as_str().unwrap_or("").to_string(),
260            id: parsed["ID"].as_str().unwrap_or("").to_string(),
261        };
262
263        // Parse registry config
264        let registry = parsed.get("RegistryConfig").map(|registry_data| {
265            let insecure_registries = registry_data["InsecureRegistryCIDRs"]
266                .as_array()
267                .map(|arr| {
268                    arr.iter()
269                        .filter_map(|v| v.as_str())
270                        .map(String::from)
271                        .collect()
272                })
273                .unwrap_or_default();
274
275            let index_configs = registry_data["IndexConfigs"]
276                .as_object()
277                .map(|obj| obj.keys().map(String::from).collect())
278                .unwrap_or_default();
279
280            let mirrors = registry_data["Mirrors"]
281                .as_array()
282                .map(|arr| {
283                    arr.iter()
284                        .filter_map(|v| v.as_str())
285                        .map(String::from)
286                        .collect()
287                })
288                .unwrap_or_default();
289
290            RegistryConfig {
291                insecure_registries,
292                index_configs,
293                mirrors,
294            }
295        });
296
297        // Parse runtime info
298        let runtime = parsed.get("Runtimes").map(|runtimes_data| {
299            let default_runtime = parsed["DefaultRuntime"].as_str().unwrap_or("").to_string();
300
301            let runtimes = runtimes_data
302                .as_object()
303                .map(|obj| obj.keys().map(String::from).collect())
304                .unwrap_or_default();
305
306            RuntimeInfo {
307                default_runtime,
308                runtimes,
309            }
310        });
311
312        // Parse warnings
313        let warnings = parsed["Warnings"]
314            .as_array()
315            .map(|arr| {
316                arr.iter()
317                    .filter_map(|v| v.as_str())
318                    .map(String::from)
319                    .collect()
320            })
321            .unwrap_or_default();
322
323        Ok(Some(DockerInfo {
324            system,
325            registry,
326            runtime,
327            warnings,
328        }))
329    }
330
331    /// Parses table formatted info output
332    fn parse_table_output(output: &CommandOutput) -> Option<DockerInfo> {
333        let lines: Vec<&str> = output.stdout.lines().collect();
334
335        if lines.is_empty() {
336            return None;
337        }
338
339        let mut data = std::collections::HashMap::new();
340        let mut warnings = Vec::new();
341
342        for line in lines {
343            let trimmed = line.trim();
344
345            if trimmed.is_empty() {
346                continue;
347            }
348
349            // Check for warnings
350            if trimmed.starts_with("WARNING:") {
351                warnings.push(trimmed.to_string());
352                continue;
353            }
354
355            // Parse key-value pairs
356            if let Some(colon_pos) = trimmed.find(':') {
357                let key = trimmed[..colon_pos].trim();
358                let value = trimmed[colon_pos + 1..].trim();
359                data.insert(key.to_string(), value.to_string());
360            }
361        }
362
363        let system = SystemInfo {
364            server_version: data.get("Server Version").cloned().unwrap_or_default(),
365            storage_driver: data.get("Storage Driver").cloned().unwrap_or_default(),
366            logging_driver: data.get("Logging Driver").cloned().unwrap_or_default(),
367            cgroup_driver: data.get("Cgroup Driver").cloned().unwrap_or_default(),
368            cgroup_version: data.get("Cgroup Version").cloned().unwrap_or_default(),
369            containers: data
370                .get("Containers")
371                .and_then(|s| s.parse().ok())
372                .unwrap_or(0),
373            containers_running: data
374                .get("Running")
375                .and_then(|s| s.parse().ok())
376                .unwrap_or(0),
377            containers_paused: data.get("Paused").and_then(|s| s.parse().ok()).unwrap_or(0),
378            containers_stopped: data
379                .get("Stopped")
380                .and_then(|s| s.parse().ok())
381                .unwrap_or(0),
382            images: data.get("Images").and_then(|s| s.parse().ok()).unwrap_or(0),
383            docker_root_dir: data.get("Docker Root Dir").cloned().unwrap_or_default(),
384            debug: data.get("Debug Mode").is_some_and(|s| s == "true"),
385            experimental: data.get("Experimental").is_some_and(|s| s == "true"),
386            mem_total: data
387                .get("Total Memory")
388                .and_then(|s| s.split_whitespace().next())
389                .and_then(|s| s.parse().ok())
390                .unwrap_or(0),
391            ncpu: data.get("CPUs").and_then(|s| s.parse().ok()).unwrap_or(0),
392            operating_system: data.get("Operating System").cloned().unwrap_or_default(),
393            os_type: data.get("OSType").cloned().unwrap_or_default(),
394            architecture: data.get("Architecture").cloned().unwrap_or_default(),
395            kernel_version: data.get("Kernel Version").cloned().unwrap_or_default(),
396            name: data.get("Name").cloned().unwrap_or_default(),
397            id: data.get("ID").cloned().unwrap_or_default(),
398        };
399
400        Some(DockerInfo {
401            system,
402            registry: None, // Not easily parseable from table format
403            runtime: None,  // Not easily parseable from table format
404            warnings,
405        })
406    }
407
408    /// Gets the output format (if set)
409    #[must_use]
410    pub fn get_format(&self) -> Option<&str> {
411        self.format.as_deref()
412    }
413}
414
415impl Default for InfoCommand {
416    fn default() -> Self {
417        Self::new()
418    }
419}
420
421impl InfoOutput {
422    /// Returns true if the info command was successful
423    #[must_use]
424    pub fn success(&self) -> bool {
425        self.output.success
426    }
427
428    /// Gets the Docker server version
429    #[must_use]
430    pub fn server_version(&self) -> Option<&str> {
431        self.docker_info
432            .as_ref()
433            .map(|info| info.system.server_version.as_str())
434    }
435
436    /// Gets the storage driver
437    #[must_use]
438    pub fn storage_driver(&self) -> Option<&str> {
439        self.docker_info
440            .as_ref()
441            .map(|info| info.system.storage_driver.as_str())
442    }
443
444    /// Gets the total number of containers
445    #[must_use]
446    pub fn container_count(&self) -> u32 {
447        self.docker_info
448            .as_ref()
449            .map_or(0, |info| info.system.containers)
450    }
451
452    /// Gets the number of running containers
453    #[must_use]
454    pub fn running_containers(&self) -> u32 {
455        self.docker_info
456            .as_ref()
457            .map_or(0, |info| info.system.containers_running)
458    }
459
460    /// Gets the number of images
461    #[must_use]
462    pub fn image_count(&self) -> u32 {
463        self.docker_info
464            .as_ref()
465            .map_or(0, |info| info.system.images)
466    }
467
468    /// Returns true if debug mode is enabled
469    #[must_use]
470    pub fn is_debug(&self) -> bool {
471        self.docker_info
472            .as_ref()
473            .is_some_and(|info| info.system.debug)
474    }
475
476    /// Returns true if experimental features are enabled
477    #[must_use]
478    pub fn is_experimental(&self) -> bool {
479        self.docker_info
480            .as_ref()
481            .is_some_and(|info| info.system.experimental)
482    }
483
484    /// Gets the operating system
485    #[must_use]
486    pub fn operating_system(&self) -> Option<&str> {
487        self.docker_info
488            .as_ref()
489            .map(|info| info.system.operating_system.as_str())
490    }
491
492    /// Gets the architecture
493    #[must_use]
494    pub fn architecture(&self) -> Option<&str> {
495        self.docker_info
496            .as_ref()
497            .map(|info| info.system.architecture.as_str())
498    }
499
500    /// Gets any warnings from the Docker daemon
501    #[must_use]
502    pub fn warnings(&self) -> Vec<&str> {
503        self.docker_info
504            .as_ref()
505            .map(|info| info.warnings.iter().map(String::as_str).collect())
506            .unwrap_or_default()
507    }
508
509    /// Returns true if there are any warnings
510    #[must_use]
511    pub fn has_warnings(&self) -> bool {
512        self.docker_info
513            .as_ref()
514            .is_some_and(|info| !info.warnings.is_empty())
515    }
516
517    /// Gets system resource information (containers and images)
518    #[must_use]
519    pub fn resource_summary(&self) -> (u32, u32, u32) {
520        if let Some(info) = &self.docker_info {
521            (
522                info.system.containers,
523                info.system.containers_running,
524                info.system.images,
525            )
526        } else {
527            (0, 0, 0)
528        }
529    }
530}
531
532#[async_trait]
533impl DockerCommand for InfoCommand {
534    type Output = InfoOutput;
535
536    fn get_executor(&self) -> &CommandExecutor {
537        &self.executor
538    }
539
540    fn get_executor_mut(&mut self) -> &mut CommandExecutor {
541        &mut self.executor
542    }
543
544    fn build_command_args(&self) -> Vec<String> {
545        self.build_command_args()
546    }
547
548    async fn execute(&self) -> Result<Self::Output> {
549        let args = self.build_command_args();
550        let output = self.execute_command(args).await?;
551
552        let docker_info = self.parse_output(&output)?;
553
554        Ok(InfoOutput {
555            output,
556            docker_info,
557        })
558    }
559}
560
561impl fmt::Display for InfoCommand {
562    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
563        write!(f, "docker info")?;
564
565        if let Some(ref format) = self.format {
566            write!(f, " --format {format}")?;
567        }
568
569        Ok(())
570    }
571}
572
573#[cfg(test)]
574mod tests {
575    use super::*;
576
577    #[test]
578    fn test_info_command_basic() {
579        let info = InfoCommand::new();
580
581        assert_eq!(info.get_format(), None);
582
583        let args = info.build_command_args();
584        assert_eq!(args, vec!["info"]);
585    }
586
587    #[test]
588    fn test_info_command_with_format() {
589        let info = InfoCommand::new().format("{{.ServerVersion}}");
590
591        assert_eq!(info.get_format(), Some("{{.ServerVersion}}"));
592
593        let args = info.build_command_args();
594        assert_eq!(args, vec!["info", "--format", "{{.ServerVersion}}"]);
595    }
596
597    #[test]
598    fn test_info_command_json_format() {
599        let info = InfoCommand::new().format_json();
600
601        assert_eq!(info.get_format(), Some("json"));
602
603        let args = info.build_command_args();
604        assert_eq!(args, vec!["info", "--format", "json"]);
605    }
606
607    #[test]
608    fn test_info_command_table_format() {
609        let info = InfoCommand::new().format_json().format_table();
610
611        assert_eq!(info.get_format(), None);
612
613        let args = info.build_command_args();
614        assert_eq!(args, vec!["info"]);
615    }
616
617    #[test]
618    fn test_info_command_default() {
619        let info = InfoCommand::default();
620
621        assert_eq!(info.get_format(), None);
622        let args = info.build_command_args();
623        assert_eq!(args, vec!["info"]);
624    }
625
626    #[test]
627    fn test_system_info_creation() {
628        let system = SystemInfo {
629            server_version: "20.10.17".to_string(),
630            storage_driver: "overlay2".to_string(),
631            logging_driver: "json-file".to_string(),
632            cgroup_driver: "systemd".to_string(),
633            cgroup_version: "2".to_string(),
634            containers: 10,
635            containers_running: 3,
636            containers_paused: 0,
637            containers_stopped: 7,
638            images: 25,
639            docker_root_dir: "/var/lib/docker".to_string(),
640            debug: false,
641            experimental: false,
642            mem_total: 8_589_934_592,
643            ncpu: 8,
644            operating_system: "Ubuntu 20.04.4 LTS".to_string(),
645            os_type: "linux".to_string(),
646            architecture: "x86_64".to_string(),
647            kernel_version: "5.15.0-56-generic".to_string(),
648            name: "docker-host".to_string(),
649            id: "ABCD:1234:5678:90EF".to_string(),
650        };
651
652        assert_eq!(system.server_version, "20.10.17");
653        assert_eq!(system.storage_driver, "overlay2");
654        assert_eq!(system.containers, 10);
655        assert_eq!(system.containers_running, 3);
656        assert_eq!(system.images, 25);
657        assert!(!system.debug);
658        assert!(!system.experimental);
659    }
660
661    #[test]
662    fn test_registry_config_creation() {
663        let registry = RegistryConfig {
664            insecure_registries: vec!["localhost:5000".to_string()],
665            index_configs: vec!["https://index.docker.io/v1/".to_string()],
666            mirrors: vec!["https://mirror.gcr.io".to_string()],
667        };
668
669        assert_eq!(registry.insecure_registries.len(), 1);
670        assert_eq!(registry.index_configs.len(), 1);
671        assert_eq!(registry.mirrors.len(), 1);
672    }
673
674    #[test]
675    fn test_runtime_info_creation() {
676        let runtime = RuntimeInfo {
677            default_runtime: "runc".to_string(),
678            runtimes: vec!["runc".to_string(), "nvidia".to_string()],
679        };
680
681        assert_eq!(runtime.default_runtime, "runc");
682        assert_eq!(runtime.runtimes.len(), 2);
683    }
684
685    #[test]
686    fn test_docker_info_creation() {
687        let system = SystemInfo {
688            server_version: "20.10.17".to_string(),
689            storage_driver: "overlay2".to_string(),
690            logging_driver: "json-file".to_string(),
691            cgroup_driver: "systemd".to_string(),
692            cgroup_version: "2".to_string(),
693            containers: 5,
694            containers_running: 2,
695            containers_paused: 0,
696            containers_stopped: 3,
697            images: 10,
698            docker_root_dir: "/var/lib/docker".to_string(),
699            debug: true,
700            experimental: true,
701            mem_total: 8_589_934_592,
702            ncpu: 4,
703            operating_system: "Ubuntu 20.04".to_string(),
704            os_type: "linux".to_string(),
705            architecture: "x86_64".to_string(),
706            kernel_version: "5.15.0".to_string(),
707            name: "test-host".to_string(),
708            id: "TEST:1234".to_string(),
709        };
710
711        let docker_info = DockerInfo {
712            system,
713            registry: None,
714            runtime: None,
715            warnings: vec!["Test warning".to_string()],
716        };
717
718        assert_eq!(docker_info.system.server_version, "20.10.17");
719        assert_eq!(docker_info.warnings.len(), 1);
720        assert!(docker_info.registry.is_none());
721        assert!(docker_info.runtime.is_none());
722    }
723
724    #[test]
725    fn test_info_output_helpers() {
726        let system = SystemInfo {
727            server_version: "20.10.17".to_string(),
728            storage_driver: "overlay2".to_string(),
729            logging_driver: "json-file".to_string(),
730            cgroup_driver: "systemd".to_string(),
731            cgroup_version: "2".to_string(),
732            containers: 15,
733            containers_running: 5,
734            containers_paused: 1,
735            containers_stopped: 9,
736            images: 30,
737            docker_root_dir: "/var/lib/docker".to_string(),
738            debug: true,
739            experimental: false,
740            mem_total: 8_589_934_592,
741            ncpu: 8,
742            operating_system: "Ubuntu 22.04 LTS".to_string(),
743            os_type: "linux".to_string(),
744            architecture: "x86_64".to_string(),
745            kernel_version: "5.15.0-56-generic".to_string(),
746            name: "test-docker".to_string(),
747            id: "TEST:ABCD:1234".to_string(),
748        };
749
750        let docker_info = DockerInfo {
751            system,
752            registry: None,
753            runtime: None,
754            warnings: vec![
755                "WARNING: No swap limit support".to_string(),
756                "WARNING: No memory limit support".to_string(),
757            ],
758        };
759
760        let output = InfoOutput {
761            output: CommandOutput {
762                stdout: String::new(),
763                stderr: String::new(),
764                exit_code: 0,
765                success: true,
766            },
767            docker_info: Some(docker_info),
768        };
769
770        assert_eq!(output.server_version(), Some("20.10.17"));
771        assert_eq!(output.storage_driver(), Some("overlay2"));
772        assert_eq!(output.container_count(), 15);
773        assert_eq!(output.running_containers(), 5);
774        assert_eq!(output.image_count(), 30);
775        assert!(output.is_debug());
776        assert!(!output.is_experimental());
777        assert_eq!(output.operating_system(), Some("Ubuntu 22.04 LTS"));
778        assert_eq!(output.architecture(), Some("x86_64"));
779        assert!(output.has_warnings());
780        assert_eq!(output.warnings().len(), 2);
781
782        let (total, running, images) = output.resource_summary();
783        assert_eq!(total, 15);
784        assert_eq!(running, 5);
785        assert_eq!(images, 30);
786    }
787
788    #[test]
789    fn test_info_output_no_data() {
790        let output = InfoOutput {
791            output: CommandOutput {
792                stdout: String::new(),
793                stderr: String::new(),
794                exit_code: 0,
795                success: true,
796            },
797            docker_info: None,
798        };
799
800        assert_eq!(output.server_version(), None);
801        assert_eq!(output.storage_driver(), None);
802        assert_eq!(output.container_count(), 0);
803        assert_eq!(output.running_containers(), 0);
804        assert_eq!(output.image_count(), 0);
805        assert!(!output.is_debug());
806        assert!(!output.is_experimental());
807        assert!(!output.has_warnings());
808        assert_eq!(output.warnings().len(), 0);
809
810        let (total, running, images) = output.resource_summary();
811        assert_eq!(total, 0);
812        assert_eq!(running, 0);
813        assert_eq!(images, 0);
814    }
815
816    #[test]
817    fn test_info_command_display() {
818        let info = InfoCommand::new().format("{{.ServerVersion}}");
819
820        let display = format!("{info}");
821        assert_eq!(display, "docker info --format {{.ServerVersion}}");
822    }
823
824    #[test]
825    fn test_info_command_display_no_format() {
826        let info = InfoCommand::new();
827
828        let display = format!("{info}");
829        assert_eq!(display, "docker info");
830    }
831
832    #[test]
833    fn test_info_command_name() {
834        let info = InfoCommand::new();
835        let args = info.build_command_args();
836        assert_eq!(args[0], "info");
837    }
838
839    #[test]
840    fn test_info_command_extensibility() {
841        let mut info = InfoCommand::new();
842
843        // Test that we can add custom raw arguments
844        info.get_executor_mut()
845            .raw_args
846            .push("--verbose".to_string());
847        info.get_executor_mut()
848            .raw_args
849            .push("--some-flag".to_string());
850
851        let args = info.build_command_args();
852
853        // Verify raw args are included
854        assert!(args.contains(&"--verbose".to_string()));
855        assert!(args.contains(&"--some-flag".to_string()));
856    }
857
858    #[test]
859    fn test_parse_json_output_concept() {
860        // This test demonstrates the concept of parsing JSON output
861        let json_output = r#"{"ServerVersion":"20.10.17","Driver":"overlay2","Containers":5}"#;
862
863        let output = CommandOutput {
864            stdout: json_output.to_string(),
865            stderr: String::new(),
866            exit_code: 0,
867            success: true,
868        };
869
870        let result = InfoCommand::parse_json_output(&output);
871
872        // The actual parsing would need real Docker JSON output
873        assert!(result.is_ok());
874    }
875
876    #[test]
877    fn test_parse_table_output_concept() {
878        // This test demonstrates the concept of parsing table output
879        let table_output = "Server Version: 20.10.17\nStorage Driver: overlay2\nContainers: 5\nRunning: 2\nImages: 10\nWARNING: Test warning";
880
881        let output = CommandOutput {
882            stdout: table_output.to_string(),
883            stderr: String::new(),
884            exit_code: 0,
885            success: true,
886        };
887
888        let result = InfoCommand::parse_table_output(&output);
889
890        // The actual parsing would need real Docker table output
891        assert!(result.is_some() || result.is_none());
892    }
893}