aboutsummaryrefslogtreecommitdiff
path: root/mingling_ci/src/task/run.rs
blob: bf83d3bdc647571a0e63908f4a37add43d334f73 (plain) (blame)
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
use std::ffi::OsString;
use std::path::Path;

use colored::Colorize;
use indicatif::{ProgressBar, ProgressStyle};

use crate::reporter::{self, ReportResult};

/// The manifest's parent directory, e.g. `./mingling` — the report location
/// for a crate-based item.
pub(crate) fn location(path: &Path) -> String {
    path.parent()
        .map_or_else(|| ".".to_string(), |d| d.to_string_lossy().into_owned())
}

/// Outcome of a `cargo` subcommand.
struct CargoResult {
    ok: bool,
    exit_code: Option<i32>,
    output: String,
}

/// Runs the given cargo task list in parallel.
///
/// Each task is an `(item, location, argv)` triple; progress and failures go
/// to stderr: a failing task prints its output immediately and writes its
/// report entry at the same time. Returns the number of failing tasks.
pub(crate) async fn run_parallel_checks(
    task: &str,
    phase: &str,
    tasks: Vec<(String, String, Vec<OsString>)>,
) -> usize {
    reporter::set_task(task);

    let n = tasks.len();
    let pb = ProgressBar::new(n as u64);
    let padding = " ".repeat(12usize.saturating_sub(phase.len()));
    let styled_prefix = format!("{}{}", padding, phase.bold().bright_cyan());
    pb.set_style(
        ProgressStyle::default_bar()
            .template(&format!(
                "{styled_prefix} [{{bar:28}}] {{pos}}/{{len}}: {{msg}}"
            ))
            .unwrap()
            .progress_chars("=> "),
    );

    // Run each task in parallel.
    let mut set = tokio::task::JoinSet::new();
    for (item, location, args) in tasks {
        set.spawn(async move { (item, location, run_cargo(args).await) });
    }

    let mut fail_count = 0;
    while let Some(joined) = set.join_next().await {
        let Ok((item, location, result)) = joined else {
            continue;
        };
        pb.inc(1);
        pb.set_message(item.clone());

        if result.ok {
            reporter::export(&item, &location, ReportResult::Ok);
        } else {
            fail_count += 1;
            // Failures print to stderr immediately (bar suspended to avoid
            // interleaving) and write their report entry at the same time.
            pb.suspend(|| {
                eprintln!(
                    "{}: {} failed{}",
                    phase.bold().bright_cyan(),
                    item,
                    result
                        .exit_code
                        .map_or_else(String::new, |c| format!(" (exit code {c})"))
                );
                for line in result.output.lines() {
                    eprintln!("  {line}");
                }
            });
            reporter::export(&item, &location, ReportResult::Error(result.output));
        }
    }

    pb.finish_and_clear();
    reporter::flush();
    fail_count
}

/// Runs a `cargo` subcommand, capturing its output.
/// Runs a cargo subcommand (`argv[0]` is the program), capturing its output.
async fn run_cargo(argv: Vec<OsString>) -> CargoResult {
    let mut argv = argv.into_iter();
    let Some(program) = argv.next() else {
        return CargoResult {
            ok: false,
            exit_code: None,
            output: "empty command".to_string(),
        };
    };

    let output = tokio::process::Command::new(program)
        .args(argv)
        .output()
        .await;

    match output {
        Ok(output) => {
            let mut log = String::from_utf8_lossy(&output.stdout).into_owned();
            log.push_str(&String::from_utf8_lossy(&output.stderr));
            CargoResult {
                ok: output.status.success(),
                exit_code: output.status.code(),
                output: log,
            }
        }
        Err(e) => CargoResult {
            ok: false,
            exit_code: None,
            output: format!("failed to run cargo: {e}"),
        },
    }
}