diff --git a/include/library.h b/include/library.h index 7c806ed6d..d91e3c7da 100644 --- a/include/library.h +++ b/include/library.h @@ -35,6 +35,7 @@ namespace kiwix { class OPDSDumper; +class Library; enum supportedListSortBy { UNSORTED, TITLE, SIZE, DATE, CREATOR, PUBLISHER }; enum supportedListMode { @@ -48,10 +49,13 @@ enum supportedListMode { }; class Filter { - private: + public: // types + using Tags = std::vector; + + private: // data uint64_t activeFilters; - std::vector _acceptTags; - std::vector _rejectTags; + Tags _acceptTags; + Tags _rejectTags; std::string _category; std::string _lang; std::string _publisher; @@ -61,7 +65,7 @@ class Filter { bool _queryIsPartial; std::string _name; - public: + public: // functions Filter(); ~Filter() = default; @@ -95,8 +99,8 @@ class Filter { /** * Set the filter to only accept book with corresponding tag. */ - Filter& acceptTags(std::vector tags); - Filter& rejectTags(std::vector tags); + Filter& acceptTags(const Tags& tags); + Filter& rejectTags(const Tags& tags); Filter& category(std::string category); Filter& lang(std::string lang); @@ -110,9 +114,28 @@ class Filter { const std::string& getQuery() const { return _query; } bool queryIsPartial() const { return _queryIsPartial; } + bool hasName() const; + const std::string& getName() const { return _name; } + + bool hasCategory() const; + const std::string& getCategory() const { return _category; } + + bool hasLang() const; + const std::string& getLang() const { return _lang; } + + bool hasPublisher() const; + const std::string& getPublisher() const { return _publisher; } + + bool hasCreator() const; + const std::string& getCreator() const { return _creator; } + + const Tags& getAcceptTags() const { return _acceptTags; } + const Tags& getRejectTags() const { return _rejectTags; } + +private: // functions + friend class Library; + bool accept(const Book& book) const; - bool acceptByQueryOnly(const Book& book) const; - bool acceptByNonQueryCriteria(const Book& book) const; }; @@ -307,7 +330,7 @@ class Library friend class libXMLDumper; private: // functions - BookIdCollection getBooksByTitleOrDescription(const Filter& filter); + BookIdCollection filterViaBookDB(const Filter& filter); void updateBookDB(const Book& book); }; diff --git a/src/library.cpp b/src/library.cpp index 6a2bc5db8..bc74c488f 100644 --- a/src/library.cpp +++ b/src/library.cpp @@ -43,7 +43,7 @@ std::string iso639_3ToXapian(const std::string& lang) { return icu::Locale(lang.c_str()).getLanguage(); }; -std::string normalizeText(const std::string& text, const std::string& language) +std::string normalizeText(const std::string& text) { return removeAccents(text); } @@ -276,35 +276,61 @@ void Library::updateBookDB(const Book& book) Xapian::Document doc; indexer.set_document(doc); - const std::string title = normalizeText(book.getTitle(), lang); - const std::string desc = normalizeText(book.getDescription(), lang); - doc.add_value(0, title); - doc.add_value(1, desc); - doc.set_data(book.getId()); + const std::string title = normalizeText(book.getTitle()); + const std::string desc = normalizeText(book.getDescription()); - indexer.index_text(title, 1, "S"); - indexer.index_text(desc, 1, "XD"); - - // Index fields without prefixes for general search + // Index title and description without prefixes for general search indexer.index_text(title); indexer.increase_termpos(); indexer.index_text(desc); + // Index all fields for field-based search + indexer.index_text(title, 1, "S"); + indexer.index_text(desc, 1, "XD"); + indexer.index_text(lang, 1, "L"); + indexer.index_text(normalizeText(book.getCreator()), 1, "A"); + indexer.index_text(normalizeText(book.getPublisher()), 1, "XP"); + indexer.index_text(normalizeText(book.getName()), 1, "XN"); + indexer.index_text(normalizeText(book.getCategory()), 1, "XC"); + + for ( const auto& tag : split(normalizeText(book.getTags()), ";") ) + doc.add_boolean_term("XT" + tag); + const std::string idterm = "Q" + book.getId(); doc.add_boolean_term(idterm); + + doc.set_data(book.getId()); + m_bookDB->replace_document(idterm, doc); } -Library::BookIdCollection Library::getBooksByTitleOrDescription(const Filter& filter) +namespace { - if ( !filter.hasQuery() ) - return getBooksIds(); - BookIdCollection bookIds; +bool willSelectEverything(const Xapian::Query& query) +{ + return query.get_type() == Xapian::Query::LEAF_MATCH_ALL; +} + + +Xapian::Query buildXapianQueryFromFilterQuery(const Filter& filter) +{ + if ( !filter.hasQuery() ) { + // This is a thread-safe way to construct an equivalent of + // a Xapian::Query::MatchAll query + return Xapian::Query(std::string()); + } + Xapian::QueryParser queryParser; queryParser.set_default_op(Xapian::Query::OP_AND); queryParser.add_prefix("title", "S"); queryParser.add_prefix("description", "XD"); + queryParser.add_prefix("name", "XN"); + queryParser.add_prefix("category", "XC"); + queryParser.add_prefix("lang", "L"); + queryParser.add_prefix("publisher", "XP"); + queryParser.add_prefix("creator", "A"); + queryParser.add_prefix("tag", "XT"); const auto partialQueryFlag = filter.queryIsPartial() ? Xapian::QueryParser::FLAG_PARTIAL : 0; @@ -314,10 +340,99 @@ Library::BookIdCollection Library::getBooksByTitleOrDescription(const Filter& fi //queryParser.set_stemming_strategy(Xapian::QueryParser::STEM_SOME); const auto flags = Xapian::QueryParser::FLAG_PHRASE | Xapian::QueryParser::FLAG_BOOLEAN + | Xapian::QueryParser::FLAG_BOOLEAN_ANY_CASE | Xapian::QueryParser::FLAG_LOVEHATE | Xapian::QueryParser::FLAG_WILDCARD | partialQueryFlag; - const auto query = queryParser.parse_query(filter.getQuery(), flags); + return queryParser.parse_query(normalizeText(filter.getQuery()), flags); +} + +Xapian::Query nameQuery(const std::string& name) +{ + return Xapian::Query("XN" + normalizeText(name)); +} + +Xapian::Query categoryQuery(const std::string& category) +{ + return Xapian::Query("XC" + normalizeText(category)); +} + +Xapian::Query langQuery(const std::string& lang) +{ + return Xapian::Query("L" + normalizeText(lang)); +} + +Xapian::Query publisherQuery(const std::string& publisher) +{ + Xapian::QueryParser queryParser; + queryParser.set_default_op(Xapian::Query::OP_OR); + queryParser.set_stemming_strategy(Xapian::QueryParser::STEM_NONE); + const auto flags = 0; + const auto q = queryParser.parse_query(normalizeText(publisher), flags, "XP"); + return Xapian::Query(Xapian::Query::OP_PHRASE, q.get_terms_begin(), q.get_terms_end(), q.get_length()); +} + +Xapian::Query creatorQuery(const std::string& creator) +{ + Xapian::QueryParser queryParser; + queryParser.set_default_op(Xapian::Query::OP_OR); + queryParser.set_stemming_strategy(Xapian::QueryParser::STEM_NONE); + const auto flags = 0; + const auto q = queryParser.parse_query(normalizeText(creator), flags, "A"); + return Xapian::Query(Xapian::Query::OP_PHRASE, q.get_terms_begin(), q.get_terms_end(), q.get_length()); +} + +Xapian::Query tagsQuery(const Filter::Tags& acceptTags, const Filter::Tags& rejectTags) +{ + Xapian::Query q = Xapian::Query(std::string()); + if (!acceptTags.empty()) { + for ( const auto& tag : acceptTags ) + q &= Xapian::Query("XT" + normalizeText(tag)); + } + + if (!rejectTags.empty()) { + for ( const auto& tag : rejectTags ) + q = Xapian::Query(Xapian::Query::OP_AND_NOT, q, "XT" + normalizeText(tag)); + } + return q; +} + +Xapian::Query buildXapianQuery(const Filter& filter) +{ + auto q = buildXapianQueryFromFilterQuery(filter); + if ( filter.hasName() ) { + q = Xapian::Query(Xapian::Query::OP_AND, q, nameQuery(filter.getName())); + } + if ( filter.hasCategory() ) { + q = Xapian::Query(Xapian::Query::OP_AND, q, categoryQuery(filter.getCategory())); + } + if ( filter.hasLang() ) { + q = Xapian::Query(Xapian::Query::OP_AND, q, langQuery(filter.getLang())); + } + if ( filter.hasPublisher() ) { + q = Xapian::Query(Xapian::Query::OP_AND, q, publisherQuery(filter.getPublisher())); + } + if ( filter.hasCreator() ) { + q = Xapian::Query(Xapian::Query::OP_AND, q, creatorQuery(filter.getCreator())); + } + if ( !filter.getAcceptTags().empty() || !filter.getRejectTags().empty() ) { + const auto tq = tagsQuery(filter.getAcceptTags(), filter.getRejectTags()); + q = Xapian::Query(Xapian::Query::OP_AND, q, tq);; + } + return q; +} + +} // unnamed namespace + +Library::BookIdCollection Library::filterViaBookDB(const Filter& filter) +{ + const auto query = buildXapianQuery(filter); + + if ( willSelectEverything(query) ) + return getBooksIds(); + + BookIdCollection bookIds; + Xapian::Enquire enquire(*m_bookDB); enquire.set_query(query); const auto results = enquire.get_mset(0, m_books.size()); @@ -331,8 +446,8 @@ Library::BookIdCollection Library::getBooksByTitleOrDescription(const Filter& fi Library::BookIdCollection Library::filter(const Filter& filter) { BookIdCollection result; - for(auto id : getBooksByTitleOrDescription(filter)) { - if(filter.acceptByNonQueryCriteria(m_books.at(id))) { + for(auto id : filterViaBookDB(filter)) { + if(filter.accept(m_books.at(id))) { result.push_back(id); } } @@ -525,14 +640,14 @@ Filter& Filter::valid(bool accept) return *this; } -Filter& Filter::acceptTags(std::vector tags) +Filter& Filter::acceptTags(const Tags& tags) { _acceptTags = tags; activeFilters |= ACCEPTTAGS; return *this; } -Filter& Filter::rejectTags(std::vector tags) +Filter& Filter::rejectTags(const Tags& tags) { _rejectTags = tags; activeFilters |= REJECTTAGS; @@ -596,12 +711,32 @@ bool Filter::hasQuery() const return ACTIVE(QUERY); } -bool Filter::accept(const Book& book) const +bool Filter::hasName() const { - return acceptByNonQueryCriteria(book) && acceptByQueryOnly(book); + return ACTIVE(NAME); } -bool Filter::acceptByNonQueryCriteria(const Book& book) const +bool Filter::hasCategory() const +{ + return ACTIVE(CATEGORY); +} + +bool Filter::hasLang() const +{ + return ACTIVE(LANG); +} + +bool Filter::hasPublisher() const +{ + return ACTIVE(_PUBLISHER); +} + +bool Filter::hasCreator() const +{ + return ACTIVE(_CREATOR); +} + +bool Filter::accept(const Book& book) const { auto local = !book.getPath().empty(); FILTER(_LOCAL, local) @@ -616,46 +751,8 @@ bool Filter::acceptByNonQueryCriteria(const Book& book) const FILTER(_NOREMOTE, !remote) FILTER(MAXSIZE, book.getSize() <= _maxSize) - FILTER(CATEGORY, book.getCategory() == _category) - FILTER(LANG, book.getLanguage() == _lang) - FILTER(_PUBLISHER, book.getPublisher() == _publisher) - FILTER(_CREATOR, book.getCreator() == _creator) - FILTER(NAME, book.getName() == _name) - - if (ACTIVE(ACCEPTTAGS)) { - if (!_acceptTags.empty()) { - auto vBookTags = split(book.getTags(), ";"); - std::set sBookTags(vBookTags.begin(), vBookTags.end()); - for (auto& t: _acceptTags) { - if (sBookTags.find(t) == sBookTags.end()) { - return false; - } - } - } - } - if (ACTIVE(REJECTTAGS)) { - if (!_rejectTags.empty()) { - auto vBookTags = split(book.getTags(), ";"); - std::set sBookTags(vBookTags.begin(), vBookTags.end()); - for (auto& t: _rejectTags) { - if (sBookTags.find(t) != sBookTags.end()) { - return false; - } - } - } - } - return true; -} - -bool Filter::acceptByQueryOnly(const Book& book) const -{ - if ( ACTIVE(QUERY) - && !(matchRegex(book.getTitle(), "\\Q" + _query + "\\E") - || matchRegex(book.getDescription(), "\\Q" + _query + "\\E"))) - return false; return true; - } } diff --git a/test/library.cpp b/test/library.cpp index c19514163..be2df95c9 100644 --- a/test/library.cpp +++ b/test/library.cpp @@ -181,6 +181,42 @@ const char * sampleOpdsStream = R"( )"; +const char sampleLibraryXML[] = R"( + + + + +)"; + #include "../include/library.h" #include "../include/manager.h" #include "../include/bookmark.h" @@ -190,9 +226,13 @@ namespace class LibraryTest : public ::testing::Test { protected: + typedef kiwix::Library::BookIdCollection BookIdCollection; + typedef std::vector TitleCollection; + void SetUp() override { kiwix::Manager manager(&lib); manager.readOpds(sampleOpdsStream, "foo.urlHost"); + manager.readXml(sampleLibraryXML, true, "./test/library.xml", true); } kiwix::Bookmark createBookmark(const std::string &id) { @@ -201,6 +241,15 @@ class LibraryTest : public ::testing::Test { return bookmark; }; + TitleCollection ids2Titles(const BookIdCollection& ids) { + TitleCollection titles; + for ( const auto& bookId : ids ) { + titles.push_back(lib.getBookById(bookId).getTitle()); + } + std::sort(titles.begin(), titles.end()); + return titles; + } + kiwix::Library lib; }; @@ -225,10 +274,10 @@ TEST_F(LibraryTest, getBookMarksTest) TEST_F(LibraryTest, sanityCheck) { - EXPECT_EQ(lib.getBookCount(true, true), 10U); - EXPECT_EQ(lib.getBooksLanguages().size(), 2U); - EXPECT_EQ(lib.getBooksCreators().size(), 8U); - EXPECT_EQ(lib.getBooksPublishers().size(), 1U); + EXPECT_EQ(lib.getBookCount(true, true), 12U); + EXPECT_EQ(lib.getBooksLanguages().size(), 3U); + EXPECT_EQ(lib.getBooksCreators().size(), 9U); + EXPECT_EQ(lib.getBooksPublishers().size(), 3U); } TEST_F(LibraryTest, categoryHandling) @@ -240,35 +289,339 @@ TEST_F(LibraryTest, categoryHandling) EXPECT_EQ("category_element_overrides_tags", lib.getBookById("14829621-c490-c376-0792-9de558b57efa").getCategory()); } -TEST_F(LibraryTest, filterCheck) +TEST_F(LibraryTest, emptyFilter) { - auto bookIds = lib.filter(kiwix::Filter()); + const auto bookIds = lib.filter(kiwix::Filter()); EXPECT_EQ(bookIds, lib.getBooksIds()); +} - bookIds = lib.filter(kiwix::Filter().lang("eng")); - EXPECT_EQ(bookIds.size(), 5U); +#define EXPECT_FILTER_RESULTS(f, ...) \ + EXPECT_EQ( \ + ids2Titles(lib.filter(f)), \ + TitleCollection({ __VA_ARGS__ }) \ + ) - bookIds = lib.filter(kiwix::Filter().acceptTags({"stackexchange"})); - EXPECT_EQ(bookIds.size(), 3U); +TEST_F(LibraryTest, filterLocal) +{ + EXPECT_FILTER_RESULTS(kiwix::Filter().local(true), + "An example ZIM archive", + "Ray Charles" + ); - bookIds = lib.filter(kiwix::Filter().acceptTags({"wikipedia"})); - EXPECT_EQ(bookIds.size(), 3U); + EXPECT_FILTER_RESULTS(kiwix::Filter().local(false), + "Encyclopédie de la Tunisie", + "Granblue Fantasy Wiki", + "Géographie par Wikipédia", + "Islam Stack Exchange", + "Mathématiques", + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange", + "TED talks - Business", + "Tania Louis", + "Wikiquote" + ); +} - bookIds = lib.filter(kiwix::Filter().acceptTags({"wikipedia", "nopic"})); - EXPECT_EQ(bookIds.size(), 2U); +TEST_F(LibraryTest, filterRemote) +{ + EXPECT_FILTER_RESULTS(kiwix::Filter().remote(true), + "Encyclopédie de la Tunisie", + "Granblue Fantasy Wiki", + "Géographie par Wikipédia", + "Islam Stack Exchange", + "Mathématiques", + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange", + "Ray Charles", + "TED talks - Business", + "Tania Louis", + "Wikiquote" + ); - bookIds = lib.filter(kiwix::Filter().acceptTags({"wikipedia"}).rejectTags({"nopic"})); - EXPECT_EQ(bookIds.size(), 1U); + EXPECT_FILTER_RESULTS(kiwix::Filter().remote(false), + "An example ZIM archive" + ); +} - bookIds = lib.filter(kiwix::Filter().query("folklore")); - EXPECT_EQ(bookIds.size(), 1U); +TEST_F(LibraryTest, filterByLanguage) +{ + EXPECT_FILTER_RESULTS(kiwix::Filter().lang("eng"), + "Granblue Fantasy Wiki", + "Islam Stack Exchange", + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange", + "Ray Charles", + "TED talks - Business" + ); - bookIds = lib.filter(kiwix::Filter().query("Wiki")); - EXPECT_EQ(bookIds.size(), 4U); + EXPECT_FILTER_RESULTS(kiwix::Filter().query("lang:eng"), + "Granblue Fantasy Wiki", + "Islam Stack Exchange", + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange", + "Ray Charles", + "TED talks - Business" + ); - bookIds = lib.filter(kiwix::Filter().query("Wiki").creator("Wiki")); - EXPECT_EQ(bookIds.size(), 1U); + EXPECT_FILTER_RESULTS(kiwix::Filter().query("eng"), + /* no results */ + ); +} +TEST_F(LibraryTest, filterByTags) +{ + EXPECT_FILTER_RESULTS(kiwix::Filter().acceptTags({"stackexchange"}), + "Islam Stack Exchange", + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange" + ); + + // filtering by tags is case and diacritics insensitive + EXPECT_FILTER_RESULTS(kiwix::Filter().acceptTags({"ståckEXÇhange"}), + "Islam Stack Exchange", + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange" + ); + + // filtering by tags requires full match of the search term + EXPECT_FILTER_RESULTS(kiwix::Filter().acceptTags({"stackexch"}), + /* no results */ + ); + + // in tags with values (tag:value form) the value is an inseparable + // part of the tag + EXPECT_FILTER_RESULTS(kiwix::Filter().acceptTags({"_category"}), + /* no results */ + ); + EXPECT_FILTER_RESULTS(kiwix::Filter().acceptTags({"_category:category_defined_via_tags_only"}), + "Tania Louis" + ); + + EXPECT_FILTER_RESULTS(kiwix::Filter().acceptTags({"wikipedia"}), + "Encyclopédie de la Tunisie", + "Géographie par Wikipédia", + "Mathématiques", + "Ray Charles" + ); + + EXPECT_FILTER_RESULTS(kiwix::Filter().acceptTags({"wikipedia", "nopic"}), + "Géographie par Wikipédia", + "Mathématiques" + ); + + EXPECT_FILTER_RESULTS(kiwix::Filter().acceptTags({"wikipedia"}).rejectTags({"nopic"}), + "Encyclopédie de la Tunisie", + "Ray Charles" + ); +} + + +TEST_F(LibraryTest, filterByQuery) +{ + // filtering by query checks the title + EXPECT_FILTER_RESULTS(kiwix::Filter().query("Exchange"), + "Islam Stack Exchange", + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange" + ); + + // filtering by query checks the description/summary + EXPECT_FILTER_RESULTS(kiwix::Filter().query("enthusiasts"), + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange" + ); + + // filtering by query is case insensitive on titles + EXPECT_FILTER_RESULTS(kiwix::Filter().query("ExcHANge"), + "Islam Stack Exchange", + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange" + ); + + // filtering by query is diacritics insensitive on titles + EXPECT_FILTER_RESULTS(kiwix::Filter().query("mathematiques"), + "Mathématiques", + ); + EXPECT_FILTER_RESULTS(kiwix::Filter().query("èxchângé"), + "Islam Stack Exchange", + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange" + ); + + // filtering by query is case insensitive on description/summary + EXPECT_FILTER_RESULTS(kiwix::Filter().query("enTHUSiaSTS"), + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange" + ); + + // filtering by query is diacritics insensitive on description/summary + EXPECT_FILTER_RESULTS(kiwix::Filter().query("selection"), + "Géographie par Wikipédia" + ); + EXPECT_FILTER_RESULTS(kiwix::Filter().query("enthúsïåsts"), + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange" + ); + + // by default, filtering by query assumes partial query + EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki"), + "Encyclopédie de la Tunisie", + "Granblue Fantasy Wiki", + "Géographie par Wikipédia", + "Ray Charles", + "Wikiquote" + ); + + // partial query can be disabled + EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki", false), + "Granblue Fantasy Wiki" + ); +} + + +TEST_F(LibraryTest, filterByCreator) +{ + EXPECT_FILTER_RESULTS(kiwix::Filter().creator("Wikipedia"), + "Encyclopédie de la Tunisie", + "Géographie par Wikipédia", + "Mathématiques", + "Ray Charles" + ); + + // filtering by creator requires full match of the search term + EXPECT_FILTER_RESULTS(kiwix::Filter().creator("Wiki"), + "Granblue Fantasy Wiki" + ); + + // filtering by creator is case and diacritics insensitive + EXPECT_FILTER_RESULTS(kiwix::Filter().creator("wIkï"), + "Granblue Fantasy Wiki" + ); + + // filtering by creator doesn't requires full match of the full creator name + EXPECT_FILTER_RESULTS(kiwix::Filter().creator("Stack"), + "Islam Stack Exchange", + "Movies & TV Stack Exchange", + "Mythology & Folklore Stack Exchange" + ); + + // filtering by creator requires a full phrase match (ignoring some non-word terms) + EXPECT_FILTER_RESULTS(kiwix::Filter().creator("Movies & TV Stack Exchange"), + "Movies & TV Stack Exchange" + ); + EXPECT_FILTER_RESULTS(kiwix::Filter().creator("Movies & TV"), + "Movies & TV Stack Exchange" + ); + EXPECT_FILTER_RESULTS(kiwix::Filter().creator("Movies TV"), + "Movies & TV Stack Exchange" + ); + EXPECT_FILTER_RESULTS(kiwix::Filter().creator("TV & Movies"), + /* no results */ + ); + EXPECT_FILTER_RESULTS(kiwix::Filter().creator("TV Movies"), + /* no results */ + ); + + EXPECT_FILTER_RESULTS(kiwix::Filter().query("creator:Wikipedia"), + "Encyclopédie de la Tunisie", + "Géographie par Wikipédia", + "Mathématiques", + "Ray Charles" + ); + +} + +TEST_F(LibraryTest, filterByPublisher) +{ + EXPECT_FILTER_RESULTS(kiwix::Filter().publisher("Kiwix"), + "An example ZIM archive", + "Ray Charles" + ); + + // filtering by publisher requires full match of the search term + EXPECT_FILTER_RESULTS(kiwix::Filter().publisher("Kiwi"), + /* no results */ + ); + + // filtering by publisher requires a full phrase match + EXPECT_FILTER_RESULTS(kiwix::Filter().publisher("Kiwix & Some Enthusiasts"), + "An example ZIM archive" + ); + EXPECT_FILTER_RESULTS(kiwix::Filter().publisher("Some Enthusiasts & Kiwix"), + /* no results */ + ); + + // filtering by publisher is case and diacritics insensitive + EXPECT_FILTER_RESULTS(kiwix::Filter().publisher("kîWIx"), + "An example ZIM archive", + "Ray Charles" + ); + + EXPECT_FILTER_RESULTS(kiwix::Filter().query("publisher:kiwix"), + "An example ZIM archive", + "Ray Charles" + ); + + EXPECT_FILTER_RESULTS(kiwix::Filter().query("kiwix"), + /* no results */ + ); +} + +TEST_F(LibraryTest, filterByName) +{ + EXPECT_FILTER_RESULTS(kiwix::Filter().name("wikibooks_de"), + "An example ZIM archive" + ); + + EXPECT_FILTER_RESULTS(kiwix::Filter().query("name:wikibooks_de"), + "An example ZIM archive" + ); + + EXPECT_FILTER_RESULTS(kiwix::Filter().query("wikibooks_de"), + /* no results */ + ); +} + +TEST_F(LibraryTest, filterByCategory) +{ + EXPECT_FILTER_RESULTS(kiwix::Filter().category("category_element_overrides_tags"), + "Géographie par Wikipédia", + "Mathématiques" + ); + + EXPECT_FILTER_RESULTS(kiwix::Filter().query("category:category_element_overrides_tags"), + "Géographie par Wikipédia", + "Mathématiques" + ); + + EXPECT_FILTER_RESULTS(kiwix::Filter().query("category_element_overrides_tags"), + /* no results */ + ); +} + +TEST_F(LibraryTest, filterByMaxSize) +{ + EXPECT_FILTER_RESULTS(kiwix::Filter().maxSize(200000), + "An example ZIM archive" + ); +} + +TEST_F(LibraryTest, filterByMultipleCriteria) +{ + EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki").creator("Wikipedia"), + "Encyclopédie de la Tunisie", + "Géographie par Wikipédia", + "Ray Charles" + ); + + EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki").creator("Wikipedia").maxSize(100000000UL), + "Encyclopédie de la Tunisie", + "Ray Charles" + ); + + EXPECT_FILTER_RESULTS(kiwix::Filter().query("Wiki").creator("Wikipedia").maxSize(100000000UL).local(false), + "Encyclopédie de la Tunisie" + ); } TEST_F(LibraryTest, getBookByPath) @@ -284,33 +637,24 @@ TEST_F(LibraryTest, getBookByPath) EXPECT_THROW(lib.getBookByPath("non/existant/path.zim"), std::out_of_range); } -class XmlLibraryTest : public ::testing::Test { - protected: - void SetUp() override { - kiwix::Manager manager(&lib); - manager.readFile( "./test/library.xml", true, true); - } - - kiwix::Library lib; -}; - -TEST_F(XmlLibraryTest, removeBookByIdRemovesTheBook) +TEST_F(LibraryTest, removeBookByIdRemovesTheBook) { - EXPECT_EQ(3U, lib.getBookCount(true, true)); + const auto initialBookCount = lib.getBookCount(true, true); + ASSERT_GT(initialBookCount, 0U); EXPECT_NO_THROW(lib.getBookById("raycharles")); lib.removeBookById("raycharles"); - EXPECT_EQ(2U, lib.getBookCount(true, true)); + EXPECT_EQ(initialBookCount - 1, lib.getBookCount(true, true)); EXPECT_THROW(lib.getBookById("raycharles"), std::out_of_range); }; -TEST_F(XmlLibraryTest, removeBookByIdDropsTheReader) +TEST_F(LibraryTest, removeBookByIdDropsTheReader) { EXPECT_NE(nullptr, lib.getReaderById("raycharles")); lib.removeBookById("raycharles"); EXPECT_THROW(lib.getReaderById("raycharles"), std::out_of_range); }; -TEST_F(XmlLibraryTest, removeBookByIdUpdatesTheSearchDB) +TEST_F(LibraryTest, removeBookByIdUpdatesTheSearchDB) { kiwix::Filter f; f.local(true).valid(true).query(R"(title:"ray charles")", false);