Skip to content
Merged
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 .github/workflows/static-analysis.yml
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,8 @@ jobs:
mago:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2
- uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # https://github.com/shivammathur/setup-php/releases/tag/2.37.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1
with:
php-version: 8.5
extensions: -pdo_mysql, -mysqli
Expand Down
5 changes: 3 additions & 2 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,12 @@ jobs:
dependency-version: [prefer-lowest, prefer-stable]
name: PHP ${{ matrix.php }} - ${{ matrix.dependency-version }}
steps:
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # https://github.com/actions/checkout/releases/tag/v6.0.2
- uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # https://github.com/shivammathur/setup-php/releases/tag/2.37.1
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
- uses: shivammathur/setup-php@7c071dfe9dc99bdf297fa79cb49ea005b9fcadbc # 2.37.1
with:
php-version: ${{ matrix.php }}
coverage: pcov
extensions: -pdo_mysql, -mysqli
ini-values: zend.assertions=1, assert.exception=1
- run: composer update --${{ matrix.dependency-version }} --prefer-dist --no-interaction
- run: composer test:coverage
2 changes: 1 addition & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -66,7 +66,7 @@
"prefer-stable": true,
"scripts": {
"mago": "vendor/bin/mago --colors=always",
"phpunit": "vendor/bin/phpunit --colors=always --display-phpunit-deprecations --display-deprecations",
"phpunit": "vendor/bin/phpunit --colors=always --display-all-issues",
"lint:check": "@composer mago -- lint --minimum-fail-level=warning --reporting-format=rich",
"lint:fix": "@composer mago -- lint --fix",
"format:check": "@composer mago -- format --dry-run",
Expand Down
1 change: 1 addition & 0 deletions mago.toml
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
extends = "mago.dist.toml"
version = "1.47"
php-version = "8.4.0"

[source]
Expand Down
1 change: 1 addition & 0 deletions src/Laravel/Constraint/Bus/WasHandled.php
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ protected function matches(mixed $other): bool
self::class . ' can only be evaluated for strings or command instances, got ' . gettype($other) . '.',
),
};
/** @var object|class-string $other */
$command = match ($other) {
$commandName => new ReflectionClass($other)->newInstanceWithoutConstructor(),
default => $other,
Expand Down
83 changes: 83 additions & 0 deletions src/PHPUnit/Constraint/PublicPropertiesComparator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
<?php

declare(strict_types=1);

namespace Craftzing\TestBench\PHPUnit\Constraint;

use PHPUnit\Framework\Constraint\IsEqual;
use PHPUnit\Util\Exporter;
use ReflectionClass;
use ReflectionProperty;
use SebastianBergmann\Comparator\Comparator;
use SebastianBergmann\Comparator\ComparisonFailure;

use function assert;
use function is_a;
use function is_object;

final class PublicPropertiesComparator extends Comparator
{
public function __construct(
/** @var class-string */
private readonly string $classFQN,
) {}

public function accepts(mixed $expected, mixed $actual): bool
{
return is_a($expected, $this->classFQN) && is_a($actual, $this->classFQN);
}

public function assertEquals(
mixed $expected,
mixed $actual,
float $delta = 0.0,
bool $canonicalize = false,
bool $ignoreCase = false,
): void {
assert(is_object($expected), description: 'Expected value is not an object');
assert(is_object($actual), description: 'Actual value is not an object');

if ($actual::class !== $expected::class) {
throw self::comparisonFailure(
$expected,
$actual,
$actual::class . ' is not a ' . $expected::class,
);
}

$isEqual = new IsEqual($this->comparableProperties($expected));

if ($isEqual->evaluate($this->comparableProperties($actual), returnResult: true)) {
return;
}

throw self::comparisonFailure($expected, $actual, 'Class does not have expected property values');
}

/** @return array<string, mixed> */
private function comparableProperties(object $subject): array
{
$properties = [];

foreach (new ReflectionClass($subject)->getProperties(ReflectionProperty::IS_PUBLIC) as $property) {
if ($property->isVirtual()) {
continue;
}

$properties[$property->getName()] = $property->getValue($subject);
}

return $properties;
}

private static function comparisonFailure(object $expected, object $actual, string $message): ComparisonFailure
{
return new ComparisonFailure(
$expected,
$actual,
Exporter::export($expected),
Exporter::export($actual),
$message,
);
}
}
172 changes: 172 additions & 0 deletions src/PHPUnit/Constraint/PublicPropertiesComparatorTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
<?php

declare(strict_types=1);

namespace Craftzing\TestBench\PHPUnit\Constraint;

use AssertionError;
use DateTimeImmutable;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\Attributes\Test;
use PHPUnit\Framework\TestCase;
use SebastianBergmann\Comparator\Comparator;
use SebastianBergmann\Comparator\ComparisonFailure;
use stdClass;

final class PublicPropertiesComparatorTest extends TestCase
{
#[Test]
public function itCanBeUsedAsComparator(): void
{
$instance = new PublicPropertiesComparator('Class');

$this->assertInstanceOf(Comparator::class, $instance);
}

public static function unacceptableInstances(): iterable
{
yield 'Expected instance is not an object' => [
stdClass::class,
[],
new stdClass(),
AssertionError::class,
];

yield 'Actual instance is not an object' => [
stdClass::class,
new stdClass(),
[],
AssertionError::class,
];

yield 'Expected instance does not match given class' => [
stdClass::class,
new DateTimeImmutable(),
new stdClass(),
ComparisonFailure::class,
];

yield 'Actual instance does not match given class' => [
stdClass::class,
new stdClass(),
new DateTimeImmutable(),
ComparisonFailure::class,
];
}

#[Test]
#[DataProvider('unacceptableInstances')]
public function itDoesntAcceptClassesThatDontMatchGivenClasses(
string $givenClassFQN,
mixed $expected,
mixed $actual,
string $exceptionClassFQN,
): void {
$instance = new PublicPropertiesComparator($givenClassFQN);

$result = $instance->accepts($expected, $actual);

$this->assertFalse($result);
}

#[Test]
public function itAcceptsClassesThatMatchGivenClasses(): void
{
$instance = new PublicPropertiesComparator(stdClass::class);

$result = $instance->accepts(new stdClass(), new stdClass());

$this->assertTrue($result);
}

#[Test]
#[DataProvider('unacceptableInstances')]
public function itFailsWhenComparingUnacceptableInstances(
string $givenClassFQN,
mixed $expected,
mixed $actual,
string $exceptionClassFQN,
): void {
$instance = new PublicPropertiesComparator($givenClassFQN);

$this->expectException($exceptionClassFQN);

$instance->assertEquals($expected, $actual);
}

#[Test]
public function itFailsWhenComparingInstancesWithDifferentPublicProperties(): void
{
$expected = self::subject();
$actual = $expected->public('Different');
$instance = new PublicPropertiesComparator($expected::class);

$this->expectException(ComparisonFailure::class);

$instance->assertEquals($expected, $actual);
}

public static function isEqual(): iterable
{
yield 'Same instances' => [
$expected = self::subject(),
$expected,
];

yield 'Equal public properties' => [
$expected = self::subject(),
$expected
->public($expected->public)
->protected('Different Protected')
->private('Different Private')
->virtual('Different Virtual'),
];
}

#[Test]
#[DataProvider('isEqual')]
public function itPassesWhenComparingInstancesWithEqualPublicProperties(object $expected, object $actual): void
{
$instance = new PublicPropertiesComparator($expected::class);

$this->expectNotToPerformAssertions();

$instance->assertEquals($expected, $actual);
}

private static function subject(): object
{
return new class() {
public string $virtual {
get => $this->privateVirtual;
}

public function __construct(
public string $public = 'Public',
protected string $protected = 'Protected',
private string $private = 'Private',
private string $privateVirtual = 'Virtual',
) {}

public function public(string $value): self
{
return new self($value, $this->protected, $this->private, $this->virtual);
}

public function protected(string $value): self
{
return new self($this->public, $value, $this->private, $this->virtual);
}

public function private(string $value): self
{
return new self($this->public, $this->protected, $value, $this->virtual);
}

public function virtual(string $value): self
{
return new self($this->public, $this->protected, $this->private, $value);
}
};
}
}
54 changes: 54 additions & 0 deletions src/PHPUnit/Constraint/StreamInterfaceComparator.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
<?php

declare(strict_types=1);

namespace Craftzing\TestBench\PHPUnit\Constraint;

use PHPUnit\Framework\Constraint\IsEqual;
use PHPUnit\Util\Exporter;
use Psr\Http\Message\StreamInterface;
use SebastianBergmann\Comparator\Comparator;
use SebastianBergmann\Comparator\ComparisonFailure;

use function assert;

final class StreamInterfaceComparator extends Comparator
{
public function accepts(mixed $expected, mixed $actual): bool
{
return $expected instanceof StreamInterface && $actual instanceof StreamInterface;
}

public function assertEquals(mixed $expected, mixed $actual, float $delta = 0.0, bool $canonicalize = false, bool $ignoreCase = false): void
{
assert($expected instanceof StreamInterface, 'Expected value is not an instance of ' . StreamInterface::class);
assert($actual instanceof StreamInterface, 'Actual value is not an instance of ' . StreamInterface::class);

if ($actual::class !== $expected::class) {
throw self::comparisonFailure(
$expected,
$actual,
$actual::class . ' is not a ' . $expected::class,
);
}

$isEqual = new IsEqual((string) $expected);

if ($isEqual->evaluate((string) $actual, returnResult: true)) {
return;
}

throw self::comparisonFailure($expected, $actual, 'Data streams are not equal.');
}

private static function comparisonFailure(object $expected, object $actual, string $message): ComparisonFailure
{
return new ComparisonFailure(
$expected,
$actual,
Exporter::export($expected),
Exporter::export($actual),
$message,
);
}
}
Loading