1// Copyright (c) 2012 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4 5#include "base/basictypes.h" 6#include "base/bind.h" 7#include "base/bind_helpers.h" 8#include "base/files/file_path.h" 9#include "base/files/file_util.h" 10#include "base/files/scoped_temp_dir.h" 11#include "base/message_loop/message_loop.h" 12#include "base/path_service.h" 13#include "base/strings/utf_string_conversions.h" 14#include "base/task/cancelable_task_tracker.h" 15#include "chrome/browser/history/history_service.h" 16#include "testing/gtest/include/gtest/gtest.h" 17 18using base::Time; 19using base::TimeDelta; 20 21// Tests the history service for querying functionality. 22 23namespace history { 24 25namespace { 26 27struct TestEntry { 28 const char* url; 29 const char* title; 30 const int days_ago; 31 Time time; // Filled by SetUp. 32} test_entries[] = { 33 // This one is visited super long ago so it will be in a different database 34 // from the next appearance of it at the end. 35 {"http://example.com/", "Other", 180}, 36 37 // These are deliberately added out of chronological order. The history 38 // service should sort them by visit time when returning query results. 39 // The correct index sort order is 4 2 3 1 7 6 5 0. 40 {"http://www.google.com/1", "Title PAGEONE FOO some text", 10}, 41 {"http://www.google.com/3", "Title PAGETHREE BAR some hello world", 8}, 42 {"http://www.google.com/2", "Title PAGETWO FOO some more blah blah blah", 9}, 43 44 // A more recent visit of the first one. 45 {"http://example.com/", "Other", 6}, 46 47 {"http://www.google.com/6", "Title I'm the second oldest", 13}, 48 {"http://www.google.com/4", "Title four", 12}, 49 {"http://www.google.com/5", "Title five", 11}, 50}; 51 52// Returns true if the nth result in the given results set matches. It will 53// return false on a non-match or if there aren't enough results. 54bool NthResultIs(const QueryResults& results, 55 int n, // Result index to check. 56 int test_entry_index) { // Index of test_entries to compare. 57 if (static_cast<int>(results.size()) <= n) 58 return false; 59 60 const URLResult& result = results[n]; 61 62 // Check the visit time. 63 if (result.visit_time() != test_entries[test_entry_index].time) 64 return false; 65 66 // Now check the URL & title. 67 return result.url() == GURL(test_entries[test_entry_index].url) && 68 result.title() == 69 base::UTF8ToUTF16(test_entries[test_entry_index].title); 70} 71 72} // namespace 73 74class HistoryQueryTest : public testing::Test { 75 public: 76 HistoryQueryTest() : page_id_(0) { 77 } 78 79 // Acts like a synchronous call to history's QueryHistory. 80 void QueryHistory(const std::string& text_query, 81 const QueryOptions& options, 82 QueryResults* results) { 83 history_->QueryHistory(base::UTF8ToUTF16(text_query), 84 options, 85 base::Bind(&HistoryQueryTest::QueryHistoryComplete, 86 base::Unretained(this)), 87 &tracker_); 88 // Will go until ...Complete calls Quit. 89 base::MessageLoop::current()->Run(); 90 results->Swap(&last_query_results_); 91 } 92 93 // Test paging through results, with a fixed number of results per page. 94 // Defined here so code can be shared for the text search and the non-text 95 // seach versions. 96 void TestPaging(const std::string& query_text, 97 const int* expected_results, 98 int results_length) { 99 ASSERT_TRUE(history_.get()); 100 101 QueryOptions options; 102 QueryResults results; 103 104 options.max_count = 1; 105 for (int i = 0; i < results_length; i++) { 106 SCOPED_TRACE(testing::Message() << "i = " << i); 107 QueryHistory(query_text, options, &results); 108 ASSERT_EQ(1U, results.size()); 109 EXPECT_TRUE(NthResultIs(results, 0, expected_results[i])); 110 options.end_time = results.back().visit_time(); 111 } 112 QueryHistory(query_text, options, &results); 113 EXPECT_EQ(0U, results.size()); 114 115 // Try with a max_count > 1. 116 options.max_count = 2; 117 options.end_time = base::Time(); 118 for (int i = 0; i < results_length / 2; i++) { 119 SCOPED_TRACE(testing::Message() << "i = " << i); 120 QueryHistory(query_text, options, &results); 121 ASSERT_EQ(2U, results.size()); 122 EXPECT_TRUE(NthResultIs(results, 0, expected_results[i * 2])); 123 EXPECT_TRUE(NthResultIs(results, 1, expected_results[i * 2 + 1])); 124 options.end_time = results.back().visit_time(); 125 } 126 127 // Add a couple of entries with duplicate timestamps. Use |query_text| as 128 // the title of both entries so that they match a text query. 129 TestEntry duplicates[] = { 130 { "http://www.google.com/x", query_text.c_str(), 1, }, 131 { "http://www.google.com/y", query_text.c_str(), 1, } 132 }; 133 AddEntryToHistory(duplicates[0]); 134 AddEntryToHistory(duplicates[1]); 135 136 // Make sure that paging proceeds even if there are duplicate timestamps. 137 options.end_time = base::Time(); 138 do { 139 QueryHistory(query_text, options, &results); 140 ASSERT_NE(options.end_time, results.back().visit_time()); 141 options.end_time = results.back().visit_time(); 142 } while (!results.reached_beginning()); 143 } 144 145 protected: 146 scoped_ptr<HistoryService> history_; 147 148 // Counter used to generate a unique ID for each page added to the history. 149 int32 page_id_; 150 151 void AddEntryToHistory(const TestEntry& entry) { 152 // We need the ID scope and page ID so that the visit tracker can find it. 153 ContextID context_id = reinterpret_cast<ContextID>(1); 154 GURL url(entry.url); 155 156 history_->AddPage(url, entry.time, context_id, page_id_++, GURL(), 157 history::RedirectList(), ui::PAGE_TRANSITION_LINK, 158 history::SOURCE_BROWSED, false); 159 history_->SetPageTitle(url, base::UTF8ToUTF16(entry.title)); 160 } 161 162 private: 163 virtual void SetUp() { 164 ASSERT_TRUE(temp_dir_.CreateUniqueTempDir()); 165 history_dir_ = temp_dir_.path().AppendASCII("HistoryTest"); 166 ASSERT_TRUE(base::CreateDirectory(history_dir_)); 167 168 history_.reset(new HistoryService); 169 if (!history_->Init(history_dir_)) { 170 history_.reset(); // Tests should notice this NULL ptr & fail. 171 return; 172 } 173 174 // Fill the test data. 175 Time now = Time::Now().LocalMidnight(); 176 for (size_t i = 0; i < arraysize(test_entries); i++) { 177 test_entries[i].time = 178 now - (test_entries[i].days_ago * TimeDelta::FromDays(1)); 179 AddEntryToHistory(test_entries[i]); 180 } 181 } 182 183 virtual void TearDown() { 184 if (history_) { 185 history_->SetOnBackendDestroyTask(base::MessageLoop::QuitClosure()); 186 history_->Cleanup(); 187 history_.reset(); 188 base::MessageLoop::current()->Run(); // Wait for the other thread. 189 } 190 } 191 192 void QueryHistoryComplete(QueryResults* results) { 193 results->Swap(&last_query_results_); 194 base::MessageLoop::current()->Quit(); // Will return out to QueryHistory. 195 } 196 197 base::ScopedTempDir temp_dir_; 198 199 base::MessageLoop message_loop_; 200 201 base::FilePath history_dir_; 202 203 base::CancelableTaskTracker tracker_; 204 205 // The QueryHistoryComplete callback will put the results here so QueryHistory 206 // can return them. 207 QueryResults last_query_results_; 208 209 DISALLOW_COPY_AND_ASSIGN(HistoryQueryTest); 210}; 211 212TEST_F(HistoryQueryTest, Basic) { 213 ASSERT_TRUE(history_.get()); 214 215 QueryOptions options; 216 QueryResults results; 217 218 // Test duplicate collapsing. 0 is an older duplicate of 4, and should not 219 // appear in the result set. 220 QueryHistory(std::string(), options, &results); 221 EXPECT_EQ(7U, results.size()); 222 223 EXPECT_TRUE(NthResultIs(results, 0, 4)); 224 EXPECT_TRUE(NthResultIs(results, 1, 2)); 225 EXPECT_TRUE(NthResultIs(results, 2, 3)); 226 EXPECT_TRUE(NthResultIs(results, 3, 1)); 227 EXPECT_TRUE(NthResultIs(results, 4, 7)); 228 EXPECT_TRUE(NthResultIs(results, 5, 6)); 229 EXPECT_TRUE(NthResultIs(results, 6, 5)); 230 231 // Next query a time range. The beginning should be inclusive, the ending 232 // should be exclusive. 233 options.begin_time = test_entries[3].time; 234 options.end_time = test_entries[2].time; 235 QueryHistory(std::string(), options, &results); 236 EXPECT_EQ(1U, results.size()); 237 EXPECT_TRUE(NthResultIs(results, 0, 3)); 238} 239 240// Tests max_count feature for basic (non-Full Text Search) queries. 241TEST_F(HistoryQueryTest, BasicCount) { 242 ASSERT_TRUE(history_.get()); 243 244 QueryOptions options; 245 QueryResults results; 246 247 // Query all time but with a limit on the number of entries. We should 248 // get the N most recent entries. 249 options.max_count = 2; 250 QueryHistory(std::string(), options, &results); 251 EXPECT_EQ(2U, results.size()); 252 EXPECT_TRUE(NthResultIs(results, 0, 4)); 253 EXPECT_TRUE(NthResultIs(results, 1, 2)); 254} 255 256TEST_F(HistoryQueryTest, ReachedBeginning) { 257 ASSERT_TRUE(history_.get()); 258 259 QueryOptions options; 260 QueryResults results; 261 262 QueryHistory(std::string(), options, &results); 263 EXPECT_TRUE(results.reached_beginning()); 264 QueryHistory("some", options, &results); 265 EXPECT_TRUE(results.reached_beginning()); 266 267 options.begin_time = test_entries[1].time; 268 QueryHistory(std::string(), options, &results); 269 EXPECT_FALSE(results.reached_beginning()); 270 QueryHistory("some", options, &results); 271 EXPECT_FALSE(results.reached_beginning()); 272 273 // Try |begin_time| just later than the oldest visit. 274 options.begin_time = test_entries[0].time + TimeDelta::FromMicroseconds(1); 275 QueryHistory(std::string(), options, &results); 276 EXPECT_FALSE(results.reached_beginning()); 277 QueryHistory("some", options, &results); 278 EXPECT_FALSE(results.reached_beginning()); 279 280 // Try |begin_time| equal to the oldest visit. 281 options.begin_time = test_entries[0].time; 282 QueryHistory(std::string(), options, &results); 283 EXPECT_TRUE(results.reached_beginning()); 284 QueryHistory("some", options, &results); 285 EXPECT_TRUE(results.reached_beginning()); 286 287 // Try |begin_time| just earlier than the oldest visit. 288 options.begin_time = test_entries[0].time - TimeDelta::FromMicroseconds(1); 289 QueryHistory(std::string(), options, &results); 290 EXPECT_TRUE(results.reached_beginning()); 291 QueryHistory("some", options, &results); 292 EXPECT_TRUE(results.reached_beginning()); 293 294 // Test with |max_count| specified. 295 options.max_count = 1; 296 QueryHistory(std::string(), options, &results); 297 EXPECT_FALSE(results.reached_beginning()); 298 QueryHistory("some", options, &results); 299 EXPECT_FALSE(results.reached_beginning()); 300 301 // Test with |max_count| greater than the number of results, 302 // and exactly equal to the number of results. 303 options.max_count = 100; 304 QueryHistory(std::string(), options, &results); 305 EXPECT_TRUE(results.reached_beginning()); 306 options.max_count = results.size(); 307 QueryHistory(std::string(), options, &results); 308 EXPECT_TRUE(results.reached_beginning()); 309 310 options.max_count = 100; 311 QueryHistory("some", options, &results); 312 EXPECT_TRUE(results.reached_beginning()); 313 options.max_count = results.size(); 314 QueryHistory("some", options, &results); 315 EXPECT_TRUE(results.reached_beginning()); 316} 317 318// This does most of the same tests above, but performs a text searches for a 319// string that will match the pages in question. This will trigger a 320// different code path. 321TEST_F(HistoryQueryTest, TextSearch) { 322 ASSERT_TRUE(history_.get()); 323 324 QueryOptions options; 325 QueryResults results; 326 327 // Query all of them to make sure they are there and in order. Note that 328 // this query will return the starred item twice since we requested all 329 // starred entries and no de-duping. 330 QueryHistory("some", options, &results); 331 EXPECT_EQ(3U, results.size()); 332 EXPECT_TRUE(NthResultIs(results, 0, 2)); 333 EXPECT_TRUE(NthResultIs(results, 1, 3)); 334 EXPECT_TRUE(NthResultIs(results, 2, 1)); 335 336 // Do a query that should only match one of them. 337 QueryHistory("PAGETWO", options, &results); 338 EXPECT_EQ(1U, results.size()); 339 EXPECT_TRUE(NthResultIs(results, 0, 3)); 340 341 // Next query a time range. The beginning should be inclusive, the ending 342 // should be exclusive. 343 options.begin_time = test_entries[1].time; 344 options.end_time = test_entries[3].time; 345 QueryHistory("some", options, &results); 346 EXPECT_EQ(1U, results.size()); 347 EXPECT_TRUE(NthResultIs(results, 0, 1)); 348} 349 350// Tests prefix searching for text search queries. 351TEST_F(HistoryQueryTest, TextSearchPrefix) { 352 ASSERT_TRUE(history_.get()); 353 354 QueryOptions options; 355 QueryResults results; 356 357 // Query with a prefix search. Should return matches for "PAGETWO" and 358 // "PAGETHREE". 359 QueryHistory("PAGET", options, &results); 360 EXPECT_EQ(2U, results.size()); 361 EXPECT_TRUE(NthResultIs(results, 0, 2)); 362 EXPECT_TRUE(NthResultIs(results, 1, 3)); 363} 364 365// Tests max_count feature for text search queries. 366TEST_F(HistoryQueryTest, TextSearchCount) { 367 ASSERT_TRUE(history_.get()); 368 369 QueryOptions options; 370 QueryResults results; 371 372 // Query all time but with a limit on the number of entries. We should 373 // get the N most recent entries. 374 options.max_count = 2; 375 QueryHistory("some", options, &results); 376 EXPECT_EQ(2U, results.size()); 377 EXPECT_TRUE(NthResultIs(results, 0, 2)); 378 EXPECT_TRUE(NthResultIs(results, 1, 3)); 379 380 // Now query a subset of the pages and limit by N items. "FOO" should match 381 // the 2nd & 3rd pages, but we should only get the 3rd one because of the one 382 // page max restriction. 383 options.max_count = 1; 384 QueryHistory("FOO", options, &results); 385 EXPECT_EQ(1U, results.size()); 386 EXPECT_TRUE(NthResultIs(results, 0, 3)); 387} 388 389// Tests IDN text search by both ASCII and UTF. 390TEST_F(HistoryQueryTest, TextSearchIDN) { 391 ASSERT_TRUE(history_.get()); 392 393 QueryOptions options; 394 QueryResults results; 395 396 TestEntry entry = { "http://xn--d1abbgf6aiiy.xn--p1ai/", "Nothing", 0, }; 397 AddEntryToHistory(entry); 398 399 struct QueryEntry { 400 std::string query; 401 size_t results_size; 402 } queries[] = { 403 { "bad query", 0 }, 404 { std::string("xn--d1abbgf6aiiy.xn--p1ai"), 1 }, 405 { base::WideToUTF8(std::wstring(L"\u043f\u0440\u0435\u0437") + 406 L"\u0438\u0434\u0435\u043d\u0442.\u0440\u0444"), 1, }, 407 }; 408 409 for (size_t i = 0; i < ARRAYSIZE_UNSAFE(queries); ++i) { 410 QueryHistory(queries[i].query, options, &results); 411 EXPECT_EQ(queries[i].results_size, results.size()); 412 } 413} 414 415// Test iterating over pages of results. 416TEST_F(HistoryQueryTest, Paging) { 417 // Since results are fetched 1 and 2 at a time, entry #0 and #6 will not 418 // be de-duplicated. 419 int expected_results[] = { 4, 2, 3, 1, 7, 6, 5, 0 }; 420 TestPaging(std::string(), expected_results, arraysize(expected_results)); 421} 422 423TEST_F(HistoryQueryTest, TextSearchPaging) { 424 // Since results are fetched 1 and 2 at a time, entry #0 and #6 will not 425 // be de-duplicated. Entry #4 does not contain the text "title", so it 426 // shouldn't appear. 427 int expected_results[] = { 2, 3, 1, 7, 6, 5 }; 428 TestPaging("title", expected_results, arraysize(expected_results)); 429} 430 431} // namespace history 432