Skip to content

Commit

Permalink
Add logical AND and OR conditions
Browse files Browse the repository at this point in the history
In order to make sure all conditions must apply to execute an action
the logical AND and OR conditions can combin multiple condition into
a single condition.
  • Loading branch information
sebastianfeldmann committed Apr 18, 2021
1 parent 832a516 commit 77a2b75
Show file tree
Hide file tree
Showing 9 changed files with 431 additions and 4 deletions.
59 changes: 59 additions & 0 deletions src/Hook/Condition/Logic.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/**
* This file is part of CaptainHook.
*
* (c) Sebastian Feldmann <sf@sebastian-feldmann.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace CaptainHook\App\Hook\Condition;

use CaptainHook\App\Hook\Condition;

/**
* Logical condition base class
*
* @package CaptainHook
* @author Sebastian Feldmann <sf@sebastian-feldmann.info>
* @author Andreas Heigl <andreas@heigl.org>
* @link https://github.com/captainhookphp/captainhook
* @since Class available since Release 5.7.0
*/
abstract class Logic implements Condition
{
/**
* List of conditions to logically connect
*
* @var \CaptainHook\App\Hook\Condition[]
*/
protected $conditions = [];

final private function __construct(Condition ...$conditions)
{
$this->conditions = $conditions;
}

/**
* Create a logic condition
*
* @param array $conditions
* @return \CaptainHook\App\Hook\Condition
*/
public static function fromConditionsArray(array $conditions): Condition
{
$realConditions = [];
foreach ($conditions as $condition) {
if (! $condition instanceof Condition) {
continue;
}
$realConditions[] = $condition;
}

return new static(...$realConditions);
}
}
40 changes: 40 additions & 0 deletions src/Hook/Condition/Logic/LogicAnd.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/**
* This file is part of CaptainHook
*
* (c) Sebastian Feldmann <sf@sebastian-feldmann.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace CaptainHook\App\Hook\Condition\Logic;

use CaptainHook\App\Console\IO;
use CaptainHook\App\Hook\Condition\Logic;
use SebastianFeldmann\Git\Repository;

/**
* Connects multiple conditions with 'and'
*
* @package CaptainHook
* @author Sebastian Feldmann <sf@sebastian-feldmann.info>
* @author Andreas Heigl <andreas@heigl.org>
* @link https://github.com/captainhookphp/captainhook
* @since Class available since Release 5.7.0
*/
final class LogicAnd extends Logic
{
public function isTrue(IO $io, Repository $repository): bool
{
foreach ($this->conditions as $condition) {
if (false === $condition->isTrue($io, $repository)) {
return false;
}
}
return true;
}
}
40 changes: 40 additions & 0 deletions src/Hook/Condition/Logic/LogicOr.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
<?php

/**
* This file is part of CaptainHook
*
* (c) Sebastian Feldmann <sf@sebastian-feldmann.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace CaptainHook\App\Hook\Condition\Logic;

use CaptainHook\App\Console\IO;
use CaptainHook\App\Hook\Condition\Logic;
use SebastianFeldmann\Git\Repository;

/**
* Connects multiple conditions with 'or'
*
* @package CaptainHook
* @author Sebastian Feldmann <sf@sebastian-feldmann.info>
* @author Andreas Heigl <andreas@heigl.org>
* @link https://github.com/captainhookphp/captainhook
* @since Class available since Release 5.7.0
*/
final class LogicOr extends Logic
{
public function isTrue(IO $io, Repository $repository): bool
{
foreach ($this->conditions as $condition) {
if (true === $condition->isTrue($io, $repository)) {
return true;
}
}
return false;
}
}
35 changes: 35 additions & 0 deletions src/Runner/Condition.php
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,10 @@ public function doesConditionApply(Config\Condition $config): bool
*/
private function createCondition(Config\Condition $config): ConditionInterface
{
if ($this->isLogicCondition($config)) {
return $this->createLogicCondition($config);
}

if (Util::getExecType($config->getExec()) === 'cli') {
return new Cli(new Processor(), $config->getExec());
}
Expand All @@ -103,6 +107,26 @@ private function createCondition(Config\Condition $config): ConditionInterface
return new $class(...$config->getArgs());
}

/**
* Create a logic condition with configures sub conditions
*
* @param \CaptainHook\App\Config\Condition $config
* @return \CaptainHook\App\Hook\Condition
*/
private function createLogicCondition(Config\Condition $config): ConditionInterface
{
$class = '\\CaptainHook\\App\\Hook\\Condition\\Logic\\Logic' . ucfirst(strtolower($config->getExec()));
$conditions = [];
foreach ($config->getArgs() as $condition) {
$currentCondition = $this->createCondition(new Config\Condition($condition['exec'], $condition['args']));
if (!$this->isApplicable($currentCondition)) {
$this->io->write('Condition skipped due to hook constraint', true, IO::VERBOSE);
continue;
}
$conditions[] = $currentCondition;
}
return $class::fromConditionsArray($conditions);
}

/**
* Make sure the condition can be used during this hook
Expand All @@ -117,4 +141,15 @@ private function isApplicable(ConditionInterface $condition)
}
return true;
}

/**
* Is the condition a logic 'AND' or 'OR' condition
*
* @param \CaptainHook\App\Config\Condition $config
* @return bool
*/
private function isLogicCondition(Config\Condition $config): bool
{
return in_array(strtolower($config->getExec()), ['and', 'or']);
}
}
13 changes: 13 additions & 0 deletions tests/CaptainHook/Config/FactoryTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -170,6 +170,19 @@ public function testCreateEmptyWithIncludes(): void
$this->assertCount(1, $config->getHookConfig('pre-commit')->getActions());
}

/**
* Tests Factory::create
*
* @throws \Exception
*/
public function testCreateWithNestedAndConditions(): void
{
$config = Factory::create(realpath(__DIR__ . '/../../files/config/valid-with-nested-and-conditions.json'));

$this->assertTrue($config->getHookConfig('pre-commit')->isEnabled());
$this->assertCount(1, $config->getHookConfig('pre-commit')->getActions());
}

/**
* Tests Factory::create
*
Expand Down
74 changes: 74 additions & 0 deletions tests/CaptainHook/Hook/Condition/Logic/LogicAndTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?php

/**
* This file is part of CaptainHook.
*
* (c) Sebastian Feldmann <sf@sebastian-feldmann.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CaptainHook\App\Hook\Condition\Logic;

use CaptainHook\App\Console\IO\Mockery as IOMockery;
use CaptainHook\App\Hook\Condition;
use CaptainHook\App\Mockery as AppMockery;
use PHPUnit\Framework\TestCase;

class LogicalAndTest extends TestCase
{
use IOMockery;
use AppMockery;

/**
* @covers \CaptainHook\App\Hook\Condition\Logic\LogicAnd::isTrue
* @covers \CaptainHook\App\Hook\Condition\Logic\LogicAnd::fromConditionsArray
*/
public function testLogicAndReturnsFalseWithOneFailure(): void
{
$io = $this->createIOMock();
$repository = $this->createRepositoryMock();
$true = $this->getMockBuilder(Condition::class)->getMock();
$false = $this->getMockBuilder(Condition::class)->getMock();

$true->method('isTrue')->willReturn(true);
$false->method('isTrue')->willReturn(false);

$condition = LogicAnd::fromConditionsArray([$true, $false]);

$this->assertFalse($condition->isTrue($io, $repository));
}

/**
* @covers \CaptainHook\App\Hook\Condition\Logic\LogicAnd::isTrue
* @covers \CaptainHook\App\Hook\Condition\Logic\LogicAnd::fromConditionsArray
*/
public function testLogicAndReturnsTrueWithAllSuccess(): void
{
$io = $this->createIOMock();
$repository = $this->createRepositoryMock();
$true = $this->getMockBuilder(Condition::class)->getMock();

$true->method('isTrue')->willReturn(true);

$condition = LogicAnd::fromConditionsArray([$true, $true, $true]);

$this->assertTrue($condition->isTrue($io, $repository));
}

/**
* @covers \CaptainHook\App\Hook\Condition\Logic\LogicAnd::fromConditionsArray
*/
public function testNamedConstructorIgnoresNonCondition(): void
{
$io = $this->createIOMock();
$repository = $this->createRepositoryMock();
$true = $this->getMockBuilder(Condition::class)->getMock();
$true->expects($this->exactly(2))->method('isTrue')->willReturn(true);

$condition = LogicAnd::fromConditionsArray([$true, 'string', $true]);

$this->assertTrue($condition->isTrue($io, $repository));
}
}
59 changes: 59 additions & 0 deletions tests/CaptainHook/Hook/Condition/Logic/LogicOrTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
<?php

/**
* This file is part of CaptainHook.
*
* (c) Sebastian Feldmann <sf@sebastian-feldmann.info>
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CaptainHook\App\Hook\Condition\Logic;

use CaptainHook\App\Console\IO\Mockery as IOMockery;
use CaptainHook\App\Hook\Condition;
use CaptainHook\App\Mockery as AppMockery;
use PHPUnit\Framework\TestCase;

class LogicalOrTest extends TestCase
{
use IOMockery;
use AppMockery;

/**
* @covers \CaptainHook\App\Hook\Condition\Logic\LogicOr::isTrue
* @covers \CaptainHook\App\Hook\Condition\Logic\LogicOr::fromConditionsArray
*/
public function testLogicOrReturnsTrueWithOneFailure(): void
{
$io = $this->createIOMock();
$repository = $this->createRepositoryMock();
$true = $this->getMockBuilder(Condition::class)->getMock();
$false = $this->getMockBuilder(Condition::class)->getMock();

$true->method('isTrue')->willReturn(true);
$false->method('isTrue')->willReturn(false);

$condition = LogicOr::fromConditionsArray([$false, $true]);

$this->assertTrue($condition->isTrue($io, $repository));
}

/**
* @covers \CaptainHook\App\Hook\Condition\Logic\LogicOr::isTrue
* @covers \CaptainHook\App\Hook\Condition\Logic\LogicOr::fromConditionsArray
*/
public function testLogicOrReturnsFalseWithAllFailing(): void
{
$io = $this->createIOMock();
$repository = $this->createRepositoryMock();
$false = $this->getMockBuilder(Condition::class)->getMock();

$false->method('isTrue')->willReturn(false);

$condition = LogicOr::fromConditionsArray([$false, $false, $false]);

$this->assertFalse($condition->isTrue($io, $repository));
}
}
Loading

0 comments on commit 77a2b75

Please sign in to comment.