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