Skip to content

Commit 2e0a022

Browse files
authored
Provide information where timeout is reached in AsyncTestCase (#14)
1 parent bc4c6fd commit 2e0a022

File tree

1 file changed

+71
-11
lines changed

1 file changed

+71
-11
lines changed

src/AsyncTestCase.php

Lines changed: 71 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,14 @@
22

33
namespace Amp\PHPUnit;
44

5+
use Amp\Coroutine;
6+
use Amp\Failure;
57
use Amp\Loop;
8+
use Amp\Promise;
9+
use Amp\Success;
610
use PHPUnit\Framework\MockObject\MockObject;
711
use PHPUnit\Framework\TestCase as PHPUnitTestCase;
8-
use function Amp\call;
12+
use React\Promise\PromiseInterface as ReactPromise;
913

1014
/**
1115
* 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
3034
/** @var bool */
3135
private $setUpInvoked = false;
3236

33-
final protected function runTest()
34-
{
35-
parent::setName('runAsyncTest');
36-
return parent::runTest();
37-
}
37+
/** @var \Generator|null */
38+
private $generator;
3839

3940
/** @internal */
4041
final public function runAsyncTest(...$args)
@@ -55,7 +56,7 @@ final public function runAsyncTest(...$args)
5556
$start = \microtime(true);
5657

5758
Loop::run(function () use (&$returnValue, &$exception, &$invoked, $args) {
58-
$promise = call([$this, $this->realTestName], ...$args);
59+
$promise = $this->call([$this, $this->realTestName], ...$args);
5960
$promise->onResolve(function ($error, $value) use (&$invoked, &$exception, &$returnValue) {
6061
$invoked = true;
6162
$exception = $error;
@@ -67,6 +68,8 @@ final public function runAsyncTest(...$args)
6768
Loop::cancel($this->timeoutId);
6869
}
6970

71+
$this->generator = null;
72+
7073
if (isset($exception)) {
7174
throw $exception;
7275
}
@@ -88,6 +91,12 @@ final public function runAsyncTest(...$args)
8891
return $returnValue;
8992
}
9093

94+
final protected function runTest()
95+
{
96+
parent::setName('runAsyncTest');
97+
return parent::runTest();
98+
}
99+
91100
/**
92101
* Fails the test if the loop does not run for at least the given amount of time.
93102
*
@@ -113,13 +122,29 @@ final protected function setTimeout(int $timeout)
113122
Loop::stop();
114123
Loop::setErrorHandler(null);
115124

125+
$additionalInfo = '';
126+
127+
if ($this->generator && $this->generator->valid()) {
128+
$reflGen = new \ReflectionGenerator($this->generator);
129+
$exeGen = $reflGen->getExecutingGenerator();
130+
if ($isSubgenerator = ($exeGen !== $this->generator)) {
131+
$reflGen = new \ReflectionGenerator($exeGen);
132+
}
133+
134+
$additionalInfo .= \sprintf(
135+
"\r\n\r\nTimeout reached on line %s in %s",
136+
$reflGen->getExecutingLine(),
137+
$reflGen->getExecutingFile()
138+
);
139+
}
140+
116141
$loop = Loop::get();
117142
if ($loop instanceof Loop\TracingDriver) {
118-
$additionalInfo = "\r\n\r\n" . $loop->dump();
143+
$additionalInfo .= "\r\n\r\n" . $loop->dump();
119144
} elseif (\class_exists(Loop\TracingDriver::class)) {
120-
$additionalInfo = "\r\n\r\nSet AMP_DEBUG_TRACE_WATCHERS=true as environment variable to trace watchers keeping the loop running.";
145+
$additionalInfo .= "\r\n\r\nSet AMP_DEBUG_TRACE_WATCHERS=true as environment variable to trace watchers keeping the loop running.";
121146
} else {
122-
$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. ";
147+
$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. ";
123148
}
124149

125150
$this->fail('Expected test to complete before ' . $timeout . 'ms time limit' . $additionalInfo);
@@ -130,7 +155,7 @@ final protected function setTimeout(int $timeout)
130155

131156
/**
132157
* @param int $invocationCount Number of times the callback must be invoked or the test will fail.
133-
* @param callable|null $returnCallback Callable providing a return value for the callback.
158+
* @param callable|null $returnCallback Callable providing a return value for the callback.
134159
*
135160
* @return callable|MockObject Mock object having only an __invoke method.
136161
*/
@@ -146,4 +171,39 @@ final protected function createCallback(int $invocationCount, callable $returnCa
146171

147172
return $mock;
148173
}
174+
175+
/**
176+
* Specialized Amp\call that stores the generator if present for debugging purposes.
177+
*
178+
* @param callable $callback
179+
* @param mixed ...$args
180+
*
181+
* @return Promise
182+
*/
183+
private function call(callable $callback, ...$args): Promise
184+
{
185+
$this->generator = null;
186+
187+
try {
188+
$result = $callback(...$args);
189+
} catch (\Throwable $exception) {
190+
return new Failure($exception);
191+
}
192+
193+
if ($result instanceof \Generator) {
194+
$this->generator = $result;
195+
196+
return new Coroutine($result);
197+
}
198+
199+
if ($result instanceof Promise) {
200+
return $result;
201+
}
202+
203+
if ($result instanceof ReactPromise) {
204+
return Promise\adapt($result);
205+
}
206+
207+
return new Success($result);
208+
}
149209
}

0 commit comments

Comments
 (0)