summaryrefslogtreecommitdiffstats
path: root/tests/languages
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 18:05:20 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-15 18:05:20 +0000
commitc86df75ab11643fa4649cfe6ed5c4692d4ee342b (patch)
treede847f47ec2669e74b9a3459319579346b7c99df /tests/languages
parentInitial commit. (diff)
downloadpre-commit-c86df75ab11643fa4649cfe6ed5c4692d4ee342b.tar.xz
pre-commit-c86df75ab11643fa4649cfe6ed5c4692d4ee342b.zip
Adding upstream version 3.6.2.upstream/3.6.2
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests/languages')
-rw-r--r--tests/languages/__init__.py0
-rw-r--r--tests/languages/conda_test.py72
-rw-r--r--tests/languages/coursier_test.py45
-rw-r--r--tests/languages/dart_test.py62
-rw-r--r--tests/languages/docker_image_test.py27
-rw-r--r--tests/languages/docker_test.py197
-rw-r--r--tests/languages/dotnet_test.py154
-rw-r--r--tests/languages/fail_test.py14
-rw-r--r--tests/languages/golang_test.py167
-rw-r--r--tests/languages/haskell_test.py50
-rw-r--r--tests/languages/lua_test.py58
-rw-r--r--tests/languages/node_test.py152
-rw-r--r--tests/languages/perl_test.py69
-rw-r--r--tests/languages/pygrep_test.py144
-rw-r--r--tests/languages/python_test.py286
-rw-r--r--tests/languages/r_test.py223
-rw-r--r--tests/languages/ruby_test.py139
-rw-r--r--tests/languages/rust_test.py106
-rw-r--r--tests/languages/script_test.py14
-rw-r--r--tests/languages/swift_test.py31
-rw-r--r--tests/languages/system_test.py9
21 files changed, 2019 insertions, 0 deletions
diff --git a/tests/languages/__init__.py b/tests/languages/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/languages/__init__.py
diff --git a/tests/languages/conda_test.py b/tests/languages/conda_test.py
new file mode 100644
index 0000000..83aaebe
--- /dev/null
+++ b/tests/languages/conda_test.py
@@ -0,0 +1,72 @@
+from __future__ import annotations
+
+import os.path
+
+import pytest
+
+from pre_commit import envcontext
+from pre_commit.languages import conda
+from pre_commit.store import _make_local_repo
+from testing.language_helpers import run_language
+
+
+@pytest.mark.parametrize(
+ ('ctx', 'expected'),
+ (
+ pytest.param(
+ (
+ ('PRE_COMMIT_USE_MICROMAMBA', envcontext.UNSET),
+ ('PRE_COMMIT_USE_MAMBA', envcontext.UNSET),
+ ),
+ 'conda',
+ id='default',
+ ),
+ pytest.param(
+ (
+ ('PRE_COMMIT_USE_MICROMAMBA', '1'),
+ ('PRE_COMMIT_USE_MAMBA', ''),
+ ),
+ 'micromamba',
+ id='default',
+ ),
+ pytest.param(
+ (
+ ('PRE_COMMIT_USE_MICROMAMBA', ''),
+ ('PRE_COMMIT_USE_MAMBA', '1'),
+ ),
+ 'mamba',
+ id='default',
+ ),
+ ),
+)
+def test_conda_exe(ctx, expected):
+ with envcontext.envcontext(ctx):
+ assert conda._conda_exe() == expected
+
+
+def test_conda_language(tmp_path):
+ environment_yml = '''\
+channels: [conda-forge, defaults]
+dependencies: [python, pip]
+'''
+ tmp_path.joinpath('environment.yml').write_text(environment_yml)
+
+ ret, out = run_language(
+ tmp_path,
+ conda,
+ 'python -c "import sys; print(sys.prefix)"',
+ )
+ assert ret == 0
+ assert os.path.basename(out.strip()) == b'conda-default'
+
+
+def test_conda_additional_deps(tmp_path):
+ _make_local_repo(tmp_path)
+
+ ret = run_language(
+ tmp_path,
+ conda,
+ 'python -c "import botocore; print(1)"',
+ deps=('botocore',),
+ )
+ assert ret == (0, b'1\n')
diff --git a/tests/languages/coursier_test.py b/tests/languages/coursier_test.py
new file mode 100644
index 0000000..dbb746c
--- /dev/null
+++ b/tests/languages/coursier_test.py
@@ -0,0 +1,45 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit.errors import FatalError
+from pre_commit.languages import coursier
+from testing.language_helpers import run_language
+
+
+def test_coursier_hook(tmp_path):
+ echo_java_json = '''\
+{
+ "repositories": ["central"],
+ "dependencies": ["io.get-coursier:echo:latest.stable"]
+}
+'''
+
+ channel_dir = tmp_path.joinpath('.pre-commit-channel')
+ channel_dir.mkdir()
+ channel_dir.joinpath('echo-java.json').write_text(echo_java_json)
+
+ ret = run_language(
+ tmp_path,
+ coursier,
+ 'echo-java',
+ args=('Hello', 'World', 'from', 'coursier'),
+ )
+ assert ret == (0, b'Hello World from coursier\n')
+
+
+def test_coursier_hook_additional_dependencies(tmp_path):
+ ret = run_language(
+ tmp_path,
+ coursier,
+ 'scalafmt --version',
+ deps=('scalafmt:3.6.1',),
+ )
+ assert ret == (0, b'scalafmt 3.6.1\n')
+
+
+def test_error_if_no_deps_or_channel(tmp_path):
+ with pytest.raises(FatalError) as excinfo:
+ run_language(tmp_path, coursier, 'dne')
+ msg, = excinfo.value.args
+ assert msg == 'expected .pre-commit-channel dir or additional_dependencies'
diff --git a/tests/languages/dart_test.py b/tests/languages/dart_test.py
new file mode 100644
index 0000000..5bb5aa6
--- /dev/null
+++ b/tests/languages/dart_test.py
@@ -0,0 +1,62 @@
+from __future__ import annotations
+
+import re_assert
+
+from pre_commit.languages import dart
+from pre_commit.store import _make_local_repo
+from testing.language_helpers import run_language
+
+
+def test_dart(tmp_path):
+ pubspec_yaml = '''\
+environment:
+ sdk: '>=2.10.0 <3.0.0'
+
+name: hello_world_dart
+
+executables:
+ hello-world-dart:
+
+dependencies:
+ ansicolor: ^2.0.1
+'''
+ hello_world_dart_dart = '''\
+import 'package:ansicolor/ansicolor.dart';
+
+void main() {
+ AnsiPen pen = new AnsiPen()..red();
+ print("hello hello " + pen("world"));
+}
+'''
+ tmp_path.joinpath('pubspec.yaml').write_text(pubspec_yaml)
+ bin_dir = tmp_path.joinpath('bin')
+ bin_dir.mkdir()
+ bin_dir.joinpath('hello-world-dart.dart').write_text(hello_world_dart_dart)
+
+ expected = (0, b'hello hello world\n')
+ assert run_language(tmp_path, dart, 'hello-world-dart') == expected
+
+
+def test_dart_additional_deps(tmp_path):
+ _make_local_repo(str(tmp_path))
+
+ ret = run_language(
+ tmp_path,
+ dart,
+ 'hello-world-dart',
+ deps=('hello_world_dart',),
+ )
+ assert ret == (0, b'hello hello world\n')
+
+
+def test_dart_additional_deps_versioned(tmp_path):
+ _make_local_repo(str(tmp_path))
+
+ ret, out = run_language(
+ tmp_path,
+ dart,
+ 'secure-random -l 4 -b 16',
+ deps=('encrypt:5.0.0',),
+ )
+ assert ret == 0
+ re_assert.Matches('^[a-f0-9]{8}\n$').assert_matches(out.decode())
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
new file mode 100644
index 0000000..836382a
--- /dev/null
+++ b/tests/languages/docker_test.py
@@ -0,0 +1,197 @@
+from __future__ import annotations
+
+import builtins
+import json
+import ntpath
+import os.path
+import posixpath
+from unittest import mock
+
+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
+11:blkio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+10:freezer:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+9:cpu,cpuacct:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+8:pids:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+7:rdma:/
+6:net_cls,net_prio:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+5:cpuset:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+4:devices:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+3:memory:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+2:perf_event:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+1:name=systemd:/docker/c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7
+0::/system.slice/containerd.service
+''' # noqa: E501
+
+# The ID should match the above cgroup example.
+CONTAINER_ID = 'c33988ec7651ebc867cb24755eaf637a6734088bc7eef59d5799293a9e5450f7' # noqa: E501
+
+NON_DOCKER_CGROUP_EXAMPLE = b'''\
+12:perf_event:/
+11:hugetlb:/
+10:devices:/
+9:blkio:/
+8:rdma:/
+7:cpuset:/
+6:cpu,cpuacct:/
+5:freezer:/
+4:memory:/
+3:pids:/
+2:net_cls,net_prio:/
+1:name=systemd:/init.scope
+0::/init.scope
+'''
+
+
+def test_docker_fallback_user():
+ def invalid_attribute():
+ raise AttributeError
+
+ with mock.patch.multiple(
+ 'os', create=True,
+ getuid=invalid_attribute,
+ getgid=invalid_attribute,
+ ):
+ assert docker.get_docker_user() == ()
+
+
+def test_in_docker_no_file():
+ with mock.patch.object(builtins, 'open', side_effect=FileNotFoundError):
+ assert docker._is_in_docker() is False
+
+
+def _mock_open(data):
+ return mock.patch.object(
+ builtins,
+ 'open',
+ new_callable=mock.mock_open,
+ read_data=data,
+ )
+
+
+def test_in_docker_docker_in_file():
+ with _mock_open(DOCKER_CGROUP_EXAMPLE):
+ assert docker._is_in_docker() is True
+
+
+def test_in_docker_docker_not_in_file():
+ with _mock_open(NON_DOCKER_CGROUP_EXAMPLE):
+ assert docker._is_in_docker() is False
+
+
+def test_get_container_id():
+ with _mock_open(DOCKER_CGROUP_EXAMPLE):
+ assert docker._get_container_id() == CONTAINER_ID
+
+
+def test_get_container_id_failure():
+ with _mock_open(b''), pytest.raises(RuntimeError):
+ docker._get_container_id()
+
+
+def test_get_docker_path_not_in_docker_returns_same():
+ with mock.patch.object(docker, '_is_in_docker', return_value=False):
+ assert docker._get_docker_path('abc') == 'abc'
+
+
+@pytest.fixture
+def in_docker():
+ with mock.patch.object(docker, '_is_in_docker', return_value=True):
+ with mock.patch.object(
+ docker, '_get_container_id', return_value=CONTAINER_ID,
+ ):
+ yield
+
+
+def _linux_commonpath():
+ return mock.patch.object(os.path, 'commonpath', posixpath.commonpath)
+
+
+def _nt_commonpath():
+ return mock.patch.object(os.path, 'commonpath', ntpath.commonpath)
+
+
+def _docker_output(out):
+ ret = (0, out, b'')
+ return mock.patch.object(docker, 'cmd_output_b', return_value=ret)
+
+
+def test_get_docker_path_in_docker_no_binds_same_path(in_docker):
+ docker_out = json.dumps([{'Mounts': []}]).encode()
+
+ with _docker_output(docker_out):
+ assert docker._get_docker_path('abc') == 'abc'
+
+
+def test_get_docker_path_in_docker_binds_path_equal(in_docker):
+ binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}]
+ docker_out = json.dumps([{'Mounts': binds_list}]).encode()
+
+ with _linux_commonpath(), _docker_output(docker_out):
+ assert docker._get_docker_path('/project') == '/opt/my_code'
+
+
+def test_get_docker_path_in_docker_binds_path_complex(in_docker):
+ binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}]
+ docker_out = json.dumps([{'Mounts': binds_list}]).encode()
+
+ with _linux_commonpath(), _docker_output(docker_out):
+ path = '/project/test/something'
+ assert docker._get_docker_path(path) == '/opt/my_code/test/something'
+
+
+def test_get_docker_path_in_docker_no_substring(in_docker):
+ binds_list = [{'Source': '/opt/my_code', 'Destination': '/project'}]
+ docker_out = json.dumps([{'Mounts': binds_list}]).encode()
+
+ with _linux_commonpath(), _docker_output(docker_out):
+ path = '/projectSuffix/test/something'
+ assert docker._get_docker_path(path) == path
+
+
+def test_get_docker_path_in_docker_binds_path_many_binds(in_docker):
+ binds_list = [
+ {'Source': '/something_random', 'Destination': '/not-related'},
+ {'Source': '/opt/my_code', 'Destination': '/project'},
+ {'Source': '/something-random-2', 'Destination': '/not-related-2'},
+ ]
+ docker_out = json.dumps([{'Mounts': binds_list}]).encode()
+
+ with _linux_commonpath(), _docker_output(docker_out):
+ assert docker._get_docker_path('/project') == '/opt/my_code'
+
+
+def test_get_docker_path_in_docker_windows(in_docker):
+ binds_list = [{'Source': r'c:\users\user', 'Destination': r'c:\folder'}]
+ docker_out = json.dumps([{'Mounts': binds_list}]).encode()
+
+ with _nt_commonpath(), _docker_output(docker_out):
+ path = r'c:\folder\test\something'
+ expected = r'c:\users\user\test\something'
+ assert docker._get_docker_path(path) == expected
+
+
+def test_get_docker_path_in_docker_docker_in_docker(in_docker):
+ # won't be able to discover "self" container in true 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
new file mode 100644
index 0000000..470c03b
--- /dev/null
+++ 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'''\
+<Project Sdk="Microsoft.NET.Sdk">
+ <PropertyGroup>
+ <OutputType>Exe</OutputType>
+ <TargetFramework>net6</TargetFramework>
+ <PackAsTool>true</PackAsTool>
+ <ToolCommandName>{tool_name}</ToolCommandName>
+ <PackageOutputPath>./nupkg</PackageOutputPath>
+ </PropertyGroup>
+</Project>
+'''
+
+
+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
new file mode 100644
index 0000000..02e35d7
--- /dev/null
+++ b/tests/languages/golang_test.py
@@ -0,0 +1,167 @@
+from __future__ import annotations
+
+from unittest import mock
+
+import pytest
+import re_assert
+
+import pre_commit.constants as C
+from pre_commit import lang_base
+from pre_commit.commands.install_uninstall import install
+from pre_commit.envcontext import envcontext
+from pre_commit.languages import golang
+from pre_commit.store import _make_local_repo
+from pre_commit.util import cmd_output
+from testing.fixtures import add_config_to_repo
+from testing.fixtures import make_config_from_repo
+from testing.language_helpers import run_language
+from testing.util import cmd_output_mocked_pre_commit_home
+from testing.util import git_commit
+
+
+ACTUAL_GET_DEFAULT_VERSION = golang.get_default_version.__wrapped__
+
+
+@pytest.fixture
+def exe_exists_mck():
+ with mock.patch.object(lang_base, 'exe_exists') as mck:
+ yield mck
+
+
+def test_golang_default_version_system_available(exe_exists_mck):
+ exe_exists_mck.return_value = True
+ assert ACTUAL_GET_DEFAULT_VERSION() == 'system'
+
+
+def test_golang_default_version_system_not_available(exe_exists_mck):
+ exe_exists_mck.return_value = False
+ assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT
+
+
+ACTUAL_INFER_GO_VERSION = golang._infer_go_version.__wrapped__
+
+
+def test_golang_infer_go_version_not_default():
+ assert ACTUAL_INFER_GO_VERSION('1.19.4') == '1.19.4'
+
+
+def test_golang_infer_go_version_default():
+ version = ACTUAL_INFER_GO_VERSION(C.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.21.1',
+ )
+
+ assert ret == 0
+ assert out.startswith(b'go version go1.21.1')
+
+
+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, world!\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)
+
+
+def test_during_commit_all(tmp_path, tempdir_factory, store, in_git_dir):
+ hook_dir = tmp_path.joinpath('hook')
+ hook_dir.mkdir()
+ _make_hello_world(hook_dir)
+ hook_dir.joinpath('.pre-commit-hooks.yaml').write_text(
+ '- id: hello-world\n'
+ ' name: hello world\n'
+ ' entry: golang-hello-world\n'
+ ' language: golang\n'
+ ' always_run: true\n',
+ )
+ cmd_output('git', 'init', hook_dir)
+ cmd_output('git', 'add', '.', cwd=hook_dir)
+ git_commit(cwd=hook_dir)
+
+ add_config_to_repo(in_git_dir, make_config_from_repo(hook_dir))
+
+ assert not install(C.CONFIG_FILE, store, hook_types=['pre-commit'])
+
+ git_commit(
+ fn=cmd_output_mocked_pre_commit_home,
+ tempdir_factory=tempdir_factory,
+ )
diff --git a/tests/languages/haskell_test.py b/tests/languages/haskell_test.py
new file mode 100644
index 0000000..f888109
--- /dev/null
+++ b/tests/languages/haskell_test.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit.errors import FatalError
+from pre_commit.languages import haskell
+from pre_commit.util import win_exe
+from testing.language_helpers import run_language
+
+
+def test_run_example_executable(tmp_path):
+ example_cabal = '''\
+cabal-version: 2.4
+name: example
+version: 0.1.0.0
+
+executable example
+ main-is: Main.hs
+
+ build-depends: base >=4
+ default-language: Haskell2010
+'''
+ main_hs = '''\
+module Main where
+
+main :: IO ()
+main = putStrLn "Hello, Haskell!"
+'''
+ tmp_path.joinpath('example.cabal').write_text(example_cabal)
+ tmp_path.joinpath('Main.hs').write_text(main_hs)
+
+ result = run_language(tmp_path, haskell, 'example')
+ assert result == (0, b'Hello, Haskell!\n')
+
+ # should not symlink things into environments
+ exe = tmp_path.joinpath(win_exe('hs_env-default/bin/example'))
+ assert exe.is_file()
+ assert not exe.is_symlink()
+
+
+def test_run_dep(tmp_path):
+ result = run_language(tmp_path, haskell, 'hello', deps=['hello'])
+ assert result == (0, b'Hello, World!\n')
+
+
+def test_run_empty(tmp_path):
+ with pytest.raises(FatalError) as excinfo:
+ run_language(tmp_path, haskell, 'example')
+ msg, = excinfo.value.args
+ assert msg == 'Expected .cabal files or additional_dependencies'
diff --git a/tests/languages/lua_test.py b/tests/languages/lua_test.py
new file mode 100644
index 0000000..b2767b7
--- /dev/null
+++ b/tests/languages/lua_test.py
@@ -0,0 +1,58 @@
+from __future__ import annotations
+
+import sys
+
+import pytest
+
+from pre_commit.languages import lua
+from pre_commit.util import make_executable
+from testing.language_helpers import run_language
+
+pytestmark = pytest.mark.skipif(
+ sys.platform == 'win32',
+ reason='lua is not supported on windows',
+)
+
+
+def test_lua(tmp_path): # pragma: win32 no cover
+ rockspec = '''\
+package = "hello"
+version = "dev-1"
+
+source = {
+ url = "git+ssh://git@github.com/pre-commit/pre-commit.git"
+}
+description = {}
+dependencies = {}
+build = {
+ type = "builtin",
+ modules = {},
+ install = {
+ bin = {"bin/hello-world-lua"}
+ },
+}
+'''
+ hello_world_lua = '''\
+#!/usr/bin/env lua
+print('hello world')
+'''
+ tmp_path.joinpath('hello-dev-1.rockspec').write_text(rockspec)
+ bin_dir = tmp_path.joinpath('bin')
+ bin_dir.mkdir()
+ bin_file = bin_dir.joinpath('hello-world-lua')
+ bin_file.write_text(hello_world_lua)
+ make_executable(bin_file)
+
+ expected = (0, b'hello world\n')
+ assert run_language(tmp_path, lua, 'hello-world-lua') == expected
+
+
+def test_lua_additional_dependencies(tmp_path): # pragma: win32 no cover
+ ret, out = run_language(
+ tmp_path,
+ lua,
+ 'luacheck --version',
+ deps=('luacheck',),
+ )
+ assert ret == 0
+ assert out.startswith(b'Luacheck: ')
diff --git a/tests/languages/node_test.py b/tests/languages/node_test.py
new file mode 100644
index 0000000..055cb1e
--- /dev/null
+++ b/tests/languages/node_test.py
@@ -0,0 +1,152 @@
+from __future__ import annotations
+
+import json
+import os
+import shutil
+import sys
+from unittest import mock
+
+import pytest
+
+import pre_commit.constants as C
+from pre_commit import envcontext
+from pre_commit import parse_shebang
+from pre_commit.languages import node
+from pre_commit.prefix import Prefix
+from pre_commit.store import _make_local_repo
+from pre_commit.util import cmd_output
+from testing.language_helpers import run_language
+from testing.util import xfailif_windows
+
+
+ACTUAL_GET_DEFAULT_VERSION = node.get_default_version.__wrapped__
+
+
+@pytest.fixture
+def is_linux():
+ with mock.patch.object(sys, 'platform', 'linux'):
+ yield
+
+
+@pytest.fixture
+def is_win32():
+ with mock.patch.object(sys, 'platform', 'win32'):
+ yield
+
+
+@pytest.fixture
+def find_exe_mck():
+ with mock.patch.object(parse_shebang, 'find_executable') as mck:
+ yield mck
+
+
+@pytest.mark.usefixtures('is_linux')
+def test_sets_system_when_node_and_npm_are_available(find_exe_mck):
+ find_exe_mck.return_value = '/path/to/exe'
+ assert ACTUAL_GET_DEFAULT_VERSION() == 'system'
+
+
+@pytest.mark.usefixtures('is_linux')
+def test_uses_default_when_node_and_npm_are_not_available(find_exe_mck):
+ find_exe_mck.return_value = None
+ assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT
+
+
+@pytest.mark.usefixtures('is_win32')
+def test_sets_default_on_windows(find_exe_mck):
+ find_exe_mck.return_value = '/path/to/exe'
+ assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_healthy_system_node(tmpdir):
+ tmpdir.join('package.json').write('{"name": "t", "version": "1.0.0"}')
+
+ prefix = Prefix(str(tmpdir))
+ node.install_environment(prefix, 'system', ())
+ assert node.health_check(prefix, 'system') is None
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_unhealthy_if_system_node_goes_missing(tmpdir):
+ bin_dir = tmpdir.join('bin').ensure_dir()
+ node_bin = bin_dir.join('node')
+ node_bin.mksymlinkto(shutil.which('node'))
+
+ prefix_dir = tmpdir.join('prefix').ensure_dir()
+ prefix_dir.join('package.json').write('{"name": "t", "version": "1.0.0"}')
+
+ path = ('PATH', (str(bin_dir), os.pathsep, envcontext.Var('PATH')))
+ with envcontext.envcontext((path,)):
+ prefix = Prefix(str(prefix_dir))
+ node.install_environment(prefix, 'system', ())
+ assert node.health_check(prefix, 'system') is None
+
+ node_bin.remove()
+ ret = node.health_check(prefix, 'system')
+ assert ret == '`node --version` returned 127'
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_installs_without_links_outside_env(tmpdir):
+ tmpdir.join('bin/main.js').ensure().write(
+ '#!/usr/bin/env node\n'
+ '_ = require("lodash"); console.log("success!")\n',
+ )
+ tmpdir.join('package.json').write(
+ json.dumps({
+ 'name': 'foo',
+ 'version': '0.0.1',
+ 'bin': {'foo': './bin/main.js'},
+ 'dependencies': {'lodash': '*'},
+ }),
+ )
+
+ prefix = Prefix(str(tmpdir))
+ node.install_environment(prefix, 'system', ())
+ assert node.health_check(prefix, 'system') is None
+
+ # this directory shouldn't exist, make sure we succeed without it existing
+ cmd_output('rm', '-rf', str(tmpdir.join('node_modules')))
+
+ with node.in_env(prefix, 'system'):
+ assert cmd_output('foo')[1] == 'success!\n'
+
+
+def _make_hello_world(tmp_path):
+ package_json = '''\
+{"name": "t", "version": "0.0.1", "bin": {"node-hello": "./bin/main.js"}}
+'''
+ tmp_path.joinpath('package.json').write_text(package_json)
+ bin_dir = tmp_path.joinpath('bin')
+ bin_dir.mkdir()
+ bin_dir.joinpath('main.js').write_text(
+ '#!/usr/bin/env node\n'
+ 'console.log("Hello World");\n',
+ )
+
+
+def test_node_hook_system(tmp_path):
+ _make_hello_world(tmp_path)
+ ret = run_language(tmp_path, node, 'node-hello')
+ assert ret == (0, b'Hello World\n')
+
+
+def test_node_with_user_config_set(tmp_path):
+ cfg = tmp_path.joinpath('cfg')
+ cfg.write_text('cache=/dne\n')
+ with envcontext.envcontext((('NPM_CONFIG_USERCONFIG', str(cfg)),)):
+ test_node_hook_system(tmp_path)
+
+
+@pytest.mark.parametrize('version', (C.DEFAULT, '18.14.0'))
+def test_node_hook_versions(tmp_path, version):
+ _make_hello_world(tmp_path)
+ ret = run_language(tmp_path, node, 'node-hello', version=version)
+ assert ret == (0, b'Hello World\n')
+
+
+def test_node_additional_deps(tmp_path):
+ _make_local_repo(str(tmp_path))
+ ret, out = run_language(tmp_path, node, 'npm ls -g', deps=('lodash',))
+ assert b' lodash@' in out
diff --git a/tests/languages/perl_test.py b/tests/languages/perl_test.py
new file mode 100644
index 0000000..042478d
--- /dev/null
+++ b/tests/languages/perl_test.py
@@ -0,0 +1,69 @@
+from __future__ import annotations
+
+from pre_commit.languages import perl
+from pre_commit.store import _make_local_repo
+from pre_commit.util import make_executable
+from testing.language_helpers import run_language
+
+
+def test_perl_install(tmp_path):
+ makefile_pl = '''\
+use strict;
+use warnings;
+
+use ExtUtils::MakeMaker;
+
+WriteMakefile(
+ NAME => "PreCommitHello",
+ VERSION_FROM => "lib/PreCommitHello.pm",
+ EXE_FILES => [qw(bin/pre-commit-perl-hello)],
+);
+'''
+ bin_perl_hello = '''\
+#!/usr/bin/env perl
+
+use strict;
+use warnings;
+use PreCommitHello;
+
+PreCommitHello::hello();
+'''
+ lib_hello_pm = '''\
+package PreCommitHello;
+
+use strict;
+use warnings;
+
+our $VERSION = "0.1.0";
+
+sub hello {
+ print "Hello from perl-commit Perl!\n";
+}
+
+1;
+'''
+ tmp_path.joinpath('Makefile.PL').write_text(makefile_pl)
+ bin_dir = tmp_path.joinpath('bin')
+ bin_dir.mkdir()
+ exe = bin_dir.joinpath('pre-commit-perl-hello')
+ exe.write_text(bin_perl_hello)
+ make_executable(exe)
+ lib_dir = tmp_path.joinpath('lib')
+ lib_dir.mkdir()
+ lib_dir.joinpath('PreCommitHello.pm').write_text(lib_hello_pm)
+
+ ret = run_language(tmp_path, perl, 'pre-commit-perl-hello')
+ assert ret == (0, b'Hello from perl-commit Perl!\n')
+
+
+def test_perl_additional_dependencies(tmp_path):
+ _make_local_repo(str(tmp_path))
+
+ ret, out = run_language(
+ tmp_path,
+ perl,
+ 'perltidy --version',
+ deps=('SHANCOCK/Perl-Tidy-20211029.tar.gz',),
+ )
+ assert ret == 0
+ assert out.startswith(b'This is perltidy, v20211029')
diff --git a/tests/languages/pygrep_test.py b/tests/languages/pygrep_test.py
new file mode 100644
index 0000000..c6271c8
--- /dev/null
+++ b/tests/languages/pygrep_test.py
@@ -0,0 +1,144 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit.languages import pygrep
+from testing.language_helpers import run_language
+
+
+@pytest.fixture
+def some_files(tmpdir):
+ tmpdir.join('f1').write_binary(b'foo\nbar\n')
+ tmpdir.join('f2').write_binary(b'[INFO] hi\n')
+ tmpdir.join('f3').write_binary(b"with'quotes\n")
+ 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
+
+
+@pytest.mark.usefixtures('some_files')
+@pytest.mark.parametrize(
+ ('pattern', 'expected_retcode', 'expected_out'),
+ (
+ ('baz', 0, ''),
+ ('foo', 1, 'f1:1:foo\n'),
+ ('bar', 1, 'f1:2:bar\n'),
+ (r'(?i)\[info\]', 1, 'f2:1:[INFO] hi\n'),
+ ("h'q", 1, "f3:1:with'quotes\n"),
+ ),
+)
+def test_main(cap_out, pattern, expected_retcode, expected_out):
+ ret = pygrep.main((pattern, 'f1', 'f2', 'f3'))
+ out = cap_out.get()
+ assert ret == expected_retcode
+ assert out == expected_out
+
+
+@pytest.mark.usefixtures('some_files')
+def test_negate_by_line_no_match(cap_out):
+ ret = pygrep.main(('pattern\nbar', 'f4', 'f5', 'f6', '--negate'))
+ out = cap_out.get()
+ assert ret == 1
+ assert out == 'f4\nf5\nf6\n'
+
+
+@pytest.mark.usefixtures('some_files')
+def test_negate_by_line_two_match(cap_out):
+ ret = pygrep.main(('foo', 'f4', 'f5', 'f6', '--negate'))
+ out = cap_out.get()
+ assert ret == 1
+ assert out == 'f5\n'
+
+
+@pytest.mark.usefixtures('some_files')
+def test_negate_by_line_all_match(cap_out):
+ ret = pygrep.main(('pattern', 'f4', 'f5', 'f6', '--negate'))
+ out = cap_out.get()
+ assert ret == 0
+ assert out == ''
+
+
+@pytest.mark.usefixtures('some_files')
+def test_negate_by_file_no_match(cap_out):
+ ret = pygrep.main(('baz', 'f4', 'f5', 'f6', '--negate', '--multiline'))
+ out = cap_out.get()
+ assert ret == 1
+ assert out == 'f4\nf5\nf6\n'
+
+
+@pytest.mark.usefixtures('some_files')
+def test_negate_by_file_one_match(cap_out):
+ ret = pygrep.main(
+ ('foo\npattern', 'f4', 'f5', 'f6', '--negate', '--multiline'),
+ )
+ out = cap_out.get()
+ assert ret == 1
+ assert out == 'f5\nf6\n'
+
+
+@pytest.mark.usefixtures('some_files')
+def test_negate_by_file_all_match(cap_out):
+ ret = pygrep.main(
+ ('pattern\nbar', 'f4', 'f5', 'f6', '--negate', '--multiline'),
+ )
+ out = cap_out.get()
+ assert ret == 0
+ assert out == ''
+
+
+@pytest.mark.usefixtures('some_files')
+def test_ignore_case(cap_out):
+ ret = pygrep.main(('--ignore-case', 'info', 'f1', 'f2', 'f3'))
+ out = cap_out.get()
+ assert ret == 1
+ assert out == 'f2:1:[INFO] hi\n'
+
+
+@pytest.mark.usefixtures('some_files')
+def test_multiline(cap_out):
+ ret = pygrep.main(('--multiline', r'foo\nbar', 'f1', 'f2', 'f3'))
+ out = cap_out.get()
+ assert ret == 1
+ assert out == 'f1:1:foo\nbar\n'
+
+
+@pytest.mark.usefixtures('some_files')
+def test_multiline_line_number(cap_out):
+ ret = pygrep.main(('--multiline', r'ar', 'f1', 'f2', 'f3'))
+ out = cap_out.get()
+ assert ret == 1
+ assert out == 'f1:2:bar\n'
+
+
+@pytest.mark.usefixtures('some_files')
+def test_multiline_dotall_flag_is_enabled(cap_out):
+ ret = pygrep.main(('--multiline', r'o.*bar', 'f1', 'f2', 'f3'))
+ out = cap_out.get()
+ assert ret == 1
+ assert out == 'f1:1:foo\nbar\n'
+
+
+@pytest.mark.usefixtures('some_files')
+def test_multiline_multiline_flag_is_enabled(cap_out):
+ ret = pygrep.main(('--multiline', r'foo$.*bar', 'f1', 'f2', 'f3'))
+ 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
new file mode 100644
index 0000000..ab26e14
--- /dev/null
+++ b/tests/languages/python_test.py
@@ -0,0 +1,286 @@
+from __future__ import annotations
+
+import os.path
+import sys
+from unittest import mock
+
+import pytest
+
+import pre_commit.constants as C
+from pre_commit.envcontext import envcontext
+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):
+ pyvenv_cfg = tmpdir.join('pyvenv.cfg')
+ pyvenv_cfg.write(
+ '# I am a comment\n'
+ '\n'
+ 'foo = bar\n'
+ 'version-info=123\n',
+ )
+ expected = {'foo': 'bar', 'version-info': '123'}
+ assert python._read_pyvenv_cfg(pyvenv_cfg) == expected
+
+
+def test_read_pyvenv_cfg_non_utf8(tmpdir):
+ pyvenv_cfg = tmpdir.join('pyvenv_cfg')
+ pyvenv_cfg.write_binary('hello = hello john.š\n'.encode())
+ expected = {'hello': 'hello john.š'}
+ assert python._read_pyvenv_cfg(pyvenv_cfg) == expected
+
+
+def test_norm_version_expanduser():
+ home = os.path.expanduser('~')
+ if sys.platform == 'win32': # pragma: win32 cover
+ path = r'~\python343'
+ expected_path = fr'{home}\python343'
+ else: # pragma: win32 no cover
+ path = '~/.pyenv/versions/3.4.3/bin/python'
+ expected_path = f'{home}/.pyenv/versions/3.4.3/bin/python'
+ result = python.norm_version(path)
+ assert result == expected_path
+
+
+def test_norm_version_of_default_is_sys_executable():
+ assert python.norm_version('default') is None
+
+
+@pytest.mark.parametrize('v', ('python3.9', 'python3', 'python'))
+def test_sys_executable_matches(v):
+ with mock.patch.object(sys, 'version_info', (3, 9, 10)):
+ assert python._sys_executable_matches(v)
+ assert python.norm_version(v) is None
+
+
+@pytest.mark.parametrize('v', ('notpython', 'python3.x'))
+def test_sys_executable_matches_does_not_match(v):
+ with mock.patch.object(sys, 'version_info', (3, 9, 10)):
+ assert not python._sys_executable_matches(v)
+
+
+@pytest.mark.parametrize(
+ ('exe', 'realpath', 'expected'), (
+ ('/usr/bin/python3', '/usr/bin/python3.7', 'python3'),
+ ('/usr/bin/python', '/usr/bin/python3.7', 'python3.7'),
+ ('/usr/bin/python', '/usr/bin/python', None),
+ ('/usr/bin/python3.7m', '/usr/bin/python3.7m', 'python3.7m'),
+ ('v/bin/python', 'v/bin/pypy', 'pypy'),
+ ),
+)
+def test_find_by_sys_executable(exe, realpath, expected):
+ with mock.patch.object(sys, 'executable', exe):
+ with mock.patch.object(os.path, 'realpath', return_value=realpath):
+ with mock.patch.object(python, 'find_executable', lambda x: x):
+ assert python._find_by_sys_executable() == expected
+
+
+@pytest.fixture
+def python_dir(tmpdir):
+ with tmpdir.as_cwd():
+ prefix = tmpdir.join('prefix').ensure_dir()
+ prefix.join('setup.py').write('import setuptools; setuptools.setup()')
+ prefix = Prefix(str(prefix))
+ yield prefix, tmpdir
+
+
+def test_healthy_default_creator(python_dir):
+ prefix, tmpdir = python_dir
+
+ python.install_environment(prefix, C.DEFAULT, ())
+
+ # should be healthy right after creation
+ assert python.health_check(prefix, C.DEFAULT) is None
+
+ # even if a `types.py` file exists, should still be healthy
+ tmpdir.join('types.py').ensure()
+ assert python.health_check(prefix, C.DEFAULT) is None
+
+
+def test_healthy_venv_creator(python_dir):
+ # venv creator produces slightly different pyvenv.cfg
+ prefix, tmpdir = python_dir
+
+ with envcontext((('VIRTUALENV_CREATOR', 'venv'),)):
+ python.install_environment(prefix, C.DEFAULT, ())
+
+ assert python.health_check(prefix, C.DEFAULT) is None
+
+
+def test_unhealthy_python_goes_missing(python_dir):
+ prefix, tmpdir = python_dir
+
+ python.install_environment(prefix, C.DEFAULT, ())
+
+ exe_name = win_exe('python')
+ py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name)
+ os.remove(py_exe)
+
+ ret = python.health_check(prefix, C.DEFAULT)
+ assert ret == (
+ f'virtualenv python version did not match created version:\n'
+ f'- actual version: <<error retrieving version from {py_exe}>>\n'
+ f'- expected version: {python._version_info(sys.executable)}\n'
+ )
+
+
+def test_unhealthy_with_version_change(python_dir):
+ prefix, tmpdir = python_dir
+
+ python.install_environment(prefix, C.DEFAULT, ())
+
+ with open(prefix.path('py_env-default/pyvenv.cfg'), 'a+') as f:
+ f.write('version_info = 1.2.3\n')
+
+ ret = python.health_check(prefix, C.DEFAULT)
+ assert ret == (
+ f'virtualenv python version did not match created version:\n'
+ f'- actual version: {python._version_info(sys.executable)}\n'
+ f'- expected version: 1.2.3\n'
+ )
+
+
+def test_unhealthy_system_version_changes(python_dir):
+ prefix, tmpdir = python_dir
+
+ python.install_environment(prefix, C.DEFAULT, ())
+
+ with open(prefix.path('py_env-default/pyvenv.cfg'), 'a') as f:
+ f.write('base-executable = /does/not/exist\n')
+
+ ret = python.health_check(prefix, C.DEFAULT)
+ assert ret == (
+ f'base executable python version does not match created version:\n'
+ f'- base-executable version: <<error retrieving version from /does/not/exist>>\n' # noqa: E501
+ f'- expected version: {python._version_info(sys.executable)}\n'
+ )
+
+
+def test_unhealthy_old_virtualenv(python_dir):
+ prefix, tmpdir = python_dir
+
+ python.install_environment(prefix, C.DEFAULT, ())
+
+ # simulate "old" virtualenv by deleting this file
+ os.remove(prefix.path('py_env-default/pyvenv.cfg'))
+
+ ret = python.health_check(prefix, C.DEFAULT)
+ assert ret == 'pyvenv.cfg does not exist (old virtualenv?)'
+
+
+def test_unhealthy_unexpected_pyvenv(python_dir):
+ prefix, tmpdir = python_dir
+
+ python.install_environment(prefix, C.DEFAULT, ())
+
+ # simulate a buggy environment build (I don't think this is possible)
+ with open(prefix.path('py_env-default/pyvenv.cfg'), 'w'):
+ pass
+
+ ret = python.health_check(prefix, C.DEFAULT)
+ assert ret == "created virtualenv's pyvenv.cfg is missing `version_info`"
+
+
+def test_unhealthy_then_replaced(python_dir):
+ prefix, tmpdir = python_dir
+
+ python.install_environment(prefix, C.DEFAULT, ())
+
+ # simulate an exe which returns an old version
+ exe_name = win_exe('python')
+ py_exe = prefix.path(python.bin_dir('py_env-default'), exe_name)
+ os.rename(py_exe, f'{py_exe}.tmp')
+
+ with open(py_exe, 'w') as f:
+ f.write('#!/usr/bin/env bash\necho 1.2.3\n')
+ make_executable(py_exe)
+
+ # should be unhealthy due to version mismatch
+ ret = python.health_check(prefix, C.DEFAULT)
+ assert ret == (
+ f'virtualenv python version did not match created version:\n'
+ f'- actual version: 1.2.3\n'
+ f'- expected version: {python._version_info(sys.executable)}\n'
+ )
+
+ # now put the exe back and it should be healthy again
+ 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')
+
+
+def _make_hello_hello(tmp_path):
+ setup_py = '''\
+from setuptools import setup
+
+setup(
+ name='socks',
+ version='0.0.0',
+ py_modules=['socks'],
+ entry_points={'console_scripts': ['socks = socks:main']},
+)
+'''
+
+ main_py = '''\
+import sys
+
+def main():
+ print(repr(sys.argv[1:]))
+ print('hello hello')
+ return 0
+'''
+ tmp_path.joinpath('setup.py').write_text(setup_py)
+ tmp_path.joinpath('socks.py').write_text(main_py)
+
+
+def test_simple_python_hook(tmp_path):
+ _make_hello_hello(tmp_path)
+
+ ret = run_language(tmp_path, python, 'socks', [os.devnull])
+ assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode())
+
+
+def test_simple_python_hook_default_version(tmp_path):
+ # 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_simple_python_hook(tmp_path)
+
+
+def test_python_hook_weird_setup_cfg(tmp_path):
+ _make_hello_hello(tmp_path)
+ setup_cfg = '[install]\ninstall_scripts=/usr/sbin'
+ tmp_path.joinpath('setup.cfg').write_text(setup_cfg)
+
+ ret = run_language(tmp_path, python, 'socks', [os.devnull])
+ assert ret == (0, f'[{os.devnull!r}]\nhello hello\n'.encode())
diff --git a/tests/languages/r_test.py b/tests/languages/r_test.py
new file mode 100644
index 0000000..02c559c
--- /dev/null
+++ b/tests/languages/r_test.py
@@ -0,0 +1,223 @@
+from __future__ import annotations
+
+import os.path
+import shutil
+
+import pytest
+
+from pre_commit import envcontext
+from pre_commit.languages import r
+from pre_commit.prefix import Prefix
+from pre_commit.store import _make_local_repo
+from pre_commit.util import win_exe
+from testing.language_helpers import run_language
+
+
+def test_r_parsing_file_no_opts_no_args(tmp_path):
+ cmd = r._cmd_from_hook(
+ Prefix(str(tmp_path)),
+ 'Rscript some-script.R',
+ (),
+ is_local=False,
+ )
+ assert cmd == (
+ 'Rscript',
+ '--no-save', '--no-restore', '--no-site-file', '--no-environ',
+ str(tmp_path.joinpath('some-script.R')),
+ )
+
+
+def test_r_parsing_file_opts_no_args():
+ with pytest.raises(ValueError) as excinfo:
+ r._entry_validate(['Rscript', '--no-init', '/path/to/file'])
+
+ msg, = excinfo.value.args
+ assert msg == (
+ 'The only valid syntax is `Rscript -e {expr}`'
+ 'or `Rscript path/to/hook/script`'
+ )
+
+
+def test_r_parsing_file_no_opts_args(tmp_path):
+ cmd = r._cmd_from_hook(
+ Prefix(str(tmp_path)),
+ 'Rscript some-script.R',
+ ('--no-cache',),
+ is_local=False,
+ )
+ assert cmd == (
+ 'Rscript',
+ '--no-save', '--no-restore', '--no-site-file', '--no-environ',
+ str(tmp_path.joinpath('some-script.R')),
+ '--no-cache',
+ )
+
+
+def test_r_parsing_expr_no_opts_no_args1(tmp_path):
+ cmd = r._cmd_from_hook(
+ Prefix(str(tmp_path)),
+ "Rscript -e '1+1'",
+ (),
+ is_local=False,
+ )
+ assert cmd == (
+ 'Rscript',
+ '--no-save', '--no-restore', '--no-site-file', '--no-environ',
+ '-e', '1+1',
+ )
+
+
+def test_r_parsing_local_hook_path_is_not_expanded(tmp_path):
+ cmd = r._cmd_from_hook(
+ Prefix(str(tmp_path)),
+ 'Rscript path/to/thing.R',
+ (),
+ is_local=True,
+ )
+ assert cmd == (
+ 'Rscript',
+ '--no-save', '--no-restore', '--no-site-file', '--no-environ',
+ 'path/to/thing.R',
+ )
+
+
+def test_r_parsing_expr_no_opts_no_args2():
+ with pytest.raises(ValueError) as excinfo:
+ r._entry_validate(['Rscript', '-e', '1+1', '-e', 'letters'])
+ msg, = excinfo.value.args
+ assert msg == 'You can supply at most one expression.'
+
+
+def test_r_parsing_expr_opts_no_args2():
+ with pytest.raises(ValueError) as excinfo:
+ r._entry_validate(
+ ['Rscript', '--vanilla', '-e', '1+1', '-e', 'letters'],
+ )
+ msg, = excinfo.value.args
+ assert msg == (
+ 'The only valid syntax is `Rscript -e {expr}`'
+ 'or `Rscript path/to/hook/script`'
+ )
+
+
+def test_r_parsing_expr_args_in_entry2():
+ with pytest.raises(ValueError) as excinfo:
+ r._entry_validate(['Rscript', '-e', 'expr1', '--another-arg'])
+
+ msg, = excinfo.value.args
+ assert msg == 'You can supply at most one expression.'
+
+
+def test_r_parsing_expr_non_Rscirpt():
+ with pytest.raises(ValueError) as excinfo:
+ r._entry_validate(['AnotherScript', '-e', '{{}}'])
+
+ msg, = excinfo.value.args
+ assert msg == 'entry must start with `Rscript`.'
+
+
+def test_rscript_exec_relative_to_r_home():
+ expected = os.path.join('r_home_dir', 'bin', win_exe('Rscript'))
+ with envcontext.envcontext((('R_HOME', 'r_home_dir'),)):
+ assert r._rscript_exec() == expected
+
+
+def test_path_rscript_exec_no_r_home_set():
+ with envcontext.envcontext((('R_HOME', envcontext.UNSET),)):
+ assert r._rscript_exec() == 'Rscript'
+
+
+def test_r_hook(tmp_path):
+ renv_lock = '''\
+{
+ "R": {
+ "Version": "4.0.3",
+ "Repositories": [
+ {
+ "Name": "CRAN",
+ "URL": "https://cloud.r-project.org"
+ }
+ ]
+ },
+ "Packages": {
+ "renv": {
+ "Package": "renv",
+ "Version": "0.12.5",
+ "Source": "Repository",
+ "Repository": "CRAN",
+ "Hash": "5c0cdb37f063c58cdab3c7e9fbb8bd2c"
+ },
+ "rprojroot": {
+ "Package": "rprojroot",
+ "Version": "1.0",
+ "Source": "Repository",
+ "Repository": "CRAN",
+ "Hash": "86704667fe0860e4fec35afdfec137f3"
+ }
+ }
+}
+'''
+ description = '''\
+Package: gli.clu
+Title: What the Package Does (One Line, Title Case)
+Type: Package
+Version: 0.0.0.9000
+Authors@R:
+ person(given = "First",
+ family = "Last",
+ role = c("aut", "cre"),
+ email = "first.last@example.com",
+ comment = c(ORCID = "YOUR-ORCID-ID"))
+Description: What the package does (one paragraph).
+License: `use_mit_license()`, `use_gpl3_license()` or friends to
+ pick a license
+Encoding: UTF-8
+LazyData: true
+Roxygen: list(markdown = TRUE)
+RoxygenNote: 7.1.1
+Imports:
+ rprojroot
+'''
+ hello_world_r = '''\
+stopifnot(
+ packageVersion('rprojroot') == '1.0',
+ packageVersion('gli.clu') == '0.0.0.9000'
+)
+cat("Hello, World, from R!\n")
+'''
+
+ tmp_path.joinpath('renv.lock').write_text(renv_lock)
+ tmp_path.joinpath('DESCRIPTION').write_text(description)
+ tmp_path.joinpath('hello-world.R').write_text(hello_world_r)
+ renv_dir = tmp_path.joinpath('renv')
+ renv_dir.mkdir()
+ shutil.copy(
+ os.path.join(
+ os.path.dirname(__file__),
+ '../../pre_commit/resources/empty_template_activate.R',
+ ),
+ renv_dir.joinpath('activate.R'),
+ )
+
+ expected = (0, b'Hello, World, from R!\n')
+ assert run_language(tmp_path, r, 'Rscript hello-world.R') == expected
+
+
+def test_r_inline(tmp_path):
+ _make_local_repo(str(tmp_path))
+
+ cmd = '''\
+Rscript -e '
+ stopifnot(packageVersion("rprojroot") == "1.0")
+ cat(commandArgs(trailingOnly = TRUE), "from R!\n", sep=", ")
+'
+'''
+
+ ret = run_language(
+ tmp_path,
+ r,
+ cmd,
+ deps=('rprojroot@1.0',),
+ args=('hi', 'hello'),
+ )
+ assert ret == (0, b'hi, hello, from R!\n')
diff --git a/tests/languages/ruby_test.py b/tests/languages/ruby_test.py
new file mode 100644
index 0000000..6397a43
--- /dev/null
+++ b/tests/languages/ruby_test.py
@@ -0,0 +1,139 @@
+from __future__ import annotations
+
+import tarfile
+from unittest import mock
+
+import pytest
+
+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 testing.language_helpers import run_language
+from testing.util import cwd
+from testing.util import xfailif_windows
+
+
+ACTUAL_GET_DEFAULT_VERSION = ruby.get_default_version.__wrapped__
+
+
+@pytest.fixture
+def find_exe_mck():
+ with mock.patch.object(parse_shebang, 'find_executable') as mck:
+ yield mck
+
+
+def test_uses_default_version_when_not_available(find_exe_mck):
+ find_exe_mck.return_value = None
+ assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT
+
+
+def test_uses_system_if_both_gem_and_ruby_are_available(find_exe_mck):
+ find_exe_mck.return_value = '/path/to/exe'
+ assert ACTUAL_GET_DEFAULT_VERSION() == 'system'
+
+
+@pytest.mark.parametrize(
+ 'filename',
+ ('rbenv.tar.gz', 'ruby-build.tar.gz', 'ruby-download.tar.gz'),
+)
+def test_archive_root_stat(filename):
+ with _resource_bytesio(filename) as f:
+ with tarfile.open(fileobj=f) as tarf:
+ root, _, _ = filename.partition('.')
+ assert oct(tarf.getmember(root).mode) == '0o755'
+
+
+def _setup_hello_world(tmp_path):
+ bin_dir = tmp_path.joinpath('bin')
+ bin_dir.mkdir()
+ bin_dir.joinpath('ruby_hook').write_text(
+ '#!/usr/bin/env ruby\n'
+ "puts 'Hello world from a ruby hook'\n",
+ )
+ gemspec = '''\
+Gem::Specification.new do |s|
+ s.name = 'ruby_hook'
+ s.version = '0.1.0'
+ s.authors = ['Anthony Sottile']
+ s.summary = 'A ruby hook!'
+ s.description = 'A ruby hook!'
+ s.files = ['bin/ruby_hook']
+ s.executables = ['ruby_hook']
+end
+'''
+ tmp_path.joinpath('ruby_hook.gemspec').write_text(gemspec)
+
+
+def test_ruby_hook_system(tmp_path):
+ assert ruby.get_default_version() == 'system'
+
+ _setup_hello_world(tmp_path)
+
+ ret = run_language(tmp_path, ruby, 'ruby_hook')
+ assert ret == (0, b'Hello world from a ruby hook\n')
+
+
+def test_ruby_with_user_install_set(tmp_path):
+ gemrc = tmp_path.joinpath('gemrc')
+ gemrc.write_text('gem: --user-install\n')
+
+ with envcontext((('GEMRC', str(gemrc)),)):
+ test_ruby_hook_system(tmp_path)
+
+
+def test_ruby_additional_deps(tmp_path):
+ _make_local_repo(tmp_path)
+
+ ret = run_language(
+ tmp_path,
+ ruby,
+ 'ruby -e',
+ args=('require "tins"',),
+ deps=('tins',),
+ )
+ assert ret == (0, b'')
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_ruby_hook_default(tmp_path):
+ _setup_hello_world(tmp_path)
+
+ out, ret = run_language(tmp_path, ruby, 'rbenv --help', version='default')
+ assert out == 0
+ assert ret.startswith(b'Usage: rbenv ')
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_ruby_hook_language_version(tmp_path):
+ _setup_hello_world(tmp_path)
+ tmp_path.joinpath('bin', 'ruby_hook').write_text(
+ '#!/usr/bin/env ruby\n'
+ 'puts RUBY_VERSION\n'
+ "puts 'Hello world from a ruby hook'\n",
+ )
+
+ ret = run_language(tmp_path, ruby, 'ruby_hook', version='3.2.0')
+ assert ret == (0, b'3.2.0\nHello world from a ruby hook\n')
+
+
+@xfailif_windows # pragma: win32 no cover
+def test_ruby_with_bundle_disable_shared_gems(tmp_path):
+ workdir = tmp_path.joinpath('workdir')
+ workdir.mkdir()
+ # this needs a `source` or there's a deprecation warning
+ # silencing this with `BUNDLE_GEMFILE` breaks some tools (#2739)
+ workdir.joinpath('Gemfile').write_text('source ""\ngem "lol_hai"\n')
+ # this bundle config causes things to be written elsewhere
+ bundle = workdir.joinpath('.bundle')
+ bundle.mkdir()
+ bundle.joinpath('config').write_text(
+ 'BUNDLE_DISABLE_SHARED_GEMS: true\n'
+ 'BUNDLE_PATH: vendor/gem\n',
+ )
+
+ with cwd(workdir):
+ # `3.2.0` has new enough `gem` reading `.bundle`
+ test_ruby_hook_language_version(tmp_path)
diff --git a/tests/languages/rust_test.py b/tests/languages/rust_test.py
new file mode 100644
index 0000000..5c17f5b
--- /dev/null
+++ b/tests/languages/rust_test.py
@@ -0,0 +1,106 @@
+from __future__ import annotations
+
+from unittest import mock
+
+import pytest
+
+import pre_commit.constants as C
+from pre_commit import parse_shebang
+from pre_commit.languages import rust
+from pre_commit.store import _make_local_repo
+from testing.language_helpers import run_language
+
+ACTUAL_GET_DEFAULT_VERSION = rust.get_default_version.__wrapped__
+
+
+@pytest.fixture
+def cmd_output_b_mck():
+ with mock.patch.object(rust, 'cmd_output_b') as mck:
+ yield mck
+
+
+def test_sets_system_when_rust_is_available(cmd_output_b_mck):
+ cmd_output_b_mck.return_value = (0, b'', b'')
+ assert ACTUAL_GET_DEFAULT_VERSION() == 'system'
+
+
+def test_uses_default_when_rust_is_not_available(cmd_output_b_mck):
+ cmd_output_b_mck.return_value = (127, b'', b'error: not found')
+ assert ACTUAL_GET_DEFAULT_VERSION() == C.DEFAULT
+
+
+def _make_hello_world(tmp_path):
+ src_dir = tmp_path.joinpath('src')
+ src_dir.mkdir()
+ src_dir.joinpath('main.rs').write_text(
+ 'fn main() {\n'
+ ' println!("Hello, world!");\n'
+ '}\n',
+ )
+ tmp_path.joinpath('Cargo.toml').write_text(
+ '[package]\n'
+ 'name = "hello_world"\n'
+ 'version = "0.1.0"\n'
+ 'edition = "2021"\n',
+ )
+
+
+def test_installs_rust_missing_rustup(tmp_path):
+ _make_hello_world(tmp_path)
+
+ # pretend like `rustup` doesn't exist so it gets bootstrapped
+ calls = []
+ orig = parse_shebang.find_executable
+
+ def mck(exe, env=None):
+ calls.append(exe)
+ if len(calls) == 1:
+ assert exe == 'rustup'
+ return None
+ return orig(exe, env=env)
+
+ with mock.patch.object(parse_shebang, 'find_executable', side_effect=mck):
+ ret = run_language(tmp_path, rust, 'hello_world', version='1.56.0')
+ assert calls == ['rustup', 'rustup', 'cargo', 'hello_world']
+ assert ret == (0, b'Hello, world!\n')
+
+
+@pytest.mark.parametrize('version', (C.DEFAULT, '1.56.0'))
+def test_language_version_with_rustup(tmp_path, version):
+ assert parse_shebang.find_executable('rustup') is not None
+
+ _make_hello_world(tmp_path)
+
+ ret = run_language(tmp_path, rust, 'hello_world', version=version)
+ assert ret == (0, b'Hello, world!\n')
+
+
+@pytest.mark.parametrize('dep', ('cli:shellharden:4.2.0', 'cli:shellharden'))
+def test_rust_cli_additional_dependencies(tmp_path, dep):
+ _make_local_repo(str(tmp_path))
+
+ t_sh = tmp_path.joinpath('t.sh')
+ t_sh.write_text('echo $hi\n')
+
+ assert rust.get_default_version() == 'system'
+ ret = run_language(
+ tmp_path,
+ rust,
+ 'shellharden --transform',
+ deps=(dep,),
+ args=(str(t_sh),),
+ )
+ assert ret == (0, b'echo "$hi"\n')
+
+
+def test_run_lib_additional_dependencies(tmp_path):
+ _make_hello_world(tmp_path)
+
+ deps = ('shellharden:4.2.0', 'git-version')
+ ret = run_language(tmp_path, rust, 'hello_world', deps=deps)
+ assert ret == (0, b'Hello, world!\n')
+
+ bin_dir = tmp_path.joinpath('rustenv-system', 'bin')
+ assert bin_dir.is_dir()
+ assert not bin_dir.joinpath('shellharden').exists()
+ assert not bin_dir.joinpath('shellharden.exe').exists()
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/swift_test.py b/tests/languages/swift_test.py
new file mode 100644
index 0000000..e0a8ea4
--- /dev/null
+++ b/tests/languages/swift_test.py
@@ -0,0 +1,31 @@
+from __future__ import annotations
+
+import sys
+
+import pytest
+
+from pre_commit.languages import swift
+from testing.language_helpers import run_language
+
+
+@pytest.mark.skipif(
+ sys.platform == 'win32',
+ reason='swift is not supported on windows',
+)
+def test_swift_language(tmp_path): # pragma: win32 no cover
+ package_swift = '''\
+// swift-tools-version:5.0
+import PackageDescription
+
+let package = Package(
+ name: "swift_hooks_repo",
+ targets: [.target(name: "swift_hooks_repo")]
+)
+'''
+ tmp_path.joinpath('Package.swift').write_text(package_swift)
+ src_dir = tmp_path.joinpath('Sources/swift_hooks_repo')
+ src_dir.mkdir(parents=True)
+ src_dir.joinpath('main.swift').write_text('print("Hello, world!")\n')
+
+ expected = (0, b'Hello, world!\n')
+ assert run_language(tmp_path, swift, 'swift_hooks_repo') == 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