1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
use crate::regex::re;
use colored::{ColoredString, Colorize};
use indexmap::IndexMap;
use std::{
    path::{Component, Path},
    time::Duration,
};

/// The core parsing function that extracts all the information from `cargo test`
/// but filters out empty tests.
pub fn parse_cargo_test<'s>(stderr: &'s str, stdout: &'s str) -> TestRunners<'s> {
    use TestType::*;

    let mut pkg = None;
    TestRunners::new(
        parse_cargo_test_with_empty_ones(stderr, stdout)
            .filter_map(|(runner, info)| {
                match runner.ty {
                    UnitLib | UnitBin => pkg = Some(runner.src.bin_name),
                    Doc => pkg = Some("Doc Tests"),
                    _ => (),
                }
                if info.stats.total == 0 {
                    // don't show test types that have no tests
                    None
                } else {
                    Some((pkg, runner, info))
                }
            })
            .collect(),
    )
}

/// The core parsing function that extracts all the information from `cargo test`.
pub fn parse_cargo_test_with_empty_ones<'s>(
    stderr: &'s str,
    stdout: &'s str,
) -> impl Iterator<Item = (TestRunner<'s>, TestInfo<'s>)> {
    let parsed_stderr = parse_stderr(stderr);
    let parsed_stdout = parse_stdout(stdout);
    assert_eq!(
        parsed_stderr.len(),
        parsed_stdout.len(),
        "the amount of test runners from stderr should equal to that from stdout"
    );
    parsed_stderr.into_iter().zip(parsed_stdout)
}

/// Pkg/crate name determined by the unittests.
/// It's possible to be None because unittests can be omitted in `cargo test`
/// and we can't determine which crate emits the tests.
/// This mainly affacts how the project structure looks like specifically the root node.
pub type Pkg<'s> = Option<Text<'s>>;

/// All the test runners with original display order but filtering empty types out.
#[derive(Debug, Default)]
pub struct TestRunners<'s> {
    pub pkgs: IndexMap<Pkg<'s>, PkgTest<'s>>,
}

impl<'s> TestRunners<'s> {
    pub fn new(v: Vec<(Pkg<'s>, TestRunner<'s>, TestInfo<'s>)>) -> TestRunners<'s> {
        let mut runners = TestRunners::default();
        for (pkg, runner, info) in v {
            match runners.pkgs.entry(pkg) {
                indexmap::map::Entry::Occupied(mut item) => {
                    item.get_mut().push(runner, info);
                }
                indexmap::map::Entry::Vacant(empty) => {
                    empty.insert(PkgTest::new(runner, info));
                }
            }
        }
        runners
    }
}

/// The raw output from `cargo test`.
pub type Text<'s> = &'s str;

/// Tests information in a pkg/crate.
/// For doc test type, tests from multiple crates are considered
/// to be under a presumed Doc pkg.
#[derive(Debug, Default)]
pub struct PkgTest<'s> {
    pub inner: Vec<Data<'s>>,
    pub stats: Stats,
}

impl<'s> PkgTest<'s> {
    pub fn new(runner: TestRunner<'s>, info: TestInfo<'s>) -> PkgTest<'s> {
        let stats = info.stats.clone();
        PkgTest {
            inner: vec![Data { runner, info }],
            stats,
        }
    }
    pub fn push(&mut self, runner: TestRunner<'s>, info: TestInfo<'s>) {
        self.stats += &info.stats;
        self.inner.push(Data { runner, info });
    }
}

/// Information extracted from stdout & stderr.
#[derive(Debug)]
pub struct Data<'s> {
    pub runner: TestRunner<'s>,
    pub info: TestInfo<'s>,
}

/// A test runner determined by the type and binary & source path.
#[derive(Debug, Hash, PartialEq, Eq)]
pub struct TestRunner<'s> {
    pub ty: TestType,
    pub src: Src<'s>,
}

/// All the information reported by a test runner.
#[derive(Debug)]
pub struct TestInfo<'s> {
    /// Raw test information from stdout.
    pub raw: Text<'s>,
    pub stats: Stats,
    pub parsed: ParsedCargoTestOutput<'s>,
}

/// Types of a test.
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
pub enum TestType {
    UnitLib,
    UnitBin,
    Doc,
    Tests,
    Examples,
    Benches,
}

/// Source location and binary name for a test runner.
#[derive(Debug, Hash, PartialEq, Eq, Clone, Copy)]
pub struct Src<'s> {
    /// Path of source code (except Doc type) which is relative to its crate
    /// rather than root of project.
    ///
    /// This means it's possible to see same path from different crates.
    pub src_path: Text<'s>,
    /// Name from the path of test runner binary. The path usually starts with `target/`.
    ///
    /// But this field doesn't contain neither the `target/...` prefix nor hash postfix,
    /// so it's possible to see same path from different crates.
    pub bin_name: Text<'s>,
}

/// Statistics of test.
#[derive(Debug, PartialEq, Eq, Clone)]
pub struct Stats {
    pub ok: bool,
    pub total: u32,
    pub passed: u32,
    pub failed: u32,
    pub ignored: u32,
    pub measured: u32,
    pub filtered_out: u32,
    pub finished_in: Duration,
}

/// Summary text on the bottom.
impl std::fmt::Display for Stats {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        let Stats {
            ok,
            total,
            passed,
            failed,
            ignored,
            measured,
            filtered_out,
            finished_in,
        } = *self;
        let time = finished_in.as_secs_f32();
        let fail = if failed == 0 {
            format!("{failed} failed")
        } else {
            format!("{failed} failed").red().bold().to_string()
        };
        write!(
            f,
            "Status: {}; total {total} tests in {time:.2}s: \
            {passed} passed; {fail}; {ignored} ignored; \
            {measured} measured; {filtered_out} filtered out",
            status(ok)
        )
    }
}

fn status(ok: bool) -> ColoredString {
    if ok {
        "OK".green().bold()
    } else {
        "FAIL".red().bold()
    }
}

impl Stats {
    /// Text at the end of root node.
    pub fn inlay_string(&self) -> String {
        let Stats {
            total,
            passed,
            failed,
            ignored,
            filtered_out,
            finished_in,
            ..
        } = *self;
        let time = finished_in.as_secs_f32();

        let mut part = Vec::with_capacity(4);
        if passed != 0 {
            part.push(format!("✅ {passed}"));
        };
        if failed != 0 {
            part.push(format!("❌ {failed}").red().to_string());
        };
        if ignored != 0 {
            part.push(format!("🔕 {ignored}"));
        };
        if filtered_out != 0 {
            part.push(format!("✂️ {filtered_out}"));
        };
        format!("{total} tests in {time:.2}s: {}", part.join("; "))
    }

    /// Root of test tree node depending on the test type.
    pub fn root_string(&self, pkg_name: Text) -> String {
        format!(
            "({}) {:} ... ({})",
            status(self.ok),
            pkg_name.blue().bold(),
            self.inlay_string().bold()
        )
    }

    /// Root of test tree node depending on the test type.
    pub fn subroot_string(&self, pkg_name: Text) -> String {
        format!(
            "({}) {} ... ({})",
            status(self.ok),
            pkg_name,
            self.inlay_string()
        )
    }
}

impl Default for Stats {
    fn default() -> Self {
        Stats {
            ok: true,
            total: 0,
            passed: 0,
            failed: 0,
            ignored: 0,
            measured: 0,
            filtered_out: 0,
            finished_in: Duration::from_secs(0),
        }
    }
}

impl std::ops::Add<&Stats> for &Stats {
    type Output = Stats;

    fn add(self, rhs: &Stats) -> Self::Output {
        Stats {
            ok: self.ok && rhs.ok,
            total: self.total + rhs.total,
            passed: self.passed + rhs.passed,
            failed: self.failed + rhs.failed,
            ignored: self.ignored + rhs.ignored,
            measured: self.measured + rhs.measured,
            filtered_out: self.filtered_out + rhs.filtered_out,
            finished_in: self.finished_in + rhs.finished_in,
        }
    }
}

impl std::ops::AddAssign<&Stats> for Stats {
    fn add_assign(&mut self, rhs: &Stats) {
        *self = &*self + rhs;
    }
}

/// Output from one test runner.
#[derive(Debug)]
pub struct ParsedCargoTestOutput<'s> {
    pub head: Text<'s>,
    pub tree: Vec<Text<'s>>,
    pub detail: Text<'s>,
}

pub fn parse_stderr(stderr: &str) -> Vec<TestRunner> {
    fn parse_stderr_inner<'s>(cap: &regex_lite::Captures<'s>) -> TestRunner<'s> {
        if let Some((path, pkg)) = cap.name("path").zip(cap.name("pkg")) {
            let path = path.as_str();
            let path_norm = Path::new(path);
            let ty = if cap.name("is_unit").is_some() {
                if path_norm
                    .components()
                    .take(2)
                    .map(Component::as_os_str)
                    .eq(["src", "lib.rs"])
                {
                    TestType::UnitLib
                } else {
                    TestType::UnitBin
                }
            } else {
                let Some(base_dir) = path_norm
                    .components()
                    .next()
                    .and_then(|p| p.as_os_str().to_str())
                else {
                    unimplemented!("failed to parse the type of test: {path:?}")
                };
                match base_dir {
                    "tests" => TestType::Tests,
                    "examples" => TestType::Examples,
                    "benches" => TestType::Benches,
                    _ => unimplemented!("failed to parse the type of test: {path:?}"),
                }
            };

            // e.g. target/debug/deps/cargo_pretty_test-xxxxxxxxxxxxxxxx
            let mut pkg_comp = Path::new(pkg.as_str()).components();
            match pkg_comp.next().map(|p| p.as_os_str() == "target") {
                Some(true) => (),
                _ => unimplemented!("failed to parse the location of test: {pkg:?}"),
            }
            let pkg = pkg_comp.nth(2).unwrap().as_os_str().to_str().unwrap();
            let pkg = &pkg[..pkg
                .find('-')
                .expect("pkg should be of `pkgname-hash` pattern")];
            TestRunner {
                ty,
                src: Src {
                    src_path: path,
                    bin_name: pkg,
                },
            }
        } else if let Some(s) = cap.name("doc").map(|m| m.as_str()) {
            TestRunner {
                ty: TestType::Doc,
                src: Src {
                    src_path: s,
                    bin_name: s,
                },
            }
        } else {
            unimplemented!();
        }
    }
    re().ty
        .captures_iter(stderr)
        .map(|cap| parse_stderr_inner(&cap))
        .collect::<Vec<_>>()
}

pub fn parse_stdout(stdout: &str) -> Vec<TestInfo> {
    fn parse_stdout_except_head(raw: &str) -> Option<(Vec<Text>, Text, Stats, Text)> {
        fn parse_tree_detail(text: &str) -> (Vec<Text>, Text) {
            let line: Vec<_> = re().tree.find_iter(text).collect();
            let tree_end = line.last().map_or(0, |cap| cap.end() + 1);
            let mut tree: Vec<_> = line.into_iter().map(|cap| cap.as_str()).collect();
            tree.sort_unstable();
            (tree, text[tree_end..].trim())
        }

        if raw.is_empty() {
            None
        } else {
            let (tree, detail) = parse_tree_detail(raw);
            let cap = re().stats.captures(detail)?;
            let stats = Stats {
                ok: cap.name("ok").map(|ok| ok.as_str() == "ok")?,
                total: tree.len().try_into().ok()?,
                passed: cap.name("passed")?.as_str().parse().ok()?,
                failed: cap.name("failed")?.as_str().parse().ok()?,
                ignored: cap.name("ignored")?.as_str().parse().ok()?,
                measured: cap.name("measured")?.as_str().parse().ok()?,
                filtered_out: cap.name("filtered")?.as_str().parse().ok()?,
                finished_in: Duration::from_secs_f32(cap.name("time")?.as_str().parse().ok()?),
            };
            let stats_start = cap.get(0)?.start();
            Some((tree, detail[..stats_start].trim(), stats, raw))
        }
    }

    let split: Vec<_> = re()
        .head
        .captures_iter(stdout)
        .filter_map(|cap| {
            let full = cap.get(0)?;
            Some((
                full.start(),
                full.as_str(),
                cap.name("amount")?.as_str().parse::<u32>().ok()?,
            ))
        })
        .collect();
    assert!(
        !split.is_empty(),
        "{stdout} should contain `running (?P<amount>\\d+) tests?` pattern"
    );
    let parsed_stdout = if split.len() == 1 {
        vec![parse_stdout_except_head(stdout).unwrap()]
    } else {
        let start = split.iter().map(|v| v.0);
        let end = start.clone().skip(1).chain([stdout.len()]);
        start
            .zip(end)
            .filter_map(|(a, b)| {
                let src = &stdout[a..b];
                parse_stdout_except_head(src)
            })
            .collect::<Vec<_>>()
    };

    // check the amount of tests
    let parsed_amount_from_head: Vec<_> = split.iter().map(|v| v.2).collect();
    let stats_total: Vec<_> = parsed_stdout.iter().map(|v| v.2.total).collect();
    assert_eq!(
        parsed_amount_from_head, stats_total,
        "the parsed amount of running tests {parsed_amount_from_head:?} \
         should equal to the number in stats.total {stats_total:?}"
    );

    split
        .iter()
        .zip(parsed_stdout)
        .map(|(head_info, v)| TestInfo {
            parsed: ParsedCargoTestOutput {
                head: head_info.1,
                tree: v.0,
                detail: v.1,
            },
            stats: v.2,
            raw: v.3,
        })
        .collect()
}