*/ protected static $sequence = []; /** * Indicates if the instance should sleep. * * @var bool */ protected $shouldSleep = true; /** * Create a new class instance. * * @param int|float|\DateInterval $duration * @return void */ public function __construct($duration) { $this->duration($duration); } /** * Sleep for the given duration. * * @param \DateInterval|int|float $duration * @return static */ public static function for($duration) { return new static($duration); } /** * Sleep until the given timestamp. * * @param \DateTimeInterface|int|float|numeric-string $timestamp * @return static */ public static function until($timestamp) { if (is_numeric($timestamp)) { $timestamp = Carbon::createFromTimestamp($timestamp, date_default_timezone_get()); } return new static(Carbon::now()->diff($timestamp)); } /** * Sleep for the given number of microseconds. * * @param int $duration * @return static */ public static function usleep($duration) { return (new static($duration))->microseconds(); } /** * Sleep for the given number of seconds. * * @param int|float $duration * @return static */ public static function sleep($duration) { return (new static($duration))->seconds(); } /** * Sleep for the given duration. Replaces any previously defined duration. * * @param \DateInterval|int|float $duration * @return $this */ protected function duration($duration) { if (! $duration instanceof DateInterval) { $this->duration = CarbonInterval::microsecond(0); $this->pending = $duration; } else { $duration = CarbonInterval::instance($duration); if ($duration->totalMicroseconds < 0) { $duration = CarbonInterval::seconds(0); } $this->duration = $duration; $this->pending = null; } return $this; } /** * Sleep for the given number of minutes. * * @return $this */ public function minutes() { $this->duration->add('minutes', $this->pullPending()); return $this; } /** * Sleep for one minute. * * @return $this */ public function minute() { return $this->minutes(); } /** * Sleep for the given number of seconds. * * @return $this */ public function seconds() { $this->duration->add('seconds', $this->pullPending()); return $this; } /** * Sleep for one second. * * @return $this */ public function second() { return $this->seconds(); } /** * Sleep for the given number of milliseconds. * * @return $this */ public function milliseconds() { $this->duration->add('milliseconds', $this->pullPending()); return $this; } /** * Sleep for one millisecond. * * @return $this */ public function millisecond() { return $this->milliseconds(); } /** * Sleep for the given number of microseconds. * * @return $this */ public function microseconds() { $this->duration->add('microseconds', $this->pullPending()); return $this; } /** * Sleep for on microsecond. * * @return $this */ public function microsecond() { return $this->microseconds(); } /** * Add additional time to sleep for. * * @param int|float $duration * @return $this */ public function and($duration) { $this->pending = $duration; return $this; } /** * Handle the object's destruction. * * @return void */ public function __destruct() { if (! $this->shouldSleep) { return; } if ($this->pending !== null) { throw new RuntimeException('Unknown duration unit.'); } if (static::$fake) { static::$sequence[] = $this->duration; if (static::$syncWithCarbon) { Carbon::setTestNow(Carbon::now()->add($this->duration)); } foreach (static::$fakeSleepCallbacks as $callback) { $callback($this->duration); } return; } $remaining = $this->duration->copy(); $seconds = (int) $remaining->totalSeconds; if ($seconds > 0) { sleep($seconds); $remaining = $remaining->subSeconds($seconds); } $microseconds = (int) $remaining->totalMicroseconds; if ($microseconds > 0) { usleep($microseconds); } } /** * Resolve the pending duration. * * @return int|float */ protected function pullPending() { if ($this->pending === null) { $this->shouldNotSleep(); throw new RuntimeException('No duration specified.'); } if ($this->pending < 0) { $this->pending = 0; } return tap($this->pending, function () { $this->pending = null; }); } /** * Stay awake and capture any attempts to sleep. * * @param bool $value * @param bool $syncWithCarbon * @return void */ public static function fake($value = true, $syncWithCarbon = false) { static::$fake = $value; static::$sequence = []; static::$fakeSleepCallbacks = []; static::$syncWithCarbon = $syncWithCarbon; } /** * Assert a given amount of sleeping occurred a specific number of times. * * @param \Closure $expected * @param int $times * @return void */ public static function assertSlept($expected, $times = 1) { $count = collect(static::$sequence)->filter($expected)->count(); PHPUnit::assertSame( $times, $count, "The expected sleep was found [{$count}] times instead of [{$times}]." ); } /** * Assert sleeping occurred a given number of times. * * @param int $expected * @return void */ public static function assertSleptTimes($expected) { PHPUnit::assertSame($expected, $count = count(static::$sequence), "Expected [{$expected}] sleeps but found [{$count}]."); } /** * Assert the given sleep sequence was encountered. * * @param array $sequence * @return void */ public static function assertSequence($sequence) { static::assertSleptTimes(count($sequence)); collect($sequence) ->zip(static::$sequence) ->eachSpread(function (?Sleep $expected, CarbonInterval $actual) { if ($expected === null) { return; } PHPUnit::assertTrue( $expected->shouldNotSleep()->duration->equalTo($actual), vsprintf('Expected sleep duration of [%s] but actually slept for [%s].', [ $expected->duration->cascade()->forHumans([ 'options' => 0, 'minimumUnit' => 'microsecond', ]), $actual->cascade()->forHumans([ 'options' => 0, 'minimumUnit' => 'microsecond', ]), ]) ); }); } /** * Assert that no sleeping occurred. * * @return void */ public static function assertNeverSlept() { return static::assertSleptTimes(0); } /** * Assert that no sleeping occurred. * * @return void */ public static function assertInsomniac() { if (static::$sequence === []) { PHPUnit::assertTrue(true); } foreach (static::$sequence as $duration) { PHPUnit::assertSame(0, (int) $duration->totalMicroseconds, vsprintf('Unexpected sleep duration of [%s] found.', [ $duration->cascade()->forHumans([ 'options' => 0, 'minimumUnit' => 'microsecond', ]), ])); } } /** * Indicate that the instance should not sleep. * * @return $this */ protected function shouldNotSleep() { $this->shouldSleep = false; return $this; } /** * Only sleep when the given condition is true. * * @param (\Closure($this): bool)|bool $condition * @return $this */ public function when($condition) { $this->shouldSleep = (bool) value($condition, $this); return $this; } /** * Don't sleep when the given condition is true. * * @param (\Closure($this): bool)|bool $condition * @return $this */ public function unless($condition) { return $this->when(! value($condition, $this)); } /** * Specify a callback that should be invoked when faking sleep within a test. * * @param callable $callback * @return void */ public static function whenFakingSleep($callback) { static::$fakeSleepCallbacks[] = $callback; } /** * Indicate that Carbon's "now" should be kept in sync when sleeping. * * @return void */ public static function syncWithCarbon($value = true) { static::$syncWithCarbon = $value; } }