xref: /aosp_15_r20/tools/repohooks/rh/hooks_unittest.py (revision d68f33bc6fb0cc2476107c2af0573a2f5a63dfc1)
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    def testREPO_PROJECT(self):
201        """Verify handling of REPO_PROJECT."""
202        os.environ['REPO_PROJECT'] = ''
203        self.assertEqual(self.replacer.get('REPO_PROJECT'), '')
204        os.environ['REPO_PROJECT'] = 'platform/foo/bar'
205        self.assertEqual(self.replacer.get('REPO_PROJECT'), 'platform/foo/bar')
206
207    @mock.patch.object(rh.hooks, '_get_build_os_name', return_value='vapier os')
208    def testBUILD_OS(self, m):
209        """Verify handling of BUILD_OS."""
210        self.assertEqual(self.replacer.get('BUILD_OS'), m.return_value)
211
212
213class ExclusionScopeTests(unittest.TestCase):
214    """Verify behavior of ExclusionScope class."""
215
216    def testEmpty(self):
217        """Verify the in operator for an empty scope."""
218        scope = rh.hooks.ExclusionScope([])
219        self.assertNotIn('external/*', scope)
220
221    def testGlob(self):
222        """Verify the in operator for a scope using wildcards."""
223        scope = rh.hooks.ExclusionScope(['vendor/*', 'external/*'])
224        self.assertIn('external/tools', scope)
225
226    def testRegex(self):
227        """Verify the in operator for a scope using regular expressions."""
228        scope = rh.hooks.ExclusionScope(['^vendor/(?!google)',
229                                         'external/*'])
230        self.assertIn('vendor/', scope)
231        self.assertNotIn('vendor/google/', scope)
232        self.assertIn('vendor/other/', scope)
233
234
235class HookOptionsTests(unittest.TestCase):
236    """Verify behavior of HookOptions object."""
237
238    @mock.patch.object(rh.hooks, '_get_build_os_name', return_value='vapier os')
239    def testExpandVars(self, m):
240        """Verify expand_vars behavior."""
241        # Simple pass through.
242        args = ['who', 'goes', 'there ?']
243        self.assertEqual(args, rh.hooks.HookOptions.expand_vars(args))
244
245        # At least one replacement.  Most real testing is in PlaceholderTests.
246        args = ['who', 'goes', 'there ?', '${BUILD_OS} is great']
247        exp_args = ['who', 'goes', 'there ?', f'{m.return_value} is great']
248        self.assertEqual(exp_args, rh.hooks.HookOptions.expand_vars(args))
249
250    def testArgs(self):
251        """Verify args behavior."""
252        # Verify initial args to __init__ has higher precedent.
253        args = ['start', 'args']
254        options = rh.hooks.HookOptions('hook name', args, {})
255        self.assertEqual(options.args(), args)
256        self.assertEqual(options.args(default_args=['moo']), args)
257
258        # Verify we fall back to default_args.
259        args = ['default', 'args']
260        options = rh.hooks.HookOptions('hook name', [], {})
261        self.assertEqual(options.args(), [])
262        self.assertEqual(options.args(default_args=args), args)
263
264    def testToolPath(self):
265        """Verify tool_path behavior."""
266        options = rh.hooks.HookOptions('hook name', [], {
267            'cpplint': 'my cpplint',
268        })
269        # Check a builtin (and not overridden) tool.
270        self.assertEqual(options.tool_path('pylint'), 'pylint')
271        # Check an overridden tool.
272        self.assertEqual(options.tool_path('cpplint'), 'my cpplint')
273        # Check an unknown tool fails.
274        self.assertRaises(AssertionError, options.tool_path, 'extra_tool')
275
276
277class UtilsTests(unittest.TestCase):
278    """Verify misc utility functions."""
279
280    def testRunCommand(self):
281        """Check _run behavior."""
282        # Most testing is done against the utils.RunCommand already.
283        # pylint: disable=protected-access
284        ret = rh.hooks._run(['true'])
285        self.assertEqual(ret.returncode, 0)
286
287    def testBuildOs(self):
288        """Check _get_build_os_name behavior."""
289        # Just verify it returns something and doesn't crash.
290        # pylint: disable=protected-access
291        ret = rh.hooks._get_build_os_name()
292        self.assertTrue(isinstance(ret, str))
293        self.assertNotEqual(ret, '')
294
295    def testGetHelperPath(self):
296        """Check get_helper_path behavior."""
297        # Just verify it doesn't crash.  It's a dirt simple func.
298        ret = rh.hooks.get_helper_path('booga')
299        self.assertTrue(isinstance(ret, str))
300        self.assertNotEqual(ret, '')
301
302    def testSortedToolPaths(self):
303        """Check TOOL_PATHS 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(list(rh.hooks.TOOL_PATHS), sorted(rh.hooks.TOOL_PATHS))
308
309    def testSortedBuiltinHooks(self):
310        """Check BUILTIN_HOOKS is sorted."""
311        # This assumes dictionary key ordering matches insertion/definition
312        # order which Python 3.7+ has codified.
313        # https://docs.python.org/3.7/library/stdtypes.html#dict
314        self.assertEqual(
315            list(rh.hooks.BUILTIN_HOOKS), sorted(rh.hooks.BUILTIN_HOOKS))
316
317
318@mock.patch.object(rh.utils, 'run')
319@mock.patch.object(rh.hooks, '_check_cmd', return_value=['check_cmd'])
320class BuiltinHooksTests(unittest.TestCase):
321    """Verify the builtin hooks."""
322
323    def setUp(self):
324        self.project = rh.Project(name='project-name', dir='/.../repo/dir')
325        self.options = rh.hooks.HookOptions('hook name', [], {})
326
327    def _test_commit_messages(self, func, accept, msgs, files=None):
328        """Helper for testing commit message hooks.
329
330        Args:
331          func: The hook function to test.
332          accept: Whether all the |msgs| should be accepted.
333          msgs: List of messages to test.
334          files: List of files to pass to the hook.
335        """
336        if files:
337            diff = [rh.git.RawDiffEntry(file=x) for x in files]
338        else:
339            diff = []
340        for desc in msgs:
341            ret = func(self.project, 'commit', desc, diff, options=self.options)
342            if accept:
343                self.assertFalse(
344                    bool(ret), msg='Should have accepted: {{{' + desc + '}}}')
345            else:
346                self.assertTrue(
347                    bool(ret), msg='Should have rejected: {{{' + desc + '}}}')
348
349    def _test_file_filter(self, mock_check, func, files):
350        """Helper for testing hooks that filter by files and run external tools.
351
352        Args:
353          mock_check: The mock of _check_cmd.
354          func: The hook function to test.
355          files: A list of files that we'd check.
356        """
357        # First call should do nothing as there are no files to check.
358        ret = func(self.project, 'commit', 'desc', (), options=self.options)
359        self.assertIsNone(ret)
360        self.assertFalse(mock_check.called)
361
362        # Second call should include some checks.
363        diff = [rh.git.RawDiffEntry(file=x) for x in files]
364        ret = func(self.project, 'commit', 'desc', diff, options=self.options)
365        self.assertEqual(ret, mock_check.return_value)
366
367    def testTheTester(self, _mock_check, _mock_run):
368        """Make sure we have a test for every hook."""
369        for hook in rh.hooks.BUILTIN_HOOKS:
370            self.assertIn(f'test_{hook}', dir(self),
371                          msg=f'Missing unittest for builtin hook {hook}')
372
373    def test_aosp_license(self, mock_check, _mock_run):
374        """Verify the aosp_license builtin hook."""
375        # First call should do nothing as there are no files to check.
376        diff = [
377            rh.git.RawDiffEntry(file='d.bp', status='D'),
378            rh.git.RawDiffEntry(file='m.bp', status='M'),
379            rh.git.RawDiffEntry(file='non-interested', status='A'),
380        ]
381        ret = rh.hooks.check_aosp_license(
382            self.project, 'commit', 'desc', diff, options=self.options)
383        self.assertIsNone(ret)
384        self.assertFalse(mock_check.called)
385
386        # Second call will have some results.
387        diff = [
388            rh.git.RawDiffEntry(file='a.bp', status='A'),
389        ]
390        ret = rh.hooks.check_aosp_license(
391            self.project, 'commit', 'desc', diff, options=self.options)
392        self.assertIsNotNone(ret)
393
394        # No result since all paths are excluded.
395        diff = [
396            rh.git.RawDiffEntry(file='a/a.bp', status='A'),
397            rh.git.RawDiffEntry(file='b/a.bp', status='A'),
398            rh.git.RawDiffEntry(file='c/d/a.bp', status='A'),
399        ]
400        ret = rh.hooks.check_aosp_license(
401            self.project, 'commit', 'desc', diff,
402            options=rh.hooks.HookOptions('hook name',
403                ['--exclude-dirs=a,b', '--exclude-dirs=c/d'], {})
404        )
405        self.assertIsNone(ret)
406
407        # Make sure that `--exclude-dir` doesn't match the path in the middle.
408        diff = [
409            rh.git.RawDiffEntry(file='a/b/c.bp', status='A'),
410        ]
411        ret = rh.hooks.check_aosp_license(
412            self.project, 'commit', 'desc', diff,
413            options=rh.hooks.HookOptions('hook name', ['--exclude-dirs=b'], {})
414        )
415        self.assertIsNotNone(ret)
416
417
418    def test_bpfmt(self, mock_check, _mock_run):
419        """Verify the bpfmt builtin hook."""
420        # First call should do nothing as there are no files to check.
421        ret = rh.hooks.check_bpfmt(
422            self.project, 'commit', 'desc', (), options=self.options)
423        self.assertIsNone(ret)
424        self.assertFalse(mock_check.called)
425
426        # Second call will have some results.
427        diff = [rh.git.RawDiffEntry(file='Android.bp')]
428        ret = rh.hooks.check_bpfmt(
429            self.project, 'commit', 'desc', diff, options=self.options)
430        self.assertIsNotNone(ret)
431        for result in ret:
432            self.assertIsNotNone(result.fixup_cmd)
433
434    def test_checkpatch(self, mock_check, _mock_run):
435        """Verify the checkpatch builtin hook."""
436        ret = rh.hooks.check_checkpatch(
437            self.project, 'commit', 'desc', (), options=self.options)
438        self.assertEqual(ret, mock_check.return_value)
439
440    def test_clang_format(self, mock_check, _mock_run):
441        """Verify the clang_format builtin hook."""
442        ret = rh.hooks.check_clang_format(
443            self.project, 'commit', 'desc', (), options=self.options)
444        self.assertEqual(ret, mock_check.return_value)
445
446    def test_google_java_format(self, mock_check, _mock_run):
447        """Verify the google_java_format builtin hook."""
448        # First call should do nothing as there are no files to check.
449        ret = rh.hooks.check_google_java_format(
450            self.project, 'commit', 'desc', (), options=self.options)
451        self.assertIsNone(ret)
452        self.assertFalse(mock_check.called)
453        # Check that .java files are included by default.
454        diff = [rh.git.RawDiffEntry(file='foo.java'),
455                rh.git.RawDiffEntry(file='bar.kt'),
456                rh.git.RawDiffEntry(file='baz/blah.java')]
457        ret = rh.hooks.check_google_java_format(
458            self.project, 'commit', 'desc', diff, options=self.options)
459        self.assertListEqual(ret[0].files, ['foo.java', 'baz/blah.java'])
460        diff = [rh.git.RawDiffEntry(file='foo/f1.java'),
461                rh.git.RawDiffEntry(file='bar/f2.java'),
462                rh.git.RawDiffEntry(file='baz/f2.java')]
463        ret = rh.hooks.check_google_java_format(
464            self.project, 'commit', 'desc', diff,
465            options=rh.hooks.HookOptions('hook name',
466            ['--include-dirs=foo,baz'], {}))
467        self.assertListEqual(ret[0].files, ['foo/f1.java', 'baz/f2.java'])
468
469    def test_commit_msg_bug_field(self, _mock_check, _mock_run):
470        """Verify the commit_msg_bug_field builtin hook."""
471        # Check some good messages.
472        self._test_commit_messages(
473            rh.hooks.check_commit_msg_bug_field, True, (
474                'subj\n\nBug: 1234\n',
475                'subj\n\nBug: 1234\nChange-Id: blah\n',
476                'subj\n\nFix: 1234\n',
477            ))
478
479        # Check some bad messages.
480        self._test_commit_messages(
481            rh.hooks.check_commit_msg_bug_field, False, (
482                'subj',
483                'subj\n\nBUG=1234\n',
484                'subj\n\nBUG: 1234\n',
485                'subj\n\nBug: N/A\n',
486                'subj\n\nBug:\n',
487                'subj\n\nFIX=1234\n',
488            ))
489
490    def test_commit_msg_changeid_field(self, _mock_check, _mock_run):
491        """Verify the commit_msg_changeid_field builtin hook."""
492        # Check some good messages.
493        self._test_commit_messages(
494            rh.hooks.check_commit_msg_changeid_field, True, (
495                'subj\n\nChange-Id: I1234\n',
496            ))
497
498        # Check some bad messages.
499        self._test_commit_messages(
500            rh.hooks.check_commit_msg_changeid_field, False, (
501                'subj',
502                'subj\n\nChange-Id: 1234\n',
503                'subj\n\nChange-ID: I1234\n',
504            ))
505
506    def test_commit_msg_prebuilt_apk_fields(self, _mock_check, _mock_run):
507        """Verify the check_commit_msg_prebuilt_apk_fields builtin hook."""
508        # Commits without APKs should pass.
509        self._test_commit_messages(
510            rh.hooks.check_commit_msg_prebuilt_apk_fields,
511            True,
512            (
513                'subj\nTest: test case\nBug: bug id\n',
514            ),
515            ['foo.cpp', 'bar.py',]
516        )
517
518        # Commits with APKs and all the required messages should pass.
519        self._test_commit_messages(
520            rh.hooks.check_commit_msg_prebuilt_apk_fields,
521            True,
522            (
523                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
524                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
525                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
526                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
527                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
528                 'http://foo.bar.com/builder\n\n'
529                 'This build IS suitable for public release.\n\n'
530                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
531                ('Test App\n\nBuilt here:\nhttp://foo.bar.com/builder\n\n'
532                 'This build IS NOT suitable for public release.\n\n'
533                 'bar.apk\npackage: name=\'com.foo.bar\'\n'
534                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
535                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
536                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
537                 'targetSdkVersion:\'28\'\n\nBug: 123\nTest: test\n'
538                 'Change-Id: XXXXXXX\n'),
539                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
540                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
541                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
542                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
543                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
544                 'http://foo.bar.com/builder\n\n'
545                 'This build IS suitable for preview release but IS NOT '
546                 'suitable for public release.\n\n'
547                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
548                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
549                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
550                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
551                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
552                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
553                 'http://foo.bar.com/builder\n\n'
554                 'This build IS NOT suitable for preview or public release.\n\n'
555                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
556            ),
557            ['foo.apk', 'bar.py',]
558        )
559
560        # Commits with APKs and without all the required messages should fail.
561        self._test_commit_messages(
562            rh.hooks.check_commit_msg_prebuilt_apk_fields,
563            False,
564            (
565                'subj\nTest: test case\nBug: bug id\n',
566                # Missing 'package'.
567                ('Test App\n\nbar.apk\n'
568                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
569                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
570                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
571                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
572                 'http://foo.bar.com/builder\n\n'
573                 'This build IS suitable for public release.\n\n'
574                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
575                # Missing 'sdkVersion'.
576                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
577                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
578                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
579                 'compileSdkVersionCodename=\'9\'\n'
580                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
581                 'http://foo.bar.com/builder\n\n'
582                 'This build IS suitable for public release.\n\n'
583                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
584                # Missing 'targetSdkVersion'.
585                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
586                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
587                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
588                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
589                 'Built here:\nhttp://foo.bar.com/builder\n\n'
590                 'This build IS suitable for public release.\n\n'
591                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
592                # Missing build location.
593                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
594                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
595                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
596                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
597                 'targetSdkVersion:\'28\'\n\n'
598                 'This build IS suitable for public release.\n\n'
599                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
600                # Missing public release indication.
601                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
602                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
603                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
604                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
605                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
606                 'http://foo.bar.com/builder\n\n'
607                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
608            ),
609            ['foo.apk', 'bar.py',]
610        )
611
612    def test_commit_msg_test_field(self, _mock_check, _mock_run):
613        """Verify the commit_msg_test_field builtin hook."""
614        # Check some good messages.
615        self._test_commit_messages(
616            rh.hooks.check_commit_msg_test_field, True, (
617                'subj\n\nTest: i did done dood it\n',
618            ))
619
620        # Check some bad messages.
621        self._test_commit_messages(
622            rh.hooks.check_commit_msg_test_field, False, (
623                'subj',
624                'subj\n\nTEST=1234\n',
625                'subj\n\nTEST: I1234\n',
626            ))
627
628    def test_commit_msg_relnote_field_format(self, _mock_check, _mock_run):
629        """Verify the commit_msg_relnote_field_format builtin hook."""
630        # Check some good messages.
631        self._test_commit_messages(
632            rh.hooks.check_commit_msg_relnote_field_format,
633            True,
634            (
635                'subj',
636                'subj\n\nTest: i did done dood it\nBug: 1234',
637                'subj\n\nMore content\n\nTest: i did done dood it\nBug: 1234',
638                'subj\n\nRelnote: This is a release note\nBug: 1234',
639                'subj\n\nRelnote:This is a release note\nBug: 1234',
640                'subj\n\nRelnote: This is a release note.\nBug: 1234',
641                'subj\n\nRelnote: "This is a release note."\nBug: 1234',
642                'subj\n\nRelnote: "This is a \\"release note\\"."\n\nBug: 1234',
643                'subj\n\nRelnote: This is a release note.\nChange-Id: 1234',
644                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
645                ('subj\n\nRelnote: "This is a release note."\n\n'
646                 'Change-Id: 1234'),
647                ('subj\n\nRelnote: This is a release note.\n\n'
648                 'It has more info, but it is not part of the release note'
649                 '\nChange-Id: 1234'),
650                ('subj\n\nRelnote: "This is a release note.\n'
651                 'It contains a correct second line."'),
652                ('subj\n\nRelnote:"This is a release note.\n'
653                 'It contains a correct second line."'),
654                ('subj\n\nRelnote: "This is a release note.\n'
655                 'It contains a correct second line.\n'
656                 'And even a third line."\n'
657                 'Bug: 1234'),
658                ('subj\n\nRelnote: "This is a release note.\n'
659                 'It contains a correct second line.\n'
660                 '\\"Quotes\\" are even used on the third line."\n'
661                 'Bug: 1234'),
662                ('subj\n\nRelnote: This is release note 1.\n'
663                 'Relnote: This is release note 2.\n'
664                 'Bug: 1234'),
665                ('subj\n\nRelnote: This is release note 1.\n'
666                 'Relnote: "This is release note 2, and it\n'
667                 'contains a correctly formatted third line."\n'
668                 'Bug: 1234'),
669                ('subj\n\nRelnote: "This is release note 1 with\n'
670                 'a correctly formatted second line."\n\n'
671                 'Relnote: "This is release note 2, and it\n'
672                 'contains a correctly formatted second line."\n'
673                 'Bug: 1234'),
674                ('subj\n\nRelnote: "This is a release note with\n'
675                 'a correctly formatted second line."\n\n'
676                 'Bug: 1234'
677                 'Here is some extra "quoted" content.'),
678                ('subj\n\nRelnote: """This is a release note.\n\n'
679                 'This relnote contains an empty line.\n'
680                 'Then a non-empty line.\n\n'
681                 'And another empty line."""\n\n'
682                 'Bug: 1234'),
683                ('subj\n\nRelnote: """This is a release note.\n\n'
684                 'This relnote contains an empty line.\n'
685                 'Then an acceptable "quoted" line.\n\n'
686                 'And another empty line."""\n\n'
687                 'Bug: 1234'),
688                ('subj\n\nRelnote: """This is a release note."""\n\n'
689                 'Bug: 1234'),
690                ('subj\n\nRelnote: """This is a release note.\n'
691                 'It has a second line."""\n\n'
692                 'Bug: 1234'),
693                ('subj\n\nRelnote: """This is a release note.\n'
694                 'It has a second line, but does not end here.\n'
695                 '"""\n\n'
696                 'Bug: 1234'),
697                ('subj\n\nRelnote: """This is a release note.\n'
698                 '"It" has a second line, but does not end here.\n'
699                 '"""\n\n'
700                 'Bug: 1234'),
701                ('subj\n\nRelnote: "This is a release note.\n'
702                 'It has a second line, but does not end here.\n'
703                 '"\n\n'
704                 'Bug: 1234'),
705            ))
706
707        # Check some bad messages.
708        self._test_commit_messages(
709            rh.hooks.check_commit_msg_relnote_field_format,
710            False,
711            (
712                'subj\n\nReleaseNote: This is a release note.\n',
713                'subj\n\nRelnotes: This is a release note.\n',
714                'subj\n\nRel-note: This is a release note.\n',
715                'subj\n\nrelnoTes: This is a release note.\n',
716                'subj\n\nrel-Note: This is a release note.\n',
717                'subj\n\nRelnote: "This is a "release note"."\nBug: 1234',
718                'subj\n\nRelnote: This is a "release note".\nBug: 1234',
719                ('subj\n\nRelnote: This is a release note.\n'
720                 'It contains an incorrect second line.'),
721                ('subj\n\nRelnote: "This is a release note.\n'
722                 'It contains multiple lines.\n'
723                 'But it does not provide an ending quote.\n'),
724                ('subj\n\nRelnote: "This is a release note.\n'
725                 'It contains multiple lines but no closing quote.\n'
726                 'Test: my test "hello world"\n'),
727                ('subj\n\nRelnote: This is release note 1.\n'
728                 'Relnote: "This is release note 2, and it\n'
729                 'contains an incorrectly formatted third line.\n'
730                 'Bug: 1234'),
731                ('subj\n\nRelnote: This is release note 1 with\n'
732                 'an incorrectly formatted second line.\n\n'
733                 'Relnote: "This is release note 2, and it\n'
734                 'contains a correctly formatted second line."\n'
735                 'Bug: 1234'),
736                ('subj\n\nRelnote: "This is release note 1 with\n'
737                 'a correctly formatted second line."\n\n'
738                 'Relnote: This is release note 2, and it\n'
739                 'contains an incorrectly formatted second line.\n'
740                 'Bug: 1234'),
741                ('subj\n\nRelnote: "This is a release note.\n'
742                 'It contains a correct second line.\n'
743                 'But incorrect "quotes" on the third line."\n'
744                 'Bug: 1234'),
745                ('subj\n\nRelnote: """This is a release note.\n'
746                 'It has a second line, but no closing triple quote.\n\n'
747                 'Bug: 1234'),
748                ('subj\n\nRelnote: "This is a release note.\n'
749                 '"It" has a second line, but does not end here.\n'
750                 '"\n\n'
751                 'Bug: 1234'),
752            ))
753
754    def test_commit_msg_relnote_for_current_txt(self, _mock_check, _mock_run):
755        """Verify the commit_msg_relnote_for_current_txt builtin hook."""
756        diff_without_current_txt = ['bar/foo.txt',
757                                    'foo.cpp',
758                                    'foo.java',
759                                    'foo_current.java',
760                                    'foo_current.txt',
761                                    'baz/current.java',
762                                    'baz/foo_current.txt']
763        diff_with_current_txt = diff_without_current_txt + ['current.txt']
764        diff_with_subdir_current_txt = \
765            diff_without_current_txt + ['foo/current.txt']
766        diff_with_experimental_current_txt = \
767            diff_without_current_txt + ['public_plus_experimental_current.txt']
768        # Check some good messages.
769        self._test_commit_messages(
770            rh.hooks.check_commit_msg_relnote_for_current_txt,
771            True,
772            (
773                'subj\n\nRelnote: This is a release note\n',
774                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
775                ('subj\n\nRelnote: This is release note 1 with\n'
776                 'an incorrectly formatted second line.\n\n'
777                 'Relnote: "This is release note 2, and it\n'
778                 'contains a correctly formatted second line."\n'
779                 'Bug: 1234'),
780            ),
781            files=diff_with_current_txt,
782        )
783        # Check some good messages.
784        self._test_commit_messages(
785            rh.hooks.check_commit_msg_relnote_for_current_txt,
786            True,
787            (
788                'subj\n\nRelnote: This is a release note\n',
789                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
790                ('subj\n\nRelnote: This is release note 1 with\n'
791                 'an incorrectly formatted second line.\n\n'
792                 'Relnote: "This is release note 2, and it\n'
793                 'contains a correctly formatted second line."\n'
794                 'Bug: 1234'),
795            ),
796            files=diff_with_experimental_current_txt,
797        )
798        # Check some good messages.
799        self._test_commit_messages(
800            rh.hooks.check_commit_msg_relnote_for_current_txt,
801            True,
802            (
803                'subj\n\nRelnote: This is a release note\n',
804                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
805                ('subj\n\nRelnote: This is release note 1 with\n'
806                 'an incorrectly formatted second line.\n\n'
807                 'Relnote: "This is release note 2, and it\n'
808                 'contains a correctly formatted second line."\n'
809                 'Bug: 1234'),
810            ),
811            files=diff_with_subdir_current_txt,
812        )
813        # Check some good messages.
814        self._test_commit_messages(
815            rh.hooks.check_commit_msg_relnote_for_current_txt,
816            True,
817            (
818                'subj',
819                'subj\nBug: 12345\nChange-Id: 1234',
820                'subj\n\nRelnote: This is a release note\n',
821                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
822                ('subj\n\nRelnote: This is release note 1 with\n'
823                 'an incorrectly formatted second line.\n\n'
824                 'Relnote: "This is release note 2, and it\n'
825                 'contains a correctly formatted second line."\n'
826                 'Bug: 1234'),
827            ),
828            files=diff_without_current_txt,
829        )
830        # Check some bad messages.
831        self._test_commit_messages(
832            rh.hooks.check_commit_msg_relnote_for_current_txt,
833            False,
834            (
835                'subj'
836                'subj\nBug: 12345\nChange-Id: 1234',
837            ),
838            files=diff_with_current_txt,
839        )
840        # Check some bad messages.
841        self._test_commit_messages(
842            rh.hooks.check_commit_msg_relnote_for_current_txt,
843            False,
844            (
845                'subj'
846                'subj\nBug: 12345\nChange-Id: 1234',
847            ),
848            files=diff_with_experimental_current_txt,
849        )
850        # Check some bad messages.
851        self._test_commit_messages(
852            rh.hooks.check_commit_msg_relnote_for_current_txt,
853            False,
854            (
855                'subj'
856                'subj\nBug: 12345\nChange-Id: 1234',
857            ),
858            files=diff_with_subdir_current_txt,
859        )
860
861    def test_cpplint(self, mock_check, _mock_run):
862        """Verify the cpplint builtin hook."""
863        self._test_file_filter(mock_check, rh.hooks.check_cpplint,
864                               ('foo.cpp', 'foo.cxx'))
865
866    def test_gofmt(self, mock_check, _mock_run):
867        """Verify the gofmt builtin hook."""
868        # First call should do nothing as there are no files to check.
869        ret = rh.hooks.check_gofmt(
870            self.project, 'commit', 'desc', (), options=self.options)
871        self.assertIsNone(ret)
872        self.assertFalse(mock_check.called)
873
874        # Second call will have some results.
875        diff = [rh.git.RawDiffEntry(file='foo.go')]
876        ret = rh.hooks.check_gofmt(
877            self.project, 'commit', 'desc', diff, options=self.options)
878        self.assertIsNotNone(ret)
879
880    def test_jsonlint(self, mock_check, _mock_run):
881        """Verify the jsonlint builtin hook."""
882        # First call should do nothing as there are no files to check.
883        ret = rh.hooks.check_json(
884            self.project, 'commit', 'desc', (), options=self.options)
885        self.assertIsNone(ret)
886        self.assertFalse(mock_check.called)
887
888        # TODO: Actually pass some valid/invalid json data down.
889
890    def test_ktfmt(self, mock_check, _mock_run):
891        """Verify the ktfmt builtin hook."""
892        # First call should do nothing as there are no files to check.
893        ret = rh.hooks.check_ktfmt(
894            self.project, 'commit', 'desc', (), options=self.options)
895        self.assertIsNone(ret)
896        self.assertFalse(mock_check.called)
897        # Check that .kt files are included by default.
898        diff = [rh.git.RawDiffEntry(file='foo.kt'),
899                rh.git.RawDiffEntry(file='bar.java'),
900                rh.git.RawDiffEntry(file='baz/blah.kt')]
901        ret = rh.hooks.check_ktfmt(
902            self.project, 'commit', 'desc', diff, options=self.options)
903        self.assertListEqual(ret[0].files, ['foo.kt', 'baz/blah.kt'])
904        diff = [rh.git.RawDiffEntry(file='foo/f1.kt'),
905                rh.git.RawDiffEntry(file='bar/f2.kt'),
906                rh.git.RawDiffEntry(file='baz/f2.kt')]
907        ret = rh.hooks.check_ktfmt(self.project, 'commit', 'desc', diff,
908                                   options=rh.hooks.HookOptions('hook name', [
909                                       '--include-dirs=foo,baz'], {}))
910        self.assertListEqual(ret[0].files, ['foo/f1.kt', 'baz/f2.kt'])
911
912    def test_pylint(self, mock_check, _mock_run):
913        """Verify the pylint builtin hook."""
914        self._test_file_filter(mock_check, rh.hooks.check_pylint2,
915                               ('foo.py',))
916
917    def test_pylint2(self, mock_check, _mock_run):
918        """Verify the pylint2 builtin hook."""
919        self._test_file_filter(mock_check, rh.hooks.check_pylint2,
920                               ('foo.py',))
921
922    def test_pylint3(self, mock_check, _mock_run):
923        """Verify the pylint3 builtin hook."""
924        self._test_file_filter(mock_check, rh.hooks.check_pylint3,
925                               ('foo.py',))
926
927    def test_rustfmt(self, mock_check, _mock_run):
928        # First call should do nothing as there are no files to check.
929        ret = rh.hooks.check_rustfmt(
930            self.project, 'commit', 'desc', (), options=self.options)
931        self.assertEqual(ret, None)
932        self.assertFalse(mock_check.called)
933
934        # Second call will have some results.
935        diff = [rh.git.RawDiffEntry(file='lib.rs')]
936        ret = rh.hooks.check_rustfmt(
937            self.project, 'commit', 'desc', diff, options=self.options)
938        self.assertNotEqual(ret, None)
939
940    def test_xmllint(self, mock_check, _mock_run):
941        """Verify the xmllint builtin hook."""
942        self._test_file_filter(mock_check, rh.hooks.check_xmllint,
943                               ('foo.xml',))
944
945    def test_android_test_mapping_format(self, mock_check, _mock_run):
946        """Verify the android_test_mapping_format builtin hook."""
947        # First call should do nothing as there are no files to check.
948        ret = rh.hooks.check_android_test_mapping(
949            self.project, 'commit', 'desc', (), options=self.options)
950        self.assertIsNone(ret)
951        self.assertFalse(mock_check.called)
952
953        # Second call will have some results.
954        diff = [rh.git.RawDiffEntry(file='TEST_MAPPING')]
955        ret = rh.hooks.check_android_test_mapping(
956            self.project, 'commit', 'desc', diff, options=self.options)
957        self.assertIsNotNone(ret)
958
959    def test_aidl_format(self, mock_check, _mock_run):
960        """Verify the aidl_format builtin hook."""
961        # First call should do nothing as there are no files to check.
962        ret = rh.hooks.check_aidl_format(
963            self.project, 'commit', 'desc', (), options=self.options)
964        self.assertIsNone(ret)
965        self.assertFalse(mock_check.called)
966
967        # Second call will have some results.
968        diff = [rh.git.RawDiffEntry(file='IFoo.go')]
969        ret = rh.hooks.check_gofmt(
970            self.project, 'commit', 'desc', diff, options=self.options)
971        self.assertIsNotNone(ret)
972
973
974if __name__ == '__main__':
975    unittest.main()
976