1# Copyright (c) 2013 The Chromium Authors. All rights reserved.
2# Use of this source code is governed by a BSD-style license that can be
3# found in the LICENSE file.
4
5"""Top-level presubmit script for Blink.
6
7See http://dev.chromium.org/developers/how-tos/depottools/presubmit-scripts
8for more details about the presubmit API built into gcl.
9"""
10
11import sys
12
13
14_EXCLUDED_PATHS = ()
15
16
17def _CheckForVersionControlConflictsInFile(input_api, f):
18    pattern = input_api.re.compile('^(?:<<<<<<<|>>>>>>>) |^=======$')
19    errors = []
20    for line_num, line in f.ChangedContents():
21        if pattern.match(line):
22            errors.append('    %s:%d %s' % (f.LocalPath(), line_num, line))
23    return errors
24
25
26def _CheckForVersionControlConflicts(input_api, output_api):
27    """Usually this is not intentional and will cause a compile failure."""
28    errors = []
29    for f in input_api.AffectedFiles():
30        errors.extend(_CheckForVersionControlConflictsInFile(input_api, f))
31
32    results = []
33    if errors:
34        results.append(output_api.PresubmitError(
35            'Version control conflict markers found, please resolve.', errors))
36    return results
37
38
39def _CheckWatchlist(input_api, output_api):
40    """Check that the WATCHLIST file parses correctly."""
41    errors = []
42    for f in input_api.AffectedFiles():
43        if f.LocalPath() != 'WATCHLISTS':
44            continue
45        import StringIO
46        import logging
47        import watchlists
48
49        log_buffer = StringIO.StringIO()
50        log_handler = logging.StreamHandler(log_buffer)
51        log_handler.setFormatter(
52            logging.Formatter('%(levelname)s: %(message)s'))
53        logger = logging.getLogger()
54        logger.addHandler(log_handler)
55
56        wl = watchlists.Watchlists(input_api.change.RepositoryRoot())
57
58        logger.removeHandler(log_handler)
59        log_handler.flush()
60        log_buffer.flush()
61
62        if log_buffer.getvalue():
63            errors.append(output_api.PresubmitError(
64                'Cannot parse WATCHLISTS file, please resolve.',
65                log_buffer.getvalue().splitlines()))
66    return errors
67
68
69def _CommonChecks(input_api, output_api):
70    """Checks common to both upload and commit."""
71    # We should figure out what license checks we actually want to use.
72    license_header = r'.*'
73
74    results = []
75    results.extend(input_api.canned_checks.PanProjectChecks(
76        input_api, output_api, excluded_paths=_EXCLUDED_PATHS,
77        maxlen=800, license_header=license_header))
78    results.extend(_CheckForVersionControlConflicts(input_api, output_api))
79    results.extend(_CheckPatchFiles(input_api, output_api))
80    results.extend(_CheckTestExpectations(input_api, output_api))
81    results.extend(_CheckUnwantedDependencies(input_api, output_api))
82    results.extend(_CheckChromiumPlatformMacros(input_api, output_api))
83    results.extend(_CheckWatchlist(input_api, output_api))
84    results.extend(_CheckFilePermissions(input_api, output_api))
85    return results
86
87
88def _CheckSubversionConfig(input_api, output_api):
89  """Verifies the subversion config file is correctly setup.
90
91  Checks that autoprops are enabled, returns an error otherwise.
92  """
93  join = input_api.os_path.join
94  if input_api.platform == 'win32':
95    appdata = input_api.environ.get('APPDATA', '')
96    if not appdata:
97      return [output_api.PresubmitError('%APPDATA% is not configured.')]
98    path = join(appdata, 'Subversion', 'config')
99  else:
100    home = input_api.environ.get('HOME', '')
101    if not home:
102      return [output_api.PresubmitError('$HOME is not configured.')]
103    path = join(home, '.subversion', 'config')
104
105  error_msg = (
106      'Please look at http://dev.chromium.org/developers/coding-style to\n'
107      'configure your subversion configuration file. This enables automatic\n'
108      'properties to simplify the project maintenance.\n'
109      'Pro-tip: just download and install\n'
110      'http://src.chromium.org/viewvc/chrome/trunk/tools/build/slave/config\n')
111
112  try:
113    lines = open(path, 'r').read().splitlines()
114    # Make sure auto-props is enabled and check for 2 Chromium standard
115    # auto-prop.
116    if (not '*.cc = svn:eol-style=LF' in lines or
117        not '*.pdf = svn:mime-type=application/pdf' in lines or
118        not 'enable-auto-props = yes' in lines):
119      return [
120          output_api.PresubmitNotifyResult(
121              'It looks like you have not configured your subversion config '
122              'file or it is not up-to-date.\n' + error_msg)
123      ]
124  except (OSError, IOError):
125    return [
126        output_api.PresubmitNotifyResult(
127            'Can\'t find your subversion config file.\n' + error_msg)
128    ]
129  return []
130
131
132def _CheckPatchFiles(input_api, output_api):
133  problems = [f.LocalPath() for f in input_api.AffectedFiles()
134      if f.LocalPath().endswith(('.orig', '.rej'))]
135  if problems:
136    return [output_api.PresubmitError(
137        "Don't commit .rej and .orig files.", problems)]
138  else:
139    return []
140
141
142def _CheckTestExpectations(input_api, output_api):
143    local_paths = [f.LocalPath() for f in input_api.AffectedFiles()]
144    if any(path.startswith('LayoutTests') for path in local_paths):
145        lint_path = input_api.os_path.join(input_api.PresubmitLocalPath(),
146            'Tools', 'Scripts', 'lint-test-expectations')
147        _, errs = input_api.subprocess.Popen(
148            [input_api.python_executable, lint_path],
149            stdout=input_api.subprocess.PIPE,
150            stderr=input_api.subprocess.PIPE).communicate()
151        if not errs:
152            return [output_api.PresubmitError(
153                "lint-test-expectations failed "
154                "to produce output; check by hand. ")]
155        if errs.strip() != 'Lint succeeded.':
156            return [output_api.PresubmitError(errs)]
157    return []
158
159
160def _CheckStyle(input_api, output_api):
161    style_checker_path = input_api.os_path.join(input_api.PresubmitLocalPath(),
162        'Tools', 'Scripts', 'check-webkit-style')
163    args = ([input_api.python_executable, style_checker_path, '--diff-files']
164        + [f.LocalPath() for f in input_api.AffectedFiles()])
165    results = []
166
167    try:
168        child = input_api.subprocess.Popen(args,
169                                           stderr=input_api.subprocess.PIPE)
170        _, stderrdata = child.communicate()
171        if child.returncode != 0:
172            results.append(output_api.PresubmitError(
173                'check-webkit-style failed', [stderrdata]))
174    except Exception as e:
175        results.append(output_api.PresubmitNotifyResult(
176            'Could not run check-webkit-style', [str(e)]))
177
178    return results
179
180
181def _CheckUnwantedDependencies(input_api, output_api):
182    """Runs checkdeps on #include statements added in this
183    change. Breaking - rules is an error, breaking ! rules is a
184    warning.
185    """
186    # We need to wait until we have an input_api object and use this
187    # roundabout construct to import checkdeps because this file is
188    # eval-ed and thus doesn't have __file__.
189    original_sys_path = sys.path
190    try:
191        sys.path = sys.path + [input_api.os_path.realpath(input_api.os_path.join(
192                input_api.PresubmitLocalPath(), '..', '..', 'buildtools', 'checkdeps'))]
193        import checkdeps
194        from cpp_checker import CppChecker
195        from rules import Rule
196    finally:
197        # Restore sys.path to what it was before.
198        sys.path = original_sys_path
199
200    added_includes = []
201    for f in input_api.AffectedFiles():
202        if not CppChecker.IsCppFile(f.LocalPath()):
203            continue
204
205        changed_lines = [line for line_num, line in f.ChangedContents()]
206        added_includes.append([f.LocalPath(), changed_lines])
207
208    deps_checker = checkdeps.DepsChecker(
209        input_api.os_path.join(input_api.PresubmitLocalPath()))
210
211    error_descriptions = []
212    warning_descriptions = []
213    for path, rule_type, rule_description in deps_checker.CheckAddedCppIncludes(
214            added_includes):
215        description_with_path = '%s\n    %s' % (path, rule_description)
216        if rule_type == Rule.DISALLOW:
217            error_descriptions.append(description_with_path)
218        else:
219            warning_descriptions.append(description_with_path)
220
221    results = []
222    if error_descriptions:
223        results.append(output_api.PresubmitError(
224                'You added one or more #includes that violate checkdeps rules.',
225                error_descriptions))
226    if warning_descriptions:
227        results.append(output_api.PresubmitPromptOrNotify(
228                'You added one or more #includes of files that are temporarily\n'
229                'allowed but being removed. Can you avoid introducing the\n'
230                '#include? See relevant DEPS file(s) for details and contacts.',
231                warning_descriptions))
232    return results
233
234
235def _CheckChromiumPlatformMacros(input_api, output_api, source_file_filter=None):
236    """Ensures that Blink code uses WTF's platform macros instead of
237    Chromium's. Using the latter has resulted in at least one subtle
238    build breakage."""
239    os_macro_re = input_api.re.compile(r'^\s*#(el)?if.*\bOS_')
240    errors = input_api.canned_checks._FindNewViolationsOfRule(
241        lambda _, x: not os_macro_re.search(x),
242        input_api, source_file_filter)
243    errors = ['Found use of Chromium OS_* macro in %s. '
244        'Use WTF platform macros instead.' % violation for violation in errors]
245    if errors:
246        return [output_api.PresubmitPromptWarning('\n'.join(errors))]
247    return []
248
249
250def _CheckForPrintfDebugging(input_api, output_api):
251    """Generally speaking, we'd prefer not to land patches that printf
252    debug output."""
253    printf_re = input_api.re.compile(r'^\s*printf\(')
254    errors = input_api.canned_checks._FindNewViolationsOfRule(
255        lambda _, x: not printf_re.search(x),
256        input_api, None)
257    errors = ['  * %s' % violation for violation in errors]
258    if errors:
259        return [output_api.PresubmitPromptOrNotify(
260                    'printf debugging is best debugging! That said, it might '
261                    'be a good idea to drop the following occurances from '
262                    'your patch before uploading:\n%s' % '\n'.join(errors))]
263    return []
264
265
266def _CheckForDangerousTestFunctions(input_api, output_api):
267    """Tests should not be using serveAsynchronousMockedRequests, since it does
268    not guarantee that the threaded HTML parser will have completed."""
269    serve_async_requests_re = input_api.re.compile(
270        r'serveAsynchronousMockedRequests')
271    errors = input_api.canned_checks._FindNewViolationsOfRule(
272        lambda _, x: not serve_async_requests_re.search(x),
273        input_api, None)
274    errors = ['  * %s' % violation for violation in errors]
275    if errors:
276        return [output_api.PresubmitError(
277                    'You should be using FrameTestHelpers::'
278                    'pumpPendingRequests() instead of '
279                    'serveAsynchronousMockedRequests() in the following '
280                    'locations:\n%s' % '\n'.join(errors))]
281    return []
282
283
284def _CheckForFailInFile(input_api, f):
285    pattern = input_api.re.compile('^FAIL')
286    errors = []
287    for line_num, line in f.ChangedContents():
288        if pattern.match(line):
289            errors.append('    %s:%d %s' % (f.LocalPath(), line_num, line))
290    return errors
291
292
293def _CheckFilePermissions(input_api, output_api):
294    """Check that all files have their permissions properly set."""
295    if input_api.platform == 'win32':
296        return []
297    path = input_api.os_path.join(
298        '..', '..', 'tools', 'checkperms', 'checkperms.py')
299    args = [sys.executable, path, '--root', input_api.change.RepositoryRoot()]
300    for f in input_api.AffectedFiles():
301        args += ['--file', f.LocalPath()]
302    checkperms = input_api.subprocess.Popen(
303        args, stdout=input_api.subprocess.PIPE)
304    errors = checkperms.communicate()[0].strip()
305    if errors:
306        return [output_api.PresubmitError(
307            'checkperms.py failed.', errors.splitlines())]
308    return []
309
310
311def _CheckForInvalidPreferenceError(input_api, output_api):
312    pattern = input_api.re.compile('Invalid name for preference: (.+)')
313    results = []
314
315    for f in input_api.AffectedFiles():
316        if not f.LocalPath().endswith('-expected.txt'):
317            continue
318        for line_num, line in f.ChangedContents():
319            error = pattern.search(line)
320            if error:
321                results.append(output_api.PresubmitError('Found an invalid preference %s in expected result %s:%s' % (error.group(1), f, line_num)))
322    return results
323
324def CheckChangeOnUpload(input_api, output_api):
325    results = []
326    results.extend(_CommonChecks(input_api, output_api))
327    results.extend(_CheckStyle(input_api, output_api))
328    results.extend(_CheckForPrintfDebugging(input_api, output_api))
329    results.extend(_CheckForDangerousTestFunctions(input_api, output_api))
330    results.extend(_CheckForInvalidPreferenceError(input_api, output_api))
331    return results
332
333
334def CheckChangeOnCommit(input_api, output_api):
335    results = []
336    results.extend(_CommonChecks(input_api, output_api))
337    results.extend(input_api.canned_checks.CheckTreeIsOpen(
338        input_api, output_api,
339        json_url='http://blink-status.appspot.com/current?format=json'))
340    results.extend(input_api.canned_checks.CheckChangeHasDescription(
341        input_api, output_api))
342    results.extend(_CheckSubversionConfig(input_api, output_api))
343    return results
344
345
346def GetPreferredTryMasters(project, change):
347    return {
348        'tryserver.blink': {
349            'android_blink_compile_dbg': set(['defaulttests']),
350            'android_blink_compile_rel': set(['defaulttests']),
351            'android_chromium_gn_compile_rel': set(['defaulttests']),
352            'linux_blink_dbg': set(['defaulttests']),
353            'linux_blink_rel': set(['defaulttests']),
354            'linux_chromium_gn_rel': set(['defaulttests']),
355            'mac_blink_compile_dbg': set(['defaulttests']),
356            'mac_blink_rel': set(['defaulttests']),
357            'win_blink_compile_dbg': set(['defaulttests']),
358            'win_blink_rel': set(['defaulttests']),
359        },
360        'tryserver.chromium.gpu': {
361            'linux_gpu': set(['defaulttests']),
362            'mac_gpu': set(['defaulttests']),
363            'win_gpu': set(['defaulttests']),
364        }
365    }
366