Skip to content
Open
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
64 changes: 54 additions & 10 deletions src/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
use Pest\TypeCoverage\Logging\JsonLogger;
use Pest\TypeCoverage\Logging\NullLogger;
use Pest\TypeCoverage\Support\ConfigurationSourceDetector;
use Pest\TypeCoverage\Support\FileResolver;
use Symfony\Component\Console\Output\OutputInterface;
use Symfony\Component\Finder\Finder;

Expand Down Expand Up @@ -63,6 +64,8 @@ public function handleOriginalArguments(array $arguments): void

$startTime = microtime(true);

$filePaths = $this->extractFileArguments($arguments);

foreach ($arguments as $argument) {
if (str_starts_with($argument, '--min')) {
// grab the value of the --min argument
Expand Down Expand Up @@ -111,24 +114,39 @@ public function handleOriginalArguments(array $arguments): void
}
}

$source = ConfigurationSourceDetector::detect();
if ($filePaths !== []) {
$resolvedFiles = FileResolver::resolve($filePaths);

if ($source === []) {
View::render('components.badge', [
'type' => 'ERROR',
'content' => 'No source section found. Did you forget to add a `source` section to your `phpunit.xml` file?',
]);
if ($resolvedFiles === []) {
View::render('components.badge', [
'type' => 'ERROR',
'content' => 'No PHP files found in the specified paths.',
]);

$this->exit(1);
}
$this->exit(1);
}

$files = Finder::create()->in($source)->name('*.php')->files();
$files = array_combine($resolvedFiles, $resolvedFiles);
} else {
$source = ConfigurationSourceDetector::detect();

if ($source === []) {
View::render('components.badge', [
'type' => 'ERROR',
'content' => 'No source section found. Did you forget to add a `source` section to your `phpunit.xml` file?',
]);

$this->exit(1);
}

$files = Finder::create()->in($source)->name('*.php')->files();
}
$totals = [];

$this->output->writeln(['']);

Analyser::analyse(
array_keys(iterator_to_array($files)),
is_array($files) ? array_keys($files) : array_keys(iterator_to_array($files)),
function (Result $result) use (&$totals): void {
$path = str_replace(TestSuite::getInstance()->rootPath.DIRECTORY_SEPARATOR, '', $result->file);

Expand Down Expand Up @@ -215,6 +233,32 @@ function (Result $result) use (&$totals): void {
$this->exit($exitCode);
}

/**
* Extracts file/directory arguments from the command line that come after the -- separator.
*
* @param array<int, string> $arguments
* @return array<int, string>
*/
public function extractFileArguments(array $arguments): array
{
$filePaths = [];
$afterDoubleDash = false;

foreach ($arguments as $argument) {
if ($argument === '--') {
$afterDoubleDash = true;

continue;
}

if ($afterDoubleDash && ! str_starts_with($argument, '-')) {
$filePaths[] = $argument;
}
}

return $filePaths;
}

/**
* Exits the process with the given code.
*/
Expand Down
49 changes: 49 additions & 0 deletions src/Support/FileResolver.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Pest\TypeCoverage\Support;

use Symfony\Component\Finder\Finder;

/**
* @internal
*/
final class FileResolver
{
/**
* Resolves a mixed array of files and directories to a list of PHP files.
*
* @param array<int, string> $paths
* @return array<int, string>
*/
public static function resolve(array $paths): array
{
if ($paths === []) {
return [];
}

$files = [];

foreach ($paths as $path) {
$realPath = realpath($path);

if ($realPath === false || ! file_exists($realPath)) {
continue;
}

if (is_file($realPath)) {
if (pathinfo($realPath, PATHINFO_EXTENSION) === 'php') {
$files[] = $realPath;
}
} elseif (is_dir($realPath)) {
$finder = Finder::create()->in($realPath)->name('*.php')->files();
foreach ($finder as $file) {
$files[] = $file->getRealPath();
}
}
}

return array_unique($files);
}
}
1 change: 1 addition & 0 deletions tests/Fixtures/TestFiles/EmptyDir/readme.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This directory contains no PHP files and is used for testing scenarios where no PHP files are found.
16 changes: 16 additions & 0 deletions tests/Fixtures/TestFiles/MissingTypes.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
<?php

namespace Tests\Fixtures\TestFiles;

class MissingTypes
{
public function processData($data)
{
return $data;
}

public function calculate($a, $b)
{
return $a + $b;
}
}
13 changes: 13 additions & 0 deletions tests/Fixtures/TestFiles/SimpleClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

declare(strict_types=1);

namespace Tests\Fixtures\TestFiles;

class SimpleClass
{
public function getName(): string
{
return 'simple';
}
}
18 changes: 18 additions & 0 deletions tests/Fixtures/TestFiles/SubDir/NestedClass.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<?php

namespace Tests\Fixtures\TestFiles\SubDir;

class NestedClass
{
private $value;

public function setValue($value)
{
$this->value = $value;
}

public function getValue()
{
return $this->value;
}
}
1 change: 1 addition & 0 deletions tests/Fixtures/TestFiles/readme.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
This is a non-PHP file that should be ignored during file resolution.
154 changes: 154 additions & 0 deletions tests/Plugin.php
Original file line number Diff line number Diff line change
Expand Up @@ -169,3 +169,157 @@ public function exit(int $code): never

unlink(__DIR__.'/../test.json');
})->todo();

test('extracts file arguments correctly', function () {
$plugin = new class(new BufferedOutput) extends Plugin
{
public function exit(int $code): never
{
throw new Exception($code);
}
};

$arguments = ['--type-coverage', '--', 'file1.php', 'dir1/', 'file2.php'];
expect($plugin->extractFileArguments($arguments))
->toBe(['file1.php', 'dir1/', 'file2.php']);

$arguments = ['--type-coverage', '--min=80'];
expect($plugin->extractFileArguments($arguments))->toBe([]);

$arguments = ['--type-coverage', '--', 'file1.php', '--some-option'];
expect($plugin->extractFileArguments($arguments))->toBe(['file1.php']);
});

test('handles specific file arguments', function () {
$output = new BufferedOutput;
$plugin = new class($output) extends Plugin
{
public function exit(int $code): never
{
throw new Exception($code);
}
};

$testFile = __DIR__.'/Fixtures/TestFiles/SimpleClass.php';
$arguments = ['--type-coverage', '--', $testFile];

expect(fn () => $plugin->handleOriginalArguments($arguments))->toThrow(Exception::class, 0);

$output = $output->fetch();
expect($output)->toContain('SimpleClass.php')
->and($output)->toContain('100%')
->and($output)->toContain('Total: 100.0 %');
});

test('handles directory arguments', function () {
$output = new BufferedOutput;
$plugin = new class($output) extends Plugin
{
public function exit(int $code): never
{
throw new Exception($code);
}
};

$testDir = __DIR__.'/Fixtures/TestFiles';
$arguments = ['--type-coverage', '--', $testDir];

expect(fn () => $plugin->handleOriginalArguments($arguments))->toThrow(Exception::class, 0);

$output = $output->fetch();
expect($output)->toContain('SimpleClass.php')
->and($output)->toContain('MissingTypes.php')
->and($output)->toContain('NestedClass.php')
->and($output)->not->toContain('readme.txt');
});

test('handles mixed file and directory arguments', function () {
$output = new BufferedOutput;
$plugin = new class($output) extends Plugin
{
public function exit(int $code): never
{
throw new Exception($code);
}
};

$testFile = __DIR__.'/Fixtures/TestFiles/SimpleClass.php';
$testSubDir = __DIR__.'/Fixtures/TestFiles/SubDir';
$arguments = ['--type-coverage', '--', $testFile, $testSubDir];

expect(fn () => $plugin->handleOriginalArguments($arguments))->toThrow(Exception::class, 0);

$output = $output->fetch();
expect($output)->toContain('SimpleClass.php')
->and($output)->toContain('NestedClass.php')
->and($output)->not->toContain('MissingTypes.php');
});

test('shows error when no PHP files found in specified paths', function () {
$output = new BufferedOutput;
$plugin = new class($output) extends Plugin
{
public function exit(int $code): never
{
throw new Exception($code);
}
};

$emptyDir = __DIR__.'/Fixtures/TestFiles/EmptyDir';
$arguments = ['--type-coverage', '--', $emptyDir];

$initialLevel = ob_get_level();

expect(fn () => $plugin->handleOriginalArguments($arguments))->toThrow(Exception::class, '1');

while (ob_get_level() > $initialLevel) {
ob_end_clean();
}
});

test('works with non-existent files and valid files mixed', function () {
$output = new BufferedOutput;
$plugin = new class($output) extends Plugin
{
public function exit(int $code): never
{
throw new Exception($code);
}
};

$validFile = __DIR__.'/Fixtures/TestFiles/SimpleClass.php';
$invalidFile = __DIR__.'/Fixtures/TestFiles/NonExistent.php';
$arguments = ['--type-coverage', '--', $validFile, $invalidFile];

expect(fn () => $plugin->handleOriginalArguments($arguments))->toThrow(Exception::class, 0);

$output = $output->fetch();
expect($output)->toContain('SimpleClass.php')
->and($output)->toContain('Total: 100.0 %');
});

test('combines file arguments with other options', function () {
$output = new BufferedOutput;
$plugin = new class($output) extends Plugin
{
public function exit(int $code): never
{
throw new Exception((string) $code);
}
};

$testFile = __DIR__.'/Fixtures/TestFiles/MissingTypes.php';
$arguments = ['--type-coverage', '--compact', '--min=90.0', '--', $testFile];

$initialLevel = ob_get_level();

expect(fn () => $plugin->handleOriginalArguments($arguments))->toThrow(Exception::class, '1');

while (ob_get_level() > $initialLevel) {
ob_end_clean();
}

$output = $output->fetch();
expect($output)->toContain('MissingTypes.php')
->and($output)->toContain('50%');
});
Loading