Skip to content

Commit

Permalink
Add Retry support
Browse files Browse the repository at this point in the history
  • Loading branch information
gehrisandro committed Dec 4, 2023
1 parent 4c43b04 commit 74de31c
Show file tree
Hide file tree
Showing 19 changed files with 267 additions and 11 deletions.
17 changes: 17 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,6 +140,7 @@ The following options are available.
- [`stopOnSurvived()`](#stopOnSurvived)
- [`stopOnNotCovered()`](#stopOnNotCovered)
- [`bail()`](#bail)
- [`retry()`](#retry)
- [`min()`](#min)
- [`ignoreMinScoreOnZeroMutations()`](#ignoreMinScoreOnZeroMutations)

Expand Down Expand Up @@ -298,6 +299,22 @@ mutate()
```


<a name="options-retry"></a>
### `retry()`
CLI: `--retry`

If a mutation previously survived, you typically want to run them first. In such cases, you can use the `--retry` option.

The `--retry` flag reorders your mutations by prioritizing the previously survived mutations. If there were no past survivals, the suite runs as usual.

Additionally, it will stop execution upon first survived mutant.

```php
mutate()
->retry();
```


<a name="options-min"></a>
### `min()`
CLI: `--min`
Expand Down
5 changes: 1 addition & 4 deletions WIP.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@
- [ ] Allow registering Custom Mutators
- [ ] Disable mutations by annotation
- [x] Caching
- [ ] Order mutations to execute
- [x] Retry (runs survived mutations first)
- [ ] Verbose output
- [ ] Text log
- [ ] HTML report
Expand All @@ -29,8 +29,6 @@
# Known Bugs

# Backlog Prio 1
- [ ] Run test that killed a mutation first
- [ ] Run mutations in a reasonable order: New, Survived, NotCovered, Skipped, Killed (Survived first, if --stop-on-survived or --bail; NotCovered first, if --stop-on-uncovered)
- [ ] Automatically empty cache when package version changes / Maybe there is another approach: Use the same cache key per php file, but store a hash of the file content and the package version in the cache. If the hash changes, the cache is invalid.
- [ ] Automatically skip "Arch" and "Stressless" tests - wait for arch() and stress() aliases for test()
- [ ] Properly support xdebug
Expand All @@ -46,7 +44,6 @@
- [ ] Dedicated help output (`vendor/bin/pest --mutate --help`)
- [ ] Add help to show available mutators and sets
- [ ] Add more Laravel mutators
- [ ] Improve test filtering. Some test names, may not work
- [ ] Better loop detection. For example when mutate break to continue in a while true loop
- [ ] Beautify output
- [ ] HTML report
Expand Down
17 changes: 17 additions & 0 deletions docs/mutation-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ The following options are available.
- [`stopOnSurvived()`](#stopOnSurvived)
- [`stopOnNotCovered()`](#stopOnNotCovered)
- [`bail()`](#bail)
- [`retry()`](#retry)
- [`min()`](#min)
- [`ignoreMinScoreOnZeroMutations()`](#ignoreMinScoreOnZeroMutations)

Expand Down Expand Up @@ -288,6 +289,22 @@ mutate()
```


<a name="options-retry"></a>
### `retry()`
CLI: `--retry`

If a mutation previously survived, you typically want to run them first. In such cases, you can use the `--retry` option.

The `--retry` flag reorders your mutations by prioritizing the previously survived mutations. If there were no past survivals, the suite runs as usual.

Additionally, it will stop execution upon first survived mutant.

```php
mutate()
->retry();
```


<a name="options-min"></a>
### `min()`
CLI: `--min`
Expand Down
2 changes: 2 additions & 0 deletions src/Contracts/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -52,4 +52,6 @@ public function class(array|string ...$classes): self;
public function uncommittedOnly(bool $uncommittedOnly = true): self;

public function changedOnly(?string $branch = 'main'): self;

public function retry(bool $retry = true): self;
}
11 changes: 11 additions & 0 deletions src/Decorators/TestCallDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -187,5 +187,16 @@ public function changedOnly(?string $branch = 'main'): Configuration

return $this;
}

public function retry(bool $retry = true): Configuration
{
$this->configuration->retry($retry);

if ($retry) {
$this->configuration->stopOnSurvived();
}

return $this;
}
}
// @codeCoverageIgnoreEnd
13 changes: 13 additions & 0 deletions src/MutationTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,11 @@ public function __construct(public readonly Mutation $mutation)
{
}

public function getId(): string
{
return hash('xxh3', $this->mutation->file->getRealPath().$this->mutation->mutator.$this->mutation->startLine);
}

public function result(): MutationTestResult
{
return $this->result;
Expand Down Expand Up @@ -154,4 +159,12 @@ public function duration(): float

return $this->finish - $this->start;
}

/**
* @param array<string, string> $results
*/
public function lastRunResult(array $results): MutationTestResult
{
return MutationTestResult::from($results[$this->getId()] ?? 'none');
}
}
34 changes: 34 additions & 0 deletions src/MutationTestCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Pest\Mutate;

use Pest\Mutate\Support\MutationTestResult;
use Pest\Mutate\Support\ResultCache;
use Symfony\Component\Finder\SplFileInfo;

class MutationTestCollection
Expand Down Expand Up @@ -60,4 +61,37 @@ public function notRun(): int
{
return count(array_filter($this->tests, fn (MutationTest $test): bool => $test->result() === MutationTestResult::None));
}

public function hasLastRunSurvivedMutation(): bool
{
return array_filter(ResultCache::instance()->get($this), fn (string $result): bool => $result === MutationTestResult::Survived->value) !== [];
}

/**
* @return array<string, string>
*/
public function results(): array
{
$results = [];

foreach ($this->tests as $test) {
if ($test->result() !== MutationTestResult::None) {
$results[$test->getId()] = $test->result()->value;
}
}

return $results;
}

public function sortBySurvivedFirst(): void
{
$lastRunResults = ResultCache::instance()->get($this);

usort($this->tests, fn (MutationTest $a, MutationTest $b): int => ($b->lastRunResult($lastRunResults) === MutationTestResult::Survived) <=> ($a->lastRunResult($lastRunResults) === MutationTestResult::Survived));
}

public function isComplete(): bool
{
return array_filter($this->tests, fn (MutationTest $test): bool => $test->result() === MutationTestResult::None) === [];
}
}
28 changes: 28 additions & 0 deletions src/Options/RetryOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
<?php

declare(strict_types=1);

namespace Pest\Mutate\Options;

use Symfony\Component\Console\Input\InputOption;

class RetryOption
{
final public const ARGUMENT = 'retry';

public static function remove(): bool
{
return true;
}

public static function match(string $argument): bool
{
return $argument === sprintf('--%s', self::ARGUMENT) ||
str_starts_with($argument, sprintf('--%s=', self::ARGUMENT));
}

public static function inputOption(): InputOption
{
return new InputOption(sprintf('--%s', self::ARGUMENT), null, InputOption::VALUE_OPTIONAL, '');
}
}
1 change: 1 addition & 0 deletions src/Repositories/ConfigurationRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -108,6 +108,7 @@ classes: $config['classes'] ?? [],
stopOnNotCovered: $config['stop_on_not_covered'] ?? false,
uncommittedOnly: $config['uncommitted_only'] ?? false,
changedOnly: $config['changed_only'] ?? false,
retry: $config['retry'] ?? false,
);
}

Expand Down
17 changes: 17 additions & 0 deletions src/Repositories/MutationRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
use Pest\Mutate\Mutation;
use Pest\Mutate\MutationTest;
use Pest\Mutate\MutationTestCollection;
use Pest\Mutate\Support\ResultCache;

class MutationRepository
{
Expand Down Expand Up @@ -92,4 +93,20 @@ public function slowest(): array

return array_slice($allTests, 0, 10);
}

public function sortBySurvivedFirst(): void
{
usort($this->tests, fn (MutationTestCollection $a, MutationTestCollection $b): int => $b->hasLastRunSurvivedMutation() <=> $a->hasLastRunSurvivedMutation());

foreach ($this->tests as $testCollection) {
$testCollection->sortBySurvivedFirst();
}
}

public function saveResults(): void
{
foreach ($this->tests as $testCollection) {
ResultCache::instance()->put($testCollection);
}
}
}
16 changes: 15 additions & 1 deletion src/Support/Configuration/AbstractConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -56,6 +56,8 @@ abstract class AbstractConfiguration implements ConfigurationContract

private ?string $changedOnly = null;

private ?bool $retry = null;

/**
* {@inheritDoc}
*/
Expand Down Expand Up @@ -189,7 +191,7 @@ public function changedOnly(?string $branch = 'main'): self
}

/**
* @return array{paths?: string[], paths_to_ignore?: string[], mutators?: class-string<Mutator>[], excluded_mutators?: class-string<Mutator>[], classes?: string[], parallel?: bool, processes?: int, profile?: bool, min_score?: float, ignore_min_score_on_zero_mutations?: bool, covered_only?: bool, stop_on_survived?: bool, stop_on_not_covered?: bool, uncommitted_only?: bool, changed_only?: string}
* @return array{paths?: string[], paths_to_ignore?: string[], mutators?: class-string<Mutator>[], excluded_mutators?: class-string<Mutator>[], classes?: string[], parallel?: bool, processes?: int, profile?: bool, min_score?: float, ignore_min_score_on_zero_mutations?: bool, covered_only?: bool, stop_on_survived?: bool, stop_on_not_covered?: bool, uncommitted_only?: bool, changed_only?: string, retry?: bool}
*/
public function toArray(): array
{
Expand All @@ -209,6 +211,7 @@ public function toArray(): array
'stop_on_not_covered' => $this->stopOnNotCovered,
'uncommitted_only' => $this->uncommittedOnly,
'changed_only' => $this->changedOnly,
'retry' => $this->retry,
], fn (mixed $value): bool => ! is_null($value));
}

Expand Down Expand Up @@ -244,4 +247,15 @@ function (string $mutator): string {

return $mutators; // @phpstan-ignore-line
}

public function retry(bool $retry = true): self
{
$this->retry = $retry;

if ($retry) {
$this->stopOnSurvived = true;
}

return $this;
}
}
7 changes: 7 additions & 0 deletions src/Support/Configuration/CliConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
use Pest\Mutate\Options\PathOption;
use Pest\Mutate\Options\ProcessesOption;
use Pest\Mutate\Options\ProfileOption;
use Pest\Mutate\Options\RetryOption;
use Pest\Mutate\Options\StopOnNotCoveredOption;
use Pest\Mutate\Options\StopOnSurvivedOption;
use Pest\Mutate\Options\UncommittedOnlyOption;
Expand All @@ -44,6 +45,7 @@ class CliConfiguration extends AbstractConfiguration
BailOption::class,
UncommittedOnlyOption::class,
ChangedOnlyOption::class,
RetryOption::class,
];

/**
Expand Down Expand Up @@ -147,6 +149,11 @@ public function fromArguments(array $arguments): array
$this->changedOnly($input->getOption(ChangedOnlyOption::ARGUMENT) !== null ? (string) $input->getOption(ChangedOnlyOption::ARGUMENT) : 'main'); // @phpstan-ignore-line
}

if ($input->hasOption(RetryOption::ARGUMENT)) {
$this->retry($input->getOption(RetryOption::ARGUMENT) !== 'false');

}

return $arguments;
}
}
1 change: 1 addition & 0 deletions src/Support/Configuration/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function __construct(
public readonly bool $stopOnNotCovered,
public readonly bool $uncommittedOnly,
public readonly string|false $changedOnly,
public readonly bool $retry,
) {
}
}
12 changes: 6 additions & 6 deletions src/Support/MutationTestResult.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@

namespace Pest\Mutate\Support;

enum MutationTestResult
enum MutationTestResult: string
{
case None;
case NotCovered;
case Killed;
case Survived;
case Timeout;
case None = 'none';
case NotCovered = 'not-covered';
case Killed = 'killed';
case Survived = 'survived';
case Timeout = 'timeout';
}
Loading

0 comments on commit 74de31c

Please sign in to comment.