xref: /dokuwiki/_test/tests/Search/IndexerTest.php (revision 79dae64d6746363b953e6c82844ef285c37c3310)
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        // move the page on disk
58        io_rename(wikiFN('old_name'), wikiFN('new_name'));
59        saveWikiText('new_name', 'Old page content words.', 'Renamed');
60
61        $indexer->renamePage('old_name', 'new_name');
62
63        // new page should be indexed
64        $pageIndex = new FileIndex('page');
65        $result = $pageIndex->search('/^new_name$/');
66        $this->assertNotEmpty($result, 'new_name not found in page.idx after rename');
67    }
68
69    /**
70     * renamePage must preserve the renamed page's outgoing references
71     *
72     * The rename only changes the page's name in the index, not its content, so all of
73     * its index associations - including the pages it links to (relation_references) -
74     * must survive under the new name. This is what allows a page renamed early during a
75     * namespace move to still be found as a backlink source for pages moved afterwards.
76     * It must work even though the destination page is not on disk yet at rename time
77     * (the move operation writes it only later), so re-indexing from disk cannot be relied
78     * upon here.
79     *
80     * @see https://github.com/dokuwiki/dokuwiki - regression after the indexer rewrite
81     */
82    public function testRenamePagePreservesOutgoingReferences()
83    {
84        $indexer = new Indexer();
85
86        saveWikiText('refsource', '[[target:page]]', 'Test initialization');
87        $indexer->addPage('refsource');
88
89        $search = new MetadataSearch();
90
91        // sanity: the source page references target:page
92        $value = 'target:page';
93        $this->assertEquals(['refsource'], $search->lookupKey('relation_references', $value));
94
95        // rename the source page WITHOUT writing the destination to disk first,
96        // mimicking how the move plugin calls renamePage before saving the new page
97        $indexer->renamePage('refsource', 'moved:newsource');
98
99        // the outgoing reference must now belong to the renamed page
100        $value = 'target:page';
101        $this->assertEquals(
102            ['moved:newsource'],
103            $search->lookupKey('relation_references', $value),
104            'rename lost the outgoing reference of the renamed page'
105        );
106    }
107
108    /**
109     * renamePage onto a name that already has its own index entry
110     *
111     * The renamed page must take over the destination name (keeping its own data) while the
112     * destination's previous data is dropped. The stale destination row must be vacated so the
113     * name resolves only to the renamed entity and does not leak as a phantom page.
114     */
115    public function testRenamePageOntoExistingPage()
116    {
117        $indexer = new Indexer();
118
119        saveWikiText('src', '[[target:fromsrc]]', 'Test initialization');
120        $indexer->addPage('src');
121        saveWikiText('dst', '[[target:fromdst]]', 'Test initialization');
122        $indexer->addPage('dst');
123
124        $indexer->renamePage('src', 'dst');
125
126        $search = new MetadataSearch();
127
128        // dst now carries src's outgoing reference ...
129        $value = 'target:fromsrc';
130        $this->assertEquals(['dst'], $search->lookupKey('relation_references', $value));
131        // ... and the destination's previous reference is gone
132        $value = 'target:fromdst';
133        $this->assertEquals([], $search->lookupKey('relation_references', $value));
134
135        // exactly one entity named 'dst', the old name and any phantom entry are gone
136        $allPages = $indexer->getAllPages();
137        $this->assertSame(['dst'], array_values(array_filter($allPages, fn($p) => $p === 'dst' || $p === 'src')));
138    }
139
140    /**
141     * Test that clear removes all index files
142     */
143    public function testClear()
144    {
145        global $conf;
146        $indexer = new Indexer();
147
148        saveWikiText('clearpage', 'Some words to index.', 'Test initialization');
149        $indexer->addPage('clearpage');
150
151        $this->assertFileExists($conf['indexdir'] . '/page.idx');
152
153        $indexer->clear();
154
155        $this->assertFileDoesNotExist($conf['indexdir'] . '/page.idx');
156    }
157
158    /**
159     * Test that getVersion returns a version string
160     */
161    public function testGetVersion()
162    {
163        $indexer = new Indexer();
164        $version = $indexer->getVersion();
165        $this->assertNotEmpty($version);
166        $this->assertIsString((string)$version);
167    }
168
169    /**
170     * Test needsIndexing returns true for new pages
171     */
172    public function testNeedsIndexing()
173    {
174        $indexer = new Indexer();
175
176        saveWikiText('needsidx', 'Some content.', 'Test initialization');
177        // a brand-new page has no .indexed tag yet, so it always needs indexing
178        $this->assertTrue($indexer->needsIndexing('needsidx'));
179
180        // once indexed it is up to date, even when saved and indexed in the same second
181        $indexer->addPage('needsidx');
182        $this->assertFalse($indexer->needsIndexing('needsidx'));
183        $this->assertTrue($indexer->needsIndexing('needsidx', true)); // force
184    }
185
186    /**
187     * Test the logger callback
188     */
189    public function testLogger()
190    {
191        $messages = [];
192        $indexer = (new Indexer())->setLogger(function ($msg) use (&$messages) {
193            $messages[] = $msg;
194        });
195
196        saveWikiText('logpage', 'Log test content.', 'Test initialization');
197        $indexer->addPage('logpage');
198
199        // second call detects the page is already up to date
200        $indexer->addPage('logpage');
201        $this->assertNotEmpty($messages);
202        $this->assertStringContainsString('up to date', end($messages));
203    }
204}
205