Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 21 additions & 11 deletions src/foundation/src/Testing/Concerns/RunTestsInCoroutine.php
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,13 @@
use function Hypervel\Coroutine\run;

/**
* Wraps each test method in a Swoole coroutine so that database connections,
* channels, and other coroutine-dependent APIs work correctly during tests.
*
* PHPUnit 10.5 made runTest() private, so we can no longer override it.
* Instead, we swap the test method name during setUp() — which runs before
* PHPUnit's private runTest() calls $this->{$this->name}().
*
* @method string name()
*/
trait RunTestsInCoroutine
Expand All @@ -24,9 +31,22 @@ trait RunTestsInCoroutine

protected string $realTestName = '';

/**
* Swap the test method name so PHPUnit's private runTest() calls
* runTestsInCoroutine() instead of the real test method.
* The real test method is then executed inside a Swoole coroutine.
*/
protected function setUpCoroutineTest(): void
{
if (Coroutine::getCid() === -1 && $this->enableCoroutine) {
$this->realTestName = $this->name();
$this->setName('runTestsInCoroutine');
}
}

final protected function runTestsInCoroutine(...$arguments)
{
parent::setName($this->realTestName);
$this->setName($this->realTestName);

$testResult = null;
$exception = null;
Expand Down Expand Up @@ -56,16 +76,6 @@ final protected function runTestsInCoroutine(...$arguments)
return $testResult;
}

final protected function runTest(): mixed
{
if (Coroutine::getCid() === -1 && $this->enableCoroutine) {
$this->realTestName = $this->name();
parent::setName('runTestsInCoroutine');
}

return parent::runTest();
}

protected function invokeSetupInCoroutine(): void
{
if (method_exists($this, 'setUpInCoroutine')) {
Expand Down
9 changes: 9 additions & 0 deletions src/foundation/src/Testing/TestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,15 @@ protected function setUp(): void
}

$this->setUpHasRun = true;

// Swap the test method name for coroutine wrapping.
// This must happen AFTER setUp completes but BEFORE PHPUnit's
// private runTest() calls $this->{$this->name}().
// PHPUnit 10.5 made runTest() private, so we can no longer
// override it — the name swap in setUp() is the only hook point.
if (method_exists($this, 'setUpCoroutineTest')) {
$this->setUpCoroutineTest();
}
}

/**
Expand Down
150 changes: 150 additions & 0 deletions tests/Foundation/Testing/Concerns/RunTestsInCoroutineTest.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,150 @@
<?php

declare(strict_types=1);

namespace Hypervel\Tests\Foundation\Testing\Concerns;

use Hypervel\Foundation\Testing\Concerns\RunTestsInCoroutine;
use Hypervel\Tests\TestCase;
use ReflectionMethod;
use ReflectionProperty;
use RuntimeException;
use Swoole\Coroutine;

/**
* @internal
* @coversNothing
*/
class RunTestsInCoroutineTest extends TestCase
{
public function testSetUpCoroutineTestSwapsNameOutsideCoroutine()
{
$testCase = new CoroutineTestStub('myTestMethod');

$this->assertSame(-1, Coroutine::getCid());
$this->assertSame('myTestMethod', $testCase->name());

$method = new ReflectionMethod($testCase, 'setUpCoroutineTest');
$method->invoke($testCase);

$this->assertSame('runTestsInCoroutine', $testCase->name());

$realName = new ReflectionProperty($testCase, 'realTestName');
$this->assertSame('myTestMethod', $realName->getValue($testCase));
}

public function testSetUpCoroutineTestDoesNotSwapWhenCoroutineDisabled()
{
$testCase = new CoroutineDisabledTestStub('myTestMethod');

$method = new ReflectionMethod($testCase, 'setUpCoroutineTest');
$method->invoke($testCase);

$this->assertSame('myTestMethod', $testCase->name());
}

public function testSetUpCoroutineTestIsNoOpInsideCoroutine()
{
$testCase = new CoroutineTestStub('myTestMethod');

\Hypervel\Coroutine\run(function () use ($testCase) {
$this->assertGreaterThan(-1, Coroutine::getCid());

$method = new ReflectionMethod($testCase, 'setUpCoroutineTest');
$method->invoke($testCase);

$this->assertSame('myTestMethod', $testCase->name());
});
}

public function testRunTestsInCoroutineExecutesInCoroutine()
{
$testCase = new CoroutineTestStub('myTestMethod');

$setUp = new ReflectionMethod($testCase, 'setUpCoroutineTest');
$setUp->invoke($testCase);

$run = new ReflectionMethod($testCase, 'runTestsInCoroutine');
$run->invoke($testCase);

$this->assertTrue($testCase->executedInCoroutine);
}

public function testRunTestsInCoroutineRestoresOriginalName()
{
$testCase = new CoroutineTestStub('myTestMethod');

$setUp = new ReflectionMethod($testCase, 'setUpCoroutineTest');
$setUp->invoke($testCase);

$this->assertSame('runTestsInCoroutine', $testCase->name());

$run = new ReflectionMethod($testCase, 'runTestsInCoroutine');
$run->invoke($testCase);

$this->assertSame('myTestMethod', $testCase->name());
}

public function testRunTestsInCoroutinePropagatesExceptions()
{
$testCase = new CoroutineExceptionTestStub('throwingMethod');

$setUp = new ReflectionMethod($testCase, 'setUpCoroutineTest');
$setUp->invoke($testCase);

$this->expectException(RuntimeException::class);
$this->expectExceptionMessage('Test exception from coroutine');

$run = new ReflectionMethod($testCase, 'runTestsInCoroutine');
$run->invoke($testCase);
}
}

/**
* @internal
* @coversNothing
*/
class CoroutineTestStub extends \PHPUnit\Framework\TestCase
{
use RunTestsInCoroutine;

public bool $executedInCoroutine = false;

public function myTestMethod(): void
{
$this->executedInCoroutine = Coroutine::getCid() > -1;
}
}

/**
* @internal
* @coversNothing
*/
class CoroutineDisabledTestStub extends \PHPUnit\Framework\TestCase
{
use RunTestsInCoroutine;

public function __construct(string $name)
{
parent::__construct($name);
$this->enableCoroutine = false;
}

public function myTestMethod(): void
{
}
}

/**
* @internal
* @coversNothing
*/
class CoroutineExceptionTestStub extends \PHPUnit\Framework\TestCase
{
use RunTestsInCoroutine;

public function throwingMethod(): void
{
throw new RuntimeException('Test exception from coroutine');
}
}
Loading