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 def testREPO_PROJECT(self): 201 """Verify handling of REPO_PROJECT.""" 202 os.environ['REPO_PROJECT'] = '' 203 self.assertEqual(self.replacer.get('REPO_PROJECT'), '') 204 os.environ['REPO_PROJECT'] = 'platform/foo/bar' 205 self.assertEqual(self.replacer.get('REPO_PROJECT'), 'platform/foo/bar') 206 207 @mock.patch.object(rh.hooks, '_get_build_os_name', return_value='vapier os') 208 def testBUILD_OS(self, m): 209 """Verify handling of BUILD_OS.""" 210 self.assertEqual(self.replacer.get('BUILD_OS'), m.return_value) 211 212 213class ExclusionScopeTests(unittest.TestCase): 214 """Verify behavior of ExclusionScope class.""" 215 216 def testEmpty(self): 217 """Verify the in operator for an empty scope.""" 218 scope = rh.hooks.ExclusionScope([]) 219 self.assertNotIn('external/*', scope) 220 221 def testGlob(self): 222 """Verify the in operator for a scope using wildcards.""" 223 scope = rh.hooks.ExclusionScope(['vendor/*', 'external/*']) 224 self.assertIn('external/tools', scope) 225 226 def testRegex(self): 227 """Verify the in operator for a scope using regular expressions.""" 228 scope = rh.hooks.ExclusionScope(['^vendor/(?!google)', 229 'external/*']) 230 self.assertIn('vendor/', scope) 231 self.assertNotIn('vendor/google/', scope) 232 self.assertIn('vendor/other/', scope) 233 234 235class HookOptionsTests(unittest.TestCase): 236 """Verify behavior of HookOptions object.""" 237 238 @mock.patch.object(rh.hooks, '_get_build_os_name', return_value='vapier os') 239 def testExpandVars(self, m): 240 """Verify expand_vars behavior.""" 241 # Simple pass through. 242 args = ['who', 'goes', 'there ?'] 243 self.assertEqual(args, rh.hooks.HookOptions.expand_vars(args)) 244 245 # At least one replacement. Most real testing is in PlaceholderTests. 246 args = ['who', 'goes', 'there ?', '${BUILD_OS} is great'] 247 exp_args = ['who', 'goes', 'there ?', f'{m.return_value} is great'] 248 self.assertEqual(exp_args, rh.hooks.HookOptions.expand_vars(args)) 249 250 def testArgs(self): 251 """Verify args behavior.""" 252 # Verify initial args to __init__ has higher precedent. 253 args = ['start', 'args'] 254 options = rh.hooks.HookOptions('hook name', args, {}) 255 self.assertEqual(options.args(), args) 256 self.assertEqual(options.args(default_args=['moo']), args) 257 258 # Verify we fall back to default_args. 259 args = ['default', 'args'] 260 options = rh.hooks.HookOptions('hook name', [], {}) 261 self.assertEqual(options.args(), []) 262 self.assertEqual(options.args(default_args=args), args) 263 264 def testToolPath(self): 265 """Verify tool_path behavior.""" 266 options = rh.hooks.HookOptions('hook name', [], { 267 'cpplint': 'my cpplint', 268 }) 269 # Check a builtin (and not overridden) tool. 270 self.assertEqual(options.tool_path('pylint'), 'pylint') 271 # Check an overridden tool. 272 self.assertEqual(options.tool_path('cpplint'), 'my cpplint') 273 # Check an unknown tool fails. 274 self.assertRaises(AssertionError, options.tool_path, 'extra_tool') 275 276 277class UtilsTests(unittest.TestCase): 278 """Verify misc utility functions.""" 279 280 def testRunCommand(self): 281 """Check _run behavior.""" 282 # Most testing is done against the utils.RunCommand already. 283 # pylint: disable=protected-access 284 ret = rh.hooks._run(['true']) 285 self.assertEqual(ret.returncode, 0) 286 287 def testBuildOs(self): 288 """Check _get_build_os_name behavior.""" 289 # Just verify it returns something and doesn't crash. 290 # pylint: disable=protected-access 291 ret = rh.hooks._get_build_os_name() 292 self.assertTrue(isinstance(ret, str)) 293 self.assertNotEqual(ret, '') 294 295 def testGetHelperPath(self): 296 """Check get_helper_path behavior.""" 297 # Just verify it doesn't crash. It's a dirt simple func. 298 ret = rh.hooks.get_helper_path('booga') 299 self.assertTrue(isinstance(ret, str)) 300 self.assertNotEqual(ret, '') 301 302 def testSortedToolPaths(self): 303 """Check TOOL_PATHS 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(list(rh.hooks.TOOL_PATHS), sorted(rh.hooks.TOOL_PATHS)) 308 309 def testSortedBuiltinHooks(self): 310 """Check BUILTIN_HOOKS is sorted.""" 311 # This assumes dictionary key ordering matches insertion/definition 312 # order which Python 3.7+ has codified. 313 # https://docs.python.org/3.7/library/stdtypes.html#dict 314 self.assertEqual( 315 list(rh.hooks.BUILTIN_HOOKS), sorted(rh.hooks.BUILTIN_HOOKS)) 316 317 318@mock.patch.object(rh.utils, 'run') 319@mock.patch.object(rh.hooks, '_check_cmd', return_value=['check_cmd']) 320class BuiltinHooksTests(unittest.TestCase): 321 """Verify the builtin hooks.""" 322 323 def setUp(self): 324 self.project = rh.Project(name='project-name', dir='/.../repo/dir') 325 self.options = rh.hooks.HookOptions('hook name', [], {}) 326 327 def _test_commit_messages(self, func, accept, msgs, files=None): 328 """Helper for testing commit message hooks. 329 330 Args: 331 func: The hook function to test. 332 accept: Whether all the |msgs| should be accepted. 333 msgs: List of messages to test. 334 files: List of files to pass to the hook. 335 """ 336 if files: 337 diff = [rh.git.RawDiffEntry(file=x) for x in files] 338 else: 339 diff = [] 340 for desc in msgs: 341 ret = func(self.project, 'commit', desc, diff, options=self.options) 342 if accept: 343 self.assertFalse( 344 bool(ret), msg='Should have accepted: {{{' + desc + '}}}') 345 else: 346 self.assertTrue( 347 bool(ret), msg='Should have rejected: {{{' + desc + '}}}') 348 349 def _test_file_filter(self, mock_check, func, files): 350 """Helper for testing hooks that filter by files and run external tools. 351 352 Args: 353 mock_check: The mock of _check_cmd. 354 func: The hook function to test. 355 files: A list of files that we'd check. 356 """ 357 # First call should do nothing as there are no files to check. 358 ret = func(self.project, 'commit', 'desc', (), options=self.options) 359 self.assertIsNone(ret) 360 self.assertFalse(mock_check.called) 361 362 # Second call should include some checks. 363 diff = [rh.git.RawDiffEntry(file=x) for x in files] 364 ret = func(self.project, 'commit', 'desc', diff, options=self.options) 365 self.assertEqual(ret, mock_check.return_value) 366 367 def testTheTester(self, _mock_check, _mock_run): 368 """Make sure we have a test for every hook.""" 369 for hook in rh.hooks.BUILTIN_HOOKS: 370 self.assertIn(f'test_{hook}', dir(self), 371 msg=f'Missing unittest for builtin hook {hook}') 372 373 def test_aosp_license(self, mock_check, _mock_run): 374 """Verify the aosp_license builtin hook.""" 375 # First call should do nothing as there are no files to check. 376 diff = [ 377 rh.git.RawDiffEntry(file='d.bp', status='D'), 378 rh.git.RawDiffEntry(file='m.bp', status='M'), 379 rh.git.RawDiffEntry(file='non-interested', status='A'), 380 ] 381 ret = rh.hooks.check_aosp_license( 382 self.project, 'commit', 'desc', diff, options=self.options) 383 self.assertIsNone(ret) 384 self.assertFalse(mock_check.called) 385 386 # Second call will have some results. 387 diff = [ 388 rh.git.RawDiffEntry(file='a.bp', status='A'), 389 ] 390 ret = rh.hooks.check_aosp_license( 391 self.project, 'commit', 'desc', diff, options=self.options) 392 self.assertIsNotNone(ret) 393 394 # No result since all paths are excluded. 395 diff = [ 396 rh.git.RawDiffEntry(file='a/a.bp', status='A'), 397 rh.git.RawDiffEntry(file='b/a.bp', status='A'), 398 rh.git.RawDiffEntry(file='c/d/a.bp', status='A'), 399 ] 400 ret = rh.hooks.check_aosp_license( 401 self.project, 'commit', 'desc', diff, 402 options=rh.hooks.HookOptions('hook name', 403 ['--exclude-dirs=a,b', '--exclude-dirs=c/d'], {}) 404 ) 405 self.assertIsNone(ret) 406 407 # Make sure that `--exclude-dir` doesn't match the path in the middle. 408 diff = [ 409 rh.git.RawDiffEntry(file='a/b/c.bp', status='A'), 410 ] 411 ret = rh.hooks.check_aosp_license( 412 self.project, 'commit', 'desc', diff, 413 options=rh.hooks.HookOptions('hook name', ['--exclude-dirs=b'], {}) 414 ) 415 self.assertIsNotNone(ret) 416 417 418 def test_bpfmt(self, mock_check, _mock_run): 419 """Verify the bpfmt builtin hook.""" 420 # First call should do nothing as there are no files to check. 421 ret = rh.hooks.check_bpfmt( 422 self.project, 'commit', 'desc', (), options=self.options) 423 self.assertIsNone(ret) 424 self.assertFalse(mock_check.called) 425 426 # Second call will have some results. 427 diff = [rh.git.RawDiffEntry(file='Android.bp')] 428 ret = rh.hooks.check_bpfmt( 429 self.project, 'commit', 'desc', diff, options=self.options) 430 self.assertIsNotNone(ret) 431 for result in ret: 432 self.assertIsNotNone(result.fixup_cmd) 433 434 def test_checkpatch(self, mock_check, _mock_run): 435 """Verify the checkpatch builtin hook.""" 436 ret = rh.hooks.check_checkpatch( 437 self.project, 'commit', 'desc', (), options=self.options) 438 self.assertEqual(ret, mock_check.return_value) 439 440 def test_clang_format(self, mock_check, _mock_run): 441 """Verify the clang_format builtin hook.""" 442 ret = rh.hooks.check_clang_format( 443 self.project, 'commit', 'desc', (), options=self.options) 444 self.assertEqual(ret, mock_check.return_value) 445 446 def test_google_java_format(self, mock_check, _mock_run): 447 """Verify the google_java_format builtin hook.""" 448 # First call should do nothing as there are no files to check. 449 ret = rh.hooks.check_google_java_format( 450 self.project, 'commit', 'desc', (), options=self.options) 451 self.assertIsNone(ret) 452 self.assertFalse(mock_check.called) 453 # Check that .java files are included by default. 454 diff = [rh.git.RawDiffEntry(file='foo.java'), 455 rh.git.RawDiffEntry(file='bar.kt'), 456 rh.git.RawDiffEntry(file='baz/blah.java')] 457 ret = rh.hooks.check_google_java_format( 458 self.project, 'commit', 'desc', diff, options=self.options) 459 self.assertListEqual(ret[0].files, ['foo.java', 'baz/blah.java']) 460 diff = [rh.git.RawDiffEntry(file='foo/f1.java'), 461 rh.git.RawDiffEntry(file='bar/f2.java'), 462 rh.git.RawDiffEntry(file='baz/f2.java')] 463 ret = rh.hooks.check_google_java_format( 464 self.project, 'commit', 'desc', diff, 465 options=rh.hooks.HookOptions('hook name', 466 ['--include-dirs=foo,baz'], {})) 467 self.assertListEqual(ret[0].files, ['foo/f1.java', 'baz/f2.java']) 468 469 def test_commit_msg_bug_field(self, _mock_check, _mock_run): 470 """Verify the commit_msg_bug_field builtin hook.""" 471 # Check some good messages. 472 self._test_commit_messages( 473 rh.hooks.check_commit_msg_bug_field, True, ( 474 'subj\n\nBug: 1234\n', 475 'subj\n\nBug: 1234\nChange-Id: blah\n', 476 'subj\n\nFix: 1234\n', 477 )) 478 479 # Check some bad messages. 480 self._test_commit_messages( 481 rh.hooks.check_commit_msg_bug_field, False, ( 482 'subj', 483 'subj\n\nBUG=1234\n', 484 'subj\n\nBUG: 1234\n', 485 'subj\n\nBug: N/A\n', 486 'subj\n\nBug:\n', 487 'subj\n\nFIX=1234\n', 488 )) 489 490 def test_commit_msg_changeid_field(self, _mock_check, _mock_run): 491 """Verify the commit_msg_changeid_field builtin hook.""" 492 # Check some good messages. 493 self._test_commit_messages( 494 rh.hooks.check_commit_msg_changeid_field, True, ( 495 'subj\n\nChange-Id: I1234\n', 496 )) 497 498 # Check some bad messages. 499 self._test_commit_messages( 500 rh.hooks.check_commit_msg_changeid_field, False, ( 501 'subj', 502 'subj\n\nChange-Id: 1234\n', 503 'subj\n\nChange-ID: I1234\n', 504 )) 505 506 def test_commit_msg_prebuilt_apk_fields(self, _mock_check, _mock_run): 507 """Verify the check_commit_msg_prebuilt_apk_fields builtin hook.""" 508 # Commits without APKs should pass. 509 self._test_commit_messages( 510 rh.hooks.check_commit_msg_prebuilt_apk_fields, 511 True, 512 ( 513 'subj\nTest: test case\nBug: bug id\n', 514 ), 515 ['foo.cpp', 'bar.py',] 516 ) 517 518 # Commits with APKs and all the required messages should pass. 519 self._test_commit_messages( 520 rh.hooks.check_commit_msg_prebuilt_apk_fields, 521 True, 522 ( 523 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 524 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 525 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 526 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 527 'targetSdkVersion:\'28\'\n\nBuilt here:\n' 528 'http://foo.bar.com/builder\n\n' 529 'This build IS suitable for public release.\n\n' 530 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 531 ('Test App\n\nBuilt here:\nhttp://foo.bar.com/builder\n\n' 532 'This build IS NOT suitable for public release.\n\n' 533 'bar.apk\npackage: name=\'com.foo.bar\'\n' 534 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 535 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 536 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 537 'targetSdkVersion:\'28\'\n\nBug: 123\nTest: test\n' 538 'Change-Id: XXXXXXX\n'), 539 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 540 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 541 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 542 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 543 'targetSdkVersion:\'28\'\n\nBuilt here:\n' 544 'http://foo.bar.com/builder\n\n' 545 'This build IS suitable for preview release but IS NOT ' 546 'suitable for public release.\n\n' 547 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 548 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 549 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 550 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 551 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 552 'targetSdkVersion:\'28\'\n\nBuilt here:\n' 553 'http://foo.bar.com/builder\n\n' 554 'This build IS NOT suitable for preview or public release.\n\n' 555 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 556 ), 557 ['foo.apk', 'bar.py',] 558 ) 559 560 # Commits with APKs and without all the required messages should fail. 561 self._test_commit_messages( 562 rh.hooks.check_commit_msg_prebuilt_apk_fields, 563 False, 564 ( 565 'subj\nTest: test case\nBug: bug id\n', 566 # Missing 'package'. 567 ('Test App\n\nbar.apk\n' 568 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 569 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 570 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 571 'targetSdkVersion:\'28\'\n\nBuilt here:\n' 572 'http://foo.bar.com/builder\n\n' 573 'This build IS suitable for public release.\n\n' 574 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 575 # Missing 'sdkVersion'. 576 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 577 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 578 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 579 'compileSdkVersionCodename=\'9\'\n' 580 'targetSdkVersion:\'28\'\n\nBuilt here:\n' 581 'http://foo.bar.com/builder\n\n' 582 'This build IS suitable for public release.\n\n' 583 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 584 # Missing 'targetSdkVersion'. 585 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 586 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 587 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 588 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 589 'Built here:\nhttp://foo.bar.com/builder\n\n' 590 'This build IS suitable for public release.\n\n' 591 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 592 # Missing build location. 593 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 594 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 595 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 596 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 597 'targetSdkVersion:\'28\'\n\n' 598 'This build IS suitable for public release.\n\n' 599 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 600 # Missing public release indication. 601 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 602 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 603 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 604 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 605 'targetSdkVersion:\'28\'\n\nBuilt here:\n' 606 'http://foo.bar.com/builder\n\n' 607 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 608 ), 609 ['foo.apk', 'bar.py',] 610 ) 611 612 def test_commit_msg_test_field(self, _mock_check, _mock_run): 613 """Verify the commit_msg_test_field builtin hook.""" 614 # Check some good messages. 615 self._test_commit_messages( 616 rh.hooks.check_commit_msg_test_field, True, ( 617 'subj\n\nTest: i did done dood it\n', 618 )) 619 620 # Check some bad messages. 621 self._test_commit_messages( 622 rh.hooks.check_commit_msg_test_field, False, ( 623 'subj', 624 'subj\n\nTEST=1234\n', 625 'subj\n\nTEST: I1234\n', 626 )) 627 628 def test_commit_msg_relnote_field_format(self, _mock_check, _mock_run): 629 """Verify the commit_msg_relnote_field_format builtin hook.""" 630 # Check some good messages. 631 self._test_commit_messages( 632 rh.hooks.check_commit_msg_relnote_field_format, 633 True, 634 ( 635 'subj', 636 'subj\n\nTest: i did done dood it\nBug: 1234', 637 'subj\n\nMore content\n\nTest: i did done dood it\nBug: 1234', 638 'subj\n\nRelnote: This is a release note\nBug: 1234', 639 'subj\n\nRelnote:This is a release note\nBug: 1234', 640 'subj\n\nRelnote: This is a release note.\nBug: 1234', 641 'subj\n\nRelnote: "This is a release note."\nBug: 1234', 642 'subj\n\nRelnote: "This is a \\"release note\\"."\n\nBug: 1234', 643 'subj\n\nRelnote: This is a release note.\nChange-Id: 1234', 644 'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234', 645 ('subj\n\nRelnote: "This is a release note."\n\n' 646 'Change-Id: 1234'), 647 ('subj\n\nRelnote: This is a release note.\n\n' 648 'It has more info, but it is not part of the release note' 649 '\nChange-Id: 1234'), 650 ('subj\n\nRelnote: "This is a release note.\n' 651 'It contains a correct second line."'), 652 ('subj\n\nRelnote:"This is a release note.\n' 653 'It contains a correct second line."'), 654 ('subj\n\nRelnote: "This is a release note.\n' 655 'It contains a correct second line.\n' 656 'And even a third line."\n' 657 'Bug: 1234'), 658 ('subj\n\nRelnote: "This is a release note.\n' 659 'It contains a correct second line.\n' 660 '\\"Quotes\\" are even used on the third line."\n' 661 'Bug: 1234'), 662 ('subj\n\nRelnote: This is release note 1.\n' 663 'Relnote: This is release note 2.\n' 664 'Bug: 1234'), 665 ('subj\n\nRelnote: This is release note 1.\n' 666 'Relnote: "This is release note 2, and it\n' 667 'contains a correctly formatted third line."\n' 668 'Bug: 1234'), 669 ('subj\n\nRelnote: "This is release note 1 with\n' 670 'a correctly formatted second line."\n\n' 671 'Relnote: "This is release note 2, and it\n' 672 'contains a correctly formatted second line."\n' 673 'Bug: 1234'), 674 ('subj\n\nRelnote: "This is a release note with\n' 675 'a correctly formatted second line."\n\n' 676 'Bug: 1234' 677 'Here is some extra "quoted" content.'), 678 ('subj\n\nRelnote: """This is a release note.\n\n' 679 'This relnote contains an empty line.\n' 680 'Then a non-empty line.\n\n' 681 'And another empty line."""\n\n' 682 'Bug: 1234'), 683 ('subj\n\nRelnote: """This is a release note.\n\n' 684 'This relnote contains an empty line.\n' 685 'Then an acceptable "quoted" line.\n\n' 686 'And another empty line."""\n\n' 687 'Bug: 1234'), 688 ('subj\n\nRelnote: """This is a release note."""\n\n' 689 'Bug: 1234'), 690 ('subj\n\nRelnote: """This is a release note.\n' 691 'It has a second line."""\n\n' 692 'Bug: 1234'), 693 ('subj\n\nRelnote: """This is a release note.\n' 694 'It has a second line, but does not end here.\n' 695 '"""\n\n' 696 'Bug: 1234'), 697 ('subj\n\nRelnote: """This is a release note.\n' 698 '"It" has a second line, but does not end here.\n' 699 '"""\n\n' 700 'Bug: 1234'), 701 ('subj\n\nRelnote: "This is a release note.\n' 702 'It has a second line, but does not end here.\n' 703 '"\n\n' 704 'Bug: 1234'), 705 )) 706 707 # Check some bad messages. 708 self._test_commit_messages( 709 rh.hooks.check_commit_msg_relnote_field_format, 710 False, 711 ( 712 'subj\n\nReleaseNote: This is a release note.\n', 713 'subj\n\nRelnotes: This is a release note.\n', 714 'subj\n\nRel-note: This is a release note.\n', 715 'subj\n\nrelnoTes: This is a release note.\n', 716 'subj\n\nrel-Note: This is a release note.\n', 717 'subj\n\nRelnote: "This is a "release note"."\nBug: 1234', 718 'subj\n\nRelnote: This is a "release note".\nBug: 1234', 719 ('subj\n\nRelnote: This is a release note.\n' 720 'It contains an incorrect second line.'), 721 ('subj\n\nRelnote: "This is a release note.\n' 722 'It contains multiple lines.\n' 723 'But it does not provide an ending quote.\n'), 724 ('subj\n\nRelnote: "This is a release note.\n' 725 'It contains multiple lines but no closing quote.\n' 726 'Test: my test "hello world"\n'), 727 ('subj\n\nRelnote: This is release note 1.\n' 728 'Relnote: "This is release note 2, and it\n' 729 'contains an incorrectly formatted third line.\n' 730 'Bug: 1234'), 731 ('subj\n\nRelnote: This is release note 1 with\n' 732 'an incorrectly formatted second line.\n\n' 733 'Relnote: "This is release note 2, and it\n' 734 'contains a correctly formatted second line."\n' 735 'Bug: 1234'), 736 ('subj\n\nRelnote: "This is release note 1 with\n' 737 'a correctly formatted second line."\n\n' 738 'Relnote: This is release note 2, and it\n' 739 'contains an incorrectly formatted second line.\n' 740 'Bug: 1234'), 741 ('subj\n\nRelnote: "This is a release note.\n' 742 'It contains a correct second line.\n' 743 'But incorrect "quotes" on the third line."\n' 744 'Bug: 1234'), 745 ('subj\n\nRelnote: """This is a release note.\n' 746 'It has a second line, but no closing triple quote.\n\n' 747 'Bug: 1234'), 748 ('subj\n\nRelnote: "This is a release note.\n' 749 '"It" has a second line, but does not end here.\n' 750 '"\n\n' 751 'Bug: 1234'), 752 )) 753 754 def test_commit_msg_relnote_for_current_txt(self, _mock_check, _mock_run): 755 """Verify the commit_msg_relnote_for_current_txt builtin hook.""" 756 diff_without_current_txt = ['bar/foo.txt', 757 'foo.cpp', 758 'foo.java', 759 'foo_current.java', 760 'foo_current.txt', 761 'baz/current.java', 762 'baz/foo_current.txt'] 763 diff_with_current_txt = diff_without_current_txt + ['current.txt'] 764 diff_with_subdir_current_txt = \ 765 diff_without_current_txt + ['foo/current.txt'] 766 diff_with_experimental_current_txt = \ 767 diff_without_current_txt + ['public_plus_experimental_current.txt'] 768 # Check some good messages. 769 self._test_commit_messages( 770 rh.hooks.check_commit_msg_relnote_for_current_txt, 771 True, 772 ( 773 'subj\n\nRelnote: This is a release note\n', 774 'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234', 775 ('subj\n\nRelnote: This is release note 1 with\n' 776 'an incorrectly formatted second line.\n\n' 777 'Relnote: "This is release note 2, and it\n' 778 'contains a correctly formatted second line."\n' 779 'Bug: 1234'), 780 ), 781 files=diff_with_current_txt, 782 ) 783 # Check some good messages. 784 self._test_commit_messages( 785 rh.hooks.check_commit_msg_relnote_for_current_txt, 786 True, 787 ( 788 'subj\n\nRelnote: This is a release note\n', 789 'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234', 790 ('subj\n\nRelnote: This is release note 1 with\n' 791 'an incorrectly formatted second line.\n\n' 792 'Relnote: "This is release note 2, and it\n' 793 'contains a correctly formatted second line."\n' 794 'Bug: 1234'), 795 ), 796 files=diff_with_experimental_current_txt, 797 ) 798 # Check some good messages. 799 self._test_commit_messages( 800 rh.hooks.check_commit_msg_relnote_for_current_txt, 801 True, 802 ( 803 'subj\n\nRelnote: This is a release note\n', 804 'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234', 805 ('subj\n\nRelnote: This is release note 1 with\n' 806 'an incorrectly formatted second line.\n\n' 807 'Relnote: "This is release note 2, and it\n' 808 'contains a correctly formatted second line."\n' 809 'Bug: 1234'), 810 ), 811 files=diff_with_subdir_current_txt, 812 ) 813 # Check some good messages. 814 self._test_commit_messages( 815 rh.hooks.check_commit_msg_relnote_for_current_txt, 816 True, 817 ( 818 'subj', 819 'subj\nBug: 12345\nChange-Id: 1234', 820 'subj\n\nRelnote: This is a release note\n', 821 'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234', 822 ('subj\n\nRelnote: This is release note 1 with\n' 823 'an incorrectly formatted second line.\n\n' 824 'Relnote: "This is release note 2, and it\n' 825 'contains a correctly formatted second line."\n' 826 'Bug: 1234'), 827 ), 828 files=diff_without_current_txt, 829 ) 830 # Check some bad messages. 831 self._test_commit_messages( 832 rh.hooks.check_commit_msg_relnote_for_current_txt, 833 False, 834 ( 835 'subj' 836 'subj\nBug: 12345\nChange-Id: 1234', 837 ), 838 files=diff_with_current_txt, 839 ) 840 # Check some bad messages. 841 self._test_commit_messages( 842 rh.hooks.check_commit_msg_relnote_for_current_txt, 843 False, 844 ( 845 'subj' 846 'subj\nBug: 12345\nChange-Id: 1234', 847 ), 848 files=diff_with_experimental_current_txt, 849 ) 850 # Check some bad messages. 851 self._test_commit_messages( 852 rh.hooks.check_commit_msg_relnote_for_current_txt, 853 False, 854 ( 855 'subj' 856 'subj\nBug: 12345\nChange-Id: 1234', 857 ), 858 files=diff_with_subdir_current_txt, 859 ) 860 861 def test_cpplint(self, mock_check, _mock_run): 862 """Verify the cpplint builtin hook.""" 863 self._test_file_filter(mock_check, rh.hooks.check_cpplint, 864 ('foo.cpp', 'foo.cxx')) 865 866 def test_gofmt(self, mock_check, _mock_run): 867 """Verify the gofmt builtin hook.""" 868 # First call should do nothing as there are no files to check. 869 ret = rh.hooks.check_gofmt( 870 self.project, 'commit', 'desc', (), options=self.options) 871 self.assertIsNone(ret) 872 self.assertFalse(mock_check.called) 873 874 # Second call will have some results. 875 diff = [rh.git.RawDiffEntry(file='foo.go')] 876 ret = rh.hooks.check_gofmt( 877 self.project, 'commit', 'desc', diff, options=self.options) 878 self.assertIsNotNone(ret) 879 880 def test_jsonlint(self, mock_check, _mock_run): 881 """Verify the jsonlint builtin hook.""" 882 # First call should do nothing as there are no files to check. 883 ret = rh.hooks.check_json( 884 self.project, 'commit', 'desc', (), options=self.options) 885 self.assertIsNone(ret) 886 self.assertFalse(mock_check.called) 887 888 # TODO: Actually pass some valid/invalid json data down. 889 890 def test_ktfmt(self, mock_check, _mock_run): 891 """Verify the ktfmt builtin hook.""" 892 # First call should do nothing as there are no files to check. 893 ret = rh.hooks.check_ktfmt( 894 self.project, 'commit', 'desc', (), options=self.options) 895 self.assertIsNone(ret) 896 self.assertFalse(mock_check.called) 897 # Check that .kt files are included by default. 898 diff = [rh.git.RawDiffEntry(file='foo.kt'), 899 rh.git.RawDiffEntry(file='bar.java'), 900 rh.git.RawDiffEntry(file='baz/blah.kt')] 901 ret = rh.hooks.check_ktfmt( 902 self.project, 'commit', 'desc', diff, options=self.options) 903 self.assertListEqual(ret[0].files, ['foo.kt', 'baz/blah.kt']) 904 diff = [rh.git.RawDiffEntry(file='foo/f1.kt'), 905 rh.git.RawDiffEntry(file='bar/f2.kt'), 906 rh.git.RawDiffEntry(file='baz/f2.kt')] 907 ret = rh.hooks.check_ktfmt(self.project, 'commit', 'desc', diff, 908 options=rh.hooks.HookOptions('hook name', [ 909 '--include-dirs=foo,baz'], {})) 910 self.assertListEqual(ret[0].files, ['foo/f1.kt', 'baz/f2.kt']) 911 912 def test_pylint(self, mock_check, _mock_run): 913 """Verify the pylint builtin hook.""" 914 self._test_file_filter(mock_check, rh.hooks.check_pylint2, 915 ('foo.py',)) 916 917 def test_pylint2(self, mock_check, _mock_run): 918 """Verify the pylint2 builtin hook.""" 919 self._test_file_filter(mock_check, rh.hooks.check_pylint2, 920 ('foo.py',)) 921 922 def test_pylint3(self, mock_check, _mock_run): 923 """Verify the pylint3 builtin hook.""" 924 self._test_file_filter(mock_check, rh.hooks.check_pylint3, 925 ('foo.py',)) 926 927 def test_rustfmt(self, mock_check, _mock_run): 928 # First call should do nothing as there are no files to check. 929 ret = rh.hooks.check_rustfmt( 930 self.project, 'commit', 'desc', (), options=self.options) 931 self.assertEqual(ret, None) 932 self.assertFalse(mock_check.called) 933 934 # Second call will have some results. 935 diff = [rh.git.RawDiffEntry(file='lib.rs')] 936 ret = rh.hooks.check_rustfmt( 937 self.project, 'commit', 'desc', diff, options=self.options) 938 self.assertNotEqual(ret, None) 939 940 def test_xmllint(self, mock_check, _mock_run): 941 """Verify the xmllint builtin hook.""" 942 self._test_file_filter(mock_check, rh.hooks.check_xmllint, 943 ('foo.xml',)) 944 945 def test_android_test_mapping_format(self, mock_check, _mock_run): 946 """Verify the android_test_mapping_format builtin hook.""" 947 # First call should do nothing as there are no files to check. 948 ret = rh.hooks.check_android_test_mapping( 949 self.project, 'commit', 'desc', (), options=self.options) 950 self.assertIsNone(ret) 951 self.assertFalse(mock_check.called) 952 953 # Second call will have some results. 954 diff = [rh.git.RawDiffEntry(file='TEST_MAPPING')] 955 ret = rh.hooks.check_android_test_mapping( 956 self.project, 'commit', 'desc', diff, options=self.options) 957 self.assertIsNotNone(ret) 958 959 def test_aidl_format(self, mock_check, _mock_run): 960 """Verify the aidl_format builtin hook.""" 961 # First call should do nothing as there are no files to check. 962 ret = rh.hooks.check_aidl_format( 963 self.project, 'commit', 'desc', (), options=self.options) 964 self.assertIsNone(ret) 965 self.assertFalse(mock_check.called) 966 967 # Second call will have some results. 968 diff = [rh.git.RawDiffEntry(file='IFoo.go')] 969 ret = rh.hooks.check_gofmt( 970 self.project, 'commit', 'desc', diff, options=self.options) 971 self.assertIsNotNone(ret) 972 973 974if __name__ == '__main__': 975 unittest.main() 976