1 /*
2     This file is part of the KDE Baloo project.
3     SPDX-FileCopyrightText: 2015 Vishesh Handa <vhanda@kde.org>
4 
5     SPDX-License-Identifier: LGPL-2.1-or-later
6 */
7 
8 #include "writetransaction.h"
9 #include "dbstate.h"
10 #include "database.h"
11 #include "idutils.h"
12 
13 #include <QTest>
14 #include <QTemporaryDir>
15 
16 using namespace Baloo;
17 
18 class WriteTransactionTest : public QObject
19 {
20     Q_OBJECT
21 private Q_SLOTS:
init()22     void init() {
23         dir = new QTemporaryDir();
24         db = new Database(dir->path());
25         db->open(Database::CreateDatabase);
26     }
27 
cleanup()28     void cleanup() {
29         delete db;
30         delete dir;
31     }
32 
33     void testAddDocument();
34     void testAddDocumentTwoDocuments();
35     void testAddAndRemoveOneDocument();
36     void testAddAndReplaceOneDocument();
37     void testIdempotentDocumentChange();
38 
39     void testRemoveRecursively();
40     void testDocumentId();
41     void testTermPositions();
42 private:
43     QTemporaryDir* dir;
44     Database* db;
45 };
46 
touchFile(const QString & path)47 static void touchFile(const QString& path) {
48     QFile file(path);
49     file.open(QIODevice::WriteOnly);
50     file.write("data");
51     file.close();
52 }
53 
testAddDocument()54 void WriteTransactionTest::testAddDocument()
55 {
56     Transaction tr(db, Transaction::ReadWrite);
57 
58     const QString filePath(dir->path() + QStringLiteral("/file"));
59     touchFile(filePath);
60     QByteArray url = QFile::encodeName(filePath);
61     quint64 id = filePathToId(url);
62 
63     QCOMPARE(tr.hasDocument(id), false);
64 
65     Document doc;
66     doc.setId(id);
67     doc.setUrl(url);
68     doc.addTerm("a");
69     doc.addTerm("ab");
70     doc.addTerm("abc");
71     doc.addTerm("power");
72     doc.addFileNameTerm("link");
73     doc.addXattrTerm("system");
74     doc.setMTime(1);
75     doc.setCTime(2);
76 
77     tr.addDocument(doc);
78     tr.commit();
79 
80     Transaction tr2(db, Transaction::ReadOnly);
81 
82     DBState state;
83     state.postingDb = {{"a", {id}}, {"ab", {id}}, {"abc", {id}}, {"power", {id}}, {"system", {id}}, {"link", {id}}};
84     state.positionDb = {};
85     state.docTermsDb = {{id, {"a", "ab", "abc", "power"} }};
86     state.docFileNameTermsDb = {{id, {"link"} }};
87     state.docXAttrTermsDb = {{id, {"system"} }};
88     state.docTimeDb = {{id, DocumentTimeDB::TimeInfo(1, 2)}};
89     state.mtimeDb = {{1, id}};
90 
91     DBState actualState = DBState::fromTransaction(&tr2);
92     QCOMPARE(actualState, state);
93 }
94 
95 
createDocument(const QString & filePath,quint32 mtime,quint32 ctime,const QVector<QByteArray> & terms,const QVector<QByteArray> & fileNameTerms,const QVector<QByteArray> & xattrTerms)96 static Document createDocument(const QString& filePath, quint32 mtime, quint32 ctime, const QVector<QByteArray>& terms,
97                                const QVector<QByteArray>& fileNameTerms, const QVector<QByteArray>& xattrTerms)
98 {
99     Document doc;
100 
101     const QByteArray url = QFile::encodeName(filePath);
102     doc.setId(filePathToId(url));
103     doc.setUrl(url);
104 
105     for (const QByteArray& term: terms) {
106         doc.addTerm(term);
107     }
108     for (const QByteArray& term: fileNameTerms) {
109         doc.addFileNameTerm(term);
110     }
111     for (const QByteArray& term: xattrTerms) {
112         doc.addXattrTerm(term);
113     }
114     doc.setMTime(mtime);
115     doc.setCTime(ctime);
116 
117     return doc;
118 }
119 
testAddDocumentTwoDocuments()120 void WriteTransactionTest::testAddDocumentTwoDocuments()
121 {
122     const QString url1(dir->path() + QStringLiteral("/file1"));
123     const QString url2(dir->path() + QStringLiteral("/file2"));
124     touchFile(url1);
125     touchFile(url2);
126 
127     Document doc1 = createDocument(url1, 5, 1, {"a", "abc", "dab"}, {"file1"}, {});
128     Document doc2 = createDocument(url2, 6, 2, {"a", "abcd", "dab"}, {"file2"}, {});
129 
130     {
131         Transaction tr(db, Transaction::ReadWrite);
132         tr.addDocument(doc1);
133         tr.addDocument(doc2);
134         tr.commit();
135     }
136 
137     Transaction tr(db, Transaction::ReadOnly);
138 
139     quint64 id1 = doc1.id();
140     quint64 id2 = doc2.id();
141 
142     DBState state;
143     state.postingDb = {{"a", {id1, id2}}, {"abc", {id1}}, {"abcd", {id2}}, {"dab", {id1, id2}}, {"file1", {id1}}, {"file2", {id2}}};
144     state.positionDb = {};
145     state.docTermsDb = {{id1, {"a", "abc", "dab"}}, {id2, {"a", "abcd", "dab"}}};
146     state.docFileNameTermsDb = {{id1, {"file1"}}, {id2, {"file2"}}};
147     state.docXAttrTermsDb = {};
148     state.docTimeDb = {{id1, DocumentTimeDB::TimeInfo(5, 1)}, {id2, DocumentTimeDB::TimeInfo(6, 2)}};
149     state.mtimeDb = {{5, id1}, {6, id2}};
150 
151     DBState actualState = DBState::fromTransaction(&tr);
152     QVERIFY(DBState::debugCompare(actualState, state));
153 }
154 
testAddAndRemoveOneDocument()155 void WriteTransactionTest::testAddAndRemoveOneDocument()
156 {
157     const QString url1(dir->path() + QStringLiteral("/file1"));
158     touchFile(url1);
159 
160     Document doc1 = createDocument(url1, 5, 1, {"a", "abc", "dab"}, {"file1"}, {});
161 
162     {
163         Transaction tr(db, Transaction::ReadWrite);
164         tr.addDocument(doc1);
165         tr.commit();
166     }
167     {
168         Transaction tr(db, Transaction::ReadWrite);
169         tr.removeDocument(doc1.id());
170         tr.commit();
171     }
172 
173     Transaction tr(db, Transaction::ReadOnly);
174     DBState actualState = DBState::fromTransaction(&tr);
175     QVERIFY(DBState::debugCompare(actualState, DBState()));
176 }
177 
testAddAndReplaceOneDocument()178 void WriteTransactionTest::testAddAndReplaceOneDocument()
179 {
180     const QString url1(dir->path() + QStringLiteral("/file1"));
181     touchFile(url1);
182 
183     Document doc1 = createDocument(url1, 5, 1, {"a", "abc", "dab"}, {"file1"}, {});
184     Document doc2 = createDocument(url1, 6, 2, {"a", "abc", "xxx"}, {"file1", "yyy"}, {});
185     quint64 id = doc1.id();
186     doc2.setId(id);
187 
188     {
189         Transaction tr(db, Transaction::ReadWrite);
190         tr.addDocument(doc1);
191         tr.commit();
192     }
193 
194     DBState state;
195     state.postingDb = {{"a", {id}}, {"abc", {id}}, {"dab", {id}}, {"file1", {id}} };
196     state.positionDb = {};
197     state.docTermsDb = {{id, {"a", "abc", "dab"} }};
198     state.docFileNameTermsDb = {{id, {"file1"} }};
199     state.docXAttrTermsDb = {};
200     state.docTimeDb = {{id, DocumentTimeDB::TimeInfo(5, 1)}};
201     state.mtimeDb = {{5, id}};
202 
203     {
204         Transaction tr(db, Transaction::ReadOnly);
205         DBState actualState = DBState::fromTransaction(&tr);
206         QVERIFY(DBState::debugCompare(actualState, state));
207     }
208 
209     {
210         Transaction tr(db, Transaction::ReadWrite);
211         tr.replaceDocument(doc2, DocumentOperation::Everything);
212         tr.commit();
213     }
214 
215     state.postingDb = {{"a", {id}}, {"abc", {id}}, {"xxx", {id}}, {"file1", {id}}, {"yyy", {id}} };
216     state.docTermsDb = {{id, {"a", "abc", "xxx"} }};
217     state.docFileNameTermsDb = {{id, {"file1", "yyy"} }};
218     state.docTimeDb = {{id, DocumentTimeDB::TimeInfo(6, 2)}};
219     state.mtimeDb = {{6, id}};
220 
221     Transaction tr(db, Transaction::ReadOnly);
222     DBState actualState = DBState::fromTransaction(&tr);
223     QVERIFY(DBState::debugCompare(actualState, state));
224 }
225 
testRemoveRecursively()226 void WriteTransactionTest::testRemoveRecursively()
227 {
228     const QString path = dir->path();
229     const QString url1(path + QStringLiteral("/file1"));
230     const QString dirPath(path + QStringLiteral("/dir"));
231     const QString url2(dirPath + QStringLiteral("/file1"));
232 
233     touchFile(url1);
234     QDir().mkpath(dirPath);
235     touchFile(url2);
236 
237     Document doc1 = createDocument(url1, 5, 1, {"a", "abc", "dab"}, {"file1"}, {});
238     Document doc2 = createDocument(url2, 5, 1, {"a", "abc", "dab"}, {"file1"}, {});
239     Document doc3 = createDocument(dirPath, 5, 1, {"a"}, {"dir"}, {});
240 
241     {
242         Transaction tr(db, Transaction::ReadWrite);
243         tr.addDocument(doc1);
244         tr.addDocument(doc2);
245         tr.addDocument(doc3);
246         tr.commit();
247     }
248     {
249         Transaction tr(db, Transaction::ReadWrite);
250         tr.removeRecursively(filePathToId(dir->path().toUtf8()));
251         tr.commit();
252     }
253 
254     Transaction tr(db, Transaction::ReadOnly);
255     DBState actualState = DBState::fromTransaction(&tr);
256     QVERIFY(DBState::debugCompare(actualState, DBState()));
257 }
258 
testDocumentId()259 void WriteTransactionTest::testDocumentId()
260 {
261     const QString url1(dir->path() + QStringLiteral("/file1"));
262     touchFile(url1);
263 
264     Document doc1 = createDocument(url1, 5, 1, {"a", "abc", "dab"}, {"file1"}, {});
265 
266     {
267         Transaction tr(db, Transaction::ReadWrite);
268         tr.addDocument(doc1);
269         tr.commit();
270     }
271 
272     Transaction tr(db, Transaction::ReadOnly);
273     QCOMPARE(tr.documentId(QFile::encodeName(url1)), doc1.id());
274 }
275 
testTermPositions()276 void WriteTransactionTest::testTermPositions()
277 {
278     const QString url1(dir->path() + QStringLiteral("/file1"));
279     const QString url2(dir->path() + QStringLiteral("/file2"));
280     const QString url3(dir->path() + QStringLiteral("/file3"));
281     touchFile(url1);
282     touchFile(url2);
283     touchFile(url3);
284 
285     Document doc1 = createDocument(url1, 5, 1, {"a", "abc", "dab"}, {"file1"}, {});
286     quint64 id1 = doc1.id();
287     Document doc2 = createDocument(url2, 5, 2, {"a", "abcd"}, {"file2"}, {});
288     quint64 id2 = doc2.id();
289     Document doc3 = createDocument(url3, 6, 3, {"dab"}, {"file3"}, {});
290     quint64 id3 = doc3.id();
291 
292     {
293         Transaction tr(db, Transaction::ReadWrite);
294         tr.addDocument(doc1);
295         tr.addDocument(doc2);
296         tr.addDocument(doc3);
297         tr.commit();
298     }
299 
300     DBState state;
301     state.postingDb = {{"a", {id1, id2}}, {"abc", {id1}}, {"abcd", {id2}}, {"dab", {id1, id3}}, {"file1", {id1}}, {"file2", {id2}}, {"file3", {id3}} };
302     state.positionDb = {};
303     state.docTermsDb = {{id1, {"a", "abc", "dab"}}, {id2, {"a", "abcd"}}, {id3, {"dab"}} };
304     state.docFileNameTermsDb = {{id1, {"file1"}}, {id2, {"file2"}}, {id3, {"file3"}} };
305     state.docXAttrTermsDb = {};
306     state.docTimeDb = {{id1, DocumentTimeDB::TimeInfo(5, 1)}, {id2, DocumentTimeDB::TimeInfo(5, 2)}, {id3, DocumentTimeDB::TimeInfo(6, 3)} };
307     state.mtimeDb = {{5, id1}, {5, id2}, {6, id3} };
308 
309     {
310         Transaction tr(db, Transaction::ReadOnly);
311         DBState actualState = DBState::fromTransaction(&tr);
312         QVERIFY(DBState::debugCompare(actualState, state));
313     }
314 
315     Document doc1_clone = doc1; // save state for later reset
316 
317     for (auto pos : {1, 3, 6}) {
318         doc1.addPositionTerm("a", pos);
319     }
320     for (auto pos : {2, 4, 5}) {
321         doc1.addPositionTerm("abc", pos);
322     }
323     for (auto pos : {12, 14, 15}) {
324         doc1.addPositionTerm("dab", pos);
325     }
326     for (auto pos : {11, 12}) {
327         doc3.addPositionTerm("dab", pos);
328     }
329     state.positionDb["a"] = {PositionInfo(id1, {1, 3, 6})};
330     state.positionDb["abc"] = {PositionInfo(id1, {2, 4, 5})};
331     if (id1 < id3) {
332         state.positionDb["dab"] = {PositionInfo(id1, {12, 14, 15}), PositionInfo(id3, {11, 12})};
333     } else {
334         state.positionDb["dab"] = {PositionInfo(id3, {11, 12}), PositionInfo(id1, {12, 14, 15})};
335     }
336 
337     {
338         Transaction tr(db, Transaction::ReadWrite);
339         tr.replaceDocument(doc1, DocumentOperation::Everything);
340         tr.replaceDocument(doc3, DocumentOperation::Everything);
341         tr.commit();
342     }
343     {
344         Transaction tr(db, Transaction::ReadOnly);
345         DBState actualState = DBState::fromTransaction(&tr);
346         QVERIFY(DBState::debugCompare(actualState, state));
347     }
348 
349     for (auto pos : {11, 12}) { // extend
350         doc1.addPositionTerm("abc", pos);
351     }
352     for (auto pos : {16, 17}) { // extend, make sure positions for doc3 are kept
353         doc1.addPositionTerm("dab", pos);
354     }
355     for (auto pos : {7, 8, 9}) { // add positions
356         doc2.addPositionTerm("a", pos);
357     }
358     for (auto pos : {7, 8, 9}) { // add new term with positions
359         doc2.addPositionTerm("abc", pos);
360     }
361 
362     Document doc2_clone = doc2; // save state for later reset
363     doc2.addPositionTerm("abcd", 500); // add position for existing term
364 
365     state.postingDb = {{"a", {id1, id2}}, {"abc", {id1, id2}}, {"abcd", {id2}}, {"dab", {id1, id3}}, {"file1", {id1}}, {"file2", {id2}}, {"file3", {id3}} };
366     state.docTermsDb = {{id1, {"a", "abc", "dab"}}, {id2, {"a", "abc", "abcd"}}, {id3, {"dab"}} };
367     if (id1 < id2) {
368         state.positionDb["a"] = {PositionInfo(id1, {1, 3, 6}), PositionInfo(id2, {7, 8, 9})};
369         state.positionDb["abc"] = {PositionInfo(id1, {2, 4, 5, 11, 12}), PositionInfo(id2, {7, 8, 9})};
370     } else {
371         state.positionDb["a"] = {PositionInfo(id2, {7, 8, 9}), PositionInfo(id1, {1, 3, 6})};
372         state.positionDb["abc"] = {PositionInfo(id2, {7, 8, 9}), PositionInfo(id1, {2, 4, 5, 11, 12})};
373     }
374     if (id1 < id3) {
375         state.positionDb["dab"] = {PositionInfo(id1, {12, 14, 15, 16, 17}), PositionInfo(id3, {11, 12})};
376     } else {
377         state.positionDb["dab"] = {PositionInfo(id3, {11, 12}), PositionInfo(id1, {12, 14, 15, 16, 17})};
378     }
379     state.positionDb["abcd"] = {PositionInfo(id2, {500})};
380 
381     {
382         Transaction tr(db, Transaction::ReadWrite);
383         tr.replaceDocument(doc1, DocumentOperation::Everything);
384         tr.replaceDocument(doc2, DocumentOperation::Everything);
385         tr.commit();
386     }
387     {
388         Transaction tr(db, Transaction::ReadOnly);
389         DBState actualState = DBState::fromTransaction(&tr);
390         QVERIFY(DBState::debugCompare(actualState, state));
391     }
392 
393     // Reset some positions of doc1
394     for (auto pos : {2, 4, 5, 11, 12}) { // keep "abc"
395         doc1_clone.addPositionTerm("abc", pos);
396     }
397     for (auto pos : {12, 14, 17}) { // remove 15, 16 from dab
398         doc1_clone.addPositionTerm("dab", pos);
399     }
400     state.positionDb["a"] = {PositionInfo(id2, {7, 8, 9})}; // doc1 removed
401     state.positionDb.remove("abcd"); // positions for abcd removed
402     if (id1 < id3) {
403         state.positionDb["dab"] = {PositionInfo(id1, {12, 14, 17}), PositionInfo(id3, {11, 12})};
404     } else {
405         state.positionDb["dab"] = {PositionInfo(id3, {11, 12}), PositionInfo(id1, {12, 14, 17})};
406     }
407 
408     {
409         Transaction tr(db, Transaction::ReadWrite);
410         tr.replaceDocument(doc1_clone, DocumentOperation::Everything);
411         tr.replaceDocument(doc2_clone, DocumentOperation::Everything);
412         tr.commit();
413     }
414     {
415         Transaction tr(db, Transaction::ReadOnly);
416         DBState actualState = DBState::fromTransaction(&tr);
417         QVERIFY(DBState::debugCompare(actualState, state));
418     }
419 }
420 
testIdempotentDocumentChange()421 void WriteTransactionTest::testIdempotentDocumentChange()
422 {
423     const QString url1(dir->path() + QStringLiteral("/file1"));
424     touchFile(url1);
425 
426     Document doc1 = createDocument(url1, 5, 1, {"a", "abc", "dab"}, {"file1"}, {});
427     Document doc2 = createDocument(url1, 5, 1, {"a", "abcd", "dab"}, {"file1"}, {});
428     quint64 id = doc1.id();
429 
430     {
431         Transaction tr(db, Transaction::ReadWrite);
432         tr.addDocument(doc1);
433         tr.commit();
434     }
435 
436     DBState state;
437     state.postingDb = {{"a", {id}}, {"abc", {id}}, {"dab", {id}}, {"file1", {id}} };
438     state.positionDb = {};
439     state.docTermsDb = {{id, {"a", "abc", "dab"} }};
440     state.docFileNameTermsDb = {{id, {"file1"} }};
441     state.docXAttrTermsDb = {};
442     state.docTimeDb = {{id, DocumentTimeDB::TimeInfo(5, 1)}};
443     state.mtimeDb = {{5, id}};
444 
445     {
446         Transaction tr(db, Transaction::ReadOnly);
447         DBState actualState = DBState::fromTransaction(&tr);
448         QVERIFY(DBState::debugCompare(actualState, state));
449     }
450 
451     {
452         Transaction tr(db, Transaction::ReadWrite);
453         tr.replaceDocument(doc2, DocumentOperation::Everything);
454         tr.replaceDocument(doc2, DocumentOperation::Everything);
455         tr.commit();
456     }
457 
458     state.postingDb = {{"a", {id}}, {"abcd", {id}}, {"dab", {id}}, {"file1", {id}} };
459     state.docTermsDb = {{id, {"a", "abcd", "dab"} }};
460 
461     {
462         Transaction tr(db, Transaction::ReadOnly);
463         DBState actualState = DBState::fromTransaction(&tr);
464         QVERIFY(DBState::debugCompare(actualState, state));
465     }
466 
467 }
468 
469 QTEST_MAIN(WriteTransactionTest)
470 
471 #include "writetransactiontest.moc"
472