diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 06d1b76a..71ceae7d 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -5,8 +5,8 @@ feel free to do this, but remember to follow this few simple rules: ## Branching strategy -- __Always__ base your changes on the `master` branch (all new development happens here), -- When you create Pull Request, always select `master` branch as target, otherwise it +- __Always__ base your changes on the latest version `v.x` branch (all new development happens here), +- When you create Pull Request, always select `v.x` branch as target, otherwise it will be closed (this is selected by default). ## Coverage diff --git a/doc/tasks.md b/doc/tasks.md index e2ebba68..64f3a4e6 100644 --- a/doc/tasks.md +++ b/doc/tasks.md @@ -61,6 +61,7 @@ grumphp: securitychecker_symfony: ~ shell: ~ stylelint: ~ + symfony_console: ~ tester: ~ twigcs: ~ twigcsfixer: ~ @@ -128,6 +129,7 @@ Every task has its own default configuration. It is possible to overwrite the pa - [Symfony](tasks/securitychecker/symfony.md) - [Shell](tasks/shell.md) - [Stylelint](tasks/stylelint.md) +- [Symfony Console](tasks/symfony_console.md) - [Tester](tasks/tester.md) - [TwigCs](tasks/twigcs.md) - [Twig-CS-Fixer](tasks/twigcsfixer.md) diff --git a/doc/tasks/symfony_console.md b/doc/tasks/symfony_console.md new file mode 100644 index 00000000..525fdc3f --- /dev/null +++ b/doc/tasks/symfony_console.md @@ -0,0 +1,92 @@ +# Symfony Console + +Run a symfony console command. + +## Requirements + +This task requires a Symfony application with console component. + +## Config + +The task lives under the `symfony_console` namespace and has following configurable parameters: + +```yaml +# grumphp.yml +grumphp: + tasks: + symfony_console: + bin: "./bin/console" + command: [ "lint:container", "-vvv" ] + ignore_patterns: [], + whitelist_patterns: [], + triggered_by: ['php', 'yml', 'xml'], + run_always: false +``` + +### Parameters + +**bin** + +*Default: `./bin/console`* + +Specify the path to the Symfony Console script. + +**command** + +*Default: `[]`* + +Specify the symfony command with defined options and arguments. +Verify the installed console component version for available commands `./bin/console list` + +**ignore_patterns** + +*Default: []* + +This is a list of file patterns that will be ignored by the Symfony console task. +Leave this option blank to run the task for all files defined in the whitelist_patterns and or triggered_by extensions. + +**whitelist_patterns** + +*Default: []* + +This is a list of regex patterns that will filter files to validate. With this option you can skip files like tests. +This option is used in relation with the parameter `triggered_by`. +For example: whitelist files in `src/FolderA/` and `src/FolderB/` you can use +```yaml +whitelist_patterns: + - /^src\/FolderA\/(.*)/ + - /^src\/FolderB\/(.*)/ +``` + +**triggered_by** + +*Default: [php, yml, xml]* + +This option will specify which file extensions will trigger the Symfony console task. +By default, altering a `php`, `yml`, `xml` file will trigger the task. +You can overwrite this option to whatever filetype you want to validate! + +**run_always** + +*Default: false* + +If this is set to `true` the Symfony console task will be executed on every commit, regardless of any modified files. + +## Multiple Console command tasks + +[Run the same task twice with different configuration](../tasks.md#run-the-same-task-twice-with-different-configuration) + +Specific running multiple symfony console commands: + +```yaml +# grumphp.yml +grumphp: + lint-container: + command: [ "lint:container", "-vvv"] + metadata: + task: symfony_console + lint-yaml: + command: [ "lint:yaml", "path/to/yaml"] + metadata: + task: symfony_console +``` diff --git a/resources/config/tasks.yml b/resources/config/tasks.yml index 318dbe8d..d494e23c 100644 --- a/resources/config/tasks.yml +++ b/resources/config/tasks.yml @@ -386,6 +386,13 @@ services: tags: - {name: grumphp.task, task: stylelint} + GrumPHP\Task\SymfonyConsole: + arguments: + - '@process_builder' + - '@formatter.raw_process' + tags: + - {name: grumphp.task, task: symfony_console} + GrumPHP\Task\Tester: class: arguments: diff --git a/src/Task/SymfonyConsole.php b/src/Task/SymfonyConsole.php new file mode 100644 index 00000000..34726945 --- /dev/null +++ b/src/Task/SymfonyConsole.php @@ -0,0 +1,80 @@ + */ +class SymfonyConsole extends AbstractExternalTask +{ + public static function getConfigurableOptions(): ConfigOptionsResolver + { + return ConfigOptionsResolver::fromOptionsResolver( + (new OptionsResolver()) + ->setDefaults([ + 'bin' => './bin/console', + 'command' => [], + 'ignore_patterns' => [], + 'whitelist_patterns' => [], + 'triggered_by' => ['php', 'yml', 'xml'], + 'run_always' => false, + ]) + ->addAllowedTypes('bin', ['string']) + ->addAllowedTypes('command', ['string[]']) + ->addAllowedTypes('ignore_patterns', ['array']) + ->addAllowedTypes('whitelist_patterns', ['array']) + ->addAllowedTypes('triggered_by', ['array']) + ->addAllowedTypes('run_always', ['bool']) + ->setAllowedValues( + 'bin', + static fn(string $bin): bool => '' !== $bin + ) + ->setAllowedValues( + 'command', + static fn(array $command): bool => '' !== \implode('', $command) + ) + ->setAllowedValues('bin', static fn(string $bin): bool => '' !== $bin) + ->setRequired('command') + ); + } + + public function canRunInContext(ContextInterface $context): bool + { + return ($context instanceof GitPreCommitContext || $context instanceof RunContext); + } + + public function run(ContextInterface $context): TaskResultInterface + { + $config = $this->getConfig()->getOptions(); + + $files = $context->getFiles() + ->extensions($config['triggered_by']) + ->paths($config['whitelist_patterns'] ?? []) + ->notPaths($config['ignore_patterns'] ?? []); + if (!$config['run_always'] && 0 === \count($files)) { + return TaskResult::createSkipped($this, $context); + } + + $arguments = $this->processBuilder->createArgumentsForCommand('php'); + $arguments->add($config['bin']); + $arguments->addArgumentArray('%s', $config['command']); + + $process = $this->processBuilder->buildProcess($arguments); + $process->run(); + + if (!$process->isSuccessful()) { + return TaskResult::createFailed($this, $context, $this->formatter->format($process)); + } + + return TaskResult::createPassed($this, $context); + } +} diff --git a/src/Test/Task/AbstractTaskTestCase.php b/src/Test/Task/AbstractTaskTestCase.php index 75d682f3..83124c1c 100644 --- a/src/Test/Task/AbstractTaskTestCase.php +++ b/src/Test/Task/AbstractTaskTestCase.php @@ -6,7 +6,6 @@ use GrumPHP\Collection\FilesCollection; use GrumPHP\Runner\TaskResult; -use GrumPHP\Runner\TaskResultInterface; use GrumPHP\Task\Config\EmptyTaskConfig; use GrumPHP\Task\Config\Metadata; use GrumPHP\Task\Config\TaskConfig; @@ -47,7 +46,7 @@ protected function setUp(): void public function it_contains_configurable_options(array $input, ?array $output): void { if (!$output) { - self::expectException(ExceptionInterface::class); + $this->expectException(ExceptionInterface::class); } $resolver = $this->task::getConfigurableOptions(); diff --git a/test/Unit/Task/SymfonyConsoleTest.php b/test/Unit/Task/SymfonyConsoleTest.php new file mode 100644 index 00000000..2306db5b --- /dev/null +++ b/test/Unit/Task/SymfonyConsoleTest.php @@ -0,0 +1,192 @@ +processBuilder->reveal(), + $this->formatter->reveal() + ); + } + + public function provideConfigurableOptions(): iterable + { + yield 'default' => [ + [ + 'command' => ['task:run'], + ], + [ + 'bin' => './bin/console', + 'command' => ['task:run'], + 'ignore_patterns' => [], + 'whitelist_patterns' => [], + 'triggered_by' => ['php', 'yml', 'xml'], + 'run_always' => false, + ] + ]; + + yield 'with-array-command' => [ + [ + 'command' => ['task:run', '--env', 'dev', '-vvv'], + ], + [ + 'bin' => './bin/console', + 'command' => [ + 'task:run', + '--env', + 'dev', + '-vvv' + ], + 'ignore_patterns' => [], + 'whitelist_patterns' => [], + 'triggered_by' => ['php', 'yml', 'xml'], + 'run_always' => false, + ] + ]; + } + + public function provideRunContexts(): iterable + { + yield 'run-context' => [ + true, + $this->mockContext(RunContext::class) + ]; + + yield 'pre-commit-context' => [ + true, + $this->mockContext(GitPreCommitContext::class) + ]; + + yield 'other' => [ + false, + $this->mockContext() + ]; + } + + public function provideFailsOnStuff(): iterable + { + yield 'exitCode1' => [ + [ + 'command' => ['--version'] + ], + $this->mockContext(RunContext::class, ['hello.php', 'hello2.php']), + function() { + $process = $this->mockProcess(1); + $this->mockProcessBuilder('php', $process); + $this->formatter->format($process)->willReturn('nope'); + }, + 'nope' + ]; + } + + public function providePassesOnStuff(): iterable + { + yield 'exitCode0' => [ + [ + 'command' => ['--version'] + ], + $this->mockContext(RunContext::class, ['hello.php', 'hello2.php']), + function() { + $this->mockProcessBuilder('php', $this->mockProcess()); + } + ]; + + yield 'exitCode0WhenRunAlways' => [ + [ + 'command' => ['--version'], + 'run_always' => true, + ], + $this->mockContext(RunContext::class, ['non-related.log']), + function() { + $this->mockProcessBuilder('php', $this->mockProcess()); + } + ]; + } + + public function provideSkipsOnStuff(): iterable + { + yield 'no-files' => [ + [ + 'command' => ['task:run'] + ], + $this->mockContext(RunContext::class), + function() { + } + ]; + + yield 'no-files-after-ignore-patterns' => [ + [ + 'command' => ['task:run'], + 'ignore_patterns' => ['test/'], + ], + $this->mockContext(RunContext::class, ['test/file.php']), + function() { + } + ]; + + yield 'no-files-after-whitelist-patterns' => [ + [ + 'command' => ['task:run'], + 'whitelist_patterns' => ['src/'], + ], + $this->mockContext(RunContext::class, ['config/file.php']), + function() { + } + ]; + + yield 'no-files-after-triggered-by' => [ + [ + 'command' => ['task:run'], + ], + $this->mockContext(RunContext::class, ['non-trigger-extension.log']), + function() { + } + ]; + } + + public function provideExternalTaskRuns(): iterable + { + yield 'single-command' => [ + [ + 'command' => ['lint:container'] + ], + $this->mockContext(RunContext::class, ['hello.php', 'hello2.php']), + 'php', + [ + './bin/console', + 'lint:container', + ] + ]; + + yield 'array-command' => [ + [ + 'command' => [ + 'task:run', + '--env', + 'dev', + '-vvv' + ] + ], + $this->mockContext(RunContext::class, ['hello.php', 'hello2.php']), + 'php', + [ + './bin/console', + 'task:run', + '--env', + 'dev', + '-vvv' + ] + ]; + } +}