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