feat: implement phpunit adapter

This commit is contained in:
kbwo 2024-08-24 22:54:18 +09:00
parent daa8db434c
commit 3e3d4c5db1
15 changed files with 2125 additions and 9 deletions

9
CONTRIBUTING.md Normal file
View file

@ -0,0 +1,9 @@
# Getting started
```sh
cargo install just
cargo install cargo-watch
just watch-build
sudo ln -s $(pwd)/target/debug/testing-language-server /usr/local/bin/testing-language-server
sudo ln -s $(pwd)/target/debug/testing-ls-adapter /usr/local/bin/testing-ls-adapter
```

19
Cargo.lock generated
View file

@ -644,6 +644,7 @@ dependencies = [
"tracing",
"tracing-appender",
"tracing-subscriber",
"tree-sitter-php",
]
[[package]]
@ -665,7 +666,9 @@ dependencies = [
"tree-sitter",
"tree-sitter-go",
"tree-sitter-javascript",
"tree-sitter-php",
"tree-sitter-rust",
"xml-rs",
]
[[package]]
@ -828,6 +831,16 @@ dependencies = [
"tree-sitter",
]
[[package]]
name = "tree-sitter-php"
version = "0.22.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1be890bd043986cc26b69968698e508dbd40060805e482f226dc873a63a88d60"
dependencies = [
"cc",
"tree-sitter",
]
[[package]]
name = "tree-sitter-rust"
version = "0.21.2"
@ -1040,3 +1053,9 @@ name = "windows_x86_64_msvc"
version = "0.52.5"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "bec47e5bfd1bff0eeaf6d8b485cc1074891a197ab4225d504cb7a1ab88b02bf0"
[[package]]
name = "xml-rs"
version = "0.8.21"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "539a77ee7c0de333dcc6da69b177380a0b81e0dacfa4f7344c465a36871ee601"

View file

@ -46,3 +46,4 @@ once_cell = { workspace = true }
strum = { workspace = true, features = ["derive"] }
glob = { workspace = true }
globwalk = "0.9.1"
tree-sitter-php = "0.22.8"

View file

@ -20,7 +20,9 @@ anyhow = { workspace = true }
tempfile = "3.10.1"
tree-sitter-javascript = "0.21.0"
tree-sitter-go = "0.21.0"
tree-sitter-php = "0.22.5"
tracing-appender = { workspace = true }
tracing = { workspace = true }
tracing-subscriber = { workspace = true, default-features = false }
dirs = "5.0.1"
xml-rs = "0.8.21"

View file

@ -2,6 +2,7 @@ use crate::runner::cargo_nextest::CargoNextestRunner;
use crate::runner::cargo_test::CargoTestRunner;
use crate::runner::deno::DenoRunner;
use crate::runner::go::GoTestRunner;
use crate::runner::phpunit::PhpunitRunner;
use crate::runner::vitest::VitestRunner;
use std::str::FromStr;
use testing_language_server::error::LSError;
@ -19,6 +20,7 @@ pub enum AvailableTestKind {
Vitest(VitestRunner),
Deno(DenoRunner),
GoTest(GoTestRunner),
Phpunit(PhpunitRunner),
}
impl Runner for AvailableTestKind {
fn discover(&self, args: DiscoverArgs) -> Result<(), LSError> {
@ -29,6 +31,7 @@ impl Runner for AvailableTestKind {
AvailableTestKind::Deno(runner) => runner.discover(args),
AvailableTestKind::GoTest(runner) => runner.discover(args),
AvailableTestKind::Vitest(runner) => runner.discover(args),
AvailableTestKind::Phpunit(runner) => runner.discover(args),
}
}
@ -40,6 +43,7 @@ impl Runner for AvailableTestKind {
AvailableTestKind::Deno(runner) => runner.run_file_test(args),
AvailableTestKind::GoTest(runner) => runner.run_file_test(args),
AvailableTestKind::Vitest(runner) => runner.run_file_test(args),
AvailableTestKind::Phpunit(runner) => runner.run_file_test(args),
}
}
@ -51,6 +55,7 @@ impl Runner for AvailableTestKind {
AvailableTestKind::Deno(runner) => runner.detect_workspaces(args),
AvailableTestKind::GoTest(runner) => runner.detect_workspaces(args),
AvailableTestKind::Vitest(runner) => runner.detect_workspaces(args),
AvailableTestKind::Phpunit(runner) => runner.detect_workspaces(args),
}
}
}
@ -66,6 +71,7 @@ impl FromStr for AvailableTestKind {
"go-test" => Ok(AvailableTestKind::GoTest(GoTestRunner)),
"vitest" => Ok(AvailableTestKind::Vitest(VitestRunner)),
"deno" => Ok(AvailableTestKind::Deno(DenoRunner)),
"phpunit" => Ok(AvailableTestKind::Phpunit(PhpunitRunner)),
_ => Err(anyhow::anyhow!("Unknown test kind: {}", s)),
}
}

View file

@ -3,5 +3,6 @@ pub mod cargo_test;
pub mod deno;
pub mod go;
pub mod jest;
pub mod phpunit;
pub mod util;
pub mod vitest;

View file

@ -0,0 +1,338 @@
use lsp_types::{Diagnostic, DiagnosticSeverity, Position, Range};
use std::fs::File;
use std::io::BufReader;
use std::process::Output;
use testing_language_server::error::LSError;
use testing_language_server::spec::{
DetectWorkspaceResult, DiscoverResult, DiscoverResultItem, RunFileTestResult,
RunFileTestResultItem, TestItem,
};
use xml::reader::{ParserConfig, XmlEvent};
use crate::model::Runner;
use super::util::{
detect_workspaces_from_file_paths, discover_with_treesitter, send_stdout, LOG_LOCATION,
MAX_CHAR_LENGTH,
};
#[derive(Debug)]
pub struct ResultFromXml {
pub message: String,
pub path: String,
pub line: u32,
}
impl Into<RunFileTestResultItem> for ResultFromXml {
fn into(self) -> RunFileTestResultItem {
RunFileTestResultItem {
path: self.path,
diagnostics: vec![Diagnostic {
message: self.message,
range: Range {
start: Position {
line: self.line - 1,
character: 0,
},
end: Position {
line: self.line - 1,
character: MAX_CHAR_LENGTH,
},
},
severity: Some(DiagnosticSeverity::ERROR),
..Default::default()
}],
}
}
}
fn detect_workspaces(file_paths: Vec<String>) -> DetectWorkspaceResult {
detect_workspaces_from_file_paths(&file_paths, &["composer.json".to_string()])
}
fn get_result_from_characters(characters: &str) -> Result<ResultFromXml, anyhow::Error> {
// characters can be like
// Tests\\CalculatorTest::testFail1\nFailed asserting that 8 matches expected 1.\n\n/home/kbwo/projects/github.com/kbwo/testing-language-server/demo/phpunit/src/CalculatorTest.php:28
let mut split = characters.split("\n\n");
let message = split
.next()
.unwrap()
.trim_start_matches("Failed asserting that ")
.trim_end_matches(".")
.to_string();
let location = split.next().unwrap().to_string();
let mut split_location = location.split(":");
let path = split_location.next().unwrap().to_string();
let line = split_location.next().unwrap().parse().unwrap();
Ok(ResultFromXml {
message,
path,
line,
})
}
fn get_result_from_xml(path: &str) -> Result<Vec<ResultFromXml>, anyhow::Error> {
use xml::common::Position;
let file = File::open(path).unwrap();
let mut reader = ParserConfig::default()
.ignore_root_level_whitespace(false)
.create_reader(BufReader::new(file));
let local_name = "failure";
let mut in_failure = false;
let mut result: Vec<ResultFromXml> = Vec::new();
loop {
match reader.next() {
Ok(e) => match e {
XmlEvent::StartElement { name, .. } => {
if name.local_name.starts_with(local_name) {
in_failure = true;
}
}
XmlEvent::EndElement { .. } => {
in_failure = false;
}
XmlEvent::Characters(data) => {
if let Ok(result_from_xml) = get_result_from_characters(&data) {
if in_failure {
result.push(result_from_xml);
}
}
}
XmlEvent::EndDocument => break,
_ => {}
},
Err(e) => {
tracing::error!("Error at {}: {e}", reader.position());
break;
}
}
}
Ok(result)
}
fn discover(file_path: &str) -> Result<Vec<TestItem>, LSError> {
// from https://github.com/olimorris/neotest-phpunit/blob/bbd79d95e927ccd16f0e1d765060058d34838e2e/lua/neotest-phpunit/init.lua#L111
// license: https://github.com/olimorris/neotest-phpunit/blob/bbd79d95e927ccd16f0e1d765060058d34838e2e/LICENSE
let query = r#"
((class_declaration
name: (name) @namespace.name (#match? @namespace.name "Test")
)) @namespace.definition
((method_declaration
(attribute_list
(attribute_group
(attribute) @test_attribute (#match? @test_attribute "Test")
)
)
(
(visibility_modifier)
(name) @test.name
) @test.definition
))
((method_declaration
(name) @test.name (#match? @test.name "test")
)) @test.definition
(((comment) @test_comment (#match? @test_comment "\\@test") .
(method_declaration
(name) @test.name
) @test.definition
))
"#;
discover_with_treesitter(file_path, &tree_sitter_php::language_php(), query)
}
#[derive(Eq, PartialEq, Debug)]
pub struct PhpunitRunner;
impl Runner for PhpunitRunner {
fn discover(&self, args: testing_language_server::spec::DiscoverArgs) -> Result<(), LSError> {
let file_paths = args.file_paths;
let mut discover_results: DiscoverResult = vec![];
for file_path in file_paths {
discover_results.push(DiscoverResultItem {
tests: discover(&file_path)?,
path: file_path,
})
}
send_stdout(&discover_results)?;
Ok(())
}
fn run_file_test(
&self,
args: testing_language_server::spec::RunFileTestArgs,
) -> Result<(), LSError> {
let file_paths = args.file_paths;
let workspace_root = args.workspace;
let log_path = LOG_LOCATION.join("phpunit.xml");
let tests = file_paths
.iter()
.map(|path| {
discover(path).map(|test_items| {
test_items
.into_iter()
.map(|item| item.id)
.collect::<Vec<String>>()
})
})
.filter_map(Result::ok)
.flatten()
.collect::<Vec<_>>();
let test_names = tests.join("|");
let filter_pattern = format!("/{test_names}/");
let output = std::process::Command::new("phpunit")
.current_dir(&workspace_root)
.args([
"--log-junit",
log_path.to_str().unwrap(),
"--filter",
&filter_pattern,
])
.args(file_paths)
.stdout(std::process::Stdio::null())
.stderr(std::process::Stdio::null())
.output()
.unwrap();
let Output { stdout, stderr, .. } = output;
if stdout.is_empty() && !stderr.is_empty() {
return Err(LSError::Adapter(String::from_utf8(stderr).unwrap()));
}
let result_from_xml = get_result_from_xml(log_path.to_str().unwrap())?;
let diagnostics: RunFileTestResult = result_from_xml
.into_iter()
.map(|result_from_xml| {
let result_item: RunFileTestResultItem = result_from_xml.into();
result_item
})
.collect();
send_stdout(&diagnostics)?;
Ok(())
}
fn detect_workspaces(
&self,
args: testing_language_server::spec::DetectWorkspaceArgs,
) -> Result<(), LSError> {
let file_paths = args.file_paths;
let detect_result = detect_workspaces(file_paths);
send_stdout(&detect_result)?;
Ok(())
}
}
#[cfg(test)]
mod tests {
use lsp_types::{Position, Range};
use crate::runner::util::MAX_CHAR_LENGTH;
use super::*;
#[test]
fn parse_xml() {
let mut path = std::env::current_dir().unwrap();
path.push("../../demo/phpunit/output.xml");
let result = get_result_from_xml(path.to_str().unwrap()).unwrap();
assert_eq!(result.len(), 1);
assert_eq!(
result[0].message,
"Tests\\CalculatorTest::testFail1\nFailed asserting that 8 matches expected 1"
);
assert_eq!(
result[0].path,
"/home/kbwo/testing-language-server/demo/phpunit/src/CalculatorTest.php"
);
assert_eq!(result[0].line, 28);
}
#[test]
fn test_discover() {
let file_path = "../../demo/phpunit/src/CalculatorTest.php";
let test_items = discover(file_path).unwrap();
assert_eq!(test_items.len(), 3);
assert_eq!(
test_items,
[
TestItem {
id: "CalculatorTest::testAdd".to_string(),
name: "CalculatorTest::testAdd".to_string(),
start_position: Range {
start: Position {
line: 9,
character: 4
},
end: Position {
line: 9,
character: MAX_CHAR_LENGTH
}
},
end_position: Range {
start: Position {
line: 14,
character: 0
},
end: Position {
line: 14,
character: 5
}
}
},
TestItem {
id: "CalculatorTest::testSubtract".to_string(),
name: "CalculatorTest::testSubtract".to_string(),
start_position: Range {
start: Position {
line: 16,
character: 4
},
end: Position {
line: 16,
character: MAX_CHAR_LENGTH
}
},
end_position: Range {
start: Position {
line: 21,
character: 0
},
end: Position {
line: 21,
character: 5
}
}
},
TestItem {
id: "CalculatorTest::testFail1".to_string(),
name: "CalculatorTest::testFail1".to_string(),
start_position: Range {
start: Position {
line: 23,
character: 4
},
end: Position {
line: 23,
character: MAX_CHAR_LENGTH
}
},
end_position: Range {
start: Position {
line: 28,
character: 0
},
end: Position {
line: 28,
character: 5
}
}
}
]
)
}
}

View file

@ -3,17 +3,17 @@
"testing": {
"command": "testing-language-server",
"trace.server": "verbose",
"filetypes": ["rust", "javascript", "go", "typescript"],
"filetypes": ["rust", "javascript", "go", "typescript", "php"],
"initializationOptions": {
"adapterCommand": {
// "cargo-test": [
// {
// "path": "testing-ls-adapter",
// "extra_args": ["--test-kind=cargo-test"],
// "include_patterns": ["/**/src/**/*.rs"],
// "exclude_patterns": ["/**/target/**"]
// }
// ],
"cargo-test": [
{
"path": "testing-ls-adapter",
"extra_args": ["--test-kind=cargo-test"],
"include_patterns": ["/**/src/**/*.rs"],
"exclude_patterns": ["/**/target/**"]
}
],
"cargo-nextest": [
{
"path": "testing-ls-adapter",
@ -56,6 +56,14 @@
"include_patterns": ["/**/*.go"],
"exclude_patterns": []
}
],
"phpunit": [
{
"path": "testing-ls-adapter",
"extra_args": ["--test-kind=phpunit"],
"include_patterns": ["/**/*Test.php"],
"exclude_patterns": ["/phpunit/vendor/**/*.php"]
}
]
}
}

1
demo/phpunit/.gitignore vendored Normal file
View file

@ -0,0 +1 @@
vendor

2
demo/phpunit/.mise.toml Normal file
View file

@ -0,0 +1,2 @@
[tools]
php = "8.3"

View file

@ -0,0 +1,17 @@
{
"name": "kbwo/phpunit",
"autoload": {
"psr-4": {
"App\\": "src/"
}
},
"authors": [
{
"name": "kbwo",
"email": "kabaaa1126@gmail.com"
}
],
"require-dev": {
"phpunit/phpunit": "^11.3"
}
}

1651
demo/phpunit/composer.lock generated Normal file

File diff suppressed because it is too large Load diff

15
demo/phpunit/output.xml Normal file
View file

@ -0,0 +1,15 @@
<?xml version="1.0" encoding="UTF-8"?>
<testsuites>
<testsuite name="CLI Arguments" tests="3" assertions="3" errors="0" failures="1" skipped="0" time="0.002791">
<testsuite name="Tests\CalculatorTest" file="/home/kbwo/testing-language-server/demo/phpunit/src/CalculatorTest.php" tests="3" assertions="3" errors="0" failures="1" skipped="0" time="0.002791">
<testcase name="testAdd" file="/home/kbwo/testing-language-server/demo/phpunit/src/CalculatorTest.php" line="10" class="Tests\CalculatorTest" classname="Tests.CalculatorTest" assertions="1" time="0.000695"/>
<testcase name="testSubtract" file="/home/kbwo/testing-language-server/demo/phpunit/src/CalculatorTest.php" line="17" class="Tests\CalculatorTest" classname="Tests.CalculatorTest" assertions="1" time="0.000046"/>
<testcase name="testFail1" file="/home/kbwo/testing-language-server/demo/phpunit/src/CalculatorTest.php" line="24" class="Tests\CalculatorTest" classname="Tests.CalculatorTest" assertions="1" time="0.002051">
<failure type="PHPUnit\Framework\ExpectationFailedException">Tests\CalculatorTest::testFail1
Failed asserting that 8 matches expected 1.
/home/kbwo/testing-language-server/demo/phpunit/src/CalculatorTest.php:28</failure>
</testcase>
</testsuite>
</testsuite>
</testsuites>

View file

@ -0,0 +1,16 @@
<?php
namespace App;
class Calculator
{
public function add($a, $b)
{
return $a + $b;
}
public function subtract($a, $b)
{
return $a - $b;
}
}

View file

@ -0,0 +1,30 @@
<?php
namespace Tests;
use App\Calculator;
use PHPUnit\Framework\TestCase;
class CalculatorTest extends TestCase
{
public function testAdd()
{
$calculator = new Calculator();
$result = $calculator->add(2, 3);
$this->assertEquals(5, $result);
}
public function testSubtract()
{
$calculator = new Calculator();
$result = $calculator->subtract(5, 3);
$this->assertEquals(2, $result);
}
public function testFail1()
{
$calculator = new Calculator();
$result = $calculator->subtract(10, 2);
$this->assertEquals(1, $result);
}
}