use std::collections::BTreeMap;
use std::io::Write;
use std::path::{Path, PathBuf};
use anyhow::{Context, Result};
use serde::{Deserialize, Serialize};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "snake_case")]
pub enum ReportSeverity {
Info,
Warning,
Error,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FileReport {
pub source: PathBuf,
pub outputs: Vec<(String, PathBuf)>,
pub warnings: Vec<(ReportSeverity, String)>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct MigrationReport {
pub started_at: String,
pub input: PathBuf,
pub output: PathBuf,
pub files: Vec<FileReport>,
#[serde(skip)]
pub report_path: PathBuf,
#[serde(skip)]
pub by_file: BTreeMap<PathBuf, BTreeMap<String, Vec<(ReportSeverity, String)>>>,
}
impl MigrationReport {
pub fn new(input: PathBuf, output: PathBuf) -> Self {
Self {
started_at: timestamp(),
input,
output,
files: Vec::new(),
report_path: PathBuf::from("migration-report.json"),
by_file: BTreeMap::new(),
}
}
pub fn push(&mut self, f: FileReport) {
self.files.push(f);
}
pub fn record_warning(&mut self, file: &Path, target: &str, msg: String) {
let entry = self.by_file.entry(file.to_path_buf()).or_default();
let t = entry.entry(target.to_string()).or_default();
t.push((ReportSeverity::Warning, msg));
}
pub fn has_errors(&self) -> bool {
self.files.iter().any(|f| {
f.warnings
.iter()
.any(|(s, _)| *s == ReportSeverity::Error)
}) || self
.by_file
.values()
.flat_map(|m| m.values())
.flatten()
.any(|(s, _)| *s == ReportSeverity::Error)
}
}
pub fn write_human_report(r: &MigrationReport) {
println!("\n=== decuda migration report ===");
println!("started: {}", r.started_at);
println!("input: {}", r.input.display());
println!("output: {}", r.output.display());
println!("files: {}", r.files.len());
let total_warnings: usize = r.files.iter().map(|f| f.warnings.len()).sum::<usize>()
+ r.by_file
.values()
.flat_map(|m| m.values())
.map(|v| v.len())
.sum::<usize>();
println!("warnings: {total_warnings}");
for (file, by_target) in &r.by_file {
println!("\n {}", file.display());
for (target, warns) in by_target {
if warns.is_empty() {
continue;
}
println!(" [{target}]");
for (sev, msg) in warns {
println!(" - ({sev:?}) {msg}");
}
}
}
}
pub fn write_json_report(r: &MigrationReport, path: &Path) -> Result<()> {
let mut files = r.files.clone();
for f in &mut files {
if let Some(map) = r.by_file.get(&f.source) {
for (target, ws) in map {
f.warnings.extend(ws.iter().cloned());
let _ = target;
}
}
}
let merged = serde_json::json!({
"started_at": r.started_at,
"input": r.input,
"output": r.output,
"files": files,
});
let body = serde_json::to_string_pretty(&merged)?;
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).ok();
}
let mut f = std::fs::File::create(path)
.with_context(|| format!("create report file {}", path.display()))?;
f.write_all(body.as_bytes())?;
Ok(())
}
pub fn emit(report: &MigrationReport) -> Result<()> {
write_human_report(report);
write_json_report(report, &report.report_path)
}
fn timestamp() -> String {
use std::time::{SystemTime, UNIX_EPOCH};
let secs = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map(|d| d.as_secs())
.unwrap_or(0);
format!("{secs}")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn serde_roundtrip() {
let mut r = MigrationReport::new("in".into(), "out".into());
r.record_warning(&PathBuf::from("a.cu"), "hip", "x".into());
let merged = serde_json::json!({
"input": r.input,
"output": r.output,
"files": r.files,
});
let json = serde_json::to_string(&merged).unwrap();
assert!(json.contains("hip") || json.contains("in"));
}
#[test]
fn by_file_records_warnings() {
let mut r = MigrationReport::new("in".into(), "out".into());
r.record_warning(&PathBuf::from("a.cu"), "hip", "x".into());
let entry = r
.by_file
.get(&PathBuf::from("a.cu"))
.expect("by_file entry");
let hip = entry.get("hip").expect("hip target");
assert_eq!(hip.len(), 1);
assert_eq!(hip[0].1, "x");
}
}