BoilerWorks

boilerworks / tests / test_generator.py
Blame History Raw 343 lines
1
"""Tests for boilerworks.generator."""
2
3
from __future__ import annotations
4
5
import shutil
6
from pathlib import Path
7
from unittest.mock import MagicMock, patch
8
9
import pytest
10
11
from boilerworks.generator import (
12
_clone_and_render_ops,
13
_dry_run_plan,
14
_write_ops_config,
15
generate_from_manifest,
16
)
17
from boilerworks.manifest import BoilerworksManifest
18
19
20
class TestDryRun:
21
def test_dry_run_no_files_created(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None:
22
"""Dry-run should print the plan without touching the filesystem."""
23
project_dir = tmp_path / valid_manifest.project
24
_dry_run_plan(valid_manifest, tmp_path)
25
assert not project_dir.exists()
26
27
def test_generate_from_manifest_dry_run(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None:
28
"""generate_from_manifest with dry_run=True must not create any files."""
29
manifest_file = tmp_path / "boilerworks.yaml"
30
valid_manifest.to_file(manifest_file)
31
32
project_dir = tmp_path / valid_manifest.project
33
generate_from_manifest(
34
manifest_path=str(manifest_file),
35
output_dir=str(tmp_path),
36
dry_run=True,
37
)
38
assert not project_dir.exists()
39
40
def test_dry_run_with_ops_standard(self, tmp_path: Path) -> None:
41
"""Dry-run with cloud + ops shows ops clone step (standard topology)."""
42
manifest = BoilerworksManifest(
43
project="test-app",
44
family="django-nextjs",
45
size="full",
46
cloud="aws",
47
ops=True,
48
topology="standard",
49
)
50
_dry_run_plan(manifest, tmp_path) # should not raise
51
52
def test_dry_run_with_ops_omni(self, tmp_path: Path) -> None:
53
"""Dry-run with cloud + ops shows ops clone step (omni topology)."""
54
manifest = BoilerworksManifest(
55
project="test-app",
56
family="django-nextjs",
57
size="full",
58
cloud="gcp",
59
ops=True,
60
topology="omni",
61
)
62
_dry_run_plan(manifest, tmp_path) # should not raise
63
64
def test_dry_run_no_ops_when_flag_false(self, tmp_path: Path) -> None:
65
"""Dry-run with cloud set but ops=False does not include ops steps."""
66
manifest = BoilerworksManifest(
67
project="test-app",
68
family="django-nextjs",
69
size="full",
70
cloud="aws",
71
ops=False,
72
)
73
_dry_run_plan(manifest, tmp_path) # should not raise
74
75
def test_dry_run_shows_mobile_step(self, tmp_path: Path) -> None:
76
manifest = BoilerworksManifest(
77
project="test-app",
78
family="django-nextjs",
79
size="full",
80
mobile=True,
81
)
82
_dry_run_plan(manifest, tmp_path)
83
84
85
class TestWriteOpsConfig:
86
def _make_ops_dir(self, tmp_path: Path, cloud: str) -> Path:
87
ops_dir = tmp_path / "ops"
88
cloud_dir = ops_dir / cloud
89
cloud_dir.mkdir(parents=True)
90
config = cloud_dir / "config.env"
91
config.write_text('PROJECT="boilerworks"\nAWS_REGION="us-west-2"\nOWNER="conflict"\n')
92
return ops_dir
93
94
def test_aws_config_written(self, tmp_path: Path) -> None:
95
ops_dir = self._make_ops_dir(tmp_path, "aws")
96
_write_ops_config(ops_dir, "aws", "myproject", "eu-west-1", "myproject.com")
97
98
content = (ops_dir / "aws" / "config.env").read_text()
99
assert 'PROJECT="myproject"' in content
100
assert 'AWS_REGION="eu-west-1"' in content
101
assert 'OWNER="myproject"' in content
102
assert 'DOMAIN="myproject.com"' in content
103
104
def test_gcp_config_written(self, tmp_path: Path) -> None:
105
ops_dir = self._make_ops_dir(tmp_path, "gcp")
106
_write_ops_config(ops_dir, "gcp", "myproject", "us-central1", None)
107
108
content = (ops_dir / "gcp" / "config.env").read_text()
109
assert 'PROJECT="myproject"' in content
110
assert 'GCP_REGION="us-central1"' in content
111
assert "DOMAIN" not in content
112
113
def test_azure_config_default_region(self, tmp_path: Path) -> None:
114
ops_dir = self._make_ops_dir(tmp_path, "azure")
115
_write_ops_config(ops_dir, "azure", "myproject", None, None)
116
117
content = (ops_dir / "azure" / "config.env").read_text()
118
assert 'AZURE_REGION="eastus"' in content
119
120
def test_missing_config_file_is_noop(self, tmp_path: Path) -> None:
121
"""If config.env doesn't exist yet, write_ops_config should not raise."""
122
ops_dir = tmp_path / "ops"
123
ops_dir.mkdir()
124
(ops_dir / "aws").mkdir()
125
# No config.env file
126
_write_ops_config(ops_dir, "aws", "myproject", "us-east-1", None)
127
128
129
class TestCloneAndRenderOps:
130
def _fake_clone(self, src: Path) -> None:
131
"""Create a fake clone that looks like a minimal boilerworks-opscode."""
132
src.mkdir(parents=True, exist_ok=True)
133
(src / ".git").mkdir()
134
(src / "aws").mkdir()
135
(src / "aws" / "config.env").write_text('PROJECT="boilerworks"\nAWS_REGION="us-west-2"\nOWNER="conflict"\n')
136
(src / "README.md").write_text("# Boilerworks Opscode\nBoilerworks infrastructure.\n")
137
138
def test_ops_clone_and_render_standard(self, tmp_path: Path) -> None:
139
"""_clone_and_render_ops populates dest and renders project name."""
140
ops_dest = tmp_path / "myproject-ops"
141
142
def fake_clone(repo: str, dest: Path) -> None:
143
self._fake_clone(dest)
144
145
progress = MagicMock()
146
progress.add_task.return_value = "task-id"
147
148
with patch("boilerworks.generator._clone_repo", side_effect=fake_clone):
149
_clone_and_render_ops("myproject", "aws", "us-east-1", "myproject.com", ops_dest, progress)
150
151
assert ops_dest.exists()
152
assert not (ops_dest / ".git").exists()
153
config_content = (ops_dest / "aws" / "config.env").read_text()
154
assert 'PROJECT="myproject"' in config_content
155
156
def test_ops_clone_failure_exits(self, tmp_path: Path) -> None:
157
"""When cloning ops fails, process exits."""
158
ops_dest = tmp_path / "myproject-ops"
159
progress = MagicMock()
160
progress.add_task.return_value = "task-id"
161
162
with (
163
patch("boilerworks.generator._clone_repo", side_effect=RuntimeError("clone failed")),
164
pytest.raises(SystemExit),
165
):
166
_clone_and_render_ops("myproject", "aws", "us-east-1", None, ops_dest, progress)
167
168
169
class TestGenerateFromManifestErrors:
170
def test_missing_manifest_exits(self, tmp_path: Path) -> None:
171
with pytest.raises(SystemExit):
172
generate_from_manifest(
173
manifest_path=str(tmp_path / "nonexistent.yaml"),
174
output_dir=str(tmp_path),
175
)
176
177
def test_invalid_manifest_yaml_exits(self, tmp_path: Path) -> None:
178
bad_yaml = tmp_path / "boilerworks.yaml"
179
bad_yaml.write_text("project: Invalid Name With Spaces\nfamily: django-nextjs\nsize: full\n")
180
with pytest.raises(SystemExit):
181
generate_from_manifest(
182
manifest_path=str(bad_yaml),
183
output_dir=str(tmp_path),
184
)
185
186
def test_existing_project_dir_exits(self, tmp_path: Path, valid_manifest: BoilerworksManifest) -> None:
187
manifest_file = tmp_path / "boilerworks.yaml"
188
valid_manifest.to_file(manifest_file)
189
190
# Pre-create the project dir
191
(tmp_path / valid_manifest.project).mkdir()
192
193
with pytest.raises(SystemExit):
194
generate_from_manifest(
195
manifest_path=str(manifest_file),
196
output_dir=str(tmp_path),
197
)
198
199
200
class TestGenerateWithOps:
201
"""Integration-style tests using mocked git operations."""
202
203
def _seed_template(self, dest: Path, project: str = "boilerworks") -> None:
204
"""Create a minimal template directory (simulates cloned repo)."""
205
dest.mkdir(parents=True, exist_ok=True)
206
(dest / ".git").mkdir()
207
(dest / "README.md").write_text(f"# {project.title()}\nA {project} app.\n")
208
(dest / "docker-compose.yaml").write_text(f"services:\n db:\n image: postgres\n # {project}\n")
209
210
def _seed_opscode(self, dest: Path) -> None:
211
dest.mkdir(parents=True, exist_ok=True)
212
(dest / ".git").mkdir()
213
(dest / "aws").mkdir()
214
(dest / "aws" / "config.env").write_text('PROJECT="boilerworks"\nAWS_REGION="us-west-2"\nOWNER="conflict"\n')
215
(dest / "README.md").write_text("# Boilerworks Opscode\n")
216
217
def test_generate_standard_with_ops(self, tmp_path: Path) -> None:
218
"""Standard topology: app and ops end up as sibling dirs."""
219
manifest = BoilerworksManifest(
220
project="myapp",
221
family="django-nextjs",
222
size="full",
223
topology="standard",
224
cloud="aws",
225
ops=True,
226
region="us-east-1",
227
)
228
manifest_file = tmp_path / "boilerworks.yaml"
229
manifest.to_file(manifest_file)
230
231
call_count = 0
232
233
def fake_clone(repo: str, dest: Path) -> None:
234
nonlocal call_count
235
call_count += 1
236
if "opscode" in repo:
237
self._seed_opscode(dest)
238
else:
239
self._seed_template(dest)
240
241
with (
242
patch("boilerworks.generator._clone_repo", side_effect=fake_clone),
243
patch("boilerworks.generator.subprocess.run"),
244
):
245
generate_from_manifest(manifest_path=str(manifest_file), output_dir=str(tmp_path))
246
247
assert call_count == 2
248
assert (tmp_path / "myapp").exists()
249
assert (tmp_path / "myapp-ops").exists()
250
assert not (tmp_path / "myapp-ops" / ".git").exists()
251
252
def test_generate_omni_with_ops(self, tmp_path: Path) -> None:
253
"""Omni topology: ops/ lives inside the app directory."""
254
manifest = BoilerworksManifest(
255
project="myapp",
256
family="django-nextjs",
257
size="full",
258
topology="omni",
259
cloud="aws",
260
ops=True,
261
region="us-east-1",
262
)
263
manifest_file = tmp_path / "boilerworks.yaml"
264
manifest.to_file(manifest_file)
265
266
call_count = 0
267
268
def fake_clone(repo: str, dest: Path) -> None:
269
nonlocal call_count
270
call_count += 1
271
if "opscode" in repo:
272
self._seed_opscode(dest)
273
else:
274
self._seed_template(dest)
275
276
with (
277
patch("boilerworks.generator._clone_repo", side_effect=fake_clone),
278
patch("boilerworks.generator.subprocess.run"),
279
):
280
generate_from_manifest(manifest_path=str(manifest_file), output_dir=str(tmp_path))
281
282
assert call_count == 2
283
assert (tmp_path / "myapp").exists()
284
assert (tmp_path / "myapp" / "ops").exists()
285
286
def test_generate_without_ops(self, tmp_path: Path) -> None:
287
"""When ops=False, only the app template is cloned."""
288
manifest = BoilerworksManifest(
289
project="myapp",
290
family="django-nextjs",
291
size="full",
292
topology="standard",
293
cloud="aws",
294
ops=False,
295
)
296
manifest_file = tmp_path / "boilerworks.yaml"
297
manifest.to_file(manifest_file)
298
299
call_count = 0
300
301
def fake_clone(repo: str, dest: Path) -> None:
302
nonlocal call_count
303
call_count += 1
304
self._seed_template(dest)
305
306
with (
307
patch("boilerworks.generator._clone_repo", side_effect=fake_clone),
308
patch("boilerworks.generator.subprocess.run"),
309
):
310
generate_from_manifest(manifest_path=str(manifest_file), output_dir=str(tmp_path))
311
312
assert call_count == 1
313
assert (tmp_path / "myapp").exists()
314
assert not (tmp_path / "myapp-ops").exists()
315
316
def test_generate_existing_ops_dir_exits(self, tmp_path: Path) -> None:
317
"""Standard topology: if ops dir already exists, exits cleanly."""
318
manifest = BoilerworksManifest(
319
project="myapp",
320
family="django-nextjs",
321
size="full",
322
topology="standard",
323
cloud="aws",
324
ops=True,
325
)
326
manifest_file = tmp_path / "boilerworks.yaml"
327
manifest.to_file(manifest_file)
328
329
# Pre-create ops dir
330
(tmp_path / "myapp-ops").mkdir()
331
332
def fake_clone(repo: str, dest: Path) -> None:
333
self._seed_template(dest)
334
335
with (
336
patch("boilerworks.generator._clone_repo", side_effect=fake_clone),
337
patch("boilerworks.generator.subprocess.run"),
338
pytest.raises(SystemExit),
339
):
340
generate_from_manifest(manifest_path=str(manifest_file), output_dir=str(tmp_path))
341
342
shutil.rmtree(tmp_path / "myapp", ignore_errors=True)
343

Keyboard Shortcuts

Open search /
Next entry (timeline) j
Previous entry (timeline) k
Open focused entry Enter
Show this help ?
Toggle theme Top nav button