Skip to content

Commit e1c637d

Browse files
author
Test
committed
test(memory): add direct unit tests for FlowMemoryHandler
Cover the memory feature blind spot (line coverage 23.17 % → 97.59 %). The handler was previously exercised only indirectly through FlowExecutor parallel paths, leaving setup/tick/shouldKill/enrichResults /attachStats unprotected against regressions. - src/Execution/FlowMemoryHandler.php: drop `final` and add two protected seams — `buildSampler()` (factory escape hatch) and `emitWarning()` (degradation-warning escape hatch). Production behavior is unchanged: subclasses are an opt-in test mechanism. - tests/Doubles/FakeMemorySampler.php: configurable MemorySampler test double that records every sample() call and replays a per-call RSS sequence. - tests/Unit/Execution/FlowMemoryHandlerTest.php: 24 tests covering the seven activation paths in setup() (no triggers, job threshold, job reserve, flow budget, --stats, disabled flag, sampler-unavailable warning), tick() PID propagation and null-pid filtering, shouldKill() on every branch (disabled, inactive, peak ≥ fail-above, peak < fail- above), enrichResults() peak/reserve/threshold attachment with the disabled-flag and orphan-result branches, and attachStats() budget- state and stats-block attachment.
1 parent 7b6ab5a commit e1c637d

3 files changed

Lines changed: 603 additions & 5 deletions

File tree

src/Execution/FlowMemoryHandler.php

Lines changed: 24 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@
2323
*
2424
* @SuppressWarnings(PHPMD.CouplingBetweenObjects) Touches every piece of the memory feature.
2525
*/
26-
final class FlowMemoryHandler
26+
class FlowMemoryHandler
2727
{
2828
private OptionsConfiguration $options;
2929

@@ -79,11 +79,10 @@ public function setup(array $jobs): bool
7979
return false;
8080
}
8181

82-
$sampler = (new MemorySamplerFactory())->create();
82+
$sampler = $this->buildSampler();
8383
if (!$sampler->isAvailable() && $thresholdsRequested) {
84-
fwrite(
85-
STDERR,
86-
'⚠ Memory budget disabled: ' . $sampler->getUnavailableReason() . PHP_EOL
84+
$this->emitWarning(
85+
'⚠ Memory budget disabled: ' . $sampler->getUnavailableReason()
8786
);
8887
}
8988

@@ -95,6 +94,26 @@ public function setup(array $jobs): bool
9594
return true;
9695
}
9796

97+
/**
98+
* Seam for tests: production builds a real sampler via the factory; tests
99+
* subclass and inject a fake to exercise the handler without touching
100+
* /proc or shelling out to `ps`.
101+
*/
102+
protected function buildSampler(): MemorySampler
103+
{
104+
return (new MemorySamplerFactory())->create();
105+
}
106+
107+
/**
108+
* Seam for tests: production writes to STDERR; tests override to capture
109+
* the one-time degradation warning emitted when the sampler is
110+
* unavailable but thresholds were declared (REQ-038).
111+
*/
112+
protected function emitWarning(string $message): void
113+
{
114+
fwrite(STDERR, $message . PHP_EOL);
115+
}
116+
98117
public function isActive(): bool
99118
{
100119
return $this->evaluator !== null;
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
<?php
2+
3+
declare(strict_types=1);
4+
5+
namespace Tests\Doubles;
6+
7+
use Wtyd\GitHooks\Execution\Memory\MemorySampler;
8+
9+
/**
10+
* Test double for MemorySampler: returns a configurable per-job RSS map
11+
* on every sample() call and exposes the call history so tests can assert
12+
* which PIDs were sampled at each tick.
13+
*/
14+
final class FakeMemorySampler implements MemorySampler
15+
{
16+
/** @var array<int, array<string, int>> Sequence of RSS maps to return per sample() call. */
17+
private array $rssSequence;
18+
19+
private bool $available;
20+
21+
private string $unavailableReason;
22+
23+
/** @var array<int, array<string, int>> Recorded jobNameToPid arguments per call. */
24+
public array $calls = [];
25+
26+
/**
27+
* @param array<int, array<string, int>> $rssSequence
28+
* Per-call sequence of jobName→RSS maps. When more sample() calls happen
29+
* than entries provided, the last entry is reused.
30+
*/
31+
public function __construct(
32+
array $rssSequence = [[]],
33+
bool $available = true,
34+
string $unavailableReason = ''
35+
) {
36+
$this->rssSequence = $rssSequence === [] ? [[]] : $rssSequence;
37+
$this->available = $available;
38+
$this->unavailableReason = $unavailableReason;
39+
}
40+
41+
public function sample(array $jobNameToPid): array
42+
{
43+
$this->calls[] = $jobNameToPid;
44+
$idx = min(count($this->calls) - 1, count($this->rssSequence) - 1);
45+
return $this->rssSequence[$idx];
46+
}
47+
48+
public function isAvailable(): bool
49+
{
50+
return $this->available;
51+
}
52+
53+
public function getUnavailableReason(): string
54+
{
55+
return $this->unavailableReason;
56+
}
57+
}

0 commit comments

Comments
 (0)