// Copyright (c) Microsoft Corporation. // Licensed under the MIT license. #include "precomp.h" #include "..\TerminalApp\fzf\fzf.h" using namespace Microsoft::Console; using namespace WEX::Logging; using namespace WEX::TestExecution; using namespace WEX::Common; namespace TerminalAppUnitTests { typedef enum { ScoreMatch = 16, ScoreGapStart = -3, ScoreGapExtension = -1, BonusBoundary = ScoreMatch / 2, BonusNonWord = ScoreMatch / 2, BonusCamel123 = BonusBoundary + ScoreGapExtension, BonusConsecutive = -(ScoreGapStart + ScoreGapExtension), BonusFirstCharMultiplier = 2, } score_t; class FzfTests { BEGIN_TEST_CLASS(FzfTests) END_TEST_CLASS() TEST_METHOD(AllPatternCharsDoNotMatch); TEST_METHOD(ConsecutiveChars); TEST_METHOD(ConsecutiveChars_FirstCharBonus); TEST_METHOD(NonWordBonusBoundary_ConsecutiveChars); TEST_METHOD(MatchOnNonWordChars_CaseInSensitive); TEST_METHOD(MatchOnNonWordCharsWithGap); TEST_METHOD(BonusForCamelCaseMatch); TEST_METHOD(BonusBoundaryAndFirstCharMultiplier); TEST_METHOD(MatchesAreCaseInSensitive); TEST_METHOD(MultipleTerms); TEST_METHOD(MultipleTerms_AllCharsMatch); TEST_METHOD(MultipleTerms_NotAllTermsMatch); TEST_METHOD(MatchesAreCaseInSensitive_BonusBoundary); TEST_METHOD(TraceBackWillPickTheFirstMatchIfBothHaveTheSameScore); TEST_METHOD(TraceBackWillPickTheMatchWithTheHighestScore); TEST_METHOD(TraceBackWillPickTheMatchWithTheHighestScore_Gaps); TEST_METHOD(TraceBackWillPickEarlierCharsWhenNoBonus); TEST_METHOD(MatchWithGapCanAHaveHigherScoreThanConsecutiveWhenGapMatchHasBoundaryBonus); TEST_METHOD(ConsecutiveMatchWillScoreHigherThanMatchWithGapWhenBothHaveFirstCharBonus); TEST_METHOD(ConsecutiveMatchWillScoreHigherThanMatchWithGapWhenBothDontHaveBonus); TEST_METHOD(MatchWithGapCanHaveHigherScoreThanConsecutiveWhenGapHasFirstCharBonus); TEST_METHOD(MatchWithGapThatMatchesOnTheFirstCharWillNoLongerScoreHigherThanConsecutiveCharsWhenTheGapIs3_NoConsecutiveChar_4CharPattern); TEST_METHOD(MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs11_2CharPattern); TEST_METHOD(MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs11_3CharPattern_1ConsecutiveChar); TEST_METHOD(MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs5_NoConsecutiveChars_3CharPattern); TEST_METHOD(Russian_CaseMisMatch); TEST_METHOD(Russian_CaseMatch); TEST_METHOD(English_CaseMatch); TEST_METHOD(English_CaseMisMatch); TEST_METHOD(SurrogatePair); TEST_METHOD(French_CaseMatch); TEST_METHOD(French_CaseMisMatch); TEST_METHOD(German_CaseMatch); TEST_METHOD(German_CaseMisMatch_FoldResultsInMultipleCodePoints); TEST_METHOD(Greek_CaseMisMatch); TEST_METHOD(Greek_CaseMatch); TEST_METHOD(SurrogatePair_ToUtf16Pos_ConsecutiveChars); TEST_METHOD(SurrogatePair_ToUtf16Pos_PreferConsecutiveChars); TEST_METHOD(SurrogatePair_ToUtf16Pos_GapAndBoundary); }; void AssertScoreAndRuns(std::wstring_view patternText, std::wstring_view text, int expectedScore, const std::vector& expectedRuns) { const auto pattern = fzf::matcher::ParsePattern(patternText); const auto match = fzf::matcher::Match(text, pattern); if (expectedScore == 0 && expectedRuns.empty()) { VERIFY_ARE_EQUAL(std::nullopt, match); return; } VERIFY_IS_TRUE(match.has_value()); VERIFY_ARE_EQUAL(expectedScore, match->Score); const auto& runs = match->Runs; VERIFY_ARE_EQUAL(expectedRuns.size(), runs.size()); for (size_t i = 0; i < expectedRuns.size(); ++i) { VERIFY_ARE_EQUAL(expectedRuns[i].Start, runs[i].Start); VERIFY_ARE_EQUAL(expectedRuns[i].End, runs[i].End); } } void FzfTests::AllPatternCharsDoNotMatch() { AssertScoreAndRuns( L"fbb", L"foo bar", 0, {}); } void FzfTests::ConsecutiveChars() { AssertScoreAndRuns( L"oba", L"foobar", ScoreMatch * 3 + BonusConsecutive * 2, { { 2, 4 } }); } void FzfTests::ConsecutiveChars_FirstCharBonus() { AssertScoreAndRuns( L"foo", L"foobar", ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2, { { 0, 2 } }); } void FzfTests::NonWordBonusBoundary_ConsecutiveChars() { AssertScoreAndRuns( L"zshc", L"/man1/zshcompctl.1", ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + BonusFirstCharMultiplier * BonusConsecutive * 3, { { 6, 9 } }); } void FzfTests::Russian_CaseMisMatch() { AssertScoreAndRuns( L"новая", L"Новая вкладка", ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4, { { 0, 4 } }); } void FzfTests::Russian_CaseMatch() { AssertScoreAndRuns( L"Новая", L"Новая вкладка", ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4, { { 0, 4 } }); } void FzfTests::German_CaseMatch() { AssertScoreAndRuns( L"fuß", L"Fußball", ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2, { { 0, 2 } }); } void FzfTests::German_CaseMisMatch_FoldResultsInMultipleCodePoints() { //This doesn't currently pass, I think ucase_toFullFolding would give the number of code points that resulted from the fold. //I wasn't sure how to reference that BEGIN_TEST_METHOD_PROPERTIES() TEST_METHOD_PROPERTY(L"Ignore", L"true") END_TEST_METHOD_PROPERTIES() AssertScoreAndRuns( L"fuss", L"Fußball", //I think ScoreMatch * 4 is correct in this case since it matches 4 codepoints pattern??? fuss ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 3, //Only 3 positions in the text were matched { { 0, 2 } }); } void FzfTests::French_CaseMatch() { AssertScoreAndRuns( L"Éco", L"École", ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2, { { 0, 2 } }); } void FzfTests::French_CaseMisMatch() { AssertScoreAndRuns( L"Éco", L"école", ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2, { { 0, 2 } }); } void FzfTests::Greek_CaseMatch() { AssertScoreAndRuns( L"λόγος", L"λόγος", ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4, { { 0, 4 } }); } void FzfTests::Greek_CaseMisMatch() { //I think this tests validates folding (σ, ς) AssertScoreAndRuns( L"λόγοσ", L"λόγος", ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4, { { 0, 4 } }); } void FzfTests::English_CaseMatch() { AssertScoreAndRuns( L"Newer", L"Newer tab", ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4, { { 0, 4 } }); } void FzfTests::English_CaseMisMatch() { AssertScoreAndRuns( L"newer", L"Newer tab", ScoreMatch * 5 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 4, { { 0, 4 } }); } void FzfTests::SurrogatePair() { AssertScoreAndRuns( L"N😀ewer", L"N😀ewer tab", ScoreMatch * 6 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 5, { { 0, 6 } }); } void FzfTests::SurrogatePair_ToUtf16Pos_ConsecutiveChars() { AssertScoreAndRuns( L"N𠀋N😀𝄞e𐐷", L"N𠀋N😀𝄞e𐐷 tab", ScoreMatch * 7 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 6, { { 0, 10 } }); } void FzfTests::SurrogatePair_ToUtf16Pos_PreferConsecutiveChars() { AssertScoreAndRuns( L"𠀋😀", L"N𠀋😀wer 😀b𐐷 ", ScoreMatch * 2 + BonusConsecutive * 2, { { 1, 4 } }); } void FzfTests::SurrogatePair_ToUtf16Pos_GapAndBoundary() { AssertScoreAndRuns( L"𠀋😀", L"N𠀋wer 😀b𐐷 ", ScoreMatch * 2 + ScoreGapStart + ScoreGapExtension * 3 + BonusBoundary, { { 1, 2 }, { 7, 8 } }); } void FzfTests::MatchOnNonWordChars_CaseInSensitive() { AssertScoreAndRuns( L"foo-b", L"xFoo-Bar Baz", (ScoreMatch + BonusCamel123 * BonusFirstCharMultiplier) + (ScoreMatch + BonusCamel123) + (ScoreMatch + BonusCamel123) + (ScoreMatch + BonusBoundary) + (ScoreMatch + BonusNonWord), { { 1, 5 } }); } void FzfTests::MatchOnNonWordCharsWithGap() { AssertScoreAndRuns( L"12356", L"abc123 456", (ScoreMatch + BonusCamel123 * BonusFirstCharMultiplier) + (ScoreMatch + BonusCamel123) + (ScoreMatch + BonusCamel123) + ScoreGapStart + ScoreGapExtension + ScoreMatch + ScoreMatch + BonusConsecutive, { { 3, 5 }, { 8, 9 } }); } void FzfTests::BonusForCamelCaseMatch() { AssertScoreAndRuns( L"def56", L"abcDEF 456", (ScoreMatch + BonusCamel123 * BonusFirstCharMultiplier) + (ScoreMatch + BonusCamel123) + (ScoreMatch + BonusCamel123) + ScoreGapStart + ScoreGapExtension + ScoreMatch + (ScoreMatch + BonusConsecutive), { { 3, 5 }, { 8, 9 } }); } void FzfTests::BonusBoundaryAndFirstCharMultiplier() { AssertScoreAndRuns( L"fbb", L"foo bar baz", ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusBoundary * 2 + 2 * ScoreGapStart + 4 * ScoreGapExtension, { { 0, 0 }, { 4, 4 }, { 8, 8 } }); } void FzfTests::MatchesAreCaseInSensitive() { AssertScoreAndRuns( L"FBB", L"foo bar baz", ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusBoundary * 2 + 2 * ScoreGapStart + 4 * ScoreGapExtension, { { 0, 0 }, { 4, 4 }, { 8, 8 } }); } void FzfTests::MultipleTerms() { auto term1Score = ScoreMatch * 2 + BonusBoundary * BonusFirstCharMultiplier + (BonusFirstCharMultiplier * BonusConsecutive); auto term2Score = ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + (BonusFirstCharMultiplier * BonusConsecutive) * 3; AssertScoreAndRuns( L"sp anta", L"Split Pane, split: horizontal, profile: SSH: Antares", term1Score + term2Score, { { 0, 1 }, { 45, 48 } }); } void FzfTests::MultipleTerms_AllCharsMatch() { auto term1Score = ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + (BonusFirstCharMultiplier * BonusConsecutive * 2); auto term2Score = term1Score; AssertScoreAndRuns( L"foo bar", L"foo bar", term1Score + term2Score, { { 0, 2 }, { 4, 6 } }); } void FzfTests::MultipleTerms_NotAllTermsMatch() { AssertScoreAndRuns( L"sp anta zz", L"Split Pane, split: horizontal, profile: SSH: Antares", 0, {}); } void FzfTests::MatchesAreCaseInSensitive_BonusBoundary() { AssertScoreAndRuns( L"fbb", L"Foo Bar Baz", ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusBoundary * 2 + 2 * ScoreGapStart + 4 * ScoreGapExtension, { { 0, 0 }, { 4, 4 }, { 8, 8 } }); } void FzfTests::TraceBackWillPickTheFirstMatchIfBothHaveTheSameScore() { AssertScoreAndRuns( L"bar", L"Foo Bar Bar", (ScoreMatch + BonusBoundary * BonusFirstCharMultiplier) + (ScoreMatch + BonusBoundary) + (ScoreMatch + BonusBoundary), //ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier * 2, { { 4, 6 } }); } void FzfTests::TraceBackWillPickTheMatchWithTheHighestScore() { AssertScoreAndRuns( L"bar", L"Foo aBar Bar", ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier * 2, { { 9, 11 } }); } void FzfTests::TraceBackWillPickTheMatchWithTheHighestScore_Gaps() { AssertScoreAndRuns( L"bar", L"Boo Author Raz Bar", ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive * BonusFirstCharMultiplier * 2, { { 15, 17 } }); } void FzfTests::TraceBackWillPickEarlierCharsWhenNoBonus() { AssertScoreAndRuns( L"clts", L"close all tabs after this", ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + BonusFirstCharMultiplier * BonusConsecutive + ScoreGapStart + ScoreGapExtension * 7 + BonusBoundary + ScoreGapStart + ScoreGapExtension, { { 0, 1 }, { 10, 10 }, { 13, 13 } }); } void FzfTests::ConsecutiveMatchWillScoreHigherThanMatchWithGapWhenBothDontHaveBonus() { auto consecutiveScore = ScoreMatch * 3 + BonusConsecutive * 2; auto gapScore = (ScoreMatch * 3) + ScoreGapStart + ScoreGapStart; AssertScoreAndRuns( L"oob", L"aoobar", consecutiveScore, { { 1, 3 } }); AssertScoreAndRuns( L"oob", L"aoaoabound", gapScore, { { 1, 1 }, { 3, 3 }, { 5, 5 } }); VERIFY_IS_GREATER_THAN(consecutiveScore, gapScore); } void FzfTests::ConsecutiveMatchWillScoreHigherThanMatchWithGapWhenBothHaveFirstCharBonus() { auto consecutiveScore = ScoreMatch * 3 + BonusFirstCharMultiplier * BonusBoundary + BonusFirstCharMultiplier * BonusConsecutive * 2; auto gapScore = (ScoreMatch * 3) + (BonusBoundary * BonusFirstCharMultiplier) + ScoreGapStart + ScoreGapStart; AssertScoreAndRuns( L"oob", L"oobar", consecutiveScore, { { 0, 2 } }); AssertScoreAndRuns( L"oob", L"oaoabound", gapScore, { { 0, 0 }, { 2, 2 }, { 4, 4 } }); VERIFY_IS_GREATER_THAN(consecutiveScore, gapScore); } void FzfTests::MatchWithGapCanAHaveHigherScoreThanConsecutiveWhenGapMatchHasBoundaryBonus() { auto consecutiveScore = ScoreMatch * 3 + BonusConsecutive * 2; auto gapScore = (ScoreMatch * 3) + (BonusBoundary * BonusFirstCharMultiplier) + (BonusBoundary * 2) + ScoreGapStart + (ScoreGapExtension * 2) + ScoreGapStart + ScoreGapExtension; AssertScoreAndRuns( L"oob", L"foobar", consecutiveScore, { { 1, 3 } }); AssertScoreAndRuns( L"oob", L"out-of-bound", gapScore, { { 0, 0 }, { 4, 4 }, { 7, 7 } }); VERIFY_IS_GREATER_THAN(gapScore, consecutiveScore); } void FzfTests::MatchWithGapCanHaveHigherScoreThanConsecutiveWhenGapHasFirstCharBonus() { auto consecutiveScore = ScoreMatch * 2 + BonusConsecutive; auto gapScore = ScoreMatch * 2 + BonusBoundary * BonusFirstCharMultiplier + ScoreGapStart; AssertScoreAndRuns( L"ob", L"aobar", consecutiveScore, { { 1, 2 } }); AssertScoreAndRuns( L"ob", L"oabar", gapScore, { { 0, 0 }, { 2, 2 } }); VERIFY_IS_GREATER_THAN(gapScore, consecutiveScore); } void FzfTests::MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs11_2CharPattern() { auto consecutiveScore = ScoreMatch * 2 + BonusConsecutive; auto gapScore = ScoreMatch * 2 + BonusBoundary * BonusFirstCharMultiplier + ScoreGapStart + ScoreGapExtension * 10; AssertScoreAndRuns( L"ob", L"aobar", consecutiveScore, { { 1, 2 } }); AssertScoreAndRuns( L"ob", L"oaaaaaaaaaaabar", gapScore, { { 0, 0 }, { 12, 12 } }); VERIFY_IS_GREATER_THAN(consecutiveScore, gapScore); } void FzfTests::MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs11_3CharPattern_1ConsecutiveChar() { auto consecutiveScore = ScoreMatch * 3 + BonusConsecutive * 2; auto gapScore = ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + BonusConsecutive + ScoreGapStart + ScoreGapExtension * 10; AssertScoreAndRuns( L"oba", L"aobar", consecutiveScore, { { 1, 3 } }); AssertScoreAndRuns( L"oba", L"oaaaaaaaaaaabar", gapScore, { { 0, 0 }, { 12, 13 } }); VERIFY_IS_GREATER_THAN(consecutiveScore, gapScore); } void FzfTests::MatchWithGapThatMatchesOnTheFirstCharWillNoLongerHigherScoreThanConsecutiveCharsWhenTheGapIs5_NoConsecutiveChars_3CharPattern() { auto allConsecutiveScore = ScoreMatch * 3 + BonusConsecutive * 2; auto allBoundaryWithGapScore = ScoreMatch * 3 + BonusBoundary * BonusFirstCharMultiplier + ScoreGapStart + ScoreGapExtension + ScoreGapExtension + ScoreGapStart + ScoreGapExtension; AssertScoreAndRuns( L"oba", L"aobar", allConsecutiveScore, { { 1, 3 } }); AssertScoreAndRuns( L"oba", L"oaaabzzar", allBoundaryWithGapScore, { { 0, 0 }, { 4, 4 }, { 7, 7 } }); VERIFY_IS_GREATER_THAN(allConsecutiveScore, allBoundaryWithGapScore); } void FzfTests::MatchWithGapThatMatchesOnTheFirstCharWillNoLongerScoreHigherThanConsecutiveCharsWhenTheGapIs3_NoConsecutiveChar_4CharPattern() { auto consecutiveScore = ScoreMatch * 4 + BonusConsecutive * 3; auto gapScore = ScoreMatch * 4 + BonusBoundary * BonusFirstCharMultiplier + ScoreGapStart * 3; AssertScoreAndRuns( L"obar", L"aobar", consecutiveScore, { { 1, 4 } }); AssertScoreAndRuns( L"obar", L"oabzazr", gapScore, { { 0, 0 }, { 2, 2 }, { 4, 4 }, { 6, 6 } }); VERIFY_IS_GREATER_THAN(consecutiveScore, gapScore); } }