-
Notifications
You must be signed in to change notification settings - Fork 13
Add covers annotation support #47
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -47,11 +47,13 @@ | |
}, | ||
"require-dev": { | ||
"drupol/php-conventions": "^3.0", | ||
"sebastian/code-unit": "^1.0.8", | ||
"vimeo/psalm": "^4.7" | ||
}, | ||
"suggest": { | ||
"ext-pcov": "Install PCov extension to generate code coverage.", | ||
"ext-xdebug": "Install Xdebug to generate phpspec code coverage." | ||
"ext-xdebug": "Install Xdebug to generate phpspec code coverage.", | ||
"sebastian/code-unit": "Install code-unit to support @covers annotations in tests." | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe adding a not in the README about the |
||
}, | ||
"extra": { | ||
"branch-alias": { | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,11 @@ | ||
version: '3.2' | ||
services: | ||
php: | ||
build: | ||
context: ./ | ||
dockerfile: docker/Dockerfile | ||
tty: true | ||
hostname: phpspec-code-coverage-php | ||
container_name: phpspec-code-coverage-php | ||
volumes: | ||
- ./:/var/www/html |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,15 @@ | ||
FROM php:7.3-cli-alpine3.12 | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Are this
In the GitHub action configuration we are testing the code against a large combination of PHP version. However it's not an exhaustive list only a large one. Helping developers to work on this lib locally is nice but we need to be sure that this env will match our requirements. Also I think that those changes must not be here in this PR, maybe in another one regarding the development environment. Documentation must also be written to explain how to use the choosen tools. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes it's only required for local development. Basically I did it so I don't have to install any PHP extensions on my host but can spin up a local dev/test environment within a few minutes where I can run the unit tests (which need at least xdebug or pcov). I used PHP 7.3 because it's the minimum requirement of this package (https://github.com/friends-of-phpspec/phpspec-code-coverage/blob/master/composer.json#L41). Of course we need to test it against many other versions but at least I immediately realize during development whether I use a feature that is not available in PHP 7.3 and if the code works on the minimum required PHP version. BC in PHP is quite good so all changes should then run on >7.3 versions smoothly as well. But I will move it out into another PR. |
||
|
||
# SYS: Install required packages | ||
RUN apk --no-cache upgrade && \ | ||
apk --no-cache add bash git sudo openssh autoconf gcc g++ make gettext make | ||
|
||
# COMPOSER: install binary | ||
RUN curl -sS https://getcomposer.org/installer | php -- --install-dir=/usr/bin --filename=composer | ||
|
||
# PHP: Install php extensions | ||
RUN pecl channel-update pecl.php.net && \ | ||
pecl install pcov && \ | ||
docker-php-ext-enable pcov | ||
|
||
WORKDIR /var/www/html |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,191 @@ | ||
<?php | ||
|
||
declare(strict_types=1); | ||
|
||
namespace FriendsOfPhpSpec\PhpSpec\CodeCoverage\Annotation; | ||
|
||
use FriendsOfPhpSpec\PhpSpec\CodeCoverage\Exception\CodeCoverageException; | ||
use FriendsOfPhpSpec\PhpSpec\CodeCoverage\Exception\InvalidCoversTargetException; | ||
use ReflectionException; | ||
use SebastianBergmann\CodeUnit\CodeUnitCollection; | ||
use SebastianBergmann\CodeUnit\InvalidCodeUnitException; | ||
use SebastianBergmann\CodeUnit\Mapper; | ||
|
||
use function count; | ||
|
||
final class CoversAnnotationUtil | ||
{ | ||
/** | ||
* @var Registry | ||
*/ | ||
private $registry; | ||
|
||
public function __construct(Registry $registry) | ||
{ | ||
$this->registry = $registry; | ||
} | ||
|
||
/** | ||
* @param class-string $className | ||
* | ||
* @throws CodeCoverageException | ||
* @throws InvalidCoversTargetException | ||
* @throws ReflectionException | ||
* | ||
* @return array<string, array>|false | ||
*/ | ||
public function getLinesToBeCovered(string $className, string $methodName) | ||
{ | ||
$annotations = $this->parseTestMethodAnnotations( | ||
$className, | ||
$methodName | ||
); | ||
|
||
if (!$this->shouldCoversAnnotationBeUsed($annotations)) { | ||
return false; | ||
} | ||
|
||
return $this->getLinesToBeCoveredOrUsed($className, $methodName, 'covers'); | ||
} | ||
|
||
/** | ||
* Returns lines of code specified with the. | ||
* | ||
* @param class-string $className . | ||
* | ||
* @throws CodeCoverageException | ||
* @throws InvalidCoversTargetException | ||
* @throws ReflectionException | ||
* | ||
* @return array<string, array> | ||
* | ||
* @uses annotation. | ||
*/ | ||
public function getLinesToBeUsed(string $className, string $methodName): array | ||
{ | ||
return $this->getLinesToBeCoveredOrUsed($className, $methodName, 'uses'); | ||
} | ||
|
||
/** | ||
* @param class-string $className | ||
* | ||
* @throws ReflectionException | ||
* | ||
* @return array<string, mixed> | ||
*/ | ||
public function parseTestMethodAnnotations(string $className, ?string $methodName = ''): array | ||
{ | ||
if (null !== $methodName) { | ||
try { | ||
return [ | ||
'method' => $this->registry->forMethod($className, $methodName)->symbolAnnotations(), | ||
'class' => $this->registry->forClassName($className)->symbolAnnotations(), | ||
]; | ||
} catch (ReflectionException $methodNotFound) { | ||
// ignored | ||
} | ||
} | ||
|
||
return [ | ||
'method' => null, | ||
'class' => $this->registry->forClassName($className)->symbolAnnotations(), | ||
]; | ||
} | ||
|
||
/** | ||
* @param class-string $className | ||
* | ||
* @throws CodeCoverageException | ||
* @throws InvalidCoversTargetException | ||
* @throws ReflectionException | ||
* | ||
* @return array<string, array> | ||
*/ | ||
private function getLinesToBeCoveredOrUsed(string $className, string $methodName, string $mode): array | ||
{ | ||
$annotations = $this->parseTestMethodAnnotations( | ||
$className, | ||
$methodName | ||
); | ||
|
||
$classShortcut = null; | ||
|
||
if (!empty($annotations['class'][$mode . 'DefaultClass'])) { | ||
if (count($annotations['class'][$mode . 'DefaultClass']) > 1) { | ||
throw new CodeCoverageException( | ||
sprintf( | ||
'More than one @%sClass annotation in class or interface "%s".', | ||
$mode, | ||
$className | ||
) | ||
); | ||
} | ||
|
||
$classShortcut = $annotations['class'][$mode . 'DefaultClass'][0]; | ||
} | ||
|
||
$list = $annotations['class'][$mode] ?? []; | ||
|
||
if (isset($annotations['method'][$mode])) { | ||
$list = array_merge($list, $annotations['method'][$mode]); | ||
} | ||
|
||
$codeUnits = CodeUnitCollection::fromArray([]); | ||
$mapper = new Mapper(); | ||
|
||
foreach (array_unique($list) as $element) { | ||
if ($classShortcut && strncmp($element, '::', 2) === 0) { | ||
$element = $classShortcut . $element; | ||
} | ||
|
||
$element = preg_replace('/[\s()]+$/', '', $element); | ||
$element = explode(' ', $element); | ||
$element = $element[0]; | ||
|
||
if ('covers' === $mode && interface_exists($element)) { | ||
throw new InvalidCoversTargetException( | ||
sprintf( | ||
'Trying to @cover interface "%s".', | ||
$element | ||
) | ||
); | ||
} | ||
|
||
try { | ||
$codeUnits = $codeUnits->mergeWith($mapper->stringToCodeUnits($element)); | ||
} catch (InvalidCodeUnitException $e) { | ||
throw new InvalidCoversTargetException( | ||
sprintf( | ||
'"@%s %s" is invalid', | ||
$mode, | ||
$element | ||
), | ||
(int) $e->getCode(), | ||
$e | ||
); | ||
} | ||
} | ||
|
||
return $mapper->codeUnitsToSourceLines($codeUnits); | ||
} | ||
|
||
/** | ||
* @param array<string, array<string, mixed>> $annotations | ||
*/ | ||
private function shouldCoversAnnotationBeUsed(array $annotations): bool | ||
{ | ||
if (isset($annotations['method']['coversNothing'])) { | ||
return false; | ||
} | ||
|
||
if (isset($annotations['method']['covers'])) { | ||
return true; | ||
} | ||
|
||
if (isset($annotations['class']['coversNothing'])) { | ||
return false; | ||
} | ||
|
||
return true; | ||
} | ||
} |
Uh oh!
There was an error while loading. Please reload this page.