Skip to content

Commit

Permalink
Add min()
Browse files Browse the repository at this point in the history
  • Loading branch information
gehrisandro committed Nov 20, 2023
1 parent 707dff0 commit 790c488
Show file tree
Hide file tree
Showing 17 changed files with 92 additions and 34 deletions.
6 changes: 2 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -368,11 +368,9 @@ Sometimes, you may want to ignore a specific mutation or line of code. To do so,
}
```

## Minimum Threshold Enforcement
## Minimum Score Threshold Enforcement

> WIP: The `min()` option is not implemented yet!
Just like code coverage, mutation coverage can also be enforced. You can use the `--mutate` and `--min` options to define the minimum threshold values for the mutation score index. If the specified thresholds are not met, Pest will report a failure.
Just like code coverage, mutation coverage can also be enforced. You can use the `--mutate` and `--min` options to define the minimum threshold value for the mutation score. If the specified threshold is not met, Pest will report a failure.

```bash
./vendor/bin/pest --mutate --min=100
Expand Down
6 changes: 2 additions & 4 deletions docs/mutation-testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -360,11 +360,9 @@ Sometimes, you may want to ignore a specific mutation or line of code. To do so,
}
```

## Minimum Threshold Enforcement
## Minimum Score Threshold Enforcement

> WIP: The `min()` option is not implemented yet!
Just like code coverage, mutation coverage can also be enforced. You can use the `--mutate` and `--min` options to define the minimum threshold values for the mutation score index. If the specified thresholds are not met, Pest will report a failure.
Just like code coverage, mutation coverage can also be enforced. You can use the `--mutate` and `--min` options to define the minimum threshold value for the mutation score. If the specified threshold is not met, Pest will report a failure.

```bash
./vendor/bin/pest --mutate --min=100
Expand Down
2 changes: 1 addition & 1 deletion src/Contracts/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ public function mutator(array|string ...$mutators): self;
*/
public function except(array|string ...$mutators): self;

public function min(float $minMSI): self;
public function min(float $minScore): self;

public function coveredOnly(bool $coveredOnly = true): self;

Expand Down
2 changes: 2 additions & 0 deletions src/Contracts/Printer.php
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@ public function reportTimedOutMutation(MutationTest $test): void;

public function reportError(string $message): void;

public function reportScoreNotReached(float $scoreReached, float $scoreRequired): void;

public function printFilename(MutationTestCollection $testCollection): void;

public function reportMutationGenerationStarted(MutationSuite $mutationSuite): void;
Expand Down
4 changes: 2 additions & 2 deletions src/Decorators/TestCallDecorator.php
Original file line number Diff line number Diff line change
Expand Up @@ -65,9 +65,9 @@ public function coveredOnly(bool $coveredOnly = true): self
return $this;
}

public function min(float $minMSI): self
public function min(float $minScore): self
{
$this->configuration->min($minMSI);
$this->configuration->min($minScore);

return $this;
}
Expand Down
20 changes: 20 additions & 0 deletions src/MutationSuite.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,10 @@

namespace Pest\Mutate;

use Pest\Mutate\Repositories\ConfigurationRepository;
use Pest\Mutate\Repositories\MutationRepository;
use Pest\Mutate\Support\Configuration\Configuration;
use Pest\Support\Container;

class MutationSuite
{
Expand Down Expand Up @@ -44,4 +47,21 @@ public function trackFinish(): void
{
$this->finish = microtime(true);
}

public function score(): float
{
return $this->repository->score();
}

public function minScoreReached(): bool
{
/** @var Configuration $configuration */
$configuration = Container::getInstance()->get(ConfigurationRepository::class)->mergedConfiguration(); // @phpstan-ignore-line

if ($configuration->minScore === null) {
return true;
}

return $configuration->minScore <= $this->score();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@

use Symfony\Component\Console\Input\InputOption;

class MinMsiOption
class MinScoreOption
{
final public const ARGUMENT = 'min';

Expand Down
2 changes: 1 addition & 1 deletion src/Repositories/ConfigurationRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ public function mergedConfiguration(): Configuration
mutators: array_diff($config['mutators'] ?? DefaultSet::mutators(), $config['excluded_mutators'] ?? []),
classes: $config['classes'] ?? [],
parallel: $config['parallel'] ?? false,
minMSI: $config['min_msi'] ?? 0.0,
minScore: $config['min_score'] ?? null,
stopOnSurvived: $config['stop_on_survived'] ?? false,
stopOnNotCovered: $config['stop_on_not_covered'] ?? false,
uncommittedOnly: $config['uncommitted_only'] ?? false,
Expand Down
5 changes: 5 additions & 0 deletions src/Repositories/MutationRepository.php
Original file line number Diff line number Diff line change
Expand Up @@ -69,4 +69,9 @@ public function notRun(): int
{
return array_sum(array_map(fn (MutationTestCollection $testCollection): int => $testCollection->notRun(), $this->tests));
}

public function score(): float
{
return ($this->killed() + $this->timedOut()) / $this->total() * 100;
}
}
10 changes: 5 additions & 5 deletions src/Support/Configuration/AbstractConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@ abstract class AbstractConfiguration implements ConfigurationContract
*/
private ?array $classes = null;

private ?float $minMSI = null;
private ?float $minScore = null;

private ?bool $coveredOnly = null;

Expand Down Expand Up @@ -75,9 +75,9 @@ public function except(array|string ...$mutators): self
return $this;
}

public function min(float $minMSI): self
public function min(float $minScore): self
{
$this->minMSI = $minMSI;
$this->minScore = $minScore;

return $this;
}
Expand Down Expand Up @@ -143,7 +143,7 @@ public function changedOnly(?string $branch = 'main'): self
}

/**
* @return array{paths?: string[], mutators?: class-string<Mutator>[], excluded_mutators?: class-string<Mutator>[], classes?: string[], parallel?: bool, min_msi?: float, covered_only?: bool, stop_on_survived?: bool, stop_on_not_covered?: bool, uncommitted_only?: bool, changed_only?: string}
* @return array{paths?: string[], mutators?: class-string<Mutator>[], excluded_mutators?: class-string<Mutator>[], classes?: string[], parallel?: bool, min_score?: float, covered_only?: bool, stop_on_survived?: bool, stop_on_not_covered?: bool, uncommitted_only?: bool, changed_only?: string}
*/
public function toArray(): array
{
Expand All @@ -153,7 +153,7 @@ public function toArray(): array
'excluded_mutators' => $this->excludedMutators,
'classes' => $this->classes,
'parallel' => $this->parallel,
'min_msi' => $this->minMSI,
'min_score' => $this->minScore,
'covered_only' => $this->coveredOnly,
'stop_on_survived' => $this->stopOnSurvived,
'stop_on_not_covered' => $this->stopOnNotCovered,
Expand Down
8 changes: 4 additions & 4 deletions src/Support/Configuration/CliConfiguration.php
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
use Pest\Mutate\Options\ClassOption;
use Pest\Mutate\Options\CoveredOnlyOption;
use Pest\Mutate\Options\ExceptOption;
use Pest\Mutate\Options\MinMsiOption;
use Pest\Mutate\Options\MinScoreOption;
use Pest\Mutate\Options\MutateOption;
use Pest\Mutate\Options\MutatorsOption;
use Pest\Mutate\Options\ParallelOption;
Expand All @@ -26,7 +26,7 @@ class CliConfiguration extends AbstractConfiguration
MutateOption::class,
ClassOption::class,
CoveredOnlyOption::class,
MinMsiOption::class,
MinScoreOption::class,
MutatorsOption::class,
ExceptOption::class,
PathsOption::class,
Expand Down Expand Up @@ -77,8 +77,8 @@ public function fromArguments(array $arguments): array
$this->except(explode(',', (string) $input->getOption(ExceptOption::ARGUMENT))); // @phpstan-ignore-line
}

if ($input->hasOption(MinMsiOption::ARGUMENT)) {
$this->min((float) $input->getOption(MinMsiOption::ARGUMENT)); // @phpstan-ignore-line
if ($input->hasOption(MinScoreOption::ARGUMENT)) {
$this->min((float) $input->getOption(MinScoreOption::ARGUMENT)); // @phpstan-ignore-line
}

if ($input->hasOption(CoveredOnlyOption::ARGUMENT)) {
Expand Down
2 changes: 1 addition & 1 deletion src/Support/Configuration/Configuration.php
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ public function __construct(
public readonly array $mutators,
public readonly array $classes,
public readonly bool $parallel,
public readonly float $minMSI,
public readonly ?float $minScore,
public readonly bool $stopOnSurvived,
public readonly bool $stopOnNotCovered,
public readonly bool $uncommittedOnly,
Expand Down
16 changes: 14 additions & 2 deletions src/Support/Printers/DefaultPrinter.php
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,16 @@ public function reportError(string $message): void
{
$this->output->writeln([
'',
' <fg=default;bg=red;options=bold> ERROR </> '.$message.'</>',
' <fg=default;bg=red;options=bold> ERROR </> <fg=default>'.$message.'</>',
'',
]);
}

public function reportScoreNotReached(float $scoreReached, float $scoreRequired): void
{
$this->output->writeln([
'',
' <fg=white;bg=red;options=bold> FAIL </> Code coverage below expected:<fg=red;options=bold> '.number_format($scoreReached, 1).' %</>. Minimum:<fg=white;options=bold> '.number_format($scoreRequired, 1).' %</>.',
'',
]);
}
Expand Down Expand Up @@ -87,9 +96,12 @@ public function reportMutationSuiteFinished(MutationSuite $mutationSuite): void
$this->output->writeln([
'',
'',
' <fg=gray>Mutations:</> <fg=default><fg=red;options=bold>'.($mutationSuite->repository->survived() !== 0 ? $mutationSuite->repository->survived().' survived</><fg=gray>,</> ' : '').'<fg=yellow;options=bold>'.($mutationSuite->repository->notCovered() !== 0 ? $mutationSuite->repository->notCovered().' not covered</><fg=gray>,</> ' : '').'<fg=yellow;options=bold>'.($mutationSuite->repository->notRun() !== 0 ? $mutationSuite->repository->notRun().' pending</><fg=gray>,</> ' : '').'<fg=green;options=bold>'.($mutationSuite->repository->timedOut() !== 0 ? $mutationSuite->repository->timedOut().' timeout</><fg=gray>,</> ' : '').'<fg=green;options=bold>'.$mutationSuite->repository->killed().' killed</>',
' <fg=gray>Mutations:</> <fg=default>'.($mutationSuite->repository->survived() !== 0 ? '<fg=red;options=bold>'.$mutationSuite->repository->survived().' survived</><fg=gray>,</> ' : '').($mutationSuite->repository->notCovered() !== 0 ? '<fg=yellow;options=bold>'.$mutationSuite->repository->notCovered().' not covered</><fg=gray>,</> ' : '').($mutationSuite->repository->notRun() !== 0 ? '<fg=yellow;options=bold>'.$mutationSuite->repository->notRun().' pending</><fg=gray>,</> ' : '').($mutationSuite->repository->timedOut() !== 0 ? '<fg=green;options=bold>'.$mutationSuite->repository->timedOut().' timeout</><fg=gray>,</> ' : '').'<fg=green;options=bold>'.$mutationSuite->repository->killed().' killed</>',
]);

$score = number_format($mutationSuite->score(), 2);
$this->output->writeln(' <fg=gray>Score:</> <fg='.($mutationSuite->minScoreReached() ? 'default' : 'red').'>'.$score.'%</>');

$duration = number_format($mutationSuite->duration(), 2);
$this->output->writeln(' <fg=gray>Duration:</> <fg=default>'.$duration.'s</>');

Expand Down
23 changes: 23 additions & 0 deletions src/Tester/MutationTestRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,8 @@ classesToMutate: $this->getConfiguration()->classes,

Facade::instance()->emitter()->finishMutationSuite($mutationSuite);

$this->ensureMinScoreIsReached($mutationSuite);

exit(0); // TODO: exit with error on failure
}

Expand Down Expand Up @@ -204,4 +206,25 @@ private function getConfiguration(): Configuration
{
return Container::getInstance()->get(ConfigurationRepository::class)->mergedConfiguration(); // @phpstan-ignore-line
}

private function ensureMinScoreIsReached(MutationSuite $mutationSuite): void
{
$minScore = Container::getInstance()->get(ConfigurationRepository::class) // @phpstan-ignore-line
->mergedConfiguration()
->minScore;

if ($minScore === null) {
return;
}

$score = $mutationSuite->score();
if ($score >= $minScore) {
return;
}

Container::getInstance()->get(Printer::class) // @phpstan-ignore-line
->reportScoreNotReached($score, $minScore);

exit(1);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,10 +19,10 @@
})->mutate(ConfigurationRepository::FAKE)
->throws('test exception');

it('sets the min MSI from test', function (): void {
it('sets the min score threshold from test', function (): void {
$configuration = $this->repository->fakeTestConfiguration(ConfigurationRepository::FAKE.'_1');

expect($configuration->toArray()['min_msi'])
expect($configuration->toArray()['min_score'])
->toEqual(2.0);
})->mutate(ConfigurationRepository::FAKE.'_1')
->min(2);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,18 +48,18 @@
->mutators->toHaveCount(count(ArithmeticSet::mutators()) - 2);
});

it('sets MSI threshold if --min argument is passed', function (): void {
it('sets min score threshold if --min argument is passed', function (): void {
$this->configuration->fromArguments(['--mutate='.ConfigurationRepository::FAKE]);
expect($this->configuration->toArray())
->min_msi->toEqual(0.0);
->min_score->toEqual(0.0);

$this->configuration->fromArguments(['--min=2']);
expect($this->configuration->toArray())
->min_msi->toEqual(2.0);
->min_score->toEqual(2.0);

$this->configuration->fromArguments(['--min=2.4']);
expect($this->configuration->toArray())
->min_msi->toEqual(2.4);
->min_score->toEqual(2.4);
});

it('enables covered only option if --covered-only argument is passed', function (): void {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
mutate(ConfigurationRepository::FAKE)
->min(20.0);

expect($this->configuration->toArray()['min_msi'])
expect($this->configuration->toArray()['min_score'])
->toEqual(20.0);
});

Expand Down Expand Up @@ -78,11 +78,11 @@
->toHaveCount(count(ArithmeticSet::mutators()) - 2);
});

test('globally configure min MSI threshold', function (): void {
test('globally configure min score threshold', function (): void {
mutate(ConfigurationRepository::FAKE)
->min(10.0);

expect($this->configuration->toArray()['min_msi'])
expect($this->configuration->toArray()['min_score'])
->toEqual(10.0);
});

Expand Down

0 comments on commit 790c488

Please sign in to comment.