xref: /dokuwiki/_test/tests/Search/IndexerTest.php (revision 2cda016644e923dbda996c52bedee2113ba6d653)
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