Skip to content

Commit d0ffc5c

Browse files
authored
feat: add support for PHPUnit filtering in test worker (#748)
1 parent 20d408b commit d0ffc5c

8 files changed

Lines changed: 236 additions & 150 deletions

File tree

phpunit.xml.dist

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,9 @@
8282
<directory suffix="TestCase.php">tests/Functional</directory>
8383
</testsuite>
8484
</testsuites>
85+
<extensions>
86+
<bootstrap class="Temporal\Tests\Acceptance\AcceptanceBootExtension"/>
87+
</extensions>
8588
<groups>
8689
<exclude>
8790
<group>skip-on-test-server</group>
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Tests\Acceptance;
6+
7+
use PHPUnit\Runner\Extension\Extension;
8+
use PHPUnit\Runner\Extension\Facade;
9+
use PHPUnit\Runner\Extension\ParameterCollection;
10+
use PHPUnit\TextUI\Configuration\Configuration;
11+
12+
final class AcceptanceBootExtension implements Extension
13+
{
14+
public function bootstrap(Configuration $configuration, Facade $facade, ParameterCollection $parameters): void
15+
{
16+
$facade->registerSubscribers(new ExecutionStartedSubscriber());
17+
}
18+
}

tests/Acceptance/App/Runtime/RRStarter.php

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,10 @@ public function __construct(
1818
\register_shutdown_function(fn() => $this->stop());
1919
}
2020

21-
public function start(): void
21+
/**
22+
* @param list<class-string> $allowedTestClasses
23+
*/
24+
public function start(array $allowedTestClasses = []): void
2225
{
2326
if ($this->environment->isRoadRunnerRunning()) {
2427
return;
@@ -27,6 +30,17 @@ public function start(): void
2730
$systemInfo = SystemInfo::detect();
2831
$run = $this->runtime->command;
2932

33+
$workerArgs = [
34+
PHP_BINARY,
35+
...$run->getPhpBinaryArguments(),
36+
$this->runtime->rrConfigDir . DIRECTORY_SEPARATOR . 'worker.php',
37+
...$run->getCommandLineArguments(),
38+
];
39+
40+
foreach ($allowedTestClasses as $class) {
41+
$workerArgs[] = 'test-class=' . $class;
42+
}
43+
3044
$rrCommand = [
3145
$this->runtime->workDir . DIRECTORY_SEPARATOR . $systemInfo->rrExecutable,
3246
'serve',
@@ -37,17 +51,17 @@ public function start(): void
3751
'-o',
3852
"temporal.address={$this->runtime->address}",
3953
'-o',
40-
'server.command=' . \implode(',', [
41-
PHP_BINARY,
42-
...$run->getPhpBinaryArguments(),
43-
$this->runtime->rrConfigDir . DIRECTORY_SEPARATOR . 'worker.php',
44-
...$run->getCommandLineArguments(),
45-
]),
54+
'server.command=' . \implode(',', $workerArgs),
4655
];
47-
$run->tlsKey === null or $rrCommand = [...$rrCommand, '-o', "tls.key={$run->tlsKey}"];
48-
$run->tlsCert === null or $rrCommand = [...$rrCommand, '-o', "tls.cert={$run->tlsCert}"];
56+
if ($run->tlsKey !== null) {
57+
$rrCommand[] = '-o';
58+
$rrCommand[] = "tls.key={$run->tlsKey}";
59+
}
60+
if ($run->tlsCert !== null) {
61+
$rrCommand[] = '-o';
62+
$rrCommand[] = "tls.cert={$run->tlsCert}";
63+
}
4964

50-
// echo "\e[1;36mStart RoadRunner with command:\e[0m {$command}\n";
5165
$this->environment->startRoadRunner(
5266
rrCommand: $rrCommand,
5367
configFile: $this->runtime->rrConfigDir . DIRECTORY_SEPARATOR . '.rr.yaml',

tests/Acceptance/App/RuntimeBuilder.php

Lines changed: 30 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -7,38 +7,44 @@
77
use PHPUnit\Framework\Attributes\Test;
88
use Temporal\Activity\ActivityInterface;
99
use Temporal\DataConverter\PayloadConverterInterface;
10-
use Temporal\Testing\DeprecationCollector;
1110
use Temporal\Testing\Command;
11+
use Temporal\Testing\DeprecationCollector;
1212
use Temporal\Tests\Acceptance\App\Input\Feature;
1313
use Temporal\Tests\Acceptance\App\Runtime\State;
1414
use Temporal\Worker\FeatureFlags;
1515
use Temporal\Workflow\WorkflowInterface;
1616

1717
final class RuntimeBuilder
1818
{
19-
public static function hydrateClasses(State $runtime): void
19+
/**
20+
* @param list<class-string> $allowedTestClasses
21+
*/
22+
public static function hydrateClasses(State $runtime, array $allowedTestClasses = []): void
2023
{
2124
foreach ($runtime->testCasesDir as $namespace => $dir) {
2225
foreach (self::iterateClasses($dir, $namespace) as $feature => $classes) {
26+
if ($allowedTestClasses !== [] && !\in_array($feature->testClass, $allowedTestClasses, true)) {
27+
continue;
28+
}
2329
foreach ($classes as $classString) {
2430
$class = new \ReflectionClass($classString);
2531

26-
# Register Workflow
27-
$class->getAttributes(WorkflowInterface::class) === [] or $runtime
28-
->addWorkflow($feature, $classString);
32+
if ($class->getAttributes(WorkflowInterface::class) !== []) {
33+
$runtime->addWorkflow($feature, $classString);
34+
}
2935

30-
# Register Activity
31-
$class->getAttributes(ActivityInterface::class) === [] or $runtime
32-
->addActivity($feature, $classString);
36+
if ($class->getAttributes(ActivityInterface::class) !== []) {
37+
$runtime->addActivity($feature, $classString);
38+
}
3339

34-
# Register Converters
35-
$class->implementsInterface(PayloadConverterInterface::class) and $runtime
36-
->addConverter($feature, $classString);
40+
if ($class->implementsInterface(PayloadConverterInterface::class)) {
41+
$runtime->addConverter($feature, $classString);
42+
}
3743

38-
# Register Check
3944
foreach ($class->getMethods() as $method) {
40-
$method->getAttributes(Test::class) === [] or $runtime
41-
->addCheck($feature, $classString, $method->getName());
45+
if ($method->getAttributes(Test::class) !== []) {
46+
$runtime->addCheck($feature, $classString, $method->getName());
47+
}
4248
}
4349
}
4450
}
@@ -57,22 +63,27 @@ public static function createEmpty(Command $command, string $workDir, iterable $
5763
/**
5864
* @param non-empty-string $workDir
5965
* @param iterable<non-empty-string, non-empty-string> $testCasesDir
66+
* @param list<class-string> $allowedTestClasses
6067
*/
61-
public static function createState(Command $command, string $workDir, iterable $testCasesDir, int $workers = 1): State
62-
{
68+
public static function createState(
69+
Command $command,
70+
string $workDir,
71+
iterable $testCasesDir,
72+
int $workers = 1,
73+
array $allowedTestClasses = [],
74+
): State {
6375
$runtime = new State($command, \dirname(__DIR__), $workDir, $testCasesDir, $workers);
6476

65-
self::hydrateClasses($runtime);
77+
self::hydrateClasses($runtime, $allowedTestClasses);
6678

6779
return $runtime;
6880
}
6981

7082
public static function init(): void
7183
{
7284
\ini_set('display_errors', 'stderr');
73-
error_reporting(-1);
85+
\error_reporting(-1);
7486
DeprecationCollector::register();
75-
// Feature flags
7687
FeatureFlags::$workflowDeferredHandlerStart = true;
7788
FeatureFlags::$cancelAbandonedChildWorkflows = false;
7889
FeatureFlags::$warnOnActivityMethodWithoutAttribute = true;
@@ -85,7 +96,6 @@ public static function init(): void
8596
*/
8697
private static function iterateClasses(string $featuresDir, string $ns): iterable
8798
{
88-
// Scan all the test cases
8999
foreach (ClassLocator::loadTestCases($featuresDir, $ns) as $class) {
90100
$namespace = \substr($class, 0, \strrpos($class, '\\'));
91101
$feature = new Feature(

tests/Acceptance/App/TestCase.php

Lines changed: 0 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -30,15 +30,6 @@
3030

3131
abstract class TestCase extends \Temporal\Tests\TestCase
3232
{
33-
protected function setUp(): void
34-
{
35-
parent::setUp();
36-
37-
/** @var State $state */
38-
$state = ContainerFacade::$container->get(State::class);
39-
$state->countFeatures() === 0 and RuntimeBuilder::hydrateClasses($state);
40-
}
41-
4233
#[\Override]
4334
protected function runTest(): mixed
4435
{
Lines changed: 133 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,133 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Tests\Acceptance;
6+
7+
use PHPUnit\Event\Code\TestMethod;
8+
use PHPUnit\Event\TestRunner\ExecutionStarted;
9+
use PHPUnit\Event\TestRunner\ExecutionStartedSubscriber as ExecutionStartedSubscriberInterface;
10+
use Psr\Container\ContainerInterface;
11+
use Psr\Log\LoggerInterface;
12+
use Spiral\Core\Attribute\Proxy;
13+
use Spiral\Core\Container;
14+
use Spiral\Goridge\RPC\RPC;
15+
use Spiral\Goridge\RPC\RPCInterface;
16+
use Spiral\RoadRunner\KeyValue\Factory;
17+
use Spiral\RoadRunner\KeyValue\StorageInterface;
18+
use Temporal\Client\ClientOptions;
19+
use Temporal\Client\GRPC\ServiceClient;
20+
use Temporal\Client\GRPC\ServiceClientInterface;
21+
use Temporal\Client\ScheduleClient;
22+
use Temporal\Client\ScheduleClientInterface;
23+
use Temporal\Client\WorkflowClient;
24+
use Temporal\Client\WorkflowClientInterface;
25+
use Temporal\Client\WorkflowStubInterface;
26+
use Temporal\DataConverter\DataConverter;
27+
use Temporal\DataConverter\DataConverterInterface;
28+
use Temporal\Testing\Environment;
29+
use Temporal\Tests\Acceptance\App\Feature\WorkflowStubInjector;
30+
use Temporal\Tests\Acceptance\App\Runtime\ContainerFacade;
31+
use Temporal\Tests\Acceptance\App\Runtime\RRStarter;
32+
use Temporal\Tests\Acceptance\App\Runtime\State;
33+
use Temporal\Tests\Acceptance\App\Runtime\TemporalStarter;
34+
use Temporal\Tests\Acceptance\App\RuntimeBuilder;
35+
use Temporal\Tests\Acceptance\App\Support;
36+
use Temporal\Worker\Logger\StderrLogger;
37+
38+
final class ExecutionStartedSubscriber implements ExecutionStartedSubscriberInterface
39+
{
40+
private const NAMESPACE_PREFIX = 'Temporal\\Tests\\Acceptance\\';
41+
42+
public function notify(ExecutionStarted $event): void
43+
{
44+
$classNames = [];
45+
foreach ($event->testSuite()->tests() as $test) {
46+
if ($test instanceof TestMethod && \str_starts_with($test->className(), self::NAMESPACE_PREFIX)) {
47+
$classNames[] = $test->className();
48+
}
49+
}
50+
51+
$selectedTestClasses = \array_values(\array_unique($classNames));
52+
53+
if ($selectedTestClasses === []) {
54+
return;
55+
}
56+
57+
$logger = new StderrLogger();
58+
$logger->info('[selection] picked test classes after filtering', ['count' => \count($selectedTestClasses)]);
59+
60+
RuntimeBuilder::init();
61+
62+
$environment = Environment::create();
63+
$state = RuntimeBuilder::createState(
64+
$environment->command,
65+
\getcwd(),
66+
[
67+
'Temporal\Tests\Acceptance\Harness' => __DIR__ . '/Harness',
68+
'Temporal\Tests\Acceptance\Extra' => __DIR__ . '/Extra',
69+
],
70+
workers: (int) (\getenv('ACTIVITY_WORKERS') ?: 2),
71+
allowedTestClasses: $selectedTestClasses,
72+
);
73+
$logger->info('[selection] registered test features', [
74+
'registered' => $state->countFeatures(),
75+
'requested' => \count($selectedTestClasses),
76+
]);
77+
78+
$container = new Container();
79+
ContainerFacade::$container = $container;
80+
$container->bindSingleton(State::class, $state);
81+
$container->bindSingleton(Environment::class, $environment);
82+
$container->bindSingleton(LoggerInterface::class, $logger);
83+
84+
$temporalRunner = new TemporalStarter($environment);
85+
$rrRunner = new RRStarter($state, $environment);
86+
$temporalRunner->start();
87+
$rrRunner->start($selectedTestClasses);
88+
89+
$serviceClient = $state->command->tlsKey === null && $state->command->tlsCert === null
90+
? ServiceClient::create($state->address)
91+
: ServiceClient::createSSL(
92+
$state->address,
93+
clientKey: $state->command->tlsKey,
94+
clientPem: $state->command->tlsCert,
95+
);
96+
echo "Connecting to Temporal service at {$state->address}... ";
97+
try {
98+
$serviceClient->getConnection()->connect(5);
99+
echo "\e[1;32mOK\e[0m\n";
100+
} catch (\Throwable $e) {
101+
echo "\e[1;31mFAILED\e[0m\n";
102+
Support::echoException($e);
103+
throw $e;
104+
}
105+
106+
$converter = DataConverter::createDefault();
107+
108+
$workflowClient = WorkflowClient::create(
109+
serviceClient: $serviceClient,
110+
options: (new ClientOptions())->withNamespace($state->namespace),
111+
converter: $converter,
112+
)->withTimeout(5);
113+
114+
$scheduleClient = ScheduleClient::create(
115+
serviceClient: $serviceClient,
116+
options: (new ClientOptions())->withNamespace($state->namespace),
117+
converter: $converter,
118+
)->withTimeout(5);
119+
120+
$container->bindSingleton(RRStarter::class, $rrRunner);
121+
$container->bindSingleton(TemporalStarter::class, $temporalRunner);
122+
$container->bindSingleton(ServiceClientInterface::class, $serviceClient);
123+
$container->bindSingleton(WorkflowClientInterface::class, $workflowClient);
124+
$container->bindSingleton(ScheduleClientInterface::class, $scheduleClient);
125+
$container->bindInjector(WorkflowStubInterface::class, WorkflowStubInjector::class);
126+
$container->bindSingleton(DataConverterInterface::class, $converter);
127+
$container->bind(RPCInterface::class, static fn() => RPC::create(\getenv('RR_RPC_ADDRESS') ?: 'tcp://127.0.0.1:6001'));
128+
$container->bind(
129+
StorageInterface::class,
130+
static fn(#[Proxy] ContainerInterface $container): StorageInterface => $container->get(Factory::class)->select('harness'),
131+
);
132+
}
133+
}

0 commit comments

Comments
 (0)