Skip to content

Commit

Permalink
Testing (#183)
Browse files Browse the repository at this point in the history
* Testing sub-component

* Fix system info detection

* Fix system info detection

* Disable service client test

* Add readme
  • Loading branch information
seregazhuk authored Jun 13, 2022
1 parent dd7f3ee commit 22b5ecd
Show file tree
Hide file tree
Showing 16 changed files with 360 additions and 27 deletions.
11 changes: 3 additions & 8 deletions .github/workflows/testing.yml
Original file line number Diff line number Diff line change
Expand Up @@ -65,7 +65,7 @@ jobs:
with:
php-version: ${{ matrix.php }}
tools: composer:v2
extensions: dom, sockets
extensions: dom, sockets, curl

- name: Check Out Code
uses: actions/checkout@v2
Expand Down Expand Up @@ -118,7 +118,7 @@ jobs:
with:
php-version: ${{ matrix.php }}
tools: composer:v2
extensions: dom
extensions: dom, curl

- name: Check Out Code
uses: actions/checkout@v2
Expand Down Expand Up @@ -168,7 +168,7 @@ jobs:
with:
php-version: ${{ matrix.php }}
tools: composer:v2
extensions: dom, sockets, grpc
extensions: dom, sockets, grpc, curl

- name: Check Out Code
uses: actions/checkout@v2
Expand Down Expand Up @@ -201,9 +201,4 @@ jobs:
- name: Run Tests
run: |
docker-compose -f ./tests/docker-compose.yaml up -d
sleep 35
chmod +x ./rr
./rr serve -c ./tests/.rr.silent.yaml &
sleep 10
vendor/bin/phpunit --testsuite=Functional --testdox --verbose
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -19,3 +19,4 @@ Thumbs.db
*.exe
rr
.rr.yaml
temporal-test-server
10 changes: 8 additions & 2 deletions composer.json
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@
},
"require": {
"php": ">=7.4",
"ext-curl": "*",
"ext-json": "*",
"google/common-protos": "^1.3",
"google/protobuf": "^3.14",
Expand All @@ -30,19 +31,24 @@
"psr/log": "^1.0 || ^2.0 || ^3.0",
"react/promise": "^2.8",
"spiral/attributes": "^2.7",
"spiral/roadrunner-worker": "^2.0.2",
"spiral/roadrunner-cli": "^2.0",
"symfony/polyfill-php80": "^1.18"
"spiral/roadrunner-worker": "^2.0.2",
"symfony/filesystem": "^v5.4",
"symfony/http-client": "^5.4",
"symfony/polyfill-php80": "^1.18",
"symfony/process": "^v5.4"
},
"autoload": {
"psr-4": {
"Temporal\\Testing\\": "testing/src",
"GPBMetadata\\": "api/v1/GPBMetadata",
"Temporal\\": "src",
"Temporal\\Api\\": "api/v1/Temporal/Api",
"Temporal\\Roadrunner\\": "api/v1/Temporal/Roadrunner"
}
},
"require-dev": {
"ext-curl": "*",
"friendsofphp/php-cs-fixer": "^2.8",
"composer/composer": "^2.0",
"laminas/laminas-code": "^4.0",
Expand Down
Empty file.
1 change: 1 addition & 0 deletions phpunit.xml
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
<?xml version="1.0" encoding="UTF-8"?>
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
bootstrap="tests/bootstrap.php"
backupGlobals="false"
backupStaticAttributes="false"
colors="true"
Expand Down
68 changes: 68 additions & 0 deletions testing/Readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
## Testing framework

### Quick start
1. Create `bootstrap.php` in `tests` folder with the following contents:
```php
declare(strict_types=1);

require __DIR__ . '/../vendor/autoload.php';

use Temporal\Testing\Environment;

$environment = Environment::create();
$environment->start();
register_shutdown_function(fn () => $environment->stop());
```

2. Add `bootstrap.php` to your `phpunit.xml`:
```xml
<phpunit xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="https://schema.phpunit.de/9.3/phpunit.xsd"
bootstrap="tests/bootstrap.php"
>
</phpunit>
```

3. Add test server executable to `.gitignore`:
```gitignore
temporal-test-server
```

### How it works
For testing workflows there is no need to run a full Temporal server (with storage and ui interface).
Instead, we can use a light-weight test server.

The code in `bootstrap.php` will start/stop (and download if it doesn't exist) Temporal test
server and RoadRunner for every phpunit run. Test server runs as a regular server on 7233 port.
Thus, if you use default connection settings, there is no need to change them.

Under the hood RoadRunner is started with `rr serve` command. You can specify your own command in `bootstrap.php`:
```php
$environment->start('./rr serve -c .rr.test.yaml -w tests');
```

The snippet above will start Temporal test server and RoadRunner with `.rr.test.yaml` config and `tests` working
directory. Having a separate RoadRunner config file for tests can be useful to mock you activities. For
example, you can create a separate *worker* that registers activity implementations mocks:

```yaml
# test/.rr.test.yaml
server:
command: "php worker.test.php"
```
And within the worker you register your workflows and mock activities:
```php
// worker.test.php
$factory = WorkerFactory::create();

$worker = $factory->newWorker();
$worker->registerWorkflowTypes(MyWorkflow::class);
$worker->registerActivity(MyActvivityMock::class);
$factory->run();
```




82 changes: 82 additions & 0 deletions testing/src/Downloader.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

declare(strict_types=1);

namespace Temporal\Testing;

use Symfony\Component\Filesystem\Filesystem;
use Symfony\Contracts\HttpClient\HttpClientInterface;

final class Downloader
{
private const LATEST_JAVA_SDK_RELEASE = 'https://api.github.com/repos/temporalio/sdk-java/releases/latest';
private Filesystem $filesystem;
private HttpClientInterface $httpClient;

public function __construct(Filesystem $filesystem, HttpClientInterface $httpClient)
{
$this->filesystem = $filesystem;
$this->httpClient = $httpClient;
}

private function findAsset(array $assets, string $systemPlatform, string $systemArch): array
{
foreach ($assets as $asset) {
preg_match('/^temporal-test-server_[^_]+_([^_]+)_([^.]+)\.(?:zip|tar.gz)$/', $asset['name'], $match);
[, $assetPlatform, $assetArch] = $match;

if ($assetPlatform === $systemPlatform) {
// TODO: assetArch === systemArch (no arm builds for test server yet)
return $asset;
}
}

throw new \RuntimeException("Asset for $systemPlatform not found");
}

public function download(SystemInfo $systemInfo): void
{
$asset = $this->getAsset($systemInfo->platform, $systemInfo->arch);
$assetUrl = $asset['browser_download_url'];
$pathToExtractedAsset = $this->downloadAsset($assetUrl);

$targetPath = getcwd() . DIRECTORY_SEPARATOR . $systemInfo->temporalServerExecutable;
$this->filesystem->copy($pathToExtractedAsset . DIRECTORY_SEPARATOR . $systemInfo->temporalServerExecutable, $targetPath);
$this->filesystem->chmod($targetPath, 755);
$this->filesystem->remove($pathToExtractedAsset);
}

public function check(string $filename): bool
{
return $this->filesystem->exists($filename);
}

private function downloadAsset(string $assetUrl): string
{
$response = $this->httpClient->request('GET', $assetUrl);
$assetPath = getcwd() . DIRECTORY_SEPARATOR . basename($assetUrl);

if ($this->filesystem->exists($assetPath)) {
$this->filesystem->remove($assetPath);
}
$this->filesystem->touch($assetPath);
$this->filesystem->appendToFile($assetPath, $response->getContent());

$phar = new \PharData($assetPath);
$extractedPath = getcwd() . DIRECTORY_SEPARATOR . $phar->getFilename();
if (!$this->filesystem->exists($extractedPath)) {
$phar->extractTo(getcwd());
}
$this->filesystem->remove($phar->getPath());

return $extractedPath;
}

private function getAsset(string $systemPlatform, string $systemArch): array
{
$response = $this->httpClient->request('GET', self::LATEST_JAVA_SDK_RELEASE);
$assets = $response->toArray()['assets'];

return $this->findAsset($assets, $systemPlatform, $systemArch);
}
}
85 changes: 85 additions & 0 deletions testing/src/Environment.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
<?php

declare(strict_types=1);

namespace Temporal\Testing;

use Symfony\Component\Console\Output\ConsoleOutput;
use Symfony\Component\Console\Output\Output;
use Symfony\Component\Filesystem\Filesystem;
use Symfony\Component\HttpClient\HttpClient;
use Symfony\Component\Process\Process;

final class Environment
{
private Downloader $downloader;
private Output $output;
private SystemInfo $systemInfo;
private ?Process $temporalServerProcess = null;
private ?Process $roadRunnerProcess = null;

public function __construct(Output $output, Downloader $downloader, SystemInfo $systemInfo) {
$this->downloader = $downloader;
$this->systemInfo = $systemInfo;
$this->output = $output;
}

public static function create(): self
{
return new self(
new ConsoleOutput(),
new Downloader(new Filesystem(), HttpClient::create()),
SystemInfo::detect(),
);
}

public function start(string $rrCommand = null): void
{
if (!$this->downloader->check($this->systemInfo->temporalServerExecutable)) {
$this->output->write('Download temporal test server... ');
$this->downloader->download($this->systemInfo);
$this->output->writeln('<info>done.</info>');
}

$this->output->write('Starting Temporal test server... ');
$this->temporalServerProcess = new Process([$this->systemInfo->temporalServerExecutable, 7233,]);
$this->temporalServerProcess->setTimeout(10);
$this->temporalServerProcess->start();
$this->output->writeln('<info>done.</info>');
sleep(1);

$this->roadRunnerProcess = new Process(
$rrCommand ? explode(' ', $rrCommand) : [$this->systemInfo->rrExecutable, 'serve']
);
$this->roadRunnerProcess->setTimeout(10);

$this->output->write('Starting RoadRunner... ');
$this->roadRunnerProcess->start();

if (!$this->roadRunnerProcess->isRunning()) {
$this->output->writeln('<error>error</error>');
$this->output->writeln('Error starting RoadRunner: ' . $this->roadRunnerProcess->getErrorOutput());
exit(1);
}

$this->roadRunnerProcess->waitUntil(
fn($type, $output) => strpos($output, 'RoadRunner server started') !== false
);
$this->output->writeln('<info>done.</info>');
}

public function stop(): void
{
if ($this->temporalServerProcess !== null && $this->temporalServerProcess->isRunning()) {
$this->output->write('Stopping Temporal server... ');
$this->temporalServerProcess->stop();
$this->output->writeln('<info>done.</info>');
}

if ($this->roadRunnerProcess !== null && $this->roadRunnerProcess->isRunning()) {
$this->output->write('Stopping RoadRunner... ');
$this->roadRunnerProcess->stop();
$this->output->writeln('<info>done.</info>');
}
}
}
68 changes: 68 additions & 0 deletions testing/src/SystemInfo.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
<?php

declare(strict_types=1);

namespace Temporal\Testing;

use Spiral\RoadRunner\Console\Environment\Architecture;
use Spiral\RoadRunner\Console\Environment\OperatingSystem;

final class SystemInfo
{
private const PLATFORM_MAPPINGS = [
'darwin' => 'macOS',
'linux' => 'linux',
'windows' => 'windows',
];

private const ARCHITECTURE_MAPPINGS = [
'x64' => 'amd64',
'amd64' => 'amd64',
'arm64' => 'aarch64'
];

private const TEMPORAL_EXECUTABLE_MAP = [
'darwin' => './temporal-test-server',
'linux' => './temporal-test-server',
'windows' => 'temporal-test-server.exe',
];

private const RR_EXECUTABLE_MAP = [
'darwin' => './rr',
'linux' => './rr',
'windows' => 'rr.exe',
];

public string $arch;
public string $platform;
public string $os;
public string $temporalServerExecutable;
public string $rrExecutable;

private function __construct(
string $arch,
string $platform,
string $os,
string $temporalServerExecutable,
string $rrExecutable
) {
$this->arch = $arch;
$this->platform = $platform;
$this->os = $os;
$this->temporalServerExecutable = $temporalServerExecutable;
$this->rrExecutable = $rrExecutable;
}

public static function detect(): self
{
$os = OperatingSystem::createFromGlobals();

return new self(
$os,
self::PLATFORM_MAPPINGS[$os],
self::ARCHITECTURE_MAPPINGS[Architecture::createFromGlobals()],
self::TEMPORAL_EXECUTABLE_MAP[$os],
self::RR_EXECUTABLE_MAP[$os],
);
}
}
Loading

0 comments on commit 22b5ecd

Please sign in to comment.