OLD | NEW |
---|---|
1 // Copyright 2017 The Chromium Authors. All rights reserved. | 1 // Copyright 2017 The Chromium Authors. All rights reserved. |
2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
4 | 4 |
5 #include "chrome/browser/ui/app_list/app_list_syncable_service.h" | 5 #include "chrome/browser/ui/app_list/app_list_syncable_service.h" |
6 #include "base/files/scoped_temp_dir.h" | 6 #include "base/files/scoped_temp_dir.h" |
7 #include "chrome/browser/extensions/extension_service.h" | 7 #include "chrome/browser/extensions/extension_service.h" |
8 #include "chrome/browser/profiles/profile_manager.h" | 8 #include "chrome/browser/profiles/profile_manager.h" |
9 #include "chrome/browser/ui/app_list/app_list_test_util.h" | 9 #include "chrome/browser/ui/app_list/app_list_test_util.h" |
10 #include "chrome/test/base/testing_browser_process.h" | 10 #include "chrome/test/base/testing_browser_process.h" |
11 #include "chrome/test/base/testing_profile.h" | 11 #include "chrome/test/base/testing_profile.h" |
12 #include "components/crx_file/id_util.h" | 12 #include "components/crx_file/id_util.h" |
13 #include "components/sync/model/attachments/attachment_service_proxy_for_test.h" | |
14 #include "components/sync/model/fake_sync_change_processor.h" | |
15 #include "components/sync/model/sync_error_factory.h" | |
16 #include "components/sync/model/sync_error_factory_mock.h" | |
17 #include "components/sync/protocol/sync.pb.h" | |
13 #include "extensions/browser/extension_system.h" | 18 #include "extensions/browser/extension_system.h" |
14 #include "extensions/common/constants.h" | 19 #include "extensions/common/constants.h" |
15 #include "ui/app_list/app_list_item.h" | 20 #include "ui/app_list/app_list_item.h" |
16 #include "ui/app_list/app_list_model.h" | 21 #include "ui/app_list/app_list_model.h" |
17 | 22 |
23 using namespace crx_file::id_util; | |
24 | |
18 namespace { | 25 namespace { |
19 | 26 |
20 scoped_refptr<extensions::Extension> MakeApp( | 27 scoped_refptr<extensions::Extension> MakeApp( |
21 const std::string& name, | 28 const std::string& name, |
22 const std::string& id, | 29 const std::string& id, |
23 extensions::Extension::InitFromValueFlags flags) { | 30 extensions::Extension::InitFromValueFlags flags) { |
24 std::string err; | 31 std::string err; |
25 base::DictionaryValue value; | 32 base::DictionaryValue value; |
26 value.SetString("name", name); | 33 value.SetString("name", name); |
27 value.SetString("version", "0.0"); | 34 value.SetString("version", "0.0"); |
(...skipping 11 matching lines...) Expand all Loading... | |
39 std::string next_app_id = app_id; | 46 std::string next_app_id = app_id; |
40 size_t index = next_app_id.length() - 1; | 47 size_t index = next_app_id.length() - 1; |
41 while (index > 0 && next_app_id[index] == 'p') | 48 while (index > 0 && next_app_id[index] == 'p') |
42 next_app_id[index--] = 'a'; | 49 next_app_id[index--] = 'a'; |
43 DCHECK(next_app_id[index] != 'p'); | 50 DCHECK(next_app_id[index] != 'p'); |
44 next_app_id[index]++; | 51 next_app_id[index]++; |
45 DCHECK(crx_file::id_util::IdIsValid(next_app_id)); | 52 DCHECK(crx_file::id_util::IdIsValid(next_app_id)); |
46 return next_app_id; | 53 return next_app_id; |
47 } | 54 } |
48 | 55 |
56 constexpr char kUnset[] = "__unset__"; | |
57 constexpr char kDefault[] = "__default__"; | |
58 | |
59 // These constants are defined as functions so their values can be derived via | |
60 // function calls. The constant naming scheme is kept to maintain readability. | |
61 const std::string kInvalidOrdinalsId() { | |
khmel
2017/06/23 22:37:27
nit: it seems const here does not make sense. You
rcui
2017/06/24 00:19:10
Keeping the const, as it maintains consistency wit
khmel
2017/06/24 00:30:16
Acknowledged.
| |
62 return GenerateId("invalid_ordinals"); | |
63 } | |
64 const std::string kEmptyItemNameId() { | |
65 return GenerateId("empty_item_name"); | |
66 } | |
67 const std::string kEmptyItemNameUnsetId() { | |
68 return GenerateId("empty_item_name_unset"); | |
69 } | |
70 const std::string kEmptyParentId() { | |
71 return GenerateId("empty_parent_id"); | |
72 } | |
73 const std::string kEmptyParentUnsetId() { | |
74 return GenerateId("empty_parent_id_unset"); | |
75 } | |
76 const std::string kEmptyOrdinalsId() { | |
77 return GenerateId("empty_ordinals"); | |
78 } | |
79 const std::string kEmptyOrdinalsUnsetId() { | |
80 return GenerateId("empty_ordinals_unset"); | |
81 } | |
82 const std::string kDupeItemId() { | |
83 return GenerateId("dupe_item_id"); | |
84 } | |
85 const std::string kParentId() { | |
86 return GenerateId("parent_id"); | |
87 } | |
88 | |
89 syncer::SyncData CreateAppRemoteData(const std::string& id, | |
90 const std::string& name, | |
91 const std::string& parent_id, | |
92 const std::string& item_ordinal, | |
93 const std::string& item_pin_ordinal) { | |
94 sync_pb::EntitySpecifics specifics; | |
95 sync_pb::AppListSpecifics* app_list = specifics.mutable_app_list(); | |
96 if (id != kUnset) | |
97 app_list->set_item_id(id); | |
98 app_list->set_item_type(sync_pb::AppListSpecifics_AppListItemType_TYPE_APP); | |
99 if (name != kUnset) | |
100 app_list->set_item_name(name); | |
101 if (parent_id != kUnset) | |
102 app_list->set_parent_id(parent_id); | |
103 if (item_ordinal != kUnset) | |
104 app_list->set_item_ordinal(item_ordinal); | |
105 if (item_pin_ordinal != kUnset) | |
106 app_list->set_item_pin_ordinal(item_pin_ordinal); | |
107 | |
108 return syncer::SyncData::CreateRemoteData( | |
109 std::hash<std::string>{}(id), specifics, base::Time(), | |
110 syncer::AttachmentIdList(), | |
111 syncer::AttachmentServiceProxyForTest::Create()); | |
112 } | |
113 | |
114 syncer::SyncDataList CreateBadAppRemoteData(const std::string& id) { | |
115 syncer::SyncDataList sync_list; | |
116 // Invalid item_ordinal and item_pin_ordinal. | |
117 sync_list.push_back(CreateAppRemoteData( | |
118 id == kDefault ? kInvalidOrdinalsId() : id, "item_name", kParentId(), | |
119 "$$invalid_ordinal$$", "$$invalid_ordinal$$")); | |
120 // Empty item name. | |
121 sync_list.push_back( | |
122 CreateAppRemoteData(id == kDefault ? kEmptyItemNameId() : id, "", | |
123 kParentId(), "ordinal", "pinordinal")); | |
124 sync_list.push_back( | |
125 CreateAppRemoteData(id == kDefault ? kEmptyItemNameUnsetId() : id, kUnset, | |
126 kParentId(), "ordinal", "pinordinal")); | |
127 // Empty parent ID. | |
128 sync_list.push_back( | |
129 CreateAppRemoteData(id == kDefault ? kEmptyParentId() : id, "item_name", | |
130 "", "ordinal", "pinordinal")); | |
131 sync_list.push_back( | |
132 CreateAppRemoteData(id == kDefault ? kEmptyParentUnsetId() : id, | |
133 "item_name", kUnset, "ordinal", "pinordinal")); | |
134 // Empty item_ordinal and item_pin_ordinal. | |
135 sync_list.push_back( | |
136 CreateAppRemoteData(id == kDefault ? kEmptyOrdinalsId() : id, "item_name", | |
137 kParentId(), "", "")); | |
138 sync_list.push_back( | |
139 CreateAppRemoteData(id == kDefault ? kEmptyOrdinalsUnsetId() : id, | |
140 "item_name", kParentId(), kUnset, kUnset)); | |
141 // Duplicate item_id. | |
142 sync_list.push_back(CreateAppRemoteData(id == kDefault ? kDupeItemId() : id, | |
143 "item_name", kParentId(), "ordinal", | |
144 "pinordinal")); | |
145 sync_list.push_back(CreateAppRemoteData(id == kDefault ? kDupeItemId() : id, | |
146 "item_name_dupe", kParentId(), | |
147 "ordinal", "pinordinal")); | |
148 // Empty item_id. | |
149 sync_list.push_back(CreateAppRemoteData("", "item_name", kParentId(), | |
150 "ordinal", "pinordinal")); | |
151 sync_list.push_back(CreateAppRemoteData(kUnset, "item_name", kParentId(), | |
152 "ordinal", "pinordinal")); | |
153 // All fields empty. | |
154 sync_list.push_back(CreateAppRemoteData("", "", "", "", "")); | |
155 sync_list.push_back( | |
156 CreateAppRemoteData(kUnset, kUnset, kUnset, kUnset, kUnset)); | |
157 | |
158 return sync_list; | |
159 } | |
160 | |
49 } // namespace | 161 } // namespace |
50 | 162 |
51 class AppListSyncableServiceTest : public AppListTestBase { | 163 class AppListSyncableServiceTest : public AppListTestBase { |
52 public: | 164 public: |
53 AppListSyncableServiceTest() = default; | 165 AppListSyncableServiceTest() = default; |
54 ~AppListSyncableServiceTest() override = default; | 166 ~AppListSyncableServiceTest() override = default; |
55 | 167 |
56 void SetUp() override { | 168 void SetUp() override { |
57 AppListTestBase::SetUp(); | 169 AppListTestBase::SetUp(); |
58 | 170 |
59 // Make sure we have a Profile Manager. | 171 // Make sure we have a Profile Manager. |
60 DCHECK(temp_dir_.CreateUniqueTempDir()); | 172 DCHECK(temp_dir_.CreateUniqueTempDir()); |
61 TestingBrowserProcess::GetGlobal()->SetProfileManager( | 173 TestingBrowserProcess::GetGlobal()->SetProfileManager( |
62 new ProfileManagerWithoutInit(temp_dir_.GetPath())); | 174 new ProfileManagerWithoutInit(temp_dir_.GetPath())); |
63 | 175 |
64 extensions::ExtensionSystem* extension_system = | 176 extensions::ExtensionSystem* extension_system = |
65 extensions::ExtensionSystem::Get(profile_.get()); | 177 extensions::ExtensionSystem::Get(profile_.get()); |
66 DCHECK(extension_system); | 178 DCHECK(extension_system); |
67 app_list_syncable_service_.reset( | 179 app_list_syncable_service_.reset( |
68 new app_list::AppListSyncableService(profile_.get(), extension_system)); | 180 new app_list::AppListSyncableService(profile_.get(), extension_system)); |
69 } | 181 } |
70 | 182 |
71 void TearDown() override { app_list_syncable_service_.reset(); } | 183 void TearDown() override { app_list_syncable_service_.reset(); } |
72 | 184 |
73 app_list::AppListModel* model() { | 185 app_list::AppListModel* model() { |
74 return app_list_syncable_service_->GetModel(); | 186 return app_list_syncable_service_->GetModel(); |
75 } | 187 } |
76 | 188 |
189 const app_list::AppListSyncableService::SyncItem* GetSyncItem( | |
190 const std::string& id) const { | |
191 return app_list_syncable_service_->GetSyncItem(id); | |
192 } | |
193 | |
194 protected: | |
195 app_list::AppListSyncableService* app_list_syncable_service() { | |
196 return app_list_syncable_service_.get(); | |
197 } | |
198 | |
77 private: | 199 private: |
78 base::ScopedTempDir temp_dir_; | 200 base::ScopedTempDir temp_dir_; |
79 std::unique_ptr<app_list::AppListSyncableService> app_list_syncable_service_; | 201 std::unique_ptr<app_list::AppListSyncableService> app_list_syncable_service_; |
80 | 202 |
81 DISALLOW_COPY_AND_ASSIGN(AppListSyncableServiceTest); | 203 DISALLOW_COPY_AND_ASSIGN(AppListSyncableServiceTest); |
82 }; | 204 }; |
83 | 205 |
84 TEST_F(AppListSyncableServiceTest, OEMFolderForConflictingPos) { | 206 TEST_F(AppListSyncableServiceTest, OEMFolderForConflictingPos) { |
85 // Create a "web store" app. | 207 // Create a "web store" app. |
86 const std::string web_store_app_id(extensions::kWebStoreAppId); | 208 const std::string web_store_app_id(extensions::kWebStoreAppId); |
(...skipping 37 matching lines...) Expand 10 before | Expand all | Expand 10 after Loading... | |
124 EXPECT_FALSE(model()->top_level_item_list()->FindItemIndex(oem_app_id, | 246 EXPECT_FALSE(model()->top_level_item_list()->FindItemIndex(oem_app_id, |
125 &oem_app_index)); | 247 &oem_app_index)); |
126 // But OEM folder is. | 248 // But OEM folder is. |
127 EXPECT_TRUE(model()->top_level_item_list()->FindItemIndex( | 249 EXPECT_TRUE(model()->top_level_item_list()->FindItemIndex( |
128 app_list::AppListSyncableService::kOemFolderId, &oem_folder_index)); | 250 app_list::AppListSyncableService::kOemFolderId, &oem_folder_index)); |
129 | 251 |
130 // Ensure right item sequence. | 252 // Ensure right item sequence. |
131 EXPECT_EQ(some_app_index, web_store_app_index + 1); | 253 EXPECT_EQ(some_app_index, web_store_app_index + 1); |
132 EXPECT_EQ(oem_folder_index, web_store_app_index + 2); | 254 EXPECT_EQ(oem_folder_index, web_store_app_index + 2); |
133 } | 255 } |
256 | |
257 TEST_F(AppListSyncableServiceTest, InitialMerge) { | |
258 const std::string kItemId1 = GenerateId("item_id1"); | |
259 const std::string kItemId2 = GenerateId("item_id2"); | |
260 | |
261 syncer::SyncDataList sync_list; | |
262 sync_list.push_back(CreateAppRemoteData(kItemId1, "item_name1", | |
263 GenerateId("parent_id1"), "ordinal", | |
264 "pinordinal")); | |
265 sync_list.push_back(CreateAppRemoteData(kItemId2, "item_name2", | |
266 GenerateId("parent_id2"), "ordinal", | |
267 "pinordinal")); | |
268 | |
269 app_list_syncable_service()->MergeDataAndStartSyncing( | |
270 syncer::APP_LIST, sync_list, | |
271 base::MakeUnique<syncer::FakeSyncChangeProcessor>(), | |
272 base::MakeUnique<syncer::SyncErrorFactoryMock>()); | |
273 content::RunAllBlockingPoolTasksUntilIdle(); | |
274 | |
275 ASSERT_TRUE(GetSyncItem(kItemId1)); | |
276 EXPECT_EQ("item_name1", GetSyncItem(kItemId1)->item_name); | |
277 EXPECT_EQ(GenerateId("parent_id1"), GetSyncItem(kItemId1)->parent_id); | |
278 EXPECT_EQ("ordinal", GetSyncItem(kItemId1)->item_ordinal.ToDebugString()); | |
279 EXPECT_EQ("pinordinal", | |
280 GetSyncItem(kItemId1)->item_pin_ordinal.ToDebugString()); | |
281 | |
282 ASSERT_TRUE(GetSyncItem(kItemId2)); | |
283 EXPECT_EQ("item_name2", GetSyncItem(kItemId2)->item_name); | |
284 EXPECT_EQ(GenerateId("parent_id2"), GetSyncItem(kItemId2)->parent_id); | |
285 EXPECT_EQ("ordinal", GetSyncItem(kItemId2)->item_ordinal.ToDebugString()); | |
286 EXPECT_EQ("pinordinal", | |
287 GetSyncItem(kItemId2)->item_pin_ordinal.ToDebugString()); | |
288 } | |
289 | |
290 TEST_F(AppListSyncableServiceTest, InitialMerge_BadData) { | |
291 syncer::SyncDataList sync_list = CreateBadAppRemoteData(kDefault); | |
khmel
2017/06/23 22:37:27
nit: const syncer::...
rcui
2017/06/24 00:19:10
Done.
| |
292 | |
293 app_list_syncable_service()->MergeDataAndStartSyncing( | |
294 syncer::APP_LIST, sync_list, | |
295 base::MakeUnique<syncer::FakeSyncChangeProcessor>(), | |
296 base::MakeUnique<syncer::SyncErrorFactoryMock>()); | |
297 content::RunAllBlockingPoolTasksUntilIdle(); | |
298 | |
299 // Invalid item_ordinal and item_pin_ordinal. | |
300 ASSERT_TRUE(GetSyncItem(kInvalidOrdinalsId())); | |
301 EXPECT_EQ("n", | |
khmel
2017/06/23 22:37:27
nit: It is not clear that "n" represents initial v
rcui
2017/06/24 00:19:10
With tests, I think it's good to be as literal as
khmel
2017/06/24 00:30:16
Acknowledged.
| |
302 GetSyncItem(kInvalidOrdinalsId())->item_ordinal.ToDebugString()); | |
303 EXPECT_EQ( | |
304 "INVALID[$$invalid_ordinal$$]", | |
305 GetSyncItem(kInvalidOrdinalsId())->item_pin_ordinal.ToDebugString()); | |
306 | |
307 // Empty item name. | |
308 ASSERT_TRUE(GetSyncItem(kEmptyItemNameId())); | |
309 EXPECT_EQ("", GetSyncItem(kEmptyItemNameId())->item_name); | |
310 EXPECT_TRUE(GetSyncItem(kEmptyItemNameUnsetId())); | |
311 EXPECT_EQ("", GetSyncItem(kEmptyItemNameUnsetId())->item_name); | |
312 | |
313 // Empty parent ID. | |
314 ASSERT_TRUE(GetSyncItem(kEmptyParentId())); | |
315 EXPECT_EQ("", GetSyncItem(kEmptyParentId())->parent_id); | |
316 EXPECT_TRUE(GetSyncItem(kEmptyParentUnsetId())); | |
317 EXPECT_EQ("", GetSyncItem(kEmptyParentUnsetId())->parent_id); | |
318 | |
319 // Empty item_ordinal and item_pin_ordinal. | |
320 ASSERT_TRUE(GetSyncItem(kEmptyOrdinalsId())); | |
321 EXPECT_EQ("n", GetSyncItem(kEmptyOrdinalsId())->item_ordinal.ToDebugString()); | |
322 EXPECT_EQ("INVALID[]", | |
323 GetSyncItem(kEmptyOrdinalsId())->item_pin_ordinal.ToDebugString()); | |
324 ASSERT_TRUE(GetSyncItem(kEmptyOrdinalsUnsetId())); | |
325 EXPECT_EQ("n", | |
326 GetSyncItem(kEmptyOrdinalsUnsetId())->item_ordinal.ToDebugString()); | |
327 EXPECT_EQ( | |
328 "INVALID[]", | |
329 GetSyncItem(kEmptyOrdinalsUnsetId())->item_pin_ordinal.ToDebugString()); | |
330 | |
331 // Duplicate item_id overrides previous. | |
332 ASSERT_TRUE(GetSyncItem(kDupeItemId())); | |
333 EXPECT_EQ("item_name_dupe", GetSyncItem(kDupeItemId())->item_name); | |
334 } | |
335 | |
336 TEST_F(AppListSyncableServiceTest, InitialMergeAndUpdate) { | |
337 const std::string kItemId1 = GenerateId("item_id1"); | |
338 const std::string kItemId2 = GenerateId("item_id2"); | |
339 | |
340 syncer::SyncDataList sync_list; | |
341 sync_list.push_back(CreateAppRemoteData(kItemId1, "item_name1", kParentId(), | |
342 "ordinal", "pinordinal")); | |
343 sync_list.push_back(CreateAppRemoteData(kItemId2, "item_name2", kParentId(), | |
344 "ordinal", "pinordinal")); | |
345 | |
346 app_list_syncable_service()->MergeDataAndStartSyncing( | |
347 syncer::APP_LIST, sync_list, | |
348 base::MakeUnique<syncer::FakeSyncChangeProcessor>(), | |
349 base::MakeUnique<syncer::SyncErrorFactoryMock>()); | |
350 content::RunAllBlockingPoolTasksUntilIdle(); | |
351 | |
352 ASSERT_TRUE(GetSyncItem(kItemId1)); | |
353 ASSERT_TRUE(GetSyncItem(kItemId2)); | |
354 | |
355 syncer::SyncChangeList change_list; | |
356 change_list.push_back(syncer::SyncChange( | |
357 FROM_HERE, syncer::SyncChange::ACTION_UPDATE, | |
358 CreateAppRemoteData(kItemId1, "item_name1x", GenerateId("parent_id1x"), | |
359 "ordinalx", "pinordinalx"))); | |
360 change_list.push_back(syncer::SyncChange( | |
361 FROM_HERE, syncer::SyncChange::ACTION_UPDATE, | |
362 CreateAppRemoteData(kItemId2, "item_name2x", GenerateId("parent_id2x"), | |
363 "ordinalx", "pinordinalx"))); | |
364 | |
365 app_list_syncable_service()->ProcessSyncChanges(tracked_objects::Location(), | |
366 change_list); | |
367 content::RunAllBlockingPoolTasksUntilIdle(); | |
368 | |
369 ASSERT_TRUE(GetSyncItem(kItemId1)); | |
370 EXPECT_EQ("item_name1x", GetSyncItem(kItemId1)->item_name); | |
khmel
2017/06/23 22:37:27
nit: It seems you frequently use this pattern. If
rcui
2017/06/24 00:19:10
I went ahead and tried it, and the issue with this
khmel
2017/06/24 00:30:16
Usually this is not a problem.
For example: https:
rcui
2017/06/24 01:01:54
It does seem like it'll make tests harder to maint
| |
371 EXPECT_EQ(GenerateId("parent_id1x"), GetSyncItem(kItemId1)->parent_id); | |
372 EXPECT_EQ("ordinalx", GetSyncItem(kItemId1)->item_ordinal.ToDebugString()); | |
373 EXPECT_EQ("pinordinalx", | |
374 GetSyncItem(kItemId1)->item_pin_ordinal.ToDebugString()); | |
375 | |
376 ASSERT_TRUE(GetSyncItem(kItemId2)); | |
377 EXPECT_EQ("item_name2x", GetSyncItem(kItemId2)->item_name); | |
378 EXPECT_EQ(GenerateId("parent_id2x"), GetSyncItem(kItemId2)->parent_id); | |
379 EXPECT_EQ("ordinalx", GetSyncItem(kItemId2)->item_ordinal.ToDebugString()); | |
380 EXPECT_EQ("pinordinalx", | |
381 GetSyncItem(kItemId2)->item_pin_ordinal.ToDebugString()); | |
382 } | |
383 | |
384 TEST_F(AppListSyncableServiceTest, InitialMergeAndUpdate_BadData) { | |
385 const std::string kItemId = GenerateId("item_id"); | |
386 | |
387 syncer::SyncDataList sync_list; | |
388 sync_list.push_back(CreateAppRemoteData(kItemId, "item_name", kParentId(), | |
389 "ordinal", "pinordinal")); | |
390 | |
391 app_list_syncable_service()->MergeDataAndStartSyncing( | |
392 syncer::APP_LIST, sync_list, | |
393 base::MakeUnique<syncer::FakeSyncChangeProcessor>(), | |
394 base::MakeUnique<syncer::SyncErrorFactoryMock>()); | |
395 content::RunAllBlockingPoolTasksUntilIdle(); | |
396 | |
397 ASSERT_TRUE(GetSyncItem(kItemId)); | |
398 | |
399 syncer::SyncChangeList change_list; | |
400 syncer::SyncDataList update_list = CreateBadAppRemoteData(kItemId); | |
401 for (syncer::SyncDataList::const_iterator iter = update_list.begin(); | |
402 iter != update_list.end(); ++iter) { | |
403 change_list.push_back(syncer::SyncChange( | |
404 FROM_HERE, syncer::SyncChange::ACTION_UPDATE, *iter)); | |
405 } | |
406 | |
407 // Validate items with bad data are processed without crashing. | |
408 app_list_syncable_service()->ProcessSyncChanges(tracked_objects::Location(), | |
409 change_list); | |
410 content::RunAllBlockingPoolTasksUntilIdle(); | |
411 | |
412 ASSERT_TRUE(GetSyncItem(kItemId)); | |
khmel
2017/06/23 22:37:27
nit: it would be nice to check that sync item was
rcui
2017/06/24 00:19:11
Given the type of bad data we're using, the sync i
khmel
2017/06/24 00:30:16
Up to you, I just wanted to see the use case when
rcui
2017/06/24 01:01:54
Acknowledged.
| |
413 } | |
OLD | NEW |