Skip to content

Commit

Permalink
Merge branch 'develop'
Browse files Browse the repository at this point in the history
* develop:
  fix workflow declaration
  increase allowed level nesting
  remove test runner trace in github ci
  fix displaying generated values when exception thrown does not match expected one
  fix type comparison
  move responsability to record failure in the printer outside of the test runner
  shrink unexpected exceptions
  remove unused variables
  • Loading branch information
Baptouuuu committed Apr 5, 2020
2 parents fe9e3c2 + 50daac3 commit de36591
Show file tree
Hide file tree
Showing 9 changed files with 415 additions and 38 deletions.
2 changes: 2 additions & 0 deletions .github/workflows/ci.yml
Expand Up @@ -19,6 +19,7 @@ jobs:
php-version: ${{ matrix.php-version }}
extensions: mbstring, intl
coverage: xdebug
ini-values: xdebug.max_nesting_level=2048
- name: Get Composer Cache Directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
Expand Down Expand Up @@ -51,6 +52,7 @@ jobs:
php-version: ${{ matrix.php-version }}
extensions: mbstring, intl
coverage: none
ini-values: xdebug.max_nesting_level=2048
- name: Get Composer Cache Directory
id: composer-cache
run: echo "::set-output name=dir::$(composer config cache-files-dir)"
Expand Down
43 changes: 42 additions & 1 deletion src/PHPUnit/BlackBox.php
Expand Up @@ -4,12 +4,53 @@
namespace Innmind\BlackBox\PHPUnit;

use Innmind\BlackBox\Set;
use PHPUnit\Framework\TestCase;

trait BlackBox
{
protected function forAll(Set $first, Set ...$sets): Scenario
{
$scenario = new Scenario($first, ...$sets);
$expectsException = static fn(\Throwable $e): bool => false;
$recordFailure = function(\Throwable $e, Set\Value $values): void {
ResultPrinterV8::record($this, $e, $values);
};

if ($this instanceof TestCase) {
$expectsException = function(\Throwable $e): bool {
$expectedException = $this->getExpectedException();
$expectedExceptionMessage = $this->getExpectedExceptionMessage();
$expectedExceptionCode = $this->getExpectedExceptionCode();

if (
\is_null($expectedException) &&
\is_null($expectedExceptionMessage) &&
\is_null($expectedExceptionCode)
) {
return false;
}

if (\is_string($expectedException) && !$e instanceof $expectedException) {
return false;
}

if (\is_string($expectedExceptionMessage) && $expectedExceptionMessage !== $e->getMessage()) {
return false;
}

if (!\is_null($expectedExceptionCode) && ((string) $expectedExceptionCode !== (string) $e->getCode())) {
return false;
}

return true;
};
}

$scenario = new Scenario(
$recordFailure,
$expectsException,
$first,
...$sets,
);
$size = \getenv('BLACKBOX_SET_SIZE');

if ($size !== false) {
Expand Down
64 changes: 55 additions & 9 deletions src/PHPUnit/ResultPrinterV8.php
Expand Up @@ -5,7 +5,11 @@

use Innmind\BlackBox\Set\Value;
use PHPUnit\TextUI\ResultPrinter;
use PHPUnit\Framework\TestFailure;
use PHPUnit\Framework\{
TestFailure,
SelfDescribing,
ExceptionWrapper,
};
use Symfony\Component\VarDumper\{
Dumper\CliDumper,
Cloner\VarCloner,
Expand Down Expand Up @@ -38,37 +42,38 @@ public function __construct($out, ...$args)
self::$currentInstance = $this;
}

public static function record(\Throwable $e, Value $value): void
public static function record(object $test, \Throwable $e, Value $value): void
{
if (\is_null(self::$currentInstance)) {
return;
}

$hash = self::$currentInstance->hash($e);
$hash = self::$currentInstance->hash($test, $e);
self::$currentInstance->dataSets[$hash] = $value;
}

protected function printDefect(TestFailure $defect, int $count): void
{
/** @psalm-suppress InternalMethod */
$this->printDefectHeader($defect, $count);
/** @psalm-suppress InternalMethod */
$this->printDataSet($defect->thrownException());
$this->printDataSet($defect);
/** @psalm-suppress InternalMethod */
$this->printDefectTrace($defect);
}

private function printDataSet(\Throwable $e): void
private function printDataSet(TestFailure $defect): void
{
if (!\array_key_exists($this->hash($e), $this->dataSets)) {
$values = $this->findFailingValues($defect);

if (\is_null($values)) {
return;
}

/** @psalm-suppress InternalMethod */
$this->write("Test failing with the following set of values : \n");

/** @psalm-suppress MixedAssignment */
foreach ($this->dataSets[$this->hash($e)]->unwrap() as $value) {
foreach ($values->unwrap() as $value) {
$this->dump($value);
}

Expand All @@ -85,6 +90,10 @@ protected function printDefectTrace(TestFailure $defect): void
$trace,
fn(string $line): bool => \strpos($line, 'innmind/black-box/src/PHPUnit/TestRunner.php') === false,
);
$trace = \array_filter(
$trace,
fn(string $line): bool => \strpos($line, '/home/runner/work/BlackBox/BlackBox/src/PHPUnit/TestRunner.php') === false,
);
$trace = \implode("\n", $trace);

/** @psalm-suppress InternalMethod */
Expand All @@ -102,8 +111,45 @@ private function dump($var): void
$this->dumper->dump($this->cloner->cloneVar($var));
}

private function hash(\Throwable $e): string
private function hash(object $test, \Throwable $e): string
{
if ($test instanceof SelfDescribing) {
return $test->toString();
}

return \spl_object_hash($e);
}

private function findFailingValues(TestFailure $defect): ?Value
{
/** @psalm-suppress InternalMethod */
$test = $defect->failedTest() ?: new \stdClass;
$hash = $this->hash(
$test,
$this->exceptionFrom($defect),
);

if (!\array_key_exists($hash, $this->dataSets)) {
return null;
}

return $this->dataSets[$hash];
}

private function exceptionFrom(TestFailure $defect): \Throwable
{
/** @psalm-suppress InternalMethod */
$thrownException = $defect->thrownException();

if ($thrownException instanceof ExceptionWrapper) {
/** @psalm-suppress InternalMethod */
$originalException = $thrownException->getOriginalException();

if ($originalException instanceof \Throwable) {
return $originalException;
}
}

return $thrownException;
}
}
27 changes: 23 additions & 4 deletions src/PHPUnit/Scenario.php
Expand Up @@ -11,13 +11,23 @@

final class Scenario
{
private \Closure $recordFailure;
private \Closure $expectsException;
private Set $set;
/** @var \Closure(Set): Set */
private \Closure $wrap;
private TestRunner $run;

public function __construct(Set $first , Set ...$sets)
{
/**
* @param callable(\Throwable, Set\Value): void $recordFailure
* @param callable(\Throwable): bool $expectsException
*/
public function __construct(
callable $recordFailure,
callable $expectsException,
Set $first,
Set ...$sets
) {
if (\count($sets) === 0) {
$set = Set\Decorate::immutable(
/** @psalm-suppress MissingParamType */
Expand All @@ -33,9 +43,14 @@ public function __construct(Set $first , Set ...$sets)
);
}

$this->recordFailure = \Closure::fromCallable($recordFailure);
$this->expectsException = \Closure::fromCallable($expectsException);
$this->set = $set->take(100);
$this->wrap = \Closure::fromCallable(static fn(Set $set): Set => new Randomize($set));
$this->run = new TestRunner;
$this->run = new TestRunner(
$recordFailure,
$expectsException,
);
}

public function take(int $size): self
Expand All @@ -53,7 +68,11 @@ public function take(int $size): self
public function disableShrinking(): self
{
$self = clone $this;
$self->run = new TestRunner(true);
$self->run = new TestRunner(
$this->recordFailure,
$this->expectsException,
true,
);

return $self;
}
Expand Down
48 changes: 36 additions & 12 deletions src/PHPUnit/TestRunner.php
Expand Up @@ -8,34 +8,58 @@

final class TestRunner
{
private \Closure $recordFailure;
private \Closure $expectsException;
private bool $shrinkingDisabled;

public function __construct(bool $disableShrinking = false)
{
return $this->shrinkingDisabled = $disableShrinking;
/**
* @param callable(\Throwable): bool $expectsException
*/
public function __construct(
callable $recordFailure,
callable $expectsException,
bool $disableShrinking = false
) {
$this->recordFailure = \Closure::fromCallable($recordFailure);
$this->expectsException = \Closure::fromCallable($expectsException);
$this->shrinkingDisabled = $disableShrinking;
}

public function __invoke(callable $test, Value $values): void
{
try {
$test(...$values->unwrap());
} catch (AssertionFailedError $e) {
if ($this->shrinkingDisabled) {
$this->throw($e, $values);
$this->tryToShrink($test, $values, $e);
} catch (\Throwable $e) {
if (($this->expectsException)($e)) {
throw $e;
}

if ($values->shrinkable()) {
$this->shrink($test, $values, $e);
} else {
$this->throw($e, $values);
}
$this->tryToShrink($test, $values, $e);
}
}

private function tryToShrink(
callable $test,
Value $values,
\Throwable $parentFailure
): void {
if ($this->shrinkingDisabled) {
$this->throw($parentFailure, $values);
}

if ($values->shrinkable()) {
$this->shrink($test, $values, $parentFailure);
}

$this->throw($parentFailure, $values);
}

private function shrink(
callable $test,
Value $values,
AssertionFailedError $parentFailure
\Throwable $parentFailure
): void {
$dichotomy = $values->shrink();

Expand All @@ -50,7 +74,7 @@ private function shrink(

private function throw(\Throwable $e, Value $values): void
{
ResultPrinterV8::record($e, $values);
($this->recordFailure)($e, $values);

throw $e;
}
Expand Down
17 changes: 17 additions & 0 deletions tests/PHPUnit/BlackBoxTest.php
Expand Up @@ -11,6 +11,8 @@

class BlackBoxTest extends TestCase
{
use BlackBox;

public function testTrait()
{
$class = new class() {
Expand All @@ -32,4 +34,19 @@ public function assert(): int
// 200 because it reads the `BLACKBOX_SET_SIZE` env var
$this->assertSame(200, $class->assert());
}

public function testDoesntFailWhenTheExceptionIsExpected()
{
$this
->forAll(Set\Strings::any(), Set\Integers::above(0))
->then(function($message, $code) {
$exception = new class($message, $code) extends \Exception{};

$this->expectException(get_class($exception));
$this->expectExceptionMessage($message);
$this->expectExceptionCode($code);

throw $exception;
});
}
}

0 comments on commit de36591

Please sign in to comment.