PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.29
ووسلام – همگام سازی ووکامرس و باسلام v1.10.29
1.10.29 1.10.28 1.1.26 1.10.27 1.10.24 1.10.25 1.10.23 1.10.22 1.10.21 1.10.19 1.10.20 1.10.18 1.10.17 1.10.15 1.10.14 1.10.13 1.10.12 1.10.10 1.10.9 1.10.8 1.10.7 1.10.6 1.10.5 1.10.4 1.10.3 All 62 releases
sync-basalam / tests / JobsRunnerTest.php

JobsRunnerTest.php in ووسلام – همگام سازی ووکامرس و باسلام 1.10.29, at tests/JobsRunnerTest.php

472 lines 16.8 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace SyncBasalam\Tests;
4
5 use PHPUnit\Framework\TestCase;
6 use SyncBasalam\JobsRunner;
7
8 class JobsRunnerTest extends TestCase
9 {
10 private const ASYNC_ACTION = 'sync_basalam_run_jobs_async';
11 private const DISPATCH_LOCK = 'sync_basalam_jobs_runner_async_dispatch_lock';
12 private const IDLE_PROBE_LOCK = 'sync_basalam_jobs_runner_idle_probe_lock';
13
14 private $originalRequest;
15 private $originalCookie;
16 private $originalWpdb;
17 private $hadOriginalWpdb;
18 private $originalOptions;
19 private $hadOriginalOptions;
20
21 protected function setUp(): void
22 {
23 $this->originalRequest = $_REQUEST;
24 $this->originalCookie = $_COOKIE;
25 $this->hadOriginalWpdb = array_key_exists('wpdb', $GLOBALS);
26 $this->originalWpdb = $GLOBALS['wpdb'] ?? null;
27 $this->hadOriginalOptions = array_key_exists('sync_basalam_test_options', $GLOBALS);
28 $this->originalOptions = $GLOBALS['sync_basalam_test_options'] ?? null;
29
30 $_REQUEST = [];
31 $_COOKIE = [];
32 $GLOBALS['sync_basalam_test_options'] = [];
33
34 $GLOBALS['sync_basalam_jobs_runner_test_state'] = [
35 'actions' => [],
36 'did_actions' => [],
37 'current_filter' => '',
38 'doing_ajax' => false,
39 'transients' => [],
40 'transient_reads' => [],
41 'transient_writes' => [],
42 'filter_values' => [],
43 'events' => [],
44 'remote_requests' => [],
45 ];
46 }
47
48 protected function tearDown(): void
49 {
50 $_REQUEST = $this->originalRequest;
51 $_COOKIE = $this->originalCookie;
52
53 if ($this->hadOriginalWpdb) {
54 $GLOBALS['wpdb'] = $this->originalWpdb;
55 } else {
56 unset($GLOBALS['wpdb']);
57 }
58
59 unset($GLOBALS['sync_basalam_jobs_runner_test_state']);
60 if ($this->hadOriginalOptions) {
61 $GLOBALS['sync_basalam_test_options'] = $this->originalOptions;
62 } else {
63 unset($GLOBALS['sync_basalam_test_options']);
64 }
65 }
66
67 public function testRegistersDispatcherOnShutdownAndNeverOnInit(): void
68 {
69 $runner = $this->newRunner();
70 $actions = $this->state()['actions'];
71
72 self::assertArrayHasKey('shutdown', $actions);
73 self::assertSame([$runner, 'maybeDispatchAsyncRequest'], $actions['shutdown'][0]['callback']);
74 self::assertSame(2, $actions['shutdown'][0]['priority']);
75 self::assertSame(1, $actions['shutdown'][0]['accepted_args']);
76 self::assertArrayNotHasKey('init', $actions);
77
78 self::assertSame(
79 [$runner, 'maybeDispatchAsyncRequest'],
80 $actions['sync_basalam_job_created'][0]['callback']
81 );
82 self::assertSame(0, $actions['sync_basalam_job_created'][0]['accepted_args']);
83 self::assertSame([$runner, 'handleAsyncRequest'], $actions['wp_ajax_' . self::ASYNC_ACTION][0]['callback']);
84 self::assertSame(
85 [$runner, 'handleAsyncRequest'],
86 $actions['wp_ajax_nopriv_' . self::ASYNC_ACTION][0]['callback']
87 );
88 }
89
90 public function testDispatchLeaseIsWrittenOnlyWhenQueueHasWork(): void
91 {
92 $jobManager = new FakeJobManager([true]);
93 $runner = $this->newRunner($jobManager);
94
95 $_COOKIE = ['wordpress_test_cookie' => 'cookie-value'];
96 $before = time();
97 $runner->maybeDispatchAsyncRequest();
98
99 self::assertSame(
100 ['get_transient', 'get_transient', 'has_pending_jobs', 'get_transient', 'set_transient', 'remote_post'],
101 $this->state()['events']
102 );
103 self::assertSame([120], $jobManager->timeouts);
104 $writes = $this->state()['transient_writes'];
105 self::assertCount(1, $writes);
106 self::assertSame(self::DISPATCH_LOCK, $writes[0]['name']);
107 self::assertSame(25, $writes[0]['expiration']);
108 self::assertGreaterThanOrEqual($before + 25, $writes[0]['value']);
109 self::assertLessThanOrEqual(time() + 25, $writes[0]['value']);
110
111 $requests = $this->state()['remote_requests'];
112 self::assertCount(1, $requests);
113 self::assertSame(
114 'https://example.test/wp-admin/admin-ajax.php?action=' . self::ASYNC_ACTION,
115 $requests[0]['url']
116 );
117 self::assertSame(0.01, $requests[0]['args']['timeout']);
118 self::assertFalse($requests[0]['args']['blocking']);
119 self::assertSame(self::ASYNC_ACTION, $requests[0]['args']['body']['action']);
120 self::assertSame('test-nonce-for-' . self::ASYNC_ACTION, $requests[0]['args']['body']['nonce']);
121 self::assertSame($_COOKIE, $requests[0]['args']['cookies']);
122 }
123
124 public function testShutdownRepairsDatabaseWithoutFinishingFastCgiResponse(): void
125 {
126 $GLOBALS['sync_basalam_jobs_runner_test_state']['current_filter'] = 'shutdown';
127 $GLOBALS['wpdb'] = new FakeWpdb();
128
129 $jobManager = new FakeJobManager([false]);
130 $runner = $this->newRunner($jobManager);
131
132 $runner->maybeDispatchAsyncRequest();
133
134 self::assertSame(
135 [
136 'db_flush',
137 'db_check_connection',
138 'get_transient',
139 'get_transient',
140 'has_pending_jobs',
141 'set_transient',
142 ],
143 $this->state()['events']
144 );
145 self::assertSame([false], $GLOBALS['wpdb']->allowBailValues);
146 }
147
148 public function testEmptyQueueReservesOnlyShortIdleProbeLease(): void
149 {
150 $jobManager = new FakeJobManager([false]);
151 $runner = $this->newRunner($jobManager);
152
153 $before = time();
154 $runner->maybeDispatchAsyncRequest();
155
156 self::assertSame(['get_transient', 'get_transient', 'has_pending_jobs', 'set_transient'], $this->state()['events']);
157 self::assertSame([120], $jobManager->timeouts);
158 $writes = $this->state()['transient_writes'];
159 self::assertCount(1, $writes);
160 self::assertSame(self::IDLE_PROBE_LOCK, $writes[0]['name']);
161 self::assertSame(5, $writes[0]['expiration']);
162 self::assertGreaterThanOrEqual($before + 5, $writes[0]['value']);
163 self::assertLessThanOrEqual(time() + 5, $writes[0]['value']);
164 self::assertSame([], $this->state()['remote_requests']);
165 }
166
167 public function testNewJobBypassesIdleProbeLeaseAndDispatchesImmediately(): void
168 {
169 $jobManager = new FakeJobManager([false, true]);
170 $runner = $this->newRunner($jobManager);
171
172 $runner->maybeDispatchAsyncRequest();
173 self::assertCount(1, $this->state()['transient_writes']);
174
175 $GLOBALS['sync_basalam_jobs_runner_test_state']['current_filter'] = 'sync_basalam_job_created';
176 $runner->maybeDispatchAsyncRequest();
177
178 self::assertSame([120, 120], $jobManager->timeouts);
179 self::assertSame(self::DISPATCH_LOCK, $this->state()['transient_writes'][1]['name']);
180 self::assertCount(1, $this->state()['remote_requests']);
181 }
182
183 public function testExistingLockSkipsQueueCheckAndDispatch(): void
184 {
185 $GLOBALS['sync_basalam_jobs_runner_test_state']['transients'][self::DISPATCH_LOCK] = time() + 25;
186 $jobManager = new FakeJobManager([true]);
187 $runner = $this->newRunner($jobManager);
188
189 $runner->maybeDispatchAsyncRequest();
190
191 self::assertSame(['get_transient', 'get_transient'], $this->state()['events']);
192 self::assertSame([], $jobManager->timeouts);
193 self::assertSame([], $this->state()['transient_writes']);
194 self::assertSame([], $this->state()['remote_requests']);
195 }
196
197 /** @dataProvider inactiveLeaseCases */
198 public function testExpiredOrOrphanedLeaseResumesDispatch(
199 string $name,
200 string $format,
201 int $leaseOffset,
202 ?int $timeoutOffset
203 ): void {
204 $this->setLease($name, $format, $leaseOffset, $timeoutOffset);
205 $jobManager = new FakeJobManager([true]);
206 $runner = $this->newRunner($jobManager);
207
208 $runner->maybeDispatchAsyncRequest();
209
210 self::assertSame([120], $jobManager->timeouts);
211 self::assertCount(1, $this->state()['remote_requests']);
212 self::assertSame(self::DISPATCH_LOCK, $this->state()['transient_writes'][0]['name']);
213 self::assertGreaterThan(time(), $this->state()['transient_writes'][0]['value']);
214 }
215
216 public function inactiveLeaseCases(): array
217 {
218 $cases = [];
219 foreach ([self::DISPATCH_LOCK, self::IDLE_PROBE_LOCK] as $name) {
220 $cases[$name . ': legacy without timeout'] = [$name, 'legacy', 0, null];
221 $cases[$name . ': legacy string without timeout'] = [$name, 'legacy_string', 0, null];
222 $cases[$name . ': legacy with expired timeout'] = [$name, 'legacy', 0, -1];
223 $cases[$name . ': expired deadline without timeout'] = [$name, 'deadline', -1, null];
224 $cases[$name . ': expired stored deadline without timeout'] = [$name, 'deadline_string', -1, null];
225 $cases[$name . ': expired deadline with future timeout'] = [$name, 'deadline', -1, 999];
226 $cases[$name . ': deadline expires at current second'] = [$name, 'deadline', 0, null];
227 }
228 return $cases;
229 }
230
231 /** @dataProvider activeLeaseCases */
232 public function testActiveLeasePreservesDispatchThrottle(
233 string $name,
234 string $format,
235 ?int $timeoutOffset,
236 bool $externalCache
237 ): void {
238 $this->setLease($name, $format, 25, $timeoutOffset);
239 $GLOBALS['sync_basalam_jobs_runner_test_state']['external_object_cache'] = $externalCache;
240 $jobManager = new FakeJobManager([true]);
241 $runner = $this->newRunner($jobManager);
242
243 $runner->maybeDispatchAsyncRequest();
244
245 self::assertSame([], $jobManager->timeouts);
246 self::assertSame([], $this->state()['transient_writes']);
247 self::assertSame([], $this->state()['remote_requests']);
248 }
249
250 public function activeLeaseCases(): array
251 {
252 $cases = [];
253 foreach ([self::DISPATCH_LOCK, self::IDLE_PROBE_LOCK] as $name) {
254 $cases[$name . ': legacy with valid timeout'] = [$name, 'legacy', 25, false];
255 $cases[$name . ': legacy string with valid timeout'] = [$name, 'legacy_string', 25, false];
256 $cases[$name . ': deadline without timeout'] = [$name, 'deadline', null, false];
257 $cases[$name . ': stored deadline without timeout'] = [$name, 'deadline_string', null, false];
258 $cases[$name . ': cached legacy without database timeout'] = [$name, 'legacy', null, true];
259 }
260 return $cases;
261 }
262
263 public function testLosingTimeoutDoesNotReleaseAnActiveDispatchLease(): void
264 {
265 $jobManager = new FakeJobManager([true, true]);
266 $runner = $this->newRunner($jobManager);
267 $runner->maybeDispatchAsyncRequest();
268 unset($GLOBALS['sync_basalam_test_options']['_transient_timeout_' . self::DISPATCH_LOCK]);
269
270 $runner->maybeDispatchAsyncRequest();
271
272 self::assertSame([120], $jobManager->timeouts);
273 self::assertCount(1, $this->state()['remote_requests']);
274 }
275
276 public function testDispatchRechecksLeaseAfterProbingQueue(): void
277 {
278 $jobManager = new FakeJobManager([true]);
279 $jobManager->onProbe = static function (): void {
280 $GLOBALS['sync_basalam_jobs_runner_test_state']['transients'][self::DISPATCH_LOCK] = time() + 25;
281 };
282 $runner = $this->newRunner($jobManager);
283
284 $runner->maybeDispatchAsyncRequest();
285
286 self::assertSame([120], $jobManager->timeouts);
287 self::assertSame([], $this->state()['remote_requests']);
288 self::assertSame([], $this->state()['transient_writes']);
289 }
290
291 public function testSelfAsyncRequestReturnsBeforeQueueAndLockChecks(): void
292 {
293 $GLOBALS['sync_basalam_jobs_runner_test_state']['doing_ajax'] = true;
294 $_REQUEST['action'] = self::ASYNC_ACTION;
295
296 $jobManager = new FakeJobManager([true]);
297 $httpBlockService = new FakeHttpBlockService(false);
298 $runner = $this->newRunner($jobManager, $httpBlockService);
299
300 $runner->maybeDispatchAsyncRequest();
301
302 self::assertSame(0, $httpBlockService->calls);
303 $this->assertNoQueueLockOrDispatchActivity($jobManager);
304 }
305
306 public function testHttpBlockReturnsBeforeQueueAndLockChecks(): void
307 {
308 $jobManager = new FakeJobManager([true]);
309 $httpBlockService = new FakeHttpBlockService(true);
310 $runner = $this->newRunner($jobManager, $httpBlockService);
311
312 $runner->maybeDispatchAsyncRequest();
313
314 self::assertSame(1, $httpBlockService->calls);
315 $this->assertNoQueueLockOrDispatchActivity($jobManager);
316 }
317
318 public function testAsyncBatchExitsImmediatelyWhenAnotherRunnerOwnsTheGlobalLock(): void
319 {
320 $jobManager = new FakeJobManager([true]);
321 $jobExecutor = new FakeJobExecutor(false);
322 $runner = $this->newRunner($jobManager, null, $jobExecutor);
323
324 self::assertSame(0, $this->invokeRunAsyncBatch($runner));
325 self::assertSame(1, $jobExecutor->acquireCalls);
326 self::assertSame(0, $jobExecutor->releaseCalls);
327 self::assertSame([], $jobManager->timeouts);
328 }
329
330 public function testAsyncBatchReleasesTheGlobalLockWhenTheQueueIsEmpty(): void
331 {
332 $jobManager = new FakeJobManager([false]);
333 $jobExecutor = new FakeJobExecutor(true);
334 $runner = $this->newRunner($jobManager, null, $jobExecutor);
335
336 self::assertSame(0, $this->invokeRunAsyncBatch($runner));
337 self::assertSame(1, $jobExecutor->acquireCalls);
338 self::assertSame(1, $jobExecutor->releaseCalls);
339 self::assertSame([120], $jobManager->timeouts);
340 }
341
342 private function newRunner(
343 ?FakeJobManager $jobManager = null,
344 ?FakeHttpBlockService $httpBlockService = null,
345 $jobExecutor = null
346 ): JobsRunner {
347 return new JobsRunner(
348 $jobManager ?? new FakeJobManager(),
349 $jobExecutor ?? new FakeJobExecutor(),
350 new \stdClass(),
351 $httpBlockService ?? new FakeHttpBlockService(false)
352 );
353 }
354
355 private function setLease(string $name, string $format, int $leaseOffset, ?int $timeoutOffset): void
356 {
357 $value = in_array($format, ['deadline', 'deadline_string'], true) ? time() + $leaseOffset : 1;
358 if (in_array($format, ['legacy_string', 'deadline_string'], true)) $value = (string) $value;
359 $GLOBALS['sync_basalam_jobs_runner_test_state']['transients'][$name] = $value;
360 if ($timeoutOffset !== null) {
361 $GLOBALS['sync_basalam_test_options']['_transient_timeout_' . $name] = (string) (time() + $timeoutOffset);
362 }
363 }
364
365 private function invokeRunAsyncBatch(JobsRunner $runner): int
366 {
367 $method = new \ReflectionMethod($runner, 'runAsyncBatch');
368 $method->setAccessible(true);
369
370 return $method->invoke($runner);
371 }
372
373 private function assertNoQueueLockOrDispatchActivity(FakeJobManager $jobManager): void
374 {
375 self::assertSame([], $jobManager->timeouts);
376 self::assertSame([], $this->state()['transient_reads']);
377 self::assertSame([], $this->state()['transient_writes']);
378 self::assertSame([], $this->state()['remote_requests']);
379 }
380
381 private function state(): array
382 {
383 return $GLOBALS['sync_basalam_jobs_runner_test_state'];
384 }
385 }
386
387 class FakeJobManager
388 {
389 public $timeouts = [];
390 public $onProbe;
391
392 private $pendingResults;
393
394 public function __construct(array $pendingResults = [])
395 {
396 $this->pendingResults = $pendingResults;
397 }
398
399 public function hasPendingOrStaleProcessingJobs(int $timeout): bool
400 {
401 $GLOBALS['sync_basalam_jobs_runner_test_state']['events'][] = 'has_pending_jobs';
402 $this->timeouts[] = $timeout;
403 if ($this->onProbe) ($this->onProbe)();
404
405 return (bool) array_shift($this->pendingResults);
406 }
407 }
408
409 class FakeWpdb
410 {
411 public $allowBailValues = [];
412
413 public function flush(): void
414 {
415 $GLOBALS['sync_basalam_jobs_runner_test_state']['events'][] = 'db_flush';
416 }
417
418 public function check_connection($allowBail = true): bool
419 {
420 $GLOBALS['sync_basalam_jobs_runner_test_state']['events'][] = 'db_check_connection';
421 $this->allowBailValues[] = $allowBail;
422
423 return true;
424 }
425 }
426
427 class FakeHttpBlockService
428 {
429 public $calls = 0;
430
431 private $blocked;
432
433 public function __construct(bool $blocked)
434 {
435 $this->blocked = $blocked;
436 }
437
438 public function SyncBasalamHttpBlock()
439 {
440 $this->calls++;
441
442 return $this->blocked;
443 }
444 }
445
446 class FakeJobExecutor
447 {
448 public $acquireCalls = 0;
449 public $releaseCalls = 0;
450
451 private $canAcquire;
452
453 public function __construct(bool $canAcquire = true)
454 {
455 $this->canAcquire = $canAcquire;
456 }
457
458 public function acquireGlobalJobsLock(int $timeout = 0): bool
459 {
460 $this->acquireCalls++;
461
462 return $this->canAcquire;
463 }
464
465 public function releaseGlobalJobsLock(): bool
466 {
467 $this->releaseCalls++;
468
469 return true;
470 }
471 }
472