""" The test performs the static code analysis check by JSHint. Target js files: - RadioInterfaceLayer.js - ril_worker.js - ril_consts.js If the js file contains the line of 'importScript()' (Ex: ril_worker.js), the test will perform a special merge step before excuting JSHint. Ex: Script A -------------------------------- importScripts('Script B') ... -------------------------------- We merge these two scripts into one by the following way. -------------------------------- [Script B (ex: ril_consts.js)] (function(){ [Script A (ex: ril_worker.js)] })(); -------------------------------- Script A (ril_worker.js) runs global strict mode. Script B (ril_consts.js) not. The above merge way ensures the correct scope of 'strict mode.' """ import bisect import inspect import os import os.path import re import unicodedata from marionette_harness import MarionetteTestCase class StringUtility: """A collection of some string utilities.""" @staticmethod def find_match_lines(lines, pattern): """Return a list of lines that contains given pattern.""" return [line for line in lines if pattern in line] @staticmethod def remove_non_ascii(data): """Remove non ascii characters in data and return it as new string.""" if type(data).__name__ == 'unicode': data = unicodedata.normalize( 'NFKD', data).encode('ascii', 'ignore') return data @staticmethod def auto_close(lines): """Ensure every line ends with '\n'.""" if lines and not lines[-1].endswith('\n'): lines[-1] += '\n' return lines @staticmethod def auto_wrap_strict_mode(lines): """Wrap by function scope if lines contain 'use strict'.""" if StringUtility.find_match_lines(lines, 'use strict'): lines[0] = '(function(){' + lines[0] lines.append('})();\n') return lines @staticmethod def get_imported_list(lines): """Get a list of imported items.""" return [item for line in StringUtility.find_match_lines(lines, 'importScripts') for item in StringUtility._get_imported_list_from_line(line)] @staticmethod def _get_imported_list_from_line(line): """Extract all items from 'importScripts(...)'. importScripts("ril_consts.js", "systemlibs.js") => ['ril_consts', 'systemlibs.js'] """ pattern = re.compile(r'\s*importScripts\((.*)\)') m = pattern.match(line) if not m: raise Exception('Parse importScripts error.') return [name.translate(None, '\' "') for name in m.group(1).split(',')] class ResourceUriFileReader: """Handle the process of reading the source code from system.""" URI_PREFIX = 'resource://gre/' URI_PATH = { 'RadioInterfaceLayer.js': 'components/RadioInterfaceLayer.js', 'ril_worker.js': 'modules/ril_worker.js', 'ril_consts.js': 'modules/ril_consts.js', 'systemlibs.js': 'modules/systemlibs.js', 'worker_buf.js': 'modules/workers/worker_buf.js', } CODE_OPEN_CHANNEL_BY_URI = ''' var Cc = Components.classes; var Ci = Components.interfaces; var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager); global.uri = '%(uri)s'; global.channel = ios.newChannel2(global.uri, null, null, null, // aLoadingNode secMan.getSystemPrincipal(), null, // aTriggeringPrincipal Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL, Ci.nsIContentPolicy.TYPE_OTHER); ''' CODE_GET_SPEC = ''' return global.channel.URI.spec; ''' CODE_READ_CONTENT = ''' var Cc = Components.classes; var Ci = Components.interfaces; var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader); var inputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream); var jaruri = global.channel.URI.QueryInterface(Ci.nsIJARURI); var file = jaruri.JARFile.QueryInterface(Ci.nsIFileURL).file; var entry = jaruri.JAREntry; zipReader.open(file); inputStream.init(zipReader.getInputStream(entry)); var content = inputStream.read(inputStream.available()); inputStream.close(); zipReader.close(); return content; ''' @classmethod def get_uri(cls, filename): """Convert filename to URI in system.""" if filename.startswith(cls.URI_PREFIX): return filename else: return cls.URI_PREFIX + cls.URI_PATH[filename] def __init__(self, marionette): self.runjs = lambda x: marionette.execute_script(x, new_sandbox=False, sandbox='system') def read_file(self, filename): """Read file and return the contents as string.""" content = self._read_uri(self.get_uri(filename)) content = content.replace('"use strict";', '') return StringUtility.remove_non_ascii(content) def _read_uri(self, uri): """Read URI in system and return the contents as string.""" # Open the uri as a channel. self.runjs(self.CODE_OPEN_CHANNEL_BY_URI % {'uri': uri}) # Make sure spec is a jar uri, and not recursive. # Ex: 'jar:file:///system/b2g/omni.ja!/modules/ril_worker.js' # # For simplicity, we don't handle other special cases in this test. # If B2G build system changes in the future, such as put the jar in # another jar, the test case will fail. spec = self.runjs(self.CODE_GET_SPEC) if (not spec.startswith('jar:file://')) or (spec.count('jar:') != 1): raise Exception('URI resolve error') # Read the content from channel. content = self.runjs(self.CODE_READ_CONTENT) return content class JSHintEngine: """Invoke jshint script on system.""" CODE_INIT_JSHINT = ''' %(script)s; global.JSHINT = JSHINT; global.options = JSON.parse(%(config_string)s); global.globals = global.options.globals; delete global.options.globals; ''' CODE_RUN_JSHINT = ''' global.script = %(code)s; return global.JSHINT(global.script, global.options, global.globals); ''' CODE_GET_JSHINT_ERROR = ''' return global.JSHINT.errors; ''' def __init__(self, marionette, script, config): # Remove single line comment in config. config = '\n'.join([line.partition('//')[0] for line in config.splitlines()]) # Set global (JSHINT, options, global) in js environment. self.runjs = lambda x: marionette.execute_script(x, new_sandbox=False, sandbox='system') self.runjs(self.CODE_INIT_JSHINT % {'script': script, 'config_string': repr(config)}) def run(self, code, filename=''): """Excute JShint check for the given code.""" check_pass = self.runjs(self.CODE_RUN_JSHINT % {'code': repr(code)}) errors = self.runjs(self.CODE_GET_JSHINT_ERROR) return check_pass, self._get_error_messages(errors, filename) def _get_error_messages(self, errors, filename=''): """ Convert an error object to a list of readable string. [{"a": null, "c": null, "code": "W033", "d": null, "character": 6, "evidence": "var a", "raw": "Missing semicolon.", "reason": "Missing semicolon.", "b": null, "scope": "(main)", "line": 1, "id": "(error)"}] => line 1, col 6, Missing semicolon. """ LINE, COL, REASON = u'line', u'character', u'reason' return ["%s: line %s, col %s, %s" % (filename, error[LINE], error[COL], error[REASON]) for error in errors if error] class Linter: """Handle the linting related process.""" def __init__(self, code_reader, jshint, reporter=None): """Set the linter with code_reader, jshint engine, and reporter. Should have following functionality. - code_reader.read_file(filename) - jshint.run(code, filename) - reporter([...]) """ self.code_reader = code_reader self.jshint = jshint if reporter is None: self.reporter = lambda x: '\n'.join(x) else: self.reporter = reporter def lint_file(self, filename): """Lint the file and return (pass, error_message).""" # Get code contents. code = self.code_reader.read_file(filename) lines = code.splitlines() import_list = StringUtility.get_imported_list(lines) if not import_list: check_pass, error_message = self.jshint.run(code, filename) else: newlines, info = self._merge_multiple_codes(filename, import_list) # Each line of |newlines| contains '\n'. check_pass, error_message = self.jshint.run(''.join(newlines)) error_message = self._convert_merged_result(error_message, info) # Only keep errors for this file. error_message = [line for line in error_message if line.startswith(filename)] check_pass = (len(error_message) == 0) return check_pass, self.reporter(error_message) def _merge_multiple_codes(self, filename, import_list): """Merge multiple codes from filename and import_list.""" dirname, filename = os.path.split(filename) dst_line = 1 dst_results = [] info = [] # Put the imported script first, and then the original script. for f in import_list + [filename]: filepath = os.path.join(dirname, f) # Maintain a mapping table. # New line number after merge => original file and line number. info.append((dst_line, filepath, 1)) try: code = self.code_reader.read_file(filepath) lines = code.splitlines(True) # Keep '\n'. src_results = StringUtility.auto_wrap_strict_mode( StringUtility.auto_close(lines)) dst_results.extend(src_results) dst_line += len(src_results) except: info.pop() return dst_results, info def _convert_merged_result(self, error_lines, line_info): pattern = re.compile(r'(.*): line (\d+),(.*)') start_line = [info[0] for info in line_info] new_result_lines = [] for line in error_lines: m = pattern.match(line) if not m: continue line_number, remain = int(m.group(2)), m.group(3) # [1, 2, 7, 8] # ^ for 7, pos = 3 # ^ for 6, pos = 2 pos = bisect.bisect_right(start_line, line_number) dst_line, name, src_line = line_info[pos - 1] real_line_number = line_number - dst_line + src_line new_result_lines.append( "%s: line %s,%s" % (name, real_line_number, remain)) return new_result_lines class TestRILCodeQuality(MarionetteTestCase): JSHINT_PATH = 'ril_jshint/jshint.js' JSHINTRC_PATH = 'ril_jshint/jshintrc' def _read_local_file(self, filepath): """Read file content from local (folder of this test case).""" test_dir = os.path.dirname(inspect.getfile(TestRILCodeQuality)) return open(os.path.join(test_dir, filepath)).read() def _get_extended_error_message(self, error_message): return '\n'.join(['See errors below and more information in Bug 880643', '\n'.join(error_message), 'See errors above and more information in Bug 880643']) def _check(self, filename): check_pass, error_message = self.linter.lint_file(filename) self.assertTrue(check_pass, error_message) def setUp(self): MarionetteTestCase.setUp(self) self.linter = Linter( ResourceUriFileReader(self.marionette), JSHintEngine(self.marionette, self._read_local_file(self.JSHINT_PATH), self._read_local_file(self.JSHINTRC_PATH)), self._get_extended_error_message) def tearDown(self): MarionetteTestCase.tearDown(self) def test_RadioInterfaceLayer(self): self._check('RadioInterfaceLayer.js') # Bug 936504. Disable the test for 'ril_worker.js'. It sometimes runs very # slow and causes the timeout fail on try server. #def test_ril_worker(self): # self._check('ril_worker.js') def test_ril_consts(self): self._check('ril_consts.js') def test_worker_buf(self): self._check('worker_buf.js')