├── .github ├── FUNDING.yml └── workflows │ └── main.yml ├── .gitignore ├── .php-cs-fixer.dist.php ├── LICENSE ├── README.md ├── composer.json ├── examples ├── GenericLoop.php ├── Loop.php └── PeriodicLoop.php ├── infection.json5 ├── lib ├── GenericLoop.php ├── Loop.php └── PeriodicLoop.php ├── phpunit.xml.dist ├── psalm-baseline.xml ├── psalm.xml └── test ├── Fixtures.php ├── GenericTest.php ├── Interfaces ├── BasicInterface.php ├── IntervalInterface.php ├── LoggingInterface.php └── LoggingPauseInterface.php ├── LoopTest.php ├── PeriodicTest.php └── Traits ├── Basic.php ├── BasicException.php ├── Logging.php └── LoggingPause.php /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: danog 2 | -------------------------------------------------------------------------------- /.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: build 2 | on: 3 | pull_request: 4 | push: 5 | jobs: 6 | run: 7 | runs-on: ${{ matrix.os }} 8 | strategy: 9 | matrix: 10 | os: [ubuntu-latest] 11 | php-versions: ["8.1", "8.2", "8.3", "8.4"] 12 | name: PHP ${{ matrix.php-versions }} Test on ${{ matrix.os }} 13 | steps: 14 | - name: Checkout 15 | uses: actions/checkout@v2 16 | 17 | - name: Setup PHP 18 | uses: shivammathur/setup-php@v2 19 | with: 20 | php-version: ${{ matrix.php-versions }} 21 | extensions: mbstring, intl, sockets 22 | coverage: xdebug 23 | 24 | - name: Check environment 25 | run: | 26 | php --version 27 | composer --version 28 | 29 | - name: Get composer cache directory 30 | id: composercache 31 | run: echo "::set-output name=dir::$(composer config cache-files-dir)" 32 | 33 | - name: Cache dependencies 34 | uses: actions/cache@v2 35 | with: 36 | path: ${{ steps.composercache.outputs.dir }} 37 | key: ${{ matrix.os }}-composer-${{ matrix.php-versions }}-${{ hashFiles('**/composer.lock') }} 38 | restore-keys: ${{ matrix.os }}-composer-${{ matrix.php-versions }}- 39 | 40 | - name: Install dependencies 41 | run: | 42 | composer install --prefer-dist 43 | wget https://github.com/infection/infection/releases/download/0.27.0/infection.phar -O /usr/local/bin/infection 44 | chmod +x /usr/local/bin/infection 45 | 46 | - name: Run codestyle check 47 | env: 48 | PHP_CS_FIXER_IGNORE_ENV: 1 49 | run: | 50 | vendor/bin/php-cs-fixer --diff --dry-run -v fix 51 | 52 | - name: Run unit tests 53 | run: | 54 | vendor/bin/phpunit --coverage-text --coverage-clover build/logs/clover.xml 55 | 56 | - name: Run mutation tests 57 | env: 58 | STRYKER_DASHBOARD_API_KEY: ${{ secrets.STRYKER_DASHBOARD_API_KEY }} 59 | run: | 60 | infection --show-mutations --threads=$(nproc) 61 | 62 | - name: Run Psalm analysis 63 | run: | 64 | vendor/bin/psalm.phar --shepherd 65 | 66 | - name: Upload coverage to Codecov 67 | env: 68 | OS: ${{ matrix.os }} 69 | PHP: ${{ matrix.php-versions }} 70 | uses: codecov/codecov-action@v1 71 | with: 72 | file: build/logs/clover.xml 73 | env_vars: OS,PHP 74 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | build 3 | composer.lock 4 | phpunit.xml 5 | vendor 6 | .php_cs.cache 7 | coverage 8 | .phpunit* 9 | *.cache 10 | -------------------------------------------------------------------------------- /.php-cs-fixer.dist.php: -------------------------------------------------------------------------------- 1 | true, 8 | ]); 9 | } 10 | }; 11 | 12 | $config->getFinder() 13 | ->in(__DIR__ . '/lib') 14 | ->in(__DIR__ . '/test') 15 | ->in(__DIR__ . '/examples'); 16 | 17 | $cacheDir = getenv('TRAVIS') ? getenv('HOME') . '/.php-cs-fixer' : __DIR__; 18 | 19 | $config->setCacheFile($cacheDir . '/.php_cs.cache'); 20 | 21 | return $config; 22 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2019-2020 Daniil Gentili 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Loop 2 | 3 | [![codecov](https://codecov.io/gh/danog/loop/branch/master/graph/badge.svg)](https://codecov.io/gh/danog/loop) 4 | [![Mutation testing badge](https://img.shields.io/endpoint?style=flat&url=https%3A%2F%2Fbadge-api.stryker-mutator.io%2Fgithub.com%2Fdanog%2Floop%2Fmaster)](https://dashboard.stryker-mutator.io/reports/github.com/danog/loop/master) 5 | [![Psalm coverage](https://shepherd.dev/github/danog/loop/coverage.svg)](https://shepherd.dev/github/danog/loop) 6 | [![Psalm level 1](https://shepherd.dev/github/danog/loop/level.svg)](https://shepherd.dev/github/danog/loop) 7 | ![License](https://img.shields.io/badge/license-MIT-blue.svg) 8 | 9 | `danog/loop` provides a set of powerful async loop APIs based on [amphp](https://amphp.org) for executing operations periodically or on demand, in background loops a-la threads. 10 | 11 | ## Installation 12 | 13 | ```bash 14 | composer require danog/loop 15 | ``` 16 | 17 | ## API 18 | 19 | - Basic 20 | - [GenericLoop](#genericloop) 21 | - [PeriodicLoop](#periodicloop) 22 | - Advanced 23 | - [Loop](#loop) 24 | 25 | ### Loop 26 | 27 | [Class](https://github.com/danog/loop/blob/master/lib/Loop.php) - [Example](https://github.com/danog/loop/blob/master/examples/Loop.php) 28 | 29 | A loop capable of running in background (asynchronously) the code contained in the `loop` function. 30 | Implements pause and resume functionality, and can be stopped from the outside or from the inside. 31 | 32 | API: 33 | 34 | ```php 35 | namespace danog\Loop; 36 | 37 | abstract class Loop 38 | { 39 | /** 40 | * Stop the loop. 41 | */ 42 | public const STOP; 43 | /** 44 | * Pause the loop. 45 | */ 46 | public const PAUSE; 47 | /** 48 | * Rerun the loop. 49 | */ 50 | public const CONTINUE; 51 | 52 | /** 53 | * Loop body. 54 | * 55 | * The return value can be: 56 | * A number - the loop will be paused for the specified number of seconds 57 | * Loop::STOP - The loop will stop 58 | * Loop::PAUSE - The loop will pause forever (or until loop is `resume()`'d 59 | * from outside the loop) 60 | * Loop::CONTINUE - Return this if you want to rerun the loop immediately 61 | * 62 | * The loop can be stopped from the outside by using stop(). 63 | * @return float|Loop::STOP|Loop::PAUSE|Loop::CONTINUE 64 | */ 65 | abstract protected function loop(): ?float; 66 | 67 | /** 68 | * Loop name, useful for logging. 69 | */ 70 | abstract public function __toString(): string; 71 | 72 | /** 73 | * Start the loop. 74 | * 75 | * Returns false if the loop is already running. 76 | */ 77 | public function start(): bool; 78 | /** 79 | * Resume the loop. 80 | * 81 | * If resume is called multiple times, and the event loop hasn't resumed the loop yet, 82 | * the loop will be resumed only once, not N times for every call. 83 | * 84 | * @param bool $postpone If true, multiple resumes will postpone the resuming to the end of the callback queue instead of leaving its position unchanged. 85 | * 86 | * @return bool Returns false if the loop is not paused. 87 | */ 88 | public function resume(bool $postpone = false): bool; 89 | /** 90 | * Stops loop. 91 | * 92 | * Returns false if the loop is not running. 93 | */ 94 | public function stop(): bool; 95 | 96 | /** 97 | * Check whether loop is running. 98 | */ 99 | public function isRunning(): bool; 100 | /** 101 | * Check whether loop is paused. 102 | */ 103 | public function isPaused(): bool; 104 | 105 | /** 106 | * Report pause, can be overriden for logging. 107 | * 108 | * @param float $timeout Pause duration, 0 = forever 109 | */ 110 | protected function reportPause(float $timeout): void; 111 | 112 | /** 113 | * Signal that loop was started. 114 | */ 115 | protected function startedLoop(): void; 116 | /** 117 | * Signal that loop has exited. 118 | */ 119 | protected function exitedLoop(): void; 120 | } 121 | ``` 122 | 123 | ### GenericLoop 124 | 125 | [Class](https://github.com/danog/loop/blob/master/lib/GenericLoop.php) - [Example](https://github.com/danog/loop/blob/master/examples/GenericLoop.php) 126 | 127 | If you want a simpler way to use the `Loop`, you can use the GenericLoop. 128 | 129 | ```php 130 | namespace danog\Loop; 131 | 132 | class GenericLoop extends Loop 133 | { 134 | /** 135 | * Constructor. 136 | * 137 | * The return value of the callable can be: 138 | * * A number - the loop will be paused for the specified number of seconds 139 | * * GenericLoop::STOP - The loop will stop 140 | * * GenericLoop::PAUSE - The loop will pause forever (or until loop is `resume()`'d 141 | * from outside the loop) 142 | * * GenericLoop::CONTINUE - Return this if you want to rerun the loop immediately 143 | * 144 | * If the callable does not return anything, 145 | * the loop will behave is if GenericLoop::PAUSE was returned. 146 | * 147 | * The loop can be stopped from the outside by using stop(). 148 | * 149 | * @param callable(static):?float $callable Callable to run 150 | * @param string $name Loop name 151 | */ 152 | public function __construct(callable $callable, private string $name); 153 | /** 154 | * Get loop name, provided to constructor. 155 | */ 156 | public function __toString(): string; 157 | } 158 | ``` 159 | 160 | ### PeriodicLoop 161 | 162 | [Class](https://github.com/danog/loop/blob/master/lib/PeriodicLoop.php) - [Example](https://github.com/danog/loop/blob/master/examples/PeriodicLoop.php) 163 | 164 | If you simply want to execute an action every N seconds, [PeriodicLoop](https://github.com/danog/MadelineProto/blob/master/src/danog/MadelineProto/Loop/Generic/PeriodicLoop.php) is the way to go. 165 | 166 | ```php 167 | namespace danog\Loop; 168 | 169 | class PeriodicLoop extends GenericLoop 170 | { 171 | /** 172 | * Constructor. 173 | * 174 | * Runs a callback at a periodic interval. 175 | * 176 | * The loop can be stopped from the outside by calling stop() 177 | * and from the inside by returning `true`. 178 | * 179 | * @param callable(static):bool $callback Callable to run 180 | * @param string $name Loop name 181 | * @param ?float $interval Loop interval; if null, pauses indefinitely or until `resume()` is called. 182 | */ 183 | public function __construct(callable $callback, string $name, ?float $interval) 184 | /** 185 | * Get name of the loop, passed to the constructor. 186 | * 187 | * @return string 188 | */ 189 | public function __toString(): string; 190 | } 191 | ``` 192 | -------------------------------------------------------------------------------- /composer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "danog/loop", 3 | "description": "Loop abstraction for AMPHP.", 4 | "keywords": [ 5 | "asynchronous", 6 | "async", 7 | "concurrent", 8 | "multi-threading", 9 | "multi-processing" 10 | ], 11 | "homepage": "https://github.com/danog/loop", 12 | "license": "MIT", 13 | "authors": [ 14 | { 15 | "name": "Daniil Gentili", 16 | "email": "daniil@daniil.it" 17 | } 18 | ], 19 | "require": { 20 | "php": ">=8.1", 21 | "amphp/amp": "^3", 22 | "symfony/polyfill-php83": "*" 23 | }, 24 | "require-dev": { 25 | "phpunit/phpunit": "^9", 26 | "amphp/phpunit-util": "^3", 27 | "amphp/php-cs-fixer-config": "^2", 28 | "vimeo/psalm": "^6" 29 | }, 30 | "autoload": { 31 | "psr-4": { 32 | "danog\\Loop\\": "lib" 33 | } 34 | }, 35 | "autoload-dev": { 36 | "psr-4": { 37 | "danog\\Loop\\Test\\": "test" 38 | } 39 | }, 40 | "scripts": { 41 | "check": [ 42 | "@cs", 43 | "@test" 44 | ], 45 | "cs": "php-cs-fixer fix -v --diff --dry-run", 46 | "cs-fix": "php-cs-fixer fix -v --diff", 47 | "test": "phpdbg -qrr -dzend.assertions=1 -dassert.exception=1 ./vendor/bin/phpunit --coverage-text" 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /examples/GenericLoop.php: -------------------------------------------------------------------------------- 1 | start(); 20 | delay(0.1); 21 | $loops []= $loop; 22 | } 23 | delay(5); 24 | echo "Resuming prematurely all loops!".PHP_EOL; 25 | foreach ($loops as $loop) { 26 | $loop->resume(); 27 | } 28 | echo "OK done, waiting 5 more seconds!".PHP_EOL; 29 | delay(5); 30 | echo "Closing all loops!".PHP_EOL; 31 | delay(0.01); 32 | -------------------------------------------------------------------------------- /examples/Loop.php: -------------------------------------------------------------------------------- 1 | number}".PHP_EOL; 21 | $this->number++; 22 | return $this->number < 10 ? 1.0 : GenericLoop::STOP; 23 | } 24 | 25 | public function __toString(): string 26 | { 27 | return $this->name; 28 | } 29 | } 30 | 31 | /** @var MyLoop[] */ 32 | $loops = []; 33 | for ($x = 0; $x < 10; $x++) { 34 | $loop = new MyLoop("Loop number $x"); 35 | $loop->start(); 36 | delay(0.1); 37 | $loops []= $loop; 38 | } 39 | delay(5); 40 | echo "Resuming prematurely all loops!".PHP_EOL; 41 | foreach ($loops as $loop) { 42 | $loop->resume(); 43 | } 44 | echo "OK done, waiting 5 more seconds!".PHP_EOL; 45 | delay(5); 46 | echo "Closing all loops!".PHP_EOL; 47 | delay(0.01); 48 | -------------------------------------------------------------------------------- /examples/PeriodicLoop.php: -------------------------------------------------------------------------------- 1 | start(); 20 | delay(0.1); 21 | $loops []= $loop; 22 | } 23 | delay(5); 24 | echo "Resuming prematurely all loops!".PHP_EOL; 25 | foreach ($loops as $loop) { 26 | $loop->resume(); 27 | } 28 | echo "OK done, waiting 5 more seconds!".PHP_EOL; 29 | delay(5); 30 | echo "Closing all loops!".PHP_EOL; 31 | delay(0.01); 32 | -------------------------------------------------------------------------------- /infection.json5: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://raw.githubusercontent.com/infection/infection/0.27.3/resources/schema.json", 3 | "source": { 4 | "directories": [ 5 | "lib" 6 | ] 7 | }, 8 | "logs": { 9 | "stryker": { 10 | "report": "master" 11 | } 12 | }, 13 | "mutators": { 14 | "@default": true 15 | } 16 | } -------------------------------------------------------------------------------- /lib/GenericLoop.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016-2020 Daniil Gentili 8 | * @license https://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | namespace danog\Loop; 12 | 13 | /** 14 | * Generic loop, runs single callable. 15 | * 16 | * @api 17 | * 18 | * @author Daniil Gentili 19 | */ 20 | class GenericLoop extends Loop 21 | { 22 | /** 23 | * Callable. 24 | * 25 | * @var callable(static):?float 26 | */ 27 | private $callable; 28 | /** 29 | * Constructor. 30 | * 31 | * The return value of the callable can be: 32 | * * A number - the loop will be paused for the specified number of seconds 33 | * * GenericLoop::STOP - The loop will stop 34 | * * GenericLoop::PAUSE - The loop will pause forever (or until loop is `resumed()` 35 | * from outside the loop) 36 | * * GenericLoop::CONTINUE - Return this if you want to rerun the loop immediately 37 | * 38 | * If the callable does not return anything, 39 | * the loop will behave is if GenericLoop::PAUSE was returned. 40 | * 41 | * The callable will be passed the instance of the current loop. 42 | * 43 | * The loop can be stopped from the outside by using stop(). 44 | * 45 | * @param callable(static):?float $callable Callable to run 46 | * @param string $name Loop name 47 | */ 48 | public function __construct(callable $callable, private string $name) 49 | { 50 | $this->callable = $callable; 51 | } 52 | 53 | #[\Override] 54 | protected function loop(): ?float 55 | { 56 | return ($this->callable)($this); 57 | } 58 | public function __toString(): string 59 | { 60 | return $this->name; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /lib/Loop.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016-2020 Daniil Gentili 8 | * @license https://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | namespace danog\Loop; 12 | 13 | use Amp\DeferredFuture; 14 | use AssertionError; 15 | use Revolt\EventLoop; 16 | use Stringable; 17 | 18 | /** 19 | * Generic loop, runs single callable. 20 | * 21 | * @api 22 | * 23 | * @author Daniil Gentili 24 | */ 25 | abstract class Loop implements Stringable 26 | { 27 | /** 28 | * Stop the loop. 29 | */ 30 | public const STOP = -1.0; 31 | /** 32 | * Pause the loop. 33 | */ 34 | public const PAUSE = null; 35 | /** 36 | * Rerun the loop. 37 | */ 38 | public const CONTINUE = 0.0; 39 | /** 40 | * Whether the loop is running. 41 | */ 42 | private bool $running = false; 43 | /** 44 | * Resume timer ID. 45 | */ 46 | private ?string $resumeTimer = null; 47 | /** 48 | * Resume deferred ID. 49 | */ 50 | private ?string $resumeImmediate = null; 51 | /** 52 | * Shutdown deferred. 53 | */ 54 | private ?DeferredFuture $shutdownDeferred = null; 55 | 56 | /** 57 | * Report pause, can be overriden for logging. 58 | * 59 | * @psalm-suppress PossiblyUnusedParam 60 | * 61 | * @param float $timeout Pause duration, 0 = forever 62 | */ 63 | protected function reportPause(float $timeout): void 64 | { 65 | } 66 | 67 | /** 68 | * Start the loop. 69 | * 70 | * Returns false if the loop is already running. 71 | */ 72 | public function start(): bool 73 | { 74 | while ($this->shutdownDeferred !== null) { 75 | $this->shutdownDeferred->getFuture()->await(); 76 | } 77 | if ($this->running) { 78 | return false; 79 | } 80 | $this->running = true; 81 | /** @infection-ignore-all */ 82 | if (!$this->resume()) { 83 | // @codeCoverageIgnoreStart 84 | throw new AssertionError("Could not resume!"); 85 | // @codeCoverageIgnoreEnd 86 | } 87 | $this->startedLoop(); 88 | return true; 89 | } 90 | /** 91 | * Stops loop. 92 | * 93 | * Returns false if the loop is not running. 94 | */ 95 | public function stop(): bool 96 | { 97 | if (!$this->running) { 98 | return false; 99 | } 100 | $this->running = false; 101 | if ($this->resumeTimer !== null) { 102 | $storedWatcherId = $this->resumeTimer; 103 | EventLoop::cancel($storedWatcherId); 104 | $this->resumeTimer = null; 105 | } 106 | if ($this->resumeImmediate !== null) { 107 | $storedWatcherId = $this->resumeImmediate; 108 | EventLoop::cancel($storedWatcherId); 109 | $this->resumeImmediate = null; 110 | } 111 | if ($this->paused) { 112 | $this->exitedLoop(); 113 | } else { 114 | /** @infection-ignore-all */ 115 | if ($this->shutdownDeferred !== null) { 116 | // @codeCoverageIgnoreStart 117 | throw new AssertionError("Shutdown deferred is not null!"); 118 | // @codeCoverageIgnoreEnd 119 | } 120 | $this->shutdownDeferred = new DeferredFuture; 121 | } 122 | return true; 123 | } 124 | abstract protected function loop(): ?float; 125 | 126 | private bool $paused = true; 127 | private function loopInternal(): void 128 | { 129 | /** @infection-ignore-all */ 130 | if (!$this->running) { 131 | // @codeCoverageIgnoreStart 132 | throw new AssertionError("Already running!"); 133 | // @codeCoverageIgnoreEnd 134 | } 135 | /** @infection-ignore-all */ 136 | if (!$this->paused) { 137 | // @codeCoverageIgnoreStart 138 | throw new AssertionError("Already paused!"); 139 | // @codeCoverageIgnoreEnd 140 | } 141 | $this->paused = false; 142 | try { 143 | $timeout = $this->loop(); 144 | } catch (\Throwable $e) { 145 | $this->exitedLoopInternal(); 146 | throw $e; 147 | } 148 | /** @var bool $this->running */ 149 | if (!$this->running) { 150 | $this->exitedLoopInternal(); 151 | return; 152 | } 153 | if ($timeout === self::STOP) { 154 | $this->exitedLoopInternal(); 155 | return; 156 | } 157 | 158 | $this->paused = true; 159 | if ($timeout === self::PAUSE) { 160 | $this->reportPause(0.0); 161 | } else { 162 | if ($this->resumeImmediate === null) { 163 | /** @infection-ignore-all */ 164 | if ($this->resumeTimer !== null) { 165 | // @codeCoverageIgnoreStart 166 | throw new AssertionError("Already have a resume timer!"); 167 | // @codeCoverageIgnoreEnd 168 | } 169 | $this->resumeTimer = EventLoop::delay($timeout, function (): void { 170 | $this->resumeTimer = null; 171 | $this->loopInternal(); 172 | }); 173 | } 174 | if ($timeout !== self::CONTINUE) { 175 | $this->reportPause($timeout); 176 | } 177 | } 178 | } 179 | 180 | private function exitedLoopInternal(): void 181 | { 182 | $this->running = false; 183 | $this->paused = true; 184 | /** @infection-ignore-all */ 185 | if ($this->resumeTimer !== null) { 186 | // @codeCoverageIgnoreStart 187 | throw new AssertionError("Already have a resume timer!"); 188 | // @codeCoverageIgnoreEnd 189 | } 190 | /** @infection-ignore-all */ 191 | if ($this->resumeImmediate !== null) { 192 | // @codeCoverageIgnoreStart 193 | throw new AssertionError("Already have a resume immediate timer!"); 194 | // @codeCoverageIgnoreEnd 195 | } 196 | $this->exitedLoop(); 197 | if ($this->shutdownDeferred !== null) { 198 | $d = $this->shutdownDeferred; 199 | $this->shutdownDeferred = null; 200 | EventLoop::queue($d->complete(...)); 201 | } 202 | } 203 | /** 204 | * Signal that loop was started. 205 | */ 206 | protected function startedLoop(): void 207 | { 208 | } 209 | /** 210 | * Signal that loop has exited. 211 | */ 212 | protected function exitedLoop(): void 213 | { 214 | } 215 | /** 216 | * Check whether loop is running. 217 | */ 218 | public function isRunning(): bool 219 | { 220 | return $this->running; 221 | } 222 | /** 223 | * Check whether loop is paused (different from isRunning, a loop may be running but paused). 224 | */ 225 | public function isPaused(): bool 226 | { 227 | return $this->paused; 228 | } 229 | 230 | /** 231 | * Resume the loop. 232 | * 233 | * If resume is called multiple times, and the event loop hasn't resumed the loop yet, 234 | * the loop will be resumed only once, not N times for every call. 235 | * 236 | * @param bool $postpone If true, multiple resumes will postpone the resuming to the end of the callback queue instead of leaving its position unchanged. 237 | * 238 | * @return bool Returns false if the loop is not paused. 239 | */ 240 | public function resume(bool $postpone = false): bool 241 | { 242 | if ($this->running && $this->paused) { 243 | if ($this->resumeImmediate !== null) { 244 | if (!$postpone) { 245 | return true; 246 | } 247 | $resumeImmediate = $this->resumeImmediate; 248 | $this->resumeImmediate = null; 249 | EventLoop::cancel($resumeImmediate); 250 | } 251 | if ($this->resumeTimer !== null) { 252 | $timer = $this->resumeTimer; 253 | $this->resumeTimer = null; 254 | EventLoop::cancel($timer); 255 | } 256 | $this->resumeImmediate = EventLoop::defer(function (): void { 257 | $this->resumeImmediate = null; 258 | $this->loopInternal(); 259 | }); 260 | return true; 261 | } 262 | return false; 263 | } 264 | } 265 | -------------------------------------------------------------------------------- /lib/PeriodicLoop.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2016-2020 Daniil Gentili 7 | * @license https://opensource.org/licenses/MIT MIT 8 | */ 9 | 10 | namespace danog\Loop; 11 | 12 | /** 13 | * Periodic loop. 14 | * 15 | * @api 16 | * 17 | * @author Daniil Gentili 18 | */ 19 | class PeriodicLoop extends GenericLoop 20 | { 21 | /** 22 | * Constructor. 23 | * 24 | * Runs a callback at a periodic interval. 25 | * 26 | * The callable will be passed the instance of the current loop. 27 | * 28 | * The loop can be stopped from the outside by calling stop() 29 | * and from the inside by returning `true`. 30 | * 31 | * @param callable(static):bool $callback Callable to run 32 | * @param string $name Loop name 33 | * @param ?float $interval Loop interval; if null, pauses indefinitely or until `resume()` is called. 34 | */ 35 | public function __construct(callable $callback, string $name, ?float $interval) 36 | { 37 | /** @psalm-suppress InvalidArgument */ 38 | parent::__construct( 39 | /** @param static $loop */ 40 | static function (self $loop) use ($callback, $interval): ?float { 41 | if ($callback($loop) === true) { 42 | return GenericLoop::STOP; 43 | } 44 | return $interval; 45 | }, 46 | $name 47 | ); 48 | } 49 | } 50 | -------------------------------------------------------------------------------- /phpunit.xml.dist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | lib 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | test 14 | 15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /psalm-baseline.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /psalm.xml: -------------------------------------------------------------------------------- 1 | 2 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /test/Fixtures.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2016-2020 Daniil Gentili 7 | * @license https://opensource.org/licenses/MIT MIT 8 | */ 9 | 10 | namespace danog\Loop\Test; 11 | 12 | use PHPUnit\Framework\TestCase; 13 | use Revolt\EventLoop; 14 | 15 | /** 16 | * Fixtures. 17 | */ 18 | abstract class Fixtures extends TestCase 19 | { 20 | const LOOP_NAME = 'TTTT'; 21 | protected static function waitTick(): void 22 | { 23 | $f = new \Amp\DeferredFuture; 24 | \Revolt\EventLoop::defer(fn () => $f->complete()); 25 | $f->getFuture()->await(); 26 | } 27 | protected function setUp(): void 28 | { 29 | EventLoop::run(); 30 | } 31 | protected function tearDown(): void 32 | { 33 | EventLoop::run(); 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /test/GenericTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2016-2020 Daniil Gentili 7 | * @license https://opensource.org/licenses/MIT MIT 8 | */ 9 | 10 | namespace danog\Loop\Test; 11 | 12 | use danog\Loop\GenericLoop; 13 | use danog\Loop\Loop; 14 | use danog\Loop\Test\Interfaces\LoggingPauseInterface; 15 | use danog\Loop\Test\Traits\Basic; 16 | use danog\Loop\Test\Traits\LoggingPause; 17 | use Revolt\EventLoop; 18 | 19 | use function Amp\delay; 20 | 21 | class GenericTest extends Fixtures 22 | { 23 | /** 24 | * Test basic loop. 25 | * 26 | * @param bool $stopSig Whether to stop with signal 27 | * 28 | * 29 | * 30 | * @dataProvider provideTrueFalse 31 | */ 32 | public function testGeneric(bool $stopSig): void 33 | { 34 | $runCount = 0; 35 | $pauseTime = GenericLoop::PAUSE; 36 | $callable = function (GenericLoop $genericLoop) use (&$runCount, &$pauseTime, &$l) { 37 | $l = $genericLoop; 38 | $runCount++; 39 | return $pauseTime; 40 | }; 41 | $this->fixtureAssertions($callable, $runCount, $pauseTime, $stopSig, $l); 42 | $obj = new class() { 43 | public $pauseTime = GenericLoop::PAUSE; 44 | public $runCount = 0; 45 | public ?GenericLoop $loop; 46 | public function run(GenericLoop $loop) 47 | { 48 | $this->loop = $loop; 49 | $this->runCount++; 50 | return $this->pauseTime; 51 | } 52 | }; 53 | $this->fixtureAssertions([$obj, 'run'], $obj->runCount, $obj->pauseTime, $stopSig, $obj->loop); 54 | $obj = new class() { 55 | public $pauseTime = GenericLoop::PAUSE; 56 | public $runCount = 0; 57 | public ?GenericLoop $loop; 58 | public function run(GenericLoop $loop) 59 | { 60 | $this->loop = $loop; 61 | $this->runCount++; 62 | return $this->pauseTime; 63 | } 64 | }; 65 | $this->fixtureAssertions(\Closure::fromCallable([$obj, 'run']), $obj->runCount, $obj->pauseTime, $stopSig, $obj->loop); 66 | } 67 | /** 68 | * Test generator loop. 69 | * 70 | * @param bool $stopSig Whether to stop with signal 71 | * 72 | * 73 | * 74 | * @dataProvider provideTrueFalse 75 | */ 76 | public function testGenerator(bool $stopSig): void 77 | { 78 | $runCount = 0; 79 | $pauseTime = GenericLoop::PAUSE; 80 | $callable = function (GenericLoop $loop) use (&$runCount, &$pauseTime, &$l) { 81 | $l = $loop; 82 | $runCount++; 83 | return $pauseTime; 84 | }; 85 | $this->fixtureAssertions($callable, $runCount, $pauseTime, $stopSig, $l); 86 | $obj = new class() { 87 | public $pauseTime = GenericLoop::PAUSE; 88 | public $runCount = 0; 89 | public ?GenericLoop $loop; 90 | public function run(GenericLoop $loop) 91 | { 92 | $this->loop = $loop; 93 | $this->runCount++; 94 | return $this->pauseTime; 95 | } 96 | }; 97 | $this->fixtureAssertions([$obj, 'run'], $obj->runCount, $obj->pauseTime, $stopSig, $obj->loop); 98 | $obj = new class() { 99 | public $pauseTime = GenericLoop::PAUSE; 100 | public $runCount = 0; 101 | public ?GenericLoop $loop; 102 | public function run(GenericLoop $loop) 103 | { 104 | $this->loop = $loop; 105 | $this->runCount++; 106 | return $this->pauseTime; 107 | } 108 | }; 109 | $this->fixtureAssertions(\Closure::fromCallable([$obj, 'run']), $obj->runCount, $obj->pauseTime, $stopSig, $obj->loop); 110 | } 111 | /** 112 | * Fixture assertions for started loop. 113 | */ 114 | private function fixtureStarted(Loop&LoggingPauseInterface $loop, int $offset = 1): void 115 | { 116 | $this->assertTrue($loop->isRunning()); 117 | $this->assertEquals($offset, $loop->startCounter()); 118 | $this->assertEquals($offset-1, $loop->endCounter()); 119 | } 120 | /** 121 | * Run fixture assertions. 122 | * 123 | * @param callable $closure Closure 124 | * @param integer $runCount Run count 125 | * @param ?float $pauseTime Pause time 126 | * @param bool $stopSig Whether to stop with signal 127 | */ 128 | private function fixtureAssertions(callable $closure, int &$runCount, ?float &$pauseTime, bool $stopSig, ?GenericLoop &$l): void 129 | { 130 | $loop = new class($closure, Fixtures::LOOP_NAME) extends GenericLoop implements LoggingPauseInterface { 131 | use LoggingPause; 132 | }; 133 | $expectedRunCount = 0; 134 | 135 | $this->assertEquals(Fixtures::LOOP_NAME, "$loop"); 136 | 137 | $this->assertFalse($loop->isRunning()); 138 | $this->assertEquals(0, $loop->startCounter()); 139 | $this->assertEquals(0, $loop->endCounter()); 140 | 141 | $this->assertEquals($expectedRunCount, $runCount); 142 | $this->assertEquals(0, $loop->getPauseCount()); 143 | 144 | $this->assertTrue($loop->start()); 145 | self::waitTick(); 146 | $this->fixtureStarted($loop); 147 | $expectedRunCount++; 148 | $this->assertEquals($loop, $l); 149 | 150 | $this->assertEquals($expectedRunCount, $runCount); 151 | $this->assertEquals(1, $loop->getPauseCount()); 152 | $this->assertEquals(0, $loop->getLastPause()); 153 | $this->assertTrue($loop->isPaused()); 154 | 155 | $pauseTime = 0.1; 156 | $this->assertTrue($loop->resume()); 157 | self::waitTick(); 158 | $this->fixtureStarted($loop); 159 | $expectedRunCount++; 160 | 161 | $this->assertEquals($expectedRunCount, $runCount); 162 | $this->assertEquals(2, $loop->getPauseCount()); 163 | $this->assertEquals(0.1, $loop->getLastPause()); 164 | $this->assertTrue($loop->isPaused()); 165 | 166 | delay(0.048); 167 | $this->fixtureStarted($loop); 168 | 169 | $this->assertEquals($expectedRunCount, $runCount); 170 | $this->assertEquals(2, $loop->getPauseCount()); 171 | $this->assertEquals(0.1, $loop->getLastPause()); 172 | $this->assertTrue($loop->isPaused()); 173 | 174 | delay(0.060); 175 | $this->fixtureStarted($loop); 176 | $expectedRunCount++; 177 | 178 | $this->assertEquals($expectedRunCount, $runCount); 179 | $this->assertEquals(3, $loop->getPauseCount()); 180 | $this->assertEquals(0.1, $loop->getLastPause()); 181 | $this->assertTrue($loop->isPaused()); 182 | 183 | $this->assertTrue($loop->resume()); 184 | self::waitTick(); 185 | $expectedRunCount++; 186 | 187 | $this->assertEquals($expectedRunCount, $runCount); 188 | $this->assertEquals(4, $loop->getPauseCount()); 189 | $this->assertEquals(0.1, $loop->getLastPause()); 190 | $this->assertTrue($loop->isPaused()); 191 | 192 | if ($stopSig) { 193 | $this->assertTrue($loop->stop()); 194 | } else { 195 | $pauseTime = GenericLoop::STOP; 196 | $this->assertTrue($loop->resume()); 197 | $expectedRunCount++; 198 | } 199 | self::waitTick(); 200 | $this->assertEquals($expectedRunCount, $runCount); 201 | $this->assertEquals(4, $loop->getPauseCount()); 202 | $this->assertEquals(0.1, $loop->getLastPause()); 203 | $this->assertTrue($loop->isPaused()); 204 | 205 | $this->assertEquals(1, $loop->startCounter()); 206 | $this->assertEquals(1, $loop->endCounter()); 207 | 208 | $this->assertFalse($loop->isRunning()); 209 | $this->assertFalse($loop->stop()); 210 | $this->assertFalse($loop->resume()); 211 | 212 | // Restart loop 213 | $pauseTime = GenericLoop::PAUSE; 214 | $this->assertTrue($loop->start()); 215 | self::waitTick(); 216 | $this->fixtureStarted($loop, 2); 217 | $expectedRunCount++; 218 | 219 | $this->assertEquals($expectedRunCount, $runCount); 220 | $this->assertEquals(5, $loop->getPauseCount()); 221 | $this->assertEquals(0.0, $loop->getLastPause()); 222 | $this->assertTrue($loop->isPaused()); 223 | 224 | if ($stopSig) { 225 | $this->assertTrue($loop->stop()); 226 | } else { 227 | $pauseTime = GenericLoop::STOP; 228 | $this->assertTrue($loop->resume()); 229 | $expectedRunCount++; 230 | } 231 | self::waitTick(); 232 | $this->assertEquals($expectedRunCount, $runCount); 233 | $this->assertEquals(5, $loop->getPauseCount()); 234 | $this->assertEquals(0.0, $loop->getLastPause()); 235 | $this->assertTrue($loop->isPaused()); 236 | 237 | $this->assertEquals(2, $loop->startCounter()); 238 | $this->assertEquals(2, $loop->endCounter()); 239 | 240 | $this->assertFalse($loop->isRunning()); 241 | $this->assertFalse($loop->stop()); 242 | $this->assertFalse($loop->resume()); 243 | 244 | // Restart loop and stop it immediately 245 | $pauseTime = GenericLoop::PAUSE; 246 | $this->assertTrue($loop->start()); 247 | $this->assertTrue($loop->stop()); 248 | self::waitTick(); 249 | 250 | $this->assertEquals($expectedRunCount, $runCount); 251 | $this->assertEquals(5, $loop->getPauseCount()); 252 | $this->assertEquals(0.0, $loop->getLastPause()); 253 | $this->assertTrue($loop->isPaused()); 254 | 255 | $this->assertEquals(3, $loop->startCounter()); 256 | $this->assertEquals(3, $loop->endCounter()); 257 | 258 | $this->assertFalse($loop->isRunning()); 259 | $this->assertFalse($loop->stop()); 260 | $this->assertFalse($loop->resume()); 261 | 262 | // Restart loop with delay and stop it immediately 263 | $pauseTime = 1.0; 264 | $this->assertTrue($loop->start()); 265 | $this->assertTrue($loop->stop()); 266 | self::waitTick(); 267 | 268 | $this->assertEquals($expectedRunCount, $runCount); 269 | $this->assertEquals(5, $loop->getPauseCount()); 270 | $this->assertEquals(0.0, $loop->getLastPause()); 271 | $this->assertTrue($loop->isPaused()); 272 | 273 | $this->assertEquals(4, $loop->startCounter()); 274 | $this->assertEquals(4, $loop->endCounter()); 275 | 276 | $this->assertFalse($loop->isRunning()); 277 | $this->assertFalse($loop->stop()); 278 | $this->assertFalse($loop->resume()); 279 | 280 | // Restart loop, without postponing resuming 281 | $pauseTime = GenericLoop::PAUSE; 282 | $this->assertTrue($loop->start()); 283 | self::waitTick(); 284 | $this->fixtureStarted($loop, 5); 285 | $expectedRunCount++; 286 | 287 | $this->assertEquals($expectedRunCount, $runCount); 288 | $this->assertEquals(6, $loop->getPauseCount()); 289 | $this->assertEquals(0.0, $loop->getLastPause()); 290 | $this->assertTrue($loop->isPaused()); 291 | 292 | $pauseTime = GenericLoop::STOP; 293 | $this->assertTrue($loop->resume()); 294 | EventLoop::queue(fn () => $this->assertTrue($loop->resume())); 295 | $expectedRunCount++; 296 | self::waitTick(); 297 | $this->assertEquals($expectedRunCount, $runCount); 298 | $this->assertEquals(6, $loop->getPauseCount()); 299 | $this->assertEquals(0.0, $loop->getLastPause()); 300 | $this->assertTrue($loop->isPaused()); 301 | 302 | $this->assertEquals(5, $loop->startCounter()); 303 | $this->assertEquals(5, $loop->endCounter()); 304 | 305 | $this->assertFalse($loop->isRunning()); 306 | $this->assertFalse($loop->stop()); 307 | $this->assertFalse($loop->resume()); 308 | 309 | // Restart loop, postponing resuming 310 | $pauseTime = GenericLoop::PAUSE; 311 | $this->assertTrue($loop->start()); 312 | self::waitTick(); 313 | $this->fixtureStarted($loop, 6); 314 | $expectedRunCount++; 315 | 316 | $this->assertEquals($expectedRunCount, $runCount); 317 | $this->assertEquals(7, $loop->getPauseCount()); 318 | $this->assertEquals(0.0, $loop->getLastPause()); 319 | $this->assertTrue($loop->isPaused()); 320 | 321 | $pauseTime = GenericLoop::STOP; 322 | $this->assertTrue($loop->resume(true)); 323 | EventLoop::queue(fn () => $this->assertTrue($loop->resume(true))); 324 | self::waitTick(); 325 | $this->assertEquals($expectedRunCount, $runCount); 326 | $this->assertEquals(7, $loop->getPauseCount()); 327 | $this->assertEquals(0.0, $loop->getLastPause()); 328 | $this->assertTrue($loop->isPaused()); 329 | 330 | $this->assertEquals(6, $loop->startCounter()); 331 | $this->assertEquals(5, $loop->endCounter()); 332 | 333 | self::waitTick(); 334 | $expectedRunCount++; 335 | $this->assertEquals($expectedRunCount, $runCount); 336 | $this->assertEquals(7, $loop->getPauseCount()); 337 | $this->assertEquals(0.0, $loop->getLastPause()); 338 | $this->assertTrue($loop->isPaused()); 339 | 340 | $this->assertEquals(6, $loop->startCounter()); 341 | $this->assertEquals(6, $loop->endCounter()); 342 | } 343 | 344 | /** 345 | * Provide true false. 346 | * 347 | */ 348 | public function provideTrueFalse(): array 349 | { 350 | return [ 351 | [true], 352 | [false] 353 | ]; 354 | } 355 | } 356 | -------------------------------------------------------------------------------- /test/Interfaces/BasicInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016-2020 Daniil Gentili 8 | * @license https://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | namespace danog\Loop\Test\Interfaces; 12 | 13 | /** 14 | * Basic loop test interface. 15 | * 16 | * @author Daniil Gentili 17 | */ 18 | interface BasicInterface extends LoggingInterface 19 | { 20 | /** 21 | * Check whether the loop inited. 22 | */ 23 | public function inited(): bool; 24 | /** 25 | * Check whether the loop ran. 26 | */ 27 | public function ran(): bool; 28 | } 29 | -------------------------------------------------------------------------------- /test/Interfaces/IntervalInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016-2020 Daniil Gentili 8 | * @license https://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | namespace danog\Loop\Test\Interfaces; 12 | 13 | /** 14 | * Resumable loop test interface. 15 | * 16 | * @author Daniil Gentili 17 | */ 18 | interface IntervalInterface 19 | { 20 | /** 21 | * Set sleep interval. 22 | * 23 | * @param ?int $interval Interval 24 | * 25 | */ 26 | public function setInterval(?int $interval): void; 27 | } 28 | -------------------------------------------------------------------------------- /test/Interfaces/LoggingInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016-2020 Daniil Gentili 8 | * @license https://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | namespace danog\Loop\Test\Interfaces; 12 | 13 | /** 14 | * Basic loop test interface. 15 | * 16 | * @author Daniil Gentili 17 | */ 18 | interface LoggingInterface 19 | { 20 | /** 21 | * Get start counter. 22 | */ 23 | public function startCounter(): int; 24 | /** 25 | * Get end counter. 26 | */ 27 | public function endCounter(): int; 28 | } 29 | -------------------------------------------------------------------------------- /test/Interfaces/LoggingPauseInterface.php: -------------------------------------------------------------------------------- 1 | 7 | * @copyright 2016-2020 Daniil Gentili 8 | * @license https://opensource.org/licenses/MIT MIT 9 | */ 10 | 11 | namespace danog\Loop\Test\Interfaces; 12 | 13 | /** 14 | * Basic loop test interface. 15 | * 16 | * @author Daniil Gentili 17 | */ 18 | interface LoggingPauseInterface extends LoggingInterface 19 | { 20 | /** 21 | * Get number of times loop was paused. 22 | */ 23 | public function getPauseCount(): int; 24 | /** 25 | * Get last pause. 26 | */ 27 | public function getLastPause(): float; 28 | } 29 | -------------------------------------------------------------------------------- /test/LoopTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2016-2020 Daniil Gentili 7 | * @license https://opensource.org/licenses/MIT MIT 8 | */ 9 | 10 | namespace danog\Loop\Test; 11 | 12 | use danog\Loop\Loop; 13 | use danog\Loop\Test\Interfaces\BasicInterface; 14 | use danog\Loop\Test\Traits\Basic; 15 | use danog\Loop\Test\Traits\BasicException; 16 | use Revolt\EventLoop; 17 | use RuntimeException; 18 | 19 | use function Amp\delay; 20 | 21 | class LoopTest extends Fixtures 22 | { 23 | /** 24 | * Execute pre-start assertions. 25 | */ 26 | private function assertPreStart(BasicInterface&Loop $loop): void 27 | { 28 | $this->assertEquals(self::LOOP_NAME, "$loop"); 29 | 30 | $this->assertFalse($loop->isRunning()); 31 | $this->assertFalse($loop->ran()); 32 | 33 | $this->assertFalse($loop->inited()); 34 | 35 | $this->assertEquals(0, $loop->startCounter()); 36 | $this->assertEquals(0, $loop->endCounter()); 37 | } 38 | /** 39 | * Execute after-start assertions. 40 | */ 41 | private function assertAfterStart(BasicInterface&Loop $loop, int $prevRun = 0): void 42 | { 43 | self::waitTick(); 44 | $this->assertTrue($loop->inited()); 45 | 46 | if ($prevRun === 0) { 47 | $this->assertFalse($loop->ran()); 48 | } else { 49 | $this->assertTrue($loop->ran()); 50 | } 51 | 52 | $this->assertTrue($loop->isRunning()); 53 | 54 | $this->assertEquals($prevRun+1, $loop->startCounter()); 55 | $this->assertEquals($prevRun, $loop->endCounter()); 56 | 57 | $this->assertFalse($loop->start()); 58 | $this->assertFalse($loop->isPaused()); 59 | } 60 | /** 61 | * Execute final assertions. 62 | */ 63 | private function assertFinal(BasicInterface&Loop $loop, int $count = 1): void 64 | { 65 | $this->assertTrue($loop->ran()); 66 | $this->assertFalse($loop->isRunning()); 67 | 68 | $this->assertTrue($loop->inited()); 69 | 70 | $this->assertEquals($count, $loop->startCounter()); 71 | $this->assertEquals($count, $loop->endCounter()); 72 | } 73 | /** 74 | * Test basic loop. 75 | */ 76 | public function testLoop(): void 77 | { 78 | $loop = new class() extends Loop implements BasicInterface { 79 | use Basic; 80 | }; 81 | $this->assertPreStart($loop); 82 | $this->assertTrue($loop->start()); 83 | $this->assertAfterStart($loop); 84 | 85 | delay(0.110); 86 | 87 | $this->assertFinal($loop); 88 | } 89 | /** 90 | * Test basic loop. 91 | */ 92 | public function testLoopStopFromOutside(): void 93 | { 94 | $loop = new class() extends Loop implements BasicInterface { 95 | use Basic; 96 | /** 97 | * Loop implementation. 98 | */ 99 | public function loop(): ?float 100 | { 101 | $this->inited = true; 102 | delay(0.1); 103 | $this->ran = true; 104 | return 1000.0; 105 | } 106 | }; 107 | $this->assertPreStart($loop); 108 | $this->assertTrue($loop->start()); 109 | $this->assertAfterStart($loop); 110 | 111 | $this->assertTrue($loop->stop()); 112 | delay(0.110); 113 | 114 | $this->assertFinal($loop); 115 | } 116 | /** 117 | * Test basic loop. 118 | */ 119 | public function testLoopStopFromOutsideRestart(): void 120 | { 121 | $loop = new class() extends Loop implements BasicInterface { 122 | use Basic; 123 | /** 124 | * Loop implementation. 125 | */ 126 | public function loop(): ?float 127 | { 128 | $this->inited = true; 129 | delay(0.1); 130 | $this->ran = true; 131 | return 1000.0; 132 | } 133 | }; 134 | $this->assertPreStart($loop); 135 | $this->assertTrue($loop->start()); 136 | $this->assertAfterStart($loop); 137 | 138 | EventLoop::queue(function () use ($loop): void { 139 | $this->assertTrue($loop->stop()); 140 | }); 141 | self::waitTick(); 142 | 143 | $this->assertTrue($loop->start()); 144 | $this->assertAfterStart($loop, 1); 145 | 146 | $this->assertTrue($loop->stop()); 147 | delay(0.110); 148 | 149 | $this->assertFinal($loop, 2); 150 | } 151 | /** 152 | * Test basic exception in loop. 153 | */ 154 | public function testException(): void 155 | { 156 | $loop = new class() extends Loop implements BasicInterface { 157 | use BasicException; 158 | }; 159 | 160 | $e_thrown = null; 161 | EventLoop::setErrorHandler(function (\RuntimeException $e) use (&$e_thrown): void { 162 | $e_thrown = $e; 163 | }); 164 | 165 | $this->assertPreStart($loop); 166 | $this->assertTrue($loop->start()); 167 | self::waitTick(); 168 | $this->assertFalse($loop->isRunning()); 169 | 170 | $this->assertTrue($loop->inited()); 171 | 172 | $this->assertEquals(1, $loop->startCounter()); 173 | $this->assertEquals(1, $loop->endCounter()); 174 | 175 | $this->assertInstanceOf(RuntimeException::class, $e_thrown); 176 | 177 | EventLoop::setErrorHandler(null); 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /test/PeriodicTest.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2016-2020 Daniil Gentili 7 | * @license https://opensource.org/licenses/MIT MIT 8 | */ 9 | 10 | namespace danog\Loop\Test; 11 | 12 | use danog\Loop\Loop; 13 | use danog\Loop\PeriodicLoop; 14 | use danog\Loop\Test\Interfaces\LoggingInterface; 15 | use danog\Loop\Test\Traits\Basic; 16 | use danog\Loop\Test\Traits\Logging; 17 | 18 | use function Amp\delay; 19 | 20 | class PeriodicTest extends Fixtures 21 | { 22 | /** 23 | * Test basic loop. 24 | * 25 | * @param bool $stopSig Whether to stop with signal 26 | * 27 | * 28 | * 29 | * @dataProvider provideTrueFalse 30 | */ 31 | public function testGeneric(bool $stopSig): void 32 | { 33 | $runCount = 0; 34 | $retValue = false; 35 | $callable = function (?PeriodicLoop $loop) use (&$runCount, &$retValue, &$l) { 36 | $l = $loop; 37 | $runCount++; 38 | return $retValue; 39 | }; 40 | $this->fixtureAssertions($callable, $runCount, $retValue, $stopSig, $l); 41 | $obj = new class() { 42 | public $retValue = false; 43 | public $runCount = 0; 44 | public ?PeriodicLoop $loop = null; 45 | public function run(PeriodicLoop $periodicLoop) 46 | { 47 | $this->loop = $periodicLoop; 48 | $this->runCount++; 49 | return $this->retValue; 50 | } 51 | }; 52 | $this->fixtureAssertions([$obj, 'run'], $obj->runCount, $obj->retValue, $stopSig, $obj->loop); 53 | $obj = new class() { 54 | public $retValue = false; 55 | public $runCount = 0; 56 | public ?PeriodicLoop $loop = null; 57 | public function run(PeriodicLoop $periodicLoop) 58 | { 59 | $this->loop = $periodicLoop; 60 | $this->runCount++; 61 | return $this->retValue; 62 | } 63 | }; 64 | $this->fixtureAssertions(\Closure::fromCallable([$obj, 'run']), $obj->runCount, $obj->retValue, $stopSig, $obj->loop); 65 | } 66 | /** 67 | * Test generator loop. 68 | * 69 | * @param bool $stopSig Whether to stop with signal 70 | * 71 | * 72 | * 73 | * @dataProvider provideTrueFalse 74 | */ 75 | public function testGenerator(bool $stopSig): void 76 | { 77 | $runCount = 0; 78 | $retValue = false; 79 | $callable = function (?PeriodicLoop $loop) use (&$runCount, &$retValue, &$l) { 80 | $l = $loop; 81 | $runCount++; 82 | return $retValue; 83 | }; 84 | $this->fixtureAssertions($callable, $runCount, $retValue, $stopSig, $l); 85 | $obj = new class() { 86 | public $retValue = false; 87 | public $runCount = 0; 88 | public ?PeriodicLoop $loop = null; 89 | public function run(?PeriodicLoop $loop) 90 | { 91 | $this->loop = $loop; 92 | $this->runCount++; 93 | return $this->retValue; 94 | } 95 | }; 96 | $this->fixtureAssertions([$obj, 'run'], $obj->runCount, $obj->retValue, $stopSig, $obj->loop); 97 | $obj = new class() { 98 | public $retValue = false; 99 | public $runCount = 0; 100 | public ?PeriodicLoop $loop = null; 101 | public function run(?PeriodicLoop $loop) 102 | { 103 | $this->loop = $loop; 104 | $this->runCount++; 105 | return $this->retValue; 106 | } 107 | }; 108 | $this->fixtureAssertions(\Closure::fromCallable([$obj, 'run']), $obj->runCount, $obj->retValue, $stopSig, $obj->loop); 109 | } 110 | /** 111 | * Fixture assertions for started loop. 112 | */ 113 | private function fixtureStarted(PeriodicLoop&LoggingInterface $loop): void 114 | { 115 | $this->assertTrue($loop->isRunning()); 116 | $this->assertEquals(1, $loop->startCounter()); 117 | $this->assertEquals(0, $loop->endCounter()); 118 | } 119 | /** 120 | * Run fixture assertions. 121 | * 122 | * @param callable $closure Closure 123 | * @param integer $runCount Run count 124 | * @param bool $retValue Pause time 125 | * @param bool $stopSig Whether to stop with signal 126 | */ 127 | private function fixtureAssertions(callable $closure, int &$runCount, bool &$retValue, bool $stopSig, ?PeriodicLoop &$l): void 128 | { 129 | $loop = new class($closure, Fixtures::LOOP_NAME, 0.1) extends PeriodicLoop implements LoggingInterface { 130 | use Logging; 131 | }; 132 | $this->assertEquals(Fixtures::LOOP_NAME, "$loop"); 133 | 134 | $this->assertFalse($loop->isRunning()); 135 | $this->assertEquals(0, $loop->startCounter()); 136 | $this->assertEquals(0, $loop->endCounter()); 137 | 138 | $this->assertEquals(0, $runCount); 139 | 140 | $this->assertTrue($loop->start()); 141 | $this->fixtureStarted($loop); 142 | self::waitTick(); 143 | 144 | $this->assertTrue($loop->isPaused()); 145 | $this->assertEquals(1, $runCount); 146 | 147 | $this->assertEquals($loop, $l); 148 | 149 | delay(0.048); 150 | $this->fixtureStarted($loop); 151 | 152 | $this->assertTrue($loop->isPaused()); 153 | $this->assertEquals(1, $runCount); 154 | 155 | delay(0.060); 156 | $this->fixtureStarted($loop); 157 | 158 | $this->assertTrue($loop->isPaused()); 159 | $this->assertEquals(2, $runCount); 160 | 161 | $this->assertTrue($loop->resume()); 162 | self::waitTick(); 163 | 164 | $this->assertTrue($loop->isPaused()); 165 | $this->assertEquals(3, $runCount); 166 | 167 | if ($stopSig) { 168 | $this->assertTrue($loop->stop()); 169 | } else { 170 | $retValue = true; 171 | $this->assertTrue($loop->resume()); 172 | } 173 | self::waitTick(); 174 | $this->assertEquals($stopSig ? 3 : 4, $runCount); 175 | 176 | $this->assertTrue($loop->isPaused()); 177 | $this->assertFalse($loop->isRunning()); 178 | 179 | $this->assertEquals(1, $loop->startCounter()); 180 | $this->assertEquals(1, $loop->endCounter()); 181 | } 182 | 183 | /** 184 | * Provide true false. 185 | * 186 | */ 187 | public function provideTrueFalse(): array 188 | { 189 | return [ 190 | [true], 191 | [false] 192 | ]; 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /test/Traits/Basic.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2016-2020 Daniil Gentili 7 | * @license https://opensource.org/licenses/MIT MIT 8 | */ 9 | 10 | namespace danog\Loop\Test\Traits; 11 | 12 | use danog\Loop\Loop; 13 | use danog\Loop\Test\LoopTest; 14 | 15 | use function Amp\delay; 16 | 17 | trait Basic 18 | { 19 | use Logging; 20 | /** 21 | * Check whether the loop inited. 22 | * 23 | * @var bool 24 | */ 25 | private $inited = false; 26 | /** 27 | * Check whether the loop ran. 28 | * 29 | * @var bool 30 | */ 31 | private $ran = false; 32 | /** 33 | * Check whether the loop inited. 34 | */ 35 | public function inited(): bool 36 | { 37 | return $this->inited; 38 | } 39 | /** 40 | * Check whether the loop ran. 41 | */ 42 | public function ran(): bool 43 | { 44 | return $this->ran; 45 | } 46 | /** 47 | * Loop implementation. 48 | */ 49 | public function loop(): ?float 50 | { 51 | $this->inited = true; 52 | delay(0.1); 53 | $this->ran = true; 54 | return Loop::STOP; 55 | } 56 | /** 57 | * Get loop name. 58 | * 59 | */ 60 | public function __toString(): string 61 | { 62 | return LoopTest::LOOP_NAME; 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /test/Traits/BasicException.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2016-2020 Daniil Gentili 7 | * @license https://opensource.org/licenses/MIT MIT 8 | */ 9 | 10 | namespace danog\Loop\Test\Traits; 11 | 12 | trait BasicException 13 | { 14 | use Basic; 15 | /** 16 | * Loop implementation. 17 | * 18 | */ 19 | public function loop(): ?float 20 | { 21 | $this->inited = true; 22 | throw new \RuntimeException('Threw exception!'); 23 | $this->ran = true; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /test/Traits/Logging.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2016-2020 Daniil Gentili 7 | * @license https://opensource.org/licenses/MIT MIT 8 | */ 9 | 10 | namespace danog\Loop\Test\Traits; 11 | 12 | trait Logging 13 | { 14 | /** 15 | * Check whether the loop started. 16 | */ 17 | private int $startCounter = 0; 18 | /** 19 | * Check whether the loop ended. 20 | */ 21 | private int $endCounter = 0; 22 | 23 | /** 24 | * Signal that loop started. 25 | * 26 | */ 27 | final protected function startedLoop(): void 28 | { 29 | parent::startedLoop(); 30 | $this->startCounter++; 31 | } 32 | /** 33 | * Signal that loop ended. 34 | * 35 | */ 36 | final protected function exitedLoop(): void 37 | { 38 | parent::exitedLoop(); 39 | $this->endCounter++; 40 | } 41 | 42 | /** 43 | * Get start counter. 44 | */ 45 | public function startCounter(): int 46 | { 47 | return $this->startCounter; 48 | } 49 | /** 50 | * Get end counter. 51 | */ 52 | public function endCounter(): int 53 | { 54 | return $this->endCounter; 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /test/Traits/LoggingPause.php: -------------------------------------------------------------------------------- 1 | 6 | * @copyright 2016-2020 Daniil Gentili 7 | * @license https://opensource.org/licenses/MIT MIT 8 | */ 9 | 10 | namespace danog\Loop\Test\Traits; 11 | 12 | use function Amp\delay; 13 | 14 | trait LoggingPause 15 | { 16 | use Logging; 17 | /** 18 | * Number of times loop was paused. 19 | */ 20 | private int $pauseCount = 0; 21 | /** 22 | * Last pause delay. 23 | */ 24 | private float $lastPause = 0; 25 | /** 26 | * Get number of times loop was paused. 27 | */ 28 | public function getPauseCount(): int 29 | { 30 | return $this->pauseCount; 31 | } 32 | 33 | /** 34 | * Get last pause. 35 | */ 36 | public function getLastPause(): float 37 | { 38 | return $this->lastPause; 39 | } 40 | /** 41 | * Report pause, can be overriden for logging. 42 | * 43 | * @param integer $timeout Pause duration, 0 = forever 44 | * 45 | */ 46 | protected function reportPause(float $timeout): void 47 | { 48 | parent::reportPause($timeout); 49 | $this->pauseCount++; 50 | $this->lastPause = $timeout; 51 | } 52 | } 53 | --------------------------------------------------------------------------------