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