Skip to main content

linux_top_parser_rs/parser/
using_nom.rs

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            // Build a body vector that only includes actual process lines (not blank lines or
413            // subsequent "top - ..." summaries). This prevents out-of-bounds slicing when the
414            // header spans positions that don't exist on non-process lines.
415            let body: Vec<String> =
416                a.3.iter()
417                    .map(|s| s.to_string())
418                    // .filter(|s| {
419                    //     let t = s.trim();
420                    //     if t.is_empty() {
421                    //         return false;
422                    //     }
423                    //     // Keep lines that start with a digit after trimming left (PID lines).
424                    //     t.chars()
425                    //         .next()
426                    //         .map(|c| c.is_ascii_digit())
427                    //         .unwrap_or(false)
428                    // })
429                    .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}