Skip to content

Commit fab4fa6

Browse files
committed
Merge remote-tracking branch 'origin/master' into nexus
# Conflicts: # tests/Acceptance/worker.php
2 parents b99fb77 + 487887e commit fab4fa6

45 files changed

Lines changed: 3300 additions & 147 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.github/workflows/run-test-suite.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,7 @@ jobs:
108108
run: ${{ inputs.test-command }}
109109
env:
110110
XDEBUG_MODE: off
111+
TEMPORAL_TRANSCRIPT_DUMP_ON_FAIL: "1"
111112

112113
- name: Check for failures
113114
if: steps.validate.outcome == 'failure'

.run/Acceptance.run.xml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,11 @@
11
<component name="ProjectRunConfigurationManager">
22
<configuration default="false" name="Acceptance" type="PHPUnitRunConfigurationType" factoryName="PHPUnit">
3+
<CommandLine>
4+
<envs>
5+
<env name="TEMPORAL_TRANSCRIPT_DUMP_ON_FAIL" value="1" />
6+
</envs>
7+
</CommandLine>
38
<TestRunner bootstrap_file="$PROJECT_DIR$/tests/bootstrap.php" configuration_file="$PROJECT_DIR$/phpunit.xml.dist" directory="$PROJECT_DIR$/tests/Acceptance" scope="XML" options="--testsuite=Acceptance --log-junit=runtime/phpunit-acceptance-junit.xml" />
49
<method v="2" />
510
</configuration>
6-
</component>
11+
</component>

composer.json

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -105,7 +105,11 @@
105105
"test:arch": "phpunit --testsuite=Arch --color=always --testdox",
106106
"test:accept": "tests/runner.php vendor/bin/phpunit --testsuite=Acceptance --color=always --testdox",
107107
"test:accept-slow": "tests/runner.php vendor/bin/phpunit --testsuite=\"Acceptance-Slow\" --color=always --testdox",
108-
"test:accept-fast": "tests/runner.php vendor/bin/phpunit --testsuite=\"Acceptance-Fast\" --color=always --testdox"
108+
"test:accept-fast": "tests/runner.php vendor/bin/phpunit --testsuite=\"Acceptance-Fast\" --color=always --testdox",
109+
"transcripts:last": "php tests/Acceptance/transcript-merge.php",
110+
"transcripts:list": "php tests/Acceptance/transcript-merge.php --list",
111+
"transcripts:merge": "php tests/Acceptance/transcript-merge.php",
112+
"transcripts:clean": "rm -rf runtime/tests/transcripts/*"
109113
},
110114
"config": {
111115
"sort-packages": true,

psalm-baseline.xml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1487,6 +1487,82 @@
14871487
<code><![CDATA[ChannelCredentials]]></code>
14881488
</UndefinedClass>
14891489
</file>
1490+
<file src="testing/src/Transcript/TranscriptActivityInterceptor.php">
1491+
<PossiblyNullReference>
1492+
<code><![CDATA[getID]]></code>
1493+
</PossiblyNullReference>
1494+
</file>
1495+
<file src="testing/src/Transcript/TranscriptPaths.php">
1496+
<InvalidOperand>
1497+
<code><![CDATA[\microtime(true) * 1000]]></code>
1498+
</InvalidOperand>
1499+
<RiskyTruthyFalsyComparison>
1500+
<code><![CDATA[\getmypid()]]></code>
1501+
</RiskyTruthyFalsyComparison>
1502+
</file>
1503+
<file src="testing/src/Transcript/TranscriptReader.php">
1504+
<RedundantFunctionCall>
1505+
<code><![CDATA[\array_values]]></code>
1506+
</RedundantFunctionCall>
1507+
</file>
1508+
<file src="testing/src/Transcript/TranscriptRun.php">
1509+
<RedundantFunctionCall>
1510+
<code><![CDATA[\array_values]]></code>
1511+
</RedundantFunctionCall>
1512+
</file>
1513+
<file src="testing/src/Transcript/TranscriptStore.php">
1514+
<ArgumentTypeCoercion>
1515+
<code><![CDATA[TranscriptPaths::writerFile($directory, $processLabel)]]></code>
1516+
</ArgumentTypeCoercion>
1517+
<RiskyTruthyFalsyComparison>
1518+
<code><![CDATA[\getmypid()]]></code>
1519+
</RiskyTruthyFalsyComparison>
1520+
</file>
1521+
<file src="testing/src/Transcript/TranscriptWorkflowInterceptor.php">
1522+
<MissingClosureReturnType>
1523+
<code><![CDATA[fn() => $next($input)]]></code>
1524+
<code><![CDATA[fn() => $next($input)]]></code>
1525+
</MissingClosureReturnType>
1526+
</file>
1527+
<file src="testing/src/Transcript/TranscriptWriter.php">
1528+
<InvalidPropertyAssignmentValue>
1529+
<code><![CDATA[$this->fileDescriptor]]></code>
1530+
<code><![CDATA[$this->fileDescriptor]]></code>
1531+
</InvalidPropertyAssignmentValue>
1532+
<PossiblyInvalidCast>
1533+
<code><![CDATA[$errorRecord['file'] ?? '']]></code>
1534+
<code><![CDATA[$errorRecord['message'] ?? '']]></code>
1535+
</PossiblyInvalidCast>
1536+
<PossiblyNullArgument>
1537+
<code><![CDATA[$this->fileDescriptor]]></code>
1538+
</PossiblyNullArgument>
1539+
<RiskyCast>
1540+
<code><![CDATA[$errorRecord['line'] ?? 0]]></code>
1541+
<code><![CDATA[$errorRecord['type'] ?? 0]]></code>
1542+
</RiskyCast>
1543+
<RiskyTruthyFalsyComparison>
1544+
<code><![CDATA[\getmypid()]]></code>
1545+
</RiskyTruthyFalsyComparison>
1546+
</file>
1547+
<file src="testing/src/Transcript/WireFrameDecoder.php">
1548+
<PossiblyNullArgument>
1549+
<code><![CDATA[$message->getHeader()]]></code>
1550+
<code><![CDATA[$message->getPayloads()]]></code>
1551+
</PossiblyNullArgument>
1552+
<TooManyTemplateParams>
1553+
<code><![CDATA[$fields]]></code>
1554+
<code><![CDATA[$meta]]></code>
1555+
</TooManyTemplateParams>
1556+
</file>
1557+
<file src="testing/src/Transcript/WorkflowHistoryDumper.php">
1558+
<InvalidOperand>
1559+
<code><![CDATA[$eventTime->getSeconds() + \round($eventTime->getNanos() / 1_000_000_000, 6)]]></code>
1560+
<code><![CDATA[($sec - $startSec) * 1000]]></code>
1561+
</InvalidOperand>
1562+
<PossiblyInvalidOperand>
1563+
<code><![CDATA[$eventTime->getSeconds()]]></code>
1564+
</PossiblyInvalidOperand>
1565+
</file>
14901566
<file src="testing/src/WorkerMock.php">
14911567
<DeprecatedMethod>
14921568
<code><![CDATA[registerActivityImplementations]]></code>

runtime/.gitignore

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1 +1,2 @@
11
*
2+
!.gitignore
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Testing\Transcript;
6+
7+
final class MalformedTranscriptException extends \RuntimeException
8+
{
9+
public function __construct(
10+
string $message,
11+
public readonly string $offendingLine,
12+
public readonly int $offendingLineNumber,
13+
public readonly string $offendingFile,
14+
) {
15+
parent::__construct(\sprintf(
16+
'%s (file=%s line=%d offending=%s)',
17+
$message,
18+
$offendingFile,
19+
$offendingLineNumber,
20+
\substr($offendingLine, 0, 200),
21+
));
22+
}
23+
}
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Testing\Transcript;
6+
7+
use Temporal\Activity;
8+
use Temporal\Interceptor\ActivityInbound\ActivityInput;
9+
use Temporal\Interceptor\ActivityInboundInterceptor;
10+
use Temporal\Interceptor\Trait\ActivityInboundInterceptorTrait;
11+
use Temporal\Testing\Transcript\TranscriptWriter;
12+
13+
final class TranscriptActivityInterceptor implements ActivityInboundInterceptor
14+
{
15+
use ActivityInboundInterceptorTrait;
16+
17+
public function __construct(
18+
private readonly TranscriptWriter $transcript,
19+
) {}
20+
21+
public function handleActivityInbound(ActivityInput $input, callable $next): mixed
22+
{
23+
$attributes = $this->buildAttributes();
24+
$this->transcript->writeMeta('activity_start', $attributes);
25+
try {
26+
$result = $next($input);
27+
$this->transcript->writeMeta('activity_completed', $attributes);
28+
return $result;
29+
} catch (\Throwable $exception) {
30+
$this->transcript->writeException('activity_throw', $attributes, $exception);
31+
throw $exception;
32+
}
33+
}
34+
35+
/**
36+
* @return array<string, scalar|null>
37+
*/
38+
private function buildAttributes(): array
39+
{
40+
try {
41+
$info = Activity::getInfo();
42+
return [
43+
'name' => $info->type->name,
44+
'attempt' => $info->attempt,
45+
'activity_id' => $info->id,
46+
'workflow_id' => $info->workflowExecution->getID(),
47+
'run_id' => $info->workflowExecution->getRunID(),
48+
];
49+
} catch (\Throwable) {
50+
return ['name' => 'unknown', 'attempt' => 0];
51+
}
52+
}
53+
}
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Testing\Transcript;
6+
7+
use Psr\Log\LoggerInterface;
8+
use Psr\Log\LoggerTrait;
9+
10+
final class TranscriptAdapter implements LoggerInterface
11+
{
12+
use LoggerTrait;
13+
14+
public function __construct(
15+
private readonly TranscriptWriter $writer,
16+
private readonly LoggerInterface $stderr,
17+
) {
18+
}
19+
20+
public function log($level, \Stringable|string $message, array $context = []): void
21+
{
22+
try {
23+
$this->writer->writeLog((string) $level, (string) $message, $context);
24+
} catch (\Throwable $error) {
25+
$this->stderr->error('transcript-adapter-error', [
26+
'message' => $error->getMessage(),
27+
]);
28+
}
29+
}
30+
}
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Testing\Transcript;
6+
7+
final class TranscriptLine
8+
{
9+
/**
10+
* @param array<string, scalar|null> $attributes
11+
* @param array<string, mixed>|null $payload
12+
*/
13+
public function __construct(
14+
public readonly \DateTimeImmutable $timestamp,
15+
public readonly int $processId,
16+
public readonly int $sequence,
17+
public readonly TranscriptSection $section,
18+
public readonly array $attributes,
19+
public readonly ?array $payload,
20+
public readonly string $rawLine,
21+
) {}
22+
23+
public function getAttribute(string $key): string|int|float|bool|null
24+
{
25+
return $this->attributes[$key] ?? null;
26+
}
27+
28+
public function hasAttribute(string $key): bool
29+
{
30+
return \array_key_exists($key, $this->attributes);
31+
}
32+
}
Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Temporal\Testing\Transcript;
6+
7+
/**
8+
* Single owner of transcript path/id grammar:
9+
* - run id format and sanitization
10+
* - per-process writer filename layout
11+
* - rotation suffix
12+
* - merged-directory + merged-file conventions
13+
* - the reserved underscore-prefix used to mark control directories
14+
*/
15+
final class TranscriptPaths
16+
{
17+
private const SLUG_PATTERN = '~[^A-Za-z0-9_-]~';
18+
private const SLUG_REPLACEMENT = '_';
19+
private const RUN_ID_MAX_LENGTH = 64;
20+
private const PROCESS_LABEL_MAX_LENGTH = 40;
21+
private const LOG_EXTENSION = '.log';
22+
private const MERGED_DIRECTORY = '_merged';
23+
private const MERGED_FILENAME = 'transcript.log';
24+
private const RESERVED_PREFIX = '_';
25+
26+
public static function generateRunId(): string
27+
{
28+
return \date('Ymd-His') . '-' . \bin2hex(\random_bytes(2));
29+
}
30+
31+
public static function sanitizeRunId(string $runId): string
32+
{
33+
$slug = self::sanitizeSegment($runId);
34+
if ($slug === '') {
35+
throw new \InvalidArgumentException(
36+
'Run id sanitizes to an empty string: ' . \var_export($runId, true),
37+
);
38+
}
39+
if (\str_starts_with($slug, self::RESERVED_PREFIX)) {
40+
$slug = 'r' . $slug;
41+
}
42+
return self::truncate($slug, self::RUN_ID_MAX_LENGTH);
43+
}
44+
45+
public static function sanitizeProcessLabel(string $label): string
46+
{
47+
$slug = self::sanitizeSegment($label);
48+
if ($slug === '') {
49+
$slug = 'process';
50+
}
51+
return self::truncate($slug, self::PROCESS_LABEL_MAX_LENGTH);
52+
}
53+
54+
public static function currentEpochMs(): int
55+
{
56+
return (int) \floor(\microtime(true) * 1000);
57+
}
58+
59+
public static function runDirectory(string $baseDirectory, string $runId): string
60+
{
61+
return $baseDirectory . '/' . self::sanitizeRunId($runId);
62+
}
63+
64+
public static function writerFile(string $runDirectory, string $processLabel): string
65+
{
66+
return $runDirectory
67+
. '/' . self::sanitizeProcessLabel($processLabel)
68+
. '__pid' . (\getmypid() ?: 0)
69+
. '__' . self::currentEpochMs()
70+
. self::LOG_EXTENSION;
71+
}
72+
73+
public static function rotatedFile(string $currentPath, int $rotationCounter): string
74+
{
75+
return $currentPath . '.' . $rotationCounter;
76+
}
77+
78+
public static function mergedDirectory(string $runDirectory): string
79+
{
80+
return $runDirectory . '/' . self::MERGED_DIRECTORY;
81+
}
82+
83+
public static function mergedFile(string $runDirectory): string
84+
{
85+
return self::mergedDirectory($runDirectory) . '/' . self::MERGED_FILENAME;
86+
}
87+
88+
public static function isReservedEntry(string $entry): bool
89+
{
90+
return \str_starts_with($entry, self::RESERVED_PREFIX);
91+
}
92+
93+
private static function sanitizeSegment(string $value): string
94+
{
95+
return \preg_replace(self::SLUG_PATTERN, self::SLUG_REPLACEMENT, $value) ?? '';
96+
}
97+
98+
private static function truncate(string $value, int $max): string
99+
{
100+
return \strlen($value) > $max ? \substr($value, 0, $max) : $value;
101+
}
102+
}

0 commit comments

Comments
 (0)