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
4 changes: 2 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
"description": "Allows mocking otherwise untestable PHP functions through the use of namespaces",
"license": "MIT",
"require": {
"php": "~7"
"php": "~7.1"
},
"require-dev": {
"phpunit/phpunit": "~6"
"phpunit/phpunit": "~7"
},
"authors": [
{
Expand Down
16 changes: 1 addition & 15 deletions phpunit.xml.dist
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,7 @@
convertErrorsToExceptions="true"
convertWarningsToExceptions="true"
convertNoticesToExceptions="true"
mapTestClassNameToCoveredClassName="true"
bootstrap="vendor/autoload.php"
strict="true"
verbose="true"
colors="true">

Expand All @@ -16,18 +14,6 @@
</testsuites>

<logging>
<log type="coverage-html" target="build/coverage" title="ExceptionBundle"
charset="UTF-8" yui="true" highlight="true"
lowUpperBound="35" highLowerBound="70"/>
<log type="coverage-clover" target="build/logs/clover.xml"/>
<log type="junit" target="build/logs/junit.xml" logIncompleteSkipped="false"/>
<log type="junit" target="build/logs/junit.xml"/>
</logging>

<filter>
<blacklist>
<directory suffix=".php">tests/</directory>
<directory suffix=".php">vendor/</directory>
<directory suffix=".php">/usr/share/php</directory>
</blacklist>
</filter>
</phpunit>
53 changes: 23 additions & 30 deletions src/PHPUnit/Extension/FunctionMocker.php
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,10 @@
namespace PHPUnit\Extension;

use PHPUnit\Extension\FunctionMocker\CodeGenerator;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\TestCase;
use function bin2hex;
use function random_bytes;

class FunctionMocker
{
Expand All @@ -15,6 +18,9 @@ class FunctionMocker
/** @var array */
private $functions = array();

/** @var array */
private $constants = [];

/** @var array */
private static $mockedFunctions = array();

Expand All @@ -30,22 +36,18 @@ private function __construct(TestCase $testCase, $namespace)
* Example: PHP global namespace function setcookie() needs to be overridden in order to test
* if a cookie gets set. When setcookie() is called from inside a class in the namespace
* \Foo\Bar the mock setcookie() created here will be used instead to the real function.
*
* @param TestCase $testCase
* @param string $namespace
* @return FunctionMocker
*/
public static function start(TestCase $testCase, $namespace)
public static function start(TestCase $testCase, string $namespace): self
{
return new static($testCase, $namespace);
}

public static function tearDown()
public static function tearDown(): void
{
unset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']);
}

public function mockFunction($function)
public function mockFunction(string $function): self
{
$function = trim(strtolower($function));

Expand All @@ -56,40 +58,31 @@ public function mockFunction($function)
return $this;
}

public function getMock()
public function mockConstant(string $constant, $value): self
{
$this->constants[trim($constant)] = $value;

return $this;
}

public function getMock(): MockObject
{
$mock = $this->testCase->getMockBuilder('stdClass')
->setMethods($this->functions)
->setMockClassName('PHPUnit_Extension_FunctionMocker_' . uniqid())
->setMockClassName('PHPUnit_Extension_FunctionMocker_' . bin2hex(random_bytes(16)))
->getMock();

foreach ($this->constants as $constant => $value) {
CodeGenerator::defineConstant($this->namespace, $constant, $value);
}

foreach ($this->functions as $function) {
$fqFunction = $this->namespace . '\\' . $function;
if (in_array($fqFunction, static::$mockedFunctions, true)) {
continue;
}

if (!extension_loaded('runkit') || !ini_get('runkit.internal_override')) {
CodeGenerator::defineFunction($function, $this->namespace);
} elseif (!function_exists('__phpunit_function_mocker_' . $function)) {
runkit_function_rename($function, '__phpunit_function_mocker_' . $function);
error_log($function);
runkit_method_redefine(
$function,
function () use ($function) {
if (!isset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][$this->namespace])) {
return call_user_func_array('__phpunit_function_mocker_' . $function, func_get_args());
}

return call_user_func_array(
array($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][$this->namespace], $function),
func_get_args()
);
}
);
var_dump(strlen("foo"));
}

CodeGenerator::defineFunction($this->namespace, $function);
static::$mockedFunctions[] = $fqFunction;
}

Expand Down
69 changes: 57 additions & 12 deletions src/PHPUnit/Extension/FunctionMocker/CodeGenerator.php
Original file line number Diff line number Diff line change
@@ -1,33 +1,78 @@
<?php
namespace PHPUnit\Extension\FunctionMocker;

use function sprintf;
use function strtr;
use function var_export;

class CodeGenerator
{
public static function generateCode($functionName, $namespaceName)
public static function generateFunction(string $namespace, string $function): string
{
$template = <<<'EOS'
namespace %1$s
namespace {namespace}
{
function %2$s()
function {function}(...$args)
{
if (!isset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']['%1$s'])) {
return call_user_func_array('%2$s', func_get_args());
if (!isset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][__NAMESPACE__])) {
return \{function}(...$args);
}

return call_user_func_array(
array($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']['%1$s'], '%2$s'),
func_get_args()
);
return $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][__NAMESPACE__]->{function}(...$args);
}
}
EOS;

return sprintf($template, $namespaceName, $functionName);
return self::renderTemplate($template, ['namespace' => $namespace, 'function' => $function]);
}

public static function defineFunction($functionName, $namespaceName)
public static function defineFunction(string $namespace, string $function): void
{
$code = static::generateCode($functionName, $namespaceName);
$code = static::generateFunction($namespace, $function);
eval($code);
}

public static function generateConstant($namespace, $constant, $value)
{
$template = <<<'EOS'
namespace {namespace}
{
if (!defined(__NAMESPACE__ . '\\{constant}')) {
define(__NAMESPACE__ . '\\{constant}', {value});
} elseif ({constant} !== {value}) {
throw new \RuntimeException(sprintf('Cannot redeclare constant "{constant}" in namespace "%s". Already defined as "%s"', __NAMESPACE__, {value}));
}
}
EOS;

return self::renderTemplate(
$template,
[
'namespace' => $namespace,
'constant' => $constant,
'value' => var_export($value, true),
]
);
}

public static function defineConstant(string $namespace, string $name, string $value): void
{
eval(self::generateConstant($namespace, $name, $value));
}

private static function renderTemplate(string $template, array $parameters): string
{
return strtr(
$template,
array_combine(
array_map(
function (string $key): string {
return '{' . $key . '}';
},
array_keys($parameters)
),
array_values($parameters)
)
);
}
}
5 changes: 5 additions & 0 deletions tests/PHPUnitTests/Extension/Fixtures/TestClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,9 @@ public static function invokeGlobalFunction()
{
return strpos('ffoo', 'o');
}

public static function getGlobalConstant()
{
return CNT;
}
}
51 changes: 41 additions & 10 deletions tests/PHPUnitTests/Extension/FunctionMocker/CodeGeneratorTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -6,26 +6,57 @@

class CodeGeneratorTest extends TestCase
{
public function testRetrieveSimpleFunctionMock()
public function testGenerateFunctionMock()
{
$code = CodeGenerator::generateCode('strlen', 'Test\Namespace');
$code = CodeGenerator::generateFunction('Test\Namespace', 'strlen');

$expected = <<<'EOS'
namespace Test\Namespace
{
function strlen()
function strlen(...$args)
{
if (!isset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']['Test\Namespace'])) {
return call_user_func_array('strlen', func_get_args());
if (!isset($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][__NAMESPACE__])) {
return \strlen(...$args);
}

return call_user_func_array(
array($GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER']['Test\Namespace'], 'strlen'),
func_get_args()
);
return $GLOBALS['__PHPUNIT_EXTENSION_FUNCTIONMOCKER'][__NAMESPACE__]->strlen(...$args);
}
}
EOS;
$this->assertEquals($expected, $code);
self::assertSame($expected, $code);
}

public function testGenerateStringConstantMock()
{
$code = CodeGenerator::generateConstant('Test\Namespace', 'CONSTANT', 'value');

$expected = <<<'EOS'
namespace Test\Namespace
{
if (!defined(__NAMESPACE__ . '\\CONSTANT')) {
define(__NAMESPACE__ . '\\CONSTANT', 'value');
} elseif (CONSTANT !== 'value') {
throw new \RuntimeException(sprintf('Cannot redeclare constant "CONSTANT" in namespace "%s". Already defined as "%s"', __NAMESPACE__, 'value'));
}
}
EOS;
self::assertSame($expected, $code);
}

public function testGenerateIntegerConstantMock(): void
{
$code = CodeGenerator::generateConstant('Test\Namespace', 'CONSTANT', 123);

$expected = <<<'EOS'
namespace Test\Namespace
{
if (!defined(__NAMESPACE__ . '\\CONSTANT')) {
define(__NAMESPACE__ . '\\CONSTANT', 123);
} elseif (CONSTANT !== 123) {
throw new \RuntimeException(sprintf('Cannot redeclare constant "CONSTANT" in namespace "%s". Already defined as "%s"', __NAMESPACE__, 123));
}
}
EOS;
self::assertSame($expected, $code);
}
}
Loading