Something went wrong. Try again.
A fork of https://github.com/crosspoint-reader/crosspoint-reader
Something went wrong. Try again.
12 kB · 287 lines
C++
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288#include <gtest/gtest.h>
#include <string>#include <utility>
#include "LibraryText.h"
using library::authorKey;using library::fold;
namespace {
// The same text in both Unicode normal forms. Every fold test runs both, because// a card holding only one form makes a one-sided test pass by accident — which// is exactly how a fold built on utf8ComposeNfc() survives until the first file// arrives from the other kind of machine.struct NormalisationPair { const char* nfc; // precomposed: e-acute is one codepoint const char* nfd; // decomposed: e followed by a combining acute const char* expected;};
constexpr NormalisationPair PAIRS[] = { {"pand\xC3\xA9mie", "pande\xCC\x81mie", "pandemie"}, {"\xC3\x89" "clipse totale", "E\xCC\x81" "clipse totale", "eclipse totale"}, {"M\xC3\xA9moires", "Me\xCC\x81moires", "memoires"}, {"Ang\xC3\xA9lina", "Ange\xCC\x81lina", "angelina"}, {"R\xC3\xA9" "camier", "Re\xCC\x81" "camier", "recamier"}, {"Derri\xC3\xA8re les collines", "Derrie\xCC\x80re les collines", "derriere les collines"},};
} // namespace
TEST(LibraryFold, BothNormalisationsAgree) { for (const auto& p : PAIRS) { EXPECT_EQ(fold(p.nfc), p.expected) << "NFC input: " << p.nfc; EXPECT_EQ(fold(p.nfd), p.expected) << "NFD input: " << p.nfd; EXPECT_EQ(fold(p.nfc), fold(p.nfd)) << "forms disagree for " << p.expected; }}
TEST(LibraryFold, LettersWithoutCanonicalDecomposition) { // These have no NFD form at all, so a decompose-only fold silently deletes // them. "Søren" losing its last letter is the case that motivated the map. EXPECT_EQ(fold("S\xC3\xB8ren"), "soren"); EXPECT_EQ(fold("\xC3\x98rsted"), "orsted"); EXPECT_EQ(fold("\xC3\x86" "sop"), "aesop"); EXPECT_EQ(fold("Stra\xC3\x9F" "e"), "strasse"); EXPECT_EQ(fold("\xC5\x81odz"), "lodz"); EXPECT_EQ(fold("s\xC5\x93ur"), "soeur");}
TEST(LibraryFold, ApostrophesSurviveInNamesAndElisions) { // U+2019 is what exporters actually emit; folding it to a space would split // "O'Malley" into two tokens and change both its sort place and its search. // ("Malley", not "Brien": C++ hex escapes are greedy, so \x99B would parse // as the single escape 0x99B.) EXPECT_EQ(fold("O\xE2\x80\x99Malley"), "o'malley"); EXPECT_EQ(fold("O'Malley"), "o'malley"); EXPECT_EQ(fold("L\xE2\x80\x99\xC3\x89n\xC3\xA9ide"), "l'eneide");}
TEST(LibraryFold, TypographicDashesAndQuotesFoldLikeTheirAsciiForms) { // An em dash used to stay inside the fold word while an ASCII hyphen broke // it, so the same title written both ways sorted and searched differently. EXPECT_EQ(library::fold("a\u2014b"), library::fold("a-b")); EXPECT_EQ(library::fold("a\u2013b"), library::fold("a-b")); EXPECT_EQ(library::fold("\u201Cquoted\u201D"), library::fold("\"quoted\""));}
TEST(LibraryFold, PunctuationSeparatesAndSpaceRunsCollapse) { EXPECT_EQ(fold("Le juge Untel.T2.Le po\xC3\xA8me"), "le juge untel t2 le poeme"); EXPECT_EQ(fold(" spaced out "), "spaced out"); EXPECT_EQ(fold("a---b"), "a b"); EXPECT_EQ(fold("2085 _ Artificial"), "2085 artificial"); EXPECT_EQ(fold(""), ""); EXPECT_EQ(fold("!!!"), "");}
TEST(LibraryFold, PreservesHebrewLettersAndDropsNiqqud) { EXPECT_EQ(fold("\u05E9\u05B8\u05C1\u05DC\u05D5\u05B9\u05DD"), "\u05E9\u05DC\u05D5\u05DD"); EXPECT_TRUE(library::matchesQuery(fold("\u05E9\u05DC\u05D5\u05DD \u05E2\u05D5\u05DC\u05DD"), fold("\u05E9\u05DC"))); EXPECT_FALSE(authorKey("\u05E2\u05DE\u05D5\u05E1 \u05E2\u05D5\u05D6").empty());}
TEST(LibraryFold, GroupInitialUsesUnicodeLettersAndBucketsNumbers) { EXPECT_EQ(library::foldedGroupInitial(fold("Alpha")), static_cast<uint32_t>('a')); EXPECT_EQ(library::foldedGroupInitial(fold("\u05E9\u05DC\u05D5\u05DD")), 0x05E9u); EXPECT_EQ(library::foldedGroupInitial(fold("\u041A\u043D\u0438\u0433\u0430")), 0x041Au); EXPECT_EQ(library::foldedGroupInitial(fold("\u4E66")), 0x4E66u); EXPECT_EQ(library::foldedGroupInitial(fold("2085")), 0u); EXPECT_EQ(library::foldedGroupInitial(fold("\u0662\u0660\u0668\u0665")), 0u); EXPECT_EQ(library::foldedGroupInitial(fold("!!!")), 0u);}
TEST(LibraryFold, LeadingWordsArePreservedForSortingAndSearch) { for (const auto& [title, expected] : { std::pair{"The Iliad", "the iliad"}, std::pair{"A Tale of Two Cities", "a tale of two cities"}, std::pair{"An Ideal Husband", "an ideal husband"}, std::pair{"I Am Number Four", "i am number four"}, std::pair{"I, Robot", "i robot"}, std::pair{"O Pioneers!", "o pioneers"}, std::pair{"Die Trying", "die trying"}, std::pair{"Les Misérables", "les miserables"}, std::pair{"L’Énéide", "l'eneide"}, std::pair{"The", "the"}, }) { const std::string folded = fold(title); EXPECT_EQ(folded, expected) << title; EXPECT_EQ(library::foldedGroupInitial(folded), static_cast<uint32_t>(expected[0])) << title; }}
TEST(LibraryAuthorKey, OrderAndPunctuationDoNotMatter) { const std::string expected = authorKey("Lu Xun"); EXPECT_FALSE(expected.empty()); for (const char* spelling : {"Lu, Xun", "Xun, Lu", "Lu Xun_", "Lu Xun [Xun, Lu]", " lu xun "}) { EXPECT_EQ(authorKey(spelling), expected) << spelling; }}
TEST(LibraryAuthorKey, InitialsAreIgnored) { EXPECT_EQ(authorKey("Herbert G Wells"), authorKey("Herbert Wells")); EXPECT_EQ(authorKey("Wells, Herbert G."), authorKey("Herbert Wells"));}
TEST(LibraryAuthorKey, SecondaryAuthorsAndBracketsDropped) { EXPECT_EQ(authorKey("Emile Erckmann; Alexandre Chatrian"), authorKey("Emile Erckmann")); EXPECT_EQ(authorKey("George Sand [Sand, George]"), authorKey("George Sand"));}
TEST(LibraryAuthorKey, FilesystemUnderscoreStandsInForAFullStop) { // The one input where the key's cleanup and cleanPersonName's differ before // folding: an underscore the filesystem took instead of a full stop. Both // reduce to the same initial, which fold() then drops as a one-letter token. EXPECT_EQ(authorKey("Herbert G_ Wells"), authorKey("Herbert Wells")); EXPECT_EQ(authorKey("Wells_ Herbert"), authorKey("Herbert Wells"));}
TEST(LibraryAuthorKey, DistinctPeopleDoNotCollide) { EXPECT_NE(authorKey("Mary Wollstonecraft"), authorKey("Charlotte Bronte")); EXPECT_NE(authorKey("Victor Hugo"), authorKey("Jules Verne"));}
TEST(LibraryAuthorKey, FitsTheRecordFieldWithoutCollapsingToAForename) { const std::string key = authorKey("Bartholomew Fitzgerald Wellington"); ASSERT_FALSE(key.empty()); EXPECT_LE(key.size(), library::AUTHOR_KEY_MAX_BYTES); EXPECT_NE(key.back(), ' ');
// Sorting puts a short forename first, so cutting on a token boundary would // reduce this to "mary" and merge every Alex in the library. The byte cut must // keep enough of the surname to discriminate. const std::string mary = authorKey("Wollstonecraft, Mary"); EXPECT_GT(mary.size(), 5u); EXPECT_NE(mary, "mary"); EXPECT_NE(mary, authorKey("Mary Trevelyan"));
// A truncated key stays a prefix of the untruncated one, so grouping is stable // however long the name is. EXPECT_EQ(authorKey("Wollstonecraft, Maryse").rfind("mary", 0), 0u);
EXPECT_FALSE(authorKey("Nebuchadnezzarson").empty()); EXPECT_TRUE(authorKey("").empty()); EXPECT_TRUE(authorKey("Q. X. Z.").empty()); // initials only: no identity}
// --- matchesQuery ------------------------------------------------------------//// Cases taken from the shape of the accented and// apostrophised titles real cards hold — what a naive matcher gets wrong.
TEST(MatchesQuery, EmptyQueryMatchesEverything) { EXPECT_TRUE(library::matchesQuery(library::fold("Wuthering Heights"), ""));}
TEST(MatchesQuery, WholeWordMatches) { EXPECT_TRUE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("heights")));}
TEST(MatchesQuery, PrefixOfOneWordIsEnough) { EXPECT_TRUE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("hei")));}
// The point of the whole design: six keypresses instead of ten, on a panel where// each one costs a full repaint.TEST(MatchesQuery, EveryWordMayBeAbbreviated) { EXPECT_TRUE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("wut hei")));}
TEST(MatchesQuery, WordsNeedNotBeInOrder) { EXPECT_TRUE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("heights wuthering")));}
TEST(MatchesQuery, EveryWordMustHit) { EXPECT_FALSE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("wuthering blue")));}
// A prefix, not a substring: "eights" is inside "heights" but starts no word.TEST(MatchesQuery, MidWordDoesNotMatch) { EXPECT_FALSE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("eights")));}
TEST(MatchesQuery, AccentsAreIgnoredOnBothSides) { EXPECT_TRUE(library::matchesQuery(library::fold("L'Énéide"), library::fold("eneide"))); EXPECT_TRUE(library::matchesQuery(library::fold("L'Eneide"), library::fold("énéide"))); EXPECT_TRUE(library::matchesQuery(library::fold("Éluard"), library::fold("eluard")));}
TEST(MatchesQuery, ApostropheSplitsWords) { EXPECT_TRUE(library::matchesQuery(library::fold("Le bureau d'à côté"), library::fold("cote")));}
TEST(MatchesQuery, CaseIsIgnored) { EXPECT_TRUE(library::matchesQuery(library::fold("Wuthering Heights"), library::fold("HEIGHTS")));}
// The stored fold is capped at 96 bytes, so a query word beyond that cannot be// found. Asserted rather than left implicit: it is the one place a search can// honestly fail to find a book that is really there.TEST(MatchesQuery, LongTitlesAreOnlySearchableWithinTheStoredFold) { const std::string longTitle(120, 'a'); const std::string folded = library::fold(longTitle + " needle").substr(0, 96); EXPECT_FALSE(library::matchesQuery(folded, library::fold("needle")));}
// --- inverted author names ---------------------------------------------------
TEST(CleanPersonName, InvertedNameIsTurnedRound) { EXPECT_EQ(library::cleanPersonName("Austen, Jane"), "Jane Austen"); EXPECT_EQ(library::cleanPersonName("Wollstonecraft, Mary"), "Mary Wollstonecraft");}
TEST(CleanPersonName, PlainNameIsUntouched) { EXPECT_EQ(library::cleanPersonName("Emily Bronte"), "Emily Bronte"); }
// Two commas mean a suffix or a list, not an inversion — leave it alone rather// than scramble it.TEST(CleanPersonName, MultipleCommasAreLeftAlone) { // The trailing full stop is stripped by the existing noise rules. EXPECT_EQ(library::cleanPersonName("Smith, John, Jr."), "Smith, John, Jr");}
TEST(CleanPersonName, DanglingCommaIsNotAnInversion) { EXPECT_EQ(library::cleanPersonName("Austen,"), "Austen"); }
// --- surnameKey --------------------------------------------------------------
TEST(SurnameKey, SurnameLeadsThenGivenNames) { EXPECT_EQ(library::surnameKey("Herman Melville"), "melville herman"); EXPECT_EQ(library::surnameKey("Mary Wollstonecraft"), "wollstonecraft mary");}
TEST(SurnameKey, SingleWordKeysOnItself) { EXPECT_EQ(library::surnameKey("Voltaire"), "voltaire"); }
TEST(SurnameKey, AccentsAreFolded) { EXPECT_EQ(library::surnameKey("Paul Éluard"), "eluard paul"); }
TEST(SurnameKey, ThreeWordNamesTakeTheLast) { EXPECT_EQ(library::surnameKey("Herbert G. Wells"), "wells herbert g"); }
TEST(SurnameKey, EmptyStaysEmpty) { EXPECT_EQ(library::surnameKey(""), ""); }
// The whole point of keying off the DISPLAY name: the spelling vote has already// made every book by one author show one name, so a group cannot land in two// places even though "Victor Hugo" and "Hugo Victor" both exist in the wild.TEST(SurnameKey, HarmonisedDisplayNameKeepsAGroupTogether) { EXPECT_NE(library::surnameKey("Victor Hugo"), library::surnameKey("Hugo Victor"));}
TEST(LibraryPath, RootDoesNotGainASecondSeparator) { EXPECT_EQ(library::joinLibraryPath("/", "book.epub"), "/book.epub"); EXPECT_EQ(library::joinLibraryPath("", "book.epub"), "/book.epub");}
TEST(LibraryPath, NestedFolderGetsOneSeparator) { EXPECT_EQ(library::joinLibraryPath("/Books", "book.epub"), "/Books/book.epub");}