use std::time::Duration;
use std::{collections::HashMap, path::PathBuf, time::SystemTime};
use lcov::{Reader as LCOVReader, Record};
use tower_lsp::lsp_types::{
ColorInformation, DiagnosticSeverity, DocumentDiagnosticReport, FullDocumentDiagnosticReport,
RelatedFullDocumentDiagnosticReport, Url, WorkspaceDiagnosticReportResult,
};
use tower_lsp::{jsonrpc::Result, lsp_types::WorkspaceDiagnosticReport};
use crate::{FileCoverage, make_error};
#[derive(Debug)]
pub struct CoverageReport {
pub path: PathBuf,
pub mtime: SystemTime,
pub id: String,
pub db: HashMap<Url, FileCoverage>,
}
impl TryFrom<PathBuf> for CoverageReport {
type Error = std::io::Error;
fn try_from(path: PathBuf) -> std::result::Result<Self, Self::Error> {
let mtime = path.metadata()?.modified()?;
Ok(Self {
id: format!(
"{path:?}:{:?}",
mtime
.duration_since(SystemTime::UNIX_EPOCH)
.unwrap_or(Duration::ZERO)
.as_secs()
),
mtime,
path,
db: Default::default(),
})
}
}
impl CoverageReport {
pub fn is_outdated(&self) -> bool {
!self
.path
.metadata()
.is_ok_and(|m| m.modified().is_ok_and(|m| m == self.mtime))
}
pub fn load(&mut self, root_uri: &Url) -> Result<()> {
self.mtime = self
.path
.metadata()
.and_then(|m| m.modified())
.map_err(|err| make_error(format!("failed update metadata: {err:?}")))?;
let parser = match LCOVReader::open_file(&self.path) {
Ok(parser) => parser,
Err(err) => {
tracing::error!(?err, file = ?self.path, "parsing error");
return Err(make_error(format!("failed to parse: {err:?}")));
}
};
let mut file: Option<FileCoverage> = None;
for parser_result in parser {
let record = match parser_result {
Ok(record) => record,
Err(err) => {
tracing::error!(?err, file = ?self.path, "parsing error");
continue;
}
};
match record {
Record::SourceFile { path } => {
if let Some(cov) = file.take() {
tracing::info!(file = cov.uri.as_str(), "loaded");
self.db.insert(cov.uri.clone(), cov);
}
let url = match root_uri.join(&path.to_string_lossy()) {
Ok(url) => url,
Err(err) => {
tracing::error!(?err, "failed to make Url from {path:?}");
continue;
}
};
file = Some(FileCoverage::new(url));
}
Record::LineData { line, count, .. } => {
if let Some(cov) = file.as_mut() {
cov.add(line.saturating_sub(1), count);
}
}
_ => (),
}
}
if let Some(cov) = file.take() {
tracing::info!(file = cov.uri.as_str(), "loaded");
self.db.insert(cov.uri.clone(), cov);
}
Ok(())
}
pub fn create_workspace_diagnostic(&self) -> WorkspaceDiagnosticReportResult {
WorkspaceDiagnosticReportResult::Report(WorkspaceDiagnosticReport {
items: self
.db
.values()
.map(|v| v.create_workspace_document_diagnostic())
.collect(),
})
}
pub fn create_document_diagnostic(
&self,
uri: &Url,
last_id: &Option<String>,
) -> Option<DocumentDiagnosticReport> {
if last_id.as_ref().is_some_and(|last_id| last_id == &self.id) {
return None;
}
Some(
RelatedFullDocumentDiagnosticReport {
related_documents: None,
full_document_diagnostic_report: FullDocumentDiagnosticReport {
result_id: Some(self.id.clone()),
items: self
.db
.get(uri)
.map(|report| {
report.create_diagnostic(
Some(DiagnosticSeverity::INFORMATION),
Some(DiagnosticSeverity::WARNING),
)
})
.unwrap_or_default(),
},
}
.into(),
)
}
pub fn create_document_color(&self, uri: &Url) -> Vec<ColorInformation> {
match self.db.get(uri) {
Some(report) => report.create_document_color(),
None => Vec::default(),
}
}
}