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