},
"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",
"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"
}
}
}
};
-$api = new seekat\API([
+$api = new seekat\API(seekat\API\Future\react(), [
"Authorization" => "token ".getenv("GITHUB_TOKEN")
], null, null, $log, $cache);
+#!/usr/bin/env php
<?php
require_once __DIR__."/../vendor/autoload.php";
-$api = new seekat\API([
+$api = new seekat\API(seekat\API\Future\react(), [
"Authorization" => "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();
$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);
$events = yield $next;
}
return $count;
-})->done(function($count) {
+})->when(function($error, $count) {
printf("Listed %d events\n", $count);
});
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();
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";
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([
"affiliation" => "owner"
]);
while ($repos) {
- $next = next($repos);
+ $next = Links\next($repos);
$batch = [];
foreach ($repos as $repo) {
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 ?: "<no title>");
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();
});
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
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) {
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");
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
*
}
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;
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
/* we did finish in the meantime */
$this->complete();
} else {
- $this->client->detach($this);
$this->client->dequeue($this->request);
($this->reject)("Cancelled");
}
$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 {
$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;
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);
$this->cache->save($this->request, $this->response);
($this->resolve)($api);
- } catch (Exception $e) {
+ } catch (\Throwable $e) {
($this->reject)($e);
}
} else {
$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"));
}
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;
+ }
}
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
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) {
$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)();
}
}
* @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;
*/
function getPromise($context) : Promise;
+ /**
+ * @param Promise $promise
+ * @return bool
+ */
+ function cancelPromise(Promise $promise) : bool;
+
/**
* @param object $context Promisor returned by createContext
* @param mixed $value
/**
* @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;
}
use Amp\Deferred as AmpDeferred;
use AsyncInterop\Promise;
-use Icicle\Awaitable\Deferred as IcicleDeferred;
use React\Promise\Deferred as ReactDeferred;
use seekat\API\Future;
* @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);
};
}
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);
$context->reject($reason);
}
- function onUpdate($context, $update) {
- /* @var $context ReactDeferred */
- $context->notify($update);
+ function onMultiple($context, array $promises) : Promise {
+ return \React\Promise\all($promises);
}
};
}
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);
}
};
}
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);
}
/**
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);
}
/**
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);
}
/**
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);
}
return $this->errors;
}
+ /**
+ * @return Response
+ */
+ function getResponse() : Response {
+ return $this->response;
+ }
+
/**
* Combine any errors into a single string
* @staticvar array $reasons
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
$message = $error->getMessage();
} else {
$message = $error;
- $error = new \Exception($error);
+ $error = new \Exception($message);
}
return $message;
}
<?php
-use Evenement\EventEmitterInterface as EventEmitter;
use Monolog\Handler\AbstractProcessingHandler;
use Monolog\Logger;
+use Peridot\Cli\Environment;
+use Peridot\Cli\Application;
use Peridot\Configuration;
-use Peridot\Console\Application;
-use Peridot\Console\Environment;
use Peridot\Core\Suite;
use Peridot\Core\Test;
-use Peridot\Reporter\AbstractBaseReporter;
-use Peridot\Reporter\AnonymousReporter;
-use Peridot\Reporter\CodeCoverage\AbstractCodeCoverageReporter;
-use Peridot\Reporter\CodeCoverageReporters;
-use Peridot\Reporter\ReporterFactory;
+use Peridot\Plugin\Scenarios;
use seekat\API;
use Symfony\Component\Console\Input\InputInterface;
use Symfony\Component\Console\Output\OutputInterface;
-return function(EventEmitter $emitter) {
- (new CodeCoverageReporters($emitter))->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 {
}
}
};
- $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";
} 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) {
--- /dev/null
+<?php
+
+use AsyncInterop\Promise;
+use seekat\API;
+
+class APITest extends BaseTest
+{
+ function provideHttpMethodAndAPI() {
+ $data = [];
+ $methods = [
+ "HEAD",
+ "GET",
+ "POST",
+ "PUT",
+ "DELETE",
+ "PATCH"
+ ];
+ foreach ($this->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);
+ }
+}
--- /dev/null
+<?php
+
+class CacheTest extends BaseTest
+{
+ use ConsumePromise;
+ use AssertSuccess;
+
+ /**
+ * @var seekat\API\Cache\Service
+ */
+ private $cache;
+
+ function setUp() {
+ $this->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");
+ }
+}
--- /dev/null
+<?php
+
+use seekat\API;
+
+class CallTest extends BaseTest
+{
+ use ConsumePromise;
+ use AssertSuccess;
+
+ /**
+ * @group testdox
+ * @dataProvider provideAPI
+ */
+ function testResolvesPromiseWithResultAfterCallCompletes($api) {
+ $m6w6 = $this->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);
+ }
+}
--- /dev/null
+<?php
+
+use seekat\API\ContentType;
+use http\Header;
+use http\Message\Body;
+
+class ContentTypeTest extends BaseTest
+{
+ /**
+ * @group testdox
+ * @dataProvider provideAPI
+ */
+ function testIsAbleToApplyVersionedContentTypeToApi($api) {
+ $api = ContentType::apply($api, "json");
+ $this->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));
+ }
+}
--- /dev/null
+<?php
+
+class ErrorsTest extends BaseTest
+{
+ use ConsumePromise;
+ use AssertCancelled;
+ use AssertFailure;
+
+ /**
+ * @dataProvider provideAPI
+ */
+ function testCancellation($api) {
+ $promise = $api->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");
+ }
+}
--- /dev/null
+<?php
+
+use seekat\API\Links;
+
+class GeneratorTest extends BaseTest
+{
+ use ConsumePromise;
+
+ /**
+ * @group testdox
+ * @dataProvider provideApi
+ */
+ function testIteratesOverAGeneratorOfPromises($api) {
+ $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);
+ }
+ });
+ $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);
+ }
+}
+++ /dev/null
-<?php
-
-use seekat\API;
-use React\Promise\PromiseInterface;
-use seekat\API\Links\ {
- function first, function last, function next, function prev
-};
-
-describe("API", function() {
-
- describe("Interface", function() {
- it("should return API on property access", function() {
- expect($this->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");
- });
- });
-});
--- /dev/null
+<?php
+
+require_once __DIR__."/../vendor/autoload.php";
+
+function headers() : array {
+ static $headers;
+
+ if (!isset($headers)) {
+ if (($token = getenv("GITHUB_TOKEN"))) {
+ $headers["Authorization"] = "token $token";
+ } elseif (function_exists("posix_isatty") && defined("STDIN") && posix_isatty(STDIN)) {
+ fprintf(STDOUT, "GITHUB_TOKEN is not set in the environment, enter Y to continue without: ");
+ fflush(STDOUT);
+ if (strncasecmp(fgets(STDIN), "Y", 1)) {
+ exit;
+ }
+ $headers = [];
+ } else {
+ throw new Exception("GITHUB_TOKEN is not set in the environment");
+ }
+ }
+
+ return $headers;
+}
+
+function logger() : \Monolog\Logger {
+ static $logger;
+
+ if (!isset($logger)) {
+ $logger = new \Monolog\Logger(
+ "test",
+ [
+ new \Monolog\Handler\FingersCrossedHandler(
+ new \Monolog\Handler\StreamHandler(STDERR),
+ \Monolog\Logger::EMERGENCY
+ )
+ ]
+ );
+ }
+
+ return $logger;
+}
+
+class BaseTest extends \PHPUnit\Framework\TestCase
+{
+ /**
+ * @return Generator
+ */
+ function provideAPI() {
+ $auth = \seekat\API\auth("token", getenv("GITHUB_TOKEN"));
+ $headers = headers();
+ $url = null;
+ $client = null;
+ $logger = logger();
+
+ return [
+ "with ReactPHP" => [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;
+ }
+
+}