initial basic cobertura coverage gutter

pull/10758/head
Dustin Lagoy 2024-05-09 06:57:51 -07:00
parent 7ebf650029
commit 6006a69b44
7 changed files with 229 additions and 4 deletions

11
Cargo.lock generated
View File

@ -1557,6 +1557,7 @@ dependencies = [
"once_cell", "once_cell",
"parking_lot", "parking_lot",
"rustix 1.0.3", "rustix 1.0.3",
"quick-xml",
"serde", "serde",
"serde_json", "serde_json",
"slotmap", "slotmap",
@ -2121,6 +2122,16 @@ dependencies = [
"unicase", "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]] [[package]]
name = "quickcheck" name = "quickcheck"
version = "1.0.3" version = "1.0.3"

View File

@ -51,6 +51,7 @@ log = "~0.4"
parking_lot.workspace = true parking_lot.workspace = true
thiserror.workspace = true thiserror.workspace = true
quick-xml = { version = "0.31.0", features = ["serialize"] }
[target.'cfg(windows)'.dependencies] [target.'cfg(windows)'.dependencies]
clipboard-win = { version = "5.4", features = ["std"] } clipboard-win = { version = "5.4", features = ["std"] }

View File

@ -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<std::path::PathBuf, FileCoverage>,
}
pub struct FileCoverage {
pub lines: HashMap<u32, bool>,
}
#[derive(Deserialize, Debug)]
struct RawCoverage {
#[serde(rename = "@version")]
version: String,
sources: Sources,
packages: Packages,
}
#[derive(Deserialize, Debug)]
struct Sources {
source: Vec<Source>,
}
#[derive(Deserialize, Debug)]
struct Source {
#[serde(rename = "$value")]
name: String,
}
#[derive(Deserialize, Debug)]
struct Packages {
package: Vec<Package>,
}
#[derive(Deserialize, Debug)]
struct Package {
#[serde(rename = "@name")]
name: String,
classes: Classes,
}
#[derive(Deserialize, Debug)]
struct Classes {
class: Vec<Class>,
}
#[derive(Deserialize, Debug)]
struct Class {
#[serde(rename = "@name")]
name: String,
#[serde(rename = "@filename")]
filename: String,
lines: Lines,
}
#[derive(Deserialize, Debug)]
struct Lines {
line: Vec<Line>,
}
#[derive(Deserialize, Debug)]
struct Line {
#[serde(rename = "@number")]
number: u32,
#[serde(rename = "@hits")]
hits: u32,
}
pub fn parse(path: std::path::PathBuf) -> Option<Coverage> {
let file = File::open(path).ok()?;
let reader = BufReader::new(file);
let tmp: RawCoverage = from_reader(reader).ok()?;
Some(tmp.into())
}
impl From<RawCoverage> 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#"<?xml version="1.0" ?>
<coverage version="7.3.0" timestamp="4333222111000">
<sources>
<source>a_src</source>
</sources>
<packages>
<package name="a package">
<classes>
<class name="a class" filename="file.ext">
<lines>
<line number="3" hits="1"/>
<line number="5" hits="0"/>
</lines>
</class>
<class name="another class" filename="other.ext">
<lines>
<line number="1" hits="0"/>
<line number="7" hits="1"/>
</lines>
</class>
</classes>
</package>
</packages>
</coverage>"#;
#[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));
}
}

View File

@ -152,7 +152,7 @@ pub struct Document {
/// update from the LSP /// update from the LSP
pub inlay_hints_oudated: bool, pub inlay_hints_oudated: bool,
path: Option<PathBuf>, pub path: Option<PathBuf>,
relative_path: OnceCell<Option<PathBuf>>, relative_path: OnceCell<Option<PathBuf>>,
encoding: &'static encoding::Encoding, encoding: &'static encoding::Encoding,
has_bom: bool, has_bom: bool,

View File

@ -420,7 +420,7 @@ pub fn get_terminal_provider() -> Option<TerminalConfig> {
}) })
} }
#[cfg(not(any(windows, target_arch = "wasm32")))] #[cfg(not(any(windows, target_os = "wasm32")))]
pub fn get_terminal_provider() -> Option<TerminalConfig> { pub fn get_terminal_provider() -> Option<TerminalConfig> {
use helix_stdx::env::{binary_exists, env_var_is_set}; use helix_stdx::env::{binary_exists, env_var_is_set};
@ -711,6 +711,8 @@ pub enum GutterType {
Spacer, Spacer,
/// Highlight local changes /// Highlight local changes
Diff, Diff,
/// Highlight local changes
Coverage,
} }
impl std::str::FromStr for GutterType { impl std::str::FromStr for GutterType {
@ -722,8 +724,9 @@ impl std::str::FromStr for GutterType {
"spacer" => Ok(Self::Spacer), "spacer" => Ok(Self::Spacer),
"line-numbers" => Ok(Self::LineNumbers), "line-numbers" => Ok(Self::LineNumbers),
"diff" => Ok(Self::Diff), "diff" => Ok(Self::Diff),
"coverage" => Ok(Self::Coverage),
_ => anyhow::bail!( _ => 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`."
), ),
} }
} }

View File

@ -1,8 +1,9 @@
use std::fmt::Write; use std::{fmt::Write, path::PathBuf};
use helix_core::syntax::LanguageServerFeature; use helix_core::syntax::LanguageServerFeature;
use crate::{ use crate::{
coverage,
editor::GutterType, editor::GutterType,
graphics::{Style, UnderlineStyle}, graphics::{Style, UnderlineStyle},
Document, Editor, Theme, View, Document, Editor, Theme, View,
@ -32,6 +33,7 @@ impl GutterType {
GutterType::LineNumbers => line_numbers(editor, doc, view, theme, is_focused), GutterType::LineNumbers => line_numbers(editor, doc, view, theme, is_focused),
GutterType::Spacer => padding(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::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::LineNumbers => line_numbers_width(view, doc),
GutterType::Spacer => 1, GutterType::Spacer => 1,
GutterType::Diff => 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>( pub fn line_numbers<'doc>(
editor: &'doc Editor, editor: &'doc Editor,
doc: &'doc Document, doc: &'doc Document,

View File

@ -4,6 +4,7 @@ pub mod macros;
pub mod annotations; pub mod annotations;
pub mod base64; pub mod base64;
pub mod clipboard; pub mod clipboard;
pub mod coverage;
pub mod document; pub mod document;
pub mod editor; pub mod editor;
pub mod events; pub mod events;