File indexing completed on 2024-05-19 16:19:07

0001 /***************************************************************************
0002     Copyright (C) 2009-2016 Robby Stephenson <robby@periapsis.org>
0003  ***************************************************************************/
0004 
0005 /***************************************************************************
0006  *                                                                         *
0007  *   This program is free software; you can redistribute it and/or         *
0008  *   modify it under the terms of the GNU General Public License as        *
0009  *   published by the Free Software Foundation; either version 2 of        *
0010  *   the License or (at your option) version 3 or any later version        *
0011  *   accepted by the membership of KDE e.V. (or its successor approved     *
0012  *   by the membership of KDE e.V.), which shall act as a proxy            *
0013  *   defined in Section 14 of version 3 of the license.                    *
0014  *                                                                         *
0015  *   This program is distributed in the hope that it will be useful,       *
0016  *   but WITHOUT ANY WARRANTY; without even the implied warranty of        *
0017  *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the         *
0018  *   GNU General Public License for more details.                          *
0019  *                                                                         *
0020  *   You should have received a copy of the GNU General Public License     *
0021  *   along with this program.  If not, see <http://www.gnu.org/licenses/>. *
0022  *                                                                         *
0023  ***************************************************************************/
0024 
0025 #include "collectiontest.h"
0026 
0027 #include "../collection.h"
0028 #include "../field.h"
0029 #include "../entry.h"
0030 #include "../collectionfactory.h"
0031 #include "../collections/collectioninitializer.h"
0032 #include "../collections/bookcollection.h"
0033 #include "../collections/gamecollection.h"
0034 #include "../translators/tellicoxmlexporter.h"
0035 #include "../translators/tellicoimporter.h"
0036 #include "../images/imagefactory.h"
0037 #include "../document.h"
0038 #include "../utils/mergeconflictresolver.h"
0039 #include "../entrycomparison.h"
0040 
0041 #include <KProcess>
0042 
0043 #include <QTest>
0044 #include <QStandardPaths>
0045 #include <QRandomGenerator>
0046 
0047 QTEST_GUILESS_MAIN( CollectionTest )
0048 
0049 Q_DECLARE_METATYPE(Tellico::EntryComparison::MatchValue)
0050 
0051 class TestResolver : public Tellico::Merge::ConflictResolver {
0052 public:
0053   TestResolver(Tellico::Merge::ConflictResolver::Result ret) : m_ret(ret) {};
0054   Tellico::Merge::ConflictResolver::Result resolve(Tellico::Data::EntryPtr,
0055                                                  Tellico::Data::EntryPtr,
0056                                                  Tellico::Data::FieldPtr,
0057                                                  const QString& value1 = QString(),
0058                                                  const QString& value2 = QString()) Q_DECL_OVERRIDE {
0059     Q_UNUSED(value1);
0060     Q_UNUSED(value2);
0061     return m_ret;
0062   }
0063 
0064 private:
0065   Tellico::Merge::ConflictResolver::Result m_ret;
0066 };
0067 
0068 void CollectionTest::initTestCase() {
0069   QStandardPaths::setTestModeEnabled(true);
0070   qRegisterMetaType<Tellico::EntryComparison::MatchValue>();
0071   Tellico::ImageFactory::init();
0072   // need to register the collection types
0073   Tellico::CollectionInitializer ci;
0074 
0075   // create the collection and entry used for testMatchScore()
0076   m_coll = Tellico::Data::CollPtr(new Tellico::Data::BookCollection(true));
0077   m_entry = Tellico::Data::EntryPtr(new Tellico::Data::Entry(m_coll));
0078   m_entry->setField(QStringLiteral("title"), QStringLiteral("title1"));
0079   m_entry->setField(QStringLiteral("author"), QStringLiteral("John Doe"));
0080   m_entry->setField(QStringLiteral("isbn"), QStringLiteral("1234367890"));
0081   m_entry->setField(QStringLiteral("lccn"), QStringLiteral("89456"));
0082   Tellico::Data::FieldPtr f(new Tellico::Data::Field(QStringLiteral("arxiv"), QStringLiteral("Arxiv ID")));
0083   m_coll->addField(f);
0084   m_entry->setField(QStringLiteral("arxiv"), QStringLiteral("hep-lat/0110180"));
0085   m_coll->addEntries(m_entry);
0086 }
0087 
0088 void CollectionTest::cleanupTestCase() {
0089   Tellico::ImageFactory::clean(true);
0090 }
0091 
0092 void CollectionTest::testEmpty() {
0093   Tellico::Data::CollPtr nullColl;
0094   QVERIFY(!nullColl);
0095 
0096   Tellico::Data::Collection coll(false, QStringLiteral("Title"));
0097 
0098   QCOMPARE(coll.id(), 2); // ID is 2 since ID of 1 is created in initTestCase()
0099   QCOMPARE(coll.entryCount(), 0);
0100   QCOMPARE(coll.type(), Tellico::Data::Collection::Base);
0101   QVERIFY(coll.fields().isEmpty());
0102   QCOMPARE(coll.title(), QStringLiteral("Title"));
0103 }
0104 
0105 void CollectionTest::testCollection() {
0106   Tellico::Data::CollPtr coll(new Tellico::Data::Collection(true)); // add default fields
0107 
0108   QCOMPARE(coll->entryCount(), 0);
0109   QCOMPARE(coll->type(), Tellico::Data::Collection::Base);
0110   QCOMPARE(coll->fields().count(), 4);
0111   QVERIFY(coll->hasField(QStringLiteral("title")));
0112   QVERIFY(coll->hasField(QStringLiteral("id")));
0113   QVERIFY(coll->hasField(QStringLiteral("cdate")));
0114   QVERIFY(coll->hasField(QStringLiteral("mdate")));
0115   QVERIFY(coll->peopleFields().isEmpty());
0116   QVERIFY(coll->imageFields().isEmpty());
0117   QVERIFY(!coll->hasImages());
0118 
0119   Tellico::Data::EntryPtr entry1(new Tellico::Data::Entry(coll));
0120   coll->addEntries(entry1);
0121 
0122   // check derived value
0123   QCOMPARE(entry1->field(QStringLiteral("id")), QStringLiteral("1"));
0124   // check created and modified values
0125   QCOMPARE(entry1->field(QStringLiteral("cdate")), QDate::currentDate().toString(Qt::ISODate));
0126   QCOMPARE(entry1->field(QStringLiteral("mdate")), QDate::currentDate().toString(Qt::ISODate));
0127 
0128   // also verify that the empty string is included in list of group names
0129   Tellico::Data::FieldPtr field1(new Tellico::Data::Field(QStringLiteral("test"), QStringLiteral("test")));
0130   coll->addField(field1);
0131   QStringList groupNames = entry1->groupNamesByFieldName(QStringLiteral("test"));
0132   QCOMPARE(groupNames.count(), 1);
0133   QVERIFY(groupNames.at(0).isEmpty());
0134 
0135   Tellico::Data::EntryPtr entry2(new Tellico::Data::Entry(coll));
0136   // add created and modified dates from earlier, to make sure they don't get overwritten
0137   QDate weekAgo = QDate::currentDate().addDays(-7);
0138   QDate yesterday = QDate::currentDate().addDays(-1);
0139   entry2->setField(QStringLiteral("cdate"), weekAgo.toString(Qt::ISODate));
0140   entry2->setField(QStringLiteral("mdate"), yesterday.toString(Qt::ISODate));
0141   coll->addEntries(entry2);
0142 
0143   // check derived value
0144   QCOMPARE(entry2->field(QStringLiteral("id")), QStringLiteral("2"));
0145   // check created and modified values
0146   QCOMPARE(entry2->field(QStringLiteral("cdate")), weekAgo.toString(Qt::ISODate));
0147   QCOMPARE(entry2->field(QStringLiteral("mdate")), yesterday.toString(Qt::ISODate));
0148 
0149   // check that mdate gets updates
0150   entry2->setField(QStringLiteral("title"), QStringLiteral("new title"));
0151   QCOMPARE(entry2->field(QStringLiteral("cdate")), weekAgo.toString(Qt::ISODate));
0152   QCOMPARE(entry2->field(QStringLiteral("mdate")), QDate::currentDate().toString(Qt::ISODate));
0153 
0154   // check Bug 361622 - properly handling empty rows in table
0155   Tellico::Data::FieldPtr tableField(new Tellico::Data::Field(QStringLiteral("table"), QStringLiteral("Table"), Tellico::Data::Field::Table));
0156   tableField->setFormatType(Tellico::FieldFormat::FormatName);
0157   coll->addField(tableField);
0158   QString tableValue = QStringLiteral("Value1")
0159                      + Tellico::FieldFormat::rowDelimiterString()
0160                      + Tellico::FieldFormat::rowDelimiterString()
0161                      + QStringLiteral("Value2");
0162   entry2->setField(QStringLiteral("table"), tableValue);
0163   QCOMPARE(entry2->formattedField(QStringLiteral("table")), tableValue);
0164   groupNames = entry2->groupNamesByFieldName(QStringLiteral("table"));
0165   QCOMPARE(groupNames.count(), 2);
0166   QVERIFY(groupNames.contains(QStringLiteral("Value1")));
0167   QVERIFY(groupNames.contains(QStringLiteral("Value2")));
0168 }
0169 
0170 void CollectionTest::testFields() {
0171   Tellico::Data::CollPtr coll(new Tellico::Data::Collection(true)); // add default fields
0172 
0173   QCOMPARE(coll->fields().count(), 4);
0174   QVERIFY(coll->peopleFields().isEmpty());
0175   QVERIFY(coll->imageFields().isEmpty());
0176 
0177   Tellico::Data::FieldPtr aField(new Tellico::Data::Field(QStringLiteral("author"),
0178                                                           QStringLiteral("Author")));
0179   aField->setFlags(Tellico::Data::Field::AllowMultiple | Tellico::Data::Field::AllowGrouped);
0180   aField->setFormatType(Tellico::FieldFormat::FormatName);
0181   QCOMPARE(coll->addField(aField), true);
0182   QVERIFY(coll->hasField(QStringLiteral("author")));
0183   QCOMPARE(coll->defaultGroupField(), QStringLiteral("author"));
0184 
0185   QCOMPARE(coll->fields().count(), 5);
0186   QCOMPARE(coll->peopleFields().count(), 1);
0187   QVERIFY(coll->imageFields().isEmpty());
0188   QVERIFY(!coll->hasImages());
0189   QCOMPARE(coll->fieldsByCategory(QStringLiteral("General")).size(), 2);
0190 
0191   Tellico::Data::FieldPtr bField(new Tellico::Data::Field(QStringLiteral("cover"),
0192                                                           QStringLiteral("Cover"),
0193                                                           Tellico::Data::Field::Image));
0194   QCOMPARE(coll->addField(bField), true);
0195   QVERIFY(coll->hasField(QStringLiteral("cover")));
0196   QVERIFY(!coll->hasField(QStringLiteral("Ccover")));
0197 
0198   QCOMPARE(coll->fields().count(), 6);
0199   QCOMPARE(coll->peopleFields().count(), 1);
0200   QCOMPARE(coll->imageFields().count(), 1);
0201   QVERIFY(coll->hasImages());
0202 
0203   QStringList cats = coll->fieldCategories();
0204   QCOMPARE(cats.size(), 3);
0205   QVERIFY(cats.contains(QStringLiteral("General")));
0206   QVERIFY(cats.contains(QStringLiteral("Personal")));
0207   QVERIFY(cats.contains(QStringLiteral("Cover")));
0208 
0209   const QStringList names = coll->fieldNames();
0210   QCOMPARE(names.size(), 6);
0211   QVERIFY(names.contains(QStringLiteral("author")));
0212   QVERIFY(names.contains(QStringLiteral("cover")));
0213 
0214   const QStringList titles = coll->fieldTitles();
0215   QCOMPARE(titles.size(), 6);
0216   QVERIFY(titles.contains(QStringLiteral("Author")));
0217   QVERIFY(titles.contains(QStringLiteral("Cover")));
0218 
0219   QCOMPARE(coll->fieldByName(QStringLiteral("author")), aField);
0220   QCOMPARE(coll->fieldByTitle(QStringLiteral("Author")), aField);
0221   QCOMPARE(coll->fieldNameByTitle(QStringLiteral("Author")), QStringLiteral("author"));
0222   QCOMPARE(coll->fieldNameByTitle(QStringLiteral("author")), QString());
0223   QCOMPARE(coll->fieldTitleByName(QStringLiteral("Author")), QString());
0224   QCOMPARE(coll->fieldTitleByName(QStringLiteral("author")), QStringLiteral("Author"));
0225 
0226   QVERIFY(coll->removeField(QStringLiteral("cover")));
0227   QVERIFY(!coll->hasField(QStringLiteral("cover")));
0228   QCOMPARE(coll->fields().count(), 5);
0229   QVERIFY(!coll->hasImages());
0230   QCOMPARE(coll->fieldTitleByName(QStringLiteral("cover")), QString());
0231   QCOMPARE(coll->fieldCategories().size(), 2);
0232 
0233   Tellico::Data::FieldPtr cField(new Tellico::Data::Field(QStringLiteral("editor"),
0234                                                           QStringLiteral("Editor")));
0235   cField->setFlags(Tellico::Data::Field::AllowGrouped);
0236   cField->setFormatType(Tellico::FieldFormat::FormatName);
0237   cField->setCategory(QStringLiteral("People"));
0238 
0239   // since the field name does not match an existing field, modifying should fail
0240   QVERIFY(!coll->modifyField(cField));
0241   cField->setName(QStringLiteral("author"));
0242   QVERIFY(coll->modifyField(cField));
0243   QCOMPARE(coll->fieldByName(QStringLiteral("author")), cField);
0244   QCOMPARE(coll->fieldByTitle(QStringLiteral("Author")), Tellico::Data::FieldPtr());
0245   QCOMPARE(coll->fieldByTitle(QStringLiteral("Editor")), cField);
0246   QCOMPARE(coll->peopleFields().count(), 1);
0247 
0248   cats = coll->fieldCategories();
0249   QCOMPARE(cats.size(), 3);
0250   QVERIFY(cats.contains(QStringLiteral("General")));
0251   QVERIFY(cats.contains(QStringLiteral("Personal")));
0252   QVERIFY(cats.contains(QStringLiteral("People")));
0253 
0254   QCOMPARE(coll->fieldsByCategory(QStringLiteral("General")).size(), 1);
0255   QCOMPARE(coll->fieldsByCategory(QStringLiteral("People")).size(), 1);
0256 
0257   coll->clear();
0258   QVERIFY(coll->fields().isEmpty());
0259   QVERIFY(coll->peopleFields().isEmpty());
0260   QVERIFY(coll->imageFields().isEmpty());
0261   QVERIFY(coll->fieldCategories().isEmpty());
0262   QVERIFY(coll->defaultGroupField().isEmpty());
0263   QCOMPARE(coll->fieldByName(QStringLiteral("author")), Tellico::Data::FieldPtr());
0264   QCOMPARE(coll->fieldByTitle(QStringLiteral("Editor")), Tellico::Data::FieldPtr());
0265 }
0266 
0267 void CollectionTest::testDerived() {
0268   Tellico::Data::CollPtr coll(new Tellico::Data::Collection(true)); // add default field
0269 
0270   Tellico::Data::FieldPtr aField(new Tellico::Data::Field(QStringLiteral("author"),
0271                                                           QStringLiteral("Author")));
0272   aField->setFlags(Tellico::Data::Field::AllowMultiple);
0273   aField->setFormatType(Tellico::FieldFormat::FormatName);
0274   coll->addField(aField);
0275 
0276   Tellico::Data::EntryPtr entry(new Tellico::Data::Entry(coll));
0277   entry->setField(QStringLiteral("author"), QStringLiteral("Albert Einstein; Niels Bohr"));
0278   coll->addEntries(entry);
0279 
0280   Tellico::Data::FieldPtr field(new Tellico::Data::Field(QStringLiteral("test"), QStringLiteral("Test")));
0281   field->setProperty(QStringLiteral("template"), QStringLiteral("%{author}"));
0282   field->setFlags(Tellico::Data::Field::Derived);
0283   field->setFormatType(Tellico::FieldFormat::FormatName);
0284   coll->addField(field);
0285 
0286   QCOMPARE(entry->field(QStringLiteral("test")), QStringLiteral("Albert Einstein; Niels Bohr"));
0287 
0288   field->setProperty(QStringLiteral("template"), QStringLiteral("%{test3}"));
0289 
0290   Tellico::Data::FieldPtr field2(new Tellico::Data::Field(QStringLiteral("test2"), QStringLiteral("Test")));
0291   field2->setProperty(QStringLiteral("template"), QStringLiteral("%{test}"));
0292   field2->setFlags(Tellico::Data::Field::Derived);
0293   coll->addField(field2);
0294 
0295   Tellico::Data::FieldPtr field3(new Tellico::Data::Field(QStringLiteral("test3"), QStringLiteral("Test")));
0296   field3->setProperty(QStringLiteral("template"), QStringLiteral("%{test3:1}"));
0297   field3->setFlags(Tellico::Data::Field::Derived);
0298   coll->addField(field3);
0299 
0300   // recursive, so template should be empty now
0301   QCOMPARE(field3->property(QStringLiteral("template")), QString());
0302 
0303   // now test all the possible format options
0304   field->setProperty(QStringLiteral("template"), QStringLiteral("%{author:1}"));
0305   QCOMPARE(entry->field(QStringLiteral("test")), QStringLiteral("Albert Einstein"));
0306 
0307   field->setProperty(QStringLiteral("template"), QStringLiteral("%{author:1/l}"));
0308   QCOMPARE(entry->field(QStringLiteral("test")), QStringLiteral("albert einstein"));
0309 
0310   field->setProperty(QStringLiteral("template"), QStringLiteral("%{author:1/u}"));
0311   QCOMPARE(entry->field(QStringLiteral("test")), QStringLiteral("ALBERT EINSTEIN"));
0312 
0313   field->setProperty(QStringLiteral("template"), QStringLiteral("%{author:1}"));
0314   QCOMPARE(entry->formattedField(QStringLiteral("test"), Tellico::FieldFormat::ForceFormat), QStringLiteral("Einstein, Albert"));
0315 
0316   field->setProperty(QStringLiteral("template"), QStringLiteral("%{author:2}"));
0317   QCOMPARE(entry->field(QStringLiteral("test")), QStringLiteral("Niels Bohr"));
0318 
0319   field->setProperty(QStringLiteral("template"), QStringLiteral("%{author:-1}"));
0320   QCOMPARE(entry->field(QStringLiteral("test")), QStringLiteral("Niels Bohr"));
0321 
0322   field->setProperty(QStringLiteral("template"), QStringLiteral("%{author:-1}"));
0323   QCOMPARE(entry->formattedField(QStringLiteral("test"), Tellico::FieldFormat::ForceFormat), QStringLiteral("Bohr, Niels"));
0324 
0325   field->setProperty(QStringLiteral("template"), QStringLiteral("%{author:-2}"));
0326   QCOMPARE(entry->field(QStringLiteral("test")), QStringLiteral("Albert Einstein"));
0327 }
0328 
0329 void CollectionTest::testValue() {
0330   QFETCH(QString, string);
0331   QFETCH(QString, formatted);
0332   QFETCH(int, typeInt);
0333 
0334   Tellico::FieldFormat::Type type = static_cast<Tellico::FieldFormat::Type>(typeInt);
0335 
0336   Tellico::Data::CollPtr coll(new Tellico::Data::Collection(true)); // add default field
0337 
0338   Tellico::Data::FieldPtr field1(new Tellico::Data::Field(QStringLiteral("test"), QStringLiteral("Test")));
0339   field1->setFlags(Tellico::Data::Field::AllowMultiple);
0340   field1->setFormatType(type);
0341   coll->addField(field1);
0342 
0343   Tellico::Data::FieldPtr field2(new Tellico::Data::Field(QStringLiteral("table"), QStringLiteral("Table"), Tellico::Data::Field::Table));
0344   field2->setFormatType(type);
0345   coll->addField(field2);
0346 
0347   Tellico::Data::EntryPtr entry(new Tellico::Data::Entry(coll));
0348   coll->addEntries(entry);
0349 
0350   entry->setField(field1, string);
0351 
0352   const QString dummy = Tellico::FieldFormat::columnDelimiterString() + QStringLiteral("dummy, the; Dummy, The; the dummy");
0353   entry->setField(field2, string + dummy);
0354 
0355   QCOMPARE(entry->formattedField(field1, Tellico::FieldFormat::ForceFormat), formatted);
0356   QCOMPARE(entry->formattedField(field2, Tellico::FieldFormat::ForceFormat), formatted.append(dummy));
0357 }
0358 
0359 void CollectionTest::testValue_data() {
0360   QTest::addColumn<QString>("string");
0361   QTest::addColumn<QString>("formatted");
0362   QTest::addColumn<int>("typeInt");
0363 
0364   QTest::newRow("test1") << "name" << "Name" << int(Tellico::FieldFormat::FormatName);
0365   QTest::newRow("test2") << "name1; name2" << "Name1; Name2" << int(Tellico::FieldFormat::FormatName);
0366   QTest::newRow("test3") << "Bob Dylan;Randy Quaid" << "Dylan, Bob; Quaid, Randy" << int(Tellico::FieldFormat::FormatName);
0367   QTest::newRow("test4") << "the return of the king" << "Return of the King, The" << int(Tellico::FieldFormat::FormatTitle);
0368   QTest::newRow("test5") << "the return of the king;the who" << "Return of the King, The; Who, The" << int(Tellico::FieldFormat::FormatTitle);
0369 }
0370 
0371 void CollectionTest::testDtd() {
0372   const QString xmllint = QStandardPaths::findExecutable(QStringLiteral("xmllint"));
0373   if(xmllint.isEmpty()) {
0374     QSKIP("This test requires xmllint", SkipAll);
0375   }
0376   // xmllint doesn't seem to support spaces in path. Is this an XML thing?
0377   if(QFINDTESTDATA("../../tellico.dtd").contains(QRegularExpression(QStringLiteral("\\s")))) {
0378     QSKIP("This test prohibits whitespace in the build path", SkipAll);
0379   }
0380 
0381   QFETCH(int, typeInt);
0382   Tellico::Data::Collection::Type type = static_cast<Tellico::Data::Collection::Type>(typeInt);
0383 
0384   Tellico::Data::CollPtr coll = Tellico::CollectionFactory::collection(type, true);
0385   QVERIFY(coll);
0386 
0387   Tellico::Data::EntryPtr entry1(new Tellico::Data::Entry(coll));
0388   coll->addEntries(entry1);
0389 
0390   foreach(Tellico::Data::FieldPtr field, coll->fields()) {
0391     switch(field->type()) {
0392       case Tellico::Data::Field::Line:   entry1->setField(field, field->title()); break;
0393       case Tellico::Data::Field::Para:   entry1->setField(field, field->title()); break;
0394       case Tellico::Data::Field::URL:    entry1->setField(field, field->title()); break;
0395       case Tellico::Data::Field::Table:  entry1->setField(field, field->title()); break;
0396       case Tellico::Data::Field::Image:  entry1->setField(field, field->title()); break;
0397       case Tellico::Data::Field::Number: entry1->setField(field, QStringLiteral("1")); break;
0398       case Tellico::Data::Field::Rating: entry1->setField(field, QStringLiteral("1")); break;
0399       case Tellico::Data::Field::Date:   entry1->setField(field, QStringLiteral("2009-01-10")); break;
0400       case Tellico::Data::Field::Bool:   entry1->setField(field, QStringLiteral("true")); break;
0401       case Tellico::Data::Field::Choice: entry1->setField(field, field->allowed().first()); break;
0402       default: break;
0403     }
0404   }
0405 
0406   Tellico::Export::TellicoXMLExporter exporter(coll);
0407   exporter.setEntries(coll->entries());
0408 
0409   KProcess proc;
0410   proc.setProgram(QStringLiteral("xmllint"),
0411                   QStringList() << QStringLiteral("--noout")
0412                                 << QStringLiteral("--nonet")
0413                                 << QStringLiteral("--nowarning")
0414                                 << QStringLiteral("--dtdvalid")
0415                                 << QFINDTESTDATA("../../tellico.dtd")
0416                                 << QStringLiteral("-"));
0417 
0418   proc.start();
0419   proc.write(exporter.text().toUtf8());
0420   proc.closeWriteChannel();
0421   proc.waitForFinished();
0422 
0423   QCOMPARE(proc.exitCode(), 0);
0424 }
0425 
0426 void CollectionTest::testDtd_data() {
0427   QTest::addColumn<int>("typeInt");
0428 
0429   QTest::newRow("book")   << int(Tellico::Data::Collection::Book);
0430   QTest::newRow("video")  << int(Tellico::Data::Collection::Video);
0431   QTest::newRow("album")  << int(Tellico::Data::Collection::Album);
0432   QTest::newRow("bibtex") << int(Tellico::Data::Collection::Bibtex);
0433   QTest::newRow("comic")  << int(Tellico::Data::Collection::ComicBook);
0434   QTest::newRow("wine")   << int(Tellico::Data::Collection::Wine);
0435   QTest::newRow("coin")   << int(Tellico::Data::Collection::Coin);
0436   QTest::newRow("stamp")  << int(Tellico::Data::Collection::Stamp);
0437   QTest::newRow("card")   << int(Tellico::Data::Collection::Card);
0438   QTest::newRow("game")   << int(Tellico::Data::Collection::Game);
0439   QTest::newRow("file")   << int(Tellico::Data::Collection::File);
0440   QTest::newRow("board")  << int(Tellico::Data::Collection::BoardGame);
0441 }
0442 
0443 void CollectionTest::testDuplicate() {
0444   Tellico::Data::CollPtr coll(new Tellico::Data::Collection(true));
0445 
0446   QCOMPARE(coll->entryCount(), 0);
0447 
0448   Tellico::Data::EntryPtr entry1(new Tellico::Data::Entry(coll));
0449   entry1->setField(QStringLiteral("title"), QStringLiteral("title1"));
0450   entry1->setField(QStringLiteral("cdate"), QStringLiteral("2019-01-01"));
0451   entry1->setField(QStringLiteral("mdate"), QStringLiteral("2019-04-01"));
0452   coll->addEntries(entry1);
0453   QCOMPARE(coll->entryCount(), 1);
0454 
0455   // this is how Controller::slotCopySelectedEntries() does it
0456   Tellico::Data::EntryPtr entry2(new Tellico::Data::Entry(*entry1));
0457   QVERIFY(entry2->field(QStringLiteral("cdate")).isEmpty());
0458   QVERIFY(entry2->field(QStringLiteral("mdate")).isEmpty());
0459   coll->addEntries(entry2);
0460   QCOMPARE(coll->entryCount(), 2);
0461 
0462   QCOMPARE(entry1->title(), entry2->title());
0463   QVERIFY(entry1->id() != entry2->id());
0464   // creation date should reflect current date in the duplicated entry
0465   QVERIFY(entry1->field(QStringLiteral("cdate")) != entry2->field(QStringLiteral("cdate")));
0466   QCOMPARE(entry2->field(QStringLiteral("cdate")), QDate::currentDate().toString(Qt::ISODate));
0467 
0468   // also test operator= which is how ModifyEntries::swapValues() works
0469   Tellico::Data::Entry* entryPtr = new Tellico::Data::Entry(coll);
0470   *entryPtr = *entry1;
0471   Tellico::Data::EntryPtr entry3(entryPtr);
0472   QVERIFY(entry3->field(QStringLiteral("cdate")).isEmpty());
0473   QVERIFY(entry3->field(QStringLiteral("mdate")).isEmpty());
0474   coll->addEntries(entry3);
0475   QCOMPARE(coll->entryCount(), 3);
0476 
0477   QCOMPARE(entry1->title(), entry3->title());
0478   // entry id should be different
0479   QVERIFY(entry1->id() != entry3->id());
0480   // creation date should reflect current date in the duplicated entry
0481   QVERIFY(entry1->field(QStringLiteral("cdate")) != entry3->field(QStringLiteral("cdate")));
0482   QCOMPARE(entry3->field(QStringLiteral("cdate")), QDate::currentDate().toString(Qt::ISODate));
0483 
0484   bool ret = Tellico::Merge::mergeEntry(entry1, entry2);
0485   QCOMPARE(ret, true);
0486 
0487   TestResolver cancelMerge(Tellico::Merge::ConflictResolver::CancelMerge);
0488   ret = Tellico::Merge::mergeEntry(entry1, entry2, &cancelMerge);
0489   QCOMPARE(ret, true);
0490 
0491   entry2->setField(QStringLiteral("title"), QStringLiteral("title2"));
0492 
0493   ret = Tellico::Merge::mergeEntry(entry1, entry2, &cancelMerge);
0494   QCOMPARE(ret, false);
0495   QCOMPARE(entry1->title(), QStringLiteral("title1"));
0496   QCOMPARE(entry2->title(), QStringLiteral("title2"));
0497 
0498   TestResolver keepFirst(Tellico::Merge::ConflictResolver::KeepFirst);
0499   ret = Tellico::Merge::mergeEntry(entry1, entry2, &keepFirst);
0500   QCOMPARE(ret, true);
0501   QCOMPARE(entry1->title(), QStringLiteral("title1"));
0502   // the second entry never gets changed
0503   QCOMPARE(entry2->title(), QStringLiteral("title2"));
0504 
0505   entry2->setField(QStringLiteral("title"), QStringLiteral("title2"));
0506 
0507   TestResolver keepSecond(Tellico::Merge::ConflictResolver::KeepSecond);
0508   ret = Tellico::Merge::mergeEntry(entry1, entry2, &keepSecond);
0509   QCOMPARE(ret, true);
0510   QCOMPARE(entry1->title(), QStringLiteral("title2"));
0511   QCOMPARE(entry2->title(), QStringLiteral("title2"));
0512 
0513   entry1->setField(QStringLiteral("title"), QStringLiteral("title1"));
0514 
0515   // returns true, ("merge successful") even if values were not merged
0516   ret = Tellico::Merge::mergeEntry(entry1, entry2);
0517   QCOMPARE(ret, true);
0518   QCOMPARE(entry1->title(), QStringLiteral("title1"));
0519   QCOMPARE(entry2->title(), QStringLiteral("title2"));
0520 }
0521 
0522 void CollectionTest::testMergeFields() {
0523   // here, we want to verify that when entries and fields from outside a collection are merged in
0524   // the allowed values for the Choice fields are retained in the same order, and new values are only
0525   // added if they are used
0526   Tellico::Data::CollPtr coll1 = Tellico::CollectionFactory::collection(Tellico::Data::Collection::Game, true);
0527   Tellico::Data::CollPtr coll2 = Tellico::CollectionFactory::collection(Tellico::Data::Collection::Game, true);
0528 
0529   // modify the allowed values for  "platform" in collection 1
0530   Tellico::Data::FieldPtr platform1 = coll1->fieldByName(QStringLiteral("platform"));
0531   QVERIFY(platform1);
0532   QStringList newValues1 = QStringList() << QStringLiteral("PSP") << QStringLiteral("Xbox 360");
0533   platform1->setAllowed(newValues1);
0534   QVERIFY(coll1->modifyField(platform1));
0535   QCOMPARE(platform1->allowed(), newValues1);
0536 
0537   Tellico::Data::EntryPtr entry2(new Tellico::Data::Entry(coll2));
0538   entry2->setField(QStringLiteral("platform"), QStringLiteral("PlayStation"));
0539   QCOMPARE(entry2->field(QStringLiteral("platform")), QStringLiteral("PlayStation"));
0540   coll2->addEntries(entry2);
0541 
0542   auto p = Tellico::Merge::mergeFields(coll1,
0543                                        Tellico::Data::FieldList() << coll2->fieldByName(QStringLiteral("platform")),
0544                                        Tellico::Data::EntryList() << entry2);
0545 
0546   Tellico::Data::FieldList modifiedFields = p.first;
0547   QCOMPARE(modifiedFields.count(), 1);
0548   // this is the zinger right here. The list of allowed values should be the original
0549   // with only the new existing value tacked on the end
0550   QCOMPARE(modifiedFields.first()->allowed(), newValues1 << entry2->field(QStringLiteral("platform")));
0551 
0552   Tellico::Data::FieldList addedFields = p.second;
0553   QVERIFY(addedFields.isEmpty());
0554 }
0555 
0556 void CollectionTest::testFieldsIntersection() {
0557   // simple test for the list intersection utility method
0558   Tellico::Data::CollPtr coll(new Tellico::Data::BookCollection(true));
0559   Tellico::Data::FieldList imageFields = coll->imageFields();
0560 
0561   Tellico::Data::FieldList list = Tellico::listIntersection(imageFields, coll->fields());
0562   QCOMPARE(imageFields.count(), list.count());
0563 
0564   QBENCHMARK {
0565     // should be something less than 0.020 msecs :)
0566     Tellico::Data::FieldList list = Tellico::listIntersection(coll->fields(), coll->fields());
0567     Q_UNUSED(list);
0568   }
0569 }
0570 
0571 void CollectionTest::testAppendCollection() {
0572   // appending a collection adds new fields, merges existing one, and add new entries
0573   // the new entries should belong to the original collection and the existing entries should
0574   // remain in the source collection
0575   Tellico::Data::CollPtr coll1 = Tellico::CollectionFactory::collection(Tellico::Data::Collection::Game, true);
0576   Tellico::Data::CollPtr coll2 = Tellico::CollectionFactory::collection(Tellico::Data::Collection::Game, true);
0577 
0578   // modify the allowed values for  "platform" in collection 1
0579   Tellico::Data::FieldPtr platform1 = coll1->fieldByName(QStringLiteral("platform"));
0580   QVERIFY(platform1);
0581   QStringList newValues1 = QStringList() << QStringLiteral("My Box");
0582   platform1->setAllowed(newValues1);
0583   QVERIFY(coll1->modifyField(platform1));
0584   // add a new field
0585   Tellico::Data::FieldPtr field1(new Tellico::Data::Field(QStringLiteral("test"), QStringLiteral("test")));
0586   QVERIFY(coll1->addField(field1));
0587 
0588   Tellico::Data::EntryPtr entry1(new Tellico::Data::Entry(coll1));
0589   QCOMPARE(entry1->collection(), coll1);
0590   coll1->addEntries(entry1);
0591 
0592   Tellico::Data::EntryPtr entry2(new Tellico::Data::Entry(coll2));
0593   QCOMPARE(entry2->collection(), coll2);
0594   coll2->addEntries(entry2);
0595 
0596   // append coll1 into coll2
0597   bool structuralChange;
0598   Tellico::Data::Document::appendCollection(coll2, coll1, &structuralChange);
0599   QVERIFY(structuralChange);
0600   // verify that the test field was added
0601   QVERIFY(coll2->hasField(QStringLiteral("test")));
0602   // verified that the modified field was merged
0603   Tellico::Data::FieldPtr platform2 = coll2->fieldByName(QStringLiteral("platform"));
0604   QVERIFY(platform2);
0605   QVERIFY(platform2->allowed().contains(QStringLiteral("My Box")));
0606 
0607   // coll2 should have two entries now, both with proper parent
0608   QCOMPARE(coll2->entryCount(), 2);
0609   Tellico::Data::EntryList e2 = coll2->entries();
0610   QCOMPARE(e2.at(0)->collection(), coll2);
0611   QCOMPARE(e2.at(1)->collection(), coll2);
0612 
0613   QCOMPARE(coll1->entryCount(), 1);
0614   Tellico::Data::EntryList e1 = coll1->entries();
0615   QCOMPARE(e1.at(0)->collection(), coll1);
0616 }
0617 
0618 void CollectionTest::testMergeCollection() {
0619   QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("data/movies-many.tc"));
0620 
0621   Tellico::Import::TellicoImporter importer1(url);
0622   Tellico::Data::CollPtr coll1 = importer1.collection();
0623   QVERIFY(coll1);
0624 
0625   Tellico::Import::TellicoImporter importer2(url);
0626   Tellico::Data::CollPtr coll2 = importer2.collection();
0627   QVERIFY(coll2);
0628 
0629   Tellico::Data::EntryPtr entryToAdd(new Tellico::Data::Entry(coll2));
0630   QCOMPARE(entryToAdd->collection(), coll2);
0631   coll2->addEntries(entryToAdd);
0632 
0633   QCOMPARE(coll1->entryCount()+1, coll2->entryCount());
0634 
0635   // merge coll2 into coll1
0636   // first item is a vector of all entries that got added in the merge process
0637   // second item is a pair of entries that had their table field modified
0638   // typedef QVector< QPair<EntryPtr, QString> > PairVector;
0639   // typedef QPair<Data::EntryList, PairVector> MergePair;
0640   bool structuralChange;
0641   Tellico::Data::MergePair mergePair = Tellico::Data::Document::mergeCollection(coll1, coll2, &structuralChange);
0642   QCOMPARE(structuralChange, false);
0643 
0644   // one new entry was added
0645   QCOMPARE(mergePair.first.count(), 1);
0646   // no table fields edited either
0647   QVERIFY(mergePair.second.isEmpty());
0648 
0649   // check item count
0650   QCOMPARE(coll1->fields().count(), coll2->fields().count());
0651   QCOMPARE(coll1->entryCount(), coll2->entryCount());
0652 }
0653 
0654 void CollectionTest::testBookMatch() {
0655   Tellico::Data::CollPtr c(new Tellico::Data::BookCollection(true));
0656 
0657   // first check merging with same isbn
0658   Tellico::Data::EntryPtr e1(new Tellico::Data::Entry(c));
0659   e1->setField(QStringLiteral("title"), QStringLiteral("title1"));
0660   e1->setField(QStringLiteral("author"), QStringLiteral("author1"));
0661   e1->setField(QStringLiteral("edition"), QStringLiteral("edition1"));
0662   e1->setField(QStringLiteral("pur_price"), QStringLiteral("price1"));
0663   e1->setField(QStringLiteral("isbn"), QStringLiteral("1234567890"));
0664   c->addEntries(e1);
0665 
0666   Tellico::Data::EntryPtr e2(new Tellico::Data::Entry(c));
0667   e2->setField(QStringLiteral("title"), QStringLiteral("title2"));
0668   e2->setField(QStringLiteral("author"), QStringLiteral("author2"));
0669   e2->setField(QStringLiteral("edition"), QStringLiteral("edition2"));
0670   e2->setField(QStringLiteral("pur_price"), QStringLiteral("price2"));
0671   e2->setField(QStringLiteral("isbn"), QStringLiteral("000000000"));
0672 
0673   // not a good match
0674   QVERIFY(c->sameEntry(e1, e2) < Tellico::EntryComparison::ENTRY_GOOD_MATCH);
0675 
0676   // perfect match now
0677   e2->setField(QStringLiteral("isbn"), QStringLiteral("1234567890"));
0678   QCOMPARE(c->sameEntry(e1, e2), int(Tellico::EntryComparison::ENTRY_PERFECT_MATCH));
0679 
0680   QBENCHMARK {
0681     QCOMPARE(c->sameEntry(e1, e2), int(Tellico::EntryComparison::ENTRY_PERFECT_MATCH));
0682   }
0683 }
0684 
0685 void CollectionTest::testMergeBenchmark() {
0686   QUrl url = QUrl::fromLocalFile(QFINDTESTDATA("data/movies-many.tc"));
0687 
0688   bool structuralChange;
0689   Tellico::Data::EntryList entriesToAdd;
0690   QBENCHMARK {
0691     Tellico::Import::TellicoImporter importer1(url, false /* load all images */);
0692     Tellico::Data::CollPtr coll1 = importer1.collection();
0693     QVERIFY(coll1);
0694 
0695     Tellico::Import::TellicoImporter importer2(url);
0696     Tellico::Data::CollPtr coll2 = importer2.collection();
0697     QVERIFY(coll2);
0698 
0699     entriesToAdd.clear();
0700     for(int i = 0; i < 500; ++i) {
0701       Tellico::Data::EntryPtr entryToAdd(new Tellico::Data::Entry(coll2));
0702       entryToAdd->setField(QStringLiteral("title"), QString::number(QRandomGenerator::global()->generate()));
0703       entryToAdd->setField(QStringLiteral("studio"), QString::number(i));
0704       entriesToAdd += entryToAdd;
0705     }
0706     coll2->addEntries(entriesToAdd);
0707 
0708     Tellico::Data::Document::mergeCollection(coll1, coll2, &structuralChange);
0709   }
0710 }
0711 
0712 void CollectionTest::testMatchScore() {
0713   QFETCH(QString, field);
0714   QFETCH(QString, value);
0715   QFETCH(Tellico::EntryComparison::MatchValue, score);
0716 
0717   QVERIFY(m_coll);
0718   Tellico::Data::EntryPtr e(new Tellico::Data::Entry(m_coll));
0719   e->setField(field, value);
0720   QCOMPARE(Tellico::EntryComparison::score(m_entry, e, field, m_coll.data()), int(score));
0721 }
0722 
0723 void CollectionTest::testMatchScore_data() {
0724   QTest::addColumn<QString>("field");
0725   QTest::addColumn<QString>("value");
0726   QTest::addColumn<Tellico::EntryComparison::MatchValue>("score");
0727 
0728   QTest::newRow("empty title") << QStringLiteral("title") << QString() << Tellico::EntryComparison::MATCH_VALUE_NONE;
0729   QTest::newRow("title match") << QStringLiteral("title") << QStringLiteral("title1") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0730   QTest::newRow("title match case") << QStringLiteral("title") << QStringLiteral("TITLE1") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0731 //  QTest::newRow("title match articles") << QStringLiteral("title") << QStringLiteral("THE TITLE1") << Tellico::EntryComparison::MATCH_VALUE_WEAK;
0732   QTest::newRow("title match non alphanum") << QStringLiteral("title") << QStringLiteral("title1.") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0733 //  QTest::newRow("title match paren") << QStringLiteral("title") << QStringLiteral("title1 (old)") << Tellico::EntryComparison::MATCH_VALUE_WEAK;
0734   QTest::newRow("isbn match") << QStringLiteral("isbn") << QStringLiteral("1234367890") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0735   QTest::newRow("isbn match formatted") << QStringLiteral("isbn") << QStringLiteral("1-234-36789-0") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0736   QTest::newRow("lccn match") << QStringLiteral("lccn") << QStringLiteral("89456") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0737   QTest::newRow("lccn match formatted") << QStringLiteral("lccn") << QStringLiteral("89-456") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0738   QTest::newRow("arxiv") << QStringLiteral("arxiv") << QStringLiteral("hep-lat/0110180") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0739   QTest::newRow("arxiv format1") << QStringLiteral("arxiv") << QStringLiteral("hep-lat/0110180v1") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0740   QTest::newRow("arxiv format2") << QStringLiteral("arxiv") << QStringLiteral("arxiv:hep-lat/0110180v1") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0741   QTest::newRow("author") << QStringLiteral("author") << QStringLiteral("John Doe") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0742   QTest::newRow("author formatted") << QStringLiteral("author") << QStringLiteral("Doe, John") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0743   QTest::newRow("author formatted2") << QStringLiteral("author") << QStringLiteral("doe, john") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0744   QTest::newRow("author multiple") << QStringLiteral("author") << QStringLiteral("John Doe; Jane Doe") << Tellico::EntryComparison::MATCH_VALUE_STRONG;
0745 }
0746 
0747 void CollectionTest::testGamePlatform() {
0748   // test that the platform name guessing heuristic works on its own names
0749   for(int i = 1; i < Tellico::Data::GameCollection::LastPlatform; i++) {
0750     QString pName = Tellico::Data::GameCollection::platformName(Tellico::Data::GameCollection::GamePlatform(i));
0751     int pGuess = Tellico::Data::GameCollection::guessPlatform(pName);
0752     QCOMPARE(i, pGuess);
0753   }
0754 
0755   // test some specific platform names that some data sources might return
0756   // thegamesdb.net returns "Nintendo Game Boy"
0757   int gameBoy = Tellico::Data::GameCollection::guessPlatform(QStringLiteral("Nintendo Game Boy"));
0758   QCOMPARE(gameBoy, int(Tellico::Data::GameCollection::GameBoy));
0759   gameBoy = Tellico::Data::GameCollection::guessPlatform(QStringLiteral("gameboy"));
0760   QCOMPARE(gameBoy, int(Tellico::Data::GameCollection::GameBoy));
0761   gameBoy = Tellico::Data::GameCollection::guessPlatform(QStringLiteral("gameboy color"));
0762   QCOMPARE(gameBoy, int(Tellico::Data::GameCollection::GameBoyColor));
0763   gameBoy = Tellico::Data::GameCollection::guessPlatform(QStringLiteral("Gameboy Advance"));
0764   QCOMPARE(gameBoy, int(Tellico::Data::GameCollection::GameBoyAdvance));
0765 
0766   // don't match Nintendo Virtual Boy with Nintendo
0767   int guess = Tellico::Data::GameCollection::guessPlatform(QStringLiteral("Nintendo Virtual Boy"));
0768   QCOMPARE(guess, int(Tellico::Data::GameCollection::UnknownPlatform));
0769   guess = Tellico::Data::GameCollection::guessPlatform(QStringLiteral("Nintendo Entertainment System"));
0770   QCOMPARE(guess, int(Tellico::Data::GameCollection::Nintendo));
0771 }