• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1 // Copyright 2014 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/directory_commit_contribution.h"
6 
7 #include "base/message_loop/message_loop.h"
8 #include "sync/internal_api/public/base/attachment_id_proto.h"
9 #include "sync/sessions/status_controller.h"
10 #include "sync/syncable/entry.h"
11 #include "sync/syncable/mutable_entry.h"
12 #include "sync/syncable/syncable_read_transaction.h"
13 #include "sync/syncable/syncable_write_transaction.h"
14 #include "sync/test/engine/test_directory_setter_upper.h"
15 #include "sync/test/engine/test_id_factory.h"
16 #include "sync/test/engine/test_syncable_utils.h"
17 #include "testing/gtest/include/gtest/gtest.h"
18 
19 namespace syncer {
20 
21 class DirectoryCommitContributionTest : public ::testing::Test {
22  public:
SetUp()23   virtual void SetUp() OVERRIDE {
24     dir_maker_.SetUp();
25 
26     syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
27     CreateTypeRoot(&trans, dir(), PREFERENCES);
28     CreateTypeRoot(&trans, dir(), EXTENSIONS);
29     CreateTypeRoot(&trans, dir(), ARTICLES);
30     CreateTypeRoot(&trans, dir(), BOOKMARKS);
31   }
32 
TearDown()33   virtual void TearDown() OVERRIDE {
34     dir_maker_.TearDown();
35   }
36 
37  protected:
CreateUnsyncedItemWithAttachments(syncable::WriteTransaction * trans,ModelType type,const std::string & tag,const sync_pb::AttachmentMetadata & attachment_metadata)38   int64 CreateUnsyncedItemWithAttachments(
39       syncable::WriteTransaction* trans,
40       ModelType type,
41       const std::string& tag,
42       const sync_pb::AttachmentMetadata& attachment_metadata) {
43     syncable::Entry parent_entry(trans, syncable::GET_TYPE_ROOT, type);
44     syncable::MutableEntry entry(
45         trans,
46         syncable::CREATE,
47         type,
48         parent_entry.GetId(),
49         tag);
50     if (attachment_metadata.record_size() > 0) {
51       entry.PutAttachmentMetadata(attachment_metadata);
52     }
53     entry.PutIsUnsynced(true);
54     return entry.GetMetahandle();
55   }
56 
CreateUnsyncedItem(syncable::WriteTransaction * trans,ModelType type,const std::string & tag)57   int64 CreateUnsyncedItem(syncable::WriteTransaction* trans,
58                            ModelType type,
59                            const std::string& tag) {
60     return CreateUnsyncedItemWithAttachments(
61         trans, type, tag, sync_pb::AttachmentMetadata());
62   }
63 
CreateSyncedItem(syncable::WriteTransaction * trans,ModelType type,const std::string & tag)64   int64 CreateSyncedItem(syncable::WriteTransaction* trans,
65                          ModelType type,
66                          const std::string& tag) {
67     syncable::Entry parent_entry(trans, syncable::GET_TYPE_ROOT, type);
68     syncable::MutableEntry entry(
69         trans,
70         syncable::CREATE,
71         type,
72         parent_entry.GetId(),
73         tag);
74 
75     entry.PutId(syncable::Id::CreateFromServerId(
76         id_factory_.NewServerId().GetServerId()));
77     entry.PutBaseVersion(10);
78     entry.PutServerVersion(10);
79     entry.PutIsUnappliedUpdate(false);
80     entry.PutIsUnsynced(false);
81     entry.PutIsDel(false);
82     entry.PutServerIsDel(false);
83 
84     return entry.GetMetahandle();
85   }
86 
CreateSuccessfulCommitResponse(const sync_pb::SyncEntity & entity,sync_pb::CommitResponse::EntryResponse * response)87   void CreateSuccessfulCommitResponse(
88       const sync_pb::SyncEntity& entity,
89       sync_pb::CommitResponse::EntryResponse* response) {
90     response->set_response_type(sync_pb::CommitResponse::SUCCESS);
91     response->set_non_unique_name(entity.name());
92     response->set_version(entity.version() + 1);
93     response->set_parent_id_string(entity.parent_id_string());
94 
95     if (entity.id_string()[0] == '-')  // Look for the - in 'c-1234' style IDs.
96       response->set_id_string(id_factory_.NewServerId().GetServerId());
97     else
98       response->set_id_string(entity.id_string());
99   }
100 
dir()101   syncable::Directory* dir() {
102     return dir_maker_.directory();
103   }
104 
105   TestIdFactory id_factory_;
106 
107   // Used in construction of DirectoryTypeDebugInfoEmitters.
108   ObserverList<TypeDebugInfoObserver> type_observers_;
109 
110  private:
111   base::MessageLoop loop_;  // Neeed to initialize the directory.
112   TestDirectorySetterUpper dir_maker_;
113 };
114 
115 // Verify that the DirectoryCommitContribution contains only entries of its
116 // specified type.
TEST_F(DirectoryCommitContributionTest,GatherByTypes)117 TEST_F(DirectoryCommitContributionTest, GatherByTypes) {
118   int64 pref1;
119   {
120     syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
121     pref1 = CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
122     CreateUnsyncedItem(&trans, PREFERENCES, "pref2");
123     CreateUnsyncedItem(&trans, EXTENSIONS, "extension1");
124   }
125 
126   DirectoryTypeDebugInfoEmitter emitter(PREFERENCES, &type_observers_);
127   scoped_ptr<DirectoryCommitContribution> cc(
128       DirectoryCommitContribution::Build(dir(), PREFERENCES, 5, &emitter));
129   ASSERT_EQ(2U, cc->GetNumEntries());
130 
131   const std::vector<int64>& metahandles = cc->metahandles_;
132   EXPECT_TRUE(std::find(metahandles.begin(), metahandles.end(), pref1) !=
133               metahandles.end());
134   EXPECT_TRUE(std::find(metahandles.begin(), metahandles.end(), pref1) !=
135               metahandles.end());
136 
137   cc->CleanUp();
138 }
139 
140 // Verify that the DirectoryCommitContributionTest builder function
141 // truncates if necessary.
TEST_F(DirectoryCommitContributionTest,GatherAndTruncate)142 TEST_F(DirectoryCommitContributionTest, GatherAndTruncate) {
143   int64 pref1;
144   int64 pref2;
145   {
146     syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
147     pref1 = CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
148     pref2 = CreateUnsyncedItem(&trans, PREFERENCES, "pref2");
149     CreateUnsyncedItem(&trans, EXTENSIONS, "extension1");
150   }
151 
152   DirectoryTypeDebugInfoEmitter emitter(PREFERENCES, &type_observers_);
153   scoped_ptr<DirectoryCommitContribution> cc(
154       DirectoryCommitContribution::Build(dir(), PREFERENCES, 1, &emitter));
155   ASSERT_EQ(1U, cc->GetNumEntries());
156 
157   int64 only_metahandle = cc->metahandles_[0];
158   EXPECT_TRUE(only_metahandle == pref1 || only_metahandle == pref2);
159 
160   cc->CleanUp();
161 }
162 
163 // Sanity check for building commits from DirectoryCommitContributions.
164 // This test makes two CommitContribution objects of different types and uses
165 // them to initialize a commit message.  Then it checks that the contents of the
166 // commit message match those of the directory they came from.
TEST_F(DirectoryCommitContributionTest,PrepareCommit)167 TEST_F(DirectoryCommitContributionTest, PrepareCommit) {
168   {
169     syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
170     CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
171     CreateUnsyncedItem(&trans, PREFERENCES, "pref2");
172     CreateUnsyncedItem(&trans, EXTENSIONS, "extension1");
173   }
174 
175   DirectoryTypeDebugInfoEmitter emitter1(PREFERENCES, &type_observers_);
176   DirectoryTypeDebugInfoEmitter emitter2(EXTENSIONS, &type_observers_);
177   scoped_ptr<DirectoryCommitContribution> pref_cc(
178       DirectoryCommitContribution::Build(dir(), PREFERENCES, 25, &emitter1));
179   scoped_ptr<DirectoryCommitContribution> ext_cc(
180       DirectoryCommitContribution::Build(dir(), EXTENSIONS, 25, &emitter2));
181 
182   sync_pb::ClientToServerMessage message;
183   pref_cc->AddToCommitMessage(&message);
184   ext_cc->AddToCommitMessage(&message);
185 
186   const sync_pb::CommitMessage& commit_message = message.commit();
187 
188   std::set<syncable::Id> ids_for_commit;
189   ASSERT_EQ(3, commit_message.entries_size());
190   for (int i = 0; i < commit_message.entries_size(); ++i) {
191     const sync_pb::SyncEntity& entity = commit_message.entries(i);
192     // The entities in this test have client-style IDs since they've never been
193     // committed before, so we must use CreateFromClientString to re-create them
194     // from the commit message.
195     ids_for_commit.insert(syncable::Id::CreateFromClientString(
196             entity.id_string()));
197   }
198 
199   ASSERT_EQ(3U, ids_for_commit.size());
200   {
201     syncable::ReadTransaction trans(FROM_HERE, dir());
202     for (std::set<syncable::Id>::iterator it = ids_for_commit.begin();
203          it != ids_for_commit.end(); ++it) {
204       SCOPED_TRACE(it->value());
205       syncable::Entry entry(&trans, syncable::GET_BY_ID, *it);
206       ASSERT_TRUE(entry.good());
207       EXPECT_TRUE(entry.GetSyncing());
208     }
209   }
210 
211   pref_cc->CleanUp();
212   ext_cc->CleanUp();
213 }
214 
215 // Check that deletion requests include a model type.
216 // This was not always the case, but was implemented to allow us to loosen some
217 // other restrictions in the protocol.
TEST_F(DirectoryCommitContributionTest,DeletedItemsWithSpecifics)218 TEST_F(DirectoryCommitContributionTest, DeletedItemsWithSpecifics) {
219   int64 pref1;
220   {
221     syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
222     pref1 = CreateSyncedItem(&trans, PREFERENCES, "pref1");
223     syncable::MutableEntry e1(&trans, syncable::GET_BY_HANDLE, pref1);
224     e1.PutIsDel(true);
225     e1.PutIsUnsynced(true);
226   }
227 
228   DirectoryTypeDebugInfoEmitter emitter(PREFERENCES, &type_observers_);
229   scoped_ptr<DirectoryCommitContribution> pref_cc(
230       DirectoryCommitContribution::Build(dir(), PREFERENCES, 25, &emitter));
231   ASSERT_TRUE(pref_cc);
232 
233   sync_pb::ClientToServerMessage message;
234   pref_cc->AddToCommitMessage(&message);
235 
236   const sync_pb::CommitMessage& commit_message = message.commit();
237   ASSERT_EQ(1, commit_message.entries_size());
238   EXPECT_TRUE(
239       commit_message.entries(0).specifics().has_preference());
240 
241   pref_cc->CleanUp();
242 }
243 
244 // As ususal, bookmarks are special.  Bookmark deletion is special.
245 // Deleted bookmarks include a valid "is folder" bit and their full specifics
246 // (especially the meta info, which is what server really wants).
TEST_F(DirectoryCommitContributionTest,DeletedBookmarksWithSpecifics)247 TEST_F(DirectoryCommitContributionTest, DeletedBookmarksWithSpecifics) {
248   int64 bm1;
249   {
250     syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
251     bm1 = CreateSyncedItem(&trans, BOOKMARKS, "bm1");
252     syncable::MutableEntry e1(&trans, syncable::GET_BY_HANDLE, bm1);
253 
254     e1.PutIsDir(true);
255     e1.PutServerIsDir(true);
256 
257     sync_pb::EntitySpecifics specifics;
258     sync_pb::BookmarkSpecifics* bm_specifics = specifics.mutable_bookmark();
259     bm_specifics->set_url("http://www.chrome.com");
260     bm_specifics->set_title("Chrome");
261     sync_pb::MetaInfo* meta_info = bm_specifics->add_meta_info();
262     meta_info->set_key("K");
263     meta_info->set_value("V");
264     e1.PutSpecifics(specifics);
265 
266     e1.PutIsDel(true);
267     e1.PutIsUnsynced(true);
268   }
269 
270   DirectoryTypeDebugInfoEmitter emitter(BOOKMARKS, &type_observers_);
271   scoped_ptr<DirectoryCommitContribution> bm_cc(
272       DirectoryCommitContribution::Build(dir(), BOOKMARKS, 25, &emitter));
273   ASSERT_TRUE(bm_cc);
274 
275   sync_pb::ClientToServerMessage message;
276   bm_cc->AddToCommitMessage(&message);
277 
278   const sync_pb::CommitMessage& commit_message = message.commit();
279   ASSERT_EQ(1, commit_message.entries_size());
280 
281   const sync_pb::SyncEntity& entity = commit_message.entries(0);
282   EXPECT_TRUE(entity.has_folder());
283   ASSERT_TRUE(entity.specifics().has_bookmark());
284   ASSERT_EQ(1, entity.specifics().bookmark().meta_info_size());
285   EXPECT_EQ("K", entity.specifics().bookmark().meta_info(0).key());
286   EXPECT_EQ("V", entity.specifics().bookmark().meta_info(0).value());
287 
288   bm_cc->CleanUp();
289 }
290 
291 // Test that bookmarks support hierarchy.
TEST_F(DirectoryCommitContributionTest,HierarchySupport_Bookmark)292 TEST_F(DirectoryCommitContributionTest, HierarchySupport_Bookmark) {
293 
294   // Create a normal-looking bookmark item.
295   int64 bm1;
296   {
297     syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
298     bm1 = CreateSyncedItem(&trans, BOOKMARKS, "bm1");
299     syncable::MutableEntry e(&trans, syncable::GET_BY_HANDLE, bm1);
300 
301     sync_pb::EntitySpecifics specifics;
302     sync_pb::BookmarkSpecifics* bm_specifics = specifics.mutable_bookmark();
303     bm_specifics->set_url("http://www.chrome.com");
304     bm_specifics->set_title("Chrome");
305     e.PutSpecifics(specifics);
306 
307     e.PutIsDel(false);
308     e.PutIsUnsynced(true);
309 
310     EXPECT_TRUE(e.ShouldMaintainHierarchy());
311   }
312 
313   DirectoryTypeDebugInfoEmitter emitter(BOOKMARKS, &type_observers_);
314   scoped_ptr<DirectoryCommitContribution> bm_cc(
315       DirectoryCommitContribution::Build(dir(), BOOKMARKS, 25, &emitter));
316 
317   sync_pb::ClientToServerMessage message;
318   bm_cc->AddToCommitMessage(&message);
319   const sync_pb::CommitMessage& commit_message = message.commit();
320   bm_cc->CleanUp();
321 
322   ASSERT_EQ(1, commit_message.entries_size());
323   EXPECT_TRUE(commit_message.entries(0).has_parent_id_string());
324   EXPECT_FALSE(commit_message.entries(0).parent_id_string().empty());
325 }
326 
327 // Test that preferences do not support hierarchy.
TEST_F(DirectoryCommitContributionTest,HierarchySupport_Preferences)328 TEST_F(DirectoryCommitContributionTest, HierarchySupport_Preferences) {
329   // Create a normal-looking prefs item.
330   int64 pref1;
331   {
332     syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
333     pref1 = CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
334     syncable::MutableEntry e(&trans, syncable::GET_BY_HANDLE, pref1);
335 
336     EXPECT_FALSE(e.ShouldMaintainHierarchy());
337   }
338 
339   DirectoryTypeDebugInfoEmitter emitter(PREFERENCES, &type_observers_);
340   scoped_ptr<DirectoryCommitContribution> pref_cc(
341       DirectoryCommitContribution::Build(dir(), PREFERENCES, 25, &emitter));
342 
343   sync_pb::ClientToServerMessage message;
344   pref_cc->AddToCommitMessage(&message);
345   const sync_pb::CommitMessage& commit_message = message.commit();
346   pref_cc->CleanUp();
347 
348   ASSERT_EQ(1, commit_message.entries_size());
349   EXPECT_FALSE(commit_message.entries(0).has_parent_id_string());
350   EXPECT_TRUE(commit_message.entries(0).parent_id_string().empty());
351 }
352 
AddAttachment(sync_pb::AttachmentMetadata * metadata,bool is_on_server)353 void AddAttachment(sync_pb::AttachmentMetadata* metadata, bool is_on_server) {
354   sync_pb::AttachmentMetadataRecord record;
355   *record.mutable_id() = CreateAttachmentIdProto();
356   record.set_is_on_server(is_on_server);
357   *metadata->add_record() = record;
358 }
359 
360 // Creates some unsynced items, pretends to commit them, and hands back a
361 // specially crafted response to the syncer in order to test commit response
362 // processing.  The response simulates a succesful commit scenario.
TEST_F(DirectoryCommitContributionTest,ProcessCommitResponse)363 TEST_F(DirectoryCommitContributionTest, ProcessCommitResponse) {
364   int64 pref1_handle;
365   int64 pref2_handle;
366   int64 ext1_handle;
367   {
368     syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
369     pref1_handle = CreateUnsyncedItem(&trans, PREFERENCES, "pref1");
370     pref2_handle = CreateUnsyncedItem(&trans, PREFERENCES, "pref2");
371     ext1_handle = CreateUnsyncedItem(&trans, EXTENSIONS, "extension1");
372   }
373 
374   DirectoryTypeDebugInfoEmitter emitter1(PREFERENCES, &type_observers_);
375   DirectoryTypeDebugInfoEmitter emitter2(EXTENSIONS, &type_observers_);
376   scoped_ptr<DirectoryCommitContribution> pref_cc(
377       DirectoryCommitContribution::Build(dir(), PREFERENCES, 25, &emitter1));
378   scoped_ptr<DirectoryCommitContribution> ext_cc(
379       DirectoryCommitContribution::Build(dir(), EXTENSIONS, 25, &emitter2));
380 
381   sync_pb::ClientToServerMessage message;
382   pref_cc->AddToCommitMessage(&message);
383   ext_cc->AddToCommitMessage(&message);
384 
385   const sync_pb::CommitMessage& commit_message = message.commit();
386   ASSERT_EQ(3, commit_message.entries_size());
387 
388   sync_pb::ClientToServerResponse response;
389   for (int i = 0; i < commit_message.entries_size(); ++i) {
390     sync_pb::SyncEntity entity = commit_message.entries(i);
391     sync_pb::CommitResponse_EntryResponse* entry_response =
392         response.mutable_commit()->add_entryresponse();
393     CreateSuccessfulCommitResponse(entity, entry_response);
394   }
395 
396   sessions::StatusController status;
397 
398   // Process these in reverse order.  Just because we can.
399   ext_cc->ProcessCommitResponse(response, &status);
400   pref_cc->ProcessCommitResponse(response, &status);
401 
402   {
403     syncable::ReadTransaction trans(FROM_HERE, dir());
404     syncable::Entry p1(&trans, syncable::GET_BY_HANDLE, pref1_handle);
405     EXPECT_TRUE(p1.GetId().ServerKnows());
406     EXPECT_FALSE(p1.GetSyncing());
407     EXPECT_LT(0, p1.GetServerVersion());
408 
409     syncable::Entry p2(&trans, syncable::GET_BY_HANDLE, pref2_handle);
410     EXPECT_TRUE(p2.GetId().ServerKnows());
411     EXPECT_FALSE(p2.GetSyncing());
412     EXPECT_LT(0, p2.GetServerVersion());
413 
414     syncable::Entry e1(&trans, syncable::GET_BY_HANDLE, ext1_handle);
415     EXPECT_TRUE(e1.GetId().ServerKnows());
416     EXPECT_FALSE(e1.GetSyncing());
417     EXPECT_LT(0, e1.GetServerVersion());
418   }
419 
420   pref_cc->CleanUp();
421   ext_cc->CleanUp();
422 }
423 
424 // Creates some unsynced items with attachments and verifies that only items
425 // where all attachments have been uploaded to the server are eligible to be
426 // committed.
TEST_F(DirectoryCommitContributionTest,ProcessCommitResponseWithAttachments)427 TEST_F(DirectoryCommitContributionTest, ProcessCommitResponseWithAttachments) {
428   int64 art1_handle;
429   int64 art2_handle;
430   int64 art3_handle;
431   {
432     syncable::WriteTransaction trans(FROM_HERE, syncable::UNITTEST, dir());
433 
434     // art1 has two attachments, both have been uploaded to the server.  art1 is
435     // eligible to be committed.
436     sync_pb::AttachmentMetadata art1_attachments;
437     AddAttachment(&art1_attachments, true /* is_on_server */);
438     AddAttachment(&art1_attachments, true /* is_on_server */);
439     art1_handle = CreateUnsyncedItemWithAttachments(
440         &trans, ARTICLES, "art1", art1_attachments);
441 
442     // art2 has two attachments, one of which has been uploaded to the
443     // server. art2 is not eligible to be committed.
444     sync_pb::AttachmentMetadata art2_attachments;
445     AddAttachment(&art2_attachments, false /* is_on_server */);
446     AddAttachment(&art2_attachments, true /* is_on_server */);
447     art2_handle = CreateUnsyncedItemWithAttachments(
448         &trans, ARTICLES, "art2", art2_attachments);
449 
450     // art3 has two attachments, neither of which have been uploaded to the
451     // server. art2 is not eligible to be committed.
452     sync_pb::AttachmentMetadata art3_attachments;
453     AddAttachment(&art3_attachments, false /* is_on_server */);
454     AddAttachment(&art3_attachments, false /* is_on_server */);
455     art3_handle = CreateUnsyncedItemWithAttachments(
456         &trans, ARTICLES, "art3", art3_attachments);
457   }
458 
459   DirectoryTypeDebugInfoEmitter emitter(ARTICLES, &type_observers_);
460   scoped_ptr<DirectoryCommitContribution> art_cc(
461       DirectoryCommitContribution::Build(dir(), ARTICLES, 25, &emitter));
462 
463   // Only art1 is ready.
464   EXPECT_EQ(1U, art_cc->GetNumEntries());
465 
466   sync_pb::ClientToServerMessage message;
467   art_cc->AddToCommitMessage(&message);
468 
469   const sync_pb::CommitMessage& commit_message = message.commit();
470   ASSERT_EQ(1, commit_message.entries_size());
471 
472   sync_pb::ClientToServerResponse response;
473   for (int i = 0; i < commit_message.entries_size(); ++i) {
474     sync_pb::SyncEntity entity = commit_message.entries(i);
475     sync_pb::CommitResponse_EntryResponse* entry_response =
476         response.mutable_commit()->add_entryresponse();
477     CreateSuccessfulCommitResponse(entity, entry_response);
478   }
479 
480   sessions::StatusController status;
481   art_cc->ProcessCommitResponse(response, &status);
482   {
483     syncable::ReadTransaction trans(FROM_HERE, dir());
484 
485     syncable::Entry a1(&trans, syncable::GET_BY_HANDLE, art1_handle);
486     EXPECT_TRUE(a1.GetId().ServerKnows());
487     EXPECT_FALSE(a1.GetSyncing());
488     EXPECT_LT(0, a1.GetServerVersion());
489 
490     syncable::Entry a2(&trans, syncable::GET_BY_HANDLE, art2_handle);
491     EXPECT_FALSE(a2.GetId().ServerKnows());
492     EXPECT_FALSE(a2.GetSyncing());
493     EXPECT_EQ(0, a2.GetServerVersion());
494 
495     syncable::Entry a3(&trans, syncable::GET_BY_HANDLE, art3_handle);
496     EXPECT_FALSE(a3.GetId().ServerKnows());
497     EXPECT_FALSE(a3.GetSyncing());
498     EXPECT_EQ(0, a3.GetServerVersion());
499   }
500 
501   art_cc->CleanUp();
502 }
503 
504 }  // namespace syncer
505