From 6006a69b4402ce869f004e23dd790906cb7248f6 Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Thu, 9 May 2024 06:57:51 -0700 Subject: [PATCH 01/15] initial basic cobertura coverage gutter --- Cargo.lock | 11 +++ helix-view/Cargo.toml | 1 + helix-view/src/coverage.rs | 162 +++++++++++++++++++++++++++++++++++++ helix-view/src/document.rs | 2 +- helix-view/src/editor.rs | 7 +- helix-view/src/gutter.rs | 49 ++++++++++- helix-view/src/lib.rs | 1 + 7 files changed, 229 insertions(+), 4 deletions(-) create mode 100644 helix-view/src/coverage.rs diff --git a/Cargo.lock b/Cargo.lock index 3fbb80c90..2d9be36f0 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1557,6 +1557,7 @@ dependencies = [ "once_cell", "parking_lot", "rustix 1.0.3", + "quick-xml", "serde", "serde_json", "slotmap", @@ -2121,6 +2122,16 @@ dependencies = [ "unicase", ] +[[package]] +name = "quick-xml" +version = "0.31.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1004a344b30a54e2ee58d66a71b32d2db2feb0a31f9a2d302bf0536f15de2a33" +dependencies = [ + "memchr", + "serde", +] + [[package]] name = "quickcheck" version = "1.0.3" diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index bcee1a0a7..bbc8929c7 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -51,6 +51,7 @@ log = "~0.4" parking_lot.workspace = true thiserror.workspace = true +quick-xml = { version = "0.31.0", features = ["serialize"] } [target.'cfg(windows)'.dependencies] clipboard-win = { version = "5.4", features = ["std"] } diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs new file mode 100644 index 000000000..cafca5d89 --- /dev/null +++ b/helix-view/src/coverage.rs @@ -0,0 +1,162 @@ +use quick_xml::de::from_reader; +use serde::Deserialize; +use std::collections::HashMap; +use std::fs::File; +use std::io::BufReader; + +pub struct Coverage { + pub files: HashMap, +} + +pub struct FileCoverage { + pub lines: HashMap, +} + +#[derive(Deserialize, Debug)] +struct RawCoverage { + #[serde(rename = "@version")] + version: String, + sources: Sources, + packages: Packages, +} + +#[derive(Deserialize, Debug)] +struct Sources { + source: Vec, +} + +#[derive(Deserialize, Debug)] +struct Source { + #[serde(rename = "$value")] + name: String, +} + +#[derive(Deserialize, Debug)] +struct Packages { + package: Vec, +} + +#[derive(Deserialize, Debug)] +struct Package { + #[serde(rename = "@name")] + name: String, + classes: Classes, +} + +#[derive(Deserialize, Debug)] +struct Classes { + class: Vec, +} + +#[derive(Deserialize, Debug)] +struct Class { + #[serde(rename = "@name")] + name: String, + #[serde(rename = "@filename")] + filename: String, + lines: Lines, +} + +#[derive(Deserialize, Debug)] +struct Lines { + line: Vec, +} + +#[derive(Deserialize, Debug)] +struct Line { + #[serde(rename = "@number")] + number: u32, + #[serde(rename = "@hits")] + hits: u32, +} + +pub fn parse(path: std::path::PathBuf) -> Option { + let file = File::open(path).ok()?; + let reader = BufReader::new(file); + let tmp: RawCoverage = from_reader(reader).ok()?; + Some(tmp.into()) +} + +impl From for Coverage { + fn from(coverage: RawCoverage) -> Self { + let mut files = HashMap::new(); + for package in coverage.packages.package { + for class in package.classes.class { + let mut lines = HashMap::new(); + for line in class.lines.line { + lines.insert(line.number - 1, line.hits > 0); + } + for source in &coverage.sources.source { + let path: std::path::PathBuf = [&source.name, &class.filename].iter().collect(); + if path.exists() { + files.insert(path, FileCoverage { lines }); + break; + } + } + } + } + Coverage { files } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use quick_xml::de::from_str; + use std::path::PathBuf; + static TEST_STRING: &str = r#" + + + a_src + + + + + + + + + + + + + + + + + + + +"#; + + #[test] + fn test_deserialize_raw_coverage_from_string() { + let result: RawCoverage = from_str(TEST_STRING).unwrap(); + println!("result is {:?}", result); + assert_eq!(result.version, "7.3.0"); + assert_eq!(result.sources.source[0].name, "a_src"); + assert_eq!(result.packages.package[0].name, "a package"); + let class = &result.packages.package[0].classes.class[0]; + assert_eq!(class.name, "a class"); + assert_eq!(class.filename, "file.ext"); + assert_eq!(class.lines.line[0].number, 3); + assert_eq!(class.lines.line[0].hits, 1); + assert_eq!(class.lines.line[1].number, 5); + assert_eq!(class.lines.line[1].hits, 0); + } + + #[test] + fn test_convert_raw_coverage_to_coverage() { + let tmp: RawCoverage = from_str(TEST_STRING).unwrap(); + let result: Coverage = tmp.into(); + assert_eq!(result.files.len(), 2); + let first = result.files.get(&PathBuf::from("a_src/file.ext")).unwrap(); + assert!(first.lines.get(&0).is_none()); + assert_eq!(first.lines.get(&3), Some(&true)); + assert_eq!(first.lines.get(&5), Some(&false)); + let second = result.files.get(&PathBuf::from("a_src/other.ext")).unwrap(); + assert!(second.lines.get(&3).is_none()); + assert_eq!(second.lines.get(&1), Some(&false)); + assert_eq!(second.lines.get(&7), Some(&true)); + } +} diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 41c9ee1ef..5c68c62ac 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -152,7 +152,7 @@ pub struct Document { /// update from the LSP pub inlay_hints_oudated: bool, - path: Option, + pub path: Option, relative_path: OnceCell>, encoding: &'static encoding::Encoding, has_bom: bool, diff --git a/helix-view/src/editor.rs b/helix-view/src/editor.rs index be2218997..52e6ede09 100644 --- a/helix-view/src/editor.rs +++ b/helix-view/src/editor.rs @@ -420,7 +420,7 @@ pub fn get_terminal_provider() -> Option { }) } -#[cfg(not(any(windows, target_arch = "wasm32")))] +#[cfg(not(any(windows, target_os = "wasm32")))] pub fn get_terminal_provider() -> Option { use helix_stdx::env::{binary_exists, env_var_is_set}; @@ -711,6 +711,8 @@ pub enum GutterType { Spacer, /// Highlight local changes Diff, + /// Highlight local changes + Coverage, } impl std::str::FromStr for GutterType { @@ -722,8 +724,9 @@ impl std::str::FromStr for GutterType { "spacer" => Ok(Self::Spacer), "line-numbers" => Ok(Self::LineNumbers), "diff" => Ok(Self::Diff), + "coverage" => Ok(Self::Coverage), _ => anyhow::bail!( - "Gutter type can only be `diagnostics`, `spacer`, `line-numbers` or `diff`." + "Gutter type can only be `diagnostics`, `spacer`, `line-numbers`, `diff` or `coverage`." ), } } diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 665a78bcc..f47343207 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -1,8 +1,9 @@ -use std::fmt::Write; +use std::{fmt::Write, path::PathBuf}; use helix_core::syntax::LanguageServerFeature; use crate::{ + coverage, editor::GutterType, graphics::{Style, UnderlineStyle}, Document, Editor, Theme, View, @@ -32,6 +33,7 @@ impl GutterType { GutterType::LineNumbers => line_numbers(editor, doc, view, theme, is_focused), GutterType::Spacer => padding(editor, doc, view, theme, is_focused), GutterType::Diff => diff(editor, doc, view, theme, is_focused), + GutterType::Coverage => coverage(editor, doc, view, theme, is_focused), } } @@ -41,6 +43,7 @@ impl GutterType { GutterType::LineNumbers => line_numbers_width(view, doc), GutterType::Spacer => 1, GutterType::Diff => 1, + GutterType::Coverage => 1, } } } @@ -139,6 +142,50 @@ pub fn diff<'doc>( } } +pub fn coverage<'doc>( + _editor: &'doc Editor, + doc: &'doc Document, + _view: &View, + theme: &Theme, + _is_focused: bool, +) -> GutterFn<'doc> { + let covered = theme.get("diff.plus.gutter"); + let not_covered = theme.get("diff.minus.gutter"); + if let Some(cov) = coverage::parse(PathBuf::from("report/coverage.xml")) { + if let Some(mut path) = doc.path.clone() { + if let Ok(cwd) = std::env::current_dir() { + if let Ok(tmp) = path.strip_prefix(cwd) { + path = tmp.into(); + } + } + if let Some(file) = cov.files.get(&path) { + let this_file = coverage::FileCoverage { + lines: file.lines.clone(), + }; + return Box::new( + move |line: usize, + _selected: bool, + _first_visual_line: bool, + out: &mut String| { + if let Some(line_coverage) = this_file.lines.get(&(line as u32)) { + let (icon, style) = if *line_coverage { + ("┃", covered) + } else { + ("┃", not_covered) + }; + write!(out, "{}", icon).unwrap(); + Some(style) + } else { + None + } + }, + ); + } + } + } + return Box::new(move |_, _, _, _| None); +} + pub fn line_numbers<'doc>( editor: &'doc Editor, doc: &'doc Document, diff --git a/helix-view/src/lib.rs b/helix-view/src/lib.rs index e30a23381..1424d6e09 100644 --- a/helix-view/src/lib.rs +++ b/helix-view/src/lib.rs @@ -4,6 +4,7 @@ pub mod macros; pub mod annotations; pub mod base64; pub mod clipboard; +pub mod coverage; pub mod document; pub mod editor; pub mod events; From 79b85030b259683904add57d73941ab864f1b41a Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Thu, 9 May 2024 07:18:01 -0700 Subject: [PATCH 02/15] load coverage file from environment variable --- helix-view/src/gutter.rs | 56 +++++++++++++++++++++------------------- 1 file changed, 29 insertions(+), 27 deletions(-) diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index f47343207..2eaa0e0d6 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -151,35 +151,37 @@ pub fn coverage<'doc>( ) -> GutterFn<'doc> { let covered = theme.get("diff.plus.gutter"); let not_covered = theme.get("diff.minus.gutter"); - if let Some(cov) = coverage::parse(PathBuf::from("report/coverage.xml")) { - if let Some(mut path) = doc.path.clone() { - if let Ok(cwd) = std::env::current_dir() { - if let Ok(tmp) = path.strip_prefix(cwd) { - path = tmp.into(); + if let Ok(coverage_path) = std::env::var("HELIX_COVERAGE_FILE") { + if let Some(cov) = coverage::parse(PathBuf::from(coverage_path)) { + if let Some(mut path) = doc.path.clone() { + if let Ok(cwd) = std::env::current_dir() { + if let Ok(tmp) = path.strip_prefix(cwd) { + path = tmp.into(); + } } - } - if let Some(file) = cov.files.get(&path) { - let this_file = coverage::FileCoverage { - lines: file.lines.clone(), - }; - return Box::new( - move |line: usize, - _selected: bool, - _first_visual_line: bool, - out: &mut String| { - if let Some(line_coverage) = this_file.lines.get(&(line as u32)) { - let (icon, style) = if *line_coverage { - ("┃", covered) + if let Some(file) = cov.files.get(&path) { + let this_file = coverage::FileCoverage { + lines: file.lines.clone(), + }; + return Box::new( + move |line: usize, + _selected: bool, + _first_visual_line: bool, + out: &mut String| { + if let Some(line_coverage) = this_file.lines.get(&(line as u32)) { + let (icon, style) = if *line_coverage { + ("┃", covered) + } else { + ("┃", not_covered) + }; + write!(out, "{}", icon).unwrap(); + Some(style) } else { - ("┃", not_covered) - }; - write!(out, "{}", icon).unwrap(); - Some(style) - } else { - None - } - }, - ); + None + } + }, + ); + } } } } From a9f2ec3eeab350bafb68995cd77c32f176e7c9c9 Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Thu, 9 May 2024 06:57:51 -0700 Subject: [PATCH 03/15] wip only show coverage if it is newer than doc --- helix-view/src/coverage.rs | 15 ++++++++++-- helix-view/src/document.rs | 2 +- helix-view/src/gutter.rs | 49 ++++++++++++++++++++++---------------- 3 files changed, 42 insertions(+), 24 deletions(-) diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs index cafca5d89..afe7b3abe 100644 --- a/helix-view/src/coverage.rs +++ b/helix-view/src/coverage.rs @@ -3,6 +3,7 @@ use serde::Deserialize; use std::collections::HashMap; use std::fs::File; use std::io::BufReader; +use std::time::SystemTime; pub struct Coverage { pub files: HashMap, @@ -10,6 +11,7 @@ pub struct Coverage { pub struct FileCoverage { pub lines: HashMap, + pub modified_time: Option, } #[derive(Deserialize, Debug)] @@ -18,6 +20,7 @@ struct RawCoverage { version: String, sources: Sources, packages: Packages, + modified_time: Option, } #[derive(Deserialize, Debug)] @@ -72,8 +75,10 @@ struct Line { pub fn parse(path: std::path::PathBuf) -> Option { let file = File::open(path).ok()?; + let metadata = file.metadata().ok()?; let reader = BufReader::new(file); - let tmp: RawCoverage = from_reader(reader).ok()?; + let mut tmp: RawCoverage = from_reader(reader).ok()?; + tmp.modified_time = metadata.modified().ok(); Some(tmp.into()) } @@ -89,7 +94,13 @@ impl From for Coverage { for source in &coverage.sources.source { let path: std::path::PathBuf = [&source.name, &class.filename].iter().collect(); if path.exists() { - files.insert(path, FileCoverage { lines }); + files.insert( + path, + FileCoverage { + lines, + modified_time: coverage.modified_time, + }, + ); break; } } diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index 5c68c62ac..cafa8904c 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -185,7 +185,7 @@ pub struct Document { // Last time we wrote to the file. This will carry the time the file was last opened if there // were no saves. - last_saved_time: SystemTime, + pub last_saved_time: SystemTime, last_saved_revision: usize, version: i32, // should be usize? diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 2eaa0e0d6..9ca73ce8f 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -159,28 +159,35 @@ pub fn coverage<'doc>( path = tmp.into(); } } - if let Some(file) = cov.files.get(&path) { - let this_file = coverage::FileCoverage { - lines: file.lines.clone(), - }; - return Box::new( - move |line: usize, - _selected: bool, - _first_visual_line: bool, - out: &mut String| { - if let Some(line_coverage) = this_file.lines.get(&(line as u32)) { - let (icon, style) = if *line_coverage { - ("┃", covered) + if let Some(file_coverage) = cov.files.get(&path) { + if file_coverage + .modified_time + .is_some_and(|x| x > doc.last_saved_time) + { + // clone file coverage so it can be moved into the closure + let this_file = coverage::FileCoverage { + lines: file_coverage.lines.clone(), + modified_time: file_coverage.modified_time, + }; + return Box::new( + move |line: usize, + _selected: bool, + _first_visual_line: bool, + out: &mut String| { + if let Some(line_coverage) = this_file.lines.get(&(line as u32)) { + let (icon, style) = if *line_coverage { + ("┃", covered) + } else { + ("┃", not_covered) + }; + write!(out, "{}", icon).unwrap(); + Some(style) } else { - ("┃", not_covered) - }; - write!(out, "{}", icon).unwrap(); - Some(style) - } else { - None - } - }, - ); + None + } + }, + ); + } } } } From b01329eb10824756e36d1f03fbed1a91c6e321a1 Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Thu, 9 May 2024 10:19:39 -0700 Subject: [PATCH 04/15] use doc path for modification time --- helix-view/src/document.rs | 2 +- helix-view/src/gutter.rs | 65 +++++++++++++++++++++++--------------- 2 files changed, 40 insertions(+), 27 deletions(-) diff --git a/helix-view/src/document.rs b/helix-view/src/document.rs index cafa8904c..5c68c62ac 100644 --- a/helix-view/src/document.rs +++ b/helix-view/src/document.rs @@ -185,7 +185,7 @@ pub struct Document { // Last time we wrote to the file. This will carry the time the file was last opened if there // were no saves. - pub last_saved_time: SystemTime, + last_saved_time: SystemTime, last_saved_revision: usize, version: i32, // should be usize? diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 9ca73ce8f..19e56cf36 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -152,46 +152,59 @@ pub fn coverage<'doc>( let covered = theme.get("diff.plus.gutter"); let not_covered = theme.get("diff.minus.gutter"); if let Ok(coverage_path) = std::env::var("HELIX_COVERAGE_FILE") { + log::debug!("coverage file is {}", coverage_path); if let Some(cov) = coverage::parse(PathBuf::from(coverage_path)) { + log::debug!("coverage is valid"); if let Some(mut path) = doc.path.clone() { + log::debug!("full document path: {:?}", path); if let Ok(cwd) = std::env::current_dir() { if let Ok(tmp) = path.strip_prefix(cwd) { path = tmp.into(); } } + log::debug!("relative document path: {:?}", path); if let Some(file_coverage) = cov.files.get(&path) { - if file_coverage - .modified_time - .is_some_and(|x| x > doc.last_saved_time) - { - // clone file coverage so it can be moved into the closure - let this_file = coverage::FileCoverage { - lines: file_coverage.lines.clone(), - modified_time: file_coverage.modified_time, - }; - return Box::new( - move |line: usize, - _selected: bool, - _first_visual_line: bool, - out: &mut String| { - if let Some(line_coverage) = this_file.lines.get(&(line as u32)) { - let (icon, style) = if *line_coverage { - ("┃", covered) + log::debug!( + "coverage time: {:?} document time: {:?}", + file_coverage.modified_time, + path.metadata().map(|meta| meta.modified()) + ); + if let Some(coverage_time) = file_coverage.modified_time { + if path.metadata().is_ok_and(|meta| { + meta.modified().is_ok_and(|time| time < coverage_time) + }) { + // clone file coverage so it can be moved into the closure + let this_file = coverage::FileCoverage { + lines: file_coverage.lines.clone(), + modified_time: file_coverage.modified_time, + }; + log::debug!("return valid coverage gutter"); + return Box::new( + move |line: usize, + _selected: bool, + _first_visual_line: bool, + out: &mut String| { + if let Some(line_coverage) = this_file.lines.get(&(line as u32)) + { + let (icon, style) = if *line_coverage { + ("┃", covered) + } else { + ("┃", not_covered) + }; + write!(out, "{}", icon).unwrap(); + Some(style) } else { - ("┃", not_covered) - }; - write!(out, "{}", icon).unwrap(); - Some(style) - } else { - None - } - }, - ); + None + } + }, + ); + } } } } } } + log::debug!("return empty coverage gutter"); return Box::new(move |_, _, _, _| None); } From 66595771b128f4b563cf38a8ae9b85ef4d4b6e3a Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Fri, 10 May 2024 17:20:48 -0700 Subject: [PATCH 05/15] break out coverage file funciton --- helix-view/src/coverage.rs | 32 +++++++++++++++++- helix-view/src/gutter.rs | 68 ++++++++++---------------------------- 2 files changed, 49 insertions(+), 51 deletions(-) diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs index afe7b3abe..3e04c98be 100644 --- a/helix-view/src/coverage.rs +++ b/helix-view/src/coverage.rs @@ -73,7 +73,7 @@ struct Line { hits: u32, } -pub fn parse(path: std::path::PathBuf) -> Option { +pub fn parse(path: &std::path::PathBuf) -> Option { let file = File::open(path).ok()?; let metadata = file.metadata().ok()?; let reader = BufReader::new(file); @@ -82,6 +82,36 @@ pub fn parse(path: std::path::PathBuf) -> Option { Some(tmp.into()) } +pub fn get_coverage(document_path: &std::path::PathBuf) -> Option { + let coverage_path = std::env::var("HELIX_COVERAGE_FILE").ok()?; + log::debug!("coverage file is {}", coverage_path); + let cov = parse(&std::path::PathBuf::from(coverage_path))?; + log::debug!("coverage is valid"); + log::debug!("full document path: {:?}", document_path); + let cwd = std::env::current_dir().ok()?; + let tmp = document_path.strip_prefix(cwd).ok()?; + let relative_path: std::path::PathBuf = tmp.into(); + log::debug!("relative document path: {:?}", relative_path); + let file_coverage = cov.files.get(&relative_path)?; + log::debug!( + "coverage time: {:?} document time: {:?}", + file_coverage.modified_time, + relative_path.metadata().map(|meta| meta.modified()) + ); + let coverage_time = file_coverage.modified_time?; + if relative_path + .metadata() + .is_ok_and(|meta| meta.modified().is_ok_and(|time| time < coverage_time)) + { + log::debug!("file coverage is {:?}", file_coverage.lines); + return Some(FileCoverage { + lines: file_coverage.lines.clone(), + modified_time: file_coverage.modified_time, + }); + } + None +} + impl From for Coverage { fn from(coverage: RawCoverage) -> Self { let mut files = HashMap::new(); diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index 19e56cf36..cfb8b7894 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -151,57 +151,25 @@ pub fn coverage<'doc>( ) -> GutterFn<'doc> { let covered = theme.get("diff.plus.gutter"); let not_covered = theme.get("diff.minus.gutter"); - if let Ok(coverage_path) = std::env::var("HELIX_COVERAGE_FILE") { - log::debug!("coverage file is {}", coverage_path); - if let Some(cov) = coverage::parse(PathBuf::from(coverage_path)) { - log::debug!("coverage is valid"); - if let Some(mut path) = doc.path.clone() { - log::debug!("full document path: {:?}", path); - if let Ok(cwd) = std::env::current_dir() { - if let Ok(tmp) = path.strip_prefix(cwd) { - path = tmp.into(); + + if let Some(document_path) = &doc.path { + if let Some(file_coverage) = coverage::get_coverage(document_path) { + log::debug!("return valid coverage gutter"); + return Box::new( + move |line: usize, _selected: bool, _first_visual_line: bool, out: &mut String| { + if let Some(line_coverage) = file_coverage.lines.get(&(line as u32)) { + let (icon, style) = if *line_coverage { + ("┃", covered) + } else { + ("┃", not_covered) + }; + write!(out, "{}", icon).unwrap(); + Some(style) + } else { + None } - } - log::debug!("relative document path: {:?}", path); - if let Some(file_coverage) = cov.files.get(&path) { - log::debug!( - "coverage time: {:?} document time: {:?}", - file_coverage.modified_time, - path.metadata().map(|meta| meta.modified()) - ); - if let Some(coverage_time) = file_coverage.modified_time { - if path.metadata().is_ok_and(|meta| { - meta.modified().is_ok_and(|time| time < coverage_time) - }) { - // clone file coverage so it can be moved into the closure - let this_file = coverage::FileCoverage { - lines: file_coverage.lines.clone(), - modified_time: file_coverage.modified_time, - }; - log::debug!("return valid coverage gutter"); - return Box::new( - move |line: usize, - _selected: bool, - _first_visual_line: bool, - out: &mut String| { - if let Some(line_coverage) = this_file.lines.get(&(line as u32)) - { - let (icon, style) = if *line_coverage { - ("┃", covered) - } else { - ("┃", not_covered) - }; - write!(out, "{}", icon).unwrap(); - Some(style) - } else { - None - } - }, - ); - } - } - } - } + }, + ); } } log::debug!("return empty coverage gutter"); From 8a594265e1371fd8fb5f59b14709dd5bf738f3f2 Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Mon, 13 May 2024 16:55:34 -0700 Subject: [PATCH 06/15] clean up coverage gutter and its testing --- helix-view/src/coverage.rs | 150 +++++++++++++++++++++++-------------- 1 file changed, 92 insertions(+), 58 deletions(-) diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs index 3e04c98be..91019b49f 100644 --- a/helix-view/src/coverage.rs +++ b/helix-view/src/coverage.rs @@ -5,10 +5,12 @@ use std::fs::File; use std::io::BufReader; use std::time::SystemTime; +#[derive(Debug)] pub struct Coverage { pub files: HashMap, } +#[derive(Debug)] pub struct FileCoverage { pub lines: HashMap, pub modified_time: Option, @@ -73,7 +75,33 @@ struct Line { hits: u32, } -pub fn parse(path: &std::path::PathBuf) -> Option { +pub fn get_coverage(document_path: &std::path::PathBuf) -> Option { + let coverage_path = std::env::var("HELIX_COVERAGE_FILE").ok()?; + log::debug!("coverage file is {}", coverage_path); + let coverage = read_cobertura_coverage(&std::path::PathBuf::from(coverage_path))?; + log::debug!("coverage is valid"); + + log::debug!("document path: {:?}", document_path); + + let file_coverage = coverage.files.get(document_path)?; + + let coverage_time = file_coverage.modified_time?; + let document_metadata = document_path.metadata().ok()?; + let document_time = document_metadata.modified().ok()?; + + if document_time < coverage_time { + log::debug!("file coverage contains {} lines", file_coverage.lines.len()); + return Some(FileCoverage { + lines: file_coverage.lines.clone(), + modified_time: file_coverage.modified_time, + }); + } else { + log::debug!("document is newer than coverage file, will not return coverage"); + return None; + } +} + +fn read_cobertura_coverage(path: &std::path::PathBuf) -> Option { let file = File::open(path).ok()?; let metadata = file.metadata().ok()?; let reader = BufReader::new(file); @@ -82,36 +110,6 @@ pub fn parse(path: &std::path::PathBuf) -> Option { Some(tmp.into()) } -pub fn get_coverage(document_path: &std::path::PathBuf) -> Option { - let coverage_path = std::env::var("HELIX_COVERAGE_FILE").ok()?; - log::debug!("coverage file is {}", coverage_path); - let cov = parse(&std::path::PathBuf::from(coverage_path))?; - log::debug!("coverage is valid"); - log::debug!("full document path: {:?}", document_path); - let cwd = std::env::current_dir().ok()?; - let tmp = document_path.strip_prefix(cwd).ok()?; - let relative_path: std::path::PathBuf = tmp.into(); - log::debug!("relative document path: {:?}", relative_path); - let file_coverage = cov.files.get(&relative_path)?; - log::debug!( - "coverage time: {:?} document time: {:?}", - file_coverage.modified_time, - relative_path.metadata().map(|meta| meta.modified()) - ); - let coverage_time = file_coverage.modified_time?; - if relative_path - .metadata() - .is_ok_and(|meta| meta.modified().is_ok_and(|time| time < coverage_time)) - { - log::debug!("file coverage is {:?}", file_coverage.lines); - return Some(FileCoverage { - lines: file_coverage.lines.clone(), - modified_time: file_coverage.modified_time, - }); - } - None -} - impl From for Coverage { fn from(coverage: RawCoverage) -> Self { let mut files = HashMap::new(); @@ -122,8 +120,11 @@ impl From for Coverage { lines.insert(line.number - 1, line.hits > 0); } for source in &coverage.sources.source { - let path: std::path::PathBuf = [&source.name, &class.filename].iter().collect(); - if path.exists() { + // it is ambiguous to which source a coverage class might belong + // so check each in the path + let raw_path: std::path::PathBuf = + [&source.name, &class.filename].iter().collect(); + if let Ok(path) = std::fs::canonicalize(raw_path) { files.insert( path, FileCoverage { @@ -145,15 +146,23 @@ mod tests { use super::*; use quick_xml::de::from_str; use std::path::PathBuf; - static TEST_STRING: &str = r#" + + fn test_string(use_relative_paths: bool) -> String { + let source_path = if use_relative_paths { + PathBuf::from("src") + } else { + PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("src") + }; + return format!( + r#" - a_src + {} - + @@ -168,36 +177,61 @@ mod tests { -"#; +"#, + source_path.to_string_lossy() + ); + } #[test] fn test_deserialize_raw_coverage_from_string() { - let result: RawCoverage = from_str(TEST_STRING).unwrap(); + let result: RawCoverage = from_str(&test_string(true)).unwrap(); println!("result is {:?}", result); assert_eq!(result.version, "7.3.0"); - assert_eq!(result.sources.source[0].name, "a_src"); + assert_eq!(result.sources.source[0].name, "src"); assert_eq!(result.packages.package[0].name, "a package"); - let class = &result.packages.package[0].classes.class[0]; - assert_eq!(class.name, "a class"); - assert_eq!(class.filename, "file.ext"); - assert_eq!(class.lines.line[0].number, 3); - assert_eq!(class.lines.line[0].hits, 1); - assert_eq!(class.lines.line[1].number, 5); - assert_eq!(class.lines.line[1].hits, 0); + let first = &result.packages.package[0].classes.class[0]; + assert_eq!(first.name, "a class"); + assert_eq!(first.filename, "coverage.rs"); + assert_eq!(first.lines.line[0].number, 3); + assert_eq!(first.lines.line[0].hits, 1); + assert_eq!(first.lines.line[1].number, 5); + assert_eq!(first.lines.line[1].hits, 0); + let second = &result.packages.package[0].classes.class[1]; + assert_eq!(second.name, "another class"); + assert_eq!(second.filename, "other.ext"); + assert_eq!(second.lines.line[0].number, 1); + assert_eq!(second.lines.line[0].hits, 0); + assert_eq!(second.lines.line[1].number, 7); + assert_eq!(second.lines.line[1].hits, 1); } #[test] - fn test_convert_raw_coverage_to_coverage() { - let tmp: RawCoverage = from_str(TEST_STRING).unwrap(); - let result: Coverage = tmp.into(); - assert_eq!(result.files.len(), 2); - let first = result.files.get(&PathBuf::from("a_src/file.ext")).unwrap(); - assert!(first.lines.get(&0).is_none()); - assert_eq!(first.lines.get(&3), Some(&true)); - assert_eq!(first.lines.get(&5), Some(&false)); - let second = result.files.get(&PathBuf::from("a_src/other.ext")).unwrap(); - assert!(second.lines.get(&3).is_none()); - assert_eq!(second.lines.get(&1), Some(&false)); - assert_eq!(second.lines.get(&7), Some(&true)); + fn test_convert_raw_coverage_to_coverage_with_relative_path() { + let tmp: RawCoverage = from_str(&test_string(true)).unwrap(); + check_coverage(tmp.into()); + } + #[test] + fn test_convert_raw_coverage_to_coverage_with_absolute_path() { + let tmp: RawCoverage = from_str(&test_string(false)).unwrap(); + check_coverage(tmp.into()); + } + + fn check_coverage(result: Coverage) { + println!("result is {:?}", result); + // only one file should be included, since src/other.ext does not exist + assert_eq!(result.files.len(), 1); + // coverage will always canonicalize path + let first = result + .files + .get( + &PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("src") + .join("coverage.rs"), + ) + .unwrap(); + println!("cov {:?}", first); + assert_eq!(first.lines.len(), 2); + assert_eq!(first.lines.get(&2), Some(&true)); + assert_eq!(first.lines.get(&4), Some(&false)); } } From c81ae710b56bf945d881f6642a8502eeac803e7d Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Tue, 14 May 2024 07:14:46 -0700 Subject: [PATCH 07/15] add some documentation --- helix-view/src/coverage.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs index 91019b49f..0c445e7ff 100644 --- a/helix-view/src/coverage.rs +++ b/helix-view/src/coverage.rs @@ -75,6 +75,11 @@ struct Line { hits: u32, } +/// Get coverage information for a document from the configured coverage file. +/// +/// The coverage file is set by environment variable HELIX_COVERAGE_FILE. This +/// function will return None if the coverage file is not found, invalid, does +/// not contain the document, or if it is out of date compared to the document. pub fn get_coverage(document_path: &std::path::PathBuf) -> Option { let coverage_path = std::env::var("HELIX_COVERAGE_FILE").ok()?; log::debug!("coverage file is {}", coverage_path); From 8a4c19ceb9852851d871c3ac485bd89fbf0efa49 Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Wed, 15 May 2024 06:45:30 -0700 Subject: [PATCH 08/15] automatically find coverage file --- Cargo.lock | 1 + helix-view/Cargo.toml | 1 + helix-view/src/coverage.rs | 22 +++++++++++++++++++--- 3 files changed, 21 insertions(+), 3 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 2d9be36f0..667c3367e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1567,6 +1567,7 @@ dependencies = [ "tokio-stream", "toml", "url", + "walkdir", ] [[package]] diff --git a/helix-view/Cargo.toml b/helix-view/Cargo.toml index bbc8929c7..72324ccad 100644 --- a/helix-view/Cargo.toml +++ b/helix-view/Cargo.toml @@ -52,6 +52,7 @@ log = "~0.4" parking_lot.workspace = true thiserror.workspace = true quick-xml = { version = "0.31.0", features = ["serialize"] } +walkdir = "2.5.0" [target.'cfg(windows)'.dependencies] clipboard-win = { version = "5.4", features = ["std"] } diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs index 0c445e7ff..df25e4898 100644 --- a/helix-view/src/coverage.rs +++ b/helix-view/src/coverage.rs @@ -4,6 +4,7 @@ use std::collections::HashMap; use std::fs::File; use std::io::BufReader; use std::time::SystemTime; +use walkdir; #[derive(Debug)] pub struct Coverage { @@ -81,9 +82,9 @@ struct Line { /// function will return None if the coverage file is not found, invalid, does /// not contain the document, or if it is out of date compared to the document. pub fn get_coverage(document_path: &std::path::PathBuf) -> Option { - let coverage_path = std::env::var("HELIX_COVERAGE_FILE").ok()?; - log::debug!("coverage file is {}", coverage_path); - let coverage = read_cobertura_coverage(&std::path::PathBuf::from(coverage_path))?; + let coverage_path = find_coverage_file()?; + log::debug!("coverage file is {:?}", coverage_path); + let coverage = read_cobertura_coverage(&coverage_path)?; log::debug!("coverage is valid"); log::debug!("document path: {:?}", document_path); @@ -106,6 +107,21 @@ pub fn get_coverage(document_path: &std::path::PathBuf) -> Option } } +fn find_coverage_file() -> Option { + if let Some(coverage_path) = std::env::var("HELIX_COVERAGE_FILE").ok() { + return Some(std::path::PathBuf::from(coverage_path)); + } + for entry in walkdir::WalkDir::new(".") + .into_iter() + .filter_map(|e| e.ok()) + { + if entry.file_name() == "coverage.xml" || entry.file_name() == "cobertura.xml" { + return Some(entry.path().to_path_buf()); + } + } + return None; +} + fn read_cobertura_coverage(path: &std::path::PathBuf) -> Option { let file = File::open(path).ok()?; let metadata = file.metadata().ok()?; From e8aa25e3324f41f644bf62bbd4503e75dd7a459f Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Thu, 3 Apr 2025 09:35:30 -0700 Subject: [PATCH 09/15] add error reporting to coverage reading --- helix-view/src/coverage.rs | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs index df25e4898..43d87c755 100644 --- a/helix-view/src/coverage.rs +++ b/helix-view/src/coverage.rs @@ -123,11 +123,22 @@ fn find_coverage_file() -> Option { } fn read_cobertura_coverage(path: &std::path::PathBuf) -> Option { - let file = File::open(path).ok()?; - let metadata = file.metadata().ok()?; + let file = File::open(path) + .inspect_err(|e| log::info!("error opening {:?}: {:?}", path, e)) + .ok()?; + let metadata = file + .metadata() + .inspect_err(|e| log::info!("error reading metadata for {:?}: {:?}", path, e)) + .ok()?; + let modified = metadata + .modified() + .inspect_err(|e| log::info!("error reading timestamp for {:?}: {:?}", path, e)) + .ok()?; let reader = BufReader::new(file); - let mut tmp: RawCoverage = from_reader(reader).ok()?; - tmp.modified_time = metadata.modified().ok(); + let mut tmp: RawCoverage = from_reader(reader) + .inspect_err(|e| log::info!("error parsing coverage for {:?}: {:?}", path, e)) + .ok()?; + tmp.modified_time = Some(modified); Some(tmp.into()) } From aa144e68c6f16ffad2a5f58121a13fd76fbf6ae4 Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Thu, 3 Apr 2025 10:02:04 -0700 Subject: [PATCH 10/15] allow class lines to be optional in coverage xml --- helix-view/src/coverage.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs index 43d87c755..fe0a0399f 100644 --- a/helix-view/src/coverage.rs +++ b/helix-view/src/coverage.rs @@ -60,7 +60,7 @@ struct Class { name: String, #[serde(rename = "@filename")] filename: String, - lines: Lines, + lines: Option, } #[derive(Deserialize, Debug)] @@ -148,8 +148,10 @@ impl From for Coverage { for package in coverage.packages.package { for class in package.classes.class { let mut lines = HashMap::new(); - for line in class.lines.line { - lines.insert(line.number - 1, line.hits > 0); + if let Some(class_lines) = class.lines { + for line in class_lines.line { + lines.insert(line.number - 1, line.hits > 0); + } } for source in &coverage.sources.source { // it is ambiguous to which source a coverage class might belong From 5fdd2e99b9f2043ef8b731cbf499c8aa6bfba77e Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Thu, 3 Apr 2025 10:11:31 -0700 Subject: [PATCH 11/15] fix coverage optional class lines --- helix-view/src/coverage.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs index fe0a0399f..55549fdd6 100644 --- a/helix-view/src/coverage.rs +++ b/helix-view/src/coverage.rs @@ -60,12 +60,12 @@ struct Class { name: String, #[serde(rename = "@filename")] filename: String, - lines: Option, + lines: Lines, } #[derive(Deserialize, Debug)] struct Lines { - line: Vec, + line: Option>, } #[derive(Deserialize, Debug)] @@ -148,8 +148,8 @@ impl From for Coverage { for package in coverage.packages.package { for class in package.classes.class { let mut lines = HashMap::new(); - if let Some(class_lines) = class.lines { - for line in class_lines.line { + if let Some(class_lines) = class.lines.line { + for line in class_lines { lines.insert(line.number - 1, line.hits > 0); } } From c142d85f7114a72d41c512d5249607881f1fb054 Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Thu, 3 Apr 2025 10:43:49 -0700 Subject: [PATCH 12/15] better logging for coverage --- helix-view/src/coverage.rs | 7 ++++++- helix-view/src/gutter.rs | 4 ++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs index 55549fdd6..d94686990 100644 --- a/helix-view/src/coverage.rs +++ b/helix-view/src/coverage.rs @@ -89,7 +89,10 @@ pub fn get_coverage(document_path: &std::path::PathBuf) -> Option log::debug!("document path: {:?}", document_path); - let file_coverage = coverage.files.get(document_path)?; + let file_coverage = coverage.files.get(document_path).or_else(|| { + log::warn!("file: {:?} not found in coverage", document_path); + None + })?; let coverage_time = file_coverage.modified_time?; let document_metadata = document_path.metadata().ok()?; @@ -159,6 +162,7 @@ impl From for Coverage { let raw_path: std::path::PathBuf = [&source.name, &class.filename].iter().collect(); if let Ok(path) = std::fs::canonicalize(raw_path) { + log::debug!("add file {:?} to coverage", path); files.insert( path, FileCoverage { @@ -168,6 +172,7 @@ impl From for Coverage { ); break; } + log::warn!("could not add file {:?} to coverage", path); } } } diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index cfb8b7894..bef8ca184 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -154,7 +154,7 @@ pub fn coverage<'doc>( if let Some(document_path) = &doc.path { if let Some(file_coverage) = coverage::get_coverage(document_path) { - log::debug!("return valid coverage gutter"); + log::info!("return valid coverage gutter for {:?}", document_path); return Box::new( move |line: usize, _selected: bool, _first_visual_line: bool, out: &mut String| { if let Some(line_coverage) = file_coverage.lines.get(&(line as u32)) { @@ -172,7 +172,7 @@ pub fn coverage<'doc>( ); } } - log::debug!("return empty coverage gutter"); + log::info!("return empty coverage gutter for {:?}", document_path); return Box::new(move |_, _, _, _| None); } From 623e5de8480eae09c21d4b2b1da938c12d1c1887 Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Mon, 7 Apr 2025 12:26:13 -0700 Subject: [PATCH 13/15] limit coverage file search depth --- helix-view/src/coverage.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs index d94686990..34158090d 100644 --- a/helix-view/src/coverage.rs +++ b/helix-view/src/coverage.rs @@ -115,6 +115,7 @@ fn find_coverage_file() -> Option { return Some(std::path::PathBuf::from(coverage_path)); } for entry in walkdir::WalkDir::new(".") + .max_depth(1) .into_iter() .filter_map(|e| e.ok()) { From b40aba78e46a6dcce2369435055e5ed0b765f87b Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Mon, 7 Apr 2025 12:51:17 -0700 Subject: [PATCH 14/15] fix coverage logging --- helix-view/src/coverage.rs | 2 +- helix-view/src/gutter.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs index 34158090d..fbe57932a 100644 --- a/helix-view/src/coverage.rs +++ b/helix-view/src/coverage.rs @@ -173,7 +173,7 @@ impl From for Coverage { ); break; } - log::warn!("could not add file {:?} to coverage", path); + log::warn!("could not add file {:?} to coverage", raw_path); } } } diff --git a/helix-view/src/gutter.rs b/helix-view/src/gutter.rs index bef8ca184..1d8cf9a54 100644 --- a/helix-view/src/gutter.rs +++ b/helix-view/src/gutter.rs @@ -172,7 +172,7 @@ pub fn coverage<'doc>( ); } } - log::info!("return empty coverage gutter for {:?}", document_path); + log::info!("return empty coverage gutter"); return Box::new(move |_, _, _, _| None); } From c86b41596519a181506758edd1f31f1baca3c9a5 Mon Sep 17 00:00:00 2001 From: Dustin Lagoy Date: Mon, 7 Apr 2025 12:58:19 -0700 Subject: [PATCH 15/15] fix coverage logging --- helix-view/src/coverage.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/helix-view/src/coverage.rs b/helix-view/src/coverage.rs index fbe57932a..1ba8fea2b 100644 --- a/helix-view/src/coverage.rs +++ b/helix-view/src/coverage.rs @@ -162,7 +162,7 @@ impl From for Coverage { // so check each in the path let raw_path: std::path::PathBuf = [&source.name, &class.filename].iter().collect(); - if let Ok(path) = std::fs::canonicalize(raw_path) { + if let Ok(path) = std::fs::canonicalize(raw_path.clone()) { log::debug!("add file {:?} to coverage", path); files.insert( path,