|
1 """ |
|
2 The test performs the static code analysis check by JSHint. |
|
3 |
|
4 Target js files: |
|
5 - RILContentHelper.js |
|
6 - RadioInterfaceLayer.js |
|
7 - ril_worker.js |
|
8 - ril_consts.js |
|
9 |
|
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. |
|
12 |
|
13 Ex: Script A |
|
14 -------------------------------- |
|
15 importScripts('Script B') |
|
16 ... |
|
17 -------------------------------- |
|
18 |
|
19 We merge these two scripts into one by the following way. |
|
20 |
|
21 -------------------------------- |
|
22 [Script B (ex: ril_consts.js)] |
|
23 (function(){ [Script A (ex: ril_worker.js)] |
|
24 })(); |
|
25 -------------------------------- |
|
26 |
|
27 Script A (ril_worker.js) runs global strict mode. |
|
28 Script B (ril_consts.js) not. |
|
29 |
|
30 The above merge way ensures the correct scope of 'strict mode.' |
|
31 """ |
|
32 |
|
33 |
|
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 |
|
41 |
|
42 |
|
43 class StringUtility: |
|
44 |
|
45 """A collection of some string utilities.""" |
|
46 |
|
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] |
|
51 |
|
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 |
|
59 |
|
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 |
|
66 |
|
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 |
|
74 |
|
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)] |
|
81 |
|
82 @staticmethod |
|
83 def _get_imported_list_from_line(line): |
|
84 """Extract all items from 'importScripts(...)'. |
|
85 |
|
86 importScripts("ril_consts.js", "systemlibs.js") |
|
87 => ['ril_consts', 'systemlibs.js'] |
|
88 |
|
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(',')] |
|
95 |
|
96 |
|
97 class ResourceUriFileReader: |
|
98 |
|
99 """Handle the process of reading the source code from system.""" |
|
100 |
|
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 } |
|
110 |
|
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 ''' |
|
118 |
|
119 CODE_GET_SPEC = ''' |
|
120 return global.channel.URI.spec; |
|
121 ''' |
|
122 |
|
123 CODE_READ_CONTENT = ''' |
|
124 var Cc = SpecialPowers.Cc; |
|
125 var Ci = SpecialPowers.Ci; |
|
126 |
|
127 var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader); |
|
128 var inputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream); |
|
129 |
|
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 ''' |
|
140 |
|
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] |
|
148 |
|
149 def __init__(self, marionette): |
|
150 self.runjs = lambda x: marionette.execute_script(x, new_sandbox=False) |
|
151 |
|
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) |
|
157 |
|
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}) |
|
162 |
|
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') |
|
172 |
|
173 # Read the content from channel. |
|
174 content = self.runjs(self.CODE_READ_CONTENT) |
|
175 return content |
|
176 |
|
177 |
|
178 class JSHintEngine: |
|
179 |
|
180 """Invoke jshint script on system.""" |
|
181 |
|
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 ''' |
|
189 |
|
190 CODE_RUN_JSHINT = ''' |
|
191 global.script = %(code)s; |
|
192 return global.JSHINT(global.script, global.options, global.globals); |
|
193 ''' |
|
194 |
|
195 CODE_GET_JSHINT_ERROR = ''' |
|
196 return global.JSHINT.errors; |
|
197 ''' |
|
198 |
|
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()]) |
|
203 |
|
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)}) |
|
208 |
|
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) |
|
214 |
|
215 def _get_error_messages(self, errors, filename=''): |
|
216 """ |
|
217 Convert an error object to a list of readable string. |
|
218 |
|
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. |
|
224 |
|
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] |
|
230 |
|
231 |
|
232 class Linter: |
|
233 |
|
234 """Handle the linting related process.""" |
|
235 |
|
236 def __init__(self, code_reader, jshint, reporter=None): |
|
237 """Set the linter with code_reader, jshint engine, and reporter. |
|
238 |
|
239 Should have following functionality. |
|
240 - code_reader.read_file(filename) |
|
241 - jshint.run(code, filename) |
|
242 - reporter([...]) |
|
243 |
|
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 |
|
251 |
|
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) |
|
270 |
|
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 = [] |
|
277 |
|
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) |
|
281 |
|
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 |
|
295 |
|
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 |
|
304 |
|
305 line_number, remain = int(m.group(2)), m.group(3) |
|
306 |
|
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 |
|
316 |
|
317 |
|
318 class TestRILCodeQuality(MarionetteTestCase): |
|
319 |
|
320 JSHINT_PATH = 'ril_jshint/jshint.js' |
|
321 JSHINTRC_PATH = 'ril_jshint/jshintrc' |
|
322 |
|
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() |
|
327 |
|
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']) |
|
332 |
|
333 def _check(self, filename): |
|
334 check_pass, error_message = self.linter.lint_file(filename) |
|
335 self.assertTrue(check_pass, error_message) |
|
336 |
|
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) |
|
345 |
|
346 def tearDown(self): |
|
347 MarionetteTestCase.tearDown(self) |
|
348 |
|
349 def test_RILContentHelper(self): |
|
350 self._check('RILContentHelper.js') |
|
351 |
|
352 def test_RadioInterfaceLayer(self): |
|
353 self._check('RadioInterfaceLayer.js') |
|
354 |
|
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') |
|
359 |
|
360 def test_ril_consts(self): |
|
361 self._check('ril_consts.js') |
|
362 |
|
363 def test_worker_buf(self): |
|
364 self._check('worker_buf.js') |