forked from cookiecutter/cookiecutter-django
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathtest_cookiecutter_generation.py
More file actions
executable file
·449 lines (373 loc) · 15.9 KB
/
test_cookiecutter_generation.py
File metadata and controls
executable file
·449 lines (373 loc) · 15.9 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
import glob # noqa: EXE002
import os
import re
import sys
from collections.abc import Iterable
from pathlib import Path
import pytest
import tomllib
try:
import sh
except (ImportError, ModuleNotFoundError):
sh = None # sh doesn't support Windows
import yaml
from binaryornot.check import is_binary
from cookiecutter.exceptions import FailedHookException
PATTERN = r"{{(\s?cookiecutter)[.](.*?)}}"
RE_OBJ = re.compile(PATTERN)
if sys.platform.startswith("win"):
pytest.skip("sh doesn't support windows", allow_module_level=True)
elif sys.platform.startswith("darwin") and os.getenv("CI"):
pytest.skip("skipping slow macOS tests on CI", allow_module_level=True)
# Run auto-fixable styles checks - skipped on CI by default. These can be fixed
# automatically by running pre-commit after generation however they are tedious
# to fix in the template, so we don't insist too much in fixing them.
AUTOFIXABLE_STYLES = os.getenv("AUTOFIXABLE_STYLES") == "1"
auto_fixable = pytest.mark.skipif(not AUTOFIXABLE_STYLES, reason="auto-fixable")
@pytest.fixture
def context():
return {
"project_name": "My Test Project",
"project_slug": "my_test_project",
"author_name": "Test Author",
"email": "test@example.com",
"description": "A short description of the project.",
"domain_name": "example.com",
"version": "0.1.0",
"timezone": "UTC",
}
SUPPORTED_COMBINATIONS = [
{"username_type": "username"},
{"username_type": "email"},
{"open_source_license": "MIT"},
{"open_source_license": "BSD"},
{"open_source_license": "GPLv3"},
{"open_source_license": "Apache Software License 2.0"},
{"open_source_license": "Not open source"},
{"windows": "y"},
{"windows": "n"},
{"editor": "None"},
{"editor": "PyCharm"},
{"editor": "VS Code"},
{"use_docker": "y"},
{"use_docker": "n"},
{"postgresql_version": "17"},
{"postgresql_version": "16"},
{"postgresql_version": "15"},
{"postgresql_version": "14"},
{"postgresql_version": "13"},
{"cloud_provider": "AWS", "use_whitenoise": "y"},
{"cloud_provider": "AWS", "use_whitenoise": "n"},
{"cloud_provider": "GCP", "use_whitenoise": "y"},
{"cloud_provider": "GCP", "use_whitenoise": "n"},
{"cloud_provider": "Azure", "use_whitenoise": "y"},
{"cloud_provider": "Azure", "use_whitenoise": "n"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Mailgun"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Mailjet"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Mandrill"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Postmark"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Sendgrid"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Brevo"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "SparkPost"},
{"cloud_provider": "None", "use_whitenoise": "y", "mail_service": "Other SMTP"},
# Note: cloud_provider=None AND use_whitenoise=n is not supported
{"cloud_provider": "AWS", "mail_service": "Mailgun"},
{"cloud_provider": "AWS", "mail_service": "Amazon SES"},
{"cloud_provider": "AWS", "mail_service": "Mailjet"},
{"cloud_provider": "AWS", "mail_service": "Mandrill"},
{"cloud_provider": "AWS", "mail_service": "Postmark"},
{"cloud_provider": "AWS", "mail_service": "Sendgrid"},
{"cloud_provider": "AWS", "mail_service": "Brevo"},
{"cloud_provider": "AWS", "mail_service": "SparkPost"},
{"cloud_provider": "AWS", "mail_service": "Other SMTP"},
{"cloud_provider": "GCP", "mail_service": "Mailgun"},
{"cloud_provider": "GCP", "mail_service": "Mailjet"},
{"cloud_provider": "GCP", "mail_service": "Mandrill"},
{"cloud_provider": "GCP", "mail_service": "Postmark"},
{"cloud_provider": "GCP", "mail_service": "Sendgrid"},
{"cloud_provider": "GCP", "mail_service": "Brevo"},
{"cloud_provider": "GCP", "mail_service": "SparkPost"},
{"cloud_provider": "GCP", "mail_service": "Other SMTP"},
{"cloud_provider": "Azure", "mail_service": "Mailgun"},
{"cloud_provider": "Azure", "mail_service": "Mailjet"},
{"cloud_provider": "Azure", "mail_service": "Mandrill"},
{"cloud_provider": "Azure", "mail_service": "Postmark"},
{"cloud_provider": "Azure", "mail_service": "Sendgrid"},
{"cloud_provider": "Azure", "mail_service": "Brevo"},
{"cloud_provider": "Azure", "mail_service": "SparkPost"},
{"cloud_provider": "Azure", "mail_service": "Other SMTP"},
# Note: cloud_providers GCP, Azure, and None
# with mail_service Amazon SES is not supported
{"use_async": "y"},
{"use_async": "n"},
{"use_drf": "y"},
{"use_drf": "n"},
{"frontend_pipeline": "None"},
{"frontend_pipeline": "Django Compressor"},
{"frontend_pipeline": "Gulp"},
{"frontend_pipeline": "Webpack"},
{"use_celery": "y"},
{"use_celery": "n"},
{"use_mailpit": "y"},
{"use_mailpit": "n"},
{"use_sentry": "y"},
{"use_sentry": "n"},
{"use_whitenoise": "y"},
{"use_whitenoise": "n"},
{"use_heroku": "y"},
{"use_heroku": "n"},
{"ci_tool": "None"},
{"ci_tool": "Travis"},
{"ci_tool": "Gitlab"},
{"ci_tool": "Github"},
{"ci_tool": "Drone"},
{"keep_local_envs_in_vcs": "y"},
{"keep_local_envs_in_vcs": "n"},
{"debug": "y"},
{"debug": "n"},
]
UNSUPPORTED_COMBINATIONS = [
{"cloud_provider": "None", "use_whitenoise": "n"},
{"cloud_provider": "GCP", "mail_service": "Amazon SES"},
{"cloud_provider": "Azure", "mail_service": "Amazon SES"},
{"cloud_provider": "None", "mail_service": "Amazon SES"},
]
def _fixture_id(ctx):
"""Helper to get a user-friendly test name from the parametrized context."""
return "-".join(f"{key}:{value}" for key, value in ctx.items())
def build_files_list(base_path: Path):
"""Build a list containing absolute paths to the generated files."""
excluded_dirs = {".venv", "__pycache__"}
f = []
for dirpath, subdirs, files in base_path.walk():
subdirs[:] = [d for d in subdirs if d not in excluded_dirs]
for file_path in files:
f.append(dirpath / file_path) # noqa: PERF401
return f
def check_paths(paths: Iterable[Path]):
"""Method to check all paths have correct substitutions."""
# Assert that no match is found in any of the files
for path in paths:
if is_binary(str(path)):
continue
for line in path.open():
match = RE_OBJ.search(line)
assert match is None, f"cookiecutter variable not replaced in {path}"
@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_project_generation(cookies, context, context_override):
"""Test that project is generated and fully rendered."""
result = cookies.bake(extra_context={**context, **context_override})
assert result.exit_code == 0
assert result.exception is None
assert result.project_path.name == context["project_slug"]
assert result.project_path.is_dir()
paths = build_files_list(result.project_path)
assert paths
check_paths(paths)
@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_ruff_check_passes(cookies, context_override):
"""Generated project should pass ruff check."""
result = cookies.bake(extra_context=context_override)
try:
sh.ruff("check", ".", _cwd=str(result.project_path))
except sh.ErrorReturnCode as e:
pytest.fail(e.stdout.decode())
@auto_fixable
@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_ruff_format_passes(cookies, context_override):
"""Check whether generated project passes ruff format."""
result = cookies.bake(extra_context=context_override)
try:
sh.ruff(
"format",
".",
_cwd=str(result.project_path),
)
except sh.ErrorReturnCode as e:
pytest.fail(e.stdout.decode())
@auto_fixable
@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_django_upgrade_passes(cookies, context_override):
"""Check whether generated project passes django-upgrade."""
result = cookies.bake(extra_context=context_override)
python_files = [
file_path.removeprefix(f"{result.project_path}/")
for file_path in glob.glob(str(result.project_path / "**" / "*.py"), recursive=True) # noqa: PTH207
]
try:
sh.django_upgrade(
"--target-version",
"5.0",
*python_files,
_cwd=str(result.project_path),
)
except sh.ErrorReturnCode as e:
pytest.fail(e.stdout.decode())
@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_djlint_lint_passes(cookies, context_override):
"""Check whether generated project passes djLint --lint."""
result = cookies.bake(extra_context=context_override)
autofixable_rules = "H014,T001"
# TODO: remove T002 when fixed https://github.com/Riverside-Healthcare/djLint/issues/687
ignored_rules = "H006,H030,H031,T002"
try:
sh.djlint(
"--lint",
"--ignore",
f"{autofixable_rules},{ignored_rules}",
".",
_cwd=str(result.project_path),
)
except sh.ErrorReturnCode as e:
pytest.fail(e.stdout.decode())
@auto_fixable
@pytest.mark.parametrize("context_override", SUPPORTED_COMBINATIONS, ids=_fixture_id)
def test_djlint_check_passes(cookies, context_override):
"""Check whether generated project passes djLint --check."""
result = cookies.bake(extra_context=context_override)
try:
sh.djlint("--check", ".", _cwd=str(result.project_path))
except sh.ErrorReturnCode as e:
pytest.fail(e.stdout.decode())
@pytest.mark.parametrize(
("use_docker", "expected_test_script"),
[
("n", "uv run pytest"),
("y", "docker compose -f docker-compose.local.yml run django pytest"),
],
)
def test_travis_invokes_pytest(cookies, context, use_docker, expected_test_script):
context.update({"ci_tool": "Travis", "use_docker": use_docker})
result = cookies.bake(extra_context=context)
assert result.exit_code == 0
assert result.exception is None
assert result.project_path.name == context["project_slug"]
assert result.project_path.is_dir()
with (result.project_path / ".travis.yml").open() as travis_yml:
try:
yml = yaml.safe_load(travis_yml)["jobs"]["include"]
assert yml[0]["script"] == ["ruff check ."]
assert yml[1]["script"] == [expected_test_script]
except yaml.YAMLError as e:
pytest.fail(str(e))
@pytest.mark.parametrize(
("use_docker", "expected_test_script"),
[
("n", "uv run pytest"),
("y", "docker compose -f docker-compose.local.yml run django pytest"),
],
)
def test_gitlab_invokes_precommit_and_pytest(cookies, context, use_docker, expected_test_script):
context.update({"ci_tool": "Gitlab", "use_docker": use_docker})
result = cookies.bake(extra_context=context)
assert result.exit_code == 0
assert result.exception is None
assert result.project_path.name == context["project_slug"]
assert result.project_path.is_dir()
with (result.project_path / ".gitlab-ci.yml").open() as gitlab_yml:
try:
gitlab_config = yaml.safe_load(gitlab_yml)
assert gitlab_config["precommit"]["script"] == [
"uv run pre-commit run --show-diff-on-failure --color=always --all-files",
]
assert gitlab_config["pytest"]["script"] == [expected_test_script]
except yaml.YAMLError as e:
pytest.fail(e)
@pytest.mark.parametrize(
("use_docker", "expected_test_script"),
[
("n", "uv run pytest"),
("y", "docker compose -f docker-compose.local.yml run django pytest"),
],
)
def test_github_invokes_linter_and_pytest(cookies, context, use_docker, expected_test_script):
context.update({"ci_tool": "Github", "use_docker": use_docker})
result = cookies.bake(extra_context=context)
assert result.exit_code == 0
assert result.exception is None
assert result.project_path.name == context["project_slug"]
assert result.project_path.is_dir()
with (result.project_path / ".github" / "workflows" / "ci.yml").open() as github_yml:
try:
github_config = yaml.safe_load(github_yml)
linter_present = False
for action_step in github_config["jobs"]["linter"]["steps"]:
if action_step.get("uses", "NA").startswith("pre-commit"):
linter_present = True
assert linter_present
expected_test_script_present = False
for action_step in github_config["jobs"]["pytest"]["steps"]:
if action_step.get("run") == expected_test_script:
expected_test_script_present = True
assert expected_test_script_present
except yaml.YAMLError as e:
pytest.fail(e)
@pytest.mark.parametrize("slug", ["project slug", "Project_Slug"])
def test_invalid_slug(cookies, context, slug):
"""Invalid slug should fail pre-generation hook."""
context.update({"project_slug": slug})
result = cookies.bake(extra_context=context)
assert result.exit_code != 0
assert isinstance(result.exception, FailedHookException)
@pytest.mark.parametrize("invalid_context", UNSUPPORTED_COMBINATIONS)
def test_error_if_incompatible(cookies, context, invalid_context):
"""It should not generate project an incompatible combination is selected."""
context.update(invalid_context)
result = cookies.bake(extra_context=context)
assert result.exit_code != 0
assert isinstance(result.exception, FailedHookException)
@pytest.mark.parametrize(
("editor", "pycharm_docs_exist"),
[
("None", False),
("PyCharm", True),
("VS Code", False),
],
)
def test_pycharm_docs_removed(cookies, context, editor, pycharm_docs_exist):
context.update({"editor": editor})
result = cookies.bake(extra_context=context)
index_rst = result.project_path / "docs" / "index.rst"
has_pycharm_docs = "pycharm/configuration" in index_rst.read_text()
assert has_pycharm_docs is pycharm_docs_exist
def test_trim_domain_email(cookies, context):
"""Check that leading and trailing spaces are trimmed in domain and email."""
context.update(
{
"use_docker": "y",
"domain_name": " example.com ",
"email": " me@example.com ",
},
)
result = cookies.bake(extra_context=context)
assert result.exit_code == 0
prod_django_env = result.project_path / ".envs" / ".production" / ".django"
assert "DJANGO_ALLOWED_HOSTS=.example.com" in prod_django_env.read_text()
base_settings = result.project_path / "config" / "settings" / "base.py"
assert '"me@example.com"' in base_settings.read_text()
def test_pyproject_toml(cookies, context):
author_name = "Project Author"
author_email = "me@example.com"
context.update(
{
"description": "DESCRIPTION",
"domain_name": "example.com",
"email": author_email,
"author_name": author_name,
},
)
result = cookies.bake(extra_context=context)
assert result.exit_code == 0
pyproject_toml = result.project_path / "pyproject.toml"
data = tomllib.loads(pyproject_toml.read_text())
assert data
assert data["project"]["authors"][0]["email"] == author_email
assert data["project"]["authors"][0]["name"] == author_name
assert data["project"]["name"] == context["project_slug"]
def test_pre_commit_without_heroku(cookies, context):
context.update({"use_heroku": "n"})
result = cookies.bake(extra_context=context)
assert result.exit_code == 0
pre_commit_config = result.project_path / ".pre-commit-config.yaml"
data = pre_commit_config.read_text()
assert "uv-pre-commit" not in data