-
Notifications
You must be signed in to change notification settings - Fork 460
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
Mock readonly
classes on PHP 82
#1319
base: 1.6.x
Are you sure you want to change the base?
Conversation
Codecov Report
Additional details and impacted files@@ Coverage Diff @@
## 1.6.x #1319 +/- ##
============================================
- Coverage 77.76% 77.57% -0.20%
+ Complexity 1021 1019 -2
============================================
Files 76 76
Lines 2613 2613
============================================
- Hits 2032 2027 -5
- Misses 581 586 +5
|
|
043aaf0
to
3264adb
Compare
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
Signed-off-by: Nathanael Esayeas <nathanael.esayeas@protonmail.com>
f0f8052
to
a0a71d3
Compare
is there any update mocking |
@comes No, not yet. However, I do have a solution in mind. I believe we may need to move all of the class properties in the to get around the and manipulate the mock and any calls to the mock using static calls to I'll try make some time to test the solution this weekend. |
Update: refactored the MockInternals as previously mentioned, (without) changing any of the current functionality. Encountered issues with at least two features that are now broken.
These issues will require some debugging to resolve. For readonly:
Mock & MockInternals Class
// …
class Mock implements MockInterface
{
/**
* @var MockInternals|null
*/
private static $mockInternals;
private static function mockInternals(): MockInternals
{
if (self::$mockInternals instanceof MockInternals){
return self::$mockInternals;
}
return self::$mockInternals = MockInternals::new(__CLASS__);
}
}
// ...
<?php
declare(strict_types=1);
namespace Mockery;
use Mockery;
use Mockery\Exception\BadMethodCallException;
use Mockery\Exception\InvalidArgumentException;
use Mockery\Exception\InvalidOrderException;
use Mockery\Exception\NoMatchingExpectationException;
use ReflectionClass;
use ReflectionException;
use ReflectionMethod;
use ReflectionObject;
use const CASE_LOWER;
use function array_change_key_case;
use function array_filter;
use function array_map;
use function call_user_func;
use function call_user_func_array;
use function count;
use function get_parent_class;
use function is_callable;
use function is_string;
use function mb_stripos;
use function mb_strtolower;
use function method_exists;
use function spl_object_hash;
use function sprintf;
final class MockInternals
{
/**
* Order number of allocation.
*
* @var int
*/
public $_mockery_allocatedOrder = 0;
public $_mockery_allowMockingProtectedMethods = false;
/**
* Mock container containing this mock object.
*
* @var Container
*/
public $_mockery_container = null;
/**
* Current ordered number.
*
* @var int
*/
public $_mockery_currentOrder = 0;
/**
* If shouldIgnoreMissing is called, this value will be returned on all calls to missing methods.
*
* @var mixed
*/
public $_mockery_defaultReturnValue = null;
/**
* Flag to indicate whether we can defer method calls missing from our
* expectations.
*
* @var bool
*/
public $_mockery_deferMissing = false;
/**
* Flag to indicate we should ignore all expectations temporarily. Used
* mainly to prevent expectation matching when in the middle of a mock
* object recording session.
*
* @var bool
*/
public $_mockery_disableExpectationMatching = false;
/**
* Stores an array of all expectation directors for this mock.
*
* @var array<string, ExpectationDirector>
*/
public $_mockery_expectations = [];
/**
* Stores an initial number of expectations that can be manipulated
* while using the getter method.
*
* @var int
*/
public $_mockery_expectations_count = 0;
/**
* Ordered groups.
*
* @var array
*/
public $_mockery_groups = [];
/**
* Flag to indicate whether we can ignore method calls missing from our
* expectations.
*
* @var bool
*/
public $_mockery_ignoreMissing = false;
/**
* Flag to indicate whether we want to set the ignoreMissing flag on
* mocks generated form this calls to this one.
*
* @var bool
*/
public $_mockery_ignoreMissingRecursive = false;
public $_mockery_ignoreVerification = null;
public $_mockery_instanceMock = true;
/**
* @var array
*/
public $_mockery_mockableMethods = [];
/**
* Stores all stubbed public methods separate from any on-object public
* properties that may exist.
*
* @var array
*/
public $_mockery_mockableProperties = [];
/**
* Given name of the mock.
*
* @var string
*/
public $_mockery_name = null;
/**
* Instance of a core object on which methods are called in the event
* it has been set, and an expectation for one of the object's methods
* does not exist. This implements a simple partial mock proxy system.
*
* @var object
*/
public $_mockery_partial = null;
public $_mockery_receivedMethodCalls;
/**
* Tracks internally all the bad method call exceptions that happened during runtime.
*
* @var array
*/
public $_mockery_thrownExceptions = [];
/**
* Flag to indicate whether this mock was verified.
*
* @var bool
*/
public $_mockery_verified = false;
/**
* The mock object.
*
* @var null|Mock
*/
public $mock;
/**
* Just a local cache for this mock's target's methods.
*
* @var ReflectionMethod[]
*/
public static $_mockery_methods = [];
public function __construct(string $name)
{
$this->_mockery_name = $name;
}
public function _mockery_constructorCalled(array $args): void
{
if (! isset($this->_mockery_expectations['__construct'])) {
/* _mockery_handleMethodCall runs the other checks */
return;
}
$this->_mockery_handleMethodCall('__construct', $args);
}
public function _mockery_findExpectedMethodHandler($method)
{
if (array_key_exists($method, $this->_mockery_expectations)) {
return $this->_mockery_expectations[$method];
}
$lowerCasedMockeryExpectations = array_change_key_case($this->_mockery_expectations, CASE_LOWER);
$lowerCasedMethod = mb_strtolower($method);
return $lowerCasedMockeryExpectations[$lowerCasedMethod] ?? null;
}
public function _mockery_getReceivedMethodCalls(): ReceivedMethodCalls
{
if ($this->_mockery_receivedMethodCalls instanceof ReceivedMethodCalls) {
return $this->_mockery_receivedMethodCalls;
}
return $this->_mockery_receivedMethodCalls = new ReceivedMethodCalls();
}
public function _mockery_handleMethodCall(string $method, array $args)
{
$this->log(__METHOD__.'|'. $method);
$this->_mockery_getReceivedMethodCalls()
->push(new MethodCall($method, $args));
$parentClass = get_parent_class($this->mock);
$rm = $this->mockery_getMethod($method);
if (! $this->_mockery_allowMockingProtectedMethods && $rm && $rm->isProtected()) {
if ($rm->isAbstract()) {
return;
}
try {
$prototype = $rm->getPrototype();
if ($prototype->isAbstract()) {
return;
}
} catch (ReflectionException $re) {
// noop - there is no hasPrototype method
}
return call_user_func_array($parentClass . '::' . $method, $args);
}
$handler = $this->_mockery_findExpectedMethodHandler($method);
if ($handler !== null && ! $this->_mockery_disableExpectationMatching) {
try {
return $handler->call($args);
} catch (NoMatchingExpectationException $e) {
if (! $this->_mockery_ignoreMissing && ! $this->_mockery_deferMissing) {
throw $e;
}
}
}
if ($this->_mockery_partial !== null &&
(method_exists($this->_mockery_partial, $method) || method_exists($this->_mockery_partial, '__call'))) {
return call_user_func_array([$this->_mockery_partial, $method], $args);
}
if ($this->_mockery_deferMissing && is_callable($parentClass . '::' . $method)
&& (! $this->hasMethodOverloadingInParentClass() || ($parentClass && method_exists($parentClass, $method)))
) {
return call_user_func_array($parentClass . '::' . $method, $args);
}
if ($this->_mockery_deferMissing && $parentClass && method_exists($parentClass, '__call')) {
return call_user_func($parentClass . '::__call', $method, $args);
}
if ($method === '__toString') {
// __toString is special because we force its addition to the class API regardless of the
// original implementation. Thus, we should always return a string rather than honor
// _mockery_ignoreMissing and break the API with an error.
return sprintf('%s#%s', $this->_mockery_name, spl_object_hash($this->mock));
}
if ($this->_mockery_ignoreMissing) {
if ( is_callable($parentClass . '::' . $method)
|| ($this->_mockery_partial !== null && method_exists($this->_mockery_partial, $method))
|| Mockery::getConfiguration()->mockingNonExistentMethodsAllowed()
) {
if ($this->_mockery_defaultReturnValue instanceof Undefined) {
return call_user_func_array([$this->_mockery_defaultReturnValue, $method], $args);
}
if ($this->_mockery_defaultReturnValue === null) {
return $this->mockery_returnValueForMethod($method);
}
return $this->_mockery_defaultReturnValue;
}
}
$message = sprintf(
'Method %s::%s() does not exist on this mock object',
$this->_mockery_name,
$method
);
if ($rm !== null) {
$message = sprintf(
'Received %s::%s(), but no expectations were specified',
$this->_mockery_name,
$method
);
}
$badMethodCallException = new BadMethodCallException($message);
$this->_mockery_thrownExceptions[] = $badMethodCallException;
throw $badMethodCallException;
}
public function _mockery_handleStaticMethodCall(string $method, array $args)
{
$associatedRealObject = Mockery::fetchMock($this->_mockery_name);
try {
return $associatedRealObject->__call($method, $args);
} catch (BadMethodCallException $e) {
throw new BadMethodCallException(
'Static method ' . $associatedRealObject->mockery_getName() . '::' . $method
. '() does not exist on this mock object',
0,
$e
);
}
}
public function allows($something)
{
if (is_string($something)) {
return $this->shouldReceive($something);
}
if (empty($something)) {
return $this->shouldReceive();
}
foreach ($something as $method => $returnValue) {
$this->shouldReceive($method)
->andReturn($returnValue);
}
return $this->mock;
}
public function asUndefined()
{
$this->_mockery_ignoreMissing = true;
$this->_mockery_defaultReturnValue = new Undefined();
return $this->mock;
}
public function byDefault()
{
foreach ($this->_mockery_expectations as $director) {
$exps = $director->getExpectations();
foreach ($exps as $exp) {
$exp->byDefault();
}
}
return $this->mock;
}
public function call($method, array $args = [])
{
return $this->_mockery_handleMethodCall($method, $args);
}
public function callStatic($method, array $args)
{
return $this->mock::_mockery_handleStaticMethodCall($method, $args);
}
public function destruct(): void
{
}
public function expects($something): ExpectsHigherOrderMessage
{
if (is_string($something)) {
return $this->shouldReceive($something)
->once();
}
return new ExpectsHigherOrderMessage($this->mock);
}
public function getNonPublicMethods(): array
{
return array_map(
static function ($method) {
return $method->getName();
},
array_filter($this->mockery_getMethods(), static function ($method) {
return ! $method->isPublic();
})
);
}
public function hasMethodOverloadingInParentClass(): bool
{
// if there's __call any name would be callable
return is_callable(
get_parent_class($this->_mockery_name) . '::aFunctionNameThatNoOneWouldEverUseInRealLife12345'
);
}
public function isset(string $name): bool
{
if (
str_starts_with($name, '_mockery_')&&
mb_stripos($name, '_mockery_') === false
&& get_parent_class($this->_mockery_name)
&& method_exists(get_parent_class($this->_mockery_name), '__isset')
) {
return call_user_func(get_parent_class($this->_mockery_name) . '::__isset', $name);
}
return false;
}
public function makePartial()
{
$this->_mockery_deferMissing = true;
return $this->mock;
}
public function mockery_allocateOrder(): int
{
++$this->_mockery_allocatedOrder;
return $this->_mockery_allocatedOrder;
}
public function mockery_callSubjectMethod(string $name, array $args)
{
if (
! method_exists($this->mock, $name)
&& get_parent_class($this->mock)
&& method_exists(get_parent_class($this->mock), '__call')
) {
return call_user_func(get_parent_class($this->mock) . '::__call', $name, $args);
}
return call_user_func_array(get_parent_class($this->mock) . '::' . $name, $args);
}
public function mockery_findExpectation(string $method, array $args): ?Expectation
{
if (! isset($this->_mockery_expectations[$method])) {
return null;
}
return $this->_mockery_expectations[$method]->findExpectation($args);
}
public function mockery_getContainer()
{
if ($this->_mockery_container instanceof Container) {
return $this->_mockery_container;
}
return $this->_mockery_container = new Container();
}
public function mockery_getCurrentOrder(): int
{
return $this->_mockery_currentOrder;
}
public function mockery_getExpectationCount(): int
{
$count = $this->_mockery_expectations_count;
foreach ($this->_mockery_expectations as $director) {
$count += $director->getExpectationCount();
}
return $count;
}
public function mockery_getExpectations(): array
{
return $this->_mockery_expectations;
}
public function mockery_getExpectationsFor(string $method): ?ExpectationDirector
{
if (isset($this->_mockery_expectations[$method])) {
return $this->_mockery_expectations[$method];
}
return null;
}
public function mockery_getGroups(): array
{
return $this->_mockery_groups;
}
public function mockery_getMethod(string $name): ?ReflectionMethod
{
foreach ($this->mockery_getMethods() as $method) {
if ($method->getName() === $name) {
return $method;
}
}
return null;
}
public function mockery_getMethods(): array
{
if (self::$_mockery_methods && Mockery::getConfiguration()->reflectionCacheEnabled()) {
return self::$_mockery_methods;
}
if (isset($this->_mockery_partial)) {
$reflected = new ReflectionObject($this->_mockery_partial);
} else {
$reflected = new ReflectionClass($this->_mockery_name);
}
return self::$_mockery_methods = $reflected->getMethods();
}
public function mockery_getMockableMethods(): array
{
return $this->_mockery_mockableMethods;
}
public function mockery_getMockableProperties(): array
{
return $this->_mockery_mockableProperties;
}
public function mockery_getName(): string
{
return $this->_mockery_name;
}
public function mockery_init(object $mock, ?Container $container, ?object $partialObject, bool $instanceMock): void
{
$this->_mockery_partial = $partialObject;
$this->_mockery_container = $container ?? new Container();
$this->_mockery_instanceMock = $instanceMock;
$this->mock = $mock;
if (! Mockery::getConfiguration()->mockingNonExistentMethodsAllowed()) {
foreach ($mock->mockery_getMethods() as $method) {
if ($method->isPublic()) {
$this->_mockery_mockableMethods[] = $method->getName();
}
}
}
}
public function mockery_isAnonymous(): bool
{
$rfc = new ReflectionClass($this->mock);
// PHP 8 has Stringable interface
$interfaces = array_filter($rfc->getInterfaces(), static function ($i) {
return $i->getName() !== 'Stringable';
});
return $rfc->getParentClass() === false && count($interfaces) === 2;
}
public function mockery_isInstance(): bool
{
return $this->_mockery_instanceMock;
}
public function mockery_returnValueForMethod(string $name)
{
$rm = $this->mockery_getMethod($name);
if ($rm === null) {
return null;
}
$returnType = Reflector::getSimplestReturnType($rm);
switch ($returnType) {
case null: return null;
case 'string': return '';
case 'int': return 0;
case 'float': return 0.0;
case 'bool': return false;
case 'true': return true;
case 'false': return false;
case 'array':
case 'iterable':
return [];
case 'callable':
case '\Closure':
return static function () {
};
case '\Traversable':
case '\Generator':
$generator = static function () {
yield;
};
return $generator();
case 'void':
return null;
case 'static':
return $this;
case 'object':
$mock = Mockery::mock();
if ($this->_mockery_ignoreMissingRecursive) {
$mock->shouldIgnoreMissing($this->_mockery_defaultReturnValue, true);
}
return $mock;
default:
$mock = Mockery::mock($returnType);
if ($this->_mockery_ignoreMissingRecursive) {
$mock->shouldIgnoreMissing($this->_mockery_defaultReturnValue, true);
}
return $mock;
}
}
public function mockery_setCurrentOrder(int $order): int
{
$this->_mockery_currentOrder = $order;
return $this->_mockery_currentOrder;
}
public function mockery_setExpectationsFor(string $method, ExpectationDirector $director): ExpectationDirector
{
$this->log(__METHOD__ . '|' . $method , $director);
return $this->_mockery_expectations[$method] = $director;
}
public function mockery_setGroup($group, int $order)
{
$this->_mockery_groups[$group] = $order;
return $this->mock;
}
public function mockery_teardown(): void
{
}
public function mockery_thrownExceptions(): array
{
return $this->_mockery_thrownExceptions;
}
public function mockery_validateOrder(string $method, int $order): void
{
if ($order < $this->_mockery_currentOrder) {
$exception = new InvalidOrderException(
'Method ' . $this->_mockery_name . '::' . $method . '()'
. ' called out of order: expected order '
. $order . ', was ' . $this->_mockery_currentOrder
);
$exception->setMock($this->mock)
->setMethodName($method)
->setExpectedOrder($order)
->setActualOrder($this->_mockery_currentOrder);
throw $exception;
}
$this->mockery_setCurrentOrder($order);
}
public function mockery_verify(): void
{
if ($this->_mockery_verified) {
return;
}
if ($this->_mockery_ignoreVerification === true) {
return;
}
$this->_mockery_verified = true;
foreach ($this->_mockery_expectations as $director) {
$director->verify();
}
}
public function name(): string
{
return $this->_mockery_name;
}
public function shouldAllowMockingMethod(string $method)
{
$this->_mockery_mockableMethods[] = $method;
return $this->mock;
}
public function shouldAllowMockingProtectedMethods()
{
if (! Mockery::getConfiguration()->mockingNonExistentMethodsAllowed()) {
foreach ($this->mockery_getMethods() as $method) {
if ($method->isProtected()) {
$this->_mockery_mockableMethods[] = $method->getName();
}
}
}
$this->_mockery_allowMockingProtectedMethods = true;
return $this->mock;
}
public function shouldDeferMissing()
{
$this->makePartial();
return $this->mock;
}
public function shouldHaveBeenCalled()
{
return $this->shouldHaveReceived('__invoke');
}
public function shouldHaveReceived($method, $args = null)
{
if ($method === null) {
return new HigherOrderMessage($this->mock, 'shouldHaveReceived');
}
$expectation = new VerificationExpectation($this->mock, $method);
if ($args !== null) {
$expectation->withArgs($args);
}
$expectation->atLeast()
->once();
$director = new VerificationDirector($this->_mockery_getReceivedMethodCalls(), $expectation);
$this->_mockery_expectations_count++;
$director->verify();
return $director;
}
public function shouldIgnoreMissing($returnValue = null, bool $recursive = false)
{
$this->_mockery_ignoreMissing = true;
$this->_mockery_ignoreMissingRecursive = $recursive;
$this->_mockery_defaultReturnValue = $returnValue;
return $this->mock;
}
public function shouldNotHaveBeenCalled(?array $args)
{
return $this->shouldNotHaveReceived('__invoke', $args);
}
public function shouldNotHaveReceived($method, $args): ?HigherOrderMessage
{
if ($method === null) {
return new HigherOrderMessage($this->mock, 'shouldNotHaveReceived');
}
$expectation = new VerificationExpectation($this->mock, $method);
if ($args !== null) {
$expectation->withArgs($args);
}
$expectation->never();
$director = new VerificationDirector($this->_mockery_getReceivedMethodCalls(), $expectation);
$this->_mockery_expectations_count++;
$director->verify();
return null;
}
public function shouldNotReceive($methodNames)
{
if ($methodNames === []) {
return new HigherOrderMessage($this->mock, 'shouldNotReceive');
}
$expectation = call_user_func_array([$this->mock, 'shouldReceive'], $methodNames);
$expectation->never();
return $expectation;
}
public function shouldReceive($methodNames)
{
if ($methodNames === []) {
return new HigherOrderMessage($this->mock, 'shouldReceive');
}
foreach ($methodNames as $method) {
if ($method === '') {
throw new InvalidArgumentException('Received empty method name');
}
}
$self = $this->mock;
$allowMockingProtectedMethods = $this->_mockery_allowMockingProtectedMethods;
return Mockery::parseShouldReturnArgs(
$this->mock,
$methodNames,
static function (string $method) use ($self, $allowMockingProtectedMethods) {
$rm = $self->mockery_getMethod($method);
if ($rm) {
if ($rm->isPrivate()) {
throw new InvalidArgumentException("{$method}() cannot be mocked as it is a private method");
}
if (! $allowMockingProtectedMethods && $rm->isProtected()) {
throw new InvalidArgumentException(
"{$method}() cannot be mocked as it is a protected method and mocking protected methods is not enabled for the currently used mock object. Use shouldAllowMockingProtectedMethods() to enable mocking of protected methods."
);
}
}
$director = $self->mockery_getExpectationsFor($method);
if (! $director) {
$director = new ExpectationDirector($method, $self);
$self->mockery_setExpectationsFor($method, $director);
}
$expectation = new Expectation($self, $method);
$director->addExpectation($expectation);
return $expectation;
}
);
}
public function toString(): string
{
return $this->call('__toString', []);
}
public function wakeup(): void
{
}
public static function new(string $name): self
{
return new self($name);
}
public function construct(bool $ignoreVerification = null): void
{
$this->_mockery_constructorCalled(func_get_args());
}
} I'll need to debug and resolve the issues with the broken features to ensure that the functionality is restored without changing the current behavior. |
🦾 awesome! Is there any way I can help? |
Hello, any progress on this? I could provide assistance, if needed. |
readonly
Class SemanticsA
readonly
property:When a class is declared
readonly
, that class:#[\AllowDynamicProperties]
attributeWhen a
readonly
class is extended by a subclass, that class:Resolve #1317