"""Tests for function unzip() from zipfile module."""

import shutil
import tempfile
from pathlib import Path

import pytest

from cookiecutter import zipfile
from cookiecutter.exceptions import InvalidZipRepository


def mock_download():
    """Fake download function."""
    with Path('tests/files/fake-repo-tmpl.zip').open('rb') as zf:
        chunk = zf.read(1024)
        while chunk:
            yield chunk
            chunk = zf.read(1024)


def mock_download_with_empty_chunks():
    """Fake download function."""
    yield
    with Path('tests/files/fake-repo-tmpl.zip').open('rb') as zf:
        chunk = zf.read(1024)
        while chunk:
            yield chunk
            chunk = zf.read(1024)


def test_unzip_local_file(mocker, clone_dir):
    """Local file reference can be unzipped."""
    mock_prompt_and_delete = mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True
    )

    output_dir = zipfile.unzip(
        'tests/files/fake-repo-tmpl.zip', is_url=False, clone_to_dir=str(clone_dir)
    )

    assert output_dir.startswith(tempfile.gettempdir())
    assert not mock_prompt_and_delete.called


def test_unzip_protected_local_file_environment_password(mocker, clone_dir):
    """In `unzip()`, the environment can be used to provide a repo password."""
    mock_prompt_and_delete = mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True
    )

    output_dir = zipfile.unzip(
        'tests/files/protected-fake-repo-tmpl.zip',
        is_url=False,
        clone_to_dir=str(clone_dir),
        password='sekrit',
    )

    assert output_dir.startswith(tempfile.gettempdir())
    assert not mock_prompt_and_delete.called


def test_unzip_protected_local_file_bad_environment_password(mocker, clone_dir):
    """In `unzip()`, an error occurs if the environment has a bad password."""
    mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True
    )

    with pytest.raises(InvalidZipRepository):
        zipfile.unzip(
            'tests/files/protected-fake-repo-tmpl.zip',
            is_url=False,
            clone_to_dir=str(clone_dir),
            password='not-the-right-password',
        )


def test_unzip_protected_local_file_user_password_with_noinput(mocker, clone_dir):
    """Can't unpack a password-protected repo in no_input mode."""
    mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True
    )

    with pytest.raises(InvalidZipRepository):
        zipfile.unzip(
            'tests/files/protected-fake-repo-tmpl.zip',
            is_url=False,
            clone_to_dir=str(clone_dir),
            no_input=True,
        )


def test_unzip_protected_local_file_user_password(mocker, clone_dir):
    """A password-protected local file reference can be unzipped."""
    mock_prompt_and_delete = mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True
    )
    mocker.patch('cookiecutter.zipfile.read_repo_password', return_value='sekrit')

    output_dir = zipfile.unzip(
        'tests/files/protected-fake-repo-tmpl.zip',
        is_url=False,
        clone_to_dir=str(clone_dir),
    )

    assert output_dir.startswith(tempfile.gettempdir())
    assert not mock_prompt_and_delete.called


def test_unzip_protected_local_file_user_bad_password(mocker, clone_dir):
    """Error in `unzip()`, if user can't provide a valid password."""
    mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True
    )
    mocker.patch(
        'cookiecutter.zipfile.read_repo_password', return_value='not-the-right-password'
    )

    with pytest.raises(InvalidZipRepository):
        zipfile.unzip(
            'tests/files/protected-fake-repo-tmpl.zip',
            is_url=False,
            clone_to_dir=str(clone_dir),
        )


def test_empty_zip_file(mocker, clone_dir):
    """In `unzip()`, an empty file raises an error."""
    mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True
    )

    with pytest.raises(InvalidZipRepository):
        zipfile.unzip(
            'tests/files/empty.zip', is_url=False, clone_to_dir=str(clone_dir)
        )


def test_non_repo_zip_file(mocker, clone_dir):
    """In `unzip()`, a repository must have a top level directory."""
    mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True
    )

    with pytest.raises(InvalidZipRepository):
        zipfile.unzip(
            'tests/files/not-a-repo.zip', is_url=False, clone_to_dir=str(clone_dir)
        )


def test_bad_zip_file(mocker, clone_dir):
    """In `unzip()`, a corrupted zip file raises an error."""
    mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True
    )

    with pytest.raises(InvalidZipRepository):
        zipfile.unzip(
            'tests/files/bad-zip-file.zip', is_url=False, clone_to_dir=str(clone_dir)
        )


def test_unzip_url(mocker, clone_dir):
    """In `unzip()`, a url will be downloaded and unzipped."""
    mock_prompt_and_delete = mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True
    )

    request = mocker.MagicMock()
    request.iter_content.return_value = mock_download()

    mocker.patch(
        'cookiecutter.zipfile.requests.get',
        return_value=request,
        autospec=True,
    )

    output_dir = zipfile.unzip(
        'https://example.com/path/to/fake-repo-tmpl.zip',
        is_url=True,
        clone_to_dir=str(clone_dir),
    )

    assert output_dir.startswith(tempfile.gettempdir())
    assert not mock_prompt_and_delete.called


def test_unzip_url_with_empty_chunks(mocker, clone_dir):
    """In `unzip()` empty chunk must be ignored."""
    mock_prompt_and_delete = mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True
    )

    request = mocker.MagicMock()
    request.iter_content.return_value = mock_download_with_empty_chunks()

    mocker.patch(
        'cookiecutter.zipfile.requests.get',
        return_value=request,
        autospec=True,
    )

    output_dir = zipfile.unzip(
        'https://example.com/path/to/fake-repo-tmpl.zip',
        is_url=True,
        clone_to_dir=str(clone_dir),
    )

    assert output_dir.startswith(tempfile.gettempdir())
    assert not mock_prompt_and_delete.called


def test_unzip_url_existing_cache(mocker, clone_dir):
    """Url should be downloaded and unzipped, old zip file will be removed."""
    mock_prompt_and_delete = mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=True, autospec=True
    )

    request = mocker.MagicMock()
    request.iter_content.return_value = mock_download()

    mocker.patch(
        'cookiecutter.zipfile.requests.get',
        return_value=request,
        autospec=True,
    )

    # Create an existing cache of the zipfile
    existing_zip = clone_dir.joinpath('fake-repo-tmpl.zip')
    existing_zip.write_text('This is an existing zipfile')

    output_dir = zipfile.unzip(
        'https://example.com/path/to/fake-repo-tmpl.zip',
        is_url=True,
        clone_to_dir=str(clone_dir),
    )

    assert output_dir.startswith(tempfile.gettempdir())
    assert mock_prompt_and_delete.call_count == 1


def test_unzip_url_existing_cache_no_input(mocker, clone_dir):
    """If no_input is provided, the existing file should be removed."""
    request = mocker.MagicMock()
    request.iter_content.return_value = mock_download()

    mocker.patch(
        'cookiecutter.zipfile.requests.get',
        return_value=request,
        autospec=True,
    )

    # Create an existing cache of the zipfile
    existing_zip = clone_dir.joinpath('fake-repo-tmpl.zip')
    existing_zip.write_text('This is an existing zipfile')

    output_dir = zipfile.unzip(
        'https://example.com/path/to/fake-repo-tmpl.zip',
        is_url=True,
        clone_to_dir=str(clone_dir),
        no_input=True,
    )

    assert output_dir.startswith(tempfile.gettempdir())


def test_unzip_should_abort_if_no_redownload(mocker, clone_dir):
    """Should exit without cloning anything If no redownload."""
    mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', side_effect=SystemExit, autospec=True
    )

    mock_requests_get = mocker.patch(
        'cookiecutter.zipfile.requests.get',
        autospec=True,
    )

    # Create an existing cache of the zipfile
    existing_zip = clone_dir.joinpath('fake-repo-tmpl.zip')
    existing_zip.write_text('This is an existing zipfile')

    zipfile_url = 'https://example.com/path/to/fake-repo-tmpl.zip'

    with pytest.raises(SystemExit):
        zipfile.unzip(zipfile_url, is_url=True, clone_to_dir=str(clone_dir))

    assert not mock_requests_get.called


def test_unzip_is_ok_to_reuse(mocker, clone_dir):
    """Already downloaded zip should not be downloaded again."""
    mock_prompt_and_delete = mocker.patch(
        'cookiecutter.zipfile.prompt_and_delete', return_value=False, autospec=True
    )

    request = mocker.MagicMock()

    existing_zip = clone_dir.joinpath('fake-repo-tmpl.zip')
    shutil.copy('tests/files/fake-repo-tmpl.zip', existing_zip)

    output_dir = zipfile.unzip(
        'https://example.com/path/to/fake-repo-tmpl.zip',
        is_url=True,
        clone_to_dir=str(clone_dir),
    )

    assert output_dir.startswith(tempfile.gettempdir())
    assert mock_prompt_and_delete.call_count == 1
    assert request.iter_content.call_count == 0

### cli 240-760
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli_overwrite_if_exists_when_output_dir_does_not_exist(
    cli_runner, overwrite_cli_flag
):
    """Test cli invocation with `overwrite-if-exists` and `no-input` flags.

    Case when output dir not exist.
    """
    result = cli_runner('tests/fake-repo-pre/', '--no-input', overwrite_cli_flag)

    assert result.exit_code == 0
    assert os.path.isdir('fake-project')


@pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir')
def test_cli_overwrite_if_exists_when_output_dir_exists(cli_runner, overwrite_cli_flag):
    """Test cli invocation with `overwrite-if-exists` and `no-input` flags.

    Case when output dir already exist.
    """
    result = cli_runner('tests/fake-repo-pre/', '--no-input', overwrite_cli_flag)
    assert result.exit_code == 0
    assert os.path.isdir('fake-project')


@pytest.fixture(params=['-o', '--output-dir'])
def output_dir_flag(request):
    """Pytest fixture return all output-dir invocation options."""
    return request.param


def test_cli_output_dir(mocker, cli_runner, output_dir_flag, output_dir):
    """Test cli invocation with `output-dir` flag changes output directory."""
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')

    template_path = 'tests/fake-repo-pre/'
    result = cli_runner(template_path, output_dir_flag, output_dir)

    assert result.exit_code == 0
    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay=False,
        overwrite_if_exists=False,
        skip_if_file_exists=False,
        output_dir=output_dir,
        config_file=None,
        default_config=False,
        extra_context=None,
        password=None,
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


@pytest.fixture(params=['-h', '--help', 'help'])
def help_cli_flag(request):
    """Pytest fixture return all help invocation options."""
    return request.param


def test_cli_help(cli_runner, help_cli_flag):
    """Test cli invocation display help message with `help` flag."""
    result = cli_runner(help_cli_flag)
    assert result.exit_code == 0
    assert result.output.startswith('Usage')


@pytest.fixture
def user_config_path(tmp_path):
    """Pytest fixture return `user_config` argument as string."""
    return str(tmp_path.joinpath("tests", "config.yaml"))


def test_user_config(mocker, cli_runner, user_config_path):
    """Test cli invocation works with `config-file` option."""
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')

    template_path = 'tests/fake-repo-pre/'
    result = cli_runner(template_path, '--config-file', user_config_path)

    assert result.exit_code == 0
    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay=False,
        overwrite_if_exists=False,
        skip_if_file_exists=False,
        output_dir='.',
        config_file=user_config_path,
        default_config=False,
        extra_context=None,
        password=None,
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


def test_default_user_config_overwrite(mocker, cli_runner, user_config_path):
    """Test cli invocation ignores `config-file` if `default-config` passed."""
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')

    template_path = 'tests/fake-repo-pre/'
    result = cli_runner(
        template_path,
        '--config-file',
        user_config_path,
        '--default-config',
    )

    assert result.exit_code == 0
    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay=False,
        overwrite_if_exists=False,
        skip_if_file_exists=False,
        output_dir='.',
        config_file=user_config_path,
        default_config=True,
        extra_context=None,
        password=None,
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


def test_default_user_config(mocker, cli_runner):
    """Test cli invocation accepts `default-config` flag correctly."""
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')

    template_path = 'tests/fake-repo-pre/'
    result = cli_runner(template_path, '--default-config')

    assert result.exit_code == 0
    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay=False,
        overwrite_if_exists=False,
        skip_if_file_exists=False,
        output_dir='.',
        config_file=None,
        default_config=True,
        extra_context=None,
        password=None,
        directory=None,
        accept_hooks=True,
        keep_project_on_failure=False,
    )


def test_echo_undefined_variable_error(output_dir, cli_runner):
    """Cli invocation return error if variable undefined in template."""
    template_path = 'tests/undefined-variable/file-name/'

    result = cli_runner(
        '--no-input',
        '--default-config',
        '--output-dir',
        output_dir,
        template_path,
    )

    assert result.exit_code == 1

    error = "Unable <response clipped><NOTE>Due to the max output limit, only part of the full response has been shown to you.</NOTE>ath.dirname(user_config_path))
    # Single quotes in YAML will not parse escape codes (\).
    Path(user_config_path).write_text(f"cookiecutters_dir: '{fake_template_dir}'")
    Path("fake-project", "cookiecutter.json").write_text('{}')

    result = cli_runner(
        '--list-installed',
        '--config-file',
        user_config_path,
        str(debug_file),
    )

    assert "1 installed templates:" in result.output
    assert result.exit_code == 0


def test_debug_list_installed_templates_failure(
    cli_runner, debug_file, user_config_path
):
    """Verify --list-installed command error on invocation."""
    os.makedirs(os.path.dirname(user_config_path))
    Path(user_config_path).write_text('cookiecutters_dir: "/notarealplace/"')

    result = cli_runner(
        '--list-installed', '--config-file', user_config_path, str(debug_file)
    )

    assert "Error: Cannot list installed templates." in result.output
    assert result.exit_code == -1


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_directory_repo(cli_runner):
    """Test cli invocation works with `directory` option."""
    result = cli_runner(
        'tests/fake-repo-dir/',
        '--no-input',
        '-v',
        '--directory=my-dir',
    )
    assert result.exit_code == 0
    assert os.path.isdir("fake-project")
    content = Path("fake-project", "README.rst").read_text()
    assert "Project name: **Fake Project**" in content


cli_accept_hook_arg_testdata = [
    ("--accept-hooks=yes", None, True),
    ("--accept-hooks=no", None, False),
    ("--accept-hooks=ask", "yes", True),
    ("--accept-hooks=ask", "no", False),
]


@pytest.mark.parametrize(
    "accept_hooks_arg,user_input,expected", cli_accept_hook_arg_testdata
)
def test_cli_accept_hooks(
    mocker,
    cli_runner,
    output_dir_flag,
    output_dir,
    accept_hooks_arg,
    user_input,
    expected,
):
    """Test cli invocation works with `accept-hooks` option."""
    mock_cookiecutter = mocker.patch("cookiecutter.cli.cookiecutter")

    template_path = "tests/fake-repo-pre/"
    result = cli_runner(
        template_path, output_dir_flag, output_dir, accept_hooks_arg, input=user_input
    )

    assert result.exit_code == 0
    mock_cookiecutter.assert_called_once_with(
        template_path,
        None,
        False,
        replay=False,
        overwrite_if_exists=False,
        output_dir=output_dir,
        config_file=None,
        default_config=False,
        extra_context=None,
        password=None,
        directory=None,
        skip_if_file_exists=False,
        accept_hooks=expected,
        keep_project_on_failure=False,
    )


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli_with_json_decoding_error(cli_runner):
    """Test cli invocation with a malformed JSON file."""
    template_path = 'tests/fake-repo-bad-json/'
    result = cli_runner(template_path, '--no-input')
    assert result.exit_code != 0

    # Validate the error message.
    # 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, result.output)
    # 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', 'fake-repo-bad-json', 'cookiecutter.json'])
    assert path in result.output


@pytest.mark.usefixtures('remove_fake_project_dir')
def test_cli_with_pre_prompt_hook(cli_runner):
    """Test cli invocation in a template with pre_prompt hook."""
    template_path = 'tests/test-pyhooks/'
    result = cli_runner(template_path, '--no-input')
    assert result.exit_code == 0
    dir_name = 'inputfake-project'
    assert os.path.isdir(dir_name)
    content = Path(dir_name, "README.rst").read_text()
    assert 'foo' in content


def test_cli_with_pre_prompt_hook_fail(cli_runner, monkeypatch):
    """Test cli invocation will fail when a given env var is present."""
    template_path = 'tests/test-pyhooks/'
    with monkeypatch.context() as m:
        m.setenv('COOKIECUTTER_FAIL_PRE_PROMPT', '1')
        result = cli_runner(template_path, '--no-input')
    assert result.exit_code == 1
    dir_name = 'inputfake-project'
    assert not Path(dir_name).exists()

### hooks full
"""Tests for `cookiecutter.hooks` module."""

import errno
import os
import stat
import sys
import textwrap
from pathlib import Path

import pytest

from cookiecutter import exceptions, hooks, utils


def make_test_repo(name, multiple_hooks=False):
    """Create test repository for test setup methods."""
    hook_dir = os.path.join(name, 'hooks')
    template = os.path.join(name, 'input{{hooks}}')
    os.mkdir(name)
    os.mkdir(hook_dir)
    os.mkdir(template)

    Path(template, 'README.rst').write_text("foo\n===\n\nbar\n")

    with Path(hook_dir, 'pre_gen_project.py').open('w') as f:
        f.write("#!/usr/bin/env python\n")
        f.write("# -*- coding: utf-8 -*-\n")
        f.write("from __future__ import print_function\n")
        f.write("\n")
        f.write("print('pre generation hook')\n")
        f.write("f = open('python_pre.txt', 'w')\n")
        f.write("f.close()\n")

    if sys.platform.startswith('win'):
        post = 'post_gen_project.bat'
        with Path(hook_dir, post).open('w') as f:
            f.write("@echo off\n")
            f.write("\n")
            f.write("echo post generation hook\n")
            f.write("echo. >shell_post.txt\n")
    else:
        post = 'post_gen_project.sh'
        filename = os.path.join(hook_dir, post)
        with Path(filename).open('w') as f:
            f.write("#!/bin/bash\n")
            f.write("\n")
            f.write("echo 'post generation hook';\n")
            f.write("touch 'shell_post.txt'\n")
        # Set the execute bit
        os.chmod(filename, os.stat(filename).st_mode | stat.S_IXUSR)

    # Adding an additional pre script
    if multiple_hooks:
        if sys.platform.startswith('win'):
            pre = 'pre_gen_project.bat'
            with Path(hook_dir, pre).open('w') as f:
                f.write("@echo off\n")
                f.write("\n")
                f.write("echo post generation hook\n")
                f.write("echo. >shell_pre.txt\n")
        else:
            pre = 'pre_gen_project.sh'
            filename = os.path.join(hook_dir, pre)
            with Path(filename).open('w') as f:
                f.write("#!/bin/bash\n")
                f.write("\n")
                f.write("echo 'post generation hook';\n")
                f.write("touch 'shell_pre.txt'\n")
            # Set the execute bit
            os.chmod(filename, os.stat(filename).st_mode | stat.S_IXUSR)

    return post


class TestFindHooks:
    """Class to unite find hooks related tests in one place."""

    repo_path = 'tests/test-hooks'

    def setup_method(self, method):
        """Find hooks related tests setup fixture."""
        self.post_hook = make_test_repo(self.repo_path)

    def teardown_method(self, method):
        """Find hooks related tests teardown fixture."""
        utils.rmtree(self.repo_path)

    def test_find_hook(self):
        """Finds the specified hook."""
        with utils.work_in(self.repo_path):
            expected_pre = os.path.abspath('hooks/pre_gen_project.py')
            actual_hook_path = hooks.find_hook('pre_gen_project')
            assert expected_pre == actual_hook_path[0]

            expected_post = os.path.abspath(f'hooks/{self.post_hook}')
            actual_hook_path = hooks.find_hook('post_gen_project')
            assert expected_post == actual_hook_path[0]

    def test_no_hooks(self):
        """`find_hooks` should return None if the hook could not be found."""
        with utils.work_in('tests/fake-repo'):
            assert None is hooks.find_hook('pre_gen_project')

    def test_unknown_hooks_dir(self):
        """`find_hooks` should return None if hook directory not found."""
        with utils.work_in(self.repo_path):
            assert hooks.find_hook('pre_gen_project', hooks_dir='hooks_dir') is None

    def test_hook_not_found(self):
        """`find_hooks` should return None if the hook could not be found."""
        with utils.work_in(self.repo_path):
            assert hooks.find_hook('unknown_hook') is None


class TestExternalHooks:
    """Class to unite tests for hooks with different project paths."""

    repo_path = os.path.abspath('tests/test-hooks/')
    hooks_path = os.path.abspath('tests/test-hooks/hooks')

    def setup_method(self, method):
        """External hooks related tests setup fixture."""
        self.post_hook = make_test_repo(self.repo_path, multiple_hooks=True)

    def teardown_method(self, method):
        """External hooks related tests teardown fixture."""
        utils.rmtree(self.repo_path)

        if os.path.exists('python_pre.txt'):
            os.remove('python_pre.txt')
        if os.path.exists('shell_post.txt'):
            os.remove('shell_post.txt')
        if os.path.exists('shell_pre.txt'):
            os.remove('shell_pre.txt')
        if os.path.exists('tests/shell_post.txt'):
            os.remove('tests/shell_post.txt')
        if os.path.exists('tests/test-hooks/input{{hooks}}/python_pre.txt'):
            os.remove('tests/test-hooks/input{{hooks}}/python_pre.txt')
        if os.path.exists('tests/test-hooks/input{{hooks}}/shell_post.txt'):
            os.remove('tests/test-hooks/input{{hooks}}/shell_post.txt')
        if os.path.exists('tests/context_post.txt'):
            os.remove('tests/context_post.txt')

    def test_run_script(self):
        """Execute a hook script, independently of project generation."""
        hooks.run_script(os.path.join(self.hooks_path, self.post_hook))
        assert os.path.isfile('shell_post.txt')

    def test_run_failing_script(self, mocker):
        """Test correct exception raise if run_script fails."""
        err = OSError()

        prompt = mocker.patch('subprocess.Popen')
        prompt.side_effect = err

        with pytest.raises(exceptions.FailedHookException) as excinfo:
            hooks.run_script(os.path.join(self.hooks_path, self.post_hook))
        assert f'Hook script failed (error: {err})' in str(excinfo.value)

    def test_run_failing_script_enoexec(self, mocker):
        """Test correct exception raise if run_script fails."""
        err = OSError()
        err.errno = errno.ENOEXEC

        prompt = mocker.patch('subprocess.Popen')
        prompt.side_effect = err

        with pytest.raises(exceptions.FailedHookException) as excinfo:
            hooks.run_script(os.path.join(self.hooks_path, self.post_hook))
        assert 'Hook script failed, might be an empty file or missing a shebang' in str(
            excinfo.value
        )

    def test_run_script_cwd(self):
        """Change directory before running hook."""
        hooks.run_script(os.path.join(self.hooks_path, self.post_hook), 'tests')
        assert os.path.isfile('tests/shell_post.txt')
        assert 'tests' not in os.getcwd()

    def test_run_script_with_context(self):
        """Execute a hook script, passing a context."""
        hook_path = os.path.join(self.hooks_path, 'post_gen_project.sh')

        if sys.platform.startswith('win'):
            post = 'post_gen_project.bat'
            with Path(self.hooks_path, post).open('w') as f:
                f.write("@echo off\n")
                f.write("\n")
                f.write("echo post generation hook\n")
                f.write("echo. >{{cookiecutter.file}}\n")
        else:
            with Path(hook_path).open('w') as fh:
                fh.write("#!/bin/bash\n")
                fh.write("\n")
                fh.write("echo 'post generation hook';\n")
                fh.write("touch 'shell_post.txt'\n")
                fh.write("touch '{{cookiecutter.file}}'\n")
                os.chmod(hook_path, os.stat(hook_path).st_mode | stat.S_IXUSR)

        hooks.run_script_with_context(
            os.path.join(self.hooks_path, self.post_hook),
            'tests',
            {'cookiecutter': {'file': 'context_post.txt'}},
        )
        assert os.path.isfile('tests/context_post.txt')
        assert 'tests' not in os.getcwd()

    def test_run_hook(self):
        """Execute hook from specified template in specified output \
        directory."""
        tests_dir = os.path.join(self.repo_path, 'input{{hooks}}')
        with utils.work_in(self.repo_path):
            hooks.run_hook('pre_gen_project', tests_dir, {})
            assert os.path.isfile(os.path.join(tests_dir, 'python_pre.txt'))
            assert os.path.isfile(os.path.join(tests_dir, 'shell_pre.txt'))

            hooks.run_hook('post_gen_project', tests_dir, {})
            assert os.path.isfile(os.path.join(tests_dir, 'shell_post.txt'))

    def test_run_failing_hook(self):
        """Test correct exception raise if hook exit code is not zero."""
        hook_path = os.path.join(self.hooks_path, 'pre_gen_project.py')
        tests_dir = os.path.join(self.repo_path, 'input{{hooks}}')

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

        with utils.work_in(self.repo_path):
            with pytest.raises(exceptions.FailedHookException) as excinfo:
                hooks.run_hook('pre_gen_project', tests_dir, {})
            assert 'Hook script failed' in str(excinfo.value)


@pytest.fixture()
def dir_with_hooks(tmp_path):
    """Yield a directory that contains hook backup files."""
    hooks_dir = tmp_path.joinpath('hooks')
    hooks_dir.mkdir()

    pre_hook_content = textwrap.dedent(
        """
        #!/usr/bin/env python
        # -*- coding: utf-8 -*-
        print('pre_gen_project.py~')
        """
    )
    pre_gen_hook_file = hooks_dir.joinpath('pre_gen_project.py~')
    pre_gen_hook_file.write_text(pre_hook_content, encoding='utf8')

    post_hook_content = textwrap.dedent(
        """
        #!/usr/bin/env python
        # -*- coding: utf-8 -*-
        print('post_gen_project.py~')
        """
    )

    post_gen_hook_file = hooks_dir.joinpath('post_gen_project.py~')
    post_gen_hook_file.write_text(post_hook_content, encoding='utf8')

    # Make sure to yield the parent directory as `find_hooks()`
    # looks into `hooks/` in the current working directory
    yield str(tmp_path)

    pre_gen_hook_file.unlink()
    post_gen_hook_file.unlink()


def test_ignore_hook_backup_files(monkeypatch, dir_with_hooks):
    """Test `find_hook` correctly use `valid_hook` verification function."""
    # Change the current working directory that contains `hooks/`
    monkeypatch.chdir(dir_with_hooks)
    assert hooks.find_hook('pre_gen_project') is None
    assert hooks.find_hook('post_gen_project') is None
[The command completed with exit code 0.]
[Current working directory: /workspace/cookiecutter]
[Python interpreter: /usr/bin/python]
[Command finished with exit code 0]