Skip to content

Commit

Permalink
Introduced RetryChoiceListFactory to deal with parallelism issues
Browse files Browse the repository at this point in the history
  • Loading branch information
mnocon committed Oct 20, 2023
1 parent e9a85ca commit 46492a6
Show file tree
Hide file tree
Showing 7 changed files with 290 additions and 1 deletion.
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,8 @@
"symfony/yaml": "^5.0",
"psy/psysh": "^0.10.8",
"oleg-andreyev/mink-phpwebdriver": "^1.2",
"oleg-andreyev/mink-phpwebdriver-extension": "^1.0"
"oleg-andreyev/mink-phpwebdriver-extension": "^1.0",
"symfony/form": "^5.4"
},
"require-dev": {
"ibexa/code-style": "^1.0",
Expand Down
45 changes: 45 additions & 0 deletions phpstan-baseline.neon
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,36 @@ parameters:
count: 1
path: src/bundle/DependencyInjection/IbexaBehatExtension.php

-
message: "#^Method Ibexa\\\\Bundle\\\\Behat\\\\Form\\\\RetryChoiceListFactory\\:\\:createListFromChoices\\(\\) has parameter \\$choices with no value type specified in iterable type iterable\\.$#"
count: 1
path: src/bundle/Form/RetryChoiceListFactory.php

-
message: "#^Method Ibexa\\\\Bundle\\\\Behat\\\\Form\\\\RetryChoiceListFactory\\:\\:createView\\(\\) has parameter \\$attr with no value type specified in iterable type array\\.$#"
count: 1
path: src/bundle/Form/RetryChoiceListFactory.php

-
message: "#^Method Ibexa\\\\Bundle\\\\Behat\\\\Form\\\\RetryChoiceListFactory\\:\\:createView\\(\\) has parameter \\$preferredChoices with no value type specified in iterable type array\\.$#"
count: 1
path: src/bundle/Form/RetryChoiceListFactory.php

-
message: "#^Method Symfony\\\\Component\\\\Form\\\\ChoiceList\\\\Factory\\\\ChoiceListFactoryInterface\\:\\:createListFromChoices\\(\\) invoked with 3 parameters, 1\\-2 required\\.$#"
count: 1
path: src/bundle/Form/RetryChoiceListFactory.php

-
message: "#^Method Symfony\\\\Component\\\\Form\\\\ChoiceList\\\\Factory\\\\ChoiceListFactoryInterface\\:\\:createListFromLoader\\(\\) invoked with 3 parameters, 1\\-2 required\\.$#"
count: 1
path: src/bundle/Form/RetryChoiceListFactory.php

-
message: "#^Method Symfony\\\\Component\\\\Form\\\\ChoiceList\\\\Factory\\\\ChoiceListFactoryInterface\\:\\:createView\\(\\) invoked with 7 parameters, 1\\-6 required\\.$#"
count: 1
path: src/bundle/Form/RetryChoiceListFactory.php

-
message: "#^Method Ibexa\\\\Bundle\\\\Behat\\\\IbexaBehatBundle\\:\\:build\\(\\) has no return type specified\\.$#"
count: 1
Expand Down Expand Up @@ -3025,6 +3055,21 @@ parameters:
count: 1
path: tests/Browser/Element/Assert/CollectionAssertTest.php

-
message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Behat\\\\Form\\\\Stub\\\\UnstableChoiceListFactory\\:\\:createListFromChoices\\(\\) has parameter \\$choices with no value type specified in iterable type iterable\\.$#"
count: 1
path: tests/bundle/Form/Stub/UnstableChoiceListFactory.php

-
message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Behat\\\\Form\\\\Stub\\\\UnstableChoiceListFactory\\:\\:createView\\(\\) has parameter \\$attr with no value type specified in iterable type array\\.$#"
count: 1
path: tests/bundle/Form/Stub/UnstableChoiceListFactory.php

-
message: "#^Method Ibexa\\\\Tests\\\\Bundle\\\\Behat\\\\Form\\\\Stub\\\\UnstableChoiceListFactory\\:\\:createView\\(\\) has parameter \\$preferredChoices with no value type specified in iterable type array\\.$#"
count: 1
path: tests/bundle/Form/Stub/UnstableChoiceListFactory.php

-
message: "#^Method Ibexa\\\\Tests\\\\Behat\\\\Browser\\\\Element\\\\BaseTestCase\\:\\:createCollection\\(\\) has parameter \\$elementTexts with no type specified\\.$#"
count: 1
Expand Down
93 changes: 93 additions & 0 deletions src/bundle/Form/RetryChoiceListFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Bundle\Behat\Form;

use ErrorException;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;

final class RetryChoiceListFactory implements ChoiceListFactoryInterface
{
private const RETRY_LIMIT = 2;

private ChoiceListFactoryInterface $choiceListFactory;

public function __construct(ChoiceListFactoryInterface $choiceListFactory)
{
$this->choiceListFactory = $choiceListFactory;
}

/** {@inheritDoc} */
public function createListFromChoices(iterable $choices, callable $value = null): ChoiceListInterface
{
$filter = \func_num_args() > 2 ? func_get_arg(2) : null;

return $this->executeWithRetry(function () use ($choices, $value, $filter) {
return $this->choiceListFactory->createListFromChoices($choices, $value, $filter);
});
}

/** {@inheritDoc} */
public function createListFromLoader(ChoiceLoaderInterface $loader, callable $value = null): ChoiceListInterface
{
$filter = \func_num_args() > 2 ? func_get_arg(2) : null;

return $this->executeWithRetry(function () use ($loader, $value, $filter) {
return $this->choiceListFactory->createListFromLoader($loader, $value, $filter);
});
}

/** {@inheritDoc} */
public function createView(
ChoiceListInterface $list,
$preferredChoices = null,
$label = null,
callable $index = null,
callable $groupBy = null,
$attr = null
): ChoiceListView {
$labelTranslationParameters = \func_num_args() > 6 ? func_get_arg(6) : [];

return $this->executeWithRetry(function () use ($list, $preferredChoices, $label, $index, $groupBy, $attr, $labelTranslationParameters) {
return $this->choiceListFactory->createView(
$list,
$preferredChoices,
$label,
$index,
$groupBy,
$attr,
$labelTranslationParameters
);
});
}

/**
* @template T
*
* @param callable(mixed ...$args): T $fn
*
* @return T
*/
private function executeWithRetry(callable $fn)
{
$counter = 0;
while (true) {
try {
return $fn();
} catch (ErrorException $e) {
if ($counter > self::RETRY_LIMIT) {
throw $e;
}
++$counter;
}
}
}
}
1 change: 1 addition & 0 deletions src/bundle/Resources/config/services.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ imports:
- { resource: services/contexts.yaml }
- { resource: services/controllers.yaml }
- { resource: services/factory.yaml }
- { resource: services/form.yaml }
- { resource: services/fieldtype_data_providers.yaml }
- { resource: services/known_issues.yaml }
- { resource: services/limitation_parsers.yaml }
Expand Down
8 changes: 8 additions & 0 deletions src/bundle/Resources/config/services/form.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
services:
_defaults:
autowire: true
autoconfigure: true
public: false

Ibexa\Bundle\Behat\Form\RetryChoiceListFactory:
decorates: 'form.choice_list_factory'
78 changes: 78 additions & 0 deletions tests/bundle/Form/RetryChoiceListFactoryTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Bundle\Behat\Form;

use ErrorException;
use Ibexa\Bundle\Behat\Form\RetryChoiceListFactory;
use Ibexa\Tests\Bundle\Behat\Form\Stub\UnstableChoiceListFactory;
use PHPUnit\Framework\TestCase;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;

final class RetryChoiceListFactoryTest extends TestCase
{
/**
* @dataProvider provider
*/
public function testCreateListFromChoicesSuccess(int $numberOfFails): void
{
$retryChoiceListFactory = new RetryChoiceListFactory(new UnstableChoiceListFactory($numberOfFails));
self::assertEquals([], $retryChoiceListFactory->createListFromChoices([], null)->getChoices());
}

/**
* @dataProvider provider
*/
public function testCreateListFromLoaderSuccess(int $numberOfFails): void
{
$retryChoiceListFactory = new RetryChoiceListFactory(new UnstableChoiceListFactory($numberOfFails));
self::assertEquals([], $retryChoiceListFactory->createListFromLoader($this->createMock(ChoiceLoaderInterface::class))->getChoices());
}

/**
* @dataProvider provider
*/
public function testCreateViewSuccess(int $numberOfFails): void
{
$retryChoiceListFactory = new RetryChoiceListFactory(new UnstableChoiceListFactory($numberOfFails));
self::assertEquals([], $retryChoiceListFactory->createView($this->createMock(ChoiceListInterface::class), null)->choices);
}

public function testCreateListFromChoicesFail(): void
{
$retryChoiceListFactory = new RetryChoiceListFactory(new UnstableChoiceListFactory(4));
$this->expectException(ErrorException::class);
$retryChoiceListFactory->createListFromChoices([], null);
}

public function testCreateListFromLoaderFail(): void
{
$retryChoiceListFactory = new RetryChoiceListFactory(new UnstableChoiceListFactory(4));
$this->expectException(ErrorException::class);
$retryChoiceListFactory->createListFromLoader($this->createMock(ChoiceLoaderInterface::class));
}

public function testCreateViewFail(): void
{
$retryChoiceListFactory = new RetryChoiceListFactory(new UnstableChoiceListFactory(4));
$this->expectException(ErrorException::class);
$retryChoiceListFactory->createView($this->createMock(ChoiceListInterface::class), null);
}

/**
* @return iterable<array<string,int>>
*/
public static function provider(): iterable
{
yield ['No failures' => 0];
yield ['One failure' => 1];
yield ['Two failures' => 2];
yield ['Three failures' => 3];
}
}
63 changes: 63 additions & 0 deletions tests/bundle/Form/Stub/UnstableChoiceListFactory.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@
<?php

/**
* @copyright Copyright (C) Ibexa AS. All rights reserved.
* @license For full copyright and license information view LICENSE file distributed with this source code.
*/
declare(strict_types=1);

namespace Ibexa\Tests\Bundle\Behat\Form\Stub;

use ErrorException;
use Symfony\Component\Form\ChoiceList\ArrayChoiceList;
use Symfony\Component\Form\ChoiceList\ChoiceListInterface;
use Symfony\Component\Form\ChoiceList\Factory\ChoiceListFactoryInterface;
use Symfony\Component\Form\ChoiceList\Loader\ChoiceLoaderInterface;
use Symfony\Component\Form\ChoiceList\View\ChoiceListView;

final class UnstableChoiceListFactory implements ChoiceListFactoryInterface
{
private int $successfulCallAfterNthTry;

private int $createListFromChoicesCounter = 0;

private int $createListFromLoaderCounter = 0;

private int $createViewCounter = 0;

public function __construct(int $successfulCallAfterNthTry)
{
$this->successfulCallAfterNthTry = $successfulCallAfterNthTry;
}

public function createListFromChoices(iterable $choices, callable $value = null)
{
++$this->createListFromChoicesCounter;
$this->failIfNeeded($this->createListFromChoicesCounter);

return new ArrayChoiceList([]);
}

public function createListFromLoader(ChoiceLoaderInterface $loader, callable $value = null)
{
++$this->createListFromLoaderCounter;
$this->failIfNeeded($this->createListFromLoaderCounter);

return new ArrayChoiceList([]);
}

public function createView(ChoiceListInterface $list, $preferredChoices = null, $label = null, callable $index = null, callable $groupBy = null, $attr = null)
{
++$this->createViewCounter;
$this->failIfNeeded($this->createViewCounter);

return new ChoiceListView([]);
}

private function failIfNeeded(int $callNumber): void
{
if ($callNumber <= $this->successfulCallAfterNthTry) {
throw new ErrorException(sprintf('Failing call: %d', $callNumber));
}
}
}

0 comments on commit 46492a6

Please sign in to comment.