Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
371 changes: 198 additions & 173 deletions AGENTS.md

Large diffs are not rendered by default.

370 changes: 130 additions & 240 deletions CLAUDE.md

Large diffs are not rendered by default.

198 changes: 4 additions & 194 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,185 +15,10 @@ Token-optimized PHPUnit testing tools for AI assistants. This extension provides

```bash
composer require --dev matesofmate/phpunit-extension
vendor/bin/mate discover
```

The extension is automatically discovered by Symfony AI Mate.

## Available Tools

### `phpunit_run_suite`

Run the entire PHPUnit test suite.

**Parameters:**
- `configuration` (optional): Path to phpunit.xml configuration file
- `filter` (optional): Filter pattern for test names
- `stopOnFailure` (optional): Stop execution on first failure

**Examples:**
```php
// Run all tests
phpunit_run_suite()

// Run with filter
phpunit_run_suite(filter: "UserTest")

// Stop on first failure
phpunit_run_suite(stopOnFailure: true)

// Custom configuration
phpunit_run_suite(configuration: "phpunit.custom.xml")
```

### `phpunit_run_file`

Run PHPUnit tests from a specific file.

**Parameters:**
- `file` (required): Path to test file
- `filter` (optional): Filter pattern for method names
- `stopOnFailure` (optional): Stop execution on first failure

**Examples:**
```php
// Run all tests in file
phpunit_run_file("tests/Service/UserServiceTest.php")

// Run specific method in file
phpunit_run_file("tests/Api/AuthTest.php", filter: "testLogin")

// Stop on first failure
phpunit_run_file("tests/Unit/CalculatorTest.php", stopOnFailure: true)
```

### `phpunit_run_method`

Run a single PHPUnit test method.

**Parameters:**
- `class` (required): Fully qualified class name
- `method` (required): Test method name

**Examples:**
```php
// Run specific test method
phpunit_run_method("App\\Tests\\UserServiceTest", "testCreateUser")

// Run another test
phpunit_run_method("App\\Tests\\Api\\AuthTest", "testLoginWithValidCredentials")
```

### `phpunit_list_tests`

List all available PHPUnit tests in the project.

**Parameters:**
- `directory` (optional): Specific directory to scan (defaults to configured test directories)

**Examples:**
```php
// List all tests
phpunit_list_tests()

// List tests in specific directory
phpunit_list_tests(directory: "tests/Unit")
```

## Output Format (TOON)

TOON (Token-Oriented Object Notation) provides minimal token usage while maintaining readability:

**Successful run:**
```
summary{tests,passed,failed,errors,warnings,skipped,time}:
42|42|0|0|0|0|1.234s

status:OK
```

**Failed run:**
```
summary{tests,passed,failed,errors,warnings,skipped,time}:
156|152|3|1|0|0|4.892s

failures[3]{class,method,message,file,line}:
UserServiceTest|testCreateUser|Expected 200 got 401|UserServiceTest.php|45
OrderTest|testCalculateTotal|99.99 !== 100.00|OrderTest.php|112
PaymentTest|testRefund|Null returned|PaymentTest.php|78

errors[1]{class,method,exception,file,line}:
DatabaseTest|testConnection|PDOException: Connection refused|DatabaseTest.php|23

status:FAILED
```

**Test listing:**
```
tests[4]{file,class,method}:
tests/UserTest.php|App\Tests\UserTest|testCreate
tests/UserTest.php|App\Tests\UserTest|testUpdate
tests/UserTest.php|App\Tests\UserTest|testDelete
tests/OrderTest.php|App\Tests\OrderTest|testCalculateTotal
```

### Token Efficiency

Compared to standard JSON output:

**JSON (186 tokens):**
```json
{
"tests": 42,
"passed": 39,
"failed": 2,
"time": "1.234s",
"failures": [
{"class": "UserTest", "method": "testCreate", "message": "Expected 200"}
]
}
```

**TOON (~110 tokens - 41% reduction):**
```
summary{tests,passed,failed,time}:
42|39|2|1.234s

failures[1]{class,method,message}:
UserTest|testCreate|Expected 200
```

## How It Works

### Architecture

The extension uses a layered architecture:

1. **Runner Layer** - Executes PHPUnit via Symfony Process (uses current PHP binary)
2. **Parser Layer** - Extracts structured data from JUnit XML output
3. **Formatter Layer** - Converts results to TOON format using helgesverre/toon
4. **Tools Layer** - MCP tools with `#[McpTool]` attributes

### Process Flow

```
User Request
MCP Tool (RunSuiteTool, RunFileTool, etc.)
PhpunitRunner (Symfony Process with current PHP binary + PHPUnit)
JUnit XML Output
JunitXmlParser (Extract failures, errors, summary)
ToonFormatter (Convert to TOON format)
Return to AI Assistant
```

### PHP Binary Detection

The extension automatically uses the same PHP binary that's currently running (`PHP_BINARY`), ensuring consistency between your environment and test execution.
The extension is automatically enabled by Symfony AI Mate.

## Development

Expand All @@ -210,22 +35,6 @@ composer lint
composer fix
```

### Testing

The extension includes comprehensive tests:

- Unit tests for all core components
- Integration tests for tool execution
- PHPStan level 8 compliance
- PHP CS Fixer code style enforcement
- Rector PHP 8.2+ modernization

### CI/CD

GitHub Actions automatically runs on every push and pull request:
- **Lint**: Validates composer.json, runs Rector, PHP CS Fixer, PHPStan
- **Test**: Runs PHPUnit on PHP 8.2 and 8.3

## Requirements

- PHP 8.2 or higher
Expand All @@ -234,7 +43,7 @@ GitHub Actions automatically runs on every push and pull request:

## Contributing

Contributions are welcome! Please see [CONTRIBUTING.md](CONTRIBUTING.md) for details.
Contributions are welcome! Please see [CONTRIBUTING.md](https://github.com/MatesOfMate/.github/blob/main/CONTRIBUTING.md) for details.

## License

Expand All @@ -243,6 +52,7 @@ MIT License - see [LICENSE](LICENSE) file for details.
## Resources

- [Symfony AI Mate Documentation](https://symfony.com/doc/current/ai/components/mate.html)
- [PHPUnit Documentation](https://phpunit.de/documentation.html)
- [TOON Format Specification](https://github.com/HelgeSverre/toon-php)
- [MatesOfMate Organization](https://github.com/matesofmate)

Expand Down
13 changes: 5 additions & 8 deletions config/config.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,6 @@
*/

use MatesOfMate\Common\Process\ProcessExecutor;
use MatesOfMate\Common\Truncator\MessageTruncator;
use MatesOfMate\PHPUnitExtension\Capability\ListTestsTool;
use MatesOfMate\PHPUnitExtension\Capability\RunFileTool;
use MatesOfMate\PHPUnitExtension\Capability\RunMethodTool;
Expand All @@ -22,24 +21,22 @@
use MatesOfMate\PHPUnitExtension\Runner\PhpunitRunner;
use Symfony\Component\DependencyInjection\Loader\Configurator\ContainerConfigurator;

use function Symfony\Component\DependencyInjection\Loader\Configurator\service;

return static function (ContainerConfigurator $container): void {
$services = $container->services()
->defaults()
->autowire()
->autoconfigure();

// Core infrastructure
$services->set(ProcessExecutor::class)
$services->set('matesofmate_phpunit.process_executor', ProcessExecutor::class)
->arg('$vendorPaths', ['%mate.root_dir%/vendor/bin/phpunit']);
$services->set(PhpunitRunner::class);
$services->set(PhpunitRunner::class)
->arg('$executor', service('matesofmate_phpunit.process_executor'));

$services->set(JunitXmlParser::class);
$services->set(ToonFormatter::class);
$services->set(MessageTruncator::class)
->arg('$prefixes', [
'Failed asserting that ',
'Expectation failed for ',
]);

$services->set(ConfigurationDetector::class)
->arg('$projectRoot', '%mate.root_dir%');
Expand Down
21 changes: 18 additions & 3 deletions src/Capability/RunFileTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use MatesOfMate\PHPUnitExtension\Parser\JunitXmlParser;
use MatesOfMate\PHPUnitExtension\Runner\PhpunitRunner;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Attribute\Schema;

/**
* Runs PHPUnit tests from a specific file with token-optimized output.
Expand All @@ -39,13 +40,27 @@ public function __construct(
description: 'Run PHPUnit tests from a specific file. Returns token-optimized TOON format. Available modes: "default" (summary + failures/errors), "summary" (just totals and status), "detailed" (full error messages without truncation). Use for: testing changes to a single test file, debugging specific test class, focused test execution.'
)]
public function execute(
string $file,
#[Schema(
description: 'Path to the PHP test file (relative to project root)',
pattern: 'Test\.php$'
)]
?string $file = null,
#[Schema(
description: 'Filter pattern to match specific test names or methods'
)]
?string $filter = null,
#[Schema(
description: 'Stop execution upon first failure or error'
)]
bool $stopOnFailure = false,
#[Schema(
description: 'Output format mode',
enum: ['default', 'summary', 'detailed']
)]
string $mode = 'default',
): string {
if (!file_exists($file)) {
throw new \InvalidArgumentException("Test file not found: {$file}");
if (null === $file) {
throw new \InvalidArgumentException('The "file" parameter is required for phpunit-run-file tool.');
}

$args = $this->buildPhpunitArgs(
Expand Down
24 changes: 22 additions & 2 deletions src/Capability/RunMethodTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use MatesOfMate\PHPUnitExtension\Parser\JunitXmlParser;
use MatesOfMate\PHPUnitExtension\Runner\PhpunitRunner;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Attribute\Schema;

/**
* Runs a single PHPUnit test method with token-optimized output.
Expand All @@ -39,10 +40,29 @@ public function __construct(
description: 'Run a single PHPUnit test method. Returns token-optimized TOON format. Available modes: "default" (summary + failure/error details), "summary" (just totals and status), "detailed" (full error messages without truncation). Use for: debugging a specific failing test, verifying a single test fix, isolated test execution.'
)]
public function execute(
string $class,
string $method,
#[Schema(
description: 'Fully qualified test class name (e.g., "Tests\\Unit\\CalculatorTest")',
pattern: '^[a-zA-Z_\\x80-\\xff][a-zA-Z0-9_\\x80-\\xff\\\\]*$'
)]
?string $class = null,
#[Schema(
description: 'Test method name (e.g., "testAddition")',
pattern: '^test[a-zA-Z0-9_]+$|^[a-zA-Z_][a-zA-Z0-9_]*$'
)]
?string $method = null,
#[Schema(
description: 'Output format mode',
enum: ['default', 'summary', 'detailed']
)]
string $mode = 'default',
): string {
if (null === $class) {
throw new \InvalidArgumentException('The "class" parameter is required for phpunit-run-method tool.');
}
if (null === $method) {
throw new \InvalidArgumentException('The "method" parameter is required for phpunit-run-method tool.');
}

$filter = \sprintf('%s::%s$', preg_quote($class, '/'), preg_quote($method, '/'));

$args = $this->buildPhpunitArgs(filter: $filter);
Expand Down
14 changes: 14 additions & 0 deletions src/Capability/RunSuiteTool.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@
use MatesOfMate\PHPUnitExtension\Parser\JunitXmlParser;
use MatesOfMate\PHPUnitExtension\Runner\PhpunitRunner;
use Mcp\Capability\Attribute\McpTool;
use Mcp\Capability\Attribute\Schema;

/**
* Runs the full PHPUnit test suite with token-optimized output.
Expand All @@ -39,9 +40,22 @@ public function __construct(
description: 'Run the full PHPUnit test suite. Returns token-optimized TOON format. Available modes: "default" (summary + failures/errors with truncated messages), "summary" (just totals and status), "detailed" (full error messages without truncation), "by-file" (errors grouped by file path), "by-class" (errors grouped by test class). Use for: running all tests, CI validation, checking overall test health.'
)]
public function execute(
#[Schema(
description: 'Path to PHPUnit configuration file (defaults to auto-detection)'
)]
?string $configuration = null,
#[Schema(
description: 'Filter pattern to match specific test names or methods'
)]
?string $filter = null,
#[Schema(
description: 'Stop execution upon first failure or error'
)]
bool $stopOnFailure = false,
#[Schema(
description: 'Output format mode',
enum: ['default', 'summary', 'detailed', 'by-file', 'by-class']
)]
string $mode = 'default',
): string {
$args = $this->buildPhpunitArgs(
Expand Down
5 changes: 4 additions & 1 deletion src/Parser/JunitXmlParser.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,10 @@
class JunitXmlParser
{
public function __construct(
private readonly MessageTruncator $truncator = new MessageTruncator(),
private readonly MessageTruncator $truncator = new MessageTruncator([
'Failed asserting that ',
'Expectation failed for ',
]),
) {
}

Expand Down
5 changes: 3 additions & 2 deletions src/Runner/PhpunitRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,8 +22,9 @@
*/
class PhpunitRunner
{
public function __construct(private readonly ProcessExecutor $executor)
{
public function __construct(
private readonly ProcessExecutor $executor,
) {
}

/**
Expand Down