diff --git a/psalm-baseline.xml b/psalm-baseline.xml index 10e3ad5fd..253b4b014 100644 --- a/psalm-baseline.xml +++ b/psalm-baseline.xml @@ -1,10 +1,5 @@ - - - - - @@ -15,21 +10,11 @@ - - - - - - - - - - @@ -40,17 +25,6 @@ - - - current()]]> - - - - - - - - @@ -274,62 +248,16 @@ - - - - - - - - - - - - - - - values]]> - - - - - - - - getName()]]> - - - getCode()]]> - getCode()]]> - - - name]]> - - - - - getName()]]> - - - - - getName()]]> - - - - - - @@ -638,25 +566,6 @@ - - - - - - - - - - - - - - - - - - - last]]> @@ -1079,9 +988,6 @@ - - getReturnType()]]> - @@ -1105,9 +1011,6 @@ start(...$args)->then(fn() => $this->getResult($returnType))]]> - - - @@ -1258,9 +1161,6 @@ - - - getOptions()['name']]]> @@ -1462,9 +1362,6 @@ - - - @@ -1483,16 +1380,6 @@ - - - - - - - - - - @@ -1503,11 +1390,6 @@ - - - getCode()]]> - - @@ -1538,4 +1420,25 @@ + + + + toArray()['assets']]]> + + + + + + + + + + + + + + + + + diff --git a/psalm.xml b/psalm.xml index 4bc82f158..3e1d498a1 100644 --- a/psalm.xml +++ b/psalm.xml @@ -17,6 +17,7 @@ > + diff --git a/src/Activity.php b/src/Activity.php index 8bc210c8a..9e79c2b4e 100644 --- a/src/Activity.php +++ b/src/Activity.php @@ -19,6 +19,9 @@ use Temporal\Exception\OutOfContextException; use Temporal\Internal\Support\Facade; +/** + * @psalm-import-type TType from Type + */ final class Activity extends Facade { /** @@ -92,7 +95,8 @@ public static function hasHeartbeatDetails(): bool * * This method retrieves the payload that was passed into the last call of the {@see Activity::heartbeat()} method. * - * @param Type|string|\ReflectionType|\ReflectionClass|null $type + * @param null|mixed $type + * @psalm-param TType $type * @throws OutOfContextException in the absence of the activity execution context. */ public static function getHeartbeatDetails($type = null): mixed diff --git a/src/Activity/ActivityContextInterface.php b/src/Activity/ActivityContextInterface.php index 3ad652e51..e912dfe70 100644 --- a/src/Activity/ActivityContextInterface.php +++ b/src/Activity/ActivityContextInterface.php @@ -18,6 +18,9 @@ use Temporal\Exception\Client\ActivityCompletionException; use Temporal\Exception\Client\ActivityPausedException; +/** + * @psalm-import-type TType from Type + */ interface ActivityContextInterface { /** @@ -46,7 +49,8 @@ public function hasHeartbeatDetails(): bool; * * @see Activity::getHeartbeatDetails() * - * @param Type|string $type + * @param null|mixed $type + * @psalm-param TType $type */ public function getLastHeartbeatDetails($type = null): mixed; diff --git a/src/Activity/ActivityOptions.php b/src/Activity/ActivityOptions.php index d0548f784..3274ae2af 100644 --- a/src/Activity/ActivityOptions.php +++ b/src/Activity/ActivityOptions.php @@ -154,7 +154,7 @@ public function mergeWith(?MethodRetry $retry = null): self $self = clone $this; if ($retry !== null && $this->diff->isPresent($self, 'retryOptions')) { - $self->retryOptions = $this->retryOptions->mergeWith($retry); + $self->retryOptions = $this->retryOptions?->mergeWith($retry); } return $self; diff --git a/src/Activity/LocalActivityInterface.php b/src/Activity/LocalActivityInterface.php index 9ed095586..f53fdbe9c 100644 --- a/src/Activity/LocalActivityInterface.php +++ b/src/Activity/LocalActivityInterface.php @@ -12,7 +12,6 @@ namespace Temporal\Activity; use Doctrine\Common\Annotations\Annotation\Target; -use JetBrains\PhpStorm\Immutable; use Spiral\Attributes\NamedArgumentConstructor; /** @@ -33,24 +32,4 @@ * @Target({ "CLASS" }) */ #[\Attribute(\Attribute::TARGET_CLASS), NamedArgumentConstructor] -final class LocalActivityInterface extends ActivityInterface -{ - /** - * Prefix to prepend to method names to generate activity types. Default is - * empty string which means that method names are used as activity types. - * - * Note that this value is ignored if a name of an activity is specified - * explicitly through {@see ActivityMethod::$name}. - * - * Be careful about names that contain special characters. These names can - * be used as metric tags. And systems like prometheus ignore metrics which - * have tags with unsupported characters. - */ - #[Immutable] - public string $prefix = ''; - - public function __construct(string $prefix = '') - { - $this->prefix = $prefix; - } -} +final class LocalActivityInterface extends ActivityInterface {} diff --git a/src/Activity/LocalActivityOptions.php b/src/Activity/LocalActivityOptions.php index d8d896ea8..dd722db39 100644 --- a/src/Activity/LocalActivityOptions.php +++ b/src/Activity/LocalActivityOptions.php @@ -94,7 +94,7 @@ public function mergeWith(?MethodRetry $retry = null): self $self = clone $this; if ($retry !== null && $this->diff->isPresent($self, 'retryOptions')) { - $self->retryOptions = $this->retryOptions->mergeWith($retry); + $self->retryOptions = $this->retryOptions?->mergeWith($retry); } return $self; diff --git a/src/Client/Common/Paginator.php b/src/Client/Common/Paginator.php index 0c0c89a50..20485abcf 100644 --- a/src/Client/Common/Paginator.php +++ b/src/Client/Common/Paginator.php @@ -30,7 +30,16 @@ private function __construct( private readonly int $pageNumber, private ?\Closure $counter, ) { - $this->collection = $loader->current(); + $current = $loader->current(); + if (!\is_array($current)) { + throw new \InvalidArgumentException( + \sprintf( + 'Generator must return an array of items, %s returned.', + \get_debug_type($current), + ), + ); + } + $this->collection = $current; } /** diff --git a/src/Common/RetryOptions.php b/src/Common/RetryOptions.php index 83b35a874..dcbf38c91 100644 --- a/src/Common/RetryOptions.php +++ b/src/Common/RetryOptions.php @@ -130,7 +130,6 @@ public function mergeWith(?MethodRetry $retry = null): self * * @param mixed $interval parseable string, null, int|float in seconds, {@see \DateInterval}, or {@see Duration} * @return static - * @psalm-assert DateIntervalValue|null $interval */ #[Pure] public function withInitialInterval(mixed $interval): self @@ -162,7 +161,6 @@ public function withBackoffCoefficient(float $coefficient): self * * @param mixed $interval parseable string, null, int|float in seconds, {@see \DateInterval}, or {@see Duration} * @return static - * @psalm-assert DateIntervalValue|null $interval */ #[Pure] public function withMaximumInterval(mixed $interval): self diff --git a/src/DataConverter/DataConverter.php b/src/DataConverter/DataConverter.php index 2e7c87376..129876c8b 100644 --- a/src/DataConverter/DataConverter.php +++ b/src/DataConverter/DataConverter.php @@ -14,6 +14,9 @@ use Temporal\Api\Common\V1\Payload; use Temporal\Exception\DataConverterException; +/** + * @psalm-import-type TType from Type + */ final class DataConverter implements DataConverterInterface { /** @@ -40,7 +43,7 @@ public static function createDefault(): DataConverterInterface } /** - * @param string|\ReflectionClass|\ReflectionType|Type|null $type + * @param TType $type */ public function fromPayload(Payload $payload, $type): mixed { diff --git a/src/DataConverter/DataConverterInterface.php b/src/DataConverter/DataConverterInterface.php index c3d7d114c..7b8347842 100644 --- a/src/DataConverter/DataConverterInterface.php +++ b/src/DataConverter/DataConverterInterface.php @@ -14,13 +14,15 @@ use Temporal\Api\Common\V1\Payload; use Temporal\Exception\DataConverterException; +/** + * @psalm-import-type TType from Type + */ interface DataConverterInterface { /** - * @param string|\ReflectionClass|\ReflectionType|Type|null $type + * @param TType $type * @return mixed * - * @psalm-mutation-free * @throws DataConverterException */ public function fromPayload(Payload $payload, mixed $type); diff --git a/src/DataConverter/EncodedCollection.php b/src/DataConverter/EncodedCollection.php index 75ef277dd..dd6fff975 100644 --- a/src/DataConverter/EncodedCollection.php +++ b/src/DataConverter/EncodedCollection.php @@ -19,6 +19,7 @@ * @psalm-type TKey = array-key * @psalm-type TValue = mixed * @psalm-type TPayloadsCollection = \Traversable&\ArrayAccess&\Countable + * @psalm-import-type TypeEnum from Type * * @implements \IteratorAggregate */ @@ -27,7 +28,7 @@ class EncodedCollection implements \IteratorAggregate, \Countable private ?DataConverterInterface $converter = null; /** - * @var TPayloadsCollection|null + * @psalm-var TPayloadsCollection|null */ private ?\ArrayAccess $payloads = null; @@ -89,7 +90,7 @@ public function isEmpty(): bool /** * @param array-key $name - * @param Type|string|null $type + * @param Type|TypeEnum|mixed $type */ public function getValue(int|string $name, mixed $type = null): mixed { diff --git a/src/DataConverter/EncodedValues.php b/src/DataConverter/EncodedValues.php index 81e371d4e..c94ca3219 100644 --- a/src/DataConverter/EncodedValues.php +++ b/src/DataConverter/EncodedValues.php @@ -25,6 +25,8 @@ * @psalm-type TPayloadsCollection = Traversable&ArrayAccess&Countable * @psalm-type TKey = int * @psalm-type TValue = string + * @psalm-import-type TType from Type + * @final */ class EncodedValues implements ValuesInterface { @@ -47,7 +49,7 @@ private function __construct() {} public static function empty(): static { - $ev = new static(); + $ev = new self(); $ev->values = []; return $ev; @@ -74,7 +76,8 @@ public static function sliceValues( /** * Decode promise response upon returning it to the domain layer. * - * @param string|\ReflectionClass|\ReflectionType|Type|null $type + * @param null|mixed $type + * @psalm-param TType $type */ public static function decodePromise(PromiseInterface $promise, $type = null): PromiseInterface { @@ -91,7 +94,7 @@ static function (mixed $value) use ($type) { public static function fromValues(array $values, ?DataConverterInterface $dataConverter = null): static { - $ev = new static(); + $ev = new self(); $ev->values = \array_values($values); $ev->converter = $dataConverter; @@ -105,7 +108,7 @@ public static function fromPayloadCollection( \Traversable $payloads, ?DataConverterInterface $dataConverter = null, ): static { - $ev = new static(); + $ev = new self(); $ev->payloads = $payloads; $ev->converter = $dataConverter; @@ -208,8 +211,10 @@ private function toProtoCollection(): array return $data; } - foreach ($this->values as $key => $value) { - $data[$key] = $this->valueToPayload($value); + if ($this->values !== null) { + foreach ($this->values as $key => $value) { + $data[$key] = $this->valueToPayload($value); + } } return $data; diff --git a/src/DataConverter/JsonConverter.php b/src/DataConverter/JsonConverter.php index a7d447d75..f04d7eb85 100644 --- a/src/DataConverter/JsonConverter.php +++ b/src/DataConverter/JsonConverter.php @@ -64,7 +64,7 @@ public function toPayload($value): ?Payload try { return $this->create(\json_encode($value, self::JSON_FLAGS)); } catch (\Throwable $e) { - throw new DataConverterException($e->getMessage(), $e->getCode(), $e); + throw new DataConverterException($e->getMessage(), (int) $e->getCode(), $e); } } @@ -82,7 +82,7 @@ public function fromPayload(Payload $payload, Type $type) self::JSON_FLAGS, ); } catch (\Throwable $e) { - throw new DataConverterException($e->getMessage(), $e->getCode(), $e); + throw new DataConverterException($e->getMessage(), (int) $e->getCode(), $e); } if ($data === null && $type->allowsNull()) { @@ -151,6 +151,9 @@ public function fromPayload(Payload $payload, Type $type) try { $reflection = new \ReflectionClass($type->getName()); if (PHP_VERSION_ID >= 80104 && $reflection->isEnum()) { + /** + * @var \UnitEnum $data + */ return $reflection->getConstant($data->name); } } catch (\ReflectionException $e) { diff --git a/src/DataConverter/PayloadConverterInterface.php b/src/DataConverter/PayloadConverterInterface.php index 24b125787..e0ba39463 100644 --- a/src/DataConverter/PayloadConverterInterface.php +++ b/src/DataConverter/PayloadConverterInterface.php @@ -34,6 +34,7 @@ public function toPayload($value): ?Payload; * @return mixed * * @throws DataConverterException + * @mutation-free */ public function fromPayload(Payload $payload, Type $type); } diff --git a/src/DataConverter/Type.php b/src/DataConverter/Type.php index 1ecb30567..4f605ba1c 100644 --- a/src/DataConverter/Type.php +++ b/src/DataConverter/Type.php @@ -14,7 +14,10 @@ use Temporal\Workflow\ReturnType; /** + * @template TIsClass of bool + * * @psalm-type TypeEnum = Type::TYPE_* + * @psalm-type TType = string|\ReflectionClass|\ReflectionType|Type|null */ final class Type { @@ -33,7 +36,7 @@ final class Type private readonly bool $allowsNull; /** - * @param TypeEnum|string $name + * @psalm-param TypeEnum|string $name */ public function __construct( private readonly string $name = Type::TYPE_ANY, @@ -75,7 +78,7 @@ public static function fromReflectionType(\ReflectionType $type): self } /** - * @param string|\ReflectionClass|\ReflectionType|Type|ReturnType $type + * @param string|\ReflectionClass|\ReflectionType|Type|ReturnType|null $type */ public static function create($type): Type { @@ -89,6 +92,9 @@ public static function create($type): Type }; } + /** + * @psalm-return (TIsClass is true ? class-string : string) + */ public function getName(): string { return $this->name; @@ -104,6 +110,10 @@ public function isUntyped(): bool return $this->name === self::TYPE_ANY; } + /** + * @psalm-assert-if-true self $this + * @psalm-assert-if-false self $this + */ public function isClass(): bool { return \class_exists($this->name); diff --git a/src/DataConverter/ValuesInterface.php b/src/DataConverter/ValuesInterface.php index f1d3a662e..7854c7486 100644 --- a/src/DataConverter/ValuesInterface.php +++ b/src/DataConverter/ValuesInterface.php @@ -25,7 +25,7 @@ interface ValuesInterface extends \Countable */ public function isEmpty(): bool; - public function setDataConverter(DataConverterInterface $converter); + public function setDataConverter(DataConverterInterface $converter): void; /** * Get value by it's index. diff --git a/src/Internal/Events/EventEmitterInterface.php b/src/Internal/Events/EventEmitterInterface.php index a49df23c6..114a2a361 100644 --- a/src/Internal/Events/EventEmitterInterface.php +++ b/src/Internal/Events/EventEmitterInterface.php @@ -12,7 +12,7 @@ namespace Temporal\Internal\Events; /** - * @template-covariant T of string + * @template T of string */ interface EventEmitterInterface { diff --git a/src/Internal/Events/EventEmitterTrait.php b/src/Internal/Events/EventEmitterTrait.php index c03b71657..de834576e 100644 --- a/src/Internal/Events/EventEmitterTrait.php +++ b/src/Internal/Events/EventEmitterTrait.php @@ -15,7 +15,7 @@ * @mixin EventEmitterInterface * @mixin EventListenerInterface * - * @template-covariant T of string + * @template T of string * * @template-implements EventEmitterInterface * @template-implements EventListenerInterface @@ -23,11 +23,11 @@ trait EventEmitterTrait { /** - * @var array> + * @var array> */ protected array $once = []; - public function once(string $event, callable $then): self + public function once(string $event, callable $then): static { $this->once[$event][] = $then; @@ -36,7 +36,10 @@ public function once(string $event, callable $then): self public function emit(string $event, array $arguments = []): void { - while (($this->once[$event] ?? []) !== []) { + if (!\array_key_exists($event, $this->once)) { + return; + } + while (!empty($this->once[$event])) { $callback = \array_shift($this->once[$event]); $callback(...$arguments); } diff --git a/src/Internal/Events/EventListenerInterface.php b/src/Internal/Events/EventListenerInterface.php index c9cf01516..f36999f0c 100644 --- a/src/Internal/Events/EventListenerInterface.php +++ b/src/Internal/Events/EventListenerInterface.php @@ -12,7 +12,7 @@ namespace Temporal\Internal\Events; /** - * @template-covariant T of string + * @template T of string */ interface EventListenerInterface { @@ -20,5 +20,5 @@ interface EventListenerInterface * @param T $event * @return $this */ - public function once(string $event, callable $then): self; + public function once(string $event, callable $then): static; } diff --git a/src/Internal/Workflow/ChildWorkflowStub.php b/src/Internal/Workflow/ChildWorkflowStub.php index 2bc9fb5b4..a918581f8 100644 --- a/src/Internal/Workflow/ChildWorkflowStub.php +++ b/src/Internal/Workflow/ChildWorkflowStub.php @@ -14,6 +14,7 @@ use React\Promise\Deferred; use React\Promise\PromiseInterface; use Temporal\DataConverter\EncodedValues; +use Temporal\DataConverter\Type; use Temporal\DataConverter\ValuesInterface; use Temporal\Interceptor\Header; use Temporal\Interceptor\HeaderInterface; @@ -29,6 +30,9 @@ use Temporal\Workflow\ParentClosePolicy; use Temporal\Workflow\WorkflowExecution; +/** + * @psalm-import-type TType from Type + */ final class ChildWorkflowStub implements ChildWorkflowStubInterface { private Deferred $execution; diff --git a/src/Worker/ActivityInvocationCache/RoadRunnerActivityInvocationCache.php b/src/Worker/ActivityInvocationCache/RoadRunnerActivityInvocationCache.php index d70f90833..0ecfbcab5 100644 --- a/src/Worker/ActivityInvocationCache/RoadRunnerActivityInvocationCache.php +++ b/src/Worker/ActivityInvocationCache/RoadRunnerActivityInvocationCache.php @@ -41,7 +41,7 @@ public function clear(): void $this->cache->clear(); } - public function saveCompletion(string $activityMethodName, $value): void + public function saveCompletion(string $activityMethodName, mixed $value): void { $this->cache->set($activityMethodName, ActivityInvocationResult::fromValue($value, $this->dataConverter)); } diff --git a/src/WorkerFactory.php b/src/WorkerFactory.php index a8496f2e7..146387ced 100644 --- a/src/WorkerFactory.php +++ b/src/WorkerFactory.php @@ -111,6 +111,7 @@ public function __construct( ?ServiceCredentials $credentials = null, ) { $this->converter = $dataConverter; + $this->codec = $this->createCodec(); $this->boot($credentials ?? ServiceCredentials::create()); } @@ -190,7 +191,6 @@ public function getEnvironment(): EnvironmentInterface public function run(?HostConnectionInterface $host = null): int { $host ??= RoadRunner::create(); - $this->codec = $this->createCodec(); while ($msg = $host->waitBatch()) { try { diff --git a/src/Workflow/ChildWorkflowOptions.php b/src/Workflow/ChildWorkflowOptions.php index 29173d86b..d30c21d0b 100644 --- a/src/Workflow/ChildWorkflowOptions.php +++ b/src/Workflow/ChildWorkflowOptions.php @@ -211,7 +211,7 @@ public function mergeWith(?MethodRetry $retry = null, ?CronSchedule $cron = null $self = clone $this; if ($retry !== null && $self->diff->isPresent($self, 'retryOptions')) { - $self->retryOptions = $self->retryOptions->mergeWith($retry); + $self->retryOptions = $self->retryOptions?->mergeWith($retry); } if ($cron !== null && $self->diff->isPresent($self, 'cronSchedule')) { diff --git a/src/Workflow/ChildWorkflowStubInterface.php b/src/Workflow/ChildWorkflowStubInterface.php index f02a9139d..6b208d250 100644 --- a/src/Workflow/ChildWorkflowStubInterface.php +++ b/src/Workflow/ChildWorkflowStubInterface.php @@ -15,6 +15,9 @@ use Temporal\DataConverter\Type; use Temporal\Internal\Transport\CompletableResultInterface; +/** + * @psalm-import-type TType from Type + */ interface ChildWorkflowStubInterface { /** @@ -27,7 +30,7 @@ public function getChildWorkflowType(): string; public function getOptions(): ChildWorkflowOptions; /** - * @param Type|string|\ReflectionType|\ReflectionClass|null $returnType + * @param TType $returnType * * @return CompletableResultInterface */ @@ -40,6 +43,9 @@ public function execute(array $args = [], $returnType = null): PromiseInterface; */ public function start(...$args): PromiseInterface; + /** + * @param TType $returnType + */ public function getResult($returnType = null): PromiseInterface; /** diff --git a/src/Workflow/Saga.php b/src/Workflow/Saga.php index 6853e3ab4..edde8d070 100644 --- a/src/Workflow/Saga.php +++ b/src/Workflow/Saga.php @@ -79,7 +79,7 @@ function () { yield Workflow::asyncDetached($handler); } catch (\Throwable $e) { if ($sagaException === null) { - $sagaException = new CompensationException($e->getMessage(), $e->getCode(), $e); + $sagaException = new CompensationException($e->getMessage(), (int) $e->getCode(), $e); } if (!$this->continueWithError) { diff --git a/testing/src/ActivityMocker.php b/testing/src/ActivityMocker.php index 8ef6e52b7..0c45d26d7 100644 --- a/testing/src/ActivityMocker.php +++ b/testing/src/ActivityMocker.php @@ -21,11 +21,17 @@ public function clear(): void $this->cache->clear(); } - public function expectCompletion(string $activityMethodName, $value): void + /** + * @param non-empty-string $activityMethodName + */ + public function expectCompletion(string $activityMethodName, mixed $value): void { $this->cache->saveCompletion($activityMethodName, $value); } + /** + * @param non-empty-string $activityMethodName + */ public function expectFailure(string $activityMethodName, \Throwable $error): void { $this->cache->saveFailure($activityMethodName, $error); diff --git a/testing/src/Command.php b/testing/src/Command.php index 8c25dbdc8..ad24b86bb 100644 --- a/testing/src/Command.php +++ b/testing/src/Command.php @@ -12,14 +12,13 @@ final class Command /** @var non-empty-string|null Temporal Address */ public ?string $address = null; - /** @var non-empty-string|null */ public ?string $tlsKey = null; - - /** @var non-empty-string|null */ public ?string $tlsCert = null; + private array $xdebug = []; - private array $xdebug; - + /** + * @param non-empty-string|null $address + */ public function __construct( ?string $address = null, ) { @@ -28,9 +27,11 @@ public function __construct( public static function fromEnv(): self { - $self = new self(\getenv('TEMPORAL_ADDRESS') ?: '127.0.0.1:7233'); + $address = \getenv('TEMPORAL_ADDRESS'); + $self = new self((\is_string($address) && $address !== '') ? $address : '127.0.0.1:7233'); - $self->namespace = \getenv('TEMPORAL_NAMESPACE') ?: 'default'; + $namespace = \getenv('TEMPORAL_NAMESPACE'); + $self->namespace = (\is_string($namespace) && $namespace !== '') ? $namespace : 'default'; $self->xdebug = [ 'xdebug.mode' => \ini_get('xdebug.mode'), 'xdebug.start_with_request' => \ini_get('xdebug.start_with_request'), @@ -70,13 +71,17 @@ public static function fromCommandLine(array $argv): self break; } } + if (empty($address)) { + throw new \InvalidArgumentException('Address cannot be empty'); + } + if (empty($namespace)) { + throw new \InvalidArgumentException('Namespace cannot be empty'); + } $self = new self($address); - $self->namespace = $namespace; $self->tlsCert = $tlsCert; $self->tlsKey = $tlsKey; - return $self; } diff --git a/testing/src/Downloader.php b/testing/src/Downloader.php index 08ac97b63..f95e4f9f2 100644 --- a/testing/src/Downloader.php +++ b/testing/src/Downloader.php @@ -15,6 +15,7 @@ final class Downloader private Filesystem $filesystem; private HttpClientInterface $httpClient; private string $javaSdkUrl; + private string $workingDir; public function __construct( Filesystem $filesystem, @@ -27,15 +28,23 @@ public function __construct( $javaSdkVersion === self::TAG_LATEST => self::TAG_LATEST, default => "tags/$javaSdkVersion", }; + + $workingDir = \getcwd(); + if ($workingDir === false) { + throw new \RuntimeException('Failed to get current working directory.'); + } + + $this->workingDir = $workingDir; } public function download(SystemInfo $systemInfo): void { $asset = $this->getAsset($systemInfo); + /** @var string $assetUrl */ $assetUrl = $asset['browser_download_url']; $pathToExtractedAsset = $this->downloadAsset($assetUrl); - $targetPath = \getcwd() . DIRECTORY_SEPARATOR . $systemInfo->temporalServerExecutable; + $targetPath = $this->workingDir . DIRECTORY_SEPARATOR . $systemInfo->temporalServerExecutable; $this->filesystem->copy($pathToExtractedAsset . DIRECTORY_SEPARATOR . $systemInfo->temporalServerExecutable, $targetPath); $this->filesystem->chmod($targetPath, 0755); $this->filesystem->remove($pathToExtractedAsset); @@ -78,7 +87,7 @@ private function findAsset(array $assets, SystemInfo $systemInfo): array private function downloadAsset(string $assetUrl): string { $response = $this->httpClient->request('GET', $assetUrl); - $assetPath = \getcwd() . DIRECTORY_SEPARATOR . \basename($assetUrl); + $assetPath = $this->workingDir . DIRECTORY_SEPARATOR . \basename($assetUrl); if ($this->filesystem->exists($assetPath)) { $this->filesystem->remove($assetPath); @@ -87,9 +96,9 @@ private function downloadAsset(string $assetUrl): string $this->filesystem->appendToFile($assetPath, $response->getContent()); $phar = new \PharData($assetPath); - $extractedPath = \getcwd() . DIRECTORY_SEPARATOR . $phar->getFilename(); + $extractedPath = $this->workingDir . DIRECTORY_SEPARATOR . $phar->getFilename(); if (!$this->filesystem->exists($extractedPath)) { - $phar->extractTo(\getcwd()); + $phar->extractTo($this->workingDir); } $this->filesystem->remove($phar->getPath()); diff --git a/testing/src/Environment.php b/testing/src/Environment.php index 2fa8119ee..e510bfcbe 100644 --- a/testing/src/Environment.php +++ b/testing/src/Environment.php @@ -36,16 +36,19 @@ public function __construct( public static function create(?Command $command = null): self { - $token = \getenv('GITHUB_TOKEN'); - $systemInfo = SystemInfo::detect(); - \is_string(\getenv('ROADRUNNER_BINARY')) and $systemInfo->rrExecutable = \getenv('ROADRUNNER_BINARY'); + $roadRunnerBinary = \getenv('ROADRUNNER_BINARY'); + if (\is_string($roadRunnerBinary)) { + $systemInfo->rrExecutable = $roadRunnerBinary; + } + + $token = \getenv('GITHUB_TOKEN'); return new self( new TestOutputStyle(new ArgvInput(), new ConsoleOutput()), new Downloader(new Filesystem(), HttpClient::create([ 'headers' => [ - 'authorization' => $token ? 'token ' . $token : null, + 'authorization' => \is_string($token) ? 'token ' . $token : null, ], ])), $systemInfo, @@ -122,7 +125,7 @@ public function startTemporalServer( $this->io->info('Running command: ' . $this->serializeProcess($this->temporalServerProcess)); $this->temporalServerProcess->start(); - $deadline = \microtime(true) + $commandTimeout; + $deadline = \microtime(true) + (float) $commandTimeout; while (!$temporalStarted && \microtime(true) < $deadline) { \usleep(10_000); $check = new Process([ @@ -162,7 +165,7 @@ public function startTemporalTestServer(int $commandTimeout = 10): void $this->io->info('Temporal test server downloaded.'); } - $temporalPort = \parse_url($this->command->address, PHP_URL_PORT); + $temporalPort = \parse_url((string) $this->command->address, PHP_URL_PORT); $this->io->info('Starting Temporal test server... '); $this->temporalTestServerProcess = new Process( @@ -192,7 +195,7 @@ public function startTemporalTestServer(int $commandTimeout = 10): void /** * @param array $envs */ - public function startRoadRunner(?string $rrCommand = null, int $commandTimeout = 10, array $envs = [], string $configFile = '.rr.yaml'): void + public function startRoadRunner(?array $rrCommand = null, int $commandTimeout = 10, array $envs = [], string $configFile = '.rr.yaml'): void { if (!$this->isTemporalRunning() && !$this->isTemporalTestRunning()) { $this->io->error([ @@ -202,7 +205,7 @@ public function startRoadRunner(?string $rrCommand = null, int $commandTimeout = } $this->roadRunnerProcess = new Process( - command: $rrCommand ? \explode(' ', $rrCommand) : [$this->systemInfo->rrExecutable, 'serve'], + command: $rrCommand ?? [$this->systemInfo->rrExecutable, 'serve'], env: $envs, ); $this->roadRunnerProcess->setTimeout($commandTimeout); @@ -213,7 +216,7 @@ public function startRoadRunner(?string $rrCommand = null, int $commandTimeout = $this->roadRunnerProcess->start(); // wait for roadrunner to start - $deadline = \microtime(true) + $commandTimeout; + $deadline = \microtime(true) + (float) $commandTimeout; while (!$roadRunnerStarted && \microtime(true) < $deadline) { \usleep(10_000); $check = new Process([$this->systemInfo->rrExecutable, 'workers', '-c', $configFile]); @@ -289,26 +292,38 @@ public function stopRoadRunner(): void } } + /** + * @psalm-assert Process $this->temporalServerProcess + */ public function isTemporalRunning(): bool { return $this->temporalServerProcess?->isRunning() === true; } + /** + * @psalm-assert Process $this->roadRunnerProcess + */ public function isRoadRunnerRunning(): bool { return $this->roadRunnerProcess?->isRunning() === true; } + /** + * @psalm-assert Process $this->temporalTestServerProcess + */ public function isTemporalTestRunning(): bool { return $this->temporalTestServerProcess?->isRunning() === true; } - private function serializeProcess(?Process $temporalServerProcess): string|array + private function serializeProcess(?Process $process): string { - $reflection = new \ReflectionClass($temporalServerProcess); + if ($process === null) { + return 'process is not started'; + } + $reflection = new \ReflectionClass($process); $reflectionProperty = $reflection->getProperty('commandline'); - $commandLine = $reflectionProperty->getValue($temporalServerProcess); + $commandLine = $reflectionProperty->getValue($process); return \implode(' ', $commandLine); } } diff --git a/testing/src/Replay/WorkflowReplayer.php b/testing/src/Replay/WorkflowReplayer.php index 325f1f1ab..a3e0f8a1f 100644 --- a/testing/src/Replay/WorkflowReplayer.php +++ b/testing/src/Replay/WorkflowReplayer.php @@ -14,7 +14,6 @@ use Temporal\Api\Common\V1\WorkflowExecution; use Temporal\Api\Common\V1\WorkflowType; use Temporal\Api\History\V1\History; -use Temporal\Api\History\V1\HistoryEvent; use Temporal\Client\GRPC\StatusCode; use Temporal\Testing\Replay\Exception\InternalServerException; use Temporal\Testing\Replay\Exception\InvalidArgumentException; @@ -34,7 +33,8 @@ final class WorkflowReplayer public function __construct() { - $this->rpc = new RPC(Relay::create(Environment::fromGlobals()->getRPCAddress()), new ProtobufCodec()); + $rpcAddress = Environment::fromGlobals()->getRPCAddress(); + $this->rpc = new RPC(Relay::create(!empty($rpcAddress) ? $rpcAddress : 'tcp://127.0.0.1:6001'), new ProtobufCodec()); } /** @@ -44,7 +44,6 @@ public function __construct() */ public function replayHistory(History $history): void { - /** @var HistoryEvent|null $firstEvent */ $firstEvent = $history->getEvents()[0] ?? null; $workflowType = $firstEvent?->getWorkflowExecutionStartedEventAttributes()?->getWorkflowType()?->getName() ?? throw new \LogicException('History is empty or broken.'); @@ -110,7 +109,10 @@ public function replayFromJSON( $this->sendRequest('temporal.ReplayFromJSON', $request); } - private function sendRequest(string $command, Message $request): ReplayResponse + /** + * @param non-empty-string $command + */ + private function sendRequest(string $command, Message $request): void { $wfType = (string) $request->getWorkflowType()?->getName(); try { @@ -132,7 +134,7 @@ private function sendRequest(string $command, Message $request): ReplayResponse \assert($status !== null); if ($status->getCode() === 0) { - return $message; + return; } throw match ($status->getCode()) { diff --git a/testing/src/SystemInfo.php b/testing/src/SystemInfo.php index 353c85a66..74fd293d6 100644 --- a/testing/src/SystemInfo.php +++ b/testing/src/SystemInfo.php @@ -10,9 +10,9 @@ final class SystemInfo { private const PLATFORM_MAPPINGS = [ - 'darwin' => 'macOS', - 'linux' => 'linux', - 'windows' => 'windows', + OperatingSystem::OS_DARWIN => 'macOS', + OperatingSystem::OS_LINUX => 'linux', + OperatingSystem::OS_WINDOWS => 'windows', ]; private const ARCHITECTURE_MAPPINGS = [ 'x64' => 'amd64', @@ -20,19 +20,19 @@ final class SystemInfo 'arm64' => 'arm64', ]; private const TEMPORAL_EXECUTABLE_MAP = [ - 'darwin' => './temporal-test-server', - 'linux' => './temporal-test-server', - 'windows' => 'temporal-test-server.exe', + OperatingSystem::OS_DARWIN => './temporal-test-server', + OperatingSystem::OS_LINUX => './temporal-test-server', + OperatingSystem::OS_WINDOWS => 'temporal-test-server.exe', ]; private const TEMPORAL_CLI_EXECUTABLE_MAP = [ - 'darwin' => './temporal', - 'linux' => './temporal', - 'windows' => 'temporal.exe', + OperatingSystem::OS_DARWIN => './temporal', + OperatingSystem::OS_LINUX => './temporal', + OperatingSystem::OS_WINDOWS => 'temporal.exe', ]; private const RR_EXECUTABLE_MAP = [ - 'darwin' => './rr', - 'linux' => './rr', - 'windows' => 'rr.exe', + OperatingSystem::OS_DARWIN => './rr', + OperatingSystem::OS_LINUX => './rr', + OperatingSystem::OS_WINDOWS => 'rr.exe', ]; public string $arch; @@ -61,14 +61,16 @@ private function __construct( public static function detect(): self { $os = OperatingSystem::createFromGlobals(); + $architecture = Architecture::createFromGlobals(); + $rrBinary = \getenv('ROADRUNNER_BINARY'); return new self( $os, - self::PLATFORM_MAPPINGS[$os], - self::ARCHITECTURE_MAPPINGS[Architecture::createFromGlobals()], - self::TEMPORAL_EXECUTABLE_MAP[$os], - \getenv('ROADRUNNER_BINARY') ?: self::RR_EXECUTABLE_MAP[$os], - self::TEMPORAL_CLI_EXECUTABLE_MAP[$os], + self::PLATFORM_MAPPINGS[$os] ?? self::PLATFORM_MAPPINGS[OperatingSystem::OS_LINUX], + self::ARCHITECTURE_MAPPINGS[$architecture] ?? self::ARCHITECTURE_MAPPINGS['amd64'], + self::TEMPORAL_EXECUTABLE_MAP[$os] ?? self::TEMPORAL_EXECUTABLE_MAP[OperatingSystem::OS_LINUX], + (\is_string($rrBinary) && $rrBinary !== '') ? $rrBinary : (self::RR_EXECUTABLE_MAP[$os] ?? self::RR_EXECUTABLE_MAP[OperatingSystem::OS_LINUX]), + self::TEMPORAL_CLI_EXECUTABLE_MAP[$os] ?? self::TEMPORAL_CLI_EXECUTABLE_MAP[OperatingSystem::OS_LINUX], ); } } diff --git a/testing/src/TestService.php b/testing/src/TestService.php index 26260bbec..bdb283a47 100644 --- a/testing/src/TestService.php +++ b/testing/src/TestService.php @@ -108,7 +108,7 @@ public function getCurrentTime(): Carbon { /** @var GetCurrentTimeResponse $result */ $result = $this->invoke('GetCurrentTime', new GPBEmpty()); - return Carbon::createFromTimestamp($result->getTime()->getSeconds()); + return Carbon::createFromTimestamp($result->getTime()?->getSeconds() ?? 0); } private function invoke(string $method, object $request): object diff --git a/testing/src/WorkerFactory.php b/testing/src/WorkerFactory.php index 181b3bca0..d5bc82626 100644 --- a/testing/src/WorkerFactory.php +++ b/testing/src/WorkerFactory.php @@ -38,6 +38,9 @@ public function __construct( parent::__construct($dataConverter, $rpc, $credentials ?? ServiceCredentials::create()); } + /** + * @psalm-suppress UnsafeInstantiation + */ public static function create( ?DataConverterInterface $converter = null, ?RPCConnectionInterface $rpc = null, @@ -63,7 +66,7 @@ public function newWorker( $worker = new WorkerMock( new Worker( $taskQueue, - $options ?? WorkerOptions::new(), + $options, ServiceContainer::fromWorkerFactory( $this, $exceptionInterceptor ?? ExceptionInterceptor::createDefault(), diff --git a/testing/src/WorkerMock.php b/testing/src/WorkerMock.php index a725dbfa9..6e9ec2885 100644 --- a/testing/src/WorkerMock.php +++ b/testing/src/WorkerMock.php @@ -7,27 +7,24 @@ use React\Promise\PromiseInterface; use Temporal\Internal\Events\EventEmitterTrait; use Temporal\Internal\Events\EventListenerInterface; -use Temporal\Internal\Repository\RepositoryInterface; use Temporal\Worker\ActivityInvocationCache\ActivityInvocationCacheInterface; use Temporal\Worker\DispatcherInterface; use Temporal\Worker\Transport\Command\ServerRequestInterface; use Temporal\Worker\WorkerInterface; use Temporal\Worker\WorkerOptions; +/** + * @template-implements EventListenerInterface + */ final class WorkerMock implements WorkerInterface, EventListenerInterface, DispatcherInterface { + /** @use EventEmitterTrait */ use EventEmitterTrait; - private WorkerInterface $wrapped; - private ActivityInvocationCacheInterface $activityInvocationCache; - public function __construct( - WorkerInterface $wrapped, - ActivityInvocationCacheInterface $activityInvocationCache, - ) { - $this->wrapped = $wrapped; - $this->activityInvocationCache = $activityInvocationCache; - } + private WorkerInterface&DispatcherInterface $wrapped, + private ActivityInvocationCacheInterface $activityInvocationCache, + ) {} public function getOptions(): WorkerOptions { @@ -43,37 +40,45 @@ public function dispatch(ServerRequestInterface $request, array $headers): Promi return $this->wrapped->dispatch($request, $headers); } - public function getID(): string + public function getID(): int|string { return $this->wrapped->getID(); } public function registerWorkflowTypes(string ...$class): WorkerInterface { - return $this->wrapped->registerWorkflowTypes(...$class); + $this->wrapped->registerWorkflowTypes(...$class); + + return $this; } - public function getWorkflows(): RepositoryInterface + public function getWorkflows(): iterable { return $this->wrapped->getWorkflows(); } public function registerActivityImplementations(object ...$activity): WorkerInterface { - return $this->wrapped->registerActivityImplementations(...$activity); + $this->wrapped->registerActivityImplementations(...$activity); + + return $this; } public function registerActivity(string $type, ?callable $factory = null): WorkerInterface { - return $this->wrapped->registerActivity($type, $factory); + $this->wrapped->registerActivity($type, $factory); + + return $this; } public function registerActivityFinalizer(\Closure $finalizer): WorkerInterface { - return $this->wrapped->registerActivityFinalizer($finalizer); + $this->wrapped->registerActivityFinalizer($finalizer); + + return $this; } - public function getActivities(): RepositoryInterface + public function getActivities(): iterable { return $this->wrapped->getActivities(); } diff --git a/testing/src/WorkflowTestCase.php b/testing/src/WorkflowTestCase.php index 6a64f9e72..95b394269 100644 --- a/testing/src/WorkflowTestCase.php +++ b/testing/src/WorkflowTestCase.php @@ -8,6 +8,9 @@ use Temporal\Client\GRPC\ServiceClient; use Temporal\Client\WorkflowClient; +/** + * @psalm-suppress PropertyNotSetInConstructor + */ class WorkflowTestCase extends TestCase { protected WorkflowClient $workflowClient; @@ -15,7 +18,8 @@ class WorkflowTestCase extends TestCase protected function setUp(): void { - $temporalAddress = \getenv('TEMPORAL_ADDRESS') ?: '127.0.0.1:7233'; + $temporalAddress = \getenv('TEMPORAL_ADDRESS'); + $temporalAddress = \is_string($temporalAddress) && !empty($temporalAddress) ? $temporalAddress : '127.0.0.1:7233'; $this->workflowClient = new WorkflowClient(ServiceClient::create($temporalAddress)); $this->testingService = TestService::create($temporalAddress); diff --git a/tests/Acceptance/App/Runtime/RRStarter.php b/tests/Acceptance/App/Runtime/RRStarter.php index 0e826be57..76b3930db 100644 --- a/tests/Acceptance/App/Runtime/RRStarter.php +++ b/tests/Acceptance/App/Runtime/RRStarter.php @@ -46,11 +46,10 @@ public function start(): void ]; $run->tlsKey === null or $rrCommand = [...$rrCommand, '-o', "tls.key={$run->tlsKey}"]; $run->tlsCert === null or $rrCommand = [...$rrCommand, '-o', "tls.cert={$run->tlsCert}"]; - $command = \implode(' ', $rrCommand); // echo "\e[1;36mStart RoadRunner with command:\e[0m {$command}\n"; $this->environment->startRoadRunner( - rrCommand: $command, + rrCommand: $rrCommand, configFile: $this->runtime->rrConfigDir . DIRECTORY_SEPARATOR . '.rr.yaml', ); } diff --git a/tests/Functional/bootstrap.php b/tests/Functional/bootstrap.php index c8837af0b..e52282867 100644 --- a/tests/Functional/bootstrap.php +++ b/tests/Functional/bootstrap.php @@ -15,7 +15,7 @@ $environment->startTemporalTestServer(); (new SearchAttributeTestInvoker())(); $environment->startRoadRunner( - rrCommand: \implode(' ', [ + rrCommand: [ $systemInfo->rrExecutable, 'serve', '-c', '.rr.silent.yaml', @@ -27,7 +27,7 @@ 'worker.php', ...$environment->command->getCommandLineArguments(), ]), - ]), + ], configFile: 'tests/Functional/.rr.silent.yaml', );