1use indexmap::IndexMap;
2
3use super::common::{from_days, from_hours, from_minutes};
4use nom::{
5 branch::alt,
6 bytes::complete::tag,
7 character::complete::{
8 crlf, digit0, digit1, line_ending, none_of, not_line_ending, space0, space1,
9 },
10 combinator::{map, map_res, opt, peek},
11 error::Error,
12 multi::{many0, many1, many_till, separated_list1},
13 number::complete::float,
14 sequence::{delimited, preceded, separated_pair, terminated},
15 IResult, Parser,
16};
17use serde::{Deserialize, Serialize};
18
19fn parse_field<'a>(
20 key: &'static str,
21) -> impl Parser<&'a str, Output = f32, Error = Error<&'a str>> {
22 terminated(
23 delimited(space0, float, space0),
24 terminated(tag(key), opt(tag(","))),
25 )
26}
27
28#[derive(Debug, PartialEq, Serialize, Deserialize)]
29#[serde(rename_all = "camelCase")]
30pub struct TaskStates {
31 pub total: u32,
32 pub running: u32,
33 pub sleeping: u32,
34 pub stopped: u32,
35 pub zombie: u32,
36}
37
38fn parse_task_states(input: &str) -> IResult<&str, TaskStates> {
39 let (input, task_states) = (
40 tag("Tasks:"),
41 parse_field("total"),
42 parse_field("running"),
43 parse_field("sleeping"),
44 parse_field("stopped"),
45 parse_field("zombie"),
46 )
47 .parse(input)?;
48
49 Ok((
50 input,
51 TaskStates {
52 total: task_states.1 as u32,
53 running: task_states.2 as u32,
54 sleeping: task_states.3 as u32,
55 stopped: task_states.4 as u32,
56 zombie: task_states.5 as u32,
57 },
58 ))
59}
60
61#[derive(Debug, PartialEq, Serialize, Deserialize)]
62pub struct CpuStates {
63 #[serde(rename = "cpu")]
64 pub id: i32,
65 #[serde(rename = "us")]
66 pub user: f32,
67 #[serde(rename = "sy")]
68 pub system: f32,
69 #[serde(rename = "ni")]
70 pub nice: f32,
71 #[serde(rename = "id")]
72 pub idle: f32,
73 #[serde(rename = "wa")]
74 pub io_wait: f32,
75 #[serde(rename = "hi")]
76 pub hw_irq: f32,
77 #[serde(rename = "si")]
78 pub soft_irq: f32,
79 #[serde(rename = "st")]
80 pub steal: f32,
81}
82
83fn parse_cpu_id(input: &str) -> IResult<&str, i32> {
84 alt((
85 map(tag("%Cpu(s):"), |_| -1),
86 map_res(
87 preceded(tag("%Cpu"), terminated(digit0, (space0, tag(":")))),
88 str::parse::<i32>,
89 ),
90 ))
91 .parse(input)
92}
93
94fn parse_cpu_states(input: &str) -> IResult<&str, CpuStates> {
95 let (input, cpus) = (
96 parse_cpu_id,
97 parse_field("us"),
98 parse_field("sy"),
99 parse_field("ni"),
100 parse_field("id"),
101 parse_field("wa"),
102 parse_field("hi"),
103 parse_field("si"),
104 parse_field("st"),
105 )
106 .parse(input)?;
107
108 Ok((
109 input,
110 CpuStates {
111 id: cpus.0,
112 user: cpus.1,
113 system: cpus.2,
114 nice: cpus.3,
115 idle: cpus.4,
116 io_wait: cpus.5,
117 hw_irq: cpus.6,
118 soft_irq: cpus.7,
119 steal: cpus.8,
120 },
121 ))
122}
123
124#[derive(Debug, PartialEq, Serialize, Deserialize)]
125#[serde(rename_all = "camelCase")]
126pub struct PhysicalMemory {
127 pub total: f32,
128 pub free: f32,
129 pub used: f32,
130 pub buff_or_cache: f32,
131}
132
133fn parse_physical_memory(input: &str) -> IResult<&str, PhysicalMemory> {
134 let (input, physical_memory) = (
135 tag("MiB Mem :"),
136 parse_field("total"),
137 parse_field("free"),
138 parse_field("used"),
139 parse_field("buff/cache"),
140 )
141 .parse(input)?;
142
143 Ok((
144 input,
145 PhysicalMemory {
146 total: physical_memory.1,
147 free: physical_memory.2,
148 used: physical_memory.3,
149 buff_or_cache: physical_memory.4,
150 },
151 ))
152}
153
154#[derive(Debug, PartialEq, Serialize, Deserialize)]
155#[serde(rename_all = "camelCase")]
156pub struct VirtualMemory {
157 pub total: f32,
158 pub free: f32,
159 pub used: f32,
160 pub available: f32,
161}
162
163fn parse_virtual_memory(input: &str) -> IResult<&str, VirtualMemory> {
164 let (input, virtual_memory) = (
165 tag("MiB Swap:"),
166 parse_field("total"),
167 parse_field("free"),
168 parse_field("used."),
169 parse_field("avail Mem"),
170 )
171 .parse(input)?;
172
173 Ok((
174 input,
175 VirtualMemory {
176 total: virtual_memory.1,
177 free: virtual_memory.2,
178 used: virtual_memory.3,
179 available: virtual_memory.4,
180 },
181 ))
182}
183
184#[derive(Debug, PartialEq, Serialize, Deserialize)]
185#[serde(rename_all = "camelCase")]
186pub struct TopInfo {
187 pub summary_display: SummaryDisplay,
188 pub field_values: Vec<IndexMap<String, String>>,
189}
190
191#[derive(Debug, PartialEq, Serialize, Deserialize)]
192#[serde(rename_all = "camelCase")]
193pub struct SummaryDisplay {
194 pub up_time_and_load_average: UpTimeAndLoadAverage,
195 pub task_states: TaskStates,
196 pub cpu_states: Vec<CpuStates>,
197 pub physical_memory: PhysicalMemory,
198 pub virtual_memory: VirtualMemory,
199}
200
201fn parse_summary_display(input: &str) -> IResult<&str, SummaryDisplay> {
202 map(
203 (
204 terminated(parse_up_time_and_load_average, line_ending),
205 terminated(parse_task_states, line_ending),
206 many1(terminated(parse_cpu_states, line_ending)),
207 terminated(parse_physical_memory, line_ending),
208 terminated(parse_virtual_memory, line_ending),
209 ),
210 |output| SummaryDisplay {
211 up_time_and_load_average: output.0,
212 task_states: output.1,
213 cpu_states: output.2,
214 physical_memory: output.3,
215 virtual_memory: output.4,
216 },
217 )
218 .parse(input)
219}
220
221#[derive(Debug)]
222enum UpTimeDuration {
223 Minutes(u32),
224 HoursMinutes(u32, u32),
225 DaysMinutes(u32, u32),
226 DaysHoursMinutes(u32, u32, u32),
227}
228
229impl UpTimeDuration {
230 fn to_seconds(&self) -> u32 {
231 match *self {
232 UpTimeDuration::Minutes(mins) => from_minutes(mins),
233 UpTimeDuration::HoursMinutes(hours, mins) => from_hours(hours) + from_minutes(mins),
234 UpTimeDuration::DaysMinutes(days, mins) => from_days(days) + from_minutes(mins),
235 UpTimeDuration::DaysHoursMinutes(days, hours, mins) => {
236 from_days(days) + from_hours(hours) + from_minutes(mins)
237 }
238 }
239 }
240}
241
242fn parse_up_time_mins(input: &str) -> IResult<&str, u32> {
243 delimited(space0, map_res(digit0, str::parse::<u32>), tag(" min,")).parse(input)
244}
245
246fn parse_up_time_hours_mins(input: &str) -> IResult<&str, (u32, u32)> {
247 delimited(
248 space0,
249 separated_pair(
250 map_res(digit0, str::parse::<u32>),
251 tag(":"),
252 map_res(digit0, str::parse::<u32>),
253 ),
254 tag(","),
255 )
256 .parse(input)
257}
258
259fn parse_up_time_days(input: &str) -> IResult<&str, u32> {
260 delimited(
261 space0,
262 map_res(digit0, str::parse::<u32>),
263 alt((tag(" day,"), tag(" days,"))),
264 )
265 .parse(input)
266}
267
268fn parse_up_time(input: &str) -> IResult<&str, u32> {
269 let (input, _) = preceded(space0, tag("up")).parse(input)?;
270
271 map(
272 alt((
273 map(parse_up_time_mins, UpTimeDuration::Minutes),
274 map(parse_up_time_hours_mins, |(hours, mins)| {
275 UpTimeDuration::HoursMinutes(hours, mins)
276 }),
277 map(
278 (parse_up_time_days, parse_up_time_hours_mins),
279 |(days, (hours, mins))| UpTimeDuration::DaysHoursMinutes(days, hours, mins),
280 ),
281 map((parse_up_time_days, parse_up_time_mins), |(days, mins)| {
282 UpTimeDuration::DaysMinutes(days, mins)
283 }),
284 )),
285 |up_time_duration: UpTimeDuration| up_time_duration.to_seconds(),
286 )
287 .parse(input)
288}
289
290#[derive(Debug, PartialEq, Serialize, Deserialize)]
291pub struct LoadAverage {
292 #[serde(rename = "loadAverageLast_1_min")]
293 pub last_1_min: f32,
294 #[serde(rename = "loadAverageLast_5_min")]
295 pub last_5_min: f32,
296 #[serde(rename = "loadAverageLast_15_min")]
297 pub last_15_min: f32,
298}
299
300impl LoadAverage {
301 fn from_vec(input: Vec<f32>) -> Self {
302 let mut input = input.into_iter();
303
304 LoadAverage {
305 last_1_min: input.next().unwrap_or_default(),
306 last_5_min: input.next().unwrap_or_default(),
307 last_15_min: input.next().unwrap_or_default(),
308 }
309 }
310}
311
312fn parse_load_average(input: &str) -> IResult<&str, LoadAverage> {
313 let (input, _) = preceded(space0, tag("load average:")).parse(input)?;
314
315 map(
316 many1(preceded(space0, terminated(float, opt(tag(","))))),
317 LoadAverage::from_vec,
318 )
319 .parse(input)
320}
321
322#[derive(Debug, PartialEq, Serialize, Deserialize)]
323#[serde(rename_all = "camelCase")]
324pub struct UpTimeAndLoadAverage {
325 pub time: String,
326 #[serde(rename = "upTime_s")]
327 pub up_time_s: u32,
328 pub total_number_of_users: u32,
329 #[serde(flatten)]
330 pub load_average: LoadAverage,
331}
332
333fn parse_up_time_and_load_average(input: &str) -> IResult<&str, UpTimeAndLoadAverage> {
334 let (input, _) = tag("top -")(input)?;
335
336 map(
337 (
338 parse_hh_mm_ss,
339 parse_up_time,
340 alt((parse_field("users"), parse_field("user"))),
341 parse_load_average,
342 ),
343 |output| UpTimeAndLoadAverage {
344 time: output.0.join(":"),
345 up_time_s: output.1,
346 total_number_of_users: output.2 as u32,
347 load_average: output.3,
348 },
349 )
350 .parse(input)
351}
352
353fn parse_hh_mm_ss(input: &str) -> IResult<&str, Vec<&str>> {
354 preceded(space0, many1(terminated(digit1, opt(tag(":"))))).parse(input)
355}
356
357fn parse_columns_header(input: &str) -> IResult<&str, Vec<String>> {
358 many1(alt((
359 map(
360 (
361 opt(tag(" ")),
362 alt((
363 tag("USER"),
364 tag("RUSER"),
365 tag("SUSER"),
366 tag("GROUP"),
367 tag("TTY"),
368 tag("TIME"),
369 tag("COMMAND"),
370 tag("WCHAN"),
371 tag("Flags"),
372 tag("CGROUPS"),
373 tag("SUPGIDS"),
374 tag("SUPGRPS"),
375 tag("ENVIRON"),
376 tag("LXC"),
377 tag("CGNAME"),
378 )),
379 many_till(tag(" "), peek((tag(" "), none_of(" ")))),
380 ),
381 |a| format!("{}{}{}", a.0.unwrap_or_default(), a.1, a.2 .0.join("")),
382 ),
383 map((space1, many1(none_of(" \n"))), |a| {
384 format!("{}{}", a.0, a.1.iter().collect::<String>())
385 }),
386 )))
387 .parse(input)
388}
389
390fn not_empty_line(input: &str) -> IResult<&str, &str> {
391 let (rest, line) = not_line_ending(input)?;
392
393 if line.trim().is_empty() {
394 Err(nom::Err::Error(Error::new(
395 rest,
396 nom::error::ErrorKind::Fail,
397 )))
398 } else {
399 Ok((rest, line))
400 }
401}
402
403fn parse_top_info_block(input: &str) -> IResult<&str, TopInfo> {
404 map(
405 (
406 parse_summary_display,
407 many1(line_ending),
408 terminated(parse_columns_header, line_ending),
409 separated_list1(alt((line_ending, crlf)), not_empty_line),
410 ),
411 |a| {
412 let body: Vec<String> =
416 a.3.iter()
417 .map(|s| s.to_string())
418 .collect();
430
431 TopInfo {
432 summary_display: a.0,
433 field_values: parse_field_values(&a.2, &body),
434 }
435 },
436 )
437 .parse(input)
438}
439
440pub fn parse_multiple_top_info_blocks(input: &str) -> IResult<&str, Vec<TopInfo>> {
441 many1(terminated(parse_top_info_block, many0(line_ending))).parse(input)
442}
443
444#[derive(Debug, PartialEq)]
445struct ColumnsHeader {
446 raw: String,
447 title: String,
448 start: usize,
449 end: usize,
450}
451
452fn parse_field_values(header: &[String], body: &[String]) -> Vec<IndexMap<String, String>> {
453 let header_start_end = header
454 .iter()
455 .enumerate()
456 .fold(Vec::new(), |mut acc, (_, token)| {
457 let raw = token.to_string();
458 let title = raw.trim().to_string();
459 let start = acc.last().map_or(0, |prev: &ColumnsHeader| prev.end);
460 let end = start + token.len();
461
462 acc.push(ColumnsHeader {
463 raw,
464 title,
465 start,
466 end,
467 });
468
469 acc
470 });
471
472 body.iter()
473 .filter(|line| !line.is_empty())
474 .map(|line| {
475 header_start_end
476 .iter()
477 .map(|h| {
478 let key = h.title.clone();
479 let value = line[h.start..h.end].trim().to_string();
480 (key, value)
481 })
482 .collect::<IndexMap<_, _>>()
483 })
484 .collect()
485}
486
487#[cfg(test)]
488mod tests {
489 use pretty_assertions::assert_eq;
490 use rstest::rstest;
491
492 use super::*;
493
494 #[rstest]
495 #[case::with_ending_newline_and_empty_line("top - 15:29:38 up 15:54, 0 users, load average: 0.14, 0.07, 0.06
496Tasks: 60 total, 1 running, 39 sleeping, 0 stopped, 20 zombie
497%Cpu(s): 0.4 us, 0.8 sy, 0.1 ni, 98.4 id, 0.2 wa, 0.3 hi, 0.4 si, 0.0 st
498MiB Mem : 7947.3 total, 408.6 free, 4257.3 used, 3281.4 buff/cache
499MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 3392.8 avail Mem
500
501 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND P
50218253 tim 20 0 23.8g 235884 37740 S 6.7 3.9 0:03.07 /home/tim/.nvm/versions/node/v18.12.0/bin/node --experimental-loader=file:///home/tim/.vscode-server/extensions/wallabyjs.wallaby-vscode-1.0.349/wallaby65f4bb/runners/node/hooks.mjs /home/tim/.vscode-server/extensions/wallabyjs.wallaby-vscode-1.0.349/wallaby65f4bb/server.js runner 0 40475 vitest@0.14.0,autoDetected /home/tim/learn/linux-top-parser/node_modules /home/tim/.vscode-server/extensions/wallabyjs.wallaby-vscode-1.0.349/proje+ 2
503
504")]
505 #[case::with_ending_newline("top - 15:29:38 up 15:54, 0 users, load average: 0.14, 0.07, 0.06
506Tasks: 60 total, 1 running, 39 sleeping, 0 stopped, 20 zombie
507%Cpu(s): 0.4 us, 0.8 sy, 0.1 ni, 98.4 id, 0.2 wa, 0.3 hi, 0.4 si, 0.0 st
508MiB Mem : 7947.3 total, 408.6 free, 4257.3 used, 3281.4 buff/cache
509MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 3392.8 avail Mem
510
511 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND P
51218253 tim 20 0 23.8g 235884 37740 S 6.7 3.9 0:03.07 /home/tim/.nvm/versions/node/v18.12.0/bin/node --experimental-loader=file:///home/tim/.vscode-server/extensions/wallabyjs.wallaby-vscode-1.0.349/wallaby65f4bb/runners/node/hooks.mjs /home/tim/.vscode-server/extensions/wallabyjs.wallaby-vscode-1.0.349/wallaby65f4bb/server.js runner 0 40475 vitest@0.14.0,autoDetected /home/tim/learn/linux-top-parser/node_modules /home/tim/.vscode-server/extensions/wallabyjs.wallaby-vscode-1.0.349/proje+ 2
513")]
514 #[case::without_ending_newline("top - 15:29:38 up 15:54, 0 users, load average: 0.14, 0.07, 0.06
515Tasks: 60 total, 1 running, 39 sleeping, 0 stopped, 20 zombie
516%Cpu(s): 0.4 us, 0.8 sy, 0.1 ni, 98.4 id, 0.2 wa, 0.3 hi, 0.4 si, 0.0 st
517MiB Mem : 7947.3 total, 408.6 free, 4257.3 used, 3281.4 buff/cache
518MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 3392.8 avail Mem
519
520 PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND P
52118253 tim 20 0 23.8g 235884 37740 S 6.7 3.9 0:03.07 /home/tim/.nvm/versions/node/v18.12.0/bin/node --experimental-loader=file:///home/tim/.vscode-server/extensions/wallabyjs.wallaby-vscode-1.0.349/wallaby65f4bb/runners/node/hooks.mjs /home/tim/.vscode-server/extensions/wallabyjs.wallaby-vscode-1.0.349/wallaby65f4bb/server.js runner 0 40475 vitest@0.14.0,autoDetected /home/tim/learn/linux-top-parser/node_modules /home/tim/.vscode-server/extensions/wallabyjs.wallaby-vscode-1.0.349/proje+ 2")]
522 fn it_can_parse_top_info_block(#[case] input: &str) {
523 let actual = parse_top_info_block(input).unwrap().1;
524
525 let expected = TopInfo {
526 summary_display: SummaryDisplay {
527 up_time_and_load_average: UpTimeAndLoadAverage {
528 time: String::from("15:29:38"),
529 up_time_s: 57240,
530 total_number_of_users: 0,
531 load_average: LoadAverage {
532 last_1_min: 0.14,
533 last_5_min: 0.07,
534 last_15_min: 0.06,
535 },
536 },
537 task_states: TaskStates {
538 total: 60,
539 running: 1,
540 sleeping: 39,
541 stopped: 0,
542 zombie: 20,
543 },
544 cpu_states: vec![CpuStates {
545 id: -1,
546 user: 0.4,
547 system: 0.8,
548 nice: 0.1,
549 idle: 98.4,
550 io_wait: 0.2,
551 hw_irq: 0.3,
552 soft_irq: 0.4,
553 steal: 0.0,
554 }],
555 physical_memory: PhysicalMemory {
556 total: 7947.3,
557 free: 408.6,
558 used: 4257.3,
559 buff_or_cache: 3281.4,
560 },
561 virtual_memory: VirtualMemory {
562 total: 2048.0,
563 free: 2048.0,
564 used: 0.0,
565 available: 3392.8,
566 },
567 },
568 field_values: vec![{
569 vec![
570 ("PID", "18253"),
571 ("USER", "tim"),
572 ("PR", "20"),
573 ("NI", "0"),
574 ("VIRT", "23.8g"),
575 ("RES", "235884"),
576 ("SHR", "37740"),
577 ("S", "S"),
578 ("%CPU", "6.7"),
579 ("%MEM", "3.9"),
580 ("TIME+", "0:03.07"),
581 ("COMMAND", "/home/tim/.nvm/versions/node/v18.12.0/bin/node --experimental-loader=file:///home/tim/.vscode-server/extensions/wallabyjs.wallaby-vscode-1.0.349/wallaby65f4bb/runners/node/hooks.mjs /home/tim/.vscode-server/extensions/wallabyjs.wallaby-vscode-1.0.349/wallaby65f4bb/server.js runner 0 40475 vitest@0.14.0,autoDetected /home/tim/learn/linux-top-parser/node_modules /home/tim/.vscode-server/extensions/wallabyjs.wallaby-vscode-1.0.349/proje+"),
582 ("P", "2"),
583 ].into_iter().map(|(k, v)| (k.to_string(), v.to_string())).collect()
584 }],
585 };
586
587 assert_eq!(actual, expected);
588 }
589
590 #[rstest]
591 #[case::start_with_pad_left(
592 " PID USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND P ",
593 vec![
594 " PID",
595 " USER ",
596 " PR",
597 " NI",
598 " VIRT",
599 " RES",
600 " SHR",
601 " S",
602 " %CPU",
603 " %MEM",
604 " TIME+",
605 " COMMAND ",
606 " P",
607 ]
608 )]
609 #[case::start_with_pad_right(
610 "USER PR NI VIRT RES SHR S %CPU %MEM TIME+ COMMAND P ",
611 vec![
612 "USER ",
613 " PR",
614 " NI",
615 " VIRT",
616 " RES",
617 " SHR",
618 " S",
619 " %CPU",
620 " %MEM",
621 " TIME+",
622 " COMMAND ",
623 " P"
624 ]
625 )]
626 #[case::with_new_line(
627 "USER PR NI
628",
629 vec![
630 "USER ",
631 " PR",
632 " NI",
633 ]
634 )]
635 fn it_can_parse_header(#[case] input: &str, #[case] expected: Vec<&str>) {
636 assert_eq!(expected, parse_columns_header(input).unwrap().1);
637 }
638
639 #[rstest]
640 #[case::without_space("%Cpu0:", 0)]
641 #[case::with_space("%Cpu12 :", 12)]
642 #[case::all_cpus("%Cpu(s):", -1)]
643 fn it_can_parse_cpu_count(#[case] input: &str, #[case] expected: i32) {
644 let actual = parse_cpu_id(input).unwrap().1;
645
646 assert_eq!(actual, expected);
647 }
648
649 #[rstest]
650 #[case("top - 14:48:52 up 2 days, 13:23, 0 user, load average: 0.06, 0.02, 0.00", ("14:48:52", 220980, 0, 0.06, 0.02, 0.0))]
651 #[case::with_plural_users("top - 23:09:37 up 21 min, 0 users, load average: 0.11, 0.10, 0.18", ("23:09:37", 1260, 0, 0.11, 0.1, 0.18))]
652 fn it_can_parse_up_time_and_load_average(
653 #[case] input: &str,
654 #[case] actual: (&str, u32, u32, f32, f32, f32),
655 ) {
656 let (time, up_time_s, total_number_of_users, last_1_min, last_5_min, last_15_min) = actual;
657 let expected = UpTimeAndLoadAverage {
658 time: time.to_string(),
659 up_time_s,
660 total_number_of_users,
661 load_average: LoadAverage {
662 last_1_min,
663 last_5_min,
664 last_15_min,
665 },
666 };
667
668 let actual = parse_up_time_and_load_average(input).unwrap().1;
669
670 assert_eq!(actual, expected);
671 }
672
673 #[rstest]
674 fn it_can_parse_hh_mm_ss(
675 #[values("12:23:32", " 12:23:32", "12:23:32 ", " 12:23:32 ")] input: &str,
676 ) {
677 let actual = parse_hh_mm_ss(input).unwrap().1;
678 let actual = actual.join(":");
679 assert_eq!(actual, "12:23:32");
680 }
681
682 #[rstest]
683 #[case("load average: 0.11, 0.10, 0.18", vec![0.11, 0.1, 0.18])]
684 #[case::with_preceded_comma(" load average: 0.11, 0.10, 0.18", vec![0.11, 0.1, 0.18])]
685 #[case::with_comma_terminated("load average: 0.11, 0.10, 0.18,", vec![0.11, 0.1, 0.18])]
686 #[case("load average: 0.063, 0.02, 0.00", vec![0.063, 0.02, 0.0])]
687 fn it_can_parse_load_average(#[case] input: &str, #[case] expected: Vec<f32>) {
688 let actual = parse_load_average(input).unwrap().1;
689
690 assert_eq!(actual, LoadAverage::from_vec(expected));
691 }
692
693 #[rstest]
694 #[case("up 21 min,", 1260)]
695 #[case::with_preceded_comma(" up 21 min,", 1260)]
696 #[case("up 15:54,", 57240)]
697 #[case("up 1 day, 23:52,", 172320)]
698 #[case("up 30 days, 5 min,", 2592300)]
699 #[case("up 2 days, 13:23,", 220980)]
700 fn it_can_parse_up_time(#[case] input: &str, #[case] expected: u32) {
701 let actual = parse_up_time(input).unwrap().1;
702
703 assert_eq!(actual, expected);
704 }
705
706 #[rstest]
707 #[case(" 55 total,", "total", 55)]
708 #[case(" 55 total", "total", 55)]
709 #[case("55 total,", "total", 55)]
710 #[case("55total", "total", 55)]
711 #[case(" 1 running,", "running", 1)]
712 #[case(" 0 stopped,", "stopped", 0)]
713 fn it_can_parse_field(#[case] input: &str, #[case] key: &'static str, #[case] expected: u32) {
714 let (_, actual) = parse_field(key).parse(input).unwrap();
715 assert_eq!(actual as u32, expected);
716 }
717
718 #[rstest]
719 #[case("Tasks: 55 total, 1 running, 53 sleeping, 0 stopped, 1 zombie", (55, 1, 53, 0, 1))]
720 #[case("Tasks: 51 total,0 running,53 sleeping,0 stopped,1012 zombie", (51, 0, 53, 0, 1012))]
721 fn it_can_parse_task_states(#[case] input: &str, #[case] expected: (u32, u32, u32, u32, u32)) {
722 let (_, actual) = parse_task_states(input).unwrap();
723
724 let (total, running, sleeping, stopped, zombie) = expected;
725
726 assert_eq!(
727 actual,
728 TaskStates {
729 total,
730 running,
731 sleeping,
732 stopped,
733 zombie,
734 }
735 );
736 }
737
738 #[rstest]
739 #[case::all_cpus("%Cpu(s): 0.4 us, 0.8 sy, 0.1 ni, 98.4 id, 0.2 wa, 0.0 hi, 0.4 si, 0.3 st", -1)]
740 #[case::single_cpu(
741 "%Cpu6 : 0.4 us, 0.8 sy, 0.1 ni, 98.4 id, 0.2 wa, 0.0 hi, 0.4 si, 0.3 st",
742 6
743 )]
744 fn it_can_parse_cpu_states(#[case] input: &str, #[case] cpu_count: i32) {
745 let actual = parse_cpu_states(input).unwrap().1;
746
747 assert_eq!(
748 actual,
749 CpuStates {
750 id: cpu_count,
751 user: 0.4,
752 system: 0.8,
753 nice: 0.1,
754 idle: 98.4,
755 io_wait: 0.2,
756 hw_irq: 0.0,
757 soft_irq: 0.4,
758 steal: 0.3,
759 }
760 );
761 }
762
763 #[test]
764 fn it_can_parse_physical_memory() {
765 let input = "MiB Mem : 7947.3 total, 408.6 free, 4257.3 used, 3281.4 buff/cache";
766 let actual = parse_physical_memory(input).unwrap().1;
767
768 assert_eq!(
769 actual,
770 PhysicalMemory {
771 total: 7947.3,
772 free: 408.6,
773 used: 4257.3,
774 buff_or_cache: 3281.4,
775 }
776 )
777 }
778
779 #[test]
780 fn it_can_parse_virtual_memory() {
781 let input = "MiB Swap: 2048.0 total, 2048.0 free, 0.0 used. 3392.8 avail Mem";
782 let actual = parse_virtual_memory(input).unwrap().1;
783
784 assert_eq!(
785 actual,
786 VirtualMemory {
787 total: 2048.0,
788 free: 2048.0,
789 used: 0.0,
790 available: 3392.8,
791 }
792 )
793 }
794
795 #[rstest]
796 fn it_can_parse_summary_display(#[values("single_all_cpu", "multi")] file_name: &str) {
797 let folder_path = std::path::Path::new(env!("CARGO_MANIFEST_DIR")).join("data");
798
799 let input_file_path = folder_path.join(format!("{}.txt", file_name));
800 let expected_file_path = folder_path.join(format!("{}_expected.json", file_name));
801
802 let input = std::fs::read_to_string(input_file_path).unwrap();
803 let expected = std::fs::read_to_string(expected_file_path).unwrap();
804
805 let expected = serde_json::from_str::<Vec<TopInfo>>(expected.as_str()).unwrap();
806 let actual = parse_multiple_top_info_blocks(input.as_str()).unwrap().1;
807 println!("{:#?}", actual);
808 assert_eq!(actual, expected);
809 }
810}