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
52 changes: 52 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,58 @@ self::assertEventually(function () use ($connection) {
});
```

### Coroutine Runner

`Utopia\Tests\Async\Runner` runs your existing PHPUnit test cases concurrently —
each test in its own [Swoole](https://www.swoole.com/) coroutine, bounded by a
pool of N coroutines. While one test waits on coroutine I/O (a channel, a hooked
socket, `Coroutine::sleep`), another runs, so a suite of slow integration tests
finishes in roughly the time of its slowest test rather than their sum.

It requires the `swoole` extension.

**Command line:**

```bash
# Run every *Test.php under tests/, 10 at a time (the default)
vendor/bin/co-phpunit tests --concurrency=20
```

**Programmatically:**

```php
use Utopia\Tests\Async\Runner;

$runner = new Runner(concurrency: 20);
$runner->addDirectory(__DIR__ . '/tests');
// ...or queue classes explicitly: $runner->addTestCase(MyTest::class);

exit($runner->run());
```

Your test classes are plain `PHPUnit\Framework\TestCase`s — `setUp`/`tearDown`,
`setUpBeforeClass`/`tearDownAfterClass`, `#[DataProvider]`, assertions and
`markTestSkipped()` all work as usual:

```php
use PHPUnit\Framework\TestCase;
use Swoole\Coroutine as Co;

class HealthTest extends TestCase
{
public function testServiceResponds(): void
{
Co::sleep(0.5); // e.g. an async HTTP call to a service
$this->assertTrue(true);
}
}
```

The runner drives each test's lifecycle directly instead of going through
PHPUnit's sequential runner, so process-global features (output-buffering
assertions, global-state isolation, separate-process tests) are out of scope —
it is built for coroutine-friendly integration tests that assert and skip.

## Development

### Run Tests
Expand Down
34 changes: 34 additions & 0 deletions bin/co-phpunit
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
#!/usr/bin/env php
<?php

use Utopia\Tests\Async\Runner;

foreach ([__DIR__.'/../../../autoload.php', __DIR__.'/../vendor/autoload.php'] as $autoload) {
if (\is_file($autoload)) {
require $autoload;
break;
}
}

$concurrency = 10;
$paths = [];

foreach (\array_slice($argv, 1) as $arg) {
if (\str_starts_with($arg, '--concurrency=')) {
$concurrency = (int) \substr($arg, \strlen('--concurrency='));
} else {
$paths[] = $arg;
}
}

if ($paths === []) {
$paths = ['tests'];
}

$runner = new Runner($concurrency);

foreach ($paths as $path) {
$runner->addDirectory($path);
}

exit($runner->run());
4 changes: 4 additions & 0 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
"require": {
"php": ">=8.3"
},
"suggest": {
"ext-swoole": "Required for the concurrent coroutine test runner (Utopia\\Tests\\Async\\Runner)"
},
"bin": ["bin/co-phpunit"],
"require-dev": {
"phpstan/phpstan": "2.0.*",
"phpunit/phpunit": "12.4.*",
Expand Down
4 changes: 2 additions & 2 deletions composer.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 2 additions & 0 deletions phpstan.neon
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
parameters:
scanFiles:
- stubs/swoole.stub
excludePaths:
- vendor
1 change: 1 addition & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
<testsuites>
<testsuite name="unit">
<directory>tests</directory>
<exclude>tests/fixtures</exclude>
</testsuite>
</testsuites>
13 changes: 13 additions & 0 deletions src/Tests/Async/Result.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?php

namespace Utopia\Tests\Async;

final readonly class Result
{
public function __construct(
public string $name,
public Status $status,
public string $message = '',
) {
}
}
Loading
Loading