From: Michael Wallner Date: Tue, 19 Sep 2017 13:44:18 +0000 (+0200) Subject: flush X-Git-Url: https://git.m6w6.name/?p=m6w6%2Fseekat;a=commitdiff_plain;h=e368287b3cd2dd40945ac8d1a1946bc32268007d flush --- diff --git a/composer.json b/composer.json index c734070..49a40af 100644 --- a/composer.json +++ b/composer.json @@ -15,12 +15,13 @@ }, "files": [ "lib/functions.php", - "lib/Exception/functions.php", + "lib/API/functions.php", "lib/API/Future/functions.php", - "lib/API/Links/functions.php" + "lib/API/Links/functions.php", + "lib/Exception/functions.php" ] }, - "minimum-stability": "beta", + "minimum-stability": "dev", "prefer-stable": true, "require": { "php": "^7.0", @@ -35,9 +36,7 @@ "react/promise": "dev-async-interop", "amphp/amp": "dev-master", "amphp/loop": "dev-master", - "peridot-php/peridot": "^1.15", "monolog/monolog": "^1.19", - "peridot-php/leo": "^1.5", - "peridot-php/peridot-code-coverage-reporters": "^2.0" + "phpunit/phpunit": "^5" } } diff --git a/examples/cache.php b/examples/cache.php old mode 100644 new mode 100755 index 87c0aef..797fe60 --- a/examples/cache.php +++ b/examples/cache.php @@ -34,7 +34,7 @@ $cache = new class($redis) implements \seekat\API\Call\Cache\Service { } }; -$api = new seekat\API([ +$api = new seekat\API(seekat\API\Future\react(), [ "Authorization" => "token ".getenv("GITHUB_TOKEN") ], null, null, $log, $cache); diff --git a/examples/cli.php b/examples/cli.php old mode 100644 new mode 100755 index 650ca85..fc72130 --- a/examples/cli.php +++ b/examples/cli.php @@ -1,24 +1,25 @@ +#!/usr/bin/env php "token ".getenv("GITHUB_TOKEN") ]); array_shift($argv); -($self = function($api) use(&$self) { +($self = function($error, $api) use(&$self) { global $argv; while (null !== ($arg = array_shift($argv))) { if ("." === $arg) { - $api->then($self); + $api->when($self); return; } $api = $api->$arg; } echo $api, "\n"; -})($api); +})(null, $api); $api->send(); diff --git a/examples/generator.php b/examples/generator.php index acee8a5..1fd81e6 100755 --- a/examples/generator.php +++ b/examples/generator.php @@ -11,13 +11,13 @@ $log->pushHandler((new Monolog\Handler\StreamHandler(STDERR))->setLevel(Monolog\ $cli = new http\Client("curl", "seekat"); -$api = new API([ +$api = new API(API\Future\react(), [ "Authorization" => "token ".getenv("GITHUB_TOKEN") ], null, $cli, $log); $api(function($api) { $count = 0; - $events = yield $api->repos->m6w6->{"ext-http"}->issues->events(); + $events = yield $api->repos->m6w6->{"ext-pq"}->issues->events(); while ($events) { /* pro-actively queue the next request */ $next = Links\next($events); @@ -38,7 +38,7 @@ $api(function($api) { $events = yield $next; } return $count; -})->done(function($count) { +})->when(function($error, $count) { printf("Listed %d events\n", $count); }); diff --git a/examples/gistlog.php b/examples/gistlog.php index 723cc0c..3a1b8ec 100755 --- a/examples/gistlog.php +++ b/examples/gistlog.php @@ -3,9 +3,15 @@ require __DIR__."/../vendor/autoload.php"; -$api = new seekat\API([ - "Authorization" => "token ".getenv("GITHUB_TOKEN") -]); + +$log = new Monolog\Logger("seekat"); +$log->pushHandler(new Monolog\Handler\StreamHandler(STDERR, Monolog\Logger::DEBUG)); + +$api = new seekat\API( + seekat\API\Future\react(), + seekat\API\auth("token", getenv("GITHUB_TOKEN")), + null, null, $log +); $api(function($api) { $gists = yield $api->users->m6w6->gists(); @@ -15,7 +21,7 @@ $api(function($api) { foreach ($gists as $gist) { foreach ($gist->files as $name => $file) { if ($name == "blog.md") { - $text = $file->as("raw")->raw();; + $text = $file->raw(); $head = $gist->description." ".$gist->created_at; echo "$head\n"; echo str_repeat("=", strlen($head))."\n\n"; diff --git a/examples/hooks.php b/examples/hooks.php index 37cb8e0..dfcb635 100755 --- a/examples/hooks.php +++ b/examples/hooks.php @@ -3,21 +3,20 @@ require_once __DIR__."/../vendor/autoload.php"; -use seekat\API; +use seekat\{API, API\Future, API\Links}; +use Monolog\{Logger, Handler}; $cli = new http\Client("curl", "seekat"); $cli->configure([ "max_host_connections" => 10, "max_total_connections" => 50, - "use_eventloop" => false, + "use_eventloop" => true, ]); -$log = new Monolog\Logger("seekat"); -$log->pushHandler((new Monolog\Handler\StreamHandler(STDERR))->setLevel(Monolog\Logger::WARNING)); +$log = new Logger("seekat"); +$log->pushHandler(new Handler\StreamHandler(STDERR, Logger::NOTICE)); -$api = new API([ - "Authorization" => "token ".getenv("GITHUB_TOKEN") -], null, $cli, $log); +$api = new API(Future\react(), API\auth("token", getenv("GITHUB_TOKEN")), null, $cli, $log); $api(function() use($api) { $repos = yield $api->users->m6w6->repos([ @@ -25,7 +24,7 @@ $api(function() use($api) { "affiliation" => "owner" ]); while ($repos) { - $next = next($repos); + $next = Links\next($repos); $batch = []; foreach ($repos as $repo) { diff --git a/examples/promise.php b/examples/promise.php index c26e911..cee5c74 100755 --- a/examples/promise.php +++ b/examples/promise.php @@ -4,16 +4,14 @@ require_once __DIR__."/../vendor/autoload.php"; use seekat\API; -use seekat\API\Future; -$log = new Monolog\Logger("seekat"); -$log->pushHandler((new Monolog\Handler\StreamHandler(STDERR))->setLevel(Monolog\Logger::NOTICE)); - -$api = new API(Future\amp(), API\auth("token", getenv("GITHUB_TOKEN")), null, null, $log); +$api = new API(API\Future\amp(), API\auth("token", getenv("GITHUB_TOKEN"))); $api->users->m6w6->gists()->when(function($error, $gists) { + $error and die($error); foreach ($gists as $gist) { $gist->commits()->when(function($error, $commits) use($gist) { + $error and die($error); foreach ($commits as $i => $commit) { if (!$i) { printf("\nGist %s, %s:\n", $gist->id, $gist->description ?: ""); diff --git a/examples/readme.php b/examples/readme.php index 93cc464..f7b6b57 100755 --- a/examples/readme.php +++ b/examples/readme.php @@ -3,6 +3,6 @@ require_once __DIR__."/../vendor/autoload.php"; -(new seekat\API)(function($api) { +(new seekat\API(seekat\API\Future\amp()))(function($api) { echo yield $api->repos->m6w6->seekat->readme->as("raw")->get(); }); diff --git a/lib/API.php b/lib/API.php index 546dd88..a047412 100644 --- a/lib/API.php +++ b/lib/API.php @@ -25,52 +25,52 @@ class API implements IteratorAggregate, Countable { private $url; /** - * Logger - * @var LoggerInterface + * Default headers to send out to the API endpoint + * @var array */ - private $logger; + private $headers; /** - * Cache - * @var Call\Cache\Service + * Current endpoints links + * @var Links */ - private $cache; + private $links; /** - * Promisor - * @var Future + * Current endpoint data's Content-Type + * @var API\ContentType */ - private $future; + private $type; /** - * The HTTP client - * @var Client + * Current endpoint's data + * @var array|object */ - private $client; + private $data; /** - * Default headers to send out to the API endpoint - * @var array + * Logger + * @var LoggerInterface */ - private $headers; + private $logger; /** - * Current endpoint data's Content-Type - * @var API\ContentType + * Cache + * @var Call\Cache\Service */ - private $type; + private $cache; /** - * Current endpoint's data - * @var array|object + * Promisor + * @var Future */ - private $data; + private $future; /** - * Current endpoints links - * @var Links + * The HTTP client + * @var Client */ - private $links; + private $client; /** * Create a new API endpoint root @@ -165,7 +165,9 @@ class API implements IteratorAggregate, Countable { function __invoke($cbg) : Promise { $this->logger->debug(__FUNCTION__); - $consumer = new Consumer($this->client); + $consumer = new Consumer($this->getFuture(), function() { + $this->client->send(); + }); invoke: if ($cbg instanceof Generator) { @@ -273,7 +275,7 @@ class API implements IteratorAggregate, Countable { function export() : array { $data = $this->data; $url = clone $this->url; - $type = clone $this->type; + $type = $this->type ? clone $this->type : null; $links = $this->links ? clone $this->links : null; $headers = $this->headers; return compact("url", "data", "type", "links", "headers"); @@ -355,6 +357,17 @@ class API implements IteratorAggregate, Countable { return $that; } + /** + * Perform a HEAD request against the endpoint's underlying URL + * + * @param mixed $args The HTTP query string parameters + * @param array $headers The request's additional HTTP headers + * @return Promise + */ + function head($args = null, array $headers = null, $cache = null) : Promise { + return $this->request("HEAD", $args, null, $headers, $cache); + } + /** * Perform a GET request against the endpoint's underlying URL * diff --git a/lib/API/Call.php b/lib/API/Call.php index ba51ca3..7eba2a8 100644 --- a/lib/API/Call.php +++ b/lib/API/Call.php @@ -25,26 +25,11 @@ final class Call } function __invoke(array $args) : Promise { - $promise = $this->api->{$this->call}->get(...$args); - - /* fetch resource, unless already localized, and try for {$method}_url */ - if (!$this->api->exists($this->call)) { - $promise->when(function($error, $value) use($args) { - if (!isset($error)) { - return $value; - } - if ($this->api->exists($this->call."_url", $url)) { - $url = new Url(uri_template($url, (array)current($args))); - return $this->api->withUrl($url)->get(...$args); - } - - $message = Exception\message($error); - $this->api->getLogger()->error("call($this->call): " . $message, [ - "url" => (string) $this->api->getUrl() - ]); - - throw $error; - }); + if ($this->api->exists($this->call."_url", $url)) { + $url = new Url(uri_template($url, (array)current($args))); + $promise = $this->api->withUrl($url)->get(...$args); + } else { + $promise = $this->api->{$this->call}->get(...$args); } return $promise; diff --git a/lib/API/Call/Deferred.php b/lib/API/Call/Deferred.php index 9bdc030..562978c 100644 --- a/lib/API/Call/Deferred.php +++ b/lib/API/Call/Deferred.php @@ -3,16 +3,13 @@ namespace seekat\API\Call; use AsyncInterop\Promise; -use Exception; use http\{ Client, Client\Request, Client\Response }; use Psr\Log\LoggerInterface; use seekat\API; -use SplObserver; -use SplSubject; -final class Deferred implements SplObserver +final class Deferred { /** * The response importer @@ -94,7 +91,6 @@ final class Deferred implements SplObserver /* we did finish in the meantime */ $this->complete(); } else { - $this->client->detach($this); $this->client->dequeue($this->request); ($this->reject)("Cancelled"); } @@ -102,7 +98,6 @@ final class Deferred implements SplObserver $this->promise = $future->getPromise($context); $this->resolve = API\Future\resolver($future, $context); $this->reject = API\Future\rejecter($future, $context); - $this->update = API\Future\updater($future, $context); } function __invoke() : Promise { @@ -115,7 +110,6 @@ final class Deferred implements SplObserver $this->response = $cached; $this->complete(); } else { - $this->client->attach($this); $this->client->enqueue($this->request, function(Response $response) use($cached) { if ($response->getResponseCode() == 304) { $this->response = $cached; @@ -136,31 +130,10 @@ final class Deferred implements SplObserver return $this->promise; } - /** - * Progress observer - * - * Import the response's data on success and resolve the promise. - * - * @param SplSubject $client The observed HTTP client - * @param Request $request The request which generated the update - * @param object $progress The progress information - */ - function update(SplSubject $client, Request $request = null, $progress = null) { - if ($request !== $this->request) { - return; - } - - ($this->update)((object) compact("client", "request", "progress")); - } - /** * Completion callback - * @param callable $resolve - * @param callable $reject */ private function complete() { - $this->client->detach($this); - if ($this->response) { try { $api = ($this->result)($this->response); @@ -168,7 +141,7 @@ final class Deferred implements SplObserver $this->cache->save($this->request, $this->response); ($this->resolve)($api); - } catch (Exception $e) { + } catch (\Throwable $e) { ($this->reject)($e); } } else { diff --git a/lib/API/Call/Result.php b/lib/API/Call/Result.php index 22ce2ef..6e1d8f8 100644 --- a/lib/API/Call/Result.php +++ b/lib/API/Call/Result.php @@ -24,16 +24,7 @@ final class Result $links = $this->checkResponseMeta($response); $type = $this->checkResponseType($response); - - try { - $data = $type->parseBody($response->getBody()); - } catch (\Exception $e) { - $this->api->getLogger()->error("response -> error: ".$e->getMessage(), [ - "url" => (string) $this->api->getUrl(), - ]); - - throw $e; - } + $data = $this->checkResponseBody($response, $type); return $this->api = $this->api->with(compact("type", "data", "links")); } @@ -74,4 +65,18 @@ final class Result return new API\ContentType($type); } + + private function checkResponseBody(Response $response, API\ContentType $type) { + try { + $data = $type->parseBody($response->getBody()); + } catch (\Exception $e) { + $this->api->getLogger()->error("response -> error: ".$e->getMessage(), [ + "url" => (string) $this->api->getUrl(), + ]); + + throw $e; + } + + return $data; + } } diff --git a/lib/API/Consumer.php b/lib/API/Consumer.php index 6e677fe..d593588 100644 --- a/lib/API/Consumer.php +++ b/lib/API/Consumer.php @@ -2,22 +2,21 @@ namespace seekat\API; +use AsyncInterop\Promise; use Generator; use http\Client; -use React\Promise\{ - Deferred, - ExtendedPromiseInterface, - PromiseInterface, - function all +use seekat\API; +use seekat\Exception\{ + InvalidArgumentException, UnexpectedValueException, function exception }; -final class Consumer extends Deferred +final class Consumer { /** - * The HTTP client - * @var Client + * Loop + * @var callable */ - private $client; + private $loop; /** * The return value of the generator @@ -32,24 +31,49 @@ final class Consumer extends Deferred private $cancelled = false; /** - * Create a new generator invoker - * @param Client $client + * @var Promise */ - function __construct(Client $client) { - $this->client = $client; + private $promise; - parent::__construct(function($resolve, $reject) { - return $this->cancel($resolve, $reject); + /** + * @var \Closure + */ + private $resolve; + + /** + * @var \Closure + */ + private $reject; + + /** + * @var \Closure + */ + private $reduce; + + /** + * Create a new generator consumer + * @param Future $future + * @param callable $loop + */ + function __construct(Future $future, callable $loop) { + $this->loop = $loop; + + $context = $future->createContext(function() { + $this->cancelled = true; }); + $this->promise = $future->getPromise($context); + $this->resolve = API\Future\resolver($future, $context); + $this->reject = API\Future\rejecter($future, $context); + $this->reduce = API\Future\reducer($future, $context); } /** * Iterate over $gen, a \Generator yielding promises * * @param Generator $gen - * @return ExtendedPromiseInterface + * @return Promise */ - function __invoke(Generator $gen) : ExtendedPromiseInterface { + function __invoke(Generator $gen) : Promise { $this->cancelled = false; foreach ($gen as $promise) { @@ -59,50 +83,47 @@ final class Consumer extends Deferred $this->give($promise, $gen); } + #($this->loop)(); + if (!$this->cancelled) { - $this->resolve($this->result = $gen->getReturn()); + $this->result = $gen->getReturn(); + } + if (isset($this->result)) { + ($this->resolve)($this->result); + } else { + ($this->reject)("Cancelled"); } - return $this->promise(); + return $this->promise; } /** * Promise handler * - * @param array|PromiseInterface $promise + * @param array|Promise $promise * @param Generator $gen */ private function give($promise, Generator $gen) { - if ($promise instanceof PromiseInterface) { - $promise->then(function($result) use($gen) { - if (($promise = $gen->send($result))) { - $this->give($promise, $gen); + if ($promise instanceof \Traversable) { + $promise = iterator_to_array($promise); + } + if (is_array($promise)) { + $promise = ($this->reduce)($promise); + } + if ($promise instanceof Promise) { + $promise->when(function($error, $result) use($gen) { + if ($error) { + $gen->throw(exception($error)); } - }); - } else { - all($promise)->then(function($results) use($gen) { - if (($promise = $gen->send($results))) { + if (($promise = $gen->send($result))) { $this->give($promise, $gen); } }); - } - $this->client->send(); - } - - /** - * Cancellation callback - * - * @param callable $resolve - * @param callable $reject - */ - private function cancel(callable $resolve, callable $reject) { - $this->cancelled = true; - - /* did we finish in the meantime? */ - if ($this->result) { - $resolve($this->result); } else { - $reject("Cancelled"); + $gen->throw(new UnexpectedValueException( + "Expected Promise or array of Promises; got ".\seekat\typeof($promise))); } + /* FIXME: external loop */ + ($this->loop)(); } } diff --git a/lib/API/ContentType.php b/lib/API/ContentType.php index 3ce123d..d13b11a 100644 --- a/lib/API/ContentType.php +++ b/lib/API/ContentType.php @@ -123,7 +123,7 @@ final class ContentType * @throws UnexpectedValueException */ private static function fromBase64(Body $base64) : string { - if (false === ($decoded = base64_decode($base64))) { + if (false === ($decoded = base64_decode($base64, true))) { throw new UnexpectedValueException("Could not decode BASE64"); } return $decoded; diff --git a/lib/API/Future.php b/lib/API/Future.php index 8c3571c..2b53ea1 100644 --- a/lib/API/Future.php +++ b/lib/API/Future.php @@ -18,6 +18,12 @@ interface Future */ function getPromise($context) : Promise; + /** + * @param Promise $promise + * @return bool + */ + function cancelPromise(Promise $promise) : bool; + /** * @param object $context Promisor returned by createContext * @param mixed $value @@ -34,8 +40,8 @@ interface Future /** * @param object $context Promisor returned by createContext - * @param mixed $update - * @return void + * @param array $promises + * @return Promise */ - function onUpdate($context, $update); + function onMultiple($context, array $promises) : Promise; } diff --git a/lib/API/Future/functions.php b/lib/API/Future/functions.php index 34d38c5..b6e8908 100644 --- a/lib/API/Future/functions.php +++ b/lib/API/Future/functions.php @@ -4,7 +4,6 @@ namespace seekat\API\Future; use Amp\Deferred as AmpDeferred; use AsyncInterop\Promise; -use Icicle\Awaitable\Deferred as IcicleDeferred; use React\Promise\Deferred as ReactDeferred; use seekat\API\Future; @@ -57,9 +56,9 @@ function rejecter(Future $future, $context) { * @param mixed $context Promisor * @return \Closure */ -function updater(Future $future, $context) { - return function($update) use($future, $context) { - return $future->onUpdate($context, $update); +function reducer(Future $future, $context) { + return function(array $promises) use($future, $context) : Promise { + return $future->onMultiple($context, $promises); }; } @@ -81,6 +80,12 @@ function react() { return $context->promise(); } + function cancelPromise(Promise $promise) : bool { + /* @var $promise \React\Promise\Promise */ + $promise->cancel(); + return true; + } + function onSuccess($context, $value) { /* @var $context ReactDeferred */ $context->resolve($value); @@ -91,9 +96,8 @@ function react() { $context->reject($reason); } - function onUpdate($context, $update) { - /* @var $context ReactDeferred */ - $context->notify($update); + function onMultiple($context, array $promises) : Promise { + return \React\Promise\all($promises); } }; } @@ -115,54 +119,22 @@ function amp() { return $context->promise(); } - function onSuccess($context, $value) { - /* @var $context AmpDeferred */ - $context->resolve($value); - } - - function onFailure($context, $reason) { - /* @var $context AmpDeferred */ - $context->fail($reason); - } - - function onUpdate($context, $update) { - /* @var $context AmpDeferred */ - /* noop */ - } - }; -} - -/** - * @return Future - */ -function icicle() { - return new class implements Future { - /** - * @param callable|null $onCancel - * @return IcicleDeferred - */ - function createContext(callable $onCancel = null) { - return new IcicleDeferred($onCancel); - } - - function getPromise($context): Promise { - /* @var $context IcicleDeferred */ - return $context->getPromise(); + function cancelPromise(Promise $promise) : bool { + return false; } function onSuccess($context, $value) { - /* @var $context IcicleDeferred */ + /* @var $context AmpDeferred */ $context->resolve($value); } function onFailure($context, $reason) { - /* @var $context IcicleDeferred */ - $context->reject($reason); + /* @var $context AmpDeferred */ + $context->fail(\seekat\Exception\exception($reason)); } - function onUpdate($context, $update) { - /* @var $context IcicleDeferred */ - /* noop */ + function onMultiple($context, array $promises) : Promise { + return \Amp\all($promises); } }; } diff --git a/lib/API/Links/functions.php b/lib/API/Links/functions.php index eb9b495..f740851 100644 --- a/lib/API/Links/functions.php +++ b/lib/API/Links/functions.php @@ -17,7 +17,7 @@ function first(API $api, Cache\Service $cache = null) : Promise { if ($links && ($first = $links->getFirst())) { return $api->withUrl($first)->get(null, null, $cache); } - return Future\reject($api->getFuture(), $links); + return Future\resolve($api->getFuture(), null); } /** @@ -30,7 +30,7 @@ function prev(API $api, Cache\Service $cache = null) : Promise { if ($links && ($prev = $links->getPrev())) { return $api->withUrl($prev)->get(null, null, $cache); } - return Future\reject($api->getFuture(), $links); + return Future\resolve($api->getFuture(), null); } /** @@ -43,7 +43,7 @@ function next(API $api, Cache\Service $cache = null) : Promise { if ($links && ($next = $links->getNext())) { return $api->withUrl($next)->get(null, null, $cache); } - return Future\reject($api->getFuture(), $links); + return Future\resolve($api->getFuture(), null); } /** @@ -56,6 +56,6 @@ function last(API $api, Cache\Service $cache = null) : Promise { if ($links && ($last = $links->getLast())) { return $api->withUrl($last)->get(null, null, $cache); } - return Future\reject($api->getFuture(), $links); + return Future\resolve($api->getFuture(), null); } diff --git a/lib/Exception/RequestException.php b/lib/Exception/RequestException.php index 575097f..3ef263c 100644 --- a/lib/Exception/RequestException.php +++ b/lib/Exception/RequestException.php @@ -60,6 +60,13 @@ class RequestException extends \Exception implements Exception return $this->errors; } + /** + * @return Response + */ + function getResponse() : Response { + return $this->response; + } + /** * Combine any errors into a single string * @staticvar array $reasons diff --git a/lib/Exception/functions.php b/lib/Exception/functions.php index 08c8ddb..9045cb3 100644 --- a/lib/Exception/functions.php +++ b/lib/Exception/functions.php @@ -2,6 +2,20 @@ namespace seekat\Exception; +/** + * @param $message + * @return \Throwable + */ +function exception(&$message) : \Throwable { + if ($message instanceof \Throwable){ + $exception = $message; + $message = $exception->getMessage(); + } else { + $exception = new \Exception($message); + } + return $exception; +} + /** * Canonical error message from a string or Exception * @param string|Exception $error @@ -12,7 +26,7 @@ function message(&$error) : string { $message = $error->getMessage(); } else { $message = $error; - $error = new \Exception($error); + $error = new \Exception($message); } return $message; } diff --git a/peridot.php b/peridot.php index 77d617f..066dca7 100644 --- a/peridot.php +++ b/peridot.php @@ -1,72 +1,25 @@ register(); - - $emitter->on('peridot.reporters', function(InputInterface $input, ReporterFactory $reporterFactory) { - $reporterFactory->register( - 'seekat', - 'Spec + Text Code coverage reporter', - function(AnonymousReporter $ar) use ($reporterFactory) { - - return new class($reporterFactory, $ar->getConfiguration(), $ar->getOutput(), $ar->getEventEmitter()) extends AbstractBaseReporter { - private $reporters = []; - private $factory; - - function __construct(ReporterFactory $factory, Configuration $configuration, OutputInterface $output, EventEmitter $eventEmitter) { - $this->factory = $factory; - parent::__construct($configuration, $output, $eventEmitter); - } - - function init() { - fprintf(STDERR, "Creating reporters\n"); - $this->reporters[] = $this->factory->create("spec"); - if (extension_loaded("xdebug")) { - $this->reporters[] = $this->factory->create("text-code-coverage"); - } - } - - function X__call($method, array $args) { - fprintf(STDERR, "Calling %s\n", $method); - foreach ($this->reporters as $reporter) { - $output = $reporter->$method(...$args); - } - return $output; - } - }; - } - ); - }); - - $emitter->on('code-coverage.start', function (AbstractCodeCoverageReporter $reporter) { - $reporter->addDirectoryToWhitelist(__DIR__."/lib") - ->addDirectoryToWhitelist(__DIR__."/tests"); - }); +return function(\Peridot\EventEmitterInterface $emitter) { + Scenarios\Plugin::register($emitter); $emitter->on("peridot.start", function(Environment $env, Application $app) { $app->setCatchExceptions(false); $definition = $env->getDefinition(); $definition->getArgument("path") ->setDefault(implode(" ", glob("tests/*"))); - $definition->getOption("reporter") - ->setDefault("seekat"); }); $log = new class extends AbstractProcessingHandler { @@ -85,7 +38,7 @@ return function(EventEmitter $emitter) { } } }; - $emitter->on("suite.start", function(Suite $suite) use($log) { + $emitter->on("suite.start", function(Suite $suite) use(&$headers, $log) { $headers = []; if (($token = getenv("GITHUB_TOKEN"))) { $headers["Authorization"] = "token $token"; @@ -98,7 +51,10 @@ return function(EventEmitter $emitter) { } else { throw new Exception("GITHUB_TOKEN is not set in the environment"); } - $suite->getScope()->api = new API($headers, null, null, new Logger("seekat", [$log])); + $suite->getScope()->amp = new API(API\Future\amp(), + $headers, null, null, new Logger("amp", [$log])); + $suite->getScope()->react = new API(API\Future\react(), + $headers, null, null, new Logger("react", [$log])); }); $emitter->on("test.failed", function(Test $test, \Throwable $e) { diff --git a/tests/APITest.php b/tests/APITest.php new file mode 100644 index 0000000..9f17af6 --- /dev/null +++ b/tests/APITest.php @@ -0,0 +1,73 @@ +provideAPI() as $name => list($api)) { + foreach ($methods as $method) { + $data["$method $name"] = [$api, $method]; + } + } + return $data; + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testReturnsApiOnPropertyAccess($api) { + $this->assertInstanceOf(API::class, $api->foo); + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testReturnsCloneOnPropertyAccess($api) { + $this->assertNotEquals($api, $api->bar); + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testReturnsPromiseOnMethodCall($api) { + $this->assertInstanceOf(Promise::class, $api->baz()); + } + + /** + * @dataProvider provideHttpMethodAndAPI + */ + function testProvidesMethodForStandardHttpMethod($api, $method) { + $this->assertTrue(method_exists($api, $method)); + } + + /** + * @dataProvider provideHttpMethodAndAPI + */ + function testReturnsPromiseOnMethodCallForStandardHttpMethod($api, $method) { + $this->assertInstanceOf(Promise::class, $api->$method()); + } + + /** + * @group testdox + * @dataProvider provideAPI + * @depends testProvidesMethodForStandardHttpMethod + * @depends testReturnsPromiseOnMethodCallForStandardHttpMethod + */ + function testProvidesMethodsForStandardHttpMethods($api) { + $this->assertTrue(true); + } +} diff --git a/tests/CacheTest.php b/tests/CacheTest.php new file mode 100644 index 0000000..6ceffbd --- /dev/null +++ b/tests/CacheTest.php @@ -0,0 +1,41 @@ +cache = new seekat\API\Call\Cache\Service\Hollow; + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testCachesSuccessiveCalls($api) { + $m6w6 = $this->assertSuccess($api->users->m6w6, null, null, $this->cache); + $data = $this->cache->getStorage(); + $m6w6_ = $this->assertSuccess($api->users->m6w6, null, null, $this->cache); + + $this->assertEquals("m6w6", $m6w6->login); + $this->assertEquals("m6w6", $m6w6_->login); + + $this->assertInternalType("array", $data); + $this->assertCount(1, $data); + $this->assertEquals($data, $this->cache->getStorage()); + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testRefreshesStaleCacheEntries($api) { + $this->markTestIncomplete("TODO"); + } +} diff --git a/tests/CallTest.php b/tests/CallTest.php new file mode 100644 index 0000000..4227e1e --- /dev/null +++ b/tests/CallTest.php @@ -0,0 +1,119 @@ +assertSuccess($api->users->m6w6); + $this->assertInstanceOf(API::class, $m6w6); + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testExportsArrayWithKeysDataAndUrlAndTypeAndLinksAndHeaders($api) { + $m6w6 = $this->assertSuccess($api->users->m6w6); + $export = $m6w6->export(); + $this->assertArrayHasKey("data", $export); + $this->assertArrayHasKey("url", $export); + $this->assertArrayHasKey("type", $export); + $this->assertArrayHasKey("links", $export); + $this->assertArrayHasKey("headers", $export); + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testFetchedDataIsAccessibleOnPropertyAccess($api) { + $m6w6 = $this->assertSuccess($api->users->m6w6); + $this->assertEquals("m6w6", $m6w6->login); + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testFetchedDataIsAccessibleOnPropertyAccessDespiteUrlSuffixAvailable($api) { + $m6w6 = $this->assertSuccess($api->users->m6w6); + $this->assertGreaterThan(0, (int) (string) $m6w6->followers); + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testFetchUrlSuffix($api) { + $m6w6 = $this->assertSuccess($api->users->m6w6); + $followers = $this->assertSuccess($api->users->m6w6->followers); + $data = $followers->export()["data"]; + $this->assertInternalType("array", $data); + $this->assertInternalType("object", $data[0]); + $this->assertInternalType("object", $followers->{0}); + $this->assertGreaterThan(30, (string) $m6w6->followers); + $this->assertGreaterThan(0, count($followers)); + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testFetchExplicitUrlSuffix($api) { + $m6w6 = $this->assertSuccess($api->users->m6w6); + $followers = $this->assertSuccess($m6w6->followers_url); + $data = $followers->export()["data"]; + $this->assertInternalType("array", $data); + $this->assertInternalType("object", $data[0]); + $this->assertInternalType("object", $followers->{0}); + $this->assertGreaterThan(30, (string) $m6w6->followers); + $this->assertGreaterThan(0, count($followers)); + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testFetchImplicitUrlSuffix($api) { + $m6w6 = $this->assertSuccess($api->users->m6w6); + $promise = $m6w6->followers(); + $this->consumePromise($promise, $errors, $results); + $api->send(); + $this->assertEmpty($errors); + $this->assertNotEmpty($results); + $followers = $results[0]; + $this->assertInstanceOf(API::class, $followers); + $data = $followers->export()["data"]; + $this->assertInternalType("array", $data); + $this->assertGreaterThan(0, count($data)); + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testFetchParallelFromIterator($api) { + $m6w6 = $this->assertSuccess($api->users->m6w6); + foreach ($m6w6 as $key => $val) { + switch ($key) { + case "html_url": + case "avatar_url": + break; + default: + if (substr($key, -4) === "_url") { + $batch[] = $val; + } + } + } + $results = $this->assertAllSuccess($batch); + $this->assertGreaterThan(2, $results); + } +} diff --git a/tests/ContentTypeTest.php b/tests/ContentTypeTest.php new file mode 100644 index 0000000..2bdeb48 --- /dev/null +++ b/tests/ContentTypeTest.php @@ -0,0 +1,162 @@ +assertEquals( + $this->getVersionedContentType("json")->value, + $api->export()["headers"]["Accept"]); + + $api = ContentType::apply($api, "+raw"); + $this->assertEquals( + $this->getVersionedContentType("+raw")->value, + $api->export()["headers"]["Accept"]); + + $api = ContentType::apply($api, ".html"); + $this->assertEquals( + $this->getVersionedContentType(".html")->value, + $api->export()["headers"]["Accept"]); + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testIsAbleToApplyBasicContentTypeToApi($api) { + $api = ContentType::apply($api, "text/plain"); + $this->assertEquals("text/plain", $api->export()["headers"]["Accept"]); + } + + /** + * @group testdox + */ + function testUserCanOverrideApiVersion() { + $this->assertEquals(3, ContentType::version(2)); + $this->assertEquals(2, ContentType::version(3)); + } + + /** + * @group testdox + */ + function testAllowsToRegisterAndUnregisterContentTypeHandlers() { + $this->assertFalse(ContentType::registered("foobar")); + ContentType::register("foobar", function() {}); + $this->assertTrue(ContentType::registered("foobar")); + ContentType::unregister("foobar"); + $this->assertFalse(ContentType::registered("foobar")); + } + + /** + * @group testdox + */ + function testAcceptsContentTypeHeader() { + new ContentType(new Header("Content-Type")); + new ContentType(new Header("content-type")); + } + + /** + * @group testdox + * @expectedException \seekat\Exception\InvalidArgumentException + */ + function testDoesNotAcceptNonContentTypeHeader() { + new ContentType(new Header("ContentType")); + } + + /** + * @group testdox + * @expectedException \seekat\Exception\UnexpectedValueException + */ + function testThrowsOnUnknownContentType() { + $ct = new ContentType($this->getVersionedContentType("foo")); + $ct->parseBody((new Body)->append("foo")); + } + + /** + * @group testdox + */ + function testHandlesJson() { + $this->assertTrue(ContentType::registered("json")); + $ct = new ContentType(new Header("Content-Type", "application/json")); + $result = $ct->parseBody((new Body())->append("[1,2,3]")); + $this->assertEquals([1, 2, 3], $result); + return $ct; + } + + /** + * @group testdox + * @depends testHandlesJson + * @expectedException \seekat\Exception\UnexpectedValueException + */ + function testThrowsOnInvalidJson(ContentType $ct) { + $ct->parseBody((new Body)->append("yaml:\n - data")); + } + + /** + * @group testdox + */ + function testHandlesBase64() { + $this->assertTrue(ContentType::registered("base64")); + $ct = new ContentType($this->getVersionedContentType("base64")); + $result = $ct->parseBody((new Body())->append(base64_encode("This is a test"))); + $this->assertEquals("This is a test", $result); + return $ct; + } + + /** + * @group testdox + * @depends testHandlesBase64 + * @expectedException \seekat\Exception\UnexpectedValueException + */ + function testThrowsOnInvalidBase64(ContentType $ct) { + $ct->parseBody((new Body)->append("[1,2,3]")); + } + + /** + * @group testdox + */ + function testHandlesOctetStream() { + $this->assertTrue(ContentType::registered("application/octet-stream")); + $ct = new ContentType(new Header("Content-Type", "application/octet-stream")); + $result = $ct->parseBody((new Body)->append("This is a test")); + $this->assertInternalType("resource", $result); + rewind($result); + $this->assertEquals("This is a test", stream_get_contents($result)); + } + + /** + * @group testdox + */ + function testHandlesData() { + $this->assertTrue(ContentType::registered("text/plain")); + $ct = new ContentType(new Header("Content-Type", "text/plain")); + $result = $ct->parseBody((new Body)->append("This is a test")); + $this->assertInternalType("string", $result); + $this->assertEquals("This is a test", $result); + } + + /** + * @param string $type + * @return Header Content-Type header + */ + private function getVersionedContentType($type) { + switch ($type{0}) { + case ".": + case "+": + case "": + break; + default: + $type = ".$type"; + } + return new Header("Content-Type", + sprintf("application/vnd.github.v%d%s", ContentType::version(), $type)); + } +} diff --git a/tests/ErrorsTest.php b/tests/ErrorsTest.php new file mode 100644 index 0000000..b99fe92 --- /dev/null +++ b/tests/ErrorsTest.php @@ -0,0 +1,29 @@ +users->m6w6(); + + if (!$api->getFuture()->cancelPromise($promise)) { + return; + } + + $this->assertCancelled($promise); + } + + /** + * @dataProvider provideAPI + */ + function test404($api) { + $error = $this->assertFailure($api->generate->a404); + $this->assertEquals($error->getMessage(), "Not Found"); + } +} diff --git a/tests/GeneratorTest.php b/tests/GeneratorTest.php new file mode 100644 index 0000000..a4cb79a --- /dev/null +++ b/tests/GeneratorTest.php @@ -0,0 +1,51 @@ +users->m6w6->gists(); + $gists_count = count($gists); + foreach ($gists as $gist) { + $files_count += count($gist->files); + } + }); + $this->assertGreaterThan(0, $gists_count); + $this->assertGreaterThanOrEqual($gists_count, $files_count); + } + + /** + * @group testdox + * @dataProvider provideAPI + */ + function testIteratesOverAGeneratorOfPromisesUsingLinks($api) { + $promise = $api(function($api) use(&$repos, &$first, &$next, &$last, &$prev) { + $repos = yield $api->users->m6w6->repos(["per_page" => 1]); + $last = yield Links\last($repos); + $prev = yield Links\prev($last); + $next = yield Links\next($prev); + $first = yield Links\first($prev); + return -123; + }); + + $this->consumePromise($promise, $errors, $results); + $this->assertEmpty($errors); + $this->assertEquals([-123], $results); + + $first_data = $first->export()["data"]; + $next_data = $next->export()["data"]; + $last_data = $last->export()["data"]; + $repos_data = $repos->export()["data"]; + + $this->assertEquals($repos_data, $first_data); + $this->assertEquals($last_data, $next_data); + } +} diff --git a/tests/api.php b/tests/api.php deleted file mode 100644 index 2ba9d04..0000000 --- a/tests/api.php +++ /dev/null @@ -1,214 +0,0 @@ -api->users)->to->be->instanceof(API::class); - }); - - it("should return a clone on property access", function() { - expect($this->api->users)->to->not->equal($this->api); - }); - - it("should return PromiseInterface on function call", function() { - expect($this->api->users->m6w6())->to->be->instanceof(PromiseInterface::class); - }); - }); - - describe("Requests", function() { - - it("should successfully request /users/m6w6", function() { - $this->api->users->m6w6()->then(function($json) use(&$m6w6) { - $m6w6 = $json; - }, function($error) use(&$errors) { - $errors[] = (string) $error; - }); - - $this->api->send(); - - expect($errors)->to->be->empty; - expect($m6w6->login)->to->loosely->equal("m6w6"); - }); - - it("should export an array of data, url, type, links and headers", function() { - $this->api->users->m6w6()->then(function($json) use(&$m6w6) { - $m6w6 = $json; - }, function($error) use(&$errors) { - $errors[] = (string) $error; - }); - - $this->api->send(); - - expect($errors)->to->be->empty(); - expect($m6w6->export())->to->be->an("array")->and->contain->keys([ - "data", "url", "type", "links", "headers" - ]); - }); - - it("should return the count of followers when accessing /users/m6w6->followers", function() { - $this->api->users->m6w6()->then(function($m6w6) use(&$followers) { - $followers = $m6w6->followers; - }, function($error) use(&$errors) { - $errors[] = (string) $error; - }); - - $this->api->send(); - - expect($errors)->to->be->empty(); - expect($followers->export()["data"])->to->be->an("integer"); - }); - - it("should fetch followers_url when accessing /users/m6w6->followers_url", function() { - $this->api->users->m6w6()->then(function($m6w6) use(&$followers, &$errors) { - $m6w6->followers_url()->then(function($json) use(&$followers) { - $followers = $json; - }, function($error) use(&$errors) { - $errors[] = (string) $error; - }); - }, function($error) use(&$errors) { - $errors[] = (string) $error; - }); - - $this->api->send(); - - expect($errors)->to->be->empty; - expect($followers->export()["data"])->to->be->an("array"); - expect(count($followers))->to->be->above(0); - }); - - it("should provide access to array indices", function() { - $this->api->users->m6w6()->then(function($m6w6) use(&$followers, &$errors) { - $m6w6->followers_url()->then(function($json) use(&$followers) { - $followers = $json; - }, function($error) use(&$errors) { - $errors[] = (string) $error; - }); - }, function($error) use(&$errors) { - $errors[] = (string) $error; - }); - - $this->api->send(); - - expect($errors)->to->be->empty; - expect($followers->{0})->to->be->an("object"); - expect($followers->export()["data"][0])->to->be->an("object"); - }); - - it("should handle a few requests in parallel", function() { - $this->api->users->m6w6()->then(function($m6w6) use(&$count, &$errors) { - foreach ($m6w6 as $key => $val) { - switch ($key) { - case "html_url": - case "avatar_url": - break; - default: - if (substr($key, -4) === "_url") { - $val->get()->then(function() use(&$count) { - ++$count; - }, function($error) use(&$errors) { - $errors[] = (string) $error; - }); - } - } - } - }, function($error) use(&$errors) { - $errors[] = (string) $error; - }); - - $this->api->send(); - - expect($errors)->to->be->empty; - expect($count)->to->be->above(2); - }); - - }); - - describe("Cache", function() { - it("should cache successive calls", function() { - $cache = new API\Call\Cache\Service\Hollow(); - $this->api->users->m6w6(null, null, $cache)->then(function($json) use(&$m6w6) { - $m6w6 = $json; - }, function($error) use(&$errors) { - $errors[] = (string) $error; - }); - - $this->api->send(); - - $data = $cache->getStorage(); - $this->api->users->m6w6(null, null, $cache)->then(function($json) use(&$m6w6_) { - $m6w6_ = $json; - }, function($error) use(&$errors) { - $errors[] = (string) $error; - }); - - $this->api->send(); - - expect($errors)->to->be->empty; - expect($m6w6->login)->to->loosely->equal("m6w6"); - expect($m6w6_->login)->to->loosely->equal("m6w6"); - expect($data)->to->equal($cache->getStorage()); - expect(count($cache->getStorage()))->to->equal(1); - }); - xit("should refresh stale cache entries"); - }); - - describe("Generators", function() { - it("should iterate over a generator of promises", function() { - ($this->api)(function($api) use(&$gists_count, &$files_count) { - $gists = yield $api->users->m6w6->gists(); - $gists_count = count($gists); - foreach ($gists as $gist) { - $files_count += count($gist->files); - } - }); - expect($gists_count)->to->be->above(0); - expect($files_count)->to->be->at->least($gists_count); - }); - it("should iterate over a generator of promises with links", function() { - ($this->api)(function($api) use(&$repos, &$first, &$next, &$last, &$prev) { - $repos = yield $api->users->m6w6->repos(["per_page" => 1]); - $last = yield last($repos); - $prev = yield prev($last); - $next = yield next($prev); - $first = yield first($prev); - return -123; - })->done(function($value) use(&$result) { - $result = $value; - }); - - expect($result)->to->equal(-123); - expect($repos->export()["data"])->to->loosely->equal($first->export()["data"]); - expect($last->export()["data"])->to->loosely->equal($next->export()["data"]); - }); - }); - - describe("Errors", function() { - it("should handle cancellation gracefully", function() { - $this->api->users->m6w6()->then(function($value) use(&$result) { - $result = $value; - }, function($error) use(&$message) { - $message = \seekat\Exception\message($error); - })->cancel(); - expect($result)->to->be->empty(); - expect($message)->to->equal("Cancelled"); - }); - - it("should handle request errors gracefully", function() { - $this->api->generate->a404()->then(function($value) use(&$result) { - $result = $value; - }, function($error) use(&$message) { - $message = \seekat\Exception\message($error); - }); - $this->api->send(); - expect($result)->to->be->empty(); - expect($message)->to->equal("Not Found"); - }); - }); -}); diff --git a/tests/bootstrap.php b/tests/bootstrap.php new file mode 100644 index 0000000..3d502be --- /dev/null +++ b/tests/bootstrap.php @@ -0,0 +1,196 @@ + [new \seekat\API(\seekat\API\Future\react(), $headers, $url, $client, $logger)], + "with AmPHP" => [new \seekat\API(\seekat\API\Future\amp(), $headers, $url, $client, $logger)], + ]; + } +} + +trait ConsumePromise +{ + function consumePromise(\AsyncInterop\Promise $p, &$errors, &$results) { + $p->when(function($error, $result) use(&$errors, &$results) { + if ($error) $errors[] = $error; + if ($result) $results[] = $result; + }); + } +} + +trait AssertSuccess +{ + function assertAllSuccess(array $apis, ...$args) { + foreach ($apis as $api) { + $this->consumePromise($api->get(...$args), $errors, $results); + } + $api->send(); + $this->assertEmpty($errors, "errors"); + $this->assertNotEmpty($results, "results"); + return $results; + } + + function assertSuccess(seekat\API $api, ...$args) { + $this->consumePromise($api->get(...$args), $errors, $results); + $api->send(); + $this->assertEmpty($errors, "errors"); + $this->assertNotEmpty($results, "results"); + return $results[0]; + } +} + +trait AssertCancelled +{ + function assertCancelled(\AsyncInterop\Promise $promise) { + $this->consumePromise($promise, $errors, $results); + + $this->assertEmpty($results); + $this->assertStringMatchesFormat("%SCancelled%S", $errors[0]->getMessage()); + } +} + +trait AssertFailure +{ + function assertFailure(seekat\API $api, ...$args) { + $this->consumePromise($api->get(...$args), $errors, $results); + $api->send(); + $this->assertNotEmpty($errors, "errors"); + $this->assertEmpty($results, "results"); + return $errors[0]; + } +} + +class CombinedTestdoxPrinter extends PHPUnit_TextUI_ResultPrinter +{ + function isTestClass(PHPUnit_Framework_TestSuite $suite) { + $suiteName = $suite->getName(); + return false === strpos($suiteName, "::") + && substr($suiteName, -4) === "Test"; + } + + function startTestSuite(PHPUnit_Framework_TestSuite $suite) { + if ($this->isTestClass($suite)) { + $this->column = 0; + } + + return parent::startTestSuite($suite); + } + + function endTestSuite(PHPUnit_Framework_TestSuite $suite) { + /* print % progress */ + if ($this->isTestClass($suite)) { + if ($this->numTestsRun != $this->numTests) { + $colWidth = $this->maxColumn - $this->column; + $this->column = $this->maxColumn - 1; + + --$this->numTestsRun; + $this->writeProgress(str_repeat(" ", $colWidth)); + } else { + $this->writeNewLine(); + } + } + + parent::endTestSuite($suite); + } +} + +class TestdoxListener extends PHPUnit_Util_TestDox_ResultPrinter_Text +{ + private $groups; + + function __construct() { + parent::__construct("php://stdout", ["testdox"]); + $this->groups = new ReflectionProperty("PHPUnit_Util_TestDox_ResultPrinter", "groups"); + $this->groups->setAccessible(true); + } + + function startTest(PHPUnit_Framework_Test $test) { + /* always show test class, even if no testdox test */ + if ($test instanceof \PHPUnit\Framework\TestCase) { + if ($test->getGroups() == ["default"]) { + $this->groups->setValue($this, ["default"]); + } + } + + parent::startTest($test); + $this->groups->setValue($this, ["testdox"]); + + } +} + +class DebugLogListener extends PHPUnit\Framework\BaseTestListener +{ + private $printLog = false; + + function endTest(PHPUnit_Framework_Test $test, $time) { + /* @var $handler \Monolog\Handler\FingersCrossedHandler */ + $handler = logger()->getHandlers()[0]; + if ($this->printLog) { + $this->printLog = false; + $handler->activate(); + } else { + $handler->clear(); + } + } + + function addError(PHPUnit_Framework_Test $test, Exception $e, $time) { + $this->printLog = true; + } + + function addFailure(PHPUnit_Framework_Test $test, PHPUnit_Framework_AssertionFailedError $e, $time) { + $this->printLog = true; + } + +}