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