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