Skip to content

feat: PHPUnit 10.4 support #63

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

Merged
merged 15 commits into from
Oct 30, 2023
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
39 changes: 39 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ jobs:
- '7.1'
- '7.0'
phpunit-version:
- '10.4.0'
- '10.3.0'
- '10.2.0'
- '10.1.0'
- '10.0.0'
- '9.6.0'
Expand Down Expand Up @@ -143,6 +146,12 @@ jobs:
phpunit-version: '6.0.0'

# PHP 8.0 Exclusions
- php-version: '8.0'
phpunit-version: '10.4.0'
- php-version: '8.0'
phpunit-version: '10.3.0'
- php-version: '8.0'
phpunit-version: '10.2.0'
- php-version: '8.0'
phpunit-version: '10.1.0'
- php-version: '8.0'
Expand Down Expand Up @@ -189,6 +198,12 @@ jobs:
phpunit-version: '6.0.0'

# PHP 7.4 Exclusions
- php-version: '7.4'
phpunit-version: '10.4.0'
- php-version: '7.4'
phpunit-version: '10.3.0'
- php-version: '7.4'
phpunit-version: '10.2.0'
- php-version: '7.4'
phpunit-version: '10.1.0'
- php-version: '7.4'
Expand Down Expand Up @@ -221,12 +236,24 @@ jobs:
phpunit-version: '6.0.0'

# PHP 7.3 Exclusions
- php-version: '7.3'
phpunit-version: '10.4.0'
- php-version: '7.3'
phpunit-version: '10.3.0'
- php-version: '7.3'
phpunit-version: '10.2.0'
- php-version: '7.3'
phpunit-version: '10.1.0'
- php-version: '7.3'
phpunit-version: '10.0.0'

# PHP 7.2 Exclusions
- php-version: '7.2'
phpunit-version: '10.4.0'
- php-version: '7.2'
phpunit-version: '10.3.0'
- php-version: '7.2'
phpunit-version: '10.2.0'
- php-version: '7.2'
phpunit-version: '10.1.0'
- php-version: '7.2'
Expand All @@ -247,6 +274,12 @@ jobs:
phpunit-version: '9.0.0'

# PHP 7.1 Exclusions
- php-version: '7.1'
phpunit-version: '10.4.0'
- php-version: '7.1'
phpunit-version: '10.3.0'
- php-version: '7.1'
phpunit-version: '10.2.0'
- php-version: '7.1'
phpunit-version: '10.1.0'
- php-version: '7.1'
Expand Down Expand Up @@ -279,6 +312,12 @@ jobs:
phpunit-version: '8.0.0'

# PHP 7.0 Exclusions
- php-version: '7.0'
phpunit-version: '10.4.0'
- php-version: '7.0'
phpunit-version: '10.3.0'
- php-version: '7.0'
phpunit-version: '10.2.0'
- php-version: '7.0'
phpunit-version: '10.1.0'
- php-version: '7.0'
Expand Down
55 changes: 54 additions & 1 deletion classes/DefaultArgumentRemoverReturnTypes100.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
use phpmock\generator\MockFunctionGenerator;
use PHPUnit\Framework\MockObject\Invocation;
use PHPUnit\Framework\MockObject\Rule\InvocationOrder;
use ReflectionClass;

/**
* Removes default arguments from the invocation.
Expand Down Expand Up @@ -37,7 +38,7 @@ public function matches(Invocation $invocation) : bool
$invocation,
$iClass ? Invocation::class : Invocation\StaticInvocation::class
);
} else {
} elseif (!$this->shouldRemoveDefaultArgumentsWithReflection($invocation)) {
MockFunctionGenerator::removeDefaultArguments($invocation->parameters);
}

Expand Down Expand Up @@ -72,10 +73,62 @@ public function toString() : string
*/
private function removeDefaultArguments(Invocation $invocation, string $class)
{
if ($this->shouldRemoveDefaultArgumentsWithReflection($invocation)) {
return;
}

$remover = function () {
MockFunctionGenerator::removeDefaultArguments($this->parameters);
};

$remover->bindTo($invocation, $class)();
}

/**
* Alternative to remove default arguments from StaticInvocation or its children (hack)
*
* @SuppressWarnings(PHPMD.StaticAccess)
*/
public static function removeDefaultArgumentsWithReflection(Invocation $invocation): Invocation
{
if (!(new self())->shouldRemoveDefaultArgumentsWithReflection($invocation)) {
return $invocation;
}

$reflection = new ReflectionClass($invocation);

$reflectionReturnType = $reflection->getProperty('returnType');
$reflectionReturnType->setAccessible(true);

$reflectionIsOptional = $reflection->getProperty('isReturnTypeNullable');
$reflectionIsOptional->setAccessible(true);

$reflectionIsProxied = $reflection->getProperty('proxiedCall');
$reflectionIsProxied->setAccessible(true);

$returnType = $reflectionReturnType->getValue($invocation);
$proxiedCall = $reflectionIsProxied->getValue($invocation);

if ($reflectionIsOptional->getValue($invocation)) {
$returnType = '?' . $returnType;
}

$parameters = $invocation->parameters();
MockFunctionGenerator::removeDefaultArguments($parameters);

return new Invocation(
$invocation->className(),
$invocation->methodName(),
$parameters,
$returnType,
$invocation->object(),
false,
$proxiedCall
);
}

protected function shouldRemoveDefaultArgumentsWithReflection(Invocation $invocation)
{
return method_exists($invocation, 'parameters');
}
}
141 changes: 139 additions & 2 deletions classes/PHPMock.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,15 @@

namespace phpmock\phpunit;

use DirectoryIterator;
use phpmock\integration\MockDelegateFunctionBuilder;
use phpmock\MockBuilder;
use phpmock\Deactivatable;
use PHPUnit\Event\Facade;
use PHPUnit\Framework\MockObject\MockObject;
use ReflectionClass;
use ReflectionProperty;
use SebastianBergmann\Template\Template;

/**
* Adds building a function mock functionality into \PHPUnit\Framework\TestCase.
Expand Down Expand Up @@ -38,6 +41,13 @@
*/
trait PHPMock
{
public static $templatesPath = '/tmp';

private $phpunitVersionClass = '\\PHPUnit\\Runner\\Version';
private $openInvocation = 'new \\PHPUnit\\Framework\\MockObject\\Invocation(';
private $openWrapper = '\\phpmock\\phpunit\\DefaultArgumentRemover::removeDefaultArgumentsWithReflection(';
private $closeFunc = ')';

/**
* Returns the enabled function mock.
*
Expand All @@ -50,6 +60,8 @@ trait PHPMock
*/
public function getFunctionMock($namespace, $name)
{
$this->prepareCustomTemplates();

$delegateBuilder = new MockDelegateFunctionBuilder();
$delegateBuilder->build($name);

Expand All @@ -70,8 +82,7 @@ public function getFunctionMock($namespace, $name)

$this->registerForTearDown($functionMock);

$proxy = new MockObjectProxy($mock);
return $proxy;
return new MockObjectProxy($mock);
}

private function addMatcher($mock, $name)
Expand Down Expand Up @@ -145,4 +156,130 @@ public static function defineFunctionMock($namespace, $name)
->build()
->define();
}

/**
* Adds a wrapper method to the Invocable object instance that makes it
* possible to remove optional parameters when it is declared read-only.
*
* @return void
*
* @SuppressWarnings(PHPMD.StaticAccess)
* @SuppressWarnings(PHPMD.IfStatementAssignment)
*/
private function prepareCustomTemplates()
{
if (!($this->shouldPrepareCustomTemplates() &&
is_dir(static::$templatesPath) &&
($phpunitTemplatesDir = $this->getPhpunitTemplatesDir())
)) {
return;
}

$templatesDir = realpath(static::$templatesPath);
$directoryIterator = new DirectoryIterator($phpunitTemplatesDir);

$templates = [];

$prefix = 'phpmock-phpunit-' . $this->getPhpUnitVersion() . '-';

foreach ($directoryIterator as $fileinfo) {
if ($fileinfo->getExtension() !== 'tpl') {
continue;
}

$filename = $fileinfo->getFilename();
$customTemplateFile = $templatesDir . DIRECTORY_SEPARATOR . $prefix . $filename;
$templateFile = $phpunitTemplatesDir . DIRECTORY_SEPARATOR . $filename;

$this->createCustomTemplateFile($templateFile, $customTemplateFile);

if (file_exists($customTemplateFile)) {
$templates[$templateFile] = new Template($customTemplateFile);
}
}

$mockMethodClasses = [
'PHPUnit\\Framework\\MockObject\\Generator\\MockMethod',
'PHPUnit\\Framework\\MockObject\\MockMethod',
];

foreach ($mockMethodClasses as $mockMethodClass) {
if (class_exists($mockMethodClass)) {
$reflection = new ReflectionClass($mockMethodClass);

$reflectionTemplates = $reflection->getProperty('templates');
$reflectionTemplates->setAccessible(true);

$reflectionTemplates->setValue($templates);

break;
}
}
}

private function shouldPrepareCustomTemplates()
{
return class_exists($this->phpunitVersionClass)
&& version_compare($this->getPhpUnitVersion(), '10.0.0') >= 0;
}

private function getPhpUnitVersion()
{
return call_user_func([$this->phpunitVersionClass, 'id']);
}

/**
* Detects the PHPUnit templates dir
*
* @return string|null
*/
private function getPhpunitTemplatesDir()
{
$phpunitLocations = [
__DIR__ . '/../../',
__DIR__ . '/../vendor/',
];

$phpunitRelativePath = '/phpunit/phpunit/src/Framework/MockObject/Generator';

foreach ($phpunitLocations as $prefix) {
$possibleDirs = [
$prefix . $phpunitRelativePath . '/templates',
$prefix . $phpunitRelativePath,
];

foreach ($possibleDirs as $dir) {
if (is_dir($dir)) {
return realpath($dir);
}
}
}
}

/**
* Clones original template with the wrapper
*
* @param string $templateFile Template filename
* @param string $customTemplateFile Custom template filename
*
* @return void
*
* @SuppressWarnings(PHPMD.IfStatementAssignment)
*/
private function createCustomTemplateFile(string $templateFile, string $customTemplateFile)
{
$template = file_get_contents($templateFile);

if (($start = strpos($template, $this->openInvocation)) !== false &&
($end = strpos($template, $this->closeFunc, $start)) !== false
) {
$template = substr_replace($template, $this->closeFunc, $end, 0);
$template = substr_replace($template, $this->openWrapper, $start, 0);

if ($file = fopen($customTemplateFile, 'w+')) {
fputs($file, $template);
fclose($file);
}
}
}
}