### tests/test_generate_context.py
"""Verify generate context behaviour and context overwrite priorities."""

import os
import re
from collections import OrderedDict

import pytest

from cookiecutter import generate
from cookiecutter.exceptions import ContextDecodingException


def context_data():
    """Generate pytest parametrization variables for test.

    Return ('input_params, expected_context') tuples.
    """
    context = (
        {'context_file': 'tests/test-generate-context/test.json'},
        {'test': {'1': 2, 'some_key': 'some_val'}},
    )

    context_with_default = (
        {
            'context_file': 'tests/test-generate-context/test.json',
            'default_context': {'1': 3},
        },
        {'test': {'1': 3, 'some_key': 'some_val'}},
    )

    context_with_extra = (
        {
            'context_file': 'tests/test-generate-context/test.json',
            'extra_context': {'1': 4},
        },
        {'test': {'1': 4, 'some_key': 'some_val'}},
    )

    context_with_default_and_extra = (
        {
            'context_file': 'tests/test-generate-context/test.json',
            'default_context': {'1': 3},
            'extra_context': {'1': 5},
        },
        {'test': {'1': 5, 'some_key': 'some_val'}},
    )

    yield context
    yield context_with_default
    yield context_with_extra
    yield context_with_default_and_extra


@pytest.mark.usefixtures('clean_system')
@pytest.mark.parametrize('input_params, expected_context', context_data())
def test_generate_context(input_params, expected_context):
    """Verify input contexts combinations result in expected content on output."""
    assert generate.generate_context(**input_params) == expected_context


@pytest.mark.usefixtures('clean_system')
def test_generate_context_with_json_decoding_error():
    """Verify malformed JSON file generates expected error output."""
    with pytest.raises(ContextDecodingException) as excinfo:
        generate.generate_context('tests/test-generate-context/invalid-syntax.json')
    # original message from json module should be included
    pattern = 'Expecting \'{0,1}:\'{0,1} delimiter: line 1 column (19|20) \\(char 19\\)'
    assert re.search(pattern, str(excinfo.value))
    # File name should be included too...for testing purposes, just test the
    # last part of the file. If we wanted to test the absolute path, we'd have
    # to do some additional work in the test which doesn't seem that needed at
    # this point.
    path = os.path.sep.join(['tests', 'test-generate-context', 'invalid-syntax.json'])
    assert path in str(excinfo.value)


def test_default_context_replacement_in_generate_context():
    """Verify default content settings are correctly replaced by template settings.

    Make sure that the default for list variables of `orientation` is based on
    the user config (`choices_template.json`) and not changed to a single value
    from `default_context`.
    """
    expected_context = {
        'choices_template': OrderedDict(
            [
                ('full_name', 'Raphael Pierzina'),
                ('github_username', 'hackebrot'),
                ('project_name', 'Kivy Project'),
                ('repo_name', '{{cookiecutter.project_name|lower}}'),
                ('orientation', ['landscape', 'all', 'portrait']),
            ]
        )
    }

    generated_context = generate.generate_context(
        context_file='tests/test-generate-context/choices_template.json',
        default_context={
            'not_in_template': 'foobar',
            'project_name': 'Kivy Project',
            'orientation': 'landscape',
        },
        extra_context={
            'also_not_in_template': 'foobar2',
            'github_username': 'hackebrot',
        },
    )

    assert generated_context == expected_context


def test_generate_context_decodes_non_ascii_chars():
    """Verify `generate_context` correctly decodes non-ascii chars."""
    expected_context = {
        'non_ascii': OrderedDict(
            [
                ('full_name', 'éèà'),
            ]
        )
    }

    generated_context = generate.generate_context(
        context_file='tests/test-generate-context/non_ascii.json'
    )

    assert generated_context == expected_context


@pytest.fixture
def template_context():
    """Fixture. Populates template content for future tests."""
    return OrderedDict(
        [
            ('full_name', 'Raphael Pierzina'),
            ('github_username', 'hackebrot'),
            ('project_name', 'Kivy Project'),
            ('repo_name', '{{cookiecutter.project_name|lower}}'),
            ('orientation', ['all', 'landscape', 'portrait']),
            ('deployment_regions', ['eu', 'us', 'ap']),
            (
                'deployments',
                {
                    'preprod': ['eu', 'us', 'ap'],
                    'prod': ['eu', 'us', 'ap'],
                },
            ),
        ]
    )


def test_apply_overwrites_does_include_unused_variables(template_context):
    """Verify `apply_overwrites_to_context` skips variables that are not in context."""
    generate.apply_overwrites_to_context(
        context=template_context, overwrite_context={'not in template': 'foobar'}
    )

    assert 'not in template' not in template_context


def test_apply_overwrites_sets_non_list_value(template_context):
    """Verify `apply_overwrites_to_context` work with string variables."""
    generate.apply_overwrites_to_context(
        context=template_context, overwrite_context={'repo_name': 'foobar'}
    )

    assert template_context['repo_name'] == 'foobar'


def test_apply_overwrites_does_not_modify_choices_for_invalid_overwrite():
    """Verify variables overwrite for list if variable not in list ignored."""
    expected_context = {
        'choices_template': OrderedDict(
            [
                ('full_name', 'Raphael Pierzina'),
                ('github_username', 'hackebrot'),
                ('project_name', 'Kivy Project'),
                ('repo_name', '{{cookiecutter.project_name|lower}}'),
                ('orientation', ['all', 'landscape', 'portrait']),
            ]
        )
    }

    with pytest.warns(UserWarning, match="Invalid default received"):
        generated_context = generate.generate_context(
            context_file='tests/test-generate-context/choices_template.json',
            default_context={
                'not_in_template': 'foobar',
                'project_name': 'Kivy Project',
                'orientation': 'foobar',
            },
            extra_context={
                'also_not_in_template': 'foobar2',
                'github_username': 'hackebrot',
            },
        )

    assert generated_context == expected_context


def test_apply_overwrites_invalid_overwrite(template_context):
    """Verify variables overwrite for list if variable not in list not ignored."""
    with pytest.raises(ValueError):
        generate.apply_overwrites_to_context(
            context=template_context, overwrite_context={'orientation': 'foobar'}
        )


def test_apply_overwrites_sets_multichoice_values(template_context):
    """Verify variable overwrite for list given multiple valid values."""
    generate.apply_overwrites_to_context(
        context=template_context,
        overwrite_context={'deployment_regions': ['eu']},
    )
    assert template_context['deployment_regions'] == ['eu']


def test_apply_overwrites_invalid_multichoice_values(template_context):
    """Verify variable overwrite for list given invalid list entries not ignored."""
    with pytest.raises(ValueError):
        generate.apply_overwrites_to_context(
            context=template_context,
            overwrite_context={'deployment_regions': ['na']},
        )


def test_apply_overwrites_error_additional_values(template_context):
    """Verify variable overwrite for list given additional entries not ignored."""
    with pytest.raises(ValueError):
        generate.apply_overwrites_to_context(
            context=template_context,
            overwrite_context={'deployment_regions': ['eu', 'na']},
        )


def test_apply_overwrites_in_dictionaries(template_context):
    """Verify variable overwrite for lists nested in dictionary variables."""
    generate.apply_overwrites_to_context(
        context=template_context,
        overwrite_context={'deployments': {'preprod': ['eu'], 'prod': ['ap']}},
    )
    assert template_context['deployments']['preprod'] == ['eu']
    assert template_context['deployments']['prod'] == ['ap']


def test_apply_overwrites_sets_default_for_choice_variable(template_context):
    """Verify overwritten list member became a default value."""
    generate.apply_overwrites_to_context(
        context=template_context, overwrite_context={'orientation': 'landscape'}
    )

    assert template_context['orientation'] == ['landscape', 'all', 'portrait']


def test_apply_overwrites_in_nested_dict():
    """Verify nested dict in default content settings are correctly replaced."""
    expected_context = {
        'nested_dict': OrderedDict(
            [
                ('full_name', 'Raphael Pierzina'),
                ('github_username', 'hackebrot'),
                (
                    'project',
                    OrderedDict(
                        [
                            ('name', 'My Kivy Project'),
                            ('description', 'My Kivy Project'),
                            ('repo_name', '{{cookiecutter.project_name|lower}}'),
                            ('orientation', ["all", "landscape", "portrait"]),
                        ]
                    ),
                ),
            ]
        )
    }

    generated_context = generate.generate_context(
        context_file='tests/test-generate-context/nested_dict.json',
        default_context={
            'not_in_template': 'foobar',
            'project': {
                'description': 'My Kivy Project',
            },
        },
        extra_context={
            'also_not_in_template': 'foobar2',
            'github_username': 'hackebrot',
            'project': {
                'name': 'My Kivy Project',
            },
        },
    )

    assert generated_context == expected_context


def test_apply_overwrite_context_as_in_nested_dict_with_additional_values():
    """Verify nested dict in default content settings are correctly added.

    The `apply_overwrites_to_context` function should add the extra values to the dict.
    """
    expected = OrderedDict({"key1": "value1", "key2": "value2"})
    context = OrderedDict({"key1": "value1"})
    overwrite_context = OrderedDict({"key2": "value2"})
    generate.apply_overwrites_to_context(
        context,
        overwrite_context,
        in_dictionary_variable=True,
    )
    assert context == expected


def test_apply_overwrites_in_nested_dict_additional_values():
    """Verify nested dict in default content settings are correctly added."""
    expected_context = {
        'nested_dict_additional': OrderedDict(
            [
                ('mainkey1', 'mainvalue1'),
                (
                    'mainkey2',
                    OrderedDict(
                        [
                            ('subkey1', 'subvalue1'),
                            (
                                'subkey2',
                                OrderedDict(
                                    [
                                        ('subsubkey1', 'subsubvalue1'),
                                        ('subsubkey2', 'subsubvalue2_default'),
                                        ('subsubkey3', 'subsubvalue3_extra'),
                                    ]
                                ),
                            ),
                            ('subkey4', 'subvalue4_default'),
                            ('subkey5', 'subvalue5_extra'),
                        ]
                    ),
                ),
            ]
        )
    }

    generated_context = generate.generate_context(
        context_file='tests/test-generate-context/nested_dict_additional.json',
        default_context={
            'not_in_template': 'foobar',
            'mainkey2': {
                'subkey2': {
                    'subsubkey2': 'subsubvalue2_default',
                },
                'subkey4': 'subvalue4_default',
            },
        },
        extra_context={
            'also_not_in_template': 'foobar2',
            'mainkey2': {
                'subkey2': {
                    'subsubkey3': 'subsubvalue3_extra',
                },
                'subkey5': 'subvalue5_extra',
            },
### tests/test_generate_file.py
"""Tests for `generate_file` function, part of `generate_files` function workflow."""

import json
import os
import re
from pathlib import Path

import pytest
from jinja2 import FileSystemLoader
from jinja2.exceptions import TemplateSyntaxError

from cookiecutter import generate
from cookiecutter.environment import StrictEnvironment


@pytest.fixture(scope='function', autouse=True)
def tear_down():
    """
    Fixture. Remove the test text file which is created by the tests.

    Used for all tests in this file.
    """
    yield
    if os.path.exists('tests/files/cheese.txt'):
        os.remove('tests/files/cheese.txt')
    if os.path.exists('tests/files/cheese_lf_newlines.txt'):
        os.remove('tests/files/cheese_lf_newlines.txt')
    if os.path.exists('tests/files/cheese_crlf_newlines.txt'):
        os.remove('tests/files/cheese_crlf_newlines.txt')
    if os.path.exists('tests/files/cheese_mixed_newlines.txt'):
        os.remove('tests/files/cheese_mixed_newlines.txt')
    if os.path.exists('tests/files/{{cookiecutter.generate_file}}_mixed_newlines.txt'):
        os.remove('tests/files/{{cookiecutter.generate_file}}_mixed_newlines.txt')


@pytest.fixture
def env():
    """Fixture. Set Jinja2 environment settings for other tests."""
    environment = StrictEnvironment()
    environment.loader = FileSystemLoader('.')
    return environment


def test_generate_file(env):
    """Verify simple file is generated with rendered context data."""
    infile = 'tests/files/{{cookiecutter.generate_file}}.txt'
    generate.generate_file(
        project_dir=".",
        infile=infile,
        context={'cookiecutter': {'generate_file': 'cheese'}},
        env=env,
    )
    assert os.path.isfile('tests/files/cheese.txt')
    generated_text = Path('tests/files/cheese.txt').read_text()
    assert generated_text == 'Testing cheese'


def test_generate_file_jsonify_filter(env):
    """Verify jsonify filter works during files generation process."""
    infile = 'tests/files/{{cookiecutter.jsonify_file}}.txt'
    data = {'jsonify_file': 'cheese', 'type': 'roquefort'}
    generate.generate_file(
        project_dir=".", infile=infile, context={'cookiecutter': data}, env=env
    )
    assert os.path.isfile('tests/files/cheese.txt')
    generated_text = Path('t<response clipped><NOTE>Due to the max output limit, only part of the full response has been shown to you.</NOTE>_dir = os.path.join(repo_path, 'hooks')
    template = os.path.join(repo_path, 'input{{cookiecutter.hooks}}')
    os.mkdir(repo_path)
    os.mkdir(hook_dir)
    os.mkdir(template)

    hook_path = os.path.join(hooks_path, 'pre_gen_project.py')

    with Path(hook_path).open('w') as f:
        f.write("#!/usr/bin/env python\n")
        f.write("import sys; sys.exit(1)\n")

    with pytest.raises(FailedHookException) as excinfo:
        generate.generate_files(
            context={'cookiecutter': {'hooks': 'hooks'}},
            repo_dir='tests/test-hooks/',
            overwrite_if_exists=True,
        )

    assert 'Hook script failed' in str(excinfo.value)
    assert not os.path.exists('inputhooks')


@pytest.mark.usefixtures('clean_system', 'remove_additional_folders')
def test_run_failing_hook_preserves_existing_output_directory():
    """Verify project directory not removed if exist before hook failed."""
    repo_path = os.path.abspath('tests/test-hooks/')
    hooks_path = os.path.abspath('tests/test-hooks/hooks')

    hook_dir = os.path.join(repo_path, 'hooks')
    template = os.path.join(repo_path, 'input{{cookiecutter.hooks}}')
    os.mkdir(repo_path)
    os.mkdir(hook_dir)
    os.mkdir(template)

    hook_path = os.path.join(hooks_path, 'pre_gen_project.py')

    with Path(hook_path).open('w') as f:
        f.write("#!/usr/bin/env python\n")
        f.write("import sys; sys.exit(1)\n")

    os.mkdir('inputhooks')
    with pytest.raises(FailedHookException) as excinfo:
        generate.generate_files(
            context={'cookiecutter': {'hooks': 'hooks'}},
            repo_dir='tests/test-hooks/',
            overwrite_if_exists=True,
        )

    assert 'Hook script failed' in str(excinfo.value)
    assert os.path.exists('inputhooks')


@pytest.mark.skipif(sys.platform.startswith('win'), reason="Linux only test")
@pytest.mark.usefixtures('clean_system', 'remove_additional_folders')
def test_run_shell_hooks(tmp_path):
    """Verify pre and post generate project shell hooks executed.

    This test for .sh files.
    """
    generate.generate_files(
        context={'cookiecutter': {'shellhooks': 'shellhooks'}},
        repo_dir='tests/test-shellhooks/',
        output_dir=tmp_path.joinpath('test-shellhooks'),
    )
    shell_pre_file = tmp_path.joinpath(
        'test-shellhooks', 'inputshellhooks', 'shell_pre.txt'
    )
    shell_post_file = tmp_path.joinpath(
        'test-shellhooks', 'inputshellhooks', 'shell_post.txt'
    )
    assert shell_pre_file.exists()
    assert shell_post_file.exists()


@pytest.mark.skipif(not sys.platform.startswith('win'), reason="Win only test")
@pytest.mark.usefixtures('clean_system', 'remove_additional_folders')
def test_run_shell_hooks_win(tmp_path):
    """Verify pre and post generate project shell hooks executed.

    This test for .bat files.
    """
    generate.generate_files(
        context={'cookiecutter': {'shellhooks': 'shellhooks'}},
        repo_dir='tests/test-shellhooks-win/',
        output_dir=tmp_path.joinpath('test-shellhooks-win'),
    )
    shell_pre_file = tmp_path.joinpath(
        'test-shellhooks-win', 'inputshellhooks', 'shell_pre.txt'
    )
    shell_post_file = tmp_path.joinpath(
        'test-shellhooks-win', 'inputshellhooks', 'shell_post.txt'
    )
    assert shell_pre_file.exists()
    assert shell_post_file.exists()


@pytest.mark.usefixtures("clean_system", "remove_additional_folders")
def test_ignore_shell_hooks(tmp_path):
    """Verify *.txt files not created, when accept_hooks=False."""
    generate.generate_files(
        context={"cookiecutter": {"shellhooks": "shellhooks"}},
        repo_dir="tests/test-shellhooks/",
        output_dir=tmp_path.joinpath('test-shellhooks'),
        accept_hooks=False,
    )
    shell_pre_file = tmp_path.joinpath("test-shellhooks/inputshellhooks/shell_pre.txt")
    shell_post_file = tmp_path.joinpath(
        "test-shellhooks/inputshellhooks/shell_post.txt"
    )
    assert not shell_pre_file.exists()
    assert not shell_post_file.exists()


@pytest.mark.usefixtures("clean_system", "remove_additional_folders")
def test_deprecate_run_hook_from_repo_dir(tmp_path):
    """Test deprecation warning in generate._run_hook_from_repo_dir."""
    repo_dir = "tests/test-shellhooks/"
    project_dir = Path(tmp_path.joinpath('test-shellhooks'))
    project_dir.mkdir()
    with pytest.deprecated_call():
        generate._run_hook_from_repo_dir(
            repo_dir=repo_dir,
            hook_name="pre_gen_project",
            project_dir=project_dir,
            context={},
            delete_project_on_failure=False,
        )
### tests/vcs/test_identify_repo.py
"""Collection of tests around repository type identification."""

import pytest

from cookiecutter import exceptions, vcs


@pytest.mark.parametrize(
    'repo_url, exp_repo_type, exp_repo_url',
    [
        (
            'git+https://github.com/pytest-dev/cookiecutter-pytest-plugin.git',
            'git',
            'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git',
        ),
        (
            'hg+https://bitbucket.org/foo/bar.hg',
            'hg',
            'https://bitbucket.org/foo/bar.hg',
        ),
        (
            'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git',
            'git',
            'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git',
        ),
        ('https://bitbucket.org/foo/bar.hg', 'hg', 'https://bitbucket.org/foo/bar.hg'),
        (
            'https://github.com/audreyfeldroy/cookiecutter-pypackage.git',
            'git',
            'https://github.com/audreyfeldroy/cookiecutter-pypackage.git',
        ),
        (
            'https://github.com/audreyfeldroy/cookiecutter-pypackage',
            'git',
            'https://github.com/audreyfeldroy/cookiecutter-pypackage',
        ),
        (
            'git@gitorious.org:cookiecutter-gitorious/cookiecutter-gitorious.git',
            'git',
            'git@gitorious.org:cookiecutter-gitorious/cookiecutter-gitorious.git',
        ),
        (
            'https://audreyr@bitbucket.org/audreyr/cookiecutter-bitbucket',
            'hg',
            'https://audreyr@bitbucket.org/audreyr/cookiecutter-bitbucket',
        ),
    ],
)
def test_identify_known_repo(repo_url, exp_repo_type, exp_repo_url):
    """Verify different correct repositories url syntax is correctly transformed."""
    assert vcs.identify_repo(repo_url) == (exp_repo_type, exp_repo_url)


@pytest.fixture(
    params=[
        'foo+git',  # uses explicit identifier with 'git' in the wrong place
        'foo+hg',  # uses explicit identifier with 'hg' in the wrong place
        'foo+bar',  # uses explicit identifier with neither 'git' nor 'hg'
        'foobar',  # no identifier but neither 'git' nor 'bitbucket' in url
        'http://norepotypespecified.com',
    ]
)
def unknown_repo_type_url(request):
    """Fixture. Return wrong formatted repository url."""
    return request.param


def test_identify_raise_on_unknown_repo(unknown_repo_type_url):
    """Verify different incorrect repositories url syntax trigger error raising."""
    with pytest.raises(exceptions.UnknownRepoType):
        vcs.identify_repo(unknown_repo_type_url)
### tests/vcs/test_is_vcs_installed.py
"""Collection of tests around VCS detection."""

import pytest

from cookiecutter import vcs


@pytest.mark.parametrize(
    'which_return, result',
    [('', False), (None, False), (False, False), ('/usr/local/bin/git', True)],
)
def test_is_vcs_installed(mocker, which_return, result):
    """Verify `is_vcs_installed` function correctly handles `which` answer."""
    mocker.patch('cookiecutter.vcs.which', autospec=True, return_value=which_return)
    assert vcs.is_vcs_installed('git') == result
### tests/vcs/test_clone.py
"""Tests around cloning repositories and detection of errors at it."""

import os
import subprocess

import pytest

from cookiecutter import exceptions, vcs


def test_clone_should_raise_if_vcs_not_installed(mocker, clone_dir):
    """In `clone()`, a `VCSNotInstalled` exception should be raised if no VCS \
    is installed."""
    mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=False)

    repo_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git'

    with pytest.raises(exceptions.VCSNotInstalled):
        vcs.clone(repo_url, clone_to_dir=str(clone_dir))


def test_clone_should_rstrip_trailing_slash_in_repo_url(mocker, clone_dir):
    """In `clone()`, repo URL's trailing slash should be stripped if one is \
    present."""
    mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True)

    mock_subprocess = mocker.patch(
        'cookiecutter.vcs.subprocess.check_output',
        autospec=True,
    )

    vcs.clone('https://github.com/foo/bar/', clone_to_dir=clone_dir, no_input=True)

    mock_subprocess.assert_called_once_with(
        ['git', 'clone', 'https://github.com/foo/bar'],
        cwd=clone_dir,
        stderr=subprocess.STDOUT,
    )


def test_clone_should_abort_if_user_does_not_want_to_reclone(mocker, clone_dir):
    """In `clone()`, if user doesn't want to reclone, Cookiecutter should exit \
    without cloning anything."""
    mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True)
    mocker.patch(
        'cookiecutter.vcs.prompt_and_delete', side_effect=SystemExit, autospec=True
    )
    mock_subprocess = mocker.patch(
        'cookiecutter.vcs.subprocess.check_output',
        autospec=True,
    )

    # Create repo_dir to trigger prompt_and_delete
    repo_dir = clone_dir.joinpath('cookiecutter-pytest-plugin')
    repo_dir.mkdir()

    repo_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git'

    with pytest.raises(SystemExit):
        vcs.clone(repo_url, clone_to_dir=str(clone_dir))
    assert not mock_subprocess.called


def test_clone_should_silent_exit_if_ok_to_reuse(mocker, tmpdir):
    """In `clone()`, if user doesn't want to reclone, Cookiecutter should exit \
    without cloning anything."""
    mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True)
    mocker.patch(
        'cookiecutter.vcs.prompt_and_delete', return_value=False, autospec=True
    )
    mock_subprocess = mocker.patch(
        'cookiecutter.vcs.subprocess.check_output',
        autospec=True,
    )

    clone_to_dir = tmpdir.mkdir('clone')

    # Create repo_dir to trigger prompt_and_delete
    clone_to_dir.mkdir('cookiecutter-pytest-plugin')

    repo_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin.git'

    vcs.clone(repo_url, clone_to_dir=str(clone_to_dir))
    assert not mock_subprocess.called


@pytest.mark.parametrize(
    'repo_type, repo_url, repo_name',
    [
        ('git', 'https://github.com/hello/world.git', 'world'),
        ('hg', 'https://bitbucket.org/foo/bar', 'bar'),
        ('git', 'git@host:gitoliterepo', 'gitoliterepo'),
        ('git', 'git@gitlab.com:cookiecutter/cookiecutter.git', 'cookiecutter'),
        ('git', 'git@github.com:cookiecutter/cookiecutter.git', 'cookiecutter'),
    ],
)
def test_clone_should_invoke_vcs_command(
    mocker, clone_dir, repo_type, repo_url, repo_name
):
    """When `clone()` is called with a git/hg repo, the corresponding VCS \
    command should be run via `subprocess.check_output()`.

    This should take place:
    * In the correct dir
    * With the correct args.
    """
    mocker.patch('cookiecutter.vcs.is_vcs_installed', autospec=True, return_value=True)

    mock_subprocess = mocker.patch(
        'cookiecutter.vcs.subprocess.check_output',
        autospec=True,
    )
    expected_repo_dir = os.path.normpath(os.path.join(clone_dir, repo_name))

    branch = 'foobar'

    repo_dir = vcs.clone(
        repo_url, checkout=branch, clone_to_dir=clone_dir, no_input=True
    )

    assert repo_dir == expected_repo_dir

    mock_subprocess.assert_any_call(
        [repo_type, 'clone', repo_url], cwd=clone_dir, stderr=subprocess.STDOUT
    )

    branch_info = [branch]
    # We sanitize branch information for Mercurial
    if repo_type == "hg":
        branch_info.insert(0, "--")

    mock_subprocess.assert_any_call(
        [repo_type, 'checkout', *branch_info],
        cwd=expected_repo_dir,
        stderr=subprocess.STDOUT,
    )


@pytest.mark.parametrize(
    'error_message',
    [
        (b"fatal: repository 'https://github.com/hackebro/cookiedozer' not found"),
        b'hg: abort: HTTP Error 404: Not Found',
    ],
)
def test_clone_handles_repo_typo(mocker, clone_dir, error_message):
    """In `clone()`, repository not found errors should raise an \
    appropriate exception."""
    # side_effect is set to an iterable here (and below),
    # because of a Python 3.4 unittest.mock regression
    # http://bugs.python.org/issue23661
    mocker.patch(
        'cookiecutter.vcs.subprocess.check_output',
        autospec=True,
        side_effect=[subprocess.CalledProcessError(-1, 'cmd', output=error_message)],
    )

    repository_url = 'https://github.com/hackebro/cookiedozer'
    with pytest.raises(exceptions.RepositoryNotFound) as err:
        vcs.clone(repository_url, clone_to_dir=str(clone_dir), no_input=True)

    assert str(err.value) == (
        f'The repository {repository_url} could not be found, have you made a typo?'
    )


@pytest.mark.parametrize(
    'error_message',
    [
        b"error: pathspec 'unknown_branch' did not match any file(s) known to git",
        b"hg: abort: unknown revision 'unknown_branch'!",
    ],
)
def test_clone_handles_branch_typo(mocker, clone_dir, error_message):
    """In `clone()`, branch not found errors should raise an \
    appropriate exception."""
    mocker.patch(
        'cookiecutter.vcs.subprocess.check_output',
        autospec=True,
        side_effect=[subprocess.CalledProcessError(-1, 'cmd', output=error_message)],
    )

    repository_url = 'https://github.com/pytest-dev/cookiecutter-pytest-plugin'
    with pytest.raises(exceptions.RepositoryCloneFailed) as err:
        vcs.clone(
            repository_url,
            clone_to_dir=str(clone_dir),
            checkout='unknown_branch',
            no_input=True,
        )

    assert str(err.value) == (
        'The unknown_branch branch of repository '
        f'{repository_url} could not found, have you made a typo?'
    )


def test_clone_unknown_subprocess_error(mocker, clone_dir):
    """In `clone()`, unknown subprocess errors should be raised."""
    mocker.patch(
        'cookiecutter.vcs.subprocess.check_output',
        autospec=True,
        side_effect=[
            subprocess.CalledProcessError(-1, 'cmd', output=b'Something went wrong')
        ],
    )

    with pytest.raises(subprocess.CalledProcessError):
        vcs.clone(
            'https://github.com/pytest-dev/cookiecutter-pytest-plugin',
            clone_to_dir=str(clone_dir),
            no_input=True,
        )
[The command completed with exit code 0.]
[Current working directory: /workspace/cookiecutter]
[Python interpreter: /usr/bin/python]
[Command finished with exit code 0]