blob: d7e5800f324b81824264d7f20caf6228544b435b [file] [log] [blame]
Mike Frysingerf6013762019-06-13 02:30:51 -04001# -*- coding:utf-8 -*-
Dan Willemsen745b4ad2015-10-06 15:23:19 -07002#
3# Copyright (C) 2015 The Android Open Source Project
4#
5# Licensed under the Apache License, Version 2.0 (the "License");
6# you may not use this file except in compliance with the License.
7# You may obtain a copy of the License at
8#
9# http://www.apache.org/licenses/LICENSE-2.0
10#
11# Unless required by applicable law or agreed to in writing, software
12# distributed under the License is distributed on an "AS IS" BASIS,
13# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14# See the License for the specific language governing permissions and
15# limitations under the License.
16
Mike Frysinger87deaef2019-07-26 21:14:55 -040017"""Unittests for the wrapper.py module."""
18
19from __future__ import print_function
20
Mike Frysinger3599cc32020-02-29 02:53:41 -050021import contextlib
Dan Willemsen745b4ad2015-10-06 15:23:19 -070022import os
Mike Frysinger84094102020-02-11 02:10:28 -050023import re
Mike Frysingercfc81112020-02-29 02:56:32 -050024import shutil
25import tempfile
Dan Willemsen745b4ad2015-10-06 15:23:19 -070026import unittest
27
Fredrik de Groot6342d562020-12-01 15:58:53 +010028import git_command
Mike Frysinger1379a9b2021-01-04 23:29:45 -050029import main
Mike Frysinger3599cc32020-02-29 02:53:41 -050030import platform_utils
Mike Frysinger84094102020-02-11 02:10:28 -050031from pyversion import is_python3
Dan Willemsen745b4ad2015-10-06 15:23:19 -070032import wrapper
33
David Pursehouse819827a2020-02-12 15:20:19 +090034
Mike Frysinger8ddff5c2020-02-09 15:00:25 -050035if is_python3():
36 from unittest import mock
37 from io import StringIO
38else:
39 import mock
40 from StringIO import StringIO
41
42
Mike Frysinger3599cc32020-02-29 02:53:41 -050043@contextlib.contextmanager
44def TemporaryDirectory():
45 """Create a new empty git checkout for testing."""
46 # TODO(vapier): Convert this to tempfile.TemporaryDirectory once we drop
47 # Python 2 support entirely.
48 try:
49 tempdir = tempfile.mkdtemp(prefix='repo-tests')
50 yield tempdir
51 finally:
52 platform_utils.rmtree(tempdir)
53
54
Dan Willemsen745b4ad2015-10-06 15:23:19 -070055def fixture(*paths):
56 """Return a path relative to tests/fixtures.
57 """
58 return os.path.join(os.path.dirname(__file__), 'fixtures', *paths)
59
David Pursehouse819827a2020-02-12 15:20:19 +090060
Mike Frysinger84094102020-02-11 02:10:28 -050061class RepoWrapperTestCase(unittest.TestCase):
62 """TestCase for the wrapper module."""
David Pursehouse819827a2020-02-12 15:20:19 +090063
Dan Willemsen745b4ad2015-10-06 15:23:19 -070064 def setUp(self):
Mike Frysinger84094102020-02-11 02:10:28 -050065 """Load the wrapper module every time."""
Dan Willemsen745b4ad2015-10-06 15:23:19 -070066 wrapper._wrapper_module = None
67 self.wrapper = wrapper.Wrapper()
68
Mike Frysinger84094102020-02-11 02:10:28 -050069 if not is_python3():
70 self.assertRegex = self.assertRegexpMatches
71
72
73class RepoWrapperUnitTest(RepoWrapperTestCase):
74 """Tests helper functions in the repo wrapper
75 """
76
Mike Frysinger8ddff5c2020-02-09 15:00:25 -050077 def test_version(self):
78 """Make sure _Version works."""
79 with self.assertRaises(SystemExit) as e:
80 with mock.patch('sys.stdout', new_callable=StringIO) as stdout:
81 with mock.patch('sys.stderr', new_callable=StringIO) as stderr:
82 self.wrapper._Version()
83 self.assertEqual(0, e.exception.code)
84 self.assertEqual('', stderr.getvalue())
85 self.assertIn('repo launcher version', stdout.getvalue())
86
Mike Frysinger1379a9b2021-01-04 23:29:45 -050087 def test_python_constraints(self):
88 """The launcher should never require newer than main.py."""
89 self.assertGreaterEqual(main.MIN_PYTHON_VERSION_HARD,
90 wrapper.MIN_PYTHON_VERSION_HARD)
91 self.assertGreaterEqual(main.MIN_PYTHON_VERSION_SOFT,
92 wrapper.MIN_PYTHON_VERSION_SOFT)
93 # Make sure the versions are themselves in sync.
94 self.assertGreaterEqual(wrapper.MIN_PYTHON_VERSION_SOFT,
95 wrapper.MIN_PYTHON_VERSION_HARD)
96
Mike Frysingerd8fda902020-02-14 00:24:38 -050097 def test_init_parser(self):
98 """Make sure 'init' GetParser works."""
99 parser = self.wrapper.GetParser(gitc_init=False)
100 opts, args = parser.parse_args([])
101 self.assertEqual([], args)
102 self.assertIsNone(opts.manifest_url)
103
104 def test_gitc_init_parser(self):
105 """Make sure 'gitc-init' GetParser works."""
106 parser = self.wrapper.GetParser(gitc_init=True)
107 opts, args = parser.parse_args([])
108 self.assertEqual([], args)
109 self.assertIsNone(opts.manifest_file)
110
Dan Willemsen745b4ad2015-10-06 15:23:19 -0700111 def test_get_gitc_manifest_dir_no_gitc(self):
112 """
113 Test reading a missing gitc config file
114 """
115 self.wrapper.GITC_CONFIG_FILE = fixture('missing_gitc_config')
116 val = self.wrapper.get_gitc_manifest_dir()
117 self.assertEqual(val, '')
118
119 def test_get_gitc_manifest_dir(self):
120 """
121 Test reading the gitc config file and parsing the directory
122 """
123 self.wrapper.GITC_CONFIG_FILE = fixture('gitc_config')
124 val = self.wrapper.get_gitc_manifest_dir()
125 self.assertEqual(val, '/test/usr/local/google/gitc')
126
127 def test_gitc_parse_clientdir_no_gitc(self):
128 """
129 Test parsing the gitc clientdir without gitc running
130 """
131 self.wrapper.GITC_CONFIG_FILE = fixture('missing_gitc_config')
132 self.assertEqual(self.wrapper.gitc_parse_clientdir('/something'), None)
133 self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test'), 'test')
134
135 def test_gitc_parse_clientdir(self):
136 """
137 Test parsing the gitc clientdir
138 """
139 self.wrapper.GITC_CONFIG_FILE = fixture('gitc_config')
140 self.assertEqual(self.wrapper.gitc_parse_clientdir('/something'), None)
141 self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test'), 'test')
142 self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test/'), 'test')
143 self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/test/extra'), 'test')
144 self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test'), 'test')
145 self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test/'), 'test')
David Pursehouse3cda50a2020-02-13 13:17:03 +0900146 self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/test/extra'),
147 'test')
Dan Willemsen745b4ad2015-10-06 15:23:19 -0700148 self.assertEqual(self.wrapper.gitc_parse_clientdir('/gitc/manifest-rw/'), None)
149 self.assertEqual(self.wrapper.gitc_parse_clientdir('/test/usr/local/google/gitc/'), None)
150
David Pursehouse819827a2020-02-12 15:20:19 +0900151
Mike Frysinger84094102020-02-11 02:10:28 -0500152class SetGitTrace2ParentSid(RepoWrapperTestCase):
153 """Check SetGitTrace2ParentSid behavior."""
154
155 KEY = 'GIT_TRACE2_PARENT_SID'
156 VALID_FORMAT = re.compile(r'^repo-[0-9]{8}T[0-9]{6}Z-P[0-9a-f]{8}$')
157
158 def test_first_set(self):
159 """Test env var not yet set."""
160 env = {}
161 self.wrapper.SetGitTrace2ParentSid(env)
162 self.assertIn(self.KEY, env)
163 value = env[self.KEY]
164 self.assertRegex(value, self.VALID_FORMAT)
165
166 def test_append(self):
167 """Test env var is appended."""
168 env = {self.KEY: 'pfx'}
169 self.wrapper.SetGitTrace2ParentSid(env)
170 self.assertIn(self.KEY, env)
171 value = env[self.KEY]
172 self.assertTrue(value.startswith('pfx/'))
173 self.assertRegex(value[4:], self.VALID_FORMAT)
174
175 def test_global_context(self):
176 """Check os.environ gets updated by default."""
177 os.environ.pop(self.KEY, None)
178 self.wrapper.SetGitTrace2ParentSid()
179 self.assertIn(self.KEY, os.environ)
180 value = os.environ[self.KEY]
181 self.assertRegex(value, self.VALID_FORMAT)
182
183
Mike Frysinger587f1622020-03-23 16:49:11 -0400184class RunCommand(RepoWrapperTestCase):
185 """Check run_command behavior."""
186
187 def test_capture(self):
188 """Check capture_output handling."""
189 ret = self.wrapper.run_command(['echo', 'hi'], capture_output=True)
190 self.assertEqual(ret.stdout, 'hi\n')
191
192 def test_check(self):
193 """Check check handling."""
194 self.wrapper.run_command(['true'], check=False)
195 self.wrapper.run_command(['true'], check=True)
196 self.wrapper.run_command(['false'], check=False)
197 with self.assertRaises(self.wrapper.RunError):
198 self.wrapper.run_command(['false'], check=True)
199
200
201class RunGit(RepoWrapperTestCase):
202 """Check run_git behavior."""
203
204 def test_capture(self):
205 """Check capture_output handling."""
206 ret = self.wrapper.run_git('--version')
207 self.assertIn('git', ret.stdout)
208
209 def test_check(self):
210 """Check check handling."""
211 with self.assertRaises(self.wrapper.CloneFailure):
212 self.wrapper.run_git('--version-asdfasdf')
213 self.wrapper.run_git('--version-asdfasdf', check=False)
214
215
216class ParseGitVersion(RepoWrapperTestCase):
217 """Check ParseGitVersion behavior."""
218
219 def test_autoload(self):
220 """Check we can load the version from the live git."""
221 ret = self.wrapper.ParseGitVersion()
222 self.assertIsNotNone(ret)
223
224 def test_bad_ver(self):
225 """Check handling of bad git versions."""
226 ret = self.wrapper.ParseGitVersion(ver_str='asdf')
227 self.assertIsNone(ret)
228
229 def test_normal_ver(self):
230 """Check handling of normal git versions."""
231 ret = self.wrapper.ParseGitVersion(ver_str='git version 2.25.1')
232 self.assertEqual(2, ret.major)
233 self.assertEqual(25, ret.minor)
234 self.assertEqual(1, ret.micro)
235 self.assertEqual('2.25.1', ret.full)
236
237 def test_extended_ver(self):
238 """Check handling of extended distro git versions."""
239 ret = self.wrapper.ParseGitVersion(
240 ver_str='git version 1.30.50.696.g5e7596f4ac-goog')
241 self.assertEqual(1, ret.major)
242 self.assertEqual(30, ret.minor)
243 self.assertEqual(50, ret.micro)
244 self.assertEqual('1.30.50.696.g5e7596f4ac-goog', ret.full)
245
246
247class CheckGitVersion(RepoWrapperTestCase):
248 """Check _CheckGitVersion behavior."""
249
250 def test_unknown(self):
251 """Unknown versions should abort."""
252 with mock.patch.object(self.wrapper, 'ParseGitVersion', return_value=None):
253 with self.assertRaises(self.wrapper.CloneFailure):
254 self.wrapper._CheckGitVersion()
255
256 def test_old(self):
257 """Old versions should abort."""
258 with mock.patch.object(
259 self.wrapper, 'ParseGitVersion',
260 return_value=self.wrapper.GitVersion(1, 0, 0, '1.0.0')):
261 with self.assertRaises(self.wrapper.CloneFailure):
262 self.wrapper._CheckGitVersion()
263
264 def test_new(self):
265 """Newer versions should run fine."""
266 with mock.patch.object(
267 self.wrapper, 'ParseGitVersion',
268 return_value=self.wrapper.GitVersion(100, 0, 0, '100.0.0')):
269 self.wrapper._CheckGitVersion()
270
271
Mike Frysinger3599cc32020-02-29 02:53:41 -0500272class NeedSetupGnuPG(RepoWrapperTestCase):
273 """Check NeedSetupGnuPG behavior."""
274
275 def test_missing_dir(self):
276 """The ~/.repoconfig tree doesn't exist yet."""
277 with TemporaryDirectory() as tempdir:
278 self.wrapper.home_dot_repo = os.path.join(tempdir, 'foo')
279 self.assertTrue(self.wrapper.NeedSetupGnuPG())
280
281 def test_missing_keyring(self):
282 """The keyring-version file doesn't exist yet."""
283 with TemporaryDirectory() as tempdir:
284 self.wrapper.home_dot_repo = tempdir
285 self.assertTrue(self.wrapper.NeedSetupGnuPG())
286
287 def test_empty_keyring(self):
288 """The keyring-version file exists, but is empty."""
289 with TemporaryDirectory() as tempdir:
290 self.wrapper.home_dot_repo = tempdir
291 with open(os.path.join(tempdir, 'keyring-version'), 'w'):
292 pass
293 self.assertTrue(self.wrapper.NeedSetupGnuPG())
294
295 def test_old_keyring(self):
296 """The keyring-version file exists, but it's old."""
297 with TemporaryDirectory() as tempdir:
298 self.wrapper.home_dot_repo = tempdir
299 with open(os.path.join(tempdir, 'keyring-version'), 'w') as fp:
300 fp.write('1.0\n')
301 self.assertTrue(self.wrapper.NeedSetupGnuPG())
302
303 def test_new_keyring(self):
304 """The keyring-version file exists, and is up-to-date."""
305 with TemporaryDirectory() as tempdir:
306 self.wrapper.home_dot_repo = tempdir
307 with open(os.path.join(tempdir, 'keyring-version'), 'w') as fp:
308 fp.write('1000.0\n')
309 self.assertFalse(self.wrapper.NeedSetupGnuPG())
310
311
312class SetupGnuPG(RepoWrapperTestCase):
313 """Check SetupGnuPG behavior."""
314
315 def test_full(self):
316 """Make sure it works completely."""
317 with TemporaryDirectory() as tempdir:
318 self.wrapper.home_dot_repo = tempdir
Marcos Marado2735bfc2020-04-09 19:44:28 +0100319 self.wrapper.gpg_dir = os.path.join(self.wrapper.home_dot_repo, 'gnupg')
Mike Frysinger3599cc32020-02-29 02:53:41 -0500320 self.assertTrue(self.wrapper.SetupGnuPG(True))
321 with open(os.path.join(tempdir, 'keyring-version'), 'r') as fp:
322 data = fp.read()
323 self.assertEqual('.'.join(str(x) for x in self.wrapper.KEYRING_VERSION),
324 data.strip())
325
326
327class VerifyRev(RepoWrapperTestCase):
328 """Check verify_rev behavior."""
329
330 def test_verify_passes(self):
331 """Check when we have a valid signed tag."""
332 desc_result = self.wrapper.RunResult(0, 'v1.0\n', '')
333 gpg_result = self.wrapper.RunResult(0, '', '')
334 with mock.patch.object(self.wrapper, 'run_git',
335 side_effect=(desc_result, gpg_result)):
336 ret = self.wrapper.verify_rev('/', 'refs/heads/stable', '1234', True)
337 self.assertEqual('v1.0^0', ret)
338
339 def test_unsigned_commit(self):
340 """Check we fall back to signed tag when we have an unsigned commit."""
341 desc_result = self.wrapper.RunResult(0, 'v1.0-10-g1234\n', '')
342 gpg_result = self.wrapper.RunResult(0, '', '')
343 with mock.patch.object(self.wrapper, 'run_git',
344 side_effect=(desc_result, gpg_result)):
345 ret = self.wrapper.verify_rev('/', 'refs/heads/stable', '1234', True)
346 self.assertEqual('v1.0^0', ret)
347
348 def test_verify_fails(self):
349 """Check we fall back to signed tag when we have an unsigned commit."""
350 desc_result = self.wrapper.RunResult(0, 'v1.0-10-g1234\n', '')
351 gpg_result = Exception
352 with mock.patch.object(self.wrapper, 'run_git',
353 side_effect=(desc_result, gpg_result)):
354 with self.assertRaises(Exception):
355 self.wrapper.verify_rev('/', 'refs/heads/stable', '1234', True)
356
357
358class GitCheckoutTestCase(RepoWrapperTestCase):
359 """Tests that use a real/small git checkout."""
Mike Frysingercfc81112020-02-29 02:56:32 -0500360
361 GIT_DIR = None
362 REV_LIST = None
363
364 @classmethod
365 def setUpClass(cls):
366 # Create a repo to operate on, but do it once per-class.
367 cls.GIT_DIR = tempfile.mkdtemp(prefix='repo-rev-tests')
368 run_git = wrapper.Wrapper().run_git
369
370 remote = os.path.join(cls.GIT_DIR, 'remote')
371 os.mkdir(remote)
Fredrik de Groot6342d562020-12-01 15:58:53 +0100372
373 # Tests need to assume, that main is default branch at init,
374 # which is not supported in config until 2.28.
375 if git_command.git_require((2, 28, 0)):
376 initstr = '--initial-branch=main'
377 else:
378 # Use template dir for init.
379 templatedir = tempfile.mkdtemp(prefix='.test-template')
380 with open(os.path.join(templatedir, 'HEAD'), 'w') as fp:
381 fp.write('ref: refs/heads/main\n')
382 initstr = '--template=' + templatedir
383
384 run_git('init', initstr, cwd=remote)
Mike Frysingercfc81112020-02-29 02:56:32 -0500385 run_git('commit', '--allow-empty', '-minit', cwd=remote)
386 run_git('branch', 'stable', cwd=remote)
387 run_git('tag', 'v1.0', cwd=remote)
388 run_git('commit', '--allow-empty', '-m2nd commit', cwd=remote)
389 cls.REV_LIST = run_git('rev-list', 'HEAD', cwd=remote).stdout.splitlines()
390
391 run_git('init', cwd=cls.GIT_DIR)
392 run_git('fetch', remote, '+refs/heads/*:refs/remotes/origin/*', cwd=cls.GIT_DIR)
393
394 @classmethod
395 def tearDownClass(cls):
396 if not cls.GIT_DIR:
397 return
398
399 shutil.rmtree(cls.GIT_DIR)
400
Mike Frysinger3599cc32020-02-29 02:53:41 -0500401
402class ResolveRepoRev(GitCheckoutTestCase):
403 """Check resolve_repo_rev behavior."""
404
Mike Frysingercfc81112020-02-29 02:56:32 -0500405 def test_explicit_branch(self):
406 """Check refs/heads/branch argument."""
407 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/heads/stable')
408 self.assertEqual('refs/heads/stable', rrev)
409 self.assertEqual(self.REV_LIST[1], lrev)
410
411 with self.assertRaises(wrapper.CloneFailure):
412 self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/heads/unknown')
413
414 def test_explicit_tag(self):
415 """Check refs/tags/tag argument."""
416 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/tags/v1.0')
417 self.assertEqual('refs/tags/v1.0', rrev)
418 self.assertEqual(self.REV_LIST[1], lrev)
419
420 with self.assertRaises(wrapper.CloneFailure):
421 self.wrapper.resolve_repo_rev(self.GIT_DIR, 'refs/tags/unknown')
422
423 def test_branch_name(self):
424 """Check branch argument."""
425 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'stable')
426 self.assertEqual('refs/heads/stable', rrev)
427 self.assertEqual(self.REV_LIST[1], lrev)
428
Mike Frysingere283b952020-11-16 22:56:35 -0500429 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'main')
430 self.assertEqual('refs/heads/main', rrev)
Mike Frysingercfc81112020-02-29 02:56:32 -0500431 self.assertEqual(self.REV_LIST[0], lrev)
432
433 def test_tag_name(self):
434 """Check tag argument."""
435 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, 'v1.0')
436 self.assertEqual('refs/tags/v1.0', rrev)
437 self.assertEqual(self.REV_LIST[1], lrev)
438
439 def test_full_commit(self):
440 """Check specific commit argument."""
441 commit = self.REV_LIST[0]
442 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, commit)
443 self.assertEqual(commit, rrev)
444 self.assertEqual(commit, lrev)
445
446 def test_partial_commit(self):
447 """Check specific (partial) commit argument."""
448 commit = self.REV_LIST[0][0:20]
449 rrev, lrev = self.wrapper.resolve_repo_rev(self.GIT_DIR, commit)
450 self.assertEqual(self.REV_LIST[0], rrev)
451 self.assertEqual(self.REV_LIST[0], lrev)
452
453 def test_unknown(self):
454 """Check unknown ref/commit argument."""
455 with self.assertRaises(wrapper.CloneFailure):
456 self.wrapper.resolve_repo_rev(self.GIT_DIR, 'boooooooya')
457
458
Mike Frysinger3599cc32020-02-29 02:53:41 -0500459class CheckRepoVerify(RepoWrapperTestCase):
460 """Check check_repo_verify behavior."""
461
462 def test_no_verify(self):
463 """Always fail with --no-repo-verify."""
464 self.assertFalse(self.wrapper.check_repo_verify(False))
465
466 def test_gpg_initialized(self):
467 """Should pass if gpg is setup already."""
468 with mock.patch.object(self.wrapper, 'NeedSetupGnuPG', return_value=False):
469 self.assertTrue(self.wrapper.check_repo_verify(True))
470
471 def test_need_gpg_setup(self):
472 """Should pass/fail based on gpg setup."""
473 with mock.patch.object(self.wrapper, 'NeedSetupGnuPG', return_value=True):
474 with mock.patch.object(self.wrapper, 'SetupGnuPG') as m:
475 m.return_value = True
476 self.assertTrue(self.wrapper.check_repo_verify(True))
477
478 m.return_value = False
479 self.assertFalse(self.wrapper.check_repo_verify(True))
480
481
482class CheckRepoRev(GitCheckoutTestCase):
483 """Check check_repo_rev behavior."""
484
485 def test_verify_works(self):
486 """Should pass when verification passes."""
487 with mock.patch.object(self.wrapper, 'check_repo_verify', return_value=True):
488 with mock.patch.object(self.wrapper, 'verify_rev', return_value='12345'):
489 rrev, lrev = self.wrapper.check_repo_rev(self.GIT_DIR, 'stable')
490 self.assertEqual('refs/heads/stable', rrev)
491 self.assertEqual('12345', lrev)
492
493 def test_verify_fails(self):
494 """Should fail when verification fails."""
495 with mock.patch.object(self.wrapper, 'check_repo_verify', return_value=True):
496 with mock.patch.object(self.wrapper, 'verify_rev', side_effect=Exception):
497 with self.assertRaises(Exception):
498 self.wrapper.check_repo_rev(self.GIT_DIR, 'stable')
499
500 def test_verify_ignore(self):
501 """Should pass when verification is disabled."""
502 with mock.patch.object(self.wrapper, 'verify_rev', side_effect=Exception):
503 rrev, lrev = self.wrapper.check_repo_rev(self.GIT_DIR, 'stable', repo_verify=False)
504 self.assertEqual('refs/heads/stable', rrev)
505 self.assertEqual(self.REV_LIST[1], lrev)
506
507
Dan Willemsen745b4ad2015-10-06 15:23:19 -0700508if __name__ == '__main__':
509 unittest.main()