1use crate::client::bridge::escape_skill_string;
2use crate::version::VirtuosoVersion;
3
4pub struct MaestroOps;
5
6impl MaestroOps {
7 pub fn open_session(&self, lib: &str, cell: &str, view: &str) -> String {
9 let lib = escape_skill_string(lib);
10 let cell = escape_skill_string(cell);
11 let view = escape_skill_string(view);
12 format!(r#"maeOpenSetup("{lib}" "{cell}" "{view}")"#)
13 }
14
15 pub fn close_session(&self, session: &str) -> String {
17 let session = escape_skill_string(session);
18 format!(r#"maeCloseSession("{session}" ?forceClose t)"#)
19 }
20
21 pub fn list_sessions(&self) -> String {
22 skill_strings_to_json("maeGetSessions()")
23 }
24
25 pub fn set_var(&self, name: &str, value: &str) -> String {
26 let name = escape_skill_string(name);
27 let value = escape_skill_string(value);
28 format!(r#"maeSetVar("{name}" "{value}")"#)
29 }
30
31 pub fn get_var(&self, name: &str) -> String {
32 let name = escape_skill_string(name);
33 format!(r#"maeGetVar("{name}")"#)
34 }
35
36 pub fn list_vars(&self) -> String {
37 r#"let((vars out sep) vars = asiGetDesignVarList(asiGetCurrentSession()) out = "[" sep = "" foreach(v vars out = strcat(out sep sprintf(nil "{\"name\":\"%s\",\"value\":\"%s\"}" car(v) cadr(v))) sep = ",") strcat(out "]"))"#.into()
38 }
39
40 pub fn get_analyses(&self, session: &str) -> String {
42 let session = escape_skill_string(session);
43 skill_strings_to_json(&format!(
44 r#"let((setup) setup = car(maeGetSetup(?session "{session}")) maeGetEnabledAnalysis(setup))"#
45 ))
46 }
47
48 pub fn set_analysis(
55 &self,
56 session: &str,
57 analysis_type: &str,
58 options_skill_alist: Option<&str>,
59 version: VirtuosoVersion,
60 ) -> String {
61 let session = escape_skill_string(session);
62 let analysis_type = escape_skill_string(analysis_type);
63 if version.is_ic25() {
64 let options_part = match options_skill_alist {
65 Some(alist) => format!(" ?options `{alist}"),
66 None => String::new(),
67 };
68 format!(
69 r#"maeSetAnalysis("{analysis_type}" ?session "{session}" ?enable t{options_part})"#
70 )
71 } else {
72 format!(
74 r#"let((setup) setup = car(maeGetSetup(?session "{session}")) maeSetAnalysis(setup "{analysis_type}"))"#
75 )
76 }
77 }
78
79 pub fn run_simulation(&self, session: &str) -> String {
81 let session = escape_skill_string(session);
82 format!(r#"maeRunSimulation(?session "{session}")"#)
83 }
84
85 #[allow(dead_code)]
86 pub fn get_outputs(&self, test_name: &str) -> String {
88 let test_name = escape_skill_string(test_name);
89 format!(
90 r#"let((outs out sep) outs = maeGetTestOutputs("{test_name}") out = "[" sep = "" foreach(o outs out = strcat(out sep sprintf(nil "{{\"name\":\"%s\",\"test\":\"%s\",\"expr\":\"%s\"}}" car(o) cadr(o) if(caddr(o) then caddr(o) else ""))) sep = ",") strcat(out "]"))"#
91 )
92 }
93
94 pub fn add_output(&self, output_name: &str, test_name: &str, expr: &str) -> String {
95 let output_name = escape_skill_string(output_name);
96 let test_name = escape_skill_string(test_name);
97 let expr = escape_skill_string(expr);
98 format!(r#"maeAddOutput("{output_name}" "{test_name}" ?expr "{expr}")"#)
99 }
100
101 #[allow(dead_code)]
102 pub fn set_design(&self, session: &str, lib: &str, cell: &str, view: &str) -> String {
103 let session = escape_skill_string(session);
104 let lib = escape_skill_string(lib);
105 let cell = escape_skill_string(cell);
106 let view = escape_skill_string(view);
107 format!(
108 r#"maeSetDesign(?session "{session}" ?libName "{lib}" ?cellName "{cell}" ?viewName "{view}")"#
109 )
110 }
111
112 pub fn save_setup(&self, session: &str) -> String {
113 let session = escape_skill_string(session);
114 format!(r#"maeSaveSetup(?session "{session}")"#)
115 }
116
117 pub fn get_sim_messages(&self, session: &str) -> String {
118 let session = escape_skill_string(session);
119 format!(r#"maeGetSimulationMessages(?session "{session}")"#)
120 }
121
122 pub fn focused_window_skill(&self) -> String {
130 r#"let((cw sess) cw=hiGetCurrentWindow() sess=if(cw cw->davSession nil) list(if(cw hiGetWindowName(cw) nil) sess mapcar(lambda((w) hiGetWindowName(w)) hiGetWindowList()) maeGetSessions() if(sess let((s) s=asiGetSession(sess) if(s asiGetAnalogRunDir(s) nil)) nil)))"#.into()
131 }
132
133 pub fn run_dir_skill(&self, session: &str) -> String {
136 let session = escape_skill_string(session);
137 format!(
138 r#"let((sess) sess=asiGetSession("{session}") if(sess asiGetAnalogRunDir(sess) nil))"#
139 )
140 }
141
142 pub fn export_results(
144 &self,
145 session: &str,
146 file_path: &str,
147 test_name: Option<&str>,
148 history: Option<&str>,
149 ) -> String {
150 let session = escape_skill_string(session);
151 let file_path = escape_skill_string(file_path);
152 let test_name_part = match test_name {
153 Some(t) => format!(r#" ?testName "{}""#, escape_skill_string(t)),
154 None => String::new(),
155 };
156 let history_part = match history {
157 Some(h) => format!(r#" ?historyName "{}""#, escape_skill_string(h)),
158 None => String::new(),
159 };
160 format!(
161 r#"maeExportOutputView(?session "{session}"{test_name_part}{history_part} ?view "Detail" ?fileName "{file_path}")"#
162 )
163 }
164
165 pub fn open_results(&self, history: &str) -> String {
170 let history = escape_skill_string(history);
171 format!(r#"maeOpenResults(?history "{history}")"#)
172 }
173
174 pub fn close_results(&self) -> String {
175 r#"maeCloseResults()"#.into()
176 }
177
178 pub fn get_result_tests(&self) -> String {
180 skill_strings_to_json("maeGetResultTests()")
181 }
182
183 pub fn get_result_outputs(&self, test_name: &str) -> String {
184 let test_name = escape_skill_string(test_name);
185 skill_strings_to_json(&format!(r#"maeGetResultOutputs(?testName "{test_name}")"#))
186 }
187
188 pub fn get_output_value(&self, name: &str, test_name: &str, corner: Option<&str>) -> String {
189 let name = escape_skill_string(name);
190 let test_name = escape_skill_string(test_name);
191 match corner {
192 Some(c) => {
193 let c = escape_skill_string(c);
194 format!(r#"maeGetOutputValue("{name}" "{test_name}" ?cornerName "{c}")"#)
195 }
196 None => format!(r#"maeGetOutputValue("{name}" "{test_name}")"#),
197 }
198 }
199
200 pub fn get_spec_status(&self, name: &str, test_name: &str) -> String {
201 let name = escape_skill_string(name);
202 let test_name = escape_skill_string(test_name);
203 format!(r#"maeGetSpecStatus("{name}" "{test_name}")"#)
204 }
205
206 pub fn get_history_list(&self, session: &str) -> String {
210 let session = escape_skill_string(session);
211 skill_strings_to_json(&format!(r#"maeGetAllExplorerHistoryNames("{session}")"#))
212 }
213
214 #[allow(dead_code)]
215 pub fn get_current_session(&self) -> String {
216 r#"let((sess) sess = asiGetCurrentSession() if(sess then sess~>name else nil))"#.into()
217 }
218}
219
220fn skill_strings_to_json(list_expr: &str) -> String {
225 format!(
226 r#"let((xs out sep) xs = {list_expr} out = "[" sep = "" foreach(x xs out = strcat(out sep sprintf(nil "\"%s\"" x)) sep = ",") strcat(out "]"))"#
227 )
228}
229
230pub(crate) fn json_to_skill_alist(json_str: &str) -> Result<String, String> {
237 let parsed: serde_json::Value =
238 serde_json::from_str(json_str).map_err(|e| format!("invalid JSON: {e}"))?;
239 let obj = parsed
240 .as_object()
241 .ok_or_else(|| "expected a JSON object".to_string())?;
242 let pairs: Vec<String> = obj
243 .iter()
244 .map(|(k, v)| {
245 let binding = v.to_string();
246 let val = v.as_str().unwrap_or(&binding);
247 format!("(\"{k}\" \"{val}\")")
248 })
249 .collect();
250 Ok(format!("({})", pairs.join(" ")))
251}
252
253#[cfg(test)]
254mod tests {
255 use super::*;
256
257 fn ops() -> MaestroOps {
258 MaestroOps
259 }
260
261 #[test]
262 fn open_session_quoting() {
263 let s = ops().open_session("myLib", "myCell", "adexl");
264 assert_eq!(s, r#"maeOpenSetup("myLib" "myCell" "adexl")"#);
265 }
266
267 #[test]
268 fn open_session_escapes_quotes() {
269 let s = ops().open_session(r#"lib"x"#, "cell", "adexl");
270 assert!(s.contains(r#"lib\"x"#), "{s}");
271 }
272
273 #[test]
274 fn set_var_format() {
275 let s = ops().set_var("Vdd", "1.8");
276 assert_eq!(s, r#"maeSetVar("Vdd" "1.8")"#);
277 }
278
279 #[test]
280 fn run_simulation_includes_session() {
281 let s = ops().run_simulation("sess1");
282 assert!(s.contains("maeRunSimulation"), "{s}");
283 assert!(s.contains("\"sess1\""), "{s}");
284 }
285
286 #[test]
287 fn list_sessions_uses_helper() {
288 let s = ops().list_sessions();
289 assert!(s.contains("maeGetSessions()"), "{s}");
290 assert!(s.contains("foreach"), "{s}");
291 assert!(s.contains(r#"strcat(out "]")"#), "{s}");
292 }
293
294 #[test]
295 fn get_analyses_resolves_setup() {
296 let s = ops().get_analyses("sess1");
297 assert!(s.contains("maeGetSetup"), "must resolve setup: {s}");
298 assert!(s.contains("maeGetEnabledAnalysis"), "{s}");
299 assert!(s.contains("foreach"), "must produce JSON array: {s}");
300 }
301
302 #[test]
303 fn get_result_tests_uses_helper() {
304 let s = ops().get_result_tests();
305 assert!(s.contains("maeGetResultTests()"), "{s}");
306 assert!(s.contains("foreach"), "{s}");
307 }
308
309 #[test]
310 fn get_history_list_uses_helper() {
311 let s = ops().get_history_list("fnxSession0");
312 assert!(s.contains("maeGetAllExplorerHistoryNames"), "{s}");
313 assert!(s.contains("fnxSession0"), "{s}");
314 assert!(s.contains("foreach"), "{s}");
315 }
316
317 #[test]
318 fn export_results_minimal() {
319 let s = ops().export_results("sess1", "/tmp/out.csv", None, None);
320 assert!(s.contains("maeExportOutputView"), "{s}");
321 assert!(s.contains(r#"?session "sess1""#), "{s}");
322 assert!(s.contains(r#"?fileName "/tmp/out.csv""#), "{s}");
323 assert!(s.contains(r#"?view "Detail""#), "{s}");
324 assert!(!s.contains("?testName"), "should be absent when None: {s}");
325 assert!(
326 !s.contains("?historyName"),
327 "should be absent when None: {s}"
328 );
329 }
330
331 #[test]
332 fn export_results_with_all_params() {
333 let s = ops().export_results("sess1", "/tmp/out.csv", Some("AC"), Some("ExplorerRun.0"));
334 assert!(s.contains(r#"?testName "AC""#), "{s}");
335 assert!(s.contains(r#"?historyName "ExplorerRun.0""#), "{s}");
336 }
337
338 #[test]
339 fn set_analysis_ic23_positional() {
340 let s = ops().set_analysis("sess1", "ac", None, VirtuosoVersion::IC23);
341 assert!(s.contains("maeGetSetup"), "IC23 must resolve setup: {s}");
342 assert!(s.contains("maeSetAnalysis"), "{s}");
343 assert!(s.contains("\"ac\""), "{s}");
344 }
345
346 #[test]
347 fn set_analysis_ic23_no_options() {
348 let s = ops().set_analysis("sess1", "ac", None, VirtuosoVersion::IC23);
349 assert!(
350 !s.contains("?options"),
351 "IC23 path must not inject options: {s}"
352 );
353 }
354
355 #[test]
356 fn add_output_includes_expr() {
357 let s = ops().add_output("gain", "AC", "getData(\"vout\")");
358 assert!(s.contains("maeAddOutput"), "{s}");
359 assert!(s.contains("\"gain\""), "{s}");
360 assert!(s.contains("\"AC\""), "{s}");
361 }
362
363 #[test]
364 fn json_to_skill_alist_valid_input() {
365 let input = r#"{"start":"1","stop":"10G"}"#;
366 let out = json_to_skill_alist(input).unwrap();
367 assert!(out.contains("(\"start\" \"1\")"), "{out}");
368 assert!(out.contains("(\"stop\" \"10G\")"), "{out}");
369 }
370
371 #[test]
372 fn json_to_skill_alist_invalid_json_returns_err() {
373 assert!(json_to_skill_alist("not json").is_err());
374 }
375
376 #[test]
377 fn json_to_skill_alist_non_object_returns_err() {
378 assert!(json_to_skill_alist("[1,2,3]").is_err());
379 }
380}