diff --git a/src/AsyncTestCase.php b/src/AsyncTestCase.php index 4a352b3..8bad523 100644 --- a/src/AsyncTestCase.php +++ b/src/AsyncTestCase.php @@ -2,10 +2,14 @@ namespace Amp\PHPUnit; +use Amp\Coroutine; +use Amp\Failure; use Amp\Loop; +use Amp\Promise; +use Amp\Success; use PHPUnit\Framework\MockObject\MockObject; use PHPUnit\Framework\TestCase as PHPUnitTestCase; -use function Amp\call; +use React\Promise\PromiseInterface as ReactPromise; /** * A PHPUnit TestCase intended to help facilitate writing async tests by running each test as coroutine with Amp's @@ -30,11 +34,8 @@ abstract class AsyncTestCase extends PHPUnitTestCase /** @var bool */ private $setUpInvoked = false; - final protected function runTest() - { - parent::setName('runAsyncTest'); - return parent::runTest(); - } + /** @var \Generator|null */ + private $generator; /** @internal */ final public function runAsyncTest(...$args) @@ -55,7 +56,7 @@ final public function runAsyncTest(...$args) $start = \microtime(true); Loop::run(function () use (&$returnValue, &$exception, &$invoked, $args) { - $promise = call([$this, $this->realTestName], ...$args); + $promise = $this->call([$this, $this->realTestName], ...$args); $promise->onResolve(function ($error, $value) use (&$invoked, &$exception, &$returnValue) { $invoked = true; $exception = $error; @@ -67,6 +68,8 @@ final public function runAsyncTest(...$args) Loop::cancel($this->timeoutId); } + $this->generator = null; + if (isset($exception)) { throw $exception; } @@ -88,6 +91,12 @@ final public function runAsyncTest(...$args) return $returnValue; } + final protected function runTest() + { + parent::setName('runAsyncTest'); + return parent::runTest(); + } + /** * Fails the test if the loop does not run for at least the given amount of time. * @@ -113,13 +122,29 @@ final protected function setTimeout(int $timeout) Loop::stop(); Loop::setErrorHandler(null); + $additionalInfo = ''; + + if ($this->generator && $this->generator->valid()) { + $reflGen = new \ReflectionGenerator($this->generator); + $exeGen = $reflGen->getExecutingGenerator(); + if ($isSubgenerator = ($exeGen !== $this->generator)) { + $reflGen = new \ReflectionGenerator($exeGen); + } + + $additionalInfo .= \sprintf( + "\r\n\r\nTimeout reached on line %s in %s", + $reflGen->getExecutingLine(), + $reflGen->getExecutingFile() + ); + } + $loop = Loop::get(); if ($loop instanceof Loop\TracingDriver) { - $additionalInfo = "\r\n\r\n" . $loop->dump(); + $additionalInfo .= "\r\n\r\n" . $loop->dump(); } elseif (\class_exists(Loop\TracingDriver::class)) { - $additionalInfo = "\r\n\r\nSet AMP_DEBUG_TRACE_WATCHERS=true as environment variable to trace watchers keeping the loop running."; + $additionalInfo .= "\r\n\r\nSet AMP_DEBUG_TRACE_WATCHERS=true as environment variable to trace watchers keeping the loop running."; } else { - $additionalInfo = "\r\n\r\nInstall amphp/amp@^2.3 and set AMP_DEBUG_TRACE_WATCHERS=true as environment variable to trace watchers keeping the loop running. "; + $additionalInfo .= "\r\n\r\nInstall amphp/amp@^2.3 and set AMP_DEBUG_TRACE_WATCHERS=true as environment variable to trace watchers keeping the loop running. "; } $this->fail('Expected test to complete before ' . $timeout . 'ms time limit' . $additionalInfo); @@ -130,7 +155,7 @@ final protected function setTimeout(int $timeout) /** * @param int $invocationCount Number of times the callback must be invoked or the test will fail. - * @param callable|null $returnCallback Callable providing a return value for the callback. + * @param callable|null $returnCallback Callable providing a return value for the callback. * * @return callable|MockObject Mock object having only an __invoke method. */ @@ -146,4 +171,39 @@ final protected function createCallback(int $invocationCount, callable $returnCa return $mock; } + + /** + * Specialized Amp\call that stores the generator if present for debugging purposes. + * + * @param callable $callback + * @param mixed ...$args + * + * @return Promise + */ + private function call(callable $callback, ...$args): Promise + { + $this->generator = null; + + try { + $result = $callback(...$args); + } catch (\Throwable $exception) { + return new Failure($exception); + } + + if ($result instanceof \Generator) { + $this->generator = $result; + + return new Coroutine($result); + } + + if ($result instanceof Promise) { + return $result; + } + + if ($result instanceof ReactPromise) { + return Promise\adapt($result); + } + + return new Success($result); + } }