| #!/usr/bin/env python3 |
| # Copyright 2016 The Android Open Source Project |
| # |
| # Licensed under the Apache License, Version 2.0 (the "License"); |
| # you may not use this file except in compliance with the License. |
| # You may obtain a copy of the License at |
| # |
| # http://www.apache.org/licenses/LICENSE-2.0 |
| # |
| # Unless required by applicable law or agreed to in writing, software |
| # distributed under the License is distributed on an "AS IS" BASIS, |
| # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| # See the License for the specific language governing permissions and |
| # limitations under the License. |
| |
| """Unittests for the hooks module.""" |
| |
| import os |
| import sys |
| import unittest |
| from unittest import mock |
| |
| import pytest |
| |
| _path = os.path.realpath(__file__ + "/../..") |
| if sys.path[0] != _path: |
| sys.path.insert(0, _path) |
| del _path |
| |
| # We have to import our local modules after the sys.path tweak. We can't use |
| # relative imports because this is an executable program, not a module. |
| # pylint: disable=wrong-import-position |
| import rh |
| import rh.config |
| import rh.hooks |
| |
| |
| # pylint: disable=unused-argument |
| def mock_find_repo_root(path=None, outer=False): |
| return "/ ${BUILD_OS}" if outer else "/ ${BUILD_OS}/sub" |
| |
| |
| class HooksDocsTests(unittest.TestCase): |
| """Make sure all hook features are documented. |
| |
| Note: These tests are a bit hokey in that they parse README.md. But they |
| get the job done, so that's all that matters right? |
| """ |
| |
| def setUp(self): |
| self.readme = os.path.join( |
| os.path.dirname(os.path.dirname(os.path.realpath(__file__))), |
| "README.md", |
| ) |
| |
| def _grab_section(self, section): |
| """Extract the |section| text out of the readme.""" |
| ret = [] |
| in_section = False |
| with open(self.readme, encoding="utf-8") as fp: |
| for line in fp: |
| if not in_section: |
| # Look for the section like "## [Tool Paths]". |
| if ( |
| line.startswith("#") |
| and line.lstrip("#").strip() == section |
| ): |
| in_section = True |
| else: |
| # Once we hit the next section (higher or lower), break. |
| if line[0] == "#": |
| break |
| ret.append(line) |
| return "".join(ret) |
| |
| def testBuiltinHooks(self): |
| """Verify builtin hooks are documented.""" |
| data = self._grab_section("[Builtin Hooks]") |
| for hook in rh.hooks.BUILTIN_HOOKS: |
| self.assertIn( |
| f"* `{hook}`:", |
| data, |
| msg=f'README.md missing docs for hook "{hook}"', |
| ) |
| |
| def testToolPaths(self): |
| """Verify tools are documented.""" |
| data = self._grab_section("[Tool Paths]") |
| for tool in rh.hooks.TOOL_PATHS: |
| self.assertIn( |
| f"* `{tool}`:", |
| data, |
| msg=f'README.md missing docs for tool "{tool}"', |
| ) |
| |
| def testPlaceholders(self): |
| """Verify placeholder replacement vars are documented.""" |
| data = self._grab_section("Placeholders") |
| for var in rh.hooks.Placeholders.vars(): |
| self.assertIn( |
| "* `${" + var + "}`:", |
| data, |
| msg=f'README.md missing docs for var "{var}"', |
| ) |
| |
| |
| class PlaceholderTests(unittest.TestCase): |
| """Verify behavior of replacement variables.""" |
| |
| def setUp(self): |
| self._saved_environ = os.environ.copy() |
| os.environ.update( |
| { |
| "PREUPLOAD_COMMIT_MESSAGE": "commit message", |
| "PREUPLOAD_COMMIT": "5c4c293174bb61f0f39035a71acd9084abfa743d", |
| } |
| ) |
| self.replacer = rh.hooks.Placeholders( |
| [ |
| rh.git.RawDiffEntry(file=x) |
| for x in ["path1/file1", "path2/file2"] |
| ] |
| ) |
| |
| def tearDown(self): |
| os.environ.clear() |
| os.environ.update(self._saved_environ) |
| |
| def testVars(self): |
| """Light test for the vars inspection generator.""" |
| ret = list(self.replacer.vars()) |
| self.assertGreater(len(ret), 4) |
| self.assertIn("PREUPLOAD_COMMIT", ret) |
| |
| @mock.patch.object( |
| rh.git, "find_repo_root", side_effect=mock_find_repo_root |
| ) |
| def testExpandVars(self, _m): |
| """Verify the replacement actually works.""" |
| input_args = [ |
| # Verify ${REPO_ROOT} is updated, but not REPO_ROOT. |
| # We also make sure that things in ${REPO_ROOT} are not double |
| # expanded (which is why the return includes ${BUILD_OS}). |
| "${REPO_ROOT}/some/prog/REPO_ROOT/ok", |
| # Verify that ${REPO_OUTER_ROOT} is expanded. |
| "${REPO_OUTER_ROOT}/some/prog/REPO_OUTER_ROOT/ok", |
| # Verify lists are merged rather than inserted. |
| "${PREUPLOAD_FILES}", |
| # Verify each file is preceded with '--file=' prefix. |
| "--file=${PREUPLOAD_FILES_PREFIXED}", |
| # Verify each file is preceded with '--file' argument. |
| "--file", |
| "${PREUPLOAD_FILES_PREFIXED}", |
| # Verify values with whitespace don't expand into multiple args. |
| "${PREUPLOAD_COMMIT_MESSAGE}", |
| # Verify multiple values get replaced. |
| "${PREUPLOAD_COMMIT}^${PREUPLOAD_COMMIT_MESSAGE}", |
| # Unknown vars should be left alone. |
| "${THIS_VAR_IS_GOOD}", |
| ] |
| output_args = self.replacer.expand_vars(input_args) |
| exp_args = [ |
| "/ ${BUILD_OS}/sub/some/prog/REPO_ROOT/ok", |
| "/ ${BUILD_OS}/some/prog/REPO_OUTER_ROOT/ok", |
| "path1/file1", |
| "path2/file2", |
| "--file=path1/file1", |
| "--file=path2/file2", |
| "--file", |
| "path1/file1", |
| "--file", |
| "path2/file2", |
| "commit message", |
| "5c4c293174bb61f0f39035a71acd9084abfa743d^commit message", |
| "${THIS_VAR_IS_GOOD}", |
| ] |
| self.assertEqual(output_args, exp_args) |
| |
| def testTheTester(self): |
| """Make sure we have a test for every variable.""" |
| for var in self.replacer.vars(): |
| self.assertIn( |
| f"test{var}", |
| dir(self), |
| msg=f"Missing unittest for variable {var}", |
| ) |
| |
| def testPREUPLOAD_COMMIT_MESSAGE(self): |
| """Verify handling of PREUPLOAD_COMMIT_MESSAGE.""" |
| self.assertEqual( |
| self.replacer.get("PREUPLOAD_COMMIT_MESSAGE"), "commit message" |
| ) |
| |
| def testPREUPLOAD_COMMIT(self): |
| """Verify handling of PREUPLOAD_COMMIT.""" |
| self.assertEqual( |
| self.replacer.get("PREUPLOAD_COMMIT"), |
| "5c4c293174bb61f0f39035a71acd9084abfa743d", |
| ) |
| |
| def testPREUPLOAD_FILES(self): |
| """Verify handling of PREUPLOAD_FILES.""" |
| self.assertEqual( |
| self.replacer.get("PREUPLOAD_FILES"), ["path1/file1", "path2/file2"] |
| ) |
| |
| @mock.patch.object(rh.git, "find_repo_root") |
| def testREPO_OUTER_ROOT(self, m): |
| """Verify handling of REPO_OUTER_ROOT.""" |
| m.side_effect = mock_find_repo_root |
| self.assertEqual( |
| self.replacer.get("REPO_OUTER_ROOT"), |
| mock_find_repo_root(path=None, outer=True), |
| ) |
| |
| @mock.patch.object(rh.git, "find_repo_root") |
| def testREPO_ROOT(self, m): |
| """Verify handling of REPO_ROOT.""" |
| m.side_effect = mock_find_repo_root |
| self.assertEqual( |
| self.replacer.get("REPO_ROOT"), |
| mock_find_repo_root(path=None, outer=False), |
| ) |
| |
| def testREPO_PATH(self): |
| """Verify handling of REPO_PATH.""" |
| os.environ["REPO_PATH"] = "" |
| self.assertEqual(self.replacer.get("REPO_PATH"), "") |
| os.environ["REPO_PATH"] = "foo/bar" |
| self.assertEqual(self.replacer.get("REPO_PATH"), "foo/bar") |
| |
| def testREPO_PROJECT(self): |
| """Verify handling of REPO_PROJECT.""" |
| os.environ["REPO_PROJECT"] = "" |
| self.assertEqual(self.replacer.get("REPO_PROJECT"), "") |
| os.environ["REPO_PROJECT"] = "platform/foo/bar" |
| self.assertEqual(self.replacer.get("REPO_PROJECT"), "platform/foo/bar") |
| |
| @mock.patch.object(rh.hooks, "_get_build_os_name", return_value="vapier os") |
| def testBUILD_OS(self, m): |
| """Verify handling of BUILD_OS.""" |
| self.assertEqual(self.replacer.get("BUILD_OS"), m.return_value) |
| |
| |
| class ExclusionScopeTests(unittest.TestCase): |
| """Verify behavior of ExclusionScope class.""" |
| |
| def testEmpty(self): |
| """Verify the in operator for an empty scope.""" |
| scope = rh.hooks.ExclusionScope([]) |
| self.assertNotIn("external/*", scope) |
| |
| def testGlob(self): |
| """Verify the in operator for a scope using wildcards.""" |
| scope = rh.hooks.ExclusionScope(["vendor/*", "external/*"]) |
| self.assertIn("external/tools", scope) |
| |
| def testRegex(self): |
| """Verify the in operator for a scope using regular expressions.""" |
| scope = rh.hooks.ExclusionScope(["^vendor/(?!google)", "external/*"]) |
| self.assertIn("vendor/", scope) |
| self.assertNotIn("vendor/google/", scope) |
| self.assertIn("vendor/other/", scope) |
| |
| |
| class HookOptionsTests(unittest.TestCase): |
| """Verify behavior of HookOptions object.""" |
| |
| @pytest.mark.skip_cq("TODO: Relies on .repo dir") |
| @mock.patch.object(rh.hooks, "_get_build_os_name", return_value="vapier os") |
| def testExpandVars(self, m): |
| """Verify expand_vars behavior.""" |
| # Simple pass through. |
| args = ["who", "goes", "there ?"] |
| self.assertEqual(args, rh.hooks.HookOptions.expand_vars(args)) |
| |
| # At least one replacement. Most real testing is in PlaceholderTests. |
| args = ["who", "goes", "there ?", "${BUILD_OS} is great"] |
| exp_args = ["who", "goes", "there ?", f"{m.return_value} is great"] |
| self.assertEqual(exp_args, rh.hooks.HookOptions.expand_vars(args)) |
| |
| @pytest.mark.skip_cq("TODO: Relies on .repo dir") |
| def testArgs(self): |
| """Verify args behavior.""" |
| # Verify initial args to __init__ has higher precedent. |
| args = ["start", "args"] |
| options = rh.hooks.HookOptions("hook name", args, {}) |
| self.assertEqual(options.args(), args) |
| self.assertEqual(options.args(default_args=["moo"]), args) |
| |
| # Verify we fall back to default_args. |
| args = ["default", "args"] |
| options = rh.hooks.HookOptions("hook name", [], {}) |
| self.assertEqual(options.args(), []) |
| self.assertEqual(options.args(default_args=args), args) |
| |
| @pytest.mark.skip_cq("TODO: Relies on .repo dir") |
| def testToolPath(self): |
| """Verify tool_path behavior.""" |
| options = rh.hooks.HookOptions( |
| "hook name", |
| [], |
| { |
| "cpplint": "my cpplint", |
| }, |
| ) |
| # Check a builtin (and not overridden) tool. |
| self.assertEqual(options.tool_path("pylint"), "pylint") |
| # Check an overridden tool. |
| self.assertEqual(options.tool_path("cpplint"), "my cpplint") |
| # Check an unknown tool fails. |
| self.assertRaises(AssertionError, options.tool_path, "extra_tool") |
| |
| |
| class UtilsTests(unittest.TestCase): |
| """Verify misc utility functions.""" |
| |
| def testRunCommand(self): |
| """Check _run behavior.""" |
| # Most testing is done against the utils.RunCommand already. |
| # pylint: disable=protected-access |
| ret = rh.hooks._run(["true"]) |
| self.assertEqual(ret.returncode, 0) |
| |
| def testBuildOs(self): |
| """Check _get_build_os_name behavior.""" |
| # Just verify it returns something and doesn't crash. |
| # pylint: disable=protected-access |
| ret = rh.hooks._get_build_os_name() |
| self.assertTrue(isinstance(ret, str)) |
| self.assertNotEqual(ret, "") |
| |
| def testGetHelperPath(self): |
| """Check get_helper_path behavior.""" |
| # Just verify it doesn't crash. It's a dirt simple func. |
| ret = rh.hooks.get_helper_path("booga") |
| self.assertTrue(isinstance(ret, str)) |
| self.assertNotEqual(ret, "") |
| |
| def testSortedToolPaths(self): |
| """Check TOOL_PATHS is sorted.""" |
| # This assumes dictionary key ordering matches insertion/definition |
| # order which Python 3.7+ has codified. |
| # https://docs.python.org/3.7/library/stdtypes.html#dict |
| self.assertEqual(list(rh.hooks.TOOL_PATHS), sorted(rh.hooks.TOOL_PATHS)) |
| |
| def testSortedBuiltinHooks(self): |
| """Check BUILTIN_HOOKS is sorted.""" |
| # This assumes dictionary key ordering matches insertion/definition |
| # order which Python 3.7+ has codified. |
| # https://docs.python.org/3.7/library/stdtypes.html#dict |
| self.assertEqual( |
| list(rh.hooks.BUILTIN_HOOKS), sorted(rh.hooks.BUILTIN_HOOKS) |
| ) |
| |
| |
| @mock.patch.object(rh.utils, "run") |
| @mock.patch.object(rh.hooks, "_check_cmd", return_value=["check_cmd"]) |
| class BuiltinHooksTests(unittest.TestCase): |
| """Verify the builtin hooks.""" |
| |
| def setUp(self): |
| self.project = rh.Project(name="project-name", dir="/.../repo/dir") |
| self.options = rh.hooks.HookOptions("hook name", [], {}) |
| |
| def _test_commit_messages(self, func, accept, msgs, files=None): |
| """Helper for testing commit message hooks. |
| |
| Args: |
| func: The hook function to test. |
| accept: Whether all the |msgs| should be accepted. |
| msgs: List of messages to test. |
| files: List of files to pass to the hook. |
| """ |
| if files: |
| diff = [rh.git.RawDiffEntry(file=x) for x in files] |
| else: |
| diff = [] |
| for desc in msgs: |
| ret = func(self.project, "commit", desc, diff, options=self.options) |
| if accept: |
| self.assertFalse( |
| bool(ret), msg="Should have accepted: {{{" + desc + "}}}" |
| ) |
| else: |
| self.assertTrue( |
| bool(ret), msg="Should have rejected: {{{" + desc + "}}}" |
| ) |
| |
| def _test_file_filter(self, mock_check, func, files): |
| """Helper for testing hooks that filter by files and run external tools. |
| |
| Args: |
| mock_check: The mock of _check_cmd. |
| func: The hook function to test. |
| files: A list of files that we'd check. |
| """ |
| # First call should do nothing as there are no files to check. |
| ret = func(self.project, "commit", "desc", (), options=self.options) |
| self.assertIsNone(ret) |
| self.assertFalse(mock_check.called) |
| |
| # Second call should include some checks. |
| diff = [rh.git.RawDiffEntry(file=x) for x in files] |
| ret = func(self.project, "commit", "desc", diff, options=self.options) |
| self.assertEqual(ret, mock_check.return_value) |
| |
| def testTheTester(self, _mock_check, _mock_run): |
| """Make sure we have a test for every hook.""" |
| for hook in rh.hooks.BUILTIN_HOOKS: |
| self.assertIn( |
| f"test_{hook}", |
| dir(self), |
| msg=f"Missing unittest for builtin hook {hook}", |
| ) |
| |
| def test_aosp_license(self, mock_check, _mock_run): |
| """Verify the aosp_license builtin hook.""" |
| # First call should do nothing as there are no files to check. |
| diff = [ |
| rh.git.RawDiffEntry(file="d.bp", status="D"), |
| rh.git.RawDiffEntry(file="m.bp", status="M"), |
| rh.git.RawDiffEntry(file="non-interested", status="A"), |
| ] |
| ret = rh.hooks.check_aosp_license( |
| self.project, "commit", "desc", diff, options=self.options |
| ) |
| self.assertIsNone(ret) |
| self.assertFalse(mock_check.called) |
| |
| # Second call will have some results. |
| diff = [ |
| rh.git.RawDiffEntry(file="a.bp", status="A"), |
| ] |
| ret = rh.hooks.check_aosp_license( |
| self.project, "commit", "desc", diff, options=self.options |
| ) |
| self.assertIsNotNone(ret) |
| |
| # No result since all paths are excluded. |
| diff = [ |
| rh.git.RawDiffEntry(file="a/a.bp", status="A"), |
| rh.git.RawDiffEntry(file="b/a.bp", status="A"), |
| rh.git.RawDiffEntry(file="c/d/a.bp", status="A"), |
| ] |
| ret = rh.hooks.check_aosp_license( |
| self.project, |
| "commit", |
| "desc", |
| diff, |
| options=rh.hooks.HookOptions( |
| "hook name", ["--exclude-dirs=a,b", "--exclude-dirs=c/d"], {} |
| ), |
| ) |
| self.assertIsNone(ret) |
| |
| # Make sure that `--exclude-dir` doesn't match the path in the middle. |
| diff = [ |
| rh.git.RawDiffEntry(file="a/b/c.bp", status="A"), |
| ] |
| ret = rh.hooks.check_aosp_license( |
| self.project, |
| "commit", |
| "desc", |
| diff, |
| options=rh.hooks.HookOptions("hook name", ["--exclude-dirs=b"], {}), |
| ) |
| self.assertIsNotNone(ret) |
| |
| def test_black(self, mock_check, _mock_run): |
| """Verify the black builtin hook.""" |
| # First call should do nothing as there are no files to check. |
| ret = rh.hooks.check_black( |
| self.project, "commit", "desc", (), options=self.options |
| ) |
| self.assertIsNone(ret) |
| self.assertFalse(mock_check.called) |
| |
| # Second call will have some results. |
| diff = [rh.git.RawDiffEntry(file="main.py")] |
| ret = rh.hooks.check_black( |
| self.project, "commit", "desc", diff, options=self.options |
| ) |
| self.assertIsNotNone(ret) |
| for result in ret: |
| self.assertIsNotNone(result.fixup_cmd) |
| |
| def test_bpfmt(self, mock_check, _mock_run): |
| """Verify the bpfmt builtin hook.""" |
| # First call should do nothing as there are no files to check. |
| ret = rh.hooks.check_bpfmt( |
| self.project, "commit", "desc", (), options=self.options |
| ) |
| self.assertIsNone(ret) |
| self.assertFalse(mock_check.called) |
| |
| # Second call will have some results. |
| diff = [rh.git.RawDiffEntry(file="Android.bp")] |
| ret = rh.hooks.check_bpfmt( |
| self.project, "commit", "desc", diff, options=self.options |
| ) |
| self.assertIsNotNone(ret) |
| for result in ret: |
| self.assertIsNotNone(result.fixup_cmd) |
| |
| def test_checkpatch(self, mock_check, _mock_run): |
| """Verify the checkpatch builtin hook.""" |
| ret = rh.hooks.check_checkpatch( |
| self.project, "commit", "desc", (), options=self.options |
| ) |
| self.assertEqual(ret, mock_check.return_value) |
| |
| def test_clang_format(self, mock_check, _mock_run): |
| """Verify the clang_format builtin hook.""" |
| ret = rh.hooks.check_clang_format( |
| self.project, "commit", "desc", (), options=self.options |
| ) |
| self.assertEqual(ret, mock_check.return_value) |
| |
| def test_google_java_format(self, mock_check, _mock_run): |
| """Verify the google_java_format builtin hook.""" |
| # First call should do nothing as there are no files to check. |
| ret = rh.hooks.check_google_java_format( |
| self.project, "commit", "desc", (), options=self.options |
| ) |
| self.assertIsNone(ret) |
| self.assertFalse(mock_check.called) |
| # Check that .java files are included by default. |
| diff = [ |
| rh.git.RawDiffEntry(file="foo.java"), |
| rh.git.RawDiffEntry(file="bar.kt"), |
| rh.git.RawDiffEntry(file="baz/blah.java"), |
| ] |
| ret = rh.hooks.check_google_java_format( |
| self.project, "commit", "desc", diff, options=self.options |
| ) |
| self.assertListEqual(ret[0].files, ["foo.java", "baz/blah.java"]) |
| diff = [ |
| rh.git.RawDiffEntry(file="foo/f1.java"), |
| rh.git.RawDiffEntry(file="bar/f2.java"), |
| rh.git.RawDiffEntry(file="baz/f2.java"), |
| ] |
| ret = rh.hooks.check_google_java_format( |
| self.project, |
| "commit", |
| "desc", |
| diff, |
| options=rh.hooks.HookOptions( |
| "hook name", ["--include-dirs=foo,baz"], {} |
| ), |
| ) |
| self.assertListEqual(ret[0].files, ["foo/f1.java", "baz/f2.java"]) |
| |
| def test_commit_msg_bug_field(self, _mock_check, _mock_run): |
| """Verify the commit_msg_bug_field builtin hook.""" |
| # Check some good messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_bug_field, |
| True, |
| ( |
| "subj\n\nBug: 1234\n", |
| "subj\n\nBug: 1234\nChange-Id: blah\n", |
| "subj\n\nFix: 1234\n", |
| ), |
| ) |
| |
| # Check some bad messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_bug_field, |
| False, |
| ( |
| "subj", |
| "subj\n\nBUG=1234\n", |
| "subj\n\nBUG: 1234\n", |
| "subj\n\nBug: N/A\n", |
| "subj\n\nBug:\n", |
| "subj\n\nFIX=1234\n", |
| ), |
| ) |
| |
| def test_commit_msg_changeid_field(self, _mock_check, _mock_run): |
| """Verify the commit_msg_changeid_field builtin hook.""" |
| # Check some good messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_changeid_field, |
| True, |
| ("subj\n\nChange-Id: I1234\n",), |
| ) |
| |
| # Check some bad messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_changeid_field, |
| False, |
| ( |
| "subj", |
| "subj\n\nChange-Id: 1234\n", |
| "subj\n\nChange-ID: I1234\n", |
| ), |
| ) |
| |
| def test_commit_msg_prebuilt_apk_fields(self, _mock_check, _mock_run): |
| """Verify the check_commit_msg_prebuilt_apk_fields builtin hook.""" |
| # Commits without APKs should pass. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_prebuilt_apk_fields, |
| True, |
| ("subj\nTest: test case\nBug: bug id\n",), |
| [ |
| "foo.cpp", |
| "bar.py", |
| ], |
| ) |
| |
| # Commits with APKs and all the required messages should pass. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_prebuilt_apk_fields, |
| True, |
| ( |
| ( |
| "Test App\n\nbar.apk\npackage: name='com.foo.bar'\n" |
| "versionCode='1001'\nversionName='1.0.1001-A'\n" |
| "platformBuildVersionName=''\ncompileSdkVersion='28'\n" |
| "compileSdkVersionCodename='9'\nsdkVersion:'16'\n" |
| "targetSdkVersion:'28'\n\nBuilt here:\n" |
| "http://foo.bar.com/builder\n\n" |
| "This build IS suitable for public release.\n\n" |
| "Bug: 123\nTest: test\nChange-Id: XXXXXXX\n" |
| ), |
| ( |
| "Test App\n\nBuilt here:\nhttp://foo.bar.com/builder\n\n" |
| "This build IS NOT suitable for public release.\n\n" |
| "bar.apk\npackage: name='com.foo.bar'\n" |
| "versionCode='1001'\nversionName='1.0.1001-A'\n" |
| "platformBuildVersionName=''\ncompileSdkVersion='28'\n" |
| "compileSdkVersionCodename='9'\nsdkVersion:'16'\n" |
| "targetSdkVersion:'28'\n\nBug: 123\nTest: test\n" |
| "Change-Id: XXXXXXX\n" |
| ), |
| ( |
| "Test App\n\nbar.apk\npackage: name='com.foo.bar'\n" |
| "versionCode='1001'\nversionName='1.0.1001-A'\n" |
| "platformBuildVersionName=''\ncompileSdkVersion='28'\n" |
| "compileSdkVersionCodename='9'\nsdkVersion:'16'\n" |
| "targetSdkVersion:'28'\n\nBuilt here:\n" |
| "http://foo.bar.com/builder\n\n" |
| "This build IS suitable for preview release but IS NOT " |
| "suitable for public release.\n\n" |
| "Bug: 123\nTest: test\nChange-Id: XXXXXXX\n" |
| ), |
| ( |
| "Test App\n\nbar.apk\npackage: name='com.foo.bar'\n" |
| "versionCode='1001'\nversionName='1.0.1001-A'\n" |
| "platformBuildVersionName=''\ncompileSdkVersion='28'\n" |
| "compileSdkVersionCodename='9'\nsdkVersion:'16'\n" |
| "targetSdkVersion:'28'\n\nBuilt here:\n" |
| "http://foo.bar.com/builder\n\n" |
| "This build IS NOT suitable for preview or public " |
| "release.\n\n" |
| "Bug: 123\nTest: test\nChange-Id: XXXXXXX\n" |
| ), |
| ), |
| [ |
| "foo.apk", |
| "bar.py", |
| ], |
| ) |
| |
| # Commits with APKs and without all the required messages should fail. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_prebuilt_apk_fields, |
| False, |
| ( |
| "subj\nTest: test case\nBug: bug id\n", |
| # Missing 'package'. |
| ( |
| "Test App\n\nbar.apk\n" |
| "versionCode='1001'\nversionName='1.0.1001-A'\n" |
| "platformBuildVersionName=''\ncompileSdkVersion='28'\n" |
| "compileSdkVersionCodename='9'\nsdkVersion:'16'\n" |
| "targetSdkVersion:'28'\n\nBuilt here:\n" |
| "http://foo.bar.com/builder\n\n" |
| "This build IS suitable for public release.\n\n" |
| "Bug: 123\nTest: test\nChange-Id: XXXXXXX\n" |
| ), |
| # Missing 'sdkVersion'. |
| ( |
| "Test App\n\nbar.apk\npackage: name='com.foo.bar'\n" |
| "versionCode='1001'\nversionName='1.0.1001-A'\n" |
| "platformBuildVersionName=''\ncompileSdkVersion='28'\n" |
| "compileSdkVersionCodename='9'\n" |
| "targetSdkVersion:'28'\n\nBuilt here:\n" |
| "http://foo.bar.com/builder\n\n" |
| "This build IS suitable for public release.\n\n" |
| "Bug: 123\nTest: test\nChange-Id: XXXXXXX\n" |
| ), |
| # Missing 'targetSdkVersion'. |
| ( |
| "Test App\n\nbar.apk\npackage: name='com.foo.bar'\n" |
| "versionCode='1001'\nversionName='1.0.1001-A'\n" |
| "platformBuildVersionName=''\ncompileSdkVersion='28'\n" |
| "compileSdkVersionCodename='9'\nsdkVersion:'16'\n" |
| "Built here:\nhttp://foo.bar.com/builder\n\n" |
| "This build IS suitable for public release.\n\n" |
| "Bug: 123\nTest: test\nChange-Id: XXXXXXX\n" |
| ), |
| # Missing build location. |
| ( |
| "Test App\n\nbar.apk\npackage: name='com.foo.bar'\n" |
| "versionCode='1001'\nversionName='1.0.1001-A'\n" |
| "platformBuildVersionName=''\ncompileSdkVersion='28'\n" |
| "compileSdkVersionCodename='9'\nsdkVersion:'16'\n" |
| "targetSdkVersion:'28'\n\n" |
| "This build IS suitable for public release.\n\n" |
| "Bug: 123\nTest: test\nChange-Id: XXXXXXX\n" |
| ), |
| # Missing public release indication. |
| ( |
| "Test App\n\nbar.apk\npackage: name='com.foo.bar'\n" |
| "versionCode='1001'\nversionName='1.0.1001-A'\n" |
| "platformBuildVersionName=''\ncompileSdkVersion='28'\n" |
| "compileSdkVersionCodename='9'\nsdkVersion:'16'\n" |
| "targetSdkVersion:'28'\n\nBuilt here:\n" |
| "http://foo.bar.com/builder\n\n" |
| "Bug: 123\nTest: test\nChange-Id: XXXXXXX\n" |
| ), |
| ), |
| [ |
| "foo.apk", |
| "bar.py", |
| ], |
| ) |
| |
| def test_commit_msg_test_field(self, _mock_check, _mock_run): |
| """Verify the commit_msg_test_field builtin hook.""" |
| # Check some good messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_test_field, |
| True, |
| ("subj\n\nTest: i did done dood it\n",), |
| ) |
| |
| # Check some bad messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_test_field, |
| False, |
| ( |
| "subj", |
| "subj\n\nTEST=1234\n", |
| "subj\n\nTEST: I1234\n", |
| ), |
| ) |
| |
| def test_commit_msg_relnote_field_format(self, _mock_check, _mock_run): |
| """Verify the commit_msg_relnote_field_format builtin hook.""" |
| # Check some good messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_relnote_field_format, |
| True, |
| ( |
| "subj", |
| "subj\n\nTest: i did done dood it\nBug: 1234", |
| "subj\n\nMore content\n\nTest: i did done dood it\nBug: 1234", |
| "subj\n\nRelnote: This is a release note\nBug: 1234", |
| "subj\n\nRelnote:This is a release note\nBug: 1234", |
| "subj\n\nRelnote: This is a release note.\nBug: 1234", |
| 'subj\n\nRelnote: "This is a release note."\nBug: 1234', |
| 'subj\n\nRelnote: "This is a \\"release note\\"."\n\nBug: 1234', |
| "subj\n\nRelnote: This is a release note.\nChange-Id: 1234", |
| "subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234", |
| ( |
| 'subj\n\nRelnote: "This is a release note."\n\n' |
| "Change-Id: 1234" |
| ), |
| ( |
| "subj\n\nRelnote: This is a release note.\n\n" |
| "It has more info, but it is not part of the release note" |
| "\nChange-Id: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: "This is a release note.\n' |
| 'It contains a correct second line."' |
| ), |
| ( |
| 'subj\n\nRelnote:"This is a release note.\n' |
| 'It contains a correct second line."' |
| ), |
| ( |
| 'subj\n\nRelnote: "This is a release note.\n' |
| "It contains a correct second line.\n" |
| 'And even a third line."\n' |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: "This is a release note.\n' |
| "It contains a correct second line.\n" |
| '\\"Quotes\\" are even used on the third line."\n' |
| "Bug: 1234" |
| ), |
| ( |
| "subj\n\nRelnote: This is release note 1.\n" |
| "Relnote: This is release note 2.\n" |
| "Bug: 1234" |
| ), |
| ( |
| "subj\n\nRelnote: This is release note 1.\n" |
| 'Relnote: "This is release note 2, and it\n' |
| 'contains a correctly formatted third line."\n' |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: "This is release note 1 with\n' |
| 'a correctly formatted second line."\n\n' |
| 'Relnote: "This is release note 2, and it\n' |
| 'contains a correctly formatted second line."\n' |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: "This is a release note with\n' |
| 'a correctly formatted second line."\n\n' |
| "Bug: 1234" |
| 'Here is some extra "quoted" content.' |
| ), |
| ( |
| 'subj\n\nRelnote: """This is a release note.\n\n' |
| "This relnote contains an empty line.\n" |
| "Then a non-empty line.\n\n" |
| 'And another empty line."""\n\n' |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: """This is a release note.\n\n' |
| "This relnote contains an empty line.\n" |
| 'Then an acceptable "quoted" line.\n\n' |
| 'And another empty line."""\n\n' |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: """This is a release note."""\n\n' |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: """This is a release note.\n' |
| 'It has a second line."""\n\n' |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: """This is a release note.\n' |
| "It has a second line, but does not end here.\n" |
| '"""\n\n' |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: """This is a release note.\n' |
| '"It" has a second line, but does not end here.\n' |
| '"""\n\n' |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: "This is a release note.\n' |
| "It has a second line, but does not end here.\n" |
| '"\n\n' |
| "Bug: 1234" |
| ), |
| ), |
| ) |
| |
| # Check some bad messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_relnote_field_format, |
| False, |
| ( |
| "subj\n\nReleaseNote: This is a release note.\n", |
| "subj\n\nRelnotes: This is a release note.\n", |
| "subj\n\nRel-note: This is a release note.\n", |
| "subj\n\nrelnoTes: This is a release note.\n", |
| "subj\n\nrel-Note: This is a release note.\n", |
| 'subj\n\nRelnote: "This is a "release note"."\nBug: 1234', |
| 'subj\n\nRelnote: This is a "release note".\nBug: 1234', |
| ( |
| "subj\n\nRelnote: This is a release note.\n" |
| "It contains an incorrect second line." |
| ), |
| ( |
| 'subj\n\nRelnote: "This is a release note.\n' |
| "It contains multiple lines.\n" |
| "But it does not provide an ending quote.\n" |
| ), |
| ( |
| 'subj\n\nRelnote: "This is a release note.\n' |
| "It contains multiple lines but no closing quote.\n" |
| 'Test: my test "hello world"\n' |
| ), |
| ( |
| "subj\n\nRelnote: This is release note 1.\n" |
| 'Relnote: "This is release note 2, and it\n' |
| "contains an incorrectly formatted third line.\n" |
| "Bug: 1234" |
| ), |
| ( |
| "subj\n\nRelnote: This is release note 1 with\n" |
| "an incorrectly formatted second line.\n\n" |
| 'Relnote: "This is release note 2, and it\n' |
| 'contains a correctly formatted second line."\n' |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: "This is release note 1 with\n' |
| 'a correctly formatted second line."\n\n' |
| "Relnote: This is release note 2, and it\n" |
| "contains an incorrectly formatted second line.\n" |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: "This is a release note.\n' |
| "It contains a correct second line.\n" |
| 'But incorrect "quotes" on the third line."\n' |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: """This is a release note.\n' |
| "It has a second line, but no closing triple quote.\n\n" |
| "Bug: 1234" |
| ), |
| ( |
| 'subj\n\nRelnote: "This is a release note.\n' |
| '"It" has a second line, but does not end here.\n' |
| '"\n\n' |
| "Bug: 1234" |
| ), |
| ), |
| ) |
| |
| def test_commit_msg_relnote_for_current_txt(self, _mock_check, _mock_run): |
| """Verify the commit_msg_relnote_for_current_txt builtin hook.""" |
| diff_without_current_txt = [ |
| "bar/foo.txt", |
| "foo.cpp", |
| "foo.java", |
| "foo_current.java", |
| "foo_current.txt", |
| "baz/current.java", |
| "baz/foo_current.txt", |
| ] |
| diff_with_current_txt = diff_without_current_txt + ["current.txt"] |
| diff_with_subdir_current_txt = diff_without_current_txt + [ |
| "foo/current.txt" |
| ] |
| diff_with_experimental_current_txt = diff_without_current_txt + [ |
| "public_plus_experimental_current.txt" |
| ] |
| # Check some good messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_relnote_for_current_txt, |
| True, |
| ( |
| "subj\n\nRelnote: This is a release note\n", |
| "subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234", |
| ( |
| "subj\n\nRelnote: This is release note 1 with\n" |
| "an incorrectly formatted second line.\n\n" |
| 'Relnote: "This is release note 2, and it\n' |
| 'contains a correctly formatted second line."\n' |
| "Bug: 1234" |
| ), |
| ), |
| files=diff_with_current_txt, |
| ) |
| # Check some good messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_relnote_for_current_txt, |
| True, |
| ( |
| "subj\n\nRelnote: This is a release note\n", |
| "subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234", |
| ( |
| "subj\n\nRelnote: This is release note 1 with\n" |
| "an incorrectly formatted second line.\n\n" |
| 'Relnote: "This is release note 2, and it\n' |
| 'contains a correctly formatted second line."\n' |
| "Bug: 1234" |
| ), |
| ), |
| files=diff_with_experimental_current_txt, |
| ) |
| # Check some good messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_relnote_for_current_txt, |
| True, |
| ( |
| "subj\n\nRelnote: This is a release note\n", |
| "subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234", |
| ( |
| "subj\n\nRelnote: This is release note 1 with\n" |
| "an incorrectly formatted second line.\n\n" |
| 'Relnote: "This is release note 2, and it\n' |
| 'contains a correctly formatted second line."\n' |
| "Bug: 1234" |
| ), |
| ), |
| files=diff_with_subdir_current_txt, |
| ) |
| # Check some good messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_relnote_for_current_txt, |
| True, |
| ( |
| "subj", |
| "subj\nBug: 12345\nChange-Id: 1234", |
| "subj\n\nRelnote: This is a release note\n", |
| "subj\n\nRelnote: This is a release note.\n\nChange-Id: 1234", |
| ( |
| "subj\n\nRelnote: This is release note 1 with\n" |
| "an incorrectly formatted second line.\n\n" |
| 'Relnote: "This is release note 2, and it\n' |
| 'contains a correctly formatted second line."\n' |
| "Bug: 1234" |
| ), |
| ), |
| files=diff_without_current_txt, |
| ) |
| # Check some bad messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_relnote_for_current_txt, |
| False, |
| ("subjsubj\nBug: 12345\nChange-Id: 1234",), |
| files=diff_with_current_txt, |
| ) |
| # Check some bad messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_relnote_for_current_txt, |
| False, |
| ("subjsubj\nBug: 12345\nChange-Id: 1234",), |
| files=diff_with_experimental_current_txt, |
| ) |
| # Check some bad messages. |
| self._test_commit_messages( |
| rh.hooks.check_commit_msg_relnote_for_current_txt, |
| False, |
| ("subjsubj\nBug: 12345\nChange-Id: 1234",), |
| files=diff_with_subdir_current_txt, |
| ) |
| |
| def test_cpplint(self, mock_check, _mock_run): |
| """Verify the cpplint builtin hook.""" |
| self._test_file_filter( |
| mock_check, rh.hooks.check_cpplint, ("foo.cpp", "foo.cxx") |
| ) |
| |
| def test_gofmt(self, mock_check, _mock_run): |
| """Verify the gofmt builtin hook.""" |
| # First call should do nothing as there are no files to check. |
| ret = rh.hooks.check_gofmt( |
| self.project, "commit", "desc", (), options=self.options |
| ) |
| self.assertIsNone(ret) |
| self.assertFalse(mock_check.called) |
| |
| # Second call will have some results. |
| diff = [rh.git.RawDiffEntry(file="foo.go")] |
| ret = rh.hooks.check_gofmt( |
| self.project, "commit", "desc", diff, options=self.options |
| ) |
| self.assertIsNotNone(ret) |
| |
| def test_jsonlint(self, mock_check, _mock_run): |
| """Verify the jsonlint builtin hook.""" |
| # First call should do nothing as there are no files to check. |
| ret = rh.hooks.check_json( |
| self.project, "commit", "desc", (), options=self.options |
| ) |
| self.assertIsNone(ret) |
| self.assertFalse(mock_check.called) |
| |
| # TODO: Actually pass some valid/invalid json data down. |
| |
| def test_ktfmt(self, mock_check, _mock_run): |
| """Verify the ktfmt builtin hook.""" |
| # First call should do nothing as there are no files to check. |
| ret = rh.hooks.check_ktfmt( |
| self.project, "commit", "desc", (), options=self.options |
| ) |
| self.assertIsNone(ret) |
| self.assertFalse(mock_check.called) |
| # Check that .kt files are included by default. |
| diff = [ |
| rh.git.RawDiffEntry(file="foo.kt"), |
| rh.git.RawDiffEntry(file="bar.java"), |
| rh.git.RawDiffEntry(file="baz/blah.kt"), |
| ] |
| ret = rh.hooks.check_ktfmt( |
| self.project, "commit", "desc", diff, options=self.options |
| ) |
| self.assertListEqual(ret[0].files, ["foo.kt", "baz/blah.kt"]) |
| diff = [ |
| rh.git.RawDiffEntry(file="foo/f1.kt"), |
| rh.git.RawDiffEntry(file="bar/f2.kt"), |
| rh.git.RawDiffEntry(file="baz/f2.kt"), |
| ] |
| ret = rh.hooks.check_ktfmt( |
| self.project, |
| "commit", |
| "desc", |
| diff, |
| options=rh.hooks.HookOptions( |
| "hook name", ["--include-dirs=foo,baz"], {} |
| ), |
| ) |
| self.assertListEqual(ret[0].files, ["foo/f1.kt", "baz/f2.kt"]) |
| |
| def test_pylint(self, mock_check, _mock_run): |
| """Verify the pylint builtin hook.""" |
| self._test_file_filter(mock_check, rh.hooks.check_pylint3, ("foo.py",)) |
| |
| def test_pylint2(self, mock_check, _mock_run): |
| """Verify the pylint2 builtin hook.""" |
| ret = rh.hooks.check_pylint2( |
| self.project, "commit", "desc", (), options=self.options |
| ) |
| self.assertEqual(len(ret), 1) |
| self.assertTrue(ret[0].is_warning()) |
| |
| def test_pylint3(self, mock_check, _mock_run): |
| """Verify the pylint3 builtin hook.""" |
| self._test_file_filter(mock_check, rh.hooks.check_pylint3, ("foo.py",)) |
| |
| def test_rustfmt(self, mock_check, _mock_run): |
| # First call should do nothing as there are no files to check. |
| ret = rh.hooks.check_rustfmt( |
| self.project, "commit", "desc", (), options=self.options |
| ) |
| self.assertEqual(ret, None) |
| self.assertFalse(mock_check.called) |
| |
| # Second call will have some results. |
| diff = [rh.git.RawDiffEntry(file="lib.rs")] |
| ret = rh.hooks.check_rustfmt( |
| self.project, "commit", "desc", diff, options=self.options |
| ) |
| self.assertNotEqual(ret, None) |
| |
| def test_xmllint(self, mock_check, _mock_run): |
| """Verify the xmllint builtin hook.""" |
| self._test_file_filter(mock_check, rh.hooks.check_xmllint, ("foo.xml",)) |
| |
| def test_android_test_mapping_format(self, mock_check, _mock_run): |
| """Verify the android_test_mapping_format builtin hook.""" |
| # First call should do nothing as there are no files to check. |
| ret = rh.hooks.check_android_test_mapping( |
| self.project, "commit", "desc", (), options=self.options |
| ) |
| self.assertIsNone(ret) |
| self.assertFalse(mock_check.called) |
| |
| # Second call will have some results. |
| diff = [rh.git.RawDiffEntry(file="TEST_MAPPING")] |
| ret = rh.hooks.check_android_test_mapping( |
| self.project, "commit", "desc", diff, options=self.options |
| ) |
| self.assertIsNotNone(ret) |
| |
| def test_aidl_format(self, mock_check, _mock_run): |
| """Verify the aidl_format builtin hook.""" |
| # First call should do nothing as there are no files to check. |
| ret = rh.hooks.check_aidl_format( |
| self.project, "commit", "desc", (), options=self.options |
| ) |
| self.assertIsNone(ret) |
| self.assertFalse(mock_check.called) |
| |
| # Second call will have some results. |
| diff = [rh.git.RawDiffEntry(file="IFoo.go")] |
| ret = rh.hooks.check_gofmt( |
| self.project, "commit", "desc", diff, options=self.options |
| ) |
| self.assertIsNotNone(ret) |
| |
| def test_alint(self, mock_check, mock_run): |
| """Verify the alint builtin hook.""" |
| commit = """Add test to the manifest |
| Bug: 11111 |
| Test: ... |
| Flag: ...""" |
| diff = [rh.git.RawDiffEntry(file="file.txt", status="A")] |
| ret = rh.hooks.check_alint( |
| self.project, commit, "desc", diff, options=self.options |
| ) |
| self.assertIsNotNone(ret) |
| |
| |
| if __name__ == "__main__": |
| unittest.main() |