1+ # SPDX-License-Identifier: BSD-2-Clause
2+ import os
3+ import sys
4+ import unittest
5+ import argparse
6+ from unittest import mock
7+ import logging
8+
9+ from chipflow_lib import ChipFlowError
10+ from chipflow_lib .cli import run , UnexpectedError
11+
12+
13+ class MockCommand :
14+ """Mock command for testing CLI"""
15+ def build_cli_parser (self , parser ):
16+ parser .add_argument ("--option" , help = "Test option" )
17+ parser .add_argument ("action" , choices = ["valid" , "error" , "unexpected" ])
18+
19+ def run_cli (self , args ):
20+ if args .action == "error" :
21+ raise ChipFlowError ("Command error" )
22+ elif args .action == "unexpected" :
23+ raise ValueError ("Unexpected error" )
24+ # Valid action does nothing
25+
26+
27+ class TestCLI (unittest .TestCase ):
28+ @mock .patch ("chipflow_lib.cli._parse_config" )
29+ @mock .patch ("chipflow_lib.cli.PinCommand" )
30+ @mock .patch ("chipflow_lib.cli._get_cls_by_reference" )
31+ def test_run_success (self , mock_get_cls , mock_pin_command , mock_parse_config ):
32+ """Test CLI run with successful command execution"""
33+ # Setup mocks
34+ mock_config = {
35+ "chipflow" : {
36+ "steps" : {
37+ "test" : "test:MockStep"
38+ }
39+ }
40+ }
41+ mock_parse_config .return_value = mock_config
42+
43+ mock_pin_cmd = MockCommand ()
44+ mock_pin_command .return_value = mock_pin_cmd
45+
46+ mock_test_cmd = MockCommand ()
47+ mock_get_cls .return_value = lambda config : mock_test_cmd
48+
49+ # Capture stdout for assertion
50+ with mock .patch ("sys.stdout" ) as mock_stdout :
51+ # Run with valid action
52+ run (["test" , "valid" ])
53+
54+ # No error message should be printed
55+ mock_stdout .write .assert_not_called ()
56+
57+ @mock .patch ("chipflow_lib.cli._parse_config" )
58+ @mock .patch ("chipflow_lib.cli.PinCommand" )
59+ @mock .patch ("chipflow_lib.cli._get_cls_by_reference" )
60+ def test_run_command_error (self , mock_get_cls , mock_pin_command , mock_parse_config ):
61+ """Test CLI run with command raising ChipFlowError"""
62+ # Setup mocks
63+ mock_config = {
64+ "chipflow" : {
65+ "steps" : {
66+ "test" : "test:MockStep"
67+ }
68+ }
69+ }
70+ mock_parse_config .return_value = mock_config
71+
72+ mock_pin_cmd = MockCommand ()
73+ mock_pin_command .return_value = mock_pin_cmd
74+
75+ mock_test_cmd = MockCommand ()
76+ mock_get_cls .return_value = lambda config : mock_test_cmd
77+
78+ # Capture stdout for assertion
79+ with mock .patch ("builtins.print" ) as mock_print :
80+ # Run with error action
81+ run (["test" , "error" ])
82+
83+ # Error message should be printed
84+ mock_print .assert_called_once ()
85+ self .assertIn ("Error while executing `test error`" , mock_print .call_args [0 ][0 ])
86+
87+ @mock .patch ("chipflow_lib.cli._parse_config" )
88+ @mock .patch ("chipflow_lib.cli.PinCommand" )
89+ @mock .patch ("chipflow_lib.cli._get_cls_by_reference" )
90+ def test_run_unexpected_error (self , mock_get_cls , mock_pin_command , mock_parse_config ):
91+ """Test CLI run with command raising unexpected exception"""
92+ # Setup mocks
93+ mock_config = {
94+ "chipflow" : {
95+ "steps" : {
96+ "test" : "test:MockStep"
97+ }
98+ }
99+ }
100+ mock_parse_config .return_value = mock_config
101+
102+ mock_pin_cmd = MockCommand ()
103+ mock_pin_command .return_value = mock_pin_cmd
104+
105+ mock_test_cmd = MockCommand ()
106+ mock_get_cls .return_value = lambda config : mock_test_cmd
107+
108+ # Capture stdout for assertion
109+ with mock .patch ("builtins.print" ) as mock_print :
110+ # Run with unexpected error action
111+ run (["test" , "unexpected" ])
112+
113+ # Error message should be printed
114+ mock_print .assert_called_once ()
115+ self .assertIn ("Error while executing `test unexpected`" , mock_print .call_args [0 ][0 ])
116+ self .assertIn ("Unexpected error" , mock_print .call_args [0 ][0 ])
117+
118+ @mock .patch ("chipflow_lib.cli._parse_config" )
119+ @mock .patch ("chipflow_lib.cli.PinCommand" )
120+ def test_step_init_error (self , mock_pin_command , mock_parse_config ):
121+ """Test CLI run with error initializing step"""
122+ # Setup mocks
123+ mock_config = {
124+ "chipflow" : {
125+ "steps" : {
126+ "test" : "test:MockStep"
127+ }
128+ }
129+ }
130+ mock_parse_config .return_value = mock_config
131+
132+ mock_pin_cmd = MockCommand ()
133+ mock_pin_command .return_value = mock_pin_cmd
134+
135+ # Make _get_cls_by_reference raise an exception during step initialization
136+ with mock .patch ("chipflow_lib.cli._get_cls_by_reference" ) as mock_get_cls :
137+ mock_get_cls .return_value = mock .Mock (side_effect = Exception ("Init error" ))
138+
139+ with self .assertRaises (ChipFlowError ) as cm :
140+ run (["test" , "valid" ])
141+
142+ self .assertIn ("Encountered error while initializing step" , str (cm .exception ))
143+
144+ @mock .patch ("chipflow_lib.cli._parse_config" )
145+ @mock .patch ("chipflow_lib.cli.PinCommand" )
146+ @mock .patch ("chipflow_lib.cli._get_cls_by_reference" )
147+ def test_build_parser_error (self , mock_get_cls , mock_pin_command , mock_parse_config ):
148+ """Test CLI run with error building CLI parser"""
149+ # Setup mocks
150+ mock_config = {
151+ "chipflow" : {
152+ "steps" : {
153+ "test" : "test:MockStep"
154+ }
155+ }
156+ }
157+ mock_parse_config .return_value = mock_config
158+
159+ # Make pin command raise an error during build_cli_parser
160+ mock_pin_cmd = mock .Mock ()
161+ mock_pin_cmd .build_cli_parser .side_effect = Exception ("Parser error" )
162+ mock_pin_command .return_value = mock_pin_cmd
163+
164+ mock_test_cmd = mock .Mock ()
165+ mock_test_cmd .build_cli_parser .side_effect = Exception ("Parser error" )
166+ mock_get_cls .return_value = lambda config : mock_test_cmd
167+
168+ with self .assertRaises (ChipFlowError ) as cm :
169+ run (["pin" , "lock" ])
170+
171+ self .assertIn ("Encountered error while building CLI argument parser" , str (cm .exception ))
172+
173+ @mock .patch ("chipflow_lib.cli._parse_config" )
174+ @mock .patch ("chipflow_lib.cli.PinCommand" )
175+ @mock .patch ("chipflow_lib.cli._get_cls_by_reference" )
176+ def test_verbosity_flags (self , mock_get_cls , mock_pin_command , mock_parse_config ):
177+ """Test CLI verbosity flags"""
178+ # Setup mocks
179+ mock_config = {
180+ "chipflow" : {
181+ "steps" : {
182+ "test" : "test:MockStep"
183+ }
184+ }
185+ }
186+ mock_parse_config .return_value = mock_config
187+
188+ mock_pin_cmd = MockCommand ()
189+ mock_pin_command .return_value = mock_pin_cmd
190+
191+ mock_test_cmd = MockCommand ()
192+ mock_get_cls .return_value = lambda config : mock_test_cmd
193+
194+ # Save original log level
195+ original_level = logging .getLogger ().level
196+
197+ try :
198+ # Test with -v
199+ with mock .patch ("sys.stdout" ):
200+ run (["-v" , "test" , "valid" ])
201+ self .assertEqual (logging .getLogger ().level , logging .INFO )
202+
203+ # Reset log level
204+ logging .getLogger ().setLevel (original_level )
205+
206+ # Test with -v -v
207+ with mock .patch ("sys.stdout" ):
208+ run (["-v" , "-v" , "test" , "valid" ])
209+ self .assertEqual (logging .getLogger ().level , logging .DEBUG )
210+ finally :
211+ # Restore original log level
212+ logging .getLogger ().setLevel (original_level )
0 commit comments