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