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

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/dom/system/gonk/tests/marionette/test_ril_code_quality.py	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,364 @@
     1.4 +"""
     1.5 +The test performs the static code analysis check by JSHint.
     1.6 +
     1.7 +Target js files:
     1.8 +- RILContentHelper.js
     1.9 +- RadioInterfaceLayer.js
    1.10 +- ril_worker.js
    1.11 +- ril_consts.js
    1.12 +
    1.13 +If the js file contains the line of 'importScript()' (Ex: ril_worker.js), the
    1.14 +test will perform a special merge step before excuting JSHint.
    1.15 +
    1.16 +Ex: Script A
    1.17 +--------------------------------
    1.18 +importScripts('Script B')
    1.19 +...
    1.20 +--------------------------------
    1.21 +
    1.22 +We merge these two scripts into one by the following way.
    1.23 +
    1.24 +--------------------------------
    1.25 +[Script B (ex: ril_consts.js)]
    1.26 +(function(){ [Script A (ex: ril_worker.js)]
    1.27 +})();
    1.28 +--------------------------------
    1.29 +
    1.30 +Script A (ril_worker.js) runs global strict mode.
    1.31 +Script B (ril_consts.js) not.
    1.32 +
    1.33 +The above merge way ensures the correct scope of 'strict mode.'
    1.34 +"""
    1.35 +
    1.36 +
    1.37 +from marionette_test import MarionetteTestCase
    1.38 +import bisect
    1.39 +import inspect
    1.40 +import os
    1.41 +import os.path
    1.42 +import re
    1.43 +import unicodedata
    1.44 +
    1.45 +
    1.46 +class StringUtility:
    1.47 +
    1.48 +    """A collection of some string utilities."""
    1.49 +
    1.50 +    @staticmethod
    1.51 +    def find_match_lines(lines, pattern):
    1.52 +        """Return a list of lines that contains given pattern."""
    1.53 +        return [line for line in lines if pattern in line]
    1.54 +
    1.55 +    @staticmethod
    1.56 +    def remove_non_ascii(data):
    1.57 +        """Remove non ascii characters in data and return it as new string."""
    1.58 +        if type(data).__name__ == 'unicode':
    1.59 +            data = unicodedata.normalize(
    1.60 +                'NFKD', data).encode('ascii', 'ignore')
    1.61 +        return data
    1.62 +
    1.63 +    @staticmethod
    1.64 +    def auto_close(lines):
    1.65 +        """Ensure every line ends with '\n'."""
    1.66 +        if lines and not lines[-1].endswith('\n'):
    1.67 +            lines[-1] += '\n'
    1.68 +        return lines
    1.69 +
    1.70 +    @staticmethod
    1.71 +    def auto_wrap_strict_mode(lines):
    1.72 +        """Wrap by function scope if lines contain 'use strict'."""
    1.73 +        if StringUtility.find_match_lines(lines, 'use strict'):
    1.74 +            lines[0] = '(function(){' + lines[0]
    1.75 +            lines.append('})();\n')
    1.76 +        return lines
    1.77 +
    1.78 +    @staticmethod
    1.79 +    def get_imported_list(lines):
    1.80 +        """Get a list of imported items."""
    1.81 +        return [item
    1.82 +                for line in StringUtility.find_match_lines(lines, 'importScripts')
    1.83 +                for item in StringUtility._get_imported_list_from_line(line)]
    1.84 +
    1.85 +    @staticmethod
    1.86 +    def _get_imported_list_from_line(line):
    1.87 +        """Extract all items from 'importScripts(...)'.
    1.88 +
    1.89 +        importScripts("ril_consts.js", "systemlibs.js")
    1.90 +        => ['ril_consts', 'systemlibs.js']
    1.91 +
    1.92 +        """
    1.93 +        pattern = re.compile(r'\s*importScripts\((.*)\)')
    1.94 +        m = pattern.match(line)
    1.95 +        if not m:
    1.96 +            raise Exception('Parse importScripts error.')
    1.97 +        return [name.translate(None, '\' "') for name in m.group(1).split(',')]
    1.98 +
    1.99 +
   1.100 +class ResourceUriFileReader:
   1.101 +
   1.102 +    """Handle the process of reading the source code from system."""
   1.103 +
   1.104 +    URI_PREFIX = 'resource://gre/'
   1.105 +    URI_PATH = {
   1.106 +        'RILContentHelper.js':    'components/RILContentHelper.js',
   1.107 +        'RadioInterfaceLayer.js': 'components/RadioInterfaceLayer.js',
   1.108 +        'ril_worker.js':          'modules/ril_worker.js',
   1.109 +        'ril_consts.js':          'modules/ril_consts.js',
   1.110 +        'systemlibs.js':          'modules/systemlibs.js',
   1.111 +        'worker_buf.js':          'modules/workers/worker_buf.js',
   1.112 +    }
   1.113 +
   1.114 +    CODE_OPEN_CHANNEL_BY_URI = '''
   1.115 +    var Cc = SpecialPowers.Cc;
   1.116 +    var Ci = SpecialPowers.Ci;
   1.117 +    var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
   1.118 +    global.uri = '%(uri)s';
   1.119 +    global.channel = ios.newChannel(global.uri, null, null);
   1.120 +    '''
   1.121 +
   1.122 +    CODE_GET_SPEC = '''
   1.123 +    return global.channel.URI.spec;
   1.124 +    '''
   1.125 +
   1.126 +    CODE_READ_CONTENT = '''
   1.127 +    var Cc = SpecialPowers.Cc;
   1.128 +    var Ci = SpecialPowers.Ci;
   1.129 +
   1.130 +    var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
   1.131 +    var inputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream);
   1.132 +
   1.133 +    var jaruri = global.channel.URI.QueryInterface(Ci.nsIJARURI);
   1.134 +    var file = jaruri.JARFile.QueryInterface(Ci.nsIFileURL).file;
   1.135 +    var entry = jaruri.JAREntry;
   1.136 +    zipReader.open(file);
   1.137 +    inputStream.init(zipReader.getInputStream(entry));
   1.138 +    var content = inputStream.read(inputStream.available());
   1.139 +    inputStream.close();
   1.140 +    zipReader.close();
   1.141 +    return content;
   1.142 +    '''
   1.143 +
   1.144 +    @classmethod
   1.145 +    def get_uri(cls, filename):
   1.146 +        """Convert filename to URI in system."""
   1.147 +        if filename.startswith(cls.URI_PREFIX):
   1.148 +            return filename
   1.149 +        else:
   1.150 +            return cls.URI_PREFIX + cls.URI_PATH[filename]
   1.151 +
   1.152 +    def __init__(self, marionette):
   1.153 +        self.runjs = lambda x: marionette.execute_script(x, new_sandbox=False)
   1.154 +
   1.155 +    def read_file(self, filename):
   1.156 +        """Read file and return the contents as string."""
   1.157 +        content = self._read_uri(self.get_uri(filename))
   1.158 +        content = content.replace('"use strict";', '')
   1.159 +        return StringUtility.remove_non_ascii(content)
   1.160 +
   1.161 +    def _read_uri(self, uri):
   1.162 +        """Read URI in system and return the contents as string."""
   1.163 +        # Open the uri as a channel.
   1.164 +        self.runjs(self.CODE_OPEN_CHANNEL_BY_URI % {'uri': uri})
   1.165 +
   1.166 +        # Make sure spec is a jar uri, and not recursive.
   1.167 +        #   Ex: 'jar:file:///system/b2g/omni.ja!/modules/ril_worker.js'
   1.168 +        #
   1.169 +        # For simplicity, we don't handle other special cases in this test.
   1.170 +        # If B2G build system changes in the future, such as put the jar in
   1.171 +        # another jar, the test case will fail.
   1.172 +        spec = self.runjs(self.CODE_GET_SPEC)
   1.173 +        if (not spec.startswith('jar:file://')) or (spec.count('jar:') != 1):
   1.174 +            raise Exception('URI resolve error')
   1.175 +
   1.176 +        # Read the content from channel.
   1.177 +        content = self.runjs(self.CODE_READ_CONTENT)
   1.178 +        return content
   1.179 +
   1.180 +
   1.181 +class JSHintEngine:
   1.182 +
   1.183 +    """Invoke jshint script on system."""
   1.184 +
   1.185 +    CODE_INIT_JSHINT = '''
   1.186 +    %(script)s;
   1.187 +    global.JSHINT = JSHINT;
   1.188 +    global.options = JSON.parse(%(config_string)s);
   1.189 +    global.globals = global.options.globals;
   1.190 +    delete global.options.globals;
   1.191 +    '''
   1.192 +
   1.193 +    CODE_RUN_JSHINT = '''
   1.194 +    global.script = %(code)s;
   1.195 +    return global.JSHINT(global.script, global.options, global.globals);
   1.196 +    '''
   1.197 +
   1.198 +    CODE_GET_JSHINT_ERROR = '''
   1.199 +    return global.JSHINT.errors;
   1.200 +    '''
   1.201 +
   1.202 +    def __init__(self, marionette, script, config):
   1.203 +        # Remove single line comment in config.
   1.204 +        config = '\n'.join([line.partition('//')[0]
   1.205 +                            for line in config.splitlines()])
   1.206 +
   1.207 +        # Set global (JSHINT, options, global) in js environment.
   1.208 +        self.runjs = lambda x: marionette.execute_script(x, new_sandbox=False)
   1.209 +        self.runjs(self.CODE_INIT_JSHINT %
   1.210 +                   {'script': script, 'config_string': repr(config)})
   1.211 +
   1.212 +    def run(self, code, filename=''):
   1.213 +        """Excute JShint check for the given code."""
   1.214 +        check_pass = self.runjs(self.CODE_RUN_JSHINT % {'code': repr(code)})
   1.215 +        errors = self.runjs(self.CODE_GET_JSHINT_ERROR)
   1.216 +        return check_pass, self._get_error_messages(errors, filename)
   1.217 +
   1.218 +    def _get_error_messages(self, errors, filename=''):
   1.219 +        """
   1.220 +        Convert an error object to a list of readable string.
   1.221 +
   1.222 +        [{"a": null, "c": null, "code": "W033", "d": null, "character": 6,
   1.223 +          "evidence": "var a", "raw": "Missing semicolon.",
   1.224 +          "reason": "Missing semicolon.", "b": null, "scope": "(main)", "line": 1,
   1.225 +          "id": "(error)"}]
   1.226 +        => line 1, col 6, Missing semicolon.
   1.227 +
   1.228 +        """
   1.229 +        LINE, COL, REASON = u'line', u'character', u'reason'
   1.230 +        return ["%s: line %s, col %s, %s" %
   1.231 +                (filename, error[LINE], error[COL], error[REASON])
   1.232 +                for error in errors if error]
   1.233 +
   1.234 +
   1.235 +class Linter:
   1.236 +
   1.237 +    """Handle the linting related process."""
   1.238 +
   1.239 +    def __init__(self, code_reader, jshint, reporter=None):
   1.240 +        """Set the linter with code_reader, jshint engine, and reporter.
   1.241 +
   1.242 +        Should have following functionality.
   1.243 +        - code_reader.read_file(filename)
   1.244 +        - jshint.run(code, filename)
   1.245 +        - reporter([...])
   1.246 +
   1.247 +        """
   1.248 +        self.code_reader = code_reader
   1.249 +        self.jshint = jshint
   1.250 +        if reporter is None:
   1.251 +            self.reporter = lambda x: '\n'.join(x)
   1.252 +        else:
   1.253 +            self.reporter = reporter
   1.254 +
   1.255 +    def lint_file(self, filename):
   1.256 +        """Lint the file and return (pass, error_message)."""
   1.257 +        # Get code contents.
   1.258 +        code = self.code_reader.read_file(filename)
   1.259 +        lines = code.splitlines()
   1.260 +        import_list = StringUtility.get_imported_list(lines)
   1.261 +        if not import_list:
   1.262 +            check_pass, error_message = self.jshint.run(code, filename)
   1.263 +        else:
   1.264 +            newlines, info = self._merge_multiple_codes(filename, import_list)
   1.265 +            # Each line of |newlines| contains '\n'.
   1.266 +            check_pass, error_message = self.jshint.run(''.join(newlines))
   1.267 +            error_message = self._convert_merged_result(error_message, info)
   1.268 +            # Only keep errors for this file.
   1.269 +            error_message = [line for line in error_message
   1.270 +                             if line.startswith(filename)]
   1.271 +            check_pass = (len(error_message) == 0)
   1.272 +        return check_pass, self.reporter(error_message)
   1.273 +
   1.274 +    def _merge_multiple_codes(self, filename, import_list):
   1.275 +        """Merge multiple codes from filename and import_list."""
   1.276 +        dirname, filename = os.path.split(filename)
   1.277 +        dst_line = 1
   1.278 +        dst_results = []
   1.279 +        info = []
   1.280 +
   1.281 +        # Put the imported script first, and then the original script.
   1.282 +        for f in import_list + [filename]:
   1.283 +            filepath = os.path.join(dirname, f)
   1.284 +
   1.285 +            # Maintain a mapping table.
   1.286 +            # New line number after merge => original file and line number.
   1.287 +            info.append((dst_line, filepath, 1))
   1.288 +            try:
   1.289 +                code = self.code_reader.read_file(filepath)
   1.290 +                lines = code.splitlines(True)  # Keep '\n'.
   1.291 +                src_results = StringUtility.auto_wrap_strict_mode(
   1.292 +                    StringUtility.auto_close(lines))
   1.293 +                dst_results.extend(src_results)
   1.294 +                dst_line += len(src_results)
   1.295 +            except:
   1.296 +                info.pop()
   1.297 +        return dst_results, info
   1.298 +
   1.299 +    def _convert_merged_result(self, error_lines, line_info):
   1.300 +        pattern = re.compile(r'(.*): line (\d+),(.*)')
   1.301 +        start_line = [info[0] for info in line_info]
   1.302 +        new_result_lines = []
   1.303 +        for line in error_lines:
   1.304 +            m = pattern.match(line)
   1.305 +            if not m:
   1.306 +                continue
   1.307 +
   1.308 +            line_number, remain = int(m.group(2)), m.group(3)
   1.309 +
   1.310 +            #  [1, 2, 7, 8]
   1.311 +            #            ^ for 7, pos = 3
   1.312 +            #         ^    for 6, pos = 2
   1.313 +            pos = bisect.bisect_right(start_line, line_number)
   1.314 +            dst_line, name, src_line = line_info[pos - 1]
   1.315 +            real_line_number = line_number - dst_line + src_line
   1.316 +            new_result_lines.append(
   1.317 +                "%s: line %s,%s" % (name, real_line_number, remain))
   1.318 +        return new_result_lines
   1.319 +
   1.320 +
   1.321 +class TestRILCodeQuality(MarionetteTestCase):
   1.322 +
   1.323 +    JSHINT_PATH = 'ril_jshint/jshint.js'
   1.324 +    JSHINTRC_PATH = 'ril_jshint/jshintrc'
   1.325 +
   1.326 +    def _read_local_file(self, filepath):
   1.327 +        """Read file content from local (folder of this test case)."""
   1.328 +        test_dir = os.path.dirname(inspect.getfile(TestRILCodeQuality))
   1.329 +        return open(os.path.join(test_dir, filepath)).read()
   1.330 +
   1.331 +    def _get_extended_error_message(self, error_message):
   1.332 +        return '\n'.join(['See errors below and more information in Bug 880643',
   1.333 +                          '\n'.join(error_message),
   1.334 +                          'See errors above and more information in Bug 880643'])
   1.335 +
   1.336 +    def _check(self, filename):
   1.337 +        check_pass, error_message = self.linter.lint_file(filename)
   1.338 +        self.assertTrue(check_pass, error_message)
   1.339 +
   1.340 +    def setUp(self):
   1.341 +        MarionetteTestCase.setUp(self)
   1.342 +        self.linter = Linter(
   1.343 +            ResourceUriFileReader(self.marionette),
   1.344 +            JSHintEngine(self.marionette,
   1.345 +                         self._read_local_file(self.JSHINT_PATH),
   1.346 +                         self._read_local_file(self.JSHINTRC_PATH)),
   1.347 +            self._get_extended_error_message)
   1.348 +
   1.349 +    def tearDown(self):
   1.350 +        MarionetteTestCase.tearDown(self)
   1.351 +
   1.352 +    def test_RILContentHelper(self):
   1.353 +        self._check('RILContentHelper.js')
   1.354 +
   1.355 +    def test_RadioInterfaceLayer(self):
   1.356 +        self._check('RadioInterfaceLayer.js')
   1.357 +
   1.358 +    # Bug 936504. Disable the test for 'ril_worker.js'. It sometimes runs very
   1.359 +    # slow and causes the timeout fail on try server.
   1.360 +    #def test_ril_worker(self):
   1.361 +    #    self._check('ril_worker.js')
   1.362 +
   1.363 +    def test_ril_consts(self):
   1.364 +        self._check('ril_consts.js')
   1.365 +
   1.366 +    def test_worker_buf(self):
   1.367 +        self._check('worker_buf.js')

mercurial