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
16 using base::Time;
17
18 namespace history {
19
20 namespace {
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.
24 const char kURL1[] = "http://www.google.com/";
25 const int kTime1 = 1000;
26 const char kTitle1[] = "Google";
27 const 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
32 const char kURL2[] = "http://images.google.com/";
33 const int kTime2 = 2000;
34 const char kTitle2[] = "Google Image Search";
35 const 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
42 const char kURL3[] = "http://slashdot.org/";
43 const int kTime3 = 3000;
44 const char kTitle3[] = "Slashdot: News for nerds, stuff that matters";
45 const 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.
RowCount(TextDatabase * db)52 int 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.
AddAllTestData(TextDatabase * db)66 void 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
ResultsHaveURL(const std::vector<TextDatabase::Match> & results,const char * url)76 bool 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
88 class TextDatabaseTest : public PlatformTest {
89 public:
TextDatabaseTest()90 TextDatabaseTest() {}
91
92 protected:
SetUp()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.
CreateDB(TextDatabase::DBIdent id,bool allow_create,bool delete_file)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
TEST_F(TextDatabaseTest,AttachDetach)127 TEST_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
TEST_F(TextDatabaseTest,AddRemove)155 TEST_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
TEST_F(TextDatabaseTest,Query)185 TEST_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
TEST_F(TextDatabaseTest,TimeRange)234 TEST_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.
TEST_F(TextDatabaseTest,MaxCount)291 TEST_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