Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

580: Adds support for --tasks option in run command. #582

Closed
wants to merge 2 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions doc/commands.md
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,12 @@ php ./vendor/bin/grumphp run --testsuite=mytestsuite

This command can also be used for continious integration.
More information about the testsuites can be found in the [testsuites documentation](testsuites.md).

If you want to run only a subset of the configured tasks, you can run the command with the `--tasks` option:

```sh
php ./vendor/bin/grumphp run --tasks=task1,task2
```

The `--tasks` value has to be a comma-separated string of task names that match the keys in the `tasks` section
of the `grumphp.yml` file. See [#580](https://github.com/phpro/grumphp/issues/580) for a more exhaustive explanation.
27 changes: 27 additions & 0 deletions spec/Collection/TasksCollectionSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,33 @@ function it_can_filter_by_empty_testsuite(TaskInterface $task1, TaskInterface $t
$tasks[1]->shouldBe($task2);
}

function it_can_filter_by_task_names(TaskInterface $task1, TaskInterface $task2)
{
$task1->getName()->willReturn('task1');
$task2->getName()->willReturn('task2');
$tasks = ['task1'];

$result = $this->filterByTaskName($tasks);
$result->shouldBeAnInstanceOf(TasksCollection::class);
$result->count()->shouldBe(1);
$tasks = $result->toArray();
$tasks[0]->shouldBe($task1);
}

function it_can_filter_by_empty_task_names(TaskInterface $task1, TaskInterface $task2)
{
$task1->getName()->willReturn('task1');
$task2->getName()->willReturn('task2');
$tasks = [];

$result = $this->filterByTaskName($tasks);
$result->shouldBeAnInstanceOf(TasksCollection::class);
$result->count()->shouldBe(2);
$tasks = $result->toArray();
$tasks[0]->shouldBe($task1);
$tasks[1]->shouldBe($task2);
}

function it_should_sort_on_priority(TaskInterface $task1, TaskInterface $task2, TaskInterface $task3, GrumPHP $grumPHP)
{
$this->beConstructedWith([$task1, $task2, $task3]);
Expand Down
14 changes: 14 additions & 0 deletions spec/Runner/TaskRunnerContextSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,20 @@ function it_has_no_test_suite(ContextInterface $context)
$this->getTestSuite()->shouldBe(null);
}

function it_has_no_tasks()
{
$this->hasTasks()->shouldBe(false);
$this->getTasks()->shouldBe([]);
}

function it_has_tasks(ContextInterface $context)
{
$tasks = ["task_1"];
$this->beConstructedWith($context, null, $tasks);
$this->hasTasks()->shouldBe(true);
$this->getTasks()->shouldBe($tasks);
}

function it_knows_to_skip_the_success_message()
{
$this->skipSuccessOutput()->shouldBe(false);
Expand Down
1 change: 1 addition & 0 deletions spec/Runner/TaskRunnerSpec.php
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ public function let(

$runnerContext->getTaskContext()->willReturn($taskContext);
$runnerContext->getTestSuite()->willReturn(null);
$runnerContext->getTasks()->willReturn([]);

$task1->getName()->willReturn('task1');
$task1->canRunInContext($taskContext)->willReturn(true);
Expand Down
15 changes: 15 additions & 0 deletions src/Collection/TasksCollection.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,21 @@ public function filterByTestSuite(TestSuiteInterface $testSuite = null)
});
}

/**
* @param string[] $tasks
*
* @return TasksCollection
*/
public function filterByTaskName($tasks)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you rename this one to filterByTaskNames?

{
return $this->filter(function (TaskInterface $task) use ($tasks) {
if (empty($tasks)) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No need to iterate through the list when the tasks are empty. You can put the empty check above the filter function and return a new collection through return new TasksCollection($this->toArray());
See \GrumPHP\Collection\TasksCollection::filterByTestSuite()

return true;
}
return in_array($task->getName(), $tasks);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can set the third argument to true here.

});
}

/**
* This method sorts the tasks by highest priority first.
*
Expand Down
33 changes: 32 additions & 1 deletion src/Console/Command/RunCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,13 @@ protected function configure()
'Specify which testsuite you want to run.',
null
);
$this->addOption(
'tasks',
null,
InputOption::VALUE_REQUIRED,
'Specify which tasks you want to run (comma separated). Example --tasks=task1,task2',
null
);
}

/**
Expand All @@ -66,9 +73,12 @@ public function execute(InputInterface $input, OutputInterface $output)
$files = $this->getRegisteredFiles();
$testSuites = $this->grumPHP->getTestSuites();

$tasks = $this->parseCommaSeparatedOption($input->getOption("tasks"));
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
$tasks = $this->parseCommaSeparatedOption($input->getOption("tasks"));
$tasks = array_map('trim', explode(',', $input->getOption('tasks', '')));

The de-duplicating logic is a bit confusing and is not required since the filter method on the taskcollection is smart enough. I would prefer to drop it for the single line suggested above. (optionally you could add array_filter to drop the empty items, but that is also covered by the filter method)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This will retain empty input values, i.e. if --tasks is empty or omitted, $tasks will be an array containing one empty element. So we would either put it in an if OR add and array_filter --- both makes the result less readable, imho.

I'm fine with removing deduplication, though.

Thoughts?

fyi - failing tests after change:

/codebase/grumphp/vendor/phpunit/phpunit/phpunit --configuration /codebase/grumphp/phpunit.xml.dist GrumPHPTest\\Console\\Command\\RunCommandTest /codebase/grumphp/test/Console/Command/RunCommandTest.php

[...]
4) GrumPHPTest\Console\Command\RunCommandTest::parses_comma_separated_options with data set "null" (null, array())
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
+    0 => ''
 )

/codebase/grumphp/test/Console/Command/RunCommandTest.php:36

5) GrumPHPTest\Console\Command\RunCommandTest::parses_comma_separated_options with data set "empty" ('', array())
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
+    0 => ''
 )

/codebase/grumphp/test/Console/Command/RunCommandTest.php:36

6) GrumPHPTest\Console\Command\RunCommandTest::parses_comma_separated_options with data set "empty after trim" (' ', array())
Failed asserting that two arrays are equal.
--- Expected
+++ Actual
@@ @@
 Array (
+    0 => ''
 )


$context = new TaskRunnerContext(
new RunContext($files),
(bool) $input->getOption('testsuite') ? $testSuites->getRequired($input->getOption('testsuite')) : null
(bool) $input->getOption('testsuite') ? $testSuites->getRequired($input->getOption('testsuite')) : null,
$tasks
);

return $this->taskRunner()->run($output, $context);
Expand Down Expand Up @@ -97,4 +107,25 @@ protected function paths()
{
return $this->getHelper(PathsHelper::HELPER_NAME);
}

/**
* Split $value on ",", trim the individual parts and
* de-deduplicate the remaining values
*
* @param string $value
* @return string[]
*/
protected function parseCommaSeparatedOption($value)
{
$stringValues = explode(",", $value);
$parsedValues = [];
foreach ($stringValues as $k => $v) {
$v = trim($v);
if (empty($v)) {
continue;
}
$parsedValues[$v] = $v;
}
return $parsedValues;
}
}
1 change: 1 addition & 0 deletions src/Runner/TaskRunner.php
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,7 @@ public function run(TaskRunnerContext $runnerContext)
$tasks = $this->tasks
->filterByContext($runnerContext->getTaskContext())
->filterByTestSuite($runnerContext->getTestSuite())
->filterByTaskName($runnerContext->getTasks())
->sortByPriority($this->grumPHP);
$taskResults = new TaskResultCollection();

Expand Down
30 changes: 28 additions & 2 deletions src/Runner/TaskRunnerContext.php
Original file line number Diff line number Diff line change
Expand Up @@ -22,16 +22,26 @@ class TaskRunnerContext
*/
private $testSuite = null;

/**
* @var string[]
*/
private $tasks = null;

/**
* TaskRunnerContext constructor.
*
* @param ContextInterface $taskContext
* @param ContextInterface $taskContext
* @param TestSuiteInterface $testSuite
* @param string[]|null $tasks
*/
public function __construct(ContextInterface $taskContext, TestSuiteInterface $testSuite = null)
public function __construct(ContextInterface $taskContext, TestSuiteInterface $testSuite = null, $tasks = null)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you make $tasks a required array?
I would prefer to always pass in a list of tasks.
(This is a BC break, but I don't think it will cause too much damage since it is an internal DTO.)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Like this?

    /**
     * TaskRunnerContext constructor.
     *
     * @param ContextInterface $taskContext
     * @param string[] $tasks
     * @param TestSuiteInterface $testSuite
     */
    public function __construct(ContextInterface $taskContext, array $tasks, TestSuiteInterface $testSuite = null)

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes

{
$this->taskContext = $taskContext;
$this->testSuite = $testSuite;
if ($tasks === null) {
$tasks = [];
}
$this->tasks = $tasks;
}

/**
Expand Down Expand Up @@ -81,4 +91,20 @@ public function setTestSuite(TestSuiteInterface $testSuite)
{
$this->testSuite = $testSuite;
}

/**
* @return string[]
*/
public function getTasks()
{
return $this->tasks;
}

/**
* @return bool
*/
public function hasTasks()
{
return !empty($this->tasks);
}
}
78 changes: 78 additions & 0 deletions test/Console/Command/RunCommandTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

namespace GrumPHPTest\Console\Command;

use GrumPHP\Configuration\GrumPHP;
use GrumPHP\Console\Command\RunCommand;
use GrumPHP\Locator\RegisteredFiles;
use PHPUnit\Framework\TestCase;

class RunCommandTest extends TestCase
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test can be dropped if you change the input option logic as suggested above.

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'dont feel quite comfortable having "nothing" that actually checks the behavior - which kinda was the initial thought on adding a test in the first place.

E.g. consider the inlined

 $tasks = array_map('trim', explode(',', $input->getOption('tasks', '')));

If somebody were to switch "," by ";" there would be "nothing" to catch that bug. Mutation testing tools like https://github.com/infection/infection usually "make those changes" to verify if everything is tested "correctly".


Are you open to introducing some sort of end2end/integration test that "acutally" calls the run command and verifies the output (see https://symfony.com/doc/4.1/console.html#testing-commands )?

It's something that we do quite frequently because it's a very good high level indicator for "some things don't work as they are supposed to" (though it comes with an increased maintenance overhead due to duplication).

Something along the lines of

    public function test_command()
    {
        $application = new Application();

        /**
         * @var RunCommand $command
         */
        $command = $application->find('run');

        $commandTester = new CommandTester($command);
        $commandTester->execute(array(
            'command'  => $command->getName(),
            '--tasks' => 'foo',
        ));

        $output = $commandTester->getDisplay();
        $this->assertContains('...', $output);
    }

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @paslandau,

In a normal situation, those tests would work fine.
However, the behaviour of the grumphp commands are different based on which configuration file you supply. This means we would have a lot of e2e tests to cover all possible situations.
I was looking in to this with @Landerstraeten a while ago, but didn't find a good solution yet.

Feel free to suggest something in this PR.

Copy link
Author

@paslandau paslandau Jan 4, 2019

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll give it some thought but I'm not to deep into symfony (more of a laravel guy) plus since those sort of tests are usually tightly coupled to the application I guess I'd need more time digging some grumphp internals.

So I would keep it separate from this PR + keep the current test for now.

{
/**
* @test
* @param null $valueString
* @param null $expected
* @dataProvider parses_comma_separated_options_dataProvider
* @throws \ReflectionException
*/
function parses_comma_separated_options($valueString = null, $expected = null)
{
/**
* @var GrumPHP $grumPhp
*/
$grumPhp = $this->createMock(GrumPHP::class);
/**
* @var RegisteredFiles $registeredFiles
*/
$registeredFiles = $this->createMock(RegisteredFiles::class);

$command = new RunCommand($grumPhp, $registeredFiles);
$method = new \ReflectionMethod($command, "parseCommaSeparatedOption");
$method->setAccessible(true);

$actual = $method->invoke($command, $valueString);

$this->assertEquals($expected, $actual);
}


public function parses_comma_separated_options_dataProvider()
{
return [
"default" => [
"valueString" => "foo,bar",
"expected" => [
"foo" => "foo",
"bar" => "bar"
],
],
"trims values" => [
"valueString" => "foo , bar",
"expected" => [
"foo" => "foo",
"bar" => "bar"
],
],
"deduplicates values" => [
"valueString" => "foo,bar,bar",
"expected" => [
"foo" => "foo",
"bar" => "bar"
],
],
"null" => [
"valueString" => null,
"expected" => [],
],
"empty" => [
"valueString" => "",
"expected" => [],
],
"empty after trim" => [
"valueString" => " ",
"expected" => [],
],
];
}
}