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