Home | History | Annotate | Download | only in foozzie
      1 #!/usr/bin/env python
      2 # Copyright 2016 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 """
      7 V8 correctness fuzzer launcher script.
      8 """
      9 
     10 import argparse
     11 import hashlib
     12 import itertools
     13 import json
     14 import os
     15 import re
     16 import sys
     17 import traceback
     18 
     19 import v8_commands
     20 import v8_suppressions
     21 
     22 CONFIGS = dict(
     23   default=['--validate-asm'],
     24   fullcode=['--nocrankshaft', '--turbo-filter=~', '--validate-asm'],
     25   ignition=['--ignition', '--turbo-filter=~', '--hydrogen-filter=~',
     26             '--validate-asm', '--nocrankshaft'],
     27   ignition_eager=['--ignition', '--turbo-filter=~', '--hydrogen-filter=~',
     28                   '--validate-asm', '--nocrankshaft', '--no-lazy',
     29                   '--no-lazy-inner-functions'],
     30   ignition_staging=['--ignition-staging', '--validate-asm'],
     31   ignition_turbo=['--ignition-staging', '--turbo', '--validate-asm'],
     32   ignition_turbo_opt=['--ignition-staging', '--turbo', '--always-opt',
     33                       '--validate-asm'],
     34 )
     35 
     36 # Timeout in seconds for one d8 run.
     37 TIMEOUT = 3
     38 
     39 # Return codes.
     40 RETURN_PASS = 0
     41 RETURN_FAIL = 2
     42 
     43 BASE_PATH = os.path.dirname(os.path.abspath(__file__))
     44 PREAMBLE = [
     45   os.path.join(BASE_PATH, 'v8_mock.js'),
     46   os.path.join(BASE_PATH, 'v8_suppressions.js'),
     47 ]
     48 ARCH_MOCKS = os.path.join(BASE_PATH, 'v8_mock_archs.js')
     49 
     50 FLAGS = ['--abort_on_stack_overflow', '--expose-gc', '--allow-natives-syntax',
     51          '--invoke-weak-callbacks', '--omit-quit', '--es-staging']
     52 
     53 SUPPORTED_ARCHS = ['ia32', 'x64', 'arm', 'arm64']
     54 
     55 # Output for suppressed failure case.
     56 FAILURE_HEADER_TEMPLATE = """#
     57 # V8 correctness failure
     58 # V8 correctness configs: %(configs)s
     59 # V8 correctness sources: %(source_key)s
     60 # V8 correctness suppression: %(suppression)s
     61 """
     62 
     63 # Extended output for failure case. The 'CHECK' is for the minimizer.
     64 FAILURE_TEMPLATE = FAILURE_HEADER_TEMPLATE + """#
     65 # CHECK
     66 #
     67 # Compared %(first_config_label)s with %(second_config_label)s
     68 #
     69 # Flags of %(first_config_label)s:
     70 %(first_config_flags)s
     71 # Flags of %(second_config_label)s:
     72 %(second_config_flags)s
     73 #
     74 # Difference:
     75 %(difference)s
     76 #
     77 # Source file:
     78 %(source)s
     79 #
     80 ### Start of configuration %(first_config_label)s:
     81 %(first_config_output)s
     82 ### End of configuration %(first_config_label)s
     83 #
     84 ### Start of configuration %(second_config_label)s:
     85 %(second_config_output)s
     86 ### End of configuration %(second_config_label)s
     87 """
     88 
     89 FUZZ_TEST_RE = re.compile(r'.*fuzz(-\d+\.js)')
     90 SOURCE_RE = re.compile(r'print\("v8-foozzie source: (.*)"\);')
     91 
     92 # The number of hex digits used from the hash of the original source file path.
     93 # Keep the number small to avoid duplicate explosion.
     94 ORIGINAL_SOURCE_HASH_LENGTH = 3
     95 
     96 # Placeholder string if no original source file could be determined.
     97 ORIGINAL_SOURCE_DEFAULT = 'none'
     98 
     99 
    100 def infer_arch(d8):
    101   """Infer the V8 architecture from the build configuration next to the
    102   executable.
    103   """
    104   with open(os.path.join(os.path.dirname(d8), 'v8_build_config.json')) as f:
    105     arch = json.load(f)['v8_current_cpu']
    106   return 'ia32' if arch == 'x86' else arch
    107 
    108 
    109 def parse_args():
    110   parser = argparse.ArgumentParser()
    111   parser.add_argument(
    112     '--random-seed', type=int, required=True,
    113     help='random seed passed to both runs')
    114   parser.add_argument(
    115       '--first-config', help='first configuration', default='ignition')
    116   parser.add_argument(
    117       '--second-config', help='second configuration', default='ignition_turbo')
    118   parser.add_argument(
    119       '--first-d8', default='d8',
    120       help='optional path to first d8 executable, '
    121            'default: bundled in the same directory as this script')
    122   parser.add_argument(
    123       '--second-d8',
    124       help='optional path to second d8 executable, default: same as first')
    125   parser.add_argument('testcase', help='path to test case')
    126   options = parser.parse_args()
    127 
    128   # Ensure we have a test case.
    129   assert (os.path.exists(options.testcase) and
    130           os.path.isfile(options.testcase)), (
    131       'Test case %s doesn\'t exist' % options.testcase)
    132 
    133   # Use first d8 as default for second d8.
    134   options.second_d8 = options.second_d8 or options.first_d8
    135 
    136   # Ensure absolute paths.
    137   if not os.path.isabs(options.first_d8):
    138     options.first_d8 = os.path.join(BASE_PATH, options.first_d8)
    139   if not os.path.isabs(options.second_d8):
    140     options.second_d8 = os.path.join(BASE_PATH, options.second_d8)
    141 
    142   # Ensure executables exist.
    143   assert os.path.exists(options.first_d8)
    144   assert os.path.exists(options.second_d8)
    145 
    146   # Infer architecture from build artifacts.
    147   options.first_arch = infer_arch(options.first_d8)
    148   options.second_arch = infer_arch(options.second_d8)
    149 
    150   # Ensure we make a sane comparison.
    151   assert (options.first_arch != options.second_arch or
    152           options.first_config != options.second_config), (
    153       'Need either arch or config difference.')
    154   assert options.first_arch in SUPPORTED_ARCHS
    155   assert options.second_arch in SUPPORTED_ARCHS
    156   assert options.first_config in CONFIGS
    157   assert options.second_config in CONFIGS
    158 
    159   return options
    160 
    161 
    162 def get_meta_data(content):
    163   """Extracts original-source-file paths from test case content."""
    164   sources = []
    165   for line in content.splitlines():
    166     match = SOURCE_RE.match(line)
    167     if match:
    168       sources.append(match.group(1))
    169   return {'sources': sources}
    170 
    171 
    172 def content_bailout(content, ignore_fun):
    173   """Print failure state and return if ignore_fun matches content."""
    174   bug = (ignore_fun(content) or '').strip()
    175   if bug:
    176     print FAILURE_HEADER_TEMPLATE % dict(
    177         configs='', source_key='', suppression=bug)
    178     return True
    179   return False
    180 
    181 
    182 def pass_bailout(output, step_number):
    183   """Print info and return if in timeout or crash pass states."""
    184   if output.HasTimedOut():
    185     # Dashed output, so that no other clusterfuzz tools can match the
    186     # words timeout or crash.
    187     print '# V8 correctness - T-I-M-E-O-U-T %d' % step_number
    188     return True
    189   if output.HasCrashed():
    190     print '# V8 correctness - C-R-A-S-H %d' % step_number
    191     return True
    192   return False
    193 
    194 
    195 def fail_bailout(output, ignore_by_output_fun):
    196   """Print failure state and return if ignore_by_output_fun matches output."""
    197   bug = (ignore_by_output_fun(output.stdout) or '').strip()
    198   if bug:
    199     print FAILURE_HEADER_TEMPLATE % dict(
    200         configs='', source_key='', suppression=bug)
    201     return True
    202   return False
    203 
    204 
    205 def main():
    206   options = parse_args()
    207 
    208   # Suppressions are architecture and configuration specific.
    209   suppress = v8_suppressions.get_suppression(
    210       options.first_arch, options.first_config,
    211       options.second_arch, options.second_config,
    212   )
    213 
    214   # Static bailout based on test case content or metadata.
    215   with open(options.testcase) as f:
    216     content = f.read()
    217   if content_bailout(get_meta_data(content), suppress.ignore_by_metadata):
    218     return RETURN_FAIL
    219   if content_bailout(content, suppress.ignore_by_content):
    220     return RETURN_FAIL
    221 
    222   # Set up runtime arguments.
    223   common_flags = FLAGS + ['--random-seed', str(options.random_seed)]
    224   first_config_flags = common_flags + CONFIGS[options.first_config]
    225   second_config_flags = common_flags + CONFIGS[options.second_config]
    226 
    227   def run_d8(d8, config_flags):
    228     preamble = PREAMBLE[:]
    229     if options.first_arch != options.second_arch:
    230       preamble.append(ARCH_MOCKS)
    231     args = [d8] + config_flags + preamble + [options.testcase]
    232     print " ".join(args)
    233     if d8.endswith('.py'):
    234       # Wrap with python in tests.
    235       args = [sys.executable] + args
    236     return v8_commands.Execute(
    237         args,
    238         cwd=os.path.dirname(options.testcase),
    239         timeout=TIMEOUT,
    240     )
    241 
    242   first_config_output = run_d8(options.first_d8, first_config_flags)
    243 
    244   # Early bailout based on first run's output.
    245   if pass_bailout(first_config_output, 1):
    246     return RETURN_PASS
    247 
    248   second_config_output = run_d8(options.second_d8, second_config_flags)
    249 
    250   # Bailout based on second run's output.
    251   if pass_bailout(second_config_output, 2):
    252     return RETURN_PASS
    253 
    254   difference, source = suppress.diff(
    255       first_config_output.stdout, second_config_output.stdout)
    256 
    257   if source:
    258     source_key = hashlib.sha1(source).hexdigest()[:ORIGINAL_SOURCE_HASH_LENGTH]
    259   else:
    260     source = ORIGINAL_SOURCE_DEFAULT
    261     source_key = ORIGINAL_SOURCE_DEFAULT
    262 
    263   if difference:
    264     # Only bail out due to suppressed output if there was a difference. If a
    265     # suppression doesn't show up anymore in the statistics, we might want to
    266     # remove it.
    267     if fail_bailout(first_config_output, suppress.ignore_by_output1):
    268       return RETURN_FAIL
    269     if fail_bailout(second_config_output, suppress.ignore_by_output2):
    270       return RETURN_FAIL
    271 
    272     # The first three entries will be parsed by clusterfuzz. Format changes
    273     # will require changes on the clusterfuzz side.
    274     first_config_label = '%s,%s' % (options.first_arch, options.first_config)
    275     second_config_label = '%s,%s' % (options.second_arch, options.second_config)
    276     print (FAILURE_TEMPLATE % dict(
    277         configs='%s:%s' % (first_config_label, second_config_label),
    278         source_key=source_key,
    279         suppression='', # We can't tie bugs to differences.
    280         first_config_label=first_config_label,
    281         second_config_label=second_config_label,
    282         first_config_flags=' '.join(first_config_flags),
    283         second_config_flags=' '.join(second_config_flags),
    284         first_config_output=
    285             first_config_output.stdout.decode('utf-8', 'replace'),
    286         second_config_output=
    287             second_config_output.stdout.decode('utf-8', 'replace'),
    288         source=source,
    289         difference=difference.decode('utf-8', 'replace'),
    290     )).encode('utf-8', 'replace')
    291     return RETURN_FAIL
    292 
    293   # TODO(machenbach): Figure out if we could also return a bug in case there's
    294   # no difference, but one of the line suppressions has matched - and without
    295   # the match there would be a difference.
    296 
    297   print '# V8 correctness - pass'
    298   return RETURN_PASS
    299 
    300 
    301 if __name__ == "__main__":
    302   try:
    303     result = main()
    304   except SystemExit:
    305     # Make sure clusterfuzz reports internal errors and wrong usage.
    306     # Use one label for all internal and usage errors.
    307     print FAILURE_HEADER_TEMPLATE % dict(
    308         configs='', source_key='', suppression='wrong_usage')
    309     result = RETURN_FAIL
    310   except MemoryError:
    311     # Running out of memory happens occasionally but is not actionable.
    312     print '# V8 correctness - pass'
    313     result = RETURN_PASS
    314   except Exception as e:
    315     print FAILURE_HEADER_TEMPLATE % dict(
    316         configs='', source_key='', suppression='internal_error')
    317     print '# Internal error: %s' % e
    318     traceback.print_exc(file=sys.stdout)
    319     result = RETURN_FAIL
    320 
    321   sys.exit(result)
    322