File indexing completed on 2024-05-12 05:09:55

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