Skip to content

Commit a2b38a8

Browse files
authored
Merge pull request #31 from iancleary/feat/abcd-matrices
Feat/abcd matrices
2 parents f4d12fb + 04f2545 commit a2b38a8

File tree

6 files changed

+706
-41
lines changed

6 files changed

+706
-41
lines changed

Cargo.lock

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ homepage = "https://github.com/iancleary/touchstone"
77
license = "MIT"
88
name = "touchstone"
99
repository = "https://github.com/iancleary/touchstone"
10-
version = "0.8.5"
10+
version = "0.9.0"
1111

1212
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
1313

src/cli.rs

Lines changed: 196 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -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

Comments
 (0)