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 "sync/internal_api/public/base/model_type_test_util.h"
6#include "sync/notifier/invalidation_util.h"
7#include "sync/notifier/object_id_invalidation_map.h"
8#include "sync/sessions/nudge_tracker.h"
9#include "testing/gtest/include/gtest/gtest.h"
10
11namespace syncer {
12
13namespace {
14
15testing::AssertionResult ModelTypeSetEquals(ModelTypeSet a, ModelTypeSet b) {
16  if (a.Equals(b)) {
17    return testing::AssertionSuccess();
18  } else {
19    return testing::AssertionFailure()
20        << "Left side " << ModelTypeSetToString(a)
21        << ", does not match rigth side: " << ModelTypeSetToString(b);
22  }
23}
24
25}  // namespace
26
27namespace sessions {
28
29class NudgeTrackerTest : public ::testing::Test {
30 public:
31  NudgeTrackerTest() {
32    SetInvalidationsInSync();
33  }
34
35  static size_t GetHintBufferSize() {
36    // Assumes that no test has adjusted this size.
37    return NudgeTracker::kDefaultMaxPayloadsPerType;
38  }
39
40  bool InvalidationsOutOfSync() const {
41    // We don't currently track invalidations out of sync on a per-type basis.
42    sync_pb::GetUpdateTriggers gu_trigger;
43    nudge_tracker_.FillProtoMessage(BOOKMARKS, &gu_trigger);
44    return gu_trigger.invalidations_out_of_sync();
45  }
46
47  int ProtoLocallyModifiedCount(ModelType type) const {
48    sync_pb::GetUpdateTriggers gu_trigger;
49    nudge_tracker_.FillProtoMessage(type, &gu_trigger);
50    return gu_trigger.local_modification_nudges();
51  }
52
53  int ProtoRefreshRequestedCount(ModelType type) const {
54    sync_pb::GetUpdateTriggers gu_trigger;
55    nudge_tracker_.FillProtoMessage(type, &gu_trigger);
56    return gu_trigger.datatype_refresh_nudges();
57  }
58
59  void SetInvalidationsInSync() {
60    nudge_tracker_.OnInvalidationsEnabled();
61    nudge_tracker_.RecordSuccessfulSyncCycle();
62  }
63
64 protected:
65  NudgeTracker nudge_tracker_;
66};
67
68// Exercise an empty NudgeTracker.
69// Use with valgrind to detect uninitialized members.
70TEST_F(NudgeTrackerTest, EmptyNudgeTracker) {
71  // Now we're at the normal, "idle" state.
72  EXPECT_FALSE(nudge_tracker_.IsSyncRequired());
73  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired());
74  EXPECT_EQ(sync_pb::GetUpdatesCallerInfo::UNKNOWN,
75            nudge_tracker_.updates_source());
76
77  sync_pb::GetUpdateTriggers gu_trigger;
78  nudge_tracker_.FillProtoMessage(BOOKMARKS, &gu_trigger);
79
80  EXPECT_EQ(sync_pb::GetUpdatesCallerInfo::UNKNOWN,
81            nudge_tracker_.updates_source());
82}
83
84// Verify that nudges override each other based on a priority order.
85// LOCAL < DATATYPE_REFRESH < NOTIFICATION
86TEST_F(NudgeTrackerTest, SourcePriorities) {
87  // Track a local nudge.
88  nudge_tracker_.RecordLocalChange(ModelTypeSet(BOOKMARKS));
89  EXPECT_EQ(sync_pb::GetUpdatesCallerInfo::LOCAL,
90            nudge_tracker_.updates_source());
91
92  // A refresh request will override it.
93  nudge_tracker_.RecordLocalRefreshRequest(ModelTypeSet(TYPED_URLS));
94  EXPECT_EQ(sync_pb::GetUpdatesCallerInfo::DATATYPE_REFRESH,
95            nudge_tracker_.updates_source());
96
97  // Another local nudge will not be enough to change it.
98  nudge_tracker_.RecordLocalChange(ModelTypeSet(BOOKMARKS));
99  EXPECT_EQ(sync_pb::GetUpdatesCallerInfo::DATATYPE_REFRESH,
100            nudge_tracker_.updates_source());
101
102  // An invalidation will override the refresh request source.
103  ObjectIdInvalidationMap invalidation_map =
104      BuildInvalidationMap(PREFERENCES, 1, "hint");
105  nudge_tracker_.RecordRemoteInvalidation(invalidation_map);
106  EXPECT_EQ(sync_pb::GetUpdatesCallerInfo::NOTIFICATION,
107            nudge_tracker_.updates_source());
108
109  // Neither local nudges nor refresh requests will override it.
110  nudge_tracker_.RecordLocalChange(ModelTypeSet(BOOKMARKS));
111  EXPECT_EQ(sync_pb::GetUpdatesCallerInfo::NOTIFICATION,
112            nudge_tracker_.updates_source());
113  nudge_tracker_.RecordLocalRefreshRequest(ModelTypeSet(TYPED_URLS));
114  EXPECT_EQ(sync_pb::GetUpdatesCallerInfo::NOTIFICATION,
115            nudge_tracker_.updates_source());
116}
117
118TEST_F(NudgeTrackerTest, HintCoalescing) {
119  // Easy case: record one hint.
120  {
121    ObjectIdInvalidationMap invalidation_map =
122        BuildInvalidationMap(BOOKMARKS, 1, "bm_hint_1");
123    nudge_tracker_.RecordRemoteInvalidation(invalidation_map);
124
125    sync_pb::GetUpdateTriggers gu_trigger;
126    nudge_tracker_.FillProtoMessage(BOOKMARKS, &gu_trigger);
127    ASSERT_EQ(1, gu_trigger.notification_hint_size());
128    EXPECT_EQ("bm_hint_1", gu_trigger.notification_hint(0));
129    EXPECT_FALSE(gu_trigger.client_dropped_hints());
130  }
131
132  // Record a second hint for the same type.
133  {
134    ObjectIdInvalidationMap invalidation_map =
135        BuildInvalidationMap(BOOKMARKS, 2, "bm_hint_2");
136    nudge_tracker_.RecordRemoteInvalidation(invalidation_map);
137
138    sync_pb::GetUpdateTriggers gu_trigger;
139    nudge_tracker_.FillProtoMessage(BOOKMARKS, &gu_trigger);
140    ASSERT_EQ(2, gu_trigger.notification_hint_size());
141
142    // Expect the most hint recent is last in the list.
143    EXPECT_EQ("bm_hint_1", gu_trigger.notification_hint(0));
144    EXPECT_EQ("bm_hint_2", gu_trigger.notification_hint(1));
145    EXPECT_FALSE(gu_trigger.client_dropped_hints());
146  }
147
148  // Record a hint for a different type.
149  {
150    ObjectIdInvalidationMap invalidation_map =
151        BuildInvalidationMap(PASSWORDS, 1, "pw_hint_1");
152    nudge_tracker_.RecordRemoteInvalidation(invalidation_map);
153
154    // Re-verify the bookmarks to make sure they're unaffected.
155    sync_pb::GetUpdateTriggers bm_gu_trigger;
156    nudge_tracker_.FillProtoMessage(BOOKMARKS, &bm_gu_trigger);
157    ASSERT_EQ(2, bm_gu_trigger.notification_hint_size());
158    EXPECT_EQ("bm_hint_1", bm_gu_trigger.notification_hint(0));
159    EXPECT_EQ("bm_hint_2",
160              bm_gu_trigger.notification_hint(1)); // most recent last.
161    EXPECT_FALSE(bm_gu_trigger.client_dropped_hints());
162
163    // Verify the new type, too.
164    sync_pb::GetUpdateTriggers pw_gu_trigger;
165    nudge_tracker_.FillProtoMessage(PASSWORDS, &pw_gu_trigger);
166    ASSERT_EQ(1, pw_gu_trigger.notification_hint_size());
167    EXPECT_EQ("pw_hint_1", pw_gu_trigger.notification_hint(0));
168    EXPECT_FALSE(pw_gu_trigger.client_dropped_hints());
169  }
170}
171
172TEST_F(NudgeTrackerTest, DropHintsLocally) {
173  ObjectIdInvalidationMap invalidation_map =
174      BuildInvalidationMap(BOOKMARKS, 1, "hint");
175
176  for (size_t i = 0; i < GetHintBufferSize(); ++i) {
177    nudge_tracker_.RecordRemoteInvalidation(invalidation_map);
178  }
179  {
180    sync_pb::GetUpdateTriggers gu_trigger;
181    nudge_tracker_.FillProtoMessage(BOOKMARKS, &gu_trigger);
182    EXPECT_EQ(GetHintBufferSize(),
183              static_cast<size_t>(gu_trigger.notification_hint_size()));
184    EXPECT_FALSE(gu_trigger.client_dropped_hints());
185  }
186
187  // Force an overflow.
188  ObjectIdInvalidationMap invalidation_map2 =
189      BuildInvalidationMap(BOOKMARKS, 1000, "new_hint");
190  nudge_tracker_.RecordRemoteInvalidation(invalidation_map2);
191
192  {
193    sync_pb::GetUpdateTriggers gu_trigger;
194    nudge_tracker_.FillProtoMessage(BOOKMARKS, &gu_trigger);
195    EXPECT_EQ(GetHintBufferSize(),
196              static_cast<size_t>(gu_trigger.notification_hint_size()));
197    EXPECT_TRUE(gu_trigger.client_dropped_hints());
198
199    // Verify the newest hint was not dropped and is the last in the list.
200    EXPECT_EQ("new_hint", gu_trigger.notification_hint(GetHintBufferSize()-1));
201
202    // Verify the oldest hint, too.
203    EXPECT_EQ("hint", gu_trigger.notification_hint(0));
204  }
205}
206
207// TODO(rlarocque): Add trickles support.  See crbug.com/223437.
208// TEST_F(NudgeTrackerTest, DropHintsAtServer);
209
210// Checks the behaviour of the invalidations-out-of-sync flag.
211TEST_F(NudgeTrackerTest, EnableDisableInvalidations) {
212  // Start with invalidations offline.
213  nudge_tracker_.OnInvalidationsDisabled();
214  EXPECT_TRUE(InvalidationsOutOfSync());
215  EXPECT_TRUE(nudge_tracker_.IsGetUpdatesRequired());
216
217  // Simply enabling invalidations does not bring us back into sync.
218  nudge_tracker_.OnInvalidationsEnabled();
219  EXPECT_TRUE(InvalidationsOutOfSync());
220  EXPECT_TRUE(nudge_tracker_.IsGetUpdatesRequired());
221
222  // We must successfully complete a sync cycle while invalidations are enabled
223  // to be sure that we're in sync.
224  nudge_tracker_.RecordSuccessfulSyncCycle();
225  EXPECT_FALSE(InvalidationsOutOfSync());
226  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired());
227
228  // If the invalidator malfunctions, we go become unsynced again.
229  nudge_tracker_.OnInvalidationsDisabled();
230  EXPECT_TRUE(InvalidationsOutOfSync());
231  EXPECT_TRUE(nudge_tracker_.IsGetUpdatesRequired());
232
233  // A sync cycle while invalidations are disabled won't reset the flag.
234  nudge_tracker_.RecordSuccessfulSyncCycle();
235  EXPECT_TRUE(InvalidationsOutOfSync());
236  EXPECT_TRUE(nudge_tracker_.IsGetUpdatesRequired());
237
238  // Nor will the re-enabling of invalidations be sufficient, even now that
239  // we've had a successful sync cycle.
240  nudge_tracker_.RecordSuccessfulSyncCycle();
241  EXPECT_TRUE(InvalidationsOutOfSync());
242  EXPECT_TRUE(nudge_tracker_.IsGetUpdatesRequired());
243}
244
245// Tests that locally modified types are correctly written out to the
246// GetUpdateTriggers proto.
247TEST_F(NudgeTrackerTest, WriteLocallyModifiedTypesToProto) {
248  // Should not be locally modified by default.
249  EXPECT_EQ(0, ProtoLocallyModifiedCount(PREFERENCES));
250
251  // Record a local bookmark change.  Verify it was registered correctly.
252  nudge_tracker_.RecordLocalChange(ModelTypeSet(PREFERENCES));
253  EXPECT_EQ(1, ProtoLocallyModifiedCount(PREFERENCES));
254
255  // Record a successful sync cycle.  Verify the count is cleared.
256  nudge_tracker_.RecordSuccessfulSyncCycle();
257  EXPECT_EQ(0, ProtoLocallyModifiedCount(PREFERENCES));
258}
259
260// Tests that refresh requested types are correctly written out to the
261// GetUpdateTriggers proto.
262TEST_F(NudgeTrackerTest, WriteRefreshRequestedTypesToProto) {
263  // There should be no refresh requested by default.
264  EXPECT_EQ(0, ProtoRefreshRequestedCount(SESSIONS));
265
266  // Record a local refresh request.  Verify it was registered correctly.
267  nudge_tracker_.RecordLocalRefreshRequest(ModelTypeSet(SESSIONS));
268  EXPECT_EQ(1, ProtoRefreshRequestedCount(SESSIONS));
269
270  // Record a successful sync cycle.  Verify the count is cleared.
271  nudge_tracker_.RecordSuccessfulSyncCycle();
272  EXPECT_EQ(0, ProtoRefreshRequestedCount(SESSIONS));
273}
274
275// Basic tests for the IsSyncRequired() flag.
276TEST_F(NudgeTrackerTest, IsSyncRequired) {
277  EXPECT_FALSE(nudge_tracker_.IsSyncRequired());
278
279  // Local changes.
280  nudge_tracker_.RecordLocalChange(ModelTypeSet(SESSIONS));
281  EXPECT_TRUE(nudge_tracker_.IsSyncRequired());
282  nudge_tracker_.RecordSuccessfulSyncCycle();
283  EXPECT_FALSE(nudge_tracker_.IsSyncRequired());
284
285  // Refresh requests.
286  nudge_tracker_.RecordLocalRefreshRequest(ModelTypeSet(SESSIONS));
287  EXPECT_TRUE(nudge_tracker_.IsSyncRequired());
288  nudge_tracker_.RecordSuccessfulSyncCycle();
289  EXPECT_FALSE(nudge_tracker_.IsSyncRequired());
290
291  // Invalidations.
292  ObjectIdInvalidationMap invalidation_map =
293      BuildInvalidationMap(PREFERENCES, 1, "hint");
294  nudge_tracker_.RecordRemoteInvalidation(invalidation_map);
295  EXPECT_TRUE(nudge_tracker_.IsSyncRequired());
296  nudge_tracker_.RecordSuccessfulSyncCycle();
297  EXPECT_FALSE(nudge_tracker_.IsSyncRequired());
298}
299
300// Basic tests for the IsGetUpdatesRequired() flag.
301TEST_F(NudgeTrackerTest, IsGetUpdatesRequired) {
302  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired());
303
304  // Local changes.
305  nudge_tracker_.RecordLocalChange(ModelTypeSet(SESSIONS));
306  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired());
307  nudge_tracker_.RecordSuccessfulSyncCycle();
308  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired());
309
310  // Refresh requests.
311  nudge_tracker_.RecordLocalRefreshRequest(ModelTypeSet(SESSIONS));
312  EXPECT_TRUE(nudge_tracker_.IsGetUpdatesRequired());
313  nudge_tracker_.RecordSuccessfulSyncCycle();
314  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired());
315
316  // Invalidations.
317  ObjectIdInvalidationMap invalidation_map =
318      BuildInvalidationMap(PREFERENCES, 1, "hint");
319  nudge_tracker_.RecordRemoteInvalidation(invalidation_map);
320  EXPECT_TRUE(nudge_tracker_.IsGetUpdatesRequired());
321  nudge_tracker_.RecordSuccessfulSyncCycle();
322  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired());
323}
324
325// Test IsSyncRequired() responds correctly to data type throttling.
326TEST_F(NudgeTrackerTest, IsSyncRequired_Throttling) {
327  const base::TimeTicks t0 = base::TimeTicks::FromInternalValue(1234);
328  const base::TimeDelta throttle_length = base::TimeDelta::FromMinutes(10);
329  const base::TimeTicks t1 = t0 + throttle_length;
330
331  EXPECT_FALSE(nudge_tracker_.IsSyncRequired());
332
333  // A local change to sessions enables the flag.
334  nudge_tracker_.RecordLocalChange(ModelTypeSet(SESSIONS));
335  EXPECT_TRUE(nudge_tracker_.IsSyncRequired());
336
337  // But the throttling of sessions unsets it.
338  nudge_tracker_.SetTypesThrottledUntil(ModelTypeSet(SESSIONS),
339                                       throttle_length,
340                                       t0);
341  EXPECT_FALSE(nudge_tracker_.IsSyncRequired());
342
343  // A refresh request for bookmarks means we have reason to sync again.
344  nudge_tracker_.RecordLocalRefreshRequest(ModelTypeSet(BOOKMARKS));
345  EXPECT_TRUE(nudge_tracker_.IsSyncRequired());
346
347  // A successful sync cycle means we took care of bookmarks.
348  nudge_tracker_.RecordSuccessfulSyncCycle();
349  EXPECT_FALSE(nudge_tracker_.IsSyncRequired());
350
351  // But we still haven't dealt with sessions.  We'll need to remember
352  // that sessions are out of sync and re-enable the flag when their
353  // throttling interval expires.
354  nudge_tracker_.UpdateTypeThrottlingState(t1);
355  EXPECT_FALSE(nudge_tracker_.IsTypeThrottled(SESSIONS));
356  EXPECT_TRUE(nudge_tracker_.IsSyncRequired());
357}
358
359// Test IsGetUpdatesRequired() responds correctly to data type throttling.
360TEST_F(NudgeTrackerTest, IsGetUpdatesRequired_Throttling) {
361  const base::TimeTicks t0 = base::TimeTicks::FromInternalValue(1234);
362  const base::TimeDelta throttle_length = base::TimeDelta::FromMinutes(10);
363  const base::TimeTicks t1 = t0 + throttle_length;
364
365  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired());
366
367  // A refresh request to sessions enables the flag.
368  nudge_tracker_.RecordLocalRefreshRequest(ModelTypeSet(SESSIONS));
369  EXPECT_TRUE(nudge_tracker_.IsGetUpdatesRequired());
370
371  // But the throttling of sessions unsets it.
372  nudge_tracker_.SetTypesThrottledUntil(ModelTypeSet(SESSIONS),
373                                       throttle_length,
374                                       t0);
375  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired());
376
377  // A refresh request for bookmarks means we have reason to sync again.
378  nudge_tracker_.RecordLocalRefreshRequest(ModelTypeSet(BOOKMARKS));
379  EXPECT_TRUE(nudge_tracker_.IsGetUpdatesRequired());
380
381  // A successful sync cycle means we took care of bookmarks.
382  nudge_tracker_.RecordSuccessfulSyncCycle();
383  EXPECT_FALSE(nudge_tracker_.IsGetUpdatesRequired());
384
385  // But we still haven't dealt with sessions.  We'll need to remember
386  // that sessions are out of sync and re-enable the flag when their
387  // throttling interval expires.
388  nudge_tracker_.UpdateTypeThrottlingState(t1);
389  EXPECT_FALSE(nudge_tracker_.IsTypeThrottled(SESSIONS));
390  EXPECT_TRUE(nudge_tracker_.IsGetUpdatesRequired());
391}
392
393// Tests throttling-related getter functions when no types are throttled.
394TEST_F(NudgeTrackerTest, NoTypesThrottled) {
395  EXPECT_FALSE(nudge_tracker_.IsAnyTypeThrottled());
396  EXPECT_FALSE(nudge_tracker_.IsTypeThrottled(SESSIONS));
397  EXPECT_TRUE(nudge_tracker_.GetThrottledTypes().Empty());
398}
399
400// Tests throttling-related getter functions when some types are throttled.
401TEST_F(NudgeTrackerTest, ThrottleAndUnthrottle) {
402  const base::TimeTicks t0 = base::TimeTicks::FromInternalValue(1234);
403  const base::TimeDelta throttle_length = base::TimeDelta::FromMinutes(10);
404  const base::TimeTicks t1 = t0 + throttle_length;
405
406  nudge_tracker_.SetTypesThrottledUntil(ModelTypeSet(SESSIONS, PREFERENCES),
407                                       throttle_length,
408                                       t0);
409
410  EXPECT_TRUE(nudge_tracker_.IsAnyTypeThrottled());
411  EXPECT_TRUE(nudge_tracker_.IsTypeThrottled(SESSIONS));
412  EXPECT_TRUE(nudge_tracker_.IsTypeThrottled(PREFERENCES));
413  EXPECT_FALSE(nudge_tracker_.GetThrottledTypes().Empty());
414  EXPECT_EQ(throttle_length, nudge_tracker_.GetTimeUntilNextUnthrottle(t0));
415
416  nudge_tracker_.UpdateTypeThrottlingState(t1);
417
418  EXPECT_FALSE(nudge_tracker_.IsAnyTypeThrottled());
419  EXPECT_FALSE(nudge_tracker_.IsTypeThrottled(SESSIONS));
420  EXPECT_TRUE(nudge_tracker_.GetThrottledTypes().Empty());
421}
422
423TEST_F(NudgeTrackerTest, OverlappingThrottleIntervals) {
424  const base::TimeTicks t0 = base::TimeTicks::FromInternalValue(1234);
425  const base::TimeDelta throttle1_length = base::TimeDelta::FromMinutes(10);
426  const base::TimeDelta throttle2_length = base::TimeDelta::FromMinutes(20);
427  const base::TimeTicks t1 = t0 + throttle1_length;
428  const base::TimeTicks t2 = t0 + throttle2_length;
429
430  // Setup the longer of two intervals.
431  nudge_tracker_.SetTypesThrottledUntil(ModelTypeSet(SESSIONS, PREFERENCES),
432                                       throttle2_length,
433                                       t0);
434  EXPECT_TRUE(ModelTypeSetEquals(
435          ModelTypeSet(SESSIONS, PREFERENCES),
436          nudge_tracker_.GetThrottledTypes()));
437  EXPECT_EQ(throttle2_length,
438            nudge_tracker_.GetTimeUntilNextUnthrottle(t0));
439
440  // Setup the shorter interval.
441  nudge_tracker_.SetTypesThrottledUntil(ModelTypeSet(SESSIONS, BOOKMARKS),
442                                       throttle1_length,
443                                       t0);
444  EXPECT_TRUE(ModelTypeSetEquals(
445          ModelTypeSet(SESSIONS, PREFERENCES, BOOKMARKS),
446          nudge_tracker_.GetThrottledTypes()));
447  EXPECT_EQ(throttle1_length,
448            nudge_tracker_.GetTimeUntilNextUnthrottle(t0));
449
450  // Expire the first interval.
451  nudge_tracker_.UpdateTypeThrottlingState(t1);
452
453  // SESSIONS appeared in both intervals.  We expect it will be throttled for
454  // the longer of the two, so it's still throttled at time t1.
455  EXPECT_TRUE(ModelTypeSetEquals(
456          ModelTypeSet(SESSIONS, PREFERENCES),
457          nudge_tracker_.GetThrottledTypes()));
458  EXPECT_EQ(throttle2_length - throttle1_length,
459            nudge_tracker_.GetTimeUntilNextUnthrottle(t1));
460
461  // Expire the second interval.
462  nudge_tracker_.UpdateTypeThrottlingState(t2);
463  EXPECT_TRUE(nudge_tracker_.GetThrottledTypes().Empty());
464}
465
466}  // namespace sessions
467}  // namespace syncer
468