Home | History | Annotate | Download | only in engine
      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