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