docker_wrapper/command/images.rs
1//! Docker Images Command Implementation
2//!
3//! This module provides a comprehensive implementation of the `docker images` command,
4//! supporting all native Docker images options for listing local images.
5//!
6//! # Examples
7//!
8//! ## Basic Usage
9//!
10//! ```no_run
11//! use docker_wrapper::ImagesCommand;
12//! use docker_wrapper::DockerCommand;
13//!
14//! #[tokio::main]
15//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
16//! // List all images
17//! let images_cmd = ImagesCommand::new();
18//! let output = images_cmd.execute().await?;
19//! println!("Images listed: {}", output.success());
20//! Ok(())
21//! }
22//! ```
23//!
24//! ## Advanced Usage
25//!
26//! ```no_run
27//! use docker_wrapper::ImagesCommand;
28//! use docker_wrapper::DockerCommand;
29//!
30//! #[tokio::main]
31//! async fn main() -> Result<(), Box<dyn std::error::Error>> {
32//! // List images with filtering and JSON format
33//! let images_cmd = ImagesCommand::new()
34//! .repository("nginx")
35//! .all()
36//! .filter("dangling=false")
37//! .format_json()
38//! .digests()
39//! .no_trunc();
40//!
41//! let output = images_cmd.execute().await?;
42//! println!("Filtered images: {}", output.success());
43//! Ok(())
44//! }
45//! ```
46
47use super::{CommandExecutor, CommandOutput, DockerCommand};
48use crate::error::Result;
49use async_trait::async_trait;
50use serde_json::Value;
51
52/// Docker Images Command Builder
53///
54/// Implements the `docker images` command for listing local Docker images.
55///
56/// # Docker Images Overview
57///
58/// The images command lists Docker images stored locally on the system. It supports:
59/// - Repository and tag filtering
60/// - Multiple output formats (table, JSON, custom templates)
61/// - Image metadata display (digests, sizes, creation dates)
62/// - Advanced filtering by various criteria
63/// - Quiet mode for scripts
64///
65/// # Image Information
66///
67/// Each image entry typically includes:
68/// - Repository name
69/// - Tag
70/// - Image ID
71/// - Creation date
72/// - Size
73/// - Optionally: digests, intermediate layers
74///
75/// # Examples
76///
77/// ```no_run
78/// use docker_wrapper::ImagesCommand;
79/// use docker_wrapper::DockerCommand;
80///
81/// #[tokio::main]
82/// async fn main() -> Result<(), Box<dyn std::error::Error>> {
83/// // List all nginx images
84/// let output = ImagesCommand::new()
85/// .repository("nginx")
86/// .execute()
87/// .await?;
88///
89/// println!("Images success: {}", output.success());
90/// Ok(())
91/// }
92/// ```
93#[derive(Debug, Clone)]
94#[allow(clippy::struct_excessive_bools)]
95pub struct ImagesCommand {
96 /// Optional repository filter (e.g., "nginx", "nginx:alpine")
97 repository: Option<String>,
98 /// Show all images (including intermediate images)
99 all: bool,
100 /// Show digests
101 digests: bool,
102 /// Filter output based on conditions
103 filters: Vec<String>,
104 /// Output format
105 format: Option<String>,
106 /// Don't truncate output
107 no_trunc: bool,
108 /// Only show image IDs
109 quiet: bool,
110 /// List multi-platform images as a tree (experimental)
111 tree: bool,
112 /// Command executor for handling raw arguments and execution
113 pub executor: CommandExecutor,
114}
115
116/// Represents a Docker image from the output
117#[derive(Debug, Clone, PartialEq)]
118pub struct ImageInfo {
119 /// Repository name
120 pub repository: String,
121 /// Tag
122 pub tag: String,
123 /// Image ID
124 pub image_id: String,
125 /// Creation date/time
126 pub created: String,
127 /// Image size
128 pub size: String,
129 /// Digest (if available)
130 pub digest: Option<String>,
131}
132
133/// Output from the images command with parsed image information
134#[derive(Debug, Clone)]
135pub struct ImagesOutput {
136 /// Raw command output
137 pub output: CommandOutput,
138 /// Parsed image information (if output is parseable)
139 pub images: Vec<ImageInfo>,
140}
141
142impl ImagesCommand {
143 /// Create a new `ImagesCommand` instance
144 ///
145 /// # Examples
146 ///
147 /// ```
148 /// use docker_wrapper::ImagesCommand;
149 ///
150 /// let images_cmd = ImagesCommand::new();
151 /// ```
152 #[must_use]
153 pub fn new() -> Self {
154 Self {
155 repository: None,
156 all: false,
157 digests: false,
158 filters: Vec::new(),
159 format: None,
160 no_trunc: false,
161 quiet: false,
162 tree: false,
163 executor: CommandExecutor::new(),
164 }
165 }
166
167 /// Filter images by repository name (and optionally tag)
168 ///
169 /// # Arguments
170 ///
171 /// * `repository` - Repository name (e.g., "nginx", "nginx:alpine", "ubuntu:20.04")
172 ///
173 /// # Examples
174 ///
175 /// ```
176 /// use docker_wrapper::ImagesCommand;
177 ///
178 /// let images_cmd = ImagesCommand::new()
179 /// .repository("nginx:alpine");
180 /// ```
181 #[must_use]
182 pub fn repository<S: Into<String>>(mut self, repository: S) -> Self {
183 self.repository = Some(repository.into());
184 self
185 }
186
187 /// Show all images (including intermediate images)
188 ///
189 /// By default, Docker hides intermediate images. This option shows them all.
190 ///
191 /// # Examples
192 ///
193 /// ```
194 /// use docker_wrapper::ImagesCommand;
195 ///
196 /// let images_cmd = ImagesCommand::new()
197 /// .all();
198 /// ```
199 #[must_use]
200 pub fn all(mut self) -> Self {
201 self.all = true;
202 self
203 }
204
205 /// Show digests
206 ///
207 /// Displays the digest (SHA256 hash) for each image.
208 ///
209 /// # Examples
210 ///
211 /// ```
212 /// use docker_wrapper::ImagesCommand;
213 ///
214 /// let images_cmd = ImagesCommand::new()
215 /// .digests();
216 /// ```
217 #[must_use]
218 pub fn digests(mut self) -> Self {
219 self.digests = true;
220 self
221 }
222
223 /// Add a filter condition
224 ///
225 /// Common filters:
226 /// - `dangling=true|false` - Show dangling images
227 /// - `label=<key>` or `label=<key>=<value>` - Filter by label
228 /// - `before=<image>` - Images created before this image
229 /// - `since=<image>` - Images created since this image
230 /// - `reference=<pattern>` - Filter by repository name pattern
231 ///
232 /// # Examples
233 ///
234 /// ```
235 /// use docker_wrapper::ImagesCommand;
236 ///
237 /// let images_cmd = ImagesCommand::new()
238 /// .filter("dangling=true")
239 /// .filter("label=maintainer=nginx");
240 /// ```
241 #[must_use]
242 pub fn filter<S: Into<String>>(mut self, filter: S) -> Self {
243 self.filters.push(filter.into());
244 self
245 }
246
247 /// Add multiple filter conditions
248 ///
249 /// # Examples
250 ///
251 /// ```
252 /// use docker_wrapper::ImagesCommand;
253 ///
254 /// let images_cmd = ImagesCommand::new()
255 /// .filters(vec!["dangling=false", "label=version=latest"]);
256 /// ```
257 #[must_use]
258 pub fn filters<I, S>(mut self, filters: I) -> Self
259 where
260 I: IntoIterator<Item = S>,
261 S: Into<String>,
262 {
263 self.filters
264 .extend(filters.into_iter().map(std::convert::Into::into));
265 self
266 }
267
268 /// Set custom output format
269 ///
270 /// # Examples
271 ///
272 /// ```
273 /// use docker_wrapper::ImagesCommand;
274 ///
275 /// let images_cmd = ImagesCommand::new()
276 /// .format("table {{.Repository}}:{{.Tag}}\t{{.Size}}");
277 /// ```
278 #[must_use]
279 pub fn format<S: Into<String>>(mut self, format: S) -> Self {
280 self.format = Some(format.into());
281 self
282 }
283
284 /// Format output as table (default)
285 ///
286 /// # Examples
287 ///
288 /// ```
289 /// use docker_wrapper::ImagesCommand;
290 ///
291 /// let images_cmd = ImagesCommand::new()
292 /// .format_table();
293 /// ```
294 #[must_use]
295 pub fn format_table(mut self) -> Self {
296 self.format = Some("table".to_string());
297 self
298 }
299
300 /// Format output as JSON
301 ///
302 /// # Examples
303 ///
304 /// ```
305 /// use docker_wrapper::ImagesCommand;
306 ///
307 /// let images_cmd = ImagesCommand::new()
308 /// .format_json();
309 /// ```
310 #[must_use]
311 pub fn format_json(mut self) -> Self {
312 self.format = Some("json".to_string());
313 self
314 }
315
316 /// Don't truncate output
317 ///
318 /// By default, Docker truncates long values. This shows full values.
319 ///
320 /// # Examples
321 ///
322 /// ```
323 /// use docker_wrapper::ImagesCommand;
324 ///
325 /// let images_cmd = ImagesCommand::new()
326 /// .no_trunc();
327 /// ```
328 #[must_use]
329 pub fn no_trunc(mut self) -> Self {
330 self.no_trunc = true;
331 self
332 }
333
334 /// Only show image IDs
335 ///
336 /// Useful for scripting and automation.
337 ///
338 /// # Examples
339 ///
340 /// ```
341 /// use docker_wrapper::ImagesCommand;
342 ///
343 /// let images_cmd = ImagesCommand::new()
344 /// .quiet();
345 /// ```
346 #[must_use]
347 pub fn quiet(mut self) -> Self {
348 self.quiet = true;
349 self
350 }
351
352 /// List multi-platform images as a tree (experimental)
353 ///
354 /// This is an experimental Docker feature for displaying multi-platform images.
355 ///
356 /// # Examples
357 ///
358 /// ```
359 /// use docker_wrapper::ImagesCommand;
360 ///
361 /// let images_cmd = ImagesCommand::new()
362 /// .tree();
363 /// ```
364 #[must_use]
365 pub fn tree(mut self) -> Self {
366 self.tree = true;
367 self
368 }
369
370 /// Build the command arguments
371 ///
372 /// This method constructs the complete argument list for the docker images command.
373 fn build_command_args(&self) -> Vec<String> {
374 let mut args = Vec::new();
375
376 // Add all flag
377 if self.all {
378 args.push("--all".to_string());
379 }
380
381 // Add digests flag
382 if self.digests {
383 args.push("--digests".to_string());
384 }
385
386 // Add filters
387 for filter in &self.filters {
388 args.push("--filter".to_string());
389 args.push(filter.clone());
390 }
391
392 // Add format
393 if let Some(ref format) = self.format {
394 args.push("--format".to_string());
395 args.push(format.clone());
396 }
397
398 // Add no-trunc flag
399 if self.no_trunc {
400 args.push("--no-trunc".to_string());
401 }
402
403 // Add quiet flag
404 if self.quiet {
405 args.push("--quiet".to_string());
406 }
407
408 // Add tree flag
409 if self.tree {
410 args.push("--tree".to_string());
411 }
412
413 // Add repository filter (must be last)
414 if let Some(ref repository) = self.repository {
415 args.push(repository.clone());
416 }
417
418 args
419 }
420
421 /// Parse the output to extract image information
422 ///
423 /// This attempts to parse the docker images output into structured data.
424 fn parse_output(&self, output: &CommandOutput) -> Vec<ImageInfo> {
425 if self.quiet {
426 // In quiet mode, output is just image IDs
427 return output
428 .stdout
429 .lines()
430 .filter(|line| !line.trim().is_empty())
431 .map(|line| ImageInfo {
432 repository: "<unknown>".to_string(),
433 tag: "<unknown>".to_string(),
434 image_id: line.trim().to_string(),
435 created: "<unknown>".to_string(),
436 size: "<unknown>".to_string(),
437 digest: None,
438 })
439 .collect();
440 }
441
442 if let Some(ref format) = self.format {
443 if format == "json" {
444 return Self::parse_json_output(&output.stdout);
445 }
446 }
447
448 // Parse table format (default)
449 self.parse_table_output(&output.stdout)
450 }
451
452 /// Parse JSON format output
453 fn parse_json_output(stdout: &str) -> Vec<ImageInfo> {
454 let mut images = Vec::new();
455
456 for line in stdout.lines() {
457 if let Ok(json) = serde_json::from_str::<Value>(line) {
458 if let Some(obj) = json.as_object() {
459 let repository = obj
460 .get("Repository")
461 .and_then(|v| v.as_str())
462 .unwrap_or("<none>")
463 .to_string();
464 let tag = obj
465 .get("Tag")
466 .and_then(|v| v.as_str())
467 .unwrap_or("<none>")
468 .to_string();
469 let image_id = obj
470 .get("ID")
471 .and_then(|v| v.as_str())
472 .unwrap_or("")
473 .to_string();
474 let created = obj
475 .get("CreatedAt")
476 .and_then(|v| v.as_str())
477 .unwrap_or("")
478 .to_string();
479 let size = obj
480 .get("Size")
481 .and_then(|v| v.as_str())
482 .unwrap_or("")
483 .to_string();
484 let digest = obj.get("Digest").and_then(|v| v.as_str()).map(String::from);
485
486 images.push(ImageInfo {
487 repository,
488 tag,
489 image_id,
490 created,
491 size,
492 digest,
493 });
494 }
495 }
496 }
497
498 images
499 }
500
501 /// Parse table format output
502 fn parse_table_output(&self, stdout: &str) -> Vec<ImageInfo> {
503 let mut images = Vec::new();
504 let lines: Vec<&str> = stdout.lines().collect();
505
506 if lines.is_empty() {
507 return images;
508 }
509
510 // Skip header line if present
511 let data_lines = if lines[0].starts_with("REPOSITORY") {
512 &lines[1..]
513 } else {
514 &lines[..]
515 };
516
517 for line in data_lines {
518 if line.trim().is_empty() {
519 continue;
520 }
521
522 // Split by whitespace, but handle multi-word fields like "2 days ago"
523 let parts: Vec<&str> = line.split_whitespace().collect();
524 if parts.len() >= 5 {
525 let repository = parts[0].to_string();
526 let tag = parts[1].to_string();
527 let image_id = parts[2].to_string();
528
529 // Handle multi-word created field and size
530 let (created, size, digest) = if self.digests && parts.len() >= 7 {
531 // With digests: REPO TAG IMAGE_ID DIGEST CREATED... SIZE
532 let digest = Some(parts[3].to_string());
533 let created_parts = &parts[4..parts.len() - 1];
534 let created = created_parts.join(" ");
535 let size = parts[parts.len() - 1].to_string();
536 (created, size, digest)
537 } else if parts.len() >= 5 {
538 // Without digests: REPO TAG IMAGE_ID CREATED... SIZE
539 let created_parts = &parts[3..parts.len() - 1];
540 let created = created_parts.join(" ");
541 let size = parts[parts.len() - 1].to_string();
542 (created, size, None)
543 } else {
544 (String::new(), String::new(), None)
545 };
546
547 images.push(ImageInfo {
548 repository,
549 tag,
550 image_id,
551 created,
552 size,
553 digest,
554 });
555 }
556 }
557
558 images
559 }
560
561 /// Get the repository filter if set
562 ///
563 /// # Examples
564 ///
565 /// ```
566 /// use docker_wrapper::ImagesCommand;
567 ///
568 /// let images_cmd = ImagesCommand::new().repository("nginx");
569 /// assert_eq!(images_cmd.get_repository(), Some("nginx"));
570 /// ```
571 #[must_use]
572 pub fn get_repository(&self) -> Option<&str> {
573 self.repository.as_deref()
574 }
575
576 /// Check if showing all images
577 ///
578 /// # Examples
579 ///
580 /// ```
581 /// use docker_wrapper::ImagesCommand;
582 ///
583 /// let images_cmd = ImagesCommand::new().all();
584 /// assert!(images_cmd.is_all());
585 /// ```
586 #[must_use]
587 pub fn is_all(&self) -> bool {
588 self.all
589 }
590
591 /// Check if showing digests
592 ///
593 /// # Examples
594 ///
595 /// ```
596 /// use docker_wrapper::ImagesCommand;
597 ///
598 /// let images_cmd = ImagesCommand::new().digests();
599 /// assert!(images_cmd.is_digests());
600 /// ```
601 #[must_use]
602 pub fn is_digests(&self) -> bool {
603 self.digests
604 }
605
606 /// Check if quiet mode is enabled
607 ///
608 /// # Examples
609 ///
610 /// ```
611 /// use docker_wrapper::ImagesCommand;
612 ///
613 /// let images_cmd = ImagesCommand::new().quiet();
614 /// assert!(images_cmd.is_quiet());
615 /// ```
616 #[must_use]
617 pub fn is_quiet(&self) -> bool {
618 self.quiet
619 }
620
621 /// Check if no-trunc is enabled
622 ///
623 /// # Examples
624 ///
625 /// ```
626 /// use docker_wrapper::ImagesCommand;
627 ///
628 /// let images_cmd = ImagesCommand::new().no_trunc();
629 /// assert!(images_cmd.is_no_trunc());
630 /// ```
631 #[must_use]
632 pub fn is_no_trunc(&self) -> bool {
633 self.no_trunc
634 }
635
636 /// Check if tree mode is enabled
637 ///
638 /// # Examples
639 ///
640 /// ```
641 /// use docker_wrapper::ImagesCommand;
642 ///
643 /// let images_cmd = ImagesCommand::new().tree();
644 /// assert!(images_cmd.is_tree());
645 /// ```
646 #[must_use]
647 pub fn is_tree(&self) -> bool {
648 self.tree
649 }
650
651 /// Get the current filters
652 ///
653 /// # Examples
654 ///
655 /// ```
656 /// use docker_wrapper::ImagesCommand;
657 ///
658 /// let images_cmd = ImagesCommand::new()
659 /// .filter("dangling=true");
660 /// assert_eq!(images_cmd.get_filters(), &["dangling=true"]);
661 /// ```
662 #[must_use]
663 pub fn get_filters(&self) -> &[String] {
664 &self.filters
665 }
666
667 /// Get the format if set
668 ///
669 /// # Examples
670 ///
671 /// ```
672 /// use docker_wrapper::ImagesCommand;
673 ///
674 /// let images_cmd = ImagesCommand::new().format_json();
675 /// assert_eq!(images_cmd.get_format(), Some("json"));
676 /// ```
677 #[must_use]
678 pub fn get_format(&self) -> Option<&str> {
679 self.format.as_deref()
680 }
681
682 /// Get a reference to the command executor
683 #[must_use]
684 pub fn get_executor(&self) -> &CommandExecutor {
685 &self.executor
686 }
687
688 /// Get a mutable reference to the command executor
689 #[must_use]
690 pub fn get_executor_mut(&mut self) -> &mut CommandExecutor {
691 &mut self.executor
692 }
693}
694
695impl Default for ImagesCommand {
696 fn default() -> Self {
697 Self::new()
698 }
699}
700
701impl ImagesOutput {
702 /// Check if the command was successful
703 ///
704 /// # Examples
705 ///
706 /// ```no_run
707 /// # use docker_wrapper::ImagesCommand;
708 /// # use docker_wrapper::DockerCommand;
709 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
710 /// let output = ImagesCommand::new().execute().await?;
711 /// if output.success() {
712 /// println!("Images listed successfully");
713 /// }
714 /// # Ok(())
715 /// # }
716 /// ```
717 #[must_use]
718 pub fn success(&self) -> bool {
719 self.output.success
720 }
721
722 /// Get the number of images found
723 ///
724 /// # Examples
725 ///
726 /// ```no_run
727 /// # use docker_wrapper::ImagesCommand;
728 /// # use docker_wrapper::DockerCommand;
729 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
730 /// let output = ImagesCommand::new().execute().await?;
731 /// println!("Found {} images", output.image_count());
732 /// # Ok(())
733 /// # }
734 /// ```
735 #[must_use]
736 pub fn image_count(&self) -> usize {
737 self.images.len()
738 }
739
740 /// Get image IDs only
741 ///
742 /// # Examples
743 ///
744 /// ```no_run
745 /// # use docker_wrapper::ImagesCommand;
746 /// # use docker_wrapper::DockerCommand;
747 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
748 /// let output = ImagesCommand::new().execute().await?;
749 /// let ids = output.image_ids();
750 /// println!("Image IDs: {:?}", ids);
751 /// # Ok(())
752 /// # }
753 /// ```
754 #[must_use]
755 pub fn image_ids(&self) -> Vec<&str> {
756 self.images
757 .iter()
758 .map(|img| img.image_id.as_str())
759 .collect()
760 }
761
762 /// Filter images by repository name
763 ///
764 /// # Examples
765 ///
766 /// ```no_run
767 /// # use docker_wrapper::ImagesCommand;
768 /// # use docker_wrapper::DockerCommand;
769 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
770 /// let output = ImagesCommand::new().execute().await?;
771 /// let nginx_images = output.filter_by_repository("nginx");
772 /// println!("Nginx images: {}", nginx_images.len());
773 /// # Ok(())
774 /// # }
775 /// ```
776 #[must_use]
777 pub fn filter_by_repository(&self, repository: &str) -> Vec<&ImageInfo> {
778 self.images
779 .iter()
780 .filter(|img| img.repository == repository)
781 .collect()
782 }
783
784 /// Check if output is empty (no images)
785 ///
786 /// # Examples
787 ///
788 /// ```no_run
789 /// # use docker_wrapper::ImagesCommand;
790 /// # use docker_wrapper::DockerCommand;
791 /// # async fn example() -> Result<(), Box<dyn std::error::Error>> {
792 /// let output = ImagesCommand::new().execute().await?;
793 /// if output.is_empty() {
794 /// println!("No images found");
795 /// }
796 /// # Ok(())
797 /// # }
798 /// ```
799 #[must_use]
800 pub fn is_empty(&self) -> bool {
801 self.images.is_empty()
802 }
803}
804
805#[async_trait]
806impl DockerCommand for ImagesCommand {
807 type Output = ImagesOutput;
808
809 fn get_executor(&self) -> &CommandExecutor {
810 &self.executor
811 }
812
813 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
814 &mut self.executor
815 }
816
817 fn build_command_args(&self) -> Vec<String> {
818 self.build_command_args()
819 }
820
821 async fn execute(&self) -> Result<Self::Output> {
822 let args = self.build_command_args();
823 let output = self.executor.execute_command("docker", args).await?;
824
825 let images = self.parse_output(&output);
826
827 Ok(ImagesOutput { output, images })
828 }
829}
830
831#[cfg(test)]
832mod tests {
833 use super::*;
834
835 #[test]
836 fn test_images_command_basic() {
837 let images_cmd = ImagesCommand::new();
838 let args = images_cmd.build_command_args();
839
840 assert!(args.is_empty()); // No arguments for basic images command
841 assert!(!images_cmd.is_all());
842 assert!(!images_cmd.is_digests());
843 assert!(!images_cmd.is_quiet());
844 assert!(!images_cmd.is_no_trunc());
845 assert!(!images_cmd.is_tree());
846 assert_eq!(images_cmd.get_repository(), None);
847 assert_eq!(images_cmd.get_format(), None);
848 assert!(images_cmd.get_filters().is_empty());
849 }
850
851 #[test]
852 fn test_images_command_with_repository() {
853 let images_cmd = ImagesCommand::new().repository("nginx:alpine");
854 let args = images_cmd.build_command_args();
855
856 assert!(args.contains(&"nginx:alpine".to_string()));
857 assert_eq!(args.last(), Some(&"nginx:alpine".to_string()));
858 assert_eq!(images_cmd.get_repository(), Some("nginx:alpine"));
859 }
860
861 #[test]
862 fn test_images_command_with_all_flags() {
863 let images_cmd = ImagesCommand::new()
864 .all()
865 .digests()
866 .no_trunc()
867 .quiet()
868 .tree();
869
870 let args = images_cmd.build_command_args();
871
872 assert!(args.contains(&"--all".to_string()));
873 assert!(args.contains(&"--digests".to_string()));
874 assert!(args.contains(&"--no-trunc".to_string()));
875 assert!(args.contains(&"--quiet".to_string()));
876 assert!(args.contains(&"--tree".to_string()));
877
878 assert!(images_cmd.is_all());
879 assert!(images_cmd.is_digests());
880 assert!(images_cmd.is_no_trunc());
881 assert!(images_cmd.is_quiet());
882 assert!(images_cmd.is_tree());
883 }
884
885 #[test]
886 fn test_images_command_with_filters() {
887 let images_cmd = ImagesCommand::new()
888 .filter("dangling=true")
889 .filter("label=maintainer=nginx")
890 .filters(vec!["before=alpine:latest", "since=ubuntu:20.04"]);
891
892 let args = images_cmd.build_command_args();
893
894 assert!(args.contains(&"--filter".to_string()));
895 assert!(args.contains(&"dangling=true".to_string()));
896 assert!(args.contains(&"label=maintainer=nginx".to_string()));
897 assert!(args.contains(&"before=alpine:latest".to_string()));
898 assert!(args.contains(&"since=ubuntu:20.04".to_string()));
899
900 let filters = images_cmd.get_filters();
901 assert_eq!(filters.len(), 4);
902 assert!(filters.contains(&"dangling=true".to_string()));
903 }
904
905 #[test]
906 fn test_images_command_with_format() {
907 let images_cmd = ImagesCommand::new().format_json();
908 let args = images_cmd.build_command_args();
909
910 assert!(args.contains(&"--format".to_string()));
911 assert!(args.contains(&"json".to_string()));
912 assert_eq!(images_cmd.get_format(), Some("json"));
913 }
914
915 #[test]
916 fn test_images_command_custom_format() {
917 let custom_format = "table {{.Repository}}:{{.Tag}}\t{{.Size}}";
918 let images_cmd = ImagesCommand::new().format(custom_format);
919 let args = images_cmd.build_command_args();
920
921 assert!(args.contains(&"--format".to_string()));
922 assert!(args.contains(&custom_format.to_string()));
923 assert_eq!(images_cmd.get_format(), Some(custom_format));
924 }
925
926 #[test]
927 fn test_images_command_all_options() {
928 let images_cmd = ImagesCommand::new()
929 .repository("ubuntu")
930 .all()
931 .digests()
932 .filter("dangling=false")
933 .format_table()
934 .no_trunc()
935 .quiet();
936
937 let args = images_cmd.build_command_args();
938
939 // Repository should be last
940 assert_eq!(args.last(), Some(&"ubuntu".to_string()));
941
942 // All options should be present
943 assert!(args.contains(&"--all".to_string()));
944 assert!(args.contains(&"--digests".to_string()));
945 assert!(args.contains(&"--filter".to_string()));
946 assert!(args.contains(&"dangling=false".to_string()));
947 assert!(args.contains(&"--format".to_string()));
948 assert!(args.contains(&"table".to_string()));
949 assert!(args.contains(&"--no-trunc".to_string()));
950 assert!(args.contains(&"--quiet".to_string()));
951
952 // Verify helper methods
953 assert_eq!(images_cmd.get_repository(), Some("ubuntu"));
954 assert!(images_cmd.is_all());
955 assert!(images_cmd.is_digests());
956 assert!(images_cmd.is_no_trunc());
957 assert!(images_cmd.is_quiet());
958 assert_eq!(images_cmd.get_format(), Some("table"));
959 assert_eq!(images_cmd.get_filters(), &["dangling=false"]);
960 }
961
962 #[test]
963 fn test_images_command_default() {
964 let images_cmd = ImagesCommand::default();
965 assert_eq!(images_cmd.get_repository(), None);
966 assert!(!images_cmd.is_all());
967 }
968
969 #[test]
970 fn test_image_info_creation() {
971 let image = ImageInfo {
972 repository: "nginx".to_string(),
973 tag: "alpine".to_string(),
974 image_id: "abc123456789".to_string(),
975 created: "2 days ago".to_string(),
976 size: "16.1MB".to_string(),
977 digest: Some("sha256:abc123".to_string()),
978 };
979
980 assert_eq!(image.repository, "nginx");
981 assert_eq!(image.tag, "alpine");
982 assert_eq!(image.image_id, "abc123456789");
983 assert_eq!(image.digest, Some("sha256:abc123".to_string()));
984 }
985
986 #[test]
987 fn test_parse_json_output() {
988 let json_output = r#"{"Containers":"N/A","CreatedAt":"2023-01-01T00:00:00Z","CreatedSince":"2 days ago","Digest":"sha256:abc123","ID":"sha256:def456","Repository":"nginx","SharedSize":"N/A","Size":"16.1MB","Tag":"alpine","UniqueSize":"N/A","VirtualSize":"16.1MB"}
989{"Containers":"N/A","CreatedAt":"2023-01-02T00:00:00Z","CreatedSince":"1 day ago","Digest":"sha256:xyz789","ID":"sha256:ghi012","Repository":"ubuntu","SharedSize":"N/A","Size":"72.8MB","Tag":"20.04","UniqueSize":"N/A","VirtualSize":"72.8MB"}"#;
990
991 let images = ImagesCommand::parse_json_output(json_output);
992
993 assert_eq!(images.len(), 2);
994 assert_eq!(images[0].repository, "nginx");
995 assert_eq!(images[0].tag, "alpine");
996 assert_eq!(images[0].image_id, "sha256:def456");
997 assert_eq!(images[0].size, "16.1MB");
998 assert_eq!(images[0].digest, Some("sha256:abc123".to_string()));
999
1000 assert_eq!(images[1].repository, "ubuntu");
1001 assert_eq!(images[1].tag, "20.04");
1002 }
1003
1004 #[test]
1005 fn test_parse_table_output() {
1006 let images_cmd = ImagesCommand::new();
1007 let table_output = r"REPOSITORY TAG IMAGE ID CREATED SIZE
1008nginx alpine abc123456789 2 days ago 16.1MB
1009ubuntu 20.04 def456789012 1 day ago 72.8MB
1010<none> <none> ghi789012345 3 hours ago 5.59MB";
1011
1012 let images = images_cmd.parse_table_output(table_output);
1013
1014 assert_eq!(images.len(), 3);
1015 assert_eq!(images[0].repository, "nginx");
1016 assert_eq!(images[0].tag, "alpine");
1017 assert_eq!(images[0].image_id, "abc123456789");
1018 assert_eq!(images[0].created, "2 days ago");
1019 assert_eq!(images[0].size, "16.1MB");
1020
1021 assert_eq!(images[1].repository, "ubuntu");
1022 assert_eq!(images[1].tag, "20.04");
1023 }
1024
1025 #[test]
1026 fn test_parse_quiet_output() {
1027 let images_cmd = ImagesCommand::new().quiet();
1028 let quiet_output = "abc123456789\ndef456789012\nghi789012345";
1029
1030 let images = images_cmd.parse_output(&CommandOutput {
1031 stdout: quiet_output.to_string(),
1032 stderr: String::new(),
1033 exit_code: 0,
1034 success: true,
1035 });
1036
1037 assert_eq!(images.len(), 3);
1038 assert_eq!(images[0].image_id, "abc123456789");
1039 assert_eq!(images[0].repository, "<unknown>");
1040 assert_eq!(images[1].image_id, "def456789012");
1041 assert_eq!(images[2].image_id, "ghi789012345");
1042 }
1043
1044 #[test]
1045 fn test_images_output_helpers() {
1046 let output = ImagesOutput {
1047 output: CommandOutput {
1048 stdout: "test".to_string(),
1049 stderr: String::new(),
1050 exit_code: 0,
1051 success: true,
1052 },
1053 images: vec![
1054 ImageInfo {
1055 repository: "nginx".to_string(),
1056 tag: "alpine".to_string(),
1057 image_id: "abc123".to_string(),
1058 created: "2 days ago".to_string(),
1059 size: "16.1MB".to_string(),
1060 digest: None,
1061 },
1062 ImageInfo {
1063 repository: "nginx".to_string(),
1064 tag: "latest".to_string(),
1065 image_id: "def456".to_string(),
1066 created: "1 day ago".to_string(),
1067 size: "133MB".to_string(),
1068 digest: None,
1069 },
1070 ImageInfo {
1071 repository: "ubuntu".to_string(),
1072 tag: "20.04".to_string(),
1073 image_id: "ghi789".to_string(),
1074 created: "3 days ago".to_string(),
1075 size: "72.8MB".to_string(),
1076 digest: None,
1077 },
1078 ],
1079 };
1080
1081 assert!(output.success());
1082 assert_eq!(output.image_count(), 3);
1083 assert!(!output.is_empty());
1084
1085 let ids = output.image_ids();
1086 assert_eq!(ids, vec!["abc123", "def456", "ghi789"]);
1087
1088 let nginx_images = output.filter_by_repository("nginx");
1089 assert_eq!(nginx_images.len(), 2);
1090 assert_eq!(nginx_images[0].tag, "alpine");
1091 assert_eq!(nginx_images[1].tag, "latest");
1092 }
1093
1094 #[test]
1095 fn test_images_command_extensibility() {
1096 let mut images_cmd = ImagesCommand::new();
1097 images_cmd
1098 .arg("--experimental")
1099 .args(vec!["--custom", "value"]);
1100
1101 // Extensibility is handled through the executor's raw_args
1102 // The actual testing of raw args is done in command.rs tests
1103 // We can't access private fields, but we know the methods work
1104 println!("Extensibility methods called successfully");
1105 }
1106}