Skip to content

Commit

Permalink
Merge pull request #405 from Landerstraeten/infection-task
Browse files Browse the repository at this point in the history
Add infection task
  • Loading branch information
Landerstraeten authored Nov 17, 2017
2 parents cbd2c89 + 2de3220 commit 88b6b83
Show file tree
Hide file tree
Showing 9 changed files with 345 additions and 0 deletions.
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,7 @@ To make GrumPHP even more awesome, it will suggest installing some extra package
- codegyre/robo : ~0.7
- doctrine/orm: ~2.5
- friendsofphp/php-cs-fixer : ~1|~2
- infection/infection: ^0.5
- maglnet/composer-require-checker : ~0.1
- malukenho/kawaii-gherkin : ~0.1
- phing/phing : ~2.0
Expand Down Expand Up @@ -111,6 +112,7 @@ parameters:
git_commit_message: ~
grunt: ~
gulp: ~
infection: ~
jsonlint: ~
kahlan: ~
make: ~
Expand Down
1 change: 1 addition & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,7 @@
"doctrine/orm": "Lets GrumPHP validate your Doctrine mapping files.",
"etsy/phan": "Lets GrumPHP unleash a static analyzer on your code",
"friendsofphp/php-cs-fixer": "Lets GrumPHP automatically fix your codestyle.",
"infection/infection": "Lets GrumPHP evaluate the quality your unit tests",
"jakub-onderka/php-parallel-lint": "Lets GrumPHP quickly lint your entire code base.",
"maglnet/composer-require-checker": "Lets GrumPHP analyze composer dependencies.",
"malukenho/kawaii-gherkin": "Lets GrumPHP lint your Gherkin files.",
Expand Down
2 changes: 2 additions & 0 deletions doc/tasks.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ parameters:
git_commit_message: ~
grunt: ~
gulp: ~
infection: ~
jsonlint: ~
kahlan: ~
make: ~
Expand Down Expand Up @@ -73,6 +74,7 @@ Every task has it's own default configuration. It is possible to overwrite the p
- [Git commit message](tasks/git_commit_message.md)
- [Grunt](tasks/grunt.md)
- [Gulp](tasks/gulp.md)
- [Infection](tasks/infection.md)
- [JsonLint](tasks/jsonlint.md)
- [Kahlan](tasks/kahlan.md)
- [Make](tasks/make.md)
Expand Down
96 changes: 96 additions & 0 deletions doc/tasks/infection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
# Infection

Infection is a PHP mutation testing framework based on Abstract Syntax Tree.

***Composer***

```
composer require --dev infection/infection
```

***Config***

It lives under the `infection` namespace and has following configurable parameters:

```yaml
# grumphp.yml
parameters:
tasks:
infection:
configuration: ~
threads: ~
test_framework: ~
only_covered: false
configuration: ~
min_msi: ~
min_covered_msi: ~
mutators: []
triggered_by: [php]
```
**configuration**
*Default: null*
By defaut the `infection.json.dist` wil be used.
You can specify an alternate location for this file by changing this option.


**threads**

*Default: null*

If you want to run tests for mutated code in parallel, set this to something bigger than 1.
It will dramatically speed up the mutation process.
Please note that if your tests somehow depends on each other or use a database, this option can lead to failing tests which give many false-positives results.


**test_framework**

*Default: null*

This is the name of a test framework to use. Currently Infection supports `PhpUnit` and `PhpSpec`.


**only_covered**

*Default: false*

Run the mutation testing only for covered by tests files.


**configuration**

*Default: null*

The path or name to the infection configuration file.


**min_msi**

*Default: null*

This is a minimum threshold of Mutation Score Indicator (MSI) in percentage.


**min_covered_msi**

*Default: null*

This is a minimum threshold of Covered Code Mutation Score Indicator (MSI) in percentage.


**mutators**

*Default: []*

This is a list separated options to specify a particular set of mutators that needs to be executed.


**triggered_by**

*Default: [php]*

This option will specify which file extensions will trigger the infection task.
By default infection will be triggered by altering a php file.
You can overwrite this option to whatever file you want to use!
10 changes: 10 additions & 0 deletions resources/config/tasks.yml
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,16 @@ services:
- '@formatter.raw_process'
tags:
- {name: grumphp.task, config: gulp}

task.infection:
class: GrumPHP\Task\Infection
arguments:
- '@config'
- '@process_builder'
- '@formatter.raw_process'
tags:
- {name: grumphp.task, config: infection}

task.jsonlint:
class: GrumPHP\Task\JsonLint
arguments:
Expand Down
11 changes: 11 additions & 0 deletions spec/Collection/ProcessArgumentsCollectionSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -111,4 +111,15 @@ function it_should_be_able_to_add_comma_separated_files()

$this->getValues()->shouldBe(['file1.txt,file2.txt']);
}

function it_should_be_able_to_add_an_argument_with_comma_separated_files()
{
$files = new FilesCollection([
new SplFileInfo('file1.txt'),
new SplFileInfo('file2.txt')
]);
$this->addArgumentWithCommaSeparatedFiles('--argument=%s', $files);

$this->getValues()->shouldBe(['--argument=file1.txt,file2.txt']);
}
}
111 changes: 111 additions & 0 deletions spec/Task/InfectionSpec.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
<?php

namespace spec\GrumPHP\Task;

use GrumPHP\Collection\FilesCollection;
use GrumPHP\Collection\ProcessArgumentsCollection;
use GrumPHP\Configuration\GrumPHP;
use GrumPHP\Formatter\ProcessFormatterInterface;
use GrumPHP\Process\ProcessBuilder;
use GrumPHP\Runner\TaskResult;
use GrumPHP\Runner\TaskResultInterface;
use GrumPHP\Task\Context\ContextInterface;
use GrumPHP\Task\Context\GitPreCommitContext;
use GrumPHP\Task\Context\RunContext;
use GrumPHP\Task\Infection;
use PhpSpec\ObjectBehavior;
use Symfony\Component\Finder\SplFileInfo;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\Process\Process;

class InfectionSpec extends ObjectBehavior
{
function let(GrumPHP $grumPHP, ProcessBuilder $processBuilder, ProcessFormatterInterface $formatter)
{
$grumPHP->getTaskConfiguration('infection')->willReturn([]);
$this->beConstructedWith($grumPHP, $processBuilder, $formatter);
}

function it_is_initializable()
{
$this->shouldHaveType(Infection::class);
}

function it_should_have_a_name()
{
$this->getName()->shouldBe('infection');
}

function it_should_have_configurable_options()
{
$options = $this->getConfigurableOptions();
$options->shouldBeAnInstanceOf(OptionsResolver::class);
$options->getDefinedOptions()->shouldContain('threads');
$options->getDefinedOptions()->shouldContain('test_framework');
$options->getDefinedOptions()->shouldContain('only_covered');
$options->getDefinedOptions()->shouldContain('configuration');
$options->getDefinedOptions()->shouldContain('min_msi');
$options->getDefinedOptions()->shouldContain('min_covered_msi');
$options->getDefinedOptions()->shouldContain('mutators');
$options->getDefinedOptions()->shouldContain('triggered_by');
}

function it_should_run_in_git_pre_commit_context(GitPreCommitContext $context)
{
$this->canRunInContext($context)->shouldReturn(true);
}

function it_should_run_in_run_context(RunContext $context)
{
$this->canRunInContext($context)->shouldReturn(true);
}

function it_does_not_do_anything_if_there_are_no_files(ProcessBuilder $processBuilder, ContextInterface $context)
{
$processBuilder->buildProcess('infection')->shouldNotBeCalled();
$processBuilder->buildProcess()->shouldNotBeCalled();
$context->getFiles()->willReturn(new FilesCollection());

$result = $this->run($context);
$result->shouldBeAnInstanceOf(TaskResultInterface::class);
$result->getResultCode()->shouldBe(TaskResult::SKIPPED);
}

function it_runs_the_suite(ProcessBuilder $processBuilder, Process $process, ContextInterface $context)
{
$arguments = new ProcessArgumentsCollection();
$processBuilder->createArgumentsForCommand('infection')->willReturn($arguments);
$processBuilder->buildProcess($arguments)->willReturn($process);

$process->run()->shouldBeCalled();
$process->isSuccessful()->willReturn(true);
$process->getErrorOutput()->willReturn('');
$process->getOutput()->willReturn('');

$context->getFiles()->willReturn(new FilesCollection([
new SplFileInfo('test.php', '.', 'test.php')
]));

$result = $this->run($context);
$result->shouldBeAnInstanceOf(TaskResultInterface::class);
$result->isPassed()->shouldBe(true);
}

function it_throws_exception_if_the_process_fails(ProcessBuilder $processBuilder, Process $process, ContextInterface $context)
{
$arguments = new ProcessArgumentsCollection();
$processBuilder->createArgumentsForCommand('infection')->willReturn($arguments);
$processBuilder->buildProcess($arguments)->willReturn($process);

$process->run()->shouldBeCalled();
$process->isSuccessful()->willReturn(false);

$context->getFiles()->willReturn(new FilesCollection([
new SplFileInfo('src/Collection/TaskResultCollection.php', 'src/Collection', 'TaskResultCollection.php'),
]));

$result = $this->run($context);
$result->shouldBeAnInstanceOf(TaskResultInterface::class);
$result->isPassed()->shouldBe(false);
}
}
15 changes: 15 additions & 0 deletions src/Collection/ProcessArgumentsCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -135,4 +135,19 @@ public function addCommaSeparatedFiles(FilesCollection $files)

$this->add(implode(',', $paths));
}

/**
* @param string $argument
* @param FilesCollection|\SplFileInfo[] $files
*/
public function addArgumentWithCommaSeparatedFiles($argument, FilesCollection $files)
{
$paths = [];

foreach ($files as $file) {
$paths[] = $file->getPathname();
}

$this->add(sprintf($argument, implode(',', $paths)));
}
}
97 changes: 97 additions & 0 deletions src/Task/Infection.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
<?php

namespace GrumPHP\Task;

use GrumPHP\Runner\TaskResult;
use GrumPHP\Task\Context\ContextInterface;
use GrumPHP\Task\Context\GitPreCommitContext;
use GrumPHP\Task\Context\RunContext;
use Symfony\Component\OptionsResolver\OptionsResolver;

/**
* Infection task
*/
class Infection extends AbstractExternalTask
{
/**
* @return string
*/
public function getName()
{
return 'infection';
}

/**
* @return OptionsResolver
*/
public function getConfigurableOptions()
{
$resolver = new OptionsResolver();

$resolver->setDefaults([
'threads' => null,
'test_framework' => null,
'only_covered' => false,
'configuration' => null,
'min_msi' => null,
'min_covered_msi' => null,
'mutators' => [],
'triggered_by' => ['php'],
]);

$resolver->addAllowedTypes('threads', ['null', 'int']);
$resolver->addAllowedTypes('test_framework', ['null', 'string']);
$resolver->addAllowedTypes('only_covered', ['bool']);
$resolver->addAllowedTypes('configuration', ['null', 'string']);
$resolver->addAllowedTypes('min_msi', ['null', 'integer']);
$resolver->addAllowedTypes('min_covered_msi', ['null', 'integer']);
$resolver->addAllowedTypes('mutators', ['array']);
$resolver->addAllowedTypes('triggered_by', ['array']);

return $resolver;
}

/**
* {@inheritdoc}
*/
public function canRunInContext(ContextInterface $context)
{
return ($context instanceof GitPreCommitContext || $context instanceof RunContext);
}

/**
* {@inheritdoc}
*/
public function run(ContextInterface $context)
{
$config = $this->getConfiguration();
$files = $context->getFiles()->extensions($config['triggered_by']);

if (0 === count($files)) {
return TaskResult::createSkipped($this, $context);
}

$arguments = $this->processBuilder->createArgumentsForCommand('infection');
$arguments->add('--no-interaction');
$arguments->addOptionalArgument('--threads=%s', $config['threads']);
$arguments->addOptionalArgument('--test-framework=%s', $config['test_framework']);
$arguments->addOptionalArgument('--only-covered', $config['only_covered']);
$arguments->addOptionalArgument('--configuration=%s', $config['configuration']);
$arguments->addOptionalArgument('--min-msi=%s', $config['min_msi']);
$arguments->addOptionalArgument('--min-covered-msi=%s', $config['min_covered_msi']);
$arguments->addOptionalCommaSeparatedArgument('--mutators=%s', $config['mutators']);

if ($context instanceof GitPreCommitContext) {
$arguments->addArgumentWithCommaSeparatedFiles('--filter=%s', $files);
}

$process = $this->processBuilder->buildProcess($arguments);
$process->run();

if (!$process->isSuccessful()) {
return TaskResult::createFailed($this, $context, $this->formatter->format($process));
}

return TaskResult::createPassed($this, $context);
}
}

0 comments on commit 88b6b83

Please sign in to comment.