1// Copyright (c) 2011 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 <string>
6
7#include "base/file_util.h"
8#include "base/memory/scoped_ptr.h"
9#include "base/memory/scoped_temp_dir.h"
10#include "base/string_util.h"
11#include "base/utf_string_conversions.h"
12#include "chrome/browser/history/text_database.h"
13#include "testing/gtest/include/gtest/gtest.h"
14#include "testing/platform_test.h"
15
16using base::Time;
17
18namespace history {
19
20namespace {
21
22// Note that all pages have "COUNTTAG" which allows us to count the number of
23// pages in the database withoujt adding any extra functions to the DB object.
24const char kURL1[] = "http://www.google.com/";
25const int kTime1 = 1000;
26const char kTitle1[] = "Google";
27const char kBody1[] =
28    "COUNTTAG Web Images Maps News Shopping Gmail more My Account | "
29    "Sign out Advanced Search Preferences Language Tools Advertising Programs "
30    "- Business Solutions - About Google, 2008 Google";
31
32const char kURL2[] = "http://images.google.com/";
33const int kTime2 = 2000;
34const char kTitle2[] = "Google Image Search";
35const char kBody2[] =
36    "COUNTTAG Web Images Maps News Shopping Gmail more My Account | "
37    "Sign out Advanced Image Search Preferences The most comprehensive image "
38    "search on the web. Want to help improve Google Image Search? Try Google "
39    "Image Labeler. Advertising Programs - Business Solutions - About Google "
40    "2008 Google";
41
42const char kURL3[] = "http://slashdot.org/";
43const int kTime3 = 3000;
44const char kTitle3[] = "Slashdot: News for nerds, stuff that matters";
45const char kBody3[] =
46    "COUNTTAG Slashdot Log In Create Account Subscribe Firehose Why "
47    "Log In? Why Subscribe? Nickname Password Public Terminal Sections "
48    "Main Apple AskSlashdot Backslash Books Developers Games Hardware "
49    "Interviews IT Linux Mobile Politics Science YRO";
50
51// Returns the number of rows currently in the database.
52int RowCount(TextDatabase* db) {
53  QueryOptions options;
54  options.begin_time = Time::FromInternalValue(0);
55  // Leave end_time at now.
56
57  std::vector<TextDatabase::Match> results;
58  Time first_time_searched;
59  TextDatabase::URLSet unique_urls;
60  db->GetTextMatches("COUNTTAG", options, &results, &unique_urls,
61                     &first_time_searched);
62  return static_cast<int>(results.size());
63}
64
65// Adds each of the test pages to the database.
66void AddAllTestData(TextDatabase* db) {
67  EXPECT_TRUE(db->AddPageData(
68      Time::FromInternalValue(kTime1), kURL1, kTitle1, kBody1));
69  EXPECT_TRUE(db->AddPageData(
70      Time::FromInternalValue(kTime2), kURL2, kTitle2, kBody2));
71  EXPECT_TRUE(db->AddPageData(
72      Time::FromInternalValue(kTime3), kURL3, kTitle3, kBody3));
73  EXPECT_EQ(3, RowCount(db));
74}
75
76bool ResultsHaveURL(const std::vector<TextDatabase::Match>& results,
77                    const char* url) {
78  GURL gurl(url);
79  for (size_t i = 0; i < results.size(); i++) {
80    if (results[i].url == gurl)
81      return true;
82  }
83  return false;
84}
85
86}  // namespace
87
88class TextDatabaseTest : public PlatformTest {
89 public:
90  TextDatabaseTest() {}
91
92 protected:
93  void SetUp() {
94    PlatformTest::SetUp();
95    ASSERT_TRUE(temp_dir_.CreateUniqueTempDir());
96  }
97
98  // Create databases with this function, which will ensure that the files are
99  // deleted on shutdown. Only open one database for each file. Returns NULL on
100  // failure.
101  //
102  // Set |delete_file| to delete any existing file. If we are trying to create
103  // the file for the first time, we don't want a previous test left in a
104  // weird state to have left a file that would affect us.
105  TextDatabase* CreateDB(TextDatabase::DBIdent id,
106                         bool allow_create,
107                         bool delete_file) {
108    TextDatabase* db = new TextDatabase(temp_dir_.path(), id, allow_create);
109
110    if (delete_file)
111      file_util::Delete(db->file_name(), false);
112
113    if (!db->Init()) {
114      delete db;
115      return NULL;
116    }
117    return db;
118  }
119
120  // Directory containing the databases.
121  ScopedTempDir temp_dir_;
122
123  // Name of the main database file.
124  FilePath file_name_;
125};
126
127TEST_F(TextDatabaseTest, AttachDetach) {
128  // First database with one page.
129  const int kIdee1 = 200801;
130  scoped_ptr<TextDatabase> db1(CreateDB(kIdee1, true, true));
131  ASSERT_TRUE(!!db1.get());
132  EXPECT_TRUE(db1->AddPageData(
133      Time::FromInternalValue(kTime1), kURL1, kTitle1, kBody1));
134
135  // Second database with one page.
136  const int kIdee2 = 200802;
137  scoped_ptr<TextDatabase> db2(CreateDB(kIdee2, true, true));
138  ASSERT_TRUE(!!db2.get());
139  EXPECT_TRUE(db2->AddPageData(
140      Time::FromInternalValue(kTime2), kURL2, kTitle2, kBody2));
141
142  // Detach, then reattach database one. The file should exist, so we force
143  // opening an existing file.
144  db1.reset();
145  db1.reset(CreateDB(kIdee1, false, false));
146  ASSERT_TRUE(!!db1.get());
147
148  // We should not be able to attach this random database for which no file
149  // exists.
150  const int kIdeeNoExisto = 999999999;
151  scoped_ptr<TextDatabase> db3(CreateDB(kIdeeNoExisto, false, true));
152  EXPECT_FALSE(!!db3.get());
153}
154
155TEST_F(TextDatabaseTest, AddRemove) {
156  // Create a database and add some pages to it.
157  const int kIdee1 = 200801;
158  scoped_ptr<TextDatabase> db(CreateDB(kIdee1, true, true));
159  ASSERT_TRUE(!!db.get());
160  URLID id1 = db->AddPageData(
161      Time::FromInternalValue(kTime1), kURL1, kTitle1, kBody1);
162  EXPECT_NE(0, id1);
163  URLID id2 = db->AddPageData(
164      Time::FromInternalValue(kTime2), kURL2, kTitle2, kBody2);
165  EXPECT_NE(0, id2);
166  URLID id3 = db->AddPageData(
167      Time::FromInternalValue(kTime3), kURL3, kTitle3, kBody3);
168  EXPECT_NE(0, id3);
169  EXPECT_EQ(3, RowCount(db.get()));
170
171  // Make sure we can delete some of the data.
172  db->DeletePageData(Time::FromInternalValue(kTime1), kURL1);
173  EXPECT_EQ(2, RowCount(db.get()));
174
175  // Close and reopen.
176  db.reset(new TextDatabase(temp_dir_.path(), kIdee1, false));
177  EXPECT_TRUE(db->Init());
178
179  // Verify that the deleted ID is gone and try to delete another one.
180  EXPECT_EQ(2, RowCount(db.get()));
181  db->DeletePageData(Time::FromInternalValue(kTime2), kURL2);
182  EXPECT_EQ(1, RowCount(db.get()));
183}
184
185TEST_F(TextDatabaseTest, Query) {
186  // Make a database with some pages.
187  const int kIdee1 = 200801;
188  scoped_ptr<TextDatabase> db(CreateDB(kIdee1, true, true));
189  EXPECT_TRUE(!!db.get());
190  AddAllTestData(db.get());
191
192  // Get all the results.
193  QueryOptions options;
194  options.begin_time = Time::FromInternalValue(0);
195
196  std::vector<TextDatabase::Match> results;
197  Time first_time_searched;
198  TextDatabase::URLSet unique_urls;
199  db->GetTextMatches("COUNTTAG", options, &results, &unique_urls,
200                     &first_time_searched);
201  EXPECT_TRUE(unique_urls.empty()) << "Didn't ask for unique URLs";
202
203  // All 3 sites should be returned in order.
204  ASSERT_EQ(3U, results.size());
205  EXPECT_EQ(GURL(kURL1), results[2].url);
206  EXPECT_EQ(GURL(kURL2), results[1].url);
207  EXPECT_EQ(GURL(kURL3), results[0].url);
208
209  // Verify the info on those results.
210  EXPECT_TRUE(Time::FromInternalValue(kTime1) == results[2].time);
211  EXPECT_TRUE(Time::FromInternalValue(kTime2) == results[1].time);
212  EXPECT_TRUE(Time::FromInternalValue(kTime3) == results[0].time);
213
214  EXPECT_EQ(std::string(kTitle1), UTF16ToUTF8(results[2].title));
215  EXPECT_EQ(std::string(kTitle2), UTF16ToUTF8(results[1].title));
216  EXPECT_EQ(std::string(kTitle3), UTF16ToUTF8(results[0].title));
217
218  // Should have no matches in the title.
219  EXPECT_EQ(0U, results[0].title_match_positions.size());
220  EXPECT_EQ(0U, results[1].title_match_positions.size());
221  EXPECT_EQ(0U, results[2].title_match_positions.size());
222
223  // We don't want to be dependent on the exact snippet algorithm, but we know
224  // since we searched for "COUNTTAG" which occurs at the beginning of each
225  // document, that each snippet should start with that.
226  EXPECT_TRUE(StartsWithASCII(UTF16ToUTF8(results[0].snippet.text()),
227                              "COUNTTAG", false));
228  EXPECT_TRUE(StartsWithASCII(UTF16ToUTF8(results[1].snippet.text()),
229                              "COUNTTAG", false));
230  EXPECT_TRUE(StartsWithASCII(UTF16ToUTF8(results[2].snippet.text()),
231                              "COUNTTAG", false));
232}
233
234TEST_F(TextDatabaseTest, TimeRange) {
235  // Make a database with some pages.
236  const int kIdee1 = 200801;
237  scoped_ptr<TextDatabase> db(CreateDB(kIdee1, true, true));
238  ASSERT_TRUE(!!db.get());
239  AddAllTestData(db.get());
240
241  // Beginning should be inclusive, and the ending exclusive.
242  // Get all the results.
243  QueryOptions options;
244  options.begin_time = Time::FromInternalValue(kTime1);
245  options.end_time = Time::FromInternalValue(kTime3);
246
247  std::vector<TextDatabase::Match> results;
248  Time first_time_searched;
249  TextDatabase::URLSet unique_urls;
250  db->GetTextMatches("COUNTTAG", options, &results,  &unique_urls,
251                     &first_time_searched);
252  EXPECT_TRUE(unique_urls.empty()) << "Didn't ask for unique URLs";
253
254  // The first and second should have been returned.
255  EXPECT_EQ(2U, results.size());
256  EXPECT_TRUE(ResultsHaveURL(results, kURL1));
257  EXPECT_TRUE(ResultsHaveURL(results, kURL2));
258  EXPECT_FALSE(ResultsHaveURL(results, kURL3));
259  EXPECT_EQ(kTime1, first_time_searched.ToInternalValue());
260
261  // ---------------------------------------------------------------------------
262  // Do a query where there isn't a result on the begin boundary, so we can
263  // test that the first time searched is set to the minimum time considered
264  // instead of the min value.
265  options.begin_time = Time::FromInternalValue((kTime2 - kTime1) / 2 + kTime1);
266  options.end_time = Time::FromInternalValue(kTime3 + 1);
267  results.clear();  // GetTextMatches does *not* clear the results.
268  db->GetTextMatches("COUNTTAG", options, &results, &unique_urls,
269                     &first_time_searched);
270  EXPECT_TRUE(unique_urls.empty()) << "Didn't ask for unique URLs";
271  EXPECT_EQ(options.begin_time.ToInternalValue(),
272            first_time_searched.ToInternalValue());
273
274  // Should have two results, the second and third.
275  EXPECT_EQ(2U, results.size());
276  EXPECT_FALSE(ResultsHaveURL(results, kURL1));
277  EXPECT_TRUE(ResultsHaveURL(results, kURL2));
278  EXPECT_TRUE(ResultsHaveURL(results, kURL3));
279
280  // No results should also set the first_time_searched.
281  options.begin_time = Time::FromInternalValue(kTime3 + 1);
282  options.end_time = Time::FromInternalValue(kTime3 * 100);
283  results.clear();
284  db->GetTextMatches("COUNTTAG", options, &results, &unique_urls,
285                     &first_time_searched);
286  EXPECT_EQ(options.begin_time.ToInternalValue(),
287            first_time_searched.ToInternalValue());
288}
289
290// Make sure that max_count works.
291TEST_F(TextDatabaseTest, MaxCount) {
292  // Make a database with some pages.
293  const int kIdee1 = 200801;
294  scoped_ptr<TextDatabase> db(CreateDB(kIdee1, true, true));
295  ASSERT_TRUE(!!db.get());
296  AddAllTestData(db.get());
297
298  // Set up the query to return all the results with "Google" (should be 2), but
299  // with a maximum of 1.
300  QueryOptions options;
301  options.begin_time = Time::FromInternalValue(kTime1);
302  options.end_time = Time::FromInternalValue(kTime3 + 1);
303  options.max_count = 1;
304
305  std::vector<TextDatabase::Match> results;
306  Time first_time_searched;
307  TextDatabase::URLSet unique_urls;
308  db->GetTextMatches("google", options, &results, &unique_urls,
309                     &first_time_searched);
310  EXPECT_TRUE(unique_urls.empty()) << "Didn't ask for unique URLs";
311
312  // There should be one result, the most recent one.
313  EXPECT_EQ(1U, results.size());
314  EXPECT_TRUE(ResultsHaveURL(results, kURL2));
315
316  // The max time considered should be the date of the returned item.
317  EXPECT_EQ(kTime2, first_time_searched.ToInternalValue());
318}
319
320}  // namespace history
321