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 xrange(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 self.assertEqual( 341 mbw.files['/fake_src/out/Debug/args.gn'], 342 'import("//build/args/bots/fake_master/fake_args_bot.gn")\n') 343 344 def test_gen_args_file_mixins(self): 345 mbw = self.fake_mbw() 346 self.check(['gen', '-m', 'fake_master', '-b', 'fake_args_file', 347 '//out/Debug'], mbw=mbw, ret=0) 348 349 self.assertEqual( 350 mbw.files['/fake_src/out/Debug/args.gn'], 351 ('import("//build/args/fake.gn")\n' 352 'use_goma = true\n')) 353 354 mbw = self.fake_mbw() 355 self.check(['gen', '-m', 'fake_master', '-b', 'fake_args_file_twice', 356 '//out/Debug'], mbw=mbw, ret=1) 357 358 def test_gen_fails(self): 359 mbw = self.fake_mbw() 360 mbw.Call = lambda cmd, env=None, buffer_output=True: (1, '', '') 361 self.check(['gen', '-c', 'debug_goma', '//out/Default'], mbw=mbw, ret=1) 362 363 def test_gen_swarming(self): 364 files = { 365 '/tmp/swarming_targets': 'base_unittests\n', 366 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 367 "{'base_unittests': {" 368 " 'label': '//base:base_unittests'," 369 " 'type': 'raw'," 370 " 'args': []," 371 "}}\n" 372 ), 373 '/fake_src/out/Default/base_unittests.runtime_deps': ( 374 "base_unittests\n" 375 ), 376 } 377 mbw = self.fake_mbw(files) 378 self.check(['gen', 379 '-c', 'debug_goma', 380 '--swarming-targets-file', '/tmp/swarming_targets', 381 '//out/Default'], mbw=mbw, ret=0) 382 self.assertIn('/fake_src/out/Default/base_unittests.isolate', 383 mbw.files) 384 self.assertIn('/fake_src/out/Default/base_unittests.isolated.gen.json', 385 mbw.files) 386 387 def test_gen_swarming_script(self): 388 files = { 389 '/tmp/swarming_targets': 'cc_perftests\n', 390 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 391 "{'cc_perftests': {" 392 " 'label': '//cc:cc_perftests'," 393 " 'type': 'script'," 394 " 'script': '/fake_src/out/Default/test_script.py'," 395 " 'args': []," 396 "}}\n" 397 ), 398 'c:\\fake_src\out\Default\cc_perftests.exe.runtime_deps': ( 399 "cc_perftests\n" 400 ), 401 } 402 mbw = self.fake_mbw(files=files, win32=True) 403 self.check(['gen', 404 '-c', 'debug_goma', 405 '--swarming-targets-file', '/tmp/swarming_targets', 406 '--isolate-map-file', 407 '/fake_src/testing/buildbot/gn_isolate_map.pyl', 408 '//out/Default'], mbw=mbw, ret=0) 409 self.assertIn('c:\\fake_src\\out\\Default\\cc_perftests.isolate', 410 mbw.files) 411 self.assertIn('c:\\fake_src\\out\\Default\\cc_perftests.isolated.gen.json', 412 mbw.files) 413 414 415 def test_multiple_isolate_maps(self): 416 files = { 417 '/tmp/swarming_targets': 'cc_perftests\n', 418 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 419 "{'cc_perftests': {" 420 " 'label': '//cc:cc_perftests'," 421 " 'type': 'raw'," 422 " 'args': []," 423 "}}\n" 424 ), 425 '/fake_src/testing/buildbot/gn_isolate_map2.pyl': ( 426 "{'cc_perftests2': {" 427 " 'label': '//cc:cc_perftests'," 428 " 'type': 'raw'," 429 " 'args': []," 430 "}}\n" 431 ), 432 'c:\\fake_src\out\Default\cc_perftests.exe.runtime_deps': ( 433 "cc_perftests\n" 434 ), 435 } 436 mbw = self.fake_mbw(files=files, win32=True) 437 self.check(['gen', 438 '-c', 'debug_goma', 439 '--swarming-targets-file', '/tmp/swarming_targets', 440 '--isolate-map-file', 441 '/fake_src/testing/buildbot/gn_isolate_map.pyl', 442 '--isolate-map-file', 443 '/fake_src/testing/buildbot/gn_isolate_map2.pyl', 444 '//out/Default'], mbw=mbw, ret=0) 445 self.assertIn('c:\\fake_src\\out\\Default\\cc_perftests.isolate', 446 mbw.files) 447 self.assertIn('c:\\fake_src\\out\\Default\\cc_perftests.isolated.gen.json', 448 mbw.files) 449 450 451 def test_duplicate_isolate_maps(self): 452 files = { 453 '/tmp/swarming_targets': 'cc_perftests\n', 454 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 455 "{'cc_perftests': {" 456 " 'label': '//cc:cc_perftests'," 457 " 'type': 'raw'," 458 " 'args': []," 459 "}}\n" 460 ), 461 '/fake_src/testing/buildbot/gn_isolate_map2.pyl': ( 462 "{'cc_perftests': {" 463 " 'label': '//cc:cc_perftests'," 464 " 'type': 'raw'," 465 " 'args': []," 466 "}}\n" 467 ), 468 'c:\\fake_src\out\Default\cc_perftests.exe.runtime_deps': ( 469 "cc_perftests\n" 470 ), 471 } 472 mbw = self.fake_mbw(files=files, win32=True) 473 # Check that passing duplicate targets into mb fails. 474 self.check(['gen', 475 '-c', 'debug_goma', 476 '--swarming-targets-file', '/tmp/swarming_targets', 477 '--isolate-map-file', 478 '/fake_src/testing/buildbot/gn_isolate_map.pyl', 479 '--isolate-map-file', 480 '/fake_src/testing/buildbot/gn_isolate_map2.pyl', 481 '//out/Default'], mbw=mbw, ret=1) 482 483 def test_isolate(self): 484 files = { 485 '/fake_src/out/Default/toolchain.ninja': "", 486 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 487 "{'base_unittests': {" 488 " 'label': '//base:base_unittests'," 489 " 'type': 'raw'," 490 " 'args': []," 491 "}}\n" 492 ), 493 '/fake_src/out/Default/base_unittests.runtime_deps': ( 494 "base_unittests\n" 495 ), 496 } 497 self.check(['isolate', '-c', 'debug_goma', '//out/Default', 498 'base_unittests'], files=files, ret=0) 499 500 # test running isolate on an existing build_dir 501 files['/fake_src/out/Default/args.gn'] = 'is_debug = True\n' 502 self.check(['isolate', '//out/Default', 'base_unittests'], 503 files=files, ret=0) 504 505 self.check(['isolate', '//out/Default', 'base_unittests'], 506 files=files, ret=0) 507 508 def test_run(self): 509 files = { 510 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 511 "{'base_unittests': {" 512 " 'label': '//base:base_unittests'," 513 " 'type': 'raw'," 514 " 'args': []," 515 "}}\n" 516 ), 517 '/fake_src/out/Default/base_unittests.runtime_deps': ( 518 "base_unittests\n" 519 ), 520 } 521 self.check(['run', '-c', 'debug_goma', '//out/Default', 522 'base_unittests'], files=files, ret=0) 523 524 def test_run_swarmed(self): 525 files = { 526 '/fake_src/testing/buildbot/gn_isolate_map.pyl': ( 527 "{'base_unittests': {" 528 " 'label': '//base:base_unittests'," 529 " 'type': 'raw'," 530 " 'args': []," 531 "}}\n" 532 ), 533 '/fake_src/out/Default/base_unittests.runtime_deps': ( 534 "base_unittests\n" 535 ), 536 } 537 538 def run_stub(cmd, **_kwargs): 539 if 'isolate.py' in cmd[1]: 540 return 0, 'fake_hash base_unittests', '' 541 else: 542 return 0, '', '' 543 544 mbw = self.fake_mbw(files=files) 545 mbw.Run = run_stub 546 self.check(['run', '-s', '-c', 'debug_goma', '//out/Default', 547 'base_unittests'], mbw=mbw, ret=0) 548 self.check(['run', '-s', '-c', 'debug_goma', '-d', 'os', 'Win7', 549 '//out/Default', 'base_unittests'], mbw=mbw, ret=0) 550 551 def test_lookup(self): 552 self.check(['lookup', '-c', 'debug_goma'], ret=0) 553 554 def test_lookup_goma_dir_expansion(self): 555 self.check(['lookup', '-c', 'rel_bot', '-g', '/foo'], ret=0, 556 out=('\n' 557 'Writing """\\\n' 558 'enable_doom_melon = true\n' 559 'goma_dir = "/foo"\n' 560 'is_debug = false\n' 561 'use_goma = true\n' 562 '""" to _path_/args.gn.\n\n' 563 '/fake_src/buildtools/linux64/gn gen _path_\n')) 564 565 def test_help(self): 566 orig_stdout = sys.stdout 567 try: 568 sys.stdout = StringIO.StringIO() 569 self.assertRaises(SystemExit, self.check, ['-h']) 570 self.assertRaises(SystemExit, self.check, ['help']) 571 self.assertRaises(SystemExit, self.check, ['help', 'gen']) 572 finally: 573 sys.stdout = orig_stdout 574 575 def test_multiple_phases(self): 576 # Check that not passing a --phase to a multi-phase builder fails. 577 mbw = self.check(['lookup', '-m', 'fake_master', '-b', 'fake_multi_phase'], 578 ret=1) 579 self.assertIn('Must specify a build --phase', mbw.out) 580 581 # Check that passing a --phase to a single-phase builder fails. 582 mbw = self.check(['lookup', '-m', 'fake_master', '-b', 'fake_builder', 583 '--phase', 'phase_1'], ret=1) 584 self.assertIn('Must not specify a build --phase', mbw.out) 585 586 # Check that passing a wrong phase key to a multi-phase builder fails. 587 mbw = self.check(['lookup', '-m', 'fake_master', '-b', 'fake_multi_phase', 588 '--phase', 'wrong_phase'], ret=1) 589 self.assertIn('Phase wrong_phase doesn\'t exist', mbw.out) 590 591 # Check that passing a correct phase key to a multi-phase builder passes. 592 mbw = self.check(['lookup', '-m', 'fake_master', '-b', 'fake_multi_phase', 593 '--phase', 'phase_1'], ret=0) 594 self.assertIn('phase = 1', mbw.out) 595 596 mbw = self.check(['lookup', '-m', 'fake_master', '-b', 'fake_multi_phase', 597 '--phase', 'phase_2'], ret=0) 598 self.assertIn('phase = 2', mbw.out) 599 600 def test_validate(self): 601 mbw = self.fake_mbw() 602 self.check(['validate'], mbw=mbw, ret=0) 603 604 def test_buildbucket(self): 605 mbw = self.fake_mbw() 606 mbw.files[mbw.default_config] = TRYSERVER_CONFIG 607 self.check(['gerrit-buildbucket-config'], mbw=mbw, 608 ret=0, 609 out=('# This file was generated using ' 610 '"tools/mb/mb.py gerrit-buildbucket-config".\n' 611 '[bucket "luci.luci_tryserver1"]\n' 612 '\tbuilder = luci_builder1\n' 613 '[bucket "luci.luci_tryserver2"]\n' 614 '\tbuilder = luci_builder2\n' 615 '[bucket "master.tryserver.chromium.linux"]\n' 616 '\tbuilder = try_builder\n' 617 '[bucket "master.tryserver.chromium.mac"]\n' 618 '\tbuilder = try_builder2\n')) 619 620 621if __name__ == '__main__': 622 unittest.main() 623