summaryrefslogtreecommitdiffstats
path: root/dom/system/gonk/tests/marionette/test_ril_code_quality.py
diff options
context:
space:
mode:
Diffstat (limited to 'dom/system/gonk/tests/marionette/test_ril_code_quality.py')
-rw-r--r--dom/system/gonk/tests/marionette/test_ril_code_quality.py371
1 files changed, 371 insertions, 0 deletions
diff --git a/dom/system/gonk/tests/marionette/test_ril_code_quality.py b/dom/system/gonk/tests/marionette/test_ril_code_quality.py
new file mode 100644
index 000000000..d741d8a2e
--- /dev/null
+++ b/dom/system/gonk/tests/marionette/test_ril_code_quality.py
@@ -0,0 +1,371 @@
+"""
+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')