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/strings/string_piece.h" 8 #include "base/strings/utf_string_conversions.h" 9 #include "base/synchronization/waitable_event.h" 10 #include "base/test/test_timeouts.h" 11 #include "base/time/time.h" 12 #include "chrome/browser/history/history_types.h" 13 #include "chrome/browser/sync/glue/typed_url_model_associator.h" 14 #include "chrome/browser/sync/profile_sync_service_mock.h" 15 #include "content/public/test/test_browser_thread.h" 16 #include "sync/protocol/typed_url_specifics.pb.h" 17 #include "testing/gtest/include/gtest/gtest.h" 18 #include "url/gurl.h" 19 20 using browser_sync::TypedUrlModelAssociator; 21 using content::BrowserThread; 22 23 namespace { 24 class SyncTypedUrlModelAssociatorTest : public testing::Test { 25 public: 26 static history::URLRow MakeTypedUrlRow(const char* url, 27 const char* title, 28 int typed_count, 29 int64 last_visit, 30 bool hidden, 31 history::VisitVector* visits) { 32 GURL gurl(url); 33 history::URLRow history_url(gurl); 34 history_url.set_title(UTF8ToUTF16(title)); 35 history_url.set_typed_count(typed_count); 36 history_url.set_last_visit( 37 base::Time::FromInternalValue(last_visit)); 38 history_url.set_hidden(hidden); 39 visits->push_back(history::VisitRow( 40 history_url.id(), history_url.last_visit(), 0, 41 content::PAGE_TRANSITION_RELOAD, 0)); 42 history_url.set_visit_count(visits->size()); 43 return history_url; 44 } 45 46 static sync_pb::TypedUrlSpecifics MakeTypedUrlSpecifics(const char* url, 47 const char* title, 48 int64 last_visit, 49 bool hidden) { 50 sync_pb::TypedUrlSpecifics typed_url; 51 typed_url.set_url(url); 52 typed_url.set_title(title); 53 typed_url.set_hidden(hidden); 54 typed_url.add_visits(last_visit); 55 typed_url.add_visit_transitions(content::PAGE_TRANSITION_TYPED); 56 return typed_url; 57 } 58 59 static bool URLsEqual(history::URLRow& lhs, history::URLRow& rhs) { 60 // Only compare synced fields (ignore typed_count and visit_count as those 61 // are maintained by the history subsystem). 62 return (lhs.url().spec().compare(rhs.url().spec()) == 0) && 63 (lhs.title().compare(rhs.title()) == 0) && 64 (lhs.hidden() == rhs.hidden()); 65 } 66 }; 67 68 class TestTypedUrlModelAssociator : public TypedUrlModelAssociator { 69 public: 70 TestTypedUrlModelAssociator() 71 : TypedUrlModelAssociator(&mock_, NULL, NULL) {} 72 private: 73 ProfileSyncServiceMock mock_; 74 }; 75 76 static void CreateModelAssociatorAsync(base::WaitableEvent* startup, 77 base::WaitableEvent* aborted, 78 base::WaitableEvent* done, 79 TypedUrlModelAssociator** associator) { 80 // Grab the done lock - when we exit, this will be released and allow the 81 // test to finish. 82 *associator = new TestTypedUrlModelAssociator(); 83 84 // Signal frontend to call AbortAssociation and proceed after it's called. 85 startup->Signal(); 86 aborted->Wait(); 87 syncer::SyncError error = (*associator)->AssociateModels(NULL, NULL); 88 EXPECT_TRUE(error.IsSet()); 89 EXPECT_EQ("datatype error was encountered: Association was aborted.", 90 error.message()); 91 delete *associator; 92 done->Signal(); 93 } 94 95 } // namespace 96 97 TEST_F(SyncTypedUrlModelAssociatorTest, MergeUrls) { 98 history::VisitVector visits1; 99 history::URLRow row1(MakeTypedUrlRow("http://pie.com/", "pie", 100 2, 3, false, &visits1)); 101 sync_pb::TypedUrlSpecifics specs1(MakeTypedUrlSpecifics("http://pie.com/", 102 "pie", 103 3, false)); 104 history::URLRow new_row1(GURL("http://pie.com/")); 105 std::vector<history::VisitInfo> new_visits1; 106 EXPECT_TRUE(TypedUrlModelAssociator::MergeUrls(specs1, row1, &visits1, 107 &new_row1, &new_visits1) == TypedUrlModelAssociator::DIFF_NONE); 108 109 history::VisitVector visits2; 110 history::URLRow row2(MakeTypedUrlRow("http://pie.com/", "pie", 111 2, 3, false, &visits2)); 112 sync_pb::TypedUrlSpecifics specs2(MakeTypedUrlSpecifics("http://pie.com/", 113 "pie", 114 3, true)); 115 history::VisitVector expected_visits2; 116 history::URLRow expected2(MakeTypedUrlRow("http://pie.com/", "pie", 117 2, 3, true, &expected_visits2)); 118 history::URLRow new_row2(GURL("http://pie.com/")); 119 std::vector<history::VisitInfo> new_visits2; 120 EXPECT_TRUE(TypedUrlModelAssociator::MergeUrls(specs2, row2, &visits2, 121 &new_row2, &new_visits2) == 122 TypedUrlModelAssociator::DIFF_LOCAL_ROW_CHANGED); 123 EXPECT_TRUE(URLsEqual(new_row2, expected2)); 124 125 history::VisitVector visits3; 126 history::URLRow row3(MakeTypedUrlRow("http://pie.com/", "pie", 127 2, 3, false, &visits3)); 128 sync_pb::TypedUrlSpecifics specs3(MakeTypedUrlSpecifics("http://pie.com/", 129 "pie2", 130 3, true)); 131 history::VisitVector expected_visits3; 132 history::URLRow expected3(MakeTypedUrlRow("http://pie.com/", "pie2", 133 2, 3, true, &expected_visits3)); 134 history::URLRow new_row3(GURL("http://pie.com/")); 135 std::vector<history::VisitInfo> new_visits3; 136 EXPECT_EQ(TypedUrlModelAssociator::DIFF_LOCAL_ROW_CHANGED | 137 TypedUrlModelAssociator::DIFF_NONE, 138 TypedUrlModelAssociator::MergeUrls(specs3, row3, &visits3, 139 &new_row3, &new_visits3)); 140 EXPECT_TRUE(URLsEqual(new_row3, expected3)); 141 142 // Create one node in history DB with timestamp of 3, and one node in sync 143 // DB with timestamp of 4. Result should contain one new item (4). 144 history::VisitVector visits4; 145 history::URLRow row4(MakeTypedUrlRow("http://pie.com/", "pie", 146 2, 3, false, &visits4)); 147 sync_pb::TypedUrlSpecifics specs4(MakeTypedUrlSpecifics("http://pie.com/", 148 "pie2", 149 4, false)); 150 history::VisitVector expected_visits4; 151 history::URLRow expected4(MakeTypedUrlRow("http://pie.com/", "pie2", 152 2, 4, false, &expected_visits4)); 153 history::URLRow new_row4(GURL("http://pie.com/")); 154 std::vector<history::VisitInfo> new_visits4; 155 EXPECT_EQ(TypedUrlModelAssociator::DIFF_UPDATE_NODE | 156 TypedUrlModelAssociator::DIFF_LOCAL_ROW_CHANGED | 157 TypedUrlModelAssociator::DIFF_LOCAL_VISITS_ADDED, 158 TypedUrlModelAssociator::MergeUrls(specs4, row4, &visits4, 159 &new_row4, &new_visits4)); 160 EXPECT_EQ(1U, new_visits4.size()); 161 EXPECT_EQ(specs4.visits(0), new_visits4[0].first.ToInternalValue()); 162 EXPECT_TRUE(URLsEqual(new_row4, expected4)); 163 EXPECT_EQ(2U, visits4.size()); 164 165 history::VisitVector visits5; 166 history::URLRow row5(MakeTypedUrlRow("http://pie.com/", "pie", 167 1, 4, false, &visits5)); 168 sync_pb::TypedUrlSpecifics specs5(MakeTypedUrlSpecifics("http://pie.com/", 169 "pie", 170 3, false)); 171 history::VisitVector expected_visits5; 172 history::URLRow expected5(MakeTypedUrlRow("http://pie.com/", "pie", 173 2, 3, false, &expected_visits5)); 174 history::URLRow new_row5(GURL("http://pie.com/")); 175 std::vector<history::VisitInfo> new_visits5; 176 177 // UPDATE_NODE should be set because row5 has a newer last_visit timestamp. 178 EXPECT_EQ(TypedUrlModelAssociator::DIFF_UPDATE_NODE | 179 TypedUrlModelAssociator::DIFF_NONE, 180 TypedUrlModelAssociator::MergeUrls(specs5, row5, &visits5, 181 &new_row5, &new_visits5)); 182 EXPECT_TRUE(URLsEqual(new_row5, expected5)); 183 EXPECT_EQ(0U, new_visits5.size()); 184 } 185 186 TEST_F(SyncTypedUrlModelAssociatorTest, MergeUrlsAfterExpiration) { 187 // Tests to ensure that we don't resurrect expired URLs (URLs that have been 188 // deleted from the history DB but still exist in the sync DB). 189 190 // First, create a history row that has two visits, with timestamps 2 and 3. 191 history::VisitVector(history_visits); 192 history_visits.push_back(history::VisitRow( 193 0, base::Time::FromInternalValue(2), 0, content::PAGE_TRANSITION_TYPED, 194 0)); 195 history::URLRow history_url(MakeTypedUrlRow("http://pie.com/", "pie", 196 2, 3, false, &history_visits)); 197 198 // Now, create a sync node with visits at timestamps 1, 2, 3, 4. 199 sync_pb::TypedUrlSpecifics node(MakeTypedUrlSpecifics("http://pie.com/", 200 "pie", 1, false)); 201 node.add_visits(2); 202 node.add_visits(3); 203 node.add_visits(4); 204 node.add_visit_transitions(2); 205 node.add_visit_transitions(3); 206 node.add_visit_transitions(4); 207 history::URLRow new_history_url(history_url.url()); 208 std::vector<history::VisitInfo> new_visits; 209 EXPECT_EQ(TypedUrlModelAssociator::DIFF_NONE | 210 TypedUrlModelAssociator::DIFF_LOCAL_VISITS_ADDED, 211 TypedUrlModelAssociator::MergeUrls( 212 node, history_url, &history_visits, &new_history_url, 213 &new_visits)); 214 EXPECT_TRUE(URLsEqual(history_url, new_history_url)); 215 EXPECT_EQ(1U, new_visits.size()); 216 EXPECT_EQ(4U, new_visits[0].first.ToInternalValue()); 217 // We should not sync the visit with timestamp #1 since it is earlier than 218 // any other visit for this URL in the history DB. But we should sync visit 219 // #4. 220 EXPECT_EQ(3U, history_visits.size()); 221 EXPECT_EQ(2U, history_visits[0].visit_time.ToInternalValue()); 222 EXPECT_EQ(3U, history_visits[1].visit_time.ToInternalValue()); 223 EXPECT_EQ(4U, history_visits[2].visit_time.ToInternalValue()); 224 } 225 226 TEST_F(SyncTypedUrlModelAssociatorTest, DiffVisitsSame) { 227 history::VisitVector old_visits; 228 sync_pb::TypedUrlSpecifics new_url; 229 230 const int64 visits[] = { 1024, 2065, 65534, 1237684 }; 231 232 for (size_t c = 0; c < arraysize(visits); ++c) { 233 old_visits.push_back(history::VisitRow( 234 0, base::Time::FromInternalValue(visits[c]), 0, 235 content::PAGE_TRANSITION_TYPED, 0)); 236 new_url.add_visits(visits[c]); 237 new_url.add_visit_transitions(content::PAGE_TRANSITION_TYPED); 238 } 239 240 std::vector<history::VisitInfo> new_visits; 241 history::VisitVector removed_visits; 242 243 TypedUrlModelAssociator::DiffVisits(old_visits, new_url, 244 &new_visits, &removed_visits); 245 EXPECT_TRUE(new_visits.empty()); 246 EXPECT_TRUE(removed_visits.empty()); 247 } 248 249 TEST_F(SyncTypedUrlModelAssociatorTest, DiffVisitsRemove) { 250 history::VisitVector old_visits; 251 sync_pb::TypedUrlSpecifics new_url; 252 253 const int64 visits_left[] = { 1, 2, 1024, 1500, 2065, 6000, 254 65534, 1237684, 2237684 }; 255 const int64 visits_right[] = { 1024, 2065, 65534, 1237684 }; 256 257 // DiffVisits will not remove the first visit, because we never delete visits 258 // from the start of the array (since those visits can get truncated by the 259 // size-limiting code). 260 const int64 visits_removed[] = { 1500, 6000, 2237684 }; 261 262 for (size_t c = 0; c < arraysize(visits_left); ++c) { 263 old_visits.push_back(history::VisitRow( 264 0, base::Time::FromInternalValue(visits_left[c]), 0, 265 content::PAGE_TRANSITION_TYPED, 0)); 266 } 267 268 for (size_t c = 0; c < arraysize(visits_right); ++c) { 269 new_url.add_visits(visits_right[c]); 270 new_url.add_visit_transitions(content::PAGE_TRANSITION_TYPED); 271 } 272 273 std::vector<history::VisitInfo> new_visits; 274 history::VisitVector removed_visits; 275 276 TypedUrlModelAssociator::DiffVisits(old_visits, new_url, 277 &new_visits, &removed_visits); 278 EXPECT_TRUE(new_visits.empty()); 279 ASSERT_EQ(removed_visits.size(), arraysize(visits_removed)); 280 for (size_t c = 0; c < arraysize(visits_removed); ++c) { 281 EXPECT_EQ(removed_visits[c].visit_time.ToInternalValue(), 282 visits_removed[c]); 283 } 284 } 285 286 TEST_F(SyncTypedUrlModelAssociatorTest, DiffVisitsAdd) { 287 history::VisitVector old_visits; 288 sync_pb::TypedUrlSpecifics new_url; 289 290 const int64 visits_left[] = { 1024, 2065, 65534, 1237684 }; 291 const int64 visits_right[] = { 1, 1024, 1500, 2065, 6000, 292 65534, 1237684, 2237684 }; 293 294 const int64 visits_added[] = { 1, 1500, 6000, 2237684 }; 295 296 for (size_t c = 0; c < arraysize(visits_left); ++c) { 297 old_visits.push_back(history::VisitRow( 298 0, base::Time::FromInternalValue(visits_left[c]), 0, 299 content::PAGE_TRANSITION_TYPED, 0)); 300 } 301 302 for (size_t c = 0; c < arraysize(visits_right); ++c) { 303 new_url.add_visits(visits_right[c]); 304 new_url.add_visit_transitions(content::PAGE_TRANSITION_TYPED); 305 } 306 307 std::vector<history::VisitInfo> new_visits; 308 history::VisitVector removed_visits; 309 310 TypedUrlModelAssociator::DiffVisits(old_visits, new_url, 311 &new_visits, &removed_visits); 312 EXPECT_TRUE(removed_visits.empty()); 313 ASSERT_TRUE(new_visits.size() == arraysize(visits_added)); 314 for (size_t c = 0; c < arraysize(visits_added); ++c) { 315 EXPECT_EQ(new_visits[c].first.ToInternalValue(), 316 visits_added[c]); 317 EXPECT_EQ(new_visits[c].second, content::PAGE_TRANSITION_TYPED); 318 } 319 } 320 321 static history::VisitRow CreateVisit(content::PageTransition type, 322 int64 timestamp) { 323 return history::VisitRow(0, base::Time::FromInternalValue(timestamp), 0, 324 type, 0); 325 } 326 327 TEST_F(SyncTypedUrlModelAssociatorTest, WriteTypedUrlSpecifics) { 328 history::VisitVector visits; 329 visits.push_back(CreateVisit(content::PAGE_TRANSITION_TYPED, 1)); 330 visits.push_back(CreateVisit(content::PAGE_TRANSITION_RELOAD, 2)); 331 visits.push_back(CreateVisit(content::PAGE_TRANSITION_LINK, 3)); 332 333 history::URLRow url(MakeTypedUrlRow("http://pie.com/", "pie", 334 1, 100, false, &visits)); 335 sync_pb::TypedUrlSpecifics typed_url; 336 TypedUrlModelAssociator::WriteToTypedUrlSpecifics(url, visits, &typed_url); 337 // RELOAD visits should be removed. 338 EXPECT_EQ(2, typed_url.visits_size()); 339 EXPECT_EQ(typed_url.visit_transitions_size(), typed_url.visits_size()); 340 EXPECT_EQ(1, typed_url.visits(0)); 341 EXPECT_EQ(3, typed_url.visits(1)); 342 EXPECT_EQ(content::PAGE_TRANSITION_TYPED, 343 static_cast<content::PageTransition>(typed_url.visit_transitions(0))); 344 EXPECT_EQ(content::PAGE_TRANSITION_LINK, 345 static_cast<content::PageTransition>(typed_url.visit_transitions(1))); 346 } 347 348 TEST_F(SyncTypedUrlModelAssociatorTest, TooManyVisits) { 349 history::VisitVector visits; 350 int64 timestamp = 1000; 351 visits.push_back(CreateVisit(content::PAGE_TRANSITION_TYPED, timestamp++)); 352 for (int i = 0 ; i < 100; ++i) { 353 visits.push_back(CreateVisit(content::PAGE_TRANSITION_LINK, timestamp++)); 354 } 355 history::URLRow url(MakeTypedUrlRow("http://pie.com/", "pie", 356 1, timestamp++, false, &visits)); 357 sync_pb::TypedUrlSpecifics typed_url; 358 TypedUrlModelAssociator::WriteToTypedUrlSpecifics(url, visits, &typed_url); 359 // # visits should be capped at 100. 360 EXPECT_EQ(100, typed_url.visits_size()); 361 EXPECT_EQ(typed_url.visit_transitions_size(), typed_url.visits_size()); 362 EXPECT_EQ(1000, typed_url.visits(0)); 363 // Visit with timestamp of 1001 should be omitted since we should have 364 // skipped that visit to stay under the cap. 365 EXPECT_EQ(1002, typed_url.visits(1)); 366 EXPECT_EQ(content::PAGE_TRANSITION_TYPED, 367 static_cast<content::PageTransition>(typed_url.visit_transitions(0))); 368 EXPECT_EQ(content::PAGE_TRANSITION_LINK, 369 static_cast<content::PageTransition>(typed_url.visit_transitions(1))); 370 } 371 372 TEST_F(SyncTypedUrlModelAssociatorTest, TooManyTypedVisits) { 373 history::VisitVector visits; 374 int64 timestamp = 1000; 375 for (int i = 0 ; i < 102; ++i) { 376 visits.push_back(CreateVisit(content::PAGE_TRANSITION_TYPED, timestamp++)); 377 visits.push_back(CreateVisit(content::PAGE_TRANSITION_LINK, timestamp++)); 378 visits.push_back(CreateVisit(content::PAGE_TRANSITION_RELOAD, timestamp++)); 379 } 380 history::URLRow url(MakeTypedUrlRow("http://pie.com/", "pie", 381 1, timestamp++, false, &visits)); 382 sync_pb::TypedUrlSpecifics typed_url; 383 TypedUrlModelAssociator::WriteToTypedUrlSpecifics(url, visits, &typed_url); 384 // # visits should be capped at 100. 385 EXPECT_EQ(100, typed_url.visits_size()); 386 EXPECT_EQ(typed_url.visit_transitions_size(), typed_url.visits_size()); 387 // First two typed visits should be skipped. 388 EXPECT_EQ(1006, typed_url.visits(0)); 389 390 // Ensure there are no non-typed visits since that's all that should fit. 391 for (int i = 0; i < typed_url.visits_size(); ++i) { 392 EXPECT_EQ(content::PAGE_TRANSITION_TYPED, 393 static_cast<content::PageTransition>(typed_url.visit_transitions(i))); 394 } 395 } 396 397 TEST_F(SyncTypedUrlModelAssociatorTest, NoTypedVisits) { 398 history::VisitVector visits; 399 history::URLRow url(MakeTypedUrlRow("http://pie.com/", "pie", 400 1, 1000, false, &visits)); 401 sync_pb::TypedUrlSpecifics typed_url; 402 TypedUrlModelAssociator::WriteToTypedUrlSpecifics(url, visits, &typed_url); 403 // URLs with no typed URL visits should be translated to a URL with one 404 // reload visit. 405 EXPECT_EQ(1, typed_url.visits_size()); 406 EXPECT_EQ(typed_url.visit_transitions_size(), typed_url.visits_size()); 407 // First two typed visits should be skipped. 408 EXPECT_EQ(1000, typed_url.visits(0)); 409 EXPECT_EQ(content::PAGE_TRANSITION_RELOAD, 410 static_cast<content::PageTransition>(typed_url.visit_transitions(0))); 411 } 412 413 // This test verifies that we can abort model association from the UI thread. 414 // We start up the model associator on the DB thread, block until we abort the 415 // association on the UI thread, then ensure that AssociateModels() returns 416 // false. 417 TEST_F(SyncTypedUrlModelAssociatorTest, TestAbort) { 418 content::TestBrowserThread db_thread(BrowserThread::DB); 419 base::WaitableEvent startup(false, false); 420 base::WaitableEvent aborted(false, false); 421 base::WaitableEvent done(false, false); 422 TypedUrlModelAssociator* associator; 423 // Fire off to the DB thread to create the model associator and start 424 // model association. 425 db_thread.Start(); 426 base::Closure callback = base::Bind( 427 &CreateModelAssociatorAsync, &startup, &aborted, &done, &associator); 428 BrowserThread::PostTask(BrowserThread::DB, FROM_HERE, callback); 429 // Wait for the model associator to get created and start assocation. 430 ASSERT_TRUE(startup.TimedWait(TestTimeouts::action_timeout())); 431 // Abort the model assocation - this should be callable from any thread. 432 associator->AbortAssociation(); 433 // Tell the remote thread to continue. 434 aborted.Signal(); 435 // Block until CreateModelAssociator() exits. 436 ASSERT_TRUE(done.TimedWait(TestTimeouts::action_timeout())); 437 } 438