summaryrefslogtreecommitdiffstatshomepage
path: root/core/tests/Drupal/Tests/Core/CronTest.php
blob: 40ae8fb1743e57c98aaac43441ec16e114663c95 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
<?php

declare(strict_types=1);

namespace Drupal\Tests\Core;

use Drupal\Component\Datetime\TimeInterface;
use Drupal\Core\Config\ConfigFactoryInterface;
use Drupal\Core\Config\ImmutableConfig;
use Drupal\Core\Cache\MemoryBackend;
use Drupal\Core\Cron;
use Drupal\Core\KeyValueStore\KeyValueMemoryFactory;
use Drupal\Core\Lock\NullLockBackend;
use Drupal\Core\Queue\DelayedRequeueException;
use Drupal\Core\Queue\Memory;
use Drupal\Core\Queue\RequeueException;
use Drupal\Core\Queue\SuspendQueueException;
use Drupal\Core\State\State;
use Drupal\Tests\UnitTestCase;
use Prophecy\Argument;
use Prophecy\Argument\ArgumentsWildcard;
use Psr\Log\LoggerInterface;
use Symfony\Component\DependencyInjection\ContainerBuilder;

/**
 * Tests the Cron class.
 *
 * @group Cron
 * @coversDefaultClass \Drupal\Core\Cron
 */
class CronTest extends UnitTestCase {

  const REQUEUE_COUNT = 3;

  /**
   * Define the duration of each item claim for this test.
   *
   * @var int
   */
  protected $claimTime = 300;

  /**
   * An instance of the Cron class for testing.
   *
   * @var \Drupal\Core\Cron
   */
  protected $cron;

  /**
   * The queue used to store test work items.
   *
   * @var \Drupal\Core\Queue\QueueInterface
   */
  protected $queue;

  /**
   * The current state of the test in memory.
   *
   * @var \Drupal\Core\State\State
   */
  protected $state;

  /**
   * {@inheritdoc}
   */
  protected function setUp(): void {
    parent::setUp();

    // Construct a state object used for testing logger assertions.
    $time = $this->prophesize(TimeInterface::class)->reveal();
    $this->state = new State(new KeyValueMemoryFactory(), new MemoryBackend($time), new NullLockBackend());

    // Create a mock logger to set a flag in the resulting state.
    $logger = $this->prophesize('Drupal\Core\Logger\LoggerChannelInterface');
    // Safely ignore the cron success message.
    $logger->info('Cron run completed.')->shouldBeCalled();
    // Set a flag to track when a message is logged by adding a callback
    // function for each logging method.
    foreach (get_class_methods(LoggerInterface::class) as $logger_method) {
      $logger->{$logger_method}(Argument::cetera())->will(function () {
        \Drupal::state()->set('cron_test.message_logged', TRUE);
      });
    }

    // Create a logger factory to produce the resulting logger.
    $logger_factory = $this->prophesize('Drupal\Core\Logger\LoggerChannelFactoryInterface');
    $logger_factory->get(Argument::exact('cron'))->willReturn($logger->reveal());

    // Create a mock time service.
    $time = $this->prophesize('Drupal\Component\Datetime\TimeInterface');

    // Create a mock config factory and config object.
    $config_factory = $this->prophesize(ConfigFactoryInterface::class);
    $config = $this->prophesize(ImmutableConfig::class);
    $config->get('logging')->willReturn(FALSE);
    $config_factory->get('system.cron')->willReturn($config->reveal());

    // Build the container using the resulting mock objects.
    \Drupal::setContainer(new ContainerBuilder());
    \Drupal::getContainer()->set('logger.factory', $logger_factory->reveal());
    \Drupal::getContainer()->set('datetime.time', $time->reveal());
    \Drupal::getContainer()->set('state', $this->state);
    \Drupal::getContainer()->set('config.factory', $config_factory->reveal());

    // Create mock objects for constructing the Cron class.
    $module_handler = $this->prophesize('Drupal\Core\Extension\ModuleHandlerInterface');
    $queue_factory = $this->prophesize('Drupal\Core\Queue\QueueFactory');
    $queue_worker_manager = $this->prophesize('Drupal\Core\Queue\QueueWorkerManagerInterface');
    $state = $this->prophesize('Drupal\Core\State\StateInterface');
    $account_switcher = $this->prophesize('Drupal\Core\Session\AccountSwitcherInterface');
    $queueConfig = [
      'suspendMaximumWait' => 30.0,
    ];

    // Create a lock that will always fail when attempting to acquire; we're
    // only interested in testing ::processQueues(), not the other stuff.
    $lock_backend = $this->prophesize('Drupal\Core\Lock\LockBackendInterface');
    $lock_backend->acquire('cron', Argument::cetera())->willReturn(TRUE);
    $lock_backend->release('cron')->shouldBeCalled();

    // Create a queue worker definition for testing purposes.
    $queue_worker = $this->randomMachineName();
    $queue_worker_definition = [
      'id' => $queue_worker,
      'cron' => [
        'time' => &$this->claimTime,
      ],
    ];

    // Create a queue instance for this queue worker.
    $this->queue = new Memory($queue_worker);
    $queue_factory->get($queue_worker)->willReturn($this->queue);

    // Create a mock queue worker plugin instance based on above definition.
    $queue_worker_plugin = $this->prophesize('Drupal\Core\Queue\QueueWorkerInterface');
    $queue_worker_plugin->getPluginId()->willReturn($queue_worker);
    $queue_worker_plugin->getPluginDefinition()->willReturn($queue_worker_definition);
    $queue_worker_plugin->processItem('Complete')->willReturn();
    $queue_worker_plugin->processItem('Exception')->willThrow(\Exception::class);
    $queue_worker_plugin->processItem('DelayedRequeueException')->willThrow(DelayedRequeueException::class);
    $queue_worker_plugin->processItem('SuspendQueueException')->willThrow(SuspendQueueException::class);
    // 'RequeueException' would normally result in an infinite loop.
    //
    // This is avoided by throwing RequeueException for the first few calls to
    // ::processItem() and then returning void. ::testRequeueException()
    // establishes sanity assertions for this case.
    $queue_worker_plugin->processItem('RequeueException')->will(function ($args, $mock, $method) {
      // Fetch the number of calls to this prophesied method. This value will
      // start at zero during the first call.
      $method_calls = count($mock->findProphecyMethodCalls($method->getMethodName(), new ArgumentsWildcard($args)));

      // Throw the expected exception on the first few calls.
      if ($method_calls < self::REQUEUE_COUNT) {
        \Drupal::state()->set('cron_test.requeue_count', $method_calls + 1);
        throw new RequeueException();
      }
    });

    // Set the mock queue worker manager to return the definition/plugin.
    $queue_worker_manager->getDefinitions()->willReturn([$queue_worker => $queue_worker_definition]);
    $queue_worker_manager->createInstance($queue_worker)->willReturn($queue_worker_plugin->reveal());

    // Construct the Cron class to test.
    $this->cron = new Cron($module_handler->reveal(), $lock_backend->reveal(), $queue_factory->reveal(), $state->reveal(), $account_switcher->reveal(), $logger->reveal(), $queue_worker_manager->reveal(), $time->reveal(), $queueConfig);
  }

  /**
   * Resets the testing state.
   */
  protected function resetTestingState(): void {
    $this->queue->deleteQueue();
    $this->state->set('cron_test.message_logged', FALSE);
    $this->state->set('cron_test.requeue_count', NULL);
  }

  /**
   * Data provider for ::testProcessQueues() method.
   */
  public static function processQueuesTestData() {
    return [
      ['Complete', 'assertFalse', 0],
      ['Exception', 'assertTrue', 1],
      ['DelayedRequeueException', 'assertFalse', 1],
      ['SuspendQueueException', 'assertTrue', 1],
      ['RequeueException', 'assertFalse', 0],
    ];
  }

  /**
   * Tests the ::processQueues() method.
   *
   * @covers ::processQueues
   * @dataProvider processQueuesTestData
   */
  public function testProcessQueues($item, $message_logged_assertion, $count_post_run): void {
    $this->resetTestingState();
    $this->queue->createItem($item);
    $this->assertFalse($this->state->get('cron_test.message_logged'));
    $this->assertEquals(1, $this->queue->numberOfItems());
    $this->cron->run();
    $this->{$message_logged_assertion}($this->state->get('cron_test.message_logged'));
    $this->assertEquals($count_post_run, $this->queue->numberOfItems());
  }

  /**
   * Verify that RequeueException causes an item to be processed multiple times.
   */
  public function testRequeueException(): void {
    $this->resetTestingState();
    $this->queue->createItem('RequeueException');
    $this->cron->run();

    // Fetch the number of times this item was requeued.
    $actual_requeue_count = $this->state->get('cron_test.requeue_count');
    // Make sure the item was requeued at least once.
    $this->assertIsInt($actual_requeue_count);
    // Ensure that the actual requeue count matches the expected value.
    $this->assertEquals(self::REQUEUE_COUNT, $actual_requeue_count);
  }

}