PluginProbe
ووسلام – همگام سازی ووکامرس و باسلام / 1.10.22
ووسلام – همگام سازی ووکامرس و باسلام v1.10.22
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 1.10.2 1.10.1 1.10.0 1.9.2 1.9.1 1.9.0 All 56 releases
← All changes | tests/JobsRunnerTest.php +135 -28 1.10.14 → 1.10.22 View file →
@@ -8,16 +8,21 @@
8 8 class JobsRunnerTest extends TestCase
9 9 {
10 10 private const ASYNC_ACTION = 'sync_basalam_run_jobs_async';
11 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';
12 13
13 14 private $originalRequest;
14 15 private $originalCookie;
16 + private $originalWpdb;
17 + private $hadOriginalWpdb;
15 18
16 19 protected function setUp(): void
17 20 {
18 21 $this->originalRequest = $_REQUEST;
19 22 $this->originalCookie = $_COOKIE;
23 + $this->hadOriginalWpdb = array_key_exists('wpdb', $GLOBALS);
24 + $this->originalWpdb = $GLOBALS['wpdb'] ?? null;
20 25
21 26 $_REQUEST = [];
22 27 $_COOKIE = [];
23 28
@@ -23,8 +28,9 @@
23 28
24 29 $GLOBALS['sync_basalam_jobs_runner_test_state'] = [
25 30 'actions' => [],
26 31 'did_actions' => [],
32 + 'current_filter' => '',
27 33 'doing_ajax' => false,
28 34 'transients' => [],
29 35 'transient_reads' => [],
30 36 'transient_writes' => [],
@@ -38,21 +44,27 @@
38 44 {
39 45 $_REQUEST = $this->originalRequest;
40 46 $_COOKIE = $this->originalCookie;
41 47
48 + if ($this->hadOriginalWpdb) {
49 + $GLOBALS['wpdb'] = $this->originalWpdb;
50 + } else {
51 + unset($GLOBALS['wpdb']);
52 + }
53 +
42 54 unset($GLOBALS['sync_basalam_jobs_runner_test_state']);
43 55 }
44 56
45 - public function testRegistersDispatcherOnInitAndNeverOnShutdown(): void
57 + public function testRegistersDispatcherOnShutdownAndNeverOnInit(): void
46 58 {
47 59 $runner = $this->newRunner();
48 60 $actions = $this->state()['actions'];
49 61
50 - self::assertArrayHasKey('init', $actions);
51 - self::assertSame([$runner, 'maybeDispatchAsyncRequest'], $actions['init'][0]['callback']);
52 - self::assertSame(10, $actions['init'][0]['priority']);
53 - self::assertSame(0, $actions['init'][0]['accepted_args']);
54 - self::assertArrayNotHasKey('shutdown', $actions);
62 + self::assertArrayHasKey('shutdown', $actions);
63 + self::assertSame([$runner, 'maybeDispatchAsyncRequest'], $actions['shutdown'][0]['callback']);
64 + self::assertSame(2, $actions['shutdown'][0]['priority']);
65 + self::assertSame(1, $actions['shutdown'][0]['accepted_args']);
66 + self::assertArrayNotHasKey('init', $actions);
55 67
56 68 self::assertSame(
57 69 [$runner, 'maybeDispatchAsyncRequest'],
58 70 $actions['sync_basalam_job_created'][0]['callback']
@@ -64,9 +76,9 @@
64 76 $actions['wp_ajax_nopriv_' . self::ASYNC_ACTION][0]['callback']
65 77 );
66 78 }
67 79
68 - public function testEligibleQueueIsConfirmedBeforeWritingLockAndDispatching(): void
80 + public function testDispatchLeaseIsWrittenOnlyWhenQueueHasWork(): void
69 81 {
70 82 $jobManager = new FakeJobManager([true]);
71 83 $runner = $this->newRunner($jobManager);
72 84
@@ -73,9 +85,9 @@
73 85 $_COOKIE = ['wordpress_test_cookie' => 'cookie-value'];
74 86 $runner->maybeDispatchAsyncRequest();
75 87
76 88 self::assertSame(
77 - ['get_transient', 'has_pending_jobs', 'set_transient', 'remote_post'],
89 + ['get_transient', 'get_transient', 'has_pending_jobs', 'get_transient', 'set_transient', 'remote_post'],
78 90 $this->state()['events']
79 91 );
80 92 self::assertSame([120], $jobManager->timeouts);
81 93 self::assertSame(
@@ -81,9 +93,9 @@
81 93 self::assertSame(
82 94 [[
83 95 'name' => self::DISPATCH_LOCK,
84 96 'value' => 1,
85 - 'expiration' => 1,
97 + 'expiration' => 25,
86 98 ]],
87 99 $this->state()['transient_writes']
88 100 );
89 101
@@ -99,35 +111,66 @@
99 111 self::assertSame('test-nonce-for-' . self::ASYNC_ACTION, $requests[0]['args']['body']['nonce']);
100 112 self::assertSame($_COOKIE, $requests[0]['args']['cookies']);
101 113 }
102 114
103 - public function testEmptyQueueDoesNotWriteLockOrDispatch(): void
115 + public function testShutdownRepairsDatabaseBeforeUsingQueueApis(): void
104 116 {
117 + $GLOBALS['sync_basalam_jobs_runner_test_state']['current_filter'] = 'shutdown';
118 + $GLOBALS['wpdb'] = new FakeWpdb();
119 +
105 120 $jobManager = new FakeJobManager([false]);
106 121 $runner = $this->newRunner($jobManager);
107 122
108 123 $runner->maybeDispatchAsyncRequest();
109 124
110 - self::assertSame(['get_transient', 'has_pending_jobs'], $this->state()['events']);
125 + self::assertSame(
126 + [
127 + 'fastcgi_finish_request',
128 + 'db_flush',
129 + 'db_check_connection',
130 + 'get_transient',
131 + 'get_transient',
132 + 'has_pending_jobs',
133 + 'set_transient',
134 + ],
135 + $this->state()['events']
136 + );
137 + self::assertSame([false], $GLOBALS['wpdb']->allowBailValues);
138 + }
139 +
140 + public function testEmptyQueueReservesOnlyShortIdleProbeLease(): void
141 + {
142 + $jobManager = new FakeJobManager([false]);
143 + $runner = $this->newRunner($jobManager);
144 +
145 + $runner->maybeDispatchAsyncRequest();
146 +
147 + self::assertSame(['get_transient', 'get_transient', 'has_pending_jobs', 'set_transient'], $this->state()['events']);
111 148 self::assertSame([120], $jobManager->timeouts);
112 - self::assertSame([], $this->state()['transient_writes']);
149 + self::assertSame(
150 + [[
151 + 'name' => self::IDLE_PROBE_LOCK,
152 + 'value' => 1,
153 + 'expiration' => 5,
154 + ]],
155 + $this->state()['transient_writes']
156 + );
113 157 self::assertSame([], $this->state()['remote_requests']);
114 158 }
115 159
116 - public function testJobCreatedAfterAnEmptyInitCanStillDispatch(): void
160 + public function testNewJobBypassesIdleProbeLeaseAndDispatchesImmediately(): void
117 161 {
118 162 $jobManager = new FakeJobManager([false, true]);
119 163 $runner = $this->newRunner($jobManager);
120 164
121 - // The init probe sees no work and must not reserve the dispatch lock.
122 165 $runner->maybeDispatchAsyncRequest();
123 - self::assertSame([], $this->state()['transient_writes']);
166 + self::assertCount(1, $this->state()['transient_writes']);
124 167
125 - // A job created later in the same request must still wake the runner.
168 + $GLOBALS['sync_basalam_jobs_runner_test_state']['current_filter'] = 'sync_basalam_job_created';
126 169 $runner->maybeDispatchAsyncRequest();
127 170
128 171 self::assertSame([120, 120], $jobManager->timeouts);
129 - self::assertCount(1, $this->state()['transient_writes']);
172 + self::assertSame(self::DISPATCH_LOCK, $this->state()['transient_writes'][1]['name']);
130 173 self::assertCount(1, $this->state()['remote_requests']);
131 174 }
132 175
133 176 public function testExistingLockSkipsQueueCheckAndDispatch(): void
@@ -137,9 +180,9 @@
137 180 $runner = $this->newRunner($jobManager);
138 181
139 182 $runner->maybeDispatchAsyncRequest();
140 183
141 - self::assertSame(['get_transient'], $this->state()['events']);
184 + self::assertSame(['get_transient', 'get_transient'], $this->state()['events']);
142 185 self::assertSame([], $jobManager->timeouts);
143 186 self::assertSame([], $this->state()['transient_writes']);
144 187 self::assertSame([], $this->state()['remote_requests']);
145 188 }
@@ -170,34 +213,53 @@
170 213 self::assertSame(1, $httpBlockService->calls);
171 214 $this->assertNoQueueLockOrDispatchActivity($jobManager);
172 215 }
173 216
174 - public function testShutdownGuardPreventsEveryDispatchSideEffect(): void
217 + public function testAsyncBatchExitsImmediatelyWhenAnotherRunnerOwnsTheGlobalLock(): void
175 218 {
176 - $GLOBALS['sync_basalam_jobs_runner_test_state']['did_actions']['shutdown'] = 1;
219 + $jobManager = new FakeJobManager([true]);
220 + $jobExecutor = new FakeJobExecutor(false);
221 + $runner = $this->newRunner($jobManager, null, $jobExecutor);
177 222
178 - $jobManager = new FakeJobManager([true]);
179 - $httpBlockService = new FakeHttpBlockService(false);
180 - $runner = $this->newRunner($jobManager, $httpBlockService);
223 + self::assertSame(0, $this->invokeRunAsyncBatch($runner));
224 + self::assertSame(1, $jobExecutor->acquireCalls);
225 + self::assertSame(0, $jobExecutor->releaseCalls);
226 + self::assertSame([], $jobManager->timeouts);
227 + }
181 228
182 - $runner->maybeDispatchAsyncRequest();
229 + public function testAsyncBatchReleasesTheGlobalLockWhenTheQueueIsEmpty(): void
230 + {
231 + $jobManager = new FakeJobManager([false]);
232 + $jobExecutor = new FakeJobExecutor(true);
233 + $runner = $this->newRunner($jobManager, null, $jobExecutor);
183 234
184 - self::assertSame(0, $httpBlockService->calls);
185 - $this->assertNoQueueLockOrDispatchActivity($jobManager);
235 + self::assertSame(0, $this->invokeRunAsyncBatch($runner));
236 + self::assertSame(1, $jobExecutor->acquireCalls);
237 + self::assertSame(1, $jobExecutor->releaseCalls);
238 + self::assertSame([120], $jobManager->timeouts);
186 239 }
187 240
188 241 private function newRunner(
189 242 ?FakeJobManager $jobManager = null,
190 - ?FakeHttpBlockService $httpBlockService = null
243 + ?FakeHttpBlockService $httpBlockService = null,
244 + $jobExecutor = null
191 245 ): JobsRunner {
192 246 return new JobsRunner(
193 247 $jobManager ?? new FakeJobManager(),
248 + $jobExecutor ?? new FakeJobExecutor(),
194 249 new \stdClass(),
195 - new \stdClass(),
196 250 $httpBlockService ?? new FakeHttpBlockService(false)
197 251 );
198 252 }
199 253
254 + private function invokeRunAsyncBatch(JobsRunner $runner): int
255 + {
256 + $method = new \ReflectionMethod($runner, 'runAsyncBatch');
257 + $method->setAccessible(true);
258 +
259 + return $method->invoke($runner);
260 + }
261 +
200 262 private function assertNoQueueLockOrDispatchActivity(FakeJobManager $jobManager): void
201 263 {
202 264 self::assertSame([], $jobManager->timeouts);
203 265 self::assertSame([], $this->state()['transient_reads']);
@@ -230,8 +292,26 @@
230 292 return (bool) array_shift($this->pendingResults);
231 293 }
232 294 }
233 295
296 +class FakeWpdb
297 +{
298 + public $allowBailValues = [];
299 +
300 + public function flush(): void
301 + {
302 + $GLOBALS['sync_basalam_jobs_runner_test_state']['events'][] = 'db_flush';
303 + }
304 +
305 + public function check_connection($allowBail = true): bool
306 + {
307 + $GLOBALS['sync_basalam_jobs_runner_test_state']['events'][] = 'db_check_connection';
308 + $this->allowBailValues[] = $allowBail;
309 +
310 + return true;
311 + }
312 +}
313 +
234 314 class FakeHttpBlockService
235 315 {
236 316 public $calls = 0;
237 317
@@ -246,6 +326,33 @@
246 326 {
247 327 $this->calls++;
248 328
249 329 return $this->blocked;
330 + }
331 +}
332 +
333 +class FakeJobExecutor
334 +{
335 + public $acquireCalls = 0;
336 + public $releaseCalls = 0;
337 +
338 + private $canAcquire;
339 +
340 + public function __construct(bool $canAcquire = true)
341 + {
342 + $this->canAcquire = $canAcquire;
343 + }
344 +
345 + public function acquireGlobalJobsLock(int $timeout = 0): bool
346 + {
347 + $this->acquireCalls++;
348 +
349 + return $this->canAcquire;
350 + }
351 +
352 + public function releaseGlobalJobsLock(): bool
353 + {
354 + $this->releaseCalls++;
355 +
356 + return true;
250 357 }
251 358 }