summaryrefslogtreecommitdiffstats
path: root/dom/system/gonk/tests/marionette/test_ril_code_quality.py
blob: d741d8a2e822a9e5a5c0dc9f28b0c93af84ccf3e (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
"""
The test performs the static code analysis check by JSHint.

Target js files:
- RadioInterfaceLayer.js
- ril_worker.js
- ril_consts.js

If the js file contains the line of 'importScript()' (Ex: ril_worker.js), the
test will perform a special merge step before excuting JSHint.

Ex: Script A
--------------------------------
importScripts('Script B')
...
--------------------------------

We merge these two scripts into one by the following way.

--------------------------------
[Script B (ex: ril_consts.js)]
(function(){ [Script A (ex: ril_worker.js)]
})();
--------------------------------

Script A (ril_worker.js) runs global strict mode.
Script B (ril_consts.js) not.

The above merge way ensures the correct scope of 'strict mode.'
"""

import bisect
import inspect
import os
import os.path
import re
import unicodedata

from marionette_harness import MarionetteTestCase


class StringUtility:

    """A collection of some string utilities."""

    @staticmethod
    def find_match_lines(lines, pattern):
        """Return a list of lines that contains given pattern."""
        return [line for line in lines if pattern in line]

    @staticmethod
    def remove_non_ascii(data):
        """Remove non ascii characters in data and return it as new string."""
        if type(data).__name__ == 'unicode':
            data = unicodedata.normalize(
                'NFKD', data).encode('ascii', 'ignore')
        return data

    @staticmethod
    def auto_close(lines):
        """Ensure every line ends with '\n'."""
        if lines and not lines[-1].endswith('\n'):
            lines[-1] += '\n'
        return lines

    @staticmethod
    def auto_wrap_strict_mode(lines):
        """Wrap by function scope if lines contain 'use strict'."""
        if StringUtility.find_match_lines(lines, 'use strict'):
            lines[0] = '(function(){' + lines[0]
            lines.append('})();\n')
        return lines

    @staticmethod
    def get_imported_list(lines):
        """Get a list of imported items."""
        return [item
                for line in StringUtility.find_match_lines(lines, 'importScripts')
                for item in StringUtility._get_imported_list_from_line(line)]

    @staticmethod
    def _get_imported_list_from_line(line):
        """Extract all items from 'importScripts(...)'.

        importScripts("ril_consts.js", "systemlibs.js")
        => ['ril_consts', 'systemlibs.js']

        """
        pattern = re.compile(r'\s*importScripts\((.*)\)')
        m = pattern.match(line)
        if not m:
            raise Exception('Parse importScripts error.')
        return [name.translate(None, '\' "') for name in m.group(1).split(',')]


class ResourceUriFileReader:

    """Handle the process of reading the source code from system."""

    URI_PREFIX = 'resource://gre/'
    URI_PATH = {
        'RadioInterfaceLayer.js': 'components/RadioInterfaceLayer.js',
        'ril_worker.js':          'modules/ril_worker.js',
        'ril_consts.js':          'modules/ril_consts.js',
        'systemlibs.js':          'modules/systemlibs.js',
        'worker_buf.js':          'modules/workers/worker_buf.js',
    }

    CODE_OPEN_CHANNEL_BY_URI = '''
    var Cc = Components.classes;
    var Ci = Components.interfaces;
    var ios = Cc["@mozilla.org/network/io-service;1"].getService(Ci.nsIIOService);
    var secMan = Cc["@mozilla.org/scriptsecuritymanager;1"].getService(Ci.nsIScriptSecurityManager);
    global.uri = '%(uri)s';
    global.channel = ios.newChannel2(global.uri,
                                     null,
                                     null,
                                     null,      // aLoadingNode
                                     secMan.getSystemPrincipal(),
                                     null,      // aTriggeringPrincipal
                                     Ci.nsILoadInfo.SEC_ALLOW_CROSS_ORIGIN_DATA_IS_NULL,
                                     Ci.nsIContentPolicy.TYPE_OTHER);
    '''

    CODE_GET_SPEC = '''
    return global.channel.URI.spec;
    '''

    CODE_READ_CONTENT = '''
    var Cc = Components.classes;
    var Ci = Components.interfaces;

    var zipReader = Cc["@mozilla.org/libjar/zip-reader;1"].createInstance(Ci.nsIZipReader);
    var inputStream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(Ci.nsIScriptableInputStream);

    var jaruri = global.channel.URI.QueryInterface(Ci.nsIJARURI);
    var file = jaruri.JARFile.QueryInterface(Ci.nsIFileURL).file;
    var entry = jaruri.JAREntry;
    zipReader.open(file);
    inputStream.init(zipReader.getInputStream(entry));
    var content = inputStream.read(inputStream.available());
    inputStream.close();
    zipReader.close();
    return content;
    '''

    @classmethod
    def get_uri(cls, filename):
        """Convert filename to URI in system."""
        if filename.startswith(cls.URI_PREFIX):
            return filename
        else:
            return cls.URI_PREFIX + cls.URI_PATH[filename]

    def __init__(self, marionette):
        self.runjs = lambda x: marionette.execute_script(x,
                                                         new_sandbox=False,
                                                         sandbox='system')

    def read_file(self, filename):
        """Read file and return the contents as string."""
        content = self._read_uri(self.get_uri(filename))
        content = content.replace('"use strict";', '')
        return StringUtility.remove_non_ascii(content)

    def _read_uri(self, uri):
        """Read URI in system and return the contents as string."""
        # Open the uri as a channel.
        self.runjs(self.CODE_OPEN_CHANNEL_BY_URI % {'uri': uri})

        # Make sure spec is a jar uri, and not recursive.
        #   Ex: 'jar:file:///system/b2g/omni.ja!/modules/ril_worker.js'
        #
        # For simplicity, we don't handle other special cases in this test.
        # If B2G build system changes in the future, such as put the jar in
        # another jar, the test case will fail.
        spec = self.runjs(self.CODE_GET_SPEC)
        if (not spec.startswith('jar:file://')) or (spec.count('jar:') != 1):
            raise Exception('URI resolve error')

        # Read the content from channel.
        content = self.runjs(self.CODE_READ_CONTENT)
        return content


class JSHintEngine:

    """Invoke jshint script on system."""

    CODE_INIT_JSHINT = '''
    %(script)s;
    global.JSHINT = JSHINT;
    global.options = JSON.parse(%(config_string)s);
    global.globals = global.options.globals;
    delete global.options.globals;
    '''

    CODE_RUN_JSHINT = '''
    global.script = %(code)s;
    return global.JSHINT(global.script, global.options, global.globals);
    '''

    CODE_GET_JSHINT_ERROR = '''
    return global.JSHINT.errors;
    '''

    def __init__(self, marionette, script, config):
        # Remove single line comment in config.
        config = '\n'.join([line.partition('//')[0]
                            for line in config.splitlines()])

        # Set global (JSHINT, options, global) in js environment.
        self.runjs = lambda x: marionette.execute_script(x,
                                                         new_sandbox=False,
                                                         sandbox='system')
        self.runjs(self.CODE_INIT_JSHINT %
                   {'script': script, 'config_string': repr(config)})

    def run(self, code, filename=''):
        """Excute JShint check for the given code."""
        check_pass = self.runjs(self.CODE_RUN_JSHINT % {'code': repr(code)})
        errors = self.runjs(self.CODE_GET_JSHINT_ERROR)
        return check_pass, self._get_error_messages(errors, filename)

    def _get_error_messages(self, errors, filename=''):
        """
        Convert an error object to a list of readable string.

        [{"a": null, "c": null, "code": "W033", "d": null, "character": 6,
          "evidence": "var a", "raw": "Missing semicolon.",
          "reason": "Missing semicolon.", "b": null, "scope": "(main)", "line": 1,
          "id": "(error)"}]
        => line 1, col 6, Missing semicolon.

        """
        LINE, COL, REASON = u'line', u'character', u'reason'
        return ["%s: line %s, col %s, %s" %
                (filename, error[LINE], error[COL], error[REASON])
                for error in errors if error]


class Linter:

    """Handle the linting related process."""

    def __init__(self, code_reader, jshint, reporter=None):
        """Set the linter with code_reader, jshint engine, and reporter.

        Should have following functionality.
        - code_reader.read_file(filename)
        - jshint.run(code, filename)
        - reporter([...])

        """
        self.code_reader = code_reader
        self.jshint = jshint
        if reporter is None:
            self.reporter = lambda x: '\n'.join(x)
        else:
            self.reporter = reporter

    def lint_file(self, filename):
        """Lint the file and return (pass, error_message)."""
        # Get code contents.
        code = self.code_reader.read_file(filename)
        lines = code.splitlines()
        import_list = StringUtility.get_imported_list(lines)
        if not import_list:
            check_pass, error_message = self.jshint.run(code, filename)
        else:
            newlines, info = self._merge_multiple_codes(filename, import_list)
            # Each line of |newlines| contains '\n'.
            check_pass, error_message = self.jshint.run(''.join(newlines))
            error_message = self._convert_merged_result(error_message, info)
            # Only keep errors for this file.
            error_message = [line for line in error_message
                             if line.startswith(filename)]
            check_pass = (len(error_message) == 0)
        return check_pass, self.reporter(error_message)

    def _merge_multiple_codes(self, filename, import_list):
        """Merge multiple codes from filename and import_list."""
        dirname, filename = os.path.split(filename)
        dst_line = 1
        dst_results = []
        info = []

        # Put the imported script first, and then the original script.
        for f in import_list + [filename]:
            filepath = os.path.join(dirname, f)

            # Maintain a mapping table.
            # New line number after merge => original file and line number.
            info.append((dst_line, filepath, 1))
            try:
                code = self.code_reader.read_file(filepath)
                lines = code.splitlines(True)  # Keep '\n'.
                src_results = StringUtility.auto_wrap_strict_mode(
                    StringUtility.auto_close(lines))
                dst_results.extend(src_results)
                dst_line += len(src_results)
            except:
                info.pop()
        return dst_results, info

    def _convert_merged_result(self, error_lines, line_info):
        pattern = re.compile(r'(.*): line (\d+),(.*)')
        start_line = [info[0] for info in line_info]
        new_result_lines = []
        for line in error_lines:
            m = pattern.match(line)
            if not m:
                continue

            line_number, remain = int(m.group(2)), m.group(3)

            #  [1, 2, 7, 8]
            #            ^ for 7, pos = 3
            #         ^    for 6, pos = 2
            pos = bisect.bisect_right(start_line, line_number)
            dst_line, name, src_line = line_info[pos - 1]
            real_line_number = line_number - dst_line + src_line
            new_result_lines.append(
                "%s: line %s,%s" % (name, real_line_number, remain))
        return new_result_lines


class TestRILCodeQuality(MarionetteTestCase):

    JSHINT_PATH = 'ril_jshint/jshint.js'
    JSHINTRC_PATH = 'ril_jshint/jshintrc'

    def _read_local_file(self, filepath):
        """Read file content from local (folder of this test case)."""
        test_dir = os.path.dirname(inspect.getfile(TestRILCodeQuality))
        return open(os.path.join(test_dir, filepath)).read()

    def _get_extended_error_message(self, error_message):
        return '\n'.join(['See errors below and more information in Bug 880643',
                          '\n'.join(error_message),
                          'See errors above and more information in Bug 880643'])

    def _check(self, filename):
        check_pass, error_message = self.linter.lint_file(filename)
        self.assertTrue(check_pass, error_message)

    def setUp(self):
        MarionetteTestCase.setUp(self)
        self.linter = Linter(
            ResourceUriFileReader(self.marionette),
            JSHintEngine(self.marionette,
                         self._read_local_file(self.JSHINT_PATH),
                         self._read_local_file(self.JSHINTRC_PATH)),
            self._get_extended_error_message)

    def tearDown(self):
        MarionetteTestCase.tearDown(self)

    def test_RadioInterfaceLayer(self):
        self._check('RadioInterfaceLayer.js')

    # Bug 936504. Disable the test for 'ril_worker.js'. It sometimes runs very
    # slow and causes the timeout fail on try server.
    #def test_ril_worker(self):
    #    self._check('ril_worker.js')

    def test_ril_consts(self):
        self._check('ril_consts.js')

    def test_worker_buf(self):
        self._check('worker_buf.js')