From c169c719aa2b8f9b9fd86e08d53ea024108776c9 Mon Sep 17 00:00:00 2001 From: Tor Egge Date: Wed, 29 May 2024 16:29:11 +0200 Subject: Rewrite match loop communicator unit test to gtest. --- .../match_loop_communicator/CMakeLists.txt | 1 + .../match_loop_communicator_test.cpp | 277 +++++++++++++-------- searchlib/src/vespa/searchlib/queryeval/scores.h | 3 + 3 files changed, 179 insertions(+), 102 deletions(-) diff --git a/searchcore/src/tests/proton/matching/match_loop_communicator/CMakeLists.txt b/searchcore/src/tests/proton/matching/match_loop_communicator/CMakeLists.txt index b545023ce97..b5b71836581 100644 --- a/searchcore/src/tests/proton/matching/match_loop_communicator/CMakeLists.txt +++ b/searchcore/src/tests/proton/matching/match_loop_communicator/CMakeLists.txt @@ -4,5 +4,6 @@ vespa_add_executable(searchcore_match_loop_communicator_test_app TEST match_loop_communicator_test.cpp DEPENDS searchcore_matching + GTest::gtest ) vespa_add_test(NAME searchcore_match_loop_communicator_test_app COMMAND searchcore_match_loop_communicator_test_app) diff --git a/searchcore/src/tests/proton/matching/match_loop_communicator/match_loop_communicator_test.cpp b/searchcore/src/tests/proton/matching/match_loop_communicator/match_loop_communicator_test.cpp index 5994385b4aa..759c4cefafe 100644 --- a/searchcore/src/tests/proton/matching/match_loop_communicator/match_loop_communicator_test.cpp +++ b/searchcore/src/tests/proton/matching/match_loop_communicator/match_loop_communicator_test.cpp @@ -1,8 +1,8 @@ // Copyright Vespa.ai. Licensed under the terms of the Apache 2.0 license. See LICENSE in the project root. -#include -#include #include #include +#include +#include #include using namespace proton::matching; @@ -16,6 +16,15 @@ using TaggedHit = MatchLoopCommunicator::TaggedHit; using TaggedHits = MatchLoopCommunicator::TaggedHits; using search::features::FirstPhaseRankLookup; using search::queryeval::SortedHitSequence; +using vespalib::test::Nexus; + +namespace search::queryeval { + +void PrintTo(const Scores& scores, std::ostream* os) { + *os << "{" << scores.low << "," << scores.high << "}"; +} + +} std::vector hit_vec(std::vector list) { return list; } @@ -30,6 +39,13 @@ Hits makeScores(size_t id) { return {}; } +Hits make_first_scores(size_t id, size_t size) { + auto result = makeScores(id); + EXPECT_LE(size, result.size()); + result.resize(size); + return result; +} + std::tuple second_phase(MatchLoopCommunicator &com, const Hits &hits, size_t thread_id, double delta = 0.0) { std::vector refs; for (size_t i = 0; i < hits.size(); ++i) { @@ -63,25 +79,6 @@ size_t my_work_size(MatchLoopCommunicator &com, const Hits &hits, size_t thread_ return work_size; } -void equal(size_t count, const Hits & a, const Hits & b) { - EXPECT_EQUAL(count, b.size()); - for (size_t i(0); i < count; i++) { - EXPECT_EQUAL(a[i].first, b[i].first); - EXPECT_EQUAL(a[i].second , b[i].second); - } -} - -void equal_range(const Range &a, const Range &b) { - EXPECT_EQUAL(a.isValid(), b.isValid()); - EXPECT_EQUAL(a.low, b.low); - EXPECT_EQUAL(a.high, b.high); -} - -void equal_ranges(const RangePair &a, const RangePair &b) { - TEST_DO(equal_range(a.first, b.first)); - TEST_DO(equal_range(a.second, b.second)); -} - struct EveryOdd : public search::queryeval::IDiversifier { bool accepted(uint32_t docId) override { return docId & 0x01; @@ -92,122 +89,196 @@ struct None : public search::queryeval::IDiversifier { bool accepted(uint32_t) override { return false; } }; -TEST_F("require that selectBest gives appropriate results for single thread", MatchLoopCommunicator(num_threads, 3)) { - TEST_DO(equal(2u, hit_vec({{1, 5}, {2, 4}}), selectBest(f1, hit_vec({{1, 5}, {2, 4}}), thread_id))); - TEST_DO(equal(3u, hit_vec({{1, 5}, {2, 4}, {3, 3}}), selectBest(f1, hit_vec({{1, 5}, {2, 4}, {3, 3}}), thread_id))); - TEST_DO(equal(3u, hit_vec({{1, 5}, {2, 4}, {3, 3}}), selectBest(f1, hit_vec({{1, 5}, {2, 4}, {3, 3}, {4, 2}}), thread_id))); +TEST(MatchLoopCommunicatorTest, require_that_selectBest_gives_appropriate_results_for_single_thread) +{ + constexpr size_t num_threads = 1; + constexpr size_t thread_id = 0; + MatchLoopCommunicator f1(num_threads, 3); + EXPECT_EQ(hit_vec({{1, 5}, {2, 4}}), selectBest(f1, hit_vec({{1, 5}, {2, 4}}), thread_id)); + EXPECT_EQ(hit_vec({{1, 5}, {2, 4}, {3, 3}}), selectBest(f1, hit_vec({{1, 5}, {2, 4}, {3, 3}}), thread_id)); + EXPECT_EQ(hit_vec({{1, 5}, {2, 4}, {3, 3}}), selectBest(f1, hit_vec({{1, 5}, {2, 4}, {3, 3}, {4, 2}}), thread_id)); } -TEST_F("require that selectBest gives appropriate results for single thread with filter", - MatchLoopCommunicator(num_threads, 3, std::make_unique(), nullptr)) +TEST(MatchLoopCommunicatorTest, require_that_selectBest_gives_appropriate_results_for_single_thread_with_filter) { - TEST_DO(equal(1u, hit_vec({{1, 5}}), selectBest(f1, hit_vec({{1, 5}, {2, 4}}), thread_id))); - TEST_DO(equal(2u, hit_vec({{1, 5}, {3, 3}}), selectBest(f1, hit_vec({{1, 5}, {2, 4}, {3, 3}}), thread_id))); - TEST_DO(equal(3u, hit_vec({{1, 5}, {3, 3}, {5, 1}}), selectBest(f1, hit_vec({{1, 5}, {2, 4}, {3, 3}, {4, 2}, {5, 1}, {6, 0}}), thread_id))); + constexpr size_t num_threads = 1; + constexpr size_t thread_id = 0; + MatchLoopCommunicator f1(num_threads, 3, std::make_unique(), nullptr); + EXPECT_EQ(hit_vec({{1, 5}}), selectBest(f1, hit_vec({{1, 5}, {2, 4}}), thread_id)); + EXPECT_EQ(hit_vec({{1, 5}, {3, 3}}), selectBest(f1, hit_vec({{1, 5}, {2, 4}, {3, 3}}), thread_id)); + EXPECT_EQ(hit_vec({{1, 5}, {3, 3}, {5, 1}}), selectBest(f1, hit_vec({{1, 5}, {2, 4}, {3, 3}, {4, 2}, {5, 1}, {6, 0}}), thread_id)); } -TEST_MT_F("require that selectBest works with no hits", 10, MatchLoopCommunicator(num_threads, 10)) { - EXPECT_TRUE(selectBest(f1, hit_vec({}), thread_id).empty()); +TEST(MatchLoopCommunicatorTest, require_that_selectBest_works_with_no_hits) +{ + constexpr size_t num_threads = 10; + MatchLoopCommunicator f1(num_threads, 10); + auto task = [&f1](Nexus& ctx) { + EXPECT_TRUE(selectBest(f1, hit_vec({}), ctx.thread_id()).empty()); + }; + Nexus::run(num_threads, task); } -TEST_MT_F("require that selectBest works with too many hits from all threads", 5, MatchLoopCommunicator(num_threads, 13)) { - if (thread_id < 3) { - TEST_DO(equal(3u, makeScores(thread_id), selectBest(f1, makeScores(thread_id), thread_id))); - } else { - TEST_DO(equal(2u, makeScores(thread_id), selectBest(f1, makeScores(thread_id), thread_id))); - } +TEST(MatchLoopCommunicatorTest, require_that_selectBest_works_with_too_many_hits_from_all_threads) +{ + constexpr size_t num_threads = 5; + MatchLoopCommunicator f1(num_threads, 13); + auto task = [&f1](Nexus& ctx) { + auto thread_id = ctx.thread_id(); + if (thread_id < 3) { + EXPECT_EQ(make_first_scores(thread_id, 3), selectBest(f1, makeScores(thread_id), thread_id)); + } else { + EXPECT_EQ(make_first_scores(thread_id, 2), selectBest(f1, makeScores(thread_id), thread_id)); + } + }; + Nexus::run(num_threads, task); } -TEST_MT_F("require that selectBest works with some exhausted threads", 5, MatchLoopCommunicator(num_threads, 22)) { - if (thread_id < 2) { - TEST_DO(equal(5u, makeScores(thread_id), selectBest(f1, makeScores(thread_id), thread_id))); - } else { - TEST_DO(equal(4u, makeScores(thread_id), selectBest(f1, makeScores(thread_id), thread_id))); - } +TEST(MatchLoopCommunicatorTest, require_that_selectBest_works_with_some_exhausted_threads) +{ + constexpr size_t num_threads = 5; + MatchLoopCommunicator f1(num_threads, 22); + auto task = [&f1](Nexus& ctx) { + auto thread_id = ctx.thread_id(); + if (thread_id < 2) { + EXPECT_EQ(makeScores(thread_id), selectBest(f1, makeScores(thread_id), thread_id)); + } else { + EXPECT_EQ(make_first_scores(thread_id, 4), selectBest(f1, makeScores(thread_id), thread_id)); + } + }; + Nexus::run(num_threads, task); } -TEST_MT_F("require that selectBest can select all hits from all threads", 5, MatchLoopCommunicator(num_threads, 100)) { - EXPECT_EQUAL(5u, selectBest(f1, makeScores(thread_id), thread_id).size()); +TEST(MatchLoopCommunicatorTest, require_that_selectBest_can_select_all_hits_from_all_threads) +{ + constexpr size_t num_threads = 5; + MatchLoopCommunicator f1(num_threads, 100); + auto task = [&f1](Nexus& ctx) { + auto thread_id = ctx.thread_id(); + EXPECT_EQ(5u, selectBest(f1, makeScores(thread_id), thread_id).size()); + }; + Nexus::run(num_threads, task); } -TEST_MT_F("require that selectBest works with some empty threads", 10, MatchLoopCommunicator(num_threads, 7)) { - if (thread_id < 2) { - TEST_DO(equal(2u, makeScores(thread_id), selectBest(f1, makeScores(thread_id), thread_id))); - } else if (thread_id < 5) { - TEST_DO(equal(1u, makeScores(thread_id), selectBest(f1, makeScores(thread_id), thread_id))); - } else { - EXPECT_TRUE(selectBest(f1, makeScores(thread_id), thread_id).empty()); - } +TEST(MatchLoopCommunicatorTest, require_that_selectBest_works_with_some_empty_threads) +{ + constexpr size_t num_threads = 5; + MatchLoopCommunicator f1(num_threads, 7); + auto task = [&f1](Nexus& ctx) { + auto thread_id = ctx.thread_id(); + if (thread_id < 2) { + EXPECT_EQ(make_first_scores(thread_id, 2), selectBest(f1, makeScores(thread_id), thread_id)); + } else if (thread_id < 5) { + EXPECT_EQ(make_first_scores(thread_id, 1), selectBest(f1, makeScores(thread_id), thread_id)); + } else { + EXPECT_TRUE(selectBest(f1, makeScores(thread_id), thread_id).empty()); + } + }; + Nexus::run(num_threads, task); } -TEST_F("require that rangeCover works with a single thread", MatchLoopCommunicator(num_threads, 5)) { +TEST(MatchLoopCommunicatorTest, require_that_rangeCover_works_with_a_single_thread) +{ + constexpr size_t num_threads = 1; + constexpr size_t thread_id = 0; + MatchLoopCommunicator f1(num_threads, 5); RangePair res = rangeCover(f1, hit_vec({{1, 7.5}, {2, 1.5}}), thread_id, 10); - TEST_DO(equal_ranges(RangePair({1.5, 7.5}, {11.5, 17.5}), res)); + EXPECT_EQ(RangePair({1.5, 7.5}, {11.5, 17.5}), res); } -TEST_MT_F("require that rangeCover works with multiple threads", 5, MatchLoopCommunicator(num_threads, 10)) { - RangePair res = rangeCover(f1, hit_vec({{thread_id * 100 + 1, 100.0 + thread_id}, {thread_id * 100 + 2, 100.0 - thread_id}}), thread_id, 10); - TEST_DO(equal_ranges(RangePair({96.0, 104.0}, {106.0, 114.0}), res)); +TEST(MatchLoopCommunicatorTest, require_that_rangeCover_works_with_multiple_threads) +{ + constexpr size_t num_threads = 5; + MatchLoopCommunicator f1(num_threads, 10); + auto task = [&f1](Nexus& ctx) { + auto thread_id = ctx.thread_id(); + RangePair res = rangeCover(f1, hit_vec({{thread_id * 100 + 1, 100.0 + thread_id}, {thread_id * 100 + 2, 100.0 - thread_id}}), thread_id, 10); + EXPECT_EQ(RangePair({96.0, 104.0}, {106.0, 114.0}), res); + }; + Nexus::run(num_threads, task); } -TEST_MT_F("require that rangeCover works with no hits", 10, MatchLoopCommunicator(num_threads, 5)) { - RangePair res = rangeCover(f1, hit_vec({}), thread_id, 10); - TEST_DO(equal_ranges(RangePair({}, {}), res)); +TEST(MatchLoopCommunicatorTest, require_that_rangeCover_works_with_no_hits) +{ + constexpr size_t num_threads = 10; + MatchLoopCommunicator f1(num_threads, 5); + auto task = [&f1](Nexus& ctx) { + auto thread_id = ctx.thread_id(); + RangePair res = rangeCover(f1, hit_vec({}), thread_id, 10); + EXPECT_EQ(RangePair({}, {}), res); + }; + Nexus::run(num_threads, task); } -TEST_FFF("require that hits dropped due to lack of diversity affects range cover result", - MatchLoopCommunicator(num_threads, 3), - MatchLoopCommunicator(num_threads, 3, std::make_unique(), nullptr), - MatchLoopCommunicator(num_threads, 3, std::make_unique(), nullptr)) +TEST(MatchLoopCommunicatorTest, require_that_hits_dropped_due_to_lack_of_diversity_affects_range_cover_result) { + constexpr size_t num_threads = 1; + constexpr size_t thread_id = 0; + MatchLoopCommunicator f1(num_threads, 3); + MatchLoopCommunicator f2(num_threads, 3, std::make_unique(), nullptr); + MatchLoopCommunicator f3(num_threads, 3, std::make_unique(), nullptr); auto hits_in = hit_vec({{1, 5}, {2, 4}, {3, 3}, {4, 2}, {5, 1}}); auto [my_work1, hits1, ranges1] = second_phase(f1, hits_in, thread_id, 10); auto [my_work2, hits2, ranges2] = second_phase(f2, hits_in, thread_id, 10); auto [my_work3, hits3, ranges3] = second_phase(f3, hits_in, thread_id, 10); - EXPECT_EQUAL(my_work1, 3u); - EXPECT_EQUAL(my_work2, 3u); - EXPECT_EQUAL(my_work3, 0u); + EXPECT_EQ(my_work1, 3u); + EXPECT_EQ(my_work2, 3u); + EXPECT_EQ(my_work3, 0u); - TEST_DO(equal(3u, hit_vec({{1, 15}, {2, 14}, {3, 13}}), hits1)); - TEST_DO(equal(3u, hit_vec({{1, 15}, {3, 13}, {5, 11}}), hits2)); - TEST_DO(equal(0u, hit_vec({}), hits3)); + EXPECT_EQ(hit_vec({{1, 15}, {2, 14}, {3, 13}}), hits1); + EXPECT_EQ(hit_vec({{1, 15}, {3, 13}, {5, 11}}), hits2); + EXPECT_EQ(hit_vec({}), hits3); - TEST_DO(equal_ranges(RangePair({3,5},{13,15}), ranges1)); - TEST_DO(equal_ranges(RangePair({4,5},{11,15}), ranges2)); // best dropped: 4 + EXPECT_EQ(RangePair({3,5},{13,15}), ranges1); + EXPECT_EQ(RangePair({4,5},{11,15}), ranges2); // best dropped: 4 // note that the 'drops all hits due to diversity' case will // trigger much of the same code path as dropping second phase // ranking due to hard doom. - TEST_DO(equal_ranges(RangePair({},{}), ranges3)); + EXPECT_EQ(RangePair({},{}), ranges3); } -TEST_MT_F("require that estimate_match_frequency will count hits and docs across threads", 4, MatchLoopCommunicator(num_threads, 5)) { - double freq = (0.0/10.0 + 1.0/11.0 + 2.0/12.0 + 3.0/13.0) / 4.0; - EXPECT_APPROX(freq, f1.estimate_match_frequency(Matches(thread_id, thread_id + 10)), 0.00001); +TEST(MatchLoopCommunicatorTest, require_that_estimate_match_frequency_will_count_hits_and_docs_across_threads) +{ + constexpr size_t num_threads = 4; + MatchLoopCommunicator f1(num_threads, 5); + auto task = [&f1](Nexus& ctx) { + auto thread_id = ctx.thread_id(); + double freq = (0.0/10.0 + 1.0/11.0 + 2.0/12.0 + 3.0/13.0) / 4.0; + EXPECT_NEAR(freq, f1.estimate_match_frequency(Matches(thread_id, thread_id + 10)), 0.00001); + }; + Nexus::run(num_threads, task); } -TEST_MT_F("require that second phase work is evenly distributed among search threads", 5, MatchLoopCommunicator(num_threads, 20)) { - size_t num_hits = thread_id * 5; - size_t docid = thread_id * 100; - double score = thread_id * 100.0; - Hits my_hits; - for(size_t i = 0; i < num_hits; ++i) { - my_hits.emplace_back(++docid, score); - score -= 1.0; - } - auto [my_work, best_hits, ranges] = second_phase(f1, my_hits, thread_id, 1000.0); - EXPECT_EQUAL(my_work, 4u); - TEST_DO(equal_ranges(RangePair({381,400},{1381,1400}), ranges)); - if (thread_id == 4) { - for (auto &hit: my_hits) { - hit.second += 1000.0; - } - TEST_DO(equal(num_hits, my_hits, best_hits)); - } else { - EXPECT_TRUE(best_hits.empty()); - } +TEST(MatchLoopCommunicatorTest, require_that_second_phase_work_is_evenly_distributed_among_search_threads) +{ + constexpr size_t num_threads = 5; + MatchLoopCommunicator f1(num_threads, 20); + auto task = [&f1](Nexus& ctx) { + auto thread_id = ctx.thread_id(); + size_t num_hits = thread_id * 5; + size_t docid = thread_id * 100; + double score = thread_id * 100.0; + Hits my_hits; + for(size_t i = 0; i < num_hits; ++i) { + my_hits.emplace_back(++docid, score); + score -= 1.0; + } + auto [my_work, best_hits, ranges] = second_phase(f1, my_hits, thread_id, 1000.0); + EXPECT_EQ(my_work, 4u); + EXPECT_EQ(RangePair({381,400},{1381,1400}), ranges); + if (thread_id == 4) { + for (auto &hit: my_hits) { + hit.second += 1000.0; + } + EXPECT_EQ(my_hits, best_hits); + } else { + EXPECT_TRUE(best_hits.empty()); + } + }; + Nexus::run(num_threads, task); } namespace { @@ -226,8 +297,10 @@ using FeatureVec = std::vector; } -TEST("require that first phase rank lookup is populated") +TEST(MatchLoopCommunicatorTest, require_that_first_phase_rank_lookup_is_populated) { + constexpr size_t num_threads = 1; + constexpr size_t thread_id = 0; FirstPhaseRankLookup l1; FirstPhaseRankLookup l2; MatchLoopCommunicator f1(num_threads, 3, {}, &l1); @@ -235,8 +308,8 @@ TEST("require that first phase rank lookup is populated") auto hits_in = hit_vec({{21, 5}, {22, 4}, {23, 3}, {24, 2}, {25, 1}}); auto res1 = second_phase(f1, hits_in, thread_id, 10); auto res2 = second_phase(f2, hits_in, thread_id, 10); - EXPECT_EQUAL(FeatureVec({1, 2, 3, unranked, unranked}), extract_ranks(l1)); - EXPECT_EQUAL(FeatureVec({1, unranked, 3, unranked, 5}), extract_ranks(l2)); + EXPECT_EQ(FeatureVec({1, 2, 3, unranked, unranked}), extract_ranks(l1)); + EXPECT_EQ(FeatureVec({1, unranked, 3, unranked, 5}), extract_ranks(l2)); } -TEST_MAIN() { TEST_RUN_ALL(); } +GTEST_MAIN_RUN_ALL_TESTS() diff --git a/searchlib/src/vespa/searchlib/queryeval/scores.h b/searchlib/src/vespa/searchlib/queryeval/scores.h index 19976a2831b..83b1ba9d406 100644 --- a/searchlib/src/vespa/searchlib/queryeval/scores.h +++ b/searchlib/src/vespa/searchlib/queryeval/scores.h @@ -24,6 +24,9 @@ struct Scores { high = score; } } + bool operator==(const Scores& rhs) const { + return low == rhs.low && high == rhs.high; + } }; } -- cgit v1.2.3