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