From 3cbe0e522eb579dd71a6721425aaefc52638846c Mon Sep 17 00:00:00 2001 From: Rob Taylor Date: Thu, 13 Nov 2025 16:05:39 +0000 Subject: [PATCH] Add browser prompt after silicon submit MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- chipflow/platform/silicon_step.py | 12 ++ tests/test_silicon_submit.py | 179 ++++++++++++++++++++++++++++++ 2 files changed, 191 insertions(+) create mode 100644 tests/test_silicon_submit.py diff --git a/chipflow/platform/silicon_step.py b/chipflow/platform/silicon_step.py index 252640d7..02eee447 100644 --- a/chipflow/platform/silicon_step.py +++ b/chipflow/platform/silicon_step.py @@ -11,6 +11,7 @@ import subprocess import sys import urllib3 +import webbrowser from pprint import pformat @@ -234,6 +235,17 @@ def network_err(e): exit_code = 0 if args.wait: exit_code = self._stream_logs(sp, network_err) + + # Ask user if they want to open the build URL + if sys.stdout.isatty(): + try: + response = input("Would you like to open the build URL in your browser? [y/N]: ").strip().lower() + if response in ('y', 'yes'): + webbrowser.open(self._build_url) + print("Opening build URL in your browser...") + except (EOFError, KeyboardInterrupt): + print() # New line after interrupt + if fh: fh.close() exit(exit_code) diff --git a/tests/test_silicon_submit.py b/tests/test_silicon_submit.py new file mode 100644 index 00000000..d074c405 --- /dev/null +++ b/tests/test_silicon_submit.py @@ -0,0 +1,179 @@ +# SPDX-License-Identifier: BSD-2-Clause + +import unittest +from unittest import mock +from argparse import Namespace +from pathlib import Path +import tempfile +import os + +from chipflow.platform.silicon_step import SiliconStep + + +class TestSiliconSubmitBrowserPrompt(unittest.TestCase): + """Test the browser prompt functionality in silicon submit""" + + def setUp(self): + """Set up test environment""" + self.temp_dir = tempfile.mkdtemp() + self.test_dir = Path(__file__).parent + self.fixtures_dir = self.test_dir / "fixtures" + + # Set CHIPFLOW_ROOT to temporary directory + os.environ["CHIPFLOW_ROOT"] = str(self.temp_dir) + + def tearDown(self): + """Clean up test environment""" + if hasattr(self, 'temp_dir') and os.path.exists(self.temp_dir): + import shutil + shutil.rmtree(self.temp_dir) + + @mock.patch('chipflow.packaging.load_pinlock') + @mock.patch('chipflow.platform.silicon_step.webbrowser.open') + @mock.patch('builtins.input') + @mock.patch('sys.stdout.isatty') + @mock.patch('chipflow.platform.silicon_step.subprocess.check_output') + def test_browser_prompt_yes(self, mock_subprocess, mock_isatty, mock_input, mock_webbrowser, mock_load_pinlock): + """Test that browser opens when user responds 'yes'""" + mock_isatty.return_value = True + mock_input.return_value = 'yes' + mock_subprocess.return_value = 'test123\n' + + # Mock pinlock + mock_pinlock = mock.MagicMock() + mock_pinlock.model_dump_json.return_value = '{}' + mock_load_pinlock.return_value = mock_pinlock + + # Create a mock SiliconStep instance + with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'): + config = mock.MagicMock() + config.chipflow.silicon = True + config.chipflow.project_name = 'test_project' + step = SiliconStep(config) + step._build_url = "https://build.chipflow.org/build/test123" + step.platform._ports = {} + + # Mock the submit method dependencies + with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'): + with mock.patch('builtins.open', mock.mock_open(read_data=b'')): + with mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post: + # Mock successful submission + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {'build_id': 'test123'} + mock_post.return_value = mock_response + + # Mock get_api_key + with mock.patch('chipflow.platform.silicon_step.get_api_key', return_value='test_key'): + # Mock exit to prevent test from exiting + with mock.patch('chipflow.platform.silicon_step.exit') as mock_exit: + args = Namespace(dry_run=False, wait=False) + step._chipflow_api_key = 'test_key' + + # Call submit with mocked dependencies + step.submit('/tmp/test.il', args) + + # Verify webbrowser.open was called + mock_webbrowser.assert_called_once_with("https://build.chipflow.org/build/test123") + mock_exit.assert_called_once_with(0) + + @mock.patch('chipflow.packaging.load_pinlock') + @mock.patch('chipflow.platform.silicon_step.webbrowser.open') + @mock.patch('builtins.input') + @mock.patch('sys.stdout.isatty') + @mock.patch('chipflow.platform.silicon_step.subprocess.check_output') + def test_browser_prompt_no(self, mock_subprocess, mock_isatty, mock_input, mock_webbrowser, mock_load_pinlock): + """Test that browser doesn't open when user responds 'no'""" + mock_isatty.return_value = True + mock_input.return_value = 'no' + mock_subprocess.return_value = 'test123\n' + + # Mock pinlock + mock_pinlock = mock.MagicMock() + mock_pinlock.model_dump_json.return_value = '{}' + mock_load_pinlock.return_value = mock_pinlock + + # Create a mock SiliconStep instance + with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'): + config = mock.MagicMock() + config.chipflow.silicon = True + config.chipflow.project_name = 'test_project' + step = SiliconStep(config) + step._build_url = "https://build.chipflow.org/build/test123" + step.platform._ports = {} + + # Mock the submit method dependencies + with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'): + with mock.patch('builtins.open', mock.mock_open(read_data=b'')): + with mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post: + # Mock successful submission + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {'build_id': 'test123'} + mock_post.return_value = mock_response + + # Mock get_api_key + with mock.patch('chipflow.platform.silicon_step.get_api_key', return_value='test_key'): + # Mock exit to prevent test from exiting + with mock.patch('chipflow.platform.silicon_step.exit'): + args = Namespace(dry_run=False, wait=False) + step._chipflow_api_key = 'test_key' + + # Call submit with mocked dependencies + step.submit('/tmp/test.il', args) + + # Verify webbrowser.open was NOT called + mock_webbrowser.assert_not_called() + + @mock.patch('chipflow.packaging.load_pinlock') + @mock.patch('chipflow.platform.silicon_step.webbrowser.open') + @mock.patch('builtins.input') + @mock.patch('sys.stdout.isatty') + @mock.patch('chipflow.platform.silicon_step.subprocess.check_output') + def test_browser_prompt_not_tty(self, mock_subprocess, mock_isatty, mock_input, mock_webbrowser, mock_load_pinlock): + """Test that browser prompt is skipped when not in a TTY""" + mock_isatty.return_value = False + mock_subprocess.return_value = 'test123\n' + + # Mock pinlock + mock_pinlock = mock.MagicMock() + mock_pinlock.model_dump_json.return_value = '{}' + mock_load_pinlock.return_value = mock_pinlock + + # Create a mock SiliconStep instance + with mock.patch('chipflow.platform.silicon_step.SiliconPlatform'): + config = mock.MagicMock() + config.chipflow.silicon = True + config.chipflow.project_name = 'test_project' + step = SiliconStep(config) + step._build_url = "https://build.chipflow.org/build/test123" + step.platform._ports = {} + + # Mock the submit method dependencies + with mock.patch.object(step, 'prepare', return_value='/tmp/test.il'): + with mock.patch('builtins.open', mock.mock_open(read_data=b'')): + with mock.patch('chipflow.platform.silicon_step.requests.post') as mock_post: + # Mock successful submission + mock_response = mock.MagicMock() + mock_response.status_code = 200 + mock_response.json.return_value = {'build_id': 'test123'} + mock_post.return_value = mock_response + + # Mock get_api_key + with mock.patch('chipflow.platform.silicon_step.get_api_key', return_value='test_key'): + # Mock exit to prevent test from exiting + with mock.patch('chipflow.platform.silicon_step.exit'): + args = Namespace(dry_run=False, wait=False) + step._chipflow_api_key = 'test_key' + + # Call submit with mocked dependencies + step.submit('/tmp/test.il', args) + + # Verify input was NOT called (no prompt in non-TTY) + mock_input.assert_not_called() + # Verify webbrowser.open was NOT called + mock_webbrowser.assert_not_called() + + +if __name__ == "__main__": + unittest.main()