dom/system/gonk/tests/marionette/test_ril_code_quality.py

Thu, 22 Jan 2015 13:21:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Thu, 22 Jan 2015 13:21:57 +0100
branch
TOR_BUG_9701
changeset 15
b8a032363ba2
permissions
-rw-r--r--

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

mercurial