Home | History | Annotate | Download | only in bindings
      1 # Copyright (C) 2011 Google Inc.  All rights reserved.
      2 #
      3 # Redistribution and use in source and binary forms, with or without
      4 # modification, are permitted provided that the following conditions
      5 # are met:
      6 # 1. Redistributions of source code must retain the above copyright
      7 #    notice, this list of conditions and the following disclaimer.
      8 # 2. Redistributions in binary form must reproduce the above copyright
      9 #    notice, this list of conditions and the following disclaimer in the
     10 #    documentation and/or other materials provided with the distribution.
     11 #
     12 # THIS SOFTWARE IS PROVIDED BY APPLE COMPUTER, INC. ``AS IS'' AND ANY
     13 # EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
     14 # IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     15 # PURPOSE ARE DISCLAIMED.  IN NO EVENT SHALL APPLE COMPUTER, INC. OR
     16 # CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
     17 # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO,
     18 # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
     19 # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
     20 # OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
     21 # (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
     22 # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
     23 #
     24 
     25 from contextlib import contextmanager
     26 import filecmp
     27 import fnmatch
     28 import os
     29 import shutil
     30 import sys
     31 import tempfile
     32 
     33 from webkitpy.common.system.executive import Executive
     34 
     35 # Source/ path is needed both to find input IDL files, and to import other
     36 # Python modules.
     37 module_path = os.path.dirname(__file__)
     38 source_path = os.path.normpath(os.path.join(module_path, os.pardir, os.pardir,
     39                                             os.pardir, os.pardir, 'Source'))
     40 sys.path.append(source_path)  # for Source/bindings imports
     41 
     42 import bindings.scripts.compute_interfaces_info_individual
     43 from bindings.scripts.compute_interfaces_info_individual import compute_info_individual, info_individual
     44 import bindings.scripts.compute_interfaces_info_overall
     45 from bindings.scripts.compute_interfaces_info_overall import compute_interfaces_info_overall, interfaces_info
     46 from bindings.scripts.idl_compiler import IdlCompilerV8
     47 
     48 
     49 PASS_MESSAGE = 'All tests PASS!'
     50 FAIL_MESSAGE = """Some tests FAIL!
     51 To update the reference files, execute:
     52     run-bindings-tests --reset-results
     53 
     54 If the failures are not due to your changes, test results may be out of sync;
     55 please rebaseline them in a separate CL, after checking that tests fail in ToT.
     56 In CL, please set:
     57 NOTRY=true
     58 TBR=(someone in Source/bindings/OWNERS or WATCHLISTS:bindings)
     59 """
     60 
     61 DEPENDENCY_IDL_FILES = frozenset([
     62     'TestImplements.idl',
     63     'TestImplements2.idl',
     64     'TestImplements3.idl',
     65     'TestPartialInterface.idl',
     66     'TestPartialInterface2.idl',
     67 ])
     68 
     69 
     70 test_input_directory = os.path.join(source_path, 'bindings', 'tests', 'idls')
     71 reference_directory = os.path.join(source_path, 'bindings', 'tests', 'results')
     72 
     73 
     74 @contextmanager
     75 def TemporaryDirectory():
     76     """Wrapper for tempfile.mkdtemp() so it's usable with 'with' statement.
     77 
     78     Simple backport of tempfile.TemporaryDirectory from Python 3.2.
     79     """
     80     name = tempfile.mkdtemp()
     81     try:
     82         yield name
     83     finally:
     84         shutil.rmtree(name)
     85 
     86 
     87 def generate_interface_dependencies():
     88     def idl_paths_recursive(directory):
     89         # This is slow, especially on Windows, due to os.walk making
     90         # excess stat() calls. Faster versions may appear in Python 3.5 or
     91         # later:
     92         # https://github.com/benhoyt/scandir
     93         # http://bugs.python.org/issue11406
     94         idl_paths = []
     95         for dirpath, _, files in os.walk(directory):
     96             idl_paths.extend(os.path.join(dirpath, filename)
     97                              for filename in fnmatch.filter(files, '*.idl'))
     98         return idl_paths
     99 
    100     # We compute interfaces info for *all* IDL files, not just test IDL
    101     # files, as code generator output depends on inheritance (both ancestor
    102     # chain and inherited extended attributes), and some real interfaces
    103     # are special-cased, such as Node.
    104     #
    105     # For example, when testing the behavior of interfaces that inherit
    106     # from Node, we also need to know that these inherit from EventTarget,
    107     # since this is also special-cased and Node inherits from EventTarget,
    108     # but this inheritance information requires computing dependencies for
    109     # the real Node.idl file.
    110 
    111     # 2-stage computation: individual, then overall
    112     #
    113     # Properly should compute separately by component (currently test
    114     # includes are invalid), but that's brittle (would need to update this file
    115     # for each new component) and doesn't test the code generator any better
    116     # than using a single component.
    117     for idl_filename in idl_paths_recursive(source_path):
    118         compute_info_individual(idl_filename, 'tests')
    119     info_individuals = [info_individual()]
    120     compute_interfaces_info_overall(info_individuals)
    121 
    122 
    123 def bindings_tests(output_directory, verbose):
    124     executive = Executive()
    125 
    126     def diff(filename1, filename2):
    127         # Python's difflib module is too slow, especially on long output, so
    128         # run external diff(1) command
    129         cmd = ['diff',
    130                '-u',  # unified format
    131                '-N',  # treat absent files as empty
    132                filename1,
    133                filename2]
    134         # Return output and don't raise exception, even though diff(1) has
    135         # non-zero exit if files differ.
    136         return executive.run_command(cmd, error_handler=lambda x: None)
    137 
    138     def delete_cache_files():
    139         # FIXME: Instead of deleting cache files, don't generate them.
    140         cache_files = [os.path.join(output_directory, output_file)
    141                        for output_file in os.listdir(output_directory)
    142                        if (output_file in ('lextab.py',  # PLY lex
    143                                            'lextab.pyc',
    144                                            'parsetab.pickle') or  # PLY yacc
    145                            output_file.endswith('.cache'))]  # Jinja
    146         for cache_file in cache_files:
    147             os.remove(cache_file)
    148 
    149     def identical_file(reference_filename, output_filename):
    150         reference_basename = os.path.basename(reference_filename)
    151 
    152         if not os.path.isfile(reference_filename):
    153             print 'Missing reference file!'
    154             print '(if adding new test, update reference files)'
    155             print reference_basename
    156             print
    157             return False
    158 
    159         if not filecmp.cmp(reference_filename, output_filename):
    160             # cmp is much faster than diff, and usual case is "no differance",
    161             # so only run diff if cmp detects a difference
    162             print 'FAIL: %s' % reference_basename
    163             print diff(reference_filename, output_filename)
    164             return False
    165 
    166         if verbose:
    167             print 'PASS: %s' % reference_basename
    168         return True
    169 
    170     def identical_output_files():
    171         file_pairs = [(os.path.join(reference_directory, output_file),
    172                        os.path.join(output_directory, output_file))
    173                       for output_file in os.listdir(output_directory)]
    174         return all([identical_file(reference_filename, output_filename)
    175                     for (reference_filename, output_filename) in file_pairs])
    176 
    177     def no_excess_files():
    178         generated_files = set(os.listdir(output_directory))
    179         generated_files.add('.svn')  # Subversion working copy directory
    180         excess_files = [output_file
    181                         for output_file in os.listdir(reference_directory)
    182                         if output_file not in generated_files]
    183         if excess_files:
    184             print ('Excess reference files! '
    185                   '(probably cruft from renaming or deleting):\n' +
    186                   '\n'.join(excess_files))
    187             return False
    188         return True
    189 
    190     try:
    191         generate_interface_dependencies()
    192         idl_compiler = IdlCompilerV8(output_directory,
    193                                      interfaces_info=interfaces_info,
    194                                      only_if_changed=True)
    195 
    196         idl_basenames = [filename
    197                          for filename in os.listdir(test_input_directory)
    198                          if (filename.endswith('.idl') and
    199                              # Dependencies aren't built
    200                              # (they are used by the dependent)
    201                              filename not in DEPENDENCY_IDL_FILES)]
    202         for idl_basename in idl_basenames:
    203             idl_path = os.path.realpath(
    204                 os.path.join(test_input_directory, idl_basename))
    205             idl_compiler.compile_file(idl_path)
    206             if verbose:
    207                 print 'Compiled: %s' % filename
    208     finally:
    209         delete_cache_files()
    210 
    211     # Detect all changes
    212     passed = identical_output_files()
    213     passed &= no_excess_files()
    214 
    215     if passed:
    216         if verbose:
    217             print
    218             print PASS_MESSAGE
    219         return 0
    220     print
    221     print FAIL_MESSAGE
    222     return 1
    223 
    224 
    225 def run_bindings_tests(reset_results, verbose):
    226     # Generate output into the reference directory if resetting results, or
    227     # a temp directory if not.
    228     if reset_results:
    229         print 'Resetting results'
    230         return bindings_tests(reference_directory, verbose)
    231     with TemporaryDirectory() as temp_dir:
    232         return bindings_tests(temp_dir, verbose)
    233