Skip to content

Commit

Permalink
feat: PHPUnit 10.4 support (#63)
Browse files Browse the repository at this point in the history
Fixes #61
  • Loading branch information
marcoscoelho authored Oct 30, 2023
1 parent 33a99c1 commit 56edee8
Show file tree
Hide file tree
Showing 3 changed files with 232 additions and 3 deletions.
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);
}
}
}
}

0 comments on commit 56edee8

Please sign in to comment.