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')