sprout-cli 0.5.0__tar.gz → 0.7.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/PKG-INFO +12 -3
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/README.md +11 -2
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/src/sprout/__init__.py +1 -1
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/src/sprout/commands/create.py +4 -1
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/src/sprout/utils.py +45 -5
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/tests/test_integration.py +96 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/tests/test_utils.py +134 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/.gitignore +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/LICENSE +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/pyproject.toml +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/src/sprout/__main__.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/src/sprout/cli.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/src/sprout/commands/__init__.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/src/sprout/commands/ls.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/src/sprout/commands/path.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/src/sprout/commands/rm.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/src/sprout/exceptions.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/src/sprout/types.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/tests/__init__.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/tests/test_commands.py +0 -0
- {sprout_cli-0.5.0 → sprout_cli-0.7.0}/tests/test_multi_env.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sprout-cli
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.7.0
|
|
4
4
|
Summary: CLI tool to automate git worktree and Docker Compose development workflows
|
|
5
5
|
Author: SecDevLab Inc.
|
|
6
6
|
License: MIT
|
|
@@ -68,6 +68,10 @@ API_PORT={{ auto_port() }}
|
|
|
68
68
|
DB_HOST=localhost
|
|
69
69
|
DB_PORT={{ auto_port() }}
|
|
70
70
|
|
|
71
|
+
# Branch-specific Configuration
|
|
72
|
+
SERVICE_NAME=myapp-{{ branch() }}
|
|
73
|
+
DEPLOYMENT_ENV={{ branch() }}
|
|
74
|
+
|
|
71
75
|
# Example: Docker Compose variables (preserved as-is)
|
|
72
76
|
# sprout will NOT process ${...} syntax - it's passed through unchanged
|
|
73
77
|
# DB_NAME=${DB_NAME}
|
|
@@ -180,7 +184,7 @@ Show the version of sprout.
|
|
|
180
184
|
|
|
181
185
|
## Template Syntax
|
|
182
186
|
|
|
183
|
-
sprout supports
|
|
187
|
+
sprout supports three types of placeholders in `.env.example`:
|
|
184
188
|
|
|
185
189
|
1. **Variable Placeholders**: `{{ VARIABLE_NAME }}`
|
|
186
190
|
- **First**: Checks if the variable exists in your environment (e.g., `export API_KEY=xxx`)
|
|
@@ -193,7 +197,12 @@ sprout supports two types of placeholders in `.env.example`:
|
|
|
193
197
|
- Checks system port availability
|
|
194
198
|
- Ensures global uniqueness even in monorepo setups
|
|
195
199
|
|
|
196
|
-
3. **
|
|
200
|
+
3. **Branch Name**: `{{ branch() }}`
|
|
201
|
+
- Replaced with the current branch/subtree name
|
|
202
|
+
- Useful for branch-specific configurations
|
|
203
|
+
- Example: `SERVICE_NAME=myapp-{{ branch() }}` becomes `SERVICE_NAME=myapp-feature-auth`
|
|
204
|
+
|
|
205
|
+
4. **Docker Compose Syntax (Preserved)**: `${VARIABLE}`
|
|
197
206
|
- NOT processed by sprout - passed through as-is
|
|
198
207
|
- Useful for Docker Compose variable substitution
|
|
199
208
|
- Example: `${DB_NAME:-default}` remains unchanged in generated `.env`
|
|
@@ -40,6 +40,10 @@ API_PORT={{ auto_port() }}
|
|
|
40
40
|
DB_HOST=localhost
|
|
41
41
|
DB_PORT={{ auto_port() }}
|
|
42
42
|
|
|
43
|
+
# Branch-specific Configuration
|
|
44
|
+
SERVICE_NAME=myapp-{{ branch() }}
|
|
45
|
+
DEPLOYMENT_ENV={{ branch() }}
|
|
46
|
+
|
|
43
47
|
# Example: Docker Compose variables (preserved as-is)
|
|
44
48
|
# sprout will NOT process ${...} syntax - it's passed through unchanged
|
|
45
49
|
# DB_NAME=${DB_NAME}
|
|
@@ -152,7 +156,7 @@ Show the version of sprout.
|
|
|
152
156
|
|
|
153
157
|
## Template Syntax
|
|
154
158
|
|
|
155
|
-
sprout supports
|
|
159
|
+
sprout supports three types of placeholders in `.env.example`:
|
|
156
160
|
|
|
157
161
|
1. **Variable Placeholders**: `{{ VARIABLE_NAME }}`
|
|
158
162
|
- **First**: Checks if the variable exists in your environment (e.g., `export API_KEY=xxx`)
|
|
@@ -165,7 +169,12 @@ sprout supports two types of placeholders in `.env.example`:
|
|
|
165
169
|
- Checks system port availability
|
|
166
170
|
- Ensures global uniqueness even in monorepo setups
|
|
167
171
|
|
|
168
|
-
3. **
|
|
172
|
+
3. **Branch Name**: `{{ branch() }}`
|
|
173
|
+
- Replaced with the current branch/subtree name
|
|
174
|
+
- Useful for branch-specific configurations
|
|
175
|
+
- Example: `SERVICE_NAME=myapp-{{ branch() }}` becomes `SERVICE_NAME=myapp-feature-auth`
|
|
176
|
+
|
|
177
|
+
4. **Docker Compose Syntax (Preserved)**: `${VARIABLE}`
|
|
169
178
|
- NOT processed by sprout - passed through as-is
|
|
170
179
|
- Useful for Docker Compose variable substitution
|
|
171
180
|
- Example: `${DB_NAME:-default}` remains unchanged in generated `.env`
|
|
@@ -109,7 +109,10 @@ def create_worktree(branch_name: BranchName, path_only: bool = False) -> Never:
|
|
|
109
109
|
|
|
110
110
|
# Parse template with combined used ports
|
|
111
111
|
env_content = parse_env_template(
|
|
112
|
-
env_example,
|
|
112
|
+
env_example,
|
|
113
|
+
silent=path_only,
|
|
114
|
+
used_ports=all_used_ports | session_ports,
|
|
115
|
+
branch_name=branch_name,
|
|
113
116
|
)
|
|
114
117
|
|
|
115
118
|
# Extract ports from generated content and add to session_ports
|
|
@@ -126,7 +126,10 @@ def find_available_port() -> PortNumber:
|
|
|
126
126
|
|
|
127
127
|
|
|
128
128
|
def parse_env_template(
|
|
129
|
-
template_path: Path,
|
|
129
|
+
template_path: Path,
|
|
130
|
+
silent: bool = False,
|
|
131
|
+
used_ports: PortSet | None = None,
|
|
132
|
+
branch_name: str | None = None,
|
|
130
133
|
) -> str:
|
|
131
134
|
"""Parse .env.example template and process placeholders.
|
|
132
135
|
|
|
@@ -134,6 +137,7 @@ def parse_env_template(
|
|
|
134
137
|
template_path: Path to the .env.example template file
|
|
135
138
|
silent: If True, use stderr for prompts to keep stdout clean
|
|
136
139
|
used_ports: Set of ports already in use (in addition to system-wide used ports)
|
|
140
|
+
branch_name: Branch name to use for {{ branch() }} placeholders
|
|
137
141
|
"""
|
|
138
142
|
if not template_path.exists():
|
|
139
143
|
raise SproutError(f".env.example file not found at {template_path}")
|
|
@@ -151,22 +155,56 @@ def parse_env_template(
|
|
|
151
155
|
file_ports.update(used_ports)
|
|
152
156
|
|
|
153
157
|
for line in content.splitlines():
|
|
154
|
-
# Process {{ auto_port() }} placeholders
|
|
158
|
+
# Process {{ auto_port() | default }} placeholders
|
|
155
159
|
def replace_auto_port(match: re.Match[str]) -> str:
|
|
160
|
+
# Extract default value if present (group 1)
|
|
161
|
+
default_value = match.group(1)
|
|
162
|
+
if default_value is not None:
|
|
163
|
+
default_value = default_value.strip()
|
|
164
|
+
|
|
165
|
+
# Generate available port
|
|
156
166
|
port = find_available_port()
|
|
157
167
|
while port in file_ports:
|
|
158
168
|
port = find_available_port()
|
|
159
169
|
file_ports.add(port)
|
|
160
170
|
return str(port)
|
|
161
171
|
|
|
162
|
-
line = re.sub(r"{{\s*auto_port\(\)
|
|
172
|
+
line = re.sub(r"{{\s*auto_port\(\)(?:\s*\|\s*([^}]*))?\s*}}", replace_auto_port, line)
|
|
173
|
+
|
|
174
|
+
# Process {{ branch() | default }} placeholders
|
|
175
|
+
def replace_branch(match: re.Match[str]) -> str:
|
|
176
|
+
# Extract default value if present (group 1)
|
|
177
|
+
default_value = match.group(1)
|
|
178
|
+
if default_value is not None:
|
|
179
|
+
default_value = default_value.strip()
|
|
180
|
+
|
|
181
|
+
# Use branch_name if provided, otherwise use default
|
|
182
|
+
if branch_name:
|
|
183
|
+
return branch_name
|
|
184
|
+
elif default_value is not None:
|
|
185
|
+
return default_value
|
|
186
|
+
else:
|
|
187
|
+
# No branch name and no default - keep placeholder unchanged
|
|
188
|
+
return match.group(0)
|
|
163
189
|
|
|
164
|
-
|
|
190
|
+
line = re.sub(r"{{\s*branch\(\)(?:\s*\|\s*([^}]*))?\s*}}", replace_branch, line)
|
|
191
|
+
|
|
192
|
+
# Process {{ VARIABLE | default }} placeholders
|
|
165
193
|
def replace_variable(match: re.Match[str]) -> str:
|
|
166
194
|
var_name = match.group(1).strip()
|
|
195
|
+
# Extract default value if present (group 2)
|
|
196
|
+
default_value = match.group(2)
|
|
197
|
+
if default_value is not None:
|
|
198
|
+
default_value = default_value.strip()
|
|
199
|
+
|
|
167
200
|
# Check environment variable first
|
|
168
201
|
value = os.environ.get(var_name)
|
|
169
202
|
if value is None:
|
|
203
|
+
# If default value is provided, use it
|
|
204
|
+
if default_value is not None:
|
|
205
|
+
return default_value
|
|
206
|
+
|
|
207
|
+
# No default value - prompt user for value
|
|
170
208
|
# Create a relative path for display
|
|
171
209
|
try:
|
|
172
210
|
display_path = template_path.relative_to(Path.cwd())
|
|
@@ -187,7 +225,9 @@ def parse_env_template(
|
|
|
187
225
|
value = console.input(prompt)
|
|
188
226
|
return value
|
|
189
227
|
|
|
190
|
-
|
|
228
|
+
# Only match variables that don't look like function calls (no parentheses)
|
|
229
|
+
# Now also captures optional | default_value
|
|
230
|
+
line = re.sub(r"{{\s*([^}()]+?)(?:\s*\|\s*([^}]*))?\s*}}", replace_variable, line)
|
|
191
231
|
|
|
192
232
|
lines.append(line)
|
|
193
233
|
|
|
@@ -198,6 +198,102 @@ class TestIntegrationWorkflow:
|
|
|
198
198
|
worktree_path = git_repo / ".sprout" / "existing-branch"
|
|
199
199
|
assert worktree_path.exists()
|
|
200
200
|
|
|
201
|
+
def test_branch_placeholder(self, git_repo, monkeypatch):
|
|
202
|
+
"""Test that {{ branch() }} placeholder is replaced with branch name."""
|
|
203
|
+
git_repo, default_branch = git_repo
|
|
204
|
+
monkeypatch.chdir(git_repo)
|
|
205
|
+
monkeypatch.setenv("API_KEY", "test_key")
|
|
206
|
+
|
|
207
|
+
# Create .env.example with branch() placeholder
|
|
208
|
+
env_example = git_repo / ".env.example"
|
|
209
|
+
env_example.write_text(
|
|
210
|
+
"# Branch-specific configuration\n"
|
|
211
|
+
"BRANCH_NAME={{ branch() }}\n"
|
|
212
|
+
"SERVICE_NAME=myapp-{{ branch() }}\n"
|
|
213
|
+
"API_KEY={{ API_KEY }}\n"
|
|
214
|
+
"PORT={{ auto_port() }}\n"
|
|
215
|
+
"COMPOSE_VAR=${BRANCH_NAME}\n"
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
# Add to git
|
|
219
|
+
subprocess.run(["git", "add", ".env.example"], cwd=git_repo, check=True)
|
|
220
|
+
subprocess.run(
|
|
221
|
+
["git", "commit", "-m", "Add .env.example with branch placeholder"],
|
|
222
|
+
cwd=git_repo,
|
|
223
|
+
check=True,
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
# Create worktree
|
|
227
|
+
branch_name = "feature-auth"
|
|
228
|
+
result = runner.invoke(app, ["create", branch_name])
|
|
229
|
+
assert result.exit_code == 0
|
|
230
|
+
|
|
231
|
+
# Verify .env was created with correct branch name substitution
|
|
232
|
+
env_file = git_repo / ".sprout" / branch_name / ".env"
|
|
233
|
+
env_content = env_file.read_text()
|
|
234
|
+
|
|
235
|
+
# Check branch name substitutions
|
|
236
|
+
assert f"BRANCH_NAME={branch_name}" in env_content
|
|
237
|
+
assert f"SERVICE_NAME=myapp-{branch_name}" in env_content
|
|
238
|
+
assert "API_KEY=test_key" in env_content
|
|
239
|
+
assert "PORT=" in env_content # Should have a port number
|
|
240
|
+
assert "COMPOSE_VAR=${BRANCH_NAME}" in env_content # Docker syntax preserved
|
|
241
|
+
|
|
242
|
+
def test_default_values(self, git_repo, monkeypatch):
|
|
243
|
+
"""Test that default values work correctly in .env.example templates."""
|
|
244
|
+
git_repo, default_branch = git_repo
|
|
245
|
+
monkeypatch.chdir(git_repo)
|
|
246
|
+
|
|
247
|
+
# Set only some environment variables
|
|
248
|
+
monkeypatch.setenv("SET_VAR", "from_environment")
|
|
249
|
+
# UNSET_VAR is not set - should use default
|
|
250
|
+
|
|
251
|
+
# Create .env.example with default values
|
|
252
|
+
env_example = git_repo / ".env.example"
|
|
253
|
+
env_example.write_text(
|
|
254
|
+
"# Test default values\n"
|
|
255
|
+
"SET_VAR={{ SET_VAR | default_value }}\n"
|
|
256
|
+
"UNSET_VAR={{ UNSET_VAR | localhost }}\n"
|
|
257
|
+
"EMPTY_VAR={{ EMPTY_VAR | }}\n"
|
|
258
|
+
"PORT={{ auto_port() | 8080 }}\n"
|
|
259
|
+
"BRANCH={{ branch() | develop }}\n"
|
|
260
|
+
"NO_DEFAULT={{ NO_DEFAULT }}\n" # Will be prompted
|
|
261
|
+
)
|
|
262
|
+
|
|
263
|
+
# Add to git
|
|
264
|
+
subprocess.run(["git", "add", ".env.example"], cwd=git_repo, check=True)
|
|
265
|
+
subprocess.run(
|
|
266
|
+
["git", "commit", "-m", "Add .env.example with defaults"],
|
|
267
|
+
cwd=git_repo,
|
|
268
|
+
check=True,
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
# Set NO_DEFAULT to avoid prompt
|
|
272
|
+
monkeypatch.setenv("NO_DEFAULT", "prompted_value")
|
|
273
|
+
|
|
274
|
+
# Create worktree
|
|
275
|
+
branch_name = "feature-test"
|
|
276
|
+
result = runner.invoke(app, ["create", branch_name])
|
|
277
|
+
assert result.exit_code == 0
|
|
278
|
+
|
|
279
|
+
# Verify .env was created with correct values
|
|
280
|
+
env_file = git_repo / ".sprout" / branch_name / ".env"
|
|
281
|
+
env_content = env_file.read_text()
|
|
282
|
+
|
|
283
|
+
# Check that environment variable overrides default
|
|
284
|
+
assert "SET_VAR=from_environment" in env_content
|
|
285
|
+
# Check that default is used when variable is not set
|
|
286
|
+
assert "UNSET_VAR=localhost" in env_content
|
|
287
|
+
# Check empty string default
|
|
288
|
+
assert "EMPTY_VAR=" in env_content or "EMPTY_VAR=\n" in env_content
|
|
289
|
+
# Check that auto_port generates a port (default not used)
|
|
290
|
+
assert "PORT=" in env_content
|
|
291
|
+
assert "PORT=8080" not in env_content # Should be a generated port
|
|
292
|
+
# Check that branch name is used (not default)
|
|
293
|
+
assert f"BRANCH={branch_name}" in env_content
|
|
294
|
+
# Check prompted value
|
|
295
|
+
assert "NO_DEFAULT=prompted_value" in env_content
|
|
296
|
+
|
|
201
297
|
def test_error_cases(self, git_repo, monkeypatch, tmp_path):
|
|
202
298
|
"""Test various error conditions."""
|
|
203
299
|
git_repo, default_branch = git_repo
|
|
@@ -179,6 +179,140 @@ class TestEnvTemplateParser:
|
|
|
179
179
|
result = parse_env_template(template)
|
|
180
180
|
assert result == "COMPOSE_VAR=${COMPOSE_VAR:-default}"
|
|
181
181
|
|
|
182
|
+
def test_parse_env_template_branch_placeholder(self, tmp_path):
|
|
183
|
+
"""Test parsing {{ branch() }} placeholders."""
|
|
184
|
+
template = tmp_path / ".env.example"
|
|
185
|
+
template.write_text("BRANCH_NAME={{ branch() }}\nFEATURE={{ branch() }}_feature")
|
|
186
|
+
|
|
187
|
+
result = parse_env_template(template, branch_name="feature-auth")
|
|
188
|
+
assert result == "BRANCH_NAME=feature-auth\nFEATURE=feature-auth_feature"
|
|
189
|
+
|
|
190
|
+
def test_parse_env_template_branch_placeholder_none(self, tmp_path):
|
|
191
|
+
"""Test {{ branch() }} placeholder when branch_name is None."""
|
|
192
|
+
template = tmp_path / ".env.example"
|
|
193
|
+
template.write_text("BRANCH={{ branch() }}")
|
|
194
|
+
|
|
195
|
+
# When branch_name is None, placeholder should remain unchanged
|
|
196
|
+
result = parse_env_template(template, branch_name=None)
|
|
197
|
+
assert result == "BRANCH={{ branch() }}"
|
|
198
|
+
|
|
199
|
+
def test_parse_env_template_mixed_placeholders(self, tmp_path, mocker):
|
|
200
|
+
"""Test parsing mixed placeholders in one template."""
|
|
201
|
+
mocker.patch("sprout.utils.find_available_port", return_value=8080)
|
|
202
|
+
mocker.patch.dict(os.environ, {"API_KEY": "secret"})
|
|
203
|
+
mocker.patch("sprout.utils.console.input", return_value="password123")
|
|
204
|
+
|
|
205
|
+
template = tmp_path / ".env.example"
|
|
206
|
+
template.write_text(
|
|
207
|
+
"API_KEY={{ API_KEY }}\n"
|
|
208
|
+
"PORT={{ auto_port() }}\n"
|
|
209
|
+
"BRANCH={{ branch() }}\n"
|
|
210
|
+
"DB_PASS={{ DB_PASS }}\n"
|
|
211
|
+
"COMPOSE_VAR=${COMPOSE_VAR}"
|
|
212
|
+
)
|
|
213
|
+
|
|
214
|
+
result = parse_env_template(template, branch_name="feature-xyz")
|
|
215
|
+
assert result == (
|
|
216
|
+
"API_KEY=secret\n"
|
|
217
|
+
"PORT=8080\n"
|
|
218
|
+
"BRANCH=feature-xyz\n"
|
|
219
|
+
"DB_PASS=password123\n"
|
|
220
|
+
"COMPOSE_VAR=${COMPOSE_VAR}"
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def test_parse_env_template_variable_with_default(self, tmp_path, mocker):
|
|
224
|
+
"""Test parsing {{ VARIABLE | default }} when variable is not set."""
|
|
225
|
+
# Ensure the variable is not in environment
|
|
226
|
+
mocker.patch.dict(os.environ, {}, clear=False)
|
|
227
|
+
if "TEST_VAR" in os.environ:
|
|
228
|
+
del os.environ["TEST_VAR"]
|
|
229
|
+
|
|
230
|
+
template = tmp_path / ".env.example"
|
|
231
|
+
template.write_text("TEST_VAR={{ TEST_VAR | localhost }}")
|
|
232
|
+
|
|
233
|
+
result = parse_env_template(template)
|
|
234
|
+
assert result == "TEST_VAR=localhost"
|
|
235
|
+
|
|
236
|
+
def test_parse_env_template_variable_with_default_env_set(self, tmp_path, mocker):
|
|
237
|
+
"""Test that environment variable overrides default value."""
|
|
238
|
+
mocker.patch.dict(os.environ, {"TEST_VAR": "custom_value"})
|
|
239
|
+
|
|
240
|
+
template = tmp_path / ".env.example"
|
|
241
|
+
template.write_text("TEST_VAR={{ TEST_VAR | localhost }}")
|
|
242
|
+
|
|
243
|
+
result = parse_env_template(template)
|
|
244
|
+
assert result == "TEST_VAR=custom_value"
|
|
245
|
+
|
|
246
|
+
def test_parse_env_template_variable_with_empty_default(self, tmp_path, mocker):
|
|
247
|
+
"""Test parsing {{ VARIABLE | }} with empty string as default."""
|
|
248
|
+
mocker.patch.dict(os.environ, {}, clear=False)
|
|
249
|
+
if "EMPTY_VAR" in os.environ:
|
|
250
|
+
del os.environ["EMPTY_VAR"]
|
|
251
|
+
|
|
252
|
+
template = tmp_path / ".env.example"
|
|
253
|
+
template.write_text("EMPTY_VAR={{ EMPTY_VAR | }}")
|
|
254
|
+
|
|
255
|
+
result = parse_env_template(template)
|
|
256
|
+
assert result == "EMPTY_VAR="
|
|
257
|
+
|
|
258
|
+
def test_parse_env_template_variable_whitespace_trimming(self, tmp_path, mocker):
|
|
259
|
+
"""Test that whitespace is trimmed from default values."""
|
|
260
|
+
mocker.patch.dict(os.environ, {}, clear=False)
|
|
261
|
+
if "TEST_VAR" in os.environ:
|
|
262
|
+
del os.environ["TEST_VAR"]
|
|
263
|
+
|
|
264
|
+
template = tmp_path / ".env.example"
|
|
265
|
+
template.write_text("TEST_VAR={{ TEST_VAR | localhost }}")
|
|
266
|
+
|
|
267
|
+
result = parse_env_template(template)
|
|
268
|
+
assert result == "TEST_VAR=localhost"
|
|
269
|
+
|
|
270
|
+
def test_parse_env_template_branch_with_default(self, tmp_path):
|
|
271
|
+
"""Test parsing {{ branch() | default }} when branch_name is None."""
|
|
272
|
+
template = tmp_path / ".env.example"
|
|
273
|
+
template.write_text("BRANCH={{ branch() | main }}")
|
|
274
|
+
|
|
275
|
+
result = parse_env_template(template, branch_name=None)
|
|
276
|
+
assert result == "BRANCH=main"
|
|
277
|
+
|
|
278
|
+
def test_parse_env_template_branch_with_default_branch_set(self, tmp_path):
|
|
279
|
+
"""Test that branch_name overrides default value."""
|
|
280
|
+
template = tmp_path / ".env.example"
|
|
281
|
+
template.write_text("BRANCH={{ branch() | main }}")
|
|
282
|
+
|
|
283
|
+
result = parse_env_template(template, branch_name="feature-xyz")
|
|
284
|
+
assert result == "BRANCH=feature-xyz"
|
|
285
|
+
|
|
286
|
+
def test_parse_env_template_auto_port_with_default(self, tmp_path, mocker):
|
|
287
|
+
"""Test {{ auto_port() | default }} - default is captured but not used."""
|
|
288
|
+
mocker.patch("sprout.utils.find_available_port", return_value=9000)
|
|
289
|
+
|
|
290
|
+
template = tmp_path / ".env.example"
|
|
291
|
+
template.write_text("PORT={{ auto_port() | 8080 }}")
|
|
292
|
+
|
|
293
|
+
# auto_port() always generates a port, so default is not used
|
|
294
|
+
result = parse_env_template(template)
|
|
295
|
+
assert result == "PORT=9000"
|
|
296
|
+
|
|
297
|
+
def test_parse_env_template_mixed_with_defaults(self, tmp_path, mocker):
|
|
298
|
+
"""Test parsing mixed placeholders with default values."""
|
|
299
|
+
mocker.patch("sprout.utils.find_available_port", return_value=8080)
|
|
300
|
+
mocker.patch.dict(os.environ, {"SET_VAR": "from_env"}, clear=False)
|
|
301
|
+
if "UNSET_VAR" in os.environ:
|
|
302
|
+
del os.environ["UNSET_VAR"]
|
|
303
|
+
|
|
304
|
+
template = tmp_path / ".env.example"
|
|
305
|
+
template.write_text(
|
|
306
|
+
"SET_VAR={{ SET_VAR | default1 }}\n"
|
|
307
|
+
"UNSET_VAR={{ UNSET_VAR | default2 }}\n"
|
|
308
|
+
"PORT={{ auto_port() | 3000 }}\n"
|
|
309
|
+
"BRANCH={{ branch() | develop }}\n"
|
|
310
|
+
"EMPTY={{ EMPTY | }}"
|
|
311
|
+
)
|
|
312
|
+
|
|
313
|
+
result = parse_env_template(template, branch_name=None)
|
|
314
|
+
assert result == ("SET_VAR=from_env\nUNSET_VAR=default2\nPORT=8080\nBRANCH=develop\nEMPTY=")
|
|
315
|
+
|
|
182
316
|
def test_parse_env_template_file_not_found(self, tmp_path):
|
|
183
317
|
"""Test error when template file doesn't exist."""
|
|
184
318
|
template = tmp_path / "nonexistent.env"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|