Skip to content

Commit 470c1e9

Browse files
committed
test(cli): cover resolve_extra_yaml env var and error paths
1 parent 9d650ac commit 470c1e9

2 files changed

Lines changed: 91 additions & 5 deletions

File tree

cumulusci/cli/extra_yaml.py

Lines changed: 18 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@
88
from typing import Optional, Tuple
99

1010
import click
11+
import yaml
1112

1213
from cumulusci.core.exceptions import CumulusCIUsageError
14+
from cumulusci.core.utils import dictmerge
1315

1416
ENV_VAR = "CUMULUSCI_EXTRA_YAML"
1517

@@ -23,8 +25,10 @@ def resolve_extra_yaml(paths: Tuple[str, ...]) -> Optional[str]:
2325
``CUMULUSCI_EXTRA_YAML`` (colon-separated paths).
2426
2527
Returns:
26-
Concatenated YAML content with ``\\n---\\n`` separators between files,
27-
or ``None`` if no paths were resolved.
28+
A single YAML document representing the deep-merge of all input files
29+
(later files override earlier files), or ``None`` if no paths were
30+
resolved. The returned string is a valid single-document YAML stream
31+
suitable for ``BaseProjectConfig(additional_yaml=...)``.
2832
2933
Raises:
3034
CumulusCIUsageError: If any listed path does not exist or is unreadable.
@@ -45,13 +49,22 @@ def resolve_extra_yaml(paths: Tuple[str, ...]) -> Optional[str]:
4549
err=True,
4650
)
4751

48-
contents = []
52+
merged: dict = {}
4953
for path in effective_paths:
5054
if not os.path.isfile(path):
5155
raise CumulusCIUsageError(f"--extra-yaml file not found: {path}")
5256
try:
5357
with open(path, "r", encoding="utf-8") as f:
54-
contents.append(f.read())
58+
raw = f.read()
5559
except OSError as e:
5660
raise CumulusCIUsageError(f"--extra-yaml could not read {path}: {e}")
57-
return "\n---\n".join(contents)
61+
try:
62+
parsed = yaml.safe_load(raw) or {}
63+
except yaml.YAMLError as e:
64+
raise CumulusCIUsageError(f"--extra-yaml could not parse {path}: {e}")
65+
if not isinstance(parsed, dict):
66+
raise CumulusCIUsageError(
67+
f"--extra-yaml expects a YAML mapping at the top level in {path}"
68+
)
69+
merged = dictmerge(merged, parsed)
70+
return yaml.safe_dump(merged, default_flow_style=False)

cumulusci/cli/tests/test_extra_yaml.py

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,7 @@
1+
import pytest
2+
13
from cumulusci.cli.extra_yaml import resolve_extra_yaml
4+
from cumulusci.core.exceptions import CumulusCIUsageError
25

36

47
def test_resolve_extra_yaml__none_when_no_input(monkeypatch):
@@ -14,3 +17,73 @@ def test_resolve_extra_yaml__single_file(tmp_path, monkeypatch):
1417
assert result is not None
1518
assert "from file" in result
1619
assert result.startswith("tasks:")
20+
21+
22+
def test_resolve_extra_yaml__missing_file_raises(tmp_path, monkeypatch):
23+
monkeypatch.delenv("CUMULUSCI_EXTRA_YAML", raising=False)
24+
with pytest.raises(CumulusCIUsageError, match="not found"):
25+
resolve_extra_yaml((str(tmp_path / "does_not_exist.yml"),))
26+
27+
28+
def test_resolve_extra_yaml__multiple_files_deep_merged(tmp_path, monkeypatch):
29+
"""Multiple files are deep-merged; last file wins on scalar conflicts."""
30+
import yaml
31+
32+
monkeypatch.delenv("CUMULUSCI_EXTRA_YAML", raising=False)
33+
a = tmp_path / "a.yml"
34+
a.write_text("tasks:\n foo:\n description: from A\n group: alpha\n")
35+
b = tmp_path / "b.yml"
36+
b.write_text("tasks:\n foo:\n description: from B\n")
37+
result = resolve_extra_yaml((str(a), str(b)))
38+
assert result is not None
39+
parsed = yaml.safe_load(result)
40+
# Later file's scalar wins.
41+
assert parsed["tasks"]["foo"]["description"] == "from B"
42+
# Sibling keys from earlier file are preserved (deep merge).
43+
assert parsed["tasks"]["foo"]["group"] == "alpha"
44+
45+
46+
def test_resolve_extra_yaml__env_var_fallback(tmp_path, monkeypatch):
47+
p = tmp_path / "env.yml"
48+
p.write_text("project:\n name: env-loaded\n")
49+
monkeypatch.setenv("CUMULUSCI_EXTRA_YAML", str(p))
50+
result = resolve_extra_yaml(())
51+
assert result is not None
52+
assert "env-loaded" in result
53+
54+
55+
def test_resolve_extra_yaml__env_var_multiple_colon_separated(tmp_path, monkeypatch):
56+
"""Env var with multiple paths produces a deep-merged document."""
57+
import yaml
58+
59+
a = tmp_path / "a.yml"
60+
a.write_text("tasks:\n a:\n description: from A\n")
61+
b = tmp_path / "b.yml"
62+
b.write_text("tasks:\n b:\n description: from B\n")
63+
monkeypatch.setenv("CUMULUSCI_EXTRA_YAML", f"{a}:{b}")
64+
result = resolve_extra_yaml(())
65+
assert result is not None
66+
parsed = yaml.safe_load(result)
67+
assert parsed["tasks"]["a"]["description"] == "from A"
68+
assert parsed["tasks"]["b"]["description"] == "from B"
69+
70+
71+
def test_resolve_extra_yaml__flag_overrides_env_var(tmp_path, monkeypatch):
72+
flag_file = tmp_path / "flag.yml"
73+
flag_file.write_text("tasks:\n from: flag\n")
74+
env_file = tmp_path / "env.yml"
75+
env_file.write_text("tasks:\n from: env\n")
76+
monkeypatch.setenv("CUMULUSCI_EXTRA_YAML", str(env_file))
77+
result = resolve_extra_yaml((str(flag_file),))
78+
assert result is not None
79+
assert "from: flag" in result
80+
assert "from: env" not in result
81+
82+
83+
def test_resolve_extra_yaml__empty_env_var_segments_ignored(tmp_path, monkeypatch):
84+
p = tmp_path / "x.yml"
85+
p.write_text("project: {}\n")
86+
monkeypatch.setenv("CUMULUSCI_EXTRA_YAML", f"::{p}::")
87+
result = resolve_extra_yaml(())
88+
assert result is not None
89+
assert "project: {}" in result

0 commit comments

Comments
 (0)