1 // Copyright (C) 2023 The Android Open Source Project
2 //
3 // Licensed under the Apache License, Version 2.0 (the "License");
4 // you may not use this file except in compliance with the License.
5 // You may obtain a copy of the License at
6 //
7 // http://www.apache.org/licenses/LICENSE-2.0
8 //
9 // Unless required by applicable law or agreed to in writing, software
10 // distributed under the License is distributed on an "AS IS" BASIS,
11 // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 // See the License for the specific language governing permissions and
13 // limitations under the License.
14
15 use std::{fmt::Display, fs::write, path::Path, str::from_utf8};
16
17 use crate::{
18 crates_with_multiple_versions, crates_with_single_version, CompatibleVersionPair, Crate,
19 CrateCollection, NameAndVersionMap, NamedAndVersioned, VersionMatch, VersionPair,
20 };
21
22 use anyhow::Result;
23 use serde::Serialize;
24 use tinytemplate::TinyTemplate;
25
26 static SIZE_REPORT_TEMPLATE: &'static str = include_str!("templates/size_report.html.template");
27 static TABLE_TEMPLATE: &'static str = include_str!("templates/table.html.template");
28
29 static CRATE_HEALTH_REPORT_TEMPLATE: &'static str =
30 include_str!("templates/crate_health.html.template");
31 static MIGRATION_REPORT_TEMPLATE: &'static str =
32 include_str!("templates/migration_report.html.template");
33
34 pub struct ReportEngine<'template> {
35 tt: TinyTemplate<'template>,
36 }
37
len_formatter(value: &serde_json::Value, out: &mut String) -> tinytemplate::error::Result<()>38 fn len_formatter(value: &serde_json::Value, out: &mut String) -> tinytemplate::error::Result<()> {
39 match value {
40 serde_json::Value::Array(a) => {
41 out.push_str(&format!("{}", a.len()));
42 Ok(())
43 }
44 _ => Err(tinytemplate::error::Error::GenericError {
45 msg: "Can only use length formatter on an array".to_string(),
46 }),
47 }
48 }
49
linkify(text: &dyn Display, url: &dyn Display) -> String50 fn linkify(text: &dyn Display, url: &dyn Display) -> String {
51 format!("<a href=\"{}\">{}</a>", url, text)
52 }
53
54 impl<'template> ReportEngine<'template> {
new() -> Result<ReportEngine<'template>>55 pub fn new() -> Result<ReportEngine<'template>> {
56 let mut tt = TinyTemplate::new();
57 tt.add_template("size_report", SIZE_REPORT_TEMPLATE)?;
58 tt.add_template("table", TABLE_TEMPLATE)?;
59 tt.add_template("crate_health", CRATE_HEALTH_REPORT_TEMPLATE)?;
60 tt.add_template("migration", MIGRATION_REPORT_TEMPLATE)?;
61 tt.add_formatter("len", len_formatter);
62 Ok(ReportEngine { tt })
63 }
size_report(&self, cc: &CrateCollection) -> Result<String>64 pub fn size_report(&self, cc: &CrateCollection) -> Result<String> {
65 let num_crates = cc.num_crates();
66 let crates_with_single_version = cc.filter_versions(&crates_with_single_version).count();
67 Ok(self.tt.render(
68 "size_report",
69 &SizeReport {
70 num_crates,
71 crates_with_single_version,
72 crates_with_multiple_versions: num_crates - crates_with_single_version,
73 num_dirs: cc.map_field().len(),
74 },
75 )?)
76 }
table<'a>(&self, crates: impl Iterator<Item = &'a Crate>) -> Result<String>77 pub fn table<'a>(&self, crates: impl Iterator<Item = &'a Crate>) -> Result<String> {
78 let mut table = Table::new(&[&"Crate", &"Version", &"Path"]);
79 for krate in crates {
80 table.add_row(&[
81 &linkify(&krate.name(), &krate.crates_io_url()),
82 &krate.version().to_string(),
83 &krate
84 .aosp_url()
85 .map_or(format!("{}", krate.path()), |url| linkify(&krate.path(), &url)),
86 ]);
87 }
88 Ok(self.tt.render("table", &table)?)
89 }
health_table<'a>(&self, crates: impl Iterator<Item = &'a Crate>) -> Result<String>90 pub fn health_table<'a>(&self, crates: impl Iterator<Item = &'a Crate>) -> Result<String> {
91 let mut table = Table::new(&[
92 &"Crate",
93 &"Version",
94 &"Path",
95 &"Has Android.bp",
96 &"Generate Android.bp succeeds",
97 &"Android.bp unchanged",
98 &"Has cargo_embargo.json",
99 &"On migration denylist",
100 ]);
101 table.set_vertical_headers();
102 for krate in crates {
103 table.add_row(&[
104 &linkify(&krate.name(), &krate.crates_io_url()),
105 &krate.version().to_string(),
106 &krate
107 .aosp_url()
108 .map_or(format!("{}", krate.path()), |url| linkify(&krate.path(), &url)),
109 &prefer_yes(krate.android_bp().abs().exists()),
110 &prefer_yes_or_summarize(
111 krate.generate_android_bp_success(),
112 krate
113 .generate_android_bp_output()
114 .map_or("Error".to_string(), |o| {
115 format!(
116 "STDOUT:\n{}\n\nSTDERR:\n{}",
117 from_utf8(&o.stdout).unwrap_or("Error"),
118 from_utf8(&o.stderr).unwrap_or("Error")
119 )
120 })
121 .as_str(),
122 ),
123 &prefer_yes_or_summarize(
124 krate.android_bp_unchanged(),
125 krate
126 .android_bp_diff()
127 .map_or("Error", |o| from_utf8(&o.stdout).unwrap_or("Error")),
128 ),
129 &prefer_yes(krate.cargo_embargo_json().abs().exists()),
130 &prefer_no(krate.is_migration_denied()),
131 ]);
132 }
133 Ok(self.tt.render("table", &table)?)
134 }
migratable_table<'a>( &self, crate_pairs: impl Iterator<Item = CompatibleVersionPair<'a, Crate>>, ) -> Result<String>135 pub fn migratable_table<'a>(
136 &self,
137 crate_pairs: impl Iterator<Item = CompatibleVersionPair<'a, Crate>>,
138 ) -> Result<String> {
139 let mut table = Table::new(&[&"Crate", &"Old Version", &"New Version", &"Path"]);
140 for crate_pair in crate_pairs {
141 let source = crate_pair.source;
142 let dest = crate_pair.dest;
143 let dest_version = if source.version() == dest.version() {
144 "".to_string()
145 } else {
146 dest.version().to_string()
147 };
148 table.add_row(&[
149 &linkify(&source.name(), &source.crates_io_url()),
150 &source.version().to_string(),
151 &dest_version,
152 &source
153 .aosp_url()
154 .map_or(format!("{}", source.path()), |url| linkify(&source.path(), &url)),
155 ]);
156 }
157 Ok(self.tt.render("table", &table)?)
158 }
migration_ineligible_table<'a>( &self, crates: impl Iterator<Item = &'a Crate>, ) -> Result<String>159 pub fn migration_ineligible_table<'a>(
160 &self,
161 crates: impl Iterator<Item = &'a Crate>,
162 ) -> Result<String> {
163 let mut table = Table::new(&[
164 &"Crate",
165 &"Version",
166 &"Path",
167 &"In crates.io",
168 &"Denylisted",
169 &"Has Android.bp",
170 &"Has cargo_embargo.json",
171 ]);
172 table.set_vertical_headers();
173 for krate in crates {
174 table.add_row(&[
175 &linkify(&krate.name(), &krate.crates_io_url()),
176 &krate.version().to_string(),
177 &krate
178 .aosp_url()
179 .map_or(format!("{}", krate.path()), |url| linkify(&krate.path(), &url)),
180 &prefer_yes(krate.is_crates_io()),
181 &prefer_no(krate.is_migration_denied()),
182 &prefer_yes(krate.android_bp().abs().exists()),
183 &prefer_yes(krate.cargo_embargo_json().abs().exists()),
184 ]);
185 }
186 Ok(self.tt.render("table", &table)?)
187 }
migration_eligible_table<'a>( &self, crate_pairs: impl Iterator<Item = VersionPair<'a, Crate>>, ) -> Result<String>188 pub fn migration_eligible_table<'a>(
189 &self,
190 crate_pairs: impl Iterator<Item = VersionPair<'a, Crate>>,
191 ) -> Result<String> {
192 let mut table = Table::new(&[
193 &"Crate",
194 &"Version",
195 &"Path",
196 &"Compatible version",
197 &"Patch succeeds",
198 &"Generate Android.bp succeeds",
199 &"Android.bp unchanged",
200 ]);
201 table.set_vertical_headers();
202 for crate_pair in crate_pairs {
203 let source = crate_pair.source;
204 let maybe_dest = crate_pair.dest;
205 table.add_row(&[
206 &linkify(&source.name(), &source.crates_io_url()),
207 &source.version().to_string(),
208 &source
209 .aosp_url()
210 .map_or(format!("{}", source.path()), |url| linkify(&source.path(), &url)),
211 maybe_dest.map_or(&"None", |dest| {
212 if dest.version() != source.version() {
213 dest.version()
214 } else {
215 &""
216 }
217 }),
218 &prefer_yes(!maybe_dest.is_some_and(|dest| !dest.patch_success())),
219 &prefer_yes_or_summarize(
220 !maybe_dest.is_some_and(|dest| !dest.generate_android_bp_success()),
221 maybe_dest
222 .map_or("Error".to_string(), |dest| {
223 dest.generate_android_bp_output().map_or("Error".to_string(), |o| {
224 format!(
225 "STDOUT:\n{}\n\nSTDERR:\n{}",
226 from_utf8(&o.stdout).unwrap_or("Error"),
227 from_utf8(&o.stderr).unwrap_or("Error")
228 )
229 })
230 })
231 .as_str(),
232 ),
233 &prefer_yes_or_summarize(
234 !maybe_dest.is_some_and(|dest| !dest.android_bp_unchanged()),
235 maybe_dest.map_or("Error", |dest| {
236 dest.android_bp_diff()
237 .map_or("Error", |o| from_utf8(&o.stdout).unwrap_or("Error"))
238 }),
239 ),
240 ]);
241 }
242 Ok(self.tt.render("table", &table)?)
243 }
health_report( &self, cc: &CrateCollection, output_path: &impl AsRef<Path>, ) -> Result<()>244 pub fn health_report(
245 &self,
246 cc: &CrateCollection,
247 output_path: &impl AsRef<Path>,
248 ) -> Result<()> {
249 let chr = CrateHealthReport {
250 crate_count: self.size_report(cc)?,
251 crate_multiversion: self.table(
252 cc.filter_versions(&crates_with_multiple_versions).map(|(_nv, krate)| krate),
253 )?,
254 healthy: self.table(
255 cc.map_field()
256 .iter()
257 .filter(|(_nv, krate)| krate.is_android_bp_healthy())
258 .map(|(_nv, krate)| krate),
259 )?,
260 unhealthy: self.health_table(
261 cc.map_field()
262 .iter()
263 .filter(|(_nv, krate)| !krate.is_android_bp_healthy())
264 .map(|(_nv, krate)| krate),
265 )?,
266 };
267 Ok(write(output_path, self.tt.render("crate_health", &chr)?)?)
268 }
migration_report( &self, m: &VersionMatch<CrateCollection>, output_path: &impl AsRef<Path>, ) -> Result<()>269 pub fn migration_report(
270 &self,
271 m: &VersionMatch<CrateCollection>,
272 output_path: &impl AsRef<Path>,
273 ) -> Result<()> {
274 let mr = MigrationReport {
275 migratable: self.migratable_table(m.migratable())?,
276 eligible: self.migration_eligible_table(m.eligible_but_not_migratable())?,
277 ineligible: self.migration_ineligible_table(m.ineligible())?,
278 superfluous: self.table(m.superfluous().map(|(_nv, krate)| krate))?,
279 };
280 Ok(write(output_path, self.tt.render("migration", &mr)?)?)
281 }
282 }
283
prefer_yes(p: bool) -> &'static str284 pub fn prefer_yes(p: bool) -> &'static str {
285 if p {
286 ""
287 } else {
288 "No"
289 }
290 }
prefer_yes_or_summarize(p: bool, details: &str) -> String291 pub fn prefer_yes_or_summarize(p: bool, details: &str) -> String {
292 if p {
293 "".to_string()
294 } else {
295 format!("<details><summary>No</summary><pre>{}</pre></details>", details)
296 }
297 }
prefer_no(p: bool) -> &'static str298 pub fn prefer_no(p: bool) -> &'static str {
299 if p {
300 "Yes"
301 } else {
302 ""
303 }
304 }
305
306 #[derive(Serialize)]
307 pub struct SizeReport {
308 num_crates: usize,
309 crates_with_single_version: usize,
310 crates_with_multiple_versions: usize,
311 num_dirs: usize,
312 }
313
314 #[derive(Serialize)]
315 pub struct Table {
316 header: Vec<String>,
317 rows: Vec<Vec<String>>,
318 vertical: bool,
319 }
320
321 impl Table {
new(header: &[&dyn Display]) -> Table322 pub fn new(header: &[&dyn Display]) -> Table {
323 Table {
324 header: header.iter().map(|cell| format!("{}", cell)).collect::<Vec<_>>(),
325 rows: Vec::new(),
326 vertical: false,
327 }
328 }
add_row(&mut self, row: &[&dyn Display])329 pub fn add_row(&mut self, row: &[&dyn Display]) {
330 self.rows.push(row.iter().map(|cell| format!("{}", cell)).collect::<Vec<_>>());
331 }
set_vertical_headers(&mut self)332 pub fn set_vertical_headers(&mut self) {
333 self.vertical = true;
334 }
335 }
336
337 #[derive(Serialize)]
338 pub struct CrateHealthReport {
339 crate_count: String,
340 crate_multiversion: String,
341 healthy: String,
342 unhealthy: String,
343 }
344 #[derive(Serialize)]
345 pub struct MigrationReport {
346 migratable: String,
347 eligible: String,
348 ineligible: String,
349 superfluous: String,
350 }
351