Compare commits

...

44 Commits

Author SHA1 Message Date
1e0bb88b73 release: v3.2.5 2024-10-01 11:55:18 +01:00
83b76d7c2e chore: bumps dependencies 2024-10-01 11:51:26 +01:00
5a870b3940 chore: style changes 2024-10-01 11:51:16 +01:00
1115c64186 chore: style changes 2024-10-01 11:48:14 +01:00
e38a271ca2 Merge pull request #1279 from midnite81/bug/declare-strict-types-with-comments-above
Strict types expectation allows for comments above declaration
2024-09-29 19:12:06 +01:00
43703ab40a Merge pull request #1280 from CamKem/fix/middleware-method-preset
Fix: Add middleware to the allowable public methods for Laravel Preset
2024-09-29 19:11:40 +01:00
86452765a4 fix: add middleware to the allowable public methods on the laravel preset 2024-09-29 16:07:39 +10:00
b8a1b7e5cc Add tests for strict types expectation
Introduced new test cases to ensure strict type declaration handling. Files with and without strict types are tested, including scenarios with comments preceding the declaration. Updated the regex in `Expectation.php` to accommodate comments and whitespaces before the `declare(strict_types=1)` statement.
2024-09-28 17:31:33 +01:00
5fe79d9c18 release: v3.2.4 2024-09-26 23:53:39 +01:00
2744da4292 Merge pull request #1277 from MuhammedAlkhudiry/ignore-handler-in-laravel-preset
ignore App\Exceptions\Handler.php in arch laravel preset
2024-09-26 23:47:26 +01:00
87f4e5e7b3 fix 2024-09-26 16:44:30 +03:00
bb3decf3cc ignore App\Exceptions\Handler.php in arch laravel preset 2024-09-26 16:41:43 +03:00
4e2987d438 release: v3.2.3 2024-09-25 16:19:39 +01:00
a25158bce8 Merge pull request #1275 from jeremynikolic/laravel-presets-ignore-concerns
feat: add ignoring of Concerns folder inside App\Enums and App\Features
2024-09-25 16:16:26 +01:00
49e77b1d4c feat: add ignoring of Concerns folder inside App\Enums and App\Features 2024-09-25 17:12:42 +02:00
989e43d1a0 release: v3.2.2 2024-09-24 10:23:43 +01:00
7cd42aafd8 fix: auto-complete on presets 2024-09-24 10:23:32 +01:00
48a1de273f release: v3.2.1 2024-09-23 14:09:55 +01:00
970e16e949 Ignores 2024-09-23 14:08:30 +01:00
432ff221c6 fix: missing != and !== on new toUseStrictEquality arch expectation 2024-09-23 14:08:21 +01:00
a55da85dd2 release: v3.2.0 2024-09-23 13:14:03 +01:00
f291cd1603 chore: bumps dependencies 2024-09-23 13:11:49 +01:00
5de0c2254a release: v3.1.0 2024-09-19 23:39:07 +01:00
b98ce0ced3 feat: adds mutates 2024-09-19 23:32:28 +01:00
28772c2609 chore: dont run integration tests yet on php 8.4 2024-09-19 13:42:01 +01:00
452ffaf8df chore: fixes windows build 2024-09-19 13:38:35 +01:00
e8338405b5 chore: tests against PHP 8.4 2024-09-19 13:36:41 +01:00
1b014e4b18 release: v3.0.8 2024-09-19 13:04:42 +01:00
034715e8b1 Merge pull request #1266 from julien-boudry/3.x
Fix #1265 -  issue parameter cannot be int (one done, pr, todo, wip)
2024-09-19 12:53:34 +01:00
09eff785c4 release: v3.0.7 2024-09-19 12:29:38 +01:00
22cc7805d7 chore: bumps dependencies 2024-09-19 12:29:38 +01:00
669dc0da71 Fix #1265 - issue parameter cannot be int (one done, pr, todo, wip) 2024-09-19 09:49:36 +00:00
689da4ed4e Merge pull request #1254 from pestphp/bugfix/jira-url
fix: update assignee URL for Jira
2024-09-18 21:48:09 +01:00
2f15861b0d fix: update assignee URL for Jira 2024-09-16 12:18:21 +01:00
0d50d35b5e release: v3.0.6 2024-09-11 18:59:43 +01:00
ce61ced8e1 Merge pull request #1237 from smirok/teamcity-fix-for-tests-with-dataset
fix: unify the `locationHint` prefix and prettify both `locationHint` and `name` parameters for testing with datasets
2024-09-11 18:51:04 +01:00
7227d24611 fix: unify the locationHint prefix and prettify both locationHint and name parameters for testing with datasets 2024-09-11 16:42:06 +02:00
45f16484d5 Merge pull request #1235 from pestphp/3.x_herd_fix
Fixes parallel mutation testing when using Laravel Herd
2024-09-11 15:13:49 +01:00
b16e8650da Fixes parallel mutation testing when using Laravel Herd. 2024-09-11 15:11:47 +01:00
c2f30e0148 Fixes parallel mutation testing when using Laravel Herd. 2024-09-11 15:04:44 +01:00
47ce45de56 release: v3.0.4 2024-09-11 00:48:29 +01:00
32881774d2 fix: global afterEach being called twice 2024-09-11 00:40:41 +01:00
ea72461f1b release: v3.0.3 2024-09-10 22:29:09 +01:00
49f15521e0 fix: printer method name 2024-09-10 22:29:01 +01:00
39 changed files with 417 additions and 86 deletions

View File

@ -13,8 +13,8 @@ jobs:
fail-fast: true
matrix:
os: [ubuntu-latest, macos-latest, windows-latest]
symfony: ['7.0']
php: ['8.2', '8.3']
symfony: ['7.1']
php: ['8.2', '8.3', '8.4']
dependency_version: [prefer-lowest, prefer-stable]
name: PHP ${{ matrix.php }} - Symfony ^${{ matrix.symfony }} - ${{ matrix.os }} - ${{ matrix.dependency_version }}
@ -36,7 +36,13 @@ jobs:
echo "::add-matcher::${{ runner.tool_cache }}/phpunit.json"
- name: Install PHP dependencies
run: composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:~${{ matrix.symfony }}"
shell: bash
run: |
if [[ "${{ matrix.php }}" == "8.4" ]]; then
composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}" --ignore-platform-req=php
else
composer update --${{ matrix.dependency_version }} --no-interaction --no-progress --ansi --with="symfony/console:^${{ matrix.symfony }}"
fi
- name: Unit Tests
run: composer test:unit
@ -45,4 +51,5 @@ jobs:
run: composer test:parallel
- name: Integration Tests
if: ${{ matrix.php != '8.4' }}
run: composer test:integration

View File

@ -1,5 +1,7 @@
#!/usr/bin/env php
<?php declare(strict_types=1);
<?php
declare(strict_types=1);
use Pest\Kernel;
use Pest\Panic;
@ -37,7 +39,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
if (str_contains($value, '--test-directory=')) {
unset($arguments[$key]);
} else if ($value === '--test-directory') {
} elseif ($value === '--test-directory') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
@ -62,7 +64,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
if (str_contains($value, '--assignee=')) {
unset($arguments[$key]);
} else if ($value === '--assignee') {
} elseif ($value === '--assignee') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
@ -72,7 +74,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
if (str_contains($value, '--issue=')) {
unset($arguments[$key]);
} else if ($value === '--issue') {
} elseif ($value === '--issue') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
@ -82,7 +84,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
if (str_contains($value, '--ticket=')) {
unset($arguments[$key]);
} else if ($value === '--ticket') {
} elseif ($value === '--ticket') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
@ -92,7 +94,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
if (str_contains($value, '--pr=')) {
unset($arguments[$key]);
} else if ($value === '--pr') {
} elseif ($value === '--pr') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
@ -102,7 +104,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
if (str_contains($value, '--pull-request=')) {
unset($arguments[$key]);
} else if ($value === '--pull-request') {
} elseif ($value === '--pull-request') {
unset($arguments[$key]);
if (isset($arguments[$key + 1])) {
@ -117,7 +119,6 @@ use Symfony\Component\Console\Output\ConsoleOutput;
}
}
// Used when Pest is required using composer.
$vendorPath = dirname(__DIR__, 4).'/vendor/autoload.php';
@ -134,7 +135,7 @@ use Symfony\Component\Console\Output\ConsoleOutput;
// Get $rootPath based on $autoloadPath
$rootPath = dirname($autoloadPath, 2);
$input = new ArgvInput();
$input = new ArgvInput;
$testSuite = TestSuite::getInstance(
$rootPath,
@ -146,11 +147,11 @@ use Symfony\Component\Console\Output\ConsoleOutput;
}
if ($todo) {
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter());
$testSuite->tests->addTestCaseMethodFilter(new TodoTestCaseFilter);
}
if ($notes) {
$testSuite->tests->addTestCaseMethodFilter(new NotesTestCaseFilter());
$testSuite->tests->addTestCaseMethodFilter(new NotesTestCaseFilter);
}
if ($assignee = $input->getParameterOption('--assignee')) {

View File

@ -18,16 +18,16 @@
],
"require": {
"php": "^8.2.0",
"brianium/paratest": "^7.5.4",
"brianium/paratest": "^7.5.5",
"nunomaduro/collision": "^8.4.0",
"nunomaduro/termwind": "^2.1.0",
"pestphp/pest-plugin": "^3.0.0",
"pestphp/pest-plugin-arch": "^3.0.0",
"pestphp/pest-plugin-mutate": "^3.0.3",
"phpunit/phpunit": "^11.3.4"
"pestphp/pest-plugin-mutate": "^3.0.5",
"phpunit/phpunit": "^11.3.6"
},
"conflict": {
"phpunit/phpunit": ">11.3.4",
"phpunit/phpunit": ">11.3.6",
"sebastian/exporter": "<6.0.0",
"webmozart/assert": "<1.11.0"
},
@ -53,8 +53,8 @@
},
"require-dev": {
"pestphp/pest-dev-tools": "^3.0.0",
"pestphp/pest-plugin-type-coverage": "^3.0.0",
"symfony/process": "^7.1.3"
"pestphp/pest-plugin-type-coverage": "^3.0.1",
"symfony/process": "^7.1.5"
},
"minimum-stability": "dev",
"prefer-stable": true,

View File

@ -27,17 +27,20 @@ final class Laravel extends AbstractPreset
->ignoring('App\Enums');
$this->expectations[] = expect('App\Enums')
->toBeEnums();
->toBeEnums()
->ignoring('App\Enums\Concerns');
$this->expectations[] = expect('App\Features')
->toBeClasses();
->toBeClasses()
->ignoring('App\Features\Concerns');
$this->expectations[] = expect('App\Features')
->toHaveMethod('resolve');
$this->expectations[] = expect('App\Exceptions')
->classes()
->toImplement('Throwable');
->toImplement('Throwable')
->ignoring('App\Exceptions\Handler');
$this->expectations[] = expect('App')
->not->toImplement(Throwable::class)
@ -149,7 +152,7 @@ final class Laravel extends AbstractPreset
->toOnlyBeUsedIn('App\Http');
$this->expectations[] = expect('App\Http\Controllers')
->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy']);
->not->toHavePublicMethodsBesides(['__construct', '__invoke', 'index', 'show', 'create', 'store', 'edit', 'update', 'destroy', 'middleware']);
$this->expectations[] = expect([
'dd',

View File

@ -21,6 +21,7 @@ final class Strict extends AbstractPreset
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toHaveProtectedMethods(),
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->not->toBeAbstract(),
fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictTypes(),
fn (Expectation $namespace): ArchExpectation => $namespace->toUseStrictEquality(),
fn (Expectation $namespace): ArchExpectation => $namespace->classes()->toBeFinal(),
);

View File

@ -25,7 +25,7 @@ final class BootOverrides implements Bootstrapper
'8abdad6413329c6fe0d7d44a8b9926e390af32c0b3123f3720bb9c5bbc6fbb7e' => 'TextUI/Output/Default/ProgressPrinter/Subscriber/TestSkippedSubscriber.php',
'43883b7e5811886cf3731c8ed6304d5a77078d9731e1e505abc2da36bde19f3e' => 'TextUI/TestSuiteFilterProcessor.php',
'357d5cd7007f8559b26e1b8cdf43bb6fb15b51b79db981779da6f31b7ec39dad' => 'Event/Value/ThrowableBuilder.php',
'01974a686eba69b5fbb87a904d936eae2176e39567616898c5b758db71d87a22' => 'Logging/JUnit/JunitXmlLogger.php',
'676273f1fe483877cf2d95c5aedbf9ae5d6a8e2f4c12d6ce716df6591e6db023' => 'Logging/JUnit/JunitXmlLogger.php',
];
/**

View File

@ -116,7 +116,7 @@ trait Testable
self::$__latestIssues = $method->issues;
self::$__latestPrs = $method->prs;
$this->__describing = $method->describing;
$this->__test = $method->getClosure($this);
$this->__test = $method->getClosure();
}
}
@ -240,6 +240,8 @@ trait Testable
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
$method->setUp($this);
$description = $method->description;
if ($this->dataName()) {
$description = str_contains((string) $description, ':dataset')
@ -298,6 +300,9 @@ trait Testable
parent::tearDown();
TestSuite::getInstance()->test = null;
$method = TestSuite::getInstance()->tests->get(self::$__filename)->getMethod($this->name());
$method->tearDown($this);
}
}
@ -392,11 +397,12 @@ trait Testable
fn (ReflectionParameter $reflectionParameter): string => $reflectionParameter->getName(),
array_filter($testReflection->getParameters(), fn (ReflectionParameter $reflectionParameter): bool => ! $reflectionParameter->isOptional()),
);
if (array_diff($testParameterNames, $datasetParameterNames) === []) {
return;
}
if (isset($testParameterNames[0])
&& $suppliedParametersCount >= $requiredParametersCount) {
if (isset($testParameterNames[0]) && $suppliedParametersCount >= $requiredParametersCount) {
return;
}

View File

@ -79,11 +79,11 @@ final readonly class Configuration
}
/**
* Gets the theme configuration.
* Gets the printer configuration.
*/
public function theme(): Configuration\Theme
public function printer(): Configuration\Printer
{
return new Configuration\Theme;
return new Configuration\Printer;
}
/**

View File

@ -9,7 +9,7 @@ use NunoMaduro\Collision\Adapters\Phpunit\Printers\DefaultPrinter;
/**
* @internal
*/
final readonly class Theme
final readonly class Printer
{
/**
* Sets the theme to compact.

View File

@ -89,7 +89,7 @@ final class Project
{
$this->issues = "https://{$namespace}.atlassian.net/browse/{$project}-%s";
$this->assignees = "https://{$namespace}.atlassian.net/secure/ViewProfile?name=%s";
$this->assignees = "https://{$namespace}.atlassian.net/secure/ViewProfile.jspa?name=%s";
return $this;
}

View File

@ -223,7 +223,7 @@ final class Expectation
throw new BadMethodCallException('Expectation value is not iterable.');
}
if (count($callbacks) == 0) {
if ($callbacks === []) {
throw new InvalidArgumentException('No sequence expectations defined.');
}
@ -264,7 +264,7 @@ final class Expectation
$matched = false;
foreach ($expressions as $key => $callback) {
if ($subject != $key) {
if ($subject != $key) { // @pest-arch-ignore-line
continue;
}
@ -380,7 +380,7 @@ final class Expectation
if (self::hasExtend($name)) {
$extend = self::$extends[$name]->bindTo($this, Expectation::class);
if ($extend != false) {
if ($extend != false) { // @pest-arch-ignore-line
return $extend;
}
}
@ -509,12 +509,25 @@ final class Expectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => (bool) preg_match('/^<\?php\s+declare\(.*?strict_types\s?=\s?1.*?\);/', (string) file_get_contents($object->path)),
fn (ObjectDescription $object): bool => (bool) preg_match('/^<\?php\s*(\/\*[\s\S]*?\*\/|\/\/[^\r\n]*(?:\r?\n|$)|\s)*declare\s*\(\s*strict_types\s*=\s*1\s*\)\s*;/m', (string) file_get_contents($object->path)),
'to use strict types',
FileLineFinder::where(fn (string $line): bool => str_contains($line, '<?php')),
);
}
/**
* Asserts that the given expectation target uses strict equality.
*/
public function toUseStrictEquality(): ArchExpectation
{
return Targeted::make(
$this,
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' == ') && ! str_contains((string) file_get_contents($object->path), ' != '),
'to use strict equality',
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' == ') || str_contains($line, ' != ')),
);
}
/**
* Asserts that the given expectation target is final.
*/

View File

@ -152,6 +152,19 @@ final readonly class OppositeExpectation
);
}
/**
* Asserts that the given expectation target does not use the strict equality operator.
*/
public function toUseStrictEquality(): ArchExpectation
{
return Targeted::make(
$this->original,
fn (ObjectDescription $object): bool => ! str_contains((string) file_get_contents($object->path), ' === ') && ! str_contains((string) file_get_contents($object->path), ' !== '),
'to use strict equality',
FileLineFinder::where(fn (string $line): bool => str_contains($line, ' === ') || str_contains($line, ' !== ')),
);
}
/**
* Asserts that the given expectation target is not final.
*/

View File

@ -118,9 +118,9 @@ final class TestCaseMethodFactory
}
/**
* Creates the test's closure.
* Sets the test's hooks, and runs any proxy to the test case.
*/
public function getClosure(TestCase $concrete): Closure
public function setUp(TestCase $concrete): void
{
$concrete::flush(); // @phpstan-ignore-line
@ -128,14 +128,29 @@ final class TestCaseMethodFactory
throw ShouldNotHappen::fromMessage('Description can not be empty.');
}
$closure = $this->closure;
$testCase = TestSuite::getInstance()->tests->get($this->filename);
assert($testCase instanceof TestCaseFactory);
$testCase->factoryProxies->proxy($concrete);
$this->factoryProxies->proxy($concrete);
}
/**
* Flushes the test case.
*/
public function tearDown(TestCase $concrete): void
{
$concrete::flush(); // @phpstan-ignore-line
}
/**
* Creates the test's closure.
*/
public function getClosure(): Closure
{
$closure = $this->closure;
$testCase = TestSuite::getInstance()->tests->get($this->filename);
assert($testCase instanceof TestCaseFactory);
$method = $this;
return function (...$arguments) use ($testCase, $method, $closure): mixed { // @phpstan-ignore-line
@ -209,10 +224,8 @@ final class TestCaseMethodFactory
$attributesCode
public function $methodName(...\$arguments)
{
\$test = \Pest\TestSuite::getInstance()->tests->get(self::\$__filename)->getMethod(\$this->name())->getClosure(\$this);
return \$this->__runTest(
\$test,
\$this->__test,
...\$arguments,
);
}

View File

@ -217,7 +217,7 @@ if (! function_exists('afterAll')) {
if (! function_exists('covers')) {
/**
* Specifies which classes, or functions, a test method covers.
* Specifies which classes, or functions, a test case covers.
*
* @param array<int, string>|string $classesOrFunctions
*/
@ -243,3 +243,38 @@ if (! function_exists('covers')) {
}
}
}
if (! function_exists('mutates')) {
/**
* Specifies which classes, enums, or traits a test case mutates.
*
* @param array<int, string>|string $targets
*/
function mutates(array|string ...$targets): void
{
$filename = Backtrace::file();
$beforeEachCall = (new BeforeEachCall(TestSuite::getInstance(), $filename));
$beforeEachCall->group('__pest_mutate_only');
/** @var MutationTestRunner $runner */
$runner = Container::getInstance()->get(MutationTestRunner::class);
/** @var \Pest\Mutate\Repositories\ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$everything = $configurationRepository->cliConfiguration->toArray()['everything'] ?? false;
$classes = $configurationRepository->cliConfiguration->toArray()['classes'] ?? false;
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if ($runner->isEnabled() && ! $everything && ! is_array($classes) && ! is_array($paths)) {
$beforeEachCall->only('__pest_mutate_only');
}
/** @var ConfigurationRepository $configurationRepository */
$configurationRepository = Container::getInstance()->get(ConfigurationRepository::class);
$paths = $configurationRepository->cliConfiguration->toArray()['paths'] ?? false;
if (! is_array($paths)) {
$configurationRepository->globalConfiguration('default')->class(...$targets); // @phpstan-ignore-line
}
}
}

View File

@ -18,6 +18,7 @@ use PHPUnit\Event\Test\Failed;
use PHPUnit\Event\Test\MarkedIncomplete;
use PHPUnit\Event\Test\Skipped;
use PHPUnit\Event\TestSuite\TestSuite;
use PHPUnit\Event\TestSuite\TestSuiteForTestMethodWithDataProvider;
use PHPUnit\Framework\Exception as FrameworkException;
use PHPUnit\TestRunner\TestResult\TestResult as PhpUnitTestResult;
@ -147,6 +148,13 @@ final readonly class Converter
*/
public function getTestSuiteName(TestSuite $testSuite): string
{
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
$firstTest = $this->getFirstTest($testSuite);
if ($firstTest instanceof \PHPUnit\Event\Code\TestMethod) {
return $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
}
}
$name = $testSuite->name();
if (! str_starts_with($name, self::PREFIX)) {
@ -168,6 +176,35 @@ final readonly class Converter
* Gets the test suite location.
*/
public function getTestSuiteLocation(TestSuite $testSuite): ?string
{
$firstTest = $this->getFirstTest($testSuite);
if (! $firstTest instanceof \PHPUnit\Event\Code\TestMethod) {
return null;
}
$path = $firstTest->testDox()->prettifiedClassName();
$classRelativePath = $this->toRelativePath($path);
if ($testSuite instanceof TestSuiteForTestMethodWithDataProvider) {
$methodName = $this->getTestMethodNameWithoutDatasetSuffix($firstTest);
return "$classRelativePath::$methodName";
}
return $classRelativePath;
}
/**
* Gets the prettified test method name without dataset-related suffix.
*/
private function getTestMethodNameWithoutDatasetSuffix(TestMethod $testMethod): string
{
return Str::beforeLast($testMethod->testDox()->prettifiedMethodName(), ' with data set ');
}
/**
* Gets the first test from the test suite.
*/
private function getFirstTest(TestSuite $testSuite): ?TestMethod
{
$tests = $testSuite->tests()->asArray();
@ -181,9 +218,7 @@ final readonly class Converter
throw ShouldNotHappen::fromMessage('Not an instance of TestMethod');
}
$path = $firstTest->testDox()->prettifiedClassName();
return $this->toRelativePath($path);
return $firstTest;
}
/**

View File

@ -38,7 +38,7 @@ final class ServiceMessage
{
return new self('testSuiteStarted', [
'name' => $name,
'locationHint' => $location === null ? null : "file://$location",
'locationHint' => $location === null ? null : "pest_qn://$location",
]);
}

View File

@ -5,6 +5,7 @@ declare(strict_types=1);
namespace Pest\PendingCalls;
use Closure;
use Pest\Concerns\Testable;
use Pest\Exceptions\InvalidArgumentException;
use Pest\Exceptions\TestDescriptionMissing;
use Pest\Factories\Attribute;
@ -25,9 +26,9 @@ use PHPUnit\Framework\TestCase;
/**
* @internal
*
* @mixin HigherOrderCallables|TestCase
* @mixin HigherOrderCallables|TestCase|Testable
*/
final class TestCall
final class TestCall // @phpstan-ignore-line
{
use Describable;
@ -358,8 +359,8 @@ final class TestCall
public function todo(// @phpstan-ignore-line
array|string|null $note = null,
array|string|null $assignee = null,
array|string|null $issue = null,
array|string|null $pr = null,
array|string|int|null $issue = null,
array|string|int|null $pr = null,
): self {
$this->skip('__TODO__');
@ -390,8 +391,8 @@ final class TestCall
public function wip(// @phpstan-ignore-line
array|string|null $note = null,
array|string|null $assignee = null,
array|string|null $issue = null,
array|string|null $pr = null,
array|string|int|null $issue = null,
array|string|int|null $pr = null,
): self {
if ($issue !== null) {
$this->issue($issue);
@ -418,8 +419,8 @@ final class TestCall
public function done(// @phpstan-ignore-line
array|string|null $note = null,
array|string|null $assignee = null,
array|string|null $issue = null,
array|string|null $pr = null,
array|string|int|null $issue = null,
array|string|int|null $pr = null,
): self {
if ($issue !== null) {
$this->issue($issue);

View File

@ -54,7 +54,7 @@ final class UsesCall
}
/**
* @deprecated Use `pest()->theme()->compact()` instead.
* @deprecated Use `pest()->printer()->compact()` instead.
*/
public function compact(): self
{

View File

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

View File

@ -122,6 +122,9 @@ final class WrapperRunner implements RunnerInterface
$parameters = array_merge($parameters, $options->passthruPhp);
}
/** @var array<int, non-empty-string> $parameters */
$parameters = $this->handleLaravelHerd($parameters);
$parameters[] = $wrapper;
$this->parameters = $parameters;
@ -153,6 +156,21 @@ final class WrapperRunner implements RunnerInterface
return $this->complete($result);
}
/**
* Handles Laravel Herd's debug and coverage modes.
*
* @param array<string> $parameters
* @return array<string>
*/
private function handleLaravelHerd(array $parameters): array
{
if (isset($_ENV['HERD_DEBUG_INI'])) {
return array_merge($parameters, ['-c', $_ENV['HERD_DEBUG_INI']]);
}
return $parameters;
}
private function startWorkers(): void
{
for ($token = 1; $token <= $this->options->processes; $token++) {

View File

@ -20,13 +20,13 @@ final class Closure
*/
public static function bind(?BaseClosure $closure, ?object $newThis, object|string|null $newScope = 'static'): BaseClosure
{
if ($closure == null) {
if (! $closure instanceof \Closure) {
throw ShouldNotHappen::fromMessage('Could not bind null closure.');
}
$closure = BaseClosure::bind($closure, $newThis, $newScope);
if ($closure == false) {
if (! $closure instanceof \Closure) {
throw ShouldNotHappen::fromMessage('Could not bind closure.');
}

View File

@ -1,5 +1,5 @@
Pest Testing Framework 3.0.1.
Pest Testing Framework 3.2.5.
USAGE: pest <file> [options]

View File

@ -1,3 +1,3 @@
Pest Testing Framework 3.0.1.
Pest Testing Framework 3.2.5.

View File

@ -1,4 +1,4 @@
##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='file://tests/.tests/Failure.php' flowId='1234']
##teamcity[testSuiteStarted name='Tests/tests/Failure' locationHint='pest_qn://tests/.tests/Failure.php' flowId='1234']
##teamcity[testCount count='8' 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 tests/.tests/Failure.php:6' type='comparisonFailure' actual='true' expected='false' flowId='1234']

View File

@ -1,11 +1,15 @@
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='file://tests/.tests/SuccessOnly.php' flowId='1234']
##teamcity[testCount count='2' flowId='1234']
##teamcity[testSuiteStarted name='Tests/tests/SuccessOnly' locationHint='pest_qn://tests/.tests/SuccessOnly.php' flowId='1234']
##teamcity[testCount count='3' 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[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[testSuiteStarted name='can pass with dataset' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset' flowId='1234']
##teamcity[testStarted name='can pass with dataset with data set "(true)"' locationHint='pest_qn://tests/.tests/SuccessOnly.php::can pass with dataset with data set "(true)"' flowId='1234']
##teamcity[testFinished name='can pass with dataset with data set "(true)"' duration='100000' flowId='1234']
##teamcity[testSuiteFinished name='can pass with dataset' flowId='1234']
##teamcity[testSuiteFinished name='Tests/tests/SuccessOnly' flowId='1234']
Tests: 2 passed (2 assertions)
Tests: 3 passed (3 assertions)
Duration: 1.00s

View File

@ -968,6 +968,16 @@
✓ it can handle a non-defined exception
✓ it can handle a class not found Error
PASS Tests\Features\Expect\toUseStrictEquality
✓ missing strict equality
✓ has strict equality
✓ opposite missing strict equality
✓ opposite has strict equality
PASS Tests\Features\Expect\toUseStrictTypes
✓ pass
✓ failures
PASS Tests\Features\Expect\toUseTrait
✓ pass
✓ failures
@ -1305,6 +1315,7 @@
✓ it executes tests in the Helpers directory
PASS Tests\Hooks\AfterEachTest
✓ nested → nested afterEach execution order
✓ global afterEach execution order
PASS Tests\Hooks\BeforeEachTest
@ -1381,7 +1392,7 @@
✓ it proxies to uses call
PASS Tests\Unit\Configuration\Theme
✓ it creates a theme instance
✓ it creates a printer instance
PASS Tests\Unit\Console\Help
✓ it outputs the help information when --help is used
@ -1573,4 +1584,4 @@
WARN Tests\Visual\Version
- visual snapshot of help command output
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 28 skipped, 1088 passed (2615 assertions)
Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 28 skipped, 1095 passed (2648 assertions)

View File

@ -9,3 +9,7 @@ it('can pass with comparison', function () {
test('can also pass', function () {
expect("string")->toBeString();
});
test('can pass with dataset', function ($value) {
expect($value)->toEqual(true);
})->with([true]);

View File

@ -0,0 +1,21 @@
<?php
use Pest\Arch\Exceptions\ArchExpectationFailedException;
test('missing strict equality')
->throws(ArchExpectationFailedException::class)
->expect('Tests\\Fixtures\\Arch\\ToUseStrictEquality\\NotStrictEquality')
->toUseStrictEquality();
test('has strict equality')
->expect('Tests\\Fixtures\\Arch\\ToUseStrictEquality\\StrictEquality')
->toUseStrictEquality();
test('opposite missing strict equality')
->throws(ArchExpectationFailedException::class)
->expect('Tests\\Fixtures\\Arch\\ToUseStrictEquality\\StrictEquality')
->not->toUseStrictEquality();
test('opposite has strict equality')
->expect('Tests\\Fixtures\\Arch\\ToUseStrictEquality\\NotStrictEquality')
->not->toUseStrictEquality();

View File

@ -0,0 +1,17 @@
<?php
declare(strict_types=1);
use Pest\Arch\Exceptions\ArchExpectationFailedException;
use Tests\Fixtures\Arch\ToUseStrictTypes\HasNoStrictType;
use Tests\Fixtures\Arch\ToUseStrictTypes\HasStrictType;
use Tests\Fixtures\Arch\ToUseStrictTypes\HasStrictTypeWithCommentsAbove;
test('pass', function () {
expect(HasStrictType::class)->toUseStrictTypes()
->and(HasStrictTypeWithCommentsAbove::class)->toUseStrictTypes();
});
test('failures', function () {
expect(HasNoStrictType::class)->toUseStrictTypes();
})->throws(ArchExpectationFailedException::class);

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Tests\Fixtures\Arch\ToUseStrictEquality;
class NotStrictEquality
{
public function test(): void
{
$a = 1;
$b = '1';
if ($a == $b) {
echo 'Equal';
}
if ($a != $b) {
echo 'Equal';
}
}
}

View File

@ -0,0 +1,22 @@
<?php
declare(strict_types=1);
namespace Tests\Fixtures\Arch\ToUseStrictEquality;
class StrictEquality
{
public function test(): void
{
$a = 1;
$b = '1';
if ($a === $b) {
echo 'Equal';
}
if ($a !== $b) {
echo 'Equal';
}
}
}

View File

@ -0,0 +1,5 @@
<?php
namespace Tests\Fixtures\Arch\ToUseStrictTypes;
class HasNoStrictType {}

View File

@ -0,0 +1,7 @@
<?php
declare(strict_types=1);
namespace Tests\Fixtures\Arch\ToUseStrictTypes;
class HasStrictType {}

View File

@ -0,0 +1,11 @@
<?php
/** @noinspection PhpUnused */
// some other comment
declare(strict_types=1);
namespace Tests\Fixtures\Arch\ToUseStrictTypes;
class HasStrictTypeWithCommentsAbove {}

View File

@ -1,23 +1,79 @@
<?php
beforeEach(function () {
$this->ith = 0;
});
pest()->afterEach(function () {
expect($this)
->toHaveProperty('ith')
->and($this->ith)
->toBe(1);
->toBe(3);
$this->ith = 2;
$this->ith++;
});
pest()->afterEach(function () {
expect($this)
->toHaveProperty('ith')
->and($this->ith)
->toBe(4);
$this->ith++;
});
afterEach(function () {
expect($this)
->toHaveProperty('ith')
->and($this->ith)
->toBe(2);
->toBe(5);
$this->ith++;
});
describe('nested', function () {
afterEach(function () {
expect($this)
->toHaveProperty('ith')
->and($this->ith)
->toBe(6);
$this->ith++;
});
test('nested afterEach execution order', function () {
expect($this)
->toHaveProperty('ith')
->and($this->ith)
->toBe(0);
$this->ith++;
});
afterEach(function () {
expect($this)
->toHaveProperty('ith')
->and($this->ith)
->toBe(7);
$this->ith++;
});
});
afterEach(function () {
expect($this)
->toHaveProperty('ith')
->and($this->ith)
->toBeBetween(6, 8);
$this->ith++;
});
test('global afterEach execution order', function () {
expect($this)
->not()
->toHaveProperty('ith');
->toHaveProperty('ith')
->and($this->ith)
->toBe(0);
$this->ith++;
});

View File

@ -32,7 +32,12 @@ pest()
$_SERVER['globalHook']->calls->beforeAll++;
})
->afterEach(function () {
$this->ith = 0;
if (! isset($this->ith)) {
return;
}
assert($this->ith === 1, 'Expected $this->ith to be 1, but got '.$this->ith);
$this->ith++;
})
->afterAll(function () {
$_SERVER['globalHook']->afterAll = 0;
@ -57,12 +62,12 @@ pest()->in('Hooks')
$_SERVER['globalHook']->beforeAll = 1;
})
->afterEach(function () {
expect($this)
->toHaveProperty('ith')
->and($this->ith)
->toBe(0);
if (! isset($this->ith)) {
return;
}
$this->ith = 1;
assert($this->ith === 2, 'Expected $this->ith to be 1, but got '.$this->ith);
$this->ith++;
})
->afterAll(function () {
expect($_SERVER['globalHook'])

View File

@ -1,7 +1,7 @@
<?php
it('creates a theme instance', function () {
$theme = pest()->theme();
it('creates a printer instance', function () {
$theme = pest()->printer();
expect($theme)->toBeInstanceOf(Pest\Configuration\Theme::class);
expect($theme)->toBeInstanceOf(Pest\Configuration\Printer::class);
});

View File

@ -36,8 +36,8 @@ test('junit output', function () use ($normalizedPath, $run) {
expect($result['testsuite']['@attributes'])
->name->toBe('Tests\tests\SuccessOnly')
->file->toBe($normalizedPath('tests/.tests/SuccessOnly.php'))
->tests->toBe('2')
->assertions->toBe('2')
->tests->toBe('3')
->assertions->toBe('3')
->errors->toBe('0')
->failures->toBe('0')
->skipped->toBe('0');

View File

@ -16,7 +16,7 @@ $run = function () {
test('parallel', function () use ($run) {
expect($run('--exclude-group=integration'))
->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 19 skipped, 1078 passed (2591 assertions)')
->toContain('Tests: 2 deprecated, 4 warnings, 5 incomplete, 2 notices, 17 todos, 19 skipped, 1085 passed (2624 assertions)')
->toContain('Parallel: 3 processes');
})->skipOnWindows();