@@ -16,7 +16,7 @@ impl Config {
1616 return Err ( "not enough arguments" ) ;
1717 }
1818
19- if args. len ( ) > 2 {
19+ if args. len ( ) > 2 && args [ 1 ] != "cascade" {
2020 return Err ( "too many arguments, expecting only 2, such as `touchstone filepath`" ) ;
2121 }
2222
@@ -30,7 +30,92 @@ impl Config {
3030 print_help ( ) ;
3131 process:: exit ( 0 ) ;
3232 }
33- _ => { }
33+ "cascade" => {
34+ // Parse arguments for --name or -n
35+ let mut output_name: Option < String > = None ;
36+ let mut file_paths = Vec :: new ( ) ;
37+
38+ let mut i = 2 ;
39+ while i < args. len ( ) {
40+ match args[ i] . as_str ( ) {
41+ "--name" | "-n" => {
42+ if i + 1 < args. len ( ) {
43+ output_name = Some ( args[ i + 1 ] . clone ( ) ) ;
44+ i += 2 ;
45+ } else {
46+ return Err ( "missing argument for --name" ) ;
47+ }
48+ }
49+ _ => {
50+ file_paths. push ( args[ i] . clone ( ) ) ;
51+ i += 1 ;
52+ }
53+ }
54+ }
55+
56+ if file_paths. len ( ) < 2 {
57+ return Err (
58+ "cascade requires at least 2 files, e.g. `touchstone cascade file1 file2`" ,
59+ ) ;
60+ }
61+
62+ let mut networks = Vec :: new ( ) ;
63+ for path in file_paths. iter ( ) {
64+ networks. push ( Network :: new ( path. clone ( ) ) ) ;
65+ }
66+
67+ let mut result = networks[ 0 ] . clone ( ) ;
68+ // Cascade remaining networks
69+ for network in networks. iter ( ) . skip ( 1 ) {
70+ result = result * network. clone ( ) ;
71+ }
72+
73+ // Determine output path
74+ let output_s2p_path = if let Some ( name) = output_name {
75+ // If name is provided, use it.
76+ // If it doesn't have an extension, add .s2p?
77+ // Let's assume user provides full filename or we just use it as is.
78+ // But we should probably ensure it ends in .s2p for consistency?
79+ // The prompt says "output s2p file", so let's trust the user or append if missing?
80+ // Let's just use it as is for now.
81+ name
82+ } else {
83+ // Default behavior: first file directory, "cascaded_result.html" (but we need s2p now)
84+ let first_file = & file_paths[ 0 ] ;
85+ let path = std:: path:: Path :: new ( first_file) ;
86+ let parent = path. parent ( ) . unwrap_or ( std:: path:: Path :: new ( "." ) ) ;
87+ parent
88+ . join ( "cascaded_result.s2p" )
89+ . to_string_lossy ( )
90+ . to_string ( )
91+ } ;
92+
93+ // Save S2P file
94+ if let Err ( e) = result. save ( & output_s2p_path) {
95+ eprintln ! ( "Failed to save S2P file: {}" , e) ;
96+ // Continue to plot generation? Or return error?
97+ // Let's return error.
98+ return Err ( "Failed to save S2P file" ) ;
99+ }
100+ println ! ( "Saved cascaded network to {}" , output_s2p_path) ;
101+
102+ // Generate plot
103+ // Plot should be named based on the output s2p file
104+ // e.g. output.s2p -> output.s2p.html
105+ let output_html_path = format ! ( "{}.html" , output_s2p_path) ;
106+
107+ generate_plot ( & result, output_html_path. clone ( ) ) ;
108+ open:: plot ( output_html_path) ;
109+
110+ return Ok ( Config { } ) ;
111+ }
112+ _ => {
113+ if args. len ( ) > 2 {
114+ return Err (
115+ "too many arguments, expecting only 2, such as `touchstone filepath`" ,
116+ ) ;
117+ }
118+ }
34119 }
35120
36121 // cargo run arg[1], such as cargo run files/ntwk1.s2p
@@ -159,15 +244,38 @@ mod tests {
159244 use std:: fs;
160245
161246 use super :: * ;
247+ use std:: path:: PathBuf ;
248+
249+ fn setup_test_dir ( name : & str ) -> PathBuf {
250+ let mut path = std:: env:: temp_dir ( ) ;
251+ path. push ( "touchstone_tests" ) ;
252+ path. push ( name) ;
253+ path. push ( format ! (
254+ "{}" ,
255+ std:: time:: SystemTime :: now( )
256+ . duration_since( std:: time:: UNIX_EPOCH )
257+ . unwrap( )
258+ . as_nanos( )
259+ ) ) ;
260+ std:: fs:: create_dir_all ( & path) . unwrap ( ) ;
261+ path
262+ }
263+
162264 #[ test]
163265 fn test_config_build ( ) {
266+ let test_dir = setup_test_dir ( "test_config_build" ) ;
267+ let s2p_path = test_dir. join ( "test_cli_config_build.s2p" ) ;
268+ fs:: copy ( "files/test_cli_config_build.s2p" , & s2p_path) . unwrap ( ) ;
269+
164270 let args = vec ! [
165271 String :: from( "program_name" ) ,
166- String :: from ( "files/test_cli_config_build.s2p" ) ,
272+ s2p_path . to_str ( ) . unwrap ( ) . to_string ( ) ,
167273 ] ;
168274 let _cli_run = Config :: run ( & args) . unwrap ( ) ;
169- let _remove_file = fs:: remove_file ( "files/test_cli_config_build.s2p.html" ) ;
170- let _remove_js_folder = fs:: remove_dir_all ( "files/js" ) ;
275+
276+ // Cleanup is optional as it's in temp dir, but good practice if we want to check it doesn't fail
277+ // let _remove_file = fs::remove_file(s2p_path.with_extension("s2p.html"));
278+ // let _remove_js_folder = fs::remove_dir_all(test_dir.join("js"));
171279 }
172280
173281 #[ test]
@@ -212,12 +320,28 @@ mod tests {
212320 #[ test]
213321 fn test_run_function ( ) {
214322 // test relative file
215- let relative_path = String :: from ( "files/test_cli_run_relative_path.s2p" ) ;
216- parse_plot_open_in_browser ( relative_path) ;
217- let _relative_remove_file = fs:: remove_file ( "files/test_cli_run_relative_path.s2p.html" ) ;
218- let _relative_remove_dir = fs:: remove_dir_all ( "files/js" ) ;
323+ let test_dir_rel = setup_test_dir ( "test_run_function_rel" ) ;
324+ // Create a "files" subdir to match the relative path structure expected if needed,
325+ // or just use the file in the temp dir.
326+ // The original test used "files/test_cli_run_relative_path.s2p".
327+ // parse_plot_open_in_browser handles relative paths.
328+
329+ let s2p_path_rel = test_dir_rel. join ( "test_cli_run_relative_path.s2p" ) ;
330+ fs:: copy ( "files/test_cli_run_relative_path.s2p" , & s2p_path_rel) . unwrap ( ) ;
331+
332+ parse_plot_open_in_browser ( s2p_path_rel. to_str ( ) . unwrap ( ) . to_string ( ) ) ;
333+ // Output should be next to it
334+ assert ! ( s2p_path_rel. with_extension( "s2p.html" ) . exists( ) ) ;
335+ assert ! ( test_dir_rel. join( "js" ) . exists( ) ) ;
219336
220337 // test bare filename
338+ // This MUST run in CWD because we pass a bare filename.
339+ // We can't easily isolate this without changing CWD.
340+ // But since we moved other tests out of "files/", this test (using root) shouldn't conflict
341+ // with them, UNLESS another test uses root.
342+ // The only other test using root is this one.
343+ // So we keep it as is, but maybe add a lock if we add more root tests.
344+
221345 let _bare_filename_copy = fs:: copy (
222346 "files/test_cli_run_bare_filename.s2p" ,
223347 "test_cli_run_bare_filename.s2p" ,
@@ -229,13 +353,70 @@ mod tests {
229353 fs:: remove_file ( "test_cli_run_bare_filename.s2p.html" ) ;
230354 let _bare_filename_remove_dir = fs:: remove_dir_all ( "js" ) ;
231355
232- // This fails if "files/ntwk1.s2p" is missing on disk
233- let path_buf = std:: fs:: canonicalize ( "files/test_cli_run_absolute_path.s2p" ) . unwrap ( ) ;
356+ // test absolute path
357+ let test_dir_abs = setup_test_dir ( "test_run_function_abs" ) ;
358+ let s2p_path_abs = test_dir_abs. join ( "test_cli_run_absolute_path.s2p" ) ;
359+ fs:: copy ( "files/test_cli_run_absolute_path.s2p" , & s2p_path_abs) . unwrap ( ) ;
360+
361+ let path_buf = std:: fs:: canonicalize ( & s2p_path_abs) . unwrap ( ) ;
234362 let absolute_path: String = path_buf. to_string_lossy ( ) . to_string ( ) ;
363+
235364 parse_plot_open_in_browser ( absolute_path) ;
236- // don't remove s2p file in files/
237- let _absolute_path_remove_file_html =
238- fs:: remove_file ( "files/test_cli_run_absolute_path.s2p.html" ) ;
239- let _absolute_path_remove_dir = fs:: remove_dir_all ( "files/js" ) ;
365+
366+ assert ! ( s2p_path_abs. with_extension( "s2p.html" ) . exists( ) ) ;
367+ assert ! ( test_dir_abs. join( "js" ) . exists( ) ) ;
368+ }
369+
370+ #[ test]
371+ fn test_cascade_command ( ) {
372+ let test_dir = setup_test_dir ( "test_cascade_command" ) ;
373+ let s2p1 = test_dir. join ( "ntwk1.s2p" ) ;
374+ let s2p2 = test_dir. join ( "ntwk2.s2p" ) ;
375+
376+ fs:: copy ( "files/ntwk1.s2p" , & s2p1) . unwrap ( ) ;
377+ fs:: copy ( "files/ntwk2.s2p" , & s2p2) . unwrap ( ) ;
378+
379+ // Test default output
380+ let args = vec ! [
381+ String :: from( "program_name" ) ,
382+ String :: from( "cascade" ) ,
383+ s2p1. to_str( ) . unwrap( ) . to_string( ) ,
384+ s2p2. to_str( ) . unwrap( ) . to_string( ) ,
385+ ] ;
386+
387+ let _cli_run = Config :: run ( & args) . unwrap ( ) ;
388+
389+ let expected_output_s2p = test_dir. join ( "cascaded_result.s2p" ) ;
390+ let expected_output_html = test_dir. join ( "cascaded_result.s2p.html" ) ;
391+ assert ! ( expected_output_s2p. exists( ) ) ;
392+ assert ! ( expected_output_html. exists( ) ) ;
393+ assert ! ( test_dir. join( "js" ) . exists( ) ) ;
394+
395+ // Test with --name
396+ let output_name = test_dir. join ( "custom_output.s2p" ) ;
397+ let args_named = vec ! [
398+ String :: from( "program_name" ) ,
399+ String :: from( "cascade" ) ,
400+ s2p1. to_str( ) . unwrap( ) . to_string( ) ,
401+ s2p2. to_str( ) . unwrap( ) . to_string( ) ,
402+ String :: from( "--name" ) ,
403+ output_name. to_str( ) . unwrap( ) . to_string( ) ,
404+ ] ;
405+
406+ let _cli_run_named = Config :: run ( & args_named) . unwrap ( ) ;
407+
408+ assert ! ( output_name. exists( ) ) ;
409+ assert ! ( output_name. with_extension( "s2p.html" ) . exists( ) ) ;
410+ }
411+
412+ #[ test]
413+ fn test_cascade_not_enough_args ( ) {
414+ let args = vec ! [
415+ String :: from( "program_name" ) ,
416+ String :: from( "cascade" ) ,
417+ String :: from( "file1" ) ,
418+ ] ;
419+ let result = Config :: run ( & args) ;
420+ assert ! ( result. is_err( ) ) ;
240421 }
241422}
0 commit comments