Skip to content

Commit

Permalink
Closes #4467
Browse files Browse the repository at this point in the history
  • Loading branch information
sebastianbergmann committed Sep 24, 2020
1 parent 5c1a227 commit 1dba8c3
Show file tree
Hide file tree
Showing 18 changed files with 711 additions and 3 deletions.
4 changes: 3 additions & 1 deletion .php_cs.dist
Expand Up @@ -18,7 +18,9 @@ $finder = PhpCsFixer\Finder::create()
->in(__DIR__ . '/tests/_files')
->notName('*.phpt')
->notName('ClassWithAllPossibleReturnTypes.php')
->notName('ClassWithUnionReturnTypes.php');
->notName('ClassWithUnionReturnTypes.php')
->notName('ValueObjectWithEqualsMethodWithUnionReturnType.php')
->notName('ValueObjectWithEqualsMethodThatHasUnionParameterType.php');

return PhpCsFixer\Config::create()
->setFinder($finder)
Expand Down
10 changes: 8 additions & 2 deletions .psalm/baseline.xml
Expand Up @@ -117,7 +117,7 @@
<code>$value</code>
<code>$value</code>
</MissingParamType>
<MissingThrowsDocblock occurrences="36">
<MissingThrowsDocblock occurrences="37">
<code>loadFile</code>
<code>loadFile</code>
<code>loadFile</code>
Expand Down Expand Up @@ -156,7 +156,8 @@
<code>$expected</code>
<code>$expected</code>
</PossiblyInvalidArgument>
<PossiblyUnusedMethod occurrences="1">
<PossiblyUnusedMethod occurrences="2">
<code>assertObjectEquals</code>
<code>getCount</code>
</PossiblyUnusedMethod>
<RedundantCondition occurrences="1">
Expand Down Expand Up @@ -679,6 +680,11 @@
<file src="src/Framework/Constraint/Object/ClassHasStaticAttribute.php">
<MissingThrowsDocblock occurrences="1"/>
</file>
<file src="src/Framework/Constraint/Object/ObjectEquals.php">
<PropertyNotSetInConstructor occurrences="1">
<code>$failureReason</code>
</PropertyNotSetInConstructor>
</file>
<file src="src/Framework/Constraint/Operator/BinaryOperator.php">
<PossiblyUnusedMethod occurrences="1">
<code>fromConstraints</code>
Expand Down
1 change: 1 addition & 0 deletions ChangeLog-9.4.md
Expand Up @@ -7,6 +7,7 @@ All notable changes of the PHPUnit 9.4 release series are documented in this fil
### Added

* [#4464](https://github.com/sebastianbergmann/phpunit/issues/4464): Filter based on covered (`@covers`) / used (`@uses`) units of code
* [#4467](https://github.com/sebastianbergmann/phpunit/issues/4467): Convenient custom comparison of objects

### Changed

Expand Down
18 changes: 18 additions & 0 deletions src/Framework/Assert.php
Expand Up @@ -70,6 +70,7 @@
use PHPUnit\Framework\Constraint\LogicalNot;
use PHPUnit\Framework\Constraint\LogicalOr;
use PHPUnit\Framework\Constraint\LogicalXor;
use PHPUnit\Framework\Constraint\ObjectEquals;
use PHPUnit\Framework\Constraint\ObjectHasAttribute;
use PHPUnit\Framework\Constraint\RegularExpression;
use PHPUnit\Framework\Constraint\SameSize;
Expand Down Expand Up @@ -2715,6 +2716,11 @@ public static function countOf(int $count): Count
return new Count($count);
}

public static function objectEquals(object $object, string $method = 'equals'): ObjectEquals
{
return new ObjectEquals($object, $method);
}

/**
* Fails a test with the given message.
*
Expand Down Expand Up @@ -2777,6 +2783,18 @@ public static function resetCount(): void
self::$count = 0;
}

/**
* @throws ExpectationFailedException
*/
public function assertObjectEquals(object $expected, object $actual, string $method = 'equals', string $message = ''): void
{
static::assertThat(
$actual,
static::objectEquals($expected, $method),
$message
);
}

private static function detectLocationHint(string $message): ?array
{
$hint = null;
Expand Down
205 changes: 205 additions & 0 deletions src/Framework/Constraint/Object/ObjectEquals.php
@@ -0,0 +1,205 @@
<?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\Constraint;

use function get_class;
use function is_object;
use ReflectionNamedType;
use ReflectionObject;

/**
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
*/
final class ObjectEquals extends Constraint
{
private const ACTUAL_IS_NOT_AN_OBJECT = 1;

private const ACTUAL_DOES_NOT_HAVE_METHOD = 2;

private const METHOD_DOES_NOT_HAVE_BOOL_RETURN_TYPE = 3;

private const METHOD_DOES_NOT_ACCEPT_EXACTLY_ONE_ARGUMENT = 4;

private const PARAMETER_DOES_NOT_HAVE_DECLARED_TYPE = 5;

private const EXPECTED_NOT_COMPATIBLE_WITH_PARAMETER_TYPE = 6;

private const OBJECTS_ARE_NOT_EQUAL_ACCORDING_TO_METHOD = 7;

/**
* @var object
*/
private $expected;

/**
* @var string
*/
private $method;

/**
* @var int
*/
private $failureReason;

public function __construct(object $object, string $method = 'equals')
{
$this->expected = $object;
$this->method = $method;
}

public function toString(): string
{
return 'two objects are equal';
}

protected function matches($other): bool
{
if (!is_object($other)) {
$this->failureReason = self::ACTUAL_IS_NOT_AN_OBJECT;

return false;
}

$object = new ReflectionObject($other);

if (!$object->hasMethod($this->method)) {
$this->failureReason = self::ACTUAL_DOES_NOT_HAVE_METHOD;

return false;
}

/** @noinspection PhpUnhandledExceptionInspection */
$method = $object->getMethod($this->method);

if (!$method->hasReturnType()) {
$this->failureReason = self::METHOD_DOES_NOT_HAVE_BOOL_RETURN_TYPE;

return false;
}

$returnType = $method->getReturnType();

if (!$returnType instanceof ReflectionNamedType) {
$this->failureReason = self::METHOD_DOES_NOT_HAVE_BOOL_RETURN_TYPE;

return false;
}

if ($returnType->allowsNull()) {
$this->failureReason = self::METHOD_DOES_NOT_HAVE_BOOL_RETURN_TYPE;

return false;
}

if ($returnType->getName() !== 'bool') {
$this->failureReason = self::METHOD_DOES_NOT_HAVE_BOOL_RETURN_TYPE;

return false;
}

if ($method->getNumberOfParameters() !== 1 || $method->getNumberOfRequiredParameters() !== 1) {
$this->failureReason = self::METHOD_DOES_NOT_ACCEPT_EXACTLY_ONE_ARGUMENT;

return false;
}

$parameter = $method->getParameters()[0];

if (!$parameter->hasType()) {
$this->failureReason = self::PARAMETER_DOES_NOT_HAVE_DECLARED_TYPE;

return false;
}

$type = $parameter->getType();

if (!$type instanceof ReflectionNamedType) {
$this->failureReason = self::PARAMETER_DOES_NOT_HAVE_DECLARED_TYPE;

return false;
}

$typeName = $type->getName();

if ($typeName === 'self') {
$typeName = get_class($other);
}

if (!$this->expected instanceof $typeName) {
$this->failureReason = self::EXPECTED_NOT_COMPATIBLE_WITH_PARAMETER_TYPE;

return false;
}

if ($other->{$this->method}($this->expected)) {
return true;
}

$this->failureReason = self::OBJECTS_ARE_NOT_EQUAL_ACCORDING_TO_METHOD;

return false;
}

protected function failureDescription($other): string
{
return $this->toString();
}

protected function additionalFailureDescription($other): string
{
switch ($this->failureReason) {
case self::ACTUAL_IS_NOT_AN_OBJECT:
return 'Actual value is not an object.';

case self::ACTUAL_DOES_NOT_HAVE_METHOD:
return sprintf(
'%s::%s() does not exist.',
get_class($other),
$this->method
);

case self::METHOD_DOES_NOT_HAVE_BOOL_RETURN_TYPE:
return sprintf(
'%s::%s() does not declare a bool return type.',
get_class($other),
$this->method
);

case self::METHOD_DOES_NOT_ACCEPT_EXACTLY_ONE_ARGUMENT:
return sprintf(
'%s::%s() does not accept exactly one argument.',
get_class($other),
$this->method
);

case self::PARAMETER_DOES_NOT_HAVE_DECLARED_TYPE:
return sprintf(
'Parameter of %s::%s() does not have a declared type.',
get_class($other),
$this->method
);

case self::EXPECTED_NOT_COMPATIBLE_WITH_PARAMETER_TYPE:
return sprintf(
'%s is not accepted an accepted argument type for %s::%s().',
get_class($this->expected),
get_class($other),
$this->method
);

default:
return sprintf(
'The objects are not equal according to %s::%s().',
get_class($other),
$this->method
);
}
}
}
30 changes: 30 additions & 0 deletions tests/_files/ObjectEquals/ValueObject.php
@@ -0,0 +1,30 @@
<?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\TestFixture\ObjectEquals;

final class ValueObject
{
private $value;

public function __construct(int $value)
{
$this->value = $value;
}

public function equals(self $other): bool
{
return $this->asInt() === $other->asInt();
}

public function asInt(): int
{
return $this->value;
}
}
@@ -0,0 +1,30 @@
<?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\TestFixture\ObjectEquals;

final class ValueObjectWithEqualsMethodThatAcceptsTooManyArguments
{
private $value;

public function __construct(int $value)
{
$this->value = $value;
}

public function equals(self $other, self $another): bool
{
return false;
}

public function asInt(): int
{
return $this->value;
}
}
@@ -0,0 +1,30 @@
<?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\TestFixture\ObjectEquals;

final class ValueObjectWithEqualsMethodThatDoesNotAcceptArguments
{
private $value;

public function __construct(int $value)
{
$this->value = $value;
}

public function equals(): bool
{
return false;
}

public function asInt(): int
{
return $this->value;
}
}

0 comments on commit 1dba8c3

Please sign in to comment.