summaryrefslogtreecommitdiffstats
path: root/tests/commands/hook_impl_test.py
diff options
context:
space:
mode:
Diffstat (limited to 'tests/commands/hook_impl_test.py')
-rw-r--r--tests/commands/hook_impl_test.py381
1 files changed, 381 insertions, 0 deletions
diff --git a/tests/commands/hook_impl_test.py b/tests/commands/hook_impl_test.py
new file mode 100644
index 0000000..d757e85
--- /dev/null
+++ b/tests/commands/hook_impl_test.py
@@ -0,0 +1,381 @@
+from __future__ import annotations
+
+import subprocess
+import sys
+from unittest import mock
+
+import pytest
+
+import pre_commit.constants as C
+from pre_commit import git
+from pre_commit.commands import hook_impl
+from pre_commit.envcontext import envcontext
+from pre_commit.util import cmd_output
+from pre_commit.util import make_executable
+from testing.fixtures import git_dir
+from testing.fixtures import sample_local_config
+from testing.fixtures import write_config
+from testing.util import cwd
+from testing.util import git_commit
+
+
+def test_validate_config_file_exists(tmpdir):
+ cfg = tmpdir.join(C.CONFIG_FILE).ensure()
+ hook_impl._validate_config(0, cfg, True)
+
+
+def test_validate_config_missing(capsys):
+ with pytest.raises(SystemExit) as excinfo:
+ hook_impl._validate_config(123, 'DNE.yaml', False)
+ ret, = excinfo.value.args
+ assert ret == 1
+ assert capsys.readouterr().out == (
+ 'No DNE.yaml file was found\n'
+ '- To temporarily silence this, run '
+ '`PRE_COMMIT_ALLOW_NO_CONFIG=1 git ...`\n'
+ '- To permanently silence this, install pre-commit with the '
+ '--allow-missing-config option\n'
+ '- To uninstall pre-commit run `pre-commit uninstall`\n'
+ )
+
+
+def test_validate_config_skip_missing_config(capsys):
+ with pytest.raises(SystemExit) as excinfo:
+ hook_impl._validate_config(123, 'DNE.yaml', True)
+ ret, = excinfo.value.args
+ assert ret == 123
+ expected = '`DNE.yaml` config file not found. Skipping `pre-commit`.\n'
+ assert capsys.readouterr().out == expected
+
+
+def test_validate_config_skip_via_env_variable(capsys):
+ with pytest.raises(SystemExit) as excinfo:
+ with envcontext((('PRE_COMMIT_ALLOW_NO_CONFIG', '1'),)):
+ hook_impl._validate_config(0, 'DNE.yaml', False)
+ ret, = excinfo.value.args
+ assert ret == 0
+ expected = '`DNE.yaml` config file not found. Skipping `pre-commit`.\n'
+ assert capsys.readouterr().out == expected
+
+
+def test_run_legacy_does_not_exist(tmpdir):
+ retv, stdin = hook_impl._run_legacy('pre-commit', tmpdir, ())
+ assert (retv, stdin) == (0, b'')
+
+
+def test_run_legacy_executes_legacy_script(tmpdir, capfd):
+ hook = tmpdir.join('pre-commit.legacy')
+ hook.write('#!/usr/bin/env bash\necho hi "$@"\nexit 1\n')
+ make_executable(hook)
+ retv, stdin = hook_impl._run_legacy('pre-commit', tmpdir, ('arg1', 'arg2'))
+ assert capfd.readouterr().out.strip() == 'hi arg1 arg2'
+ assert (retv, stdin) == (1, b'')
+
+
+def test_run_legacy_pre_push_returns_stdin(tmpdir):
+ with mock.patch.object(sys.stdin.buffer, 'read', return_value=b'stdin'):
+ retv, stdin = hook_impl._run_legacy('pre-push', tmpdir, ())
+ assert (retv, stdin) == (0, b'stdin')
+
+
+def test_run_legacy_recursive(tmpdir):
+ hook = tmpdir.join('pre-commit.legacy').ensure()
+ make_executable(hook)
+
+ # simulate a call being recursive
+ def call(*_, **__):
+ return hook_impl._run_legacy('pre-commit', tmpdir, ())
+
+ with mock.patch.object(subprocess, 'run', call):
+ with pytest.raises(SystemExit):
+ call()
+
+
+@pytest.mark.parametrize(
+ ('hook_type', 'args'),
+ (
+ ('pre-commit', []),
+ ('pre-merge-commit', []),
+ ('pre-push', ['branch_name', 'remote_name']),
+ ('commit-msg', ['.git/COMMIT_EDITMSG']),
+ ('post-commit', []),
+ ('post-merge', ['1']),
+ ('pre-rebase', ['main', 'topic']),
+ ('pre-rebase', ['main']),
+ ('post-checkout', ['old_head', 'new_head', '1']),
+ ('post-rewrite', ['amend']),
+ # multiple choices for commit-editmsg
+ ('prepare-commit-msg', ['.git/COMMIT_EDITMSG']),
+ ('prepare-commit-msg', ['.git/COMMIT_EDITMSG', 'message']),
+ ('prepare-commit-msg', ['.git/COMMIT_EDITMSG', 'commit', 'deadbeef']),
+ ),
+)
+def test_check_args_length_ok(hook_type, args):
+ hook_impl._check_args_length(hook_type, args)
+
+
+def test_check_args_length_error_too_many_plural():
+ with pytest.raises(SystemExit) as excinfo:
+ hook_impl._check_args_length('pre-commit', ['run', '--all-files'])
+ msg, = excinfo.value.args
+ assert msg == (
+ 'hook-impl for pre-commit expected 0 arguments but got 2: '
+ "['run', '--all-files']"
+ )
+
+
+def test_check_args_length_error_too_many_singular():
+ with pytest.raises(SystemExit) as excinfo:
+ hook_impl._check_args_length('commit-msg', [])
+ msg, = excinfo.value.args
+ assert msg == 'hook-impl for commit-msg expected 1 argument but got 0: []'
+
+
+def test_check_args_length_prepare_commit_msg_error():
+ with pytest.raises(SystemExit) as excinfo:
+ hook_impl._check_args_length('prepare-commit-msg', [])
+ msg, = excinfo.value.args
+ assert msg == (
+ 'hook-impl for prepare-commit-msg expected 1, 2, or 3 arguments '
+ 'but got 0: []'
+ )
+
+
+def test_check_args_length_pre_rebase_error():
+ with pytest.raises(SystemExit) as excinfo:
+ hook_impl._check_args_length('pre-rebase', [])
+ msg, = excinfo.value.args
+ assert msg == 'hook-impl for pre-rebase expected 1 or 2 arguments but got 0: []' # noqa: E501
+
+
+def test_run_ns_pre_commit():
+ ns = hook_impl._run_ns('pre-commit', True, (), b'')
+ assert ns is not None
+ assert ns.hook_stage == 'pre-commit'
+ assert ns.color is True
+
+
+def test_run_ns_pre_rebase():
+ ns = hook_impl._run_ns('pre-rebase', True, ('main', 'topic'), b'')
+ assert ns is not None
+ assert ns.hook_stage == 'pre-rebase'
+ assert ns.color is True
+ assert ns.pre_rebase_upstream == 'main'
+ assert ns.pre_rebase_branch == 'topic'
+
+ ns = hook_impl._run_ns('pre-rebase', True, ('main',), b'')
+ assert ns is not None
+ assert ns.hook_stage == 'pre-rebase'
+ assert ns.color is True
+ assert ns.pre_rebase_upstream == 'main'
+ assert ns.pre_rebase_branch is None
+
+
+def test_run_ns_commit_msg():
+ ns = hook_impl._run_ns('commit-msg', False, ('.git/COMMIT_MSG',), b'')
+ assert ns is not None
+ assert ns.hook_stage == 'commit-msg'
+ assert ns.color is False
+ assert ns.commit_msg_filename == '.git/COMMIT_MSG'
+
+
+def test_run_ns_prepare_commit_msg_one_arg():
+ ns = hook_impl._run_ns(
+ 'prepare-commit-msg', False,
+ ('.git/COMMIT_MSG',), b'',
+ )
+ assert ns is not None
+ assert ns.hook_stage == 'prepare-commit-msg'
+ assert ns.color is False
+ assert ns.commit_msg_filename == '.git/COMMIT_MSG'
+
+
+def test_run_ns_prepare_commit_msg_two_arg():
+ ns = hook_impl._run_ns(
+ 'prepare-commit-msg', False,
+ ('.git/COMMIT_MSG', 'message'), b'',
+ )
+ assert ns is not None
+ assert ns.hook_stage == 'prepare-commit-msg'
+ assert ns.color is False
+ assert ns.commit_msg_filename == '.git/COMMIT_MSG'
+ assert ns.prepare_commit_message_source == 'message'
+
+
+def test_run_ns_prepare_commit_msg_three_arg():
+ ns = hook_impl._run_ns(
+ 'prepare-commit-msg', False,
+ ('.git/COMMIT_MSG', 'message', 'HEAD'), b'',
+ )
+ assert ns is not None
+ assert ns.hook_stage == 'prepare-commit-msg'
+ assert ns.color is False
+ assert ns.commit_msg_filename == '.git/COMMIT_MSG'
+ assert ns.prepare_commit_message_source == 'message'
+ assert ns.commit_object_name == 'HEAD'
+
+
+def test_run_ns_post_commit():
+ ns = hook_impl._run_ns('post-commit', True, (), b'')
+ assert ns is not None
+ assert ns.hook_stage == 'post-commit'
+ assert ns.color is True
+
+
+def test_run_ns_post_merge():
+ ns = hook_impl._run_ns('post-merge', True, ('1',), b'')
+ assert ns is not None
+ assert ns.hook_stage == 'post-merge'
+ assert ns.color is True
+ assert ns.is_squash_merge == '1'
+
+
+def test_run_ns_post_rewrite():
+ ns = hook_impl._run_ns('post-rewrite', True, ('amend',), b'')
+ assert ns is not None
+ assert ns.hook_stage == 'post-rewrite'
+ assert ns.color is True
+ assert ns.rewrite_command == 'amend'
+
+
+def test_run_ns_post_checkout():
+ ns = hook_impl._run_ns('post-checkout', True, ('a', 'b', 'c'), b'')
+ assert ns is not None
+ assert ns.hook_stage == 'post-checkout'
+ assert ns.color is True
+ assert ns.from_ref == 'a'
+ assert ns.to_ref == 'b'
+ assert ns.checkout_type == 'c'
+
+
+@pytest.fixture
+def push_example(tempdir_factory):
+ src = git_dir(tempdir_factory)
+ git_commit(cwd=src)
+ src_head = git.head_rev(src)
+
+ clone = tempdir_factory.get()
+ cmd_output('git', 'clone', src, clone)
+ git_commit(cwd=clone)
+ clone_head = git.head_rev(clone)
+ return (src, src_head, clone, clone_head)
+
+
+def test_run_ns_pre_push_updating_branch(push_example):
+ src, src_head, clone, clone_head = push_example
+
+ with cwd(clone):
+ args = ('origin', src)
+ stdin = f'HEAD {clone_head} refs/heads/b {src_head}\n'.encode()
+ ns = hook_impl._run_ns('pre-push', False, args, stdin)
+
+ assert ns is not None
+ assert ns.hook_stage == 'pre-push'
+ assert ns.color is False
+ assert ns.remote_name == 'origin'
+ assert ns.remote_url == src
+ assert ns.from_ref == src_head
+ assert ns.to_ref == clone_head
+ assert ns.all_files is False
+
+
+def test_run_ns_pre_push_new_branch(push_example):
+ src, src_head, clone, clone_head = push_example
+
+ with cwd(clone):
+ args = ('origin', src)
+ stdin = f'HEAD {clone_head} refs/heads/b {hook_impl.Z40}\n'.encode()
+ ns = hook_impl._run_ns('pre-push', False, args, stdin)
+
+ assert ns is not None
+ assert ns.from_ref == src_head
+ assert ns.to_ref == clone_head
+
+
+def test_run_ns_pre_push_new_branch_existing_rev(push_example):
+ src, src_head, clone, _ = push_example
+
+ with cwd(clone):
+ args = ('origin', src)
+ stdin = f'HEAD {src_head} refs/heads/b2 {hook_impl.Z40}\n'.encode()
+ ns = hook_impl._run_ns('pre-push', False, args, stdin)
+
+ assert ns is None
+
+
+def test_run_ns_pre_push_ref_with_whitespace(push_example):
+ src, src_head, clone, _ = push_example
+
+ with cwd(clone):
+ args = ('origin', src)
+ line = f'HEAD^{{/ }} {src_head} refs/heads/b2 {hook_impl.Z40}\n'
+ stdin = line.encode()
+ ns = hook_impl._run_ns('pre-push', False, args, stdin)
+
+ assert ns is None
+
+
+def test_pushing_orphan_branch(push_example):
+ src, src_head, clone, _ = push_example
+
+ cmd_output('git', 'checkout', '--orphan', 'b2', cwd=clone)
+ git_commit(cwd=clone, msg='something else to get unique hash')
+ clone_rev = git.head_rev(clone)
+
+ with cwd(clone):
+ args = ('origin', src)
+ stdin = f'HEAD {clone_rev} refs/heads/b2 {hook_impl.Z40}\n'.encode()
+ ns = hook_impl._run_ns('pre-push', False, args, stdin)
+
+ assert ns is not None
+ assert ns.all_files is True
+
+
+def test_run_ns_pre_push_deleting_branch(push_example):
+ src, src_head, clone, _ = push_example
+
+ with cwd(clone):
+ args = ('origin', src)
+ stdin = f'(delete) {hook_impl.Z40} refs/heads/b {src_head}'.encode()
+ ns = hook_impl._run_ns('pre-push', False, args, stdin)
+
+ assert ns is None
+
+
+def test_hook_impl_main_noop_pre_push(cap_out, store, push_example):
+ src, src_head, clone, _ = push_example
+
+ stdin = f'(delete) {hook_impl.Z40} refs/heads/b {src_head}'.encode()
+ with mock.patch.object(sys.stdin.buffer, 'read', return_value=stdin):
+ with cwd(clone):
+ write_config('.', sample_local_config())
+ ret = hook_impl.hook_impl(
+ store,
+ config=C.CONFIG_FILE,
+ color=False,
+ hook_type='pre-push',
+ hook_dir='.git/hooks',
+ skip_on_missing_config=False,
+ args=('origin', src),
+ )
+ assert ret == 0
+ assert cap_out.get() == ''
+
+
+def test_hook_impl_main_runs_hooks(cap_out, tempdir_factory, store):
+ with cwd(git_dir(tempdir_factory)):
+ write_config('.', sample_local_config())
+ ret = hook_impl.hook_impl(
+ store,
+ config=C.CONFIG_FILE,
+ color=False,
+ hook_type='pre-commit',
+ hook_dir='.git/hooks',
+ skip_on_missing_config=False,
+ args=(),
+ )
+ assert ret == 0
+ expected = '''\
+Block if "DO NOT COMMIT" is found....................(no files to check)Skipped
+'''
+ assert cap_out.get() == expected