feat(adapter): add diagnostics to show which test failed

This commit is contained in:
kbwo 2024-11-30 18:19:28 +09:00
parent e09a50d60c
commit aa4883e038
6 changed files with 167 additions and 76 deletions

55
Cargo.lock generated
View file

@ -721,31 +721,6 @@ dependencies = [
"windows-sys 0.52.0",
]
[[package]]
name = "testing-language-server"
version = "0.0.11"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "4c1a48aec4305834e4f80ad28dfe5bea8c98c0bb215a092eb2af15c4ac86efb7"
dependencies = [
"anyhow",
"chrono",
"clap",
"dirs",
"glob",
"globwalk",
"lsp-types",
"once_cell",
"regex",
"serde",
"serde_json",
"strum",
"thiserror",
"tracing",
"tracing-appender",
"tracing-subscriber",
"tree-sitter-php",
]
[[package]]
name = "testing-language-server"
version = "0.1.7"
@ -770,9 +745,35 @@ dependencies = [
"tree-sitter-php",
]
[[package]]
name = "testing-language-server"
version = "0.1.7"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8fae6ce9cb290e80814d1796e4207a0a0dc21c216807136d48b68c0f27be29b9"
dependencies = [
"anyhow",
"chrono",
"clap",
"dirs",
"glob",
"globwalk",
"lsp-types",
"once_cell",
"regex",
"serde",
"serde_json",
"strum",
"thiserror",
"toml",
"tracing",
"tracing-appender",
"tracing-subscriber",
"tree-sitter-php",
]
[[package]]
name = "testing-ls-adapter"
version = "0.0.10"
version = "0.0.11"
dependencies = [
"anyhow",
"clap",
@ -782,7 +783,7 @@ dependencies = [
"serde",
"serde_json",
"tempfile",
"testing-language-server 0.0.11",
"testing-language-server 0.1.7 (registry+https://github.com/rust-lang/crates.io-index)",
"tracing",
"tracing-appender",
"tracing-subscriber",

View file

@ -1,6 +1,6 @@
[package]
name = "testing-ls-adapter"
version = "0.0.10"
version = "0.0.11"
edition = "2021"
description = "testing-language-server adapter"
license = "MIT"
@ -8,7 +8,7 @@ license = "MIT"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[dependencies]
testing-language-server = "0.0.11"
testing-language-server = "0.1.7"
lsp-types = { workspace = true }
serde_json = { workspace = true }
serde = { workspace = true }

View file

@ -8,6 +8,7 @@ use testing_language_server::spec::RunFileTestResult;
use testing_language_server::spec::DiscoverResult;
use testing_language_server::spec::DiscoverResultItem;
use testing_language_server::spec::TestItem;
use crate::model::Runner;
@ -16,14 +17,6 @@ use super::util::discover_rust_tests;
use super::util::parse_cargo_diagnostics;
use super::util::write_result_log;
fn parse_diagnostics(
contents: &str,
workspace_root: PathBuf,
file_paths: &[String],
) -> RunFileTestResult {
parse_cargo_diagnostics(contents, workspace_root, file_paths)
}
fn detect_workspaces(file_paths: &[String]) -> DetectWorkspaceResult {
detect_workspaces_from_file_list(file_paths, &["Cargo.toml".to_string()])
}
@ -54,19 +47,16 @@ impl Runner for CargoNextestRunner {
args: testing_language_server::spec::RunFileTestArgs,
) -> Result<(), LSError> {
let file_paths = args.file_paths;
let tests = file_paths
let discovered_tests: Vec<TestItem> = file_paths
.iter()
.map(|path| {
discover_rust_tests(path).map(|test_items| {
test_items
.into_iter()
.map(|item| item.id)
.collect::<Vec<String>>()
})
})
.map(|path| discover_rust_tests(path))
.filter_map(Result::ok)
.flatten()
.collect::<Vec<_>>();
let test_ids = discovered_tests
.iter()
.map(|item| item.id.clone())
.collect::<Vec<String>>();
let workspace_root = args.workspace;
let test_result = std::process::Command::new("cargo")
.current_dir(&workspace_root)
@ -76,7 +66,7 @@ impl Runner for CargoNextestRunner {
.arg("--no-fail-fast")
.args(args.extra)
.arg("--")
.args(tests)
.args(&test_ids)
.output()
.unwrap();
let output = test_result;
@ -91,10 +81,11 @@ impl Runner for CargoNextestRunner {
return Err(LSError::Adapter(String::from_utf8(stderr).unwrap()));
}
let test_result = String::from_utf8(stderr)?;
let diagnostics: RunFileTestResult = parse_diagnostics(
let diagnostics: RunFileTestResult = parse_cargo_diagnostics(
&test_result,
PathBuf::from_str(&workspace_root).unwrap(),
&file_paths,
&discovered_tests,
);
send_stdout(&diagnostics)?;
Ok(())
@ -115,7 +106,7 @@ impl Runner for CargoNextestRunner {
#[cfg(test)]
mod tests {
use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
use testing_language_server::spec::RunFileTestResultItem;
use testing_language_server::spec::{RunFileTestResultItem, TestItem};
use crate::runner::util::MAX_CHAR_LENGTH;
@ -142,10 +133,35 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
"#;
let file_paths =
vec!["/home/example/projects/rocks-lib/src/rocks/dependency.rs".to_string()];
let diagnostics: RunFileTestResult = parse_diagnostics(
let test_items: Vec<TestItem> = vec![TestItem {
id: "rocks::dependency::tests::parse_dependency".to_string(),
name: "rocks::dependency::tests::parse_dependency".to_string(),
start_position: Range {
start: Position {
line: 85,
character: 63,
},
end: Position {
line: 85,
character: MAX_CHAR_LENGTH,
},
},
end_position: Range {
start: Position {
line: 85,
character: 63,
},
end: Position {
line: 85,
character: MAX_CHAR_LENGTH,
},
},
}];
let diagnostics: RunFileTestResult = parse_cargo_diagnostics(
fixture,
PathBuf::from_str("/home/example/projects").unwrap(),
&file_paths,
&test_items,
);
let message = r#"called `Result::unwrap()` on an `Err` value: unexpected end of input while parsing min or version number
Location:

View file

@ -8,6 +8,7 @@ use testing_language_server::spec::RunFileTestResult;
use testing_language_server::spec::DiscoverResult;
use testing_language_server::spec::DiscoverResultItem;
use testing_language_server::spec::TestItem;
use crate::model::Runner;
@ -16,14 +17,6 @@ use super::util::discover_rust_tests;
use super::util::parse_cargo_diagnostics;
use super::util::write_result_log;
fn parse_diagnostics(
contents: &str,
workspace_root: PathBuf,
file_paths: &[String],
) -> RunFileTestResult {
parse_cargo_diagnostics(contents, workspace_root, file_paths)
}
fn detect_workspaces(file_paths: &[String]) -> DetectWorkspaceResult {
detect_workspaces_from_file_list(file_paths, &["Cargo.toml".to_string()])
}
@ -54,26 +47,23 @@ impl Runner for CargoTestRunner {
args: testing_language_server::spec::RunFileTestArgs,
) -> Result<(), LSError> {
let file_paths = args.file_paths;
let tests = file_paths
let discovered_tests: Vec<TestItem> = file_paths
.iter()
.map(|path| {
discover_rust_tests(path).map(|test_items| {
test_items
.into_iter()
.map(|item| item.id)
.collect::<Vec<String>>()
})
})
.map(|path| discover_rust_tests(path))
.filter_map(Result::ok)
.flatten()
.collect::<Vec<_>>();
let test_ids = discovered_tests
.iter()
.map(|item| item.id.clone())
.collect::<Vec<String>>();
let workspace_root = args.workspace;
let test_result = std::process::Command::new("cargo")
.current_dir(&workspace_root)
.arg("test")
.args(args.extra)
.arg("--")
.args(tests)
.args(&test_ids)
.output()
.unwrap();
let output = test_result;
@ -85,10 +75,12 @@ impl Runner for CargoTestRunner {
// When `--nocapture` option is set, stderr has some important information
// to parse test result
let test_result = String::from_utf8(stderr)? + &String::from_utf8(stdout)?;
let diagnostics: RunFileTestResult = parse_diagnostics(
let diagnostics: RunFileTestResult = parse_cargo_diagnostics(
&test_result,
PathBuf::from_str(&workspace_root).unwrap(),
&file_paths,
&discovered_tests,
);
send_stdout(&diagnostics)?;
Ok(())
@ -136,10 +128,35 @@ note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
"#;
let file_paths =
vec!["/home/example/projects/rocks-lib/src/rocks/dependency.rs".to_string()];
let diagnostics: RunFileTestResult = parse_diagnostics(
let test_items: Vec<TestItem> = vec![TestItem {
id: "rocks::dependency::tests::parse_dependency".to_string(),
name: "rocks::dependency::tests::parse_dependency".to_string(),
start_position: Range {
start: Position {
line: 85,
character: 63,
},
end: Position {
line: 85,
character: MAX_CHAR_LENGTH,
},
},
end_position: Range {
start: Position {
line: 85,
character: 63,
},
end: Position {
line: 85,
character: MAX_CHAR_LENGTH,
},
},
}];
let diagnostics: RunFileTestResult = parse_cargo_diagnostics(
fixture,
PathBuf::from_str("/home/example/projects").unwrap(),
&file_paths,
&test_items,
);
let message = r#"called `Result::unwrap()` on an `Err` value: unexpected end of input while parsing min or version number
Location:

View file

@ -276,19 +276,35 @@ pub fn parse_cargo_diagnostics(
contents: &str,
workspace_root: PathBuf,
file_paths: &[String],
test_items: &[TestItem],
) -> RunFileTestResult {
let contents = contents.replace("\r\n", "\n");
let lines = contents.lines();
let mut result_map: HashMap<String, Vec<Diagnostic>> = HashMap::new();
for (i, line) in lines.clone().enumerate() {
// Example:
// thread 'server::tests::test_panic' panicked at src/server.rs:584:9:
let re = Regex::new(r"thread '([^']+)' panicked at ([^:]+):(\d+):(\d+):").unwrap();
if let Some(m) = re.captures(line) {
let mut message = String::new();
let file = m.get(2).unwrap().as_str().to_string();
if let Some(file_path) = file_paths
.iter()
.find(|path| path.contains(workspace_root.join(&file).to_str().unwrap()))
{
// <filename>::<id>
let id_with_file = m.get(1).unwrap().as_str().to_string();
// relaive path
let relative_file_path = m.get(2).unwrap().as_str().to_string();
// name of the file without extension
let file_stem = Path::new(&relative_file_path)
.file_stem()
.unwrap()
.to_str()
.unwrap();
let executed_test_id = id_with_file.replace(&(file_stem.to_string() + "::"), "");
if let Some(file_path) = file_paths.iter().find(|path| {
path.contains(workspace_root.join(&relative_file_path).to_str().unwrap())
}) {
let matched_test_item = test_items.iter().find(|item| item.id == executed_test_id);
let lnum = m.get(3).unwrap().as_str().parse::<u32>().unwrap() - 1;
let col = m.get(4).unwrap().as_str().parse::<u32>().unwrap() - 1;
let mut next_i = i + 1;
@ -298,6 +314,33 @@ pub fn parse_cargo_diagnostics(
message = format!("{}{}\n", message, lines.clone().nth(next_i).unwrap());
next_i += 1;
}
let diagnostic = Diagnostic {
range: Range {
start: Position {
line: lnum,
character: col,
},
end: Position {
line: lnum,
character: MAX_CHAR_LENGTH,
},
},
message: message.clone(),
severity: Some(DiagnosticSeverity::ERROR),
..Diagnostic::default()
};
// if the test item is matched,
// add a diagnostic to the beginning of the test item
// in order to show which test failed.
// If this code does not exist, only panicked positions are shown
if let Some(test_item) = matched_test_item {
let message = format!(
"`{}` failed at {relative_file_path}:{lnum}:{col}\nMessage:\n{message}",
test_item.name
);
let lnum = test_item.start_position.start.line;
let col = test_item.start_position.start.character;
let diagnostic = Diagnostic {
range: Range {
start: Position {
@ -317,6 +360,11 @@ pub fn parse_cargo_diagnostics(
.entry(file_path.to_string())
.or_default()
.push(diagnostic);
}
result_map
.entry(file_path.to_string())
.or_default()
.push(diagnostic);
} else {
continue;
}

View file

@ -53,4 +53,13 @@ mod tests {
}
}
}
fn p() {
panic!("test failed");
}
#[test]
fn test_panic() {
p();
}
}