class CronSuspendQueueDelayTest
Same name in other branches
- 10 core/tests/Drupal/Tests/Core/Cron/CronSuspendQueueDelayTest.php \Drupal\Tests\Core\Cron\CronSuspendQueueDelayTest
Test Cron handling of suspended queues with a delay.
@group Cron @covers \Drupal\Core\Queue\SuspendQueueException @coversDefaultClass \Drupal\Core\Cron
Hierarchy
- class \Drupal\Tests\UnitTestCase extends \PHPUnit\Framework\TestCase uses \Drupal\Tests\PhpUnitCompatibilityTrait, \Prophecy\PhpUnit\ProphecyTrait, \Drupal\TestTools\Extension\DeprecationBridge\ExpectDeprecationTrait, \Drupal\Tests\RandomGeneratorTrait
- class \Drupal\Tests\Core\Cron\CronSuspendQueueDelayTest extends \Drupal\Tests\UnitTestCase
Expanded class hierarchy of CronSuspendQueueDelayTest
File
-
core/
tests/ Drupal/ Tests/ Core/ Cron/ CronSuspendQueueDelayTest.php, line 31
Namespace
Drupal\Tests\Core\CronView source
final class CronSuspendQueueDelayTest extends UnitTestCase {
/**
* Constructor arguments for \Drupal\Core\Cron.
*
* @var object[]|\PHPUnit\Framework\MockObject\MockObject[]
*/
protected $cronConstructorArguments;
/**
* A worker for testing.
*
* @var \Drupal\Core\Queue\QueueWorkerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $workerA;
/**
* A worker for testing.
*
* @var \Drupal\Core\Queue\QueueWorkerInterface|\PHPUnit\Framework\MockObject\MockObject
*/
protected $workerB;
/**
* {@inheritdoc}
*/
protected function setUp() : void {
parent::setUp();
$lock = $this->createMock(LockBackendInterface::class);
$lock->expects($this->any())
->method('acquire')
->willReturn(TRUE);
$this->cronConstructorArguments = [
'moduleHandler' => $this->createMock(ModuleHandlerInterface::class),
'lock' => $lock,
'queueFactory' => $this->createMock(QueueFactory::class),
'state' => $this->createMock(StateInterface::class),
'accountSwitcher' => $this->createMock(AccountSwitcherInterface::class),
'logger' => $this->createMock(LoggerInterface::class),
'queueManager' => $this->createMock(QueueWorkerManagerInterface::class),
'time' => $this->createMock(TimeInterface::class),
'queue_config' => [],
];
// Capture error logs.
$config = $this->createMock(ImmutableConfig::class);
$config->expects($this->any())
->method('get')
->with('logging')
->willReturn(0);
$configFactory = $this->createMock(ConfigFactoryInterface::class);
$configFactory->expects($this->any())
->method('get')
->with('system.cron')
->willReturn($config);
$container = new ContainerBuilder();
$container->set('config.factory', $configFactory);
\Drupal::setContainer($container);
$this->workerA = $this->createMock(QueueWorkerInterface::class);
$this->workerA
->expects($this->any())
->method('getPluginDefinition')
->willReturn([
'cron' => [
'time' => 300,
],
]);
$this->workerB = $this->createMock(QueueWorkerInterface::class);
$this->workerB
->expects($this->any())
->method('getPluginDefinition')
->willReturn([
'cron' => [
'time' => 300,
],
]);
}
/**
* Tests a queue is reprocessed again after other queues.
*
* Two queues are created:
* - test_worker_a.
* - test_worker_b.
*
* Queues and items are processed:
* - test_worker_a:
* - item throws SuspendQueueException with 2.0 delay.
* - test_worker_b:
* - item executes normally.
* - test_worker_a:
* - item throws SuspendQueueException with 3.0 delay.
* - test_worker_a:
* - no items remaining, quits.
*/
public function testSuspendQueue() : void {
[
'queueFactory' => $queueFactory,
'queueManager' => $queueManager,
'time' => $time,
] = $this->cronConstructorArguments;
$cron = $this->getMockBuilder(Cron::class)
->onlyMethods([
'usleep',
])
->setConstructorArgs($this->cronConstructorArguments)
->getMock();
$delays = [
2000000,
3000000,
];
$cron->expects($this->exactly(count($delays)))
->method('usleep')
->with($this->callback(function (int $delay) use (&$delays) : bool {
return array_shift($delays) === $delay;
}));
$queueManager->expects($this->once())
->method('getDefinitions')
->willReturn([
'test_worker_a' => [
'id' => 'test_worker_a',
'cron' => [
'time' => 300,
],
],
'test_worker_b' => [
'id' => 'test_worker_b',
'cron' => [
'time' => 300,
],
],
]);
$queueA = $this->createMock(QueueInterface::class);
$queueB = $this->createMock(QueueInterface::class);
$queueFactory->expects($this->exactly(2))
->method('get')
->willReturnMap([
[
'test_worker_a',
FALSE,
$queueA,
],
[
'test_worker_b',
FALSE,
$queueB,
],
]);
// Expect this queue to be processed twice.
$queueA->expects($this->exactly(3))
->method('claimItem')
->willReturnOnConsecutiveCalls((object) [
'data' => 'test_data_a1',
], (object) [
'data' => 'test_data_a2',
], FALSE);
// Expect this queue to be processed once.
$queueB->expects($this->exactly(2))
->method('claimItem')
->willReturnOnConsecutiveCalls((object) [
'data' => 'test_data_b1',
], FALSE);
$queueManager->expects($this->any())
->method('createInstance')
->willReturnMap([
[
'test_worker_a',
[],
$this->workerA,
],
[
'test_worker_b',
[],
$this->workerB,
],
]);
$this->workerA
->expects($this->exactly(2))
->method('processItem')
->with($this->anything())
->willReturnOnConsecutiveCalls($this->throwException(new SuspendQueueException('', 0, NULL, 2.0)), $this->throwException(new SuspendQueueException('', 0, NULL, 3.0)));
$this->workerB
->expects($this->once())
->method('processItem')
->with('test_data_b1');
$time->expects($this->any())
->method('getCurrentTime')
->willReturn(60);
$cron->run();
}
/**
* Tests queues may be re-processed by whether delay exceeds threshold.
*
* Cron will pause and reprocess a queue after a delay if a worker throws
* a SuspendQueueException with a delay time not exceeding the maximum wait
* config.
*
* @param float $threshold
* The configured threshold.
* @param float $suspendQueueDelay
* An interval in seconds a worker will suspend the queue.
* @param bool $expectQueueDelay
* Whether to expect cron to sleep and re-process the queue.
*
* @dataProvider providerSuspendQueueThreshold
*/
public function testSuspendQueueThreshold(float $threshold, float $suspendQueueDelay, bool $expectQueueDelay) : void {
$this->cronConstructorArguments['queue_config'] = [
'suspendMaximumWait' => $threshold,
];
[
'queueFactory' => $queueFactory,
'queueManager' => $queueManager,
] = $this->cronConstructorArguments;
$cron = $this->getMockBuilder(Cron::class)
->onlyMethods([
'usleep',
])
->setConstructorArgs($this->cronConstructorArguments)
->getMock();
$cron->expects($expectQueueDelay ? $this->once() : $this->never())
->method('usleep');
$queueManager->expects($this->once())
->method('getDefinitions')
->willReturn([
'test_worker' => [
'id' => 'test_worker',
'cron' => 300,
],
]);
$queue = $this->createMock(QueueInterface::class);
$queueFactory->expects($this->once())
->method('get')
->willReturnMap([
[
'test_worker',
FALSE,
$queue,
],
]);
$queue->expects($this->exactly($expectQueueDelay ? 2 : 1))
->method('claimItem')
->willReturnOnConsecutiveCalls((object) [
'data' => 'test_data',
], FALSE);
$queueManager->expects($this->exactly(1))
->method('createInstance')
->with('test_worker')
->willReturn($this->workerA);
$this->workerA
->expects($this->once())
->method('processItem')
->with($this->anything())
->willReturnOnConsecutiveCalls($this->throwException(new SuspendQueueException('', 0, NULL, $suspendQueueDelay)));
$cron->run();
}
/**
* Data for testing.
*
* @return array
* Scenarios for testing.
*/
public static function providerSuspendQueueThreshold() : array {
$scenarios = [];
$scenarios['cron will wait for the queue, and rerun'] = [
15.0,
10.0,
TRUE,
];
$scenarios['cron will not wait for the queue, and exit'] = [
15.0,
20.0,
FALSE,
];
return $scenarios;
}
/**
* Tests queues are executed in order.
*
* If multiple queues are delayed, they must execute in order of time.
*/
public function testSuspendQueueOrder() : void {
[
'queueFactory' => $queueFactory,
'queueManager' => $queueManager,
'time' => $time,
] = $this->cronConstructorArguments;
$cron = $this->getMockBuilder(Cron::class)
->onlyMethods([
'usleep',
])
->setConstructorArgs($this->cronConstructorArguments)
->getMock();
$cron->expects($this->any())
->method('usleep');
$queueManager->expects($this->once())
->method('getDefinitions')
->willReturn([
'test_worker_a' => [
'id' => 'test_worker_a',
'cron' => [
'time' => 300,
],
],
'test_worker_b' => [
'id' => 'test_worker_b',
'cron' => [
'time' => 300,
],
],
'test_worker_c' => [
'id' => 'test_worker_c',
'cron' => [
'time' => 300,
],
],
'test_worker_d' => [
'id' => 'test_worker_d',
'cron' => [
'time' => 300,
],
],
]);
$queueA = $this->createMock(QueueInterface::class);
$queueB = $this->createMock(QueueInterface::class);
$queueC = $this->createMock(QueueInterface::class);
$queueD = $this->createMock(QueueInterface::class);
$queueFactory->expects($this->exactly(4))
->method('get')
->willReturnMap([
[
'test_worker_a',
FALSE,
$queueA,
],
[
'test_worker_b',
FALSE,
$queueB,
],
[
'test_worker_c',
FALSE,
$queueC,
],
[
'test_worker_d',
FALSE,
$queueD,
],
]);
$queueA->expects($this->any())
->method('claimItem')
->willReturnOnConsecutiveCalls((object) [
'data' => 'test_data_from_queue_a',
], FALSE);
$queueB->expects($this->any())
->method('claimItem')
->willReturnOnConsecutiveCalls((object) [
'data' => 'test_data_from_queue_b',
], (object) [
'data' => 'test_data_from_queue_b',
], FALSE);
$queueC->expects($this->any())
->method('claimItem')
->willReturnOnConsecutiveCalls((object) [
'data' => 'test_data_from_queue_c',
], (object) [
'data' => 'test_data_from_queue_c',
], FALSE);
$queueD->expects($this->any())
->method('claimItem')
->willReturnOnConsecutiveCalls((object) [
'data' => 'test_data_from_queue_d',
], FALSE);
// Recycle the same worker for all queues to test order sanely:
$queueManager->expects($this->any())
->method('createInstance')
->willReturnMap([
[
'test_worker_a',
[],
$this->workerA,
],
[
'test_worker_b',
[],
$this->workerA,
],
[
'test_worker_c',
[],
$this->workerA,
],
[
'test_worker_d',
[],
$this->workerA,
],
]);
$queues = [
// All queues are executed in sequence of definition:
'test_data_from_queue_a',
'test_data_from_queue_b',
'test_data_from_queue_c',
'test_data_from_queue_d',
// Queue C is executed again, and before queue B.
'test_data_from_queue_c',
// Queue B is executed again, after queue C since its delay was longer.
'test_data_from_queue_b',
];
$this->workerA
->expects($this->exactly(count($queues)))
->method('processItem')
->with($this->callback(function ($queue) use (&$queues) : bool {
return array_shift($queues) === $queue;
}))
->willReturnOnConsecutiveCalls(NULL, $this->throwException(new SuspendQueueException('', 0, NULL, 16.0)), $this->throwException(new SuspendQueueException('', 0, NULL, 8.0)), NULL, NULL, NULL);
$currentTime = 60;
$time->expects($this->any())
->method('getCurrentTime')
->willReturnCallback(function () use (&$currentTime) : int {
return (int) $currentTime;
});
$time->expects($this->any())
->method('getCurrentMicroTime')
->willReturnCallback(function () use (&$currentTime) : float {
return (double) $currentTime;
});
$delays = [
// Expect to wait for 8 seconds, then accelerate time by 4 seconds.
4,
8000000,
// SuspendQueueException requests to delay by 16 seconds, but 4 seconds
// have passed above, so there are just 12 seconds remaining:
0,
12000000,
];
$cron->expects($this->exactly(count($delays) / 2))
->method('usleep')
->with($this->callback(function (int $delay) use (&$currentTime, &$delays) : bool {
$currentTime += array_shift($delays);
return array_shift($delays) === $delay;
}));
$cron->run();
}
}
Members
Title Sort descending | Modifiers | Object type | Summary | Overriden Title |
---|---|---|---|---|
CronSuspendQueueDelayTest::$cronConstructorArguments | protected | property | Constructor arguments for \Drupal\Core\Cron. | |
CronSuspendQueueDelayTest::$workerA | protected | property | A worker for testing. | |
CronSuspendQueueDelayTest::$workerB | protected | property | A worker for testing. | |
CronSuspendQueueDelayTest::providerSuspendQueueThreshold | public static | function | Data for testing. | |
CronSuspendQueueDelayTest::setUp | protected | function | Overrides UnitTestCase::setUp | |
CronSuspendQueueDelayTest::testSuspendQueue | public | function | Tests a queue is reprocessed again after other queues. | |
CronSuspendQueueDelayTest::testSuspendQueueOrder | public | function | Tests queues are executed in order. | |
CronSuspendQueueDelayTest::testSuspendQueueThreshold | public | function | Tests queues may be re-processed by whether delay exceeds threshold. | |
ExpectDeprecationTrait::expectDeprecation | public | function | Adds an expected deprecation. | |
ExpectDeprecationTrait::getCallableName | private static | function | Returns a callable as a string suitable for inclusion in a message. | |
ExpectDeprecationTrait::setUpErrorHandler | public | function | Sets up the test error handler. | |
ExpectDeprecationTrait::tearDownErrorHandler | public | function | Tears down the test error handler. | |
RandomGeneratorTrait::getRandomGenerator | protected | function | Gets the random generator for the utility methods. | |
RandomGeneratorTrait::randomMachineName | protected | function | Generates a unique random string containing letters and numbers. | |
RandomGeneratorTrait::randomObject | public | function | Generates a random PHP object. | |
RandomGeneratorTrait::randomString | public | function | Generates a pseudo-random string of ASCII characters of codes 32 to 126. | |
UnitTestCase::$root | protected | property | The app root. | |
UnitTestCase::getClassResolverStub | protected | function | Returns a stub class resolver. | |
UnitTestCase::getConfigFactoryStub | public | function | Returns a stub config factory that behaves according to the passed array. | |
UnitTestCase::getConfigStorageStub | public | function | Returns a stub config storage that returns the supplied configuration. | |
UnitTestCase::getContainerWithCacheTagsInvalidator | protected | function | Sets up a container with a cache tags invalidator. | |
UnitTestCase::getStringTranslationStub | public | function | Returns a stub translation manager that just returns the passed string. | |
UnitTestCase::setUpBeforeClass | public static | function |
Buggy or inaccurate documentation? Please file an issue. Need support? Need help programming? Connect with the Drupal community.