Skip to main content

flowey_lib_hvlite/
collect_unit_test_coverage.rs

1// Copyright (c) Microsoft Corporation.
2// Licensed under the MIT License.
3
4//! Collect local, per-test LLVM coverage using the unit-test build plan.
5
6use flowey::node::prelude::*;
7use flowey_lib_common::run_cargo_nextest_run::build_params::NextestBuildParams;
8
9#[derive(Serialize, Deserialize)]
10pub struct Options {
11    pub output: PathBuf,
12    pub filter: Option<String>,
13    pub base: Option<String>,
14}
15
16flowey_request! {
17    pub enum Request {
18        Begin {
19            repo_root: PathBuf,
20            done: WriteVar<SideEffect>,
21        },
22        Collect {
23            xtask: ReadVar<crate::build_xtask::XtaskOutput>,
24            runs: Vec<(String, NextestBuildParams)>,
25            options: Options,
26            working_dir: ReadVar<PathBuf>,
27            pre_run_deps: Vec<ReadVar<SideEffect>>,
28            done: WriteVar<SideEffect>,
29        },
30    }
31}
32
33new_flow_node!(struct Node);
34
35impl FlowNode for Node {
36    type Request = Request;
37
38    fn imports(ctx: &mut ImportCtx<'_>) {
39        ctx.import::<flowey_lib_common::cfg_cargo_common_flags::Node>();
40        ctx.import::<flowey_lib_common::install_rust::Node>();
41        ctx.import::<flowey_lib_common::install_cargo_nextest::Node>();
42        ctx.import::<flowey_lib_common::install_cargo_llvm_cov::Node>();
43    }
44
45    fn emit(requests: Vec<Request>, ctx: &mut NodeCtx<'_>) -> anyhow::Result<()> {
46        if requests.is_empty() {
47            return Ok(());
48        }
49        anyhow::ensure!(
50            matches!(ctx.backend(), FlowBackend::Local),
51            "unit coverage currently supports only the local backend"
52        );
53        for request in requests {
54            let (xtask, runs, options, working_dir, pre_run_deps, done) = match request {
55                Request::Begin { repo_root, done } => {
56                    ctx.emit_rust_step("clear active unit coverage", |ctx| {
57                        done.claim(ctx);
58                        move |_rt| match fs_err::remove_file(
59                            repo_root.join("target/unit-coverage/latest.json"),
60                        ) {
61                            Err(error) if error.kind() == std::io::ErrorKind::NotFound => Ok(()),
62                            result => result.context("clearing previous active unit coverage"),
63                        }
64                    });
65                    continue;
66                }
67                Request::Collect {
68                    xtask,
69                    runs,
70                    options,
71                    working_dir,
72                    pre_run_deps,
73                    done,
74                } => (xtask, runs, options, working_dir, pre_run_deps, done),
75            };
76            let flags = ctx.reqv(flowey_lib_common::cfg_cargo_common_flags::Request::GetFlags);
77            ctx.req(flowey_lib_common::install_rust::Request::InstallComponent(
78                format!("llvm-tools-{}", target_lexicon::Triple::host(),),
79            ));
80            let toolchain = ctx.reqv(flowey_lib_common::install_rust::Request::GetRustupToolchain);
81            let nextest = ctx.reqv(flowey_lib_common::install_cargo_nextest::Request);
82            let llvm_cov = ctx.reqv(flowey_lib_common::install_cargo_llvm_cov::Request);
83            ctx.emit_rust_step("collect local unit test coverage", |ctx| {
84                done.claim(ctx);
85                pre_run_deps.claim(ctx);
86                nextest.claim(ctx);
87                llvm_cov.claim(ctx);
88                let flags = flags.claim(ctx);
89                let toolchain = toolchain.claim(ctx);
90                let working_dir = working_dir.claim(ctx);
91                let xtask = xtask.claim(ctx);
92                let runs: Vec<_> = runs
93                    .into_iter()
94                    .map(|(name, params)| (name, params.claim(ctx)))
95                    .collect();
96                move |rt| {
97                    let working_dir = rt.read(working_dir);
98                    let xtask = match rt.read(xtask) {
99                        crate::build_xtask::XtaskOutput::LinuxBin { bin, .. } => bin,
100                        crate::build_xtask::XtaskOutput::WindowsBin { exe, .. } => exe,
101                    };
102                    let toolchain = rt.read(toolchain);
103                    let script = working_dir.join("repo_support/unit_test_coverage.py");
104                    let mut config = fs_err::read_to_string(
105                        working_dir.join(".config/nextest.toml"),
106                    )?.parse::<toml_edit::DocumentMut>()?;
107                    if let Some(profiles) = config.get("profile").and_then(|v| v.as_table()) {
108                        for (_, profile) in profiles {
109                            if let Some(scripts) = profile
110                                .get("scripts")
111                                .and_then(|v| v.as_array_of_tables())
112                            {
113                                anyhow::ensure!(
114                                    scripts.iter().all(|s| !s.contains_key("run-wrapper")),
115                                    "unit coverage cannot replace an existing nextest run-wrapper"
116                                );
117                            }
118                        }
119                    }
120                    let experimental = config
121                        .entry("experimental")
122                        .or_insert(toml_edit::value(toml_edit::Array::new()))
123                        .as_array_mut()
124                        .context("nextest experimental features must be an array")?;
125                    if !experimental.iter().any(|v| v.as_str() == Some("wrapper-scripts")) {
126                        experimental.push("wrapper-scripts");
127                    }
128                    let mut command = toml_edit::Array::new();
129                    for arg in ["python3", &script.display().to_string(), "nextest-runner"] {
130                        command.push(arg);
131                    }
132                    config["scripts"]["wrapper"]["unit-coverage"]["command"] =
133                        toml_edit::value(command);
134                    config["scripts"]["wrapper"]["unit-coverage"]["target-runner"] =
135                        toml_edit::value("within-wrapper");
136                    let mut rule = toml_edit::Table::new();
137                    rule["filter"] = toml_edit::value("all()");
138                    rule["run-wrapper"] = toml_edit::value("unit-coverage");
139                    config["profile"]["agent"]
140                        .as_table_mut()
141                        .context("missing nextest agent profile")?
142                        .entry("scripts")
143                        .or_insert(toml_edit::Item::ArrayOfTables(toml_edit::ArrayOfTables::new()))
144                        .as_array_of_tables_mut()
145                        .context("nextest scripts must be an array of tables")?
146                        .push(rule);
147                    let config_file = rt.sh.current_dir().join("unit-coverage-nextest.toml");
148                    fs_err::write(&config_file, config.to_string())?;
149                    let mut plan = Vec::new();
150                    for (name, params) in runs {
151                        let (args, env) =
152                            flowey_lib_common::gen_cargo_nextest_run_cmd::cargo_nextest_build_args_and_env(
153                                rt.read(flags.clone()),
154                                params.profile,
155                                params.target,
156                                rt.read(params.packages),
157                                params.features,
158                                params.no_default_features,
159                                rt.read(params.extra_env),
160                            );
161                        plan.push(serde_json::json!({ "name": name, "args": args, "env": env }));
162                    }
163                    let plan_file = rt.sh.current_dir().join("unit-coverage-plan.json");
164                    fs_err::write(
165                        &plan_file,
166                        serde_json::to_vec(&serde_json::json!({
167                            "runs": plan,
168                            "toolchain": toolchain,
169                            "filter": options.filter,
170                            "nextest_config": config_file,
171                            "base": options.base,
172                            "xtask": xtask,
173                        }))?,
174                    )?;
175                    let output = options.output;
176                    rt.sh.change_dir(working_dir);
177                    flowey::shell_cmd!(
178                        rt,
179                        "python3 {script} collect --plan {plan_file} --output {output}"
180                    )
181                    .run()
182                    .context("unit coverage failed; see report.json for partial results if available")?;
183                    Ok(())
184                }
185            });
186        }
187        Ok(())
188    }
189}