Compare commits

...

41 Commits

Author SHA1 Message Date
f75063c420 release: 2.32.2 2024-01-23 18:12:07 +00:00
1f8e6e4e9f fix: helper access 2024-01-23 17:40:37 +00:00
bb593846e5 release: 2.32.1 2024-01-23 17:04:48 +00:00
ac5d6c1f67 chore: fixes constrains no workflow 2024-01-20 13:48:00 +00:00
5aa3b91d56 chore: fixes windows builds 2024-01-20 13:36:31 +00:00
9a01504b76 chore: fixes workflow 2024-01-20 13:32:21 +00:00
0ab636e436 chore: fixes workflow 2024-01-20 13:28:43 +00:00
b9d2be87a2 fix: missing things on junit 2024-01-20 13:21:57 +00:00
fef02594db release: 2.32.0 2024-01-20 11:44:11 +00:00
e135e2671f style 2024-01-20 11:44:11 +00:00
6d74965727 chore: bump dependencies 2024-01-20 11:44:11 +00:00
146e141b2a Merge pull request #887 from nuernbergerA/fix-junit-output
[2.x] Junit support
2024-01-20 11:43:20 +00:00
6fed7545c0 Merge pull request #990 from rudashi/patch-1
[2.x] Fix typo in `toHaveProperties` PHPDoc block
2024-01-13 01:44:16 +00:00
5332858782 chore: fixes snapshots 2024-01-11 15:46:50 +00:00
3457841a9b release: v2.31.0 2024-01-11 15:33:20 +00:00
5258e569c1 feat: adds skipOnPHP 2024-01-11 15:33:12 +00:00
abb416c2ff chore: bumps dependencies 2024-01-11 15:32:44 +00:00
dc1e4f040d docs: adds sponsor 2024-01-04 18:26:20 +00:00
5e1e701ce5 Merge pull request #1051 from krencl/fix-cache-directory-config-override
Fix cache directory config override
2024-01-02 14:33:48 +00:00
f004591c5a fix: checking existing argument with equal sign 2024-01-02 15:03:46 +01:00
86a96dd157 fix: overriding cli argument --cache-directory 2024-01-02 15:01:13 +01:00
97dc32f9d2 release: v2.30.0 2023-12-28 10:36:40 +00:00
a3ab065343 chore: coding style 2023-12-28 10:36:30 +00:00
c390721ac3 chore: update snapshots 2023-12-28 10:34:22 +00:00
f83d758d4b feat: adds fails 2023-12-28 10:31:39 +00:00
e00aba539a release: v2.29.1 2023-12-27 15:27:07 +00:00
7799500d06 release: v2.29.0 2023-12-27 11:12:01 +00:00
c099991cd9 Merge pull request #1044 from nhrrs/fix-typo
Fix typo in `toBeClass`
2023-12-23 02:03:57 +00:00
e27d2e7394 Fix typo in toBeClass 2023-12-23 00:36:41 +00:00
14fb992ef2 unify converter 2023-12-19 06:29:28 +01:00
4550a344d3 overwrite phpunit junit logging with noop 2023-12-19 06:29:28 +01:00
8efd25ef65 remove debug output 2023-12-19 06:29:28 +01:00
117694f210 cleanup 2023-12-19 06:29:28 +01:00
e5dc6f0ae2 junit support 2023-12-19 06:29:28 +01:00
8f738f5d49 Revert "Merge pull request #919 from WendellAdriel/feature/coverage-errors-only-flag-2"
This reverts commit 1e2ca40c5b, reversing
changes made to 4522cb5dcb.
2023-12-17 22:03:15 +00:00
1e2ca40c5b Merge pull request #919 from WendellAdriel/feature/coverage-errors-only-flag-2
[2.x] Print only files below the min coverage
2023-12-17 21:56:14 +00:00
4522cb5dcb Merge pull request #1014 from mjsafarali/chore/docker-file-optimization
[2.x] Dockerfile Optimization
2023-12-17 21:39:38 +00:00
8be46b57a0 Update toHaveProperties() $names param 2023-11-24 09:16:13 +01:00
4f35dbc607 chore: optimized version of the Dockerfile 2023-11-18 14:57:03 +03:30
2cbecd10e6 Fix typo in toHaveProperties() PHPDoc block 2023-10-23 11:23:53 +02:00
8ea7b2b802 Add errors-only flag 2023-08-18 10:13:28 +01:00
42 changed files with 878 additions and 54 deletions

View File

@ -15,12 +15,12 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
symfony: ['6.4.0', '7.0.1']
symfony: ['6.4', '7.0']
php: ['8.1', '8.2', '8.3']
dependency_version: [prefer-lowest, prefer-stable]
exclude:
- php: '8.1'
symfony: '7.0.1'
symfony: '7.0'
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
@ -41,7 +41,7 @@ jobs:
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install PHP dependencies
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}"
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:~${{ matrix.symfony }}"
- name: Unit Tests
run: composer test:unit

View File

@ -27,11 +27,12 @@ We cannot thank our sponsors enough for their incredible support in funding Pest
### Premium Sponsors
- [Akaunting](https://akaunting.com)
- [Codecourse](https://codecourse.com)
- [Laracasts](https://laracasts.com)
- [Laradir](https://laradir.com)
- [Localazy](https://localazy.com)
- [Zapiet](https://www.zapiet.com)
- [Akaunting](https://akaunting.com/?ref=pestphp)
- [Codecourse](https://codecourse.com/?ref=pestphp)
- [Laracasts](https://laracasts.com/?ref=pestphp)
- [Laradir](https://laradir.com/?ref=pestphp)
- [Localazy](https://localazy.com/?ref=pestphp)
- [Stormlikes](https://www.stormlikes.net/?ref=pestphp)
- [Zapiet](https://www.zapiet.com/?ref=pestphp)
Pest is an open-sourced software licensed under the **[MIT license](https://opensource.org/licenses/MIT)**.

View File

@ -19,14 +19,14 @@
"require": {
"php": "^8.1.0",
"brianium/paratest": "^7.3.1",
"nunomaduro/collision": "^7.10.0|^8.0.0",
"nunomaduro/collision": "^7.10.0|^8.1.0",
"nunomaduro/termwind": "^1.15.1|^2.0.0",
"pestphp/pest-plugin": "^2.1.1",
"pestphp/pest-plugin-arch": "^2.5.0",
"phpunit/phpunit": "^10.5.3"
"pestphp/pest-plugin-arch": "^2.6.1",
"phpunit/phpunit": "^10.5.9"
},
"conflict": {
"phpunit/phpunit": ">10.5.3",
"phpunit/phpunit": ">10.5.9",
"sebastian/exporter": "<5.1.0",
"webmozart/assert": "<1.11.0"
},
@ -52,8 +52,8 @@
},
"require-dev": {
"pestphp/pest-dev-tools": "^2.16.0",
"pestphp/pest-plugin-type-coverage": "^2.5.0",
"symfony/process": "^6.4.0|^7.0.1"
"pestphp/pest-plugin-type-coverage": "^2.8.0",
"symfony/process": "^6.4.0|^7.0.2"
},
"minimum-stability": "dev",
"prefer-stable": true,
@ -107,7 +107,8 @@
"Pest\\Plugins\\Snapshot",
"Pest\\Plugins\\Verbose",
"Pest\\Plugins\\Version",
"Pest\\Plugins\\Parallel"
"Pest\\Plugins\\Parallel",
"Pest\\Plugins\\JUnit"
]
},
"phpstan": {

View File

@ -1,21 +1,16 @@
ARG PHP=8.1
FROM php:${PHP}-cli-alpine
RUN apk update \
&& apk add zip libzip-dev icu-dev git
RUN apk update && apk add \
zip libzip-dev icu-dev git \
RUN docker-php-ext-configure zip
RUN docker-php-ext-install zip
RUN docker-php-ext-enable zip
RUN docker-php-ext-configure zip intl
RUN docker-php-ext-install zip intl
RUN docker-php-ext-enable zip intl
RUN docker-php-ext-configure intl
RUN docker-php-ext-install intl
RUN docker-php-ext-enable intl
RUN apk add --no-cache $PHPIZE_DEPS linux-headers
RUN apk add --no-cache linux-headers
RUN pecl install xdebug
RUN docker-php-ext-enable xdebug
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
WORKDIR /var/www/html

View File

@ -0,0 +1,16 @@
<?php
declare(strict_types=1);
namespace PHPUnit\Logging\JUnit;
use PHPUnit\Event\Facade;
use PHPUnit\TextUI\Output\Printer;
final class JunitXmlLogger
{
public function __construct(Printer $printer, Facade $facade)
{
/** @see \Pest\Logging\JUnit\JUnitLogger */
}
}

View File

@ -25,6 +25,7 @@ final class BootOverrides implements Bootstrapper
'TextUI/Output/Default/ProgressPrinter/TestSkippedSubscriber.php',
'TextUI/TestSuiteFilterProcessor.php',
'Event/Value/ThrowableBuilder.php',
'Logging/JUnit/JunitXmlLogger.php',
];
/**

View File

@ -25,6 +25,7 @@ final class BootSubscribers implements Bootstrapper
Subscribers\EnsureIgnorableTestCasesAreIgnored::class,
Subscribers\EnsureKernelDumpIsFlushed::class,
Subscribers\EnsureTeamCityEnabled::class,
Subscribers\EnsureJunitEnabled::class,
];
/**

View File

@ -535,7 +535,7 @@ final class Expectation
}
/**
* Asserts that the given expectation targets is an class.
* Asserts that the given expectation target is a class.
*/
public function toBeClass(): ArchExpectation
{

View File

@ -2,7 +2,7 @@
declare(strict_types=1);
namespace Pest\Logging\TeamCity;
namespace Pest\Logging;
use NunoMaduro\Collision\Adapters\Phpunit\State;
use Pest\Exceptions\ShouldNotHappen;
@ -150,6 +150,14 @@ final class Converter
return Str::after($name, self::PREFIX);
}
/**
* Gets the trimmed test class name.
*/
public function getTrimmedTestClassName(TestMethod $test): string
{
return Str::after($test->className(), self::PREFIX);
}
/**
* Gets the test suite location.
*/

View File

@ -0,0 +1,384 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\JUnit;
use DOMDocument;
use DOMElement;
use Pest\Logging\Converter;
use Pest\Logging\JUnit\Subscriber\TestErroredSubscriber;
use Pest\Logging\JUnit\Subscriber\TestFailedSubscriber;
use Pest\Logging\JUnit\Subscriber\TestFinishedSubscriber;
use Pest\Logging\JUnit\Subscriber\TestMarkedIncompleteSubscriber;
use Pest\Logging\JUnit\Subscriber\TestPreparedSubscriber;
use Pest\Logging\JUnit\Subscriber\TestRunnerExecutionFinishedSubscriber;
use Pest\Logging\JUnit\Subscriber\TestSkippedSubscriber;
use Pest\Logging\JUnit\Subscriber\TestSuiteFinishedSubscriber;
use Pest\Logging\JUnit\Subscriber\TestSuiteStartedSubscriber;
use PHPUnit\Event\EventFacadeIsSealedException;
use PHPUnit\Event\Facade;
use PHPUnit\Event\InvalidArgumentException;
use PHPUnit\Event\Telemetry\HRTime;
use PHPUnit\Event\Telemetry\Info;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\TestData\NoDataSetFromDataProviderException;
use PHPUnit\Event\TestSuite\Started;
use PHPUnit\Event\UnknownSubscriberTypeException;
use PHPUnit\TextUI\Output\Printer;
use PHPUnit\Util\Xml;
/**
* @internal
*/
final class JUnitLogger
{
private DOMDocument $document;
private DOMElement $root;
/**
* @var DOMElement[]
*/
private array $testSuites = [];
/**
* @psalm-var array<int,int>
*/
private array $testSuiteTests = [0];
/**
* @psalm-var array<int,int>
*/
private array $testSuiteAssertions = [0];
/**
* @psalm-var array<int,int>
*/
private array $testSuiteErrors = [0];
/**
* @psalm-var array<int,int>
*/
private array $testSuiteFailures = [0];
/**
* @psalm-var array<int,int>
*/
private array $testSuiteSkipped = [0];
/**
* @psalm-var array<int,int|float>
*/
private array $testSuiteTimes = [0];
private int $testSuiteLevel = 0;
private ?DOMElement $currentTestCase = null;
private ?HRTime $time = null;
private bool $prepared = false;
/**
* @throws EventFacadeIsSealedException
* @throws UnknownSubscriberTypeException
*/
public function __construct(
private readonly Printer $printer,
private readonly Converter $converter,
) {
$this->registerSubscribers();
$this->createDocument();
}
public function flush(): void
{
$this->printer->print((string) $this->document->saveXML());
$this->printer->flush();
}
public function testSuiteStarted(Started $event): void
{
$testSuite = $this->document->createElement('testsuite');
$testSuite->setAttribute('name', $this->converter->getTestSuiteName($event->testSuite()));
if ($event->testSuite()->isForTestClass()) {
$testSuite->setAttribute('file', $this->converter->getTestSuiteLocation($event->testSuite()) ?? '');
}
if ($this->testSuiteLevel > 0) {
$this->testSuites[$this->testSuiteLevel]->appendChild($testSuite);
} else {
$this->root->appendChild($testSuite);
}
$this->testSuiteLevel++;
$this->testSuites[$this->testSuiteLevel] = $testSuite;
$this->testSuiteTests[$this->testSuiteLevel] = 0;
$this->testSuiteAssertions[$this->testSuiteLevel] = 0;
$this->testSuiteErrors[$this->testSuiteLevel] = 0;
$this->testSuiteFailures[$this->testSuiteLevel] = 0;
$this->testSuiteSkipped[$this->testSuiteLevel] = 0;
$this->testSuiteTimes[$this->testSuiteLevel] = 0;
}
public function testSuiteFinished(): void
{
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'tests',
(string) $this->testSuiteTests[$this->testSuiteLevel],
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'assertions',
(string) $this->testSuiteAssertions[$this->testSuiteLevel],
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'errors',
(string) $this->testSuiteErrors[$this->testSuiteLevel],
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'failures',
(string) $this->testSuiteFailures[$this->testSuiteLevel],
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'skipped',
(string) $this->testSuiteSkipped[$this->testSuiteLevel],
);
$this->testSuites[$this->testSuiteLevel]->setAttribute(
'time',
sprintf('%F', $this->testSuiteTimes[$this->testSuiteLevel]),
);
if ($this->testSuiteLevel > 1) {
$this->testSuiteTests[$this->testSuiteLevel - 1] += $this->testSuiteTests[$this->testSuiteLevel];
$this->testSuiteAssertions[$this->testSuiteLevel - 1] += $this->testSuiteAssertions[$this->testSuiteLevel];
$this->testSuiteErrors[$this->testSuiteLevel - 1] += $this->testSuiteErrors[$this->testSuiteLevel];
$this->testSuiteFailures[$this->testSuiteLevel - 1] += $this->testSuiteFailures[$this->testSuiteLevel];
$this->testSuiteSkipped[$this->testSuiteLevel - 1] += $this->testSuiteSkipped[$this->testSuiteLevel];
$this->testSuiteTimes[$this->testSuiteLevel - 1] += $this->testSuiteTimes[$this->testSuiteLevel];
}
$this->testSuiteLevel--;
}
/**
* @throws InvalidArgumentException
* @throws NoDataSetFromDataProviderException
*/
public function testPrepared(Prepared $event): void
{
$this->createTestCase($event);
$this->prepared = true;
}
/**
* @throws InvalidArgumentException
*/
public function testFinished(Finished $event): void
{
$this->handleFinish($event->telemetryInfo(), $event->numberOfAssertionsPerformed());
}
/**
* @throws InvalidArgumentException
* @throws NoDataSetFromDataProviderException
*/
public function testMarkedIncomplete(MarkedIncomplete $event): void
{
$this->handleIncompleteOrSkipped($event);
}
/**
* @throws InvalidArgumentException
* @throws NoDataSetFromDataProviderException
*/
public function testSkipped(Skipped $event): void
{
$this->handleIncompleteOrSkipped($event);
}
/**
* @throws InvalidArgumentException
* @throws NoDataSetFromDataProviderException
*/
public function testErrored(Errored $event): void
{
$this->handleFault($event, 'error');
$this->testSuiteErrors[$this->testSuiteLevel]++;
}
/**
* @throws InvalidArgumentException
* @throws NoDataSetFromDataProviderException
*/
public function testFailed(Failed $event): void
{
$this->handleFault($event, 'failure');
$this->testSuiteFailures[$this->testSuiteLevel]++;
}
/**
* @throws InvalidArgumentException
*/
private function handleFinish(Info $telemetryInfo, int $numberOfAssertionsPerformed): void
{
assert($this->currentTestCase instanceof \DOMElement);
assert($this->time instanceof \PHPUnit\Event\Telemetry\HRTime);
$time = $telemetryInfo->time()->duration($this->time)->asFloat();
$this->testSuiteAssertions[$this->testSuiteLevel] += $numberOfAssertionsPerformed;
$this->currentTestCase->setAttribute(
'assertions',
(string) $numberOfAssertionsPerformed,
);
$this->currentTestCase->setAttribute(
'time',
sprintf('%F', $time),
);
$this->testSuites[$this->testSuiteLevel]->appendChild(
$this->currentTestCase,
);
$this->testSuiteTests[$this->testSuiteLevel]++;
$this->testSuiteTimes[$this->testSuiteLevel] += $time;
$this->currentTestCase = null;
$this->time = null;
$this->prepared = false;
}
/**
* @throws EventFacadeIsSealedException
* @throws UnknownSubscriberTypeException
*/
private function registerSubscribers(): void
{
$subscribers = [
new TestSuiteStartedSubscriber($this),
new TestSuiteFinishedSubscriber($this),
new TestPreparedSubscriber($this),
new TestFinishedSubscriber($this),
new TestErroredSubscriber($this),
new TestFailedSubscriber($this),
new TestMarkedIncompleteSubscriber($this),
new TestSkippedSubscriber($this),
new TestRunnerExecutionFinishedSubscriber($this),
];
Facade::instance()->registerSubscribers(...$subscribers);
}
private function createDocument(): void
{
$this->document = new DOMDocument('1.0', 'UTF-8');
$this->document->formatOutput = true;
$this->root = $this->document->createElement('testsuites');
$this->document->appendChild($this->root);
}
/**
* @throws InvalidArgumentException
* @throws NoDataSetFromDataProviderException
*/
private function handleFault(Errored|Failed $event, string $type): void
{
if (! $this->prepared) {
$this->createTestCase($event);
}
assert($this->currentTestCase instanceof \DOMElement);
$throwable = $event->throwable();
$testName = $this->converter->getTestCaseMethodName($event->test());
$message = $this->converter->getExceptionMessage($throwable);
$details = $this->converter->getExceptionDetails($throwable);
$buffer = $testName;
$buffer .= trim(
$message.PHP_EOL.
$details,
);
$fault = $this->document->createElement(
$type,
Xml::prepareString($buffer),
);
$fault->setAttribute('type', $throwable->className());
$this->currentTestCase->appendChild($fault);
if (! $this->prepared) {
$this->handleFinish($event->telemetryInfo(), 0);
}
}
/**
* @throws InvalidArgumentException
* @throws NoDataSetFromDataProviderException
*/
private function handleIncompleteOrSkipped(MarkedIncomplete|Skipped $event): void
{
if (! $this->prepared) {
$this->createTestCase($event);
}
assert($this->currentTestCase instanceof \DOMElement);
$skipped = $this->document->createElement('skipped');
$this->currentTestCase->appendChild($skipped);
$this->testSuiteSkipped[$this->testSuiteLevel]++;
if (! $this->prepared) {
$this->handleFinish($event->telemetryInfo(), 0);
}
}
/**
* @throws InvalidArgumentException
* @throws NoDataSetFromDataProviderException
*
* @psalm-assert !null $this->currentTestCase
*/
private function createTestCase(Errored|Failed|MarkedIncomplete|Prepared|Skipped $event): void
{
$testCase = $this->document->createElement('testcase');
$test = $event->test();
$file = $this->converter->getTestCaseLocation($test);
$testCase->setAttribute('name', $this->converter->getTestCaseMethodName($test));
$testCase->setAttribute('file', $file);
if ($test->isTestMethod()) {
$className = $this->converter->getTrimmedTestClassName($test);
$testCase->setAttribute('class', $className);
$testCase->setAttribute('classname', str_replace('\\', '.', $className));
}
$this->currentTestCase = $testCase;
$this->time = $event->telemetryInfo()->time();
}
}

View File

@ -0,0 +1,28 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\JUnit\Subscriber;
use Pest\Logging\JUnit\JUnitLogger;
/**
* @internal
*/
abstract class Subscriber
{
/**
* Creates a new Subscriber instance.
*/
public function __construct(private readonly JUnitLogger $logger)
{
}
/**
* Creates a new JunitLogger instance.
*/
final protected function logger(): JUnitLogger
{
return $this->logger;
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\JUnit\Subscriber;
use PHPUnit\Event\Test\Errored;
use PHPUnit\Event\Test\ErroredSubscriber;
/**
* @internal
*/
final class TestErroredSubscriber extends Subscriber implements ErroredSubscriber
{
public function notify(Errored $event): void
{
$this->logger()->testErrored($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\JUnit\Subscriber;
use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\FailedSubscriber;
/**
* @internal
*/
final class TestFailedSubscriber extends Subscriber implements FailedSubscriber
{
public function notify(Failed $event): void
{
$this->logger()->testFailed($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\JUnit\Subscriber;
use PHPUnit\Event\Test\Finished;
use PHPUnit\Event\Test\FinishedSubscriber;
/**
* @internal
*/
final class TestFinishedSubscriber extends Subscriber implements FinishedSubscriber
{
public function notify(Finished $event): void
{
$this->logger()->testFinished($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\JUnit\Subscriber;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\MarkedIncompleteSubscriber;
/**
* @internal
*/
final class TestMarkedIncompleteSubscriber extends Subscriber implements MarkedIncompleteSubscriber
{
public function notify(MarkedIncomplete $event): void
{
$this->logger()->testMarkedIncomplete($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\JUnit\Subscriber;
use PHPUnit\Event\Test\Prepared;
use PHPUnit\Event\Test\PreparedSubscriber;
/**
* @internal
*/
final class TestPreparedSubscriber extends Subscriber implements PreparedSubscriber
{
public function notify(Prepared $event): void
{
$this->logger()->testPrepared($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\JUnit\Subscriber;
use PHPUnit\Event\TestRunner\ExecutionFinished;
use PHPUnit\Event\TestRunner\ExecutionFinishedSubscriber;
/**
* @internal
*/
final class TestRunnerExecutionFinishedSubscriber extends Subscriber implements ExecutionFinishedSubscriber
{
public function notify(ExecutionFinished $event): void
{
$this->logger()->flush();
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\JUnit\Subscriber;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\Test\SkippedSubscriber;
/**
* @internal
*/
final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
{
public function notify(Skipped $event): void
{
$this->logger()->testSkipped($event);
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\JUnit\Subscriber;
use PHPUnit\Event\TestSuite\Finished;
use PHPUnit\Event\TestSuite\FinishedSubscriber;
/**
* @internal
*/
final class TestSuiteFinishedSubscriber extends Subscriber implements FinishedSubscriber
{
public function notify(Finished $event): void
{
$this->logger()->testSuiteFinished();
}
}

View File

@ -0,0 +1,19 @@
<?php
declare(strict_types=1);
namespace Pest\Logging\JUnit\Subscriber;
use PHPUnit\Event\TestSuite\Started;
use PHPUnit\Event\TestSuite\StartedSubscriber;
/**
* @internal
*/
final class TestSuiteStartedSubscriber extends Subscriber implements StartedSubscriber
{
public function notify(Started $event): void
{
$this->logger()->testSuiteStarted($event);
}
}

View File

@ -63,7 +63,7 @@ final class ServiceMessage
}
/**
* @param int $duration in milliseconds
* @param int $duration in milliseconds
*/
public static function testFinished(string $name, int $duration): self
{

View File

@ -6,6 +6,7 @@ namespace Pest\Logging\TeamCity;
use NunoMaduro\Collision\Adapters\Phpunit\Style;
use Pest\Exceptions\ShouldNotHappen;
use Pest\Logging\Converter;
use Pest\Logging\TeamCity\Subscriber\TestConsideredRiskySubscriber;
use Pest\Logging\TeamCity\Subscriber\TestErroredSubscriber;
use Pest\Logging\TeamCity\Subscriber\TestExecutionFinishedSubscriber;

View File

@ -314,13 +314,13 @@ final class Expectation
/**
* Asserts that the value contains the provided properties $names.
*
* @param iterable<array-key, string> $names
* @param iterable<string, mixed>|iterable<int, string> $names
* @return self<TValue>
*/
public function toHaveProperties(iterable $names, string $message = ''): self
{
foreach ($names as $name => $value) {
is_int($name) ? $this->toHaveProperty($value, message: $message) : $this->toHaveProperty($name, $value, $message);
is_int($name) ? $this->toHaveProperty($value, message: $message) : $this->toHaveProperty($name, $value, $message); // @phpstan-ignore-line
}
return $this;

View File

@ -18,6 +18,7 @@ use Pest\Support\HigherOrderCallables;
use Pest\Support\NullClosure;
use Pest\Support\Str;
use Pest\TestSuite;
use PHPUnit\Framework\AssertionFailedError;
use PHPUnit\Framework\TestCase;
/**
@ -57,6 +58,14 @@ final class TestCall
$this->testSuite->beforeEach->get($this->filename)[0]($this);
}
/**
* Asserts that the test fails with the given message.
*/
public function fails(?string $message = null): self
{
return $this->throws(AssertionFailedError::class, $message);
}
/**
* Asserts that the test throws the given `$exceptionClass` when called.
*/
@ -199,12 +208,37 @@ final class TestCall
return $this;
}
/**
* Skips the current test on the given PHP version.
*/
public function skipOnPhp(string $version): self
{
if (mb_strlen($version) < 2) {
throw new InvalidArgumentException('The version must start with [<] or [>].');
}
if (str_starts_with($version, '>=') || str_starts_with($version, '<=')) {
$operator = substr($version, 0, 2);
$version = substr($version, 2);
} elseif (str_starts_with($version, '>') || str_starts_with($version, '<')) {
$operator = $version[0];
$version = substr($version, 1);
// ensure starts with number:
} elseif (is_numeric($version[0])) {
$operator = '==';
} else {
throw new InvalidArgumentException('The version must start with [<, >, <=, >=] or a number.');
}
return $this->skip(version_compare(PHP_VERSION, $version, $operator), sprintf('This test is skipped on PHP [%s%s].', $operator, $version));
}
/**
* Skips the current test if the given test is running on Windows.
*/
public function skipOnWindows(): self
{
return $this->skipOn('Windows', 'This test is skipped on [Windows].');
return $this->skipOnOs('Windows', 'This test is skipped on [Windows].');
}
/**
@ -212,7 +246,7 @@ final class TestCall
*/
public function skipOnMac(): self
{
return $this->skipOn('Darwin', 'This test is skipped on [Mac].');
return $this->skipOnOs('Darwin', 'This test is skipped on [Mac].');
}
/**
@ -220,13 +254,13 @@ final class TestCall
*/
public function skipOnLinux(): self
{
return $this->skipOn('Linux', 'This test is skipped on [Linux].');
return $this->skipOnOs('Linux', 'This test is skipped on [Linux].');
}
/**
* Skips the current test if the given test is running on the given operating systems.
*/
private function skipOn(string $osFamily, string $message): self
private function skipOnOs(string $osFamily, string $message): self
{
return $osFamily === PHP_OS_FAMILY
? $this->skip($message)

View File

@ -6,7 +6,7 @@ namespace Pest;
function version(): string
{
return '2.28.1';
return '2.32.2';
}
function testDirectory(string $file = ''): string

View File

@ -30,10 +30,11 @@ final class Cache implements HandlesArguments
*/
public function handleArguments(array $arguments): array
{
$arguments = $this->pushArgument(
sprintf('--cache-directory=%s', realpath(self::TEMPORARY_FOLDER)),
$arguments
);
if (! $this->hasArgument('--cache-directory', $arguments)) {
$arguments = $this->pushArgument('--cache-directory', $arguments);
$arguments = $this->pushArgument((string) realpath(self::TEMPORARY_FOLDER), $arguments);
}
if (! $this->hasArgument('--parallel', $arguments)) {
return $this->pushArgument('--cache-result', $arguments);

View File

@ -16,7 +16,17 @@ trait HandleArguments
*/
public function hasArgument(string $argument, array $arguments): bool
{
return in_array($argument, $arguments, true);
foreach ($arguments as $arg) {
if ($arg === $argument) {
return true;
}
if (str_starts_with($arg, "$argument=")) {
return true;
}
}
return false;
}
/**

View File

@ -93,10 +93,9 @@ final class Help implements HandlesArguments
*/
private function getContent(): array
{
$helpReflection = new \ReflectionClass(PHPUnitHelp::class);
$helpReflection = new PHPUnitHelp();
/** @var array<string, array<int, array{arg: string, desc: string}>> $content */
$content = $helpReflection->getConstant('HELP_TEXT');
$content = (fn (): array => $this->elements())->call($helpReflection);
$content['Configuration'] = [...[[
'arg' => '--init',

44
src/Plugins/JUnit.php Normal file
View File

@ -0,0 +1,44 @@
<?php
declare(strict_types=1);
namespace Pest\Plugins;
use Pest\Contracts\Plugins\HandlesArguments;
use Pest\Plugins\Concerns\HandleArguments;
/**
* @internal
*/
final class JUnit implements HandlesArguments
{
use HandleArguments;
/**
* Handles the arguments, adding the cache directory and the cache result arguments.
*/
public function handleArguments(array $arguments): array
{
if (! $this->hasArgument('--log-junit', $arguments)) {
return $arguments;
}
$logUnitArgument = null;
$arguments = array_filter($arguments, function (string $argument) use (&$logUnitArgument): bool {
if (str_starts_with($argument, '--log-junit')) {
$logUnitArgument = $argument;
return false;
}
return true;
});
assert(is_string($logUnitArgument));
$arguments[] = $logUnitArgument;
return array_values($arguments);
}
}

View File

@ -30,12 +30,12 @@ final class TestRepository
private array $uses = [];
/**
* @var array<int, TestCaseFilter>
* @var array<int, TestCaseFilter>
*/
private array $testCaseFilters = [];
/**
* @var array<int, TestCaseMethodFilter>
* @var array<int, TestCaseMethodFilter>
*/
private array $testCaseMethodFilters = [];

View File

@ -0,0 +1,48 @@
<?php
declare(strict_types=1);
namespace Pest\Subscribers;
use Pest\Logging\Converter;
use Pest\Logging\JUnit\JUnitLogger;
use Pest\Support\Container;
use Pest\TestSuite;
use PHPUnit\Event\TestRunner\Configured;
use PHPUnit\Event\TestRunner\ConfiguredSubscriber;
use PHPUnit\TextUI\Configuration\Configuration;
use PHPUnit\TextUI\Output\DefaultPrinter;
use Symfony\Component\Console\Input\InputInterface;
/**
* @internal
*/
final class EnsureJunitEnabled implements ConfiguredSubscriber
{
/**
* Creates a new Configured Subscriber instance.
*/
public function __construct(
private readonly InputInterface $input,
private readonly TestSuite $testSuite,
) {
}
/**
* Runs the subscriber.
*/
public function notify(Configured $event): void
{
if (! $this->input->hasParameterOption('--log-junit')) {
return;
}
$configuration = Container::getInstance()->get(Configuration::class);
assert($configuration instanceof Configuration);
new JUnitLogger(
DefaultPrinter::from($configuration->logfileJunit()),
new Converter($this->testSuite->rootPath),
);
}
}

View File

@ -4,7 +4,7 @@ declare(strict_types=1);
namespace Pest\Subscribers;
use Pest\Logging\TeamCity\Converter;
use Pest\Logging\Converter;
use Pest\Logging\TeamCity\TeamCityLogger;
use Pest\TestSuite;
use PHPUnit\Event\TestRunner\Configured;

View File

@ -58,7 +58,7 @@ final class HigherOrderMessageCollection
/**
* Count the number of messages with the given name.
*
* @param string $name A higher order message name (usually a method name)
* @param string $name A higher order message name (usually a method name)
*/
public function count(string $name): int
{

View File

@ -24,7 +24,7 @@ final class Str
* Create a (unsecure & non-cryptographically safe) random alpha-numeric
* string value.
*
* @param int $length the length of the resulting randomized string
* @param int $length the length of the resulting randomized string
*
* @see https://github.com/laravel/framework/blob/4.2/src/Illuminate/Support/Str.php#L240-L242
*/

View File

@ -1,5 +1,5 @@
Pest Testing Framework 2.28.1.
Pest Testing Framework 2.32.2.
USAGE: pest <file> [options]
@ -84,6 +84,7 @@
--reverse-list .............................. Print defects in reverse order
--teamcity . Replace default progress and result output with TeamCity format
--testdox ................ Replace default result output with TestDox format
--debug Replace default progress and result output with debugging information
--compact ................ Replace default result output with Compact format
LOGGING OPTIONS:

View File

@ -1,3 +1,3 @@
Pest Testing Framework 2.28.1.
Pest Testing Framework 2.32.2.

View File

@ -898,6 +898,14 @@
✓ it skips with falsy closure condition
✓ it can be used in higher order tests
PASS Tests\Features\Fail
✓ it may fail
✓ it may fail with the given message
PASS Tests\Features\Fails
✓ it may fail
✓ it may fail with the given message
WARN Tests\Features\Helpers
✓ it can set/get properties on $this
! it gets null if property do not exist → Undefined property Tests\Features\Helpers::$wqdwqdqw
@ -1085,6 +1093,11 @@
- it can use something in the test case as a condition → This test was skipped
- it can user higher order callables and skip
WARN Tests\Features\SkipOnPhp
✓ it can run on php version
✓ it can run on specific php version
- it can skip on php versions depending on constraint → This test is skipped on PHP [>=7.4.0].
PASS Tests\Features\Test
✓ a test
✓ higher order message test
@ -1204,6 +1217,14 @@
PASS Tests\Unit\Overrides\ThrowableBuilder
✓ collision editor can be added to the stack trace
PASS Tests\Unit\Plugins\Concerns\HandleArguments
✓ method hasArgument with ('--long-argument', true)
✓ method hasArgument with ('-a', true)
✓ method hasArgument with ('--with-equal-sign', true)
✓ method hasArgument with ('someValue', true)
✓ method hasArgument with ('--a', false)
✓ method hasArgument with ('--undefined-argument', false)
PASS Tests\Unit\Plugins\Environment
✓ environment is set to CI when --ci option is used
✓ environment is set to Local when --ci option is not used
@ -1356,4 +1377,4 @@
WARN Tests\Visual\Version
- visual snapshot of help command output
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 19 skipped, 966 passed (2286 assertions)
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 20 skipped, 978 passed (2304 assertions)

11
tests/Features/Fail.php Normal file
View File

@ -0,0 +1,11 @@
<?php
use PHPUnit\Framework\AssertionFailedError;
it('may fail', function () {
$this->fail();
})->throws(AssertionFailedError::class);
it('may fail with the given message', function () {
$this->fail('this is a failure');
})->throws(AssertionFailedError::class, 'this is a failure');

9
tests/Features/Fails.php Normal file
View File

@ -0,0 +1,9 @@
<?php
it('may fail', function () {
$this->fail();
})->fails();
it('may fail with the given message', function () {
$this->fail('this is a failure');
})->fails('this is a failure');

View File

@ -0,0 +1,13 @@
<?php
it('can run on php version')
->skipOnPhp('<=7.4.0')
->assertTrue(true);
it('can run on specific php version')
->skipOnPhp('7.4.0')
->assertTrue(true);
it('can skip on php versions depending on constraint')
->skipOnPhp('>=7.4.0')
->assertTrue(false);

View File

@ -0,0 +1,26 @@
<?php
use Pest\Plugins\Concerns\HandleArguments;
test('method hasArgument', function (string $argument, bool $expectedResult) {
$obj = new class
{
use HandleArguments;
};
$arguments = [
'--long-argument',
'someValue',
'-a',
'--with-equal-sign=1337',
];
expect($obj->hasArgument($argument, $arguments))->toBe($expectedResult);
})->with([
['--long-argument', true],
['-a', true],
['--with-equal-sign', true],
['someValue', true],
['--a', false],
['--undefined-argument', false],
]);

View File

@ -16,7 +16,7 @@ $run = function () {
test('parallel', function () use ($run) {
expect($run('--exclude-group=integration'))
->toContain('Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 15 skipped, 953 passed (2267 assertions)')
->toContain('Tests: 1 deprecated, 4 warnings, 5 incomplete, 2 notices, 13 todos, 16 skipped, 965 passed (2285 assertions)')
->toContain('Parallel: 3 processes');
})->skipOnWindows();