Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(336)

Side by Side Diff: chrome/browser/sync/engine/apply_updates_command_unittest.cc

Issue 8625005: [Sync] Make ModelSafeWorker a true interface (Closed) Base URL: svn://svn.chromium.org/chrome/trunk/src
Patch Set: Sync to head Created 9 years, 1 month ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch | Annotate | Revision Log
« no previous file with comments | « no previous file | chrome/browser/sync/engine/download_updates_command_unittest.cc » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 // Copyright (c) 2011 The Chromium Authors. All rights reserved. 1 // Copyright (c) 2011 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 <string> 5 #include <string>
6 6
7 #include "base/format_macros.h" 7 #include "base/format_macros.h"
8 #include "base/location.h" 8 #include "base/location.h"
9 #include "base/stringprintf.h" 9 #include "base/stringprintf.h"
10 #include "chrome/browser/sync/engine/apply_updates_command.h" 10 #include "chrome/browser/sync/engine/apply_updates_command.h"
11 #include "chrome/browser/sync/engine/nigori_util.h" 11 #include "chrome/browser/sync/engine/nigori_util.h"
12 #include "chrome/browser/sync/engine/syncer.h" 12 #include "chrome/browser/sync/engine/syncer.h"
13 #include "chrome/browser/sync/engine/syncer_util.h" 13 #include "chrome/browser/sync/engine/syncer_util.h"
14 #include "chrome/browser/sync/protocol/bookmark_specifics.pb.h" 14 #include "chrome/browser/sync/protocol/bookmark_specifics.pb.h"
15 #include "chrome/browser/sync/protocol/password_specifics.pb.h" 15 #include "chrome/browser/sync/protocol/password_specifics.pb.h"
16 #include "chrome/browser/sync/sessions/sync_session.h" 16 #include "chrome/browser/sync/sessions/sync_session.h"
17 #include "chrome/browser/sync/syncable/directory_manager.h" 17 #include "chrome/browser/sync/syncable/directory_manager.h"
18 #include "chrome/browser/sync/syncable/syncable.h" 18 #include "chrome/browser/sync/syncable/syncable.h"
19 #include "chrome/browser/sync/syncable/syncable_id.h" 19 #include "chrome/browser/sync/syncable/syncable_id.h"
20 #include "chrome/browser/sync/test/engine/fake_model_worker.h"
20 #include "chrome/browser/sync/test/engine/syncer_command_test.h" 21 #include "chrome/browser/sync/test/engine/syncer_command_test.h"
21 #include "chrome/browser/sync/test/engine/test_id_factory.h" 22 #include "chrome/browser/sync/test/engine/test_id_factory.h"
22 #include "testing/gtest/include/gtest/gtest.h" 23 #include "testing/gtest/include/gtest/gtest.h"
23 24
24 namespace browser_sync { 25 namespace browser_sync {
25 26
26 using sessions::SyncSession; 27 using sessions::SyncSession;
27 using std::string; 28 using std::string;
28 using syncable::Entry; 29 using syncable::Entry;
29 using syncable::GetAllRealModelTypes; 30 using syncable::GetAllRealModelTypes;
(...skipping 15 matching lines...) Expand all
45 // A test fixture for tests exercising ApplyUpdatesCommand. 46 // A test fixture for tests exercising ApplyUpdatesCommand.
46 class ApplyUpdatesCommandTest : public SyncerCommandTest { 47 class ApplyUpdatesCommandTest : public SyncerCommandTest {
47 public: 48 public:
48 protected: 49 protected:
49 ApplyUpdatesCommandTest() : next_revision_(1) {} 50 ApplyUpdatesCommandTest() : next_revision_(1) {}
50 virtual ~ApplyUpdatesCommandTest() {} 51 virtual ~ApplyUpdatesCommandTest() {}
51 52
52 virtual void SetUp() { 53 virtual void SetUp() {
53 workers()->clear(); 54 workers()->clear();
54 mutable_routing_info()->clear(); 55 mutable_routing_info()->clear();
55 // GROUP_PASSIVE worker. 56 workers()->push_back(
56 workers()->push_back(make_scoped_refptr(new ModelSafeWorker())); 57 make_scoped_refptr(new FakeModelWorker(GROUP_UI)));
57 (*mutable_routing_info())[syncable::BOOKMARKS] = GROUP_PASSIVE; 58 workers()->push_back(
58 (*mutable_routing_info())[syncable::PASSWORDS] = GROUP_PASSIVE; 59 make_scoped_refptr(new FakeModelWorker(GROUP_PASSWORD)));
60 workers()->push_back(
61 make_scoped_refptr(new FakeModelWorker(GROUP_PASSIVE)));
62 (*mutable_routing_info())[syncable::BOOKMARKS] = GROUP_UI;
63 (*mutable_routing_info())[syncable::PASSWORDS] = GROUP_PASSWORD;
59 (*mutable_routing_info())[syncable::NIGORI] = GROUP_PASSIVE; 64 (*mutable_routing_info())[syncable::NIGORI] = GROUP_PASSIVE;
60 SyncerCommandTest::SetUp(); 65 SyncerCommandTest::SetUp();
61 } 66 }
62 67
63 // Create a new unapplied folder node with a parent. 68 // Create a new unapplied folder node with a parent.
64 void CreateUnappliedNewItemWithParent( 69 void CreateUnappliedNewItemWithParent(
65 const string& item_id, 70 const string& item_id,
66 const sync_pb::EntitySpecifics& specifics, 71 const sync_pb::EntitySpecifics& specifics,
67 const string& parent_id) { 72 const string& parent_id) {
68 ScopedDirLookup dir(syncdb()->manager(), syncdb()->name()); 73 ScopedDirLookup dir(syncdb()->manager(), syncdb()->name());
(...skipping 83 matching lines...) Expand 10 before | Expand all | Expand 10 after
152 DefaultBookmarkSpecifics(), 157 DefaultBookmarkSpecifics(),
153 root_server_id); 158 root_server_id);
154 CreateUnappliedNewItemWithParent("child", 159 CreateUnappliedNewItemWithParent("child",
155 DefaultBookmarkSpecifics(), 160 DefaultBookmarkSpecifics(),
156 "parent"); 161 "parent");
157 162
158 apply_updates_command_.ExecuteImpl(session()); 163 apply_updates_command_.ExecuteImpl(session());
159 164
160 sessions::StatusController* status = session()->status_controller(); 165 sessions::StatusController* status = session()->status_controller();
161 166
162 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); 167 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
163 EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize()) 168 EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize())
164 << "All updates should have been attempted"; 169 << "All updates should have been attempted";
165 EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize()) 170 EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize())
166 << "Simple update shouldn't result in conflicts"; 171 << "Simple update shouldn't result in conflicts";
167 EXPECT_EQ(2, status->update_progress().SuccessfullyAppliedUpdateCount()) 172 EXPECT_EQ(2, status->update_progress().SuccessfullyAppliedUpdateCount())
168 << "All items should have been successfully applied"; 173 << "All items should have been successfully applied";
169 } 174 }
170 175
171 TEST_F(ApplyUpdatesCommandTest, UpdateWithChildrenBeforeParents) { 176 TEST_F(ApplyUpdatesCommandTest, UpdateWithChildrenBeforeParents) {
172 // Set a bunch of updates which are difficult to apply in the order 177 // Set a bunch of updates which are difficult to apply in the order
(...skipping 11 matching lines...) Expand all
184 CreateUnappliedNewItemWithParent("a_child_created_second", 189 CreateUnappliedNewItemWithParent("a_child_created_second",
185 DefaultBookmarkSpecifics(), 190 DefaultBookmarkSpecifics(),
186 "parent"); 191 "parent");
187 CreateUnappliedNewItemWithParent("x_child_created_second", 192 CreateUnappliedNewItemWithParent("x_child_created_second",
188 DefaultBookmarkSpecifics(), 193 DefaultBookmarkSpecifics(),
189 "parent"); 194 "parent");
190 195
191 apply_updates_command_.ExecuteImpl(session()); 196 apply_updates_command_.ExecuteImpl(session());
192 197
193 sessions::StatusController* status = session()->status_controller(); 198 sessions::StatusController* status = session()->status_controller();
194 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); 199 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
195 EXPECT_EQ(5, status->update_progress().AppliedUpdatesSize()) 200 EXPECT_EQ(5, status->update_progress().AppliedUpdatesSize())
196 << "All updates should have been attempted"; 201 << "All updates should have been attempted";
197 EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize()) 202 EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize())
198 << "Simple update shouldn't result in conflicts, even if out-of-order"; 203 << "Simple update shouldn't result in conflicts, even if out-of-order";
199 EXPECT_EQ(5, status->update_progress().SuccessfullyAppliedUpdateCount()) 204 EXPECT_EQ(5, status->update_progress().SuccessfullyAppliedUpdateCount())
200 << "All updates should have been successfully applied"; 205 << "All updates should have been successfully applied";
201 } 206 }
202 207
203 TEST_F(ApplyUpdatesCommandTest, NestedItemsWithUnknownParent) { 208 TEST_F(ApplyUpdatesCommandTest, NestedItemsWithUnknownParent) {
204 // We shouldn't be able to do anything with either of these items. 209 // We shouldn't be able to do anything with either of these items.
205 CreateUnappliedNewItemWithParent("some_item", 210 CreateUnappliedNewItemWithParent("some_item",
206 DefaultBookmarkSpecifics(), 211 DefaultBookmarkSpecifics(),
207 "unknown_parent"); 212 "unknown_parent");
208 CreateUnappliedNewItemWithParent("some_other_item", 213 CreateUnappliedNewItemWithParent("some_other_item",
209 DefaultBookmarkSpecifics(), 214 DefaultBookmarkSpecifics(),
210 "some_item"); 215 "some_item");
211 216
212 apply_updates_command_.ExecuteImpl(session()); 217 apply_updates_command_.ExecuteImpl(session());
213 218
214 sessions::StatusController* status = session()->status_controller(); 219 sessions::StatusController* status = session()->status_controller();
215 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); 220 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
216 EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize()) 221 EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize())
217 << "All updates should have been attempted"; 222 << "All updates should have been attempted";
218 EXPECT_EQ(2, status->conflict_progress().ConflictingItemsSize()) 223 EXPECT_EQ(2, status->conflict_progress().ConflictingItemsSize())
219 << "All updates with an unknown ancestors should be in conflict"; 224 << "All updates with an unknown ancestors should be in conflict";
220 EXPECT_EQ(0, status->update_progress().SuccessfullyAppliedUpdateCount()) 225 EXPECT_EQ(0, status->update_progress().SuccessfullyAppliedUpdateCount())
221 << "No item with an unknown ancestor should be applied"; 226 << "No item with an unknown ancestor should be applied";
222 } 227 }
223 228
224 TEST_F(ApplyUpdatesCommandTest, ItemsBothKnownAndUnknown) { 229 TEST_F(ApplyUpdatesCommandTest, ItemsBothKnownAndUnknown) {
225 // See what happens when there's a mixture of good and bad updates. 230 // See what happens when there's a mixture of good and bad updates.
(...skipping 13 matching lines...) Expand all
239 CreateUnappliedNewItemWithParent("third_known_item", 244 CreateUnappliedNewItemWithParent("third_known_item",
240 DefaultBookmarkSpecifics(), 245 DefaultBookmarkSpecifics(),
241 "fourth_known_item"); 246 "fourth_known_item");
242 CreateUnappliedNewItemWithParent("fourth_known_item", 247 CreateUnappliedNewItemWithParent("fourth_known_item",
243 DefaultBookmarkSpecifics(), 248 DefaultBookmarkSpecifics(),
244 root_server_id); 249 root_server_id);
245 250
246 apply_updates_command_.ExecuteImpl(session()); 251 apply_updates_command_.ExecuteImpl(session());
247 252
248 sessions::StatusController* status = session()->status_controller(); 253 sessions::StatusController* status = session()->status_controller();
249 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); 254 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
250 EXPECT_EQ(6, status->update_progress().AppliedUpdatesSize()) 255 EXPECT_EQ(6, status->update_progress().AppliedUpdatesSize())
251 << "All updates should have been attempted"; 256 << "All updates should have been attempted";
252 EXPECT_EQ(2, status->conflict_progress().ConflictingItemsSize()) 257 EXPECT_EQ(2, status->conflict_progress().ConflictingItemsSize())
253 << "The updates with unknown ancestors should be in conflict"; 258 << "The updates with unknown ancestors should be in conflict";
254 EXPECT_EQ(4, status->update_progress().SuccessfullyAppliedUpdateCount()) 259 EXPECT_EQ(4, status->update_progress().SuccessfullyAppliedUpdateCount())
255 << "The updates with known ancestors should be successfully applied"; 260 << "The updates with known ancestors should be successfully applied";
256 } 261 }
257 262
258 TEST_F(ApplyUpdatesCommandTest, DecryptablePassword) { 263 TEST_F(ApplyUpdatesCommandTest, DecryptablePassword) {
259 // Decryptable password updates should be applied. 264 // Decryptable password updates should be applied.
(...skipping 15 matching lines...) Expand all
275 sync_pb::PasswordSpecificsData data; 280 sync_pb::PasswordSpecificsData data;
276 data.set_origin("http://example.com"); 281 data.set_origin("http://example.com");
277 282
278 cryptographer->Encrypt(data, 283 cryptographer->Encrypt(data,
279 specifics.MutableExtension(sync_pb::password)->mutable_encrypted()); 284 specifics.MutableExtension(sync_pb::password)->mutable_encrypted());
280 CreateUnappliedNewItem("item", specifics, false); 285 CreateUnappliedNewItem("item", specifics, false);
281 286
282 apply_updates_command_.ExecuteImpl(session()); 287 apply_updates_command_.ExecuteImpl(session());
283 288
284 sessions::StatusController* status = session()->status_controller(); 289 sessions::StatusController* status = session()->status_controller();
285 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); 290 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSWORD);
286 EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize()) 291 EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize())
287 << "All updates should have been attempted"; 292 << "All updates should have been attempted";
288 EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize()) 293 EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize())
289 << "No update should be in conflict because they're all decryptable"; 294 << "No update should be in conflict because they're all decryptable";
290 EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount()) 295 EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount())
291 << "The updates that can be decrypted should be applied"; 296 << "The updates that can be decrypted should be applied";
292 } 297 }
293 298
294 TEST_F(ApplyUpdatesCommandTest, UndecryptableData) { 299 TEST_F(ApplyUpdatesCommandTest, UndecryptableData) {
295 // Undecryptable updates should not be applied. 300 // Undecryptable updates should not be applied.
296 sync_pb::EntitySpecifics encrypted_bookmark; 301 sync_pb::EntitySpecifics encrypted_bookmark;
297 encrypted_bookmark.mutable_encrypted(); 302 encrypted_bookmark.mutable_encrypted();
298 AddDefaultExtensionValue(syncable::BOOKMARKS, &encrypted_bookmark); 303 AddDefaultExtensionValue(syncable::BOOKMARKS, &encrypted_bookmark);
299 string root_server_id = syncable::GetNullId().GetServerId(); 304 string root_server_id = syncable::GetNullId().GetServerId();
300 CreateUnappliedNewItemWithParent("folder", 305 CreateUnappliedNewItemWithParent("folder",
301 encrypted_bookmark, 306 encrypted_bookmark,
302 root_server_id); 307 root_server_id);
303 CreateUnappliedNewItem("item2", encrypted_bookmark, false); 308 CreateUnappliedNewItem("item2", encrypted_bookmark, false);
304 sync_pb::EntitySpecifics encrypted_password; 309 sync_pb::EntitySpecifics encrypted_password;
305 encrypted_password.MutableExtension(sync_pb::password); 310 encrypted_password.MutableExtension(sync_pb::password);
306 CreateUnappliedNewItem("item3", encrypted_password, false); 311 CreateUnappliedNewItem("item3", encrypted_password, false);
307 312
308 apply_updates_command_.ExecuteImpl(session()); 313 apply_updates_command_.ExecuteImpl(session());
309 314
310 sessions::StatusController* status = session()->status_controller(); 315 sessions::StatusController* status = session()->status_controller();
311 EXPECT_TRUE(status->HasConflictingUpdates()) 316 EXPECT_TRUE(status->HasConflictingUpdates())
312 << "Updates that can't be decrypted should trigger the syncer to have " 317 << "Updates that can't be decrypted should trigger the syncer to have "
313 << "conflicting updates."; 318 << "conflicting updates.";
314 { 319 {
315 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); 320 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_UI);
316 EXPECT_EQ(3, status->update_progress().AppliedUpdatesSize()) 321 EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize())
317 << "All updates should have been attempted"; 322 << "All updates should have been attempted";
318 EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize()) 323 EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize())
319 << "The updates that can't be decrypted should not be in regular " 324 << "The updates that can't be decrypted should not be in regular "
320 << "conflict"; 325 << "conflict";
321 EXPECT_EQ(3, status->conflict_progress().NonblockingConflictingItemsSize()) 326 EXPECT_EQ(2, status->conflict_progress().NonblockingConflictingItemsSize())
322 << "The updates that can't be decrypted should be in nonblocking " 327 << "The updates that can't be decrypted should be in nonblocking "
323 << "conflict"; 328 << "conflict";
324 EXPECT_EQ(0, status->update_progress().SuccessfullyAppliedUpdateCount()) 329 EXPECT_EQ(0, status->update_progress().SuccessfullyAppliedUpdateCount())
330 << "No update that can't be decrypted should be applied";
331 }
332 {
333 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSWORD);
334 EXPECT_EQ(1, status->update_progress().AppliedUpdatesSize())
335 << "All updates should have been attempted";
336 EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize())
337 << "The updates that can't be decrypted should not be in regular "
338 << "conflict";
339 EXPECT_EQ(1, status->conflict_progress().NonblockingConflictingItemsSize())
340 << "The updates that can't be decrypted should be in nonblocking "
341 << "conflict";
342 EXPECT_EQ(0, status->update_progress().SuccessfullyAppliedUpdateCount())
325 << "No update that can't be decrypted should be applied"; 343 << "No update that can't be decrypted should be applied";
326 } 344 }
327 } 345 }
328 346
329 TEST_F(ApplyUpdatesCommandTest, SomeUndecryptablePassword) { 347 TEST_F(ApplyUpdatesCommandTest, SomeUndecryptablePassword) {
330 // Only decryptable password updates should be applied. 348 // Only decryptable password updates should be applied.
331 { 349 {
332 sync_pb::EntitySpecifics specifics; 350 sync_pb::EntitySpecifics specifics;
333 sync_pb::PasswordSpecificsData data; 351 sync_pb::PasswordSpecificsData data;
334 data.set_origin("http://example.com/1"); 352 data.set_origin("http://example.com/1");
(...skipping 27 matching lines...) Expand all
362 CreateUnappliedNewItem("item2", specifics, false); 380 CreateUnappliedNewItem("item2", specifics, false);
363 } 381 }
364 382
365 apply_updates_command_.ExecuteImpl(session()); 383 apply_updates_command_.ExecuteImpl(session());
366 384
367 sessions::StatusController* status = session()->status_controller(); 385 sessions::StatusController* status = session()->status_controller();
368 EXPECT_TRUE(status->HasConflictingUpdates()) 386 EXPECT_TRUE(status->HasConflictingUpdates())
369 << "Updates that can't be decrypted should trigger the syncer to have " 387 << "Updates that can't be decrypted should trigger the syncer to have "
370 << "conflicting updates."; 388 << "conflicting updates.";
371 { 389 {
372 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSIVE); 390 sessions::ScopedModelSafeGroupRestriction r(status, GROUP_PASSWORD);
373 EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize()) 391 EXPECT_EQ(2, status->update_progress().AppliedUpdatesSize())
374 << "All updates should have been attempted"; 392 << "All updates should have been attempted";
375 EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize()) 393 EXPECT_EQ(0, status->conflict_progress().ConflictingItemsSize())
376 << "The updates that can't be decrypted should not be in regular " 394 << "The updates that can't be decrypted should not be in regular "
377 << "conflict"; 395 << "conflict";
378 EXPECT_EQ(1, status->conflict_progress().NonblockingConflictingItemsSize()) 396 EXPECT_EQ(1, status->conflict_progress().NonblockingConflictingItemsSize())
379 << "The updates that can't be decrypted should be in nonblocking " 397 << "The updates that can't be decrypted should be in nonblocking "
380 << "conflict"; 398 << "conflict";
381 EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount()) 399 EXPECT_EQ(1, status->update_progress().SuccessfullyAppliedUpdateCount())
382 << "The undecryptable password update shouldn't be applied"; 400 << "The undecryptable password update shouldn't be applied";
(...skipping 295 matching lines...) Expand 10 before | Expand all | Expand 10 after
678 encrypted_types.insert(syncable::BOOKMARKS); 696 encrypted_types.insert(syncable::BOOKMARKS);
679 EXPECT_EQ(GetAllRealModelTypes(), cryptographer->GetEncryptedTypes()); 697 EXPECT_EQ(GetAllRealModelTypes(), cryptographer->GetEncryptedTypes());
680 698
681 Syncer::UnsyncedMetaHandles handles; 699 Syncer::UnsyncedMetaHandles handles;
682 SyncerUtil::GetUnsyncedEntries(&trans, &handles); 700 SyncerUtil::GetUnsyncedEntries(&trans, &handles);
683 EXPECT_EQ(2*batch_s+1, handles.size()); 701 EXPECT_EQ(2*batch_s+1, handles.size());
684 } 702 }
685 } 703 }
686 704
687 } // namespace browser_sync 705 } // namespace browser_sync
OLDNEW
« no previous file with comments | « no previous file | chrome/browser/sync/engine/download_updates_command_unittest.cc » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698