• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/python
2# Copyright 2016 the V8 project authors. All rights reserved.
3# Copyright 2015 The Chromium Authors. All rights reserved.
4# Use of this source code is governed by a BSD-style license that can be
5# found in the LICENSE file.
6"""Tests for mb.py."""
7
8import json
9import StringIO
10import os
11import sys
12import unittest
13
14import mb
15
16
17class FakeMBW(mb.MetaBuildWrapper):
18
19  def __init__(self, win32=False):
20    super(FakeMBW, self).__init__()
21
22    # Override vars for test portability.
23    if win32:
24      self.chromium_src_dir = 'c:\\fake_src'
25      self.default_config = 'c:\\fake_src\\tools\\mb\\mb_config.pyl'
26      self.default_isolate_map = ('c:\\fake_src\\testing\\buildbot\\'
27                                  'gn_isolate_map.pyl')
28      self.platform = 'win32'
29      self.executable = 'c:\\python\\python.exe'
30      self.sep = '\\'
31    else:
32      self.chromium_src_dir = '/fake_src'
33      self.default_config = '/fake_src/tools/mb/mb_config.pyl'
34      self.default_isolate_map = '/fake_src/testing/buildbot/gn_isolate_map.pyl'
35      self.executable = '/usr/bin/python'
36      self.platform = 'linux2'
37      self.sep = '/'
38
39    self.files = {}
40    self.calls = []
41    self.cmds = []
42    self.cross_compile = None
43    self.out = ''
44    self.err = ''
45    self.rmdirs = []
46
47  def ExpandUser(self, path):
48    return '$HOME/%s' % path
49
50  def Exists(self, path):
51    return self.files.get(path) is not None
52
53  def MaybeMakeDirectory(self, path):
54    self.files[path] = True
55
56  def PathJoin(self, *comps):
57    return self.sep.join(comps)
58
59  def ReadFile(self, path):
60    return self.files[path]
61
62  def WriteFile(self, path, contents, force_verbose=False):
63    if self.args.dryrun or self.args.verbose or force_verbose:
64      self.Print('\nWriting """\\\n%s""" to %s.\n' % (contents, path))
65    self.files[path] = contents
66
67  def Call(self, cmd, env=None, buffer_output=True):
68    self.calls.append(cmd)
69    if self.cmds:
70      return self.cmds.pop(0)
71    return 0, '', ''
72
73  def Print(self, *args, **kwargs):
74    sep = kwargs.get('sep', ' ')
75    end = kwargs.get('end', '\n')
76    f = kwargs.get('file', sys.stdout)
77    if f == sys.stderr:
78      self.err += sep.join(args) + end
79    else:
80      self.out += sep.join(args) + end
81
82  def TempFile(self, mode='w'):
83    return FakeFile(self.files)
84
85  def RemoveFile(self, path):
86    del self.files[path]
87
88  def RemoveDirectory(self, path):
89    self.rmdirs.append(path)
90    files_to_delete = [f for f in self.files if f.startswith(path)]
91    for f in files_to_delete:
92      self.files[f] = None
93
94
95class FakeFile(object):
96
97  def __init__(self, files):
98    self.name = '/tmp/file'
99    self.buf = ''
100    self.files = files
101
102  def write(self, contents):
103    self.buf += contents
104
105  def close(self):
106    self.files[self.name] = self.buf
107
108
109TEST_CONFIG = """\
110{
111  'builder_groups': {
112    'chromium': {},
113    'fake_builder_group': {
114      'fake_builder': 'rel_bot',
115      'fake_debug_builder': 'debug_goma',
116      'fake_args_bot': '//build/args/bots/fake_builder_group/fake_args_bot.gn',
117      'fake_multi_phase': { 'phase_1': 'phase_1', 'phase_2': 'phase_2'},
118      'fake_args_file': 'args_file_goma',
119      'fake_args_file_twice': 'args_file_twice',
120    },
121  },
122  'configs': {
123    'args_file_goma': ['args_file', 'goma'],
124    'args_file_twice': ['args_file', 'args_file'],
125    'rel_bot': ['rel', 'goma', 'fake_feature1'],
126    'debug_goma': ['debug', 'goma'],
127    'phase_1': ['phase_1'],
128    'phase_2': ['phase_2'],
129  },
130  'mixins': {
131    'fake_feature1': {
132      'gn_args': 'enable_doom_melon=true',
133    },
134    'goma': {
135      'gn_args': 'use_goma=true',
136    },
137    'args_file': {
138      'args_file': '//build/args/fake.gn',
139    },
140    'phase_1': {
141      'gn_args': 'phase=1',
142    },
143    'phase_2': {
144      'gn_args': 'phase=2',
145    },
146    'rel': {
147      'gn_args': 'is_debug=false',
148    },
149    'debug': {
150      'gn_args': 'is_debug=true',
151    },
152  },
153}
154"""
155
156TRYSERVER_CONFIG = """\
157{
158  'builder_groups': {
159    'not_a_tryserver': {
160      'fake_builder': 'fake_config',
161    },
162    'tryserver.chromium.linux': {
163      'try_builder': 'fake_config',
164    },
165    'tryserver.chromium.mac': {
166      'try_builder2': 'fake_config',
167    },
168  },
169  'luci_tryservers': {
170    'luci_tryserver1': ['luci_builder1'],
171    'luci_tryserver2': ['luci_builder2'],
172  },
173  'configs': {},
174  'mixins': {},
175}
176"""
177
178
179class UnitTest(unittest.TestCase):
180
181  def fake_mbw(self, files=None, win32=False):
182    mbw = FakeMBW(win32=win32)
183    mbw.files.setdefault(mbw.default_config, TEST_CONFIG)
184    mbw.files.setdefault(
185        mbw.ToAbsPath('//testing/buildbot/gn_isolate_map.pyl'), '''{
186        "foo_unittests": {
187          "label": "//foo:foo_unittests",
188          "type": "console_test_launcher",
189          "args": [],
190        },
191      }''')
192    mbw.files.setdefault(
193        mbw.ToAbsPath('//build/args/bots/fake_builder_group/fake_args_bot.gn'),
194        'is_debug = false\n')
195    if files:
196      for path, contents in files.items():
197        mbw.files[path] = contents
198    return mbw
199
200  def check(self, args, mbw=None, files=None, out=None, err=None, ret=None):
201    if not mbw:
202      mbw = self.fake_mbw(files)
203
204    actual_ret = mbw.Main(args)
205
206    self.assertEqual(actual_ret, ret)
207    if out is not None:
208      self.assertEqual(mbw.out, out)
209    if err is not None:
210      self.assertEqual(mbw.err, err)
211    return mbw
212
213  def test_analyze(self):
214    files = {
215        '/tmp/in.json':
216            '''{\
217               "files": ["foo/foo_unittest.cc"],
218               "test_targets": ["foo_unittests"],
219               "additional_compile_targets": ["all"]
220             }''',
221        '/tmp/out.json.gn':
222            '''{\
223               "status": "Found dependency",
224               "compile_targets": ["//foo:foo_unittests"],
225               "test_targets": ["//foo:foo_unittests"]
226             }'''
227    }
228
229    mbw = self.fake_mbw(files)
230    mbw.Call = lambda cmd, env=None, buffer_output=True: (0, '', '')
231
232    self.check([
233        'analyze', '-c', 'debug_goma', '//out/Default', '/tmp/in.json',
234        '/tmp/out.json'
235    ],
236               mbw=mbw,
237               ret=0)
238    out = json.loads(mbw.files['/tmp/out.json'])
239    self.assertEqual(
240        out, {
241            'status': 'Found dependency',
242            'compile_targets': ['foo:foo_unittests'],
243            'test_targets': ['foo_unittests']
244        })
245
246  def test_analyze_optimizes_compile_for_all(self):
247    files = {
248        '/tmp/in.json':
249            '''{\
250               "files": ["foo/foo_unittest.cc"],
251               "test_targets": ["foo_unittests"],
252               "additional_compile_targets": ["all"]
253             }''',
254        '/tmp/out.json.gn':
255            '''{\
256               "status": "Found dependency",
257               "compile_targets": ["//foo:foo_unittests", "all"],
258               "test_targets": ["//foo:foo_unittests"]
259             }'''
260    }
261
262    mbw = self.fake_mbw(files)
263    mbw.Call = lambda cmd, env=None, buffer_output=True: (0, '', '')
264
265    self.check([
266        'analyze', '-c', 'debug_goma', '//out/Default', '/tmp/in.json',
267        '/tmp/out.json'
268    ],
269               mbw=mbw,
270               ret=0)
271    out = json.loads(mbw.files['/tmp/out.json'])
272
273    # check that 'foo_unittests' is not in the compile_targets
274    self.assertEqual(['all'], out['compile_targets'])
275
276  def test_analyze_handles_other_toolchains(self):
277    files = {
278        '/tmp/in.json':
279            '''{\
280               "files": ["foo/foo_unittest.cc"],
281               "test_targets": ["foo_unittests"],
282               "additional_compile_targets": ["all"]
283             }''',
284        '/tmp/out.json.gn':
285            '''{\
286               "status": "Found dependency",
287               "compile_targets": ["//foo:foo_unittests",
288                                   "//foo:foo_unittests(bar)"],
289               "test_targets": ["//foo:foo_unittests"]
290             }'''
291    }
292
293    mbw = self.fake_mbw(files)
294    mbw.Call = lambda cmd, env=None, buffer_output=True: (0, '', '')
295
296    self.check([
297        'analyze', '-c', 'debug_goma', '//out/Default', '/tmp/in.json',
298        '/tmp/out.json'
299    ],
300               mbw=mbw,
301               ret=0)
302    out = json.loads(mbw.files['/tmp/out.json'])
303
304    # crbug.com/736215: If GN returns a label containing a toolchain,
305    # MB (and Ninja) don't know how to handle it; to work around this,
306    # we give up and just build everything we were asked to build. The
307    # output compile_targets should include all of the input test_targets and
308    # additional_compile_targets.
309    self.assertEqual(['all', 'foo_unittests'], out['compile_targets'])
310
311  def test_analyze_handles_way_too_many_results(self):
312    too_many_files = ', '.join(['"//foo:foo%d"' % i for i in range(4 * 1024)])
313    files = {
314        '/tmp/in.json':
315            '''{\
316               "files": ["foo/foo_unittest.cc"],
317               "test_targets": ["foo_unittests"],
318               "additional_compile_targets": ["all"]
319             }''',
320        '/tmp/out.json.gn':
321            '''{\
322               "status": "Found dependency",
323               "compile_targets": [''' + too_many_files + '''],
324               "test_targets": ["//foo:foo_unittests"]
325             }'''
326    }
327
328    mbw = self.fake_mbw(files)
329    mbw.Call = lambda cmd, env=None, buffer_output=True: (0, '', '')
330
331    self.check([
332        'analyze', '-c', 'debug_goma', '//out/Default', '/tmp/in.json',
333        '/tmp/out.json'
334    ],
335               mbw=mbw,
336               ret=0)
337    out = json.loads(mbw.files['/tmp/out.json'])
338
339    # If GN returns so many compile targets that we might have command-line
340    # issues, we should give up and just build everything we were asked to
341    # build. The output compile_targets should include all of the input
342    # test_targets and additional_compile_targets.
343    self.assertEqual(['all', 'foo_unittests'], out['compile_targets'])
344
345  def test_gen(self):
346    mbw = self.fake_mbw()
347    self.check(['gen', '-c', 'debug_goma', '//out/Default', '-g', '/goma'],
348               mbw=mbw,
349               ret=0)
350    self.assertMultiLineEqual(mbw.files['/fake_src/out/Default/args.gn'],
351                              ('goma_dir = "/goma"\n'
352                               'is_debug = true\n'
353                               'use_goma = true\n'))
354
355    # Make sure we log both what is written to args.gn and the command line.
356    self.assertIn('Writing """', mbw.out)
357    self.assertIn('/fake_src/buildtools/linux64/gn gen //out/Default --check',
358                  mbw.out)
359
360    mbw = self.fake_mbw(win32=True)
361    self.check(['gen', '-c', 'debug_goma', '-g', 'c:\\goma', '//out/Debug'],
362               mbw=mbw,
363               ret=0)
364    self.assertMultiLineEqual(mbw.files['c:\\fake_src\\out\\Debug\\args.gn'],
365                              ('goma_dir = "c:\\\\goma"\n'
366                               'is_debug = true\n'
367                               'use_goma = true\n'))
368    self.assertIn(
369        'c:\\fake_src\\buildtools\\win\\gn.exe gen //out/Debug '
370        '--check\n', mbw.out)
371
372    mbw = self.fake_mbw()
373    self.check([
374        'gen', '-m', 'fake_builder_group', '-b', 'fake_args_bot', '//out/Debug'
375    ],
376               mbw=mbw,
377               ret=0)
378    # TODO(almuthanna): disable test temporarily to
379    #   solve this issue https://crbug.com/v8/11102
380    # self.assertEqual(
381    #     mbw.files['/fake_src/out/Debug/args.gn'],
382    #     'import("//build/args/bots/fake_builder_group/fake_args_bot.gn")\n')
383
384  def test_gen_args_file_mixins(self):
385    mbw = self.fake_mbw()
386    self.check([
387        'gen', '-m', 'fake_builder_group', '-b', 'fake_args_file', '//out/Debug'
388    ],
389               mbw=mbw,
390               ret=0)
391
392    self.assertEqual(mbw.files['/fake_src/out/Debug/args.gn'],
393                     ('import("//build/args/fake.gn")\n'
394                      'use_goma = true\n'))
395
396    mbw = self.fake_mbw()
397    self.check([
398        'gen', '-m', 'fake_builder_group', '-b', 'fake_args_file_twice',
399        '//out/Debug'
400    ],
401               mbw=mbw,
402               ret=1)
403
404  def test_gen_fails(self):
405    mbw = self.fake_mbw()
406    mbw.Call = lambda cmd, env=None, buffer_output=True: (1, '', '')
407    self.check(['gen', '-c', 'debug_goma', '//out/Default'], mbw=mbw, ret=1)
408
409  def test_gen_swarming(self):
410    files = {
411        '/tmp/swarming_targets':
412            'base_unittests\n',
413        '/fake_src/testing/buildbot/gn_isolate_map.pyl':
414            ("{'base_unittests': {"
415             "  'label': '//base:base_unittests',"
416             "  'type': 'raw',"
417             "  'args': [],"
418             "}}\n"),
419        '/fake_src/out/Default/base_unittests.runtime_deps':
420            ("base_unittests\n"),
421    }
422    mbw = self.fake_mbw(files)
423    self.check([
424        'gen', '-c', 'debug_goma', '--swarming-targets-file',
425        '/tmp/swarming_targets', '//out/Default'
426    ],
427               mbw=mbw,
428               ret=0)
429    self.assertIn('/fake_src/out/Default/base_unittests.isolate', mbw.files)
430    self.assertIn('/fake_src/out/Default/base_unittests.isolated.gen.json',
431                  mbw.files)
432
433  def test_gen_swarming_script(self):
434    files = {
435        '/tmp/swarming_targets':
436            'cc_perftests\n',
437        '/fake_src/testing/buildbot/gn_isolate_map.pyl':
438            ("{'cc_perftests': {"
439             "  'label': '//cc:cc_perftests',"
440             "  'type': 'script',"
441             "  'script': '/fake_src/out/Default/test_script.py',"
442             "  'args': [],"
443             "}}\n"),
444        'c:\\fake_src\out\Default\cc_perftests.exe.runtime_deps':
445            ("cc_perftests\n"),
446    }
447    mbw = self.fake_mbw(files=files, win32=True)
448    self.check([
449        'gen', '-c', 'debug_goma', '--swarming-targets-file',
450        '/tmp/swarming_targets', '--isolate-map-file',
451        '/fake_src/testing/buildbot/gn_isolate_map.pyl', '//out/Default'
452    ],
453               mbw=mbw,
454               ret=0)
455    self.assertIn('c:\\fake_src\\out\\Default\\cc_perftests.isolate', mbw.files)
456    self.assertIn('c:\\fake_src\\out\\Default\\cc_perftests.isolated.gen.json',
457                  mbw.files)
458
459  def test_multiple_isolate_maps(self):
460    files = {
461        '/tmp/swarming_targets':
462            'cc_perftests\n',
463        '/fake_src/testing/buildbot/gn_isolate_map.pyl':
464            ("{'cc_perftests': {"
465             "  'label': '//cc:cc_perftests',"
466             "  'type': 'raw',"
467             "  'args': [],"
468             "}}\n"),
469        '/fake_src/testing/buildbot/gn_isolate_map2.pyl':
470            ("{'cc_perftests2': {"
471             "  'label': '//cc:cc_perftests',"
472             "  'type': 'raw',"
473             "  'args': [],"
474             "}}\n"),
475        'c:\\fake_src\out\Default\cc_perftests.exe.runtime_deps':
476            ("cc_perftests\n"),
477    }
478    mbw = self.fake_mbw(files=files, win32=True)
479    self.check([
480        'gen', '-c', 'debug_goma', '--swarming-targets-file',
481        '/tmp/swarming_targets', '--isolate-map-file',
482        '/fake_src/testing/buildbot/gn_isolate_map.pyl', '--isolate-map-file',
483        '/fake_src/testing/buildbot/gn_isolate_map2.pyl', '//out/Default'
484    ],
485               mbw=mbw,
486               ret=0)
487    self.assertIn('c:\\fake_src\\out\\Default\\cc_perftests.isolate', mbw.files)
488    self.assertIn('c:\\fake_src\\out\\Default\\cc_perftests.isolated.gen.json',
489                  mbw.files)
490
491  def test_duplicate_isolate_maps(self):
492    files = {
493        '/tmp/swarming_targets':
494            'cc_perftests\n',
495        '/fake_src/testing/buildbot/gn_isolate_map.pyl':
496            ("{'cc_perftests': {"
497             "  'label': '//cc:cc_perftests',"
498             "  'type': 'raw',"
499             "  'args': [],"
500             "}}\n"),
501        '/fake_src/testing/buildbot/gn_isolate_map2.pyl':
502            ("{'cc_perftests': {"
503             "  'label': '//cc:cc_perftests',"
504             "  'type': 'raw',"
505             "  'args': [],"
506             "}}\n"),
507        'c:\\fake_src\out\Default\cc_perftests.exe.runtime_deps':
508            ("cc_perftests\n"),
509    }
510    mbw = self.fake_mbw(files=files, win32=True)
511    # Check that passing duplicate targets into mb fails.
512    self.check([
513        'gen', '-c', 'debug_goma', '--swarming-targets-file',
514        '/tmp/swarming_targets', '--isolate-map-file',
515        '/fake_src/testing/buildbot/gn_isolate_map.pyl', '--isolate-map-file',
516        '/fake_src/testing/buildbot/gn_isolate_map2.pyl', '//out/Default'
517    ],
518               mbw=mbw,
519               ret=1)
520
521  def test_isolate(self):
522    files = {
523        '/fake_src/out/Default/toolchain.ninja':
524            "",
525        '/fake_src/testing/buildbot/gn_isolate_map.pyl':
526            ("{'base_unittests': {"
527             "  'label': '//base:base_unittests',"
528             "  'type': 'raw',"
529             "  'args': [],"
530             "}}\n"),
531        '/fake_src/out/Default/base_unittests.runtime_deps':
532            ("base_unittests\n"),
533    }
534    self.check(
535        ['isolate', '-c', 'debug_goma', '//out/Default', 'base_unittests'],
536        files=files,
537        ret=0)
538
539    # test running isolate on an existing build_dir
540    files['/fake_src/out/Default/args.gn'] = 'is_debug = True\n'
541    self.check(['isolate', '//out/Default', 'base_unittests'],
542               files=files,
543               ret=0)
544
545    self.check(['isolate', '//out/Default', 'base_unittests'],
546               files=files,
547               ret=0)
548
549  def test_run(self):
550    files = {
551        '/fake_src/testing/buildbot/gn_isolate_map.pyl':
552            ("{'base_unittests': {"
553             "  'label': '//base:base_unittests',"
554             "  'type': 'raw',"
555             "  'args': [],"
556             "}}\n"),
557        '/fake_src/out/Default/base_unittests.runtime_deps':
558            ("base_unittests\n"),
559    }
560    self.check(['run', '-c', 'debug_goma', '//out/Default', 'base_unittests'],
561               files=files,
562               ret=0)
563
564  def test_lookup(self):
565    self.check(['lookup', '-c', 'debug_goma'],
566               ret=0,
567               out=('\n'
568                    'Writing """\\\n'
569                    'is_debug = true\n'
570                    'use_goma = true\n'
571                    '""" to _path_/args.gn.\n\n'
572                    '/fake_src/buildtools/linux64/gn gen _path_\n'))
573
574  def test_quiet_lookup(self):
575    self.check(['lookup', '-c', 'debug_goma', '--quiet'],
576               ret=0,
577               out=('is_debug = true\n'
578                    'use_goma = true\n'))
579
580  def test_lookup_goma_dir_expansion(self):
581    self.check(['lookup', '-c', 'rel_bot', '-g', '/foo'],
582               ret=0,
583               out=('\n'
584                    'Writing """\\\n'
585                    'enable_doom_melon = true\n'
586                    'goma_dir = "/foo"\n'
587                    'is_debug = false\n'
588                    'use_goma = true\n'
589                    '""" to _path_/args.gn.\n\n'
590                    '/fake_src/buildtools/linux64/gn gen _path_\n'))
591
592  def test_help(self):
593    orig_stdout = sys.stdout
594    try:
595      sys.stdout = StringIO.StringIO()
596      self.assertRaises(SystemExit, self.check, ['-h'])
597      self.assertRaises(SystemExit, self.check, ['help'])
598      self.assertRaises(SystemExit, self.check, ['help', 'gen'])
599    finally:
600      sys.stdout = orig_stdout
601
602  def test_multiple_phases(self):
603    # Check that not passing a --phase to a multi-phase builder fails.
604    mbw = self.check(
605        ['lookup', '-m', 'fake_builder_group', '-b', 'fake_multi_phase'], ret=1)
606    self.assertIn('Must specify a build --phase', mbw.out)
607
608    # Check that passing a --phase to a single-phase builder fails.
609    mbw = self.check([
610        'lookup', '-m', 'fake_builder_group', '-b', 'fake_builder', '--phase',
611        'phase_1'
612    ],
613                     ret=1)
614    self.assertIn('Must not specify a build --phase', mbw.out)
615
616    # Check that passing a wrong phase key to a multi-phase builder fails.
617    mbw = self.check([
618        'lookup', '-m', 'fake_builder_group', '-b', 'fake_multi_phase',
619        '--phase', 'wrong_phase'
620    ],
621                     ret=1)
622    self.assertIn('Phase wrong_phase doesn\'t exist', mbw.out)
623
624    # Check that passing a correct phase key to a multi-phase builder passes.
625    mbw = self.check([
626        'lookup', '-m', 'fake_builder_group', '-b', 'fake_multi_phase',
627        '--phase', 'phase_1'
628    ],
629                     ret=0)
630    self.assertIn('phase = 1', mbw.out)
631
632    mbw = self.check([
633        'lookup', '-m', 'fake_builder_group', '-b', 'fake_multi_phase',
634        '--phase', 'phase_2'
635    ],
636                     ret=0)
637    self.assertIn('phase = 2', mbw.out)
638
639  def test_recursive_lookup(self):
640    files = {
641        '/fake_src/build/args/fake.gn': ('enable_doom_melon = true\n'
642                                         'enable_antidoom_banana = true\n')
643    }
644    self.check([
645        'lookup', '-m', 'fake_builder_group', '-b', 'fake_args_file',
646        '--recursive'
647    ],
648               files=files,
649               ret=0,
650               out=('enable_antidoom_banana = true\n'
651                    'enable_doom_melon = true\n'
652                    'use_goma = true\n'))
653
654  def test_validate(self):
655    mbw = self.fake_mbw()
656    self.check(['validate'], mbw=mbw, ret=0)
657
658  def test_buildbucket(self):
659    mbw = self.fake_mbw()
660    mbw.files[mbw.default_config] = TRYSERVER_CONFIG
661    self.check(['gerrit-buildbucket-config'],
662               mbw=mbw,
663               ret=0,
664               out=('# This file was generated using '
665                    '"tools/mb/mb.py gerrit-buildbucket-config".\n'
666                    '[bucket "luci.luci_tryserver1"]\n'
667                    '\tbuilder = luci_builder1\n'
668                    '[bucket "luci.luci_tryserver2"]\n'
669                    '\tbuilder = luci_builder2\n'
670                    '[bucket "builder_group.tryserver.chromium.linux"]\n'
671                    '\tbuilder = try_builder\n'
672                    '[bucket "builder_group.tryserver.chromium.mac"]\n'
673                    '\tbuilder = try_builder2\n'))
674
675
676if __name__ == '__main__':
677  unittest.main()
678