1 // Copyright 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/engine/process_commit_response_command.h" 6 7 #include <vector> 8 9 #include "base/location.h" 10 #include "base/strings/stringprintf.h" 11 #include "sync/internal_api/public/test/test_entry_factory.h" 12 #include "sync/protocol/bookmark_specifics.pb.h" 13 #include "sync/protocol/sync.pb.h" 14 #include "sync/sessions/sync_session.h" 15 #include "sync/syncable/entry.h" 16 #include "sync/syncable/mutable_entry.h" 17 #include "sync/syncable/syncable_id.h" 18 #include "sync/syncable/syncable_proto_util.h" 19 #include "sync/syncable/syncable_read_transaction.h" 20 #include "sync/syncable/syncable_write_transaction.h" 21 #include "sync/test/engine/fake_model_worker.h" 22 #include "sync/test/engine/syncer_command_test.h" 23 #include "sync/test/engine/test_id_factory.h" 24 #include "testing/gtest/include/gtest/gtest.h" 25 26 using std::string; 27 using sync_pb::ClientToServerMessage; 28 using sync_pb::CommitResponse; 29 30 namespace syncer { 31 32 using sessions::SyncSession; 33 using syncable::BASE_VERSION; 34 using syncable::Entry; 35 using syncable::ID; 36 using syncable::IS_DIR; 37 using syncable::IS_UNSYNCED; 38 using syncable::Id; 39 using syncable::MutableEntry; 40 using syncable::NON_UNIQUE_NAME; 41 using syncable::UNIQUE_POSITION; 42 using syncable::UNITTEST; 43 using syncable::WriteTransaction; 44 45 // A test fixture for tests exercising ProcessCommitResponseCommand. 46 class ProcessCommitResponseCommandTest : public SyncerCommandTest { 47 public: 48 virtual void SetUp() { 49 workers()->clear(); 50 mutable_routing_info()->clear(); 51 52 workers()->push_back( 53 make_scoped_refptr(new FakeModelWorker(GROUP_DB))); 54 workers()->push_back( 55 make_scoped_refptr(new FakeModelWorker(GROUP_UI))); 56 (*mutable_routing_info())[BOOKMARKS] = GROUP_UI; 57 (*mutable_routing_info())[PREFERENCES] = GROUP_UI; 58 (*mutable_routing_info())[AUTOFILL] = GROUP_DB; 59 60 SyncerCommandTest::SetUp(); 61 62 test_entry_factory_.reset(new TestEntryFactory(directory())); 63 } 64 65 protected: 66 67 ProcessCommitResponseCommandTest() 68 : next_new_revision_(4000), 69 next_server_position_(10000) { 70 } 71 72 void CheckEntry(Entry* e, const std::string& name, 73 ModelType model_type, const Id& parent_id) { 74 EXPECT_TRUE(e->good()); 75 ASSERT_EQ(name, e->Get(NON_UNIQUE_NAME)); 76 ASSERT_EQ(model_type, e->GetModelType()); 77 ASSERT_EQ(parent_id, e->Get(syncable::PARENT_ID)); 78 ASSERT_LT(0, e->Get(BASE_VERSION)) 79 << "Item should have a valid (positive) server base revision"; 80 } 81 82 // Create a new unsynced item in the database, and synthesize a commit record 83 // and a commit response for it in the syncer session. If item_id is a local 84 // ID, the item will be a create operation. Otherwise, it will be an edit. 85 // Returns the metahandle of the newly created item. 86 int CreateUnprocessedCommitResult( 87 const Id& item_id, 88 const Id& parent_id, 89 const string& name, 90 bool is_folder, 91 ModelType model_type, 92 sessions::OrderedCommitSet *commit_set, 93 sync_pb::ClientToServerMessage *commit, 94 sync_pb::ClientToServerResponse *response) { 95 int64 metahandle = 0; 96 test_entry_factory_->CreateUnsyncedItem(item_id, parent_id, name, 97 is_folder, model_type, &metahandle); 98 99 // ProcessCommitResponseCommand consumes commit_ids from the session 100 // state, so we need to update that. O(n^2) because it's a test. 101 commit_set->AddCommitItem(metahandle, item_id, model_type); 102 103 WriteTransaction trans(FROM_HERE, UNITTEST, directory()); 104 MutableEntry entry(&trans, syncable::GET_BY_ID, item_id); 105 EXPECT_TRUE(entry.good()); 106 entry.Put(syncable::SYNCING, true); 107 108 // Add to the commit message. 109 // TODO(sync): Use the real commit-building code to construct this. 110 commit->set_message_contents(ClientToServerMessage::COMMIT); 111 sync_pb::SyncEntity* entity = commit->mutable_commit()->add_entries(); 112 entity->set_non_unique_name(entry.Get(syncable::NON_UNIQUE_NAME)); 113 entity->set_folder(entry.Get(syncable::IS_DIR)); 114 entity->set_parent_id_string( 115 SyncableIdToProto(entry.Get(syncable::PARENT_ID))); 116 entity->set_version(entry.Get(syncable::BASE_VERSION)); 117 entity->mutable_specifics()->CopyFrom(entry.Get(syncable::SPECIFICS)); 118 entity->set_id_string(SyncableIdToProto(item_id)); 119 120 if (!entry.Get(syncable::UNIQUE_CLIENT_TAG).empty()) { 121 entity->set_client_defined_unique_tag( 122 entry.Get(syncable::UNIQUE_CLIENT_TAG)); 123 } 124 125 // Add to the response message. 126 response->set_error_code(sync_pb::SyncEnums::SUCCESS); 127 sync_pb::CommitResponse_EntryResponse* entry_response = 128 response->mutable_commit()->add_entryresponse(); 129 entry_response->set_response_type(CommitResponse::SUCCESS); 130 entry_response->set_name("Garbage."); 131 entry_response->set_non_unique_name(entity->name()); 132 if (item_id.ServerKnows()) 133 entry_response->set_id_string(entity->id_string()); 134 else 135 entry_response->set_id_string(id_factory_.NewServerId().GetServerId()); 136 entry_response->set_version(next_new_revision_++); 137 138 // If the ID of our parent item committed earlier in the batch was 139 // rewritten, rewrite it in the entry response. This matches 140 // the server behavior. 141 entry_response->set_parent_id_string(entity->parent_id_string()); 142 for (int i = 0; i < commit->commit().entries_size(); ++i) { 143 if (commit->commit().entries(i).id_string() == 144 entity->parent_id_string()) { 145 entry_response->set_parent_id_string( 146 response->commit().entryresponse(i).id_string()); 147 } 148 } 149 150 return metahandle; 151 } 152 153 void SetLastErrorCode(sync_pb::CommitResponse::ResponseType error_code, 154 sync_pb::ClientToServerResponse* response) { 155 sync_pb::CommitResponse_EntryResponse* entry_response = 156 response->mutable_commit()->mutable_entryresponse( 157 response->mutable_commit()->entryresponse_size() - 1); 158 entry_response->set_response_type(error_code); 159 } 160 161 TestIdFactory id_factory_; 162 scoped_ptr<TestEntryFactory> test_entry_factory_; 163 private: 164 int64 next_new_revision_; 165 int64 next_server_position_; 166 DISALLOW_COPY_AND_ASSIGN(ProcessCommitResponseCommandTest); 167 }; 168 169 TEST_F(ProcessCommitResponseCommandTest, MultipleCommitIdProjections) { 170 sessions::OrderedCommitSet commit_set(session()->context()->routing_info()); 171 sync_pb::ClientToServerMessage request; 172 sync_pb::ClientToServerResponse response; 173 174 Id bookmark_folder_id = id_factory_.NewLocalId(); 175 int bookmark_folder_handle = CreateUnprocessedCommitResult( 176 bookmark_folder_id, id_factory_.root(), "A bookmark folder", true, 177 BOOKMARKS, &commit_set, &request, &response); 178 int bookmark1_handle = CreateUnprocessedCommitResult( 179 id_factory_.NewLocalId(), bookmark_folder_id, "bookmark 1", false, 180 BOOKMARKS, &commit_set, &request, &response); 181 int bookmark2_handle = CreateUnprocessedCommitResult( 182 id_factory_.NewLocalId(), bookmark_folder_id, "bookmark 2", false, 183 BOOKMARKS, &commit_set, &request, &response); 184 int pref1_handle = CreateUnprocessedCommitResult( 185 id_factory_.NewLocalId(), id_factory_.root(), "Pref 1", false, 186 PREFERENCES, &commit_set, &request, &response); 187 int pref2_handle = CreateUnprocessedCommitResult( 188 id_factory_.NewLocalId(), id_factory_.root(), "Pref 2", false, 189 PREFERENCES, &commit_set, &request, &response); 190 int autofill1_handle = CreateUnprocessedCommitResult( 191 id_factory_.NewLocalId(), id_factory_.root(), "Autofill 1", false, 192 AUTOFILL, &commit_set, &request, &response); 193 int autofill2_handle = CreateUnprocessedCommitResult( 194 id_factory_.NewLocalId(), id_factory_.root(), "Autofill 2", false, 195 AUTOFILL, &commit_set, &request, &response); 196 197 ProcessCommitResponseCommand command(commit_set, request, response); 198 ExpectGroupsToChange(command, GROUP_UI, GROUP_DB); 199 command.ExecuteImpl(session()); 200 201 syncable::ReadTransaction trans(FROM_HERE, directory()); 202 203 Entry b_folder(&trans, syncable::GET_BY_HANDLE, bookmark_folder_handle); 204 ASSERT_TRUE(b_folder.good()); 205 206 Id new_fid = b_folder.Get(syncable::ID); 207 ASSERT_FALSE(new_fid.IsRoot()); 208 EXPECT_TRUE(new_fid.ServerKnows()); 209 EXPECT_FALSE(bookmark_folder_id.ServerKnows()); 210 EXPECT_FALSE(new_fid == bookmark_folder_id); 211 212 ASSERT_EQ("A bookmark folder", b_folder.Get(NON_UNIQUE_NAME)) 213 << "Name of bookmark folder should not change."; 214 ASSERT_LT(0, b_folder.Get(BASE_VERSION)) 215 << "Bookmark folder should have a valid (positive) server base revision"; 216 217 // Look at the two bookmarks in bookmark_folder. 218 Entry b1(&trans, syncable::GET_BY_HANDLE, bookmark1_handle); 219 Entry b2(&trans, syncable::GET_BY_HANDLE, bookmark2_handle); 220 CheckEntry(&b1, "bookmark 1", BOOKMARKS, new_fid); 221 CheckEntry(&b2, "bookmark 2", BOOKMARKS, new_fid); 222 223 // Look at the prefs and autofill items. 224 Entry p1(&trans, syncable::GET_BY_HANDLE, pref1_handle); 225 Entry p2(&trans, syncable::GET_BY_HANDLE, pref2_handle); 226 CheckEntry(&p1, "Pref 1", PREFERENCES, id_factory_.root()); 227 CheckEntry(&p2, "Pref 2", PREFERENCES, id_factory_.root()); 228 229 Entry a1(&trans, syncable::GET_BY_HANDLE, autofill1_handle); 230 Entry a2(&trans, syncable::GET_BY_HANDLE, autofill2_handle); 231 CheckEntry(&a1, "Autofill 1", AUTOFILL, id_factory_.root()); 232 CheckEntry(&a2, "Autofill 2", AUTOFILL, id_factory_.root()); 233 } 234 235 // In this test, we test processing a commit response for a commit batch that 236 // includes a newly created folder and some (but not all) of its children. 237 // In particular, the folder has 50 children, which alternate between being 238 // new items and preexisting items. This mixture of new and old is meant to 239 // be a torture test of the code in ProcessCommitResponseCommand that changes 240 // an item's ID from a local ID to a server-generated ID on the first commit. 241 // We commit only the first 25 children in the sibling order, leaving the 242 // second 25 children as unsynced items. http://crbug.com/33081 describes 243 // how this scenario used to fail, reversing the order for the second half 244 // of the children. 245 TEST_F(ProcessCommitResponseCommandTest, NewFolderCommitKeepsChildOrder) { 246 sessions::OrderedCommitSet commit_set(session()->context()->routing_info()); 247 sync_pb::ClientToServerMessage request; 248 sync_pb::ClientToServerResponse response; 249 250 // Create the parent folder, a new item whose ID will change on commit. 251 Id folder_id = id_factory_.NewLocalId(); 252 CreateUnprocessedCommitResult(folder_id, id_factory_.root(), 253 "A", true, BOOKMARKS, 254 &commit_set, &request, &response); 255 256 // Verify that the item is reachable. 257 { 258 syncable::ReadTransaction trans(FROM_HERE, directory()); 259 syncable::Entry root(&trans, syncable::GET_BY_ID, id_factory_.root()); 260 ASSERT_TRUE(root.good()); 261 Id child_id = root.GetFirstChildId(); 262 ASSERT_EQ(folder_id, child_id); 263 } 264 265 // The first 25 children of the parent folder will be part of the commit 266 // batch. They will be placed left to right in order of creation. 267 int batch_size = 25; 268 int i = 0; 269 Id prev_id = TestIdFactory::root(); 270 for (; i < batch_size; ++i) { 271 // Alternate between new and old child items, just for kicks. 272 Id id = (i % 4 < 2) ? id_factory_.NewLocalId() : id_factory_.NewServerId(); 273 int64 handle = CreateUnprocessedCommitResult( 274 id, folder_id, base::StringPrintf("Item %d", i), false, 275 BOOKMARKS, &commit_set, &request, &response); 276 { 277 syncable::WriteTransaction trans(FROM_HERE, UNITTEST, directory()); 278 syncable::MutableEntry e(&trans, syncable::GET_BY_HANDLE, handle); 279 ASSERT_TRUE(e.good()); 280 e.PutPredecessor(prev_id); 281 } 282 prev_id = id; 283 } 284 // The second 25 children will be unsynced items but NOT part of the commit 285 // batch. When the ID of the parent folder changes during the commit, 286 // these items PARENT_ID should be updated, and their ordering should be 287 // preserved. 288 for (; i < 2*batch_size; ++i) { 289 // Alternate between new and old child items, just for kicks. 290 Id id = (i % 4 < 2) ? id_factory_.NewLocalId() : id_factory_.NewServerId(); 291 int64 handle = -1; 292 test_entry_factory_->CreateUnsyncedItem( 293 id, folder_id, base::StringPrintf("Item %d", i), 294 false, BOOKMARKS, &handle); 295 { 296 syncable::WriteTransaction trans(FROM_HERE, UNITTEST, directory()); 297 syncable::MutableEntry e(&trans, syncable::GET_BY_HANDLE, handle); 298 ASSERT_TRUE(e.good()); 299 e.PutPredecessor(prev_id); 300 } 301 prev_id = id; 302 } 303 304 // Process the commit response for the parent folder and the first 305 // 25 items. This should apply the values indicated by 306 // each CommitResponse_EntryResponse to the syncable Entries. All new 307 // items in the commit batch should have their IDs changed to server IDs. 308 ProcessCommitResponseCommand command(commit_set, request, response); 309 ExpectGroupToChange(command, GROUP_UI); 310 command.ExecuteImpl(session()); 311 312 syncable::ReadTransaction trans(FROM_HERE, directory()); 313 // Lookup the parent folder by finding a child of the root. We can't use 314 // folder_id here, because it changed during the commit. 315 syncable::Entry root(&trans, syncable::GET_BY_ID, id_factory_.root()); 316 ASSERT_TRUE(root.good()); 317 Id new_fid = root.GetFirstChildId(); 318 ASSERT_FALSE(new_fid.IsRoot()); 319 EXPECT_TRUE(new_fid.ServerKnows()); 320 EXPECT_FALSE(folder_id.ServerKnows()); 321 EXPECT_TRUE(new_fid != folder_id); 322 Entry parent(&trans, syncable::GET_BY_ID, new_fid); 323 ASSERT_TRUE(parent.good()); 324 ASSERT_EQ("A", parent.Get(NON_UNIQUE_NAME)) 325 << "Name of parent folder should not change."; 326 ASSERT_LT(0, parent.Get(BASE_VERSION)) 327 << "Parent should have a valid (positive) server base revision"; 328 329 Id cid = parent.GetFirstChildId(); 330 331 int child_count = 0; 332 // Now loop over all the children of the parent folder, verifying 333 // that they are in their original order by checking to see that their 334 // names are still sequential. 335 while (!cid.IsRoot()) { 336 SCOPED_TRACE(::testing::Message("Examining item #") << child_count); 337 Entry c(&trans, syncable::GET_BY_ID, cid); 338 DCHECK(c.good()); 339 ASSERT_EQ(base::StringPrintf("Item %d", child_count), 340 c.Get(NON_UNIQUE_NAME)); 341 ASSERT_EQ(new_fid, c.Get(syncable::PARENT_ID)); 342 if (child_count < batch_size) { 343 ASSERT_FALSE(c.Get(IS_UNSYNCED)) << "Item should be committed"; 344 ASSERT_TRUE(cid.ServerKnows()); 345 ASSERT_LT(0, c.Get(BASE_VERSION)); 346 } else { 347 ASSERT_TRUE(c.Get(IS_UNSYNCED)) << "Item should be uncommitted"; 348 // We alternated between creates and edits; double check that these items 349 // have been preserved. 350 if (child_count % 4 < 2) { 351 ASSERT_FALSE(cid.ServerKnows()); 352 ASSERT_GE(0, c.Get(BASE_VERSION)); 353 } else { 354 ASSERT_TRUE(cid.ServerKnows()); 355 ASSERT_LT(0, c.Get(BASE_VERSION)); 356 } 357 } 358 cid = c.GetSuccessorId(); 359 child_count++; 360 } 361 ASSERT_EQ(batch_size*2, child_count) 362 << "Too few or too many children in parent folder after commit."; 363 } 364 365 } // namespace syncer 366