dom/system/gonk/tests/marionette/test_ril_code_quality.py

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6

     1 """
     2 The test performs the static code analysis check by JSHint.
     4 Target js files:
     5 - RILContentHelper.js
     6 - RadioInterfaceLayer.js
     7 - ril_worker.js
     8 - ril_consts.js
    10 If the js file contains the line of 'importScript()' (Ex: ril_worker.js), the
    11 test will perform a special merge step before excuting JSHint.
    13 Ex: Script A
    14 --------------------------------
    15 importScripts('Script B')
    16 ...
    17 --------------------------------
    19 We merge these two scripts into one by the following way.
    21 --------------------------------
    22 [Script B (ex: ril_consts.js)]
    23 (function(){ [Script A (ex: ril_worker.js)]
    24 })();
    25 --------------------------------
    27 Script A (ril_worker.js) runs global strict mode.
    28 Script B (ril_consts.js) not.
    30 The above merge way ensures the correct scope of 'strict mode.'
    31 """
    34 from marionette_test import MarionetteTestCase
    35 import bisect
    36 import inspect
    37 import os
    38 import os.path
    39 import re
    40 import unicodedata
    43 class StringUtility:
    45     """A collection of some string utilities."""
    47     @staticmethod
    48     def find_match_lines(lines, pattern):
    49         """Return a list of lines that contains given pattern."""
    50         return [line for line in lines if pattern in line]
    52     @staticmethod
    53     def remove_non_ascii(data):
    54         """Remove non ascii characters in data and return it as new string."""
    55         if type(data).__name__ == 'unicode':
    56             data = unicodedata.normalize(
    57                 'NFKD', data).encode('ascii', 'ignore')
    58         return data
    60     @staticmethod
    61     def auto_close(lines):
    62         """Ensure every line ends with '\n'."""
    63         if lines and not lines[-1].endswith('\n'):
    64             lines[-1] += '\n'
    65         return lines
    67     @staticmethod
    68     def auto_wrap_strict_mode(lines):
    69         """Wrap by function scope if lines contain 'use strict'."""
    70         if StringUtility.find_match_lines(lines, 'use strict'):
    71             lines[0] = '(function(){' + lines[0]
    72             lines.append('})();\n')
    73         return lines
    75     @staticmethod
    76     def get_imported_list(lines):
    77         """Get a list of imported items."""
    78         return [item
    79                 for line in StringUtility.find_match_lines(lines, 'importScripts')
    80                 for item in StringUtility._get_imported_list_from_line(line)]
    82     @staticmethod
    83     def _get_imported_list_from_line(line):
    84         """Extract all items from 'importScripts(...)'.
    86         importScripts("ril_consts.js", "systemlibs.js")
    87         => ['ril_consts', 'systemlibs.js']
    89         """
    90         pattern = re.compile(r'\s*importScripts\((.*)\)')
    91         m = pattern.match(line)
    92         if not m:
    93             raise Exception('Parse importScripts error.')
    94         return [name.translate(None, '\' "') for name in m.group(1).split(',')]
    97 class ResourceUriFileReader:
    99     """Handle the process of reading the source code from system."""
   101     URI_PREFIX = 'resource://gre/'
   102     URI_PATH = {
   103         'RILContentHelper.js':    'components/RILContentHelper.js',
   104         'RadioInterfaceLayer.js': 'components/RadioInterfaceLayer.js',
   105         'ril_worker.js':          'modules/ril_worker.js',
   106         'ril_consts.js':          'modules/ril_consts.js',
   107         'systemlibs.js':          'modules/systemlibs.js',
   108         'worker_buf.js':          'modules/workers/worker_buf.js',
   109     }
   111     CODE_OPEN_CHANNEL_BY_URI = '''
   112     var Cc = SpecialPowers.Cc;
   113     var Ci = SpecialPowers.Ci;
   114     var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
   115     global.uri = '%(uri)s';
   116     global.channel = ios.newChannel(global.uri, null, null);
   117     '''
   119     CODE_GET_SPEC = '''
   120     return global.channel.URI.spec;
   121     '''
   123     CODE_READ_CONTENT = '''
   124     var Cc = SpecialPowers.Cc;
   125     var Ci = SpecialPowers.Ci;
   127     var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
   128     var inputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream);
   130     var jaruri = global.channel.URI.QueryInterface(Ci.nsIJARURI);
   131     var file = jaruri.JARFile.QueryInterface(Ci.nsIFileURL).file;
   132     var entry = jaruri.JAREntry;
   133     zipReader.open(file);
   134     inputStream.init(zipReader.getInputStream(entry));
   135     var content = inputStream.read(inputStream.available());
   136     inputStream.close();
   137     zipReader.close();
   138     return content;
   139     '''
   141     @classmethod
   142     def get_uri(cls, filename):
   143         """Convert filename to URI in system."""
   144         if filename.startswith(cls.URI_PREFIX):
   145             return filename
   146         else:
   147             return cls.URI_PREFIX + cls.URI_PATH[filename]
   149     def __init__(self, marionette):
   150         self.runjs = lambda x: marionette.execute_script(x, new_sandbox=False)
   152     def read_file(self, filename):
   153         """Read file and return the contents as string."""
   154         content = self._read_uri(self.get_uri(filename))
   155         content = content.replace('"use strict";', '')
   156         return StringUtility.remove_non_ascii(content)
   158     def _read_uri(self, uri):
   159         """Read URI in system and return the contents as string."""
   160         # Open the uri as a channel.
   161         self.runjs(self.CODE_OPEN_CHANNEL_BY_URI % {'uri': uri})
   163         # Make sure spec is a jar uri, and not recursive.
   164         #   Ex: 'jar:file:///system/b2g/omni.ja!/modules/ril_worker.js'
   165         #
   166         # For simplicity, we don't handle other special cases in this test.
   167         # If B2G build system changes in the future, such as put the jar in
   168         # another jar, the test case will fail.
   169         spec = self.runjs(self.CODE_GET_SPEC)
   170         if (not spec.startswith('jar:file://')) or (spec.count('jar:') != 1):
   171             raise Exception('URI resolve error')
   173         # Read the content from channel.
   174         content = self.runjs(self.CODE_READ_CONTENT)
   175         return content
   178 class JSHintEngine:
   180     """Invoke jshint script on system."""
   182     CODE_INIT_JSHINT = '''
   183     %(script)s;
   184     global.JSHINT = JSHINT;
   185     global.options = JSON.parse(%(config_string)s);
   186     global.globals = global.options.globals;
   187     delete global.options.globals;
   188     '''
   190     CODE_RUN_JSHINT = '''
   191     global.script = %(code)s;
   192     return global.JSHINT(global.script, global.options, global.globals);
   193     '''
   195     CODE_GET_JSHINT_ERROR = '''
   196     return global.JSHINT.errors;
   197     '''
   199     def __init__(self, marionette, script, config):
   200         # Remove single line comment in config.
   201         config = '\n'.join([line.partition('//')[0]
   202                             for line in config.splitlines()])
   204         # Set global (JSHINT, options, global) in js environment.
   205         self.runjs = lambda x: marionette.execute_script(x, new_sandbox=False)
   206         self.runjs(self.CODE_INIT_JSHINT %
   207                    {'script': script, 'config_string': repr(config)})
   209     def run(self, code, filename=''):
   210         """Excute JShint check for the given code."""
   211         check_pass = self.runjs(self.CODE_RUN_JSHINT % {'code': repr(code)})
   212         errors = self.runjs(self.CODE_GET_JSHINT_ERROR)
   213         return check_pass, self._get_error_messages(errors, filename)
   215     def _get_error_messages(self, errors, filename=''):
   216         """
   217         Convert an error object to a list of readable string.
   219         [{"a": null, "c": null, "code": "W033", "d": null, "character": 6,
   220           "evidence": "var a", "raw": "Missing semicolon.",
   221           "reason": "Missing semicolon.", "b": null, "scope": "(main)", "line": 1,
   222           "id": "(error)"}]
   223         => line 1, col 6, Missing semicolon.
   225         """
   226         LINE, COL, REASON = u'line', u'character', u'reason'
   227         return ["%s: line %s, col %s, %s" %
   228                 (filename, error[LINE], error[COL], error[REASON])
   229                 for error in errors if error]
   232 class Linter:
   234     """Handle the linting related process."""
   236     def __init__(self, code_reader, jshint, reporter=None):
   237         """Set the linter with code_reader, jshint engine, and reporter.
   239         Should have following functionality.
   240         - code_reader.read_file(filename)
   241         - jshint.run(code, filename)
   242         - reporter([...])
   244         """
   245         self.code_reader = code_reader
   246         self.jshint = jshint
   247         if reporter is None:
   248             self.reporter = lambda x: '\n'.join(x)
   249         else:
   250             self.reporter = reporter
   252     def lint_file(self, filename):
   253         """Lint the file and return (pass, error_message)."""
   254         # Get code contents.
   255         code = self.code_reader.read_file(filename)
   256         lines = code.splitlines()
   257         import_list = StringUtility.get_imported_list(lines)
   258         if not import_list:
   259             check_pass, error_message = self.jshint.run(code, filename)
   260         else:
   261             newlines, info = self._merge_multiple_codes(filename, import_list)
   262             # Each line of |newlines| contains '\n'.
   263             check_pass, error_message = self.jshint.run(''.join(newlines))
   264             error_message = self._convert_merged_result(error_message, info)
   265             # Only keep errors for this file.
   266             error_message = [line for line in error_message
   267                              if line.startswith(filename)]
   268             check_pass = (len(error_message) == 0)
   269         return check_pass, self.reporter(error_message)
   271     def _merge_multiple_codes(self, filename, import_list):
   272         """Merge multiple codes from filename and import_list."""
   273         dirname, filename = os.path.split(filename)
   274         dst_line = 1
   275         dst_results = []
   276         info = []
   278         # Put the imported script first, and then the original script.
   279         for f in import_list + [filename]:
   280             filepath = os.path.join(dirname, f)
   282             # Maintain a mapping table.
   283             # New line number after merge => original file and line number.
   284             info.append((dst_line, filepath, 1))
   285             try:
   286                 code = self.code_reader.read_file(filepath)
   287                 lines = code.splitlines(True)  # Keep '\n'.
   288                 src_results = StringUtility.auto_wrap_strict_mode(
   289                     StringUtility.auto_close(lines))
   290                 dst_results.extend(src_results)
   291                 dst_line += len(src_results)
   292             except:
   293                 info.pop()
   294         return dst_results, info
   296     def _convert_merged_result(self, error_lines, line_info):
   297         pattern = re.compile(r'(.*): line (\d+),(.*)')
   298         start_line = [info[0] for info in line_info]
   299         new_result_lines = []
   300         for line in error_lines:
   301             m = pattern.match(line)
   302             if not m:
   303                 continue
   305             line_number, remain = int(m.group(2)), m.group(3)
   307             #  [1, 2, 7, 8]
   308             #            ^ for 7, pos = 3
   309             #         ^    for 6, pos = 2
   310             pos = bisect.bisect_right(start_line, line_number)
   311             dst_line, name, src_line = line_info[pos - 1]
   312             real_line_number = line_number - dst_line + src_line
   313             new_result_lines.append(
   314                 "%s: line %s,%s" % (name, real_line_number, remain))
   315         return new_result_lines
   318 class TestRILCodeQuality(MarionetteTestCase):
   320     JSHINT_PATH = 'ril_jshint/jshint.js'
   321     JSHINTRC_PATH = 'ril_jshint/jshintrc'
   323     def _read_local_file(self, filepath):
   324         """Read file content from local (folder of this test case)."""
   325         test_dir = os.path.dirname(inspect.getfile(TestRILCodeQuality))
   326         return open(os.path.join(test_dir, filepath)).read()
   328     def _get_extended_error_message(self, error_message):
   329         return '\n'.join(['See errors below and more information in Bug 880643',
   330                           '\n'.join(error_message),
   331                           'See errors above and more information in Bug 880643'])
   333     def _check(self, filename):
   334         check_pass, error_message = self.linter.lint_file(filename)
   335         self.assertTrue(check_pass, error_message)
   337     def setUp(self):
   338         MarionetteTestCase.setUp(self)
   339         self.linter = Linter(
   340             ResourceUriFileReader(self.marionette),
   341             JSHintEngine(self.marionette,
   342                          self._read_local_file(self.JSHINT_PATH),
   343                          self._read_local_file(self.JSHINTRC_PATH)),
   344             self._get_extended_error_message)
   346     def tearDown(self):
   347         MarionetteTestCase.tearDown(self)
   349     def test_RILContentHelper(self):
   350         self._check('RILContentHelper.js')
   352     def test_RadioInterfaceLayer(self):
   353         self._check('RadioInterfaceLayer.js')
   355     # Bug 936504. Disable the test for 'ril_worker.js'. It sometimes runs very
   356     # slow and causes the timeout fail on try server.
   357     #def test_ril_worker(self):
   358     #    self._check('ril_worker.js')
   360     def test_ril_consts(self):
   361         self._check('ril_consts.js')
   363     def test_worker_buf(self):
   364         self._check('worker_buf.js')

mercurial