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