1#!/usr/bin/env python3 2# Copyright 2017 the V8 project authors. All rights reserved. 3# Use of this source code is governed by a BSD-style license that can be 4# found in the LICENSE file. 5 6""" 7Global system tests for V8 test runners and fuzzers. 8 9This hooks up the framework under tools/testrunner testing high-level scenarios 10with different test suite extensions and build configurations. 11""" 12 13# TODO(machenbach): Mock out util.GuessOS to make these tests really platform 14# independent. 15# TODO(machenbach): Move coverage recording to a global test entry point to 16# include other unittest suites in the coverage report. 17# TODO(machenbach): Coverage data from multiprocessing doesn't work. 18# TODO(majeski): Add some tests for the fuzzers. 19 20import collections 21import contextlib 22import json 23import os 24import shutil 25import subprocess 26import sys 27import tempfile 28import unittest 29 30from io import StringIO 31 32TOOLS_ROOT = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) 33TEST_DATA_ROOT = os.path.join(TOOLS_ROOT, 'unittests', 'testdata') 34RUN_TESTS_PY = os.path.join(TOOLS_ROOT, 'run-tests.py') 35 36Result = collections.namedtuple( 37 'Result', ['stdout', 'stderr', 'returncode']) 38 39Result.__str__ = lambda self: ( 40 '\nReturncode: %s\nStdout:\n%s\nStderr:\n%s\n' % 41 (self.returncode, self.stdout, self.stderr)) 42 43 44@contextlib.contextmanager 45def temp_dir(): 46 """Wrapper making a temporary directory available.""" 47 path = None 48 try: 49 path = tempfile.mkdtemp('v8_test_') 50 yield path 51 finally: 52 if path: 53 shutil.rmtree(path) 54 55 56@contextlib.contextmanager 57def temp_base(baseroot='testroot1'): 58 """Wrapper that sets up a temporary V8 test root. 59 60 Args: 61 baseroot: The folder with the test root blueprint. Relevant files will be 62 copied to the temporary test root, to guarantee a fresh setup with no 63 dirty state. 64 """ 65 basedir = os.path.join(TEST_DATA_ROOT, baseroot) 66 with temp_dir() as tempbase: 67 builddir = os.path.join(tempbase, 'out', 'build') 68 testroot = os.path.join(tempbase, 'test') 69 os.makedirs(builddir) 70 shutil.copy(os.path.join(basedir, 'v8_build_config.json'), builddir) 71 shutil.copy(os.path.join(basedir, 'd8_mocked.py'), builddir) 72 73 for suite in os.listdir(os.path.join(basedir, 'test')): 74 os.makedirs(os.path.join(testroot, suite)) 75 for entry in os.listdir(os.path.join(basedir, 'test', suite)): 76 shutil.copy( 77 os.path.join(basedir, 'test', suite, entry), 78 os.path.join(testroot, suite)) 79 yield tempbase 80 81 82@contextlib.contextmanager 83def capture(): 84 """Wrapper that replaces system stdout/stderr an provides the streams.""" 85 oldout = sys.stdout 86 olderr = sys.stderr 87 try: 88 stdout=StringIO() 89 stderr=StringIO() 90 sys.stdout = stdout 91 sys.stderr = stderr 92 yield stdout, stderr 93 finally: 94 sys.stdout = oldout 95 sys.stderr = olderr 96 97 98def run_tests(basedir, *args, **kwargs): 99 """Executes the test runner with captured output.""" 100 with capture() as (stdout, stderr): 101 sys_args = ['--command-prefix', sys.executable] + list(args) 102 if kwargs.get('infra_staging', False): 103 sys_args.append('--infra-staging') 104 else: 105 sys_args.append('--no-infra-staging') 106 code = standard_runner.StandardTestRunner(basedir=basedir).execute(sys_args) 107 return Result(stdout.getvalue(), stderr.getvalue(), code) 108 109 110def override_build_config(basedir, **kwargs): 111 """Override the build config with new values provided as kwargs.""" 112 path = os.path.join(basedir, 'out', 'build', 'v8_build_config.json') 113 with open(path) as f: 114 config = json.load(f) 115 config.update(kwargs) 116 with open(path, 'w') as f: 117 json.dump(config, f) 118 119 120class SystemTest(unittest.TestCase): 121 @classmethod 122 def setUpClass(cls): 123 # Try to set up python coverage and run without it if not available. 124 cls._cov = None 125 try: 126 import coverage 127 if int(coverage.__version__.split('.')[0]) < 4: 128 cls._cov = None 129 print('Python coverage version >= 4 required.') 130 raise ImportError() 131 cls._cov = coverage.Coverage( 132 source=([os.path.join(TOOLS_ROOT, 'testrunner')]), 133 omit=['*unittest*', '*__init__.py'], 134 ) 135 cls._cov.exclude('raise NotImplementedError') 136 cls._cov.exclude('if __name__ == .__main__.:') 137 cls._cov.exclude('except TestRunnerError:') 138 cls._cov.exclude('except KeyboardInterrupt:') 139 cls._cov.exclude('if options.verbose:') 140 cls._cov.exclude('if verbose:') 141 cls._cov.exclude('pass') 142 cls._cov.exclude('assert False') 143 cls._cov.start() 144 except ImportError: 145 print('Running without python coverage.') 146 sys.path.append(TOOLS_ROOT) 147 global standard_runner 148 from testrunner import standard_runner 149 global num_fuzzer 150 from testrunner import num_fuzzer 151 from testrunner.local import command 152 from testrunner.local import pool 153 command.setup_testing() 154 pool.setup_testing() 155 156 @classmethod 157 def tearDownClass(cls): 158 if cls._cov: 159 cls._cov.stop() 160 print('') 161 print(cls._cov.report(show_missing=True)) 162 163 def testPass(self): 164 """Test running only passing tests in two variants. 165 166 Also test printing durations. 167 """ 168 with temp_base() as basedir: 169 result = run_tests( 170 basedir, 171 '--progress=verbose', 172 '--variants=default,stress', 173 '--time', 174 'sweet/bananas', 175 'sweet/raspberries', 176 ) 177 self.assertIn('sweet/bananas default: PASS', result.stdout, result) 178 # TODO(majeski): Implement for test processors 179 # self.assertIn('Total time:', result.stderr, result) 180 # self.assertIn('sweet/bananas', result.stderr, result) 181 self.assertEqual(0, result.returncode, result) 182 183 def testPassHeavy(self): 184 """Test running with some tests marked heavy.""" 185 with temp_base(baseroot='testroot3') as basedir: 186 result = run_tests( 187 basedir, 188 '--progress=verbose', 189 '--variants=nooptimization', 190 '-j2', 191 'sweet', 192 ) 193 self.assertIn('7 tests ran', result.stdout, result) 194 self.assertEqual(0, result.returncode, result) 195 196 def testShardedProc(self): 197 with temp_base() as basedir: 198 for shard in [1, 2]: 199 result = run_tests( 200 basedir, 201 '--progress=verbose', 202 '--variants=default,stress', 203 '--shard-count=2', 204 '--shard-run=%d' % shard, 205 'sweet/blackberries', 206 'sweet/raspberries', 207 infra_staging=False, 208 ) 209 # One of the shards gets one variant of each test. 210 self.assertIn('2 tests ran', result.stdout, result) 211 if shard == 1: 212 self.assertIn('sweet/raspberries default', result.stdout, result) 213 self.assertIn('sweet/raspberries stress', result.stdout, result) 214 self.assertEqual(0, result.returncode, result) 215 else: 216 self.assertIn( 217 'sweet/blackberries default: FAIL', result.stdout, result) 218 self.assertIn( 219 'sweet/blackberries stress: FAIL', result.stdout, result) 220 self.assertEqual(1, result.returncode, result) 221 222 @unittest.skip("incompatible with test processors") 223 def testSharded(self): 224 """Test running a particular shard.""" 225 with temp_base() as basedir: 226 for shard in [1, 2]: 227 result = run_tests( 228 basedir, 229 '--progress=verbose', 230 '--variants=default,stress', 231 '--shard-count=2', 232 '--shard-run=%d' % shard, 233 'sweet/bananas', 234 'sweet/raspberries', 235 ) 236 # One of the shards gets one variant of each test. 237 self.assertIn('Running 2 tests', result.stdout, result) 238 self.assertIn('sweet/bananas', result.stdout, result) 239 self.assertIn('sweet/raspberries', result.stdout, result) 240 self.assertEqual(0, result.returncode, result) 241 242 def testFail(self): 243 """Test running only failing tests in two variants.""" 244 with temp_base() as basedir: 245 result = run_tests( 246 basedir, 247 '--progress=verbose', 248 '--variants=default,stress', 249 'sweet/strawberries', 250 infra_staging=False, 251 ) 252 self.assertIn('sweet/strawberries default: FAIL', result.stdout, result) 253 self.assertEqual(1, result.returncode, result) 254 255 def check_cleaned_json_output( 256 self, expected_results_name, actual_json, basedir): 257 # Check relevant properties of the json output. 258 with open(actual_json) as f: 259 json_output = json.load(f) 260 261 # Replace duration in actual output as it's non-deterministic. Also 262 # replace the python executable prefix as it has a different absolute 263 # path dependent on where this runs. 264 def replace_variable_data(data): 265 data['duration'] = 1 266 data['command'] = ' '.join( 267 ['/usr/bin/python'] + data['command'].split()[1:]) 268 data['command'] = data['command'].replace(basedir + '/', '') 269 for data in json_output['slowest_tests']: 270 replace_variable_data(data) 271 for data in json_output['results']: 272 replace_variable_data(data) 273 json_output['duration_mean'] = 1 274 # We need lexicographic sorting here to avoid non-deterministic behaviour 275 # The original sorting key is duration, but in our fake test we have 276 # non-deterministic durations before we reset them to 1 277 def sort_key(x): 278 return str(sorted(x.items())) 279 json_output['slowest_tests'].sort(key=sort_key) 280 281 with open(os.path.join(TEST_DATA_ROOT, expected_results_name)) as f: 282 expected_test_results = json.load(f) 283 284 pretty_json = json.dumps(json_output, indent=2, sort_keys=True) 285 msg = None # Set to pretty_json for bootstrapping. 286 self.assertDictEqual(json_output, expected_test_results, msg) 287 288 def testFailWithRerunAndJSON(self): 289 """Test re-running a failing test and output to json.""" 290 with temp_base() as basedir: 291 json_path = os.path.join(basedir, 'out.json') 292 result = run_tests( 293 basedir, 294 '--progress=verbose', 295 '--variants=default', 296 '--rerun-failures-count=2', 297 '--random-seed=123', 298 '--json-test-results', json_path, 299 'sweet/strawberries', 300 infra_staging=False, 301 ) 302 self.assertIn('sweet/strawberries default: FAIL', result.stdout, result) 303 # With test processors we don't count reruns as separated failures. 304 # TODO(majeski): fix it? 305 self.assertIn('1 tests failed', result.stdout, result) 306 self.assertEqual(0, result.returncode, result) 307 308 # TODO(majeski): Previously we only reported the variant flags in the 309 # flags field of the test result. 310 # After recent changes we report all flags, including the file names. 311 # This is redundant to the command. Needs investigation. 312 self.maxDiff = None 313 self.check_cleaned_json_output( 314 'expected_test_results1.json', json_path, basedir) 315 316 def testFlakeWithRerunAndJSON(self): 317 """Test re-running a failing test and output to json.""" 318 with temp_base(baseroot='testroot2') as basedir: 319 json_path = os.path.join(basedir, 'out.json') 320 result = run_tests( 321 basedir, 322 '--progress=verbose', 323 '--variants=default', 324 '--rerun-failures-count=2', 325 '--random-seed=123', 326 '--json-test-results', json_path, 327 'sweet', 328 infra_staging=False, 329 ) 330 self.assertIn('sweet/bananaflakes default: FAIL PASS', result.stdout, result) 331 self.assertIn('=== sweet/bananaflakes (flaky) ===', result.stdout, result) 332 self.assertIn('1 tests failed', result.stdout, result) 333 self.assertIn('1 tests were flaky', result.stdout, result) 334 self.assertEqual(0, result.returncode, result) 335 self.maxDiff = None 336 self.check_cleaned_json_output( 337 'expected_test_results2.json', json_path, basedir) 338 339 def testAutoDetect(self): 340 """Fake a build with several auto-detected options. 341 342 Using all those options at once doesn't really make much sense. This is 343 merely for getting coverage. 344 """ 345 with temp_base() as basedir: 346 override_build_config( 347 basedir, dcheck_always_on=True, is_asan=True, is_cfi=True, 348 is_msan=True, is_tsan=True, is_ubsan_vptr=True, target_cpu='x86', 349 v8_enable_i18n_support=False, v8_target_cpu='x86', 350 v8_enable_verify_csa=False, v8_enable_lite_mode=False, 351 v8_enable_pointer_compression=False, 352 v8_enable_pointer_compression_shared_cage=False, 353 v8_enable_shared_ro_heap=False, 354 v8_enable_sandbox=False) 355 result = run_tests( 356 basedir, 357 '--progress=verbose', 358 '--variants=default', 359 'sweet/bananas', 360 ) 361 expect_text = ( 362 '>>> Autodetected:\n' 363 'asan\n' 364 'cfi_vptr\n' 365 'dcheck_always_on\n' 366 'msan\n' 367 'no_i18n\n' 368 'tsan\n' 369 'ubsan_vptr\n' 370 'webassembly\n' 371 '>>> Running tests for ia32.release') 372 self.assertIn(expect_text, result.stdout, result) 373 self.assertEqual(0, result.returncode, result) 374 # TODO(machenbach): Test some more implications of the auto-detected 375 # options, e.g. that the right env variables are set. 376 377 def testSkips(self): 378 """Test skipping tests in status file for a specific variant.""" 379 with temp_base() as basedir: 380 result = run_tests( 381 basedir, 382 '--progress=verbose', 383 '--variants=nooptimization', 384 'sweet/strawberries', 385 infra_staging=False, 386 ) 387 self.assertIn('0 tests ran', result.stdout, result) 388 self.assertEqual(2, result.returncode, result) 389 390 def testRunSkips(self): 391 """Inverse the above. Test parameter to keep running skipped tests.""" 392 with temp_base() as basedir: 393 result = run_tests( 394 basedir, 395 '--progress=verbose', 396 '--variants=nooptimization', 397 '--run-skipped', 398 'sweet/strawberries', 399 ) 400 self.assertIn('1 tests failed', result.stdout, result) 401 self.assertIn('1 tests ran', result.stdout, result) 402 self.assertEqual(1, result.returncode, result) 403 404 def testDefault(self): 405 """Test using default test suites, though no tests are run since they don't 406 exist in a test setting. 407 """ 408 with temp_base() as basedir: 409 result = run_tests( 410 basedir, 411 infra_staging=False, 412 ) 413 self.assertIn('0 tests ran', result.stdout, result) 414 self.assertEqual(2, result.returncode, result) 415 416 def testNoBuildConfig(self): 417 """Test failing run when build config is not found.""" 418 with temp_dir() as basedir: 419 result = run_tests(basedir) 420 self.assertIn('Failed to load build config', result.stdout, result) 421 self.assertEqual(5, result.returncode, result) 422 423 def testInconsistentArch(self): 424 """Test failing run when attempting to wrongly override the arch.""" 425 with temp_base() as basedir: 426 result = run_tests(basedir, '--arch=ia32') 427 self.assertIn( 428 '--arch value (ia32) inconsistent with build config (x64).', 429 result.stdout, result) 430 self.assertEqual(5, result.returncode, result) 431 432 def testWrongVariant(self): 433 """Test using a bogus variant.""" 434 with temp_base() as basedir: 435 result = run_tests(basedir, '--variants=meh') 436 self.assertEqual(5, result.returncode, result) 437 438 def testModeFromBuildConfig(self): 439 """Test auto-detection of mode from build config.""" 440 with temp_base() as basedir: 441 result = run_tests(basedir, '--outdir=out/build', 'sweet/bananas') 442 self.assertIn('Running tests for x64.release', result.stdout, result) 443 self.assertEqual(0, result.returncode, result) 444 445 @unittest.skip("not available with test processors") 446 def testReport(self): 447 """Test the report feature. 448 449 This also exercises various paths in statusfile logic. 450 """ 451 with temp_base() as basedir: 452 result = run_tests( 453 basedir, 454 '--variants=default', 455 'sweet', 456 '--report', 457 ) 458 self.assertIn( 459 '3 tests are expected to fail that we should fix', 460 result.stdout, result) 461 self.assertEqual(1, result.returncode, result) 462 463 @unittest.skip("not available with test processors") 464 def testWarnUnusedRules(self): 465 """Test the unused-rules feature.""" 466 with temp_base() as basedir: 467 result = run_tests( 468 basedir, 469 '--variants=default,nooptimization', 470 'sweet', 471 '--warn-unused', 472 ) 473 self.assertIn( 'Unused rule: carrots', result.stdout, result) 474 self.assertIn( 'Unused rule: regress/', result.stdout, result) 475 self.assertEqual(1, result.returncode, result) 476 477 @unittest.skip("not available with test processors") 478 def testCatNoSources(self): 479 """Test printing sources, but the suite's tests have none available.""" 480 with temp_base() as basedir: 481 result = run_tests( 482 basedir, 483 '--variants=default', 484 'sweet/bananas', 485 '--cat', 486 ) 487 self.assertIn('begin source: sweet/bananas', result.stdout, result) 488 self.assertIn('(no source available)', result.stdout, result) 489 self.assertEqual(0, result.returncode, result) 490 491 def testPredictable(self): 492 """Test running a test in verify-predictable mode. 493 494 The test will fail because of missing allocation output. We verify that and 495 that the predictable flags are passed and printed after failure. 496 """ 497 with temp_base() as basedir: 498 override_build_config(basedir, v8_enable_verify_predictable=True) 499 result = run_tests( 500 basedir, 501 '--progress=verbose', 502 '--variants=default', 503 'sweet/bananas', 504 infra_staging=False, 505 ) 506 self.assertIn('1 tests ran', result.stdout, result) 507 self.assertIn('sweet/bananas default: FAIL', result.stdout, result) 508 self.assertIn('Test had no allocation output', result.stdout, result) 509 self.assertIn('--predictable --verify-predictable', result.stdout, result) 510 self.assertEqual(1, result.returncode, result) 511 512 def testSlowArch(self): 513 """Test timeout factor manipulation on slow architecture.""" 514 with temp_base() as basedir: 515 override_build_config(basedir, v8_target_cpu='arm64') 516 result = run_tests( 517 basedir, 518 '--progress=verbose', 519 '--variants=default', 520 'sweet/bananas', 521 ) 522 # TODO(machenbach): We don't have a way for testing if the correct 523 # timeout was used. 524 self.assertEqual(0, result.returncode, result) 525 526 def testRandomSeedStressWithDefault(self): 527 """Test using random-seed-stress feature has the right number of tests.""" 528 with temp_base() as basedir: 529 result = run_tests( 530 basedir, 531 '--progress=verbose', 532 '--variants=default', 533 '--random-seed-stress-count=2', 534 'sweet/bananas', 535 infra_staging=False, 536 ) 537 self.assertIn('2 tests ran', result.stdout, result) 538 self.assertEqual(0, result.returncode, result) 539 540 def testRandomSeedStressWithSeed(self): 541 """Test using random-seed-stress feature passing a random seed.""" 542 with temp_base() as basedir: 543 result = run_tests( 544 basedir, 545 '--progress=verbose', 546 '--variants=default', 547 '--random-seed-stress-count=2', 548 '--random-seed=123', 549 'sweet/strawberries', 550 ) 551 self.assertIn('2 tests ran', result.stdout, result) 552 # We use a failing test so that the command is printed and we can verify 553 # that the right random seed was passed. 554 self.assertIn('--random-seed=123', result.stdout, result) 555 self.assertEqual(1, result.returncode, result) 556 557 def testSpecificVariants(self): 558 """Test using NO_VARIANTS modifiers in status files skips the desire tests. 559 560 The test runner cmd line configures 4 tests to run (2 tests * 2 variants). 561 But the status file applies a modifier to each skipping one of the 562 variants. 563 """ 564 with temp_base() as basedir: 565 override_build_config(basedir, is_asan=True) 566 result = run_tests( 567 basedir, 568 '--progress=verbose', 569 '--variants=default,stress', 570 'sweet/bananas', 571 'sweet/raspberries', 572 ) 573 # Both tests are either marked as running in only default or only 574 # slow variant. 575 self.assertIn('2 tests ran', result.stdout, result) 576 self.assertEqual(0, result.returncode, result) 577 578 def testStatusFilePresubmit(self): 579 """Test that the fake status file is well-formed.""" 580 with temp_base() as basedir: 581 from testrunner.local import statusfile 582 self.assertTrue(statusfile.PresubmitCheck( 583 os.path.join(basedir, 'test', 'sweet', 'sweet.status'))) 584 585 def testDotsProgress(self): 586 with temp_base() as basedir: 587 result = run_tests( 588 basedir, 589 '--progress=dots', 590 'sweet/cherries', 591 'sweet/bananas', 592 '--no-sorting', '-j1', # make results order deterministic 593 infra_staging=False, 594 ) 595 self.assertIn('2 tests ran', result.stdout, result) 596 self.assertIn('F.', result.stdout, result) 597 self.assertEqual(1, result.returncode, result) 598 599 def testMonoProgress(self): 600 self._testCompactProgress('mono') 601 602 def testColorProgress(self): 603 self._testCompactProgress('color') 604 605 def _testCompactProgress(self, name): 606 with temp_base() as basedir: 607 result = run_tests( 608 basedir, 609 '--progress=%s' % name, 610 'sweet/cherries', 611 'sweet/bananas', 612 infra_staging=False, 613 ) 614 if name == 'color': 615 expected = ('\033[34m% 28\033[0m|' 616 '\033[32m+ 1\033[0m|' 617 '\033[31m- 1\033[0m]: Done') 618 else: 619 expected = '% 28|+ 1|- 1]: Done' 620 self.assertIn(expected, result.stdout) 621 self.assertIn('sweet/cherries', result.stdout) 622 self.assertIn('sweet/bananas', result.stdout) 623 self.assertEqual(1, result.returncode, result) 624 625 def testExitAfterNFailures(self): 626 with temp_base() as basedir: 627 result = run_tests( 628 basedir, 629 '--progress=verbose', 630 '--exit-after-n-failures=2', 631 '-j1', 632 'sweet/mangoes', # PASS 633 'sweet/strawberries', # FAIL 634 'sweet/blackberries', # FAIL 635 'sweet/raspberries', # should not run 636 ) 637 self.assertIn('sweet/mangoes default: PASS', result.stdout, result) 638 self.assertIn('sweet/strawberries default: FAIL', result.stdout, result) 639 self.assertIn('Too many failures, exiting...', result.stdout, result) 640 self.assertIn('sweet/blackberries default: FAIL', result.stdout, result) 641 self.assertNotIn('sweet/raspberries', result.stdout, result) 642 self.assertIn('2 tests failed', result.stdout, result) 643 self.assertIn('3 tests ran', result.stdout, result) 644 self.assertEqual(1, result.returncode, result) 645 646 def testNumFuzzer(self): 647 sys_args = ['--command-prefix', sys.executable, '--outdir', 'out/build'] 648 649 with temp_base() as basedir: 650 with capture() as (stdout, stderr): 651 code = num_fuzzer.NumFuzzer(basedir=basedir).execute(sys_args) 652 result = Result(stdout.getvalue(), stderr.getvalue(), code) 653 654 self.assertEqual(0, result.returncode, result) 655 656 def testRunnerFlags(self): 657 """Test that runner-specific flags are passed to tests.""" 658 with temp_base() as basedir: 659 result = run_tests( 660 basedir, 661 '--progress=verbose', 662 '--variants=default', 663 '--random-seed=42', 664 'sweet/bananas', 665 '-v', 666 ) 667 668 self.assertIn( 669 '--test bananas --random-seed=42 --nohard-abort --testing-d8-test-runner', 670 result.stdout, result) 671 self.assertEqual(0, result.returncode, result) 672 673 674if __name__ == '__main__': 675 unittest.main() 676