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.
5 #include "sync/engine/process_commit_response_command.h"
9 #include "base/location.h"
10 #include "base/stringprintf.h"
11 #include "sync/protocol/bookmark_specifics.pb.h"
12 #include "sync/protocol/sync.pb.h"
13 #include "sync/sessions/sync_session.h"
14 #include "sync/syncable/entry.h"
15 #include "sync/syncable/mutable_entry.h"
16 #include "sync/syncable/read_transaction.h"
17 #include "sync/syncable/syncable_id.h"
18 #include "sync/syncable/write_transaction.h"
19 #include "sync/test/engine/fake_model_worker.h"
20 #include "sync/test/engine/syncer_command_test.h"
21 #include "sync/test/engine/test_id_factory.h"
22 #include "testing/gtest/include/gtest/gtest.h"
26 using sessions::SyncSession
;
28 using syncable::BASE_VERSION
;
29 using syncable::Entry
;
30 using syncable::IS_DIR
;
31 using syncable::IS_UNSYNCED
;
33 using syncable::MutableEntry
;
34 using syncable::NON_UNIQUE_NAME
;
35 using syncable::UNITTEST
;
36 using syncable::WriteTransaction
;
38 // A test fixture for tests exercising ProcessCommitResponseCommand.
39 class ProcessCommitResponseCommandTest
: public SyncerCommandTest
{
41 virtual void SetUp() {
43 mutable_routing_info()->clear();
46 make_scoped_refptr(new FakeModelWorker(GROUP_DB
)));
48 make_scoped_refptr(new FakeModelWorker(GROUP_UI
)));
49 (*mutable_routing_info())[syncer::BOOKMARKS
] = GROUP_UI
;
50 (*mutable_routing_info())[syncer::PREFERENCES
] = GROUP_UI
;
51 (*mutable_routing_info())[syncer::AUTOFILL
] = GROUP_DB
;
53 SyncerCommandTest::SetUp();
58 ProcessCommitResponseCommandTest()
59 : next_old_revision_(1),
60 next_new_revision_(4000),
61 next_server_position_(10000) {
64 void CheckEntry(Entry
* e
, const std::string
& name
,
65 syncer::ModelType model_type
, const Id
& parent_id
) {
66 EXPECT_TRUE(e
->good());
67 ASSERT_EQ(name
, e
->Get(NON_UNIQUE_NAME
));
68 ASSERT_EQ(model_type
, e
->GetModelType());
69 ASSERT_EQ(parent_id
, e
->Get(syncable::PARENT_ID
));
70 ASSERT_LT(0, e
->Get(BASE_VERSION
))
71 << "Item should have a valid (positive) server base revision";
74 // Create an unsynced item in the database. If item_id is a local ID, it
75 // will be treated as a create-new. Otherwise, if it's a server ID, we'll
76 // fake the server data so that it looks like it exists on the server.
77 // Returns the methandle of the created item in |metahandle_out| if not NULL.
78 void CreateUnsyncedItem(const Id
& item_id
,
82 syncer::ModelType model_type
,
83 int64
* metahandle_out
) {
84 WriteTransaction
trans(FROM_HERE
, UNITTEST
, directory());
87 directory()->GetLastChildIdForTest(&trans
, parent_id
, &predecessor_id
));
88 MutableEntry
entry(&trans
, syncable::CREATE
, parent_id
, name
);
89 ASSERT_TRUE(entry
.good());
90 entry
.Put(syncable::ID
, item_id
);
91 entry
.Put(syncable::BASE_VERSION
,
92 item_id
.ServerKnows() ? next_old_revision_
++ : 0);
93 entry
.Put(syncable::IS_UNSYNCED
, true);
94 entry
.Put(syncable::IS_DIR
, is_folder
);
95 entry
.Put(syncable::IS_DEL
, false);
96 entry
.Put(syncable::PARENT_ID
, parent_id
);
97 entry
.PutPredecessor(predecessor_id
);
98 sync_pb::EntitySpecifics default_specifics
;
99 syncer::AddDefaultFieldValue(model_type
, &default_specifics
);
100 entry
.Put(syncable::SPECIFICS
, default_specifics
);
101 if (item_id
.ServerKnows()) {
102 entry
.Put(syncable::SERVER_SPECIFICS
, default_specifics
);
103 entry
.Put(syncable::SERVER_IS_DIR
, is_folder
);
104 entry
.Put(syncable::SERVER_PARENT_ID
, parent_id
);
105 entry
.Put(syncable::SERVER_IS_DEL
, false);
108 *metahandle_out
= entry
.Get(syncable::META_HANDLE
);
111 // Create a new unsynced item in the database, and synthesize a commit
112 // record and a commit response for it in the syncer session. If item_id
113 // is a local ID, the item will be a create operation. Otherwise, it
115 void CreateUnprocessedCommitResult(
119 syncer::ModelType model_type
,
120 sessions::OrderedCommitSet
*commit_set
,
121 syncer::ClientToServerMessage
*commit
,
122 syncer::ClientToServerResponse
*response
) {
123 bool is_folder
= true;
124 int64 metahandle
= 0;
125 CreateUnsyncedItem(item_id
, parent_id
, name
, is_folder
, model_type
,
128 // ProcessCommitResponseCommand consumes commit_ids from the session
129 // state, so we need to update that. O(n^2) because it's a test.
130 commit_set
->AddCommitItem(metahandle
, item_id
, model_type
);
132 WriteTransaction
trans(FROM_HERE
, UNITTEST
, directory());
133 MutableEntry
entry(&trans
, syncable::GET_BY_ID
, item_id
);
134 ASSERT_TRUE(entry
.good());
135 entry
.Put(syncable::SYNCING
, true);
137 // Add to the commit message.
138 commit
->set_message_contents(ClientToServerMessage::COMMIT
);
139 SyncEntity
* entity
= static_cast<SyncEntity
*>(
140 commit
->mutable_commit()->add_entries());
141 entity
->set_non_unique_name(name
);
142 entity
->set_folder(is_folder
);
143 entity
->set_parent_id(parent_id
);
144 entity
->set_version(entry
.Get(syncable::BASE_VERSION
));
145 entity
->mutable_specifics()->CopyFrom(entry
.Get(syncable::SPECIFICS
));
146 entity
->set_id(item_id
);
148 // Add to the response message.
149 response
->set_error_code(sync_pb::SyncEnums::SUCCESS
);
150 sync_pb::CommitResponse_EntryResponse
* entry_response
=
151 response
->mutable_commit()->add_entryresponse();
152 entry_response
->set_response_type(CommitResponse::SUCCESS
);
153 entry_response
->set_name("Garbage.");
154 entry_response
->set_non_unique_name(entity
->name());
155 if (item_id
.ServerKnows())
156 entry_response
->set_id_string(entity
->id_string());
158 entry_response
->set_id_string(id_factory_
.NewServerId().GetServerId());
159 entry_response
->set_version(next_new_revision_
++);
160 entry_response
->set_position_in_parent(next_server_position_
++);
162 // If the ID of our parent item committed earlier in the batch was
163 // rewritten, rewrite it in the entry response. This matches
164 // the server behavior.
165 entry_response
->set_parent_id_string(entity
->parent_id_string());
166 for (int i
= 0; i
< commit
->commit().entries_size(); ++i
) {
167 if (commit
->commit().entries(i
).id_string() ==
168 entity
->parent_id_string()) {
169 entry_response
->set_parent_id_string(
170 response
->commit().entryresponse(i
).id_string());
175 void SetLastErrorCode(CommitResponse::ResponseType error_code
,
176 sync_pb::ClientToServerResponse
* response
) {
177 sync_pb::CommitResponse_EntryResponse
* entry_response
=
178 response
->mutable_commit()->mutable_entryresponse(
179 response
->mutable_commit()->entryresponse_size() - 1);
180 entry_response
->set_response_type(error_code
);
183 TestIdFactory id_factory_
;
185 int64 next_old_revision_
;
186 int64 next_new_revision_
;
187 int64 next_server_position_
;
188 DISALLOW_COPY_AND_ASSIGN(ProcessCommitResponseCommandTest
);
191 TEST_F(ProcessCommitResponseCommandTest
, MultipleCommitIdProjections
) {
192 sessions::OrderedCommitSet
commit_set(session()->routing_info());
193 syncer::ClientToServerMessage request
;
194 syncer::ClientToServerResponse response
;
196 Id bookmark_folder_id
= id_factory_
.NewLocalId();
197 Id bookmark_id1
= id_factory_
.NewLocalId();
198 Id bookmark_id2
= id_factory_
.NewLocalId();
199 Id pref_id1
= id_factory_
.NewLocalId(), pref_id2
= id_factory_
.NewLocalId();
200 Id autofill_id1
= id_factory_
.NewLocalId();
201 Id autofill_id2
= id_factory_
.NewLocalId();
202 CreateUnprocessedCommitResult(bookmark_folder_id
, id_factory_
.root(),
203 "A bookmark folder", syncer::BOOKMARKS
,
204 &commit_set
, &request
, &response
);
205 CreateUnprocessedCommitResult(bookmark_id1
, bookmark_folder_id
,
206 "bookmark 1", syncer::BOOKMARKS
,
207 &commit_set
, &request
, &response
);
208 CreateUnprocessedCommitResult(bookmark_id2
, bookmark_folder_id
,
209 "bookmark 2", syncer::BOOKMARKS
,
210 &commit_set
, &request
, &response
);
211 CreateUnprocessedCommitResult(pref_id1
, id_factory_
.root(),
212 "Pref 1", syncer::PREFERENCES
,
213 &commit_set
, &request
, &response
);
214 CreateUnprocessedCommitResult(pref_id2
, id_factory_
.root(),
215 "Pref 2", syncer::PREFERENCES
,
216 &commit_set
, &request
, &response
);
217 CreateUnprocessedCommitResult(autofill_id1
, id_factory_
.root(),
218 "Autofill 1", syncer::AUTOFILL
,
219 &commit_set
, &request
, &response
);
220 CreateUnprocessedCommitResult(autofill_id2
, id_factory_
.root(),
221 "Autofill 2", syncer::AUTOFILL
,
222 &commit_set
, &request
, &response
);
224 ProcessCommitResponseCommand
command(commit_set
, request
, response
);
225 ExpectGroupsToChange(command
, GROUP_UI
, GROUP_DB
);
226 command
.ExecuteImpl(session());
228 syncable::ReadTransaction
trans(FROM_HERE
, directory());
230 ASSERT_TRUE(directory()->GetFirstChildId(
231 &trans
, id_factory_
.root(), &new_fid
));
232 ASSERT_FALSE(new_fid
.IsRoot());
233 EXPECT_TRUE(new_fid
.ServerKnows());
234 EXPECT_FALSE(bookmark_folder_id
.ServerKnows());
235 EXPECT_FALSE(new_fid
== bookmark_folder_id
);
236 Entry
b_folder(&trans
, syncable::GET_BY_ID
, new_fid
);
237 ASSERT_TRUE(b_folder
.good());
238 ASSERT_EQ("A bookmark folder", b_folder
.Get(NON_UNIQUE_NAME
))
239 << "Name of bookmark folder should not change.";
240 ASSERT_LT(0, b_folder
.Get(BASE_VERSION
))
241 << "Bookmark folder should have a valid (positive) server base revision";
243 // Look at the two bookmarks in bookmark_folder.
245 ASSERT_TRUE(directory()->GetFirstChildId(&trans
, new_fid
, &cid
));
246 Entry
b1(&trans
, syncable::GET_BY_ID
, cid
);
247 Entry
b2(&trans
, syncable::GET_BY_ID
, b1
.Get(syncable::NEXT_ID
));
248 CheckEntry(&b1
, "bookmark 1", syncer::BOOKMARKS
, new_fid
);
249 CheckEntry(&b2
, "bookmark 2", syncer::BOOKMARKS
, new_fid
);
250 ASSERT_TRUE(b2
.Get(syncable::NEXT_ID
).IsRoot());
252 // Look at the prefs and autofill items.
253 Entry
p1(&trans
, syncable::GET_BY_ID
, b_folder
.Get(syncable::NEXT_ID
));
254 Entry
p2(&trans
, syncable::GET_BY_ID
, p1
.Get(syncable::NEXT_ID
));
255 CheckEntry(&p1
, "Pref 1", syncer::PREFERENCES
, id_factory_
.root());
256 CheckEntry(&p2
, "Pref 2", syncer::PREFERENCES
, id_factory_
.root());
258 Entry
a1(&trans
, syncable::GET_BY_ID
, p2
.Get(syncable::NEXT_ID
));
259 Entry
a2(&trans
, syncable::GET_BY_ID
, a1
.Get(syncable::NEXT_ID
));
260 CheckEntry(&a1
, "Autofill 1", syncer::AUTOFILL
, id_factory_
.root());
261 CheckEntry(&a2
, "Autofill 2", syncer::AUTOFILL
, id_factory_
.root());
262 ASSERT_TRUE(a2
.Get(syncable::NEXT_ID
).IsRoot());
265 // In this test, we test processing a commit response for a commit batch that
266 // includes a newly created folder and some (but not all) of its children.
267 // In particular, the folder has 50 children, which alternate between being
268 // new items and preexisting items. This mixture of new and old is meant to
269 // be a torture test of the code in ProcessCommitResponseCommand that changes
270 // an item's ID from a local ID to a server-generated ID on the first commit.
271 // We commit only the first 25 children in the sibling order, leaving the
272 // second 25 children as unsynced items. http://crbug.com/33081 describes
273 // how this scenario used to fail, reversing the order for the second half
275 TEST_F(ProcessCommitResponseCommandTest
, NewFolderCommitKeepsChildOrder
) {
276 sessions::OrderedCommitSet
commit_set(session()->routing_info());
277 syncer::ClientToServerMessage request
;
278 syncer::ClientToServerResponse response
;
280 // Create the parent folder, a new item whose ID will change on commit.
281 Id folder_id
= id_factory_
.NewLocalId();
282 CreateUnprocessedCommitResult(folder_id
, id_factory_
.root(), "A",
284 &commit_set
, &request
, &response
);
286 // Verify that the item is reachable.
288 syncable::ReadTransaction
trans(FROM_HERE
, directory());
290 ASSERT_TRUE(directory()->GetFirstChildId(
291 &trans
, id_factory_
.root(), &child_id
));
292 ASSERT_EQ(folder_id
, child_id
);
295 // The first 25 children of the parent folder will be part of the commit
299 for (; i
< batch_size
; ++i
) {
300 // Alternate between new and old child items, just for kicks.
301 Id id
= (i
% 4 < 2) ? id_factory_
.NewLocalId() : id_factory_
.NewServerId();
302 CreateUnprocessedCommitResult(
303 id
, folder_id
, base::StringPrintf("Item %d", i
), syncer::BOOKMARKS
,
304 &commit_set
, &request
, &response
);
306 // The second 25 children will be unsynced items but NOT part of the commit
307 // batch. When the ID of the parent folder changes during the commit,
308 // these items PARENT_ID should be updated, and their ordering should be
310 for (; i
< 2*batch_size
; ++i
) {
311 // Alternate between new and old child items, just for kicks.
312 Id id
= (i
% 4 < 2) ? id_factory_
.NewLocalId() : id_factory_
.NewServerId();
313 CreateUnsyncedItem(id
, folder_id
, base::StringPrintf("Item %d", i
),
314 false, syncer::BOOKMARKS
, NULL
);
317 // Process the commit response for the parent folder and the first
318 // 25 items. This should apply the values indicated by
319 // each CommitResponse_EntryResponse to the syncable Entries. All new
320 // items in the commit batch should have their IDs changed to server IDs.
321 ProcessCommitResponseCommand
command(commit_set
, request
, response
);
322 ExpectGroupToChange(command
, GROUP_UI
);
323 command
.ExecuteImpl(session());
325 syncable::ReadTransaction
trans(FROM_HERE
, directory());
326 // Lookup the parent folder by finding a child of the root. We can't use
327 // folder_id here, because it changed during the commit.
329 ASSERT_TRUE(directory()->GetFirstChildId(
330 &trans
, id_factory_
.root(), &new_fid
));
331 ASSERT_FALSE(new_fid
.IsRoot());
332 EXPECT_TRUE(new_fid
.ServerKnows());
333 EXPECT_FALSE(folder_id
.ServerKnows());
334 EXPECT_TRUE(new_fid
!= folder_id
);
335 Entry
parent(&trans
, syncable::GET_BY_ID
, new_fid
);
336 ASSERT_TRUE(parent
.good());
337 ASSERT_EQ("A", parent
.Get(NON_UNIQUE_NAME
))
338 << "Name of parent folder should not change.";
339 ASSERT_LT(0, parent
.Get(BASE_VERSION
))
340 << "Parent should have a valid (positive) server base revision";
343 ASSERT_TRUE(directory()->GetFirstChildId(&trans
, new_fid
, &cid
));
345 // Now loop over all the children of the parent folder, verifying
346 // that they are in their original order by checking to see that their
347 // names are still sequential.
348 while (!cid
.IsRoot()) {
349 SCOPED_TRACE(::testing::Message("Examining item #") << child_count
);
350 Entry
c(&trans
, syncable::GET_BY_ID
, cid
);
352 ASSERT_EQ(base::StringPrintf("Item %d", child_count
),
353 c
.Get(NON_UNIQUE_NAME
));
354 ASSERT_EQ(new_fid
, c
.Get(syncable::PARENT_ID
));
355 if (child_count
< batch_size
) {
356 ASSERT_FALSE(c
.Get(IS_UNSYNCED
)) << "Item should be committed";
357 ASSERT_TRUE(cid
.ServerKnows());
358 ASSERT_LT(0, c
.Get(BASE_VERSION
));
360 ASSERT_TRUE(c
.Get(IS_UNSYNCED
)) << "Item should be uncommitted";
361 // We alternated between creates and edits; double check that these items
362 // have been preserved.
363 if (child_count
% 4 < 2) {
364 ASSERT_FALSE(cid
.ServerKnows());
365 ASSERT_GE(0, c
.Get(BASE_VERSION
));
367 ASSERT_TRUE(cid
.ServerKnows());
368 ASSERT_LT(0, c
.Get(BASE_VERSION
));
371 cid
= c
.Get(syncable::NEXT_ID
);
374 ASSERT_EQ(batch_size
*2, child_count
)
375 << "Too few or too many children in parent folder after commit.";
378 // This test fixture runs across a Cartesian product of per-type fail/success
381 TEST_PARAM_BOOKMARK_ENABLE_BIT
,
382 TEST_PARAM_AUTOFILL_ENABLE_BIT
,
386 public ProcessCommitResponseCommandTest
,
387 public ::testing::WithParamInterface
<int> {
389 bool ShouldFailBookmarkCommit() {
390 return (GetParam() & (1 << TEST_PARAM_BOOKMARK_ENABLE_BIT
)) == 0;
392 bool ShouldFailAutofillCommit() {
393 return (GetParam() & (1 << TEST_PARAM_AUTOFILL_ENABLE_BIT
)) == 0;
396 INSTANTIATE_TEST_CASE_P(ProcessCommitResponse
,
398 testing::Range(0, 1 << TEST_PARAM_BIT_COUNT
));
400 // This test commits 2 items (one bookmark, one autofill) and validates what
401 // happens to the extensions activity records. Commits could fail or succeed,
402 // depending on the test parameter.
403 TEST_P(MixedResult
, ExtensionActivity
) {
404 sessions::OrderedCommitSet
commit_set(session()->routing_info());
405 syncer::ClientToServerMessage request
;
406 syncer::ClientToServerResponse response
;
408 EXPECT_NE(routing_info().find(syncer::BOOKMARKS
)->second
,
409 routing_info().find(syncer::AUTOFILL
)->second
)
410 << "To not be lame, this test requires more than one active group.";
412 // Bookmark item setup.
413 CreateUnprocessedCommitResult(id_factory_
.NewServerId(),
414 id_factory_
.root(), "Some bookmark", syncer::BOOKMARKS
,
415 &commit_set
, &request
, &response
);
416 if (ShouldFailBookmarkCommit())
417 SetLastErrorCode(CommitResponse::TRANSIENT_ERROR
, &response
);
418 // Autofill item setup.
419 CreateUnprocessedCommitResult(id_factory_
.NewServerId(),
420 id_factory_
.root(), "Some autofill", syncer::AUTOFILL
,
421 &commit_set
, &request
, &response
);
422 if (ShouldFailAutofillCommit())
423 SetLastErrorCode(CommitResponse::TRANSIENT_ERROR
, &response
);
425 // Put some extensions activity in the session.
427 ExtensionsActivityMonitor::Records
* records
=
428 session()->mutable_extensions_activity();
429 (*records
)["ABC"].extension_id
= "ABC";
430 (*records
)["ABC"].bookmark_write_count
= 2049U;
431 (*records
)["xyz"].extension_id
= "xyz";
432 (*records
)["xyz"].bookmark_write_count
= 4U;
434 ProcessCommitResponseCommand
command(commit_set
, request
, response
);
435 command
.ExecuteImpl(session());
436 ExpectGroupsToChange(command
, GROUP_UI
, GROUP_DB
);
438 ExtensionsActivityMonitor::Records final_monitor_records
;
439 context()->extensions_monitor()->GetAndClearRecords(&final_monitor_records
);
441 if (ShouldFailBookmarkCommit()) {
442 ASSERT_EQ(2U, final_monitor_records
.size())
443 << "Should restore records after unsuccessful bookmark commit.";
444 EXPECT_EQ("ABC", final_monitor_records
["ABC"].extension_id
);
445 EXPECT_EQ("xyz", final_monitor_records
["xyz"].extension_id
);
446 EXPECT_EQ(2049U, final_monitor_records
["ABC"].bookmark_write_count
);
447 EXPECT_EQ(4U, final_monitor_records
["xyz"].bookmark_write_count
);
449 EXPECT_TRUE(final_monitor_records
.empty())
450 << "Should not restore records after successful bookmark commit.";
454 } // namespace syncer