cli_denoiser/filters/
docker.rs1use regex::Regex;
2use std::sync::LazyLock;
3
4use super::{Filter, FilterResult};
5
6static DOCKER_LAYER_CACHE: LazyLock<Regex> = LazyLock::new(|| {
7 Regex::new(r"^\s*(?:--->\s+[a-f0-9]{12}|---> Using cache|Removing intermediate container)")
8 .expect("docker layer regex valid")
9});
10
11static DOCKER_PULL_PROGRESS: LazyLock<Regex> = LazyLock::new(|| {
12 Regex::new(
13 r"^[a-f0-9]{12}:\s+(?:Pulling|Waiting|Downloading|Extracting|Verifying|Pull complete)",
14 )
15 .expect("docker pull regex valid")
16});
17
18static DOCKER_DIGEST: LazyLock<Regex> = LazyLock::new(|| {
19 Regex::new(r"^(?:Digest:\s+sha256:|Status:\s+Downloaded newer image)")
20 .expect("docker digest regex valid")
21});
22
23pub struct DockerFilter;
31
32impl Filter for DockerFilter {
33 fn name(&self) -> &'static str {
34 "docker"
35 }
36
37 fn filter_line(&self, line: &str) -> FilterResult {
38 let trimmed = line.trim();
39
40 if trimmed.is_empty() {
41 return FilterResult::Keep;
42 }
43
44 if DOCKER_LAYER_CACHE.is_match(trimmed) {
46 return FilterResult::Drop;
47 }
48
49 if DOCKER_PULL_PROGRESS.is_match(trimmed) {
51 return FilterResult::Drop;
52 }
53
54 if DOCKER_DIGEST.is_match(trimmed) {
56 return FilterResult::Drop;
57 }
58
59 FilterResult::Keep
60 }
61
62 fn filter_block(&self, lines: &[String]) -> Vec<String> {
63 let mut result = Vec::with_capacity(lines.len());
64 let mut pull_count: usize = 0;
65 let mut cache_count: usize = 0;
66
67 for line in lines {
68 let trimmed = line.trim();
69
70 if DOCKER_PULL_PROGRESS.is_match(trimmed) || DOCKER_DIGEST.is_match(trimmed) {
71 pull_count += 1;
72 continue;
73 }
74
75 if DOCKER_LAYER_CACHE.is_match(trimmed) {
76 cache_count += 1;
77 continue;
78 }
79
80 emit_summaries(&mut result, &mut pull_count, &mut cache_count);
81 result.push(line.clone());
82 }
83
84 emit_summaries(&mut result, &mut pull_count, &mut cache_count);
85 result
86 }
87}
88
89fn emit_summaries(result: &mut Vec<String>, pull_count: &mut usize, cache_count: &mut usize) {
90 if *pull_count > 0 {
91 result.push(format!("[pulled {pull_count} layers]"));
92 *pull_count = 0;
93 }
94 if *cache_count > 0 {
95 result.push(format!("[{cache_count} cached layers]"));
96 *cache_count = 0;
97 }
98}
99
100#[cfg(test)]
101mod tests {
102 use super::*;
103
104 #[test]
105 fn drops_layer_cache() {
106 let filter = DockerFilter;
107 assert_eq!(filter.filter_line("---> Using cache"), FilterResult::Drop);
108 assert_eq!(filter.filter_line("---> a1b2c3d4e5f6"), FilterResult::Drop);
109 }
110
111 #[test]
112 fn drops_pull_progress() {
113 let filter = DockerFilter;
114 assert_eq!(
115 filter.filter_line("a1b2c3d4e5f6: Downloading 45.2MB/100.0MB"),
116 FilterResult::Drop
117 );
118 assert_eq!(
119 filter.filter_line("a1b2c3d4e5f6: Pull complete"),
120 FilterResult::Drop
121 );
122 }
123
124 #[test]
125 fn keeps_build_steps() {
126 let filter = DockerFilter;
127 assert_eq!(
128 filter.filter_line("Step 3/10 : RUN apt-get update"),
129 FilterResult::Keep
130 );
131 }
132
133 #[test]
134 fn keeps_errors() {
135 let filter = DockerFilter;
136 assert_eq!(
137 filter.filter_line("ERROR: failed to solve: process did not complete"),
138 FilterResult::Keep
139 );
140 }
141
142 #[test]
143 fn block_collapses_pull() {
144 let filter = DockerFilter;
145 let lines = vec![
146 "Using default tag: latest".to_string(),
147 "a1b2c3d4e5f6: Pulling fs layer".to_string(),
148 "b2c3d4e5f6a1: Pulling fs layer".to_string(),
149 "a1b2c3d4e5f6: Downloading 45MB/100MB".to_string(),
150 "a1b2c3d4e5f6: Pull complete".to_string(),
151 "b2c3d4e5f6a1: Pull complete".to_string(),
152 "Digest: sha256:abc123".to_string(),
153 "Status: Downloaded newer image for node:20".to_string(),
154 ];
155 let result = filter.filter_block(&lines);
156 assert_eq!(result.len(), 2);
157 assert_eq!(result[0], "Using default tag: latest");
158 assert!(result[1].contains("pulled"));
159 }
160}