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. 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. 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 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: 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 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 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 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 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. 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