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