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_bpfmt(self, mock_check, _mock_run):
374        """Verify the bpfmt builtin hook."""
375        # First call should do nothing as there are no files to check.
376        ret = rh.hooks.check_bpfmt(
377            self.project, 'commit', 'desc', (), options=self.options)
378        self.assertIsNone(ret)
379        self.assertFalse(mock_check.called)
380
381        # Second call will have some results.
382        diff = [rh.git.RawDiffEntry(file='Android.bp')]
383        ret = rh.hooks.check_bpfmt(
384            self.project, 'commit', 'desc', diff, options=self.options)
385        self.assertIsNotNone(ret)
386        for result in ret:
387            self.assertIsNotNone(result.fixup_cmd)
388
389    def test_checkpatch(self, mock_check, _mock_run):
390        """Verify the checkpatch builtin hook."""
391        ret = rh.hooks.check_checkpatch(
392            self.project, 'commit', 'desc', (), options=self.options)
393        self.assertEqual(ret, mock_check.return_value)
394
395    def test_clang_format(self, mock_check, _mock_run):
396        """Verify the clang_format builtin hook."""
397        ret = rh.hooks.check_clang_format(
398            self.project, 'commit', 'desc', (), options=self.options)
399        self.assertEqual(ret, mock_check.return_value)
400
401    def test_google_java_format(self, mock_check, _mock_run):
402        """Verify the google_java_format builtin hook."""
403        # First call should do nothing as there are no files to check.
404        ret = rh.hooks.check_google_java_format(
405            self.project, 'commit', 'desc', (), options=self.options)
406        self.assertIsNone(ret)
407        self.assertFalse(mock_check.called)
408        # Check that .java files are included by default.
409        diff = [rh.git.RawDiffEntry(file='foo.java'),
410                rh.git.RawDiffEntry(file='bar.kt'),
411                rh.git.RawDiffEntry(file='baz/blah.java')]
412        ret = rh.hooks.check_google_java_format(
413            self.project, 'commit', 'desc', diff, options=self.options)
414        self.assertListEqual(ret[0].files, ['foo.java', 'baz/blah.java'])
415        diff = [rh.git.RawDiffEntry(file='foo/f1.java'),
416                rh.git.RawDiffEntry(file='bar/f2.java'),
417                rh.git.RawDiffEntry(file='baz/f2.java')]
418        ret = rh.hooks.check_google_java_format(
419            self.project, 'commit', 'desc', diff,
420            options=rh.hooks.HookOptions('hook name',
421            ['--include-dirs=foo,baz'], {}))
422        self.assertListEqual(ret[0].files, ['foo/f1.java', 'baz/f2.java'])
423
424    def test_commit_msg_bug_field(self, _mock_check, _mock_run):
425        """Verify the commit_msg_bug_field builtin hook."""
426        # Check some good messages.
427        self._test_commit_messages(
428            rh.hooks.check_commit_msg_bug_field, True, (
429                'subj\n\nBug: 1234\n',
430                'subj\n\nBug: 1234\nChange-Id: blah\n',
431            ))
432
433        # Check some bad messages.
434        self._test_commit_messages(
435            rh.hooks.check_commit_msg_bug_field, False, (
436                'subj',
437                'subj\n\nBUG=1234\n',
438                'subj\n\nBUG: 1234\n',
439                'subj\n\nBug: N/A\n',
440                'subj\n\nBug:\n',
441            ))
442
443    def test_commit_msg_changeid_field(self, _mock_check, _mock_run):
444        """Verify the commit_msg_changeid_field builtin hook."""
445        # Check some good messages.
446        self._test_commit_messages(
447            rh.hooks.check_commit_msg_changeid_field, True, (
448                'subj\n\nChange-Id: I1234\n',
449            ))
450
451        # Check some bad messages.
452        self._test_commit_messages(
453            rh.hooks.check_commit_msg_changeid_field, False, (
454                'subj',
455                'subj\n\nChange-Id: 1234\n',
456                'subj\n\nChange-ID: I1234\n',
457            ))
458
459    def test_commit_msg_prebuilt_apk_fields(self, _mock_check, _mock_run):
460        """Verify the check_commit_msg_prebuilt_apk_fields builtin hook."""
461        # Commits without APKs should pass.
462        self._test_commit_messages(
463            rh.hooks.check_commit_msg_prebuilt_apk_fields,
464            True,
465            (
466                'subj\nTest: test case\nBug: bug id\n',
467            ),
468            ['foo.cpp', 'bar.py',]
469        )
470
471        # Commits with APKs and all the required messages should pass.
472        self._test_commit_messages(
473            rh.hooks.check_commit_msg_prebuilt_apk_fields,
474            True,
475            (
476                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
477                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
478                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
479                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
480                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
481                 'http://foo.bar.com/builder\n\n'
482                 'This build IS suitable for public release.\n\n'
483                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
484                ('Test App\n\nBuilt here:\nhttp://foo.bar.com/builder\n\n'
485                 'This build IS NOT suitable for public release.\n\n'
486                 'bar.apk\npackage: name=\'com.foo.bar\'\n'
487                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
488                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
489                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
490                 'targetSdkVersion:\'28\'\n\nBug: 123\nTest: test\n'
491                 'Change-Id: XXXXXXX\n'),
492                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
493                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
494                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
495                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
496                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
497                 'http://foo.bar.com/builder\n\n'
498                 'This build IS suitable for preview release but IS NOT '
499                 'suitable for public release.\n\n'
500                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
501                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
502                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
503                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
504                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
505                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
506                 'http://foo.bar.com/builder\n\n'
507                 'This build IS NOT suitable for preview or public release.\n\n'
508                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
509            ),
510            ['foo.apk', 'bar.py',]
511        )
512
513        # Commits with APKs and without all the required messages should fail.
514        self._test_commit_messages(
515            rh.hooks.check_commit_msg_prebuilt_apk_fields,
516            False,
517            (
518                'subj\nTest: test case\nBug: bug id\n',
519                # Missing 'package'.
520                ('Test App\n\nbar.apk\n'
521                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
522                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
523                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
524                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
525                 'http://foo.bar.com/builder\n\n'
526                 'This build IS suitable for public release.\n\n'
527                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
528                # Missing 'sdkVersion'.
529                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
530                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
531                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
532                 'compileSdkVersionCodename=\'9\'\n'
533                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
534                 'http://foo.bar.com/builder\n\n'
535                 'This build IS suitable for public release.\n\n'
536                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
537                # Missing 'targetSdkVersion'.
538                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
539                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
540                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
541                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
542                 'Built here:\nhttp://foo.bar.com/builder\n\n'
543                 'This build IS suitable for public release.\n\n'
544                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
545                # Missing build location.
546                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
547                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
548                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
549                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
550                 'targetSdkVersion:\'28\'\n\n'
551                 'This build IS suitable for public release.\n\n'
552                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
553                # Missing public release indication.
554                ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n'
555                 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n'
556                 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n'
557                 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n'
558                 'targetSdkVersion:\'28\'\n\nBuilt here:\n'
559                 'http://foo.bar.com/builder\n\n'
560                 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'),
561            ),
562            ['foo.apk', 'bar.py',]
563        )
564
565    def test_commit_msg_test_field(self, _mock_check, _mock_run):
566        """Verify the commit_msg_test_field builtin hook."""
567        # Check some good messages.
568        self._test_commit_messages(
569            rh.hooks.check_commit_msg_test_field, True, (
570                'subj\n\nTest: i did done dood it\n',
571            ))
572
573        # Check some bad messages.
574        self._test_commit_messages(
575            rh.hooks.check_commit_msg_test_field, False, (
576                'subj',
577                'subj\n\nTEST=1234\n',
578                'subj\n\nTEST: I1234\n',
579            ))
580
581    def test_commit_msg_relnote_field_format(self, _mock_check, _mock_run):
582        """Verify the commit_msg_relnote_field_format builtin hook."""
583        # Check some good messages.
584        self._test_commit_messages(
585            rh.hooks.check_commit_msg_relnote_field_format,
586            True,
587            (
588                'subj',
589                'subj\n\nTest: i did done dood it\nBug: 1234',
590                'subj\n\nMore content\n\nTest: i did done dood it\nBug: 1234',
591                'subj\n\nRelnote: This is a release note\nBug: 1234',
592                'subj\n\nRelnote:This is a release note\nBug: 1234',
593                'subj\n\nRelnote: This is a release note.\nBug: 1234',
594                'subj\n\nRelnote: "This is a release note."\nBug: 1234',
595                'subj\n\nRelnote: "This is a \\"release note\\"."\n\nBug: 1234',
596                'subj\n\nRelnote: This is a release note.\nChange-Id: 1234',
597                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
598                ('subj\n\nRelnote: "This is a release note."\n\n'
599                 'Change-Id: 1234'),
600                ('subj\n\nRelnote: This is a release note.\n\n'
601                 'It has more info, but it is not part of the release note'
602                 '\nChange-Id: 1234'),
603                ('subj\n\nRelnote: "This is a release note.\n'
604                 'It contains a correct second line."'),
605                ('subj\n\nRelnote:"This is a release note.\n'
606                 'It contains a correct second line."'),
607                ('subj\n\nRelnote: "This is a release note.\n'
608                 'It contains a correct second line.\n'
609                 'And even a third line."\n'
610                 'Bug: 1234'),
611                ('subj\n\nRelnote: "This is a release note.\n'
612                 'It contains a correct second line.\n'
613                 '\\"Quotes\\" are even used on the third line."\n'
614                 'Bug: 1234'),
615                ('subj\n\nRelnote: This is release note 1.\n'
616                 'Relnote: This is release note 2.\n'
617                 'Bug: 1234'),
618                ('subj\n\nRelnote: This is release note 1.\n'
619                 'Relnote: "This is release note 2, and it\n'
620                 'contains a correctly formatted third line."\n'
621                 'Bug: 1234'),
622                ('subj\n\nRelnote: "This is release note 1 with\n'
623                 'a correctly formatted second line."\n\n'
624                 'Relnote: "This is release note 2, and it\n'
625                 'contains a correctly formatted second line."\n'
626                 'Bug: 1234'),
627                ('subj\n\nRelnote: "This is a release note with\n'
628                 'a correctly formatted second line."\n\n'
629                 'Bug: 1234'
630                 'Here is some extra "quoted" content.'),
631                ('subj\n\nRelnote: """This is a release note.\n\n'
632                 'This relnote contains an empty line.\n'
633                 'Then a non-empty line.\n\n'
634                 'And another empty line."""\n\n'
635                 'Bug: 1234'),
636                ('subj\n\nRelnote: """This is a release note.\n\n'
637                 'This relnote contains an empty line.\n'
638                 'Then an acceptable "quoted" line.\n\n'
639                 'And another empty line."""\n\n'
640                 'Bug: 1234'),
641                ('subj\n\nRelnote: """This is a release note."""\n\n'
642                 'Bug: 1234'),
643                ('subj\n\nRelnote: """This is a release note.\n'
644                 'It has a second line."""\n\n'
645                 'Bug: 1234'),
646                ('subj\n\nRelnote: """This is a release note.\n'
647                 'It has a second line, but does not end here.\n'
648                 '"""\n\n'
649                 'Bug: 1234'),
650                ('subj\n\nRelnote: """This is a release note.\n'
651                 '"It" has a second line, but does not end here.\n'
652                 '"""\n\n'
653                 'Bug: 1234'),
654                ('subj\n\nRelnote: "This is a release note.\n'
655                 'It has a second line, but does not end here.\n'
656                 '"\n\n'
657                 'Bug: 1234'),
658            ))
659
660        # Check some bad messages.
661        self._test_commit_messages(
662            rh.hooks.check_commit_msg_relnote_field_format,
663            False,
664            (
665                'subj\n\nReleaseNote: This is a release note.\n',
666                'subj\n\nRelnotes: This is a release note.\n',
667                'subj\n\nRel-note: This is a release note.\n',
668                'subj\n\nrelnoTes: This is a release note.\n',
669                'subj\n\nrel-Note: This is a release note.\n',
670                'subj\n\nRelnote: "This is a "release note"."\nBug: 1234',
671                'subj\n\nRelnote: This is a "release note".\nBug: 1234',
672                ('subj\n\nRelnote: This is a release note.\n'
673                 'It contains an incorrect second line.'),
674                ('subj\n\nRelnote: "This is a release note.\n'
675                 'It contains multiple lines.\n'
676                 'But it does not provide an ending quote.\n'),
677                ('subj\n\nRelnote: "This is a release note.\n'
678                 'It contains multiple lines but no closing quote.\n'
679                 'Test: my test "hello world"\n'),
680                ('subj\n\nRelnote: This is release note 1.\n'
681                 'Relnote: "This is release note 2, and it\n'
682                 'contains an incorrectly formatted third line.\n'
683                 'Bug: 1234'),
684                ('subj\n\nRelnote: This is release note 1 with\n'
685                 'an incorrectly formatted second line.\n\n'
686                 'Relnote: "This is release note 2, and it\n'
687                 'contains a correctly formatted second line."\n'
688                 'Bug: 1234'),
689                ('subj\n\nRelnote: "This is release note 1 with\n'
690                 'a correctly formatted second line."\n\n'
691                 'Relnote: This is release note 2, and it\n'
692                 'contains an incorrectly formatted second line.\n'
693                 'Bug: 1234'),
694                ('subj\n\nRelnote: "This is a release note.\n'
695                 'It contains a correct second line.\n'
696                 'But incorrect "quotes" on the third line."\n'
697                 'Bug: 1234'),
698                ('subj\n\nRelnote: """This is a release note.\n'
699                 'It has a second line, but no closing triple quote.\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    def test_commit_msg_relnote_for_current_txt(self, _mock_check, _mock_run):
708        """Verify the commit_msg_relnote_for_current_txt builtin hook."""
709        diff_without_current_txt = ['bar/foo.txt',
710                                    'foo.cpp',
711                                    'foo.java',
712                                    'foo_current.java',
713                                    'foo_current.txt',
714                                    'baz/current.java',
715                                    'baz/foo_current.txt']
716        diff_with_current_txt = diff_without_current_txt + ['current.txt']
717        diff_with_subdir_current_txt = \
718            diff_without_current_txt + ['foo/current.txt']
719        diff_with_experimental_current_txt = \
720            diff_without_current_txt + ['public_plus_experimental_current.txt']
721        # Check some good messages.
722        self._test_commit_messages(
723            rh.hooks.check_commit_msg_relnote_for_current_txt,
724            True,
725            (
726                'subj\n\nRelnote: This is a release note\n',
727                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
728                ('subj\n\nRelnote: This is release note 1 with\n'
729                 'an incorrectly formatted second line.\n\n'
730                 'Relnote: "This is release note 2, and it\n'
731                 'contains a correctly formatted second line."\n'
732                 'Bug: 1234'),
733            ),
734            files=diff_with_current_txt,
735        )
736        # Check some good messages.
737        self._test_commit_messages(
738            rh.hooks.check_commit_msg_relnote_for_current_txt,
739            True,
740            (
741                'subj\n\nRelnote: This is a release note\n',
742                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
743                ('subj\n\nRelnote: This is release note 1 with\n'
744                 'an incorrectly formatted second line.\n\n'
745                 'Relnote: "This is release note 2, and it\n'
746                 'contains a correctly formatted second line."\n'
747                 'Bug: 1234'),
748            ),
749            files=diff_with_experimental_current_txt,
750        )
751        # Check some good messages.
752        self._test_commit_messages(
753            rh.hooks.check_commit_msg_relnote_for_current_txt,
754            True,
755            (
756                'subj\n\nRelnote: This is a release note\n',
757                'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234',
758                ('subj\n\nRelnote: This is release note 1 with\n'
759                 'an incorrectly formatted second line.\n\n'
760                 'Relnote: "This is release note 2, and it\n'
761                 'contains a correctly formatted second line."\n'
762                 'Bug: 1234'),
763            ),
764            files=diff_with_subdir_current_txt,
765        )
766        # Check some good messages.
767        self._test_commit_messages(
768            rh.hooks.check_commit_msg_relnote_for_current_txt,
769            True,
770            (
771                'subj',
772                'subj\nBug: 12345\nChange-Id: 1234',
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_without_current_txt,
782        )
783        # Check some bad messages.
784        self._test_commit_messages(
785            rh.hooks.check_commit_msg_relnote_for_current_txt,
786            False,
787            (
788                'subj'
789                'subj\nBug: 12345\nChange-Id: 1234',
790            ),
791            files=diff_with_current_txt,
792        )
793        # Check some bad messages.
794        self._test_commit_messages(
795            rh.hooks.check_commit_msg_relnote_for_current_txt,
796            False,
797            (
798                'subj'
799                'subj\nBug: 12345\nChange-Id: 1234',
800            ),
801            files=diff_with_experimental_current_txt,
802        )
803        # Check some bad messages.
804        self._test_commit_messages(
805            rh.hooks.check_commit_msg_relnote_for_current_txt,
806            False,
807            (
808                'subj'
809                'subj\nBug: 12345\nChange-Id: 1234',
810            ),
811            files=diff_with_subdir_current_txt,
812        )
813
814    def test_cpplint(self, mock_check, _mock_run):
815        """Verify the cpplint builtin hook."""
816        self._test_file_filter(mock_check, rh.hooks.check_cpplint,
817                               ('foo.cpp', 'foo.cxx'))
818
819    def test_gofmt(self, mock_check, _mock_run):
820        """Verify the gofmt builtin hook."""
821        # First call should do nothing as there are no files to check.
822        ret = rh.hooks.check_gofmt(
823            self.project, 'commit', 'desc', (), options=self.options)
824        self.assertIsNone(ret)
825        self.assertFalse(mock_check.called)
826
827        # Second call will have some results.
828        diff = [rh.git.RawDiffEntry(file='foo.go')]
829        ret = rh.hooks.check_gofmt(
830            self.project, 'commit', 'desc', diff, options=self.options)
831        self.assertIsNotNone(ret)
832
833    def test_jsonlint(self, mock_check, _mock_run):
834        """Verify the jsonlint builtin hook."""
835        # First call should do nothing as there are no files to check.
836        ret = rh.hooks.check_json(
837            self.project, 'commit', 'desc', (), options=self.options)
838        self.assertIsNone(ret)
839        self.assertFalse(mock_check.called)
840
841        # TODO: Actually pass some valid/invalid json data down.
842
843    def test_ktfmt(self, mock_check, _mock_run):
844        """Verify the ktfmt builtin hook."""
845        # First call should do nothing as there are no files to check.
846        ret = rh.hooks.check_ktfmt(
847            self.project, 'commit', 'desc', (), options=self.options)
848        self.assertIsNone(ret)
849        self.assertFalse(mock_check.called)
850        # Check that .kt files are included by default.
851        diff = [rh.git.RawDiffEntry(file='foo.kt'),
852                rh.git.RawDiffEntry(file='bar.java'),
853                rh.git.RawDiffEntry(file='baz/blah.kt')]
854        ret = rh.hooks.check_ktfmt(
855            self.project, 'commit', 'desc', diff, options=self.options)
856        self.assertListEqual(ret[0].files, ['foo.kt', 'baz/blah.kt'])
857        diff = [rh.git.RawDiffEntry(file='foo/f1.kt'),
858                rh.git.RawDiffEntry(file='bar/f2.kt'),
859                rh.git.RawDiffEntry(file='baz/f2.kt')]
860        ret = rh.hooks.check_ktfmt(self.project, 'commit', 'desc', diff,
861                                   options=rh.hooks.HookOptions('hook name', [
862                                       '--include-dirs=foo,baz'], {}))
863        self.assertListEqual(ret[0].files, ['foo/f1.kt', 'baz/f2.kt'])
864
865    def test_pylint(self, mock_check, _mock_run):
866        """Verify the pylint builtin hook."""
867        self._test_file_filter(mock_check, rh.hooks.check_pylint2,
868                               ('foo.py',))
869
870    def test_pylint2(self, mock_check, _mock_run):
871        """Verify the pylint2 builtin hook."""
872        self._test_file_filter(mock_check, rh.hooks.check_pylint2,
873                               ('foo.py',))
874
875    def test_pylint3(self, mock_check, _mock_run):
876        """Verify the pylint3 builtin hook."""
877        self._test_file_filter(mock_check, rh.hooks.check_pylint3,
878                               ('foo.py',))
879
880    def test_rustfmt(self, mock_check, _mock_run):
881        # First call should do nothing as there are no files to check.
882        ret = rh.hooks.check_rustfmt(
883            self.project, 'commit', 'desc', (), options=self.options)
884        self.assertEqual(ret, None)
885        self.assertFalse(mock_check.called)
886
887        # Second call will have some results.
888        diff = [rh.git.RawDiffEntry(file='lib.rs')]
889        ret = rh.hooks.check_rustfmt(
890            self.project, 'commit', 'desc', diff, options=self.options)
891        self.assertNotEqual(ret, None)
892
893    def test_xmllint(self, mock_check, _mock_run):
894        """Verify the xmllint builtin hook."""
895        self._test_file_filter(mock_check, rh.hooks.check_xmllint,
896                               ('foo.xml',))
897
898    def test_android_test_mapping_format(self, mock_check, _mock_run):
899        """Verify the android_test_mapping_format builtin hook."""
900        # First call should do nothing as there are no files to check.
901        ret = rh.hooks.check_android_test_mapping(
902            self.project, 'commit', 'desc', (), options=self.options)
903        self.assertIsNone(ret)
904        self.assertFalse(mock_check.called)
905
906        # Second call will have some results.
907        diff = [rh.git.RawDiffEntry(file='TEST_MAPPING')]
908        ret = rh.hooks.check_android_test_mapping(
909            self.project, 'commit', 'desc', diff, options=self.options)
910        self.assertIsNotNone(ret)
911
912    def test_aidl_format(self, mock_check, _mock_run):
913        """Verify the aidl_format builtin hook."""
914        # First call should do nothing as there are no files to check.
915        ret = rh.hooks.check_aidl_format(
916            self.project, 'commit', 'desc', (), options=self.options)
917        self.assertIsNone(ret)
918        self.assertFalse(mock_check.called)
919
920        # Second call will have some results.
921        diff = [rh.git.RawDiffEntry(file='IFoo.go')]
922        ret = rh.hooks.check_gofmt(
923            self.project, 'commit', 'desc', diff, options=self.options)
924        self.assertIsNotNone(ret)
925
926
927if __name__ == '__main__':
928    unittest.main()
929