diff --git a/.gitattributes b/.gitattributes
index 3654b689..c6e004ff 100644
--- a/.gitattributes
+++ b/.gitattributes
@@ -12,3 +12,4 @@ phpstan.neon export-ignore
CHANGELOG.md export-ignore
CONTRIBUTING.md export-ignore
README.md export-ignore
+
diff --git a/.github/workflows/static.yml b/.github/workflows/static.yml
index eb2596ec..0a8089f6 100644
--- a/.github/workflows/static.yml
+++ b/.github/workflows/static.yml
@@ -28,5 +28,8 @@ jobs:
- name: Types
run: composer test:types
+ - name: Refacto
+ run: composer test:refacto
+
- name: Style
run: composer test:lint
diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml
index 2a855c50..d45b90ee 100644
--- a/.github/workflows/tests.yml
+++ b/.github/workflows/tests.yml
@@ -7,7 +7,7 @@ jobs:
runs-on: ${{ matrix.os }}
strategy:
matrix:
- os: [ubuntu-latest] # (macos-latest, windows-latest) 2.x-dev is under development
+ os: [ubuntu-latest, macos-latest] # "windows-latest" is waiting for https://github.com/pestphp/pest/issues/638
php: ['8.1', '8.2']
dependency-version: [prefer-lowest, prefer-stable]
parallel: ['', '--parallel']
@@ -38,7 +38,7 @@ jobs:
- name: Unit Tests
run: test:parallel
- if: ${{ false }} # 2.x-dev is under development
+ if: ${{ false }} # Parallel's not yet supported.
- name: Integration Tests
run: composer test:integration
diff --git a/README.md b/README.md
index 4febffd5..151869b7 100644
--- a/README.md
+++ b/README.md
@@ -5,7 +5,7 @@
-
+
diff --git a/TODO.md b/TODO.md
deleted file mode 100644
index 58d25d72..00000000
--- a/TODO.md
+++ /dev/null
@@ -1,5 +0,0 @@
-1. TeamCity. (oliver)
-2. Junit. (nuno)
-3. Check all plugins. (nuno - almost done)
-4. Parallel testing. (luke)
-5. Finish Collision's todo. (nuno)
diff --git a/composer.json b/composer.json
index 31b8bdb7..557eebcf 100644
--- a/composer.json
+++ b/composer.json
@@ -19,7 +19,7 @@
"require": {
"php": "^8.1.0",
"nunomaduro/collision": "^7.0.0",
- "nunomaduro/termwind": "^1.14.2",
+ "nunomaduro/termwind": "^1.15",
"pestphp/pest-plugin": "^2.0.0",
"phpunit/phpunit": "10.0.x-dev"
},
@@ -27,15 +27,7 @@
"psr-4": {
"Pest\\": "src/"
},
- "exclude-from-classmap": [
- "../phpunit/phpunit/src/Runner/Filter/NameFilterIterator.php",
- "vendor/phpunit/phpunit/src/Runner/Filter/NameFilterIterator.php",
- "../phpunit/src/Runner/TestSuiteLoader.php",
- "vendor/phpunit/phpunit/src/Runner/TestSuiteLoader.php"
- ],
"files": [
- "overrides/Runner/Filter/NameFilterIterator.php",
- "overrides/Runner/TestSuiteLoader.php",
"src/Functions.php",
"src/Pest.php"
]
@@ -50,7 +42,8 @@
]
},
"require-dev": {
- "pestphp/pest-dev-tools": "^2.0.0",
+ "pestphp/pest-dev-tools": "^2.1.0",
+ "pestphp/pest-plugin-arch": "^2.0.0",
"symfony/process": "^6.2.0"
},
"minimum-stability": "dev",
@@ -66,14 +59,18 @@
"bin/pest"
],
"scripts": {
+ "refacto": "rector",
"lint": "pint",
+ "test:refacto": "rector --dry-run",
"test:lint": "pint --test",
"test:types": "phpstan analyse --ansi --memory-limit=-1 --debug",
"test:unit": "php bin/pest --colors=always --exclude-group=integration --compact",
+ "test:inline": "php bin/pest --colors=always --configuration=phpunit.inline.xml",
"test:parallel": "exit 1",
"test:integration": "php bin/pest --colors=always --group=integration -v",
"update:snapshots": "REBUILD_SNAPSHOTS=true php bin/pest --colors=always",
"test": [
+ "@test:refacto",
"@test:lint",
"@test:types",
"@test:unit",
diff --git a/overrides/Runner/TestSuiteLoader.php b/overrides/Runner/TestSuiteLoader.php
index 0719288b..6e9df439 100644
--- a/overrides/Runner/TestSuiteLoader.php
+++ b/overrides/Runner/TestSuiteLoader.php
@@ -98,14 +98,14 @@ final class TestSuiteLoader
self::$loadedClasses = array_merge($loadedClasses, self::$loadedClasses);
- if (empty(self::$loadedClasses)) {
+ if (empty($loadedClasses)) {
return $this->exceptionFor($suiteClassName, $suiteClassFile);
}
$testCaseFound = false;
- foreach (self::$loadedClasses as $loadedClass) {
- if (is_subclass_of($loadedClass, HasPrintableTestCaseName::class)) {
+ foreach (array_reverse($loadedClasses) as $loadedClass) {
+ if (is_subclass_of($loadedClass, HasPrintableTestCaseName::class) || is_subclass_of($loadedClass, TestCase::class)) {
$suiteClassName = $loadedClass;
$testCaseFound = true;
@@ -115,13 +115,7 @@ final class TestSuiteLoader
}
if (! $testCaseFound) {
- foreach (self::$loadedClasses as $loadedClass) {
- if (is_subclass_of($loadedClass, TestCase::class)) {
- $suiteClassName = $loadedClass;
-
- break;
- }
- }
+ return $this->exceptionFor($suiteClassName, $suiteClassFile);
}
if (! class_exists($suiteClassName, false)) {
diff --git a/phpstan.neon b/phpstan.neon
index d8b81f41..e74b4290 100644
--- a/phpstan.neon
+++ b/phpstan.neon
@@ -16,7 +16,6 @@ parameters:
- "#with a nullable type declaration#"
- "#type mixed is not subtype of native#"
- "#is not allowed to extend#"
- - "#Language construct eval#"
- "# with null as default value#"
- "#has parameter \\$closure with default value.#"
- "#has parameter \\$description with default value.#"
diff --git a/rector.php b/rector.php
new file mode 100644
index 00000000..589b8cd0
--- /dev/null
+++ b/rector.php
@@ -0,0 +1,27 @@
+paths([
+ __DIR__.'/src',
+ ]);
+
+ $rectorConfig->rules([
+ InlineConstructorDefaultToPropertyRector::class,
+ ]);
+
+ $rectorConfig->sets([
+ LevelSetList::UP_TO_PHP_81,
+ SetList::CODE_QUALITY,
+ SetList::DEAD_CODE,
+ SetList::EARLY_RETURN,
+ SetList::TYPE_DECLARATION,
+ SetList::PRIVATIZATION,
+ ]);
+};
diff --git a/src/Bootstrappers/BootExceptionHandler.php b/src/Bootstrappers/BootExceptionHandler.php
index a504ee7e..99119f41 100644
--- a/src/Bootstrappers/BootExceptionHandler.php
+++ b/src/Bootstrappers/BootExceptionHandler.php
@@ -5,16 +5,17 @@ declare(strict_types=1);
namespace Pest\Bootstrappers;
use NunoMaduro\Collision;
+use Pest\Contracts\Bootstrapper;
/**
* @internal
*/
-final class BootExceptionHandler
+final class BootExceptionHandler implements Bootstrapper
{
/**
* Boots the Exception Handler.
*/
- public function __invoke(): void
+ public function boot(): void
{
$handler = new Collision\Provider();
diff --git a/src/Bootstrappers/BootFiles.php b/src/Bootstrappers/BootFiles.php
index 765d0843..44235b80 100644
--- a/src/Bootstrappers/BootFiles.php
+++ b/src/Bootstrappers/BootFiles.php
@@ -4,16 +4,19 @@ declare(strict_types=1);
namespace Pest\Bootstrappers;
+use Pest\Contracts\Bootstrapper;
+use Pest\Support\DatasetInfo;
use Pest\Support\Str;
use function Pest\testDirectory;
use Pest\TestSuite;
use RecursiveDirectoryIterator;
use RecursiveIteratorIterator;
+use SebastianBergmann\FileIterator\Facade as PhpUnitFileIterator;
/**
* @internal
*/
-final class BootFiles
+final class BootFiles implements Bootstrapper
{
/**
* The Pest convention.
@@ -21,8 +24,6 @@ final class BootFiles
* @var array
*/
private const STRUCTURE = [
- 'Datasets',
- 'Datasets.php',
'Expectations',
'Expectations.php',
'Helpers',
@@ -33,7 +34,7 @@ final class BootFiles
/**
* Boots the Subscribers.
*/
- public function __invoke(): void
+ public function boot(): void
{
$rootPath = TestSuite::getInstance()->rootPath;
$testsPath = $rootPath.DIRECTORY_SEPARATOR.testDirectory();
@@ -56,6 +57,8 @@ final class BootFiles
$this->load($filename);
}
}
+
+ $this->bootDatasets($testsPath);
}
/**
@@ -73,4 +76,15 @@ final class BootFiles
include_once $filename;
}
+
+ private function bootDatasets(string $testsPath): void
+ {
+ $files = (new PhpUnitFileIterator)->getFilesAsArray($testsPath, '.php');
+
+ foreach ($files as $file) {
+ if (DatasetInfo::isADatasetsFile($file) || DatasetInfo::isInsideADatasetsDirectory($file)) {
+ $this->load($file);
+ }
+ }
+ }
}
diff --git a/src/Bootstrappers/BootOverrides.php b/src/Bootstrappers/BootOverrides.php
new file mode 100644
index 00000000..9c637d0f
--- /dev/null
+++ b/src/Bootstrappers/BootOverrides.php
@@ -0,0 +1,40 @@
+
+ */
+ private const FILES = [
+ 'Runner/Filter/NameFilterIterator.php',
+ 'Runner/TestSuiteLoader.php',
+ ];
+
+ /**
+ * Boots the Subscribers.
+ */
+ public function boot(): void
+ {
+ foreach (self::FILES as $file) {
+ $file = __DIR__."/../../overrides/$file";
+
+ if (! file_exists($file)) {
+ throw ShouldNotHappen::fromMessage(sprintf('File [%s] does not exist.', $file));
+ }
+
+ require_once $file;
+ }
+ }
+}
diff --git a/src/Bootstrappers/BootSubscribers.php b/src/Bootstrappers/BootSubscribers.php
index 815c7821..ab31e337 100644
--- a/src/Bootstrappers/BootSubscribers.php
+++ b/src/Bootstrappers/BootSubscribers.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest\Bootstrappers;
+use Pest\Contracts\Bootstrapper;
use Pest\Subscribers;
use PHPUnit\Event;
use PHPUnit\Event\Subscriber;
@@ -11,7 +12,7 @@ use PHPUnit\Event\Subscriber;
/**
* @internal
*/
-final class BootSubscribers
+final class BootSubscribers implements Bootstrapper
{
/**
* The Kernel subscribers.
@@ -22,13 +23,14 @@ final class BootSubscribers
Subscribers\EnsureConfigurationIsValid::class,
Subscribers\EnsureConfigurationDefaults::class,
Subscribers\EnsureRetryRepositoryExists::class,
- Subscribers\EnsureFailedTestsAreStoredForRetry::class,
+ Subscribers\EnsureErroredTestsAreRetryable::class,
+ Subscribers\EnsureFailedTestsAreRetryable::class,
];
/**
* Boots the Subscribers.
*/
- public function __invoke(): void
+ public function boot(): void
{
foreach (self::SUBSCRIBERS as $subscriber) {
Event\Facade::registerSubscriber(
diff --git a/src/Bootstrappers/BootView.php b/src/Bootstrappers/BootView.php
index 226b7982..2ae4c509 100644
--- a/src/Bootstrappers/BootView.php
+++ b/src/Bootstrappers/BootView.php
@@ -4,13 +4,14 @@ declare(strict_types=1);
namespace Pest\Bootstrappers;
+use Pest\Contracts\Bootstrapper;
use Pest\Support\View;
use Symfony\Component\Console\Output\OutputInterface;
/**
* @internal
*/
-final class BootView
+final class BootView implements Bootstrapper
{
public function __construct(
private readonly OutputInterface $output
@@ -21,7 +22,7 @@ final class BootView
/**
* Boots the view renderer.
*/
- public function __invoke(): void
+ public function boot(): void
{
View::renderUsing($this->output);
}
diff --git a/src/Concerns/Pipeable.php b/src/Concerns/Pipeable.php
index 0a29d6a1..c23427cd 100644
--- a/src/Concerns/Pipeable.php
+++ b/src/Concerns/Pipeable.php
@@ -58,6 +58,6 @@ trait Pipeable
*/
private function pipes(string $name, object $context, string $scope): array
{
- return array_map(fn (Closure $pipe) => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []);
+ return array_map(fn (Closure $pipe): \Closure => $pipe->bindTo($context, $scope), self::$pipes[$name] ?? []);
}
}
diff --git a/src/Concerns/Retrievable.php b/src/Concerns/Retrievable.php
index a2716887..a95031b1 100644
--- a/src/Concerns/Retrievable.php
+++ b/src/Concerns/Retrievable.php
@@ -13,7 +13,6 @@ trait Retrievable
* @template TRetrievableValue
*
* Safely retrieve the value at the given key from an object or array.
- *
* @template TRetrievableValue
*
* @param array|object $value
diff --git a/src/Concerns/Testable.php b/src/Concerns/Testable.php
index 36d47e38..f7075c28 100644
--- a/src/Concerns/Testable.php
+++ b/src/Concerns/Testable.php
@@ -259,7 +259,7 @@ trait Testable
*/
private function __callClosure(Closure $closure, array $arguments): mixed
{
- return ExceptionTrace::ensure(fn () => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
+ return ExceptionTrace::ensure(fn (): mixed => call_user_func_array(Closure::bind($closure, $this, $this::class), $arguments));
}
/**
diff --git a/src/Console/Thanks.php b/src/Console/Thanks.php
index 10583b41..cd149a0d 100644
--- a/src/Console/Thanks.php
+++ b/src/Console/Thanks.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest\Console;
+use Pest\Bootstrappers\BootView;
use Pest\Support\View;
use Symfony\Component\Console\Helper\SymfonyQuestionHelper;
use Symfony\Component\Console\Input\ArrayInput;
@@ -39,6 +40,9 @@ final class Thanks
*/
public function __invoke(): void
{
+ $bootstrapper = new BootView($this->output);
+ $bootstrapper->boot();
+
$wantsToSupport = (new SymfonyQuestionHelper())->ask(
new ArrayInput([]),
$this->output,
diff --git a/src/Contracts/Bootstrapper.php b/src/Contracts/Bootstrapper.php
new file mode 100644
index 00000000..520aede1
--- /dev/null
+++ b/src/Contracts/Bootstrapper.php
@@ -0,0 +1,16 @@
+ $methods
+ *
+ * @throws self
+ */
+ public static function fromMethods(array $methods): never
+ {
+ throw new self(sprintf('Expectation [%s] is not valid.', implode('->', $methods)));
+ }
+}
diff --git a/src/Exceptions/InvalidExpectationValue.php b/src/Exceptions/InvalidExpectationValue.php
index 68a15c64..7a223f8c 100644
--- a/src/Exceptions/InvalidExpectationValue.php
+++ b/src/Exceptions/InvalidExpectationValue.php
@@ -12,11 +12,9 @@ use InvalidArgumentException;
final class InvalidExpectationValue extends InvalidArgumentException
{
/**
- * @return never
- *
* @throws self
*/
- public static function expected(string $type): void
+ public static function expected(string $type): never
{
throw new self(sprintf('Invalid expectation value type. Expected [%s].', $type));
}
diff --git a/src/Expectation.php b/src/Expectation.php
index 59d5f573..4d5e001c 100644
--- a/src/Expectation.php
+++ b/src/Expectation.php
@@ -6,10 +6,18 @@ namespace Pest;
use BadMethodCallException;
use Closure;
+use Pest\Arch\Contracts\ArchExpectation;
+use Pest\Arch\Expectations\ToBeUsedOn;
+use Pest\Arch\Expectations\ToBeUsedOnNothing;
+use Pest\Arch\Expectations\ToOnlyBeUsedOn;
+use Pest\Arch\Expectations\ToOnlyUse;
+use Pest\Arch\Expectations\ToUse;
+use Pest\Arch\Expectations\ToUseNothing;
use Pest\Concerns\Extendable;
use Pest\Concerns\Pipeable;
use Pest\Concerns\Retrievable;
use Pest\Exceptions\ExpectationNotFound;
+use Pest\Exceptions\InvalidExpectation;
use Pest\Exceptions\InvalidExpectationValue;
use Pest\Expectations\EachExpectation;
use Pest\Expectations\HigherOrderExpectation;
@@ -24,7 +32,7 @@ use PHPUnit\Framework\ExpectationFailedException;
*
* @template TValue
*
- * @property Expectation $not Creates the opposite expectation.
+ * @property OppositeExpectation $not Creates the opposite expectation.
* @property EachExpectation $each Creates an expectation on each element on the traversable value.
*
* @mixin Mixins\Expectation
@@ -70,10 +78,12 @@ final class Expectation
InvalidExpectationValue::expected('string');
}
- /** @var array|bool $value */
- $value = json_decode($this->value, true, 512);
+ $this->toBeJson();
- return $this->toBeJson()->and($value);
+ /** @var array|bool $value */
+ $value = json_decode($this->value, true, 512, JSON_THROW_ON_ERROR);
+
+ return $this->and($value);
}
/**
@@ -348,4 +358,65 @@ final class Expectation
{
return new Any();
}
+
+ /**
+ * Asserts that the given expectation target use the given dependencies.
+ *
+ * @param array|string $targets
+ */
+ public function toUse(array|string $targets): ArchExpectation
+ {
+ return ToUse::make($this, $targets);
+ }
+
+ /**
+ * Asserts that the given expectation target "only" use on the given dependencies.
+ *
+ * @param array|string $targets
+ */
+ public function toOnlyUse(array|string $targets): ArchExpectation
+ {
+ return ToOnlyUse::make($this, $targets);
+ }
+
+ /**
+ * Asserts that the given expectation target does not use any dependencies.
+ */
+ public function toUseNothing(): ArchExpectation
+ {
+ return ToUseNothing::make($this);
+ }
+
+ public function toBeUsed(): never
+ {
+ throw InvalidExpectation::fromMethods(['toBeUsed']);
+ }
+
+ /**
+ * Asserts that the given expectation dependency is used by the given targets.
+ *
+ * @param array|string $targets
+ */
+ public function toBeUsedOn(array|string $targets): ArchExpectation
+ {
+ return ToBeUsedOn::make($this, $targets);
+ }
+
+ /**
+ * Asserts that the given expectation dependency is "only" used by the given targets.
+ *
+ * @param array|string $targets
+ */
+ public function toOnlyBeUsedOn(array|string $targets): ArchExpectation
+ {
+ return ToOnlyBeUsedOn::make($this, $targets);
+ }
+
+ /**
+ * Asserts that the given expectation dependency is not used.
+ */
+ public function toBeUsedOnNothing(): ArchExpectation
+ {
+ return ToBeUsedOnNothing::make($this);
+ }
}
diff --git a/src/Expectations/OppositeExpectation.php b/src/Expectations/OppositeExpectation.php
index beed91a8..2c9e53a2 100644
--- a/src/Expectations/OppositeExpectation.php
+++ b/src/Expectations/OppositeExpectation.php
@@ -4,6 +4,13 @@ declare(strict_types=1);
namespace Pest\Expectations;
+use Pest\Arch\Contracts\ArchExpectation;
+use Pest\Arch\Expectations\ToBeUsedOn;
+use Pest\Arch\Expectations\ToBeUsedOnNothing;
+use Pest\Arch\Expectations\ToUse;
+use Pest\Arch\GroupArchExpectation;
+use Pest\Arch\SingleArchExpectation;
+use Pest\Exceptions\InvalidExpectation;
use Pest\Expectation;
use Pest\Support\Arr;
use PHPUnit\Framework\ExpectationFailedException;
@@ -52,6 +59,64 @@ final class OppositeExpectation
return $this->original;
}
+ /**
+ * Asserts that the given expectation target does not use any of the given dependencies.
+ *
+ * @param array|string $targets
+ */
+ public function toUse(array|string $targets): ArchExpectation
+ {
+ return GroupArchExpectation::fromExpectations($this->original, array_map(fn (string $target): SingleArchExpectation => ToUse::make($this->original, $target)->opposite(
+ fn () => $this->throwExpectationFailedException('toUse', $target),
+ ), is_string($targets) ? [$targets] : $targets));
+ }
+
+ /**
+ * @param array|string $targets
+ */
+ public function toOnlyUse(array|string $targets): never
+ {
+ throw InvalidExpectation::fromMethods(['not', 'toOnlyUse']);
+ }
+
+ public function toUseNothing(): never
+ {
+ throw InvalidExpectation::fromMethods(['not', 'toUseNothing']);
+ }
+
+ /**
+ * Asserts that the given expectation dependency is not used.
+ */
+ public function toBeUsed(): ArchExpectation
+ {
+ return ToBeUsedOnNothing::make($this->original);
+ }
+
+ /**
+ * Asserts that the given expectation dependency is not used by any of the given targets.
+ *
+ * @param array|string $targets
+ */
+ public function toBeUsedOn(array|string $targets): ArchExpectation
+ {
+ return GroupArchExpectation::fromExpectations($this->original, array_map(fn (string $target): GroupArchExpectation => ToBeUsedOn::make($this->original, $target)->opposite(
+ fn () => $this->throwExpectationFailedException('toBeUsedOn', $target),
+ ), is_string($targets) ? [$targets] : $targets));
+ }
+
+ public function toOnlyBeUsedOn(): never
+ {
+ throw InvalidExpectation::fromMethods(['not', 'toOnlyBeUsedOn']);
+ }
+
+ /**
+ * Asserts that the given expectation dependency is not used.
+ */
+ public function toBeUsedOnNothing(): never
+ {
+ throw InvalidExpectation::fromMethods(['not', 'toBeUsedOnNothing']);
+ }
+
/**
* Handle dynamic method calls into the original expectation.
*
@@ -89,11 +154,12 @@ final class OppositeExpectation
/**
* Creates a new expectation failed exception with a nice readable message.
*
- * @param array $arguments
- * @return never
+ * @param array|string $arguments
*/
- private function throwExpectationFailedException(string $name, array $arguments = []): void
+ public function throwExpectationFailedException(string $name, array|string $arguments = []): never
{
+ $arguments = is_array($arguments) ? $arguments : [$arguments];
+
$exporter = new Exporter();
$toString = fn ($argument): string => $exporter->shortenedExport($argument);
diff --git a/src/Factories/Attributes/Attribute.php b/src/Factories/Attributes/Attribute.php
index 70dae523..9bd43de9 100644
--- a/src/Factories/Attributes/Attribute.php
+++ b/src/Factories/Attributes/Attribute.php
@@ -13,10 +13,8 @@ abstract class Attribute
{
/**
* Determine if the attribute should be placed above the class instead of above the method.
- *
- * @var bool
*/
- public const ABOVE_CLASS = false;
+ public static bool $above = false;
/**
* @param array $attributes
diff --git a/src/Factories/Attributes/Covers.php b/src/Factories/Attributes/Covers.php
index da07b2a1..21d1f857 100644
--- a/src/Factories/Attributes/Covers.php
+++ b/src/Factories/Attributes/Covers.php
@@ -15,10 +15,8 @@ final class Covers extends Attribute
{
/**
* Determine if the attribute should be placed above the classe instead of above the method.
- *
- * @var bool
*/
- public const ABOVE_CLASS = true;
+ public static bool $above = true;
/**
* Adds attributes regarding the "covers" feature.
diff --git a/src/Factories/TestCaseFactory.php b/src/Factories/TestCaseFactory.php
index 4591d9f1..9a48cd5c 100644
--- a/src/Factories/TestCaseFactory.php
+++ b/src/Factories/TestCaseFactory.php
@@ -85,7 +85,7 @@ final class TestCaseFactory
$methods = array_values(array_filter(
$this->methods,
- fn ($method) => $methodsUsingOnly === [] || in_array($method, $methodsUsingOnly, true)
+ fn ($method): bool => $methodsUsingOnly === [] || in_array($method, $methodsUsingOnly, true)
));
if ($methods !== []) {
@@ -165,21 +165,21 @@ final class TestCaseFactory
$classFQN .= $className;
}
- $classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute) => $attribute::ABOVE_CLASS);
- $methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute) => ! $attribute::ABOVE_CLASS);
+ $classAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => $attribute::$above);
+ $methodAvailableAttributes = array_filter(self::ATTRIBUTES, fn (string $attribute): bool => ! $attribute::$above);
$classAttributes = [];
foreach ($classAvailableAttributes as $attribute) {
$classAttributes = array_reduce(
$methods,
- fn (array $carry, TestCaseMethodFactory $methodFactory) => (new $attribute())->__invoke($methodFactory, $carry),
+ fn (array $carry, TestCaseMethodFactory $methodFactory): array => (new $attribute())->__invoke($methodFactory, $carry),
$classAttributes
);
}
$methodsCode = implode('', array_map(
- fn (TestCaseMethodFactory $methodFactory) => $methodFactory->buildForEvaluation(
+ fn (TestCaseMethodFactory $methodFactory): string => $methodFactory->buildForEvaluation(
$classFQN,
self::ANNOTATIONS,
$methodAvailableAttributes
@@ -188,7 +188,7 @@ final class TestCaseFactory
));
$classAttributesCode = implode('', array_map(
- static fn (string $attribute) => sprintf("\n%s", $attribute),
+ static fn (string $attribute): string => sprintf("\n%s", $attribute),
array_unique($classAttributes),
));
@@ -209,7 +209,7 @@ final class TestCaseFactory
}
PHP;
- eval($classCode);
+ eval($classCode); // @phpstan-ignore-line
} catch (ParseError $caught) {
throw new RuntimeException(sprintf(
"Unable to create test case for test file at %s. \n %s",
diff --git a/src/Factories/TestCaseMethodFactory.php b/src/Factories/TestCaseMethodFactory.php
index 09cf51e2..1bca4dd9 100644
--- a/src/Factories/TestCaseMethodFactory.php
+++ b/src/Factories/TestCaseMethodFactory.php
@@ -97,7 +97,7 @@ final class TestCaseMethodFactory
$testCase->chains->chain($this);
$method->chains->chain($this);
- return \Pest\Support\Closure::bind($closure, $this, $this::class)(...func_get_args());
+ return \Pest\Support\Closure::bind($closure, $this, self::class)(...func_get_args());
};
}
@@ -123,7 +123,9 @@ final class TestCaseMethodFactory
$methodName = Str::evaluable($this->description);
- if (Retry::$retrying && ! TestSuite::getInstance()->retryTempRepository->exists(sprintf('%s::%s', $classFQN, $methodName))) {
+ $retryRepository = TestSuite::getInstance()->retryRepository;
+
+ if (Retry::$retrying && ! $retryRepository->isEmpty() && ! $retryRepository->exists(sprintf('%s::%s', $classFQN, $methodName))) {
return '';
}
@@ -147,11 +149,11 @@ final class TestCaseMethodFactory
}
$annotations = implode('', array_map(
- static fn ($annotation) => sprintf("\n * %s", $annotation), $annotations,
+ static fn ($annotation): string => sprintf("\n * %s", $annotation), $annotations,
));
$attributes = implode('', array_map(
- static fn ($attribute) => sprintf("\n %s", $attribute), $attributes,
+ static fn ($attribute): string => sprintf("\n %s", $attribute), $attributes,
));
return <<|TestCall
*/
- function test(string $description = null, Closure $closure = null)
+ function test(string $description = null, Closure $closure = null): HigherOrderTapProxy|TestCall
{
if ($description === null && TestSuite::getInstance()->test !== null) {
return new HigherOrderTapProxy(TestSuite::getInstance()->test);
@@ -128,10 +130,11 @@ if (! function_exists('todo')) {
*/
function todo(string $description): TestCall
{
- /* @phpstan-ignore-next-line */
- return test($description, fn () => self::markTestSkipped(
- '__TODO__',
- ));
+ $test = test($description);
+
+ assert($test instanceof TestCall);
+
+ return $test->skip('__TODO__');
}
}
diff --git a/src/Kernel.php b/src/Kernel.php
index 76ab91f1..aa8afa34 100644
--- a/src/Kernel.php
+++ b/src/Kernel.php
@@ -4,6 +4,7 @@ declare(strict_types=1);
namespace Pest;
+use Pest\Contracts\Bootstrapper;
use Pest\Plugins\Actions\CallsAddsOutput;
use Pest\Plugins\Actions\CallsBoot;
use Pest\Plugins\Actions\CallsShutdown;
@@ -22,6 +23,7 @@ final class Kernel
* @var array
*/
private const BOOTSTRAPPERS = [
+ Bootstrappers\BootOverrides::class,
Bootstrappers\BootExceptionHandler::class,
Bootstrappers\BootSubscribers::class,
Bootstrappers\BootFiles::class,
@@ -49,7 +51,10 @@ final class Kernel
public static function boot(): self
{
foreach (self::BOOTSTRAPPERS as $bootstrapper) {
- Container::getInstance()->get($bootstrapper)->__invoke();
+ $bootstrapper = Container::getInstance()->get($bootstrapper);
+ assert($bootstrapper instanceof Bootstrapper);
+
+ $bootstrapper->boot();
}
(new CallsBoot())->__invoke();
diff --git a/src/Logging/JUnit.php b/src/Logging/JUnit.php
deleted file mode 100644
index 22c05afd..00000000
--- a/src/Logging/JUnit.php
+++ /dev/null
@@ -1,23 +0,0 @@
-
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
-
-namespace Pest\Logging;
-
-use Pest\Support\Printer;
-
-/**
- * @internal This class is not covered by the backward compatibility promise for PHPUnit
- */
-final class JUnit extends Printer
-{
- // @todo
-}
diff --git a/src/PendingCalls/TestCall.php b/src/PendingCalls/TestCall.php
index 215b4de8..3cd8cee6 100644
--- a/src/PendingCalls/TestCall.php
+++ b/src/PendingCalls/TestCall.php
@@ -154,7 +154,7 @@ final class TestCall
$condition = is_callable($condition)
? $condition
- : fn () => $condition;
+ : fn (): bool => $condition;
$message = is_string($conditionOrMessage)
? $conditionOrMessage
@@ -170,6 +170,16 @@ final class TestCall
return $this;
}
+ /**
+ * Sets the test as "todo".
+ */
+ public function todo(): self
+ {
+ $this->skip('__TODO__');
+
+ return $this;
+ }
+
/**
* Sets the covered classes or methods.
*/
diff --git a/src/Plugin.php b/src/Plugin.php
index 55c91134..54780337 100644
--- a/src/Plugin.php
+++ b/src/Plugin.php
@@ -23,7 +23,7 @@ final class Plugin
public static function uses(string ...$traits): void
{
self::$callables[] = function () use ($traits): void {
- uses(...$traits)->in(TestSuite::getInstance()->rootPath.DIRECTORY_SEPARATOR.testDirectory());
+ uses(...$traits)->in(TestSuite::getInstance()->rootPath);
};
}
}
diff --git a/src/Plugins/Memory.php b/src/Plugins/Memory.php
index 5d29960e..0755f743 100644
--- a/src/Plugins/Memory.php
+++ b/src/Plugins/Memory.php
@@ -46,7 +46,7 @@ final class Memory implements AddsOutput, HandlesArguments
{
if ($this->enabled) {
$this->output->writeln(sprintf(
- ' Memory:> %s MB>',
+ ' Memory:> %s MB>',
round(memory_get_usage(true) / 1000 ** 2, 3)
));
}
diff --git a/src/Repositories/DatasetsRepository.php b/src/Repositories/DatasetsRepository.php
index f4c7c11d..3b00b7b4 100644
--- a/src/Repositories/DatasetsRepository.php
+++ b/src/Repositories/DatasetsRepository.php
@@ -5,7 +5,8 @@ declare(strict_types=1);
namespace Pest\Repositories;
use Closure;
-use Pest\Exceptions\DatasetAlreadyExist;
+use Generator;
+use Pest\Exceptions\DatasetAlreadyExists;
use Pest\Exceptions\DatasetDoesNotExist;
use Pest\Exceptions\ShouldNotHappen;
use SebastianBergmann\Exporter\Exporter;
@@ -17,6 +18,8 @@ use Traversable;
*/
final class DatasetsRepository
{
+ private const SEPARATOR = '>>';
+
/**
* Holds the datasets.
*
@@ -36,13 +39,15 @@ final class DatasetsRepository
*
* @param Closure|iterable $data
*/
- public static function set(string $name, Closure|iterable $data): void
+ public static function set(string $name, Closure|iterable $data, string $scope): void
{
- if (array_key_exists($name, self::$datasets)) {
- throw new DatasetAlreadyExist($name);
+ $datasetKey = "$scope".self::SEPARATOR."$name";
+
+ if (array_key_exists("$datasetKey", self::$datasets)) {
+ throw new DatasetAlreadyExists($name, $scope);
}
- self::$datasets[$name] = $data;
+ self::$datasets[$datasetKey] = $data;
}
/**
@@ -52,12 +57,12 @@ final class DatasetsRepository
*/
public static function with(string $filename, string $description, array $with): void
{
- self::$withs[$filename.'>>>'.$description] = $with;
+ self::$withs["$filename".self::SEPARATOR."$description"] = $with;
}
public static function has(string $filename, string $description): bool
{
- return array_key_exists($filename.'>>>'.$description, self::$withs);
+ return array_key_exists($filename.self::SEPARATOR.$description, self::$withs);
}
/**
@@ -67,9 +72,9 @@ final class DatasetsRepository
*/
public static function get(string $filename, string $description)
{
- $dataset = self::$withs[$filename.'>>>'.$description];
+ $dataset = self::$withs[$filename.self::SEPARATOR.$description];
- $dataset = self::resolve($description, $dataset);
+ $dataset = self::resolve($dataset, $filename);
if ($dataset === null) {
throw ShouldNotHappen::fromMessage('Dataset [%s] not resolvable.');
@@ -84,14 +89,13 @@ final class DatasetsRepository
* @param array|string> $dataset
* @return array|null
*/
- public static function resolve(string $description, array $dataset): array|null
+ public static function resolve(array $dataset, string $currentTestFile): array|null
{
- /* @phpstan-ignore-next-line */
- if (empty($dataset)) {
+ if ($dataset === []) {
return null;
}
- $dataset = self::processDatasets($dataset);
+ $dataset = self::processDatasets($dataset, $currentTestFile);
$datasetCombinations = self::getDatasetsCombinations($dataset);
@@ -136,7 +140,7 @@ final class DatasetsRepository
* @param array|string> $datasets
* @return array>
*/
- private static function processDatasets(array $datasets): array
+ private static function processDatasets(array $datasets, string $currentTestFile): array
{
$processedDatasets = [];
@@ -144,11 +148,7 @@ final class DatasetsRepository
$processedDataset = [];
if (is_string($data)) {
- if (! array_key_exists($data, self::$datasets)) {
- throw new DatasetDoesNotExist($data);
- }
-
- $datasets[$index] = self::$datasets[$data];
+ $datasets[$index] = self::getScopedDataset($data, $currentTestFile);
}
if (is_callable($datasets[$index])) {
@@ -156,14 +156,16 @@ final class DatasetsRepository
}
if ($datasets[$index] instanceof Traversable) {
- $datasets[$index] = iterator_to_array($datasets[$index], false);
+ $preserveKeysForArrayIterator = $datasets[$index] instanceof Generator
+ && is_string($datasets[$index]->key());
+
+ $datasets[$index] = iterator_to_array($datasets[$index], $preserveKeysForArrayIterator);
}
- // @phpstan-ignore-next-line
foreach ($datasets[$index] as $key => $values) {
$values = is_array($values) ? $values : [$values];
$processedDataset[] = [
- 'label' => self::getDatasetDescription($key, $values), // @phpstan-ignore-line
+ 'label' => self::getDatasetDescription($key, $values),
'values' => $values,
];
}
@@ -174,6 +176,33 @@ final class DatasetsRepository
return $processedDatasets;
}
+ /**
+ * @return Closure|iterable
+ */
+ private static function getScopedDataset(string $name, string $currentTestFile): Closure|iterable
+ {
+ $matchingDatasets = array_filter(self::$datasets, function (string $key) use ($name, $currentTestFile): bool {
+ [$datasetScope, $datasetName] = explode(self::SEPARATOR, $key);
+
+ if ($name !== $datasetName) {
+ return false;
+ }
+
+ return str_starts_with($currentTestFile, $datasetScope);
+ }, ARRAY_FILTER_USE_KEY);
+
+ $closestScopeDatasetKey = array_reduce(
+ array_keys($matchingDatasets),
+ fn ($keyA, $keyB) => $keyA !== null && strlen((string) $keyA) > strlen($keyB) ? $keyA : $keyB
+ );
+
+ if ($closestScopeDatasetKey === null) {
+ throw new DatasetDoesNotExist($name);
+ }
+
+ return $matchingDatasets[$closestScopeDatasetKey];
+ }
+
/**
* @param array> $combinations
* @return array>>
diff --git a/src/Repositories/TempRepository.php b/src/Repositories/RetryRepository.php
similarity index 64%
rename from src/Repositories/TempRepository.php
rename to src/Repositories/RetryRepository.php
index 9ad1b601..e2a158c2 100644
--- a/src/Repositories/TempRepository.php
+++ b/src/Repositories/RetryRepository.php
@@ -7,9 +7,15 @@ namespace Pest\Repositories;
/**
* @internal
*/
-final class TempRepository
+final class RetryRepository
{
- private const FOLDER = __DIR__.'/../../.temp';
+ private const TEMPORARY_FOLDER = __DIR__
+ .DIRECTORY_SEPARATOR
+ .'..'
+ .DIRECTORY_SEPARATOR
+ .'..'
+ .DIRECTORY_SEPARATOR
+ .'.temp';
/**
* Creates a new Temp Repository instance.
@@ -32,11 +38,19 @@ final class TempRepository
*/
public function boot(): void
{
- @unlink(self::FOLDER.'/'.$this->filename.'.json'); // @phpstan-ignore-line
+ @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.
*/
@@ -52,7 +66,9 @@ final class TempRepository
*/
private function all(): array
{
- $contents = file_get_contents(self::FOLDER.'/'.$this->filename.'.json');
+ $path = self::TEMPORARY_FOLDER.'/'.$this->filename.'.json';
+
+ $contents = file_exists($path) ? file_get_contents($path) : '{}';
assert(is_string($contents));
@@ -70,6 +86,6 @@ final class TempRepository
{
$contents = json_encode($elements, JSON_THROW_ON_ERROR);
- file_put_contents(self::FOLDER.'/'.$this->filename.'.json', $contents);
+ file_put_contents(self::TEMPORARY_FOLDER.'/'.$this->filename.'.json', $contents);
}
}
diff --git a/src/Repositories/TestRepository.php b/src/Repositories/TestRepository.php
index f0bffa6a..ad553487 100644
--- a/src/Repositories/TestRepository.php
+++ b/src/Repositories/TestRepository.php
@@ -50,7 +50,7 @@ final class TestRepository
*/
public function getFilenames(): array
{
- $testCases = array_filter($this->testCases, static fn(TestCaseFactory $testCase) => $testCase->methodsUsingOnly() !== []);
+ $testCases = array_filter($this->testCases, static fn (TestCaseFactory $testCase): bool => $testCase->methodsUsingOnly() !== []);
if ($testCases === []) {
$testCases = $this->testCases;
diff --git a/src/Subscribers/EnsureErroredTestsAreRetryable.php b/src/Subscribers/EnsureErroredTestsAreRetryable.php
new file mode 100644
index 00000000..6eb2661d
--- /dev/null
+++ b/src/Subscribers/EnsureErroredTestsAreRetryable.php
@@ -0,0 +1,23 @@
+retryRepository->add($event->test()->id());
+ }
+}
diff --git a/src/Subscribers/EnsureFailedTestsAreStoredForRetry.php b/src/Subscribers/EnsureFailedTestsAreRetryable.php
similarity index 64%
rename from src/Subscribers/EnsureFailedTestsAreStoredForRetry.php
rename to src/Subscribers/EnsureFailedTestsAreRetryable.php
index 46fce87a..73468723 100644
--- a/src/Subscribers/EnsureFailedTestsAreStoredForRetry.php
+++ b/src/Subscribers/EnsureFailedTestsAreRetryable.php
@@ -11,13 +11,13 @@ use PHPUnit\Event\Test\FailedSubscriber;
/**
* @internal
*/
-final class EnsureFailedTestsAreStoredForRetry implements FailedSubscriber
+final class EnsureFailedTestsAreRetryable implements FailedSubscriber
{
/**
* Runs the subscriber.
*/
public function notify(Failed $event): void
{
- TestSuite::getInstance()->retryTempRepository->add($event->test()->id());
+ TestSuite::getInstance()->retryRepository->add($event->test()->id());
}
}
diff --git a/src/Subscribers/EnsureRetryRepositoryExists.php b/src/Subscribers/EnsureRetryRepositoryExists.php
index dc988d1d..91226b0c 100644
--- a/src/Subscribers/EnsureRetryRepositoryExists.php
+++ b/src/Subscribers/EnsureRetryRepositoryExists.php
@@ -18,6 +18,6 @@ final class EnsureRetryRepositoryExists implements StartedSubscriber
*/
public function notify(Started $event): void
{
- TestSuite::getInstance()->retryTempRepository->boot();
+ TestSuite::getInstance()->retryRepository->boot();
}
}
diff --git a/src/Support/Backtrace.php b/src/Support/Backtrace.php
index 33a44a2e..5077fb5a 100644
--- a/src/Support/Backtrace.php
+++ b/src/Support/Backtrace.php
@@ -44,6 +44,30 @@ final class Backtrace
return $current[self::FILE];
}
+ /**
+ * Returns the current datasets file.
+ */
+ public static function datasetsFile(): string
+ {
+ $current = null;
+
+ foreach (debug_backtrace(self::BACKTRACE_OPTIONS) as $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')) {
+ break;
+ }
+
+ $current = $trace;
+ }
+
+ if ($current === null) {
+ throw ShouldNotHappen::fromMessage('Dataset file not found.');
+ }
+
+ return $current[self::FILE];
+ }
+
/**
* Returns the filename that called the current function/method.
*/
diff --git a/src/Support/ChainableClosure.php b/src/Support/ChainableClosure.php
index 3c228872..55dcca3a 100644
--- a/src/Support/ChainableClosure.php
+++ b/src/Support/ChainableClosure.php
@@ -22,8 +22,8 @@ final class ChainableClosure
throw ShouldNotHappen::fromMessage('$this not bound to chainable closure.');
}
- \Pest\Support\Closure::bind($closure, $this, $this::class)(...func_get_args());
- \Pest\Support\Closure::bind($next, $this, $this::class)(...func_get_args());
+ \Pest\Support\Closure::bind($closure, $this, self::class)(...func_get_args());
+ \Pest\Support\Closure::bind($next, $this, self::class)(...func_get_args());
};
}
diff --git a/src/Support/Container.php b/src/Support/Container.php
index 8f865ce9..3afa9818 100644
--- a/src/Support/Container.php
+++ b/src/Support/Container.php
@@ -16,7 +16,7 @@ final class Container
private static ?Container $instance = null;
/**
- * @var array
+ * @var array
*/
private array $instances = [];
@@ -25,37 +25,30 @@ final class Container
*/
public static function getInstance(): self
{
- if (static::$instance === null) {
- static::$instance = new self();
+ if (self::$instance === null) {
+ self::$instance = new self();
}
- return static::$instance;
+ return self::$instance;
}
/**
* Gets a dependency from the container.
- *
- * @template TObject of object
- *
- * @param class-string $id
- * @return TObject
*/
- public function get(string $id): mixed
+ public function get(string $id): object|string
{
if (! array_key_exists($id, $this->instances)) {
+ /** @var class-string $id */
$this->instances[$id] = $this->build($id);
}
- /** @var TObject $concrete */
- $concrete = $this->instances[$id];
-
- return $concrete;
+ return $this->instances[$id];
}
/**
* Adds the given instance to the container.
*/
- public function add(string $id, mixed $instance): void
+ public function add(string $id, object|string $instance): void
{
$this->instances[$id] = $instance;
}
@@ -68,7 +61,7 @@ final class Container
* @param class-string $id
* @return TObject
*/
- private function build(string $id): mixed
+ private function build(string $id): object
{
$reflectionClass = new ReflectionClass($id);
@@ -77,7 +70,7 @@ final class Container
if ($constructor !== null) {
$params = array_map(
- function (ReflectionParameter $param) use ($id) {
+ function (ReflectionParameter $param) use ($id): object|string {
$candidate = Reflection::getParameterClassName($param);
if ($candidate === null) {
@@ -90,7 +83,6 @@ final class Container
}
}
- // @phpstan-ignore-next-line
return $this->get($candidate);
},
$constructor->getParameters()
diff --git a/src/Support/Coverage.php b/src/Support/Coverage.php
index 743c73ab..91ecfe1a 100644
--- a/src/Support/Coverage.php
+++ b/src/Support/Coverage.php
@@ -10,9 +10,9 @@ use SebastianBergmann\CodeCoverage\Node\Directory;
use SebastianBergmann\CodeCoverage\Node\File;
use SebastianBergmann\Environment\Runtime;
use Symfony\Component\Console\Output\OutputInterface;
-use Symfony\Component\Console\Terminal;
use function Termwind\render;
use function Termwind\renderUsing;
+use function Termwind\terminal;
/**
* @internal
@@ -42,15 +42,15 @@ final class Coverage
return false;
}
- if ($runtime->hasXdebug()) {
- if (version_compare((string) phpversion('xdebug'), '3.1', '>=')) {
- if (! in_array('coverage', xdebug_info('mode'), true)) {
- return false;
- }
- }
+ if (! $runtime->hasXdebug()) {
+ return true;
}
- return true;
+ if (! version_compare((string) phpversion('xdebug'), '3.1', '>=')) {
+ return true;
+ }
+
+ return in_array('coverage', xdebug_info('mode'), true);
}
/**
@@ -83,10 +83,6 @@ final class Coverage
$codeCoverage = require $reportPath;
unlink($reportPath);
- $totalWidth = (new Terminal())->getWidth();
-
- $dottedLineLength = $totalWidth - 6;
-
$totalCoverage = $codeCoverage->getReport()->percentageOfExecutedLines();
/** @var Directory $report */
@@ -103,36 +99,23 @@ final class Coverage
$dirname,
$basename,
]);
- $rawName = $dirname === '.' ? $basename : implode(DIRECTORY_SEPARATOR, [
- $dirname,
- $basename,
- ]);
-
- $linesExecutedTakenSize = 0;
-
- if ($file->percentageOfExecutedLines()->asString() != '0.00%') {
- $linesExecutedTakenSize = strlen($uncoveredLines = trim(implode(', ', self::getMissingCoverage($file)))) + 1;
- $name .= sprintf(' %s>', $uncoveredLines);
- }
$percentage = $file->numberOfExecutableLines() === 0
? '100.0'
: number_format($file->percentageOfExecutedLines()->asFloat(), 1, '.', '');
- $takenSize = strlen($rawName.$percentage) + 2 + $linesExecutedTakenSize; // adding 3 space and percent sign
+ $color = $percentage === '100.0' ? 'green' : ($percentage === '0.0' ? 'red' : 'yellow');
- $percentage = sprintf(
- '%s>',
- $percentage === '100.0' ? 'green' : ($percentage === '0.0' ? 'red' : 'yellow'),
- $percentage
- );
+ $truncateAt = max(1, terminal()->width() - 12);
- $output->writeln(sprintf(
- ' %s %s> %s %%>',
- $name,
- str_repeat('.', max($dottedLineLength - $takenSize, 1)),
- $percentage
- ));
+ renderUsing($output);
+ render(<<
+ {$name}
+
+ {$percentage}%
+
+ HTML);
}
$totalCoverageAsString = $totalCoverage->asFloat() === 0.0
diff --git a/src/Support/DatasetInfo.php b/src/Support/DatasetInfo.php
new file mode 100644
index 00000000..cfcf65b6
--- /dev/null
+++ b/src/Support/DatasetInfo.php
@@ -0,0 +1,38 @@
+getProperty($property);
@@ -127,7 +128,7 @@ final class Reflection
$reflectionProperty = null;
- while ($reflectionProperty === null) {
+ while (! $reflectionProperty instanceof ReflectionProperty) {
try {
/* @var ReflectionProperty $reflectionProperty */
$reflectionProperty = $reflectionClass->getProperty($property);
diff --git a/src/Support/Str.php b/src/Support/Str.php
index 69522b6a..6ceaed1d 100644
--- a/src/Support/Str.php
+++ b/src/Support/Str.php
@@ -56,7 +56,7 @@ final class Str
{
$code = str_replace(' ', '_', $code);
- return (string) preg_replace('/[^A-Z_a-z0-9\\\\]/', '', $code);
+ return (string) preg_replace('/[^A-Z_a-z0-9]/', '_', $code);
}
/**
diff --git a/src/TestSuite.php b/src/TestSuite.php
index 79402046..1bf64ace 100644
--- a/src/TestSuite.php
+++ b/src/TestSuite.php
@@ -9,7 +9,7 @@ use Pest\Repositories\AfterAllRepository;
use Pest\Repositories\AfterEachRepository;
use Pest\Repositories\BeforeAllRepository;
use Pest\Repositories\BeforeEachRepository;
-use Pest\Repositories\TempRepository;
+use Pest\Repositories\RetryRepository;
use Pest\Repositories\TestRepository;
use PHPUnit\Framework\TestCase;
@@ -44,9 +44,9 @@ final class TestSuite
public AfterAllRepository $afterAll;
/**
- * Holds the retry temp repository.
+ * Holds the retry repository.
*/
- public TempRepository $retryTempRepository;
+ public RetryRepository $retryRepository;
/**
* Holds the root path.
@@ -71,7 +71,7 @@ final class TestSuite
$this->beforeEach = new BeforeEachRepository();
$this->afterEach = new AfterEachRepository();
$this->afterAll = new AfterAllRepository();
- $this->retryTempRepository = new TempRepository('retry');
+ $this->retryRepository = new RetryRepository('retry');
$this->rootPath = (string) realpath($rootPath);
}
@@ -95,7 +95,7 @@ final class TestSuite
return self::$instance;
}
- if (self::$instance === null) {
+ if (! self::$instance instanceof self) {
throw new InvalidPestCommand();
}
diff --git a/tests/.snapshots/help-command.txt b/tests/.snapshots/help-command.txt
index f6caacbc..b291ec65 100644
--- a/tests/.snapshots/help-command.txt
+++ b/tests/.snapshots/help-command.txt
@@ -76,9 +76,9 @@
LOGGING OPTIONS:
--log-junit ................ Log test execution in JUnit XML format to file
--log-teamcity .............. Log test execution in TeamCity format to file
- --testdox-html ........... Write agile documentation in HTML format to file
- --testdox-text ........... Write agile documentation in Text format to file
- --testdox-xml ............. Write agile documentation in XML format to file
+ --testdox-html ................. Write documentation in HTML format to file
+ --testdox-text ................. Write documentation in Text format to file
+ --testdox-xml ................... Write documentation in XML format to file
--log-events-text ..................... Stream events as plain text to file
--log-events-verbose-text Stream events as plain text to file (with telemetry information)
--no-logging .................................. Ignore logging configuration
diff --git a/tests/.snapshots/success.txt b/tests/.snapshots/success.txt
index af0bfcef..e91aae32 100644
--- a/tests/.snapshots/success.txt
+++ b/tests/.snapshots/success.txt
@@ -29,7 +29,7 @@
✓ it does not append CoversNothing to other methods
✓ it throws exception if no class nor method has been found
- PASS Tests\Features\Datasets
+ PASS Tests\Features\DatasetsTests
✓ it throws exception if dataset does not exist
✓ it throws exception if dataset already exist
✓ it sets closures
@@ -107,6 +107,8 @@
✓ eager registered wrapped datasets with Generator functions with (3)
✓ eager registered wrapped datasets with Generator functions with (4)
✓ eager registered wrapped datasets with Generator functions did the job right
+ ✓ eager registered wrapped datasets with Generator functions display description with data set "taylor"
+ ✓ eager registered wrapped datasets with Generator functions display description with data set "james"
✓ it can resolve a dataset after the test case is available with (Closure Object (...)) #1
✓ it can resolve a dataset after the test case is available with (Closure Object (...)) #2
✓ it can resolve a dataset after the test case is available with shared yield sets with (Closure Object (...)) #1
@@ -121,6 +123,7 @@
✓ it will not resolve a closure if it is type hinted as a callable with (Closure Object (...)) #2
✓ it can correctly resolve a bound dataset that returns an array with (Closure Object (...))
✓ it can correctly resolve a bound dataset that returns an array but wants to be spread with (Closure Object (...))
+ ↓ forbids to define tests in Datasets dirs and Datasets.php files
PASS Tests\Features\Depends
✓ first
@@ -668,6 +671,47 @@
✓ get 'foo' → get 'bar' → expect true → toBeTrue
✓ get 'foo' → expect true → toBeTrue
+ PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory1\TestFileInNestedDirectoryWithDatasetsFile
+ ✓ uses dataset with (1)
+ ✓ uses dataset with (2)
+ ✓ uses dataset with (3)
+ ✓ uses dataset with (4)
+ ✓ uses dataset with (5)
+ ✓ uses dataset with ('ScopedDatasets/NestedDirector...ts.php')
+ ✓ the right dataset is taken
+
+ PASS Tests\Features\ScopedDatasets\Directory\NestedDirectory2\TestFileInNestedDirectory
+ ✓ uses dataset with (1)
+ ✓ uses dataset with (2)
+ ✓ uses dataset with (3)
+ ✓ uses dataset with (4)
+ ✓ uses dataset with (5)
+ ✓ uses dataset with ('ScopedDatasets/Datasets/Scoped.php')
+ ✓ the right dataset is taken
+
+ PASS Tests\Features\ScopedDatasets\Directory\TestFileWithLocallyDefinedDataset
+ ✓ uses dataset with (1)
+ ✓ uses dataset with (2)
+ ✓ uses dataset with (3)
+ ✓ uses dataset with (4)
+ ✓ uses dataset with (5)
+ ✓ uses dataset with ('ScopedDatasets/ScopedDatasets.php')
+ ✓ the right dataset is taken
+
+ PASS Tests\Features\ScopedDatasets\Directory\TestFileWithScopedDataset
+ ✓ uses dataset with (1)
+ ✓ uses dataset with (2)
+ ✓ uses dataset with (3)
+ ✓ uses dataset with (4)
+ ✓ uses dataset with (5)
+ ✓ uses dataset with ('ScopedDatasets/Datasets/Scoped.php')
+ ✓ the right dataset is taken
+
+ PASS Tests\Features\ScopedDatasets\TestFileOutOfScope
+ ✓ uses dataset with (1)
+ ✓ uses dataset with (2)
+ ✓ the right dataset is taken
+
WARN Tests\Features\Skip
✓ it do not skips
- it skips with truthy → 1
@@ -690,6 +734,8 @@
PASS Tests\Features\Todo
↓ something todo later
+ ↓ something todo later chained
+ ↓ something todo later chained and with function body
✓ it does something within a file with a todo
PASS Tests\Fixtures\DirectoryWithTests\ExampleTest
@@ -713,29 +759,32 @@
✓ global beforeEach execution order
PASS Tests\PHPUnit\CustomAffixes\InvalidTestName
- ✓ it runs file names like `@#$%^&()-_=+.php`
+ ✓ it runs file names like @#$%^&()-_=+.php
PASS Tests\PHPUnit\CustomAffixes\ATestWithSpaces
- ✓ it runs file names like `A Test With Spaces.php`
+ ✓ it runs file names like A Test With Spaces.php
PASS Tests\PHPUnit\CustomAffixes\AdditionalFileExtension
- ✓ it runs file names like `AdditionalFileExtension.spec.php`
+ ✓ it runs file names like AdditionalFileExtension.spec.php
PASS Tests\PHPUnit\CustomAffixes\FolderWithAn\ExampleTest
✓ custom traits can be used
✓ trait applied in this file
PASS Tests\PHPUnit\CustomAffixes\ManyExtensions
- ✓ it runs file names like `ManyExtensions.class.test.php`
+ ✓ it runs file names like ManyExtensions.class.test.php
PASS Tests\PHPUnit\CustomAffixes\TestCaseWithQuotes
- ✓ it runs file names like `Test 'Case' With Quotes.php`
+ ✓ it runs file names like Test 'Case' With Quotes.php
PASS Tests\PHPUnit\CustomAffixes\kebabcasespec
- ✓ it runs file names like `kebab-case-spec.php`
+ ✓ it runs file names like kebab-case-spec.php
PASS Tests\PHPUnit\CustomAffixes\snakecasespec
- ✓ it runs file names like `snake_case_spec.php`
+ ✓ it runs file names like snake_case_spec.php
+
+ PASS Tests\CustomTestCase\ExecutedTest
+ ✓ that gets executed
PASS Tests\PHPUnit\CustomTestCase\UsesPerDirectory
✓ closure was bound to CustomTestCase
@@ -766,13 +815,17 @@
PASS Tests\Unit\Console\Help
✓ it outputs the help information when --help is used
- PASS Tests\Unit\Datasets
+ PASS Tests\Unit\DatasetsTests
✓ it show only the names of named datasets in their description
✓ it show the actual dataset of non-named datasets in their description
✓ it show only the names of multiple named datasets in their description
✓ it show the actual dataset of multiple non-named datasets in their description
✓ it show the correct description for mixed named and not-named datasets
+ PASS Tests\Unit\Expectations\OppositeExpectation
+ ✓ it throw expectation failed exception with string argument
+ ✓ it throw expectation failed exception with array argument
+
PASS Tests\Unit\Plugins\Environment
✓ environment is set to CI when --ci option is used
✓ environment is set to Local when --ci option is not used
@@ -792,10 +845,35 @@
✓ it can resolve builtin value types
✓ it cannot resolve a parameter without type
+ PASS Tests\Unit\Support\DatasetInfo
+ ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Datase...rs.php', true)
+ ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Datasets.php', false)
+ ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur...rs.php', true)
+ ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur...rs.php', false)
+ ✓ it can check if dataset is defined inside a Datasets directory with ('/var/www/project/tests/Featur...ts.php', false)
+ ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Datase...rs.php', false)
+ ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Datasets.php', true)
+ ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur...rs.php', false) #1
+ ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur...rs.php', false) #2
+ ✓ it can check if dataset is defined inside a Datasets.php file with ('/var/www/project/tests/Featur...ts.php', true)
+ ✓ it computes the dataset scope with ('/var/www/project/tests/Datase...rs.php', '/var/www/project/tests')
+ ✓ it computes the dataset scope with ('/var/www/project/tests/Datasets.php', '/var/www/project/tests')
+ ✓ it computes the dataset scope with ('/var/www/project/tests/Featur...rs.php', '/var/www/project/tests/Features')
+ ✓ it computes the dataset scope with ('/var/www/project/tests/Featur...rs.php', '/var/www/project/tests/Featur...rs.php') #1
+ ✓ it computes the dataset scope with ('/var/www/project/tests/Featur...ts.php', '/var/www/project/tests/Features')
+ ✓ it computes the dataset scope with ('/var/www/project/tests/Featur...rs.php', '/var/www/project/tests/Featur...ollers')
+ ✓ it computes the dataset scope with ('/var/www/project/tests/Featur...rs.php', '/var/www/project/tests/Featur...rs.php') #2
+ ✓ it computes the dataset scope with ('/var/www/project/tests/Featur...ts.php', '/var/www/project/tests/Featur...ollers')
+
PASS Tests\Unit\Support\Reflection
✓ it gets file name from closure
✓ it gets property values
+ PASS Tests\Unit\Support\Str
+ ✓ it evaluates the code with ('version()', 'version__')
+ ✓ it evaluates the code with ('version__ ', 'version___')
+ ✓ it evaluates the code with ('version\', 'version_')
+
PASS Tests\Unit\TestSuite
✓ it does not allow to add the same test description twice
✓ it alerts users about tests with arguments but no input
@@ -806,9 +884,6 @@
PASS Tests\Visual\Help
✓ visual snapshot of help command output
- WARN Tests\Visual\JUnit
- - it is can successfully call all public methods → Not supported yet.
-
PASS Tests\Visual\SingleTestOrDirectory
✓ allows to run a single test
✓ allows to run a directory
@@ -823,4 +898,4 @@
PASS Tests\Visual\Version
✓ visual snapshot of help command output
- Tests: 4 incomplete, 1 todo, 18 skipped, 567 passed (1465 assertions)
\ No newline at end of file
+ Tests: 4 incomplete, 4 todos, 17 skipped, 624 passed (1511 assertions)
\ No newline at end of file
diff --git a/tests/Features/Datasets.php b/tests/Features/DatasetsTests.php
similarity index 91%
rename from tests/Features/Datasets.php
rename to tests/Features/DatasetsTests.php
index 073b1e68..aeb0ba82 100644
--- a/tests/Features/Datasets.php
+++ b/tests/Features/DatasetsTests.php
@@ -1,6 +1,6 @@
expectException(DatasetDoesNotExist::class);
$this->expectExceptionMessage("A dataset with the name `first` does not exist. You can create it using `dataset('first', ['a', 'b']);`.");
- DatasetsRepository::resolve('foo', ['first']);
+ DatasetsRepository::resolve(['first'], __FILE__);
});
it('throws exception if dataset already exist', function () {
- DatasetsRepository::set('second', [[]]);
- $this->expectException(DatasetAlreadyExist::class);
- $this->expectExceptionMessage('A dataset with the name `second` already exist.');
- DatasetsRepository::set('second', [[]]);
+ DatasetsRepository::set('second', [[]], __DIR__);
+ $this->expectException(DatasetAlreadyExists::class);
+ $this->expectExceptionMessage('A dataset with the name `second` already exist in scope ['.__DIR__.'].');
+ DatasetsRepository::set('second', [[]], __DIR__);
});
it('sets closures', function () {
DatasetsRepository::set('foo', function () {
yield [1];
- });
+ }, __DIR__);
- expect(DatasetsRepository::resolve('foo', ['foo']))->toBe(['(1)' => [1]]);
+ expect(DatasetsRepository::resolve(['foo'], __FILE__))->toBe(['(1)' => [1]]);
});
it('sets arrays', function () {
- DatasetsRepository::set('bar', [[2]]);
+ DatasetsRepository::set('bar', [[2]], __DIR__);
- expect(DatasetsRepository::resolve('bar', ['bar']))->toBe(['(2)' => [2]]);
+ expect(DatasetsRepository::resolve(['bar'], __FILE__))->toBe(['(2)' => [2]]);
});
it('gets bound to test case object', function ($value) {
@@ -249,6 +249,13 @@ test('eager registered wrapped datasets with Generator functions did the job rig
expect($wrapped_generator_state->text)->toBe('1234');
});
+test('eager registered wrapped datasets with Generator functions display description', function ($wrapped_generator_state_with_description) {
+ expect($wrapped_generator_state_with_description)->not->toBeEmpty();
+})->with(function () {
+ yield 'taylor' => 'taylor@laravel.com';
+ yield 'james' => 'james@laravel.com';
+});
+
it('can resolve a dataset after the test case is available', function ($result) {
expect($result)->toBe('bar');
})->with([
@@ -323,3 +330,5 @@ it('can correctly resolve a bound dataset that returns an array but wants to be
return ['foo', 'bar', 'baz'];
},
]);
+
+todo('forbids to define tests in Datasets dirs and Datasets.php files');
diff --git a/tests/Features/ScopedDatasets/Directory/Datasets/Scoped.php b/tests/Features/ScopedDatasets/Directory/Datasets/Scoped.php
new file mode 100644
index 00000000..d49e32c3
--- /dev/null
+++ b/tests/Features/ScopedDatasets/Directory/Datasets/Scoped.php
@@ -0,0 +1,5 @@
+text = '';
+test('uses dataset', function ($value) use ($state) {
+ $state->text .= $value;
+ expect(true)->toBe(true);
+})->with('numbers.array');
+
+test('the right dataset is taken', function () use ($state) {
+ expect($state->text)->toBe('12345ScopedDatasets/NestedDirectory1/Datasets.php');
+});
diff --git a/tests/Features/ScopedDatasets/Directory/NestedDirectory2/TestFileInNestedDirectory.php b/tests/Features/ScopedDatasets/Directory/NestedDirectory2/TestFileInNestedDirectory.php
new file mode 100644
index 00000000..a86b9d23
--- /dev/null
+++ b/tests/Features/ScopedDatasets/Directory/NestedDirectory2/TestFileInNestedDirectory.php
@@ -0,0 +1,12 @@
+text = '';
+test('uses dataset', function ($value) use ($state) {
+ $state->text .= $value;
+ expect(true)->toBe(true);
+})->with('numbers.array');
+
+test('the right dataset is taken', function () use ($state) {
+ expect($state->text)->toBe('12345ScopedDatasets/Datasets/Scoped.php');
+});
diff --git a/tests/Features/ScopedDatasets/Directory/TestFileWithLocallyDefinedDataset.php b/tests/Features/ScopedDatasets/Directory/TestFileWithLocallyDefinedDataset.php
new file mode 100644
index 00000000..0bf23b8c
--- /dev/null
+++ b/tests/Features/ScopedDatasets/Directory/TestFileWithLocallyDefinedDataset.php
@@ -0,0 +1,16 @@
+text = '';
+test('uses dataset', function ($value) use ($state) {
+ $state->text .= $value;
+ expect(true)->toBe(true);
+})->with('numbers.array');
+
+test('the right dataset is taken', function () use ($state) {
+ expect($state->text)->toBe('12345ScopedDatasets/ScopedDatasets.php');
+});
diff --git a/tests/Features/ScopedDatasets/Directory/TestFileWithScopedDataset.php b/tests/Features/ScopedDatasets/Directory/TestFileWithScopedDataset.php
new file mode 100644
index 00000000..a86b9d23
--- /dev/null
+++ b/tests/Features/ScopedDatasets/Directory/TestFileWithScopedDataset.php
@@ -0,0 +1,12 @@
+text = '';
+test('uses dataset', function ($value) use ($state) {
+ $state->text .= $value;
+ expect(true)->toBe(true);
+})->with('numbers.array');
+
+test('the right dataset is taken', function () use ($state) {
+ expect($state->text)->toBe('12345ScopedDatasets/Datasets/Scoped.php');
+});
diff --git a/tests/Features/ScopedDatasets/TestFileOutOfScope.php b/tests/Features/ScopedDatasets/TestFileOutOfScope.php
new file mode 100644
index 00000000..e2d23225
--- /dev/null
+++ b/tests/Features/ScopedDatasets/TestFileOutOfScope.php
@@ -0,0 +1,12 @@
+text = '';
+test('uses dataset', function ($value) use ($state) {
+ $state->text .= $value;
+ expect(true)->toBe(true);
+})->with('numbers.array');
+
+test('the right dataset is taken', function () use ($state) {
+ expect($state->text)->toBe('12');
+});
diff --git a/tests/Features/Todo.php b/tests/Features/Todo.php
index f9cb4cc8..6086b77c 100644
--- a/tests/Features/Todo.php
+++ b/tests/Features/Todo.php
@@ -2,6 +2,12 @@
todo('something todo later');
+test('something todo later chained')->todo();
+
+test('something todo later chained and with function body', function () {
+ expect(true)->toBeFalse();
+})->todo();
+
it('does something within a file with a todo', function () {
expect(true)->toBeTrue();
});
diff --git a/tests/Unit/Datasets.php b/tests/Unit/DatasetsTests.php
similarity index 81%
rename from tests/Unit/Datasets.php
rename to tests/Unit/DatasetsTests.php
index 83b33b04..a1914f82 100644
--- a/tests/Unit/Datasets.php
+++ b/tests/Unit/DatasetsTests.php
@@ -3,31 +3,31 @@
use Pest\Repositories\DatasetsRepository;
it('show only the names of named datasets in their description', function () {
- $descriptions = array_keys(DatasetsRepository::resolve('test description', [
+ $descriptions = array_keys(DatasetsRepository::resolve([
[
'one' => [1],
'two' => [[2]],
],
- ]));
+ ], __FILE__));
expect($descriptions[0])->toBe('data set "one"')
->and($descriptions[1])->toBe('data set "two"');
});
it('show the actual dataset of non-named datasets in their description', function () {
- $descriptions = array_keys(DatasetsRepository::resolve('test description', [
+ $descriptions = array_keys(DatasetsRepository::resolve([
[
[1],
[[2]],
],
- ]));
+ ], __FILE__));
expect($descriptions[0])->toBe('(1)');
expect($descriptions[1])->toBe('(array(2))');
});
it('show only the names of multiple named datasets in their description', function () {
- $descriptions = array_keys(DatasetsRepository::resolve('test description', [
+ $descriptions = array_keys(DatasetsRepository::resolve([
[
'one' => [1],
'two' => [[2]],
@@ -36,7 +36,7 @@ it('show only the names of multiple named datasets in their description', functi
'three' => [3],
'four' => [[4]],
],
- ]));
+ ], __FILE__));
expect($descriptions[0])->toBe('data set "one" / data set "three"');
expect($descriptions[1])->toBe('data set "one" / data set "four"');
@@ -45,7 +45,7 @@ it('show only the names of multiple named datasets in their description', functi
});
it('show the actual dataset of multiple non-named datasets in their description', function () {
- $descriptions = array_keys(DatasetsRepository::resolve('test description', [
+ $descriptions = array_keys(DatasetsRepository::resolve([
[
[1],
[[2]],
@@ -54,7 +54,7 @@ it('show the actual dataset of multiple non-named datasets in their description'
[3],
[[4]],
],
- ]));
+ ], __FILE__));
expect($descriptions[0])->toBe('(1) / (3)');
expect($descriptions[1])->toBe('(1) / (array(4))');
@@ -63,7 +63,7 @@ it('show the actual dataset of multiple non-named datasets in their description'
});
it('show the correct description for mixed named and not-named datasets', function () {
- $descriptions = array_keys(DatasetsRepository::resolve('test description', [
+ $descriptions = array_keys(DatasetsRepository::resolve([
[
'one' => [1],
[[2]],
@@ -72,7 +72,7 @@ it('show the correct description for mixed named and not-named datasets', functi
[3],
'four' => [[4]],
],
- ]));
+ ], __FILE__));
expect($descriptions[0])->toBe('data set "one" / (3)');
expect($descriptions[1])->toBe('data set "one" / data set "four"');
diff --git a/tests/Unit/Expectations/OppositeExpectation.php b/tests/Unit/Expectations/OppositeExpectation.php
new file mode 100644
index 00000000..448ebf6d
--- /dev/null
+++ b/tests/Unit/Expectations/OppositeExpectation.php
@@ -0,0 +1,16 @@
+throwExpectationFailedException('toBe', 'bar');
+})->throws(ExpectationFailedException::class, "Expecting 'foo' not to be 'bar'.");
+
+it('throw expectation failed exception with array argument', function (): void {
+ $expectation = new OppositeExpectation(expect('foo'));
+
+ $expectation->throwExpectationFailedException('toBe', ['bar']);
+})->throws(ExpectationFailedException::class, "Expecting 'foo' not to be 'bar'.");
diff --git a/tests/Unit/Support/DatasetInfo.php b/tests/Unit/Support/DatasetInfo.php
new file mode 100644
index 00000000..dfe2b417
--- /dev/null
+++ b/tests/Unit/Support/DatasetInfo.php
@@ -0,0 +1,36 @@
+toBe($inside);
+})->with([
+ ['file' => '/var/www/project/tests/Datasets/Numbers.php', 'inside' => true],
+ ['file' => '/var/www/project/tests/Datasets.php', 'inside' => false],
+ ['file' => '/var/www/project/tests/Features/Datasets/Numbers.php', 'inside' => true],
+ ['file' => '/var/www/project/tests/Features/Numbers.php', 'inside' => false],
+ ['file' => '/var/www/project/tests/Features/Datasets.php', 'inside' => false],
+]);
+
+it('can check if dataset is defined inside a Datasets.php file', function (string $path, bool $inside) {
+ expect(DatasetInfo::isADatasetsFile($path))->toBe($inside);
+})->with([
+ ['file' => '/var/www/project/tests/Datasets/Numbers.php', 'inside' => false],
+ ['file' => '/var/www/project/tests/Datasets.php', 'inside' => true],
+ ['file' => '/var/www/project/tests/Features/Datasets/Numbers.php', 'inside' => false],
+ ['file' => '/var/www/project/tests/Features/Numbers.php', 'inside' => false],
+ ['file' => '/var/www/project/tests/Features/Datasets.php', 'inside' => true],
+]);
+
+it('computes the dataset scope', function (string $file, string $scope) {
+ expect(DatasetInfo::scope($file))->toBe($scope);
+})->with([
+ ['file' => '/var/www/project/tests/Datasets/Numbers.php', 'scope' => '/var/www/project/tests'],
+ ['file' => '/var/www/project/tests/Datasets.php', 'scope' => '/var/www/project/tests'],
+ ['file' => '/var/www/project/tests/Features/Datasets/Numbers.php', 'scope' => '/var/www/project/tests/Features'],
+ ['file' => '/var/www/project/tests/Features/Numbers.php', 'scope' => '/var/www/project/tests/Features/Numbers.php'],
+ ['file' => '/var/www/project/tests/Features/Datasets.php', 'scope' => '/var/www/project/tests/Features'],
+ ['file' => '/var/www/project/tests/Features/Controllers/Datasets/Numbers.php', 'scope' => '/var/www/project/tests/Features/Controllers'],
+ ['file' => '/var/www/project/tests/Features/Controllers/Numbers.php', 'scope' => '/var/www/project/tests/Features/Controllers/Numbers.php'],
+ ['file' => '/var/www/project/tests/Features/Controllers/Datasets.php', 'scope' => '/var/www/project/tests/Features/Controllers'],
+]);
diff --git a/tests/Unit/Support/Str.php b/tests/Unit/Support/Str.php
new file mode 100644
index 00000000..870adf6d
--- /dev/null
+++ b/tests/Unit/Support/Str.php
@@ -0,0 +1,13 @@
+toBe($expected);
+})->with([
+ ['version()', 'version__'],
+ ['version__ ', 'version___'],
+ ['version\\', 'version_'],
+]);
diff --git a/tests/Visual/JUnit.php b/tests/Visual/JUnit.php
deleted file mode 100644
index 41481b4d..00000000
--- a/tests/Visual/JUnit.php
+++ /dev/null
@@ -1,29 +0,0 @@
-startTestSuite(new TestSuite());
- $junit->startTest($this);
- $junit->addError($this, new Exception(), 0);
- $junit->addFailure($this, new AssertionFailedError(), 0);
- $junit->addWarning($this, new Warning(), 0);
- $junit->addIncompleteTest($this, new Exception(), 0);
- $junit->addRiskyTest($this, new Exception(), 0);
- $junit->addSkippedTest($this, new Exception(), 0);
- $junit->endTest($this, 0);
- $junit->endTestSuite(new TestSuite());
- $this->expectNotToPerformAssertions();
-})->skip('Not supported yet.');
-
-afterEach(function () {
- unlink(__DIR__.'/junit.html');
-});