| // Copyright (c) 2012 The Chromium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style license that can be |
| // found in the LICENSE file. |
| |
| #include "chrome/browser/predictors/autocomplete_action_predictor.h" |
| |
| #include <stddef.h> |
| |
| #include <string> |
| #include <vector> |
| |
| #include "base/auto_reset.h" |
| #include "base/command_line.h" |
| #include "base/guid.h" |
| #include "base/macros.h" |
| #include "base/memory/ref_counted.h" |
| #include "base/run_loop.h" |
| #include "base/stl_util.h" |
| #include "base/strings/string_util.h" |
| #include "base/strings/utf_string_conversions.h" |
| #include "base/time/time.h" |
| #include "chrome/browser/history/history_service_factory.h" |
| #include "chrome/browser/prerender/prerender_field_trial.h" |
| #include "chrome/browser/prerender/prerender_manager.h" |
| #include "chrome/browser/prerender/prerender_test_utils.h" |
| #include "chrome/common/chrome_switches.h" |
| #include "chrome/test/base/testing_profile.h" |
| #include "components/history/core/browser/history_service.h" |
| #include "components/history/core/browser/in_memory_database.h" |
| #include "components/history/core/browser/url_database.h" |
| #include "components/omnibox/browser/autocomplete_match.h" |
| #include "components/omnibox/browser/autocomplete_result.h" |
| #include "content/public/test/test_browser_thread_bundle.h" |
| #include "content/public/test/test_utils.h" |
| #include "testing/gmock/include/gmock/gmock.h" |
| #include "testing/gtest/include/gtest/gtest.h" |
| |
| using base::ASCIIToUTF16; |
| using predictors::AutocompleteActionPredictor; |
| |
| namespace { |
| |
| struct TestUrlInfo { |
| GURL url; |
| base::string16 title; |
| int days_from_now; |
| base::string16 user_text; |
| int number_of_hits; |
| int number_of_misses; |
| AutocompleteActionPredictor::Action expected_action; |
| } test_url_db[] = { |
| { GURL("http://www.testsite.com/a.html"), |
| ASCIIToUTF16("Test - site - just a test"), 1, |
| ASCIIToUTF16("j"), 5, 0, |
| AutocompleteActionPredictor::ACTION_PRERENDER }, |
| { GURL("http://www.testsite.com/b.html"), |
| ASCIIToUTF16("Test - site - just a test"), 1, |
| ASCIIToUTF16("ju"), 3, 0, |
| AutocompleteActionPredictor::ACTION_PRERENDER }, |
| { GURL("http://www.testsite.com/c.html"), |
| ASCIIToUTF16("Test - site - just a test"), 5, |
| ASCIIToUTF16("just"), 3, 1, |
| AutocompleteActionPredictor::ACTION_PRECONNECT }, |
| { GURL("http://www.testsite.com/d.html"), |
| ASCIIToUTF16("Test - site - just a test"), 5, |
| ASCIIToUTF16("just"), 3, 0, |
| AutocompleteActionPredictor::ACTION_PRERENDER }, |
| { GURL("http://www.testsite.com/e.html"), |
| ASCIIToUTF16("Test - site - just a test"), 8, |
| ASCIIToUTF16("just"), 3, 1, |
| AutocompleteActionPredictor::ACTION_PRECONNECT }, |
| { GURL("http://www.testsite.com/f.html"), |
| ASCIIToUTF16("Test - site - just a test"), 8, |
| ASCIIToUTF16("just"), 3, 0, |
| AutocompleteActionPredictor::ACTION_PRERENDER }, |
| { GURL("http://www.testsite.com/g.html"), |
| ASCIIToUTF16("Test - site - just a test"), 12, |
| base::string16(), 5, 0, |
| AutocompleteActionPredictor::ACTION_NONE }, |
| { GURL("http://www.testsite.com/h.html"), |
| ASCIIToUTF16("Test - site - just a test"), 21, |
| ASCIIToUTF16("just a test"), 2, 0, |
| AutocompleteActionPredictor::ACTION_NONE }, |
| { GURL("http://www.testsite.com/i.html"), |
| ASCIIToUTF16("Test - site - just a test"), 28, |
| ASCIIToUTF16("just a test"), 2, 0, |
| AutocompleteActionPredictor::ACTION_NONE } |
| }; |
| |
| GURL GenerateTestURL(size_t size) { |
| std::string prefix = "http://b/"; |
| // Cannot generate an URL shorter than |prefix|. |
| DCHECK_GE(size, prefix.size()); |
| size_t suffix_len = size - prefix.size(); |
| std::string suffix(suffix_len, 'c'); |
| GURL url(prefix + suffix); |
| DCHECK_EQ(url.spec().size(), size); |
| return url; |
| } |
| |
| } // end namespace |
| |
| namespace predictors { |
| |
| class AutocompleteActionPredictorTest : public testing::Test { |
| public: |
| AutocompleteActionPredictorTest() |
| : profile_(std::make_unique<TestingProfile>()), predictor_(nullptr) { |
| CHECK(profile_->CreateHistoryService(true, false)); |
| predictor_ = std::make_unique<AutocompleteActionPredictor>(profile_.get()); |
| predictor_->CreateLocalCachesFromDatabase(); |
| profile_->BlockUntilHistoryProcessesPendingRequests(); |
| content::RunAllTasksUntilIdle(); |
| |
| CHECK(predictor_->initialized_); |
| CHECK(db_cache()->empty()); |
| CHECK(db_id_cache()->empty()); |
| } |
| |
| ~AutocompleteActionPredictorTest() override { |
| // Wait for all pending tasks on the DB sequence. |
| content::RunAllTasksUntilIdle(); |
| // Since we instantiated the predictor instead of going through a factory |
| // and dependencies, no one else is going to call Shutdown(), which is |
| // supposed to be called as part of being a KeyedService. The behavior of |
| // this method is not explicitly verified. |
| predictor_->Shutdown(); |
| } |
| |
| protected: |
| typedef AutocompleteActionPredictor::DBCacheKey DBCacheKey; |
| typedef AutocompleteActionPredictor::DBCacheValue DBCacheValue; |
| typedef AutocompleteActionPredictor::DBCacheMap DBCacheMap; |
| typedef AutocompleteActionPredictor::DBIdCacheMap DBIdCacheMap; |
| |
| history::URLID AddRowToHistory(const TestUrlInfo& test_row) { |
| history::HistoryService* history = HistoryServiceFactory::GetForProfile( |
| profile_.get(), ServiceAccessType::EXPLICIT_ACCESS); |
| CHECK(history); |
| history::URLDatabase* url_db = history->InMemoryDatabase(); |
| CHECK(url_db); |
| |
| const base::Time visit_time = |
| base::Time::Now() - base::TimeDelta::FromDays( |
| test_row.days_from_now); |
| |
| history::URLRow row(test_row.url); |
| row.set_title(test_row.title); |
| row.set_last_visit(visit_time); |
| |
| return url_db->AddURL(row); |
| } |
| |
| AutocompleteActionPredictorTable::Row CreateRowFromTestUrlInfo( |
| const TestUrlInfo& test_row) const { |
| AutocompleteActionPredictorTable::Row row; |
| row.id = base::GenerateGUID(); |
| row.user_text = test_row.user_text; |
| row.url = test_row.url; |
| row.number_of_hits = test_row.number_of_hits; |
| row.number_of_misses = test_row.number_of_misses; |
| return row; |
| } |
| |
| void AddAllRows() { |
| for (size_t i = 0; i < base::size(test_url_db); ++i) |
| AddRow(test_url_db[i]); |
| } |
| |
| std::string AddRow(const TestUrlInfo& test_row) { |
| AutocompleteActionPredictorTable::Row row = |
| CreateRowFromTestUrlInfo(test_row); |
| predictor_->AddAndUpdateRows( |
| AutocompleteActionPredictorTable::Rows(1, row), |
| AutocompleteActionPredictorTable::Rows()); |
| |
| return row.id; |
| } |
| |
| void UpdateRow(const AutocompleteActionPredictorTable::Row& row) { |
| AutocompleteActionPredictor::DBCacheKey key = { row.user_text, row.url }; |
| ASSERT_TRUE(db_cache()->find(key) != db_cache()->end()); |
| predictor_->AddAndUpdateRows( |
| AutocompleteActionPredictorTable::Rows(), |
| AutocompleteActionPredictorTable::Rows(1, row)); |
| } |
| |
| void OnURLsDeletedTest(bool expired) { |
| ASSERT_NO_FATAL_FAILURE(AddAllRows()); |
| |
| EXPECT_EQ(base::size(test_url_db), db_cache()->size()); |
| EXPECT_EQ(base::size(test_url_db), db_id_cache()->size()); |
| |
| std::vector<size_t> expected; |
| history::URLRows rows; |
| for (size_t i = 0; i < base::size(test_url_db); ++i) { |
| bool expect_deleted = false; |
| |
| if (i < 2) { |
| rows.push_back(history::URLRow(test_url_db[i].url)); |
| expect_deleted = true; |
| } |
| |
| if (!expired && |
| test_url_db[i].days_from_now > maximum_days_to_keep_entry()) { |
| expect_deleted = true; |
| } |
| |
| if (i != 3 && i != 4) |
| ASSERT_TRUE(AddRowToHistory(test_url_db[i])); |
| else if (!expired) |
| expect_deleted = true; |
| |
| if (!expect_deleted) |
| expected.push_back(i); |
| } |
| |
| history::HistoryService* history_service = |
| HistoryServiceFactory::GetForProfile( |
| profile_.get(), ServiceAccessType::EXPLICIT_ACCESS); |
| ASSERT_TRUE(history_service); |
| |
| predictor_->OnURLsDeleted( |
| history_service, |
| history::DeletionInfo(history::DeletionTimeRange::Invalid(), expired, |
| rows, std::set<GURL>(), base::nullopt)); |
| |
| EXPECT_EQ(expected.size(), db_cache()->size()); |
| EXPECT_EQ(expected.size(), db_id_cache()->size()); |
| |
| for (size_t i = 0; i < base::size(test_url_db); ++i) { |
| DBCacheKey key = {test_url_db[i].user_text, test_url_db[i].url}; |
| |
| bool deleted = !base::ContainsValue(expected, i); |
| EXPECT_EQ(deleted, db_cache()->find(key) == db_cache()->end()); |
| EXPECT_EQ(deleted, db_id_cache()->find(key) == db_id_cache()->end()); |
| } |
| } |
| |
| void DeleteAllRows() { |
| predictor_->DeleteAllRows(); |
| } |
| |
| void DeleteRowsFromCaches( |
| const history::URLRows& rows, |
| std::vector<AutocompleteActionPredictorTable::Row::Id>* id_list) { |
| predictor_->DeleteRowsFromCaches(rows, id_list); |
| } |
| |
| void DeleteOldIdsFromCaches( |
| std::vector<AutocompleteActionPredictorTable::Row::Id>* id_list) { |
| history::HistoryService* history_service = |
| HistoryServiceFactory::GetForProfile( |
| profile_.get(), ServiceAccessType::EXPLICIT_ACCESS); |
| ASSERT_TRUE(history_service); |
| |
| history::URLDatabase* url_db = history_service->InMemoryDatabase(); |
| ASSERT_TRUE(url_db); |
| |
| predictor_->DeleteOldIdsFromCaches(url_db, id_list); |
| } |
| |
| AutocompleteActionPredictor* predictor() { return predictor_.get(); } |
| |
| DBCacheMap* db_cache() { return &predictor_->db_cache_; } |
| DBIdCacheMap* db_id_cache() { return &predictor_->db_id_cache_; } |
| std::vector<AutocompleteActionPredictor::TransitionalMatch>* |
| transitional_matches() { |
| return &predictor_->transitional_matches_; |
| } |
| |
| static int maximum_days_to_keep_entry() { |
| return AutocompleteActionPredictor::kMaximumDaysToKeepEntry; |
| } |
| |
| static size_t minimum_user_text_length() { |
| return AutocompleteActionPredictor::kMinimumUserTextLength; |
| } |
| |
| static size_t maximum_string_length() { |
| return AutocompleteActionPredictor::kMaximumStringLength; |
| } |
| |
| private: |
| content::TestBrowserThreadBundle test_browser_thread_bundle_; |
| std::unique_ptr<TestingProfile> profile_; |
| std::unique_ptr<AutocompleteActionPredictor> predictor_; |
| }; |
| |
| |
| TEST_F(AutocompleteActionPredictorTest, AddRow) { |
| // Add a test entry to the predictor. |
| std::string guid = AddRow(test_url_db[0]); |
| |
| // Get the data back out of the cache. |
| const DBCacheKey key = { test_url_db[0].user_text, test_url_db[0].url }; |
| DBCacheMap::const_iterator it = db_cache()->find(key); |
| EXPECT_TRUE(it != db_cache()->end()); |
| |
| const DBCacheValue value = { test_url_db[0].number_of_hits, |
| test_url_db[0].number_of_misses }; |
| EXPECT_EQ(value.number_of_hits, it->second.number_of_hits); |
| EXPECT_EQ(value.number_of_misses, it->second.number_of_misses); |
| |
| DBIdCacheMap::const_iterator id_it = db_id_cache()->find(key); |
| EXPECT_TRUE(id_it != db_id_cache()->end()); |
| EXPECT_EQ(guid, id_it->second); |
| } |
| |
| TEST_F(AutocompleteActionPredictorTest, UpdateRow) { |
| ASSERT_NO_FATAL_FAILURE(AddAllRows()); |
| |
| EXPECT_EQ(base::size(test_url_db), db_cache()->size()); |
| EXPECT_EQ(base::size(test_url_db), db_id_cache()->size()); |
| |
| // Get the data back out of the cache. |
| const DBCacheKey key = { test_url_db[0].user_text, test_url_db[0].url }; |
| DBCacheMap::const_iterator it = db_cache()->find(key); |
| EXPECT_TRUE(it != db_cache()->end()); |
| |
| DBIdCacheMap::const_iterator id_it = db_id_cache()->find(key); |
| EXPECT_TRUE(id_it != db_id_cache()->end()); |
| |
| AutocompleteActionPredictorTable::Row update_row; |
| update_row.id = id_it->second; |
| update_row.user_text = key.user_text; |
| update_row.url = key.url; |
| update_row.number_of_hits = it->second.number_of_hits + 1; |
| update_row.number_of_misses = it->second.number_of_misses + 2; |
| |
| UpdateRow(update_row); |
| |
| // Get the updated version. |
| DBCacheMap::const_iterator update_it = db_cache()->find(key); |
| EXPECT_TRUE(update_it != db_cache()->end()); |
| |
| EXPECT_EQ(update_row.number_of_hits, update_it->second.number_of_hits); |
| EXPECT_EQ(update_row.number_of_misses, update_it->second.number_of_misses); |
| |
| DBIdCacheMap::const_iterator update_id_it = db_id_cache()->find(key); |
| EXPECT_TRUE(update_id_it != db_id_cache()->end()); |
| |
| EXPECT_EQ(id_it->second, update_id_it->second); |
| } |
| |
| TEST_F(AutocompleteActionPredictorTest, DeleteAllRows) { |
| ASSERT_NO_FATAL_FAILURE(AddAllRows()); |
| |
| EXPECT_EQ(base::size(test_url_db), db_cache()->size()); |
| EXPECT_EQ(base::size(test_url_db), db_id_cache()->size()); |
| |
| DeleteAllRows(); |
| |
| EXPECT_TRUE(db_cache()->empty()); |
| EXPECT_TRUE(db_id_cache()->empty()); |
| } |
| |
| TEST_F(AutocompleteActionPredictorTest, DeleteRowsFromCaches) { |
| std::vector<AutocompleteActionPredictorTable::Row::Id> all_ids; |
| history::URLRows rows; |
| for (size_t i = 0; i < base::size(test_url_db); ++i) { |
| std::string row_id = AddRow(test_url_db[i]); |
| all_ids.push_back(row_id); |
| |
| if (i < 2) |
| rows.push_back(history::URLRow(test_url_db[i].url)); |
| } |
| |
| EXPECT_EQ(base::size(test_url_db), all_ids.size()); |
| EXPECT_EQ(base::size(test_url_db), db_cache()->size()); |
| EXPECT_EQ(base::size(test_url_db), db_id_cache()->size()); |
| |
| std::vector<AutocompleteActionPredictorTable::Row::Id> id_list; |
| DeleteRowsFromCaches(rows, &id_list); |
| |
| EXPECT_EQ(base::size(test_url_db) - 2, db_cache()->size()); |
| EXPECT_EQ(base::size(test_url_db) - 2, db_id_cache()->size()); |
| |
| for (size_t i = 0; i < base::size(test_url_db); ++i) { |
| DBCacheKey key = { test_url_db[i].user_text, test_url_db[i].url }; |
| |
| bool deleted = (i < 2); |
| EXPECT_EQ(deleted, db_cache()->find(key) == db_cache()->end()); |
| EXPECT_EQ(deleted, db_id_cache()->find(key) == db_id_cache()->end()); |
| EXPECT_EQ(deleted, base::ContainsValue(id_list, all_ids[i])); |
| } |
| } |
| |
| TEST_F(AutocompleteActionPredictorTest, DeleteOldIdsFromCaches) { |
| std::vector<AutocompleteActionPredictorTable::Row::Id> expected; |
| std::vector<AutocompleteActionPredictorTable::Row::Id> all_ids; |
| |
| for (size_t i = 0; i < base::size(test_url_db); ++i) { |
| std::string row_id = AddRow(test_url_db[i]); |
| all_ids.push_back(row_id); |
| |
| bool exclude_url = |
| base::StartsWith(test_url_db[i].url.path(), "/d", |
| base::CompareCase::SENSITIVE) || |
| (test_url_db[i].days_from_now > maximum_days_to_keep_entry()); |
| |
| if (exclude_url) |
| expected.push_back(row_id); |
| else |
| ASSERT_TRUE(AddRowToHistory(test_url_db[i])); |
| } |
| |
| std::vector<AutocompleteActionPredictorTable::Row::Id> id_list; |
| DeleteOldIdsFromCaches(&id_list); |
| EXPECT_EQ(expected.size(), id_list.size()); |
| EXPECT_EQ(all_ids.size() - expected.size(), db_cache()->size()); |
| EXPECT_EQ(all_ids.size() - expected.size(), db_id_cache()->size()); |
| |
| for (auto it = all_ids.begin(); it != all_ids.end(); ++it) { |
| bool in_expected = base::ContainsValue(expected, *it); |
| bool in_list = base::ContainsValue(id_list, *it); |
| EXPECT_EQ(in_expected, in_list); |
| } |
| } |
| |
| TEST_F(AutocompleteActionPredictorTest, OnURLsDeletedExpired) { |
| OnURLsDeletedTest(true); |
| } |
| |
| TEST_F(AutocompleteActionPredictorTest, OnURLsDeletedNonExpired) { |
| OnURLsDeletedTest(false); |
| } |
| |
| TEST_F(AutocompleteActionPredictorTest, RecommendActionURL) { |
| ASSERT_NO_FATAL_FAILURE(AddAllRows()); |
| |
| AutocompleteMatch match; |
| match.type = AutocompleteMatchType::HISTORY_URL; |
| prerender::test_utils::RestorePrerenderMode restore_prerender_mode; |
| prerender::PrerenderManager::SetMode( |
| prerender::PrerenderManager::PRERENDER_MODE_NOSTATE_PREFETCH); |
| |
| for (size_t i = 0; i < base::size(test_url_db); ++i) { |
| match.destination_url = GURL(test_url_db[i].url); |
| EXPECT_EQ(test_url_db[i].expected_action, |
| predictor()->RecommendAction(test_url_db[i].user_text, match)) |
| << "Unexpected action for " << match.destination_url; |
| } |
| } |
| |
| TEST_F(AutocompleteActionPredictorTest, RecommendActionSearch) { |
| ASSERT_NO_FATAL_FAILURE(AddAllRows()); |
| |
| AutocompleteMatch match; |
| match.type = AutocompleteMatchType::SEARCH_WHAT_YOU_TYPED; |
| prerender::test_utils::RestorePrerenderMode restore_prerender_mode; |
| prerender::PrerenderManager::SetMode( |
| prerender::PrerenderManager::PRERENDER_MODE_NOSTATE_PREFETCH); |
| |
| for (size_t i = 0; i < base::size(test_url_db); ++i) { |
| match.destination_url = GURL(test_url_db[i].url); |
| AutocompleteActionPredictor::Action expected_action = |
| (test_url_db[i].expected_action == |
| AutocompleteActionPredictor::ACTION_PRERENDER) ? |
| AutocompleteActionPredictor::ACTION_PRECONNECT : |
| test_url_db[i].expected_action; |
| EXPECT_EQ(expected_action, |
| predictor()->RecommendAction(test_url_db[i].user_text, match)) |
| << "Unexpected action for " << match.destination_url; |
| } |
| } |
| |
| TEST_F(AutocompleteActionPredictorTest, |
| RegisterTransitionalMatchesUserTextSizeLimits) { |
| auto test = [this](const base::string16& user_text, |
| bool should_be_registered) { |
| predictor()->RegisterTransitionalMatches(user_text, AutocompleteResult()); |
| bool registered = base::ContainsValue(*transitional_matches(), user_text); |
| EXPECT_EQ(registered, should_be_registered); |
| }; |
| |
| base::string16 short_text = |
| ASCIIToUTF16(std::string(minimum_user_text_length(), 'g')); |
| test(short_text, true); |
| |
| base::string16 too_short_text = |
| ASCIIToUTF16(std::string(minimum_user_text_length() - 1, 'g')); |
| test(too_short_text, false); |
| |
| base::string16 long_text = |
| ASCIIToUTF16(std::string(maximum_string_length(), 'g')); |
| test(long_text, true); |
| |
| base::string16 too_long_text = |
| ASCIIToUTF16(std::string(maximum_string_length() + 1, 'g')); |
| test(too_long_text, false); |
| } |
| |
| TEST_F(AutocompleteActionPredictorTest, |
| RegisterTransitionalMatchesURLSizeLimits) { |
| GURL urls[] = {GenerateTestURL(10), GenerateTestURL(maximum_string_length()), |
| GenerateTestURL(maximum_string_length() + 1), |
| GenerateTestURL(maximum_string_length() * 10)}; |
| ACMatches matches; |
| for (const auto& url : urls) { |
| AutocompleteMatch match; |
| match.destination_url = url; |
| matches.push_back(match); |
| } |
| AutocompleteResult result; |
| result.AppendMatches(AutocompleteInput(), matches); |
| base::string16 user_text = ASCIIToUTF16("google"); |
| predictor()->RegisterTransitionalMatches(user_text, result); |
| auto it = std::find(transitional_matches()->begin(), |
| transitional_matches()->end(), user_text); |
| ASSERT_NE(it, transitional_matches()->end()); |
| EXPECT_THAT(it->urls, ::testing::ElementsAre(urls[0], urls[1])); |
| } |
| |
| } // namespace predictors |