Skip to content

Commit bdde47d

Browse files
committed
wip
1 parent 224f1e6 commit bdde47d

File tree

2 files changed

+314
-1
lines changed

2 files changed

+314
-1
lines changed

src/main.py

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -76,7 +76,16 @@ def run_command(cmd: str, debug: bool = False, capture: bool = True) -> str:
7676
print(f"STDERR:\n{result.stderr}")
7777

7878
if result.returncode != 0:
79-
raise Exception(result.stderr)
79+
# Build a comprehensive error message
80+
error_parts = []
81+
if result.stderr and result.stderr.strip():
82+
error_parts.append(result.stderr.strip())
83+
if result.stdout and result.stdout.strip():
84+
# Sometimes errors are in stdout
85+
error_parts.append(result.stdout.strip())
86+
87+
error_msg = "\n".join(error_parts) if error_parts else f"Command failed with exit code {result.returncode}"
88+
raise Exception(error_msg)
8089
return result.stdout
8190
else:
8291
# Stream output in real-time (better for CI/CD logs)

tests/test_run_command.py

Lines changed: 304 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,304 @@
1+
# SPDX-FileCopyrightText: 2025 Sequent Tech Inc <legal@sequentech.io>
2+
#
3+
# SPDX-License-Identifier: MIT
4+
5+
"""Tests for run_command function in release-bot."""
6+
7+
import pytest
8+
import sys
9+
from unittest.mock import patch, Mock
10+
from pathlib import Path
11+
import subprocess
12+
13+
# Add parent directory to path to import main module
14+
sys.path.insert(0, str(Path(__file__).parent.parent / "src"))
15+
from main import run_command
16+
17+
18+
class TestRunCommandSuccess:
19+
"""Test suite for successful run_command executions."""
20+
21+
def test_run_command_success_with_output(self):
22+
"""Test successful command with stdout output."""
23+
mock_result = Mock()
24+
mock_result.returncode = 0
25+
mock_result.stdout = "Command output\n"
26+
mock_result.stderr = ""
27+
28+
with patch('subprocess.run', return_value=mock_result):
29+
output = run_command("echo test", debug=False)
30+
31+
assert output == "Command output\n"
32+
33+
def test_run_command_success_empty_output(self):
34+
"""Test successful command with no output."""
35+
mock_result = Mock()
36+
mock_result.returncode = 0
37+
mock_result.stdout = ""
38+
mock_result.stderr = ""
39+
40+
with patch('subprocess.run', return_value=mock_result):
41+
output = run_command("true", debug=False)
42+
43+
assert output == ""
44+
45+
def test_run_command_success_with_stderr_warnings(self):
46+
"""Test successful command with warnings in stderr (exit code 0)."""
47+
mock_result = Mock()
48+
mock_result.returncode = 0
49+
mock_result.stdout = "Success\n"
50+
mock_result.stderr = "Warning: something minor\n"
51+
52+
with patch('subprocess.run', return_value=mock_result):
53+
output = run_command("some-tool", debug=False)
54+
55+
assert output == "Success\n"
56+
57+
58+
class TestRunCommandFailureCapture:
59+
"""Test suite for failed run_command executions with capture=True (default)."""
60+
61+
def test_run_command_failure_stderr_only(self):
62+
"""Test command failure with error message in stderr only."""
63+
mock_result = Mock()
64+
mock_result.returncode = 1
65+
mock_result.stdout = ""
66+
mock_result.stderr = "Error: file not found"
67+
68+
with patch('subprocess.run', return_value=mock_result):
69+
with pytest.raises(Exception) as exc_info:
70+
run_command("cat nonexistent.txt", debug=False)
71+
72+
assert "Error: file not found" in str(exc_info.value)
73+
74+
def test_run_command_failure_stdout_only(self):
75+
"""Test command failure with error message in stdout only."""
76+
mock_result = Mock()
77+
mock_result.returncode = 1
78+
mock_result.stdout = "Fatal: operation failed"
79+
mock_result.stderr = ""
80+
81+
with patch('subprocess.run', return_value=mock_result):
82+
with pytest.raises(Exception) as exc_info:
83+
run_command("git status", debug=False)
84+
85+
assert "Fatal: operation failed" in str(exc_info.value)
86+
87+
def test_run_command_failure_both_stderr_and_stdout(self):
88+
"""Test command failure with error messages in both stderr and stdout."""
89+
mock_result = Mock()
90+
mock_result.returncode = 1
91+
mock_result.stdout = "Additional context from stdout"
92+
mock_result.stderr = "Error from stderr"
93+
94+
with patch('subprocess.run', return_value=mock_result):
95+
with pytest.raises(Exception) as exc_info:
96+
run_command("complex-command", debug=False)
97+
98+
error_message = str(exc_info.value)
99+
assert "Error from stderr" in error_message
100+
assert "Additional context from stdout" in error_message
101+
102+
def test_run_command_failure_empty_output(self):
103+
"""Test command failure with no output (empty stderr and stdout)."""
104+
mock_result = Mock()
105+
mock_result.returncode = 127
106+
mock_result.stdout = ""
107+
mock_result.stderr = ""
108+
109+
with patch('subprocess.run', return_value=mock_result):
110+
with pytest.raises(Exception) as exc_info:
111+
run_command("nonexistent-command", debug=False)
112+
113+
# Should provide a fallback message with exit code
114+
assert "Command failed with exit code 127" in str(exc_info.value)
115+
116+
def test_run_command_failure_whitespace_only(self):
117+
"""Test command failure with only whitespace in output."""
118+
mock_result = Mock()
119+
mock_result.returncode = 1
120+
mock_result.stdout = " \n "
121+
mock_result.stderr = "\n\t "
122+
123+
with patch('subprocess.run', return_value=mock_result):
124+
with pytest.raises(Exception) as exc_info:
125+
run_command("failing-command", debug=False)
126+
127+
# Should provide a fallback message since whitespace is stripped
128+
assert "Command failed with exit code 1" in str(exc_info.value)
129+
130+
131+
class TestRunCommandFailureNoCapture:
132+
"""Test suite for failed run_command executions with capture=False."""
133+
134+
def test_run_command_no_capture_failure_stderr(self):
135+
"""Test command failure without capture, error in stderr."""
136+
mock_result = Mock()
137+
mock_result.returncode = 1
138+
mock_result.stdout = ""
139+
mock_result.stderr = "Deployment failed"
140+
141+
with patch('subprocess.run', return_value=mock_result):
142+
with pytest.raises(Exception) as exc_info:
143+
run_command("deploy", debug=False, capture=False)
144+
145+
assert "Deployment failed" in str(exc_info.value)
146+
147+
def test_run_command_no_capture_failure_both(self):
148+
"""Test command failure without capture, error in both streams."""
149+
mock_result = Mock()
150+
mock_result.returncode = 2
151+
mock_result.stdout = "Partial progress made"
152+
mock_result.stderr = "Then it crashed"
153+
154+
with patch('subprocess.run', return_value=mock_result):
155+
with pytest.raises(Exception) as exc_info:
156+
run_command("risky-operation", debug=False, capture=False)
157+
158+
error_message = str(exc_info.value)
159+
assert "Then it crashed" in error_message
160+
assert "Partial progress made" in error_message
161+
162+
def test_run_command_no_capture_failure_empty(self):
163+
"""Test command failure without capture, no output."""
164+
mock_result = Mock()
165+
mock_result.returncode = 1
166+
mock_result.stdout = ""
167+
mock_result.stderr = ""
168+
169+
with patch('subprocess.run', return_value=mock_result):
170+
with pytest.raises(Exception) as exc_info:
171+
run_command("silent-failure", debug=False, capture=False)
172+
173+
assert "Command failed with exit code 1" in str(exc_info.value)
174+
175+
176+
class TestRunCommandDebug:
177+
"""Test suite for run_command with debug mode enabled."""
178+
179+
def test_run_command_debug_success(self, capsys):
180+
"""Test debug output for successful command."""
181+
mock_result = Mock()
182+
mock_result.returncode = 0
183+
mock_result.stdout = "Success output"
184+
mock_result.stderr = "Debug info"
185+
186+
with patch('subprocess.run', return_value=mock_result):
187+
run_command("test-cmd", debug=True)
188+
189+
captured = capsys.readouterr()
190+
assert "Exit code: 0" in captured.out
191+
assert "STDOUT:\nSuccess output" in captured.out
192+
assert "STDERR:\nDebug info" in captured.out
193+
194+
def test_run_command_debug_failure(self, capsys):
195+
"""Test debug output for failed command."""
196+
mock_result = Mock()
197+
mock_result.returncode = 1
198+
mock_result.stdout = "Attempted operation"
199+
mock_result.stderr = "Error occurred"
200+
201+
with patch('subprocess.run', return_value=mock_result):
202+
with pytest.raises(Exception):
203+
run_command("failing-cmd", debug=True)
204+
205+
captured = capsys.readouterr()
206+
assert "Exit code: 1" in captured.out
207+
assert "STDOUT:\nAttempted operation" in captured.out
208+
assert "STDERR:\nError occurred" in captured.out
209+
210+
def test_run_command_debug_empty_streams(self, capsys):
211+
"""Test debug output when streams are empty."""
212+
mock_result = Mock()
213+
mock_result.returncode = 0
214+
mock_result.stdout = ""
215+
mock_result.stderr = ""
216+
217+
with patch('subprocess.run', return_value=mock_result):
218+
run_command("quiet-cmd", debug=True)
219+
220+
captured = capsys.readouterr()
221+
assert "Exit code: 0" in captured.out
222+
# Should not print STDOUT/STDERR labels when empty
223+
assert "STDOUT:" not in captured.out
224+
assert "STDERR:" not in captured.out
225+
226+
227+
class TestRunCommandIntegration:
228+
"""Integration-like tests with more realistic scenarios."""
229+
230+
def test_run_command_prints_command(self, capsys):
231+
"""Test that the command being run is printed."""
232+
mock_result = Mock()
233+
mock_result.returncode = 0
234+
mock_result.stdout = ""
235+
mock_result.stderr = ""
236+
237+
with patch('subprocess.run', return_value=mock_result):
238+
run_command("release-tool pull", debug=False)
239+
240+
captured = capsys.readouterr()
241+
assert "Running: release-tool pull" in captured.out
242+
243+
def test_run_command_multiline_error(self):
244+
"""Test command failure with multiline error message."""
245+
mock_result = Mock()
246+
mock_result.returncode = 1
247+
mock_result.stdout = ""
248+
mock_result.stderr = """Error: Multiple issues found
249+
- Issue 1: Missing configuration
250+
- Issue 2: Invalid credentials
251+
- Issue 3: Network timeout"""
252+
253+
with patch('subprocess.run', return_value=mock_result):
254+
with pytest.raises(Exception) as exc_info:
255+
run_command("complex-tool", debug=False)
256+
257+
error = str(exc_info.value)
258+
assert "Multiple issues found" in error
259+
assert "Missing configuration" in error
260+
assert "Invalid credentials" in error
261+
assert "Network timeout" in error
262+
263+
def test_run_command_preserves_newlines(self):
264+
"""Test that newlines in error messages are preserved."""
265+
mock_result = Mock()
266+
mock_result.returncode = 1
267+
mock_result.stdout = "Line 1\nLine 2\nLine 3"
268+
mock_result.stderr = "Error A\nError B"
269+
270+
with patch('subprocess.run', return_value=mock_result):
271+
with pytest.raises(Exception) as exc_info:
272+
run_command("some-tool", debug=False)
273+
274+
error = str(exc_info.value)
275+
# Both stderr and stdout should be present with newline separator
276+
assert "Error A\nError B" in error
277+
assert "Line 1\nLine 2\nLine 3" in error
278+
279+
def test_run_command_no_capture_success(self):
280+
"""Test successful command without capture."""
281+
mock_result = Mock()
282+
mock_result.returncode = 0
283+
mock_result.stdout = "Build successful"
284+
mock_result.stderr = ""
285+
286+
with patch('subprocess.run', return_value=mock_result):
287+
output = run_command("make build", debug=False, capture=False)
288+
289+
# capture=False should return empty string on success
290+
assert output == ""
291+
292+
def test_run_command_captures_exit_codes(self):
293+
"""Test that different exit codes are properly captured."""
294+
for exit_code in [1, 2, 127, 255]:
295+
mock_result = Mock()
296+
mock_result.returncode = exit_code
297+
mock_result.stdout = ""
298+
mock_result.stderr = ""
299+
300+
with patch('subprocess.run', return_value=mock_result):
301+
with pytest.raises(Exception) as exc_info:
302+
run_command("test", debug=False)
303+
304+
assert f"Command failed with exit code {exit_code}" in str(exc_info.value)

0 commit comments

Comments
 (0)