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