Skip to content

Commit 44e613e

Browse files
authored
DEVOPS-735 fix: sf cli not accessible in windows for clariti commands (#9)
* DEVOPS-735 refactor: replace subprocess calls with sfdx function for org checkout and alias setting * DEVOPS-735 refactor: replace subprocess calls with sfdx function in test_utils_clariti.py * DEVOPS-735 chore: improve stdout and stderr handling in checkout_org_from_pool and set_sf_alias functions * DEVOPS-735 chore: streamline stdout and stderr handling in checkout_org_from_pool and set_sf_alias functions
1 parent 77acbd9 commit 44e613e

2 files changed

Lines changed: 110 additions & 70 deletions

File tree

cumulusci/tests/test_utils_clariti.py

Lines changed: 42 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import io
12
import json
23
from types import SimpleNamespace
34

@@ -13,6 +14,14 @@
1314
)
1415

1516

17+
def _make_proc(*, stdout: str = "", stderr: str = "", returncode: int = 0):
18+
return SimpleNamespace(
19+
returncode=returncode,
20+
stdout_text=io.StringIO(stdout),
21+
stderr_text=io.StringIO(stderr),
22+
)
23+
24+
1625
def test_resolve_pool_id_prefers_explicit():
1726
assert resolve_pool_id("Pool42", None) == "Pool42"
1827

@@ -43,12 +52,15 @@ def test_checkout_org_from_pool_parses_username(monkeypatch):
4352
"poolId": "Pool42",
4453
}
4554

46-
def fake_run(command, check, text, capture_output, env):
47-
assert command[:4] == ["sf", "clariti", "org", "checkout"]
48-
assert "--json" in command
49-
return SimpleNamespace(returncode=0, stdout=json.dumps(payload), stderr="")
55+
def fake_sfdx(command, **kwargs):
56+
assert command == "clariti org checkout"
57+
args = kwargs["args"]
58+
assert args[0] == "--json"
59+
assert "--pool-id" in args and "Pool42" in args
60+
assert "--alias" in args and "MyAlias" in args
61+
return _make_proc(stdout=json.dumps(payload))
5062

51-
monkeypatch.setattr("cumulusci.utils.clariti.subprocess.run", fake_run)
63+
monkeypatch.setattr("cumulusci.utils.clariti.sfdx", fake_sfdx)
5264

5365
result = checkout_org_from_pool("Pool42", alias="MyAlias")
5466

@@ -68,11 +80,11 @@ def test_checkout_org_from_pool_reads_nested_username(monkeypatch):
6880
},
6981
}
7082

71-
def fake_run(command, check, text, capture_output, env):
72-
assert "--pool-id" in command
73-
return SimpleNamespace(returncode=0, stdout=json.dumps(payload), stderr="")
83+
def fake_sfdx(command, **kwargs):
84+
assert "--pool-id" in kwargs["args"]
85+
return _make_proc(stdout=json.dumps(payload))
7486

75-
monkeypatch.setattr("cumulusci.utils.clariti.subprocess.run", fake_run)
87+
monkeypatch.setattr("cumulusci.utils.clariti.sfdx", fake_sfdx)
7688

7789
result = checkout_org_from_pool("PoolNested")
7890

@@ -86,22 +98,22 @@ def test_checkout_org_from_pool_without_pool_id(monkeypatch):
8698
"poolId": "Pool-From-Config",
8799
}
88100

89-
def fake_run(command, check, text, capture_output, env):
90-
assert "--pool-id" not in command
91-
return SimpleNamespace(returncode=0, stdout=json.dumps(payload), stderr="")
101+
def fake_sfdx(command, **kwargs):
102+
assert "--pool-id" not in kwargs["args"]
103+
return _make_proc(stdout=json.dumps(payload))
92104

93-
monkeypatch.setattr("cumulusci.utils.clariti.subprocess.run", fake_run)
105+
monkeypatch.setattr("cumulusci.utils.clariti.sfdx", fake_sfdx)
94106

95107
result = checkout_org_from_pool(None)
96108

97109
assert result.username == "user@example.com"
98110

99111

100112
def test_checkout_org_from_pool_handles_failure(monkeypatch):
101-
def fake_run(command, check, text, capture_output, env):
102-
return SimpleNamespace(returncode=1, stdout="", stderr="No orgs available")
113+
def fake_sfdx(command, **kwargs):
114+
return _make_proc(stdout="", stderr="No orgs available", returncode=1)
103115

104-
monkeypatch.setattr("cumulusci.utils.clariti.subprocess.run", fake_run)
116+
monkeypatch.setattr("cumulusci.utils.clariti.sfdx", fake_sfdx)
105117

106118
with pytest.raises(ClaritiError) as exc:
107119
checkout_org_from_pool("EmptyPool")
@@ -117,12 +129,10 @@ def test_checkout_org_from_pool_formats_json_error(monkeypatch):
117129
"message": "Failed to get org from pool: No healthy orgs available in this pool",
118130
}
119131

120-
def fake_run(command, check, text, capture_output, env):
121-
return SimpleNamespace(
122-
returncode=1, stdout=json.dumps(payload), stderr=""
123-
)
132+
def fake_sfdx(command, **kwargs):
133+
return _make_proc(stdout=json.dumps(payload), stderr="", returncode=1)
124134

125-
monkeypatch.setattr("cumulusci.utils.clariti.subprocess.run", fake_run)
135+
monkeypatch.setattr("cumulusci.utils.clariti.sfdx", fake_sfdx)
126136

127137
with pytest.raises(ClaritiError) as exc:
128138
checkout_org_from_pool("Pool42")
@@ -139,12 +149,10 @@ def test_checkout_org_from_pool_formats_json_error_debug(monkeypatch):
139149
"message": "Failed", "extra": "details",
140150
}
141151

142-
def fake_run(command, check, text, capture_output, env):
143-
return SimpleNamespace(
144-
returncode=1, stdout=json.dumps(payload), stderr=""
145-
)
152+
def fake_sfdx(command, **kwargs):
153+
return _make_proc(stdout=json.dumps(payload), stderr="", returncode=1)
146154

147-
monkeypatch.setattr("cumulusci.utils.clariti.subprocess.run", fake_run)
155+
monkeypatch.setattr("cumulusci.utils.clariti.sfdx", fake_sfdx)
148156

149157
from cumulusci.core.debug import set_debug_mode
150158

@@ -159,11 +167,12 @@ def fake_run(command, check, text, capture_output, env):
159167

160168

161169
def test_set_sf_alias_success(monkeypatch):
162-
def fake_run(command, check, text, capture_output, env):
163-
assert command == ["sf", "alias", "set", "target=user@example.com"]
164-
return SimpleNamespace(returncode=0, stdout="", stderr="")
170+
def fake_sfdx(command, **kwargs):
171+
assert command == "alias set"
172+
assert kwargs["args"] == ["target=user@example.com"]
173+
return _make_proc()
165174

166-
monkeypatch.setattr("cumulusci.utils.clariti.subprocess.run", fake_run)
175+
monkeypatch.setattr("cumulusci.utils.clariti.sfdx", fake_sfdx)
167176

168177
success, message = set_sf_alias("target", "user@example.com")
169178

@@ -172,10 +181,10 @@ def fake_run(command, check, text, capture_output, env):
172181

173182

174183
def test_set_sf_alias_failure(monkeypatch):
175-
def fake_run(command, check, text, capture_output, env):
176-
return SimpleNamespace(returncode=1, stdout="", stderr="Alias failure")
184+
def fake_sfdx(command, **kwargs):
185+
return _make_proc(stdout="", stderr="Alias failure", returncode=1)
177186

178-
monkeypatch.setattr("cumulusci.utils.clariti.subprocess.run", fake_run)
187+
monkeypatch.setattr("cumulusci.utils.clariti.sfdx", fake_sfdx)
179188

180189
success, message = set_sf_alias("target", "username")
181190

cumulusci/utils/clariti.py

Lines changed: 68 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@
88
from pathlib import Path
99
from typing import Any, Dict, Optional, Sequence, Tuple, cast
1010

11-
import subprocess
11+
from cumulusci.core.sfdx import sfdx
1212

1313
from cumulusci.core.debug import get_debug_mode
1414

@@ -105,38 +105,33 @@ def checkout_org_from_pool(
105105
data.
106106
"""
107107

108-
command = [
109-
"sf",
110-
"clariti",
111-
"org",
112-
"checkout",
113-
"--json",
114-
]
108+
command_args = ["--json"]
115109
if pool_id:
116-
command.extend(["--pool-id", pool_id])
110+
command_args.extend(["--pool-id", pool_id])
117111
if alias:
118-
command.extend(["--alias", alias])
112+
command_args.extend(["--alias", alias])
119113

120114
try:
121-
proc = subprocess.run(
122-
command,
123-
check=False,
124-
text=True,
125-
capture_output=True,
115+
proc = sfdx(
116+
"clariti org checkout",
117+
args=command_args,
126118
env=env,
119+
capture_output=True,
120+
check_return=False,
127121
)
128122
except FileNotFoundError as err:
129123
raise ClaritiError("Salesforce CLI 'sf' was not found on PATH.") from err
124+
except OSError as err:
125+
raise ClaritiError("Failed to execute Salesforce CLI 'sf'.") from err
126+
127+
stdout = _read_process_stream(proc, "stdout_text", "stdout")
128+
stderr = _read_process_stream(proc, "stderr_text", "stderr")
130129

131-
stdout = (proc.stdout or "").strip()
132-
stderr = (proc.stderr or "").strip()
133130
if proc.returncode:
134131
summary, raw_output = _summarize_error_output(stdout, stderr, proc.returncode)
135132
summary = f"Clariti checkout failed: {summary}"
136133
if get_debug_mode():
137-
raise ClaritiError(
138-
f"{summary}\nClariti raw response:\n{raw_output}"
139-
)
134+
raise ClaritiError(f"{summary}\nClariti raw response:\n{raw_output}")
140135
raise ClaritiError(summary)
141136

142137
if not stdout:
@@ -152,18 +147,12 @@ def checkout_org_from_pool(
152147

153148
username = cast(str, _extract_string(payload, _USERNAME_PATHS))
154149
alias_value = _extract_string(payload, _ALIAS_PATHS, allow_missing=True)
155-
org_id_value = _extract_string(
156-
payload, (("orgId",),), allow_missing=True
157-
)
150+
org_id_value = _extract_string(payload, (("orgId",),), allow_missing=True)
158151
instance_url_value = _extract_string(
159152
payload, (("instanceUrl",),), allow_missing=True
160153
)
161-
org_type_value = _extract_string(
162-
payload, (("orgType",),), allow_missing=True
163-
)
164-
pool_id_value = _extract_string(
165-
payload, (("poolId",),), allow_missing=True
166-
)
154+
org_type_value = _extract_string(payload, (("orgType",),), allow_missing=True)
155+
pool_id_value = _extract_string(payload, (("poolId",),), allow_missing=True)
167156

168157
return ClaritiCheckoutResult(
169158
username=username,
@@ -190,20 +179,21 @@ def set_sf_alias(
190179
if not alias or not username:
191180
return False, "Alias and username are required."
192181

193-
command = ["sf", "alias", "set", f"{alias}={username}"]
194182
try:
195-
proc = subprocess.run(
196-
command,
197-
check=False,
198-
text=True,
199-
capture_output=True,
183+
proc = sfdx(
184+
"alias set",
185+
args=[f"{alias}={username}"],
200186
env=env,
187+
capture_output=True,
188+
check_return=False,
201189
)
202190
except FileNotFoundError:
203191
return False, "Salesforce CLI 'sf' was not found on PATH."
192+
except OSError:
193+
return False, "Failed to execute Salesforce CLI 'sf'."
204194

205-
stdout = (proc.stdout or "").strip()
206-
stderr = (proc.stderr or "").strip()
195+
stdout = _read_process_stream(proc, "stdout_text", "stdout")
196+
stderr = _read_process_stream(proc, "stderr_text", "stderr")
207197
if proc.returncode:
208198
summary, raw_output = _summarize_error_output(stdout, stderr, proc.returncode)
209199
summary = f"Failed to set SF alias: {summary}"
@@ -214,6 +204,47 @@ def set_sf_alias(
214204
return True, None
215205

216206

207+
def _read_process_stream(proc: Any, text_attr: str, raw_attr: str) -> str:
208+
"""Normalize subprocess output attributes into plain text."""
209+
210+
value = getattr(proc, text_attr, None)
211+
text = _coerce_stream_value(value)
212+
if not text:
213+
text = _coerce_stream_value(getattr(proc, raw_attr, None))
214+
return text.strip()
215+
216+
217+
def _coerce_stream_value(value: Any) -> str:
218+
"""Return a string representation from subprocess output containers."""
219+
220+
if value is None:
221+
return ""
222+
if isinstance(value, str):
223+
return value
224+
if isinstance(value, (bytes, bytearray)):
225+
return value.decode()
226+
getvalue = getattr(value, "getvalue", None)
227+
if callable(getvalue):
228+
result = getvalue()
229+
if isinstance(result, str):
230+
return result
231+
if isinstance(result, (bytes, bytearray)):
232+
return result.decode()
233+
return str(result)
234+
read = getattr(value, "read", None)
235+
if callable(read):
236+
position = value.tell() if hasattr(value, "tell") else None
237+
result = read()
238+
if position is not None and hasattr(value, "seek"):
239+
value.seek(position)
240+
if isinstance(result, str):
241+
return result
242+
if isinstance(result, (bytes, bytearray)):
243+
return result.decode()
244+
return str(result) if result is not None else ""
245+
return str(value)
246+
247+
217248
def _extract_string(
218249
payload: Dict[str, Any],
219250
paths: Sequence[Sequence[str]],

0 commit comments

Comments
 (0)