Passed
Push — master ( d7e7b2...90434f )
by Andrey
06:54 queued 05:53
created

tests.test_hooks   B

Complexity

Total Complexity 43

Size/Duplication

Total Lines 248
Duplicated Lines 14.11 %

Importance

Changes 0
Metric Value
wmc 43
eloc 164
dl 35
loc 248
rs 8.96
c 0
b 0
f 0

3 Functions

Rating   Name   Duplication   Size   Complexity  
A dir_with_hooks() 0 32 1
A test_ignore_hook_backup_files() 0 6 1
C make_test_repo() 35 59 10

13 Methods

Rating   Name   Duplication   Size   Complexity  
A TestFindHooks.teardown_method() 0 3 1
A TestExternalHooks.test_run_hook() 0 11 2
A TestExternalHooks.test_run_script() 0 4 1
A TestExternalHooks.test_run_failing_hook() 0 13 4
A TestFindHooks.setup_method() 0 3 1
A TestFindHooks.test_find_hook() 0 10 2
A TestExternalHooks.setup_method() 0 3 1
B TestExternalHooks.teardown_method() 0 18 8
A TestExternalHooks.test_run_script_with_context() 0 27 4
A TestFindHooks.test_no_hooks() 0 4 2
A TestExternalHooks.test_run_script_cwd() 0 5 1
A TestFindHooks.test_hook_not_found() 0 4 2
A TestFindHooks.test_unknown_hooks_dir() 0 4 2

How to fix   Duplicated Code    Complexity   

Duplicated Code

Duplicate code is one of the most pungent code smells. A rule that is often used is to re-structure code once it is duplicated in three or more places.

Common duplication problems, and corresponding solutions are:

Complexity

 Tip:   Before tackling complexity, make sure that you eliminate any duplication first. This often can reduce the size of classes significantly.

Complex classes like tests.test_hooks often do a lot of different things. To break such a class down, we need to identify a cohesive component within that class. A common approach to find such a component is to look for fields/methods that share the same prefixes, or suffixes.

Once you have determined the fields that belong together, you can apply the Extract Class refactoring. If the component makes sense as a sub-class, Extract Subclass is also a candidate, and is often faster.

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