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