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 7"""Tests for mb.py.""" 8 9import json 10import StringIO 11import os 12import sys 13import unittest 14 15import mb 16 17 18class FakeMBW(mb.MetaBuildWrapper): 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 def __init__(self, files): 97 self.name = '/tmp/file' 98 self.buf = '' 99 self.files = files 100 101 def write(self, contents): 102 self.buf += contents 103 104 def close(self): 105 self.files[self.name] = self.buf 106 107 108TEST_CONFIG = """\ 109{ 110 'masters': { 111 'chromium': {}, 112 'fake_master': { 113 'fake_builder': 'rel_bot', 114 'fake_debug_builder': 'debug_goma', 115 'fake_args_bot': '//build/args/bots/fake_master/fake_args_bot.gn', 116 'fake_multi_phase': { 'phase_1': 'phase_1', 'phase_2': 'phase_2'}, 117 'fake_args_file': 'args_file_goma', 118 'fake_args_file_twice': 'args_file_twice', 119 }, 120 }, 121 'configs': { 122 'args_file_goma': ['args_file', 'goma'], 123 'args_file_twice': ['args_file', 'args_file'], 124 'rel_bot': ['rel', 'goma', 'fake_feature1'], 125 'debug_goma': ['debug', 'goma'], 126 'phase_1': ['phase_1'], 127 'phase_2': ['phase_2'], 128 }, 129 'mixins': { 130 'fake_feature1': { 131 'gn_args': 'enable_doom_melon=true', 132 }, 133 'goma': { 134 'gn_args': 'use_goma=true', 135 }, 136 'args_file': { 137 'args_file': '//build/args/fake.gn', 138 }, 139 'phase_1': { 140 'gn_args': 'phase=1', 141 }, 142 'phase_2': { 143 'gn_args': 'phase=2', 144 }, 145 'rel': { 146 'gn_args': 'is_debug=false', 147 }, 148 'debug': { 149 'gn_args': 'is_debug=true', 150 }, 151 }, 152} 153""" 154 155 156TRYSERVER_CONFIG = """\ 157{ 158 'masters': { 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 def fake_mbw(self, files=None, win32=False): 181 mbw = FakeMBW(win32=win32) 182 mbw.files.setdefault(mbw.default_config, TEST_CONFIG) 183 mbw.files.setdefault( 184 mbw.ToAbsPath('//testing/buildbot/gn_isolate_map.pyl'), 185 '''{ 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_master/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 = {'/tmp/in.json': '''{\ 215 "files": ["foo/foo_unittest.cc"], 216 "test_targets": ["foo_unittests"], 217 "additional_compile_targets": ["all"] 218 }''', 219 '/tmp/out.json.gn': '''{\ 220 "status": "Found dependency", 221 "compile_targets": ["//foo:foo_unittests"], 222 "test_targets": ["//foo:foo_unittests"] 223 }'''} 224 225 mbw = self.fake_mbw(files) 226 mbw.Call = lambda cmd, env=None, buffer_output=True: (0, '', '') 227 228 self.check(['analyze', '-c', 'debug_goma', '//out/Default', 229 '/tmp/in.json', '/tmp/out.json'], mbw=mbw, ret=0) 230 out = json.loads(mbw.files['/tmp/out.json']) 231 self.assertEqual(out, { 232 'status': 'Found dependency', 233 'compile_targets': ['foo:foo_unittests'], 234 'test_targets': ['foo_unittests'] 235 }) 236 237 def test_analyze_optimizes_compile_for_all(self): 238 files = {'/tmp/in.json': '''{\ 239 "files": ["foo/foo_unittest.cc"], 240 "test_targets": ["foo_unittests"], 241 "additional_compile_targets": ["all"] 242 }''', 243 '/tmp/out.json.gn': '''{\ 244 "status": "Found dependency", 245 "compile_targets": ["//foo:foo_unittests", "all"], 246 "test_targets": ["//foo:foo_unittests"] 247 }'''} 248 249 mbw = self.fake_mbw(files) 250 mbw.Call = lambda cmd, env=None, buffer_output=True: (0, '', '') 251 252 self.check(['analyze', '-c', 'debug_goma', '//out/Default', 253 '/tmp/in.json', '/tmp/out.json'], mbw=mbw, ret=0) 254 out = json.loads(mbw.files['/tmp/out.json']) 255 256 # check that 'foo_unittests' is not in the compile_targets 257 self.assertEqual(['all'], out['compile_targets']) 258 259 def test_analyze_handles_other_toolchains(self): 260 files = {'/tmp/in.json': '''{\ 261 "files": ["foo/foo_unittest.cc"], 262 "test_targets": ["foo_unittests"], 263 "additional_compile_targets": ["all"] 264 }''', 265 '/tmp/out.json.gn': '''{\ 266 "status": "Found dependency", 267 "compile_targets": ["//foo:foo_unittests", 268 "//foo:foo_unittests(bar)"], 269 "test_targets": ["//foo:foo_unittests"] 270 }'''} 271 272 mbw = self.fake_mbw(files) 273 mbw.Call = lambda cmd, env=None, buffer_output=True: (0, '', '') 274 275 self.check(['analyze', '-c', 'debug_goma', '//out/Default', 276 '/tmp/in.json', '/tmp/out.json'], mbw=mbw, ret=0) 277 out = json.loads(mbw.files['/tmp/out.json']) 278 279 # crbug.com/736215: If GN returns a label containing a toolchain, 280 # MB (and Ninja) don't know how to handle it; to work around this, 281 # we give up and just build everything we were asked to build. The 282 # output compile_targets should include all of the input test_targets and 283 # additional_compile_targets. 284 self.assertEqual(['all', 'foo_unittests'], out['compile_targets']) 285 286 def test_analyze_handles_way_too_many_results(self): 287 too_many_files = ', '.join(['"//foo:foo%d"' % i for i in range(4 * 1024)]) 288 files = {'/tmp/in.json': '''{\ 289 "files": ["foo/foo_unittest.cc"], 290 "test_targets": ["foo_unittests"], 291 "additional_compile_targets": ["all"] 292 }''', 293 '/tmp/out.json.gn': '''{\ 294 "status": "Found dependency", 295 "compile_targets": [''' + too_many_files + '''], 296 "test_targets": ["//foo:foo_unittests"] 297 }'''} 298 299 mbw = self.fake_mbw(files) 300 mbw.Call = lambda cmd, env=None, buffer_output=True: (0, '', '') 301 302 self.check(['analyze', '-c', 'debug_goma', '//out/Default', 303 '/tmp/in.json', '/tmp/out.json'], mbw=mbw, ret=0) 304 out = json.loads(mbw.files['/tmp/out.json']) 305 306 # If GN returns so many compile targets that we might have command-line 307 # issues, we should give up and just build everything we were asked to 308 # build. The output compile_targets should include all of the input 309 # test_targets and additional_compile_targets. 310 self.assertEqual(['all', 'foo_unittests'], out['compile_targets']) 311 312 def test_gen(self): 313 mbw = self.fake_mbw() 314 self.check(['gen', '-c', 'debug_goma', '//out/Default', '-g', '/goma'], 315 mbw=mbw, ret=0) 316 self.assertMultiLineEqual(mbw.files['/fake_src/out/Default/args.gn'], 317 ('goma_dir = "/goma"\n' 318 'is_debug = true\n' 319 'use_goma = true\n')) 320 321 # Make sure we log both what is written to args.gn and the command line. 322 self.assertIn('Writing """', mbw.out) 323 self.assertIn('/fake_src/buildtools/linux64/gn gen //out/Default --check', 324 mbw.out) 325 326 mbw = self.fake_mbw(win32=True) 327 self.check(['gen', '-c', 'debug_goma', '-g', 'c:\\goma', '//out/Debug'], 328 mbw=mbw, ret=0) 329 self.assertMultiLineEqual(mbw.files['c:\\fake_src\\out\\Debug\\args.gn'], 330 ('goma_dir = "c:\\\\goma"\n' 331 'is_debug = true\n' 332 'use_goma = true\n')) 333 self.assertIn('c:\\fake_src\\buildtools\\win\\gn.exe gen //out/Debug ' 334 '--check\n', mbw.out) 335 336 mbw = self.fake_mbw() 337 self.check(['gen', '-m', 'fake_master', '-b', 'fake_args_bot', 338 '//out/Debug'], 339 mbw=mbw, ret=0) 340 # TODO(almuthanna): disable test temporarily to 341 # solve this issue https://crbug.com/v8/11102 342 # self.assertEqual( 343 # mbw.files['/fake_src/out/Debug/args.gn'], 344 # 'import("//build/args/bots/fake_master/fake_args_bot.gn")\n') 345 346 def test_gen_args_file_mixins(self): 347 mbw = self.fake_mbw() 348 self.check(['gen', '-m', 'fake_master', '-b', 'fake_args_file', 349 '//out/Debug'], mbw=mbw, ret=0) 350 351 self.assertEqual( 352 mbw.files['/fake_src/out/Debug/args.gn'], 353 ('import("//build/args/fake.gn")\n' 354 'use_goma = true\n')) 355 356 mbw = self.fake_mbw() 357 self.check(['gen', '-m', 'fake_master', '-b', 'fake_args_file_twice', 358 '//out/Debug'], mbw=mbw, ret=1) 359 360 def test_gen_fails(self): 361 mbw = self.fake_mbw() 362 mbw.Call = lambda cmd, env=None, buffer_output=True: (1, '', '') 363 self.check(['gen', '-c', 'debug_goma', '//out/Default'], mbw=mbw, ret=1) 364 365 def test_gen_swarming(self): 366 files = { 367 '/tmp/swarming_targets': 'base_unittests\n', 368 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 369 "{'base_unittests': {" 370 " 'label': '//base:base_unittests'," 371 " 'type': 'raw'," 372 " 'args': []," 373 "}}\n" 374 ), 375 '/fake_src/out/Default/base_unittests.runtime_deps': ( 376 "base_unittests\n" 377 ), 378 } 379 mbw = self.fake_mbw(files) 380 self.check(['gen', 381 '-c', 'debug_goma', 382 '--swarming-targets-file', '/tmp/swarming_targets', 383 '//out/Default'], mbw=mbw, ret=0) 384 self.assertIn('/fake_src/out/Default/base_unittests.isolate', 385 mbw.files) 386 self.assertIn('/fake_src/out/Default/base_unittests.isolated.gen.json', 387 mbw.files) 388 389 def test_gen_swarming_script(self): 390 files = { 391 '/tmp/swarming_targets': 'cc_perftests\n', 392 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 393 "{'cc_perftests': {" 394 " 'label': '//cc:cc_perftests'," 395 " 'type': 'script'," 396 " 'script': '/fake_src/out/Default/test_script.py'," 397 " 'args': []," 398 "}}\n" 399 ), 400 'c:\\fake_src\out\Default\cc_perftests.exe.runtime_deps': ( 401 "cc_perftests\n" 402 ), 403 } 404 mbw = self.fake_mbw(files=files, win32=True) 405 self.check(['gen', 406 '-c', 'debug_goma', 407 '--swarming-targets-file', '/tmp/swarming_targets', 408 '--isolate-map-file', 409 '/fake_src/testing/buildbot/gn_isolate_map.pyl', 410 '//out/Default'], mbw=mbw, ret=0) 411 self.assertIn('c:\\fake_src\\out\\Default\\cc_perftests.isolate', 412 mbw.files) 413 self.assertIn('c:\\fake_src\\out\\Default\\cc_perftests.isolated.gen.json', 414 mbw.files) 415 416 417 def test_multiple_isolate_maps(self): 418 files = { 419 '/tmp/swarming_targets': 'cc_perftests\n', 420 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 421 "{'cc_perftests': {" 422 " 'label': '//cc:cc_perftests'," 423 " 'type': 'raw'," 424 " 'args': []," 425 "}}\n" 426 ), 427 '/fake_src/testing/buildbot/gn_isolate_map2.pyl': ( 428 "{'cc_perftests2': {" 429 " 'label': '//cc:cc_perftests'," 430 " 'type': 'raw'," 431 " 'args': []," 432 "}}\n" 433 ), 434 'c:\\fake_src\out\Default\cc_perftests.exe.runtime_deps': ( 435 "cc_perftests\n" 436 ), 437 } 438 mbw = self.fake_mbw(files=files, win32=True) 439 self.check(['gen', 440 '-c', 'debug_goma', 441 '--swarming-targets-file', '/tmp/swarming_targets', 442 '--isolate-map-file', 443 '/fake_src/testing/buildbot/gn_isolate_map.pyl', 444 '--isolate-map-file', 445 '/fake_src/testing/buildbot/gn_isolate_map2.pyl', 446 '//out/Default'], mbw=mbw, ret=0) 447 self.assertIn('c:\\fake_src\\out\\Default\\cc_perftests.isolate', 448 mbw.files) 449 self.assertIn('c:\\fake_src\\out\\Default\\cc_perftests.isolated.gen.json', 450 mbw.files) 451 452 453 def test_duplicate_isolate_maps(self): 454 files = { 455 '/tmp/swarming_targets': 'cc_perftests\n', 456 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 457 "{'cc_perftests': {" 458 " 'label': '//cc:cc_perftests'," 459 " 'type': 'raw'," 460 " 'args': []," 461 "}}\n" 462 ), 463 '/fake_src/testing/buildbot/gn_isolate_map2.pyl': ( 464 "{'cc_perftests': {" 465 " 'label': '//cc:cc_perftests'," 466 " 'type': 'raw'," 467 " 'args': []," 468 "}}\n" 469 ), 470 'c:\\fake_src\out\Default\cc_perftests.exe.runtime_deps': ( 471 "cc_perftests\n" 472 ), 473 } 474 mbw = self.fake_mbw(files=files, win32=True) 475 # Check that passing duplicate targets into mb fails. 476 self.check(['gen', 477 '-c', 'debug_goma', 478 '--swarming-targets-file', '/tmp/swarming_targets', 479 '--isolate-map-file', 480 '/fake_src/testing/buildbot/gn_isolate_map.pyl', 481 '--isolate-map-file', 482 '/fake_src/testing/buildbot/gn_isolate_map2.pyl', 483 '//out/Default'], mbw=mbw, ret=1) 484 485 def test_isolate(self): 486 files = { 487 '/fake_src/out/Default/toolchain.ninja': "", 488 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 489 "{'base_unittests': {" 490 " 'label': '//base:base_unittests'," 491 " 'type': 'raw'," 492 " 'args': []," 493 "}}\n" 494 ), 495 '/fake_src/out/Default/base_unittests.runtime_deps': ( 496 "base_unittests\n" 497 ), 498 } 499 self.check(['isolate', '-c', 'debug_goma', '//out/Default', 500 'base_unittests'], files=files, ret=0) 501 502 # test running isolate on an existing build_dir 503 files['/fake_src/out/Default/args.gn'] = 'is_debug = True\n' 504 self.check(['isolate', '//out/Default', 'base_unittests'], 505 files=files, ret=0) 506 507 self.check(['isolate', '//out/Default', 'base_unittests'], 508 files=files, ret=0) 509 510 def test_run(self): 511 files = { 512 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 513 "{'base_unittests': {" 514 " 'label': '//base:base_unittests'," 515 " 'type': 'raw'," 516 " 'args': []," 517 "}}\n" 518 ), 519 '/fake_src/out/Default/base_unittests.runtime_deps': ( 520 "base_unittests\n" 521 ), 522 } 523 self.check(['run', '-c', 'debug_goma', '//out/Default', 524 'base_unittests'], files=files, ret=0) 525 526 def test_run_swarmed(self): 527 files = { 528 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 529 "{'base_unittests': {" 530 " 'label': '//base:base_unittests'," 531 " 'type': 'raw'," 532 " 'args': []," 533 "}}\n" 534 ), 535 '/fake_src/out/Default/base_unittests.runtime_deps': ( 536 "base_unittests\n" 537 ), 538 'out/Default/base_unittests.archive.json': 539 ("{\"base_unittests\":\"fake_hash\"}"), 540 } 541 542 mbw = self.fake_mbw(files=files) 543 self.check(['run', '-s', '-c', 'debug_goma', '//out/Default', 544 'base_unittests'], mbw=mbw, ret=0) 545 self.check(['run', '-s', '-c', 'debug_goma', '-d', 'os', 'Win7', 546 '//out/Default', 'base_unittests'], mbw=mbw, ret=0) 547 548 def test_lookup(self): 549 self.check(['lookup', '-c', 'debug_goma'], ret=0, 550 out=('\n' 551 'Writing """\\\n' 552 'is_debug = true\n' 553 'use_goma = true\n' 554 '""" to _path_/args.gn.\n\n' 555 '/fake_src/buildtools/linux64/gn gen _path_\n')) 556 557 def test_quiet_lookup(self): 558 self.check(['lookup', '-c', 'debug_goma', '--quiet'], ret=0, 559 out=('is_debug = true\n' 560 'use_goma = true\n')) 561 562 def test_lookup_goma_dir_expansion(self): 563 self.check(['lookup', '-c', 'rel_bot', '-g', '/foo'], ret=0, 564 out=('\n' 565 'Writing """\\\n' 566 'enable_doom_melon = true\n' 567 'goma_dir = "/foo"\n' 568 'is_debug = false\n' 569 'use_goma = true\n' 570 '""" to _path_/args.gn.\n\n' 571 '/fake_src/buildtools/linux64/gn gen _path_\n')) 572 573 def test_help(self): 574 orig_stdout = sys.stdout 575 try: 576 sys.stdout = StringIO.StringIO() 577 self.assertRaises(SystemExit, self.check, ['-h']) 578 self.assertRaises(SystemExit, self.check, ['help']) 579 self.assertRaises(SystemExit, self.check, ['help', 'gen']) 580 finally: 581 sys.stdout = orig_stdout 582 583 def test_multiple_phases(self): 584 # Check that not passing a --phase to a multi-phase builder fails. 585 mbw = self.check(['lookup', '-m', 'fake_master', '-b', 'fake_multi_phase'], 586 ret=1) 587 self.assertIn('Must specify a build --phase', mbw.out) 588 589 # Check that passing a --phase to a single-phase builder fails. 590 mbw = self.check(['lookup', '-m', 'fake_master', '-b', 'fake_builder', 591 '--phase', 'phase_1'], ret=1) 592 self.assertIn('Must not specify a build --phase', mbw.out) 593 594 # Check that passing a wrong phase key to a multi-phase builder fails. 595 mbw = self.check(['lookup', '-m', 'fake_master', '-b', 'fake_multi_phase', 596 '--phase', 'wrong_phase'], ret=1) 597 self.assertIn('Phase wrong_phase doesn\'t exist', mbw.out) 598 599 # Check that passing a correct phase key to a multi-phase builder passes. 600 mbw = self.check(['lookup', '-m', 'fake_master', '-b', 'fake_multi_phase', 601 '--phase', 'phase_1'], ret=0) 602 self.assertIn('phase = 1', mbw.out) 603 604 mbw = self.check(['lookup', '-m', 'fake_master', '-b', 'fake_multi_phase', 605 '--phase', 'phase_2'], ret=0) 606 self.assertIn('phase = 2', mbw.out) 607 608 def test_recursive_lookup(self): 609 files = { 610 '/fake_src/build/args/fake.gn': ( 611 'enable_doom_melon = true\n' 612 'enable_antidoom_banana = true\n' 613 ) 614 } 615 self.check(['lookup', '-m', 'fake_master', '-b', 'fake_args_file', 616 '--recursive'], files=files, ret=0, 617 out=('enable_antidoom_banana = true\n' 618 'enable_doom_melon = true\n' 619 'use_goma = true\n')) 620 621 def test_validate(self): 622 mbw = self.fake_mbw() 623 self.check(['validate'], mbw=mbw, ret=0) 624 625 def test_buildbucket(self): 626 mbw = self.fake_mbw() 627 mbw.files[mbw.default_config] = TRYSERVER_CONFIG 628 self.check(['gerrit-buildbucket-config'], mbw=mbw, 629 ret=0, 630 out=('# This file was generated using ' 631 '"tools/mb/mb.py gerrit-buildbucket-config".\n' 632 '[bucket "luci.luci_tryserver1"]\n' 633 '\tbuilder = luci_builder1\n' 634 '[bucket "luci.luci_tryserver2"]\n' 635 '\tbuilder = luci_builder2\n' 636 '[bucket "master.tryserver.chromium.linux"]\n' 637 '\tbuilder = try_builder\n' 638 '[bucket "master.tryserver.chromium.mac"]\n' 639 '\tbuilder = try_builder2\n')) 640 641 642if __name__ == '__main__': 643 unittest.main() 644