Skip to content
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

Support doubling readonly classes #5804

Closed
wants to merge 15 commits into from
Closed
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
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
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 @@
}
}

abstract public function __phpunit_state(): TestDoubleState;

abstract public function __phpunit_getInvocationHandler(): InvocationHandler;

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

if (!isset(self::$__phpunit_deprecation_emitted_for_test[$test->id()])) {
if (!$this->__phpunit_state()->wasDeprecationAlreadyEmittedFor($test->id())) {

Check warning on line 56 in src/Framework/MockObject/Runtime/Api/MockObjectApi.php

View workflow job for this annotation

GitHub Actions / Mutation Testing

Escaped Mutant for Mutator "LogicalNot": --- Original +++ New @@ @@ $message = 'Expectations configured on test doubles that are created as test stubs are no longer verified since PHPUnit 10. Test doubles that are created as test stubs will no longer have the expects() method in PHPUnit 12. Update your test code to use createMock() instead of createStub(), for example.'; try { $test = TestMethodBuilder::fromCallStack(); - if (!$this->__phpunit_state()->wasDeprecationAlreadyEmittedFor($test->id())) { + if ($this->__phpunit_state()->wasDeprecationAlreadyEmittedFor($test->id())) { EventFacade::emitter()->testTriggeredPhpunitDeprecation($test, $message); $this->__phpunit_state()->deprecationWasEmittedFor($test->id()); }
EventFacade::emitter()->testTriggeredPhpunitDeprecation(
$test,
$message,
);

self::$__phpunit_deprecation_emitted_for_test[$test->id()] = true;
$this->__phpunit_state()->deprecationWasEmittedFor($test->id());

Check warning on line 62 in src/Framework/MockObject/Runtime/Api/MockObjectApi.php

View workflow job for this annotation

GitHub Actions / Mutation Testing

Escaped Mutant for Mutator "MethodCallRemoval": --- Original +++ New @@ @@ $test = TestMethodBuilder::fromCallStack(); if (!$this->__phpunit_state()->wasDeprecationAlreadyEmittedFor($test->id())) { EventFacade::emitter()->testTriggeredPhpunitDeprecation($test, $message); - $this->__phpunit_state()->deprecationWasEmittedFor($test->id()); + } // @codeCoverageIgnoreStart } catch (NoTestCaseObjectOnCallStackException) {
}
// @codeCoverageIgnoreStart
} catch (NoTestCaseObjectOnCallStackException) {
EventFacade::emitter()->testRunnerTriggeredDeprecation($message);
// @codeCoverageIgnoreEnd
}
}

Expand Down
Loading