From 2236246bdec4c3d10e81d4dd1c06cd2e476b94f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gr=C3=A9gory=20Planchat?= Date: Tue, 29 Jun 2021 21:38:49 +0200 Subject: [PATCH] Added promise helper functions --- .github/workflows/actions.yml | 96 ++++++++++++---------- .idea/.gitignore | 2 - .idea/misc.xml | 6 -- .idea/modules.xml | 8 -- .idea/php-test-framework.xml | 40 --------- .idea/php.xml | 143 -------------------------------- .idea/promise.iml | 68 --------------- .idea/vcs.xml | 6 -- composer.json | 14 +++- composer.lock | 103 ++++++++++++++++++++++- src/promises.php | 150 ++++++++++++++++++++++++++++++++++ tests/unit/AllTest.php | 44 ++++++++++ tests/unit/AnyTest.php | 47 +++++++++++ tests/unit/RaceTest.php | 37 +++++++++ tests/unit/SomeTest.php | 47 +++++++++++ 15 files changed, 491 insertions(+), 320 deletions(-) delete mode 100644 .idea/.gitignore delete mode 100644 .idea/misc.xml delete mode 100644 .idea/modules.xml delete mode 100644 .idea/php-test-framework.xml delete mode 100644 .idea/php.xml delete mode 100644 .idea/promise.iml delete mode 100644 .idea/vcs.xml create mode 100644 src/promises.php create mode 100644 tests/unit/AllTest.php create mode 100644 tests/unit/AnyTest.php create mode 100644 tests/unit/RaceTest.php create mode 100644 tests/unit/SomeTest.php diff --git a/.github/workflows/actions.yml b/.github/workflows/actions.yml index 6bb6130..79f8adb 100644 --- a/.github/workflows/actions.yml +++ b/.github/workflows/actions.yml @@ -1,48 +1,60 @@ name: Quality on: push jobs: - cs-fixer: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - name: Cs-Fixer - run: | - wget -q https://cs.symfony.com/download/php-cs-fixer-v2.phar -O php-cs-fixer - chmod a+x php-cs-fixer - PHP_CS_FIXER_IGNORE_ENV=true ./php-cs-fixer fix src --dry-run + cs-fixer: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Cs-Fixer + run: | + wget -q https://cs.symfony.com/download/php-cs-fixer-v2.phar -O php-cs-fixer + chmod a+x php-cs-fixer + PHP_CS_FIXER_IGNORE_ENV=true ./php-cs-fixer fix src --dry-run - phpstan: - runs-on: ubuntu-latest - strategy: - matrix: - phpstan-level: [ 3, 5, 7, 8 ] - steps: - - uses: actions/checkout@v2 - - uses: actions/cache@v2 - with: - path: '**/vendor' - key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} - restore-keys: | - ${{ runner.os }}-composer- - - uses: php-actions/composer@v5 - with: - args: --prefer-dist - php_version: 8.0 + phpstan: + runs-on: ubuntu-latest + strategy: + matrix: + phpstan-level: [ 3, 5, 7, 8 ] + steps: + - uses: actions/checkout@v2 + - uses: actions/cache@v2 + with: + path: '**/vendor' + key: ${{ runner.os }}-composer-${{ hashFiles('**/composer.lock') }} + restore-keys: | + ${{ runner.os }}-composer- + - uses: php-actions/composer@v5 + with: + args: --prefer-dist + php_version: 8.0 - - name: PHPStan - uses: php-actions/phpstan@v2 - with: - path: src/ - args: --level=${{ matrix.phpstan-level }} + - name: PHPStan + uses: php-actions/phpstan@v2 + with: + path: src/ + args: --level=${{ matrix.phpstan-level }} - phpspec: - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v2 - - uses: php-actions/composer@v5 - with: - args: --prefer-dist - php_version: 8.0 - extensions: xdebug - - name: PHP Spec - run: bin/phpspec run spec + phpspec: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: php-actions/composer@v5 + with: + args: --prefer-dist + php_version: 8.0 + extensions: xdebug + - name: PHPSpec + run: bin/phpspec run spec + + phpunit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - uses: php-actions/composer@v5 + with: + args: --prefer-dist + php_version: 8.0 + extensions: xdebug + - name: PHPUnit + run: bin/phpunit tests diff --git a/.idea/.gitignore b/.idea/.gitignore deleted file mode 100644 index 5c98b42..0000000 --- a/.idea/.gitignore +++ /dev/null @@ -1,2 +0,0 @@ -# Default ignored files -/workspace.xml \ No newline at end of file diff --git a/.idea/misc.xml b/.idea/misc.xml deleted file mode 100644 index 28a804d..0000000 --- a/.idea/misc.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - \ No newline at end of file diff --git a/.idea/modules.xml b/.idea/modules.xml deleted file mode 100644 index 2b3828e..0000000 --- a/.idea/modules.xml +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - \ No newline at end of file diff --git a/.idea/php-test-framework.xml b/.idea/php-test-framework.xml deleted file mode 100644 index 40c9ece..0000000 --- a/.idea/php-test-framework.xml +++ /dev/null @@ -1,40 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/php.xml b/.idea/php.xml deleted file mode 100644 index 7eee1da..0000000 --- a/.idea/php.xml +++ /dev/null @@ -1,143 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - /usr/local/etc/php/conf.d/docker-php-ext-intl.ini, /usr/local/etc/php/conf.d/docker-php-ext-opcache.ini, /usr/local/etc/php/conf.d/docker-php-ext-pcntl.ini, /usr/local/etc/php/conf.d/docker-php-ext-sodium.ini, /usr/local/etc/php/conf.d/docker-php-ext-xdebug.ini, /usr/local/etc/php/conf.d/memory.ini, /usr/local/etc/php/conf.d/security.ini, /usr/local/etc/php/conf.d/xdebug.ini - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/promise.iml b/.idea/promise.iml deleted file mode 100644 index eccf802..0000000 --- a/.idea/promise.iml +++ /dev/null @@ -1,68 +0,0 @@ - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/.idea/vcs.xml b/.idea/vcs.xml deleted file mode 100644 index 94a25f7..0000000 --- a/.idea/vcs.xml +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/composer.json b/composer.json index 136919b..f2e94ed 100644 --- a/composer.json +++ b/composer.json @@ -21,18 +21,24 @@ }, "require-dev": { "phpunit/phpunit": "^9.0", - "phpspec/phpspec": "^7.0" + "phpspec/phpspec": "^7.0", + "johnkary/phpunit-speedtrap": "*", + "mybuilder/phpunit-accelerator": "*", + "phpunit/php-invoker": "*" }, "autoload": { "psr-4": { "Kiboko\\Component\\Promise\\": "src/" - } + }, + "files": [ + "src/promises.php" + ] }, "autoload-dev": { "psr-4": { "spec\\Kiboko\\Component\\Promise\\": "spec/", - "unit\\Kiboko\\Component\\Promise\\": "unit/", - "functional\\Kiboko\\Component\\Promise\\": "functional/" + "unit\\Kiboko\\Component\\Promise\\": "tests/unit/", + "functional\\Kiboko\\Component\\Promise\\": "tests/functional/" } }, "config": { diff --git a/composer.lock b/composer.lock index c84cc75..83d92b8 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "cb8a5715447ba350d35a17b6e8e054dd", + "content-hash": "c1fc094c985951048d3d36085de2ff70", "packages": [ { "name": "php-etl/promise-contracts", @@ -127,6 +127,107 @@ ], "time": "2020-11-10T18:47:58+00:00" }, + { + "name": "johnkary/phpunit-speedtrap", + "version": "v4.0.0", + "source": { + "type": "git", + "url": "https://github.com/johnkary/phpunit-speedtrap.git", + "reference": "5f9b160eac87e975f1c6ca9faee5125f0616fba3" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/johnkary/phpunit-speedtrap/zipball/5f9b160eac87e975f1c6ca9faee5125f0616fba3", + "reference": "5f9b160eac87e975f1c6ca9faee5125f0616fba3", + "shasum": "" + }, + "require": { + "php": ">=7.1", + "phpunit/phpunit": "^7.0 || ^8.0 || ^9.0" + }, + "type": "library", + "extra": { + "branch-alias": { + "dev-master": "4.0-dev" + } + }, + "autoload": { + "psr-4": { + "JohnKary\\PHPUnit\\Listener\\": "src/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "John Kary", + "email": "john@johnkary.net" + } + ], + "description": "Find and report on slow tests in your PHPUnit test suite", + "homepage": "https://github.com/johnkary/phpunit-speedtrap", + "keywords": [ + "phpunit", + "profile", + "slow" + ], + "support": { + "issues": "https://github.com/johnkary/phpunit-speedtrap/issues", + "source": "https://github.com/johnkary/phpunit-speedtrap/tree/v4.0.0" + }, + "time": "2021-05-03T02:37:05+00:00" + }, + { + "name": "mybuilder/phpunit-accelerator", + "version": "1.2.0", + "source": { + "type": "git", + "url": "https://github.com/mybuilder/phpunit-accelerator.git", + "reference": "11d4850d174257b3718f2f3ac016cb9cc66e4de7" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/mybuilder/phpunit-accelerator/zipball/11d4850d174257b3718f2f3ac016cb9cc66e4de7", + "reference": "11d4850d174257b3718f2f3ac016cb9cc66e4de7", + "shasum": "" + }, + "require": { + "php": ">=5.6", + "phpunit/phpunit": ">=4.7" + }, + "type": "library", + "autoload": { + "psr-4": { + "MyBuilder\\PhpunitAccelerator\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Keyvan Akbary", + "email": "keyvan@mybuilder.com" + } + ], + "description": "PHPUnit accelerator", + "keywords": [ + "accelerator", + "fast", + "free", + "memory", + "phpunit", + "property" + ], + "support": { + "issues": "https://github.com/mybuilder/phpunit-accelerator/issues", + "source": "https://github.com/mybuilder/phpunit-accelerator/tree/master" + }, + "time": "2016-11-15T10:37:19+00:00" + }, { "name": "myclabs/deep-copy", "version": "1.10.2", diff --git a/src/promises.php b/src/promises.php new file mode 100644 index 0000000..c7c23cd --- /dev/null +++ b/src/promises.php @@ -0,0 +1,150 @@ +> $promises + * @return PromiseInterface, \Throwable> + */ +function all(array $promises): PromiseInterface +{ + $promise = new Promise(); + + $count = count($promises); + /** @var \SplFixedArray> $bucket */ + $bucket = new \SplFixedArray($count); + + foreach ($promises as $index => $deferred) { + $bucket[$index] = $deferred; + + $deferred + ->failure(function ($failure) use ($promise, $bucket, $index, &$count) { + $bucket[$index] = $failure; + + if (--$count <= 0) { + $promise->resolve($bucket->toArray()); + } + }) + ->then(function ($value) use ($promise, $bucket, $index, &$count) { + $bucket[$index] = $value; + + if (--$count <= 0) { + $promise->resolve($bucket->toArray()); + } + }); + } + + return $promise; +} + +/** + * @param array> $promises + * @return PromiseInterface, \Throwable> + */ +function any(array $promises): PromiseInterface +{ + return namespace\some($promises, 1); +} + +/** + * @param array> $promises + * @return PromiseInterface, \Throwable> + */ +function race(array $promises): PromiseInterface +{ + $promise = new Promise(); + + foreach ($promises as $index => $deferred) { + $bucket[$index] = $deferred; + + $deferred + ->failure(function ($failure) use ($promise) { + $promise->fail($failure); + }) + ->then(function ($value) use ($promise) { + $promise->resolve($value); + }); + } + + return $promise; +} + +/** + * @param array> $promises + * @return PromiseInterface, \Throwable> + */ +function some(array $promises, int $count = 1): PromiseInterface +{ + $promise = new Promise(); + + $total = $pendingResolution = count($promises); + /** @var \SplFixedArray> $bucket */ + $bucket = new \SplFixedArray($pendingResolution); + + foreach ($promises as $index => $deferred) { + $bucket[$index] = $deferred; + + $deferred + ->failure(function ($failure) use ($promise, $bucket, $index, $count, $total, &$pendingResolution) { + $bucket[$index] = $failure; + --$pendingResolution; + + if (($pendingResolution <= 0 || ($total - $pendingResolution) >= $count) + && !$promise->isResolved() + ) { + $promise->resolve($bucket->toArray()); + } + }) + ->then(function ($value) use ($promise, $bucket, $index, $count, $total, &$pendingResolution) { + $bucket[$index] = $value; + --$pendingResolution; + + if (($pendingResolution <= 0 || ($total - $pendingResolution) >= $count) + && !$promise->isResolved() + ) { + $promise->resolve($bucket->toArray()); + } + }); + } + + return $promise; +} + +/** + * @param array> $promises + * @param callable(mixed|\Throwable|PromiseInterface): PromiseInterface $callback + * @return PromiseInterface, \Throwable> + */ +function map(array $promises, callable $callback): PromiseInterface +{ + $promise = new Promise(); + + namespace\all($promises) + ->then(function (array $values) use ($promise, $callback) { + $promise->resolve(array_map($callback, $values)); + }); + + return $promise; +} + +/** + * @template Type + * + * @param array> $promises + * @param callable $callback + * @param null|Type $seed + * @return PromiseInterface + */ +function reduce(array $promises, callable $callback, $seed = null): PromiseInterface +{ + $promise = new Promise(); + + namespace\all($promises) + ->then(function (array $values) use ($promise, $callback, $seed) { + $promise->resolve(array_reduce($values, $callback, $seed)); + }); + + return $promise; +} diff --git a/tests/unit/AllTest.php b/tests/unit/AllTest.php new file mode 100644 index 0000000..4658aef --- /dev/null +++ b/tests/unit/AllTest.php @@ -0,0 +1,44 @@ +assertfalse($promise->isResolved()); + + $childPromise->resolve('success'); + + $this->assertTrue($promise->isResolved()); + $this->assertInstanceOf(SuccessInterface::class, $promise->resolution()); + $this->assertEquals(['success', 'Resolved Value', new \Exception()], $promise->resolution()->value()); + } + + public function testFailedPromise() + { + $promise = Component\all([ + $childPromise = new Component\Promise(), + new Component\SucceededPromise('Resolved Value'), + new Component\FailedPromise(new \Exception()), + ]); + + $this->assertfalse($promise->isResolved()); + + $childPromise->fail(new \Exception()); + + $this->assertTrue($promise->isResolved()); + $this->assertInstanceOf(SuccessInterface::class, $promise->resolution()); + $this->assertEquals([new \Exception(), 'Resolved Value', new \Exception()], $promise->resolution()->value()); + } +} diff --git a/tests/unit/AnyTest.php b/tests/unit/AnyTest.php new file mode 100644 index 0000000..a07d97c --- /dev/null +++ b/tests/unit/AnyTest.php @@ -0,0 +1,47 @@ +assertfalse($promise->isResolved()); + + $childPromise1->resolve('success'); + + $this->assertTrue($promise->isResolved()); + $this->assertInstanceOf(SuccessInterface::class, $promise->resolution()); + $this->assertEquals(['success', $childPromise2, $childPromise3, $childPromise4], $promise->resolution()->value()); + } + + public function testFailedPromise() + { + $promise = Component\any([ + $childPromise1 = new Component\Promise(), + $childPromise2 = new Component\Promise(), + $childPromise3 = new Component\Promise(), + $childPromise4 = new Component\Promise(), + ]); + + $this->assertfalse($promise->isResolved()); + + $childPromise1->fail(new \Exception()); + + $this->assertTrue($promise->isResolved()); + $this->assertInstanceOf(SuccessInterface::class, $promise->resolution()); + $this->assertEquals([new \Exception(), $childPromise2, $childPromise3, $childPromise4], $promise->resolution()->value()); + } +} diff --git a/tests/unit/RaceTest.php b/tests/unit/RaceTest.php new file mode 100644 index 0000000..12ddcae --- /dev/null +++ b/tests/unit/RaceTest.php @@ -0,0 +1,37 @@ +assertTrue($promise->isResolved()); + $this->assertInstanceOf(SuccessInterface::class, $promise->resolution()); + $this->assertEquals('Resolved Value', $promise->resolution()->value()); + } + + public function testFailedPromise() + { + $promise = Component\race([ + new Component\Promise(), + new Component\Promise(), + new Component\FailedPromise(new \Exception()), + ]); + + $this->assertTrue($promise->isResolved()); + $this->assertInstanceOf(FailureInterface::class, $promise->resolution()); + $this->assertEquals(new \Exception(), $promise->resolution()->error()); + } +} diff --git a/tests/unit/SomeTest.php b/tests/unit/SomeTest.php new file mode 100644 index 0000000..5e860f2 --- /dev/null +++ b/tests/unit/SomeTest.php @@ -0,0 +1,47 @@ +assertfalse($promise->isResolved()); + + $childPromise1->resolve('success'); + + $this->assertTrue($promise->isResolved()); + $this->assertInstanceOf(SuccessInterface::class, $promise->resolution()); + $this->assertEquals(['success', $childPromise2, 'Resolved Value', new \Exception()], $promise->resolution()->value()); + } + + public function testFailedPromise() + { + $promise = Component\some([ + $childPromise1 = new Component\Promise(), + $childPromise2 = new Component\Promise(), + new Component\SucceededPromise('Resolved Value'), + new Component\FailedPromise(new \Exception()), + ], 3); + + $this->assertfalse($promise->isResolved()); + + $childPromise1->fail(new \Exception()); + + $this->assertTrue($promise->isResolved()); + $this->assertInstanceOf(SuccessInterface::class, $promise->resolution()); + $this->assertEquals([new \Exception(), $childPromise2, 'Resolved Value', new \Exception()], $promise->resolution()->value()); + } +}