Skip to content

Commit

Permalink
Add dry option for groups, refactor loader
Browse files Browse the repository at this point in the history
  • Loading branch information
jkniest committed Apr 3, 2024
1 parent f3012ec commit 7f7e0ff
Show file tree
Hide file tree
Showing 6 changed files with 136 additions and 83 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -7,3 +7,4 @@
/public/coverage
/src/Resources/app/storefront/node_modules/
composer.lock
src/Examples
1 change: 1 addition & 0 deletions psalm.xml
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@

<MoreSpecificImplementedParamType errorLevel="info" />
<ImplementedReturnTypeMismatch errorLevel="info" />
<RiskyTruthyFalsyComparison errorLevel="info" />
</issueHandlers>
</psalm>
25 changes: 20 additions & 5 deletions src/Command/LoadFixtureGroupCommand.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
namespace Basecom\FixturePlugin\Command;

use Basecom\FixturePlugin\FixtureLoader;
use Basecom\FixturePlugin\FixtureOption;
use Symfony\Component\Console\Attribute\AsCommand;
use Symfony\Component\Console\Command\Command;
use Symfony\Component\Console\Input\InputArgument;
Expand All @@ -23,24 +24,38 @@ public function __construct(

protected function configure(): void
{
$this->addArgument('groupName', InputArgument::REQUIRED, 'Name of fixture group');
$this->addArgument('groupName', InputArgument::REQUIRED, 'Name of fixture group')
->addOption('dry', description: 'Only list fixtures that would run without executing them');
}

protected function execute(InputInterface $input, OutputInterface $output): int
{
$io = new SymfonyStyle($input, $output);

$io->title('Running a group of fixtures');
$io = new SymfonyStyle($input, $output);

/** @var string $groupNameInput */
$groupNameInput = $input->getArgument('groupName');
$dry = (bool) ($input->getOption('dry') ?? false);

if (!\is_string($groupNameInput)) {
$io->error('Please make sure that your argument is of type string');

return Command::FAILURE;
}

$this->loader->runFixtureGroup($io, $groupNameInput);
$io->title("Running fixture of group: {$groupNameInput}");

if ($dry) {
$io->note('[INFO] Dry run mode enabled. No fixtures will be executed.');
}

$options = new FixtureOption(
dryMode: $dry,
groupName: $groupNameInput
);

if (!$this->loader->run($options, $io)) {
return Command::FAILURE;
}

$io->success('Done!');

Expand Down
2 changes: 1 addition & 1 deletion src/Fixture.php
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

namespace Basecom\FixturePlugin;

abstract readonly class Fixture
abstract class Fixture
{
abstract public function load(FixtureBag $bag): void;

Expand Down
176 changes: 99 additions & 77 deletions src/FixtureLoader.php
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,44 @@ public function __construct(\Traversable $fixtures)
$this->fixtures = iterator_to_array($fixtures);
}

public function run(FixtureOption $option, ?SymfonyStyle $io = null): bool
{
$fixtures = $this->prefilterFixtures($option);
if (\count($fixtures) <= 0) {
$io?->note('No fixtures found!');

return true;
}

$references = $this->buildFixtureReference($fixtures);
if (!empty($option->groupName) && !$this->checkThatAllDependenciesAreInGroup($references, $option->groupName, $io)) {
return false;
}

$this->runFixtures($option, $references, $io);

return true;
}

private function prefilterFixtures(FixtureOption $option): array
{
$fixtures = $this->fixtures;
$group = $option->groupName;

if (!empty($group)) {
$fixtures = array_filter(
$fixtures,
static fn (Fixture $fixture) => \in_array(strtolower($group), array_map('strtolower', $fixture->groups()), true)
);
}

return $fixtures;
}

public function runAll(SymfonyStyle $io): void
{
$this->fixtureReference = $this->buildFixtureReference($this->fixtures);
$this->runFixtures($io, $this->fixtures);
$this->runFixtures(new FixtureOption(), $this->fixtures, $io);
}

public function runSingle(SymfonyStyle $io, string $fixtureName, bool $withDependencies = false): void
Expand All @@ -41,77 +75,55 @@ public function runSingle(SymfonyStyle $io, string $fixtureName, bool $withDepen
}

$this->fixtureReference = $this->buildFixtureReference($this->fixtures);
$this->runFixtures($io, array_merge(array_map(
$this->runFixtures(new FixtureOption(), array_merge(array_map(
fn (string $fixtureClass) => $this->fixtureReference[$fixtureClass],
$this->recursiveGetAllDependenciesOfFixture($fixture)
), [$fixture]));
), [$fixture]), $io);

return;
}

$io->comment('No Fixture with name '.$fixtureName.' found');
}

public function runFixtureGroup(SymfonyStyle $io, string $groupName): void
{
$fixturesInGroup = [];

/** @var Fixture $fixture */
foreach ($this->fixtures as $fixture) {
// Check if fixture has been assigned to any group, if not stop the iteration
if (\count($fixture->groups()) <= 0) {
continue;
}

foreach ($fixture->groups() as $group) {
// Check if fixture is in affected group(from the command parameter). If not, skip the iteration.
if (strtolower($group) !== strtolower($groupName)) {
continue;
}

$fixturesInGroup[] = $fixture;
break;
}
}

// If no fixture was found for the group, return.
if (\count($fixturesInGroup) <= 0) {
$io->note('No fixtures in group '.$groupName);

return;
}

// Build the references, they are needed in dependency check.
$this->fixtureReference = $this->buildFixtureReference($this->fixtures);

foreach ($fixturesInGroup as $fixture) {
// If fixture doesn´t has any dependencies, skip the check.
/**
* @param array<string, Fixture> $fixtureReferences
*/
private function checkThatAllDependenciesAreInGroup(
array $fixtureReferences,
string $groupName,
?SymfonyStyle $io = null
): bool {
foreach ($fixtureReferences as $fixture) {
// If fixture doesn't have any dependencies, skip the check.
if (\count($fixture->dependsOn()) <= 0) {
continue;
}

// Check if dependencies of fixture are in the same group.
if (!$this->checkDependenciesAreInSameGroup($io, $fixture, $groupName)) {
return;
if (!$this->checkDependenciesAreInSameGroup($fixture, $fixtureReferences, $groupName, $io)) {
return false;
}
}

$this->runFixtures($io, $fixturesInGroup);
return true;
}

/**
* Check if dependencies of fixture are also in the same group. If not, show error and stop process.
*/
private function checkDependenciesAreInSameGroup(SymfonyStyle $io, Fixture $fixture, string $groupName): bool
{
private function checkDependenciesAreInSameGroup(
Fixture $fixture,
array $references,
string $groupName,
?SymfonyStyle $io = null
): bool {
$dependencies = $fixture->dependsOn();
$inGroup = array_map('strtolower', array_keys($references));

foreach ($dependencies as $dependency) {
/** @var Fixture $fixtureReference */
$fixtureReference = $this->fixtureReference[$dependency];
$lowerCaseDependencies = array_map('strtolower', $fixtureReference->groups());
if (!\in_array(strtolower($groupName), $lowerCaseDependencies, true)) {
$io->error('Dependency '.$dependency.' of fixture '.$fixture::class.' is not in the same group. Please add dependant fixture '.$dependency.' to group '.$groupName);
if (!\in_array(strtolower($dependency), $inGroup, true)) {
$io?->error(sprintf("Dependency '%s' of fixture '%s' is not in group '%s'", $dependency, $fixture::class, $groupName));

return false;
}
Expand All @@ -120,17 +132,25 @@ private function checkDependenciesAreInSameGroup(SymfonyStyle $io, Fixture $fixt
return true;
}

private function runFixtures(SymfonyStyle $io, array $fixtures): void
/**
* @param array<string, Fixture> $fixtures
*/
private function runFixtures(FixtureOption $option, array $fixtures, ?SymfonyStyle $io = null): void
{
$io->comment('Found '.\count($fixtures).' fixtures');
$io?->comment('Found '.\count($fixtures).' fixtures');

$fixtures = $this->sortAllByPriority($fixtures);
$fixtures = $this->buildDependencyTree($fixtures);
$fixtures = $this->runCorrectionLoop($fixtures, 10);

$bag = new FixtureBag();
foreach ($fixtures as $fixture) {
$io->note('Running '.$fixture::class);
$io?->note('Running '.$fixture::class);

if ($option->dryMode) {
continue;
}

$fixture->load($bag);
}
}
Expand All @@ -153,10 +173,14 @@ private function buildFixtureReference(array $fixtures): array
return $result;
}

/** @return Fixture[] */
/**
* @param array<string, Fixture> $fixtures
*
* @return array<string, Fixture>
*/
private function sortAllByPriority(array $fixtures): array
{
usort(
uasort(
$fixtures,
static fn (Fixture $fixture1, Fixture $fixture2): int => $fixture2->priority() <=> $fixture1->priority()
);
Expand All @@ -165,46 +189,44 @@ private function sortAllByPriority(array $fixtures): array
}

/**
* @param Fixture[] $fixtures
* @param array<string, Fixture> $fixtures
*
* @return Fixture[]
* @return array<string, Fixture>
*/
private function buildDependencyTree(array $fixtures): array
{
/** @var Fixture[] $sorted */
$sorted = [];

foreach ($fixtures as $fixture) {
foreach ($sorted as $sort) {
foreach ($sort->dependsOn() as $dependent) {
if ($dependent !== $fixture::class) {
continue;
}
uasort(
$fixtures,
fn (Fixture $a, Fixture $b) => $this->compareDependencies($a, $b)
);

/** @var int $sortIndex */
$sortIndex = array_search($sort, $sorted, true);
return $fixtures;
}

array_splice($sorted, $sortIndex, 0, [$fixture]);
continue 3;
}
}
private function compareDependencies(Fixture $a, Fixture $b): int
{
$aDependsOnB = \in_array($b::class, $a->dependsOn(), true);
$bDependsOnA = \in_array($a::class, $b->dependsOn(), true);

$sorted[] = $fixture;
if ($aDependsOnB && $bDependsOnA) {
return -1;
}

return $sorted;
return $bDependsOnA ? 1 : 0;
}

/**
* @param Fixture[] $fixtures
* @param array<string, Fixture> $fixtures
*
* @return array<string, Fixture>
*/
private function runCorrectionLoop(array $fixtures, int $tries): array
{
if ($tries <= 0) {
throw new \LogicException('Circular dependency tree detected. Please check your dependsOn methods');
}

/** @var Fixture[] $existing */
/** @var array<string, Fixture> $existing */
$existing = [];
$failed = false;

Expand All @@ -214,15 +236,15 @@ private function runCorrectionLoop(array $fixtures, int $tries): array
}

foreach ($fixture->dependsOn() as $dependent) {
if (\in_array($this->fixtureReference[$dependent], $existing, true)) {
if (\in_array($fixtures[$dependent], $existing, true)) {
continue;
}

$failed = true;
$existing[] = $this->fixtureReference[$dependent];
$failed = true;
$existing[$dependent] = $fixtures[$dependent];
}

$existing[] = $fixture;
$existing[$fixture::class] = $fixture;
}

if (!$failed) {
Expand Down
14 changes: 14 additions & 0 deletions src/FixtureOption.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<?php

declare(strict_types=1);

namespace Basecom\FixturePlugin;

readonly class FixtureOption
{
public function __construct(
public bool $dryMode = false,
public ?string $groupName = null
) {
}
}

0 comments on commit 7f7e0ff

Please sign in to comment.