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_bpfmt(self, mock_check, _mock_run): 374 """Verify the bpfmt builtin hook.""" 375 # First call should do nothing as there are no files to check. 376 ret = rh.hooks.check_bpfmt( 377 self.project, 'commit', 'desc', (), options=self.options) 378 self.assertIsNone(ret) 379 self.assertFalse(mock_check.called) 380 381 # Second call will have some results. 382 diff = [rh.git.RawDiffEntry(file='Android.bp')] 383 ret = rh.hooks.check_bpfmt( 384 self.project, 'commit', 'desc', diff, options=self.options) 385 self.assertIsNotNone(ret) 386 for result in ret: 387 self.assertIsNotNone(result.fixup_cmd) 388 389 def test_checkpatch(self, mock_check, _mock_run): 390 """Verify the checkpatch builtin hook.""" 391 ret = rh.hooks.check_checkpatch( 392 self.project, 'commit', 'desc', (), options=self.options) 393 self.assertEqual(ret, mock_check.return_value) 394 395 def test_clang_format(self, mock_check, _mock_run): 396 """Verify the clang_format builtin hook.""" 397 ret = rh.hooks.check_clang_format( 398 self.project, 'commit', 'desc', (), options=self.options) 399 self.assertEqual(ret, mock_check.return_value) 400 401 def test_google_java_format(self, mock_check, _mock_run): 402 """Verify the google_java_format builtin hook.""" 403 # First call should do nothing as there are no files to check. 404 ret = rh.hooks.check_google_java_format( 405 self.project, 'commit', 'desc', (), options=self.options) 406 self.assertIsNone(ret) 407 self.assertFalse(mock_check.called) 408 # Check that .java files are included by default. 409 diff = [rh.git.RawDiffEntry(file='foo.java'), 410 rh.git.RawDiffEntry(file='bar.kt'), 411 rh.git.RawDiffEntry(file='baz/blah.java')] 412 ret = rh.hooks.check_google_java_format( 413 self.project, 'commit', 'desc', diff, options=self.options) 414 self.assertListEqual(ret[0].files, ['foo.java', 'baz/blah.java']) 415 diff = [rh.git.RawDiffEntry(file='foo/f1.java'), 416 rh.git.RawDiffEntry(file='bar/f2.java'), 417 rh.git.RawDiffEntry(file='baz/f2.java')] 418 ret = rh.hooks.check_google_java_format( 419 self.project, 'commit', 'desc', diff, 420 options=rh.hooks.HookOptions('hook name', 421 ['--include-dirs=foo,baz'], {})) 422 self.assertListEqual(ret[0].files, ['foo/f1.java', 'baz/f2.java']) 423 424 def test_commit_msg_bug_field(self, _mock_check, _mock_run): 425 """Verify the commit_msg_bug_field builtin hook.""" 426 # Check some good messages. 427 self._test_commit_messages( 428 rh.hooks.check_commit_msg_bug_field, True, ( 429 'subj\n\nBug: 1234\n', 430 'subj\n\nBug: 1234\nChange-Id: blah\n', 431 )) 432 433 # Check some bad messages. 434 self._test_commit_messages( 435 rh.hooks.check_commit_msg_bug_field, False, ( 436 'subj', 437 'subj\n\nBUG=1234\n', 438 'subj\n\nBUG: 1234\n', 439 'subj\n\nBug: N/A\n', 440 'subj\n\nBug:\n', 441 )) 442 443 def test_commit_msg_changeid_field(self, _mock_check, _mock_run): 444 """Verify the commit_msg_changeid_field builtin hook.""" 445 # Check some good messages. 446 self._test_commit_messages( 447 rh.hooks.check_commit_msg_changeid_field, True, ( 448 'subj\n\nChange-Id: I1234\n', 449 )) 450 451 # Check some bad messages. 452 self._test_commit_messages( 453 rh.hooks.check_commit_msg_changeid_field, False, ( 454 'subj', 455 'subj\n\nChange-Id: 1234\n', 456 'subj\n\nChange-ID: I1234\n', 457 )) 458 459 def test_commit_msg_prebuilt_apk_fields(self, _mock_check, _mock_run): 460 """Verify the check_commit_msg_prebuilt_apk_fields builtin hook.""" 461 # Commits without APKs should pass. 462 self._test_commit_messages( 463 rh.hooks.check_commit_msg_prebuilt_apk_fields, 464 True, 465 ( 466 'subj\nTest: test case\nBug: bug id\n', 467 ), 468 ['foo.cpp', 'bar.py',] 469 ) 470 471 # Commits with APKs and all the required messages should pass. 472 self._test_commit_messages( 473 rh.hooks.check_commit_msg_prebuilt_apk_fields, 474 True, 475 ( 476 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 477 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 478 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 479 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 480 'targetSdkVersion:\'28\'\n\nBuilt here:\n' 481 'http://foo.bar.com/builder\n\n' 482 'This build IS suitable for public release.\n\n' 483 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 484 ('Test App\n\nBuilt here:\nhttp://foo.bar.com/builder\n\n' 485 'This build IS NOT suitable for public release.\n\n' 486 'bar.apk\npackage: name=\'com.foo.bar\'\n' 487 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 488 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 489 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 490 'targetSdkVersion:\'28\'\n\nBug: 123\nTest: test\n' 491 'Change-Id: XXXXXXX\n'), 492 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 493 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 494 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 495 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 496 'targetSdkVersion:\'28\'\n\nBuilt here:\n' 497 'http://foo.bar.com/builder\n\n' 498 'This build IS suitable for preview release but IS NOT ' 499 'suitable for public release.\n\n' 500 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 501 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 502 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 503 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 504 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 505 'targetSdkVersion:\'28\'\n\nBuilt here:\n' 506 'http://foo.bar.com/builder\n\n' 507 'This build IS NOT suitable for preview or public release.\n\n' 508 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 509 ), 510 ['foo.apk', 'bar.py',] 511 ) 512 513 # Commits with APKs and without all the required messages should fail. 514 self._test_commit_messages( 515 rh.hooks.check_commit_msg_prebuilt_apk_fields, 516 False, 517 ( 518 'subj\nTest: test case\nBug: bug id\n', 519 # Missing 'package'. 520 ('Test App\n\nbar.apk\n' 521 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 522 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 523 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 524 'targetSdkVersion:\'28\'\n\nBuilt here:\n' 525 'http://foo.bar.com/builder\n\n' 526 'This build IS suitable for public release.\n\n' 527 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 528 # Missing 'sdkVersion'. 529 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 530 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 531 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 532 'compileSdkVersionCodename=\'9\'\n' 533 'targetSdkVersion:\'28\'\n\nBuilt here:\n' 534 'http://foo.bar.com/builder\n\n' 535 'This build IS suitable for public release.\n\n' 536 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 537 # Missing 'targetSdkVersion'. 538 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 539 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 540 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 541 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 542 'Built here:\nhttp://foo.bar.com/builder\n\n' 543 'This build IS suitable for public release.\n\n' 544 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 545 # Missing build location. 546 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 547 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 548 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 549 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 550 'targetSdkVersion:\'28\'\n\n' 551 'This build IS suitable for public release.\n\n' 552 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 553 # Missing public release indication. 554 ('Test App\n\nbar.apk\npackage: name=\'com.foo.bar\'\n' 555 'versionCode=\'1001\'\nversionName=\'1.0.1001-A\'\n' 556 'platformBuildVersionName=\'\'\ncompileSdkVersion=\'28\'\n' 557 'compileSdkVersionCodename=\'9\'\nsdkVersion:\'16\'\n' 558 'targetSdkVersion:\'28\'\n\nBuilt here:\n' 559 'http://foo.bar.com/builder\n\n' 560 'Bug: 123\nTest: test\nChange-Id: XXXXXXX\n'), 561 ), 562 ['foo.apk', 'bar.py',] 563 ) 564 565 def test_commit_msg_test_field(self, _mock_check, _mock_run): 566 """Verify the commit_msg_test_field builtin hook.""" 567 # Check some good messages. 568 self._test_commit_messages( 569 rh.hooks.check_commit_msg_test_field, True, ( 570 'subj\n\nTest: i did done dood it\n', 571 )) 572 573 # Check some bad messages. 574 self._test_commit_messages( 575 rh.hooks.check_commit_msg_test_field, False, ( 576 'subj', 577 'subj\n\nTEST=1234\n', 578 'subj\n\nTEST: I1234\n', 579 )) 580 581 def test_commit_msg_relnote_field_format(self, _mock_check, _mock_run): 582 """Verify the commit_msg_relnote_field_format builtin hook.""" 583 # Check some good messages. 584 self._test_commit_messages( 585 rh.hooks.check_commit_msg_relnote_field_format, 586 True, 587 ( 588 'subj', 589 'subj\n\nTest: i did done dood it\nBug: 1234', 590 'subj\n\nMore content\n\nTest: i did done dood it\nBug: 1234', 591 'subj\n\nRelnote: This is a release note\nBug: 1234', 592 'subj\n\nRelnote:This is a release note\nBug: 1234', 593 'subj\n\nRelnote: This is a release note.\nBug: 1234', 594 'subj\n\nRelnote: "This is a release note."\nBug: 1234', 595 'subj\n\nRelnote: "This is a \\"release note\\"."\n\nBug: 1234', 596 'subj\n\nRelnote: This is a release note.\nChange-Id: 1234', 597 'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234', 598 ('subj\n\nRelnote: "This is a release note."\n\n' 599 'Change-Id: 1234'), 600 ('subj\n\nRelnote: This is a release note.\n\n' 601 'It has more info, but it is not part of the release note' 602 '\nChange-Id: 1234'), 603 ('subj\n\nRelnote: "This is a release note.\n' 604 'It contains a correct second line."'), 605 ('subj\n\nRelnote:"This is a release note.\n' 606 'It contains a correct second line."'), 607 ('subj\n\nRelnote: "This is a release note.\n' 608 'It contains a correct second line.\n' 609 'And even a third line."\n' 610 'Bug: 1234'), 611 ('subj\n\nRelnote: "This is a release note.\n' 612 'It contains a correct second line.\n' 613 '\\"Quotes\\" are even used on the third line."\n' 614 'Bug: 1234'), 615 ('subj\n\nRelnote: This is release note 1.\n' 616 'Relnote: This is release note 2.\n' 617 'Bug: 1234'), 618 ('subj\n\nRelnote: This is release note 1.\n' 619 'Relnote: "This is release note 2, and it\n' 620 'contains a correctly formatted third line."\n' 621 'Bug: 1234'), 622 ('subj\n\nRelnote: "This is release note 1 with\n' 623 'a correctly formatted second line."\n\n' 624 'Relnote: "This is release note 2, and it\n' 625 'contains a correctly formatted second line."\n' 626 'Bug: 1234'), 627 ('subj\n\nRelnote: "This is a release note with\n' 628 'a correctly formatted second line."\n\n' 629 'Bug: 1234' 630 'Here is some extra "quoted" content.'), 631 ('subj\n\nRelnote: """This is a release note.\n\n' 632 'This relnote contains an empty line.\n' 633 'Then a non-empty line.\n\n' 634 'And another empty line."""\n\n' 635 'Bug: 1234'), 636 ('subj\n\nRelnote: """This is a release note.\n\n' 637 'This relnote contains an empty line.\n' 638 'Then an acceptable "quoted" line.\n\n' 639 'And another empty line."""\n\n' 640 'Bug: 1234'), 641 ('subj\n\nRelnote: """This is a release note."""\n\n' 642 'Bug: 1234'), 643 ('subj\n\nRelnote: """This is a release note.\n' 644 'It has a second line."""\n\n' 645 'Bug: 1234'), 646 ('subj\n\nRelnote: """This is a release note.\n' 647 'It has a second line, but does not end here.\n' 648 '"""\n\n' 649 'Bug: 1234'), 650 ('subj\n\nRelnote: """This is a release note.\n' 651 '"It" has a second line, but does not end here.\n' 652 '"""\n\n' 653 'Bug: 1234'), 654 ('subj\n\nRelnote: "This is a release note.\n' 655 'It has a second line, but does not end here.\n' 656 '"\n\n' 657 'Bug: 1234'), 658 )) 659 660 # Check some bad messages. 661 self._test_commit_messages( 662 rh.hooks.check_commit_msg_relnote_field_format, 663 False, 664 ( 665 'subj\n\nReleaseNote: This is a release note.\n', 666 'subj\n\nRelnotes: This is a release note.\n', 667 'subj\n\nRel-note: This is a release note.\n', 668 'subj\n\nrelnoTes: This is a release note.\n', 669 'subj\n\nrel-Note: This is a release note.\n', 670 'subj\n\nRelnote: "This is a "release note"."\nBug: 1234', 671 'subj\n\nRelnote: This is a "release note".\nBug: 1234', 672 ('subj\n\nRelnote: This is a release note.\n' 673 'It contains an incorrect second line.'), 674 ('subj\n\nRelnote: "This is a release note.\n' 675 'It contains multiple lines.\n' 676 'But it does not provide an ending quote.\n'), 677 ('subj\n\nRelnote: "This is a release note.\n' 678 'It contains multiple lines but no closing quote.\n' 679 'Test: my test "hello world"\n'), 680 ('subj\n\nRelnote: This is release note 1.\n' 681 'Relnote: "This is release note 2, and it\n' 682 'contains an incorrectly formatted third line.\n' 683 'Bug: 1234'), 684 ('subj\n\nRelnote: This is release note 1 with\n' 685 'an incorrectly formatted second line.\n\n' 686 'Relnote: "This is release note 2, and it\n' 687 'contains a correctly formatted second line."\n' 688 'Bug: 1234'), 689 ('subj\n\nRelnote: "This is release note 1 with\n' 690 'a correctly formatted second line."\n\n' 691 'Relnote: This is release note 2, and it\n' 692 'contains an incorrectly formatted second line.\n' 693 'Bug: 1234'), 694 ('subj\n\nRelnote: "This is a release note.\n' 695 'It contains a correct second line.\n' 696 'But incorrect "quotes" on the third line."\n' 697 'Bug: 1234'), 698 ('subj\n\nRelnote: """This is a release note.\n' 699 'It has a second line, but no closing triple quote.\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 def test_commit_msg_relnote_for_current_txt(self, _mock_check, _mock_run): 708 """Verify the commit_msg_relnote_for_current_txt builtin hook.""" 709 diff_without_current_txt = ['bar/foo.txt', 710 'foo.cpp', 711 'foo.java', 712 'foo_current.java', 713 'foo_current.txt', 714 'baz/current.java', 715 'baz/foo_current.txt'] 716 diff_with_current_txt = diff_without_current_txt + ['current.txt'] 717 diff_with_subdir_current_txt = \ 718 diff_without_current_txt + ['foo/current.txt'] 719 diff_with_experimental_current_txt = \ 720 diff_without_current_txt + ['public_plus_experimental_current.txt'] 721 # Check some good messages. 722 self._test_commit_messages( 723 rh.hooks.check_commit_msg_relnote_for_current_txt, 724 True, 725 ( 726 'subj\n\nRelnote: This is a release note\n', 727 'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234', 728 ('subj\n\nRelnote: This is release note 1 with\n' 729 'an incorrectly formatted second line.\n\n' 730 'Relnote: "This is release note 2, and it\n' 731 'contains a correctly formatted second line."\n' 732 'Bug: 1234'), 733 ), 734 files=diff_with_current_txt, 735 ) 736 # Check some good messages. 737 self._test_commit_messages( 738 rh.hooks.check_commit_msg_relnote_for_current_txt, 739 True, 740 ( 741 'subj\n\nRelnote: This is a release note\n', 742 'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234', 743 ('subj\n\nRelnote: This is release note 1 with\n' 744 'an incorrectly formatted second line.\n\n' 745 'Relnote: "This is release note 2, and it\n' 746 'contains a correctly formatted second line."\n' 747 'Bug: 1234'), 748 ), 749 files=diff_with_experimental_current_txt, 750 ) 751 # Check some good messages. 752 self._test_commit_messages( 753 rh.hooks.check_commit_msg_relnote_for_current_txt, 754 True, 755 ( 756 'subj\n\nRelnote: This is a release note\n', 757 'subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234', 758 ('subj\n\nRelnote: This is release note 1 with\n' 759 'an incorrectly formatted second line.\n\n' 760 'Relnote: "This is release note 2, and it\n' 761 'contains a correctly formatted second line."\n' 762 'Bug: 1234'), 763 ), 764 files=diff_with_subdir_current_txt, 765 ) 766 # Check some good messages. 767 self._test_commit_messages( 768 rh.hooks.check_commit_msg_relnote_for_current_txt, 769 True, 770 ( 771 'subj', 772 'subj\nBug: 12345\nChange-Id: 1234', 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_without_current_txt, 782 ) 783 # Check some bad messages. 784 self._test_commit_messages( 785 rh.hooks.check_commit_msg_relnote_for_current_txt, 786 False, 787 ( 788 'subj' 789 'subj\nBug: 12345\nChange-Id: 1234', 790 ), 791 files=diff_with_current_txt, 792 ) 793 # Check some bad messages. 794 self._test_commit_messages( 795 rh.hooks.check_commit_msg_relnote_for_current_txt, 796 False, 797 ( 798 'subj' 799 'subj\nBug: 12345\nChange-Id: 1234', 800 ), 801 files=diff_with_experimental_current_txt, 802 ) 803 # Check some bad messages. 804 self._test_commit_messages( 805 rh.hooks.check_commit_msg_relnote_for_current_txt, 806 False, 807 ( 808 'subj' 809 'subj\nBug: 12345\nChange-Id: 1234', 810 ), 811 files=diff_with_subdir_current_txt, 812 ) 813 814 def test_cpplint(self, mock_check, _mock_run): 815 """Verify the cpplint builtin hook.""" 816 self._test_file_filter(mock_check, rh.hooks.check_cpplint, 817 ('foo.cpp', 'foo.cxx')) 818 819 def test_gofmt(self, mock_check, _mock_run): 820 """Verify the gofmt builtin hook.""" 821 # First call should do nothing as there are no files to check. 822 ret = rh.hooks.check_gofmt( 823 self.project, 'commit', 'desc', (), options=self.options) 824 self.assertIsNone(ret) 825 self.assertFalse(mock_check.called) 826 827 # Second call will have some results. 828 diff = [rh.git.RawDiffEntry(file='foo.go')] 829 ret = rh.hooks.check_gofmt( 830 self.project, 'commit', 'desc', diff, options=self.options) 831 self.assertIsNotNone(ret) 832 833 def test_jsonlint(self, mock_check, _mock_run): 834 """Verify the jsonlint builtin hook.""" 835 # First call should do nothing as there are no files to check. 836 ret = rh.hooks.check_json( 837 self.project, 'commit', 'desc', (), options=self.options) 838 self.assertIsNone(ret) 839 self.assertFalse(mock_check.called) 840 841 # TODO: Actually pass some valid/invalid json data down. 842 843 def test_ktfmt(self, mock_check, _mock_run): 844 """Verify the ktfmt builtin hook.""" 845 # First call should do nothing as there are no files to check. 846 ret = rh.hooks.check_ktfmt( 847 self.project, 'commit', 'desc', (), options=self.options) 848 self.assertIsNone(ret) 849 self.assertFalse(mock_check.called) 850 # Check that .kt files are included by default. 851 diff = [rh.git.RawDiffEntry(file='foo.kt'), 852 rh.git.RawDiffEntry(file='bar.java'), 853 rh.git.RawDiffEntry(file='baz/blah.kt')] 854 ret = rh.hooks.check_ktfmt( 855 self.project, 'commit', 'desc', diff, options=self.options) 856 self.assertListEqual(ret[0].files, ['foo.kt', 'baz/blah.kt']) 857 diff = [rh.git.RawDiffEntry(file='foo/f1.kt'), 858 rh.git.RawDiffEntry(file='bar/f2.kt'), 859 rh.git.RawDiffEntry(file='baz/f2.kt')] 860 ret = rh.hooks.check_ktfmt(self.project, 'commit', 'desc', diff, 861 options=rh.hooks.HookOptions('hook name', [ 862 '--include-dirs=foo,baz'], {})) 863 self.assertListEqual(ret[0].files, ['foo/f1.kt', 'baz/f2.kt']) 864 865 def test_pylint(self, mock_check, _mock_run): 866 """Verify the pylint builtin hook.""" 867 self._test_file_filter(mock_check, rh.hooks.check_pylint2, 868 ('foo.py',)) 869 870 def test_pylint2(self, mock_check, _mock_run): 871 """Verify the pylint2 builtin hook.""" 872 self._test_file_filter(mock_check, rh.hooks.check_pylint2, 873 ('foo.py',)) 874 875 def test_pylint3(self, mock_check, _mock_run): 876 """Verify the pylint3 builtin hook.""" 877 self._test_file_filter(mock_check, rh.hooks.check_pylint3, 878 ('foo.py',)) 879 880 def test_rustfmt(self, mock_check, _mock_run): 881 # First call should do nothing as there are no files to check. 882 ret = rh.hooks.check_rustfmt( 883 self.project, 'commit', 'desc', (), options=self.options) 884 self.assertEqual(ret, None) 885 self.assertFalse(mock_check.called) 886 887 # Second call will have some results. 888 diff = [rh.git.RawDiffEntry(file='lib.rs')] 889 ret = rh.hooks.check_rustfmt( 890 self.project, 'commit', 'desc', diff, options=self.options) 891 self.assertNotEqual(ret, None) 892 893 def test_xmllint(self, mock_check, _mock_run): 894 """Verify the xmllint builtin hook.""" 895 self._test_file_filter(mock_check, rh.hooks.check_xmllint, 896 ('foo.xml',)) 897 898 def test_android_test_mapping_format(self, mock_check, _mock_run): 899 """Verify the android_test_mapping_format builtin hook.""" 900 # First call should do nothing as there are no files to check. 901 ret = rh.hooks.check_android_test_mapping( 902 self.project, 'commit', 'desc', (), options=self.options) 903 self.assertIsNone(ret) 904 self.assertFalse(mock_check.called) 905 906 # Second call will have some results. 907 diff = [rh.git.RawDiffEntry(file='TEST_MAPPING')] 908 ret = rh.hooks.check_android_test_mapping( 909 self.project, 'commit', 'desc', diff, options=self.options) 910 self.assertIsNotNone(ret) 911 912 def test_aidl_format(self, mock_check, _mock_run): 913 """Verify the aidl_format builtin hook.""" 914 # First call should do nothing as there are no files to check. 915 ret = rh.hooks.check_aidl_format( 916 self.project, 'commit', 'desc', (), options=self.options) 917 self.assertIsNone(ret) 918 self.assertFalse(mock_check.called) 919 920 # Second call will have some results. 921 diff = [rh.git.RawDiffEntry(file='IFoo.go')] 922 ret = rh.hooks.check_gofmt( 923 self.project, 'commit', 'desc', diff, options=self.options) 924 self.assertIsNotNone(ret) 925 926 927if __name__ == '__main__': 928 unittest.main() 929