mirror of
https://github.com/pestphp/pest.git
synced 2026-03-06 07:47:22 +01:00
Merge branch '2.x' into feature/compact-dataset-description
This commit is contained in:
3
.gitattributes
vendored
3
.gitattributes
vendored
@ -1,4 +1,5 @@
|
|||||||
/art export-ignore
|
/art export-ignore
|
||||||
|
/docker export-ignore
|
||||||
/docs export-ignore
|
/docs export-ignore
|
||||||
/tests export-ignore
|
/tests export-ignore
|
||||||
/scripts export-ignore
|
/scripts export-ignore
|
||||||
@ -11,5 +12,7 @@ phpstan.neon export-ignore
|
|||||||
/phpunit.xml export-ignore
|
/phpunit.xml export-ignore
|
||||||
CHANGELOG.md export-ignore
|
CHANGELOG.md export-ignore
|
||||||
CONTRIBUTING.md export-ignore
|
CONTRIBUTING.md export-ignore
|
||||||
|
docker-compose.yml export-ignore
|
||||||
|
Makefile export-ignore
|
||||||
README.md export-ignore
|
README.md export-ignore
|
||||||
|
|
||||||
|
|||||||
6
.github/workflows/static.yml
vendored
6
.github/workflows/static.yml
vendored
@ -1,6 +1,10 @@
|
|||||||
name: Static Analysis
|
name: Static Analysis
|
||||||
|
|
||||||
on: ['push', 'pull_request']
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
static:
|
static:
|
||||||
|
|||||||
9
.github/workflows/tests.yml
vendored
9
.github/workflows/tests.yml
vendored
@ -1,13 +1,18 @@
|
|||||||
name: Tests
|
name: Tests
|
||||||
|
|
||||||
on: ['push', 'pull_request']
|
on:
|
||||||
|
push:
|
||||||
|
pull_request:
|
||||||
|
schedule:
|
||||||
|
- cron: '0 0 * * *'
|
||||||
|
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
ci:
|
ci:
|
||||||
runs-on: ${{ matrix.os }}
|
runs-on: ${{ matrix.os }}
|
||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
os: [ubuntu-latest, macos-latest] # "windows-latest" is waiting for https://github.com/pestphp/pest/issues/638
|
os: [ubuntu-latest, macos-latest, windows-latest]
|
||||||
php: ['8.1', '8.2']
|
php: ['8.1', '8.2']
|
||||||
dependency-version: [prefer-lowest, prefer-stable]
|
dependency-version: [prefer-lowest, prefer-stable]
|
||||||
parallel: ['', '--parallel']
|
parallel: ['', '--parallel']
|
||||||
|
|||||||
2
.gitignore
vendored
2
.gitignore
vendored
@ -1,5 +1,6 @@
|
|||||||
.idea/*
|
.idea/*
|
||||||
.idea/codeStyleSettings.xml
|
.idea/codeStyleSettings.xml
|
||||||
|
.temp/*
|
||||||
composer.lock
|
composer.lock
|
||||||
/vendor/
|
/vendor/
|
||||||
coverage.xml
|
coverage.xml
|
||||||
@ -8,7 +9,6 @@ coverage.xml
|
|||||||
/.php-cs-fixer.php
|
/.php-cs-fixer.php
|
||||||
.php-cs-fixer.cache
|
.php-cs-fixer.cache
|
||||||
.temp/coverage.php
|
.temp/coverage.php
|
||||||
.temp/retry.json
|
|
||||||
*.swp
|
*.swp
|
||||||
*.swo
|
*.swo
|
||||||
.vscode/
|
.vscode/
|
||||||
|
|||||||
@ -54,3 +54,22 @@ Integration tests:
|
|||||||
```bash
|
```bash
|
||||||
composer test:integration
|
composer test:integration
|
||||||
```
|
```
|
||||||
|
|
||||||
|
## Simplified setup using Docker
|
||||||
|
|
||||||
|
If you have Docker installed, you can quickly get all dependencies for Pest in place using
|
||||||
|
our Docker files. Assuming you have the repository cloned, you may run the following
|
||||||
|
commands:
|
||||||
|
|
||||||
|
1. `make build` to build the Docker image
|
||||||
|
2. `make install` to install Composer dependencies
|
||||||
|
3. `make test` to run the project tests and analysis tools
|
||||||
|
|
||||||
|
If you want to check things work against a specific version of PHP, you may include
|
||||||
|
the `PHP` build argument when building the image:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
make build ARGS="--build-arg PHP=8.2"
|
||||||
|
```
|
||||||
|
|
||||||
|
The default PHP version will always be the lowest version of PHP supported by Pest.
|
||||||
|
|||||||
14
Makefile
Normal file
14
Makefile
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
# Well documented Makefiles
|
||||||
|
DEFAULT_GOAL := help
|
||||||
|
help:
|
||||||
|
@awk 'BEGIN {FS = ":.*##"; printf "\nUsage:\n make \033[36m<target>\033[0m\n"} /^[a-zA-Z0-9_-]+:.*?##/ { printf " \033[36m%-40s\033[0m %s\n", $$1, $$2 } /^##@/ { printf "\n\033[1m%s\033[0m\n", substr($$0, 5) } ' $(MAKEFILE_LIST)
|
||||||
|
|
||||||
|
build: ## Build all docker images. Specify the command e.g. via make build ARGS="--build-arg PHP=8.2"
|
||||||
|
docker compose build $(ARGS)
|
||||||
|
|
||||||
|
##@ [Application]
|
||||||
|
install: ## Install the composer dependencies
|
||||||
|
docker compose run --rm composer install
|
||||||
|
|
||||||
|
test: ## Run the tests
|
||||||
|
docker compose run --rm composer test
|
||||||
17
bin/pest
17
bin/pest
@ -17,6 +17,7 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
$_SERVER['COLLISION_PRINTER'] = 'DefaultPrinter';
|
$_SERVER['COLLISION_PRINTER'] = 'DefaultPrinter';
|
||||||
|
|
||||||
$args = $_SERVER['argv'];
|
$args = $_SERVER['argv'];
|
||||||
|
|
||||||
$dirty = false;
|
$dirty = false;
|
||||||
$todo = false;
|
$todo = false;
|
||||||
|
|
||||||
@ -68,11 +69,11 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
|
|
||||||
// Get $rootPath based on $autoloadPath
|
// Get $rootPath based on $autoloadPath
|
||||||
$rootPath = dirname($autoloadPath, 2);
|
$rootPath = dirname($autoloadPath, 2);
|
||||||
$argv = new ArgvInput();
|
$input = new ArgvInput();
|
||||||
|
|
||||||
$testSuite = TestSuite::getInstance(
|
$testSuite = TestSuite::getInstance(
|
||||||
$rootPath,
|
$rootPath,
|
||||||
$argv->getParameterOption('--test-directory', (new ConfigLoader($rootPath))->getTestsDirectory()),
|
$input->getParameterOption('--test-directory', (new ConfigLoader($rootPath))->getTestsDirectory()),
|
||||||
);
|
);
|
||||||
|
|
||||||
if ($dirty) {
|
if ($dirty) {
|
||||||
@ -83,19 +84,13 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter());
|
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter());
|
||||||
}
|
}
|
||||||
|
|
||||||
$isDecorated = $argv->getParameterOption('--colors', 'always') !== 'never';
|
$isDecorated = $input->getParameterOption('--colors', 'always') !== 'never';
|
||||||
|
|
||||||
$output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated);
|
$output = new ConsoleOutput(ConsoleOutput::VERBOSITY_NORMAL, $isDecorated);
|
||||||
|
|
||||||
$container = Container::getInstance();
|
$kernel = Kernel::boot($testSuite, $input, $output);
|
||||||
$container->add(TestSuite::class, $testSuite);
|
|
||||||
$container->add(OutputInterface::class, $output);
|
|
||||||
$container->add(InputInterface::class, $argv);
|
|
||||||
$container->add(Container::class, $container);
|
|
||||||
|
|
||||||
$kernel = Kernel::boot();
|
$result = $kernel->handle($args);
|
||||||
|
|
||||||
$result = $kernel->handle($output, $args);
|
|
||||||
|
|
||||||
$kernel->shutdown();
|
$kernel->shutdown();
|
||||||
|
|
||||||
|
|||||||
101
bin/worker.php
Normal file
101
bin/worker.php
Normal file
@ -0,0 +1,101 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
use ParaTest\WrapperRunner\ApplicationForWrapperWorker;
|
||||||
|
use ParaTest\WrapperRunner\WrapperWorker;
|
||||||
|
use Pest\ConfigLoader;
|
||||||
|
use Pest\Kernel;
|
||||||
|
use Pest\Plugins\Actions\CallsHandleArguments;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
$bootPest = (static function (): void {
|
||||||
|
$workerArgv = new ArgvInput();
|
||||||
|
|
||||||
|
$rootPath = dirname(PHPUNIT_COMPOSER_INSTALL, 2);
|
||||||
|
$testSuite = TestSuite::getInstance($rootPath, $workerArgv->getParameterOption(
|
||||||
|
'--test-directory',
|
||||||
|
(new ConfigLoader($rootPath))->getTestsDirectory()
|
||||||
|
));
|
||||||
|
|
||||||
|
$input = new ArgvInput();
|
||||||
|
|
||||||
|
$output = new ConsoleOutput(OutputInterface::VERBOSITY_NORMAL, true);
|
||||||
|
|
||||||
|
Kernel::boot($testSuite, $input, $output);
|
||||||
|
});
|
||||||
|
|
||||||
|
(static function () use ($bootPest): void {
|
||||||
|
$getopt = getopt('', [
|
||||||
|
'status-file:',
|
||||||
|
'progress-file:',
|
||||||
|
'testresult-file:',
|
||||||
|
'teamcity-file:',
|
||||||
|
'testdox-file:',
|
||||||
|
'testdox-color',
|
||||||
|
'phpunit-argv:',
|
||||||
|
]);
|
||||||
|
|
||||||
|
$composerAutoloadFiles = [
|
||||||
|
dirname(__DIR__, 3).DIRECTORY_SEPARATOR.'autoload.php',
|
||||||
|
dirname(__DIR__, 2).DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php',
|
||||||
|
dirname(__DIR__).DIRECTORY_SEPARATOR.'vendor'.DIRECTORY_SEPARATOR.'autoload.php',
|
||||||
|
];
|
||||||
|
|
||||||
|
foreach ($composerAutoloadFiles as $file) {
|
||||||
|
if (file_exists($file)) {
|
||||||
|
require_once $file;
|
||||||
|
define('PHPUNIT_COMPOSER_INSTALL', $file);
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert(isset($getopt['status-file']) && is_string($getopt['status-file']));
|
||||||
|
$statusFile = fopen($getopt['status-file'], 'wb');
|
||||||
|
assert(is_resource($statusFile));
|
||||||
|
|
||||||
|
assert(isset($getopt['progress-file']) && is_string($getopt['progress-file']));
|
||||||
|
assert(isset($getopt['testresult-file']) && is_string($getopt['testresult-file']));
|
||||||
|
assert(! isset($getopt['teamcity-file']) || is_string($getopt['teamcity-file']));
|
||||||
|
assert(! isset($getopt['testdox-file']) || is_string($getopt['testdox-file']));
|
||||||
|
|
||||||
|
assert(isset($getopt['phpunit-argv']) && is_string($getopt['phpunit-argv']));
|
||||||
|
$phpunitArgv = unserialize($getopt['phpunit-argv'], ['allowed_classes' => false]);
|
||||||
|
assert(is_array($phpunitArgv));
|
||||||
|
|
||||||
|
$bootPest();
|
||||||
|
|
||||||
|
$phpunitArgv = (new CallsHandleArguments())($phpunitArgv);
|
||||||
|
|
||||||
|
$application = new ApplicationForWrapperWorker(
|
||||||
|
$phpunitArgv,
|
||||||
|
$getopt['progress-file'],
|
||||||
|
$getopt['testresult-file'],
|
||||||
|
$getopt['teamcity-file'] ?? null,
|
||||||
|
$getopt['testdox-file'] ?? null,
|
||||||
|
isset($getopt['testdox-color']),
|
||||||
|
);
|
||||||
|
|
||||||
|
while (true) {
|
||||||
|
if (feof(STDIN)) {
|
||||||
|
$application->end();
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$testPath = fgets(STDIN);
|
||||||
|
if ($testPath === false || $testPath === WrapperWorker::COMMAND_EXIT) {
|
||||||
|
$application->end();
|
||||||
|
|
||||||
|
exit;
|
||||||
|
}
|
||||||
|
|
||||||
|
$exitCode = $application->runTest(trim($testPath));
|
||||||
|
|
||||||
|
fwrite($statusFile, (string) $exitCode);
|
||||||
|
fflush($statusFile);
|
||||||
|
}
|
||||||
|
})();
|
||||||
@ -18,10 +18,13 @@
|
|||||||
],
|
],
|
||||||
"require": {
|
"require": {
|
||||||
"php": "^8.1.0",
|
"php": "^8.1.0",
|
||||||
"nunomaduro/collision": "^7.0.0",
|
"nunomaduro/collision": "^7.0.2",
|
||||||
"nunomaduro/termwind": "^1.15",
|
"nunomaduro/termwind": "^1.15.1",
|
||||||
"pestphp/pest-plugin": "^2.0.0",
|
"pestphp/pest-plugin": "^2.0.0",
|
||||||
"phpunit/phpunit": "10.0.x-dev"
|
"phpunit/phpunit": "^10.0.7"
|
||||||
|
},
|
||||||
|
"conflict": {
|
||||||
|
"brianium/paratest": "<7.0.4"
|
||||||
},
|
},
|
||||||
"version": "2.x-dev",
|
"version": "2.x-dev",
|
||||||
"autoload": {
|
"autoload": {
|
||||||
@ -43,9 +46,10 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"require-dev": {
|
"require-dev": {
|
||||||
"pestphp/pest-dev-tools": "^2.2",
|
"brianium/paratest": "^7.0.6",
|
||||||
|
"pestphp/pest-dev-tools": "^2.4.0",
|
||||||
"pestphp/pest-plugin-arch": "^2.0.0",
|
"pestphp/pest-plugin-arch": "^2.0.0",
|
||||||
"symfony/process": "^6.2.0"
|
"symfony/process": "^6.2.5"
|
||||||
},
|
},
|
||||||
"minimum-stability": "dev",
|
"minimum-stability": "dev",
|
||||||
"prefer-stable": true,
|
"prefer-stable": true,
|
||||||
@ -81,6 +85,7 @@
|
|||||||
"extra": {
|
"extra": {
|
||||||
"pest": {
|
"pest": {
|
||||||
"plugins": [
|
"plugins": [
|
||||||
|
"Pest\\Plugins\\Cache",
|
||||||
"Pest\\Plugins\\Coverage",
|
"Pest\\Plugins\\Coverage",
|
||||||
"Pest\\Plugins\\Init",
|
"Pest\\Plugins\\Init",
|
||||||
"Pest\\Plugins\\Environment",
|
"Pest\\Plugins\\Environment",
|
||||||
@ -88,7 +93,8 @@
|
|||||||
"Pest\\Plugins\\Memory",
|
"Pest\\Plugins\\Memory",
|
||||||
"Pest\\Plugins\\Printer",
|
"Pest\\Plugins\\Printer",
|
||||||
"Pest\\Plugins\\Retry",
|
"Pest\\Plugins\\Retry",
|
||||||
"Pest\\Plugins\\Version"
|
"Pest\\Plugins\\Version",
|
||||||
|
"Pest\\Plugins\\Parallel"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
14
docker-compose.yml
Normal file
14
docker-compose.yml
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
version: "3.8"
|
||||||
|
|
||||||
|
services:
|
||||||
|
php:
|
||||||
|
build:
|
||||||
|
context: ./docker
|
||||||
|
volumes:
|
||||||
|
- .:/var/www/html
|
||||||
|
composer:
|
||||||
|
build:
|
||||||
|
context: ./docker
|
||||||
|
volumes:
|
||||||
|
- .:/var/www/html
|
||||||
|
entrypoint: ["composer"]
|
||||||
23
docker/Dockerfile
Normal file
23
docker/Dockerfile
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
ARG PHP=8.1
|
||||||
|
FROM php:${PHP}-cli-alpine
|
||||||
|
|
||||||
|
RUN apk update \
|
||||||
|
&& apk add zip libzip-dev icu-dev
|
||||||
|
|
||||||
|
RUN docker-php-ext-configure zip
|
||||||
|
RUN docker-php-ext-install zip
|
||||||
|
RUN docker-php-ext-enable zip
|
||||||
|
|
||||||
|
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 pecl install xdebug
|
||||||
|
RUN docker-php-ext-enable xdebug
|
||||||
|
|
||||||
|
COPY --from=composer:2 /usr/bin/composer /usr/bin/composer
|
||||||
|
|
||||||
|
WORKDIR /var/www/html
|
||||||
|
|
||||||
|
ENTRYPOINT ["php"]
|
||||||
@ -0,0 +1,45 @@
|
|||||||
|
<?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\TextUI\Output\Default\ProgressPrinter;
|
||||||
|
|
||||||
|
use PHPUnit\Event\Test\Skipped;
|
||||||
|
use PHPUnit\Event\Test\SkippedSubscriber;
|
||||||
|
use ReflectionClass;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal This class is not covered by the backward compatibility promise for PHPUnit
|
||||||
|
*
|
||||||
|
* This file is overridden to allow Pest Parallel to show todo items in the progress output.
|
||||||
|
*/
|
||||||
|
final class TestSkippedSubscriber extends Subscriber implements SkippedSubscriber
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Notifies the printer that a test was skipped.
|
||||||
|
*/
|
||||||
|
public function notify(Skipped $event): void
|
||||||
|
{
|
||||||
|
str_contains($event->message(), '__TODO__')
|
||||||
|
? $this->printTodoItem()
|
||||||
|
: $this->printer()->testSkipped();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints a "T" to the standard PHPUnit output to indicate a todo item.
|
||||||
|
*/
|
||||||
|
private function printTodoItem(): void
|
||||||
|
{
|
||||||
|
$mirror = new ReflectionClass($this->printer());
|
||||||
|
$printerMirror = $mirror->getMethod('printProgress');
|
||||||
|
$printerMirror->invoke($this->printer(), 'T');
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -12,10 +12,10 @@ parameters:
|
|||||||
reportUnmatchedIgnoredErrors: true
|
reportUnmatchedIgnoredErrors: true
|
||||||
|
|
||||||
ignoreErrors:
|
ignoreErrors:
|
||||||
- '#Cannot instantiate interface PHPUnit\\Util\\Exception#'
|
- "#Language construct isset\\(\\) should not be used.#"
|
||||||
|
- "#is not allowed to extend#"
|
||||||
- "#with a nullable type declaration#"
|
- "#with a nullable type declaration#"
|
||||||
- "#type mixed is not subtype of native#"
|
- "#type mixed is not subtype of native#"
|
||||||
- "#is not allowed to extend#"
|
|
||||||
- "# with null as default value#"
|
- "# with null as default value#"
|
||||||
- "#has parameter \\$closure with default value.#"
|
- "#has parameter \\$closure with default value.#"
|
||||||
- "#has parameter \\$description with default value.#"
|
- "#has parameter \\$description with default value.#"
|
||||||
|
|||||||
@ -5,14 +5,12 @@
|
|||||||
beStrictAboutTestsThatDoNotTestAnything="true"
|
beStrictAboutTestsThatDoNotTestAnything="true"
|
||||||
beStrictAboutOutputDuringTests="true"
|
beStrictAboutOutputDuringTests="true"
|
||||||
bootstrap="vendor/autoload.php"
|
bootstrap="vendor/autoload.php"
|
||||||
cacheResult="false"
|
|
||||||
colors="true"
|
colors="true"
|
||||||
failOnRisky="true"
|
failOnRisky="true"
|
||||||
failOnWarning="true"
|
failOnWarning="true"
|
||||||
processIsolation="false"
|
processIsolation="false"
|
||||||
stopOnError="false"
|
stopOnError="false"
|
||||||
stopOnFailure="false"
|
stopOnFailure="false"
|
||||||
cacheDirectory=".phpunit.cache"
|
|
||||||
backupStaticProperties="false"
|
backupStaticProperties="false"
|
||||||
displayDetailsOnIncompleteTests="true"
|
displayDetailsOnIncompleteTests="true"
|
||||||
displayDetailsOnSkippedTests="true"
|
displayDetailsOnSkippedTests="true"
|
||||||
|
|||||||
@ -13,7 +13,7 @@ use Pest\Contracts\Bootstrapper;
|
|||||||
final class BootExceptionHandler implements Bootstrapper
|
final class BootExceptionHandler implements Bootstrapper
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Boots the Exception Handler.
|
* Boots the "Collision" exception handler.
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@ -19,7 +19,7 @@ use SebastianBergmann\FileIterator\Facade as PhpUnitFileIterator;
|
|||||||
final class BootFiles implements Bootstrapper
|
final class BootFiles implements Bootstrapper
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The Pest convention.
|
* The structure of the tests directory.
|
||||||
*
|
*
|
||||||
* @var array<int, string>
|
* @var array<int, string>
|
||||||
*/
|
*/
|
||||||
@ -32,7 +32,7 @@ final class BootFiles implements Bootstrapper
|
|||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boots the Subscribers.
|
* Boots the structure of the tests directory.
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@ -20,10 +20,11 @@ final class BootOverrides implements Bootstrapper
|
|||||||
private const FILES = [
|
private const FILES = [
|
||||||
'Runner/Filter/NameFilterIterator.php',
|
'Runner/Filter/NameFilterIterator.php',
|
||||||
'Runner/TestSuiteLoader.php',
|
'Runner/TestSuiteLoader.php',
|
||||||
|
'TextUI/Output/Default/ProgressPrinter/TestSkippedSubscriber.php',
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boots the Subscribers.
|
* Boots the list of files to be overridden.
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@ -16,21 +16,18 @@ use PHPUnit\Event\Subscriber;
|
|||||||
final class BootSubscribers implements Bootstrapper
|
final class BootSubscribers implements Bootstrapper
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* The Kernel subscribers.
|
* The list of Subscribers.
|
||||||
*
|
*
|
||||||
* @var array<int, class-string<Subscriber>>
|
* @var array<int, class-string<Subscriber>>
|
||||||
*/
|
*/
|
||||||
private const SUBSCRIBERS = [
|
private const SUBSCRIBERS = [
|
||||||
Subscribers\EnsureConfigurationIsValid::class,
|
Subscribers\EnsureConfigurationIsValid::class,
|
||||||
Subscribers\EnsureConfigurationDefaults::class,
|
Subscribers\EnsureConfigurationIsAvailable::class,
|
||||||
Subscribers\EnsureRetryRepositoryExists::class,
|
|
||||||
Subscribers\EnsureErroredTestsAreRetryable::class,
|
|
||||||
Subscribers\EnsureFailedTestsAreRetryable::class,
|
|
||||||
Subscribers\EnsureTeamCityEnabled::class,
|
Subscribers\EnsureTeamCityEnabled::class,
|
||||||
];
|
];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new Subscriber instance.
|
* Creates a new instance of the Boot Subscribers.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Container $container,
|
private readonly Container $container,
|
||||||
@ -38,7 +35,7 @@ final class BootSubscribers implements Bootstrapper
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Boots the Subscribers.
|
* Boots the list of Subscribers.
|
||||||
*/
|
*/
|
||||||
public function boot(): void
|
public function boot(): void
|
||||||
{
|
{
|
||||||
|
|||||||
@ -13,6 +13,9 @@ use Symfony\Component\Console\Output\OutputInterface;
|
|||||||
*/
|
*/
|
||||||
final class BootView implements Bootstrapper
|
final class BootView implements Bootstrapper
|
||||||
{
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new instance of the Boot View.
|
||||||
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly OutputInterface $output
|
private readonly OutputInterface $output
|
||||||
) {
|
) {
|
||||||
|
|||||||
23
src/Contracts/Panicable.php
Normal file
23
src/Contracts/Panicable.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Contracts;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
interface Panicable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Renders the panic on the given output.
|
||||||
|
*/
|
||||||
|
public function render(OutputInterface $output): void;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The exit code to be used.
|
||||||
|
*/
|
||||||
|
public function exitCode(): int;
|
||||||
|
}
|
||||||
@ -12,7 +12,7 @@ use Symfony\Component\Console\Exception\ExceptionInterface;
|
|||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class InvalidConsoleArgument extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
final class InvalidOption extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Creates a new Exception instance.
|
* Creates a new Exception instance.
|
||||||
38
src/Exceptions/NoDirtyTestsFound.php
Normal file
38
src/Exceptions/NoDirtyTestsFound.php
Normal file
@ -0,0 +1,38 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Exceptions;
|
||||||
|
|
||||||
|
use InvalidArgumentException;
|
||||||
|
use NunoMaduro\Collision\Contracts\RenderlessEditor;
|
||||||
|
use NunoMaduro\Collision\Contracts\RenderlessTrace;
|
||||||
|
use Pest\Contracts\Panicable;
|
||||||
|
use Symfony\Component\Console\Exception\ExceptionInterface;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class NoDirtyTestsFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace, Panicable
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Renders the panic on the given output.
|
||||||
|
*/
|
||||||
|
public function render(OutputInterface $output): void
|
||||||
|
{
|
||||||
|
$output->writeln([
|
||||||
|
'',
|
||||||
|
' <fg=white;options=bold;bg=blue> INFO </> No "dirty" tests found.',
|
||||||
|
'',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The exit code to be used.
|
||||||
|
*/
|
||||||
|
public function exitCode(): int
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -1,26 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Exceptions;
|
|
||||||
|
|
||||||
use InvalidArgumentException;
|
|
||||||
use NunoMaduro\Collision\Contracts\RenderlessEditor;
|
|
||||||
use NunoMaduro\Collision\Contracts\RenderlessTrace;
|
|
||||||
use Symfony\Component\Console\Exception\ExceptionInterface;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class NoTestsFound extends InvalidArgumentException implements ExceptionInterface, RenderlessEditor, RenderlessTrace
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Creates a new Exception instance.
|
|
||||||
*/
|
|
||||||
public function __construct()
|
|
||||||
{
|
|
||||||
parent::__construct('No tests found.');
|
|
||||||
|
|
||||||
// ...
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -8,7 +8,6 @@ use Closure;
|
|||||||
use Pest\Contracts\AddsAnnotations;
|
use Pest\Contracts\AddsAnnotations;
|
||||||
use Pest\Exceptions\ShouldNotHappen;
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
use Pest\Factories\Concerns\HigherOrderable;
|
use Pest\Factories\Concerns\HigherOrderable;
|
||||||
use Pest\Plugins\Retry;
|
|
||||||
use Pest\Repositories\DatasetsRepository;
|
use Pest\Repositories\DatasetsRepository;
|
||||||
use Pest\Support\Str;
|
use Pest\Support\Str;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
@ -129,12 +128,6 @@ final class TestCaseMethodFactory
|
|||||||
|
|
||||||
$methodName = Str::evaluable($this->description);
|
$methodName = Str::evaluable($this->description);
|
||||||
|
|
||||||
$retryRepository = TestSuite::getInstance()->retryRepository;
|
|
||||||
|
|
||||||
if (Retry::$retrying && ! $retryRepository->isEmpty() && ! $retryRepository->exists(sprintf('%s::%s', $classFQN, $methodName))) {
|
|
||||||
return '';
|
|
||||||
}
|
|
||||||
|
|
||||||
$datasetsCode = '';
|
$datasetsCode = '';
|
||||||
$annotations = ['@test'];
|
$annotations = ['@test'];
|
||||||
$attributes = [];
|
$attributes = [];
|
||||||
@ -188,7 +181,7 @@ final class TestCaseMethodFactory
|
|||||||
|
|
||||||
return <<<EOF
|
return <<<EOF
|
||||||
|
|
||||||
public function $dataProviderName()
|
public static function $dataProviderName()
|
||||||
{
|
{
|
||||||
return __PestDatasets::get(self::\$__filename, "$methodName");
|
return __PestDatasets::get(self::\$__filename, "$methodName");
|
||||||
}
|
}
|
||||||
|
|||||||
@ -5,13 +5,13 @@ declare(strict_types=1);
|
|||||||
namespace Pest;
|
namespace Pest;
|
||||||
|
|
||||||
use Pest\Contracts\Bootstrapper;
|
use Pest\Contracts\Bootstrapper;
|
||||||
use Pest\Exceptions\NoTestsFound;
|
use Pest\Exceptions\NoDirtyTestsFound;
|
||||||
use Pest\Plugins\Actions\CallsAddsOutput;
|
use Pest\Plugins\Actions\CallsAddsOutput;
|
||||||
use Pest\Plugins\Actions\CallsBoot;
|
use Pest\Plugins\Actions\CallsBoot;
|
||||||
use Pest\Plugins\Actions\CallsShutdown;
|
use Pest\Plugins\Actions\CallsShutdown;
|
||||||
use Pest\Support\Container;
|
use Pest\Support\Container;
|
||||||
use PHPUnit\TextUI\Application;
|
use PHPUnit\TextUI\Application;
|
||||||
use PHPUnit\TextUI\Exception;
|
use Symfony\Component\Console\Input\InputInterface;
|
||||||
use Symfony\Component\Console\Output\OutputInterface;
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -36,7 +36,8 @@ final class Kernel
|
|||||||
* Creates a new Kernel instance.
|
* Creates a new Kernel instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly Application $application
|
private readonly Application $application,
|
||||||
|
private readonly OutputInterface $output,
|
||||||
) {
|
) {
|
||||||
register_shutdown_function(function (): void {
|
register_shutdown_function(function (): void {
|
||||||
if (error_get_last() !== null) {
|
if (error_get_last() !== null) {
|
||||||
@ -50,8 +51,16 @@ final class Kernel
|
|||||||
/**
|
/**
|
||||||
* Boots the Kernel.
|
* Boots the Kernel.
|
||||||
*/
|
*/
|
||||||
public static function boot(): self
|
public static function boot(TestSuite $testSuite, InputInterface $input, OutputInterface $output): self
|
||||||
{
|
{
|
||||||
|
$container = Container::getInstance();
|
||||||
|
|
||||||
|
$container
|
||||||
|
->add(TestSuite::class, $testSuite)
|
||||||
|
->add(InputInterface::class, $input)
|
||||||
|
->add(OutputInterface::class, $output)
|
||||||
|
->add(Container::class, $container);
|
||||||
|
|
||||||
foreach (self::BOOTSTRAPPERS as $bootstrapper) {
|
foreach (self::BOOTSTRAPPERS as $bootstrapper) {
|
||||||
$bootstrapper = Container::getInstance()->get($bootstrapper);
|
$bootstrapper = Container::getInstance()->get($bootstrapper);
|
||||||
assert($bootstrapper instanceof Bootstrapper);
|
assert($bootstrapper instanceof Bootstrapper);
|
||||||
@ -61,24 +70,25 @@ final class Kernel
|
|||||||
|
|
||||||
(new CallsBoot())->__invoke();
|
(new CallsBoot())->__invoke();
|
||||||
|
|
||||||
return new self(new Application());
|
return new self(
|
||||||
|
new Application(),
|
||||||
|
$output,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Handles the given argv.
|
* Runs the application, and returns the exit code.
|
||||||
*
|
*
|
||||||
* @param array<int, string> $argv
|
* @param array<int, string> $args
|
||||||
*
|
|
||||||
* @throws Exception
|
|
||||||
*/
|
*/
|
||||||
public function handle(OutputInterface $output, array $argv): int
|
public function handle(array $args): int
|
||||||
{
|
{
|
||||||
$argv = (new Plugins\Actions\CallsHandleArguments())->__invoke($argv);
|
$args = (new Plugins\Actions\CallsHandleArguments())->__invoke($args);
|
||||||
|
|
||||||
try {
|
try {
|
||||||
$this->application->run($argv);
|
$this->application->run($args);
|
||||||
} catch (NoTestsFound) {
|
} catch (NoDirtyTestsFound) {
|
||||||
$output->writeln([
|
$this->output->writeln([
|
||||||
'',
|
'',
|
||||||
' <fg=white;options=bold;bg=blue> INFO </> No tests found.',
|
' <fg=white;options=bold;bg=blue> INFO </> No tests found.',
|
||||||
'',
|
'',
|
||||||
|
|||||||
@ -5,20 +5,14 @@ declare(strict_types=1);
|
|||||||
namespace Pest\Logging\TeamCity;
|
namespace Pest\Logging\TeamCity;
|
||||||
|
|
||||||
use NunoMaduro\Collision\Adapters\Phpunit\State;
|
use NunoMaduro\Collision\Adapters\Phpunit\State;
|
||||||
use NunoMaduro\Collision\Adapters\Phpunit\TestResult;
|
|
||||||
use Pest\Exceptions\ShouldNotHappen;
|
use Pest\Exceptions\ShouldNotHappen;
|
||||||
|
use Pest\Support\StateGenerator;
|
||||||
use Pest\Support\Str;
|
use Pest\Support\Str;
|
||||||
use PHPUnit\Event\Code\Test;
|
use PHPUnit\Event\Code\Test;
|
||||||
use PHPUnit\Event\Code\TestDox;
|
|
||||||
use PHPUnit\Event\Code\TestMethod;
|
use PHPUnit\Event\Code\TestMethod;
|
||||||
use PHPUnit\Event\Code\Throwable;
|
use PHPUnit\Event\Code\Throwable;
|
||||||
use PHPUnit\Event\Test\Errored;
|
|
||||||
use PHPUnit\Event\TestData\TestDataCollection;
|
|
||||||
use PHPUnit\Event\TestSuite\TestSuite;
|
use PHPUnit\Event\TestSuite\TestSuite;
|
||||||
use PHPUnit\Framework\Exception as FrameworkException;
|
use PHPUnit\Framework\Exception as FrameworkException;
|
||||||
use PHPUnit\Framework\IncompleteTestError;
|
|
||||||
use PHPUnit\Framework\SkippedWithMessageException;
|
|
||||||
use PHPUnit\Metadata\MetadataCollection;
|
|
||||||
use PHPUnit\TestRunner\TestResult\TestResult as PhpUnitTestResult;
|
use PHPUnit\TestRunner\TestResult\TestResult as PhpUnitTestResult;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -28,12 +22,15 @@ final class Converter
|
|||||||
{
|
{
|
||||||
private const PREFIX = 'P\\';
|
private const PREFIX = 'P\\';
|
||||||
|
|
||||||
|
private readonly StateGenerator $stateGenerator;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Creates a new instance of the Converter.
|
* Creates a new instance of the Converter.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly string $rootPath,
|
private readonly string $rootPath,
|
||||||
) {
|
) {
|
||||||
|
$this->stateGenerator = new StateGenerator();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -175,7 +172,7 @@ final class Converter
|
|||||||
private function toRelativePath(string $path): string
|
private function toRelativePath(string $path): string
|
||||||
{
|
{
|
||||||
// Remove cwd from the path.
|
// Remove cwd from the path.
|
||||||
return str_replace("$this->rootPath/", '', $path);
|
return str_replace("$this->rootPath".DIRECTORY_SEPARATOR, '', $path);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@ -183,83 +180,6 @@ final class Converter
|
|||||||
*/
|
*/
|
||||||
public function getStateFromResult(PhpUnitTestResult $result): State
|
public function getStateFromResult(PhpUnitTestResult $result): State
|
||||||
{
|
{
|
||||||
$state = new State();
|
return $this->stateGenerator->fromPhpUnitTestResult($result);
|
||||||
|
|
||||||
foreach ($result->testErroredEvents() as $resultEvent) {
|
|
||||||
assert($resultEvent instanceof Errored);
|
|
||||||
$state->add(TestResult::fromTestCase(
|
|
||||||
$resultEvent->test(),
|
|
||||||
TestResult::FAIL,
|
|
||||||
$resultEvent->throwable()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($result->testFailedEvents() as $resultEvent) {
|
|
||||||
$state->add(TestResult::fromTestCase(
|
|
||||||
$resultEvent->test(),
|
|
||||||
TestResult::FAIL,
|
|
||||||
$resultEvent->throwable()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($result->testMarkedIncompleteEvents() as $resultEvent) {
|
|
||||||
$state->add(TestResult::fromTestCase(
|
|
||||||
$resultEvent->test(),
|
|
||||||
TestResult::INCOMPLETE,
|
|
||||||
$resultEvent->throwable()
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($result->testConsideredRiskyEvents() as $riskyEvents) {
|
|
||||||
foreach ($riskyEvents as $riskyEvent) {
|
|
||||||
$state->add(TestResult::fromTestCase(
|
|
||||||
$riskyEvent->test(),
|
|
||||||
TestResult::RISKY,
|
|
||||||
Throwable::from(new IncompleteTestError($riskyEvent->message()))
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach ($result->testSkippedEvents() as $resultEvent) {
|
|
||||||
if ($resultEvent->message() === '__TODO__') {
|
|
||||||
$state->add(TestResult::fromTestCase($resultEvent->test(), TestResult::TODO));
|
|
||||||
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
$state->add(TestResult::fromTestCase(
|
|
||||||
$resultEvent->test(),
|
|
||||||
TestResult::SKIPPED,
|
|
||||||
Throwable::from(new SkippedWithMessageException($resultEvent->message()))
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
$numberOfPassedTests = $result->numberOfTests()
|
|
||||||
- $result->numberOfTestErroredEvents()
|
|
||||||
- $result->numberOfTestFailedEvents()
|
|
||||||
- $result->numberOfTestSkippedEvents()
|
|
||||||
- $result->numberOfTestsWithTestConsideredRiskyEvents()
|
|
||||||
- $result->numberOfTestMarkedIncompleteEvents();
|
|
||||||
|
|
||||||
for ($i = 0; $i < $numberOfPassedTests; $i++) {
|
|
||||||
$state->add(TestResult::fromTestCase(
|
|
||||||
|
|
||||||
new TestMethod(
|
|
||||||
/** @phpstan-ignore-next-line */
|
|
||||||
"$i",
|
|
||||||
/** @phpstan-ignore-next-line */
|
|
||||||
'',
|
|
||||||
'',
|
|
||||||
1,
|
|
||||||
/** @phpstan-ignore-next-line */
|
|
||||||
TestDox::fromClassNameAndMethodName('', ''),
|
|
||||||
MetadataCollection::fromArray([]),
|
|
||||||
TestDataCollection::fromArray([])
|
|
||||||
),
|
|
||||||
TestResult::PASS
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
return $state;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -211,7 +211,7 @@ final class TeamCityLogger
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
$style->writeRecap($state, $telemetry);
|
$style->writeRecap($state, $telemetry, $result);
|
||||||
}
|
}
|
||||||
|
|
||||||
public function output(ServiceMessage $message): void
|
public function output(ServiceMessage $message): void
|
||||||
|
|||||||
@ -302,6 +302,36 @@ final class Expectation
|
|||||||
return $this;
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the value has the method $name.
|
||||||
|
*
|
||||||
|
* @return self<TValue>
|
||||||
|
*/
|
||||||
|
public function toHaveMethod(string $name, string $message = ''): self
|
||||||
|
{
|
||||||
|
$this->toBeObject();
|
||||||
|
|
||||||
|
// @phpstan-ignore-next-line
|
||||||
|
Assert::assertTrue(method_exists($this->value, $name), $message);
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Asserts that the value has the provided methods $names.
|
||||||
|
*
|
||||||
|
* @param iterable<array-key, string> $names
|
||||||
|
* @return self<TValue>
|
||||||
|
*/
|
||||||
|
public function toHaveMethods(iterable $names, string $message = ''): self
|
||||||
|
{
|
||||||
|
foreach ($names as $name) {
|
||||||
|
$this->toHaveMethod($name, message: $message);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Asserts that two variables have the same value.
|
* Asserts that two variables have the same value.
|
||||||
*
|
*
|
||||||
|
|||||||
60
src/Panic.php
Normal file
60
src/Panic.php
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest;
|
||||||
|
|
||||||
|
use NunoMaduro\Collision\Writer;
|
||||||
|
use Pest\Support\Container;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Throwable;
|
||||||
|
use Whoops\Exception\Inspector;
|
||||||
|
|
||||||
|
final class Panic
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* Creates a new Panic instance.
|
||||||
|
*/
|
||||||
|
private function __construct(
|
||||||
|
private readonly Throwable $throwable
|
||||||
|
) {
|
||||||
|
// ...
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new Panic instance, and exits the application.
|
||||||
|
*/
|
||||||
|
public static function with(Throwable $throwable): never
|
||||||
|
{
|
||||||
|
$panic = new self($throwable);
|
||||||
|
|
||||||
|
$panic->handle();
|
||||||
|
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the panic.
|
||||||
|
*/
|
||||||
|
private function handle(): void
|
||||||
|
{
|
||||||
|
/** @var OutputInterface $output */
|
||||||
|
$output = Container::getInstance()->get(OutputInterface::class);
|
||||||
|
|
||||||
|
if ($this->throwable instanceof Contracts\Panicable) {
|
||||||
|
$this->throwable->render($output);
|
||||||
|
|
||||||
|
exit($this->throwable->exitCode());
|
||||||
|
}
|
||||||
|
|
||||||
|
$writer = new Writer(null, $output);
|
||||||
|
|
||||||
|
$inspector = new Inspector($this->throwable);
|
||||||
|
|
||||||
|
$output->writeln('');
|
||||||
|
$writer->write($inspector);
|
||||||
|
$output->writeln('');
|
||||||
|
|
||||||
|
exit(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
44
src/Plugins/Cache.php
Normal file
44
src/Plugins/Cache.php
Normal 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 Cache implements HandlesArguments
|
||||||
|
{
|
||||||
|
use HandleArguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The temporary folder.
|
||||||
|
*/
|
||||||
|
private const TEMPORARY_FOLDER = __DIR__
|
||||||
|
.DIRECTORY_SEPARATOR
|
||||||
|
.'..'
|
||||||
|
.DIRECTORY_SEPARATOR
|
||||||
|
.'..'
|
||||||
|
.DIRECTORY_SEPARATOR
|
||||||
|
.'.temp';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the arguments, adding the cache directory and the cache result arguments.
|
||||||
|
*/
|
||||||
|
public function handleArguments(array $arguments): array
|
||||||
|
{
|
||||||
|
if (! $this->hasArgument('--parallel', $arguments)) {
|
||||||
|
$arguments = $this->pushArgument(
|
||||||
|
sprintf('--cache-directory=%s', realpath(self::TEMPORARY_FOLDER)),
|
||||||
|
$arguments
|
||||||
|
);
|
||||||
|
|
||||||
|
$arguments = $this->pushArgument('--cache-result', $arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -55,6 +55,7 @@ final class Coverage implements AddsOutput, HandlesArguments
|
|||||||
if ($original === sprintf('--%s', $option)) {
|
if ($original === sprintf('--%s', $option)) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Str::startsWith($original, sprintf('--%s=', $option))) {
|
if (Str::startsWith($original, sprintf('--%s=', $option))) {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|||||||
190
src/Plugins/Parallel.php
Normal file
190
src/Plugins/Parallel.php
Normal file
@ -0,0 +1,190 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins;
|
||||||
|
|
||||||
|
use ParaTest\ParaTestCommand;
|
||||||
|
use Pest\Contracts\Plugins\HandlesArguments;
|
||||||
|
use Pest\Plugins\Actions\CallsAddsOutput;
|
||||||
|
use Pest\Plugins\Concerns\HandleArguments;
|
||||||
|
use Pest\Plugins\Parallel\Contracts\HandlersWorkerArguments;
|
||||||
|
use Pest\Plugins\Parallel\Paratest\CleanConsoleOutput;
|
||||||
|
use Pest\Support\Arr;
|
||||||
|
use Pest\Support\Container;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use function Pest\version;
|
||||||
|
use Symfony\Component\Console\Application;
|
||||||
|
use Symfony\Component\Console\Command\Command;
|
||||||
|
use Symfony\Component\Console\Input\ArgvInput;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
final class Parallel implements HandlesArguments
|
||||||
|
{
|
||||||
|
use HandleArguments;
|
||||||
|
|
||||||
|
private const HANDLERS = [
|
||||||
|
Parallel\Handlers\Parallel::class,
|
||||||
|
Parallel\Handlers\Pest::class,
|
||||||
|
Parallel\Handlers\Laravel::class,
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var string[]
|
||||||
|
*/
|
||||||
|
private const UNSUPPORTED_ARGUMENTS = ['--todo', '--retry'];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the given command line arguments indicate that the test suite should be run in parallel.
|
||||||
|
*/
|
||||||
|
public static function isEnabled(): bool
|
||||||
|
{
|
||||||
|
$argv = new ArgvInput();
|
||||||
|
if ($argv->hasParameterOption('--parallel')) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $argv->hasParameterOption('-p');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* If this code is running in a worker process rather than the main process.
|
||||||
|
*/
|
||||||
|
public static function isWorker(): bool
|
||||||
|
{
|
||||||
|
$argvValue = Arr::get($_SERVER, 'PARATEST');
|
||||||
|
|
||||||
|
assert(is_string($argvValue) || is_int($argvValue) || is_null($argvValue));
|
||||||
|
|
||||||
|
return ((int) $argvValue) === 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function handleArguments(array $arguments): array
|
||||||
|
{
|
||||||
|
if ($this->hasArgumentsThatWouldBeFasterWithoutParallel()) {
|
||||||
|
return $this->runTestSuiteInSeries($arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isEnabled()) {
|
||||||
|
exit($this->runTestSuiteInParallel($arguments));
|
||||||
|
}
|
||||||
|
|
||||||
|
if (self::isWorker()) {
|
||||||
|
return $this->runWorkerHandlers($arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the test suite in parallel. This method will exit the process upon completion.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
*/
|
||||||
|
private function runTestSuiteInParallel(array $arguments): int
|
||||||
|
{
|
||||||
|
if (! class_exists(ParaTestCommand::class)) {
|
||||||
|
$this->askUserToInstallParatest();
|
||||||
|
|
||||||
|
return Command::FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
$handlers = array_filter(
|
||||||
|
array_map(fn ($handler): object|string => Container::getInstance()->get($handler), self::HANDLERS),
|
||||||
|
fn ($handler): bool => $handler instanceof HandlesArguments,
|
||||||
|
);
|
||||||
|
|
||||||
|
$filteredArguments = array_reduce(
|
||||||
|
$handlers,
|
||||||
|
fn ($arguments, HandlesArguments $handler): array => $handler->handleArguments($arguments),
|
||||||
|
$arguments
|
||||||
|
);
|
||||||
|
|
||||||
|
$exitCode = $this->paratestCommand()->run(new ArgvInput($filteredArguments), new CleanConsoleOutput());
|
||||||
|
|
||||||
|
return (new CallsAddsOutput())($exitCode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs any handlers that have been registered to handle worker arguments, and returns the modified arguments.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function runWorkerHandlers(array $arguments): array
|
||||||
|
{
|
||||||
|
$handlers = array_filter(
|
||||||
|
array_map(fn ($handler): object|string => Container::getInstance()->get($handler), self::HANDLERS),
|
||||||
|
fn ($handler): bool => $handler instanceof HandlersWorkerArguments,
|
||||||
|
);
|
||||||
|
|
||||||
|
return array_reduce(
|
||||||
|
$handlers,
|
||||||
|
fn ($arguments, HandlersWorkerArguments $handler): array => $handler->handleWorkerArguments($arguments),
|
||||||
|
$arguments
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs a message to the user asking them to install ParaTest as a dev dependency.
|
||||||
|
*/
|
||||||
|
private function askUserToInstallParatest(): void
|
||||||
|
{
|
||||||
|
/** @var OutputInterface $output */
|
||||||
|
$output = Container::getInstance()->get(OutputInterface::class);
|
||||||
|
|
||||||
|
$output->writeln([
|
||||||
|
'<fg=red>Pest Parallel requires ParaTest to run.</>',
|
||||||
|
'Please run <fg=yellow>composer require --dev brianium/paratest</>.',
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Builds an instance of the Paratest command.
|
||||||
|
*/
|
||||||
|
private function paratestCommand(): Application
|
||||||
|
{
|
||||||
|
/** @var non-empty-string $rootPath */
|
||||||
|
$rootPath = TestSuite::getInstance()->rootPath;
|
||||||
|
|
||||||
|
$command = ParaTestCommand::applicationFactory($rootPath);
|
||||||
|
$command->setAutoExit(false);
|
||||||
|
$command->setName('Pest');
|
||||||
|
$command->setVersion(version());
|
||||||
|
|
||||||
|
return $command;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the command line arguments contain any arguments that are
|
||||||
|
* not supported or are suboptimal when running in parallel.
|
||||||
|
*/
|
||||||
|
private function hasArgumentsThatWouldBeFasterWithoutParallel(): bool
|
||||||
|
{
|
||||||
|
$arguments = new ArgvInput();
|
||||||
|
|
||||||
|
foreach (self::UNSUPPORTED_ARGUMENTS as $unsupportedArgument) {
|
||||||
|
if ($arguments->hasParameterOption($unsupportedArgument)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes any parallel arguments.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function runTestSuiteInSeries(array $arguments): array
|
||||||
|
{
|
||||||
|
$arguments = $this->popArgument('--parallel', $arguments);
|
||||||
|
|
||||||
|
return $this->popArgument('-p', $arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
14
src/Plugins/Parallel/Contracts/HandlersWorkerArguments.php
Normal file
14
src/Plugins/Parallel/Contracts/HandlersWorkerArguments.php
Normal file
@ -0,0 +1,14 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Parallel\Contracts;
|
||||||
|
|
||||||
|
interface HandlersWorkerArguments
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
public function handleWorkerArguments(array $arguments): array;
|
||||||
|
}
|
||||||
106
src/Plugins/Parallel/Handlers/Laravel.php
Normal file
106
src/Plugins/Parallel/Handlers/Laravel.php
Normal file
@ -0,0 +1,106 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Parallel\Handlers;
|
||||||
|
|
||||||
|
use Closure;
|
||||||
|
use Composer\InstalledVersions;
|
||||||
|
use Illuminate\Testing\ParallelRunner;
|
||||||
|
use ParaTest\Options;
|
||||||
|
use ParaTest\RunnerInterface;
|
||||||
|
use Pest\Contracts\Plugins\HandlesArguments;
|
||||||
|
use Pest\Plugins\Concerns\HandleArguments;
|
||||||
|
use Pest\Plugins\Parallel\Paratest\WrapperRunner;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Laravel implements HandlesArguments
|
||||||
|
{
|
||||||
|
use HandleArguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
public function handleArguments(array $arguments): array
|
||||||
|
{
|
||||||
|
return self::whenUsingLaravel($arguments, function (array $arguments): array {
|
||||||
|
$this->ensureRunnerIsResolvable();
|
||||||
|
|
||||||
|
$arguments = $this->ensureEnvironmentVariables($arguments);
|
||||||
|
|
||||||
|
return $this->ensureRunner($arguments);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Executes the given closure when running Laravel.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
* @param CLosure(array<int, string>): array<int, string> $closure
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private static function whenUsingLaravel(array $arguments, Closure $closure): array
|
||||||
|
{
|
||||||
|
$isLaravelApplication = InstalledVersions::isInstalled('laravel/framework', false);
|
||||||
|
$isLaravelPackage = class_exists(\Orchestra\Testbench\TestCase::class);
|
||||||
|
|
||||||
|
if ($isLaravelApplication && ! $isLaravelPackage) {
|
||||||
|
return $closure($arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $arguments;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the runner is resolvable.
|
||||||
|
*/
|
||||||
|
private function ensureRunnerIsResolvable(): void
|
||||||
|
{
|
||||||
|
ParallelRunner::resolveRunnerUsing( // @phpstan-ignore-line
|
||||||
|
fn (Options $options, OutputInterface $output): RunnerInterface => new WrapperRunner($options, $output)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensures the environment variables are set.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function ensureEnvironmentVariables(array $arguments): array
|
||||||
|
{
|
||||||
|
$_ENV['LARAVEL_PARALLEL_TESTING'] = 1;
|
||||||
|
|
||||||
|
if ($this->hasArgument('--recreate-databases', $arguments)) {
|
||||||
|
$_ENV['LARAVEL_PARALLEL_TESTING_RECREATE_DATABASES'] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->hasArgument('--drop-databases', $arguments)) {
|
||||||
|
$_ENV['LARAVEL_PARALLEL_TESTING_DROP_DATABASES'] = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
$arguments = $this->popArgument('--recreate-databases', $arguments);
|
||||||
|
|
||||||
|
return $this->popArgument('--drop-databases', $arguments);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Ensure the runner is set.
|
||||||
|
*
|
||||||
|
* @param array<int, string> $arguments
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function ensureRunner(array $arguments): array
|
||||||
|
{
|
||||||
|
foreach ($arguments as $value) {
|
||||||
|
if (str_starts_with($value, '--runner')) {
|
||||||
|
$arguments = $this->popArgument($value, $arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->pushArgument('--runner=\Illuminate\Testing\ParallelRunner', $arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
37
src/Plugins/Parallel/Handlers/Parallel.php
Normal file
37
src/Plugins/Parallel/Handlers/Parallel.php
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Parallel\Handlers;
|
||||||
|
|
||||||
|
use Pest\Contracts\Plugins\HandlesArguments;
|
||||||
|
use Pest\Plugins\Concerns\HandleArguments;
|
||||||
|
use Pest\Plugins\Parallel\Paratest\WrapperRunner;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class Parallel implements HandlesArguments
|
||||||
|
{
|
||||||
|
use HandleArguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of arguments to remove.
|
||||||
|
*/
|
||||||
|
private const ARGS_TO_REMOVE = [
|
||||||
|
'--parallel',
|
||||||
|
'-p',
|
||||||
|
'--no-output',
|
||||||
|
'--cache-result',
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the arguments, removing the ones that are not needed, and adds the "runner" argument.
|
||||||
|
*/
|
||||||
|
public function handleArguments(array $arguments): array
|
||||||
|
{
|
||||||
|
$args = array_reduce(self::ARGS_TO_REMOVE, fn ($args, $arg): array => $this->popArgument($arg, $args), $arguments);
|
||||||
|
|
||||||
|
return $this->pushArgument('--runner='.WrapperRunner::class, $args);
|
||||||
|
}
|
||||||
|
}
|
||||||
23
src/Plugins/Parallel/Handlers/Pest.php
Normal file
23
src/Plugins/Parallel/Handlers/Pest.php
Normal file
@ -0,0 +1,23 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Parallel\Handlers;
|
||||||
|
|
||||||
|
use Pest\Plugins\Concerns\HandleArguments;
|
||||||
|
use Pest\Plugins\Parallel\Contracts\HandlersWorkerArguments;
|
||||||
|
|
||||||
|
final class Pest implements HandlersWorkerArguments
|
||||||
|
{
|
||||||
|
use HandleArguments;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Handles the arguments, adding the "PEST_PARALLEL" environment variable to the global $_SERVER.
|
||||||
|
*/
|
||||||
|
public function handleWorkerArguments(array $arguments): array
|
||||||
|
{
|
||||||
|
$_SERVER['PEST_PARALLEL'] = '1';
|
||||||
|
|
||||||
|
return $arguments;
|
||||||
|
}
|
||||||
|
}
|
||||||
30
src/Plugins/Parallel/Paratest/CleanConsoleOutput.php
Normal file
30
src/Plugins/Parallel/Paratest/CleanConsoleOutput.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Parallel\Paratest;
|
||||||
|
|
||||||
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
|
|
||||||
|
final class CleanConsoleOutput extends ConsoleOutput
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* {@inheritdoc}
|
||||||
|
*/
|
||||||
|
protected function doWrite(string $message, bool $newline): void
|
||||||
|
{
|
||||||
|
if ($this->isOpeningHeadline($message)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
parent::doWrite($message, $newline);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the opening headline, witch is not needed.
|
||||||
|
*/
|
||||||
|
private function isOpeningHeadline(string $message): bool
|
||||||
|
{
|
||||||
|
return str_contains($message, 'by Sebastian Bergmann and contributors.');
|
||||||
|
}
|
||||||
|
}
|
||||||
203
src/Plugins/Parallel/Paratest/ResultPrinter.php
Normal file
203
src/Plugins/Parallel/Paratest/ResultPrinter.php
Normal file
@ -0,0 +1,203 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Parallel\Paratest;
|
||||||
|
|
||||||
|
use function assert;
|
||||||
|
use function fclose;
|
||||||
|
use function feof;
|
||||||
|
use function fopen;
|
||||||
|
use function fread;
|
||||||
|
use function fseek;
|
||||||
|
use function ftell;
|
||||||
|
use function fwrite;
|
||||||
|
use ParaTest\Options;
|
||||||
|
use Pest\Plugins\Parallel\Support\CompactPrinter;
|
||||||
|
use Pest\Support\StateGenerator;
|
||||||
|
use PHPUnit\TestRunner\TestResult\TestResult;
|
||||||
|
use PHPUnit\TextUI\Output\Printer;
|
||||||
|
use function preg_replace;
|
||||||
|
use SebastianBergmann\Timer\Duration;
|
||||||
|
use SplFileInfo;
|
||||||
|
use function sprintf;
|
||||||
|
use function strlen;
|
||||||
|
use Symfony\Component\Console\Formatter\OutputFormatter;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
|
||||||
|
/** @internal */
|
||||||
|
final class ResultPrinter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The "native" printer.
|
||||||
|
*/
|
||||||
|
public readonly Printer $printer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The "compact" printer.
|
||||||
|
*/
|
||||||
|
private readonly CompactPrinter $compactPrinter;
|
||||||
|
|
||||||
|
/** @var resource|null */
|
||||||
|
private $teamcityLogFileHandle;
|
||||||
|
|
||||||
|
/** @var array<string, int> */
|
||||||
|
private array $tailPositions;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly OutputInterface $output,
|
||||||
|
private readonly Options $options
|
||||||
|
) {
|
||||||
|
$this->printer = new class($this->output) implements Printer
|
||||||
|
{
|
||||||
|
public function __construct(
|
||||||
|
private readonly OutputInterface $output,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
public function print(string $buffer): void
|
||||||
|
{
|
||||||
|
$this->output->write(OutputFormatter::escape($buffer));
|
||||||
|
}
|
||||||
|
|
||||||
|
public function flush(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
$this->compactPrinter = CompactPrinter::default();
|
||||||
|
|
||||||
|
if (! $this->options->configuration->hasLogfileTeamcity()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$teamcityLogFileHandle = fopen($this->options->configuration->logfileTeamcity(), 'ab+');
|
||||||
|
assert($teamcityLogFileHandle !== false);
|
||||||
|
$this->teamcityLogFileHandle = $teamcityLogFileHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
public function start(int $numberOfTests): void
|
||||||
|
{
|
||||||
|
$this->compactPrinter->line(sprintf(
|
||||||
|
'Running %d test%s using %d process%s',
|
||||||
|
$numberOfTests,
|
||||||
|
$numberOfTests === 1 ? '' : 's',
|
||||||
|
$this->options->processes,
|
||||||
|
$this->options->processes === 1 ? '' : 'es')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, SplFileInfo> $teamcityFiles */
|
||||||
|
public function printFeedback(SplFileInfo $progressFile, array $teamcityFiles): void
|
||||||
|
{
|
||||||
|
if ($this->options->needsTeamcity) {
|
||||||
|
$teamcityProgress = $this->tailMultiple($teamcityFiles);
|
||||||
|
|
||||||
|
if ($this->teamcityLogFileHandle !== null) {
|
||||||
|
fwrite($this->teamcityLogFileHandle, $teamcityProgress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->options->configuration->outputIsTeamCity()) {
|
||||||
|
assert(isset($teamcityProgress));
|
||||||
|
$this->output->write($teamcityProgress);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->options->configuration->noProgress()) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$feedbackItems = $this->tail($progressFile);
|
||||||
|
if ($feedbackItems === '') {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$feedbackItems = (string) preg_replace('/ +\\d+ \\/ \\d+ \\( ?\\d+%\\)\\s*/', '', $feedbackItems);
|
||||||
|
|
||||||
|
$actualTestCount = strlen($feedbackItems);
|
||||||
|
for ($index = 0; $index < $actualTestCount; $index++) {
|
||||||
|
$this->printFeedbackItem($feedbackItems[$index]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @param array<int, SplFileInfo> $teamcityFiles
|
||||||
|
* @param array<int, SplFileInfo> $testdoxFiles
|
||||||
|
*/
|
||||||
|
public function printResults(TestResult $testResult, array $teamcityFiles, array $testdoxFiles, Duration $duration): void
|
||||||
|
{
|
||||||
|
if ($this->options->needsTeamcity) {
|
||||||
|
$teamcityProgress = $this->tailMultiple($teamcityFiles);
|
||||||
|
|
||||||
|
if ($this->teamcityLogFileHandle !== null) {
|
||||||
|
fwrite($this->teamcityLogFileHandle, $teamcityProgress);
|
||||||
|
$resource = $this->teamcityLogFileHandle;
|
||||||
|
$this->teamcityLogFileHandle = null;
|
||||||
|
fclose($resource);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->options->configuration->outputIsTeamCity()) {
|
||||||
|
assert(isset($teamcityProgress));
|
||||||
|
$this->output->write($teamcityProgress);
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($this->options->configuration->outputIsTestDox()) {
|
||||||
|
$this->output->write($this->tailMultiple($testdoxFiles));
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$state = (new StateGenerator())->fromPhpUnitTestResult($testResult);
|
||||||
|
|
||||||
|
$this->compactPrinter->errors($state);
|
||||||
|
$this->compactPrinter->recap($state, $testResult, $duration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function printFeedbackItem(string $item): void
|
||||||
|
{
|
||||||
|
$this->compactPrinter->descriptionItem($item);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, SplFileInfo> $files */
|
||||||
|
private function tailMultiple(array $files): string
|
||||||
|
{
|
||||||
|
$content = '';
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (! $file->isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$content .= $this->tail($file);
|
||||||
|
}
|
||||||
|
|
||||||
|
return $content;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function tail(SplFileInfo $file): string
|
||||||
|
{
|
||||||
|
$path = $file->getPathname();
|
||||||
|
$handle = fopen($path, 'r');
|
||||||
|
assert($handle !== false);
|
||||||
|
$fseek = fseek($handle, $this->tailPositions[$path] ?? 0);
|
||||||
|
assert($fseek === 0);
|
||||||
|
|
||||||
|
$contents = '';
|
||||||
|
while (! feof($handle)) {
|
||||||
|
$fread = fread($handle, 8192);
|
||||||
|
assert($fread !== false);
|
||||||
|
$contents .= $fread;
|
||||||
|
}
|
||||||
|
|
||||||
|
$ftell = ftell($handle);
|
||||||
|
assert($ftell !== false);
|
||||||
|
$this->tailPositions[$path] = $ftell;
|
||||||
|
fclose($handle);
|
||||||
|
|
||||||
|
return $contents;
|
||||||
|
}
|
||||||
|
}
|
||||||
402
src/Plugins/Parallel/Paratest/WrapperRunner.php
Normal file
402
src/Plugins/Parallel/Paratest/WrapperRunner.php
Normal file
@ -0,0 +1,402 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Parallel\Paratest;
|
||||||
|
|
||||||
|
use function array_merge;
|
||||||
|
use function array_merge_recursive;
|
||||||
|
use function array_shift;
|
||||||
|
use function assert;
|
||||||
|
use function count;
|
||||||
|
use const DIRECTORY_SEPARATOR;
|
||||||
|
use function dirname;
|
||||||
|
use function file_get_contents;
|
||||||
|
use function max;
|
||||||
|
use ParaTest\Coverage\CoverageMerger;
|
||||||
|
use ParaTest\JUnit\LogMerger;
|
||||||
|
use ParaTest\JUnit\Writer;
|
||||||
|
use ParaTest\Options;
|
||||||
|
use ParaTest\RunnerInterface;
|
||||||
|
use ParaTest\WrapperRunner\SuiteLoader;
|
||||||
|
use ParaTest\WrapperRunner\WrapperWorker;
|
||||||
|
use Pest\TestSuite;
|
||||||
|
use PHPUnit\Event\Facade as EventFacade;
|
||||||
|
use PHPUnit\Runner\CodeCoverage;
|
||||||
|
use PHPUnit\TestRunner\TestResult\Facade as TestResultFacade;
|
||||||
|
use PHPUnit\TestRunner\TestResult\TestResult;
|
||||||
|
use PHPUnit\TextUI\Configuration\CodeCoverageFilterRegistry;
|
||||||
|
use PHPUnit\TextUI\ShellExitCodeCalculator;
|
||||||
|
use PHPUnit\Util\ExcludeList;
|
||||||
|
use function realpath;
|
||||||
|
use SebastianBergmann\Timer\Timer;
|
||||||
|
use SplFileInfo;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use Symfony\Component\Process\PhpExecutableFinder;
|
||||||
|
use function unlink;
|
||||||
|
use function unserialize;
|
||||||
|
use function usleep;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class WrapperRunner implements RunnerInterface
|
||||||
|
{
|
||||||
|
private const CYCLE_SLEEP = 10000;
|
||||||
|
|
||||||
|
private readonly ResultPrinter $printer;
|
||||||
|
|
||||||
|
private readonly Timer $timer;
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
private array $pending = [];
|
||||||
|
|
||||||
|
private int $exitCode = -1;
|
||||||
|
|
||||||
|
/** @var array<int,WrapperWorker> */
|
||||||
|
private array $workers = [];
|
||||||
|
|
||||||
|
/** @var array<int,int> */
|
||||||
|
private array $batches = [];
|
||||||
|
|
||||||
|
/** @var array<int, SplFileInfo> */
|
||||||
|
private array $testresultFiles = [];
|
||||||
|
|
||||||
|
/** @var array<int, SplFileInfo> */
|
||||||
|
private array $coverageFiles = [];
|
||||||
|
|
||||||
|
/** @var array<int, SplFileInfo> */
|
||||||
|
private array $junitFiles = [];
|
||||||
|
|
||||||
|
/** @var array<int, SplFileInfo> */
|
||||||
|
private array $teamcityFiles = [];
|
||||||
|
|
||||||
|
/** @var array<int, SplFileInfo> */
|
||||||
|
private array $testdoxFiles = [];
|
||||||
|
|
||||||
|
/** @var array<int, string> */
|
||||||
|
private readonly array $parameters;
|
||||||
|
|
||||||
|
private readonly CodeCoverageFilterRegistry $codeCoverageFilterRegistry;
|
||||||
|
|
||||||
|
public function __construct(
|
||||||
|
private readonly Options $options,
|
||||||
|
private readonly OutputInterface $output
|
||||||
|
) {
|
||||||
|
$this->printer = new ResultPrinter($output, $options);
|
||||||
|
$this->timer = new Timer();
|
||||||
|
|
||||||
|
$worker = realpath(
|
||||||
|
dirname(__DIR__, 4).DIRECTORY_SEPARATOR.'bin'.DIRECTORY_SEPARATOR.'worker.php',
|
||||||
|
);
|
||||||
|
|
||||||
|
assert($worker !== false);
|
||||||
|
$phpFinder = new PhpExecutableFinder();
|
||||||
|
$phpBin = $phpFinder->find(false);
|
||||||
|
assert($phpBin !== false);
|
||||||
|
$parameters = [$phpBin];
|
||||||
|
$parameters = array_merge($parameters, $phpFinder->findArguments());
|
||||||
|
|
||||||
|
if ($options->passthruPhp !== null) {
|
||||||
|
$parameters = array_merge($parameters, $options->passthruPhp);
|
||||||
|
}
|
||||||
|
|
||||||
|
$parameters[] = $worker;
|
||||||
|
|
||||||
|
$this->parameters = $parameters;
|
||||||
|
$this->codeCoverageFilterRegistry = new CodeCoverageFilterRegistry();
|
||||||
|
}
|
||||||
|
|
||||||
|
public function run(): int
|
||||||
|
{
|
||||||
|
$directory = dirname(__DIR__);
|
||||||
|
assert(strlen($directory) > 0);
|
||||||
|
ExcludeList::addDirectory($directory);
|
||||||
|
|
||||||
|
TestResultFacade::init();
|
||||||
|
EventFacade::seal();
|
||||||
|
|
||||||
|
$suiteLoader = new SuiteLoader($this->options, $this->output, $this->codeCoverageFilterRegistry);
|
||||||
|
$this->pending = $this->getTestFiles($suiteLoader);
|
||||||
|
|
||||||
|
$result = TestResultFacade::result();
|
||||||
|
|
||||||
|
$this->printer->start($suiteLoader->testCount);
|
||||||
|
|
||||||
|
$this->timer->start();
|
||||||
|
|
||||||
|
$this->startWorkers();
|
||||||
|
$this->assignAllPendingTests();
|
||||||
|
$this->waitForAllToFinish();
|
||||||
|
|
||||||
|
return $this->complete($result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function startWorkers(): void
|
||||||
|
{
|
||||||
|
for ($token = 1; $token <= $this->options->processes; $token++) {
|
||||||
|
$this->startWorker($token);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function assignAllPendingTests(): void
|
||||||
|
{
|
||||||
|
$batchSize = $this->options->maxBatchSize;
|
||||||
|
|
||||||
|
while ($this->pending !== [] && $this->workers !== []) {
|
||||||
|
foreach ($this->workers as $token => $worker) {
|
||||||
|
if (! $worker->isRunning()) {
|
||||||
|
throw $worker->getWorkerCrashedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $worker->isFree()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->flushWorker($worker);
|
||||||
|
|
||||||
|
if ($batchSize !== 0 && $this->batches[$token] === $batchSize) {
|
||||||
|
$this->destroyWorker($token);
|
||||||
|
$worker = $this->startWorker($token);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (
|
||||||
|
$this->exitCode > 0
|
||||||
|
&& $this->options->configuration->stopOnFailure()
|
||||||
|
) {
|
||||||
|
$this->pending = [];
|
||||||
|
} elseif (($pending = array_shift($this->pending)) !== null) {
|
||||||
|
$this->debug(sprintf('Assigning %s to worker %d', $pending, $token));
|
||||||
|
|
||||||
|
$worker->assign($pending);
|
||||||
|
$this->batches[$token]++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep(self::CYCLE_SLEEP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function flushWorker(WrapperWorker $worker): void
|
||||||
|
{
|
||||||
|
$this->exitCode = max($this->exitCode, $worker->getExitCode());
|
||||||
|
$this->printer->printFeedback(
|
||||||
|
$worker->progressFile,
|
||||||
|
$this->teamcityFiles,
|
||||||
|
);
|
||||||
|
$worker->reset();
|
||||||
|
}
|
||||||
|
|
||||||
|
private function waitForAllToFinish(): void
|
||||||
|
{
|
||||||
|
$stopped = [];
|
||||||
|
while ($this->workers !== []) {
|
||||||
|
foreach ($this->workers as $index => $worker) {
|
||||||
|
if ($worker->isRunning()) {
|
||||||
|
if (! array_key_exists($index, $stopped) && $worker->isFree()) {
|
||||||
|
$worker->stop();
|
||||||
|
$stopped[$index] = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (! $worker->isFree()) {
|
||||||
|
throw $worker->getWorkerCrashedException();
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->flushWorker($worker);
|
||||||
|
unset($this->workers[$index]);
|
||||||
|
}
|
||||||
|
|
||||||
|
usleep(self::CYCLE_SLEEP);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private function startWorker(int $token): WrapperWorker
|
||||||
|
{
|
||||||
|
/** @var array<non-empty-string> $parameters */
|
||||||
|
$parameters = $this->parameters;
|
||||||
|
|
||||||
|
$worker = new WrapperWorker(
|
||||||
|
$this->output,
|
||||||
|
$this->options,
|
||||||
|
$parameters,
|
||||||
|
$token,
|
||||||
|
);
|
||||||
|
|
||||||
|
$worker->start();
|
||||||
|
|
||||||
|
$this->batches[$token] = 0;
|
||||||
|
|
||||||
|
$this->testresultFiles[] = $worker->testresultFile;
|
||||||
|
|
||||||
|
if (isset($worker->junitFile)) {
|
||||||
|
$this->junitFiles[] = $worker->junitFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($worker->coverageFile)) {
|
||||||
|
$this->coverageFiles[] = $worker->coverageFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($worker->teamcityFile)) {
|
||||||
|
$this->teamcityFiles[] = $worker->teamcityFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (isset($worker->testdoxFile)) {
|
||||||
|
$this->testdoxFiles[] = $worker->testdoxFile;
|
||||||
|
}
|
||||||
|
|
||||||
|
return $this->workers[$token] = $worker;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function destroyWorker(int $token): void
|
||||||
|
{
|
||||||
|
// Mutation Testing tells us that the following `unset()` already destroys
|
||||||
|
// the `WrapperWorker`, which destroys the Symfony's `Process`, which
|
||||||
|
// automatically calls `Process::stop` within `Process::__destruct()`.
|
||||||
|
// But we prefer to have an explicit stops.
|
||||||
|
$this->workers[$token]->stop();
|
||||||
|
|
||||||
|
unset($this->workers[$token]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function complete(TestResult $testResultSum): int
|
||||||
|
{
|
||||||
|
foreach ($this->testresultFiles as $testresultFile) {
|
||||||
|
if (! $testresultFile->isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$contents = file_get_contents($testresultFile->getPathname());
|
||||||
|
assert($contents !== false);
|
||||||
|
$testResult = unserialize($contents);
|
||||||
|
assert($testResult instanceof TestResult);
|
||||||
|
|
||||||
|
$testResultSum = new TestResult(
|
||||||
|
$testResultSum->numberOfTests() + $testResult->numberOfTests(),
|
||||||
|
$testResultSum->numberOfTestsRun() + $testResult->numberOfTestsRun(),
|
||||||
|
$testResultSum->numberOfAssertions() + $testResult->numberOfAssertions(),
|
||||||
|
array_merge_recursive($testResultSum->testErroredEvents(), $testResult->testErroredEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testFailedEvents(), $testResult->testFailedEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testConsideredRiskyEvents(), $testResult->testConsideredRiskyEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testSuiteSkippedEvents(), $testResult->testSuiteSkippedEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testSkippedEvents(), $testResult->testSkippedEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testMarkedIncompleteEvents(), $testResult->testMarkedIncompleteEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testTriggeredDeprecationEvents(), $testResult->testTriggeredDeprecationEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testTriggeredPhpDeprecationEvents(), $testResult->testTriggeredPhpDeprecationEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testTriggeredPhpunitDeprecationEvents(), $testResult->testTriggeredPhpunitDeprecationEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testTriggeredErrorEvents(), $testResult->testTriggeredErrorEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testTriggeredNoticeEvents(), $testResult->testTriggeredNoticeEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testTriggeredPhpNoticeEvents(), $testResult->testTriggeredPhpNoticeEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testTriggeredWarningEvents(), $testResult->testTriggeredWarningEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testTriggeredPhpWarningEvents(), $testResult->testTriggeredPhpWarningEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testTriggeredPhpunitErrorEvents(), $testResult->testTriggeredPhpunitErrorEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testTriggeredPhpunitWarningEvents(), $testResult->testTriggeredPhpunitWarningEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testRunnerTriggeredDeprecationEvents(), $testResult->testRunnerTriggeredDeprecationEvents()),
|
||||||
|
array_merge_recursive($testResultSum->testRunnerTriggeredWarningEvents(), $testResult->testRunnerTriggeredWarningEvents()),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->printer->printResults(
|
||||||
|
$testResultSum,
|
||||||
|
$this->teamcityFiles,
|
||||||
|
$this->testdoxFiles,
|
||||||
|
$this->timer->stop(),
|
||||||
|
);
|
||||||
|
$this->generateCodeCoverageReports();
|
||||||
|
$this->generateLogs();
|
||||||
|
|
||||||
|
$exitcode = (new ShellExitCodeCalculator())->calculate(
|
||||||
|
$this->options->configuration->failOnEmptyTestSuite(),
|
||||||
|
$this->options->configuration->failOnRisky(),
|
||||||
|
$this->options->configuration->failOnWarning(),
|
||||||
|
$this->options->configuration->failOnIncomplete(),
|
||||||
|
$this->options->configuration->failOnSkipped(),
|
||||||
|
$testResultSum,
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->clearFiles($this->testresultFiles);
|
||||||
|
$this->clearFiles($this->coverageFiles);
|
||||||
|
$this->clearFiles($this->junitFiles);
|
||||||
|
$this->clearFiles($this->teamcityFiles);
|
||||||
|
$this->clearFiles($this->testdoxFiles);
|
||||||
|
|
||||||
|
return $exitcode;
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateCodeCoverageReports(): void
|
||||||
|
{
|
||||||
|
if ($this->coverageFiles === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$coverageManager = new CodeCoverage();
|
||||||
|
$coverageManager->init($this->options->configuration, $this->codeCoverageFilterRegistry);
|
||||||
|
$coverageMerger = new CoverageMerger($coverageManager->codeCoverage());
|
||||||
|
foreach ($this->coverageFiles as $coverageFile) {
|
||||||
|
$coverageMerger->addCoverageFromFile($coverageFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
$coverageManager->generateReports(
|
||||||
|
$this->printer->printer,
|
||||||
|
$this->options->configuration,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
private function generateLogs(): void
|
||||||
|
{
|
||||||
|
if ($this->junitFiles === []) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
$testSuite = (new LogMerger())->merge($this->junitFiles);
|
||||||
|
(new Writer())->write(
|
||||||
|
$testSuite,
|
||||||
|
$this->options->configuration->logfileJunit(),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/** @param array<int, SplFileInfo> $files */
|
||||||
|
private function clearFiles(array $files): void
|
||||||
|
{
|
||||||
|
foreach ($files as $file) {
|
||||||
|
if (! $file->isFile()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
unlink($file->getPathname());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns the test files to be executed.
|
||||||
|
*
|
||||||
|
* @return array<int, string>
|
||||||
|
*/
|
||||||
|
private function getTestFiles(SuiteLoader $suiteLoader): array
|
||||||
|
{
|
||||||
|
$this->debug(sprintf('Found %d test file%s', count($suiteLoader->files), count($suiteLoader->files) === 1 ? '' : 's'));
|
||||||
|
|
||||||
|
/** @var array<string, string> $files */
|
||||||
|
$files = $suiteLoader->files;
|
||||||
|
|
||||||
|
return [
|
||||||
|
...array_values(array_filter(
|
||||||
|
$files,
|
||||||
|
fn (string $filename): bool => ! str_ends_with($filename, "eval()'d code")
|
||||||
|
)),
|
||||||
|
...TestSuite::getInstance()->tests->getFilenames(),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prints a debug message.
|
||||||
|
*/
|
||||||
|
private function debug(string $message): void
|
||||||
|
{
|
||||||
|
if ($this->options->verbose) {
|
||||||
|
$this->output->writeln(" <fg=blue>{$message}</> ");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
143
src/Plugins/Parallel/Support/CompactPrinter.php
Normal file
143
src/Plugins/Parallel/Support/CompactPrinter.php
Normal file
@ -0,0 +1,143 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Plugins\Parallel\Support;
|
||||||
|
|
||||||
|
use NunoMaduro\Collision\Adapters\Phpunit\State;
|
||||||
|
use NunoMaduro\Collision\Adapters\Phpunit\Style;
|
||||||
|
use PHPUnit\Event\Telemetry\HRTime;
|
||||||
|
use PHPUnit\Event\Telemetry\Info;
|
||||||
|
use PHPUnit\Event\Telemetry\MemoryUsage;
|
||||||
|
use PHPUnit\Event\Telemetry\Snapshot;
|
||||||
|
use PHPUnit\TestRunner\TestResult\TestResult as PHPUnitTestResult;
|
||||||
|
use SebastianBergmann\Timer\Duration;
|
||||||
|
use Symfony\Component\Console\Output\ConsoleOutput;
|
||||||
|
use Symfony\Component\Console\Output\OutputInterface;
|
||||||
|
use function Termwind\render;
|
||||||
|
use Termwind\Terminal;
|
||||||
|
use function Termwind\terminal;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @internal
|
||||||
|
*/
|
||||||
|
final class CompactPrinter
|
||||||
|
{
|
||||||
|
/**
|
||||||
|
* The number of processed tests.
|
||||||
|
*/
|
||||||
|
private int $processed = 0;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @var array<string, array<int, string>>
|
||||||
|
*/
|
||||||
|
private const LOOKUP_TABLE = [
|
||||||
|
'.' => ['gray', '.'],
|
||||||
|
'S' => ['yellow', 's'],
|
||||||
|
'T' => ['cyan', 't'],
|
||||||
|
'I' => ['yellow', 'i'],
|
||||||
|
'N' => ['yellow', 'i'],
|
||||||
|
'R' => ['yellow', '!'],
|
||||||
|
'W' => ['yellow', '!'],
|
||||||
|
'E' => ['red', '⨯'],
|
||||||
|
'F' => ['red', '⨯'],
|
||||||
|
];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of the Compact Printer.
|
||||||
|
*/
|
||||||
|
public function __construct(
|
||||||
|
private readonly Terminal $terminal,
|
||||||
|
private readonly OutputInterface $output,
|
||||||
|
private readonly Style $style,
|
||||||
|
private readonly int $compactSymbolsPerLine,
|
||||||
|
) {
|
||||||
|
// ..
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new instance of the Compact Printer.
|
||||||
|
*/
|
||||||
|
public static function default(): self
|
||||||
|
{
|
||||||
|
return new self(
|
||||||
|
terminal(),
|
||||||
|
new ConsoleOutput(decorated: true),
|
||||||
|
new Style(new ConsoleOutput(decorated: true)),
|
||||||
|
terminal()->width() - 4,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output an empty line in the console. Useful for providing a little breathing room.
|
||||||
|
*/
|
||||||
|
public function newLine(): void
|
||||||
|
{
|
||||||
|
render('<div class="py-1"></div>');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the given message to the console, adding vertical and horizontal padding.
|
||||||
|
*/
|
||||||
|
public function line(string $message): void
|
||||||
|
{
|
||||||
|
render("<span class='mx-2 py-1 text-gray-700'>{$message}</span>");
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs the given description item from the ProgressPrinter as a gorgeous, colored symbol.
|
||||||
|
*/
|
||||||
|
public function descriptionItem(string $item): void
|
||||||
|
{
|
||||||
|
[$color, $icon] = self::LOOKUP_TABLE[$item] ?? self::LOOKUP_TABLE['.'];
|
||||||
|
|
||||||
|
$symbolsOnCurrentLine = $this->processed % $this->compactSymbolsPerLine;
|
||||||
|
|
||||||
|
if ($symbolsOnCurrentLine >= $this->terminal->width() - 4) {
|
||||||
|
$symbolsOnCurrentLine = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($symbolsOnCurrentLine === 0) {
|
||||||
|
$this->output->writeln('');
|
||||||
|
$this->output->write(' ');
|
||||||
|
}
|
||||||
|
|
||||||
|
$this->output->write(sprintf('<fg=%s;options=bold>%s</>', $color, $icon));
|
||||||
|
|
||||||
|
$this->processed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs all errors from the given state using Collision's beautiful error output.
|
||||||
|
*/
|
||||||
|
public function errors(State $state): void
|
||||||
|
{
|
||||||
|
$this->style->writeErrorsSummary($state, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Outputs a clean recap of the test run, including the number of tests, assertions, and failures.
|
||||||
|
*/
|
||||||
|
public function recap(State $state, PHPUnitTestResult $testResult, Duration $duration): void
|
||||||
|
{
|
||||||
|
assert($this->output instanceof ConsoleOutput);
|
||||||
|
|
||||||
|
$nanoseconds = $duration->asNanoseconds() % 1_000_000_000;
|
||||||
|
$snapshotDuration = HRTime::fromSecondsAndNanoseconds((int) $duration->asSeconds(), $nanoseconds);
|
||||||
|
$telemetryDuration = \PHPUnit\Event\Telemetry\Duration::fromSecondsAndNanoseconds((int) $duration->asSeconds(), $nanoseconds);
|
||||||
|
|
||||||
|
$telemetry = new Info(
|
||||||
|
new Snapshot(
|
||||||
|
$snapshotDuration,
|
||||||
|
MemoryUsage::fromBytes(0),
|
||||||
|
MemoryUsage::fromBytes(0),
|
||||||
|
),
|
||||||
|
$telemetryDuration,
|
||||||
|
MemoryUsage::fromBytes(0),
|
||||||
|
\PHPUnit\Event\Telemetry\Duration::fromSecondsAndNanoseconds(0, 0),
|
||||||
|
MemoryUsage::fromBytes(0),
|
||||||
|
);
|
||||||
|
|
||||||
|
$this->style->writeRecap($state, $telemetry, $testResult);
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -13,18 +13,19 @@ final class Retry implements HandlesArguments
|
|||||||
{
|
{
|
||||||
use Concerns\HandleArguments;
|
use Concerns\HandleArguments;
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether it should show retry or not.
|
|
||||||
*/
|
|
||||||
public static bool $retrying = false;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* {@inheritDoc}
|
* {@inheritDoc}
|
||||||
*/
|
*/
|
||||||
public function handleArguments(array $arguments): array
|
public function handleArguments(array $arguments): array
|
||||||
{
|
{
|
||||||
self::$retrying = $this->hasArgument('--retry', $arguments);
|
if (! $this->hasArgument('--retry', $arguments)) {
|
||||||
|
return $arguments;
|
||||||
|
}
|
||||||
|
|
||||||
return $this->popArgument('--retry', $arguments);
|
$arguments = $this->popArgument('--retry', $arguments);
|
||||||
|
|
||||||
|
$arguments = $this->pushArgument('--order-by=defects', $arguments);
|
||||||
|
|
||||||
|
return $this->pushArgument('--stop-on-failure', $arguments);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -33,7 +33,7 @@ final class AfterEachRepository
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Gets a after each closure by the given filename.
|
* Gets an after each closure by the given filename.
|
||||||
*/
|
*/
|
||||||
public function get(string $filename): Closure
|
public function get(string $filename): Closure
|
||||||
{
|
{
|
||||||
|
|||||||
@ -1,91 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Repositories;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class RetryRepository
|
|
||||||
{
|
|
||||||
private const TEMPORARY_FOLDER = __DIR__
|
|
||||||
.DIRECTORY_SEPARATOR
|
|
||||||
.'..'
|
|
||||||
.DIRECTORY_SEPARATOR
|
|
||||||
.'..'
|
|
||||||
.DIRECTORY_SEPARATOR
|
|
||||||
.'.temp';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new Temp Repository instance.
|
|
||||||
*/
|
|
||||||
public function __construct(private readonly string $filename)
|
|
||||||
{
|
|
||||||
// ..
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Adds a new element.
|
|
||||||
*/
|
|
||||||
public function add(string $element): void
|
|
||||||
{
|
|
||||||
$this->save([...$this->all(), ...[$element]]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Clears the existing file, if any, and re-creates it.
|
|
||||||
*/
|
|
||||||
public function boot(): void
|
|
||||||
{
|
|
||||||
@unlink(self::TEMPORARY_FOLDER.'/'.$this->filename.'.json'); // @phpstan-ignore-line
|
|
||||||
|
|
||||||
$this->save([]);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if there is any element.
|
|
||||||
*/
|
|
||||||
public function isEmpty(): bool
|
|
||||||
{
|
|
||||||
return $this->all() === [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the given element exists.
|
|
||||||
*/
|
|
||||||
public function exists(string $element): bool
|
|
||||||
{
|
|
||||||
return in_array($element, $this->all(), true);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Gets all elements.
|
|
||||||
*
|
|
||||||
* @return array<int, string>
|
|
||||||
*/
|
|
||||||
private function all(): array
|
|
||||||
{
|
|
||||||
$path = self::TEMPORARY_FOLDER.'/'.$this->filename.'.json';
|
|
||||||
|
|
||||||
$contents = file_exists($path) ? file_get_contents($path) : '{}';
|
|
||||||
|
|
||||||
assert(is_string($contents));
|
|
||||||
|
|
||||||
$all = json_decode($contents, true, 512, JSON_THROW_ON_ERROR);
|
|
||||||
|
|
||||||
return is_array($all) ? $all : [];
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Save the given elements.
|
|
||||||
*
|
|
||||||
* @param array<int, string> $elements
|
|
||||||
*/
|
|
||||||
private function save(array $elements): void
|
|
||||||
{
|
|
||||||
$contents = json_encode($elements, JSON_THROW_ON_ERROR);
|
|
||||||
|
|
||||||
file_put_contents(self::TEMPORARY_FOLDER.'/'.$this->filename.'.json', $contents);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -125,6 +125,12 @@ final class TestRepository
|
|||||||
*/
|
*/
|
||||||
public function set(TestCaseMethodFactory $method): void
|
public function set(TestCaseMethodFactory $method): void
|
||||||
{
|
{
|
||||||
|
foreach ($this->testCaseFilters as $filter) {
|
||||||
|
if (! $filter->accept($method->filename)) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
foreach ($this->testCaseMethodFilters as $filter) {
|
foreach ($this->testCaseMethodFilters as $filter) {
|
||||||
if (! $filter->accept($method)) {
|
if (! $filter->accept($method)) {
|
||||||
return;
|
return;
|
||||||
@ -147,17 +153,15 @@ final class TestRepository
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
$accepted = array_reduce(
|
foreach ($this->testCaseFilters as $filter) {
|
||||||
$this->testCaseFilters,
|
if (! $filter->accept($filename)) {
|
||||||
fn (bool $carry, TestCaseFilter $filter): bool => $carry && $filter->accept($filename),
|
return;
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
if ($accepted) {
|
|
||||||
$this->make($this->testCases[$filename]);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
$this->make($this->testCases[$filename]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Makes a Test Case using the given factory.
|
* Makes a Test Case using the given factory.
|
||||||
*/
|
*/
|
||||||
|
|||||||
@ -4,19 +4,21 @@ declare(strict_types=1);
|
|||||||
|
|
||||||
namespace Pest\Subscribers;
|
namespace Pest\Subscribers;
|
||||||
|
|
||||||
|
use Pest\Support\Container;
|
||||||
use PHPUnit\Event\TestRunner\Configured;
|
use PHPUnit\Event\TestRunner\Configured;
|
||||||
use PHPUnit\Event\TestRunner\ConfiguredSubscriber;
|
use PHPUnit\Event\TestRunner\ConfiguredSubscriber;
|
||||||
|
use PHPUnit\TextUI\Configuration\Configuration;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @internal
|
* @internal
|
||||||
*/
|
*/
|
||||||
final class EnsureConfigurationDefaults implements ConfiguredSubscriber
|
final class EnsureConfigurationIsAvailable implements ConfiguredSubscriber
|
||||||
{
|
{
|
||||||
/**
|
/**
|
||||||
* Runs the subscriber.
|
* Runs the subscriber.
|
||||||
*/
|
*/
|
||||||
public function notify(Configured $event): void
|
public function notify(Configured $event): void
|
||||||
{
|
{
|
||||||
// TODO...
|
Container::getInstance()->add(Configuration::class, $event->configuration());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Subscribers;
|
|
||||||
|
|
||||||
use Pest\TestSuite;
|
|
||||||
use PHPUnit\Event\Test\Errored;
|
|
||||||
use PHPUnit\Event\Test\ErroredSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class EnsureErroredTestsAreRetryable implements ErroredSubscriber
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Runs the subscriber.
|
|
||||||
*/
|
|
||||||
public function notify(Errored $event): void
|
|
||||||
{
|
|
||||||
TestSuite::getInstance()->retryRepository->add($event->test()->id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Subscribers;
|
|
||||||
|
|
||||||
use Pest\TestSuite;
|
|
||||||
use PHPUnit\Event\Test\Failed;
|
|
||||||
use PHPUnit\Event\Test\FailedSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class EnsureFailedTestsAreRetryable implements FailedSubscriber
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Runs the subscriber.
|
|
||||||
*/
|
|
||||||
public function notify(Failed $event): void
|
|
||||||
{
|
|
||||||
TestSuite::getInstance()->retryRepository->add($event->test()->id());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -1,23 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Subscribers;
|
|
||||||
|
|
||||||
use Pest\TestSuite;
|
|
||||||
use PHPUnit\Event\TestRunner\Started;
|
|
||||||
use PHPUnit\Event\TestRunner\StartedSubscriber;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* @internal
|
|
||||||
*/
|
|
||||||
final class EnsureRetryRepositoryExists implements StartedSubscriber
|
|
||||||
{
|
|
||||||
/**
|
|
||||||
* Runs the subscriber.
|
|
||||||
*/
|
|
||||||
public function notify(Started $event): void
|
|
||||||
{
|
|
||||||
TestSuite::getInstance()->retryRepository->boot();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@ -21,8 +21,8 @@ final class EnsureTeamCityEnabled implements ConfiguredSubscriber
|
|||||||
* Creates a new Configured Subscriber instance.
|
* Creates a new Configured Subscriber instance.
|
||||||
*/
|
*/
|
||||||
public function __construct(
|
public function __construct(
|
||||||
private readonly OutputInterface $output,
|
|
||||||
private readonly InputInterface $input,
|
private readonly InputInterface $input,
|
||||||
|
private readonly OutputInterface $output,
|
||||||
private readonly TestSuite $testSuite,
|
private readonly TestSuite $testSuite,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|||||||
@ -54,7 +54,9 @@ final class Backtrace
|
|||||||
foreach (debug_backtrace(self::BACKTRACE_OPTIONS) as $trace) {
|
foreach (debug_backtrace(self::BACKTRACE_OPTIONS) as $trace) {
|
||||||
assert(array_key_exists(self::FILE, $trace));
|
assert(array_key_exists(self::FILE, $trace));
|
||||||
|
|
||||||
if (Str::endsWith($trace['file'], 'Bootstrappers/BootFiles.php') || Str::endsWith($trace[self::FILE], 'overrides/Runner/TestSuiteLoader.php')) {
|
$traceFile = str_replace(DIRECTORY_SEPARATOR, '/', $trace[self::FILE]);
|
||||||
|
|
||||||
|
if (Str::endsWith($traceFile, 'Bootstrappers/BootFiles.php') || Str::endsWith($traceFile, 'overrides/Runner/TestSuiteLoader.php')) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -47,10 +47,14 @@ final class Container
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Adds the given instance to the container.
|
* Adds the given instance to the container.
|
||||||
|
*
|
||||||
|
* @return $this
|
||||||
*/
|
*/
|
||||||
public function add(string $id, object|string $instance): void
|
public function add(string $id, object|string $instance): self
|
||||||
{
|
{
|
||||||
$this->instances[$id] = $instance;
|
$this->instances[$id] = $instance;
|
||||||
|
|
||||||
|
return $this;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@ -1,62 +0,0 @@
|
|||||||
<?php
|
|
||||||
|
|
||||||
declare(strict_types=1);
|
|
||||||
|
|
||||||
namespace Pest\Support;
|
|
||||||
|
|
||||||
use PHPUnit\Util\Exception;
|
|
||||||
use PHPUnit\Util\Filesystem;
|
|
||||||
|
|
||||||
abstract class Printer implements \PHPUnit\Util\Printer
|
|
||||||
{
|
|
||||||
/** @var resource|bool */
|
|
||||||
private $stream;
|
|
||||||
|
|
||||||
private readonly bool $isPhpStream;
|
|
||||||
|
|
||||||
private bool $isOpen;
|
|
||||||
|
|
||||||
private function __construct(string $out)
|
|
||||||
{
|
|
||||||
if (str_starts_with($out, 'socket://')) {
|
|
||||||
$tmp = explode(':', str_replace('socket://', '', $out));
|
|
||||||
|
|
||||||
if (count($tmp) !== 2) {
|
|
||||||
throw new Exception(sprintf('"%s" does not match "socket://hostname:port" format', $out));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->stream = fsockopen($tmp[0], (int) $tmp[1]);
|
|
||||||
$this->isOpen = true;
|
|
||||||
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->isPhpStream = str_starts_with($out, 'php://');
|
|
||||||
|
|
||||||
if (! $this->isPhpStream && ! Filesystem::createDirectory(dirname($out))) {
|
|
||||||
throw new Exception(sprintf('Directory "%s" was not created', dirname($out)));
|
|
||||||
}
|
|
||||||
|
|
||||||
$this->stream = fopen($out, 'wb');
|
|
||||||
$this->isOpen = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
final public function print(string $buffer): void
|
|
||||||
{
|
|
||||||
assert($this->isOpen);
|
|
||||||
assert($this->stream !== false);
|
|
||||||
|
|
||||||
// @phpstan-ignore-next-line
|
|
||||||
fwrite($this->stream, $buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
final public function flush(): void
|
|
||||||
{
|
|
||||||
if ($this->isOpen && $this->isPhpStream && $this->stream !== false) {
|
|
||||||
// @phpstan-ignore-next-line
|
|
||||||
fclose($this->stream);
|
|
||||||
|
|
||||||
$this->isOpen = false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
105
src/Support/StateGenerator.php
Normal file
105
src/Support/StateGenerator.php
Normal file
@ -0,0 +1,105 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
declare(strict_types=1);
|
||||||
|
|
||||||
|
namespace Pest\Support;
|
||||||
|
|
||||||
|
use NunoMaduro\Collision\Adapters\Phpunit\State;
|
||||||
|
use NunoMaduro\Collision\Adapters\Phpunit\TestResult;
|
||||||
|
use PHPUnit\Event\Code\TestDox;
|
||||||
|
use PHPUnit\Event\Code\TestMethod;
|
||||||
|
use PHPUnit\Event\Code\Throwable;
|
||||||
|
use PHPUnit\Event\Test\Errored;
|
||||||
|
use PHPUnit\Event\TestData\TestDataCollection;
|
||||||
|
use PHPUnit\Framework\IncompleteTestError;
|
||||||
|
use PHPUnit\Framework\SkippedWithMessageException;
|
||||||
|
use PHPUnit\Metadata\MetadataCollection;
|
||||||
|
use PHPUnit\TestRunner\TestResult\TestResult as PHPUnitTestResult;
|
||||||
|
|
||||||
|
final class StateGenerator
|
||||||
|
{
|
||||||
|
public function fromPhpUnitTestResult(PHPUnitTestResult $testResult): State
|
||||||
|
{
|
||||||
|
$state = new State();
|
||||||
|
|
||||||
|
foreach ($testResult->testErroredEvents() as $testResultEvent) {
|
||||||
|
if ($testResultEvent instanceof Errored) {
|
||||||
|
$state->add(TestResult::fromTestCase(
|
||||||
|
$testResultEvent->test(),
|
||||||
|
TestResult::FAIL,
|
||||||
|
$testResultEvent->throwable()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
$state->add(TestResult::fromBeforeFirstTestMethodErrored($testResultEvent));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($testResult->testFailedEvents() as $testResultEvent) {
|
||||||
|
$state->add(TestResult::fromTestCase(
|
||||||
|
$testResultEvent->test(),
|
||||||
|
TestResult::FAIL,
|
||||||
|
$testResultEvent->throwable()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($testResult->testMarkedIncompleteEvents() as $testResultEvent) {
|
||||||
|
$state->add(TestResult::fromTestCase(
|
||||||
|
$testResultEvent->test(),
|
||||||
|
TestResult::INCOMPLETE,
|
||||||
|
$testResultEvent->throwable()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($testResult->testConsideredRiskyEvents() as $riskyEvents) {
|
||||||
|
foreach ($riskyEvents as $riskyEvent) {
|
||||||
|
$state->add(TestResult::fromTestCase(
|
||||||
|
$riskyEvent->test(),
|
||||||
|
TestResult::RISKY,
|
||||||
|
Throwable::from(new IncompleteTestError($riskyEvent->message()))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($testResult->testSkippedEvents() as $testResultEvent) {
|
||||||
|
if ($testResultEvent->message() === '__TODO__') {
|
||||||
|
$state->add(TestResult::fromTestCase($testResultEvent->test(), TestResult::TODO));
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
$state->add(TestResult::fromTestCase(
|
||||||
|
$testResultEvent->test(),
|
||||||
|
TestResult::SKIPPED,
|
||||||
|
Throwable::from(new SkippedWithMessageException($testResultEvent->message()))
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
$numberOfPassedTests = $testResult->numberOfTestsRun()
|
||||||
|
- $testResult->numberOfTestErroredEvents()
|
||||||
|
- $testResult->numberOfTestFailedEvents()
|
||||||
|
- $testResult->numberOfTestSkippedEvents()
|
||||||
|
- $testResult->numberOfTestsWithTestConsideredRiskyEvents()
|
||||||
|
- $testResult->numberOfTestMarkedIncompleteEvents();
|
||||||
|
|
||||||
|
for ($i = 0; $i < $numberOfPassedTests; $i++) {
|
||||||
|
$state->add(TestResult::fromTestCase(
|
||||||
|
|
||||||
|
new TestMethod(
|
||||||
|
/** @phpstan-ignore-next-line */
|
||||||
|
"$i",
|
||||||
|
/** @phpstan-ignore-next-line */
|
||||||
|
'',
|
||||||
|
'',
|
||||||
|
1,
|
||||||
|
/** @phpstan-ignore-next-line */
|
||||||
|
TestDox::fromClassNameAndMethodName('', ''),
|
||||||
|
MetadataCollection::fromArray([]),
|
||||||
|
TestDataCollection::fromArray([])
|
||||||
|
),
|
||||||
|
TestResult::PASS
|
||||||
|
));
|
||||||
|
}
|
||||||
|
|
||||||
|
return $state;
|
||||||
|
}
|
||||||
|
}
|
||||||
@ -6,7 +6,8 @@ namespace Pest\TestCaseFilters;
|
|||||||
|
|
||||||
use Pest\Contracts\TestCaseFilter;
|
use Pest\Contracts\TestCaseFilter;
|
||||||
use Pest\Exceptions\MissingDependency;
|
use Pest\Exceptions\MissingDependency;
|
||||||
use Pest\Exceptions\NoTestsFound;
|
use Pest\Exceptions\NoDirtyTestsFound;
|
||||||
|
use Pest\Panic;
|
||||||
use Pest\TestSuite;
|
use Pest\TestSuite;
|
||||||
use Symfony\Component\Process\Process;
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
@ -66,7 +67,7 @@ final class GitDirtyTestCaseFilter implements TestCaseFilter
|
|||||||
$dirtyFiles = array_values($dirtyFiles);
|
$dirtyFiles = array_values($dirtyFiles);
|
||||||
|
|
||||||
if ($dirtyFiles === []) {
|
if ($dirtyFiles === []) {
|
||||||
throw new NoTestsFound();
|
Panic::with(new NoDirtyTestsFound());
|
||||||
}
|
}
|
||||||
|
|
||||||
$this->changedFiles = $dirtyFiles;
|
$this->changedFiles = $dirtyFiles;
|
||||||
|
|||||||
@ -9,7 +9,6 @@ use Pest\Repositories\AfterAllRepository;
|
|||||||
use Pest\Repositories\AfterEachRepository;
|
use Pest\Repositories\AfterEachRepository;
|
||||||
use Pest\Repositories\BeforeAllRepository;
|
use Pest\Repositories\BeforeAllRepository;
|
||||||
use Pest\Repositories\BeforeEachRepository;
|
use Pest\Repositories\BeforeEachRepository;
|
||||||
use Pest\Repositories\RetryRepository;
|
|
||||||
use Pest\Repositories\TestRepository;
|
use Pest\Repositories\TestRepository;
|
||||||
use PHPUnit\Framework\TestCase;
|
use PHPUnit\Framework\TestCase;
|
||||||
|
|
||||||
@ -48,11 +47,6 @@ final class TestSuite
|
|||||||
*/
|
*/
|
||||||
public AfterAllRepository $afterAll;
|
public AfterAllRepository $afterAll;
|
||||||
|
|
||||||
/**
|
|
||||||
* Holds the retry repository.
|
|
||||||
*/
|
|
||||||
public RetryRepository $retryRepository;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Holds the root path.
|
* Holds the root path.
|
||||||
*/
|
*/
|
||||||
@ -75,7 +69,6 @@ final class TestSuite
|
|||||||
$this->tests = new TestRepository();
|
$this->tests = new TestRepository();
|
||||||
$this->afterEach = new AfterEachRepository();
|
$this->afterEach = new AfterEachRepository();
|
||||||
$this->afterAll = new AfterAllRepository();
|
$this->afterAll = new AfterAllRepository();
|
||||||
$this->retryRepository = new RetryRepository('retry');
|
|
||||||
|
|
||||||
$this->rootPath = (string) realpath($rootPath);
|
$this->rootPath = (string) realpath($rootPath);
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
##teamcity[testSuiteStarted name='Tests\tests\Failure' locationHint='file://tests/.tests/Failure.php' flowId='1234']
|
##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='file://tests/.tests/Failure.php' flowId='1234']
|
||||||
##teamcity[testStarted name='it can fail with comparison' locationHint='pest_qn://tests/.tests/Failure.php::it can fail with comparison' flowId='1234']
|
##teamcity[testStarted name='it can fail with comparison' locationHint='pest_qn://tests/.tests/Failure.php::it can fail with comparison' flowId='1234']
|
||||||
##teamcity[testFailed name='it can fail with comparison' message='Failed asserting that true matches expected false.' details='at src/Mixins/Expectation.php:312|nat src/Support/ExpectationPipeline.php:75|nat src/Support/ExpectationPipeline.php:79|nat src/Expectation.php:300|nat tests/.tests/Failure.php:6|nat src/Factories/TestCaseMethodFactory.php:106|nat src/Concerns/Testable.php:262|nat src/Support/ExceptionTrace.php:28|nat src/Concerns/Testable.php:262|nat src/Concerns/Testable.php:217|nat src/Kernel.php:79' type='comparisonFailure' actual='true' expected='false' flowId='1234']
|
##teamcity[testFailed name='it can fail with comparison' message='Failed asserting that true matches expected false.' details='at src/Mixins/Expectation.php:342|nat src/Support/ExpectationPipeline.php:75|nat src/Support/ExpectationPipeline.php:79|nat src/Expectation.php:300|nat tests/.tests/Failure.php:6|nat src/Factories/TestCaseMethodFactory.php:105|nat src/Concerns/Testable.php:262|nat src/Support/ExceptionTrace.php:28|nat src/Concerns/Testable.php:262|nat src/Concerns/Testable.php:217|nat src/Kernel.php:89' type='comparisonFailure' actual='true' expected='false' flowId='1234']
|
||||||
##teamcity[testFinished name='it can fail with comparison' duration='100000' flowId='1234']
|
##teamcity[testFinished name='it can fail with comparison' duration='100000' flowId='1234']
|
||||||
##teamcity[testStarted name='it can be ignored because of no assertions' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because of no assertions' flowId='1234']
|
##teamcity[testStarted name='it can be ignored because of no assertions' locationHint='pest_qn://tests/.tests/Failure.php::it can be ignored because of no assertions' flowId='1234']
|
||||||
##teamcity[testIgnored name='it can be ignored because of no assertions' message='This test did not perform any assertions' details='' flowId='1234']
|
##teamcity[testIgnored name='it can be ignored because of no assertions' message='This test did not perform any assertions' details='' flowId='1234']
|
||||||
@ -9,7 +9,7 @@
|
|||||||
##teamcity[testIgnored name='it can be ignored because it is skipped' message='This test was ignored.' details='' flowId='1234']
|
##teamcity[testIgnored name='it can be ignored because it is skipped' message='This test was ignored.' details='' flowId='1234']
|
||||||
##teamcity[testFinished name='it can be ignored because it is skipped' duration='100000' flowId='1234']
|
##teamcity[testFinished name='it can be ignored because it is skipped' duration='100000' flowId='1234']
|
||||||
##teamcity[testStarted name='it can fail' locationHint='pest_qn://tests/.tests/Failure.php::it can fail' flowId='1234']
|
##teamcity[testStarted name='it can fail' locationHint='pest_qn://tests/.tests/Failure.php::it can fail' flowId='1234']
|
||||||
##teamcity[testFailed name='it can fail' message='oh noo' details='at tests/.tests/Failure.php:18|nat src/Factories/TestCaseMethodFactory.php:106|nat src/Concerns/Testable.php:262|nat src/Support/ExceptionTrace.php:28|nat src/Concerns/Testable.php:262|nat src/Concerns/Testable.php:217|nat src/Kernel.php:79' flowId='1234']
|
##teamcity[testFailed name='it can fail' message='oh noo' details='at tests/.tests/Failure.php:18|nat src/Factories/TestCaseMethodFactory.php:105|nat src/Concerns/Testable.php:262|nat src/Support/ExceptionTrace.php:28|nat src/Concerns/Testable.php:262|nat src/Concerns/Testable.php:217|nat src/Kernel.php:89' flowId='1234']
|
||||||
##teamcity[testFinished name='it can fail' duration='100000' flowId='1234']
|
##teamcity[testFinished name='it can fail' duration='100000' flowId='1234']
|
||||||
##teamcity[testStarted name='it is not done yet' locationHint='pest_qn://tests/.tests/Failure.php::it is not done yet' flowId='1234']
|
##teamcity[testStarted name='it is not done yet' locationHint='pest_qn://tests/.tests/Failure.php::it is not done yet' flowId='1234']
|
||||||
##teamcity[testIgnored name='it is not done yet' message='This test was ignored.' details='' flowId='1234']
|
##teamcity[testIgnored name='it is not done yet' message='This test was ignored.' details='' flowId='1234']
|
||||||
@ -17,7 +17,7 @@
|
|||||||
##teamcity[testStarted name='build this one.' locationHint='pest_qn://tests/.tests/Failure.php::build this one.' flowId='1234']
|
##teamcity[testStarted name='build this one.' locationHint='pest_qn://tests/.tests/Failure.php::build this one.' flowId='1234']
|
||||||
##teamcity[testIgnored name='build this one.' message='This test was ignored.' details='' flowId='1234']
|
##teamcity[testIgnored name='build this one.' message='This test was ignored.' details='' flowId='1234']
|
||||||
##teamcity[testFinished name='build this one.' duration='100000' flowId='1234']
|
##teamcity[testFinished name='build this one.' duration='100000' flowId='1234']
|
||||||
##teamcity[testSuiteFinished name='Tests\tests\Failure' flowId='1234']
|
##teamcity[testSuiteFinished name='Tests/tests/Failure' flowId='1234']
|
||||||
|
|
||||||
[90mTests:[39m [31;1m2 failed[39;22m[90m,[39m[39m [39m[33;1m1 risky[39;22m[90m,[39m[39m [39m[36;1m2 todos[39;22m[90m,[39m[39m [39m[33;1m1 skipped[39;22m[90m (2 assertions)[39m
|
[90mTests:[39m [31;1m2 failed[39;22m[90m,[39m[39m [39m[33;1m1 risky[39;22m[90m,[39m[39m [39m[36;1m2 todos[39;22m[90m,[39m[39m [39m[33;1m1 skipped[39;22m[90m (2 assertions)[39m
|
||||||
[90mDuration:[39m [39m1.00s[39m
|
[90mDuration:[39m [39m1.00s[39m
|
||||||
|
|||||||
@ -1,9 +1,9 @@
|
|||||||
##teamcity[testSuiteStarted name='Tests\tests\SuccessOnly' locationHint='file://tests/.tests/SuccessOnly.php' flowId='1234']
|
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='file://tests/.tests/SuccessOnly.php' flowId='1234']
|
||||||
##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234']
|
##teamcity[testStarted name='it can pass with comparison' locationHint='pest_qn://tests/.tests/SuccessOnly.php::it can pass with comparison' flowId='1234']
|
||||||
##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234']
|
##teamcity[testFinished name='it can pass with comparison' duration='100000' flowId='1234']
|
||||||
##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234']
|
##teamcity[testStarted name='can also pass' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can also pass' flowId='1234']
|
||||||
##teamcity[testFinished name='can also pass' duration='100000' flowId='1234']
|
##teamcity[testFinished name='can also pass' duration='100000' flowId='1234']
|
||||||
##teamcity[testSuiteFinished name='Tests\tests\SuccessOnly' flowId='1234']
|
##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234']
|
||||||
|
|
||||||
[90mTests:[39m [32;1m2 passed[39;22m[90m (2 assertions)[39m
|
[90mTests:[39m [32;1m2 passed[39;22m[90m (2 assertions)[39m
|
||||||
[90mDuration:[39m [39m1.00s[39m
|
[90mDuration:[39m [39m1.00s[39m
|
||||||
|
|||||||
@ -49,7 +49,6 @@
|
|||||||
--fail-on-risky .............................. Treat risky tests as failures
|
--fail-on-risky .............................. Treat risky tests as failures
|
||||||
--fail-on-skipped .......................... Treat skipped tests as failures
|
--fail-on-skipped .......................... Treat skipped tests as failures
|
||||||
--fail-on-warning .................... Treat tests with warnings as failures
|
--fail-on-warning .................... Treat tests with warnings as failures
|
||||||
--repeat <times> ...................................... Runs the test(s) repeatedly
|
|
||||||
--cache-result ............................ Write test results to cache file
|
--cache-result ............................ Write test results to cache file
|
||||||
--do-not-cache-result .............. Do not write test results to cache file
|
--do-not-cache-result .............. Do not write test results to cache file
|
||||||
--order-by <order> Run tests in order: default|defects|duration|no-depends|random|reverse|size
|
--order-by <order> Run tests in order: default|defects|duration|no-depends|random|reverse|size
|
||||||
@ -78,7 +77,6 @@
|
|||||||
--log-teamcity <file> .............. Log test execution in TeamCity format to file
|
--log-teamcity <file> .............. Log test execution in TeamCity format to file
|
||||||
--testdox-html <file> ................. Write documentation in HTML format to file
|
--testdox-html <file> ................. Write documentation in HTML format to file
|
||||||
--testdox-text <file> ................. Write documentation in Text format to file
|
--testdox-text <file> ................. Write documentation in Text format to file
|
||||||
--testdox-xml <file> ................... Write documentation in XML format to file
|
|
||||||
--log-events-text <file> ..................... Stream events as plain text to file
|
--log-events-text <file> ..................... Stream events as plain text to file
|
||||||
--log-events-verbose-text <file> Stream events as plain text to file (with telemetry information)
|
--log-events-verbose-text <file> Stream events as plain text to file (with telemetry information)
|
||||||
--no-logging .................................. Ignore logging configuration
|
--no-logging .................................. Ignore logging configuration
|
||||||
|
|||||||
@ -29,7 +29,7 @@
|
|||||||
✓ it does not append CoversNothing to other methods
|
✓ it does not append CoversNothing to other methods
|
||||||
✓ it throws exception if no class nor method has been found
|
✓ it throws exception if no class nor method has been found
|
||||||
|
|
||||||
PASS Tests\Features\DatasetsTests
|
PASS Tests\Features\DatasetsTests - 1 todo
|
||||||
✓ it throws exception if dataset does not exist
|
✓ it throws exception if dataset does not exist
|
||||||
✓ it throws exception if dataset already exist
|
✓ it throws exception if dataset already exist
|
||||||
✓ it sets closures
|
✓ it sets closures
|
||||||
@ -556,6 +556,18 @@
|
|||||||
✓ it fails
|
✓ it fails
|
||||||
✓ it fails with message
|
✓ it fails with message
|
||||||
|
|
||||||
|
PASS Tests\Features\Expect\toHaveMethod
|
||||||
|
✓ pass
|
||||||
|
✓ failures
|
||||||
|
✓ failures with message
|
||||||
|
✓ not failures
|
||||||
|
|
||||||
|
PASS Tests\Features\Expect\toHaveMethods
|
||||||
|
✓ pass
|
||||||
|
✓ failures
|
||||||
|
✓ failures with custom message
|
||||||
|
✓ not failures
|
||||||
|
|
||||||
PASS Tests\Features\Expect\toHaveProperties
|
PASS Tests\Features\Expect\toHaveProperties
|
||||||
✓ pass
|
✓ pass
|
||||||
✓ failures
|
✓ failures
|
||||||
@ -732,7 +744,7 @@
|
|||||||
✓ it allows access to the underlying expectNotToPerformAssertions method
|
✓ it allows access to the underlying expectNotToPerformAssertions method
|
||||||
✓ it allows performing no expectations without being risky
|
✓ it allows performing no expectations without being risky
|
||||||
|
|
||||||
PASS Tests\Features\Todo
|
PASS Tests\Features\Todo - 3 todos
|
||||||
↓ something todo later
|
↓ something todo later
|
||||||
↓ something todo later chained
|
↓ something todo later chained
|
||||||
↓ something todo later chained and with function body
|
↓ something todo later chained and with function body
|
||||||
@ -833,7 +845,7 @@
|
|||||||
✓ environment is set to Local when --ci option is not used
|
✓ environment is set to Local when --ci option is not used
|
||||||
|
|
||||||
PASS Tests\Unit\Plugins\Retry
|
PASS Tests\Unit\Plugins\Retry
|
||||||
✓ it retries if --retry argument is used
|
✓ it orders by defects and stop on defects if when --retry is used
|
||||||
|
|
||||||
PASS Tests\Unit\Support\Backtrace
|
PASS Tests\Unit\Support\Backtrace
|
||||||
✓ it gets file name from called file
|
✓ it gets file name from called file
|
||||||
@ -886,6 +898,9 @@
|
|||||||
PASS Tests\Visual\Help
|
PASS Tests\Visual\Help
|
||||||
✓ visual snapshot of help command output
|
✓ visual snapshot of help command output
|
||||||
|
|
||||||
|
PASS Tests\Visual\Parallel
|
||||||
|
✓ parallel
|
||||||
|
|
||||||
PASS Tests\Visual\SingleTestOrDirectory
|
PASS Tests\Visual\SingleTestOrDirectory
|
||||||
✓ allows to run a single test
|
✓ allows to run a single test
|
||||||
✓ allows to run a directory
|
✓ allows to run a directory
|
||||||
@ -905,3 +920,4 @@
|
|||||||
✓ visual snapshot of help command output
|
✓ visual snapshot of help command output
|
||||||
|
|
||||||
Tests: 4 incomplete, 4 todos, 18 skipped, 627 passed (1514 assertions)
|
Tests: 4 incomplete, 4 todos, 18 skipped, 627 passed (1514 assertions)
|
||||||
|
|
||||||
|
|||||||
@ -1,7 +1,7 @@
|
|||||||
PASS Tests\Features\DatasetsTests
|
TODO Tests\Features\DatasetsTests - 1 todo
|
||||||
↓ forbids to define tests in Datasets dirs and Datasets.php files
|
↓ forbids to define tests in Datasets dirs and Datasets.php files
|
||||||
|
|
||||||
PASS Tests\Features\Todo
|
TODO Tests\Features\Todo - 3 todos
|
||||||
↓ something todo later
|
↓ something todo later
|
||||||
↓ something todo later chained
|
↓ something todo later chained
|
||||||
↓ something todo later chained and with function body
|
↓ something todo later chained and with function body
|
||||||
|
|||||||
28
tests/Features/Expect/toHaveMethod.php
Normal file
28
tests/Features/Expect/toHaveMethod.php
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\ExpectationFailedException;
|
||||||
|
|
||||||
|
$object = new class
|
||||||
|
{
|
||||||
|
public function foo(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test('pass', function () use ($object) {
|
||||||
|
expect($object)->toHaveMethod('foo')
|
||||||
|
->and($object)->toHaveMethod('foo')
|
||||||
|
->and($object)->not->toHaveMethod('fooNull');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failures', function () use ($object) {
|
||||||
|
expect($object)->toHaveMethod('bar');
|
||||||
|
})->throws(ExpectationFailedException::class);
|
||||||
|
|
||||||
|
test('failures with message', function () use ($object) {
|
||||||
|
expect($object)->toHaveMethod(name: 'bar', message: 'oh no!');
|
||||||
|
})->throws(ExpectationFailedException::class, 'oh no!');
|
||||||
|
|
||||||
|
test('not failures', function () use ($object) {
|
||||||
|
expect($object)->not->toHaveMethod('foo');
|
||||||
|
})->throws(ExpectationFailedException::class);
|
||||||
30
tests/Features/Expect/toHaveMethods.php
Normal file
30
tests/Features/Expect/toHaveMethods.php
Normal file
@ -0,0 +1,30 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use PHPUnit\Framework\ExpectationFailedException;
|
||||||
|
|
||||||
|
$object = new class
|
||||||
|
{
|
||||||
|
public function foo(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
public function bar(): void
|
||||||
|
{
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
test('pass', function () use ($object) {
|
||||||
|
expect($object)->toHaveMethods(['foo', 'bar']);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('failures', function () use ($object) {
|
||||||
|
expect($object)->toHaveMethods(['foo', 'bar', 'baz']);
|
||||||
|
})->throws(ExpectationFailedException::class);
|
||||||
|
|
||||||
|
test('failures with custom message', function () use ($object) {
|
||||||
|
expect($object)->toHaveMethods(['foo', 'bar', 'baz'], 'oh no!');
|
||||||
|
})->throws(ExpectationFailedException::class, 'oh no!');
|
||||||
|
|
||||||
|
test('not failures', function () use ($object) {
|
||||||
|
expect($object)->not->toHaveMethods(['foo', 'bar']);
|
||||||
|
})->throws(ExpectationFailedException::class);
|
||||||
@ -1,11 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
use Pest\Plugins\Parallel;
|
||||||
use Pest\Support\Str;
|
use Pest\Support\Str;
|
||||||
|
|
||||||
// HACK: we have to determine our $_SERVER['globalHook-]>calls baseline. This is because
|
// HACK: we have to determine our $_SERVER['globalHook-]>calls baseline. This is because
|
||||||
// two other tests are executed before this one due to filename ordering.
|
// two other tests are executed before this one due to filename ordering.
|
||||||
$args = $_SERVER['argv'] ?? [];
|
$args = $_SERVER['argv'] ?? [];
|
||||||
$single = (isset($args[1]) && Str::endsWith(__FILE__, $args[1])) || ($_SERVER['PEST_PARALLEL'] ?? false);
|
$single = (isset($args[1]) && Str::endsWith(__FILE__, $args[1])) || Parallel::isWorker();
|
||||||
$offset = $single ? 0 : 2;
|
$offset = $single ? 0 : 2;
|
||||||
|
|
||||||
uses()->beforeAll(function () use ($offset) {
|
uses()->beforeAll(function () use ($offset) {
|
||||||
|
|||||||
@ -2,14 +2,13 @@
|
|||||||
|
|
||||||
use Pest\Plugins\Retry;
|
use Pest\Plugins\Retry;
|
||||||
|
|
||||||
beforeEach(fn () => Retry::$retrying = false);
|
it('orders by defects and stop on defects if when --retry is used ', function () {
|
||||||
|
|
||||||
afterEach(fn () => Retry::$retrying = false);
|
|
||||||
|
|
||||||
it('retries if --retry argument is used', function () {
|
|
||||||
$retry = new Retry();
|
$retry = new Retry();
|
||||||
|
|
||||||
$retry->handleArguments(['--retry']);
|
$arguments = $retry->handleArguments(['--retry']);
|
||||||
|
|
||||||
expect(Retry::$retrying)->toBeTrue();
|
expect($arguments)->toBe([
|
||||||
|
'--order-by=defects',
|
||||||
|
'--stop-on-failure',
|
||||||
|
]);
|
||||||
});
|
});
|
||||||
|
|||||||
18
tests/Visual/Parallel.php
Normal file
18
tests/Visual/Parallel.php
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
<?php
|
||||||
|
|
||||||
|
use Symfony\Component\Process\Process;
|
||||||
|
|
||||||
|
$run = function () {
|
||||||
|
$process = new Process(['php', 'bin/pest', '--parallel', '--processes=3', '--exclude-group=integration'], dirname(__DIR__, 2),
|
||||||
|
['COLLISION_PRINTER' => 'DefaultPrinter', 'COLLISION_IGNORE_DURATION' => 'true'],
|
||||||
|
);
|
||||||
|
|
||||||
|
$process->run();
|
||||||
|
|
||||||
|
return preg_replace('#\\x1b[[][^A-Za-z]*[A-Za-z]#', '', $process->getOutput());
|
||||||
|
};
|
||||||
|
|
||||||
|
test('parallel', function () use ($run) {
|
||||||
|
expect($run())->toContain('Running 650 tests using 3 processes')
|
||||||
|
->toContain('Tests: 4 incomplete, 4 todos, 15 skipped, 627 passed (1546 assertions)');
|
||||||
|
});
|
||||||
@ -1,5 +1,12 @@
|
|||||||
<?php
|
<?php
|
||||||
|
|
||||||
|
function normalize_windows_os_output(string $text): string
|
||||||
|
{
|
||||||
|
$text = str_replace('\r', '', $text);
|
||||||
|
|
||||||
|
return str_replace('\\', '/', $text);
|
||||||
|
}
|
||||||
|
|
||||||
test('visual snapshot of team city', function (string $testFile) {
|
test('visual snapshot of team city', function (string $testFile) {
|
||||||
$testsPath = dirname(__DIR__)."/.tests/$testFile";
|
$testsPath = dirname(__DIR__)."/.tests/$testFile";
|
||||||
|
|
||||||
@ -28,13 +35,9 @@ test('visual snapshot of team city', function (string $testFile) {
|
|||||||
};
|
};
|
||||||
|
|
||||||
if (getenv('REBUILD_SNAPSHOTS')) {
|
if (getenv('REBUILD_SNAPSHOTS')) {
|
||||||
$outputContent = explode("\n", $output());
|
file_put_contents($snapshot, normalize_windows_os_output($output()));
|
||||||
|
|
||||||
file_put_contents($snapshot, implode("\n", $outputContent));
|
|
||||||
} elseif (! getenv('EXCLUDE')) {
|
} elseif (! getenv('EXCLUDE')) {
|
||||||
$output = explode("\n", $output());
|
expect(normalize_windows_os_output($output()))->toEqual(file_get_contents($snapshot));
|
||||||
|
|
||||||
expect(implode("\n", $output))->toEqual(file_get_contents($snapshot));
|
|
||||||
}
|
}
|
||||||
})->with([
|
})->with([
|
||||||
'Failure.php',
|
'Failure.php',
|
||||||
|
|||||||
Reference in New Issue
Block a user