Passed
Push — master ( c48119...a6113e )
by
unknown
02:32 queued 01:20
created

tests.test_cli   A

Complexity

Total Complexity 42

Size/Duplication

Total Lines 596
Duplicated Lines 26.68 %

Importance

Changes 0
Metric Value
wmc 42
eloc 380
dl 159
loc 596
rs 9.0399
c 0
b 0
f 0

35 Functions

Rating   Name   Duplication   Size   Complexity  
A test_cli_verbose() 0 8 2
A test_cli_version() 0 5 1
A make_fake_project_dir() 0 4 1
A version_cli_flag() 0 4 1
A test_cli() 0 8 2
A test_cli_error_on_existing_output_directory() 0 7 1
A cli_runner() 0 10 1
A remove_fake_project_dir() 0 9 2
A test_cli_replay() 23 23 1
A overwrite_cli_flag() 0 4 1
A test_run_cookiecutter_on_overwrite_if_exists_and_replay() 0 28 1
A output_dir_flag() 0 4 1
A test_cli_exit_on_noinput_and_replay() 0 32 1
A test_cli_overwrite_if_exists_when_output_dir_exists() 0 9 1
A test_cli_overwrite_if_exists_when_output_dir_does_not_exist() 0 12 1
A test_cli_replay_file() 0 23 1
A test_default_user_config_overwrite() 24 24 1
A test_echo_undefined_variable_error() 0 28 1
A test_debug_file_non_verbose() 21 21 1
A debug_file() 0 4 1
A test_cli_extra_context_invalid_format() 0 9 1
A test_default_user_config() 22 22 1
A test_debug_list_installed_templates() 0 19 2
A test_directory_repo() 0 10 2
A test_cli_with_json_decoding_error() 0 17 1
A test_echo_unknown_extension_error() 0 11 1
A test_cli_extra_context() 0 10 2
A test_cli_accept_hooks() 0 35 1
A user_config_path() 0 4 1
A help_cli_flag() 0 4 1
A test_debug_file_verbose() 25 25 1
A test_debug_list_installed_templates_failure() 0 14 2
A test_cli_output_dir() 22 22 1
A test_cli_help() 0 5 1
A test_user_config() 22 22 1

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_cli 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
"""Collection of tests around cookiecutter's command-line interface."""
2
3
import json
4
import os
5
import re
6
7
import pytest
8
from click.testing import CliRunner
9
10
from cookiecutter import utils
11
from cookiecutter.__main__ import main
12
from cookiecutter.main import cookiecutter
13
14
15
@pytest.fixture(scope='session')
16
def cli_runner():
17
    """Fixture that returns a helper function to run the cookiecutter cli."""
18
    runner = CliRunner()
19
20
    def cli_main(*cli_args, **cli_kwargs):
21
        """Run cookiecutter cli main with the given args."""
22
        return runner.invoke(main, cli_args, **cli_kwargs)
23
24
    return cli_main
25
26
27
@pytest.fixture
28
def remove_fake_project_dir(request):
29
    """Remove the fake project directory created during the tests."""
30
31
    def fin_remove_fake_project_dir():
32
        if os.path.isdir('fake-project'):
33
            utils.rmtree('fake-project')
34
35
    request.addfinalizer(fin_remove_fake_project_dir)
36
37
38
@pytest.fixture
39
def make_fake_project_dir(request):
40
    """Create a fake project to be overwritten in the according tests."""
41
    os.makedirs('fake-project')
42
43
44
@pytest.fixture(params=['-V', '--version'])
45
def version_cli_flag(request):
46
    """Pytest fixture return both version invocation options."""
47
    return request.param
48
49
50
def test_cli_version(cli_runner, version_cli_flag):
51
    """Verify correct version output by `cookiecutter` on cli invocation."""
52
    result = cli_runner(version_cli_flag)
53
    assert result.exit_code == 0
54
    assert result.output.startswith('Cookiecutter')
55
56
57
@pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir')
58
def test_cli_error_on_existing_output_directory(cli_runner):
59
    """Test cli invocation without `overwrite-if-exists` fail if dir exist."""
60
    result = cli_runner('tests/fake-repo-pre/', '--no-input')
61
    assert result.exit_code != 0
62
    expected_error_msg = 'Error: "fake-project" directory already exists\n'
63
    assert result.output == expected_error_msg
64
65
66
@pytest.mark.usefixtures('remove_fake_project_dir')
67
def test_cli(cli_runner):
68
    """Test cli invocation work without flags if directory not exist."""
69
    result = cli_runner('tests/fake-repo-pre/', '--no-input')
70
    assert result.exit_code == 0
71
    assert os.path.isdir('fake-project')
72
    with open(os.path.join('fake-project', 'README.rst')) as f:
73
        assert 'Project name: **Fake Project**' in f.read()
74
75
76
@pytest.mark.usefixtures('remove_fake_project_dir')
77
def test_cli_verbose(cli_runner):
78
    """Test cli invocation display log if called with `verbose` flag."""
79
    result = cli_runner('tests/fake-repo-pre/', '--no-input', '-v')
80
    assert result.exit_code == 0
81
    assert os.path.isdir('fake-project')
82
    with open(os.path.join('fake-project', 'README.rst')) as f:
83
        assert 'Project name: **Fake Project**' in f.read()
84
85
86 View Code Duplication
@pytest.mark.usefixtures('remove_fake_project_dir')
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
87
def test_cli_replay(mocker, cli_runner):
88
    """Test cli invocation display log with `verbose` and `replay` flags."""
89
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')
90
91
    template_path = 'tests/fake-repo-pre/'
92
    result = cli_runner(template_path, '--replay', '-v')
93
94
    assert result.exit_code == 0
95
    mock_cookiecutter.assert_called_once_with(
96
        template_path,
97
        None,
98
        False,
99
        replay=True,
100
        overwrite_if_exists=False,
101
        skip_if_file_exists=False,
102
        output_dir='.',
103
        config_file=None,
104
        default_config=False,
105
        extra_context=None,
106
        password=None,
107
        directory=None,
108
        accept_hooks=True,
109
    )
110
111
112
@pytest.mark.usefixtures('remove_fake_project_dir')
113
def test_cli_replay_file(mocker, cli_runner):
114
    """Test cli invocation correctly pass --replay-file option."""
115
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')
116
117
    template_path = 'tests/fake-repo-pre/'
118
    result = cli_runner(template_path, '--replay-file', '~/custom-replay-file', '-v')
119
120
    assert result.exit_code == 0
121
    mock_cookiecutter.assert_called_once_with(
122
        template_path,
123
        None,
124
        False,
125
        replay='~/custom-replay-file',
126
        overwrite_if_exists=False,
127
        skip_if_file_exists=False,
128
        output_dir='.',
129
        config_file=None,
130
        default_config=False,
131
        extra_context=None,
132
        password=None,
133
        directory=None,
134
        accept_hooks=True,
135
    )
136
137
138
@pytest.mark.usefixtures('remove_fake_project_dir')
139
def test_cli_exit_on_noinput_and_replay(mocker, cli_runner):
140
    """Test cli invocation fail if both `no-input` and `replay` flags passed."""
141
    mock_cookiecutter = mocker.patch(
142
        'cookiecutter.cli.cookiecutter', side_effect=cookiecutter
143
    )
144
145
    template_path = 'tests/fake-repo-pre/'
146
    result = cli_runner(template_path, '--no-input', '--replay', '-v')
147
148
    assert result.exit_code == 1
149
150
    expected_error_msg = (
151
        "You can not use both replay and no_input or extra_context at the same time."
152
    )
153
154
    assert expected_error_msg in result.output
155
156
    mock_cookiecutter.assert_called_once_with(
157
        template_path,
158
        None,
159
        True,
160
        replay=True,
161
        overwrite_if_exists=False,
162
        skip_if_file_exists=False,
163
        output_dir='.',
164
        config_file=None,
165
        default_config=False,
166
        extra_context=None,
167
        password=None,
168
        directory=None,
169
        accept_hooks=True,
170
    )
171
172
173
@pytest.fixture(params=['-f', '--overwrite-if-exists'])
174
def overwrite_cli_flag(request):
175
    """Pytest fixture return all `overwrite-if-exists` invocation options."""
176
    return request.param
177
178
179
@pytest.mark.usefixtures('remove_fake_project_dir')
180
def test_run_cookiecutter_on_overwrite_if_exists_and_replay(
181
    mocker, cli_runner, overwrite_cli_flag
182
):
183
    """Test cli invocation with `overwrite-if-exists` and `replay` flags."""
184
    mock_cookiecutter = mocker.patch(
185
        'cookiecutter.cli.cookiecutter', side_effect=cookiecutter
186
    )
187
188
    template_path = 'tests/fake-repo-pre/'
189
    result = cli_runner(template_path, '--replay', '-v', overwrite_cli_flag)
190
191
    assert result.exit_code == 0
192
193
    mock_cookiecutter.assert_called_once_with(
194
        template_path,
195
        None,
196
        False,
197
        replay=True,
198
        overwrite_if_exists=True,
199
        skip_if_file_exists=False,
200
        output_dir='.',
201
        config_file=None,
202
        default_config=False,
203
        extra_context=None,
204
        password=None,
205
        directory=None,
206
        accept_hooks=True,
207
    )
208
209
210
@pytest.mark.usefixtures('remove_fake_project_dir')
211
def test_cli_overwrite_if_exists_when_output_dir_does_not_exist(
212
    cli_runner, overwrite_cli_flag
213
):
214
    """Test cli invocation with `overwrite-if-exists` and `no-input` flags.
215
216
    Case when output dir not exist.
217
    """
218
    result = cli_runner('tests/fake-repo-pre/', '--no-input', overwrite_cli_flag)
219
220
    assert result.exit_code == 0
221
    assert os.path.isdir('fake-project')
222
223
224
@pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir')
225
def test_cli_overwrite_if_exists_when_output_dir_exists(cli_runner, overwrite_cli_flag):
226
    """Test cli invocation with `overwrite-if-exists` and `no-input` flags.
227
228
    Case when output dir already exist.
229
    """
230
    result = cli_runner('tests/fake-repo-pre/', '--no-input', overwrite_cli_flag)
231
    assert result.exit_code == 0
232
    assert os.path.isdir('fake-project')
233
234
235
@pytest.fixture(params=['-o', '--output-dir'])
236
def output_dir_flag(request):
237
    """Pytest fixture return all output-dir invocation options."""
238
    return request.param
239
240
241 View Code Duplication
def test_cli_output_dir(mocker, cli_runner, output_dir_flag, output_dir):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
242
    """Test cli invocation with `output-dir` flag changes output directory."""
243
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')
244
245
    template_path = 'tests/fake-repo-pre/'
246
    result = cli_runner(template_path, output_dir_flag, output_dir)
247
248
    assert result.exit_code == 0
249
    mock_cookiecutter.assert_called_once_with(
250
        template_path,
251
        None,
252
        False,
253
        replay=False,
254
        overwrite_if_exists=False,
255
        skip_if_file_exists=False,
256
        output_dir=output_dir,
257
        config_file=None,
258
        default_config=False,
259
        extra_context=None,
260
        password=None,
261
        directory=None,
262
        accept_hooks=True,
263
    )
264
265
266
@pytest.fixture(params=['-h', '--help', 'help'])
267
def help_cli_flag(request):
268
    """Pytest fixture return all help invocation options."""
269
    return request.param
270
271
272
def test_cli_help(cli_runner, help_cli_flag):
273
    """Test cli invocation display help message with `help` flag."""
274
    result = cli_runner(help_cli_flag)
275
    assert result.exit_code == 0
276
    assert result.output.startswith('Usage')
277
278
279
@pytest.fixture
280
def user_config_path(tmp_path):
281
    """Pytest fixture return `user_config` argument as string."""
282
    return str(tmp_path.joinpath("tests", "config.yaml"))
283
284
285 View Code Duplication
def test_user_config(mocker, cli_runner, user_config_path):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
286
    """Test cli invocation works with `config-file` option."""
287
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')
288
289
    template_path = 'tests/fake-repo-pre/'
290
    result = cli_runner(template_path, '--config-file', user_config_path)
291
292
    assert result.exit_code == 0
293
    mock_cookiecutter.assert_called_once_with(
294
        template_path,
295
        None,
296
        False,
297
        replay=False,
298
        overwrite_if_exists=False,
299
        skip_if_file_exists=False,
300
        output_dir='.',
301
        config_file=user_config_path,
302
        default_config=False,
303
        extra_context=None,
304
        password=None,
305
        directory=None,
306
        accept_hooks=True,
307
    )
308
309
310 View Code Duplication
def test_default_user_config_overwrite(mocker, cli_runner, user_config_path):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
311
    """Test cli invocation ignores `config-file` if `default-config` passed."""
312
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')
313
314
    template_path = 'tests/fake-repo-pre/'
315
    result = cli_runner(
316
        template_path, '--config-file', user_config_path, '--default-config',
317
    )
318
319
    assert result.exit_code == 0
320
    mock_cookiecutter.assert_called_once_with(
321
        template_path,
322
        None,
323
        False,
324
        replay=False,
325
        overwrite_if_exists=False,
326
        skip_if_file_exists=False,
327
        output_dir='.',
328
        config_file=user_config_path,
329
        default_config=True,
330
        extra_context=None,
331
        password=None,
332
        directory=None,
333
        accept_hooks=True,
334
    )
335
336
337 View Code Duplication
def test_default_user_config(mocker, cli_runner):
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
338
    """Test cli invocation accepts `default-config` flag correctly."""
339
    mock_cookiecutter = mocker.patch('cookiecutter.cli.cookiecutter')
340
341
    template_path = 'tests/fake-repo-pre/'
342
    result = cli_runner(template_path, '--default-config')
343
344
    assert result.exit_code == 0
345
    mock_cookiecutter.assert_called_once_with(
346
        template_path,
347
        None,
348
        False,
349
        replay=False,
350
        overwrite_if_exists=False,
351
        skip_if_file_exists=False,
352
        output_dir='.',
353
        config_file=None,
354
        default_config=True,
355
        extra_context=None,
356
        password=None,
357
        directory=None,
358
        accept_hooks=True,
359
    )
360
361
362
def test_echo_undefined_variable_error(output_dir, cli_runner):
363
    """Cli invocation return error if variable undefined in template."""
364
    template_path = 'tests/undefined-variable/file-name/'
365
366
    result = cli_runner(
367
        '--no-input', '--default-config', '--output-dir', output_dir, template_path,
368
    )
369
370
    assert result.exit_code == 1
371
372
    error = "Unable to create file '{{cookiecutter.foobar}}'"
373
    assert error in result.output
374
375
    message = (
376
        "Error message: 'collections.OrderedDict object' has no attribute 'foobar'"
377
    )
378
    assert message in result.output
379
380
    context = {
381
        'cookiecutter': {
382
            'github_username': 'hackebrot',
383
            'project_slug': 'testproject',
384
            '_template': template_path,
385
            '_output_dir': output_dir,
386
        }
387
    }
388
    context_str = json.dumps(context, indent=4, sort_keys=True)
389
    assert context_str in result.output
390
391
392
def test_echo_unknown_extension_error(output_dir, cli_runner):
393
    """Cli return error if extension incorrectly defined in template."""
394
    template_path = 'tests/test-extensions/unknown/'
395
396
    result = cli_runner(
397
        '--no-input', '--default-config', '--output-dir', output_dir, template_path,
398
    )
399
400
    assert result.exit_code == 1
401
402
    assert 'Unable to load extension: ' in result.output
403
404
405
@pytest.mark.usefixtures('remove_fake_project_dir')
406
def test_cli_extra_context(cli_runner):
407
    """Cli invocation replace content if called with replacement pairs."""
408
    result = cli_runner(
409
        'tests/fake-repo-pre/', '--no-input', '-v', 'project_name=Awesomez',
410
    )
411
    assert result.exit_code == 0
412
    assert os.path.isdir('fake-project')
413
    with open(os.path.join('fake-project', 'README.rst')) as f:
414
        assert 'Project name: **Awesomez**' in f.read()
415
416
417
@pytest.mark.usefixtures('remove_fake_project_dir')
418
def test_cli_extra_context_invalid_format(cli_runner):
419
    """Cli invocation raise error if called with unknown argument."""
420
    result = cli_runner(
421
        'tests/fake-repo-pre/', '--no-input', '-v', 'ExtraContextWithNoEqualsSoInvalid',
422
    )
423
    assert result.exit_code == 2
424
    assert "Error: Invalid value for '[EXTRA_CONTEXT]...'" in result.output
425
    assert 'should contain items of the form key=value' in result.output
426
427
428
@pytest.fixture
429
def debug_file(tmp_path):
430
    """Pytest fixture return `debug_file` argument as path object."""
431
    return tmp_path.joinpath('fake-repo.log')
432
433
434 View Code Duplication
@pytest.mark.usefixtures('remove_fake_project_dir')
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
435
def test_debug_file_non_verbose(cli_runner, debug_file):
436
    """Test cli invocation writes log to `debug-file` if flag enabled.
437
438
    Case for normal log output.
439
    """
440
    assert not debug_file.exists()
441
442
    result = cli_runner(
443
        '--no-input', '--debug-file', str(debug_file), 'tests/fake-repo-pre/',
444
    )
445
    assert result.exit_code == 0
446
447
    assert debug_file.exists()
448
449
    context_log = (
450
        "DEBUG cookiecutter.main: context_file is "
451
        "tests/fake-repo-pre/cookiecutter.json"
452
    )
453
    assert context_log in debug_file.read_text()
454
    assert context_log not in result.output
455
456
457 View Code Duplication
@pytest.mark.usefixtures('remove_fake_project_dir')
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
458
def test_debug_file_verbose(cli_runner, debug_file):
459
    """Test cli invocation writes log to `debug-file` if flag enabled.
460
461
    Case for verbose log output.
462
    """
463
    assert not debug_file.exists()
464
465
    result = cli_runner(
466
        '--verbose',
467
        '--no-input',
468
        '--debug-file',
469
        str(debug_file),
470
        'tests/fake-repo-pre/',
471
    )
472
    assert result.exit_code == 0
473
474
    assert debug_file.exists()
475
476
    context_log = (
477
        "DEBUG cookiecutter.main: context_file is "
478
        "tests/fake-repo-pre/cookiecutter.json"
479
    )
480
    assert context_log in debug_file.read_text()
481
    assert context_log in result.output
482
483
484
@pytest.mark.usefixtures('make_fake_project_dir', 'remove_fake_project_dir')
485
def test_debug_list_installed_templates(cli_runner, debug_file, user_config_path):
486
    """Verify --list-installed command correct invocation."""
487
    fake_template_dir = os.path.dirname(os.path.abspath('fake-project'))
488
    os.makedirs(os.path.dirname(user_config_path))
489
    with open(user_config_path, 'w') as config_file:
490
        # In YAML, double quotes mean to use escape sequences.
491
        # Single quotes mean we will have unescaped backslahes.
492
        # http://blogs.perl.org/users/tinita/2018/03/
493
        # strings-in-yaml---to-quote-or-not-to-quote.html
494
        config_file.write("cookiecutters_dir: '%s'" % fake_template_dir)
495
    open(os.path.join('fake-project', 'cookiecutter.json'), 'w').write('{}')
496
497
    result = cli_runner(
498
        '--list-installed', '--config-file', user_config_path, str(debug_file),
499
    )
500
501
    assert "1 installed templates:" in result.output
502
    assert result.exit_code == 0
503
504
505
def test_debug_list_installed_templates_failure(
506
    cli_runner, debug_file, user_config_path
507
):
508
    """Verify --list-installed command error on invocation."""
509
    os.makedirs(os.path.dirname(user_config_path))
510
    with open(user_config_path, 'w') as config_file:
511
        config_file.write('cookiecutters_dir: "/notarealplace/"')
512
513
    result = cli_runner(
514
        '--list-installed', '--config-file', user_config_path, str(debug_file)
515
    )
516
517
    assert "Error: Cannot list installed templates." in result.output
518
    assert result.exit_code == -1
519
520
521
@pytest.mark.usefixtures('remove_fake_project_dir')
522
def test_directory_repo(cli_runner):
523
    """Test cli invocation works with `directory` option."""
524
    result = cli_runner(
525
        'tests/fake-repo-dir/', '--no-input', '-v', '--directory=my-dir',
526
    )
527
    assert result.exit_code == 0
528
    assert os.path.isdir("fake-project")
529
    with open(os.path.join("fake-project", "README.rst")) as f:
530
        assert "Project name: **Fake Project**" in f.read()
531
532
533
cli_accept_hook_arg_testdata = [
534
    ("--accept-hooks=yes", None, True),
535
    ("--accept-hooks=no", None, False),
536
    ("--accept-hooks=ask", "yes", True),
537
    ("--accept-hooks=ask", "no", False),
538
]
539
540
541
@pytest.mark.parametrize(
542
    "accept_hooks_arg,user_input,expected", cli_accept_hook_arg_testdata
543
)
544
def test_cli_accept_hooks(
545
    mocker,
546
    cli_runner,
547
    output_dir_flag,
548
    output_dir,
549
    accept_hooks_arg,
550
    user_input,
551
    expected,
552
):
553
    """Test cli invocation works with `accept-hooks` option."""
554
    mock_cookiecutter = mocker.patch("cookiecutter.cli.cookiecutter")
555
556
    template_path = "tests/fake-repo-pre/"
557
    result = cli_runner(
558
        template_path, output_dir_flag, output_dir, accept_hooks_arg, input=user_input
559
    )
560
561
    assert result.exit_code == 0
562
    mock_cookiecutter.assert_called_once_with(
563
        template_path,
564
        None,
565
        False,
566
        replay=False,
567
        overwrite_if_exists=False,
568
        output_dir=output_dir,
569
        config_file=None,
570
        default_config=False,
571
        extra_context=None,
572
        password=None,
573
        directory=None,
574
        skip_if_file_exists=False,
575
        accept_hooks=expected,
576
    )
577
578
579
@pytest.mark.usefixtures('remove_fake_project_dir')
580
def test_cli_with_json_decoding_error(cli_runner):
581
    """Test cli invocation with a malformed JSON file."""
582
    template_path = 'tests/fake-repo-bad-json/'
583
    result = cli_runner(template_path, '--no-input')
584
    assert result.exit_code != 0
585
586
    # Validate the error message.
587
    # original message from json module should be included
588
    pattern = 'Expecting \'{0,1}:\'{0,1} delimiter: line 1 column (19|20) \\(char 19\\)'
589
    assert re.search(pattern, result.output)
590
    # File name should be included too...for testing purposes, just test the
591
    # last part of the file. If we wanted to test the absolute path, we'd have
592
    # to do some additional work in the test which doesn't seem that needed at
593
    # this point.
594
    path = os.path.sep.join(['tests', 'fake-repo-bad-json', 'cookiecutter.json'])
595
    assert path in result.output
596