search_api-8.x-1.15/tests/src/Kernel/BasicTrackerTest.php
tests/src/Kernel/BasicTrackerTest.php
<?php
namespace Drupal\Tests\search_api\Kernel;
use Drupal\Core\Database\Connection;
use Drupal\Core\Database\Transaction;
use Drupal\KernelTests\KernelTestBase;
use Drupal\search_api\Entity\Index;
use Drupal\search_api\Utility\Utility;
use Psr\Log\LoggerInterface;
/**
* Tests the "default" tracker plugin.
*
* @group search_api
*
* @coversDefaultClass \Drupal\search_api\Plugin\search_api\tracker\Basic
*/
class BasicTrackerTest extends KernelTestBase {
/**
* {@inheritdoc}
*/
public static $modules = [
'user',
'search_api',
'system',
];
/**
* The tracker plugin used for this test.
*
* @var \Drupal\search_api\Plugin\search_api\tracker\Basic
*/
protected $tracker;
/**
* The search index used for this test.
*
* @var \Drupal\search_api\IndexInterface
*/
protected $index;
/**
* The test time service used for this test.
*
* @var \Drupal\Tests\search_api\Kernel\TestTimeService
*/
protected $timeService;
/**
* {@inheritdoc}
*/
public function setUp() {
parent::setUp();
$this->installSchema('search_api', ['search_api_item']);
$this->installConfig('search_api');
// Do not use a batch for tracking the initial items after creating an
// index when running the tests via the GUI. Otherwise, it seems Drupal's
// Batch API gets confused and the test fails.
if (!Utility::isRunningInCli()) {
\Drupal::state()->set('search_api_use_tracking_batch', FALSE);
}
$this->index = Index::create([
'id' => 'index',
'tracker_settings' => [
'default' => [],
],
]);
$this->tracker = $this->index->getTrackerInstance();
$this->timeService = new TestTimeService();
$this->tracker->setTimeService($this->timeService);
}
/**
* Tests tracking.
*
* @param string $indexing_order
* The indexing order setting to use – "fifo" or "lifo".
*
* @dataProvider trackingDataProvider
*/
public function testTracking($indexing_order) {
// Add a logger that throws an exception when used, so a caught exception
// within any of the tracker methods will still cause a test fail.
/** @var \PHPUnit_Framework_MockObject_MockObject|\Psr\Log\LoggerInterface $logger */
$logger = $this->createMock(LoggerInterface::class);
$logger->method('log')
->willReturnCallback(function ($level, $message, array $variables) {
$error = 'Tracking operation threw ';
$error .= strtr($message, $variables);
throw new \Exception($error);
});
$this->tracker->setLogger($logger);
$this->tracker->setConfiguration(['indexing_order' => $indexing_order]);
$datasource_1 = 'test1';
$datasource_2 = 'test2';
$ids = [];
foreach ([$datasource_1, $datasource_2] as $num => $datasource_id) {
foreach ([1, 2, 3] as $raw_id) {
$ids[$num][] = Utility::createCombinedId($datasource_id, $raw_id);
}
}
// Make sure we start from a "clean slate".
$this->assertIndexingStatus(0, 0);
$this->assertIndexingStatus(0, 0, $datasource_1);
$this->assertIndexingStatus(0, 0, $datasource_2);
// Make sure tracking items as deleted, updated or indexed has no effect if
// none were inserted before.
$this->tracker->trackItemsDeleted([$ids[0][0]]);
$this->assertIndexingStatus(0, 0);
$this->tracker->trackItemsUpdated([$ids[0][0]]);
$this->assertIndexingStatus(0, 0);
$this->tracker->trackItemsIndexed([$ids[0][0]]);
$this->assertIndexingStatus(0, 0);
// Now, finally, actually do something sensible and insert some items.
$this->tracker->trackItemsInserted([$ids[0][0]]);
$this->tracker->trackItemsInserted([$ids[1][1]]);
$this->timeService->advanceTime();
$this->tracker->trackItemsInserted([$ids[0][2], $ids[1][0]]);
$this->timeService->advanceTime();
// Make sure re-inserting an item doesn't cause problems.
$this->tracker->trackItemsInserted([$ids[0][1], $ids[1][2], $ids[0][0]]);
$this->timeService->advanceTime();
$this->assertIndexingStatus(0, 6);
$this->assertIndexingStatus(0, 3, $datasource_1);
$this->assertIndexingStatus(0, 3, $datasource_2);
// Make sure the remaining items are returned as expected.
$fifo = $indexing_order === 'fifo';
$to_index = $this->tracker->getRemainingItems(4);
sort($to_index);
if ($fifo) {
$expected = [$ids[0][0], $ids[0][2], $ids[1][0], $ids[1][1]];
}
else {
$expected = [$ids[0][1], $ids[0][2], $ids[1][0], $ids[1][2]];
}
$this->assertEquals($expected, $to_index);
$to_index = $this->tracker->getRemainingItems(1, $datasource_1);
if ($fifo) {
$expected = [$ids[0][0]];
}
else {
$expected = [$ids[0][1]];
}
$this->assertEquals($expected, $to_index);
$to_index = $this->tracker->getRemainingItems(-1);
sort($to_index);
$expected = array_merge($ids[0], $ids[1]);
$this->assertEquals($expected, $to_index);
$to_index = $this->tracker->getRemainingItems(-1, $datasource_2);
sort($to_index);
$this->assertEquals($ids[1], $to_index);
// Make sure that tracking an unindexed item as updated will not affect its
// position for FIFO, but will get it to the front for LIFO. (If we do this
// with the item that's in front for FIFO anyways, we can use the same code
// in both cases.)
$this->tracker->trackItemsUpdated([$ids[0][0]]);
$this->timeService->advanceTime();
$to_index = $this->tracker->getRemainingItems(1, $datasource_1);
$this->assertEquals([$ids[0][0]], $to_index);
// Make sure calling methods with an empty $ids array doesn't blow anything
// up.
$this->tracker->trackItemsInserted([]);
$this->tracker->trackItemsUpdated([]);
$this->tracker->trackItemsIndexed([]);
$this->tracker->trackItemsDeleted([]);
// None of this should have changed the indexing status of any items.
$this->assertIndexingStatus(0, 6);
$this->assertIndexingStatus(0, 3, $datasource_1);
$this->assertIndexingStatus(0, 3, $datasource_2);
// Now, change the status of some of the items.
$this->tracker->trackItemsIndexed([$ids[0][0], $ids[0][1], $ids[1][0]]);
$this->assertIndexingStatus(3, 6);
$this->assertIndexingStatus(2, 3, $datasource_1);
$this->assertIndexingStatus(1, 3, $datasource_2);
$to_index = $this->tracker->getRemainingItems(-1);
sort($to_index);
$expected = [$ids[0][2], $ids[1][1], $ids[1][2]];
$this->assertEquals($expected, $to_index);
$this->tracker->trackItemsUpdated([$ids[0][0], $ids[0][2]]);
$this->timeService->advanceTime();
$this->assertIndexingStatus(2, 6);
$this->assertIndexingStatus(1, 3, $datasource_1);
$this->assertIndexingStatus(1, 3, $datasource_2);
$to_index = $this->tracker->getRemainingItems(-1);
sort($to_index);
array_unshift($expected, $ids[0][0]);
$this->assertEquals($expected, $to_index);
$this->tracker->trackItemsDeleted([$ids[1][0], $ids[1][2]]);
$this->assertIndexingStatus(1, 4);
$this->assertIndexingStatus(1, 3, $datasource_1);
$this->assertIndexingStatus(0, 1, $datasource_2);
$to_index = $this->tracker->getRemainingItems(-1);
sort($to_index);
// The last element of $expected is $ids[1][2], which we just deleted.
unset($expected[3]);
$this->assertEquals($expected, $to_index);
// Make sure the right items are "at the front" of the queue in each case.
if ($fifo) {
// These are the only two (remaining) items that were never indexed, so
// they still have their original insert time stamp and thus go first.
$expected = [$ids[0][2], $ids[1][1]];
}
else {
// We just tracked an update for both of these, so they go first.
$expected = [$ids[0][0], $ids[0][2]];
}
$to_index = $this->tracker->getRemainingItems(2);
sort($to_index);
$this->assertEquals($expected, $to_index);
// Some more status changes.
$this->tracker->trackItemsInserted([$ids[1][2]]);
$this->timeService->advanceTime();
$this->assertIndexingStatus(1, 5);
$this->assertIndexingStatus(1, 3, $datasource_1);
$this->assertIndexingStatus(0, 2, $datasource_2);
$this->tracker->trackItemsIndexed(array_merge($ids[0], $ids[1]));
$this->assertIndexingStatus(5, 5);
$this->assertIndexingStatus(3, 3, $datasource_1);
$this->assertIndexingStatus(2, 2, $datasource_2);
$this->tracker->trackAllItemsUpdated($datasource_1);
$this->timeService->advanceTime();
$this->assertIndexingStatus(2, 5);
$this->assertIndexingStatus(0, 3, $datasource_1);
$this->assertIndexingStatus(2, 2, $datasource_2);
$this->tracker->trackItemsIndexed([$ids[0][0]]);
$this->assertIndexingStatus(3, 5);
$this->assertIndexingStatus(1, 3, $datasource_1);
$this->assertIndexingStatus(2, 2, $datasource_2);
$this->tracker->trackAllItemsUpdated();
$this->timeService->advanceTime();
$this->assertIndexingStatus(0, 5);
$this->assertIndexingStatus(0, 3, $datasource_1);
$this->assertIndexingStatus(0, 2, $datasource_2);
$this->tracker->trackAllItemsDeleted($datasource_2);
$this->assertIndexingStatus(0, 3);
$this->assertIndexingStatus(0, 3, $datasource_1);
$this->assertIndexingStatus(0, 0, $datasource_2);
$this->tracker->trackAllItemsDeleted();
$this->assertIndexingStatus(0, 0);
$this->assertIndexingStatus(0, 0, $datasource_1);
$this->assertIndexingStatus(0, 0, $datasource_2);
}
/**
* Provides test data for testTracking().
*
* @return array[]
* An array of argument arrays for testTracking().
*
* @see \Drupal\Tests\search_api\Kernel\BasicTrackerTest::testTracking()
*/
public function trackingDataProvider() {
return [
'FIFO' => ['fifo'],
'LIFO' => ['lifo'],
];
}
/**
* Tests whether a method properly handles exceptions.
*
* @param string $tracker_method
* The method to test.
* @param array $args
* (optional) The arguments to pass to the method.
* @param bool $uses_transaction
* (optional) Whether the method is expected to use a transaction (and roll
* it back upon encountering an exception).
*
* @dataProvider exceptionHandlingDataProvider
*/
public function testExceptionHandling($tracker_method, array $args = [], $uses_transaction = FALSE) {
/** @var \PHPUnit_Framework_MockObject_MockObject|\Drupal\Core\Database\Connection $connection */
$connection = $this->getMockBuilder(Connection::class)
->disableOriginalConstructor()
->getMock();
foreach (['select', 'insert', 'update', 'delete'] as $method) {
$connection->method($method)->willThrowException(new \Exception());
}
$transaction = $this->getMockBuilder(Transaction::class)
->disableOriginalConstructor()
->getMock();
$rolled_back = FALSE;
$rollback = function () use (&$rolled_back) {
$rolled_back = TRUE;
};
$transaction->method('rollback')->willReturnCallback($rollback);
$connection->method('startTransaction')->willReturn($transaction);
$this->tracker->setDatabaseConnection($connection);
/** @var \PHPUnit_Framework_MockObject_MockObject|\Psr\Log\LoggerInterface $logger */
$logger = $this->createMock(LoggerInterface::class);
$log = [];
$logger->method('log')->willReturnCallback(function () use (&$log) {
$log[] = func_get_args();
});
$this->tracker->setLogger($logger);
call_user_func_array([$this->tracker, $tracker_method], $args);
$this->assertCount(1, $log);
$this->assertStringStartsWith('%type', $log[0][1]);
if ($uses_transaction) {
$this->assertEquals(TRUE, $rolled_back);
}
}
/**
* Provides test data for testExceptionHandling().
*
* @return array[]
* An array of argument arrays for testExceptionHandling().
*
* @see \Drupal\Tests\search_api\Kernel\BasicTrackerTest::testExceptionHandling()
*/
public function exceptionHandlingDataProvider() {
return [
'trackItemsInserted()' => ['trackItemsInserted', [['']], TRUE],
'trackItemsUpdated()' => ['trackItemsUpdated', [['']], TRUE],
'trackAllItemsUpdated()' => ['trackAllItemsUpdated', [], TRUE],
'trackItemsIndexed()' => ['trackItemsIndexed', [['']], TRUE],
'trackItemsDeleted()' => ['trackItemsDeleted', [], TRUE],
'trackAllItemsDeleted()' => ['trackAllItemsDeleted', [], TRUE],
'getRemainingItems()' => ['getRemainingItems'],
'getTotalItemsCount()' => ['getTotalItemsCount'],
'getIndexedItemsCount()' => ['getIndexedItemsCount'],
'getRemainingItemsCount()' => ['getRemainingItemsCount'],
];
}
/**
* Asserts that the current tracking status is as expected.
*
* @param int $indexed
* The expected number of indexed items.
* @param int $total
* The expected total number of items.
* @param string|null $datasource_id
* (optional) The datasource for which to check indexing status, or NULL to
* check for the whole index.
*/
protected function assertIndexingStatus($indexed, $total, $datasource_id = NULL) {
$datasource = $datasource_id ? " for datasource $datasource_id" : '';
$actual_indexed = $this->tracker->getIndexedItemsCount($datasource_id);
$this->assertEquals($indexed, $actual_indexed, "$actual_indexed items indexed$datasource, $indexed expected.");
$actual_total = $this->tracker->getTotalItemsCount($datasource_id);
$this->assertEquals($total, $actual_total, "$actual_total items tracked in total$datasource, $total expected.");
$remaining = $total - $indexed;
$actual_remaining = $this->tracker->getRemainingItemsCount($datasource_id);
$this->assertEquals($remaining, $actual_remaining, "$actual_remaining items remaining to be indexed$datasource, $remaining expected.");
}
}
