File indexing completed on 2024-05-05 03:52:08

0001 /*
0002     This file is part of the KDE Baloo Project
0003     SPDX-FileCopyrightText: 2014 Vishesh Handa <vhanda@kde.org>
0004 
0005     SPDX-License-Identifier: LGPL-2.1-or-later
0006 */
0007 
0008 #include "advancedqueryparser.h"
0009 
0010 #include <QTest>
0011 
0012 Q_DECLARE_METATYPE(Baloo::Term)
0013 
0014 using Term = Baloo::Term;
0015 using AdvancedQueryParser = Baloo::AdvancedQueryParser;
0016 
0017 class AdvancedQueryParserTest : public QObject
0018 {
0019     Q_OBJECT
0020 private Q_SLOTS:
0021 
0022     void testSimpleProperty();
0023     void testSimpleString();
0024     void testStringAndProperty();
0025     void testLogicalOps();
0026     void testNesting();
0027     void testDateTime();
0028     void testOperators();
0029     void testBinaryOperatorMissingFirstArg();
0030     void testNestedParentheses();
0031     void testNestedParentheses_data();
0032     void testOptimizedLogic();
0033     void testOptimizedLogic_data();
0034     void testPhrases();
0035     void testPhrases_data();
0036     void testIncompleteTokens();
0037     void testIncompleteTokens_data();
0038     void testQuoting();
0039     void testQuoting_data();
0040     void testSpecialCharacters();
0041     void testSpecialCharacters_data();
0042 };
0043 
0044 void AdvancedQueryParserTest::testSimpleProperty()
0045 {
0046     AdvancedQueryParser parser;
0047     Term term = parser.parse(QStringLiteral("artist:Coldplay"));
0048     Term expectedTerm(QStringLiteral("artist"), QStringLiteral("Coldplay"));
0049 
0050     QCOMPARE(term, expectedTerm);
0051 }
0052 
0053 void AdvancedQueryParserTest::testSimpleString()
0054 {
0055     AdvancedQueryParser parser;
0056     Term term = parser.parse(QStringLiteral("Coldplay"));
0057     Term expectedTerm(QString(), QStringLiteral("Coldplay"));
0058 
0059     QCOMPARE(term, expectedTerm);
0060 }
0061 
0062 void AdvancedQueryParserTest::testStringAndProperty()
0063 {
0064     AdvancedQueryParser parser;
0065     Term term = parser.parse(QStringLiteral("stars artist:Coldplay fire"));
0066     Term expectedTerm(Term::And);
0067 
0068     expectedTerm.addSubTerm(Term(QString(), QStringLiteral("stars")));
0069     expectedTerm.addSubTerm(Term(QStringLiteral("artist"), QStringLiteral("Coldplay")));
0070     expectedTerm.addSubTerm(Term(QString(), QStringLiteral("fire")));
0071 
0072     QCOMPARE(term, expectedTerm);
0073 }
0074 
0075 void AdvancedQueryParserTest::testLogicalOps()
0076 {
0077     // AND
0078     AdvancedQueryParser parser;
0079     Term term = parser.parse(QStringLiteral("artist:Coldplay AND type:song"));
0080     Term expectedTerm(Term::And);
0081 
0082     expectedTerm.addSubTerm(Term(QStringLiteral("artist"), QStringLiteral("Coldplay")));
0083     expectedTerm.addSubTerm(Term(QStringLiteral("type"), QStringLiteral("song")));
0084 
0085     QCOMPARE(term, expectedTerm);
0086 
0087     // OR
0088     term = parser.parse(QStringLiteral("artist:Coldplay OR type:song"));
0089     expectedTerm = Term(Term::Or);
0090 
0091     expectedTerm.addSubTerm(Term(QStringLiteral("artist"), QStringLiteral("Coldplay")));
0092     expectedTerm.addSubTerm(Term(QStringLiteral("type"), QStringLiteral("song")));
0093 
0094     QCOMPARE(term, expectedTerm);
0095 
0096     // AND then OR
0097     term = parser.parse(QStringLiteral("artist:Coldplay AND type:song OR stars"));
0098     expectedTerm = Term(Term::Or);
0099 
0100     expectedTerm.addSubTerm(Term(QStringLiteral("artist"), QStringLiteral("Coldplay")) && Term(QStringLiteral("type"), QStringLiteral("song")));
0101     expectedTerm.addSubTerm(Term(QString(), QStringLiteral("stars")));
0102 
0103     QCOMPARE(term, expectedTerm);
0104 
0105     // OR then AND
0106     term = parser.parse(QStringLiteral("artist:Coldplay OR type:song AND stars"));
0107     expectedTerm = Term(Term::And);
0108 
0109     expectedTerm.addSubTerm(Term(QStringLiteral("artist"), QStringLiteral("Coldplay")) || Term(QStringLiteral("type"), QStringLiteral("song")));
0110     expectedTerm.addSubTerm(Term(QString(), QStringLiteral("stars")));
0111 
0112     QCOMPARE(term, expectedTerm);
0113 
0114     // Multiple ANDs
0115     term = parser.parse(QStringLiteral("artist:Coldplay AND type:song AND stars"));
0116     expectedTerm = Term(Term::And);
0117 
0118     expectedTerm.addSubTerm(Term(QStringLiteral("artist"), QStringLiteral("Coldplay")));
0119     expectedTerm.addSubTerm(Term(QStringLiteral("type"), QStringLiteral("song")));
0120     expectedTerm.addSubTerm(Term(QString(), QStringLiteral("stars")));
0121 
0122     QCOMPARE(term, expectedTerm);
0123 
0124     // Multiple ORs
0125     term = parser.parse(QStringLiteral("artist:Coldplay OR type:song OR stars"));
0126     expectedTerm = Term(Term::Or);
0127 
0128     expectedTerm.addSubTerm(Term(QStringLiteral("artist"), QStringLiteral("Coldplay")));
0129     expectedTerm.addSubTerm(Term(QStringLiteral("type"), QStringLiteral("song")));
0130     expectedTerm.addSubTerm(Term(QString(), QStringLiteral("stars")));
0131 
0132     QCOMPARE(term, expectedTerm);
0133 }
0134 
0135 void AdvancedQueryParserTest::testNesting()
0136 {
0137     AdvancedQueryParser parser;
0138     Term term = parser.parse(QStringLiteral("artist:Coldplay AND (type:song OR stars) fire"));
0139     Term expectedTerm(Term::And);
0140 
0141     expectedTerm.addSubTerm(Term(QStringLiteral("artist"), QStringLiteral("Coldplay")));
0142     expectedTerm.addSubTerm(Term(QStringLiteral("type"), QStringLiteral("song")) || Term(QString(), QStringLiteral("stars")));
0143     expectedTerm.addSubTerm(Term(QString(), QStringLiteral("fire")));
0144 
0145     QCOMPARE(term, expectedTerm);
0146 }
0147 
0148 void AdvancedQueryParserTest::testDateTime()
0149 {
0150     // Integers
0151     AdvancedQueryParser parser;
0152     Term term;
0153     Term expectedTerm;
0154 
0155     term = parser.parse(QStringLiteral("modified:2014-12-02"));
0156     expectedTerm = Term(QStringLiteral("modified"), QStringLiteral("2014-12-02"));
0157     QCOMPARE(term, expectedTerm);
0158 
0159     term = parser.parse(QStringLiteral("modified:\"2014-12-02T23:22:1\""));
0160     expectedTerm = Term(QStringLiteral("modified"), QStringLiteral("2014-12-02T23:22:1"));
0161     QCOMPARE(term, expectedTerm);
0162 }
0163 
0164 void AdvancedQueryParserTest::testOperators()
0165 {
0166     AdvancedQueryParser parser;
0167     Term term;
0168     Term expectedTerm;
0169 
0170     term = parser.parse(QStringLiteral("width:500"));
0171     expectedTerm = Term(QStringLiteral("width"), QStringLiteral("500"), Term::Contains);
0172     QCOMPARE(term, expectedTerm);
0173 
0174     term = parser.parse(QStringLiteral("width=500"));
0175     expectedTerm = Term(QStringLiteral("width"), QStringLiteral("500"), Term::Equal);
0176     QCOMPARE(term, expectedTerm);
0177 
0178     term = parser.parse(QStringLiteral("width<500"));
0179     expectedTerm = Term(QStringLiteral("width"), QStringLiteral("500"), Term::Less);
0180     QCOMPARE(term, expectedTerm);
0181 
0182     term = parser.parse(QStringLiteral("width<=500"));
0183     expectedTerm = Term(QStringLiteral("width"), QStringLiteral("500"), Term::LessEqual);
0184     QCOMPARE(term, expectedTerm);
0185 
0186     term = parser.parse(QStringLiteral("width>500"));
0187     expectedTerm = Term(QStringLiteral("width"), QStringLiteral("500"), Term::Greater);
0188     QCOMPARE(term, expectedTerm);
0189 
0190     term = parser.parse(QStringLiteral("width>=500"));
0191     expectedTerm = Term(QStringLiteral("width"), QStringLiteral("500"), Term::GreaterEqual);
0192     QCOMPARE(term, expectedTerm);
0193 }
0194 
0195 void AdvancedQueryParserTest::testBinaryOperatorMissingFirstArg()
0196 {
0197     AdvancedQueryParser parser;
0198     Term term = parser.parse(QStringLiteral("=:2"));
0199     Term expectedTerm;
0200     QCOMPARE(term, expectedTerm);
0201 }
0202 
0203 void AdvancedQueryParserTest::testNestedParentheses()
0204 {
0205     QFETCH(QString, searchInput);
0206     QFETCH(QString, failmessage);
0207     QFETCH(Term, expectedTerm);
0208 
0209     AdvancedQueryParser parser;
0210     const auto testTerm = parser.parse(searchInput);
0211     qDebug() << "  result term" << testTerm;
0212     qDebug() << "expected term" << expectedTerm;
0213     if (!failmessage.isEmpty()) {
0214         QEXPECT_FAIL("", qPrintable(failmessage), Continue);
0215     }
0216 
0217     QCOMPARE(testTerm, expectedTerm);
0218 }
0219 
0220 void AdvancedQueryParserTest::testNestedParentheses_data()
0221 {
0222     QTest::addColumn<QString>("searchInput");
0223     QTest::addColumn<Term>("expectedTerm");
0224     QTest::addColumn<QString>("failmessage");
0225 
0226     QTest::newRow("a AND b AND c AND d")
0227         << QStringLiteral("a AND b AND c AND d")
0228         << Term{Term::And, QList<Term>{
0229             Term{QString(), QStringLiteral("a"), Term::Contains},
0230             Term{QString(), QStringLiteral("b"), Term::Contains},
0231             Term{QString(), QStringLiteral("c"), Term::Contains},
0232             Term{QString(), QStringLiteral("d"), Term::Contains},
0233         }}
0234         << QString()
0235         ;
0236     QTest::newRow("(a AND b) AND (c OR d)")
0237         << QStringLiteral("(a AND b) AND (c OR d)")
0238         << Term{Term::And, QList<Term>{
0239             Term{QString(), QStringLiteral("a"), Term::Contains},
0240             Term{QString(), QStringLiteral("b"), Term::Contains},
0241             Term{Term::Or, QList<Term>{
0242                 Term{QString(), QStringLiteral("c"), Term::Contains},
0243                 Term{QString(), QStringLiteral("d"), Term::Contains},
0244             }}
0245         }}
0246         << QString()
0247         ;
0248     QTest::newRow("(a AND (b AND (c AND d)))")
0249         << QStringLiteral("(a AND (b AND (c AND d)))")
0250         << Term{Term::And, QList<Term>{
0251             Term{QString(), QStringLiteral("a"), Term::Contains},
0252             Term{QString(), QStringLiteral("b"), Term::Contains},
0253             Term{QString(), QStringLiteral("c"), Term::Contains},
0254             Term{QString(), QStringLiteral("d"), Term::Contains},
0255         }}
0256         << QString()
0257         ;
0258     // Test 1 for BUG: 392620
0259     QTest::newRow("a OR ((b AND c) AND d)")
0260         << QStringLiteral("a OR ((b AND c) AND d)")
0261         << Term{Term::Or, QList<Term>{
0262                 Term{QString(), QStringLiteral("a"), Term::Contains},
0263                 Term{Term::And, QList<Term>{
0264                     Term{QString(), QStringLiteral("b"), Term::Contains},
0265                     Term{QString(), QStringLiteral("c"), Term::Contains},
0266                     Term{QString(), QStringLiteral("d"), Term::Contains}
0267                 }}
0268             }}
0269         << QString()
0270         ;
0271     // Test 2 for BUG: 392620
0272     QTest::newRow("a AND ((b OR c) OR d)")
0273         << QStringLiteral("a AND ((b OR c) OR d)")
0274         << Term{Term::And, QList<Term>{
0275                 Term{QString(), QStringLiteral("a"), Term::Contains},
0276                 Term{Term::Or, QList<Term>{
0277                     Term{QString(), QStringLiteral("b"), Term::Contains},
0278                     Term{QString(), QStringLiteral("c"), Term::Contains},
0279                     Term{QString(), QStringLiteral("d"), Term::Contains}
0280                 }}
0281             }}
0282         << QString();
0283         ;
0284 }
0285 
0286 void AdvancedQueryParserTest::testOptimizedLogic()
0287 {
0288     QFETCH(Term, testTerm);
0289     QFETCH(Term, expectedTerm);
0290     qDebug() << "  result term" << testTerm;
0291     qDebug() << "expected term" << expectedTerm;
0292 
0293     QCOMPARE(testTerm, expectedTerm);
0294 }
0295 
0296 void AdvancedQueryParserTest::testOptimizedLogic_data()
0297 {
0298     QTest::addColumn<Term>("testTerm");
0299     QTest::addColumn<Term>("expectedTerm");
0300 
0301     // a && b && c && d can be combined into one AND term with 4 subterms
0302     QTest::addRow("a && b && c && d")
0303         << (Term{QString(), QStringLiteral("a"), Term::Contains}
0304             && Term{QString(), QStringLiteral("b"), Term::Contains}
0305             && Term{QString(), QStringLiteral("c"), Term::Contains}
0306             && Term{QString(), QStringLiteral("d"), Term::Contains})
0307         << Term{Term::And, QList<Term>{
0308             Term{QString(), QStringLiteral("a"), Term::Contains},
0309             Term{QString(), QStringLiteral("b"), Term::Contains},
0310             Term{QString(), QStringLiteral("c"), Term::Contains},
0311             Term{QString(), QStringLiteral("d"), Term::Contains},
0312         }}
0313     ;
0314 
0315     // (a AND b) AND (c OR d) can be merged as (a AND b AND (c OR D)
0316     QTest::addRow("(a && b) && (c || d)")
0317         << ((Term{QString(), QStringLiteral("a"), Term::Contains}
0318                 && Term{QString(), QStringLiteral("b"), Term::Contains})
0319             && (Term{QString(), QStringLiteral("c"), Term::Contains}
0320                 || Term{QString(), QStringLiteral("d"), Term::Contains}
0321             ))
0322         << Term{Term::And, QList<Term>{
0323             Term{QString(), QStringLiteral("a"), Term::Contains},
0324             Term{QString(), QStringLiteral("b"), Term::Contains},
0325             Term{Term::Or, QList<Term>{
0326                 Term{QString(), QStringLiteral("c"), Term::Contains},
0327                 Term{QString(), QStringLiteral("d"), Term::Contains}
0328             }}
0329         }}
0330     ;
0331 }
0332 
0333 void AdvancedQueryParserTest::testPhrases()
0334 {
0335     QFETCH(QString, input);
0336     QFETCH(Term, expectedTerm);
0337 
0338     AdvancedQueryParser parser;
0339     Term term = parser.parse(input);
0340     QCOMPARE(term, expectedTerm);
0341 }
0342 
0343 void AdvancedQueryParserTest::testPhrases_data()
0344 {
0345     QTest::addColumn<QString>("input");
0346     QTest::addColumn<Term>("expectedTerm");
0347 
0348     auto addRow = [](const QString& input, const Term& term)
0349     { QTest::addRow("%s", qPrintable(input)) << input << term; };
0350 
0351     addRow(QStringLiteral("artist:ColdPlay"),         {QStringLiteral("artist"), QStringLiteral("ColdPlay"), Term::Contains});
0352     addRow(QStringLiteral("artist:\"ColdPlay\""),     {QStringLiteral("artist"), QStringLiteral("ColdPlay"), Term::Contains});
0353     addRow(QStringLiteral("artist:\"Foo Fighters\""), {QStringLiteral("artist"), QStringLiteral("Foo Fighters"), Term::Contains});
0354     addRow(QStringLiteral("artist:\"Foo Fighters\" OR artist:ColdPlay "), {Term::Or, {
0355     {QStringLiteral("artist"), QStringLiteral("Foo Fighters"), Term::Contains},
0356     {QStringLiteral("artist"), QStringLiteral("ColdPlay"), Term::Contains},
0357     }});
0358 }
0359 
0360 void AdvancedQueryParserTest::testIncompleteTokens()
0361 {
0362     QFETCH(QString, input);
0363     QFETCH(Term, expectedTerm);
0364 
0365     AdvancedQueryParser parser;
0366     Term term = parser.parse(input);
0367     QCOMPARE(term, expectedTerm);
0368 }
0369 
0370 void AdvancedQueryParserTest::testIncompleteTokens_data()
0371 {
0372     QTest::addColumn<QString>("input");
0373     QTest::addColumn<Term>("expectedTerm");
0374 
0375     auto addRow = [](const QString& name, const QString& input, const Term& term)
0376     { QTest::addRow("%s", qPrintable(name)) << input << term; };
0377 
0378     addRow(QStringLiteral("ends with quote"),          QStringLiteral("foo \""), {QString(), QStringLiteral("foo"), Term::Auto});
0379     addRow(QStringLiteral("ends with comparator"),     QStringLiteral("foo>"),   {QStringLiteral("foo"), QString(), Term::Contains});
0380     addRow(QStringLiteral("ends with opening parens"), QStringLiteral("foo ("),  {QString(), QStringLiteral("foo")});
0381     addRow(QStringLiteral("ends with closing parens"), QStringLiteral("foo )"),  {QString(), QStringLiteral("foo")});
0382 }
0383 
0384 void AdvancedQueryParserTest::testQuoting()
0385 {
0386     QFETCH(QString, input);
0387     QFETCH(Term, expectedTerm);
0388 
0389     AdvancedQueryParser parser;
0390     Term term = parser.parse(input);
0391     QCOMPARE(term, expectedTerm);
0392 }
0393 
0394 void AdvancedQueryParserTest::testQuoting_data()
0395 {
0396     QTest::addColumn<QString>("input");
0397     QTest::addColumn<Term>("expectedTerm");
0398 
0399     auto addRow = [](const QString& name, const QString& input, const Term& term)
0400     { QTest::addRow("%s", qPrintable(name)) << input << term; };
0401 
0402     addRow(QStringLiteral("empty"),                    QStringLiteral("\"\""),   {QString(), QStringLiteral(""), Term::Auto});
0403     addRow(QStringLiteral("two quoted"),     QStringLiteral("\"foo\"\"bar\""),   {Term::And, {
0404     {QStringLiteral(""), QStringLiteral("foo"), Term::Contains},
0405     {QStringLiteral(""), QStringLiteral("bar"), Term::Contains},
0406     }});
0407     addRow(QStringLiteral("two quoted properties"),     QStringLiteral("artist:\"foo\" OR artist:\"bar\""),   {Term::Or, {
0408     {QStringLiteral("artist"), QStringLiteral("foo"), Term::Contains},
0409     {QStringLiteral("artist"), QStringLiteral("bar"), Term::Contains},
0410     }});
0411     addRow(QStringLiteral("two empty properties"),     QStringLiteral("artist:\"\" AND filename:\"\""),   {Term::And, {
0412     {QStringLiteral("artist"), QStringLiteral(""), Term::Contains},
0413     {QStringLiteral("filename"), QStringLiteral(""), Term::Contains},
0414     }});
0415     addRow(QStringLiteral("quoted and unquoted"), QStringLiteral("one \"two and three\" four"),   {Term::And, {
0416     {QStringLiteral(""), QStringLiteral("one"), Term::Contains},
0417     {QStringLiteral(""), QStringLiteral("two and three"), Term::Contains},
0418     {QStringLiteral(""), QStringLiteral("four"), Term::Contains},
0419     }});
0420     addRow(QStringLiteral("multiple quoted"), QStringLiteral("\"one and two\" \"three and four\""),   {Term::And, {
0421     {QStringLiteral(""), QStringLiteral("one and two"), Term::Contains},
0422     {QStringLiteral(""), QStringLiteral("three and four"), Term::Contains},
0423     }});
0424 }
0425 
0426 void AdvancedQueryParserTest::testSpecialCharacters()
0427 {
0428     QFETCH(QString, input);
0429     QFETCH(Term, expectedTerm);
0430 
0431     AdvancedQueryParser parser;
0432     Term term = parser.parse(input);
0433     QCOMPARE(term, expectedTerm);
0434 }
0435 
0436 void AdvancedQueryParserTest::testSpecialCharacters_data()
0437 {
0438     QTest::addColumn<QString>("input");
0439     QTest::addColumn<Term>("expectedTerm");
0440 
0441     auto addRow = [](const QString& name, const QString& input, const Term& term)
0442     { QTest::addRow("%s", qPrintable(name)) << input << term; };
0443 
0444     addRow(QStringLiteral("mail"), QStringLiteral("foo@bar.com"),
0445         {QStringLiteral(""), QStringLiteral("foo@bar.com"), Term::Contains});
0446     addRow(QStringLiteral("mail and other"), QStringLiteral("one foo@bar.com two"), {Term::And, {
0447         {QStringLiteral(""), QStringLiteral("one"), Term::Contains},
0448         {QStringLiteral(""), QStringLiteral("foo@bar.com"), Term::Contains},
0449         {QStringLiteral(""), QStringLiteral("two"), Term::Contains},
0450     }});
0451     addRow(QStringLiteral("filename"), QStringLiteral("foo_bar.png"),
0452         {QStringLiteral(""), QStringLiteral("foo_bar.png"), Term::Contains});
0453     addRow(QStringLiteral("various phrases"), QStringLiteral("one_two \"foo@bar.com\" two.png"), {Term::And, {
0454         {QStringLiteral(""), QStringLiteral("one_two"), Term::Contains},
0455         {QStringLiteral(""), QStringLiteral("foo@bar.com"), Term::Contains},
0456         {QStringLiteral(""), QStringLiteral("two.png"), Term::Contains},
0457     }});
0458 }
0459 
0460 QTEST_MAIN(AdvancedQueryParserTest)
0461 
0462 #include "advancedqueryparsertest.moc"