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