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