Skip to content

Commit 3cbe0e5

Browse files
robtaylorclaude
andcommitted
Add browser prompt after silicon submit
After a successful silicon submission, the user is now prompted to open the build URL in their browser. This applies whether using --wait or not. The prompt only appears in interactive terminals (when stdout.isatty() returns true). Changes: - Import webbrowser module in silicon_step.py - Add user prompt after successful submission asking if they want to open the build URL - Handle both 'y' and 'yes' responses - Gracefully handle KeyboardInterrupt and EOFError during prompt - Skip prompt in non-interactive terminals (CI, scripts, etc.) Tests: - Add comprehensive test suite for browser prompt functionality - Test 'yes' response opens browser - Test 'no' response doesn't open browser - Test non-TTY environments skip the prompt 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 43c32b9 commit 3cbe0e5

File tree

2 files changed

+191
-0
lines changed

2 files changed

+191
-0
lines changed

chipflow/platform/silicon_step.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
import subprocess
1212
import sys
1313
import urllib3
14+
import webbrowser
1415
from pprint import pformat
1516

1617

@@ -234,6 +235,17 @@ def network_err(e):
234235
exit_code = 0
235236
if args.wait:
236237
exit_code = self._stream_logs(sp, network_err)
238+
239+
# Ask user if they want to open the build URL
240+
if sys.stdout.isatty():
241+
try:
242+
response = input("Would you like to open the build URL in your browser? [y/N]: ").strip().lower()
243+
if response in ('y', 'yes'):
244+
webbrowser.open(self._build_url)
245+
print("Opening build URL in your browser...")
246+
except (EOFError, KeyboardInterrupt):
247+
print() # New line after interrupt
248+
237249
if fh:
238250
fh.close()
239251
exit(exit_code)

tests/test_silicon_submit.py

Lines changed: 179 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,179 @@
1+
# SPDX-License-Identifier: BSD-2-Clause
2+
3+
import unittest
4+
from unittest import mock
5+
from argparse import Namespace
6+
from pathlib import Path
7+
import tempfile
8+
import os
9+
10+
from chipflow.platform.silicon_step import SiliconStep
11+
12+
13+
class TestSiliconSubmitBrowserPrompt(unittest.TestCase):
14+
"""Test the browser prompt functionality in silicon submit"""
15+
16+
def setUp(self):
17+
"""Set up test environment"""
18+
self.temp_dir = tempfile.mkdtemp()
19+
self.test_dir = Path(__file__).parent
20+
self.fixtures_dir = self.test_dir / "fixtures"
21+
22+
# Set CHIPFLOW_ROOT to temporary directory
23+
os.environ["CHIPFLOW_ROOT"] = str(self.temp_dir)
24+
25+
def tearDown(self):
26+
"""Clean up test environment"""
27+
if hasattr(self, 'temp_dir') and os.path.exists(self.temp_dir):
28+
import shutil
29+
shutil.rmtree(self.temp_dir)
30+
31+
@mock.patch('chipflow.packaging.load_pinlock')
32+
@mock.patch('chipflow.platform.silicon_step.webbrowser.open')
33+
@mock.patch('builtins.input')
34+
@mock.patch('sys.stdout.isatty')
35+
@mock.patch('chipflow.platform.silicon_step.subprocess.check_output')
36+
def test_browser_prompt_yes(self, mock_subprocess, mock_isatty, mock_input, mock_webbrowser, mock_load_pinlock):
37+
"""Test that browser opens when user responds 'yes'"""
38+
mock_isatty.return_value = True
39+
mock_input.return_value = 'yes'
40+
mock_subprocess.return_value = 'test123\n'
41+
42+
# Mock pinlock
43+
mock_pinlock = mock.MagicMock()
44+
mock_pinlock.model_dump_json.return_value = '{}'
45+
mock_load_pinlock.return_value = mock_pinlock
46+
47+
# Create a mock SiliconStep instance
48+
with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'):
49+
config = mock.MagicMock()
50+
config.chipflow.silicon = True
51+
config.chipflow.project_name = 'test_project'
52+
step = SiliconStep(config)
53+
step._build_url = "https://build.chipflow.org/build/test123"
54+
step.platform._ports = {}
55+
56+
# Mock the submit method dependencies
57+
with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'):
58+
with mock.patch('builtins.open', mock.mock_open(read_data=b'')):
59+
with mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post:
60+
# Mock successful submission
61+
mock_response = mock.MagicMock()
62+
mock_response.status_code = 200
63+
mock_response.json.return_value = {'build_id': 'test123'}
64+
mock_post.return_value = mock_response
65+
66+
# Mock get_api_key
67+
with mock.patch('chipflow.platform.silicon_step.get_api_key', return_value='test_key'):
68+
# Mock exit to prevent test from exiting
69+
with mock.patch('chipflow.platform.silicon_step.exit') as mock_exit:
70+
args = Namespace(dry_run=False, wait=False)
71+
step._chipflow_api_key = 'test_key'
72+
73+
# Call submit with mocked dependencies
74+
step.submit('/tmp/test.il', args)
75+
76+
# Verify webbrowser.open was called
77+
mock_webbrowser.assert_called_once_with("https://build.chipflow.org/build/test123")
78+
mock_exit.assert_called_once_with(0)
79+
80+
@mock.patch('chipflow.packaging.load_pinlock')
81+
@mock.patch('chipflow.platform.silicon_step.webbrowser.open')
82+
@mock.patch('builtins.input')
83+
@mock.patch('sys.stdout.isatty')
84+
@mock.patch('chipflow.platform.silicon_step.subprocess.check_output')
85+
def test_browser_prompt_no(self, mock_subprocess, mock_isatty, mock_input, mock_webbrowser, mock_load_pinlock):
86+
"""Test that browser doesn't open when user responds 'no'"""
87+
mock_isatty.return_value = True
88+
mock_input.return_value = 'no'
89+
mock_subprocess.return_value = 'test123\n'
90+
91+
# Mock pinlock
92+
mock_pinlock = mock.MagicMock()
93+
mock_pinlock.model_dump_json.return_value = '{}'
94+
mock_load_pinlock.return_value = mock_pinlock
95+
96+
# Create a mock SiliconStep instance
97+
with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'):
98+
config = mock.MagicMock()
99+
config.chipflow.silicon = True
100+
config.chipflow.project_name = 'test_project'
101+
step = SiliconStep(config)
102+
step._build_url = "https://build.chipflow.org/build/test123"
103+
step.platform._ports = {}
104+
105+
# Mock the submit method dependencies
106+
with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'):
107+
with mock.patch('builtins.open', mock.mock_open(read_data=b'')):
108+
with mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post:
109+
# Mock successful submission
110+
mock_response = mock.MagicMock()
111+
mock_response.status_code = 200
112+
mock_response.json.return_value = {'build_id': 'test123'}
113+
mock_post.return_value = mock_response
114+
115+
# Mock get_api_key
116+
with mock.patch('chipflow.platform.silicon_step.get_api_key', return_value='test_key'):
117+
# Mock exit to prevent test from exiting
118+
with mock.patch('chipflow.platform.silicon_step.exit'):
119+
args = Namespace(dry_run=False, wait=False)
120+
step._chipflow_api_key = 'test_key'
121+
122+
# Call submit with mocked dependencies
123+
step.submit('/tmp/test.il', args)
124+
125+
# Verify webbrowser.open was NOT called
126+
mock_webbrowser.assert_not_called()
127+
128+
@mock.patch('chipflow.packaging.load_pinlock')
129+
@mock.patch('chipflow.platform.silicon_step.webbrowser.open')
130+
@mock.patch('builtins.input')
131+
@mock.patch('sys.stdout.isatty')
132+
@mock.patch('chipflow.platform.silicon_step.subprocess.check_output')
133+
def test_browser_prompt_not_tty(self, mock_subprocess, mock_isatty, mock_input, mock_webbrowser, mock_load_pinlock):
134+
"""Test that browser prompt is skipped when not in a TTY"""
135+
mock_isatty.return_value = False
136+
mock_subprocess.return_value = 'test123\n'
137+
138+
# Mock pinlock
139+
mock_pinlock = mock.MagicMock()
140+
mock_pinlock.model_dump_json.return_value = '{}'
141+
mock_load_pinlock.return_value = mock_pinlock
142+
143+
# Create a mock SiliconStep instance
144+
with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'):
145+
config = mock.MagicMock()
146+
config.chipflow.silicon = True
147+
config.chipflow.project_name = 'test_project'
148+
step = SiliconStep(config)
149+
step._build_url = "https://build.chipflow.org/build/test123"
150+
step.platform._ports = {}
151+
152+
# Mock the submit method dependencies
153+
with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'):
154+
with mock.patch('builtins.open', mock.mock_open(read_data=b'')):
155+
with mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post:
156+
# Mock successful submission
157+
mock_response = mock.MagicMock()
158+
mock_response.status_code = 200
159+
mock_response.json.return_value = {'build_id': 'test123'}
160+
mock_post.return_value = mock_response
161+
162+
# Mock get_api_key
163+
with mock.patch('chipflow.platform.silicon_step.get_api_key', return_value='test_key'):
164+
# Mock exit to prevent test from exiting
165+
with mock.patch('chipflow.platform.silicon_step.exit'):
166+
args = Namespace(dry_run=False, wait=False)
167+
step._chipflow_api_key = 'test_key'
168+
169+
# Call submit with mocked dependencies
170+
step.submit('/tmp/test.il', args)
171+
172+
# Verify input was NOT called (no prompt in non-TTY)
173+
mock_input.assert_not_called()
174+
# Verify webbrowser.open was NOT called
175+
mock_webbrowser.assert_not_called()
176+
177+
178+
if __name__ == "__main__":
179+
unittest.main()

0 commit comments

Comments
 (0)