Skip to content

Commit ec286b1

Browse files
Extract shared child-process result-envelope handling into one helper
1 parent 396fe00 commit ec286b1

3 files changed

Lines changed: 114 additions & 73 deletions

File tree

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
<?php declare(strict_types=1);
2+
/*
3+
* This file is part of PHPUnit.
4+
*
5+
* (c) Sebastian Bergmann <sebastian@phpunit.de>
6+
*
7+
* For the full copyright and license information, please view the LICENSE
8+
* file that was distributed with this source code.
9+
*/
10+
namespace PHPUnit\Framework\TestRunner;
11+
12+
use function hash_equals;
13+
use function strlen;
14+
use function substr;
15+
use PHPUnit\Runner\CodeCoverage;
16+
use SebastianBergmann\CodeCoverage\CodeCoverage as CodeCoverageData;
17+
18+
/**
19+
* The mechanics shared by the two consumers of the serialized result envelope
20+
* that a process-isolated child or a parallel worker writes back to the parent:
21+
* the ChildProcessResultProcessor and the parallel ResultAggregator.
22+
*
23+
* Only the parts that are identical between the two live here. The shape each
24+
* one requires of the decoded envelope, and the way each reports a malformed
25+
* result, differ and stay with each consumer.
26+
*
27+
* @no-named-arguments Parameter names are not covered by the backward compatibility promise for PHPUnit
28+
*
29+
* @internal This class is not covered by the backward compatibility promise for PHPUnit
30+
*/
31+
final class ChildProcessResultEnvelope
32+
{
33+
/**
34+
* Verify the nonce that prefixes the serialized result and strip it,
35+
* returning the bare payload.
36+
*
37+
* A null nonce or an empty payload is passed through unverified — the caller
38+
* then fails on the empty or unexpected payload when it tries to decode it.
39+
* Null is returned only when a nonce was expected but the prefix does not
40+
* match it, which means the result was written by an unexpected process or
41+
* was tampered with.
42+
*
43+
* @param ?non-empty-string $nonce
44+
*/
45+
public static function verifyAndStripNonce(string $serialized, ?string $nonce): ?string
46+
{
47+
if ($nonce === null || $serialized === '') {
48+
return $serialized;
49+
}
50+
51+
$length = strlen($nonce);
52+
53+
if (strlen($serialized) < $length ||
54+
!hash_equals($nonce, substr($serialized, 0, $length))) {
55+
return null;
56+
}
57+
58+
return substr($serialized, $length);
59+
}
60+
61+
/**
62+
* Merge the code coverage carried by a decoded result envelope into the
63+
* parent's coverage, when coverage collection is active and the envelope
64+
* actually carries it.
65+
*/
66+
public static function mergeCodeCoverage(object $result, CodeCoverage $codeCoverage): void
67+
{
68+
if (!$codeCoverage->isActive()) {
69+
return;
70+
}
71+
72+
// @codeCoverageIgnoreStart
73+
if (!isset($result->codeCoverage) || !$result->codeCoverage instanceof CodeCoverageData) {
74+
return;
75+
}
76+
77+
CodeCoverage::instance()->codeCoverage()->merge($result->codeCoverage);
78+
// @codeCoverageIgnoreEnd
79+
}
80+
}

src/Framework/TestRunner/ChildProcessResultProcessor.php

Lines changed: 19 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,8 @@
1010
namespace PHPUnit\Framework\TestRunner;
1111

1212
use function assert;
13-
use function hash_equals;
1413
use function is_int;
1514
use function property_exists;
16-
use function strlen;
17-
use function substr;
1815
use function trim;
1916
use function unserialize;
2017
use PHPUnit\Event\Code\TestMethodBuilder;
@@ -81,38 +78,33 @@ public function process(Test $test, string $serializedProcessResult, string $std
8178
return;
8279
}
8380

84-
if ($processResultNonce !== null && $serializedProcessResult !== '') {
85-
$nonceLength = strlen($processResultNonce);
81+
$verifiedProcessResult = ChildProcessResultEnvelope::verifyAndStripNonce($serializedProcessResult, $processResultNonce);
8682

87-
if (strlen($serializedProcessResult) < $nonceLength ||
88-
!hash_equals($processResultNonce, substr($serializedProcessResult, 0, $nonceLength))) {
89-
$this->emitter->childProcessErrored();
90-
91-
$exception = new AssertionFailedError(
92-
'Test was run in child process and the result file was tampered with or written by an unexpected process',
93-
);
83+
if ($verifiedProcessResult === null) {
84+
$this->emitter->childProcessErrored();
9485

95-
assert($test instanceof TestCase);
86+
$exception = new AssertionFailedError(
87+
'Test was run in child process and the result file was tampered with or written by an unexpected process',
88+
);
9689

97-
$test->setStatus(TestStatus::error($exception->getMessage()));
90+
assert($test instanceof TestCase);
9891

99-
$this->emitter->testErrored(
100-
TestMethodBuilder::fromTestCase($test),
101-
ThrowableBuilder::from($exception),
102-
);
92+
$test->setStatus(TestStatus::error($exception->getMessage()));
10393

104-
$this->emitter->testFinished(
105-
TestMethodBuilder::fromTestCase($test),
106-
0,
107-
);
94+
$this->emitter->testErrored(
95+
TestMethodBuilder::fromTestCase($test),
96+
ThrowableBuilder::from($exception),
97+
);
10898

109-
return;
110-
}
99+
$this->emitter->testFinished(
100+
TestMethodBuilder::fromTestCase($test),
101+
0,
102+
);
111103

112-
$serializedProcessResult = substr($serializedProcessResult, $nonceLength);
104+
return;
113105
}
114106

115-
$childResult = @unserialize($serializedProcessResult);
107+
$childResult = @unserialize($verifiedProcessResult);
116108

117109
if (!$childResult instanceof stdClass ||
118110
!property_exists($childResult, 'events') ||
@@ -155,18 +147,6 @@ public function process(Test $test, string $serializedProcessResult, string $std
155147
$test->setStatus($childResult->status);
156148
$test->addToAssertionCount($childResult->numAssertions);
157149

158-
if (!$this->codeCoverage->isActive()) {
159-
return;
160-
}
161-
162-
// @codeCoverageIgnoreStart
163-
if (!isset($childResult->codeCoverage) || !$childResult->codeCoverage instanceof \SebastianBergmann\CodeCoverage\CodeCoverage) {
164-
return;
165-
}
166-
167-
CodeCoverage::instance()->codeCoverage()->merge(
168-
$childResult->codeCoverage,
169-
);
170-
// @codeCoverageIgnoreEnd
150+
ChildProcessResultEnvelope::mergeCodeCoverage($childResult, $this->codeCoverage);
171151
}
172152
}

src/Runner/Parallel/ResultAggregator.php

Lines changed: 15 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,13 @@
99
*/
1010
namespace PHPUnit\Runner\Parallel;
1111

12-
use function hash_equals;
1312
use function property_exists;
1413
use function sprintf;
15-
use function strlen;
16-
use function substr;
1714
use function unserialize;
1815
use PHPUnit\Event\Emitter;
1916
use PHPUnit\Event\EventCollection;
2017
use PHPUnit\Event\Facade;
18+
use PHPUnit\Framework\TestRunner\ChildProcessResultEnvelope;
2119
use PHPUnit\Runner\CodeCoverage;
2220
use PHPUnit\TestRunner\TestResult\PassedTests;
2321
use stdClass;
@@ -164,26 +162,21 @@ private function forward(CompletedWorkUnit $completed): void
164162
return;
165163
}
166164

167-
$serializedResult = $completed->serializedResult();
168-
$nonce = $completed->nonce();
169-
170-
if ($nonce !== null && $serializedResult !== '') {
171-
$nonceLength = strlen($nonce);
172-
173-
if (strlen($serializedResult) < $nonceLength ||
174-
!hash_equals($nonce, substr($serializedResult, 0, $nonceLength))) {
175-
$this->emitter->childProcessErrored();
176-
$this->emitter->testRunnerTriggeredPhpunitWarning(
177-
sprintf(
178-
'The result of the worker process running %s was tampered with or written by an unexpected process',
179-
$completed->unit()->name(),
180-
),
181-
);
165+
$serializedResult = ChildProcessResultEnvelope::verifyAndStripNonce(
166+
$completed->serializedResult(),
167+
$completed->nonce(),
168+
);
182169

183-
return;
184-
}
170+
if ($serializedResult === null) {
171+
$this->emitter->childProcessErrored();
172+
$this->emitter->testRunnerTriggeredPhpunitWarning(
173+
sprintf(
174+
'The result of the worker process running %s was tampered with or written by an unexpected process',
175+
$completed->unit()->name(),
176+
),
177+
);
185178

186-
$serializedResult = substr($serializedResult, $nonceLength);
179+
return;
187180
}
188181

189182
$childResult = @unserialize($serializedResult);
@@ -207,18 +200,6 @@ private function forward(CompletedWorkUnit $completed): void
207200
$this->eventFacade->forward($childResult->events);
208201
$this->passedTests->import($childResult->passedTests);
209202

210-
if (!$this->codeCoverage->isActive()) {
211-
return;
212-
}
213-
214-
// @codeCoverageIgnoreStart
215-
if (!isset($childResult->codeCoverage) || !$childResult->codeCoverage instanceof \SebastianBergmann\CodeCoverage\CodeCoverage) {
216-
return;
217-
}
218-
219-
CodeCoverage::instance()->codeCoverage()->merge(
220-
$childResult->codeCoverage,
221-
);
222-
// @codeCoverageIgnoreEnd
203+
ChildProcessResultEnvelope::mergeCodeCoverage($childResult, $this->codeCoverage);
223204
}
224205
}

0 commit comments

Comments
 (0)