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 {
28 let name = escape_skill_string(name);
29 let value = escape_skill_string(value);
30 format!(r#"maeSetVar("{name}" "{value}")"#)
31 }
32
33 pub fn get_var(&self, name: &str) -> String {
34 let name = escape_skill_string(name);
35 format!(r#"maeGetVar("{name}")"#)
36 }
37
38 pub fn list_vars(&self) -> String {
40 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()
41 }
42
43 pub fn get_analyses(&self, session: &str, version: VirtuosoVersion) -> String {
48 let session = escape_skill_string(session);
49 if version.is_ic25() {
50 format!(r#"maeGetEnabledAnalysis(?session "{session}")"#)
51 } else {
52 format!(
53 r#"let((setup) setup = car(maeGetSetup(?session "{session}")) maeGetEnabledAnalysis(setup))"#
54 )
55 }
56 }
57
58 pub fn set_analysis(
65 &self,
66 session: &str,
67 analysis_type: &str,
68 options_skill_alist: Option<&str>,
69 version: VirtuosoVersion,
70 ) -> String {
71 let session = escape_skill_string(session);
72 let analysis_type = escape_skill_string(analysis_type);
73 if version.is_ic25() {
74 let options_part = match options_skill_alist {
75 Some(alist) => format!(" ?options `{alist}"),
76 None => String::new(),
77 };
78 format!(
79 r#"maeSetAnalysis("{analysis_type}" ?session "{session}" ?enable t{options_part})"#
80 )
81 } else {
82 format!(
84 r#"let((setup) setup = car(maeGetSetup(?session "{session}")) maeSetAnalysis(setup "{analysis_type}"))"#
85 )
86 }
87 }
88
89 pub fn run_simulation(&self, session: &str) -> String {
91 let session = escape_skill_string(session);
92 format!(r#"maeRunSimulation(?session "{session}")"#)
93 }
94
95 #[allow(dead_code)]
100 pub fn get_outputs(&self, test_name: &str) -> String {
101 let test_name = escape_skill_string(test_name);
102 format!(
103 r#"let((outs out sep) outs = maeGetTestOutputs("{test_name}") out = "[" sep = "" foreach(o outs out = strcat(out sep sprintf(nil "{{\"name\":\"%s\",\"type\":\"%s\",\"signalName\":\"%s\",\"expr\":\"%s\"}}" o~>name o~>outputType o~>signalName o~>expr)) sep = ",") strcat(out "]"))"#
104 )
105 }
106
107 pub fn add_output(&self, output_name: &str, test_name: &str, expr: &str) -> String {
108 let output_name = escape_skill_string(output_name);
109 let test_name = escape_skill_string(test_name);
110 let expr = escape_skill_string(expr);
111 format!(r#"maeAddOutput("{output_name}" "{test_name}" ?expr "{expr}")"#)
112 }
113
114 #[allow(dead_code)]
115 pub fn set_design(&self, session: &str, lib: &str, cell: &str, view: &str) -> String {
116 let session = escape_skill_string(session);
117 let lib = escape_skill_string(lib);
118 let cell = escape_skill_string(cell);
119 let view = escape_skill_string(view);
120 format!(
121 r#"maeSetDesign(?session "{session}" ?libName "{lib}" ?cellName "{cell}" ?viewName "{view}")"#
122 )
123 }
124
125 pub fn save_setup(&self, session: &str) -> String {
126 let session = escape_skill_string(session);
127 format!(r#"maeSaveSetup(?session "{session}")"#)
128 }
129
130 pub fn get_sim_messages(&self, session: &str) -> String {
131 let session = escape_skill_string(session);
132 format!(r#"maeGetSimulationMessages(?session "{session}")"#)
133 }
134
135 pub fn focused_window_skill(&self) -> String {
143 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()
144 }
145
146 pub fn run_dir_skill(&self, session: &str) -> String {
149 let session = escape_skill_string(session);
150 format!(
151 r#"let((sess) sess=asiGetSession("{session}") if(sess asiGetAnalogRunDir(sess) nil))"#
152 )
153 }
154
155 pub fn export_results(
157 &self,
158 session: &str,
159 file_path: &str,
160 test_name: Option<&str>,
161 history: Option<&str>,
162 ) -> String {
163 let session = escape_skill_string(session);
164 let file_path = escape_skill_string(file_path);
165 let test_name_part = match test_name {
166 Some(t) => format!(r#" ?testName "{}""#, escape_skill_string(t)),
167 None => String::new(),
168 };
169 let history_part = match history {
170 Some(h) => format!(r#" ?historyName "{}""#, escape_skill_string(h)),
171 None => String::new(),
172 };
173 format!(
174 r#"maeExportOutputView(?session "{session}"{test_name_part}{history_part} ?view "Detail" ?fileName "{file_path}")"#
175 )
176 }
177
178 pub fn open_results(&self, history: &str) -> String {
184 let history = escape_skill_string(history);
185 format!(r#"maeOpenResults(?history "{history}")"#)
186 }
187
188 pub fn close_results(&self) -> String {
190 r#"maeCloseResults()"#.into()
191 }
192
193 pub fn get_result_tests(&self) -> String {
195 r#"let((tests out sep) tests = maeGetResultTests() out = "[" sep = "" foreach(t tests out = strcat(out sep sprintf(nil "\"%s\"" t)) sep = ",") strcat(out "]"))"#.into()
196 }
197
198 pub fn get_result_outputs(&self, test_name: &str) -> String {
200 let test_name = escape_skill_string(test_name);
201 format!(
202 r#"let((outs out sep) outs = maeGetResultOutputs(?testName "{test_name}") out = "[" sep = "" foreach(o outs out = strcat(out sep sprintf(nil "\"%s\"" o)) sep = ",") strcat(out "]"))"#
203 )
204 }
205
206 pub fn get_output_value(&self, name: &str, test_name: &str, corner: Option<&str>) -> String {
208 let name = escape_skill_string(name);
209 let test_name = escape_skill_string(test_name);
210 match corner {
211 Some(c) => {
212 let c = escape_skill_string(c);
213 format!(r#"maeGetOutputValue("{name}" "{test_name}" ?cornerName "{c}")"#)
214 }
215 None => format!(r#"maeGetOutputValue("{name}" "{test_name}")"#),
216 }
217 }
218
219 pub fn get_spec_status(&self, name: &str, test_name: &str) -> String {
221 let name = escape_skill_string(name);
222 let test_name = escape_skill_string(test_name);
223 format!(r#"maeGetSpecStatus("{name}" "{test_name}")"#)
224 }
225
226 pub fn get_history_list(&self) -> String {
229 r#"let((base histories out sep) base = getDirFiles(strcat(asiGetResultsDir(asiGetCurrentSession()) "/..")) histories = remove("maestro" remove("exprOutputs.log" base)) out = "[" sep = "" foreach(h histories when(h && !index(h ".") out = strcat(out sep sprintf(nil "\"%s\"" h)) sep = ",")) strcat(out "]"))"#.into()
230 }
231
232 #[allow(dead_code)]
234 pub fn get_current_session(&self) -> String {
235 r#"let((sess out) sess = asiGetCurrentSession() out = if(sess then sess~>name else "nil"))"#
236 .into()
237 }
238}
239
240fn skill_strings_to_json(list_expr: &str) -> String {
245 format!(
246 r#"let((xs out sep) xs = {list_expr} out = "[" sep = "" foreach(x xs out = strcat(out sep sprintf(nil "\"%s\"" x)) sep = ",") strcat(out "]"))"#
247 )
248}
249
250pub(crate) fn json_to_skill_alist(json_str: &str) -> Result<String, String> {
257 let parsed: serde_json::Value =
258 serde_json::from_str(json_str).map_err(|e| format!("invalid JSON: {e}"))?;
259 let obj = parsed
260 .as_object()
261 .ok_or_else(|| "expected a JSON object".to_string())?;
262 let pairs: Vec<String> = obj
263 .iter()
264 .map(|(k, v)| {
265 let binding = v.to_string();
266 let val = v.as_str().unwrap_or(&binding);
267 format!("(\"{k}\" \"{val}\")")
268 })
269 .collect();
270 Ok(format!("({})", pairs.join(" ")))
271}
272
273#[cfg(test)]
274mod tests {
275 use super::*;
276
277 fn ops() -> MaestroOps {
278 MaestroOps
279 }
280
281 #[test]
282 fn open_session_quoting() {
283 let s = ops().open_session("myLib", "myCell", "adexl");
284 assert_eq!(s, r#"maeOpenSetup("myLib" "myCell" "adexl")"#);
285 }
286
287 #[test]
288 fn open_session_escapes_quotes() {
289 let s = ops().open_session(r#"lib"x"#, "cell", "adexl");
290 assert!(s.contains(r#"lib\"x"#), "{s}");
291 }
292
293 #[test]
294 fn set_var_format() {
295 let s = ops().set_var("Vdd", "1.8");
296 assert_eq!(s, r#"maeSetVar("Vdd" "1.8")"#);
297 }
298
299 #[test]
300 fn run_simulation_includes_session() {
301 let s = ops().run_simulation("sess1");
302 assert!(s.contains("maeRunSimulation"), "{s}");
303 assert!(s.contains("\"sess1\""), "{s}");
304 }
305
306 #[test]
307 fn get_analyses_ic23_resolves_setup() {
308 let s = ops().get_analyses("sess1", VirtuosoVersion::IC23);
309 assert!(s.contains("maeGetSetup"), "IC23 must resolve setup: {s}");
310 assert!(s.contains("maeGetEnabledAnalysis"), "{s}");
311 }
312
313 #[test]
314 fn get_analyses_ic25_uses_ic23_path() {
315 let s = ops().get_analyses("sess1", VirtuosoVersion::IC25);
318 assert!(
319 s.contains("maeGetSetup"),
320 "IC25 currently uses IC23 path: {s}"
321 );
322 assert!(s.contains("maeGetEnabledAnalysis"), "{s}");
323 }
324
325 #[test]
326 fn list_sessions_uses_helper() {
327 let s = ops().list_sessions();
328 assert!(s.contains("maeGetSessions()"), "{s}");
329 assert!(s.contains("foreach"), "{s}");
330 assert!(s.contains(r#"strcat(out "]")"#), "{s}");
331 }
332
333 #[test]
334 fn get_result_tests_uses_helper() {
335 let s = ops().get_result_tests();
336 assert!(s.contains("maeGetResultTests()"), "{s}");
337 assert!(s.contains("foreach"), "{s}");
338 }
339
340 #[test]
341 fn get_history_list_uses_helper() {
342 let s = ops().get_history_list();
343 assert!(s.contains("asiGetResultsDir"), "{s}");
344 assert!(s.contains("foreach"), "{s}");
345 }
346
347 #[test]
348 fn export_results_minimal() {
349 let s = ops().export_results("sess1", "/tmp/out.csv", None, None);
350 assert!(s.contains("maeExportOutputView"), "{s}");
351 assert!(s.contains(r#"?session "sess1""#), "{s}");
352 assert!(s.contains(r#"?fileName "/tmp/out.csv""#), "{s}");
353 assert!(s.contains(r#"?view "Detail""#), "{s}");
354 assert!(!s.contains("?testName"), "should be absent when None: {s}");
355 assert!(
356 !s.contains("?historyName"),
357 "should be absent when None: {s}"
358 );
359 }
360
361 #[test]
362 fn export_results_with_all_params() {
363 let s = ops().export_results("sess1", "/tmp/out.csv", Some("AC"), Some("ExplorerRun.0"));
364 assert!(s.contains(r#"?testName "AC""#), "{s}");
365 assert!(s.contains(r#"?historyName "ExplorerRun.0""#), "{s}");
366 }
367
368 #[test]
369 fn set_analysis_ic23_positional() {
370 let s = ops().set_analysis("sess1", "ac", None, VirtuosoVersion::IC23);
371 assert!(s.contains("maeGetSetup"), "IC23 must resolve setup: {s}");
372 assert!(s.contains("maeSetAnalysis"), "{s}");
373 assert!(s.contains("\"ac\""), "{s}");
374 }
375
376 #[test]
377 fn set_analysis_ic23_no_options() {
378 let s = ops().set_analysis("sess1", "ac", None, VirtuosoVersion::IC23);
379 assert!(
380 !s.contains("?options"),
381 "IC23 path must not inject options: {s}"
382 );
383 }
384
385 #[test]
386 fn set_analysis_ic25_uses_ic23_path() {
387 let s = ops().set_analysis("sess1", "ac", None, VirtuosoVersion::IC25);
389 assert!(
390 s.contains("maeGetSetup"),
391 "IC25 currently uses IC23 path: {s}"
392 );
393 assert!(s.contains("maeSetAnalysis"), "{s}");
394 }
395
396 #[test]
397 fn add_output_includes_expr() {
398 let s = ops().add_output("gain", "AC", "getData(\"vout\")");
399 assert!(s.contains("maeAddOutput"), "{s}");
400 assert!(s.contains("\"gain\""), "{s}");
401 assert!(s.contains("\"AC\""), "{s}");
402 }
403
404 #[test]
405 fn json_to_skill_alist_valid_input() {
406 let input = r#"{"start":"1","stop":"10G"}"#;
407 let out = json_to_skill_alist(input).unwrap();
408 assert!(out.contains("(\"start\" \"1\")"), "{out}");
409 assert!(out.contains("(\"stop\" \"10G\")"), "{out}");
410 }
411
412 #[test]
413 fn json_to_skill_alist_invalid_json_returns_err() {
414 assert!(json_to_skill_alist("not json").is_err());
415 }
416
417 #[test]
418 fn json_to_skill_alist_non_object_returns_err() {
419 assert!(json_to_skill_alist("[1,2,3]").is_err());
420 }
421}