From 2cfd2818b58853c323f99c4b3c4afcecc46aeda8 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Mon, 16 Jun 2014 21:11:00 -0700 Subject: [PATCH] Add pcre type. --- pre_commit/languages/all.py | 2 + pre_commit/languages/helpers.py | 6 +- pre_commit/languages/pcre.py | 27 +++++++ pre_commit/languages/script.py | 6 +- pre_commit/languages/system.py | 6 +- pre_commit/util.py | 5 +- testing/resources/pcre_hooks_repo/hooks.yaml | 10 +++ tests/languages/all_test.py | 27 ++++++- tests/languages/helpers_test.py | 16 +++++ tests/repository_test.py | 76 +++++++++++++++++++- tests/util_test.py | 13 ++++ 11 files changed, 184 insertions(+), 10 deletions(-) create mode 100644 pre_commit/languages/pcre.py create mode 100644 testing/resources/pcre_hooks_repo/hooks.yaml create mode 100644 tests/languages/helpers_test.py diff --git a/pre_commit/languages/all.py b/pre_commit/languages/all.py index 9506fbea..4aa8787f 100644 --- a/pre_commit/languages/all.py +++ b/pre_commit/languages/all.py @@ -1,6 +1,7 @@ from __future__ import unicode_literals from pre_commit.languages import node +from pre_commit.languages import pcre from pre_commit.languages import python from pre_commit.languages import ruby from pre_commit.languages import script @@ -36,6 +37,7 @@ from pre_commit.languages import system languages = { 'node': node, + 'pcre': pcre, 'python': python, 'ruby': ruby, 'script': script, diff --git a/pre_commit/languages/helpers.py b/pre_commit/languages/helpers.py index 780d928f..474fc6e9 100644 --- a/pre_commit/languages/helpers.py +++ b/pre_commit/languages/helpers.py @@ -1,10 +1,14 @@ from __future__ import unicode_literals +def file_args_to_stdin(file_args): + return '\n'.join(list(file_args) + ['']) + + def run_hook(env, hook, file_args): return env.run( ' '.join(['xargs', hook['entry']] + hook['args']), - stdin='\n'.join(list(file_args) + ['']), + stdin=file_args_to_stdin(file_args), retcode=None, ) diff --git a/pre_commit/languages/pcre.py b/pre_commit/languages/pcre.py new file mode 100644 index 00000000..eebe9ac0 --- /dev/null +++ b/pre_commit/languages/pcre.py @@ -0,0 +1,27 @@ +from __future__ import unicode_literals + +from pre_commit.languages.helpers import file_args_to_stdin +from pre_commit.util import shell_escape + + +ENVIRONMENT_DIR = None + + +def install_environment(repo_cmd_runner, version='default'): + """Installation for pcre type is a noop.""" + raise AssertionError('Cannot install pcre repo.') + + +def run_hook(repo_cmd_runner, hook, file_args): + # For PCRE the entry is the regular expression to match + return repo_cmd_runner.run( + [ + 'xargs', 'sh', '-c', + # Grep usually returns 0 for matches, and nonzero for non-matches + # so we flip it here. + '! grep -H -n -P {0} $@'.format(shell_escape(hook['entry'])), + '--', + ], + stdin=file_args_to_stdin(file_args), + retcode=None, + ) diff --git a/pre_commit/languages/script.py b/pre_commit/languages/script.py index 860d4bf6..37531128 100644 --- a/pre_commit/languages/script.py +++ b/pre_commit/languages/script.py @@ -1,16 +1,20 @@ from __future__ import unicode_literals +from pre_commit.languages.helpers import file_args_to_stdin + + ENVIRONMENT_DIR = None def install_environment(repo_cmd_runner, version='default'): """Installation for script type is a noop.""" + raise AssertionError('Cannot install script repo.') def run_hook(repo_cmd_runner, hook, file_args): return repo_cmd_runner.run( ['xargs', '{{prefix}}{0}'.format(hook['entry'])] + hook['args'], # TODO: this is duplicated in pre_commit/languages/helpers.py - stdin='\n'.join(list(file_args) + ['']), + stdin=file_args_to_stdin(file_args), retcode=None, ) diff --git a/pre_commit/languages/system.py b/pre_commit/languages/system.py index a75c618a..c2b65503 100644 --- a/pre_commit/languages/system.py +++ b/pre_commit/languages/system.py @@ -2,18 +2,20 @@ from __future__ import unicode_literals import shlex +from pre_commit.languages.helpers import file_args_to_stdin + ENVIRONMENT_DIR = None def install_environment(repo_cmd_runner, version='default'): """Installation for system type is a noop.""" + raise AssertionError('Cannot install system repo.') def run_hook(repo_cmd_runner, hook, file_args): return repo_cmd_runner.run( ['xargs'] + shlex.split(hook['entry']) + hook['args'], - # TODO: this is duplicated in pre_commit/languages/helpers.py - stdin='\n'.join(list(file_args) + ['']), + stdin=file_args_to_stdin(file_args), retcode=None, ) diff --git a/pre_commit/util.py b/pre_commit/util.py index 4c4b37df..5b9822fa 100644 --- a/pre_commit/util.py +++ b/pre_commit/util.py @@ -49,7 +49,10 @@ def clean_path_on_failure(path): raise -# TODO: asottile.contextlib this with a forward port of nested @contextlib.contextmanager def noop_context(): yield + + +def shell_escape(arg): + return "'" + arg.replace("'", "'\"'\"'".strip()) + "'" diff --git a/testing/resources/pcre_hooks_repo/hooks.yaml b/testing/resources/pcre_hooks_repo/hooks.yaml new file mode 100644 index 00000000..700bf972 --- /dev/null +++ b/testing/resources/pcre_hooks_repo/hooks.yaml @@ -0,0 +1,10 @@ +- id: regex-with-quotes + name: Regex with quotes + entry: "foo'bar" + language: pcre + files: '' +- id: other-regex + name: Other regex + entry: ^\[INFO\] + language: pcre + files: '' diff --git a/tests/languages/all_test.py b/tests/languages/all_test.py index a66162dd..1f84c6ce 100644 --- a/tests/languages/all_test.py +++ b/tests/languages/all_test.py @@ -1,5 +1,6 @@ from __future__ import unicode_literals +import inspect import pytest from pre_commit.languages.all import all_languages @@ -7,7 +8,27 @@ from pre_commit.languages.all import languages @pytest.mark.parametrize('language', all_languages) -def test_all_languages_support_interface(language): - assert hasattr(languages[language], 'install_environment') - assert hasattr(languages[language], 'run_hook') +def test_install_environment_argspec(language): + expected_argspec = inspect.ArgSpec( + args=['repo_cmd_runner', 'version'], + varargs=None, + keywords=None, + defaults=('default',), + ) + argspec = inspect.getargspec(languages[language].install_environment) + assert argspec == expected_argspec + + +@pytest.mark.parametrize('language', all_languages) +def test_ENVIRONMENT_DIR(language): assert hasattr(languages[language], 'ENVIRONMENT_DIR') + + +@pytest.mark.parametrize('language', all_languages) +def test_run_hook_argpsec(language): + expected_argspec = inspect.ArgSpec( + args=['repo_cmd_runner', 'hook', 'file_args'], + varargs=None, keywords=None, defaults=None, + ) + argspec = inspect.getargspec(languages[language].run_hook) + assert argspec == expected_argspec diff --git a/tests/languages/helpers_test.py b/tests/languages/helpers_test.py new file mode 100644 index 00000000..b9dfdf47 --- /dev/null +++ b/tests/languages/helpers_test.py @@ -0,0 +1,16 @@ +from __future__ import absolute_import +from __future__ import unicode_literals + +from pre_commit.languages.helpers import file_args_to_stdin + + +def test_file_args_to_stdin_empty(): + assert file_args_to_stdin([]) == '' + + +def test_file_args_to_stdin_some(): + assert file_args_to_stdin(['foo', 'bar']) == 'foo\nbar\n' + + +def test_file_args_to_stdin_tuple(): + assert file_args_to_stdin(('foo', 'bar')) == 'foo\nbar\n' diff --git a/tests/repository_test.py b/tests/repository_test.py index 5d130e88..efcfc387 100644 --- a/tests/repository_test.py +++ b/tests/repository_test.py @@ -1,6 +1,7 @@ from __future__ import absolute_import from __future__ import unicode_literals +import io import mock import os.path import pytest @@ -25,12 +26,20 @@ def test_install_python_repo_in_env(tmpdir_factory, store): assert os.path.exists(os.path.join(store.directory, repo.sha, 'py_env')) -def _test_hook_repo(tmpdir_factory, store, repo_path, hook_id, args, expected): +def _test_hook_repo( + tmpdir_factory, + store, + repo_path, + hook_id, + args, + expected, + expected_return_code=0, +): path = make_repo(tmpdir_factory, repo_path) config = make_config_from_repo(path) repo = Repository.create(config, store) ret = repo.run_hook(hook_id, args) - assert ret[0] == 0 + assert ret[0] == expected_return_code assert ret[1] == expected @@ -102,6 +111,69 @@ def test_run_a_script_hook(tmpdir_factory, store): ) +@pytest.mark.integration +def test_pcre_hook_no_match(tmpdir_factory, store): + path = git_dir(tmpdir_factory) + with local.cwd(path): + with io.open('herp', 'w') as herp: + herp.write('foo') + + with io.open('derp', 'w') as derp: + derp.write('bar') + + _test_hook_repo( + tmpdir_factory, store, 'pcre_hooks_repo', + 'regex-with-quotes', ['herp', 'derp'], '', + ) + + _test_hook_repo( + tmpdir_factory, store, 'pcre_hooks_repo', + 'other-regex', ['herp', 'derp'], '', + ) + + +@pytest.mark.integration +def test_pcre_hook_matching(tmpdir_factory, store): + path = git_dir(tmpdir_factory) + with local.cwd(path): + with io.open('herp', 'w') as herp: + herp.write("\nherpfoo'bard\n") + + with io.open('derp', 'w') as derp: + derp.write('[INFO] information yo\n') + + _test_hook_repo( + tmpdir_factory, store, 'pcre_hooks_repo', + 'regex-with-quotes', ['herp', 'derp'], "herp:2:herpfoo'bard\n", + expected_return_code=123, + ) + + _test_hook_repo( + tmpdir_factory, store, 'pcre_hooks_repo', + 'other-regex', ['herp', 'derp'], 'derp:1:[INFO] information yo\n', + expected_return_code=123, + ) + + +@pytest.mark.integration +def test_pcre_many_files(tmpdir_factory, store): + # This is intended to simulate lots of passing files and one failing file + # to make sure it still fails. This is not the case when naively using + # a system hook with `grep -H -n '...'` and expected_return_code=123. + path = git_dir(tmpdir_factory) + with local.cwd(path): + with io.open('herp', 'w') as herp: + herp.write('[INFO] info\n') + + _test_hook_repo( + tmpdir_factory, store, 'pcre_hooks_repo', + 'other-regex', + ['/dev/null'] * 15000 + ['herp'], + 'herp:1:[INFO] info\n', + expected_return_code=123, + ) + + @pytest.mark.integration def test_cwd_of_hook(tmpdir_factory, store): # Note: this doubles as a test for `system` hooks diff --git a/tests/util_test.py b/tests/util_test.py index 0eb2cf7e..e406d604 100644 --- a/tests/util_test.py +++ b/tests/util_test.py @@ -11,6 +11,7 @@ from plumbum import local from pre_commit.util import clean_path_on_failure from pre_commit.util import entry from pre_commit.util import memoize_by_cwd +from pre_commit.util import shell_escape @pytest.fixture @@ -99,3 +100,15 @@ def test_clean_path_on_failure_cleans_for_system_exit(in_tmpdir): raise MySystemExit assert not os.path.exists('foo') + + +@pytest.mark.parametrize( + ('input_str', 'expected'), + ( + ('', "''"), + ('foo"bar', "'foo\"bar'"), + ("foo'bar", "'foo'\"'\"'bar'") + ), +) +def test_shell_escape(input_str, expected): + assert shell_escape(input_str) == expected