mirror of
https://github.com/l0ng-ai/openidea.git
synced 2026-11-07 00:07:29 +00:00
Project & files - Project view modes (Project Files / Open Files / All Changed Files / Scratches); External Libraries for Python, JDK (src.zip), Node, Cargo, Go, Maven/Gradle jars; read-only jar/zip browsing - ⌘N New popup with type search and more file kinds; New Project, Rename Project, Reload All from Disk, Export/Import Settings, Power Save Mode - File type associations, extra content roots and exclude patterns (indexed and watched without reopening the project) Editor & navigation - Find bar: search scope (comments/strings), Preserve Case, Exclude, multiline, history; Find in Files: Module scope, custom pattern scopes, multiline - Search Everywhere filters, Assign Shortcut, settings results; named bookmark lists, file/folder bookmarks; Structure filters and Show Inherited; Recent Locations timestamps and breadcrumbs; Code Vision author - Completion options, editor tab placement/layout, inline completion (LSP 3.18) - Spell checking (Typo inspection, ide-spell) and Inspections settings with custom profiles; Code Style schemes, formatter markers, Do not format Run, VCS, services, database - Typed run configurations (Shell, Program, npm, Gradle, Maven, Docker, Compound), Modify options, Before launch, env dialog, Current File, Emulate terminal (PTY) - Git log graph/view options, Go to Hash/Branch/Tag, date filter keeps the graph; Pull/Merge/Rebase dialogs, Manage Remotes, Worktrees - Generic Services host (Docker connections/registries, run sessions); TODO scope/changes tabs with changelists; Coverage report import - Data sources: wire-compatible vendors, URL import, SSL certs, Options, Schemas and Advanced tabs, color and comment Workbench - Run Anything, Customize Main Toolbar, named tool window layouts, menu completeness, Code Cleanup, macros, "Don't Show Again", editor banners - Chinese translations for the new UI Claude-Session: https://claude.ai/code/session_01SaPPsVPuk6ZTNsoUCNCbMv
174 lines
7.3 KiB
Rust
174 lines
7.3 KiB
Rust
//! "Import a report collected in CI from disk": the format is sniffed from the
|
|
//! content (lcov, Go cover profile, JaCoCo XML) and paths recorded on another
|
|
//! machine are mapped into the project by their longest existing suffix.
|
|
|
|
use crate::{Accumulator, CoverageSuite, goprofile, jacoco, lcov};
|
|
use ide_core::{IdeError, IdeResult};
|
|
use std::path::{Component, Path, PathBuf};
|
|
|
|
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
|
|
pub(crate) enum Format {
|
|
Lcov,
|
|
GoProfile,
|
|
Jacoco,
|
|
}
|
|
|
|
pub(crate) fn detect(text: &str) -> Option<Format> {
|
|
let head = text.trim_start_matches('\u{feff}').trim_start();
|
|
if head.starts_with('<') {
|
|
return head.contains("<report").then_some(Format::Jacoco);
|
|
}
|
|
let first = head.lines().next().unwrap_or_default();
|
|
if first.starts_with("mode:") {
|
|
return Some(Format::GoProfile);
|
|
}
|
|
head.lines().any(|l| l.trim_start().starts_with("SF:")).then_some(Format::Lcov)
|
|
}
|
|
|
|
/// Reads the report at `path` for the project at `root`. Blocking.
|
|
pub fn import(path: &Path, root: &Path) -> IdeResult<CoverageSuite> {
|
|
let text = std::fs::read_to_string(path)?;
|
|
let mut suite = match detect(&text) {
|
|
Some(Format::Lcov) => lcov::parse(&text, root).finish("lcov"),
|
|
Some(Format::GoProfile) => {
|
|
let (module, module_dir) = go_module_for(root, &text);
|
|
goprofile::parse(&text, &module, &module_dir).finish("go test -cover")
|
|
}
|
|
Some(Format::Jacoco) => {
|
|
// A report inside the project (`<module>/target/site/jacoco/…`) knows its module.
|
|
let module = jacoco::module_dir(path);
|
|
let module =
|
|
if path.starts_with(root) && module.starts_with(root) && module != path.parent().unwrap_or(path) {
|
|
module
|
|
} else {
|
|
root.to_path_buf()
|
|
};
|
|
let mut acc = Accumulator { has_branches: true, ..Default::default() };
|
|
jacoco::parse_into(&text, &module, &mut acc)
|
|
.map_err(|e| IdeError::invalid(format!("{}: {e}", path.display())))?;
|
|
acc.finish("JaCoCo")
|
|
}
|
|
None => {
|
|
return Err(IdeError::invalid(format!(
|
|
"{} is not an lcov tracefile, Go cover profile or JaCoCo XML report",
|
|
path.display()
|
|
)));
|
|
}
|
|
};
|
|
for f in &mut suite.files {
|
|
let p: &Path = f.path.as_ref();
|
|
if !p.exists()
|
|
&& let Some(found) = relocate(p, root)
|
|
{
|
|
f.path = found.into();
|
|
}
|
|
}
|
|
suite.files.sort_by(|a, b| (a.path.as_ref() as &Path).cmp(b.path.as_ref()));
|
|
Ok(suite)
|
|
}
|
|
|
|
/// `/home/runner/work/app/app/src/lib.rs` → `<root>/src/lib.rs`: the longest suffix of
|
|
/// `path` that exists under `root`.
|
|
pub(crate) fn relocate(path: &Path, root: &Path) -> Option<PathBuf> {
|
|
let parts: Vec<&std::ffi::OsStr> = path
|
|
.components()
|
|
.filter_map(|c| match c {
|
|
Component::Normal(s) => Some(s),
|
|
_ => None,
|
|
})
|
|
.collect();
|
|
(0..parts.len()).find_map(|skip| {
|
|
let candidate = parts[skip..].iter().fold(root.to_path_buf(), |p, s| p.join(s));
|
|
candidate.is_file().then_some(candidate)
|
|
})
|
|
}
|
|
|
|
/// The Go module the profile's import paths belong to: the project's `go.mod`, else one
|
|
/// a level or two down whose module path prefixes the profile's files.
|
|
fn go_module_for(root: &Path, profile: &str) -> (String, PathBuf) {
|
|
let first = profile
|
|
.lines()
|
|
.map(str::trim)
|
|
.find(|l| !l.is_empty() && !l.starts_with("mode:"))
|
|
.and_then(|l| l.rsplit_once(':').map(|(f, _)| f.to_owned()))
|
|
.unwrap_or_default();
|
|
let matches = |module: &str| !module.is_empty() && (first == module || first.starts_with(&format!("{module}/")));
|
|
let read = |dir: &Path| -> Option<String> {
|
|
let text = std::fs::read_to_string(dir.join("go.mod")).ok()?;
|
|
text.lines().find_map(|l| l.trim().strip_prefix("module")).map(|m| m.trim().trim_matches('"').to_owned())
|
|
};
|
|
let mut candidates = vec![root.to_path_buf()];
|
|
for dir in std::fs::read_dir(root).into_iter().flatten().flatten().map(|e| e.path()).filter(|p| p.is_dir()) {
|
|
candidates
|
|
.extend(std::fs::read_dir(&dir).into_iter().flatten().flatten().map(|e| e.path()).filter(|p| p.is_dir()));
|
|
candidates.push(dir);
|
|
}
|
|
let found: Vec<(String, PathBuf)> = candidates.into_iter().filter_map(|d| read(&d).map(|m| (m, d))).collect();
|
|
found
|
|
.iter()
|
|
.find(|(m, _)| matches(m))
|
|
.or_else(|| found.iter().find(|(_, d)| d == root))
|
|
.cloned()
|
|
.unwrap_or_else(|| (String::new(), root.to_path_buf()))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn detects_formats() {
|
|
assert_eq!(detect("TN:\nSF:a.rs\nDA:1,1\nend_of_record\n"), Some(Format::Lcov));
|
|
assert_eq!(detect("mode: atomic\nm/a.go:1.1,2.2 1 0\n"), Some(Format::GoProfile));
|
|
assert_eq!(
|
|
detect("<?xml version=\"1.0\"?><!DOCTYPE report><report name=\"x\"></report>"),
|
|
Some(Format::Jacoco)
|
|
);
|
|
assert_eq!(detect("<?xml version=\"1.0\"?><coverage/>"), None);
|
|
assert_eq!(detect("hello"), None);
|
|
}
|
|
|
|
#[test]
|
|
fn imports_ci_lcov_with_foreign_paths() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let root = tmp.path().join("app");
|
|
std::fs::create_dir_all(root.join("src")).unwrap();
|
|
std::fs::write(root.join("src/lib.rs"), "").unwrap();
|
|
let report = tmp.path().join("lcov.info");
|
|
std::fs::write(&report, "SF:/home/runner/work/app/app/src/lib.rs\nDA:1,1\nDA:2,0\nend_of_record\n").unwrap();
|
|
let suite = import(&report, &root).unwrap();
|
|
assert_eq!(suite.runner, "lcov");
|
|
assert_eq!(suite.files[0].path.as_ref() as &Path, root.join("src/lib.rs"));
|
|
assert_eq!(suite.files[0].lines_covered, 1);
|
|
assert!(import(&tmp.path().join("missing"), &root).is_err());
|
|
std::fs::write(&report, "nothing").unwrap();
|
|
assert!(import(&report, &root).is_err());
|
|
}
|
|
|
|
#[test]
|
|
fn imports_go_profiles_of_nested_modules() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let root = tmp.path();
|
|
std::fs::create_dir_all(root.join("svc/calc")).unwrap();
|
|
std::fs::write(root.join("svc/go.mod"), "module example.com/svc\n").unwrap();
|
|
let report = root.join("cover.out");
|
|
std::fs::write(&report, "mode: set\nexample.com/svc/calc/calc.go:3.1,4.2 1 1\n").unwrap();
|
|
let suite = import(&report, root).unwrap();
|
|
assert_eq!(suite.files[0].path.as_ref() as &Path, root.join("svc/calc/calc.go"));
|
|
}
|
|
|
|
#[test]
|
|
fn imports_jacoco_against_the_project() {
|
|
let tmp = tempfile::tempdir().unwrap();
|
|
let root = tmp.path().join("p");
|
|
std::fs::create_dir_all(root.join("src/main/java/com/x")).unwrap();
|
|
std::fs::write(root.join("src/main/java/com/x/A.java"), "").unwrap();
|
|
let report = tmp.path().join("jacoco.xml");
|
|
let xml = r#"<report name="p"><package name="com/x"><sourcefile name="A.java"><line nr="2" mi="0" ci="1" mb="0" cb="0"/></sourcefile></package></report>"#;
|
|
std::fs::write(&report, xml).unwrap();
|
|
let suite = import(&report, &root).unwrap();
|
|
assert_eq!(suite.files[0].path.as_ref() as &Path, root.join("src/main/java/com/x/A.java"));
|
|
assert!(suite.has_branches);
|
|
}
|
|
}
|