Thu, 22 Jan 2015 13:21:57 +0100
Incorporate requested changes from Mozilla in review:
https://bugzilla.mozilla.org/show_bug.cgi?id=1123480#c6
michael@0 | 1 | """ |
michael@0 | 2 | The test performs the static code analysis check by JSHint. |
michael@0 | 3 | |
michael@0 | 4 | Target js files: |
michael@0 | 5 | - RILContentHelper.js |
michael@0 | 6 | - RadioInterfaceLayer.js |
michael@0 | 7 | - ril_worker.js |
michael@0 | 8 | - ril_consts.js |
michael@0 | 9 | |
michael@0 | 10 | If the js file contains the line of 'importScript()' (Ex: ril_worker.js), the |
michael@0 | 11 | test will perform a special merge step before excuting JSHint. |
michael@0 | 12 | |
michael@0 | 13 | Ex: Script A |
michael@0 | 14 | -------------------------------- |
michael@0 | 15 | importScripts('Script B') |
michael@0 | 16 | ... |
michael@0 | 17 | -------------------------------- |
michael@0 | 18 | |
michael@0 | 19 | We merge these two scripts into one by the following way. |
michael@0 | 20 | |
michael@0 | 21 | -------------------------------- |
michael@0 | 22 | [Script B (ex: ril_consts.js)] |
michael@0 | 23 | (function(){ [Script A (ex: ril_worker.js)] |
michael@0 | 24 | })(); |
michael@0 | 25 | -------------------------------- |
michael@0 | 26 | |
michael@0 | 27 | Script A (ril_worker.js) runs global strict mode. |
michael@0 | 28 | Script B (ril_consts.js) not. |
michael@0 | 29 | |
michael@0 | 30 | The above merge way ensures the correct scope of 'strict mode.' |
michael@0 | 31 | """ |
michael@0 | 32 | |
michael@0 | 33 | |
michael@0 | 34 | from marionette_test import MarionetteTestCase |
michael@0 | 35 | import bisect |
michael@0 | 36 | import inspect |
michael@0 | 37 | import os |
michael@0 | 38 | import os.path |
michael@0 | 39 | import re |
michael@0 | 40 | import unicodedata |
michael@0 | 41 | |
michael@0 | 42 | |
michael@0 | 43 | class StringUtility: |
michael@0 | 44 | |
michael@0 | 45 | """A collection of some string utilities.""" |
michael@0 | 46 | |
michael@0 | 47 | @staticmethod |
michael@0 | 48 | def find_match_lines(lines, pattern): |
michael@0 | 49 | """Return a list of lines that contains given pattern.""" |
michael@0 | 50 | return [line for line in lines if pattern in line] |
michael@0 | 51 | |
michael@0 | 52 | @staticmethod |
michael@0 | 53 | def remove_non_ascii(data): |
michael@0 | 54 | """Remove non ascii characters in data and return it as new string.""" |
michael@0 | 55 | if type(data).__name__ == 'unicode': |
michael@0 | 56 | data = unicodedata.normalize( |
michael@0 | 57 | 'NFKD', data).encode('ascii', 'ignore') |
michael@0 | 58 | return data |
michael@0 | 59 | |
michael@0 | 60 | @staticmethod |
michael@0 | 61 | def auto_close(lines): |
michael@0 | 62 | """Ensure every line ends with '\n'.""" |
michael@0 | 63 | if lines and not lines[-1].endswith('\n'): |
michael@0 | 64 | lines[-1] += '\n' |
michael@0 | 65 | return lines |
michael@0 | 66 | |
michael@0 | 67 | @staticmethod |
michael@0 | 68 | def auto_wrap_strict_mode(lines): |
michael@0 | 69 | """Wrap by function scope if lines contain 'use strict'.""" |
michael@0 | 70 | if StringUtility.find_match_lines(lines, 'use strict'): |
michael@0 | 71 | lines[0] = '(function(){' + lines[0] |
michael@0 | 72 | lines.append('})();\n') |
michael@0 | 73 | return lines |
michael@0 | 74 | |
michael@0 | 75 | @staticmethod |
michael@0 | 76 | def get_imported_list(lines): |
michael@0 | 77 | """Get a list of imported items.""" |
michael@0 | 78 | return [item |
michael@0 | 79 | for line in StringUtility.find_match_lines(lines, 'importScripts') |
michael@0 | 80 | for item in StringUtility._get_imported_list_from_line(line)] |
michael@0 | 81 | |
michael@0 | 82 | @staticmethod |
michael@0 | 83 | def _get_imported_list_from_line(line): |
michael@0 | 84 | """Extract all items from 'importScripts(...)'. |
michael@0 | 85 | |
michael@0 | 86 | importScripts("ril_consts.js", "systemlibs.js") |
michael@0 | 87 | => ['ril_consts', 'systemlibs.js'] |
michael@0 | 88 | |
michael@0 | 89 | """ |
michael@0 | 90 | pattern = re.compile(r'\s*importScripts\((.*)\)') |
michael@0 | 91 | m = pattern.match(line) |
michael@0 | 92 | if not m: |
michael@0 | 93 | raise Exception('Parse importScripts error.') |
michael@0 | 94 | return [name.translate(None, '\' "') for name in m.group(1).split(',')] |
michael@0 | 95 | |
michael@0 | 96 | |
michael@0 | 97 | class ResourceUriFileReader: |
michael@0 | 98 | |
michael@0 | 99 | """Handle the process of reading the source code from system.""" |
michael@0 | 100 | |
michael@0 | 101 | URI_PREFIX = 'resource://gre/' |
michael@0 | 102 | URI_PATH = { |
michael@0 | 103 | 'RILContentHelper.js': 'components/RILContentHelper.js', |
michael@0 | 104 | 'RadioInterfaceLayer.js': 'components/RadioInterfaceLayer.js', |
michael@0 | 105 | 'ril_worker.js': 'modules/ril_worker.js', |
michael@0 | 106 | 'ril_consts.js': 'modules/ril_consts.js', |
michael@0 | 107 | 'systemlibs.js': 'modules/systemlibs.js', |
michael@0 | 108 | 'worker_buf.js': 'modules/workers/worker_buf.js', |
michael@0 | 109 | } |
michael@0 | 110 | |
michael@0 | 111 | CODE_OPEN_CHANNEL_BY_URI = ''' |
michael@0 | 112 | var Cc = SpecialPowers.Cc; |
michael@0 | 113 | var Ci = SpecialPowers.Ci; |
michael@0 | 114 | var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService); |
michael@0 | 115 | global.uri = '%(uri)s'; |
michael@0 | 116 | global.channel = ios.newChannel(global.uri, null, null); |
michael@0 | 117 | ''' |
michael@0 | 118 | |
michael@0 | 119 | CODE_GET_SPEC = ''' |
michael@0 | 120 | return global.channel.URI.spec; |
michael@0 | 121 | ''' |
michael@0 | 122 | |
michael@0 | 123 | CODE_READ_CONTENT = ''' |
michael@0 | 124 | var Cc = SpecialPowers.Cc; |
michael@0 | 125 | var Ci = SpecialPowers.Ci; |
michael@0 | 126 | |
michael@0 | 127 | var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader); |
michael@0 | 128 | var inputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream); |
michael@0 | 129 | |
michael@0 | 130 | var jaruri = global.channel.URI.QueryInterface(Ci.nsIJARURI); |
michael@0 | 131 | var file = jaruri.JARFile.QueryInterface(Ci.nsIFileURL).file; |
michael@0 | 132 | var entry = jaruri.JAREntry; |
michael@0 | 133 | zipReader.open(file); |
michael@0 | 134 | inputStream.init(zipReader.getInputStream(entry)); |
michael@0 | 135 | var content = inputStream.read(inputStream.available()); |
michael@0 | 136 | inputStream.close(); |
michael@0 | 137 | zipReader.close(); |
michael@0 | 138 | return content; |
michael@0 | 139 | ''' |
michael@0 | 140 | |
michael@0 | 141 | @classmethod |
michael@0 | 142 | def get_uri(cls, filename): |
michael@0 | 143 | """Convert filename to URI in system.""" |
michael@0 | 144 | if filename.startswith(cls.URI_PREFIX): |
michael@0 | 145 | return filename |
michael@0 | 146 | else: |
michael@0 | 147 | return cls.URI_PREFIX + cls.URI_PATH[filename] |
michael@0 | 148 | |
michael@0 | 149 | def __init__(self, marionette): |
michael@0 | 150 | self.runjs = lambda x: marionette.execute_script(x, new_sandbox=False) |
michael@0 | 151 | |
michael@0 | 152 | def read_file(self, filename): |
michael@0 | 153 | """Read file and return the contents as string.""" |
michael@0 | 154 | content = self._read_uri(self.get_uri(filename)) |
michael@0 | 155 | content = content.replace('"use strict";', '') |
michael@0 | 156 | return StringUtility.remove_non_ascii(content) |
michael@0 | 157 | |
michael@0 | 158 | def _read_uri(self, uri): |
michael@0 | 159 | """Read URI in system and return the contents as string.""" |
michael@0 | 160 | # Open the uri as a channel. |
michael@0 | 161 | self.runjs(self.CODE_OPEN_CHANNEL_BY_URI % {'uri': uri}) |
michael@0 | 162 | |
michael@0 | 163 | # Make sure spec is a jar uri, and not recursive. |
michael@0 | 164 | # Ex: 'jar:file:///system/b2g/omni.ja!/modules/ril_worker.js' |
michael@0 | 165 | # |
michael@0 | 166 | # For simplicity, we don't handle other special cases in this test. |
michael@0 | 167 | # If B2G build system changes in the future, such as put the jar in |
michael@0 | 168 | # another jar, the test case will fail. |
michael@0 | 169 | spec = self.runjs(self.CODE_GET_SPEC) |
michael@0 | 170 | if (not spec.startswith('jar:file://')) or (spec.count('jar:') != 1): |
michael@0 | 171 | raise Exception('URI resolve error') |
michael@0 | 172 | |
michael@0 | 173 | # Read the content from channel. |
michael@0 | 174 | content = self.runjs(self.CODE_READ_CONTENT) |
michael@0 | 175 | return content |
michael@0 | 176 | |
michael@0 | 177 | |
michael@0 | 178 | class JSHintEngine: |
michael@0 | 179 | |
michael@0 | 180 | """Invoke jshint script on system.""" |
michael@0 | 181 | |
michael@0 | 182 | CODE_INIT_JSHINT = ''' |
michael@0 | 183 | %(script)s; |
michael@0 | 184 | global.JSHINT = JSHINT; |
michael@0 | 185 | global.options = JSON.parse(%(config_string)s); |
michael@0 | 186 | global.globals = global.options.globals; |
michael@0 | 187 | delete global.options.globals; |
michael@0 | 188 | ''' |
michael@0 | 189 | |
michael@0 | 190 | CODE_RUN_JSHINT = ''' |
michael@0 | 191 | global.script = %(code)s; |
michael@0 | 192 | return global.JSHINT(global.script, global.options, global.globals); |
michael@0 | 193 | ''' |
michael@0 | 194 | |
michael@0 | 195 | CODE_GET_JSHINT_ERROR = ''' |
michael@0 | 196 | return global.JSHINT.errors; |
michael@0 | 197 | ''' |
michael@0 | 198 | |
michael@0 | 199 | def __init__(self, marionette, script, config): |
michael@0 | 200 | # Remove single line comment in config. |
michael@0 | 201 | config = '\n'.join([line.partition('//')[0] |
michael@0 | 202 | for line in config.splitlines()]) |
michael@0 | 203 | |
michael@0 | 204 | # Set global (JSHINT, options, global) in js environment. |
michael@0 | 205 | self.runjs = lambda x: marionette.execute_script(x, new_sandbox=False) |
michael@0 | 206 | self.runjs(self.CODE_INIT_JSHINT % |
michael@0 | 207 | {'script': script, 'config_string': repr(config)}) |
michael@0 | 208 | |
michael@0 | 209 | def run(self, code, filename=''): |
michael@0 | 210 | """Excute JShint check for the given code.""" |
michael@0 | 211 | check_pass = self.runjs(self.CODE_RUN_JSHINT % {'code': repr(code)}) |
michael@0 | 212 | errors = self.runjs(self.CODE_GET_JSHINT_ERROR) |
michael@0 | 213 | return check_pass, self._get_error_messages(errors, filename) |
michael@0 | 214 | |
michael@0 | 215 | def _get_error_messages(self, errors, filename=''): |
michael@0 | 216 | """ |
michael@0 | 217 | Convert an error object to a list of readable string. |
michael@0 | 218 | |
michael@0 | 219 | [{"a": null, "c": null, "code": "W033", "d": null, "character": 6, |
michael@0 | 220 | "evidence": "var a", "raw": "Missing semicolon.", |
michael@0 | 221 | "reason": "Missing semicolon.", "b": null, "scope": "(main)", "line": 1, |
michael@0 | 222 | "id": "(error)"}] |
michael@0 | 223 | => line 1, col 6, Missing semicolon. |
michael@0 | 224 | |
michael@0 | 225 | """ |
michael@0 | 226 | LINE, COL, REASON = u'line', u'character', u'reason' |
michael@0 | 227 | return ["%s: line %s, col %s, %s" % |
michael@0 | 228 | (filename, error[LINE], error[COL], error[REASON]) |
michael@0 | 229 | for error in errors if error] |
michael@0 | 230 | |
michael@0 | 231 | |
michael@0 | 232 | class Linter: |
michael@0 | 233 | |
michael@0 | 234 | """Handle the linting related process.""" |
michael@0 | 235 | |
michael@0 | 236 | def __init__(self, code_reader, jshint, reporter=None): |
michael@0 | 237 | """Set the linter with code_reader, jshint engine, and reporter. |
michael@0 | 238 | |
michael@0 | 239 | Should have following functionality. |
michael@0 | 240 | - code_reader.read_file(filename) |
michael@0 | 241 | - jshint.run(code, filename) |
michael@0 | 242 | - reporter([...]) |
michael@0 | 243 | |
michael@0 | 244 | """ |
michael@0 | 245 | self.code_reader = code_reader |
michael@0 | 246 | self.jshint = jshint |
michael@0 | 247 | if reporter is None: |
michael@0 | 248 | self.reporter = lambda x: '\n'.join(x) |
michael@0 | 249 | else: |
michael@0 | 250 | self.reporter = reporter |
michael@0 | 251 | |
michael@0 | 252 | def lint_file(self, filename): |
michael@0 | 253 | """Lint the file and return (pass, error_message).""" |
michael@0 | 254 | # Get code contents. |
michael@0 | 255 | code = self.code_reader.read_file(filename) |
michael@0 | 256 | lines = code.splitlines() |
michael@0 | 257 | import_list = StringUtility.get_imported_list(lines) |
michael@0 | 258 | if not import_list: |
michael@0 | 259 | check_pass, error_message = self.jshint.run(code, filename) |
michael@0 | 260 | else: |
michael@0 | 261 | newlines, info = self._merge_multiple_codes(filename, import_list) |
michael@0 | 262 | # Each line of |newlines| contains '\n'. |
michael@0 | 263 | check_pass, error_message = self.jshint.run(''.join(newlines)) |
michael@0 | 264 | error_message = self._convert_merged_result(error_message, info) |
michael@0 | 265 | # Only keep errors for this file. |
michael@0 | 266 | error_message = [line for line in error_message |
michael@0 | 267 | if line.startswith(filename)] |
michael@0 | 268 | check_pass = (len(error_message) == 0) |
michael@0 | 269 | return check_pass, self.reporter(error_message) |
michael@0 | 270 | |
michael@0 | 271 | def _merge_multiple_codes(self, filename, import_list): |
michael@0 | 272 | """Merge multiple codes from filename and import_list.""" |
michael@0 | 273 | dirname, filename = os.path.split(filename) |
michael@0 | 274 | dst_line = 1 |
michael@0 | 275 | dst_results = [] |
michael@0 | 276 | info = [] |
michael@0 | 277 | |
michael@0 | 278 | # Put the imported script first, and then the original script. |
michael@0 | 279 | for f in import_list + [filename]: |
michael@0 | 280 | filepath = os.path.join(dirname, f) |
michael@0 | 281 | |
michael@0 | 282 | # Maintain a mapping table. |
michael@0 | 283 | # New line number after merge => original file and line number. |
michael@0 | 284 | info.append((dst_line, filepath, 1)) |
michael@0 | 285 | try: |
michael@0 | 286 | code = self.code_reader.read_file(filepath) |
michael@0 | 287 | lines = code.splitlines(True) # Keep '\n'. |
michael@0 | 288 | src_results = StringUtility.auto_wrap_strict_mode( |
michael@0 | 289 | StringUtility.auto_close(lines)) |
michael@0 | 290 | dst_results.extend(src_results) |
michael@0 | 291 | dst_line += len(src_results) |
michael@0 | 292 | except: |
michael@0 | 293 | info.pop() |
michael@0 | 294 | return dst_results, info |
michael@0 | 295 | |
michael@0 | 296 | def _convert_merged_result(self, error_lines, line_info): |
michael@0 | 297 | pattern = re.compile(r'(.*): line (\d+),(.*)') |
michael@0 | 298 | start_line = [info[0] for info in line_info] |
michael@0 | 299 | new_result_lines = [] |
michael@0 | 300 | for line in error_lines: |
michael@0 | 301 | m = pattern.match(line) |
michael@0 | 302 | if not m: |
michael@0 | 303 | continue |
michael@0 | 304 | |
michael@0 | 305 | line_number, remain = int(m.group(2)), m.group(3) |
michael@0 | 306 | |
michael@0 | 307 | # [1, 2, 7, 8] |
michael@0 | 308 | # ^ for 7, pos = 3 |
michael@0 | 309 | # ^ for 6, pos = 2 |
michael@0 | 310 | pos = bisect.bisect_right(start_line, line_number) |
michael@0 | 311 | dst_line, name, src_line = line_info[pos - 1] |
michael@0 | 312 | real_line_number = line_number - dst_line + src_line |
michael@0 | 313 | new_result_lines.append( |
michael@0 | 314 | "%s: line %s,%s" % (name, real_line_number, remain)) |
michael@0 | 315 | return new_result_lines |
michael@0 | 316 | |
michael@0 | 317 | |
michael@0 | 318 | class TestRILCodeQuality(MarionetteTestCase): |
michael@0 | 319 | |
michael@0 | 320 | JSHINT_PATH = 'ril_jshint/jshint.js' |
michael@0 | 321 | JSHINTRC_PATH = 'ril_jshint/jshintrc' |
michael@0 | 322 | |
michael@0 | 323 | def _read_local_file(self, filepath): |
michael@0 | 324 | """Read file content from local (folder of this test case).""" |
michael@0 | 325 | test_dir = os.path.dirname(inspect.getfile(TestRILCodeQuality)) |
michael@0 | 326 | return open(os.path.join(test_dir, filepath)).read() |
michael@0 | 327 | |
michael@0 | 328 | def _get_extended_error_message(self, error_message): |
michael@0 | 329 | return '\n'.join(['See errors below and more information in Bug 880643', |
michael@0 | 330 | '\n'.join(error_message), |
michael@0 | 331 | 'See errors above and more information in Bug 880643']) |
michael@0 | 332 | |
michael@0 | 333 | def _check(self, filename): |
michael@0 | 334 | check_pass, error_message = self.linter.lint_file(filename) |
michael@0 | 335 | self.assertTrue(check_pass, error_message) |
michael@0 | 336 | |
michael@0 | 337 | def setUp(self): |
michael@0 | 338 | MarionetteTestCase.setUp(self) |
michael@0 | 339 | self.linter = Linter( |
michael@0 | 340 | ResourceUriFileReader(self.marionette), |
michael@0 | 341 | JSHintEngine(self.marionette, |
michael@0 | 342 | self._read_local_file(self.JSHINT_PATH), |
michael@0 | 343 | self._read_local_file(self.JSHINTRC_PATH)), |
michael@0 | 344 | self._get_extended_error_message) |
michael@0 | 345 | |
michael@0 | 346 | def tearDown(self): |
michael@0 | 347 | MarionetteTestCase.tearDown(self) |
michael@0 | 348 | |
michael@0 | 349 | def test_RILContentHelper(self): |
michael@0 | 350 | self._check('RILContentHelper.js') |
michael@0 | 351 | |
michael@0 | 352 | def test_RadioInterfaceLayer(self): |
michael@0 | 353 | self._check('RadioInterfaceLayer.js') |
michael@0 | 354 | |
michael@0 | 355 | # Bug 936504. Disable the test for 'ril_worker.js'. It sometimes runs very |
michael@0 | 356 | # slow and causes the timeout fail on try server. |
michael@0 | 357 | #def test_ril_worker(self): |
michael@0 | 358 | # self._check('ril_worker.js') |
michael@0 | 359 | |
michael@0 | 360 | def test_ril_consts(self): |
michael@0 | 361 | self._check('ril_consts.js') |
michael@0 | 362 | |
michael@0 | 363 | def test_worker_buf(self): |
michael@0 | 364 | self._check('worker_buf.js') |