• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
1#!/usr/bin/env python3
2# Copyright 2016 The Android Open Source Project
3#
4# Licensed under the Apache License, Version 2.0 (the "License");
5# you may not use this file except in compliance with the License.
6# You may obtain a copy of the License at
7#
8#      http://www.apache.org/licenses/LICENSE-2.0
9#
10# Unless required by applicable law or agreed to in writing, software
11# distributed under the License is distributed on an "AS IS" BASIS,
12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13# See the License for the specific language governing permissions and
14# limitations under the License.
15
16"""Unittests for the hooks module."""
17
18import os
19import sys
20import unittest
21from unittest import mock
22
23_path = os.path.realpath(__file__ + '/../..')
24if sys.path[0] != _path:
25    sys.path.insert(0, _path)
26del _path
27
28# We have to import our local modules after the sys.path tweak.  We can't use
29# relative imports because this is an executable program, not a module.
30# pylint: disable=wrong-import-position
31import rh
32import rh.config
33import rh.hooks
34
35
36# pylint: disable=unused-argument
37def mock_find_repo_root(path=None, outer=False):
38    return '/ ${BUILD_OS}' if outer else '/ ${BUILD_OS}/sub'
39
40
41class HooksDocsTests(unittest.TestCase):
42    """Make sure all hook features are documented.
43
44    Note: These tests are a bit hokey in that they parse README.md.  But they
45    get the job done, so that's all that matters right?
46    """
47
48    def setUp(self):
49        self.readme = os.path.join(os.path.dirname(os.path.dirname(
50            os.path.realpath(__file__))), 'README.md')
51
52    def _grab_section(self, section):
53        """Extract the |section| text out of the readme."""
54        ret = []
55        in_section = False
56        with open(self.readme, encoding='utf-8') as fp:
57            for line in fp:
58                if not in_section:
59                    # Look for the section like "## [Tool Paths]".
60                    if (line.startswith('#') and
61                            line.lstrip('#').strip() == section):
62                        in_section = True
63                else:
64                    # Once we hit the next section (higher or lower), break.
65                    if line[0] == '#':
66                        break
67                    ret.append(line)
68        return ''.join(ret)
69
70    def testBuiltinHooks(self):
71        """Verify builtin hooks are documented."""
72        data = self._grab_section('[Builtin Hooks]')
73        for hook in rh.hooks.BUILTIN_HOOKS:
74            self.assertIn(f'* `{hook}`:', data,
75                          msg=f'README.md missing docs for hook "{hook}"')
76
77    def testToolPaths(self):
78        """Verify tools are documented."""
79        data = self._grab_section('[Tool Paths]')
80        for tool in rh.hooks.TOOL_PATHS:
81            self.assertIn(f'* `{tool}`:', data,
82                          msg=f'README.md missing docs for tool "{tool}"')
83
84    def testPlaceholders(self):
85        """Verify placeholder replacement vars are documented."""
86        data = self._grab_section('Placeholders')
87        for var in rh.hooks.Placeholders.vars():
88            self.assertIn('* `${' + var + '}`:', data,
89                          msg=f'README.md missing docs for var "{var}"')
90
91
92class PlaceholderTests(unittest.TestCase):
93    """Verify behavior of replacement variables."""
94
95    def setUp(self):
96        self._saved_environ = os.environ.copy()
97        os.environ.update({
98            'PREUPLOAD_COMMIT_MESSAGE': 'commit message',
99            'PREUPLOAD_COMMIT': '5c4c293174bb61f0f39035a71acd9084abfa743d',
100        })
101        self.replacer = rh.hooks.Placeholders(
102            [rh.git.RawDiffEntry(file=x)
103             for x in ['path1/file1', 'path2/file2']])
104
105    def tearDown(self):
106        os.environ.clear()
107        os.environ.update(self._saved_environ)
108
109    def testVars(self):
110        """Light test for the vars inspection generator."""
111        ret = list(self.replacer.vars())
112        self.assertGreater(len(ret), 4)
113        self.assertIn('PREUPLOAD_COMMIT', ret)
114
115    @mock.patch.object(rh.git, 'find_repo_root',
116                       side_effect=mock_find_repo_root)
117    def testExpandVars(self, _m):
118        """Verify the replacement actually works."""
119        input_args = [
120            # Verify ${REPO_ROOT} is updated, but not REPO_ROOT.
121            # We also make sure that things in ${REPO_ROOT} are not double
122            # expanded (which is why the return includes ${BUILD_OS}).
123            '${REPO_ROOT}/some/prog/REPO_ROOT/ok',
124            # Verify that ${REPO_OUTER_ROOT} is expanded.
125            '${REPO_OUTER_ROOT}/some/prog/REPO_OUTER_ROOT/ok',
126            # Verify lists are merged rather than inserted.
127            '${PREUPLOAD_FILES}',
128            # Verify each file is preceded with '--file=' prefix.
129            '--file=${PREUPLOAD_FILES_PREFIXED}',
130            # Verify each file is preceded with '--file' argument.
131            '--file',
132            '${PREUPLOAD_FILES_PREFIXED}',
133            # Verify values with whitespace don't expand into multiple args.
134            '${PREUPLOAD_COMMIT_MESSAGE}',
135            # Verify multiple values get replaced.
136            '${PREUPLOAD_COMMIT}^${PREUPLOAD_COMMIT_MESSAGE}',
137            # Unknown vars should be left alone.
138            '${THIS_VAR_IS_GOOD}',
139        ]
140        output_args = self.replacer.expand_vars(input_args)
141        exp_args = [
142            '/ ${BUILD_OS}/sub/some/prog/REPO_ROOT/ok',
143            '/ ${BUILD_OS}/some/prog/REPO_OUTER_ROOT/ok',
144            'path1/file1',
145            'path2/file2',
146            '--file=path1/file1',
147            '--file=path2/file2',
148            '--file',
149            'path1/file1',
150            '--file',
151            'path2/file2',
152            'commit message',
153            '5c4c293174bb61f0f39035a71acd9084abfa743d^commit message',
154            '${THIS_VAR_IS_GOOD}',
155        ]
156        self.assertEqual(output_args, exp_args)
157
158    def testTheTester(self):
159        """Make sure we have a test for every variable."""
160        for var in self.replacer.vars():
161            self.assertIn(f'test{var}', dir(self),
162                          msg=f'Missing unittest for variable {var}')
163
164    def testPREUPLOAD_COMMIT_MESSAGE(self):
165        """Verify handling of PREUPLOAD_COMMIT_MESSAGE."""
166        self.assertEqual(self.replacer.get('PREUPLOAD_COMMIT_MESSAGE'),
167                         'commit message')
168
169    def testPREUPLOAD_COMMIT(self):
170        """Verify handling of PREUPLOAD_COMMIT."""
171        self.assertEqual(self.replacer.get('PREUPLOAD_COMMIT'),
172                         '5c4c293174bb61f0f39035a71acd9084abfa743d')
173
174    def testPREUPLOAD_FILES(self):
175        """Verify handling of PREUPLOAD_FILES."""
176        self.assertEqual(self.replacer.get('PREUPLOAD_FILES'),
177                         ['path1/file1', 'path2/file2'])
178
179    @mock.patch.object(rh.git, 'find_repo_root')
180    def testREPO_OUTER_ROOT(self, m):
181        """Verify handling of REPO_OUTER_ROOT."""
182        m.side_effect = mock_find_repo_root
183        self.assertEqual(self.replacer.get('REPO_OUTER_ROOT'),
184                         mock_find_repo_root(path=None, outer=True))
185
186    @mock.patch.object(rh.git, 'find_repo_root')
187    def testREPO_ROOT(self, m):
188        """Verify handling of REPO_ROOT."""
189        m.side_effect = mock_find_repo_root
190        self.assertEqual(self.replacer.get('REPO_ROOT'),
191                         mock_find_repo_root(path=None, outer=False))
192
193    def testREPO_PATH(self):
194        """Verify handling of REPO_PATH."""
195        os.environ['REPO_PATH'] = ''
196        self.assertEqual(self.replacer.get('REPO_PATH'), '')
197        os.environ['REPO_PATH'] = 'foo/bar'
198        self.assertEqual(self.replacer.get('REPO_PATH'), 'foo/bar')
199
200    @mock.patch.object(rh.hooks, '_get_build_os_name', return_value='vapier os')
201    def testBUILD_OS(self, m):
202        """Verify handling of BUILD_OS."""
203        self.assertEqual(self.replacer.get('BUILD_OS'), m.return_value)
204
205
206class ExclusionScopeTests(unittest.TestCase):
207    """Verify behavior of ExclusionScope class."""
208
209    def testEmpty(self):
210        """Verify the in operator for an empty scope."""
211        scope = rh.hooks.ExclusionScope([])
212        self.assertNotIn('external/*', scope)
213
214    def testGlob(self):
215        """Verify the in operator for a scope using wildcards."""
216        scope = rh.hooks.ExclusionScope(['vendor/*', 'external/*'])
217        self.assertIn('external/tools', scope)
218
219    def testRegex(self):
220        """Verify the in operator for a scope using regular expressions."""
221        scope = rh.hooks.ExclusionScope(['^vendor/(?!google)',
222                                         'external/*'])
223        self.assertIn('vendor/', scope)
224        self.assertNotIn('vendor/google/', scope)
225        self.assertIn('vendor/other/', scope)
226
227
228class HookOptionsTests(unittest.TestCase):
229    """Verify behavior of HookOptions object."""
230
231    @mock.patch.object(rh.hooks, '_get_build_os_name', return_value='vapier os')
232    def testExpandVars(self, m):
233        """Verify expand_vars behavior."""
234        # Simple pass through.
235        args = ['who', 'goes', 'there ?']
236        self.assertEqual(args, rh.hooks.HookOptions.expand_vars(args))
237
238        # At least one replacement.  Most real testing is in PlaceholderTests.
239        args = ['who', 'goes', 'there ?', '${BUILD_OS} is great']
240        exp_args = ['who', 'goes', 'there ?', f'{m.return_value} is great']
241        self.assertEqual(exp_args, rh.hooks.HookOptions.expand_vars(args))
242
243    def testArgs(self):
244        """Verify args behavior."""
245        # Verify initial args to __init__ has higher precedent.
246        args = ['start', 'args']
247        options = rh.hooks.HookOptions('hook name', args, {})
248        self.assertEqual(options.args(), args)
249        self.assertEqual(options.args(default_args=['moo']), args)
250
251        # Verify we fall back to default_args.
252        args = ['default', 'args']
253        options = rh.hooks.HookOptions('hook name', [], {})
254        self.assertEqual(options.args(), [])
255        self.assertEqual(options.args(default_args=args), args)
256
257    def testToolPath(self):
258        """Verify tool_path behavior."""
259        options = rh.hooks.HookOptions('hook name', [], {
260            'cpplint': 'my cpplint',
261        })
262        # Check a builtin (and not overridden) tool.
263        self.assertEqual(options.tool_path('pylint'), 'pylint')
264        # Check an overridden tool.
265        self.assertEqual(options.tool_path('cpplint'), 'my cpplint')
266        # Check an unknown tool fails.
267        self.assertRaises(AssertionError, options.tool_path, 'extra_tool')
268
269
270class UtilsTests(unittest.TestCase):
271    """Verify misc utility functions."""
272
273    def testRunCommand(self):
274        """Check _run behavior."""
275        # Most testing is done against the utils.RunCommand already.
276        # pylint: disable=protected-access
277        ret = rh.hooks._run(['true'])
278        self.assertEqual(ret.returncode, 0)
279
280    def testBuildOs(self):
281        """Check _get_build_os_name behavior."""
282        # Just verify it returns something and doesn't crash.
283        # pylint: disable=protected-access
284        ret = rh.hooks._get_build_os_name()
285        self.assertTrue(isinstance(ret, str))
286        self.assertNotEqual(ret, '')
287
288    def testGetHelperPath(self):
289        """Check get_helper_path behavior."""
290        # Just verify it doesn't crash.  It's a dirt simple func.
291        ret = rh.hooks.get_helper_path('booga')
292        self.assertTrue(isinstance(ret, str))
293        self.assertNotEqual(ret, '')
294
295    def testSortedToolPaths(self):
296        """Check TOOL_PATHS is sorted."""
297        # This assumes dictionary key ordering matches insertion/definition
298        # order which Python 3.7+ has codified.
299        # https://docs.python.org/3.7/library/stdtypes.html#dict
300        self.assertEqual(list(rh.hooks.TOOL_PATHS), sorted(rh.hooks.TOOL_PATHS))
301
302    def testSortedBuiltinHooks(self):
303        """Check BUILTIN_HOOKS is sorted."""
304        # This assumes dictionary key ordering matches insertion/definition
305        # order which Python 3.7+ has codified.
306        # https://docs.python.org/3.7/library/stdtypes.html#dict
307        self.assertEqual(
308            list(rh.hooks.BUILTIN_HOOKS), sorted(rh.hooks.BUILTIN_HOOKS))
309
310
311@mock.patch.object(rh.utils, 'run')
312@mock.patch.object(rh.hooks, '_check_cmd', return_value=['check_cmd'])
313class BuiltinHooksTests(unittest.TestCase):
314    """Verify the builtin hooks."""
315
316    def setUp(self):
317        self.project = rh.Project(name='project-name', dir='/.../repo/dir')
318        self.options = rh.hooks.HookOptions('hook name', [], {})
319
320    def _test_commit_messages(self, func, accept, msgs, files=None):
321        """Helper for testing commit message hooks.
322
323        Args:
324          func: The hook function to test.
325          accept: Whether all the |msgs| should be accepted.
326          msgs: List of messages to test.
327          files: List of files to pass to the hook.
328        """
329        if files:
330            diff = [rh.git.RawDiffEntry(file=x) for x in files]
331        else:
332            diff = []
333        for desc in msgs:
334            ret = func(self.project, 'commit', desc, diff, options=self.options)
335            if accept:
336                self.assertFalse(
337                    bool(ret), msg='Should have accepted: {{{' + desc + '}}}')
338            else:
339                self.assertTrue(
340                    bool(ret), msg='Should have rejected: {{{' + desc + '}}}')
341
342    def _test_file_filter(self, mock_check, func, files):
343        """Helper for testing hooks that filter by files and run external tools.
344
345        Args:
346          mock_check: The mock of _check_cmd.
347          func: The hook function to test.
348          files: A list of files that we'd check.
349        """
350        # First call should do nothing as there are no files to check.
351        ret = func(self.project, 'commit', 'desc', (), options=self.options)
352        self.assertIsNone(ret)
353        self.assertFalse(mock_check.called)
354
355        # Second call should include some checks.
356        diff = [rh.git.RawDiffEntry(file=x) for x in files]
357        ret = func(self.project, 'commit', 'desc', diff, options=self.options)
358        self.assertEqual(ret, mock_check.return_value)
359
360    def testTheTester(self, _mock_check, _mock_run):
361        """Make sure we have a test for every hook."""
362        for hook in rh.hooks.BUILTIN_HOOKS:
363            self.assertIn(f'test_{hook}', dir(self),
364                          msg=f'Missing unittest for builtin hook {hook}')
365
366    def test_bpfmt(self, mock_check, _mock_run):
367        """Verify the bpfmt builtin hook."""
368        # First call should do nothing as there are no files to check.
369        ret = rh.hooks.check_bpfmt(
370            self.project, 'commit', 'desc', (), options=self.options)
371        self.assertIsNone(ret)
372        self.assertFalse(mock_check.called)
373
374        # Second call will have some results.
375        diff = [rh.git.RawDiffEntry(file='Android.bp')]
376        ret = rh.hooks.check_bpfmt(
377            self.project, 'commit', 'desc', diff, options=self.options)
378        self.assertIsNotNone(ret)
379        for result in ret:
380            self.assertIsNotNone(result.fixup_cmd)
381
382    def test_checkpatch(self, mock_check, _mock_run):
383        """Verify the checkpatch builtin hook."""
384        ret = rh.hooks.check_checkpatch(
385            self.project, 'commit', 'desc', (), options=self.options)
386        self.assertEqual(ret, mock_check.return_value)
387
388    def test_clang_format(self, mock_check, _mock_run):
389        """Verify the clang_format builtin hook."""
390        ret = rh.hooks.check_clang_format(
391            self.project, 'commit', 'desc', (), options=self.options)
392        self.assertEqual(ret, mock_check.return_value)
393
394    def test_google_java_format(self, mock_check, _mock_run):
395        """Verify the google_java_format builtin hook."""
396        ret = rh.hooks.check_google_java_format(
397            self.project, 'commit', 'desc', (), options=self.options)
398        self.assertEqual(ret, mock_check.return_value)
399
400    def test_commit_msg_bug_field(self, _mock_check, _mock_run):
401        """Verify the commit_msg_bug_field builtin hook."""
402        # Check some good messages.
403        self._test_commit_messages(
404            rh.hooks.check_commit_msg_bug_field, True, (
405                'subj\n\nBug: 1234\n',
406                'subj\n\nBug: 1234\nChange-Id: blah\n',
407            ))
408
409        # Check some bad messages.
410        self._test_commit_messages(
411            rh.hooks.check_commit_msg_bug_field, False, (
412                'subj',
413                'subj\n\nBUG=1234\n',
414                'subj\n\nBUG: 1234\n',
415                'subj\n\nBug: N/A\n',
416                'subj\n\nBug:\n',
417            ))
418
419    def test_commit_msg_changeid_field(self, _mock_check, _mock_run):
420        """Verify the commit_msg_changeid_field builtin hook."""
421        # Check some good messages.
422        self._test_commit_messages(
423            rh.hooks.check_commit_msg_changeid_field, True, (
424                'subj\n\nChange-Id: I1234\n',
425            ))
426
427        # Check some bad messages.
428        self._test_commit_messages(
429            rh.hooks.check_commit_msg_changeid_field, False, (
430                'subj',
431                'subj\n\nChange-Id: 1234\n',
432                'subj\n\nChange-ID: I1234\n',
433            ))
434
435    def test_commit_msg_prebuilt_apk_fields(self, _mock_check, _mock_run):
436        """Verify the check_commit_msg_prebuilt_apk_fields builtin hook."""
437        # Commits without APKs should pass.
438        self._test_commit_messages(
439            rh.hooks.check_commit_msg_prebuilt_apk_fields,
440            True,
441            (
442                'subj\nTest: test case\nBug: bug id\n',
443            ),
444            ['foo.cpp', 'bar.py',]
445        )
446
447        # Commits with APKs and all the required messages should pass.
448        self._test_commit_messages(
449            rh.hooks.check_commit_msg_prebuilt_apk_fields,
450            True,
451            (
452                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
453                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
454                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
455                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
456                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
457                 'http://foo.bar.com/builder\n\n'
458                 'This build IS suitable for public release.\n\n'
459                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
460                ('Test App\n\nBuilt here:\nhttp://foo.bar.com/builder\n\n'
461                 'This build IS NOT suitable for public release.\n\n'
462                 'bar.apk\npackage: name=\'com.foo.bar\'\n'
463                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
464                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
465                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
466                 'targetSdkVersion:\'28\'\n\nBug: 123\nTest: test\n'
467                 'Change-Id: XXXXXXX\n'),
468                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
469                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
470                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
471                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
472                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
473                 'http://foo.bar.com/builder\n\n'
474                 'This build IS suitable for preview release but IS NOT '
475                 'suitable for public release.\n\n'
476                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
477                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
478                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
479                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
480                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
481                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
482                 'http://foo.bar.com/builder\n\n'
483                 'This build IS NOT suitable for preview or public release.\n\n'
484                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
485            ),
486            ['foo.apk', 'bar.py',]
487        )
488
489        # Commits with APKs and without all the required messages should fail.
490        self._test_commit_messages(
491            rh.hooks.check_commit_msg_prebuilt_apk_fields,
492            False,
493            (
494                'subj\nTest: test case\nBug: bug id\n',
495                # Missing 'package'.
496                ('Test App\n\nbar.apk\n'
497                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
498                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
499                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
500                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
501                 'http://foo.bar.com/builder\n\n'
502                 'This build IS suitable for public release.\n\n'
503                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
504                # Missing 'sdkVersion'.
505                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
506                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
507                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
508                 'compileSdkVersionCodename=\'9\'\n'
509                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
510                 'http://foo.bar.com/builder\n\n'
511                 'This build IS suitable for public release.\n\n'
512                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
513                # Missing 'targetSdkVersion'.
514                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
515                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
516                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
517                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
518                 'Built here:\nhttp://foo.bar.com/builder\n\n'
519                 'This build IS suitable for public release.\n\n'
520                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
521                # Missing build location.
522                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
523                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
524                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
525                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
526                 'targetSdkVersion:\'28\'\n\n'
527                 'This build IS suitable for public release.\n\n'
528                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
529                # Missing public release indication.
530                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
531                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
532                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
533                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
534                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
535                 'http://foo.bar.com/builder\n\n'
536                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
537            ),
538            ['foo.apk', 'bar.py',]
539        )
540
541    def test_commit_msg_test_field(self, _mock_check, _mock_run):
542        """Verify the commit_msg_test_field builtin hook."""
543        # Check some good messages.
544        self._test_commit_messages(
545            rh.hooks.check_commit_msg_test_field, True, (
546                'subj\n\nTest: i did done dood it\n',
547            ))
548
549        # Check some bad messages.
550        self._test_commit_messages(
551            rh.hooks.check_commit_msg_test_field, False, (
552                'subj',
553                'subj\n\nTEST=1234\n',
554                'subj\n\nTEST: I1234\n',
555            ))
556
557    def test_commit_msg_relnote_field_format(self, _mock_check, _mock_run):
558        """Verify the commit_msg_relnote_field_format builtin hook."""
559        # Check some good messages.
560        self._test_commit_messages(
561            rh.hooks.check_commit_msg_relnote_field_format,
562            True,
563            (
564                'subj',
565                'subj\n\nTest: i did done dood it\nBug: 1234',
566                'subj\n\nMore content\n\nTest: i did done dood it\nBug: 1234',
567                'subj\n\nRelnote: This is a release note\nBug: 1234',
568                'subj\n\nRelnote:This is a release note\nBug: 1234',
569                'subj\n\nRelnote: This is a release note.\nBug: 1234',
570                'subj\n\nRelnote: "This is a release note."\nBug: 1234',
571                'subj\n\nRelnote: "This is a \\"release note\\"."\n\nBug: 1234',
572                'subj\n\nRelnote: This is a release note.\nChange-Id: 1234',
573                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
574                ('subj\n\nRelnote: "This is a release note."\n\n'
575                 'Change-Id: 1234'),
576                ('subj\n\nRelnote: This is a release note.\n\n'
577                 'It has more info, but it is not part of the release note'
578                 '\nChange-Id: 1234'),
579                ('subj\n\nRelnote: "This is a release note.\n'
580                 'It contains a correct second line."'),
581                ('subj\n\nRelnote:"This is a release note.\n'
582                 'It contains a correct second line."'),
583                ('subj\n\nRelnote: "This is a release note.\n'
584                 'It contains a correct second line.\n'
585                 'And even a third line."\n'
586                 'Bug: 1234'),
587                ('subj\n\nRelnote: "This is a release note.\n'
588                 'It contains a correct second line.\n'
589                 '\\"Quotes\\" are even used on the third line."\n'
590                 'Bug: 1234'),
591                ('subj\n\nRelnote: This is release note 1.\n'
592                 'Relnote: This is release note 2.\n'
593                 'Bug: 1234'),
594                ('subj\n\nRelnote: This is release note 1.\n'
595                 'Relnote: "This is release note 2, and it\n'
596                 'contains a correctly formatted third line."\n'
597                 'Bug: 1234'),
598                ('subj\n\nRelnote: "This is release note 1 with\n'
599                 'a correctly formatted second line."\n\n'
600                 'Relnote: "This is release note 2, and it\n'
601                 'contains a correctly formatted second line."\n'
602                 'Bug: 1234'),
603                ('subj\n\nRelnote: "This is a release note with\n'
604                 'a correctly formatted second line."\n\n'
605                 'Bug: 1234'
606                 'Here is some extra "quoted" content.'),
607                ('subj\n\nRelnote: """This is a release note.\n\n'
608                 'This relnote contains an empty line.\n'
609                 'Then a non-empty line.\n\n'
610                 'And another empty line."""\n\n'
611                 'Bug: 1234'),
612                ('subj\n\nRelnote: """This is a release note.\n\n'
613                 'This relnote contains an empty line.\n'
614                 'Then an acceptable "quoted" line.\n\n'
615                 'And another empty line."""\n\n'
616                 'Bug: 1234'),
617                ('subj\n\nRelnote: """This is a release note."""\n\n'
618                 'Bug: 1234'),
619                ('subj\n\nRelnote: """This is a release note.\n'
620                 'It has a second line."""\n\n'
621                 'Bug: 1234'),
622                ('subj\n\nRelnote: """This is a release note.\n'
623                 'It has a second line, but does not end here.\n'
624                 '"""\n\n'
625                 'Bug: 1234'),
626                ('subj\n\nRelnote: """This is a release note.\n'
627                 '"It" has a second line, but does not end here.\n'
628                 '"""\n\n'
629                 'Bug: 1234'),
630                ('subj\n\nRelnote: "This is a release note.\n'
631                 'It has a second line, but does not end here.\n'
632                 '"\n\n'
633                 'Bug: 1234'),
634            ))
635
636        # Check some bad messages.
637        self._test_commit_messages(
638            rh.hooks.check_commit_msg_relnote_field_format,
639            False,
640            (
641                'subj\n\nReleaseNote: This is a release note.\n',
642                'subj\n\nRelnotes: This is a release note.\n',
643                'subj\n\nRel-note: This is a release note.\n',
644                'subj\n\nrelnoTes: This is a release note.\n',
645                'subj\n\nrel-Note: This is a release note.\n',
646                'subj\n\nRelnote: "This is a "release note"."\nBug: 1234',
647                'subj\n\nRelnote: This is a "release note".\nBug: 1234',
648                ('subj\n\nRelnote: This is a release note.\n'
649                 'It contains an incorrect second line.'),
650                ('subj\n\nRelnote: "This is a release note.\n'
651                 'It contains multiple lines.\n'
652                 'But it does not provide an ending quote.\n'),
653                ('subj\n\nRelnote: "This is a release note.\n'
654                 'It contains multiple lines but no closing quote.\n'
655                 'Test: my test "hello world"\n'),
656                ('subj\n\nRelnote: This is release note 1.\n'
657                 'Relnote: "This is release note 2, and it\n'
658                 'contains an incorrectly formatted third line.\n'
659                 'Bug: 1234'),
660                ('subj\n\nRelnote: This is release note 1 with\n'
661                 'an incorrectly formatted second line.\n\n'
662                 'Relnote: "This is release note 2, and it\n'
663                 'contains a correctly formatted second line."\n'
664                 'Bug: 1234'),
665                ('subj\n\nRelnote: "This is release note 1 with\n'
666                 'a correctly formatted second line."\n\n'
667                 'Relnote: This is release note 2, and it\n'
668                 'contains an incorrectly formatted second line.\n'
669                 'Bug: 1234'),
670                ('subj\n\nRelnote: "This is a release note.\n'
671                 'It contains a correct second line.\n'
672                 'But incorrect "quotes" on the third line."\n'
673                 'Bug: 1234'),
674                ('subj\n\nRelnote: """This is a release note.\n'
675                 'It has a second line, but no closing triple quote.\n\n'
676                 'Bug: 1234'),
677                ('subj\n\nRelnote: "This is a release note.\n'
678                 '"It" has a second line, but does not end here.\n'
679                 '"\n\n'
680                 'Bug: 1234'),
681            ))
682
683    def test_commit_msg_relnote_for_current_txt(self, _mock_check, _mock_run):
684        """Verify the commit_msg_relnote_for_current_txt builtin hook."""
685        diff_without_current_txt = ['bar/foo.txt',
686                                    'foo.cpp',
687                                    'foo.java',
688                                    'foo_current.java',
689                                    'foo_current.txt',
690                                    'baz/current.java',
691                                    'baz/foo_current.txt']
692        diff_with_current_txt = diff_without_current_txt + ['current.txt']
693        diff_with_subdir_current_txt = \
694            diff_without_current_txt + ['foo/current.txt']
695        diff_with_experimental_current_txt = \
696            diff_without_current_txt + ['public_plus_experimental_current.txt']
697        # Check some good messages.
698        self._test_commit_messages(
699            rh.hooks.check_commit_msg_relnote_for_current_txt,
700            True,
701            (
702                'subj\n\nRelnote: This is a release note\n',
703                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
704                ('subj\n\nRelnote: This is release note 1 with\n'
705                 'an incorrectly formatted second line.\n\n'
706                 'Relnote: "This is release note 2, and it\n'
707                 'contains a correctly formatted second line."\n'
708                 'Bug: 1234'),
709            ),
710            files=diff_with_current_txt,
711        )
712        # Check some good messages.
713        self._test_commit_messages(
714            rh.hooks.check_commit_msg_relnote_for_current_txt,
715            True,
716            (
717                'subj\n\nRelnote: This is a release note\n',
718                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
719                ('subj\n\nRelnote: This is release note 1 with\n'
720                 'an incorrectly formatted second line.\n\n'
721                 'Relnote: "This is release note 2, and it\n'
722                 'contains a correctly formatted second line."\n'
723                 'Bug: 1234'),
724            ),
725            files=diff_with_experimental_current_txt,
726        )
727        # Check some good messages.
728        self._test_commit_messages(
729            rh.hooks.check_commit_msg_relnote_for_current_txt,
730            True,
731            (
732                'subj\n\nRelnote: This is a release note\n',
733                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
734                ('subj\n\nRelnote: This is release note 1 with\n'
735                 'an incorrectly formatted second line.\n\n'
736                 'Relnote: "This is release note 2, and it\n'
737                 'contains a correctly formatted second line."\n'
738                 'Bug: 1234'),
739            ),
740            files=diff_with_subdir_current_txt,
741        )
742        # Check some good messages.
743        self._test_commit_messages(
744            rh.hooks.check_commit_msg_relnote_for_current_txt,
745            True,
746            (
747                'subj',
748                'subj\nBug: 12345\nChange-Id: 1234',
749                'subj\n\nRelnote: This is a release note\n',
750                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
751                ('subj\n\nRelnote: This is release note 1 with\n'
752                 'an incorrectly formatted second line.\n\n'
753                 'Relnote: "This is release note 2, and it\n'
754                 'contains a correctly formatted second line."\n'
755                 'Bug: 1234'),
756            ),
757            files=diff_without_current_txt,
758        )
759        # Check some bad messages.
760        self._test_commit_messages(
761            rh.hooks.check_commit_msg_relnote_for_current_txt,
762            False,
763            (
764                'subj'
765                'subj\nBug: 12345\nChange-Id: 1234',
766            ),
767            files=diff_with_current_txt,
768        )
769        # Check some bad messages.
770        self._test_commit_messages(
771            rh.hooks.check_commit_msg_relnote_for_current_txt,
772            False,
773            (
774                'subj'
775                'subj\nBug: 12345\nChange-Id: 1234',
776            ),
777            files=diff_with_experimental_current_txt,
778        )
779        # Check some bad messages.
780        self._test_commit_messages(
781            rh.hooks.check_commit_msg_relnote_for_current_txt,
782            False,
783            (
784                'subj'
785                'subj\nBug: 12345\nChange-Id: 1234',
786            ),
787            files=diff_with_subdir_current_txt,
788        )
789
790    def test_cpplint(self, mock_check, _mock_run):
791        """Verify the cpplint builtin hook."""
792        self._test_file_filter(mock_check, rh.hooks.check_cpplint,
793                               ('foo.cpp', 'foo.cxx'))
794
795    def test_gofmt(self, mock_check, _mock_run):
796        """Verify the gofmt builtin hook."""
797        # First call should do nothing as there are no files to check.
798        ret = rh.hooks.check_gofmt(
799            self.project, 'commit', 'desc', (), options=self.options)
800        self.assertIsNone(ret)
801        self.assertFalse(mock_check.called)
802
803        # Second call will have some results.
804        diff = [rh.git.RawDiffEntry(file='foo.go')]
805        ret = rh.hooks.check_gofmt(
806            self.project, 'commit', 'desc', diff, options=self.options)
807        self.assertIsNotNone(ret)
808
809    def test_jsonlint(self, mock_check, _mock_run):
810        """Verify the jsonlint builtin hook."""
811        # First call should do nothing as there are no files to check.
812        ret = rh.hooks.check_json(
813            self.project, 'commit', 'desc', (), options=self.options)
814        self.assertIsNone(ret)
815        self.assertFalse(mock_check.called)
816
817        # TODO: Actually pass some valid/invalid json data down.
818
819    def test_ktfmt(self, mock_check, _mock_run):
820        """Verify the ktfmt builtin hook."""
821        # First call should do nothing as there are no files to check.
822        ret = rh.hooks.check_ktfmt(
823            self.project, 'commit', 'desc', (), options=self.options)
824        self.assertIsNone(ret)
825        self.assertFalse(mock_check.called)
826        # Check that .kt files are included by default.
827        diff = [rh.git.RawDiffEntry(file='foo.kt'),
828                rh.git.RawDiffEntry(file='bar.java'),
829                rh.git.RawDiffEntry(file='baz/blah.kt')]
830        ret = rh.hooks.check_ktfmt(
831            self.project, 'commit', 'desc', diff, options=self.options)
832        self.assertListEqual(ret[0].files, ['foo.kt', 'baz/blah.kt'])
833        diff = [rh.git.RawDiffEntry(file='foo/f1.kt'),
834                rh.git.RawDiffEntry(file='bar/f2.kt'),
835                rh.git.RawDiffEntry(file='baz/f2.kt')]
836        ret = rh.hooks.check_ktfmt(self.project, 'commit', 'desc', diff,
837                                   options=rh.hooks.HookOptions('hook name', [
838                                       '--include-dirs=foo,baz'], {}))
839        self.assertListEqual(ret[0].files, ['foo/f1.kt', 'baz/f2.kt'])
840
841    def test_pylint(self, mock_check, _mock_run):
842        """Verify the pylint builtin hook."""
843        self._test_file_filter(mock_check, rh.hooks.check_pylint2,
844                               ('foo.py',))
845
846    def test_pylint2(self, mock_check, _mock_run):
847        """Verify the pylint2 builtin hook."""
848        self._test_file_filter(mock_check, rh.hooks.check_pylint2,
849                               ('foo.py',))
850
851    def test_pylint3(self, mock_check, _mock_run):
852        """Verify the pylint3 builtin hook."""
853        self._test_file_filter(mock_check, rh.hooks.check_pylint3,
854                               ('foo.py',))
855
856    def test_rustfmt(self, mock_check, _mock_run):
857        # First call should do nothing as there are no files to check.
858        ret = rh.hooks.check_rustfmt(
859            self.project, 'commit', 'desc', (), options=self.options)
860        self.assertEqual(ret, None)
861        self.assertFalse(mock_check.called)
862
863        # Second call will have some results.
864        diff = [rh.git.RawDiffEntry(file='lib.rs')]
865        ret = rh.hooks.check_rustfmt(
866            self.project, 'commit', 'desc', diff, options=self.options)
867        self.assertNotEqual(ret, None)
868
869    def test_xmllint(self, mock_check, _mock_run):
870        """Verify the xmllint builtin hook."""
871        self._test_file_filter(mock_check, rh.hooks.check_xmllint,
872                               ('foo.xml',))
873
874    def test_android_test_mapping_format(self, mock_check, _mock_run):
875        """Verify the android_test_mapping_format builtin hook."""
876        # First call should do nothing as there are no files to check.
877        ret = rh.hooks.check_android_test_mapping(
878            self.project, 'commit', 'desc', (), options=self.options)
879        self.assertIsNone(ret)
880        self.assertFalse(mock_check.called)
881
882        # Second call will have some results.
883        diff = [rh.git.RawDiffEntry(file='TEST_MAPPING')]
884        ret = rh.hooks.check_android_test_mapping(
885            self.project, 'commit', 'desc', diff, options=self.options)
886        self.assertIsNotNone(ret)
887
888    def test_aidl_format(self, mock_check, _mock_run):
889        """Verify the aidl_format builtin hook."""
890        # First call should do nothing as there are no files to check.
891        ret = rh.hooks.check_aidl_format(
892            self.project, 'commit', 'desc', (), options=self.options)
893        self.assertIsNone(ret)
894        self.assertFalse(mock_check.called)
895
896        # Second call will have some results.
897        diff = [rh.git.RawDiffEntry(file='IFoo.go')]
898        ret = rh.hooks.check_gofmt(
899            self.project, 'commit', 'desc', diff, options=self.options)
900        self.assertIsNotNone(ret)
901
902
903if __name__ == '__main__':
904    unittest.main()
905