Skip to content
Merged
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
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ Latest
* [#220](https://github.com/cleverage/process-bundle/issues/220) Fix the stop error strategy: throw a `ProcessFailedException` (with the original exception as `previous`) instead of a `FatalError`, so that the command exits with a non-zero code when a process fails and `ProcessLauncherTask` detects failed sub-processes. Update documentation, add tests.
* [#221](https://github.com/cleverage/process-bundle/issues/221) Fix CsvSplitterTask: each produced file contains `max_lines` data lines (instead of `max_lines - 2`), no infinite loop with `max_lines` <= 2 (`max_lines` must now be an integer greater than 0), no header-only file at the end. Update documentation, add tests.
* [#223](https://github.com/cleverage/process-bundle/issues/223) Fix CounterTask: the final count is outputted once, as `flush()` may be called several times. Document that `flush()` implementations must be idempotent. Update documentation, add tests.
* [#228](https://github.com/cleverage/process-bundle/issues/228) Add the missing `setAllowedTypes()` on HashTransformer (`raw_output`), SimpleBatchTask (`batch_count`), ConditionTrait (`empty`, `not_empty`), NormalizerTask / SerializerTask / DeserializerTask (`context`) and ObjectUpdaterTask (`property_path`): a wrong type is now reported when the options are resolved. Add tests.

## Deprecated
* [#189](https://github.com/cleverage/process-bundle/issues/189) EventDispatcherTask: when `event_name` is set, listening to `CleverAge\ProcessBundle\Event\EventDispatcherTaskEvent` is deprecated (the event is still dispatched under its class name, with an `E_USER_DEPRECATED` error, if it has listeners). Listen to the configured `event_name` instead: the BC layer will be removed in v6.0.
Expand Down
2 changes: 2 additions & 0 deletions src/Task/ObjectUpdaterTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
use CleverAge\ProcessBundle\Model\ProcessState;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccessorInterface;
use Symfony\Component\PropertyAccess\PropertyPathInterface;

/**
* Takes an array containing an object and a value updates an object's property with this value, then return the object.
Expand Down Expand Up @@ -44,5 +45,6 @@ public function execute(ProcessState $state): void
protected function configureOptions(OptionsResolver $resolver): void
{
$resolver->setRequired(['property_path']);
$resolver->setAllowedTypes('property_path', ['string', PropertyPathInterface::class]);
}
}
1 change: 1 addition & 0 deletions src/Task/Serialization/DeserializerTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -45,5 +45,6 @@ protected function configureOptions(OptionsResolver $resolver): void
$resolver->setDefaults([
'context' => [],
]);
$resolver->setAllowedTypes('context', ['array']);
}
}
1 change: 1 addition & 0 deletions src/Task/Serialization/NormalizerTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -51,5 +51,6 @@ protected function configureOptions(OptionsResolver $resolver): void
$resolver->setDefaults([
'context' => [],
]);
$resolver->setAllowedTypes('context', ['array']);
}
}
1 change: 1 addition & 0 deletions src/Task/Serialization/SerializerTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -39,5 +39,6 @@ protected function configureOptions(OptionsResolver $resolver): void
$resolver->setDefaults([
'context' => [],
]);
$resolver->setAllowedTypes('context', ['array']);
}
}
1 change: 1 addition & 0 deletions src/Task/SimpleBatchTask.php
Original file line number Diff line number Diff line change
Expand Up @@ -53,5 +53,6 @@ protected function configureOptions(OptionsResolver $resolver): void
$resolver->setDefaults([
'batch_count' => 10,
]);
$resolver->setAllowedTypes('batch_count', ['int', 'null']);
}
}
2 changes: 2 additions & 0 deletions src/Transformer/ConditionTrait.php
Original file line number Diff line number Diff line change
Expand Up @@ -99,6 +99,8 @@ protected function configureConditionOptions(OptionsResolver $resolver): void
$resolver->setDefault('match_regexp', []);
$resolver->setAllowedTypes('not_match', 'array');
$resolver->setAllowedTypes('match', 'array');
$resolver->setAllowedTypes('not_empty', 'array');
$resolver->setAllowedTypes('empty', 'array');
$resolver->setAllowedTypes('not_match_regexp', 'array');
$resolver->setAllowedTypes('match_regexp', 'array');
}
Expand Down
1 change: 1 addition & 0 deletions src/Transformer/String/HashTransformer.php
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ public function configureOptions(OptionsResolver $resolver): void

$resolver->setDefined('raw_output');
$resolver->setDefault('raw_output', false);
$resolver->setAllowedTypes('raw_output', 'bool');
}

public function transform(mixed $value, array $options = []): string
Expand Down
186 changes: 186 additions & 0 deletions tests/OptionAllowedTypesTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,186 @@
<?php

declare(strict_types=1);

/*
* This file is part of the CleverAge/ProcessBundle package.
*
* Copyright (c) Clever-Age
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

namespace CleverAge\ProcessBundle\Tests;

use CleverAge\ProcessBundle\Configuration\ProcessConfiguration;
use CleverAge\ProcessBundle\Configuration\TaskConfiguration;
use CleverAge\ProcessBundle\Context\ContextualOptionResolver;
use CleverAge\ProcessBundle\Model\AbstractConfigurableTask;
use CleverAge\ProcessBundle\Model\ProcessHistory;
use CleverAge\ProcessBundle\Model\ProcessState;
use CleverAge\ProcessBundle\Task\ObjectUpdaterTask;
use CleverAge\ProcessBundle\Task\Serialization\DeserializerTask;
use CleverAge\ProcessBundle\Task\Serialization\NormalizerTask;
use CleverAge\ProcessBundle\Task\Serialization\SerializerTask;
use CleverAge\ProcessBundle\Task\SimpleBatchTask;
use CleverAge\ProcessBundle\Transformer\Array\ArrayFilterTransformer;
use CleverAge\ProcessBundle\Transformer\ConditionTrait;
use CleverAge\ProcessBundle\Transformer\ConfigurableTransformerInterface;
use CleverAge\ProcessBundle\Transformer\String\HashTransformer;
use PHPUnit\Framework\Attributes\DataProvider;
use PHPUnit\Framework\TestCase;
use Symfony\Component\OptionsResolver\Exception\InvalidOptionsException;
use Symfony\Component\OptionsResolver\OptionsResolver;
use Symfony\Component\PropertyAccess\PropertyAccess;
use Symfony\Component\PropertyAccess\PropertyPath;
use Symfony\Component\Serializer\Serializer;

/**
* Options are validated when resolved, instead of failing later with an unclear error.
*/
#[\PHPUnit\Framework\Attributes\CoversClass(HashTransformer::class)]
#[\PHPUnit\Framework\Attributes\CoversClass(ArrayFilterTransformer::class)]
#[\PHPUnit\Framework\Attributes\CoversTrait(ConditionTrait::class)]
#[\PHPUnit\Framework\Attributes\CoversClass(SimpleBatchTask::class)]
#[\PHPUnit\Framework\Attributes\CoversClass(NormalizerTask::class)]
#[\PHPUnit\Framework\Attributes\CoversClass(SerializerTask::class)]
#[\PHPUnit\Framework\Attributes\CoversClass(DeserializerTask::class)]
#[\PHPUnit\Framework\Attributes\CoversClass(ObjectUpdaterTask::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(AbstractConfigurableTask::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessConfiguration::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(TaskConfiguration::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(ContextualOptionResolver::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessHistory::class)]
#[\PHPUnit\Framework\Attributes\UsesClass(ProcessState::class)]
class OptionAllowedTypesTest extends TestCase
{
/**
* @return iterable<string, array{string, array<string, mixed>}>
*/
public static function invalidTransformerOptionsProvider(): iterable
{
yield 'hash raw_output' => [HashTransformer::class, ['algo' => 'md5', 'raw_output' => 'yes']];
yield 'condition empty' => [ArrayFilterTransformer::class, ['condition' => ['empty' => 'name']]];
yield 'condition not_empty' => [ArrayFilterTransformer::class, ['condition' => ['not_empty' => 'name']]];
}

/**
* @param class-string<ConfigurableTransformerInterface> $class
* @param array<string, mixed> $options
*/
#[DataProvider('invalidTransformerOptionsProvider')]
public function testInvalidTransformerOptionIsRejected(string $class, array $options): void
{
$this->expectException(InvalidOptionsException::class);

$this->resolveTransformerOptions($class, $options);
}

/**
* @return iterable<string, array{string, array<string, mixed>}>
*/
public static function validTransformerOptionsProvider(): iterable
{
yield 'hash raw_output' => [HashTransformer::class, ['algo' => 'md5', 'raw_output' => true]];
yield 'condition empty' => [ArrayFilterTransformer::class, ['condition' => ['empty' => ['name' => null]]]];
yield 'condition not_empty' => [ArrayFilterTransformer::class, ['condition' => ['not_empty' => ['name' => null]]]];
}

/**
* @param class-string<ConfigurableTransformerInterface> $class
* @param array<string, mixed> $options
*/
#[DataProvider('validTransformerOptionsProvider')]
public function testValidTransformerOptionIsAccepted(string $class, array $options): void
{
self::assertNotEmpty($this->resolveTransformerOptions($class, $options));
}

/**
* @return iterable<string, array{string, array<string, mixed>}>
*/
public static function invalidTaskOptionsProvider(): iterable
{
yield 'simple batch batch_count' => [SimpleBatchTask::class, ['batch_count' => '10']];
yield 'normalizer context' => [NormalizerTask::class, ['format' => 'json', 'context' => 'groups']];
yield 'serializer context' => [SerializerTask::class, ['format' => 'json', 'context' => 'groups']];
yield 'deserializer context' => [DeserializerTask::class, ['type' => 'array', 'format' => 'json', 'context' => 'groups']];
yield 'object updater property_path' => [ObjectUpdaterTask::class, ['property_path' => ['name']]];
}

/**
* @param class-string<AbstractConfigurableTask> $class
* @param array<string, mixed> $options
*/
#[DataProvider('invalidTaskOptionsProvider')]
public function testInvalidTaskOptionIsRejected(string $class, array $options): void
{
$this->expectException(InvalidOptionsException::class);

$this->initializeTask($class, $options);
}

/**
* @return iterable<string, array{string, array<string, mixed>}>
*/
public static function validTaskOptionsProvider(): iterable
{
yield 'simple batch batch_count' => [SimpleBatchTask::class, ['batch_count' => 10]];
yield 'simple batch null batch_count' => [SimpleBatchTask::class, ['batch_count' => null]];
yield 'normalizer context' => [NormalizerTask::class, ['format' => 'json', 'context' => ['groups' => ['a']]]];
yield 'serializer context' => [SerializerTask::class, ['format' => 'json', 'context' => []]];
yield 'deserializer context' => [DeserializerTask::class, ['type' => 'array', 'format' => 'json', 'context' => []]];
yield 'object updater string property_path' => [ObjectUpdaterTask::class, ['property_path' => 'name']];
yield 'object updater PropertyPath property_path' => [ObjectUpdaterTask::class, ['property_path' => new PropertyPath('name')]];
}

/**
* @param class-string<AbstractConfigurableTask> $class
* @param array<string, mixed> $options
*/
#[DataProvider('validTaskOptionsProvider')]
public function testValidTaskOptionIsAccepted(string $class, array $options): void
{
$this->expectNotToPerformAssertions();

$this->initializeTask($class, $options);
}

/**
* @param class-string<ConfigurableTransformerInterface> $class
* @param array<string, mixed> $options
*
* @return array<string, mixed>
*/
private function resolveTransformerOptions(string $class, array $options): array
{
$transformer = ArrayFilterTransformer::class === $class
? new ArrayFilterTransformer(PropertyAccess::createPropertyAccessor())
: new $class();
$resolver = new OptionsResolver();
$transformer->configureOptions($resolver);

return $resolver->resolve($options);
}

/**
* @param class-string<AbstractConfigurableTask> $class
* @param array<string, mixed> $options
*/
private function initializeTask(string $class, array $options): void
{
$task = match ($class) {
NormalizerTask::class, SerializerTask::class, DeserializerTask::class => new $class(new Serializer()),
ObjectUpdaterTask::class => new ObjectUpdaterTask(PropertyAccess::createPropertyAccessor()),
default => new $class(),
};

$processConfiguration = new ProcessConfiguration('test', []);
$state = new ProcessState($processConfiguration, new ProcessHistory($processConfiguration));
$state->setContextualOptionResolver(new ContextualOptionResolver());
$state->setContext([]);
$state->setTaskConfiguration(new TaskConfiguration('task', $class, $options));
$task->initialize($state);
}
}
Loading