// Copyright (C) 2023 The Android Open Source Project
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
use std::{fmt::Display, fs::write, path::Path, str::from_utf8};
use crate::{
crates_with_multiple_versions, crates_with_single_version, CompatibleVersionPair, Crate,
CrateCollection, NameAndVersionMap, NamedAndVersioned, VersionMatch, VersionPair,
};
use anyhow::Result;
use serde::Serialize;
use tinytemplate::TinyTemplate;
static SIZE_REPORT_TEMPLATE: &'static str = include_str!("templates/size_report.html.template");
static TABLE_TEMPLATE: &'static str = include_str!("templates/table.html.template");
static CRATE_HEALTH_REPORT_TEMPLATE: &'static str =
include_str!("templates/crate_health.html.template");
static MIGRATION_REPORT_TEMPLATE: &'static str =
include_str!("templates/migration_report.html.template");
pub struct ReportEngine<'template> {
tt: TinyTemplate<'template>,
}
fn len_formatter(value: &serde_json::Value, out: &mut String) -> tinytemplate::error::Result<()> {
match value {
serde_json::Value::Array(a) => {
out.push_str(&format!("{}", a.len()));
Ok(())
}
_ => Err(tinytemplate::error::Error::GenericError {
msg: "Can only use length formatter on an array".to_string(),
}),
}
}
fn linkify(text: &dyn Display, url: &dyn Display) -> String {
format!("{}", url, text)
}
impl<'template> ReportEngine<'template> {
pub fn new() -> Result> {
let mut tt = TinyTemplate::new();
tt.add_template("size_report", SIZE_REPORT_TEMPLATE)?;
tt.add_template("table", TABLE_TEMPLATE)?;
tt.add_template("crate_health", CRATE_HEALTH_REPORT_TEMPLATE)?;
tt.add_template("migration", MIGRATION_REPORT_TEMPLATE)?;
tt.add_formatter("len", len_formatter);
Ok(ReportEngine { tt })
}
pub fn size_report(&self, cc: &CrateCollection) -> Result {
let num_crates = cc.num_crates();
let crates_with_single_version = cc.filter_versions(&crates_with_single_version).count();
Ok(self.tt.render(
"size_report",
&SizeReport {
num_crates,
crates_with_single_version,
crates_with_multiple_versions: num_crates - crates_with_single_version,
num_dirs: cc.map_field().len(),
},
)?)
}
pub fn table<'a>(&self, crates: impl Iterator- ) -> Result {
let mut table = Table::new(&[&"Crate", &"Version", &"Path"]);
for krate in crates {
table.add_row(&[
&linkify(&krate.name(), &krate.crates_io_url()),
&krate.version().to_string(),
&krate
.aosp_url()
.map_or(format!("{}", krate.path()), |url| linkify(&krate.path(), &url)),
]);
}
Ok(self.tt.render("table", &table)?)
}
pub fn health_table<'a>(&self, crates: impl Iterator
- ) -> Result {
let mut table = Table::new(&[
&"Crate",
&"Version",
&"Path",
&"Has Android.bp",
&"Generate Android.bp succeeds",
&"Android.bp unchanged",
&"Has cargo_embargo.json",
&"On migration denylist",
]);
table.set_vertical_headers();
for krate in crates {
table.add_row(&[
&linkify(&krate.name(), &krate.crates_io_url()),
&krate.version().to_string(),
&krate
.aosp_url()
.map_or(format!("{}", krate.path()), |url| linkify(&krate.path(), &url)),
&prefer_yes(krate.android_bp().abs().exists()),
&prefer_yes_or_summarize(
krate.generate_android_bp_success(),
krate
.generate_android_bp_output()
.map_or("Error".to_string(), |o| {
format!(
"STDOUT:\n{}\n\nSTDERR:\n{}",
from_utf8(&o.stdout).unwrap_or("Error"),
from_utf8(&o.stderr).unwrap_or("Error")
)
})
.as_str(),
),
&prefer_yes_or_summarize(
krate.android_bp_unchanged(),
krate
.android_bp_diff()
.map_or("Error", |o| from_utf8(&o.stdout).unwrap_or("Error")),
),
&prefer_yes(krate.cargo_embargo_json().abs().exists()),
&prefer_no(krate.is_migration_denied()),
]);
}
Ok(self.tt.render("table", &table)?)
}
pub fn migratable_table<'a>(
&self,
crate_pairs: impl Iterator
- >,
) -> Result {
let mut table = Table::new(&[&"Crate", &"Old Version", &"New Version", &"Path"]);
for crate_pair in crate_pairs {
let source = crate_pair.source;
let dest = crate_pair.dest;
let dest_version = if source.version() == dest.version() {
"".to_string()
} else {
dest.version().to_string()
};
table.add_row(&[
&linkify(&source.name(), &source.crates_io_url()),
&source.version().to_string(),
&dest_version,
&source
.aosp_url()
.map_or(format!("{}", source.path()), |url| linkify(&source.path(), &url)),
]);
}
Ok(self.tt.render("table", &table)?)
}
pub fn migration_ineligible_table<'a>(
&self,
crates: impl Iterator
- ,
) -> Result {
let mut table = Table::new(&[
&"Crate",
&"Version",
&"Path",
&"In crates.io",
&"Denylisted",
&"Has Android.bp",
&"Has cargo_embargo.json",
]);
table.set_vertical_headers();
for krate in crates {
table.add_row(&[
&linkify(&krate.name(), &krate.crates_io_url()),
&krate.version().to_string(),
&krate
.aosp_url()
.map_or(format!("{}", krate.path()), |url| linkify(&krate.path(), &url)),
&prefer_yes(krate.is_crates_io()),
&prefer_no(krate.is_migration_denied()),
&prefer_yes(krate.android_bp().abs().exists()),
&prefer_yes(krate.cargo_embargo_json().abs().exists()),
]);
}
Ok(self.tt.render("table", &table)?)
}
pub fn migration_eligible_table<'a>(
&self,
crate_pairs: impl Iterator
- >,
) -> Result {
let mut table = Table::new(&[
&"Crate",
&"Version",
&"Path",
&"Compatible version",
&"Patch succeeds",
&"Generate Android.bp succeeds",
&"Android.bp unchanged",
]);
table.set_vertical_headers();
for crate_pair in crate_pairs {
let source = crate_pair.source;
let maybe_dest = crate_pair.dest;
table.add_row(&[
&linkify(&source.name(), &source.crates_io_url()),
&source.version().to_string(),
&source
.aosp_url()
.map_or(format!("{}", source.path()), |url| linkify(&source.path(), &url)),
maybe_dest.map_or(&"None", |dest| {
if dest.version() != source.version() {
dest.version()
} else {
&""
}
}),
&prefer_yes(!maybe_dest.is_some_and(|dest| !dest.patch_success())),
&prefer_yes_or_summarize(
!maybe_dest.is_some_and(|dest| !dest.generate_android_bp_success()),
maybe_dest
.map_or("Error".to_string(), |dest| {
dest.generate_android_bp_output().map_or("Error".to_string(), |o| {
format!(
"STDOUT:\n{}\n\nSTDERR:\n{}",
from_utf8(&o.stdout).unwrap_or("Error"),
from_utf8(&o.stderr).unwrap_or("Error")
)
})
})
.as_str(),
),
&prefer_yes_or_summarize(
!maybe_dest.is_some_and(|dest| !dest.android_bp_unchanged()),
maybe_dest.map_or("Error", |dest| {
dest.android_bp_diff()
.map_or("Error", |o| from_utf8(&o.stdout).unwrap_or("Error"))
}),
),
]);
}
Ok(self.tt.render("table", &table)?)
}
pub fn health_report(
&self,
cc: &CrateCollection,
output_path: &impl AsRef,
) -> Result<()> {
let chr = CrateHealthReport {
crate_count: self.size_report(cc)?,
crate_multiversion: self.table(
cc.filter_versions(&crates_with_multiple_versions).map(|(_nv, krate)| krate),
)?,
healthy: self.table(
cc.map_field()
.iter()
.filter(|(_nv, krate)| krate.is_android_bp_healthy())
.map(|(_nv, krate)| krate),
)?,
unhealthy: self.health_table(
cc.map_field()
.iter()
.filter(|(_nv, krate)| !krate.is_android_bp_healthy())
.map(|(_nv, krate)| krate),
)?,
};
Ok(write(output_path, self.tt.render("crate_health", &chr)?)?)
}
pub fn migration_report(
&self,
m: &VersionMatch,
output_path: &impl AsRef,
) -> Result<()> {
let mr = MigrationReport {
migratable: self.migratable_table(m.migratable())?,
eligible: self.migration_eligible_table(m.eligible_but_not_migratable())?,
ineligible: self.migration_ineligible_table(m.ineligible())?,
superfluous: self.table(m.superfluous().map(|(_nv, krate)| krate))?,
};
Ok(write(output_path, self.tt.render("migration", &mr)?)?)
}
}
pub fn prefer_yes(p: bool) -> &'static str {
if p {
""
} else {
"No"
}
}
pub fn prefer_yes_or_summarize(p: bool, details: &str) -> String {
if p {
"".to_string()
} else {
format!("
No
{}
", details)
}
}
pub fn prefer_no(p: bool) -> &'static str {
if p {
"Yes"
} else {
""
}
}
#[derive(Serialize)]
pub struct SizeReport {
num_crates: usize,
crates_with_single_version: usize,
crates_with_multiple_versions: usize,
num_dirs: usize,
}
#[derive(Serialize)]
pub struct Table {
header: Vec,
rows: Vec>,
vertical: bool,
}
impl Table {
pub fn new(header: &[&dyn Display]) -> Table {
Table {
header: header.iter().map(|cell| format!("{}", cell)).collect::>(),
rows: Vec::new(),
vertical: false,
}
}
pub fn add_row(&mut self, row: &[&dyn Display]) {
self.rows.push(row.iter().map(|cell| format!("{}", cell)).collect::>());
}
pub fn set_vertical_headers(&mut self) {
self.vertical = true;
}
}
#[derive(Serialize)]
pub struct CrateHealthReport {
crate_count: String,
crate_multiversion: String,
healthy: String,
unhealthy: String,
}
#[derive(Serialize)]
pub struct MigrationReport {
migratable: String,
eligible: String,
ineligible: String,
superfluous: String,
}