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

Side by Side Diff: tests/gclient_smoketest.py

Issue 2968005: Add testing for the From(File()) case, fix revinfo. (Closed)
Patch Set: Created 10 years, 5 months 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
« gclient.py ('K') | « tests/fake_repos.py ('k') | no next file » | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
1 #!/usr/bin/python 1 #!/usr/bin/python
2 # Copyright (c) 2010 The Chromium Authors. All rights reserved. 2 # Copyright (c) 2010 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be 3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file. 4 # found in the LICENSE file.
5 5
6 """Smoke tests for gclient.py. 6 """Smoke tests for gclient.py.
7 7
8 Shell out 'gclient' and run basic conformance tests. 8 Shell out 'gclient' and run basic conformance tests.
9 9
10 This test assumes GClientSmokeBase.URL_BASE is valid. 10 This test assumes GClientSmokeBase.URL_BASE is valid.
(...skipping 27 matching lines...) Expand all
38 # Don't use the wrapper script. 38 # Don't use the wrapper script.
39 cmd_base = ['coverage', 'run', '-a', GCLIENT_PATH + '.py'] 39 cmd_base = ['coverage', 'run', '-a', GCLIENT_PATH + '.py']
40 else: 40 else:
41 cmd_base = [GCLIENT_PATH] 41 cmd_base = [GCLIENT_PATH]
42 cmd = cmd_base + cmd 42 cmd = cmd_base + cmd
43 process = subprocess.Popen(cmd, cwd=cwd, env=self.env, 43 process = subprocess.Popen(cmd, cwd=cwd, env=self.env,
44 stdout=subprocess.PIPE, stderr=subprocess.PIPE, 44 stdout=subprocess.PIPE, stderr=subprocess.PIPE,
45 shell=sys.platform.startswith('win')) 45 shell=sys.platform.startswith('win'))
46 (stdout, stderr) = process.communicate() 46 (stdout, stderr) = process.communicate()
47 logging.debug("XXX: %s\n%s\nXXX" % (' '.join(cmd), stdout)) 47 logging.debug("XXX: %s\n%s\nXXX" % (' '.join(cmd), stdout))
48 logging.debug("YYY: %s\n%s\nYYY" % (' '.join(cmd), stderr))
48 return (stdout.replace('\r\n', '\n'), stderr.replace('\r\n', '\n'), 49 return (stdout.replace('\r\n', '\n'), stderr.replace('\r\n', '\n'),
49 process.returncode) 50 process.returncode)
50 51
51 def parseGclient(self, cmd, items): 52 def parseGclient(self, cmd, items):
52 """Parse gclient's output to make it easier to test.""" 53 """Parse gclient's output to make it easier to test."""
53 (stdout, stderr, returncode) = self.gclient(cmd) 54 (stdout, stderr, returncode) = self.gclient(cmd)
54 self.checkString('', stderr) 55 self.checkString('', stderr)
55 self.assertEquals(0, returncode) 56 self.assertEquals(0, returncode)
56 return self.checkBlock(stdout, items) 57 return self.checkBlock(stdout, items)
57 58
(...skipping 35 matching lines...) Expand 10 before | Expand all | Expand 10 after
93 94
94 def checkBlock(self, stdout, items): 95 def checkBlock(self, stdout, items):
95 results = self.splitBlock(stdout) 96 results = self.splitBlock(stdout)
96 for i in xrange(min(len(results), len(items))): 97 for i in xrange(min(len(results), len(items))):
97 if isinstance(items[i], (list, tuple)): 98 if isinstance(items[i], (list, tuple)):
98 verb = items[i][0] 99 verb = items[i][0]
99 path = items[i][1] 100 path = items[i][1]
100 else: 101 else:
101 verb = items[i] 102 verb = items[i]
102 path = self.root_dir 103 path = self.root_dir
103 self.checkString(results[i][0][0], verb) 104 self.checkString(results[i][0][0], verb, (i, results[i][0][0], verb))
104 self.checkString(results[i][0][2], path) 105 self.checkString(results[i][0][2], path, (i, results[i][0][2], path))
105 self.assertEquals(len(results), len(items)) 106 self.assertEquals(len(results), len(items))
106 return results 107 return results
107 108
108 def svnBlockCleanup(self, out): 109 def svnBlockCleanup(self, out):
109 """Work around svn status difference between svn 1.5 and svn 1.6 110 """Work around svn status difference between svn 1.5 and svn 1.6
110 I don't know why but on Windows they are reversed. So sorts the items.""" 111 I don't know why but on Windows they are reversed. So sorts the items."""
111 for i in xrange(len(out)): 112 for i in xrange(len(out)):
112 if len(out[i]) < 2: 113 if len(out[i]) < 2:
113 continue 114 continue
114 out[i] = [out[i][0]] + sorted([x[1:].strip() for x in out[i][1:]]) 115 out[i] = [out[i][0]] + sorted([x[1:].strip() for x in out[i][1:]])
(...skipping 85 matching lines...) Expand 10 before | Expand all | Expand 10 after
200 self.FAKE_REPOS.setUpSVN() 201 self.FAKE_REPOS.setUpSVN()
201 202
202 def testSync(self): 203 def testSync(self):
203 # TODO(maruel): safesync. 204 # TODO(maruel): safesync.
204 self.gclient(['config', self.svn_base + 'trunk/src/']) 205 self.gclient(['config', self.svn_base + 'trunk/src/'])
205 # Test unversioned checkout. 206 # Test unversioned checkout.
206 self.parseGclient(['sync', '--deps', 'mac'], 207 self.parseGclient(['sync', '--deps', 'mac'],
207 ['running', 'running', 208 ['running', 'running',
208 # This is due to the way svn update is called for a 209 # This is due to the way svn update is called for a
209 # single file when File() is used in a DEPS file. 210 # single file when File() is used in a DEPS file.
210 ('running', self.root_dir + '/src/file/foo'), 211 ('running', self.root_dir + '/src/file/other'),
211 'running', 'running', 'running', 'running']) 212 'running', 'running', 'running', 'running'])
212 tree = self.mangle_svn_tree( 213 tree = self.mangle_svn_tree(
213 ('trunk/src@2', 'src'), 214 ('trunk/src@2', 'src'),
214 ('trunk/third_party/foo@1', 'src/third_party/foo'), 215 ('trunk/third_party/foo@1', 'src/third_party/foo'),
215 ('trunk/other@2', 'src/other')) 216 ('trunk/other@2', 'src/other'))
216 tree['src/file/foo/origin'] = 'svn/trunk/third_party/foo@2\n' 217 tree['src/file/other/DEPS'] = (
218 self.FAKE_REPOS.svn_revs[2]['trunk/other/DEPS'])
217 tree['src/svn_hooked1'] = 'svn_hooked1' 219 tree['src/svn_hooked1'] = 'svn_hooked1'
218 self.assertTree(tree) 220 self.assertTree(tree)
219 221
220 # Manually remove svn_hooked1 before synching to make sure it's not 222 # Manually remove svn_hooked1 before synching to make sure it's not
221 # recreated. 223 # recreated.
222 os.remove(join(self.root_dir, 'src', 'svn_hooked1')) 224 os.remove(join(self.root_dir, 'src', 'svn_hooked1'))
223 225
224 # Test incremental versioned sync: sync backward. 226 # Test incremental versioned sync: sync backward.
225 self.parseGclient(['sync', '--revision', 'src@1', '--deps', 'mac', 227 self.parseGclient(['sync', '--revision', 'src@1', '--deps', 'mac',
226 '--delete_unversioned_trees'], 228 '--delete_unversioned_trees'],
227 ['running', 'running', 'running', 'running', 'deleting']) 229 ['running', 'running', 'running', 'running', 'deleting'])
228 tree = self.mangle_svn_tree( 230 tree = self.mangle_svn_tree(
229 ('trunk/src@1', 'src'), 231 ('trunk/src@1', 'src'),
230 ('trunk/third_party/foo@2', 'src/third_party/fpp'), 232 ('trunk/third_party/foo@2', 'src/third_party/fpp'),
231 ('trunk/other@1', 'src/other'), 233 ('trunk/other@1', 'src/other'),
232 ('trunk/third_party/foo@2', 'src/third_party/prout')) 234 ('trunk/third_party/foo@2', 'src/third_party/prout'))
233 tree['src/file/foo/origin'] = 'svn/trunk/third_party/foo@2\n' 235 tree['src/file/other/DEPS'] = (
236 self.FAKE_REPOS.svn_revs[2]['trunk/other/DEPS'])
234 self.assertTree(tree) 237 self.assertTree(tree)
235 # Test incremental sync: delete-unversioned_trees isn't there. 238 # Test incremental sync: delete-unversioned_trees isn't there.
236 self.parseGclient(['sync', '--deps', 'mac'], 239 self.parseGclient(['sync', '--deps', 'mac'],
237 ['running', 'running', 'running', 'running', 'running']) 240 ['running', 'running', 'running', 'running', 'running'])
238 tree = self.mangle_svn_tree( 241 tree = self.mangle_svn_tree(
239 ('trunk/src@2', 'src'), 242 ('trunk/src@2', 'src'),
240 ('trunk/third_party/foo@2', 'src/third_party/fpp'), 243 ('trunk/third_party/foo@2', 'src/third_party/fpp'),
241 ('trunk/third_party/foo@1', 'src/third_party/foo'), 244 ('trunk/third_party/foo@1', 'src/third_party/foo'),
242 ('trunk/other@2', 'src/other'), 245 ('trunk/other@2', 'src/other'),
243 ('trunk/third_party/foo@2', 'src/third_party/prout')) 246 ('trunk/third_party/foo@2', 'src/third_party/prout'))
244 tree['src/file/foo/origin'] = 'svn/trunk/third_party/foo@2\n' 247 tree['src/file/other/DEPS'] = (
248 self.FAKE_REPOS.svn_revs[2]['trunk/other/DEPS'])
245 tree['src/svn_hooked1'] = 'svn_hooked1' 249 tree['src/svn_hooked1'] = 'svn_hooked1'
246 self.assertTree(tree) 250 self.assertTree(tree)
247 251
248 def testSyncIgnoredSolutionName(self): 252 def testSyncIgnoredSolutionName(self):
249 """TODO(maruel): This will become an error soon.""" 253 """TODO(maruel): This will become an error soon."""
250 self.gclient(['config', self.svn_base + 'trunk/src/']) 254 self.gclient(['config', self.svn_base + 'trunk/src/'])
251 results = self.gclient(['sync', '--deps', 'mac', '-r', 'invalid@1']) 255 results = self.gclient(['sync', '--deps', 'mac', '-r', 'invalid@1'])
252 self.checkBlock(results[0], [ 256 self.checkBlock(results[0], [
253 'running', 'running', 257 'running', 'running',
254 # This is due to the way svn update is called for a single file when 258 # This is due to the way svn update is called for a single file when
255 # File() is used in a DEPS file. 259 # File() is used in a DEPS file.
256 ('running', self.root_dir + '/src/file/foo'), 260 ('running', self.root_dir + '/src/file/other'),
257 'running', 'running', 'running', 'running']) 261 'running', 'running', 'running', 'running'])
258 self.checkString('Please fix your script, having invalid --revision flags ' 262 self.checkString('Please fix your script, having invalid --revision flags '
259 'will soon considered an error.\n', results[1]) 263 'will soon considered an error.\n', results[1])
260 self.assertEquals(0, results[2]) 264 self.assertEquals(0, results[2])
261 tree = self.mangle_svn_tree( 265 tree = self.mangle_svn_tree(
262 ('trunk/src@2', 'src'), 266 ('trunk/src@2', 'src'),
263 ('trunk/third_party/foo@1', 'src/third_party/foo'), 267 ('trunk/third_party/foo@1', 'src/third_party/foo'),
264 ('trunk/other@2', 'src/other')) 268 ('trunk/other@2', 'src/other'))
265 tree['src/file/foo/origin'] = 'svn/trunk/third_party/foo@2\n' 269 tree['src/file/other/DEPS'] = (
270 self.FAKE_REPOS.svn_revs[2]['trunk/other/DEPS'])
266 tree['src/svn_hooked1'] = 'svn_hooked1' 271 tree['src/svn_hooked1'] = 'svn_hooked1'
267 self.assertTree(tree) 272 self.assertTree(tree)
268 273
269 def testSyncNoSolutionName(self): 274 def testSyncNoSolutionName(self):
270 # When no solution name is provided, gclient uses the first solution listed. 275 # When no solution name is provided, gclient uses the first solution listed.
271 self.gclient(['config', self.svn_base + 'trunk/src/']) 276 self.gclient(['config', self.svn_base + 'trunk/src/'])
272 self.parseGclient(['sync', '--deps', 'mac', '-r', '1'], 277 self.parseGclient(['sync', '--deps', 'mac', '-r', '1'],
273 ['running', 'running', 'running', 'running']) 278 ['running', 'running', 'running', 'running'])
274 tree = self.mangle_svn_tree( 279 tree = self.mangle_svn_tree(
275 ('trunk/src@1', 'src'), 280 ('trunk/src@1', 'src'),
(...skipping 26 matching lines...) Expand all
302 out = self.splitBlock(results[0]) 307 out = self.splitBlock(results[0])
303 # src, src/other is missing, src/other, src/third_party/foo is missing, 308 # src, src/other is missing, src/other, src/third_party/foo is missing,
304 # src/third_party/foo, 2 svn hooks, 3 related to File(). 309 # src/third_party/foo, 2 svn hooks, 3 related to File().
305 self.assertEquals(10, len(out)) 310 self.assertEquals(10, len(out))
306 self.checkString('', results[1]) 311 self.checkString('', results[1])
307 self.assertEquals(0, results[2]) 312 self.assertEquals(0, results[2])
308 tree = self.mangle_svn_tree( 313 tree = self.mangle_svn_tree(
309 ('trunk/src@2', 'src'), 314 ('trunk/src@2', 'src'),
310 ('trunk/third_party/foo@1', 'src/third_party/foo'), 315 ('trunk/third_party/foo@1', 'src/third_party/foo'),
311 ('trunk/other@2', 'src/other')) 316 ('trunk/other@2', 'src/other'))
312 tree['src/file/foo/origin'] = 'svn/trunk/third_party/foo@2\n' 317 tree['src/file/other/DEPS'] = (
318 self.FAKE_REPOS.svn_revs[2]['trunk/other/DEPS'])
313 tree['src/svn_hooked1'] = 'svn_hooked1' 319 tree['src/svn_hooked1'] = 'svn_hooked1'
314 tree['src/svn_hooked2'] = 'svn_hooked2' 320 tree['src/svn_hooked2'] = 'svn_hooked2'
315 self.assertTree(tree) 321 self.assertTree(tree)
316 322
317 out = self.parseGclient(['status', '--deps', 'mac'], 323 out = self.parseGclient(['status', '--deps', 'mac'],
318 [['running', join(self.root_dir, 'src')]]) 324 [['running', join(self.root_dir, 'src')]])
319 out = self.svnBlockCleanup(out) 325 out = self.svnBlockCleanup(out)
320 self.checkString('file', out[0][1]) 326 self.checkString('file', out[0][1])
321 self.checkString('other', out[0][2]) 327 self.checkString('other', out[0][2])
322 self.checkString('svn_hooked1', out[0][3]) 328 self.checkString('svn_hooked1', out[0][3])
(...skipping 71 matching lines...) Expand 10 before | Expand all | Expand 10 after
394 self.gclient(['config', self.svn_base + 'trunk/src/']) 400 self.gclient(['config', self.svn_base + 'trunk/src/'])
395 self.gclient(['sync', '--deps', 'mac', '--revision', 'src@1']) 401 self.gclient(['sync', '--deps', 'mac', '--revision', 'src@1'])
396 out = self.parseGclient(['runhooks', '--deps', 'mac'], []) 402 out = self.parseGclient(['runhooks', '--deps', 'mac'], [])
397 self.assertEquals([], out) 403 self.assertEquals([], out)
398 404
399 def testRevInfo(self): 405 def testRevInfo(self):
400 self.gclient(['config', self.svn_base + 'trunk/src/']) 406 self.gclient(['config', self.svn_base + 'trunk/src/'])
401 self.gclient(['sync', '--deps', 'mac']) 407 self.gclient(['sync', '--deps', 'mac'])
402 results = self.gclient(['revinfo', '--deps', 'mac']) 408 results = self.gclient(['revinfo', '--deps', 'mac'])
403 out = ('src: %(base)s/src@2;\n' 409 out = ('src: %(base)s/src@2;\n'
410 'src/file/other: %(base)s/other/DEPS@2;\n'
404 'src/other: %(base)s/other@2;\n' 411 'src/other: %(base)s/other@2;\n'
405 'src/third_party/foo: %(base)s/third_party/foo@1\n' % 412 'src/third_party/foo: %(base)s/third_party/foo@1\n' %
406 { 'base': self.svn_base + 'trunk' }) 413 { 'base': self.svn_base + 'trunk' })
407 self.check((out, '', 0), results) 414 self.check((out, '', 0), results)
408 415
409 416
410 class GClientSmokeGIT(GClientSmokeBase): 417 class GClientSmokeGIT(GClientSmokeBase):
411 def setUp(self): 418 def setUp(self):
412 GClientSmokeBase.setUp(self) 419 GClientSmokeBase.setUp(self)
413 self.enabled = self.FAKE_REPOS.setUpGIT() 420 self.enabled = self.FAKE_REPOS.setUpGIT()
(...skipping 191 matching lines...) Expand 10 before | Expand all | Expand 10 after
605 '{"name": "src",' 612 '{"name": "src",'
606 ' "url": "' + self.svn_base + 'trunk/src/"},' 613 ' "url": "' + self.svn_base + 'trunk/src/"},'
607 '{"name": "src-git",' 614 '{"name": "src-git",'
608 '"url": "' + self.git_base + 'repo_1"}]']) 615 '"url": "' + self.git_base + 'repo_1"}]'])
609 results = self.gclient(['sync', '--deps', 'mac']) 616 results = self.gclient(['sync', '--deps', 'mac'])
610 # 3x svn checkout, 3x run hooks 617 # 3x svn checkout, 3x run hooks
611 self.checkBlock(results[0], 618 self.checkBlock(results[0],
612 ['running', 'running', 619 ['running', 'running',
613 # This is due to the way svn update is called for a single 620 # This is due to the way svn update is called for a single
614 # file when File() is used in a DEPS file. 621 # file when File() is used in a DEPS file.
615 ('running', self.root_dir + '/src/file/foo'), 622 ('running', self.root_dir + '/src/file/other'),
616 'running', 'running', 'running', 'running', 'running', 623 'running', 'running', 'running', 'running', 'running',
617 'running', 'running']) 624 'running', 'running'])
618 # TODO(maruel): Something's wrong here. git outputs to stderr 'Switched to 625 # TODO(maruel): Something's wrong here. git outputs to stderr 'Switched to
619 # new branch \'hash\''. 626 # new branch \'hash\''.
620 #self.checkString('', results[1]) 627 #self.checkString('', results[1])
621 self.assertEquals(0, results[2]) 628 self.assertEquals(0, results[2])
622 tree = self.mangle_git_tree(('repo_1@2', 'src-git'), 629 tree = self.mangle_git_tree(('repo_1@2', 'src-git'),
623 ('repo_2@1', 'src/repo2'), 630 ('repo_2@1', 'src/repo2'),
624 ('repo_3@2', 'src/repo2/repo_renamed')) 631 ('repo_3@2', 'src/repo2/repo_renamed'))
625 tree.update(self.mangle_svn_tree( 632 tree.update(self.mangle_svn_tree(
626 ('trunk/src@2', 'src'), 633 ('trunk/src@2', 'src'),
627 ('trunk/third_party/foo@1', 'src/third_party/foo'), 634 ('trunk/third_party/foo@1', 'src/third_party/foo'),
628 ('trunk/other@2', 'src/other'))) 635 ('trunk/other@2', 'src/other')))
629 tree['src/file/foo/origin'] = 'svn/trunk/third_party/foo@2\n' 636 tree['src/file/other/DEPS'] = (
637 self.FAKE_REPOS.svn_revs[2]['trunk/other/DEPS'])
630 tree['src/git_hooked1'] = 'git_hooked1' 638 tree['src/git_hooked1'] = 'git_hooked1'
631 tree['src/git_hooked2'] = 'git_hooked2' 639 tree['src/git_hooked2'] = 'git_hooked2'
632 tree['src/svn_hooked1'] = 'svn_hooked1' 640 tree['src/svn_hooked1'] = 'svn_hooked1'
633 tree['src/svn_hooked2'] = 'svn_hooked2' 641 tree['src/svn_hooked2'] = 'svn_hooked2'
634 self.assertTree(tree) 642 self.assertTree(tree)
635 643
636 def testMultiSolutionsMultiRev(self): 644 def testMultiSolutionsMultiRev(self):
637 if not self.enabled: 645 if not self.enabled:
638 return 646 return
639 self.gclient(['config', '--spec', 647 self.gclient(['config', '--spec',
(...skipping 26 matching lines...) Expand all
666 self.gclient(['config', '--spec', 674 self.gclient(['config', '--spec',
667 'solutions=[' 675 'solutions=['
668 '{"name": "src",' 676 '{"name": "src",'
669 ' "url": "' + self.svn_base + 'trunk/src/"},' 677 ' "url": "' + self.svn_base + 'trunk/src/"},'
670 '{"name": "src-git",' 678 '{"name": "src-git",'
671 '"url": "' + self.git_base + 'repo_1"}]']) 679 '"url": "' + self.git_base + 'repo_1"}]'])
672 self.gclient(['sync', '--deps', 'mac']) 680 self.gclient(['sync', '--deps', 'mac'])
673 results = self.gclient(['revinfo', '--deps', 'mac']) 681 results = self.gclient(['revinfo', '--deps', 'mac'])
674 out = ('src: %(svn_base)s/src/@2;\n' 682 out = ('src: %(svn_base)s/src/@2;\n'
675 'src-git: %(git_base)srepo_1@%(hash1)s;\n' 683 'src-git: %(git_base)srepo_1@%(hash1)s;\n'
684 'src/file/other: %(svn_base)s/other/DEPS@2;\n'
676 'src/other: %(svn_base)s/other@2;\n' 685 'src/other: %(svn_base)s/other@2;\n'
677 'src/repo2: %(git_base)srepo_2@%(hash2)s;\n' 686 'src/repo2: %(git_base)srepo_2@%(hash2)s;\n'
678 'src/repo2/repo_renamed: %(git_base)srepo_3@%(hash3)s;\n' 687 'src/repo2/repo_renamed: %(git_base)srepo_3@%(hash3)s;\n'
679 'src/third_party/foo: %(svn_base)s/third_party/foo@1\n') % { 688 'src/third_party/foo: %(svn_base)s/third_party/foo@1\n') % {
680 'svn_base': self.svn_base + 'trunk', 689 'svn_base': self.svn_base + 'trunk',
681 'git_base': self.git_base, 690 'git_base': self.git_base,
682 'hash1': self.githash('repo_1', 2), 691 'hash1': self.githash('repo_1', 2),
683 'hash2': self.githash('repo_2', 1), 692 'hash2': self.githash('repo_2', 1),
684 'hash3': self.githash('repo_3', 2), 693 'hash3': self.githash('repo_3', 2),
685 } 694 }
(...skipping 34 matching lines...) Expand 10 before | Expand all | Expand 10 after
720 if __name__ == '__main__': 729 if __name__ == '__main__':
721 if '-c' in sys.argv: 730 if '-c' in sys.argv:
722 COVERAGE = True 731 COVERAGE = True
723 sys.argv.remove('-c') 732 sys.argv.remove('-c')
724 if os.path.exists('.coverage'): 733 if os.path.exists('.coverage'):
725 os.remove('.coverage') 734 os.remove('.coverage')
726 os.environ['COVERAGE_FILE'] = os.path.join( 735 os.environ['COVERAGE_FILE'] = os.path.join(
727 os.path.dirname(os.path.dirname(os.path.abspath(__file__))), 736 os.path.dirname(os.path.dirname(os.path.abspath(__file__))),
728 '.coverage') 737 '.coverage')
729 unittest.main() 738 unittest.main()
OLDNEW
« gclient.py ('K') | « tests/fake_repos.py ('k') | no next file » | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698