Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
* develop:
  simplify string set implementation
  change set too small that it may return the same value twice
  improve sequence shrinking
  • Loading branch information
Baptouuuu committed Jun 18, 2020
2 parents 011acc0 + b12cb97 commit f25ccb9
Show file tree
Hide file tree
Showing 6 changed files with 152 additions and 153 deletions.
151 changes: 131 additions & 20 deletions src/Set/Sequence.php
Expand Up @@ -87,12 +87,12 @@ public function values(Random $rand): \Generator
if ($immutable) {
yield Set\Value::immutable(
$this->wrap($values),
$this->shrink(false, $values),
$this->shrinkFast(false, $values),
);
} else {
yield Set\Value::mutable(
fn() => $this->wrap($values),
$this->shrink(true, $values),
$this->shrinkFast(true, $values),
);
}
}
Expand Down Expand Up @@ -122,9 +122,16 @@ private function wrap(array $values): array
}

/**
* The shrinking starts here and recursively will do:
* - remove the half end of the sequence (while it breaks the test)
* - remove the last element (while it breaks the test)
* - remove the head element (while it breaks the test)
* - shrink elements with their A strategy (while it breaks the test)
* - shrink elements with their B strategy (while it breaks the test)
*
* @param list<Value<I>> $sequence
*/
private function shrink(bool $mutable, array $sequence): ?Dichotomy
private function shrinkFast(bool $mutable, array $sequence): ?Dichotomy
{
if (\count($sequence) === 0) {
return null;
Expand All @@ -140,6 +147,63 @@ private function shrink(bool $mutable, array $sequence): ?Dichotomy
);
}

/**
* @param list<Value<I>> $sequence
*/
private function shrinkEnds(bool $mutable, array $sequence): ?Dichotomy
{
if (\count($sequence) === 0) {
return null;
}

if (!($this->predicate)($this->wrap($sequence))) {
return null;
}

return new Dichotomy(
$this->removeTailElement($mutable, $sequence),
$this->removeHeadElement($mutable, $sequence),
);
}

/**
* @param list<Value<I>> $sequence
*/
private function shrinkDecoratedWithStrategyA(bool $mutable, array $sequence): ?Dichotomy
{
if (\count($sequence) === 0) {
return null;
}

if (!($this->predicate)($this->wrap($sequence))) {
return null;
}

return new Dichotomy(
$this->removeHeadElement($mutable, $sequence),
$this->shrinkValuesWithStrategyA($mutable, $sequence),
);
}

/**
* @param list<Value<I>> $sequence
*/
private function shrinkDecoratedWithStrategyB(bool $mutable, array $sequence): ?Dichotomy
{
if (\count($sequence) === 0) {
return null;
}

if (!($this->predicate)($this->wrap($sequence))) {
return null;
}

return new Dichotomy(
$this->shrinkValuesWithStrategyA($mutable, $sequence),
$this->shrinkValuesWithStrategyB($mutable, $sequence),
);
}

/**
* @param list<Value<I>> $sequence
*
Expand All @@ -156,7 +220,17 @@ private function removeHalfTheStructure(bool $mutable, array $sequence): callabl
return $this->removeTailElement($mutable, $sequence);
}

return $this->strategy($mutable, $shrinked);
if ($mutable) {
return fn(): Value => Value::mutable(
fn() => $this->wrap($shrinked),
$this->shrinkFast(true, $shrinked),
);
}

return fn(): Value => Value::immutable(
$this->wrap($shrinked),
$this->shrinkFast(false, $shrinked),
);
}

/**
Expand All @@ -169,11 +243,48 @@ private function removeTailElement(bool $mutable, array $sequence): callable
$shrinked = $sequence;
\array_pop($shrinked);

if (!($this->predicate)($this->wrap($shrinked))) {
return $this->removeHeadElement($mutable, $sequence);
}

if ($mutable) {
return fn(): Value => Value::mutable(
fn() => $this->wrap($shrinked),
$this->shrinkEnds(true, $shrinked),
);
}

return fn(): Value => Value::immutable(
$this->wrap($shrinked),
$this->shrinkEnds(false, $shrinked),
);
}

/**
* @param list<Value<I>> $sequence
*
* @return callable(): Value<list<I>>
*/
private function removeHeadElement(bool $mutable, array $sequence): callable
{
$shrinked = $sequence;
\array_shift($shrinked);

if (!($this->predicate)($this->wrap($shrinked))) {
return $this->shrinkValuesWithStrategyA($mutable, $sequence);
}

return $this->strategy($mutable, $shrinked);
if ($mutable) {
return fn(): Value => Value::mutable(
fn() => $this->wrap($shrinked),
$this->shrinkDecoratedWithStrategyA(true, $shrinked),
);
}

return fn(): Value => Value::immutable(
$this->wrap($shrinked),
$this->shrinkDecoratedWithStrategyA(false, $shrinked),
);
}

/**
Expand Down Expand Up @@ -206,7 +317,17 @@ private function shrinkValuesWithStrategyA(bool $mutable, array $sequence): call
return $this->shrinkValuesWithStrategyB($mutable, $sequence);
}

return $this->strategy($mutable, $shrinked);
if ($mutable) {
return fn(): Value => Value::mutable(
fn() => $this->wrap($shrinked),
$this->shrinkDecoratedWithStrategyB(true, $shrinked),
);
}

return fn(): Value => Value::immutable(
$this->wrap($shrinked),
$this->shrinkDecoratedWithStrategyB(false, $shrinked),
);
}

/**
Expand Down Expand Up @@ -239,26 +360,16 @@ private function shrinkValuesWithStrategyB(bool $mutable, array $sequence): call
return $this->identity($mutable, $sequence);
}

return $this->strategy($mutable, $shrinked);
}

/**
* @param list<Value<I>> $sequence
*
* @return callable(): Value<list<I>>
*/
private function strategy(bool $mutable, array $sequence): callable
{
if ($mutable) {
return fn(): Value => Value::mutable(
fn() => $this->wrap($sequence),
$this->shrink(true, $sequence),
fn() => $this->wrap($shrinked),
$this->shrinkEnds(true, $shrinked),
);
}

return fn(): Value => Value::immutable(
$this->wrap($sequence),
$this->shrink(false, $sequence),
$this->wrap($shrinked),
$this->shrinkEnds(false, $shrinked),
);
}

Expand Down
111 changes: 13 additions & 98 deletions src/Set/Strings.php
Expand Up @@ -13,10 +13,8 @@
*/
final class Strings implements Set
{
private int $maxLength;
private int $minLength;
private int $size;
private \Closure $predicate;
/** @var Set<string> */
private Set $set;

public function __construct(int $boundA = null, int $boundB = null)
{
Expand All @@ -25,10 +23,14 @@ public function __construct(int $boundA = null, int $boundB = null)
$boundA ??= 128;
$boundB ??= 0;

$this->maxLength = \max($boundA, $boundB);
$this->minLength = \min($boundA, $boundB);
$this->size = 100;
$this->predicate = fn(string $value): bool => mb_strlen($value) >= $this->minLength && mb_strlen($value) <= $this->maxLength;
/** @psalm-suppress MixedArgumentTypeCoercion */
$this->set = Decorate::immutable(
static fn(array $chars): string => \implode('', $chars),
Sequence::of(
Chars::any(),
Integers::between(\min($boundA, $boundB), \max($boundA, $boundB)),
),
);
}

public static function any(int $maxLength = null): self
Expand Down Expand Up @@ -65,106 +67,19 @@ public static function matching(string $expression): Regex

public function take(int $size): Set
{
$self = clone $this;
$self->size = $size;

return $self;
return $this->set->take($size);
}

public function filter(callable $predicate): Set
{
$previous = $this->predicate;
$self = clone $this;
$self->predicate = static function(string $value) use ($previous, $predicate): bool {
if (!$previous($value)) {
return false;
}

return $predicate($value);
};

return $self;
return $this->set->filter($predicate);
}

/**
* @psalm-suppress MixedReturnTypeCoercion
*/
public function values(Random $rand): \Generator
{
$iterations = 0;

while ($iterations < $this->size) {
$value = '';
$maxLength = $rand($this->minLength + 1, $this->maxLength);

for ($i = 0; $i < $maxLength; $i++) {
$value .= \chr($rand(0, 255));
}

if (!($this->predicate)($value)) {
continue ;
}

yield Value::immutable(
$value,
$this->shrink($value),
);
++$iterations;
}
}

/**
* @return Dichotomy<string>|null
*/
private function shrink(string $value): ?Dichotomy
{
if ($value === '') {
return null;
}

return new Dichotomy(
$this->removeTrailingCharacter($value),
$this->removeLeadingCharacter($value),
);
}

/**
* @return callable(): Value<string>
*/
private function removeTrailingCharacter(string $value): callable
{
/** @var string */
$shrinked = \mb_substr($value, 0, -1, 'ASCII');

if (!($this->predicate)($shrinked)) {
return $this->identity($value);
}

return fn(): Value => Value::immutable($shrinked, $this->shrink($shrinked));
}

/**
* @return callable(): Value<string>
*/
private function removeLeadingCharacter(string $value): callable
{
/** @var string */
$shrinked = \mb_substr($value, 1, null, 'ASCII');

if (!($this->predicate)($shrinked)) {
return $this->identity($value);
}

return fn(): Value => Value::immutable($shrinked, $this->shrink($shrinked));
}

/**
* Non shrinkable as it is alreay the minimum value accepted by the predicate
*
* @return callable(): Value<string>
*/
private function identity(string $value): callable
{
return static fn(): Value => Value::immutable($value);
yield from $this->set->values($rand);
}
}
2 changes: 1 addition & 1 deletion tests/PHPUnit/SeederTest.php
Expand Up @@ -21,7 +21,7 @@ public function testAlwaysReturnADifferentValue()
{
$this
->forAll(Set\Elements::of(
Set\Chars::any(),
Set\Unicode::strings(),
Set\Email::any(),
Set\Strings::any(),
Set\Uuid::any(),
Expand Down
7 changes: 0 additions & 7 deletions tests/Set/CompositeTest.php
Expand Up @@ -337,13 +337,6 @@ function(string ...$args) {
}

$this->assertSame('', $a->unwrap());

$b = $value;
while ($b->shrinkable()) {
$b = $b->shrink()->b();
}

$this->assertSame('', $b->unwrap());
}
}
}
2 changes: 1 addition & 1 deletion tests/Set/SequenceTest.php
Expand Up @@ -238,7 +238,7 @@ public function testSequenceIsNeverShrunkBelowTheSpecifiedLowerBound()

foreach ($sequences->values(new MtRand) as $sequence) {
while ($sequence->shrinkable()) {
$sequence = $sequence->shrink()->b();
$sequence = $sequence->shrink()->a();
}

$this->assertCount(10, $sequence->unwrap());
Expand Down

0 comments on commit f25ccb9

Please sign in to comment.