From d7784aa0c412c80cfcb33a29fd1b2ea69dbe8ab8 Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Mon, 27 Feb 2023 11:32:22 +0100 Subject: Merging upstream version 3.1.0. Signed-off-by: Daniel Baumann --- tests/all_languages_test.py | 7 + tests/commands/migrate_config_test.py | 33 ++++ tests/lang_base_test.py | 173 ++++++++++++++++++ tests/languages/docker_image_test.py | 27 +++ tests/languages/docker_test.py | 14 ++ tests/languages/dotnet_test.py | 154 ++++++++++++++++ tests/languages/fail_test.py | 14 ++ tests/languages/golang_test.py | 97 +++++++++- tests/languages/helpers_test.py | 135 -------------- tests/languages/pygrep_test.py | 17 ++ tests/languages/python_test.py | 23 +++ tests/languages/ruby_test.py | 4 +- tests/languages/script_test.py | 14 ++ tests/languages/system_test.py | 9 + tests/repository_test.py | 331 ++++------------------------------ tests/staged_files_only_test.py | 50 +++++ tests/store_test.py | 26 ++- tests/util_test.py | 2 +- 18 files changed, 694 insertions(+), 436 deletions(-) create mode 100644 tests/all_languages_test.py create mode 100644 tests/lang_base_test.py create mode 100644 tests/languages/docker_image_test.py create mode 100644 tests/languages/fail_test.py delete mode 100644 tests/languages/helpers_test.py create mode 100644 tests/languages/script_test.py create mode 100644 tests/languages/system_test.py (limited to 'tests') diff --git a/tests/all_languages_test.py b/tests/all_languages_test.py new file mode 100644 index 0000000..98c9121 --- /dev/null +++ b/tests/all_languages_test.py @@ -0,0 +1,7 @@ +from __future__ import annotations + +from pre_commit.all_languages import languages + + +def test_python_venv_is_an_alias_to_python(): + assert languages['python_venv'] is languages['python'] diff --git a/tests/commands/migrate_config_test.py b/tests/commands/migrate_config_test.py index fca1ad9..ba18463 100644 --- a/tests/commands/migrate_config_test.py +++ b/tests/commands/migrate_config_test.py @@ -134,6 +134,39 @@ def test_migrate_config_sha_to_rev(tmpdir): ) +def test_migrate_config_language_python_venv(tmp_path): + src = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: python_venv + - id: example + name: example + entry: example + language: system +''' + expected = '''\ +repos: +- repo: local + hooks: + - id: example + name: example + entry: example + language: python + - id: example + name: example + entry: example + language: system +''' + cfg = tmp_path.joinpath('cfg.yaml') + cfg.write_text(src) + assert migrate_config(str(cfg)) == 0 + assert cfg.read_text() == expected + + def test_migrate_config_invalid_yaml(tmpdir): contents = '[' cfg = tmpdir.join(C.CONFIG_FILE) diff --git a/tests/lang_base_test.py b/tests/lang_base_test.py new file mode 100644 index 0000000..a532b6a --- /dev/null +++ b/tests/lang_base_test.py @@ -0,0 +1,173 @@ +from __future__ import annotations + +import multiprocessing +import os.path +import sys +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit import lang_base +from pre_commit import parse_shebang +from pre_commit.prefix import Prefix +from pre_commit.util import CalledProcessError + + +@pytest.fixture +def find_exe_mck(): + with mock.patch.object(parse_shebang, 'find_executable') as mck: + yield mck + + +@pytest.fixture +def homedir_mck(): + def fake_expanduser(pth): + assert pth == '~' + return os.path.normpath('/home/me') + + with mock.patch.object(os.path, 'expanduser', fake_expanduser): + yield + + +def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): + find_exe_mck.return_value = None + assert lang_base.exe_exists('ruby') is False + + +def test_exe_exists_exists(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') + assert lang_base.exe_exists('ruby') is True + + +def test_exe_exists_false_if_shim(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/foo/shims/ruby') + assert lang_base.exe_exists('ruby') is False + + +def test_exe_exists_false_if_homedir(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/home/me/somedir/ruby') + assert lang_base.exe_exists('ruby') is False + + +def test_exe_exists_commonpath_raises_ValueError(find_exe_mck, homedir_mck): + find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') + with mock.patch.object(os.path, 'commonpath', side_effect=ValueError): + assert lang_base.exe_exists('ruby') is True + + +def test_exe_exists_true_when_homedir_is_slash(find_exe_mck): + find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') + with mock.patch.object(os.path, 'expanduser', return_value=os.sep): + assert lang_base.exe_exists('ruby') is True + + +def test_basic_get_default_version(): + assert lang_base.basic_get_default_version() == C.DEFAULT + + +def test_basic_health_check(): + assert lang_base.basic_health_check(Prefix('.'), 'default') is None + + +def test_failed_setup_command_does_not_unicode_error(): + script = ( + 'import sys\n' + "sys.stderr.buffer.write(b'\\x81\\xfe')\n" + 'raise SystemExit(1)\n' + ) + + # an assertion that this does not raise `UnicodeError` + with pytest.raises(CalledProcessError): + lang_base.setup_cmd(Prefix('.'), (sys.executable, '-c', script)) + + +def test_environment_dir(tmp_path): + ret = lang_base.environment_dir(Prefix(tmp_path), 'langenv', 'default') + assert ret == f'{tmp_path}{os.sep}langenv-default' + + +def test_assert_version_default(): + with pytest.raises(AssertionError) as excinfo: + lang_base.assert_version_default('lang', '1.2.3') + msg, = excinfo.value.args + assert msg == ( + 'for now, pre-commit requires system-installed lang -- ' + 'you selected `language_version: 1.2.3`' + ) + + +def test_assert_no_additional_deps(): + with pytest.raises(AssertionError) as excinfo: + lang_base.assert_no_additional_deps('lang', ['hmmm']) + msg, = excinfo.value.args + assert msg == ( + 'for now, pre-commit does not support additional_dependencies for ' + 'lang -- ' + "you selected `additional_dependencies: ['hmmm']`" + ) + + +def test_no_env_noop(tmp_path): + before = os.environ.copy() + with lang_base.no_env(Prefix(tmp_path), '1.2.3'): + inside = os.environ.copy() + after = os.environ.copy() + assert before == inside == after + + +def test_target_concurrency_normal(): + with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): + with mock.patch.dict(os.environ, {}, clear=True): + assert lang_base.target_concurrency() == 123 + + +def test_target_concurrency_testing_env_var(): + with mock.patch.dict( + os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, + ): + assert lang_base.target_concurrency() == 1 + + +def test_target_concurrency_on_travis(): + with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): + assert lang_base.target_concurrency() == 2 + + +def test_target_concurrency_cpu_count_not_implemented(): + with mock.patch.object( + multiprocessing, 'cpu_count', side_effect=NotImplementedError, + ): + with mock.patch.dict(os.environ, {}, clear=True): + assert lang_base.target_concurrency() == 1 + + +def test_shuffled_is_deterministic(): + seq = [str(i) for i in range(10)] + expected = ['4', '0', '5', '1', '8', '6', '2', '3', '7', '9'] + assert lang_base._shuffled(seq) == expected + + +def test_xargs_require_serial_is_not_shuffled(): + ret, out = lang_base.run_xargs( + ('echo',), [str(i) for i in range(10)], + require_serial=True, + color=False, + ) + assert ret == 0 + assert out.strip() == b'0 1 2 3 4 5 6 7 8 9' + + +def test_basic_run_hook(tmp_path): + ret, out = lang_base.basic_run_hook( + Prefix(tmp_path), + 'echo hi', + ['hello'], + ['file', 'file', 'file'], + is_local=False, + require_serial=False, + color=False, + ) + assert ret == 0 + out = out.replace(b'\r\n', b'\n') + assert out == b'hi hello file file file\n' diff --git a/tests/languages/docker_image_test.py b/tests/languages/docker_image_test.py new file mode 100644 index 0000000..7993c11 --- /dev/null +++ b/tests/languages/docker_image_test.py @@ -0,0 +1,27 @@ +from __future__ import annotations + +from pre_commit.languages import docker_image +from testing.language_helpers import run_language +from testing.util import xfailif_windows + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_hook_via_entrypoint(tmp_path): + ret = run_language( + tmp_path, + docker_image, + '--entrypoint echo ubuntu:22.04', + args=('hello hello world',), + ) + assert ret == (0, b'hello hello world\n') + + +@xfailif_windows # pragma: win32 no cover +def test_docker_image_hook_via_args(tmp_path): + ret = run_language( + tmp_path, + docker_image, + 'ubuntu:22.04 echo', + args=('hello hello world',), + ) + assert ret == (0, b'hello hello world\n') diff --git a/tests/languages/docker_test.py b/tests/languages/docker_test.py index 5f7c85e..836382a 100644 --- a/tests/languages/docker_test.py +++ b/tests/languages/docker_test.py @@ -11,6 +11,8 @@ import pytest from pre_commit.languages import docker from pre_commit.util import CalledProcessError +from testing.language_helpers import run_language +from testing.util import xfailif_windows DOCKER_CGROUP_EXAMPLE = b'''\ 12:hugetlb:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7 @@ -181,3 +183,15 @@ def test_get_docker_path_in_docker_docker_in_docker(in_docker): err = CalledProcessError(1, (), b'', b'') with mock.patch.object(docker, 'cmd_output_b', side_effect=err): assert docker._get_docker_path('/project') == '/project' + + +@xfailif_windows # pragma: win32 no cover +def test_docker_hook(tmp_path): + dockerfile = '''\ +FROM ubuntu:22.04 +CMD ["echo", "This is overwritten by the entry"'] +''' + tmp_path.joinpath('Dockerfile').write_text(dockerfile) + + ret = run_language(tmp_path, docker, 'echo hello hello world') + assert ret == (0, b'hello hello world\n') diff --git a/tests/languages/dotnet_test.py b/tests/languages/dotnet_test.py index e69de29..470c03b 100644 --- a/tests/languages/dotnet_test.py +++ b/tests/languages/dotnet_test.py @@ -0,0 +1,154 @@ +from __future__ import annotations + +from pre_commit.languages import dotnet +from testing.language_helpers import run_language + + +def _write_program_cs(tmp_path): + program_cs = '''\ +using System; + +namespace dotnet_tests +{ + class Program + { + static void Main(string[] args) + { + Console.WriteLine("Hello from dotnet!"); + } + } +} +''' + tmp_path.joinpath('Program.cs').write_text(program_cs) + + +def _csproj(tool_name): + return f'''\ + + + Exe + net6 + true + {tool_name} + ./nupkg + + +''' + + +def test_dotnet_csproj(tmp_path): + csproj = _csproj('testeroni') + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_csproj.csproj').write_text(csproj) + ret = run_language(tmp_path, dotnet, 'testeroni') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_csproj_prefix(tmp_path): + csproj = _csproj('testeroni.tool') + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_hooks_csproj_prefix.csproj').write_text(csproj) + ret = run_language(tmp_path, dotnet, 'testeroni.tool') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_sln(tmp_path): + csproj = _csproj('testeroni') + sln = '''\ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio 15 +VisualStudioVersion = 15.0.26124.0 +MinimumVisualStudioVersion = 15.0.26124.0 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "dotnet_hooks_sln_repo", "dotnet_hooks_sln_repo.csproj", "{6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Debug|x64 = Debug|x64 + Debug|x86 = Debug|x86 + Release|Any CPU = Release|Any CPU + Release|x64 = Release|x64 + Release|x86 = Release|x86 + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|Any CPU.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x64.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.ActiveCfg = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Debug|x86.Build.0 = Debug|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|Any CPU.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x64.Build.0 = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.ActiveCfg = Release|Any CPU + {6568CFDB-6F6F-45A9-932C-8C7DAABC8E56}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal +''' # noqa: E501 + _write_program_cs(tmp_path) + tmp_path.joinpath('dotnet_hooks_sln_repo.csproj').write_text(csproj) + tmp_path.joinpath('dotnet_hooks_sln_repo.sln').write_text(sln) + + ret = run_language(tmp_path, dotnet, 'testeroni') + assert ret == (0, b'Hello from dotnet!\n') + + +def _setup_dotnet_combo(tmp_path): + sln = '''\ +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.30114.105 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj1", "proj1\\proj1.csproj", "{38A939C3-DEA4-47D7-9B75-0418C4249662}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "proj2", "proj2\\proj2.csproj", "{4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Debug|Any CPU.Build.0 = Debug|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.ActiveCfg = Release|Any CPU + {38A939C3-DEA4-47D7-9B75-0418C4249662}.Release|Any CPU.Build.0 = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Debug|Any CPU.Build.0 = Debug|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.ActiveCfg = Release|Any CPU + {4C9916CB-165C-4EF5-8A57-4CB6794C1EBF}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection +EndGlobal +''' # noqa: E501 + tmp_path.joinpath('dotnet_hooks_combo_repo.sln').write_text(sln) + + csproj1 = _csproj('proj1') + proj1 = tmp_path.joinpath('proj1') + proj1.mkdir() + proj1.joinpath('proj1.csproj').write_text(csproj1) + _write_program_cs(proj1) + + csproj2 = _csproj('proj2') + proj2 = tmp_path.joinpath('proj2') + proj2.mkdir() + proj2.joinpath('proj2.csproj').write_text(csproj2) + _write_program_cs(proj2) + + +def test_dotnet_combo_proj1(tmp_path): + _setup_dotnet_combo(tmp_path) + ret = run_language(tmp_path, dotnet, 'proj1') + assert ret == (0, b'Hello from dotnet!\n') + + +def test_dotnet_combo_proj2(tmp_path): + _setup_dotnet_combo(tmp_path) + ret = run_language(tmp_path, dotnet, 'proj2') + assert ret == (0, b'Hello from dotnet!\n') diff --git a/tests/languages/fail_test.py b/tests/languages/fail_test.py new file mode 100644 index 0000000..7c74886 --- /dev/null +++ b/tests/languages/fail_test.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pre_commit.languages import fail +from testing.language_helpers import run_language + + +def test_fail_hooks(tmp_path): + ret = run_language( + tmp_path, + fail, + 'watch out for', + file_args=('bunnies',), + ) + assert ret == (1, b'watch out for\n\nbunnies\n') diff --git a/tests/languages/golang_test.py b/tests/languages/golang_test.py index 7c04255..ec5a878 100644 --- a/tests/languages/golang_test.py +++ b/tests/languages/golang_test.py @@ -6,8 +6,11 @@ import pytest import re_assert import pre_commit.constants as C +from pre_commit import lang_base +from pre_commit.envcontext import envcontext from pre_commit.languages import golang -from pre_commit.languages import helpers +from pre_commit.store import _make_local_repo +from testing.language_helpers import run_language ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__ @@ -15,7 +18,7 @@ ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__ @pytest.fixture def exe_exists_mck(): - with mock.patch.object(helpers, 'exe_exists') as mck: + with mock.patch.object(lang_base, 'exe_exists') as mck: yield mck @@ -41,3 +44,93 @@ def test_golang_infer_go_version_default(): assert version != C.DEFAULT re_assert.Matches(r'^\d+\.\d+(?:\.\d+)?$').assert_matches(version) + + +def _make_hello_world(tmp_path): + go_mod = '''\ +module golang-hello-world + +go 1.18 + +require github.com/BurntSushi/toml v1.1.0 +''' + go_sum = '''\ +github.com/BurntSushi/toml v1.1.0 h1:ksErzDEI1khOiGPgpwuI7x2ebx/uXQNw7xJpn9Eq1+I= +github.com/BurntSushi/toml v1.1.0/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= +''' # noqa: E501 + hello_world_go = '''\ +package main + + +import ( + "fmt" + "github.com/BurntSushi/toml" +) + +type Config struct { + What string +} + +func main() { + var conf Config + toml.Decode("What = 'world'\\n", &conf) + fmt.Printf("hello %v\\n", conf.What) +} +''' + tmp_path.joinpath('go.mod').write_text(go_mod) + tmp_path.joinpath('go.sum').write_text(go_sum) + mod_dir = tmp_path.joinpath('golang-hello-world') + mod_dir.mkdir() + main_file = mod_dir.joinpath('main.go') + main_file.write_text(hello_world_go) + + +def test_golang_system(tmp_path): + _make_hello_world(tmp_path) + + ret = run_language(tmp_path, golang, 'golang-hello-world') + assert ret == (0, b'hello world\n') + + +def test_golang_default_version(tmp_path): + _make_hello_world(tmp_path) + + ret = run_language( + tmp_path, + golang, + 'golang-hello-world', + version=C.DEFAULT, + ) + assert ret == (0, b'hello world\n') + + +def test_golang_versioned(tmp_path): + _make_local_repo(str(tmp_path)) + + ret, out = run_language( + tmp_path, + golang, + 'go version', + version='1.18.4', + ) + + assert ret == 0 + assert out.startswith(b'go version go1.18.4') + + +def test_local_golang_additional_deps(tmp_path): + _make_local_repo(str(tmp_path)) + + ret = run_language( + tmp_path, + golang, + 'hello', + deps=('golang.org/x/example/hello@latest',), + ) + + assert ret == (0, b'Hello, Go examples!\n') + + +def test_golang_hook_still_works_when_gobin_is_set(tmp_path): + with envcontext((('GOBIN', str(tmp_path.joinpath('gobin'))),)): + test_golang_system(tmp_path) diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py deleted file mode 100644 index c209e7e..0000000 --- a/tests/languages/helpers_test.py +++ /dev/null @@ -1,135 +0,0 @@ -from __future__ import annotations - -import multiprocessing -import os.path -import sys -from unittest import mock - -import pytest - -import pre_commit.constants as C -from pre_commit import parse_shebang -from pre_commit.languages import helpers -from pre_commit.prefix import Prefix -from pre_commit.util import CalledProcessError - - -@pytest.fixture -def find_exe_mck(): - with mock.patch.object(parse_shebang, 'find_executable') as mck: - yield mck - - -@pytest.fixture -def homedir_mck(): - def fake_expanduser(pth): - assert pth == '~' - return os.path.normpath('/home/me') - - with mock.patch.object(os.path, 'expanduser', fake_expanduser): - yield - - -def test_exe_exists_does_not_exist(find_exe_mck, homedir_mck): - find_exe_mck.return_value = None - assert helpers.exe_exists('ruby') is False - - -def test_exe_exists_exists(find_exe_mck, homedir_mck): - find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') - assert helpers.exe_exists('ruby') is True - - -def test_exe_exists_false_if_shim(find_exe_mck, homedir_mck): - find_exe_mck.return_value = os.path.normpath('/foo/shims/ruby') - assert helpers.exe_exists('ruby') is False - - -def test_exe_exists_false_if_homedir(find_exe_mck, homedir_mck): - find_exe_mck.return_value = os.path.normpath('/home/me/somedir/ruby') - assert helpers.exe_exists('ruby') is False - - -def test_exe_exists_commonpath_raises_ValueError(find_exe_mck, homedir_mck): - find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') - with mock.patch.object(os.path, 'commonpath', side_effect=ValueError): - assert helpers.exe_exists('ruby') is True - - -def test_exe_exists_true_when_homedir_is_slash(find_exe_mck): - find_exe_mck.return_value = os.path.normpath('/usr/bin/ruby') - with mock.patch.object(os.path, 'expanduser', return_value=os.sep): - assert helpers.exe_exists('ruby') is True - - -def test_basic_get_default_version(): - assert helpers.basic_get_default_version() == C.DEFAULT - - -def test_basic_health_check(): - assert helpers.basic_health_check(Prefix('.'), 'default') is None - - -def test_failed_setup_command_does_not_unicode_error(): - script = ( - 'import sys\n' - "sys.stderr.buffer.write(b'\\x81\\xfe')\n" - 'raise SystemExit(1)\n' - ) - - # an assertion that this does not raise `UnicodeError` - with pytest.raises(CalledProcessError): - helpers.run_setup_cmd(Prefix('.'), (sys.executable, '-c', script)) - - -def test_assert_no_additional_deps(): - with pytest.raises(AssertionError) as excinfo: - helpers.assert_no_additional_deps('lang', ['hmmm']) - msg, = excinfo.value.args - assert msg == ( - 'for now, pre-commit does not support additional_dependencies for ' - 'lang -- ' - "you selected `additional_dependencies: ['hmmm']`" - ) - - -def test_target_concurrency_normal(): - with mock.patch.object(multiprocessing, 'cpu_count', return_value=123): - with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency() == 123 - - -def test_target_concurrency_testing_env_var(): - with mock.patch.dict( - os.environ, {'PRE_COMMIT_NO_CONCURRENCY': '1'}, clear=True, - ): - assert helpers.target_concurrency() == 1 - - -def test_target_concurrency_on_travis(): - with mock.patch.dict(os.environ, {'TRAVIS': '1'}, clear=True): - assert helpers.target_concurrency() == 2 - - -def test_target_concurrency_cpu_count_not_implemented(): - with mock.patch.object( - multiprocessing, 'cpu_count', side_effect=NotImplementedError, - ): - with mock.patch.dict(os.environ, {}, clear=True): - assert helpers.target_concurrency() == 1 - - -def test_shuffled_is_deterministic(): - seq = [str(i) for i in range(10)] - expected = ['4', '0', '5', '1', '8', '6', '2', '3', '7', '9'] - assert helpers._shuffled(seq) == expected - - -def test_xargs_require_serial_is_not_shuffled(): - ret, out = helpers.run_xargs( - ('echo',), [str(i) for i in range(10)], - require_serial=True, - color=False, - ) - assert ret == 0 - assert out.strip() == b'0 1 2 3 4 5 6 7 8 9' diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py index 8420046..c6271c8 100644 --- a/tests/languages/pygrep_test.py +++ b/tests/languages/pygrep_test.py @@ -3,6 +3,7 @@ from __future__ import annotations import pytest from pre_commit.languages import pygrep +from testing.language_helpers import run_language @pytest.fixture @@ -13,6 +14,9 @@ def some_files(tmpdir): tmpdir.join('f4').write_binary(b'foo\npattern\nbar\n') tmpdir.join('f5').write_binary(b'[INFO] hi\npattern\nbar') tmpdir.join('f6').write_binary(b"pattern\nbarwith'foo\n") + tmpdir.join('f7').write_binary(b"hello'hi\nworld\n") + tmpdir.join('f8').write_binary(b'foo\nbar\nbaz\n') + tmpdir.join('f9').write_binary(b'[WARN] hi\n') with tmpdir.as_cwd(): yield @@ -125,3 +129,16 @@ def test_multiline_multiline_flag_is_enabled(cap_out): out = cap_out.get() assert ret == 1 assert out == 'f1:1:foo\nbar\n' + + +def test_grep_hook_matching(some_files, tmp_path): + ret = run_language( + tmp_path, pygrep, 'ello', file_args=('f7', 'f8', 'f9'), + ) + assert ret == (1, b"f7:1:hello'hi\n") + + +@pytest.mark.parametrize('regex', ('nope', "foo'bar", r'^\[INFO\]')) +def test_grep_hook_not_matching(regex, some_files, tmp_path): + ret = run_language(tmp_path, pygrep, regex, file_args=('f7', 'f8', 'f9')) + assert ret == (0, b'') diff --git a/tests/languages/python_test.py b/tests/languages/python_test.py index 54fb98f..8bb284e 100644 --- a/tests/languages/python_test.py +++ b/tests/languages/python_test.py @@ -12,6 +12,7 @@ from pre_commit.languages import python from pre_commit.prefix import Prefix from pre_commit.util import make_executable from pre_commit.util import win_exe +from testing.language_helpers import run_language def test_read_pyvenv_cfg(tmpdir): @@ -210,3 +211,25 @@ def test_unhealthy_then_replaced(python_dir): os.replace(f'{py_exe}.tmp', py_exe) assert python.health_check(prefix, C.DEFAULT) is None + + +def test_language_versioned_python_hook(tmp_path): + setup_py = '''\ +from setuptools import setup +setup( + name='example', + py_modules=['mod'], + entry_points={'console_scripts': ['myexe=mod:main']}, +) +''' + tmp_path.joinpath('setup.py').write_text(setup_py) + tmp_path.joinpath('mod.py').write_text('def main(): print("ohai")') + + # we patch this to force virtualenv executing with `-p` since we can't + # reliably have multiple pythons available in CI + with mock.patch.object( + python, + '_sys_executable_matches', + return_value=False, + ): + assert run_language(tmp_path, python, 'myexe') == (0, b'ohai\n') diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py index 9cfaad5..6397a43 100644 --- a/tests/languages/ruby_test.py +++ b/tests/languages/ruby_test.py @@ -9,8 +9,8 @@ import pre_commit.constants as C from pre_commit import parse_shebang from pre_commit.envcontext import envcontext from pre_commit.languages import ruby +from pre_commit.languages.ruby import _resource_bytesio from pre_commit.store import _make_local_repo -from pre_commit.util import resource_bytesio from testing.language_helpers import run_language from testing.util import cwd from testing.util import xfailif_windows @@ -40,7 +40,7 @@ def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck): ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'), ) def test_archive_root_stat(filename): - with resource_bytesio(filename) as f: + with _resource_bytesio(filename) as f: with tarfile.open(fileobj=f) as tarf: root, _, _ = filename.partition('.') assert oct(tarf.getmember(root).mode) == '0o755' diff --git a/tests/languages/script_test.py b/tests/languages/script_test.py new file mode 100644 index 0000000..a02f615 --- /dev/null +++ b/tests/languages/script_test.py @@ -0,0 +1,14 @@ +from __future__ import annotations + +from pre_commit.languages import script +from pre_commit.util import make_executable +from testing.language_helpers import run_language + + +def test_script_language(tmp_path): + exe = tmp_path.joinpath('main') + exe.write_text('#!/usr/bin/env bash\necho hello hello world\n') + make_executable(exe) + + expected = (0, b'hello hello world\n') + assert run_language(tmp_path, script, 'main') == expected diff --git a/tests/languages/system_test.py b/tests/languages/system_test.py new file mode 100644 index 0000000..dcd9cf1 --- /dev/null +++ b/tests/languages/system_test.py @@ -0,0 +1,9 @@ +from __future__ import annotations + +from pre_commit.languages import system +from testing.language_helpers import run_language + + +def test_system_language(tmp_path): + expected = (0, b'hello hello world\n') + assert run_language(tmp_path, system, 'echo hello hello world') == expected diff --git a/tests/repository_test.py b/tests/repository_test.py index b43b344..9e5d9d6 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -10,15 +10,12 @@ import pytest import re_assert import pre_commit.constants as C -from pre_commit import git +from pre_commit import lang_base +from pre_commit.all_languages import languages 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 python -from pre_commit.languages.all import languages from pre_commit.prefix import Prefix from pre_commit.repository import _hook_installed from pre_commit.repository import all_hooks @@ -28,26 +25,24 @@ 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.language_helpers import run_language from testing.util import cwd from testing.util import get_resource_path -from testing.util import skipif_cant_run_docker - - -def _norm_out(b): - return b.replace(b'\r\n', b'\n') def _hook_run(hook, filenames, color): - with languages[hook.language].in_env(hook.prefix, hook.language_version): - return languages[hook.language].run_hook( - hook.prefix, - hook.entry, - hook.args, - filenames, - is_local=hook.src == 'local', - require_serial=hook.require_serial, - color=color, - ) + return run_language( + path=hook.prefix.prefix_dir, + language=languages[hook.language], + exe=hook.entry, + args=hook.args, + file_args=filenames, + version=hook.language_version, + deps=hook.additional_dependencies, + is_local=hook.src == 'local', + require_serial=hook.require_serial, + color=color, + ) def _get_hook_no_install(repo_config, store, hook_id): @@ -81,7 +76,7 @@ def _test_hook_repo( 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 + assert out == expected def test_python_hook(tempdir_factory, store): @@ -129,66 +124,21 @@ def test_python_hook_weird_setup_cfg(in_git_dir, tempdir_factory, store): ) -def test_python_venv(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'python_venv_hooks_repo', - 'foo', [os.devnull], - f'[{os.devnull!r}]\nHello World\n'.encode(), - ) - - -def test_language_versioned_python_hook(tempdir_factory, store): - # we patch this force virtualenv executing with `-p` since we can't - # reliably have multiple pythons available in CI - with mock.patch.object( - python, - '_sys_executable_matches', - return_value=False, - ): - _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_python_venv_deprecation(store, caplog): + config = { + 'repo': 'local', + 'hooks': [{ + 'id': 'example', + 'name': 'example', + 'language': 'python_venv', + 'entry': 'echo hi', + }], + } + _get_hook(config, store, 'example') + assert caplog.messages[-1] == ( + '`repo: local` uses deprecated `language: python_venv`. ' + 'This is an alias for `language: python`. ' + 'Often `pre-commit autoupdate --repo local` will fix this.' ) @@ -199,92 +149,6 @@ def test_system_hook_with_spaces(tempdir_factory, store): ) -def test_golang_system_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', ['system'], b'hello world from system\n', - config_kwargs={ - 'hooks': [{ - 'id': 'golang-hook', - 'language_version': 'system', - }], - }, - ) - - -def test_golang_versioned_hook(tempdir_factory, store): - _test_hook_repo( - tempdir_factory, store, 'golang_hooks_repo', - 'golang-hook', [], b'hello world from go1.18.4\n', - config_kwargs={ - 'hooks': [{ - 'id': 'golang-hook', - 'language_version': '1.18.4', - }], - }, - ) - - -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_system_hook(tempdir_factory, store) - assert os.listdir(gobin_dir) == [] - - -def test_golang_with_recursive_submodule(tmpdir, tempdir_factory, store): - sub_go = '''\ -package sub - -import "fmt" - -func Func() { - fmt.Println("hello hello world") -} -''' - sub = tmpdir.join('sub').ensure_dir() - sub.join('sub.go').write(sub_go) - cmd_output('git', '-C', str(sub), 'init', '.') - cmd_output('git', '-C', str(sub), 'add', '.') - git.commit(str(sub)) - - pre_commit_hooks = '''\ -- id: example - name: example - entry: example - language: golang - verbose: true -''' - go_mod = '''\ -module github.com/asottile/example - -go 1.14 -''' - main_go = '''\ -package main - -import "github.com/asottile/example/sub" - -func main() { - sub.Func() -} -''' - repo = tmpdir.join('repo').ensure_dir() - repo.join('.pre-commit-hooks.yaml').write(pre_commit_hooks) - repo.join('go.mod').write(go_mod) - repo.join('main.go').write(main_go) - cmd_output('git', '-C', str(repo), 'init', '.') - cmd_output('git', '-C', str(repo), 'add', '.') - cmd_output('git', '-C', str(repo), 'submodule', 'add', str(sub), 'sub') - git.commit(str(repo)) - - config = make_config_from_repo(str(repo)) - hook = _get_hook(config, store, 'example') - ret, out = _hook_run(hook, (), color=False) - assert ret == 0 - assert _norm_out(out) == b'hello hello world\n' - - def test_missing_executable(tempdir_factory, store): _test_hook_repo( tempdir_factory, store, 'not_found_exe', @@ -345,52 +209,6 @@ def test_output_isatty(tempdir_factory, store): ) -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 @@ -440,7 +258,7 @@ def test_repository_state_compatibility(tempdir_factory, store, v): config = make_config_from_repo(path) hook = _get_hook(config, store, 'foo') - envdir = helpers.environment_dir( + envdir = lang_base.environment_dir( hook.prefix, python.ENVIRONMENT_DIR, hook.language_version, @@ -449,67 +267,6 @@ def test_repository_state_compatibility(tempdir_factory, store, v): assert _hook_installed(hook) is True -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 = ['golang.org/x/example/hello@latest'] - config['hooks'][0]['additional_dependencies'] = deps - hook = _get_hook(config, store, 'golang-hook') - envdir = helpers.environment_dir( - hook.prefix, - golang.ENVIRONMENT_DIR, - golang.get_default_version(), - ) - binaries = os.listdir(os.path.join(envdir, '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': ['golang.org/x/example/hello@latest'], - }], - } - 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_fail_hooks(store): - config = { - 'repo': 'local', - 'hooks': [{ - 'id': 'fail', - 'name': 'fail', - 'language': 'fail', - 'entry': 'make sure to name changelogs as .rst!', - 'files': r'changelog/.*(?