Home | History | Annotate | Download | only in rh
      1 #!/usr/bin/python
      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 
     19 from __future__ import print_function
     20 
     21 import mock
     22 import os
     23 import sys
     24 import unittest
     25 
     26 _path = os.path.realpath(__file__ + '/../..')
     27 if sys.path[0] != _path:
     28     sys.path.insert(0, _path)
     29 del _path
     30 
     31 import rh
     32 import rh.hooks
     33 import rh.config
     34 
     35 
     36 class 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         for line in open(self.readme):
     52             if not in_section:
     53                 # Look for the section like "## [Tool Paths]".
     54                 if line.startswith('#') and line.lstrip('#').strip() == section:
     55                     in_section = True
     56             else:
     57                 # Once we hit the next section (higher or lower), break.
     58                 if line[0] == '#':
     59                     break
     60                 ret.append(line)
     61         return ''.join(ret)
     62 
     63     def testBuiltinHooks(self):
     64         """Verify builtin hooks are documented."""
     65         data = self._grab_section('[Builtin Hooks]')
     66         for hook in rh.hooks.BUILTIN_HOOKS:
     67             self.assertIn('* `%s`:' % (hook,), data,
     68                           msg='README.md missing docs for hook "%s"' % (hook,))
     69 
     70     def testToolPaths(self):
     71         """Verify tools are documented."""
     72         data = self._grab_section('[Tool Paths]')
     73         for tool in rh.hooks.TOOL_PATHS:
     74             self.assertIn('* `%s`:' % (tool,), data,
     75                           msg='README.md missing docs for tool "%s"' % (tool,))
     76 
     77     def testPlaceholders(self):
     78         """Verify placeholder replacement vars are documented."""
     79         data = self._grab_section('Placeholders')
     80         for var in rh.hooks.Placeholders.vars():
     81             self.assertIn('* `${%s}`:' % (var,), data,
     82                           msg='README.md missing docs for var "%s"' % (var,))
     83 
     84 
     85 class PlaceholderTests(unittest.TestCase):
     86     """Verify behavior of replacement variables."""
     87 
     88     def setUp(self):
     89         self._saved_environ = os.environ.copy()
     90         os.environ.update({
     91             'PREUPLOAD_COMMIT_MESSAGE': 'commit message',
     92             'PREUPLOAD_COMMIT': '5c4c293174bb61f0f39035a71acd9084abfa743d',
     93         })
     94         self.replacer = rh.hooks.Placeholders()
     95 
     96     def tearDown(self):
     97         os.environ.clear()
     98         os.environ.update(self._saved_environ)
     99 
    100     def testVars(self):
    101         """Light test for the vars inspection generator."""
    102         ret = list(self.replacer.vars())
    103         self.assertGreater(len(ret), 4)
    104         self.assertIn('PREUPLOAD_COMMIT', ret)
    105 
    106     @mock.patch.object(rh.git, 'find_repo_root', return_value='/ ${BUILD_OS}')
    107     def testExpandVars(self, _m):
    108         """Verify the replacement actually works."""
    109         input_args = [
    110             # Verify ${REPO_ROOT} is updated, but not REPO_ROOT.
    111             # We also make sure that things in ${REPO_ROOT} are not double
    112             # expanded (which is why the return includes ${BUILD_OS}).
    113             '${REPO_ROOT}/some/prog/REPO_ROOT/ok',
    114             # Verify lists are merged rather than inserted.  In this case, the
    115             # list is empty, but we'd hit an error still if we saw [] in args.
    116             '${PREUPLOAD_FILES}',
    117             # Verify values with whitespace don't expand into multiple args.
    118             '${PREUPLOAD_COMMIT_MESSAGE}',
    119             # Verify multiple values get replaced.
    120             '${PREUPLOAD_COMMIT}^${PREUPLOAD_COMMIT_MESSAGE}',
    121             # Unknown vars should be left alone.
    122             '${THIS_VAR_IS_GOOD}',
    123         ]
    124         output_args = self.replacer.expand_vars(input_args)
    125         exp_args = [
    126             '/ ${BUILD_OS}/some/prog/REPO_ROOT/ok',
    127             'commit message',
    128             '5c4c293174bb61f0f39035a71acd9084abfa743d^commit message',
    129             '${THIS_VAR_IS_GOOD}',
    130         ]
    131         self.assertEqual(output_args, exp_args)
    132 
    133     def testTheTester(self):
    134         """Make sure we have a test for every variable."""
    135         for var in self.replacer.vars():
    136             self.assertIn('test%s' % (var,), dir(self),
    137                           msg='Missing unittest for variable %s' % (var,))
    138 
    139     def testPREUPLOAD_COMMIT_MESSAGE(self):
    140         """Verify handling of PREUPLOAD_COMMIT_MESSAGE."""
    141         self.assertEqual(self.replacer.get('PREUPLOAD_COMMIT_MESSAGE'),
    142                          'commit message')
    143 
    144     def testPREUPLOAD_COMMIT(self):
    145         """Verify handling of PREUPLOAD_COMMIT."""
    146         self.assertEqual(self.replacer.get('PREUPLOAD_COMMIT'),
    147                          '5c4c293174bb61f0f39035a71acd9084abfa743d')
    148 
    149     def testPREUPLOAD_FILES(self):
    150         """Verify handling of PREUPLOAD_FILES."""
    151         self.assertEqual(self.replacer.get('PREUPLOAD_FILES'), [])
    152 
    153     @mock.patch.object(rh.git, 'find_repo_root', return_value='/repo!')
    154     def testREPO_ROOT(self, m):
    155         """Verify handling of REPO_ROOT."""
    156         self.assertEqual(self.replacer.get('REPO_ROOT'), m.return_value)
    157 
    158     @mock.patch.object(rh.hooks, '_get_build_os_name', return_value='vapier os')
    159     def testBUILD_OS(self, m):
    160         """Verify handling of BUILD_OS."""
    161         self.assertEqual(self.replacer.get('BUILD_OS'), m.return_value)
    162 
    163 
    164 class HookOptionsTests(unittest.TestCase):
    165     """Verify behavior of HookOptions object."""
    166 
    167     @mock.patch.object(rh.hooks, '_get_build_os_name', return_value='vapier os')
    168     def testExpandVars(self, m):
    169         """Verify expand_vars behavior."""
    170         # Simple pass through.
    171         args = ['who', 'goes', 'there ?']
    172         self.assertEqual(args, rh.hooks.HookOptions.expand_vars(args))
    173 
    174         # At least one replacement.  Most real testing is in PlaceholderTests.
    175         args = ['who', 'goes', 'there ?', '${BUILD_OS} is great']
    176         exp_args = ['who', 'goes', 'there ?', '%s is great' % (m.return_value,)]
    177         self.assertEqual(exp_args, rh.hooks.HookOptions.expand_vars(args))
    178 
    179     def testArgs(self):
    180         """Verify args behavior."""
    181         # Verify initial args to __init__ has higher precedent.
    182         args = ['start', 'args']
    183         options = rh.hooks.HookOptions('hook name', args, {})
    184         self.assertEqual(options.args(), args)
    185         self.assertEqual(options.args(default_args=['moo']), args)
    186 
    187         # Verify we fall back to default_args.
    188         args = ['default', 'args']
    189         options = rh.hooks.HookOptions('hook name', [], {})
    190         self.assertEqual(options.args(), [])
    191         self.assertEqual(options.args(default_args=args), args)
    192 
    193     def testToolPath(self):
    194         """Verify tool_path behavior."""
    195         options = rh.hooks.HookOptions('hook name', [], {
    196             'cpplint': 'my cpplint',
    197         })
    198         # Check a builtin (and not overridden) tool.
    199         self.assertEqual(options.tool_path('pylint'), 'pylint')
    200         # Check an overridden tool.
    201         self.assertEqual(options.tool_path('cpplint'), 'my cpplint')
    202         # Check an unknown tool fails.
    203         self.assertRaises(AssertionError, options.tool_path, 'extra_tool')
    204 
    205 
    206 class UtilsTests(unittest.TestCase):
    207     """Verify misc utility functions."""
    208 
    209     def testRunCommand(self):
    210         """Check _run_command behavior."""
    211         # Most testing is done against the utils.RunCommand already.
    212         # pylint: disable=protected-access
    213         ret = rh.hooks._run_command(['true'])
    214         self.assertEqual(ret.returncode, 0)
    215 
    216     def testBuildOs(self):
    217         """Check _get_build_os_name behavior."""
    218         # Just verify it returns something and doesn't crash.
    219         # pylint: disable=protected-access
    220         ret = rh.hooks._get_build_os_name()
    221         self.assertTrue(isinstance(ret, str))
    222         self.assertNotEqual(ret, '')
    223 
    224     def testGetHelperPath(self):
    225         """Check get_helper_path behavior."""
    226         # Just verify it doesn't crash.  It's a dirt simple func.
    227         ret = rh.hooks.get_helper_path('booga')
    228         self.assertTrue(isinstance(ret, str))
    229         self.assertNotEqual(ret, '')
    230 
    231 
    232 
    233 @mock.patch.object(rh.utils, 'run_command')
    234 @mock.patch.object(rh.hooks, '_check_cmd', return_value=['check_cmd'])
    235 class BuiltinHooksTests(unittest.TestCase):
    236     """Verify the builtin hooks."""
    237 
    238     def setUp(self):
    239         self.project = rh.Project(name='project-name', dir='/.../repo/dir',
    240                                   remote='remote')
    241         self.options = rh.hooks.HookOptions('hook name', [], {})
    242 
    243     def _test_commit_messages(self, func, accept, msgs):
    244         """Helper for testing commit message hooks.
    245 
    246         Args:
    247           func: The hook function to test.
    248           accept: Whether all the |msgs| should be accepted.
    249           msgs: List of messages to test.
    250         """
    251         for desc in msgs:
    252             ret = func(self.project, 'commit', desc, (), options=self.options)
    253             if accept:
    254                 self.assertEqual(
    255                     ret, None, msg='Should have accepted: {{{%s}}}' % (desc,))
    256             else:
    257                 self.assertNotEqual(
    258                     ret, None, msg='Should have rejected: {{{%s}}}' % (desc,))
    259 
    260     def _test_file_filter(self, mock_check, func, files):
    261         """Helper for testing hooks that filter by files and run external tools.
    262 
    263         Args:
    264           mock_check: The mock of _check_cmd.
    265           func: The hook function to test.
    266           files: A list of files that we'd check.
    267         """
    268         # First call should do nothing as there are no files to check.
    269         ret = func(self.project, 'commit', 'desc', (), options=self.options)
    270         self.assertEqual(ret, None)
    271         self.assertFalse(mock_check.called)
    272 
    273         # Second call should include some checks.
    274         diff = [rh.git.RawDiffEntry(file=x) for x in files]
    275         ret = func(self.project, 'commit', 'desc', diff, options=self.options)
    276         self.assertEqual(ret, mock_check.return_value)
    277 
    278     def testTheTester(self, _mock_check, _mock_run):
    279         """Make sure we have a test for every hook."""
    280         for hook in rh.hooks.BUILTIN_HOOKS:
    281             self.assertIn('test_%s' % (hook,), dir(self),
    282                           msg='Missing unittest for builtin hook %s' % (hook,))
    283 
    284     def test_checkpatch(self, mock_check, _mock_run):
    285         """Verify the checkpatch builtin hook."""
    286         ret = rh.hooks.check_checkpatch(
    287             self.project, 'commit', 'desc', (), options=self.options)
    288         self.assertEqual(ret, mock_check.return_value)
    289 
    290     def test_clang_format(self, mock_check, _mock_run):
    291         """Verify the clang_format builtin hook."""
    292         ret = rh.hooks.check_clang_format(
    293             self.project, 'commit', 'desc', (), options=self.options)
    294         self.assertEqual(ret, mock_check.return_value)
    295 
    296     def test_google_java_format(self, mock_check, _mock_run):
    297         """Verify the google_java_format builtin hook."""
    298         ret = rh.hooks.check_google_java_format(
    299             self.project, 'commit', 'desc', (), options=self.options)
    300         self.assertEqual(ret, mock_check.return_value)
    301 
    302     def test_commit_msg_bug_field(self, _mock_check, _mock_run):
    303         """Verify the commit_msg_bug_field builtin hook."""
    304         # Check some good messages.
    305         self._test_commit_messages(
    306             rh.hooks.check_commit_msg_bug_field, True, (
    307                 'subj\n\nBug: 1234\n',
    308                 'subj\n\nBug: 1234\nChange-Id: blah\n',
    309             ))
    310 
    311         # Check some bad messages.
    312         self._test_commit_messages(
    313             rh.hooks.check_commit_msg_bug_field, False, (
    314                 'subj',
    315                 'subj\n\nBUG=1234\n',
    316                 'subj\n\nBUG: 1234\n',
    317             ))
    318 
    319     def test_commit_msg_changeid_field(self, _mock_check, _mock_run):
    320         """Verify the commit_msg_changeid_field builtin hook."""
    321         # Check some good messages.
    322         self._test_commit_messages(
    323             rh.hooks.check_commit_msg_changeid_field, True, (
    324                 'subj\n\nChange-Id: I1234\n',
    325             ))
    326 
    327         # Check some bad messages.
    328         self._test_commit_messages(
    329             rh.hooks.check_commit_msg_changeid_field, False, (
    330                 'subj',
    331                 'subj\n\nChange-Id: 1234\n',
    332                 'subj\n\nChange-ID: I1234\n',
    333             ))
    334 
    335     def test_commit_msg_test_field(self, _mock_check, _mock_run):
    336         """Verify the commit_msg_test_field builtin hook."""
    337         # Check some good messages.
    338         self._test_commit_messages(
    339             rh.hooks.check_commit_msg_test_field, True, (
    340                 'subj\n\nTest: i did done dood it\n',
    341             ))
    342 
    343         # Check some bad messages.
    344         self._test_commit_messages(
    345             rh.hooks.check_commit_msg_test_field, False, (
    346                 'subj',
    347                 'subj\n\nTEST=1234\n',
    348                 'subj\n\nTEST: I1234\n',
    349             ))
    350 
    351     def test_cpplint(self, mock_check, _mock_run):
    352         """Verify the cpplint builtin hook."""
    353         self._test_file_filter(mock_check, rh.hooks.check_cpplint,
    354                                ('foo.cpp', 'foo.cxx'))
    355 
    356     def test_gofmt(self, mock_check, _mock_run):
    357         """Verify the gofmt builtin hook."""
    358         # First call should do nothing as there are no files to check.
    359         ret = rh.hooks.check_gofmt(
    360             self.project, 'commit', 'desc', (), options=self.options)
    361         self.assertEqual(ret, None)
    362         self.assertFalse(mock_check.called)
    363 
    364         # Second call will have some results.
    365         diff = [rh.git.RawDiffEntry(file='foo.go')]
    366         ret = rh.hooks.check_gofmt(
    367             self.project, 'commit', 'desc', diff, options=self.options)
    368         self.assertNotEqual(ret, None)
    369 
    370     def test_jsonlint(self, mock_check, _mock_run):
    371         """Verify the jsonlint builtin hook."""
    372         # First call should do nothing as there are no files to check.
    373         ret = rh.hooks.check_json(
    374             self.project, 'commit', 'desc', (), options=self.options)
    375         self.assertEqual(ret, None)
    376         self.assertFalse(mock_check.called)
    377 
    378         # TODO: Actually pass some valid/invalid json data down.
    379 
    380     def test_pylint(self, mock_check, _mock_run):
    381         """Verify the pylint builtin hook."""
    382         self._test_file_filter(mock_check, rh.hooks.check_pylint,
    383                                ('foo.py',))
    384 
    385     def test_xmllint(self, mock_check, _mock_run):
    386         """Verify the xmllint builtin hook."""
    387         self._test_file_filter(mock_check, rh.hooks.check_xmllint,
    388                                ('foo.xml',))
    389 
    390 
    391 if __name__ == '__main__':
    392     unittest.main()
    393