1use super::{CommandExecutor, CommandOutput, DockerCommand};
7use crate::error::Result;
8use async_trait::async_trait;
9use serde::{Deserialize, Serialize};
10
11#[derive(Debug, Clone)]
44pub struct StatsCommand {
45 containers: Vec<String>,
47 all: bool,
49 format: Option<String>,
51 no_stream: bool,
53 no_trunc: bool,
55 pub executor: CommandExecutor,
57}
58
59impl StatsCommand {
60 #[must_use]
70 pub fn new() -> Self {
71 Self {
72 containers: Vec::new(),
73 all: false,
74 format: None,
75 no_stream: false,
76 no_trunc: false,
77 executor: CommandExecutor::new(),
78 }
79 }
80
81 #[must_use]
93 pub fn container(mut self, container: impl Into<String>) -> Self {
94 self.containers.push(container.into());
95 self
96 }
97
98 #[must_use]
100 pub fn containers(mut self, containers: Vec<impl Into<String>>) -> Self {
101 self.containers
102 .extend(containers.into_iter().map(Into::into));
103 self
104 }
105
106 #[must_use]
116 pub fn all(mut self) -> Self {
117 self.all = true;
118 self
119 }
120
121 #[must_use]
135 pub fn format(mut self, format: impl Into<String>) -> Self {
136 self.format = Some(format.into());
137 self
138 }
139
140 #[must_use]
150 pub fn no_stream(mut self) -> Self {
151 self.no_stream = true;
152 self
153 }
154
155 #[must_use]
157 pub fn no_trunc(mut self) -> Self {
158 self.no_trunc = true;
159 self
160 }
161
162 pub async fn run(&self) -> Result<StatsResult> {
189 let output = self.execute().await?;
190
191 let parsed_stats = if self.format.as_deref() == Some("json") {
193 Self::parse_json_stats(&output.stdout)
194 } else {
195 Vec::new()
196 };
197
198 Ok(StatsResult {
199 output,
200 containers: self.containers.clone(),
201 parsed_stats,
202 })
203 }
204
205 fn parse_json_stats(stdout: &str) -> Vec<ContainerStats> {
207 let mut stats = Vec::new();
208
209 for line in stdout.lines() {
211 let line = line.trim();
212 if line.is_empty() {
213 continue;
214 }
215
216 if let Ok(stat) = serde_json::from_str::<ContainerStats>(line) {
217 stats.push(stat);
218 }
219 }
220
221 stats
222 }
223}
224
225impl Default for StatsCommand {
226 fn default() -> Self {
227 Self::new()
228 }
229}
230
231#[async_trait]
232impl DockerCommand for StatsCommand {
233 type Output = CommandOutput;
234
235 fn get_executor(&self) -> &CommandExecutor {
236 &self.executor
237 }
238
239 fn get_executor_mut(&mut self) -> &mut CommandExecutor {
240 &mut self.executor
241 }
242
243 fn build_command_args(&self) -> Vec<String> {
244 let mut args = vec!["stats".to_string()];
245
246 if self.all {
247 args.push("--all".to_string());
248 }
249
250 if let Some(ref format) = self.format {
251 args.push("--format".to_string());
252 args.push(format.clone());
253 }
254
255 if self.no_stream {
256 args.push("--no-stream".to_string());
257 }
258
259 if self.no_trunc {
260 args.push("--no-trunc".to_string());
261 }
262
263 args.extend(self.containers.clone());
265
266 args.extend(self.executor.raw_args.clone());
268
269 args
270 }
271
272 async fn execute(&self) -> Result<Self::Output> {
273 let args = self.build_command_args();
274 self.execute_command(args).await
275 }
276}
277
278#[derive(Debug, Clone)]
280pub struct StatsResult {
281 pub output: CommandOutput,
283 pub containers: Vec<String>,
285 pub parsed_stats: Vec<ContainerStats>,
287}
288
289impl StatsResult {
290 #[must_use]
292 pub fn success(&self) -> bool {
293 self.output.success
294 }
295
296 #[must_use]
298 pub fn containers(&self) -> &[String] {
299 &self.containers
300 }
301
302 #[must_use]
304 pub fn parsed_stats(&self) -> &[ContainerStats] {
305 &self.parsed_stats
306 }
307
308 #[must_use]
310 pub fn raw_output(&self) -> &str {
311 &self.output.stdout
312 }
313}
314
315#[derive(Debug, Clone, Serialize, Deserialize)]
317pub struct ContainerStats {
318 #[serde(alias = "Container")]
320 pub container_id: String,
321
322 #[serde(alias = "Name")]
324 pub name: String,
325
326 #[serde(alias = "CPUPerc")]
328 pub cpu_percent: String,
329
330 #[serde(alias = "MemUsage")]
332 pub memory_usage: String,
333
334 #[serde(alias = "MemPerc")]
336 pub memory_percent: String,
337
338 #[serde(alias = "NetIO")]
340 pub network_io: String,
341
342 #[serde(alias = "BlockIO")]
344 pub block_io: String,
345
346 #[serde(alias = "PIDs")]
348 pub pids: String,
349}
350
351impl ContainerStats {
352 #[must_use]
354 pub fn cpu_percentage(&self) -> Option<f64> {
355 self.cpu_percent.trim_end_matches('%').parse().ok()
356 }
357
358 #[must_use]
360 pub fn memory_percentage(&self) -> Option<f64> {
361 self.memory_percent.trim_end_matches('%').parse().ok()
362 }
363
364 #[must_use]
366 pub fn pid_count(&self) -> Option<u32> {
367 self.pids.parse().ok()
368 }
369}
370
371#[cfg(test)]
372mod tests {
373 use super::*;
374
375 #[test]
376 fn test_stats_basic() {
377 let cmd = StatsCommand::new();
378 let args = cmd.build_command_args();
379 assert_eq!(args, vec!["stats"]);
380 }
381
382 #[test]
383 fn test_stats_with_containers() {
384 let cmd = StatsCommand::new().container("web").container("db");
385 let args = cmd.build_command_args();
386 assert_eq!(args, vec!["stats", "web", "db"]);
387 }
388
389 #[test]
390 fn test_stats_with_all_flag() {
391 let cmd = StatsCommand::new().all();
392 let args = cmd.build_command_args();
393 assert_eq!(args, vec!["stats", "--all"]);
394 }
395
396 #[test]
397 fn test_stats_with_format() {
398 let cmd = StatsCommand::new().format("json");
399 let args = cmd.build_command_args();
400 assert_eq!(args, vec!["stats", "--format", "json"]);
401 }
402
403 #[test]
404 fn test_stats_no_stream() {
405 let cmd = StatsCommand::new().no_stream();
406 let args = cmd.build_command_args();
407 assert_eq!(args, vec!["stats", "--no-stream"]);
408 }
409
410 #[test]
411 fn test_stats_no_trunc() {
412 let cmd = StatsCommand::new().no_trunc();
413 let args = cmd.build_command_args();
414 assert_eq!(args, vec!["stats", "--no-trunc"]);
415 }
416
417 #[test]
418 fn test_stats_all_options() {
419 let cmd = StatsCommand::new()
420 .all()
421 .format("table")
422 .no_stream()
423 .no_trunc()
424 .container("test-container");
425 let args = cmd.build_command_args();
426 assert_eq!(
427 args,
428 vec![
429 "stats",
430 "--all",
431 "--format",
432 "table",
433 "--no-stream",
434 "--no-trunc",
435 "test-container"
436 ]
437 );
438 }
439
440 #[test]
441 fn test_container_stats_parsing() {
442 let stats = ContainerStats {
443 container_id: "abc123".to_string(),
444 name: "test-container".to_string(),
445 cpu_percent: "1.23%".to_string(),
446 memory_usage: "512MiB / 2GiB".to_string(),
447 memory_percent: "25.00%".to_string(),
448 network_io: "1.2kB / 3.4kB".to_string(),
449 block_io: "4.5MB / 6.7MB".to_string(),
450 pids: "42".to_string(),
451 };
452
453 assert_eq!(stats.cpu_percentage(), Some(1.23));
454 assert_eq!(stats.memory_percentage(), Some(25.0));
455 assert_eq!(stats.pid_count(), Some(42));
456 }
457
458 #[test]
459 fn test_parse_json_stats() {
460 let json_output = r#"{"Container":"abc123","Name":"test","CPUPerc":"1.23%","MemUsage":"512MiB / 2GiB","MemPerc":"25.00%","NetIO":"1.2kB / 3.4kB","BlockIO":"4.5MB / 6.7MB","PIDs":"42"}"#;
461
462 let stats = StatsCommand::parse_json_stats(json_output);
463 assert_eq!(stats.len(), 1);
464 assert_eq!(stats[0].name, "test");
465 assert_eq!(stats[0].container_id, "abc123");
466 }
467
468 #[test]
469 fn test_parse_json_stats_multiple_lines() {
470 let json_output = r#"{"Container":"abc123","Name":"test1","CPUPerc":"1.23%","MemUsage":"512MiB / 2GiB","MemPerc":"25.00%","NetIO":"1.2kB / 3.4kB","BlockIO":"4.5MB / 6.7MB","PIDs":"42"}
471{"Container":"def456","Name":"test2","CPUPerc":"2.34%","MemUsage":"1GiB / 4GiB","MemPerc":"25.00%","NetIO":"2.3kB / 4.5kB","BlockIO":"5.6MB / 7.8MB","PIDs":"24"}"#;
472
473 let stats = StatsCommand::parse_json_stats(json_output);
474 assert_eq!(stats.len(), 2);
475 assert_eq!(stats[0].name, "test1");
476 assert_eq!(stats[1].name, "test2");
477 }
478
479 #[test]
480 fn test_parse_json_stats_empty() {
481 let stats = StatsCommand::parse_json_stats("");
482 assert!(stats.is_empty());
483 }
484}