1use std::collections::HashMap;
6
7use super::types::{
8 BashAnalysisCache, BashArgParser, BashBackend, BashCliOption, BashCondition,
9 BashConstantFoldingHelper, BashDepGraph, BashDominatorTree, BashExpr, BashFunction,
10 BashHereDoc, BashHeredoc, BashJobManager, BashLivenessInfo, BashLogLevel, BashLogger,
11 BashPassConfig, BashPassPhase, BashPassRegistry, BashPassStats, BashScript, BashTemplate,
12 BashTrap, BashVar, BashWorklist,
13};
14
15#[cfg(test)]
16mod tests {
17 use super::*;
18 pub(super) fn backend() -> BashBackend {
19 BashBackend::new()
20 }
21 #[test]
22 pub(super) fn test_bash_var_display_local() {
23 let v = BashVar::Local("count".to_string());
24 assert_eq!(format!("{}", v), "${count}");
25 }
26 #[test]
27 pub(super) fn test_bash_var_display_env() {
28 let v = BashVar::Env("HOME".to_string());
29 assert_eq!(format!("{}", v), "${HOME}");
30 }
31 #[test]
32 pub(super) fn test_bash_var_name() {
33 let v = BashVar::AssocArray("my_map".to_string());
34 assert_eq!(v.name(), "my_map");
35 }
36 #[test]
37 pub(super) fn test_bash_var_is_exported() {
38 assert!(BashVar::Env("X".to_string()).is_exported());
39 assert!(!BashVar::Local("X".to_string()).is_exported());
40 assert!(!BashVar::Global("X".to_string()).is_exported());
41 }
42 #[test]
43 pub(super) fn test_bash_var_is_readonly() {
44 assert!(BashVar::Readonly("X".to_string()).is_readonly());
45 assert!(!BashVar::Local("X".to_string()).is_readonly());
46 }
47 #[test]
48 pub(super) fn test_bash_var_decl_local() {
49 let b = backend();
50 let v = BashVar::Local("x".to_string());
51 assert_eq!(b.emit_var_decl(&v, Some("0")), "local x=0");
52 assert_eq!(b.emit_var_decl(&v, None), "local x");
53 }
54 #[test]
55 pub(super) fn test_bash_var_decl_export() {
56 let b = backend();
57 let v = BashVar::Env("PATH".to_string());
58 let result = b.emit_var_decl(&v, Some("/usr/bin"));
59 assert_eq!(result, "export PATH=/usr/bin");
60 }
61 #[test]
62 pub(super) fn test_bash_var_decl_integer() {
63 let b = backend();
64 let v = BashVar::Integer("n".to_string());
65 assert_eq!(b.emit_var_decl(&v, Some("42")), "declare -i n=42");
66 }
67 #[test]
68 pub(super) fn test_bash_var_decl_assoc_array() {
69 let b = backend();
70 let v = BashVar::AssocArray("opts".to_string());
71 assert_eq!(b.emit_var_decl(&v, None), "declare -A opts");
72 }
73 #[test]
74 pub(super) fn test_bash_var_decl_nameref() {
75 let b = backend();
76 let v = BashVar::NameRef("ref".to_string());
77 assert_eq!(b.emit_var_decl(&v, Some("target")), "declare -n ref=target");
78 }
79 #[test]
80 pub(super) fn test_bash_expr_var() {
81 let b = backend();
82 let expr = BashExpr::Var(BashVar::Local("x".to_string()));
83 assert_eq!(b.emit_expr(&expr), "${x}");
84 }
85 #[test]
86 pub(super) fn test_bash_expr_lit() {
87 let b = backend();
88 let expr = BashExpr::Lit("hello world".to_string());
89 assert_eq!(b.emit_expr(&expr), "'hello world'");
90 }
91 #[test]
92 pub(super) fn test_bash_expr_dquoted() {
93 let b = backend();
94 let expr = BashExpr::DQuoted("hello $name".to_string());
95 assert_eq!(b.emit_expr(&expr), "\"hello $name\"");
96 }
97 #[test]
98 pub(super) fn test_bash_expr_cmd_subst() {
99 let b = backend();
100 let expr = BashExpr::CmdSubst("date +%s".to_string());
101 assert_eq!(b.emit_expr(&expr), "$(date +%s)");
102 }
103 #[test]
104 pub(super) fn test_bash_expr_arith() {
105 let b = backend();
106 let expr = BashExpr::ArithExpr("x + y * 2".to_string());
107 assert_eq!(b.emit_expr(&expr), "$((x + y * 2))");
108 }
109 #[test]
110 pub(super) fn test_bash_expr_array_elem() {
111 let b = backend();
112 let expr = BashExpr::ArrayElem("arr".to_string(), Box::new(BashExpr::Lit("0".to_string())));
113 assert_eq!(b.emit_expr(&expr), "${arr['0']}");
114 }
115 #[test]
116 pub(super) fn test_bash_expr_array_len() {
117 let b = backend();
118 let expr = BashExpr::ArrayLen("items".to_string());
119 assert_eq!(b.emit_expr(&expr), "${#items[@]}");
120 }
121 #[test]
122 pub(super) fn test_bash_expr_array_all() {
123 let b = backend();
124 let expr = BashExpr::ArrayAll("items".to_string());
125 assert_eq!(b.emit_expr(&expr), "\"${items[@]}\"");
126 }
127 #[test]
128 pub(super) fn test_bash_expr_default() {
129 let b = backend();
130 let expr = BashExpr::Default(
131 "NAME".to_string(),
132 Box::new(BashExpr::Lit("world".to_string())),
133 );
134 assert_eq!(b.emit_expr(&expr), "${NAME:-'world'}");
135 }
136 #[test]
137 pub(super) fn test_bash_expr_string_len() {
138 let b = backend();
139 let expr = BashExpr::StringLen("msg".to_string());
140 assert_eq!(b.emit_expr(&expr), "${#msg}");
141 }
142 #[test]
143 pub(super) fn test_bash_expr_strip_prefix() {
144 let b = backend();
145 let expr = BashExpr::StripPrefix("path".to_string(), "*/".to_string());
146 assert_eq!(b.emit_expr(&expr), "${path#*/}");
147 }
148 #[test]
149 pub(super) fn test_bash_expr_strip_suffix() {
150 let b = backend();
151 let expr = BashExpr::StripSuffix("file".to_string(), ".txt".to_string());
152 assert_eq!(b.emit_expr(&expr), "${file%.txt}");
153 }
154 #[test]
155 pub(super) fn test_bash_expr_uppercase() {
156 let b = backend();
157 let expr = BashExpr::UpperCase("word".to_string());
158 assert_eq!(b.emit_expr(&expr), "${word^^}");
159 }
160 #[test]
161 pub(super) fn test_bash_expr_lowercase() {
162 let b = backend();
163 let expr = BashExpr::LowerCase("word".to_string());
164 assert_eq!(b.emit_expr(&expr), "${word,,}");
165 }
166 #[test]
167 pub(super) fn test_bash_expr_special_vars() {
168 let b = backend();
169 assert_eq!(b.emit_expr(&BashExpr::LastStatus), "$?");
170 assert_eq!(b.emit_expr(&BashExpr::ShellPid), "$$");
171 assert_eq!(b.emit_expr(&BashExpr::ScriptName), "$0");
172 assert_eq!(b.emit_expr(&BashExpr::AllArgs), "\"$@\"");
173 assert_eq!(b.emit_expr(&BashExpr::ArgCount), "$#");
174 assert_eq!(b.emit_expr(&BashExpr::Positional(1)), "$1");
175 }
176 #[test]
177 pub(super) fn test_bash_expr_substring() {
178 let b = backend();
179 let e1 = BashExpr::Substring("s".to_string(), 2, None);
180 let e2 = BashExpr::Substring("s".to_string(), 2, Some(5));
181 assert_eq!(b.emit_expr(&e1), "${s:2}");
182 assert_eq!(b.emit_expr(&e2), "${s:2:5}");
183 }
184 #[test]
185 pub(super) fn test_bash_expr_concat() {
186 let b = backend();
187 let expr = BashExpr::Concat(
188 Box::new(BashExpr::DQuoted("hello ".to_string())),
189 Box::new(BashExpr::Var(BashVar::Local("name".to_string()))),
190 );
191 assert_eq!(b.emit_expr(&expr), "\"hello \"${name}");
192 }
193 #[test]
194 pub(super) fn test_bash_condition_file_exists() {
195 let b = backend();
196 let cond = BashCondition::FileExists(BashExpr::DQuoted("/etc/passwd".to_string()));
197 assert_eq!(b.emit_condition(&cond), "[[ -e \"/etc/passwd\" ]]");
198 }
199 #[test]
200 pub(super) fn test_bash_condition_str_eq() {
201 let b = backend();
202 let cond = BashCondition::StrEq(
203 BashExpr::Var(BashVar::Local("a".to_string())),
204 BashExpr::Lit("yes".to_string()),
205 );
206 assert_eq!(b.emit_condition(&cond), "[[ ${a} == 'yes' ]]");
207 }
208 #[test]
209 pub(super) fn test_bash_condition_arith_lt() {
210 let b = backend();
211 let cond = BashCondition::ArithLt("count".to_string(), "10".to_string());
212 assert_eq!(b.emit_condition(&cond), "(( count < 10 ))");
213 }
214 #[test]
215 pub(super) fn test_bash_condition_not() {
216 let b = backend();
217 let inner = BashCondition::IsFile(BashExpr::Lit("config.txt".to_string()));
218 let cond = BashCondition::Not(Box::new(inner));
219 assert_eq!(b.emit_condition(&cond), "! [[ -f 'config.txt' ]]");
220 }
221 #[test]
222 pub(super) fn test_mangle_simple() {
223 let b = backend();
224 assert_eq!(b.mangle_name("my_func"), "my_func");
225 assert_eq!(b.mangle_name("MyFunc"), "MyFunc");
226 }
227 #[test]
228 pub(super) fn test_mangle_dot_separator() {
229 let b = backend();
230 assert_eq!(b.mangle_name("Nat.add"), "Nat__add");
231 }
232 #[test]
233 pub(super) fn test_mangle_colon_separator() {
234 let b = backend();
235 assert_eq!(b.mangle_name("List::map"), "List__map");
236 }
237 #[test]
238 pub(super) fn test_mangle_leading_digit() {
239 let b = backend();
240 let result = b.mangle_name("3foo");
241 assert!(
242 result.starts_with('_'),
243 "expected _ prefix, got: {}",
244 result
245 );
246 assert!(result.contains("foo"));
247 }
248 #[test]
249 pub(super) fn test_mangle_reserved_builtin() {
250 let b = backend();
251 assert_eq!(b.mangle_name("echo"), "echo__ox");
252 assert_eq!(b.mangle_name("read"), "read__ox");
253 assert_eq!(b.mangle_name("local"), "local__ox");
254 }
255 #[test]
256 pub(super) fn test_emit_simple_function() {
257 let b = backend();
258 let func = BashFunction::new("greet", vec!["echo \"Hello, $1!\"".to_string()]);
259 let code = b.emit_function(&func);
260 assert!(code.contains("greet() {"), "got: {}", code);
261 assert!(code.contains("echo \"Hello, $1!\""), "got: {}", code);
262 assert!(code.contains("}\n"), "got: {}", code);
263 }
264 #[test]
265 pub(super) fn test_emit_function_with_locals() {
266 let b = backend();
267 let func = BashFunction::with_locals(
268 "add",
269 vec!["a=$1".to_string(), "b=$2".to_string()],
270 vec!["echo $(( a + b ))".to_string()],
271 );
272 let code = b.emit_function(&func);
273 assert!(code.contains("add() {"));
274 assert!(code.contains("local a=$1"));
275 assert!(code.contains("local b=$2"));
276 assert!(code.contains("echo $(( a + b ))"));
277 }
278 #[test]
279 pub(super) fn test_emit_function_with_description() {
280 let b = backend();
281 let mut func = BashFunction::new("helper", vec!["true".to_string()]);
282 func.description = Some("This is a helper function".to_string());
283 let code = b.emit_function(&func);
284 assert!(code.contains("# This is a helper function"));
285 }
286 #[test]
287 pub(super) fn test_emit_heredoc() {
288 let b = backend();
289 let hd = BashHereDoc::new("EOF", vec!["line1".to_string(), "line2".to_string()]);
290 let code = b.emit_heredoc(&hd);
291 assert!(code.contains("<<EOF"), "got: {}", code);
292 assert!(code.contains("line1"));
293 assert!(code.contains("line2"));
294 assert!(code.contains("EOF"));
295 }
296 #[test]
297 pub(super) fn test_emit_heredoc_no_expand() {
298 let hd = BashHereDoc {
299 delimiter: "EOF".to_string(),
300 strip_tabs: false,
301 no_expand: true,
302 content: vec!["$literal".to_string()],
303 };
304 let b = backend();
305 let code = b.emit_heredoc(&hd);
306 assert!(code.contains("<<'EOF'"), "got: {}", code);
307 }
308 #[test]
309 pub(super) fn test_emit_script_shebang() {
310 let b = backend();
311 let script = BashScript::new();
312 let code = b.emit_script(&script);
313 assert!(code.starts_with("#!/usr/bin/env bash\n"), "got: {}", code);
314 }
315 #[test]
316 pub(super) fn test_emit_script_set_flags() {
317 let b = backend();
318 let script = BashScript::new();
319 let code = b.emit_script(&script);
320 assert!(code.contains("set -euo pipefail"), "got: {}", code);
321 }
322 #[test]
323 pub(super) fn test_emit_script_with_globals() {
324 let b = backend();
325 let mut script = BashScript::new();
326 script
327 .globals
328 .push(("VERSION".to_string(), "1.0.0".to_string()));
329 let code = b.emit_script(&script);
330 assert!(code.contains("readonly VERSION=1.0.0"), "got: {}", code);
331 }
332 #[test]
333 pub(super) fn test_emit_script_with_functions() {
334 let b = backend();
335 let mut script = BashScript::new();
336 script.functions.push(BashFunction::new(
337 "main",
338 vec!["echo 'running'".to_string()],
339 ));
340 let code = b.emit_script(&script);
341 assert!(code.contains("main() {"));
342 assert!(code.contains("echo 'running'"));
343 }
344 #[test]
345 pub(super) fn test_emit_script_with_trap() {
346 let b = backend();
347 let mut script = BashScript::new();
348 script
349 .traps
350 .push(("EXIT".to_string(), "cleanup".to_string()));
351 let code = b.emit_script(&script);
352 assert!(code.contains("trap 'cleanup' EXIT"), "got: {}", code);
353 }
354 #[test]
355 pub(super) fn test_emit_script_full() {
356 let b = backend();
357 let mut script = BashScript::new();
358 script
359 .globals
360 .push(("PROG".to_string(), "oxilean".to_string()));
361 script.functions.push(BashFunction::new(
362 "usage",
363 vec!["echo \"Usage: $PROG [options]\"".to_string()],
364 ));
365 script.main.push("usage".to_string());
366 let code = b.emit_script(&script);
367 assert!(code.contains("#!/usr/bin/env bash"));
368 assert!(code.contains("readonly PROG=oxilean"));
369 assert!(code.contains("usage() {"));
370 assert!(code.contains("usage"));
371 }
372 #[test]
373 pub(super) fn test_emit_array_assign() {
374 let b = backend();
375 let elems = vec![
376 BashExpr::Lit("a".to_string()),
377 BashExpr::Lit("b".to_string()),
378 BashExpr::Lit("c".to_string()),
379 ];
380 let code = b.emit_array_assign("items", &elems);
381 assert_eq!(code, "items=('a' 'b' 'c')");
382 }
383 #[test]
384 pub(super) fn test_emit_assoc_array_assign() {
385 let b = backend();
386 let pairs = vec![
387 ("key1".to_string(), "val1".to_string()),
388 ("key2".to_string(), "val2".to_string()),
389 ];
390 let code = b.emit_assoc_array_assign("opts", &pairs);
391 assert!(code.contains("declare -A opts"), "got: {}", code);
392 assert!(code.contains("opts[key1]=val1"), "got: {}", code);
393 assert!(code.contains("opts[key2]=val2"), "got: {}", code);
394 }
395 #[test]
396 pub(super) fn test_emit_if() {
397 let b = backend();
398 let cond = BashCondition::StrEq(
399 BashExpr::Var(BashVar::Local("x".to_string())),
400 BashExpr::Lit("yes".to_string()),
401 );
402 let code = b.emit_if(&cond, &["echo 'yes'"], Some(&["echo 'no'"]));
403 assert!(
404 code.contains("if [[ ${x} == 'yes' ]]; then"),
405 "got: {}",
406 code
407 );
408 assert!(code.contains("echo 'yes'"), "got: {}", code);
409 assert!(code.contains("else"), "got: {}", code);
410 assert!(code.contains("echo 'no'"), "got: {}", code);
411 assert!(code.contains("fi"), "got: {}", code);
412 }
413 #[test]
414 pub(super) fn test_emit_for_in() {
415 let b = backend();
416 let items = vec![
417 BashExpr::Lit("a".to_string()),
418 BashExpr::Lit("b".to_string()),
419 ];
420 let code = b.emit_for_in("item", &items, &["echo $item"]);
421 assert!(code.contains("for item in 'a' 'b'; do"), "got: {}", code);
422 assert!(code.contains("echo $item"), "got: {}", code);
423 assert!(code.contains("done"), "got: {}", code);
424 }
425 #[test]
426 pub(super) fn test_emit_while() {
427 let b = backend();
428 let cond = BashCondition::ArithLt("i".to_string(), "10".to_string());
429 let code = b.emit_while(&cond, &["echo $i", "(( i++ ))"]);
430 assert!(code.contains("while (( i < 10 )); do"), "got: {}", code);
431 assert!(code.contains("echo $i"), "got: {}", code);
432 assert!(code.contains("done"), "got: {}", code);
433 }
434 #[test]
435 pub(super) fn test_emit_case() {
436 let b = backend();
437 let expr = BashExpr::Var(BashVar::Local("cmd".to_string()));
438 let arms = vec![
439 ("start", vec!["do_start"]),
440 ("stop", vec!["do_stop"]),
441 ("*", vec!["echo 'unknown'"]),
442 ];
443 let code = b.emit_case(&expr, &arms);
444 assert!(code.contains("case ${cmd} in"), "got: {}", code);
445 assert!(code.contains("start)"), "got: {}", code);
446 assert!(code.contains("do_start"), "got: {}", code);
447 assert!(code.contains("esac"), "got: {}", code);
448 }
449 #[test]
450 pub(super) fn test_lenient_script_no_strict() {
451 let b = backend();
452 let script = BashScript::lenient();
453 let code = b.emit_script(&script);
454 assert!(code.starts_with("#!/usr/bin/env bash"));
455 assert!(
456 !code.contains("set -euo pipefail"),
457 "lenient should not have strict mode"
458 );
459 }
460 #[test]
461 pub(super) fn test_compact_backend_indent() {
462 let b = BashBackend::compact();
463 let func = BashFunction::new("f", vec!["echo hi".to_string()]);
464 let code = b.emit_function(&func);
465 assert!(code.contains(" echo hi"), "got: {}", code);
466 }
467}
468#[allow(dead_code)]
470pub mod bash_colors {
471 pub const RESET: &str = "\\033[0m";
472 pub const BOLD: &str = "\\033[1m";
473 pub const DIM: &str = "\\033[2m";
474 pub const ITALIC: &str = "\\033[3m";
475 pub const UNDERLINE: &str = "\\033[4m";
476 pub const BLINK: &str = "\\033[5m";
477 pub const REVERSE: &str = "\\033[7m";
478 pub const HIDDEN: &str = "\\033[8m";
479 pub const STRIKE: &str = "\\033[9m";
480 pub const BLACK: &str = "\\033[30m";
481 pub const RED: &str = "\\033[31m";
482 pub const GREEN: &str = "\\033[32m";
483 pub const YELLOW: &str = "\\033[33m";
484 pub const BLUE: &str = "\\033[34m";
485 pub const MAGENTA: &str = "\\033[35m";
486 pub const CYAN: &str = "\\033[36m";
487 pub const WHITE: &str = "\\033[37m";
488 pub const BRIGHT_BLACK: &str = "\\033[90m";
489 pub const BRIGHT_RED: &str = "\\033[91m";
490 pub const BRIGHT_GREEN: &str = "\\033[92m";
491 pub const BRIGHT_YELLOW: &str = "\\033[93m";
492 pub const BRIGHT_BLUE: &str = "\\033[94m";
493 pub const BRIGHT_MAGENTA: &str = "\\033[95m";
494 pub const BRIGHT_CYAN: &str = "\\033[96m";
495 pub const BRIGHT_WHITE: &str = "\\033[97m";
496 pub const BG_RED: &str = "\\033[41m";
497 pub const BG_GREEN: &str = "\\033[42m";
498 pub const BG_YELLOW: &str = "\\033[43m";
499 pub const BG_BLUE: &str = "\\033[44m";
500 pub const BG_MAGENTA: &str = "\\033[45m";
501 pub const BG_CYAN: &str = "\\033[46m";
502}
503#[allow(dead_code)]
505pub fn emit_bash_split(str_var: &str, delim: &str, arr_var: &str) -> std::string::String {
506 format!(
507 "IFS='{}' read -r -a {} <<< \"${{{}}}\"\n",
508 delim, arr_var, str_var
509 )
510}
511#[allow(dead_code)]
513pub fn emit_bash_join(arr_var: &str, delim: &str, result_var: &str) -> std::string::String {
514 format!(
515 "local IFS='{}'; {}=\"${{{}[*]}}\"\n",
516 delim, result_var, arr_var
517 )
518}
519#[allow(dead_code)]
521pub fn emit_bash_trim(var: &str, result_var: &str) -> std::string::String {
522 format!(
523 "{var}=\"${{${{{var}}}##*( )}}\" # trim leading\n{result_var}=\"${{${{{var}}}%%*( )}}\" # trim trailing\n",
524 var = var, result_var = result_var
525 )
526}
527#[allow(dead_code)]
529pub fn emit_bash_url_encode(var: &str, result_var: &str) -> std::string::String {
530 format!(
531 "{result}=$(printf '%s' \"${{{var}}}\" | jq -Rr @uri 2>/dev/null || python3 -c \"import sys,urllib.parse; print(urllib.parse.quote(sys.stdin.read().rstrip()))\" <<< \"${{{var}}}\")\n",
532 var = var, result = result_var
533 )
534}
535#[allow(dead_code)]
537pub fn emit_bash_require_cmd(cmd: &str) -> std::string::String {
538 format!(
539 "command -v {} &>/dev/null || {{ echo \"Error: '{}' not found in PATH\" >&2; exit 1; }}\n",
540 cmd, cmd
541 )
542}
543#[allow(dead_code)]
545pub fn emit_bash_source_env(file_path: &str, required: bool) -> std::string::String {
546 if required {
547 format!(
548 "if [[ -f \"{path}\" ]]; then\n # shellcheck source=/dev/null\n source \"{path}\"\nelse\n echo \"Error: config file not found: {path}\" >&2\n exit 1\nfi\n",
549 path = file_path
550 )
551 } else {
552 format!(
553 "if [[ -f \"{path}\" ]]; then\n # shellcheck source=/dev/null\n source \"{path}\"\nfi\n",
554 path = file_path
555 )
556 }
557}
558#[allow(dead_code)]
560pub fn emit_bash_read_env(file_path: &str) -> std::string::String {
561 format!(
562 "while IFS='=' read -r _key _val || [[ -n \"$_key\" ]]; do\n [[ \"$_key\" =~ ^#.*$ || -z \"$_key\" ]] && continue\n export \"$_key\"=\"$_val\"\ndone < \"{}\"\n",
563 file_path
564 )
565}
566#[allow(dead_code)]
568pub fn emit_bash_lock(lock_file: &str, lock_fd_var: &str) -> std::string::String {
569 format!(
570 "exec {fd}<>\"{file}\"\nflock -n ${fd} || {{ echo \"Another instance is running\" >&2; exit 1; }}\n",
571 fd = lock_fd_var, file = lock_file
572 )
573}
574#[allow(dead_code)]
576pub fn emit_bash_unlock(lock_fd_var: &str) -> std::string::String {
577 format!("flock -u ${}\n", lock_fd_var)
578}
579#[allow(dead_code)]
581pub fn emit_bash_retry_fn(max_attempts: u8, delay_secs: u8) -> std::string::String {
582 format!(
583 "retry() {{\n local _attempt=1\n until \"$@\"; do\n _attempt=$(( _attempt + 1 ))\n if [[ $_attempt -gt {max} ]]; then\n echo \"Command failed after {max} attempts\" >&2\n return 1\n fi\n echo \"Attempt $_attempt of {max} failed, retrying in {delay}s...\" >&2\n sleep {delay}\n done\n}}\n",
584 max = max_attempts, delay = delay_secs
585 )
586}
587#[allow(dead_code)]
589pub fn emit_bash_progress_bar(width: usize) -> std::string::String {
590 format!(
591 "progress_bar() {{\n local current=$1 total=$2 width={w}\n local pct=$(( current * 100 / total ))\n local filled=$(( pct * width / 100 ))\n local bar; bar=$(printf '%0.s#' $(seq 1 $filled))\n local empty; empty=$(printf '%0.s-' $(seq 1 $(( width - filled ))))\n printf \"\\r[%s%s] %d%%\" \"$bar\" \"$empty\" \"$pct\"\n [[ $current -eq $total ]] && echo\n}}\n",
592 w = width
593 )
594}
595#[allow(dead_code)]
597pub fn emit_bash_http_get(
598 url_var: &str,
599 result_var: &str,
600 _timeout_secs: u8,
601) -> std::string::String {
602 format!(
603 "{result}=$(curl -fsSL --max-time {timeout} \"${{{url}}}\" 2>/dev/null)\nif [[ $? -ne 0 ]]; then\n echo \"HTTP GET failed for ${{url}}\" >&2\n exit 1\nfi\n",
604 result = result_var, timeout = _timeout_secs, url = url_var
605 )
606}
607#[allow(dead_code)]
609pub fn emit_bash_http_post_json(
610 url_var: &str,
611 body_var: &str,
612 result_var: &str,
613 _timeout_secs: u8,
614) -> std::string::String {
615 format!(
616 "{result}=$(curl -fsSL --max-time {timeout} -X POST -H 'Content-Type: application/json' -d \"${{{body}}}\" \"${{{url}}}\" 2>/dev/null)\n",
617 result = result_var, timeout = _timeout_secs, body = body_var, url = url_var
618 )
619}
620#[allow(dead_code)]
622pub fn emit_bash_for_arith(
623 init: &str,
624 cond: &str,
625 incr: &str,
626 body: &[&str],
627 indent: &str,
628) -> std::string::String {
629 let mut out = format!("for (( {}; {}; {} )); do\n", init, cond, incr);
630 for stmt in body {
631 out.push_str(&format!("{}{}\n", indent, stmt));
632 }
633 out.push_str("done\n");
634 out
635}
636#[allow(dead_code)]
638pub fn emit_bash_until(cond: &BashCondition, body: &[&str], indent: &str) -> std::string::String {
639 let mut out = format!("until {}; do\n", cond);
640 for stmt in body {
641 out.push_str(&format!("{}{}\n", indent, stmt));
642 }
643 out.push_str("done\n");
644 out
645}
646#[allow(dead_code)]
648pub fn emit_bash_select_menu(
649 var: &str,
650 choices: &[&str],
651 body: &[&str],
652 indent: &str,
653) -> std::string::String {
654 let choices_str: Vec<std::string::String> =
655 choices.iter().map(|c| format!("\"{}\"", c)).collect();
656 let mut out = format!(
657 "PS3=\"Select: \"\nselect {} in {}; do\n",
658 var,
659 choices_str.join(" ")
660 );
661 for stmt in body {
662 out.push_str(&format!("{}{}\n", indent, stmt));
663 }
664 out.push_str("done\n");
665 out
666}
667#[cfg(test)]
668mod bash_extended_tests {
669 use super::*;
670 #[test]
671 pub(super) fn test_heredoc_emit() {
672 let h = BashHeredoc::new("EOF")
673 .line("Hello, World!")
674 .line("Second line");
675 let out = h.emit();
676 assert!(out.contains("<<EOF"), "missing heredoc tag: {}", out);
677 assert!(out.contains("Hello"), "missing content: {}", out);
678 }
679 #[test]
680 pub(super) fn test_heredoc_quoted() {
681 let h = BashHeredoc::new("SCRIPT").quoted().line("$var");
682 let out = h.emit();
683 assert!(out.contains("<<'SCRIPT'"), "missing quoted tag: {}", out);
684 }
685 #[test]
686 pub(super) fn test_trap_emit() {
687 let t = BashTrap::on_exit("cleanup");
688 assert_eq!(t.emit(), "trap 'cleanup' EXIT");
689 let t2 = BashTrap::on_err("handle_error");
690 assert_eq!(t2.emit(), "trap 'handle_error' ERR");
691 let t3 = BashTrap::reset("INT");
692 assert!(t3.emit().contains("INT"), "missing signal: {}", t3.emit());
693 }
694 #[test]
695 pub(super) fn test_log_level_ordering() {
696 assert!(BashLogLevel::Debug < BashLogLevel::Info);
697 assert!(BashLogLevel::Info < BashLogLevel::Warn);
698 assert!(BashLogLevel::Warn < BashLogLevel::Error);
699 assert!(BashLogLevel::Error < BashLogLevel::Fatal);
700 assert!(BashLogLevel::Error.is_stderr());
701 assert!(BashLogLevel::Fatal.is_stderr());
702 assert!(!BashLogLevel::Info.is_stderr());
703 }
704 #[test]
705 pub(super) fn test_logger_emit_framework() {
706 let logger = BashLogger::new()
707 .with_timestamps()
708 .with_color()
709 .with_min_level(BashLogLevel::Debug)
710 .with_log_file("/var/log/app.log");
711 let code = logger.emit_framework();
712 assert!(code.contains("_log()"), "missing _log: {}", code);
713 assert!(code.contains("log_debug"), "missing log_debug: {}", code);
714 assert!(code.contains("log_fatal"), "missing log_fatal: {}", code);
715 assert!(
716 code.contains("/var/log/app.log"),
717 "missing log file: {}",
718 code
719 );
720 }
721 #[test]
722 pub(super) fn test_cli_option_flag() {
723 let opt = BashCliOption::flag("verbose", Some('v'), "verbose", "Enable verbose output");
724 assert!(!opt.has_arg);
725 assert_eq!(opt.default, Some("false".to_string()));
726 assert!(!opt.required);
727 }
728 #[test]
729 pub(super) fn test_arg_parser_emit() {
730 let parser = BashArgParser::new("myapp", "A test application")
731 .add_option(BashCliOption::flag(
732 "verbose",
733 Some('v'),
734 "verbose",
735 "Verbose mode",
736 ))
737 .add_option(
738 BashCliOption::arg(
739 "output",
740 Some('o'),
741 "output",
742 Some("/tmp/out"),
743 "Output file",
744 )
745 .required(),
746 )
747 .add_positional("input_file");
748 let usage = parser.emit_usage();
749 assert!(usage.contains("usage()"), "missing usage fn: {}", usage);
750 assert!(usage.contains("--verbose"), "missing verbose: {}", usage);
751 assert!(usage.contains("--output"), "missing output: {}", usage);
752 let parse = parser.emit_parse_block();
753 assert!(
754 parse.contains("VERBOSE=false"),
755 "missing default: {}",
756 parse
757 );
758 assert!(
759 parse.contains("--verbose"),
760 "missing verbose case: {}",
761 parse
762 );
763 assert!(
764 parse.contains("OUTPUT is required"),
765 "missing required check: {}",
766 parse
767 );
768 }
769 #[test]
770 pub(super) fn test_job_manager_emit() {
771 let jm = BashJobManager::new(4).with_pids_var("JOB_PIDS");
772 let code = jm.emit_framework();
773 assert!(code.contains("_MAX_JOBS=4"), "missing max_jobs: {}", code);
774 assert!(code.contains("JOB_PIDS"), "missing pids var: {}", code);
775 assert!(code.contains("run_job()"), "missing run_job: {}", code);
776 assert!(code.contains("_wait_jobs()"), "missing wait_jobs: {}", code);
777 }
778 #[test]
779 pub(super) fn test_template_render() {
780 let tmpl = BashTemplate::new("Hello, {{NAME}}! You are {{AGE}} years old.")
781 .set("NAME", "Alice")
782 .set("AGE", "30");
783 let rendered = tmpl.render();
784 assert_eq!(rendered, "Hello, Alice! You are 30 years old.");
785 }
786 #[test]
787 pub(super) fn test_split_join_emit() {
788 let split = emit_bash_split("INPUT", ":", "PARTS");
789 assert!(split.contains("IFS=':'"), "missing IFS: {}", split);
790 assert!(split.contains("PARTS"), "missing arr: {}", split);
791 let join = emit_bash_join("PARTS", ",", "RESULT");
792 assert!(join.contains("IFS=','"), "missing IFS: {}", join);
793 }
794 #[test]
795 pub(super) fn test_retry_fn_emit() {
796 let code = emit_bash_retry_fn(3, 5);
797 assert!(code.contains("retry()"), "missing fn: {}", code);
798 assert!(code.contains("3"), "missing max: {}", code);
799 assert!(code.contains("sleep 5"), "missing sleep: {}", code);
800 }
801 #[test]
802 pub(super) fn test_progress_bar_emit() {
803 let code = emit_bash_progress_bar(50);
804 assert!(code.contains("progress_bar()"), "missing fn: {}", code);
805 assert!(code.contains("50"), "missing width: {}", code);
806 }
807 #[test]
808 pub(super) fn test_source_env_emit() {
809 let required = emit_bash_source_env("/etc/app/config.conf", true);
810 assert!(required.contains("source"), "missing source: {}", required);
811 assert!(required.contains("exit 1"), "missing exit: {}", required);
812 let optional = emit_bash_source_env("/etc/app/config.conf", false);
813 assert!(optional.contains("source"), "missing source: {}", optional);
814 assert!(
815 !optional.contains("exit 1"),
816 "should not exit: {}",
817 optional
818 );
819 }
820 #[test]
821 pub(super) fn test_lock_unlock_emit() {
822 let lock = emit_bash_lock("/var/run/app.lock", "9");
823 assert!(lock.contains("flock"), "missing flock: {}", lock);
824 let unlock = emit_bash_unlock("9");
825 assert!(unlock.contains("flock -u"), "missing unlock: {}", unlock);
826 }
827 #[test]
828 pub(super) fn test_for_arith_emit() {
829 let code = emit_bash_for_arith("i=0", "i<10", "i++", &["echo $i"], " ");
830 assert!(
831 code.contains("for (( i=0; i<10; i++ ))"),
832 "missing for arith: {}",
833 code
834 );
835 assert!(code.contains("echo $i"), "missing body: {}", code);
836 assert!(code.contains("done"), "missing done: {}", code);
837 }
838 #[test]
839 pub(super) fn test_bash_colors_module() {
840 assert!(bash_colors::RED.contains("31m"));
841 assert!(bash_colors::GREEN.contains("32m"));
842 assert!(bash_colors::BLUE.contains("34m"));
843 assert!(bash_colors::RESET.contains("0m"));
844 assert!(bash_colors::BOLD.contains("1m"));
845 }
846 #[test]
847 pub(super) fn test_require_cmd_emit() {
848 let code = emit_bash_require_cmd("jq");
849 assert!(
850 code.contains("command -v jq"),
851 "missing command check: {}",
852 code
853 );
854 assert!(code.contains("exit 1"), "missing exit: {}", code);
855 }
856 #[test]
857 pub(super) fn test_http_get_emit() {
858 let code = emit_bash_http_get("URL", "RESPONSE", 30);
859 assert!(code.contains("curl"), "missing curl: {}", code);
860 assert!(code.contains("RESPONSE"), "missing result var: {}", code);
861 assert!(code.contains("30"), "missing timeout: {}", code);
862 }
863}
864#[cfg(test)]
865mod Bash_infra_tests {
866 use super::*;
867 #[test]
868 pub(super) fn test_pass_config() {
869 let config = BashPassConfig::new("test_pass", BashPassPhase::Transformation);
870 assert!(config.enabled);
871 assert!(config.phase.is_modifying());
872 assert_eq!(config.phase.name(), "transformation");
873 }
874 #[test]
875 pub(super) fn test_pass_stats() {
876 let mut stats = BashPassStats::new();
877 stats.record_run(10, 100, 3);
878 stats.record_run(20, 200, 5);
879 assert_eq!(stats.total_runs, 2);
880 assert!((stats.average_changes_per_run() - 15.0).abs() < 0.01);
881 assert!((stats.success_rate() - 1.0).abs() < 0.01);
882 let s = stats.format_summary();
883 assert!(s.contains("Runs: 2/2"));
884 }
885 #[test]
886 pub(super) fn test_pass_registry() {
887 let mut reg = BashPassRegistry::new();
888 reg.register(BashPassConfig::new("pass_a", BashPassPhase::Analysis));
889 reg.register(BashPassConfig::new("pass_b", BashPassPhase::Transformation).disabled());
890 assert_eq!(reg.total_passes(), 2);
891 assert_eq!(reg.enabled_count(), 1);
892 reg.update_stats("pass_a", 5, 50, 2);
893 let stats = reg.get_stats("pass_a").expect("stats should exist");
894 assert_eq!(stats.total_changes, 5);
895 }
896 #[test]
897 pub(super) fn test_analysis_cache() {
898 let mut cache = BashAnalysisCache::new(10);
899 cache.insert("key1".to_string(), vec![1, 2, 3]);
900 assert!(cache.get("key1").is_some());
901 assert!(cache.get("key2").is_none());
902 assert!((cache.hit_rate() - 0.5).abs() < 0.01);
903 cache.invalidate("key1");
904 assert!(!cache.entries["key1"].valid);
905 assert_eq!(cache.size(), 1);
906 }
907 #[test]
908 pub(super) fn test_worklist() {
909 let mut wl = BashWorklist::new();
910 assert!(wl.push(1));
911 assert!(wl.push(2));
912 assert!(!wl.push(1));
913 assert_eq!(wl.len(), 2);
914 assert_eq!(wl.pop(), Some(1));
915 assert!(!wl.contains(1));
916 assert!(wl.contains(2));
917 }
918 #[test]
919 pub(super) fn test_dominator_tree() {
920 let mut dt = BashDominatorTree::new(5);
921 dt.set_idom(1, 0);
922 dt.set_idom(2, 0);
923 dt.set_idom(3, 1);
924 assert!(dt.dominates(0, 3));
925 assert!(dt.dominates(1, 3));
926 assert!(!dt.dominates(2, 3));
927 assert!(dt.dominates(3, 3));
928 }
929 #[test]
930 pub(super) fn test_liveness() {
931 let mut liveness = BashLivenessInfo::new(3);
932 liveness.add_def(0, 1);
933 liveness.add_use(1, 1);
934 assert!(liveness.defs[0].contains(&1));
935 assert!(liveness.uses[1].contains(&1));
936 }
937 #[test]
938 pub(super) fn test_constant_folding() {
939 assert_eq!(BashConstantFoldingHelper::fold_add_i64(3, 4), Some(7));
940 assert_eq!(BashConstantFoldingHelper::fold_div_i64(10, 0), None);
941 assert_eq!(BashConstantFoldingHelper::fold_div_i64(10, 2), Some(5));
942 assert_eq!(
943 BashConstantFoldingHelper::fold_bitand_i64(0b1100, 0b1010),
944 0b1000
945 );
946 assert_eq!(BashConstantFoldingHelper::fold_bitnot_i64(0), -1);
947 }
948 #[test]
949 pub(super) fn test_dep_graph() {
950 let mut g = BashDepGraph::new();
951 g.add_dep(1, 2);
952 g.add_dep(2, 3);
953 g.add_dep(1, 3);
954 assert_eq!(g.dependencies_of(2), vec![1]);
955 let topo = g.topological_sort();
956 assert_eq!(topo.len(), 3);
957 assert!(!g.has_cycle());
958 let pos: std::collections::HashMap<u32, usize> =
959 topo.iter().enumerate().map(|(i, &n)| (n, i)).collect();
960 assert!(pos[&1] < pos[&2]);
961 assert!(pos[&1] < pos[&3]);
962 assert!(pos[&2] < pos[&3]);
963 }
964}