summaryrefslogtreecommitdiffstats
path: root/testing
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/__init__.py0
-rw-r--r--testing/auto_namedtuple.py13
-rw-r--r--testing/fixtures.py148
-rwxr-xr-xtesting/get-coursier.sh29
-rwxr-xr-xtesting/get-dart.sh17
-rw-r--r--testing/language_helpers.py40
-rwxr-xr-xtesting/languages92
-rwxr-xr-xtesting/make-archives84
-rw-r--r--testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml5
-rwxr-xr-xtesting/resources/arbitrary_bytes_repo/hook.sh7
-rw-r--r--testing/resources/arg_per_line_hooks_repo/.pre-commit-hooks.yaml6
-rwxr-xr-xtesting/resources/arg_per_line_hooks_repo/bin/hook.sh5
-rw-r--r--testing/resources/exclude_types_repo/.pre-commit-hooks.yaml6
-rwxr-xr-xtesting/resources/exclude_types_repo/bin/hook.sh3
-rw-r--r--testing/resources/failing_hook_repo/.pre-commit-hooks.yaml5
-rwxr-xr-xtesting/resources/failing_hook_repo/bin/hook.sh4
-rw-r--r--testing/resources/img1.jpgbin0 -> 843 bytes
-rw-r--r--testing/resources/img2.jpgbin0 -> 891 bytes
-rw-r--r--testing/resources/img3.jpgbin0 -> 859 bytes
-rw-r--r--testing/resources/logfile_repo/.pre-commit-hooks.yaml6
-rwxr-xr-xtesting/resources/logfile_repo/bin/hook.sh5
-rw-r--r--testing/resources/modified_file_returns_zero_repo/.pre-commit-hooks.yaml15
-rwxr-xr-xtesting/resources/modified_file_returns_zero_repo/bin/hook.sh7
-rwxr-xr-xtesting/resources/modified_file_returns_zero_repo/bin/hook2.sh2
-rwxr-xr-xtesting/resources/modified_file_returns_zero_repo/bin/hook3.sh6
-rw-r--r--testing/resources/not_found_exe/.pre-commit-hooks.yaml5
-rw-r--r--testing/resources/prints_cwd_repo/.pre-commit-hooks.yaml5
-rw-r--r--testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml6
-rw-r--r--testing/resources/python3_hooks_repo/py3_hook.py8
-rw-r--r--testing/resources/python3_hooks_repo/setup.py8
-rw-r--r--testing/resources/python_hooks_repo/.pre-commit-hooks.yaml5
-rw-r--r--testing/resources/python_hooks_repo/foo.py9
-rw-r--r--testing/resources/python_hooks_repo/setup.py10
-rw-r--r--testing/resources/script_hooks_repo/.pre-commit-hooks.yaml5
-rwxr-xr-xtesting/resources/script_hooks_repo/bin/hook.sh4
-rw-r--r--testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml8
-rwxr-xr-xtesting/resources/stdout_stderr_repo/stdout-stderr-entry7
-rwxr-xr-xtesting/resources/stdout_stderr_repo/tty-check-entry11
-rw-r--r--testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml5
-rw-r--r--testing/resources/types_or_repo/.pre-commit-hooks.yaml6
-rwxr-xr-xtesting/resources/types_or_repo/bin/hook.sh3
-rw-r--r--testing/resources/types_repo/.pre-commit-hooks.yaml5
-rwxr-xr-xtesting/resources/types_repo/bin/hook.sh3
-rw-r--r--testing/util.py106
-rw-r--r--testing/zipapp/Dockerfile14
-rwxr-xr-xtesting/zipapp/entry70
-rwxr-xr-xtesting/zipapp/make117
-rwxr-xr-xtesting/zipapp/python47
48 files changed, 972 insertions, 0 deletions
diff --git a/testing/__init__.py b/testing/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/testing/__init__.py
diff --git a/testing/auto_namedtuple.py b/testing/auto_namedtuple.py
new file mode 100644
index 0000000..d5a4377
--- /dev/null
+++ b/testing/auto_namedtuple.py
@@ -0,0 +1,13 @@
+from __future__ import annotations
+
+import collections
+
+
+def auto_namedtuple(classname='auto_namedtuple', **kwargs):
+ """Returns an automatic namedtuple object.
+
+ Args:
+ classname - The class name for the returned object.
+ **kwargs - Properties to give the returned object.
+ """
+ return (collections.namedtuple(classname, kwargs.keys())(**kwargs))
diff --git a/testing/fixtures.py b/testing/fixtures.py
new file mode 100644
index 0000000..79a1160
--- /dev/null
+++ b/testing/fixtures.py
@@ -0,0 +1,148 @@
+from __future__ import annotations
+
+import contextlib
+import os.path
+import shutil
+
+from cfgv import apply_defaults
+from cfgv import validate
+
+import pre_commit.constants as C
+from pre_commit import git
+from pre_commit.clientlib import CONFIG_SCHEMA
+from pre_commit.clientlib import load_manifest
+from pre_commit.util import cmd_output
+from pre_commit.yaml import yaml_dump
+from pre_commit.yaml import yaml_load
+from testing.util import get_resource_path
+from testing.util import git_commit
+
+
+def copy_tree_to_path(src_dir, dest_dir):
+ """Copies all of the things inside src_dir to an already existing dest_dir.
+
+ This looks eerily similar to shutil.copytree, but copytree has no option
+ for not creating dest_dir.
+ """
+ names = os.listdir(src_dir)
+
+ for name in names:
+ srcname = os.path.join(src_dir, name)
+ destname = os.path.join(dest_dir, name)
+
+ if os.path.isdir(srcname):
+ shutil.copytree(srcname, destname)
+ else:
+ shutil.copy(srcname, destname)
+
+
+def git_dir(tempdir_factory):
+ path = tempdir_factory.get()
+ cmd_output('git', '-c', 'init.defaultBranch=master', 'init', path)
+ return path
+
+
+def make_repo(tempdir_factory, repo_source):
+ path = git_dir(tempdir_factory)
+ copy_tree_to_path(get_resource_path(repo_source), path)
+ cmd_output('git', 'add', '.', cwd=path)
+ git_commit(msg=make_repo.__name__, cwd=path)
+ return path
+
+
+@contextlib.contextmanager
+def modify_manifest(path, commit=True):
+ """Modify the manifest yielded by this context to write to
+ .pre-commit-hooks.yaml.
+ """
+ manifest_path = os.path.join(path, C.MANIFEST_FILE)
+ with open(manifest_path) as f:
+ manifest = yaml_load(f.read())
+ yield manifest
+ with open(manifest_path, 'w') as manifest_file:
+ manifest_file.write(yaml_dump(manifest))
+ if commit:
+ git_commit(msg=modify_manifest.__name__, cwd=path)
+
+
+@contextlib.contextmanager
+def modify_config(path='.', commit=True):
+ """Modify the config yielded by this context to write to
+ .pre-commit-config.yaml
+ """
+ config_path = os.path.join(path, C.CONFIG_FILE)
+ with open(config_path) as f:
+ config = yaml_load(f.read())
+ yield config
+ with open(config_path, 'w', encoding='UTF-8') as config_file:
+ config_file.write(yaml_dump(config))
+ if commit:
+ git_commit(msg=modify_config.__name__, cwd=path)
+
+
+def sample_local_config():
+ return {
+ 'repo': 'local',
+ 'hooks': [{
+ 'id': 'do_not_commit',
+ 'name': 'Block if "DO NOT COMMIT" is found',
+ 'entry': 'DO NOT COMMIT',
+ 'language': 'pygrep',
+ }],
+ }
+
+
+def sample_meta_config():
+ return {'repo': 'meta', 'hooks': [{'id': 'check-useless-excludes'}]}
+
+
+def make_config_from_repo(repo_path, rev=None, hooks=None, check=True):
+ manifest = load_manifest(os.path.join(repo_path, C.MANIFEST_FILE))
+ config = {
+ 'repo': f'file://{repo_path}',
+ 'rev': rev or git.head_rev(repo_path),
+ 'hooks': hooks or [{'id': hook['id']} for hook in manifest],
+ }
+
+ if check:
+ wrapped = validate({'repos': [config]}, CONFIG_SCHEMA)
+ wrapped = apply_defaults(wrapped, CONFIG_SCHEMA)
+ config, = wrapped['repos']
+ return config
+ else:
+ return config
+
+
+def read_config(directory, config_file=C.CONFIG_FILE):
+ config_path = os.path.join(directory, config_file)
+ with open(config_path) as f:
+ config = yaml_load(f.read())
+ return config
+
+
+def write_config(directory, config, config_file=C.CONFIG_FILE):
+ if type(config) is not list and 'repos' not in config:
+ assert isinstance(config, dict), config
+ config = {'repos': [config]}
+ with open(os.path.join(directory, config_file), 'w') as outfile:
+ outfile.write(yaml_dump(config))
+
+
+def add_config_to_repo(git_path, config, config_file=C.CONFIG_FILE):
+ write_config(git_path, config, config_file=config_file)
+ cmd_output('git', 'add', config_file, cwd=git_path)
+ git_commit(msg=add_config_to_repo.__name__, cwd=git_path)
+ return git_path
+
+
+def remove_config_from_repo(git_path, config_file=C.CONFIG_FILE):
+ cmd_output('git', 'rm', config_file, cwd=git_path)
+ git_commit(msg=remove_config_from_repo.__name__, cwd=git_path)
+ return git_path
+
+
+def make_consuming_repo(tempdir_factory, repo_source):
+ path = make_repo(tempdir_factory, repo_source)
+ config = make_config_from_repo(path)
+ git_path = git_dir(tempdir_factory)
+ return add_config_to_repo(git_path, config)
diff --git a/testing/get-coursier.sh b/testing/get-coursier.sh
new file mode 100755
index 0000000..958e73b
--- /dev/null
+++ b/testing/get-coursier.sh
@@ -0,0 +1,29 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+if [ "$OSTYPE" = msys ]; then
+ URL='https://github.com/coursier/coursier/releases/download/v2.1.0-RC4/cs-x86_64-pc-win32.zip'
+ SHA256='0d07386ff0f337e3e6264f7dde29d137dda6eaa2385f29741435e0b93ccdb49d'
+ TARGET='/tmp/coursier/cs.zip'
+
+ unpack() {
+ unzip "$TARGET" -d /tmp/coursier
+ mv /tmp/coursier/cs-*.exe /tmp/coursier/cs.exe
+ cygpath -w /tmp/coursier >> "$GITHUB_PATH"
+ }
+else
+ URL='https://github.com/coursier/coursier/releases/download/v2.1.0-RC4/cs-x86_64-pc-linux.gz'
+ SHA256='176e92e08ab292531aa0c4993dbc9f2c99dec79578752f3b9285f54f306db572'
+ TARGET=/tmp/coursier/cs.gz
+
+ unpack() {
+ gunzip "$TARGET"
+ chmod +x /tmp/coursier/cs
+ echo /tmp/coursier >> "$GITHUB_PATH"
+ }
+fi
+
+mkdir -p /tmp/coursier
+curl --location --silent --output "$TARGET" "$URL"
+echo "$SHA256 $TARGET" | sha256sum --check
+unpack
diff --git a/testing/get-dart.sh b/testing/get-dart.sh
new file mode 100755
index 0000000..998b9d9
--- /dev/null
+++ b/testing/get-dart.sh
@@ -0,0 +1,17 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+VERSION=2.13.4
+
+if [ "$OSTYPE" = msys ]; then
+ URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-windows-x64-release.zip"
+ cygpath -w /tmp/dart-sdk/bin >> "$GITHUB_PATH"
+else
+ URL="https://storage.googleapis.com/dart-archive/channels/stable/release/${VERSION}/sdk/dartsdk-linux-x64-release.zip"
+ echo '/tmp/dart-sdk/bin' >> "$GITHUB_PATH"
+fi
+
+curl --silent --location --output /tmp/dart.zip "$URL"
+
+unzip -q -d /tmp /tmp/dart.zip
+rm /tmp/dart.zip
diff --git a/testing/language_helpers.py b/testing/language_helpers.py
new file mode 100644
index 0000000..05c94eb
--- /dev/null
+++ b/testing/language_helpers.py
@@ -0,0 +1,40 @@
+from __future__ import annotations
+
+import os
+from collections.abc import Sequence
+
+from pre_commit.lang_base import Language
+from pre_commit.prefix import Prefix
+
+
+def run_language(
+ path: os.PathLike[str],
+ language: Language,
+ exe: str,
+ args: Sequence[str] = (),
+ file_args: Sequence[str] = (),
+ version: str | None = None,
+ deps: Sequence[str] = (),
+ is_local: bool = False,
+ require_serial: bool = True,
+ color: bool = False,
+) -> tuple[int, bytes]:
+ prefix = Prefix(str(path))
+ version = version or language.get_default_version()
+
+ if language.ENVIRONMENT_DIR is not None:
+ language.install_environment(prefix, version, deps)
+ health_error = language.health_check(prefix, version)
+ assert health_error is None, health_error
+ with language.in_env(prefix, version):
+ ret, out = language.run_hook(
+ prefix,
+ exe,
+ args,
+ file_args,
+ is_local=is_local,
+ require_serial=require_serial,
+ color=color,
+ )
+ out = out.replace(b'\r\n', b'\n')
+ return ret, out
diff --git a/testing/languages b/testing/languages
new file mode 100755
index 0000000..f4804c7
--- /dev/null
+++ b/testing/languages
@@ -0,0 +1,92 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import concurrent.futures
+import json
+import os.path
+import subprocess
+import sys
+
+EXCLUDED = frozenset((
+ ('windows-latest', 'docker'),
+ ('windows-latest', 'docker_image'),
+ ('windows-latest', 'lua'),
+ ('windows-latest', 'swift'),
+))
+
+
+def _always_run() -> frozenset[str]:
+ ret = ['.github/workflows/languages.yaml', 'testing/languages']
+ ret.extend(
+ os.path.join('pre_commit/resources', fname)
+ for fname in os.listdir('pre_commit/resources')
+ )
+ return frozenset(ret)
+
+
+def _lang_files(lang: str) -> frozenset[str]:
+ prog = f'''\
+import json
+import os.path
+import sys
+
+import pre_commit.languages.{lang}
+import tests.languages.{lang}_test
+
+modules = sorted(
+ os.path.relpath(v.__file__)
+ for k, v in sys.modules.items()
+ if k.startswith(('pre_commit.', 'tests.', 'testing.'))
+)
+print(json.dumps(modules))
+'''
+ out = json.loads(subprocess.check_output((sys.executable, '-c', prog)))
+ return frozenset(out)
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--all', action='store_true')
+ args = parser.parse_args()
+
+ langs = [
+ os.path.splitext(fname)[0]
+ for fname in sorted(os.listdir('pre_commit/languages'))
+ if fname.endswith('.py') and fname != '__init__.py'
+ ]
+
+ triggers_all = _always_run()
+ for fname in triggers_all:
+ assert os.path.exists(fname), fname
+
+ if not args.all:
+ with concurrent.futures.ThreadPoolExecutor(os.cpu_count()) as exe:
+ by_lang = {
+ lang: files | triggers_all
+ for lang, files in zip(langs, exe.map(_lang_files, langs))
+ }
+
+ diff_cmd = ('git', 'diff', '--name-only', 'origin/main...HEAD')
+ files = set(subprocess.check_output(diff_cmd).decode().splitlines())
+
+ langs = [
+ lang
+ for lang, lang_files in by_lang.items()
+ if lang_files & files
+ ]
+
+ matched = [
+ {'os': os, 'language': lang}
+ for os in ('windows-latest', 'ubuntu-latest')
+ for lang in langs
+ if (os, lang) not in EXCLUDED
+ ]
+
+ with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
+ f.write(f'languages={json.dumps(matched)}\n')
+ return 0
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())
diff --git a/testing/make-archives b/testing/make-archives
new file mode 100755
index 0000000..3c7ab9d
--- /dev/null
+++ b/testing/make-archives
@@ -0,0 +1,84 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import gzip
+import os.path
+import shutil
+import subprocess
+import tarfile
+import tempfile
+from collections.abc import Sequence
+
+
+# This is a script for generating the tarred resources for git repo
+# dependencies. Currently it's just for "vendoring" ruby support packages.
+
+
+REPOS = (
+ ('rbenv', 'https://github.com/rbenv/rbenv', '38e1fbb'),
+ ('ruby-build', 'https://github.com/rbenv/ruby-build', '855b963'),
+ (
+ 'ruby-download',
+ 'https://github.com/garnieretienne/rvm-download',
+ '09bd7c6',
+ ),
+)
+
+
+def reset(tarinfo: tarfile.TarInfo) -> tarfile.TarInfo:
+ tarinfo.uid = tarinfo.gid = 0
+ tarinfo.uname = tarinfo.gname = 'root'
+ tarinfo.mtime = 0
+ return tarinfo
+
+
+def make_archive(name: str, repo: str, ref: str, destdir: str) -> str:
+ output_path = os.path.join(destdir, f'{name}.tar.gz')
+ with tempfile.TemporaryDirectory() as tmpdir:
+ # this ensures that the root directory has umask permissions
+ gitdir = os.path.join(tmpdir, 'root')
+
+ # Clone the repository to the temporary directory
+ subprocess.check_call(('git', 'clone', repo, gitdir))
+ subprocess.check_call(('git', '-C', gitdir, 'checkout', ref))
+
+ # We don't want the '.git' directory
+ # It adds a bunch of size to the archive and we don't use it at
+ # runtime
+ shutil.rmtree(os.path.join(gitdir, '.git'))
+
+ arcs = [(name, gitdir)]
+ for root, dirs, filenames in os.walk(gitdir):
+ for filename in dirs + filenames:
+ abspath = os.path.abspath(os.path.join(root, filename))
+ relpath = os.path.relpath(abspath, gitdir)
+ arcs.append((os.path.join(name, relpath), abspath))
+ arcs.sort()
+
+ with gzip.GzipFile(output_path, 'wb', mtime=0) as gzipf:
+ # https://github.com/python/typeshed/issues/5491
+ with tarfile.open(fileobj=gzipf, mode='w') as tf: # type: ignore
+ for arcname, abspath in arcs:
+ tf.add(
+ abspath,
+ arcname=arcname,
+ recursive=False,
+ filter=reset,
+ )
+
+ return output_path
+
+
+def main(argv: Sequence[str] | None = None) -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('--dest', default='pre_commit/resources')
+ args = parser.parse_args(argv)
+ for archive_name, repo, ref in REPOS:
+ print(f'Making {archive_name}.tar.gz for {repo}@{ref}')
+ make_archive(archive_name, repo, ref, args.dest)
+ return 0
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())
diff --git a/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml b/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..c2aec9b
--- /dev/null
+++ b/testing/resources/arbitrary_bytes_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,5 @@
+- id: hook
+ name: hook
+ entry: ./hook.sh
+ language: script
+ files: \.py$
diff --git a/testing/resources/arbitrary_bytes_repo/hook.sh b/testing/resources/arbitrary_bytes_repo/hook.sh
new file mode 100755
index 0000000..9df0c5a
--- /dev/null
+++ b/testing/resources/arbitrary_bytes_repo/hook.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+# Intentionally write mixed encoding to the output. This should not crash
+# pre-commit and should write bytes to the output.
+# '☃'.encode() + '²'.encode('latin1')
+echo -e '\xe2\x98\x83\xb2'
+# exit 1 to trigger printing
+exit 1
diff --git a/testing/resources/arg_per_line_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/arg_per_line_hooks_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..4c101db
--- /dev/null
+++ b/testing/resources/arg_per_line_hooks_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,6 @@
+- id: arg-per-line
+ name: Args per line hook
+ entry: bin/hook.sh
+ language: script
+ files: ''
+ args: [hello, world]
diff --git a/testing/resources/arg_per_line_hooks_repo/bin/hook.sh b/testing/resources/arg_per_line_hooks_repo/bin/hook.sh
new file mode 100755
index 0000000..47fd21d
--- /dev/null
+++ b/testing/resources/arg_per_line_hooks_repo/bin/hook.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+
+for i in "$@"; do
+ echo "arg: $i"
+done
diff --git a/testing/resources/exclude_types_repo/.pre-commit-hooks.yaml b/testing/resources/exclude_types_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..ed8794f
--- /dev/null
+++ b/testing/resources/exclude_types_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,6 @@
+- id: python-files
+ name: Python files
+ entry: bin/hook.sh
+ language: script
+ types: [python]
+ exclude_types: [python3]
diff --git a/testing/resources/exclude_types_repo/bin/hook.sh b/testing/resources/exclude_types_repo/bin/hook.sh
new file mode 100755
index 0000000..a828db4
--- /dev/null
+++ b/testing/resources/exclude_types_repo/bin/hook.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+echo "$@"
+exit 1
diff --git a/testing/resources/failing_hook_repo/.pre-commit-hooks.yaml b/testing/resources/failing_hook_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..118cc8b
--- /dev/null
+++ b/testing/resources/failing_hook_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,5 @@
+- id: failing_hook
+ name: Failing hook
+ entry: bin/hook.sh
+ language: script
+ files: .
diff --git a/testing/resources/failing_hook_repo/bin/hook.sh b/testing/resources/failing_hook_repo/bin/hook.sh
new file mode 100755
index 0000000..7dcffeb
--- /dev/null
+++ b/testing/resources/failing_hook_repo/bin/hook.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+echo 'Fail'
+echo "$@"
+exit 1
diff --git a/testing/resources/img1.jpg b/testing/resources/img1.jpg
new file mode 100644
index 0000000..dea4262
--- /dev/null
+++ b/testing/resources/img1.jpg
Binary files differ
diff --git a/testing/resources/img2.jpg b/testing/resources/img2.jpg
new file mode 100644
index 0000000..68568e5
--- /dev/null
+++ b/testing/resources/img2.jpg
Binary files differ
diff --git a/testing/resources/img3.jpg b/testing/resources/img3.jpg
new file mode 100644
index 0000000..392d2cf
--- /dev/null
+++ b/testing/resources/img3.jpg
Binary files differ
diff --git a/testing/resources/logfile_repo/.pre-commit-hooks.yaml b/testing/resources/logfile_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..dcaba2e
--- /dev/null
+++ b/testing/resources/logfile_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,6 @@
+- id: logfile test hook
+ name: Logfile test hook
+ entry: bin/hook.sh
+ language: script
+ files: .
+ log_file: test.log
diff --git a/testing/resources/logfile_repo/bin/hook.sh b/testing/resources/logfile_repo/bin/hook.sh
new file mode 100755
index 0000000..890d941
--- /dev/null
+++ b/testing/resources/logfile_repo/bin/hook.sh
@@ -0,0 +1,5 @@
+#!/usr/bin/env bash
+echo "This is STDOUT output"
+echo "This is STDERR output" 1>&2
+
+exit 1
diff --git a/testing/resources/modified_file_returns_zero_repo/.pre-commit-hooks.yaml b/testing/resources/modified_file_returns_zero_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..8d79ef3
--- /dev/null
+++ b/testing/resources/modified_file_returns_zero_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,15 @@
+- id: bash_hook
+ name: Bash hook
+ entry: bin/hook.sh
+ language: script
+ files: 'foo.py'
+- id: bash_hook2
+ name: Bash hook
+ entry: bin/hook2.sh
+ language: script
+ files: ''
+- id: bash_hook3
+ name: Bash hook
+ entry: bin/hook3.sh
+ language: script
+ files: 'bar.py'
diff --git a/testing/resources/modified_file_returns_zero_repo/bin/hook.sh b/testing/resources/modified_file_returns_zero_repo/bin/hook.sh
new file mode 100755
index 0000000..98b05f9
--- /dev/null
+++ b/testing/resources/modified_file_returns_zero_repo/bin/hook.sh
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+
+for f in $@; do
+ # Non UTF-8 bytes
+ echo -e '\x01\x97' > "$f"
+ echo "Modified: $f!"
+done
diff --git a/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh b/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh
new file mode 100755
index 0000000..a9f1dcd
--- /dev/null
+++ b/testing/resources/modified_file_returns_zero_repo/bin/hook2.sh
@@ -0,0 +1,2 @@
+#!/usr/bin/env bash
+echo "$@"
diff --git a/testing/resources/modified_file_returns_zero_repo/bin/hook3.sh b/testing/resources/modified_file_returns_zero_repo/bin/hook3.sh
new file mode 100755
index 0000000..3180eb3
--- /dev/null
+++ b/testing/resources/modified_file_returns_zero_repo/bin/hook3.sh
@@ -0,0 +1,6 @@
+#!/usr/bin/env bash
+
+for f in $@; do
+ # Non UTF-8 bytes
+ echo -e '\x01\x97' > "$f"
+done
diff --git a/testing/resources/not_found_exe/.pre-commit-hooks.yaml b/testing/resources/not_found_exe/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..566f3c1
--- /dev/null
+++ b/testing/resources/not_found_exe/.pre-commit-hooks.yaml
@@ -0,0 +1,5 @@
+- id: not-found-exe
+ name: Not found exe
+ entry: i-dont-exist-lol
+ language: system
+ files: ''
diff --git a/testing/resources/prints_cwd_repo/.pre-commit-hooks.yaml b/testing/resources/prints_cwd_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..7092379
--- /dev/null
+++ b/testing/resources/prints_cwd_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,5 @@
+- id: prints_cwd
+ name: Prints Cwd
+ entry: pwd
+ language: system
+ files: \.sh$
diff --git a/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..2c23700
--- /dev/null
+++ b/testing/resources/python3_hooks_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,6 @@
+- id: python3-hook
+ name: Python 3 Hook
+ entry: python3-hook
+ language: python
+ language_version: python3
+ files: \.py$
diff --git a/testing/resources/python3_hooks_repo/py3_hook.py b/testing/resources/python3_hooks_repo/py3_hook.py
new file mode 100644
index 0000000..8c9cda4
--- /dev/null
+++ b/testing/resources/python3_hooks_repo/py3_hook.py
@@ -0,0 +1,8 @@
+import sys
+
+
+def main():
+ print(sys.version_info[0])
+ print(repr(sys.argv[1:]))
+ print('Hello World')
+ return 0
diff --git a/testing/resources/python3_hooks_repo/setup.py b/testing/resources/python3_hooks_repo/setup.py
new file mode 100644
index 0000000..9125dc1
--- /dev/null
+++ b/testing/resources/python3_hooks_repo/setup.py
@@ -0,0 +1,8 @@
+from setuptools import setup
+
+setup(
+ name='python3_hook',
+ version='0.0.0',
+ py_modules=['py3_hook'],
+ entry_points={'console_scripts': ['python3-hook = py3_hook:main']},
+)
diff --git a/testing/resources/python_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/python_hooks_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..e10ad50
--- /dev/null
+++ b/testing/resources/python_hooks_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,5 @@
+- id: foo
+ name: Foo
+ entry: foo
+ language: python
+ files: \.py$
diff --git a/testing/resources/python_hooks_repo/foo.py b/testing/resources/python_hooks_repo/foo.py
new file mode 100644
index 0000000..40efde3
--- /dev/null
+++ b/testing/resources/python_hooks_repo/foo.py
@@ -0,0 +1,9 @@
+from __future__ import annotations
+
+import sys
+
+
+def main():
+ print(repr(sys.argv[1:]))
+ print('Hello World')
+ return 0
diff --git a/testing/resources/python_hooks_repo/setup.py b/testing/resources/python_hooks_repo/setup.py
new file mode 100644
index 0000000..cff6cad
--- /dev/null
+++ b/testing/resources/python_hooks_repo/setup.py
@@ -0,0 +1,10 @@
+from __future__ import annotations
+
+from setuptools import setup
+
+setup(
+ name='foo',
+ version='0.0.0',
+ py_modules=['foo'],
+ entry_points={'console_scripts': ['foo = foo:main']},
+)
diff --git a/testing/resources/script_hooks_repo/.pre-commit-hooks.yaml b/testing/resources/script_hooks_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..21cad4a
--- /dev/null
+++ b/testing/resources/script_hooks_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,5 @@
+- id: bash_hook
+ name: Bash hook
+ entry: bin/hook.sh
+ language: script
+ files: ''
diff --git a/testing/resources/script_hooks_repo/bin/hook.sh b/testing/resources/script_hooks_repo/bin/hook.sh
new file mode 100755
index 0000000..cbc4b35
--- /dev/null
+++ b/testing/resources/script_hooks_repo/bin/hook.sh
@@ -0,0 +1,4 @@
+#!/usr/bin/env bash
+
+echo "$@"
+echo 'Hello World'
diff --git a/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml b/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..6800d25
--- /dev/null
+++ b/testing/resources/stdout_stderr_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,8 @@
+- id: stdout-stderr
+ name: stdout-stderr
+ language: script
+ entry: ./stdout-stderr-entry
+- id: tty-check
+ name: tty-check
+ language: script
+ entry: ./tty-check-entry
diff --git a/testing/resources/stdout_stderr_repo/stdout-stderr-entry b/testing/resources/stdout_stderr_repo/stdout-stderr-entry
new file mode 100755
index 0000000..7563df5
--- /dev/null
+++ b/testing/resources/stdout_stderr_repo/stdout-stderr-entry
@@ -0,0 +1,7 @@
+#!/usr/bin/env bash
+echo 0
+echo 1 1>&2
+echo 2
+echo 3 1>&2
+echo 4
+echo 5 1>&2
diff --git a/testing/resources/stdout_stderr_repo/tty-check-entry b/testing/resources/stdout_stderr_repo/tty-check-entry
new file mode 100755
index 0000000..01a9d38
--- /dev/null
+++ b/testing/resources/stdout_stderr_repo/tty-check-entry
@@ -0,0 +1,11 @@
+#!/usr/bin/env bash
+t() {
+ if [ -t "$1" ]; then
+ echo "$2: True"
+ else
+ echo "$2: False"
+ fi
+}
+t 0 stdin
+t 1 stdout
+t 2 stderr
diff --git a/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml b/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..b2c347c
--- /dev/null
+++ b/testing/resources/system_hook_with_spaces_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,5 @@
+- id: system-hook-with-spaces
+ name: System hook with spaces
+ entry: bash -c 'echo "Hello World"'
+ language: system
+ files: \.sh$
diff --git a/testing/resources/types_or_repo/.pre-commit-hooks.yaml b/testing/resources/types_or_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..a4ea920
--- /dev/null
+++ b/testing/resources/types_or_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,6 @@
+- id: python-cython-files
+ name: Python and Cython files
+ entry: bin/hook.sh
+ language: script
+ types: [file]
+ types_or: [python, cython]
diff --git a/testing/resources/types_or_repo/bin/hook.sh b/testing/resources/types_or_repo/bin/hook.sh
new file mode 100755
index 0000000..a828db4
--- /dev/null
+++ b/testing/resources/types_or_repo/bin/hook.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+echo "$@"
+exit 1
diff --git a/testing/resources/types_repo/.pre-commit-hooks.yaml b/testing/resources/types_repo/.pre-commit-hooks.yaml
new file mode 100644
index 0000000..2e5e4a6
--- /dev/null
+++ b/testing/resources/types_repo/.pre-commit-hooks.yaml
@@ -0,0 +1,5 @@
+- id: python-files
+ name: Python files
+ entry: bin/hook.sh
+ language: script
+ types: [python]
diff --git a/testing/resources/types_repo/bin/hook.sh b/testing/resources/types_repo/bin/hook.sh
new file mode 100755
index 0000000..a828db4
--- /dev/null
+++ b/testing/resources/types_repo/bin/hook.sh
@@ -0,0 +1,3 @@
+#!/usr/bin/env bash
+echo "$@"
+exit 1
diff --git a/testing/util.py b/testing/util.py
new file mode 100644
index 0000000..08d52cb
--- /dev/null
+++ b/testing/util.py
@@ -0,0 +1,106 @@
+from __future__ import annotations
+
+import contextlib
+import os.path
+import subprocess
+import sys
+
+import pytest
+
+from pre_commit.util import cmd_output
+from testing.auto_namedtuple import auto_namedtuple
+
+
+TESTING_DIR = os.path.abspath(os.path.dirname(__file__))
+
+
+def get_resource_path(path):
+ return os.path.join(TESTING_DIR, 'resources', path)
+
+
+def cmd_output_mocked_pre_commit_home(
+ *args, tempdir_factory, pre_commit_home=None, env=None, **kwargs,
+):
+ if pre_commit_home is None:
+ pre_commit_home = tempdir_factory.get()
+ env = env if env is not None else os.environ
+ kwargs.setdefault('stderr', subprocess.STDOUT)
+ # Don't want to write to the home directory
+ env = dict(env, PRE_COMMIT_HOME=pre_commit_home)
+ ret, out, _ = cmd_output(*args, env=env, **kwargs)
+ return ret, out.replace('\r\n', '\n'), None
+
+
+xfailif_windows = pytest.mark.xfail(sys.platform == 'win32', reason='windows')
+
+
+def run_opts(
+ all_files=False,
+ files=(),
+ color=False,
+ verbose=False,
+ hook=None,
+ remote_branch='',
+ local_branch='',
+ from_ref='',
+ to_ref='',
+ pre_rebase_upstream='',
+ pre_rebase_branch='',
+ remote_name='',
+ remote_url='',
+ hook_stage='pre-commit',
+ show_diff_on_failure=False,
+ commit_msg_filename='',
+ prepare_commit_message_source='',
+ commit_object_name='',
+ checkout_type='',
+ is_squash_merge='',
+ rewrite_command='',
+):
+ # These are mutually exclusive
+ assert not (all_files and files)
+ return auto_namedtuple(
+ all_files=all_files,
+ files=files,
+ color=color,
+ verbose=verbose,
+ hook=hook,
+ remote_branch=remote_branch,
+ local_branch=local_branch,
+ from_ref=from_ref,
+ to_ref=to_ref,
+ pre_rebase_upstream=pre_rebase_upstream,
+ pre_rebase_branch=pre_rebase_branch,
+ remote_name=remote_name,
+ remote_url=remote_url,
+ hook_stage=hook_stage,
+ show_diff_on_failure=show_diff_on_failure,
+ commit_msg_filename=commit_msg_filename,
+ prepare_commit_message_source=prepare_commit_message_source,
+ commit_object_name=commit_object_name,
+ checkout_type=checkout_type,
+ is_squash_merge=is_squash_merge,
+ rewrite_command=rewrite_command,
+ )
+
+
+@contextlib.contextmanager
+def cwd(path):
+ original_cwd = os.getcwd()
+ os.chdir(path)
+ try:
+ yield
+ finally:
+ os.chdir(original_cwd)
+
+
+def git_commit(*args, fn=cmd_output, msg='commit!', all_files=True, **kwargs):
+ kwargs.setdefault('stderr', subprocess.STDOUT)
+
+ cmd = ('git', 'commit', '--allow-empty', '--no-gpg-sign', *args)
+ if all_files: # allow skipping `-a` with `all_files=False`
+ cmd += ('-a',)
+ if msg is not None: # allow skipping `-m` with `msg=None`
+ cmd += ('-m', msg)
+ ret, out, _ = fn(*cmd, **kwargs)
+ return ret, out.replace('\r\n', '\n')
diff --git a/testing/zipapp/Dockerfile b/testing/zipapp/Dockerfile
new file mode 100644
index 0000000..ea967e3
--- /dev/null
+++ b/testing/zipapp/Dockerfile
@@ -0,0 +1,14 @@
+FROM ubuntu:jammy
+RUN : \
+ && apt-get update \
+ && DEBIAN_FRONTEND=noninteractive apt-get install -y --no-install-recommends \
+ python3 \
+ python3-distutils \
+ python3-venv \
+ && apt-get clean \
+ && rm -rf /var/lib/apt/lists/*
+
+ENV LANG=C.UTF-8 PATH=/venv/bin:$PATH
+RUN : \
+ && python3 -mvenv /venv \
+ && pip install --no-cache-dir pip distlib no-manylinux --upgrade
diff --git a/testing/zipapp/entry b/testing/zipapp/entry
new file mode 100755
index 0000000..15758d9
--- /dev/null
+++ b/testing/zipapp/entry
@@ -0,0 +1,70 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import os.path
+import shutil
+import stat
+import sys
+import tempfile
+import zipfile
+
+from pre_commit.file_lock import lock
+
+CACHE_DIR = os.path.expanduser('~/.cache/pre-commit-zipapp')
+
+
+def _make_executable(filename: str) -> None:
+ os.chmod(filename, os.stat(filename).st_mode | stat.S_IXUSR)
+
+
+def _ensure_cache(zipf: zipfile.ZipFile, cache_key: str) -> str:
+ os.makedirs(CACHE_DIR, exist_ok=True)
+
+ cache_dest = os.path.join(CACHE_DIR, cache_key)
+ lock_filename = os.path.join(CACHE_DIR, f'{cache_key}.lock')
+
+ if os.path.exists(cache_dest):
+ return cache_dest
+
+ with lock(lock_filename, blocked_cb=lambda: None):
+ # another process may have completed this work
+ if os.path.exists(cache_dest):
+ return cache_dest
+
+ tmpdir = tempfile.mkdtemp(prefix=os.path.join(CACHE_DIR, ''))
+ try:
+ zipf.extractall(tmpdir)
+ # zip doesn't maintain permissions
+ _make_executable(os.path.join(tmpdir, 'python'))
+ _make_executable(os.path.join(tmpdir, 'python.exe'))
+ os.rename(tmpdir, cache_dest)
+ except BaseException:
+ shutil.rmtree(tmpdir)
+ raise
+
+ return cache_dest
+
+
+def main() -> int:
+ with zipfile.ZipFile(os.path.dirname(__file__)) as zipf:
+ with zipf.open('CACHE_KEY') as f:
+ cache_key = f.read().decode().strip()
+
+ cache_dest = _ensure_cache(zipf, cache_key)
+
+ if sys.platform != 'win32':
+ exe = os.path.join(cache_dest, 'python')
+ else:
+ exe = os.path.join(cache_dest, 'python.exe')
+
+ cmd = (exe, '-mpre_commit', *sys.argv[1:])
+ if sys.platform == 'win32': # https://bugs.python.org/issue19124
+ import subprocess
+
+ return subprocess.call(cmd)
+ else:
+ os.execvp(cmd[0], cmd)
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())
diff --git a/testing/zipapp/make b/testing/zipapp/make
new file mode 100755
index 0000000..165046f
--- /dev/null
+++ b/testing/zipapp/make
@@ -0,0 +1,117 @@
+#!/usr/bin/env python3
+from __future__ import annotations
+
+import argparse
+import base64
+import hashlib
+import io
+import os.path
+import shutil
+import subprocess
+import tempfile
+import zipapp
+import zipfile
+
+HERE = os.path.dirname(os.path.realpath(__file__))
+IMG = 'make-pre-commit-zipapp'
+
+
+def _msg(s: str) -> None:
+ print(f'\033[7m{s}\033[m')
+
+
+def _exit_if_retv(*cmd: str) -> None:
+ if subprocess.call(cmd):
+ raise SystemExit(1)
+
+
+def _check_no_shared_objects(wheeldir: str) -> None:
+ for zip_filename in os.listdir(wheeldir):
+ with zipfile.ZipFile(os.path.join(wheeldir, zip_filename)) as zipf:
+ for filename in zipf.namelist():
+ if filename.endswith('.so') or '.so.' in filename:
+ raise AssertionError(zip_filename, filename)
+
+
+def _add_shim(dest: str) -> None:
+ shim = os.path.join(HERE, 'python')
+ shutil.copy(shim, dest)
+
+ bio = io.BytesIO()
+ with zipfile.ZipFile(bio, 'w') as zipf:
+ zipf.write(shim, arcname='__main__.py')
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ _exit_if_retv(
+ 'podman', 'run', '--rm', '--volume', f'{tmpdir}:/out:rw', IMG,
+ 'cp', '/venv/lib/python3.10/site-packages/distlib/t32.exe', '/out',
+ )
+
+ with open(os.path.join(dest, 'python.exe'), 'wb') as f:
+ with open(os.path.join(tmpdir, 't32.exe'), 'rb') as t32:
+ f.write(t32.read())
+ f.write(b'#!py.exe -3\n')
+ f.write(bio.getvalue())
+
+
+def _write_cache_key(version: str, wheeldir: str, dest: str) -> None:
+ cache_hash = hashlib.sha256(f'{version}\n'.encode())
+ for filename in sorted(os.listdir(wheeldir)):
+ cache_hash.update(f'{filename}\n'.encode())
+ with open(os.path.join(HERE, 'python'), 'rb') as f:
+ cache_hash.update(f.read())
+ with open(os.path.join(dest, 'CACHE_KEY'), 'wb') as f:
+ f.write(base64.urlsafe_b64encode(cache_hash.digest()).rstrip(b'='))
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser()
+ parser.add_argument('version')
+ args = parser.parse_args()
+
+ with tempfile.TemporaryDirectory() as tmpdir:
+ wheeldir = os.path.join(tmpdir, 'wheels')
+ os.mkdir(wheeldir)
+
+ _msg('building podman image...')
+ _exit_if_retv('podman', 'build', '-q', '-t', IMG, HERE)
+
+ _msg('populating wheels...')
+ _exit_if_retv(
+ 'podman', 'run', '--rm', '--volume', f'{wheeldir}:/wheels:rw', IMG,
+ 'pip', 'wheel', f'pre_commit=={args.version}', 'setuptools',
+ '--wheel-dir', '/wheels',
+ )
+
+ _msg('validating wheels...')
+ _check_no_shared_objects(wheeldir)
+
+ _msg('adding __main__.py...')
+ mainfile = os.path.join(tmpdir, '__main__.py')
+ shutil.copy(os.path.join(HERE, 'entry'), mainfile)
+
+ _msg('adding shim...')
+ _add_shim(tmpdir)
+
+ _msg('copying file_lock.py...')
+ file_lock_py = os.path.join(HERE, '../../pre_commit/file_lock.py')
+ file_lock_py_dest = os.path.join(tmpdir, 'pre_commit/file_lock.py')
+ os.makedirs(os.path.dirname(file_lock_py_dest))
+ shutil.copy(file_lock_py, file_lock_py_dest)
+
+ _msg('writing CACHE_KEY...')
+ _write_cache_key(args.version, wheeldir, tmpdir)
+
+ filename = f'pre-commit-{args.version}.pyz'
+ _msg(f'writing {filename}...')
+ shebang = '/usr/bin/env python3'
+ zipapp.create_archive(tmpdir, filename, interpreter=shebang)
+
+ with open(f'{filename}.sha256sum', 'w') as f:
+ subprocess.check_call(('sha256sum', filename), stdout=f)
+
+ return 0
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())
diff --git a/testing/zipapp/python b/testing/zipapp/python
new file mode 100755
index 0000000..67910fc
--- /dev/null
+++ b/testing/zipapp/python
@@ -0,0 +1,47 @@
+#!/usr/bin/env python3
+"""A shim executable to put dependencies on sys.path"""
+from __future__ import annotations
+
+import argparse
+import os.path
+import runpy
+import sys
+
+# an exe-zipapp will have a __file__ of shim.exe/__main__.py
+EXE = __file__ if os.path.isfile(__file__) else os.path.dirname(__file__)
+EXE = os.path.realpath(EXE)
+HERE = os.path.dirname(EXE)
+WHEELDIR = os.path.join(HERE, 'wheels')
+SITE_DIRS = frozenset(('dist-packages', 'site-packages'))
+
+
+def main() -> int:
+ parser = argparse.ArgumentParser(add_help=False)
+ parser.add_argument('-m')
+ args, rest = parser.parse_known_args()
+
+ if args.m:
+ # try and remove site-packages from sys.path so our packages win
+ sys.path[:] = [
+ p for p in sys.path
+ if os.path.split(p)[1] not in SITE_DIRS
+ ]
+ for wheel in sorted(os.listdir(WHEELDIR)):
+ sys.path.append(os.path.join(WHEELDIR, wheel))
+ if args.m == 'pre_commit' or args.m.startswith('pre_commit.'):
+ sys.executable = EXE
+ sys.argv[1:] = rest
+ runpy.run_module(args.m, run_name='__main__', alter_sys=True)
+ return 0
+ else:
+ cmd = (sys.executable, *sys.argv[1:])
+ if sys.platform == 'win32': # https://bugs.python.org/issue19124
+ import subprocess
+
+ return subprocess.call(cmd)
+ else:
+ os.execvp(cmd[0], cmd)
+
+
+if __name__ == '__main__':
+ raise SystemExit(main())