1<?php 2 3namespace dokuwiki\test\Search; 4 5use dokuwiki\Search\Indexer; 6use dokuwiki\Search\Index\FileIndex; 7use dokuwiki\Search\MetadataSearch; 8 9/** 10 * Tests the Indexer class 11 */ 12class IndexerTest extends \DokuWikiTest 13{ 14 /** 15 * Test basic page indexing via addPage 16 */ 17 public function testAddPage() 18 { 19 $indexer = new Indexer(); 20 21 saveWikiText('testpage', 'Foo bar baz.', 'Test initialization'); 22 $indexer->addPage('testpage'); 23 24 // page should be in the entity index 25 $pageIndex = new FileIndex('page'); 26 $result = $pageIndex->search('/^testpage$/'); 27 $this->assertNotEmpty($result, 'testpage not found in page.idx'); 28 } 29 30 /** 31 * Test that deletePage clears data 32 */ 33 public function testDeletePage() 34 { 35 $indexer = new Indexer(); 36 37 saveWikiText('delpage', 'Delete me content.', 'Test initialization'); 38 $indexer->addPage('delpage'); 39 $indexer->deletePage('delpage', true); 40 41 // page entity persists in page.idx but data is cleared 42 $pageIndex = new FileIndex('page'); 43 $result = $pageIndex->search('/^delpage$/'); 44 $this->assertNotEmpty($result, 'delpage should persist in page.idx'); 45 } 46 47 /** 48 * Test renamePage clears old and indexes new 49 */ 50 public function testRenamePage() 51 { 52 $indexer = new Indexer(); 53 54 saveWikiText('old_name', 'Old page content words.', 'Test initialization'); 55 $indexer->addPage('old_name'); 56 57 $indexer->renamePage('old_name', 'new_name'); 58 59 // the entity is renamed in place: new name present, old name gone 60 $pageIndex = new FileIndex('page'); 61 $this->assertNotEmpty($pageIndex->search('/^new_name$/'), 'new_name not found in page.idx after rename'); 62 $this->assertEmpty($pageIndex->search('/^old_name$/'), 'old_name should be gone from page.idx after rename'); 63 } 64 65 /** 66 * renamePage must preserve the renamed page's outgoing references 67 * 68 * The rename only changes the page's name in the index, not its content, so all of 69 * its index associations - including the pages it links to (relation_references) - 70 * must survive under the new name. This is what allows a page renamed early during a 71 * namespace move to still be found as a backlink source for pages moved afterwards. 72 * It must work even though the destination page is not on disk yet at rename time 73 * (the move operation writes it only later), so re-indexing from disk cannot be relied 74 * upon here. 75 * 76 * @see https://github.com/dokuwiki/dokuwiki - regression after the indexer rewrite 77 */ 78 public function testRenamePagePreservesOutgoingReferences() 79 { 80 $indexer = new Indexer(); 81 82 saveWikiText('refsource', '[[target:page]]', 'Test initialization'); 83 $indexer->addPage('refsource'); 84 85 $search = new MetadataSearch(); 86 87 // sanity: the source page references target:page 88 $value = 'target:page'; 89 $this->assertEquals(['refsource'], $search->lookupKey('relation_references', $value)); 90 91 // rename the source page WITHOUT writing the destination to disk first, 92 // mimicking how the move plugin calls renamePage before saving the new page 93 $indexer->renamePage('refsource', 'moved:newsource'); 94 95 // the outgoing reference must now belong to the renamed page 96 $value = 'target:page'; 97 $this->assertEquals( 98 ['moved:newsource'], 99 $search->lookupKey('relation_references', $value), 100 'rename lost the outgoing reference of the renamed page' 101 ); 102 } 103 104 /** 105 * renamePage onto a name that already has its own index entry 106 * 107 * The renamed page must take over the destination name (keeping its own data) while the 108 * destination's previous data is dropped. The stale destination row must be vacated so the 109 * name resolves only to the renamed entity and does not leak as a phantom page. 110 */ 111 public function testRenamePageOntoExistingPage() 112 { 113 $indexer = new Indexer(); 114 115 saveWikiText('src', '[[target:fromsrc]]', 'Test initialization'); 116 $indexer->addPage('src'); 117 saveWikiText('dst', '[[target:fromdst]]', 'Test initialization'); 118 $indexer->addPage('dst'); 119 120 $indexer->renamePage('src', 'dst'); 121 122 $search = new MetadataSearch(); 123 124 // dst now carries src's outgoing reference ... 125 $value = 'target:fromsrc'; 126 $this->assertEquals(['dst'], $search->lookupKey('relation_references', $value)); 127 // ... and the destination's previous reference is gone 128 $value = 'target:fromdst'; 129 $this->assertEquals([], $search->lookupKey('relation_references', $value)); 130 131 // exactly one entity named 'dst', the old name and any phantom entry are gone 132 $allPages = $indexer->getAllPages(); 133 $this->assertSame(['dst'], array_values(array_filter($allPages, fn($p) => $p === 'dst' || $p === 'src'))); 134 } 135 136 /** 137 * Test that clear removes all index files 138 */ 139 public function testClear() 140 { 141 global $conf; 142 $indexer = new Indexer(); 143 144 saveWikiText('clearpage', 'Some words to index.', 'Test initialization'); 145 $indexer->addPage('clearpage'); 146 147 $this->assertFileExists($conf['indexdir'] . '/page.idx'); 148 149 $indexer->clear(); 150 151 $this->assertFileDoesNotExist($conf['indexdir'] . '/page.idx'); 152 } 153 154 /** 155 * Test that getVersion returns a version string 156 */ 157 public function testGetVersion() 158 { 159 $indexer = new Indexer(); 160 // with no version-modifying plugins active the raw INDEXER_VERSION is returned 161 $this->assertSame(\dokuwiki\Search\INDEXER_VERSION, $indexer->getVersion()); 162 } 163 164 /** 165 * Test needsIndexing returns true for new pages 166 */ 167 public function testNeedsIndexing() 168 { 169 $indexer = new Indexer(); 170 171 saveWikiText('needsidx', 'Some content.', 'Test initialization'); 172 // a brand-new page has no .indexed tag yet, so it always needs indexing 173 $this->assertTrue($indexer->needsIndexing('needsidx')); 174 175 // once indexed it is up to date, even when saved and indexed in the same second 176 $indexer->addPage('needsidx'); 177 $this->assertFalse($indexer->needsIndexing('needsidx')); 178 $this->assertTrue($indexer->needsIndexing('needsidx', true)); // force 179 } 180 181 /** 182 * addPage returns true when it indexed the page and false when there was nothing to do 183 */ 184 public function testAddPageReturn() 185 { 186 $indexer = new Indexer(); 187 188 saveWikiText('retadd', 'Some content to index.', 'Test initialization'); 189 $this->assertTrue($indexer->addPage('retadd'), 'addPage should report work done'); 190 191 // already up to date: nothing to do 192 $this->assertFalse($indexer->addPage('retadd'), 'addPage should report nothing to do when up to date'); 193 194 // forcing reindexing always reports work done 195 $this->assertTrue($indexer->addPage('retadd', true), 'forced addPage should report work done'); 196 } 197 198 /** 199 * deletePage returns true when it removed the page and false when there was nothing to do 200 */ 201 public function testDeletePageReturn() 202 { 203 $indexer = new Indexer(); 204 205 // never indexed and not forced: nothing to do 206 $this->assertFalse($indexer->deletePage('retdel'), 'deletePage should report nothing to do for an unknown page'); 207 208 saveWikiText('retdel', 'Delete me content.', 'Test initialization'); 209 $indexer->addPage('retdel'); 210 $this->assertTrue($indexer->deletePage('retdel'), 'deletePage should report work done'); 211 212 // the delete removed the .indexed tag, so a second unforced call has nothing to do 213 $this->assertFalse($indexer->deletePage('retdel'), 'deletePage should report nothing to do once removed'); 214 } 215 216 /** 217 * renamePage returns true when it renamed the page and false for the no-op cases 218 */ 219 public function testRenamePageReturn() 220 { 221 $indexer = new Indexer(); 222 223 // identical names: nothing to do 224 $this->assertFalse($indexer->renamePage('retrename', 'retrename'), 'renamePage should report nothing to do for identical names'); 225 226 // old page not in the index: nothing to do 227 $this->assertFalse($indexer->renamePage('retrename', 'retrenamed'), 'renamePage should report nothing to do for an unindexed page'); 228 229 saveWikiText('retrename', 'Rename me content.', 'Test initialization'); 230 $indexer->addPage('retrename'); 231 $this->assertTrue($indexer->renamePage('retrename', 'retrenamed'), 'renamePage should report work done'); 232 } 233 234 /** 235 * Test the logger callback 236 */ 237 public function testLogger() 238 { 239 $messages = []; 240 $indexer = (new Indexer())->setLogger(function ($msg) use (&$messages) { 241 $messages[] = $msg; 242 }); 243 244 saveWikiText('logpage', 'Log test content.', 'Test initialization'); 245 $indexer->addPage('logpage'); 246 247 // second call detects the page is already up to date 248 $indexer->addPage('logpage'); 249 $this->assertNotEmpty($messages); 250 $this->assertStringContainsString('up to date', end($messages)); 251 } 252} 253