mirror of
https://github.com/kbwo/testing-language-server.git
synced 2025-07-23 10:55:01 +00:00
feat: Add RSpec test adapter and perform project cleanup
This commit introduces a new test adapter for the RSpec testing framework and includes general code cleanup. RSpec Adapter Features: - Implements the `Runner` trait for RSpec. - Uses `tree-sitter-ruby` for test discovery from `*_spec.rb` files, identifying `describe`, `context`, and `it` blocks. - Executes RSpec tests using the `rspec -f json` command and parses the JSON output to generate diagnostics for failed tests. - Detects RSpec workspaces by looking for `Gemfile` or `.rspec` files. - Includes a demo RSpec project (`demo/rspec_project`) for testing. - Adds integration tests (`crates/adapter/tests/rspec_test.rs`) covering workspace detection, test discovery, and diagnostic parsing. Cleanup and Fixes: - I refactored the RSpec adapter to be fully synchronous, resolving previous compilation issues with the `Runner` trait. - I removed unused imports and dead code in the RSpec adapter files. - I ensured the `crates/adapter/src/runner/phpunit.rs` uses the correct tree-sitter PHP language function, confirming a previous fix. - I verified that the entire project builds successfully using `cargo build --all-targets`. - I confirmed all RSpec-specific tests pass. Pre-existing test failures in Jest, Node, and Vitest adapters remain and are noted as out of scope for this change. The implementation adheres to the synchronous nature of the `Runner` trait, ensuring correct integration with the existing adapter framework.
This commit is contained in:
parent
c53cdb6a6b
commit
ca66d265d1
14 changed files with 571 additions and 27 deletions
209
crates/adapter/tests/rspec_test.rs
Normal file
209
crates/adapter/tests/rspec_test.rs
Normal file
|
@ -0,0 +1,209 @@
|
|||
// crates/adapter/tests/rspec_test.rs
|
||||
|
||||
// use std::fs; // Removed
|
||||
use std::path::PathBuf; // Path removed, PathBuf kept
|
||||
// use std::str::FromStr; // Removed
|
||||
|
||||
use lsp_types::{Position, Range, DiagnosticSeverity};
|
||||
use testing_language_server::spec::{
|
||||
// DetectWorkspaceArgs, DetectWorkspaceResult, DiscoverArgs, DiscoverResult,
|
||||
FileDiagnostics, RunFileTestResult, TestItem, // Kept these as they are used by code under test or for assertions
|
||||
};
|
||||
// use testing_language_server::error::LSError; // Removed
|
||||
use testing_ls_adapter::runner::rspec::{RspecRunner, RspecReport};
|
||||
use testing_ls_adapter::runner::util::{
|
||||
detect_workspaces_from_file_list, discover_with_treesitter, MAX_CHAR_LENGTH,
|
||||
};
|
||||
|
||||
// Helper to get the absolute path to the demo project
|
||||
fn get_demo_project_root() -> PathBuf {
|
||||
let mut path = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
||||
path.pop();
|
||||
path.push("demo/rspec_project");
|
||||
path
|
||||
}
|
||||
|
||||
// Helper to get the absolute path to a file within the demo project
|
||||
fn get_demo_file_path(relative_file_name: &str) -> PathBuf {
|
||||
get_demo_project_root().join(relative_file_name)
|
||||
}
|
||||
|
||||
const RSPEC_QUERY: &str = r#"
|
||||
(block
|
||||
(call
|
||||
method: (identifier) @func_name (#any-of? @func_name "describe" "context")
|
||||
arguments: (arguments (string (string_content) @namespace.name))
|
||||
)
|
||||
) @namespace.definition
|
||||
|
||||
(block
|
||||
(call
|
||||
method: (identifier) @func_name (#any-of? @func_name "it" "specify" "example")
|
||||
arguments: (arguments (string (string_content) @test.name))
|
||||
)
|
||||
) @test.definition
|
||||
"#; // This IS used.
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn test_detect_rspec_workspace() {
|
||||
let demo_spec_file = get_demo_file_path("spec/example_spec.rb");
|
||||
let file_paths = vec![demo_spec_file.to_str().unwrap().to_string()];
|
||||
let indicator_files = vec!["Gemfile".to_string(), ".rspec".to_string()];
|
||||
|
||||
let result = detect_workspaces_from_file_list(&file_paths, &indicator_files);
|
||||
|
||||
let expected_workspace_path_str = get_demo_project_root().to_str().unwrap().to_string();
|
||||
|
||||
assert_eq!(result.data.len(), 1, "Should detect one workspace. Detected: {:#?}", result.data);
|
||||
assert!(result.data.contains_key(&expected_workspace_path_str), "Detected workspace path is incorrect. Expected: {}, Got keys: {:?}", expected_workspace_path_str, result.data.keys());
|
||||
|
||||
let files_in_workspace = result.data.get(&expected_workspace_path_str).unwrap();
|
||||
assert_eq!(files_in_workspace.len(), 1, "Workspace should contain one file");
|
||||
assert_eq!(
|
||||
files_in_workspace[0],
|
||||
demo_spec_file.to_str().unwrap().to_string(),
|
||||
"File in workspace is incorrect"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_discover_rspec_tests() {
|
||||
let spec_file_path = get_demo_file_path("spec/example_spec.rb");
|
||||
let spec_file_path_str = spec_file_path.to_str().unwrap();
|
||||
|
||||
let discovered_items = discover_with_treesitter(
|
||||
spec_file_path_str,
|
||||
&tree_sitter_ruby::language(),
|
||||
RSPEC_QUERY,
|
||||
).unwrap_or_else(|e| panic!("discover_with_treesitter failed: {:?}", e));
|
||||
|
||||
let expected_items = vec![
|
||||
TestItem {
|
||||
id: "MyString::concatenation::should concatenate two strings".to_string(),
|
||||
name: "MyString::concatenation::should concatenate two strings".to_string(),
|
||||
path: spec_file_path_str.to_string(),
|
||||
start_position: Range { start: Position { line: 2, character: 4 }, end: Position { line: 2, character: MAX_CHAR_LENGTH } },
|
||||
end_position: Range { start: Position { line: 7, character: 0 }, end: Position { line: 7, character: 5 } },
|
||||
},
|
||||
TestItem {
|
||||
id: "Math::should add two numbers correctly".to_string(),
|
||||
name: "Math::should add two numbers correctly".to_string(),
|
||||
path: spec_file_path_str.to_string(),
|
||||
start_position: Range { start: Position { line: 10, character: 2 }, end: Position { line: 10, character: MAX_CHAR_LENGTH } },
|
||||
end_position: Range { start: Position { line: 12, character: 0 }, end: Position { line: 12, character: 5 } },
|
||||
},
|
||||
TestItem {
|
||||
id: "Math::should fail a test".to_string(),
|
||||
name: "Math::should fail a test".to_string(),
|
||||
path: spec_file_path_str.to_string(),
|
||||
start_position: Range { start: Position { line: 14, character: 2 }, end: Position { line: 14, character: MAX_CHAR_LENGTH } },
|
||||
end_position: Range { start: Position { line: 16, character: 0 }, end: Position { line: 16, character: 5 } },
|
||||
},
|
||||
TestItem {
|
||||
id: "Math::is a pending test".to_string(),
|
||||
name: "Math::is a pending test".to_string(),
|
||||
path: spec_file_path_str.to_string(),
|
||||
start_position: Range { start: Position { line: 18, character: 2 }, end: Position { line: 18, character: MAX_CHAR_LENGTH } },
|
||||
end_position: Range { start: Position { line: 18, character: 0 }, end: Position { line: 18, character: "it \"is a pending test\"".len() as u32 } },
|
||||
},
|
||||
];
|
||||
|
||||
assert_eq!(discovered_items.len(), expected_items.len(), "Number of discovered tests does not match. Actual: {:#?}", discovered_items);
|
||||
|
||||
for expected in expected_items.iter() {
|
||||
assert!(
|
||||
discovered_items.contains(expected),
|
||||
"Expected test item not found: {:?}\nActual items: {:#?}",
|
||||
expected,
|
||||
discovered_items
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_run_rspec_tests_and_parse_diagnostics() {
|
||||
let rspec_json_output = r#"
|
||||
{
|
||||
"version": "3.13.0",
|
||||
"summary_line": "4 examples, 1 failure, 1 pending",
|
||||
"examples": [
|
||||
{
|
||||
"id": "./spec/example_spec.rb[1:1:1]",
|
||||
"description": "should concatenate two strings",
|
||||
"full_description": "MyString concatenation should concatenate two strings",
|
||||
"status": "passed",
|
||||
"file_path": "./spec/example_spec.rb",
|
||||
"line_number": 3,
|
||||
"run_time": 0.0001
|
||||
},
|
||||
{
|
||||
"id": "./spec/example_spec.rb[2:1:1]",
|
||||
"description": "should add two numbers correctly",
|
||||
"full_description": "Math should add two numbers correctly",
|
||||
"status": "passed",
|
||||
"file_path": "./spec/example_spec.rb",
|
||||
"line_number": 11,
|
||||
"run_time": 0.00005
|
||||
},
|
||||
{
|
||||
"id": "./spec/example_spec.rb[2:2:1]",
|
||||
"description": "should fail a test",
|
||||
"full_description": "Math should fail a test",
|
||||
"status": "failed",
|
||||
"file_path": "./spec/example_spec.rb",
|
||||
"line_number": 15,
|
||||
"run_time": 0.0005,
|
||||
"exception": {
|
||||
"class": "RSpec::Expectations::ExpectationNotMetError",
|
||||
"message": "\nexpected: 3\n got: 2\n\n(compared using ==)\n",
|
||||
"backtrace": [
|
||||
"/app/demo/rspec_project/spec/example_spec.rb:16:in `block (2 levels) in <top (required)>'"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "./spec/example_spec.rb[2:3:1]",
|
||||
"description": "is a pending test",
|
||||
"full_description": "Math is a pending test",
|
||||
"status": "pending",
|
||||
"file_path": "./spec/example_spec.rb",
|
||||
"line_number": 19,
|
||||
"pending_message": "No reason given"
|
||||
}
|
||||
]
|
||||
}
|
||||
"#;
|
||||
|
||||
let report: RspecReport = serde_json::from_str(rspec_json_output)
|
||||
.unwrap_or_else(|e| panic!("Failed to parse RSpec JSON: {}", e));
|
||||
|
||||
let runner = RspecRunner::default();
|
||||
let workspace_root_path = get_demo_project_root();
|
||||
let workspace_root_str = workspace_root_path.to_str().unwrap().to_string();
|
||||
|
||||
let dummy_file_paths_for_parsing = vec![get_demo_file_path("spec/example_spec.rb").to_str().unwrap().to_string()];
|
||||
|
||||
let result: RunFileTestResult = runner.parse_diagnostics(report, &dummy_file_paths_for_parsing, &workspace_root_str)
|
||||
.unwrap_or_else(|e| panic!("parse_diagnostics failed: {:?}", e)); // result is RunFileTestResult
|
||||
|
||||
assert_eq!(result.data.len(), 1, "Should be diagnostics for one file. Actual: {:#?}", result.data); // result.data is Vec<FileDiagnostics>
|
||||
|
||||
let file_diag: &FileDiagnostics = result.data.first().unwrap(); // file_diag is &FileDiagnostics
|
||||
let expected_diag_file_path = get_demo_file_path("spec/example_spec.rb").to_str().unwrap().to_string();
|
||||
assert_eq!(file_diag.path, expected_diag_file_path, "Diagnostic file path mismatch");
|
||||
|
||||
assert_eq!(file_diag.diagnostics.len(), 1, "Should be one diagnostic for the failing test. Actual: {:#?}", file_diag.diagnostics);
|
||||
|
||||
let diag = file_diag.diagnostics.first().unwrap();
|
||||
assert_eq!(diag.message, "\nexpected: 3\n got: 2\n\n(compared using ==)\n", "Diagnostic message mismatch");
|
||||
assert_eq!(diag.severity, Some(DiagnosticSeverity::ERROR), "Diagnostic severity mismatch");
|
||||
assert_eq!(diag.range.start.line, 14, "Diagnostic start line mismatch");
|
||||
assert_eq!(diag.range.start.character, 0, "Diagnostic start character mismatch");
|
||||
assert_eq!(diag.range.end.line, 14, "Diagnostic end line mismatch");
|
||||
assert_eq!(diag.range.end.character, MAX_CHAR_LENGTH, "Diagnostic end character mismatch");
|
||||
}
|
||||
}
|
Loading…
Add table
Add a link
Reference in a new issue