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