TestExternalHooks.teardown_method()   B
last analyzed

Complexity

Conditions 8

Size

Total Lines 18
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 18
rs 7.3333
c 0
b 0
f 0
cc 8
nop 2
1
"""Tests for `cookiecutter.hooks` module."""
2
import errno
3
import os
4
import stat
5
import sys
6
import textwrap
7
from pathlib import Path
8
9
import pytest
10
11
from cookiecutter import hooks, utils, exceptions
12
13
14
def make_test_repo(name, multiple_hooks=False):
15
    """Create test repository for test setup methods."""
16
    hook_dir = os.path.join(name, 'hooks')
17
    template = os.path.join(name, 'input{{hooks}}')
18
    os.mkdir(name)
19
    os.mkdir(hook_dir)
20
    os.mkdir(template)
21
22
    Path(template, 'README.rst').write_text("foo\n===\n\nbar\n")
23
24
    with Path(hook_dir, 'pre_gen_project.py').open('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 Path(hook_dir, post).open('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 Path(filename).open('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 Path(hook_dir, pre).open('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 Path(filename).open('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:
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(f'hooks/{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:
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
        err = OSError()
151
152
        prompt = mocker.patch('subprocess.Popen')
153
        prompt.side_effect = err
154
155
        with pytest.raises(exceptions.FailedHookException) as excinfo:
156
            hooks.run_script(os.path.join(self.hooks_path, self.post_hook))
157
        assert f'Hook script failed (error: {err})' in str(excinfo.value)
158
159
    def test_run_failing_script_enoexec(self, mocker):
160
        """Test correct exception raise if run_script fails."""
161
        err = OSError()
162
        err.errno = errno.ENOEXEC
163
164
        prompt = mocker.patch('subprocess.Popen')
165
        prompt.side_effect = err
166
167
        with pytest.raises(exceptions.FailedHookException) as excinfo:
168
            hooks.run_script(os.path.join(self.hooks_path, self.post_hook))
169
        assert 'Hook script failed, might be an empty file or missing a shebang' in str(
170
            excinfo.value
171
        )
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 Path(self.hooks_path, post).open('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 Path(hook_path).open('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 Path(hook_path).open('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(tmp_path):
236
    """Yield a directory that contains hook backup files."""
237
    hooks_dir = tmp_path.joinpath('hooks')
238
    hooks_dir.mkdir()
239
240
    pre_hook_content = textwrap.dedent(
241
        """
242
        #!/usr/bin/env python
243
        # -*- coding: utf-8 -*-
244
        print('pre_gen_project.py~')
245
        """
246
    )
247
    pre_gen_hook_file = hooks_dir.joinpath('pre_gen_project.py~')
248
    pre_gen_hook_file.write_text(pre_hook_content, encoding='utf8')
249
250
    post_hook_content = textwrap.dedent(
251
        """
252
        #!/usr/bin/env python
253
        # -*- coding: utf-8 -*-
254
        print('post_gen_project.py~')
255
        """
256
    )
257
258
    post_gen_hook_file = hooks_dir.joinpath('post_gen_project.py~')
259
    post_gen_hook_file.write_text(post_hook_content, encoding='utf8')
260
261
    # Make sure to yield the parent directory as `find_hooks()`
262
    # looks into `hooks/` in the current working directory
263
    yield str(tmp_path)
264
265
    pre_gen_hook_file.unlink()
266
    post_gen_hook_file.unlink()
267
268
269
def test_ignore_hook_backup_files(monkeypatch, dir_with_hooks):
270
    """Test `find_hook` correctly use `valid_hook` verification function."""
271
    # Change the current working directory that contains `hooks/`
272
    monkeypatch.chdir(dir_with_hooks)
273
    assert hooks.find_hook('pre_gen_project') is None
274
    assert hooks.find_hook('post_gen_project') is None
275