chore: refactors exceptions

This commit is contained in:
nuno maduro
2026-08-07 15:26:55 +01:00
parent 42d9b777bf
commit 8c554a81e2
54 changed files with 145 additions and 94 deletions
+1 -1
View File
@@ -16,6 +16,6 @@ final class AfterAllAlreadyExist extends InvalidArgumentException implements Exc
{ {
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The afterAll already exists in the filename [%s].', $filename)); parent::__construct(sprintf('The [afterAll] hook is already defined in [%s]. Each test file may only define it once.', $filename));
} }
} }
+1 -1
View File
@@ -16,6 +16,6 @@ final class AfterAllWithinDescribe extends InvalidArgumentException implements E
{ {
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The afterAll method can not be used within describe functions. Filename [%s].', $filename)); parent::__construct(sprintf('The [afterAll] hook may not be used inside a [describe] block. Please move it to the top level of [%s].', $filename));
} }
} }
+1 -1
View File
@@ -16,6 +16,6 @@ final class AfterBeforeTestFunction extends InvalidArgumentException implements
{ {
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct('After method cannot be used with before the [test|it] functions in the filename ['.$filename.'].'); parent::__construct(sprintf('The [after] hook may only be chained onto [beforeEach] inside a [describe] block. Please move it inside one in [%s].', $filename));
} }
} }
+1 -1
View File
@@ -16,6 +16,6 @@ final class BeforeAllAlreadyExist extends InvalidArgumentException implements Ex
{ {
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The beforeAll already exists in the filename [%s].', $filename)); parent::__construct(sprintf('The [beforeAll] hook is already defined in [%s]. Each test file may only define it once.', $filename));
} }
} }
+1 -1
View File
@@ -16,6 +16,6 @@ final class BeforeAllWithinDescribe extends InvalidArgumentException implements
{ {
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The beforeAll method can not be used within describe functions. Filename [%s].', $filename)); parent::__construct(sprintf('The [beforeAll] hook may not be used inside a [describe] block. Please move it to the top level of [%s].', $filename));
} }
} }
+1 -1
View File
@@ -16,6 +16,6 @@ final class DatasetAlreadyExists extends InvalidArgumentException implements Exc
{ {
public function __construct(string $name, string $scope) public function __construct(string $name, string $scope)
{ {
parent::__construct(sprintf('A dataset with the name [%s] already exists in scope [%s].', $name, $scope)); parent::__construct(sprintf('A dataset named [%s] is already registered in [%s]. Please choose a different name.', $name, $scope));
} }
} }
+2 -2
View File
@@ -11,9 +11,9 @@ final class DatasetArgumentsMismatch extends Exception
public function __construct(int $requiredCount, int $suppliedCount) public function __construct(int $requiredCount, int $suppliedCount)
{ {
if ($requiredCount <= $suppliedCount) { if ($requiredCount <= $suppliedCount) {
parent::__construct('Test argument names and dataset keys do not match'); parent::__construct('The test arguments do not match the dataset keys. Please make sure each argument is named after a key in the dataset.');
} else { } else {
parent::__construct(sprintf('Test expects %d arguments but dataset only provides %d', $requiredCount, $suppliedCount)); parent::__construct(sprintf('The test expects [%d] argument(s), but the dataset only provides [%d].', $requiredCount, $suppliedCount));
} }
} }
} }
+1 -1
View File
@@ -16,6 +16,6 @@ final class DatasetDoesNotExist extends InvalidArgumentException implements Exce
{ {
public function __construct(string $name) public function __construct(string $name)
{ {
parent::__construct(sprintf("A dataset with the name [%s] does not exist. You can create it using `dataset('%s', ['a', 'b']);`.", $name, $name)); parent::__construct(sprintf("A dataset named [%s] does not exist. You may create one using `dataset('%s', ['a', 'b']);`.", $name, $name));
} }
} }
+2 -2
View File
@@ -20,11 +20,11 @@ final class DatasetMissing extends BadFunctionCallException implements Exception
public function __construct(string $file, string $name, array $arguments) public function __construct(string $file, string $name, array $arguments)
{ {
parent::__construct(sprintf( parent::__construct(sprintf(
'A test with the description [%s] has [%d] argument(s) ([%s]) and no dataset(s) provided in [%s]', 'The test [%s] in [%s] expects [%d] argument(s) ([%s]), but no dataset was provided. Please chain [with()] onto the test to supply one.',
$name, $name,
$file,
count($arguments), count($arguments),
implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)), implode(', ', array_map(static fn (string $arg, string $type): string => sprintf('%s $%s', $type, $arg), array_keys($arguments), $arguments)),
$file,
)); ));
} }
} }
+1 -1
View File
@@ -13,6 +13,6 @@ final class ExpectationNotFound extends Exception
{ {
public static function fromName(string $name): ExpectationNotFound public static function fromName(string $name): ExpectationNotFound
{ {
return new self("Expectation [$name] does not exist."); return new self("The expectation [$name] does not exist. You may register it using [expect()->extend()].");
} }
} }
+1 -1
View File
@@ -16,6 +16,6 @@ final class FileOrFolderNotFound extends InvalidArgumentException implements Exc
{ {
public function __construct(string $filename) public function __construct(string $filename)
{ {
parent::__construct(sprintf('The file or folder with the name [%s] could not be found.', $filename)); parent::__construct(sprintf('The file or folder [%s] could not be found. Please check the path and try again.', $filename));
} }
} }
+1 -1
View File
@@ -21,6 +21,6 @@ final class InvalidExpectation extends LogicException implements ExceptionInterf
*/ */
public static function fromMethods(array $methods): never public static function fromMethods(array $methods): never
{ {
throw new self(sprintf('Expectation [%s] is not valid.', implode('->', $methods))); throw new self(sprintf('The expectation [%s] does not exist. Please check the spelling, or register it using [expect()->extend()].', implode('->', $methods)));
} }
} }
+1 -1
View File
@@ -16,6 +16,6 @@ final class InvalidExpectationValue extends InvalidArgumentException
*/ */
public static function expected(string $type): never public static function expected(string $type): never
{ {
throw new self(sprintf('Invalid expectation value type. Expected [%s].', $type)); throw new self(sprintf('This expectation may only be used on a value of type [%s].', $type));
} }
} }
+1 -1
View File
@@ -16,6 +16,6 @@ final class InvalidPestCommand extends InvalidArgumentException implements Excep
{ {
public function __construct() public function __construct()
{ {
parent::__construct('Please run [./vendor/bin/pest] instead.'); parent::__construct('Pest must be run through its own binary. Please run [./vendor/bin/pest] instead.');
} }
} }
+2 -2
View File
@@ -17,7 +17,7 @@ final class InvalidTestClassName extends InvalidArgumentException implements Exc
public static function fromClassName(string $filename, string $className): self public static function fromClassName(string $filename, string $className): self
{ {
return new self(sprintf( return new self(sprintf(
'The test file [%s] would create the class [%s], which is not a valid PHP class name. Please rename the test file.', 'The test file [%s] would create the class [%s], which is not a valid PHP class name. Please rename the file.',
$filename, $filename,
$className, $className,
)); ));
@@ -26,7 +26,7 @@ final class InvalidTestClassName extends InvalidArgumentException implements Exc
public static function fromNamespace(string $filename, string $namespace, string $part): self public static function fromNamespace(string $filename, string $namespace, string $part): self
{ {
return new self(sprintf( return new self(sprintf(
'The test file [%s] would create the namespace [%s], which is not a valid PHP namespace, as [%s] may not be used as a namespace name. Please rename the folder in question.', 'The test file [%s] would create the namespace [%s], which is not a valid PHP namespace, because [%s] may not be used as a namespace name. Please rename that folder.',
$filename, $filename,
$namespace, $namespace,
$part, $part,
+1 -1
View File
@@ -16,6 +16,6 @@ final class MissingDependency extends InvalidArgumentException implements Except
{ {
public function __construct(string $feature, string $dependency) public function __construct(string $feature, string $dependency)
{ {
parent::__construct(sprintf('The feature [%s] requires [%s].', $feature, $dependency)); parent::__construct(sprintf('The [%s] feature requires [%s]. Please install it and try again.', $feature, $dependency));
} }
} }
+1 -1
View File
@@ -17,7 +17,7 @@ final class ShouldNotHappen extends RuntimeException
$message = $exception->getMessage(); $message = $exception->getMessage();
parent::__construct(sprintf(<<<'EOF' parent::__construct(sprintf(<<<'EOF'
This should not happen - please create an new issue here: https://github.com/pestphp/pest/issues This should not have happened. Please report it here: https://github.com/pestphp/pest/issues
Issue: %s Issue: %s
PHP version: %s PHP version: %s
+1 -1
View File
@@ -16,6 +16,6 @@ final class TestAlreadyExist extends InvalidArgumentException implements Excepti
{ {
public function __construct(string $fileName, string $description) public function __construct(string $fileName, string $description)
{ {
parent::__construct(sprintf('A test with the description [%s] already exists in the filename [%s].', $description, $fileName)); parent::__construct(sprintf('A test named [%s] already exists in [%s]. Please give this test a different description.', $description, $fileName));
} }
} }
+1 -1
View File
@@ -17,7 +17,7 @@ final class TestCaseAlreadyInUse extends InvalidArgumentException implements Exc
public function __construct(string $inUse, string $newOne, string $folder) public function __construct(string $inUse, string $newOne, string $folder)
{ {
parent::__construct(sprintf( parent::__construct(sprintf(
'Test case [%s] can not be used. The folder [%s] already uses the test case [%s].', 'The test case [%s] may not be used here. The folder [%s] is already bound to the test case [%s].',
$newOne, $newOne,
$folder, $folder,
$inUse, $inUse,
@@ -16,6 +16,6 @@ final class TestCaseClassOrTraitNotFound extends InvalidArgumentException implem
{ {
public function __construct(string $testCaseClass) public function __construct(string $testCaseClass)
{ {
parent::__construct(sprintf('The class [%s] was not found.', $testCaseClass)); parent::__construct(sprintf('The class or trait [%s] could not be found. Please check the name, and make sure it is autoloadable.', $testCaseClass));
} }
} }
@@ -19,7 +19,7 @@ final class TestClosureMustNotBeStatic extends InvalidArgumentException implemen
{ {
parent::__construct( parent::__construct(
sprintf( sprintf(
'Test closure must not be static. Please remove the [static] keyword from the [%s] method in [%s].', 'Test closures may not be static. Please remove the [static] keyword from the test [%s] in [%s].',
$method->description, $method->description,
$method->filename $method->filename
) )
+1 -1
View File
@@ -16,6 +16,6 @@ final class TestDescriptionMissing extends InvalidArgumentException implements E
{ {
public function __construct(string $fileName) public function __construct(string $fileName)
{ {
parent::__construct(sprintf('Test description is missing in the filename [%s].', $fileName)); parent::__construct(sprintf('A test in [%s] is missing its description. Please give every test a description.', $fileName));
} }
} }
+1 -1
View File
@@ -19,7 +19,7 @@ final class TiaRequiresPestTests extends RuntimeException implements ExceptionIn
public function __construct(private readonly string $className, string $filename) public function __construct(private readonly string $className, string $filename)
{ {
parent::__construct(sprintf( parent::__construct(sprintf(
'Tia mode requires only functional based Pest tests, but encountered PHPUnit class [%s] in [%s].', 'Tia mode supports functional Pest tests only, but found the PHPUnit class [%s] in [%s]. Please convert it to a Pest test, or run without Tia.',
$className, $className,
$filename, $filename,
)); ));
+1 -1
View File
@@ -19,7 +19,7 @@ final class TiaRequiresRepositoryRoot extends RuntimeException implements Except
public function __construct(private readonly string $subdirectoryPrefix) public function __construct(private readonly string $subdirectoryPrefix)
{ {
parent::__construct(sprintf( parent::__construct(sprintf(
'Tia mode requires the project root to be the git repository root, but it sits in the subdirectory [%s] of a larger repo.', 'Tia mode requires the project root to be the git repository root, but this project sits in the subdirectory [%s] of a larger repository. Please give it its own repository to use Tia.',
$this->subdirectoryPrefix, $this->subdirectoryPrefix,
)); ));
} }
+6 -6
View File
@@ -318,7 +318,7 @@ final class Expectation
if (! is_object($this->value)) { if (! is_object($this->value)) {
throw new BadMethodCallException(sprintf( throw new BadMethodCallException(sprintf(
'Method "%s" does not exist in %s.', 'Method [%s] does not exist in [%s].',
$method, $method,
gettype($this->value) gettype($this->value)
)); ));
@@ -534,7 +534,7 @@ final class Expectation
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => isset($object->reflectionClass) && $object->reflectionClass->hasMethod($method))) === count($methods), fn (ObjectDescription $object): bool => count(array_filter($methods, fn (string $method): bool => isset($object->reflectionClass) && $object->reflectionClass->hasMethod($method))) === count($methods),
sprintf("to have method '%s'", implode("', '", $methods)), sprintf('to have method [%s]', implode('], [', $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
@@ -659,7 +659,7 @@ final class Expectation
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && ($class === $object->reflectionClass->getName() || $object->reflectionClass->isSubclassOf($class)), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && ($class === $object->reflectionClass->getName() || $object->reflectionClass->isSubclassOf($class)),
sprintf("to extend '%s'", $class), sprintf('to extend [%s]', $class),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
@@ -753,7 +753,7 @@ final class Expectation
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_starts_with($object->reflectionClass->getShortName(), $prefix), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_starts_with($object->reflectionClass->getShortName(), $prefix),
"to have prefix '{$prefix}'", "to have prefix [{$prefix}]",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
@@ -763,7 +763,7 @@ final class Expectation
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_ends_with($object->reflectionClass->getName(), $suffix), fn (ObjectDescription $object): bool => isset($object->reflectionClass) && str_ends_with($object->reflectionClass->getName(), $suffix),
"to have suffix '{$suffix}'", "to have suffix [{$suffix}]",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
@@ -930,7 +930,7 @@ final class Expectation
return Targeted::make( return Targeted::make(
$this, $this,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->getAttributes($attribute) !== [], fn (ObjectDescription $object): bool => isset($object->reflectionClass) && $object->reflectionClass->getAttributes($attribute) !== [],
"to have attribute '{$attribute}'", "to have attribute [{$attribute}]",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
+7 -7
View File
@@ -296,7 +296,7 @@ final readonly class OppositeExpectation
}, },
$methods === [] $methods === []
? 'not to have public methods' ? 'not to have public methods'
: sprintf("not to have public methods besides '%s'", implode("', '", $methods)), : sprintf('not to have public methods besides [%s]', implode('], [', $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, (string) $state->contains)), FileLineFinder::where(fn (string $line): bool => str_contains($line, (string) $state->contains)),
); );
} }
@@ -337,7 +337,7 @@ final readonly class OppositeExpectation
}, },
$methods === [] $methods === []
? 'not to have protected methods' ? 'not to have protected methods'
: sprintf("not to have protected methods besides '%s'", implode("', '", $methods)), : sprintf('not to have protected methods besides [%s]', implode('], [', $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, (string) $state->contains)), FileLineFinder::where(fn (string $line): bool => str_contains($line, (string) $state->contains)),
); );
} }
@@ -378,7 +378,7 @@ final readonly class OppositeExpectation
}, },
$methods === [] $methods === []
? 'not to have private methods' ? 'not to have private methods'
: sprintf("not to have private methods besides '%s'", implode("', '", $methods)), : sprintf('not to have private methods besides [%s]', implode('], [', $methods)),
FileLineFinder::where(fn (string $line): bool => str_contains($line, (string) $state->contains)), FileLineFinder::where(fn (string $line): bool => str_contains($line, (string) $state->contains)),
); );
} }
@@ -450,7 +450,7 @@ final readonly class OppositeExpectation
return Targeted::make( return Targeted::make(
$original, $original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isSubclassOf($class), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! $object->reflectionClass->isSubclassOf($class),
sprintf("not to extend '%s'", $class), sprintf('not to extend [%s]', $class),
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
@@ -535,7 +535,7 @@ final readonly class OppositeExpectation
return Targeted::make( return Targeted::make(
$original, $original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_starts_with($object->reflectionClass->getShortName(), $prefix), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_starts_with($object->reflectionClass->getShortName(), $prefix),
"not to have prefix '{$prefix}'", "not to have prefix [{$prefix}]",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
@@ -548,7 +548,7 @@ final readonly class OppositeExpectation
return Targeted::make( return Targeted::make(
$original, $original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_ends_with($object->reflectionClass->getName(), $suffix), fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || ! str_ends_with($object->reflectionClass->getName(), $suffix),
"not to have suffix '{$suffix}'", "not to have suffix [{$suffix}]",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')), FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')),
); );
} }
@@ -615,7 +615,7 @@ final readonly class OppositeExpectation
return Targeted::make( return Targeted::make(
$original, $original,
fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getAttributes($attribute) === [], fn (ObjectDescription $object): bool => isset($object->reflectionClass) === false || $object->reflectionClass->getAttributes($attribute) === [],
"to not have attribute '{$attribute}'", "to not have attribute [{$attribute}]",
FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class')) FileLineFinder::where(fn (string $line): bool => str_contains($line, 'class'))
); );
} }
+2 -2
View File
@@ -170,7 +170,7 @@ final class TestCaseFactory
eval($classCode); eval($classCode);
} catch (ParseError $caught) { } catch (ParseError $caught) {
throw new RuntimeException(sprintf( throw new RuntimeException(sprintf(
"Unable to create test case for test file at %s. \n %s", "Unable to create test case for test file at [%s]. \n %s",
$filename, $filename,
$classCode $classCode
), 1, $caught); ), 1, $caught);
@@ -237,6 +237,6 @@ final class TestCaseFactory
} }
} }
throw ShouldNotHappen::fromMessage(sprintf('Method %s not found.', $methodName)); throw ShouldNotHappen::fromMessage(sprintf('Method [%s] not found.', $methodName));
} }
} }
+3 -3
View File
@@ -541,7 +541,7 @@ final class Expectation
/* @phpstan-ignore-next-line */ /* @phpstan-ignore-next-line */
} catch (ExpectationFailedException $exception) { } catch (ExpectationFailedException $exception) {
if ($message === '') { if ($message === '') {
$message = "Failed asserting that an array has the key '$key'"; $message = "Failed asserting that an array has the key [$key]";
} }
throw new ExpectationFailedException($message, $exception->getComparisonFailure()); throw new ExpectationFailedException($message, $exception->getComparisonFailure());
@@ -863,10 +863,10 @@ final class Expectation
Assert::assertTrue(true); Assert::assertTrue(true);
if (! $exception instanceof Throwable && ! class_exists($exception)) { if (! $exception instanceof Throwable && ! class_exists($exception)) {
throw new ExpectationFailedException("Exception with message \"$exception\" not thrown."); throw new ExpectationFailedException("Exception with message [$exception] not thrown.");
} }
throw new ExpectationFailedException("Exception \"$exception\" not thrown."); throw new ExpectationFailedException("Exception [$exception] not thrown.");
} }
private function export(mixed $value): string private function export(mixed $value): string
+1 -1
View File
@@ -504,7 +504,7 @@ final class TestCall // @phpstan-ignore-line
$isFunction = function_exists($classOrFunction); $isFunction = function_exists($classOrFunction);
if (! $isClass && ! $isTrait && ! $isFunction) { if (! $isClass && ! $isTrait && ! $isFunction) {
throw new InvalidArgumentException(sprintf('No class, trait or method named "%s" has been found.', $classOrFunction)); throw new InvalidArgumentException(sprintf('No class, trait or method named [%s] has been found.', $classOrFunction));
} }
if ($isClass) { if ($isClass) {
+9 -3
View File
@@ -17,7 +17,7 @@ use Symfony\Component\Process\Process;
*/ */
final readonly class BaselineSync final readonly class BaselineSync
{ {
private const string WORKFLOW_FILE = 'tia-baseline.yml'; private const string DEFAULT_WORKFLOW_FILE = 'tia-baseline.yml';
private const string ARTIFACT_NAME = 'pest-tia-baseline'; private const string ARTIFACT_NAME = 'pest-tia-baseline';
@@ -57,8 +57,14 @@ final readonly class BaselineSync
public function __construct( public function __construct(
private State $state, private State $state,
private OutputInterface $output, private OutputInterface $output,
private WatchPatterns $watchPatterns,
) {} ) {}
private function workflowFile(): string
{
return $this->watchPatterns->baselineWorkflow() ?? self::DEFAULT_WORKFLOW_FILE;
}
private function renderBadge(string $type, string $content): void private function renderBadge(string $type, string $content): void
{ {
View::render('components.badge', ['type' => $type, 'content' => $content]); View::render('components.badge', ['type' => $type, 'content' => $content]);
@@ -276,7 +282,7 @@ final readonly class BaselineSync
Panic::with(new BaselineFetchFailed( Panic::with(new BaselineFetchFailed(
sprintf('%s — %s', $contextPrefix, $diagnosis['message']), sprintf('%s — %s', $contextPrefix, $diagnosis['message']),
'Verify workflow tia-baseline.yml, artifact pest-tia-baseline, and gh token scope.', sprintf('Verify workflow [%s], artifact [%s], and gh token scope.', $this->workflowFile(), self::ARTIFACT_NAME),
$hasAnchor, $hasAnchor,
)); ));
} }
@@ -528,7 +534,7 @@ final readonly class BaselineSync
$process = new Process([ $process = new Process([
'gh', 'run', 'list', 'gh', 'run', 'list',
'-R', $repo, '-R', $repo,
'--workflow', self::WORKFLOW_FILE, '--workflow', $this->workflowFile(),
'--status', 'success', '--status', 'success',
'--limit', '1', '--limit', '1',
'--json', 'databaseId', '--json', 'databaseId',
+5 -1
View File
@@ -51,12 +51,16 @@ final class Configuration
/** /**
* @return $this * @return $this
*/ */
public function baselined(): self public function baselined(?string $workflow = null): self
{ {
/** @var WatchPatterns $watchPatterns */ /** @var WatchPatterns $watchPatterns */
$watchPatterns = Container::getInstance()->get(WatchPatterns::class); $watchPatterns = Container::getInstance()->get(WatchPatterns::class);
$watchPatterns->markBaselined(); $watchPatterns->markBaselined();
if ($workflow !== null) {
$watchPatterns->setBaselineWorkflow($workflow);
}
return $this; return $this;
} }
+12 -6
View File
@@ -27,10 +27,12 @@ final class CoverageCollector
} }
try { try {
$lineCoverage = PhpUnitCodeCoverage::instance() $data = PhpUnitCodeCoverage::instance()
->codeCoverage() ->codeCoverage()
->getData() ->getData();
->lineCoverage();
$lineCoverage = $data->lineCoverage();
$idByIndex = $data->testIds();
} catch (Throwable) { } catch (Throwable) {
return []; return [];
} }
@@ -46,12 +48,16 @@ final class CoverageCollector
continue; continue;
} }
foreach ($hits as $id) { foreach (array_keys($hits) as $index) {
$testIds[$id] = true; if (! isset($idByIndex[$index])) {
continue;
}
$testIds[$index] = $idByIndex[$index];
} }
} }
foreach (array_keys($testIds) as $testId) { foreach ($testIds as $testId) {
$testFile = $this->testIdToFile($testId); $testFile = $this->testIdToFile($testId);
if ($testFile === null) { if ($testFile === null) {
+29 -9
View File
@@ -100,19 +100,32 @@ final class CoverageMerger
} }
$cachedData = $cached->getData(); $cachedData = $cached->getData();
$staleIndexes = [];
foreach ($cachedData->testIds() as $index => $id) {
if (in_array($id, $currentIds, true)) {
$staleIndexes[$index] = true;
}
}
if ($staleIndexes === []) {
return;
}
$lineCoverage = $cachedData->lineCoverage(); $lineCoverage = $cachedData->lineCoverage();
foreach ($lineCoverage as $file => $lines) { foreach ($lineCoverage as $file => $lines) {
foreach ($lines as $line => $ids) { foreach ($lines as $line => $hits) {
if ($ids === null) { if ($hits === null) {
continue; continue;
} }
if ($ids === []) { if ($hits === []) {
continue; continue;
} }
$filtered = array_values(array_diff($ids, $currentIds)); $filtered = array_diff_key($hits, $staleIndexes);
if ($filtered !== $ids) { if ($filtered !== $hits) {
$lineCoverage[$file][$line] = $filtered; $lineCoverage[$file][$line] = $filtered;
} }
} }
@@ -126,21 +139,28 @@ final class CoverageMerger
*/ */
private static function collectTestIds(CodeCoverage $coverage): array private static function collectTestIds(CodeCoverage $coverage): array
{ {
$data = $coverage->getData();
$idByIndex = $data->testIds();
$ids = []; $ids = [];
foreach ($coverage->getData()->lineCoverage() as $lines) { foreach ($data->lineCoverage() as $lines) {
foreach ($lines as $hits) { foreach ($lines as $hits) {
if ($hits === null) { if ($hits === null) {
continue; continue;
} }
foreach ($hits as $id) { foreach (array_keys($hits) as $index) {
$ids[$id] = true; if (! isset($idByIndex[$index])) {
continue;
}
$ids[$index] = $idByIndex[$index];
} }
} }
} }
return array_keys($ids); return array_values($ids);
} }
private static function state(): State private static function state(): State
+15
View File
@@ -46,6 +46,8 @@ final class WatchPatterns
private ?string $defaultBranch = null; private ?string $defaultBranch = null;
private ?string $baselineWorkflow = null;
public function useDefaults(string $projectRoot): void public function useDefaults(string $projectRoot): void
{ {
$testPath = TestSuite::getInstance()->testPath; $testPath = TestSuite::getInstance()->testPath;
@@ -189,6 +191,18 @@ final class WatchPatterns
return $this->defaultBranch; return $this->defaultBranch;
} }
public function setBaselineWorkflow(string $workflow): void
{
$workflow = trim($workflow);
$this->baselineWorkflow = $workflow === '' ? null : $workflow;
}
public function baselineWorkflow(): ?string
{
return $this->baselineWorkflow;
}
public function reset(): void public function reset(): void
{ {
$this->patterns = []; $this->patterns = [];
@@ -198,6 +212,7 @@ final class WatchPatterns
$this->filtered = false; $this->filtered = false;
$this->baselined = false; $this->baselined = false;
$this->defaultBranch = null; $this->defaultBranch = null;
$this->baselineWorkflow = null;
} }
private function keyMatches(string $key, string $file): bool private function keyMatches(string $key, string $file): bool
+2 -2
View File
@@ -73,7 +73,7 @@ final class Container
if ($type instanceof \ReflectionType && $type->isBuiltin()) { if ($type instanceof \ReflectionType && $type->isBuiltin()) {
$candidate = $param->getName(); $candidate = $param->getName();
} else { } else {
throw ShouldNotHappen::fromMessage(sprintf('The type of `$%s` in `%s` cannot be determined.', $id, $param->getName())); throw ShouldNotHappen::fromMessage(sprintf('The type of [$%s] in [%s] cannot be determined.', $id, $param->getName()));
} }
} }
@@ -88,6 +88,6 @@ final class Container
return $reflectionClass->newInstance(); return $reflectionClass->newInstance();
} }
throw ShouldNotHappen::fromMessage(sprintf('A dependency with the name `%s` cannot be resolved.', $id)); throw ShouldNotHappen::fromMessage(sprintf('A dependency with the name [%s] cannot be resolved.', $id));
} }
} }
+1 -1
View File
@@ -74,7 +74,7 @@ final class Coverage
return 0.0; return 0.0;
} }
throw ShouldNotHappen::fromMessage(sprintf('Coverage not found in path: %s.', $reportPath)); throw ShouldNotHappen::fromMessage(sprintf('Coverage not found in path: [%s].', $reportPath));
} }
CoverageMerger::applyIfMarked($reportPath); CoverageMerger::applyIfMarked($reportPath);
+1 -1
View File
@@ -9,4 +9,4 @@ it('throws exception if no class nor method has been found', function (): void {
$testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure'); $testCall = new TestCall(TestSuite::getInstance(), 'filename', 'description', fn () => 'closure');
$testCall->covers('fakeName'); $testCall->covers('fakeName');
})->throws(InvalidArgumentException::class, 'No class, trait or method named "fakeName" has been found.'); })->throws(InvalidArgumentException::class, 'No class, trait or method named [fakeName] has been found.');
+2 -2
View File
@@ -21,7 +21,7 @@ test('reports missing datasets as errors for a single file run', function () use
$result = $run('tests/Fixtures/Suites/IssueOnly.php'); $result = $run('tests/Fixtures/Suites/IssueOnly.php');
expect($result['output']) expect($result['output'])
->toContain("A dataset with the name [missing] does not exist. You can create it using `dataset('missing', ['a', 'b']);`.") ->toContain("A dataset named [missing] does not exist. You may create one using `dataset('missing', ['a', 'b']);`.")
->toContain('FAILED') ->toContain('FAILED')
->toContain('Tests: 1 failed') ->toContain('Tests: 1 failed')
->and($result['code'])->not->toBe(0); ->and($result['code'])->not->toBe(0);
@@ -31,7 +31,7 @@ test('reports missing datasets as errors alongside passing tests', function () u
$result = $run('tests/Fixtures/Suites/IssueWithPassing.php'); $result = $run('tests/Fixtures/Suites/IssueWithPassing.php');
expect($result['output']) expect($result['output'])
->toContain("A dataset with the name [missing] does not exist. You can create it using `dataset('missing', ['a', 'b']);`.") ->toContain("A dataset named [missing] does not exist. You may create one using `dataset('missing', ['a', 'b']);`.")
->toContain('1 passed') ->toContain('1 passed')
->toContain('1 failed') ->toContain('1 failed')
->and($result['code'])->not->toBe(0); ->and($result['code'])->not->toBe(0);
+2 -2
View File
@@ -10,12 +10,12 @@ beforeEach(function (): void {
}); });
it('throws exception if dataset does not exist', function (): void { it('throws exception if dataset does not exist', function (): void {
expect(fn () => DatasetsRepository::resolve(['first'], __FILE__))->toThrow(DatasetDoesNotExist::class, "A dataset with the name [first] does not exist. You can create it using `dataset('first', ['a', 'b']);`."); expect(fn () => DatasetsRepository::resolve(['first'], __FILE__))->toThrow(DatasetDoesNotExist::class, "A dataset named [first] does not exist. You may create one using `dataset('first', ['a', 'b']);`.");
}); });
it('throws exception if dataset already exist', function (): void { it('throws exception if dataset already exist', function (): void {
DatasetsRepository::set('second', [[]], __DIR__); DatasetsRepository::set('second', [[]], __DIR__);
expect(fn () => DatasetsRepository::set('second', [[]], __DIR__))->toThrow(DatasetAlreadyExists::class, 'A dataset with the name [second] already exists in scope ['.__DIR__.'].'); expect(fn () => DatasetsRepository::set('second', [[]], __DIR__))->toThrow(DatasetAlreadyExists::class, 'A dataset named [second] is already registered in ['.__DIR__.'].');
}); });
it('sets closures', function (): void { it('sets closures', function (): void {
+1 -1
View File
@@ -31,7 +31,7 @@ test('failures with malformed input', function (): void {
test('failures with invalid type', function (): void { test('failures with invalid type', function (): void {
expect([])->toBeBase64(); expect([])->toBeBase64();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); })->throws(InvalidExpectationValue::class, 'This expectation may only be used on a value of type [string].');
test('failures with custom message', function (): void { test('failures with custom message', function (): void {
expect('!!invalid!!')->toBeBase64('oh no!'); expect('!!invalid!!')->toBeBase64('oh no!');
+1 -1
View File
@@ -22,7 +22,7 @@ test('failures with leading dot', function (): void {
test('failures with invalid type', function (): void { test('failures with invalid type', function (): void {
expect([])->toBeDomain(); expect([])->toBeDomain();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); })->throws(InvalidExpectationValue::class, 'This expectation may only be used on a value of type [string].');
test('failures with custom message', function (): void { test('failures with custom message', function (): void {
expect('example')->toBeDomain('oh no!'); expect('example')->toBeDomain('oh no!');
+1 -1
View File
@@ -31,7 +31,7 @@ test('failures with empty string', function (): void {
test('failures with invalid type', function (): void { test('failures with invalid type', function (): void {
expect([])->toBeHexadecimal(); expect([])->toBeHexadecimal();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); })->throws(InvalidExpectationValue::class, 'This expectation may only be used on a value of type [string].');
test('failures with custom message', function (): void { test('failures with custom message', function (): void {
expect('xyz')->toBeHexadecimal('oh no!'); expect('xyz')->toBeHexadecimal('oh no!');
+1 -1
View File
@@ -22,7 +22,7 @@ test('failures with trailing hyphen', function (): void {
test('failures with invalid type', function (): void { test('failures with invalid type', function (): void {
expect([])->toBeHostname(); expect([])->toBeHostname();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); })->throws(InvalidExpectationValue::class, 'This expectation may only be used on a value of type [string].');
test('failures with custom message', function (): void { test('failures with custom message', function (): void {
expect('-example')->toBeHostname('oh no!'); expect('-example')->toBeHostname('oh no!');
+1 -1
View File
@@ -17,7 +17,7 @@ test('failures', function (): void {
test('failures with invalid type', function (): void { test('failures with invalid type', function (): void {
expect([])->toBeIpAddress(); expect([])->toBeIpAddress();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); })->throws(InvalidExpectationValue::class, 'This expectation may only be used on a value of type [string].');
test('failures with custom message', function (): void { test('failures with custom message', function (): void {
expect('not-an-ip')->toBeIpAddress('oh no!'); expect('not-an-ip')->toBeIpAddress('oh no!');
+1 -1
View File
@@ -17,7 +17,7 @@ test('failures', function (): void {
test('failures with invalid type', function (): void { test('failures with invalid type', function (): void {
expect([])->toBeMacAddress(); expect([])->toBeMacAddress();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); })->throws(InvalidExpectationValue::class, 'This expectation may only be used on a value of type [string].');
test('failures with custom message', function (): void { test('failures with custom message', function (): void {
expect('not-a-mac')->toBeMacAddress('oh no!'); expect('not-a-mac')->toBeMacAddress('oh no!');
+1 -1
View File
@@ -7,7 +7,7 @@ use PHPUnit\Framework\ExpectationFailedException;
test('failures with wrong type', function (): void { test('failures with wrong type', function (): void {
expect([])->toBeUlid(); expect([])->toBeUlid();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); })->throws(InvalidExpectationValue::class, 'This expectation may only be used on a value of type [string].');
test('pass', function (): void { test('pass', function (): void {
expect('01ARZ3NDEKTSV4RRFFQ69G5FAV')->toBeUlid() expect('01ARZ3NDEKTSV4RRFFQ69G5FAV')->toBeUlid()
+1 -1
View File
@@ -7,7 +7,7 @@ use PHPUnit\Framework\ExpectationFailedException;
test('failures with wrong type', function (): void { test('failures with wrong type', function (): void {
expect([])->toBeUuid(); expect([])->toBeUuid();
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [string].'); })->throws(InvalidExpectationValue::class, 'This expectation may only be used on a value of type [string].');
test('pass', function (): void { test('pass', function (): void {
expect('3cafb226-4326-11ee-a516-846993788c86')->toBeUuid() expect('3cafb226-4326-11ee-a516-846993788c86')->toBeUuid()
+1 -1
View File
@@ -11,7 +11,7 @@ test('pass', function (): void {
test('failures with invalid type', function (): void { test('failures with invalid type', function (): void {
expect('foo')->toHaveCount(3); expect('foo')->toHaveCount(3);
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [countable|iterable]'); })->throws(InvalidExpectationValue::class, 'This expectation may only be used on a value of type [countable|iterable]');
test('failures', function (): void { test('failures', function (): void {
expect([1, 2, 3])->toHaveCount(4); expect([1, 2, 3])->toHaveCount(4);
+3 -3
View File
@@ -24,7 +24,7 @@ test('pass with value check and plain key with dots')->expect($test_array)->toHa
test('failures', function () use ($test_array): void { test('failures', function () use ($test_array): void {
expect($test_array)->toHaveKey('foo'); expect($test_array)->toHaveKey('foo');
})->throws(ExpectationFailedException::class, "Failed asserting that an array has the key 'foo'"); })->throws(ExpectationFailedException::class, 'Failed asserting that an array has the key [foo]');
test('failures with custom message', function () use ($test_array): void { test('failures with custom message', function () use ($test_array): void {
expect($test_array)->toHaveKey('foo', message: 'oh no!'); expect($test_array)->toHaveKey('foo', message: 'oh no!');
@@ -36,7 +36,7 @@ test('failures with custom message and Any matcher', function () use ($test_arra
test('failures with nested key', function () use ($test_array): void { test('failures with nested key', function () use ($test_array): void {
expect($test_array)->toHaveKey('d.bar'); expect($test_array)->toHaveKey('d.bar');
})->throws(ExpectationFailedException::class, "Failed asserting that an array has the key 'd.bar'"); })->throws(ExpectationFailedException::class, 'Failed asserting that an array has the key [d.bar]');
test('failures with nested key and custom message', function () use ($test_array): void { test('failures with nested key and custom message', function () use ($test_array): void {
expect($test_array)->toHaveKey('d.bar', message: 'oh no!'); expect($test_array)->toHaveKey('d.bar', message: 'oh no!');
@@ -48,7 +48,7 @@ test('failures with nested key and custom message with Any matcher', function ()
test('failures with plain key with dots', function () use ($test_array): void { test('failures with plain key with dots', function () use ($test_array): void {
expect($test_array)->toHaveKey('missing.key.with.dots'); expect($test_array)->toHaveKey('missing.key.with.dots');
})->throws(ExpectationFailedException::class, "Failed asserting that an array has the key 'missing.key.with.dots'"); })->throws(ExpectationFailedException::class, 'Failed asserting that an array has the key [missing.key.with.dots]');
test('fails with wrong value', function () use ($test_array): void { test('fails with wrong value', function () use ($test_array): void {
expect($test_array)->toHaveKey('c', 'bar'); expect($test_array)->toHaveKey('c', 'bar');
+1 -1
View File
@@ -7,7 +7,7 @@ use PHPUnit\Framework\ExpectationFailedException;
test('failures with wrong type', function (): void { test('failures with wrong type', function (): void {
expect('foo')->toHaveSameSize([1]); expect('foo')->toHaveSameSize([1]);
})->throws(InvalidExpectationValue::class, 'Invalid expectation value type. Expected [countable|iterable].'); })->throws(InvalidExpectationValue::class, 'This expectation may only be used on a value of type [countable|iterable].');
test('pass', function (): void { test('pass', function (): void {
expect([1, 2, 3])->toHaveSameSize([4, 5, 6]); expect([1, 2, 3])->toHaveSameSize([4, 5, 6]);
+3 -3
View File
@@ -33,11 +33,11 @@ test('passes', function (): void {
test('failures 1', function (): void { test('failures 1', function (): void {
expect(function (): void {})->toThrow(RuntimeException::class); expect(function (): void {})->toThrow(RuntimeException::class);
})->throws(ExpectationFailedException::class, 'Exception "'.RuntimeException::class.'" not thrown.'); })->throws(ExpectationFailedException::class, 'Exception ['.RuntimeException::class.'] not thrown.');
test('failures 2', function (): void { test('failures 2', function (): void {
expect(function (): void {})->toThrow(function (RuntimeException $e): void {}); expect(function (): void {})->toThrow(function (RuntimeException $e): void {});
})->throws(ExpectationFailedException::class, 'Exception "'.RuntimeException::class.'" not thrown.'); })->throws(ExpectationFailedException::class, 'Exception ['.RuntimeException::class.'] not thrown.');
test('failures 3', function (): void { test('failures 3', function (): void {
expect(function (): void { expect(function (): void {
@@ -64,7 +64,7 @@ test('failures 5', function (): void {
test('failures 6', function (): void { test('failures 6', function (): void {
expect(function (): void {})->toThrow('actual message'); expect(function (): void {})->toThrow('actual message');
})->throws(ExpectationFailedException::class, 'Exception with message "actual message" not thrown'); })->throws(ExpectationFailedException::class, 'Exception with message [actual message] not thrown');
test('failures 7', function (): void { test('failures 7', function (): void {
expect(function (): void { expect(function (): void {
@@ -175,7 +175,7 @@ test('tia still requires git', function (): void {
$result = $project->pest('--tia'); $result = $project->pest('--tia');
expect($result->output)->toContain('The feature [Tia mode] requires [git].') expect($result->output)->toContain('The [Tia mode] feature requires [git].')
->and($result->exitCode)->not->toBe(0); ->and($result->exitCode)->not->toBe(0);
})->skipOnWindows(); })->skipOnWindows();
+3 -3
View File
@@ -15,7 +15,7 @@ it('does not allow to add the same test description twice', function (): void {
$testSuite->tests->set($method); $testSuite->tests->set($method);
})->throws( })->throws(
TestAlreadyExist::class, TestAlreadyExist::class,
sprintf('A test with the description [%s] already exists in the filename [%s].', 'bar', 'foo'), sprintf('A test named [%s] already exists in [%s]. Please give this test a different description.', 'bar', 'foo'),
); );
it('does not allow static closures', function (): void { it('does not allow static closures', function (): void {
@@ -27,7 +27,7 @@ it('does not allow static closures', function (): void {
$testSuite->tests->set($method); $testSuite->tests->set($method);
})->throws( })->throws(
TestClosureMustNotBeStatic::class, TestClosureMustNotBeStatic::class,
'Test closure must not be static. Please remove the [static] keyword from the [bar] method in [foo].', 'Test closures may not be static. Please remove the [static] keyword from the test [bar] in [foo].',
); );
it('alerts users about tests with arguments but no input', function (): void { it('alerts users about tests with arguments but no input', function (): void {
@@ -40,7 +40,7 @@ it('alerts users about tests with arguments but no input', function (): void {
$testSuite->tests->set($method); $testSuite->tests->set($method);
})->throws( })->throws(
DatasetMissing::class, DatasetMissing::class,
sprintf('A test with the description [%s] has [%d] argument(s) ([%s]) and no dataset(s) provided in [%s]', 'bar', 1, 'int $arg', 'foo'), sprintf('The test [%s] in [%s] expects [%d] argument(s) ([%s]), but no dataset was provided. Please chain [with()] onto the test to supply one.', 'bar', 'foo', 1, 'int $arg'),
); );
it('can return an array of all test suite filenames', function (): void { it('can return an array of all test suite filenames', function (): void {
+1 -1
View File
@@ -45,7 +45,7 @@ test('a parallel test can extend another test with same name', function () use (
test('parallel reports invalid datasets as failures', function () use ($run): void { test('parallel reports invalid datasets as failures', function () use ($run): void {
expect($run('tests/Fixtures/Suites/ParallelInvalidDataset')) expect($run('tests/Fixtures/Suites/ParallelInvalidDataset'))
->toContain("A dataset with the name [missing.dataset] does not exist. You can create it using `dataset('missing.dataset', ['a', 'b']);`.") ->toContain("A dataset named [missing.dataset] does not exist. You may create one using `dataset('missing.dataset', ['a', 'b']);`.")
->toContain('Tests: 1 failed, 1 passed (1 assertions)') ->toContain('Tests: 1 failed, 1 passed (1 assertions)')
->toContain('Parallel: 3 processes'); ->toContain('Parallel: 3 processes');
})->skipOnWindows(); })->skipOnWindows();