Completed
Push — master ( 2fd137...43b8da )
by Simone
20s queued 12s
created

TestExternalHooks.test_run_failing_script_enoexec()   A

Complexity

Conditions 2

Size

Total Lines 12
Code Lines 8

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 8
dl 0
loc 12
rs 10
c 0
b 0
f 0
cc 2
nop 2
1
"""Tests for `cookiecutter.hooks` module."""
2
import os
3
import errno
4
import stat
5
import sys
6
import textwrap
7
8
import pytest
9
10
from cookiecutter import hooks, utils, exceptions
11
12
13
def make_test_repo(name, multiple_hooks=False):
14
    """Create test repository for test setup methods."""
15
    hook_dir = os.path.join(name, 'hooks')
16
    template = os.path.join(name, 'input{{hooks}}')
17
    os.mkdir(name)
18
    os.mkdir(hook_dir)
19
    os.mkdir(template)
20
21
    with open(os.path.join(template, 'README.rst'), 'w') as f:
22
        f.write("foo\n===\n\nbar\n")
23
24
    with open(os.path.join(hook_dir, 'pre_gen_project.py'), 'w') as f:
25
        f.write("#!/usr/bin/env python\n")
26
        f.write("# -*- coding: utf-8 -*-\n")
27
        f.write("from __future__ import print_function\n")
28
        f.write("\n")
29
        f.write("print('pre generation hook')\n")
30
        f.write("f = open('python_pre.txt', 'w')\n")
31
        f.write("f.close()\n")
32
33 View Code Duplication
    if sys.platform.startswith('win'):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
34
        post = 'post_gen_project.bat'
35
        with open(os.path.join(hook_dir, post), 'w') as f:
36
            f.write("@echo off\n")
37
            f.write("\n")
38
            f.write("echo post generation hook\n")
39
            f.write("echo. >shell_post.txt\n")
40
    else:
41
        post = 'post_gen_project.sh'
42
        filename = os.path.join(hook_dir, post)
43
        with open(filename, 'w') as f:
44
            f.write("#!/bin/bash\n")
45
            f.write("\n")
46
            f.write("echo 'post generation hook';\n")
47
            f.write("touch 'shell_post.txt'\n")
48
        # Set the execute bit
49
        os.chmod(filename, os.stat(filename).st_mode | stat.S_IXUSR)
50
51
    # Adding an additional pre script
52 View Code Duplication
    if multiple_hooks:
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
53
        if sys.platform.startswith('win'):
54
            pre = 'pre_gen_project.bat'
55
            with open(os.path.join(hook_dir, pre), 'w') as f:
56
                f.write("@echo off\n")
57
                f.write("\n")
58
                f.write("echo post generation hook\n")
59
                f.write("echo. >shell_pre.txt\n")
60
        else:
61
            pre = 'pre_gen_project.sh'
62
            filename = os.path.join(hook_dir, pre)
63
            with open(filename, 'w') as f:
64
                f.write("#!/bin/bash\n")
65
                f.write("\n")
66
                f.write("echo 'post generation hook';\n")
67
                f.write("touch 'shell_pre.txt'\n")
68
            # Set the execute bit
69
            os.chmod(filename, os.stat(filename).st_mode | stat.S_IXUSR)
70
71
    return post
72
73
74
class TestFindHooks(object):
75
    """Class to unite find hooks related tests in one place."""
76
77
    repo_path = 'tests/test-hooks'
78
79
    def setup_method(self, method):
80
        """Find hooks related tests setup fixture."""
81
        self.post_hook = make_test_repo(self.repo_path)
82
83
    def teardown_method(self, method):
84
        """Find hooks related tests teardown fixture."""
85
        utils.rmtree(self.repo_path)
86
87
    def test_find_hook(self):
88
        """Finds the specified hook."""
89
        with utils.work_in(self.repo_path):
90
            expected_pre = os.path.abspath('hooks/pre_gen_project.py')
91
            actual_hook_path = hooks.find_hook('pre_gen_project')
92
            assert expected_pre == actual_hook_path[0]
93
94
            expected_post = os.path.abspath('hooks/{}'.format(self.post_hook))
95
            actual_hook_path = hooks.find_hook('post_gen_project')
96
            assert expected_post == actual_hook_path[0]
97
98
    def test_no_hooks(self):
99
        """`find_hooks` should return None if the hook could not be found."""
100
        with utils.work_in('tests/fake-repo'):
101
            assert None is hooks.find_hook('pre_gen_project')
102
103
    def test_unknown_hooks_dir(self):
104
        """`find_hooks` should return None if hook directory not found."""
105
        with utils.work_in(self.repo_path):
106
            assert hooks.find_hook('pre_gen_project', hooks_dir='hooks_dir') is None
107
108
    def test_hook_not_found(self):
109
        """`find_hooks` should return None if the hook could not be found."""
110
        with utils.work_in(self.repo_path):
111
            assert hooks.find_hook('unknown_hook') is None
112
113
114
class TestExternalHooks(object):
115
    """Class to unite tests for hooks with different project paths."""
116
117
    repo_path = os.path.abspath('tests/test-hooks/')
118
    hooks_path = os.path.abspath('tests/test-hooks/hooks')
119
120
    def setup_method(self, method):
121
        """External hooks related tests setup fixture."""
122
        self.post_hook = make_test_repo(self.repo_path, multiple_hooks=True)
123
124
    def teardown_method(self, method):
125
        """External hooks related tests teardown fixture."""
126
        utils.rmtree(self.repo_path)
127
128
        if os.path.exists('python_pre.txt'):
129
            os.remove('python_pre.txt')
130
        if os.path.exists('shell_post.txt'):
131
            os.remove('shell_post.txt')
132
        if os.path.exists('shell_pre.txt'):
133
            os.remove('shell_pre.txt')
134
        if os.path.exists('tests/shell_post.txt'):
135
            os.remove('tests/shell_post.txt')
136
        if os.path.exists('tests/test-hooks/input{{hooks}}/python_pre.txt'):
137
            os.remove('tests/test-hooks/input{{hooks}}/python_pre.txt')
138
        if os.path.exists('tests/test-hooks/input{{hooks}}/shell_post.txt'):
139
            os.remove('tests/test-hooks/input{{hooks}}/shell_post.txt')
140
        if os.path.exists('tests/context_post.txt'):
141
            os.remove('tests/context_post.txt')
142
143
    def test_run_script(self):
144
        """Execute a hook script, independently of project generation."""
145
        hooks.run_script(os.path.join(self.hooks_path, self.post_hook))
146
        assert os.path.isfile('shell_post.txt')
147
148
    def test_run_failing_script(self, mocker):
149
        """Test correct exception raise if run_script fails."""
150
151
        err = OSError()
152
153
        prompt = mocker.patch('subprocess.Popen')
154
        prompt.side_effect = err
155
156
        with pytest.raises(exceptions.FailedHookException) as excinfo:
157
            hooks.run_script(os.path.join(self.hooks_path, self.post_hook))
158
        assert 'Hook script failed (error: {})'.format(err) in str(excinfo.value)
159
160
    def test_run_failing_script_enoexec(self, mocker):
161
        """Test correct exception raise if run_script fails."""
162
163
        err = OSError()
164
        err.errno = errno.ENOEXEC
165
166
        prompt = mocker.patch('subprocess.Popen')
167
        prompt.side_effect = err
168
169
        with pytest.raises(exceptions.FailedHookException) as excinfo:
170
            hooks.run_script(os.path.join(self.hooks_path, self.post_hook))
171
        assert 'Hook script failed, might be an empty file or missing a shebang' in str(excinfo.value)
172
173
    def test_run_script_cwd(self):
174
        """Change directory before running hook."""
175
        hooks.run_script(os.path.join(self.hooks_path, self.post_hook), 'tests')
176
        assert os.path.isfile('tests/shell_post.txt')
177
        assert 'tests' not in os.getcwd()
178
179
    def test_run_script_with_context(self):
180
        """Execute a hook script, passing a context."""
181
        hook_path = os.path.join(self.hooks_path, 'post_gen_project.sh')
182
183
        if sys.platform.startswith('win'):
184
            post = 'post_gen_project.bat'
185
            with open(os.path.join(self.hooks_path, post), 'w') as f:
186
                f.write("@echo off\n")
187
                f.write("\n")
188
                f.write("echo post generation hook\n")
189
                f.write("echo. >{{cookiecutter.file}}\n")
190
        else:
191
            with open(hook_path, 'w') as fh:
192
                fh.write("#!/bin/bash\n")
193
                fh.write("\n")
194
                fh.write("echo 'post generation hook';\n")
195
                fh.write("touch 'shell_post.txt'\n")
196
                fh.write("touch '{{cookiecutter.file}}'\n")
197
                os.chmod(hook_path, os.stat(hook_path).st_mode | stat.S_IXUSR)
198
199
        hooks.run_script_with_context(
200
            os.path.join(self.hooks_path, self.post_hook),
201
            'tests',
202
            {'cookiecutter': {'file': 'context_post.txt'}},
203
        )
204
        assert os.path.isfile('tests/context_post.txt')
205
        assert 'tests' not in os.getcwd()
206
207
    def test_run_hook(self):
208
        """Execute hook from specified template in specified output \
209
        directory."""
210
        tests_dir = os.path.join(self.repo_path, 'input{{hooks}}')
211
        with utils.work_in(self.repo_path):
212
            hooks.run_hook('pre_gen_project', tests_dir, {})
213
            assert os.path.isfile(os.path.join(tests_dir, 'python_pre.txt'))
214
            assert os.path.isfile(os.path.join(tests_dir, 'shell_pre.txt'))
215
216
            hooks.run_hook('post_gen_project', tests_dir, {})
217
            assert os.path.isfile(os.path.join(tests_dir, 'shell_post.txt'))
218
219
    def test_run_failing_hook(self):
220
        """Test correct exception raise if hook exit code is not zero."""
221
        hook_path = os.path.join(self.hooks_path, 'pre_gen_project.py')
222
        tests_dir = os.path.join(self.repo_path, 'input{{hooks}}')
223
224
        with open(hook_path, 'w') as f:
225
            f.write("#!/usr/bin/env python\n")
226
            f.write("import sys; sys.exit(1)\n")
227
228
        with utils.work_in(self.repo_path):
229
            with pytest.raises(exceptions.FailedHookException) as excinfo:
230
                hooks.run_hook('pre_gen_project', tests_dir, {})
231
            assert 'Hook script failed' in str(excinfo.value)
232
233
234
@pytest.fixture()
235
def dir_with_hooks(tmpdir):
236
    """Yield a directory that contains hook backup files."""
237
    hooks_dir = tmpdir.mkdir('hooks')
238
239
    pre_hook_content = textwrap.dedent(
240
        """
241
        #!/usr/bin/env python
242
        # -*- coding: utf-8 -*-
243
        print('pre_gen_project.py~')
244
        """
245
    )
246
    pre_gen_hook_file = hooks_dir / 'pre_gen_project.py~'
247
    pre_gen_hook_file.write_text(pre_hook_content, encoding='utf8')
248
249
    post_hook_content = textwrap.dedent(
250
        """
251
        #!/usr/bin/env python
252
        # -*- coding: utf-8 -*-
253
        print('post_gen_project.py~')
254
        """
255
    )
256
257
    post_gen_hook_file = hooks_dir / 'post_gen_project.py~'
258
    post_gen_hook_file.write_text(post_hook_content, encoding='utf8')
259
260
    # Make sure to yield the parent directory as `find_hooks()`
261
    # looks into `hooks/` in the current working directory
262
    yield str(tmpdir)
263
264
    pre_gen_hook_file.remove()
265
    post_gen_hook_file.remove()
266
267
268
def test_ignore_hook_backup_files(monkeypatch, dir_with_hooks):
269
    """Test `find_hook` correctly use `valid_hook` verification function."""
270
    # Change the current working directory that contains `hooks/`
271
    monkeypatch.chdir(dir_with_hooks)
272
    assert hooks.find_hook('pre_gen_project') is None
273
    assert hooks.find_hook('post_gen_project') is None
274