Skip to content

Commit

Permalink
Closes #5804
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianbergmann committed Apr 11, 2024
1 parent e91fafb commit eb52978
Show file tree
Hide file tree
Showing 92 changed files with 436 additions and 175 deletions.
1 change: 0 additions & 1 deletion .psalm/baseline.xml
Original file line number Diff line number Diff line change
Expand Up @@ -255,7 +255,6 @@
<code><![CDATA[testDouble]]></code>
<code><![CDATA[testDouble]]></code>
<code><![CDATA[testDouble]]></code>
<code><![CDATA[testDouble]]></code>
</MissingThrowsDocblock>
<PossiblyNullArgument>
<code><![CDATA[$client->__getFunctions()]]></code>
Expand Down
1 change: 1 addition & 0 deletions ChangeLog-11.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ All notable changes of the PHPUnit 11.2 release series are documented in this fi
### Added

* [#5799](https://github.com/sebastianbergmann/phpunit/issues/5799): `#[CoversTrait]` and `#[UsesTrait]` attributes
* [#5804](https://github.com/sebastianbergmann/phpunit/pull/5804): Support doubling `readonly` classes

### Deprecated

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,22 +7,19 @@
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Framework\MockObject\Generator;

use function sprintf;
namespace PHPUnit\Framework\MockObject;

/**
* @internal This class is not covered by the backward compatibility promise for PHPUnit
*
* @codeCoverageIgnore
*/
final class ClassIsReadonlyException extends \PHPUnit\Framework\Exception implements Exception
final class CannotCloneTestDoubleForReadonlyClassException extends \PHPUnit\Framework\Exception implements Exception
{
public function __construct(string $className)
public function __construct()
{
parent::__construct(
sprintf(
'Class "%s" is declared "readonly" and cannot be doubled',
$className,
),
'Cloning test doubles for readonly classes is not supported on PHP 8.2',
);
}
}
69 changes: 48 additions & 21 deletions src/Framework/MockObject/Generator/Generator.php
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,22 @@
use PHPUnit\Framework\InvalidArgumentException;
use PHPUnit\Framework\MockObject\ConfigurableMethod;
use PHPUnit\Framework\MockObject\DoubledCloneMethod;
use PHPUnit\Framework\MockObject\ErrorCloneMethod;
use PHPUnit\Framework\MockObject\GeneratedAsMockObject;
use PHPUnit\Framework\MockObject\GeneratedAsTestStub;
use PHPUnit\Framework\MockObject\Method;
use PHPUnit\Framework\MockObject\MockObject;
use PHPUnit\Framework\MockObject\MockObjectApi;
use PHPUnit\Framework\MockObject\MockObjectInternal;
use PHPUnit\Framework\MockObject\MutableStubApi;
use PHPUnit\Framework\MockObject\ProxiedCloneMethod;
use PHPUnit\Framework\MockObject\Stub;
use PHPUnit\Framework\MockObject\StubApi;
use PHPUnit\Framework\MockObject\StubInternal;
use PHPUnit\Framework\MockObject\TestDoubleState;
use ReflectionClass;
use ReflectionMethod;
use ReflectionObject;
use SoapClient;
use SoapFault;
use Throwable;
Expand Down Expand Up @@ -99,7 +103,6 @@ final class Generator
* @throws ClassAlreadyExistsException
* @throws ClassIsEnumerationException
* @throws ClassIsFinalException
* @throws ClassIsReadonlyException
* @throws DuplicateMethodException
* @throws InvalidMethodNameException
* @throws OriginalConstructorInvocationRequiredException
Expand Down Expand Up @@ -232,7 +235,6 @@ public function testDoubleForInterfaceIntersection(array $interfaces, bool $mock
* @throws ClassAlreadyExistsException
* @throws ClassIsEnumerationException
* @throws ClassIsFinalException
* @throws ClassIsReadonlyException
* @throws DuplicateMethodException
* @throws InvalidArgumentException
* @throws InvalidMethodNameException
Expand Down Expand Up @@ -293,7 +295,6 @@ interface_exists($originalClassName, $callAutoload)) {
* @throws ClassAlreadyExistsException
* @throws ClassIsEnumerationException
* @throws ClassIsFinalException
* @throws ClassIsReadonlyException
* @throws DuplicateMethodException
* @throws InvalidArgumentException
* @throws InvalidMethodNameException
Expand Down Expand Up @@ -381,7 +382,6 @@ public function objectForTrait(string $traitName, string $traitClassName = '', b
/**
* @throws ClassIsEnumerationException
* @throws ClassIsFinalException
* @throws ClassIsReadonlyException
* @throws ReflectionException
* @throws RuntimeException
*
Expand Down Expand Up @@ -580,12 +580,20 @@ private function getObject(MockType $mockClass, string $type = '', bool $callOri
$className = $mockClass->generate();
$object = $this->instantiate($className, $callOriginalConstructor, $arguments);

if ($callOriginalMethods) {
$this->instantiateProxyTarget($proxyTarget, $object, $type, $arguments);
}
if ($object instanceof StubInternal && $mockClass instanceof MockClass) {
/**
* @psalm-suppress MissingThrowsDocblock
*
* @noinspection PhpUnhandledExceptionInspection
*/
(new ReflectionObject($object))->getProperty('__phpunit_state')->setValue(
$object,
new TestDoubleState($mockClass->configurableMethods(), $returnValueGeneration),
);

if ($object instanceof StubInternal) {
$object->__phpunit_setReturnValueGeneration($returnValueGeneration);
if ($callOriginalMethods) {
$this->instantiateProxyTarget($proxyTarget, $object, $type, $arguments);
}
}

return $object;
Expand All @@ -594,7 +602,6 @@ private function getObject(MockType $mockClass, string $type = '', bool $callOri
/**
* @throws ClassIsEnumerationException
* @throws ClassIsFinalException
* @throws ClassIsReadonlyException
* @throws ReflectionException
* @throws RuntimeException
*/
Expand All @@ -605,6 +612,7 @@ private function generateCodeForTestDoubleClass(string $type, bool $mockObject,
$doubledCloneMethod = false;
$proxiedCloneMethod = false;
$isClass = false;
$isReadonly = false;
$isInterface = false;
$class = null;
$mockMethods = new MockMethodSet;
Expand Down Expand Up @@ -646,7 +654,7 @@ private function generateCodeForTestDoubleClass(string $type, bool $mockObject,
}

if ($class->isReadOnly()) {
throw new ClassIsReadonlyException($_mockClassName['fullClassName']);
$isReadonly = true;
}

// @see https://github.com/sebastianbergmann/phpunit/issues/2995
Expand Down Expand Up @@ -754,7 +762,16 @@ private function generateCodeForTestDoubleClass(string $type, bool $mockObject,
}

/** @psalm-var trait-string[] $traits */
$traits = [StubApi::class];
$traits = [];
$isPhp82 = PHP_MAJOR_VERSION === 8 && PHP_MINOR_VERSION === 2;

if (!$isReadonly && $isPhp82) {
// @codeCoverageIgnoreStart
$traits[] = MutableStubApi::class;
// @codeCoverageIgnoreEnd
} else {
$traits[] = StubApi::class;
}

if ($mockObject) {
$traits[] = MockObjectApi::class;
Expand Down Expand Up @@ -788,12 +805,16 @@ private function generateCodeForTestDoubleClass(string $type, bool $mockObject,
$traits[] = Method::class;
}

if ($doubledCloneMethod) {
$traits[] = DoubledCloneMethod::class;
}

if ($proxiedCloneMethod) {
$traits[] = ProxiedCloneMethod::class;
if ($isPhp82 && $isReadonly) {
// @codeCoverageIgnoreStart
$traits[] = ErrorCloneMethod::class;
// @codeCoverageIgnoreEnd
} else {
if ($doubledCloneMethod) {
$traits[] = DoubledCloneMethod::class;
} elseif ($proxiedCloneMethod) {
$traits[] = ProxiedCloneMethod::class;
}
}

$useStatements = '';
Expand All @@ -816,6 +837,7 @@ private function generateCodeForTestDoubleClass(string $type, bool $mockObject,
$_mockClassName,
$isInterface,
$additionalInterfaces,
$isReadonly,
),
'use_statements' => $useStatements,
'mock_class_name' => $_mockClassName['className'],
Expand Down Expand Up @@ -862,15 +884,20 @@ private function generateClassName(string $type, string $className, string $pref
];
}

private function generateTestDoubleClassDeclaration(bool $mockObject, array $mockClassName, bool $isInterface, array $additionalInterfaces = []): string
private function generateTestDoubleClassDeclaration(bool $mockObject, array $mockClassName, bool $isInterface, array $additionalInterfaces, bool $isReadonly): string
{
if ($mockObject) {
$additionalInterfaces[] = MockObjectInternal::class;
} else {
$additionalInterfaces[] = StubInternal::class;
}

$buffer = 'class ';
if ($isReadonly) {
$buffer = 'readonly class ';
} else {
$buffer = 'class ';
}

$interfaces = implode(', ', $additionalInterfaces);

if ($isInterface) {
Expand Down Expand Up @@ -1041,7 +1068,7 @@ private function instantiateProxyTarget(?object $proxyTarget, object $object, st
}
}

$object->__phpunit_setOriginalObject($proxyTarget);
$object->__phpunit_state()->setProxyTarget($proxyTarget);
}

/**
Expand Down
17 changes: 8 additions & 9 deletions src/Framework/MockObject/Generator/MockClass.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
*/
namespace PHPUnit\Framework\MockObject\Generator;

use function call_user_func;
use function class_exists;
use PHPUnit\Framework\MockObject\ConfigurableMethod;

Expand Down Expand Up @@ -48,14 +47,6 @@ public function generate(): string
{
if (!class_exists($this->mockName, false)) {
eval($this->classCode);

call_user_func(
[
$this->mockName,
'__phpunit_initConfigurableMethods',
],
...$this->configurableMethods,
);
}

return $this->mockName;
Expand All @@ -65,4 +56,12 @@ public function classCode(): string
{
return $this->classCode;
}

/**
* @psalm-return list<ConfigurableMethod>
*/
public function configurableMethods(): array
{
return $this->configurableMethods;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,5 +33,5 @@
)
);

$__phpunit_result = call_user_func_array([$this->__phpunit_originalObject, "{method_name}"], $__phpunit_arguments);{return_result}
$__phpunit_result = call_user_func_array([$this->__phpunit_state()->proxyTarget(), "{method_name}"], $__phpunit_arguments);{return_result}
}
2 changes: 0 additions & 2 deletions src/Framework/MockObject/MockBuilder.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,6 @@
use PHPUnit\Framework\MockObject\Generator\ClassAlreadyExistsException;
use PHPUnit\Framework\MockObject\Generator\ClassIsEnumerationException;
use PHPUnit\Framework\MockObject\Generator\ClassIsFinalException;
use PHPUnit\Framework\MockObject\Generator\ClassIsReadonlyException;
use PHPUnit\Framework\MockObject\Generator\DuplicateMethodException;
use PHPUnit\Framework\MockObject\Generator\Generator;
use PHPUnit\Framework\MockObject\Generator\InvalidMethodNameException;
Expand Down Expand Up @@ -81,7 +80,6 @@ public function __construct(TestCase $testCase, string $type)
* @throws ClassAlreadyExistsException
* @throws ClassIsEnumerationException
* @throws ClassIsFinalException
* @throws ClassIsReadonlyException
* @throws DuplicateMethodException
* @throws InvalidArgumentException
* @throws InvalidMethodNameException
Expand Down
6 changes: 5 additions & 1 deletion src/Framework/MockObject/Runtime/Api/DoubledCloneMethod.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,10 @@ trait DoubledCloneMethod
{
public function __clone(): void
{
$this->__phpunit_invocationMocker = clone $this->__phpunit_getInvocationHandler();
$this->__phpunit_state = clone $this->__phpunit_state;

$this->__phpunit_state()->cloneInvocationHandler();
}

abstract public function __phpunit_state(): TestDoubleState;
}
23 changes: 23 additions & 0 deletions src/Framework/MockObject/Runtime/Api/ErrorCloneMethod.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
<?php declare(strict_types=1);
/*
* This file is part of PHPUnit.
*
* (c) Sebastian Bergmann <sebastian@phpunit.de>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/
namespace PHPUnit\Framework\MockObject;

/**
* @internal This trait is not covered by the backward compatibility promise for PHPUnit
*
* @codeCoverageIgnore
*/
trait ErrorCloneMethod
{
public function __clone(): void
{
throw new CannotCloneTestDoubleForReadonlyClassException;
}
}
2 changes: 2 additions & 0 deletions src/Framework/MockObject/Runtime/Api/Method.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
*/
trait Method
{
abstract public function __phpunit_getInvocationHandler(): InvocationHandler;

public function method(): InvocationMocker
{
$expects = $this->__phpunit_getInvocationHandler()->expects(new AnyInvokedCount);
Expand Down
17 changes: 6 additions & 11 deletions src/Framework/MockObject/Runtime/Api/MockObjectApi.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,12 @@
*/
trait MockObjectApi
{
private static array $__phpunit_deprecation_emitted_for_test = [];
private object $__phpunit_originalObject;

/** @noinspection MagicMethodsValidityInspection */
public function __phpunit_hasMatchers(): bool
{
return $this->__phpunit_getInvocationHandler()->hasMatchers();
}

/** @noinspection MagicMethodsValidityInspection */
public function __phpunit_setOriginalObject(object $originalObject): void
{
$this->__phpunit_originalObject = $originalObject;
}

/** @noinspection MagicMethodsValidityInspection */
public function __phpunit_verify(bool $unsetInvocationMocker = true): void
{
Expand All @@ -46,6 +37,8 @@ public function __phpunit_verify(bool $unsetInvocationMocker = true): void
}
}

abstract public function __phpunit_state(): TestDoubleState;

abstract public function __phpunit_getInvocationHandler(): InvocationHandler;

abstract public function __phpunit_unsetInvocationMocker(): void;
Expand All @@ -60,16 +53,18 @@ public function expects(InvocationOrder $matcher): InvocationMockerBuilder
try {
$test = TestMethodBuilder::fromCallStack();

if (!isset(self::$__phpunit_deprecation_emitted_for_test[$test->id()])) {
if (!$this->__phpunit_state()->wasDeprecationAlreadyEmittedFor($test->id())) {
EventFacade::emitter()->testTriggeredPhpunitDeprecation(
$test,
$message,
);

self::$__phpunit_deprecation_emitted_for_test[$test->id()] = true;
$this->__phpunit_state()->deprecationWasEmittedFor($test->id());
}
// @codeCoverageIgnoreStart
} catch (NoTestCaseObjectOnCallStackException) {
EventFacade::emitter()->testRunnerTriggeredDeprecation($message);
// @codeCoverageIgnoreEnd
}
}

Expand Down
Loading

0 comments on commit eb52978

Please sign in to comment.