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