diff options
Diffstat (limited to '')
-rw-r--r-- | tests/commands/autoupdate_test.py | 532 |
1 files changed, 532 insertions, 0 deletions
diff --git a/tests/commands/autoupdate_test.py b/tests/commands/autoupdate_test.py new file mode 100644 index 0000000..71bd044 --- /dev/null +++ b/tests/commands/autoupdate_test.py @@ -0,0 +1,532 @@ +from __future__ import annotations + +import shlex +from unittest import mock + +import pytest + +import pre_commit.constants as C +from pre_commit import envcontext +from pre_commit import git +from pre_commit import yaml +from pre_commit.commands.autoupdate import _check_hooks_still_exist_at_rev +from pre_commit.commands.autoupdate import autoupdate +from pre_commit.commands.autoupdate import RepositoryCannotBeUpdatedError +from pre_commit.commands.autoupdate import RevInfo +from pre_commit.util import cmd_output +from testing.auto_namedtuple import auto_namedtuple +from testing.fixtures import add_config_to_repo +from testing.fixtures import make_config_from_repo +from testing.fixtures import make_repo +from testing.fixtures import modify_manifest +from testing.fixtures import read_config +from testing.fixtures import sample_local_config +from testing.fixtures import write_config +from testing.util import git_commit + + +@pytest.fixture +def up_to_date(tempdir_factory): + yield make_repo(tempdir_factory, 'python_hooks_repo') + + +@pytest.fixture +def out_of_date(tempdir_factory): + path = make_repo(tempdir_factory, 'python_hooks_repo') + original_rev = git.head_rev(path) + + git_commit(cwd=path) + head_rev = git.head_rev(path) + + yield auto_namedtuple( + path=path, original_rev=original_rev, head_rev=head_rev, + ) + + +@pytest.fixture +def tagged(out_of_date): + cmd_output('git', 'tag', 'v1.2.3', cwd=out_of_date.path) + yield out_of_date + + +@pytest.fixture +def hook_disappearing(tempdir_factory): + path = make_repo(tempdir_factory, 'python_hooks_repo') + original_rev = git.head_rev(path) + + with modify_manifest(path) as manifest: + manifest[0]['id'] = 'bar' + + yield auto_namedtuple(path=path, original_rev=original_rev) + + +def test_rev_info_from_config(): + info = RevInfo.from_config({'repo': 'repo/path', 'rev': 'v1.2.3'}) + assert info == RevInfo('repo/path', 'v1.2.3', None) + + +def test_rev_info_update_up_to_date_repo(up_to_date): + config = make_config_from_repo(up_to_date) + info = RevInfo.from_config(config)._replace(hook_ids=frozenset(('foo',))) + new_info = info.update(tags_only=False, freeze=False) + assert info == new_info + + +def test_rev_info_update_out_of_date_repo(out_of_date): + config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, + ) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=False, freeze=False) + assert new_info.rev == out_of_date.head_rev + + +def test_rev_info_update_non_master_default_branch(out_of_date): + # change the default branch to be not-master + cmd_output('git', '-C', out_of_date.path, 'branch', '-m', 'dev') + test_rev_info_update_out_of_date_repo(out_of_date) + + +def test_rev_info_update_tags_even_if_not_tags_only(tagged): + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=False, freeze=False) + assert new_info.rev == 'v1.2.3' + + +def test_rev_info_update_tags_only_does_not_pick_tip(tagged): + git_commit(cwd=tagged.path) + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=False) + assert new_info.rev == 'v1.2.3' + + +def test_rev_info_update_tags_prefers_version_tag(tagged, out_of_date): + cmd_output('git', 'tag', 'latest', cwd=out_of_date.path) + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=False) + assert new_info.rev == 'v1.2.3' + + +def test_rev_info_update_tags_non_version_tag(out_of_date): + cmd_output('git', 'tag', 'latest', cwd=out_of_date.path) + config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, + ) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=False) + assert new_info.rev == 'latest' + + +def test_rev_info_update_freeze_tag(tagged): + git_commit(cwd=tagged.path) + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=True) + assert new_info.rev == tagged.head_rev + assert new_info.frozen == 'v1.2.3' + + +def test_rev_info_update_does_not_freeze_if_already_sha(out_of_date): + config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, + ) + info = RevInfo.from_config(config) + new_info = info.update(tags_only=True, freeze=True) + assert new_info.rev == out_of_date.head_rev + assert new_info.frozen is None + + +def test_autoupdate_up_to_date_repo(up_to_date, tmpdir): + contents = ( + f'repos:\n' + f'- repo: {up_to_date}\n' + f' rev: {git.head_rev(up_to_date)}\n' + f' hooks:\n' + f' - id: foo\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) + + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 + assert cfg.read() == contents + + +def test_autoupdate_old_revision_broken(tempdir_factory, in_tmpdir): + """In $FUTURE_VERSION, hooks.yaml will no longer be supported. This + asserts that when that day comes, pre-commit will be able to autoupdate + despite not being able to read hooks.yaml in that repository. + """ + path = make_repo(tempdir_factory, 'python_hooks_repo') + config = make_config_from_repo(path, check=False) + + cmd_output('git', 'mv', C.MANIFEST_FILE, 'nope.yaml', cwd=path) + git_commit(cwd=path) + # Assume this is the revision the user's old repository was at + rev = git.head_rev(path) + cmd_output('git', 'mv', 'nope.yaml', C.MANIFEST_FILE, cwd=path) + git_commit(cwd=path) + update_rev = git.head_rev(path) + + config['rev'] = rev + write_config('.', config) + with open(C.CONFIG_FILE) as f: + before = f.read() + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 + with open(C.CONFIG_FILE) as f: + after = f.read() + assert before != after + assert update_rev in after + + +def test_autoupdate_out_of_date_repo(out_of_date, tmpdir): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev)) + + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 + assert cfg.read() == fmt.format(out_of_date.path, out_of_date.head_rev) + + +def test_autoupdate_with_core_useBuiltinFSMonitor(out_of_date, tmpdir): + # force the setting on "globally" for git + home = tmpdir.join('fakehome').ensure_dir() + home.join('.gitconfig').write('[core]\nuseBuiltinFSMonitor = true\n') + with envcontext.envcontext((('HOME', str(home)),)): + test_autoupdate_out_of_date_repo(out_of_date, tmpdir) + + +def test_autoupdate_pure_yaml(out_of_date, tmpdir): + with mock.patch.object(yaml, 'Dumper', yaml.yaml.SafeDumper): + test_autoupdate_out_of_date_repo(out_of_date, tmpdir) + + +def test_autoupdate_only_one_to_update(up_to_date, out_of_date, tmpdir): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' + '- repo: {}\n' + ' rev: {}\n' + ' hooks:\n' + ' - id: foo\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + before = fmt.format( + up_to_date, git.head_rev(up_to_date), + out_of_date.path, out_of_date.original_rev, + ) + cfg.write(before) + + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 + assert cfg.read() == fmt.format( + up_to_date, git.head_rev(up_to_date), + out_of_date.path, out_of_date.head_rev, + ) + + +def test_autoupdate_out_of_date_repo_with_correct_repo_name( + out_of_date, in_tmpdir, +): + stale_config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, check=False, + ) + local_config = sample_local_config() + config = {'repos': [stale_config, local_config]} + write_config('.', config) + + with open(C.CONFIG_FILE) as f: + before = f.read() + repo_name = f'file://{out_of_date.path}' + ret = autoupdate( + C.CONFIG_FILE, freeze=False, tags_only=False, + repos=(repo_name,), + ) + with open(C.CONFIG_FILE) as f: + after = f.read() + assert ret == 0 + assert before != after + assert out_of_date.head_rev in after + assert 'local' in after + + +def test_autoupdate_out_of_date_repo_with_wrong_repo_name( + out_of_date, in_tmpdir, +): + config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, check=False, + ) + write_config('.', config) + + with open(C.CONFIG_FILE) as f: + before = f.read() + # It will not update it, because the name doesn't match + ret = autoupdate( + C.CONFIG_FILE, freeze=False, tags_only=False, + repos=('dne',), + ) + with open(C.CONFIG_FILE) as f: + after = f.read() + assert ret == 0 + assert before == after + + +def test_does_not_reformat(tmpdir, out_of_date): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {} # definitely the version I want!\n' + ' hooks:\n' + ' - id: foo\n' + ' # These args are because reasons!\n' + ' args: [foo, bar, baz]\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(fmt.format(out_of_date.path, out_of_date.original_rev)) + + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 + expected = fmt.format(out_of_date.path, out_of_date.head_rev) + assert cfg.read() == expected + + +def test_does_not_change_mixed_endlines_read(up_to_date, tmpdir): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {} # definitely the version I want!\r\n' + ' hooks:\r\n' + ' - id: foo\n' + ' # These args are because reasons!\r\n' + ' args: [foo, bar, baz]\r\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + + expected = fmt.format(up_to_date, git.head_rev(up_to_date)).encode() + cfg.write_binary(expected) + + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 + assert cfg.read_binary() == expected + + +def test_does_not_change_mixed_endlines_write(tmpdir, out_of_date): + fmt = ( + 'repos:\n' + '- repo: {}\n' + ' rev: {} # definitely the version I want!\r\n' + ' hooks:\r\n' + ' - id: foo\n' + ' # These args are because reasons!\r\n' + ' args: [foo, bar, baz]\r\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write_binary( + fmt.format(out_of_date.path, out_of_date.original_rev).encode(), + ) + + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 + expected = fmt.format(out_of_date.path, out_of_date.head_rev).encode() + assert cfg.read_binary() == expected + + +def test_loses_formatting_when_not_detectable(out_of_date, tmpdir): + """A best-effort attempt is made at updating rev without rewriting + formatting. When the original formatting cannot be detected, this + is abandoned. + """ + config = ( + 'repos: [\n' + ' {{\n' + ' repo: {}, rev: {},\n' + ' hooks: [\n' + ' # A comment!\n' + ' {{id: foo}},\n' + ' ],\n' + ' }}\n' + ']\n'.format( + shlex.quote(out_of_date.path), out_of_date.original_rev, + ) + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(config) + + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 + expected = ( + f'repos:\n' + f'- repo: {out_of_date.path}\n' + f' rev: {out_of_date.head_rev}\n' + f' hooks:\n' + f' - id: foo\n' + ) + assert cfg.read() == expected + + +def test_autoupdate_tagged_repo(tagged, in_tmpdir): + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + write_config('.', config) + + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 + with open(C.CONFIG_FILE) as f: + assert 'v1.2.3' in f.read() + + +def test_autoupdate_freeze(tagged, in_tmpdir): + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + write_config('.', config) + + assert autoupdate(C.CONFIG_FILE, freeze=True, tags_only=False) == 0 + with open(C.CONFIG_FILE) as f: + expected = f'rev: {tagged.head_rev} # frozen: v1.2.3' + assert expected in f.read() + + # if we un-freeze it should remove the frozen comment + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 + with open(C.CONFIG_FILE) as f: + assert 'rev: v1.2.3\n' in f.read() + + +def test_autoupdate_tags_only(tagged, in_tmpdir): + # add some commits after the tag + git_commit(cwd=tagged.path) + + config = make_config_from_repo(tagged.path, rev=tagged.original_rev) + write_config('.', config) + + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=True) == 0 + with open(C.CONFIG_FILE) as f: + assert 'v1.2.3' in f.read() + + +def test_autoupdate_latest_no_config(out_of_date, in_tmpdir): + config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, + ) + write_config('.', config) + + cmd_output('git', 'rm', '-r', ':/', cwd=out_of_date.path) + git_commit(cwd=out_of_date.path) + + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 1 + with open(C.CONFIG_FILE) as f: + assert out_of_date.original_rev in f.read() + + +def test_hook_disppearing_repo_raises(hook_disappearing): + config = make_config_from_repo( + hook_disappearing.path, + rev=hook_disappearing.original_rev, + hooks=[{'id': 'foo'}], + ) + info = RevInfo.from_config(config).update(tags_only=False, freeze=False) + with pytest.raises(RepositoryCannotBeUpdatedError): + _check_hooks_still_exist_at_rev(config, info) + + +def test_autoupdate_hook_disappearing_repo(hook_disappearing, tmpdir): + contents = ( + f'repos:\n' + f'- repo: {hook_disappearing.path}\n' + f' rev: {hook_disappearing.original_rev}\n' + f' hooks:\n' + f' - id: foo\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(contents) + + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 1 + assert cfg.read() == contents + + +def test_autoupdate_local_hooks(in_git_dir): + config = sample_local_config() + add_config_to_repo('.', config) + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 + new_config_written = read_config('.') + assert len(new_config_written['repos']) == 1 + assert new_config_written['repos'][0] == config + + +def test_autoupdate_local_hooks_with_out_of_date_repo( + out_of_date, in_tmpdir, +): + stale_config = make_config_from_repo( + out_of_date.path, rev=out_of_date.original_rev, check=False, + ) + local_config = sample_local_config() + config = {'repos': [local_config, stale_config]} + write_config('.', config) + assert autoupdate(C.CONFIG_FILE, freeze=False, tags_only=False) == 0 + new_config_written = read_config('.') + assert len(new_config_written['repos']) == 2 + assert new_config_written['repos'][0] == local_config + + +def test_autoupdate_meta_hooks(tmpdir): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write( + 'repos:\n' + '- repo: meta\n' + ' hooks:\n' + ' - id: check-useless-excludes\n', + ) + assert autoupdate(str(cfg), freeze=False, tags_only=True) == 0 + assert cfg.read() == ( + 'repos:\n' + '- repo: meta\n' + ' hooks:\n' + ' - id: check-useless-excludes\n' + ) + + +def test_updates_old_format_to_new_format(tmpdir, capsys): + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write( + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n', + ) + assert autoupdate(str(cfg), freeze=False, tags_only=True) == 0 + contents = cfg.read() + assert contents == ( + 'repos:\n' + '- repo: local\n' + ' hooks:\n' + ' - id: foo\n' + ' name: foo\n' + ' entry: ./bin/foo.sh\n' + ' language: script\n' + ) + out, _ = capsys.readouterr() + assert out == 'Configuration has been migrated.\n' + + +def test_maintains_rev_quoting_style(tmpdir, out_of_date): + fmt = ( + 'repos:\n' + '- repo: {path}\n' + ' rev: "{rev}"\n' + ' hooks:\n' + ' - id: foo\n' + '- repo: {path}\n' + " rev: '{rev}'\n" + ' hooks:\n' + ' - id: foo\n' + ) + cfg = tmpdir.join(C.CONFIG_FILE) + cfg.write(fmt.format(path=out_of_date.path, rev=out_of_date.original_rev)) + + assert autoupdate(str(cfg), freeze=False, tags_only=False) == 0 + expected = fmt.format(path=out_of_date.path, rev=out_of_date.head_rev) + assert cfg.read() == expected |