#!/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
from pathlib
import Path
import sys
import unittest
from unittest
import mock
THIS_FILE = Path(__file__).resolve()
THIS_DIR = THIS_FILE.parent
sys.path.insert(
0, str(THIS_DIR.parent))
# 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 = THIS_DIR.parent /
"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."""
@mock.patch.object(
rh.git,
"find_repo_root", side_effect=mock_find_repo_root
)
@mock.patch.object(rh.hooks,
"_get_build_os_name", return_value=
"vapier os")
def testExpandVars(self, m, _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))
@mock.patch.object(
rh.git,
"find_repo_root", side_effect=mock_find_repo_root
)
def testArgs(self, _m):
"""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)
@mock.patch.object(
rh.git,
"find_repo_root", side_effect=mock_find_repo_root
)
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", [], {})
mock.patch.object(
rh.git,
"find_repo_root", side_effect=mock_find_repo_root
).start()
def tearDown(self):
mock.patch.stopall()
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")]
# Test success.
mock_run.return_value = rh.utils.CompletedProcess(returncode=
0)
ret = rh.hooks.check_alint(
self.project, commit,
"desc", diff, options=self.options
)
self.assertIsNotNone(ret)
self.assertIsNone(ret[
0].fixup_cmd)
# Test error with fix.
mock_run.return_value = rh.utils.CompletedProcess(returncode=
5)
ret = rh.hooks.check_alint(
self.project, commit,
"desc", diff, options=self.options
)
self.assertIsNotNone(ret)
self.assertEqual(ret[
0].fixup_cmd, [
"alint",
"fix",
"--no_amend"])
self.assertFalse(ret[
0].is_warning())
self.assertEqual(ret[
0].result.returncode,
5)
# Test warning with fix.
mock_run.return_value = rh.utils.CompletedProcess(returncode=
6)
ret = rh.hooks.check_alint(
self.project, commit,
"desc", diff, options=self.options
)
self.assertIsNotNone(ret)
self.assertEqual(ret[
0].fixup_cmd, [
"alint",
"fix",
"--no_amend"])
self.assertFalse(ret[
0].is_warning())
self.assertEqual(ret[
0].result.returncode,
6)
# Test warning without fix.
mock_run.return_value = rh.utils.CompletedProcess(returncode=
77)
ret = rh.hooks.check_alint(
self.project, commit,
"desc", diff, options=self.options
)
self.assertIsNotNone(ret)
self.assertIsNone(ret[
0].fixup_cmd)
self.assertTrue(ret[
0].is_warning())
self.assertEqual(ret[
0].result.returncode,
77)
if __name__ ==
"__main__":
unittest.main()