flowey_lib_hvlite/
collect_unit_test_coverage.rs1use 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}