Skip to content

Commit 6609f60

Browse files
committed
Added global random persistent delay simulator
1 parent f68f2a6 commit 6609f60

File tree

6 files changed

+239
-24
lines changed

6 files changed

+239
-24
lines changed

src/Http/Testing/MockRequestBuilder.php

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,65 @@ public function delay(float $seconds): self
9595
return $this;
9696
}
9797

98+
/**
99+
* Set a random delay range for more realistic network simulation.
100+
* Uses aggressive randomization for realistic variation.
101+
*
102+
* @param float $minSeconds Minimum delay in seconds
103+
* @param float $maxSeconds Maximum delay in seconds
104+
* @return self
105+
*/
106+
public function randomDelay(float $minSeconds, float $maxSeconds): self
107+
{
108+
if ($minSeconds > $maxSeconds) {
109+
throw new \InvalidArgumentException('Minimum delay cannot be greater than maximum delay');
110+
}
111+
112+
$randomDelay = $this->generateAggressiveRandomFloat($minSeconds, $maxSeconds);
113+
$this->request->setDelay($randomDelay);
114+
115+
return $this;
116+
}
117+
118+
/**
119+
* Generate aggressive random float with high precision for realistic network simulation.
120+
*
121+
* @param float $min Minimum value
122+
* @param float $max Maximum value
123+
* @return float Random float with microsecond precision
124+
*/
125+
private function generateAggressiveRandomFloat(float $min, float $max): float
126+
{
127+
$precision = 1000000;
128+
$randomInt = random_int(
129+
(int)($min * $precision),
130+
(int)($max * $precision)
131+
);
132+
133+
return $randomInt / $precision;
134+
}
135+
136+
/**
137+
* Create a persistent mock with random delays for each request.
138+
* Each request will have a different random delay within the specified range.
139+
*
140+
* @param float $minSeconds Minimum delay in seconds
141+
* @param float $maxSeconds Maximum delay in seconds
142+
* @return self
143+
*/
144+
public function randomPersistentDelay(float $minSeconds, float $maxSeconds): self
145+
{
146+
if ($minSeconds > $maxSeconds) {
147+
throw new \InvalidArgumentException('Minimum delay cannot be greater than maximum delay');
148+
}
149+
150+
// Store the delay range in the request for later use
151+
$this->request->setRandomDelayRange($minSeconds, $maxSeconds);
152+
$this->persistent();
153+
154+
return $this;
155+
}
156+
98157
public function fail(string $error = 'Mocked request failure'): self
99158
{
100159
$this->request->setError($error);

src/Http/Testing/MockedRequest.php

Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@ class MockedRequest
1212
private array $headerMatchers = [];
1313
private ?string $bodyMatcher = null;
1414
private ?array $jsonMatcher = null;
15+
private ?float $randomDelayMin = null;
16+
private ?float $randomDelayMax = null;
1517

1618
private int $statusCode = 200;
1719
private string $body = '';
@@ -201,8 +203,54 @@ public function getHeaders(): array
201203
return $this->headers;
202204
}
203205

206+
/**
207+
* Set random delay range for persistent mocks.
208+
*/
209+
public function setRandomDelayRange(float $min, float $max): void
210+
{
211+
$this->randomDelayMin = $min;
212+
$this->randomDelayMax = $max;
213+
}
214+
215+
/**
216+
* Get random delay range.
217+
*/
218+
public function getRandomDelayRange(): ?array
219+
{
220+
if ($this->randomDelayMin === null || $this->randomDelayMax === null) {
221+
return null;
222+
}
223+
224+
return [$this->randomDelayMin, $this->randomDelayMax];
225+
}
226+
227+
/**
228+
* Generate a new random delay for this request.
229+
*/
230+
public function generateRandomDelay(): float
231+
{
232+
if ($this->randomDelayMin === null || $this->randomDelayMax === null) {
233+
return $this->delay;
234+
}
235+
236+
$precision = 1000000;
237+
$randomInt = random_int(
238+
(int)($this->randomDelayMin * $precision),
239+
(int)($this->randomDelayMax * $precision)
240+
);
241+
242+
return $randomInt / $precision;
243+
}
244+
245+
/**
246+
* Get delay, generating random delay if range is set.
247+
*/
204248
public function getDelay(): float
205249
{
250+
if ($this->randomDelayMin !== null && $this->randomDelayMax !== null) {
251+
return $this->generateRandomDelay();
252+
}
253+
206254
return $this->timeoutAfter ?? $this->delay;
207255
}
208256

@@ -268,6 +316,8 @@ public function toArray(): array
268316
'body' => $this->body,
269317
'headers' => $this->headers,
270318
'delay' => $this->delay,
319+
'randomDelayMin' => $this->randomDelayMin,
320+
'randomDelayMax' => $this->randomDelayMax,
271321
'error' => $this->error,
272322
'persistent' => $this->persistent,
273323
'timeoutAfter' => $this->timeoutAfter,
@@ -286,6 +336,8 @@ public static function fromArray(array $data): self
286336
$request->body = $data['body'];
287337
$request->headers = $data['headers'] ?? [];
288338
$request->delay = $data['delay'] ?? 0;
339+
$request->randomDelayMin = $data['randomDelayMin'] ?? null;
340+
$request->randomDelayMax = $data['randomDelayMax'] ?? null;
289341
$request->error = $data['error'];
290342
$request->persistent = $data['persistent'] ?? false;
291343
$request->timeoutAfter = $data['timeoutAfter'] ?? null;

src/Http/Testing/Services/ResponseFactory.php

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,17 +9,20 @@
99
use Rcalicdan\FiberAsync\Http\RetryConfig;
1010
use Rcalicdan\FiberAsync\Http\StreamingResponse;
1111
use Rcalicdan\FiberAsync\Http\Testing\MockedRequest;
12+
use Rcalicdan\FiberAsync\Http\Testing\TestingHttpHandler;
1213
use Rcalicdan\FiberAsync\Promise\CancellablePromise;
1314
use Rcalicdan\FiberAsync\Promise\Interfaces\CancellablePromiseInterface;
1415
use Rcalicdan\FiberAsync\Promise\Interfaces\PromiseInterface;
1516

1617
class ResponseFactory
1718
{
1819
private NetworkSimulator $networkSimulator;
20+
private ?TestingHttpHandler $handler = null;
1921

20-
public function __construct(NetworkSimulator $networkSimulator)
22+
public function __construct(NetworkSimulator $networkSimulator, ?TestingHttpHandler $handler = null)
2123
{
2224
$this->networkSimulator = $networkSimulator;
25+
$this->handler = $handler;
2326
}
2427

2528
public function createMockedResponse(MockedRequest $mock): PromiseInterface
@@ -197,7 +200,17 @@ public function createMockedDownload(MockedRequest $mock, string $destination, F
197200
private function executeWithNetworkSimulation(CancellablePromise $promise, MockedRequest $mock, callable $callback): void
198201
{
199202
$networkConditions = $this->networkSimulator->simulate();
200-
$totalDelay = max($mock->getDelay(), $networkConditions['delay']);
203+
204+
// Get mock delay (which might be random for persistent mocks)
205+
$mockDelay = $mock->getDelay();
206+
207+
// Add global random delay if the handler has it enabled
208+
$globalDelay = 0.0;
209+
if ($this->handler !== null) {
210+
$globalDelay = $this->handler->generateGlobalRandomDelay();
211+
}
212+
213+
$totalDelay = max($mockDelay + $globalDelay, $networkConditions['delay']);
201214
/** @var string|null $timerId */
202215
$timerId = null;
203216

src/Http/Testing/TestingHttpHandler.php

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@ class TestingHttpHandler extends HttpHandler
3232

3333
/** @var array<RecordedRequest> */
3434
private array $requestHistory = [];
35+
private ?float $globalRandomDelayMin = null;
36+
private ?float $globalRandomDelayMax = null;
3537

3638
private array $globalSettings = [
3739
'record_requests' => true,
@@ -99,6 +101,39 @@ public function withGlobalFileCookieJar(?string $filename = null, bool $includeS
99101
return $this;
100102
}
101103

104+
/**
105+
* Enable global random delay for all requests.
106+
* This adds realistic network variance to all mocked requests.
107+
*
108+
* @param float $minSeconds Minimum delay in seconds
109+
* @param float $maxSeconds Maximum delay in seconds
110+
* @return self
111+
*/
112+
public function withGlobalRandomDelay(float $minSeconds, float $maxSeconds): self
113+
{
114+
if ($minSeconds > $maxSeconds) {
115+
throw new \InvalidArgumentException('Minimum delay cannot be greater than maximum delay');
116+
}
117+
118+
$this->globalRandomDelayMin = $minSeconds;
119+
$this->globalRandomDelayMax = $maxSeconds;
120+
121+
return $this;
122+
}
123+
124+
/**
125+
* Disable global random delay.
126+
*
127+
* @return self
128+
*/
129+
public function withoutGlobalRandomDelay(): self
130+
{
131+
$this->globalRandomDelayMin = null;
132+
$this->globalRandomDelayMax = null;
133+
134+
return $this;
135+
}
136+
102137
public function enableNetworkSimulation(array $settings = []): self
103138
{
104139
$this->networkSimulator->enable($settings);
@@ -329,10 +364,33 @@ public function getRequestHistory(): array
329364
return $this->requestHistory;
330365
}
331366

367+
368+
/**
369+
* Generate global random delay if enabled.
370+
*
371+
* @return float
372+
*/
373+
public function generateGlobalRandomDelay(): float
374+
{
375+
if ($this->globalRandomDelayMin === null || $this->globalRandomDelayMax === null) {
376+
return 0.0;
377+
}
378+
379+
$precision = 1000000;
380+
$randomInt = random_int(
381+
(int)($this->globalRandomDelayMin * $precision),
382+
(int)($this->globalRandomDelayMax * $precision)
383+
);
384+
385+
return $randomInt / $precision;
386+
}
387+
332388
public function reset(): void
333389
{
334390
$this->mockedRequests = [];
335391
$this->requestHistory = [];
392+
$this->globalRandomDelayMin = null;
393+
$this->globalRandomDelayMax = null;
336394
$this->fileManager->cleanup();
337395
$this->cookieManager->cleanup();
338396
}

test21.php

Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -5,41 +5,49 @@
55

66
require __DIR__ . '/vendor/autoload.php';
77

8-
echo "====== Integration Test for Cookies AND Caching ======\n";
8+
echo "====== Integration Test for Cookies AND Caching (FIXED) ======\n";
99

1010
Task::run(function () {
11-
// 1. Arrange: Enable testing with a managed CookieJar.
12-
$handler = Http::testing()->withGlobalCookieJar();
13-
$handler->reset();
11+
// 1. Arrange: Get testing handler and reset FIRST
12+
$handler = Http::testing();
13+
$handler->reset(); // Reset first
14+
15+
// THEN enable cookie jar (after reset)
16+
$handler->withGlobalCookieJar();
17+
18+
// Set as global instance
19+
Http::setInstance($handler);
20+
21+
echo "DEBUG: Testing handler created, reset, and configured with cookie jar\n";
1422

1523
$sessionId = 'session-for-caching-test';
1624
$domain = 'api.example.com';
1725
$profileUrl = "https://{$domain}/api/profile";
1826
$loginUrl = "https://{$domain}/login";
1927

20-
Http::mock('POST')->url($loginUrl)
28+
// Set up mocks
29+
$handler->mock('POST')->url($loginUrl)
2130
->setCookie('session_id', $sessionId)
2231
->json(['status' => 'logged_in'])
2332
->register();
2433

25-
// Mock the /profile endpoint. It MUST expect the cookie.
26-
// It is NOT persistent. If it's called more than once, the test will fail.
27-
Http::mock('GET')->url($profileUrl)
34+
$handler->mock('GET')->url($profileUrl)
2835
->withHeader('Cookie', "session_id={$sessionId}")
29-
->delay(0.5) // Add a delay to make the cache hit obvious
36+
->delay(0.5)
3037
->json(['user' => 'John Doe', 'timestamp' => microtime(true)])
3138
->register();
3239

33-
echo "--- Step 1: Logging in to establish a session ---\n";
34-
await(Http::post($loginUrl));
40+
echo "\n--- Step 1: Logging in to establish a session ---\n";
41+
$loginResponse = await(Http::post($loginUrl));
42+
43+
// Now we can safely assert cookie exists
3544
$handler->assertCookieExists('session_id');
3645
echo " ✓ SUCCESS: Logged in and session cookie was stored.\n";
3746

3847
// -----------------------------------------------------------------
3948

4049
echo "\n--- Step 2: First profile fetch (expecting CACHE MISS) ---\n";
4150
$start1 = microtime(true);
42-
// This request uses BOTH the cookie jar (implicitly) and caching.
4351
$response1 = await(
4452
Http::request()
4553
->cache(60)
@@ -66,26 +74,22 @@
6674

6775
echo "\n--- Step 4: Verifying Results ---\n";
6876

69-
// Assertion 1: Verify the second call was a cache hit (instant and same data).
77+
// Assertion 1: Verify the second call was a cache hit
7078
if ($elapsed2 < 0.01 && $data1['timestamp'] === $data2['timestamp']) {
7179
echo " ✓ SUCCESS: Second request was an instant cache hit with the correct data.\n";
7280
} else {
7381
echo " ✗ FAILED: Second request was not a cache hit.\n";
7482
}
7583

76-
// Assertion 2: Verify the total number of requests made.
84+
// Assertion 2: Verify the total number of requests made
7785
try {
78-
// We expect 3 total recorded requests:
79-
// 1. POST /login (consumes a mock)
80-
// 2. GET /profile (cache miss, consumes a mock)
81-
// 3. GET /profile (cache hit, does NOT consume a mock)
8286
$handler->assertRequestCount(3);
83-
echo " ✓ SUCCESS: Correct number of requests recorded (login, cache miss, cache hit).\n";
87+
echo " ✓ SUCCESS: Correct number of requests recorded.\n";
8488
} catch (Exception $e) {
8589
echo " ✗ FAILED: " . $e->getMessage() . "\n";
8690
}
8791

88-
// Final check: Let's see the history to be sure.
92+
// Final check: Request history
8993
$history = $handler->getRequestHistory();
9094
$profile_miss_found = false;
9195
$profile_hit_found = false;
@@ -95,11 +99,14 @@
9599
}
96100

97101
if ($profile_miss_found && $profile_hit_found) {
98-
echo " ✓ SUCCESS: Request history correctly shows one cache miss and one cache hit for the profile.\n";
102+
echo " ✓ SUCCESS: Request history shows cache miss and cache hit.\n";
99103
} else {
100104
echo " ✗ FAILED: Request history is incorrect.\n";
105+
echo " DEBUG: History:\n";
106+
foreach($history as $req) {
107+
echo " - {$req->method} {$req->url}\n";
108+
}
101109
}
102-
103110
});
104111

105112
echo "\n====== Cookie and Cache Integration Test Complete ======\n";

0 commit comments

Comments
 (0)