Passed
Push — master ( 0b4062...c0e769 )
by
unknown
05:31
created

tests.test_cli.test_local_extension()   A

Complexity

Conditions 2

Size

Total Lines 17
Code Lines 14

Duplication

Lines 0
Ratio 0 %

Importance

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