Home | History | Annotate | Download | only in history
      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