xref: /dokuwiki/_test/tests/Search/IndexerTest.php (revision 2ff7e61c42ef53b6a5d0a212c1785d7adeed08df)
1ede46466SAndreas Gohr<?php
2ede46466SAndreas Gohr
3ede46466SAndreas Gohrnamespace dokuwiki\test\Search;
4ede46466SAndreas Gohr
5ede46466SAndreas Gohruse dokuwiki\Search\Indexer;
6ede46466SAndreas Gohruse dokuwiki\Search\Index\FileIndex;
7*2ff7e61cSAndreas Gohruse dokuwiki\Search\MetadataSearch;
8ede46466SAndreas Gohr
9ede46466SAndreas Gohr/**
10ede46466SAndreas Gohr * Tests the Indexer class
11ede46466SAndreas Gohr */
12ede46466SAndreas Gohrclass IndexerTest extends \DokuWikiTest
13ede46466SAndreas Gohr{
14ede46466SAndreas Gohr    /**
15ede46466SAndreas Gohr     * Test basic page indexing via addPage
16ede46466SAndreas Gohr     */
17ede46466SAndreas Gohr    public function testAddPage()
18ede46466SAndreas Gohr    {
19ede46466SAndreas Gohr        $indexer = new Indexer();
20ede46466SAndreas Gohr
21ede46466SAndreas Gohr        saveWikiText('testpage', 'Foo bar baz.', 'Test initialization');
22ede46466SAndreas Gohr        $indexer->addPage('testpage');
23ede46466SAndreas Gohr
24ede46466SAndreas Gohr        // page should be in the entity index
25ede46466SAndreas Gohr        $pageIndex = new FileIndex('page');
26ede46466SAndreas Gohr        $result = $pageIndex->search('/^testpage$/');
27ede46466SAndreas Gohr        $this->assertNotEmpty($result, 'testpage not found in page.idx');
28ede46466SAndreas Gohr    }
29ede46466SAndreas Gohr
30ede46466SAndreas Gohr    /**
31ede46466SAndreas Gohr     * Test that deletePage clears data
32ede46466SAndreas Gohr     */
33ede46466SAndreas Gohr    public function testDeletePage()
34ede46466SAndreas Gohr    {
35ede46466SAndreas Gohr        $indexer = new Indexer();
36ede46466SAndreas Gohr
37ede46466SAndreas Gohr        saveWikiText('delpage', 'Delete me content.', 'Test initialization');
38ede46466SAndreas Gohr        $indexer->addPage('delpage');
39ede46466SAndreas Gohr        $indexer->deletePage('delpage', true);
40ede46466SAndreas Gohr
41ede46466SAndreas Gohr        // page entity persists in page.idx but data is cleared
42ede46466SAndreas Gohr        $pageIndex = new FileIndex('page');
43ede46466SAndreas Gohr        $result = $pageIndex->search('/^delpage$/');
44ede46466SAndreas Gohr        $this->assertNotEmpty($result, 'delpage should persist in page.idx');
45ede46466SAndreas Gohr    }
46ede46466SAndreas Gohr
47ede46466SAndreas Gohr    /**
48ede46466SAndreas Gohr     * Test renamePage clears old and indexes new
49ede46466SAndreas Gohr     */
50ede46466SAndreas Gohr    public function testRenamePage()
51ede46466SAndreas Gohr    {
52ede46466SAndreas Gohr        $indexer = new Indexer();
53ede46466SAndreas Gohr
54ede46466SAndreas Gohr        saveWikiText('old_name', 'Old page content words.', 'Test initialization');
55ede46466SAndreas Gohr        $indexer->addPage('old_name');
56ede46466SAndreas Gohr
57ede46466SAndreas Gohr        // move the page on disk
58ede46466SAndreas Gohr        io_rename(wikiFN('old_name'), wikiFN('new_name'));
59ede46466SAndreas Gohr        saveWikiText('new_name', 'Old page content words.', 'Renamed');
60ede46466SAndreas Gohr
61ede46466SAndreas Gohr        $indexer->renamePage('old_name', 'new_name');
62ede46466SAndreas Gohr
63ede46466SAndreas Gohr        // new page should be indexed
64ede46466SAndreas Gohr        $pageIndex = new FileIndex('page');
65ede46466SAndreas Gohr        $result = $pageIndex->search('/^new_name$/');
66ede46466SAndreas Gohr        $this->assertNotEmpty($result, 'new_name not found in page.idx after rename');
67ede46466SAndreas Gohr    }
68ede46466SAndreas Gohr
69ede46466SAndreas Gohr    /**
70*2ff7e61cSAndreas Gohr     * renamePage must preserve the renamed page's outgoing references
71*2ff7e61cSAndreas Gohr     *
72*2ff7e61cSAndreas Gohr     * The rename only changes the page's name in the index, not its content, so all of
73*2ff7e61cSAndreas Gohr     * its index associations - including the pages it links to (relation_references) -
74*2ff7e61cSAndreas Gohr     * must survive under the new name. This is what allows a page renamed early during a
75*2ff7e61cSAndreas Gohr     * namespace move to still be found as a backlink source for pages moved afterwards.
76*2ff7e61cSAndreas Gohr     * It must work even though the destination page is not on disk yet at rename time
77*2ff7e61cSAndreas Gohr     * (the move operation writes it only later), so re-indexing from disk cannot be relied
78*2ff7e61cSAndreas Gohr     * upon here.
79*2ff7e61cSAndreas Gohr     *
80*2ff7e61cSAndreas Gohr     * @see https://github.com/dokuwiki/dokuwiki - regression after the indexer rewrite
81*2ff7e61cSAndreas Gohr     */
82*2ff7e61cSAndreas Gohr    public function testRenamePagePreservesOutgoingReferences()
83*2ff7e61cSAndreas Gohr    {
84*2ff7e61cSAndreas Gohr        $indexer = new Indexer();
85*2ff7e61cSAndreas Gohr
86*2ff7e61cSAndreas Gohr        saveWikiText('refsource', '[[target:page]]', 'Test initialization');
87*2ff7e61cSAndreas Gohr        $indexer->addPage('refsource');
88*2ff7e61cSAndreas Gohr
89*2ff7e61cSAndreas Gohr        $search = new MetadataSearch();
90*2ff7e61cSAndreas Gohr
91*2ff7e61cSAndreas Gohr        // sanity: the source page references target:page
92*2ff7e61cSAndreas Gohr        $value = 'target:page';
93*2ff7e61cSAndreas Gohr        $this->assertEquals(['refsource'], $search->lookupKey('relation_references', $value));
94*2ff7e61cSAndreas Gohr
95*2ff7e61cSAndreas Gohr        // rename the source page WITHOUT writing the destination to disk first,
96*2ff7e61cSAndreas Gohr        // mimicking how the move plugin calls renamePage before saving the new page
97*2ff7e61cSAndreas Gohr        $indexer->renamePage('refsource', 'moved:newsource');
98*2ff7e61cSAndreas Gohr
99*2ff7e61cSAndreas Gohr        // the outgoing reference must now belong to the renamed page
100*2ff7e61cSAndreas Gohr        $value = 'target:page';
101*2ff7e61cSAndreas Gohr        $this->assertEquals(
102*2ff7e61cSAndreas Gohr            ['moved:newsource'],
103*2ff7e61cSAndreas Gohr            $search->lookupKey('relation_references', $value),
104*2ff7e61cSAndreas Gohr            'rename lost the outgoing reference of the renamed page'
105*2ff7e61cSAndreas Gohr        );
106*2ff7e61cSAndreas Gohr    }
107*2ff7e61cSAndreas Gohr
108*2ff7e61cSAndreas Gohr    /**
109*2ff7e61cSAndreas Gohr     * renamePage onto a name that already has its own index entry
110*2ff7e61cSAndreas Gohr     *
111*2ff7e61cSAndreas Gohr     * The renamed page must take over the destination name (keeping its own data) while the
112*2ff7e61cSAndreas Gohr     * destination's previous data is dropped. The stale destination row must be vacated so the
113*2ff7e61cSAndreas Gohr     * name resolves only to the renamed entity and does not leak as a phantom page.
114*2ff7e61cSAndreas Gohr     */
115*2ff7e61cSAndreas Gohr    public function testRenamePageOntoExistingPage()
116*2ff7e61cSAndreas Gohr    {
117*2ff7e61cSAndreas Gohr        $indexer = new Indexer();
118*2ff7e61cSAndreas Gohr
119*2ff7e61cSAndreas Gohr        saveWikiText('src', '[[target:fromsrc]]', 'Test initialization');
120*2ff7e61cSAndreas Gohr        $indexer->addPage('src');
121*2ff7e61cSAndreas Gohr        saveWikiText('dst', '[[target:fromdst]]', 'Test initialization');
122*2ff7e61cSAndreas Gohr        $indexer->addPage('dst');
123*2ff7e61cSAndreas Gohr
124*2ff7e61cSAndreas Gohr        $indexer->renamePage('src', 'dst');
125*2ff7e61cSAndreas Gohr
126*2ff7e61cSAndreas Gohr        $search = new MetadataSearch();
127*2ff7e61cSAndreas Gohr
128*2ff7e61cSAndreas Gohr        // dst now carries src's outgoing reference ...
129*2ff7e61cSAndreas Gohr        $value = 'target:fromsrc';
130*2ff7e61cSAndreas Gohr        $this->assertEquals(['dst'], $search->lookupKey('relation_references', $value));
131*2ff7e61cSAndreas Gohr        // ... and the destination's previous reference is gone
132*2ff7e61cSAndreas Gohr        $value = 'target:fromdst';
133*2ff7e61cSAndreas Gohr        $this->assertEquals([], $search->lookupKey('relation_references', $value));
134*2ff7e61cSAndreas Gohr
135*2ff7e61cSAndreas Gohr        // exactly one entity named 'dst', the old name and any phantom entry are gone
136*2ff7e61cSAndreas Gohr        $allPages = $indexer->getAllPages();
137*2ff7e61cSAndreas Gohr        $this->assertSame(['dst'], array_values(array_filter($allPages, fn($p) => $p === 'dst' || $p === 'src')));
138*2ff7e61cSAndreas Gohr    }
139*2ff7e61cSAndreas Gohr
140*2ff7e61cSAndreas Gohr    /**
141ede46466SAndreas Gohr     * Test that clear removes all index files
142ede46466SAndreas Gohr     */
143ede46466SAndreas Gohr    public function testClear()
144ede46466SAndreas Gohr    {
145ede46466SAndreas Gohr        global $conf;
146ede46466SAndreas Gohr        $indexer = new Indexer();
147ede46466SAndreas Gohr
148ede46466SAndreas Gohr        saveWikiText('clearpage', 'Some words to index.', 'Test initialization');
149ede46466SAndreas Gohr        $indexer->addPage('clearpage');
150ede46466SAndreas Gohr
151ede46466SAndreas Gohr        $this->assertFileExists($conf['indexdir'] . '/page.idx');
152ede46466SAndreas Gohr
153ede46466SAndreas Gohr        $indexer->clear();
154ede46466SAndreas Gohr
155ede46466SAndreas Gohr        $this->assertFileDoesNotExist($conf['indexdir'] . '/page.idx');
156ede46466SAndreas Gohr    }
157ede46466SAndreas Gohr
158ede46466SAndreas Gohr    /**
159ede46466SAndreas Gohr     * Test that getVersion returns a version string
160ede46466SAndreas Gohr     */
161ede46466SAndreas Gohr    public function testGetVersion()
162ede46466SAndreas Gohr    {
163ede46466SAndreas Gohr        $indexer = new Indexer();
164ede46466SAndreas Gohr        $version = $indexer->getVersion();
165ede46466SAndreas Gohr        $this->assertNotEmpty($version);
166ede46466SAndreas Gohr        $this->assertIsString((string)$version);
167ede46466SAndreas Gohr    }
168ede46466SAndreas Gohr
169ede46466SAndreas Gohr    /**
170ede46466SAndreas Gohr     * Test needsIndexing returns true for new pages
171ede46466SAndreas Gohr     */
172ede46466SAndreas Gohr    public function testNeedsIndexing()
173ede46466SAndreas Gohr    {
174ede46466SAndreas Gohr        $indexer = new Indexer();
175ede46466SAndreas Gohr
176ede46466SAndreas Gohr        saveWikiText('needsidx', 'Some content.', 'Test initialization');
177ede46466SAndreas Gohr        $this->assertTrue($indexer->needsIndexing('needsidx'));
178ede46466SAndreas Gohr
179ede46466SAndreas Gohr        $indexer->addPage('needsidx');
180ede46466SAndreas Gohr        // ensure the .indexed tag is strictly newer than the wiki file
181ede46466SAndreas Gohr        touch(metaFN('needsidx', '.indexed'), time() + 1);
182ede46466SAndreas Gohr        $this->assertFalse($indexer->needsIndexing('needsidx'));
183ede46466SAndreas Gohr        $this->assertTrue($indexer->needsIndexing('needsidx', true)); // force
184ede46466SAndreas Gohr    }
185ede46466SAndreas Gohr
186ede46466SAndreas Gohr    /**
187ede46466SAndreas Gohr     * Test the logger callback
188ede46466SAndreas Gohr     */
189ede46466SAndreas Gohr    public function testLogger()
190ede46466SAndreas Gohr    {
191ede46466SAndreas Gohr        $messages = [];
192ede46466SAndreas Gohr        $indexer = (new Indexer())->setLogger(function ($msg) use (&$messages) {
193ede46466SAndreas Gohr            $messages[] = $msg;
194ede46466SAndreas Gohr        });
195ede46466SAndreas Gohr
196ede46466SAndreas Gohr        saveWikiText('logpage', 'Log test content.', 'Test initialization');
197ede46466SAndreas Gohr        $indexer->addPage('logpage');
198ede46466SAndreas Gohr
199ede46466SAndreas Gohr        // ensure the .indexed tag is strictly newer so second call detects "up to date"
200ede46466SAndreas Gohr        touch(metaFN('logpage', '.indexed'), time() + 1);
201ede46466SAndreas Gohr
202ede46466SAndreas Gohr        $indexer->addPage('logpage');
203ede46466SAndreas Gohr        $this->assertNotEmpty($messages);
204ede46466SAndreas Gohr        $this->assertStringContainsString('up to date', end($messages));
205ede46466SAndreas Gohr    }
206ede46466SAndreas Gohr}
207