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.
NthResultIs(const QueryResults & results,int n,int test_entry_index)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:
HistoryQueryTest()74 HistoryQueryTest() : page_id_(0) {
75 }
76
77 // Acts like a synchronous call to history's QueryHistory.
QueryHistory(const std::string & text_query,const QueryOptions & options,QueryResults * results)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.
TestPaging(const std::string & query_text,const int * expected_results,int results_length)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
AddEntryToHistory(const TestEntry & entry)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:
SetUp()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
TearDown()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
QueryHistoryComplete(HistoryService::Handle,QueryResults * results)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
TEST_F(HistoryQueryTest,Basic)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.
TEST_F(HistoryQueryTest,BasicCount)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
TEST_F(HistoryQueryTest,ReachedBeginning)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.
TEST_F(HistoryQueryTest,TextSearch)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.
TEST_F(HistoryQueryTest,TextSearchPrefix)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.
TEST_F(HistoryQueryTest,TextSearchCount)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.
TEST_F(HistoryQueryTest,TextSearchIDN)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.
TEST_F(HistoryQueryTest,Paging)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
TEST_F(HistoryQueryTest,TextSearchPaging)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