1# -*- coding: utf-8 -*- 2 3"""Tests around cloning repositories and detection of errors at it.""" 4 5import os 6import subprocess 7 8import pytest 9 10from cookiecutter import exceptions, vcs 11 12 13@pytest.fixture 14def clone_dir(tmpdir): 15 """Simulate creation of a directory called `clone_dir` inside of `tmpdir`. \ 16 Returns a str to said directory.""" 17 return str(tmpdir.mkdir('clone_dir')) 18 19 20def test_clone_should_raise_if_vcs_not_installed(mocker, clone_dir): 21 """In `clone()`, a `VCSNotInstalled` exception should be raised if no VCS \ 22 is installed.""" 23 mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=False) 24 25 repo_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git' 26 27 with pytest.raises(exceptions.VCSNotInstalled): 28 vcs.clone(repo_url, clone_to_dir=clone_dir) 29 30 31def test_clone_should_rstrip_trailing_slash_in_repo_url(mocker, clone_dir): 32 """In `clone()`, repo URL's trailing slash should be stripped if one is \ 33 present.""" 34 mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True) 35 36 mock_subprocess = mocker.patch( 37 'cookiecutter.vcs.subprocess.check_output', autospec=True, 38 ) 39 40 vcs.clone('https://github.com/foo/bar/', clone_to_dir=clone_dir, no_input=True) 41 42 mock_subprocess.assert_called_once_with( 43 ['git', 'clone', 'https://github.com/foo/bar'], 44 cwd=clone_dir, 45 stderr=subprocess.STDOUT, 46 ) 47 48 49def test_clone_should_abort_if_user_does_not_want_to_reclone(mocker, tmpdir): 50 """In `clone()`, if user doesn't want to reclone, Cookiecutter should exit \ 51 without cloning anything.""" 52 mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True) 53 mocker.patch( 54 'cookiecutter.vcs.prompt_and_delete', side_effect=SystemExit, autospec=True 55 ) 56 mock_subprocess = mocker.patch( 57 'cookiecutter.vcs.subprocess.check_output', autospec=True, 58 ) 59 60 clone_to_dir = tmpdir.mkdir('clone') 61 62 # Create repo_dir to trigger prompt_and_delete 63 clone_to_dir.mkdir('cookiecutter-pytest-plugin') 64 65 repo_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git' 66 67 with pytest.raises(SystemExit): 68 vcs.clone(repo_url, clone_to_dir=str(clone_to_dir)) 69 assert not mock_subprocess.called 70 71 72@pytest.mark.parametrize( 73 'repo_type, repo_url, repo_name', 74 [ 75 ('git', 'https://github.com/hello/world.git', 'world'), 76 ('hg', 'https://bitbucket.org/foo/bar', 'bar'), 77 ], 78) 79def test_clone_should_invoke_vcs_command( 80 mocker, clone_dir, repo_type, repo_url, repo_name 81): 82 """When `clone()` is called with a git/hg repo, the corresponding VCS \ 83 command should be run via `subprocess.check_output()`. 84 85 This should take place: 86 * In the correct dir 87 * With the correct args. 88 """ 89 mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True) 90 91 mock_subprocess = mocker.patch( 92 'cookiecutter.vcs.subprocess.check_output', autospec=True, 93 ) 94 expected_repo_dir = os.path.normpath(os.path.join(clone_dir, repo_name)) 95 96 branch = 'foobar' 97 98 repo_dir = vcs.clone( 99 repo_url, checkout=branch, clone_to_dir=clone_dir, no_input=True 100 ) 101 102 assert repo_dir == expected_repo_dir 103 104 mock_subprocess.assert_any_call( 105 [repo_type, 'clone', repo_url], cwd=clone_dir, stderr=subprocess.STDOUT 106 ) 107 mock_subprocess.assert_any_call( 108 [repo_type, 'checkout', branch], cwd=expected_repo_dir, stderr=subprocess.STDOUT 109 ) 110 111 112@pytest.mark.parametrize( 113 'error_message', 114 [ 115 ( 116 "fatal: repository 'https://github.com/hackebro/cookiedozer' " "not found" 117 ).encode('utf-8'), 118 'hg: abort: HTTP Error 404: Not Found'.encode('utf-8'), 119 ], 120) 121def test_clone_handles_repo_typo(mocker, clone_dir, error_message): 122 """In `clone()`, repository not found errors should raise an \ 123 appropriate exception.""" 124 # side_effect is set to an iterable here (and below), 125 # because of a Python 3.4 unittest.mock regression 126 # http://bugs.python.org/issue23661 127 mocker.patch( 128 'cookiecutter.vcs.subprocess.check_output', 129 autospec=True, 130 side_effect=[subprocess.CalledProcessError(-1, 'cmd', output=error_message)], 131 ) 132 133 repository_url = 'https://github.com/hackebro/cookiedozer' 134 with pytest.raises(exceptions.RepositoryNotFound) as err: 135 vcs.clone(repository_url, clone_to_dir=clone_dir, no_input=True) 136 137 assert str(err.value) == ( 138 'The repository {} could not be found, have you made a typo?' 139 ).format(repository_url) 140 141 142@pytest.mark.parametrize( 143 'error_message', 144 [ 145 ( 146 "error: pathspec 'unknown_branch' did not match any file(s) known " "to git" 147 ).encode('utf-8'), 148 "hg: abort: unknown revision 'unknown_branch'!".encode('utf-8'), 149 ], 150) 151def test_clone_handles_branch_typo(mocker, clone_dir, error_message): 152 """In `clone()`, branch not found errors should raise an \ 153 appropriate exception.""" 154 mocker.patch( 155 'cookiecutter.vcs.subprocess.check_output', 156 autospec=True, 157 side_effect=[subprocess.CalledProcessError(-1, 'cmd', output=error_message)], 158 ) 159 160 repository_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin' 161 with pytest.raises(exceptions.RepositoryCloneFailed) as err: 162 vcs.clone( 163 repository_url, 164 clone_to_dir=clone_dir, 165 checkout='unknown_branch', 166 no_input=True, 167 ) 168 169 assert str(err.value) == ( 170 'The unknown_branch branch of repository ' 171 '{} could not found, have you made a typo?' 172 ).format(repository_url) 173 174 175def test_clone_unknown_subprocess_error(mocker, clone_dir): 176 """In `clone()`, unknown subprocess errors should be raised.""" 177 mocker.patch( 178 'cookiecutter.vcs.subprocess.check_output', 179 autospec=True, 180 side_effect=[ 181 subprocess.CalledProcessError( 182 -1, 'cmd', output='Something went wrong'.encode('utf-8') 183 ) 184 ], 185 ) 186 187 with pytest.raises(subprocess.CalledProcessError): 188 vcs.clone( 189 'https://github.com/pytest-dev/cookiecutter-pytest-plugin', 190 clone_to_dir=clone_dir, 191 no_input=True, 192 ) 193