From b9ac89e7af57c89ebbd965c8ac9e3d6e98e256fc Mon Sep 17 00:00:00 2001 From: Lander Vanderstraeten Date: Mon, 11 Sep 2017 17:45:50 +0200 Subject: [PATCH 1/7] Add infection task --- README.md | 1 + doc/tasks.md | 2 + doc/tasks/infection.md | 79 +++++++++++++ resources/config/tasks.yml | 10 ++ .../ProcessArgumentsCollectionSpec.php | 11 ++ spec/Task/InfectionSpec.php | 111 ++++++++++++++++++ src/Collection/ProcessArgumentsCollection.php | 15 +++ src/Task/Infection.php | 97 +++++++++++++++ 8 files changed, 326 insertions(+) create mode 100644 doc/tasks/infection.md create mode 100644 spec/Task/InfectionSpec.php create mode 100644 src/Task/Infection.php diff --git a/README.md b/README.md index 5be961f69..d3e425362 100644 --- a/README.md +++ b/README.md @@ -111,6 +111,7 @@ parameters: git_commit_message: ~ grunt: ~ gulp: ~ + infection: ~ jsonlint: ~ kahlan: ~ make: ~ diff --git a/doc/tasks.md b/doc/tasks.md index 13bcb769e..e7e8c5a60 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -25,6 +25,7 @@ parameters: git_commit_message: ~ grunt: ~ gulp: ~ + infection: ~ jsonlint: ~ kahlan: ~ make: ~ @@ -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) diff --git a/doc/tasks/infection.md b/doc/tasks/infection.md new file mode 100644 index 000000000..09401a6ef --- /dev/null +++ b/doc/tasks/infection.md @@ -0,0 +1,79 @@ +# Infection + +Infection is a PHP mutation testing framework based on Abstract Syntax Tree. + +It lives under the `infection` namespace and has following configurable parameters: + +```yaml +# grumphp.yml +parameters: + tasks: + infection: + threads: ~ + test_framework: ~ + only_covered: ~ + configuration: ~ + min_msi: ~ + min_covered_msi: ~ + mutators: ~ + triggered_by: [php] +``` + +**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: null* + +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: null* + +This is a comma separated option 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! diff --git a/resources/config/tasks.yml b/resources/config/tasks.yml index 2de204d18..9d8d87d0c 100644 --- a/resources/config/tasks.yml +++ b/resources/config/tasks.yml @@ -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: diff --git a/spec/Collection/ProcessArgumentsCollectionSpec.php b/spec/Collection/ProcessArgumentsCollectionSpec.php index 708372f42..36033dea4 100644 --- a/spec/Collection/ProcessArgumentsCollectionSpec.php +++ b/spec/Collection/ProcessArgumentsCollectionSpec.php @@ -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']); + } } diff --git a/spec/Task/InfectionSpec.php b/spec/Task/InfectionSpec.php new file mode 100644 index 000000000..bd20e2844 --- /dev/null +++ b/spec/Task/InfectionSpec.php @@ -0,0 +1,111 @@ +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); + } +} diff --git a/src/Collection/ProcessArgumentsCollection.php b/src/Collection/ProcessArgumentsCollection.php index 6519866f0..ffc664cb3 100644 --- a/src/Collection/ProcessArgumentsCollection.php +++ b/src/Collection/ProcessArgumentsCollection.php @@ -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))); + } } diff --git a/src/Task/Infection.php b/src/Task/Infection.php new file mode 100644 index 000000000..0aeeeb5d3 --- /dev/null +++ b/src/Task/Infection.php @@ -0,0 +1,97 @@ +setDefaults([ + 'threads' => null, + 'test_framework' => null, + 'only_covered' => null, + 'configuration' => null, + 'min_msi' => null, + 'min_covered_msi' => null, + 'mutators' => null, + 'triggered_by' => ['php'], + ]); + + $resolver->addAllowedTypes('threads', ['null', 'int']); + $resolver->addAllowedTypes('test_framework', ['null', 'string']); + $resolver->addAllowedTypes('only_covered', ['null', 'bool']); + $resolver->addAllowedTypes('configuration', ['null', 'string']); + $resolver->addAllowedTypes('min_msi', ['null', 'integer']); + $resolver->addAllowedTypes('min_covered_msi', ['null', 'integer']); + $resolver->addAllowedTypes('mutators', ['null', 'string']); + $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->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->addOptionalArgument('--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); + } +} From 3d469faf34c9416ed53c4775d521bb70bde78c1d Mon Sep 17 00:00:00 2001 From: Lander Vanderstraeten Date: Fri, 29 Sep 2017 09:29:52 +0200 Subject: [PATCH 2/7] Add infection to suggestions --- README.md | 1 + composer.json | 1 + 2 files changed, 2 insertions(+) diff --git a/README.md b/README.md index d3e425362..00cfe8de6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/composer.json b/composer.json index bb0cf6a16..a0b4a8766 100644 --- a/composer.json +++ b/composer.json @@ -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.", From cf5d83b1f1646a68bc6faa25f7859fd898f01c53 Mon Sep 17 00:00:00 2001 From: Lander Vanderstraeten Date: Fri, 29 Sep 2017 09:46:28 +0200 Subject: [PATCH 3/7] Set the default value of only covered to false --- doc/tasks/infection.md | 4 ++-- src/Task/Infection.php | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/doc/tasks/infection.md b/doc/tasks/infection.md index 09401a6ef..2d04be96a 100644 --- a/doc/tasks/infection.md +++ b/doc/tasks/infection.md @@ -11,7 +11,7 @@ parameters: infection: threads: ~ test_framework: ~ - only_covered: ~ + only_covered: false configuration: ~ min_msi: ~ min_covered_msi: ~ @@ -37,7 +37,7 @@ This is the name of a test framework to use. Currently Infection supports `PhpUn **only_covered** -*Default: null* +*Default: false* Run the mutation testing only for covered by tests files. diff --git a/src/Task/Infection.php b/src/Task/Infection.php index 0aeeeb5d3..f5f441b00 100644 --- a/src/Task/Infection.php +++ b/src/Task/Infection.php @@ -31,7 +31,7 @@ public function getConfigurableOptions() $resolver->setDefaults([ 'threads' => null, 'test_framework' => null, - 'only_covered' => null, + 'only_covered' => false, 'configuration' => null, 'min_msi' => null, 'min_covered_msi' => null, @@ -41,7 +41,7 @@ public function getConfigurableOptions() $resolver->addAllowedTypes('threads', ['null', 'int']); $resolver->addAllowedTypes('test_framework', ['null', 'string']); - $resolver->addAllowedTypes('only_covered', ['null', 'bool']); + $resolver->addAllowedTypes('only_covered', ['bool']); $resolver->addAllowedTypes('configuration', ['null', 'string']); $resolver->addAllowedTypes('min_msi', ['null', 'integer']); $resolver->addAllowedTypes('min_covered_msi', ['null', 'integer']); From 720ce38b963a37f980d1da6dc80a6596f9bba016 Mon Sep 17 00:00:00 2001 From: Lander Vanderstraeten Date: Fri, 29 Sep 2017 09:55:26 +0200 Subject: [PATCH 4/7] Use an array instead of a comma separated list --- doc/tasks/infection.md | 6 +++--- src/Task/Infection.php | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/doc/tasks/infection.md b/doc/tasks/infection.md index 2d04be96a..229150dc2 100644 --- a/doc/tasks/infection.md +++ b/doc/tasks/infection.md @@ -15,7 +15,7 @@ parameters: configuration: ~ min_msi: ~ min_covered_msi: ~ - mutators: ~ + mutators: [] triggered_by: [php] ``` @@ -65,9 +65,9 @@ This is a minimum threshold of Covered Code Mutation Score Indicator (MSI) in pe **mutators** -*Default: null* +*Default: []* -This is a comma separated option to specify a particular set of mutators that needs to be executed. +This is a list separated options to specify a particular set of mutators that needs to be executed. **triggered_by** diff --git a/src/Task/Infection.php b/src/Task/Infection.php index f5f441b00..7f194ffe8 100644 --- a/src/Task/Infection.php +++ b/src/Task/Infection.php @@ -35,7 +35,7 @@ public function getConfigurableOptions() 'configuration' => null, 'min_msi' => null, 'min_covered_msi' => null, - 'mutators' => null, + 'mutators' => [], 'triggered_by' => ['php'], ]); @@ -45,7 +45,7 @@ public function getConfigurableOptions() $resolver->addAllowedTypes('configuration', ['null', 'string']); $resolver->addAllowedTypes('min_msi', ['null', 'integer']); $resolver->addAllowedTypes('min_covered_msi', ['null', 'integer']); - $resolver->addAllowedTypes('mutators', ['null', 'string']); + $resolver->addAllowedTypes('mutators', ['array']); $resolver->addAllowedTypes('triggered_by', ['array']); return $resolver; @@ -79,7 +79,7 @@ public function run(ContextInterface $context) $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->addOptionalArgument('--mutators=%s', $config['mutators']); + $arguments->addOptionalCommaSeparatedArgument('--mutators=%s', $config['mutators']); if ($context instanceof GitPreCommitContext) { $arguments->addArgumentWithCommaSeparatedFiles('--filter=%s', $files); From 0db75936e8e2612f895e3db47d5223f904ff50d3 Mon Sep 17 00:00:00 2001 From: Lander Vanderstraeten Date: Fri, 29 Sep 2017 10:34:33 +0200 Subject: [PATCH 5/7] Add configuration parameter --- doc/tasks/infection.md | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/doc/tasks/infection.md b/doc/tasks/infection.md index 229150dc2..c02032615 100644 --- a/doc/tasks/infection.md +++ b/doc/tasks/infection.md @@ -9,6 +9,7 @@ It lives under the `infection` namespace and has following configurable paramete parameters: tasks: infection: + configuration: ~ threads: ~ test_framework: ~ only_covered: false @@ -19,6 +20,14 @@ parameters: 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* From 64992ce99e8f83d079002d8b44e22c12589442a0 Mon Sep 17 00:00:00 2001 From: Lander Vanderstraeten Date: Mon, 13 Nov 2017 10:22:20 +0100 Subject: [PATCH 6/7] Disable interaction --- src/Task/Infection.php | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Task/Infection.php b/src/Task/Infection.php index 7f194ffe8..9c45cc88d 100644 --- a/src/Task/Infection.php +++ b/src/Task/Infection.php @@ -72,7 +72,7 @@ public function run(ContextInterface $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']); From 2de32206cdfb648b790b41ad024da530980539cf Mon Sep 17 00:00:00 2001 From: Lander Vanderstraeten Date: Sat, 18 Nov 2017 00:27:46 +0200 Subject: [PATCH 7/7] Add composer require to the documentation --- doc/tasks/infection.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/doc/tasks/infection.md b/doc/tasks/infection.md index c02032615..697322e1e 100644 --- a/doc/tasks/infection.md +++ b/doc/tasks/infection.md @@ -2,6 +2,14 @@ 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