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