Skip to content
3 changes: 2 additions & 1 deletion composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,8 @@
},
"require-dev": {
"mockery/mockery": "^1.3.3",
"phpunit/phpunit": ">=8.5.23|^9"
"phpunit/phpunit": ">=8.5.23|^9",
"orchestra/testbench": "^8.0"
},
"autoload": {
"psr-4": {
Expand Down
4 changes: 2 additions & 2 deletions src/Arbiter.php
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,9 @@ public function __construct($id)
};
}

public function handle($exception, $bypassProtections = false)
public function handle($exception, $shouldForcefullyThrowException = false)
{
if ($bypassProtections) {
if ($shouldForcefullyThrowException) {
$this->callHandler($exception);

return;
Expand Down
28 changes: 27 additions & 1 deletion src/Flaky.php
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@

namespace Hammerstone\Flaky;

use Exception;
use Illuminate\Support\Traits\Macroable;
use Throwable;

Expand All @@ -22,6 +23,11 @@ class Flaky

protected $flakyProtectionDisabled = false;

/**
* @var class-string[]|null
*/
protected $flakyExceptions;

public static function make($id)
{
return new static($id);
Expand Down Expand Up @@ -54,7 +60,10 @@ public function run(callable $callable)
$exception = $e;
}

$this->arbiter->handle($exception, $this->protectionsBypassed());
$this->arbiter->handle(
$exception,
$exception && $this->protectionsBypassed() || $this->shouldAlwaysThrowException($exception)
);

return new Result($value, $exception);
}
Expand Down Expand Up @@ -175,6 +184,16 @@ public function allowTotalFailures($failures)
return $this;
}

/**
* @param array<class-string> $exceptions
*/
public function forExceptions(array $exceptions): self
{
$this->flakyExceptions = $exceptions;

return $this;
}

protected function protectionsBypassed()
{
return static::$disabledGlobally || $this->flakyProtectionDisabled;
Expand Down Expand Up @@ -202,4 +221,11 @@ protected function normalizeRetryWhen($when = null)

return $when;
}

protected function shouldAlwaysThrowException(?Exception $exception): bool
{
return ! is_null($exception)
&& ! is_null($this->flakyExceptions)
&& ! in_array(get_class($exception), $this->flakyExceptions, true);
}
}
56 changes: 56 additions & 0 deletions tests/Unit/BasicTest.php
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,44 @@ public function report(Throwable $e)
$this->assertInstanceOf(Exception::class, $handler->reported);
}

/** @test */
public function throws_for_unset_specific_exceptions()
{
$this->expectException(Exception::class);

Carbon::setTestNow();

// We've specified a flaky exception, but we will throw another, so it should throw.
$flaky = Flaky::make(__FUNCTION__)->forExceptions([SpecificException::class])->allowFailuresForSeconds(60);

$result = $flaky->run(function () {
throw new Exception();
});
}

/** @test */
public function does_not_throws_for_specific_exceptions()
{
Carbon::setTestNow();

$flaky = Flaky::make(__FUNCTION__)->forExceptions([SpecificException::class])->allowFailuresForSeconds(60);

// Should not throw, since it is the first occurrence of a defined flaky exception.
$result = $flaky->run(function () {
throw new SpecificException();
});

$this->assertTrue($result->failed);

Carbon::setTestNow(now()->addSeconds(61));

$this->expectException(SpecificException::class);

$flaky->run(function () {
throw new SpecificException();
});
}

/** @test */
public function can_disable()
{
Expand Down Expand Up @@ -185,4 +223,22 @@ public function can_pass_in_our_own_exception()

$this->assertInstanceOf(Result::class, $result);
}

/** @test */
public function it_does_not_throw_for_non_exceptions_when_protections_are_bypassed()
{
$result = Flaky::make(__FUNCTION__)
->allowFailuresForADay()
->disableFlakyProtection()
->run(function () {
return 1;
});

$this->assertEquals(1, $result->value);
}
}

class SpecificException extends \Exception
{

}