diff options
Diffstat (limited to '')
-rw-r--r-- | tests/repository_test.py | 902 |
1 files changed, 902 insertions, 0 deletions
diff --git a/tests/repository_test.py b/tests/repository_test.py new file mode 100644 index 0000000..df7e7d1 --- /dev/null +++ b/tests/repository_test.py @@ -0,0 +1,902 @@ +import os.path +import re +import shutil +import sys +from typing import Any +from typing import Dict +from unittest import mock + +import cfgv +import pytest + +import pre_commit.constants as C +from pre_commit.clientlib import CONFIG_SCHEMA +from pre_commit.clientlib import load_manifest +from pre_commit.envcontext import envcontext +from pre_commit.hook import Hook +from pre_commit.languages import golang +from pre_commit.languages import helpers +from pre_commit.languages import node +from pre_commit.languages import python +from pre_commit.languages import ruby +from pre_commit.languages import rust +from pre_commit.languages.all import languages +from pre_commit.prefix import Prefix +from pre_commit.repository import all_hooks +from pre_commit.repository import install_hook_envs +from pre_commit.util import cmd_output +from pre_commit.util import cmd_output_b +from testing.fixtures import make_config_from_repo +from testing.fixtures import make_repo +from testing.fixtures import modify_manifest +from testing.util import cwd +from testing.util import get_resource_path +from testing.util import skipif_cant_run_docker +from testing.util import skipif_cant_run_swift +from testing.util import xfailif_no_venv +from testing.util import xfailif_windows_no_ruby + + +def _norm_out(b): + return b.replace(b'\r\n', b'\n') + + +def _hook_run(hook, filenames, color): + return languages[hook.language].run_hook(hook, filenames, color) + + +def _get_hook_no_install(repo_config, store, hook_id): + config = {'repos': [repo_config]} + config = cfgv.validate(config, CONFIG_SCHEMA) + config = cfgv.apply_defaults(config, CONFIG_SCHEMA) + hooks = all_hooks(config, store) + hook, = [hook for hook in hooks if hook.id == hook_id] + return hook + + +def _get_hook(repo_config, store, hook_id): + hook = _get_hook_no_install(repo_config, store, hook_id) + install_hook_envs([hook], store) + return hook + + +def _test_hook_repo( + tempdir_factory, + store, + repo_path, + hook_id, + args, + expected, + expected_return_code=0, + config_kwargs=None, + color=False, +): + path = make_repo(tempdir_factory, repo_path) + config = make_config_from_repo(path, **(config_kwargs or {})) + hook = _get_hook(config, store, hook_id) + ret, out = _hook_run(hook, args, color=color) + assert ret == expected_return_code + assert _norm_out(out) == expected + + +def test_conda_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'conda_hooks_repo', + 'sys-exec', [os.devnull], + b'conda-default\n', + ) + + +def test_conda_with_additional_dependencies_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'conda_hooks_repo', + 'additional-deps', [os.devnull], + b'OK\n', + config_kwargs={ + 'hooks': [{ + 'id': 'additional-deps', + 'args': ['-c', 'import mccabe; print("OK")'], + 'additional_dependencies': ['mccabe'], + }], + }, + ) + + +def test_local_conda_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'local-conda', + 'name': 'local-conda', + 'entry': 'python', + 'language': 'conda', + 'args': ['-c', 'import mccabe; print("OK")'], + 'additional_dependencies': ['mccabe'], + }], + } + hook = _get_hook(config, store, 'local-conda') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + assert _norm_out(out) == b'OK\n' + + +def test_python_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'python_hooks_repo', + 'foo', [os.devnull], + f'[{os.devnull!r}]\nHello World\n'.encode(), + ) + + +def test_python_hook_default_version(tempdir_factory, store): + # make sure that this continues to work for platforms where default + # language detection does not work + with mock.patch.object( + python, 'get_default_version', return_value=C.DEFAULT, + ): + test_python_hook(tempdir_factory, store) + + +def test_python_hook_args_with_spaces(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'python_hooks_repo', + 'foo', + [], + b"['i have spaces', 'and\"\\'quotes', '$and !this']\n" + b'Hello World\n', + config_kwargs={ + 'hooks': [{ + 'id': 'foo', + 'args': ['i have spaces', 'and"\'quotes', '$and !this'], + }], + }, + ) + + +def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): + in_git_dir.join('setup.cfg').write('[install]\ninstall_scripts=/usr/sbin') + + _test_hook_repo( + tempdir_factory, store, 'python_hooks_repo', + 'foo', [os.devnull], + f'[{os.devnull!r}]\nHello World\n'.encode(), + ) + + +@xfailif_no_venv +def test_python_venv(tempdir_factory, store): # pragma: no cover (no venv) + _test_hook_repo( + tempdir_factory, store, 'python_venv_hooks_repo', + 'foo', [os.devnull], + f'[{os.devnull!r}]\nHello World\n'.encode(), + ) + + +def test_switch_language_versions_doesnt_clobber(tempdir_factory, store): + # We're using the python3 repo because it prints the python version + path = make_repo(tempdir_factory, 'python3_hooks_repo') + + def run_on_version(version, expected_output): + config = make_config_from_repo(path) + config['hooks'][0]['language_version'] = version + hook = _get_hook(config, store, 'python3-hook') + ret, out = _hook_run(hook, [], color=False) + assert ret == 0 + assert _norm_out(out) == expected_output + + run_on_version('python2', b'2\n[]\nHello World\n') + run_on_version('python3', b'3\n[]\nHello World\n') + + +def test_versioned_python_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'python3_hooks_repo', + 'python3-hook', + [os.devnull], + f'3\n[{os.devnull!r}]\nHello World\n'.encode(), + ) + + +@skipif_cant_run_docker # pragma: win32 no cover +def test_run_a_docker_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'docker_hooks_repo', + 'docker-hook', + ['Hello World from docker'], b'Hello World from docker\n', + ) + + +@skipif_cant_run_docker # pragma: win32 no cover +def test_run_a_docker_hook_with_entry_args(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'docker_hooks_repo', + 'docker-hook-arg', + ['Hello World from docker'], b'Hello World from docker', + ) + + +@skipif_cant_run_docker # pragma: win32 no cover +def test_run_a_failing_docker_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'docker_hooks_repo', + 'docker-hook-failing', + ['Hello World from docker'], + mock.ANY, # an error message about `bork` not existing + expected_return_code=127, + ) + + +@skipif_cant_run_docker # pragma: win32 no cover +@pytest.mark.parametrize('hook_id', ('echo-entrypoint', 'echo-cmd')) +def test_run_a_docker_image_hook(tempdir_factory, store, hook_id): + _test_hook_repo( + tempdir_factory, store, 'docker_image_hooks_repo', + hook_id, + ['Hello World from docker'], b'Hello World from docker\n', + ) + + +def test_run_a_node_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'node_hooks_repo', + 'foo', [os.devnull], b'Hello World\n', + ) + + +def test_run_versioned_node_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'node_versioned_hooks_repo', + 'versioned-node-hook', [os.devnull], b'v9.3.0\nHello World\n', + ) + + +@xfailif_windows_no_ruby +def test_run_a_ruby_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'ruby_hooks_repo', + 'ruby_hook', [os.devnull], b'Hello world from a ruby hook\n', + ) + + +@xfailif_windows_no_ruby +def test_run_versioned_ruby_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'ruby_versioned_hooks_repo', + 'ruby_hook', + [os.devnull], + b'2.5.1\nHello world from a ruby hook\n', + ) + + +@xfailif_windows_no_ruby +def test_run_ruby_hook_with_disable_shared_gems( + tempdir_factory, + store, + tmpdir, +): + """Make sure a Gemfile in the project doesn't interfere.""" + tmpdir.join('Gemfile').write('gem "lol_hai"') + tmpdir.join('.bundle').mkdir() + tmpdir.join('.bundle', 'config').write( + 'BUNDLE_DISABLE_SHARED_GEMS: true\n' + 'BUNDLE_PATH: vendor/gem\n', + ) + with cwd(tmpdir.strpath): + _test_hook_repo( + tempdir_factory, store, 'ruby_versioned_hooks_repo', + 'ruby_hook', + [os.devnull], + b'2.5.1\nHello world from a ruby hook\n', + ) + + +def test_system_hook_with_spaces(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'system_hook_with_spaces_repo', + 'system-hook-with-spaces', [os.devnull], b'Hello World\n', + ) + + +@skipif_cant_run_swift # pragma: win32 no cover +def test_swift_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'swift_hooks_repo', + 'swift-hooks-repo', [], b'Hello, world!\n', + ) + + +def test_golang_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'golang_hooks_repo', + 'golang-hook', [], b'hello world\n', + ) + + +def test_golang_hook_still_works_when_gobin_is_set(tempdir_factory, store): + gobin_dir = tempdir_factory.get() + with envcontext((('GOBIN', gobin_dir),)): + test_golang_hook(tempdir_factory, store) + assert os.listdir(gobin_dir) == [] + + +def test_rust_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'rust_hooks_repo', + 'rust-hook', [], b'hello world\n', + ) + + +@pytest.mark.parametrize('dep', ('cli:shellharden:3.1.0', 'cli:shellharden')) +def test_additional_rust_cli_dependencies_installed( + tempdir_factory, store, dep, +): + path = make_repo(tempdir_factory, 'rust_hooks_repo') + config = make_config_from_repo(path) + # A small rust package with no dependencies. + config['hooks'][0]['additional_dependencies'] = [dep] + hook = _get_hook(config, store, 'rust-hook') + binaries = os.listdir( + hook.prefix.path( + helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + ), + ) + # normalize for windows + binaries = [os.path.splitext(binary)[0] for binary in binaries] + assert 'shellharden' in binaries + + +def test_additional_rust_lib_dependencies_installed( + tempdir_factory, store, +): + path = make_repo(tempdir_factory, 'rust_hooks_repo') + config = make_config_from_repo(path) + # A small rust package with no dependencies. + deps = ['shellharden:3.1.0'] + config['hooks'][0]['additional_dependencies'] = deps + hook = _get_hook(config, store, 'rust-hook') + binaries = os.listdir( + hook.prefix.path( + helpers.environment_dir(rust.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + ), + ) + # normalize for windows + binaries = [os.path.splitext(binary)[0] for binary in binaries] + assert 'rust-hello-world' in binaries + assert 'shellharden' not in binaries + + +def test_missing_executable(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'not_found_exe', + 'not-found-exe', [os.devnull], + b'Executable `i-dont-exist-lol` not found', + expected_return_code=1, + ) + + +def test_run_a_script_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'script_hooks_repo', + 'bash_hook', ['bar'], b'bar\nHello World\n', + ) + + +def test_run_hook_with_spaced_args(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'arg_per_line_hooks_repo', + 'arg-per-line', + ['foo bar', 'baz'], + b'arg: hello\narg: world\narg: foo bar\narg: baz\n', + ) + + +def test_run_hook_with_curly_braced_arguments(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'arg_per_line_hooks_repo', + 'arg-per-line', + [], + b"arg: hi {1}\narg: I'm {a} problem\n", + config_kwargs={ + 'hooks': [{ + 'id': 'arg-per-line', + 'args': ['hi {1}', "I'm {a} problem"], + }], + }, + ) + + +def test_intermixed_stdout_stderr(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'stdout_stderr_repo', + 'stdout-stderr', + [], + b'0\n1\n2\n3\n4\n5\n', + ) + + +@pytest.mark.xfail(os.name == 'nt', reason='ptys are posix-only') +def test_output_isatty(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'stdout_stderr_repo', + 'tty-check', + [], + b'stdin: False\nstdout: True\nstderr: True\n', + color=True, + ) + + +def _make_grep_repo(entry, store, args=()): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'grep-hook', + 'name': 'grep-hook', + 'language': 'pygrep', + 'entry': entry, + 'args': args, + 'types': ['text'], + }], + } + return _get_hook(config, store, 'grep-hook') + + +@pytest.fixture +def greppable_files(tmpdir): + with tmpdir.as_cwd(): + cmd_output_b('git', 'init', '.') + tmpdir.join('f1').write_binary(b"hello'hi\nworld\n") + tmpdir.join('f2').write_binary(b'foo\nbar\nbaz\n') + tmpdir.join('f3').write_binary(b'[WARN] hi\n') + yield tmpdir + + +def test_grep_hook_matching(greppable_files, store): + hook = _make_grep_repo('ello', store) + ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) + assert ret == 1 + assert _norm_out(out) == b"f1:1:hello'hi\n" + + +def test_grep_hook_case_insensitive(greppable_files, store): + hook = _make_grep_repo('ELLO', store, args=['-i']) + ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) + assert ret == 1 + assert _norm_out(out) == b"f1:1:hello'hi\n" + + +@pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) +def test_grep_hook_not_matching(regex, greppable_files, store): + hook = _make_grep_repo(regex, store) + ret, out = _hook_run(hook, ('f1', 'f2', 'f3'), color=False) + assert (ret, out) == (0, b'') + + +def _norm_pwd(path): + # Under windows bash's temp and windows temp is different. + # This normalizes to the bash /tmp + return cmd_output_b( + 'bash', '-c', f"cd '{path}' && pwd", + )[1].strip() + + +def test_cwd_of_hook(in_git_dir, tempdir_factory, store): + # Note: this doubles as a test for `system` hooks + _test_hook_repo( + tempdir_factory, store, 'prints_cwd_repo', + 'prints_cwd', ['-L'], _norm_pwd(in_git_dir.strpath) + b'\n', + ) + + +def test_lots_of_files(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'script_hooks_repo', + 'bash_hook', [os.devnull] * 15000, mock.ANY, + ) + + +def test_additional_dependencies_roll_forward(tempdir_factory, store): + path = make_repo(tempdir_factory, 'python_hooks_repo') + + config1 = make_config_from_repo(path) + hook1 = _get_hook(config1, store, 'foo') + with python.in_env(hook1.prefix, hook1.language_version): + assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] + + # Make another repo with additional dependencies + config2 = make_config_from_repo(path) + config2['hooks'][0]['additional_dependencies'] = ['mccabe'] + hook2 = _get_hook(config2, store, 'foo') + with python.in_env(hook2.prefix, hook2.language_version): + assert 'mccabe' in cmd_output('pip', 'freeze', '-l')[1] + + # should not have affected original + with python.in_env(hook1.prefix, hook1.language_version): + assert 'mccabe' not in cmd_output('pip', 'freeze', '-l')[1] + + +@xfailif_windows_no_ruby # pragma: win32 no cover +def test_additional_ruby_dependencies_installed(tempdir_factory, store): + path = make_repo(tempdir_factory, 'ruby_hooks_repo') + config = make_config_from_repo(path) + config['hooks'][0]['additional_dependencies'] = ['tins'] + hook = _get_hook(config, store, 'ruby_hook') + with ruby.in_env(hook.prefix, hook.language_version): + output = cmd_output('gem', 'list', '--local')[1] + assert 'tins' in output + + +def test_additional_node_dependencies_installed(tempdir_factory, store): + path = make_repo(tempdir_factory, 'node_hooks_repo') + config = make_config_from_repo(path) + # Careful to choose a small package that's not depped by npm + config['hooks'][0]['additional_dependencies'] = ['lodash'] + hook = _get_hook(config, store, 'foo') + with node.in_env(hook.prefix, hook.language_version): + output = cmd_output('npm', 'ls', '-g')[1] + assert 'lodash' in output + + +def test_additional_golang_dependencies_installed( + tempdir_factory, store, +): + path = make_repo(tempdir_factory, 'golang_hooks_repo') + config = make_config_from_repo(path) + # A small go package + deps = ['github.com/golang/example/hello'] + config['hooks'][0]['additional_dependencies'] = deps + hook = _get_hook(config, store, 'golang-hook') + binaries = os.listdir( + hook.prefix.path( + helpers.environment_dir(golang.ENVIRONMENT_DIR, C.DEFAULT), 'bin', + ), + ) + # normalize for windows + binaries = [os.path.splitext(binary)[0] for binary in binaries] + assert 'hello' in binaries + + +def test_local_golang_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'hello', + 'name': 'hello', + 'entry': 'hello', + 'language': 'golang', + 'additional_dependencies': ['github.com/golang/example/hello'], + }], + } + hook = _get_hook(config, store, 'hello') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + assert _norm_out(out) == b'Hello, Go examples!\n' + + +def test_local_rust_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'hello', + 'name': 'hello', + 'entry': 'hello', + 'language': 'rust', + 'additional_dependencies': ['cli:hello-cli:0.2.2'], + }], + } + hook = _get_hook(config, store, 'hello') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + assert _norm_out(out) == b'Hello World!\n' + + +def test_fail_hooks(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'fail', + 'name': 'fail', + 'language': 'fail', + 'entry': 'make sure to name changelogs as .rst!', + 'files': r'changelog/.*(?<!\.rst)$', + }], + } + hook = _get_hook(config, store, 'fail') + ret, out = _hook_run( + hook, ('changelog/123.bugfix', 'changelog/wat'), color=False, + ) + assert ret == 1 + assert out == ( + b'make sure to name changelogs as .rst!\n' + b'\n' + b'changelog/123.bugfix\n' + b'changelog/wat\n' + ) + + +def test_unknown_keys(store, fake_log_handler): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'too-much', + 'name': 'too much', + 'hello': 'world', + 'foo': 'bar', + 'language': 'system', + 'entry': 'true', + }], + } + _get_hook(config, store, 'too-much') + expected = 'Unexpected key(s) present on local => too-much: foo, hello' + assert fake_log_handler.handle.call_args[0][0].msg == expected + + +def test_reinstall(tempdir_factory, store, log_info_mock): + path = make_repo(tempdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + _get_hook(config, store, 'foo') + # We print some logging during clone (1) + install (3) + assert log_info_mock.call_count == 4 + log_info_mock.reset_mock() + # Reinstall on another run should not trigger another install + _get_hook(config, store, 'foo') + assert log_info_mock.call_count == 0 + + +def test_control_c_control_c_on_install(tempdir_factory, store): + """Regression test for #186.""" + path = make_repo(tempdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + hooks = [_get_hook_no_install(config, store, 'foo')] + + class MyKeyboardInterrupt(KeyboardInterrupt): + pass + + # To simulate a killed install, we'll make PythonEnv.run raise ^C + # and then to simulate a second ^C during cleanup, we'll make shutil.rmtree + # raise as well. + with pytest.raises(MyKeyboardInterrupt): + with mock.patch.object( + helpers, 'run_setup_cmd', side_effect=MyKeyboardInterrupt, + ): + with mock.patch.object( + shutil, 'rmtree', side_effect=MyKeyboardInterrupt, + ): + install_hook_envs(hooks, store) + + # Should have made an environment, however this environment is broken! + hook, = hooks + assert hook.prefix.exists( + helpers.environment_dir(python.ENVIRONMENT_DIR, hook.language_version), + ) + + # However, it should be perfectly runnable (reinstall after botched + # install) + install_hook_envs(hooks, store) + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + + +def test_invalidated_virtualenv(tempdir_factory, store): + # A cached virtualenv may become invalidated if the system python upgrades + # This should not cause every hook in that virtualenv to fail. + path = make_repo(tempdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + hook = _get_hook(config, store, 'foo') + + # Simulate breaking of the virtualenv + libdir = hook.prefix.path( + helpers.environment_dir(python.ENVIRONMENT_DIR, hook.language_version), + 'lib', hook.language_version, + ) + paths = [ + os.path.join(libdir, p) for p in ('site.py', 'site.pyc', '__pycache__') + ] + cmd_output_b('rm', '-rf', *paths) + + # pre-commit should rebuild the virtualenv and it should be runnable + hook = _get_hook(config, store, 'foo') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + + +def test_really_long_file_paths(tempdir_factory, store): + base_path = tempdir_factory.get() + really_long_path = os.path.join(base_path, 'really_long' * 10) + cmd_output_b('git', 'init', really_long_path) + + path = make_repo(tempdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path) + + with cwd(really_long_path): + _get_hook(config, store, 'foo') + + +def test_config_overrides_repo_specifics(tempdir_factory, store): + path = make_repo(tempdir_factory, 'script_hooks_repo') + config = make_config_from_repo(path) + + hook = _get_hook(config, store, 'bash_hook') + assert hook.files == '' + # Set the file regex to something else + config['hooks'][0]['files'] = '\\.sh$' + hook = _get_hook(config, store, 'bash_hook') + assert hook.files == '\\.sh$' + + +def _create_repo_with_tags(tempdir_factory, src, tag): + path = make_repo(tempdir_factory, src) + cmd_output_b('git', 'tag', tag, cwd=path) + return path + + +def test_tags_on_repositories(in_tmpdir, tempdir_factory, store): + tag = 'v1.1' + git1 = _create_repo_with_tags(tempdir_factory, 'prints_cwd_repo', tag) + git2 = _create_repo_with_tags(tempdir_factory, 'script_hooks_repo', tag) + + config1 = make_config_from_repo(git1, rev=tag) + hook1 = _get_hook(config1, store, 'prints_cwd') + ret1, out1 = _hook_run(hook1, ('-L',), color=False) + assert ret1 == 0 + assert out1.strip() == _norm_pwd(in_tmpdir) + + config2 = make_config_from_repo(git2, rev=tag) + hook2 = _get_hook(config2, store, 'bash_hook') + ret2, out2 = _hook_run(hook2, ('bar',), color=False) + assert ret2 == 0 + assert out2 == b'bar\nHello World\n' + + +@pytest.fixture +def local_python_config(): + # Make a "local" hooks repo that just installs our other hooks repo + repo_path = get_resource_path('python_hooks_repo') + manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE)) + hooks = [ + dict(hook, additional_dependencies=[repo_path]) for hook in manifest + ] + return {'repo': 'local', 'hooks': hooks} + + +@pytest.mark.xfail( # pragma: win32 no cover + sys.platform == 'win32', + reason='microsoft/azure-pipelines-image-generation#989', +) +def test_local_python_repo(store, local_python_config): + hook = _get_hook(local_python_config, store, 'foo') + # language_version should have been adjusted to the interpreter version + assert hook.language_version != C.DEFAULT + ret, out = _hook_run(hook, ('filename',), color=False) + assert ret == 0 + assert _norm_out(out) == b"['filename']\nHello World\n" + + +def test_default_language_version(store, local_python_config): + config: Dict[str, Any] = { + 'default_language_version': {'python': 'fake'}, + 'default_stages': ['commit'], + 'repos': [local_python_config], + } + + # `language_version` was not set, should default + hook, = all_hooks(config, store) + assert hook.language_version == 'fake' + + # `language_version` is set, should not default + config['repos'][0]['hooks'][0]['language_version'] = 'fake2' + hook, = all_hooks(config, store) + assert hook.language_version == 'fake2' + + +def test_default_stages(store, local_python_config): + config: Dict[str, Any] = { + 'default_language_version': {'python': C.DEFAULT}, + 'default_stages': ['commit'], + 'repos': [local_python_config], + } + + # `stages` was not set, should default + hook, = all_hooks(config, store) + assert hook.stages == ['commit'] + + # `stages` is set, should not default + config['repos'][0]['hooks'][0]['stages'] = ['push'] + hook, = all_hooks(config, store) + assert hook.stages == ['push'] + + +def test_hook_id_not_present(tempdir_factory, store, fake_log_handler): + path = make_repo(tempdir_factory, 'script_hooks_repo') + config = make_config_from_repo(path) + config['hooks'][0]['id'] = 'i-dont-exist' + with pytest.raises(SystemExit): + _get_hook(config, store, 'i-dont-exist') + assert fake_log_handler.handle.call_args[0][0].msg == ( + f'`i-dont-exist` is not present in repository file://{path}. ' + f'Typo? Perhaps it is introduced in a newer version? ' + f'Often `pre-commit autoupdate` fixes this.' + ) + + +def test_too_new_version(tempdir_factory, store, fake_log_handler): + path = make_repo(tempdir_factory, 'script_hooks_repo') + with modify_manifest(path) as manifest: + manifest[0]['minimum_pre_commit_version'] = '999.0.0' + config = make_config_from_repo(path) + with pytest.raises(SystemExit): + _get_hook(config, store, 'bash_hook') + msg = fake_log_handler.handle.call_args[0][0].msg + assert re.match( + r'^The hook `bash_hook` requires pre-commit version 999\.0\.0 but ' + r'version \d+\.\d+\.\d+ is installed. ' + r'Perhaps run `pip install --upgrade pre-commit`\.$', + msg, + ) + + +@pytest.mark.parametrize('version', ('0.1.0', C.VERSION)) +def test_versions_ok(tempdir_factory, store, version): + path = make_repo(tempdir_factory, 'script_hooks_repo') + with modify_manifest(path) as manifest: + manifest[0]['minimum_pre_commit_version'] = version + config = make_config_from_repo(path) + # Should succeed + _get_hook(config, store, 'bash_hook') + + +def test_manifest_hooks(tempdir_factory, store): + path = make_repo(tempdir_factory, 'script_hooks_repo') + config = make_config_from_repo(path) + hook = _get_hook(config, store, 'bash_hook') + + assert hook == Hook( + src=f'file://{path}', + prefix=Prefix(mock.ANY), + additional_dependencies=[], + alias='', + always_run=False, + args=[], + description='', + entry='bin/hook.sh', + exclude='^$', + exclude_types=[], + files='', + id='bash_hook', + language='script', + language_version='default', + log_file='', + minimum_pre_commit_version='0', + name='Bash hook', + pass_filenames=True, + require_serial=False, + stages=( + 'commit', 'merge-commit', 'prepare-commit-msg', 'commit-msg', + 'manual', 'post-checkout', 'push', + ), + types=['file'], + verbose=False, + ) + + +def test_perl_hook(tempdir_factory, store): + _test_hook_repo( + tempdir_factory, store, 'perl_hooks_repo', + 'perl-hook', [], b'Hello from perl-commit Perl!\n', + ) + + +def test_local_perl_additional_dependencies(store): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'hello', + 'name': 'hello', + 'entry': 'perltidy --version', + 'language': 'perl', + 'additional_dependencies': ['SHANCOCK/Perl-Tidy-20200110.tar.gz'], + }], + } + hook = _get_hook(config, store, 'hello') + ret, out = _hook_run(hook, (), color=False) + assert ret == 0 + assert _norm_out(out).startswith(b'This is perltidy, v20200110') |