from __future__ import annotations import os.path import sqlite3 import stat from unittest import mock import pytest from pre_commit import git from pre_commit.store import _get_default_directory from pre_commit.store import _LOCAL_RESOURCES from pre_commit.store import Store from pre_commit.util import CalledProcessError from pre_commit.util import cmd_output from testing.fixtures import git_dir from testing.util import cwd from testing.util import git_commit from testing.util import xfailif_windows def test_our_session_fixture_works(): """There's a session fixture which makes `Store` invariantly raise to prevent writing to the home directory. """ with pytest.raises(AssertionError): Store() def test_get_default_directory_defaults_to_home(): # Not we use the module level one which is not mocked ret = _get_default_directory() expected = os.path.realpath(os.path.expanduser('~/.cache/pre-commit')) assert ret == expected def test_adheres_to_xdg_specification(): with mock.patch.dict( os.environ, {'XDG_CACHE_HOME': '/tmp/fakehome'}, ): ret = _get_default_directory() expected = os.path.realpath('/tmp/fakehome/pre-commit') assert ret == expected def test_uses_environment_variable_when_present(): with mock.patch.dict( os.environ, {'PRE_COMMIT_HOME': '/tmp/pre_commit_home'}, ): ret = _get_default_directory() expected = os.path.realpath('/tmp/pre_commit_home') assert ret == expected def test_store_init(store): # Should create the store directory assert os.path.exists(store.directory) # Should create a README file indicating what the directory is about with open(os.path.join(store.directory, 'README')) as readme_file: readme_contents = readme_file.read() for text_line in ( 'This directory is maintained by the pre-commit project.', 'Learn more: https://github.com/pre-commit/pre-commit', ): assert text_line in readme_contents def test_clone(store, tempdir_factory, log_info_mock): path = git_dir(tempdir_factory) with cwd(path): git_commit() rev = git.head_rev(path) git_commit() ret = store.clone(path, rev) # Should have printed some stuff assert log_info_mock.call_args_list[0][0][0].startswith( 'Initializing environment for ', ) # Should return a directory inside of the store assert os.path.exists(ret) assert ret.startswith(store.directory) # Directory should start with `repo` _, dirname = os.path.split(ret) assert dirname.startswith('repo') # Should be checked out to the rev we specified assert git.head_rev(ret) == rev # Assert there's an entry in the sqlite db for this assert store.select_all_repos() == [(path, rev, ret)] def test_clone_cleans_up_on_checkout_failure(store): with pytest.raises(Exception) as excinfo: # This raises an exception because you can't clone something that # doesn't exist! store.clone('/i_dont_exist_lol', 'fake_rev') assert '/i_dont_exist_lol' in str(excinfo.value) repo_dirs = [ d for d in os.listdir(store.directory) if d.startswith('repo') ] assert repo_dirs == [] def test_clone_when_repo_already_exists(store): # Create an entry in the sqlite db that makes it look like the repo has # been cloned. with sqlite3.connect(store.db_path) as db: db.execute( 'INSERT INTO repos (repo, ref, path) ' 'VALUES ("fake_repo", "fake_ref", "fake_path")', ) assert store.clone('fake_repo', 'fake_ref') == 'fake_path' def test_clone_shallow_failure_fallback_to_complete( store, tempdir_factory, log_info_mock, ): path = git_dir(tempdir_factory) with cwd(path): git_commit() rev = git.head_rev(path) git_commit() # Force shallow clone failure def fake_shallow_clone(self, *args, **kwargs): raise CalledProcessError(1, (), b'', None) store._shallow_clone = fake_shallow_clone ret = store.clone(path, rev) # Should have printed some stuff assert log_info_mock.call_args_list[0][0][0].startswith( 'Initializing environment for ', ) # Should return a directory inside of the store assert os.path.exists(ret) assert ret.startswith(store.directory) # Directory should start with `repo` _, dirname = os.path.split(ret) assert dirname.startswith('repo') # Should be checked out to the rev we specified assert git.head_rev(ret) == rev # Assert there's an entry in the sqlite db for this assert store.select_all_repos() == [(path, rev, ret)] def test_clone_tag_not_on_mainline(store, tempdir_factory): path = git_dir(tempdir_factory) with cwd(path): git_commit() cmd_output('git', 'checkout', 'master', '-b', 'branch') git_commit() cmd_output('git', 'tag', 'v1') cmd_output('git', 'checkout', 'master') cmd_output('git', 'branch', '-D', 'branch') # previously crashed on unreachable refs store.clone(path, 'v1') def test_create_when_directory_exists_but_not_db(store): # In versions <= 0.3.5, there was no sqlite db causing a need for # backward compatibility os.remove(store.db_path) store = Store(store.directory) assert os.path.exists(store.db_path) def test_create_when_store_already_exists(store): # an assertion that this is idempotent and does not crash Store(store.directory) def test_db_repo_name(store): assert store.db_repo_name('repo', ()) == 'repo' assert store.db_repo_name('repo', ('b', 'a', 'c')) == 'repo:b,a,c' def test_local_resources_reflects_reality(): on_disk = { res.removeprefix('empty_template_') for res in os.listdir('pre_commit/resources') if res.startswith('empty_template_') } assert on_disk == {os.path.basename(x) for x in _LOCAL_RESOURCES} def test_mark_config_as_used(store, tmpdir): with tmpdir.as_cwd(): f = tmpdir.join('f').ensure() store.mark_config_used('f') assert store.select_all_configs() == [f.strpath] def test_mark_config_as_used_idempotent(store, tmpdir): test_mark_config_as_used(store, tmpdir) test_mark_config_as_used(store, tmpdir) def test_mark_config_as_used_does_not_exist(store): store.mark_config_used('f') assert store.select_all_configs() == [] def _simulate_pre_1_14_0(store): with store.connect() as db: db.executescript('DROP TABLE configs') def test_select_all_configs_roll_forward(store): _simulate_pre_1_14_0(store) assert store.select_all_configs() == [] def test_mark_config_as_used_roll_forward(store, tmpdir): _simulate_pre_1_14_0(store) test_mark_config_as_used(store, tmpdir) @xfailif_windows # pragma: win32 no cover def test_mark_config_as_used_readonly(tmpdir): cfg = tmpdir.join('f').ensure() store_dir = tmpdir.join('store') # make a store, then we'll convert its directory to be readonly assert not Store(str(store_dir)).readonly # directory didn't exist assert not Store(str(store_dir)).readonly # directory did exist def _chmod_minus_w(p): st = os.stat(p) os.chmod(p, st.st_mode & ~(stat.S_IWUSR | stat.S_IWOTH | stat.S_IWGRP)) _chmod_minus_w(store_dir) for fname in os.listdir(store_dir): assert not os.path.isdir(fname) _chmod_minus_w(os.path.join(store_dir, fname)) store = Store(str(store_dir)) assert store.readonly # should be skipped due to readonly store.mark_config_used(str(cfg)) assert store.select_all_configs() == [] def test_clone_with_recursive_submodules(store, tmp_path): sub = tmp_path.joinpath('sub') sub.mkdir() sub.joinpath('submodule').write_text('i am a submodule') cmd_output('git', '-C', str(sub), 'init', '.') cmd_output('git', '-C', str(sub), 'add', '.') git.commit(str(sub)) repo = tmp_path.joinpath('repo') repo.mkdir() repo.joinpath('repository').write_text('i am a repo') cmd_output('git', '-C', str(repo), 'init', '.') cmd_output('git', '-C', str(repo), 'add', '.') cmd_output('git', '-C', str(repo), 'submodule', 'add', str(sub), 'sub') git.commit(str(repo)) rev = git.head_rev(str(repo)) ret = store.clone(str(repo), rev) assert os.path.exists(ret) assert os.path.exists(os.path.join(ret, str(repo), 'repository')) assert os.path.exists(os.path.join(ret, str(sub), 'submodule'))