Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Worker\SearchAttributeInvocationCache;

final class InMemorySearchAttributeInvocationCache implements SearchAttributeInvocationCacheInterface
{
/**
* @var array<string, array{operation: string, type: string, value?: mixed}>
*/
private array $upserts = [];

public function clear(): void
{
$this->upserts = [];
}

public function recordUpsert(string $name, array $entry): void
{
$this->upserts[$name] = $entry;
}

public function wasUpserted(string $name): bool
{
return \array_key_exists($name, $this->upserts);
}

public function getUpsert(string $name): ?array
{
return $this->upserts[$name] ?? null;
}

public function all(): array
{
return $this->upserts;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Worker\SearchAttributeInvocationCache;

use Spiral\Goridge\RPC\RPC;
use Spiral\RoadRunner\Environment;
use Spiral\RoadRunner\KeyValue\Factory;
use Spiral\RoadRunner\KeyValue\StorageInterface;

final class RoadRunnerSearchAttributeInvocationCache implements SearchAttributeInvocationCacheInterface
{
private const CACHE_NAME = 'test';
private const STORE_KEY = '__temporal_testing_search_attributes__';

private StorageInterface $cache;

/**
* @param non-empty-string $host
* @param non-empty-string $cacheName
*/
public function __construct(string $host, string $cacheName)
{
$this->cache = (new Factory(RPC::create($host)))->select($cacheName);
}

public static function create(): self
{
$env = Environment::fromGlobals();
$host = $env->getRPCAddress();
if ($host === '') {
throw new \RuntimeException('RoadRunner RPC address is not set.');
}

return new self($host, self::CACHE_NAME);
}

public function clear(): void
{
$this->cache->delete(self::STORE_KEY);
}

public function recordUpsert(string $name, array $entry): void
{
$map = $this->map();
$map[$name] = $entry;
$this->cache->set(self::STORE_KEY, $map);
}

public function wasUpserted(string $name): bool
{
return \array_key_exists($name, $this->map());
}

public function getUpsert(string $name): ?array
{
return $this->map()[$name] ?? null;
}

public function all(): array
{
return $this->map();
}

/**
* @return array<string, array{operation: string, type: string, value?: mixed}>
*/
private function map(): array
{
$map = $this->cache->get(self::STORE_KEY);

return \is_array($map) ? $map : [];
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Worker\SearchAttributeInvocationCache;

interface SearchAttributeInvocationCacheInterface
{
public const OPERATION_SET = 'set';
public const OPERATION_UNSET = 'unset';

public function clear(): void;

/**
* @param non-empty-string $name
* @param array{operation: string, type: string, value?: mixed} $entry
*/
public function recordUpsert(string $name, array $entry): void;

/**
* @param non-empty-string $name
*/
public function wasUpserted(string $name): bool;

/**
* @param non-empty-string $name
* @return array{operation: string, type: string, value?: mixed}|null
*/
public function getUpsert(string $name): ?array;

/**
* @return array<string, array{operation: string, type: string, value?: mixed}>
*/
public function all(): array;
}
49 changes: 49 additions & 0 deletions testing/src/MockSearchAttributeInterceptor.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
<?php

declare(strict_types=1);

namespace Temporal\Testing;

use React\Promise\PromiseInterface;
use Temporal\Common\SearchAttributes\SearchAttributeUpdate\ValueSet;
use Temporal\Interceptor\WorkflowOutboundRequestInterceptor;
use Temporal\Internal\Transport\Request\UpsertTypedSearchAttributes;
use Temporal\Worker\SearchAttributeInvocationCache\RoadRunnerSearchAttributeInvocationCache;
use Temporal\Worker\SearchAttributeInvocationCache\SearchAttributeInvocationCacheInterface;
use Temporal\Worker\Transport\Command\RequestInterface;

use function React\Promise\resolve;

final class MockSearchAttributeInterceptor implements WorkflowOutboundRequestInterceptor
{
private SearchAttributeInvocationCacheInterface $cache;

public function __construct(?SearchAttributeInvocationCacheInterface $cache = null)
{
$this->cache = $cache ?? RoadRunnerSearchAttributeInvocationCache::create();
}

public function handleOutboundRequest(RequestInterface $request, callable $next): PromiseInterface
{
if (!$request instanceof UpsertTypedSearchAttributes) {
return $next($request);
}

foreach ($request->getSearchAttributes() as $update) {
$entry = $update instanceof ValueSet
? [
'operation' => SearchAttributeInvocationCacheInterface::OPERATION_SET,
'type' => $update->type->value,
'value' => $update->value,
]
: [
'operation' => SearchAttributeInvocationCacheInterface::OPERATION_UNSET,
'type' => $update->type->value,
];

$this->cache->recordUpsert($update->name, $entry);
}

return resolve(null);
}
}
119 changes: 119 additions & 0 deletions testing/src/SearchAttributeMocker.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
<?php

declare(strict_types=1);

namespace Temporal\Testing;

use PHPUnit\Framework\Assert;
use Temporal\Worker\SearchAttributeInvocationCache\RoadRunnerSearchAttributeInvocationCache;
use Temporal\Worker\SearchAttributeInvocationCache\SearchAttributeInvocationCacheInterface;

final class SearchAttributeMocker
{
private SearchAttributeInvocationCacheInterface $cache;

public function __construct(?SearchAttributeInvocationCacheInterface $cache = null)
{
$this->cache = $cache ?? RoadRunnerSearchAttributeInvocationCache::create();
}

public function clear(): void
{
$this->cache->clear();
}

/**
* @param non-empty-string $name
*/
public function wasUpserted(string $name): bool
{
return $this->cache->wasUpserted($name);
}

/**
* @param non-empty-string $name
*/
public function assertUpserted(string $name): void
{
Assert::assertTrue(
$this->cache->wasUpserted($name),
\sprintf('Expected search attribute "%s" to be upserted, but it was not.', $name),
);
}

/**
* @param non-empty-string $name
*/
public function assertUpsertedValue(string $name, mixed $value): void
{
$entry = $this->cache->getUpsert($name);

Assert::assertNotNull(
$entry,
\sprintf('Expected search attribute "%s" to be upserted, but it was not.', $name),
);
Assert::assertSame(
SearchAttributeInvocationCacheInterface::OPERATION_SET,
$entry['operation'] ?? null,
\sprintf('Expected search attribute "%s" to be set, but it was unset.', $name),
);

$actual = $entry['value'] ?? null;
$message = \sprintf('Search attribute "%s" value mismatch.', $name);

if ($value instanceof \DateTimeInterface) {
Assert::assertEquals($value, $actual, $message);

return;
}

Assert::assertSame($value, $actual, $message);
}

/**
* @param non-empty-string $name
*/
public function assertUnset(string $name): void
{
$entry = $this->cache->getUpsert($name);

Assert::assertNotNull(
$entry,
\sprintf('Expected search attribute "%s" to be unset, but it was not upserted at all.', $name),
);
Assert::assertSame(
SearchAttributeInvocationCacheInterface::OPERATION_UNSET,
$entry['operation'] ?? null,
\sprintf('Expected search attribute "%s" to be unset, but it was set.', $name),
);
}

/**
* @param non-empty-string $name
*/
public function assertNotUpserted(string $name): void
{
Assert::assertFalse(
$this->cache->wasUpserted($name),
\sprintf('Expected search attribute "%s" NOT to be upserted, but it was.', $name),
);
}

/**
* @param non-empty-string $name
*/
public function getUpserted(string $name): mixed
{
$entry = $this->cache->getUpsert($name);

return $entry === null ? null : ($entry['value'] ?? null);
}

/**
* @return array<string, array{operation: string, type: string, value?: mixed}>
*/
public function getUpsertedAttributes(): array
{
return $this->cache->all();
}
}
3 changes: 3 additions & 0 deletions testing/src/WorkflowTestCase.php
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ class WorkflowTestCase extends TestCase
protected TestService $testingService;
protected ActivityMocker $activityMocks;
protected WorkflowMocker $workflowMocks;
protected SearchAttributeMocker $searchAttributeMocks;
protected DelayedCallbackScheduler $delayedCallbacks;

protected function setUp(): void
Expand All @@ -29,6 +30,7 @@ protected function setUp(): void
$this->testingService = TestService::create($temporalAddress);
$this->activityMocks = new ActivityMocker();
$this->workflowMocks = new WorkflowMocker();
$this->searchAttributeMocks = new SearchAttributeMocker();
$this->workflowClient = new WorkflowClient(
ServiceClient::create($temporalAddress),
interceptorProvider: new SimplePipelineProvider($this->clientInterceptors()),
Expand All @@ -42,6 +44,7 @@ protected function tearDown(): void
{
$this->activityMocks->clear();
$this->workflowMocks->clear();
$this->searchAttributeMocks->clear();
$this->assertTimeSkippingBalanced();

parent::tearDown();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
<?php

/**
* This file is part of Temporal package.
*
* For the full copyright and license information, please view the LICENSE
* file that was distributed with this source code.
*/

declare(strict_types=1);

namespace Temporal\Tests\Workflow;

use Temporal\Common\SearchAttributes\SearchAttributeKey;
use Temporal\Workflow;
use Temporal\Workflow\WorkflowMethod;

#[Workflow\WorkflowInterface]
class UpsertTypedSearchAttributesWorkflow
{
#[WorkflowMethod]
public function handler(string $keyword = 'CustomValue')
{
Workflow::upsertTypedSearchAttributes(
SearchAttributeKey::forKeyword('CustomKeyword')->valueSet($keyword),
SearchAttributeKey::forInteger('CustomInt')->valueSet(42),
SearchAttributeKey::forKeyword('index')->valueSet('idx'),
SearchAttributeKey::forKeyword('2024')->valueSet('year'),
);

return 'done';
}
}
Loading