summaryrefslogtreecommitdiffstats
path: root/tests
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:47:18 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-05-21 20:47:18 +0000
commitceb85610c77b7487b0b7d742415301922c6b13b6 (patch)
tree82456c5d0bc77961759812ddd85414435ba89127 /tests
parentInitial commit. (diff)
downloadpre-commit-hooks-upstream.tar.xz
pre-commit-hooks-upstream.zip
Adding upstream version 4.5.0+dfsg.upstream/4.5.0+dfsgupstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'tests')
-rw-r--r--tests/__init__.py0
-rw-r--r--tests/check_added_large_files_test.py134
-rw-r--r--tests/check_ast_test.py14
-rw-r--r--tests/check_builtin_literals_test.py151
-rw-r--r--tests/check_byte_order_marker_test.py15
-rw-r--r--tests/check_case_conflict_test.py124
-rw-r--r--tests/check_docstring_first_test.py69
-rw-r--r--tests/check_executables_have_shebangs_test.py127
-rw-r--r--tests/check_json_test.py28
-rw-r--r--tests/check_merge_conflict_test.py157
-rw-r--r--tests/check_shebang_scripts_are_executable_test.py89
-rw-r--r--tests/check_symlinks_test.py25
-rw-r--r--tests/check_toml_test.py38
-rw-r--r--tests/check_vcs_permalinks_test.py43
-rw-r--r--tests/check_xml_test.py17
-rw-r--r--tests/check_yaml_test.py53
-rw-r--r--tests/conftest.py12
-rw-r--r--tests/debug_statement_hook_test.py63
-rw-r--r--tests/destroyed_symlinks_test.py75
-rw-r--r--tests/detect_aws_credentials_test.py170
-rw-r--r--tests/detect_private_key_test.py28
-rw-r--r--tests/end_of_file_fixer_test.py44
-rw-r--r--tests/file_contents_sorter_test.py91
-rw-r--r--tests/fix_byte_order_marker_test.py15
-rw-r--r--tests/fix_encoding_pragma_test.py161
-rw-r--r--tests/forbid_new_submodules_test.py59
-rw-r--r--tests/mixed_line_ending_test.py118
-rw-r--r--tests/no_commit_to_branch_test.py79
-rw-r--r--tests/pretty_format_json_test.py139
-rw-r--r--tests/readme_test.py12
-rw-r--r--tests/removed_test.py19
-rw-r--r--tests/requirements_txt_fixer_test.py130
-rw-r--r--tests/sort_simple_yaml_test.py119
-rw-r--r--tests/string_fixer_test.py62
-rw-r--r--tests/tests_should_end_in_test_test.py50
-rw-r--r--tests/trailing_whitespace_fixer_test.py103
-rw-r--r--tests/util_test.py27
37 files changed, 2660 insertions, 0 deletions
diff --git a/tests/__init__.py b/tests/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/tests/__init__.py
diff --git a/tests/check_added_large_files_test.py b/tests/check_added_large_files_test.py
new file mode 100644
index 0000000..54c4e68
--- /dev/null
+++ b/tests/check_added_large_files_test.py
@@ -0,0 +1,134 @@
+from __future__ import annotations
+
+import shutil
+
+import pytest
+
+from pre_commit_hooks.check_added_large_files import find_large_added_files
+from pre_commit_hooks.check_added_large_files import main
+from pre_commit_hooks.util import cmd_output
+from testing.util import git_commit
+
+
+def test_nothing_added(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ assert find_large_added_files(['f.py'], 0) == 0
+
+
+def test_adding_something(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ temp_git_dir.join('f.py').write("print('hello world')")
+ cmd_output('git', 'add', 'f.py')
+
+ # Should fail with max size of 0
+ assert find_large_added_files(['f.py'], 0) == 1
+
+
+def test_add_something_giant(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ temp_git_dir.join('f.py').write('a' * 10000)
+
+ # Should not fail when not added
+ assert find_large_added_files(['f.py'], 0) == 0
+
+ cmd_output('git', 'add', 'f.py')
+
+ # Should fail with strict bound
+ assert find_large_added_files(['f.py'], 0) == 1
+
+ # Should also fail with actual bound
+ assert find_large_added_files(['f.py'], 9) == 1
+
+ # Should pass with higher bound
+ assert find_large_added_files(['f.py'], 10) == 0
+
+
+def test_enforce_all(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ temp_git_dir.join('f.py').write('a' * 10000)
+
+ # Should fail, when not staged with enforce_all
+ assert find_large_added_files(['f.py'], 0, enforce_all=True) == 1
+
+ # Should pass, when not staged without enforce_all
+ assert find_large_added_files(['f.py'], 0, enforce_all=False) == 0
+
+
+def test_added_file_not_in_pre_commits_list(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ temp_git_dir.join('f.py').write("print('hello world')")
+ cmd_output('git', 'add', 'f.py')
+
+ # Should pass even with a size of 0
+ assert find_large_added_files(['g.py'], 0) == 0
+
+
+def test_integration(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ assert main(argv=[]) == 0
+
+ temp_git_dir.join('f.py').write('a' * 10000)
+ cmd_output('git', 'add', 'f.py')
+
+ # Should not fail with default
+ assert main(argv=['f.py']) == 0
+
+ # Should fail with --maxkb
+ assert main(argv=['--maxkb', '9', 'f.py']) == 1
+
+
+def has_gitlfs():
+ return shutil.which('git-lfs') is not None
+
+
+xfailif_no_gitlfs = pytest.mark.xfail(
+ not has_gitlfs(), reason='This test requires git-lfs',
+)
+
+
+@xfailif_no_gitlfs
+def test_allows_gitlfs(temp_git_dir): # pragma: no cover
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'lfs', 'install', '--local')
+ temp_git_dir.join('f.py').write('a' * 10000)
+ cmd_output('git', 'lfs', 'track', 'f.py')
+ cmd_output('git', 'add', '--', '.')
+ # Should succeed
+ assert main(('--maxkb', '9', 'f.py')) == 0
+
+
+@xfailif_no_gitlfs
+def test_moves_with_gitlfs(temp_git_dir): # pragma: no cover
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'lfs', 'install', '--local')
+ cmd_output('git', 'lfs', 'track', 'a.bin', 'b.bin')
+ # First add the file we're going to move
+ temp_git_dir.join('a.bin').write('a' * 10000)
+ cmd_output('git', 'add', '--', '.')
+ git_commit('-am', 'foo')
+ # Now move it and make sure the hook still succeeds
+ cmd_output('git', 'mv', 'a.bin', 'b.bin')
+ assert main(('--maxkb', '9', 'b.bin')) == 0
+
+
+@xfailif_no_gitlfs
+def test_enforce_allows_gitlfs(temp_git_dir): # pragma: no cover
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'lfs', 'install', '--local')
+ temp_git_dir.join('f.py').write('a' * 10000)
+ cmd_output('git', 'lfs', 'track', 'f.py')
+ cmd_output('git', 'add', '--', '.')
+ # With --enforce-all large files on git lfs should succeed
+ assert main(('--enforce-all', '--maxkb', '9', 'f.py')) == 0
+
+
+@xfailif_no_gitlfs
+def test_enforce_allows_gitlfs_after_commit(temp_git_dir): # pragma: no cover
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'lfs', 'install', '--local')
+ temp_git_dir.join('f.py').write('a' * 10000)
+ cmd_output('git', 'lfs', 'track', 'f.py')
+ cmd_output('git', 'add', '--', '.')
+ git_commit('-am', 'foo')
+ # With --enforce-all large files on git lfs should succeed
+ assert main(('--enforce-all', '--maxkb', '9', 'f.py')) == 0
diff --git a/tests/check_ast_test.py b/tests/check_ast_test.py
new file mode 100644
index 0000000..6243966
--- /dev/null
+++ b/tests/check_ast_test.py
@@ -0,0 +1,14 @@
+from __future__ import annotations
+
+from pre_commit_hooks.check_ast import main
+from testing.util import get_resource_path
+
+
+def test_failing_file():
+ ret = main([get_resource_path('cannot_parse_ast.notpy')])
+ assert ret == 1
+
+
+def test_passing_file():
+ ret = main([__file__])
+ assert ret == 0
diff --git a/tests/check_builtin_literals_test.py b/tests/check_builtin_literals_test.py
new file mode 100644
index 0000000..1b18257
--- /dev/null
+++ b/tests/check_builtin_literals_test.py
@@ -0,0 +1,151 @@
+from __future__ import annotations
+
+import ast
+
+import pytest
+
+from pre_commit_hooks.check_builtin_literals import Call
+from pre_commit_hooks.check_builtin_literals import main
+from pre_commit_hooks.check_builtin_literals import Visitor
+
+BUILTIN_CONSTRUCTORS = '''\
+import builtins
+
+c1 = complex()
+d1 = dict()
+f1 = float()
+i1 = int()
+l1 = list()
+s1 = str()
+t1 = tuple()
+
+c2 = builtins.complex()
+d2 = builtins.dict()
+f2 = builtins.float()
+i2 = builtins.int()
+l2 = builtins.list()
+s2 = builtins.str()
+t2 = builtins.tuple()
+'''
+BUILTIN_LITERALS = '''\
+c1 = 0j
+d1 = {}
+f1 = 0.0
+i1 = 0
+l1 = []
+s1 = ''
+t1 = ()
+'''
+
+
+@pytest.fixture
+def visitor():
+ return Visitor()
+
+
+@pytest.mark.parametrize(
+ ('expression', 'calls'),
+ [
+ # see #285
+ ('x[0]()', []),
+ # complex
+ ('0j', []),
+ ('complex()', [Call('complex', 1, 0)]),
+ ('complex(0, 0)', []),
+ ("complex('0+0j')", []),
+ ('builtins.complex()', []),
+ # float
+ ('0.0', []),
+ ('float()', [Call('float', 1, 0)]),
+ ("float('0.0')", []),
+ ('builtins.float()', []),
+ # int
+ ('0', []),
+ ('int()', [Call('int', 1, 0)]),
+ ("int('0')", []),
+ ('builtins.int()', []),
+ # list
+ ('[]', []),
+ ('list()', [Call('list', 1, 0)]),
+ ("list('abc')", []),
+ ("list([c for c in 'abc'])", []),
+ ("list(c for c in 'abc')", []),
+ ('builtins.list()', []),
+ # str
+ ("''", []),
+ ('str()', [Call('str', 1, 0)]),
+ ("str('0')", []),
+ ('builtins.str()', []),
+ # tuple
+ ('()', []),
+ ('tuple()', [Call('tuple', 1, 0)]),
+ ("tuple('abc')", []),
+ ("tuple([c for c in 'abc'])", []),
+ ("tuple(c for c in 'abc')", []),
+ ('builtins.tuple()', []),
+ ],
+)
+def test_non_dict_exprs(visitor, expression, calls):
+ visitor.visit(ast.parse(expression))
+ assert visitor.builtin_type_calls == calls
+
+
+@pytest.mark.parametrize(
+ ('expression', 'calls'),
+ [
+ ('{}', []),
+ ('dict()', [Call('dict', 1, 0)]),
+ ('dict(a=1, b=2, c=3)', []),
+ ("dict(**{'a': 1, 'b': 2, 'c': 3})", []),
+ ("dict([(k, v) for k, v in [('a', 1), ('b', 2), ('c', 3)]])", []),
+ ("dict((k, v) for k, v in [('a', 1), ('b', 2), ('c', 3)])", []),
+ ('builtins.dict()', []),
+ ],
+)
+def test_dict_allow_kwargs_exprs(visitor, expression, calls):
+ visitor.visit(ast.parse(expression))
+ assert visitor.builtin_type_calls == calls
+
+
+@pytest.mark.parametrize(
+ ('expression', 'calls'),
+ [
+ ('dict()', [Call('dict', 1, 0)]),
+ ('dict(a=1, b=2, c=3)', [Call('dict', 1, 0)]),
+ ("dict(**{'a': 1, 'b': 2, 'c': 3})", [Call('dict', 1, 0)]),
+ ('builtins.dict()', []),
+ ],
+)
+def test_dict_no_allow_kwargs_exprs(expression, calls):
+ visitor = Visitor(allow_dict_kwargs=False)
+ visitor.visit(ast.parse(expression))
+ assert visitor.builtin_type_calls == calls
+
+
+def test_ignore_constructors():
+ visitor = Visitor(
+ ignore=('complex', 'dict', 'float', 'int', 'list', 'str', 'tuple'),
+ )
+ visitor.visit(ast.parse(BUILTIN_CONSTRUCTORS))
+ assert visitor.builtin_type_calls == []
+
+
+def test_failing_file(tmpdir):
+ f = tmpdir.join('f.py')
+ f.write(BUILTIN_CONSTRUCTORS)
+ rc = main([str(f)])
+ assert rc == 1
+
+
+def test_passing_file(tmpdir):
+ f = tmpdir.join('f.py')
+ f.write(BUILTIN_LITERALS)
+ rc = main([str(f)])
+ assert rc == 0
+
+
+def test_failing_file_ignore_all(tmpdir):
+ f = tmpdir.join('f.py')
+ f.write(BUILTIN_CONSTRUCTORS)
+ rc = main(['--ignore=complex,dict,float,int,list,str,tuple', str(f)])
+ assert rc == 0
diff --git a/tests/check_byte_order_marker_test.py b/tests/check_byte_order_marker_test.py
new file mode 100644
index 0000000..909a39b
--- /dev/null
+++ b/tests/check_byte_order_marker_test.py
@@ -0,0 +1,15 @@
+from __future__ import annotations
+
+from pre_commit_hooks import check_byte_order_marker
+
+
+def test_failure(tmpdir):
+ f = tmpdir.join('f.txt')
+ f.write_text('ohai', encoding='utf-8-sig')
+ assert check_byte_order_marker.main((str(f),)) == 1
+
+
+def test_success(tmpdir):
+ f = tmpdir.join('f.txt')
+ f.write_text('ohai', encoding='utf-8')
+ assert check_byte_order_marker.main((str(f),)) == 0
diff --git a/tests/check_case_conflict_test.py b/tests/check_case_conflict_test.py
new file mode 100644
index 0000000..a914f45
--- /dev/null
+++ b/tests/check_case_conflict_test.py
@@ -0,0 +1,124 @@
+from __future__ import annotations
+
+import sys
+
+import pytest
+
+from pre_commit_hooks.check_case_conflict import find_conflicting_filenames
+from pre_commit_hooks.check_case_conflict import main
+from pre_commit_hooks.check_case_conflict import parents
+from pre_commit_hooks.util import cmd_output
+from testing.util import git_commit
+
+skip_win32 = pytest.mark.skipif(
+ sys.platform == 'win32',
+ reason='case conflicts between directories and files',
+)
+
+
+def test_parents():
+ assert set(parents('a')) == set()
+ assert set(parents('a/b')) == {'a'}
+ assert set(parents('a/b/c')) == {'a/b', 'a'}
+ assert set(parents('a/b/c/d')) == {'a/b/c', 'a/b', 'a'}
+
+
+def test_nothing_added(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ assert find_conflicting_filenames(['f.py']) == 0
+
+
+def test_adding_something(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ temp_git_dir.join('f.py').write("print('hello world')")
+ cmd_output('git', 'add', 'f.py')
+
+ assert find_conflicting_filenames(['f.py']) == 0
+
+
+def test_adding_something_with_conflict(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ temp_git_dir.join('f.py').write("print('hello world')")
+ cmd_output('git', 'add', 'f.py')
+ temp_git_dir.join('F.py').write("print('hello world')")
+ cmd_output('git', 'add', 'F.py')
+
+ assert find_conflicting_filenames(['f.py', 'F.py']) == 1
+
+
+@skip_win32 # pragma: win32 no cover
+def test_adding_files_with_conflicting_directories(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ temp_git_dir.mkdir('dir').join('x').write('foo')
+ temp_git_dir.mkdir('DIR').join('y').write('foo')
+ cmd_output('git', 'add', '-A')
+
+ assert find_conflicting_filenames([]) == 1
+
+
+@skip_win32 # pragma: win32 no cover
+def test_adding_files_with_conflicting_deep_directories(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ temp_git_dir.mkdir('x').mkdir('y').join('z').write('foo')
+ temp_git_dir.join('X').write('foo')
+ cmd_output('git', 'add', '-A')
+
+ assert find_conflicting_filenames([]) == 1
+
+
+@skip_win32 # pragma: win32 no cover
+def test_adding_file_with_conflicting_directory(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ temp_git_dir.mkdir('dir').join('x').write('foo')
+ temp_git_dir.join('DIR').write('foo')
+ cmd_output('git', 'add', '-A')
+
+ assert find_conflicting_filenames([]) == 1
+
+
+def test_added_file_not_in_pre_commits_list(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ temp_git_dir.join('f.py').write("print('hello world')")
+ cmd_output('git', 'add', 'f.py')
+
+ assert find_conflicting_filenames(['g.py']) == 0
+
+
+def test_file_conflicts_with_committed_file(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ temp_git_dir.join('f.py').write("print('hello world')")
+ cmd_output('git', 'add', 'f.py')
+ git_commit('-m', 'Add f.py')
+
+ temp_git_dir.join('F.py').write("print('hello world')")
+ cmd_output('git', 'add', 'F.py')
+
+ assert find_conflicting_filenames(['F.py']) == 1
+
+
+@skip_win32 # pragma: win32 no cover
+def test_file_conflicts_with_committed_dir(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ temp_git_dir.mkdir('dir').join('x').write('foo')
+ cmd_output('git', 'add', '-A')
+ git_commit('-m', 'Add f.py')
+
+ temp_git_dir.join('DIR').write('foo')
+ cmd_output('git', 'add', '-A')
+
+ assert find_conflicting_filenames([]) == 1
+
+
+def test_integration(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ assert main(argv=[]) == 0
+
+ temp_git_dir.join('f.py').write("print('hello world')")
+ cmd_output('git', 'add', 'f.py')
+
+ assert main(argv=['f.py']) == 0
+
+ temp_git_dir.join('F.py').write("print('hello world')")
+ cmd_output('git', 'add', 'F.py')
+
+ assert main(argv=['F.py']) == 1
diff --git a/tests/check_docstring_first_test.py b/tests/check_docstring_first_test.py
new file mode 100644
index 0000000..8bafae8
--- /dev/null
+++ b/tests/check_docstring_first_test.py
@@ -0,0 +1,69 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.check_docstring_first import check_docstring_first
+from pre_commit_hooks.check_docstring_first import main
+
+
+# Contents, expected, expected_output
+TESTS = (
+ # trivial
+ (b'', 0, ''),
+ # Acceptable
+ (b'"foo"', 0, ''),
+ # Docstring after code
+ (
+ b'from __future__ import unicode_literals\n'
+ b'"foo"\n',
+ 1,
+ '{filename}:2: Module docstring appears after code '
+ '(code seen on line 1).\n',
+ ),
+ # Test double docstring
+ (
+ b'"The real docstring"\n'
+ b'from __future__ import absolute_import\n'
+ b'"fake docstring"\n',
+ 1,
+ '{filename}:3: Multiple module docstrings '
+ '(first docstring on line 1).\n',
+ ),
+ # Test multiple lines of code above
+ (
+ b'import os\n'
+ b'import sys\n'
+ b'"docstring"\n',
+ 1,
+ '{filename}:3: Module docstring appears after code '
+ '(code seen on line 1).\n',
+ ),
+ # String literals in expressions are ok.
+ (b'x = "foo"\n', 0, ''),
+)
+
+
+all_tests = pytest.mark.parametrize(
+ ('contents', 'expected', 'expected_out'), TESTS,
+)
+
+
+@all_tests
+def test_unit(capsys, contents, expected, expected_out):
+ assert check_docstring_first(contents) == expected
+ assert capsys.readouterr()[0] == expected_out.format(filename='<unknown>')
+
+
+@all_tests
+def test_integration(tmpdir, capsys, contents, expected, expected_out):
+ f = tmpdir.join('test.py')
+ f.write_binary(contents)
+ assert main([str(f)]) == expected
+ assert capsys.readouterr()[0] == expected_out.format(filename=str(f))
+
+
+def test_arbitrary_encoding(tmpdir):
+ f = tmpdir.join('f.py')
+ contents = '# -*- coding: cp1252\nx = "£"'.encode('cp1252')
+ f.write_binary(contents)
+ assert main([str(f)]) == 0
diff --git a/tests/check_executables_have_shebangs_test.py b/tests/check_executables_have_shebangs_test.py
new file mode 100644
index 0000000..82d03e3
--- /dev/null
+++ b/tests/check_executables_have_shebangs_test.py
@@ -0,0 +1,127 @@
+from __future__ import annotations
+
+import os
+import sys
+
+import pytest
+
+from pre_commit_hooks import check_executables_have_shebangs
+from pre_commit_hooks.check_executables_have_shebangs import main
+from pre_commit_hooks.util import cmd_output
+
+skip_win32 = pytest.mark.skipif(
+ sys.platform == 'win32',
+ reason="non-git checks aren't relevant on windows",
+)
+
+
+@skip_win32 # pragma: win32 no cover
+@pytest.mark.parametrize(
+ 'content', (
+ b'#!/bin/bash\nhello world\n',
+ b'#!/usr/bin/env python3.6',
+ b'#!python',
+ '#!☃'.encode(),
+ ),
+)
+def test_has_shebang(content, tmpdir):
+ path = tmpdir.join('path')
+ path.write(content, 'wb')
+ assert main((str(path),)) == 0
+
+
+@skip_win32 # pragma: win32 no cover
+@pytest.mark.parametrize(
+ 'content', (
+ b'',
+ b' #!python\n',
+ b'\n#!python\n',
+ b'python\n',
+ '☃'.encode(),
+ ),
+)
+def test_bad_shebang(content, tmpdir, capsys):
+ path = tmpdir.join('path')
+ path.write(content, 'wb')
+ assert main((str(path),)) == 1
+ _, stderr = capsys.readouterr()
+ assert stderr.startswith(f'{path}: marked executable but')
+
+
+def test_check_git_filemode_passing(tmpdir):
+ with tmpdir.as_cwd():
+ cmd_output('git', 'init', '.')
+
+ f = tmpdir.join('f')
+ f.write('#!/usr/bin/env bash')
+ f_path = str(f)
+ cmd_output('chmod', '+x', f_path)
+ cmd_output('git', 'add', f_path)
+ cmd_output('git', 'update-index', '--chmod=+x', f_path)
+
+ g = tmpdir.join('g').ensure()
+ g_path = str(g)
+ cmd_output('git', 'add', g_path)
+
+ # this is potentially a problem, but not something the script intends
+ # to check for -- we're only making sure that things that are
+ # executable have shebangs
+ h = tmpdir.join('h')
+ h.write('#!/usr/bin/env bash')
+ h_path = str(h)
+ cmd_output('git', 'add', h_path)
+
+ files = (f_path, g_path, h_path)
+ assert check_executables_have_shebangs._check_git_filemode(files) == 0
+
+
+def test_check_git_filemode_passing_unusual_characters(tmpdir):
+ with tmpdir.as_cwd():
+ cmd_output('git', 'init', '.')
+
+ f = tmpdir.join('mañana.txt')
+ f.write('#!/usr/bin/env bash')
+ f_path = str(f)
+ cmd_output('chmod', '+x', f_path)
+ cmd_output('git', 'add', f_path)
+ cmd_output('git', 'update-index', '--chmod=+x', f_path)
+
+ files = (f_path,)
+ assert check_executables_have_shebangs._check_git_filemode(files) == 0
+
+
+def test_check_git_filemode_failing(tmpdir):
+ with tmpdir.as_cwd():
+ cmd_output('git', 'init', '.')
+
+ f = tmpdir.join('f').ensure()
+ f_path = str(f)
+ cmd_output('chmod', '+x', f_path)
+ cmd_output('git', 'add', f_path)
+ cmd_output('git', 'update-index', '--chmod=+x', f_path)
+
+ files = (f_path,)
+ assert check_executables_have_shebangs._check_git_filemode(files) == 1
+
+
+@pytest.mark.parametrize(
+ ('content', 'mode', 'expected'),
+ (
+ pytest.param('#!python', '+x', 0, id='shebang with executable'),
+ pytest.param('#!python', '-x', 0, id='shebang without executable'),
+ pytest.param('', '+x', 1, id='no shebang with executable'),
+ pytest.param('', '-x', 0, id='no shebang without executable'),
+ ),
+)
+def test_git_executable_shebang(temp_git_dir, content, mode, expected):
+ with temp_git_dir.as_cwd():
+ path = temp_git_dir.join('path')
+ path.write(content)
+ cmd_output('git', 'add', str(path))
+ cmd_output('chmod', mode, str(path))
+ cmd_output('git', 'update-index', f'--chmod={mode}', str(path))
+
+ # simulate how identify chooses that something is executable
+ filenames = [path for path in [str(path)] if os.access(path, os.X_OK)]
+
+ assert main(filenames) == expected
diff --git a/tests/check_json_test.py b/tests/check_json_test.py
new file mode 100644
index 0000000..53e1f52
--- /dev/null
+++ b/tests/check_json_test.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.check_json import main
+from testing.util import get_resource_path
+
+
+@pytest.mark.parametrize(
+ ('filename', 'expected_retval'), (
+ ('bad_json.notjson', 1),
+ ('bad_json_latin1.nonjson', 1),
+ ('ok_json.json', 0),
+ ('duplicate_key_json.notjson', 1),
+ ),
+)
+def test_main(capsys, filename, expected_retval):
+ ret = main([get_resource_path(filename)])
+ assert ret == expected_retval
+ if expected_retval == 1:
+ stdout, _ = capsys.readouterr()
+ assert filename in stdout
+
+
+def test_non_utf8_file(tmpdir):
+ f = tmpdir.join('t.json')
+ f.write_binary(b'\xa9\xfe\x12')
+ assert main((str(f),))
diff --git a/tests/check_merge_conflict_test.py b/tests/check_merge_conflict_test.py
new file mode 100644
index 0000000..76c4283
--- /dev/null
+++ b/tests/check_merge_conflict_test.py
@@ -0,0 +1,157 @@
+from __future__ import annotations
+
+import os
+import shutil
+
+import pytest
+
+from pre_commit_hooks.check_merge_conflict import main
+from pre_commit_hooks.util import cmd_output
+from testing.util import get_resource_path
+from testing.util import git_commit
+
+
+@pytest.fixture
+def f1_is_a_conflict_file(tmpdir):
+ # Make a merge conflict
+ repo1 = tmpdir.join('repo1')
+ repo1_f1 = repo1.join('f1')
+ repo2 = tmpdir.join('repo2')
+ repo2_f1 = repo2.join('f1')
+
+ cmd_output('git', 'init', '--', str(repo1))
+ with repo1.as_cwd():
+ repo1_f1.ensure()
+ cmd_output('git', 'add', '.')
+ git_commit('-m', 'commit1')
+
+ cmd_output('git', 'clone', str(repo1), str(repo2))
+
+ # Commit in master
+ with repo1.as_cwd():
+ repo1_f1.write('parent\n')
+ git_commit('-am', 'master commit2')
+
+ # Commit in clone and pull
+ with repo2.as_cwd():
+ repo2_f1.write('child\n')
+ git_commit('-am', 'clone commit2')
+ cmd_output('git', 'pull', '--no-rebase', retcode=None)
+ # We should end up in a merge conflict!
+ f1 = repo2_f1.read()
+ assert f1.startswith(
+ '<<<<<<< HEAD\n'
+ 'child\n'
+ '=======\n'
+ 'parent\n'
+ '>>>>>>>',
+ ) or f1.startswith(
+ '<<<<<<< HEAD\n'
+ 'child\n'
+ # diff3 conflict style git merges add this line:
+ '||||||| merged common ancestors\n'
+ '=======\n'
+ 'parent\n'
+ '>>>>>>>',
+ ) or f1.startswith(
+ # .gitconfig with [pull] rebase = preserve causes a rebase which
+ # flips parent / child
+ '<<<<<<< HEAD\n'
+ 'parent\n'
+ '=======\n'
+ 'child\n'
+ '>>>>>>>',
+ )
+ assert os.path.exists(os.path.join('.git', 'MERGE_MSG'))
+ yield repo2
+
+
+@pytest.fixture
+def repository_pending_merge(tmpdir):
+ # Make a (non-conflicting) merge
+ repo1 = tmpdir.join('repo1')
+ repo1_f1 = repo1.join('f1')
+ repo2 = tmpdir.join('repo2')
+ repo2_f1 = repo2.join('f1')
+ repo2_f2 = repo2.join('f2')
+ cmd_output('git', 'init', str(repo1))
+ with repo1.as_cwd():
+ repo1_f1.ensure()
+ cmd_output('git', 'add', '.')
+ git_commit('-m', 'commit1')
+
+ cmd_output('git', 'clone', str(repo1), str(repo2))
+
+ # Commit in master
+ with repo1.as_cwd():
+ repo1_f1.write('parent\n')
+ git_commit('-am', 'master commit2')
+
+ # Commit in clone and pull without committing
+ with repo2.as_cwd():
+ repo2_f2.write('child\n')
+ cmd_output('git', 'add', '.')
+ git_commit('-m', 'clone commit2')
+ cmd_output('git', 'pull', '--no-commit', '--no-rebase')
+ # We should end up in a pending merge
+ assert repo2_f1.read() == 'parent\n'
+ assert repo2_f2.read() == 'child\n'
+ assert os.path.exists(os.path.join('.git', 'MERGE_HEAD'))
+ yield repo2
+
+
+@pytest.mark.usefixtures('f1_is_a_conflict_file')
+def test_merge_conflicts_git(capsys):
+ assert main(['f1']) == 1
+ out, _ = capsys.readouterr()
+ assert out == (
+ "f1:1: Merge conflict string '<<<<<<<' found\n"
+ "f1:3: Merge conflict string '=======' found\n"
+ "f1:5: Merge conflict string '>>>>>>>' found\n"
+ )
+
+
+@pytest.mark.parametrize(
+ 'contents', (b'<<<<<<< HEAD\n', b'=======\n', b'>>>>>>> master\n'),
+)
+def test_merge_conflicts_failing(contents, repository_pending_merge):
+ repository_pending_merge.join('f2').write_binary(contents)
+ assert main(['f2']) == 1
+
+
+@pytest.mark.parametrize(
+ 'contents', (b'# <<<<<<< HEAD\n', b'# =======\n', b'import mod', b''),
+)
+def test_merge_conflicts_ok(contents, f1_is_a_conflict_file):
+ f1_is_a_conflict_file.join('f1').write_binary(contents)
+ assert main(['f1']) == 0
+
+
+@pytest.mark.usefixtures('f1_is_a_conflict_file')
+def test_ignores_binary_files():
+ shutil.copy(get_resource_path('img1.jpg'), 'f1')
+ assert main(['f1']) == 0
+
+
+def test_does_not_care_when_not_in_a_merge(tmpdir):
+ f = tmpdir.join('README.md')
+ f.write_binary(b'problem\n=======\n')
+ assert main([str(f.realpath())]) == 0
+
+
+def test_care_when_assumed_merge(tmpdir):
+ f = tmpdir.join('README.md')
+ f.write_binary(b'problem\n=======\n')
+ assert main([str(f.realpath()), '--assume-in-merge']) == 1
+
+
+def test_worktree_merge_conflicts(f1_is_a_conflict_file, tmpdir, capsys):
+ worktree = tmpdir.join('worktree')
+ cmd_output('git', 'worktree', 'add', str(worktree))
+ with worktree.as_cwd():
+ cmd_output(
+ 'git', 'pull', '--no-rebase', 'origin', 'master', retcode=None,
+ )
+ msg = f1_is_a_conflict_file.join('.git/worktrees/worktree/MERGE_MSG')
+ assert msg.exists()
+ test_merge_conflicts_git(capsys)
diff --git a/tests/check_shebang_scripts_are_executable_test.py b/tests/check_shebang_scripts_are_executable_test.py
new file mode 100644
index 0000000..e4bd07c
--- /dev/null
+++ b/tests/check_shebang_scripts_are_executable_test.py
@@ -0,0 +1,89 @@
+from __future__ import annotations
+
+import os
+
+import pytest
+
+from pre_commit_hooks.check_shebang_scripts_are_executable import \
+ _check_git_filemode
+from pre_commit_hooks.check_shebang_scripts_are_executable import main
+from pre_commit_hooks.util import cmd_output
+
+
+def test_check_git_filemode_passing(tmpdir):
+ with tmpdir.as_cwd():
+ cmd_output('git', 'init', '.')
+
+ f = tmpdir.join('f')
+ f.write('#!/usr/bin/env bash')
+ f_path = str(f)
+ cmd_output('chmod', '+x', f_path)
+ cmd_output('git', 'add', f_path)
+ cmd_output('git', 'update-index', '--chmod=+x', f_path)
+
+ g = tmpdir.join('g').ensure()
+ g_path = str(g)
+ cmd_output('git', 'add', g_path)
+
+ files = [f_path, g_path]
+ assert _check_git_filemode(files) == 0
+
+ # this is the one we should trigger on
+ h = tmpdir.join('h')
+ h.write('#!/usr/bin/env bash')
+ h_path = str(h)
+ cmd_output('git', 'add', h_path)
+
+ files = [h_path]
+ assert _check_git_filemode(files) == 1
+
+
+def test_check_git_filemode_passing_unusual_characters(tmpdir):
+ with tmpdir.as_cwd():
+ cmd_output('git', 'init', '.')
+
+ f = tmpdir.join('mañana.txt')
+ f.write('#!/usr/bin/env bash')
+ f_path = str(f)
+ cmd_output('chmod', '+x', f_path)
+ cmd_output('git', 'add', f_path)
+ cmd_output('git', 'update-index', '--chmod=+x', f_path)
+
+ files = (f_path,)
+ assert _check_git_filemode(files) == 0
+
+
+def test_check_git_filemode_failing(tmpdir):
+ with tmpdir.as_cwd():
+ cmd_output('git', 'init', '.')
+
+ f = tmpdir.join('f').ensure()
+ f.write('#!/usr/bin/env bash')
+ f_path = str(f)
+ cmd_output('git', 'add', f_path)
+
+ files = (f_path,)
+ assert _check_git_filemode(files) == 1
+
+
+@pytest.mark.parametrize(
+ ('content', 'mode', 'expected'),
+ (
+ pytest.param('#!python', '+x', 0, id='shebang with executable'),
+ pytest.param('#!python', '-x', 1, id='shebang without executable'),
+ pytest.param('', '+x', 0, id='no shebang with executable'),
+ pytest.param('', '-x', 0, id='no shebang without executable'),
+ ),
+)
+def test_git_executable_shebang(temp_git_dir, content, mode, expected):
+ with temp_git_dir.as_cwd():
+ path = temp_git_dir.join('path')
+ path.write(content)
+ cmd_output('git', 'add', str(path))
+ cmd_output('chmod', mode, str(path))
+ cmd_output('git', 'update-index', f'--chmod={mode}', str(path))
+
+ # simulate how identify chooses that something is executable
+ filenames = [path for path in [str(path)] if os.access(path, os.X_OK)]
+
+ assert main(filenames) == expected
diff --git a/tests/check_symlinks_test.py b/tests/check_symlinks_test.py
new file mode 100644
index 0000000..e2c2c78
--- /dev/null
+++ b/tests/check_symlinks_test.py
@@ -0,0 +1,25 @@
+from __future__ import annotations
+
+import os
+
+import pytest
+
+from pre_commit_hooks.check_symlinks import main
+
+
+xfail_symlink = pytest.mark.xfail(os.name == 'nt', reason='No symlink support')
+
+
+@xfail_symlink
+@pytest.mark.parametrize(
+ ('dest', 'expected'), (('exists', 0), ('does-not-exist', 1)),
+)
+def test_main(tmpdir, dest, expected): # pragma: no cover (symlinks)
+ tmpdir.join('exists').ensure()
+ symlink = tmpdir.join('symlink')
+ symlink.mksymlinkto(tmpdir.join(dest))
+ assert main((str(symlink),)) == expected
+
+
+def test_main_normal_file(tmpdir):
+ assert main((str(tmpdir.join('f').ensure()),)) == 0
diff --git a/tests/check_toml_test.py b/tests/check_toml_test.py
new file mode 100644
index 0000000..d594f81
--- /dev/null
+++ b/tests/check_toml_test.py
@@ -0,0 +1,38 @@
+from __future__ import annotations
+
+from pre_commit_hooks.check_toml import main
+
+
+def test_toml_bad(tmpdir):
+ filename = tmpdir.join('f')
+ filename.write("""
+key = # INVALID
+
+= "no key name" # INVALID
+""")
+ ret = main((str(filename),))
+ assert ret == 1
+
+
+def test_toml_good(tmpdir):
+ filename = tmpdir.join('f')
+ filename.write(
+ """
+# This is a TOML document.
+
+title = "TOML Example"
+
+[owner]
+name = "John"
+dob = 1979-05-27T07:32:00-08:00 # First class dates
+""",
+ )
+ ret = main((str(filename),))
+ assert ret == 0
+
+
+def test_toml_good_unicode(tmpdir):
+ filename = tmpdir.join('f')
+ filename.write_binary('letter = "\N{SNOWMAN}"\n'.encode())
+ ret = main((str(filename),))
+ assert ret == 0
diff --git a/tests/check_vcs_permalinks_test.py b/tests/check_vcs_permalinks_test.py
new file mode 100644
index 0000000..01ce94d
--- /dev/null
+++ b/tests/check_vcs_permalinks_test.py
@@ -0,0 +1,43 @@
+from __future__ import annotations
+
+from pre_commit_hooks.check_vcs_permalinks import main
+
+
+def test_trivial(tmpdir):
+ f = tmpdir.join('f.txt').ensure()
+ assert not main((str(f),))
+
+
+def test_passing(tmpdir):
+ f = tmpdir.join('f.txt')
+ f.write_binary(
+ # permalinks are ok
+ b'https://github.com/asottile/test/blob/649e6/foo%20bar#L1\n'
+ # tags are ok
+ b'https://github.com/asottile/test/blob/1.0.0/foo%20bar#L1\n'
+ # links to files but not line numbers are ok
+ b'https://github.com/asottile/test/blob/master/foo%20bar\n'
+ # regression test for overly-greedy regex
+ b'https://github.com/ yes / no ? /blob/master/foo#L1\n',
+ )
+ assert not main((str(f),))
+
+
+def test_failing(tmpdir, capsys):
+ with tmpdir.as_cwd():
+ tmpdir.join('f.txt').write_binary(
+ b'https://github.com/asottile/test/blob/master/foo#L1\n'
+ b'https://example.com/asottile/test/blob/master/foo#L1\n'
+ b'https://example.com/asottile/test/blob/main/foo#L1\n',
+ )
+
+ assert main(('f.txt', '--additional-github-domain', 'example.com'))
+ out, _ = capsys.readouterr()
+ assert out == (
+ 'f.txt:1:https://github.com/asottile/test/blob/master/foo#L1\n'
+ 'f.txt:2:https://example.com/asottile/test/blob/master/foo#L1\n'
+ 'f.txt:3:https://example.com/asottile/test/blob/main/foo#L1\n'
+ '\n'
+ 'Non-permanent github link detected.\n'
+ 'On any page on github press [y] to load a permalink.\n'
+ )
diff --git a/tests/check_xml_test.py b/tests/check_xml_test.py
new file mode 100644
index 0000000..767619f
--- /dev/null
+++ b/tests/check_xml_test.py
@@ -0,0 +1,17 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.check_xml import main
+from testing.util import get_resource_path
+
+
+@pytest.mark.parametrize(
+ ('filename', 'expected_retval'), (
+ ('bad_xml.notxml', 1),
+ ('ok_xml.xml', 0),
+ ),
+)
+def test_main(filename, expected_retval):
+ ret = main([get_resource_path(filename)])
+ assert ret == expected_retval
diff --git a/tests/check_yaml_test.py b/tests/check_yaml_test.py
new file mode 100644
index 0000000..54eb16e
--- /dev/null
+++ b/tests/check_yaml_test.py
@@ -0,0 +1,53 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.check_yaml import main
+from testing.util import get_resource_path
+
+
+@pytest.mark.parametrize(
+ ('filename', 'expected_retval'), (
+ ('bad_yaml.notyaml', 1),
+ ('ok_yaml.yaml', 0),
+ ),
+)
+def test_main(filename, expected_retval):
+ ret = main([get_resource_path(filename)])
+ assert ret == expected_retval
+
+
+def test_main_allow_multiple_documents(tmpdir):
+ f = tmpdir.join('test.yaml')
+ f.write('---\nfoo\n---\nbar\n')
+
+ # should fail without the setting
+ assert main((str(f),))
+
+ # should pass when we allow multiple documents
+ assert not main(('--allow-multiple-documents', str(f)))
+
+
+def test_fails_even_with_allow_multiple_documents(tmpdir):
+ f = tmpdir.join('test.yaml')
+ f.write('[')
+ assert main(('--allow-multiple-documents', str(f)))
+
+
+def test_main_unsafe(tmpdir):
+ f = tmpdir.join('test.yaml')
+ f.write(
+ 'some_foo: !vault |\n'
+ ' $ANSIBLE_VAULT;1.1;AES256\n'
+ ' deadbeefdeadbeefdeadbeef\n',
+ )
+ # should fail "safe" check
+ assert main((str(f),))
+ # should pass when we allow unsafe documents
+ assert not main(('--unsafe', str(f)))
+
+
+def test_main_unsafe_still_fails_on_syntax_errors(tmpdir):
+ f = tmpdir.join('test.yaml')
+ f.write('[')
+ assert main(('--unsafe', str(f)))
diff --git a/tests/conftest.py b/tests/conftest.py
new file mode 100644
index 0000000..807f15b
--- /dev/null
+++ b/tests/conftest.py
@@ -0,0 +1,12 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.util import cmd_output
+
+
+@pytest.fixture
+def temp_git_dir(tmpdir):
+ git_dir = tmpdir.join('gits')
+ cmd_output('git', 'init', '--', str(git_dir))
+ yield git_dir
diff --git a/tests/debug_statement_hook_test.py b/tests/debug_statement_hook_test.py
new file mode 100644
index 0000000..5a8e0bb
--- /dev/null
+++ b/tests/debug_statement_hook_test.py
@@ -0,0 +1,63 @@
+from __future__ import annotations
+
+import ast
+
+from pre_commit_hooks.debug_statement_hook import Debug
+from pre_commit_hooks.debug_statement_hook import DebugStatementParser
+from pre_commit_hooks.debug_statement_hook import main
+from testing.util import get_resource_path
+
+
+def test_no_breakpoints():
+ visitor = DebugStatementParser()
+ visitor.visit(ast.parse('import os\nfrom foo import bar\n'))
+ assert visitor.breakpoints == []
+
+
+def test_finds_debug_import_attribute_access():
+ visitor = DebugStatementParser()
+ visitor.visit(ast.parse('import ipdb; ipdb.set_trace()'))
+ assert visitor.breakpoints == [Debug(1, 0, 'ipdb', 'imported')]
+
+
+def test_finds_debug_import_from_import():
+ visitor = DebugStatementParser()
+ visitor.visit(ast.parse('from pudb import set_trace; set_trace()'))
+ assert visitor.breakpoints == [Debug(1, 0, 'pudb', 'imported')]
+
+
+def test_finds_breakpoint():
+ visitor = DebugStatementParser()
+ visitor.visit(ast.parse('breakpoint()'))
+ assert visitor.breakpoints == [Debug(1, 0, 'breakpoint', 'called')]
+
+
+def test_returns_one_for_failing_file(tmpdir):
+ f_py = tmpdir.join('f.py')
+ f_py.write('def f():\n import pdb; pdb.set_trace()')
+ ret = main([str(f_py)])
+ assert ret == 1
+
+
+def test_returns_zero_for_passing_file():
+ ret = main([__file__])
+ assert ret == 0
+
+
+def test_syntaxerror_file():
+ ret = main([get_resource_path('cannot_parse_ast.notpy')])
+ assert ret == 1
+
+
+def test_non_utf8_file(tmpdir):
+ f_py = tmpdir.join('f.py')
+ f_py.write_binary('# -*- coding: cp1252 -*-\nx = "€"\n'.encode('cp1252'))
+ assert main((str(f_py),)) == 0
+
+
+def test_py37_breakpoint(tmpdir, capsys):
+ f_py = tmpdir.join('f.py')
+ f_py.write('def f():\n breakpoint()\n')
+ assert main((str(f_py),)) == 1
+ out, _ = capsys.readouterr()
+ assert out == f'{f_py}:2:4: breakpoint called\n'
diff --git a/tests/destroyed_symlinks_test.py b/tests/destroyed_symlinks_test.py
new file mode 100644
index 0000000..39c474a
--- /dev/null
+++ b/tests/destroyed_symlinks_test.py
@@ -0,0 +1,75 @@
+from __future__ import annotations
+
+import os
+import subprocess
+
+import pytest
+
+from pre_commit_hooks.destroyed_symlinks import find_destroyed_symlinks
+from pre_commit_hooks.destroyed_symlinks import main
+from testing.util import git_commit
+
+TEST_SYMLINK = 'test_symlink'
+TEST_SYMLINK_TARGET = '/doesnt/really/matters'
+TEST_FILE = 'test_file'
+TEST_FILE_RENAMED = f'{TEST_FILE}_renamed'
+
+
+@pytest.fixture
+def repo_with_destroyed_symlink(tmpdir):
+ source_repo = tmpdir.join('src')
+ os.makedirs(source_repo, exist_ok=True)
+ test_repo = tmpdir.join('test')
+ with source_repo.as_cwd():
+ subprocess.check_call(('git', 'init'))
+ os.symlink(TEST_SYMLINK_TARGET, TEST_SYMLINK)
+ with open(TEST_FILE, 'w') as f:
+ print('some random content', file=f)
+ subprocess.check_call(('git', 'add', '.'))
+ git_commit('-m', 'initial')
+ assert b'120000 ' in subprocess.check_output(
+ ('git', 'cat-file', '-p', 'HEAD^{tree}'),
+ )
+ subprocess.check_call(
+ ('git', '-c', 'core.symlinks=false', 'clone', source_repo, test_repo),
+ )
+ with test_repo.as_cwd():
+ subprocess.check_call(
+ ('git', 'config', '--local', 'core.symlinks', 'true'),
+ )
+ subprocess.check_call(('git', 'mv', TEST_FILE, TEST_FILE_RENAMED))
+ assert not os.path.islink(test_repo.join(TEST_SYMLINK))
+ yield test_repo
+
+
+def test_find_destroyed_symlinks(repo_with_destroyed_symlink):
+ with repo_with_destroyed_symlink.as_cwd():
+ assert find_destroyed_symlinks([]) == []
+ assert main([]) == 0
+
+ subprocess.check_call(('git', 'add', TEST_SYMLINK))
+ assert find_destroyed_symlinks([TEST_SYMLINK]) == [TEST_SYMLINK]
+ assert find_destroyed_symlinks([]) == []
+ assert main([]) == 0
+ assert find_destroyed_symlinks([TEST_FILE_RENAMED, TEST_FILE]) == []
+ ALL_STAGED = [TEST_SYMLINK, TEST_FILE_RENAMED]
+ assert find_destroyed_symlinks(ALL_STAGED) == [TEST_SYMLINK]
+ assert main(ALL_STAGED) != 0
+
+ with open(TEST_SYMLINK, 'a') as f:
+ print(file=f) # add trailing newline
+ subprocess.check_call(['git', 'add', TEST_SYMLINK])
+ assert find_destroyed_symlinks(ALL_STAGED) == [TEST_SYMLINK]
+ assert main(ALL_STAGED) != 0
+
+ with open(TEST_SYMLINK, 'w') as f:
+ print('0' * len(TEST_SYMLINK_TARGET), file=f)
+ subprocess.check_call(('git', 'add', TEST_SYMLINK))
+ assert find_destroyed_symlinks(ALL_STAGED) == []
+ assert main(ALL_STAGED) == 0
+
+ with open(TEST_SYMLINK, 'w') as f:
+ print('0' * (len(TEST_SYMLINK_TARGET) + 3), file=f)
+ subprocess.check_call(('git', 'add', TEST_SYMLINK))
+ assert find_destroyed_symlinks(ALL_STAGED) == []
+ assert main(ALL_STAGED) == 0
diff --git a/tests/detect_aws_credentials_test.py b/tests/detect_aws_credentials_test.py
new file mode 100644
index 0000000..afda47a
--- /dev/null
+++ b/tests/detect_aws_credentials_test.py
@@ -0,0 +1,170 @@
+from __future__ import annotations
+
+from unittest.mock import patch
+
+import pytest
+
+from pre_commit_hooks.detect_aws_credentials import get_aws_cred_files_from_env
+from pre_commit_hooks.detect_aws_credentials import get_aws_secrets_from_env
+from pre_commit_hooks.detect_aws_credentials import get_aws_secrets_from_file
+from pre_commit_hooks.detect_aws_credentials import main
+from testing.util import get_resource_path
+
+
+@pytest.mark.parametrize(
+ ('env_vars', 'values'),
+ (
+ ({}, set()),
+ ({'AWS_PLACEHOLDER_KEY': '/foo'}, set()),
+ ({'AWS_CONFIG_FILE': '/foo'}, {'/foo'}),
+ ({'AWS_CREDENTIAL_FILE': '/foo'}, {'/foo'}),
+ ({'AWS_SHARED_CREDENTIALS_FILE': '/foo'}, {'/foo'}),
+ ({'BOTO_CONFIG': '/foo'}, {'/foo'}),
+ ({'AWS_PLACEHOLDER_KEY': '/foo', 'AWS_CONFIG_FILE': '/bar'}, {'/bar'}),
+ (
+ {
+ 'AWS_PLACEHOLDER_KEY': '/foo', 'AWS_CONFIG_FILE': '/bar',
+ 'AWS_CREDENTIAL_FILE': '/baz',
+ },
+ {'/bar', '/baz'},
+ ),
+ (
+ {
+ 'AWS_CONFIG_FILE': '/foo', 'AWS_CREDENTIAL_FILE': '/bar',
+ 'AWS_SHARED_CREDENTIALS_FILE': '/baz',
+ },
+ {'/foo', '/bar', '/baz'},
+ ),
+ ),
+)
+def test_get_aws_credentials_file_from_env(env_vars, values):
+ with patch.dict('os.environ', env_vars, clear=True):
+ assert get_aws_cred_files_from_env() == values
+
+
+@pytest.mark.parametrize(
+ ('env_vars', 'values'),
+ (
+ ({}, set()),
+ ({'AWS_PLACEHOLDER_KEY': 'foo'}, set()),
+ ({'AWS_SECRET_ACCESS_KEY': 'foo'}, {'foo'}),
+ ({'AWS_SECURITY_TOKEN': 'foo'}, {'foo'}),
+ ({'AWS_SESSION_TOKEN': 'foo'}, {'foo'}),
+ ({'AWS_SESSION_TOKEN': ''}, set()),
+ ({'AWS_SESSION_TOKEN': 'foo', 'AWS_SECURITY_TOKEN': ''}, {'foo'}),
+ (
+ {'AWS_PLACEHOLDER_KEY': 'foo', 'AWS_SECRET_ACCESS_KEY': 'bar'},
+ {'bar'},
+ ),
+ (
+ {'AWS_SECRET_ACCESS_KEY': 'foo', 'AWS_SECURITY_TOKEN': 'bar'},
+ {'foo', 'bar'},
+ ),
+ ),
+)
+def test_get_aws_secrets_from_env(env_vars, values):
+ """Test that reading secrets from environment variables works."""
+ with patch.dict('os.environ', env_vars, clear=True):
+ assert get_aws_secrets_from_env() == values
+
+
+@pytest.mark.parametrize(
+ ('filename', 'expected_keys'),
+ (
+ (
+ 'aws_config_with_secret.ini',
+ {'z2rpgs5uit782eapz5l1z0y2lurtsyyk6hcfozlb'},
+ ),
+ ('aws_config_with_session_token.ini', {'foo'}),
+ (
+ 'aws_config_with_secret_and_session_token.ini',
+ {'z2rpgs5uit782eapz5l1z0y2lurtsyyk6hcfozlb', 'foo'},
+ ),
+ (
+ 'aws_config_with_multiple_sections.ini',
+ {
+ '7xebzorgm5143ouge9gvepxb2z70bsb2rtrh099e',
+ 'z2rpgs5uit782eapz5l1z0y2lurtsyyk6hcfozlb',
+ 'ixswosj8gz3wuik405jl9k3vdajsnxfhnpui38ez',
+ 'foo',
+ },
+ ),
+ ('aws_config_without_secrets.ini', set()),
+ ('aws_config_without_secrets_with_spaces.ini', set()),
+ ('nonsense.txt', set()),
+ ('ok_json.json', set()),
+ ),
+)
+def test_get_aws_secrets_from_file(filename, expected_keys):
+ """Test that reading secrets from files works."""
+ keys = get_aws_secrets_from_file(get_resource_path(filename))
+ assert keys == expected_keys
+
+
+@pytest.mark.parametrize(
+ ('filename', 'expected_retval'),
+ (
+ ('aws_config_with_secret.ini', 1),
+ ('aws_config_with_session_token.ini', 1),
+ ('aws_config_with_multiple_sections.ini', 1),
+ ('aws_config_without_secrets.ini', 0),
+ ('aws_config_without_secrets_with_spaces.ini', 0),
+ ('nonsense.txt', 0),
+ ('ok_json.json', 0),
+ ),
+)
+def test_detect_aws_credentials(filename, expected_retval):
+ # with a valid credentials file
+ ret = main((
+ get_resource_path(filename),
+ '--credentials-file',
+ 'testing/resources/aws_config_with_multiple_sections.ini',
+ ))
+ assert ret == expected_retval
+
+
+def test_allows_arbitrarily_encoded_files(tmpdir):
+ src_ini = tmpdir.join('src.ini')
+ src_ini.write(
+ '[default]\n'
+ 'aws_access_key_id=AKIASDFASDF\n'
+ 'aws_secret_Access_key=9018asdf23908190238123\n',
+ )
+ arbitrary_encoding = tmpdir.join('f')
+ arbitrary_encoding.write_binary(b'\x12\x9a\xe2\xf2')
+ ret = main((str(arbitrary_encoding), '--credentials-file', str(src_ini)))
+ assert ret == 0
+
+
+@patch('pre_commit_hooks.detect_aws_credentials.get_aws_secrets_from_file')
+@patch('pre_commit_hooks.detect_aws_credentials.get_aws_secrets_from_env')
+def test_non_existent_credentials(mock_secrets_env, mock_secrets_file, capsys):
+ """Test behavior with no configured AWS secrets."""
+ mock_secrets_env.return_value = set()
+ mock_secrets_file.return_value = set()
+ ret = main((
+ get_resource_path('aws_config_without_secrets.ini'),
+ '--credentials-file=testing/resources/credentailsfilethatdoesntexist',
+ ))
+ assert ret == 2
+ out, _ = capsys.readouterr()
+ assert out == (
+ 'No AWS keys were found in the configured credential files '
+ 'and environment variables.\nPlease ensure you have the '
+ 'correct setting for --credentials-file\n'
+ )
+
+
+@patch('pre_commit_hooks.detect_aws_credentials.get_aws_secrets_from_file')
+@patch('pre_commit_hooks.detect_aws_credentials.get_aws_secrets_from_env')
+def test_non_existent_credentials_with_allow_flag(
+ mock_secrets_env, mock_secrets_file,
+):
+ mock_secrets_env.return_value = set()
+ mock_secrets_file.return_value = set()
+ ret = main((
+ get_resource_path('aws_config_without_secrets.ini'),
+ '--credentials-file=testing/resources/credentailsfilethatdoesntexist',
+ '--allow-missing-credentials',
+ ))
+ assert ret == 0
diff --git a/tests/detect_private_key_test.py b/tests/detect_private_key_test.py
new file mode 100644
index 0000000..41f8bae
--- /dev/null
+++ b/tests/detect_private_key_test.py
@@ -0,0 +1,28 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.detect_private_key import main
+
+# Input, expected return value
+TESTS = (
+ (b'-----BEGIN RSA PRIVATE KEY-----', 1),
+ (b'-----BEGIN DSA PRIVATE KEY-----', 1),
+ (b'-----BEGIN EC PRIVATE KEY-----', 1),
+ (b'-----BEGIN OPENSSH PRIVATE KEY-----', 1),
+ (b'PuTTY-User-Key-File-2: ssh-rsa', 1),
+ (b'---- BEGIN SSH2 ENCRYPTED PRIVATE KEY ----', 1),
+ (b'-----BEGIN ENCRYPTED PRIVATE KEY-----', 1),
+ (b'-----BEGIN OpenVPN Static key V1-----', 1),
+ (b'ssh-rsa DATA', 0),
+ (b'ssh-dsa DATA', 0),
+ # Some arbitrary binary data
+ (b'\xa2\xf1\x93\x12', 0),
+)
+
+
+@pytest.mark.parametrize(('input_s', 'expected_retval'), TESTS)
+def test_main(input_s, expected_retval, tmpdir):
+ path = tmpdir.join('file.txt')
+ path.write_binary(input_s)
+ assert main([str(path)]) == expected_retval
diff --git a/tests/end_of_file_fixer_test.py b/tests/end_of_file_fixer_test.py
new file mode 100644
index 0000000..8a5d889
--- /dev/null
+++ b/tests/end_of_file_fixer_test.py
@@ -0,0 +1,44 @@
+from __future__ import annotations
+
+import io
+
+import pytest
+
+from pre_commit_hooks.end_of_file_fixer import fix_file
+from pre_commit_hooks.end_of_file_fixer import main
+
+
+# Input, expected return value, expected output
+TESTS = (
+ (b'foo\n', 0, b'foo\n'),
+ (b'', 0, b''),
+ (b'\n\n', 1, b''),
+ (b'\n\n\n\n', 1, b''),
+ (b'foo', 1, b'foo\n'),
+ (b'foo\n\n\n', 1, b'foo\n'),
+ (b'\xe2\x98\x83', 1, b'\xe2\x98\x83\n'),
+ (b'foo\r\n', 0, b'foo\r\n'),
+ (b'foo\r\n\r\n\r\n', 1, b'foo\r\n'),
+ (b'foo\r', 0, b'foo\r'),
+ (b'foo\r\r\r\r', 1, b'foo\r'),
+)
+
+
+@pytest.mark.parametrize(('input_s', 'expected_retval', 'output'), TESTS)
+def test_fix_file(input_s, expected_retval, output):
+ file_obj = io.BytesIO(input_s)
+ ret = fix_file(file_obj)
+ assert file_obj.getvalue() == output
+ assert ret == expected_retval
+
+
+@pytest.mark.parametrize(('input_s', 'expected_retval', 'output'), TESTS)
+def test_integration(input_s, expected_retval, output, tmpdir):
+ path = tmpdir.join('file.txt')
+ path.write_binary(input_s)
+
+ ret = main([str(path)])
+ file_output = path.read_binary()
+
+ assert file_output == output
+ assert ret == expected_retval
diff --git a/tests/file_contents_sorter_test.py b/tests/file_contents_sorter_test.py
new file mode 100644
index 0000000..49b3b79
--- /dev/null
+++ b/tests/file_contents_sorter_test.py
@@ -0,0 +1,91 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.file_contents_sorter import FAIL
+from pre_commit_hooks.file_contents_sorter import main
+from pre_commit_hooks.file_contents_sorter import PASS
+
+
+@pytest.mark.parametrize(
+ ('input_s', 'argv', 'expected_retval', 'output'),
+ (
+ (b'', [], PASS, b''),
+ (b'\n', [], FAIL, b''),
+ (b'\n\n', [], FAIL, b''),
+ (b'lonesome\n', [], PASS, b'lonesome\n'),
+ (b'missing_newline', [], FAIL, b'missing_newline\n'),
+ (b'newline\nmissing', [], FAIL, b'missing\nnewline\n'),
+ (b'missing\nnewline', [], FAIL, b'missing\nnewline\n'),
+ (b'alpha\nbeta\n', [], PASS, b'alpha\nbeta\n'),
+ (b'beta\nalpha\n', [], FAIL, b'alpha\nbeta\n'),
+ (b'C\nc\n', [], PASS, b'C\nc\n'),
+ (b'c\nC\n', [], FAIL, b'C\nc\n'),
+ (b'mag ical \n tre vor\n', [], FAIL, b' tre vor\nmag ical \n'),
+ (b'@\n-\n_\n#\n', [], FAIL, b'#\n-\n@\n_\n'),
+ (b'extra\n\n\nwhitespace\n', [], FAIL, b'extra\nwhitespace\n'),
+ (b'whitespace\n\n\nextra\n', [], FAIL, b'extra\nwhitespace\n'),
+ (
+ b'fee\nFie\nFoe\nfum\n',
+ [],
+ FAIL,
+ b'Fie\nFoe\nfee\nfum\n',
+ ),
+ (
+ b'Fie\nFoe\nfee\nfum\n',
+ [],
+ PASS,
+ b'Fie\nFoe\nfee\nfum\n',
+ ),
+ (
+ b'fee\nFie\nFoe\nfum\n',
+ ['--ignore-case'],
+ PASS,
+ b'fee\nFie\nFoe\nfum\n',
+ ),
+ (
+ b'Fie\nFoe\nfee\nfum\n',
+ ['--ignore-case'],
+ FAIL,
+ b'fee\nFie\nFoe\nfum\n',
+ ),
+ (
+ b'Fie\nFoe\nfee\nfee\nfum\n',
+ ['--ignore-case'],
+ FAIL,
+ b'fee\nfee\nFie\nFoe\nfum\n',
+ ),
+ (
+ b'Fie\nFoe\nfee\nfum\n',
+ ['--unique'],
+ PASS,
+ b'Fie\nFoe\nfee\nfum\n',
+ ),
+ (
+ b'Fie\nFie\nFoe\nfee\nfum\n',
+ ['--unique'],
+ FAIL,
+ b'Fie\nFoe\nfee\nfum\n',
+ ),
+ (
+ b'fee\nFie\nFoe\nfum\n',
+ ['--unique', '--ignore-case'],
+ PASS,
+ b'fee\nFie\nFoe\nfum\n',
+ ),
+ (
+ b'fee\nfee\nFie\nFoe\nfum\n',
+ ['--unique', '--ignore-case'],
+ FAIL,
+ b'fee\nFie\nFoe\nfum\n',
+ ),
+ ),
+)
+def test_integration(input_s, argv, expected_retval, output, tmpdir):
+ path = tmpdir.join('file.txt')
+ path.write_binary(input_s)
+
+ output_retval = main([str(path)] + argv)
+
+ assert path.read_binary() == output
+ assert output_retval == expected_retval
diff --git a/tests/fix_byte_order_marker_test.py b/tests/fix_byte_order_marker_test.py
new file mode 100644
index 0000000..d7a6599
--- /dev/null
+++ b/tests/fix_byte_order_marker_test.py
@@ -0,0 +1,15 @@
+from __future__ import annotations
+
+from pre_commit_hooks import fix_byte_order_marker
+
+
+def test_failure(tmpdir):
+ f = tmpdir.join('f.txt')
+ f.write_text('ohai', encoding='utf-8-sig')
+ assert fix_byte_order_marker.main((str(f),)) == 1
+
+
+def test_success(tmpdir):
+ f = tmpdir.join('f.txt')
+ f.write_text('ohai', encoding='utf-8')
+ assert fix_byte_order_marker.main((str(f),)) == 0
diff --git a/tests/fix_encoding_pragma_test.py b/tests/fix_encoding_pragma_test.py
new file mode 100644
index 0000000..98557e9
--- /dev/null
+++ b/tests/fix_encoding_pragma_test.py
@@ -0,0 +1,161 @@
+from __future__ import annotations
+
+import io
+
+import pytest
+
+from pre_commit_hooks.fix_encoding_pragma import _normalize_pragma
+from pre_commit_hooks.fix_encoding_pragma import fix_encoding_pragma
+from pre_commit_hooks.fix_encoding_pragma import main
+
+
+def test_integration_inserting_pragma(tmpdir):
+ path = tmpdir.join('foo.py')
+ path.write_binary(b'import httplib\n')
+
+ assert main((str(path),)) == 1
+
+ assert path.read_binary() == (
+ b'# -*- coding: utf-8 -*-\n'
+ b'import httplib\n'
+ )
+
+
+def test_integration_ok(tmpdir):
+ path = tmpdir.join('foo.py')
+ path.write_binary(b'# -*- coding: utf-8 -*-\nx = 1\n')
+ assert main((str(path),)) == 0
+
+
+def test_integration_remove(tmpdir):
+ path = tmpdir.join('foo.py')
+ path.write_binary(b'# -*- coding: utf-8 -*-\nx = 1\n')
+
+ assert main((str(path), '--remove')) == 1
+
+ assert path.read_binary() == b'x = 1\n'
+
+
+def test_integration_remove_ok(tmpdir):
+ path = tmpdir.join('foo.py')
+ path.write_binary(b'x = 1\n')
+ assert main((str(path), '--remove')) == 0
+
+
+@pytest.mark.parametrize(
+ 'input_str',
+ (
+ b'',
+ (
+ b'# -*- coding: utf-8 -*-\n'
+ b'x = 1\n'
+ ),
+ (
+ b'#!/usr/bin/env python\n'
+ b'# -*- coding: utf-8 -*-\n'
+ b'foo = "bar"\n'
+ ),
+ ),
+)
+def test_ok_inputs(input_str):
+ bytesio = io.BytesIO(input_str)
+ assert fix_encoding_pragma(bytesio) == 0
+ bytesio.seek(0)
+ assert bytesio.read() == input_str
+
+
+@pytest.mark.parametrize(
+ ('input_str', 'output'),
+ (
+ (
+ b'import httplib\n',
+ b'# -*- coding: utf-8 -*-\n'
+ b'import httplib\n',
+ ),
+ (
+ b'#!/usr/bin/env python\n'
+ b'x = 1\n',
+ b'#!/usr/bin/env python\n'
+ b'# -*- coding: utf-8 -*-\n'
+ b'x = 1\n',
+ ),
+ (
+ b'#coding=utf-8\n'
+ b'x = 1\n',
+ b'# -*- coding: utf-8 -*-\n'
+ b'x = 1\n',
+ ),
+ (
+ b'#!/usr/bin/env python\n'
+ b'#coding=utf8\n'
+ b'x = 1\n',
+ b'#!/usr/bin/env python\n'
+ b'# -*- coding: utf-8 -*-\n'
+ b'x = 1\n',
+ ),
+ # These should each get truncated
+ (b'#coding: utf-8\n', b''),
+ (b'# -*- coding: utf-8 -*-\n', b''),
+ (b'#!/usr/bin/env python\n', b''),
+ (b'#!/usr/bin/env python\n#coding: utf8\n', b''),
+ (b'#!/usr/bin/env python\n# -*- coding: utf-8 -*-\n', b''),
+ ),
+)
+def test_not_ok_inputs(input_str, output):
+ bytesio = io.BytesIO(input_str)
+ assert fix_encoding_pragma(bytesio) == 1
+ bytesio.seek(0)
+ assert bytesio.read() == output
+
+
+def test_ok_input_alternate_pragma():
+ input_s = b'# coding: utf-8\nx = 1\n'
+ bytesio = io.BytesIO(input_s)
+ ret = fix_encoding_pragma(bytesio, expected_pragma=b'# coding: utf-8')
+ assert ret == 0
+ bytesio.seek(0)
+ assert bytesio.read() == input_s
+
+
+def test_not_ok_input_alternate_pragma():
+ bytesio = io.BytesIO(b'x = 1\n')
+ ret = fix_encoding_pragma(bytesio, expected_pragma=b'# coding: utf-8')
+ assert ret == 1
+ bytesio.seek(0)
+ assert bytesio.read() == b'# coding: utf-8\nx = 1\n'
+
+
+@pytest.mark.parametrize(
+ ('input_s', 'expected'),
+ (
+ ('# coding: utf-8', b'# coding: utf-8'),
+ # trailing whitespace
+ ('# coding: utf-8\n', b'# coding: utf-8'),
+ ),
+)
+def test_normalize_pragma(input_s, expected):
+ assert _normalize_pragma(input_s) == expected
+
+
+def test_integration_alternate_pragma(tmpdir, capsys):
+ f = tmpdir.join('f.py')
+ f.write('x = 1\n')
+
+ pragma = '# coding: utf-8'
+ assert main((str(f), '--pragma', pragma)) == 1
+ assert f.read() == '# coding: utf-8\nx = 1\n'
+ out, _ = capsys.readouterr()
+ assert out == f'Added `# coding: utf-8` to {str(f)}\n'
+
+
+def test_crlf_ok(tmpdir):
+ f = tmpdir.join('f.py')
+ f.write_binary(b'# -*- coding: utf-8 -*-\r\nx = 1\r\n')
+ assert not main((str(f),))
+
+
+def test_crfl_adds(tmpdir):
+ f = tmpdir.join('f.py')
+ f.write_binary(b'x = 1\r\n')
+ assert main((str(f),))
+ assert f.read_binary() == b'# -*- coding: utf-8 -*-\r\nx = 1\r\n'
diff --git a/tests/forbid_new_submodules_test.py b/tests/forbid_new_submodules_test.py
new file mode 100644
index 0000000..058a329
--- /dev/null
+++ b/tests/forbid_new_submodules_test.py
@@ -0,0 +1,59 @@
+from __future__ import annotations
+
+import os
+import subprocess
+from unittest import mock
+
+import pytest
+
+from pre_commit_hooks.forbid_new_submodules import main
+from testing.util import git_commit
+
+
+@pytest.fixture
+def git_dir_with_git_dir(tmpdir):
+ with tmpdir.as_cwd():
+ subprocess.check_call(('git', 'init', '.'))
+ git_commit('--allow-empty', '-m', 'init')
+ subprocess.check_call(('git', 'init', 'foo'))
+ git_commit('--allow-empty', '-m', 'init', cwd=str(tmpdir.join('foo')))
+ yield
+
+
+@pytest.mark.parametrize(
+ 'cmd',
+ (
+ # Actually add the submodule
+ ('git', 'submodule', 'add', './foo'),
+ # Sneaky submodule add (that doesn't show up in .gitmodules)
+ ('git', 'add', 'foo'),
+ ),
+)
+def test_main_new_submodule(git_dir_with_git_dir, capsys, cmd):
+ subprocess.check_call(cmd)
+ assert main(('random_non-related_file',)) == 0
+ assert main(('foo',)) == 1
+ out, _ = capsys.readouterr()
+ assert out.startswith('foo: new submodule introduced\n')
+
+
+def test_main_new_submodule_committed(git_dir_with_git_dir, capsys):
+ rev_parse_cmd = ('git', 'rev-parse', 'HEAD')
+ from_ref = subprocess.check_output(rev_parse_cmd).decode().strip()
+ subprocess.check_call(('git', 'submodule', 'add', './foo'))
+ git_commit('-m', 'new submodule')
+ to_ref = subprocess.check_output(rev_parse_cmd).decode().strip()
+ with mock.patch.dict(
+ os.environ,
+ {'PRE_COMMIT_FROM_REF': from_ref, 'PRE_COMMIT_TO_REF': to_ref},
+ ):
+ assert main(('random_non-related_file',)) == 0
+ assert main(('foo',)) == 1
+ out, _ = capsys.readouterr()
+ assert out.startswith('foo: new submodule introduced\n')
+
+
+def test_main_no_new_submodule(git_dir_with_git_dir):
+ open('test.py', 'a+').close()
+ subprocess.check_call(('git', 'add', 'test.py'))
+ assert main(('test.py',)) == 0
diff --git a/tests/mixed_line_ending_test.py b/tests/mixed_line_ending_test.py
new file mode 100644
index 0000000..a7e7971
--- /dev/null
+++ b/tests/mixed_line_ending_test.py
@@ -0,0 +1,118 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.mixed_line_ending import main
+
+
+@pytest.mark.parametrize(
+ ('input_s', 'output'),
+ (
+ # mixed with majority of 'LF'
+ (b'foo\r\nbar\nbaz\n', b'foo\nbar\nbaz\n'),
+ # mixed with majority of 'CRLF'
+ (b'foo\r\nbar\nbaz\r\n', b'foo\r\nbar\r\nbaz\r\n'),
+ # mixed with majority of 'CR'
+ (b'foo\rbar\nbaz\r', b'foo\rbar\rbaz\r'),
+ # mixed with as much 'LF' as 'CRLF'
+ (b'foo\r\nbar\n', b'foo\nbar\n'),
+ # mixed with as much 'LF' as 'CR'
+ (b'foo\rbar\n', b'foo\nbar\n'),
+ # mixed with as much 'CRLF' as 'CR'
+ (b'foo\r\nbar\r', b'foo\r\nbar\r\n'),
+ # mixed with as much 'CRLF' as 'LF' as 'CR'
+ (b'foo\r\nbar\nbaz\r', b'foo\nbar\nbaz\n'),
+ ),
+)
+def test_mixed_line_ending_fixes_auto(input_s, output, tmpdir):
+ path = tmpdir.join('file.txt')
+ path.write_binary(input_s)
+ ret = main((str(path),))
+
+ assert ret == 1
+ assert path.read_binary() == output
+
+
+def test_non_mixed_no_newline_end_of_file(tmpdir):
+ path = tmpdir.join('f.txt')
+ path.write_binary(b'foo\nbar\nbaz')
+ assert not main((str(path),))
+ # the hook *could* fix the end of the file, but leaves it alone
+ # this is mostly to document the current behaviour
+ assert path.read_binary() == b'foo\nbar\nbaz'
+
+
+def test_mixed_no_newline_end_of_file(tmpdir):
+ path = tmpdir.join('f.txt')
+ path.write_binary(b'foo\r\nbar\nbaz')
+ assert main((str(path),))
+ # the hook rewrites the end of the file, this is slightly inconsistent
+ # with the non-mixed case but I think this is the better behaviour
+ # this is mostly to document the current behaviour
+ assert path.read_binary() == b'foo\nbar\nbaz\n'
+
+
+@pytest.mark.parametrize(
+ ('fix_option', 'input_s'),
+ (
+ # All --fix=auto with uniform line endings should be ok
+ ('--fix=auto', b'foo\r\nbar\r\nbaz\r\n'),
+ ('--fix=auto', b'foo\rbar\rbaz\r'),
+ ('--fix=auto', b'foo\nbar\nbaz\n'),
+ # --fix=crlf with crlf endings
+ ('--fix=crlf', b'foo\r\nbar\r\nbaz\r\n'),
+ # --fix=lf with lf endings
+ ('--fix=lf', b'foo\nbar\nbaz\n'),
+ ),
+)
+def test_line_endings_ok(fix_option, input_s, tmpdir, capsys):
+ path = tmpdir.join('input.txt')
+ path.write_binary(input_s)
+ ret = main((fix_option, str(path)))
+
+ assert ret == 0
+ assert path.read_binary() == input_s
+ out, _ = capsys.readouterr()
+ assert out == ''
+
+
+def test_no_fix_does_not_modify(tmpdir, capsys):
+ path = tmpdir.join('input.txt')
+ contents = b'foo\r\nbar\rbaz\nwomp\n'
+ path.write_binary(contents)
+ ret = main(('--fix=no', str(path)))
+
+ assert ret == 1
+ assert path.read_binary() == contents
+ out, _ = capsys.readouterr()
+ assert out == f'{path}: mixed line endings\n'
+
+
+def test_fix_lf(tmpdir, capsys):
+ path = tmpdir.join('input.txt')
+ path.write_binary(b'foo\r\nbar\rbaz\n')
+ ret = main(('--fix=lf', str(path)))
+
+ assert ret == 1
+ assert path.read_binary() == b'foo\nbar\nbaz\n'
+ out, _ = capsys.readouterr()
+ assert out == f'{path}: fixed mixed line endings\n'
+
+
+def test_fix_crlf(tmpdir):
+ path = tmpdir.join('input.txt')
+ path.write_binary(b'foo\r\nbar\rbaz\n')
+ ret = main(('--fix=crlf', str(path)))
+
+ assert ret == 1
+ assert path.read_binary() == b'foo\r\nbar\r\nbaz\r\n'
+
+
+def test_fix_lf_all_crlf(tmpdir):
+ """Regression test for #239"""
+ path = tmpdir.join('input.txt')
+ path.write_binary(b'foo\r\nbar\r\n')
+ ret = main(('--fix=lf', str(path)))
+
+ assert ret == 1
+ assert path.read_binary() == b'foo\nbar\n'
diff --git a/tests/no_commit_to_branch_test.py b/tests/no_commit_to_branch_test.py
new file mode 100644
index 0000000..eaae5e6
--- /dev/null
+++ b/tests/no_commit_to_branch_test.py
@@ -0,0 +1,79 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.no_commit_to_branch import is_on_branch
+from pre_commit_hooks.no_commit_to_branch import main
+from pre_commit_hooks.util import cmd_output
+from testing.util import git_commit
+
+
+def test_other_branch(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'checkout', '-b', 'anotherbranch')
+ assert is_on_branch({'master'}) is False
+
+
+def test_multi_branch(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'checkout', '-b', 'another/branch')
+ assert is_on_branch({'master'}) is False
+
+
+def test_multi_branch_fail(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'checkout', '-b', 'another/branch')
+ assert is_on_branch({'another/branch'}) is True
+
+
+def test_master_branch(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ assert is_on_branch({'master'}) is True
+
+
+def test_main_branch_call(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'checkout', '-b', 'other')
+ assert main(('--branch', 'other')) == 1
+
+
+@pytest.mark.parametrize('branch_name', ('b1', 'b2'))
+def test_forbid_multiple_branches(temp_git_dir, branch_name):
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'checkout', '-b', branch_name)
+ assert main(('--branch', 'b1', '--branch', 'b2'))
+
+
+def test_branch_pattern_fail(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'checkout', '-b', 'another/branch')
+ assert is_on_branch(set(), {'another/.*'}) is True
+
+
+@pytest.mark.parametrize('branch_name', ('master', 'another/branch'))
+def test_branch_pattern_multiple_branches_fail(temp_git_dir, branch_name):
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'checkout', '-b', branch_name)
+ assert main(('--branch', 'master', '--pattern', 'another/.*'))
+
+
+def test_main_default_call(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'checkout', '-b', 'anotherbranch')
+ assert main(()) == 0
+
+
+def test_not_on_a_branch(temp_git_dir):
+ with temp_git_dir.as_cwd():
+ git_commit('--allow-empty', '-m1')
+ head = cmd_output('git', 'rev-parse', 'HEAD').strip()
+ cmd_output('git', 'checkout', head)
+ # we're not on a branch!
+ assert main(()) == 0
+
+
+@pytest.mark.parametrize('branch_name', ('master', 'main'))
+def test_default_branch_names(temp_git_dir, branch_name):
+ with temp_git_dir.as_cwd():
+ cmd_output('git', 'checkout', '-b', branch_name)
+ assert main(()) == 1
diff --git a/tests/pretty_format_json_test.py b/tests/pretty_format_json_test.py
new file mode 100644
index 0000000..5ded724
--- /dev/null
+++ b/tests/pretty_format_json_test.py
@@ -0,0 +1,139 @@
+from __future__ import annotations
+
+import os
+import shutil
+
+import pytest
+
+from pre_commit_hooks.pretty_format_json import main
+from pre_commit_hooks.pretty_format_json import parse_num_to_int
+from testing.util import get_resource_path
+
+
+def test_parse_num_to_int():
+ assert parse_num_to_int('0') == 0
+ assert parse_num_to_int('2') == 2
+ assert parse_num_to_int('\t') == '\t'
+ assert parse_num_to_int(' ') == ' '
+
+
+@pytest.mark.parametrize(
+ ('filename', 'expected_retval'), (
+ ('not_pretty_formatted_json.json', 1),
+ ('unsorted_pretty_formatted_json.json', 1),
+ ('non_ascii_pretty_formatted_json.json', 1),
+ ('pretty_formatted_json.json', 0),
+ ),
+)
+def test_main(filename, expected_retval):
+ ret = main([get_resource_path(filename)])
+ assert ret == expected_retval
+
+
+@pytest.mark.parametrize(
+ ('filename', 'expected_retval'), (
+ ('not_pretty_formatted_json.json', 1),
+ ('unsorted_pretty_formatted_json.json', 0),
+ ('non_ascii_pretty_formatted_json.json', 1),
+ ('pretty_formatted_json.json', 0),
+ ),
+)
+def test_unsorted_main(filename, expected_retval):
+ ret = main(['--no-sort-keys', get_resource_path(filename)])
+ assert ret == expected_retval
+
+
+@pytest.mark.parametrize(
+ ('filename', 'expected_retval'), (
+ ('not_pretty_formatted_json.json', 1),
+ ('unsorted_pretty_formatted_json.json', 1),
+ ('non_ascii_pretty_formatted_json.json', 1),
+ ('pretty_formatted_json.json', 1),
+ ('tab_pretty_formatted_json.json', 0),
+ ),
+)
+def test_tab_main(filename, expected_retval):
+ ret = main(['--indent', '\t', get_resource_path(filename)])
+ assert ret == expected_retval
+
+
+def test_non_ascii_main():
+ ret = main((
+ '--no-ensure-ascii',
+ get_resource_path('non_ascii_pretty_formatted_json.json'),
+ ))
+ assert ret == 0
+
+
+def test_autofix_main(tmpdir):
+ srcfile = tmpdir.join('to_be_json_formatted.json')
+ shutil.copyfile(
+ get_resource_path('not_pretty_formatted_json.json'),
+ str(srcfile),
+ )
+
+ # now launch the autofix on that file
+ ret = main(['--autofix', str(srcfile)])
+ # it should have formatted it
+ assert ret == 1
+
+ # file was formatted (shouldn't trigger linter again)
+ ret = main([str(srcfile)])
+ assert ret == 0
+
+
+def test_orderfile_get_pretty_format():
+ ret = main((
+ '--top-keys=alist', get_resource_path('pretty_formatted_json.json'),
+ ))
+ assert ret == 0
+
+
+def test_not_orderfile_get_pretty_format():
+ ret = main((
+ '--top-keys=blah', get_resource_path('pretty_formatted_json.json'),
+ ))
+ assert ret == 1
+
+
+def test_top_sorted_get_pretty_format():
+ ret = main((
+ '--top-keys=01-alist,alist', get_resource_path('top_sorted_json.json'),
+ ))
+ assert ret == 0
+
+
+def test_badfile_main():
+ ret = main([get_resource_path('ok_yaml.yaml')])
+ assert ret == 1
+
+
+def test_diffing_output(capsys):
+ resource_path = get_resource_path('not_pretty_formatted_json.json')
+ expected_retval = 1
+ a = os.path.join('a', resource_path)
+ b = os.path.join('b', resource_path)
+ expected_out = f'''\
+--- {a}
++++ {b}
+@@ -1,6 +1,9 @@
+ {{
+- "foo":
+- "bar",
+- "alist": [2, 34, 234],
+- "blah": null
++ "alist": [
++ 2,
++ 34,
++ 234
++ ],
++ "blah": null,
++ "foo": "bar"
+ }}
+'''
+ actual_retval = main([resource_path])
+ actual_out, actual_err = capsys.readouterr()
+
+ assert actual_retval == expected_retval
+ assert actual_out == expected_out
+ assert actual_err == ''
diff --git a/tests/readme_test.py b/tests/readme_test.py
new file mode 100644
index 0000000..038868d
--- /dev/null
+++ b/tests/readme_test.py
@@ -0,0 +1,12 @@
+from __future__ import annotations
+
+from pre_commit_hooks.check_yaml import yaml
+
+
+def test_readme_contains_all_hooks():
+ with open('README.md', encoding='UTF-8') as f:
+ readme_contents = f.read()
+ with open('.pre-commit-hooks.yaml', encoding='UTF-8') as f:
+ hooks = yaml.load(f)
+ for hook in hooks:
+ assert f'`{hook["id"]}`' in readme_contents
diff --git a/tests/removed_test.py b/tests/removed_test.py
new file mode 100644
index 0000000..cd66957
--- /dev/null
+++ b/tests/removed_test.py
@@ -0,0 +1,19 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.removed import main
+
+
+def test_always_fails():
+ with pytest.raises(SystemExit) as excinfo:
+ main((
+ 'autopep8-wrapper', 'autopep8',
+ 'https://github.com/pre-commit/mirrors-autopep8',
+ '--foo', 'bar',
+ ))
+ msg, = excinfo.value.args
+ assert msg == (
+ '`autopep8-wrapper` has been removed -- '
+ 'use `autopep8` from https://github.com/pre-commit/mirrors-autopep8'
+ )
diff --git a/tests/requirements_txt_fixer_test.py b/tests/requirements_txt_fixer_test.py
new file mode 100644
index 0000000..b725afa
--- /dev/null
+++ b/tests/requirements_txt_fixer_test.py
@@ -0,0 +1,130 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.requirements_txt_fixer import FAIL
+from pre_commit_hooks.requirements_txt_fixer import main
+from pre_commit_hooks.requirements_txt_fixer import PASS
+from pre_commit_hooks.requirements_txt_fixer import Requirement
+
+
+@pytest.mark.parametrize(
+ ('input_s', 'expected_retval', 'output'),
+ (
+ (b'', PASS, b''),
+ (b'\n', PASS, b'\n'),
+ (b'# intentionally empty\n', PASS, b'# intentionally empty\n'),
+ (b'foo\n# comment at end\n', PASS, b'foo\n# comment at end\n'),
+ (b'foo\nbar\n', FAIL, b'bar\nfoo\n'),
+ (b'bar\nfoo\n', PASS, b'bar\nfoo\n'),
+ (b'a\nc\nb\n', FAIL, b'a\nb\nc\n'),
+ (b'a\nc\nb', FAIL, b'a\nb\nc\n'),
+ (b'a\nb\nc', FAIL, b'a\nb\nc\n'),
+ (
+ b'#comment1\nfoo\n#comment2\nbar\n',
+ FAIL,
+ b'#comment2\nbar\n#comment1\nfoo\n',
+ ),
+ (
+ b'#comment1\nbar\n#comment2\nfoo\n',
+ PASS,
+ b'#comment1\nbar\n#comment2\nfoo\n',
+ ),
+ (b'#comment\n\nfoo\nbar\n', FAIL, b'#comment\n\nbar\nfoo\n'),
+ (b'#comment\n\nbar\nfoo\n', PASS, b'#comment\n\nbar\nfoo\n'),
+ (
+ b'foo\n\t#comment with indent\nbar\n',
+ FAIL,
+ b'\t#comment with indent\nbar\nfoo\n',
+ ),
+ (
+ b'bar\n\t#comment with indent\nfoo\n',
+ PASS,
+ b'bar\n\t#comment with indent\nfoo\n',
+ ),
+ (b'\nfoo\nbar\n', FAIL, b'bar\n\nfoo\n'),
+ (b'\nbar\nfoo\n', PASS, b'\nbar\nfoo\n'),
+ (
+ b'pyramid-foo==1\npyramid>=2\n',
+ FAIL,
+ b'pyramid>=2\npyramid-foo==1\n',
+ ),
+ (
+ b'a==1\n'
+ b'c>=1\n'
+ b'bbbb!=1\n'
+ b'c-a>=1;python_version>="3.6"\n'
+ b'e>=2\n'
+ b'd>2\n'
+ b'g<2\n'
+ b'f<=2\n',
+ FAIL,
+ b'a==1\n'
+ b'bbbb!=1\n'
+ b'c>=1\n'
+ b'c-a>=1;python_version>="3.6"\n'
+ b'd>2\n'
+ b'e>=2\n'
+ b'f<=2\n'
+ b'g<2\n',
+ ),
+ (b'ocflib\nDjango\nPyMySQL\n', FAIL, b'Django\nocflib\nPyMySQL\n'),
+ (
+ b'-e git+ssh://git_url@tag#egg=ocflib\nDjango\nPyMySQL\n',
+ FAIL,
+ b'Django\n-e git+ssh://git_url@tag#egg=ocflib\nPyMySQL\n',
+ ),
+ (b'bar\npkg-resources==0.0.0\nfoo\n', FAIL, b'bar\nfoo\n'),
+ (b'foo\npkg-resources==0.0.0\nbar\n', FAIL, b'bar\nfoo\n'),
+ (
+ b'git+ssh://git_url@tag#egg=ocflib\nDjango\nijk\n',
+ FAIL,
+ b'Django\nijk\ngit+ssh://git_url@tag#egg=ocflib\n',
+ ),
+ (
+ b'b==1.0.0\n'
+ b'c=2.0.0 \\\n'
+ b' --hash=sha256:abcd\n'
+ b'a=3.0.0 \\\n'
+ b' --hash=sha256:a1b1c1d1',
+ FAIL,
+ b'a=3.0.0 \\\n'
+ b' --hash=sha256:a1b1c1d1\n'
+ b'b==1.0.0\n'
+ b'c=2.0.0 \\\n'
+ b' --hash=sha256:abcd\n',
+ ),
+ (
+ b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n',
+ PASS,
+ b'a=2.0.0 \\\n --hash=sha256:abcd\nb==1.0.0\n',
+ ),
+ ),
+)
+def test_integration(input_s, expected_retval, output, tmpdir):
+ path = tmpdir.join('file.txt')
+ path.write_binary(input_s)
+
+ output_retval = main([str(path)])
+
+ assert path.read_binary() == output
+ assert output_retval == expected_retval
+
+
+def test_requirement_object():
+ top_of_file = Requirement()
+ top_of_file.comments.append(b'#foo')
+ top_of_file.value = b'\n'
+
+ requirement_foo = Requirement()
+ requirement_foo.value = b'foo'
+
+ requirement_bar = Requirement()
+ requirement_bar.value = b'bar'
+
+ # This may look redundant, but we need to test both foo.__lt__(bar) and
+ # bar.__lt__(foo)
+ assert requirement_foo > top_of_file
+ assert top_of_file < requirement_foo
+ assert requirement_foo > requirement_bar
+ assert requirement_bar < requirement_foo
diff --git a/tests/sort_simple_yaml_test.py b/tests/sort_simple_yaml_test.py
new file mode 100644
index 0000000..6cbda85
--- /dev/null
+++ b/tests/sort_simple_yaml_test.py
@@ -0,0 +1,119 @@
+from __future__ import annotations
+
+import os
+
+import pytest
+
+from pre_commit_hooks.sort_simple_yaml import first_key
+from pre_commit_hooks.sort_simple_yaml import main
+from pre_commit_hooks.sort_simple_yaml import parse_block
+from pre_commit_hooks.sort_simple_yaml import parse_blocks
+from pre_commit_hooks.sort_simple_yaml import sort
+
+RETVAL_GOOD = 0
+RETVAL_BAD = 1
+TEST_SORTS = [
+ (
+ ['c: true', '', 'b: 42', 'a: 19'],
+ ['b: 42', 'a: 19', '', 'c: true'],
+ RETVAL_BAD,
+ ),
+
+ (
+ ['# i am', '# a header', '', 'c: true', '', 'b: 42', 'a: 19'],
+ ['# i am', '# a header', '', 'b: 42', 'a: 19', '', 'c: true'],
+ RETVAL_BAD,
+ ),
+
+ (
+ ['# i am', '# a header', '', 'already: sorted', '', 'yup: i am'],
+ ['# i am', '# a header', '', 'already: sorted', '', 'yup: i am'],
+ RETVAL_GOOD,
+ ),
+
+ (
+ ['# i am', '# a header'],
+ ['# i am', '# a header'],
+ RETVAL_GOOD,
+ ),
+]
+
+
+@pytest.mark.parametrize('bad_lines,good_lines,retval', TEST_SORTS)
+def test_integration_good_bad_lines(tmpdir, bad_lines, good_lines, retval):
+ file_path = os.path.join(str(tmpdir), 'foo.yaml')
+
+ with open(file_path, 'w') as f:
+ f.write('\n'.join(bad_lines) + '\n')
+
+ assert main([file_path]) == retval
+
+ with open(file_path) as f:
+ assert [line.rstrip() for line in f.readlines()] == good_lines
+
+
+def test_parse_header():
+ lines = ['# some header', '# is here', '', 'this is not a header']
+ assert parse_block(lines, header=True) == ['# some header', '# is here']
+ assert lines == ['', 'this is not a header']
+
+ lines = ['this is not a header']
+ assert parse_block(lines, header=True) == []
+ assert lines == ['this is not a header']
+
+
+def test_parse_block():
+ # a normal block
+ lines = ['a: 42', 'b: 17', '', 'c: 19']
+ assert parse_block(lines) == ['a: 42', 'b: 17']
+ assert lines == ['', 'c: 19']
+
+ # a block at the end
+ lines = ['c: 19']
+ assert parse_block(lines) == ['c: 19']
+ assert lines == []
+
+ # no block
+ lines = []
+ assert parse_block(lines) == []
+ assert lines == []
+
+
+def test_parse_blocks():
+ # normal blocks
+ lines = ['a: 42', 'b: 17', '', 'c: 19']
+ assert parse_blocks(lines) == [['a: 42', 'b: 17'], ['c: 19']]
+ assert lines == []
+
+ # a single block
+ lines = ['a: 42', 'b: 17']
+ assert parse_blocks(lines) == [['a: 42', 'b: 17']]
+ assert lines == []
+
+ # no blocks
+ lines = []
+ assert parse_blocks(lines) == []
+ assert lines == []
+
+
+def test_first_key():
+ # first line
+ lines = ['a: 42', 'b: 17', '', 'c: 19']
+ assert first_key(lines) == 'a: 42'
+
+ # second line
+ lines = ['# some comment', 'a: 42', 'b: 17', '', 'c: 19']
+ assert first_key(lines) == 'a: 42'
+
+ # second line with quotes
+ lines = ['# some comment', '"a": 42', 'b: 17', '', 'c: 19']
+ assert first_key(lines) == 'a": 42'
+
+ # no lines (not a real situation)
+ lines = []
+ assert first_key(lines) == ''
+
+
+@pytest.mark.parametrize('bad_lines,good_lines,_', TEST_SORTS)
+def test_sort(bad_lines, good_lines, _):
+ assert sort(bad_lines) == good_lines
diff --git a/tests/string_fixer_test.py b/tests/string_fixer_test.py
new file mode 100644
index 0000000..8eb164c
--- /dev/null
+++ b/tests/string_fixer_test.py
@@ -0,0 +1,62 @@
+from __future__ import annotations
+
+import textwrap
+
+import pytest
+
+from pre_commit_hooks.string_fixer import main
+
+TESTS = (
+ # Base cases
+ ("''", "''", 0),
+ ('""', "''", 1),
+ (r'"\'"', r'"\'"', 0),
+ (r'"\""', r'"\""', 0),
+ (r"'\"\"'", r"'\"\"'", 0),
+ # String somewhere in the line
+ ('x = "foo"', "x = 'foo'", 1),
+ # Test escaped characters
+ (r'"\'"', r'"\'"', 0),
+ # Docstring
+ ('""" Foo """', '""" Foo """', 0),
+ (
+ textwrap.dedent(
+ """
+ x = " \\
+ foo \\
+ "\n
+ """,
+ ),
+ textwrap.dedent(
+ """
+ x = ' \\
+ foo \\
+ '\n
+ """,
+ ),
+ 1,
+ ),
+ ('"foo""bar"', "'foo''bar'", 1),
+ pytest.param(
+ "f'hello{\"world\"}'",
+ "f'hello{\"world\"}'",
+ 0,
+ id='ignore nested fstrings',
+ ),
+)
+
+
+@pytest.mark.parametrize(('input_s', 'output', 'expected_retval'), TESTS)
+def test_rewrite(input_s, output, expected_retval, tmpdir):
+ path = tmpdir.join('file.py')
+ path.write(input_s)
+ retval = main([str(path)])
+ assert path.read() == output
+ assert retval == expected_retval
+
+
+def test_rewrite_crlf(tmpdir):
+ f = tmpdir.join('f.py')
+ f.write_binary(b'"foo"\r\n"bar"\r\n')
+ assert main((str(f),))
+ assert f.read_binary() == b"'foo'\r\n'bar'\r\n"
diff --git a/tests/tests_should_end_in_test_test.py b/tests/tests_should_end_in_test_test.py
new file mode 100644
index 0000000..2b5a0de
--- /dev/null
+++ b/tests/tests_should_end_in_test_test.py
@@ -0,0 +1,50 @@
+from __future__ import annotations
+
+from pre_commit_hooks.tests_should_end_in_test import main
+
+
+def test_main_all_pass():
+ ret = main(['foo_test.py', 'bar_test.py'])
+ assert ret == 0
+
+
+def test_main_one_fails():
+ ret = main(['not_test_ending.py', 'foo_test.py'])
+ assert ret == 1
+
+
+def test_regex():
+ assert main(('foo_test_py',)) == 1
+
+
+def test_main_django_all_pass():
+ ret = main((
+ '--django', 'tests.py', 'test_foo.py', 'test_bar.py',
+ 'tests/test_baz.py',
+ ))
+ assert ret == 0
+
+
+def test_main_django_one_fails():
+ ret = main(['--django', 'not_test_ending.py', 'test_foo.py'])
+ assert ret == 1
+
+
+def test_validate_nested_files_django_one_fails():
+ ret = main(['--django', 'tests/not_test_ending.py', 'test_foo.py'])
+ assert ret == 1
+
+
+def test_main_not_django_fails():
+ ret = main(['foo_test.py', 'bar_test.py', 'test_baz.py'])
+ assert ret == 1
+
+
+def test_main_django_fails():
+ ret = main(['--django', 'foo_test.py', 'test_bar.py', 'test_baz.py'])
+ assert ret == 1
+
+
+def test_main_pytest_test_first():
+ assert main(['--pytest-test-first', 'test_foo.py']) == 0
+ assert main(['--pytest-test-first', 'foo_test.py']) == 1
diff --git a/tests/trailing_whitespace_fixer_test.py b/tests/trailing_whitespace_fixer_test.py
new file mode 100644
index 0000000..c07497a
--- /dev/null
+++ b/tests/trailing_whitespace_fixer_test.py
@@ -0,0 +1,103 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.trailing_whitespace_fixer import main
+
+
+@pytest.mark.parametrize(
+ ('input_s', 'expected'),
+ (
+ ('foo \nbar \n', 'foo\nbar\n'),
+ ('bar\t\nbaz\t\n', 'bar\nbaz\n'),
+ ),
+)
+def test_fixes_trailing_whitespace(input_s, expected, tmpdir):
+ path = tmpdir.join('file.md')
+ path.write(input_s)
+ assert main((str(path),)) == 1
+ assert path.read() == expected
+
+
+def test_ok_no_newline_end_of_file(tmpdir):
+ filename = tmpdir.join('f')
+ filename.write_binary(b'foo\nbar')
+ ret = main((str(filename),))
+ assert filename.read_binary() == b'foo\nbar'
+ assert ret == 0
+
+
+def test_ok_with_dos_line_endings(tmpdir):
+ filename = tmpdir.join('f')
+ filename.write_binary(b'foo\r\nbar\r\nbaz\r\n')
+ ret = main((str(filename),))
+ assert filename.read_binary() == b'foo\r\nbar\r\nbaz\r\n'
+ assert ret == 0
+
+
+@pytest.mark.parametrize('ext', ('md', 'Md', '.md', '*'))
+def test_fixes_markdown_files(tmpdir, ext):
+ path = tmpdir.join('test.md')
+ path.write(
+ 'foo \n' # leaves alone
+ 'bar \n' # less than two so it is removed
+ 'baz \n' # more than two so it becomes two spaces
+ '\t\n' # trailing tabs are stripped anyway
+ '\n ', # whitespace at the end of the file is removed
+ )
+ ret = main((str(path), f'--markdown-linebreak-ext={ext}'))
+ assert ret == 1
+ assert path.read() == (
+ 'foo \n'
+ 'bar\n'
+ 'baz \n'
+ '\n'
+ '\n'
+ )
+
+
+@pytest.mark.parametrize('arg', ('--', 'a.b', 'a/b', ''))
+def test_markdown_linebreak_ext_badopt(arg):
+ with pytest.raises(SystemExit) as excinfo:
+ main(['--markdown-linebreak-ext', arg])
+ assert excinfo.value.code == 2
+
+
+def test_prints_warning_with_no_markdown_ext(capsys, tmpdir):
+ f = tmpdir.join('f').ensure()
+ assert main((str(f), '--no-markdown-linebreak-ext')) == 0
+ out, _ = capsys.readouterr()
+ assert out == '--no-markdown-linebreak-ext now does nothing!\n'
+
+
+def test_preserve_non_utf8_file(tmpdir):
+ non_utf8_bytes_content = b'<a>\xe9 \n</a>\n'
+ path = tmpdir.join('file.txt')
+ path.write_binary(non_utf8_bytes_content)
+ ret = main([str(path)])
+ assert ret == 1
+ assert path.size() == (len(non_utf8_bytes_content) - 1)
+
+
+def test_custom_charset_change(tmpdir):
+ # strip spaces only, no tabs
+ path = tmpdir.join('file.txt')
+ path.write('\ta \t \n')
+ ret = main([str(path), '--chars', ' '])
+ assert ret == 1
+ assert path.read() == '\ta \t\n'
+
+
+def test_custom_charset_no_change(tmpdir):
+ path = tmpdir.join('file.txt')
+ path.write('\ta \t\n')
+ ret = main([str(path), '--chars', ' '])
+ assert ret == 0
+
+
+def test_markdown_with_custom_charset(tmpdir):
+ path = tmpdir.join('file.md')
+ path.write('\ta \t \n')
+ ret = main([str(path), '--chars', ' ', '--markdown-linebreak-ext', '*'])
+ assert ret == 1
+ assert path.read() == '\ta \t \n'
diff --git a/tests/util_test.py b/tests/util_test.py
new file mode 100644
index 0000000..92473e5
--- /dev/null
+++ b/tests/util_test.py
@@ -0,0 +1,27 @@
+from __future__ import annotations
+
+import pytest
+
+from pre_commit_hooks.util import CalledProcessError
+from pre_commit_hooks.util import cmd_output
+from pre_commit_hooks.util import zsplit
+
+
+def test_raises_on_error():
+ with pytest.raises(CalledProcessError):
+ cmd_output('sh', '-c', 'exit 1')
+
+
+def test_output():
+ ret = cmd_output('sh', '-c', 'echo hi')
+ assert ret == 'hi\n'
+
+
+@pytest.mark.parametrize('out', ('\0f1\0f2\0', '\0f1\0f2', 'f1\0f2\0'))
+def test_check_zsplits_str_correctly(out):
+ assert zsplit(out) == ['f1', 'f2']
+
+
+@pytest.mark.parametrize('out', ('\0\0', '\0', ''))
+def test_check_zsplit_returns_empty(out):
+ assert zsplit(out) == []